⛩︎

OpenTofu で Azureリソースを構築してみる

2024/04/26に公開

準備

Azure を利用する。利用する変数を宣言している。

init.tf
terraform {
  required_version = ">=0.12"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~>3.50"
    }
    http = {
      source  = "hashicorp/http"
      version = "~> 3.0"
    }
  }
  backend "azurerm" {
    resource_group_name  = "management"
    storage_account_name = "<storage account name>"
    container_name       = "<container name>"
    key                  = "terraform.tfstate"
  }
}

provider "azurerm" {
  features {}
}
provider http {
}

data "http" "getaddress" {
  url = "https://example.net/getaddress/"
}

locals {
  allowed_address_prefix_ssh = jsondecode(data.http.getaddress.response_body).sourceip
}

variable "subscription" {}
variable "environment" {}
variable "location" {}
variable "address_space" {}
variable "subnet1_prefixes" {}

リソースグループ、VNET、サブネット、NSGを作成する。
サブネットにNSGを割り当てる。

vnet.tf
resource "azurerm_resource_group" "rg" {                                                                                                                                                                                            [29/1857]
  name = var.environment
  location = var.location
  tags = {
    environment = var.environment
  }
}

resource "azurerm_virtual_network" "vnet" {
  name = "${var.environment}_vnet"
  location = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  address_space = var.address_space
  # dns_servers = var.dns_servers
  tags = {
    environment = var.environment
  }
}

resource "azurerm_subnet" "subnet1" {
  name = "${var.environment}_subnet1"
  resource_group_name = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes = var.subnet1_prefixes
  service_endpoints = ["Microsoft.Storage"]
}

resource "azurerm_network_security_group" "subnet1_nsg" {
  name = "${var.environment}_nsg"
  location = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  security_rule {
    name = "allow_inbound_ssh"
    priority = 200
    direction = "Inbound"
    access = "Allow"
    protocol = "Tcp"
    source_port_range = "*"
    destination_port_range = "22"
    destination_address_prefix = "*"
    source_address_prefix = local.allowed_address_prefix_ssh
  }
  security_rule {
    name = "deny_inbound_all"
    priority = 4096
    direction = "Inbound"
    access = "Deny"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    destination_address_prefix = "*"
    source_address_prefix = "*"
  }
  security_rule {
    name = "allow_outbound_internet"
    priority = 3700
    direction = "Outbound"
    access = "Allow"
    protocol = "Tcp"
    source_port_range = "*"
    destination_port_ranges = ["80","443"]
    destination_address_prefix = "Internet"
    source_address_prefix = "*"
  }
  security_rule {
    name = "deny_outbound_all"
    priority = 4096
    direction = "Outbound"
    access = "Deny"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    destination_address_prefix = "*"
    source_address_prefix = "*"
  }
  tags = {
    environment = var.environment
  }
}

resource "azurerm_subnet_network_security_group_association" "nsg_association" {
  subnet_id = azurerm_subnet.subnet1.id
  network_security_group_id = azurerm_network_security_group.subnet1_nsg.id
}  

変数を記載する。

parameters.tfvars
subscription = "<subscription id>"
environment = "<environment name>"
location = "japaneast"
address_space = ["10.0.0.0/16"]
subnet1_prefixes = ["10.0.0.0/24"]

init

$ tofu init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/azurerm versions matching "~> 3.50"...
- Reusing previous version of hashicorp/http from the dependency lock file
- Installing hashicorp/azurerm v3.100.0...
- Installed hashicorp/azurerm v3.100.0 (signed, key ID 0C0AF313E5FD9F80)
- Using previously-installed hashicorp/http v3.4.2

Providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://opentofu.org/docs/cli/plugins/signing/

OpenTofu has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

OpenTofu has been successfully initialized!

You may now begin working with OpenTofu. Try running "tofu plan" to see
any changes that are required for your infrastructure. All OpenTofu commands
should now work.

If you ever set or change modules or backend configuration for OpenTofu,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

環境は以下のとおり。

$ tofu version
OpenTofu v1.6.0
on linux_arm64
+ provider registry.opentofu.org/hashicorp/azurerm v3.100.0
+ provider registry.opentofu.org/hashicorp/http v3.4.2

test

test 機能を試す
リソースグループ名とリソースグループのロケーションが狙ったものとなるのかをテストするために、以下の内容のファイルを準備する。

vnet.tftest.hcl
run "check_rg_name" {
    command = plan

    assert {
        condition = azurerm_resource_group.rg.name == "<environment name>"
        error_message  = "storage account name is wrong"
    }
}

run "check_location" {
    command = plan

    assert {
        condition = azurerm_resource_group.rg.location == "japaneast"
        error_message = "location is wrong"
    }
}

test 実行
command = plan のとき、管理外で同名のリソースが存在していても怒られない。

$ tofu test -var-file='parameters.tfvars' | cat
vnet.tftest.hcl... pass
  run "check_rg_name"... pass
  run "check_location"... pass

Success! 2 passed, 0 failed.

リソースグループ名が間違ってるときの出力例

$ tofu test -var-file='parameters.tfvars' | cat
vnet.tftest.hcl... fail
  run "check_rg_name"... fail
╷
│ Error: Test assertion failed
│
│   on vnet.tftest.hcl line 5, in run "check_rg_name":
│    5:         condition = azurerm_resource_group.rg.name == "correctname"
│     ├────────────────
│     │ azurerm_resource_group.rg.name is "<environment name>"
│
│ storage account name is wrong
╵
  run "check_location"... pass

Failure! 1 passed, 1 failed.

command = apply のとき

vnet.tftest.hcl
run "check_rg_name" {
    command = apply

    assert {
        condition = azurerm_resource_group.rg.name == "<environment name>"
        error_message  = "storage account name is wrong"
    }
}

run "check_location" {
    command = apply

    assert {
        condition = azurerm_resource_group.rg.location == "japaneast"
        error_message = "location is wrong"
    }
}

command = apply の場合、test 実行するときに実施にリソースが作られる。
管理外で同名のリソースが存在すると、インポートしろと言われる。

$ tofu test -var-file='parameters.tfvars' | cat
╷
│ Error: A resource with the ID "/subscriptions/<subscription id>/resourceGroups/<environment name>" already exists - to be managed via Terraform this resource needs to be imported into the State. Please see the resource documentation for "azurerm_resource_group" for more information.
│
│   with azurerm_resource_group.rg,
│   on vnet.tf line 1, in resource "azurerm_resource_group" "rg":
│    1: resource "azurerm_resource_group" "rg" {
│
╵
vnet.tftest.hcl... fail
  run "check_rg_name"... fail
  run "check_location"... skip

Failure! 0 passed, 1 failed, 1 skipped.

import して 再度 test を実行

$ tofu import -var-file='parameters.tfvars' azurerm_resource_group.rg /subscriptions/<subscription id>/resourceGroups/<environment name>
data.http.getaddress: Reading...
data.http.getaddress: Read complete after 0s [id=https://example.net/getaddress/]
azurerm_resource_group.rg: Importing from ID "/subscriptions/<subscription id>/resourceGroups/<environment name>"...
azurerm_resource_group.rg: Import prepared!
  Prepared azurerm_resource_group for import
azurerm_resource_group.rg: Refreshing state... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>]

Import successful!

The resources that were imported are shown above. These resources are now in
your OpenTofu state and will henceforth be managed by OpenTofu.


$ tofu test -var-file='parameters.tfvars' | cat
vnet.tftest.hcl... pass
  run "check_rg_name"... pass
  run "check_location"... pass

Success! 2 passed, 0 failed.

plan

tfファイルを検証する。

$ tofu validate
Success! The configuration is valid.

plan を実行して、プランを tofu.out ファイルに出力する。

$ tofu plan -var-file='parameters.tfvars' -out='tofu.out' | cat                                                                                                                                                           [103/1880]
data.http.getaddress: Reading...
data.http.getaddress: Read complete after 0s [id=https://example.net/getaddress/]

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

OpenTofu will perform the following actions:

  # azurerm_network_security_group.subnet1_nsg will be created
  + resource "azurerm_network_security_group" "subnet1_nsg" {
      + id                  = (known after apply)
      + location            = "japaneast"
      + name                = "<environment name>_nsg"
      + resource_group_name = "<environment name>"
      + security_rule       = [
          + {
              + access                                     = "Allow"
              + description                                = ""
              + destination_address_prefix                 = "*"
              + destination_address_prefixes               = []
              + destination_application_security_group_ids = []
              + destination_port_range                     = "22"
              + destination_port_ranges                    = []
              + direction                                  = "Inbound"
              + name                                       = "allow_inbound_ssh"
              + priority                                   = 200
              + protocol                                   = "Tcp"
              + source_address_prefix                      = "x.x.x.x"
              + source_address_prefixes                    = []
              + source_application_security_group_ids      = []
              + source_port_range                          = "*"
              + source_port_ranges                         = []
            },
          + {
              + access                                     = "Allow"
              + description                                = ""
              + destination_address_prefix                 = "Internet"
              + destination_address_prefixes               = []
              + destination_application_security_group_ids = []
              + destination_port_range                     = ""
              + destination_port_ranges                    = [
                  + "443",
                  + "80",
                ]
              + direction                                  = "Outbound"
              + name                                       = "allow_outbound_internet"
              + priority                                   = 3700
              + protocol                                   = "Tcp"
              + source_address_prefix                      = "*"
              + source_address_prefixes                    = []
              + source_application_security_group_ids      = []
              + source_port_range                          = "*"
              + source_port_ranges                         = []
            },
          + {                                                                                                                                                                                                                       [47/1880]
              + access                                     = "Deny"
              + description                                = ""
              + destination_address_prefix                 = "*"
              + destination_address_prefixes               = []
              + destination_application_security_group_ids = []
              + destination_port_range                     = "*"
              + destination_port_ranges                    = []
              + direction                                  = "Inbound"
              + name                                       = "deny_inbound_all"
              + priority                                   = 4096
              + protocol                                   = "*"
              + source_address_prefix                      = "*"
              + source_address_prefixes                    = []
              + source_application_security_group_ids      = []
              + source_port_range                          = "*"
              + source_port_ranges                         = []
            },
          + {
              + access                                     = "Deny"
              + description                                = ""
              + destination_address_prefix                 = "*"
              + destination_address_prefixes               = []
              + destination_application_security_group_ids = []
              + destination_port_range                     = "*"
              + destination_port_ranges                    = []
              + direction                                  = "Outbound"
              + name                                       = "deny_outbound_all"
              + priority                                   = 4096
              + protocol                                   = "*"
              + source_address_prefix                      = "*"
              + source_address_prefixes                    = []
              + source_application_security_group_ids      = []
              + source_port_range                          = "*"
              + source_port_ranges                         = []
            },
        ]
      + tags                = {
          + "environment" = "<environment name>"
        }
    }

  # azurerm_resource_group.rg will be created
  + resource "azurerm_resource_group" "rg" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = "<environment name>"
      + tags     = {
          + "environment" = "<environment name>"
        }
    }

  # azurerm_subnet.subnet1 will be created
  + resource "azurerm_subnet" "subnet1" {
      + address_prefixes                               = [
          + "10.0.0.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "<environment name>_subnet1"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = (known after apply)
      + resource_group_name                            = "<environment name>"
      + service_endpoints                              = [
          + "Microsoft.Storage",
        ]
      + virtual_network_name                           = "<environment name>_vnet"
    }

  # azurerm_subnet_network_security_group_association.nsg_association will be created
  + resource "azurerm_subnet_network_security_group_association" "nsg_association" {
      + id                        = (known after apply)
      + network_security_group_id = (known after apply)
      + subnet_id                 = (known after apply)
    }

  # azurerm_virtual_network.vnet will be created
  + resource "azurerm_virtual_network" "vnet" {
      + address_space       = [
          + "10.0.0.0/16",
        ]
      + dns_servers         = (known after apply)
      + guid                = (known after apply)
      + id                  = (known after apply)
      + location            = "japaneast"
      + name                = "<environment name>_vnet"
      + resource_group_name = "<environment name>"
      + subnet              = (known after apply)
      + tags                = {
          + "environment" = "<environment name>"
        }
    }

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

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

Saved the plan to: tofu.out

To perform exactly these actions, run the following command to apply:
    tofu apply "tofu.out"

なお tofu plan -var-file='parameters.tfvars' -var location='japanwest' のように、コマンドラインで変数を与えることもできる。

apply

plan を出力したファイル tofu.out を指定して apply を実行する。

$ tofu apply "tofu.out"
azurerm_resource_group.rg: Creating...
azurerm_resource_group.rg: Creation complete after 0s [id=/subscriptions/<subscription id>/resourceGroups/<environment name>]
azurerm_virtual_network.vnet: Creating...
azurerm_network_security_group.subnet1_nsg: Creating...
azurerm_network_security_group.subnet1_nsg: Creation complete after 2s [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/networkSecurityGroups/<environment name>_nsg]
azurerm_virtual_network.vnet: Creation complete after 4s [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/virtualNetworks/<environment name>_vnet]
azurerm_subnet.subnet1: Creating...
azurerm_subnet.subnet1: Creation complete after 3s [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/virtualNetworks/<environment name>_vnet/subnets/<environment name>_subnet1]
azurerm_subnet_network_security_group_association.nsg_association: Creating...
azurerm_subnet_network_security_group_association.nsg_association: Creation complete after 4s [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/virtualNetworks/<environment name>_vnet/subnets/<environment name>_subnet1]

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

確認

$ tofu state list
data.http.getaddress
azurerm_network_security_group.subnet1_nsg
azurerm_resource_group.rg
azurerm_subnet.subnet1
azurerm_subnet_network_security_group_association.nsg_association
azurerm_virtual_network.vnet
$ tofu state show azurerm_resource_group.rg
# azurerm_resource_group.rg:
resource "azurerm_resource_group" "rg" {
    id       = "/subscriptions/<subscription id>/resourceGroups/<environment name>"
    location = "japaneast"
    name     = "<environment name>"
    tags     = {
        "environment" = "<environment name>"
    }
}

destroy

$ tofu destroy -var-file='parameters.tfvars' | cat
data.http.getaddress: Reading...
data.http.getaddress: Read complete after 0s [id=https://example.net/getaddress/]
azurerm_resource_group.rg: Refreshing state... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>]
azurerm_virtual_network.vnet: Refreshing state... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/virtualNetworks/<environment name>_vnet]
azurerm_network_security_group.subnet1_nsg: Refreshing state... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/networkSecurityGroups/<environment name>_nsg]
azurerm_subnet.subnet1: Refreshing state... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/virtualNetworks/<environment name>_vnet/subnets/<environment name>_subnet1]
azurerm_subnet_network_security_group_association.nsg_association: Refreshing state... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/virtualNetworks/<environment name>_vnet/subnets/<environment name>_subnet1]

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

OpenTofu will perform the following actions:

略

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

Do you really want to destroy all resources?
  OpenTofu will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

azurerm_subnet_network_security_group_association.nsg_association: Destroying... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/virtualNetworks/<environment name>_vnet/subnets/<environment name>_subnet1]
azurerm_subnet_network_security_group_association.nsg_association: Destruction complete after 3s
azurerm_subnet.subnet1: Destroying... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/virtualNetworks/<environment name>_vnet/subnets/<environment name>_subnet1]
azurerm_network_security_group.subnet1_nsg: Destroying... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/networkSecurityGroups/<environment name>_nsg]
azurerm_network_security_group.subnet1_nsg: Destruction complete after 2s
azurerm_subnet.subnet1: Still destroying... [id=/subscriptions/49a97ccc-4d64-4d3f-af2b-...<environment name>_vnet/subnets/<environment name>_subnet1, 10s elapsed]
azurerm_subnet.subnet1: Destruction complete after 11s
azurerm_virtual_network.vnet: Destroying... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/virtualNetworks/<environment name>_vnet]
azurerm_virtual_network.vnet: Still destroying... [id=/subscriptions/49a97ccc-4d64-4d3f-af2b-....Network/virtualNetworks/<environment name>_vnet, 10s elapsed]
azurerm_virtual_network.vnet: Destruction complete after 10s
azurerm_resource_group.rg: Destroying... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>]
azurerm_resource_group.rg: Still destroying... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>, 10s elapsed]
azurerm_resource_group.rg: Destruction complete after 16s

Destroy complete! Resources: 5 destroyed.

確認

$ tofu show
The state file is empty. No resources are represented.

失敗したとき

検証時、リソースグループを作成する時点で以下のように失敗した。

$ tofu apply "tofu.out"
azurerm_resource_group.rg: Creating...
╷
│ Error: retrieving Resource Group "<environment name>": resources.GroupsClient#Get: Failure responding to request: StatusCode=404 -- Original Error: autorest/azure: Service returned an error. Status=404 Code="ResourceGroupNotFound" Message="Resource group '<environment name>' could not be found."
│
│   with azurerm_resource_group.rg,
│   on vnet.tf line 1, in resource "azurerm_resource_group" "rg":
│    1: resource "azurerm_resource_group" "rg" {
│
╵

確認すると、リソースグループ自体はできていた。

$ az group list --tag environment=<environment name>
[
  {
    "id": "/subscriptions/<subscription id>/resourceGroups/<environment name>",
    "location": "japaneast",
    "managedBy": null,
    "name": "<environment name>",
    "properties": {
      "provisioningState": "Succeeded"
    },
    "tags": {
      "environment": "<environment name>"
    },
    "type": "Microsoft.Resources/resourceGroups"
  }
]

$ az group show --resource-group <environment name>
{
  "id": "/subscriptions/<subscription id>/resourceGroups/<environment name>",
  "location": "japaneast",
  "managedBy": null,
  "name": "<environment name>",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": {
    "environment": "<environment name>"
  },
  "type": "Microsoft.Resources/resourceGroups"
}

作成できたリソースグループを、Tofu 管理下にインポートする。

$ tofu import -var-file='parameters.tfvars' azurerm_resource_group.rg /subscriptions/<subscription id>/resourceGroups/<environment name>
data.http.getaddress: Reading...
data.http.getaddress: Read complete after 1s [id=https://example.net/getaddress/]
azurerm_resource_group.rg: Importing from ID "/subscriptions/<subscription id>/resourceGroups/<environment name>"...
azurerm_resource_group.rg: Import prepared!
  Prepared azurerm_resource_group for import
azurerm_resource_group.rg: Refreshing state... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>]

Import successful!

The resources that were imported are shown above. These resources are now in
your OpenTofu state and will henceforth be managed by OpenTofu.

確認

$ tofu state list
data.http.getaddress
azurerm_resource_group.rg

$ tofu state show azurerm_resource_group.rg
# azurerm_resource_group.rg:
resource "azurerm_resource_group" "rg" {
    id       = "/subscriptions/<subscription id>/resourceGroups/<environment name>"
    location = "japaneast"
    name     = "<environment name>"
    tags     = {
        "environment" = "<environment name>"
    }
}

あらためて plan 実行。
作成されるリソースが4で、一つ減っている。

$ tofu plan -var-file='parameters.tfvars' -out='tofu.out' | cat
data.http.getaddress: Reading...
data.http.getaddress: Read complete after 0s [id=https://example.net/getaddress/]
azurerm_resource_group.rg: Refreshing state... [id=/subscriptions/<subscription id>/resourceGroups/<environment name>]

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

OpenTofu will perform the following actions:

略

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

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

Saved the plan to: tofu.out

To perform exactly these actions, run the following command to apply:
    tofu apply "tofu.out"

apply 実行する。

$ tofu apply "tofu.out"
azurerm_virtual_network.vnet: Creating...
azurerm_network_security_group.subnet1_nsg: Creating...
azurerm_network_security_group.subnet1_nsg: Creation complete after 2s [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/networkSecurityGroups/<environment name>_nsg]
azurerm_virtual_network.vnet: Creation complete after 4s [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/virtualNetworks/<environment name>_vnet]
azurerm_subnet.subnet1: Creating...
azurerm_subnet.subnet1: Creation complete after 4s [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/virtualNetworks/<environment name>_vnet/subnets/<environment name>_subnet1]
azurerm_subnet_network_security_group_association.nsg_association: Creating...
azurerm_subnet_network_security_group_association.nsg_association: Creation complete after 3s [id=/subscriptions/<subscription id>/resourceGroups/<environment name>/providers/Microsoft.Network/virtualNetworks/<environment name>_vnet/subnets/<environment name>_subnet1]

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

結果確認

$ tofu state list
data.http.getaddress
azurerm_network_security_group.subnet1_nsg
azurerm_resource_group.rg
azurerm_subnet.subnet1
azurerm_subnet_network_security_group_association.nsg_association
azurerm_virtual_network.vnet
GitHubで編集を提案

Discussion