この記事はコンシューマーチームブログリレー5日目の記事です。
こんにちは。エムスリーのコンシューマーチームエンジニアの園田です。
以前のポストにもあるように、エムスリーでは GitLab から GitHub EE へ移行しています。
コンシューマーチームで管理している GitLab リポジトリも、いくつか GitHub に移行しました。当然ですが、GitLab CI の CI/CD パイプラインは GitHub Actions に置き換える必要があります。移行前は「GitLab でできることは GitHub でもできるんでしょ?」と軽く考えていたのですが、実際に移行を進めてみると、想定外の制限や落とし穴に数多く遭遇しました。
本稿では、GitLab CI から GitHub Actions へ移行する際に直面したいくつかの課題と、それぞれの解決策を共有します。これから GitHub への移行を検討されている方の参考になれば幸いです。
お断り: この記事は社内LTで発表した内容を生成AIでリライトしたものです。ほぼ手直ししていますが、絵文字など生成 AI っぽい箇所も残っています。
課題1: 開発ブランチでの手動実行ができない
最初に遭遇したのは、開発中のブランチでワークフローを手動実行できないという問題でした。
GitLab CIでは、when: manual
を使うことで、任意のブランチで任意のジョブを手動実行できます。これは開発中のデバッグやテストに非常に便利な機能でした。
しかしGitHub Actionsのworkflow_dispatch
トリガーは、デフォルトブランチ(通常はmain)にマージされたワークフローファイルでのみ手動実行が可能という制限があります。
つまり、次のような状況では手動実行ができません:
- 新しいワークフローを開発中のブランチ
- デバッグ用の一時的なジョブを追加したブランチ
- 既存ワークフローを修正中のブランチ
GitLabのwhen: manual
との比較
GitLab CIでは、次のように簡単に手動実行ジョブを定義できました:
debug-job: stage: test when: manual script: - echo "Debug information..." - ./debug-script.sh
このジョブは、どのブランチからでもGitLabのUIから手動で実行できます。開発中の機能をテストしたり、本番環境へのデプロイ前に最終確認したりする際に重宝していました。
ワークアラウンド
こちら、ざっと調べただけでも多くの方が同じ問題に直面しているようで、ワークアラウンドも探せばすぐに見つかりました:
こちらの記事を参考に次のような「プレースホルダー」となるワークフローを定義しました:
name: Debug Workflow on: workflow_dispatch: inputs: debug_mode: description: 'Debug mode' required: false type: boolean jobs: nothing: runs-on: ubuntu-latest steps: - name: Placeholder run: echo "This is a placeholder job for manual dispatch"
開発時の手順:
debug.yml
に実際のデバッグロジックを実装- 開発ブランチにコミット
- Actions の
debug.yml
を実行し、開発ブランチを選択して手動実行(開発ブランチのコードが使われる) - デバッグ完了後、
debug.yml
の内容を本来あるべきファイルに転記する debug.yml
を元のプレースホルダーに戻す
運用での課題
この方法でも次のような不便さが残ります:
- デバッグ後に本来のファイルに転記する作業と、debug.yml を元に戻す作業を忘れるリスクがある
- このファイルが存在すること自体がなんか気持ち悪い
- ワークフローのリストに表示される名称も変更されるため紛らわしい
こういった課題を解決・軽減可能なナレッジがあれば知りたいです。
課題2: CI成功時のみPRマージ可能とする設定の罠
次に見つかった大きな問題のひとつとして、「CI成功時のみPRマージを許可する」という基本的な設定に関するものでした。
GitLabでは、Merge Requestに対して「パイプラインが成功したらマージ可能」という設定が簡単に行えます。
これと似たような機能で GitHub では「ジョブ名を指定して、それらのジョブがすべて成功していたらマージ可能にする」という設定が可能です。
しかし、この機能を利用して制限をかけると、条件分岐(if
)でスキップされたジョブがステータスチェックの対象になってしまい、マージがブロックされるという問題が発生しました。
具体的には、次のようなワークフローを実装していました:
changes: runs-on: ubuntu-latest outputs: changed: ${{ steps.changes.outputs.changed }} steps: - uses: actions/checkout@v5 - id: changes run: | echo "changed=true" >> $GITHUB_OUTPUT lint: needs: changes if: ${{ needs.changes.outputs.changed == 'true' }} runs-on: ubuntu-latest steps: - name: Run linter run: echo "Linting code..."
このワークフローで、changed == 'false'
の場合、lint
ジョブはスキップされます。しかし、ブランチ保護ルールでlint
をステータスチェック対象に設定していると、スキップ = 未実行 = マージ不可となってしまうのです。
解決策
この問題を解決するため、すべてのジョブの結果を集約する「チェックポイントジョブ」を実装しました:
check-all-jobs: runs-on: ubuntu-latest needs: [changes, lint, test, build] if: always() steps: - name: Check merge conditions if: | contains(needs.*.result, 'failure') || contains(needs.*.result, 'canceled') run: exit 1 - name: All checks passed run: echo "All required checks passed successfully"
if
に always()
を指定しているので、needs
で指定したジョブが失敗していようがスキップされていようが必ず実行されます。
そのうえで、step の中で if
により成否を判定しています。
このチェックポイントジョブのみをステータスチェック対象とすることにより、スキップされたジョブは成功したジョブとして扱えるようになります。
課題3: Slack通知がノイジー
こちらは GitHub の問題ではなく Slack が提供している公式の GitHub Slack App の問題なのですが、通知設定が複雑でノイジーにならないように設定するのが非常に困難です。
GitLabでの設定方法
GitLabでは、Slackへの通知設定が非常にシンプルでした。プロジェクトの設定画面から、GUIで次のような操作が可能です:
- Slack Integrationを有効化
- Webhook URLを入力
- 通知したいイベントをチェックボックスで選択
- パイプライン成功/失敗
- デプロイメント
- マージリクエスト
- など
すべての設定がGUI上で完結し、直感的に管理できました。
GitHubでの現実
一方、GitHub Slack App での通知設定は、コマンドベースで行う必要があります。
Slackへの通知を設定するには、各リポジトリで次のようなコマンドを実行します:
# リポジトリをSlackチャンネルに接続 /github subscribe owner/repo # 通知種別を個別に設定 /github subscribe owner/repo issues /github subscribe owner/repo pulls /github subscribe owner/repo deployments /github subscribe owner/repo commits:main # 不要な通知を解除 /github unsubscribe owner/repo deployments
この方法には次の課題があります:
- 設定が煩雑: リポジトリごと、通知種別ごとにコマンドを実行する必要がある
- フィルタリングの制限: 通知内容の細かいフィルタリングができない、または限定的
- 全通知か選択通知か: すべて受け取るとノイジーだが、絞りすぎると重要な通知を見逃す
- 組み合わせが必要: 例えば、プルリクエストとそのコメントは別の通知種別なので、両方購読する必要がある
特に大きな課題
デプロイの承認依頼も Slack 通知で受け取りたいのですが、こちらは deployments
ではなく workflows
の通知となります。
ところが、この workflows
を購読すると、すべてのジョブの通知が飛んでくるのでとんでもなくノイジーです。
本番環境へのリリース承認依頼通知が埋もれてしまってリリースが滞る、といったことが実際にありました。
実際の運用での対処法
workflows
をすべて購読するとノイジーなのですが、workflows
の購読はフィルタリング構文があるので、それを利用してデプロイに関連するワークフローのみを購読する、デプロイ専用のチャンネルを作成しました。
- リリースチャンネル:
releases
を購読 - デプロイ関連チャンネル:
deployments
および デプロイ関連ジョブのみに限定したworkflows
を購読 - PR/レビュー関連チャンネル:
pulls
、reviews
,comments
などを購読
とはいえ、これをやってもまだまだ全然ノイジーです。
より運用性を向上させるのであれば、Slack SDK などを使って通知の仕組みを整えてあげる必要がありそうです。
まとめ
- GitHubの通知設定はコマンドベースで煩雑
- フィルタリングができない/限定的なため、ノイジーになりがち
- デプロイ承認やレビュー依頼などは複数の通知種別を購読する必要がある
- 通知種別ごとにSlackチャンネルを分けて対処している
課題4: ジョブ共通化の制限とその影響
GitLabの柔軟性
GitLab CIでは、ジョブの共通化に関して YAML 構文を利用した柔軟な仕組みが用意されています。
.template: &template before_script: - echo "Common setup" script: - echo "Common processing" job1: extends: .template *template script: - !reference [.template, script] - echo "Additional processing for job1" include: - local: '.gitlab/ci/templates.yml' - project: 'group/common-templates' file: '/templates/deployment.yml'
このように、GitLabでは以下が可能です:
extends
やによる継承
!reference
による部分的な参照と拡張include
による外部テンプレートの取り込み- 柔軟な組み合わせと上書き
GitHubの2つの選択肢
GitHub Actionsには、ジョブの共通化に2つの方法があります:
1. Composite Action(ステップのまとまり)
name: 'Setup Environment' description: 'Common setup steps' runs: using: "composite" steps: - name: Checkout uses: actions/checkout@v3 - name: Setup Node uses: actions/setup-node@v3 with: node-version: '18' - name: Cache dependencies uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} jobs: build: runs-on: ubuntu-latest steps: - uses: ./.github/actions/setup - name: Build run: npm run build
Composite Actionの特徴:
- ✅ ステップの再利用が可能
- ✅ ローカルまたは公開アクションとして利用可能
- ❌
uses: actions/checkout@v3
などの特定のアクションと組み合わせにくい - ❌ UI上でログがまとめられ、デバッグ時の視認性が悪い
- ❌ ジョブレベルの設定(runs-onなど)は共通化できない
2. Reusable Workflow(ワークフロー全体)
name: Reusable Deploy Workflow on: workflow_call: inputs: environment: required: true type: string secrets: DEPLOY_TOKEN: required: true jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Deploy run: ./deploy.sh env: TOKEN: ${{ secrets.DEPLOY_TOKEN }} jobs: production-deploy: uses: ./.github/workflows/reusable-deploy.yml with: environment: production secrets: DEPLOY_TOKEN: ${{ secrets.PRODUCTION_TOKEN }}
Reusable Workflowの特徴:
- ✅ ワークフロー全体の再利用が可能
- ✅
secrets
の引き継ぎが可能(強力な機能) - ❌ 部分的な取り込みや上書きができない
それぞれの制限事項と使い分け
GitLab CIの柔軟性に比べると、GitHub Actionsの共通化には明確な制約があります:
要件 | GitLab CI | Composite Action | Reusable Workflow |
---|---|---|---|
ステップの再利用 | ✅ | ✅ | ✅ |
部分的な参照・拡張 | ✅ | ❌ | ❌ |
ジョブレベル設定の共通化 | ✅ | ❌ | ✅ |
外部ファイルインクルード | ✅ | ✅ | ✅ |
GitHub でもYAML Anchor を使って部分的な参照は可能になりましたが、GitLab と違いマージ(
)が使えないため、実際に利用可能なユースケースはほとんどありません。
まとめ
- 共通化は「Composite Action」と「Reusable Workflow」を適材適所で使い分ける
- Composite Action はステップをまとめられるが UI 出力が見づらい
- Reusable Workflow はワークフロー全体を共通化できるが部分的な参照・拡張ができない
- GitLab からの移行時は、構成の分割と再利用単位の再設計が必要
課題5: Reusable Workflow と Environment の連携方法
これは、移行後に数ヶ月間も勘違いしていた大きな落とし穴でした。
GitHubの公式ドキュメントに書かれている一文を読んで、「Reusable WorkflowではEnvironmentが使えない」と完全に誤解していました。そのため、環境別にワークフローファイルを複製したり、Composite Actionで無理やり回避したりと、不必要に複雑な実装をしていました。
Environmentとは
GitHubの「Environment」は、デプロイ先の環境を定義する機能です。主に次の用途で使用します:
- 環境ごとのシークレット管理: 本番環境とステージング環境で異なるAPIキーなどを管理
- デプロイ承認フロー: 本番デプロイ前に承認を必須にする
- デプロイ履歴の管理: 環境ごとのデプロイ履歴を可視化
何を勘違いしていたのか
公式ドキュメントには以下のように記載されています:
Jobs that call reusable workflows cannot use the
environment
keyword.
この一文を読んで、「Reusable Workflowを使うとEnvironmentの機能がまったく使えない」と思い込んでしまいました。
実際、呼び出し側のジョブレベルでenvironment
を指定することはできません:
jobs: deploy: uses: ./.github/workflows/reusable-deploy.yml environment: production with: env_name: production
実際の解決方法
しかし、Reusable Workflow内のジョブでenvironment
を指定することは可能です。
環境名をinputs
として受け取り、それをReusable Workflow内でenvironment
に指定することで、Environmentの機能を活用できます:
name: Reusable Deploy Workflow on: workflow_call: inputs: environment: required: true type: string secrets: DEPLOY_TOKEN: required: true jobs: deploy: runs-on: ubuntu-latest environment: ${{ inputs.environment }} steps: - uses: actions/checkout@v3 - name: Deploy run: ./deploy.sh env: TOKEN: ${{ secrets.DEPLOY_TOKEN }}
name: Deploy on: push: tags: - 'prod_v*' - 'dev_v*' jobs: deploy-to-prod: if: startsWith(github.ref, 'refs/tags/prod_v') uses: ./.github/workflows/reusable-deploy.yml with: environment: production secrets: inherit deploy-to-dev: if: startsWith(github.ref, 'refs/tags/dev_v') uses: ./.github/workflows/reusable-deploy.yml with: environment: dev secrets: inherit
この方法の利点
- 承認フローが機能する: Environment設定で承認を有効にすれば、デプロイ前の承認が必須になる
- 環境ごとのシークレット管理が簡単: Environment設定でシークレットを管理でき、自動で注入される
- デプロイ履歴が可視化される: GitHub UI上で環境ごとのデプロイ履歴が確認できる
- ワークフローの共通化: 環境別に別々のワークフローを作る必要がなくなる
この解決方法に気づいたのは、以下のZenn記事のおかげです。同じ誤解をしている方は多いと思うので、ぜひ参考にしてください。
重要な注意点: vars
の暗黙的な依存関係
上記の記事にはないのですが、実際に運用してみてわかった重要な注意点があります。
Environment固有のvariablesを使いたい場合、Reusable Workflow側で直接参照することになります:
on: workflow_call: inputs: environment: required: true type: string jobs: deploy: environment: ${{ inputs.environment }} steps: - name: Deploy env: DEPLOY_URL: ${{ vars.DEPLOY_URL }}
問題は、inputs
やsecrets
はon.workflow_call
で明示的にインターフェースを定義できますが、vars
は宣言する方法がありません。
そのため、Reusable Workflowでvars
を使う場合は、暗黙的な依存関係となってしまいます。これを防ぐため、必ず以下のような対応が必要です:
1. コメントで明記する:
on: workflow_call: inputs: environment: required: true type: string
2. READMEやドキュメントに記載する:
- 各Environmentに設定すべきvariablesをドキュメント化
- 必須のvariablesと任意のvariablesを明確に区別
3. Terraform で IaC する:
執筆中に気づいたのですけど、GitHub 公式の Terraform Provider があることを知りました。
Environments や Variables の管理がソースコードでできるので、もっと早く知りたかった・・・!
補足: secrets
の評価タイミング
ちなみに、secrets
は遅延評価されるという仕様があります:
jobs: deploy-to-prod: uses: ./.github/workflows/reusable-deploy.yml with: environment: production secrets: API_KEY: ${{ secrets.API_KEY }}
on: workflow_call: secrets: API_KEY: { required: true } jobs: deploy: environment: ${{ inputs.environment }} steps: - name: Deploy env: API_KEY: ${{ secrets.API_KEY }}
この仕様を見ると、vars
も同じように遅延評価されるのでは?と考えたくなりますが、実際にはvars
は遅延評価されません。vars
は呼び出し側のコンテキストで評価されます。
何の役にも立たない仕様ですが「vars
も同じように遅延評価されるのでは?」と試行錯誤するのを防ぐための参考として記載しておきます。
まとめ
- Reusable Workflowの呼び出し側では
environment
を指定できない(これは事実) - しかし、Reusable Workflow内部のジョブで
environment: ${{ inputs.environment }}
と指定することは可能(これに気づかなかった!) - この方法により、Environmentのすべての機能(承認フロー、環境別シークレット、デプロイ履歴)を活用しながらワークフローを共通化できる
- 重要:
vars
はインターフェースとして宣言できないため、必要なvariablesはコメントやドキュメントに明記すること - 公式ドキュメントの記述は正確だが、誤解を招きやすい。実装前にコミュニティ記事も確認することをおすすめします
その他の課題
移行中に遭遇したその他の課題も簡単に紹介します。
クロスリポジトリのジョブ実行
GitLabのtrigger
機能のような標準機能がなく、GitHub Appの作成とトークン管理が必要です。
steps: - name: Generate token id: generate_token uses: tibdex/github-app-token@v1 with: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} - name: Trigger workflow in other repo uses: actions/github-script@v6 with: github-token: ${{ steps.generate_token.outputs.token }} script: | await github.rest.actions.createWorkflowDispatch({ owner: 'your-org', repo: 'other-repo', workflow_id: 'deploy.yml', ref: 'main' });
キャッシュの実行環境依存
ホストランナーとコンテナランナーではキャッシュが共有されません。
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} - run: npm ci test: runs-on: ubuntu-latest container: node:18 needs: build steps: - uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} - run: npm test
対処法は実行環境を統一するか、Artifactで受け渡す方法があります。
JIRA Server連携
公式統合はJIRA Cloudのみ対応。JIRA Serverとの連携にはGitHub Actionsで自動リンク作成ワークフローを独自実装しました。
on: pull_request: types: [opened, edited] jobs: jira-link: if: contains(github.event.pull_request.title, 'PROJ-') steps: - uses: ./.github/actions/jira-link-issue
まとめ
GitLab CIからGitHub Actionsへの移行は、想像以上に大変な作業でした。表面的には似ている両者ですが、細部の仕様や設計思想の違いにより、多くの課題に直面しました。
主な学び:
- ステータスチェックの考え方が異なる: 個別ジョブの状態管理が重要
- 柔軟性に制限がある: 手動実行、ジョブ共通化など、GitLabほど柔軟ではない
- 通知設定が煩雑: コマンドベースの設定が必要
- Reusable Workflowの制約: Environment との共存に一手間が必要
しかし、これらの課題を乗り越えた今、GitHub Actionsの良い点も見えてきました:
- 豊富なMarketplace: 公開アクションの質と量が素晴らしい
- 活発なコミュニティ: 問題解決のための情報が豊富
- エコシステムの広さ: GitHub全体との統合が強力
- 継続的な改善: 頻繁に新機能が追加される
最後まで読んでいただき、ありがとうございました。本稿がGitHubへの移行を検討されている方にとって、少しでも参考になれば幸いです。
関連リンク
We are hiring
エムスリーでは GitLab や GitHub を活用した CI/CD パイプラインの構築・運用しています。DevOps やインフラ自動化、ワークフロー改善に興味がある方、ぜひ一緒に働きませんか。軽く話を聞いてみるだけでも OK ですので、ぜひともカジュアル面談をお申し込みください!