Terraform で GCE インスタンスを作成する際に initialize_params を避けるべき理由
こんにちは、クラウドエース SRE 部の阿部です。
この記事では、Terraform で Google Compute Engine インスタンス(以降、GCE インスタンス)を作成する際のベストプラクティスについて紹介します。特に、initialize_params
の使用を避ける理由とその代替方法について説明します。
先日、弊社の海外メンバーと Terraform の書き方について議論になった際に、 GCE インスタンスのパラメータを変更すると強制再作成となってしまい、メンテナンスのときに困っていると相談を受けて、この記事を書くことを思いつきました。
結論
GCE インスタンスのブートディスクを定義する際には、以下の点に注意してください。
-
google_compute_instance
リソースのboot_disk
ブロックでinitialize_params
を使ってブートディスクを定義してはいけない - 代わりにブートディスクに相当する
google_compute_disk
リソースを定義し、google_compute_instance
リソースのboot_disk.source
で参照する -
boot_disk.auto_delete
はfalse
に設定する
検証環境
当記事で使用するサンプルコードは以下のバージョンで動作確認しました。
- OS: Windows 11 (Ubuntu 24.04 LTS on WSL)
- Terraform CLI: v1.9.3
- Google Cloud Provider: v5.38.0
なお、当記事の内容は特定バージョンに依存する動作ではないため、参考程度としてください。
よくあるサンプルコード
以下は Terraform Registry の Google Cloud Provider ドキュメントに基づいた google_compute_instance のサンプルコードです。
このコードは、initialize_params
を使用してブートディスクを定義しています。
resource "google_compute_instance" "default" {
name = "my-instance"
machine_type = "n2-standard-2"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-11"
size = 10
}
}
network_interface {
network = "default"
access_config {
// Ephemeral public IP
}
}
}
このコードを terraform apply
すると、 my-instance
という名前の GCE インスタンスが us-central1-a
ゾーンに作成されます。
問題点
initialize_params
を使用してブートディスクを定義すると、ディスクサイズの変更などの操作が forces replacement
となり、インスタンスの再作成が必要になります。これにより、インスタンスのデータが消失するリスクがあります。
例えば、インスタンスのブートディスクの使用率が高くなったため、ディスクサイズを 20 GB に拡張しようと考えたときにどうするのがよいでしょうか?
率直に考えると boot_disk.initialize_params.size
の数値を 10 から 20 に変更するのではないかと思います。
公式ドキュメントの「ファイル システムとパーティションのサイズを変更する」に記載されている通り、GCE インスタンスはオンラインでディスクサイズを増加可能です。
しかし、サンプルコードの size
を変更して terraform apply
を実行すると、以下のように forces replacement
になってしまいます。
~ initialize_params {
- enable_confidential_compute = false -> null
~ image = "https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-11-bullseye-v20240709" -> "debian-cloud/debian-11"
~ labels = {} -> (known after apply)
~ provisioned_iops = 0 -> (known after apply)
~ provisioned_throughput = 0 -> (known after apply)
- resource_manager_tags = {} -> null
~ size = 10 -> 20 # forces replacement
~ type = "pd-standard" -> (known after apply)
}
差分メッセージ全体
google_compute_instance.default: Refreshing state... [id=projects/example-project/zones/us-central1-a/instances/my-instance]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement
Terraform will perform the following actions:
# google_compute_instance.default must be replaced
-/+ resource "google_compute_instance" "default" {
~ cpu_platform = "Intel Cascade Lake" -> (known after apply)
~ current_status = "RUNNING" -> (known after apply)
~ effective_labels = {} -> (known after apply)
- enable_display = false -> null
~ guest_accelerator = [] -> (known after apply)
~ id = "projects/example-project/zones/us-central1-a/instances/my-instance" -> (known after apply)
~ instance_id = "1186162411377179530" -> (known after apply)
~ label_fingerprint = "42WmSpB8rSM=" -> (known after apply)
- labels = {} -> null
- metadata = {} -> null
~ metadata_fingerprint = "dtIz4d76InM=" -> (known after apply)
+ min_cpu_platform = (known after apply)
name = "my-instance"
- resource_policies = [] -> null
~ self_link = "https://www.googleapis.com/compute/v1/projects/example-project/zones/us-central1-a/instances/my-instance" -> (known after apply)
- tags = [] -> null
~ tags_fingerprint = "42WmSpB8rSM=" -> (known after apply)
~ terraform_labels = {} -> (known after apply)
# (7 unchanged attributes hidden)
~ boot_disk {
~ device_name = "persistent-disk-0" -> (known after apply)
+ disk_encryption_key_sha256 = (known after apply)
+ kms_key_self_link = (known after apply)
~ source = "https://www.googleapis.com/compute/v1/projects/example-project/zones/us-central1-a/disks/my-instance" -> (known after apply)
# (3 unchanged attributes hidden)
~ initialize_params {
- enable_confidential_compute = false -> null
~ image = "https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-11-bullseye-v20240709" -> "debian-cloud/debian-11"
~ labels = {} -> (known after apply)
~ provisioned_iops = 0 -> (known after apply)
~ provisioned_throughput = 0 -> (known after apply)
- resource_manager_tags = {} -> null
~ size = 10 -> 20 # forces replacement
~ type = "pd-standard" -> (known after apply)
}
}
~ confidential_instance_config {
+ allow_stopping_for_update = (known after apply)
+ can_ip_forward = (known after apply)
+ cpu_platform = (known after apply)
+ current_status = (known after apply)
+ deletion_protection = (known after apply)
+ description = (known after apply)
+ desired_status = (known after apply)
+ effective_labels = (known after apply)
+ enable_display = (known after apply)
+ guest_accelerator = (known after apply)
+ hostname = (known after apply)
+ id = (known after apply)
+ instance_id = (known after apply)
+ label_fingerprint = (known after apply)
+ labels = (known after apply)
+ machine_type = (known after apply)
+ metadata = (known after apply)
+ metadata_fingerprint = (known after apply)
+ metadata_startup_script = (known after apply)
+ min_cpu_platform = (known after apply)
+ name = (known after apply)
+ project = (known after apply)
+ resource_policies = (known after apply)
+ self_link = (known after apply)
+ tags = (known after apply)
+ tags_fingerprint = (known after apply)
+ terraform_labels = (known after apply)
+ zone = (known after apply)
} -> (known after apply)
~ network_interface {
~ internal_ipv6_prefix_length = 0 -> (known after apply)
+ ipv6_access_type = (known after apply)
+ ipv6_address = (known after apply)
~ name = "nic0" -> (known after apply)
~ network = "https://www.googleapis.com/compute/v1/projects/example-project/global/networks/default" -> "default"
~ network_ip = "10.128.0.54" -> (known after apply)
- queue_count = 0 -> null
~ stack_type = "IPV4_ONLY" -> (known after apply)
~ subnetwork = "https://www.googleapis.com/compute/v1/projects/example-project/regions/us-central1/subnetworks/default" -> (known after apply)
~ subnetwork_project = "example-project" -> (known after apply)
# (1 unchanged attribute hidden)
~ access_config {
~ nat_ip = "xxx.xxx.xxx.xxx" -> (known after apply)
~ network_tier = "PREMIUM" -> (known after apply)
# (1 unchanged attribute hidden)
}
}
~ reservation_affinity {
+ allow_stopping_for_update = (known after apply)
+ can_ip_forward = (known after apply)
+ cpu_platform = (known after apply)
+ current_status = (known after apply)
+ deletion_protection = (known after apply)
+ description = (known after apply)
+ desired_status = (known after apply)
+ effective_labels = (known after apply)
+ enable_display = (known after apply)
+ guest_accelerator = (known after apply)
+ hostname = (known after apply)
+ id = (known after apply)
+ instance_id = (known after apply)
+ label_fingerprint = (known after apply)
+ labels = (known after apply)
+ machine_type = (known after apply)
+ metadata = (known after apply)
+ metadata_fingerprint = (known after apply)
+ metadata_startup_script = (known after apply)
+ min_cpu_platform = (known after apply)
+ name = (known after apply)
+ project = (known after apply)
+ resource_policies = (known after apply)
+ self_link = (known after apply)
+ tags = (known after apply)
+ tags_fingerprint = (known after apply)
+ terraform_labels = (known after apply)
+ zone = (known after apply)
} -> (known after apply)
~ scheduling {
+ allow_stopping_for_update = (known after apply)
+ can_ip_forward = (known after apply)
+ cpu_platform = (known after apply)
+ current_status = (known after apply)
+ deletion_protection = (known after apply)
+ description = (known after apply)
+ desired_status = (known after apply)
+ effective_labels = (known after apply)
+ enable_display = (known after apply)
+ guest_accelerator = (known after apply)
+ hostname = (known after apply)
+ id = (known after apply)
+ instance_id = (known after apply)
+ label_fingerprint = (known after apply)
+ labels = (known after apply)
+ machine_type = (known after apply)
+ metadata = (known after apply)
+ metadata_fingerprint = (known after apply)
+ metadata_startup_script = (known after apply)
+ min_cpu_platform = (known after apply)
+ name = (known after apply)
+ project = (known after apply)
+ resource_policies = (known after apply)
+ self_link = (known after apply)
+ tags = (known after apply)
+ tags_fingerprint = (known after apply)
+ terraform_labels = (known after apply)
+ zone = (known after apply)
} -> (known after apply)
- shielded_instance_config {
- enable_integrity_monitoring = true -> null
- enable_secure_boot = false -> null
- enable_vtpm = true -> null
}
}
Plan: 1 to add, 0 to change, 1 to destroy.
何故このような動作になるのか
何故 initialize_params
のパラメータを変更すると forces replacement
と判断されるかというと、 Google Cloud Provider の実装としてパラメータが変更が発生したときにそうなるよう設定されているためです。
(下記ソースコードの ForceNew: true
が該当ロジックです。)
ただ、先にも述べたとおり、 GCE インスタンスのディスクサイズはオンライン変更可能なため、Google Cloud Provider で forces replacement
とするのは少しおかしいのではないかと思うかもしれません。
実はインスタンスが更新できるプロパティでは、ブートディスクは更新対象外になっています。
実際に、Compute Engine API を直接実行してブートディスクの initializeParams.diskSizeGb
を更新すると以下のようなエラーになってしまいます。
"error": {
"code": 400,
"message": "Invalid value for field 'resource.disks[0]': '{ \"initializeParams\": { \"diskSizeGb\": \"20\" }}'. Boot disk must be the first disk attached to the instance.",
"errors": [
{
"message": "Invalid value for field 'resource.disks[0]': '{ \"initializeParams\": { \"diskSizeGb\": \"20\" }}'. Boot disk must be the first disk attached to the instance.",
"domain": "global",
"reason": "invalid"
}
]
}
}
そのため、Google Cloud Provider では initialize_params
の変更は forces replacement
になるように実装されているのだと思います。
改善したサンプルコード
では、どのように実装すればブートディスクサイズを Terraform 上で変更できるようになるかというと、 google_compute_instance
とは別にブートディスクとして google_compute_disk
リソースを定義します。
google_compute_instance
では、 boot_disk.source
で google_compute_disk
を参照するように設定します。
例えば、前述のサンプルを以下のように修正します。
resource "google_compute_instance" "default" {
name = "my-instance"
machine_type = "n2-standard-2"
zone = "us-central1-a"
boot_disk {
auto_delete = false
source = google_compute_disk.default.self_link
}
network_interface {
network = "default"
access_config {
// Ephemeral public IP
}
}
}
resource "google_compute_disk" "default" {
name = "my-instance"
zone = "us-central1-a"
image = "debian-cloud/debian-11"
size = 10
}
--- instances_before.tf 2024-07-29 14:16:20.266982950 +0900
+++ instances.tf 2024-07-29 14:09:25.606229671 +0900
@@ -4,10 +4,8 @@
zone = "us-central1-a"
boot_disk {
- initialize_params {
- image = "debian-cloud/debian-11"
- size = 20
- }
+ auto_delete = false
+ source = google_compute_disk.default.self_link
}
network_interface {
@@ -18,3 +16,15 @@
}
}
}
+
+resource "google_compute_disk" "default" {
+ name = "my-instance"
+ zone = "us-central1-a"
+ image = "debian-cloud/debian-11"
+ size = 20
+}
+
+import {
+ id = "ca-abe-test/us-central1-a/my-instance"
+ to = google_compute_disk.default
+}
このように設定しておけば、 initialize_params
の動作仕様に悩まされることなくブートディスクのサイズ変更が可能です。
試しに google_compute_disk
の size
を変更すると以下のようになります。想定通り、インプレース更新になっておりディスクのデータは保持されます。
差分メッセージ
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# google_compute_disk.default will be updated in-place
~ resource "google_compute_disk" "default" {
id = "projects/example-project/zones/us-central1-a/disks/my-instance"
name = "my-instance"
~ size = 10 -> 20
# (26 unchanged attributes hidden)
# (4 unchanged blocks hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
注意事項
必ず google_compute_instance
の boot_disk.auto_delete
に false
を設定しましょう。
boot_disk.auto_delete
はデフォルト true
になっていますが、これを設定し忘れると google_compute_instance
が Destroy されるタイミングで依存関係になっているブートディスクも削除されてしまうため、 google_compute_disk
が意図しないタイミングで削除されてしまいます。
boot_disk.source
と boot_disk.auto_delete
はセットで設定しましょう。
既に initialize_params で GCE インスタンスを構築している場合
既に initialize_params
を使って構築している場合はどうするかというと、ブートディスクをインポートして google_compute_disk
を設定すればよいです。
ただし、現状の Google Cloud Provider では、 auto_delete
を変更しようとすると forces replacement
が発生してしまうため、事前に以下のコマンドで auto_delete
を false
にする必要があります。
gcloud compute instances set-disk-auto-delete インスタンス名 --no-auto-delete --disk ブートディスク名 --zone ゾーン名
今回のサンプルコードであれば以下のコマンドを実行します。
gcloud compute instances set-disk-auto-delete my-instance --no-auto-delete --disk my-instance --zone us-central1-a
上記コマンドを実行後に、 import
ブロックを使ってブートディスクリソースをインポートしながらコード修正すれば OK です。
resource "google_compute_instance" "default" {
name = "my-instance"
machine_type = "n2-standard-2"
zone = "us-central1-a"
boot_disk {
auto_delete = false
source = google_compute_disk.default.self_link
}
network_interface {
network = "default"
access_config {
// Ephemeral public IP
}
}
}
resource "google_compute_disk" "default" {
name = "my-instance"
zone = "us-central1-a"
image = "debian-cloud/debian-11"
size = 10
}
import {
id = "example-project/us-central1-a/my-instance"
to = google_compute_disk.default
}
まとめ
あらかじめ initialze_params
の動作を理解していればコーディング規約や Linter で防ぐことが可能です。
しかし、世間で公開されている GCE インスタンスのサンプルコードの中には、initialize_params
を使用しているケースが散見されます。
そのため、初めて Terraform で GCE インスタンスを構築する方は、この落とし穴にはまりやすいと感じました。
この記事が、Terraform で GCE インスタンスを構築する方のお役に立ちましたら幸いです。
Discussion