Azure ARMテンプレートで Key Vault にシークレットを書き込む

# ちょっとハマったので

ゴール
**最終的には１つのテンプレートで「Azure Cosmos DB アカウントを作成して、Key vault にそのアカウントのURIとキーを保存する」というのが目的でした。**

この一連の処理を二つに分けて、それぞれ動作確認したあとにひとつにまとめる予定でしたが、ちょっと方針変更したのでひとつにまとめたものは動作確認してません（いちおう、多分動くだろう状態で後述します）

以下、動作確認済みの２つのサンプルについて解説します。

1. Azure Cosmos DB アカウントを作成して outputs として URIとキーを出力する
2. Key vault の適当なリテラルなシークレットを保存する

なお、両方とも下のパラメータファイルを使います。（```example-database-name-123```と```examplekeyvalut123```は、ご自分の環境に合わせて変更します）

```json:Parameters.json
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "DatabaseAccountName": {
            "value": "example-database-name-123"
        },
        "Location": {
            "value": "West US"
        },
        "KeyVaultName": {
            "value": "examplekeyvalut123"
        }
    }
}
```

## Azure Cosmos DB アカウントを作成して outputs として URIとキーを出力する

```json:Db-template.json
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "DatabaseAccountName": {
        "type": "string",
        "metadata": {
          "description": "The Azure Cosmos DB account name."
        }
      },
      "Location": {
        "type": "string",
        "defaultValue": "[resourceGroup().location]",
        "metadata": {
          "description": "Location for all resources."
        }
      },
      "KeyVaultName": {
        "type": "string",
        "metadata": {
          "description": "The Azure Key vault name."
        }
      }
    },
    "resources": [
      {
        "apiVersion": "2015-04-08",
        "type": "Microsoft.DocumentDB/databaseAccounts",
        "kind": "GlobalDocumentDB",
        "name": "[parameters('DatabaseAccountName')]",
        "location": "[parameters('Location')]",
        "properties": {
          "databaseAccountOfferType": "Standard",
          "locations": [
            {
              "locationName": "[parameters('Location')]",
              "failoverPriority": 0
            }
          ],
          "capabilities": []
        },
        "tags": {
          "defaultExperience": "Core (SQL)"
        }
      }
    ],
    "outputs":{
      "accountUri": {
        "type" : "string",
        "value": "[reference(parameters('DatabaseAccountName')).documentEndpoint]"
      },
      "keys": {
        "type": "string",
        "value": "[listKeys(parameters('DatabaseAccountName'), '2015-04-08').primaryMasterKey]"
      }
    }
  }
```

テンプレートの実行はPowerShell Az module を使います。

```
New-AzResourceGroupDeployment -Name ExampleDeployment -ResourceGroupName myrg123 -TemplateFile .\Db-Template.json -TemplateParameterFile .\Parameters.json
```

実行すると、こんな結果が出ます

```
DeploymentName          : ExampleDeployment
ResourceGroupName       : myrg123
ProvisioningState       : Succeeded
Timestamp               : 2/1/2019 4:13:34 AM
Mode                    : Incremental
TemplateLink            : 
Parameters              : 
                          Name                   Type                       Value     
                          =====================  =========================  ==========
                          databaseAccountName    String                     example-database-name-123
                          location               String                     West US 
                          
Outputs                 : 
                          Name             Type                       Value     
                          ===============  =========================  ==========
                          accountUri       String                     https://example-database-name-123.documents.azure.com:443/
                          key1             String                     
                          XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
                          key2             String                     
                          XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==
                          
DeploymentDebugLogLevel : 
```

ここで注目すべきは Outputs として新規の Cosmos DB アカウントのURIとキー情報が出力されていることです。目的はコンソールに表示することではなく、テンプレート内で、Key vault への新規シークレットの値として使うことです。

もうひとつのテンプレートを見てみましょう。

## Key vault に適当なリテラルなシークレットを保存する

```json:Kv-template.json
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "Location": {
        "type": "string",
        "defaultValue": "[resourceGroup().location]",
        "metadata": {
          "description": "Location for all resources."
        }
      },
      "KeyVaultName": {
        "type": "string",
        "metadata": {
          "description": "The Azure Key vault name."
        }
      }
    },
    "resources": [
      {
        "type": "Microsoft.KeyVault/vaults",
        "name": "[parameters('KeyVaultName')]",
        "apiVersion": "2016-10-01",
        "location": "westus",
        "properties": {
          "mode": "Incremental",
            "sku": {
                "family": "A",
                "name": "Standard"
            },
            "tenantId": "あなたのテナントID",
            "accessPolicies": [
              {
                  "tenantId": "あなたのテナントID",
                  "objectId": "とあるユーザーのID",
                  "permissions": {
                      "keys": ["All"],
                      "secrets": ["All"],
                      "certificates": ["All"]
                  }
              },
              {
                  "tenantId": "あなたのテナントID",
                  "objectId": "とあるサービスアカウントのID",
                  "permissions": {
                    "keys": ["All"],
                    "secrets": ["All"],
                    "certificates": ["All"]
                }
              },
              {
                  "tenantId": "あなたのテナントID",
                  "objectId": "とあるアプリのID",
                  "permissions": {
                    "keys": ["All"],
                    "secrets": ["All"],
                    "certificates": ["All"]
                }
              }
          ]
        }
    },
      {
        "type": "Microsoft.KeyVault/vaults/secrets",
        "name": "[concat(parameters('KeyVaultName'), '/mysecretitem')]",
        "apiVersion": "2016-10-01",
        "location": "[parameters('Location')]",
        "properties": {
          "value": "this is my secret"
        }
      }
     ]
  }
```

実行はほぼ同じやりかた。

```
New-AzResourceGroupDeployment -Name ExampleDeployment -ResourceGroupName myrg123 -TemplateFile .\Kv-Template.json -TemplateParameterFile .\Parameters.json
```

結果として、key vault に新たに「mysecretitem」シークレットが加えられます。値は「this is my secret」になります。

### 注意点その１

ここが実はハマったとこの一つなんですが、最初は「既存の key vault に新規のシークレットを加えるだけなので、テンプレートにはシークレットを表す resource が定義されていれば十分」だと思ってたんですが、そのままだと「parent リソースが無いよ！」みたいにエラーが出てしました。なぜかというと、シークレットは key vault リソースの下にあるサブリソースなので、key vault を表すresource も同時に指定しなくてはいけなかったのです。この辺りはARMの作法をつい忘れてしまいますが、ARMテンプレートはあくまでも「望ましい結果の状態」を記述し、テンプレートのエンジンがその状態にしてくれる、というものなので、 シークレットを子にもつ key vault の「望ましい状態」も同時に記述しないといけないんですね。

### 注意点その２

注意点その１で指摘したように、テンプレートに key vault を表すresourceも記述しないといけないんですが、出来るだけ短めにしておきたいところです。特にAccess Policiesの情報は長くなる場合が多いので、それをいちいちテンプレートにも書かなくてはならなかったら面倒です。しかし、結論としてはそれをしないといけないようです。

[ドキュメンテーション](https://docs.microsoft.com/en-us/azure/templates/microsoft.keyvault/2018-02-14/vaults)によると最近の２つのバージョンでは Access Policiesは Required = No で、必須ではないはずです。

で、あるならば、Access Policiesを指定しないか、値をnullにしてもきちんと動くはずなんですが、エラーになります。なので```accessPolicies : []``` のように空の配列を指定して実行してみたら、なんと key vault の Access Policiesが全てきれいさっぱり空になってしまいました。これは注意が必要です。システムを全て止めてしまうリスクがあります。

ネットの情報によると、Access Policiesに関しては「指定されたまま上書き」とう振る舞いをするそうです。 

みなさんも気を付けて！（私は幸いテスト用の key vault を使ってたので同僚をパニックに陥れることは避けることが出来ました）

# Azure Cosmos DB アカウントを作成して、Key vault にそのアカウントのURIとキーを保存する

２つのテンプレートを合わせたものです。多分ですがこれでうまくいくはずです。

なぜうまくいくと思うのかの根拠ですが、[ドキュメンテーション](https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-template-functions-resource#reference)によるとこうあります：

> You don't need to also use the dependsOn property. The function isn't evaluated until the referenced resource has completed deployment.

```reference()``` 関数が使われている場合、それを先に解決するために順番が決定されるようです。

```
{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "DatabaseAccountName": {
      "type": "string",
      "metadata": {
        "description": "The Azure Cosmos DB account name."
      }
    },
    "Location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]",
      "metadata": {
        "description": "Location for all resources."
      }
    },
    "KeyVaultName": {
      "type": "string",
      "metadata": {
        "description": "The Azure Key vault name."
      }
    }
  },
  "resources": [
    {
      "apiVersion": "2015-04-08",
      "type": "Microsoft.DocumentDB/databaseAccounts",
      "kind": "GlobalDocumentDB",
      "name": "[parameters('DatabaseAccountName')]",
      "location": "[parameters('Location')]",
      "properties": {
        "databaseAccountOfferType": "Standard",
        "locations": [
          {
            "locationName": "[parameters('Location')]",
            "failoverPriority": 0
          }
        ],
        "capabilities": []
      },
      "tags": {
        "defaultExperience": "Core (SQL)"
      }
    },
    {
      "type": "Microsoft.KeyVault/vaults",
      "name": "[parameters('KeyVaultName')]",
      "apiVersion": "2016-10-01",
      "location": "westus",
      "properties": {
        "mode": "Incremental",
        "sku": {
          "family": "A",
          "name": "Standard"
        },
        "tenantId": "あなたのテナントID",
        "accessPolicies": [
          {
            "tenantId": "あなたのテナントID",
            "objectId": "とあるユーザーのID",
            "permissions": {
              "keys": ["All"],
              "secrets": ["All"],
              "certificates": ["All"]
            }
          },
          {
            "tenantId": "あなたのテナントID",
            "objectId": "とあるサービスアカウントのID",
            "permissions": {
              "keys": ["All"],
              "secrets": ["All"],
              "certificates": ["All"]
            }
          },
          {
            "tenantId": "あなたのテナントID",
            "objectId": "とあるアプリのID",
            "permissions": {
              "keys": ["All"],
              "secrets": ["All"],
              "certificates": ["All"]
            }
          }
        ]
      }
    },
    {
      "type": "Microsoft.KeyVault/vaults/secrets",
      "name": "[concat(parameters('KeyVaultName'), '/dbaccountUri')]",
      "apiVersion": "2016-10-01",
      "location": "[parameters('Location')]",
      "properties": {
        "value": "[reference(parameters('DatabaseAccountName')).documentEndpoint]"
      }
    },
    {
      "type": "Microsoft.KeyVault/vaults/secrets",
      "name": "[concat(parameters('KeyVaultName'), '/dbaccountKey')]",
      "apiVersion": "2016-10-01",
      "location": "[parameters('Location')]",
      "properties": {
        "value": "[listKeys(parameters('DatabaseAccountName'), '2015-04-08').primaryMasterKey]"
      }
    }
  ]
}
```

この、ひとつにまとめたテンプレートですが、これはこれでうまくいけば便利だとは思うんですが、重大な問題があると判断して別の方法を考えることにしました。それは、このパターンで、他のリソース用のテンプレートをたくさん書いた場合、Key vault のアクセスに関する情報を何か所にもコピーして保守しないといけなくなるからです。

リソースの数が少ない場合や、なんらかの限定的なテスト環境のセットアップなどには使えるかもしれませんが、それ以外の場合はメンテが大変になるかもしれません。

ただ、回避する方法としては、Access Policies をパラメータファイルにまとめるという手もあります。ただし、パラメータファイルはテンプレートにつきひとつなので、全てのリソースに関するパラメーターをひとつのパラメータファイルで管理すると、それはそれでメンテがしにくくなります。複数のパラメータファイルをPowerShellなどでマージする方法も紹介されていましたが、そうなると別の種類の複雑さを招きいれることになります。難しいところですね。

# まとめ

Azure Resource Template ARMテンプレートは便利なんだけど、いろいろと作法があってはまってしまうことがありますね。ですがテンプレートをバージョン管理して、クラウドリソースとしてデプロイするという流れは、多くのクラウドリソースを利用することで混乱しがちなシステムを整理して運用するには強力なツールだと思います。