ECSで開発用と本番用のCodePipelineを作る
前提
- 本番用と開発用のアカウントが別れている
- GitHubを使っている
- 開発用はdevブランチがマージされるとcodepipelineが発火
- 本番用はmasterブランチがマージされるとcodepipelineが発火
- それぞれビルドが走りイメージが作成されそれぞれのアカウントのECRに登録されたのちに、CodeDeployが発火する
問題点
- 一旦開発ようにtaskdef.jsonやappspec.yamlとbuildspec.yamlを作成したが本番アカウントに流用できない
以下3つのファイルをレポジトリ上で管理している。
appspec.yaml
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: <TASK_DEFINITION>
LoadBalancerInfo:
ContainerName: "app-container"
ContainerPort: 3000
ビルド時に必要な環境変数があればssmのパラメータストアから渡す。ssmからも渡せる。
AWS_ACCOUNT_IDとAWS_DEFAULT_REGIONとIMAGE_NAMEに関してはcode buildの環境変数で定義している。
buildspec.yaml
version: 0.2
env:
parameter-store:
DOCKER_USER: dockerhub-user
DOCKER_TOKEN: dockerhub-token
DB_USER_NAME: db-user-name
DB_PASSWORD: db-password
DB_NAME: db-name
DB_HOST: db-host
phases:
install:
runtime-versions:
docker: 18
pre_build:
commands:
- echo Logging in to Amazon ECR...
- aws --version
## ECRにログイン
- $(aws ecr get-login --no-include-email --region ap-northeast-1)
## dockerhubにログイン。pull rate limit回避
- echo $DOCKER_TOKEN | docker login -u $DOCKER_USER --password-stdin
- REPOSITORY_URI=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_NAME}
## Dockerイメージのタグとして使用するため、ハッシュを取得
- IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | tr -dc 'a-zA-Z' | cut -c 1-7)
## テストようの.envファイル準備
- cp ./infra/.env.testing .env.testing
build:
commands:
## Dockerイメージのビルド
- echo Build started on `date`
- echo Building the Docker image...
- docker build -f infra/node/Dockerfile -t $REPOSITORY_URI:latest .
- docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
# NOTE: execute migration
# - docker run --rm -e DB_USER_NAME -e DB_PASSWORD -e DB_NAME -e DB_NAME -e DB_HOST ${REPOSITORY_URI}:${IMAGE_TAG} migration command
post_build:
commands:
## DockerイメージのECRへのプッシュ
- echo Build completed on `date`
- echo Pushing the Docker images...
- docker push $REPOSITORY_URI:latest
- docker push $REPOSITORY_URI:$IMAGE_TAG
- echo Writing image definitions file...
# blue/greenデプロイ出ない場合はこちらのjsonが必要
# - echo "[{\"name\":\"${APP_NAME}\",\"imageUri\":\"${REPOSITORY_URI}:${IMAGE_TAG}\"}]" > imagedefinitions.json
- printf '{"Version":"1.0","ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
artifacts:
## buildの最後で作成したファイルをアーティファクトとして流す
## blue/green deployの場合はimageDetail.jsonが必要
files:
- imageDetail.json
タスク定義の雛形。codepipelineのデプロイフェーズでblue/greenデプロイを選択したときに実行される。
環境変数をS3にファイルとして保存して渡している。
taskdef.json
{
"executionRoleArn": "arn:aws:iam::MY_ACCOUNT_ROLE:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "app-container",
"image": "<IMAGE1_NAME>",
"essential": true,
"environmentFiles": [
{
"value": "MY_S3_BUCKET_NAME/.env",
"type": "s3"
}
],
"healthCheck": {
"retries": 10,
"command": [
"CMD-SHELL",
"curl -f http://localhost:3000/healthcheck || exit 1"
],
"timeout": 30,
"interval": 10,
"startPeriod": 60
},
"logConfiguration": {
"logDriver": "awslogs",
"secretOptions": null,
"options": {
"awslogs-group": "/ecs/stg-app",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "ecs"
}
},
"portMappings": [
{
"hostPort": 3000,
"protocol": "tcp",
"containerPort": 3000
}
]
}
],
"requiresCompatibilities": [
"FARGATE"
],
"networkMode": "awsvpc",
"cpu": "256",
"memory": "512",
"family": "app",
"taskRoleArn": "arn:aws:iam::MY_ACCOUNT_ID:role/myTaskRole"
}
問題点
このレポジトリをそのまま本番用のアカウントでECSを立てようとすると次の部分でつまずく
- taskdef.jsonのハードコーディングされた設定が本番では適用できない
- taskRoleArn
- logConfiguration
- environmentFiles
- executionRoleArn
appspec.yamlのコンテナ名は本番環境と開発環境で同じ名前にすると差異が吸収できるがtaskdef.jsonの内容は他に対応が必要そう。
おさらい
以下の手順を自動化するためにCodePipelineを利用する
最初にECS上にサービスクラスターを構築
- ローカルでDockerイメージを作成しECRにpush
- タスク定義ファイルを作成し、先程登録されたレポジトリを指定。この際にhealthcheckの設定とか環境変数の設定とかを同時に記述する
- サービスクラスター上のタスク定義ファイルを最新のものに変更
自動化するにあたって以下3つのファイルが必要になる
- buildspec.yaml__CodeBuildステージにおいて上記1を実現する。
- appspec.yaml__CodeDeployステージにおいて上記2,3を実現する。
- taskdef.json__CodeDeployステージにおいてタスク定義ファイルを作成するさいに利用される雛形ファイル
問題点に対応するためにtaskdef.jsonに環境変数的に外部から値を注入できればよさげ。
以下の記事によるとそもそも構築に2通りの方法があるみたい。
2つめのほうほうは完全にCloudFormation管理していないと使えないのでなし。
taskdef.jsonのプレースホルダーにカスタムな値を利用できないか調べてみる。
CodePipelineのDeployステージにて以下のPlaceholder text in the task definition
の項目にカスタムな値いれればいけるか検証
ひとまず、
appsepc.prod.yaml
taskdef.prod.yaml
みたいに複数ファイルで管理はしたくない。
ちょっと調べてみたけどプレースホルダにカスタムな値を埋め込むみたいなのがない。
他の選択肢としてはBuildステージでsedコマンドを使う方法がある
もしかして以下の記事のようにTerraformに任せたほうがよさげかも?
結論
Terraform化している場合は、Terraformにtaskdef.jsonのアカウントごとの差分を吸収させる
Terraform化していない場合は、環境ごとのファイルを作成してCodePipelineでファイル名を選択する
buildspec.stg.yaml
appspec.stg.yaml
taskdef.stg.json
buildspec.prod.yaml
appspec.prod.yaml
taskdef.prod.json
もしくはBuildステージでsedコマンドを使いtaskdef.jsonの内容を環境変数に置き換える
上記サイトで以下のような問答があった
Q「タスク定義はIaCで管理するのが良いんでしょうか、GithubやCodecommitにのせて、パイプラインで更新するみたいなことは出来るんでしょうか?」
オススメとしては、TerraformのLifeCycleでイメージURIの変更を無視する方法です。さらに組み合わせて、本日のセッションで説明した、CodeDeployに含ませるtaskdef.jsonの雛形機能や、GitHub Actionsの機能を使えば、IaCで管理するECSタスク定義をデプロイとTerraformの両方で併用可能です。
タスク定義はterraformで管理するにしてもLifeCycleによってイメージURIを無視という方法。
とりあえずbuildspec.yamlとappspec.yamlはなるべく開発と本番同じ値にして、taskdef.jsonを本番だけtaskdef.prod.jsonとした。
CodePipelineでtaskdef.prod.jsonを見るように変更。
ここらへんterraformを本格導入したらうまいこと管理できるようにしたい。
環境変数は結局S3で管理するよりもssmのパラメータストアで管理するほうがいいなとなった。理由は、S3だとダウンロードして編集してアップロードしてと、変更がとにかくしにくいから
こちらを参考に、taskdef.jsonとtaskdef.prod.jsonを変更する
なんかへんに検索上位にあがってしまってるからちゃんと記事化しようかな。。