for Startups Tech blog

このブログのデザインを刷新しました。(2023/12/26)

Reusable WorkflowsとComposite Actionsを使い分けてGitHub Actionsの記述を削減する

目次

はじめに

こんにちは。SREの高場です。

テクノロジーグループで、主に社内プロダクトのCI/CDの整備やインフラの安定化を担当しています。

今回は、GitHub Actionsの2種類の機能を利用してワークフローファイルの記述量を削減した取り組みをご紹介します。

きっかけ

テクノロジーグループの社内向けプロダクトでは、CI/CDにGitHub Actionsを利用しています。

またビルドパイプラインにはGitHub Actionsを使っており、AWS環境のECSにデプロイしています。

最近、開発者用のステージング環境を新たに増やしました。

それに合わせてビルドパイプラインを増やす際に、既存のワークフローを参考に作成しました。

しかしパイプラインを記述したymlファイルの内容は、一部サービス名等が異なるだけでほとんど同じ内容でした。

そこで、GitHub Actionsの2つの仕組みを使って記述の重複を可能な限り排除してみました。

GitHub Actionsの2つの仕組み

GitHub Actionsには以下の2つの異なる仕組みがあります。

  • Reusable Workflows
  • Composite Actions

それぞれについて簡単に説明します。

Reusable Workflows

docs.github.com

参考:Reuse Workflows

Reusable Workflowsは、ワークフロー全体を再利用することを可能にします。

  • ユースケース: 複数のリポジトリや異なる環境で共通のワークフローロジックを再利用したい場合に最適です。
  • 特徴:
    • 他のリポジトリから参照可能: 組織内で共通のワークフローを定義し、それを複数のリポジトリから利用できます。
    • トリガー設定: 再利用されるワークフロー自体にトリガーを設定することができます。
    • 複数階層のネスト: ワークフロー内でさらに別のワークフローを呼び出す、といった複数階層の構造を構築できます。

利用例

社内プロダクトの開発では、ECSで複数のステージング環境を利用しています。

それぞれbacon,avocadoという名前をつけていて、ECSサービス名やECRイメージ名に使っています。

ステージング環境でのビルド・デプロイを行うために、それぞれの環境用にファイルを用意しています。

今回、共通した部分をReusable Workflowsに切り出し、ビルド・デプロイ部分を共通のファイルに集約しました。

結果として、stg環境名 (bacon, avocado)のようなパラメータを与えるだけで、複数環境へのビルド・デプロイが可能になりました。

ファイル構成の変更

変更前はdeploy_stg_avocado.ymldeploy_stg_bacon.ymlが独立して存在していましたが、Reusable Workflowsを導入することで、これらはdeploy_stg.ymlという共通のワークフローを参照する形になります。

記述量の削減: この変更により、全体の記述量が80行、約27%削減されました。

  • 変更前 (合計300行):
    • deploy_stg_bacon.yml: 150行
    • deploy_stg_avocado.yml: 150行
  • 変更後 (合計220行):
    • deploy_stg_bacon.yml: 25行
    • deploy_stg_avocado.yml: 25行
    • deploy_stg.yml: 170行

将来的にproduction環境を実装する際にも、環境ごとの分岐を追加して、この共通ワークフローを使い回すことを想定しています。

実際のファイル

以下は、Reusable Workflowsを利用してdeploy_stg.ymlを呼び出すGitHub Actionsのジョブの記述例です。実際のファイルを元にしています。

jobs:
  deploy-staging:
    uses: ./.github/workflows/deploy_stg.yml
    with:
      environment_name: 'bacon'
      ecr_repository: 'product-name-ecr'
      ecs_service: 'product-name-service'
      app_env: 'staging01'
      nginx_hostname: 'bacon.stg.example.com'
      need_ecs_service_start: false
    secrets:
      NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
      SF_CRT_KEY_STG: ${{ secrets.SF_CRT_KEY_STG }}

Composite Actions

docs.github.com

参考:Composite Actions

Composite Actionsは、複数のステップを一つのアクションとしてまとめることができる機能です。

  • ユースケース: 同じジョブ内で繰り返し実行される一連のステップを共通化したい場合に特に有用です。
  • 特徴:
    • 同一ジョブ内で実行: Composite Actionsは、呼び出し元のジョブと同じランナー(OS)内で実行されます。
    • secretsの受け渡し不可: secretsをComposite Actionに渡すことはできません(❌) 。ただし、withを使って明示的に渡すことは可能です。
    • 単独でのトリガー実行不可: Composite Actions単体でトリガーすることはできません (❌)。

ジョブとステップの違い

Composite Actionsの利点を説明するために、GitHub Actionsにおける「ジョブ」と「ステップ」の違いを明確にしておきます。

  • ジョブ (job): 同一ランナー(OS)内で行われる処理のまとまりの単位です。
  • ステップ (step): ジョブ内で実行される個別の処理です。

Composite Actionsの利点

ジョブを分割すると、各ジョブでcheckoutnpm installなどの共通の前処理をその都度行う必要があり、記述量が増えてしまいます。

この前処理はランナーを分けるごとに必須となる処理であるため、別ランナーで実行されるReusable Workflowsでは解決できない問題です。

Composite Actionsは、このような「異なるジョブに何度も現れる記述の重複」を解決します。

利用例

各種Linterやビルドチェックのために、ジョブを分割しています。

それぞれのテストは独立しており、並列で実行することが可能です。

上述したとおり、並列化したジョブのすべてにNode.jsのセットアップを実行する必要がありました。

Composite Actionsを使用することで、これらの同一の処理の記述をまとめることが可能になります。

例えば、.github/actions/setup-node/action.ymlのようなファイルでNode.jsのセットアップ処理を定義し、CIで実行されるeslintstylelintBiometestdocker buildなどのチェックにおいて、このsetup-nodeアクションをuses:で指定して利用します。

実際のファイル

Node.jsのセットアップとnpmrc認証を行うComposite Actionsの記述例です。実際のファイルを元にしています。

---
  name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version-file: ./node-version
    cache: 'yarn'
    registry-url: https://npm.pkg.github.com/
    scope: '@myOrganization'
- name: Setup .npmrc authentication
  if: inputs.setup-npmrc == 'true'
  shell: bash
  run: |
    sed -i "s/\${NODE_AUTH_TOKEN}/${{ inputs.node-auth-token }}/" ~/work/_temp/.npmrc
    cp ~/work/_temp/.npmrc ./.npmrc

注意点としては、記述した処理は同じジョブ内で実行されるので、実行時間は変わらないという点です。

あくまで効果としては記述上の短縮のみになります。

CIでの呼び出し例:

eslintジョブ内で上記Composite Actionを呼び出す例です。secretsは直接渡せないため、Nodeのトークンはwithで引き渡す必要があることに注意してください。

eslint:
  name: runner / eslint
  runs-on: ubuntu-latest
  steps:
    - name: Checkout
      uses: actions/checkout@v4
    - name: Setup Node.js and Dependencies
      uses: ./.github/actions/setup-node
      with:
        node-auth-token: ${{ secrets.NODE_AUTH_TOKEN }}
    - name: Run eslint
      uses: reviewdog/action-eslint@v1
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        eslint_flags: './src/**/*.js,{jsx,ts,tsx}'
        reporter: 'github-pr-review'
        filter_mode: 'diff_context'
        fail_level: 'error'

記述量の削減: この変更により、CI関連の記述量も削減されました。

削減量は69行で、約23%です。

  • 変更前 (合計300行):
    • ci.yml: 242行
    • run-knip.yml: 58行
  • 変更後 (合計231行):
    • ci.yml: 139行
    • run-knip.yml: 41行
    • actions.yml: 51行

Reusable WorkflowsとComposite Actionsの比較

個人的な感覚としては、Reusable Workflowsはワークフローの中からワークフローを呼び出すものですが、

Composite Actionsは純粋に処理をまとめたコンポーネントを呼び出すような印象を受けました。

複雑な分岐や共通化をしたいときはReusable Workflows、重複した記述をまとめたいときはComposite Actionsのような使い分けをすると良さそうです。

項目 Reusable Workflows Composite Actions
リポジトリからの参照 リポジトリを跨げる 同一リポジトリ
Secrets 渡せる 渡せない(withを使えば可能)
if文 使える 使えない
トリガー 使える 使えない

まとめ

全体の記述量としては、約25%の削減になりました。

今後ビルド手順に変更箇所があっても、複数のファイルを変更せずに済むため保守性も向上しています。

しかし、あまり使いすぎると不必要な複雑さを呼び込んだり、メンバーの認知負荷が上がるリスクはあります。

闇雲に使うのではなく、適切に使う場面を選択していきたいです。

ここまでお読みいただき、ありがとうございました。