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

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

- [GitHubActions + TerraformでECRを作成してみた](https://qiita.com/zumax/items/3741b10fd7f07d4c1a95)
- [GitHubActions + TerraformでNWを作成してみた](https://qiita.com/zumax/items/5a9435d0d7371a8c5033)

# まとめ
## 全体像
今回は、下記の環境を構築します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/165b8822-0f34-7b51-f7cd-35e4da026139.png)


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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/697269e6-9a7d-4f97-97d6-b98c25850e43.png)

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

# 実践

## インフラ

- `alb`、`ecs` モジュールを用意します。ECRと、S3、VPCは作成済み
    ```bash
    .
    ├── 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を作成してみた](https://qiita.com/zumax/items/3741b10fd7f07d4c1a95)で作成した環境を前提とします。
- 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 に下記のコードを追加します。※下記はすべてのポリシをー記しています
    ```json
    {
        "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`フォルダを作成します
1. `alb`フォルダ配下に、`main.tf`、`variables.tf`、`outputs.tf`ファイルを作成します
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/ede1880d-7075-82a4-24bf-0eac82627f3e.png)

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

    ```terraform
    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. 下記のコードを追加します
    ```terraform
    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`フォルダを作成します
1. `ecs`フォルダ配下に、`main.tf`、`variables.tf`、`outputs.tf`ファイルを作成します
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/2dfc3232-de6a-9147-b4f2-db97169dceab.png)

#### main.tf
1. 下記のコードを追加します
    ```terraform
    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. 下記のコードを追加します
    ```terraform
    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. 下記のコードを追加します
    ```terraform
    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. 下記のコードを追加します
    ```terraform
    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. 下記のコードを追加します
    ```terraform
    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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/031e5b03-775b-eef6-2dd5-58f9bca70b67.png)

#### ALBの確認
1. AWSにサインインします
1. EC2に移動します
1. ナビゲーションペインで、「ロードバランサー」をクリックします
1. 作成されていました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/6d1a7dea-bbcb-e9e7-3ba5-990bc17ea8d4.png)

#### ECSクラスタの確認
1. ECSに移動します
1. ナビゲーションペインで「クラスター」を選択します
1. 作成されていました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/38b02963-50ff-bb4d-e6c3-22246013cd2d.png)


## アプリケーション
### 前提
下記の環境を前提としています。
- OpenJDK Runtime Environment Corretto-17.0.11.9.1
- Apache Maven 3.9.8
- VSCode
- MacOS

### 初期セットアップ
#### プロジェクトの作成
1. VSCodeを開きます
1. 表示＞コマンドパレット>Spring Initializerをクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/0989cec0-fc1f-54df-f38b-262df30f18c1.png)
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/a973ec1f-af01-dbea-e154-93707c028773.png)

1. 3.3.2を選択します
1. Javaを選択します
1. groupIdに`com.example`を入力します
1. artifactIdに`java-backend`を入力します
1. packaging typeを`Jar`を選択します
1. Java versionを`17'とします
1. Choose dependenciesには、`Spring Reactive Web`を選択します
1. Generate into this folderで作成します
1. Add to Workspaceを選択します。追加されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/392c1351-040d-ae53-c324-17deb39987eb.png)

#### Gitで管理
1. プロジェクトに移動します
    ```bash
    $ cd java-backend
    ```
1. リポジトリを作成します
    ```bash
    $ git init 
    Initialized empty Git repository in /Users/XXX/git/spring/java-backend/.git/
    $
    ```
1. `.gitignore`ファイルを作成します
    ```bash
    target/
    .mvn/*
    !.mvn/wrapper
    !.mvn/wrapper/maven-wrapper.jar
    !.mvn/wrapper/maven-wrapper.properties
    
    ### VS Code ###
    .vscode/
    ```

1. ステージング追加&コミット
    ```
    $ git add .
    $ git commit -m "Initial Commit"
    ```
1. プライベートリポジトリを作成します
    ```bash 
    $ gh repo create java-backend --private
    ```
1. GitHubにアクセスします。できてました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/3a7ec7a2-3e9e-32c4-0f2f-4c3a9adc5498.png)

1. リモート リポジトリの追加します
    ```bash 
    $ git remote add origin https://github.com/XXXX/java-backend.git
    ```
1. pushします
    ```bash 
    $ git push origin main
    ```
1. 最初のファイルを登録できました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/646603bf-f3b2-f081-e2c0-c011ff64be99.png)

### Javaアプリの作成
#### pom.xml
1. ルートディレクトリ配下に `pom.xml`を作成します
    ```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`を追加します
    ```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. ビルドとアプリケーション起動します
    ```shell
    $ mvn clean package
    $ java -jar target/java-backend-0.0.1-SNAPSHOT.jar
    ```
1. ブラウザで`localhost:8080`を開きます。想定通り表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/3a5edee9-8592-6034-bfb4-6a2ceffc8135.png)

#### イメージの作成
1. ルートディレクトリでDockerファイルを作成します
    ```Dockerfile
    # 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. ビルドします
    ```shell
    $ docker build -t java-backend:latest .
    ```

1. 確認します
    ```shell
    $ docker image ls
    REPOSITORY                   TAG       IMAGE ID       CREATED          SIZE
    java-backend                 latest    07ea83d496fb   33 seconds ago   488MB
    $
    ```
1. コンテナを実行します
    ```shell
    $ docker run -d -p 8080:8080 --name java-backend-container java-backend:latest
    ```

1. ログを確認します。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)
    $ 
    ```
1. ブラウザで`localhost:8080`を開きます。想定通り表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/6a70f123-867e-fdb5-ddcc-4f7d5002dd87.png)

1. 停止します
    ```
    $ docker stop java-backend-container
    ```
1. コンテナを削除します。消えました
    ```shell
    # 削除前
    $ 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`を修正します。※必須ではありません
    ```properties
    spring.application.name=java-backend
    server.port=80
    ```

#### Workflowの開発
1. GitHubにサインインします
1. ①「Actions」＞②「ECS」で検索し、③「Configure」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/700b862d-a1bb-fc74-1770-28752df03dc3.png)

1. ファイル名を変更し、「Commit changes」をクリックします
1. ローカルにダウンロードします
    ```
    $ 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
    $ 
    ```
1. 下記のように修正します。※一部ゴミが残っています。ご容赦ください
    ```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ロールを作成します

1. AWSにサインインします
1. IAMに移動し、ナビゲーションペインでロールをクリックします
1. 「ロールを作成」をクリックします
1. ウェブアイデンティティを選択します
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/96cb3089-ceae-508a-30d1-f0ecfe7268ab.png)
1. 下記を入力し、「次へ」をクリックします
    - アイデンティティプロバイダー：以前作成したGitHubプロバイダーを指定します
    - Audience:sts.amazonaws.com
    - GitHub組織：自分のアカウント
    - GitHubリポジトリ：対象リポジトリ
    - GitHubブランチ:対象のブランチ
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/07ac9020-d952-73c8-318c-0ab3b18d2b90.png)
1. ポリシーは追加せずに、そのまま「次へ」をクリックします
1. ロール名を入力し、「ロールを作成」をクリックします
1. 作成したIAMロールを選択し、「許可を追加」＞「インラインポリシーを作成」をクリックします
1. JSONタブをクリックし、追加します
    ```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": "*"
            }
        ]
    }
    ```
1. IAMポリシー名を入力し、「作成」をクリックします

#### GitHub Secrets
1. GitHubにサインインします
1. 対象リポジトリを選択します
1. ①「Settings」＞②「Secrets and variables>Actions」＞③「Secrets」タブ＞④「New repository secret」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/3c52fbad-4d53-ca0c-741c-d1f130d9967f.png)
1. AWSのアカウントIDと、ロールを追加します
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/d2e740c4-57e7-224b-ac38-1e75cf106feb.png)

### 確認
1. GitHubにPushします
1. 成功しました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/e638f421-4888-8a72-7ec9-b391dd1abf43.png)

1. ブラウザでALBのDNS名にアクセスします。表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/d08d3975-e9c8-13b5-a25d-8ce81e842233.png)


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

# 参考
https://qiita.com/zumax/items/e241894713016871301e

https://qiita.com/zumax/items/71184fc108b6894e5b27

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/security-iam-awsmanpol.html

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/vpc-endpoints.html#ecr-setting-up-s3-gateway