2023/12/14

Terraform コードのテスト方法

詳解 Terraform 第3版

2023年11月に「詳解 Terraform 第3版」の日本語版がオライリーから発売された。

https://www.oreilly.co.jp/books/9784814400522/

詳解 Terraform の目次

  • 1章 なぜ Terraform を使うのか
  • 2章 Terraform をはじめよう
  • 3章 Terraform ステートを管理する
  • 4章 モジュールで再利用可能なインフラを作る
  • 5章 Terraform を使うためのヒントとコツ:ループ、条件文機、デプロイ、その他つまずきポイント
  • 6章 シークレットを管理する
  • 7章 複数のプロバイダを使う
  • 8章 本番レベルの Terraform コード
  • 9章 Terraform のコードをテストする
  • 10章 チームで Terraform を使う

「9章 Terraform のコードをテストする」が気になったので購入し、気になるところをメモした。

Terraform コードのテスト手法

本書の中ではテスト手法を以下のように区分している。

  1. 静的解析
  2. プランテスト
  3. サーバテスト
  4. ユニットテスト
  5. 結合テスト
  6. E2Eテスト

リストの下に行くにつれて実装・実行コストが増えるが、Terraform コードの動作確認における確からしさが上がっていく。

実際には特定の区分のみ用いるのではなく、静的解析 + プランテスト + ユニットテスト + E2Eテストなどのように組み合わせて使う。

結論

やはり銀の弾丸はない。自分たちの身の丈、運用、サービスレベルにあったテスト手法を選択することになる。

テスト手法の組み合わせ、各テスト手法にかけるコスト比重、導入順序を検討し、サービスの成長に合わせて変えていくことになる。

静的解析はコストが非常に少ないのでとりあえず導入してみるでも良さそう

以下はそれぞれの手法の概要です。

静的解析

代表的なツール

  • terraform validate
  • tfsec
  • tflint
  • Terrascan

静的解析ツールの強み

  • 動作が高速
  • 使用が簡単
  • 安定している
  • 実際のプロバイダに認証の必要がない
  • 実際のリソースをデプロイ・削除しない

静的解析ツールの弱み

  • 非常に限られた種類のエラーのみ発見できる
  • 機能性をチェックしないので、すべてのチェックが成功してもインフラが動かない可能性がある

プランテスト

terraform plan を実行し、プランの出力を解析する

静的解析より踏み込んでいるが、コードを完全に実行するわけではない

プランテストツール

  • Terratest
  • Open Policy Agent
  • Hashi Corp Sentinel
  • Checkov
  • terraform-compliance
  • terraform test で plan を使用する
    • v1.6で追加されたコマンド。本書はv1.3時点に書かれているそうなので、記載されていない

Terratestを用いた例

planの出力結果

Terraform will perform the following actions:

  # module.alb.aws_lb.example will be created
  + resource "aws_lb" "example" {
      + arn = (known after apply)
      + load_balancer_type = "application"
      + name = "test-4Ti6CP"
      (...)
  }

  (...)

Plan: 5 to add, 0 to change, 0 to destroy.

Terratestでのplanを使ったテスト

func TestAlbExamplePlan(t *testing.T) {
    t.Parallel()

    albName := fmt.Sprintf("test-%s", random.UniqueId())

    opts := &terraform.Options{
        // この相対パスは、自分の alb モジュールの example ディレクトリを
        // 指すよう変更すること
        TerraformDir: "../examples/alb",
        Vars: map[string]interface{}{
            "alb_name": albName,
        },
    }

    planString := terraform.InitAndPlan(t, opts)

    // plan の出力の add/change/destroy の数をチェックする方法の例
    resourceCounts := terraform.GetResourceCount(t, planString)
    require.Equal(t, 5, resourceCounts.Add)
    require.Equal(t, 0, resourceCounts.Change)
    require.Equal(t, 0, resourceCounts.Destroy)
}

Policy as Codeツール

  • Open Policy Agent(OPA)が人気
  • Rego という宣言型言語でポリシーをコートして記録できる
  • Terraformで管理しているリソースに ManagedBy というタグがあるかチェックする
// enforce_tagging.rego
package terraform

allow {
    resource_change := input.resource_changes[_]
    resource_change.change.after.tags["ManagedBy"]
}
  • 使い方
    • planの出力をプランファイルに保存する terraform plan -out tfplan.binary
    • OPAはJSONに対してのみ実行できる。JSONに変換 terraform show -json tfplan.binary > tfplan.json
    • JSONになったプランファイルをチェックする opa eval --data enforce_tagging.rego --input tfplan.json --format pretty data.terraform.allow

プランテストツールの強み

  • ユニットテスト・結合テストと静的解析の間の実行速度
  • 使いやすい
  • 安定している。不安定なテストはほとんどない
  • 実際のリソースを作成・削除しない

プランテストツールの弱み

  • 静的解析よりは多くのエラーを発見できる
  • プロバイダの認証が必要
  • 静的解析と同じく、テストが通ってもインフラが動かないことはあり得る

サーバテスト

サーバ(仮想サーバも含む)が正しく設定されているかテストすることに焦点を当てたテストツール

サーバテストツール

  • InSpec
  • Serverspec
  • Goss

InSpecの例

describe file('/etc/myapp.conf') do
  it { should exist }
  its('mode') { should cmp 0644 }
end

describe apache_conf do
  its('Listen') { should cmp 8080 }
end

describe port(8080) do
  it { should be_listening }
end

サーバテストツールの強み

  • ツールが提供するDSLで一般的な項目は簡単に確認できる
  • applyして、動作しているサーバを確認するので、より多くのエラーを発見できる。

サーバテストツールの弱み

  • 高速ではない。applyとdestroy(必要ないケースもあり)が必要になる
  • 実際にapplyするため、不安定なテストが存在する
  • プロバイダの認証が必要
  • リソースの作成・削除が必要。お金がかかる
  • サーバはチェックできるが、それ以外のインフラはチェックされない

ユニットテスト

Terraform におけるユニットは、再利用可能なモジュール

Terraform コードはAWSのAPIなどを呼びだすため、外部依存関係を全く無くす現実的な方法は存在しない

  • 純粋なユニットテストができない。実際のところ結合テストになる

実際のインフラを実際の環境にデプロイする自動テストを書くことで、Terraform コードが期待通りに動作するか確認できるため

基本戦略

  1. 小さく、独立したモジュールを作る
  2. terraform apply を実行し、実環境にデプロイする
  3. 期待通りに動作するか確認。ALBならHTTPリクエストを送る等
  4. terraform destroy で後片付け

terraform test で apply を使用する

  • v1.6で追加されたコマンド。本書はv1.3時点に書かれているそうなので、記載されていない

Terratest

  • terraform applyを実行し、動作確認をし、terraform destroy をする
  • HTTPリクエストの送信等のヘルパー関数が含まれる
  • terraform の出力変数の値を利用してテストコードを書ける
func TestAlbExample(t *testing.T) {
    opts := &terraform.Options{
        // この相対パスは、自分の alb モジュールの example ディレクトリを
        // 指すよう変更すること
        TerraformDir: "../examples/alb",
    }

    // テストの最後にすべてを後片付け
    defer terraform.Destroy(t, opts)

    // サンプルをデプロイ
    terraform.InitAndApply(t, opts)

    // ALB の URL を取得
    albDnsName := terraform.OutputRequired(t, opts, "alb_dns_name")
    url := fmt.Sprintf("http://%s", albDnsName)

    // ALB のデフォルトアクションが動作し、404 を返すことをテスト
    expectedStatus := 404
    expectedBody := "404: page not found"
    maxRetries := 10
    timeBetweenRetries := 10 * time.Second
    
    http_helper.HttpGetWithRetry(
        t,
        url,
        nil,
        expectedStatus,
        expectedBody,
        maxRetries,
        timeBetweenRetries,
    )
}

結合テスト

複数のUnitを組み合わせてテストをする

  • mysql serverの作成、mysql server を使用するweb app

E2E テスト

インフラが複雑になると実行に時間がかかりすぎる

現実的な対応として、本番に近いテスト環境を1回だけ構築し、動かしたままにする。

インフラコードに変更を加えたらテスト環境に変更を適用し、SeleniumなどでE2Eテストを実行する

その他:不安定なテストに対してリトライする

インフラのコードの定期的に自動テストをすると、不安定なテスト(flaky tests)に直面する

  • 時々EC2が立ち上がらないとか

Terratestを使っているなら、terraform.Options の MaxRetries、TimeBetweenRetries、RetryableTerraformErrors の各引数を使ってリトライを有効にする