ほりひログ

所属組織の製品 (Azure とか) に関連する内容が多めだけど、個人の見解であって、所属組織を代表する公式情報ではないです。

Azure での SSL 証明書対応状況 (2022 年 2 月版)

API Management のマネージド証明書が Public Preview になったのを記念して(?)、他のサービスの証明書の対応状況 (2022 年 2 月現在) のまとめたメモ。間違ってたらごめんなさい。

Service Managed Certificate Bring Your Own Certificate, Key Vault
App Service
Key Vault Certfificate:
Key Vault Secret:
Kubernetes Services -
Key Vault:
Container Instances -
Application Gateway -
Key Vault:
Front Door Key Vault:
API Management
Key Vault:
Static Web Apps -
Cloud Services -

# 状況が変わったら更新する、かもしれないし、しないかもしれない。

オレ流 Azure RBAC のカスタム ロールの作り方 (3) ~ CLI ツール作りました

前回、前々回で紹介したカスタム ロール作成手順があまりに面倒だったので、少しでも手間を減らすために CLI ツール custom-role-generator を作りました。

github.com

初 Rust です。ググりながら作ったコードです。

ちなみに、前回・前々回はこちら。

uncaughtexception.hatenablog.com
uncaughtexception.hatenablog.com

こちらで紹介した下記の手順のうち、主に 1. と 2. あたりをサポートします。

  1. Azure CLI を使って必要な Azure REST API を確認する
  2. HTTP メソッドとエンドポイント URL からアクションを組み立てる
  3. とりあえずカスタム ロールを試す
  4. 地道にカスタム ロールを修正する

インストールは Releases から zip をダウンロードして解凍するだけです。さすが Rust 。ワンバイナリー*1

使い方

custom-role-generator は標準入力からインプットされた内容を解析します。

なので、az コマンドに --debug を付けて標準エラー出力にログを出力し、そのログを 2>&1 で標準出力にリダイレクトして、パイプ |custom-role-generator で渡せば動きます。

$ az [何かのコマンド] --debug 2>&1 | custom-role-generator

ファイルに出力したログも、< を使えば標準入力に渡せます。

$ az [何かのコマンド] --debug 2>az.log # 標準エラー出力を `az.log` に出力
$ custom-role-generator < az.log # `az.log` を標準入力から入力

まともに動けば、標準出力にカスタム ロールの定義ファイルが出力されます。

{
  "AssignableScopes": [
    "/subscriptions/..."
  ],
  "actions": [
    "Some.Provider/.../...",
      :
  ],
  "dataActions": [],
  "description": "Custom role definition for `az [何かのコマンド]` generated by custom-role-generator",
  "isCustom": true,
  "name": "<REPLACE_THIS>",
  "notActions": [],
  "notDataActions": []
}

既存のカスタム ロール定義ファイルを --append-to オプションで指定することで、ログから抽出した action を actions に追加することもできます。

$ az [何かのコマンド] --debug 2>&1 | custom-role-generator --append-to /path/to/existing/role-definition.json
{
  "AssignableScopes": [
    "/subscriptions/..."
  ],
  "actions": [
    "[もともとあった action1]",
    "[もともとあった action2]",
      :
    "[追加された action1]",
    "[追加された action2]",
      :
  ],
  "dataActions": [],
  "description": "Custom role definition for `az [何かのコマンド]` generated by custom-role-generator",
  "isCustom": true,
  "name": "<REPLACE_THIS>",
  "notActions": [],
  "notDataActions": []
}

あと custom-role-generator --debug のように --debug オプションを付けると、入力されたログをそのまま標準エラー出力に出力します。

実行例

やっぱり VNET 統合

まずは共同作成者なり、 VNET 統合を設定可能な権限を持ったユーザーでコマンドを実行し、ベースになるロール定義を作ります。

# 共同作成者ロールでの実行
$ az webapp vnet-integration add --id <WebApp のリソース ID> --vnet <VNET のリソース ID> --subnet <Subnet 名> --debug 2>&1 | custom-role-generator > role-vnet-integration1.json
$ cat role-vnet-integration1.json
{
  "AssignableScopes": [
    "/subscriptions/..."
  ],
  "actions": [
    "Microsoft.Network/locations/operations/read",
    "Microsoft.Network/virtualNetworks/read",
    "Microsoft.Network/virtualNetworks/subnets/read",
    "Microsoft.Network/virtualNetworks/subnets/write",
    "Microsoft.Resources/subscriptions/locations/read",
    "Microsoft.Web/serverfarms/read",
    "Microsoft.Web/sites/config/read",
    "Microsoft.Web/sites/config/write",
    "Microsoft.Web/sites/publishxml/action",
    "Microsoft.Web/sites/read",
    "Microsoft.Web/sites/write"
  ],
  "dataActions": [],
  "description": "",
  "isCustom": true,
  "name": "Custom role definition for `az webapp vnet-integration add` generated by custom-role-generator",
  "notActions": [],
  "notDataActions": []
}

これで、上述の 4 ステップのうち 1. と 2. が終了。

このロール定義を使ってカスタム ロールを作成し、テスト ユーザーに関連リソースのカスタム ロールに割り当てて、今度はそのテスト ユーザーで実行します。
これで成功したら嬉しいですが、前回のエントリーで書いた通り、VNET 統合の場合はこの段階ではまだ成功しません。
原因 (足りない action) を追加するため、あえて失敗するユーザーでの実行ログをもう一度 custom-role-generator に入れます

# テストユーザーでの実行
$ az webapp vnet-integration add --id <WebApp のリソース ID> --vnet <VNET のリソース ID> --subnet <Subnet 名> --debug 2>&1 \
| custom-role-generator --append-to role-vnet-integration1.json > role-vnet-integration2.json
$ cat role-vnet-integration2.json
{
  "AssignableScopes": [
    "/subscriptions/..."
  ],
  "actions": [
    "Microsoft.Network/locations/operations/read",
    "Microsoft.Network/virtualNetworks/read",
    "Microsoft.Network/virtualNetworks/subnets/join/action", # <- 追加される action
    "Microsoft.Network/virtualNetworks/subnets/read",
    "Microsoft.Network/virtualNetworks/subnets/write",
    "Microsoft.Resources/subscriptions/locations/read",
    "Microsoft.Web/serverfarms/read",
    "Microsoft.Web/sites/config/read",
    "Microsoft.Web/sites/config/write",
    "Microsoft.Web/sites/publishxml/action",
    "Microsoft.Web/sites/read",
    "Microsoft.Web/sites/write"
  ],
  "dataActions": [],
  "description": "",
  "isCustom": true,
  "name": "Custom role definition for `az webapp vnet-integration add` generated by custom-role-generator",
  "notActions": [],
  "notDataActions": []
}

これをもう一回テストユーザーに割り当てると問題なく VNET 統合が設定できます。

他のコマンドでも似たようなものだと思うので、ぜひ手強そうなカスタム ロールづくりに試してもらってフィードバックいただければ。

なお Rust 始めたばかりで全然わかりません。リファクタリングしないといけないし、テストもまともに書いていない、書けない。
PR も絶賛募集中。

これにてカスタム ロールの作り方はおしまい。


*1:Rust だけじゃないですけど

オレ流 Azure RBAC のカスタム ロールの作り方 (2) ~ VNET 統合の場合

今回はですね、こちらのオレ流 Azure RBAC のカスタム ロールの作り方 (1)の手順を使って、App Service (Web Apps や Function App)の VNET 統合を Azure CLI から設定できるカスタム ロールを作ってみますよ。
VNET 統合は App Service だけでなく、VNET/Subnet も操作するので、割と面倒複雑なカスタム ロールになります。

ではやってみます。

1. Azure CLI を使って必要な Azure REST API を確認する

まず共同作成者ロールのユーザーで下記 Azure CLI で実行してみます。

az webapp vnet-integration add --id <WebApp のリソース ID> --vnet <VNET のリソース ID> --subnet <Subnet 名> --debug

このコマンドに出力されるログから、grep 等で Azure REST API のアクセス ログを探すと、以下の 17 件が抽出できます。

DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../providers/Microsoft.Network/virtualNetworks?api-version=... HTTP/1.1" 200 749
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/...?api-version=... HTTP/1.1" 200 None
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/...?api-version=... HTTP/1.1" 200 None
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/.../config/web?api-version=... HTTP/1.1" 200 None
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "POST /subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/.../publishxml?api-version=... HTTP/1.1" 200 1589
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../resourceGroups/rg-linux-plan/providers/Microsoft.Web/serverfarms/...?api-version=... HTTP/1.1" 200 None
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../resourceGroups/.../providers/Microsoft.Network/virtualNetworks/...?api-version=... HTTP/1.1" 200 None
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../locations?api-version=... HTTP/1.1" 200 4592
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../locations?api-version=... HTTP/1.1" 200 4592
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../resourceGroups/.../providers/Microsoft.Network/virtualNetworks/.../subnets/...?api-version=... HTTP/1.1" 200 None
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "PUT /subscriptions/.../resourceGroups/.../providers/Microsoft.Network/virtualNetworks/.../subnets/...?api-version=... HTTP/1.1" 200 None
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../providers/Microsoft.Network/locations/.../operations/...?api-version=... HTTP/1.1" 200 None
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../resourceGroups/.../providers/Microsoft.Network/virtualNetworks/.../subnets/...?api-version=... HTTP/1.1" 200 None
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "PATCH /subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/...?api-version=... HTTP/1.1" 200 None
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/.../config/web?api-version=... HTTP/1.1" 200 None
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "GET /subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/.../config/web?api-version=... HTTP/1.1" 200 None
DEBUG: urllib3.connectionpool: https://management.azure.com:443 "PATCH /subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/.../config/web?api-version=... HTTP/1.1" 200 None

結構いろんなところにリクエストを投げてますね。

2. HTTP メソッドとエンドポイント URL からアクションを組み立てる

上の 17 件のアクセス ログ、それぞれについて、リソース プロバイダーとリソース タイプ、HTTP メソッドを元にして構築し重複分を除きます。

    "Microsoft.Network/locations/operations/read",
    "Microsoft.Network/virtualNetworks/read",
    "Microsoft.Network/virtualNetworks/subnets/read",
    "Microsoft.Network/virtualNetworks/subnets/write",
    "Microsoft.Resources/subscriptions/locations/read",
    "Microsoft.Web/serverfarms/read",
    "Microsoft.Web/sites/config/read",
    "Microsoft.Web/sites/config/write",
    "Microsoft.Web/sites/publishxml/action",
    "Microsoft.Web/sites/read",
    "Microsoft.Web/sites/write"

11 の action までまとまりました。

一点、GET /subscriptions/.../locations のアクセス ログはリソース プロバイダーを含まず、サブスクリプション直下のロケーション情報なので、対応する action である Microsoft.Resources/subscriptions/location/read に変換しています。

3. とりあえずカスタム ロールを試す

今回は Azure CLI でカスタム ロールを作ります。
上でまとめた 11 の action から下記の JSON を用意しました。

{
  "AssignableScopes": [
    "/subscriptions/..."
  ],
  "actions": [
    "Microsoft.Network/locations/operations/read",
    "Microsoft.Network/virtualNetworks/read",
    "Microsoft.Network/virtualNetworks/subnets/read",
    "Microsoft.Network/virtualNetworks/subnets/write",
    "Microsoft.Resources/subscriptions/locations/read",
    "Microsoft.Web/serverfarms/read",
    "Microsoft.Web/sites/config/read",
    "Microsoft.Web/sites/config/write",
    "Microsoft.Web/sites/publishxml/action",
    "Microsoft.Web/sites/read",
    "Microsoft.Web/sites/write"
  ],
  "dataActions": [],
  "description": "",
  "isCustom": true,
  "name": "<何か名前を付ける>",
  "notActions": [],
  "notDataActions": []
}

AssignableScopes に記載するサブスクリプション ID は適宜変更してください。

この JSON ファイルを使ってカスタム ロールの定義を作ります。

az role definition create --role-definition <JSON ファイルのパス>

上記の action では Microsoft.Web/sitesMicrosoft.Web/serverfarmsMicrosoft.Networks/virutalNetworks の 3 種類のリソースが出てきているので、VNET 統合に使う App Service 、App Service Plan、仮想ネットワークの 3 つのリソースに対して作ったカスタム ロールをテスト ユーザーに割り当てて、そのユーザーで最初の Azure CLI を実行してて結果を見ます。

4. 地道にカスタム ロールを修正する

ここで終わればラッキーですが、テスト ユーザーに作って実行してみると下記のエラーが出ます。
やはりまだ権限が足りていませんでした。

Message: The client '...' with object id '...' has permission to perform action 'Microsoft.Web/sites/write' on scope '/subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/...'; however, it does not have permission to perform action 'join/action' on the linked scope(s) '/subscriptions/.../resourceGroups/../providers/Microsoft.Network/virtualNetworks/.../subnets/...' or the linked scope(s) are invalid.

App Service への書き込み権限はあるけど、Subnet への接続の権限がない、といった様子。

メッセージを元に、Microsoft.Network/virtualNetworks/subnets/join/action の action をカスタム ロールに追加し、再度ロールの割り当てからトライすると無事成功することが確認できました。

{
  "AssignableScopes": [
    "/subscriptions/..."
  ],
  "actions": [
    "Microsoft.Network/locations/operations/read",
    "Microsoft.Network/virtualNetworks/read",
    "Microsoft.Network/virtualNetworks/subnets/read",
    "Microsoft.Network/virtualNetworks/subnets/write",
    "Microsoft.Resources/subscriptions/locations/read",
    "Microsoft.Web/serverfarms/read",
    "Microsoft.Web/sites/config/read",
    "Microsoft.Web/sites/config/write",
    "Microsoft.Web/sites/publishxml/action",
    "Microsoft.Web/sites/read",
    "Microsoft.Web/sites/write",
    "Microsoft.Network/virtualNetworks/subnets/join/action"
  ],
  "dataActions": [],
  "description": "",
  "isCustom": true,
  "name": "<何か名前を付ける>",
  "notActions": [],
  "notDataActions": []
}

これで Azure CLI から VNET 統合を設定できるカスタム ロールの完成です。 成功しない (追加したはずの action でエラーが出る) 場合は、ロールの反映に時間がかかっている場合がほとんどなので、時間を置く and/or ログアウト&ログインを試せば、いずれ成功します。

おまけ (削除 & ポータル操作対応)

ついでに VNET 統合の無効化と Azure ポータルからの操作権限を追加したカスタム ロールこちらです。

{
  "AssignableScopes": [
    "/subscriptions/..."
  ],
  "actions": [
    "Microsoft.Network/locations/operations/read",
    "Microsoft.Network/virtualNetworks/read",
    "Microsoft.Network/virtualNetworks/subnets/read",
    "Microsoft.Network/virtualNetworks/subnets/write",
    "Microsoft.Resources/subscriptions/locations/read",
    "Microsoft.Web/serverfarms/read",
    "Microsoft.Web/sites/config/read",
    "Microsoft.Web/sites/config/write",
    "Microsoft.Web/sites/publishxml/action",
    "Microsoft.Web/sites/read",
    "Microsoft.Web/sites/write",
    "Microsoft.Network/virtualNetworks/subnets/join/action",
    "Microsoft.Web/sites/networkConfig/delete",
    "Microsoft.Web/sites/config/list/Action",
    "Microsoft.Web/sites/virtualnetworkconnections/read",
    "Microsoft.Web/sites/networkConfig/read"
  ],
  "dataActions": [],
  "description": "",
  "isCustom": true,
  "name": "<何か名前を付ける>",
  "notActions": [],
  "notDataActions": []
}

削除に必要な action Microsoft.Web/sites/networkConfig/deleteaz functionapp vnet-integration remove コマンドのリクエスト ログから、Azure ポータル上から VNET 統合を構成するために必要な action Microsoft.Web/sites/config/list/Action, Microsoft.Web/sites/virtualnetworkconnections/read, Microsoft.Web/sites/networkConfig/read の 3 つは Web ブラウザーのネットワーク トレースを地道に読み解いています。

以上、App Service の VNET 統合を Azure CLI で設定可能なカスタム ロール作成の実例でした。

「これじゃうまくいかない」といったことがあればコメントででもいただければ。

オレ流 Azure RBAC のカスタム ロールの作り方 (1)

今回は、Azure 上のリソースのアクセス管理ができる Azure RBAC のカスタム ロールの作り方です。

カスタム ロールを作って、(共同作成者みたいな巨大な権限ではなく)一部の操作のみができる権限をユーザーに割り当てるシーンはよくあると思います。

作り方はドキュメントに書いてあるのですが、、、

docs.microsoft.com f:id:horihiro:20220122121314p:plain

この「使用可能なアクションを把握」というのが割と大変なので、なんとなくいつもやってるはず、という手順をメモとして残しておきます。
もっと楽なやり方があれば教えてもらえるとありがたいです🙇‍♂️

1. Azure CLI を使って必要な Azure REST API を確認する

まず最初に設定に必要な Azure REST API へのリクエスト パターンを把握するため、対象の操作を実行できる権限を持つユーザーで Azure CLI--debug オプション付きで実行します。

az <sub-command> <options ...> --debug

すると、標準エラー出力に大量のログが表示される中、以下のような Azure REST API へのリクエスト ログが確認できます。

f:id:horihiro:20220122122059p:plain

フォーマットは以下のようになっているはずです (2022 年 1 月現在)。

DEBUG: urllib3.connectionpool: https://management.azure.com:443 "<HTTP メソッド> <Azure REST API のエンドポイント URL > HTTP/1.1" <レスポンス コード> <レスポンス ボディの長さ>

このうち使うのは、HTTP メソッドと Azure REST API のエンドポイント URL です。
これを全てピックアップします。

なお、この REST API の実行は、リソースの状態によってスキップするリクエストもあるので、作り立てホヤホヤのリソースで試すことがお勧めです。

2. HTTP メソッドとエンドポイント URL からアクションを組み立てる

Azure REST API のエンドポイント URL は、一般的には以下のフォーマットです。

https://management.azure.com:443/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME>/providers/<RESOURCE_PROVIDER>/<RESOURCE_TYPE_1>/<RESOURCE_NAME_1>/.../<RESOURCE_TYPE_n>/<RESOURCE_NAME_n>?api-version=<API_VERSION>

このうち使うのは、<RESOURCE_PROVIDER> と <RESOURCE_TYPE_[1..n]> で、以下のように連結します。

<RESOURCE_PROVIDER>/<RESOURCE_TYPE_1>/.../<RESOURCE_TYPE_n>

最後に以下の表から HTTP メソッドに対応した動詞を見つけてそれを足します。

HTTP メソッド 動詞
GET read
PUT write
PATCH write
DELETE detete
POST action

例えば、といったリクエスト ログが見つかったとします。

DEBUG: urllib3.connectionpool: https://management.azure.com:443 "PATCH /subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/.../config/...?api-version=... HTTP/1.1" 200 ...

すると、これを実行するためにカスタム ロールに必要な action は

Microsoft.Web/sites/config/write

になります。

これをピックアップしたリクエスト ログすべてに対して実施して、action のリストを作ります。

全 action はこのドキュメントにあるので、作った action が載っているか確認しましょう。

docs.microsoft.com

3. とりあえずカスタム ロールを試す

action のリストができたら一度カスタム ロールを作ります。

作り方は Azure CLI でも Azure ポータルでもお好みで。

docs.microsoft.com

作成したロールをテスト ユーザーに割り当てて、そのユーザーとして最初に実行した Azure CLI を実行してみます。
ロール割り当ての反映には、時間がかかったりトークンを更新しないといけない場合が多く、追加したはずの action でエラーになる場合もありますが、根気よく試しましょう。

4. 地道にカスタム ロールを修正する

これで成功したらは完成ですが、そうはいかない場合も多々あります。

ここまででリスト アップした action は、最初にリクエスト ログを取得した Azure CLI のコマンドが、直接リクエストを実行しているログを元にしています。
一方で、Azure CLI からのリクエストを起点に、Azure Resource Manager が実行ユーザーの権限で裏で別の API を実行するケースがあります。

この場合、Azure CLI のログにはエンドポイント URL は記録されません。
このケースでの action 不足の時は、エラー ログとして以下のようなメッセージが表示されます。

Message: The client '...' with object id '...' has permission to perform action '<カスタム ロールに記載した action>' on scope '<ロールのスコープ>'; however, it does not have permission to perform action '<不足している action>' on the linked scope(s) '<不足 action の対象スコープ>' or the linked scope(s) are invalid.

however 以降が足りない action とスコープの説明になっているので、これを元に再度カスタム ロール用の action を追加してロールを割り当てて実行します。

これを繰り返して、エラーが消えれば、カスタム ロールの完成です。
結局、ここの部分が割と時間がかかりるのですが、諦めず頑張りましょう。

FAQ

Q1. write の action である機能の無効化もできますか?

一般的に write の action は既存のリソースの情報を書き換えるために必要な権限です。
その為、例えばある機能の有効化/無効化の切り替えを、既存のリソース情報の書き換え (プロパティ foobarEnabledtruefalse を入れる、など) で制御しているのであれば、write の action でも無効化は可能だと思います。

一方で、有効化に子リソースを追加、無効化に子リソースの削除、といった操作を伴う機能の場合、writeaction だけで対象リソースの削除はできないので、delete の action が必要になります。

ちなみに write の action で read の action をカバーすることもできないのでご注意を。

Q2. Azure ポータルでの操作と Azure CLI での操作を、一つのカスタム ロールでカバー出来ますか?

二つの操作は共通部分は多いですが、それぞれ独自に必要な action もあります。 Azure ポータルでの操作は、UI 上に表示するための情報取得のリクエストが発生する場合があり、逆に Azure CLI の場合はオプションでその辺りを決め打ちで指定するので、比較的 Azure ポータルでの操作の方が Azure CLI での操作よりも広い権限を求めることが多いようです。

なので一つのカスタム ロールにまとめたい場合は、それらの action 全てを含くんだ (= OR を取った) カスタム ロールを作る必要があります。

次は App Service の VNET 統合を例にして、以上のステップを試してみたいと思いますが、長くなったので別エントリーへ(書きました)。

Azure Static Web Apps の Enterprise-grade edge がパブリック プレビューになりました

Azure Static Web Apps (以下 SWA) の Enterprise-grade edge はハッキリ言うと、マネージドの (ユーザーのサブスクリプション外にある) Azure Front Door (以下 AFD)連携です。

azure.microsoft.com

SWA は一つのリソースでいくつかのリージョンにコンテンツをデプロイしていて、リソース内部に持つ Traffic Manager (以下 TM) を使って、コンテンツ配信に最適なリージョンに振り分けてます。
で、今回パブリック プレビューとなった Enterprise-grade edge では、この SWA の前段にある TM をマネージドの AFD に置き換えているようです。

絵で描くとこれ↓が、

f:id:horihiro:20220119165303p:plain

こう↓なります。

f:id:horihiro:20220119165552p:plain

AFD は CDN なので、SWA が配信するコンテンツを世界各地のエッジ サーバーでキャッシュできますし、AFD 自身も TM を中に持っているので、クライアントの場所によって (ネットワーク的に) 最も近いエッジ サーバーからコンテンツを配信できるので、コンテンツの配信速度の向上が見込めます

やってみる

ドキュメントによると、いくつか事前に設定が必要です。

  • カスタム ドメイン (TTL が 48 時間未満) をSWA に設定
  • SWA のプランを Standard に
  • 対象の Azure サブスクリプションにて、Microsoft.CDN リソース プロバイダーを再登録

docs.microsoft.com

あとは、Azure ポータルで SWA の [Enterprise-grade edge] というブレードを開いて有効化するだけです。

f:id:horihiro:20220119160840p:plain

ちなみに、Free プランの場合は [Enterprise-grade edge] ブレードがグレー アウトして選択できません。 Standard プランでもカスタム ドメインを設定していない場合は、以下のエラーが出るので、キチンとカスタム ドメインは設定しましょう。

f:id:horihiro:20220119161956p:plain

Azure CLI で有効化する場合は、事前に enterprise-edge という拡張が必要になるので、事前に下記コマンドでインストールしておきましょう。

az extension add -n enterprise-edge

確認してみる

まず Enterprise-grade edge 有効化前の名前解決結果です。 f:id:horihiro:20220119162601p:plain SWA に設定したカスタム ドメイン (swa-std.■■■.net) に対して nslookup を実行すると、

  1. SWA の既定のドメイン
  2. SWA 用の TM
  3. どこかの Web Apps ( .azurewebsites.net ドメインなので)
    (以下は Web Apps の名前解決の流れ)

という順序で CNAME を追っていき、最終的にどこか (恐らく最寄りの) の Web Apps の IP アドレスに辿りつきます。

次に、Enterprise-grade edge を有効化して、同じカスタム ドメインに対して nslookup を実行してみます。

f:id:horihiro:20220119170656p:plain

  1. SWA の既定のドメイン
  2. マネージドの AFD
  3. AFD 用の TM
  4. どこかのエッジ サーバー

という名前解決の流れになり、AFD が提供するエッジ サーバーの IP アドレスに辿りつくわけです。

実際にここで得られた IP アドレスを、Azure の IP アドレスリストと照合してみると、AzureFrontDoor.Frontend というタグが付いたグループに含まれることが確認できるので、興味のある方はこちらから。

www.microsoft.com

# が、どのリージョンかまでは追えなかった。

価格

最後のお値段ですが、ひと月&一アプリ当たり $17.52 課金されるようです。

f:id:horihiro:20220120083155p:plain

azure.microsoft.com

おまけ

Enterprise-grade edge の有効/無効はポータルから何度もできますが、無効化した直後に SWA からカスタム ドメインを削除すると、一時的にロックに近い状態になります。
こうなると、1時間ほど再有効化ができなくなるのでご注意ください。
# これでリソース壊しかけました

Azure Functions Linux 従量課金プランでファイルの実行パーミッションを維持する方法

本エントリーは Microsoft Azure Tech Advent Calendar 2021 9 日目の記事です。

当初 Container Apps 関連を書こうとして、↓ のように「Container Apps で何か書くぞ!」と登録してましたが、最近 Azure Functions でハマってしまい、開発チームとやり取りする中で挙動が見えてきたものの、割とややこしい動きのようなので、それについて書くことにします。

f:id:horihiro:20211205172232p:plain

この実行パーミッションに関する似たような issue がいろんなところで上がるので、需要はありそうだと、勝手に思ってます。

ちなみに書く予定だっ Container Apps の話は、下記サンプルの解説です。
まずは devconatiner なりで動かしてみるのが手っ取り早いと思います。

github.com

Azure Functions Linux 従量課金プランでのプロセス起動

さて本題。

Azure Functions の Linux 従量課金プランは、Windows のようなサンドボックス制限 (Azure Functions の下回りにある App Service 基盤に由来) もなく、かつ、使った分だけの課金なので、割と好んで使っています。
Linux なので関数処理から別プロセスを起動する場合、そのプロセスの実行ファイルに実行パーミッションがついている必要があります。

この外部プロセスを起動するユースケースをいくつか紹介します。

ユースケース 1) Headless Browser

Web ページから PDF を作成する機能が欲しい場合、Azure Functions でも App Service でも、Windows だと上述のサンドボックス制限によって OS の画面描画系 API が使えないので Linux でやるのが近道です。

下記のブログは、Puppeteer や Playwright といった Chromium ベースのHeadless (画面がない)Browser を使って、Web ページのスクリーンショットを撮る例です。

anthonychu.ca

これらの Chromium ベースの Headless Browser を使う場合、内部で chrome のプロセスを起動しているので、chrome に実行パーミッションが割り当てられている必要があります。

ユースケース 2) カスタム ハンドラー

Azure Functions で既定でサポートされていないプログラミング言語を使いたい場合に使用するのがカスタム ハンドラーです。

docs.microsoft.com

このカスタム ハンドラーを使う場合、お好みのプログラミング言語で関数の実行ロジックを含めた HTTP サーバーを作成し、デプロイ パッケージに含めて (+ちょっとした設定する) デプロイします。

この時、Azure Functions のホスト プロセスがカスタム ハンドラーの HTTP サーバーを起動するので、HTTP サーバーを起動するコマンドにも実行パーミッションが必要になります。

デプロイ パッケージ内の実行パーミッションを有効にする設定

というわけで、上記のユースケースで使えるように、実行パーミッションを有効にする設定ですが、細かい説明は抜きにしたい人向けに表にまとめました*1
デプロイ方法や対象のファイルの使い方 *2 によって対応方法が違っていてややこしいです。
※ 2021 年 12 月時点の状況です。

カスタム ハンドラー 一般的な実行ファイル
ZipDeploy WEBSITE_MOUNT_ENABLED01 リモート ビルド … 2
Run from Package - … 3 v3: WEBSITE_MOUNT_ENABLED1
v4: -
4
補足) Linux 従量課金プランでの ZipDeploy

ZipDeploy は、Kudu(Lite) の API /api/zipdeploy に直接 zip パッケージを HTTP POST する、Azure Functions を含む App Service のデプロイ方法です。

docs.microsoft.com

Linux 従量課金プランの場合は少し特殊で、ZipDeploy を使ってデプロイすると、アプリケーション設定 AzureWebJobsStorage で指定された Storage Account 内の scm-release というコンテナーの中に scm-latest-<Function App 名>.zip というファイルで保存されます。
このファイル、拡張子 は .zip となっていますが、中身は SquashFS *3 というディスク イメージです。
なので、squashfs-tools を入れた Linux や WSL 上にダウンロードして、

mount -t squashfs <squashfs ファイル パス> <マウント パス>

のようにマウントすると中身が確認できます*4

実際に ZipDeploy をしてできた SquashFS イメージをマウントして中身を覗いてみると、このイメージ内のファイルには実行パーミッションがないことが確認できます。
HTTP POST された zip パッケージの展開、もしくは、その後の SquashFS イメージの作成の際に外れてしまっているのかもしれません。

(補足ここまで)

以下に、それぞれのデプロイ方法 × 実行ファイルに関する設定について解説します。

1. ZipDeploy × カスタム ハンドラー

カスタム ハンドラーの場合、host.json にどの実行ファイルを使うかが記載されています。
この host.json 内の記載を頼りに Azure Functions のホスト プロセスは起動時にカスタム ハンドラー ファイルに実行パーミッションを追加しようとします。

ただ Linux 従量課金プランの場合、上述の SquashFS イメージをマウントしており、これは読み取り専用のため、実行パーミッションの追加に失敗します。

このケースでは、アプリケーション設定 WEBSITE_MOUNT_ENABLED0 を設定することで、SquashFS ファイルをマウントする代わりに展開するので、実行パーミッションの追加が可能になります。
このアプリケーション設定がない場合、もしくは 1 に設定されている場合は、実行パーミッションは追加できません。

# このアプリケーション設定 WEBSITE_MOUNT_ENABLEDソースコード*5 以外にほとんど出てこないので、挙動を理解するのが大変です。

2. ZipDeploy × 一般的な実行可能ファイル

一般的な実行ファイルについては、カスタム ハンドラーのように、どのファイルの実行パーミッションを有効にしたらいいのかを判断するものがないので、読み書き可能だとしても、一律に実行パーミッションを追加していません。

この場合、ZipDeploy する際にリモート ビルド (Kudu 上で npm install や ビルドなどを行うオプション) を指定すると、実行パーミッションを維持できます。

リモート ビルドは、

  • アプリケーション設定 SCM_DO_BUILD_DURING_DEPLOYMENTtrue にする
    もしくは
  • --build remote オプション付きの func コマンドでデプロイする

のいずれかで有効にできます。

ユースケース 1 のブログではこの方法を使っています。

3. Run From Package × カスタム ハンドラー

Run From Package は、アプリケーション設定 WEBSITE_RUN_FROM_PACKAGE に設定された URL から zip パッケージを取ってきてマウントするデプロイ方法です。

docs.microsoft.com

Run From Package を使った場合、カスタム ハンドラーの実行パーミッションはローカルで zip パッケージを作成した時のものが引き継がれるはずなので、まずは zip パッケージ作成前に実行パーミッションが付いているか確認しするのがよいでしょう。

もしどうしても WSL も Linux も使えない場合は Windows で zip パッケージを作ることになりますが、Windows のファイル システムだと通常は Linux のファイル パーミッションを付けられません。
その場合、Azure Functions Core Tools に同梱されている gozip というツールを使うと、Windows からでも指定のファイルに実行パーミッションを付けたまま zip パッケージを作ることができます。

こんな感じで使います。

gozip -base-dir <基点になるディレクトリ> ^
      -output <出力 zip パッケージ> ^
      -input-file <パッケージ対象ファイルのリスト ファイル名> ^
      -set-executable <実行パーミッションを付けるファイル>

# Go 言語はオプションが - (ハイフン) 一つで始まるんですね。

4. Run From Package × 一般的な実行可能ファイル

Function のランタイム バージョンで挙動が違うようです。

  • v3 ランタイム
    ZipDeploy × カスタム ハンドラー で出てきたアプリケーション設定 WEBSITE_MOUNT_ENABLED を、このケースでは 1 にする必要があります。
    ない場合、もしくは 0 に設定されている場合は、実行パーミッションは引き継がれません。
  • v4 ランタイム
    こちらも Run From Package × カスタム ハンドラー と同様、追加設定なしで、実行パーミッションが引き継がれます。

ランタイムのバージョン設定は、アプリケーション設定 FUNCTIONS_EXTENSION_VERSION~3~4 と指定してください。

まとめ

今回は、Linux 従量課金プランを使用した場合の、実行パーミッションに関する設定をまとめてみました。
ドキュメントにない設定が多いので、ランタイムのアップデートで変わる可能性はありますが、カスタム ハンドラーや Puppeteer/Playwright などの外部コマンドを実行するモジュールを使用する場合は、思い出していただければ。

*1:デプロイ前のローカルファイルシステム上で、実行パーミッションが有効になっていることが大前提です

*2:カスタム ハンドラーなのか、その他一般的なファイルなのか

*3:SquashFS - WikiPedia

*4:SquashFSをマウントするまで

*5:Azure Functions Core Tools, Azure Functions Host

Azure Virtual Network には DNS フォワーダーがあった方がいい

ドキュメントにも書かれているので、知っている人は当然のように知っていることかもしれないですが、自分は最近知ったのでメモがてらに。

docs.microsoft.com

DNS フォワーダー?

DNS リクエストを受け付けて、受け付けた DNS のリクエストを上位の DNS サーバーに転送(フォワード)してくれるものです。

なぜあるといいの?

VNET 内の プライベート IP アドレスの名前解決に Azure Private DNS Zone でレコード管理するのが楽なんですが、この Azure Private DNS Zone が使えるのは、VNET 内のリソースからの名前解決リクエストに限られます (ということを、最近知った次第)。
つまりオンプレから ExpressRoute で接続、または、ローカル PC から直接 P2S VPN で VNET に接続した時、VNET に接続した端末からの名前解決に Azure Private DNS Zone を使うことができません。

この制限を回避するために、一旦 VNET 内の DNS フォワーダーでリクエストを受けて、そのリクエストを DNS フォワーダーから Azure DNS へ転送する、という形を取る、というのが冒頭のドキュメントに書いてあることです。
そうすることで、VNET 内のリソースからのリクエスト、という形になるので、ExpressRoute/VPN 経由のリクエストでも Azure Private DNS Zone が使えるようになります。

DNS フォワーダー用コンテナー

というわけで、お手軽に VNET 内に DNS フォワーダーを立てられるよう、専用コンテナー イメージを作りました。
VNET 内の Azure Container Instances (ACI) で動作確認済みです。

github.com

dnsmasqDNS クエリーをフォワードするだけの、チョーシンプルな distroless ベースの Docker コンテナー イメージです。

ついでに勉強がてら、v~~~ という tag で Release を作るたびに、GitHub Container Registry に push する GitHub Actions を書いてみました。
これh便利。

使い方

Docker コンテナーなので多分 VM とかでも動くんでしょうが、上に書いたとおり、Azure VNET の中の ACI で動かすために作ったので、ACI での使い方を。

まずは ACI をデプロイするところから。

  1. イメージ名に ghcr.io/horihiro/simple-dns-forwarder:<tagName> と入れてください。
    <tagName>latestv0.0.1-alpha1 です (2021/10/02 現在) 。
    https://user-images.githubusercontent.com/4566555/135413167-94e03e07-18f8-4ec6-8391-eafc33442000.png
    特に意味はないけど、GitHub の Container Registry にしてみました。
    github.com
  2. Networking Type を Private にして、配置先の VNET とサブネットを指定してください。
    DNS なので、ポートは 53UDP になります。
    https://user-images.githubusercontent.com/4566555/135404958-d4f3eff2-a4bd-4728-8a6b-b3a9d88058ce.png
  3. 起動コマンドには、
     
    ["dnsmasq", "--no-daemon", "--server", "168.63.129.16"]
     
    と入れてください。 https://user-images.githubusercontent.com/4566555/135405110-d4bae919-94d1-4f11-b9c9-cfe172fbfbb9.png
    最後に指定した IP アドレスが、DNS クエリーのフォワード先です。
    168.63.129.16 は Azure で使われる DNS サーバー用 IP アドレスです。詳しくはこちら。
    docs.microsoft.com
  4. デプロイが完了したら、ACI の [概要] ブレードからプライベート IP アドレスを確認して控えておいてください。この後使います。
    https://user-images.githubusercontent.com/4566555/135694859-6de8ccc5-bc38-4965-99a0-d6e590d8e4e4.png
  5. ACI をデプロイした VNET の DNS サーバー設定で、上で控えたプライベート IP アドレスを設定してください。
    https://user-images.githubusercontent.com/4566555/135694798-b96aa88c-aa2d-483a-b3bd-cdcb4b80559c.png

以下、余談

職業柄、普段から Azure のいろんなサービスをデプロイして検証することが多いです。
その中で、VNET 内に閉じたサービスの動作検証もするのですが、当初は同じ VNET の中にパブリック IP アドレスを持った Windows VM を立てて、使う時だけ起動してリモート デスクトップ経由で VNET 内のリソースにアクセスしていました。

f:id:horihiro:20211002091553p:plain

ただパブリック IP アドレスを持つ VM の管理も手間だし、都度 VM の起動完了を待つ、というのも億劫になったので、いつの頃からか P2S VPN でローカル PC を VNET に直接繋げるようにしています。

docs.microsoft.com

この時、冒頭に書いた Azure Private DNS Zone の制限のため、VPN でつなげたローカル PC からの名前解決に使えませんでした。

特に Private Endpoint 経由で PaaS にアクセスする場合、多くのサービスでは FQDN を使ってアクセスする必要があるので Private DNS Zone に管理してもらうのが楽です。
Private DNS Zone が使えないとパブリック IP アドレスで名前解決されてしまうので、Private Endpoint が有効な PaaS へのアクセス拒否されてしまいます。

f:id:horihiro:20211002091628p:plain

最近まで DNS フォワーダーを使う方法を知らなかったので、ローカル PC の hosts ファイルをいじることで凌いでいましたが、当然 hosts ファイルは Private DNS Zone と連携しないので、二重管理になるだけです。
違う FQDN のリソースが VNET 内に増えるたびに追記していくのもつらいし、使わなくなった時に消すのを忘れて肥大化してしまうので、あまりいいやり方ではないでしょう。

というわけで、今回 DNS フォワーダーを ACI で立てて、VNET に接続する時だけサクッと起動し、用が済んだら終了という構成を試しています。

f:id:horihiro:20211002091650p:plain

コンテナーの起動が割と早いので、ストレスなくいい感じです。

似たような環境を使っている方はお試しを。