[Day.05] AWS Fargate への CircleCI(等)による全自動/半自動デプロイ
以下の5日目のエントリです。
- CI/CD Advent Calendar 2021 [1]
- AWS Containers Advent Calendar 2021 [2]
- CircleCI Advent Calendar 2021 [3]
2021年の年明けから Fargate に取り組み、3月・5月に運用され始め、問題起きては改善していったた我がCI/CDのおさらい的な記事です。
土台情報をまず並べておきます。
3->4->8->10月 に公開した 4記事
3月 Laravelプロダクト Fargate化への道
4月 とある Blue/Green Deployment 構築・運用案(ecspresso + CodeDeploy)
8月 思い立って70分で tfenv の CircleCI Orb を作った
10月 Bitbucket Pipelines から CD の一部を CodeBuild へ移行した
Fargate を CI/CD する際のだいたいの手順
「Laravel のプロダクト」という前提ですが、以下になると自分は理解しています。
1. 環境構築
1.1. アプリケーションをイメージ化して[4]ローカルで動かす
1.2. (Terraformで)インフラ構築(ECS は cluster まででOK)
1.3. ecspresso で ECS サービス・タスク定義構築
1.4. Blue/Green Deployment 設定 (AWS CodeDeploy)
2. CI/CDスクリプト整備・検証
(以下は内容の概要)
2.1. ローカル環境相当の環境を自動構築(docker-compose up -d)して自動で Unit/Feature/E2E Test
- 十分ではないものの、リグレッションテストの位置付け
2.2. ECR に最新のコードをベースにしたイメージの push
2.3. ecspresso を利用した deploy (ECS サービス・タスク定義更新)
- 最終段階で DB migration を自動実行
2.4. 万一に備えて、rollback スクリプトも用意
- 構築初期には検証したが、運用に入って以降使われることがなく、使うのが不安になりつつある。たまに訓練が必要そう。
3. CI/CDツールにスクリプトを搭載
3.1. CI/CDを実行する docker イメージの整備
- CircleCI は Orb を利用すると便利。最近始めたばかりですが GitHub Actions も類似してそうに感じています。
- Bitbucket Pipelines は Dockerfile で諸々をインストールするスクリプトを作ることになりそう(ベターなというか楽な方法があるなら教えてほしい)。
3.2. 2.のスクリプトを実装
3.3. 可能であればローカルで検証
- CircleCI には CLIツール、GitHub Actions には act があってある程度検証可能。
- BitBucket Pipelines では、自分の利用している機構においては Docker in Docker のさらにその中で docker-compose up という感じになり、「やれない」[5]
(Pipelines で実行してみる、しかないと思っている)
CI/CD を「拡張」する
開発者主体でデプロイできるように、そして、現場のニーズに応じて、少しずつ拡張していきました。
API 呼び出しからの実行から全自動へ
CircleCI に限らないかと思うのですが、初期の頃は API を叩いて[6]のデプロイで運用していました。
しかしこれだと自分以外の人が実行したいときにトークンが必要になり、それを教えるのはどうなのか?(いいわけない)となるので、特定ブランチへの push をトリガーに全自動でデプロイが走るようにしました。
テストカバレッジレポートを Artifact に保存するように
CircleCI は直接ブラウザで Artifact からレポートが見れるようにできました。
Bitbucket Pipelines、GitHub Actions はダウンロードすれば確認できます。
ECS Schedule Task の設定更新時に ecschedule apply ができるように準備
変更頻度が高くないので ecschedule apply の実行はローカルでコマンドから、になっていますが、機構として Terraform の tfstate を参照しているなど見えない変動要素があるので、定期的に --dry-run のシンタックスチェックを行うようにしておきました。
リリース中は「メンテナンスモード」とする機構
ECS のローリングアップデート・Blue/Green Deployment 共に停止時間は発生しないはずになっているのですが、まあ念の為というのと「やってますよ」を明示する意図で機構の整備をしました。Appendix に .circleci/config.yml のサンプルを載せています(一部、XXXXX
で伏せています)
実はこの機構、terraform apply でALBのリスナールールを「差し込む」[7]、terraform-provider-mackerel を利用して外形監視アラートをリリース中だけ mute する、といった、ややがんばったことをしてたりします。
Vue.js のビルドがメモリ不足で失敗するので CodeBuild に移行
詳しくは ↓ へ
Appendix
自動でメンテナンスモードを開始終了する CircleCI ワークフロー(APIから呼び出す)
version: 2.1
orbs:
aws-ecr: circleci/aws-ecr@6.15.3
ecspresso: fujiwara/ecspresso@0.0.15
ecschedule: sogaoh/orb-ecschedule@0.0.8
tfenv: sogaoh/orb-tfenv@0.0.1
slack : circleci/slack@3.4.2
anchors:
- node_version: &node_version
NODE_VERSION: v14.XX.X
- install_ecspresso: &install_ecspresso
ecspresso/install:
version: v1.7.0
- terraform_version: &terraform_version
TERRAFORM_VERSION: 1.0.10
- install_terraform: &install_terraform
run:
name: Install terraform & Show Version
command: |
tfenv install ${TERRAFORM_VERSION}
tfenv use ${TERRAFORM_VERSION}
terraform --version
- download_maintenance_envrc: &download_maintenance_envrc
run:
name: Download .envrc for ecs/maintenance
command: |
cd infrastructures/environments/<< pipeline.parameters.environment >>/ecs/maintenance
aws s3 cp s3://XXXXX-envrc-store/environments/<< pipeline.parameters.environment >>/ecs/.envrc ./.envrc
- download_maintenance_envrc_ar: &download_maintenance_envrc_ar
run:
name: Download .envrc for ecs/maintenance
command: |
cd infrastructures/environments/<< parameters.environment >>/ecs/maintenance
aws s3 cp s3://XXXXX-envrc-store/environments/<< parameters.environment >>/ecs/.envrc ./.envrc
- scale1_maintenance: &scale1_maintenance
run:
name: Maintenance Service set desiredCount 1
command: |
cd infrastructures/environments/<< pipeline.parameters.environment >>/ecs/maintenance
direnv allow . && eval "$(direnv export bash)"
make verify
make scale1
- scale1_maintenance_ar: &scale1_maintenance_ar
run:
name: Maintenance Service set desiredCount 1
command: |
cd infrastructures/environments/<< parameters.environment >>/ecs/maintenance
direnv allow . && eval "$(direnv export bash)"
make verify
make scale1
- scale0_maintenance: &scale0_maintenance
run:
name: Maintenance Service set desiredCount 0
command: |
cd infrastructures/environments/<< pipeline.parameters.environment >>/ecs/maintenance
direnv allow . && eval "$(direnv export bash)"
make verify
make scale0
- scale0_maintenance_ar: &scale0_maintenance_ar
run:
name: Maintenance Service set desiredCount 0
command: |
cd infrastructures/environments/<< parameters.environment >>/ecs/maintenance
direnv allow . && eval "$(direnv export bash)"
make verify
make scale0
- download_maintenance_tfvars: &download_maintenance_tfvars
run:
name: Download terraform.tfvars for alb_listener_rule_maintenance
command: |
cd infrastructures/environments/<< pipeline.parameters.environment >>/alb_listener_rule_maintenance
aws s3 cp s3://XXXXX-tf-store/<< pipeline.parameters.environment >>/alb_listener_rule_maintenance/terraform.tfvars ./terraform.tfvars
- download_maintenance_tfvars_ar: &download_maintenance_tfvars_ar
run:
name: Download terraform.tfvars for alb_listener_rule_maintenance
command: |
cd infrastructures/environments/<< parameters.environment >>/alb_listener_rule_maintenance
aws s3 cp s3://XXXXX-tf-store/<< parameters.environment >>/alb_listener_rule_maintenance/terraform.tfvars ./terraform.tfvars
- insert_alb_maintenance_rule: &insert_alb_maintenance_rule
run:
name: Insert Maintenance ALB Listener Rule
command: |
cd infrastructures/environments/<< pipeline.parameters.environment >>/alb_listener_rule_maintenance
terraform init
terraform plan
terraform apply -auto-approve
- insert_alb_maintenance_rule_ar: &insert_alb_maintenance_rule_ar
run:
name: Insert Maintenance ALB Listener Rule
command: |
cd infrastructures/environments/<< parameters.environment >>/alb_listener_rule_maintenance
terraform init
terraform plan
terraform apply -auto-approve
- delete_alb_maintenance_rule: &delete_alb_maintenance_rule
run:
name: Delete Maintenance ALB Listener Rule
command: |
cd infrastructures/environments/<< pipeline.parameters.environment >>/alb_listener_rule_maintenance
terraform init
terraform plan -destroy
terraform apply -destroy -auto-approve
- delete_alb_maintenance_rule_ar: &delete_alb_maintenance_rule_ar
run:
name: Delete Maintenance ALB Listener Rule
command: |
cd infrastructures/environments/<< parameters.environment >>/alb_listener_rule_maintenance
terraform init
terraform plan -destroy
terraform apply -destroy -auto-approve
- download_monitor_mute_on_off_tfvars: &download_monitor_mute_on_off_tfvars
run:
name: Download terraform.tfvars for monitor mute on/off
command: |
cd surroundings/mackerel/external-monitor/<< pipeline.parameters.environment >>
aws s3 cp s3://XXXXX-tf-store/mackerel/external-monitor/terraform.tfvars ./terraform.tfvars
- download_monitor_mute_on_off_tfvars_ar: &download_monitor_mute_on_off_tfvars_ar
run:
name: Download terraform.tfvars for monitor mute on/off
command: |
cd surroundings/mackerel/external-monitor/<< parameters.environment >>
aws s3 cp s3://XXXXX-tf-store/mackerel/external-monitor/terraform.tfvars ./terraform.tfvars
- change_monitor_mute_on: &change_monitor_mute_on
run:
name: Change External Monitor is_mute ON
command: |
cd surroundings/mackerel/external-monitor/<< pipeline.parameters.environment >>
terraform init
terraform plan -var 'is_mute=true'
terraform apply -auto-approve -var 'is_mute=true'
- change_monitor_mute_on_ar: &change_monitor_mute_on_ar
run:
name: Change External Monitor is_mute ON
command: |
cd surroundings/mackerel/external-monitor/<< parameters.environment >>
terraform init
terraform plan -var 'is_mute=true'
terraform apply -auto-approve -var 'is_mute=true'
- change_monitor_mute_off: &change_monitor_mute_off
run:
name: Change External Monitor is_mute OFF
command: |
cd surroundings/mackerel/external-monitor/<< pipeline.parameters.environment >>
terraform init
terraform plan -var 'is_mute=false'
terraform apply -auto-approve -var 'is_mute=false'
- change_monitor_mute_off_ar: &change_monitor_mute_off_ar
run:
name: Change External Monitor is_mute OFF
command: |
cd surroundings/mackerel/external-monitor/<< parameters.environment >>
terraform init
terraform plan -var 'is_mute=false'
terraform apply -auto-approve -var 'is_mute=false'
executors:
amzn2:
docker:
- image: XXXXX/builder-amzn2:latest
auth:
username: $DOCKER_LOGIN
password: $DOCKER_PWD
parameters:
branch:
type: string
default: development
environment:
type: string
default: development
# Job Trigger: execute by API v2
maintenance_start:
type: boolean
default: false
maintenance_end:
type: boolean
default: false
workflows:
version: 2
maintenance-start:
when: << pipeline.parameters.maintenance_start >>
jobs:
- confirm_begin-maintenance:
type: approval
filters:
branches:
only:
- develop
- begin-maintenance:
requires:
- confirm_begin-maintenance
maintenance-end:
when: << pipeline.parameters.maintenance_end >>
jobs:
- confirm_finish-maintenance:
type: approval
filters:
branches:
only:
- develop
- finish-maintenance:
requires:
- confirm_finish-maintenance
commands:
start-notify:
steps:
- slack/notify:
title: "${MARK_S}"
color: '#FFC300'
message: "\n
:neutral_face: ${CIRCLE_USERNAME} :evergreen_tree: ${CIRCLE_BRANCH} \n
Job: ${CIRCLE_JOB} \n"
webhook: "${SLACK_ENDPOINT}"
end-notify:
steps:
- slack/status:
fail_only: true
failure_message: "\n
:neutral_face: ${CIRCLE_USERNAME} :evergreen_tree: ${CIRCLE_BRANCH} \n
Workflow: https://circleci.com/workflow-run/${CIRCLE_WORKFLOW_ID} \n
Job: ${CIRCLE_JOB} \n
Build URL: ${CIRCLE_BUILD_URL} \n"
webhook: "${SLACK_ENDPOINT}"
- slack/notify:
title: "${MARK_E}"
color: '#42f486'
message: "\n
:neutral_face: ${CIRCLE_USERNAME} :evergreen_tree: ${CIRCLE_BRANCH} \n
Workflow: https://circleci.com/workflow-run/${CIRCLE_WORKFLOW_ID} \n
Job: ${CIRCLE_JOB} \n
Build URL: ${CIRCLE_BUILD_URL} \n"
webhook: "${SLACK_ENDPOINT}"
jobs:
begin-maintenance:
executor:
name: amzn2
environment:
<<: *terraform_version
MARK_S: ":earth_asia: ENV : << pipeline.parameters.environment >>"
MARK_E: ":hammer_and_wrench: ENV : << pipeline.parameters.environment >>"
steps:
- start-notify
- checkout
- setup_remote_docker:
docker_layer_caching: false
- <<: *install_ecspresso
- <<: *download_maintenance_envrc
- <<: *scale1_maintenance
- tfenv/install
- <<: *install_terraform
- <<: *download_monitor_mute_on_off_tfvars
- <<: *change_monitor_mute_on
- <<: *download_maintenance_tfvars
- <<: *insert_alb_maintenance_rule
- end-notify
finish-maintenance:
executor:
name: amzn2
environment:
<<: *terraform_version
MARK_S: ":hammer_and_wrench: ENV : << pipeline.parameters.environment >>"
MARK_E: ":earth_asia: ENV : << pipeline.parameters.environment >>"
steps:
- start-notify
- checkout
- setup_remote_docker:
docker_layer_caching: false
- tfenv/install
- <<: *install_terraform
- <<: *download_maintenance_tfvars
- <<: *delete_alb_maintenance_rule
- <<: *download_monitor_mute_on_off_tfvars
- <<: *change_monitor_mute_off
- <<: *install_ecspresso
- <<: *download_maintenance_envrc
- <<: *scale0_maintenance
- end-notify
さいごに
最後まで読んでいただきありがとうございました。
来年は GitHub Actions や GitLab CI にも手を出しそうなので、知見となりそうな事例を経験できたら記事にできればと思っています。
良き1年となることを願い。
明日6日目の予定は以下になっています。それぞれお楽しみに。
- CI/CD : 「Bitbucket Pipelineについて何か」 by @Nerosui777 さん
- AWS Containers : 「ECS task tracer」 by @fujiwara さん ・・・おっ[8]
- CircleCI : 「CircleCI の Concurrency と Parallelism」 by @srz_zumix さん
そういえば「半自動」に全くふれていなかったのですが、CI/CDする際の手順のところの 2. のスクリプトを ecr-update
, ecs-deploy
, (ecs-rollback)
のパートごとにパラメーターを指定して実行する、という感じのことです。
-
4日目は・・・12/5 10:30 時点で未定です。 ↩︎
-
4日目は @_y_ohgi さんの ECR からDocker Hub のオフィシャルイメージを使用する でした。 ↩︎
-
4日目は @bufferings さんの Spring Boot プロジェクトの自動テストを CircleCI で始めるための2ステップ でした。 ↩︎
-
refs https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_image docker-compose.yml では記述できる build コンテキストは Fargate では利用できない、と自分は判断しています。 ↩︎
-
refs https://support.atlassian.com/ja/bitbucket-cloud/docs/debug-pipelines-locally-with-docker/ ↩︎
-
このために作られたのが、sogaoh/orb-tfenv、ソースは https://github.com/sogaoh/orb-tfenv ↩︎
Discussion