Terraformの挙動をちょっとだけ解析してみた

Terraform使っていますか?

私はバリバリ使っています。もうTerraform無しではインフラ構築できない体になってしまいました。。。

しかし、Terraformをただ単に使っているだけでは表面的な部分を理解したに過ぎず、本質は理解できていない感がずっとありました。

というわけでTerraformの挙動を自分なりに解析して、理解を深めてみます。

※今回はGCPをベースとした解説となる点、ご了承ください。

解析してみよう

STEP1(プロバイダ設定)

まずはTerraformをはじめる第一歩として、providerを定義します。

provider "google" {}

terraform apply すると、Terraformの要となるterraform.tfstate(以下、tfstate)がこのように作成されます。

{
  "version": 4,
  "terraform_version": "0.12.24",
  "serial": 1,
  "lineage": "xxx",
  "outputs": {},
  "resources": []
}

JSONフォーマットですので、人間でも読めますね。(ホッ

STEP2(VMインスタンス作成)

ここでVMインスタンスを1つ作ってみます。main.tfに以下コードを追加します。

resource "google_compute_instance" "default" {
  name         = "test-instance"
  machine_type = "g1-small"
  zone         = "asia-northeast1-a"

  boot_disk {
    initialize_params {
      size  = 10
      type  = "pd-standard"
      image = "debian-cloud/debian-9"
    }
  }

  network_interface {
    network       = "default"
    access_config {}
  }
}

applyすると、tfstateの中身が大きく更新されます。

{
  "version": 4,
  "terraform_version": "0.12.24",
  "serial": 3,
  "lineage": "xxx",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "google_compute_instance",
      "name": "default",
      "provider": "provider.google",
      "instances": [
        {
          "schema_version": 6,
          "attributes": {
            "allow_stopping_for_update": null,
            "attached_disk": [],
            "boot_disk": [
              {
                "auto_delete": true,
                "device_name": "persistent-disk-0",
                "disk_encryption_key_raw": "",
                "disk_encryption_key_sha256": "",
                "initialize_params": [
                  {
                    "image": "https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-9-stretch-v20201216",
                    "labels": {},
                    "size": 10,
                    "type": "pd-standard"
                  }
                ],
                "kms_key_self_link": "",
                "mode": "READ_WRITE",
                "source": "https://www.googleapis.com/compute/v1/projects/xxx/zones/asia-northeast1-a/disks/test-instance"
              }
            ],
            "can_ip_forward": false,
            "confidential_instance_config": [],
            "cpu_platform": "Intel Broadwell",
            "current_status": "RUNNING",
            "deletion_protection": false,
            "description": "",
            "desired_status": null,
            "enable_display": false,
            "guest_accelerator": [],
            "hostname": "",
            "id": "projects/xxx/zones/asia-northeast1-a/instances/test-instance",
            "instance_id": "xxx",
            "label_fingerprint": "42WmSpB8rSM=",
            "labels": null,
            "machine_type": "g1-small",
            "metadata": null,
            "metadata_fingerprint": "Ff6fHQo9OT8=",
            "metadata_startup_script": "",
            "min_cpu_platform": "",
            "name": "test-instance",
            "network_interface": [
              {
                "access_config": [
                  {
                    "nat_ip": "xx.xx.xx.xx",
                    "network_tier": "PREMIUM",
                    "public_ptr_domain_name": ""
                  }
                ],
                "alias_ip_range": [],
                "name": "nic0",
                "network": "https://www.googleapis.com/compute/v1/projects/xxx/global/networks/default",
                "network_ip": "10.146.0.2",
                "subnetwork": "https://www.googleapis.com/compute/v1/projects/xxx/regions/asia-northeast1/subnetworks/default",
                "subnetwork_project": "xxx"
              }
            ],
            "project": "xxx",
            "resource_policies": null,
            "scheduling": [
              {
                "automatic_restart": true,
                "node_affinities": [],
                "on_host_maintenance": "MIGRATE",
                "preemptible": false
              }
            ],
            "scratch_disk": [],
            "self_link": "https://www.googleapis.com/compute/v1/projects/xxx/zones/asia-northeast1-a/instances/test-instance",
            "service_account": [],
            "shielded_instance_config": [],
            "tags": null,
            "tags_fingerprint": "42WmSpB8rSM=",
            "timeouts": null,
            "zone": "asia-northeast1-a"
          },
          "private": "xxx"
        }
      ]
    }
  ]
}

中身を見る限りVMインスタンスの構成情報が記載されています。そしてこの中のidあるいはinstance_idを使って、GCP上のどのインスタンスか識別可能なのではと思われます。識別に使っているのはidかinstance_idだと思われますが、idについてはインスタンス再作成で同じものにできてしまうので、おそらくinstance_idでしょうか。(あくまでも個人的推測)

STEP3(オプション変更)

main.tfにパラメータを1つ追加してみます。(+ が追加分)

ちなみにこのパラメータは後ほどTerraformからマシンタイプを変更するために必要なものです。

  zone         = "asia-northeast1-a"
+ allow_stopping_for_update = true

  boot_disk {

applyし、terraform.tfstateの差分を確認します。

すると、serialの番号がインクリメントされ(2つインクリメントされるのは要確認)、今回変更したallow_stopping_for_updateがtrueになっています。

   "terraform_version": "0.12.24",
-  "serial": 3,
+  "serial": 5,
   "lineage": "xxx",
   "outputs": {},
   "resources": [
         {
           "schema_version": 6,
           "attributes": {
-            "allow_stopping_for_update": null,
+            "allow_stopping_for_update": true,
             "attached_disk": [],

STEP4(マシンタイプ変更)

VMインスタンスのマシンタイプを変更してみます。(- が削除分。-+なので実際には置換)

 resource "google_compute_instance" "default" {
   name         = "test-instance"
-  machine_type = "g1-small"
+  machine_type = "f1-micro"
   zone         = "asia-northeast1-a"

apply し、terraform.tfstateに発生する差分を確認します。

 {
   "version": 4,
   "terraform_version": "0.12.24",
-  "serial": 5,
+  "serial": 7,
   "lineage": "xxx",
・
・
・
             "label_fingerprint": "42WmSpB8rSM=",
             "labels": {},
-            "machine_type": "g1-small",
+            "machine_type": "f1-micro",
             "metadata": {},
・
・
・
                 "access_config": [
                   {
-                    "nat_ip": "xx.xx.xx.xx",
+                    "nat_ip": "yy.yy.yy.yy",
                     "network_tier": "PREMIUM",

serialの番号がさらにインクリメントされ、マシンタイプがtfファイルで指定したものに変わりました。そして今回のVMインスタンスは公開用IPアドレスを固定にしていないので、別のIPアドレスに変わっています。

STEP5(管理画面から設定変更)

組織的にTerraformを使うという文化が醸成されていない場合に起こりがちですが、Terraformを使わずに管理画面等から直接設定変更してしまった場合にどうなるのかを試してみました。

直接設定変更したケースとして、管理画面からVMインスタンスのマシンタイプを変更してみました。(f1-micro→g1-small)

さて、ここでapplyすると、どうなるでしょうか?

2パターン考えられます。

  1. tfstateを正とし、tfファイルとの差異が無いので何も起きない。
  2. 実際のVMインスタンスの設定を正とし、tfファイルとの差異があるので、マシンタイプが変更される。

では、実際にapplyしてみましょう。

google_compute_instance.default: Refreshing state... [id=projects/xxx/zones/asia-northeast1-a/instances/test-instance]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # google_compute_instance.default will be updated in-place
  ~ resource "google_compute_instance" "default" {
        allow_stopping_for_update = true
        can_ip_forward            = false
        cpu_platform              = "Intel Broadwell"
        current_status            = "RUNNING"
        deletion_protection       = false
        enable_display            = false
        guest_accelerator         = []
        id                        = "projects/xxx/zones/asia-northeast1-a/instances/test-instance"
        instance_id               = "xxx"
        label_fingerprint         = "42WmSpB8rSM="
        labels                    = {}
      ~ machine_type              = "g1-small" -> "f1-micro"
        metadata                  = {}
・
・
・
    }

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

答えは”2″でした。tfstateの中身は無視し、実際のVMインスタンスの設定が優先され、マシンタイプが変更されます。

逆に実際のVMインスタンスの設定を優先し、tfファイルを実態にあわせてみます。

 resource "google_compute_instance" "default" {
   name         = "test-instance"
-  machine_type = "f1-micro"
+  machine_type = "g1-small"
   zone         = "asia-northeast1-a"

applyすると、VMインスタンスに対しては何も起きません。その代わり、tfstateは実態にあわせるかたちに更新されます。

 {
   "version": 4,
   "terraform_version": "0.12.24",
-  "serial": 7,
+  "serial": 8,
   "lineage": "xxx",
   "outputs": {},
   "resources": [
・
・
・
             "instance_id": "xxx",
             "label_fingerprint": "42WmSpB8rSM=",
             "labels": {},
-            "machine_type": "f1-micro",
+            "machine_type": "g1-small",

この挙動を使って、もしtfファイルと実際のVMインスタンスとの間に乖離があったとしても、tfファイルを調整することで実態に合わせることが可能となります。

しかし、この手法だと結構な手間がかかるので、大量のVMインスタンスに対して行う場合は、以前紹介したterraformer等のエクスポートツールを使うのがベターだと思います。

まとめ

今回はTerraformについての理解を深めるために、Terraformの挙動について少しだけ解析してみました。これをきっかけにTerraformのソースに目を通してみたいと思います。(それはまた別の記事で)