tfactionを導入してみた
tfactionとは
高度なTerraformのCI/CDをGitHub Actionsで簡単に実現できるActionです。
TerraformのCI/CDを組むにあたって欲しい機能が多く搭載されており、OSSのActionを会社のセキュリティポリシーで使えないとかがない限り、個人的にはこれを使用しないという選択肢がない位、非常におすすめなActionです。
詳しくは、開発者であるShunsuke Suzuki氏のブログを参照下さい。
導入目標
以下機能を使えること
-
Support Monorepo with GitHub Actions build matrix
- ワークフロー設定ファイルを各ルートモジュール共通で管理しつつ、変更があったルートモジュールのみCI/CDを実行できる機能
- tfactionを使用せず、ワークフローの発火条件でパスフィルターを設定する方法もあるが、各ルートモジュール毎にワークフロー設定ファイルが必要で冗長
- ワークフロー設定ファイルを各ルートモジュール共通で管理しつつ、変更があったルートモジュールのみCI/CDを実行できる機能
-
Notify the result of terraform plan and apply with tfcmt
- プルリクエストのコメントにplanとapplyの結果を通知する機能
-
Apply safely with Terraform Plan File
- CIで実行されたplanの内容のみをCDで実行されるapplyで適用する機能
- CI(プルリクエストの作成)とCD(プルリクエストのマージ)がノンストップで行えれば良いが、CIとCDの間にはレビューが挟まるので、基本的に間が開いてしまう。その間に別経路(手動変更など)からリモートが変更されることが偶にあり、作業ブランチの最新コードをそのままapplyしてしまうと、CIで実行されたplanとは違う内容のapplyが適用される危険があるが、この機能はそれを防いでくれる
- CIで実行されたplanの内容のみをCDで実行されるapplyで適用する機能
-
Automatically update related pull requests when the remote state is updated
- プルリクエストのマージ後、関連する未マージのプルリクエストのCIを自動再実行する機能
- プルリクエストがパラレルで複数作成されている時、あるプルリクエストがマージされるとtfstateの状態が更新されるので、関連する未マージのプルリクエストで再度CIを実行する必要があるが、それを自動で行ってくれる
- プルリクエストのマージ後、関連する未マージのプルリクエストのCIを自動再実行する機能
-
Create a pull request automatically to handle the problem when apply failed
- CD(apply)が失敗した時、それをフォローアップするプルリクエストを自動作成する機能
- CD(apply)が失敗している状態は、コードとtfstateで差分が出ている状態なので、いち早く修正のプルリクエストを再作成する必要あるが、それを自動かつメンションを飛ばして「早く直せ」とケツを叩いてくれるのが良い
- CD(apply)が失敗した時、それをフォローアップするプルリクエストを自動作成する機能
-
Manage Terraform Modules
- 子モジュール上でのリンターや自動フォーマッタを実行する機能
-
Auto Fix .terraform.lock.hcl and Terraform Configuration
- .terraform.lock.hclの差分のコミット、Terraform fmtの実行/コミットを自動実行する機能
-
Linters
- プルリクエストのコメントにtflint、trivyなどのリンターの結果を通知する機能
-
Run CI on working directories that depend on a updated local path Module
- 子モジュールがローカル管理(ルートモジュールと同じレポジトリで管理)の場合、必要な機能
以下ディレクトリ構成で動くこと
├ environments
├ dev
├ init
├ main.tf
├ providers.tf
├ variables.tf
├ main
├ backend.tf
├ main.tf
├ providers.tf
├ variables.tf
├ stg
├ init
├ main
├ prd
├ init
├ main
├ modules
├ sample
├ main.tf
├ output.tf
├ variables.tf
- ルートモジュールと子モジュールは同一レポジトリで管理
- 環境毎にルートモジュールを分割
- 環境毎のディレクトリ配下に以下ディレクトリを作成
- init
- GitHub Actions、tfactionに必要なリソース、tfstateを保存するS3バケットを管理
- CI/CDの実行対象からは除外する
- main
- 環境毎のリソースを管理
- CI/CDの実行対象
- init
導入手順
前提
- Terraformの実行対象はAWSを前提とする
- Terraform実行ロールの使い方やディレクトリ構成などは、筆者の宗教的な考え
GitHubレポジトリの作成、クローン
レポジトリのタイプはパブリック or プライベートのどちらでも良いが、プライベートの場合は無料でGitHub Actionsを使える枠(時間)に制限があるので、注意。
GitHub Appの作成、インストール
tfactionのドキュメントとGitHubのドキュメントを参考にtfaction用のGitHub Appを作成し、1で作成したレポジトリにインストールする。
本導入手順では実施しないが、tfactionは各アクション毎に異なる権限を必要とするので、各アクションの前段のStepで権限を指定してToken発行することで、最小権限の原則に従うことも可能。
GitHub AppのAppIDとPrivate Keyをレポジトリシークレットに登録
シークレット名はここではTFACTION_GITHUB_APP_ID
とTFACTION_GITHUB_APP_PRIVATE_KEY
とする。
コードの作成
サンプルコードをベースに、1で作成したレポジトリのローカルにコードを作成する。
アカウントIDやロール名などは、適宜設定する。
設定ファイル解説
tfaction-root.yaml
- plan_workflow_name
- GitHub Actionsのワークフロー設定ファイルで指定するCIのワークフロー名を設定する
- update_local_path_module_caller
- 子モジュールがローカル管理の場合は
true
- 子モジュールがローカル管理の場合は
- tflint
- CIでtflintを実行する場合、enabledは
true
- 自動修正を実行する場合、fixは
true
- CIでtflintを実行する場合、enabledは
- trivy
- CIでtrivyを実行する場合、enabledは
true
- CIでtrivyを実行する場合、enabledは
- working_directory
- CI/CDの実行対象のディレクトリ(ルートモジュール)のパスを相対パスで設定
- target
- working_directoryのalias名を設定
- ここでは環境名を設定
- working_directoryのalias名を設定
- aws_region
- リージョンを設定
- tfstate保存用のS3のリージョンを設定しとけば、取り敢えずは良いと思う
- 設定必須なパラメータだが、ここで設定したリージョンにしかリソースを作成できないなどの制約は無い
- リージョンを設定
- terraform_plan_config/terraform_apply_config
- GitHub ActionからAssumeするロールのARNを指定
- ここでは
OIDCGitHubIaCRole
を設定
- ここでは
- GitHub ActionからAssumeするロールのARNを指定
environments/${env}/tfaction.yaml
中身は空({}
)で良いが、CI/CDの実行対象のルートモジュール配下への配置は必須。
空ファイルだとtfactionがコケるので注意。
tfaction-root.yaml
の設定値をオーバライドしたい時に中身を記載する。
module/sample/tfaction_module.yaml
中身は空({}
)で良いが、CIの実行対象の子モジュール配下への配置は必須。
空ファイルだとtfactionがコケるので注意。
aqua.yaml
tfactionで必要なツールはaquaでインストールされるので、そのツールとバージョンをここで指定。
tfactionの各action毎に必要なツールが異なり、このファイルで明示的な指定が必要なツールもあれば、そうでない(tfcmtとか)ツールがある。
明示的な指定が不要なツールの場合でも、このファイルで明示的な指定をすれば、指定したバージョンで固定できる。
明示的な指定が不要なツール一覧は以下を参照。
trivy.yaml
trivyの設定を記載する。
ルートモジュール毎、子モジュール毎に設定を分ける場合は、それぞれの配下に配置する。
共通で良い場合は、レポジトリのルートに配置で良い。
ここでは、ルートに配置。
.trivyignore
trivyで無視するAVDを記載する。
ルートモジュール毎、子モジュール毎に設定を分ける場合は、それぞれの配下に配置する。
共通で良い場合は、レポジトリのルートに配置で良い。
ここでは、ルートに配置。
environments/.tflint.hcl、module/.tflint.hcl
tflintの設定を記載する。
ルートモジュール毎、子モジュール毎に設定を分ける場合は、それぞれの配下に配置する。
共通で良い場合は、レポジトリのルートに配置で良い。
ここでは、environmentsとmodules配下に配置。
.github/workflows/ci.yaml
name
tfaction-root.yamlで設定したplan_workflow_name
の値と合わせる。
name: CI
on
mainブランチへのプルリクエストで発火。
on:
pull_request:
branches: main
concurrency
ワークフローの多重起動の抑制。
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions
最小権限の原則に従う場合は、ジョブ毎に設定する必要があるが、ここではワークフロー共通で設定。
permissions:
id-token: write
contents: read
pull-requests: write
env
ワークフロー共通の環境変数を設定。
TRIVY用の設定ファイルはルートモジュールと子モジュールで共通なので、ここで設定。
env:
TRIVY_CONFIG: ${{ github.workspace }}/trivy.yaml
TRIVY_IGNOREFILE: ${{ github.workspace }}/.trivyignore
job(Set up)
セットアップジョブ。
Checkoutで作業ブランチの最新のコードを取得。
Install toolsでaqua.yamlに明示したツールをインストール。
Get changed working directoryで変更のあったルートディレクトリ、子モジュールを取得。
jobs:
setup:
name: Set up
runs-on: ubuntu-latest
timeout-minutes: 5
defaults:
run:
shell: bash
outputs:
targets: ${{ steps.list-targets.outputs.targets }}
modules: ${{ steps.list-targets.outputs.modules }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
uses: aquaproj/[email protected]
with:
aqua_version: v2.40.0
- name: Get changed working directory
uses: suzuki-shunsuke/tfaction/[email protected]
id: list-targets
job(Test-module)
子モジュールのCIジョブ。
変更があった子モジュール毎にパラレルで実行。
どの子モジュールにも変更がない場合は、ifでスキップする。
envでtflintの子モジュール用の設定ファイルのパスを指定。
Checkoutで作業ブランチの最新のコードを取得。
Install toolsでaqua.yamlに明示したツールをインストール。
Generate tokenでGitHub Appからトークンを取得。
Test(module)で、子モジュールに対するtrivy、tflint、自動フォーマッタ、terraform-docsによるREADMEの作成などを実行。
test-module:
name: Test module (${{ matrix.target }})
needs: setup
if: join(fromJSON(needs.setup.outputs.modules), '') != ''
runs-on: ubuntu-latest
timeout-minutes: 5
defaults:
run:
shell: bash
env:
TFACTION_TARGET: ${{ matrix.target }}
TFLINT_CONFIG_FILE: ${{ github.workspace }}/modules/.tflint.hcl
strategy:
fail-fast: true
matrix:
target: ${{ fromJSON(needs.setup.outputs.modules) }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
uses: aquaproj/[email protected]
with:
aqua_version: v2.40.0
- name: Generate token
id: generate_token
uses: tibdex/[email protected]
with:
app_id: ${{ secrets.TFACTION_GITHUB_APP_ID }}
private_key: ${{ secrets.TFACTION_GITHUB_APP_PRIVATE_KEY }}
- name: Test(module)
uses: suzuki-shunsuke/tfaction/[email protected]
with:
github_token: ${{ steps.generate_token.outputs.token }}
job(Plan)
ルートモジュールのCIジョブ。
変更があったルートモジュール毎にパラレルで実行。
どのルートモジュールにも変更がない場合は、ifでスキップする。
ルートモジュールで呼び出している子モジュールに変更があった場合は、ルートモジュールの変更として認識される(update_local_path_module_callerがtrueの場合)。
envでtflintのルートモジュール用の設定ファイルのパスを指定。
Checkoutで作業ブランチの最新のコードを取得。
Install toolsでaqua.yamlに明示したツールをインストール。
Generate tokenでGitHub Appからトークンを取得。
Set upでTerraform init等々の初期設定を実行。
Testで、ルートモジュールに対するtrivy、tflint、自動フォーマッタなどを実行。
Planで、planの実行、プルリクエストのコメントへ結果の通知、GitHub Actionsのアーティファクトにplan結果のバイナリーの保存(applyで使用)などを実行。
plan:
name: Plan (${{ matrix.target.target }})
needs: setup
if: join(fromJSON(needs.setup.outputs.targets), '') != ''
runs-on: ubuntu-latest
timeout-minutes: 5
defaults:
run:
shell: bash
env:
TFACTION_TARGET: ${{ matrix.target.target }}
TFACTION_WORKING_DIR: ${{ matrix.target.working_directory }}
TFACTION_JOB_TYPE: ${{ matrix.target.job_type }}
TFLINT_CONFIG_FILE: ${{ github.workspace }}/environments/.tflint.hcl
strategy:
fail-fast: true
matrix:
target: ${{ fromJSON(needs.setup.outputs.targets) }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
uses: aquaproj/[email protected]
with:
aqua_version: v2.40.0
- name: Generate token
id: generate_token
uses: tibdex/[email protected]
with:
app_id: ${{ secrets.TFACTION_GITHUB_APP_ID }}
private_key: ${{ secrets.TFACTION_GITHUB_APP_PRIVATE_KEY }}
- name: Set up
uses: suzuki-shunsuke/tfaction/[email protected]
with:
github_token: ${{ steps.generate_token.outputs.token }}
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
- name: Test
uses: suzuki-shunsuke/tfaction/[email protected]
with:
github_token: ${{ steps.generate_token.outputs.token }}
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
- name: Plan
uses: suzuki-shunsuke/tfaction/[email protected]
with:
github_token: ${{ steps.generate_token.outputs.token }}
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
.github/workflows/cd.yaml
name
好きな名前で良い。
name: CD
on
mainブランチへのプルリクエストのクローズで発火。
on:
pull_request:
branches: main
types: closed
concurrency
ワークフローの多重起動の抑制。
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions
最小権限の原則に従う場合は、ジョブ毎に設定する必要があるが、ここではワークフロー共通で設定。
permissions:
id-token: write
contents: read
pull-requests: write
actions: read
job(Set up)
セットアップジョブ。
ワークフロー自体はプルリクエストのクローズで発火するので、マージによるクローズ以外は、ifでスキップする。
Checkoutで作業ブランチの最新のコードを取得。
Install toolsでaqua.yamlに明示したツールをインストール。
Get changed working directoryで変更のあったルートディレクトリ、子モジュールを取得。
jobs:
setup:
name: Set up
if: ${{ github.event.pull_request.merged }} == true
runs-on: ubuntu-latest
timeout-minutes: 5
defaults:
run:
shell: bash
outputs:
targets: ${{ steps.list-targets.outputs.targets }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
uses: aquaproj/[email protected]
with:
aqua_version: v2.40.0
- name: Get changed working directory
uses: suzuki-shunsuke/tfaction/[email protected]
id: list-targets
job(apply)
ルートモジュールのCDジョブ。
変更があったルートモジュール毎にパラレルで実行。
どのルートモジュールにも変更がない場合は、ifでスキップする。
ルートモジュールで呼び出している子モジュールに変更があった場合は、ルートモジュールの変更として認識される(update_local_path_module_callerがtrueの場合)。
Checkoutで作業ブランチの最新のコードを取得。
Install toolsでaqua.yamlに明示したツールをインストール。
Generate tokenでGitHub Appからトークンを取得。
Set upでTerraform init等々の初期設定を実行。
Applyで、applyの実行、プルリクエストのコメントへ結果の通知を実行。
Follow up PRで、関連するプルリクエストのCIの再実行をトリガー。
apply:
name: Apply (${{ matrix.target.target }})
needs: setup
if: join(fromJSON(needs.setup.outputs.targets), '') != ''
runs-on: ubuntu-latest
timeout-minutes: 5
defaults:
run:
shell: bash
env:
TFACTION_TARGET: ${{ matrix.target.target }}
TFACTION_WORKING_DIR: ${{ matrix.target.working_directory }}
TFACTION_JOB_TYPE: ${{ matrix.target.job_type }}
TFACTION_IS_APPLY: true
strategy:
fail-fast: true
matrix:
target: ${{ fromJSON(needs.setup.outputs.targets) }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
uses: aquaproj/[email protected]
with:
aqua_version: v2.40.0
- name: Generate token
id: generate_token
uses: tibdex/[email protected]
with:
app_id: ${{ secrets.TFACTION_GITHUB_APP_ID }}
private_key: ${{ secrets.TFACTION_GITHUB_APP_PRIVATE_KEY }}
- name: Set up
uses: suzuki-shunsuke/tfaction/[email protected]
with:
github_token: ${{ steps.generate_token.outputs.token }}
env:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
- name: Apply
uses: suzuki-shunsuke/tfaction/[email protected]
with:
github_token: ${{ steps.generate_token.outputs.token }}
- name: Follow up PR
uses: suzuki-shunsuke/tfaction/[email protected]
if: failure()
with:
github_token: ${{ steps.generate_token.outputs.token }}
GitHub Actions、tfactionに必要なリソース、tfstateを保存するS3バケットを作成
各環境のルートモジュール配下のinitのmain.tfで定義されている以下リソースを作成する(ローカルでapplyを実行する)。
- tfstate保存用のS3
- OIDC用のIAM IDプロバイダ
- OIDC用のIAM ロール
- Terraform実行ロール
initのtfstateはローカルで管理する。
各環境が同一AWSアカウントの場合は「tfstate保存用のS3」以外は、環境共通(どこかの環境で一つ作成したら、他の環境ではコメントアウト)とする。
OIDC用のIAMロールとTerraform実行用のロールを分けている理由は、ローカル経由とCI/CD経由のplan/applyで使うIAMロールを揃えたいから。
OIDC用のIAMロールの権限は、Terraform実行用ロールへのAssumeRoleとtfstate保存用のS3に対するGetとPutのみを許可する。
tfstate保存用のS3にtfstateをPushする
v1.14.0時点では、tfstate保存用のS3が空だとtfactionがコケるようなので、各環境のルートモジュール配下のmainのmain.tfで定義されているサンプルのS3を作成(ローカルでapplyを実行する)し、tfstate保存用のS3にtfstateをPushする。
S3名が重複する場合は、local.system
を適当な値に変更する。
plan/applyで差分が出るように、コードに変更を加える
例えば、サンプルのS3のタグ(Flag)のtrue/falseを変更するなど。
作業ブランチを作成し、ローカルの変更をコミット&Pushする
作業ブランチ → mainブランチのプルリクエストを作成する
CIが発火し、plan結果がプルリクエストにコメントされる。
trivyのスキャンに引っかかった場合は、検知したAVDがプルリクエストにコメントされる(CIは失敗扱い)。
tflintのスキャンに引っかかった場合は、修正のコミットが自動で行われる。
フォーマッタによる差分が発生した場合は、修正のコミットが自動で行われる。
プルリクエストをマージする
CDが発火し、apply結果がプルリクエストにコメントされる。
CD(apply)が失敗した場合は、フォーローアップのプルリクエストが自動で作成される。
おまけ
コードの変更無しでCI/CDを発火させる方法
tfactionはコードに変更があったルートモジュール、子モジュールを検出し、CI/CDの対象としている為、コードに変更がないプルリクエストではCI/CDが発火しない(ワークフローは発火するが、セットアップ以降のジョブがスキップされる)が、ラベルにtarget:${target}
を付与したプルリクエストを作成すると、コードの変更無しでCI/CDを発火させることが可能。
ラベルの${target}
は、tfaction-root.yamlで設定したworking_directoryのalias名(target)を設定する。
手順の流れは以下の通り。
作業ブランチを作成
↓
空コミット&Push
↓
target:${target}のラベルを付与したプルリクエストを作成
Discussion
tfaction の紹介記事ありがとうございます!
こちらについて補足させてください。
明示的なインストールが不要なものはこちらで管理されています。
この install という action が setup action などで内部的に呼ばれて一部のツールがインストールされています。
これが導入されたのは v1.12.0 なので古いバージョンを使っている場合は全て明示的にインストールする必要があります。
これは現状 YES です。
install action の aqua.yaml は AQUA_GLOBAL_CONFIG という aqua の設定に追加されますが、
これは AQUA_CONFIG やファイルのパスを探索して発見された aqua.yaml よりも優先順位が下なので、リポジトリに aqua.yaml を置いて明示的に管理すればそちらが優先されます。
コメントありがとうございます!
補足頂いた箇所、記事に反映させて貰いましたm
正確に言うと、実行時間に制限があるだけで使えるかと思います。
最小権限に従う場合でも app を使って token を発行する際に権限を指定できるので action 毎に token を発行してあげればよいかと思います。
tfaction-example でも実際に用途ごとに発行しています。
まぁここまで細かく分けるかはお任せしますが。
コメントありがとうございます!
補足頂いた箇所、記事に反映させて貰いましたm