Resource Group 内の特定 Resource の JSON テンプレートを取得する（Azure CLI 編）

# 背景

Azure でシステムを構築した際に、後々のために JSON テンプレートを用意しておくと、同じシステム（サーバ、ネットワークなどのインフラ部分）がサクッと構築できる。
システムの規模にもよるが、Resource Group 全体で1つの JSON テンプレートにすると結構な量になる。

そこで各リソースごとに JSON テンプレートを出力しようとする。
これが結構面倒くさい。

今回は Linux & Azure CLI ユーザー向けの内容。
即席で作ったものなので最適化等は読者の皆様にお任せ。（投げやり）
時間と余裕があったら改善することにする。

PowerShell の方が楽そうだなぁ、なんて思いながら Python でゴリゴリ書いていた。

# 実行する Azure CLI コマンド

詳細は後述するとして、まずはコマンドでなにを使うのかを紹介。
Azure CLI は JSON 出力しないと各値を再利用できないので、`--json` オプションは必須。

1. Resource Group 内の Resource 一覧を取得

    ```
    azure resource list --resource-group [RG Name] --tags [Key]=[Value] --json
    ```

2. Resource Provider の詳細情報を取得

    ```
    azure provider show --namespace [Resource Provider] --json
    ```

3. Resource の詳細を取得（今回の目的）

    ```
    azure resource show --resource-id [Resource ID] --api-version [API Version] --json
    ```

# 全体像

```python:Python
import os
import commands
import json

def getresourcejson():
    # 特定のタグのリソースのみを取得するコマンドを用意する
    resourceListCmd = 'azure resource list --resource-group [RG Name] --tags [Key]=[Value] --json'
    # コマンドを実行し、出力を取得する
    resourceList = commands.getoutput(resourceListCmd)
    # JSON出力をオブジェクト化する
    resourceListJson = json.loads(resourceList)
    # Resourceを1つ1つ解析する
    for resource in resourceListJson:
        # API Versionを取得するため、各リソースタイプを取得する
        # この時、'[Resourece Provider]/[Resource Type]' となっているため、スラッシュでSplitして各々変数化する
        resourceTypeStr = str(resource['type']).split('/')
        resourceProv = resourceTypeStr[0]
        resourceType = resourceTypeStr[1]
        # 取得したリソースタイプから最新のAPI Versionを取得するためのコマンドを用意する
        resourceTypeListCmd = 'azure provider show --namespace ' + resourceProv + ' --json'
        # コマンドを実行し、出力を取得する
        resourceTypeList = commands.getoutput(resourceTypeListCmd)
        # JSON出力をオブジェクト化する
        resourceTypeListJson = json.loads(resourceTypeList)
        # Resource Provider配下のResource Typeが欲しいので取り出す
        rTypes = resourceTypeListJson['resourceTypes']
        # API Versionの箱を用意する
        apiVer = ''
        # API Versionを取得する
        for rType in rTypes:
            # 目的のもの以外のResource Typeには興味ないのでフィルタリングする
            if str(rType['resourceType']) == resourceType:
                # apiVersionsの先頭が最新（っぽい）のでそれを取得する
                # 中には末尾が最新のResourceもあるので、事前に調べておくことを推奨する
                apiVer = str(rType['apiVersions'][0])
        # 出力ファイル名を定義する
        outFileName = str(resource['name']) + '.json'
        # 各リソースのJSONを取得するためのコマンドを用意する
        resCmd = 'azure resource show --resource-id ' + str(resource['id']) + ' --api-version ' + apiVer + ' --json'
        # コマンドを実行する
        res = commands.getoutput(resCmd)
        # 標準出力だとタブサイズが2なので4にする
        # JSON出力をオブジェクト化する
        resJson = json.loads(res)
        # タブサイズを4にしてファイル書き込み
        with open(outFileName, 'w') as f:
            # sort_keysをTrueにしちゃうと、Azure JSONテンプレート的には見づらいのでFalseにしておく
            json.dump(resJson, f, sort_keys = False, indent = 4)
        # 出力完了を表示する
        print str('Complete output. File: ') + outFileName

if __name__ == '__main__':
    getresourcejson()
```

# 解説
## 1. "タグ" を指定する

冒頭の Azure CLI コマンドでタグを指定しているのが1つのポイント。

```python:Python
# 特定のタグのリソースのみを取得するコマンドを用意する
resourceListCmd = 'azure resource list --resource-group [RG Name] --tags [Key]=[Value] --json'
```

後々 JSON テンプレートがあると便利なリソースは、ほとんどタグを付与できる。
タグを付けていない場合、JSON テンプレートに必要ないものをフィルタリングする必要が出てくる。

タグを付けておくと他にも便利なことがあるので、結構オススメ。

複数指定できるので、例えば DMZ に置いている VM だけ欲しい、とかにも対応できる。
以下がタグ付けの例。

```
azure vm set --resource-group [RG Name] --name [VM Name] --tags "ResourceType=VM;Segment=DMZ"
```

## 2. 「azure resource list --json」の出力

このコマンドで出力される情報は限定的。
だけども、今回の目的を達成するためには十分。

必要なのは、下記3つ。

1. Resource ID

    対象リソースの JSON テンプレートを取得するのに必要。

2. Name

    対象リソースの JSON テンプレートを保存するファイル名に利用。

3. Resource Type

    対象リソースの JSON テンプレートを取得する際に必要となる API Version を取得するのに必要。

出力される JSON は、各リソースの ID などの情報をカンマ区切りで整形しているので、配列として扱う。
そのために Foreach で回している。

## 3. API Version の取得

ここからは API Version を取得するためにいろいろ試行錯誤している。
もっと手軽に取得させてくれよ、Microsoft さん。。。

### 3-1. Resource Type を Provider と Type に分ける

API Version を取得には、Resource Provider から情報を引っ張ってこないといけない。
ここで登場するのが `azure provider show` コマンド。

しかも `--json` オプションを付けないと API Version のリストを拾えないというクセモノ。
JSON 化しないとコード上で再利用できないからいいけど、先に確認しておきたいじゃない？
確認するためにもわざわざ JSON 化しなきゃいけない。
要注意。

`azure provider show` コマンドを実行する際の必須パラメータが `--namespace` 。
ここに渡すのは Resource Provider であって Resource Type でないことに注意。

`azure resource list` で拾った Resource Type ではエラーとなる。
この Resource Type のスラッシュ以前が Resource Provider で、スラッシュ以後が Resource Type 。

なので Split しましょう。
Resource Type も後々使うので、両方とも変数化しておいた方が識別しやすい。

### 3-2. Resource Provider 配下の Resource Type 一覧から目的の Resource Type を絞り API Version を取得

`azure provider show` の JSON 出力は1つのオブジェクトだけども、この中の Resource Type は複数あるので、またしても Foreach 。

先ほど Split した Resource Type はここで使う。
この Resource Type が "目的の Resource Type" であるから。

目的の Resource Type の中に API Version が入っているので、ここから取り出す。
ただし、複数あるので注意。

私がざっと確認した限りでは、先頭のものが最新だったので、決め打ちで先頭の API Version を拾っている。
Resource Type によっては先頭じゃない場合があるので、必要に応じて修正して欲しい。

## 4. 最終目的 JSON テンプレート取得

API Version が取得できれば、あとは Resource ID を指定して `azure resource show` を実行すればよい。

`--json` オプションの標準出力から直接ファイルに書き込んでもよいが、インデントのタブサイズが 2 である点が気になる私のような方は、`json.dump` でタブサイズを変更する。

一般的な JSON だったら Key でソートするのがデファクトスタンダードっぽいけれども、Azure JSON テンプレートの場合は、Azure 側が指定した順番の方が見やすいので、ソートしないことをオススメする。

# JSON テンプレートをお料理

この Python スクリプトを実行すれば、各リソースごとに JSON テンプレートがファイル出力されるので、あとは Azure Resource Manager のしきたりに従って、Resource Group をテンプレート化しちゃいましょう。

テンプレート化したあとは、Ansible 等で VM 上にミドルをインストールしたりとかすれば、ほぼ自動化できちゃう。

# Azure PowerShell 版は後日

冒頭で述べたとおり、今回は Azure CLI 向けの方法。
後日 Azure PowerShell 版も作ってみる。

# 追記（2016/12/28 1回目） - お詫び

気づいてしまった。
Azure PowerShell だと JSON 出力できないことに・・・。

パイプで Convertto-Json すればいいかな、って思ったけど、実際やってみたら中身が結構違う。
Azure CLI のように `-Json` オプション作ってくれないかな・・・。

というわけで、当初 Azure PowerShell 編も書こうとしていましたが、誤った情報を掲載することになるのでやめます。
期待していた方、申し訳無い・・・。

# 追記（2016/12/28 2回目） - ソースの最適化

汚いソースのまま公開したのがちょっと心残りだったので、ソースをキレイに。

```python:Python
import sys
import commands
import json

def ExecuteCmdlet(cmdlet):
    result = commands.getoutput(cmdlet)
    return json.loads(result)

def GetResourceList(rgName, tags):
    cmdlet = 'azure resource list --resource-group ' + rgName + ' --tags ' + tags + ' --json'
    return ExecuteCmdlet(cmdlet)

def GetResourceTypeList(provider):
    cmdlet = 'azure provider show --namespace ' + provider + ' --json'
    result = ExecuteCmdlet(cmdlet)
    return result['resourceTypes']

def GetResource(resourceId, apiVersion):
    cmdlet = 'azure resource show --resource-id ' + resourceId + ' --api-version ' + apiVersion + ' --json'
    return ExecuteCmdlet(cmdlet)

def GetApiVersion(typeList, target):
    result = ''
    for t in typeList:
        if str(t['resourceType']) == target:
            result = str(t['apiVersions'][0])
    return result

def GetResourceJson(resourceGroupName, tagName, tagValue):
    tags = tagName + '=' + tagValue
    resourceList = GetResourceList(resourceGroupName, tags)
    for resource in resourceList:
        resourceName = str(resource['name'])
        resourceId = str(resource['id'])
        resourceProvider = str(resource['type']).split('/')[0]
        targetResourceType = str(resource['type']).split('/')[1]
        resourceTypeList = GetResourceTypeList(resourceProvider)
        apiVersion = GetApiVersion(resourceTypeList, targetResourceType)
        outFileName = resourceName + '.json'
        resources = GetResource(resourceId, apiVersion)
        with open(outFileName, 'w') as f:
            json.dump(resources, f, sort_keys = False, indent = 4)
        print str('Complete output. File: ') + outFileName

if __name__ == '__main__':
    argvs = sys.argv
    if len(argvs) != 4:
        print 'Usage: python GetResourceJson.py [Resource Group Name] [Tag Name] [Tag Value]'
        exit()
    rgName = argvs[1]
    tagName = argvs[2]
    tagValue = argvs[3]
    GetResourceJson(rgName, tagName, tagValue)
    exit()
```