Terraform 1.9  の新機能紹介

Terraform 1.9 の新機能紹介

変数のvalidationで色々参照できるようになり、templatestring関数が追加され、terraform_dataリソースへのリファクタリングがしやすくなり、removed blockでprovisionerが定義できるようになりました
Clock Icon2024.07.04

Terraformのversion 1.9が2024年の6月26日にGAになりました。1.9の新機能を見ていきましょう。

変数のvalidationで色々参照できるようになった

変数にはvalidationを実装することができます。例えば以下のようなものです。

variable "aws_account_id" {
  type        = string
  description = "AWS Account ID"
  validation {
    condition     = can(regex("^[0-9]{12}$", var.aws_account_id))
    error_message = "Invalid AWS accountID."
  }
}

これはAWSアカウントIDが格納されるのを想定した変数です。AWSアカウントIDは必ず12桁の数字ですので、そうでない場合はエラーにしています。

上記例では condition句にて変数自身(var.aws_account_id)を参照しています。実はこれまでは参照できる変数はこのようにvalidationされる変数自身のみでした。

1.9から他の要素も参照できるようになりました。

  • 他の変数
  • data source
  • local変数

他の変数を参照する例

例えば、リリースブログに記載されていた例ですと以下のようなコードが考えられます。

variable "create_cluster" {
  description = "Whether to create a new cluster."
  type        = bool
  default     = false
}

variable "cluster_endpoint" {
  description = "Endpoint of the existing cluster to use."
  type        = string
  default     = ""

  validation {
    condition     = var.create_cluster == false ? length(var.cluster_endpoint) > 0 : true
    error_message = "You must specify a value for cluster_endpoint if create_cluster is false."
  }
}

引用元: Terraform 1.9 enhances input variable validations

このコードはおそらくクラスター(EKSクラスター?)をこのモジュール内で新規作成するか、もしくは既存のクラスター(のエンドポイント)を参照するか選べるようになっているモジュールです。

cluster_endpoint変数は、新規作成する場合は指定不要ですが、既存クラスター参照の場合は必須にしたいですよね。そのような条件が condition = var.create_cluster == false ? length(var.cluster_endpoint) > 0 : true でうまく実現できています。

data sourceを参照する例

実在するインスタンスタイプのみ変数値として許可する、が実現できます。

data "aws_ec2_instance_types" "test" {}

variable "instance_type" {
  type        = string
  description = "instance type you want to use"

  validation {
    condition     = can(index(data.aws_ec2_instance_types.test.instance_types , var.instance_type))
    error_message = "Invalid instance type."
  }
}

リソースのattributesも参照できた

まあ使う機会はないと思いますが、以下のようなリソースのattributesを参照するコードもエラーなく実行できました。

resource "aws_s3_bucket" "example" {
  bucket_prefix = "my-tf-test-bucket"
}

variable "region" {
  type = string

  validation {
    condition     = var.region == aws_s3_bucket.example.region
    error_message = "This validation is meaningless"
  }
}

この場合、 aws_s3_bucket.exampleリソース作成後にvalidationが実行されました。

% terraform apply -var "region=hoge"

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" {
      (割愛)
    }

Plan: 1 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.example: Creating...
aws_s3_bucket.example: Creation complete after 1s [id=my-tf-test-bucket20240703055717715700000001]
╷
│ Error: Invalid value for variable
│ 
│   on variables.tf line 17:
│   17: variable "region" {
│     ├────────────────
│     │ aws_s3_bucket.example.region is "ap-northeast-1"
│     │ var.region is "hoge"
│ 
│ This validation is meaningless
│ 
│ This was checked by the validation rule at variables.tf:20,3-13.

参照しているaws_s3_bucket.exampleが作成済の2回目以降のapplyでは、planフェーズでvalidationが行われました。

% terraform apply -var "region=hoge"
aws_s3_bucket.example: Refreshing state... [id=my-tf-test-bucket20240703055717715700000001]

Planning failed. Terraform encountered an error while generating this plan.

╷
│ Error: Invalid value for variable
│ 
│   on variables.tf line 17:
│   17: variable "region" {
│     ├────────────────
│     │ aws_s3_bucket.example.region is "ap-northeast-1"
│     │ var.region is "hoge"
│ 
│ This validation is meaningless
│ 
│ This was checked by the validation rule at variables.tf:20,3-13.

templatestring関数

既存のtemplatefile関数の亜種みたいな関数です。

templatefileはローカルにあるファイルをベースに一部記述を動的に変更できる関数です。
対して今回追加されたtemplatestring関数は、ファイルを用意する必要なく、変数やresource attributeなどをベースに一部記述を動的に変更できます。

Terraform公式ブログで紹介されていたの例は、インターネット越しにテンプレートを取得してそれを加工する例です。

data "http" "manifest" {
  url = "https://git.democorp.example/repocontent/k8s-templates/ingress-template.yaml"
}

locals {
  manifest_final = templatestring(data.http.manifest.response_body, {
    APP_NAME       = var.app_name
    NAMESPACE      = kubernetes_namespace_v1.example.metadata.0.name
    SERVICE_NAME   = kubernetes_service_v1.example.metadata.0.name
    CONTAINER_PORT = var.container_port
  })
}

resource "kubernetes_manifest" "example" {
  manifest = local.manifest_final
}

引用元: Terraform 1.9 enhances input variable validations

他にも、テンプレートを外部ファイル化するまでもないなと思った場合に、ヒアドキュメント形式でローカル変数を用意して使う事も考えられますね。以下が例です。

以下ブログではtemplatefile関数を使ってIAM Policyを定義しています。

これをtemplatestring関数で書き換えると以下になります。

locals {
  policy_template = <<-EOT
  {
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": "dynamodb:*",
      "Resource": "arn:aws:dynamodb:$${region}:$${account_id}:table/$${table_name}"
    }]
  }
  EOT
}

resource "aws_iam_policy" "templatestring" {
  name = "templatestring"
  policy = templatestring(
    local.policy_template,
    {
      region     = data.aws_region.current.id,
      account_id = data.aws_caller_identity.current.account_id,
      table_name = aws_dynamodb_table.book.name
    }
  )
}

注意点としては、 local.policy_templateの中の変数は一旦$${hoge}$を2個書いてエスケープさせることです。これをしておかないとtemplatestring関数実行前の、local変数評価時に変数部分が変換されてしまうので。

余談ですが、同様のことはこれまでも replace関数で実現できましたね。少しだけtemplatestring関数の方が読みやすいですかね?

moved blockで null_resource → terraform_dataリソースの移動ができる

前提情報

  1. null providerのnull_resourceという、なんというか「かゆいところに手が届く感じ」のリソースがあります。AWS providerなどの各通常リソースだけでは実現が難しい要件があった場合に、特定条件下で任意のコマンドを実行する、みたいなことが実現できます。ただ、可読性が落ちる場合が多いので多用は厳禁、だと私は思っています。
  2. version 1.4にてTerraform本体にnull_resourceと同等のterraform_dataリソースが追加されました。これにより、既存のnull_resourceterraform_dataリソースに置き換えていくことが推奨されました。
  3. version 1.8にて、異なるリソース間でmoved blockを使ってリソース移動ができるようになりました。ただしリソース移動できるのは、各プロバイダーがサポートしているリソース間のみです。

本題

本version 1.9より、moved blockで null_resourceterraform_dataリソースの移動ができるようになりました。

以下は、TypeScriptのLambda関数をTerraformだけでデプロイする | DevelopersIOで使用したnull_resourceをmoved blockを使ってterraform_dataリソースに移動させた例です。以下だけで簡単に移動できました!

  • moved blockを書く
  • resourceタイプを null_resource → terraform_dataに変更
  • triggers argumentをtriggers_replaceに変更
  • (他のリソースにてnull_resource.lambda_builddepends_onで参照していたので、それをterraform_data.lambda_buildに変更)
+ moved {
+   from = null_resource.lambda_build
+   to   = terraform_data.lambda_build
+ }
+ 
+ resource "terraform_data" "lambda_build" {
- resource "null_resource" "lambda_build" {
    depends_on = [aws_s3_bucket.lambda_assets]

+   triggers_replace = {
-   triggers = {
      code_diff = join("", [
        for file in fileset(local.helloworld_function_dir_local_path, "{*.ts, package*.json}")
        : filebase64("${local.helloworld_function_dir_local_path}/${file}")
      ])
    }

    provisioner "local-exec" {
      command = "cd ${local.helloworld_function_dir_local_path} && npm install"
    }
    provisioner "local-exec" {
      command = "cd ${local.helloworld_function_dir_local_path} && npm run build"
    }
    provisioner "local-exec" {
      command = "aws s3 cp ${local.helloworld_function_package_local_path} s3://${aws_s3_bucket.lambda_assets.bucket}/${local.helloworld_function_package_s3_key}"
    }
    provisioner "local-exec" {
      command = "openssl dgst -sha256 -binary ${local.helloworld_function_package_local_path} | openssl enc -base64 | tr -d \"\n\" > ${local.helloworld_function_package_base64sha256_local_path}"
    }
    provisioner "local-exec" {
      command = "aws s3 cp ${local.helloworld_function_package_base64sha256_local_path} s3://${aws_s3_bucket.lambda_assets.bucket}/${local.helloworld_function_package_base64sha256_s3_key} --content-type \"text/plain\""
    }
  }
plan結果抜粋
Terraform will perform the following actions:

  # null_resource.lambda_build has moved to terraform_data.lambda_build
    resource "terraform_data" "lambda_build" {
        id               = "2045579073458313029"
        # (1 unchanged attribute hidden)
    }

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

これは個人的にはとても嬉しいですねー。というのもまだnull_resourceterraform_dataリソース移行をやっていない箇所があったので。本機能を使ってスマートに移行させていきたいと思います!

removed blockでprovisionerを定義できる

removed blockはversion 1.7で追加された機能です。削除されたリソースの情報をコード内に残しておくことができます。

lifecycleブロックのdestroy値を falseにした場合、リソースはTerrafrom管理外になるだけで削除はされません。

今回の新機能は、destroy値をtrueにした場合に、provisionerを定義することができるようになった、というものです。これによってリソース削除時に任意の処理を実行できます。以下は削除するEC2インスタンスのidを出力するprovisionerの例です。

removed {
  from = aws_instance.example

  lifecycle {
    destroy = true
  }

  provisioner "local-exec" {
    when    = destroy
    command = "echo 'Instance ${self.id} has been destroyed.'"
  }
}

引用元: Removing Resources | Resources - Configuration Language | Terraform | HashiCorp Developer

apply実行例
% terraform apply
data.aws_ami.ubuntu: Reading...
aws_instance.example: Refreshing state... [id=i-04ed96fbb7922abef]
data.aws_ami.ubuntu: Read complete after 0s [id=ami-0162fe8bfebb6ea16]

Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_instance.example will be destroyed
  # (because aws_instance.example is not in configuration)
  - resource "aws_instance" "example" {
      (割愛)
    }

Plan: 0 to add, 0 to change, 1 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_instance.example: Destroying... [id=i-04ed96fbb7922abef]
aws_instance.example: Provisioning with 'local-exec'...
aws_instance.example (local-exec): Executing: ["/bin/sh" "-c" "echo 'Instance i-04ed96fbb7922abef has been destroyed.'"]
aws_instance.example (local-exec): Instance i-04ed96fbb7922abef has been destroyed.
aws_instance.example: Still destroying... [id=i-04ed96fbb7922abef, 10s elapsed]
aws_instance.example: Still destroying... [id=i-04ed96fbb7922abef, 20s elapsed]
aws_instance.example: Still destroying... [id=i-04ed96fbb7922abef, 30s elapsed]
aws_instance.example: Destruction complete after 31s

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

上記の実行例を見ていただくとわかりますが、provisionerが実行されるのはdestroy処理の前ですので、その点ご注意ください。つまり、リソース削除の前処理には使えるけど、後処理には使えないですね。

リソースブロックにdestroy時のprovisionerを書くケースとの比較

1.9以前でも以下のように、リソース内にdestroy時のprovisionerを定義することはできました。

resource "aws_instance" "example" {
  instance_type = "t3.nano"
  ami = data.aws_ami.ubuntu.id

  provisioner "local-exec" {
    when    = destroy
    command = "echo 'Instance ${self.id} has been destroyed.'"
  }
}

この書き方と、新機能の「removed blockでprovisionerを定義する」の違いは何でしょう?

1. 発動条件が異なる

「removed blockでprovisionerを定義する」の場合、該当リソース(今回の例だとaws_instance.example)のコードを削除した後のterraform applyでprovisionerが実行されます。

ところが「リソース内にdestroy時のprovisionerを定義する」の場合、このケースだとprovisionerは実行されません。provisionerもリソースコードの中に含まれているので、一緒に削除された、ということになってしまいます。

というわけで、「リソース内にdestroy時のprovisionerを定義する」の場合、以下の様にしてコードの削除とリソースの削除のタイミングをズラす必要があります。

  1. count = 0をリソースブロックに追加する
  2. terraform apply実行 → リソースは削除され、削除前にprovisionerが実行される
  3. リソースコード全体を削除(当然その中のprovisionerのコードも削除される)
  4. terraform apply実行

ちょっとトリッキーというか、わかりにくいですよね。

2. provisionerの情報がコードに残る

「removed blockでprovisionerを定義する」の場合、リソース削除後もremoved blockは残る(残していても良い)ので、「以前こういう処理が実行されたんだな」ということが後からでも容易に把握できます。

一方「リソース内にdestroy時のprovisionerを定義する」ですと該当のprovisionerのコードはリソースコードの一部なので削除されているはずです。VCSで履歴を辿るなどしないと過去に実行されたprovisionerの内容はわからないでしょう。まあ、前述の「コードの削除とリソースの削除のタイミングをズラす4ステップ」の3と4をやらなければprovisioner コードを残しておくことはできますが、その場合 count = 0がついたリソースコードが残っていることになるので、それはそれでわかりにくいコードだと思います。

これら2点の観点から、今後は基本的には「removed blockでprovisionerを定義する」の方を採用するのが良いのではないかと思います。

参考情報

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.