🛁

TerraformからAzureを構築する【Azure VMを作ってみよう】

タグ
TerraformAzure
公開日
目次

はじめに

前回のTerraformからAzureを構築する【準備編】ではterraformのインストールから、最小構成でterraformからリソースグループを作成するまでを行いました。

今回はより実践的なAzureVMをterraformで作っていきたいと思います。

想定する読者

何を作るか

今回はLinuxのVMを作成していきます。

Azure VMを作成する

MSさんが出してくれている以下の記事と

クイック スタート: Terraform を使用して Linux VM を作成する - Azure Virtual Machines

適用対象: ✔️ Linux VM 次の Terraform と Terraform プロバイダーのバージョンでテストされた記事: この記事では、Terraform を使用して、完全な Linux 環境とサポート リソースを作成する方法を示します。 これらのリソースには、仮想ネットワーク、サブネット、パブリック IP アドレスなどが含まれます。 Terraform を使用すると、クラウド インフラストラクチャの定義、プレビュー、およびデプロイを行うことができます。 Terraform を使用する際は、 HCL 構文を使って構成ファイルを作成します。 HCL 構文では、Azure などのクラウド プロバイダーと、クラウド インフラストラクチャを構成する要素を指定できます。 構成ファイルを作成したら、" 実行プラン" を作成します。これにより、インフラストラクチャの変更をデプロイ前にプレビューすることができます。 変更を確認したら、実行プランを適用してインフラストラクチャをデプロイします。 この記事では、次のことについて説明します。 仮想ネットワークの作成 サブネットの作成 パブリック IP アドレスの作成 ネットワーク セキュリティ グループと SSH 受信規則を作成する 仮想ネットワーク インターフェイス カードの作成 ネットワーク セキュリティ グループをネットワーク

クイック スタート: Terraform を使用して Linux VM を作成する - Azure Virtual Machines

terraformのリファレンスを見ると作成できます!

と、それだとこの記事の意味がなくなってしまうので最小構成で分かりやすく細かい解説を入れながらやって行こうと思います。

ファイル構成

terraform (作業ディレクトリ)
 ∟ main.tf (tfファイル)
 ∟ terraform.tfstate  (tfstateファイル) <= 自動で生成されます。

ファイル構成は前回の「TerraformからAzureを構築する【準備編】」と同じになります。

main.tf (tfファイル)の解説

リソースの設定を記述するtfファイルをいくつかに分割して説明していきます。

terraformの設定とリソースグループ作成

terraform {
  backend "local" {}
  required_version = "1.3.4"
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "3.34.0"
    }
    tls = { #SSHの秘密鍵を生成するために必要なprovider
      source = "hashicorp/tls"
      version = "~>4.0"
    }
  }
}
provider "azurerm" {
  features {}
}
resource "azurerm_resource_group" "resource-group" {
  location = "japaneast"
  name     = "terraform-test"
  tags = {
    ENVIRONMENT = "dev"
  }
}

前回で使用した部分に、tlsの項目を追加しています。

SSHで秘密鍵を生成するために必要なプロバイダーです。

ネットワークの設定

# 仮想ネットワーク
resource "azurerm_virtual_network" "test_network" {
  name                = "test-vnet"
  address_space       = ["192.168.0.0/16"]
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name
}

# サブネット
resource "azurerm_subnet" "test_subnet" {
  name                 = "test-subnet"
  resource_group_name  = azurerm_resource_group.resource-group.name
  virtual_network_name = azurerm_virtual_network.test_network.name
  address_prefixes     = ["192.168.0.0/24"]
}

# パブリックIPアドレス
resource "azurerm_public_ip" "test_public_ip" {
  name                = "test-public-ip"
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name
  allocation_method   = "Dynamic"
}

# NSG(ネットワークセキュリティグループ)
resource "azurerm_network_security_group" "test_nsg" {
  name                = "test-network-security-group"
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name

  security_rule {
    name                       = "SSH"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

# NIC(ネットワークインタフェースカード)
resource "azurerm_network_interface" "test_nic" {
  name                = "test-nic"
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name

  ip_configuration {
    name                          = "test_nic_configuration"
    subnet_id                     = azurerm_subnet.test_subnet.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.test_public_ip.id
  }
}

# NSGとNICの紐付け
resource "azurerm_network_interface_security_group_association" "nsg-nic" {
  network_interface_id      = azurerm_network_interface.test_nic.id
  network_security_group_id = azurerm_network_security_group.test_nsg.id
}

ネットワークやNSGに関する設定になります。

locationやresource_group_nameなど、文字列でそのまま書いても良いのですが

リソースの種類.tfファイル内でのリソース名.プロパティ

と指定する事で別のリソースの情報を参照する事ができます。

このようにすることで、汎用的なテンプレートを作る事ができます。

NSGにて複数のsecurity_ruleを指定したい場合はsecurity_ruleを別リソースにして紐づけます。

# NSG(ネットワークセキュリティグループ)
resource "azurerm_network_security_group" "test_nsg" {
  name                = "test-network-security-group"
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name
}

# セキュリティルール(SSH)
resource "azurerm_network_security_rule" "test_nsg-rule-ssh" {
  access                      = "Allow"
  destination_address_prefix  = "*"
  destination_port_range      = "22"
  direction                   = "Inbound"
  name                        = "AllowAnySSHInbound"
  network_security_group_name = azurerm_network_security_group.test_nsg.name #ここで紐付け
  priority                    = 100
  protocol                    = "Tcp"
  resource_group_name         = azurerm_resource_group.resource-group.name
  source_address_prefix       = "*"
  source_port_range           = "*"
}

# セキュリティルール(http)
resource "azurerm_network_security_rule" "test_nsg-rule-http" {
  access                      = "Allow"
  destination_address_prefix  = "*"
  destination_port_range      = "80"
  direction                   = "Inbound"
  name                        = "AllowAnyHTTPInbound"
  network_security_group_name = azurerm_network_security_group.test_nsg.name #ここで紐付け
  priority                    = 110
  protocol                    = "Tcp"
  resource_group_name         = azurerm_resource_group.resource-group.name
  source_address_prefix       = "*"
  source_port_range           = "*"
}

このようにterraformでは、親リソースの中にまとめて記述するパターンと、子リソースにして外出しする記述パターンとが混在しているのでややこしい部分があります。

SSHキーの作成

# SSHキー
resource "tls_private_key" "test_ssh" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

SSHの秘密鍵を生成します。生成した秘密鍵は後述のoutputという項目でterraformからエクスポートできるようにします。

VMの設定

# VM
resource "azurerm_linux_virtual_machine" "test_vm" {
  name                  = "test-vm"
  location              = azurerm_resource_group.resource-group.location
  resource_group_name   = azurerm_resource_group.resource-group.name
  network_interface_ids = [azurerm_network_interface.test_nic.id]
  size                  = "Standard_DS1_v2"

  os_disk {
    name                 = "test-on-disk"
    caching              = "ReadWrite"
    storage_account_type = "Premium_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }

  computer_name                   = "test-vm"
  admin_username                  = "azureuser"
  disable_password_authentication = true

  admin_ssh_key {
    username   = "azureuser"
    public_key = tls_private_key.test_ssh.public_key_openssh
  }

}

sizeやstorage_account_type、OSの種類などVMのパラメータを設定できます。

エクスポート用の設定

output "resource_group_name" {
  value = azurerm_resource_group.resource-group.name
}

output "public_ip_address" {
  value = azurerm_linux_virtual_machine.test_vm.public_ip_address
}

output "tls_private_key" {
  value     = tls_private_key.test_ssh.private_key_pem
  sensitive = true
}

outputとしてリソース名を指定しておくと、リソースのプロパティの値をエクスポートする事ができます。

より正確にいうと、tfstateファイルから参照しているだけなので、実際の値はtfstateファイルに記載されています。

tfstateファイルには機密情報であっても平文で保存されているので管理する方法を考えなくてはいけませんが、それはまた別の記事で解説していきます。

main.tf (tfファイル)全体

terraform {
  backend "local" {}
  required_version = "1.3.4"
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "3.34.0"
    }
    tls = {
      source = "hashicorp/tls"
      version = "~>4.0"
    }
  }
}
provider "azurerm" {
  features {}
}
resource "azurerm_resource_group" "resource-group" {
  location = "japaneast"
  name     = "terraform-test"
  tags = {
    ENVIRONMENT = "dev"
  }
}

# 仮想ネットワーク
resource "azurerm_virtual_network" "test_network" {
  name                = "test-vnet"
  address_space       = ["192.168.0.0/16"]
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name
}

# サブネット
resource "azurerm_subnet" "test_subnet" {
  name                 = "test-subnet"
  resource_group_name  = azurerm_resource_group.resource-group.name
  virtual_network_name = azurerm_virtual_network.test_network.name
  address_prefixes     = ["192.168.0.0/24"]
}

# パブリックIPアドレス
resource "azurerm_public_ip" "test_public_ip" {
  name                = "test-public-ip"
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name
  allocation_method   = "Dynamic"
}

# NSG(ネットワークセキュリティグループ)
resource "azurerm_network_security_group" "test_nsg" {
  name                = "test-network-security-group"
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name

  security_rule {
    name                       = "SSH"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

# NIC(ネットワークインタフェースカード)
resource "azurerm_network_interface" "test_nic" {
  name                = "test-nic"
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name

  ip_configuration {
    name                          = "test_nic_configuration"
    subnet_id                     = azurerm_subnet.test_subnet.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.test_public_ip.id
  }
}

# NSGとNICの紐付け
resource "azurerm_network_interface_security_group_association" "nsg-nic" {
  network_interface_id      = azurerm_network_interface.test_nic.id
  network_security_group_id = azurerm_network_security_group.test_nsg.id
}

# SSHキー
resource "tls_private_key" "test_ssh" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

# VM
resource "azurerm_linux_virtual_machine" "test_vm" {
  name                  = "test-vm"
  location              = azurerm_resource_group.resource-group.location
  resource_group_name   = azurerm_resource_group.resource-group.name
  network_interface_ids = [azurerm_network_interface.test_nic.id]
  size                  = "Standard_DS1_v2"

  os_disk {
    name                 = "test-on-disk"
    caching              = "ReadWrite"
    storage_account_type = "Premium_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }

  computer_name                   = "test-vm"
  admin_username                  = "azureuser"
  disable_password_authentication = true

  admin_ssh_key {
    username   = "azureuser"
    public_key = tls_private_key.test_ssh.public_key_openssh
  }

}

output "resource_group_name" {
  value = azurerm_resource_group.resource-group.name
}

output "public_ip_address" {
  value = azurerm_linux_virtual_machine.test_vm.public_ip_address
}

output "tls_private_key" {
  value     = tls_private_key.test_ssh.private_key_pem
  sensitive = true
}

※各リソースのnameに入る部分は同じサブスクリプション内で競合する名前がある場合実行中にエラーになります。被らないような値に修正して下さい。

image

terraformでVMを作成

実際に作成していきます。

作業ディレクトリの初期化

terraform init

前回の記事で作成したディレクトリで実行するとエラーが表示されるかもしれません。

その場合はオプションを指定して以下のように実行して下さい。

terraform init -upgrade

変更内容の確認

terraform plan

今回作成対象のリソースが表示されます(確認のみ)

image

リソースの作成

terraform apply

差分を確認後「yes」を実行するとリソースが作成されます。

image

Outputsとして指定した値が出力されていますが、tls_private_keyに関してはsensitiveオプションをTrueにしているのでディスプレイ上には表示されません(tfstateファイルには平文で記載されています)

秘密鍵のエクスポート

terraform output -raw tls_private_key > id_rsa

terraform outputで秘密鍵をエクスポートします。

chmod 600 id_rsa

秘密鍵のアクセス権を変更します。

接続確認

ssh -i id_rsa azureuser@<public_ip_address>

sshで接続できるようになっています

image

Azureポータルでも確認

image

一旦お片付け

terraform destroy

AzureVMを10台作ってみる

tfファイルを書き換えて、terraformからAzureVMを10台作ってみたいと思います。

main.tf (tfファイル)の修正箇所

VM用の変数を定義

locals {
  vm_map = {
    "test-vm01" = {"subnet":"192.168.1.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm02" = {"subnet":"192.168.2.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm03" = {"subnet":"192.168.3.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm04" = {"subnet":"192.168.4.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm05" = {"subnet":"192.168.5.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm06" = {"subnet":"192.168.6.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm07" = {"subnet":"192.168.7.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm08" = {"subnet":"192.168.8.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm09" = {"subnet":"192.168.9.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm10" = {"subnet":"192.168.10.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
  }
}

localsはtfファイル内で使えるローカル変数です。

map形式で作成するVM分のパラメータを記載します。

複数作成するリソース毎に行う設定

# サブネット
resource "azurerm_subnet" "test_subnet" {
  for_each             = local.vm_map # ローカル変数として定義したVMのMAPを指定する
  name                 = "${each.key}-subnet"
  resource_group_name  = azurerm_resource_group.resource-group.name
  virtual_network_name = azurerm_virtual_network.test_network.name
  address_prefixes     = ["${each.value.subnet}"]
}

ここではサブネットを取り出して、どうやってループ処理を描くか見ていきます。

まずlocal.要素localsで定義した変数を参照する事ができます。

ループ処理させたいリソースの中でfor_eachを追加し、値として定義したVMのMAPを指定します。

文字列の中で変数を扱いたい場合は${}を利用します。

nameはサブスクリプションの中でユニークである必要があるので、each.keyでMAPのkeyを指定します。

address_prefixesのようにMAPの値を指定したい場合はeach.value.要素という形で指定します。

これはループ処理が必要なリソース全てに追記していきます。

for_each利用時のoutputの指定方法

output "public_ip_address" {
  value = [ for value in azurerm_linux_virtual_machine.test_vm : value.public_ip_address ]  
}

ここではパブリックIPを取り出して、どうやってfor_each利用時にoutputを指定するかを見ていきます。

for 変数 in リソース種類.tfファイル上のリソース名 : 変数.プロパティ

このような形で複数のoutputをリスト形式にして出力できるようにします。

修正後のmain.tf (tfファイル)全体

terraform {
  backend "local" {}
  required_version = "1.3.4"
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "3.34.0"
    }
    tls = {
      source = "hashicorp/tls"
      version = "~>4.0"
    }
  }
}
provider "azurerm" {
  features {}
}

locals {
  vm_map = {
    "test-vm01" = {"subnet":"192.168.1.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm02" = {"subnet":"192.168.2.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm03" = {"subnet":"192.168.3.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm04" = {"subnet":"192.168.4.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm05" = {"subnet":"192.168.5.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm06" = {"subnet":"192.168.6.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm07" = {"subnet":"192.168.7.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm08" = {"subnet":"192.168.8.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm09" = {"subnet":"192.168.9.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
    "test-vm10" = {"subnet":"192.168.10.0/24","size":"Standard_DS1_v2","storage_account_type":"Premium_LRS","admin_username":"azureuser","ssh_username":"azureuser"},
  }
}

resource "azurerm_resource_group" "resource-group" {
  location = "japaneast"
  name     = "terrafrom-test"
  tags = {
    ENVIRONMENT = "dev"
  }
}
# 仮想ネットワーク
resource "azurerm_virtual_network" "test_network" {
  name                = "test-vnet"
  address_space       = ["192.168.0.0/16"]
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name
}

# サブネット
resource "azurerm_subnet" "test_subnet" {
  for_each             = local.vm_map
  name                 = "${each.key}-subnet"
  resource_group_name  = azurerm_resource_group.resource-group.name
  virtual_network_name = azurerm_virtual_network.test_network.name
  address_prefixes     = ["${each.value.subnet}"]
}

# パブリックIPアドレス
resource "azurerm_public_ip" "test_public_ip" {
  for_each             = local.vm_map
  name                = "${each.key}-public-ip"
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name
  allocation_method   = "Dynamic"
}

# NSG(ネットワークセキュリティグループ)
resource "azurerm_network_security_group" "test_nsg" {
  for_each             = local.vm_map
  name                = "${each.key}-network-security-group"
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name

  security_rule {
    name                       = "SSH"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

# NIC(ネットワークインタフェースカード)
resource "azurerm_network_interface" "test_nic" {
  for_each             = local.vm_map
  name                = "${each.key}-nic"
  location            = azurerm_resource_group.resource-group.location
  resource_group_name = azurerm_resource_group.resource-group.name

  ip_configuration {
    name                          = "${each.key}_nic_configuration"
    subnet_id                     = azurerm_subnet.test_subnet[each.key].id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.test_public_ip[each.key].id
  }
}

# NSGとNICの紐付け
resource "azurerm_network_interface_security_group_association" "nsg-nic" {
  for_each             = local.vm_map
  network_interface_id      = azurerm_network_interface.test_nic[each.key].id
  network_security_group_id = azurerm_network_security_group.test_nsg[each.key].id
}

# SSHキー
resource "tls_private_key" "test_ssh" {
  for_each             = local.vm_map
  algorithm = "RSA"
  rsa_bits  = 4096
}

# VM
resource "azurerm_linux_virtual_machine" "test_vm" {
  for_each             = local.vm_map
  name                  = "${each.key}"
  location              = azurerm_resource_group.resource-group.location
  resource_group_name   = azurerm_resource_group.resource-group.name
  network_interface_ids = [azurerm_network_interface.test_nic[each.key].id]
  size                  = "${each.value.size}"

  os_disk {
    name                 = "${each.key}-on-disk"
    caching              = "ReadWrite"
    storage_account_type = "${each.value.storage_account_type}"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }

  computer_name                   = "${each.key}"
  admin_username                  = "${each.value.admin_username}"
  disable_password_authentication = true

  admin_ssh_key {
    username   = "${each.value.ssh_username}"
    public_key = tls_private_key.test_ssh[each.key].public_key_openssh
  }

}

output "resource_group_name" {
  value = azurerm_resource_group.resource-group.name
}

output "public_ip_address" {
  value = [ for value in azurerm_linux_virtual_machine.test_vm : value.public_ip_address ]  
}

output "tls_private_key" {
  value = [ for value in tls_private_key.test_ssh : value.private_key_pem ]
  sensitive = true
}

terraformでVM10台を作成

実際に作っていきます。

変更内容の確認

terraform plan

今回作成対象のリソースが表示されます(確認のみ)

リソースの作成

terraform apply

差分を確認後「yes」を実行するとリソースが作成されます。

image

約1分程度で10台のVMが作成できました。

Azureポータルで確認

image

見切れていますが10台のVMとそれに紐づくリソースが作成されています。

またお片付け

terraform destroy

まとめ

今回はterraformにてVMを作成しました。

for_eachを使うと同じ構成や一部分だけ変更するようなリソースが簡単に複製する事が可能です。

次回は、terraformでAzureFunctionsを作成してみたいと思います。