こんにちは、tsub です。2023 年 11 月から育休を取得していて今年の 5 月に復職しました。今回が Social PLUS Tech Blog への初投稿となります 😄
この数ヶ月間、徐々に tfaction の導入を進めていました。元々 Terraform の CI/CD は GitHub Actions で自前のパイプラインを組んでいましたが、通常の開発タスクを優先しつつ隙間時間で CI/CD の改善を行っていたため、段階的に移行する形となりました。
この記事では tfaction を導入した背景や、どのように導入を進めていったかについて説明したいと思います。
tfaction とは
tfaction とは、簡単に説明すると Terraform (あるいは Terragrunt, OpenTofu) 向けの GitHub Actions ベースの CI/CD ツールです。
公式ドキュメントでは以下のように説明されています。
tfaction is a framework for a Monorepo to build high-level Terraform workflows using GitHub Actions. You don’t have to run terraform apply in your laptop, and don’t have to reinvent the wheel for Terraform Workflows anymore.
便宜的に CI/CD ツールと書きましたが、実際には GitHub Actions のカスタムアクションの集合体で、terraform plan や terraform apply を実行するカスタムアクションは勿論、terraform validate や tflint などを実行するカスタムアクションなど、Terraform の開発フローで便利な CI/CD のための機能を多く提供しています。
なぜ tfaction を導入したのか
弊社では素の Terraform ではなく Terragrunt を介して使っているのですが、これまで gruntwork-io/terragrunt-action を使いつつ、ワークフローの大半を自前実装していました。
自前実装で 1 つ課題となっていたのが、ローカルモジュール変更時にモジュールの呼び出し元の plan を実行するような仕組みが実現できていませんでした。
ローカルモジュールのみを変更する機会は割とあるため、CI で plan/apply を実行するために、呼び出し元の方で不要な変更を入れるなどの運用でカバーしている状態でしたが、さすがにこの運用を長く続けるのは大変でした。
当時、ローカルモジュール変更を検知して plan/apply を実行する方法を調べていたところ、tfaction が提供している list-targets Action を使うことで解決できそうなことが分かったため、部分的に list-targets Action を導入しました。
ここで tfaction の利点に気がつきましたが、tfaction はカスタムアクションの集合体であるため、必要な機能のみを段階的に導入することができます。
また、tfaction は PR をマージしたら apply するというフローで設計されており、我々の自前ワークフローも同様のフローで設計していたため、その点でも親和性が高く、全体的に tfaction を導入していこうという方針となりました。
ここで、list-targets Action 以外の部分は自前実装のままでも問題なかったのですが、ローカルモジュールの変更検知以外にもいくつか課題感があったため、Terraform の CI/CD を全体的に改善していくために tfaction の導入を進めていくことを決めました。
段階的な導入の進め方
以下で説明する各ステップはそれぞれ master にマージした変更で、単体で動作するようになっています。
段階的な導入ということなので、それぞれのステップ間で期間が開いているものもありますし、その間も通常のインフラの開発タスクは継続しているため、単体で実行可能な状態を維持しています。
なお、本記事では導入当時の tfaction v1.16.1 を前提としています。記事執筆時点で v1.19.2 が最新バージョンです。
また、tfaction とは関係ないですが、本記事で紹介しているコードではカスタムアクションの呼び出しにコミットハッシュを使わずにタグで指定しているため、安全に利用するためにコミットハッシュへの置き換えも行うことを推奨します 🙏 (参考)
0. tfaction 導入前のワークフロー
まず前提として弊社のインフラリポジトリの構成は Monorepo 環境となっていて 1 つのリポジトリ内に複数のサービスのルートモジュールがあります。
tfaction の導入を始める前は以下のようなワークフローが定義されていました。
(本記事と直接的に関係のない細かい部分は省いています)
.github/workflows/terragrunt_plan_xxxx.yml
name: Terrgrunt Plan xxxx State
on:
pull_request:
branches:
- master
- staging
paths:
- "terragrunt/xxxx/**"
jobs:
terragrunt-plan:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-[email protected]
with:
role-to-assume: ${{ secrets.TERRAGRUNT_AWS_IAM_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- name: terragrunt plan
uses: gruntwork-io/terragrunt-action@v2
with:
tf_version: 1.9.8
tg_version: 0.69.0
tg_dir: terragrunt/xxxx
tg_command: 'plan -lock=false'
tg_comment: 1
env:
TERRAGRUNT_FORWARD_TF_STDOUT: "true"
GITHUB_TOKEN: ${{ github.token }}
このワークフローがルートモジュールごとに定義されており、弊社のルートモジュールは全部で 14 個あるため、計 14 ワークフローとなります。
ワークフローが分かれていた理由は、変更したディレクトリごとに実行する CI を最小限に抑えたいため、on.pull_request.paths
の部分で terragrunt/xxxx/**
のようにルートモジュールごとのディレクトリ変更を検知していました。
1. list-targets Action の導入
まずは前述のローカルモジュールの変更を検知するために list-targets Action を導入しました。
また、14 個のワークフローの保守性も課題となっていたため、list-targets Action の導入に伴い、matrix ジョブの導入も同時に行なうことで 14 個のワークフローを 1 つに統合しました。
.github/workflows/terragrunt_plan.yml
name: Terragrunt Plan
on:
pull_request:
branches:
- master
- staging
paths:
- "terragrunt/**"
push:
branches:
- master
paths:
- "terragrunt/**"
jobs:
setup:
runs-on: ubuntu-latest
outputs:
targets: ${{ steps.list-targets.outputs.targets }}
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
env:
tfaction_version: v1.16.1
with:
path: ~/.local/share/aquaproj-aqua
key: v2-aqua-installer-${{runner.os}}-${{runner.arch}}-for-tfaction-${{env.tfaction_version}}
restore-keys: |
v2-aqua-installer-${{runner.os}}-${{runner.arch}}-
- uses: aquaproj/aqua-[email protected]
with:
aqua_version: v2.51.1
- name: Install terragrunt
uses: jaxxstorm/action-install-gh-[email protected]
with:
repo: gruntwork-io/terragrunt
tag: v0.69.0
cache: enable
rename-to: terragrunt
chmod: 0755
extension-matching: disable
- uses: hashicorp/setup-[email protected]
with:
terraform_version: "1.9.8"
terraform_wrapper: false
- uses: suzuki-shunsuke/tfaction/list-[email protected]
id: list-targets
with:
github_token: ${{ github.token }}
terragrunt-plan:
name: "terragrunt-plan for ${{ matrix.target.target }}"
runs-on: ubuntu-latest
needs: setup
if: join(fromJSON(needs.setup.outputs.targets), '') != ''
strategy:
fail-fast: false
matrix:
target: ${{ fromJSON(needs.setup.outputs.targets) }}
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-[email protected]
with:
role-to-assume: ${{ secrets.TERRAGRUNT_AWS_IAM_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- name: terragrunt plan
uses: gruntwork-io/terragrunt-action@v2
with:
tf_version: 1.9.8
tg_version: 0.69.0
tg_dir: terragrunt/${{ matrix.target.target }}
tg_command: 'plan -lock=false'
tg_comment: 1
env:
TERRAGRUNT_FORWARD_TF_STDOUT: "true"
GITHUB_TOKEN: ${{ github.token }}
また、tfaction の利用に必要な設定ファイルの追加も合わせて実施しています。
tfaction-root.yaml
plan_workflow_name: Terragrunt Plan
update_local_path_module_caller:
enabled: true
target_groups:
- working_directory: terragrunt/xxxx
target: xxxx
terragrunt/xxxx/tfaction.yaml
{}
ワークフローの実装がガラッと変わったようにも見えますが、本質的には terragrunt plan を実行する前準備として、実行対象を検出する仕組みが増えただけです。
list-targets Action によって変更したローカルモジュールやルートモジュールを検出し、PR 上で変更したディレクトリのみ terragrunt plan を実行するような仕組みに変わりました。
terragrunt plan を実行する部分には変わらず gruntwork-io/terragrunt-action Action を使っています。
また、今回は list-targets Action のローカルモジュール変更検知が必要なだけでしたので、tfaction-root.yaml
の設定も必要最小限に抑えていますし、tfaction が前提としている aqua パッケージマネージャの導入も行いませんでした。
tfaction 内部で使っている aqua のために CI 上では aqua CLI をインストールしていますが、ローカル環境などには aqua を導入していませんし、aqua.yaml
も追加していません。
2. plan Action の導入
次のステップとして、tfaction の plan Action を導入しました。
plan Action の導入理由は gruntwork-io/terragrunt-action Action の PR コメント機能にサマリ表示がなく、1 つ 1 つ詳細を見るのが大変だったため、tfaction に組み込まれている tfcmt による PR コメントを使いたかったからです。
(ちなみに元々 tfcmt は使っていましたが Terragrunt 導入時に公式 Action に寄せるために一度廃止されていました)
No changes であることを確認するのにも都度トグルを開いて中身を確認する必要があった
.github/workflows/terragrunt_plan.yml
@@ -43,7 +43,7 @@
uses: jaxxstorm/action-install-gh-[email protected]
with:
repo: gruntwork-io/terragrunt
- tag: v0.69.0
+ tag: v0.81.0
cache: enable
rename-to: terragrunt
chmod: 0755
@@ -51,7 +51,7 @@
- uses: hashicorp/setup-[email protected]
with:
- terraform_version: "1.9.8"
+ terraform_version: "1.12.2"
terraform_wrapper: false
- uses: suzuki-shunsuke/tfaction/list-[email protected]
@@ -64,15 +64,19 @@
runs-on: ubuntu-latest
needs: setup
- if: join(fromJSON(needs.setup.outputs.targets), '') != ''
+ if: github.event_name == 'pull_request' && join(fromJSON(needs.setup.outputs.targets), '') != ''
strategy:
fail-fast: false
matrix:
target: ${{ fromJSON(needs.setup.outputs.targets) }}
permissions:
id-token: write
- contents: read
+ contents: write
pull-requests: write
+ issues: write
+ env:
+ TFACTION_TARGET: ${{ matrix.target.target }}
+ TFACTION_JOB_TYPE: terraform
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
@@ -80,14 +84,36 @@
with:
role-to-assume: ${{ secrets.TERRAGRUNT_AWS_IAM_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- - name: terragrunt plan
- uses: gruntwork-io/terragrunt-action@v2
+
+ - uses: actions/cache@v4
+ env:
+ tfaction_version: v1.16.1
with:
- tf_version: 1.9.8
- tg_version: 0.69.0
- tg_dir: terragrunt/${{ matrix.target.target }}
- tg_command: 'plan -lock=false'
- tg_comment: 1
+ path: ~/.local/share/aquaproj-aqua
+
+ key: v2-aqua-installer-${{runner.os}}-${{runner.arch}}-for-tfaction-${{env.tfaction_version}}
+ restore-keys: |
+ v2-aqua-installer-${{runner.os}}-${{runner.arch}}-
+
+ - uses: aquaproj/aqua-[email protected]
+ with:
+ aqua_version: v2.51.1
+ - name: Install terragrunt
+ uses: jaxxstorm/action-install-gh-[email protected]
+ with:
+ repo: gruntwork-io/terragrunt
+ tag: v0.81.0
+ cache: enable
+ rename-to: terragrunt
+ chmod: 0755
+ extension-matching: disable
+ - uses: hashicorp/setup-[email protected]
+ with:
+ terraform_version: "1.12.2"
+ terraform_wrapper: false
+ - uses: suzuki-shunsuke/tfaction/[email protected]
env:
- TERRAGRUNT_FORWARD_TF_STDOUT: "true"
- GITHUB_TOKEN: ${{ github.token }}
+ TG_TF_FORWARD_STDOUT: "true"
+ - uses: suzuki-shunsuke/tfaction/[email protected]
+ env:
+ TG_TF_FORWARD_STDOUT: "true"
tfaction-root.yaml
@@ -7,6 +7,8 @@
update_local_path_module_caller:
enabled: true
+terraform_command: terragrunt
+
target_groups:
- working_directory: terragrunt/xxxx
target: xxxx
ソースコード全体
.github/workflows/terragrunt_plan.yml
name: Terragrunt Plan
on:
pull_request:
branches:
- master
- staging
paths:
- "terragrunt/**"
push:
branches:
- master
paths:
- "terragrunt/**"
jobs:
setup:
runs-on: ubuntu-latest
outputs:
targets: ${{ steps.list-targets.outputs.targets }}
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
env:
tfaction_version: v1.16.1
with:
path: ~/.local/share/aquaproj-aqua
key: v2-aqua-installer-${{runner.os}}-${{runner.arch}}-for-tfaction-${{env.tfaction_version}}
restore-keys: |
v2-aqua-installer-${{runner.os}}-${{runner.arch}}-
- uses: aquaproj/aqua-[email protected]
with:
aqua_version: v2.51.1
- name: Install terragrunt
uses: jaxxstorm/action-install-gh-[email protected]
with:
repo: gruntwork-io/terragrunt
tag: v0.81.0
cache: enable
rename-to: terragrunt
chmod: 0755
extension-matching: disable
- uses: hashicorp/setup-[email protected]
with:
terraform_version: "1.12.2"
terraform_wrapper: false
- uses: suzuki-shunsuke/tfaction/list-[email protected]
id: list-targets
with:
github_token: ${{ github.token }}
terragrunt-plan:
name: "terragrunt-plan for ${{ matrix.target.target }}"
runs-on: ubuntu-latest
needs: setup
if: github.event_name == 'pull_request' && join(fromJSON(needs.setup.outputs.targets), '') != ''
strategy:
fail-fast: false
matrix:
target: ${{ fromJSON(needs.setup.outputs.targets) }}
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
env:
TFACTION_TARGET: ${{ matrix.target.target }}
TFACTION_JOB_TYPE: terraform
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-[email protected]
with:
role-to-assume: ${{ secrets.TERRAGRUNT_AWS_IAM_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- uses: actions/cache@v4
env:
tfaction_version: v1.16.1
with:
path: ~/.local/share/aquaproj-aqua
key: v2-aqua-installer-${{runner.os}}-${{runner.arch}}-for-tfaction-${{env.tfaction_version}}
restore-keys: |
v2-aqua-installer-${{runner.os}}-${{runner.arch}}-
- uses: aquaproj/aqua-[email protected]
with:
aqua_version: v2.51.1
- name: Install terragrunt
uses: jaxxstorm/action-install-gh-[email protected]
with:
repo: gruntwork-io/terragrunt
tag: v0.81.0
cache: enable
rename-to: terragrunt
chmod: 0755
extension-matching: disable
- uses: hashicorp/setup-[email protected]
with:
terraform_version: "1.12.2"
terraform_wrapper: false
- uses: suzuki-shunsuke/tfaction/[email protected]
env:
TG_TF_FORWARD_STDOUT: "true"
- uses: suzuki-shunsuke/tfaction/[email protected]
env:
TG_TF_FORWARD_STDOUT: "true"
tfaction-root.yaml
plan_workflow_name: Terragrunt Plan
update_local_path_module_caller:
enabled: true
terraform_command: terragrunt
target_groups:
- working_directory: terragrunt/xxxx
target: xxxx
弊社のインフラリポジトリでは Argo CD の CI も実行しているため、tfcmt の PR コメントを一部変更しています。また、ラベルの自動付与は現時点では不要なため無効化しています。
tfcmt.yaml
terraform:
plan:
disable_label: true
templates:
plan_title: "## {{if eq .ExitCode 1}}:x: {{end}}terragrunt plan result{{if .Vars.target}} ({{.Vars.target}}){{end}}"
tfcmt による PR コメントで変更内容のサマリが表示されるようになり、非常に見やすくなりました。
また、合わせて PR ラベルとして変更があったルートモジュールが記載されるようになり、Monorepo 開発において変更箇所が分かりやすくなりました。
plan Action と同時に setup Action も導入したことにより、.terraform.lock.hcl
の自動更新も行なってくれるようになりました。
3. Terragrunt のディレクトリ構成変更
次のステップは tfaction と直接的に関係ないのですが、既存の Terragrunt のディレクトリ構成が tfaction の利用に適しておらず、複数の環境の plan を実行できない状態だったため、Terragrunt のディレクトリ構成を変更しました。
元々 Terragrunt 導入時に以下のようなディレクトリ構成となっていました。
- terragrunt
|- xxxx (サービス/ルートモジュール ごとのディレクトリ)
|- terragrunt.hcl
|- *.tf (Terraform ソースコード)
環境ごとにディレクトリが分かれていないため、staging/production の切り替えをどうやっているかというと、Terragrunt の run_cmd function を使ってシェルスクリプトを実行し、環境変数やハードコードされた値などで実行対象の環境を決めていました。
そのため、tfaction の設定でも target_groups
は環境ごとのディレクトリを記述せずに以下のようになっていました。
tfaction-root.yaml
target_groups:
- working_directory: terragrunt/xxxx
target: xxxx
そして、CI 上では PREFIX
という環境変数を用いて PR のベースブランチに応じて環境の切り替えを行なっていました。
.github/workflows/terragrunt_plan.yml
- uses: suzuki-shunsuke/tfaction/[email protected]
env:
PREFIX: ${{ github.base_ref == 'master' && 'production' || github.base_ref == 'staging' && 'staging' }}
この設計でも tfaction の動作は問題ないのですが、1 つの PR 上で複数の環境の plan を実行しようとすると、tfaction の target_groups
が同一のままだと plan Action でのアーティファクトアップロード部分 (plan 結果をファイルとして出力している) で名前の競合がおき、以下のようなエラーが出てしまいました。
Error: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
そのため、Terragrunt のディレクトリ構成を以下のように変更しました。こちらのディレクトリ構成の方が Terraform/Terragrunt において割と一般的ではないかと思います。
- terragrunt
|- envs
| |- production
| |- xxxx (サービス/ルートモジュール ごとのディレクトリ)
| |- terragrunt.hcl (Terragrunt ワーキングディレクトリ)
| |- staging
| |- xxxx
| |- terragrunt.hcl (Terragrunt ワーキングディレクトリ)
| |- review
| |- xxxx
| |- terragrunt.hcl (Terragrunt ワーキングディレクトリ)
|- xxxx (サービス/ルートモジュール ごとのディレクトリ)
|- terragrunt.hcl (互換性維持のため一旦残す)
|- *.tf (Terraform ソースコード)
一度にディレクトリ構成を完全に切り替えると開発チーム内で混乱が起きそうだと思い、一応従来のディレクトリ構成でもローカルから plan などが実行できるように terragrunt.hcl
を残してあります。
ただし CI 上では新しいディレクトリ構成を前提として動作するようにしました。
tfaction の設定も環境ごとに target_groups
の定義を分けるようにしました。
tfaction-root.yaml
target_groups:
- working_directory: terragrunt/envs/production/xxxx
target: production/xxxx
- working_directory: terragrunt/envs/staging/xxxx
target: staging/xxxx
- working_directory: terragrunt/envs/review/xxxx
target: review/xxx
なお、GitHub Actions ワークフローの実装例は本筋から外れるため省略します。
4. test, test-module Action の導入
次のステップとして、tfaction の test, test-module Action を導入しました。
tfaction の test Aciton や test-module Action では以下のことが行えます。
- terraform validate によるエラー検出 (test Action のみ)
- terraform fmt によるコードの自動修正
- tflint –fix によるコードの自動修正
- terraform-docs によるモジュールのドキュメント生成
- その他、tfsec や trivy の実行 (今回は使っていない)
これまで、terraform fmt や tflint は自前のワークフローや reviewdog などを使って実装していましたが、tfaction に統合することで、管理するワークフローの数を減らせる点や、CI の実行時間などでいくつかメリットがありました。
test Acton の導入は基本的に plan のワークフローに以下を追加するだけです。弊社の場合は前述の通り aqua を使っていないため、tflint のインストールも行なっています。
.github/workflows/terragrunt_plan.yml
@@ -119,6 +120,10 @@ jobs:
+ - uses: terraform-linters/setup-[email protected]
+ with:
+ tflint_version: v0.50.3
@@ -152,6 +157,41 @@ jobs:
+
+ - uses: suzuki-shunsuke/tfaction/[email protected]
+ env:
+ TFLINT_CONFIG_FILE: "${{ github.workspace }}/.tflint.hcl"
+ TG_TF_FORWARD_STDOUT: "true"
また、tfaction の設定にも追加が必要です。
tfaction-root.yaml
+ tflint:
+ enabled: true
+ fix: true
+
+
+ trivy:
+ enabled: false
test-module Action の導入は plan のワークフローに別のジョブを追加しています。
.github/workflows/terragrunt_plan.yml
test-module:
name: "test-module for ${{ matrix.module }}"
runs-on: ubuntu-latest
needs: setup
if: github.event_name == 'pull_request' && join(fromJSON(needs.setup.outputs.modules), '') != ''
strategy:
fail-fast: false
matrix:
module: ${{ fromJSON(needs.setup.outputs.modules) }}
permissions:
contents: write
pull-requests: write
issues: write
env:
TFACTION_TARGET: ${{ matrix.module }}
TFACTION_JOB_TYPE: terraform
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
env:
tfaction_version: v1.16.1
with:
path: ~/.local/share/aquaproj-aqua
key: v2-aqua-installer-${{runner.os}}-${{runner.arch}}-for-tfaction-${{env.tfaction_version}}
restore-keys: |
v2-aqua-installer-${{runner.os}}-${{runner.arch}}-
- uses: aquaproj/aqua-[email protected]
with:
aqua_version: v2.51.1
- name: Install terragrunt
uses: jaxxstorm/action-install-gh-[email protected]
with:
repo: gruntwork-io/terragrunt
tag: v0.81.0
cache: enable
rename-to: terragrunt
chmod: 0755
extension-matching: disable
- uses: hashicorp/setup-[email protected]
with:
terraform_version: "1.12.2"
terraform_wrapper: false
- uses: terraform-linters/setup-[email protected]
with:
tflint_version: v0.50.3
- uses: suzuki-shunsuke/tfaction/[email protected]
- uses: ./.github/actions/test-module
env:
TFLINT_CONFIG_FILE: "${{ github.workspace }}/.tflint.hcl"
test-module Action でテストする対象は Terraform のルートモジュール以外ということになりますが、弊社の場合は terragrunt/modules/
ディレクトリ配下にモジュールを定義しており、これらのローカルモジュールは素の Terraform で書かれているため terragrunt.hcl
を配置しておらず、test-module Action では terraform コマンドを実行する必要があります。
tfaction の設定でトップレベルに terraform_command: terragrunt
を指定していると、test-module Action で terragrunt コマンドが使われるようになってしまい、terragrunt.hcl
が存在しないため実行エラーとなってしまいます。
そのため、terraform_command: terragrunt
を target_groups
配下のみ適用するように変更を加える必要がありました。
tfaction-root.yaml
- terraform_command: terragrunt
+
+
+
+
target_groups:
- working_directory: terragrunt/envs/production/xxxx
target: production/xxxx
+ terraform_command: terragrunt
- working_directory: terragrunt/envs/staging/xxxx
target: staging/xxxx
+ terraform_command: terragrunt
- working_directory: terragrunt/envs/review/xxxx
target: review/xxx
+ terraform_command: terragrunt
# ...
また、test-module Action 内では terraform-docs を使用してドキュメントの自動生成・更新を行なってくれるのですが、弊社では元々 terraform-docs を使っていなかったため、この時点で terraform-docs の導入まで一緒にやってしまうと認知負荷が増えてしまうため、一時的に terraform-docs は実行しないようにしています。
tfaction 側には terraform-docs を無効化するオプションは現在提供されていません。
そのため、test-module Action をローカルの Composite Action としてコピーし、terraform-docs 実行部分だけ一部コメントアウトするような形を取っています。
.github/actions/test-module/action.yml
+ MIT License
+
+ Copyright (c) 2022 Shunsuke Suzuki
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+
+
+
+
+
+
+
name: Test Module
description: Test Module
inputs:
@@ -56,10 +63,11 @@ runs:
shell: bash
working-directory: ${{ env.TFACTION_TARGET }}
- - uses: suzuki-shunsuke/tfaction/terraform-[email protected]
- with:
- github_token: ${{ inputs.github_token }}
- working_directory: ${{ env.TFACTION_TARGET }}
+
+
+
+
+
terraform-docs が実行されること自体は問題ないため、今後この Composite Action 廃止して通常通り test-module Action を使う予定です。
5. GitHub Actions トークンを GitHub App トークンへ移行
次のステップとして、GitHub Actions の GITHUB_TOKEN (ここでは便宜上 GitHub Actions トークンと表現します) から GitHub App トークンへ移行しました。
移行した主な理由としては、Renovate の Terraform Provider アップデートの PR でオートマージを行うためです。
tfaction には Renovate の PR 上で terraform plan の差分があると CI を失敗扱いにしてくれる機能があり、これを使うことで Terraform Provider などのアップデートを安全にオートマージすることができます。
この機能を使って Renovate の Terraform Provider アップデートの PR をオートマージにしたいのですが、ここで問題になるのが、GitHub Actions トークンと GitHub Action の仕様です。
GitHub Actions トークンを用いてコミットした場合は、そのコミットで次のワークフローがトリガーされないため、GitHub App トークンを使うことでワークフローがトリガーされるようにしました。
GitHub App トークンの生成には actions/create-github-app-token Action を使っています。一般的な使い方だと思いますので、詳しい実装は割愛します。
6. apply Action の導入
最後のステップとして tfaction の apply Action を導入しました。
ここまで来れば tfaction を導入したと言って良いでしょう 😁
tfaction では plan 時に terraform plan の -out=tfplan.binary
オプションを使って plan 結果をファイル出力し、それを apply 時に参照してくれます。(異なるワークフロー間ですので GitHub Actions のアーティファクトを経由します)
これによって、plan の結果として表示された差分だけを適用することができ、安全に terraform apply の CI/CD を組むことができます。
また、tfplan.binary
を使って apply を行うと、古い tfplan.binary
で apply した時に以下のエラーが出ます。
╷
│ Error: Saved plan is stale
│
│ The given plan file can no longer be applied because the state was changed
│ by another operation after the plan was created.
╵
このようなケースを考慮して tfaction では自動的に同じルートモジュールに変更がある PR に対してデフォルトブランチを取り込んでくれるような機能があります。
自前でワークフローを実装する場合はこういった考慮があって難しいため、tfaction を使うことで安全なワークフローを比較的組みやすいと思います。
さて、apply Action の導入ですが、基本的には plan と同様の実装で、違う点としては env.TFACTION_IS_APPLY: "true"
が必要になるのと、gruntwork-io/terragrunt-action Action を suzuki-shunsuke/tfaction/apply Action に置き換えるくらいでした。
あとは GITHUB_TOKEN に actions: read
の権限が必要になります。
env:
TFACTION_IS_APPLY: "true"
TFACTION_TARGET: ${{ matrix.target.target }}
TFACTION_JOB_TYPE: terraform
steps:
- uses: actions/create-github-app-[email protected]
id: tfaction-app-token
with:
app-id: ${{ vars.INFRA_TFACTION_GITHUB_APP_ID }}
private-key: ${{ secrets.INFRA_TFACTION_GITHUB_APP_PRIVATE_KEY }}
permission-actions: read
permission-issues: write
permission-pull-requests: write
permission-contents: write
- name: terragrunt apply
id: apply
uses: suzuki-shunsuke/tfaction/[email protected]
with:
github_token: ${{ steps.tfaction-app-token.outputs.token }}
env:
TG_TF_FORWARD_STDOUT: "true"
TG_LOG_LEVEL: warn
ただ、弊社では apply 実行後に Slack へ通知していたため、その運用を維持するために以下の改修も行なっています。もともと terragrunt apply の標準出力で No changes
かどうかを判断して通知メッセージの内容を書き分けていましたが、tfaction の apply Action では標準出力が outputs
経由で取れないため、tfplan.binary
の中から .applyable
を参照して No changes
かどうかを判断するようにしています。
.github/workflows/terragrunt_plan.yml
- name: create success message
id: slack_success_message
working-directory: ${{ matrix.target.working_directory }}
env:
TFPLAN_FILE_PATH: tfplan.binary
run: |
ls -al ./ # to debug
TERRAGRUNT_CHANGES=$(terragrunt show -json "$TFPLAN_FILE_PATH" | jq '.applyable')
if [ "$TERRAGRUNT_CHANGES" = 'false' ]; then
message=$(cat *${{ github.actor }}* による *${{ github.ref_name }}* Mergeによって *${{ matrix.target.target }}* への terragrunt apply による差分はありませんでした
EOS
)
else
message=$(cat *${{ github.actor }}* による *${{ github.ref_name }}* Mergeによって *${{ matrix.target.target }}* への terragrunt apply が実行されました。
正常に完了しました
://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|Workflow URL>
EOS
)
fi
{
echo 'message} >> "$GITHUB_OUTPUT"
最終的な plan, apply ワークフローの実装を載せておきます。
ソースコード全体
.github/workflows/terragrunt_plan.yml
name: Terragrunt Plan
on:
pull_request:
branches:
- master
- production
paths:
- "terragrunt/**"
push:
branches:
- master
paths:
- "terragrunt/**"
jobs:
setup:
uses: ./.github/workflows/terragrunt_setup.yml
secrets: inherit
with:
target_envs: |-
${{
github.base_ref == 'master' && 'production staging review' ||
github.base_ref == 'production' && 'production' ||
}}
terragrunt-plan:
name: "terragrunt-plan for ${{ matrix.target.target }}"
runs-on: ubuntu-latest
needs: setup
if: github.event_name == 'pull_request' && join(fromJSON(needs.setup.outputs.targets), '') != ''
strategy:
fail-fast: false
matrix:
target: ${{ fromJSON(needs.setup.outputs.targets) }}
permissions:
id-token: write
contents: read
env:
TFACTION_TARGET: ${{ matrix.target.target }}
TFACTION_JOB_TYPE: terraform
steps:
- uses: actions/create-github-app-[email protected]
id: tfaction-app-token
with:
app-id: ${{ vars.INFRA_TFACTION_GITHUB_APP_ID }}
private-key: ${{ secrets.INFRA_TFACTION_GITHUB_APP_PRIVATE_KEY }}
permission-issues: write
permission-pull-requests: write
permission-contents: write
- id: export-target
name: Export target service and env from matrix.target.target
env:
target: ${{ matrix.target.target }}
run: |
# $target: production/account
# $env: production
# $service: account
env=$(echo "$target" | cut -d "https://zenn.dev/" -f 1)
service=$(echo "$target" | cut -d "https://zenn.dev/" -f 2)
echo "env: $env"
echo "service: $service"
echo "env=$env" >> "$GITHUB_OUTPUT"
echo "service=$service" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-[email protected]
with:
role-to-assume: ${{ secrets.TERRAGRUNT_AWS_IAM_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- uses: actions/cache@v4
env:
tfaction_version: v1.16.1
with:
path: ~/.local/share/aquaproj-aqua
key: v2-aqua-installer-${{runner.os}}-${{runner.arch}}-for-tfaction-${{env.tfaction_version}}
restore-keys: |
v2-aqua-installer-${{runner.os}}-${{runner.arch}}-
- uses: aquaproj/aqua-[email protected]
with:
aqua_version: v2.51.1
- name: Install terragrunt
uses: jaxxstorm/action-install-gh-[email protected]
with:
repo: gruntwork-io/terragrunt
tag: v0.81.0
cache: enable
rename-to: terragrunt
chmod: 0755
extension-matching: disable
- uses: hashicorp/setup-[email protected]
with:
terraform_version: "1.12.2"
terraform_wrapper: false
- uses: terraform-linters/setup-[email protected]
with:
tflint_version: v0.50.3
- uses: shmokmt/actions-setup-github-[email protected]
with:
version: v6.3.2
- name: Hide old PR comments
run: github-comment exec -k hide -- github-comment hide -k tfcmt
env:
GITHUB_TOKEN: ${{ steps.tfaction-app-token.outputs.token }}
GH_COMMENT_VAR_tfaction_target: ${{ env.TFACTION_TARGET }}
- uses: suzuki-shunsuke/tfaction/[email protected]
with:
github_token: ${{ steps.tfaction-app-token.outputs.token }}
env:
TG_TF_FORWARD_STDOUT: "true"
- uses: suzuki-shunsuke/tfaction/[email protected]
with:
github_token: ${{ steps.tfaction-app-token.outputs.token }}
env:
GITHUB_TOKEN: ${{ steps.tfaction-app-token.outputs.token }}
TFLINT_CONFIG_FILE: "${{ github.workspace }}/.tflint.hcl"
TG_TF_FORWARD_STDOUT: "true"
- uses: suzuki-shunsuke/tfaction/[email protected]
with:
github_token: ${{ steps.tfaction-app-token.outputs.token }}
env:
GITHUB_TOKEN: ${{ steps.tfaction-app-token.outputs.token }}
TG_TF_FORWARD_STDOUT: "true"
test-module:
name: "test-module for ${{ matrix.module }}"
runs-on: ubuntu-latest
needs: setup
if: github.event_name == 'pull_request' && join(fromJSON(needs.setup.outputs.modules), '') != ''
strategy:
fail-fast: false
matrix:
module: ${{ fromJSON(needs.setup.outputs.modules) }}
permissions:
id-token: write
contents: read
env:
TFACTION_TARGET: ${{ matrix.module }}
TFACTION_JOB_TYPE: terraform
steps:
- uses: actions/create-github-app-[email protected]
id: tfaction-app-token
with:
app-id: ${{ vars.INFRA_TFACTION_GITHUB_APP_ID }}
private-key: ${{ secrets.INFRA_TFACTION_GITHUB_APP_PRIVATE_KEY }}
permission-pull-requests: write
permission-contents: write
- uses: actions/checkout@v4
- uses: actions/cache@v4
env:
tfaction_version: v1.16.1
with:
path: ~/.local/share/aquaproj-aqua
key: v2-aqua-installer-${{runner.os}}-${{runner.arch}}-for-tfaction-${{env.tfaction_version}}
restore-keys: |
v2-aqua-installer-${{runner.os}}-${{runner.arch}}-
- uses: aquaproj/aqua-[email protected]
with:
aqua_version: v2.51.1
- name: Install terragrunt
uses: jaxxstorm/action-install-gh-[email protected]
with:
repo: gruntwork-io/terragrunt
tag: v0.81.0
cache: enable
rename-to: terragrunt
chmod: 0755
extension-matching: disable
- uses: hashicorp/setup-[email protected]
with:
terraform_version: "1.12.2"
terraform_wrapper: false
- uses: terraform-linters/setup-[email protected]
with:
tflint_version: v0.50.3
- uses: suzuki-shunsuke/tfaction/[email protected]
- uses: ./.github/actions/test-module
with:
github_token: ${{ steps.tfaction-app-token.outputs.token }}
env:
TFLINT_CONFIG_FILE: "${{ github.workspace }}/.tflint.hcl"
terragrunt-plan-status-check:
runs-on: ubuntu-latest
needs: [terragrunt-plan, test-module]
if: failure()
steps:
- run: exit 1
.github/workflows/terragrunt_apply.yml
name: Terragrunt Apply
on:
push:
branches:
- master
- staging
paths:
- "terragrunt/**"
jobs:
setup:
uses: ./.github/workflows/terragrunt_setup.yml
secrets: inherit
with:
target_envs: |-
${{
github.ref_name == 'master' && 'production' ||
github.ref_name == 'staging' && 'staging'
}}
terragrunt-apply:
name: "terragrunt-apply for ${{ matrix.target.target }}"
runs-on: ubuntu-latest
needs: setup
if: join(fromJSON(needs.setup.outputs.targets), '') != ''
strategy:
fail-fast: false
matrix:
target: ${{ fromJSON(needs.setup.outputs.targets) }}
permissions:
id-token: write
contents: read
env:
TFACTION_IS_APPLY: "true"
TFACTION_TARGET: ${{ matrix.target.target }}
TFACTION_JOB_TYPE: terraform
steps:
- uses: actions/create-github-app-[email protected]
id: tfaction-app-token
with:
app-id: ${{ vars.INFRA_TFACTION_GITHUB_APP_ID }}
private-key: ${{ secrets.INFRA_TFACTION_GITHUB_APP_PRIVATE_KEY }}
permission-actions: read
permission-issues: write
permission-pull-requests: write
permission-contents: write
- id: export-target
name: Export target service and env from matrix.target.target
env:
target: ${{ matrix.target.target }}
run: |
# $target: production/account
# $env: production
# $service: account
env=$(echo "$target" | cut -d "https://zenn.dev/" -f 1)
service=$(echo "$target" | cut -d "https://zenn.dev/" -f 2)
echo "env: $env"
echo "service: $service"
echo "env=$env" >> "$GITHUB_OUTPUT"
echo "service=$service" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-[email protected]
with:
role-to-assume: ${{ secrets.TERRAGRUNT_AWS_IAM_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- uses: actions/cache@v4
env:
tfaction_version: v1.16.1
with:
path: ~/.local/share/aquaproj-aqua
key: v2-aqua-installer-${{runner.os}}-${{runner.arch}}-for-tfaction-${{env.tfaction_version}}
restore-keys: |
v2-aqua-installer-${{runner.os}}-${{runner.arch}}-
- uses: aquaproj/aqua-[email protected]
with:
aqua_version: v2.51.1
- name: Install terragrunt
uses: jaxxstorm/action-install-gh-[email protected]
with:
repo: gruntwork-io/terragrunt
tag: v0.81.0
cache: enable
rename-to: terragrunt
chmod: 0755
extension-matching: disable
- uses: hashicorp/setup-[email protected]
with:
terraform_version: "1.12.2"
terraform_wrapper: false
- uses: suzuki-shunsuke/tfaction/[email protected]
with:
github_token: ${{ steps.tfaction-app-token.outputs.token }}
env:
TG_TF_FORWARD_STDOUT: "true"
- name: terragrunt apply
id: apply
uses: suzuki-shunsuke/tfaction/[email protected]
with:
github_token: ${{ steps.tfaction-app-token.outputs.token }}
env:
TG_TF_FORWARD_STDOUT: "true"
TG_LOG_LEVEL: warn
- name: create failure message
if: failure()
id: slack_failure_message
run: |
message=$(cat
EOS
)
{
echo 'message} >> "$GITHUB_OUTPUT"
- name: create success message
id: slack_success_message
working-directory: ${{ matrix.target.working_directory }}
env:
TFPLAN_FILE_PATH: tfplan.binary
run: |
ls -al ./ # to debug
TERRAGRUNT_CHANGES=$(terragrunt show -json "$TFPLAN_FILE_PATH" | jq '.applyable')
if [ "$TERRAGRUNT_CHANGES" = 'false' ]; then
message=$(cat *${{ github.actor }}* による *${{ github.ref_name }}* Mergeによって *${{ matrix.target.target }}* への terragrunt apply による差分はありませんでした
EOS
)
else
message=$(cat *${{ github.actor }}* による *${{ github.ref_name }}* Mergeによって *${{ matrix.target.target }}* への terragrunt apply が実行されました。
正常に完了しました
://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|Workflow URL>
EOS
)
fi
{
echo 'message} >> "$GITHUB_OUTPUT"
- name: Send Slack notification
if: always()
uses: ./.github/actions/slack_custom_notification
with:
message: ${{ steps.slack_success_message.outputs.message }}${{ steps.slack_failure_message.outputs.message }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL_DEV_TEAM_INFRA }}
.github/workflows/terragrunt_setup.yml
name: Terragrunt Setup
on:
workflow_call:
inputs:
target_envs:
required: true
type: string
description: 'plan/apply 対象の env。スペース区切りで複数指定可能'
outputs:
targets:
description: '変更があった Terragrunt ルートモジュール'
value: ${{ jobs.setup.outputs.targets }}
modules:
description: '変更があった Terragrunt 共有モジュール'
value: ${{ jobs.setup.outputs.modules }}
jobs:
setup:
runs-on: ubuntu-latest
outputs:
targets: ${{ steps.filtered-targets.outputs.targets }}
modules: ${{ steps.list-targets.outputs.modules }}
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
env:
tfaction_version: v1.16.1
with:
path: ~/.local/share/aquaproj-aqua
key: v2-aqua-installer-${{runner.os}}-${{runner.arch}}-for-tfaction-${{env.tfaction_version}}
restore-keys: |
v2-aqua-installer-${{runner.os}}-${{runner.arch}}-
- uses: aquaproj/aqua-[email protected]
with:
aqua_version: v2.51.1
- name: Install terragrunt
uses: jaxxstorm/action-install-gh-[email protected]
with:
repo: gruntwork-io/terragrunt
tag: v0.81.0
cache: enable
rename-to: terragrunt
chmod: 0755
extension-matching: disable
- uses: hashicorp/setup-[email protected]
with:
terraform_version: "1.12.2"
terraform_wrapper: false
- uses: suzuki-shunsuke/tfaction/list-[email protected]
id: list-targets
with:
github_token: ${{ github.token }}
- name: Exclude all but the target env
id: filtered-targets
env:
targets: ${{ steps.list-targets.outputs.targets }}
target_envs: ${{ inputs.target_envs }}
run: |
outputs="[]"
for target_env in ${target_envs}; do
filtered_targets=$(echo "$targets" | jq -c "map(select(
(.working_directory | startswith(\"terragrunt/envs/$target_env\"))
))")
echo "filtered_targets: $filtered_targets"
outputs=$(jq -n -c --argjson a "$outputs" --argjson b "$filtered_targets" '$a + $b')
done
echo "outputs: $outputs"
echo "targets=$outputs" >> "$GITHUB_OUTPUT"
tfaction-root.yaml
plan_workflow_name: Terragrunt Plan
update_local_path_module_caller:
enabled: true
tflint:
enabled: true
fix: true
trivy:
enabled: false
providers_lock_opts: -platform=linux_amd64 -platform=linux_arm64 -platform=darwin_arm64
target_groups:
target_groups:
- working_directory: terragrunt/envs/production/xxxx
target: production/xxxx
terraform_command: terragrunt
- working_directory: terragrunt/envs/staging/xxxx
target: staging/xxxx
terraform_command: terragrunt
- working_directory: terragrunt/envs/review/xxxx
target: review/xxx
terraform_command: terragrunt
今後の方針
今回導入した Action 以外にも、tfaction には様々な機能が提供されています。
- ドリフト検出
- apply 失敗時のフォローアップ PR の自動作成
- ワーキングディレクトリ、モジュールなどの scaffold
- Conftest サポート
- tfsec, trivy の実行
- terraform-docs の実行
今回紹介したように、tfaction は段階的・部分的な導入ができるため、これらすべての機能を使う必要はありませんが、社内の需要に応じて導入を進めていければと思います。
特にドリフト検出は Terraform の CI/CD を組む上でのリスクを減らせるため、積極的に導入したいと考えています。
まとめ
今回社内の課題を解決するのに tfaction がピッタリハマりました。
Terraform の CI/CD でお困りの方は部分的にでも tfaction を導入してみると、課題に対して必要最小限の実装・労力で済むため、非常におすすめです。
tfaction の良いところは小さく導入できるところだと思っています。他の Terraform CI/CD ツールだと割と組み込むのが大掛かりになったり、既存の開発フローを大きく変える必要があると思います。
Views: 0