Azure Resource Manager - テンプレート間のパラメータ受け渡し

# 過去記事

- [Azure Resource Manager - 基本＋ Functions のデプロイ](https://qiita.com/takmot/items/35639db96b1a539b9e1b)
- [Azure Resource Manager - テンプレートの階層化（テンプレートのリンク）](https://qiita.com/takmot/items/895d4afe19ef20d0f1dd)
- [Azure API Management - エラーレスポンスのカスタマイズ](https://qiita.com/takmot/items/880a2a1129b14d24c42a)

# 目的

- `Functions` と `Api Management` のデプロイを行う
- カスタマイズ性を考慮し、以下定義をテンプレートから切り出す
  - Functions の環境変数定義（環境/ユーザで変更する可能性のある値のみ）
  - Api Management の API 定義（Open API）
  - Api Management のポリシー定義
  - その他パラメータ（リソースプレフィックス、Api Management に設定するメールアドレス、組織名等）定義

# ファイル構成

以下のファイル構成とする

```
  ├─ ApiManagement/
  │   ├─ openapi.yml               # Api Management の API 定義（Open API）
  │   ├─ policies.xml              # Api Management のポリシー定義
  │   └─ template.json
  ├─ ApplicationInsights/
  │   └─ template.json
  ├─ Functions/
  │   ├─ environmentVariable.json  # Functions の環境変数定義
  │   └─ template.json
  ├─ Storage/
  │   └─ template.json
  │
  ├─ template.json                 # Parentテンプレート
  ├─ parameters.json               # パラメータ定義
  │
  ├─ package.json
  ├─ package-lock.json
  └─ deploy.js                     # deploy用スクリプト
```

# ソースコード

## トップ階層

<details><summary>parameters.json</summary>

```json: parameters.json
{
  "deployVersion": "1.0",
  "resourceGroup": "<resource group name>",
  "prefix": "<prefix>",
  "administratorEmail": "xxx@yyy.co.jp",
  "organizationName": "zzz"
}
```

</details>

<details><summary>deploy.js</summary>

```javascript: deploy.js
const fs = require('fs');
const { execSync } = require('child_process');

async function deploy() {
  try {
    const parameters = JSON.parse(fs.readFileSync('./parameters.json', 'utf8'));
    const environmentVariable = JSON.parse(
      fs.readFileSync('./Functions/environmentVariable.json', 'utf8')
    );
    const openApi = fs.readFileSync('./ApiManagement/openapi.yml', 'utf8');
    const policies = fs.readFileSync('./ApiManagement/policies.xml', 'utf8');
    const {
      deployVersion,
      resourceGroup,
      prefix,
      administratorEmail,
      organizationName,
    } = parameters;
    const deployParameters = {
      deployVersion: { value: deployVersion },
      prefix: { value: prefix },
      administratorEmail: { value: administratorEmail },
      organizationName: { value: organizationName },
      environmentVariable: { value: environmentVariable },
      openApi: { value: openApi },
      policies: { value: policies },
      storageSpecName: { value: 'storageSpec' },
      applicationInsightsSpecName: { value: 'applicationInsightsSpec' },
      functionsSpecName: { value: 'functionsSpec' },
      apiManagementSpecName: { value: 'apiManagementSpec' },
    };
    fs.writeFileSync(
      '_parameters.json',
      JSON.stringify(deployParameters, null, '    ')
    );
    // Storage テンプレートスペックのデプロイ
    execSync(
      `az ts create --name ${deployParameters.storageSpecName.value} --version ${deployVersion} --resource-group ${resourceGroup} --template-file "Storage/template.json" --yes`
    );
    // Application Insights テンプレートスペックのデプロイ
    execSync(
      `az ts create --name ${deployParameters.applicationInsightsSpecName.value} --version ${deployVersion} --resource-group ${resourceGroup} --template-file "ApplicationInsights/template.json" --yes`
    );
    // Functions テンプレートスペックのデプロイ
    execSync(
      `az ts create --name ${deployParameters.functionsSpecName.value} --version ${deployVersion} --resource-group ${resourceGroup} --template-file "Functions/template.json" --yes`
    );
    // Api Management テンプレートスペックのデプロイ
    execSync(
      `az ts create --name ${deployParameters.apiManagementSpecName.value} --version ${deployVersion} --resource-group ${resourceGroup} --template-file "ApiManagement/template.json" --yes`
    );
    // 親テンプレートのデプロイ
    execSync(
      `az deployment group create --name ${prefix}-deploy \
        --resource-group ${resourceGroup} \
        --template-file template.json \
        --parameters _parameters.json`
    );
  } catch (err) {
    console.log(err);
  }
}

deploy();
```

</details>

<details><summary>template.json</summary>

```json: template.json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "deployVersion": {
      "type": "string",
      "defaultValue": "1.0"
    },
    "prefix": {
      "type": "string",
      "defaultValue": "prefix"
    },
    "administratorEmail": {
      "type": "string",
      "defaultValue": "xxxxx@yyyyy.co.jp"
    },
    "organizationName": {
      "type": "string",
      "defaultValue": "zzzzz"
    },
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]"
    },
    "environmentVariable": {
      "type": "array",
      "defaultValue": []
    },
    "openApi": {
      "type": "string",
      "defaultValue": ""
    },
    "policies": {
      "type": "string",
      "defaultValue": ""
    },
    "storageSpecName": {
      "type": "string",
      "defaultValue": "storageSpec"
    },
    "applicationInsightsSpecName": {
      "type": "string",
      "defaultValue": "applicationInsightsSpec"
    },
    "functionsSpecName": {
      "type": "string",
      "defaultValue": "functionsSpec"
    },
    "apiManagementSpecName": {
      "type": "string",
      "defaultValue": "apiManagementSpec"
    }
  },
  "functions": [],
  "variables": {},
  "resources": [
    {
      "name": "StorageDeployment",
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "2021-04-01",
      "properties": {
        "mode": "Incremental",
        "templateLink": {
          "id": "[resourceId('Microsoft.Resources/templateSpecs/versions', parameters('storageSpecName'), parameters('deployVersion'))]"
        },
        "parameters": {
          "prefix": { "value": "[parameters('prefix')]" },
          "location": { "value": "[parameters('location')]" }
        }
      }
    },
    {
      "name": "ApplicationInsightsDeployment",
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "2021-04-01",
      "properties": {
        "mode": "Incremental",
        "templateLink": {
          "id": "[resourceId('Microsoft.Resources/templateSpecs/versions', parameters('applicationInsightsSpecName'), parameters('deployVersion'))]"
        },
        "parameters": {
          "prefix": { "value": "[parameters('prefix')]" },
          "location": { "value": "[parameters('location')]" }
        }
      }
    },
    {
      "name": "FunctionsDeployment",
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "2021-04-01",
      "dependsOn": [
        "[resourceId('Microsoft.Resources/deployments', 'StorageDeployment')]",
        "[resourceId('Microsoft.Resources/deployments', 'ApplicationInsightsDeployment')]"
      ],
      "properties": {
        "mode": "Incremental",
        "templateLink": {
          "id": "[resourceId('Microsoft.Resources/templateSpecs/versions', parameters('functionsSpecName'), parameters('deployVersion'))]"
        },
        "parameters": {
          "prefix": { "value": "[parameters('prefix')]" },
          "location": { "value": "[parameters('location')]" },
          "storageConnectionString": {
            "value": "[reference('StorageDeployment').outputs.storageConnectionString.value]"
          },
          "applicationInsightsInstrumentationKey": {
            "value": "[reference('ApplicationInsightsDeployment').outputs.applicationInsightsInstrumentationKey.value]"
          },
          "environmentVariable": {
            "value": "[parameters('environmentVariable')]"
          }
        }
      }
    },
    {
      "name": "ApiManagementDeployment",
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "2021-04-01",
      "dependsOn": [
        "[resourceId('Microsoft.Resources/deployments', 'StorageDeployment')]",
        "[resourceId('Microsoft.Resources/deployments', 'ApplicationInsightsDeployment')]",
        "[resourceId('Microsoft.Resources/deployments', 'FunctionsDeployment')]"
      ],
      "properties": {
        "mode": "Incremental",
        "templateLink": {
          "id": "[resourceId('Microsoft.Resources/templateSpecs/versions', parameters('apiManagementSpecName'), parameters('deployVersion'))]"
        },
        "parameters": {
          "prefix": { "value": "[parameters('prefix')]" },
          "administratorEmail": {
            "value": "[parameters('administratorEmail')]"
          },
          "organizationName": { "value": "[parameters('organizationName')]" },
          "location": { "value": "[parameters('location')]" },
          "openApi": { "value": "[parameters('openApi')]" },
          "policies": { "value": "[parameters('policies')]" },
          "functionsName": {
            "value": "[reference('FunctionsDeployment').outputs.functionsName.value]"
          },
          "functionsResourceId": {
            "value": "[reference('FunctionsDeployment').outputs.functionsResourceId.value]"
          },
          "functionsKey": {
            "value": "[reference('FunctionsDeployment').outputs.functionsKey.value]"
          }
        }
      }
    }
  ],
  "outputs": {}
}
```

</details>

## Storage

<details><summary>template.json</summary>

```json: template.json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "prefix": {
      "type": "string"
    },
    "location": {
      "type": "string"
    },
    "storageAccountType": {
      "type": "string",
      "defaultValue": "Standard_LRS"
    }
  },
  "functions": [],
  "variables": {
    "storageAccountName": "[concat(parameters('prefix'), 'storage')]"
  },
  "resources": [
    {
      "type": "Microsoft.Storage/storageAccounts",
      "apiVersion": "2021-04-01",
      "kind": "Storage",
      "name": "[variables('storageAccountName')]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "[parameters('storageAccountType')]"
      }
    }
  ],
  "outputs": {
    "storageAccountName": {
      "type": "string",
      "value": "[variables('storageAccountName')]"
    },
    "storageConnectionString": {
      "type": "string",
      "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value, ';EndpointSuffix=', environment().suffixes.storage)]"
    }
  }
}
```

</details>

## ApplicationInsights

<details><summary>template.json</summary>

```json: template.json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "prefix": {
      "type": "string"
    },
    "location": {
      "type": "string"
    }
  },
  "functions": [],
  "variables": {
    "applicationInsightsName": "[concat(parameters('prefix'), '-Application-insights')]"
  },
  "resources": [
    {
      "type": "microsoft.insights/components",
      "apiVersion": "2020-02-02",
      "name": "[variables('applicationInsightsName')]",
      "location": "[parameters('location')]",
      "kind": "web",
      "tags": {
        "[concat('hidden-link:', resourceId('Microsoft.Web/sites', variables('applicationInsightsName')))]": "Resource"
      },
      "properties": {
        "Application_Type": "web",
        "ApplicationId": "[variables('applicationInsightsName')]"
      }
    }
  ],
  "outputs": {
    "applicationInsightsName": {
      "type": "string",
      "value": "[variables('applicationInsightsName')]"
    },
    "applicationInsightsInstrumentationKey": {
      "type": "string",
      "value": "[reference(variables('applicationInsightsName')).InstrumentationKey]"
    }
  }
}
```

</details>

## Functions

<details><summary>environmentVariable.json</summary>

```json: environmentVariable.json
[
  {
    "name": "value-name-1",
    "value": "value-1"
  },
  {
    "name": "value-name-2",
    "value": "value-2"
  }
]

```

</details>

<details><summary>template.json</summary>

```json: template.json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "prefix": {
      "type": "string"
    },
    "location": {
      "type": "string"
    },
    "storageConnectionString": {
      "type": "string"
    },
    "applicationInsightsInstrumentationKey": {
      "type": "string"
    },
    "environmentVariable": {
      "type": "array"
    },
    "functionsName": {
      "type": "string",
      "defaultValue": "[concat(parameters('prefix'), '-functions-app')]"
    },
    "hostingPlanName": {
      "type": "string",
      "defaultValue": "[concat(parameters('functionsName'), '-hosting-plan')]"
    }
  },
  "functions": [],
  "variables": {
    "appSettings": [
      {
        "name": "APPINSIGHTS_INSTRUMENTATIONKEY",
        "value": "[parameters('applicationInsightsInstrumentationKey')]"
      },
      {
        "name": "AzureWebJobsDashboard",
        "value": "[parameters('storageConnectionString')]"
      },
      {
        "name": "AzureWebJobsStorage",
        "value": "[parameters('storageConnectionString')]"
      },
      {
        "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
        "value": "[parameters('storageConnectionString')]"
      },
      {
        "name": "FUNCTIONS_EXTENSION_VERSION",
        "value": "~4"
      },
      {
        "name": "FUNCTIONS_WORKER_RUNTIME",
        "value": "node"
      },
      {
        "name": "WEBSITE_CONTENTSHARE",
        "value": "[toLower('functionName')]"
      },
      {
        "name": "WEBSITE_NODE_DEFAULT_VERSION",
        "value": "~14"
      }
    ]
  },
  "resources": [
    {
      "type": "Microsoft.Web/serverfarms",
      "apiVersion": "2020-12-01",
      "name": "[parameters('hostingPlanName')]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "Y1",
        "tier": "Dynamic"
      },
      "properties": {
        "name": "[parameters('hostingPlanName')]"
      }
    },
    {
      "type": "Microsoft.Web/sites",
      "apiVersion": "2020-12-01",
      "kind": "functionapp",
      "name": "[parameters('functionsName')]",
      "location": "[parameters('location')]",
      "dependsOn": [
        "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]"
      ],
      "properties": {
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]",
        "siteConfig": {
          "appSettings": "[concat(variables('appSettings'), parameters('environmentVariable'))]"
        }
      }
    }
  ],
  "outputs": {
    "functionsName": {
      "type": "string",
      "value": "[parameters('functionsName')]"
    },
    "functionsResourceId": {
      "type": "string",
      "value": "[concat('https://management.azure.com', resourceId('Microsoft.Web/sites', parameters('functionsName')))]"
    },
    "functionsKey": {
      "type": "string",
      "value": "[listkeys(concat(resourceId('Microsoft.Web/sites', parameters('functionsName')), '/host/default/'),'2021-02-01').functionKeys.default]"
    }
  }
}
```

</details>

## ApiManagement

<details><summary>openapi.yml</summary>

```yaml: openapi.yml
openapi: 3.0.1
info:
  title: APIs
  description: ''
  version: '1.0'
paths:
  /HttpTrigger1:
    get:
      summary: HttpTrigger1
      operationId: HttpTrigger1
      responses:
        '200':
          description: null
components:
  securitySchemes:
    apiKeyHeader:
      type: apiKey
      name: Ocp-Apim-Subscription-Key
      in: header
    apiKeyQuery:
      type: apiKey
      name: subscription-key
      in: query
security:
  - apiKeyHeader: []
  - apiKeyQuery: []

```

</details>

<details><summary>policies.xml</summary>

```xml: policies.xml
<policies>
    <inbound>
        <base />
        <rate-limit calls="30" renewal-period="1" remaining-calls-variable-name="remainingCallsPerSubscription"/>
        <set-backend-service backend-id="backend" />
    </inbound>
    <backend>
        <forward-request timeout="30" fail-on-error-status-code="true" />
    </backend>
    <outbound>
        <set-header name="Content-Type" exists-action="override">
            <value>application/json; charset=utf-8</value>
        </set-header>
        <base />
    </outbound>
    <on-error>
        <choose>
            <when condition="@(context.LastError.Reason == "HeaderNotFound" && context.Response.StatusCode == 401)">
                <return-response>
                    <set-status code="401" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json; charset=utf-8</value>
                    </set-header>
                    <set-header name="Access-Control-Allow-Origin" exists-action="skip">
                        <value>@("*")</value>
                    </set-header>
                    <set-body>@{
                        return new JObject(
                            new JProperty("type", "/api/common/v1/errors/authorization-missing"),
                            new JProperty("title", "Authorization information is missing."),
                            new JProperty("detail", new JObject(
                                new JProperty("Source", context.LastError.Source.ToString()),
                                new JProperty("Reason", context.LastError.Reason.ToString()),
                                new JProperty("Message", context.LastError.Message.ToString())
                                ).ToString()
                            )
                        ).ToString();
                    }</set-body>
                </return-response>
            </when>
            <when condition="@(context.LastError.Source == "validate-jwt")">
                <return-response>
                    <set-status code="403" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json; charset=utf-8</value>
                    </set-header>
                    <set-header name="Access-Control-Allow-Origin" exists-action="skip">
                        <value>@("*")</value>
                    </set-header>
                    <set-body>@{
                        return new JObject(
                            new JProperty("type", "/api/common/v1/errors/authorization-failed"),
                            new JProperty("title", "Authorization information is provided but access is forbidden."),
                            new JProperty("detail", new JObject(
                                new JProperty("Source", context.LastError.Source.ToString()),
                                new JProperty("Reason", context.LastError.Reason.ToString()),
                                new JProperty("Message", context.LastError.Message.ToString())
                                ).ToString()
                            )
                        ).ToString();
                    }</set-body>
                </return-response>
            </when>
            <when condition="@(context.LastError.Reason == "OperationNotFound")">
                <return-response>
                    <set-status code="404" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json; charset=utf-8</value>
                    </set-header>
                    <set-header name="Access-Control-Allow-Origin" exists-action="skip">
                        <value>@("*")</value>
                    </set-header>
                    <set-body>@{
                        return new JObject(
                            new JProperty("type", "/api/common/v1/errors/uri-not-found"),
                            new JProperty("title", "Resource specified by URI is not found."),
                            new JProperty("detail", new JObject(
                                new JProperty("Source", context.LastError.Source.ToString()),
                                new JProperty("Reason", context.LastError.Reason.ToString()),
                                new JProperty("Message", context.LastError.Message.ToString())
                                ).ToString()
                            )
                        ).ToString();
                    }</set-body>
                </return-response>
            </when>
            <when condition="@(context.LastError.Reason == "HeaderNotFound" && context.Response.StatusCode == 400)">
                <return-response>
                    <set-status code="400" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json; charset=utf-8</value>
                    </set-header>
                    <set-header name="Access-Control-Allow-Origin" exists-action="skip">
                        <value>@("*")</value>
                    </set-header>
                    <set-body>@{
                        return new JObject(
                            new JProperty("type", "/api/common/v1/errors/bad-request"),
                            new JProperty("title", "Bad Request."),
                            new JProperty("detail", new JObject(
                                new JProperty("Source", context.LastError.Source.ToString()),
                                new JProperty("Reason", context.LastError.Reason.ToString()),
                                new JProperty("Message", context.LastError.Message.ToString())
                                ).ToString()
                            )
                        ).ToString();
                    }</set-body>
                </return-response>
            </when>
            <when condition="@(context.LastError.Reason == "HeaderValueNotAllowed")">
                <return-response>
                    <set-status code="415" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json; charset=utf-8</value>
                    </set-header>
                    <set-header name="Access-Control-Allow-Origin" exists-action="skip">
                        <value>@("*")</value>
                    </set-header>
                    <set-body>@{
                        return new JObject(
                            new JProperty("type", "/api/common/v1/errors/media-type-request-unsupported"),
                            new JProperty("title", "Media type in request is not supported."),
                            new JProperty("detail", new JObject(
                                new JProperty("Source", context.LastError.Source.ToString()),
                                new JProperty("Reason", context.LastError.Reason.ToString()),
                                new JProperty("Message", context.LastError.Message.ToString())
                                ).ToString()
                            )
                        ).ToString();
                    }</set-body>
                </return-response>
            </when>
            <when condition="@(context.LastError.Reason == "RateLimitExceeded")">
                <return-response>
                    <set-status code="429" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json; charset=utf-8</value>
                    </set-header>
                    <set-header name="Access-Control-Allow-Origin" exists-action="skip">
                        <value>@("*")</value>
                    </set-header>
                    <set-body>@{
                        return new JObject(
                            new JProperty("type", "/api/common/v1/errors/api-too-many-requests"),
                            new JProperty("title", "Request to API is too many."),
                            new JProperty("detail", new JObject(
                                new JProperty("Source", context.LastError.Source.ToString()),
                                new JProperty("Reason", context.LastError.Reason.ToString()),
                                new JProperty("Message", context.LastError.Message.ToString())
                                ).ToString()
                            )
                        ).ToString();
                    }</set-body>
                </return-response>
            </when>
            <when condition="@(context.LastError.Reason == "Timeout")">
                <return-response>
                    <set-status code="504" />
                    <set-header name="Content-Type" exists-action="override">
                        <value>application/json; charset=utf-8</value>
                    </set-header>
                    <set-header name="Access-Control-Allow-Origin" exists-action="skip">
                        <value>@("*")</value>
                    </set-header>
                    <set-body>@{
                        return new JObject(
                            new JProperty("type", "/api/common/v1/errors/gateway-time-out"),
                            new JProperty("title", "Backend server didn't respond in time."),
                            new JProperty("detail", new JObject(
                                new JProperty("Source", context.LastError.Source.ToString()),
                                new JProperty("Reason", context.LastError.Reason.ToString()),
                                new JProperty("Message", context.LastError.Message.ToString())
                                ).ToString()
                            )
                        ).ToString();
                    }</set-body>
                </return-response>
            </when>
        </choose>
        <base />
    </on-error>
</policies>
```

</details>

<details><summary>template.json</summary>

```json: template.json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "prefix": {
      "type": "string"
    },
    "administratorEmail": {
      "type": "string"
    },
    "organizationName": {
      "type": "string"
    },
    "location": {
      "type": "string"
    },
    "openApi": {
      "type": "string"
    },
    "policies": {
      "type": "string"
    },
    "functionsName": {
      "type": "String"
    },
    "functionsResourceId": {
      "type": "String"
    },
    "functionsKey": {
      "type": "String"
    },
    "apiManagementName": {
      "type": "String",
      "defaultValue": "[concat(parameters('prefix'), '-api-managment')]"
    },
    "apiManagementSku": {
      "type": "String",
      "defaultValue": "Developer"
    },
    "apiName": {
      "type": "String",
      "defaultValue": "api-1"
    },
    "namedValuesName": {
      "type": "String",
      "defaultValue": "functions-key"
    },
    "backendName": {
      "type": "String",
      "defaultValue": "backend"
    },
    "productName": {
      "type": "String",
      "defaultValue": "starter"
    }
  },
  "functions": [],
  "variables": {
    "functionsUrl": "[concat('https://', parameters('functionsName'), '.azurewebsites.net/api')]",
    "apiName": "[concat(parameters('apiManagementName'), '/', parameters('apiName'))]",
    "policyName": "[concat(variables('apiName'), '/', 'policy')]",
    "namedValuesName": "[concat(parameters('apiManagementName'), '/', parameters('namedValuesName'))]",
    "backendName": "[concat(parameters('apiManagementName'), '/', parameters('backendName'))]",
    "productApiName": "[concat(parameters('apiManagementName'), '/', parameters('productName'), '/', parameters('apiName'))]"
  },
  "resources": [
    {
      "type": "Microsoft.ApiManagement/service",
      "apiVersion": "2021-08-01",
      "name": "[parameters('apiManagementName')]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "[parameters('apiManagementSku')]",
        "capacity": 1
      },
      "properties": {
        "publisherEmail": "[parameters('administratorEmail')]",
        "publisherName": "[parameters('organizationName')]"
      }
    },
    {
      "type": "Microsoft.ApiManagement/service/namedValues",
      "apiVersion": "2021-08-01",
      "name": "[variables('namedValuesName')]",
      "dependsOn": [
        "[resourceId('Microsoft.ApiManagement/service', parameters('apiManagementName'))]"
      ],
      "properties": {
        "displayName": "[parameters('namedValuesName')]",
        "value": "[parameters('functionsKey')]",
        "secret": true
      }
    },
    {
      "type": "Microsoft.ApiManagement/service/backends",
      "apiVersion": "2021-08-01",
      "name": "[variables('backendName')]",
      "dependsOn": [
        "[resourceId('Microsoft.ApiManagement/service/namedValues', parameters('apiManagementName'), parameters('namedValuesName'))]"
      ],
      "properties": {
        "url": "[variables('functionsUrl')]",
        "protocol": "http",
        "resourceId": "[parameters('functionsResourceId')]",
        "credentials": {
          "query": {},
          "header": {
            "x-functions-key": [
              "{{functions-key}}"
            ]
          }
        },
        "tls": {
          "validateCertificateChain": false,
          "validateCertificateName": false
        }
      }
    },
    {
      "type": "Microsoft.ApiManagement/service/apis",
      "apiVersion": "2021-08-01",
      "name": "[variables('apiName')]",
      "dependsOn": [
        "[resourceId('Microsoft.ApiManagement/service', parameters('apiManagementName'))]"
      ],
      "properties": {
        "apiRevision": "1",
        "subscriptionRequired": true,
        "protocols": [
          "https"
        ],
        "isCurrent": true,
        "path": "",
        "format": "openapi",
        "value": "[parameters('openApi')]"
      }
    },
    {
      "type": "Microsoft.ApiManagement/service/apis/policies",
      "apiVersion": "2021-08-01",
      "name": "[variables('policyName')]",
      "dependsOn": [
        "[resourceId('Microsoft.ApiManagement/service/apis', parameters('apiManagementName'), parameters('apiName'))]",
        "[resourceId('Microsoft.ApiManagement/service/backends', parameters('apiManagementName'), parameters('backendName'))]"
      ],
      "properties": {
        "format": "rawxml",
        "value": "[parameters('policies')]"
      }
    },
    {
      "type": "Microsoft.ApiManagement/service/products/apis",
      "apiVersion": "2021-08-01",
      "name": "[variables('productApiName')]",
      "dependsOn": [
        "[resourceId('Microsoft.ApiManagement/service/apis', parameters('apiManagementName'), parameters('apiName'))]"
      ]
    }
  ],
  "outputs": {
    "apiManagementName": {
      "type": "string",
      "value": "[parameters('apiManagementName')]"
    },
    "namedValues": {
      "type": "Object",
      "value": "[reference(parameters('namedValuesName'))]"
    }
  }
}
```

</details>

# Functions の環境変数構築

環境変数のうち、環境/ユーザで変更する可能性のある値のみをテンプレートから切り出すため、
テンプレート内とテンプレート外で定義した環境変数が存在する

## テンプレート外で定義した環境変数（`Functions/environmentVariable.json`）の取り込み

デプロイ用スクリプト（`deploy.js`）で取り込み、Parent テンプレートにパラメータとして受け渡す
（ファイル指定のほうが楽なため、パラメータは一旦 `_parameters.json` として出力している）

```javascript: deploy.js
  const environmentVariable = JSON.parse(
    fs.readFileSync('./Functions/environmentVariable.json', 'utf8')
  );

  // ...省略

  // 親テンプレートのデプロイ
  execSync(
    `az deployment group create --name ${prefix}-deploy \
      --resource-group ${resourceGroup} \
      --template-file template.json \
      --parameters _parameters.json`
  );
```

## テンプレート内/外で定義した環境変数の結合

テンプレート内で定義した環境変数（`variables('appSettings')`）、
テンプレート外で定義した環境変数（`parameters('environmentVariable')`）
を`concat`で結合する

```json: Functions/template.json
  "siteConfig": {
    "appSettings": "[concat(variables('appSettings'), parameters('environmentVariable'))]"
  }
```

## 他リソース（`Storage`、`Application Insights`）プロパティの参照

- `concat`内で配列定義はできないため、変数`variables('appSettings')`として定義しておく

```json: Functions/template.json
  "siteConfig": {
    "appSettings": "[concat(variables('appSettings'), parameters('environmentVariable'))]"
  }
```

- `variables` で同テンプレート内の `resources` で定義したリソースを参照できないため、
  環境変数の変数（`variables('appSettings')`）に含めるプロパティを持つ他リソースのテンプレートを分割する

- `resourceId` によるリソース参照を `parameters`、`variables` で行うとエラーになることが多いため、
  環境変数の変数（`variables('appSettings')`）に含める他リソースのプロパティは文字列として output しておく

- output したプロパティの文字列を使用するテンプレートで文字列のパラメータとして受け取る

< ストレージ接続文字列の例 >

`Storage/template.json` からストレージ接続文字列（`storageConnectionString`）を文字列として output し、
`Functions/template.json` で文字列のパラメータとして受け取ることでリソース参照する必要がなくなる

```json: Storage/template.json
  "outputs": {
    ...
    "storageConnectionString": {
      "type": "string",
      "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value, ';EndpointSuffix=', environment().suffixes.storage)]"
    }
  }
```

```json: Functions/template.json
  "parameters": {
    ...
    "storageConnectionString": {
      "type": "string"
    },
    ...
```

## 環境変数のデプロイ結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/265ff06e-831f-e7d6-8e6e-61f8644c218d.png)


# Api Management の API 構築

## API 定義（ `ApiManagement/openapi.yml`）、ポリシー定義（`ApiManagement/policies.xml`）の取り込み

デプロイ用スクリプト（`deploy.js`）で取り込み、Parent テンプレートにパラメータとして受け渡す
（ファイル指定のほうが楽なため、パラメータは一旦 `_parameters.json` として出力している）

```javascript: deploy.js
  const openApi = fs.readFileSync('./ApiManagement/openapi.yml', 'utf8');
  const policies = fs.readFileSync('./ApiManagement/policies.xml', 'utf8');

  // ...省略

  // 親テンプレートのデプロイ
  execSync(
    `az deployment group create --name ${prefix}-deploy \
      --resource-group ${resourceGroup} \
      --template-file template.json \
      --parameters _parameters.json`
  );
```

## 他リソース（`Functions`）プロパティの参照

`Functions/template.json` から API キー（`functionsKey`）を文字列として output し、
`ApiManagement/template.json` で文字列のパラメータとして受け取る

```json: Functions/template.json
  "outputs": {
    ...
    "functionsKey": {
      "type": "string",
      "value": "[listkeys(concat(resourceId('Microsoft.Web/sites', parameters('functionsName')), '/host/default/'),'2021-02-01').functionKeys.default]"
    }
  }
```

```json: ApiManagement/template.json
  "parameters": {
    ...
    "functionsKey": {
      "type": "String"
    },
```

# リソースのプロパティ確認

[reference](https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/templates/template-functions-resource#reference) 関数でリソースのプロパティを参照できる

```json: ApplicationInsights/template.json
    "applicationInsightsInstrumentationKey": {
      "type": "string",
      "value": "[reference(variables('applicationInsightsName')).InstrumentationKey]"
    }
```

reference 関数の戻り値を Object として出力することでリソースのプロパティを確認することができる

```json: ApiManagement/template.json
  "outputs": {
    ...
    "namedValues": {
      "type": "Object",
      "value": "[reference(parameters('namedValuesName'))]"
    }
  }
```

出力結果
![2022-05-04-13-25-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/b1aac93f-661d-4a7c-5dda-29199c8cb697.png)