AWS EC2環境にCircleCI＋CodeDeployを導入した時の記録

この記事はウェブクルー Advent Calendar 2023 19日目の記事です。
昨日は@wc-kadowaki さんの「[Nuxt3のproxyについて](https://qiita.com/wc-kadowaki/items/51273ce053fe2b6ba27c)」でした。

# 概要
AWS　EC2で立てているサーバにCircleCIを導入しました。
対象アプリケーションのシステム構成概要↓

サーバ環境:
フロントエンドサーバ: 2台（AWS EC2）
バックエンドサーバ: 2台（AWS EC2）
ソースコード管理:
Github（単一リポジトリ）

各サーバはEC2の内部でDockerコンテナを立てています（ECS on EC2ではない）。
そんな構成なので、CircleCIでCI/CDをトリガーしてその後CodeDeployでサービスをデプロイする流れでCI/CD環境を作成しました。
この構成だとCodeDeployの設定でハマったので、その時に詰まった部分とその対応についての記録。

## 利用サービス
### CircleCI
SaaSのCICDサービスで、GithubなどのVCSと連携可能。pushしただけでビルド～デプロイしてくれるし自動テストもできます。
いろんな環境に対応しやすいように、公式がライブラリ（[Orb](https://circleci.com/developer/ja/orbs)）を提供してくれています。

### CodeDeploy
AWSでもCircleCIと似たようなサービスとしてCodePipelineを提供しています。CodeDeployはそのサービスの一部で、名前の通りAWS環境でデプロイを実施してくれるもので、デプロイ対象はECS、EC2、Lambdaなどです。
https://aws.amazon.com/jp/codepipeline/
CircleCIはAWS環境にデプロイするためにAWSのサービスと連携するOrbをいくつか提供しています。そのうちの一つがCodeDeployを利用するものです。

CircleCIとCodeDeployの詳しい説明についてはすでにたくさん記事があるので割愛。

## はまったポイント
1つのリポジトリでフロント・バックエンドのソースコードを管理しており、またそれぞれのサーバが別に管理されている場合、CodeDeployで対応するのが難しかった。
CodeDeployは設定ファイルに動的な分岐を用意できない作りになっており、また環境ごとにファイルを変更したりもできない。


### 対応方法
結論として、CircleCIのダイナミックコンフィグを使って、採用するappconfig.yml（CodeDeployの設定ファイル）を動的に変更する構成にしました。
CircelCIはjobへ渡すパラメータにstepを指定できるので、その仕様を利用しました。

### 記述例
構成

- .circleci/build-deploy.yml
- .circleci/config.yml
- appspec.back.yml
- appspec.front.yml
- codedeploy/start_front.sh
- codedeploy/stop_front.sh
- codedeploy/start_back.sh 
- codedeploy/start_back.sh 

以下CircleCIの設定ファイル記述例です。CodeDeploy用のファイルは各サービスの実装構成によってだいぶ変わるので `start_front.sh` , `stop_front.sh` , `start_back.sh` , `start_back.sh`は省略します。
また、 `appspec.front.yml` も `appspec.back.yml` とほぼ同じ内容なので割愛します。

```.circleci/config.yml
version: 2.1

setup: true

orbs:
  path-filtering: circleci/path-filtering@1.0.0

parameters:
  base-revision:
    type: string
    default: "origin/master"

workflows:
  setup:
    jobs:
      - path-filtering/filter:
          base-revision: <<pipeline.parameters.base-revision>>
          config-path: ./.circleci/build-deploy.yml
          mapping: |
            Application/front/.* build-deploy-front true
            Application/api/.* build-deploy-back true
```

ほぼcodedeploy用orbの[job](https://circleci.com/developer/ja/orbs/orb/circleci/aws-code-deploy#jobs-deploy)のコピペ↓

```.circleci/build-deploy.yml
version: 2.1

orbs:
  aws-code-deploy: circleci/aws-code-deploy@3.0.0
  aws-cli: circleci/aws-cli@3.1.4

parameters:
  service-name:
    type: string
    default: service-name
  bucket-name:
    type: string
    default: myS3BucketKey
  build-deploy-app:
    type: boolean
    default: false
  build-deploy-web:
    type: boolean
    default: false


jobs: 
  deploy:  # aws-code-deploy/deployのコピペjob
    docker:
      - image: cimg/aws:2023.03
    parameters:
      application-name:
        type: string
      arguments:
        default: ''
        type: string
# ここだけ追記
      pre-command:
        default: []
        description: >
          実行前に実施してほしいコマンド
        type: steps
# 追記ここまで
      auth:
        description: >
          The authentication method used to access your AWS account. Import the
          aws-cli orb in your config and

          provide the aws-cli/setup command to authenticate with your preferred
          method. View examples for more information.
        type: steps
      bundle-bucket:
        type: string
      bundle-key:
        type: string
      bundle-source:
        default: .
        type: string
      bundle-type:
        default: zip
        type: string
      deploy-bundle-arguments:
        default: ''
        type: string
      deployment-config:
        default: CodeDeployDefault.OneAtATime
        enum:
          - CodeDeployDefault.OneAtATime
          - CodeDeployDefault.HalfAtATime
          - CodeDeployDefault.AllAtOnce
        type: enum
      deployment-group:
        type: string
      get-deployment-group-arguments:
        default: ''
        type: string
      profile-name:
        default: default
        type: string
      region:
        default: ${AWS_DEFAULT_REGION}
        description: >
          AWS region of CodeDeploy App. Defaults to environment variable
          ${AWS_DEFAULT_REGION}.
        type: string
      service-role-arn:
        description: The service role for a deployment group.
        type: string
    steps:
      - checkout
      - steps: << parameters.command >>
      - steps: << parameters.auth >>
      - aws-code-deploy/create-application:
          application-name: << parameters.application-name >>
          arguments: << parameters.arguments >>
          profile-name: << parameters.profile-name >>
          region: << parameters.region >>
      - aws-code-deploy/create-deployment-group:
          application-name: << parameters.application-name >>
          arguments: << parameters.arguments >>
          deployment-config: << parameters.deployment-config >>
          deployment-group: << parameters.deployment-group >>
          get-deployment-group-arguments: << parameters.get-deployment-group-arguments >>
          profile-name: << parameters.profile-name >>
          region: << parameters.region >>
          service-role-arn: << parameters.service-role-arn >>
      - aws-code-deploy/push-bundle:
          application-name: << parameters.application-name >>
          arguments: << parameters.arguments >>
          bundle-bucket: << parameters.bundle-bucket >>
          bundle-key: << parameters.bundle-key >>
          bundle-source: << parameters.bundle-source >>
          bundle-type: << parameters.bundle-type >>
          profile-name: << parameters.profile-name >>
          region: << parameters.region >>
      - aws-code-deploy/deploy-bundle:
          application-name: << parameters.application-name >>
          bundle-bucket: << parameters.bundle-bucket >>
          bundle-key: << parameters.bundle-key >>
          bundle-type: << parameters.bundle-type >>
          deploy-bundle-arguments: << parameters.deploy-bundle-arguments >>
          deployment-config: << parameters.deployment-config >>
          deployment-group: << parameters.deployment-group >>
          get-deployment-group-arguments: << parameters.get-deployment-group-arguments >>
          profile-name: << parameters.profile-name >>
          region: << parameters.region >>

workflows:
  deploy_back_application:
    when: <<pipeline.parameters.build-deploy-back>>
    jobs:
      - aws-code-deploy/deploy:
          context: aws-context
          pre-command:
            - run: cp appspec.back.yml appspec.yml
          auth:
            - aws-cli/setup:
                profile-name: CircleCI IDENTITY PROFILE
                role-arn: $ASSUME_ROLE_ARN    # aws-contextに登録している環境変数
                role-session-name: pdm-codedeploy-session
          name: <<pipeline.parameters.service-name>>-Back-App
          application-name: <<pipeline.parameters.service-name>>-Back-App
          bundle-bucket: <<pipeline.parameters.bucket-name>>
          service-role-arn: $SERVICE_ROLE_ARN
          profile-name: CircleCI IDENTITY PROFILE
          bundle-key: <<pipeline.parameters.service-name>>-Back
          deployment-group: <<pipeline.parameters.service-name>>>-Back-DepGrp
          filters:
            branches:
              only:
                - master
  deploy_front_application:
    when: <<pipeline.parameters.build-deploy-front>>
    jobs:
      - deploy:
          context: aws-context
          pre-command:
            - run: cp appspec.front.yml appspec.yml
          auth:
            - aws-cli/setup:
                profile-name: CircleCI IDENTITY PROFILE
                role-arn: $ASSUME_ROLE_ARN
                role-session-name: pdm-codedeploy-session
          name: <<pipeline.parameters.service-name>>-Front-App
          application-name: <<pipeline.parameters.service-name>>-Front-App
          bundle-bucket: <<pipeline.parameters.bucket-name>>
          service-role-arn: $SERVICE_ROLE_ARN
          profile-name: CircleCI IDENTITY PROFILE
          bundle-key: <<pipeline.parameters.service-name>>-Front
          deployment-group: <<pipeline.parameters.service-name>>-Front-DepGrp
          requires:
            - approval-deploy
          filters:
            branches:
              only:
                - master
```

```appspec.back.yml
version: 0.0
os: linux
files:
  - source: /Application/api
    destination: /home/app/api
file_exists_behavior: OVERWRITE
hooks:
  ApplicationStop:
    - location: odedeploy/stop_back.sh
      timeout: 300
      runas: root
  ApplicationStart:
    - location: codedeploy/start_back.sh
      timeout: 300
      runas: root
```

## 所感
せっかくorbで簡潔にかけるはずが、今回の構成のためにjobを定義することになってしまった点が残念でした。IaaCがデファクトスタンダードになってきている現状では、バックエンドとフロントエンドのリポジトリを分けるべきですね。

## 参考
https://circleci.com/docs/ja/contexts/
https://circleci.com/developer/ja/orbs/orb/circleci/aws-code-deploy
https://circleci.com/docs/ja/dynamic-config/
https://docs.aws.amazon.com/codedeploy/latest/userguide/tutorials-wordpress.html
https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-example.html#appspec-file-example-server

明日は @wc-fukuda さんの投稿です。