Azure Resource Managerテンプレートのノウハウ

# Infrastructure as Codeはお好きですか？

この記事は[#インフラ勉強会 AdventCalendar2018](http://adventar.org/calendars/3022)の ~~15日目~~ 20日目の記事です。
(投稿が遅くなってしまったので日を改めました！)

「Azureのリソースをコード化したいなぁ」と思ったとき、
最も有力なのが「ARMテンプレート」です。
ここ半年くらいずっとテンプレートの事ばかり考えてたのでノウハウをまとめます。
誰かの役に立ったらいいな。

「そもそもARMテンプレートって何？」という方は前に書いた記事をどうぞ↓
[[記事リンク]AzureでInfrastructure as Codeするときの設計を考える](https://qiita.com/tenn25/items/afc36e1091b7bdacf6de)


# 俺のノウハウフォルダが火を吹くぜ！！
## 1.命名規則大事
当たり前ですが、Azure上のリソースやその中に作られるモノ全てに名前をつけなければなりません。
ARMテンプレートは変数名だらけになります。
普通のプログラム以上に命名が大事です。
リーダブルコードを読みましょう。

リソースによっては、ドメイン名に使われるため小文字のみとか、ハイフンすら使えない、とかもあります。
しんどいですが命名規則を決めて頑張るしかないです。

## 2.変数を極力減らす
parameters{}では変数宣言をし、
variables{}では変数を加工することができます。

命名規則をちゃんと決め、文字連結のconcat関数を使うことでparametersの変数を極力減らしましょう。

```json:環境名を変数として環境ごとに一意になる名前をつける
"parameters": {
    "environment": {
      "type": "string",
      "allowedValues": ["Prod","Stg","Dev"]
    }

"variables": {
    "webName": "[concat(parameters('environment'),'Web')]",
    "apiName": "[concat(parameters('environment'),'API')]",
    "dbServerName": "[concat(toLower(parameters('environment')),'DB')]"
}
```

## 3.完全モードor増分モード
ARMテンプレートを触ってて一番悩むところです。
上記のリンクの記事で詳しく触れています。

完全モードは「テンプレートに書かれていないものは削除される」のでテンプレートの信憑性が高いですが、GUIから作ったリソースをテンプレートにマージしておかないと意図せず消してしまいます。

増分モードは「テンプレートに書かれていないものは削除されない」ので、そういったリスクは無いですが、デプロイされてるリソースとテンプレートが完全一致しない可能性が出てくるので変更管理の統制が取れなくなる恐れがあります。

0からの初期構築ならどちらも同じですが、
変更管理をしていくとどちらも一長一短だと思います。

個人的には
構築フェーズは完全モードで良いですが、
運用フェーズは増分モードが無難だと思います。

増分モードでの運用は、テンプレート以外からの変更を禁止するか、テンプレート以外の変更をアクティビティログで検知して、テンプレートに後追いで反映するようなルールが必要になってくると思います。

完全モードで使うなら、テンプレートのデプロイ時にドライランの機能が欲しいです…
(この辺、何かいい方法あったら教えてください)

## 4.テンプレートの書き方の参考になる場所
最初は取っ付きにくさが凄いと思います。
以下を参考にしてみてください。

#### ①公式リファレンス
MS全般に言えますが、Azureに関しては公式ドキュメントをちゃんと読みましょう。
ARMテンプレートについても、各リソースごとにどのようなプロパティがあるか載っています。

Azure 上の Resource Manager のドキュメント
https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/

このページの下の方、「リファレンス＞テンプレートリファレンス」を参考にしましょう。

注意点

- 情報が古い場合があり、任意と書いてあるパラメータが必須になってたりすることがある(実際にあった)
- 日本語版のページはさらに情報が古い可能性がある

#### ②GUIから作ってコードにリバースする
Azureではリソースグループ単位でARMテンプレートに出力することができます。
とりあえずGUIから作ってみて、コード化すると手取り早いです。
各リソースグループ＞Automationスクリプト　から出力可能で、デプロイ実行用のコマンドも確認できます。

注意点
- Automationスクリプトに対応していないスクリプトあり、出力されないものがある
- 変数などがまとまってないので整形が必要

#### ③先人の書いたテンプレートを参考にする
Azure公式で、よくある構成のテンプレートを共有するサイトがあります。

Azure Quickstart Templates
https://azure.microsoft.com/en-us/resources/templates/

Githubはこちら
https://github.com/Azure/azure-quickstart-templates

注意点
- テンプレートが古くて使えない可能性がある
- APIバージョンを確認すること(後述)

## 5.APIバージョンに注意
ARMテンプレートのデプロイは、
裏でAzureResourceManagerのAPIを叩いているわけですが、
同じリソースでもAPIが更新されているので複数バージョンがあります。

APIバージョンが変わるとJSONの形式やプロパティが変わったりするので、注意が必要です。
リソースごとに"apiVersion"の指定が必要なのはその為です。


## 6.パスワードなどの機密情報の扱い

「テンプレートの中にパスワードを埋め込みたくないなぁ・・・」
という状況、よくあると思います。
SQLDatabaseを立てるのにもAdminアカウント名とパスワードの指定が必須です。

一番良い方法は、
機密情報の置き場としてKeyVaultを作成しておき、
そこにDBのAdminパスワードを格納しておくことです。

1. 事前に別リソースグループにKeyVault(OriginKeyVault)を用意して機密情報を格納しておく
2. テンプレート実行
3. parameter.jsonでOriginkeyVaultを参照し、機密情報を取得
4. SQLSatabaseとKeyVaultを新しく作成、DBへの接続文字列を新しいKeyVaultに格納

ということができます。
APIなどがDBに接続する場合は4で作成した接続文字列を参照すれば良いです。

テンプレートから一部抜粋します。

```json:parametar.json
  "parameters": {
    "dbAdminPassword": {
      "reference": {
        "keyVault": {
          "id": "/subscriptions/**********/resourceGroups/[別のリソースグループ]/providers/Microsoft.KeyVault/vaults/OriginKeyVault"
        },
        "secretName": "dbAdminPassword"
      }
    }
  }
```


```json:template.json
  "parameters": {
    "dbAdminPassword": {
      "type": "securestring"
    }
  },

  "resources": [
    {
      "comments": "SQLDatabase",
      "type": "Microsoft.Sql/servers",
      "name": "[variables('dbServerName')]",
      "apiVersion": "2015-05-01-preview",
      "location": "[resourceGroup().location]",
      "properties": {
        "administratorLogin": "admin",
        "administratorLoginPassword": "[parameters('dbAdminPassword')]",
        "version": "12.0"
      }
    },
    {
      "type": "Microsoft.KeyVault/vaults",
      "name": "[variables('keyvaultName')]",
      "apiVersion": "2016-10-01",
      "location": "[resourceGroup().location]",
      "properties": {
        "enabledForDeployment": false,
        "enabledForTemplateDeployment": false,
        "enabledForDiskEncryption": false,
        "accessPolicies": [.....],
        "tenantId": "[subscription().tenantId]",
        "sku": {
          "name": "[parameters('keyVaultSku')]",
          "family": "A"
        },
        "networkAcls": "[parameters('networkAcls')]"
      },
      "resources": [
        {
          "type": "secrets",
          "name": "DbConnectionString",
          "apiVersion": "2016-10-01",
          "location": "[resourceGroup().location]",
          "properties": {
            "contentType": "DB ConnectionString",
            "value": "[concat('Server=tcp:', reference(resourceId('Microsoft.Sql/servers', variables('dbServerName'))).fullyQualifiedDomainName, ',', '1433;Initial Catalog=hoge;Persist Security Info=False;User ID=admin;Password=', parameters('dbAdminPassword'), ';MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;')]"
          },
          "dependsOn": [
            "[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
          ]
        }
  ]
```

## 7.インフラとアプリの境界線に注意

#### Automationの例
Automationは、cronのようにスクリプトを定期実行させるサービスです。

Automationアカウントの中に複数のRunbookを作成し、
Runbookごとにスケジュールを紐づけて実行させます。

注意点としては、
Runbook上で動かす**スクリプトファイルもARMテンプレート上でファイルパスの指定が必要**な点です。
とりあえず空のRunbookだけ作って後からそこにスクリプトをデプロイする、という形が取れません。
「runbookというAzureリソースを作成する」＝「スクリプトを配置する」という扱いなのでインフラレイヤーなんですね。

FunctionAppのように箱だけ作っておいて、中に配置するスクリプトは別途デプロイできる仕様なら良かったのですが、Automationはこの辺が少しモヤモヤしました。
もちろんPowerShell等からもrunbookの作成ができますが、完全モードで運用するとインフラをデプロイする度に消えてしまうので、注意してください。

#### Automationの対策

スクリプトのパスはURL指定で、githubのようにインターネットに公開されてる場所にあれば直接指定可能です。
問題なのはスクリプトを管理しているリポジトリがPrivateの場合です。
自分はAzureDevOpsのGitを使っているのですが、ARMからAzureDevOpsへの認証の通し方が分かりませんでした。。
(AzureDevOpsのGitからAutomationスクリプトを取得してRunbookを作成する方法を知ってる方居たら教えてください)

仕方がないので、事前に別リソースグループのBlobストレージにスクリプトファイルを配置しておき、
ARMテンプレートからはBlobのパスを指定するようにしています。

```json:Automationアカウントとrunbookの作成

  "variables": {
   "runbookUriForSqlDatabase": "[concat('https://*************.blob.core.windows.net/AutomationScripts/AutoScale/forSQLDatabase.ps1')]",
  }

  "resources": {
    {
      "comments": "Automationアカウント",
      "name": "[variables('automationAccountName')]",
      "type": "Microsoft.Automation/automationAccounts",
      "apiVersion": "2015-10-31",
      "location": "[resourceGroup().location]",
      "dependsOn": [],
      "tags": {},
      "properties": {
        "sku": {
          "name": "Free"
        }
      }
    },
    {
      "comments": "SQL Databaseのスケール変更用RunBook",
      "name": "[concat(variables('automationAccountName'), '/', variables('runbookNameForSqlDatabase'))]",
      "type": "Microsoft.Automation/automationAccounts/runbooks",
      "apiVersion": "2015-10-31",
      "location": "[resourceGroup().location]",
      "tags": {},
      "properties": {
        "runbookType": "Script",
        "logProgress": false,
        "logVerbose": false,
        "description": "SQL Databaseのスケール変更用RunBook",
        "publishContentLink": {
          "uri": "[variables('runbookUriForSqlDatabase')]",
          "version": "1.0.0.0"
        }
      },
      "dependsOn": [
        "[resourceId('Microsoft.Automation/automationAccounts',variables('automationAccountName'))]"
      ]
    }
  }
```


## 8.変数を動的に作成したい

例えば、現在時刻を取得してテンプレート内で使いたい場合、どうすれば良いでしょうか。
残念ながらARMテンプレートの関数にはget_date()のような関数はありません。。

動的な変数を使いたい場合は
AzureDevOpsのパイプラインを使ってデプロイしましょう。

AzureDevOpsにインフラデプロイ用のビルドパイプラインを作成します。
ステップとしては、
1.PowerSHellによる変数作成ステップ
2.ARMテンプレートの実行

のようにします。
パイプラインを使えば、前ステップにKeyVaultから値を取得するステップを入れることもできます。

#### Automationの例
AutomationのRunbookがテンプレート化しづらい要因として、
"startTime"というプロパティがあります。

これは、「いつからスケジュールを有効化するか」という時刻を表すのですが、
何故かデプロイした現在時刻よりも15分以上先の時間を指定しなければなりません。

そのため、どうにか現在時刻を取得して、+15分してからARMテンプレートのparametersに渡す必要があります。

例としてパイプライン上で後続に変数を渡す例を載せます。

1. PowerShellを実行し、＄StartTimeという変数に15分後の時間を格納
2. ARMテンプレートのデプロイステップでparameter.jsonを上書きする設定をします。
[Override template parameters]の項目を指定すると、parameter.jsonで指定された変数が一覧表示されるので、PowerShellの変数に上書きしたい箇所を"$変数名"としましょう。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f37393233362f37326532383436622d376236312d613834302d613461612d6264323864616239666337342e706e67.png](https://qiita-image-store.s3.amazonaws.com/0/79236/fdfaccdd-65bb-8a58-67e9-c096f5d7e46e.png)


これでARMテンプレート内で動的な変数が使用可能になります。

## 9.リソース作成後に発行されるキーを取得する

例えば、RedisCacheへの接続キーは、Redis自体が作成された時に自動発行されます。
そのキーを後で使うためにKeyVaultに格納したい場合どうすれば良いでしょうか。

そんな時は、リソースの値を取得する関数を使いましょう。
これが使えるようになると、出来ることの幅が広がります。

Azure Resource Manager テンプレートのリソース関数
https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/resource-group-template-functions-resource

KeyVaultのsecretにRedisCacheのPrimaryKeyを格納する例です。
接続文字列のpasswordの部分は、listKeys関数によってRedisのPrimaryKeyを参照しています。

```json
        {
          "type": "secrets",
          "name": "RedisSecret",
          "apiVersion": "2016-10-01",
          "location": "[resourceGroup().location]",
          "properties": {
            "contentType": "Redis ConnectionString",
            "value": "[concat(variables('redisName'), '.redis.cache.windows.net:6380,password=', listKeys(resourceId('Microsoft.Cache/Redis', variables('redisName')), providers('Microsoft.Cache', 'Redis').apiVersions[0]).primaryKey, ',ssl=True,abortConnect=False')]"
          },
          "dependsOn": [
            "[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
          ]
        }
```


## 10.リソースのエンドポイントやドメイン名を取得する

listKeys()関数と似ているのですが、
reference()という関数もあります。

実は上の方で、DBのパスワードをKeyVaultから取得するサンプルでしれっと使われてます。
接続文字列を作成して、KeyVaultに格納する部分でSQLDatabaseのドメイン名の取得に使っています。

```json
"value": "[concat('Server=tcp:', reference(resourceId('Microsoft.Sql/servers', variables('dbServerName'))).fullyQualifiedDomainName, ',', '1433;Initial Catalog=hoge;Persist Security Info=False;User ID=admin;Password=', parameters('dbAdminPassword'), ';MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;')]"
```

KeyVaultに格納される接続文字列としては

```
Server=tcp:hogehoge.database.windows.net,1433;Initial Catalog=hoge;Persist Security Info=False;User ID=admin;Password=password1234;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;
```

のようになります。
ドメイン名hogehoge.database.windows.netを関数で取得することができています。
dbserver名から頑張って文字列生成してもできますが、
BLOBのエンドポイントURIや、完全修飾ドメイン名などをオブジェクトから返すことができるので覚えておきましょう。



# さいごに

複雑なことをやろうとすると細かいテクニックが必要だったり、
テンプレートの仕様に惑わされることも多いですが、
一度書いてしまえばその後が非常に楽なので、是非参考にしてARMテンプレートを使ってみてください！！