MoTではマイクロサービスアーキテクチャを採用しており、標準技術スタックにGitHub Actionsを採用しています。本記事では数多くのリポジトリのCI/CDパイプラインを管理していくアプローチを紹介します。
はじめに
昨年10月頃にSREグループにjoinした古越です。クラウドインフラの構築、運用とアプリケーションのCI/CD構成などを担当しています。
MoTの中での開発体験向上はSREグループのミッションの一つです。CI/CDについては開発体験とアプリケーションの品質に大きく寄与する要素だと考えています。
MoTのSREグループが構築するサービスのCI/CDにはTravisCIが長く使われていました。最近になりGitHub Actionsを使う方針に切り替えており、現在は移行途中になります。移行については別記事で触れようと思いますが、移行過程でCI/CDの共通化や管理上の課題が幾つか明らかになりました。リポジトリ数の多いマイクロサービス特有の課題と解決策について紹介しようと思います。
背景と課題
MoTではマイクロサービスアーキテクチャを採用しているため、アプリケーションのリポジトリは毎月数個単位で増え続けています。2022年5月現在で30~40個ほどのリポジトリがアクティブに開発されています。開発はGitHubを使って進められており、基本的にCI/CDを導入しているためほぼ毎時間CI/CDが発火しています。休眠状態のリポジトリを含めると100を超えるリポジトリにCI/CDパイプラインが存在する事になるため、CI/CDパイプラインのメンテナンス性は課題の一つになっています。
TravisCI時代 既存のCI/CDモジュール化アプローチ
CIとして行う項目はアプリケーションによって異なるため、リポジトリ単位で設定するというのが前提としてあります。しかし、マイクロサービスを前提とする場合最終的には共通基盤の上で動かすことになりますので、共通化や展開を容易にする方法が課題になります。
以前からは以下のようなテンプレート展開方式が採用されていました
- CI/CDスクリプトをある程度の単位で纏める (install.sh, build.sh, test.sh, deploy.shなど)
- GitHubテンプレートリポジトリの中にアプリケーションコードと同梱してCI/CDスクリプトを入れる
- アプリケーション構築時にテンプレートから展開
- 以後の細かい調整はリポジトリ毎に行う
このアプローチは容易に実装できて展開が早くて良かったのですが、中長期的なメンテナンス性という面では課題があります。例えばCI/CDパイプライン全域に影響する更新をしたい時には数十〜数百規模のリポジトリ一つ一つにPullRequestを起こす作業が必要です。簡単な修正でも作業量が多く必要になってしまいます。
MoTのGitHub Organizationは本記事執筆時点で広大で、もう少しでリポジトリ数1000に到達します。その中から関連する数十個のリポジトリをピックアップする必要があるため検索も工夫する必要があります。MoTでは事業統合を起因とするOrganizationの引っ越しをしたりと様々な大きなイベントを経たことで管理が行き届かず野放しになっていました。
CircleCIではOrbという機能を使う事でパイプラインをモジュール管理する手がありますが、TravisCIにはそういった機能が無いため、個別修正PullRequestを重要度の高いプロジェクトや目に届く範囲で上げていくという方針でメンテナンスされていました。
そのような背景が有る中でTravisCIをやめてActionsへ移行する事になったため、CI/CDの基本方針を維持しつつモジュール化する方法を模索してアップデートすることにしました。
GitHub Actions CI/CDモジュール管理イメージ
移行途中ではあるものの、モジュール化については実現することが出来たのでGolangのアプリケーションを例として紹介いたします。
全体像としては以下のようなイメージです。
構成上のポイントとしては幾つかあります
テンプレートリポジトリ:
アプリケーションリポジトリ:
- テンプレートリポジトリから展開してアプリケーションリポジトリを構成
- golangのビルド、デプロイ部分はモジュール化されているComposite Actionリポジトリを参照
- その他 用途に応じて任意のActionを設定可能
Composite Actionリポジトリ:
Dependabot:
- Golangアプリケーションのモジュールバージョン追跡
- Actionのバージョンも追跡させてメンテナンス
モジュール化して良い所について
このアプローチの何が良いかというと、
という所が挙げられます。
管理の集約とリポジトリ自治管理という両面のバランスを取る事を考えた結果この形に落ち着きました。
構成要素の解説
MoTではGitHub Enterprise Cloudを利用しており、Enterprise Cloud限定の機能も使っています。
- GitHub Actions
- Dependabot
Custom Actionについて
GitHub ActionsではCustom Actionを作る手は3つ用意されています。
- Docker Container Action
- JavaScript Action
- Composite Action
DockerとJavaScriptについては今回触れませんが、少し複雑なActionになる場合はDockerやJavaScriptで作る方が良いかと思います。
Composite Actionを使ったモジュール化
Composite Actionの特徴は
- 複数のActionを纏める事が出来る
- yamlにshellをそのまま書ける
という点がポイントでプライベート用途や組織内で限定公開する場合に向いています。
その他にも可読性とメンテナンスの容易さという所が良かったのでモジュール化する用途にはComposite Actionを利用する事にしています。
補足: Reusable Workflow
Comoste Actionと似たようなものにReusable Workflowという機能があり、私達も普段使っているのですが今回は主題ではありません。Reusable Workflowでも似たような事は実現出来そうですがが、GitHub Actionsはワークフローという単位でIPアドレスやコンテナが別になります。例えばソースコードを取得する actons/checkout やgolangの実行環境をセットアップする actions/setup-go などのactionをワークフロー毎に毎度実行する必要があったりするので、逐次実行が必要なビルドジョブの一つをモジュール化する用途に向きません。並列でジョブを回す場合や、別ワークフローから再呼び出しするケースには使えるのでケースバイケースになります。
具体例
MoTのサーバーサイドアプリとしてはGolangを標準技術スタックとして認定しています。
GolangアプリケーションのCIの例と合わせて以下を紹介いたします。
- Composite Actionリポジトリの具体的例
- Composite Actionの使い方例
- Dependabot設定例
なお、GitHub Organization内のリポジトリは全部プライベートリポジトリにする前提です。
プライベートリポジトリを参照する部分で工夫が必要になるので、具体策踏まえて紹介していきます。
1. Composite Actionリポジトリの具体例
赤枠の部分をこれから説明します。
例として MyActions/internal-tools
という複数のComposite Actionを格納するリポジトリを作成するとします。
GitHubActionsの現状仕様では複数のComposite Actionを1リポジトリに集約して利用することが出来ます。集約にはデメリットもあり、バージョンが1リポジトリで共有されます。きめ細かいバージョニングが重要な場合は個別で管理するのが良いと思います。あまり細かくても管理が煩雑になるため、今回は1リポジトリに纏める例を記載します。
ディレクトリ構成
# MyActions/internal-tool $ tree . ├── README.md └── golang ├── init # Golang初期設定を纏めたもの │ └── action.yml └── docker-build-push # docker-build-push関係のActionを纏めたもの └── action.yml
ディレクトリ構成は自由度が高く、任意の階層に設定出来ます。
Composite Actionの内容は action.yml
に記載する必要がありますが、それ以外は自由です
1-1. golang/init Action
internal-tool/golang/init/action.yml
name: 'Golang Tools Initialize' description: 'Golang Tools Initialize' inputs: token: description: 'Github PAT for getting libraries with go get command' required: true golang-version: description: 'golang version' required: true golangci-lint-version: description: 'golangci-lint version' default: '' runs: using: "composite" steps: - name: Setup Go uses: actions/setup-go@v3 with: go-version: ${{ inputs.golang-version }} - name: Setup Go mod run: | git config --global url."https://${{ inputs.token }}:x-oauth-basic@github.com/".insteadOf "https://github.com/" go mod download shell: bash - name: Install golangci-lint if: ${{ inputs.golangci-lint-version != '' }} run: | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b "$(go env GOPATH)/bin" ${{ inputs.golangci-lint-version }} echo "$(go env GOPATH)/bin" >> $GITHUB_PATH shell: bash - name: Cache for golangci-lint if: ${{ inputs.golangci-lint-version != '' }} uses: actions/cache@v3 with: path: | ~/.cache/golangci-lint ~/.cache/go-build key: golangci-lint.cache-${{ runner.arch }}-${{ hashFiles('**/go.sum') }} restore-keys: | golangci-lint.cache-${{ runner.arch }}-
- actions/setup-go 導入
- go mod関係のセットアップ
- golangci-lintの導入とキャッシュ設定
を実行しています。
GolangでGitHub Privateリポジトリに独自モジュールを配置している場合はgit configにPersonal Access Tokenを使った認証設定を入れる方法が一般的です。go getなどでモジュール取得する場合にgitエコシステムを使ってモジュール郡をダウンロードするためです。
golangci-lintについては特に縛りがなければ公式で用意されている golangci/golangci-lint-action を使うのが良いかと思います。ただ、そちらのActionではgolangci-lintの1.28.3未満に対応してません。MoTでは1.28.3未満を使っているリポジトリも幾つかあり、旧バージョンを利用するリポジトリのためにこのActionから導入出来るよう設定しました。
1-2. golang/docker-build-push Action
もう一つのActionを紹介します
internal-tool/golang/docker-build-push/action.yml
name: 'Docker build and push Action' description: 'Docker build and push Action' inputs: token: description: 'Github PAT for getting libraries with go get command' required: true image-tags: description: 'Docker Image Tags' required: true dockerhub-username: description: 'DockerHub Username' required: true dockerhub-token: description: 'DockerHub Personal Acess Token' required: true push: description: 'Docker Push Enable (true|false)' default: false runs: using: "composite" steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ inputs.dockerhub-username }} password: ${{ inputs.dockerhub-token }} - name: Build and ECR Push uses: docker/build-push-action@v3 with: context: . push: ${{ inputs.push }} tags: ${{ inputs.image-tags }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | GITHUB_TOKEN=${{ inputs.token }}
setup-buildx-actionはビルド速度の高速化のために導入しています。
DockerHubログインは Dockerのダウンロード率制限を緩和する目的で設定しており、CI用にDockerHubでmachine userを作り、Personal Access Tokenを発行して設定しています。
build-push-actionはsetup-buildx-actionとの相性のために採用しています。pathにECRを指定した場合でも、オプション一つでdocker pushまで行ってくれる点も良い所です。cache-from, cache-toの欄に type=gha
と指定するとGitHub Actions向けのキャッシュ設定をしてくれるため設定していますが、2022/06現在で実験的オプションのようです。ご利用の際は注意して下さい。
type=gha
のキャッシュ設定については以下に詳細記載があります。
https://github.com/moby/buildkit#github-actions-cache-experimental
Dockerfile上でgo modのプライベートリポジトリの取得部分は注意が必要で、Dockerfileの ARG
を指定してPersonal Access Tokenをコンテナ内に伝搬しています。
Composite Action補足
Composite Action上で他のActionを利用するケースでは通常のWorkflowと同じ記載が出来ます。
shellを記載する場合、 shell: bash
の記述を追記する必要があるため、既存のworkflowをComposite Actionに移植する場合は注意が必要です。
1-3. Organization内でActionを共有する
GitHub Enterprise Cloud限定機能になるかと思いますが、OrganizationまたはEnterprise内でActionを共有する設定が可能です。具体的には以下ドキュメントを参考に設定可能です。
今回はComposite Actionを配置したリポジトリ internal-tools
を共有するように設定します。
1-4. Actionのリリース管理設定
Actionsを配布するときには基本的にはGitHubのTagを利用することになります。GitHub Release機能は不要です。細かい挙動はGitHub Actionsのworkflow開始直後のログを見ると分かりますが、起動直後にActionを取得する処理が走ります。この時にgit tagやコミットIDを元にtar.gzを取得しています。Organization内で共有している場合でも同じ方法で配布可能となっています。
リリース管理については具体的にはドキュメントに記載されてます。
自作Actionを配布するために注意するポイントとしては以下です。
- 配布元のリリースタグとしては
v1.0.1
,v1.0
,v1
,v2
などが有効 v1
,v2
などのメジャーバージョンタグはユーザ側で上書きする必要有り
マイナーバージョンのタグを手動でpushする所までは良いですが、メジャーバージョンの上書きまでは漏れる可能性があるため、自動化しておくと便利です。
Composite Actionをリリースするためのワークフローとして以下を記載しておくと良いでしょう。
internal-tool/.github/workflows/release.yml
name: Release Tag on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - name: renew git tags run: | # update major/minor version tag MAJOR_VERSION_TAG=`echo ${{github.ref_name}} | sed -n -E "s/^(v[0-9]+)\.[0-9]+\.[0-9]+$/\1/p"` if [ ! -z ${MAJOR_VERSION_TAG} ]; then set +e git tag -d ${MAJOR_VERSION_TAG} set -e git tag ${MAJOR_VERSION_TAG} fi MINOR_VERSION_TAG=`echo ${{github.ref_name}} | sed -n -E "s/^(v[0-9]+\.[0-9]+)\.[0-9]+$/\1/p"` if [ ! -z ${MINOR_VERSION_TAG} ]; then set +e git tag -d ${MINOR_VERSION_TAG} set -e git tag ${MINOR_VERSION_TAG} fi # push tags force git push --tags --force
細かい解説は割愛しますが、 v1.0.1
などのバージョンでtagがpushされたときにv1
,v1.0
を上書きするものです。v1.0.1-rc
などsuffixに文字列が付与されている場合は更新しません。
リリース手順
上記のRelease Actionを設定した状態で以下のコマンドでtagをpushするとタグが差し替わり、リリース出来るはずです。
$ git switch main $ git pull $ git tag v1.0.1 $ git push origin v1.0.1
運用上の注意点として、リリースタグをpushする前に別のタグでテストする事を推奨します。
例えば v1.0.1-rc
などのタグをpushし、別リポジトリのworkflowで動作確認して正式版をリリースするほうが良いと思います。
今回は複数のCompositeActionを1リポジトリにまとめているので、バージョンが共有されている点は留意したほうが良いかもしれません。リポジトリが細かく別れても支障ない場合は細分化してしまって良いと思います。
2. アプリケーションリポジトリのWorkflow例
上で作成したComposite Actionを使う例として CI用のワークフローを紹介します。
application/.github/workflows/check.yml
name: Check on: pull_request: branches: - main jobs: check: runs-on: ubuntu-latest timeout-minutes: 60 env: GOPRIVATE: github.com/MobilityTechnologies/* DOCKER_REPOSITORY_NAME: application-name GOLANGCI_LINT_VERSION: v1.44.0 steps: - name: Checkout uses: actions/checkout@v3 - name: Get golang version run: echo "GOLANG_VERSION=$(cat .go-version)" >> $GITHUB_ENV - name: Golang init uses: MyActions/internal-tools/golang/init@v0 with: token: ${{ secrets.MY_MACHINE_USER_PAT }} golangci-lint-version: ${{ env.GOLANGCI_LINT_VERSION }} golang-version: ${{ env.GOLANG_VERSION }} - name: Golang go mod tidy run: | go mod tidy git diff --exit-code go.mod go.sum - name: Golang lint run: golangci-lint run - name: Golang test run: go test -cover ./... - name: Golang docker-build check uses: MyActions/internal-tools/golang/docker-build-push@v0 with: token: ${{ secrets.MY_MACHINE_USER_PAT }} image-tags: ${{ env.DOCKER_REPOSITORY_NAME }}:${{ github.sha }} dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} push: false
複数Actionを纏めているため、アプリケーション側の設定はシンプルに纏める事が出来たかと思います。
Composite Actionの利用方式としては v0
, v1
などのメジャーバージョンタグを設定していますが、重要なアプリケーションでは v1.0.1
などのパッチバージョンで固定化すると事故防止になるため良いかと思います。
モジュール化を避けている部分
何らかのトラブルでtestが通らなくなってしまったときに、例外的にtestを無視したい、実行しなくて良いケースがあったりします。lint, testなど細かいコントロールが必要になるポイントはモジュール化を避けた方が良いと思います。
記述量が多すぎたり複雑化するとカスタマイズを敬遠されてしまうので、記述をシンプルにするためにモジュール化する使い方が良いかと思います。
3. Dependabot設定
Composite Actionをメジャーバージョンアップしたときに、数十~数百個あるリポジトリに反映していく作業は単純作業の繰り返しになるため、避けたい作業になります。モジュール化したActionリポジトリを継続的にメンテしていくためDependabotを活用する例を紹介します。
設定としてはアプリケーションリポジトリの中に以下を入れるだけです。
application/.github/dependabot.yml
version: 2 registries: github-my-actions: type: git url: https://github.com username: x-access-token password: ${{ secrets.MY_MACHINE_USER_PAT }} - package-ecosystem: "github-actions" directory: "/" registries: - github-my-actions schedule: interval: "daily"
Composite Action用にOrganization内で共有する設定を入れていたと思いますが、そちらを有効化してもDependabotからは参照できません。Dependabotから参照出来るようにmachine userのpersonal access token等を使って参照可能する必要があります。
secretsについては細かく触れませんが、Actions用secretsではなくDependabot用のsecretsに設定して下さい。
これを設定しておくことで、Composite Action側でメジャーバージョンアップした場合でも
アプリケーションリポジトリの方で自動的にPullRequestがポコポコと上がることになります。
CODEOWNERS設定
必須ではないですが、CODEOWNERS
ファイルを使って .github/workflows
以下のOWNERを記載しておくとDependabotが上げたPullRequestのレビュアーが自動設定されます。一緒に仕込んでおくと、全体に影響する変更をしてもPullRequestをマージするだけになるため楽が出来ると思います。
補足: Composite ActionのDependabot
ここの部分もDependabotで自動化出来ると良いのですが、本記事執筆時点 (2022/06) ではDependabot公式で正式サポートはされてなさそうです。グリッチのような設定を書けば動作しますので一応紹介します。正式にサポートされた場合には修正したほうが良さそうです。
internal-tool/.github/dependabot.yml
version: 2 updates: # --- for composite action --- # github-actions / path starts .github/workflows - package-ecosystem: "github-actions" directory: "/../../golang/init" schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/../../golang/docker-build-push" schedule: interval: "daily"
参考: https://github.com/dependabot/dependabot-core/issues/4178
まとめ
- マイクロサービスはリポジトリ数が多く、CI/CDパイプライン管理に課題がある
- GitHub ActionsではComopsite ActionとActionのOrganization内共有機能を使ってモジュール化するアプローチを取れる
- Dependabotでモジュール化したActionの配布を楽に出来る
紹介した内容以外にも幾つかのActionをモジュール化しており、デプロイパイプラインを標準化する用途で利用しています。マイクロサービスを採用していない場合でもCI/CDパイプラインの標準化アプローチとして使えると思いますので、何かの参考になれば幸いです。