[Azure x IoT] Azure IoTHubで受け取ったメッセージをDBに保存しよう[Terraform編]

# はじめに

[[Azure x IoT] Azure IoTHubで受け取ったメッセージをDBに保存しよう](https://qiita.com/iron-samurai/items/efb422e75e43c3414e51)では、UI操作を紹介しながらアプリケーションを作成しましたが、「そんなちまちまやらずにインフラ構築はterraformに任せてとりあえず試してみたい！」という方向けの記事です

# 本記事のゴール

本記事ではIoTHubで受信したメッセージをRDBに保存できるようにします.

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/e9df56d6-0774-1b48-fd39-4aa53d8d2474.png)


AzureFunctionからAzureDatabaseへのアクセスについてはVnet統合という機能を使います。

本記事の内容は筆者の次の4つの記事を組み合わせた内容になります。IotHubは使わないけど、部分的に参考にしたいという方は以下を参考にしてください。

- [[Azure x IoT]Azure IoTHubにメッセージを送ろう](https://qiita.com/iron-samurai/items/3015971f15a20ccf954c)
- [[Azure x IoT]Azure IoTHub とAzure Functionsを連携しよう](https://qiita.com/iron-samurai/items/d5015f4bb18609f71aaa)
- [[Azure] Azure FunctionsからVnet内のRDBを操作する](https://qiita.com/iron-samurai/items/b86777889df16829775d)

# コード

下記レポジトリを参照してください。

[https://github.com/ironkicka/tff_az_templates/tree/main/iothub_to_db](https://github.com/ironkicka/tff_az_templates/tree/main/iothub_to_db)

# 構築方法

以下はレポジトリのREADMEの転載です。
1.デプロイ

```bash
terraform plan
terraform apply
```

2.関数の中身のデプロイ

```bash
cd IoTHubTriggeredFunction && npm i && npm run deploy
```

3.IoTHub上にデバイスを作成

```bash
export IOTHUB_NAME=xxxx
export DEVICE_NAME=xxxx
az iot hub device-identity create -n $IOTHUB_NAME -d $DEVICE_NAME
```

4.SASトークンを発行

```bash
export IOTHUB_NAME=xxxx
export EXPIRATION_SECONDS=xxxx
az iot hub generate-sas-token -n $IOTHUB_NAME --du $EXPIRATION_SECONDS
```

5.秘密鍵を取得

```bash
terraform output -raw tls_private_key > secret_key.pem
```

6.VMに接続(PUBLIC_IPはterraformのコンソール出力 or Azure Portalで確認できます)

```bash
ssh -i path/to/secret_key.pem azureuser@VM_PUBLIC_IP
```

7.DBに接続

```bash
mysql -h terraform-db.mysql.database.azure.com -u test_db_admin -p --ssl-mode=REQUIRED --ssl-ca=DigiCertGlobalRootCA.crt.pem
```

8.テーブルを作成

```bash
CREATE TABLE
    test_table (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        message VARCHAR(255),
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    );
```

9.IoTHubにメッセージを送信

```bash
export SAS=xxxx
export IOTHUB_NAME=xxxx
export DEVICE_NAME=xxxx
sh publish.sh
```

参考 publish.sh

```bash
curl -i -X POST \
-H "Content-Type:application/json" \
-H "Authorization:$SAS" \
-d '{"value":"hello,world from iothub"}' \
"https://$IOTHUB_NAME.azure-devices.net/devices/$DEVICE_NAME/messages/events?api-version=2018-06-30"
```

10.確認

メッセージを送信後、テーブルにレコードが追加されていれば成功です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/840bcebe-71be-3d63-dc8a-997ddaab03a1.png)


# 解説

今回のアプリケーションを構築する上で中心的役割を果たすリソースとそのポイントのみ解説していきます。

## **IotHub**

```bash
resource "azurerm_iothub" "my-iot-hub" {
  name = "terra-iot-hub"
  resource_group_name = azurerm_resource_group.rg.name
  location = var.resource_group_location
  sku {
    capacity = 1
    name     = "F1"
  }
}
```

単にメッセージを受け取るだけであれば上記の定義だけで大丈夫です！skuはF1(無料)を選べるのは、自分のサブスクリプションに他にF1のiothubがいない時だけなので注意してください。

またIoTHubを使ったことのある方はお気づきかもしれませんが、terraformではIoTHub内のデバイスを定義する項目がありませんでした。

そのため、”構築”セクションでazコマンドにより作成しています。

[Terraform Registry](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/iothub)

## Azure Key Vault周り

```bash
data "azurerm_client_config" "current" {}

resource "azurerm_key_vault" "myKeyVault" {
  name                = "terra-vault-2022"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  tenant_id           = data.azurerm_client_config.current.tenant_id
  sku_name            = "standard"
  access_policy {
    tenant_id = data.azurerm_client_config.current.tenant_id
    object_id = data.azurerm_client_config.current.object_id

    secret_permissions = [
      "List",
      "Set",
      "Get",
      "Delete",
      "Purge"
    ]
  }
}

resource "azurerm_key_vault_secret" "example" {
  name         = "DB-PASSWORD"
  value        = var.db_password
  key_vault_id = azurerm_key_vault.myKeyVault.id
}

resource "azurerm_key_vault_access_policy" "example" {
  key_vault_id = azurerm_key_vault.myKeyVault.id
  tenant_id    = data.azurerm_client_config.current.tenant_id
  object_id    = azurerm_function_app.myFunction.identity[0].principal_id

  secret_permissions = [
    "Get",
  ]
}
```

データベースのパスワードを格納するのに用います

## Function App周り

以下の5つを定義します

- AzureStorageAccountFunctionのトリガーの管理やログを保存するのに必要参考:
    
    [Storage considerations for Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/storage-considerations#storage-account-requirements)
    
- AzureServicePlan課金プラン
- AzureFunctionAppFunction本体
- ApplicationInsightsアプリケーションログを見るためのリソース
- Vnet統合設定用のリソース

上３つのterraform

```bash
# randomな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
}

# AzureStorageAccount
resource "azurerm_storage_account" "mystorageaccount" {
  name                        = "st${random_id.randomId.hex}"
  resource_group_name         = azurerm_resource_group.rg.name
  location                     = var.resource_group_location
  account_tier                = "Standard"
  account_replication_type    = "LRS"

  tags = {
    environment = "Terraform Demo"
  }
}

resource "azurerm_service_plan" "myAppservicePlan" {
  name                = "my-app-service-plan-${random_id.randomId.hex}"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  os_type             = "Linux"
  sku_name            = "S1"
}

resource "azurerm_application_insights" "myAppInsight" {
  name                = "tf-test-appinsights"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  application_type    = "Node.JS"
}
```

Azure FunctionとVnet統合設定

```bash
resource "azurerm_function_app" "myFunction" {
  name                       = "my-iothub-triggered-function"
  location                   = azurerm_resource_group.rg.location
  resource_group_name        = azurerm_resource_group.rg.name
  app_service_plan_id        = azurerm_service_plan.myAppservicePlan.id
  storage_account_name       = azurerm_storage_account.mystorageaccount.name
  storage_account_access_key = azurerm_storage_account.mystorageaccount.primary_access_key
  os_type                    = "linux"
  version                    = "~4"

  app_settings = {
    APPLICATIONINSIGHTS_CONNECTION_STRING = azurerm_application_insights.myAppInsight.connection_string
    APPINSIGHTS_INSTRUMENTATIONKEY = azurerm_application_insights.myAppInsight.instrumentation_key
    myEventHubReadConnectionAppSetting = "Endpoint=${azurerm_iothub.my-iot-hub.event_hub_events_endpoint};SharedAccessKeyName=${azurerm_iothub.my-iot-hub.shared_access_policy[0].key_name};SharedAccessKey=${azurerm_iothub.my-iot-hub.shared_access_policy[0].primary_key};EntityPath=${azurerm_iothub.my-iot-hub.event_hub_events_path}"
    FUNCTIONS_WORKER_RUNTIME ="node" #Without this you won't be able to publish function
    WEBSITE_RUN_FROM_PACKAGE =1
    DB_HOST = "${azurerm_mysql_flexible_server.database_server.name}.mysql.database.azure.com"
    DB_USER_NAME = azurerm_mysql_flexible_server.database_server.administrator_login
    DB_DATABASE_NAME = var.db_name
    DB_PASSWORD = "@Microsoft.KeyVault(SecretUri=https://${azurerm_key_vault.myKeyVault.name}.vault.azure.net/secrets/DB-PASSWORD/${azurerm_key_vault_secret.example.version})"
  }

  site_config {
    linux_fx_version = "node|14"
  }

  identity {
    type = "SystemAssigned"
  }
}

resource "azurerm_app_service_virtual_network_swift_connection" "function_vnet_integration" {
  app_service_id = azurerm_function_app.myFunction.id
  subnet_id      = azurerm_subnet.subnet-for-vent-integration.id
}
```

実は`azurerm_function_app`は型落ちしたリソースで、最新のリソースは`azurerm_linux_function_app`です.

なぜわざわざこのようなことをしているかというと、`azurerm_linux_function_app`でリソースを作成した際に,azure functionのコードのデプロイがうまくいかないという問題が発生したためです。自分の何らかの設定ミスなのかバグなのか定かではないですが、現状うまくいくことが確認できた`azurerm_function_app`で構成しています.もし原因ご存知の方がいたら共有いただきたいです!

- `azurerm_function_app`

[https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/function_app#attributes-reference](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/function_app#attributes-reference)

- `azurerm_linux_function_app`

[https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_function_app](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_function_app)

ポイント

- app_settingsブロックで`eventHubCompatibleEndpoint`をきちんと定義すること
    
    この値はIoTHubとの連携を担う値で、Functionコードから参照されます。Functionコードのfunction.jsonと同じ名前になるようにしてください。
    
- app_settingsブロックでDB_PASSWORDにKeyVaultの値を参照させるため、`システム割り当てマネージド ID`を有効にする
以下の部分で有効にしています。
    
    ```bash
    identity {
        type = "SystemAssigned"
      }
    ```
    
    DB_PASSWORDの値には`@Microsoft.KeyVault(SecretUri=xxx)`を定義することでAzureKeyVaultに保存された値を読み出すことができます。
    以前[[Azure] Azure FunctionsからAzure Key Vaultの値を参照する](https://qiita.com/iron-samurai/items/0ca65cf7262111bc62a0)で紹介した内容です。
    
- `azurerm_app_service_virtual_network_swift_connection`でVnet統合を有効化
このリソースでvnet統合用のsubnetとfunctionの紐付けを行います
この紐付けができていないとFunctionからDBへの書き込みができないので重要です！

## DB周り

subnetやらvnet周りは少し省略しています。

```bash
# データベースサーバー
resource "azurerm_mysql_flexible_server" "database_server" {
  name                = "terraform-db"
  resource_group_name = azurerm_resource_group.rg.name
  location = var.resource_group_location
  administrator_login    = var.db_user_name
  administrator_password = var.db_password
  backup_retention_days  = 7
  delegated_subnet_id = azurerm_subnet.db_subnet.id
  private_dns_zone_id = azurerm_private_dns_zone.dns_zone.id
  zone = 1
  version    = "8.0.21"
  sku_name = "B_Standard_B1s"
  depends_on = [azurerm_private_dns_zone_virtual_network_link.pdz_vent_link]
}

# mysqlデータベース
resource "azurerm_mysql_flexible_database" "database" {
  name                = var.db_name
  resource_group_name = azurerm_resource_group.rg.name
  server_name         = azurerm_mysql_flexible_server.database_server.name
  charset             = "utf8"
  collation           = "utf8_unicode_ci"
}
```

上記２つでデータベースサーバーとデータベースを定義できます。特段説明することもないです

## 踏み台サーバー

storageAccount,nicなどは省略してます。

```bash
resource "azurerm_linux_virtual_machine" "myterraformvm" {
  name                  = "myBastion"
  resource_group_name         = azurerm_resource_group.rg.name
  location                     = var.resource_group_location
  network_interface_ids = [azurerm_network_interface.bastion_nic.id]
  size                  = "Standard_B1s"

  os_disk {
    name              = "myOsDisk"
    caching           = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

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

  computer_name  = "mybastion"
  admin_username = "azureuser"
  disable_password_authentication = true

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

  boot_diagnostics {
    storage_account_uri = azurerm_storage_account.mystorageaccount.primary_blob_endpoint
  }
}

resource "tls_private_key" "bastion_ssh" {
  algorithm = "RSA"
  rsa_bits = 4096
}

resource "azurerm_virtual_machine_extension" "myextension" {
  name                 = "mybastionExtension"
  virtual_machine_id   = azurerm_linux_virtual_machine.myterraformvm.id
  publisher            = "Microsoft.Azure.Extensions"
  type                 = "CustomScript"
  type_handler_version = "2.0"

  settings = <<SETTINGS
    {
        "script": "${base64encode(file(var.script_file))}"
    }
  SETTINGS
}
```

ポイント

- `tls_private_key`でssh鍵ペアを作成
上記リソースでssh鍵ペアを作成できます。
リソースのデプロイ後は以下のようにすればファイルに書き出せます
    
    ```bash
    terraform output -raw tls_private_key > secret_key.pem
    ```
    
- `azurerm_virtual_machine_extension`でVMのデプロイ完了後にVM内で実行する処理を定義
上記リソースを使うことで、デプロイ後にVM内でshellscriptを実行するようにできます。
今回は、mysqlクライアントのインストールや
コマンドを実行することもできます。

# おわりに

いかがでしたでしょうか。UI操作より圧倒的なスピードで動作確認できたのではないでしょうか。次はAzure専用のbicepも試してみたいと思います。