Terraform のVersion 1.1がGAになりました

2021.12.27

2021/12/9、Terraform Version 1.1がGAになりました。主要な変更点を見ていきます。

moved block

新しく追加されたmoved blockによって、リファクタリングがやりやすくなりました。

まずは、moved block導入前のリファクタリングの難しいところを説明します。例えばRDSインスタンスを作るTerraformコードを書いたとしましょう。

resource "aws_db_instance" "default" {
  allocated_storage    = 10
  engine               = "mysql"
  engine_version       = "5.7"
  instance_class       = "db.t3.micro"
  name                 = "mydb"
  username             = "foo"
  password             = "foobarbaz"
  parameter_group_name = "default.mysql5.7"
  skip_final_snapshot  = true
}

これはaws_db_instanceのドキュメントからそのまま拝借してきたコードです。このコードでRDBインスタンスを作ったとします。

作ってから、このRDSインスタンスのTerraform上の名前defaultがイマイチだったなと思い、変えたくなりました。

- resource "aws_db_instance" "default" {
+ resource "aws_db_instance" "main" {
   allocated_storage    = 10
   engine               = "mysql"
   engine_version       = "5.7"
   instance_class       = "db.t3.micro"
   name                 = "mydb"
   username             = "foo"
   password             = "foobarbaz"
   parameter_group_name = "default.mysql5.7"
   skip_final_snapshot  = true
 }

ですが、この変更のあとterraform planすると、RDSインスタンスの削除→作成が走ることがわかります。DB内のデータが消えてしまいますのでこれはやりたくありませんね。

plan結果

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

Terraform will perform the following actions:

  # aws_db_instance.default will be destroyed
  # (because aws_db_instance.default is not in configuration)
  - resource "aws_db_instance" "default" {
      (省略)
    }

  # aws_db_instance.main will be created
  + resource "aws_db_instance" "main" {
      (省略)
    }

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

Terraformをある程度使われた方なら遭遇したことのある場面かと思います。

こういうときはterraform state mvコマンドを使ってリソースアドレスを事前に変更すれば、前述の削除→作成は回避することができます。

% terraform state mv aws_db_instance.default aws_db_instance.main   
Move "aws_db_instance.default" to "aws_db_instance.main"
Successfully moved 1 object(s).
% terraform plan 
(省略)
No changes. Your infrastructure matches the configuration.

なのですが、例えば自分が公開モジュールを作っていて、自分以外の人がそのモジュールを利用しているような状況だったらどうでしょうか?リファクタリングした後に利用者全員に「terraform state mvを使ってね」と伝えるのは無理があります。他のケースだと、Terraformの実行は基本的にCI/CDパイプラインで自動化していて、terraform state mvコマンドを気軽に実行できないとか、シングルテナント構成で大量に同じ構成のStateがあるのですべてでterraform state mvコマンドを打つのは煩雑だ、みたいなケースが考えられます。

そこでこのmovedblockの出番です。

moved {
  from = aws_db_instance.default
  to   = aws_db_instance.main
}

上記のようなコードを書いておくと、plan前に前述のterraform state mvコマンドをやったような挙動になり、結果として削除→作成は起こりません。つまり、Stateファイル内にaws_db_instance.defaultというアドレスのリソースがあった場合は、それをaws_db_instance.mainと読み替えてからTerraformコードとの差分を調べてくれます。

ちなみに、実際にアドレスが更新されるのはapply実行時です。それまで(=plan実行しただけなど)はアドレスはあくまで変更前のもののままであり、movedblockがうまく解釈してくれているだけです。

上記は単純なリソース名の変更のパターンでしたが、以下のようなケースでも使えます。

  • 単一リソース(=countもfor_eachも使わず作ったリソース)、countで作ったリソース、for_eachで作ったリソース間の移動
  • サブモジュール名の変更
  • サブモジュールの、単一呼び出し、countを使った呼び出し、for_eachを使った呼び出し間の移動
  • 既存リソースを複数のサブモジュール内に移動

cloud block

Terraform CloudをCLIで使う場合の設定方法が増えて、新しい方法が推奨される様になりました。

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

  • UI/VCS-driven workflows
  • CLI-driven workflows
  • API-driven workflows

今回のアップデートはこのうちのCLI-driven workflowsに関するものです。

CLI-driven workflowsはその名の通り、CLIでTerraform Cloudを使う方法です。使用感としてはTerraform Cloudを使わずローカルで使うのと同じ感じです。

1.1より前だと、Terraform Cloudを使う場合、以下のようにterraform.backendブロックにremoteと指定します。

terraform {
  backend "remote" {
    organization = "test202112"
    workspaces {
      name = "test_v1_0"
    }
  }
}

この書き方は1.1でも引き続き使えますが、代わりに次の書き方が推奨されます。

terraform {
  cloud {
    organization = "test202112"
    workspaces {
      name = "test_v1_1"
    }
  }
}

この新しい書き方、何が嬉しいのでしょうか? 3つあります。

1.エラーメッセージがわかりやすくなる

cloudblockを使ったほうが、よりTerraform Cloudに即したエラーメッセージを返してくれるようです。

例を挙げます。以下は、backendblockでremote指定した場合とcloudblockを使った場合それぞれでの、terraform init未実行状態でterraform planを実行した場合のエラーメッセージです。

`backend`使用時

% terraform plan
╷
│ Error: Backend initialization required, please run "terraform init"
│ 
│ Reason: Initial configuration of the requested backend "remote"
│ 
│ The "backend" is the interface that Terraform uses to store state,
│ perform operations, etc. If this message is showing up, it means that the
│ Terraform configuration you're using is using a custom configuration for
│ the Terraform backend.
│ 
│ Changes to backend configurations require reinitialization. This allows
│ Terraform to set up the new configuration, copy existing state, etc. Please run
│ "terraform init" with either the "-reconfigure" or "-migrate-state" flags to
│ use the current configuration.
│ 
│ If the change reason above is incorrect, please verify your configuration
│ hasn't changed and try again. At this point, no changes to your existing
│ configuration or state have been made.

`cloud`使用時

% terraform plan
╷
│ Error: Terraform Cloud initialization required: please run "terraform init"
│ 
│ Reason: Initial configuration of Terraform Cloud.
│ 
│ Changes to the Terraform Cloud configuration block require reinitialization, to discover any changes to the available workspaces.
│ 
│ To re-initialize, run:
│   terraform init
│ 
│ Terraform has not yet made changes to your existing configuration or state.

2.Terraform Cloudの設定をbackendに書くのはそもそもちょっとわかりにくかった

backendブロックはその名の通りバックエンドの設定を書くところです。TerraformでいうバックエンドとはStateファイルを格納するストレージのことです。

Terraform Cloudを使うと、StateファイルはTerraform Cloud側で管理してくれます。ここまではいいのですが、Terraform Cloudはそれだけでなく、Terraformコマンドを実行する環境もTerraform Cloud上になります。(※オプションでローカルに変えることもできます)

というわけで、backendremoteと書くと、本来Stateファイル格納先について設定するはずのbackendブロックにて、Terraform実行環境についても設定していることになってしまいます。それが少しわかりにくいので、今回別のcloudblockが用意されたというわけです。(このような特異性から、remote backendを"enhanced" backendと呼ぶこともあるそうです)

3.tagでworkspaceが指定できる

さきほどはTerraform Cloudのworkspacesの指定にnameを使いました。

再掲

terraform {
  cloud {
    organization = "test202112"
    workspaces {
      name = "test_v1_1"
    }
  }
}

ですが、 name以外にも tagsで指定する方法もあります。例えば一連のVPCリソース(サブネットとかNACLとか)を作るTerraformモジュールを作るとします。

terraform {
  cloud {
    organization = "test202112"
    workspaces {
      tags = ["vpc"]
    }
  }
}

terraform initしてみましょう。

% terraform init

Initializing Terraform Cloud...

No workspaces found.
  
  There are no workspaces with the configured tags (vpc)
  in your Terraform Cloud organization. To finish initializing, Terraform needs at
  least one workspace available.
  
  Terraform can create a properly tagged workspace for you now. Please enter a
  name to create a new Terraform Cloud workspace.

  Enter a value:

「workspace作るから名前入力して」と言われているので、入力します。

(省略)
  Enter a value: dev 


Initializing provider plugins...

Terraform Cloud has been successfully initialized!

You may now begin working with Terraform Cloud. Try running "terraform plan" to
see any changes that are required for your infrastructure.

If you ever set or change modules or Terraform Settings, run "terraform init"
again to reinitialize your working directory.

Terraform Cloudのコンソールで確認すると、devという名前で、vpcタグが付いているworkspaceが作成されています。 dev-vpc

terraform workspace listでもこのworkspaceが確認できます。

% terraform workspace list
* dev

この状態で、以下のコードを使ってVPCを作ってみましょう。ポイントは7行目のterraform.workspace変数を使っているところです。

resource "aws_vpc" "vpc" {
  cidr_block           = "192.168.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "cloud-block-test-vpc-${terraform.workspace}"
  }
}

plan結果は以下です。

% terraform plan
(省略)

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_vpc.vpc will be created
  + resource "aws_vpc" "vpc" {
      + arn                            = (known after apply)
      + cidr_block                     = "192.168.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)
      + main_route_table_id            = (known after apply)
      + owner_id                       = (known after apply)
      + tags                           = {
          + "Name" = "cloud-block-test-vpc-dev"
        }
      + tags_all                       = {
          + "Name" = "cloud-block-test-vpc-dev"
        }
    }

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

terraform.workspace変数がTerraform Cloudのworkspace名に変換されていることがわかります。

同じ要領で別のworkspaceを作って、別環境を作成できます。

% terraform workspace new stg
Created and switched to workspace "stg"!

You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
% terraform plan
(省略)
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_vpc.vpc will be created
  + resource "aws_vpc" "vpc" {
      + arn                            = (known after apply)
      + cidr_block                     = "192.168.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)
      + main_route_table_id            = (known after apply)
      + owner_id                       = (known after apply)
      + tags                           = {
          + "Name" = "cloud-block-test-vpc-stg"
        }
      + tags_all                       = {
          + "Name" = "cloud-block-test-vpc-stg"
        }
    }

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

これまで(=backendでやる場合)はどうだったのか

tagを使うことはできず、代わりにworkspace名のprefixを使うことができます。

terraform {
  backend "remote" {
    organization = "test202112"
    workspaces {
      prefix = "vpc_remote_"
    }
  }
}

この状態で、vpc_remote_devやvpc_remote_stgなどの名前のworkspaceを作ります。

Terraform Cloud workspace名が変数で参照可能に

さきほど既に使っていますが、${terraform.workspace}で使用中のTerraform Cloud workspace名が参照できるようになりました。これはcloudblockを使った場合だけではなく、backendblockを使う場合や、UI/VCS-driven workflowsなど別のworkflowを使う場合でも参照可能です。

planやapplyでdeleteが発生する際の説明を追加

公式ドキュメントで例として挙げられていたのが、countを使って複数リソースを作成するコードを書いていた場合に、countの値を減らした場合でした。実際に確認してみます。

こういうコードを書いて、一度applyします。

resource "aws_subnet" "public_subnet" {
  count                   = 2
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = cidrsubnet(aws_vpc.vpc.cidr_block, 8, count.index)
  availability_zone       = data.aws_availability_zones.az.names[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet-${count.index + 1}"
  }
}

count値を減らして、planを実行してみました。

 resource "aws_subnet" "public_subnet" {
+  count                   = 1
-  count                   = 2
   vpc_id                  = aws_vpc.vpc.id
   cidr_block              = cidrsubnet(aws_vpc.vpc.cidr_block, 8, count.index)
   availability_zone       = data.aws_availability_zones.az.names[count.index]
   map_public_ip_on_launch = true

   tags = {
     Name = "public-subnet-${count.index + 1}"
   }
 }
% terraform plan 
(省略)
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_subnet.public_subnet[1] will be destroyed
  # (because index [1] is out of range for count)
  - resource "aws_subnet" "public_subnet" {
      - arn                             = "arn:aws:ec2:ap-northeast-1:047429787746:subnet/subnet-00a2b967422ca83dc" -> null
      - assign_ipv6_address_on_creation = false -> null
      - availability_zone               = "ap-northeast-1c" -> null
      - availability_zone_id            = "apne1-az1" -> null
      - cidr_block                      = "192.168.1.0/24" -> null
      - id                              = "subnet-00a2b967422ca83dc" -> null
      - map_customer_owned_ip_on_launch = false -> null
      - map_public_ip_on_launch         = true -> null
      - owner_id                        = "047429787746" -> null
      - tags                            = {
          - "Name" = "public-subnet-2"
        } -> null
      - tags_all                        = {
          - "Name" = "public-subnet-2"
        } -> null
      - vpc_id                          = "vpc-075d0669c6c930621" -> null
    }

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

ハイライトした (because index [1] is out of range for count)が追加された説明です。version 1.0.11でも同様の操作をしてみましたが表示されませんでした。

次は別の例です。初心者あるある(?)な、リソース名を変えてしまって削除→作成が実行されるパターン。(※前述のとおり、こういった場合はterraform state mvコマンドを使うか、moved blockを使いましょう)

+ resource "aws_vpc" "main" {
- resource "aws_vpc" "vpc" {
   cidr_block           = "192.168.0.0/16"
   enable_dns_support   = true
   enable_dns_hostnames = true

   tags = {
     Name = "cloud-block-test-vpc-${terraform.workspace}"
   }
 }
% terraform plan
(省略)

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

Terraform will perform the following actions:

  # aws_vpc.main will be created
  + resource "aws_vpc" "main" {
      (省略)
    }

  # aws_vpc.vpc will be destroyed
  # (because aws_vpc.vpc is not in configuration)
  - resource "aws_vpc" "vpc" {
      (省略)
    }

(because aws_vpc.vpc is not in configuration)が追加されています。

変数の「nullable」が設定可能に

これまでは、変数にnull値を渡すことが可能でした。null値を渡されたattributeは未設定とみなされ、デフォルト値が適用されます。

例えば以下のコードで、var.override_private_ipにnullを渡せばプライベートIPは動的に割り当てられます。反対に指定すれば明示的にIPを設定できます。

resource "aws_instance" "example" {
  (省略)

  private_ip = var.override_private_ip
}

Version 1.1からは変数にnull値を渡すことを抑制できます。変数のattirbutenullableが追加され、これをfalseにすることで可能です。

variable "override_private_ip" {
  nullable = false
}

nullable未設定の場合の値、つまりデフォルト値はtrueです。これまでのVersionと同じということですね。ですが将来のVersionではfalseをデフォルト値にしていきたいそうです。

リソースの移動の明示化

既存のcountなしリソースにcountを追加する、もしくはその逆をやった場合、Terraformは自動的にリソースアドレスの移動をやってくれます。これまではそのことがわかりにくかったのですが、planもしくはapplyの出力で明示的に説明してくれるようになりました。

たとえば、以下のようにcountなしで一度作成済みのVPCにあとから count=1を追加してapplyしてみます。

 resource "aws_vpc" "vpc" {
+  count = 1

   cidr_block           = "192.168.0.0/16"
   enable_dns_support   = true
   enable_dns_hostnames = true
 
   tags = {
     Name = "cloud-block-test-vpc-${terraform.workspace}"
   } 
 }

applyの出力は以下です。ハイライトした5行目aws_vpc.vpc has moved to aws_vpc.vpc[0]が追加されました。

% terraform apply
(省略)
Terraform will perform the following actions:

  # aws_vpc.vpc has moved to aws_vpc.vpc[0]
    resource "aws_vpc" "vpc" {
        id                               = "vpc-075d0669c6c930621"
        tags                             = {
            "Name" = "cloud-block-test-vpc-stg"
        }
        # (15 unchanged attributes hidden)
    }

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


Do you want to perform these actions in workspace "stg"?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

ちなみに、Version 1.0.11で同様の変更をかけたところ以下のような出力になりました。わかりにくいですよね。1.1で大分わかりやすくなっているのがわかると思います。

% terraform apply
(省略)

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the last "terraform apply":

  # aws_vpc.vpc has been deleted
  - resource "aws_vpc" "vpc" {
      (省略)
    }

Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or
respond to these changes.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

No changes. Your infrastructure matches the configuration.

Your configuration already matches the changes detected above. If you'd like to update the Terraform state to match, create and apply a refresh-only plan:
  terraform apply -refresh-only

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

workspace削除条件の緩和

Data SourceとOutputしか存在しないWorkspaceを削除したい場合、これまでは -forceオプション付きでterraform workspace deleteする必要がありました。が、本Versionからは -forceオプション不要になりました。

terraform console にtype関数を追加

terraform consoleというコマンドがあります。関数の挙動を確かめたり、リソースにどんなattiributeがあるのか確認するのにべんりです。

今回このterraform console内で使えるtype()関数が追加されました。引数に指定した要素の型を教えてくれる関数です。

% terraform console
> type(aws_vpc.vpc)
object({
    arn: string,
    assign_generated_ipv6_cidr_block: bool,
    cidr_block: string,
    default_network_acl_id: string,
    default_route_table_id: string,
    default_security_group_id: string,
    dhcp_options_id: string,
    enable_classiclink: bool,
    enable_classiclink_dns_support: bool,
    enable_dns_hostnames: bool,
    enable_dns_support: bool,
    id: string,
    instance_tenancy: string,
    ipv4_ipam_pool_id: string,
    ipv4_netmask_length: number,
    ipv6_association_id: string,
    ipv6_cidr_block: string,
    ipv6_ipam_pool_id: string,
    ipv6_netmask_length: number,
    main_route_table_id: string,
    owner_id: string,
    tags: map(string),
    tags_all: map(string),
})
> type(aws_vpc.vpc.arn)
string
> type(aws_vpc.vpc.enable_dns_hostnames)
bool

すでに1.1使われている方は1.1.2にアップグレードを

バージョン1.1.0と1.1.1にはバグがあり、空のStateファイルが保存されてしまう場合があるそうです。1.1.2への早急なアップグレード要請がアナウンスされています。

参考情報