🎄

[Day.05] AWS Fargate への CircleCI(等)による全自動/半自動デプロイ

2021/12/05に公開

以下の5日目のエントリです。

2021年の年明けから Fargate に取り組み、3月・5月に運用され始め、問題起きては改善していったた我がCI/CDのおさらい的な記事です。
土台情報をまず並べておきます。

3->4->8->10月 に公開した 4記事

3月 Laravelプロダクト Fargate化への道
https://zenn.dev/sogaoh/articles/21-03-27-def9b519527b3d417b4d

4月 とある Blue/Green Deployment 構築・運用案(ecspresso + CodeDeploy)
https://zenn.dev/sogaoh/articles/21-04-15-61afc6904b9c21ff9c7b

8月 思い立って70分で tfenv の CircleCI Orb を作った
https://zenn.dev/sogaoh/articles/21-08-27-5f56bcf3a7348a

10月 Bitbucket Pipelines から CD の一部を CodeBuild へ移行した
https://zenn.dev/sogaoh/articles/21-10-02-7b0e74b38758ba

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 に移行

詳しくは ↓ へ
https://zenn.dev/sogaoh/articles/21-10-02-7b0e74b38758ba

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する際の手順のところの 2. のスクリプトを ecr-update, ecs-deploy, (ecs-rollback) のパートごとにパラメーターを指定して実行する、という感じのことです。

脚注
  1. 4日目は・・・12/5 10:30 時点で未定です。 ↩︎

  2. 4日目は @_y_ohgi さんの ECR からDocker Hub のオフィシャルイメージを使用する でした。 ↩︎

  3. 4日目は @bufferings さんの Spring Boot プロジェクトの自動テストを CircleCI で始めるための2ステップ でした。 ↩︎

  4. refs https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_image docker-compose.yml では記述できる build コンテキストは Fargate では利用できない、と自分は判断しています。 ↩︎

  5. refs https://support.atlassian.com/ja/bitbucket-cloud/docs/debug-pipelines-locally-with-docker/ ↩︎

  6. refs https://circleci.com/docs/ja/2.0/api-intro/ ↩︎

  7. このために作られたのが、sogaoh/orb-tfenv、ソースは https://github.com/sogaoh/orb-tfenv ↩︎

  8. Author of ecspresso ↩︎

Discussion