Azure Web PubSub Service を試してみる

Web PubSub Service とは?

Azure のサービスで WebSocket を使える PubSub サービスが出てきました。

azure.microsoft.com

Azure SignalR Service is based on the SignalR library, which is widely used with the .NET platform. Azure Web PubSub can be used with any platform supporting WebSockets, and you can use it to build sophisticated applications.

ということで、Azure では PubSub サービスとしてこれまでも SignalR がありましたが、SignalR を使うための SDKAPI を理解する必要がありました。 この PubSub Service では WebSocket をそのまま利用できるため、各言語・フレームワークに用意されたライブラリ、自作のライブラリをそのまま使うことができます。

SKU、価格はこちら。Standard SKU では、ユニット数あたりの価格と、100 万メッセージを超えると追加の価格が発生します。

azure.microsoft.com

アーキテクチャーはこのページが分かりやすいです。

azure.github.io

f:id:mitsuki0820:20210430150930p:plain

早速こちらのチュートリアルを試してみたいと思います。

Quick start: publish and subscribe messages in Azure Web PubSub · Azure Web PubSub Service

試してみる

1. サービスを作る

[リソースの作成] で pubsub と検索すると出てくるのでこちらから作成します。

f:id:mitsuki0820:20210430113621p:plain

日本リージョンはまだです。

f:id:mitsuki0820:20210430113649p:plain

価格レベルは、Free と Standard からの選択です。

f:id:mitsuki0820:20210430113707p:plain

Free と Standard の違い

f:id:mitsuki0820:20210430113732p:plain

Free は開発用途なので SLA なし、スケールも無し、なので Standard を使いましょう。

Standard のユニット数は最大 100 までスケール出来るようです。 f:id:mitsuki0820:20210430115721p:plain

参考までに ARM テンプレート。プロバイダーは SignalRService になるんですね。

{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "location": {
            "type": "String"
        },
        "name": {
            "type": "String"
        },
        "skuName": {
            "type": "String"
        },
        "tier": {
            "type": "String"
        },
        "capacity": {
            "type": "Int"
        }
    },
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.SignalRService/WebPubSub",
            "apiVersion": "2021-04-01-preview",
            "name": "[parameters('name')]",
            "location": "[parameters('location')]",
            "dependsOn": [],
            "tags": {},
            "sku": {
                "name": "[parameters('skuName')]",
                "tier": "[parameters('tier')]",
                "capacity": "[parameters('capacity')]"
            },
            "properties": {}
        }
    ],
    "outputs": {}
}

リソースメニュー

f:id:mitsuki0820:20210430120337p:plain

キー

f:id:mitsuki0820:20210430120700p:plain

プロパティ。概要ページで確認できない DNS / パブリック IP が見えます。

f:id:mitsuki0820:20210430120816p:plain

スケール。あとから SKU、ユニット数は変えられるようです。

f:id:mitsuki0820:20210430120940p:plain

設定。イベントハンドラーの設定をできます。

f:id:mitsuki0820:20210430121236p:plain

メトリック

f:id:mitsuki0820:20210430121405p:plain

診断設定。リンクだけで、Live Trace Tool というサイトにジャンプします。あとでどうなるか見てみましょう。

f:id:mitsuki0820:20210430121448p:plain

f:id:mitsuki0820:20210430121517p:plain

2. サブスクライバー(サーバー側)

npm init -y
npm install --save ws
npm install --save @azure/web-pubsub

server.js

const WebSocket = require('ws');
const { WebPubSubServiceClient } = require('@azure/web-pubsub');

async function main() {
  if (process.argv.length !== 4) {
    console.log('Usage: node subscribe <connection-string> <hub-name>');
    return 1;
  }

  let serviceClient = new WebPubSubServiceClient(process.argv[2], process.argv[3]);
  let token = await serviceClient.getAuthenticationToken();
  let ws = new WebSocket(token.url);
  ws.on('open', () => console.log('connected'));
  ws.on('message', data => console.log(data));
}

main();

3. パブリッシャー(クライアント側)

client.js

const { WebPubSubServiceClient } = require('@azure/web-pubsub');

if (process.argv.length !== 5) {
  console.log('Usage: node publish <connection-string> <hub-name> <message>');
  return 1;
}

let serviceClient = new WebPubSubServiceClient(process.argv[2], process.argv[3]);

// by default it uses `application/json`, specify contentType as `text/plain` if you want plain-text
serviceClient.sendToAll(process.argv[4], { contentType: "text/plain" });

4. メッセージをクライアントから送信する

ターミナルを 2 つ起動して、Node.js を実行します。

サーバー側

$ node server.js "Endpoint=https://tsunomurpubsub.webpubsub.azure.com;AccessKey=*****=;Version=1.0;" tsunomurpubsub
connected

クライアント側

$ node client.js  "Endpoint=https://tsunomurpubsub.webpubsub.azure.com;AccessKey=*****=;Version=1.0;" tsunomurpubsub  hoge

そうすると、サーバー側でメッセージが確認できます。

$ node server.js "Endpoint=https://tsunomurpubsub.webpubsub.azure.com;AccessKey=*****=;Version=1.0;" tsunomurpubsub
connected
hogehoge

先ほどの Live Trace Tool を確認してみましょう。サーバーがコネクションを開始・終了したログが残ります。 f:id:mitsuki0820:20210430132102p:plain

まとめ

今回試した例ではクライアントからサーバーにメッセージを一方的に送り付けるだけの非常にシンプルな例でした。

これだけだと WebSocket 感があまりないのと、トークンの取り方、ロールの考え方、イベントハンドラー等、PubSub Service 特有の部分が含まれていないので、 このチュートリアル

azure.github.io

このチュートリアル

azure.github.io

試してみるとより理解が深まると思います。

GitHub のサンプルはこちらにまとまってます。

github.com

開発の幅が広がりますね!

AKS の監視機能 Azure Monitor for Containers の裏側を垣間見る

Azure のマネージド Kubernetes サービスである AKS には、Azure Monitor for Containers という監視機能があります。 ノードのパフォーマンス情報や Pod の標準出力ログ等を統合的に確認できるので非常に便利です。

今回はこの AKS と Azure Monitor for Containers がどのように連携しているかを見ていきます。
# ちなみに、ConfigMaps で一部設定をチューニングできますが今回の前提は既定値としています。

この記事に書いてあること

  • Azure Monitor for Containers を有効にすると、DaemonSet と Deployment が展開される
  • Pod のインベントリやノード情報等クラスタ全体の情報は Deployment が情報を取得
  • Pod ごとの情報は DaemonSet が情報を取得
  • Pod の標準出力は、DaemonSet がノードのログディレクトリをマウントして td-agent-bit で取得

f:id:mitsuki0820:20200613202059p:plain

そもそも Azure Monitor for Containers って?

Azure には、Azure Monitor というブランドでさまざまなリソースの監視が出来るような統合的な監視サービスが提供されています。

AKS に関してもこれで監視をすることになるのですが、特に AKS(コンテナ) 向けのものと位置づけられているのが、Azure Monitor for Containers です。

その開発コンセプトについては、US の開発チームの原田さんの記事が非常にわかりやすいのでぜひご参照ください。
qiita.com

こちらにも引用しておきます。

• Azureと言う広範囲な場所にAKSなどのサービス群を発見し、簡単に監視できること。
• 監視されていないAKSはあるのか?
クラウドサービスの傍らに監視がないといけない。監視ツールを探す手間が面倒。
• 簡単に問題が発見でき、監視ツールを覚える必要のないユーザー体験(UX)が必要。
• 監視ツールの管理や監視をしなくても良く、手間いらずで、自動的にメンテナンスやアップグレードをしてくれる。つまり、監視ツールにでなく、自社のビジネス向上だけに人件費をかけることが出来る。

原田さんの記事にもありますが、 Azure Monitor for Containers を使うと、次のような情報が取得できます。

  • API サーバーの正常性
  • ノードごとの CPU 使用率、メモリ使用状況
  • ノード数、Pod 数
  • Deployment の Ready や Up to date、Available の情報
  • コンテナの標準出力

さて、この監視機能の中身ですが、Log Analytics という Azure のエージェント型の SaaS 監視ソリューションが使われています。

Log Analytics と AKS の連携

この Log Analytics ですがどのようにコンテナの情報を集め、Azure Monitor for Containers にデータを送信しているのか、を調べてみます。

ノードに展開されているリソース

おもむろに kube-system に展開されているリソースを見てみると、こんなリソースがありました。

以下抜粋です。

$ k get all
NAME                                                     READY   STATUS    RESTARTS   AGE
pod/omsagent-62kxc                                       1/1     Running   1          2d3h
pod/omsagent-rs-66964c9f75-wn5hg                         1/1     Running   1          2d3h

NAME                         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                 AGE
daemonset.apps/omsagent      5         5         5       5            5           beta.kubernetes.io/os=linux   4d18h

NAME                                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/omsagent-rs                          1/1     1            1           2d3h

NAME                                                           DESIRED   CURRENT   READY   AGE
replicaset.apps/omsagent-rs-66964c9f75                         1         1         1       2d3h

以前から Azure を使っていると、この oms というキーワードにピンときます。OMS というのは、Azure Monitor の前身の様なものです。

というところで、恐らくこの Deployment と DaemonSet が何かしてるんだろうというあたりがつきます。

omsagent-rs の役割

まず、Deployment の omsagent-rs のシェルでプロセスを見てみましょう。

# ps auxwwww
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          1  0.0  0.0  18508  3008 ?        Ss   02:49   0:00 /bin/bash /opt/main.sh
root         29  0.0  0.0   6704   120 ?        S    02:49   0:00 inotifywait /etc/config/settings --daemon --recursive --outfile /opt/inotifyoutput.txt --event create,delete --format %e : %T --timefmt +%s
syslog      225  0.0  0.0 129672  2776 ?        Ssl  02:49   0:00 /usr/sbin/rsyslogd
omsagent    266  0.8  0.8 396948 63648 ?        Sl   02:49   2:02 /opt/microsoft/omsagent/ruby/bin/ruby /opt/microsoft/omsagent/bin/omsagent-6408bb34-6b60-4a40-b218-3ad8d671bd15 -d /var/opt/microsoft/omsagent/6408bb34-6b60-4a40-b218-3ad8d671bd15/run/omsagent.pid --no-supervisor -o /var/opt/microsoft/omsagent/6408bb34-6b60-4a40-b218-3ad8d671bd15/log/omsagent.log -c /etc/opt/microsoft/omsagent/6408bb34-6b60-4a40-b218-3ad8d671bd15/conf/omsagent.conf
root        314  0.0  0.0  28356  2636 ?        Ss   02:49   0:00 /usr/sbin/cron
root        340  0.0  0.5 148080 42300 ?        Sl   02:50   0:01 /opt/td-agent-bit/bin/td-agent-bit -c /etc/opt/microsoft/docker-cimprov/td-agent-bit-rs.conf -e /opt/td-agent-bit/bin/out_oms.so
root        346  0.0  0.5 215480 39520 ?        Sl   02:50   0:05 /opt/telegraf --config /etc/opt/microsoft/docker-cimprov/telegraf-rs.conf
root        440  0.0  0.0   4536   764 ?        S    02:50   0:00 sleep inf
root      11637  0.0  0.0  18624  3548 pts/0    Ss   06:15   0:00 bash

そうすると、2つの td-agent が立ち上がっています。

/opt/microsoft/omsagent/ruby/bin/ruby /opt/microsoft/omsagent/bin/omsagent-6408bb34-6b60-4a40-b218-3ad8d671bd15
/opt/td-agent-bit/bin/td-agent-bit

※1 つ目のプロセスって td-agent なの?と思われるかもしれませんが、実は、Azure の Log Analytics のエージェントの実態は、td-agent だったりします。そしてプロセス名の後ろの GUID は、omsagent を識別するための ID になっています。

ちなみに、td-agent-bit では、CPU 使用率を stdout に吐いているだけの様なので割愛します。

td-agent の1つ目: omsagent-**

では、omsagent-** の設定を見てみます。設定は、/etc/opt/microsoft/omsagent//conf/omsagent.conf なのですが、ファイルをインクルードしており、特に AKS に関わりがありそうな設定は /etc/opt/microsoft/omsagent/conf/omsagent.d/container.conf の様です。

とりあえず、source の type だけ見てみるとこんな感じでした。(これも Linux AKS に関係ありそうなものに絞ってます)

kubepodinventory
kubeevents
kubenodeinventory
kubehealth

このキーワードどこかで見たことないでしょうか?



実はこちら、Log Analytics のログから確認できる、テーブルの名前と同じものがあります。
f:id:mitsuki0820:20200613191200p:plain

※上記4つのtype以外に存在するテーブルとして、KubeServices、ContainerNodeInventory、KubeMonAgentEvents がありますが、KubeServices に関しては、kubepodinventory と同じスクリプト、ContainerNodeInventoryに関しては kubenodeinventory で取得されています。KubeMonAgentEvents は出所が分かりませんでしたが、恐らく omsagent-rs から送信されているものと思われます。

さらに、これらのソースとなるものがどのように定義されているのかを見てみます。おもむろに、 /opt/microsoft/omsagent/plugin/* を grep してみます。

# egrep -Ir "kubepodinventory|kubeevents|kubenodeinventory|kubehealth" /opt/microsoft/omsagent/plugin/*  | grep Plugin.reg
/opt/microsoft/omsagent/plugin/in_kube_events.rb:    Plugin.register_input("kubeevents", self)
/opt/microsoft/omsagent/plugin/in_kube_health.rb:    Plugin.register_input("kubehealth", self)
/opt/microsoft/omsagent/plugin/in_kube_nodes.rb:    Plugin.register_input("kubenodeinventory", self)
/opt/microsoft/omsagent/plugin/in_kube_podinventory.rb:    Plugin.register_input("kubepodinventory", self)

この中の一つ、in_kube_podinventory.rb を見てみましょう。

# Initializing continuation token to nil
continuationToken = nil
$log.info("in_kube_podinventory::enumerate : Getting pods from Kube API @ #{Time.now.utc.iso8601}")
continuationToken, podInventory = KubernetesApiClient.getResourcesAndContinuationToken("pods?limit=#{@PODS_CHUNK_SIZE}")
$log.info("in_kube_podinventory::enumerate : Done getting pods from Kube API @ #{Time.now.utc.iso8601}")

雰囲気ですがなんとなくこの in_kube_podinventory.rb の中で Pod 周りの情報を取得している、ということが分かりました。

最後に、もう一度設定ファイルに戻って、これらのログをどこに送っているか見てみましょう。

<match oms.containerinsights.KubeNodeInventory**>
 type out_oms
 log_level debug
 num_threads 5
 buffer_chunk_limit 4m
 buffer_type file
 buffer_path /var/opt/microsoft/omsagent/6408bb34-6b60-4a40-b218-3ad8d671bd15/state/state/out_oms_kubenodes*.buffer
 buffer_queue_limit 20
 buffer_queue_full_action drop_oldest_chunk
 flush_interval 20s
 retry_limit 10
 retry_wait 5s
 max_retry_wait 5m
</match>

大体想像がつきますが、out_oms プラグインを使って Log Analytics にデータを送り付けていそうですね。
プラグインの実態はこちら。

/opt/microsoft/omsagent/plugin/out_oms.rb

プラグインから参照している設定ファイルはこちら。なんかそれっぽい設定がありますね。

root@omsagent-rs-66964c9f75-wn5hg:/etc/opt/microsoft/omsagent/conf/omsagent.d# cat /etc/opt/microsoft/omsagent/conf/omsadmin.conf
WORKSPACE_ID=6408bb34-6b60-4a40-b218-3ad8d671bd15
AGENT_GUID=e5fe9077-2ee1-4483-87ca-be35a694bd2e
LOG_FACILITY=local0
CERTIFICATE_UPDATE_ENDPOINT=https://6408bb34-6b60-4a40-b218-3ad8d671bd15.oms.opinsights.azure.com/ConfigurationService.Svc/RenewCertificate
URL_TLD=opinsights.azure.com
DSC_ENDPOINT=https://6408bb34-6b60-4a40-b218-3ad8d671bd15.agentsvc.azure-automation.net/Accounts/6408bb34-6b60-4a40-b218-3ad8d671bd15/Nodes\(AgentId='e5fe9077-2ee1-4483-87ca-be35a694bd2e'\)
OMS_ENDPOINT=https://6408bb34-6b60-4a40-b218-3ad8d671bd15.ods.opinsights.azure.com/OperationalData.svc/PostJsonDataItems
AZURE_RESOURCE_ID=
OMSCLOUD_ID=7783-7084-3265-9085-8269-3286-77
UUID=0255E095-271E-9F4A-A4A1-D1C3EDC4B0AF
root@omsagent-rs-66964c9f75-wn5hg:/etc/opt/microsoft/omsagent/conf/omsagent.d# fg
view /opt/microsoft/omsagent/plugin/out_oms.rb

ひとまずここまで omsagent-rs が何をやっているか、ということをまとめると、Pod や ノードのインベントリー、イベント、正常性を取得して、Log Analytics へ送信していることが分かりました。

omsagent の役割

では続いて同じようなステップで DaemonSet である omsagent を見てみましょう。同じように ps してみます。

# ps auxwwww
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          1  0.0  0.0  18504  3140 ?        Ss   02:49   0:00 /bin/bash /opt/main.sh
root         29  0.0  0.0   6704   124 ?        S    02:49   0:00 inotifywait /etc/config/settings --daemon --recursive --outfile /opt/inotifyoutput.txt --event create,delete --format %e : %T --timefmt +%s
syslog      227  0.0  0.0 129672  3124 ?        Ssl  02:50   0:00 /usr/sbin/rsyslogd
omsagent    268  0.4  0.6 317832 46796 ?        Sl   02:50   1:43 /opt/microsoft/omsagent/ruby/bin/ruby /opt/microsoft/omsagent/bin/omsagent-6408bb34-6b60-4a40-b218-3ad8d671bd15 -d /var/opt/microsoft/omsagent/6408bb34-6b60-4a40-b218-3ad8d671bd15/run/omsagent.pid --no-supervisor -o /var/opt/microsoft/omsagent/6408bb34-6b60-4a40-b218-3ad8d671bd15/log/omsagent.log -c /etc/opt/microsoft/omsagent/6408bb34-6b60-4a40-b218-3ad8d671bd15/conf/omsagent.conf
root        297  0.0  0.0  28356  2632 ?        Ss   02:50   0:00 /usr/sbin/cron
root        339  0.0  0.6 160440 49044 ?        Sl   02:50   0:03 /opt/td-agent-bit/bin/td-agent-bit -c /etc/opt/microsoft/docker-cimprov/td-agent-bit.conf -e /opt/td-agent-bit/bin/out_oms.so
root        361  0.0  0.6 203316 48108 ?        Sl   02:50   0:12 /opt/telegraf --config /etc/opt/microsoft/docker-cimprov/telegraf.conf
root        409  0.0  0.0   4536   784 ?        S    02:50   0:00 sleep inf
root      15617  0.0  0.0  18624  3448 pts/0    Ss   04:54   0:00 bash
root      46957  0.0  0.0  34408  2800 pts/0    R+   09:05   0:00 ps auxwwww

こちらでも、2つの td-agent が動いてますね。

/opt/microsoft/omsagent/ruby/bin/ruby /opt/microsoft/omsagent/bin/omsagent-6408bb34-6b60-4a40-b218-3ad8d671bd15
/opt/td-agent-bit/bin/td-agent-bit
td-agent の1つ目: omsagent-**

設定ファイルのパスは同じなので、source の type から見ていきます。

containerinventory
cadvisorperf

Deployment の omsagent-rs と違ってますね。
これも名前から大体想像がつきますが、コンテナのインベントリと、cAdvisor によるパフォーマンスを取得しているようです。

先ほどと同じように、Log Analytics のテーブルと照らし合わせてみます。
f:id:mitsuki0820:20200613192543p:plain

まず、containerinventory はそのまま、td-agent の source です。

次に、cadvisorperf がどこに行ってしまったかというと、LogManagement の Perf テーブルに送信されているようです。これは、プラグインの in_cadvisor_perf.rb を見てみると、以下の様なコードがありました。

record["DataType"] = "LINUX_PERF_BLOB"
record["IPName"] = "LogManagement"
eventStream.add(time, record) if record

恐らくですが、LINUX_PERF_BLOB というのはコンテナに関係なく、Linux のパフォーマンス情報を格納するための LogManagement 配下の Perf のことを指していると思われます。このテーブルは、Windows / Linux 共通で、Log Analytics から取得されてきたパフォーマンス情報を格納するテーブルになるので、同じ仕組みを AKS のコンテナに対しても使っているようです。

そして実はこの Perf テーブルですが、omsagent-rs から、ノードレベルの情報を取得しているようです。そういう意味で、色を紫にしています
また、InsightsMetrics も同様にコンテナ個々の情報、ノード情報をそれぞれ omsaget、omsagent-rs から情報を取得しているようです。

次に、ContainerImageInventory は、filter_container.rb というフィルタープラグインの中で識別されていました。

dataType = case r["ClassName"]
        when "Container_ImageInventory" then "CONTAINER_IMAGE_INVENTORY_BLOB"
        when "Container_ContainerInventory" then "CONTAINER_INVENTORY_BLOB"
        when "Container_DaemonEvent" then "CONTAINER_SERVICE_LOG_BLOB"
        when "Container_ContainerLog" then "CONTAINER_LOG_BLOB"
end

一旦ここまででまとめると、DaemonSet で展開されている omsagent は、ノード上にあるコンテナ特有の情報を取得して、Log Analytics へ送信していることが分かりました。

コンテナの標準出力は?

さて、ここで、Azure Monitor for Containers では、コンテナの標準出力も収集されると最初に書きました。
それはどのように収集されてるのでしょうか?

答えは、この DaemonSet の omsagent の td-agent-bit です。設定ファイルを見てみましょう。

# cat /etc/opt/microsoft/docker-cimprov/td-agent-bit.conf
[INPUT]
    Name tail
    Tag oms.container.log.la.*
    Path ${AZMON_LOG_TAIL_PATH}
# env | grep AZMON_LOG_TAIL_PATH
AZMON_LOG_TAIL_PATH=/var/log/containers/*.log

td-agent-bit の INPUT を見ると、AZMON_LOG_TAIL_PATH にあるファイルを tail しているようです。そして、/var/log は、ノードのディレクトリをマウントしていることで、各ノードのコンテナの標準出力をキャッチできる、というわけです。

volumeMounts:
    - mountPath: /var/log
      name: host-log
volumes:
  - hostPath:
      path: /var/log
      type: ""
    name: host-log

まとめ

今回は Log Analytics に関する Pod がどのような動きをしているのかをざっと調べてみました。

今回かいていない内容として、それぞれのプラグインの処理の詳細、omsagent の ConfigMaps によるカスタマイズの方法、telegraf によるメトリクスの取得など、Deep Dive するとどこまでも行けそうなので引き続き Azure Monitor for Container ネタを書いていきたいと思います。

Resource Graph API で Azure のリソース情報を取得する

サポートという仕事柄、お客様の Azure 環境の情報を頂いてレビューしたりトラシューしたりするのですが、Azure の情報をとってくる方法というのは中々厄介です。

一応社内ツールもあるものの、いろいろな制約があったりお客様と別の見え方をしたりするので、一番簡単かつ確実な方法はポータルのアクセス権をもらうことなのですが、それはそれでまた別のハードルがあります。

ということで、よさげなツールがないかと探していたもののどうやらなさそうだったので自分で作ってみました。

github.com

一番よく使いそうなパターンとしては、こちらで取得してほしいクエリを YAML として提供し、お客様側でこのツールを叩いてもらうような状況を想定してます。

使い方等詳細は、README を読んで頂くとして、今回はこのツールをベースに、Azure SDK for Go と Resource Graph API を使ってどうやって Azure のリソース情報を取得してくるかを紹介していきます。

ちなみに結構ググったのですが、Resource Graph API 周りの公式以外の記事は英語でもなさそうだったのでもしかしたら世界初。。かもしれません。

Azure SDK for Go

まず、このツールの動作環境と処理内容から、マルチ OS と並行処理は外せないなということを最初に考えたので、Go 言語で実装することにしました。幸い、Azure には Go 言語用の SDKがあるので REST API を叩かずに実装できます。

ドキュメントはこちら。

Go 開発者向けの Azure | Microsoft Docs

GoDoc はこちらにあります。

sdk - GoDoc

認証ってどうするの?

Azure SDK for Go では、環境に応じて使えるさまざまな認証方法があります。

詳しくはこちらのドキュメントを見てください。

Azure SDK for Go での認証 | Microsoft Docs

今回は、CLI ツールなので、

のいずれかを採用することにしました。(先ほどのツールはまだ Azure CLI の認証情報のみです)

こういうツールを作る時、結構悩ましいのは認証で、特に Azure の閲覧権限しかないとサービスプリンシパルが作れなかったりするので、代替方法があるのは非常に助かりますね。

今回のツールだとこの部分

https://github.com/tsubasaxZZZ/azr/blob/90cef9938e59e9be0d69241c79c8c5facae94064/azure.go#L29

a, err := auth.NewAuthorizerFromCLI()

予め Azure CLI のインストールと az login でサインインしておく必要がありますが、なんとこれだけで認証できます。

あとは、各 API に NewなんとかClient というメソッドが用意されているのでその戻り値の構造体の Authorizer に認証情報をセットしてあげれば準備は完了。

ちなみにこのあたりの流れは GitHub の README に載ってます。

GitHub - Azure/azure-sdk-for-go: Microsoft Azure SDK for Go

1. Import a package from the services directory.
2. Create and authenticate a client with a New*Client func, e.g. c := compute.NewVirtualMachinesClient(...).
3. Invoke API methods using the client, e.g. res, err := c.CreateOrUpdate(...).
4. Handle responses and errors.

初めはお作法が分からず途方に暮れてましたがちゃんと書いてあったのでドキュメントはちゃんと読みましょうということですね。。

Resource Graph API

続いて、Resource Graph API を叩く部分です。SDK ではこちらになります。

resourcegraph - GoDoc

あとドキュメントで欠かせないのがこちらの API リファレンス。

Resources - Resources (Azure Azure Resource Graph) | Microsoft Docs

開発中はこの2つのドキュメントとにらめっこしながら実装することになりますが、ヒントとなりそうなあたりをいくつか紹介します。

どーやってリクエストするの?

こちらのメソッドを使います。

Resources

https://godoc.org/github.com/Azure/azure-sdk-for-go/services/resourcegraph/mgmt/2019-04-01/resourcegraph#BaseClient.Resources

今回のツールでは azure.go のこの部分。

request := &resourcegraph.QueryRequest{
    Subscriptions: &[]string{params.subscriptionID},
    Query:         &params.query,
    Options:       &resourcegraph.QueryRequestOptions{ResultFormat: resourcegraph.ResultFormatTable},
    Facets:        &facetRequest,
}
queryResponse, err := client.ResourceGraphClient.Resources(c, *request)
if err != nil {
    return nil, err
}

始めに、QueryRequest 構造体を組み立てて、resourcegraphapi.BaseClientAPI の第二引数として渡してあげると、QueryResponse が返ってきます。

QueryResponse

https://godoc.org/github.com/Azure/azure-sdk-for-go/services/resourcegraph/mgmt/2019-04-01/resourcegraph#QueryResponse

まず、 QueryRequest にリソース グラフのクエリや サブスクリプション ID やらをセットしてあげます。この時、一緒に設定できる、QueryRequestOptions が地味に重要な働きをします。

QueryRequestOptions

// QueryRequestOptions the options for query evaluation
type QueryRequestOptions struct {
    // SkipToken - Continuation token for pagination, capturing the next page size and offset, as well as the context of the query.
    SkipToken *string `json:"$skipToken,omitempty"`
    // Top - The maximum number of rows that the query should return. Overrides the page size when ```$skipToken``` property is present.
    Top *int32 `json:"$top,omitempty"`
    // Skip - The number of rows to skip from the beginning of the results. Overrides the next page offset when ```$skipToken``` property is present.
    Skip *int32 `json:"$skip,omitempty"`
    // ResultFormat - Defines in which format query result returned. Possible values include: 'ResultFormatTable', 'ResultFormatObjectArray'
    ResultFormat ResultFormat `json:"resultFormat,omitempty"`
}

このオプションの中で、ResultFormat があります。セットできるのは、ResultFormatTable か、ResultFormatObjectArray です。前者は、Column と Row が map と array の形式で表現され、後者は、列名が map のキーになります。どちらを使うかはその時々に応じて、になりますが、データの型に応じて処理をしたい時は、ResultFormatTableがおすすめでしょうか。

というのは、Column に型の情報(integerか、stringか、objectか)が入ってくるのでそれに応じて処理を変えられます。

このツールでも、object の場合は、json に Marshal しています。

for i, r := range row.([]interface{}) {
    // カラムの型に応じて処理を変える
    switch columns.([]interface{})[i].(map[string]interface{})["type"] {
    case "integer", "string":
        _result = append(_result, fmt.Sprint(r))
    case "object": // object の場合は JSON 化
        j, err := json.Marshal(r)
        if err != nil {
            return nil, err
        }
        _result = append(_result, string(j))
    }
}

ResultFormatObjectArray は、決まった型の構造体にデータを入れたいときに便利だと思います。

for _, elem := range queryResponse.Data.([]interface{}) {
    b, err := json.Marshal(elem)
    if err != nil {
        return nil, err
    }
    r := reflect.New(reflect.TypeOf(v).Elem()).Interface()
    errUnmarshal := json.Unmarshal(b, &r)
    if errUnmarshal != nil {
        return nil, errUnmarshal
    }
    result = append(result, r)
}
return result, nil

この方法がベストかどうかがわからないのですが、今回のツールとは別の上のコードでは、v という構造体を別に作って、その情報を reflect して 一旦 Marshal した json を Unmarshal することで、カラム数も型も不定形な QueryResponse.Data を型の決まった構造体に突っ込んでます。

この ResultFormat を説明したドキュメントを見つけました。

https://docs.microsoft.com/ja-jp/azure/governance/resource-graph/concepts/work-with-data#formatting-results

Facet って何よ?

QueryRequest に Facets というメンバがいます。

そのままの意味としては切り口とか面とかの意味で、返ってきた結果を切り出して、並び替えたりトップ3を出したり等サマライズするカラムとか条件を指定することが出来ます。

こちらの API リファレンスが参考になるでしょう。

https://docs.microsoft.com/ja-jp/rest/api/azureresourcegraph/resourcegraph(2019-04-01)/resources/resources#query-with-a-facet-request

  "facets": [
~~略~~
    {
      "expression": "resourceGroup",
      "options": {
        "filter": "resourceGroup contains 'test'",
        "$top": 3
      }
    }

一部を抜き出したものですが、リクエストの Body で query とは別にこのように facets オプションを指定することで、返ってきた結果に対して、さらにサマライズできます。

重要なのは、QueryResponse とは別の結果として返ってくる点です。リソース グラフのクエリで返ってきた結果に対してさらにフィルターをするようなイメージです。

例えば、特定のリソース グループの中のリソースを全取得するというクエリに対して、上の様に test という文字列を含んだ仮想マシンのみを返すとか、仮想マシンにアタッチされたディスク数でソートした結果を返すとか共通のフィルターを掛ける時に便利かなと思います。

SDK のコードだとこのあたりです。

https://github.com/Azure/azure-sdk-for-go/blob/0aac01c260f21d958b4cf0b67e7dd231212e7a56/services/resourcegraph/mgmt/2019-04-01/resourcegraph/models.go#L457

API が分かっていれば単純に構造体に落としてるだけなので難しくはないでしょう。

ちなみに、このツールでは facet 周りはまだ実装していません。

結果って何個リソースがあっても無限数返ってくるの?

あらゆるリソースは有限です。

大体の一般的な API がそうであるように、Resource Graph API もスロットリングと最大レコード数があります。

いい感じのドキュメントがまとまってます。

スロットルされた要求に関するガイダンス - Azure Resource Graph | Microsoft Docs

こちらのスロットリングですが、5 秒間のウィンドウで最大 15 ということなので、調子に乗ってゴルーチンを 100 とかにするとエラーになる可能性が高くなります。 ツールへの影響だけならまだしも、Resource Graph API を使っている、Azure ポータルの表示にも影響が出そうなのでちゃんと制限してあげた方が良さそうです。(実際影響が出るかは未確認です)

次に、最大のレコード数です。

大規模なデータ セットを処理する - Azure Resource Graph | Microsoft Docs

API 経由でのアクセスは、最大 1000 レコードの上限があります。それなりの規模で、resources とかクエリしたら簡単に超えそうですね。

なので次のドキュメントにあるように、$skipToken にセットされたトークンを使って、ページング処理をしてあげる必要があります。

https://docs.microsoft.com/ja-jp/rest/api/azureresourcegraph/resourcegraph(2019-04-01)/resources/resources#next-page-query

まぁそもそもリソース グラフのクエリを使って何かをしようとしている状況で 1000 を超える結果が返ってくる状態はクエリがいけてないか、目的にマッチしていない可能性もあるんじゃないかなとも思います。

ちなみに今回のこのツールではページング処理は実装していないですが、対応する予定です。

まとめ

ということで、Resource Graph API と Azure SDK for Go についてつらつら書いてみました。

Go ネタとして、

  • インターフェースの reflect
  • GitHub Action & goreleaser
  • CLI のオプションを処理するurfave/cli/v2 周り

とか書きたいことがいろいろ出てきたので追って書いてきます。

またこのツールについては引き続きアップデートしてどんどん使いやすくしていく予定なので、ぜひ issue お待ちしています!

Azure Kubernetes Service(AKS)を勉強するときのまとめ

これまでなんとなく触ってきた AKS ですが、k8s も含めてちゃんとお勉強しようと思い、ドキュメントをまとめました。
AKS まわりを効率的に勉強できるかと思います。

書籍

これまでいろんな本を買ってつまみ食い的に読んできましたが、1冊ちゃんと読もうと思ってこちらをこのお正月休みに読み切りました。

k8s の一般的な内容ももちろんあるので Azure 以外のクラウドを使っていても参考になりますが、今回は特に AKS にフォーカスしていたので非常に参考になりました。あと、機能的な部分以外、可用性や保守性の観点でもあるので実践という点でも参考になります。
k8s の多くの機能を網羅的に学習するというより、設計原則や運用面を含めて、ざっくりと理解できるので時間がない時にもおすすめ。原則が分かれば他の機能や細かい点は公式ドキュメントで分かりますしね。

真壁さんの GitHub にサンプルコードがあり、手を動かしながら進められます。
github.com

terraform 部分に関しては、最新の v0.12 では破壊的な変更があるので、そのまま動かそうとするなら README にある通り、v0.11 を使う必要があります。

若干誤記があったりサンプルコードのディレクトリ階層が違ってたりが多めな気もしますが、そこはきっと試練としてあえて残してあるんでしょう(違
※この本だけの話しではないですが、後からの読者のためにエラッタを読者が編集&共有できるサービスがあったらいいのになぁとふと思うなどしました。

公式ドキュメント

今回は AKS にフォーカスしているので、何はともあれ公式ドキュメント。
docs.microsoft.com

特にベストプラクティスのページと FAQ のページは必読。
docs.microsoft.com
docs.microsoft.com

Azure Monitor 周りはこちら。
docs.microsoft.com

MS 陣による記事

Ignite 2019 での Mohammad Nofal さんによるベストプラクティス紹介。
https://myignite.techcommunity.microsoft.com/sessions/81598
こちらにサンプルコードも含めた GitHub があります。
github.com


そして↑を日本語でまとめられた記事。
medium.com


真壁さんにてまとめられているべスプラ&最新情報。

www.slideshare.net

MS の CSE 中村さん と、私と同じロールの @komiyasa1110 が書いている Qiita。実際に環境を作って試されている過程が書かれているので読むだけでもイメージできると思います。
qiita.com
qiita.com

そのほか

inductor さんにて翻訳された Kubernetes The Hard Way。
github.com

kubernetes のコードリーディングに役立ちそうなドキュメント類。kuberctl のコードリーディングをしてみたので私もまとめてみよう。
speakerdeck.com
speakerdeck.com
github.com

Dapr まわり

github.com
yonehub.y10e.com

KEDA まわり

github.com
docs.microsoft.com
blog.okazuki.jp
tsukatoh.hatenablog.com





引き続きアップデートしてきます。

Visual Studio Online の環境をカスタムする

2代目 Visual Studio Online(以下 VSO) がパブリックプレビューになりました。

機能やクイックスタートは id:okazuki さんが記事を書かれているのでこちらから。
blog.okazuki.jp

この記事では、VSO でオレオレ開発環境を作る方法をご紹介します。

簡単に説明すると、Dockerfile を使って開発環境を作ることができるというものです。ざっくりとイメージ。
f:id:mitsuki0820:20191105140751p:plain

それでは簡単に流れを見ていきましょう

1-1. VSO の設定

VSO は、Environment の作成時に、GitHubリポジトリを指定することができます。
f:id:mitsuki0820:20191105133045p:plain

この時に、

  • repository-root}/.devcontainer.json

もしくは

  • {repository-root}/.devcontainer/devcontainer.json

に設定ファイルを置いておくと、拡張やエディタ設定、フォワードポートを、展開時に自動的に設定してくれます。

ここで、"dockerfile" オプションに、Dockerfile のパス(.devcontainer.json からの相対パス)を指定すると、VSO が docker build してコンテナを作ってくれるという仕組みです。

ということで、.devcontainer.json を書いてみましょう。

※2019/12/1 追記:
以下の様なエラーになってしまうため -u, vscodeコメントアウトを外しました。

COMMAND: docker start vsonline_5e523a68-be25-4086-b1a9-1ed8fe5cad4f
Error: failed to start containers: vsonline_5e523a68-be25-4086-b1a9-1ed8fe5cad4f
Error response from daemon: linux spec user: unable to find user vsonline: no matching entries in passwd file
{
	"name": "Azure CLI",
	"dockerfile": "Dockerfile",
	//"image": "nginx",
	
	// The optional 'runArgs' property can be used to specify additional runtime arguments.
	"runArgs": [
		// Uncomment the line if you will use a ptrace-based debugger like C++, Go, and Rust.
		// "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined",

		// Uncomment the next line to use a non-root user. On Linux, this will prevent
		// new files getting created as root, but you may need to update the USER_UID
		// and USER_GID in .devcontainer/Dockerfile to match your user if not 1000.
		"-u", "vscode"
	],

	// Use 'settings' to set *default* container specific settings.json values on container create. 
	// You can edit these settings after create using File > Preferences > Settings > Remote.
	"settings": { 
		"terminal.integrated.shell.linux": "/bin/bash"
	},

	// Uncomment the next line if you want to publish any ports.
	// "appPort": [],

	// Uncomment the next line to run commands after the container is created.
	"postCreateCommand": "az --version",

	// Add the IDs of extensions you want installed when the container is created in the array below.
	"extensions": [
		"ms-vscode.azurecli"
	]
	
}

書き方が分からないときは、実は、こちらのリポジトリに一通りありますのでここからコピーしてきます。
github.com

先ほどの .devcontainer.json も、Azure CLI のテンプレートから持ってきました。

1-2. Dockerfile の作成

次に Dockerfile を作ります。といってもこれもテンプレからそのままです。

#-------------------------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
#-------------------------------------------------------------------------------------------------------------

FROM debian:9

# Avoid warnings by switching to noninteractive
ENV DEBIAN_FRONTEND=noninteractive

# This Dockerfile adds a non-root 'vscode' user with sudo access. However, for Linux,
# this user's GID/UID must match your local user UID/GID to avoid permission issues
# with bind mounts. Update USER_UID / USER_GID if yours is not 1000. See
# https://aka.ms/vscode-remote/containers/non-root-user for details.
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

# Configure apt and install packages
RUN apt-get update \
    && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \
    #
    # Verify git, process tools installed
    && apt-get -y install git iproute2 procps \
    #
    # Install the Azure CLI
    && apt-get install -y apt-transport-https curl gnupg2 lsb-release \
    && echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list \
    && curl -sL https://packages.microsoft.com/keys/microsoft.asc | apt-key add - 2>/dev/null \
    && apt-get update \
    && apt-get install -y azure-cli \
    #
    # Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user.
    && groupadd --gid $USER_GID $USERNAME \
    && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \
    # [Optional] Add sudo support for the non-root user
    && apt-get install -y sudo \
    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\
    && chmod 0440 /etc/sudoers.d/$USERNAME \
    #
    # Clean up
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*

# Switch back to dialog for any ad-hoc use of apt-get
ENV DEBIAN_FRONTEND=

ここまでで、一旦 GitHub にプルしておきます。

参考までに、こちらの私のリポジトリを置いておきます。
github.com

2. オプトインの設定

ちょっと分かり辛いのがこちら。ドキュメントにもありますが、この機能は試験的な機能なのでオプトインで有効化してあげる必要があります。
docs.microsoft.com

.cloudenv-settings.json の部分ですね。

重要なのはこれを、VSCode を実行しているローカルの端末上に作る必要があるということです。
# どうやら今のところ VSO 上で作る方法は無いようです。

なので、Windows であれば、%USERPROFILE% 、Linux であれば、$HOME に以下の記述がされた .cloudenv-settings.json を配置します。

{
    "isInternal": true,
    "experimentalFeatures": {
        "customContainers": true
    }
}

3. Environment の作成

ここまで来たら、あとは Environment を作るだけです。

1. 作成

f:id:mitsuki0820:20191105135303p:plain

2. Custom Setting

f:id:mitsuki0820:20191105135327p:plain

3. リポジトリの指定

f:id:mitsuki0820:20191105135356p:plain

4. 名前を入れて

f:id:mitsuki0820:20191105135517p:plain

5. SKU を決めて

f:id:mitsuki0820:20191105135540p:plain

6. Suspend を決めると

f:id:mitsuki0820:20191105135620p:plain

7. docker build が始まります

f:id:mitsuki0820:20191105135801p:plain

VSO 上から見ると Creating 状態になってます。
f:id:mitsuki0820:20191105135957p:plain

4. 完成

ということで、az コマンドの入った VSO 環境が出来上がりました。
f:id:mitsuki0820:20191105140305p:plain


PC を変えたあとのセットアップの時間短縮や言語によって環境を変えたいとき、他の人と環境を共有したいときに使えそうです。あとは Linux VM を度々セットアップするのが面倒なときとかも。

これは夢が広がる機能ですね!

Front Door のバックエンドを冗長化するときの設定

最近個人的に Front Door が流行っていますがいろいろ考えないといけないポイントがあるので先日に続きメモしていきます。

まず前提としてこちらの構成がありますのではじめにお読みください。
tsunomur.hateblo.jp

要件としては以下があります。

  1. Front Door のバックエンドに Web Apps が存在する
  2. Web Apps は東西日本にデプロイされており、普段は片方がプライマリになっている
  3. Web Apps は直接アクセスさせず、特定の Front Door を必ず経由させたい
    (*.azurewebsites.net にアクセスさせたくない)

上記を実現するために(特に 3 番目)、Web Apps 側の .htaccess で以下の様な設定をしています。

SetEnvIf X-Forwarded-Host "www\.contoso\.com" allowed_fd <---------- 特定の Front Door のホスト名を指定する
Order Deny,Allow
Deny from all
Allow from env=allowed_fd

この時、東西の冗長構成にしていると(+優先度に差をつけていると)、片方はプローブのトラフィックのみが発生することになります。

ここで発覚した問題として、東西の切り替わりが頻繁に発生するというものでした。

結論としては、プローブのリクエストヘッダーには X-Forwarded-Host が含まれないために、403 が返ってしまい、シーソー状態になっているという問題でした。
※ちなみに、バックエンドの状態は、メトリックの Backend Health Percentage で確認できます。

ではどうするかというと、プローブのリクエストには、X-FD-HealthProbe というヘッダーが設定されるので、そのヘッダーの場合も allow する .htaccess を書いてあげると想定通りの動作になりました。

こんな感じ。

SetEnvIf X-Forwarded-Host "www\.contoso\.com" allowed_fd
SetEnvIf X-FD-HealthProbe "1" allowed_fd <-------------------- これ
Order Deny,Allow
Deny from all
Allow from env=allowed_fd

以前はこんなことなかった気がするんですが気のせいだったのかなぁ。。とか思ったり、その他いろいろハマりつつも新しめのサービスは触ってて面白いですね。

Japan Tour in Summer: Java & k8s on Azure まつり に参加してきたのでその感想

寺田佳央さんが主宰する Japan Tour in Summer: Java & k8s on Azure まつり に参加してきましたのでその感想を書いてみます。
# 具体的な内容とかは別の場所で開催された際の他の方のブログがありましたので最後にリンクを置いておきます。

connpass
jazug.connpass.com

connpass にもある通り、2 日間で k8s の基礎操作からアプリのデプロイ、CI/CD までの流れを試してみようというイベントです。
ちなみに場所は福岡で関東からだと結構距離はあるのですが、週休三日のお休みも使いつつ、博多への旅行がてら参加してみました。
また最近よく AKS の案件の話しが来ることもあり一人でモクモクやってたのですが、疑問な点も出てきたし聞けちゃえるかなという期待もありました。

1 日目は、寺田さんが用意された↓のアプリケーションをモブプロ形式で AKS 上へデプロイしていきます。
github.com

今回 k8s 経験者があまりいなかったということもありチーム形式ではなく、一人ずつ前に出て、発表者がドライバー、寺田さんがナビゲーターという形で進めました。
ハンズオンラボの資料をベースに、Microsoft アカウントの作成から Azure の無料試用版サブスクリプションの準備、踏み台サーバーの展開まで一通りすすめて、そのあと AKS の展開と kubectl でのオペレーションを持ち回りでやっていきます。
Cloud Shell 使うとこういう時環境依存が出ないのでいいですね。

また、他の人のオペレーションを見ながらやっていくと、一人で進めるよりは進みは確かに遅いのですがいろんな気づきがあるのでモブプロ形式はそういう点でもよかったです。

そして 2 日目は、1 日目と同じ内容を今度は参加者のみで進めていくというスタイル。
1 日目はサクサク進んでいたものでも、やっぱり聞き逃していたところやちゃんと整理できていない点は躓きポイントとして出てくるので、それをみんなであーだこーだ言いながら解決していくという感じでした。

最後の方で、実際 k8s ってどうバージョンアップすればいいの?とか、リソース制限の考え方等、導入した後も含めて寺田さんのご経験上からの Tips のレクチャーがありました。
このあたりのお話し。

www.slideshare.net

実際手を動かしてやってみることができたという点も当然ながらよかったのですが、サポートという普段の業務上、やはり実際導入するまでにどうなのか、導入してからはどうなのかという点を聞けたのはとても良かったですね。

また、最初に書いたように本を片手に一人で進めると、本当にこの機能が必要なのか?なんで必要なのか?どう使うのか?というのを一つ一つ考えていかないといけないですが、エッセンスを教えてもらえたという点で今後の学習コストを大幅に削減できた気がします。
仕事柄、普段圧倒的に参加者よりもトレーナー側の機会が多いのですが、そういう意味でもこういう風に伝えればいいんだなぁーとかいろいろな学びがありました。


ということで、まとめるととてもいいイベントでした!


最後に寺田さんからのキーワードのメモ。

  • k8s のすべての機能を使う必要はない。シンプルイズベスト
    • その構成、その規模、障害が起きたときや頻繁なバージョンアップに耐えられるの?
  • k8s はあくまでもツール。ほかの選択肢があるということは忘れない
    • k8s は特にサービスが多く変更が頻繁にあるアプリケーションに向く
    • k8s は Dev と Ops をつなげるツール。Dev も Ops のことを、Ops も Dev のことを知っておく必要がある
  • マイクロサービスなのか、ウォーターフォールなのか、IaaS / PaaS なのか k8s なのかを考える前に、なぜそれが必要なのか?を考える
    • 今のやり方でビジネスは伸ばせるのか?今のやり方で技術者として成長できるのか?

参考リンク

過去に開催されたイベントに関する記事

syobochim.hatenablog.com
blog.okazuki.jp

寺田さんのブログ

yoshio3.com

App Service on Linux で .htaccess を使ってアクセス制御する

備忘&ちょっとでもインターネット上に App Service on Linux の情報を流しておきたいので大した内容ではないですが書いておきます(

Front Door のバックエンドに App Service を置く場合、Front Door の IP アドレスはユーザーごとに違うわけではないため、App Service の機能としてある、IP アドレスによるアクセス制御では、アクセス元を制限することができません。
IPアドレスのレンジは公開されているため、(特定ではなく)Front Door サービスからのアクセスのみ許可、ということは可能です。以下ドキュメントより。

使用するバックエンドが、Azure Front Door のバックエンド IP アドレス空間と Azure のインフラストラクチャ サービスからのトラフィックのみを受け入れるように IP ACL 処理を構成します。 Microsoft は Azure IP 範囲およびサービス タグとの統合に向けて作業を進めていますが、現在のところ、IP 範囲は次のように参照することができます。
Front Door のIPv4 バックエンド IP 空間: 147.243.0.0/16
Front Door のIPv6 バックエンド IP 空間: 2a01:111:2050::/44
仮想化されたホスト IP アドレスを通した Azure の基本的なインフラストラクチャ サービス: 168.63.129.16 および 169.254.169.254

docs.microsoft.com


これがどういうときに問題かというと、Front Door の WAF を使って、バックエンドに App Service を置いた場合、悪意のある第三者が別の Front Door を置くことで WAF をスルー出来る可能性があります。もちろん、App Service 側のホスト名がわからないとたどり着けないのですが、可能性はゼロではありません。

こういうこと↓
f:id:mitsuki0820:20190808012943p:plain

で、どうするかというと、上のドキュメントにもある通り、X-Forwarded-Host に Front Door のホスト名がのってくるため、それをチェックする必要があります。
では、App Service on Linux ではどうするかですが、アプリ側で見なくても普通に .htaccess を置けばアクセス制御ができました。
こんな感じ。

SetEnvIf X-Forwarded-Host "www\.contoso\.com" allowed_fd
Order Deny,Allow
Deny from all
Allow from env=allowed_fd

ちょっとここで不安だったのが .htaccess でヘッダーのアクセス元チェック(要はApache のモジュール)を使うのって App Service の世界で一般的なんだろうか、ということでした。なんとなくつぶやいていたら、なんと中の方から返信いただけました。

ということで安心して使っていきます。おそらく他のモジュールも特に問題なく使えるでしょう。
ドキュメントをよく読むと、Laravel のデプロイのところでも .htaccess が書かれていました。
docs.microsoft.com

最後に phpinfo から抜いてきた Loaded Modules の一覧です。(今回は PHP アプリの話しでした)
mod_rewrite、mod_auth* も使えます。

core
mod_so
mod_watchdog
http_core
mod_log_config
mod_logio
mod_version
mod_unixd
mod_access_compat
mod_alias
mod_auth_basic
mod_authn_core
mod_authn_file
mod_authz_core
mod_authz_host
mod_authz_user
mod_autoindex
mod_deflate
mod_dir
mod_env
mod_expires
mod_filter
mod_include
mod_mime
prefork
mod_negotiation
mod_php7
mod_reqtimeout
mod_rewrite
mod_setenvif
mod_status

何でもかんでも .htaccess にするとあとで地獄を見ることになりますが、どうしてもという最後の隙間を埋める部分で使っていけそうです。

いまさらだけど Azure の Load Balancer のことをちょっと調べてみた

Standard Load Balancer が登場してからずーっと検証しようと思っていた NAT(SNAT)周りですが必要に迫られたのでついでにまとめました。

Azure の NAT まわりのこと

Load Balancer の話をする前に、まず、Azure の NAT の観点でネットワークまわりの現状をまとめます。

いまとなっては当たり前のことですが Azure の VM から直接見える IP アドレスは、192や10から始まるプライベート IP アドレスです。NIC に対して、グローバル IP アドレスが割り当てられているわけではありません。

つまりどういうことかというと、インターネットへの通信を行うには何らかの方法で、プライベート IP アドレスをグローバル IP アドレスに変換してあげる必要があります。

ここで、Azure の仮想マシンにパブリック IP アドレスを割り当てる場合(正確には仮想マシンの持つ NIC に割り当てる)と、割り当てない場合に、どのような NAT がされるかを考えます。

パブリック IP を割り当てる場合

パブリック IP アドレスを割り当てる場合は簡単です。単純に、割り当てたパブリック IP アドレスが外側のアドレスとして使われて、NAT 変換が行われます。
ドキュメントはこちら。

シナリオ 1:インスタンス レベルのパブリック IP アドレスがある VM
このシナリオでは、VM にはインスタンス レベルのパブリック IP (ILPIP) が割り当てられています。 送信接続に関する限り、VM が負荷分散されているかどうかは関係ありません。 このシナリオは他のシナリオよりも優先されます。 ILPIP が使用される場合、すべての送信フローで VM によって ILPIP が使用されます。
VM に割り当てられたパブリック IP は (1 対多ではなく) 1 対 1 の関係であり、1 対 1 のステートレス NAT として実装されます。 ポート マスカレード (PAT) は使用されず、VM は使用可能なすべてのエフェメラル ポートを備えます。
Azure の送信接続 | Microsoft Docs

パブリック IP を割り当てない場合

一方、パブリック IP アドレスを割り当てない場合どうなるかというと、Azure が暗黙のうちに用意してくれる NAT 機能が、これまた暗黙のうちに用意してくれているグローバルの IP アドレスに変換します。
ドキュメントはこちら。

シナリオ 3:インスタンス レベルのパブリック IP アドレスがないスタンドアロン VM
VM が送信フローを作成すると、Azure が、送信フローのプライベート ソース IP アドレスをパブリック ソース IP アドレスに変換します。 この送信フローで使用されるパブリック IP アドレスは構成不可能であり、サブスクリプションのパブリック IP リソースの制限に対してカウントされません。 このパブリック IP アドレスはユーザーのものではなく、予約することはできません。 VM、可用性セット、または仮想マシン スケール セットを再デプロイすると、このパブリック IP アドレスは解放され、新しいパブリック IP アドレスが要求されます。 IP アドレスをホワイトリストに登録する場合は、このシナリオを使用しないでください。 代わりに、送信シナリオと、送信接続で使用されるパブリック IP アドレスを明示的に宣言する他の 2 つのシナリオのいずれかを使用します。
Azure の送信接続 | Microsoft Docs

上記のドキュメントの通り、通常は課金されるパブリック IP アドレスですが、Azure が勝手に用意してくれるリソースなので当然課金は発生しません。

ここで問題がひとつ
エンタープライズでよくあるシナリオで、VM にはパブリックの IP アドレスを付与しないことで外部からのアクセスはさせず、ただ VM からはインターネットに接続させたいというケースがあります。さらに、これもよくあるのですが、他の Azure のサービスやアプリケーション等で送信元をホワイトリスト化したい、というケースです。

これまで説明してきた内容からは、送信元パブリック IP アドレスを固定化するには、VM にパブリック IP アドレスを割り当てる必要がありました。ただそうするとインターネット上にエンドポイントを晒してしまうということになってしまい、要件が満たせなくなります。

これを回避する1つの方法としては、プロキシサーバーを立てる方法です。ただ、単純にパブリック IP アドレスを固定化するのに、プロキシサーバーを立てるというのは、仮想マシンのコストや管理の工数を考えるとあまりやりたくはありません。
そこでもう1つの方法として、Azure の Load Balancer を使う方法があります。

ドキュメントのシナリオ2の部分です。

負荷分散 VM が送信フローを作成すると、Azure が、送信フローのプライベート ソース IP アドレスをパブリック ロード バランサー フロントエンドのパブリック IP アドレスに変換します。 Azure は、SNAT を使用してこの機能を実行します。 また、Azure は、PAT を使用して、複数のプライベート IP アドレスをパブリック IP アドレスでマスカレードします。
Azure の送信接続 | Microsoft Docs

AWS だと NAT ゲートウェイというのがあるらしいのですが、Azure の場合はそういうサービスがなく、その代わりに Load Balancer を使うと同じようなことが実現できます。
※ただ、負荷分散はいらないのに Load Balancer のサービスを作らないといけないのはいまいちな点もあるので、おそらくたぶんきっと、NAT ゲートウェイがそろそろ出てくるに違いありません。

Azure の Load Balancer まわりのこと

Azure には、いわゆる L4 のロードバランサーとして、Basic Load Balancer と Standard Load Balancer があります。Standard Load Balancer はそれなりに最近出てきたサービスです。

何が違うの?という具体的な内容は↓の公式ドキュメントに譲りますが、大きな違いとしてはざっくり、AZに対応していること、バックエンドプールとして仮想マシンが指定できること(Basic は原則可用性セットのみ)、その他いろいろ機能が追加されていること、です。
docs.microsoft.com

ちなみに、お金の話をすると、Basic Load Balancer は無料で使えていましたが、Standard Load Balancer は有料です。

azure.microsoft.com

とは言え、機能や今後の方向性を考えると、Standard を使うのがいいと思います。

検証

ようやくここからが今回の記事の本題です。

上記のドキュメントを読むと、Standard Load Balancer については、アウトバウンドの通信を明示的に作ってあげる必要がある、という記述があります。

仮想マシン、可用性セット、仮想マシン スケール セットが送信接続 (アウトバウンド接続) を使用するためには、送信シナリオを明示的に作成する "必要があります"。

送信シナリオは明示的であり、発信接続は指定されるまで存在しません。

実際、Standard Load Balancer のバックエンドにパブリック IP アドレスが割り当てられていない仮想マシンを展開して、インターネットに接続しようとすると、SYN_SENT の状態でとまってしまい通信ができません。

[tsunomur@VM ~]$ netstat -an --tcp | grep 13.71
tcp        0      1 10.8.0.4:37436          13.71.134.177:80        SYN_SENT

ということで、送信規則を作って割り当ててみました。

送信規則を作ってみる

ざっくりとした流れとしては、

  1. Standard Load Balancer を作る
  2. バックエンドプールを作って仮想マシンを指定する
  3. 送信規則を作る

という感じです。

バックエンドプールを作って仮想マシンを指定するところまでは、ポータルからできます。
現状、送信規則を作るには、Azure PowerShell/Azure CLI を使う必要があります。

こんな感じ。

# az network lb outbound-rule create  --resource-group rg  --lb-name lb  --name outboundrule  --frontend-ip-configs LoadBalancerFrontEnd  --protocol All  --idle-timeout 15  --outbound-ports 10000  --address-pool bepooloutbound
AllocatedOutboundPorts    EnableTcpReset    IdleTimeoutInMinutes    Name          Protocol    ProvisioningState    ResourceGroup
------------------------  ----------------  ----------------------  ------------  ----------  -------------------  ---------------
10000                     False             15                      outboundrule  All         Succeeded            rg

あとからパラメーターを変える時はこんな感じ。

az network lb outbound-rule update -g rg --lb-name lb --name outboundrule --enable-tcp-reset true --idle-timeout 4 --protocol tcp --outbound-ports 10000
NAT の割り当てポートを変えてみる

本当にAllocatedOutboundPortsが効いているのか?という疑問がふと出て来たので、実験してみました。


ポート割り当ての数を 8 に

# az network lb outbound-rule update -g rg --lb-name lb --name outboundrule --outbound-ports  8
AllocatedOutboundPorts    EnableTcpReset    IdleTimeoutInMinutes    Name          Protocol    ProvisioningState    ResourceGroup
------------------------  ----------------  ----------------------  ------------  ----------  -------------------  ---------------
8                         True              4                       outboundrule  Tcp         Succeeded            rg

VM からコネクションを 8 本張ってみる。

[tsunomur@VM ~]$ for i in `seq 8`; do nc nomupro.com 80&  done
[tsunomur@VM ~]$ netstat -an --tcp | grep 13.71
tcp        0      0 10.8.0.4:41290          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41276          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41288          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41286          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41282          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41284          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41277          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41280          13.71.134.177:80        ESTABLISHED

9本目のコネクションを張ってみる。

[tsunomur@VM ~]$ nc nomupro.com 80
Ncat: Connection timed out.
[tsunomur@VM ~]$
[tsunomur@VM ~]$ netstat -an --tcp | grep 13.71
tcp        0      0 10.8.0.4:41290          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41276          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41288          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41286          13.71.134.177:80        ESTABLISHED
tcp        0      1 10.8.0.4:41398          13.71.134.177:80        SYN_SENT
tcp        0      0 10.8.0.4:41282          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41284          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41277          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:41280          13.71.134.177:80        ESTABLISHED
[tsunomur@VM ~]$

Standard Load Balancer では、メトリックを見ることができるので変化を見てみました。

f:id:mitsuki0820:20190730232528p:plain

f:id:mitsuki0820:20190730232718p:plain

ちゃんと割り当てポートが8になり、使用済みも8になってますね。
※ちなみに Allocated SNAT Portsは、プロトコル毎(TCPUDP)でそれぞれ設定できるのですが、今回はあらかじめ TCP のみにしています。なので、メトリックの値は 8 になります。

ポート割り当ての数を 16 に

# az network lb outbound-rule update -g rg --lb-name lb --name outboundrule --outbound-ports  16
AllocatedOutboundPorts    EnableTcpReset    IdleTimeoutInMinutes    Name          Protocol    ProvisioningState    ResourceGroup
------------------------  ----------------  ----------------------  ------------  ----------  -------------------  ---------------
16                        True              4                       outboundrule  Tcp         Succeeded            rg

VM からコネクションを 16 本張ってみる。

[tsunomur@VM ~]$ for i in `seq 16`; do nc nomupro.com 80&  done
[tsunomur@VM ~]$ netstat -an --tcp | grep 13.71
tcp        0      0 10.8.0.4:45922          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45934          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45930          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45918          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45936          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45914          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45932          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45912          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45916          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45928          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45908          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45910          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45920          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45906          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45926          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45924          13.71.134.177:80        ESTABLISHED

17 本目のコネクションを張ってみる。

[tsunomur@VM ~]$ nc nomupro.com 80
Ncat: Connection timed out.
[tsunomur@VM ~]$
[tsunomur@VM ~]$ netstat -an --tcp | grep 13.71
tcp        0      0 10.8.0.4:45922          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45934          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45930          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45918          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45936          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45914          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45932          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45912          13.71.134.177:80        ESTABLISHED
tcp        0      1 10.8.0.4:46108          13.71.134.177:80        SYN_SENT
tcp        0      0 10.8.0.4:45916          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45928          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45908          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45910          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45920          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45906          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45926          13.71.134.177:80        ESTABLISHED
tcp        0      0 10.8.0.4:45924          13.71.134.177:80        ESTABLISHED
[tsunomur@VM ~]$

f:id:mitsuki0820:20190730234326p:plain

f:id:mitsuki0820:20190730234213p:plain

あたり前ですが16本まで張れましたね。

まとめ

ということで、ドキュメントに書かれていることができるよね、というあたり前のことを試しただけですが Azure の NAT の理解が少し深まりました。
それにしてもネットワーク周りの検証ってやっぱり地味w

また今度機会と時間があれば

  • 複数のフロントエンド、複数の VM を設定した時にどういう動きになるか
  • Azure Firewall の SNAT の動きってどうなってるのか

のあたりを試してみようと思います。

Azure DNS の TTL を ARM テンプレートで展開する

ARM リファレンスを見れば載ってるけど、Azure DNS を ARM テンプレートで展開するときに TTL も一緒に設定するテンプレート。
単に NS レコード、SOA レコードで指定してあげればいいけど、name に @ を指定するまでにたどり着くのにちょっと時間が掛かったのでメモしておきます。

尚、ネームサーバーを指定することはあまりお勧めできませんので、別デプロイとして、NS レコードの設定を行います。

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
    },
    "resources": [
        {
            "type": "Microsoft.Network/dnszones",
            "name": "tsunomurdns.contoso.com",
            "apiVersion": "2016-04-01",
            "location": "global",
            "properties": {},
            "resources": [
                {
                    "name": "@",
                    "type": "SOA",
                    "apiVersion": "2018-05-01",
                    "properties": {
                        "metadata": {},
                        "TTL": "123",
                        "SOARecord": {
                            "host": "tsunomurdns.contoso.com",
                            "email": "contoso.com",
                            "serialNumber": "1",
                            "refreshTime": "11",
                            "retryTime": "22",
                            "expireTime": "33",
                            "minimumTTL": "44"
                        }
                    },
                    "dependsOn": [
                        "[resourceId('Microsoft.Network/dnszones/', 'tsunomurdns.contoso.com')]"
                    ]
                }
                
            ]
        },
		{
      "apiVersion": "2015-01-01",
      "type": "Microsoft.Resources/deployments",
      "name": "updateZone",
      "dependsOn": [
          "[resourceId('Microsoft.Network/dnszones/', 'tsunomurdns.contoso.com')]"
      ],
      "properties": {
        "mode": "Incremental",
        "parameters": {},
        "template": {
          "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
          "contentVersion": "1.0.0.0",
          "parameters": {},
          "variables": {},
          "resources": [
              		 {
                    "name": "tsunomurdns.contoso.com/@",
                    "type": "Microsoft.Network/dnszones/NS",
                    "apiVersion": "2018-05-01",
                    "properties": {
                        "metadata": {},
                        "TTL": 111111,
                        "NSRecords": "[reference(resourceid('Microsoft.Network/dnszones/NS/','tsunomurdns.contoso.com','@'),'2018-05-01').nsRecords]"
                        ,
                        "targetResource": {}
                    }
                    
                }
          ],
          "outputs": {}
          }
        }
    }
    ],
    "outputs": {
    }
}