GitHub Actionsでterraform planするworking directoryを動的に抽出する

システム開発部Misocaチームエンジニアの mizukmb です。

TerraformのCIをGitHub Actionsで実装する際に工夫した時の話を紹介します。

GitHub ActionsでCIしたいけどworkflowをどうやって書けばいいの?

TerraformコードをGitHubレポジトリで管理する場合、1 GitHubレポジトリ = 1 Terraform working directory よりも 1 GitHubレポジトリ = 複数Terraform working directory といったmonorepo構成になる事が多いと思います。

例えば以下のようなproduction環境とstaging環境それぞれのworking directoryとそれらから利用されるterraform moduleを1つのGitHubレポジトリで管理するといったケースです。

❯ tree terraform-repo                         
terraform-repo
├── modules
│   ├── ecs
│   │   ├── main.tf
│   │   ├── output.tf
│   │   └── variables.tf
│   ├── iam
│   └── vpc
├── production
│   ├── app
│   │   ├── main.tf
│   │   ├── output.tf
│   │   └── variables.tf
│   └── vpc
└── staging
    ├── review_app
    ├── staging_app
    │   ├── main.tf
    │   ├── output.tf
    │   └── variables.tf
    └── vpc

11 directories, 9 files

こうしたmonorepo構成で terraform plan 等の実行を想定したCIを構築したい場合に困りがちなのは どのworking directoryが対象であるか の判別が難しいという事です。

上の例の場合だと、 production/app/main.tf を編集したGitHub Pull Request (以降、PR) では production/app だけ terraform plan できれば十分ですし、 production/vpcstaging/vpc を編集したPRでは両方で terraform plan してほしいです。

さらに modules/ 以下のファイルを編集した場合は、 そのモジュールを利用しているworking directoryが対象になり 、単なるファイル変更があったディレクトリで terraform plan を実行すれば良いという話ではなくなってきます。

こうした要件を満たしたGitHub Actions worflowをどのように作成すればよいかを次の章で説明します。

terraform plan対象のworking directoryを抽出する

tj-actions/changed-files でPRの変更ファイル一覧を取得し、k1LoW/github-script-rubyを使ってファイルから terraform plan 対象のworking directoryを抽出するRubyスクリプトを実行し、outputsとして後続のjobに渡すといった解決策をとりました。

working directoryの抽出jobは actions/github-script でも同じ事ができますが、開発メンバーが普段からRuby on Railsを使った開発を行っているという背景から k1LoW/github-script-ruby を使う事にしました。

jobs以下はこのように書いてます。

jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      targets: ${{ steps.output-targets.outputs.targets }}
      plan_targets: ${{ steps.output-targets.outputs.plan_targets }}
    steps:
      - uses: actions/checkout@v2
      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v18.6
      - name: Output target dirs
        id: output-targets
        uses: k1LoW/github-script-ruby@v2
        with:
          script: |
            # steps.output-targets.outputs.targets
            #   PRの変更したファイルから `valid_dirs` に含まれるファイルのみを抽出し、outputs.targetsを作成する
            # steps.output-targets.outputs.plan_targets
            #   outputs.targesからterraform plan対象のディレクトリを抽出し、outputs.plan_targetsを作成する
            require 'json'
            valid_dirs = [
              'production/app',
              'production/vpc',
              'staging/staging_app',
              'staging/review_app',
              'staging/vpc',
              'modules/ecs',
              'modules/iam',
              'modules/vpc
            ]
            files = '${{ steps.changed-files.outputs.all_changed_files }}'.split
            targets = valid_dirs.select { |v| files.select { |d| d.match?(v) }.size > 0 }
            plan_targets = targets.map do |t|
              # modules以下を変更した場合は参照してるworking directoryをplan_targetsの対象とする
              case t
              when 'modules/ecs', 'modules/iam'
                ['production/app', 'staging/staging_app', 'staging/review_app']
              when 'modules/vpc'
                ['production/vpc', 'staging/vpc']
              else
                t
              end
            end.flatten.uniq
            puts "Target dirs: #{targets}"
            puts "Plan target dirs: #{plan_targets}"
            core.set_output('targets', targets.to_json)
            core.set_output('plan_targets', plan_targets.to_json)

  terraform_fmt:
    runs-on: ubuntu-latest
    needs: setup
    strategy:
      matrix:
        target: ${{ fromJSON(needs.setup.outputs.targets) }}
    steps:
    - uses: actions/checkout@v2
    - name: Setup terraform
      uses: hashicorp/setup-terraform@v1
      with:
        terraform_version: ${{ env.TERRAFORM_VERSION }}
    - run: terraform fmt -recursive -diff -check ${{ matrix.target }}

  terraform_plan:
    runs-on: ubuntu-latest
    needs: setup
    defaults:
      run:
        working-directory: ${{ matrix.target }}
    strategy:
      matrix:
        target: ${{ fromJSON(needs.setup.outputs.plan_targets) }}
    steps:
    - uses: actions/checkout@v2
    - uses: aws-actions/configure-aws-credentials@master
      with:
        role-to-assume: ${{ env.AWS_ROLE_FOR_PLAN_ARN }}
        aws-region: ap-northeast-1
    - uses: hashicorp/setup-terraform@v1
      with:
        terraform_version: ${{ env.TERRAFORM_VERSION }}
    - name: Setup github-comment, tfcmt
      run: |
           curl -sL -o github-comment.tar.gz https://github.com/suzuki-shunsuke/github-comment/releases/download/v${{ env.GITHUB_COMMENT_VERSION }}/github-comment_${{ env.GITHUB_COMMENT_VERSION }}_linux_amd64.tar.gz
           sudo tar -C /usr/bin -zxvf github-comment.tar.gz
           curl -sL -o tfcmt.tar.gz https://github.com/suzuki-shunsuke/tfcmt/releases/download/v${{ env.TFCMT_VERSION }}/tfcmt_linux_amd64.tar.gz
           sudo tar -C /usr/bin -zxvf tfcmt.tar.gz
    - name: Run terraform plan
      run: |
           terraform init
           tfcmt plan -- terraform plan -no-color
           github-comment hide
      continue-on-error: true
      env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs.setup

このjobの目的は、 terraform planterraform fmt といった本来CIでやりたい事の前処理として、対象working directoryを抽出しoutputsを設定、そして後続のjobに渡す事です。outputsには targetsplan_targets の2つを設定します。それぞれの役割は以下の通りです。

  • targets
    • modules/ 含むworking directory一覧
    • fmtやtflintで使う事を想定
  • plan_targets
    • modules/ の変更があった場合、利用しているworking directoryのみを抽出
    • なので modules/ はこちらに含まれない
    • planやapplyで使う事を想定

stepsですが、まず tj-actions/changed-files でPRの変更ファイルの一覧を取得します。outputsは勝手に設定されるので uses で指定するだけで終わります。READMEから使用可能なoutputsを確認できます。

k1LoW/github-script-ruby ではoutputsの targetsplan_targets に渡す値を抽出するRubyスクリプトが書かれています。modulesを利用するworking directoryの動的な抽出が難しそうだったのでcaseを使った分岐を入れています。monorepo内にあるworking directoryの数が多いとしんどくなるかもしれません。

まとめ

GitHub ActionsでPRの変更ファイルから terraform plan の対象となるTerraform working directoryの動的な抽出を行うworkflowの書き方について書きました。これによってplan結果をPR毎に簡単に確認できるようになり、開発体験の向上を感じています。

参考文献

workflowの作成にあたってsuzuki-shunsuke/tfactionの実装や設計思想を参考にしており、強く影響を受けています。説明を省略していましたが、 terraform_fmtterraform_planstrategy.matrix 使うといったアイデアはまさにこちらを参考にしたものです。working_directoryの動的抽出の方法もtfactionに刺激された結果として実装アイデアを思いつく事ができました :)

お知らせ

弥生では一緒に働く仲間を募集しています! herp.careers