Azure Functions を Terraform を使ってプロビジョニングすると便利という話

プロジェクトで、Azure Functions を使っているが、このAzure Functions をプロビジョニングをするといろいろ楽というのが見えてきたので共有したい。

# Azure Functions のコンフィグレーション

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

<img width="634" alt="Function03.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/4fb4bec2-aeb9-8483-2a76-face5625f593.png">


# 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](https://github.com/Azure/azure-quickstart-templates/blob/master/101-function-app-create-dynamic/azuredeploy.json) から、取得して改造するといい。

```json
{
    "$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}"
    } 
```

```json
                   "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 のテスト環境の構築も終了する。こらべんりｗ

# おわりに

結構いい感じにコンフィグできるけど、唯一の面倒くさい部分は、ARM / Terraform の両方書かなあかん事なので、ここは、Terraform に貢献しようと思う。

* [Automate resource deployment for your function app in Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/functions-infrastructure-as-code)
* [Function app on Consumption plan](https://github.com/Azure/azure-quickstart-templates/blob/master/101-function-app-create-dynamic/azuredeploy.json)