はじめに
[Azure x IoT] Azure IoTHubで受け取ったメッセージをDBに保存しようでは、UI操作を紹介しながらアプリケーションを作成しましたが、「そんなちまちまやらずにインフラ構築はterraformに任せてとりあえず試してみたい!」という方向けの記事です
本記事のゴール
本記事ではIoTHubで受信したメッセージをRDBに保存できるようにします.
AzureFunctionからAzureDatabaseへのアクセスについてはVnet統合という機能を使います。
本記事の内容は筆者の次の4つの記事を組み合わせた内容になります。IotHubは使わないけど、部分的に参考にしたいという方は以下を参考にしてください。
- [Azure x IoT]Azure IoTHubにメッセージを送ろう
- [Azure x IoT]Azure IoTHub とAzure Functionsを連携しよう
- [Azure] Azure FunctionsからVnet内のRDBを操作する
コード
下記レポジトリを参照してください。
https://github.com/ironkicka/tff_az_templates/tree/main/iothub_to_db
構築方法
以下はレポジトリのREADMEの転載です。
1.デプロイ
terraform plan
terraform apply
2.関数の中身のデプロイ
cd IoTHubTriggeredFunction && npm i && npm run deploy
3.IoTHub上にデバイスを作成
export IOTHUB_NAME=xxxx
export DEVICE_NAME=xxxx
az iot hub device-identity create -n $IOTHUB_NAME -d $DEVICE_NAME
4.SASトークンを発行
export IOTHUB_NAME=xxxx
export EXPIRATION_SECONDS=xxxx
az iot hub generate-sas-token -n $IOTHUB_NAME --du $EXPIRATION_SECONDS
5.秘密鍵を取得
terraform output -raw tls_private_key > secret_key.pem
6.VMに接続(PUBLIC_IPはterraformのコンソール出力 or Azure Portalで確認できます)
ssh -i path/to/secret_key.pem azureuser@VM_PUBLIC_IP
7.DBに接続
mysql -h terraform-db.mysql.database.azure.com -u test_db_admin -p --ssl-mode=REQUIRED --ssl-ca=DigiCertGlobalRootCA.crt.pem
8.テーブルを作成
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にメッセージを送信
export SAS=xxxx
export IOTHUB_NAME=xxxx
export DEVICE_NAME=xxxx
sh publish.sh
参考 publish.sh
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.確認
メッセージを送信後、テーブルにレコードが追加されていれば成功です!
解説
今回のアプリケーションを構築する上で中心的役割を果たすリソースとそのポイントのみ解説していきます。
IotHub
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コマンドにより作成しています。
Azure Key Vault周り
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のトリガーの管理やログを保存するのに必要参考:
-
AzureServicePlan課金プラン
-
AzureFunctionAppFunction本体
-
ApplicationInsightsアプリケーションログを見るためのリソース
-
Vnet統合設定用のリソース
上3つのterraform
# 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統合設定
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
azurerm_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
を有効にする
以下の部分で有効にしています。identity { type = "SystemAssigned" }
DB_PASSWORDの値には
@Microsoft.KeyVault(SecretUri=xxx)
を定義することでAzureKeyVaultに保存された値を読み出すことができます。
以前[Azure] Azure FunctionsからAzure Key Vaultの値を参照するで紹介した内容です。 -
azurerm_app_service_virtual_network_swift_connection
でVnet統合を有効化
このリソースでvnet統合用のsubnetとfunctionの紐付けを行います
この紐付けができていないとFunctionからDBへの書き込みができないので重要です!
DB周り
subnetやらvnet周りは少し省略しています。
# データベースサーバー
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"
}
上記2つでデータベースサーバーとデータベースを定義できます。特段説明することもないです
踏み台サーバー
storageAccount,nicなどは省略してます。
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鍵ペアを作成できます。
リソースのデプロイ後は以下のようにすればファイルに書き出せますterraform output -raw tls_private_key > secret_key.pem
-
azurerm_virtual_machine_extension
でVMのデプロイ完了後にVM内で実行する処理を定義
上記リソースを使うことで、デプロイ後にVM内でshellscriptを実行するようにできます。
今回は、mysqlクライアントのインストールや
コマンドを実行することもできます。
おわりに
いかがでしたでしょうか。UI操作より圧倒的なスピードで動作確認できたのではないでしょうか。次はAzure専用のbicepも試してみたいと思います。