👔

Terraform State 生成 (import/aztfexport/番外編: Data Sources) at Azure

に公開

はじめに

Terraform の State ファイルを生成する方法についてまとめた記事です。
なお、初学者なため手法や手順についてはもっと良い方法があるかもしれません。

既に構築済みの環境や VNET/VM 等ある程度のリソースが払い出される環境、手出しできない環境と共存している等 Terraform で構築していない環境において、後から Terraform で管理する場合 リソース定義である tf ファイルとともに tfstate (State) ファイルも必要になります。

State ファイルを生成する方法である Import コマンド、Import ブロック、Azure Export for Terraform (aztfexport) の3つの使用方法と使用してみた感想を記述します。

番外編として、そもそも手出しできない環境やリソースを含むならば Data Sources (data ブロック) で参照するが適当かと思うのでその方法についても記載します。
IaC とは、、、というのは一旦おいておきます。

なお、aztfexport は Bicep 生成とともに Azure ポータルと統合されるんじゃないかと思っています。

import

以下の terraform import コマンドを使用する。
terraform import [Terraformリソース定義.変数名] [リソースID]

  • import コマンドにより、バックエンドに tfstate (State) が生成される。
    • よって、生成された tfstate ファイルを確認し、必要な値を抜き出し、tf ファイルを完成させなければならない。
    • import コマンドはリソース一つ一つに対して実行しなければならない。
  • つらい。そもそも公式では import blocks があるよと記載されている。以下略。

▼ 公式ドキュメントと分かりやすい解説記事
https://developer.hashicorp.com/terraform/cli/import
https://zenn.dev/microsoft/articles/20240322-terraform-import-azure

import block

import コマンドでは一つ一つコマンドを入力しなければならなかったが、import block では任意 tf ファイルに複数記述でき、且つ tf ファイル自体を生成できる (不要なプロパティも生成される)。
Terraform v1.5.0 以降 import block が使えるが、import block 自体は Experimental で生成される構文については将来変更される可能性がある。
https://developer.hashicorp.com/terraform/language/import

  • 任意 tf ファイルに import {} で複数リソースを定義しておきコマンド実行する。
import {
    id = リソース ID
    to = Terraform リソース名.変数名
}
  • terraform plan -generate-config-out="import-main.tf" を実行すると、指定したファイル名で tf ファイルが生成される。
    • import コマンドのように手書きではなく、記載した分の resource ブロックを自動生成してくれる機能。
    • 指定したファイル名で生成されるので、同名のファイルがあった場合エラーとなる。
    • リソース毎に import block の記述が必要になる。
resource "azurerm_resource_group" "rg" {
    name = "rg-name"
    location ="japanwest"
    # (other resource arguments...)
}

以下リソースグループ内のリソースを import block を使用して tf/tfstate ファイルを生成する。

  1. import block 用のファイルとして、importblock-main.tf を用意する
  2. 上記ファイルにリソース分の import block を記述する
    ・Azure のリソース ID を調査しておく。
    ・Terraform に該当するリソース名と変数名を指定する。
    ※VM の OS Disk は不要だった。Extension も import 可能。
# Resource Group
import {
    id = "/subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish"
    to = azurerm_resource_group.rg
}
# VNet
import {
    id = "/subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish/providers/Microsoft.Network/virtualNetworks/
win-vm-iis-monkfish-vnet"
    to = azurerm_virtual_network.my_vnet
}
# Subnet
import {
    id = "/subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish/providers/Microsoft.Network/virtualNetworks/
win-vm-iis-monkfish-vnet/subnets/win-vm-iis-monkfish-subnet"
    to = azurerm_subnet.my_subnet 
}

~略~

# VM Extension
import {
    id = "/subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish/providers/Microsoft.Compute/virtualMachines/win-vm-iis-vm/extensions/win-vm-iis-monkfish-wsi"
    to = azurerm_virtual_machine_extension.web_server_install
}
  1. terraform plan -generate-config-out="import-main.tf" を実行する

  2. tf ファイルが生成されるが、CLI にエラーが表示されているので修正する

  3. terraform plan でエラーが出なくなるまで修正する = tf ファイルが完成する
    ※VM パスワードを修正、VNETからサブネットを除外等



  4. terraform apply で tfstate ファイルを生成する

  5. "importblock-main.tf" を削除する

所感

  • 任意のリソース分生成されるのは良い場面があると想定される (大量リソースなど)。
  • リソースの変数名が指定できて良い。
  • 各リソースのプロパティが大量に出力されるため、必須/不要の編集に工数がかかる。
    • 変数化し別ファイルにするためには工数がかかる。
  • もちろん tf ファイルに設定変更や新規リソース追加することも出来る (こっちが本題であるが…)。

aztfexport

Microsoft が提供する Terraform 移行ツール。2025/01 時点のバージョンは v0.15。
https://learn.microsoft.com/ja-jp/azure/developer/terraform/azure-export-for-terraform/export-terraform-overview

Azure Export for Terraform を使用すると、単一のコマンドで Azure リソースを Terraform に移行できます。
単一のコマンドで、ユーザー指定のリソース セットを Terraform HCL コードと状態にエクスポートします。

上記の通り、aztfexport コマンドで tf/tfstate ファイル双方を生成できる。
単一リソース、リソースグループ、Resource Graphの3種類を指定して生成可能。

ここでは、import block でも使用したリソースグループを対象として tf/tfstate ファイルを生成する。

  1. Azure Export for Terraform (aztfexport.exe) をダウンロードしてPATHを通す

https://github.com/Azure/aztfexport/releases

  1. aztfexport でエクスポートする
    aztfexport resource-group rg-win-vm-iis-monkfish
    ・このとき、空のディレクトリにて実行する (ファイルやフォルダがあると警告が出る)。
    ・CUI が表示され、リソースグループ内のリソース一覧が表示される。
    ・フィルターや取り込まないようスキップすることが可能。

    ・w でインポート開始する。数秒で完了した。


    ・以下のファイルが生成された。このとき、ローカルに tfstate が生成される。

    ※aztfexport コマンドの引数にバックエンドを設定することでリモートバックエンドにエクスポート可能。

https://learn.microsoft.com/ja-jp/azure/developer/terraform/azure-export-for-terraform/export-advanced-scenarios#inline-experience

  1. 生成されたファイルを確認する。

・terraform.tf

terraform {
  backend "local" {}
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "3.99.0"
    }
  }
}

・provider.tf

provider "azurerm" {
  features {
  }
  skip_provider_registration = true
  subscription_id            = "xxxxxxxxxx"
  environment                = "public"
  use_msi                    = false
  use_cli                    = true
  use_oidc                   = false
}

・main.tf:変数名は「res-数字」となる。

resource "azurerm_resource_group" "res-0" {
  location = "japanwest"
  name     = "rg-win-vm-iis-monkfish"
}
resource "azurerm_windows_virtual_machine" "res-1" {
  admin_password        = "ignored-as-imported"
  admin_username        = "azureuser"
  location              = "japanwest"
  name                  = "win-vm-iis-vm"
  network_interface_ids = ["/subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish/providers/Microsoft.Network/networkInterfaces/win-vm-iis-monkfish-nic"]
  resource_group_name   = "rg-win-vm-iis-monkfish"
  size                  = "Standard_DS1_v2"
  boot_diagnostics {
    storage_account_uri = "https://xxxxxxxxxx.blob.core.windows.net/"
  }
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Premium_LRS"
  }
  source_image_reference {
    offer     = "WindowsServer"
    publisher = "MicrosoftWindowsServer"
    sku       = "2022-datacenter-azure-edition"
    version   = "latest"
  }
  depends_on = [
    azurerm_network_interface.res-3,
  ]
}
resource "azurerm_virtual_machine_extension" "res-2" {
  auto_upgrade_minor_version = true
  name                       = "win-vm-iis-monkfish-wsi"
  publisher                  = "Microsoft.Compute"
  settings = jsonencode({
    commandToExecute = "powershell -ExecutionPolicy Unrestricted Install-WindowsFeature -Name Web-Server -IncludeAllSubFeature -IncludeManagementTools"
  })
  type                 = "CustomScriptExtension"
  type_handler_version = "1.8"
  virtual_machine_id   = "/subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish/providers/Microsoft.Compute/virtualMachines/win-vm-iis-vm"
  depends_on = [
    azurerm_windows_virtual_machine.res-1,
  ]
}
~略~

・aztfexportResourceMapping.json:リソースIDとTerraformの変数名のマッピングが記載されている。

{
  "/subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish": {
    "resource_id": "/subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish",
    "resource_type": "azurerm_resource_group",
    "resource_name": "res-0"
  },
  "/subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish/providers/Microsoft.Compute/virtualMachines/win-vm-iis-vm": {
    "resource_id": "/subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish/providers/Microsoft.Compute/virtualMachines/win-vm-iis-vm",
    "resource_type": "azurerm_windows_virtual_machine",
    "resource_name": "res-1"
  },
  "/subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish/providers/Microsoft.Compute/virtualMachines/win-vm-iis-vm/extensions/win-vm-iis-monkfish-wsi": {
    "resource_id": "/subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish/providers/Microsoft.Compute/virtualMachines/win-vm-iis-vm/extensions/win-vm-iis-monkfish-wsi",
    "resource_type": "azurerm_virtual_machine_extension",
    "resource_name": "res-2"
  },
~略~

・aztfexportSkippedResources.txt:CUIでスキップしたリソースが記載されている。

Following resources are marked to be skipped:

- /subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish/providers/Microsoft.Network/networkSecurityGroups/win-vm-iis-monkfish-nsg/securityRules/web
- /subscriptions/xxxxxxxxxx/resourceGroups/rg-win-vm-iis-monkfish/providers/Microsoft.Storage/storageAccounts/xxxxxxxxxx/blobServices/default
~略~

・terraform.tfstate

  1. terraform init / terraform plan してみる
    ・VM パスワードの要件のみエラーとなったため、パスワードをダミーに修正する。
    ・リモートバックエンドに修正する。

  2. 再度 terraform init / terraform plan してみる
    ・backend が変わった旨エラーとなっている。

  3. 既存のステートをリモートに持っていきたいので、terraform init -migrate-state を実行する

https://developer.hashicorp.com/terraform/cli/commands/init#backend-initialization

  1. terraform plan を実行する。何もエラーは発生しなかった。

所感

  • tf ファイルを用意等せず、コマンド 1 行で各ファイル毎に分割して生成されるのは import block と比較してかなり便利。
  • CUI でフィルターできる、取り込まないよう選択できるのも便利。
  • 出力されるプロパティが最小限に見える。編集の時間も削減できそう。
  • リソースの変数名が指定できなかった点や変数化し別ファイル化にするには工数がかかる。
  • もちろん tf ファイルに設定変更や新規リソース追加することも出来る (こっちが本題であるが…)。

▼ 分かりやすい解説記事
https://azure.github.io/jpazpaas/2024/04/09/arm-Azure-Export-for-Terraform.html
https://zenn.dev/yuta28/articles/azure-export-terraform
https://zenn.dev/microsoft/articles/20240412-aztfexp

Data Sources

番外編として、そもそも手出しできない環境やリソースを含むならば Data Sources (data ブロック) で参照する方が適当かと思うのでその方法についても記載します。

  • Data Sources とは
    • 既存リソースを data block にて定義し参照する
      • 既存リソースの tfstate ファイルを生成する必要がない。
      • 既存リソースが管理できないリソースであって設定が一部変わったとしても、ほぼ関係ない (と思われる)。
      • 既存リソースの tfstate を保持管理していないので terraform apply でコケない (と思われる)。
    • 新規リソースの追加に必要な既存リソースの参照を data block にて行う。
  • 後は通常通り新規リソースの定義を行い、terraform planterraform applyする

▼ 公式ドキュメント
https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/resource_group
https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/resources

先程と同様に、import block でも使用したリソースグループを対象として新規リソースの追加を行う。

  1. main.tf に既存リソースの data block を用意する (必要な参照のみ且つ一意になるように指定する)

    data "azurerm_resource_group" "res-rg" {
        name = "rg-win-vm-iis-monkfish"
    }
    
    data "azurerm_virtual_network" "res-vnet" {
        resource_group_name = data.azurerm_resource_group.res-rg.name
        name = "win-vm-iis-monkfish-vnet"
    } 
    
    data "azurerm_network_interface" "res-nic" {
        resource_group_name = data.azurerm_resource_group.res-rg.name
        name = "win-vm-iis-monkfish-nic"
    }
    
  2. main.tf に Application Gateway の追加を行う (data block を参照する)

    variable "backend_address_pool_name" {
        default = "myBackendPool"
    }
    variable "frontend_port_name" {
        default = "myFrontendPort"
    }    
    variable "frontend_ip_configuration_name" {
        default = "myAGIPConfig"
    }    
    variable "http_setting_name" {
        default = "myHTTPsetting"
    }    
    variable "listener_name" {
        default = "myListener"
    }    
    variable "request_routing_rule_name" {
        default = "myRoutingRule"
    }
    
    resource "azurerm_subnet" "frontend" {
        name                 = "myAGSubnet"
        resource_group_name  = data.azurerm_resource_group.res-rg.name
        virtual_network_name = data.azurerm_virtual_network.res-vnet.name
        address_prefixes     = ["10.0.10.0/24"]
    }
    resource "azurerm_public_ip" "pip" {
        name                = "myAGPublicIPAddress"
        resource_group_name = data.azurerm_resource_group.res-rg.name
        location            = data.azurerm_resource_group.res-rg.location
        allocation_method   = "Static"
        sku                 = "Standard"
    }
    resource "azurerm_application_gateway" "main" {
        name                = "myAppGateway"
        resource_group_name = data.azurerm_resource_group.res-rg.name
        location            = data.azurerm_resource_group.res-rg.location
        sku {
            name     = "Standard_v2"
            tier     = "Standard_v2"
            capacity = 2
        }
        gateway_ip_configuration {
            name      = "my-gateway-ip-configuration"
            subnet_id = azurerm_subnet.frontend.id
        }
        frontend_port {
            name = var.frontend_port_name
            port = 80
        }
        frontend_ip_configuration {
            name                 = var.frontend_ip_configuration_name
            public_ip_address_id = azurerm_public_ip.pip.id
        }
        backend_address_pool {
            name = var.backend_address_pool_name
        }
        backend_http_settings {
            name                  = var.http_setting_name
            cookie_based_affinity = "Disabled"
            port                  = 80
            protocol              = "Http"
            request_timeout       = 60
        }
        http_listener {
            name                           = var.listener_name
            frontend_ip_configuration_name = var.frontend_ip_configuration_name
            frontend_port_name             = var.frontend_port_name
            protocol                       = "Http"
        }
        request_routing_rule {
            name                       = var.request_routing_rule_name
            rule_type                  = "Basic"
            http_listener_name         = var.listener_name
            backend_address_pool_name  = var.backend_address_pool_name
            backend_http_settings_name = var.http_setting_name
            priority                   = 1
        }
    }
    resource "azurerm_network_interface_application_gateway_backend_address_pool_association" "nic-assoc" {
        network_interface_id    = data.azurerm_network_interface.res-nic.id
        ip_configuration_name   = "terraform_work3_nic_configuration"
        backend_address_pool_id = one(azurerm_application_gateway.main.backend_address_pool).id
    }
    
  3. terraform plan を実行する

  4. terraform apply を実行する

  5. リソースが作成された

  6. tfstate を確認する

  7. Application Gateway の Public IP に HTTP でアクセスすると IIS が動作していることが確認できる

所感

  • 管理できないリソースの tfstate は作成してもハマるだけと思われる
  • よって Data Sources を利用することが望ましいと思われる
  • 大規模な場合、tfstate を分割して terraform_remote_state で参照することで衝突を回避するのが解決策なようである

この記事が参考になれば幸甚です。

Discussion