はじめに
この記事ではSpring BootでHello Worldアプリを作成し、CDKで構築したECS+ALB環境で動作させたいと思います。
前提
- VPC・Subnetは既に作成されている
- 動作環境はM1 MacBook
目次
リソース構成
HelloWorldアプリを作成
spring initializrで雛形を作成します
spring initializr
記事作成時は以下を設定しgenerateしました
Project:Gradle-Groovy
Artifact:HelloWorld
Spring Boot:3.3
Packaging:Jar
Java:21
Dependencies:Spring Web
HelloWorldを返すControllerを作成します
main/java/com/example/HelloWorld以下にIndexController.javaを作成します
package com.example.HelloWorld;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/")
public class IndexController {
@RequestMapping
public String index() {
return "HelloWorld";
}
}
Dockerfileを作成します
FROM openjdk:21
EXPOSE 8080
RUN microdnf install findutils
WORKDIR /var/www/html/server
COPY .. /var/www/html/server
RUN ./gradlew build
ENTRYPOINT ["java", "-jar" , "build/libs/HelloWorld-0.0.1-SNAPSHOT.jar"]
動作確認
以下のコマンドを実行します
curl
実行後にHelloWorld
が出力されればOKです
% docker build -f app/Dockerfile -t hello-world .
% docker run --rm --name hello-world-container -p 8080:8080 -it hello-world
% curl http://localhost:8080
CDKでインフラを定義する
CDKのテンプレートを作成します
以下のコマンドを実行すると、typescriptファイルが作成されます
% npx cdk init --language=typescript
stackを定義します
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
export class HelloWorldStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const projectName = "hello-world";
const vpcId = "{vpcのid}";
const availabilityZones = ["{vpcで定義されているavailabilityZone}"];
const publicSubnetIds = ["{vpcで定義されているpublicSubnetId}"];
const publicSubnetRouteTableIds = ["{public subnetに紐づくRouteTableId}"];
// Create ECR Repository
const ecrRepository = new ecr.Repository(this, "EcrRepo", {
repositoryName: `${projectName}-ecr-repo`,
removalPolicy: cdk.RemovalPolicy.DESTROY,
emptyOnDelete: true,
});
// find VPC
const vpc = ec2.Vpc.fromVpcAttributes(this, "Vpc", {
vpcId: vpcId,
availabilityZones: availabilityZones,
publicSubnetIds: publicSubnetIds,
publicSubnetRouteTableIds: publicSubnetRouteTableIds
});
// Create ECS Cluster
const cluster = new ecs.Cluster(this, "EcsCluster", {
clusterName: `${projectName}-cluster`,
vpc: vpc,
});
// Create ALB and ECS Fargate Service
const service = new cdk.aws_ecs_patterns.ApplicationLoadBalancedFargateService(
this,
"FargateService",
{
loadBalancerName: `${projectName}-lb`,
publicLoadBalancer: true,
cluster: cluster,
serviceName: `${projectName}-service`,
cpu: 256,
memoryLimitMiB: 512,
assignPublicIp: true,
taskSubnets: {subnetType: ec2.SubnetType.PUBLIC},
taskImageOptions: {
family: `${projectName}-taskdef`,
containerName: `${projectName}-container`,
image: ecs.ContainerImage.fromEcrRepository(ecrRepository, "latest"),
logDriver: new ecs.AwsLogDriver({
streamPrefix: `container`,
}),
containerPort: 8080,
},
runtimePlatform: {
cpuArchitecture: ecs.CpuArchitecture.ARM64
},
}
);
}
}
基本的には
ECSとECRのコンテナ構成をCDKで実装してみたの記事通りに設定しているのですが、何点かハマったところがあるので解説していきます。
VPCの取得が上手くいかなかった
// find VPC
const vpc = ec2.Vpc.fromVpcAttributes(this, "Vpc", {
vpcId: vpcId,
availabilityZones: availabilityZones,
publicSubnetIds: publicSubnetIds,
publicSubnetRouteTableIds: publicSubnetRouteTableIds
});
VPCを取得するコードです
fromVpcAttributes
にはpublicSubnetIds
やpublicSubnetRouteTableIds
が必須ではないですが、publicSubnetIds
がない状態だと、diff実行時にVPCが適切に取得できず以下のエラーを出力する場合があります
Error: There are no 'Public' subnet groups in this VPC. Available types:
また、publicSubnetRouteTableIds
を指定しない場合はrouteTableを認識できていないため以下のwarningが出力されます
[Warning at /CdkStack/Vpc/PublicSubnet1] No routeTableId was provided to the subnet ''. Attempting to read its .routeTable.routeTableId will return null/undefined.
実行環境の指定
runtimePlatform: {
cpuArchitecture: ecs.CpuArchitecture.ARM64
}
実行環境をARM64に設定しています
ECS FargateのデフォルトがX86_64のようなので、M1 MacBookでイメージをビルドしている関係上、そちらに合わせる必要があります
ECS実行時に以下のエラーが出力されていると、上記が原因の可能性があるので、確認してみてください
exec /usr/java/openjdk-21/bin/java: exec format error
portの指定
containerPort: 8080
今回アプリケーションを8080ポートで実行しているので、ALBからECSへの8080ポートを通過可能にする必要があります
設定していなければ、HealthCheckがずっと通らずにECSのdeployが終わらないので、deployがなかなか終わらない時に確認してみてください
CDKを使ったdeploy
deployしていくのですが、初回実行時に注意点があるので、初回実行時と2回目以降で分けて解説していきます
初回実行時
初回実行時にそのままdeployをしてしまうと、ECRにイメージがpushされていないので、ECSがイメージを取得できずエラーになってしまいます
そのため、まずはECRのみを作成→イメージをpush→ECS+ALBをdeployするという手順を踏む必要があります
cdk-ecr-deploymentを使うと、そこも上手くやってくれるようなのですが、今回は上手く動作させることができなかったので別々の手順を踏みます
- ECR以外をコメントアウトしdeployを行います
- ECRにビルドしたイメージをpushします
コンソールでECRへ遷移し「プッシュコマンドの表示」で表示されたコマンドを実行していきます - コメントアウトを外し、deployを行います
- 実行完了後、ALBのDNS名が表示されるので、そちらをcurlコマンドで実行し、HelloWorldと表示されれば動作確認完了です
% cdk deploy
Outputs:
CdkStack.FargateServiceLoadBalancerDNSXXXXXXXX = <ALBのDNS名>
CdkStack.FargateServiceServiceURLXXXXXXXX = <ECSサービスURL>
% curl <ALBのDNS名>
HelloWorld ← 表示されれば動作確認完了
2回目以降
2回目以降はECRにイメージが既に存在するので、先に実行する必要はありませんが、ECSはlatestタグのものを参照するようになっているので、先ほどと同じようにlatestタグでpushしdeployしてしまうと変更がされません
そのためタグをlatestではないimageをpush→ECSの参照先を変える→deployという手順を踏む必要があります
- ECRにタグ名を変えたイメージをpushします
「プッシュコマンドの表示」で表示されたコマンドの、latestを任意の名前に変更し実行します - ECSの参照先を変更します
cdk-stack.ts
image: ecs.ContainerImage.fromEcrRepository(ecrRepository, "latest")
- deployを実行、変更が反映されていれば完了です
まとめ
今回はCDKでECS+ALBの環境を作成してみました
ApplicationLoadBalancedFargateServiceがよしなに必要なものを作成してくれるので、セキュリティグループを1つずつ作成する手間もなく、思ったよりも簡単に作成することができました
cdk-ecr-deploymentを上手く動作させれなかったのでdeployの部分が煩雑になりましたが、上手く動いていればdeployするだけで良いので、次の機会では動作させたいと思います