[アップデート]TerraformのProviderが関数を定義できるようになりました

2024.04.17

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)
}

example_outputの値

apiVersion: v1
data:
  EXAMPLE: example
kind: ConfigMap
metadata:
  name: example

こちらもmanifest_decode()と同様、マニフェストファイルとしての書式チェックをしてくれます。

必須項目kindが無かった場合のエラーメッセージ

│ 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の関数を使ったほうがより簡潔に書けたり、意図が伝わるコードになりやすかったりするので、積極的に使っていくのが良いのではないかと思います。

参考情報