Terraform 1.4がGAになりました

2023.03.14

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

2023/03/09にTerraformのversion 1.4がGAになりました。主要アップデートを実際使ってみたのでレポートします。

Terraform CloudのCLI-driven runの場合でもWeb UIでPlan結果が見やすく見れるようになった

まずTerraform Cloudについておさらいです。Terraform Cloudには3種類のワークフロー(≒使い方)が存在します。

  • UI/VCS-driven workflows … GitなどのVCSと連携させてコミットがマージされる度にTerraformコマンドが実行されるもの
  • CLI-driven workflows … OSS版と同様terraform xxxコマンドを実行して使うもの
  • API-driven workflows … Terraform CloudのAPIを直接叩いて使うもの

このアップデートはCLI-drivenに関するものです。CLI-driven以外の方法でTerraformを実行した場合、Web UIの該当の実行(Run)ページにて、structured run outputと言われる見やすい表記でdiffを確認することができました。リソースをフィルターしたり、リソース毎にdiff詳細を表示・非表示したりといったことができます。

今回の1.4から、CLI-drivenで実行した場合でもWeb UIの該当実行ページにてstructured run outputでdiffを確認できるようになりました。1.3系最新バージョン1.3.9と1.4.0で表示の違いを確認しました。

before: v1.3.9

CLIコンソールと同じ表示ですね。 1-3console

after: v1.4.0

こちらがstructured run outputと言われる表記です。各リソースにある「+」をクリックすることで、そのリソースのdiff詳細を開閉できます。またフィルターもありますね。 1-4console

CLIでもOPAの結果が確認可能になった

OPA(Open Policy Agent)とは

汎用的なポリシーエンジンです。要は様々なツール・サービスに対して、同じ方法、同じ言語を使ってポリシー(セキュリティのルール、基準、条件)を満たしているかチェックするためのもの、です。Terraform以外にもKubernetes、Envoy、Kafka、SQLなどのチェックができるようです。

先日ネイティブサポートがGAしていた

2023/01/31に、Terraform CloudのこのOPAのネイティブサポートがGAになっていました。Terraform Cloud には以前からHashiCorp Sentinelによるポリシーチェックがサポートされていましたが、すでにOPAを使っている組織の要望を受けてこのOPAネイティブサポートの対応を行なったそうです。

※ なお、FreeプランはOPAもSentinelも未対応です。Team & Governanceプラン以上にアップグレードしましょう。(参考: Features欄のPolicy & securityをクリック)

そしてこの度、CLI-driven workflowを採用した場合のコンソールでも、OPAのチェック結果が確認できるようになりました。v1.3.9とv1.4.0での実行結果の違いを見てみましょう。

before: v1.3.9で実行

% terraform apply
(plan結果…)
  # module.network.module.vpc.aws_vpc.this[0] will be created
  + resource "aws_vpc" "this" {
      + arn                                  = (known after apply)
      + assign_generated_ipv6_cidr_block     = false
      + cidr_block                           = "10.0.0.0/16"
      + default_network_acl_id               = (known after apply)
      + default_route_table_id               = (known after apply)
      + default_security_group_id            = (known after apply)
      + dhcp_options_id                      = (known after apply)
      + enable_classiclink                   = (known after apply)
      + enable_classiclink_dns_support       = (known after apply)
      + enable_dns_hostnames                 = true
      + enable_dns_support                   = true
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = ""
        }
      + tags_all                             = {
          + "Name" = (known after apply)
        }
    }

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


Post-plan Tasks:

All tasks completed! 0 passed, 0 failed

│ 
│ Overall Result: Passed

------------------------------------------------------------------------


------------------------------------------------------------------------

Cost Estimation:

Waiting for cost estimate to complete...

╷
│ Error: Unknown or unexpected cost estimate state: unreachable
│ 
│ 
╵

cost estimationでエラーになったみたいに見えますね… ただ、該当Runをコンソールで確認するとOPAで引っかかったことがわかります。

opa-policies-failed-1-3

after: v1.4.0で実行

% terraform apply
(plan結果…)
  # module.network.module.vpc.aws_vpc.this[0] will be created
  + resource "aws_vpc" "this" {
      + arn                                  = (known after apply)
      + assign_generated_ipv6_cidr_block     = false
      + cidr_block                           = "10.0.0.0/16"
      + default_network_acl_id               = (known after apply)
      + default_route_table_id               = (known after apply)
      + default_security_group_id            = (known after apply)
      + dhcp_options_id                      = (known after apply)
      + enable_classiclink                   = (known after apply)
      + enable_classiclink_dns_support       = (known after apply)
      + enable_dns_hostnames                 = true
      + enable_dns_support                   = true
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = ""
        }
      + tags_all                             = {}
    }

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

Post-plan Tasks:

OPA Policy Evaluation

→→ Overall Result: FAILED
 This result means that one or more OPA policies failed. More than likely, this was due to the discovery of violations by the main rule and other sub rules
2 policies evaluated

→ Policy set 1: learn-terraform-drift-and-opa (2)
  ↳ Policy name: friday_deploys
     | × Failed
     | No description available
  ↳ Policy name: public_ingress
     | ✓ Passed
     | No description available
╷
│ Error: Task Stage failed.
│ 
│ 
╵

OPAのチェック結果が確認できますね。

null_resourceがTerraform標準機能になった

null_resourceをご存知でしょうか? 基本的にこのリソースは何もしないのですが、他のリソースでは実現できないことを実現したい際にトリッキーな使い方をされることが多いです。例えば私はLambda関数をTerraformだけでデプロイする際に、null_resourceをProvisionersと組み合わせてビルドコマンドを実行するために使いました。

null_resourcenull providerのリソースなのですが、今回の1.4でこのnull_resourceと同等のリソースがTerraformの標準リソースterraform_dataとして用意されることになりました。なので今後はproviderのインストールが不要になります。

null_resource → terraform_dataへの置き換えをやってみた

前述の、Lambda関数のデプロイに使っていたnull_resourceterraform_dataに置き換えてみました。

before: null_resource

resource "null_resource" "lambda_build" {
  depends_on = [aws_s3_bucket.lambda_assets]

  triggers = {
    code_diff = sha256(join("", [
      for file in setunion(
        fileset(local.transfer_function_dir_local_path, "{*.ts,package*.json}")
        , fileset(local.transfer_function_dir_local_path, "src/**/*.ts")
      )
      : filebase64("${local.transfer_function_dir_local_path}/${file}")
    ]))
  }

  provisioner "local-exec" {
    command = "cd ${local.transfer_function_dir_local_path} && npm install"
  }
  provisioner "local-exec" {
    command = "cd ${local.transfer_function_dir_local_path} && npm run build"
  }
  provisioner "local-exec" {
    command = "aws s3 cp ${local.transfer_function_package_local_path} s3://${aws_s3_bucket.lambda_assets.bucket}/${local.transfer_function_package_s3_key}"
  }

  provisioner "local-exec" {
    command = "openssl dgst -sha256 -binary ${local.transfer_function_package_local_path} | openssl enc -base64 | tr -d \"\n\" > ${local.transfer_function_package_base64sha256_local_path}"
  }
  provisioner "local-exec" {
    command = "aws s3 cp ${local.transfer_function_package_base64sha256_local_path} s3://${aws_s3_bucket.lambda_assets.bucket}/${local.transfer_function_package_base64sha256_s3_key} --content-type \"text/plain\""
  }
}

after: terraform_data

とても簡単でした。変えたのは以下だけです。

  • resourceタイプを null_resource → terraform_dataに変更
  • triggers argumentをtriggers_replaceに変更
  • 他のリソースでnull_resource.lambda_buildをdepends_onで参照しているものがあったので、それをterraform_data.lambda_buildに変更
resource "terraform_data" "lambda_build" {
  depends_on = [aws_s3_bucket.lambda_assets]

  triggers_replace = {
    code_diff = sha256(join("", [
      for file in setunion(
        fileset(local.transfer_function_dir_local_path, "{*.ts,package*.json}")
        , fileset(local.transfer_function_dir_local_path, "src/**/*.ts")
      )
      : filebase64("${local.transfer_function_dir_local_path}/${file}")
    ]))
  }

  provisioner "local-exec" {
    command = "cd ${local.transfer_function_dir_local_path} && npm install"
  }
  provisioner "local-exec" {
    command = "cd ${local.transfer_function_dir_local_path} && npm run build"
  }
  provisioner "local-exec" {
    command = "aws s3 cp ${local.transfer_function_package_local_path} s3://${aws_s3_bucket.lambda_assets.bucket}/${local.transfer_function_package_s3_key}"
  }

  provisioner "local-exec" {
    command = "openssl dgst -sha256 -binary ${local.transfer_function_package_local_path} | openssl enc -base64 | tr -d \"\n\" > ${local.transfer_function_package_base64sha256_local_path}"
  }
  provisioner "local-exec" {
    command = "aws s3 cp ${local.transfer_function_package_base64sha256_local_path} s3://${aws_s3_bucket.lambda_assets.bucket}/${local.transfer_function_package_base64sha256_s3_key} --content-type \"text/plain\""
  }
}

inputとoutputのユースケースを考える

null_resourceには無くてterraform_dataにあるものとして、inputargumentとoutput reference attributeがあります。これらの使い道を考えてみました。

まず、input単体でなら、公式ドキュメントにもあるように、変数を参照したいけど変数を参照することが禁じられているargumentに対して、間にterraform_dataをかます、という使い方があると思います。

次にoutputですが、公式ドキュメントに以下のように説明されています。

The computed value derived from the input argument. During a plan where output is unknown, it will still be of the same type as input. (参照元)

さらにinputにも以下のように説明があります。

A value which will be stored in the instance state, and reflected in the output attribute after apply. (参照元)

つまりapplyフェーズでしか値が明らかにならないattributeです。これを利用して、プロビジョニング順を制御するのに使えそうです。以下のようにoutput値を参照するリソースを定義しました。

resource "aws_s3_object" "test" {
  bucket  = aws_s3_bucket.lambda_assets.id
  key     = terraform_data.lambda_build.output
  content = "test"
}

すると、terraform_dataリソースのプロビジョニングが完了した後に上記リソースのプロビジョニングが始まっていることがわかります。

terraform applyの出力の抜粋

terraform_data.lambda_build: Creation complete after 5s [id=5fe2d0b0-1602-e96b-c7f2-cff8974e7d40]
data.aws_s3_object.package: Reading...
data.aws_s3_object.package_hash: Reading...
aws_s3_object.test: Creating...
data.aws_s3_object.package: Read complete after 0s [id=kze-bucket-123456789012-lambda-assets/transfer/index.zip]
data.aws_s3_object.package_hash: Read complete after 0s [id=kze-bucket-123456789012-lambda-assets/transfer/index.zip.base64sha256.txt]
aws_s3_object.test: Creation complete after 0s [id=hoge]

当たり前ですが、以下のようにterraform_dataに依存関係のないリソースの場合は、

resource "aws_s3_object" "test" {
  bucket  = aws_s3_bucket.lambda_assets.id
  key     = "hoge"
  content = "test"
}

並列でプロビジョニングされ、terraform_dataより前にプロビジョニング完了します(する場合があります)

terraform applyの出力の抜粋

aws_s3_object.test: Creating...
terraform_data.lambda_build: Creating...
terraform_data.lambda_build: Provisioning with 'local-exec'...
terraform_data.lambda_build (local-exec): Executing: ["/bin/sh" "-c" "cd ./lambdas/transfer && npm install"]
aws_s3_object.test: Creation complete after 0s [id=hoge]
aws_s3_bucket_acl.lambda_assets: Creation complete after 1s [id=kze-bucket-123456789012-lambda-assets,private]
aws_s3_bucket_policy.lambda_assets: Creation complete after 1s [id=kze-bucket-123456789012-lambda-assets]
aws_s3_bucket_public_access_block.lambda_assets: Creation complete after 1s [id=kze-bucket-123456789012-lambda-assets]

terraform_data.lambda_build (local-exec): > transfer@1.0.0 prepare
(略)
terraform_data.lambda_build: Creation complete after 4s [id=44ff1758-28f8-2e26-8989-70684e4c63eb]

プロビジョニング順を制御というとdepends_on = [terraform_data.lambda_build]でも実現できます。どちらも初回プロビジョニング時の順番を制御することが可能です。違いは、depends_onだとterraform_dataリソースが再作成される場合には該当リソースの更新が走らない、outputの場合は走るという点です。outputはterraform_dataリソースがapply対象になるたびに、apply後に値が明らかになるattributeだからです。

まあ現時点で、このoutputによるプロビジョニングの制御を使う具体的なユースケースは?と言われると思いついていないのですが… とりうる実装法として頭に留めておきたいと思います。(とはいえわかりにくいので、使わないのが一番だと思います)

null_data_sourceのoutputとの比較

おそらくこのterraform_dataのoutputは、null_data_sourceのoutput attributeと近いと思います。が、null_data_sourceのoutputはdata sourceのreadが完了後に値が入るものだということで、微妙にタイミングが違うので、使い分けることができる、はず?(未検証です)

参考情報