TECHSTEP

ITインフラ関連の記事を公開してます。

AzureのIaaS検証環境をTerraformで用意する

はじめに

ここ最近はAzureを検証環境としてよく利用しています。これまではAzureポータルから各リソースをデプロイしていましたが、同じ作業を何度もやるのがつらくなり、Azureの検証環境をサクッと用意できると楽が出来るなあと考えていました。今回はTerraformを使ってAzureリソースを用意する方法を調べたので、簡単にまとめます。

今回の検証環境は、仮想マシンを1台以上用意し、場合によってはデプロイ時に共通コマンド(yum updateなど)を実行できる環境を用意します。

Azureで最低限必要なリソース

AzureのIaaSを利用するには、それに関連するリソースも用意する必要があります。今回は仮想マシンを作成するのに合わせ、以下のリソースを用意するtfファイルを用意します。

  • リソースグループ
  • 仮想ネットワーク
  • サブネット
  • パブリックIP
  • NIC
  • OSディスク
  • データディスク
  • 仮想マシン

Azureサブスクリプション情報の取得

まずはAzureのこちらのドキュメントの手順に従い、Terraformの環境変数で利用するAzureの情報を取得します。

Azure Cloud Shellを開き、以下のコマンドを入力します。

# サブスクリプションID、テナントIDを取得する
user@Azure:~$ az account list --query "[].{name:name, subscriptionId:id, tenantId:tenantId}"
[
  {
    "name": "従量課金制",
    "subscriptionId": "サブスクリプションID",
    "tenantId": "テナントID"
  }
]

# アプリケーションID、パスワードの取得
user@Azure:~$ az account set --subscription="サブスクリプションID"
user@Azure:~$ az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/サブスクリプションID"
Creating a role assignment under the scope of "/subscriptions/サブスクリプションID"
  Retrying role assignment creation: 1/36
  Retrying role assignment creation: 2/36
  Retrying role assignment creation: 3/36
{
  "appId": "アプリケーションID",
  "displayName": "azure-cli-2020-01-08-01-51-27",
  "name": "http://azure-cli-2020-01-08-01-51-27",
  "password": "パスワード",
  "tenant": "テナントID"
}
user@Azure:~$

上記コマンドで生成された情報をtfファイルに入力して利用します。

Terraformのインストール

Terraformを利用するには様々な方法があります。上述のAzure Cloud ShellはデフォルトでTerraformが利用可能です。またTerraformはこちらからファイルをダウンロードしてローカル環境で利用することもできます。

今回はWindowsのローカルPCでコマンドを実行しましたが、いずれの環境でも同様のコマンドを実行することでAzureリソースの作成を行えます。

tfファイルの内容

今回利用したtfファイルは以下のようになります。仮想マシンを複数作成できるようcount_vmという変数を設定しており、必要に応じてcount_vmの数値を変更します。

# 利用するAzure Providerの情報を設定
provider "azurerm" {
  subscription_id = "サブスクリプションID"
  client_id       = "アプリケーションID"
  client_secret   = "パスワード"
  tenant_id       = "テナントID"
}

# 変数の設定

variable "count_vm" {
  default = "1"
}

# リソースグループの作成
resource "azurerm_resource_group" "rg" {
  name     = "azuretest_rg"
  location = "japaneast"

  tags = {
    environment = "Terraform"
  }
}

# 仮想ネットワークの作成
resource "azurerm_virtual_network" "vnet" {
  name                = "azuretest_vnet"
  address_space       = ["10.0.0.0/16"]
  location            = "japaneast"
  resource_group_name = azurerm_resource_group.rg.name

  tags = {
    environment = "Terraform"
  }
}

# サブネットの作成
resource "azurerm_subnet" "subnet" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefix       = "10.0.1.0/24"
}

# パブリックIPの作成
resource "azurerm_public_ip" "publicip" {
  name                = "pip-${count.index}"
  location            = "japaneast"
  resource_group_name = azurerm_resource_group.rg.name
  allocation_method   = "Dynamic"
  count               = "${var.count_vm}"

  tags = {
    environment = "Terraform"
  }
}

# NSGの作成と通信ルールの設定
resource "azurerm_network_security_group" "nsg" {
  name                = "azuretest_nsg"
  location            = "japaneast"
  resource_group_name = azurerm_resource_group.rg.name

  security_rule {
    name                       = "SSH"
    priority                   = 1001
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "仮想マシンにアクセスする送信元のグローバルIPアドレス"
    destination_address_prefix = "*"
  }

  tags = {
    environment = "Terraform"
  }
}

# ネットワークインターフェイスの作成
resource "azurerm_network_interface" "nic" {
  name                      = "nic-${count.index}"
  location                  = "japaneast"
  resource_group_name       = azurerm_resource_group.rg.name
  network_security_group_id = azurerm_network_security_group.nsg.id
  count                     = "${var.count_vm}"

  ip_configuration {
    name                          = "ipconfig${count.index}"
    subnet_id                     = azurerm_subnet.subnet.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = "${element(azurerm_public_ip.publicip.*.id, count.index)}"
  }

  tags = {
    environment = "Terraform"
  }
}

# ストレージアカウント用IDの生成(Azure内で一意なIDを割り当てる必要がある)
resource "random_id" "randomId" {
  keepers = {
    # Generate a new ID only when a new resource group is defined
    resource_group = azurerm_resource_group.rg.name
  }

  byte_length = 8
}

# ブート診断用ストレージアカウントの作成
resource "azurerm_storage_account" "sa" {
  name                     = "diag${random_id.randomId.hex}"
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = "japaneast"
  account_tier             = "Standard"
  account_replication_type = "LRS"

  tags = {
    environment = "Terraform"
  }
}

# 仮想マシンの作成
resource "azurerm_virtual_machine" "vm" {
  name                  = "vm${count.index}"
  location              = "japaneast"
  resource_group_name   = azurerm_resource_group.rg.name
  network_interface_ids = ["${element(azurerm_network_interface.nic.*.id, count.index)}"]
  vm_size               = "Standard_D2s_v3"
  count                 = "${var.count_vm}"

  storage_os_disk {
    name              = "osdisk${count.index}"
    caching           = "ReadWrite"
    create_option     = "FromImage"
    managed_disk_type = "StandardSSD_LRS"
  }

  storage_data_disk {
    name              = "datadisk${count.index}"
    managed_disk_type = "StandardSSD_LRS"
    create_option     = "Empty"
    lun               = 0
    disk_size_gb      = "100"
  }

  storage_image_reference {
    publisher = "OpenLogic"
    offer     = "CentOS"
    sku       = "7.7"
    version   = "latest"
  }

  os_profile {
    computer_name  = "vm${count.index}"
    admin_username = "任意のユーザー名を指定"
    admin_password = "任意のパスワードを指定"
  }

  os_profile_linux_config {
    disable_password_authentication = false
  }
  
  boot_diagnostics {
    enabled     = "true"
    storage_uri = azurerm_storage_account.sa.primary_blob_endpoint
  }

  tags = {
    environment = "Terraform"
  }
}

resource "azurerm_virtual_machine_extension" "test" {
  name                 = "vm${count.index}"
  location             = "japaneast"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_machine_name = "${element(azurerm_virtual_machine.vm.*.name, count.index)}"
  publisher            = "Microsoft.Azure.Extensions"
  type                 = "CustomScript"
  type_handler_version = "2.0"
  count                = "${var.count_vm}"

  settings = <<SETTINGS
    {
        "script": "${filebase64("custom_script.sh")}"
    }
SETTINGS
}

# パブリックPIアドレス表示用
data "azurerm_public_ip" "publicip" {
  count               = "${var.count_vm}"
  name                = "${element(azurerm_public_ip.publicip.*.name, count.index)}"
  resource_group_name = "${element(azurerm_virtual_machine.vm.*.resource_group_name, count.index)}"
}

output "publicip_address" {
  value = "${data.azurerm_public_ip.publicip.*.ip_address}"
}

上記tfファイルと同じディレクトリにcustom_script.shを配置して実行します。custom_script.shの内容は任意に変更すればよいですが、今回は以下のような内容にしました。

#!/bin/sh

# update
yum update -y

# SELinux
setenforce 0
sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config

# Docker
yum install -y docker
systemctl start docker && systemctl enable docker

上記ファイルを利用するときの注意点を残しておきます。

  • Network Security Groupでは、セキュリティを考慮し、仮想マシンへアクセスする送信元のIPアドレスのみを許可しています。
  • ストレージアカウントは無くても問題ありません。
  • Virtual Machineのos_profileで指定するadmin_username admin_passwordは、以下の条件を満たす必要があります。
    • admin_usernameadmin rootなどの予約済みアカウントは利用不可
    • admin_password:アルファベットの大文字、任意の記号、数字を含む12文字以上を指定

※参考リンク

Terraform - azurerm_virtual_machine

Microsoft Azure - Create a complete Linux virtual machine infrastructure in Azure with Terraform

Terraform - Data Source: azurerm_managed_disk

Terraformコマンドの実行

上記ファイルを用意したら、各種Terraformコマンドを実行します。

# 初期化
C:\Users>terraform init

Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "azurerm" (hashicorp/azurerm) 1.39.0...
- Downloading plugin for provider "random" (hashicorp/random) 2.2.1...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.azurerm: version = "~> 1.39"
* provider.random: version = "~> 2.2"

Terraform has been successfully initialized!

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

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

C:\Users>

# tfファイルの記載方法が正しいか確認(terraform validate)

C:\Users>terraform validate
Success! The configuration is valid.

C:\Users>


# 作成予定のリソースを確認(terraform plan)
C:\Users>terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:


(中略)


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

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.


C:\Users>


# リソースの作成(terraform apply)

C:\Users>terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:


(中略)


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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes  # yesと入力


(中略)


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

Outputs:

publicip_address = [
  "13.71.135.94",
]

C:\Users>

コマンドが完了すると、publicip_addressが出力され、アクセスするパブリックIPアドレスが表示されます。

provisioning時に実行するコマンドの指定方法

TerraformでAzureリソースを操作する場合、コマンドを指定する方法がいくつかあります。

1. remote-exec Provisioner

remote-exec Provisionerはリモートホストにアクセスしてコマンドを実行することができます。利用するためにはconnection blockを合わせて指定し、Linuxの場合はssh鍵のパスを指定する必要があります。実行コマンドはinlineに記載します。

resource "aws_instance" "web" {
  # ...

  connection {
    type     = "ssh"
    user     = "root"
    password = "${var.root_password}"
    host     = "${var.host}"
  }

  provisioner "remote-exec" {
    inline = [
      "puppet apply",
      "consul join ${aws_instance.web.private_ip}",
    ]
  }
}

※参考リンク:

Terraform Doc - remote-exec Provisioner

Terraform Doc - Provisioner Connection Settings

2. commandToExecute property

azurerm_virtual_machine_extensionは、デプロイ後のAzure仮想マシンに対して実行コマンドなどを指定し、設定を追加することができるリソースです。azurerm_virtual_machine_extension内でcommandToExecuteを利用し、仮想マシン上で実行するコマンドを直接指定します。

resource "azurerm_virtual_machine_extension" "example" {
  name                 = "hostname"
  location             = "${azurerm_resource_group.example.location}"
  resource_group_name  = "${azurerm_resource_group.example.name}"
  virtual_machine_name = "${azurerm_virtual_machine.example.name}"
  publisher            = "Microsoft.Azure.Extensions"
  type                 = "CustomScript"
  type_handler_version = "2.0"

  settings = <<SETTINGS
    {
        "commandToExecute": "hostname && uptime"
    }
SETTINGS

  tags = {
    environment = "Production"
  }
}

3. script property

今回利用したのは、azurerm_virtual_machine_extensionリソース内で利用するscript propertyです。scriptで指定したスクリプトファイル内にある記述内容を実行します。

commandToExecuteと比べて複雑なコマンドを実行できること、またremote-execのようにssh鍵を指定する必要がないことから、今回はこちらを採用しました。

resource "azurerm_virtual_machine_extension" "test" {
  name                 = "hostname"
  location             = "${azurerm_resource_group.test.location}"
  resource_group_name  = "${azurerm_resource_group.test.name}"
  virtual_machine_name = "${azurerm_virtual_machine.test.name}"
  publisher            = "Microsoft.Azure.Extensions"
  type                 = "CustomScript"
  type_handler_version = "2.0"

  settings = <<SETTINGS
    {
        "script": "${filebase64("custom_script.sh")}"
    }
SETTINGS
}

※参考リンク

Terraform Doc - azurerm_virtual_machine_extension

hypernephelist - Azure VM Custom Script Extensions with Terraform