Terraformを使ったAWS CodePipelineを含むAWS Fargate自動デプロイ(Blue/Green)
概要
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の手順は以下の通りです。
- Terraform main.tfの作成&構築
- AWS NetworkのTerraform作成&構築
- AWS ECRのTerraform作成&構築
- AWS ALBのTerraform作成&構築
- AWS ECSのTerraform作成&構築
- AWS CodeCommitのTerraform作成&構築
- AWS CodeBuildのTerraform作成&構築
- AWS CodeDeployのTerraform作成&構築
- 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
│ ├── ecs_task_policy.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"
}
}
}
]
files/ecs_task_policy.json.tpl
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
これからは他の値を参照するケースがあるので、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自動デプロイについての記事でした。ありがとうございました!
Discussion