背景・目的
以前、下記の記事でECSの知識の整理や、VPC、ECR等の環境を構築しました。
今回は、バックエンドサービスとして、ECS上でアプリを実装します。
まとめ
全体像
構成
インフラとアプリの2つのリポジトリで構成します。
リポジトリ | 対象リソース |
---|---|
インフラ | ALB Fargateクラスタ ECRリポジトリ |
アプリ | タスク定義 Dockerイメージ |
実践
インフラ
-
alb
、ecs
モジュールを用意します。ECRと、S3、VPCは作成済み. ├── README.md ├── backend.hcl ├── backend.tf ├── main.tf ├── provider.tf ├── terraform-modules │ ├── alb │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── ecr │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── ecs │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── s3 │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ └── vpc │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── terraform.tfvars └── variable.tf
前提
GitHubActions + TerraformでECRを作成してみたで作成した環境を前提とします。
- Code Repository:GitHub
- CI/CD:GitHub Actions
- IaC: Terraform
- AWS
- ap-northeast-1リージョン
- 1aと1cの2AZ
- VPCとサブネット構築済み
- ECRとVPCe
IAMポリシーを修正
- GitHub Actionsから呼び出されるIAMロールのポリシーのStatement に下記のコードを追加します。※下記はすべてのポリシをー記しています
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "ec2:AuthorizeSecurityGroupIngress", "ec2:DeleteSubnet", "ec2:DeleteTags", "ec2:CreateVpc", "ec2:AttachInternetGateway", "ec2:DescribeVpcAttribute", "ec2:ReplaceRoute", "ec2:DeleteRouteTable", "ec2:ModifySubnetAttribute", "ec2:AssociateRouteTable", "ec2:DescribeInternetGateways", "ec2:DescribeNetworkInterfaces", "ec2:DescribeAvailabilityZones", "ec2:CreateRoute", "ec2:CreateInternetGateway", "ec2:RevokeSecurityGroupEgress", "ec2:CreateSecurityGroup", "ec2:DescribeAccountAttributes", "ec2:ModifyVpcAttribute", "ec2:DeleteInternetGateway", "ec2:DescribeRouteTables", "ec2:AuthorizeSecurityGroupEgress", "ec2:CreateTags", "ec2:DescribeVpcPeeringConnections", "ec2:DeleteRoute", "ec2:CreateRouteTable", "ec2:DetachInternetGateway", "ec2:DisassociateRouteTable", "ec2:DescribeSecurityGroups", "ec2:RevokeSecurityGroupIngress", "ec2:DescribeVpcs", "ec2:DeleteSecurityGroup", "ec2:DeleteVpc", "ec2:CreateSubnet", "ec2:DescribeSubnets" ], "Resource": "*" }, { "Sid": "VisualEditor1", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObjectAcl", "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::XXXXX", "arn:aws:s3:::XXXXX/*" ] }, { "Sid": "VisualEditor2", "Effect": "Allow", "Action": "s3:*", "Resource": [ "arn:aws:s3:::XXXXX", "arn:aws:s3:::XXXXX/*" ] }, { "Sid": "ECR", "Effect": "Allow", "Action": [ "ecr:CreateRepository", "ecr:ListTagsForResource", "ecr:DeleteRepository", "ecr:TagResource", "ec2:DescribePrefixLists", "ec2:CreateVpcEndpoint", "ec2:DescribeVpcEndpoints", "ec2:DeleteVpcEndpoints", "ec2:ModifyVpcEndpoint", "ec2:DescribeRouteTables", "ec2:DescribeSecurityGroups", "ecr:DescribeRepositories", "ecr:ListImages", "ecr:DescribeImages", "ecr:GetRepositoryPolicy", "ecr:BatchCheckLayerAvailability", "ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer", "ecr:SetRepositoryPolicy", "ecr:DeleteRepositoryPolicy" ], "Resource": "*" }, { "Sid": "VPCe", "Effect": "Allow", "Action": [ "ec2:CreateVpcEndpoint" ], "Resource": "*" }, { "Sid": "ALB", "Effect": "Allow", "Action": [ "elasticloadbalancing:CreateLoadBalancer", "elasticloadbalancing:DeleteLoadBalancer", "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:CreateTargetGroup", "elasticloadbalancing:DeleteTargetGroup", "elasticloadbalancing:DescribeTargetGroups", "elasticloadbalancing:RegisterTargets", "elasticloadbalancing:DeregisterTargets", "elasticloadbalancing:DescribeTargetHealth", "elasticloadbalancing:CreateListener", "elasticloadbalancing:DeleteListener", "elasticloadbalancing:DescribeListeners", "elasticloadbalancing:AddTags", "elasticloadbalancing:RemoveTags", "elasticloadbalancing:ModifyLoadBalancerAttributes", "elasticloadbalancing:DescribeLoadBalancerAttributes", "elasticloadbalancing:DescribeTargetGroupAttributes", "elasticloadbalancing:DescribeTags", "elasticloadbalancing:ModifyTargetGroupAttributes" ], "Resource": "*" }, { "Sid": "ECS", "Effect": "Allow", "Action": [ "ecs:CreateCluster", "ecs:DescribeClusters", "ecs:DeleteCluster", "ecs:RegisterTaskDefinition", "ecs:DescribeTaskDefinition", "ecs:DeregisterTaskDefinition", "ecs:CreateService", "ecs:DescribeServices", "ecs:UpdateService", "ecs:DeleteService", "ecs:ListTasks", "ecs:DescribeTasks", "ecs:StopTask", "ecs:TagResource" ], "Resource": "*" }, { "Sid": "IAM", "Effect": "Allow", "Action": [ "iam:CreateRole", "iam:DeleteRole", "iam:AttachRolePolicy", "iam:DetachRolePolicy", "iam:PassRole", "iam:TagRole", "iam:GetRole", "iam:CreatePolicy", "iam:ListRolePolicies", "iam:ListAttachedRolePolicies", "iam:ListInstanceProfilesForRole", "iam:GetPolicy", "iam:GetPolicyVersion", "iam:ListPolicyVersions", "iam:DeletePolicy", "iam:CreatePolicyVersion" ], "Resource": "*" }, { "Sid": "CWL", "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogGroups", "logs:DescribeLogStreams", "logs:PutRetentionPolicy", "logs:TagResource", "logs:ListTagsLogGroup", "logs:DeleteLogGroup" ], "Resource": "*" } ] }
ALBモジュールを作成
main.tf
- 下記のコードを追加します
resource "aws_lb" "this" { name = lower("${var.project_name}-elb2-loadbalancer-public") internal = false load_balancer_type = "application" security_groups = [aws_security_group.alb_sg.id] subnets = var.public_subnets tags = merge(var.tags, { "Name" = format("%s-elb2-loadbalancer-public", var.project_name) }) } resource "aws_security_group" "alb_sg" { name = format("%s-ec2-SecurityGroup-AlbSg", var.project_name) description = "ALB security group" vpc_id = var.vpc_id ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = merge(var.tags, { "Name" = format("%s-ec2-SecurityGroup-AlbSg", var.project_name) }) } resource "aws_lb_target_group" "this" { name = "${var.project_name}-elb2-TargetGroup" port = 80 protocol = "HTTP" vpc_id = var.vpc_id target_type = "ip" health_check { path = "/" interval = 30 timeout = 5 healthy_threshold = 2 unhealthy_threshold = 2 matcher = "200" } tags = merge(var.tags, { "Name" = format("%s-elb2-TargetGroup", var.project_name) }) } resource "aws_lb_listener" "this" { load_balancer_arn = aws_lb.this.arn port = 80 protocol = "HTTP" default_action { type = "forward" target_group_arn = aws_lb_target_group.this.arn } }
variables.tf
-
下記のコードを追加します
variable "vpc_id" { description = "The ID of the VPC" type = string } variable "public_subnets" { description = "List of public subnet IDs" type = list(string) } variable "project_name" { description = "Name of the project" type = string } variable "tags" { description = "A map of tags to assign to the resource" type = map(string) default = {} }
outputs.tf
- 下記のコードを追加します
output "alb_arn" { value = aws_lb.this.arn } output "alb_dns_name" { value = aws_lb.this.dns_name } output "alb_target_group_arn" { value = aws_lb_target_group.this.arn } output "alb_security_group_id" { value = aws_security_group.alb_sg.id }
ECSモジュールを作成
main.tf
- 下記のコードを追加します
resource "aws_iam_role" "ecs_task_execution_role" { name = "${var.project_name}-iam-role-EcsTaskExecution" assume_role_policy = jsonencode({ Version = "2012-10-17", Statement = [ { Effect = "Allow", Principal = { Service = "ecs-tasks.amazonaws.com" }, Action = "sts:AssumeRole" } ] }) tags = merge(var.tags, { "Name" = format("%s-iam-role-EcsTaskExecution", var.project_name) }) } resource "aws_iam_policy" "ecs_task_execution_policy" { name = "${var.project_name}-iam-policy-TaskExecution" description = "Policy for ECS Task Execution Role" policy = jsonencode({ Version = "2012-10-17", Statement = [ { Effect = "Allow", Action = [ "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:BatchCheckLayerAvailability", "ecr:GetAuthorizationToken" ], Resource = "*" }, { Effect = "Allow", Action = [ "logs:CreateLogStream", "logs:PutLogEvents" ], Resource = "arn:aws:logs:*:*:*" }, { Effect = "Allow", Action = [ "ecs:DescribeTasks" ], Resource = "*" }, { Effect = "Allow", Action = [ "s3:GetObject", "s3:PutObject", "s3:ListBucket" ], Resource = "*" } ] }) } resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy" { role = aws_iam_role.ecs_task_execution_role.name policy_arn = aws_iam_policy.ecs_task_execution_policy.arn } resource "aws_iam_role" "ecs_task_role" { name = "${var.project_name}-iam-role-EcsTask" assume_role_policy = jsonencode({ Version = "2012-10-17", Statement = [ { Effect = "Allow", Principal = { Service = "ecs-tasks.amazonaws.com" }, Action = "sts:AssumeRole" } ] }) tags = merge(var.tags, { "Name" = format("%s-iam-role-EcsTask", var.project_name) }) } resource "aws_ecs_cluster" "this" { name = "${var.project_name}-ecs-cluster-JavaBackEndApp" tags = var.tags } resource "aws_security_group" "ecs_sg" { name = "${var.project_name}-ec2-securitygroup-EcsSg" description = "ECS service security group" vpc_id = var.vpc_id ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = merge(var.tags, { "Name" = format("%s-ec2-securitygroup-EcsSg", var.project_name) }) } resource "aws_vpc_endpoint" "cloudwatch_logs" { vpc_id = var.vpc_id service_name = "com.amazonaws.ap-northeast-1.logs" vpc_endpoint_type = "Interface" subnet_ids = var.private_subnets security_group_ids = [aws_security_group.cloudwatch_logs_vpc_endpoint_sg.id] private_dns_enabled = true tags = { Name = "cloudwatch-logs-vpc-endpoint" } } resource "aws_security_group" "cloudwatch_logs_vpc_endpoint_sg" { name = "${var.project_name}-ec2-securitygroup-CloudwatchLogsVpcEndpointSg" description = "Security group for VPC endpoints" vpc_id = var.vpc_id ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } } resource "aws_cloudwatch_log_group" "ecs_log_group" { name = "/ecs/Example-ecs-service-JavaBackEndApp" retention_in_days = 7 # ログの保持期間を7日とする場合 tags = { Name = "ECS Log Group" } }
variables.tf
- 下記のコードを追加します
variable "project_name" { description = "Name of the project" type = string } variable "vpc_id" { description = "The ID of the VPC" type = string } variable "private_subnets" { description = "List of private subnet IDs" type = list(string) } variable "cpu" { description = "CPU units for the task" type = string default = "256" } variable "memory" { description = "Memory for the task" type = string default = "512" } variable "image_url" { description = "URL of the Docker image" type = string } variable "target_group_arn" { description = "ARN of the ALB target group" type = string } variable "tags" { description = "A map of tags to assign to the resource" type = map(string) default = {} }
outputs.tf
- 下記のコードを追加します
output "ecs_cluster_id" { value = aws_ecs_cluster.this.id } output "ecs_security_group_id" { value = aws_security_group.ecs_sg.id }
ルートモジュールの修正
mina.tf
- 下記のコードを追加します
module "alb" { source = "./terraform-modules/alb" vpc_id = module.vpc.vpc_id public_subnets = module.vpc.public_subnet_ids project_name = var.project_name tags = var.alb_tags } module "ecs" { source = "./terraform-modules/ecs" project_name = var.project_name vpc_id = module.vpc.vpc_id private_subnets = module.vpc.private_subnet_ids cpu = var.cpu memory = var.memory image_url = var.image_url target_group_arn = module.alb.alb_target_group_arn tags = var.ecs_tags }
variables.tf
- 下記のコードを追加します
variable "cpu" { description = "CPU units for the task" type = string default = "256" } variable "memory" { description = "Memory for the task" type = string default = "512" } variable "image_url" { description = "URL of the Docker image" type = string } variable "ecs_tags" { description = "A map of tags to assign to the ECS resources" type = map(string) default = {} }
デプロイと確認
ALBの確認
ECSクラスタの確認
アプリケーション
前提
下記の環境を前提としています。
- OpenJDK Runtime Environment Corretto-17.0.11.9.1
- Apache Maven 3.9.8
- VSCode
- MacOS
初期セットアップ
プロジェクトの作成
-
VSCodeを開きます
-
3.3.2を選択します
-
Javaを選択します
-
groupIdに
com.example
を入力します -
artifactIdに
java-backend
を入力します -
packaging typeを
Jar
を選択します -
Java versionを`17'とします
-
Choose dependenciesには、
Spring Reactive Web
を選択します -
Generate into this folderで作成します
Gitで管理
-
プロジェクトに移動します
$ cd java-backend
-
リポジトリを作成します
$ git init Initialized empty Git repository in /Users/XXX/git/spring/java-backend/.git/ $
-
.gitignore
ファイルを作成しますtarget/ .mvn/* !.mvn/wrapper !.mvn/wrapper/maven-wrapper.jar !.mvn/wrapper/maven-wrapper.properties ### VS Code ### .vscode/
-
ステージング追加&コミット
$ git add . $ git commit -m "Initial Commit"
-
プライベートリポジトリを作成します
$ gh repo create java-backend --private
-
リモート リポジトリの追加します
$ git remote add origin https://github.com/XXXX/java-backend.git
-
pushします
$ git push origin main
Javaアプリの作成
pom.xml
- ルートディレクトリ配下に
pom.xml
を作成します<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>sample</artifactId> <version>0.0.1-SNAPSHOT</version> <name>sample</name> <description>Demo project for Spring Boot</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
コントローラーを追加
- メインアプリケーションと同じフォルダに、
SampleRestController.java
を追加しますpackage com.example.java_backend; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class SampleRestController { @RequestMapping("/") public String index(){ return "Hello World!"; } }
動作確認
- ビルドとアプリケーション起動します
$ mvn clean package $ java -jar target/java-backend-0.0.1-SNAPSHOT.jar
- ブラウザで
localhost:8080
を開きます。想定通り表示されました
イメージの作成
- ルートディレクトリでDockerファイルを作成します
# Use the official Amazon Corretto 17 as a parent image FROM amazoncorretto:17 # Set the working directory WORKDIR /app # Copy the jar file from the target directory to the container COPY target/java-backend-0.0.1-SNAPSHOT.jar /app/app.jar # Run the jar file ENTRYPOINT ["java", "-jar", "/app/app.jar"]
ローカルで動作確認
-
ビルドします
$ docker build -t java-backend:latest .
-
確認します
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE java-backend latest 07ea83d496fb 33 seconds ago 488MB $
-
コンテナを実行します
$ docker run -d -p 8080:8080 --name java-backend-container java-backend:latest
-
ログを確認します。SpringBootが実行されています
$ docker logs java-backend-container . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.3.2) 2024-07-25T10:12:16.598Z INFO 1 --- [java-backend] [ main] c.e.java_backend.JavaBackendApplication : Starting JavaBackendApplication v0.0.1-SNAPSHOT using Java 17.0.12 with PID 1 (/app/app.jar started by root in /app) 2024-07-25T10:12:16.604Z INFO 1 --- [java-backend] [ main] c.e.java_backend.JavaBackendApplication : No active profile set, falling back to 1 default profile: "default" 2024-07-25T10:12:18.721Z INFO 1 --- [java-backend] [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 8080 (http) 2024-07-25T10:12:18.745Z INFO 1 --- [java-backend] [ main] c.e.java_backend.JavaBackendApplication : Started JavaBackendApplication in 2.854 seconds (process running for 3.719) $
-
停止します
$ docker stop java-backend-container
-
コンテナを削除します。消えました
# 削除前 $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES XXXX java-backend:latest "java -jar /app/app.…" 2 minutes ago Exited (143) About a minute ago java-backend-container $ # 削除 $ docker rm java-backend-container # 削除後確認 $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES $
CI/CD
ポート番号の変更
- ポート80でアクセスするために、
resources
フォルダのapplications.properties
を修正します。※必須ではありませんspring.application.name=java-backend server.port=80
Workflowの開発
-
GitHubにサインインします
-
ファイル名を変更し、「Commit changes」をクリックします
-
ローカルにダウンロードします
$ git pull origin main From https://github.com/XXXX/java-backend * branch main -> FETCH_HEAD Updating XXXX..XXXXX Fast-forward .github/workflows/build-and-deploy.yml | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 .github/workflows/build-and-deploy.yml $
-
下記のように修正します。※一部ゴミが残っています。ご容赦ください
name: Deploy to Amazon ECS on: push: branches: [ "main" ] env: VPC_MAME: Example-ec2-Vpc SUBNET_NAME: Example-vpc-Subnet-private-subnet-* TARGET_GROUP_NAME: Example-elb2-TargetGroup REGION_NAME: ap-northeast-1 ECR_REPOSITORY: example-java-backend ECS_SERVICE: Example-ecs-service-JavaBackEndApp3 ECS_CLUSTER: Example-ecs-cluster-JavaBackEndApp ECS_TASK_DEFINITION: ecs_task_definition_template.json CONTAINER_NAME: Example-java-backend ECS_SECURITY_GROUP_NAME: Example-ec2-securitygroup-EcsSg CONTAINER_PORT: 80 ECS_SERVICE_FILE: ecs_service_template.json ROLE_ARN: arn:aws:iam::${{secrets.AWS_ID}}:role/${{secrets.ROLE_NAME}} SESSION_NAME: gh-oidc-java-backend-${{github.run_id}}-${{github.run_attempt}} permissions: contents: read jobs: deploy: name: Deploy runs-on: ubuntu-latest # environment: production timeout-minutes: 15 permissions: id-token: write contents: read steps: - name: Checkout uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: 'corretto' java-version: '17' - name: Build with Maven run: ./mvnw clean package - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ env.ROLE_ARN }} aws-region: ${{ env.REGION_NAME }} role-session-name: ${{env.SESSION_NAME}} - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - name: Build, tag, and push image to Amazon ECR id: build-image env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} IMAGE_TAG: ${{ github.sha }} run: | # Build the Docker image docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . # Push the Docker image to ECR docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG # Output the image URI echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT - name: Fill in the new image ID in the Amazon ECS task definition id: task-def uses: aws-actions/amazon-ecs-render-task-definition@v1 with: task-definition: ${{ env.ECS_TASK_DEFINITION }} container-name: ${{ env.CONTAINER_NAME }} image: ${{ steps.build-image.outputs.image }} - name: Replace placeholders in task definition run: | sed -i 's/AWS_ID/${{ secrets.AWS_ID }}/g' ${{ steps.task-def.outputs.task-definition }} - name: Register new task definition run: | TASK_DEF_ARN=$(aws ecs register-task-definition --cli-input-json file://${{ steps.task-def.outputs.task-definition }} --query "taskDefinition.taskDefinitionArn" --output text) echo "task_def_arn=$TASK_DEF_ARN" >> $GITHUB_ENV - name: Check if ECS service exists id: check-service run: | SERVICE_EXISTS=$(aws ecs describe-services --cluster ${{ env.ECS_CLUSTER }} --services ${{ env.ECS_SERVICE }} --region ${{ env.REGION_NAME }} | jq -r '.failures | length') echo "service_exists=$SERVICE_EXISTS" >> $GITHUB_ENV - name: output step service_exists run: echo "output step service_exists[ ${{env.service_exists}} ]" - name: output step task definition run: | echo "output step task definition[ ${{steps.task-def.outputs.task-definition}} ]" echo "output step task arn [ ${{env.task_def_arn}}]" - name: get VPC ID if: env.service_exists == '1' run: | VPC_ID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=${{ env.VPC_MAME }}" --query "Vpcs[0].VpcId" --output text) echo "vpc_id=$VPC_ID" >> $GITHUB_ENV - name: echo vpc_id run: echo "vpc_id[ ${{env.vpc_id}} ]" - name: get Subnet IDs if: env.service_exists == '1' run: | SUBNET_ID0=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=${{env.vpc_id}}" "Name=tag:Name,Values=${{env.SUBNET_NAME}}" --query "Subnets[0].SubnetId" --output text) echo "subnet_id0=$(echo $SUBNET_ID0 | tr '\t' ',')" >> $GITHUB_ENV SUBNET_ID1=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=${{env.vpc_id}}" "Name=tag:Name,Values=${{env.SUBNET_NAME}}" --query "Subnets[1].SubnetId" --output text) echo "subnet_id1=$(echo $SUBNET_ID1 | tr '\t' ',')" >> $GITHUB_ENV - name: echo subnet_ids run: | echo "subnet_id0 [ ${{env.subnet_id0}} ]" echo "subnet_id1 [ ${{env.subnet_id1}} ]" - name: get Security Group ID if: env.service_exists == '1' run: | SECURITY_GROUP_ID=$(aws ec2 describe-security-groups --filters "Name=tag:Name,Values=${{ env.ECS_SECURITY_GROUP_NAME }}" --query "SecurityGroups[0].GroupId" --output text) echo "security_group_id=$SECURITY_GROUP_ID" >> $GITHUB_ENV - name: echo security_group_id run: echo "security_group_id[ ${{env.security_group_id}} ]" - name: Retrieve Target Group ARN id: get-tg-arn run: | TG_ARN=$(aws elbv2 describe-target-groups --names ${{ env.TARGET_GROUP_NAME }} --region ${{ env.REGION_NAME }} --query "TargetGroups[0].TargetGroupArn" --output text) echo "tg_arn=$TG_ARN" >> $GITHUB_ENV - name: echo tg_arn run: echo "tg_arn[ ${{env.tg_arn}} ]" - name: Replace TASK_DEFINITION_ARN in service JSON run: | jq --arg TASK_DEFINITION_ARN env.task_def_arn '.taskDefinition = env.task_def_arn' ${{ env.SERVICE_JSON_FILE }} > ecs-service-elb-updated.json - name: Create Amazon ECS service if it does not exist if: env.service_exists == '1' run: | aws ecs create-service \ --cluster ${{ env.ECS_CLUSTER }} \ --service-name ${{ env.ECS_SERVICE }} \ --task-definition ${{ env.task_def_arn }} \ --desired-count 1 \ --launch-type FARGATE \ --network-configuration "awsvpcConfiguration={subnets=[${{env.subnet_id0}},${{env.subnet_id1}}],securityGroups=[${{env.security_group_id}}],assignPublicIp=ENABLED}" \ --load-balancers "targetGroupArn=${{env.tg_arn}},containerName=${{env.CONTAINER_NAME}},containerPort=${{env.CONTAINER_PORT}}" \ --region ${{ env.AWS_REGION }} - name: Update Amazon ECS service if: env.service_exists == '0' run: | aws ecs update-service \ --cluster ${{ env.ECS_CLUSTER }} \ --service ${{ env.ECS_SERVICE }} \ --task-definition ${{ env.task_def_arn }} \ --region ${{ env.AWS_REGION }}
IAMロールの作成
GitHubからOIDCで接続されるIAMロールを作成します
- AWSにサインインします
- IAMに移動し、ナビゲーションペインでロールをクリックします
- 「ロールを作成」をクリックします
- ウェブアイデンティティを選択します
- 下記を入力し、「次へ」をクリックします
- ポリシーは追加せずに、そのまま「次へ」をクリックします
- ロール名を入力し、「ロールを作成」をクリックします
- 作成したIAMロールを選択し、「許可を追加」>「インラインポリシーを作成」をクリックします
- JSONタブをクリックし、追加します
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ecr:BatchCheckLayerAvailability", "ecr:CompleteLayerUpload", "ecr:GetDownloadUrlForLayer", "ecr:DescribeRepositories", "ecr:InitiateLayerUpload", "ecr:UploadLayerPart", "ecr:PutImage", "ecr:CreateRepository", "ecr:GetAuthorizationToken", "ecr:DeleteRepository", "ecr:DeleteRepositoryPolicy", "ecr:DescribeImages", "ecr:DescribeRepositories", "ecr:GetRepositoryPolicy", "ecr:ListImages", "ecr:PutImageScanningConfiguration", "ecr:SetRepositoryPolicy", "ecr:UploadLayerPart", "ecr:CompleteLayerUpload" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "ecs:RegisterTaskDefinition", "ecs:UpdateService", "ecs:DescribeServices", "ecs:DescribeTaskDefinition", "ecs:DescribeTasks", "ecs:ListClusters", "ecs:ListServices", "ecs:ListTaskDefinitions", "ecs:ListTasks", "ecs:CreateService" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "iam:PassRole" ], "Resource": [ "arn:aws:iam::XXXXXX:role/Example-iam-role-EcsTask", "arn:aws:iam::XXXXXX:role/Example-iam-role-EcsTaskExecution" ] }, { "Sid": "EC2", "Effect": "Allow", "Action": [ "ec2:DescribeVpcs", "ec2:DescribeSubnets", "ec2:DescribeSecurityGroups", "elasticloadbalancing:DescribeTargetGroups" ], "Resource": "*" } ] }
- IAMポリシー名を入力し、「作成」をクリックします
GitHub Secrets
- GitHubにサインインします
- 対象リポジトリを選択します
- ①「Settings」>②「Secrets and variables>Actions」>③「Secrets」タブ>④「New repository secret」をクリックします
- AWSのアカウントIDと、ロールを追加します
確認
考察
今回、JavaプログラムをECS(Fargate)に、GitHubActions+Terraformでデプロイしてみました。
インフラとアプリのどちらで、何を管理するかの検討が予想以上に手こずりました。
おそらく、すでに事例はいくつもあるはずなので、今後も継続して試してみたいとおもいます。
参考