🌊

GitHub Actions Workflowの共通化について。Reusable Workflowのハマりポイントも。

に公開

もともと開発・ステージング・商用などのステージごとにそれぞれのWorkflowを作成していましたが、
処理を共通化するためにReusable Workflowを利用するようにしました。

その際にWorkflowの設定や書き方ではまった点や制約などをまとめようと思います。

  • GitHub Actionsを少し使ったことがあるけどReusable Workflowは利用したことがない
  • Workflowの共通化に関する記事をいろいろ探している

といった方向けに、なるべく簡潔にまとめてみます。

Reusable Workflowの概要

Reusable Workflowは2021年秋頃に追加されたGitHub Actionsの機能で、2022年にプライベートリポジトリでも利用できるようになりましたので、誰でも手軽に試すことができます。

Workflowが2種類に分かれていて、Called WorkflowとCaller Workflowがあります。

Called Workflowは共通で利用可能な呼び出されるためのWorkflowで、以下のようなイメージです。

name: 'Reusable Deploy Example'
on:
  workflow_call:
    inputs:
      app_name:
        required: true
        type: string
      version:
        required: true
        type: string
jobs:
  shared-deploy-job:
    name: Deploy
    runs-on: ubuntu-20.04
    steps:
      # 途中でAssume RoleやCheckoutなどのStepがあるが省略
      - name: Run deploy
        run: npm run deploy

呼び出し元のCaller WorkflowからCalled Workflowを呼び出して利用します。

name: 'Deploy Example'
on:
  workflow_dispatch:
    inputs:
      app_name:
        description: 'App name'
        required: true
      version:
        description: 'App version'
        required: true
jobs:
  deploy:
    name: 'deploy: ${{ inputs.app_name }}@${{ inputs.version }}'
    uses: ./.github/workflows/shared-deploy.yaml
    with:
      app_name: ${{ github.event.inputs.app_name }}
      version: ${{ github.event.inputs.version }}

Reusable Workflowの利用

Reusable Workflowの利用方法はシンプルではありますが、
Called WorkflowとCaller Workflowでは文法が少し異なるのでハマりやすいと思います。

Called Workflowを作成する際のトリガーはon.workflow_callとなり、inputsにはtypeの指定が必須となります。

以下のtypeが指定可能:

https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_callinputsinput_idtype

on:
  workflow_call:
    inputs:
      app_name:
        required: true
        type: string

認証情報の受け渡し

secretsを利用することも多いです。認証情報等はGitHub Secretsで設定した上で、secretsキーワードに渡す必要があります。

name: 'Deploy Example'
on:
  workflow_dispatch:
    inputs:
      app_name:
        description: 'App name'
        required: true
      version:
        description: 'App version'
        required: true
jobs:
  deploy:
    name: 'deploy: ${{ inputs.app_name }}@${{ inputs.version }}'
    uses: ./.github/workflows/shared-deploy.yaml
    with:
      app_name: ${{ github.event.inputs.app_name }}
      version: ${{ github.event.inputs.version }}
+    secrets:
+      TEST_POSTGRESQL_USER: ${{ secrets.TEST_POSTGRESQL_USER }}
+      TEST_POSTGRESQL_PASS: ${{ secrets.TEST_POSTGRESQL_PASS }}
name: 'Reusable Deploy Example'
on:
  workflow_call:
    inputs:
      app_name:
        required: true
        type: string
      version:
        required: true
        type: string
jobs:
  shared-deploy-job:
    name: Deploy
    steps:
+      - name: Test psql connection
+        run: |
+          psql -d "postgresql://${{ secrets.TEST_POSTGRESQL_USER }}:${{ secrets.TEST_POSTGRESQL_PASS }}@localhost"

認証情報の受け渡しの注意点

以下のようにSecretsをwithキーワードの部分で渡すと”Unrecognized name-value”といったエラーが発生しますので、secretsで渡しているかを確認しましょう。

name: 'Deploy Example'
on:
  workflow_dispatch:
    inputs:
      app_name:
        description: 'App name'
        required: true
      version:
        description: 'App version'
        required: true
jobs:
  deploy:
    name: 'deploy: ${{ inputs.app_name }}@${{ inputs.version }}'
    uses: ./.github/workflows/shared-deploy.yaml
    with:
      app_name: ${{ github.event.inputs.app_name }}
      version: ${{ github.event.inputs.version }}
      # これはNG
      TEST_POSTGRESQL_USER: ${{ secrets.TEST_POSTGRESQL_USER }}

Reusable Workflowの制約

主な制約には

  • Called Workflowは4レベルまでネストできる (ネストは最小限にしたい)
  • envの受け渡しができないのでoutputsを使う

があります。

他には、1つのWorkflowから呼び出しできるWorkflowは20まで、などがありますがそこまで呼び出しするケースはほぼ無いかと思います。

Called Workflowをネストしすぎると依存関係が分かりづらくなるため、個人的にはネストはせず1レベルまでが良いかと思います。

値の受け渡し

認証情報以外で、必要な値をWorkflow間で受け渡しすることも多いです。

envキーワードで環境変数を定義できますが、これはWorkflow間で受け渡しができません。そこでoutputsを利用します。

例えば、IAMのAssume Roleを行うWorkflowを作成し、RoleのARNを受け渡しする場合を考えてみます。

Caller側では以下のように呼び出しを追加。

name: 'Deploy Example'
on:
  workflow_dispatch:
    inputs:
      app_name:
        description: 'App name'
        required: true
      version:
        description: 'App version'
        required: true
jobs:
+  assume-role:
+    name: 'Assume Role'
+    uses: ./.github/workflows/shared-assume-role.yaml
+    with:
+      app_name: ${{ github.event.inputs.app_name }}
+
  deploy:
    name: 'deploy: ${{ inputs.app_name }}@${{ inputs.version }}'
    uses: ./.github/workflows/shared-deploy.yaml
    with:
      app_name: ${{ github.event.inputs.app_name }}
      version: ${{ github.event.inputs.version }}
    secrets:
      TEST_POSTGRESQL_USER: ${{ secrets.TEST_POSTGRESQL_USER }}
      TEST_POSTGRESQL_PASS: ${{ secrets.TEST_POSTGRESQL_PASS }}

新しいCalled Workflowを作成します。以下のYAMLのような内容になります。

onキーワード内で指定するoutputsとjobs内のoutputsがあり、on.outputsからjobs内のoutputsを参照する形になります。

そして受け渡しの際はCaller Workflowからon.outputsで指定したvalueを参照する流れです。

name: 'Reusable Assume Role Example'
on:
  workflow_call:
    inputs:
      app_name:
        required: true
        type: string
      version:
        required: true
        type: string
  outputs:
    role_arn:
      description: Role ARN
      value: ${{ jobs.shared-assume-role.outputs.result }}
jobs:
  shared-assume-role:
    name: Assume Role
    outputs:
      result: ${{ steps.role-arn.outputs.arn }}
    steps:
      - name: Gets Role ARN
        id: role-arn
	run: |
          arn=$(aws iam list-roles --query 'Roles[0].Arn')
          echo "arn=$arn" >> $GITHUB_OUTPUT

再度Caller Workflowに戻って、outputsを参照するように修正します。

以下のようにneedsでジョブの依存関係を指定し、Assume Roleが先に実行されるようにします。needsキーワードから先ほど追加したoutputsを参照できるようになっています。

(shared-deploy.yaml には別途role_arnのinputs追加も必要)

name: 'Deploy Example'
on:
  workflow_dispatch:
    inputs:
      app_name:
        description: 'App name'
        required: true
      version:
        description: 'App version'
        required: true
jobs:
  assume-role:
    name: 'Assume Role'
    uses: ./.github/workflows/shared-assume-role.yaml
    with:
      app_name: ${{ github.event.inputs.app_name }}

  deploy:
    name: 'deploy: ${{ inputs.app_name }}@${{ inputs.version }}'
    uses: ./.github/workflows/shared-deploy.yaml
+   needs:
+     - assume-role
    with:
      app_name: ${{ github.event.inputs.app_name }}
      version: ${{ github.event.inputs.version }}
+     role_arn: ${{ needs.assume-role.outputs.role_arn }}
    secrets:
      TEST_POSTGRESQL_USER: ${{ secrets.TEST_POSTGRESQL_USER }}
      TEST_POSTGRESQL_PASS: ${{ secrets.TEST_POSTGRESQL_PASS }}

詳しい文法についてはドキュメントが参考になります。

https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_calloutputs

補足:カスタムアクションの作成も検討してみる

Reusable Workflowは簡単で便利ですが、
基本的にBashで書いていくため、複雑なステップがある場合はテストが難しくなり変更コストがかかる可能性があります。

Reusable Workflowではなく、カスタムアクションという機能があり、これはWorkflowより小さな単位で再利用できるステップです。

JavaScript(TypeScriptでも) でカスタムアクションの実装が可能ですので、テストフレームワークが充実しているのでテストがやりやすく変更しやすいため、JavaScriptでアクションを作成することも検討して良いですね。

https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action

補足:VSCode拡張機能のインストール

GitHub Actions用の拡張機能インストールはほぼ必須級です。
WorkflowのYAMLファイルを編集する際に補完や文法チェックができるので、手戻りが少なくなります。

https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-github-actions


お読み頂きありがとうございました。

随時、不足があったら追記していこうと思います。

Discussion