FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

GithubActionsのreusable workflowで複数リポジトリでのterraformを用いたCICDを楽にする

はじめに

この記事はFLINTERS10周年記念ブログリレーの36日目の投稿です

グループ企業向けにシステム開発などを担当しているチームでエンジニアをしている脇田です。 最近では担当projectが落ち着いて時間がとれたのでDevOpsに相当するような取り組みをいくつか行なっています、今回はその中の取り組みの一つを紹介します。

課題

私のチームでは複数プロダクトを管理している都合上インフラを定義しているTerraformリポジトリが4,5個あり、これからも管理リポジトリが増えることも予想されます。 そして各々のリポジトリでTerraformによるCICDのGithubActionsをコピー&ペーストのように管理しているため保守コストが高くなってしまっています。 そのためGithubActionsを共通化し保守コストを下げる対応をしました。

別リポジトリに共通化用のworkflowを導入

現状のCICD整理

導入するにあたってまず現状のCICDを整理します。 主に使われているworkflowは

plan,fmt,validateなどを行うtest workflow

env:
  TERRAFORM_WORKDIR: ...
  GOOGLE_CREDENTIALS: ...

jobs:
  plan:
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: ${{ env.TERRAFORM_WORKDIR }}

    steps:
      - uses: actions/checkout@v2
      - uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: 1.4.5

      - name: Terraform fmt
        id: fmt
        run: terraform fmt -check
        continue-on-error: true

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color
        continue-on-error: true

      - name: Create Issue Comment
        uses: actions/github-script@v3
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: ...

applyを行うdeploy workflow

env:
  TERRAFORM_WORKDIR: ...
  GOOGLE_CREDENTIALS: ...

jobs:
  apply:
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: ${{ env.TERRAFORM_WORKDIR }}

    steps:
      - uses: actions/checkout@v2
      - uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: 1.4.5

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color
        continue-on-error: true

      - name: Terraform Apply
        id: apply
        run: terraform apply -auto-approve -no-color
        continue-on-error: true

      - name: Create Issue Comment
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: ...

上記の例ではGCP用のworkflowですが同じような内容でaws providerに対するものがある、それぞれstaging,production用に分かれている現状でした。 これらのworkflowの依存している値を抽出すると

  • terraformのversion
  • terraformの実行ディレクトリ
  • terraformを適用するcloudに対するcredentials
  • 実行結果の通知をPR上にコメントするためのGithub token

が挙げられます、これらを外部から与えて動作する共通workflowを考えてみます。

導入後のCICDを考える

まず外部から呼び出すための方法を考えます。

共通化の手段として利用可能なものは

があります、今回はjob粒度で再利用を行いたいのでreuseable workflowを選択しました。

実装

reusable workflowを使って共通用のworkflowを実装していきます。 新しくリポジトリを作り、まず外部リポジトリからの呼び出しを許可する設定を行います。 reusable workflowを管理するリポジトリからSettings->Actions-Generalに遷移し、Actions permissionsをAllow actions and reusable workflows*1をONにします。

リポジトリの設定をしたところで実際にreusable workflowを実装していきます。 まず発火条件として外部から呼び出されるのみなのでworkflow_callを指定し、inputsとして先ほど抽出した依存値を指定していきます。 なおcloudのクレデンシャルはOIDCを前提としたものとしています。(この共通workflow使いたいならoidc設定をしろ、という強いメッセージをinterfaceに込めます)

.github/workflows/test.yaml

on:
  workflow_call:
    inputs:
      terraform_version:
        required: true
        type: number
      terraform_working_dir:
        required: true
        type: string
      terraform_provider:
        required: true
        type: string
        description: \"aws\" or \"gcp\"

      # For gcp
      gcp_workload_identity_provider:
        required: false
        type: string
        description: When using terraform_provider=gcp, Require this parameter
      gcp_service_account:
        required: false
        type: string
        description: When using terraform_provider=gcp, Require this parameter

      # For aws
      aws_role_to_assume:
        required: false
        type: string
        description: When using terraform_provider=aws, Require this parameter
      aws_region:
        required: false
        type: string
        description: When using terraform_provider=aws, Require this parameter

    secrets:
      token:
        required: true

次はjob部分です。 gcp,awsどちらのproviderにも対応するため両クラウドのOIDCで必要となるパラメータを受け取り、指定されたproviderで必要となるパラメータの検証をgithub-scriptのjavascriptで行ないます。 こちらは後述する実際のtest,deployのreusable workflowから利用するものになります。

.github/workflows/assert-inputs.yaml

on:
  workflow_call:
    inputs:
      terraform_provider:
        required: true
        type: string
      gcp_workload_identity_provider:
        required: false
        type: string
      gcp_service_account:
        required: false
        type: string
      aws_role_to_assume:
        required: false
        type: string
      aws_region:
        required: false
        type: string

jobs:
  main:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/github-script@v6
        env:
          TERRAFORM_PROVIDER: ${{ inputs.terraform_provider }}
          AWS_ROLE_TO_ASSUME: ${{ inputs.aws_role_to_assume }}
          AWS_REGION: ${{ inputs.aws_region }}
          GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ inputs.gcp_workload_identity_provider }}
          GCP_SERVICE_ACCOUNT: ${{ inputs.gcp_service_account }}

        with:
          script: |
            const {
              TERRAFORM_PROVIDER,
              AWS_ROLE_TO_ASSUME, 
              AWS_REGION, 
              GCP_WORKLOAD_IDENTITY_PROVIDER, 
              GCP_SERVICE_ACCOUNT
            } = process.env;

            switch(TERRAFORM_PROVIDER) {
                case 'aws':
                    if (!AWS_ROLE_TO_ASSUME) throw new Error("aws_role_to_assume parameter is missing, require when using terraform_provider=aws")
                    if (!AWS_REGION) throw new Error("aws_region parameter is missing, require when using terraform_provider=aws")
                    break;
                case 'gcp':
                    if (!GCP_WORKLOAD_IDENTITY_PROVIDER) throw new Error("gcp_workload_identity_provider parameter is missing, require when using terraform_provider=gcp")
                    if (!GCP_SERVICE_ACCOUNT) throw new Error("gcp_service_account parameter is missing, require when using terraform_provider=gcp")
                    break;
                default:
                    throw new Error(`terraform_provider=${TERRAFORM_PROVIDER} is invalid, expected 'aws' or 'gcp'`)
            }

次に実際に外部から呼び出されるtest,deployのworkflowです。差分がほぼないため説明としてtest workflowを挙げます。

jobは先ほど説明したinputsを検証するassert-inputsと実際にterraformを実行するmainに分かれます。 要点としては

  • assert-inputsをmainのneedsにしておいてassertが落ちたら実行をskipさせる
  • inputs.terraform_providerの値ごとにOIDCのactionを切り替える
  • permissionsでid-token,contentsを指定しているが明示的にpermissionsを指定するとdefaultで指定されているpermissionが無効になります。そのため今回はPRにコメントするための権限としてdefaultで許可されているpull-requests,issuesを追加で定義しています。

.github/workflows/test.yaml

jobs:
  assert-inputs:
    uses: ./.github/workflows/assert-inputs.yaml
    with:
      terraform_provider: ${{ inputs.terraform_provider }}
      gcp_workload_identity_provider: ${{ inputs.gcp_workload_identity_provider }}
      gcp_service_account: ${{ inputs.gcp_service_account }}
      aws_role_to_assume: ${{ inputs.aws_role_to_assume }}
      aws_region: ${{ inputs.aws_region }}

  main:
    runs-on: ubuntu-latest

    needs:
      - assert-inputs

    permissions:
      pull-requests: write
      issues: write
      id-token: write
      contents: read

    defaults:
      run:
        working-directory: ${{ inputs.terraform_working_dir }}

    steps:
      - uses: actions/checkout@v4

      - name: Auth gcp
        if: ${{ inputs.terraform_provider == 'gcp' }}
        uses: google-github-actions/auth@v1
        with:
          workload_identity_provider: ${{ inputs.gcp_workload_identity_provider }}
          service_account: ${{ inputs.gcp_service_account }}

      - name: Auth aws
        if: ${{ inputs.terraform_provider == 'aws' }}
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ${{ inputs.aws_region }}
          role-to-assume: ${{ inputs.aws_role_to_assume }}

      - uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: ${{ inputs.terraform_version }}

      - name: Terraform fmt
        id: fmt
        run: terraform fmt -check
        continue-on-error: true

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color
        continue-on-error: true

      - name: Create Issue Comment
        uses: actions/github-script@v6
        env:
        with:
          github-token: ${{ secrets.token }}
          script: ...

ディレクトリ構成、全体のworkflowは下記のようになりました。

ディレクトリ構成

.github
└── workflows
    ├── assert-inputs.yaml
    ├── deploy.yaml
    └── test.yaml

.github/workflows/test.yaml

on:
  workflow_call:
    inputs:
      terraform_version:
        required: true
        type: number
      terraform_working_dir:
        required: true
        type: string
      terraform_provider:
        required: true
        type: string
        description: \"aws\" or \"gcp\"

      # For gcp
      gcp_workload_identity_provider:
        required: false
        type: string
        description: When using terraform_provider=gcp, Require this parameter
      gcp_service_account:
        required: false
        type: string
        description: When using terraform_provider=gcp, Require this parameter

      # For aws
      aws_role_to_assume:
        required: false
        type: string
        description: When using terraform_provider=aws, Require this parameter
      aws_region:
        required: false
        type: string
        description: When using terraform_provider=aws, Require this parameter

    secrets:
      token:
        required: true

jobs:
  assert-inputs:
    uses: ./.github/workflows/assert-inputs.yaml
    with:
      terraform_provider: ${{ inputs.terraform_provider }}
      gcp_workload_identity_provider: ${{ inputs.gcp_workload_identity_provider }}
      gcp_service_account: ${{ inputs.gcp_service_account }}
      aws_role_to_assume: ${{ inputs.aws_role_to_assume }}
      aws_region: ${{ inputs.aws_region }}

  main:
    runs-on: ubuntu-latest

    needs:
      - assert-inputs

    permissions:
      pull-requests: write
      issues: write
      id-token: write
      contents: read

    defaults:
      run:
        working-directory: ${{ inputs.terraform_working_dir }}

    steps:
      - uses: actions/checkout@v4

      - name: Auth gcp
        if: ${{ inputs.terraform_provider == 'gcp' }}
        uses: google-github-actions/auth@v1
        with:
          workload_identity_provider: ${{ inputs.gcp_workload_identity_provider }}
          service_account: ${{ inputs.gcp_service_account }}

      - name: Auth aws
        if: ${{ inputs.terraform_provider == 'aws' }}
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ${{ inputs.aws_region }}
          role-to-assume: ${{ inputs.aws_role_to_assume }}

      - uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: ${{ inputs.terraform_version }}

      - name: Terraform fmt
        id: fmt
        run: terraform fmt -check
        continue-on-error: true

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color
        continue-on-error: true

      - name: Create Issue Comment
        uses: actions/github-script@v6
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ secrets.token }}
          script: ...

reusable workflowを呼び出す

最後に上記で実装したreusable workflowを呼び出します。

on:
  pull_request:
    types:
      - synchronize
      - opened
      - reopened
      - ready_for_review
  workflow_dispatch:

jobs:
  test:
    uses: organization/repository/.github/workflows/test.yaml@main
    with:
      terraform_provider: gcp
      terraform_version: 1.6
      terraform_working_dir: terraform/gcp/entrypoint
      gcp_workload_identity_provider: ...
      gcp_service_account: ...
    secrets:
      token: ${{ secrets.GITHUB_TOKEN }}

まとめ

これでterraformのCICDを共通化することで保守コストを下げ、新規に導入する際のコストも大幅に下げることができました。 今後tflintやtrivyなどの静的検査ツールを組み込む際もreusable workflowのみを対応すれば良いので非常に楽です。

*1:今回は例としてAllow actions and reusable workflowsを選択していますがprojectに合わせて適切なpermissionを選択してください。