モジュールの契約をテストする

ShellBeginner
オンラインで実践に進む

はじめに

設定の書式と構造が正しくても、レポート環境のラベルが誤っていることがあります。チームがデリバリーのプランを信頼するには、その前に、小さなストレージモジュールの入力、出力、計画されたタグを実行可能なチェックで確かめる必要があります。

モジュールの契約を定義し、対応する 2 つの環境と未対応の入力をテストして、実際の業務上の不備でジョブを停止させます。モジュールの変数と出力、および前の実験で扱った書式と妥当性の検証ゲートを理解していることを前提とします。既存の参照用ストレージは変更せずに保持してください。

認定試験との関連

認定資格 試験のタスク 演習内容
DevOps Engineer – Professional (DOP-C02) Task 1.2 実行可能なモジュールのアサーションを自動の品質ゲートに組み込む基礎演習です。

参照用ストレージを保持しながら、デリバリーのプラン作成前に plan モードのアサーションでモジュールの入力を確認します。

入力の契約を定義する

このステップでは、ストレージモジュールがどの環境入力を受け入れるかを定義します。入力検証は、リソースのプランを作成する前に、未対応のリクエストを拒否します。

用意されたモジュールに移動し、準備済みのツールを読み込みます。

cd /home/labex/project/report-module
source environment.sh

インターフェースとリソースの実装を読みます。

cat variables.tf main.tf outputs.tf pipeline.json

environment は preview または review を選択し、bucket_prefix は呼び出し側のストレージ名を区別します。バケット名は両方の入力を組み合わせたものになり、Environment タグと出力は、要求された環境を示す必要があります。用意された main.tf、outputs.tf、provider.tf、run-job.py は保持する対象です。最終的な内容を変更しないでください。作業対象は variables.tf、tests/、pipeline.json です。

environment に validation ブロックを追加します。contains は対応するリストに値が含まれるかを確認し、condition が false の場合は指定したエラーメッセージを出します。引用符付きの区切り語を使うヒアドキュメントは、HCL をそのまま書き込みます。

cat > variables.tf <<'EOF'
variable "environment" {
  type    = string
  default = "preview"
  validation {
    condition     = contains(["preview", "review"], var.environment)
    error_message = "Choose preview or review."
  }
}
variable "bucket_prefix" {
  type    = string
  default = "delivery-contract"
}
EOF

変更の書式を整えます。

terraform fmt

未対応のリクエストを試します。-var は、この呼び出しに対して値を渡します。このコマンドは意図的に Terraform 自体の終了コード 1 を返します。production のバケットを提案するのではなく、Choose preview or review. を表示しなければなりません。

terraform plan -input=false -var=environment=production

検証は、受け入れるインターフェースを定義します。次のステップでは、受け入れる両方の値と、この想定された拒否をテストします。

アサーションを書いて実際の失敗を観察する

このステップでは、Terraform 標準の terraform test で、モジュールの計画された動作を確認します。書式や妥当性の検証は、固定値で書かれた誤った出力を受け入れる場合があります。アサーションは、計算された結果を要求された入力と比較します。

Terraform は tests/ 内の .tftest.hcl ファイルを読みます。各 run は独立したテストシナリオです。明示的な command = plan は、適用せずにプランを評価します。テストファイルは tests/ 内に .tftest.hcl 形式で保持してください。この実験では、すべての run が plan モードと用意されたプロバイダー/モジュールを使用する必要があります。別のモジュール、プロバイダーのモック、オーバーライドは対象外です。bucket_prefix はスイートの入力から継承したままにし、ファイルレベルや run レベルのテスト変数で代入しないでください。これにより、呼び出し側が別の正当なプレフィックスを選択できます。これらのテストが確認するのは、既知のプラン値であり、実際の作成、実行時の権限、デプロイ済みのアプリケーションではありません。

テストディレクトリを作成します。mkdir -p は、必要ならディレクトリを作成し、すでに存在する場合も受け入れます。

mkdir -p tests

デフォルトの環境と別の環境に対するアサーションを書きます。var は run の入力を参照し、リソース参照は計画された値を確認し、output はモジュールの出力を参照します。expect_failures は、指定した変数の検証が失敗することを要求します。任意のプロバイダーエラーやネットワークエラーをテストの成功に変えるものではありません。

cat > tests/contract.tftest.hcl <<'EOF'
run "preview_contract" {
  command = plan
  assert {
    condition     = output.environment == var.environment
    error_message = "The output must identify the requested environment."
  }
  assert {
    condition     = aws_s3_bucket.reports.bucket == "${var.bucket_prefix}-${var.environment}"
    error_message = "The bucket name must use the requested prefix and environment."
  }
  assert {
    condition     = aws_s3_bucket.reports.tags.Environment == var.environment
    error_message = "The planned tag must identify the requested environment."
  }
}
run "review_contract" {
  command = plan
  variables { environment = "review" }
  assert {
    condition     = output.environment == var.environment
    error_message = "The alternate environment must not use the default label."
  }
  assert {
    condition     = aws_s3_bucket.reports.bucket == "${var.bucket_prefix}-${var.environment}"
    error_message = "The alternate bucket must use its requested inputs."
  }
  assert {
    condition     = aws_s3_bucket.reports.tags.Environment == var.environment
    error_message = "The alternate environment tag must match its input."
  }
}
run "unsupported_environment" {
  command         = plan
  variables { environment = "production" }
  expect_failures = [var.environment]
}
EOF

テストとモジュールのファイルを再帰的に整形します。

terraform fmt -recursive

テストスイートを実行します。

terraform test

3 つの run が通過し、成功の要約が表示されるはずです。テスト用バケットはデプロイされません。入力と出力の値は、設定済みのプロバイダーを使用する実際の Terraform プランから取得されます。

アサーションが、起こり得る不備を検出することを確かめます。一時的に、環境の出力だけを固定のデフォルトラベルに置き換え、バケット名の出力は保持します。

cat > outputs.tf <<'EOF'
output "environment" {
  value = "preview"
}
output "bucket_name" {
  value = aws_s3_bucket.reports.bucket
}
EOF

スイートを再実行します。このコマンドは意図的に Terraform 自体の終了コード 1 を返します。別環境のアサーションが失敗し、有効な HCL でもモジュールの契約に違反し得ることを示すはずです。

terraform test

用意された出力の実装を Git から復元します。これにより、作成した検証とテストを保持したまま、一時的な不備を取り除きます。

git restore -- outputs.tf

修正した実装を、同じアサーションで実行します。

terraform test

すべての run が再び通過するはずです。テストは、1 つのデフォルト出力を認識するだけでなく、正当な入力を受け入れなければなりません。

テストでデリバリージョブを停止させる

このステップでは、静的チェックのゲートとプラン作成の間に、モジュールテストを配置します。用意された実行プログラムは準備済みのものです。run-job.py を変更せず、ステージ設定を編集してください。

fmt/validate の後、plan の前に test を追加します。

cat > pipeline.json <<'EOF'
{"stages":["fmt","validate","test","plan"]}
EOF

ジョブ全体を実行します。

python3 run-job.py

4 つすべてのステージが通過するはずです。このモジュールは適用されていないため、最後のプランは Terraform 自体の終了コード 2 を返す場合があります。ランナーはそれを正常なプレビューとして扱います。一方、テストが失敗した場合は、plan ステージより前にジョブを停止させなければなりません。apply ステージは存在しません。

別の正当なバケットプレフィックスで、スイートを実行します。この -var は、モジュールやテストを編集せずに、スイートの入力を変更します。

terraform test -var=bucket_prefix=audit-contract

同じ 3 つの run が通過するはずです。この演習では、各 run の bucket_prefix を呼び出し側から継承したままにしてください。テストファイルや run レベルの variables ブロックで代入してはいけません。そのような代入は CLI のテスト入力を上書きし、このコマンドが要求したプレフィックスを試せなくなります。両方の対応環境を試すための環境の上書きは、引き続き必要です。要求された入力から構築したアサーションは、呼び出し側が別の有効な名前を選んでも機能し続けます。

既存の参照用レポートをストレージから直接読み取ります。これらの準備済みバケットは別の環境に属するため、削除や変更をしてはいけません。

aws s3   cp s3://delivery-reference-reports/report.txt -

引き続き reference report v1 と表示されるはずです。隣接するデータも確認します。

aws s3   cp s3://delivery-reference-neighbor/keep.txt -

preserve contract neighbor と表示されるはずです。テストはストレージリソースを作成していないため、削除すべきテストスタックはありません。最後のチェックのために、入力の契約、テスト、パイプライン設定を保持してください。一時的な出力の不備は、すでに取り除かれています。

まとめ

対応する入力の契約を定義し、Terraform 標準の plan モードのテストで、計画された出力とタグを入力と比較して、実際のアサーション失敗を観察しました。その後、既存のデータと業務状態を保持しながら、テストをデリバリーの品質ゲートに組み込みました。

plan モードのテストは、デプロイ後の動作を証明するものではありません。後のデリバリー手順では、これらのチェックを、確認済みの具体的なプランや、実際の制御された適用と組み合わせます。今回練習した仕組みについては、公式の Terraform test リファレンスとテスト設定のドキュメントを参照してください。