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パイプラインのメンテナンス性は課題の一つになっています。
CIとして行う項目はアプリケーションによって異なるため、リポジトリ単位で設定するというのが前提としてあります。しかし、マイクロサービスを前提とする場合最終的には共通基盤の上で動かすことになりますので、共通化や展開を容易にする方法が課題になります。
以前からは以下のようなテンプレート展開方式が採用されていました
このアプローチは容易に実装できて展開が早くて良かったのですが、中長期的なメンテナンス性という面では課題があります。例えばCI/CDパイプライン全域に影響する更新をしたい時には数十〜数百規模のリポジトリ一つ一つにPullRequestを起こす作業が必要です。簡単な修正でも作業量が多く必要になってしまいます。
MoTのGitHub Organizationは本記事執筆時点で広大で、もう少しでリポジトリ数1000に到達します。その中から関連する数十個のリポジトリをピックアップする必要があるため検索も工夫する必要があります。MoTでは事業統合を起因とするOrganizationの引っ越しをしたりと様々な大きなイベントを経たことで管理が行き届かず野放しになっていました。
CircleCIではOrbという機能を使う事でパイプラインをモジュール管理する手がありますが、TravisCIにはそういった機能が無いため、個別修正PullRequestを重要度の高いプロジェクトや目に届く範囲で上げていくという方針でメンテナンスされていました。
そのような背景が有る中でTravisCIをやめてActionsへ移行する事になったため、CI/CDの基本方針を維持しつつモジュール化する方法を模索してアップデートすることにしました。
移行途中ではあるものの、モジュール化については実現することが出来たのでGolangのアプリケーションを例として紹介いたします。
全体像としては以下のようなイメージです。
構成上のポイントとしては幾つかあります
テンプレートリポジトリ:
アプリケーションリポジトリ:
Composite Actionリポジトリ:
Dependabot:
このアプローチの何が良いかというと、
という所が挙げられます。
管理の集約とリポジトリ自治管理という両面のバランスを取る事を考えた結果この形に落ち着きました。
MoTではGitHub Enterprise Cloudを利用しており、Enterprise Cloud限定の機能も使っています。
GitHub ActionsではCustom Actionを作る手は3つ用意されています。
DockerとJavaScriptについては今回触れませんが、少し複雑なActionになる場合はDockerやJavaScriptで作る方が良いかと思います。
Composite Actionの特徴は
という点がポイントでプライベート用途や組織内で限定公開する場合に向いています。
その他にも可読性とメンテナンスの容易さという所が良かったのでモジュール化する用途にはComposite Actionを利用する事にしています。
Comoste Actionと似たようなものにReusable Workflowという機能があり、私達も普段使っているのですが今回は主題ではありません。Reusable Workflowでも似たような事は実現出来そうですがが、GitHub Actionsはワークフローという単位でIPアドレスやコンテナが別になります。例えばソースコードを取得する actons/checkout やgolangの実行環境をセットアップする actions/setup-go などのactionをワークフロー毎に毎度実行する必要があったりするので、逐次実行が必要なビルドジョブの一つをモジュール化する用途に向きません。並列でジョブを回す場合や、別ワークフローから再呼び出しするケースには使えるのでケースバイケースになります。
MoTのサーバーサイドアプリとしてはGolangを標準技術スタックとして認定しています。
GolangアプリケーションのCIの例と合わせて以下を紹介いたします。
なお、GitHub Organization内のリポジトリは全部プライベートリポジトリにする前提です。
プライベートリポジトリを参照する部分で工夫が必要になるので、具体策踏まえて紹介していきます。
赤枠の部分をこれから説明します。
例として 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 に記載する必要がありますが、それ以外は自由です
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 }}-
を実行しています。
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から導入出来るよう設定しました。
もう一つの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上で他のActionを利用するケースでは通常のWorkflowと同じ記載が出来ます。
shellを記載する場合、 shell: bash の記述を追記する必要があるため、既存のworkflowをComposite Actionに移植する場合は注意が必要です。
GitHub Enterprise Cloud限定機能になるかと思いますが、OrganizationまたはEnterprise内でActionを共有する設定が可能です。具体的には以下ドキュメントを参考に設定可能です。
今回はComposite Actionを配置したリポジトリ internal-tools を共有するように設定します。
Actionsを配布するときには基本的にはGitHubのTagを利用することになります。GitHub Release機能は不要です。細かい挙動はGitHub Actionsのworkflow開始直後のログを見ると分かりますが、起動直後にActionを取得する処理が走ります。この時にgit tagやコミットIDを元にtar.gzを取得しています。Organization内で共有している場合でも同じ方法で配布可能となっています。
リリース管理については具体的にはドキュメントに記載されてます。
自作Actionを配布するために注意するポイントとしては以下です。
マイナーバージョンのタグを手動で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リポジトリにまとめているので、バージョンが共有されている点は留意したほうが良いかもしれません。リポジトリが細かく別れても支障ない場合は細分化してしまって良いと思います。
上で作成した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など細かいコントロールが必要になるポイントはモジュール化を避けた方が良いと思います。
記述量が多すぎたり複雑化するとカスタマイズを敬遠されてしまうので、記述をシンプルにするためにモジュール化する使い方が良いかと思います。
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をマージするだけになるため楽が出来ると思います。
ここの部分も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
紹介した内容以外にも幾つかのActionをモジュール化しており、デプロイパイプラインを標準化する用途で利用しています。マイクロサービスを採用していない場合でもCI/CDパイプラインの標準化アプローチとして使えると思いますので、何かの参考になれば幸いです。
興味のある方は 採用ページ も見ていただけると嬉しいです。
Twitter @mot_techtalk のフォローもよろしくお願いします!