相互排他引数の煩わしさ
先週、クライアントのレガシーなS3バケットをリファクタリングしていた際に壁にぶつかりました。シンプルな既定のACL(canned ACLs)から、より詳細な所有権制御ポリシーへと移行しようとしていた時のことです。新しい設定ブロックを追加してterraform planを実行したところ、ターミナルに次のような苛立たしいエラーが表示されました。
Error: Invalid combination of arguments
An argument named "acl" cannot be specified when "access_control_policy" is specified.
Terraformプロバイダーは厳格なスキーマを強制します。2つの属性が**相互に排他的(mutually exclusive)**であるとマークされている場合、どちらか一方を選択しなければなりません。両方を同時に使用することはできません。これは単なるTerraformの癖ではなく、通常、1つのリクエストで相反する指示を処理できない基盤となるCloud APIの仕様を反映しています。
競合箇所の特定
最初のステップは、どの2つの引数が衝突しているかを正確に特定することです。私のAWSの例では、競合はaws_s3_bucket_aclリソース内で発生していました。プロバイダーは、単純なacl文字列と複雑なaccess_control_policyブロックを、同じ目的を果たすための2つの異なる方法として認識します。
問題のあるコードは以下の通りです。
resource "aws_s3_bucket_acl" "example" {
bucket = aws_s3_bucket.my_bucket.id
# レガシーな属性
acl = "private"
# 新しい詳細なポリシー
access_control_policy {
grant {
grantee {
type = "Group"
uri = "http://acs.amazonaws.com/groups/global/AllUsers"
}
permission = "READ"
}
owner {
id = data.aws_canonical_user_id.current.id
}
}
}
プロバイダーがユーザーの意図を推測することはありません。たとえロジックが重複していなくても、スキーマでは権限を定義するためのトップレベル引数は1つしか許可されていません。両方を残したままにすると、プランは毎回失敗します。
明確化のためのリファクタリング
これを修正するには、「どちらの属性を信頼できる情報源(Source of Truth)とするか」という厳しい選択が必要です。より複雑な構成にアップグレードする場合は、単純な方の属性を単に空にするのではなく、完全に削除する必要があります。
ステップ1:ロジックの選択
現在の要件にどちらの属性が適しているかを決定します。きめ細かな制御が必要な場合は、複雑なブロックを保持します。迅速なセットアップが必要なだけなら、シンプルな文字列を使用します。最近のAWSデプロイの多くは、より優れたセキュリティ監査のためにaccess_control_policyへと移行しています。
ステップ2:リソースのクリーンアップ
こちらが修正版です。aclの行が消え、曖昧さがなくなっていることに注目してください。
resource "aws_s3_bucket_acl" "example" {
bucket = aws_s3_bucket.my_bucket.id
access_control_policy {
grant {
grantee {
type = "CanonicalUser"
id = data.aws_canonical_user_id.current.id
}
permission = "FULL_CONTROL"
}
owner {
id = data.aws_canonical_user_id.current.id
}
}
}
dynamicブロックと条件分岐の処理
このエラーは、機能を切り替えるためにdynamicブロックや変数を使用している際によく発生します。ロジックが厳密でないと、誤って両方の引数を有効にしてしまう可能性があります。これを防ぐには、null値を使用して、使用しない引数を明示的に無効にします。
resource "some_resource" "example" {
# var.use_simpleがtrueの場合はattribute_xを設定し、そうでない場合はnullに設定する
attribute_x = var.use_simple ? var.x_value : null
dynamic "attribute_y" {
# var.use_simpleがfalseの場合のみブロックを作成する
for_each = var.use_simple ? [] : [1]
content {
# ここに複雑な設定を記述
}
}
}
より良いワークフローの習慣
私は標準的なgitのpre-commitフックとしてterraform validateを実行するようにしています。これには3秒もかからず、ローカルでこれらのスキーマ違反をキャッチできます。これにより、構文の競合を見つけるためだけにフルプランのサイクルを待つ手間が省けます。
複雑なJSONポリシーを作成する際は、まずYAML ↔ JSON Converterで階層を視覚化するようにしています。YAMLの方が構造の重複を見つけるのがずっと簡単だからです。ロジックが固まったら、jsonencode()関数で使用するためにJSONに変換します。このワークフローにより、HCLファイルがクリーンに保たれ、エラーの原因となる「引数のスープ」状態を防ぐことができます。
修正を確認する方法
競合するコードを取り除いたら、以下の3つのステップで作業を確認します。
- バリデーションの実行:
terraform validateを実行します。エラーが消えていれば、構文はプロバイダーのスキーマと一致しています。 - プランの確認:
terraform planを実行します。「forces replacement(強制置換)」の警告がないか確認してください。相互排他的な引数を切り替えると、リソースの再作成がトリガーされることがあります。 - 変更の適用:
terraform applyを実行します。Cloud APIがペイロードを受け入れれば、完了です。
まとめ
- ドキュメントを確認する: ほとんどのプロバイダーのドキュメントには「Note」セクションがあります。そこには「引数Xは引数Yと相互に排他的です」と明記されています。
- 明示的なnullの使用: 変数を使用する場合、引数がプロバイダーによって完全に無視されるように
nullを使用してください。 - リプレースに注意: これらの引数の入れ替えは、破壊的なアクションになる可能性があります。Terraformがリソースを削除して再作成しようとしていないか、実行前に必ず確認してください。

