Terraform version 1.5の新機能達を使ってみた
2023/06/12にTerraform のversion 1.5がGAになりました。(2023/07/31時点でv1.5.4まで出ています。)
だいぶ遅いですが、新機能達を使ってみたのでレポートします。
checkブロック
一言で言えば汎用的なvalidation機能です。
以下がcheckブロックの例です。
check "health_check" { data "http" "example" { url = "https://${aws_lb.example.dns_name}" } assert { condition = data.http.example.status_code == 200 error_message = "${data.http.example.url} returned an unhealthy status code" } }
- checkブロックは1個以上のassertブロックと、0-1個のdata sourceブロックから構成されます。assertブロックを複数個かけるということは複数個のテストができるということです。
data sourceについては後述します。 - assertブロックはconditionとerror_message attributesで構成され、conditionの結果がfalseの時にerror_message値が出力されます。
- conditionとerror_messageでは同module内の全要素(resource,data source,variable,local variable,output)を参照することができます。
- これまでもvalidation系の機能はありましたが、variableに対してのものだったり、data source、resourceに対してのものだったりと、特定の要素単体に対してのものでした。対してcheck ブロックは全要素を参照できます。そういう意味で「汎用的な」validation機能だと述べました。
- テストはplan完了後、apply完了後に実行されます。plan完了後にcheck blockでエラーが出たとしても、applyは可能です。またapply完了後にエラーが出たとしても、applyを取り消すことはできません。
つまり、この機能はエラーの出るapplyを抑止する、中断することはできません。 - checkブロックの中のdata sourceブロックはscoped data sourceブロックと呼ばれ、checkブロック内でしか使用することができません。が、checkブロック内のassertブロックで参照できます。そしてdataを参照しに行くのはcheckブロックの処理が実施されるタイミングつまりplan後とapply後です。
使い道としては主に以下いずれかかなと思います。
- 前述の例のようにapply後のe2eテストのようなもの。前述のcheckブロックの例は、同Terraformモジュール内でプロビジョニングしたALBのDNSにhttpsアクセスして、そのレスポンスステータスコードが200かどうかチェックするものです。こんな感じでTerraformでプロビジョニングしたリソース群が要件を満たしているか確認することができます。
- ユーザーに対しての警告。サービスクオータが上限に達しそうだよ、とかリソースが期限切れになっているよ(もしくはなりそうだよ)みたいな、プロビジョニングした(する)リソースについてユーザーが知っておいたほうがいい情報を提供する事ができます。
前述の通りこのcheckブロックでエラーになったとしてもapplyを抑止することはできませんので、そういった目的では使うべきではありません。apply抑止を目的にする場合は、1.2でGAになったprecondition / postcondition、これらはエラー時に後段の処理を止めることが出来ますので、こちらで頑張るべきでしょう。
importブロック
既存リソースをTerraformに取り込む新しい方法です。
これまで上記を実現しようとした際には、terraform import
コマンドを使う必要がありました。ですがこのコマンドで既存リソースをTerraformに取り込むことにはいくつか問題点があります。
- 1回の
terraform import
コマンドで1リソースしかインポートできない。たくさん対象リソースがある場合は大変です。 terraform import
は対象リソースをstateに取り込むだけです。リソースの状態に即したTerraformのresource
コードを書いてくれるわけではありません。また、プレビューはなく即時対象リソースをstateに取り込みます。そのため、コードが出来上がるまでの間に他のチームメンバーがterraform apply
してしまうとインポートしたリソースが意図しない削除や変更を受けてしまう危険性があります。- また、上記「リソースの状態に即したTerraformの
resource
コード」を書くのは時間がかかります。resourceのattribute値を修正してplan→修正してplan…をdiffが検出されなくなるまでやることになるでしょう。
新機能importブロックはこのあたりの問題を解決してくれます。以下がimportブロックの例です。
import { id = "terraform1-5importtest" to = aws_s3_bucket.test }
id
はterraform import
コマンドで指定するリソースの固有値ですね。そのidを持つ既存リソースをto
のアドレスにインポートする、というコードです。この例だとterraform1-5importtestという名前のS3バケットをaws_s3_bucket.test
にインポートする、というコードになります。
これをterraform plan
すると…
% terraform plan Planning failed. Terraform encountered an error while generating this plan. ╷ │ Error: Import block target does not exist │ │ on s3.tf line 1: │ 1: import { │ │ The target for the given import block does not exist. If you wish to automatically generate config for │ this resource, use the -generate-config-out option within terraform plan. Otherwise, make sure the │ target resource exists within your configuration. For example: │ │ terraform plan -generate-config-out=generated.tf ╵
エラーになりました。targetに指定したアドレスのコード(今回の例だとaws_s3_bucket.test
)は予め用意しておく必要があるようですね。そしてエラーメッセージに書かれているようにそのリソースのコードを自動生成してくれるオプションがあります!
% terraform plan -generate-config-out=generated_resources.tf aws_s3_bucket.test: Preparing import... [id=terraform1-5importtest] aws_s3_bucket.test: Refreshing state... [id=terraform1-5importtest] Terraform will perform the following actions: # aws_s3_bucket.test will be imported # (config will be generated) resource "aws_s3_bucket" "test" { arn = "arn:aws:s3:::terraform1-5importtest" bucket = "terraform1-5importtest" bucket_domain_name = "terraform1-5importtest.s3.amazonaws.com" bucket_regional_domain_name = "terraform1-5importtest.s3.ap-northeast-1.amazonaws.com" hosted_zone_id = "Z2M4EHUR26P7ZW" id = "terraform1-5importtest" object_lock_enabled = false region = "ap-northeast-1" request_payer = "BucketOwner" tags = {} tags_all = {} grant { id = "3a13d3890609a4deee6c8971f4cbfc5fe859d0d9e414f3bd88bc5889d2a97be8" permissions = [ "FULL_CONTROL", ] type = "CanonicalUser" } server_side_encryption_configuration { rule { bucket_key_enabled = true apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } versioning { enabled = false mfa_delete = false } } Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. ╷ │ Warning: Config generation is experimental │ │ Generating configuration during import is currently experimental, and the generated configuration │ format may change in future versions. ╵ ───────────────────────────────────────────────────────────────────────────────────────────────────────── Terraform has generated configuration and written it to generated_resources.tf. Please review the configuration and edit it as necessary before adding it to version control. Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
はい。generate-config-out
オプションを指定すると、import ブロックの内容に沿ってインポート対象のresourceのコードを自動生成してくれます!
# __generated__ by Terraform # Please review these resources and move them into your main configuration files. # __generated__ by Terraform from "terraform1-5importtest" resource "aws_s3_bucket" "test" { bucket = "terraform1-5importtest" bucket_prefix = null force_destroy = null object_lock_enabled = false tags = {} tags_all = {} }
このままapplyしてもOKです。私はいらないattributeを削除して、ファイルも移動させてからapplyしたいと思います。
import { id = "terraform1-5importtest" to = aws_s3_bucket.test } + + resource "aws_s3_bucket" "test" { + bucket = "terraform1-5importtest" + } # generated_resources.tfは削除しました
% terraform apply (略) Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes aws_s3_bucket.test: Importing... [id=terraform1-5importtest] aws_s3_bucket.test: Import complete [id=terraform1-5importtest] Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed.
これでインポート完了です!便利。
インポート後はimportブロックを削除しても構いません。残したままでも良いです。
importブロックにより、最初に挙げたterraform import
でのリソースインポートの問題を解決することができます。
1回の
terraform import
コマンドで1リソースしかインポートできない。たくさん対象リソースがある場合は大変。
→ importブロックは複数書けるので、一度のapplyでまとめてインポート可能です。
terraform import
は対象リソースをstateに取り込むだけです。リソースの状態に即したTerraformのresource
コードを書いてくれるわけではありません。また、プレビューはなく即時対象リソースをstateに取り込みます。そのため、コードが出来上がるまでの間に他のチームメンバーがterraform apply
してしまうとインポートしたリソースが意図しない削除や変更を受けてしまう危険性があります。
→ terraform plan
でプレビューができます。またインポート先のリソースのコードが無いとインポートできません。そしてstateに取り込まれるのはapply時です。以上のことから挙げたような意図しない変更や削除が発生してしまうリスクを大幅に減らせるでしょう。
また、上記「リソースの状態に即したTerraformの
resource
コード」を書くのは時間がかかります。resourceのattribute値を修正してplan→修正してplan…をdiffが検出されなくなるまでやることになるでしょう。
→ terraform plan
の新オプションgenerate-config-out
がコードを自動生成してくれます。もちろん汎用的なコードにするなどのためにいくつかの手直しが必要になることも多いと思いますが、ひとまず土台を作ってくれるので大幅に時間短縮できるでしょう。
細かな仕様
変数は使えない
importブロックのid
にvariableやlocal variableは使えません。エラーになります。
╷ │ Error: Variables not allowed │ │ on s3.tf line 2, in import: │ 2: id = var.bucket_name │ │ Variables may not be used here. ╵ ╷ │ Error: Unsuitable value type │ │ on s3.tf line 2, in import: │ 2: id = var.bucket_name │ │ Unsuitable value: value must be known
for_each, countも使えない
variableやlocal variableが使えない時点で予想できますが、for_eachやcountも使えません。
for_each
import { for_each = toset(["terraform1-5importtest", "terraform1-5importtest2"]) id = each.value to = aws_s3_bucket.test[each.value] }
% terraform apply ╷ │ Error: Unsupported argument │ │ on s3.tf line 2, in import: │ 2: for_each = toset(["terraform1-5importtest", "terraform1-5importtest2"]) │ │ An argument named "for_each" is not expected here. ╵ ╷ │ Error: Variables not allowed │ │ on s3.tf line 4, in import: │ 3: id = each.value │ │ Variables may not be used here. ╵ ╷ │ Error: Unsuitable value type │ │ on s3.tf line 4, in import: │ 3: id = each.value │ │ Unsuitable value: value must be known ╵ ╷ │ Error: Invalid expression │ │ on s3.tf line 5, in import: │ 4: to = aws_s3_bucket.test[each.value] │ │ A single static variable reference is required: only attribute access and indexing with constant keys. │ No calculations, function calls, template expressions, etc are allowed here.
count
import { count = 2 id = element(["terraform1-5importtest", "terraform1-5importtest2"], count.index) to = aws_s3_bucket.test[count.index] }
% terraform apply ╷ │ Error: Unsupported argument │ │ on s3.tf line 2, in import: │ 7: count = 2 │ │ An argument named "count" is not expected here. ╵ ╷ │ Error: Unsuitable value type │ │ on s3.tf line 4, in import: │ 9: id = element(["terraform1-5importtest", "terraform1-5importtest2"], count.index) │ │ Unsuitable value: value must be known ╵ ╷ │ Error: Function calls not allowed │ │ on s3.tf line 4, in import: │ 9: id = element(["terraform1-5importtest", "terraform1-5importtest2"], count.index) │ │ Functions may not be called here. ╵ ╷ │ Error: Invalid expression │ │ on s3.tf line 5, in import: │ 10: to = aws_s3_bucket.test[count.index] │ │ A single static variable reference is required: only attribute access and indexing with constant keys. │ No calculations, function calls, template expressions, etc are allowed here. ╵
providerを指定できる
provider
attiributeでaliasを指定できます。
provider "aws" { alias = "europe" region = "eu-west-1" } import { provider = aws.europe to = aws_instance.example["foo"] id = "i-abcd1234" }
sub module内にインポートしたい時は
これまでと同様root module内でimportブロックを書いて、to
のアドレスをsub module内にすれば良いです。
import { id = "terraform1-5importtest" to = module.hogehoge.aws_s3_bucket.test } module "hogehoge" { source = "./modules/hogehoge" }
resource "aws_s3_bucket" "test" { bucket = "terraform1-5importtest" }
逆にsub module内にimport blockを書くことは出来ません。
import { id = "terraform1-5importtest" to = aws_s3_bucket.test } resource "aws_s3_bucket" "test" { bucket = "terraform1-5importtest" }
% terraform apply ╷ │ Error: Invalid import configuration │ │ on modules/hogehoge/s3.tf line 1: │ 1: import { │ │ An import block was detected in "module.hogehoge". Import blocks are only allowed in the root module.
plantimestamp関数
こちらについては以下エントリにまとめています。
strcontains関数
第一引数の文字列の中に第二引数の文字列が含まれているかチェックし、含まれているならtrue、含まれていないならfalseを返す関数です。
これまでも既存の関数の組み合わせで同様のことはできましたが、strcontainsを使うことでよりシンプルに書けるようになります。正規表現は使えないので、正規表現が必要な場合は従来どおりのやや複雑な記法が必要です。
これまで
% terraform console > length(regexall("abcd", "abcdefg")) > 0 true > length(regexall("aaaa", "abcdefg")) > 0 false > can(regex("abcd", "abcdefg")) true > can(regex("aaaa", "abcdefg")) false > replace("abcdefg", "abcd", "") != "abcdefg" true > replace("abcdefg", "aaaa", "") != "abcdefg" false
これから
% terraform console > strcontains("abcdefg","abcd") true > strcontains("abcdefg","aaaa") false
補足: 前方一致、後方一致の場合
startswith関数とendswith関数があるのでこちらを使いましょう。
参考情報
- Release v1.5.0 · hashicorp/terraform · GitHub
- Release v1.5.1 · hashicorp/terraform · GitHub
- Terraform 1.5 brings config-driven import and checks
- New Terraform Cloud capabilities to import, view, and manage infrastructure
- Terraform v1.5.0 で追加された import ブロックと check ブロックを試してみた | DevelopersIO
- Checks - Configuration Language | Terraform | HashiCorp Developer
- Custom Conditions - Configuration Language | Terraform | HashiCorp Developer
- Feat: Add strcontains function and documentation by Cliftonz · Pull Request #33069 · hashicorp/terraform