はじめに
こんにちは。CTO室 Platform開発チーム SREの原(@kouzyunJa)です。
ファインディでは日々サービスが爆速で開発されており、新しいプロダクトも次々と生まれています。それらのインフラ構築を、私たちのチームが支えています。
インフラ構築にもスピード感が求められるため、効率的かつ安全に進める仕組みが欠かせません。
今回は、そのスピード感あるインフラ構築を支えるTerraform Testの取り組みについてご紹介します。
Terraform Testの導入背景
Terraform TestはTerraform 公式のテストフレームワークで、Terraform v1.6.0から利用可能です。
モジュール更新による破壊的変更を事前に検証でき、テストは新規にリソースが構築され、その後自動的に削除されます。既存のインフラ環境やstateへの影響はありません。
弊社ではIaCとしてTerraformを導入しており、HCP Terraformにてstateの情報を管理しています。
スピード感のあるインフラ構築を実現するため、ファインディのプロダクトでよく利用するリソースをモジュール化しています。
これらのモジュールを、私たちは「汎用モジュール」と呼んでいます。
AWSの新規インフラ環境構築時にはこの汎用モジュールを活用する取り組みを推進しています。
しかし、汎用モジュールから環境を構築する際にTerraform Planは通るものの、Terraform Applyで失敗するケースがあり、その結果、構築スピードが低下するという課題がありました。

この課題を解決するため、Terraform Testを導入しました。
Terraform Testの書き方紹介
Terraform Test は専用のテストファイルに記述します。.tftest.hclまたは.tftest.jsonの拡張子のファイルにテストコードを記載していきます。
イメージが付きやすいように、S3バケットを構築するシンプルな例を使って説明します。
# main.tf provider "aws" { region = "ap-northeast-1" } variable "bucket_prefix" { type = string } resource "aws_s3_bucket" "example" { bucket = "${var.bucket_prefix}-bucket" }
このS3バケットの名前が想定通りで構築されているかのテストを書いてみますと次のようになります。
# main.tftest.hcl variables { bucket_prefix = "test" } run "valid_string_concat" { command = plan assert { condition = aws_s3_bucket.bucket.bucket == "test-bucket" error_message = "S3 bucket name did not match expected" } }
runブロック内でテストを記述しており、assertで条件式を定義し、期待する値と一致するかを検証します。
このように作成するリソースの値が正常に設定されているかを確認できます。
UnitテストとIntegrationテスト
ファインディではTerraform Testにおいて、独自にUnitテストとIntegrationテストの2種類のテストカテゴリを設けています。
- Unitテスト: Terraform Planに相当し、リソースを作らず論理的に検証する
- Integrationテスト: 実際にリソースをApplyして作成し検証、その後は自動的にリソースのDestroy行われる
この2つを組み合わせることで、インフラ環境のテストを行っています。
使い分けはrunブロック内のcommandの指定によって決まります。
command = planを指定すればUnitテストを実行するcommand = applyを指定すればIntegrationテストを実行する
よって、さきほどの例はcommand = planを指定しているのでunitテストとしています。Integrationテストでも、assertでの値の確認は同様に行うことができます。
ディレクトリ構成
テストのコードは、各モジュール配下のtestsディレクトリに配置しています。
UnitテストとIntegrationテストをディレクトリで分けており、対象のテストだけを個別に実行できます。
# Terraform モジュール と テスト構成例
modules/
<module-name>/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
├── unit/
│ └── main.tftest.hcl
└── integration/
└── main.tftest.hcl
テストを実施したい場合は、terraform testコマンドを使用します。
また、 -test-directoryオプションを指定することで、Unitテスト用のディレクトリや Integrationテスト用のディレクトリを切り替えて実行できます。
$ terraform test -test-directory=./tests/unit
実行結果は次のように表示されます。
runブロックごとにテスト名と結果が表示され、成功(pass)か失敗(fail)かを確認できます。
tests/unit/main.tftest.hcl... in progress run "test1"... pass run "test2"... pass run "test3"... pass run "test4"... pass run "test5"... pass tests/unit/main.tftest.hcl... tearing down tests/unit/main.tftest.hcl... pass Success! 5 passed, 0 failed
mock providorとUnitテスト
Unitテストではcommand = planを利用して実リソースを作らず論理的に検証します。
テストを行うモジュールの外部にあるリソース情報(例: 既存のIAM RoleのARNや、外モジュールのS3バケットのARN)を参照する場合、 mock providerを活用すると、外部リソースを実際に参照せずにテストを実行できます。
# main.tftest.hcl mock_provider "aws" { mock_data "aws_iam_role" { defaults = { arn = "arn:aws:iam::111122223333:role/mock-role" } } } run "check_role_arn" { command = plan assert { condition = data.aws_iam_role.example.arn == "arn:aws:iam::111122223333:role/mock-role" error_message = "IAM Role ARN does not match expected value" } }
このようにmock providorを活用することで、モジュール外部のリソースを実際に参照せずともUnitテストを行うことができます。
複数モジュールを組み合わせたIntegrationテスト
ファインディでは汎用モジュールを「Network」「Container」など機能単位でモジュールを分けて作成しています。
このため、例えばContainerモジュールを単体でIntegrationテストしようとしても、Networkモジュールで作成しているVPCやSubnetが存在しないためApplyに失敗します。
そこで依存するNetworkモジュールを先にApply → その出力値をContainerモジュールに渡す、という流れを取り入れました。
これにより、依存関係を含めた最小構成を再現しながら Integrationテストを実行できます。
# main.tftest.hcl run "network_module_apply" { command = apply # Call the Network module module { source = "./../../../network" } variables { vpc_cidr_block = "XX.XX.XX.XX/XX" subnet_public_1a_cidr_block = "XX.XX.XX.XX/XX" subnet_public_1c_cidr_block = "XX.XX.XX.XX/XX" subnet_public_1d_cidr_block = "XX.XX.XX.XX/XX" subnet_private_1a_cidr_block = "XX.XX.XX.XX/XX" subnet_private_1c_cidr_block = "XX.XX.XX.XX/XX" subnet_private_1d_cidr_block = "XX.XX.XX.XX/XX" } } run "container_module_apply" { command = apply # Call the container module module { source = "./../../../container" } variables { vpc_id = run.network_module_apply.vpc_id private_subnets = [ run.network_module_apply.subnet_private_1a_id, run.network_module_apply.subnet_private_1c_id, run.network_module_apply.subnet_private_1d_id ] public_subnets = [ run.network_module_apply.subnet_public_1a_id, run.network_module_apply.subnet_public_1c_id, run.network_module_apply.subnet_public_1d_id ] } }
CIワークフローでの活用
ここからはGitHub ActionsのCIに組み込んだTerraform Test活用方法について紹介します。
Terraform TestはCI ワークフローに組み込んで自動実行しています。
CIに組み込む際のポイントとしてUnitテストとIntegrationテストは使い分けています。
- Unitテスト: PR作成時に実行
- Integrationテスト: Mainブランチへマージ後、HCP Terraformへリリースする前に実行
使い分けている理由として、Integrationテストはリソースの構築から削除までを伴うため、完了まで時間がかかります。
例えばAuroraの場合、構築から削除完了まで長時間のテストを待つ必要があり、PRのレビュー速度に影響するため、マージ後に実行する設計としています。
Unitテストは論理的に検証するため、PR作成時に実行してスピーディーな確認ができます。

サンプルコードは次のようになります。
name: PR - Terraform Test (Unit)
on:
pull_request:
paths:
- "**.tf"
- "**.hcl"
jobs:
unit-tests:
runs-on: ubuntu-XX.XX
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: X.XX
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ap-northeast-1
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
role-duration-seconds: 1200
- name: Terraform init
run: terraform init
- name: Terraform validate
run: terraform validate -no-color
- name: Terraform test (unit)
run: terraform test -test-directory=tests/unit
PRのUnitテストで問題ないと判断したら、mainブランチへマージを行いIntegrationテストを実行します。
AWSのSandbox環境で apply → assert → destroy を行い、リソースがApplyに失敗せず正しく作成できるかを検証します。
Integrationテストがパスされれば汎用モジュールのstateを管理しているHCP Terraformへリリースされます。

サンプルコードはこちらになりますが、ほとんどUnitテストと変わりません。
name: Release - Terraform Test (Integration)
on:
push:
branches:
- main
jobs:
integration-tests:
runs-on: ubuntu-XX.XX
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: X.XX
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ap-northeast-1
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
role-duration-seconds: 1200
- name: Terraform init
run: terraform init
- name: Terraform validate
run: terraform validate -no-color
- name: Terraform test (integration)
run: terraform test -test-directory=tests/integration
このようにCIのワークフローにTerraform Testを組み込むことで、PR時にUnitテストで素早いフィードバックを得られ、マージ後にはIntegrationテストで実際の構築検証を行うという二段構えが実現できました。
結果として、より信頼性の高い汎用モジュールを安全に開発できるようになっています。
まとめ
以上がTerraform TestとCIワークフローへの組み込みの紹介となります。
テストを汎用モジュールに組み込むことで、事前にApply失敗を検知できる仕組みを整え、信頼性を高めつつスピード感のあるインフラ構築を進められるようになりました。
一方で、Integration TestはSandbox環境に実際のリソースを構築するため実行時間が長くなるという課題もあります。今後はテストスピードの改善や効率化の仕組みを検討していく予定です。
皆さんのTerraform活用のヒントになれば幸いです。
ファインディでは一緒に会社を盛り上げてくれるSREメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。