2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社クライドAdvent Calendar 2023

Day 25

【CI/CD】複数リポジトリでCircleCIのコードを共有する

Last updated at Posted at 2023-12-31

はじめに

皆さんは、CI/CDのコードを実装している時に、「このコード他のリポジトリのものとやってることはほぼ同じなんだよな」や、実装の変更時に「同じ変更を他の全てのリポジトリでやることが億劫だな」と思ったことはありませんか?

この記事では、CircleCiのコードを複数リポジトリにまたがって、共通化する方法について述べています。通常であれば、各リポジトリごとに実装しなくてはならないCI/CDのコードを一括管理することによって、変更容易性を増し、保守運用をしやすくすることが目的です。
こちらの記事と同様の実装を行えば、GitHub Actionsでも同じようなことができると思います(未検証)

背景

ここでは、筆者がどのような課題に直面し、この方法を取り入れることにしたのかを話しています。結論のみをお急ぎの方は読み飛ばしてください。

弊チームのCI/CD事情

弊チームのサービス構成

私が所属しているチームでは下記5つのリポジトリを管理しています。

リポジトリ名 使用技術
Dockerリポジトリ Docker
Admin/Serviceリポジトリ PHP(7.3), JS
Service Aリポジトリ TypeScript
Service Bリポジトリ Vue.js, JS
Service Cリポジトリ PHP(8.1), Vue.js, TypeScript

動作環境として、下記の3つがありました。

  • 本番環境
  • ステージング環境
  • 海外チームのステージング環境

元のCircleCiの実装

サービスの構成をもとに実装したCircleCiの構成が下記です。
スクリーンショット 2023-12-30 13.22.10.png
こちらの実装の詳細についての説明は省きますが、まず環境ごとのYAMLファイル間で実装が酷似すること、それぞれのリポジトリで技術スタックがかなり似ているため、必然的にCDのコードもほぼ同じようなものが作られます。

  • JS, Vue, TypeScriptのbuild(buildコマンドは統一されている)
  • PHPのリリース関連コード
  • 全てのリリース後に行われる共通のキャッシュ削除
  • など。。。

さらに、実際にCDを動かし始めてから発見されるバグや軽微な修正等もほぼ全てのリポジトリで同様の修正をしなくてはならないなど、さまざまな悩みがありました。

解決策

上記の問題に対峙した時、真っ先に思い浮かんだのが実装の共通化でした。しかし、これを叶えるためには下記問題がありました。

  1. 実行されているYAMLファイルから他のファイルを参照することはできない
  2. 1つのpipelineで呼び出すことのできるファイルは最大2ファイル
    1. circleci/continuation orbを使用することによって、「setup」と「メインプロセス」の2ファイルに分けることができる

今回この問題を解決しつつ実装の共通化を行った方法は、「setup内で条件に合った最適なメインプロセスYAMLファイルを構築し、それを後続の処理として呼び出す」という方法です。

CircleCiの実装の共通化の仕組み

この章より、主題の複数リポジトリ間でCircleCiのコードを共有する内容について説明していきます。

どう共通化のするのか

CircleCiは主に6つのトップインデントから成り立っています。

名前 役割
version CircleCiのバージョン指定
parameters pipeline実行時の引数の定義
job 処理の流れを定義
workflow jobの順番を定義
executers jobの実行環境を定義
commands 処理を関数として定義、jobから呼び出される

スクリーンショット 2023-12-31 12.23.33.png

commandsで関数を共通化することができるので、job, workflowを各リポジトリ、環境ごとに定義することによって、実装の共通化ができるのではないかと考えました。

全体の構造

上記を踏まえた、全体の構造が下記です。
スクリーンショット 2023-12-31 15.38.00.png

どのようなフローで動作するかというと

  1. UserがCircleCiの管理画面からデプロイ先の環境を引数としてpipelineを実行する
  2. config.yml内で、次に実行するメインプロセスであるnext.ymlをリポジトリ名、デプロイ先の環境をもとに構築する
    1. CircleCiリポジトリをCircleCiのコンテナ内にpull
    2. リポジトリ情報や、引数のデプロイ先の環境をもとに各インデントファイルを選ぶ。
    3. 各要素を結合することによって、メインプロセスであるnext.ymlを構築
  3. config.ymlから、next.ymlを実行する
  4. next.ymlでデプロイを行う

以上のプロセスによって、CircleCiの実装は共通化され、リポジトリ、環境ごとの最適なworkflowが構築されます。
次の章からは、実際の実装内容に触れながら、より詳細について説明していきます。

CircleCiリポジトリの詳細

全体の構造の画像にあったように、CircleCiリポジトリはheader.yml, executer.yml, commands.yml, workflowディレクトリ, jobディレクトリで構成されています。

header.yml, executer.yml, commands.ymlはどのリポジトリでも共通のファイルを使用しており、workflow, job がリポジトリ、環境によって使用されるファイルが変わっていきます。

header.ymlについて

このファイルには、versionparametersが定義されています。

  • versionは、CircleCiのバージョンを表しており、これをpipelineごとに変更する理由はないので、最新の2.1を指定しています。
  • parametersに関しては、config.yml, next.yml ともに同じparameterを定義しないと、CircleCiの仕様上動作しなくなってしまうので、全てのpipelineで共通化しています。
header.yml
version: 2.1

parameters:
  env:
    type: enum
    enum: ["", prod, stg, stgOversea]
    default: ""
  repo-name:
    type: string
    default: ""

parameterのrepo-nameに関しては後述しますが、 関数の中でもリポジトリによって条件分岐する部分があったため、そのために受け取れるようにしています

executer.ymlについて

このファイルでは、jobを動かす環境(コンテナ)を定義します。ここで定義することによって、jobの中では単にここに定義されている環境を参照するだけで良くなります。

executer.yml
executors:
  node12:
    docker:
      - image: cimg/node:12.19.0
        user: root
  node16:
    docker:
      - image: cimg/node:16.13.0
        user: root
  basic:
    docker:
      - image: cimg/base:stable
        user: root

commands.ymlについて

このファイル内で、全てのリポジトリで使用される関数を全て定義します。

commands.yml
commands:
  greeting:
    description: "Standard output for incoming greetings"
    parameters:
      message:
        type:
        default: "Hello world!"
    steps:
      - run:
          name: "export greeting"
          command: echo << parameters.message >>
  deploy:
    ...

workflow/${リポジトリ名}/${環境名}.ymlについて

ここでは、各リポジトリごと、各環境ごとにファイルを書いていきます。どのレベルまで分割するかについては、workflowでやりたいことに依存するかとは思うのですが、弊チームでは環境レベルまで分割しなくてはいけなかったため、そのパターンで説明していきます。

  • リポジトリ単位で分割しなくてはいけなかった理由
    • jobがリポジトリごとに変わるため、その引数等はjobによって変化するため
  • 環境単位に分割しなくてはいけなかった理由
    • デプロイ先をworkflow側で管理しているため、stagingへのリリースか本番へのリリースかを分けたかったため
workflow/docker/prod.yml
workflows:
  deploy-to-prod-all:
    jobs:
      - deploy:
          name: deploy-to-production01
          server: "production01"
      - deploy:
          name: deploy-to-production02
          requires:
            - deploy-to-production01
          server: "production02"

job/${リポジトリ名}.ymlについて

このファイルでは、処理の流れを記載していきます。
commands.ymlで関数は全て定義されているため、それを参照する形で関数の呼ぶ順番、引数のみを定義します。

job/docker.yml
jobs:
  deploy:
    executor: basic
    working_directory: /root/workspace
    parameters:
      server:
        description: "deploy server"
        type: string
        default: ""
      main-dir:
        description: "directory where repository exists in the server"
        type: string
        default: "/home/ubuntu/docker"
    steps:
      - checkout
      - setup-container
      - when:
          condition:
            equal: ["prod", << pipeline.parameters.env >>]
          steps:
            - server-status-check:
                server: << parameters.server >>
      - container-down:
          server: << parameters.server >>
      - deploy-codes:
          server: << parameters.server >>
          main-dir: << parameters.main-dir >>
      - container-setup:
          server: << parameters.server >>
      - check-container-status:
          server: << parameters.server >>

ここでexecutor: basicに関してはexecuterで定義したubuntumコンテナであるbasicを参照しています。
steps配下に関しても、commands.yml のサンプルコード内では紹介していませんが、全てcommands.yml内で定義された関数を呼び出して処理を行なっています。

テストについて

ここまで、YAMLファイルをインデントごとにファイル分割する方法について説明してきました。ただ、YAMLファイルはもともと外部ファイルを参照しながら実装していくものではないため、IDEによる定義ジャンプなどが使えません。
また、インデントが一つずれるだけで、YAMLファイルでの意味が変わってきてしまうため、十分な注意が必要です。

ただ、これらを実装者やレビュワーにの注意によって全て防ぐことは難しいです。そのため、このCircleCiリポジトリのファイルたちが正しいCircleCiの実行ファイルを構築できるかをテストする方法についても述べてたいと思います。CI/CDのコードをCIでテストするということですね(ややこしい...)

テスト方法についてですが、CircleCi公式が提供しているCLIを用いて行います。
このCLIではCircleCiのデフォルト実行ファイルであるconfig.ymlが正しく書かれているかを確認してくれるコマンドが存在しています。

そこで、テストフローとして、

  1. config.ymlからstatic-analysis-test.ymlを呼び出す
    1. CircleCiのCLIでテストできるのは、config.ymlという名前のファイルのみのため、static-analysis-test.ymlで処理を進めつつ、config.ymlを上書きしてCLIを実行するというフローを辿る必要があります。
  2. 全リポジトリ×全環境のconfig.ymlを作成
  3. 各ファイルに対して、CLIでチェックを走らせる。

このようなフローでテストを行うことによって、実装を自動でテストすることができます。

circleci/comfig.yml
version: 2.1

setup: true

orbs:
  continuation: circleci/continuation@0.3.1

jobs:
  setup:
    executor: continuation/default
    steps:
      - checkout
      - continuation/continue:
          configuration_path: .circleci/static-analysis-test.yml

workflows:
  setup-test:
    jobs:
      - setup
.circleci/static-analysis-test.yml
version: 2.1

executors:
  base:
    docker:
      - image: cimg/base:stable
        user: root

commands:
  create-config-to-test:
    description: "create config.yaml to test"
    parameters:
      repo-name:
        description: "Repository name"
        type: enum
        enum: ["docker","adminService", "serviceA", "serviceB", "serviceC"]
      env: 
        description: "environment name"
        type: enum
        enum: ["prod", "stg", "stgOverseas"]
    steps:
      - run: 
          name: "delete config.yml if it exists"
          command: |
            #!/bin/bash

            file="/root/workspace/.circleci/config.yml"
            if [ -e ${file} ]; then
              rm -rf ${file}
            fi
      - run: 
          name: "create config.yaml to test"
          command: |
            #!/bin/bash
            
            header="header.yml"
            jobs="jobs/<< parameters.repo-name >>.yml"
            workflow="workflow/<< parameters.repo-name >>/<< parameters.env >>.yml"
            commands="commands.yml"
            executors="executors.yml"

            cd /root/workspace/
            cat ${header} ${jobs} ${workflow} ${commands} ${executors} > /root/workspace/.circleci/config.yml
      - run: 
          name: "Check the contents of the created config.yml"
          command: |
            #!/bin/bash

            file=".circleci/config.yml"
            cd /root/workspace/
            cat ${file}
  install-circleci-cli:
    description: "install CircleCi CLI to execute static analysis test"
    steps:
      - run:
          name: "install CircleCi CLI by using curl"
          command: curl -fLSs https://circle.ci/cli | bash
  execute-static-analysis-test:
    description: "execute static analysis test by using command of CircleCi CLI"
    steps: 
      - run:
          name: "execute command"
          command: circleci config validate

jobs:
  test:
    executor: base
    working_directory: /root/workspace
    parameters: 
      repo-name:
        description: "Repository name"
        type: enum
        enum: ["docker","adminService", "serviceA", "serviceB", "serviceC"]
      env: 
        description: "environment name"
        type: enum
        enum: ["prod", "stg","stgOversea"]
    steps:
      - checkout
      - create-config-to-test:
          repo-name: << parameters.repo-name >>
          env: << parameters.env >>
      - install-circleci-cli
      - execute-static-analysis-test

workflows:
  docker-test:
    jobs:
      - test:
          name: "prod test"
          repo-name: "docker"
          env: "prod"
      - test:
          name: "staging test"
          repo-name: "docker"
          env: "stg"
      - test:
          name: "oversea staging test"
          repo-name: "docker"
          env: "stgOversea"
  adminService-test:
    jobs:
      - test:
          name: "prod test"
          repo-name: "adminService"
          env: "prod"
      - test:
          name: "staging test"
          repo-name: "adminService"
          env: "stg"
      - test:
          name: "oversea staging test"
          repo-name: "adminService"
          env: "stgOversea"
  serviceA-test:
    jobs:
      ...

Circleciリポジトリのまとめ

この章では、CircleCiリポジトリがどのように全体像の説明から、各ファイルについて説明、最後にそのテストについて説明してきました。
改めてこの章で話したことのまとめとしては、

  • CircleCiのトップインデントごとにファイルを分割する
    • version, parametersは全ファイル共通で定義する
    • executersでは、jobの動作環境を定義する
    • commandsでは全jobの関数を定義する
    • workflow, jobはリポジトリごと、必要であれば 環境ごとに定義する
  • 構築されたメインプロセスが正しいかどうかを確認するために、CircleCiのCLIを用いて自動テストを行う。

次の章では、各リポジトリでどのようにメインプロセスのYAMLファイルを構築し、呼び出すのかについて説明します。

各リポジトリの実装

ここからは、各リポジトリで実装する内容について説明していきます。
各リポジトリで行わなくてはいけないことは下記3点になります。

  1. CircleCiリポジトリのpull
  2. メインプロセスであるnext.ymlの構築
  3. next.ymlを後続の処理として実行

ディレクトリ構成

全てのリポジトリでディレクトリ構成は一緒になります。

.
├── config
│   └── ssh_config
├── config.yml
└── next.yml
  • ssh_congfig
    • CircleCiリポジトリはGitHubからpullしてくるため、Githubへ接続するためのsshの設定をここに書きます
  • config.yml
    • ここで全ての処理を定義します
  • next.yml
    • CirclrCiのjob内でファイルを作成、後続の処理としての呼び出しができなかったため、ファイルだけ作成しています。

各プロセス詳細

各リポジトリの実装にて触れましたが、config.ymlでは「CircleCiリポジトリのpull」, 「メインプロセスであるnext.ymlの構築」, 「next.ymlを後続の処理として実行」の3つのプロセスがあります。

CircleCiリポジトリのpull

まずは、CircleCiリポジトリのpullの説明から始めます。
GitHubのリポジトリをpullするには、まずGitHubと接続できる状態にする必要があるので、CircleCiの設定ページでGitHubに接続する用のSSHキーを登録します。

その後、config/ssh_configファイル内でGitHubと接続するためのSSHの設定を行います

ssh_config
Host github.com
    HostName ssh.github.com
    User git
    port 443
    identityFile /root/.ssh/id_rsa_${finger_print}
    Compression yes
    TCPKeepAlive yes
    IdentitiesOnly yes
    StrictHostKeyChecking no

${finger_print}は登録されたSSHキーの下に表示されている英数字の":"を除いた連続した英数字です。

実際にPullをするcommand

config.yml
...
setup-repository-of-b-hack-circleci:
    description: "Set up the CircleCi repository to use"
    steps:
      - add_ssh_keys:
          fingerprints: "${finger_prints}"
      - run:
          name: "set ssh config"
          command: |
            #!/bin/bash

            cd ${CIRCLE_WORKING_DIRECTORY}/.circleci/config
            cat ssh_config \>> /root/.ssh/config
      - run:
          name: "git pull the CircleCi repository"
          command: |
            #!/bin/bash

            cd ${CIRCLE_WORKING_DIRECTORY}/.circleci
            git clone git@github.com:${リポジトリ URL}
...

となります。
これでCircleCiリポジトリを実行環境に落としてくることができたので、次の章で構築、実行します。

next.ymlの構築と実行

next.ymlの構築に関してですが、ここに関しては全く難しいことはしていません(むしろもう少し工夫すべき部分かもしれませんが...)。

config.yml
  create-executing-config:
    description: "Create property config.yml file depending env"
    parameters:
      env:
        description: "environment name"
        type: enum
        enum: [ "", "prod", "stg", "stgOversea" ]
      config-name:
        description: "The file name which is created."
        type: string
    steps:
      - run:
          name: "create release_config.yml"
          command: |
            #!/bin/bash

            header="header.yml"
            jobs="jobs/docker.yml"
            workflow="workflow/docker/<< parameters.env >>.yml"
            commands="commands.yml"
            executors="executors.yml"

            cd ${CIRCLE_WORKING_DIRECTORY}/.circleci/B-Hack_CircleCi
            cat ${header} ${jobs} ${workflow} ${commands} ${executors} > ${CIRCLE_WORKING_DIRECTORY}/.circleci/<< parameters.config-name >>.yml
      - run:
          name: "Check the contents of the created config.yml"
          command: cat ${CIRCLE_WORKING_DIRECTORY}/.circleci/<< parameters.config-name >>.yml
      - run:
          name: "Change authentication"
          command: chown -R circleci:circleci /tmp

まず、next.ymlを作成します。
job, workflowのファイルはリポジトリ名、引数を用いてファイルを指定します。catコマンドで、各ファイルを繋げて読み込み、next.ymlに書き込みます。
そして、next.ymlにcircleciユーザー権限を与えることで、CircleCiからnext.ymlを読み込めるようにします。

next.ymlの実行方法としては、continuation Orbを用いて実行します

config.yml
- continuation/continue:
    configuration_path: /tmp/workspace/.circleci/release_config.yml
    parameters: |
      { "repo-name": "${リポジトリ名}"}

config.yml のparametersに記載されている引数以外の引数を追加したい場合はJSON形式でparametersに設定することで、next.ymlで受け取ることができます

最後に下記が実装全体のサンプルになります。

config.yml
version: 2.1

setup: true

orbs:
  continuation: circleci/continuation@0.3.1

parameters:
  env:
    type: enum
    enum: ["", prod, stg, stgOversea]
    default: ""

executors:
  base:
    docker:
      - image: cimg/base:stable
        user: root

jobs:
  setup-container:
    executor: base
    working_directory: /tmp/workspace
    environment:
      CONFIG_NAME: "release_config"
    steps:
      - checkout
      - setup-repository-of-b-hack-circleci:
          circleci-branch: << pipeline.parameters.circleci-branch >>
      - create-executing-config:
          env: << pipeline.parameters.env >>
          config-name: ${CONFIG_NAME}
      - persist_to_workspace:
          root: /tmp/workspace
          paths:
            - .circleci/*
  continue-release:
    executor: continuation/default
    working_directory: /tmp/workspace
    steps:
      - attach_workspace:
          at: /tmp/workspace
      - run:
          name: "check"
          command: |
            ls -al /tmp/workspace
            ls -al /tmp/workspace/.circleci
            cat /tmp/workspace/.circleci/release_config.yml
      - continuation/continue:
          configuration_path: /tmp/workspace/.circleci/release_config.yml
          parameters: |
            { "repo-name": "${リポジトリ名}"}

workflows:
  setup-and-release:
    jobs:
      - setup-container
      - continue-release:
          requires:
            - setup-container

commands:
  setup-repository-of-b-hack-circleci:
    description: "Set up the B-Hack_CircleCi repository to use"
    parameters:
      circleci-branch:
        description: "Branch name of B-Hack_CircleCi"
        type: string
        default: ""
    steps:
      - add_ssh_keys:
          fingerprints: "03:6e:7f:4b:10:7a:f3:7c:83:db:d4:ac:dc:1f:3f:45"
      - run:
          name: "set ssh config"
          command: |
            #!/bin/bash

            cd ${CIRCLE_WORKING_DIRECTORY}/.circleci/config
            cat ssh_config \>> /root/.ssh/config
      - run:
          name: "git pull the B-Hack_CircleCi repository"
          command: |
            #!/bin/bash

            cd ${CIRCLE_WORKING_DIRECTORY}/.circleci
            git clone git@github.com:FullSpeedInc/B-Hack_CircleCi.git

  create-executing-config:
    description: "Create property config.yml file depending env"
    parameters:
      env:
        description: "environment name"
        type: enum
        enum: [ "", "prod", "stg", "stgOversea" ]
      config-name:
        description: "The file name which is created."
        type: string
    steps:
      - run:
          name: "create release_config.yml"
          command: |
            #!/bin/bash

            header="header.yml"
            jobs="jobs/docker.yml"
            workflow="workflow/docker/<< parameters.env >>.yml"
            commands="commands.yml"
            executors="executors.yml"

            cd ${CIRCLE_WORKING_DIRECTORY}/.circleci/B-Hack_CircleCi
            cat ${header} ${jobs} ${workflow} ${commands} ${executors} > ${CIRCLE_WORKING_DIRECTORY}/.circleci/<< parameters.config-name >>.yml
      - run:
          name: "Check the contents of the created config.yml"
          command: cat ${CIRCLE_WORKING_DIRECTORY}/.circleci/<< parameters.config-name >>.yml
      - run:
          name: "Change authentication"
          command: chown -R circleci:circleci /tmp

まとめ

ここまでで、CircleCiの実装の共通化の仕方について解説してきました。
CI/CDの実装を共通化したいと思った際に参考にしていただけますと幸いです。

ここまで読んでいただきありがとうございました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?