GitHub Actions の Reusable workflows を使ったワークフローの共通化
セーフィーのインフラグループに所属している佐伯です。
今年もアドベントカレンダーを執筆することになったので、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 という仕組みがあります。ドキュメントは以下です。いくつか注意点を記載します。
アクセス設定
再利用するワークフローをプライベートリポジトリで管理する場合はアクセス設定が必要になります。
環境変数
制限事項にも記載されている通り、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
に出力し、環境変数に設定しています。
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 を選択しました。
{
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/checkout
の 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