0
0

GitHubActions + Terraformでコンテナアプリを構築してみた

Last updated at Posted at 2024-07-31

背景・目的

以前、下記の記事でECSの知識の整理や、VPC、ECR等の環境を構築しました。
今回は、バックエンドサービスとして、ECS上でアプリを実装します。

まとめ

全体像

今回は、下記の環境を構築します。
image.png

構成

インフラとアプリの2つのリポジトリで構成します。

image.png

リポジトリ 対象リソース
インフラ ALB
Fargateクラスタ
ECRリポジトリ
アプリ タスク定義
Dockerイメージ

実践

インフラ

  • albecs モジュールを用意します。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ポリシーを修正

  1. 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モジュールを作成

  1. terraform-modules配下に、albフォルダを作成します
  2. albフォルダ配下に、main.tfvariables.tfoutputs.tfファイルを作成します
    image.png

main.tf

  1. 下記のコードを追加します
    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

  1. 下記のコードを追加します

    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

  1. 下記のコードを追加します
    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モジュールを作成

  1. terraform-modules配下に、ecsフォルダを作成します
  2. ecsフォルダ配下に、main.tfvariables.tfoutputs.tfファイルを作成します
    image.png

main.tf

  1. 下記のコードを追加します
    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

  1. 下記のコードを追加します
    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

  1. 下記のコードを追加します
    output "ecs_cluster_id" {
      value = aws_ecs_cluster.this.id
    }
    
    output "ecs_security_group_id" {
      value = aws_security_group.ecs_sg.id
    }
    

ルートモジュールの修正

mina.tf

  1. 下記のコードを追加します
    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

  1. 下記のコードを追加します
    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     = {}
    }
    

デプロイと確認

  1. CommitとPushをします。問題なく完了しています
    image.png

ALBの確認

  1. AWSにサインインします
  2. EC2に移動します
  3. ナビゲーションペインで、「ロードバランサー」をクリックします
  4. 作成されていました
    image.png

ECSクラスタの確認

  1. ECSに移動します
  2. ナビゲーションペインで「クラスター」を選択します
  3. 作成されていました
    image.png

アプリケーション

前提

下記の環境を前提としています。

  • OpenJDK Runtime Environment Corretto-17.0.11.9.1
  • Apache Maven 3.9.8
  • VSCode
  • MacOS

初期セットアップ

プロジェクトの作成

  1. VSCodeを開きます

  2. 表示>コマンドパレット>Spring Initializerをクリックします
    image.png
    image.png

  3. 3.3.2を選択します

  4. Javaを選択します

  5. groupIdにcom.exampleを入力します

  6. artifactIdにjava-backendを入力します

  7. packaging typeをJarを選択します

  8. Java versionを`17'とします

  9. Choose dependenciesには、Spring Reactive Webを選択します

  10. Generate into this folderで作成します

  11. Add to Workspaceを選択します。追加されました
    image.png

Gitで管理

  1. プロジェクトに移動します

    $ cd java-backend
    
  2. リポジトリを作成します

    $ git init 
    Initialized empty Git repository in /Users/XXX/git/spring/java-backend/.git/
    $
    
  3. .gitignoreファイルを作成します

    target/
    .mvn/*
    !.mvn/wrapper
    !.mvn/wrapper/maven-wrapper.jar
    !.mvn/wrapper/maven-wrapper.properties
    
    ### VS Code ###
    .vscode/
    
  4. ステージング追加&コミット

    $ git add .
    $ git commit -m "Initial Commit"
    
  5. プライベートリポジトリを作成します

    $ gh repo create java-backend --private
    
  6. GitHubにアクセスします。できてました
    image.png

  7. リモート リポジトリの追加します

    $ git remote add origin https://github.com/XXXX/java-backend.git
    
  8. pushします

    $ git push origin main
    
  9. 最初のファイルを登録できました
    image.png

Javaアプリの作成

pom.xml

  1. ルートディレクトリ配下に 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>
    

コントローラーを追加

  1. メインアプリケーションと同じフォルダに、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!";
      }
    
    }
    

動作確認

  1. ビルドとアプリケーション起動します
    $ mvn clean package
    $ java -jar target/java-backend-0.0.1-SNAPSHOT.jar
    
  2. ブラウザでlocalhost:8080を開きます。想定通り表示されました
    image.png

イメージの作成

  1. ルートディレクトリで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"]
    

ローカルで動作確認

  1. ビルドします

    $ docker build -t java-backend:latest .
    
  2. 確認します

    $ docker image ls
    REPOSITORY                   TAG       IMAGE ID       CREATED          SIZE
    java-backend                 latest    07ea83d496fb   33 seconds ago   488MB
    $
    
  3. コンテナを実行します

    $ docker run -d -p 8080:8080 --name java-backend-container java-backend:latest
    
  4. ログを確認します。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)
    $ 
    
  5. ブラウザでlocalhost:8080を開きます。想定通り表示されました
    image.png

  6. 停止します

    $ docker stop java-backend-container
    
  7. コンテナを削除します。消えました

    # 削除前
    $ 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

ポート番号の変更

  1. ポート80でアクセスするために、resourcesフォルダのapplications.propertiesを修正します。※必須ではありません
    spring.application.name=java-backend
    server.port=80
    

Workflowの開発

  1. GitHubにサインインします

  2. ①「Actions」>②「ECS」で検索し、③「Configure」をクリックします
    image.png

  3. ファイル名を変更し、「Commit changes」をクリックします

  4. ローカルにダウンロードします

    $ 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
    $ 
    
  5. 下記のように修正します。※一部ゴミが残っています。ご容赦ください

    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ロールを作成します

  1. AWSにサインインします
  2. IAMに移動し、ナビゲーションペインでロールをクリックします
  3. 「ロールを作成」をクリックします
  4. ウェブアイデンティティを選択します
    image.png
  5. 下記を入力し、「次へ」をクリックします
    • アイデンティティプロバイダー:以前作成したGitHubプロバイダーを指定します
    • Audience:sts.amazonaws.com
    • GitHub組織:自分のアカウント
    • GitHubリポジトリ:対象リポジトリ
    • GitHubブランチ:対象のブランチ
      image.png
  6. ポリシーは追加せずに、そのまま「次へ」をクリックします
  7. ロール名を入力し、「ロールを作成」をクリックします
  8. 作成したIAMロールを選択し、「許可を追加」>「インラインポリシーを作成」をクリックします
  9. 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": "*"
            }
        ]
    }
    
  10. IAMポリシー名を入力し、「作成」をクリックします

GitHub Secrets

  1. GitHubにサインインします
  2. 対象リポジトリを選択します
  3. ①「Settings」>②「Secrets and variables>Actions」>③「Secrets」タブ>④「New repository secret」をクリックします
    image.png
  4. AWSのアカウントIDと、ロールを追加します
    image.png

確認

  1. GitHubにPushします

  2. 成功しました
    image.png

  3. ブラウザでALBのDNS名にアクセスします。表示されました
    image.png

考察

今回、JavaプログラムをECS(Fargate)に、GitHubActions+Terraformでデプロイしてみました。
インフラとアプリのどちらで、何を管理するかの検討が予想以上に手こずりました。
おそらく、すでに事例はいくつもあるはずなので、今後も継続して試してみたいとおもいます。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0