Azure CLI でリソースをデプロイする (EventHubs, Stream Analytics)

普段リソースのプロビジョンは断然 Terraform 派で、なければ貢献するという私ですが、今回は他の会社さんにお願いするという関係上、PowerShell か Bash かという縛りがありチームで Azure CLI を主に使ってプロビジョンすることになりましたので、使い方をメモしておきます。

# Azure CLI をデプロイに使うときのメリットデメリット

Azure CLI を使ったデプロイはなかなか思ったより快適です。

## メリット

* 簡単に書ける (ARM template よりずっと楽)
* Azure CLI というオフィシャルのツールのみあれば、他にインストール要らない
* Cloud Shell からも使える (これは Terraformも）
* だいたいどのOSでも動く (Linux, Mac, Windows 最近 PowerShell もそうなりつつあるが、挙動はまだ怪しい)

## デメリット

Terraform みたいに間違えたら定義変えてデプロイとかのルーチーンができない

* べき等性はない
* Terraform 程簡潔ではない

# インストール

めんどくさかったら、Cloud Shell を使うといいですし、面倒でなければ下記のドキュメントを見てインストールしてもいいし、Docker イメージを使ってもいいです。

* [Azure CLI 2.0 のインストール](https://docs.microsoft.com/ja-jp/cli/azure/install-azure-cli?view=azure-cli-latest)

# ログイン

Azure CLI を使うためには、Login が必要です。(Cloud Shell なら既にログイン済み)

```bash
az login 
```

インタラクティブにしたくない場合は

```bash
az login -u username(e-mail) -p password
```

もしくは、Service Principal も使用可能です。

```
    Log in with a service principal using client secret.
        az login --service-principal -u http://azure-cli-2016-08-05-14-31-15 -p VerySecret --tenant
        contoso.onmicrosoft.com


    Log in with a service principal using client certificate.
        az login --service-principal -u http://azure-cli-2016-08-05-14-31-15 -p ~/mycertfile.pem
        --tenant contoso.onmicrosoft.com
```

# EventHubs で使いがちなコマンド

EventHubs のデプロイで使うだろうコマンドを整理します。おそらく、EventHubs の Namespace のデプロイと、EventHub のデプロイ、そして、アクセスキーや、コネクションストリングの取得でしょう。

## EventHubs Namespace

```bash
az eventhubs namespace create -g {resource_group_name} -n {namespace name} -l eastus --sku Basic

{
  "createdAt": "2018-06-28T05:55:16.393000+00:00",
  "id": "/subscriptions/xxxxxxxxxxxx/resourceGroups/xxxxxxxxxx0109-rg/providers/Microsoft.EventHub/namespaces/dooh2tsushisome",
  "isAutoInflateEnabled": false,
  "location": "East US",
  "maximumThroughputUnits": 0,
  "metricId": xxxxxxx:xxxxxxxxxx",
  "name": "xxxxxx",
  "provisioningState": "Succeeded",
  "resourceGroup": "dooh2tsushipro0109-rg",
  "serviceBusEndpoint": "https://xxxxxxxx.servicebus.windows.net:443/",
  "sku": {
    "capacity": 1,
    "name": "Basic",
    "tier": "Basic"
  },
  "tags": {},
  "type": "Microsoft.EventHub/Namespaces",
  "updatedAt": "2018-06-28T0
```

## EventHub 

ちなみにマニュアルだと、必須は、最初の３つとのことですが、実際は、違いました。ここに書いているのをしているする必要がありました。

```bash
$ az eventhubs eventhub create --name {eventhub name} --namespace-name {namespace name} -g {resource group name} --message-retention 1 --partition-count 15
{
  "additionalProperties": {
    "location": "East US"
  },
  "captureDescription": null,
  "createdAt": "2018-06-28T06:00:59.453000+00:00",
  "id": "/subscriptions/022b3013-c323-4ec6-a859-5ea2eff17ba2/resourceGroups/dooh2tsushipro0109-rg/providers/Microsoft.EventHub/namespaces/xxxxxxxxxx/eventhubs/downtime",
  "messageRetentionInDays": 1,
  "name": "downtime",
  "partitionCount": 15,
  "partitionIds": [
    "0",
    "1",
    "2",
    "3",
    "4",
    "5",
    "6",
    "7",
    "8",
    "9",
    "10",
    "11",
    "12",
    "13",
    "14"
  ],
  "resourceGroup": "dooh2tsushipro0109-rg",
  "status": "Active",
  "type": "Microsoft.EventHub/Namespaces/EventHubs",
  "updatedAt": "2018-06-28T06:01:06.417000+00:00"
}

```

## PrimaryKey と Connection String 

`az eventhubs namespace authorization-rule keys` というサブコマンドがありました。

```bash
az eventhubs namespace authorization-rule keys list --resource-group {resource group name} --namespace-name {namespace name} --name RootManageSharedAccessKey
{
  "aliasPrimaryConnectionString": null,
  "aliasSecondaryConnectionString": null,
  "keyName": "RootManageSharedAccessKey",
  "primaryConnectionString": "YOUR_CONNECTION_STRING",
  "primaryKey": "YOUR_PRIMARY_KEY",
  "secondaryConnectionString": "YOUR_SECONDARY_CONNECTION_STRING",
  "secondaryKey": "YOUR_SECONDARY_KEY"
}
```

最後にクエリーを書けば直接取得できます。

```bash
az eventhubs namespace authorization-rule keys list --resource-group dooh2tsushipro0109-rg --namespace-name dooh2tsushisome --name RootManageSharedAccessKey --query primaryKey --output tsv
YOUR_PRIMARY_KEY_HERE
```

# Stream Analytics 

Stream Analytics の方は残念ながら、Azure CLI は対応していません。また、Output / Input の設定とかクエリの設定とかいかにも面倒くさそうです。クエリってどうやってデプロイすればいいだろう、、、と悩みますが基本的にデプロイに関しては ARM テンプレートで可能です。[Stream Analytics の Visual Studio 連携を試してみる](https://qiita.com/TsuyoshiUshio@github/items/b493a6ab8276cdbd9108)のエントリで書いたのですが、Visual Studio で Stream Analytics 連携を使って SQL や、Input/Output を開発すると ARM テンプレートを自動で吐いてくれます。今回はその中身を見てみましょう。

## テンプレート

コードをよく見ると、クエリーや、Input/Output の設定がすでに組み込まれています。これは素晴らしい。後はパラメータとして、コネクションストリングなどを渡せばよさそうです。

_template.json_

```json
{
  "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "StreamAnalyticsJobName": {
      "type": "string",
      "minLength": 3,
      "maxLength": 63,
      "metadata": {
        "description": "Stream Analytics Job Name, can contain alphanumeric characters and hypen and must be 3-63 characters long"
      }
    },
    "Location": {
      "type": "string"
    },
    "OutputStartMode": {
      "type": "string",
      "allowedValues": [
        "JobStartTime",
        "CustomTime",
        "JobStartTime"
      ]
    },
    "DataLocale": {
      "type": "string"
    },
    "OutputErrorPolicy": {
      "type": "string",
      "allowedValues": [
        "Drop",
        "Stop"
      ]
    },
    "EventsLateArrivalMaxDelayInSeconds": {
      "type": "int"
    },
    "EventsOutOfOrderMaxDelayInSeconds": {
      "type": "int"
    },
    "EventsOutOfOrderPolicy": {
      "type": "string",
      "allowedValues": [
        "Adjust",
        "Drop"
      ]
    },
    "StreamingUnits": {
      "type": "int",
      "minValue": 1,
      "maxValue": 48,
      "metadata": {
        "description": "Number of Streaming Units"
      },
      "allowedValues": [
        1,
        3,
        6,
        12,
        18,
        24,
        30,
        36,
        42,
        48
      ]
    },
    "CompatibilityLevel": {
      "type": "string",
      "allowedValues": [
        "1.0",
        "1.1"
      ]
    },
    "Input_downtime_serviceBusNamespace": {
      "type": "string"
    },
    "Input_downtime_eventHubName": {
      "type": "string"
    },
    "Input_downtime_sharedAccessPolicyName": {
      "type": "string"
    },
    "Input_downtime_sharedAccessPolicyKey": {
      "type": "string"
    },
    "Output_cosmosdb_accountId": {
      "type": "string"
    },
    "Output_cosmosdb_accountKey": {
      "type": "string"
    },
    "Output_cosmosdb_database": {
      "type": "string"
    },
    "Output_cosmosdb_collectionNamePattern": {
      "type": "string"
    }
  },
  "resources": [
    {
      "type": "Microsoft.StreamAnalytics/StreamingJobs",
      "apiVersion": "2015-10-01",
      "name": "[parameters('StreamAnalyticsJobName')]",
      "location": "[parameters('Location')]",
      "properties": {
        "outputStartMode": "[parameters('OutputStartMode')]",
        "outputStartTime": null,
        "sku": {
          "name": "standard"
        },
        "eventsOutOfOrderPolicy": "[parameters('EventsOutOfOrderPolicy')]",
        "outputErrorPolicy": "[parameters('OutputErrorPolicy')]",
        "eventsOutOfOrderMaxDelayInSeconds": "[parameters('EventsOutOfOrderMaxDelayInSeconds')]",
        "eventsLateArrivalMaxDelayInSeconds": "[parameters('EventsLateArrivalMaxDelayInSeconds')]",
        "dataLocale": "[parameters('DataLocale')]",
        "compatibilityLevel": "[parameters('CompatibilityLevel')]",
        "inputs": [
          {
            "name": "downtime",
            "properties": {
              "type": "Stream",
              "datasource": {
                "type": "Microsoft.ServiceBus/EventHub",
                "properties": {
                  "serviceBusNamespace": "[parameters('Input_downtime_serviceBusNamespace')]",
                  "eventHubName": "[parameters('Input_downtime_eventHubName')]",
                  "consumerGroupName": null,
                  "sharedAccessPolicyName": "[parameters('Input_downtime_sharedAccessPolicyName')]",
                  "sharedAccessPolicyKey": "[parameters('Input_downtime_sharedAccessPolicyKey')]"
                }
              },
              "compression": {
                "type": "None"
              },
              "serialization": {
                "type": "Json",
                "properties": {
                  "encoding": "UTF8"
                }
              }
            }
          }
        ],
        "outputs": [
          {
            "name": "cosmosdb",
            "properties": {
              "serialization": {
                "type": "Json",
                "properties": {
                  "encoding": "UTF8",
                  "format": "LineSeparated"
                }
              },
              "datasource": {
                "type": "Microsoft.Storage/DocumentDB",
                "properties": {
                  "accountId": "[parameters('Output_cosmosdb_accountId')]",
                  "accountKey": "[parameters('Output_cosmosdb_accountKey')]",
                  "database": "[parameters('Output_cosmosdb_database')]",
                  "collectionNamePattern": "[parameters('Output_cosmosdb_collectionNamePattern')]",
                  "partitionKey": null,
                  "documentId": null
                }
              }
            }
          }
        ],
        "transformation": {
          "name": "Transformation",
          "properties": {
            "streamingUnits": "[parameters('StreamingUnits')]",
            "query": "WITH SelectPreviousEvent AS\r\n(\r\nSELECT\r\n    TeamId,\r\n    System.TimeStamp As Time,\r\n    Count(*) as Count\r\nFROM\r\n    downtime TIMESTAMP BY Date\r\nGROUP BY\r\n  TeamId,\r\n  TumblingWindow(second, 1)\r\n)\r\n\r\nSELECT \r\n  TeamId,\r\n  System.Timestamp As Time,\r\n  Count(*) As Count\r\nINTO\r\n    [cosmosdb]\r\nFROM SelectPreviousEvent\r\nGroup BY\r\n  TeamId,\r\n  TumblingWindow(minute, 1)"
          }
        },
        "functions": []
      }
    }
  ]
}
```

## パラメータ

パラメータも吐いてくれます。私の固有のデータをはいているところは null 等に変更しましたが、基本的にキー以外は埋めてくれます。

```json
{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "StreamAnalyticsJobName": {
        "value": null
      },
      "Location": {
        "value": "Central US"
      },
      "OutputStartMode": {
        "value": "JobStartTime"
      },
      "DataLocale": {
        "value": "en-US"
      },
      "OutputErrorPolicy": {
        "value": "Drop"
      },
      "EventsLateArrivalMaxDelayInSeconds": {
        "value": 5
      },
      "EventsOutOfOrderMaxDelayInSeconds": {
        "value": 0
      },
      "EventsOutOfOrderPolicy": {
        "value": "Adjust"
      },
      "StreamingUnits": {
        "value": 1
      },
      "CompatibilityLevel": {
        "value": "1.1"
      },
      "Input_downtime_serviceBusNamespace": {
        "value": null
      },
      "Input_downtime_eventHubName": {
        "value": "downtime"
      },
      "Input_downtime_sharedAccessPolicyName": {
        "value": "RootManageSharedAccessKey"
      },
      "Input_downtime_sharedAccessPolicyKey": {
        "value": null
      },
      "Output_cosmosdb_accountId": {
        "value": null
      },
      "Output_cosmosdb_accountKey": {
        "value": null
      },
      "Output_cosmosdb_database": {
        "value": "leaderboard"
      },
      "Output_cosmosdb_collectionNamePattern": {
        "value": "DowntimeRecord"
      }
    }
  }
```

## ARM テンプレートを Azure CLI からコールする

さて、このパラメータの数をみるとうんざりするのではないでしょうか？これをパラメータとしてAzure CLI に渡すのは面倒すぎです。そのケースだと、デフォルト値とか、固定値はこのパラメータファイルを使って、足りないところだけをパラメータで渡せる様子です。次のような感じです。

```bash

az group deployment create --name StreamAnalyticsDeployment --resource-group $resourceGroupName --template-file stream_analytics_arm_template.json --parameters @stream_analytics_arm_parameter.json --parameters StreamAnalyticsJobName=$streamAnalyticsJobName Location=$location Input_downtime_serviceBusNamespace=$eventHubsNamespace Input_downtime_sharedAccessPolicyKey=$eventhubsKey Output_cosmosdb_accountId=$cosmosDBName Output_cosmosdb_accountKey=$cosmosdbKey
```

ちなみに、EventHubsとCosmos のシークレット取得は次のコードです。`$xxxx` は変数になっています。

```bash
echo -e "Retrieving cosmosdb connection string\n"
cosmosdbKey=$(az cosmosdb list-keys --name $cosmosDBName  --resource-group $resourceGroupName --query primaryMasterKey --output tsv)


echo -e "Retriving eventhubs primary key string\n"
eventhubsKey=$(az eventhubs namespace authorization-rule keys list --resource-group $resourceGroupName --namespace-name $eventHubsNamespace --name RootManageSharedAccessKey --query primaryKey --output tsv)


```

これで、デプロイも完成です。

## Stream Analytics Job起動

さて、最後の関門として、Stream Analytics の Job 起動の問題があります。PowerShell だとできますが、Azure CLI はサポートしていません。ただ、.Net SDK はサポートしていますので、.NET Core で起動、停止のコマンドを作って、それを呼ぶようにしようと思います。下記のパーサーとか有用そうです。


* [.NET Core 時代のコマンドライン引数解析](https://tnakamura.hatenablog.com/entry/2017/10/24/microsoft-extensions-commandlineutils)

あと、シングルバイナリになってくれればなぁ。
* [Any progress on .NET Core single file executable?](https://www.reddit.com/r/dotnet/comments/8fr9k8/any_progress_on_net_core_single_file_executable/)