YAMLの肥大化を防ぐ:Reusable WorkflowsとComposite ActionsによるGitHub Actionsの共通化

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

エンジニアリング速度を低下させる「コピペ」の代償

リポジトリが1つから50へと増えるとき、それは単にコードが増えるだけでなく、インフラの回復力が試される時でもあります。そこには、静かな生産性キラーが潜んでいます。それが「YAMLの重複」です。リポジトリAのために完璧なCI/CDパイプラインを構築したとします。次にリポジトリBでも同じロジックが必要になり、.github/workflows/main.ymlファイルをコピーします。リポジトリJに達する頃には、10個の同一ファイルを世話し続けることになります。

組織全体でセキュリティスキャナーを更新したり、Node.jsをv18からv20に上げたりする必要がある場面を想像してみてください。20のプルリクエストを作成し、20のビルドを待ち、どこか一行でも修正し忘れていないか祈る必要があります。この手作業こそが、DevOpsが排除すべき対象です。

以前の職場で、私は最初の1週間を費やして、42個の別々のリポジトリにあるDockerレジストリURLを手動で更新しました。34時間もかかったその作業は、心が折れそうなほど退屈なものでした。もし「信頼できる唯一の情報源(Single Source of Truth)」があれば回避できたはずです。「YAMLエンジニア」から真のDevOpsアーキテクトへと脱皮するには、抽象化の技術を習得しなければなりません。

GitHub Actionsには、このための2つの主要なツールがあります。それがComposite Actionsと**Reusable Workflows**です。これらは似ているように見えるかもしれませんが、DRY(Don’t Repeat Yourself:同じことを繰り返さない)なパイプラインを目指す上では異なる役割を担います。

ツールボックス:レンガか、設計図か

早い段階で適切なツールを選択することで、後々のアーキテクチャ上の問題を回避できます。プレキャストコンクリート板を使えるのに、個々の砂粒から家を建てたいとは思いませんよね。

Composite Actions:カスタムのレゴブロック

A Composite Actionは、複数のワークフロー・ステップを単一の呼び出し可能なアクションにまとめます。ジョブの中に存在する「プライベート関数」のようなものだと考えてください。常に同じ環境でセットで行われる、繰り返しのシェルコマンドに使用します。

  • スコープ:単一のジョブ内で動作。
  • 構造:action.ymlファイルで定義。
  • 必須要件:すべてのrunステップでshell(例:shell: bash)を明示的に宣言する必要がある。
  • 最適な用途:特定のCLIツールのインストール、クラウド認証情報の設定、または標準的なクリーンアップスクリプトの実行など。

Reusable Workflows:標準化された組立ライン

Reusable Workflowは、別のワークフローからトリガーできるYAMLファイル全体を指します。アクションとは異なり、ジョブ、ランナー、ロジックを含む完全な設計図です。これらは、CI/CDプロセス全体の「ゴールデンパス(黄金律)」として機能します。

  • スコープ:ジョブレベルで呼び出し。
  • トリガー:on: workflow_callイベントによって有効化。
  • 制御:呼び出し元から必要な入力(inputs)やシークレット(secrets)を厳密に定義できる。
  • 最適な用途:100以上のマイクロサービスのビルド・テスト・デプロイのライフサイクル全体を標準化する場合。

信頼できる唯一の情報源を構築する

実践してみましょう。devops-assetsという名前の中央リポジトリを作成します。ここに共有ロジックを格納し、組織内の任意のプロジェクトリポジトリから呼び出せるようにします。

ステップ1:環境セットアップ用のComposite Actionを作成する

devops-assets内に、.github/actions/setup-node-cache/action.ymlというパスを作成します。これはNode.jsのインストールと依存関係のキャッシュを処理します。通常、各リポジトリで10〜15行のYAMLを必要とする繰り返しのタスクです。

# devops-assets/.github/actions/setup-node-cache/action.yml
name: 'Node環境のセットアップとキャッシュ'
description: 'Node.jsのインストールとnpmキャッシュの設定を行います'
inputs:
  node-version:
    description: '使用するNode.jsのバージョン'
    required: true
    default: '20'

runs:
  using: "composite"
  steps:
    - name: Node.jsのセットアップ
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}

    - name: 依存関係のキャッシュ
      uses: actions/cache@v4
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

    - name: 依存関係のインストール
      shell: bash
      run: npm ci

ここでshell: bashという行は必須です。これにより、ランナー의 デフォルトのOSシェルに関係なく、スクリプトが予測通りに実行されるようになります。

ステップ2:テスト用のReusable Workflowを設計する

次に、.github/workflows/standard-ci.ymlに完全なテストスイートの設計図を作成します。このワークフローは、先ほど作成したComposite Actionを使用します。

# devops-assets/.github/workflows/standard-ci.yml
name: 標準CI

on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '20'
    secrets:
      SONAR_TOKEN:
        required: false

jobs:
  test-and-lint:
    runs-on: ubuntu-latest
    steps:
      - name: コードのチェックアウト
        uses: actions/checkout@v4

      - name: 作成したComposite Actionを使用
        uses: your-org/devops-assets/.github/actions/setup-node-cache@v1
        with:
          node-version: ${{ inputs.node-version }}

      - name: テストの実行
        run: npm test

workflow_callトリガーがゲートウェイとなります。これにより、このファイルが他のリポジトリから再利用可能なライブラリ関数のよう扱えるようになります。

ステップ3:軽量な呼び出し元ワークフロー

では、アプリケーションリポジトリ側がどれほどスッキリするか見てみましょう。my-web-app/.github/workflows/ci.ymlには、これだけ記述すれば十分です。

# my-web-app/.github/workflows/ci.yml
name: CI

on:
  push:
    branches: [ main ]

jobs:
  call-standard-ci:
    uses: your-org/devops-assets/.github/workflows/standard-ci.yml@v1
    with:
      node-version: '22'
    secrets: inherit

100行あったワークフローを10行に削減できました。明日、すべてのプロジェクトにセキュリティスキャンを追加する必要が出てきたら、devops-assetsにある1つのファイルを更新するだけで、その変更が即座にすべての場所に反映されます。

集中管理型パイプラインを運用するためのルール

集中管理は管理を簡素化しますが、単一障害点(Single Point of Failure)も生み出します。共有ワークフローを壊してしまうと、会社全体のデプロイ能力を停止させてしまいます。

1. バージョンを固定する

決して@mainを使用しないでください。mainブランチに破壊的な変更をプッシュすると、すべてのプロジェクトが一度に失敗します。常にタグ(例:@v1.2.0)またはコミットSHAを使用してください。変更はブランチでテストし、タグを付け、その後各プロジェクトを段階的にアップグレードします。

2. シークレットの受け渡し

シークレットは自動的には呼び出し先に引き継がれません。明示的に渡すか、secrets: inheritを使用する必要があります。内部ツールにはinheritが便利ですが、セキュリティ監査や最小権限の原則の観点からは、workflow_callブロックでシークレットを明示的に定義する方が望ましいです。

3. 境界線を知る

ビルドジョブが終わってからデプロイジョブを開始するなど、複数のジョブを調整する必要がある場合は、**Reusable Workflow**が唯一の選択肢です。単にnpm installaws configureを千回も入力するのを避けたいだけなら、**Composite Action**を使いましょう。

DevOps成熟度への飛躍

中央集権的なアーキテクチャへの移行は、大きなマイルストーンです。これはフォーカスを「動かすこと」から「スケールさせること」へとシフトさせます。インフラをReusable Workflowsに抽象化することで、開発者はYAMLの構文ではなく機能開発に集中できるようになります。まずは小さく始めましょう。今日、繰り返されているセットアップタスクを1つ見つけ、それをComposite Actionに変えてみてください。将来の自分、そしてオンコールの担当者は、メンテナンスの負債が減ったことに感謝するはずです。

Share: