メインコンテンツにスキップ

OpenTofu の新しいテスト機能の開発中に学んだこと

Eran Elbaz
Arel Rabinowitz
What We Learned While Working on OpenTofu's New Test Feature

新しくフォークされたプロジェクトに飛び込むのは、困難で、場合によっては恐ろしいことかもしれません!さて、9年分のレガシーコードベースを持つ本番環境対応のプロジェクトに飛び込むことを考えてみてください。さらに、プロジェクトの新しい機能の1つを実験段階から本番環境対応にするというタスクがあります。

これは、OpenTofu のテスト機能に関する私たちの旅です。

これは、私たちが独自のパイプラインで作業する際に、OpenTofu の舞台裏で何が起こっているのかを理解するための興味深い入門編です。このケースでは、生の実験的なコードを、ドロップイン置換である OpenTofu v1.6.alpha で Terraform v1.6.0 に匹敵する(またはそれ以上の)パフォーマンスを維持するものに変えています。

この機能

HashiCorp Terraform ライセンスの変更に伴い、私たちは OpenTofu イニシアチブに参加しました。そして、私たちの最初のタスクの1つは、OpenTofu を今後の Terraform 1.6.0 に追いつかせることです。つまり、テスト機能を実験段階から本番環境対応にすることです。

フォーク時、テスト機能はすでにコードベースに存在していました。しかし、機能の仕組みに関するドキュメントがなく、テスト機能自体の機能に対するテストカバレッジもあまりなく、依然として実験段階にありました。

つまり、WIP テスト機能と思われるものの目的、このアプローチのメリットとデメリットを把握し、「開発の泥沼」から抜け出すために何が欠けているのかを理解する必要がありました。

私たちは、いくつかの異なることを行うことで、機能のマッピングを開始しました。

まず、以前のテストを読んで、さまざまなシナリオで機能がどのように動作するかを理解しました。次に、そのコードを徹底的に読んで、詳細を把握しました。次に、新しいテスト機能を実際に使ってみて、感触をつかみました。

これらは重要です。なぜなら、あなたが考えるべき姿に基づいて、コードを変更し始めるだけではいけないからです。元の実装者のアーキテクチャと意図を理解して、あなたが書くコードがそれに反するのではなく、それを補完するようにする必要があります。

この作業の後、私たちはテスト機能の全体像を把握しました。これは、ユーザーがモジュールごとのエンドツーエンドの方法でモジュールの構成をテストするのに役立つように構築されたフレームワークです。モジュールが一般的なケースで期待どおりに動作することを確認しながら、誤った構成などの障害条件に予測可能かつ安全に対応できるようにします。

テスト機能では、*.tftest.hcl ファイルが導入されています。これらのファイルは、1) テストスイートを記述し、2) HCL ブロックの特定のサブセットをサポートします。最も重要なブロックは、テスト実行を構成する新しい run ブロックです。

tofu test を実行すると、各 run ブロックはバックグラウンドで tofu plan または tofu apply を実行し、テスト用に指定された構成でモジュールを実行し、実際にクラウドにリソースを作成します (apply の場合)。

各実行後、検証を実行します。つまり、1) すべてのアサーションがパスし、2) チェックの失敗がなく、3) plan/apply が正常に完了したことを確認します。

tofu がすべての実行とテストの実行を完了した後、その tofu test 実行の一部として作成されたすべてのリソースを破棄しようとします。

どのようにアプローチしたか?

最初のテストとコードリーディングを通じて、コードベースのテストでカバーされていない機能の動作のリストを作成し、また、適切に動作しない可能性があると感じた動作のリストも作成しました。

私たちは、主に読んだコードと、レガシー Terraform および以前の問題に関する知識に頼りました。それらのほとんどについて、プルリクエストを作成し、テストカバレッジを追加したり、実際にバグを修正したりしました。

この機能をテストした際、いくつかのバグに遭遇し、フォーク前のコードベースにすでに存在していたものに加えて、実際に機能を改善するためのいくつかの提案を思いつきました。以下に、私たちが最終的に修正したバグのいくつかの例を示します。

バグ 1: run ブロック内の機密値

バグの説明: run ブロックのアサーション内で機密値を評価すると、tofu test がクラッシュします。

手動 QA シナリオを試しているうちに、特定のシナリオで tofu test を実行するとプログラムがクラッシュすることを発見しました。具体的には、設定にアサーションを持つ run ブロックが含まれており、そのアサーション自体が機密値に依存している場合に発生します (main.tftest.hcl のコード例を以下参照)。

これは理想的ではありません。tofu test がクラッシュするということは、クラウドにリソースが残ってしまい、tofu がそれを認識できなくなることを意味するからです。

次の設定を実行することで、このクラッシュを再現できます。

main.tf

コードブロック
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.14.0"
}
}
}

resource "aws_secretsmanager_secret" "my_secret" {
name = "my_secret"
}

resource "aws_secretsmanager_secret_version" "my_secret_version" {
secret_id = aws_secretsmanager_secret.my_secret.id
secret_string = "secret_value"
}

main.tftest.hcl

コードブロック
run "secret_test" {
assert {
condition = aws_secretsmanager_secret_version.my_secret_version.secret_string == "secret_value"
error_message = "bad secret"
}
}

この設定を tofu test で実行すると、実際に tofu がクラッシュします。aws_secretsmanager_secret_version.my_secret_version.secret_string は機密値であり、OpenTofu はそれが truefalse かを評価しようとしたときにクラッシュしました。(GitHub のスレッドを参照してください)。

このデバッグは、デバッガーで CLI を実行する (デバッグにdelveを使用するdebug-opentofu スクリプトを使用) ことで簡単に行えました。次に、コードとクラッシュスタックトレースを追跡して問題の原因を突き止めました。最後に、問題を修正する方法を把握するために、コードベース内の他の種類の条件でこの問題がどのように解決されたかを確認しました。

この問題は、go-cty ライブラリを使用した HCL での値の評価方法に起因していました。この評価がどのように機能するかについては詳しく説明しませんが、特定の値は追加情報 (値を機密としてマークするなど) で「マーク」することができ、マークされた値に対して特定の操作を行うことができないため、このパニックが発生します。

これは、コード作成者が (意図しない可能性のある) 暗黙的な動作に頼るのではなく、常にこれらのマークを明示的に処理するようにするための意図的なものです。

この場合、ブール値をチェックする前に値をマーク解除するだけで解決しました: resultVal, _ = resultVal.Unmark()。完全なコードはこちらをご覧ください。

興味深いことに、この問題は、従来のコードベースで、同様の動作をするprecondition ブロックや、variable のカスタム検証ルールなど、他の条件ですでに発生していました。

バグ 2: Null 出力参照

バグの説明: null 出力を参照すると、tofu test がクラッシュします。

前のバグと同様に、手動 QA シナリオを確認したところ、tofu test がクラッシュするシナリオが見つかりました。run 条件が null 値を持つ output を参照する場合です。

次の設定を実行することで、このクラッシュを再現できます (詳細についてはこちらをご覧ください)。

main.tftest.hcl

コードブロック
output "my_output" {
value = null
}


run "test_run" {
assert {
condition = output.my_output != "something"
error_message = "good"
}
}

この問題についてデバッグ作業を行ったところ、tofu が null 出力を実際の出力としてシリアル化していないことが原因であることがわかりました。例えば、状態では出力として表示されません。

しかし、tofu test 機能では、アサーションはこれらの null 出力をシリアル化されていないにもかかわらず、nil 値を持つものとして評価する必要がありました。

コードは次のとおりでした。

コードブロック
output := d.Evaluator.State.OutputValue(addr.Absolute(d.ModulePath))
val := output.Value
if val == cty.NilVal {
// Not evaluated yet?
val = cty.DynamicVal
}

if output.Sensitive {
val = val.Mark(marks.Sensitive)
}

return val, diags

OutputValue は、アドレスに出力が見つからなかった場合、出力値が null でシリアル化されていない場合を含め、nil を返します。したがって、修正のために、この場合に nil 値を返すようにしました。

コードブロック
if output == nil {
return cty.NilVal, diags
}

完全なコードはこちらをご覧ください。

その他のテスト機能の改善と提案

これらのクラッシュ修正以外にも、テストカバレッジが全体的に不足していることがわかりました(これは当然のことです。まだアルファ版でした)。テスト機能については、どのようなテストでもカバーされていないシナリオが多数あり、バグ修正にもテストカバレッジが欠けていることがよくありました。

その結果、テスト機能の統合テストスイートに多くの新しいテストケースを追加しました。既存のコードベースのほとんどがかなり古いことを考えると、広く使用されるオープンソースプロジェクトには、より高いテストカバレッジが不可欠だと感じました。これらのテストケースの詳細については、こちらをご覧ください。

また、機能の安定性を高めるための提案に関する Issue の作成も開始しました。たとえば、tofu test 実行の最後にリソースクリーンアップ中にエラーが発生すると、クラウドプロバイダーにリソースが残ってしまう可能性があります。このようなシナリオが発生した場合、OpenTofu は適切に削除されなかったリソースの名前(HCL 内)を一覧表示するだけです。

これはおそらく十分ではありません。クラウドプロバイダー内で追加情報なしにこれらのリソースを見つけることが非常に困難になる可能性があるからです。そのため、少なくともこれらのリソースの ID を出力して、クラウドプロバイダーでより簡単に見つけて削除できるようにすることを提案しました。

学んだこと

このブログでは、OpenTofu の新しいテスト機能の開発とトラブルシューティング中に学んだことについて説明しました。コードデバッグとブラックボックステストを使用して、OpenTofu のアルファリリース向けの機能の構築を行いました。

開発中の機能のコードを学習し、安定化させるというこの冒険を通じて、コードベースとデバッグ方法をより深く理解できました。さらに、コードを読んで実際に操作することで、機能の仕様を完全に構築する方法を学びました。

また、この規模のオープンソースプロジェクトにおけるテストの重要性(ドキュメントの作成と、機能が長期的に動作することを確認することの両方において)を改めて認識し、今後プロジェクトのカバレッジを増やしていくつもりです。

もちろん、これは投入された作業のほんの一例にすぎませんが、OpenTofu のために新しいものをどのように作成したかのアイデアを掴んでいただけると思います。これは、オープンソースの Terraform の過去の機能との同等性を維持するだけでなく、新しい機能にも対応し、単なる Terraform の代替として機能するだけでなく、独自の機能を開発します。

OpenTofu バージョン 1.6.alphaと、OpenTofu の起動とインストールに関するブログもご確認ください。