クラウド同上

Terraformを0.11から0.12に移行するときの注意点とは

Author
Shohei Abe
Lv:9 Exp:14010

2017年10月より入社しました。これまでの技術分野は主にインフラ関係です。
今後はGCPでフルスタック対応できるエンジニアを目指し日々いろいろと活動しています。

みんな大好きわたしも大好きなTerraformが0.12にバージョンアップして、早いもので1か月が経過しようとしています。Terraform0.12で採用されているHCL2はより柔軟な設定がかけたり、validateやplan時のチェックが強化されていたりと、なかなか興味深い機能が追加されていたります。この記事では差し当たって0.12ベースに移行するときに何を気を付けるべきかについてまとめてみました。そのため、Terraform 0.12以降の新構文についてはあまり触れていません。

対象読者

  • Terraform を業務等で使用している
  • Terraform に関する基本的な構文・機能の知識がある
  • Terraform が大好きな人

Terraform 0.11→0.12 で何が変わるか

Terraform 0.12から、HCL(Terraformの設定構文、Hashicorp Configuration Language)がバージョン2に更新されました。(以降、Terraform 0.12以降の設定構文をHCL2、0.11以前の設定構文をHCL1と呼ぶことにします。)

HCL1とHCL2には一部非互換が存在するため、Terraform 0.11以前に作成したTFファイルがそのままTerraform 0.12以降で使えない可能性があります。単純にTerraform CLIのバイナリを0.12以降に置き換えただけでは terraform apply 等がエラーになり、ツール等の実行に支障をきたす可能性もあります。

Terraform 0.12 対応の Google Provider Version

Google Provider は 2.5.0 から Terraform 0.12 に対応しています。Google Provider の version が 2.5.0 より古いものに固定している場合は、Providerのバージョンアップを先に実施する必要があります。

参考: Provider Google 2.5.0 の CHANGELOG.md

0.11→0.12アップグレードの手順概略

  1. 念のためアップデート前に tfファイル、及び、 stateファイルをバックアップ
  2. Terraform コマンドを 0.11.14 にアップグレード
  3. Google Provider を 2.5.0 以降にアップグレード
  4. terraform 0.12checklist コマンドを実行、その後、メッセージ内容に従って tfファイルを修正
  5. Terraform コマンドを 0.12.0 にアップグレード
  6. terraform 0.12upgrade コマンドを実行し、tfファイルとstateファイルをHCL2ベースにアップグレード
  7. TF-UPGRADE-TODO コメント部分の設定について確認、必要に応じて修正
  8. terraform validate や terraform plan でチェックし、構文エラーが無いことを確認

terraform 0.12checklist コマンドについて

Terraform 0.11.14 にのみ、 terraform 0.12checklist サブコマンドが実装されています。(以降の文章では、 terraform 0.12checklist を単にchecklistコマンドと呼ぶことにします。)

checklistコマンドはtfファイルのベースが HCL1 から HCL2 に変更された際に明らかに構文エラーとなるものについて事前チェックを行うコマンドです。既存のtfファイルに対して読み取りのみ行い、tfファイル内容やstateファイルへの変更は行いません。このchecklistコマンドで特に問題無ければ以下のようなメッセージが出力されます。

Looks good! We did not detect any problems that ought to be
addressed before upgrading to Terraform v0.12.

チェック項目の詳細について以下に記載します。

プロバイダのバージョンチェック

terraform init で導入済みのプロバイダをチェックし、 0.12 未対応のプロバイダがある場合にメッセージを出力します。以下は、 Google Prover version が 2.4.1 のときに checklistコマンドを実行した場合のメッセージ例です。

- [ ] Upgrade provider "google" to version 2.7.0 or newer.

  No currently-installed version is compatible with Terraform 0.12. To upgrade, set the version constraint for this provider as follows and then run `terraform init`:

      version = "~> 2.7.0"

provider の version 属性で、Providerを古いバージョンに固定している場合は、少なくとも 2.5.0 以降をダウンロードするよう設定変更し、 terraform init でアップグレードする必要があります。version 属性で固定化していない場合は、 terraform init -upgrade と実行すると最新版にアップグレードされます。

リソース名、エイリアス名

HCL1では、リソース名の先頭文字に数字や記号を使うことができました。

例: google_compute_network.012network google_compute_subnetwork._012sub

HCL2では、リソース名の先頭文字はletter(A~Z,a~z)のみ許容される仕様となったため、リソース名の先頭文字がletter以外の場合は修正が必要になります。リソース名の修正は、tfファイルを修正した後、 terraform state mv コマンドを使って state ファイル上のリソース名をtfファイルのリソース名と一致するようにします。

ちなみに、同様の制約は、リソース名だけでなくプロバイダのエイリアス名にも適用されます。以下のようなエイリアス名はHCL2以降はエラーとなります。

provier "google" {
  alias = "012error" #先頭文字が数字のためHCL2ではエラー扱いとなる
  project = "${var.project_id}"
}

変数名(variable)

HCL2では変数名として使用できない予約キーワードが追加されました。例えば、 depends_on という変数名は HCL1では使えますが、HCL2では予約キーワードとなり使用できなくなります。その他の使用できないキーワードについては下記ドキュメントを参照してください。

Input Variables Declaring an Input Variable

※ちなみに、変数名の先頭文字はリソース名と同様の制約はありません。ただ、リソース名と記述レベルを合わせるという意味では、変数名の先頭文字にletter以外を使わない方がよいと思います。

terraform 0.12upgrade コマンドについて

terraform 0.11.14 の terraform 0.12checklist コマンドで無事チェックが通りましたらいよいよ HCL2ベースの構文にアップデートすることになります。ここからは、 terraform 0.12 以降に実装されている terraform 0.12upgrade サブコマンドがどのようなことを行うかについて説明します。(以降の文章では、 terraform 0.12upgrade をupgradeコマンドと呼ぶことにします。)

なお、upgrade コマンドは既存のtfファイルを変更してしまうため、コマンド実行前にtfファイルをバックアップしておくべきです。

upgradeコマンド実行後の作業

upgradeコマンドによりtfファイルのコードベースがHCL1からHCL2に修正されます。ただし、機械的に変更できない(もしくは、変更したが正しく動作しない可能性がある)記述については「TF-UPGRADE-TODO」というコメントが挿入されます。「TF-UPGRADE-TODO」のコメントが挿入された設定についてはユーザーが何らかチェックし、場合により手で修正する必要があります。

upgradeコマンドを実行するときに出力されるメッセージ

アップデートしたいtfファイルが配置されているディレクトリで terraform 0.12upgrade を実行すると、以下のようなメッセージが表示されます。

This command will rewrite the configuration files in the given directory so
that they use the new syntax features from Terraform v0.12, and will identify
any constructs that may need to be adjusted for correct operation with
Terraform v0.12.

We recommend using this command in a clean version control work tree, so that
you can easily see the proposed changes as a diff against the latest commit.
If you have uncommited changes already present, we recommend aborting this
command and dealing with them before running this command again.

Would you like to upgrade the module in the current directory?
  Only 'yes' will be accepted to confirm.

  Enter a value:

「Enter a value」に続けて「yes」と入力すると、tfファイルの自動修正が実施され、以下のようなメッセージが表示されます。

Upgrade complete!

The configuration files were upgraded successfully. Use your version control
system to review the proposed changes, make any necessary adjustments, and
then commit.

versions.tf ファイル生成

terraform.required_version が未設定の場合、upgradeコマンド正常終了後に versions.tf が作成され、Terraform 0.12以降にバージョン制限する設定が追加されます。
なお、元々 versions.tf というファイルがある場合は、 versions-1.tf というファイルが作成されます。

terraform {
  required_version = ">= 0.12"
}

ちなみに、 terraform.required_version でコマンドラインバージョンを 0.12 以降の動作に制限する設定がある場合に、再度 upgrade コマンド実行すると既にアップグレードされている旨のメッセージが出力されます。

Error: Module already upgraded

  on versions.tf line 3, in terraform:
   3:   required_version = ">= 0.12"

The module in directory . has a version constraint that suggests it has
already been upgraded for v0.12. If this is incorrect, either remove this
constraint or override this heuristic with the -force argument. Upgrading a
module that was already upgraded may change the meaning of that module.

ブロックタイプ属性記述の厳密化

あるリソースの属性がブロックタイプ (attr { … } という波括弧で記述する属性)である場合、 0.11までは attr = { … } と attr { … } の両方の記述を許容していました。
0.12では属性がブロックタイプの場合は attr = { … } は許容されないため、 attr { … } の記述に統一されます。 attr = { … } という記述が残っているとHCL2では構文エラーとなります。upgrade コマンドは、このような記述を attr { … } に修正します。

変更例: upgrade前

resource "google_compute_instance" "practice" {
  ...
  network_interface {
    subnetwork    = "${google_compute_subnetwork.practice.self_link}"
    network_ip    = "${google_compute_address.practice-int.address}"
    access_config = {}
  }
...

変更例: upgrade後

resource "google_compute_instance" "practice" {
  ...
  network_interface {
    subnetwork    = google_compute_subnetwork.practice.self_link
    network_ip    = google_compute_address.practice-int.address
    access_config {
    }
  }
...

公式ドキュメントの説明

Attributes vs. blocks

マップ属性記述の厳密化

あるリソースの属性がマップ (attr = { key = value } という記述) である場合、 HCL1までは attr { key = value } というブロックタイプの記述を許容していました。
HCL2では属性がマップの場合は attr { key = value } は許容されないため、 attr = { key = value } の記述に統一されます。マップ属性設定で attr { key = value } という記述が残っているとHCL2では構文エラーとなります。
upgrade コマンドはこのような記述を attr = { key = value } に修正します。

変更例: upgrade前

resource "google_compute_instance" "practice" {
  ...
  labels {
    environment = "production"
  }  
...

変更例: upgrade後

resource "google_compute_instance" "practice" {
  ...
  labels = {
    environment = "production"
  }  
...

公式ドキュメントの説明

Attributes vs. blocks

リソース属性、変数の参照の記述修正

あるリソースが別リソースの属性を参照する、または、変数(variable,local)を参照する際にHCL1では attr = “${resource_type.resource_name.attr}” と記述する必要がありましたが、HCL2ではそのまま attr = resource_type.resource_name.attr と記述できます。(このような記述を First-class Expressionと呼びます。)
upgradeコマンドは、上記の参照をHCL2標準の記述に修正します。なお、 attr = “${resource_type.resource_name.attr}” という記述が残っていてもHCL2では許容されます。

変更例: upgrade前

locals {
  vm_name     = "practice-vm"
}

resource "google_compute_instance" "practice" {
  name = "${local.vm_name}"
...

変更例: upgrade後

locals {
  vm_name     = "practice-vm"
}

resource "google_compute_instance" "practice" {
  name = local.vm_name
...

公式ドキュメントの説明

First-class expressions

count 属性の外部参照の記述修正

リソースを複数作成したいときに使う count 属性を使用した際、HCL1ではリソース count 値を参照する疑似属性として resource_type.resource_name.count で参照することができました。
HCL2では外部参照としての count 属性は使用できないため、 upgradeコマンドにより length(resource_type.resource_name) に置換されます。

変更例: upgrade前

resource "google_compute_address" "practice-int" {
  count = 2
...
}

resource "google_compute_instance" "practice" {
  count = "${google_compute_address.practice-int.count}"
...
}

変更例: upgrade後

resource "google_compute_address" "practice-int" {
  count = 2
...
}

resource "google_compute_instance" "practice" {
  count = length(google_compute_address.practice-int)
...
}

公式ドキュメントの説明

Working with count on resources

count属性有りリソース参照設定の修正と非互換

count属性を設定することで、1つのリソース設定から複数のリソースを作成することが可能です。count属性を使用したリソースへの参照を行う場合、HCL1では resource_type.resource_name.*.attr[index] のような記述になります。(このような表現をLegacy Splat Expression と呼ぶようです。)

HCL2では、count属性を持つリソースに参照したい場合は、 resource_type.resource_name[index].attr と記述します。(こちらを Splat Expression と呼ぶようです。)
現時点ではまだHCL2で Legacy Splat Expression による記述を併用することが可能ですが、HCL2ではより新しい記述である Splat Expression が推奨されます。

変更例: upgrade前

locals {
  ip_cidr_range = ["192.168.10.0/24", "192.168.11.0/24", "192.168.12.0/24"]
}

resource "google_compute_subnetwork" "practice" {
  count   = 3
  network = "${google_compute_network.practice.self_link}"
  name    = "terraform-study-${count.index + 1}"
  region  = "${var.default_region}"

  ip_cidr_range = "${local.ip_cidr_range[count.index]}"
}

resource "google_compute_address" "practice-int" {
  count        = "${google_compute_subnetwork.practice.count}"
  name         = "practice-internal-address-${count.index + 1}"
  address_type = "INTERNAL"
  subnetwork   = "${google_compute_subnetwork.practice.*.self_link[count.index]}"
  address      = "${cidrhost(google_compute_subnetwork.practice.*.ip_cidr_range[count.index], count.index + 2)}"
}

変更例: upgrade後

locals {
  ip_cidr_range = ["192.168.10.0/24", "192.168.11.0/24", "192.168.12.0/24"]
}

resource "google_compute_subnetwork" "practice" {
  count   = 3
  network = google_compute_network.practice.self_link
  name    = "terraform-study-${count.index + 1}"
  region  = var.default_region

  ip_cidr_range = local.ip_cidr_range[count.index]
}

resource "google_compute_address" "practice-int" {
  count        = length(google_compute_subnetwork.practice)
  name         = "practice-internal-address-${count.index + 1}"
  address_type = "INTERNAL"
  subnetwork   = google_compute_subnetwork.practice[count.index].self_link
  address = cidrhost(
    google_compute_subnetwork.practice[count.index].ip_cidr_range,
    count.index + 2,
  )
}

追加の注意事項

HCL1ではcount属性有りリソースでも resource_type.resource_name.attr のようなcount属性の無いリソースと同じ記述で参照することができました。この場合、暗黙で resource_type.resource_name.*.attr[0] として参照されます。

しかし、HCL2ではcount属性有りリソースを resource_type.resource_name.attr と参照するとエラーになります。必ず Splat Expression(resource_type.resource_name[0].attr) で記述するか、または、Legacy Splat expression(resource_type.resource_name.*.attr[0])で参照する必要があります。

公式ドキュメントの説明

Working with count on resources

ファイル読み込みを伴うハッシュ関数の置換

ハッシュ関数(base64sha256, base64sha512, md5, sha1, sha256, sha512)と file 関数を併用して静的ファイルのハッシュ値をtfファイル内で使用している場合、upgradeコマンドによりファイルハッシュ関数(filebase64sha256, filebase64sha512, filemd5, filesha1, filesha256, filesha512)へ置換が行われます。 HCL2の構文上はハッシュ関数と file 関数を併用しても問題ありませんが、 file 関数がUTF8テキストを前提に処理するため、upgradeコマンドにより非UTF8テキストの読み込みに対応する関数に置換されるようです。

変更例: upgrade前

locals {
  sshkey-hash = "${base64sha256(file("id.pub"))}"
}

変更例: upgrade後

locals {
  sshkey-hash = filebase64sha256("id.pub")
}

公式ドキュメントの説明

file Function

variable 設定の変数型記述の厳密化

variable のtype属性に list や map を指定している場合、 typeがHCL2の厳密な指定に修正されます。修正後のtypeは list(string) または map(string) です。 default の内容にかかわらず、 string内包のlistまたは mapと修正されてしまうため、 variableで期待するがlist(string)やmap(string)以外の場合は、 upgradeコマンド後のvariableのtype指定は確認が必要と思います。なお、HCL2でもtypeに list 、 map と指定可能です。その場合は、 list(any) 、 map(any) と認識されるようです。

変更例: upgrade前

variable "zone_list" {
  type    = list(string)
  default = ["asia-northeast1-a", "asia-northeast1-b", "asia-northeast1-c"]
}

variable "machine_type" {
  type = map(string)

  default = {
    production  = "n1-standard-4"
    development = "n1-standard-1"
  }
}

variable "vm_count" {
  type = map(string)
}

変更例: upgrade後

variable "zone_list" {
  type    = list(string)
  default = ["asia-northeast1-a", "asia-northeast1-b", "asia-northeast1-c"]
}

variable "machine_type" {
  type = map(string)

  default = {
    production  = "n1-standard-4"
    development = "n1-standard-1"
  }
}

variable "vm_count" {
  type = map(string)
}

公式ドキュメントの説明

Type Constraints on Variables

リストの要素に変数(variable,locals)使用している場合の警告

リストの要素に変数を指定している場合(attr = [“${local.name}”] または attr = [“${var.name}”])、以下のような警告コメントが追加されます。

  # TF-UPGRADE-TODO: In Terraform v0.10 and earlier, it was sometimes necessary to
  # force an interpolation expression to be interpreted as a list by wrapping it
  # in an extra set of list brackets. That form was supported for compatibilty in
  # v0.11, but is no longer supported in Terraform v0.12.
  #
  # If the expression in the following list itself returns a list, remove the
  # brackets to avoid interpretation as a list of lists. If the expression
  # returns a single list item then leave it as-is and remove this TODO comment.

HCL1では、リスト要素にリスト型の変数を指定しても暗黙で単一のリストにフラット化する処理を行っていましたが、HCL2ではリスト型の扱いが厳密になったため、対象の属性値と設定するリスト型が一致している必要があります。

例として、upgradeコマンド後に以下のような状態になった場合、 target_tags = [local.vm_tag_names] の部分が二重リストになるためエラーになります。

locals {
  vm_tag_names = ["web","db"]
}

resource "google_compute_firewall" "allow-ssh" {
  network   = google_compute_network.practice.self_link
  name      = "allow-common-ssh-from-internet"
  direction = "INGRESS"
  priority  = 1000

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  # TF-UPGRADE-TODO: In Terraform v0.10 and earlier, it was sometimes necessary to
  # force an interpolation expression to be interpreted as a list by wrapping it
  # in an extra set of list brackets. That form was supported for compatibilty in
  # v0.11, but is no longer supported in Terraform v0.12.
  #
  # If the expression in the following list itself returns a list, remove the
  # brackets to avoid interpretation as a list of lists. If the expression
  # returns a single list item then leave it as-is and remove this TODO comment.
  target_tags   = [local.vm_tag_names]
  source_ranges = ["0.0.0.0/0"]
}

このケースでは以下のように target_tags のリストブラケットを削除するような修正が必要です。

locals {
  vm_tag_names = ["web","db"]
}

resource "google_compute_firewall" "allow-ssh" {
  network   = google_compute_network.practice.self_link
  name      = "allow-common-ssh-from-internet"
  direction = "INGRESS"
  priority  = 1000

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  target_tags   = local.vm_tag_names
  source_ranges = ["0.0.0.0/0"]
}

また、複数のリスト型変数を1つのリストにまとめたい場合は、 flatten 関数を使用します。

公式ドキュメントの説明

Referring to List Variables

属性にマップを要素に持つリストを指定している場合

HCL1ではリソースに複数のブロック要素を設定したい場合、マップ要素をもつリストを変数(variable,locals)として定義しておいてから設定することが可能でした。
HCL2では属性の型と設定値の型を厳密に合わせるため、ブロック要素に対してマップ要素を設定できません。HCL1で前述のような設定がある場合、upgradeコマンドはHCL2で新たに導入されたdynamic blocksによる表現に置換します。

変更例: upgrade前

locals {
  allow-common-nw = [
    {protocol = "tcp", ports = ["22"]},
    {protocol = "tcp", ports = ["80"]},
    {protocol = "icmp"},
  ]
}

resource "google_compute_firewall" "allow-common-service" {
  network   = "${google_compute_network.practice.self_link}"
  name      = "allow-common-service-internet"
  direction = "INGRESS"
  priority  = 1000

  allow = "${local.allow-common-nw}"

  target_tags   = ["practice"]
  source_ranges = ["0.0.0.0/0"]
}

変更例: upgrade後

locals {
  allow-common-nw = [
    {
      protocol = "tcp"
      ports    = ["22"]
    },
    {
      protocol = "tcp"
      ports    = ["80"]
    },
    {
      protocol = "icmp"
    },
  ]
}

resource "google_compute_firewall" "allow-common-service" {
  network   = google_compute_network.practice.self_link
  name      = "allow-common-service-internet"
  direction = "INGRESS"
  priority  = 1000

  dynamic "allow" {
    for_each = local.allow-common-nw
    content {
      # TF-UPGRADE-TODO: The automatic upgrade tool can't predict
      # which keys might be set in maps assigned here, so it has
      # produced a comprehensive set here. Consider simplifying
      # this after confirming which keys can be set in practice.

      ports    = lookup(allow.value, "ports", null)
      protocol = allow.value.protocol
    }
  }

  target_tags   = ["practice"]
  source_ranges = ["0.0.0.0/0"]
}

公式ドキュメントの説明

Attributes vs. blocks

lifecycle.ignore_changes に指定するキーワード変更と厳密化

upgrade コマンドを実行すると、 lifecycle.ignore_changes に指定するキーワードからダブルクオートが外れます。HCL2から、文字列ではなくキーワードを指定する構文に変更になるためでs。また、HCL1では、 lifecycle.ignore_changes に指定する全ての属性を指定するキーワードとして “” を使用しますが、HCL2では all と指定する必要があるため、 “” は all に置換されます。

upgrade前

resource "google_compute_network" "practice" {
  project      = "${data.google_project.practice.project_id}"
  name         = "terraform-study"
  routing_mode = "REGIONAL"

  auto_create_subnetworks = false

  lifecycle {
    ignore_changes = ["*"]
  }
}

resource "google_compute_subnetwork" "practice" {
  network = "${google_compute_network.practice.self_link}"
  name    = "terraform-study"
  region  = "${var.default_region}"

  ip_cidr_range = "192.168.10.0/24"

  lifecycle {
    ignore_changes = ["hoge"]
  }
}

upgrade後

resource "google_compute_network" "practice" {
  project      = data.google_project.practice.project_id
  name         = "terraform-study"
  routing_mode = "REGIONAL"

  auto_create_subnetworks = false

  lifecycle {
    ignore_changes = all
  }
}

resource "google_compute_subnetwork" "practice" {
  network = google_compute_network.practice.self_link
  name    = "terraform-study"
  region  = var.default_region

  ip_cidr_range = "192.168.10.0/24"

  lifecycle {
    ignore_changes = [hoge]
  }
}

なお、HCL2では ignore_changes に指定するキーワードを厳密に検証するようになり、ignore_changes に存在しないキーワードを設定しているとエラーになります。 HCL1では単に無視され、エラーにはなりませんでした。HCL2では以下のようなエラーになります。

Error: Unsupported attribute

  on network.tf line 21, in resource "google_compute_subnetwork" "practice":
  21:     ignore_changes = [hoge]

This object has no argument, nested block, or exported attribute named "hoge".

公式ドキュメントの説明

lifecycle: Lifecycle Customizations

余談: VS CodeのTerraform拡張の動作について

2019/6/24時点の情報ですが、VS CodeのTerraform拡張はまだTerraform 0.12(HCL2)に対応していません。そのため、HCL2固有の記述(First-class Expressionや新しいSplat Expression、dynamic blocks等)が存在すると、構文解析エラーになりコード補完等が効かなくなります。Terraformによる開発にVS Codeを活用している人にとっては対応が待ち遠しい感じです。(私もVS Codeを使ってます……)

HCL2パースエラーの回避方法も含めて、下記のIssueをウォッチすると良いと思います。

VS Code Terraform拡張のTerraform 0.12対応のFeatureRequest

feat: Terraform 0.12 support (was hcl2 support)

まとめ

いかがでしょうか。checklistコマンド/upgradeコマンドもかなり親切な機能とは言え万全ではないため、「TF-UPGRADE-TODO」が出なかったとしても注意した方がよいかなと思う箇所はあると感じます。

私個人は、少なくとも自分の検証で使う分のTerraformについては積極的に0.12にしていきたいという反面、VSCodeの支援を受けづらい状況にあることから、みんな!即!絶対!0.12使うべき!とまで断言できないのが正直なところです。今後、VSCode Terraform拡張等、IDEのHCL2支援対応が行われると急速に普及していくと期待してます。特に、dynamic blocksやFor構文等、今まで痒かったところに手が届く機能が追加されたことは、Terraform大好きな自分には非常に嬉しいです。

参考リンク

次の記事を読み込んでいます
次の記事を読み込んでいます
次の記事を読み込んでいます