🌟

増えすぎた GitHub Actions を「3 層アーキテクチャ」で整理した話

に公開

はじめに

GitHub Actions (GHA)、便利ですよね。

便利なんですが、じゃんじゃん作るとワークフローが増えてエラいことになりませんか?私はなりました。

  • 環境ごとに似たようなワークフローが乱立し、保守が大変
  • 変更を加える際、複数のファイルを修正する必要がある
  • ワークフローの構造がアプリケーションごとに異なり、認知負荷が高い
  • そもそも数が多すぎて何が何やら...

GHA は特にこう構成すれば良い、みたいなプラクティスやフレームワークが無いので、リポジトリの規模が大きく複雑いなってくるにしたがい、GHA も複雑になってしまいがちだと思います。

本記事では、そのような状況から脱却するべく、 3 層アーキテクチャ を採用してリファクタリングした事例を紹介します。

対象読者

  • GitHub Actions を使い倒している方
  • 割と大きめな規模の GitHub Actions を構成している方
  • 大量のワークフローにお悩みの方

課題: 大量のワークフロー乱立

リファクタリング前は、以下のように環境ごと、アプリごとにワークフローが分かれていました。

❌ 変更前
deploy-app1-prod.yml
deploy-app1-stg.yml
deploy-app1-dev.yml
deploy-app2-prod.yml
deploy-app2-stg.yml
deploy-app2-dev.yml
...

じゃんじゃん増えたワークフローの様子はこちらです。ひぃ... ↓
gha_chaos

この構成には以下のような問題がありました:

  1. 重複コードが多い: 各環境のワークフローでほぼ同じロジックを記述
  2. 保守コストが高い: 修正時に複数ファイルを更新する必要
  3. ロジックが統一されていない: app ごとに同じことをしているはずなのに微妙にロジックが違う

解決策: 3 層アーキテクチャの導入

これらの課題に対し、 責務を明確に分離した 3 層アーキテクチャ を採用しました。

✅ 変更後
_deploy-app1.yml (Reusable Workflow)
  ↑
  ├─ on_release_deploy-app1-prod.yml (env: prod)
  ├─ on_push_deploy-app1-stg.yml (env: stg)
  └─ on_dispatch_deploy-app1-dev.yml (env: dev)

_deploy-app2.yml (Reusable Workflow)
  ↑
  ├─ on_release_deploy-app2-prod.yml (env: prod)
  ├─ on_push_deploy-app2-stg.yml (env: stg)
  └─ on_dispatch_deploy-app2-dev.yml (env: dev)

...

3 層アーキテクチャの全体像

名称が安直なのはご容赦ください 🙏 変にもじるよりかはストレートで分かりやすいだろうと思ってます。

各層の役割と実装

1. Trigger 層 (トリガー層)

責務: 外部イベントを受け取り、環境固有のパラメータを設定する

Trigger 層は、GitHub のイベント (release, push, workflow_dispatch) を受け取るエントリーポイントです。デプロイロジックは一切含まず、パラメータの定義のみ を行います。

環境別トリガーの固定化

各環境で使用するトリガーを固定することで、一貫性を確保します。環境とトリガーとの対応は GitHub Flow をベースにしています。

環境 トリガー タイミング ファイル名
prod release GitHub リリース作成時 on_release_deploy-xxx-prod.yml
stg push main ブランチへのマージ時 on_push_deploy-xxx-stg.yml
dev workflow_dispatch 手動実行 on_dispatch_deploy-xxx-dev.yml

実装例: stg 環境の Trigger 層

on_push_deploy-app1-stg.yml
name: "[app1/stg] Build & Deploy"

on:
  push:
    branches:
      - main
    paths:
      - "services/app1/**"

env:
  SERVICE: app1
  ENVIRONMENT: stg

jobs:
  vars:
    runs-on: ubuntu-latest
    outputs:
      env: ${{ env.ENVIRONMENT }}
      account_id: ${{ steps.get_account_config.outputs.account_id }}
      deploy_role: ${{ steps.get_account_config.outputs.deploy_role }}
    steps:
      - uses: actions/checkout@v5
      - id: get_account_config
        uses: ./.github/actions/get_account_config
        with:
          service: ${{ env.SERVICE }}
          env: ${{ env.ENVIRONMENT }}

  deploy_app:
    needs: vars
    uses: ./.github/workflows/_deploy-app1.yml
    with:
      env: ${{ needs.vars.outputs.env }} # Reusable Workflow では inputs に環境変数を受け取れないため、前ジョブの出力にして渡している
      account_id: ${{ needs.vars.outputs.account_id }}
      deploy_role: ${{ needs.vars.outputs.deploy_role }}

ポイント:

  • 環境固有の情報 (SERVICE, ENVIRONMENT) のみを定義
  • Reusable Workflow への呼び出しに徹する
  • デプロイロジックは一切含まない

2. Reusable Workflow 層 (再利用ワークフロー層)

責務: ビジネスロジックの実装と、差分チェックやビルド・デプロイのオーケストレーション

この層では、workflow_call で呼び出される再利用可能なワークフローを定義します。複数の環境から共通して利用されるため、環境に依存しない汎用的な実装 が重要です。

実装例: Reusable Workflow

_deploy-app1.yml
name: "[app1] Build & Deploy (Reusable Workflow)"

on:
  workflow_call:
    inputs:
      env:
        required: true
        type: string
        description: "Environment name"
      account_id:
        required: true
        type: string
        description: "Account ID for deployment"
      deploy_role:
        required: true
        type: string
        description: "IAM role ARN for deploy app1"
      deploy_all:
        required: false
        type: boolean
        default: false
        description: "Deploy all components regardless of diff"

jobs:
  check_diff:
    runs-on: ubuntu-latest
    outputs:
      diff_backend: ${{ steps.diff_backend.outputs.changed_files }}
      diff_frontend: ${{ steps.diff_frontend.outputs.changed_files }}
    steps:
      - uses: actions/checkout@v5

      # バックエンドの差分チェック
      - id: diff_backend
        if: ${{ !inputs.deploy_all }}
        uses: ./.github/actions/get_diff
        with:
          file_patterns: |
            services/backend/**
          ignore_file_patterns: |
            services/backend/**/*.md

      # フロントエンドの差分チェック
      - id: diff_frontend
        if: ${{ !inputs.deploy_all }}
        uses: ./.github/actions/get_diff
        with:
          file_patterns: |
            services/frontend/**
          ignore_file_patterns: |
            services/frontend/**/*.md

  # 差分がある場合のみバックエンドをデプロイ
  deploy_backend:
    needs: check_diff
    # 前ジョブの一部がキャンセルされても実行できるように !(failure() || cancelled()) を指定
    if: ${{ ! (failure() || cancelled()) && (needs.check_diff.outputs.diff_backend != '' || inputs.deploy_all) }}
    uses: ./.github/workflows/_deploy_backend.yml
    with:
      env: ${{ inputs.env }}
      account_id: ${{ inputs.account_id }}

  # 差分がある場合のみフロントエンドをデプロイ
  deploy_frontend:
    needs: check_diff
    if: ${{ ! (failure() || cancelled()) && (needs.check_diff.outputs.diff_frontend != '' || inputs.deploy_all) }}
    uses: ./.github/workflows/_deploy_frontend.yml
    with:
      env: ${{ inputs.env }}
      account_id: ${{ inputs.account_id }}

ポイント:

  • 標準的な入力パラメータ (env, account_id, deploy_all など) を受け取る
  • 差分チェックの結果に基づいてデプロイを制御
  • 環境に依存しない汎用的な実装

3. Composite Action 層 (コンポジットアクション層)

責務: 基本的な操作を組み合わせた再利用可能なアクションの提供

この層では、.github/actions/ 配下に配置される共通処理を Composite action として定義します。実装例については OSS として様々な action が提供されているため本記事では割愛します。

実装のポイント

1. 命名規則の統一

ワークフローファイルは、トリガー種別に応じたプレフィックスを付与します。

トリガー種別 プレフィックス
release on_release_ on_release_deploy-xxx-prod.yml
push on_push_ on_push_deploy-xxx-stg.yml
workflow_dispatch on_dispatch_ on_dispatch_deploy-xxx-dev.yml
workflow_call _ _deploy-xxx.yml

この命名規則により、ファイル名から 役割とトリガーが一目で分かる ようになります。

2. 標準インターフェースの定義

Reusable Workflow は、以下の標準的な入力パラメータを受け取ります。

on:
  workflow_call:
    inputs:
      env:
        required: true
        type: string
        description: "Environment name"
      account_id:
        required: true
        type: string
        description: "Account ID for deployment"
      deploy_role:
        required: true
        type: string
        description: "IAM role ARN for deploy app1"
      deploy_all:
        required: false
        type: boolean
        default: false
        description: "Deploy all components regardless of diff"

この標準化により、新規サービス追加時のパターンが明確 になります。

3. ドキュメントの充実

ここまでの内容を開発ガイドラインとしてまとめ、チームメンバーに共有します。

  • 3 層アーキテクチャの概要と各層の責務
  • 環境別トリガー管理の方針
  • 命名規則
  • 差分ベースデプロイの仕組み
  • 標準インターフェース
  • 新規サービス追加方法
  • 検証方法

これにより、チーム全体で 一貫した実装方針を共有 できます。

導入による効果

まだ導入したてではありますが、以下の効果を期待しています。

  1. 責務の明確な分離: 各層が単一の責務を持つ
  2. 再利用性の向上: Reusable Workflow と Composite Action による共通化
  3. 保守性の向上: 変更影響範囲が明確で、修正が容易
  4. 拡張性: 新規サービス追加時のパターンが明確

さいごに

GHA の 整理の仕方の一例として、3 層アーキテクチャについて紹介しました。GHA のワークフロー管理でお困りの方の参考になれば幸いです。

もし、他にこんなプラクティス導入してるよ!こんな方法知ってるよ!という方がいましたら、是非コメントいただけますと嬉しいです。

謝辞

これらのアイディアは Claude Code に壁打ちしてもらいながら構築し、本記事も一部 Claude Code に手伝ってもらっています。

いつも大変お世話になっております 🙇

SimpleForm Tech Blog

Discussion