GitHub Actionsを使ったマイクロサービスのCI/CDモジュール管理

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パイプライン全域に影響する更新をしたい時には数十〜数百規模のリポジトリ一つ一つにPullRequestを起こす作業が必要です。簡単な修正でも作業量が多く必要になってしまいます。

MoTGitHub Organizationは本記事執筆時点で広大で、もう少しでリポジトリ数1000に到達します。その中から関連する数十個のリポジトリをピックアップする必要があるため検索も工夫する必要があります。MoTでは事業統合を起因とするOrganizationの引っ越しをしたりと様々な大きなイベントを経たことで管理が行き届かず野放しになっていました。

CircleCIではOrbという機能を使う事でパイプラインをモジュール管理する手がありますが、TravisCIにはそういった機能が無いため、個別修正PullRequestを重要度の高いプロジェクトや目に届く範囲で上げていくという方針でメンテナンスされていました。

そのような背景が有る中でTravisCIをやめてActionsへ移行する事になったため、CI/CDの基本方針を維持しつつモジュール化する方法を模索してアップデートすることにしました。

GitHub Actions CI/CDモジュール管理イメージ

移行途中ではあるものの、モジュール化については実現することが出来たのでGolangのアプリケーションを例として紹介いたします。

全体像としては以下のようなイメージです。

GitHub Actionsモジュール管理.drawio.png

構成上のポイントとしては幾つかあります

テンプレートリポジトリ:

  • アプリケーションの基本セットを入れておくリポジトリ
  • CI/CDとして良く使う項目を .github/workflows/*.ymlに記載
  • 一緒にdependabot.ymlも記載

アプリケーションリポジトリ:

  • テンプレートリポジトリから展開してアプリケーションリポジトリを構成
  • golangのビルド、デプロイ部分はモジュール化されているComposite Actionリポジトリを参照
  • その他 用途に応じて任意のActionを設定可能

Composite Actionリポジトリ:

Dependabot:

  • Golangアプリケーションのモジュールバージョン追跡
  • Actionのバージョンも追跡させてメンテナンス

モジュール化して良い所について

このアプローチの何が良いかというと、

という所が挙げられます。

管理の集約とリポジトリ自治管理という両面のバランスを取る事を考えた結果この形に落ち着きました。

構成要素の解説

MoTではGitHub Enterprise Cloudを利用しており、Enterprise Cloud限定の機能も使っています。

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/checkoutgolangの実行環境をセットアップする actions/setup-go などのactionをワークフロー毎に毎度実行する必要があったりするので、逐次実行が必要なビルドジョブの一つをモジュール化する用途に向きません。並列でジョブを回す場合や、別ワークフローから再呼び出しするケースには使えるのでケースバイケースになります。

具体例

MoTのサーバーサイドアプリとしてはGolang標準技術スタックとして認定しています。

GolangアプリケーションのCIの例と合わせて以下を紹介いたします。

  1. Composite Actionリポジトリの具体的例
  2. Composite Actionの使い方例
  3. Dependabot設定例

なお、GitHub Organization内のリポジトリは全部プライベートリポジトリにする前提です。

プライベートリポジトリを参照する部分で工夫が必要になるので、具体策踏まえて紹介していきます。

1. Composite Actionリポジトリの具体例

Untitled

赤枠の部分をこれから説明します。

例として 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の導入とキャッシュ設定

を実行しています。

GolangGitHub 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を共有する設定が可能です。具体的には以下ドキュメントを参考に設定可能です。

https://docs.github.com/ja/enterprise-cloud@latest/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#allowing-access-to-components-in-an-internal-repository

今回はComposite Actionを配置したリポジトリ internal-tools を共有するように設定します。

1-4. Actionのリリース管理設定

Actionsを配布するときには基本的にはGitHubのTagを利用することになります。GitHub Release機能は不要です。細かい挙動はGitHub Actionsのworkflow開始直後のログを見ると分かりますが、起動直後にActionを取得する処理が走ります。この時にgit tagやコミットIDを元にtar.gzを取得しています。Organization内で共有している場合でも同じ方法で配布可能となっています。

リリース管理については具体的にはドキュメントに記載されてます。

https://docs.github.com/ja/actions/creating-actions/about-custom-actions#using-tags-for-release-management

自作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例

Untitled

上で作成した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設定

Untitled

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に設定して下さい。

参考: https://docs.github.com/ja/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

これを設定しておくことで、Composite Action側でメジャーバージョンアップした場合でも

アプリケーションリポジトリの方で自動的にPullRequestがポコポコと上がることになります。

CODEOWNERS設定

必須ではないですが、CODEOWNERS ファイルを使って .github/workflows 以下のOWNERを記載しておくとDependabotが上げたPullRequestのレビュアーが自動設定されます。一緒に仕込んでおくと、全体に影響する変更をしてもPullRequestをマージするだけになるため楽が出来ると思います。

補足: Composite ActionのDependabot

Untitled

ここの部分も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パイプラインの標準化アプローチとして使えると思いますので、何かの参考になれば幸いです。