GitHub Action の workflow_dispatchを用いたmonorepoの部分デプロイ

## はじめに

この記事は [Wano Group Advent Calendar 2024](https://qiita.com/advent-calendar/2024/wano-group) の10日目の記事です。

本稿では、私たちのチームが GitHub Actions を使用してどのようにデプロイを実施しているかについて記載します。

## Goのモノレポ構成について

私たちのチームでは、Go言語を中心として利用しており、モノレポ（monorepo）で管理しています。この構成に至るまでには様々な試行錯誤があり、以下のような特徴を持つ構成へと発展させてきました。


### リポジトリの基本構造

projectフォルダの下に各モジュールが並んでいます。
それぞれ`go.mod`を持っています。
```
project
├── README.md
├── zzzz
├    ├── go.mod
├── yyy
└── zzzz
```




マイクロサービスを進めていたり分散モノリス化してしまっているわけではなく、純粋にデプロイライフサイクルの違うアプリケーション群をある程度の論理的な塊感を持って分けています。

業務ドメインロジックとの関わりが深いものに関しては、npmモジュールやPythonなどの非Goプロジェクトも含むこともあります。
(あまりにもデプロイライフサイクルの違うものや、非機能的な非ドメイン要素に属するものは別のリポジトリで管理しています。)


## workflow_dispatchとは

デプロイにはGitHub Actionsの機能である[workflow_dispatch](https://docs.github.com/ja/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows)を[GitHub CLI](https://cli.github.com/)経由で実行しています。

workflow_dispatchは、GitHub Actionsの機能の一つで、手動でワークフローを実行できる仕組みです。この機能の特徴は、

- 実行時にパラメータを渡すことが可能
- 特定のブランチやタグを指定してデプロイできる
- 複数のワークフローを連携させやすい

この柔軟性により、モノレポ内の特定のプロジェクトを選択的にデプロイすることが可能になります。


## 実際のデプロイフロー


実際のデプロイは、[GitHub CLI](https://cli.github.com/)を使用して以下のように実行しています。

```bash
gh workflow run deploy -r <branch/tag> --field env=<production/stage> --field app=<対象の傘下プロジェクト>
```

例えば、`A`、`B`、`C`という3つのデプロイ論理単位があり、Aだけデプロイする場合、以下のようにデプロイします。

```bash
gh workflow run deploy -r feature/add-emoji-deploy --field env=stage --field app=A
```

このコマンドにより、特定のブランチから特定の環境に、選択したプロジェクト群のみをデプロイしています。

## GitHub CLIで発火されているものはなにか


上記のコマンドを受けるworkflow側では、以下のように、親/イベント起点となる`deploy.yml`とそれによって呼ばれる`_call.deploy_A.yml`のようなデプロイ単位によって別れています。

どれを発火させるか決めるのは`deploy.yml`であり、`_call.deploy_A.yml`などは個別のデプロイジョブの単なる集合体となっています。

```
.github
├── actions
│   └── shared-action
├── pull_request_template.md
└── workflows
    ├── _call.deploy_A.yml
    ├── _call.deploy_B.yml
    ├── _call.deploy_C.yml
    ├── _call.deploy_OTHER.yml
    ├── deploy.yml
    ├── set-aws-deploy-secrets-dispatch.sh
    └── test-result-slack.yml

```

親となるGitHub Action

```yml:.github/deploy.yml
name: 'deploy'
run-name: デプロイ ${{ github.event.inputs.env }} by @${{ github.actor }} ( ${{ github.event.inputs.app }} )

on:
  workflow_dispatch:
    inputs:
      env:
        description: 'production / stage'
        required: true
      app:
        description: 'デプロイ対象'
        default: 'A,B,C,OTHER'

env:
  # 組織シークレット
  ...


jobs:

  call_A:
    uses: ./.github/workflows/_call.deploy_A.yml
    if: contains(github.event.inputs.app, 'A')
    with:
      MY_ENV: ${{ github.event.inputs.env }}
    secrets: inherit

  call_B:
    uses: ./.github/workflows/_call.deploy_B.yml
    if: contains(github.event.inputs.app, 'B')
    with:
      MY_ENV: ${{ github.event.inputs.env }}
    secrets: inherit

  call_C:
    uses: ./.github/workflows/_call.deploy_OTHER.yml
    if: contains(github.event.inputs.app, 'OTHER')
    with:
      MY_ENV: ${{ github.event.inputs.env }}
    secrets: inherit

  call_OTHER:
    uses: ./.github/workflows/_call.deploy_C.yml
    if: contains(github.event.inputs.app, 'C')
    with:
      MY_ENV: ${{ github.event.inputs.env }}
    secrets: inherit

  on-request:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v4

      - name: Dump GitHub context
        env:
          GITHUB_CONTEXT: ${{ toJSON(github) }}
        run: echo "$GITHUB_CONTEXT"

      - name: Slack Notification
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_WEBHOOK: ${{ env.MY_SLACK_WEBHOOK_DEPLOY }}
          SLACK_TITLE: 🦾 A デプロイ開始  ${{ github.event.inputs.env }}
          SLACK_COLOR: '#6f7a72'
          SLACK_MESSAGE: |
            env : ${{ github.event.inputs.env }}
            app : ${{ github.event.inputs.app }}

  on-result:
    runs-on: ubuntu-20.04
    needs: [call_A, call_B, call_OTHER , call_C , ]
    if: ${{ always() }}
    steps:
      - name: checkout
        uses: actions/checkout@v4
      - name: Dump GitHub context
        env:
          GITHUB_CONTEXT: ${{ toJSON(github) }}
        run: echo "$GITHUB_CONTEXT"

      - name: Slack Notification
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_WEBHOOK: ${{ env.MY_SLACK_WEBHOOK_DEPLOY }}
          SLACK_TITLE: 🚀 A  ${{ github.event.inputs.env }} デプロイ結果
          SLACK_COLOR: '#42555c'
          SLACK_MESSAGE: |
            対象ワークフロー
            
            A: ${{ needs.call_A.result == 'success' && '✅' || needs.call_A.result == 'skipped' && 'skipped' || '🔥' }}
            B: ${{ needs.call_B.result == 'success' && '✅'  || needs.call_B.result == 'skipped' && 'skipped' || '🔥' }}
            OTHER: ${{ needs.call_OTHER.result == 'success' &&  '✅' || needs.call_OTHER.result == 'skipped' && 'skipped' || '🔥'  }}
            C: ${{ needs.call_C.result == 'success' && '✅' || needs.call_C.result == 'skipped' && 'skipped' || '🔥'  }}


```

`if: contains(github.event.inputs.app, 'B')` この if構文が特徴で、これによってGutHub CLIからのデプロイ命令をフィルタしています。



上記のdeployによって呼ばれる子YAMLは以下です。
` workflow_call`でいくつかのオプション等を受け取れる他は普通のjobの羅列ですので割愛します。

```yaml:.github/workflows/_call.deploy_C.yml
name: _call.B

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

env:
  REGION: ap-northeast-1          
....

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 12
    env:
      GOCACHE: /tmp/.cache-go
    strategy:
      matrix:
        environment:
            - TITLE: (1) xxxxxx
              DEPLOY: |
....
```




## 最後に - GitHub Actions × モノレポの評価

1年ほどGitHub Actionでこのデプロイ方法を運用してきてどうだったか。

### メリット

1. コードの一元管理
   - 全てのプロジェクトが1つのリポジトリに集約され、依存関係の把握が容易
   - デプロイスクリプトの共通化が可能

2. 開発効率の向上
   - プロジェクト間の変更の影響を即座に確認可能
   - 共通コードの更新が一括で行える

### デメリット

1. キャッシュの制限
   - エンタープライズプランでも1リポジトリあたり**10GBという制限**
   - 大規模なモノレポではすぐに限界に達する可能性

2. 対策
   - 頻繁なリリースが必要なプロジェクトを特定し、優先的にキャッシュを活用
   - 必要に応じてキャッシュのクリーンアップを実施

...キャッシュ周りがなんかもうちょっとならないかなあ、という感想ですね。
簡単ですが本稿を終わります。

明日は[nokazn](https://qiita.com/nokazn)さんです。乞うご期待。