🚀

Terraformを使ったAWS CodePipelineを含むAWS Fargate自動デプロイ(Blue/Green)

2023/10/23に公開

概要

MEGAZONE JAPANのジョンです。🫡

今回はTerraformを使用して、AWS FragateとAWS CodePipeline(AWS CodeCommit + AWS CodeBuild + AWS CodeDeploy(Blue/Green))を構築及びデプロイプロセスを自動化してみました。
その構成は以下の図となります。

まず、動作の仕組みは、開発者がAWS CodeCommitのMain BranchにPushをトリガーにして、AWS CodePipelineを実行します。その後、AWS CodeCommitからソースコードを順次取得し、Amazon S3に格納します。AWS CodeBuildが起動して、Amazon S3からソースコードを取得し、ECRへのdocker pushを行います。最終的に、AWS CodeDeployがAWS FargateのServiceを新しいTask DefinitionでUpdateします。ちなみに、今回のデプロイ作業はBlue/Greenデプロイを行いました。

Hands-onにあたっての前提条件は以下のようになります。(インストール方法は省略する)

  • Local環境
    • Terraform v1.5.4 (~> 1.5.0)
    • aws-cli
    • docker
    • Git
  • AWS環境
    • Terraform state file管理用のAmazon S3 Bucket

なお、Hands-onの手順は以下の通りです。

  1. Terraform main.tfの作成&構築
  2. AWS NetworkのTerraform作成&構築
  3. AWS ECRのTerraform作成&構築
  4. AWS ALBのTerraform作成&構築
  5. AWS ECSのTerraform作成&構築
  6. AWS CodeCommitのTerraform作成&構築
  7. AWS CodeBuildのTerraform作成&構築
  8. AWS CodeDeployのTerraform作成&構築
  9. AWS CodePipelineのTerraform作成&構築

※本Hands-onでは、情報の量に応じて、AWSやTerraformの詳細仕様については触れません。

Hands-on

1. Terraform main.tfの作成&構築

Terraformで使う変数は以下の通りです。必要に応じて変数の値を書き換えても構いません。

main.tfでは

  • Terraform Version
  • 使用するProviders
  • tfstateファイルの管理方法

を作成しました。そして、terraform initでワークスペースを初期化しておきましょう。

2. AWS NetworkのTerraform作成&構築

構成図にもありますが、AWS Networkの部分は以下のリソースを構築します。

No Resource Usage
1 VPC 仮想ネットーワーク
2 Public Subnet Public用 x2
3 Private Subnet Private用 x2
4 Internet Gateway VPCとインターネットとの間の通信
5 Elastic IP NAT Gateway用のEIP x2
6 NAT Gateway VPCをインターネットに対するOutBound通信 x2
7 Public Route Table Internet Gatewayにつなぐ
8 Private Route Table NAT Gatewayにつなぐ
9 ALB Secirity Group ALB用
10 ECS Secirity Group ECS(Fargate)用

nw.tf作成後、terraform planして、問題なければterraform applyを実行します。

3. AWS ECRのTerraform作成&構築

AWS ECS Fargateでコンテナ起動をするために、事前にDocker ImageをAWS ECRにPushしておきます。
まずは、AWS ECRを作成して、terraform applyまで実行します。

その後、Docker Imageを作成します。Dockerfileはシンプルなnginxを使用しましたので、以下を参考してください。

.
├── Dockerfile
├── src
│   └── index.html

Dockerfile

FROM 'nginx:latest'
RUN service nginx start
COPY src /usr/share/nginx/html
VOLUME /usr/share/nginx/html

src/index.html

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

次は、このDockerfileをAWS ECRにPushする作業が必要です。やり方は、AWS ECR詳細画面の「View push commands」で確認できますので、そのページ(以下のイメージ)を参考ください。
ちなみに、macOSのApple siliconを使っている場合、docker buildする際に、docker build --platform linux/amd64 -t ${YOUR_ECR_NAME} .のコマンドように--platformオプションを使う必要があります。

4. AWS ALBのTerraform作成&構築

AWS ECSにはLoad Balancerが必要なため、AWS ALBを構築します。
同じく、terraform applyまで実行します。

後ほどAWS ECSを構築してから、AWS ALB DNSを使うので、Outputとして記載しておきましょう。

5. AWS ECSのTerraform作成&構築

次はAWS ECSを構築します。これまでのFileとコードの量が増えていくので、現在のFile構成は以下のようになることが想定されます。(File名は以下に合わせなくてもOK)
では、<-- add部分を作成していきます。

.
├── Dockerfile
├── alb.tf
├── ecr.tf
├── ecs.tf <-- add
├── files  <-- add
│   ├── assumerole.json.tpl  <-- add
│   ├── container_definitions.json.tpl  <-- add
├── locals.tf <-- add
├── main.tf
├── nw.tf
├── output.tf
├── src
│   └── index.html
└── vars.tf

assumroleとTask definitionは以下のように作成します。

files/assumerole.json.tpl

{
 "Version": "2012-10-17",
 "Statement": [
   {
     "Action": "sts:AssumeRole",
     "Principal": {
       "Service": "${resource}.amazonaws.com"
     },
     "Effect": "Allow",
     "Sid": ""
   }
 ]
}

files/container_definitions.json.tpl

[
    {
        "name": "${container_name}",
        "image": "${container_image}",
        "cpu": 256,
        "essential": true,
        "memory": 512,
        "portMappings": [
            {
                "protocol": "tcp",
                "containerPort": ${container_port},
                "hostPort": ${container_port}
            }
        ],
        "LogConfiguration": {
            "logDriver": "awslogs",
            "options": {
                "awslogs-group": "${cloudwatch_log_group_name}",
                "awslogs-region": "ap-northeast-1",
                "awslogs-stream-prefix": "terraform"
            }
        }
    }
]

これからは他の値を参照するケースがあるので、Localsを利用します。

AWS ECSは次の通りに作成して、terraform applyまで実行します。

AWS ECSまで構築できたら、ALB DNS名でアクセスして、index.htmlが表示されるのかを確認します。

次は、デプロイプロセスを自動化してみます。

6. AWS CodeCommitのTerraform作成&構築

AWS CodePepilineの構築にあたって、AWS CodeCommit,CodeBuild,CodeDeployを構築します。
AWS CodePepilineをterraform applyする際に、AWS CodePepilineが実行されるので、事前にAWS CodeCommitにDockerfileなどデプロイに必要なFileをgit pushしておきます。

terraform applyまで実行してから、AWS CodeCommitからgit cloneします。
やり方は以下のように、AWS CodeCommitを作成して、詳細ページに移動すると、Connetion stepsで説明しているので、自分の環境に合わせて設定しておきます。

その後、以下のFilesをAWS CodeCommitにgit pushします。Dockerfileとsrc/は上記のAWS ECR構築時に説明したので、残りのFileを作成します。(他の.tf filesもpushしても構いません)

.
├── Dockerfile <-- 先ほどのAWS ECRに利用したFile
├── appspec.yml
├── buildspec.yml
├── imageDetail.json
├── src <-- 先ほどのAWS ECRに利用したFile
│   └── index.html
└── taskdef.json

appspec.ymlはアプリケーションの仕様を設定するFileです。基本的にはBlue/Greenで使います。
変数に関して、

  • <TASK_DEFINITION>: Task DefinitionのARNに自動で置き換えられる。(プロバイダーがCodeDeployToECSのため)
  • <CONTAINER_NAME>: AWS CodeBuildで設定する。
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
          ContainerName: <CONTAINER_NAME>
          ContainerPort: 80

buildspec.ymlはAWS CodeBuildの仕様を設定するFileです。ここでやっていることは、AWS ECRにdocker pushを手動で行ったことをスクリプトで実行します。
変数に関して、

  • $AWS_ACCOUNT_ID: 機密情報なので、AWS SSM Parameter storeから取得する。
  • 他の変数はAWS CodeBuildで設定する。
version: 0.2

env:
  parameter-store:
    AWS_ACCOUNT_ID: "MY_ACCOUNT_ID"
phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $ECR_NAME:$IMAGE_TAG .
      - docker tag $ECR_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$ECR_NAME:$IMAGE_TAG      
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$ECR_NAME:$IMAGE_TAG
      - printf '{"ImageURI":"%s"}' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$ECR_NAME:$IMAGE_TAG  > imageDetail.json
      - echo "$(cat imageDetail.json)"
      - echo Rewriting task definitions file...
      - sed -i -e "s#<ECR_NAME>#$ECR_NAME#" taskdef.json
      - sed -i -e "s#<IMAGE_TAG>#$IMAGE_TAG#" taskdef.json
      - sed -i -e "s#<LOGS_GROUP>#$LOGS_GROUP#" taskdef.json
      - sed -i -e "s#<TASK_FAMILY>#$TASK_FAMILY#" taskdef.json
      - sed -i -e "s#<TASK_ROLE_NAME>#$TASK_ROLE_NAME#" taskdef.json
      - sed -i -e "s#<CONTAINER_NAME>#$CONTAINER_NAME#" taskdef.json
      - sed -i -e "s#<AWS_ACCOUNT_ID>#$AWS_ACCOUNT_ID#" taskdef.json
      - sed -i -e "s#<EXECUTION_ROLE_NAME>#$EXECUTION_ROLE_NAME#" taskdef.json
      - sed -i -e "s#<AWS_DEFAULT_REGION>#$AWS_DEFAULT_REGION#" taskdef.json
      - echo "$(cat taskdef.json)"
      - echo Rewriting appspec file...
      - sed -i -e "s#<CONTAINER_NAME>#$CONTAINER_NAME#" appspec.yml
      - echo "$(cat appspec.yml)"

artifacts:
  files: 
    - imageDetail.json
    - taskdef.json
    - appspec.yml

imageDetail.jsonはFileだけを作成しておきます。これはイメージ定義のFileです。
ちなみに、RollingとBlue/Greenで扱っているFile名が異なるため、一回目を通していただくと良いのではないかと思います。
イメージ定義ファイルのリファレンスについて

taskdef.jsonはAWS ECSのTask DefinitionのFileです。
変数に関して、

  • <AWS_ACCOUNT_ID>: 機密情報なので、AWS SSM Parameter storeから取得する。
  • <IMAGE1_NAME>: imageDetail.jsonから取得したImage URLに自動で置き換えられる。(プロバイダーがCodeDeployToECSのため)
  • 他の変数はAWS CodeBuildで設定する。
{
	"containerDefinitions": [{
		"name": "<CONTAINER_NAME>",
		"image": "<IMAGE1_NAME>",
		"essential": true,
		"cpu": 256,
                "memory": 512,
		"portMappings": [{
			"hostPort": 80,
			"protocol": "tcp",
			"containerPort": 80
		}],
		"logConfiguration": {
			"logDriver": "awslogs",
			"options": {
				"awslogs-group": "<LOGS_GROUP>",
				"awslogs-region": "<AWS_DEFAULT_REGION>",
				"awslogs-stream-prefix": "terraform"
			}
		}
	}],
	"cpu": "256",
	"memory": "512",
	"taskRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/<TASK_ROLE_NAME>",
	"executionRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/<EXECUTION_ROLE_NAME>",
	"family": "<TASK_FAMILY>",
	"networkMode": "awsvpc",
	"requiresCompatibilities": [
		"FARGATE"
	]
}

では、以下のFilesができたと思いますので、AWS CodeCommitにgit pushします。

.
├── Dockerfile
├── appspec.yml
├── buildspec.yml
├── imageDetail.json
├── src
│   └── index.html
└── taskdef.json

7. AWS CodeBuildのTerraform作成&構築

AWS CodeBuildに必要なPolicyを作成します。

files/codebuild_policy.json.tpl

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Resource": [
                "arn:aws:logs:${region}:${account_id}:log-group:/aws/codebuild/${codebuild_name}",
                "arn:aws:logs:${region}:${account_id}:log-group:/aws/codebuild/${codebuild_name}:*"
            ],
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ]
        },
        {
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::${bucket_name}/*"
            ],
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:GetBucketAcl",
                "s3:GetBucketLocation"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "ecr:*"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ssm:GetParametersByPath",
                "ssm:GetParameters"
            ],
            "Resource": "arn:aws:ssm:${region}:${account_id}:parameter/${param_store_name}"
        }
    ]
}

次は、処理の成果物を格納するために、Artifact用のAmazon S3とAWS Account IDを管理するAWS SSM Parameter storeを作成します。
また、AWS CodeBuildを作成して、terraform applyまで実行します。

8. AWS CodeDeployのTerraform作成&構築

AWS CodeDeployに必要なPolicyを作成します。

files/codedeploy_policy.json.tpl

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "ecs:DescribeServices",
                "ecs:CreateTaskSet",
                "ecs:UpdateServicePrimaryTaskSet",
                "ecs:DeleteTaskSet",
                "cloudwatch:DescribeAlarms"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "sns:Publish"
            ],
            "Resource": "arn:aws:sns:*:*:CodeDeployTopic_*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "elasticloadbalancing:DescribeTargetGroups",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:ModifyListener",
                "elasticloadbalancing:DescribeRules",
                "elasticloadbalancing:ModifyRule"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "lambda:InvokeFunction"
            ],
            "Resource": "arn:aws:lambda:*:*:function:CodeDeployHook_*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "s3:ExistingObjectTag/UseWithCodeDeploy": "true"
                }
            },
            "Effect": "Allow"
        },
        {
            "Action": [
                "iam:PassRole"
            ],
            "Effect": "Allow",
            "Resource": "*",
            "Condition": {
                "StringLike": {
                    "iam:PassedToService": [
                        "ecs-tasks.amazonaws.com"
                    ]
                }
            }
        }
    ]
}

次は、AWS CodeDeployを作成して、terraform applyまで実行します。

9. AWS CodePipelineのTerraform作成&構築

AWS CodeCommit, CodeBuild, CodeDeployを構築しましたので、AWS CodePipelineを構築します。
まずは、AWS CodePipelineに必要なPolicyを作成します。

files/codepipeline_policy.json.tpl

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "iam:PassRole"
            ],
            "Resource": "*",
            "Effect": "Allow",
            "Condition": {
                "StringEqualsIfExists": {
                    "iam:PassedToService": [
                        "cloudformation.amazonaws.com",
                        "elasticbeanstalk.amazonaws.com",
                        "ec2.amazonaws.com",
                        "ecs-tasks.amazonaws.com"
                    ]
                }
            }
        },
        {
            "Action": [
                "codecommit:CancelUploadArchive",
                "codecommit:GetBranch",
                "codecommit:GetCommit",
                "codecommit:GetUploadArchiveStatus",
                "codecommit:UploadArchive"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "codedeploy:CreateDeployment",
                "codedeploy:GetApplication",
                "codedeploy:GetApplicationRevision",
                "codedeploy:GetDeployment",
                "codedeploy:GetDeploymentConfig",
                "codedeploy:RegisterApplicationRevision"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "elasticbeanstalk:*",
                "ec2:*",
                "elasticloadbalancing:*",
                "autoscaling:*",
                "cloudwatch:*",
                "s3:*",
                "sns:*",
                "cloudformation:*",
                "rds:*",
                "sqs:*",
                "ecs:*"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "lambda:InvokeFunction",
                "lambda:ListFunctions"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "opsworks:CreateDeployment",
                "opsworks:DescribeApps",
                "opsworks:DescribeCommands",
                "opsworks:DescribeDeployments",
                "opsworks:DescribeInstances",
                "opsworks:DescribeStacks",
                "opsworks:UpdateApp",
                "opsworks:UpdateStack"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "cloudformation:CreateStack",
                "cloudformation:DeleteStack",
                "cloudformation:DescribeStacks",
                "cloudformation:UpdateStack",
                "cloudformation:CreateChangeSet",
                "cloudformation:DeleteChangeSet",
                "cloudformation:DescribeChangeSet",
                "cloudformation:ExecuteChangeSet",
                "cloudformation:SetStackPolicy",
                "cloudformation:ValidateTemplate"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "codebuild:BatchGetBuilds",
                "codebuild:StartBuild"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Effect": "Allow",
            "Action": [
                "devicefarm:ListProjects",
                "devicefarm:ListDevicePools",
                "devicefarm:GetRun",
                "devicefarm:GetUpload",
                "devicefarm:CreateUpload",
                "devicefarm:ScheduleRun"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "servicecatalog:ListProvisioningArtifacts",
                "servicecatalog:CreateProvisioningArtifact",
                "servicecatalog:DescribeProvisioningArtifact",
                "servicecatalog:DeleteProvisioningArtifact",
                "servicecatalog:UpdateProduct"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudformation:ValidateTemplate"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ecr:DescribeImages"
            ],
            "Resource": "*"
        }
    ]
}

次は、AWS CodePipelineを作成して、terraform applyまで実行します。

AWS CodePipelineが構築されたら、すぐ動き始めますが、以下のようにSourceからDeployまで実行できていることが確認できます。

AWS ALB DNSでアクセスすると、先程git pushしたindex.htmlが表示されます。

では、実際にgit pushでデプロイ自動化を試してみます。
index.htmlの背景をGreenに設定して、git push origin mainを実行します。


<-- 省略 -->
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; 
background-color:green;}
</style>
<-- 省略 -->

AWS CodePipelineではgit commitのメッセージedit background colorが表示され、Deployまで実行されていることが確認できます。

アクセスしてみると、背景がGreenになっているindex.htmlが表示されましたので、これでgit pushでデプロイの自動化ができました。

最後に

この内容をまとめてみたいと思っていましたが、ついに作成できました。異なる方法やアプローチが人それぞれ存在するかもしれませんが、この記事が皆様の参考になれば幸いです。
以上、Terraformを使ったAWS CodePipelineを含むAWS Fargate自動デプロイについての記事でした。ありがとうございました!

MEGAZONE株式会社 Tech Blog

Discussion