ほりひログ

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

Azure Functions & Node.js で async 関数の中で context.done を呼ぶのはやめた方がいいよ、という話

Azure Functions V4 が Public Preview になりました🎉

azure.microsoft.com

で、このエントリーは V4 とは全く関係ないですが、Azure Functions での関数実装について、最近気になったことをいくつかのところで目にしたので書いてみました。

context オブジェクト

Node.js を使った Azure Functions では、各関数の引数に context というオブジェクトが渡ってきます。
これは、context.bindings を通したバインディングや、context.log を通した Azure 基盤側へのログ記録などが行えるオブジェクトです。

その中で、context.done というメソッドがあります。

context.done メソッド

これは、Azure Functions が V1 だった頃は、関数の処理が終わったことを Function Worker 側に伝える必須のメソッドです。

docs.microsoft.com

バインディングのログの情報を一通り詰めて、context.done を呼ぶと、Function Worker から Function Host 側に context オブジェクトが転送され、バインディングの実行やログの記録などが実行される仕組みです。

Azure Functions での Promise/async 関数と context.done

一方で、関数が Promise を返す場合、または V2 以降でサポートされた Javascript の async 関数を使った場合 (Promise を返す) は、関数コード内に明示的に context.done を書かなくてもよくなりました。
関数の戻り値が Promise (厳密には Thenable ) の場合は、Node.js Worker のこの辺り で、context.done が自動的に実行されます。

ですが、V1 から Azure Functions を使っている開発者の中には、context.done オブジェクトを呼ぶことが習慣になっている方がいるようです。

もちろん、Promise を返す関数や async 関数内で context.done を実行すること自体は問題ありません。
context.done は、2 回目の実行時に警告だけログに記録し、その他の処理をスキップするようになっています。

github.com

ただ気を付けてほしい点としては、context.done を実行した時点で関数の処理が終了するわけではない、ということです。
context.done を実行した時点で context オブジェクトにかかわる情報を Function Host に転送しますが、以降の処理はそのまま実行されます。

一方で、バインディングやログなど context オブジェクトにかかわる情報は、context.done の時点で Function Worker から Function Host に転送されて、バインディング処理/ログ記録等々が行われるので、context.done 以降に関連する処理を書いても反映されません。
これが、一見すると、関数処理が終わっているように誤認させる*1一因ですが、以降の関数コードもそのまま実行されるので、気を付けましょう。

例えば、context.done 以降に自分で SDK などを使って外部を呼び出ししている、などは、関数自体が終わっていないので、その呼び出しは実行されてしまい、書き込み用の呼び出しであれば書き込み自体も行われます。

どうするといいの?

async 関数内では、可能な限り context.done の利用はやめましょう。
既存の関数で既に context.done を書いていているのであれば、context.done とセットで return; を使いましょう。

(9/25 追記)
return してもダメなケースもあるので、ちゃんと書きます。

例1: エラー処理の中で「関数終了」を意図して context.done を使っている場合

こんな感じ。

module.exports = async (context) => {
  
  // ︙
  if (error) { // 何かの処理のエラー判定
    // エラー時の処理
    // ︙
    context.done(); // 「ここで終るよね?」 -> いえ、終わりません。
  }
  // 正常時の処理。context.done 以降も実行。
  // ︙
}

context.done のタイミングで関数を終了させたければ、単純に return してしまいましょう。

module.exports = async (context) => {
  // ︙
  if (error) { // 何かの処理のエラー判定
    // エラー時の処理
    // ︙
    return; // 「ここで終るよね?」-> はい、終わります
  }
  // 正常時の処理
  // ︙
}

例2: コールバック関数の中のエラー処理後に context.done を使っている場合

何かの処理をする非同期関数 someFunc のコールバック関数の中で、エラー時の関数終了として context.done を使っていたとします。

module.exports = async (context) => {
  someFunc((err) => {
    if (error) { // `someFunc` のエラー判定
      // エラー時の処理
      // ︙
      context.done(); // 「ここで終るよね?」 -> いえ、終わりません。
    }
    // 処理 A context.done 以降も実行。
    // ︙
  }); 
  // 処理 B someFunc 実行前に実行
  // ︙
}

この場合、処理 A も B も実行されます。
処理 A は context.done 実行後に、処理 B は someFunc のコールバック関数実行前に、それぞれ実行されます。

もしエラー時には処理 B も実行させたくなければ、下記のように Promise でくるんで try - catch すると見通しもよくなります。

module.exports = async (context) => {
  try {
    await new Promise ((resolve, reject) => {
      someFunc((err) => {
        if (error) { // `someFunc` のエラー判定
          // エラー時の処理
          // ︙
          reject(error); // 「ここで終るよね?」 -> 終らないけど、return した時に catch ブロックに飛ばします。
          return;
        }
        // 処理 A
        // ︙
        resolve();
      }) 
    });
    // 処理 B
    // ︙
  } catch {
    // エラー時の処理
    // ︙
  } 
}

(9/25 追記ここまで)

取り急ぎ。

*1: context.done 実行により Function Host へのログ送信を打ち切っているので以降のログが出ません。

Docker Desktop なしで VSCode の DevContainer を使う

Docker Desktop ライセンスが変わり、個人利用か比較的小規模な事業者*1での利用に限った場合だけ、無料になりました。
# Docker CLI、Docker Engine は変わらず無料です。

www.docker.com

というわけで、仕事をする PC で Docker Desktop を使ってお金払わないとライセンス違反になるのでアンインストールしたいのですが、Docker Desktop が WSL2 をサポートして以来、すっかりに頼ってきました。
特に Docker Desktop をアンインストールした場合、VSCode の devcontainer を使った作業ができなくなるのはつらいので、そこだけ早めに確認してみました。

ザックリ結論

  • WSL2 に Docker Engine を入れる
  • Windows 側の VSCode に Remote-Containers 拡張機能を入れる
  • VSCode で開く作業フォルダーは、WSL2 の中のやつ

これをやっておけば、Docker Desktop がなくても、今まで通り devcontainer での作業ができます。

構築手順

そんなにやることないですが、一応まとめます。

1. Docker Desktop アンインストール

もしまだ Docker Desktop が入っていたら、通常の手順に従って完全に消し去ってください。
コントロール パネルから消せます。

2. Ubuntu on WSL2 に docker をインストール

お使いの WSL2 の distro に合わせてお好みで。

docs.docker.com

Ubuntu であれば、コマンドをコピペして apt をガシガシたたいたら入ります。

WSL2 の bash から sudo service docker start などを実行すると動き始めます。

むかーしは cgroup ?的な何か? (おぼえてない) の設定等も必要だった気もしたのですが、今はいらないみたいです*2

VSCode の devcontainer で開く

Remote-Containers 拡張機能は必要です。既に使っているなら入っているままで問題ありません。

ポイントは、VSCode で作業フォルダーを開く時に、

\\wsl$\<ディストリビューション名>\PATH\TO\PROJECT\FOLDER

のように \\wsl$\<ディストリビューション名>\ 以下のフォルダーを開くことです。
中身は、WSL2 の / ディレクトリーですが、Windows からネットワーク フォルダーとして見えます。

f:id:horihiro:20210902192117p:plain

ここを普通に Windows のフォルダーとして開いた後、Remote-Containers の機能で devcontainer で開きなおすだけです。

f:id:horihiro:20210902192256p:plain

なお Git リポジトリーを clone して開く場合も、\\wsl$\<ディストリビューション名>\ 配下のディレクトリーを指定して clone し、その後、Windows フォルダーとして開く -> DevConainer で開きなおす、という手順で開けます。

以上の手順で devcontainer が Docker Desktop なしで開けることがわかりました。

さよなら Docker Desktop 。今までありがとう。

*1:従業員数250人未満、かつ、年間収益1,000万ドル未満

*2:WSL1 の頃?

Azure Functions でBlob 作成をトリガーに関数を実行するパターン

f:id:horihiro:20210828132053p:plain

先日、社内の Hack イベントに参加しました。
その中で、課題シナリオの一部に

‐ Storage Account の Blob コンテナーへの保存をトリガーに、Azure Functions の関数で処理を実行する

という要件があり、改めていくつかの方式を調べて比較したので、まとめてみました。

サポートされているトリガー

現在プレビューも含めて、Blob の保存をトリガーとして Azure Functions は以下の 3 つです。

  • Blob トリガー
  • Event Grid トリガー
  • Event Grid Blob トリガー

それぞれについて簡単な説明と Pros./Cons. をまとめてみました。

結論から見たい人はこちら

Blob トリガー

最初は Blob トリガーです。

docs.microsoft.com

まず最初に試すのはこれではないでしょうか。名前もわかりやすいですし。

仕組み

Blob トリガーは、Azure Functions のホスト プロセスから対象のコンテナーに対して定期的にスキャンを実行し、新規に保存されたファイルがあればトリガーする、という仕組みで実現しています。

Pros.

Blob コンテンツの取得がめちゃくちゃ楽です。

例えば JavaScript の場合、function.json の中で以下のように Blob トリガーを設定すると、context.bindings.myBlob という変数でコンテンツの本体にアクセスできます。

{
  "bindings": [
    {
      "name": "myBlob",
      "type": "blobTrigger",
      "direction": "in",
      "path": "samples-workitems/{name}", // `<コンテナー名>/<Blob 名>` の書式が必要
      "connection":"MyStorageAccountAppSetting"
    }
  ]
}

Cons.

仕組みのところに書いたように、このトリガーは Azure Functions 側からの対象のコンテナーに新規の Blob がないかスキャンしています。
なので、コンテナー内のファイルの数が膨大になると、コンテナーのスキャンに時間がかかるので、その結果、Blob の保存とトリガーのタイミングに差が出ることがあります。
この時間差を回避するため、Blob コンテナーのログ ファイルのスキャンで Blob 作成のログで新規の Blob 作成もチェックもしていますが、ストレージ ログの記録がベストエフォートのため、全てカバーできるわけではありません。

また関数実行は 1 回だけなので、関数内部で例外等が発生し失敗した場合もリトライは行われません。
リトライ処理をしたければ、関数内の例外処理の中で対象 Blob の情報を Queue に退避させて Queue トリガーで再処理、といった処理を自前で実装する必要があります。

あと Blob トリガーの仕様として、必ず対象の Blob コンテナーを一つだけ決める必要があります。
その為、path プロパティの値は必ず <コンテナー名>/<Blob パス> となります。

コンテンツ取得 リアルタイム性 信頼性
Blob トリガー
※ファイル数に依存

Event Grid トリガー

次に Event Grid トリガーです。

docs.microsoft.com

仕組み

これは Storage Account をイベント ソース、Azure Functions をイベント ハンドラーとして、Storage Account で発生したイベントを Azure Functions にリアルタイムに送信する仕組みです。
ちなみに、Azure Functions の Event Grid トリガー関数は、/runtime/webhooks/eventgrid?functionName=<関数名> という特殊な URL を持った HTTP トリガー関数なので、このイベントの送信は、その URL に対する Web Hook を実行する形で実装されています。

Storage Account 側の設定は下記ドキュメントのように、[イベント] ブレードからイベント サブスクリプションを作成するだけです。
エンドポイントについては、Web Hook でも Azure Functions に接続できますが、[Azure 関数] を選べば関数を自動的にピックアップしてくれるので楽チンです。

docs.microsoft.com

トリガー バインディングの設定はこれだけです。

{
  "bindings": [
    {
      "type": "eventGridTrigger",
      "name": "eventGridEvent",
      "direction": "in"
    }
  ]
}

Pros.

イベント ソース側でイベントが発生するとすぐに Web Hook が実行され関数がトリガーされるので、実行は常にリアルタイムです。

また関数側でエラーが発生した場合など、正しい応答が返らなかった場合、Event Grid が自動的にリトライをしてくれるので、信頼性も高いです。

docs.microsoft.com

Cons.

一方で、このトリガーだけで取れるのは、Blob に関するイベントだけでは Blob のコンテンツそのものは取れません。

コンテンツ取得 リアルタイム性 信頼性
Event Grid トリガー ×

Event Grid Blob トリガー

Blob トリガーの簡単なコンテンツ ハンドリングと Event Grid トリガーのリアルタイム性をいいとこどりしたトリガーです。

docs.microsoft.com

仕組み

Event Grid トリガー同様、Event Grid を介して、特殊な URL /runtime/webhooks/blobs?functionName=<関数名> に対して Web Hook が実行されます。

通常の Blob トリガーとの違いは、Azure Functions 側はトリガー用のバインディング設定に "source": "EventGrid" という設定を追加することと、Storage Account 側は Event Grid トリガー同様、[イベント] ブレードからイベント サブスクリプションを作成することです。

Blob コンテンツは、通常の Blob トリガー同様、content.bindings.myBlob で取得可能です。

{
  "bindings": [
    {
      "name": "myBlob",
      "type": "blobTrigger",
      "direction": "in",
      "path": "samples-workitems/{name}", // `<コンテナー名>/<Blob 名>` の書式が必要
      "source": "EventGrid",
      "connection": "MyStorageAccountConnectionString"
    }
  ]
}

Pros.

上に書いた通り、Blob トリガーのコンテンツ ハンドリングと Event Grid トリガーのリアルタイム性を、1 つのトリガー バインディング設定で実現できます。

Cons.

path プロパティについては、通常の Blob トリガーと同様なので、対象の Blob コンテナーを 1 つに決める必要があります。

また、この Event Grid Blob トリガーを使うには、Microsoft.Azure.WebJobs.Extensions.Storage の v5 以降が必要になります。
これはまだプレビューなので、サポートがベスト エフォートになります。

コンテンツ取得 リアルタイム性 信頼性
Event Grid Blobトリガー
※プレビュー

Event Grid トリガー + Blob バインディング

ここまでは単独のトリガー設定でしたが、他の機能 (入力バインディング) と組み合わせることで、さらにいいとこどりもできます。

仕組み

トリガー自体は Event Grid トリガーをそのまま使い、Blob 入力バインディングで Blob のコンテンツを取得します。

これは、Blob 入力バインディング設定の path に EventGrid トリガーで受信する JSON ペイロードに含まれる Blob の URL を設定することで実現できます。

{
  "bindings": [
    {
      "type": "eventGridTrigger",
      "name": "eventGridEvent",
      "direction": "in"
    },
    {
      "type": "blob",
      "direction": "in",
      "name": "myBlob",
      "path": "{data.url}", // Event Grid トリガーからの JSON ペイロードから取得するバインディング式
      "connection": "MyStorageAccountConnectionString"
    }
  ]
}

Pros.

バインディングを組み合わせていますが、それぞれはサポートされているもので安心して使えますし、必要な設定/情報は Event Grid Blob トリガーと同じです。

Event Grid トリガーをそのまま使っているので、リアルタイム性も高くリトライもサポートしていますし、コンテンツのハンドリングについては、Blob 入力バインディングcontext.bindings.myBlob から簡単にアクセスできます。

また Blob トリガーと違って、実は Blob 入力バインディングではコンテナーを 1 つに定める必要はないので、全てのコンテナーに対する処理ができます。

Cons.

弱点は、バインディング設定が 2 つ必要、ということくらいでしょうか。

コンテンツ取得 リアルタイム性 信頼性
Event Grid トリガー
+ Blob 入力バインディング

※複数 Blob コンテナーに対応

まとめ

いくつかある Blob の保存をトリガーとした関数の実装方法について、機能面から比較をしてみました。

コンテンツ取得 リアルタイム性 信頼性
Blob トリガー
※ファイル数に依存
Event Grid トリガー ×
Event Grid Blobトリガー
※プレビュー
Event Grid トリガー
+ Blob 入力バインディング

※複数 Blob コンテナーに対応

ご参考になれば。

Visual Studio Code で Azure Functions のデバッグができなくなった(けど直った)話

あまり需要はなさそうだけど記録用に*1

はじめに

タイトルの通り、ふと気づいたら Visual Studio Code 上で開発する Azure Functions のプロジェクトで、デバッグができなくなっていました。
症状としては、Node.js の関数プロジェクトを作りデバッグを開始すると、func コマンドは起動するものの、少したつと以下のダイアログが出て、デバッガの起動に失敗する、という感じです。

f:id:horihiro:20210823074408p:plain

曰く、「localhost の 9229 ポート (Node.js の inspector のポート ) につながらない」と。

Visual Studio Code 上での Node.js の Azure Functions のデバッグは、inspector を有効にした状態で Node.js の Worker (JavaScript の関数コードをロードして実行するプロセス) を 起動し、その Worker に Visual Studio Code からアタッチする形で行います。
なので、Worker の 9229 ポートにアタッチができず、デバッグが開始できない、と見てよさそうです。

対処方法

トラブル シューティングの過程は長くなったので、さっさと直したい人向けに先に結論を。

この現象の原因は Logic App 用の Visual Studio Code 拡張機能 Azure Logic Apps (Standard) が有効になっていることでした。

marketplace.visualstudio.com

この拡張機能を無効化すると、無事 Azure Functions のデバッグが可能になりました。

似た症状に遭遇した場合、Azure Logic Apps (Standard) が入っているか・有効になっているかを確認して、少なくとも Workspace レベルで無効にした方がよさそうです。

f:id:horihiro:20210823083834p:plain

原因についての考察はこちら

トラブルシューティング

ここからは、紆余曲折のトラブルシューティングの記録です。
結果としてあまり意味のないこともやっていました。

状況確認

まずは 9229 ポートにアタッチができない、という状況が何なのか確認です。

1. 9229 ポートは正しく使われているか?

netstat を使って、デバッグ開始前後の 9229 ポートの利用状況を見てみます。
netstat -nao でプロセス ID まで追えるので、9229 ポートが使われていれば、func から起動された Worker プロセスが正しく使っているのか、他のプロセスに占拠されているのかわかるはずです。

結果はこちら。

f:id:horihiro:20210823075621p:plain

デバッグ開始前と開始直後で確認しても、9229 ポート使っているプロセスは見つかりませんでした。
だれも使ってないのに Worker が 9229 ポートを使おうとしていません。

2. inspector は正常に起動しているのか?

次に Worker で inspector を有効にしようとしているのか確認してみます。
そもそも inpector を有効にしていなければ、アタッチできるはずがありません。

起動オプションは、タスク マネージャーで コマンド ライン 列を表示することで確認ができます。

inspector を有効にする場合は、--inspect=9229 が Node.js プロセスの起動オプションについているはずですが、結果はこの通り、--inspect オプションがありません。

f:id:horihiro:20210823075043p:plain

inspector を有効するオプションがないので、9229 ポートが使われるはずがありませんね。

# 正しくはこうです。

f:id:horihiro:20210823075005p:plain

Azure Functions 拡張機能デバッグ時に何をしているのか?

では Azure Functions 用拡張機能は、どのように --inspect オプションを付けているのでしょうか?

拡張機能のソース コードで、Node.js 用のデバッガ起動の処理、NodeDebugProvider.ts の 29 行目に --inspect オプションが書いてあります。

f:id:horihiro:20210823084753p:plain

github.com

この関数は、FuncTaskProvider.ts の以下の 箇所から呼ばれ、env というオブジェクトにセットされています。
環境変数っぽいですね。

github.com f:id:horihiro:20210823083502p:plain

環境変数名になる debugProvider.workerArgKey() は、上の NodeDebugProvider.ts に戻って 22 行目で languageWorkers__node__arguments と定義されています。

github.com

つまり、デバッグ用の func コマンドを起動する時、環境変数 languageWorkers__node__arguments の値として --inspect=9229 がセットして、func コマンドがその環境変数の値 --inspect=9229 を Worker である Node.js プロセスの起動オプションに渡す、というのが正しい動作です。

ちなみに languageWorkers__node__arguments についてググってみると、ここに書いてありました。

docs.microsoft.com

環境変数は設定されているのか?

環境変数 languageWorkers__node__arguments がカギになるので、デバッグに失敗した時の func コマンドの環境変数を確認したいところ。
でもタスク マネージャーでの確認方法がわからなかったので、Process Explorer で確認してみます*2

L または l から始まる環境変数で、それらしきものはありません。

f:id:horihiro:20210823095207p:plain

func プロセスに環境変数が設定されていないので、何らかの原因で上のデバッガ起動処理を通っていないようです。

# こちらも、こうなるのが正解です。

f:id:horihiro:20210823095339p:plain

拡張機能が動いていない???

デバッグ起動処理を通らない、となると、Visual Studio Code 自体のコードを追わないといけなく、正直やってられないので、ここは初心に戻って、一度 Azure Functions 拡張機能だけで試してみます。

と言っても、他の拡張機能をアンインストールして、、、とやると復元するのもつらくなるので、以下のように適当な空っぽのフォルダーを拡張機能用フォルダーとして起動します。

code --extensions-dir=C:\extensions_debug

すると、拡張機能がまっさらな状態の Visual Studio Code が起動するので、Azure Functions 拡張機能だけ入れます (といっても、いくつかの依存モジュールが自動で入ります)。

f:id:horihiro:20210823101316p:plain

このシンプルな状態で対象のフォルダーを開いて、デバッグを開始してみると、、、

f:id:horihiro:20210823101915p:plain

Debugger attached. できてますね。。。

Azure Functions 拡張機能と最低限の依存モジュールだけだと、正常にデバッガが起動します。
ということは、何か他の拡張機能を入れた時にこの現象が発生するっぽいので、ここからは元の環境に入れていた拡張機能一つ一つを、入れてはデバッガ起動、入れてはデバッガ起動、、、と繰り返してみます。

すると、Azure Logic Apps (Standard) を入れた時点でデバッガの起動に失敗することがわかりました。
こいつが犯人です。

考察編

Visual Studio Code の Azure Functions 拡張機能は、package.json で以下のように宣言して、特定の task をハンドルするよう要求しています。

        "taskDefinitions": [
            {
                "type": "func",
                "required": [
                    "command"
                ],
                "properties": {
                    "command": {
                        "type": "string"
                    }
                }
            }
        ],

github.com

この宣言により、その拡張機能がどのような type の task をハンドルするかを指定することができます。
上記の宣言の場合、以下のような typefunc の task が対象になります。

  {
    "type": "func",
    "command": "host start",
      :
  }

一方で、Logic Apps 用の拡張機能を見てみると (ソースコードリポジトリーが公開されていないので、インストールされた拡張機能を直接確認) 、package.json で全く同じ宣言、typefunc の task をハンドルすることを要求しています。

つまり Azure Functions 拡張機能と Logic Apps 拡張機能、2 つの拡張機能が同じ task をハンドルする宣言をしている状態です。

ここからは推測になりますが*3、Logic Apps 拡張機能を入れた時点からデバッガが正常に起動できなくなったことから考えると、何故か必ず先に Logic Apps 拡張機能の方で func の task が処理されてしまい、Azure Functions 拡張機能の方で --inspect=9229 を付与する処理まで到達してない、という流れのようです。

なので、Azure Functions 開発を進めるにあたっては、現時点では Logic Apps 拡張機能*4を無効にしておく方がよさそうです。

ちなみに、Logic Apps 拡張機能には Azure の古いロゴ f:id:horihiro:20210824090203p:plainも同梱されているので、この拡張機能を入れると activity bar のアイコンも古くなります。

*1:Twitter 等でも日本語で類似の現象を見つけられなかったので、特殊な状況かもしれない。

*2:ここで Process Explorer を使うなら、起動オプション確認もタスク マネージャーじゃなくてよかった、と後悔

*3:Visual Studio Code 自体と Logic Apps 拡張のソースコードを確認していないので。

*4:同じ task 処理を要求する拡張機能も同じ対応が必要

Azure Static Web Apps から VNET のリソースを使う

はじめに

夏休みの宿題として、即席 POST を。

Static Web Apps を使って Web サイトを構築する時、VNET 内で保護されたストレージやデータベースの中のデータを使ったり、そのデータを更新したりしたい、かもしれない。

でも Static Web Apps には VNET につなぐ機能 (App Service でいうところの VNET 統合) がない。
# 静的サイトのホスティングなんだから、当然な気もする。

そうだ!

Static Web Apps は、バックエンド API として Function App が使えるじゃないか!
Function App の VNET 統合を使って、VNET 内につなごう!!

構成

つまりはこう。

f:id:horihiro:20210727105346p:plain

構成のポイント。

  1. Static Web Apps の後ろに Function App
  2. ストレージ アカウントやデータベースは、プライベート リンクを使って VNET 外からのアクセスを全力で拒否
  3. Function App は VNET 統合で、2. のプライベート リンクと同じ VNET に接続

やってみる

Azure Functions を VNET 統合する

docs.microsoft.com

全く難しいことはなく、ポータルからポチポチしたら設定完了。

最近プレビューで出てきた [ネットワーク (プレビュー)] ブレードも見やすくていい感じ。

f:id:horihiro:20210727203435p:plain

注意点は以下の通り

  • VNET 統合が使える Function App のプランは、Elastic Premium プラン、または、Standard 以上の App Serviceプラン
  • 統合先の VNET と Function App のリージョンは同じ
  • VNET には、Function App からの VNET 統合専用、つまり空っぽの のサブネットが必須

一番の注意点は一つ目、 Elastic Premium プラン、または、Standard 以上の App Serviceプラン
Static Web Apps 付属の Function App は、Linux の従量課金プランなので、そのままでは VNET 統合が使えない。

なので、Static Web Apps の Bring Your Own Functions を使って、VNET 統合が使える Function App をリンクしてみる。

Static Web Apps で自前の Function App を使う (Bring Your Own Functions)

ドキュメントはこちら。

docs.microsoft.com

何故かこれだけ英語。

でも VNET 統合よりも設定は簡単で、ポータルから [関数] ブレードからポチポチしてリンク対象の Function App を選ぶだけ。

f:id:horihiro:20210727204236p:plain

結果

目論見通り、Function App からのアクセスは VNET 内のリソースにつながるので、関数のトリガーは HTTP にしてバックエンド API として作り、VNET 内にあるストレージ アカウントやデータベースから入力バインドで取ってきたデータを HTTP のレスポンスにする、なんてことも簡単にできた。

バインディング、最高。

諸注意

一度 Static Web Apps と Function App をリンクすると、そのリンクを解除した後も Function App への HTTP アクセスで 401 が返ったままになり、まともに使えない。

これは、Static Web Apps とリンクした際に、Function App 側の [認証] 機能が有効になることが原因。
# 何故かドキュメントには見当たらない

なので、元に戻したければ下図のように、未認証のアクセスを許可するよう変更する。

f:id:horihiro:20210727105414p:plain

これでリンク前の状態に戻って HTTP アクセスが可能になる。( ホッ

最後に

Qiita のエンジニア フェスタ 2021 で Static Web Apps をテーマにした記事を絶賛募集中です。

qiita.com

f:id:horihiro:20210727210246p:plain

4K 42 インチ モニターとかが当たる (かもしれない) らしいです。ほしい。

Web App for Containers でセキュアな ACR から docker pull する + おまけ

f:id:horihiro:20210706155005p:plain

Web App for Containers でコンテナー イメージを docker pull する際に、Private Endpoint で保護されたセキュアな Azure Container Registry (ACR) からの docker pull も可能になりました!👏👏👏

元ネタはこちらの、Azure App Service チームのブログです。

azure.github.io

背景的なアレ

Web App for Containers も含めた App Service では VNET 統合を使用することで、App Service から VNET 内のリソースにアクセスできるようになります。
でも実は、ACR 側で Private link を使って、かつ、パブリック アクセスを拒否することで ACR をセキュアにすると、今までは Web App for Containers から ACR から docker pull できない (アクセスが拒否される) という状況でした。

イメージとしては、docker デーモンを起動しているホスト マシンでの docker pull 時の通信が、VNET に向けられていない、という感じです。
# あくまでイメージなので、正確なところはわかりません。起動した docker コンテナーは VNET 内のリソースへのアクセスはできます。

これが docker pull の段階から VNET 経由で ACR にアクセスできるようになった、というお話です。

事前設定

まず Web App for Containers を VNET 統合しておきます。これが大前提。

次に ACR の Private Link を有効にしておきます。
ACR での Private Link の利用についてはこちら。

docs.microsoft.com

Azure ポータルから ACR の [Networking] ブレードからポチポチしていくとできるので、そんなに難しいことはありません。

f:id:horihiro:20210706104453p:plain

Private Endpoint を作成する VNET は、Web App for Containers が VNET 統合しているものと同じものを選択しましょう (サブネットはそれぞれ専用のものになるので、違っていても大丈夫) 。

なお ACR からの docker pull に Web App for Containers の Managed Identity は使えないので、ACR の [Access keys] ブレードから Admin user を有効にしておく必要があります。

f:id:horihiro:20210706112628p:plain

元ブログにも書いてありますが、見落としてて数十分無駄にしたので、自分用の備忘録に。

We need Premium SKU to enable private endpoint and currently admin access must be enabled:

# なぜ元ブログのトップ絵には、Managed Identity があるのかw

キモになる設定

ここからは Web App for Containers 側の設定です。

1. docker pull を VNET 越しにする設定

この度、WEBSITE_PULL_IMAGE_OVER_VNET というアプリ設定が仲間入りしました!
いかにも VNET 越しに docker pull しそうな設定名です。

ちなみに、Web 検索してみたら、2021/7/6 現在で 2 件ほどしかヒットしませんので、今なら検索トップも夢ではありません!!(ォィ

az webapp config appsettings set --resource-group secureacrsetup --name secureacrweb2021 --settings 'WEBSITE_PULL_IMAGE_OVER_VNET=true'

元ブログでは Azure CLI を使って設定していますが、もちろん Azure ポータルから設定しても大丈夫です。 f:id:horihiro:20210706113413p:plain

2. Outbound 通信を全て VNET 経由にする設定

Outbound を全て VNET 経由にする、と言えば、みんな大好き WEBSITE_VNET_ROUTE_ALL=1 が思い浮かびますが、上記ブログには別の方法が書かれています。

az webapp config set --resource-group secureacrsetup --name secureacrweb2021 --generic-configurations '{"vnetRouteAllEnabled": true}'

こちらも見慣れない vnetRouteAllEnabled というプロパティを true に設定しています。
見慣れないですが、またもや名前から動作が想像できるプロパティです。名前重要。

Resource Explorer で確認してみると、確かにありますね。

f:id:horihiro:20210706110652p:plain

ARM テンプレートのリファレンスを確認してみると、api-version=2020-06-01 から SiteConfig の中にあるようなので、恐らく 1 年位前には既にあったのかもしれません 。
へー、知らなかった。

f:id:horihiro:20210706114058p:plain

docs.microsoft.com

ちなみに動作としては WEBSITE_VNET_ROUTE_ALL=1 と全く同じのようで、WEBSITE_VNET_ROUTE_ALL=1vnetRouteAllEnabled": true 、どちらかが設定されていれば OK です。
アプリ設定で管理したい場合は、WEBSITE_VNET_ROUTE_ALL を使えばいいでしょう。

意外だったのが、WEBSITE_DNS_SERVER=168.63.129.16 が要らないことです。

試しに sshd 入りのコンテナーを動かして、その中で nslookup してみると、アプリ設定 (=環境変数) WEBSITE_DNS_SERVER がないコンテナーでも、ACR の IP アドレスがプライベート IP アドレスとして解決できています。

f:id:horihiro:20210706121809p:plain

ちょっとこのあたりについては、どの時に WEBSITE_DNS_SERVER=168.63.129.16 が必要でどの時にいらないのか、まだ勉強中です。

以上の設定で、無事 Private Link を使って Private Endpoint 経由のみにアクセス制限した ACR 上のコンテナー イメージを、Web App for Containers から docker pull できるようになります。
お試しあれ。

おまけ ~ Private DNS Zone を削除する

そんなに使い機会はないんですが、こちらも備忘録に。

Private Link を試してみて、その後不要になれば当然リソースを削除すると思いますが、その時に ACR をプライベート IP アドレスを解決するための Private DNS Zone リソースも消そうとするとたまに見るのがこれ。

f:id:horihiro:20210706122253p:plain

意訳すると「子リソースを消してないのに消せねぇって」といったところでしょうか。
ただ、関係しそうなリソースをあらかた消しても、 Private DNS Zone を消す時だけはこれが出て失敗します。

そんな時は、Private DNS Zone があるリソース グループで隠しリソースを表示してみてください。

f:id:horihiro:20210706122649p:plain

リソース タイプが microsoft.network/privatednszones/virtualnetworklinks で、試した Private Link にいかにも関連してそうなリソースが出てきます。
これを先に消してしまえば、Private DNS Zone も消せるようになります。

(一緒に消してくれたらいいのに。ボソッ

以上、おまけでした。

Function App から全ての接続文字列から解放してみた (かった)

f:id:horihiro:20210529170353p:plain

Microsoft Build 2021

先週やってました。

が、App Service や Azure Functions 関連のセッションは少なかったですね。。。
アップデートとしてはこのあたりです。

  • GA
    • App Service Managed Certificates
    • Azure Static Web Apps
    • PowerShell Durable Functions
  • Preview
    • Durable Functions backend providers (Microsoft SQL server / Netherite)
    • OpenAPI extension for Azure Functions
    • Azure Application Services on Kubernetes with Azure Arc

ASE v3 あたりは GA に向けての機能整理も進んでいるようなのでセッションあるのかな、とは思っていたのですけどね。
Azure Application Services のインパクトが大きかった感じ。

本題

Build とは全く関係ありません。

Azure SDK が更新されて、Blob Storage、Queue Storage、Event Hubs、Service Bus、Event Grid への接続で、接続文字列を使用しないマネージド ID / クライアント シークレット / クライアント証明書での接続がサポートされました (ベータ版です)。

devblogs.microsoft.com

Function App でも WebJobs Extension を介してこれが使えるらいしいので、アプリケーション設定から接続文字列が一掃できるのでは、という期待を胸に試してみました。

実験

今回は Timer トリガー関数に Queue ストレージの出力バインディングを足したもので試してみます。
なので、本来は Timer トリガーで使うアプリケーション設定 AzureWebJobsStorage と、Queue ストレージの出力バインディング用アプリケーション設定、これら 2 つのアプリケーション設定で接続文字列を使っているはずです。

この関数が接続文字列無しで、マネージド ID を使って問題なく動作するかを試してみました。

デプロイする関数コードと function.json

まずは関数コード。

module.exports = async function (context, myTimer) {
    var timeStamp = new Date().toISOString();
    
    if (myTimer.isPastDue)
    {
        context.log('JavaScript is running late!');
    }
    context.bindings.outputQueueItem = {datetime: new Date()};
    context.log('JavaScript timer trigger function ran!', timeStamp);   
};

VScode拡張機能が作ってくれるテンプレートに、Queue ストレージへの出力バインディング

    context.bindings.outputQueueItem = {datetime: new Date()};

の 1 行を足しただけです。

次に function.json

{
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 * * * * *"
    },
    {
      "type": "queue",
      "direction": "out",
      "name": "outputQueueItem",
      "queueName": "outqueue",
      "connection": "withoutConnectionString_STORAGE"
    }
  ]
}

上半分は Timer トリガーの設定、下半分が Queue の出力バインディングです。
本来は connection に指定した名称のアプリケーション設定に接続文字列を格納しているはずです。

ここはほぼテンプレ通りです。

ベータ版 WebJobs Extension の追加

ベータ版 WebJobs Extension を追加するため host.json にはひと手間加えます。

host.json に Extension Bundle (拡張機能一式) に関する記述があれば、それをを削除して Extension Bundle のロードをやめます。
これは、この Extension Bundle の設定があると、この後のベータ版の WebJobs Extension を追加できないためです。

f:id:horihiro:20210529141120p:plain
# 選択部分を削除。

次に、ターミナルで下記コマンドを実行して、(Blob と Queue を試したいので) Storage 用 の WebJobs Extension の最新ベータ版を改めて入れます。

func extensions install --package Microsoft.Azure.WebJobs.Extensions.Storage --version 5.0.0-beta.4

まだデプロイしません。

マネージド ID の有効化

今度はデプロイ先になる Azure 側の事前設定をします。

Function App*1Indentity ブレードでポチっとやるだけです。
f:id:horihiro:20210529155950p:plain

RBAC 設定

Storage Account (AzureWebJobsStorage と Queue バインディングでの出力先) の Access Control (IAM) ブレードに移動し、上で有効化したマネージド ID に対して、Storage Account のデータ操作に関する権限を与えます。
f:id:horihiro:20210529160212p:plain

AzureWebJobsStorage 設定については Blob ストレージを操作するので Storage Blob Data Contributor 、あと出力バインディングでの Queue ストレージに対する操作 (Queue 自体の作成とメッセージ追加) のために Storage Queue Data Contributor を与えています。

今回 AzureWebJobsStorage と Queue バインディングで使う Storage Account を同じものにしているので、一つの Storage Account に二つのロール割り当てをしました。 もしこれらが別の Storage Account だったり、バインディングやトリガーで Event Hubs や Service Bus を使う場合は、それらのリソースの Access Control (IAM) ブレードにて、マネージド ID に対する適切なロールを割り当てましょう。

アプリケーション設定の変更

アプリケーション設定の名称と値を変えます。

こちらが変更前の設定。
# フィルター初めて使った

f:id:horihiro:20210529161117p:plain
AzureWebJobsStorage と Queue バインディング用アプリケーション設定 withoutConnectionString_STORAGE ( function.jsonconnection で指定) 、それぞれに接続文字列が入っています。

これらのアプリケーション設定から変更します。
f:id:horihiro:20210529161908p:plain

AzureWebJobsStorageAzureWebJobsStorage__accountName に変更し、その値は Storage Account 名をズバリ書きます。

バインディング用の設定 withoutConnectionString_STORAGEwithoutConnectionString_STORAGE__queueServiceUri に変更し、値は https://${accountName}.queue.core.windows.net という書式で指定します。

こちらにはもっといろいろ設定名が書いてありますが、マネージド ID を使っているせいか、特に不要のようです。

最後にデプロイ

関数コードをデプロイすると、AzureWebJobsStorage や接続文字列がなくても、Timer トリガーの関数が毎分実行され、Queue にメッセージが追加されるはずです。

めでたしめでたし。

ちなみに

従量課金プランと Premium プランの Function App の場合、D:\home *2 としてマウントしているファイル/フォルダーの実態が File ストレージ内にあり、その設定には File ストレージへの接続文字列を格納した WEBSITE_CONTENTAZUREFILECONNECTIONSTRING というアプリケーション設定が必要です。

今回のベータ版は Blob と Queue に関する更新で、File ストレージについては未対応なようなので、File ストレージを使うこれらのプランでは、接続文字列を一掃することができませんでした。悔しい。
あと Application Insights への接続文字列も残ってました。

*1:個々のリソースが Function App、サービス名が Azure Functions という使い分けをしています。

*2:場合によっては C:\home