Speee DEVELOPER BLOG

Speee開発陣による技術情報発信ブログです。 メディア開発・運用、スマートフォンアプリ開発、Webマーケティング、アドテクなどで培った技術ノウハウを発信していきます!

GitHub Actions のワークフローファイルを共通化した話

この記事は、Speee Advent Calendar5日目の記事です。 昨日の記事はこちら

tech.speee.jp

はじめに

DX事業本部 開発基盤グループの秋吉です。

Speee では GitHub Actions を積極的に活用しており、開発する上で必要な様々な作業を自動化しています。

一方で、リポジトリや環境数が増加するにつれて同じような GitHub Actions のワークフローファイルが増加していました。 したがって、ワークフローに対して共通の変更を加える場合であっても、全リポジトリ・全環境のワークフローファイルをそれぞれ手動で編集する必要がありました。 GitHub Actions の活用が進みワークフロー数が増加してきた結果、これが開発の妨げとなる技術的負債になっていました。

そこで、本記事では GitHub Actions の Reusable workflow という機能を使用し、それらを共通化したことについて説明します。

背景

まず、GitHub Actions を利用して実現しているこれまでの状況について説明します。

DX事業本部ではインフラのプラットフォームとして主に AWS を利用しており、各インフラを Terraform で管理しています。*1*2 また、Terraform の各種操作を GitHub Actions 上で実行しており、GitHub への操作をトリガーとして Terraform の Plan や Apply といった操作を自動的に実行しています。

インフラリポジトリの構成として、DX事業本部で提供している各サービスごとにリポジトリが分かれています。 そして、各リポジトリでは、主に以下の3種類の GitHub Actions ワークフローを定義しています。

  1. Test
    • terraform plan コマンドを実行するためのワークフロー
    • Pull Request (PR) への Push (PR作成時を含む)をトリガーにワークフローを開始する
    • Plan の結果を PR にコメントする
  2. Test cron
    • terraform plan を定期的(1日に4回)に実行するためのワークフロー
    • 差分を検知した場合に Slack に通知する
  3. Deploy
    • terraform apply を実行するためのワークフロー
    • staging ブランチもしくは production ブランチに PR がマージされたときにワークフローを開始する

これらを図にすると以下のようになります。

f:id:rakiyoshi:20211130170955p:plain
GitHub Actions の構成図

GitHub Actions で Terraform の操作を自動化することで、 GitHub への操作のみでインフラを管理することを実現しています。

課題

Terraform の操作は自動化されており、効率的な開発はできていました。 しかしながら、ここで問題となっていたのは GitHub Actions のワークフローを定義する YAML ファイル自体の運用でした。

DX事業本部では、サービスごとにインフラ管理用リポジトリを分割しており、社内のインフラリポジトリ数は10以上存在しています。 さらに、各リポジトリでは production 環境と staging 環境で構成(tfstate) を分けており、それぞれの環境で Terraform の各種コマンドを実行するための GitHub Actions ワークフローが存在しています。

すなわち、 .github/workflows ディレクトリの最小構成は以下のような状態になっていました。

.github/workflows
├── prod-deploy.yml    # Production 環境デプロイ用ワークフロー
├── prod-test-cron.yml # Production 環境 Plan 定期実行用ワークフロー
├── prod-test.yml      # Production 環境 Plan 用ワークフロー
├── stg-deploy.yml     # Staging 環境デプロイ用ワークフロー
├── stg-test-cron.yml  # Staging 環境 Plan 定期実行用ワークフロー
└── stg-test.yml       # Staging 環境 Plan 用ワークフロー

これがサービス(インフラ管理用リポジトリ)ごとに存在しているというわけです。

上記の構成が最小構成、かつ、インフラリポジトリ数が10以上あるため、GitHub Actions のワークフローの数は Terraform に関するものだけでも約80ファイルも存在していました。

問題は、Test, Test cron, Deploy それぞれのワークフローが、リポジトリや環境が異なったとしてもほとんどが同じ構成だという点です。 つまり、サービスや環境が増えるごとに、似たような GitHub Actions ワークフローの定義ファイルがコピペによって増えていくという非効率的な運用を行っていました。

したがって、ワークフローの定義ファイルに対して共通の変更を加えようとした場合に、小さな変更であったとしてもそれらのファイル全てに変更を加えなければなりませんでした。 当然ながら、複数リポジトリにまたがる約80個のファイルに対して手動で変更を加えるのは非常に手間がかかります。 結果として、初期設定が済んだあとは塩漬けにし、本当に必要な場合以外はほとんどメンテナンスを行うことができませんでした。

ワークフローを再利用する

上述した問題を解決するために、 Reusable workflows という機能を利用することにしました。

こちらの機能は2021年11月にGAになったばかりの機能です。 github.blog

Reusable workflows という機能は、その名の通りワークフローファイルを再利用して複数のワークフローを作成することを可能にします。

使用方法

呼び出される側のワークフローを called workflow 、呼び出す側のワークフローを caller workflowと言います。

called workflow では、ワークフローの開始条件である on の値に workflow_call を指定する必要があります。 また、workflow_call の値にinputs および secrets を指定することで、引数と秘匿値を呼び出す側の caller workflow から受け取ります。

# called.yml (called workflow)
on:
  workflow_call:
    inputs:
      name:
        required: true
        type: string
    secrets:
      my-secret:
        required: true
jobs:
  hello:
# (省略)

caller workflow では、jobs.*.uses の値に呼び出される側の called workflow を指定します。 また、引数と秘匿値はそれぞれ withsecrets の値に渡します。

# caller.yml (caller workflow)
on:
# (省略)
jobs:
  caller:
    uses: my-org/my-repo/.github/workflows/called.yml@main
    with:
      name: bob
    secrets:
      my-secret: ${{ secrets.bobs-secret }}

さらに詳細な機能については GitHub のドキュメントをご参照ください。

docs.github.com

使用例

今回は例として staging 環境の Test (terraform plan) を実行する GitHub Actions ワークフローを再利用可能な状態に変更してみます。

共通化前のワークフローファイル

まず、共通化前のワークフローファイルは以下のような内容でした。 実際に使用していたワークフローファイルから一部の記述を省略・簡略化して記載します。

# test-stg.yml
name: test-stg
on:
  pull_request:
    branches:
      - staging
    paths:
      - infra/**
jobs:
  terraform:
    name: Terraform
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
        working-directory: infra/staging/main
    steps:
      - uses: actions/checkout@v2
      - uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ap-northeast-1
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      - run: echo "##[set-output name=text;]$(cat $GITHUB_WORKSPACE/infra/.terraform-version | tr -d '\n')"
        id: terraform_version
      - uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: ${{ steps.terraform_version.outputs.text }}
      - run: terraform fmt -check -recursive
      - run: terraform init
      - run: terraform validate
      - run: terraform plan -input=false
        id: plan
      - name: Comment to PR
        env:
          PLAN_RESULT: ${{ steps.plan.outputs.stdout }}
        uses: actions/github-script@5.0.0
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            // Slack にコメント

また、同じディレクトリには上記とほぼ同じ内容の production 環境用のファイルも存在しています。

共通化後のワークフローファイル

再利用可能なワークフロー(called workflow)ファイルは以下のようになります。

# _terraform-plan.yml (called workflow)
name: Terraform Plan
on:
  workflow_call:
    inputs:
      working-directory:
        description: ワーキングディレクトリ
        type: string
        required: true
      aws-region:
        description: AWS のリージョン名
        type: string
        default: ap-northeast-1
        required: false
    secrets:
      aws-access-key-id:
        description: AWSアクセスキーID
        required: true
      aws-secret-access-key:
        description: AWSシークレットアクセスキー
        required: true
      github-token:
        description: GitHubのトークン
        required: true
      slack-webhook-url:
        description: SlackのWebhook URL
        required: false
jobs:
  terraform:
    name: Terraform
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
        working-directory: ${{ inputs.working-directory }}
    steps:
      - uses: actions/checkout@v2
      - uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ap-northeast-1
          aws-access-key-id: ${{ secrets.aws-access-key-id }}
          aws-secret-access-key: ${{ secrets.aws-secret-access-key }}
      - run: echo "##[set-output name=text;]$(cat $GITHUB_WORKSPACE/infra/.terraform-version | tr -d '\n')" # Terraform のバージョンを取得する
        id: terraform_version
      - uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: ${{ steps.terraform_version.outputs.text }}
      - run: terraform fmt -check -recursive
      - run: terraform init
      - run: terraform validate
      - run: terraform plan -input=false
        id: plan
      - name: Comment to PR
        env:
          PLAN_RESULT: ${{ steps.plan.outputs.stdout }}
        uses: actions/github-script@5.0.0
        with:
          github-token: ${{ secrets.github-token }}
          script: |
            // Slack にコメント

caller workflow では以下のように記述し、上記のワークフローを呼び出します。

# test-stg.yml (caller workflow)
name: test-stg
on:
  pull_request:
    branches:
      - staging
    paths:
      - infra/**
jobs:
  plan:
    uses: my-org/my-infra-repo/.github/workflows/_terraform-plan.yml@staging
    with:
      working-directory: infra/staging/main
    secrets:
      aws-access-key-id: ${{ secrets.STG_TF_AWS_ACCESS_KEY_ID }}
      aws-secret-access-key: ${{ secrets.STG_TF_AWS_SECRET_ACCESS_KEY }}
      github-token: ${{ secrets.GITHUB_TOKEN }}

非常にシンプルですね。

このように、Reusable workflow の機能を使用してワークフローファイルを共通化することで、関数のように呼び出すことができるようになりました。 また、環境数が増加したとしても記述を最小限に抑えることができます。

共通化後のディレクトリ構成

上記の例と同様の方法で Test cron および Deploy のワークフローも再利用可能なワークフロー(_terraform-*.yml)に共通化した結果、ディレクトリ構成は以下のようになりました。

.github/workflows
├── _terraform-plan.yml
├── _terraform-plan-cron.yml
├── _terraform-apply.yml
├── prod-deploy.yml
├── prod-test-cron.yml
├── prod-test.yml
├── stg-deploy.yml
├── stg-test-cron.yml
└── stg-test.yml

ファイル数は増えているものの、呼び出す側(caller workflow) の記述量は大幅に削減されました。 さらに、ワークフローに対して共通の変更を加える際も、呼び出される側(called workflow) のみを編集するだけで全環境に反映されるようになりました。

今後の課題

Reusable Workflow の制約として、プライベートリポジトリのワークフローは同じリポジトリのワークフローからしか呼び出すことができません。 そのため、現段階ではリポジトリ内でのワークフローの共通化は実現しましたが、複数リポジトリに渡っての共通化はできていません。 そこで、次はより汎用的な Reusable workflow を作成し、それをパブリックリポジトリに公開する予定です。

DX事業本部の全インフラリポジトリからその Reusable workflow を参照することで、より効率的に GitHub Actions のワークフローを管理できるようになることを目指しています。

結論

GitHub Actions ワークフローのファイル共通化することで冗長な記述を削減することができました。 それにより、これまでは困難だった機能追加や修正などの運用を積極的に行うことができるようになりました。

DX事業本部 開発基盤グループでは引き続き業務の効率化に取り組み、開発効率の最大化を目指して行こうと思います!

さいごに

Speeeでは一緒にサービス開発を推進してくれる仲間を大募集しています! もしSpeeeに興味を持っていただいた方は以下で社内メンバーのカジュアル面談を公開しているので、お気軽にご連絡ください💁

tech.speee.jp

エンジニアだけでなく、様々なポジションで募集中なので、「どんなポジションがあるの?」と気になってくれてた方は、こちらチェックしてみてください!もちろんオープンポジション的に上記に限らず積極採用中です!!!

*1:一部のサービスでは GCP も利用しています

*2:Datadog や PagerDuty も Terraform で管理しています