プロジェクトで、Azure Functions を使っているが、このAzure Functions をプロビジョニングをするといろいろ楽というのが見えてきたので共有したい。
Azure Functions のコンフィグレーション
Azure Functions をプロビジョニングすること自体はとても簡単だ。Azure Functions を自動でプロビジョニング、コンフィグレーションしたいときに、何が面倒か?というと、Application settings
だったり、Connection Strings
を設定することだ。Azure Functions は性質上、様々なサービスと連携することになると思う。その時に、様々なサービスも同時にデプロイする必要があるのだ。ということは、そのサービスから、いちいちコネクションストリングをとってきて、設定して、、、となると超めんどくさい。だから、リソースマネージャごと、ごっそりプロビジョニングして、コンフィグしてしまえばいい。ARM テンプレートを使えばできるといえばできるのだが、延々と JSON を書き続けるのは辛いものだ。

Terraform と、ARM を組み合わせる
そこで、Terraform と、ARM を組み合わせるといい感じになる。Terraform でサポートされていないリソースのみ、ARM を呼び出すようにする。残念ながら、Azure Functions 自体は、Terraform でサポートされていない。だから、いま書くとこんな感じ。
# Service Principle
variable "subscription_id" {
}
variable "client_id" {
}
variable "client_secret" {
}
variable "tenant_id" {
}
# Resource Group
variable "resource_group" {
}
variable "location" {
}
variable "environment" {
default = "test"
}
# functions
variable "app_name" {
}
variable "processing_queue_name" {
}
# KeyVault
variable "key_vault_name" {
}
# Azure Data Factory
variable "include_factories" {
}
# EventHub
variable "event_hub_entity_path" {
}
variable "event_hub_connection_string" {
}
# Redis
variable "redis_connection_string" {
}
# Storage for Queue
variable "queue_storage_connection_string" {
}
provider "azurerm" {
# Service Principle
subscription_id = "${var.subscription_id}"
client_id = "${var.client_id}"
client_secret = "${var.client_secret}"
tenant_id = "${var.tenant_id}"
}
# Create a resource group
resource "azurerm_resource_group" "test" {
name = "${var.resource_group}"
location = "${var.location}"
tags {
environment = "${var.environment}"
}
}
resource "azurerm_template_deployment" "test" {
name = "functions-deployment-01"
resource_group_name = "${azurerm_resource_group.test.name}"
template_body = "${file("${path.cwd}/functions/template.json")}"
parameters {
appName = "${var.app_name}"
keyVaultApplicationId = "${var.client_id}"
keyVaultApplicationSecret = "${var.client_secret}"
keyVaultName = "${var.key_vault_name}"
includeFactories = "${var.include_factories}"
resourceGroup = "${azurerm_resource_group.test.name}"
processingQueueName = "${var.processing_queue_name}"
eventHubEntityPath = "${var.event_hub_entity_path}"
eventHubConnectionString = "${var.event_hub_connection_string}"
redisConnectionString = "${var.redis_connection_string}"
queueStorageConnectionString = "${var.queue_storage_connection_string}"
}
deployment_mode = "Incremental"
}
あとは、template.json (ARM) を、Function app on Consumption plan から、取得して改造するといい。
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"appName": {
"type": "string",
"metadata": {
"description": "The name of the function app that you wish to create."
}
},
"storageAccountType": {
"type": "string",
"defaultValue": "Standard_LRS",
"allowedValues": [
"Standard_LRS",
"Standard_GRS",
"Standard_ZRS",
"Premium_LRS"
],
"metadata": {
"description": "Storage Account type"
}
},
"keyVaultApplicationId": {
"type": "string",
"metadata":{
"description": "Service Principal ClientID for operating KeyVault"
}
},
"keyVaultApplicationSecret": {
"type": "string",
"metadata":{
"description": "Service Principal Password for operating KeyVault"
}
},
"keyVaultName": {
"type": "string",
"metadata":{
"description": "KeyVault Name"
}
},
"includeAllFactories": {
"type": "string",
"defaultValue": "false",
"metadata":{
"description": "true/false"
}
},
"excludeFactories": {
"type": "string",
"defaultValue": "",
"metadata":{
"description": "Exclude Azure Data Factory Name"
}
},
"includeFactories": {
"type": "string",
"metadata":{
"description": "Include Azure Data Factory Name"
}
},
"resourceGroup": {
"type": "string",
"metadata":{
"description": "Resource Group Name"
}
},
"processingQueueName": {
"type": "string",
"metadata":{
"description": "queueName for Queue Trigger"
}
},
"eventHubEntityPath": {
"type": "string",
"metadata":{
"description": "Event Hub Name"
}
},
"eventHubConnectionString": {
"type": "string",
"metadata":{
"description": "Event Hubs Connection String"
}
},
"redisConnectionString": {
"type": "string",
"metadata":{
"description": "Redis Connection String"
}
},
"queueStorageConnectionString": {
"type": "string",
"metadata":{
"description": "Queue Storage Connection Storage"
}
}
},
"variables": {
"functionAppName": "[parameters('appName')]",
"hostingPlanName": "[parameters('appName')]",
"storageAccountName": "[concat(uniquestring(resourceGroup().id), 'azfunctions')]",
"storageAccountid": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]"
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"name": "[variables('storageAccountName')]",
"apiVersion": "2016-12-01",
"location": "[resourceGroup().location]",
"kind": "Storage",
"sku": {
"name": "[parameters('storageAccountType')]"
}
},
{
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2015-04-01",
"name": "[variables('hostingPlanName')]",
"location": "[resourceGroup().location]",
"properties": {
"name": "[variables('hostingPlanName')]",
"computeMode": "Dynamic",
"sku": "Dynamic"
}
},
{
"apiVersion": "2015-08-01",
"type": "Microsoft.Web/sites",
"name": "[variables('functionAppName')]",
"location": "[resourceGroup().location]",
"kind": "functionapp",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
],
"properties": {
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
"siteConfig": {
"appSettings": [
{
"name": "AzureWebJobsDashboard",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
},
{
"name": "AzureWebJobsStorage",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
},
{
"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
},
{
"name": "WEBSITE_CONTENTSHARE",
"value": "[toLower(variables('functionAppName'))]"
},
{
"name": "FUNCTIONS_EXTENSION_VERSION",
"value": "~1"
},
{
"name": "WEBSITE_NODE_DEFAULT_VERSION",
"value": "6.5.0"
},
{
"name": "KeyVaultApplicationId",
"value": "[parameters('keyVaultApplicationId')]"
},
{
"name": "KeyVaultApplicationSecret",
"value": "[parameters('keyVaultApplicationSecret')]"
},
{
"name": "KeyVaultName",
"value": "[parameters('keyVaultName')]"
},
{
"name": "IncludeAllFactories",
"value": "[parameters('includeAllFactories')]"
},
{
"name": "ExcludeFactories",
"value": "[parameters('excludeFactories')]"
},
{
"name": "IncludeFactories",
"value": "[parameters('includeFactories')]"
},
{
"name": "ResourceGroup",
"value": "[parameters('resourceGroup')]"
},
{
"name": "ProcessingQueueName",
"value": "[parameters('processingQueueName')]"
},
{
"name": "EventHubEntityPath",
"value": "[parameters('eventHubEntityPath')]"
},
{
"name": "AzureWebJobsSecretStorageType",
"value": "disabled"
}
],
"connectionStrings": [
{
"name": "EventHub",
"connectionString": "[parameters('eventHubConnectionString')]",
"type": "Custom"
},
{
"name": "Redis",
"connectionString": "[parameters('redisConnectionString')]",
"type": "Custom"
},
{
"name": "QueueStorage",
"connectionString": "[parameters('queueStorageConnectionString')]",
"type": "Custom"
}
]
}
}
}
]
}
ポイントは、それぞれの下記の部分で、App Settings や、 Connection Strings を ARM / Terraform から設定してしまえること。最初は面倒だが、作ってしまえば便利。
parameters {
appName = "${var.app_name}"
keyVaultApplicationId = "${var.client_id}"
keyVaultApplicationSecret = "${var.client_secret}"
keyVaultName = "${var.key_vault_name}"
includeFactories = "${var.include_factories}"
resourceGroup = "${azurerm_resource_group.test.name}"
processingQueueName = "${var.processing_queue_name}"
eventHubEntityPath = "${var.event_hub_entity_path}"
eventHubConnectionString = "${var.event_hub_connection_string}"
redisConnectionString = "${var.redis_connection_string}"
queueStorageConnectionString = "${var.queue_storage_connection_string}"
}
"appSettings": [
{
"name": "AzureWebJobsDashboard",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
},
{
"name": "AzureWebJobsStorage",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
},
{
"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
},
{
"name": "WEBSITE_CONTENTSHARE",
"value": "[toLower(variables('functionAppName'))]"
},
{
"name": "FUNCTIONS_EXTENSION_VERSION",
"value": "~1"
},
{
"name": "WEBSITE_NODE_DEFAULT_VERSION",
"value": "6.5.0"
},
{
"name": "KeyVaultApplicationId",
"value": "[parameters('keyVaultApplicationId')]"
},
{
"name": "KeyVaultApplicationSecret",
"value": "[parameters('keyVaultApplicationSecret')]"
},
{
"name": "KeyVaultName",
"value": "[parameters('keyVaultName')]"
},
{
"name": "IncludeAllFactories",
"value": "[parameters('includeAllFactories')]"
},
{
"name": "ExcludeFactories",
"value": "[parameters('excludeFactories')]"
},
{
"name": "IncludeFactories",
"value": "[parameters('includeFactories')]"
},
{
"name": "ResourceGroup",
"value": "[parameters('resourceGroup')]"
},
{
"name": "ProcessingQueueName",
"value": "[parameters('processingQueueName')]"
},
{
"name": "EventHubEntityPath",
"value": "[parameters('eventHubEntityPath')]"
},
{
"name": "AzureWebJobsSecretStorageType",
"value": "disabled"
}
],
"connectionStrings": [
{
"name": "EventHub",
"connectionString": "[parameters('eventHubConnectionString')]",
"type": "Custom"
},
{
"name": "Redis",
"connectionString": "[parameters('redisConnectionString')]",
"type": "Custom"
},
{
"name": "QueueStorage",
"connectionString": "[parameters('queueStorageConnectionString')]",
"type": "Custom"
}
]
Terraform の Attribute reference と組み合わせる
ARM もできるが、Terraform も大抵のリソースで、Attribute reference
が取得できる。何かというと、リソースを作ったあとに取得できるキーとか、コネクションストリングを参照できるようになっている。つまり、手でコピーしていたような、その辺の値をとってきて張り付けるとか面倒なことが不要になる。それをつかって、上記の部分を書き換える。
resource "azurerm_template_deployment" "test" {
name = "functions-deployment-01"
resource_group_name = "${azurerm_resource_group.test.name}"
template_body = "${file("${path.cwd}/functions/template.json")}"
parameters {
appName = "${var.app_name}"
keyVaultApplicationId = "${var.client_id}"
keyVaultApplicationSecret = "${var.client_secret}"
keyVaultName = "${var.key_vault_name}"
includeFactories = "${var.data_factory_name}"
resourceGroup = "${azurerm_resource_group.test.name}"
processingQueueName = "${var.processing_queue_name}"
eventHubEntityPath = "${var.event_hub_name}"
eventHubConnectionString = "${azurerm_eventhub_namespace.test.default_primary_connection_string}"
redisConnectionString = "${azurerm_redis_cache.test.name}.redis.cache.windows.net:6380,password=.${azurerm_redis_cache.test.primary_access_key},ssl=True,abortConnect=False"
queueStorageConnectionString = "${azurerm_storage_account.test.primary_blob_connection_string}"
}
deployment_mode = "Incremental"
}
こんな感じで、設定してしまえば、自分でわざわざコネクションストリングとか、キーとか設定しなくても、まるっと設定できる。さらに出来上がった、Azure Functions から、Azure Functions CLI を使って、
func azure functionapp fetch-app-settings AZURE_FUNCTIONS_NAME
を実施すると、local.settings.json を取得できるので、ローカルの Azure Functions のテスト環境の構築も終了する。こらべんりw
おわりに
結構いい感じにコンフィグできるけど、唯一の面倒くさい部分は、ARM / Terraform の両方書かなあかん事なので、ここは、Terraform に貢献しようと思う。