GitHub Actions の Reusable workflows を使ったワークフローの共通化

2023/12/09に公開

セーフィーのインフラグループに所属している佐伯です。

今年もアドベントカレンダーを執筆することになったので、Zenn で書いてみます(Zenn が完全にアドベントカレンダー用になってしまってるので普段からアウトプットしなければなとは思ってます...)

概要

GitHub Actions ではワークフローを再利用できる仕組みがあります。共通化できるワークフローを共通化し、Actions のバージョンアップなども Renovate で継続的に行えるよう仕組み化しました、というのが今回の主な内容です。

課題

弊社では Git ホスティングサービスに GitHub を使用しているため GitHub と親和性の高い GitHub Actions を CI/CD に利用しています。また、多くのアプリケーションは ECS on Fargate で実行しており、デプロイには kayac/ecspresso を使っています。

各アプリケーションごとに GitHub リポジトリが存在し、ECS へのデプロイを行う GitHub Actions のワークフローも各リポジトリごとに管理していました。そんな中、ワークフロー内で使用している Actions のバージョンアップが継続的に行われていなかったり、save-state, set-output の廃止(結局延期となりましたが)対応のため様々なリポジトリで同じような変更しなければならず効率的ではないといった課題がありました。

Reusable workflows

GitHub Actions にはワークフローを再利用できる Reusable workflows という仕組みがあります。ドキュメントは以下です。いくつか注意点を記載します。

https://docs.github.com/ja/actions/using-workflows/reusing-workflows

アクセス設定

再利用するワークフローをプライベートリポジトリで管理する場合はアクセス設定が必要になります。

環境変数

制限事項にも記載されている通り、env コンテキストによる環境変数の設定はできません。そのため環境変数依存の機能がある場合は inputs で入力し、ワークフロー内で $GITHUB_ENV に設定するなどのワークアラウンドが必要です。

ワークフローのサンプル

呼び出し元ワークフロー(caller workflow)

先行ジョブでコンテナイメージのビルド、プッシュは実行済み、且つ先行ジョブのアウトプットを参照する例になってるのでご留意ください。

  deploy:
    uses: org/repo/.github/workflows/ecs-deploy.yml@v1
    needs: build-and-push-app
    permissions:
      id-token: write
      contents: read
    with:
      aws-region: ap-northeast-1
      config: ecspresso/app/config.yaml
      external-variables: >-
        --ext-str image=${{ needs.build-and-push-app.outputs.image-uri-with-tag }}
    secrets:
      assume-role: ${{ secrets.ASSUME_ROLE_ARN }}

呼び出されるワークフロー(called workflow)

以下が再利用されるワークフローです。簡単に内容を補足すると、 ecspresso の設定ファイル内で {{ must_env `FOO` }} などを使用しているケースがあったため、制限事項のワークアラウンドとしてSet environment variable では inputs.envs に入力された JSON を $GITHUB_ENV に出力し、環境変数に設定しています。

org/repo/.github/workflows/ecs-deploy.yml@v1
name: Deploy Amazon ECS with ecspresso

on:
  workflow_call:
    inputs:
      args:
        description: "Arguments for ecspresso deploy command (e.g., --no-update-service)"
        required: false
        type: string
        default: ""
      aws-region:
        description: "AWS region"
        required: false
        type: string
        default: "ap-northeast-1"
      config:
        description: "ecspresso config file"
        required: true
        type: string
      envs:
        description: 'Environment variables in json array (e.g., ["CLUSTER=ecs-cluster", "SERVICE=app"])'
        required: false
        type: string
        default: "[]"
      external-variables:
        description: "External variables (e.g., --ext-str image=nginx:latest)"
        required: false
        type: string
        default: ""
    secrets:
      assume-role:
        description: "Assume role arn"
        required: true

jobs:
  deploy-ecs:
    name: Deploy Amazon ECS with ecspresso
    runs-on: ubuntu-latest
    steps:
      - name: Set environment variable
        run: |
          for v in $(echo '${{ inputs.envs }}' | jq -r '.[]'); do
            echo "${v}" >> "$GITHUB_ENV"
          done
      - name: Checkout
        uses: actions/checkout@v4
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.assume-role }}
          aws-region: ${{ inputs.aws-region }}
      - name: Install ecspresso
        uses: kayac/ecspresso@v2
        with:
          version: v2.2.4 # renovate: depName=kayac/ecspresso
      - name: Execute ecspresso verify
        run: |
          ecspresso verify --no-put-logs --no-get-secrets --config ${{ inputs.config }} ${{ inputs.external-variables }}
      - name: Execute ecspresso deploy
        run: |
          ecspresso deploy --config ${{ inputs.config }} ${{ inputs.external-variables }} ${{ inputs.args }}

Renovate における継続的なバージョンアップ

actions/checkout, aws-actions/configure-aws-credentials, kayac/ecspresso などの Actions は Renovate を導入するのみで継続的にバージョンアップできます。

ただし ecspresso 自体のバージョンアップは Renovate を導入しただけでは対応できないので、以下のように Regex Manager を使ってコメント部分でキャプチャするようにしています。Dependabot ではこういったことはできないので Renovate を選択しました。

.github/renovate.json5
{
  extends: ["config:base"],
  timezone: "Asia/Tokyo",
  labels: ["dependencies"],
  prHourlyLimit: 0,
  regexManagers: [
    {
      fileMatch: ["^\\.github/workflows/[^/]+\\.ya?ml$"],
      matchStrings: [
        ": +(?<currentValue>.*) +# renovate: depName=(?<depName>.*?)\\n",
      ],
      datasourceTemplate: "github-releases",
    }
  ]
}

Reusable workflow のバージョン管理

各 Actions 等のバージョンアップに伴い、破壊的変更が発生するケースがあるため、@main などによるブランチ名指定の参照はせず、@v1 などのタグを参照する方法を採用しました。

リリースを作成すると v1, v2 などのタグを更新するワークフローを作成しています。破壊的変更が含まれる場合はメジャーバージョンを更新する運用としています。このアイデア自体は actions/checkoutupdate-main-version.yml を参考にしました。

.github/workflows/update-main-version.yml
name: Update Main Version
run-name: Move main version to ${{ github.event.release.tag_name }}

on:
  release:
    types:
      - released

jobs:
  update-tag:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Set major-version
        id: set-major-version
        run: |
          v=$(echo ${{ github.event.release.tag_name }} | cut -d. -f1)
          echo "major-version=$v" >> "$GITHUB_OUTPUT"
      - name: Git config
        run: |
          git config user.name github-actions
          git config user.email github-actions@github.com
      - name: Tag new target
        run: git tag -f ${{ steps.set-major-version.outputs.major-version }} ${{ github.event.release.tag_name }}
      - name: Push new tag
        run: git push origin ${{ steps.set-major-version.outputs.major-version }} --force

さいごに

若干小ネタ的な内容ですが、GitHub Actions と Renovate は本当に便利だと思っています。本エントリがどなたかの参考になれば幸いです。

Discussion