TerraformでAzure環境を構築する

# はじめに
Infrastructure as Code（IaC）にチャレンジしてみました。
Terraformを使ってAzureのリソースを作成します。

# 事前準備
* [Azure CLIのインストール](https://docs.microsoft.com/ja-jp/cli/azure/install-azure-cli-windows?view=azure-cli-latest)
* [Terraformのインストール](https://www.terraform.io/downloads.html)
* VSCodeに拡張機能インストール（筆者がVSCodeで記述したいためです）
    * [Azure Terraform](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azureterraform)
    * [Terraform](https://marketplace.visualstudio.com/items?itemName=HashiCorp.terraform)  
    
# ディレクトリ構成
envの下に各環境のディレクトリを用意して環境依存のリソースに対応します。
共通で利用できるものに関してはディレクトリの最上位に配置します。

```plaintext
terraform
│  availability.tf
│  recovery_services_vault.tf
│  resource_group.tf
│  storage_account.tf
│  variables.tf
│  virtual_network.tf
│
├─env
│  └─sample
│          main.tf
│          sample-lb.tf
│          sample-nsg.tf
│          sample-vm.tf
│          variables.tf
│
└─modules
    ├─lb
    │      lb.tf
    │      outputs.tf
    │      variables.tf
    │
    ├─nsg
    │      nsg.tf
    │      variables.tf
    │
    └─vm
            outputs.tf
            variables.tf
            vm.tf
```

# 定義
## 基本の定義
ディレクトリの最上位で定義しているリソースの作成は`env\sample\variables.tf`で宣言した変数と値を`env\sample\main.tf`で最上位の`variables.tf`内の変数に渡すことで行います。
環境が複数ある場合は、`env\sample2`のようなディレクトリを作成して対応します。

```:env\sample\variables.tf
locals {
  ##### Azureログイン情報 #####
  login = {
    subscription_id = "xxxxxx"
    tenant_id       = "xxxxxx"
  }

  location = {
    main = "japaneast"
    sub  = "japanwest"
  }

  ##### リソースグループ #####
  resource_group = {
    rg1 = {
      name     = "sample-rg"
      location = local.location.main
    }
  }

～～～省略～～～
}
```

変数の渡し方は以下のようになります。

```:env\sample\main.tf
provider "azurerm" {
  version = "~>2.0"
  features {}
  subscription_id = local.login.subscription_id
  tenant_id       = local.login.tenant_id
}

module "main" {
  source = "../../"

  resource_group            = local.resource_group
  virtual_network           = local.virtual_network
  subnet                    = local.subnet
  public_ip                 = local.public_ip
  availability_set          = local.availability_set
  storage_account           = local.storage_account
  recovery_services_vault   = local.recovery_services_vault
  backup_policy_vm_daily    = local.backup_policy_vm_daily
  backup_policy_vm_weekly   = local.backup_policy_vm_weekly
  proximity_placement_group = local.proximity_placement_group
  backup_storage_account    = local.backup_storage_account
}
```

最上位の`variables.tf`は値を受け取るだけです。

```:variables.tf
variable "resource_group" {}

variable "virtual_network" {}

variable "subnet" {}

variable "public_ip" {}

variable "availability_set" {}

variable "storage_account" {}

variable "recovery_services_vault" {}

variable "backup_policy_vm_daily" {}

variable "backup_policy_vm_weekly" {}

variable "proximity_placement_group" {}

variable "backup_storage_account" {}
```

## モジュールの定義
設定項目が多いVM、NSG、LBは可読性を高めるため、モジュール化して1リソース＝1ファイルで定義することにしました。
VMの場合は以下`env\sample\sample-vm.tf`のように定義した値を`modules\vm\variables.tf`に渡します。
LBやNSGも同じ構成です。

```plaintext:env\sample\sample-vm.tf
locals {
  sample-vm_var = {
    resource_group_key            = "rg1"
    subnet_key                    = "sub1"
    ehable_boot_diagnostics       = false
    ehable_backup                 = false
    storage_account_key           = "sa1"
    availability_set_key          = ""
    proximity_placement_group_key = ""
    public_ip_key                 = ""
    recovery_services_vault_key   = ""
    backup_policy_vm_key          = ""
  }
}

locals {
  ##### 仮想マシン #####
  sample-vm_vm = {

    name                         = "sample-vm"
    resource_group_name          = module.main.resource_group[local.sample-vm_var.resource_group_key].name
    location                     = module.main.resource_group[local.sample-vm_var.resource_group_key].location
    vm_size                      = "Standard_D2s_v3"
    availability_set_id          = length(local.sample-vm_var.availability_set_key) > 0 ? module.main.availability_set[local.sample-vm_var.availability_set_key].id : ""
    proximity_placement_group_id = length(local.sample-vm_var.proximity_placement_group_key) > 0 ? module.main.proximity_placement_group[local.sample-vm_var.proximity_placement_group_key].id : ""

    ##### ディスク関連 #####
    delete_os_disk_on_termination    = false
    delete_data_disks_on_termination = false
    ultra_ssd_enabled                = false

    ##### 仮想マシンイメージ #####
    image = {
      is_windows_image = true
      os_id            = ""
      publisher        = "MicrosoftWindowsServer"
      offer            = "WindowsServer"
      sku              = "2019-Datacenter"
      version          = "latest"
    }

    ##### OSディスク #####
    # managed_disk_type : Standard_LRS, StandardSSD_LRS, Premium_LRS
    os_disk = {
      managed_disk_type         = "Standard_LRS"
      write_accelerator_enabled = false
    }

    ##### 仮想ネットワーク #####
    network = {
      subnet_id                     = module.main.subnet[local.sample-vm_var.subnet_key].id
      private_ip_address            = "10.0.0.5"
      public_ip_address_id          = length(local.sample-vm_var.public_ip_key) > 0 ? module.main.public_ip[local.sample-vm_var.public_ip_key].id : ""
      enable_accelerated_networking = false
    }

    ##### ローカル管理者 #####
    os_profile = {
      admin_username = "sampleadmin"
      admin_password = "Hogehoge12#$"
    }

    ##### ブート診断 #####
    boot_diagnostics = {
      ehable_boot_diagnostics = local.sample-vm_var.ehable_boot_diagnostics
      primary_blob_endpoint   = local.sample-vm_var.ehable_boot_diagnostics ? module.main.storage_account[local.sample-vm_var.storage_account_key].primary_blob_endpoint : ""
    }

    ##### Windows個別設定 #####
    windows = {
      timezone = "Tokyo Standard Time"
    }

    ##### Linux個別設定 #####
    linux = {
      enable_ssh_key = false
      ssh_key        = ""
    }

    ##### Backup #####
    backup = {
      recovery_vault_name = local.sample-vm_var.ehable_backup ? module.main.recovery_services_vault[local.sample-vm_var.recovery_services_vault_key].name : ""
      backup_policy_id    = local.sample-vm_var.ehable_backup ? module.main.backup_policy_vm_daily[local.sample-vm_var.backup_policy_vm_key].id : ""
    }

    ##### Load Balancer #####
    load_balancer = {
      ehabled_load_balancer   = false
      backend_address_pool_id = module.sample-lb.lb_backend_address_pool.id
    }

    tags = {
      name = "サンプル仮想マシン"
    }
  }

  ##### データディスク #####
  sample-vm_data_disk = {
    "disk1" = {
      disk_size_gb              = "16"
      managed_disk_type         = "Standard_LRS"
      caching                   = "ReadOnly"
      lun                       = 0
      write_accelerator_enabled = false
    }
  }

}

##### 仮想マシンの作成 #####
module "sample-vm" {
  source = "../../modules/vm/"

  vm        = local.sample-vm_vm
  data_disk = local.sample-vm_data_disk
}

```

受け取り側

```plaintext:modules\vm\variables.tf
variable "vm" {}

variable "data_disk" {}
```

## リソース作成有無の制御
リソース有無による修正箇所を少なくするため、`env\sample`内の変数をコメントアウトすることでも対応できるようにしました。

例えばパブリックIPアドレスを作る必要がない場合は以下のようにします。

```:evn\sample\variables.tf
public_ip = {
  # pip1 = {
  #   name               = "vm1-pip"
  #   resource_group_key = "rg1"
  #   sku                = "Basic"
  #   allocation_method  = "Static"
  # }
}
```

パブリックIPアドレスのリソース作成部分は`for_each`を使って制御しています。
変数`public_ip`のmapが存在しない場合は、スルーされます。

```:virtual_network.tf
resource "azurerm_public_ip" "main" {
  for_each            = var.public_ip
  name                = each.value.name
  resource_group_name = azurerm_resource_group.main[each.value.resource_group_key].name
  sku                 = each.value.sku
  location            = azurerm_resource_group.main[each.value.resource_group_key].location
  allocation_method   = each.value.allocation_method
}
```

# 実行
定義が準備できたので実際にリソースを作ってみます。
今回はサービスプリンシパルを使用しないため、事前にAzure CLIでAzureにログインします。

```
az login
```

`env\sample`に移動後、`terraform init`で初期化します。

```
terraform init

Initializing modules...
- main in ..\..
- sample-lb in ..\..\modules\lb
- sample-nsg in ..\..\modules\nsg
- sample-vm in ..\..\modules\vm

Initializing the backend...

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

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.
```

実行計画レビューを行い、エラーがないことを確認します。

```
terraform plan

～～～省略～～～

Plan: 15 to add, 0 to change, 0 to destroy.
```

`terraform apply`でリソース作成します。

```
terraform apply

～～～省略～～～

Apply complete! Resources: 15 added, 0 changed, 0 destroyed.
```

ポータルからもリソースが作成されたことが確認できました。
<img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/5db77e90-23d2-b865-b4e9-f9b0e2aeb055.png" width=80%>

# 最後に
一度定義すると簡単にリソースの作成、変更、削除が行えるのは素晴らしいですね。
今回汎用的に使えるように定義しようとして上手くいかず大分苦戦しました。
ただ、そのおかげで少しはTerraformの構文が理解できたのではないかと思っています。

今回書いたサンプルコードはGitHubで公開していますので良かったら利用してみてください。
至らない点も多々あると思いますので使いやすいように修正してお使いいただければと思います。
https://github.com/ytamu2/Terraform-Azure-Template