- Amazon ECS on Fargate 1.4.0
- Java 21
- Spring Boot 3.5
- Terraform 1.13
- ecspresso 2.6
ECSって何?という人は下記のBlackbelt資料を読むと良いでしょう(PDFへの直リンクです)。
また、要点のみに絞るため、ECS関連のTerraformリソースの作成方法も詳細には解説しません。それらを学ぶ場合は下記の本をどうぞ。
- ecspressoいいですよ
- タスク定義に
"readonlyRootFilesystem": true
を設定しましょう - ECS on Fargateの場合、Cloud Native BuildpacksではなくDockerfileでコンテナイメージを作成しましょう
ecspressoは簡単に言えば、ECSのサービスとタスク定義をデプロイしてくれるツールです。
それ以外のクラスター・ロードバランサー・IAMロール・セキュリティグループなどはecspressoの範囲外なので、Terraformなどで作成します。
サービスとタスク定義もTerraformで作ることはできます。しかし、サービス+タスク定義=アプリケーションのデプロイ時にTerraformを触らずに済むのは嬉しいです。
ecspresso.yml
ecspressoの設定ファイルです。
ecspresso.yml
region: ap-northeast-1
cluster: web-app-cluster
service: web-app-service
service_definition: ecs-service-def.json
task_definition: ecs-task-def.json
plugins:
- name: tfstate
config:
url: s3://バケット名/tfstateファイル名
サービス定義
ところどころ {{tfstate ... }}
と書かれている部分は、Terraformで作成したリソースを利用しています。
このJSONはAWS公式ドキュメントに書かれたサービス定義テンプレートに従って記述します。場合によっては不要な設定もあったりするので、精査しながら記述してください。
ecs-service-def.json
{
"serviceName": "web-app-service",
"loadBalancers": [
{
"targetGroupArn": "{{ tfstate `aws_lb_target_group.web_app.arn` }}",
"containerName": "web-app",
"containerPort": 8080
}
],
"desiredCount": 2,
"launchType": "FARGATE",
"platformVersion": "LATEST",
"platformFamily": "LINUX",
"networkConfiguration": {
"awsvpcConfiguration": {
"subnets": [
"{{ tfstate `aws_subnet.app_az1.id` }}",
"{{ tfstate `aws_subnet.app_az2.id` }}"
],
"securityGroups": [
"{{ tfstate `aws_security_group.web_app.id` }}"
],
"assignPublicIp": "DISABLED"
}
},
"healthCheckGracePeriodSeconds": 10,
"schedulingStrategy": "REPLICA",
"enableExecuteCommand": true,
"availabilityZoneRebalancing": "ENABLED"
}
タスク定義
タスク定義のJSONも、AWS公式ドキュメントに書かれたサービス定義テンプレートに従って記述します。場合によっては不要な設定もあったりするので、精査しながら記述してください。
コンテナ内のファイルシステムを悪い人に変更されないよう、"readonlyRootFilesystem": true
としてルートファイルシステムを読み取りのみに設定します(ただしECS Execができなくなるので注意してください)。
しかしそうすると、Spring Boot内のTomcatが必要なディレクトリを/tmpに作成できなくなります。
そこで、タスクにEphemeral Storageというボリュームを別途定義し、それをコンテナ内の/tmpにマウントします。そうすることで、Tomcatが/tmpに書き込みできるようになります。
ecs-task-def.json
{
"family": "web-app-task-definition",
"runtimePlatform": {
"operatingSystemFamily": "LINUX",
"cpuArchitecture": "ARM64"
},
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "512",
"memory": "1024",
"taskRoleArn": "{{ tfstate `aws_iam_role.web_app.arn` }}",
"executionRoleArn": "{{ tfstate `aws_iam_role.web_app_execution.arn` }}",
"networkMode": "awsvpc",
"volumes": [ {
"name": "web-app-ephemeral-volume"
}
],
"containerDefinitions": [
{
"name": "web-app",
"image": "{{ tfstate `aws_ecr_repository.web_app.repository_url` }}:{{ must_env `IMAGE_TAG` }}",
"user": "webappuser", "readonlyRootFilesystem": true, "mountPoints": [
{
"sourceVolume": "web-app-ephemeral-volume", "containerPath": "/tmp", "readOnly": false
}
],
"portMappings": [
{
"containerPort": 8080,
"hostPort": 8080,
"protocol": "tcp"
}
],
"essential": true,
"environment": [
{
"name": "TZ",
"value": "Asia/Tokyo"
},
{
"name": "SPRING_PROFILES_ACTIVE",
"value": "{{ must_env `SPRING_PROFILES_ACTIVE` }}"
}
],
"secrets": [
{
"name": "SPRING_DATASOURCE_URL",
"valueFrom": "{{ tfstate `data.aws_secretsmanager_secret.rds.arn`}}:jdbcUrl::"
},
{
"name": "SPRING_DATASOURCE_USERNAME",
"valueFrom": "{{ tfstate `data.aws_secretsmanager_secret.rds.arn`}}:jdbcUsername::"
},
{
"name": "SPRING_DATASOURCE_PASSWORD",
"valueFrom": "{{ tfstate `data.aws_secretsmanager_secret.rds.arn`}}:jdbcPassword::"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "ap-northeast-1",
"awslogs-group": "{{ tfstate `aws_cloudwatch_log_group.web_app.name` }}",
"awslogs-stream-prefix": ""
}
}
}
]
}
Dockerfileを書く際は色々と注意点があるのですが、最低限以下の点は気をつけましょう。
- ベースイメージの脆弱性が無いか確認する
- TrivyやDocker Hubなどで確認できる
- アプリ実行用のユーザーを作成する
- これをしないと、root権限でコンテナが実行される
細かい注意点はSpring公式ブログを見てください。
加えて、ECS on FargateがEphemeral StorageとマウントするディレクトリをVOLUME
で指定し、かつchown
コマンドでディレクトリの所有者をアプリ実行用ユーザーに変更します。
Dockerfile
FROM amazoncorretto:21-alpine3.22
RUN apk add --no-cache shadow
RUN mkdir /app
RUN groupadd -r webappgroup && useradd -r -s /usr/sbin/nologin -g webappgroup webappuser
WORKDIR /app
VOLUME /tmp
RUN chown -R webappuser:webappgroup /app \
&& chown -R webappuser:webappgroup /tmp
USER webappuser
COPY ./target/web-app.jar app.jar
ENTRYPOINT ["java","-jar","app.jar"]
Cloud Native Buildpacksには、DockerfileのVOLUME
相当の設定が無いようでした(見つけたら教えてください・・・)。なので、Cloud Native Buildpacksは使っていません。
イメージのタグは、個人的にはGitのコミットハッシュにするのが好きです。git rev-parse --short HEAD)
でコミットハッシュを取得できます。
Dockerfileでのビルド・ECRへのプッシュ・ecspressoでのデプロイを、シェルスクリプトにまとめます(アプリのディレクトリ直下にDockerfileやecspresso関連ファイルがある前提です)。
build-push-deploy.sh
export IMAGE_TAG=$(git rev-parse --short HEAD)
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
export SPRING_PROFILES_ACTIVE=development
mvn clean package -Dmaven.test.skip=true
docker buildx build --platform linux/arm64 -t web-app:${IMAGE_TAG} .
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
docker image tag web-app:${IMAGE_TAG} ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/web-app:${IMAGE_TAG}
docker image push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/web-app:${IMAGE_TAG}
ecspresso deploy
Views: 0