はじめに

今回は初めてCircleCIを利用することになりました。
一番大きな要件として、フォルダごとが1サービスとなっているため、サービスごとにCI/CDが走る必要がありました。
1フォルダの変更で全てのフォルダにCI/CDが適用されると不要なCI/CDが走ることになるためです。

今回は1リポジトリの中に2つのフォルダがあり、それらを別々のFargateへデプロイすることを想定しています。

実装の概要

この実装では、下記2つの設定ファイルを使用します:

  1. .circleci/config.yml:メインの設定ファイル
  2. .circleci/continue-config.yml:条件に応じて実行されるワークフロー

CircleCIが用意してくれているOrbであるpath-filtering を利用します。

メイン設定ファイル(config.yml)

version: 2.1

orbs:
  aws-cli: circleci/aws-cli@4.1.3
  aws-ecr: circleci/aws-ecr@9.1.0
  aws-ecs: circleci/aws-ecs@4.0.0
  path-filtering: circleci/path-filtering@1.0.0

executors:
  aws-executor:
    docker:
      - image: cimg/python:3.10

setup: true

workflows:
  version: 2
  deploy:
    jobs:
      - path-filtering/filter:
          name: check-updated-files-dev
          mapping: |
            service_A/.* run-service_A-workflow true
            service_B/.* run-service_B-workflow true
          base-revision: << pipeline.git.base_revision >>
          config-path: .circleci/continue-config.yml
          filters:
            branches:
              only: develop

........(環境設定が違うだけで同じため省略)

このファイルでは以下の重要な点があります:

  1. path-filtering Orbを使用して、変更されたファイルのパスを検出します。
  2. 環境ごと(dev, stg, prod)に異なるジョブを設定し、それぞれのブランチに対応させています。
  3. mappingセクションで、特定のフォルダの変更が検出された場合にパラメータを設定します。

継続設定ファイル(continue-config.yml)の解説

version: 2.1

orbs:
  aws-cli: circleci/aws-cli@4.1.3
  aws-ecr: circleci/aws-ecr@9.1.0
  aws-ecs: circleci/aws-ecs@4.0.0

executors:
  aws-executor:
    docker:
      - image: cimg/python:3.10

parameters:
  run-service_A-workflow:
    type: boolean
    default: false
  run-service_B-workflow:
    type: boolean
    default: false

jobs:
  build_service_A:
    executor: aws-executor
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Set Build Tag
          command: |
            export BUILD_TAG=$(TZ=Asia/Tokyo date +"${ENV}_%Y%m%d-%H%M%S")
            echo "export BUILD_TAG=$BUILD_TAG" >> $BASH_ENV
      - aws-ecr/build_and_push_image:
          auth:
            - aws-cli/setup:
                role_arn: $ROLE_ARN
          region: $AWS_REGION
          repo: $SERVICE_A_ECR_REPOSITORY
          tag: $BUILD_TAG
          platform: linux/arm64
          extra_build_args: "--provenance=false"
          path: ./service_A
      - aws-cli/setup:
          role_arn: $ROLE_ARN
      - aws-ecs/update_service:
          family: $SERVICE_A_ECS_TASK
          service_name: $SERVICE_A_ECS_SERVICE
          cluster: $SERVICE_ECS_CLUSTER
          container_image_name_updates: "container=${SERVICE_A_ECS_CONTAINER},tag=${BUILD_TAG}"

  build_serivce_B:
    executor: aws-executor
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Set Build Tag
          command: |
            export BUILD_TAG=$(TZ=Asia/Tokyo date +"${ENV}_%Y%m%d-%H%M%S")
            echo "export BUILD_TAG=$BUILD_TAG" >> $BASH_ENV
      - aws-ecr/build_and_push_image:
          auth:
            - aws-cli/setup:
                role_arn: $ROLE_ARN
          region: $AWS_REGION
          repo: $SERVICE_B_ECR_REPOSITORY
          tag: $BUILD_TAG
          platform: linux/arm64
          extra_build_args: "--provenance=false"
          path: ./service_B
      - aws-cli/setup:
          role_arn: $ROLE_ARN
      - aws-ecs/update_service:
          family: $SERVICE_B_ECS_TASK
          service_name: $SERVICE_B_ECS_SERVICE
          cluster: $SERVICE_B_ECS_CLUSTER
          container_image_name_updates: "container=${SERVICE_B_ECS_CONTAINER},tag=${BUILD_TAG}"

workflows:
  service-A-updated-dev:
    when:
      and:
        - equal: [ true, << pipeline.parameters.run-service_A-workflow >> ]
        - equal: [ develop, << pipeline.git.branch >> ]
    jobs:
      - build_and_deploy_serivce_A:
          context: service_context

  service-B-updated-dev:
    when:
      and:
        - equal: [ true, << pipeline.parameters.run-service_B-workflow >> ]
        - equal: [ develop, << pipeline.git.branch >> ]
    jobs:
      - build_and_deploy_service_B:
          context: service-context

 ........(環境設定が違うだけで同じため省略)

このファイルの主なポイントは:

  1. parametersセクションで、フォルダごとのワークフロー実行フラグを定義しています。
  2. jobsセクションで、実際のビルドとデプロイのステップを定義しています。
  3. workflowsセクションで、条件に応じたジョブの実行を設定しています。

フォルダごとのデプロイの仕組み

  1. config.ymlpath-filtering/filterジョブが変更されたファイルのパスを検出します。
  2. 検出結果に基づいて、run-service_A-workflowrun-service_B-workflowパラメータが設定されます。
  3. continue-config.ymlのワークフローが、これらのパラメータと現在のブランチに基づいて実行されます。
  4. 条件に合致した場合、対応するビルドとデプロイジョブが実行されます。

まとめ

無駄なCI/CDを走らせることなく、時間短縮も可能となり良さそうです。
同プロジェクトのLambdaの実装においてもこちらのCI/CDを適用してます。
Lambdaだとよりフォルダごとでのサービス管理を行う機会も多いので、重宝するかと思います。
フロントとバックエンドを同一リポジトリ管理している小規模プロジェクトなどでも良さそうですね。

参考文献

CircleCI公式ドキュメント
CircleCIで設定ファイルを分割してpath-filteringも適用する方法