はじめに
CodePipelineによるCI/CDパイプラインは便利なんだけど、せっかくTerraformやCloudFormationであれこれ変数化して自動化しているにもかかわらず、ECS Fargateにデプロイするための taskdef.json と Appspec.yml で変数化できずにハマるケースがあって困る。今回は、これを解決する手段を考えた。
前提条件
初学者向けのパイプライン作成ハンズオンに出てくるアウトプットをごちゃごちゃいじるので、少なくとも、CodePipelineの基礎は抑えていないと厳しい。過去の記事では以下のあたりを読んでおくと分かりやすくなっているはずだ。
なお、この記事ではIaCはTerraformで書いている。
- CloudFormationテンプレートを1からしっかり理解しながらECS on Fargateなアプリを自動構築する(前編)
- CloudFormationテンプレートを1からしっかり理解しながらECS on Fargateなアプリを自動構築する(中編)
- CloudFormationテンプレートを1からしっかり理解しながらECS on Fargateなアプリを自動構築する(後編)
- Terraformの初心者がAmazon EC2に実行環境を作ってECS Fargateなアプリの自動構築をしてみる
方針
CodePipelineの taskdef.json には、プレースホルダ(チュートリアルやハンズオンではという名前で出てくる、コンテナのイメージIDで置換する機能)という強力な機能があるが、これは1つのビルドアーティファクトに対して1つのプレースホルダにしか対応していないし、最大4つまでしか指定できない。
これを駆使して頑張ろうとすると、変なビルドステージを作ったりしなければならずBuildSpecが煩雑化するので没にする。
となると、ビルドステージで良い感じに taskdef.json を生成してあげるのが簡単だろう。
IaC見直し箇所
さて、通常のハンズオンやチュートリアルでは、taskdef.json はリポジトリに入っているものをそのままソースステージから引っ張ってくることになるが、今回はビルドステージで編集したものを利用するため、CodePipelineのリソースを以下のように修正する。
stage {
name = "Deploy"
action {
run_order = 3
name = "Deploy"
category = "Deploy"
owner = "AWS"
provider = "CodeDeployToECS"
version = "1"
input_artifacts = [
"SourceArtifact",
"BuildArtifact",
]
configuration = {
ApplicationName = aws_codedeploy_app.application.name
DeploymentGroupName = aws_codedeploy_deployment_group.application.deployment_group_name
AppSpecTemplateArtifact = "SourceArtifact"
AppSpecTemplatePath = var.appspec_file_name
TaskDefinitionTemplateArtifact = "BuildArtifact" # ★ここをSourceArtifactから変更
Image1ArtifactName = "BuildArtifact"
Image1ContainerName = "IMAGE1_NAME"
}
}
また、taskdef.json は以下のように修正しておこう。
以下を、外部から指定可能にして置換できるようにする。
- <EXECUTION_ROLE_ARN>
- <TASK_ROLE_ARN>
- <CONTAINER_NAME>
- <LOGGROUP_NAME>
- <TASK_FAMILY>
{
"executionRoleArn": "<EXECUTION_ROLE_ARN>",
"taskRoleArn": "<TASK_ROLE_ARN>",
"containerDefinitions": [
{
"name": "<CONTAINER_NAME>",
"image": "<IMAGE1_NAME>",
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "<LOGGROUP_NAME>",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "ecs"
}
},
"portMappings": [
{
"containerPort": 80,
"hostPort": 80,
"protocol": "tcp"
}
],
"essential": true,
"cpu": 0,
"memoryReservation": 256,
}
],
"requiresCompatibilities": ["FARGATE"],
"networkMode": "awsvpc",
"cpu": "256",
"memory": "512",
"family": "<TASK_FAMILY>"
}
さて、CodeBuildでは、Buildspec の post_build で以下のようにして、上記の置換対象のタグを環境変数で置換する。
sed の置換の区切り文字をスラッシュから変更しているのは、IAMロールのARNやロググループ名にスラッシュが入ってくる可能性があるからだ。Terraformのtfファイル側で上手くエスケープしようと思ったが、面倒なのでBuildspec側で対応した。
post_build:
commands:
- docker push ${REPOSITORY_URI}:${IMAGE_TAG}
- docker push ${REPOSITORY_URI}:latest
- printf '{"name":"%s","ImageURI":"%s"}' $ECR_REPOSITORY_NAME $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
# ★以下を追加
- sed -i -e "s#<EXECUTION_ROLE_ARN>#${EXECUTION_ROLE_ARN}#" taskdef.json
- sed -i -e "s#<TASK_ROLE_ARN>#${TASK_ROLE_ARN}#" taskdef.json
- sed -i -e "s#<CONTAINER_NAME>#${CONTAINER_NAME}#" taskdef.json
- sed -i -e "s#<LOGGROUP_NAME>#${LOGGROUP_NAME}#" taskdef.json
- sed -i -e "s#<TASK_FAMILY>#${TASK_FAMILY}#" taskdef.json
また、taskdef.json をアーティファクトに指定することを忘れないように。
artifacts:
files:
- imageDetail.json
- taskdef.json
で、肝心のCodeBuild側から、良い感じに環境変数を渡してあげれば、taskdef.json も自動化の流れに乗せらえれる。CPUやメモリを検証環境とプロダクション環境で変更する場合も、tfファイル側でmapしてあげれば環境差分も吸収可能だ!
resource "aws_codebuild_project" "application" {
(中略)
environment {
type = "LINUX_CONTAINER"
compute_type = "BUILD_GENERAL1_SMALL"
image = "aws/codebuild/amazonlinux2-x86_64-standard:3.0"
privileged_mode = "true"
# 以下を追加
environment_variable {
name = "EXECUTION_ROLE_ARN"
value = aws_iam_role.ecs_taskexecution.arn
}
environment_variable {
name = "TASK_ROLE_ARN"
value = aws_iam_role.ecs.arn
}
environment_variable {
name = "CONTAINER_NAME"
value = var.container_name
}
environment_variable {
name = "LOGGROUP_NAME"
value = aws_cloudwatch_log_group.ecstask_log_group.name
}
environment_variable {
name = "TASK_FAMILY"
value = aws_ecs_task_definition.ecsfargate.family
}
}
(中略)
}
これでビルドを走らせると、ビルドアーティファクトの taskdef.json が以下のように出力される。
{
"executionRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/ContainerPipeline-ECSTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/ContainerPipeline-ECSTaskRole",
"containerDefinitions": [
{
"name": "ContainerPipeline-ECSContainer",
"image": "<IMAGE1_NAME>",
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecstask/ContainerPipeline-LogGroup",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "ecs"
}
},
"portMappings": [
{
"containerPort": 80,
"hostPort": 80,
"protocol": "tcp"
}
],
"essential": true,
"cpu": 0,
"memoryReservation": 256,
}
],
"requiresCompatibilities": ["FARGATE"],
"networkMode": "awsvpc",
"cpu": "256",
"memory": "512",
"family": "ContainerPipeline-ECSTaskFamily"
}
Appspecの ContainerName
も同じ要領で置換可能なはず。
これで、夢の全自動ビルドに一歩近づいた!