terraform applyをより安全に実行するためにできること(precondition編)
はじめに
西藤です。
タイトルに「〜〜編」とある通りシリーズ続編記事です。
前回の記事はこちら:
terraform applyをより安全に実行するためにできること - デロイト トーマツ ウェブサービス株式会社(DWS)公式ブログ
前回は、terraformにおける標準のコマンドやAWSのIAMの仕組みなどを有効活用して、terraform applyの実行をいかにして安全なものにしていくかを紹介しました。
そこからまた新たな手法を検討しましたので、紹介します。
バージョン1.2.0の新機能
terraformのバージョン1.2.0で追加されたものに
- precondition
- postcondition
があります。
https://github.com/hashicorp/terraform/releases/tag/v1.2.0
これはterraformのresource, data sourceそして、moduleのoutputにおける前提条件などを定義し、条件が満たされなかった際にはエラーメッセージを発して、処理の進行をストップしてくれる仕組みです。
これにより、生成されるリソースに期待される状態の定義をやりやすくなりました。
今回はこの2つのうちのpreconditionについて掘り下げていきます。
例1:EC2インスタンスのアーキテクチャの指定
まずは、
https://developer.hashicorp.com/terraform/language/expressions/custom-conditions#preconditions-and-postconditions
にて紹介されている公式ドキュメントにある例
- EC2インスタンスを作成するときに”
x86_64
”のAMIが使われていることを前提条件としたい
というシナリオで考えてみます。
preconditionを使わない例
preconditionを使わないとすると
data "aws_ami" "example" {
owners = ["amazon"]
filter {
name = "image-id"
values = ["ami-abc123"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
resource "aws_instance" "example" {
instance_type = "t3.micro"
ami = data.aws_ami.example.id
}
という具合でしょうか。aws_ami.example
のdataブロックの中で「x86_64
のアーキテクチャのもの」というfilterを記述する方法で、やり方としては可能です。
この書き方をしているときに条件(「x86_64
であること」)が満たされず、terraformコマンドが失敗するとしたら以下のようなエラーメッセージになります。
$ terraform apply
data.aws_ami.example: Reading...
╷
│ Error: Your query returned no results. Please change your search criteria and try again.
│
│ with data.aws_ami.example,
│ on test.tf line 1, in data "aws_ami" "example":
│ 1: data "aws_ami" "example" {
│
╵
aws_ami.example
のdataブロックの中で「条件にあうものが見つからなかった」という表示に留まっています。
もちろんここからトラブルシュートしていけば良いですが、本来達成したい
- 「”example”のEC2インスタンスが
x86_64
のアーキテクチャで起動されること」
という前提条件からは離れた内容の情報になっていて、エラーになった理由を知るためには少し不親切です。
preconditionを使った例
次にpreconditionを使った例を考えます。
公式ドキュメントにある例を見ると
data "aws_ami" "example" {
owners = ["amazon"]
filter {
name = "image-id"
values = ["ami-abc123"]
}
}
resource "aws_instance" "example" {
instance_type = "t3.micro"
ami = data.aws_ami.example.id
lifecycle {
# The AMI ID must refer to an AMI that contains an operating system
# for the `x86_64` architecture.
precondition {
condition = data.aws_ami.example.architecture == "x86_64"
error_message = "The selected AMI must be for the x86_64 architecture."
}
(以下略)
}
という書き方ができます。
aws_instance.example
のresourceの中に「使用するAMIはx86_64
のアーキテクチャのものか」ということをprecondition
で記載しています。
実態としては冒頭のpreconditionを使わない例と同じですが、preconditionを使うことで
- このEC2インスタンス”example”は
x86_64
のアーキテクチャのマインイメージで作られる必要がある
という前提条件をaws_instance.example
のresourceの中で記載できるようになり、条件が満たされなかったときにはリソースの作成自体を防いでくれます。
関連するresource内に書かれることで、(プロジェクトを引き継ぐなどして)このコードをはじめて見た管理者はよりスムーズに「このリソースがどうあるべきと意図されていたか」を理解しやすくなります。
そして、precondition
ではerror_message
を定義できるので、terraformコマンドが失敗した時のトラブルシュートがより簡単になります。
条件が満たされず失敗した際には以下のようなエラーメッセージになります。
$ terraform apply
data.aws_ami.example: Reading...
data.aws_ami.example: Read complete after 0s [id=ami-0b6ffdb4868a0bcac]
╷
│ Error: Resource precondition failed
│
│ on test.tf line 17, in resource "aws_instance" "example":
│ 17: condition = data.aws_ami.example.architecture == "x86_64"
│ ├────────────────
│ │ data.aws_ami.example.architecture is "arm64"
│
│ The selected AMI must be for the x86_64 architecture.
╵
このようにerror_message
で記述したメッセージが表示されて、terraform処理が停止してくれるので、
- 「
x86_64
のアーキテクチャのAMIが選択されなかったからエラーになって、applyが中断されたのだ」
ということを直接的に伝えてくれる親切な表示になっております。
このようにpreconditionの仕組みを使うことで、期待される状態の記述がやりやすくなり、読みやすくなります。
そして、期待されない状態のリソースが作られてしまうことを防ぐガードレールとしても機能します。
例2:S3バケットのポリシー内容
さらに他のシナリオも考えてみます。
たとえば、
- S3バケットを構築し、そのバケットに置かれたファイルはユーザー・Aliceのみアクセス(
s3:GetObject
)できるようにする
という構成を目指すとします。
わざとエラーを混ぜた形で記述すると以下のようになります。
# 123456789012のアカウントからS3にアクセスできるようにするバケットポリシーの内容
data "aws_iam_policy_document" "example" {
version = "2012-10-17"
statement {
sid = "AllowGetObject"
effect = "Allow"
principals {
type = "AWS"
# わざと誤ったユーザー名を指定
identifiers = ["arn:aws:iam::123456789012:user/Bob"]
}
actions = ["s3:GetObject"]
resources = ["arn:aws:s3:::example-bucket-name", "arn:aws:s3:::example-bucket-name/*"]
}
}
# バケットポリシーを適用する対象となるS3バケットを作成
resource "aws_s3_bucket" "example" {
bucket = "example-bucket-name"
lifecycle {
precondition {
condition = can(regex("Alice", data.aws_iam_policy_document.example.json))
error_message = "適用するバケットポリシーには、\"Alice\"の記載がある必要があります"
}
}
}
# S3バケットにバケットポリシーを適用
resource "aws_s3_bucket_policy" "example" {
bucket = aws_s3_bucket.example.id
policy = data.aws_iam_policy_document.example.json
}
data.aws_iam_policy_document.example
の部分の中でexample-bucket-name
という名前のS3バケット内でのGetObject
アクションを許可するポリシー内容を記述しています。
シナリオに沿って実装するならば、principals
の中ではAlice
のArnを指定する必要がありますが、検証用にわざと誤った指定をしています。
そして、resource.aws_s3_bucket.example
にて、S3バケットを定義していますが、その中にpreconditionを記述しています。
ここでは
https://developer.hashicorp.com/terraform/language/expressions/custom-conditions#can-function
に記載されているようにcan
という仕組みで正規表現(regex
)と組み合わせることで
- 「”Alice”という文字列が含まれていること」
ということを条件においています。
(※厳密にシナリオに沿うのであれば、定義するべき条件は「”Alice”がGetObjectをできるようなポリシーになっているか」なのですが、複雑になりそうだったので、今回は文字列の有無の確認に留めています。)
この状態で、terraformコマンドを実行すると…
$ terraform apply
data.aws_iam_policy_document.example: Reading...
data.aws_iam_policy_document.example: Read complete after 0s [id=3825146340]
aws_s3_bucket.example: Refreshing state... [id=example-bucket-name-hello]
╷
│ Error: Resource precondition failed
│
│ on test.tf line 20, in resource "aws_s3_bucket" "example":
│ 20: condition = can(regex("Alice", data.aws_iam_policy_document.example.json))
│ ├────────────────
│ │ data.aws_iam_policy_document.example.json is "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Sid\": \"AllowGetObject\",\n \"Effect\": \"Allow\",\n \"Action\": \"s3:GetObject\",\n \"Resource\": [\n \"arn:aws:s3:::example-bucket-name/*\",\n \"arn:aws:s3:::example-bucket-name\"\n ],\n \"Principal\": {\n \"AWS\": \"arn:aws:iam::123456789012:user/Bob\"\n }\n }\n ]\n}"
│
│ 適用するバケットポリシーには、"Alice"の記載がある必要があります
のようになり、バケットポリシーが前提条件を満たしていなかったためエラーメッセージと共に中止されました。
これは
- 「バケットポリシーが前提条件を満たしていないうちにS3バケットのストレージが作られてしまわないようにする」
というガードレール的な動きをしてくれており、期待通りの動作です。
次に、Bob
と記載していた箇所を正しくAlice
と修正して実行するとどうなるかみてみます。
$ terraform apply
data.aws_iam_policy_document.example: Reading...
data.aws_iam_policy_document.example: Read complete after 0s [id=133686337]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_s3_bucket.example will be created
+ resource "aws_s3_bucket" "example" {
+ acceleration_status = (known after apply)
+ acl = (known after apply)
+ arn = (known after apply)
+ bucket = "example-bucket-name"
+ bucket_domain_name = (known after apply)
+ bucket_regional_domain_name = (known after apply)
+ force_destroy = false
+ hosted_zone_id = (known after apply)
+ id = (known after apply)
(略)
}
# aws_s3_bucket_policy.example will be created
+ resource "aws_s3_bucket_policy" "example" {
+ bucket = (known after apply)
+ id = (known after apply)
+ policy = jsonencode(
{
+ Statement = [
+ {
+ Action = "s3:GetObject"
+ Effect = "Allow"
+ Principal = {
+ AWS = "arn:aws:iam::123456789012:user/Alice"
}
+ Resource = [
+ "arn:aws:s3:::example-bucket-name/*",
+ "arn:aws:s3:::example-bucket-name",
]
+ Sid = "AllowGetObject"
},
]
+ Version = "2012-10-17"
}
)
}
Plan: 2 to add, 0 to change, 0 to destroy.
今度はエラーなく、applyの動作に入っており、リソースが作成できました。
まとめ
以上、preconditionの仕組みを使ったリソース作成時の前提条件の設定例を紹介しました。
この機能は比較的新しく、ノウハウがなかなか少ないですが、使いこなせばかなり便利になりそうです。
もともと、リソース同士の依存関係を検出して、順序立ててリソース作成してくれるterraformの仕組みなどは便利なものでした。しかし、それでもなかなかカバーできなかった個々のリソースの属性・内容に対して前提条件を作れるのはterraform applyの安全性を高める上で大きな利点になりそうです。
そして、コード内の記述で「このリソースがどうあるべきと意図されているか」のドキュメント化が進むので、チームでインフラ管理を行う際の効率アップも期待できます。
今回もTerraformでインフラ実装をしていく際の安全を高めるためのアイディアになれば幸いです。