Terraform を使用して Azure Linux VM を作成する

## 目的
Azure learnで公開されている[Terraform を使用して Linux VM を作成するチュートリアル](https://learn.microsoft.com/ja-jp/azure/virtual-machines/linux/quick-create-terraform?tabs=azure-cli)を参考に開発した内容について、チュートリアル通りでは動作しなかった点や工夫した点を、備忘録的に残しておきます。


## 環境
```sh
$ terraform -v
Terraform v1.10.2
on linux_amd64
```

## 遭遇したエラー
まずチュートリアルの通りやってもエラーが出てしまう部分の紹介とその解決策です。

### Public Keyを取得したい！
チュートリアルでは下記のコードの通り、作成したSSHキーをoutputしていますがエラーがでます。
``` ssh.tf
resource "azapi_resource_action" "ssh_public_key_gen" {
  type        = "Microsoft.Compute/sshPublicKeys@2022-11-01"
  resource_id = azapi_resource.ssh_public_key.id
  action      = "generateKeyPair"
  method      = "POST"

  response_export_values = ["publicKey", "privateKey"]
}
...
output "key_data" {
  # 下記の構文では,outputに"publicKey"というAttributeが存在しない、というエラーが出る
  value = azapi_resource_action.ssh_public_key_gen.output.publicKey
}
```
正しくは、次のように書き替える必要があります。
``` ssh.tf
...
output "key_data" {
  value = jsondecode(azapi_resource_action.ssh_public_key_gen.output).publicKey
}
```
下記サイトのAttributes Referenceを参考に実装しました。
迷ったら公式リファレンスを読もう！と心に深く刻みました。

https://registry.terraform.io/providers/Azure/azapi/1.12.1/docs/resources/azapi_resource_action


### 突然のエラー
> Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.

突然上記のエラーが出るようになりました(つい昨日までは何ともなかったのに！)
次のサイトの情報を頼りに調べを進めると、SASトークンを発行するマシンのシステム時刻が合っているか確認しろ！とあります。

https://learn.microsoft.com/en-us/answers/questions/1821066/azure-blob-storage-not-being-able-to-authenticate

> Verify System Time (Make sure that the system time on the machine generating the SAS token is accurate. A significant time discrepancy could lead to authentication failures)

そんな馬鹿なと思い`date`コマンドで確認したところ、現在時刻とは大幅にズレていることがわかりました。
結局のところ、PCを再起動し事なきを得ました。
稀なエラーケースかと思いましたが、記事化することで、同じエラーでお困りの方の一助になれば嬉しいです。


## 工夫したところ
さて次は、チュートリアルを参考にしつつ本番運用を念頭に工夫した点を記載します。

### module化
各モジュールの拡張性と可読性、テスト容易性を意識した設計にしました。
特に、
* environment
    * デプロイ先の環境を分けるディレクトリを用意
* example
    * 単体テスト用のシンプルな構成で起動するリソース用ディレクトリを用意

今回は取り入れなかったのですが、
より複雑な構成となる場合その管理の容易さを目的に、[terraform_remote_stateデータソース](https://developer.hashicorp.com/terraform/language/state/remote-state-data)を活用し、参照パラメタを一元管理するディレクトリ構成も検討できそうです。


```sh
$ tree
.
├── common
│   ├── ssh_key
│   └── yaml
│       └── tags.yml
├── environment
│   ├── prd
│   └── dev 
├── locals.tf
├── main.tf
├── module
│   ├── network
│   │   ├── example
│   │   │   └── main.tf
│   │   ├── network.tf
│   │   ├── providers.tf
│   │   └── variables.tf
│   ├── ssh
│   │   ├── example
│   │   │   └── main.tf
│   │   ├── providers.tf
│   │   ├── ssh.tf
│   │   └── variables.tf
│   └── vm
│       ├── example
│       │   └── main.tf
│       ├── cloud_init_script
│       │   └── cloud_init.yml
│       ├── providers.tf
│       ├── variables.tf
│       └── vm.tf
├── providers.tf
└── variables.tf
```

### VM起動時にcloud initを実行
> cloud-Init は、Linux VM を初回起動時にカスタマイズするために広く使用されている手法です。

VM起動時にDockerコンテナを自動的に立上げたくcloud initを実装しました。

``` tf
data "template_file" "cloud_init_script" {
  template = file("${path.module}/cloud_init_script/cloud_init.yml")
}

# Create virtual machine
resource "azurerm_linux_virtual_machine" "vm" {
  name                  = "${var.prefix}VM"
  location              = var.location
  resource_group_name   = var.resource_group_name
  ...
  custom_data = base64encode("${data.template_file.cloud_init_script.rendered}")

}
```
ソースコードの通り、dataソースで`cloud_init.yml`を取得、レンダリングしたコンテンツをbase64にエンコーディングして引き渡すことでVM起動時に処理が実行されます。


### タグの管理
タグはymlで別ファイルとして管理し、ルートのmain.tfファイルから各モジュールに変数として引き渡す設計としました。
Tagに限らず、Terraformのリソースの一部分をymlで管理し構成する設計はとても便利です！

``` locals.tf
# rootのlocals.tfでymlを取得
locals {
  # 設定ファイルをロード
  tags_yml = yamldecode(
    file("${path.root}/common/yaml/tags.yml")
  )
  vm_tags      = flatten(local.tags_yml["vm"])
  network_tags = flatten(local.tags_yml["network"])
  ssh_tags     = flatten(local.tags_yml["ssh"])
}
```
``` main.tf 
module "ssh" {
  source              = "./module/ssh/"
  ...
  # localsで展開したタグ情報を各モジュールに引き渡す
  ssh_tags            = local.ssh_tags 
}
```
自身の記事で僭越ではありますが、ymlによる管理の有無でTerraformの記述がどれだけ簡潔になるか、がより分かりやすい記事になっていると思います！

https://qiita.com/s_rokuemon/items/c013143fca839151f640

### stringタイプからobjectタイプの変数を作る
これはちょっとしたTipsなのですが、次のような実装で可能です。完全に備忘録になりますが、残しておきます。

```variables.tf
# stringタイプの変数2つからobjectタイプの変数1つを作ります
variable "subscription_id" {
  description = "Azure Subscription ID"
  type        = string
}

variable "tenant_id" {
  description = "Azure Tenant ID"
  type        = string
}

variable "provider_credentials" {
  type = object({
    subscription_id = string
    tenant_id       = string
  })
  description = "Azure provider credentials containing subscription_id and tenant_id"
  default     = null
}
```
```locals.tf
locals {
  provider_credentials = {
    subscription_id = var.provider_credentials != null ? var.provider_credentials.subscription_id : var.subscription_id
    tenant_id       = var.provider_credentials != null ? var.provider_credentials.tenant_id : var.tenant_id
  }
}
```

## 最後に
公式チュートリアルの通りやってもうまくいかない、はもはやアルアルですね。
この記事が苦しむ誰かの助けになれば嬉しいです。

Terraformはまだまだ勉強中です。
設計から自分で考えてみることでより勉強になるなぁと実感します。
そしてIaCは面白いですね。
これからもブログを通して、地道な試行錯誤の道程をアウトプットしていきたいと思います。

それでは良いお年を。