はじめに
newmoではGoogle Cloud等のリソース管理にTerraformを使っています。また、newmoではMonorepoを使って開発しています。 Monorepoについてここでは詳しく説明しませんが、バックエンドのGoのコードもフロントエンドのTypeScriptのコードもTerraformのコードもすべて同じGitHubのレポジトリで管理し開発を行っています。
TerraformのコードをMonorepoで管理することで、以下の要素を統一的に制御できるようになりました
- CICDパイプライン
- TerraformとProviderのバージョン
- セキュリティポリシー
- Lintルール
- クラウドリソースの構成
- パフォーマンスとコストの最適化
リソースをTerraformのコードで管理する場合に用意するGitHubでのWorkflowは一般的には以下のようなものになると思います。
- Terraformのコードを書いてPull Requestを作成する
- 自動的にTerraformのPlanが実行される
- TerraformのコードとPlanの結果をレビューして承認する
- Mainブランチにマージすると自動的にTerraform Applyが実行される
私もこれまで何度かこのようなWorkflowを作成して利用してきたことがあります。単純にPlanとApplyを実行するだけなら難しいことはあまりないのですが、Monorepoで今後広く長く使われるWorkflowということでもう少し要件が出てきました。
- 複数のTerraform stateに関連するPull Requestは並列に実行したい
- Terraform のPlan, Apply結果をPull Requestのコメントで通知したい
- Applyに失敗したときに、リトライしたい
- 一時的なエラーによりApplyが失敗するケースが多いため、リトライ機能は重要です
- Terraform stateが進んでいる場合にはApplyをさせない
- 他のPull Requestが先にマージされた場合にはstateが変わっているので、Plan結果とは違う変更が行われてしまうのを防ぎたい
- GitHub の Require branches to be up to date before mergingで防ぐことができるが、Monorepoで有効にするとマージ待ちが多く発生してしまうため、有効にしたくない
- terraform fmt, terraform validateに加えてTFLintやTrivyでコードのチェックをしたい
以上のような要件をすべて自分で実装するのは結構大変なので利用可能なツールを探していたところ、 tfaction を知りました。
tfactionとは
tfaction - GitHub Actions で良い感じの Terraform Workflow を構築 に概要が書かれていますが、自分が上で挙げていた要件を満たすようなGitHub ActionsのWorkflowを簡単に構築できるActionのセットです。
tfactionの主な機能と特徴
ここでは実際に私たちが利用している機能だけ紹介します。
1:高度なPlan/Apply管理
- Pull RequestコメントにPlan結果を表示
- Apply失敗時にFollow-up Pull Requestの作成
- plan fileを利用した安全なApply
- remote stateが進んだ場合のPull Requestの自動update
2: PlanとApplyの並列実行
GitHub Actions build matrixを利用したWorkflow
3: セキュリティとコード品質
- TFLint, Trivy, Conftestなどのサポート
- terraform fmtの自動実行(fmtしてコミットしてくれる)
4: local moduleのサポート
これらの機能をGitHub ActionsのWorkflowを書くだけで利用することができます。 AtlantisやHCP Terraformなどとちゃんと比較したわけではないのですが、tfactionでやりたいことが実現できています。
newmoでの利用
Monorepoにおけるディレクトリ構成
newmoではMonorepoの lib/terraform
以下にTerraform関連の設定、たとえばTerraform Moduleやtfactionの設定、TFLintのルールなどを置いていて、Terraformの実際のコードはいくつかのディレクトリに分かれています。
Terraform stateのサイズを管理可能な範囲に保つため、サービス単位とProvider単位でディレクトリを分割しています。
. ├── lib │ └── terraform │ ├── tfaction-root.yaml │ ├── tflint.hcl │ ├── modules │ │ └── # 共通で使用するTerraformモジュール │ └── templates │ └── # 共通で使用するTerraformテンプレート └── server ├── a │ └── terraform │ ├── dev │ │ ├── googlecloud │ │ └── cloudflare │ └── prod │ ├── googlecloud │ └── cloudflare └── b └── terraform ├── dev │ └── googlecloud └── prod └── googlecloud
tfactionを使ったTerraform Workflowの設定
GitHub ActionsのWorkflowの設定の例を以下に載せておきます。 かなりコードを省略しているのでこのままでは動きませんが、設定の雰囲気は伝わるかと思います。 バージョンも省略していますがこの設定の時点ではtfaction v1.7.0を利用していました。
name: CI Terraform Plan on: pull_request: paths: - lib/terraform/** - server/**/terraform/** concurrency: group: ${{ github.workflow }}--${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true env: AQUA_CONFIG: "${{ github.workspace }}/lib/terraform/aqua.yaml" TFACTION_CONFIG: "${{ github.workspace }}/lib/terraform/tfaction-root.yaml" jobs: setup: timeout-minutes: 10 outputs: targets: ${{ steps.list-targets.outputs.targets }} steps: - uses: actions/checkout - uses: aquaproj/aqua-installer - uses: suzuki-shunsuke/tfaction/list-targets id: list-targets plan: name: "terraform plan (${{ matrix.target.target }})" timeout-minutes: 30 needs: setup # skip if targets is empty if: join(fromJSON(needs.setup.outputs.targets), '') != '' strategy: fail-fast: false matrix: target: ${{ fromJSON(needs.setup.outputs.targets) }} env: TFACTION_TARGET: ${{ matrix.target.target }} TFLINT_CONFIG_FILE: "${{ github.workspace }}/lib/terraform/tflint.hcl" TRIVY_SEVERITY: HIGH,CRITICAL steps: - uses: actions/checkout with: sparse-checkout: | .github/actions lib/terraform ${{ matrix.target.working_directory }} - uses: aquaproj/aqua-installer - uses: suzuki-shunsuke/tfaction/get-target-config - run: github-comment exec -- github-comment hide - uses: suzuki-shunsuke/tfaction/setup - uses: suzuki-shunsuke/tfaction/test - uses: suzuki-shunsuke/tfaction/plan - name: Playbook if: failure() uses: ./.github/actions/playbook with: message: | # ${{ github.job }} が失敗しました ## 影響 - このままでは Terraform applyができません ## 調査方法 - Jobの落ちてるステップのエラーログを確認してください ## 修正方法 - Jobをretryすることで解決する場合もあります - 分からない場合は Platform Teamメンバー に聞いてください
最後のstepにあるPlaybookについては GitHub ActionsのJobが落ちたときに何をするべきかを記述するPlaybookの仕組みを作って運用している話 - newmo 技術ブログ の記事で詳しく紹介しています。
tfactionについてはほとんどデフォルトの設定のまま使っています。
tfaction-root.yamlの設定の一部
# 高速化のためparallelism を50に変える (defaultは10) env: TF_CLI_ARGS_plan: "-parallelism=50" TF_CLI_ARGS_apply: "-parallelism=50" # lintの有効化(デフォルトで有効) tflint: enabled: true trivy: enabled: true
TFLintの設定は、まだ特別なものはないです。
plugin "terraform" { enabled = true preset = "recommended" } plugin "google" { enabled = true version = "0.28.0" source = "github.com/terraform-linters/tflint-ruleset-google" } rule "terraform_naming_convention" { enabled = true }
今後の改善
newmoのTerraform Workflowにおける今後の改善点として考えているものには以下のようなものがあります。
TerraformやTerraform providerのバージョンを統一する
monorepo内でのパッケージのバージョンを1つだけに統一するOne Version Ruleをpnpm catalogで実装する - newmo 技術ブログ で紹介したように、newmoではできる限りMonorepo内のTerraformやTerraform Providerのversionを統一したいと考えています。Terraformとしてterraform.tfに記述するバージョンを外から指定する機能は自分が知る限りないので、Terragrunt などのツールを試してみるかもしれません。
セルフサービス化
現在はPlatform teamが一元的にコードレビューを行っていますが、開発チームの安全で自律的な運用に向けて以下の取り組みを検討しています
- Terraform Moduleより高度な抽象化
- さまざまなセキュリティポリシーの自動チェック
- TerraformやProviderの自動アップデート
Workflowの高速化
サービスやPlatformの成長とともに、Workflowのstepが増えたりTerraform stateが大きくなったりしてTerraform Workflowにかかる時間も増加していくことが予想されます。 今はまだ問題になることはないですが、Terraform Workflowに要する時間を計測して継続的に改善していきたいと考えています。
OpenTofu?
移行することは考えていないですが、気になっています。
ちなみにtfactionはOpenTofuもTerraguntもサポートしているようなのでその点も安心です。
まとめ
以上のようにnewmoではMonorepoにおいてtfactionを利用してTerraform Workflowを構築しています。 特に以下の点で効果を実感しています:
- 運用しやすいシンプルなWorkflowの設定
- Workflowの自動化による運用効率の向上
- セキュリティとコード品質の確保
- インフラ変更の安全性向上
GitHub ActionsでのTerraform Workflow構築を検討されている組織にとって、tfactionは優れた選択肢となるでしょう。 以前はいくつかnewmoのレポジトリにフィットしない部分もあったのですが、作者の suzuki-shunsuke さんに相談してv1.6.0に入れてもらった改善によって以前より快適に使えるようになりました。
書いた人: tjun