GitHub Actions の再利用可能ワークフローを用いた様々なイベントで動く複数の CI/CD パイプラインの運用

2023/02/04に公開

はじめに

こんにちは。トドケールの山本(@hayata-yamamoto)です。

今回は、GitHub Actions の再利用ワークフローを題材に、弊社がどのように GitHub Actions を運用しているかを紹介します。

GitHub Actions の再利用ワークフローについて

GitHub Actions の Reusable Workflows とは、GitHub Actions 内で頻繁に用いるワークフローを、再利用可能な状態で切り出すための機能です。

https://docs.github.com/ja/actions/using-workflows/reusing-workflows

例えば、よくあるブランチごとにデプロイする環境を分けるような Action を考える時、従来であれば、環境ごとに YAML ファイルを記載し、デプロイの処理をコピペして利用していました。

しかし、この処理では開発環境で動いた処理が、本番でも同様に動くことを保証しづらく、シークレットキーなどの環境差分に加えて、コピペ時のヒューマンエラーを誘発してしまっておりました。

また、コピペでの運用では、プロダクトが成長し、デプロイの実行制御や実行前後での処理が追加された際に、全ての YAML ファイルに変更を加え、更新を施す必要があり、この際にもヒューマンエラーが発生したりしていました。(私もよくやっていました...)

Reusable Workflow を用いれば、デプロイやビルドなどどの環境でも同様に行われる処理を一箇所にまとめ、シークレットキーや環境ごとに異なる変数を外部から引き渡すことが可能になります。

どのように運用しているか

Speee さんが公開していた運用を参考に弊社では、GitHub Actions のワークフローを運用しています。

https://tech.speee.jp/entry/terraform-reusable-workflow

具体的には以下のような設計でワークフローを管理しています。

実際のディレクトリ構造は以下のような感じになっています。弊社のリポジトリは、モノレポ運用をしているため、ある特定のサービスだけに依存する処理は、<service>-<github-event-name>.yml で定義されています。

.github/
└── workflows/
├── \_backend-deploy.yml
├── \_frontend-deploy.yml
├── \_mobile-build.yml
├── backend-on-pr.yml
├── mobile-on-pr.yml
├── mobile-on-push-develop.yml
├── mobile-on-push-staging.yml
├── on-push-develop.yml
├── on-push-main.yml
└── on-push-staging.yml

フロントエンドのデプロイ処理は以下のように定義しており、引数の指定を変更するだけで、全ての環境で同じ処理が走るような実装にしています。[1]

  • workflow_call.inputs
  • workflow_call.secrets

に要求したい情報を記載していくことで、CircleCI で言うところの commands のような使い方ができる設計となっています。(設定次第で、ワークフロー実行時の環境変数をそのまま引き継ぐこともできるのですが、暗黙的な依存ができてしまうためあまりおすすめはしません)

name: フロントエンドデプロイ用コマンド
on:
  workflow_call:
    inputs:
      amplify-react-app-id:
        description: Amplify React AppId
        required: true
        type: string
      amplify-nextjs-app-id:
        description: Amplify Nextjs AppId
        required: false
        default: ""
        type: string
      aws-default-region:
        description: AWS DEFAULT REGION の値
        default: ap-northeast-1
        required: false
        type: string
      branch-name:
        description: deploy したいブランチ
        required: false
        type: string
        default: develop
    secrets:
      aws-access-key-id:
        description: AWS アクセスキー
        required: true
      aws-secret-access-key:
        description: AWSシークレットキー
        required: true
jobs:
  deploy:
    name: Deploy Frontend to Amplify
    runs-on: ubuntu-latest
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.aws-access-key-id }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.aws-secret-access-key }}
      AWS_DEFAULT_REGION: ${{ inputs.aws-default-region }}
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: Deploy frontend
        run: |
          aws amplify start-job --app-id ${{ inputs.amplify-react-app-id }} --branch-name ${{ inputs.branch-name }} --job-type RELEASE
          aws amplify start-job --app-id ${{ inputs.amplify-nextjs-app-id }} --branch-name ${{ inputs.branch-name }} --job-type RELEASE

また、デプロイ時の処理では以下のような感じで定義しています。バックエンドのデプロイが完了したら、フロントエンドのデプロイが実行される制御を入れています。

name: Workflow on push to develop

on:
  push:
    branches: [develop]

jobs:
  deploy-backend:
    name: Deploy Backend
    uses: ./.github/workflows/_backend-deploy.yml
    with:
      stage: dev
    secrets:
      aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
      aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}

  deploy-frontend:
    name: Deploy Frontend
    uses: ./.github/workflows/_frontend-deploy.yml
    needs:
      - deploy-backend
    with:
      amplify-react-app-id: xxx
      amplify-nextjs-app-id: xxx
    secrets:
      aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
      aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}

どうして、イベントごとにワークフローを定義しているのか?

最後に、よくメンバーからも出る質問に答えてこの記事を終わりにします。

GitHub Actions のサンプルコードを見ていると、<usecase-name>.yml と命名された CI/CD のファイルをよく目にします。例えば、 xxx-deploy.yml とか、 build.yml のような命名のモノです。

これ自体が悪いとは思いませんが、GitHub Actions のように、さまざまなイベントでトリガーが引かれるような CI/CD を管理する場合、あまりわかりやすい命名ではないだろうと考え、今回は採用しませんでした。具体的な理由としては以下のようなことが挙げられます。

  • そのユースケースが、どのタイミングで実行されるのかファイルの中身を見ないとわからないため
  • 複数のイベントに対応するワークフローを記載してしまうと、 if github.event_name == 'pull_request' のような、yaml ファイル内での条件分岐が発生してしまい、ワークフローが複雑になってしまうため。

実際問題、運用時には「〇〇ブランチにプッシュされた時に、XXX って処理が走るようにしよう」といった具合に、ブランチとイベントのペアで物事を考えることが多く、個別のユースケースについては、ジョブの名前にしたり、再利用可能ワークフローの名前にしたりするでも、十分対応が可能です。

であれば、実際に開発・改修を入れる時に考える思考プロセスと同じようにファイル名を定義し運用する方が、初めてコードを読む際の認知負荷を下げることができ、スムーズに作業に取り掛かれると考えて、上記のような設計としました。

終わりに

今回は、GitHub Actions の再利用ワークフローを題材に、弊社がどのように GitHub Actions を運用しているかを紹介しました。各社さまざまな運用があるかと思いますが、弊社は上のような形で CI/CD のフローを管理し、自動化を進めています。

この記事や弊社に少しでもご興味持っていただけたら、是非カジュアル面談などにお越しください!
https://todoker.notion.site/efc2eea5eb054b6e8757fa3553af58d1

脚注
  1. フロントエンドは複数のアプリケーションを運用しているため、app-id が複数存在します。詳細 ↩︎

Discussion