システム開発部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/vpc
と staging/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 plan
や terraform fmt
といった本来CIでやりたい事の前処理として、対象working directoryを抽出しoutputsを設定、そして後続のjobに渡す事です。outputsには targets
と plan_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の targets
と plan_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_fmt
や terraform_plan
で strategy.matrix
使うといったアイデアはまさにこちらを参考にしたものです。working_directoryの動的抽出の方法もtfactionに刺激された結果として実装アイデアを思いつく事ができました :)
- tfaction - GitHub Actions で良い感じの Terraform Workflow を構築
- suzuki-shunsuke/tfaction: GitHub Actions collection for Opinionated Terraform Workflow
お知らせ
弥生では一緒に働く仲間を募集しています! herp.careers