Terraformの新関数plantimestampの使い所を考える

2023.06.30

2023/06/12にGAになったTerraform のversion 1.5にて、新関数 plantimestampが追加されました。

どんな関数?

その名の通り、plan実行時のタイムスタンプを返す 関数です。

従来より timestampという関数がありました。これはapply実行時のタイムスタンプを返す関数です。これのplan版ですね。

細かな挙動

「plan実行時のタイムスタンプを返す」関数なので、もちろんterraform plan実行の度に新しい値が返されます。加えて、基本的にはterraform apply実行時にも新しい値が返されます。

これは何故かというと、terraform applyを実行するとまずterraform plan相当の処理、つまりTerraformのコードとリソースの現状の差分を確認する処理が走りますよね。このterraform plan相当の処理の部分でplantimestampの返り値が更新されるからです。

ただし、上記で「基本的には」と書いた通り、terraform applyでplantimestampの返り値が更新されない場合もあります。それはplan fileを指定した時です。

あまり知られていない気がしますが、terraform planコマンドには -outオプションがあり、plan結果をファイル(plan file)化することができます。そしてそのファイルをterraform apply (そのファイルのパス)というようにapplyコマンドで指定することで、terraform planを実行した際のplanを使ってapplyすることができます。この際、terraform applyコマンド内で前述のplan相当の処理=Terraformのコードとリソースの現状の差分を確認する処理は行われません。ですのでplantimestampの返り値は更新されないのです。代わりに前段のterraform planコマンドを実行した際のタイムスタンプが返ってきます。

terraform applyでplantimestamp返り値が更新される例

以下のようにplantimestamp関数返り値を使ったoutputを定義します。

output "plantimestamp" {
  value = plantimestamp()
}

terraform planを実行すると上記output値は2023-06-30T06:43:01Zになると出力されました。

% terraform plan
(省略)
Changes to Outputs:
  + plantimestamp = "2023-06-30T06:43:01Z"

terraform applyを実行すると、さらに新しい値2023-06-30T06:45:48Zになると出力されました。これはapply内でplan相当の処理をやっているからです。

% terraform apply
(省略)
Changes to Outputs:
  + plantimestamp = "2023-06-30T06:45:48Z"

このapplyを実行したところ、

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
(省略)

Outputs:

plantimestamp = "2023-06-30T06:45:48Z"

直前に出力された2023-06-30T06:45:48Zが使われたことがわかります。

plan fileを使った、apply時にplantimestamp返り値が更新されない例

前項と同様、plantimestamp関数返り値を使ったoutputを定義します。

output "plantimestamp" {
  value = plantimestamp()
}

今回は -outオプションを使ってplanfileを生成します。output値は2023-06-30T06:49:59Zになると出力されていますね。

% terraform plan -out=planfile 
(省略)
Changes to Outputs:
  ~ plantimestamp = "2023-06-30T06:45:48Z" -> "2023-06-30T06:49:59Z"

上記plan fileを使ってapplyします。今回はplan実行時に出力された2023-06-30T06:49:59Zがそのままoutput値に使われていることがわかります。(=apply時にplantimestamp返り値が更新されていない)

% terraform apply planfile
(省略)
Outputs:

plantimestamp = "2023-06-30T06:49:59Z"

使い所を考える

どういう関数かは理解できたのですが、どういう時に使うべき関数なのかがパッとわかりませんでした。なので少し調べて&考えてみました。

リソースやデータソースのattributeが期限切れになっていないかチェックする

まず公式ドキュメントに記載されていたのは、リソースやデータソースのattributeが期限切れになっていないか、v1.5の新機能check blockでチェックするというものです。TLS証明書期限が切れていないかチェックしています。

check "terraform_io_certificate" {
  data "tls_certificate" "terraform_io" {
    url = "https://www.terraform.io/"
  }

  assert {
    condition = timecmp(plantimestamp(), data.tls_certificate.terraform_io.certificates[0].not_after) < 0
    error_message = "terraform.io certificate has expired"
  }
}

なるほどこれは確かにアリな感じがしますね。ですが普段メインで扱っているAWS(AWS provider)でこのケースがハマる場合はあまりなさそうな気がします。。ACM証明書は基本自動更新で使うことが多いのでチェックが必要なケースが少なそうです。他に何かタイムスタンプがクリティカルになるリソースありますかね?

特定の日時以前/以降のみapplyすることを促す

以下のようなcheck blockを定義します。

locals {
  apply_after = "2023-07-01T00:00:00Z"
}
check "terraform_io_certificate" {
  assert {
    condition     = timecmp(plantimestamp(), local.apply_after) > 0
    error_message = "now is ${plantimestamp()}. run apply only after ${local.apply_after}"
  }
}

すると、2023/7/1(UTC)より前にplanやapplyを実行するとWarningが出力されます。

% terraform apply 
(省略)
│ Warning: Check block assertion failed
│ 
│   on check.tf line 10, in check "terraform_io_certificate":
│   10:     condition     = timecmp(plantimestamp(), local.apply_after) > 0
│     ├────────────────
│     │ local.apply_after is "2023-07-01T00:00:00Z"
│ 
│ now is 2023-06-30T08:20:05Z. run apply only after 2023-07-01T00:00:00Z

ただし、check blockでwarningが出ている状態でもapply(プロビジョニング)は実行可能です。apply禁止を強制できるわけではないということですね。

特定の日時以前/以降のみapplyすることを強制する

「促す」だけでなくapply禁止を強制したい場合は、いずれかのdata sourceに対してpreconditionを設定するのが良いでしょう。

locals {
  apply_after = "2023-07-01T00:00:00Z"
}
data "aws_caller_identity" "current" {
  lifecycle {
    precondition {
      condition     = timecmp(plantimestamp(), local.apply_after) > 0
      error_message = "now is ${plantimestamp()}. you can run apply only after ${local.apply_after}"
    }
  }
}
% terraform plan 
(省略)╷
│ Error: Resource precondition failed
│ 
│   on precondition.tf line 7, in data "aws_caller_identity" "current":
│    7:       condition     = timecmp(plantimestamp(), local.apply_after) > 0
│     ├────────────────
│     │ local.apply_after is "2023-07-01T00:00:00Z"
│ 
│ now is 2023-06-30T08:37:53Z. you can run apply only after 2023-07-01T00:00:00Z
╵

apply内のplan処理部分の処理時間を調べる

-auto-approveオプションを使う、かつ plan fileを使わないterraform applyコマンド実行時に限られます。

plantimestamp関数の返り値は、おおよそapply内のplan処理(Terraformのコードとリソースの現状の差分を確認する処理)部分の開始時点のタイムスタンプになります。

一方timestamp関数の返り値は、上記plan処理が終わってプロビジョニングが開始された時点のタイムスタンプになります。

両方の値をoutput化してその差分を求めれば、plan処理部分にかかったおおよその時間がわかります。

% TZ=UTC date && terraform apply -auto-approve
Fri Jun 30 09:15:46 UTC 2023 # plan開始直前の時刻
aws_s3_bucket.name: Refreshing state... [id=hoge20230630084702669700000001]

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:

  # time_sleep.wait_1_minute will be created # 作成に1分かかるリソース
  + resource "time_sleep" "wait_1_minute" {
      + create_duration = "1m"
      + id              = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  ~ plantimestamp = "2023-06-30T09:14:26Z" -> "2023-06-30T09:15:47Z" # 大体 前述のplan開始直前の時刻と一致 ≒ plan処理開始時点の時刻
  ~ timestamp     = "2023-06-30T09:14:28Z" -> (known after apply)
time_sleep.wait_1_minute: Creating...
time_sleep.wait_1_minute: Still creating... [10s elapsed]
time_sleep.wait_1_minute: Still creating... [20s elapsed]
time_sleep.wait_1_minute: Still creating... [30s elapsed]
time_sleep.wait_1_minute: Still creating... [40s elapsed]
time_sleep.wait_1_minute: Still creating... [50s elapsed]
time_sleep.wait_1_minute: Creation complete after 1m0s [id=2023-06-30T09:16:49Z] # idがリソース作成完了時刻

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

plantimestamp = "2023-06-30T09:15:47Z"
timestamp = "2023-06-30T09:15:49Z" # リソース作成完了時刻のちょうど1分前 ≒ プロビジョニングが開始された時点の時刻
# timestamp - plantimestamp = 2秒 これがplan処理部分の処理時間概算

…まあ、こんなこと知りたいケースなんてほとんどないと思いますが?

感想

あまり良いユースケースを考えられなかったというのが正直なところです。リソースのattribute値に使うのではなく、check blockやpreconditionといったバリデーション系の処理で使用して、apply処理を抑止する、何かをユーザーに通知する、みたいなところがメインの使い方なのかなと思います。何か良い使い方をご存知の方は是非教えていただきたいです!