【Azure】複数Availability ZoneにVMをTerraformで構築する

[前々回の記事](https://qiita.com/hidekko/items/19871f8fd14304c13e84)の派生記事です。
この記事を社内で見てもらったところ、色んな方からいろんな角度でのご意見を貰い、その中からいろんな記事のネタが上がってきたのですが、今回はそれらのネタからマルチクラウド観点からの記事になります。
AWSで構築した環境と同じような環境をAzureでもIaC(私だとTerraform)で構築できるようにしておきたいので、そのコードを記載します。

# 構成図
## AWSの構成図
ということで早速構成図です。
[前々回の記事](https://qiita.com/hidekko/items/19871f8fd14304c13e84)の派生記事なので、まずはおさらいの意味を込めてAWSの構成図から。
![構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/7b117fc9-ec29-a760-27fd-c17548594676.png)
まぁなんてことないWebサーバの冗長構成の構成図です。
特徴というほどでもないですが、ALBがあり、オンプレミス環境相当のVPCがあること、くらいですかね。

AWSとAzureが同じ構成になっていることを極力客観的に示すため、WebサーバのOSくらいは示しておきましょうかね。
[前々回の記事](https://qiita.com/hidekko/items/19871f8fd14304c13e84)のコード内をご覧頂ければ十分わかるのですが、AWSはAmazon Linux 2023です。

## Azureの構成図
AWSの構成図をAzureに置き換えるとこうなります。
![構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/135c4923-2595-29da-16a8-d73ab0782be9.png)
私の使っているVS Codeの構成図を記載するDraw IOというアドオンではAzureのVnet Peeringのアイコンが用意されていなかったので文字になりました。
基本的な構成はAWSの構成と全く同じです。
**(今更気付きましたがAWSの構成図ではNetwork Securityが記載されていませんが、コードをご覧になったらわかる通りちゃんと構築してネットワークセキュリティも設定しています。)**
AzureではWeb ServerはAlma Linuxにしています。
なぜAWSはAmazon Linuxで、AzureはAlma Linuxにしているかというと、2024年6月30日(よく考えたらもう今月末・・・)に迫ったCent OSのサポート終了を意識しています。
Cent OSはご存知の通りRHELをベースに開発されたLinux Distributionで、Amazon Linuxも同じくRHELをベースに開発されたLinux Distributionです。
機能的な互換性という意味ではCent OSの方がRHELに近いですが、Amazon Linux はAWSがサポートしてくれることを考慮するとCent OSの載せ替え先の候補して検討しても良いのではないでしょうか？
https://aws.amazon.com/jp/mp/linux/
https://aws.amazon.com/jp/amazon-linux-ami/faqs/
MicrosoftはAlma Linuxへの載せ替えを推奨している(もちろん他のDistributionも選択肢として挙げています)ので、今回のようにベーシックなWeb Serverの構成をCent OSで構築されている読者の方がいらっしゃたらと思いそれぞれのLinux Distributionを選択しました。
https://learn.microsoft.com/ja-jp/azure/virtual-machines/workloads/centos/centos-end-of-life
Alma Linuxは確かRHELとABI互換を目指していたはずです。
https://almalinux.org/ja/blog/future-of-almalinux/

## AWSとAzureの機能的差異1
AWSと違いAzureはサブネットごとにAvailability Zone(Azureの用語、かつ日本語訳でいうと可用性ゾーン)を設けなくて良いので構成はAWSよりも若干単純になります。
単純に包含関係が違ってきますので、念のため記載しておくと、
AWSは
**サブネット⊇Availability Zone⊃EC2**
サブネットはAvailability Zoneを含み(サブネットがAvailability Zoneと同じになるため⊇と表現)、更にEC2はこのサブネットで定義されたAvailability Zoneに含まれる
となります。
となり、Azureでは
**Availability Zone⊃VM**
となります。
<font color="Red">AzureはVMに直接可用性ゾーンが関連付くため、サブネットの定義やその上位の仮想ネットワークの概念は絡んできません。</font>
文字で整理しても<font color="Red">**やはりAzureの方が単純ですね！**</font>

## AWSとAzureの機能的差異2
またこれも機能的な差異ですが、**AzureはWindows Client OS(今だとWindows 10とWindows 11のPro以上のエディション)を構築できる**ため、AWSだとClient PC相当のEC2のOSはWindows Server 2022でしたが、<font color="Red">AzureではWindows 11を使っています。</font>

## 機能的な差異ではないが、元になった前々回の記事との差異
もう一点この構成図で特筆すべきは<font color="Red">**Spot Instance**</font>を利用しているという点でしょうか。
**AWSでもSpot Instance使ったらええんやないですかと自分自身でも思うんですが**、このAzureのコード書いてるときにVMでSpot Instance使うことを思い付いたんですよね。
なので次回以降のAWSネタでEC2を構築する場合はきっとたぶんSpot Instanceで構築しているはずです。(ちゃんと私自身が覚えていれば・・・)
<font color="Red">毎回こういう検証するときは**自腹**でやってるので、**Spot Instance**は少しでもお安くPublic Cloudを利用しようと思うと、とっても大事な機能ですからね！</font>

本題からはズレてしまいますが、**お安くVM(EC2)を構築でき、かつIaC(今回だとTerraform)で簡単に環境をScrap&buildできる状態を整えることで、<font color="Red">皆様の検証が(お財布に)遠慮なくどんどん進み、結果として世の中がどんどん良くなっていくことを願っており、私の記事がその一翼を担えればと考えております。</font>**

# Terraform
## 変数設定
前々回の記事と同じくまずは変数設定から
```variables.tf
#------------------------------------
# Set Valiables
#------------------------------------

# Import Subscription ID variable from OS's Enviroment variable
variable "ARM_SUBSCRIPTION_ID" {
  type        = string
}

# Import Tenant ID variable from OS's Enviroment variable
variable "ARM_TENANT_ID" {
  type        = string
}

# Import Client ID variable from OS's Enviroment variable
variable "ARM_CLIENT_ID" {
  type        = string
}

# Import Client Secret variable from OS's Enviroment variable
variable "ARM_CLIENT_SECRET" {
  type        = string
}

# Generate random text for a unique storage account name
resource "random_id" "random_id" {
  keepers = {
    # Generate a new ID only when a new resource group is defined
    resource_group = azurerm_resource_group.rg.name
  }
  byte_length = 8
}

# system name
variable "system_name" {
  type        = string
  default     = "avz01"
}

# region
variable "region" {
  type        = string
  default     = "southeastasia"
}

# resource group (rg) name prefix
variable "rg_name_pre" {
  type        = string
  default     = "rg-"
}

# virtual network (vnet) name prefix
variable "vnet_name_pre" {
  type        = string
  default     = "vnet-"
}

# virtual network (vnet) name suffix for web-server
variable "vnet_name_suf01" {
  type        = string
  default     = "web-server01"
}

# virtual network (vnet) name suffix for client
variable "vnet_name_suf02" {
  type        = string
  default     = "client01"
}

# vnet ip address prefix01 for web-server
variable "vnet_addr_pre01" {
  type        = string
  default     = "10.0."
}

# vnet ip address prefix02 for client
variable "vnet_addr_pre02" {
  type        = string
  default     = "172.16."
}

# vnet ip address suffix for web-server
variable "vnet_addr_suf01" {
  type        = string
  default     = "0.0/24"
}

# vnet ip address suffix for clinet01
variable "vnet_addr_suf02" {
  type        = string
  default     = "1.0/24"
}

# subnet (sn) name prefix
variable "sn_name_pre" {
  type        = string
  default     = "sn-"
}

# sn for web name suffix for web-server
variable "sn_name_suf01" {
  type        = string
  default     = "web-server01"
}

# sn for web ip address suffix for web-server
variable "sn_addr_suf01" {
  type        = string
  default     = "0.0/24"
}

# sn for client name suffix for client
variable "sn_name_suf02" {
  type        = string
  default     = "client01"
}

# sn for db ip address suffix for client
variable "sn_addr_suf02" {
  type        = string
  default     = "1.0/24"
}

# Network Interface Card (nic) name prefix
variable "nic_name_pre" {
  type        = string
  default     = "nic-"  
}

# Network Security Group (nsg) name prefix
variable "nsg_name_pre" {
  type        = string
  default     = "nsg-"  
}

# Public IP Address (pip) prefix
variable "pip_name_pre" {
  type        = string
  default     = "pip-"
}

# IP Configuration (ipconf) suffix
variable "ipconf_suf" {
  type        = string
  default     = "-configuration"
}

# Private IP Address Allocation (priv_ip_alloc)
variable "priv_ip_alloc" {
  type        = string
  default     = "Static" # or Dynamic
}

# NAT Gateway name prefix
variable "ng_name_pre" {
  type        = string
  default     = "nat-gateway-"
}

# NAT Gateway name suffix
variable "ng_name_suf" {
  type        = string
  default     = "01"
}

# admin user (adminuser) name
variable "adminuser" {
  type        = string
  default     = "init-Admin001"
}

# Virtual Machine OS Disk (osdisk) name prefix
variable "vm_osdisk_pre" {
  type        = string
  default     = "osdisk-"
}

# Virtual Machine OS Disk SKU (osdisk_sku)
variable "vm_osdisk_sku" {
  type        = string
  default     = "StandardSSD_LRS"
}

# Virtual Machine SKU (vmsku) name
variable "vm_sku" {
  type        = string
  default     = "Standard_D4_v5"
}

# Virtual Machine (vm) name prefix
variable "vm_name_pre" {
  type        = string
  default     = "vm-"
}

# vm name suffix
variable "vm_name_suf" {
  type        = string
  default     = "app0"
}

# vm name suffix client
variable "vm_name_suf_client" {
  type        = string
  default     = "client01"
}

#------------------------------------
# Virtual Machine OS Publisher (os_pub) name
# "MicrosoftWindowsDesktop" is Client OS 
# "MicrosoftWindowsServer" is Server OS
# "Canonical" is Ubuntu
# "almalinux" is Almalinux
#------------------------------------
variable "os_pub_sv" {
  type        = string
  default     = "almalinux"
}

variable "os_pub_client" {
  type        = string
  default     = "MicrosoftWindowsDesktop"
}

#------------------------------------
# Virtual Machine OS Offer (os_offer) name
# "Windows-10" is Windows 10 
# "Windows-11" is Windows 11
# "WindowsServer" os Windows Server OS
# "0001-com-ubuntu-server-jammy" is Ubuntu
# "almalinux-x86_64" is Almalinux
#------------------------------------
variable "os_offer_sv" {
  type        = string
  default     = "almalinux-x86_64"
}

variable "os_offer_client" {
  type        = string
  default     = "Windows-11"
}

#------------------------------------
# Virtual Machine OS (os_sku) name
# 2016-Datacenter (Windows Server 2016 Datacenter)
# 2019-Datacenter (Windows Server 2019 Datacenter)
# 2022-datacenter-azure-edition (Windows Server 2022 Datacenter Azure Edition)
# 22_04-lts-gen2 (Ubuntu 22.04 LTS)
# 9-gen2 (Almalinux 9)
# win11-22h2-pro (Windows 11 22H2 Pro)
#------------------------------------
variable "os_sku_sv" {
  type        = string
  default     = "9-gen1"
}

variable "os_sku_client" {
  type        = string
  default     = "win11-22h2-pro"
}

#------------------------------------
# Virtual Machine Server OS Version (os_ver) name
#------------------------------------
variable "os_ver_sv" {
  type        = string
  default     = "latest"
}

#------------------------------------
# Virtual Machine Client OS Version (os_ver) name
#------------------------------------
variable "os_ver_client" {
  type        = string
  default     = "latest"
}

# Load Balancer Name
variable "load_balancer_name" {
  type        = string
  default     = "lb01"
}

```
まぁなんてことないですね。
Windows 11のOS指定の部分くらいでしょうか？
OS関連の指定の部分をコメントアウトで記述しているので、そこをご覧いただければわかると思います。

## リソースグループ
次にリソースグループです。
これはAzureにしかない概念です。
```resource_group.tf
# rg Create
resource "azurerm_resource_group" "rg" {
  location = var.region
  name     = "${var.rg_name_pre}${var.system_name}"
}
```
名前の指定とリージョン指定以外何にもやってないですね。

## 仮想ネットワーク(Vnet)
Vnetです。
```vnet_subnet.tf
#------------------------------------
# web-server vnet Create
#------------------------------------
resource "azurerm_virtual_network" "vnet-web-server" {
  resource_group_name = azurerm_resource_group.rg.name
  location = azurerm_resource_group.rg.location
  name     = "${var.vnet_name_pre}${var.vnet_name_suf01}"
  address_space = [ "${var.vnet_addr_pre01}${var.vnet_addr_suf01}" ]
}

# sn-web-server01 Create
resource "azurerm_subnet" "sn-web-server01" {
  resource_group_name = azurerm_resource_group.rg.name
  name = "${var.sn_name_pre}${var.sn_name_suf01}"
  virtual_network_name = azurerm_virtual_network.vnet-web-server.name
  address_prefixes = [ "${var.vnet_addr_pre01}${var.sn_addr_suf01}" ]
  service_endpoints = [ "Microsoft.Storage" ]
}

#------------------------------------
# client vnet Create
#------------------------------------
resource "azurerm_virtual_network" "vnet-client" {
  resource_group_name = azurerm_resource_group.rg.name
  location = azurerm_resource_group.rg.location
  name     = "${var.vnet_name_pre}${var.vnet_name_suf02}"
  address_space = [ "${var.vnet_addr_pre02}${var.vnet_addr_suf02}" ]
}

# sn-client01 Create
resource "azurerm_subnet" "sn-client01" {
  resource_group_name = azurerm_resource_group.rg.name
  name = "${var.sn_name_pre}${var.sn_name_suf02}"
  virtual_network_name = azurerm_virtual_network.vnet-client.name
  address_prefixes = [ "${var.vnet_addr_pre02}${var.sn_addr_suf02}" ]
  service_endpoints = [ "Microsoft.Storage" ]
}


#------------------------------------
# Create vnet peering
#------------------------------------
# vnet peering01
resource "azurerm_virtual_network_peering" "peering01" {
  name                      = "vnet-web-server-to-vnet-client"
  resource_group_name       = azurerm_resource_group.rg.name
  virtual_network_name      = azurerm_virtual_network.vnet-web-server.name
  remote_virtual_network_id = azurerm_virtual_network.vnet-client.id
}

# vnet peering02
resource "azurerm_virtual_network_peering" "peering02" {
  name                      = "vnet-client-to-vnet-web-server"
  resource_group_name       = azurerm_resource_group.rg.name
  virtual_network_name      = azurerm_virtual_network.vnet-client.name
  remote_virtual_network_id = azurerm_virtual_network.vnet-web-server.id
}
```
ここも[前々回の記事](https://qiita.com/hidekko/items/19871f8fd14304c13e84)と同じくNSGは除いて記載してあります。
AWSと同じくNAT Gatewayはありますが、AzureはAWSと違いNAT Gatewayがあるからと言って、NAT Gatewayに対してDefault Routeを切る必要もありません。
[前々回の記事](https://qiita.com/hidekko/items/19871f8fd14304c13e84)と違い<font color="Red">NAT Gatewayのコードを別の.tfファイルに記載していますが、Route Tableの設定が無いことから**全体のコード記載が少なくて済みます。**
この辺はやっぱりAWSと比べてAzureは単純なんで良いですね。</font>

## NAT Gateway
NAT Gatewayの.tfファイルです。
```nat_gateway.tf
# pip01 Create
resource "azurerm_public_ip" "pip_nat_gw" {
  name                = "${var.pip_name_pre}${var.ng_name_pre}${var.ng_name_suf}"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  sku                 = "Standard"
  allocation_method   = "Static"
}

# NAT Gateway Create
resource "azurerm_nat_gateway" "natgw" {
  resource_group_name     = azurerm_resource_group.rg.name
  location                = azurerm_resource_group.rg.location
  name                    = "${var.ng_name_pre}${var.ng_name_suf}"
  sku_name                = "Standard"
}

# Connect nat gateway to subnet
resource "azurerm_subnet_nat_gateway_association" "subnet-to-ng01" {
  subnet_id = azurerm_subnet.sn-web-server01.id
  nat_gateway_id = azurerm_nat_gateway.natgw.id
}

# Connect pip to nat gateway
resource "azurerm_nat_gateway_public_ip_association" "pip-to-ng01" {
  public_ip_address_id = azurerm_public_ip.pip_nat_gw.id
  nat_gateway_id = azurerm_nat_gateway.natgw.id
}

```
これも特筆すべき点は無いですね。
NAT Gateway作ってパブリックIPアドレス割り当てて、サブネットに割り当てます。
このサブネットに**NAT Gatewayを割り当てる、という行為(操作)行った時点で<font color="Red">該当のサブネットのDefault Routeが書き換わっているんだと想像しています。</font>**
これもネタとしてどこかで検証しましょうかね。

## VM(Web Server)
続いてWeb ServerのVMです。
```linux_vm.tf
#--------------------------------
#web-server * 3 create
#--------------------------------

# nic-vm-app Create
resource "azurerm_network_interface" "nic-vm-app" {
  count = 3
  resource_group_name = azurerm_resource_group.rg.name
  location = azurerm_resource_group.rg.location
  name = "${var.nic_name_pre}${var.vm_name_pre}${var.vm_name_suf}${count.index+1}"

  ip_configuration {
    name = "${var.nic_name_pre}${var.vm_name_pre}${var.vm_name_suf}${count.index+1}${var.ipconf_suf}"
    subnet_id = azurerm_subnet.sn-web-server01.id
    private_ip_address_allocation = "${var.priv_ip_alloc}"
    private_ip_address = "${var.vnet_addr_pre01}0.${count.index+11}"
  }
}

# linux vm Create
resource "azurerm_linux_virtual_machine" "vm-app" {
  count = 3
  name                  = "${var.vm_name_pre}${var.vm_name_suf}${count.index+1}"
  admin_username        = "${var.adminuser}"
  admin_password        = "P@ssw0rd0123"
  disable_password_authentication = false
  location              = azurerm_resource_group.rg.location
  resource_group_name   = azurerm_resource_group.rg.name
  network_interface_ids = ["${element(azurerm_network_interface.nic-vm-app.*.id, count.index+1)}"]
  size                  = "${var.vm_sku}"
  priority              = "Spot" # or Regular
  eviction_policy       = "Deallocate"
  
  os_disk {
    name                 = "${var.vm_osdisk_pre}${var.vm_name_pre}${var.vm_name_suf}${count.index+1}"
    caching              = "ReadWrite"
    storage_account_type = "${var.vm_osdisk_sku}"
  }

  source_image_reference {
    publisher = "${var.os_pub_sv}"
    offer     = "${var.os_offer_sv}"
    sku       = "${var.os_sku_sv}"
    version   = "${var.os_ver_sv}"
  }

  custom_data = "IyEvYmluL2Jhc2gNCnN1ZG8gZG5mIHVwZGF0ZSAteQ0Kc3VkbyBkbmYgaW5zdGFsbCAteSBodHRwZA0Kc3VkbyBzeXN0ZW1jdGwgc3RhcnQgaHR0cGQNCnN1ZG8gc3lzdGVtY3RsIGVuYWJsZSBodHRwZA0Kc3VkbyB1c2VybW9kIC1hIC1HIGFwYWNoZSAke3Zhci5hZG1pbnVzZXJ9DQpzdWRvIGNob3duIC1SICR7dmFyLmFkbWludXNlcn06YXBhY2hlIC92YXIvd3d3DQpzdWRvIGNobW9kIDI3NzUgL3Zhci93d3cNCmZpbmQgL3Zhci93d3cgLXR5cGUgZCAtZXhlYyBzdWRvIGNobW9kIDI3NzUge30gXDsNCmZpbmQgL3Zhci93d3cgLXR5cGUgZiAtZXhlYyBzdWRvIGNobW9kIDA2NjQge30gXDsNCmVjaG8gIkl0IFdvcmtzICEiID4vdmFyL3d3dy9odG1sL2luZGV4Lmh0bWwNCmhvc3RuYW1lID4+L3Zhci93d3cvaHRtbC9pbmRleC5odG1sDQpzdWRvIGZpcmV3YWxsLWNtZCAtLWxpc3QtYWxsIC0tem9uZT1wdWJsaWMgLS1wZXJtYW5lbnQNCnN1ZG8gZmlyZXdhbGwtY21kIC0tcmVtb3ZlLXNlcnZpY2U9Y29ja3BpdCAtLXpvbmU9cHVibGljIC0tcGVybWFuZW50DQpzdWRvIGZpcmV3YWxsLWNtZCAtLWxpc3QtYWxsIC0tem9uZT1wdWJsaWMgLS1wZXJtYW5lbnQNCnN1ZG8gZmlyZXdhbGwtY21kIC0tYWRkLXNlcnZpY2U9aHR0cCAtLXpvbmU9cHVibGljIC0tcGVybWFuZW50DQpzdWRvIGZpcmV3YWxsLWNtZCAtLWxpc3QtYWxsIC0tem9uZT1wdWJsaWMgLS1wZXJtYW5lbnQNCnN1ZG8gZmlyZXdhbGwtY21kIC0tcmVsb2FkDQpzdWRvIHNldHNlYm9vbCAtUCBodHRwZF9jYW5fbmV0d29ya19jb25uZWN0IDE="

  computer_name  = "${var.vm_name_pre}${var.vm_name_suf}${count.index+1}"

  boot_diagnostics {
    storage_account_uri = azurerm_storage_account.storage_account.primary_blob_endpoint
  }
  zone = count.index +1
  depends_on = [
    azurerm_subnet_nat_gateway_association.subnet-to-ng01
  ]
}

```
はい。
私の過去のどこかの記事でも記載していますが、AzureでLinux VMを構築し、ヒアドキュメントでCloud-initを動かしたい場合、ヒアドキュメントをbase 64でエンコードしなくてはなりません。
```
custom_data = 
```
の部分がそれにあたります。
もう一点、このヒアドキュメントの書き方を調べているときに知った小ネタが
```
count = 3
```
と、これに対応する
```
name = "${var.nic_name_pre}${var.vm_name_pre}${var.vm_name_suf}${count.index+1}"
```
のようにこの.tfファイル内に出現する```count.index```の部分です。
**同様の構成のVMを構築する場合、```count=何某```で定義すると、```何某```の部分で指定した回数分、<font color="Red">初期値0からカウントアップしながら処理を繰り返してくれます。</font>**
これはAWSでもできると思うんですが、今回初めて知ったのでこのコードから使います。
これが無ければ同様の処理でも変数内容をちょっとずつ変えながらずらずらとほぼ同じコードを記載する必要があるので、前述のヒアドキュメントをbase 64でエンコードする処理以外はほぼ同じ長さになります。
また今回Windows VMは.tfファイルを分けています(AWSではWebサーバもWindowsのEC2も同じ.tfファイル内に記載)ので、その分コードが短くなっています。

## ヒアドキュメントの中身
```web_almalinux_init_settings_01.sh
#!/bin/bash
sudo dnf update -y
sudo dnf install -y httpd
sudo systemctl start httpd
sudo systemctl enable httpd
sudo usermod -a -G apache ${var.adminuser}
sudo chown -R ${var.adminuser}:apache /var/www
sudo chmod 2775 /var/www
find /var/www -type d -exec sudo chmod 2775 {} \;
find /var/www -type f -exec sudo chmod 0664 {} \;
echo "It Works !" >/var/www/html/index.html
hostname >>/var/www/html/index.html
sudo firewall-cmd --list-all --zone=public --permanent
sudo firewall-cmd --remove-service=cockpit --zone=public --permanent
sudo firewall-cmd --list-all --zone=public --permanent
sudo firewall-cmd --add-service=http --zone=public --permanent
sudo firewall-cmd --list-all --zone=public --permanent
sudo firewall-cmd --reload
sudo setsebool -P httpd_can_network_connect 1
```
はい。
大したことしてないですね。
AWSのEC2はホスト名とAWS Console上のEC2名が違うのでこのヒアドキュメント内でホスト名をAWS Console上のEC2名と同じ値を設定していましたが、AzureはVM構築時に指定するVM名がそのままホスト名になるため、この部分の処理が無いくらいですかね。
<font color="Red">**地味ですが、こういう細かい部分が後発サービスの影響かAzureは行き届いていますね！**</font>

### ついでにbase 64でエンコードするコマンド
私の作業用端末はWindows 11 Proなので、他のOSだとどうかわかんないですが、以下のコマンドで普通のテキストファイルからbase 64にエンコードしました。
```
certutil -f -encode "入力ファイル" "出力ファイル"
```
コマンドプロンプトで実行しました。
実行すると```-----Begin なんたらかんたら------```みたいな文字からはじまる、テキストファイルが出力されます。
中身は改行されているので、1行になるように成型してから.tfファイルの```custom_data = ```の後に記載してください。

<font color="Red">**Azureで面倒なのはこの辺でしょうかね・・・**</font>
でもまぁこれはヒアドキュメント内で実行している作業が機微情報に関わる場合は難読化されている方がセキュリティ的に安全という観点からでしょうか？
今回は大したことやっていないのでただ面倒なだけですけどね・・・

## VM(Windows 11)
Windows 11の.tfファイルです。
```windows_vm.tf
#------------------------------------
# pip01 Create
#------------------------------------
resource "azurerm_public_ip" "pip-client01" {
  name                = "${var.pip_name_pre}${var.vm_name_pre}${var.vm_name_suf_client}"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  sku                 = "Standard"
  allocation_method   = "Static"
}

#------------------------------------
# nic01 (attached for vm01) Create
#------------------------------------
resource "azurerm_network_interface" "nic-client01" {
  resource_group_name = azurerm_resource_group.rg.name
  location = azurerm_resource_group.rg.location
  name = "${var.nic_name_pre}${var.vm_name_pre}${var.vm_name_suf_client}"

  ip_configuration {
    name = "${var.nic_name_pre}${var.vm_name_pre}${var.vm_name_suf_client}${var.ipconf_suf}"
    subnet_id = azurerm_subnet.sn-client01.id
    private_ip_address_allocation = "${var.priv_ip_alloc}"
    private_ip_address = "172.16.1.11"
    public_ip_address_id = azurerm_public_ip.pip-client01.id
  }
}

#------------------------------------
# windows vm01 Create
#------------------------------------
resource "azurerm_windows_virtual_machine" "vm-client01" {
  name                  = "${var.vm_name_pre}${var.vm_name_suf_client}"
  admin_username        = "${var.adminuser}"
#  admin_password        = random_password.password.result
  admin_password        = "P@ssw0rd0123"
  location              = azurerm_resource_group.rg.location
  resource_group_name   = azurerm_resource_group.rg.name
  network_interface_ids = [azurerm_network_interface.nic-client01.id]
  size                  = "${var.vm_sku}"
  priority              = "Spot" # or Regular
  eviction_policy       = "Deallocate"
  
  os_disk {
    name                 = "${var.vm_osdisk_pre}${var.vm_name_pre}${var.vm_name_suf_client}"
    caching              = "ReadWrite"
    storage_account_type = "${var.vm_osdisk_sku}"
  }

  source_image_reference {
    publisher = "${var.os_pub_client}"
    offer     = "${var.os_offer_client}"
    sku       = "${var.os_sku_client}"
    version   = "${var.os_ver_client}"
  }

  boot_diagnostics {
    storage_account_uri = azurerm_storage_account.storage_account.primary_blob_endpoint
  }
}

```


## Load Balancer(LB)
続いてLBです。
```lb.tf
#------------------------------------
# Associate Network Interface to the Backend Pool of the Load Balancer
#------------------------------------
resource "azurerm_network_interface_backend_address_pool_association" "lb_be_lb_pool_association" {
  count = 3
  network_interface_id = "${element(azurerm_network_interface.nic-vm-app.*.id, count.index)}"
  ip_configuration_name = "${var.nic_name_pre}${var.vm_name_pre}${var.vm_name_suf}${count.index+1}${var.ipconf_suf}"
  backend_address_pool_id = azurerm_lb_backend_address_pool.lb_be_pool.id
}

#------------------------------------
# Create an Internal Load Balancer
#------------------------------------
resource "azurerm_lb" "lb" {
  name = var.load_balancer_name
  location = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  sku = "Standard"

  frontend_ip_configuration {
    name = "lb-frontend-ip"
    subnet_id = azurerm_subnet.sn-web-server01.id
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_lb_backend_address_pool" "lb_be_pool" {
  loadbalancer_id = azurerm_lb.lb.id
  name = "be-pool01"
}

resource "azurerm_lb_probe" "lb_probe" {
  loadbalancer_id = azurerm_lb.lb.id
  name = "lb-probe01"
  port = 80
}

resource "azurerm_lb_rule" "lb_rule" {
  loadbalancer_id = azurerm_lb.lb.id
  name = "lb-rule01"
  protocol = "Tcp"
  frontend_port = 80
  backend_port = 80
  disable_outbound_snat = true
  frontend_ip_configuration_name = "lb-frontend-ip"
  probe_id = azurerm_lb_probe.lb_probe.id
  backend_address_pool_ids = [azurerm_lb_backend_address_pool.lb_be_pool.id]
}
```
**<font color="Orange">AWSだと各EC2のSecurity Groupに対してLBのバックエンドプール指定を行いました</font>が、<font color="Blue">AzureではLBの背後に配置されるVMのNICを指定します。</font>**
以前私のどこかの記事でもやった、AWSとAzureそれぞれのイメージカラーの文字色で違いを記載しました。
どうですかね？
分かりやすいですかね？
今思い付いたんで、これより前に記載した部分の修正はご勘弁を！

## Storage Account
Linux VM、Windows VM共にブート診断設定を設定しており、この出力先にあたるStorage Accountを構築する.tfファイルです。
```storage.tf
# Create storage account for boot diagnostics
resource "azurerm_storage_account" "storage_account" {
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = azurerm_resource_group.rg.location
  name                     = "stdiag${random_id.random_id.hex}"
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_account_network_rules" "for_multiple_subnet_id" {
  storage_account_id = azurerm_storage_account.storage_account.id
  default_action = "Deny"
  virtual_network_subnet_ids = [azurerm_subnet.sn-client01.id,azurerm_subnet.sn-web-server01.id]
  ip_rules = ["123.225.10.132"]
}
```
はい。
なんてことは無いですね。

## NSG
AWSでいうことろのSecurity GroupであるNetwork Security Group(NSG)の.tfファイルです。
Security GroupとNSGの違いは[過去のこの記事](https://qiita.com/hidekko/items/98e68fd2fa10036da749)をご覧ください。
```nsg.tf
#------------------------------------
# nsg-sn-web-server01 Create
#------------------------------------

resource "azurerm_network_security_group" "nsg-sn-web-server01" {
  resource_group_name = azurerm_resource_group.rg.name
  location = azurerm_resource_group.rg.location
  name = "${var.nsg_name_pre}${azurerm_subnet.sn-web-server01.name}"

## InBound Rule
  security_rule  {
    name = "AllowVnetInBound"
    priority = 100
    direction = "Inbound"
    access = "Allow"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    source_address_prefix = "VirtualNetwork"
    destination_address_prefix = "VirtualNetwork"
  }

 security_rule  {
    name = "AllowAzureLoadBalancerInBound"
    priority = 101
    direction = "Inbound"
    access = "Allow"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    source_address_prefix = "AzureLoadBalancer"
    destination_address_prefix = "*"
  }

  security_rule {
    name = "AllowInBoundSSHOnPremises"
    priority = 1001
    direction = "Inbound"
    access = "Allow"
    protocol = "Tcp"
    source_port_range = "*"
    destination_port_range = "22"
    source_address_prefix = "123.225.10.132"
    destination_address_prefix = "*"
  }

  security_rule {
    name = "AllowInBoundHTTPOnPremises"
    priority = 1002
    direction = "Inbound"
    access = "Allow"
    protocol = "Tcp"
    source_port_range = "*"
    destination_port_range = "80"
    source_address_prefix = "123.225.10.132"
    destination_address_prefix = "*"
  }

  security_rule {
    name = "DenyAllInBound"
    priority = 4096
    direction = "Inbound"
    access = "Deny"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    source_address_prefix = "*"
    destination_address_prefix = "*"
  }

## OutBound Rule
  security_rule  {
    name = "AllowVnetOutBound"
    priority = 100
    direction = "Outbound"
    access = "Allow"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    source_address_prefix = "VirtualNetwork"
    destination_address_prefix = "VirtualNetwork"
  }

 security_rule  {
    name = "AllowAzureFrontDoor.FirstPartyHttpOutBound"
    priority = 101
    direction = "Outbound"
    access = "Allow"
    protocol = "Tcp"
    source_port_range = "*"
    destination_port_range = "80"
    source_address_prefix = "*"
    destination_address_prefix = "AzureFrontDoor.FirstParty"
    description = "Allow Windows Update Rule01"
  }

 security_rule  {
    name = "AllowAzureUpdateDeliveryHttpsOutBound"
    priority = 102
    direction = "Outbound"
    access = "Allow"
    protocol = "Tcp"
    source_port_range = "*"
    destination_port_range = "443"
    source_address_prefix = "*"
    destination_address_prefix = "AzureUpdateDelivery"
    description = "Allow Windows Update Rule02"
  }

  security_rule {
    name = "AllowAllOutBound"
    priority = 4095
    direction = "Outbound"
    access = "Allow"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    source_address_prefix = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name = "DenyAllOutBound"
    priority = 4096
    direction = "Outbound"
    access = "Deny"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    source_address_prefix = "*"
    destination_address_prefix = "*"
  }

}

#------------------------------------
# Connect nsg-sn-web-server01 to sn-web-server01
#------------------------------------
resource "azurerm_subnet_network_security_group_association" "nsg-sn-web-server01-to-sn-web-server01" {
  subnet_id =  azurerm_subnet.sn-web-server01.id
  network_security_group_id = azurerm_network_security_group.nsg-sn-web-server01.id
}

#------------------------------------
# nsg-sn-client01 Create
#------------------------------------

resource "azurerm_network_security_group" "nsg-sn-client01" {
  resource_group_name = azurerm_resource_group.rg.name
  location = azurerm_resource_group.rg.location
  name = "${var.nsg_name_pre}${azurerm_subnet.sn-client01.name}"

## InBound Rule
  security_rule  {
    name = "AllowVnetInBound"
    priority = 100
    direction = "Inbound"
    access = "Allow"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    source_address_prefix = "VirtualNetwork"
    destination_address_prefix = "VirtualNetwork"
  }

 security_rule  {
    name = "AllowAzureLoadBalancerInBound"
    priority = 101
    direction = "Inbound"
    access = "Allow"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    source_address_prefix = "AzureLoadBalancer"
    destination_address_prefix = "*"
  }

  security_rule {
    name = "AllowInBoundSSHOnPremises"
    priority = 1001
    direction = "Inbound"
    access = "Allow"
    protocol = "Tcp"
    source_port_range = "*"
    destination_port_range = "3389"
    source_address_prefix = "123.225.10.132"
    destination_address_prefix = "*"
  }

  security_rule {
    name = "DenyAllInBound"
    priority = 4096
    direction = "Inbound"
    access = "Deny"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    source_address_prefix = "*"
    destination_address_prefix = "*"
  }

## OutBound Rule
  security_rule  {
    name = "AllowVnetOutBound"
    priority = 100
    direction = "Outbound"
    access = "Allow"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    source_address_prefix = "VirtualNetwork"
    destination_address_prefix = "VirtualNetwork"
  }

 security_rule  {
    name = "AllowAzureFrontDoor.FirstPartyHttpOutBound"
    priority = 101
    direction = "Outbound"
    access = "Allow"
    protocol = "Tcp"
    source_port_range = "*"
    destination_port_range = "80"
    source_address_prefix = "*"
    destination_address_prefix = "AzureFrontDoor.FirstParty"
    description = "Allow Windows Update Rule01"
  }

 security_rule  {
    name = "AllowAzureUpdateDeliveryHttpsOutBound"
    priority = 102
    direction = "Outbound"
    access = "Allow"
    protocol = "Tcp"
    source_port_range = "*"
    destination_port_range = "443"
    source_address_prefix = "*"
    destination_address_prefix = "AzureUpdateDelivery"
    description = "Allow Windows Update Rule02"
  }

  security_rule {
    name = "AllowAllOutBound"
    priority = 4095
    direction = "Outbound"
    access = "Allow"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    source_address_prefix = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name = "DenyAllOutBound"
    priority = 4096
    direction = "Outbound"
    access = "Deny"
    protocol = "*"
    source_port_range = "*"
    destination_port_range = "*"
    source_address_prefix = "*"
    destination_address_prefix = "*"
  }

}

#------------------------------------
# Connect nsg-sn-client01 to sn-client01
#------------------------------------
resource "azurerm_subnet_network_security_group_association" "nsg-sn-client01-to-sn-client01" {
  subnet_id =  azurerm_subnet.sn-client01.id
  network_security_group_id = azurerm_network_security_group.nsg-sn-client01.id
}
```
これもまぁなんてことないですね。
特筆すべき点が無さすぎてコメントに困りますね。

## Output
最後にoutput.tfです。
なんでこの値(ストレージアカウントのリソース名)を出力したのかというと、デプロイするまでリソース名がわからないんですよね。
ただ別にわかんなくてもどうってことは無いです。
この値よりもNAT GatewayのパブリックIPアドレスの方が大事なんじゃないかなと今更ながら思いました。
```output.tf
output "diag_storage_account" {
  value = azurerm_storage_account.storage_account.name
}
```

# 動作確認
```terraform apply```コマンドを実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/a0f3a04f-2709-2f08-8f5b-f96c012fc4a4.png)
同じような構成でも、Azureでは34個のリソースをデプロイしていますね。
[AWSは47個](https://qiita.com/hidekko/items/19871f8fd14304c13e84#%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D)でした。

Internet GatewayやRoute Table、EC2ごとのSecurity Groupなど、それぞれ仕様に差異があるため同様の構成にしてもAzureの方がリソースは少ないですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/6efe7449-1661-aa9c-e08c-f9d7545a8b3c.png)

はい。
とまぁこのように問題なくデプロイできました。

## デプロイされたリソース確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/26d6db90-5cca-0f3e-9e30-642de5fc5fab.png)
こちらも問題なさそうです。

## Web Serverの動作確認及びLBの設定確認
AWSのALBだと正常性Probeで一発で確認できましたが、Azureだとそうはいきませんね・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/1b42c580-1056-d3dd-430b-4ed244d785d8.png)
どこみても正常性Probeの結果が正常かわからない・・・
**<font color="Red">この辺はAWSの方が便利ですね。</font>**
Azureも確かApplication Gatewayだとバックエンドプールの正常性が確認できたはずですが・・・
と思いきや、バックエンドプール見てみると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/6a7e0b35-4e38-8ae6-fa16-6f28911c7dd8.png)
<font color="Red">**？**</font>
![LB_Backendpool01_集中線.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/59cc17b2-a6a7-8c54-4764-646d49d58acb.png)
<font color="Red">**？？？
とりあえずリソース(VM)が実行中である、ということはわかりました！**</font>
でもこれではWeb Serverとして動作しているかどうかわかんないんで、結局クライアントPC相当のWindows 11にログインして確認します。

### Windows 11にログイン
Azure上のWindows 11にRDPしてみると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/b2b9cdad-6be8-2d69-cc59-209e41386d6f.png)
はい。
Windows 11の初期セットアップですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/35c036cc-734b-2284-d5ea-7d24285d1b19.png)
まぁ```winver```たたいてみても普通にWindows 11で起動していますね。
なんてことない。
コード通りの挙動です。

英語OSなのと、今回Webサーバの動作確認なのでEdgeで確認しますが、Edge初回起動時のセットアップもうざいですがチャチャっと済ませてLBのフロントのローカルIPアドレスにアクセスします。
ついでにWindows 11のローカルIPも晒しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/0eab06a5-22b9-abab-2486-1b372c375e04.png)
Windows 11、ホスト名がvm-clinet01で、IPアドレスが172.16.1.11、ですね。

LBのフロントのローカルIPアドレスは
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/9172c474-b344-bb3a-aea1-9d3f1b07fa8a.png)
10.0.0.4
Azure上の仮想ネットワークのDHCPで割り当てられる最初のIPアドレスですね。

### LBへのアクセス
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/e521abdf-69d7-c13c-2d51-a8f05a2001ba.png)
はい。
設計通り
It Works! vm-app01(hostnameの実行結果)
が出力されてますね。
ちゃんとLBの背後の3台ともWeb Serverが動作しているかなー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/491d51a2-76d4-3ac1-fc3b-37052a5ff25f.png)
はい。
ちゃんと3台ともWeb Serverは動いてますね。

<font color="Red">**やっぱりTerraformは素晴らしいですね！**</font>

# Spot Instanceで動作しているかどうかの確認
**はい。**
<font color="Red">お金の話です。</font>
<font color="Red">**大事な話ですね！**</font>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/f1a4e09c-7dc6-b2d5-cf0c-fc6c17faed14.png)
はい。
こんな感じで検証するには勿体ない、**そこそこ**<font color="Red">**ええスペックのマシンデプロイ**</font>**してます。**
これが正規料金だったら・・・ﾋｪｰ・・・ですね。

# そんで結局Spot Instanceなの？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/6e9cfdbb-1fdd-bbbe-2a1c-611c559aa229.png)
<font color="Red">**キタ！**</font>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/bada68b1-7a2c-5080-a123-646a0aaae0c7.png)
<font color="Red">**ｷﾀｷﾀ!!!**</font>
![SpotInstance_集中線.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/3d65040f-498b-28af-c3e0-9404c8c6c430.png)
<font color="Red">**見よ！これがSpot Instanceだ！**</font>

## もうちょっとちゃんと表示してみて簡単に解説してみる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/49286af5-fe37-38ba-395e-08a96a2cdf06.png)

こんな感じですね。
解説
**Azure スポットの削除の種類**
というのが該当VMをSpot Instanceとして動作させていて、「Spot Instanceを維持できなくなる条件」を記載しています。
ここは図にある通り**容量**という設定かまた別の設定で**価格**という設定の2種類が選択でき、**容量**は該当VMをデプロイしているリージョン内でSpot Instanceを利用できるCPUやメモリの空き**容量**が確保できるかどうかを判断基準とし、一方の**価格**を選択すると、別途価格の閾値を決めるパラメーターが出現し、Spot Instanceは基本的に時価なので、この時価の価格が元々のスペック、今回だとD4v5の正規料金の何%割引を上回ったら(ちょっとわかりづらいですが、何%割引を上回ったら、なので、正規料金を超える設定はできない。当たり前ですね。割引で利用できるオプションの意味が無いですからね)Spot Intanceを停止するかどうかの設定を行います。
容量がリージョンの容量に対して、価格はユーザーがVMに対して支払う価格にSpot Intanceの存在意義を求めていることがわかりますね。
そして**Azure スポットの削除ポリシー**は図の通り**停止/割り当て解除**という設定か、別の設定で**削除**を選択できます。
**停止/割り当て解除**を指定している場合、**Spot Instanceを維持できなく条件**に抵触した場合、該当VMを停止/割り当て解除(VMの課金がかからない状態)にするのか、VM自体を**削除**するのか選択します。
<font color="Red">**Spot Instanceの設定と動作は完璧に把握できましたね！**</font>
<font color="Red">**これで~~お財布を気にすることなく~~Scrap＆Buildを繰り返せますね！**</font>

最後は勢いで押し切った感ありますが、もう眠いので本日はこれまで！

# 2024年6月23日追記
Terraformの記事を書いていると、記事を書き終わったらそのまま環境を放置しちゃいそうになるんですよね。
元ネタになった[前々回の記事](https://qiita.com/hidekko/items/19871f8fd14304c13e84)でもそうでした。
今回はちゃんと環境を壊して(Scrapして)課金をとめるところまで書きます。
今日中には終わるはず。

# terraform destroy
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/347e0429-5516-0c82-5528-7541d5f31a04.png)
はい。
```terraform destory```コマンドを発行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/79686416-dd07-a676-9b91-5b71936722a5.png)
ちゃんと構成したのと同じ数のリソースをdestroyしてくれることを確認します。
今回だと34リソースですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/ca32f3af-4eb4-ce10-9f4c-9395d912e0ef.png)
ちゃんと```yes```と打ってあげて```Enter```押しちゃいましょう！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/fecb9cc3-9337-5d5d-7c43-b43df4e8f335.png)
<font color="Red">**消えた！**</font>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643392/62860c45-2212-90a2-9d89-c4eb8e8348b6.png)

これにて本当に本日はこれまで！