インフラの静的解析を超えて
長年、私のInfrastructure as Code(IaC)のワークフローは予測可能なものでした。HCLを書き、terraform planを実行し、出力がほぼ正しければapplyを押す。このアプローチは基本的なセットアップには有効です。しかし、アーキテクチャがスケールするにつれて、「plan」は偽の安心感を与えるようになります。それはTerraformの意図(intent)を確認するだけであり、VPCピアリング接続が実際にトラフィックをルーティングしているか、あるいはデータベースにポート5432で到達可能であるかを保証するものではないからです。
転機は6ヶ月前に訪れました。共有VPCモジュールでの単純なCIDRブロックの変更が、すべての手動チェックをパスしたにもかかわらず、サイレントなルーティング競合を引き起こしたのです。ステージング環境が崩壊するまで、私たちはそれに気づきませんでした。その失敗をきっかけに、私たちはTerratestを導入することにしました。推測するのをやめ、インフラをソフトウェアのように扱うことにしたのです。実際のリソースをプロビジョニングし、それらに対して機能テストを実行し、終了後はすぐに破棄します。
このワークフローを採用して以来、デプロイ後の「ホットフィックス」率は約40%減少しました。金曜日の午後のデプロイで祈る必要はもうありません。代わりに、コードがメインブランチに到達する前に、HTTP 200レスポンスから特定のIAMポリシーの添付まで、すべてを検証するGoベースのテストスイートを信頼しています。
テスト環境の構築
TerratestはGoライブラリであるため、Terraformと並んでGoのランタイムが必要になります。Goを触ったことがないDevOpsエンジニアの方も、恐れる必要はありません。ほとんどのインフラテストは、非常に反復的なパターンに従います。最初の構造さえ掴んでしまえば、ほぼすべてのモジュールにコピーして適応させることができます。
前提条件
ローカルマシンまたはCIランナーに以下のツールが用意されていることを確認してください。
- Terraform: 本番環境と一致するバージョン(例:v1.5.0以降)。
- Go: モダンなライブラリサポートのために、バージョン1.18以降が推奨されます。
- クラウド認証情報: 専用のテスト用アカウントを指すように環境変数(
AWS_ACCESS_KEY_IDなど)を設定します。
まずは、プロジェクトのルートで新しいGoモジュールを初期化します。これにより、テストの依存関係が管理されます。
# テスト専用ディレクトリを作成
mkdir test && cd test
# Goモジュールを初期化
go mod init github.com/your-org/infra-tests
# Terratestのterraformモジュールを取得
go get github.com/gruntwork-io/terratest/modules/terraform
初めてのTerratestスイートの設定
Terratestは厳格なライフサイクルに従います。それは、「デプロイ(Deploy)、検証(Validate)、アンデプロイ(Undeploy)」です。バージョニングが有効なS3バケットを作成するモジュールをテストしてみましょう。コマンドが完了したからといって「動いたはずだ」と仮定するのではなく、バケットが存在し、バージョニングステータスが「Enabled」であることを証明したいのです。
Terraformモジュール (main.tf)
modules/s3にある標準的なモジュールを例にします:
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = "Enabled"
}
}
output "bucket_id" {
value = aws_s3_bucket.this.id
}
Goテストスクリプト (s3_test.go)
test/フォルダにs3_test.goを作成します。複数の開発者が同時にテストを実行した際の名前の衝突を防ぐため、リソースには一意の命名規則を使用することをお勧めします。
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestS3BucketVersioning(t *testing.T) {
t.Parallel()
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../modules/s3",
Vars: map[string]interface{}{
"bucket_name": "terratest-audit-log-bucket-unique-id",
},
})
// 最後に「terraform destroy」が実行されるようにする
defer terraform.Destroy(t, terraformOptions)
// 「terraform init」と「terraform apply」を実行
terraform.InitAndApply(t, terraformOptions)
// 出力変数を取得
bucketId := terraform.Output(t, terraformOptions, "bucket_id")
// バケットが実際に作成されたことをアサート(検証)する
assert.NotEmpty(t, bucketId)
}
defer terraform.Destroyの行には細心の注意を払ってください。これはあなたの保険です。テストが途中で失敗した場合でも、このコマンドによってリソースが確実に削除されます。これがないと、テスト終了後も月額500ドルのNATゲートウェイやアイドル状態のEKSクラスターがアカウントに残り続けることになりかねません。
検証とCI/CDへの統合
コードを書くことは第一歩に過ぎません。自動化こそが、真の投資対効果(ROI)を生む場所です。ローカルテストはデバッグには最適ですが、CI/CDパイプラインこそが、これらのテストが最終的なゲートキーパーとして機能する場所です。
スイートの実行
test/ディレクトリから、次のコマンドでテストを実行します:
go test -v -timeout 30m
タイムアウトを30分に設定しています。標準的なGoのテストは数秒で終わりますが、インフラテストはクラウドプロバイダーの都合に左右されます。RDSインスタンスやCloudFrontディストリビューションのプロビジョニングには、15分から20分かかることも珍しくありません。
GitHub Actionsワークフロー
以下は、典型的な検証ジョブの設定例です。これにより、すべてのプルリクエストがマージされる前に、ライブのサンドボックス環境でテストされることが保証されます。
name: IaCの検証
on: [pull_request]
jobs:
terratest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: Terraformのセットアップ
uses: hashicorp/setup-terraform@v2
- name: テストの実行
run: |
cd test
go mod tidy
go test -v -timeout 60m
env:
# AWSテストアカウントの認証情報を設定
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_TEST_ACCOUNT_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_TEST_ACCOUNT_SECRET }}
AWS_REGION: "us-west-2"
「リーク」したリソースの管理
deferを使用していても、テストが極端に失敗してリソースが残ってしまうことが稀にあります。予期せぬ請求を避けるため、私は「nuke(全消去)」戦略を採用しています。テスト用アカウントでaws-nukeというツールを使用し、testing: trueタグが付いた24時間以上経過したリソースをパージするようにしています。また、テストには完全に独立したAWSアカウントを使用することが必須です。これにより、本番データが誤って削除されるのを防ぐ強固な境界が提供されます。
6ヶ月後の振り返り
インフラに対してテスト駆動のアプローチを導入するには、事前の時間投資と学習が必要です。初期の開発スピードは低下するでしょう。しかし、その代償として、デプロイに対する自信が大幅に向上します。私たちのチームは、コアとなるネットワークモジュールのリファクタリングを恐れなくなりました。セーフティネットが、エラーがユーザーに届く前にキャッチしてくれるからです。
ミッションクリティカルな環境を管理しているなら、まずは小さく始めてみてください。重要なモジュールを1つ選び(セキュリティグループのロジックやロードバランサーの設定など)、テストを1つ書いてみましょう。それが実際の環境での設定ミスをキャッチするのを一度目の当たりにすれば、もう手動検証には戻りたくなくなるはずです。

