[アップデート]TerraformのProviderが関数を定義できるようになりました
2024/4/10にGAになったTerraformのVersion1.8にて、Providerが関数を定義できるようになりました。各Providerの開発者はそのProvider固有の問題解決に特化した関数を作成できるようになりました。
本エントリでは新関数たちを触ってみてレポートします。
Providerって?
Terraformをあまりご存じない方向けに説明すると、ProviderはTerraformのプラグインです。実はTerraform単体ではAWSのリソースなどをプロビジョニングすることはできません。AWSリソースをプロビジョニングしたい場合はAWS provider、Google Cloudのリソースをプロビジョニングしたい場合はGoogle Cloud providerなどといったように、対応するproviderと組み合わせてTerraformを使うことではじめてリソースをプロビジョニングできます。
注意点
当然ですがTerraform本体のversionを1.8.0以上にアップグレードする必要があります。加えて各providerのversionも使いたい関数がリリースされた以降のversionにアップグレードする必要があります。
AWS
arn_build
ARNを作成する際に役立つ関数です。引数でARNの構成要素を指定していきます。
# result: arn:aws:iam::444455556666:role/example output "example" { value = provider::aws::arn_build("aws", "iam", "", "444455556666", "role/example") }
これまでもjoin()
を使って以下のように同様のことは可能でした。
# result: arn:aws:iam::444455556666:role/example output "example" { value = join( ":", ["arn", "aws", "iam", "", "444455556666", "role/example"] ) }
ですがarn_build()
を使うほうが意味が伝わりやすいコードになるので良いですね! 個人的にも、CDKで同様の関数があってTerraformにも欲しいなーと思っていたので嬉しいです。
arn_parse
arn_build()
の逆です。ARNの各部品を取得する関数です。
以下の例ではECRリポジトリのARNからアカウントIDを取得しています。
# create an ECR repository resource "aws_ecr_repository" "hashicups" { name = "hashicups" image_scanning_configuration { scan_on_push = true } } # output the account ID of the ECR repository output "hashicups_ecr_repository_account_id" { value = provider::aws::arn_parse(aws_ecr_repository.hashicups.arn).account_id }
これは、現時点では私は使い所が思い浮かびませんでした。上記例の様にアカウントIDを取得するときは、私は特定のリソースに紐づけずにaws_caller_identity data sourceを使うほうが好きです。アカウントID以外の他の部品を使いたくなるケースも…思い浮かびませんでした。
aws_arn data sourceとの使い分けは?
ARNをパースすることを目的としたdata sourceが既に存在します。aws_arn
です。こちらとarn_parse()
、どう使い分ければよいでしょうか?
結論としては、あまり差は無いです。好きな方を使えば良いと思います。ですが細かな差異を3点以下に記載します。
arn_parse()の方が簡潔に書ける
まず、arn_parse()
を使うほうがより簡潔に書くことができます。aws_arnはdata sourceを定義する必要があるぶん、arn_parse()
より記述量が増えます。
先程のECRリポジトリのARNからアカウントIDを取得する例をaws_arn data sourceを使って書き直すと以下のようになります。
# create an ECR repository resource "aws_ecr_repository" "hashicups" { name = "hashicups" image_scanning_configuration { scan_on_push = true } } + data "aws_arn" "hashicups" { + arn = aws_ecr_repository.hashicups.arn + } # output the account ID of the ECR repository output "hashicups_ecr_repository_account_id" { + value = data.aws_arn.hashicups.account - value = provider::aws::arn_parse(aws_ecr_repository.hashicups.arn).account_id }
arn_parse()の方が先に実行される(らしい)
次に、 Pull Request では以下のように述べられていました。
The arn_parse function provides similar utility to the aws_arn data source, but with the benefit of running earlier in the execution order.
これを検証するために、以下のコードを書いてapplyしてみました。time_staticリソースを利用して、arn_parse()
/ aws_arn
data sourceそれぞれの処理完了時間を記録して比較しようというものです。
# create an ECR repository resource "aws_ecr_repository" "hashicups" { name = "hashicups" image_scanning_configuration { scan_on_push = true } } data "aws_arn" "hashicups" { arn = aws_ecr_repository.hashicups.arn } resource "time_static" "arn_parse" { triggers = { arn_parse = provider::aws::arn_parse(aws_ecr_repository.hashicups.arn).account_id } } resource "time_static" "aws_arn" { triggers = { arn_parse = data.aws_arn.hashicups.account } } output "arn_parse" { value = resource.time_static.arn_parse.rfc3339 } output "aws_arn" { value = resource.time_static.aws_arn.rfc3339 }
実行結果は以下で、どちらも同時刻でした。この方法では「running earlier in the execution order」を確かめられませんでした…
aws_ecr_repository.hashicups: Creating... aws_ecr_repository.hashicups: Creation complete after 0s [id=hashicups] data.aws_arn.hashicups: Reading... data.aws_arn.hashicups: Read complete after 0s [id=arn:aws:ecr:ap-northeast-1:444455556666:repository/hashicups] time_static.arn_parse: Creating... time_static.aws_arn: Creating... time_static.arn_parse: Creation complete after 0s [id=2024-04-16T06:22:33Z] time_static.aws_arn: Creation complete after 0s [id=2024-04-16T06:22:33Z] Apply complete! Resources: 3 added, 0 changed, 0 destroyed. Outputs: arn_parse = "2024-04-16T06:22:33Z" aws_arn = "2024-04-16T06:22:33Z"
arn_parse()はstateに残らない / aws_arnは残る
aws_arn
はdata sourceなので、その結果がstateファイルに記録されます。 arn_parse()
は関数なので記録されません。
terraform state list
をするとaws_arn
は一覧に出てきますね。terraform state show
で中身を確認することも出来ます。
個人的にはterraform state list
の結果が長くなるのが嫌なのと、別にterraform state show
することもないと思うので、この点でいうとarn_parse()
を使いたいですかね。
trim_iam_role_path
名前の通り IAM RoleのARNからパスを除去した結果を返します。v5.44.0で追加されました。
# result: arn:aws:iam::444455556666:role/example output "example" { value = provider::aws::trim_iam_role_path("arn:aws:iam::444455556666:role/with/path/example") }
パスを除去したい状況をあまり思いつきませんでした。唯一過去にあったのは、EKSのConfigMapにてIAM Roleに権限付与しようとした際のARN指定時です。詳細は以下をご覧ください。
以下は当関数の詳細ページのリンクです。
Google Cloud
私がGoogle Cloudに詳しくないので概要説明に留めます。idから各種情報を取得できる便利関数群などが追加されているようです。
Kubernetes
manifest_decode
KubernetesのマニフェストファイルのフォーマットはYAMLですが、それをTerraformのobjectに変換してくれる関数です。
基本的に既存の yamldecode()
と同じ挙動のようです。
output "manifest_decode" { value = provider::kubernetes::manifest_decode(file("manifest.yaml")) } output "yamldecode" { value = yamldecode(file("manifest.yaml")) }
--- kind: Namespace apiVersion: v1 metadata: name: hoge labels: name: hoge
manifest_decode = { "apiVersion" = "v1" "kind" = "Namespace" "metadata" = { "labels" = { "name" = "hoge" } "name" = "hoge" } } yamldecode = { "apiVersion" = "v1" "kind" = "Namespace" "metadata" = { "labels" = { "name" = "hoge" } "name" = "hoge" } }
ただし、manifest_decode()
の方はマニフェストファイルとしての書式もチェックしてくれます。
例えば以下のように、必須項目kind
でタイポしたとします。
--- + kin: Namespace - kind: Namespace apiVersion: v1 metadata: name: hoge labels: name: hoge
すると以下のようにmissing field "kind"
エラーを吐いてくれました!これは嬉しいですね!
│ Error: Error in function call │ │ on k8s_functions.tf line 2, in output "manifest_decode": │ 2: value = provider::kubernetes::manifest_decode(file("manifest.yaml")) │ ├──────────────── │ │ while calling provider::kubernetes::manifest_decode(manifest) │ │ Call to function "provider::kubernetes::manifest_decode" failed: Invalid Kubernetes manifest: missing field "kind".
manifest_decode_multi
マニフェストファイルが複数のリソースを含んでいる場合はこちらを使います。
--- kind: Namespace apiVersion: v1 metadata: name: test-1 labels: name: test-1 --- kind: Namespace apiVersion: v1 metadata: name: test-2 labels: name: test-2
resource "kubernetes_manifest" "example" { for_each = { for m in provider::kubernetes::manifest_decode_multi(file("${path.module}/manifest.yaml"))): m.metadata.name => m } manifest = each.value }
ちなみに、上記複数リソースを含んでいるマニフェストファイルをmanifest_decode()
の引数にすると以下のようにdecode_manifest_multi()
を使ってねと教えてくれます。
│ Error: Error in function call │ │ on k8s_functions.tf line 2, in output "manifest_decode": │ 2: value = provider::kubernetes::manifest_decode(file("manifest.yaml")) │ ├──────────────── │ │ while calling provider::kubernetes::manifest_decode(manifest) │ │ Call to function "provider::kubernetes::manifest_decode" failed: YAML manifest contains multiple resources: use decode_manifest_multi to decode │ manifests containing more than one resource.
manifest_encode
manifest_decode()
の反対、TerraformオブジェクトをマニフェストファイルのYAML形式に変換してくれます。
locals { manifest = { apiVersion = "v1" kind = "ConfigMap" metadata = { name = "example" } data = { EXAMPLE = "example" } } } output "example_output" { value = provider::kubernetes::manifest_encode(local.manifest) }
apiVersion: v1 data: EXAMPLE: example kind: ConfigMap metadata: name: example
こちらもmanifest_decode()
と同様、マニフェストファイルとしての書式チェックをしてくれます。
│ Error: Error in function call │ │ on k8s_functions.tf line 23, in output "example_output": │ 23: value = provider::kubernetes::manifest_encode(local.manifest) │ ├──────────────── │ │ while calling provider::kubernetes::manifest_encode(manifest) │ │ local.manifest is object with 4 attributes │ │ Call to function "provider::kubernetes::manifest_encode" failed: Invalid Kubernetes manifest: missing field "kind".
time
rfc3339_parse
前述のarn_parseのタイムスタンプ版のような感じです。RFC3339形式のタイムスタンプを引数として渡すと、year
,day
,month
などの各種時間単位のattributeを持つオブジェクトを返します。
output "rfc3339_parse" { value = provider::time::rfc3339_parse("2024-04-16T12:34:56Z") }
rfc3339_parse = { "day" = 16 "hour" = 12 "iso_week" = 16 "iso_year" = 2024 "minute" = 34 "month" = 4 "month_name" = "April" "second" = 56 "unix" = 1713270896 "weekday" = 2 "weekday_name" = "Tuesday" "year" = 2024 "year_day" = 107 }
local
direxists
fileexists()
のディレクトリ版です。引数に指定したディレクトリが存在していればtrue、なければfalseを返す関数です。
output "direxists_output" { value = provider::local::direxists("${path.module}/hoge-directory") }
direxists_output = false
fileexists()
はTerraform本体のビルトイン関数なのに、こちらはlocal providerの関数です。Terraform本体のビルトイン関数にしてしまえばよかったのになんでなんでしょうね?
ちなみに存在するファイルのパスを引数に指定するとエラーになります。
│ Error: Invalid function argument │ │ on localprovider.tf line 6, in output "direxists_error": │ 6: value = provider::local::direxists("${path.module}/outputs.tf") │ ├──────────────── │ │ while calling provider::local::direxists(path) │ │ path.module is "." │ │ Invalid value for "path" parameter: Invalid file mode detected: "./outputs.tf" was found, but │ is not a directory.
存在しないファイルのパスならエラーにならず、ただfalseになるだけです。
まとめ
TerraformのVersion1.8の新機能、providerの関数たちをご紹介しました。既存の(Terraform本体の)関数で同等のことができる場合も結構ありましたが、providerの関数を使ったほうがより簡潔に書けたり、意図が伝わるコードになりやすかったりするので、積極的に使っていくのが良いのではないかと思います。