ほりひログ

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

GitHub Codespaces を使って、Web ブラウザーだけで Static Web Apps アプリを開発/デバッグしてみる

f:id:horihiro:20210516074018p:plain

Azure Static Web Apps

ついに、一般提供を開始しました! azure.microsoft.com

サポート エンジニア時代にいたチームはこの製品も担当していたので、割と早い段階 (パブリック プレビュー開始の前あたり) から知っている製品でした。
でも正直「静的ファイルや SPA のホスティングなんて、Web Apps でやったらいいじゃない」と思い、あまり本腰入れて試していませんでした (ごめんなさい💦)。

が、ある程度試していく中で、Web フロントエンドに特化した製品であることが理解でき、「なるほど便利だ」とひとしきり反省しております。

確かに Web Apps だけで SPA をホスティングしようとして、ハマることもあるんですよね。

uncaughtexception.hatenablog.com

今もこの問題が残っているか確認してませんが、ビルド周りの違いだけではなく、Azure Static Web Apps はグローバル サービスなので、常にクライアントに最も近いエッジ サーバーから配信される、というメリットもあります。
SPA に特化したホスティングにはオススメですね。
# Web Apps も大好きです。適材適所です。

その他の機能、日本語ではこちらの三宅さんのブログ、公式ドキュメントに網羅されているので、興味のある方はこちらをご覧ください。

k-miyake.github.io

docs.microsoft.com

本題

Static Web Apps 猛者の三宅さんのツイートを発端に疑問が。

ということで、
GitHub Codespaces/Web ブラウザーだけを使って、Azure Static Web Apps に載せる SPA と Serverless API の開発/デバッグができるか
を試してみました。

GitHub Codespaces

github.com

ざっくりいうと、

Web ブラウザー上で、VSCode 風(というかそのもの)な UI で、GitHub リポジトリーのファイルを直接?編集できる

というものです。

GiHub Codespaces を使えば、コーディングはできます。
あと GiHub Codespaces からリポジトリーに直接 git push ができるので、GitHub Actions 経由で Azure Static Web Apps へのコードのデプロイもできます。

では、デバッグはどうなのか。

結論

  • SPA の Javascript は、Web ブラウザーの開発者ツール
  • バックエンドの Serverless API は、GiHub Codespaces の Dev Container 上の Azure Functions Core Tools

を使うと、ブラウザーだけで両者のデバッグが同時に可能です。

さらに GiHub Codespaces の Dev Container 上で Static Web Apps CLI を動かして、認証関連の動きもエミュレートできるようになります。

docs.microsoft.com

tasks.json

GitHub Codespaces はデバッグ機能もほぼ VSCode なので、.vscode/tasks.json に個々の task を書いていけば VSCode 同様にデバッグ実行ができます。

SPA ビルド用 task

プロジェクト ルートで npm run build しています。vue であれば vue-cli-service build が実際のコマンドになります。

    {
      "type": "shell",
      "label": "npm run build",
      "command": "npm run build",
      "dependsOn": "npm install (for app)",
      "options": {
        "cwd": "${workspaceFolder}"
      }
    },

あと webpack でのビルド設定などで、source map を出力するようにします。
これでブラウザーの開発者ツール上で Break point が設定できます。
vue であれば、vue.config.js に下記の configureWebpack 設定を追加をしておきます。

module.exports = {
    // : 
    configureWebpack: {
        devtool: 'source-map'
    },
    // : 
}

この task はビルド結果 (バンドルされた JS ファイルなど) を ./dist などに出力して終了です。
# Webpack Dev Server を使ってない (vue-cli-service serve してない) 理由は後述

バックエンド Serverless API デバッグ開始 task

Azure Functions Core Tools でバックエンドの Serverless APIデバッグ実行します。
./api に Azure Functions のプロジェクトがあるとして、そこで func host start を実行しています。

オプション --language-worker -- \"--inspect=9229\" を追加して、GitHub Codespaces 上のデバッグ プロセスから Launguage Worker プロセスにアタッチできるようにしています。

    {
      "type": "shell",
      "label": "func start",
      "command": "func host start --language-worker -- \"--inspect=9229\"",
      "problemMatcher": {
        "owner": "custom",
        "pattern": {
          "regexp": "^$"
        },
        "background": {
          "activeOnStart": true,
          "beginsPattern": "^.*(Job host stopped|signaling restart).*$",
          "endsPattern": "^.*(Worker process started and initialized|Host lock lease acquired by instance ID).*$"
        }
      },
      "isBackground": true,
      "dependsOn": "npm install (for api)",
      "options": {
        "cwd": "${workspaceFolder}/api"
      }
    },

この task を実行すると、通常の Azure Functions プロジェクトのデバッグと同様、http://localhost:7071 動作し続けます。

Static Web Apps CLI 起動用 task

npm run build で出力した ./dist を Static サイトに、func host start で動かした http://localhost:7071 をバックエンド API に指定して、Static Web Apps CLI を実行しています。

# npm script に引数を渡すために -- が必要、ということをここで知りました。

    {
      // 
      "type": "shell",
      "label": "swa start",
      "command": "npm run swa -- start dist --api=http://localhost:7071",
      "isBackground": true,
      "problemMatcher": {
        "owner": "custom",
        "pattern": {
          "regexp": "^$"
        },
        "background": {
          "activeOnStart": true,
          "beginsPattern": "^.*Using dev server for static content .*$",
          "endsPattern": "^.*Azure Static Web Apps emulator started.*$"
        }
      },
      "dependsOn": [
        "npm run build", 
        "func start"             
      ],
      "options": {
        "cwd": "${workspaceFolder}"
      }
    },

dependsOn で上の 2 つの task を指定しているので、これらの task が完了状態になってから実行しています。

launch.json

上のタスクを実行する GitHub Codespaces のデバッグ設定は、Azure Functions 用の設定をマルっとパクります。

        {
            "name": "Launch Static Web Apps Dev Server and Backend Functions",
            "type": "node",
            "request": "attach",
            "port": 9229,
            "preLaunchTask": "swa start"
        }

これで GitHub Codespaces がバックエンド Serverless APIデバッグ プロセスにアタッチします。

その他

Dev Container で Azure Functions Core Tools を入れたり、devDendencies の一つとして Static Web Apps CLI を入れたりしてますが、まとめたものはこちらにあるので興味があれば。

github.com

動作の様子

デバッグ実行の様子はこちら。

Dev Container 内の Static Web Apps CLI のサーバー プロセスには、

https://${ユーザー名/組織名}-${リポジトリー名}-${4桁のランダム文字列}-${フォワード先のポート番号}.githubpreview.dev/

という URL でアクセスできます。

PORTS のリストに URL が出てくるので、Static Web Apps CLI は 4280 で待ち受けているので、それにフォワードしている URL を選びましょう。

f:id:horihiro:20210516141530p:plain

現時点でわかっているできないこと

実はいくつかのことが、このデバッグ構成ではできません。

ログアウト時のルーティング

この GitHub Codespaces の構成でログアウト (/.auth/logout にアクセス) すると、必ず http://localhost にリダイレクトされます。
当然、ローカルでサーバーを実行していなければエラー画面が表示されます。

どうやら、上の URL へのリクエストは Dev Container 内の Static Web Apps CLI のプロセスに届く前にリバース プロキシを通っているようで、リクエストの host ヘッダーには localhost 、本来リダイレクト先に使いたい上の URL のホスト名は x-forwarded-host ヘッダーに入ってきます。

一方で、Static Web Apps CLI の開発サーバーのログアウト時のリダイレクト先は、以下のように host ヘッダーだけを見ているので、http://localhost にリダイレクトされるようです。

  const host = req?.headers?.host;

PR は出していますが、このリバース プロキシ環境でのデバッグ、ということ自体が想定外な気がするので、マージされるか怪しいですね。

github.com

SPA の Hot reload

SPA 用の task で、npm run serve (Webpack Dev Server でのホスティング) ではなく npm run build./dist にバンドルされた JS ファイル作成していました。
なので、SPA 側のファイルを更新しても、自動でリロードしてくれません。

当然、SPA 側を npm run serve で WebPack dev server で起動し、Static Web Apps CLInpm run swa -- start http://localhost:8080 --api=http://localhost:7071 のように実行すると Hot Relaod が可能になります。
ただし、Static Web Apps CLI の現在の動作として、Dev Server を使った場合に staticwebapp.config.json をロードしないので、認証や独自のルーティングが動きません。

github.com

Static Web Apps CLI のルーティングを改善しようとしているそうなので、それと合わせて改善されるようです。

ただ、GitHub Codespaces の場合、ちょっとした変更でもファイルは自動で保存されるので、その度に Hot Relaod が動くのもどうかと思うので、Hot Relaod ではなく都度手動で npm run build を実行する方がいい気もします。
このあたりは、ファイル保存のタイミングをコントロールできるローカル環境と違いが出る部分ですね。

以上、だいぶニッチにエントリーでした。

Azure Web PubSub Public Preview

Azure Web PubSub がパブリック プレビューになりました。

docs.microsoft.com

azure.github.io

Azure Web PubSub って何?

Azure 上で WebSocket サーバーをホスティングして、Web アプリケーションでの pub/sub が簡単にできるようにする PaaS サービスです (たぶん。

ドキュメントをちゃんと読むのはこれからですが、似たようなサービスである Azure SignalR Service との違いは、こちらの Azure Web PubSub は WebSocket 通信に特化したサービス、という点ですかね。
なので、追加ライブラリーなしで Web ブラウザー組み込みの WebSocket オブジェクトがそのまま使えるし、サーバー サイドも .NET 色が強い SignalR ライブラリーが不要、ということでしょうか。

デプロイしてみる

もう Azure ポータルからもデプロイできます。
f:id:horihiro:20210429121411p:plain

リージョンはまだ少ないですね。
f:id:horihiro:20210429121311p:plain
アジア リージョンは東南アジアだけ。

価格レベルは、Free と Standard の 2 つ。
f:id:horihiro:20210429121235p:plain
コネクション数やメッセージ数に違いがあります。

リソース名は WebSocket エンドポイントの FQDN に使われる (<リソース名>.webpubsub.azure.com) ので、Global Unique である必要がありますが、制限はそれくらいです。

デプロイ、その後

が、どうも自分の環境ではデプロイ後も表示されないので (これを書いている 2021/4/29 現在)、設定変更等は Azure CLI (az webpubsub) からやります。
似たような状況の人に参考になれば。

Azure Web PubSub 用のサブ コマンド webpubsub はデフォルトでは入ってないので、Azure CLI が入った環境で、

az extension add --name webpubsub

と Azure Web PubSub 用の extension を入れる必要があります。

ローカル環境に入れたくない人は、Azure CloudShell で入れるといいでしょう。
適当な az webpubsub コマンドを実行すると、入れるかどうか聞いてくるので、そのまま Enter を押せばインストールが始まります。 f:id:horihiro:20210429122231p:plain

サンプル アプリを動かしてみる

ここに Node.js を使った Web アプリケーションのサンプルが載っているのでこれを動かしてみます。

azure.github.io

上のページでは、ローカル環境で Node.js を動かし、ngrok を使ってエンドポイントを公開して、、、とちゃんと丁寧に書いてありますが、ここでは一気にすっ飛ばして、Web Apps を使った構成にします。

使ったソース一式はこちら。

github.com

Web PubSub の設定確認

Web App 上のサーバーとブラウザーから Web PubSub につなぐためには、Web PubSub の接続文字列と Web PubSub からサーバー アプリへのハンドラー設定を確認する必要があります。

このページ には、「Azure ポータルから確認」と書いてありますが、なぜか Azure ポータルに Web PubSub リソース自体が表示されないので、Azure CLI での確認を書いておきます。

接続文字列の確認

az webpubsub key show で、接続文字列とキー、それぞれのプライマリー、セカンダリーが取得できます。

az webpubsub key show -g $RG_NAME -n $WEBPUBSUB_NAME

取得結果。

{
  "primaryConnectionString": "Endpoint=https://...",
  "primaryKey": "bBD5...",
  "secondaryConnectionString": "Endpoint=https://...",
  "secondaryKey": "8ZZv..."
}

ここで取得した接続文字列を、ここWebPubSubServiceClient の第一引数に入れます。
もし Web Apps のアプリケーション設定を使うなら、

let serviceClient = new WebPubSubServiceClient(process.env.CONNECTION_STRING_WEBPUBSUB, hubName);

のように、環境変数から取得するコードを書けばよいです。
CONNECTION_STRING_WEBPUBSUB の部分は、アプリケーション設定名に合わせて変更してください。

ハンドラー設定の更新

ハンドラーは、Web PubSub がクライアントからメッセージを受信した受信した時に Web アプリケーションに通知するための設定です。

このサンプル アプリでは /eventhandler で Web PubSub からの通知を待っているので、このパスを設定します。

az webpubsub event-handler hub update -g $RG_NAME -n $WEBPUBSUB_NAME --hub-name chat --template system-event-pattern=connected user-event-pattern=message url-template=https://$WEBAPP_NAME.azurewebsites.net/eventhandler

この設定をすることで、Web PubSub から Web App へメッセージが飛ぶようになり、サンプルのチャット アプリが Web App 上で動きます。

処理の流れ

Web App と Web PubSub 、各クライアント上の Web ブラウザー間の、ハンドシェイク時とメッセージング時、それぞれの流れをまとめておきます。
# 「Web App」と「Web PubSub」の両方が出てくるので、わかりづらいです。

1. ハンドシェイク

f:id:horihiro:20210429133509p:plain

  1. Web ブラウザーは、Web App から HTML を取得
  2. Web ブラウザーは、Web App に Web PubSub のエンドポイントをリクエス
  3. Web App は、Web PubSub からトークンを取得
  4. Web App は、Web ブラウザーに Web PubSub のエンドポイントを返却
  5. Web ブラウザーは、Web PubSub のエンドポイントを使って WebSocket を接続

2. メッセージング

f:id:horihiro:20210429135045p:plain

  1. Web ブラウザー A は、 WebSocket でメッセージを Web PubSub へ送信
  2. Web PubSub は、ブラウザーから受信したメッセージをWeb App に転送
  3. Web App は、ブラウザー A から (Web PubSub 経由で) 届いたメッセージ を、全クライアントに転送するよう Web PubSub に送信
  4. Web PubSub は、Web ブラウザー A からのメッセージを接続中の全クライアントに プッシュ

おわり

恐らく、Web App だけで Web アプリケーションに socket.io などを入れて、アプリケーション内で直接 WebSocket の管理をすると、スケール アウト時のインスタンス間の整合管理が大変なるんじゃないかと思いますが、
Azure Web PubSub を使うと WebSocket 管理の部分を丸っとお任せできるので、Web アプリケーションの実装がシンプルになるのではないでしょうか。

Azure Web PubSub は、Azure Functions のトリガーとバインディングにも対応しているので、近いうちにそちらも試してみます。

Key Vault 参照の自動更新を試してみた

f:id:horihiro:20210311154652p:plain

Key Vault 参照とは

ざっくりいうと、

  • Key Vault で厳重に管理されたシークレットに、App Service 上の Web アプリケーションから簡単に参照する機能。

というもの。

これを使わないと、Key Vault で管理されたシークレット (秘匿性の高い文字列、他のサービスから提供された API キーとか) に Web アプリケーションから参照するには、

  • Key Vault へのアクセス処理を、アプリケーション上にゴリゴリ実装する (Key Vault の SDK が必要)
  • アプリケーション設定に生で保存する ( Key Vault での安全な管理をあきらめる) 。

のどれかだった(はず)。

当然、アプリケーション設定に保存してた場合は、App Service リソースの参照権限があればあっさり見れてしまう。

Key Vault 参照では、

  • App Service のアプリケーション設定に特殊な文字列 ( @Microsoft.KeyVault({referenceString}) という参照用の文字列) を設定
  • App Service のマネージド ID を有効にして、その App Service リソースのマネージド ID に Key Vault 参照権限を持たせる

とすることで、Web アプリケーション (と Key Vault への参照権限があるユーザー) からのみ参照を許可にする制御が可能になる。

しかも Web アプリケーションからはアプリケーション設定に生で保存した時と同じように、環境変数として見える。 Web アプリケーションに特殊な実装は不要。なので Key Vault SDK もいらない。割と便利。

ちゃんと知りたい人はこちら。

docs.microsoft.com

Key Vault シークレットのバージョンとバージョン レス参照

一方で、Key Vault に保存されたシークレットにはバージョンがあり。上記の Key Vault 参照用の文字列も下記 2 パターンの書式になっていた。

  • SecretUri=https://VAULT_NAME.vault.azure.net/secrets/SECRET_NAME/SECRET_VERSION
  • VaultName=VAULT_NAME;SecretName=SECRET_NAME;SecretVersion=SECRET_VERSION

この通り、シークレットのバージョン (SECRET_VERSION) が必要。2020 年までは。

これが、最近バージョン指定が不要になり、バージョンを省略した場合は、最新バージョンを参照するようになった。

f:id:horihiro:20210314074332p:plain

App Service 基盤が、App Service から参照されている Key Vault シークレットを定期巡回 (1日1回くらい?) し、新バージョンが追加されていたらアプリケーション側に自動反映する、というもの。なるほど。

試してみた

アプリケーション上から Key Vault 参照の結果を確認できれば何でもいいので、Web ブラウザー上でサクッと環境変数一覧が確認できる php を使用。

手順。

  1. App Service でマネージド ID を有効化する
    f:id:horihiro:20210314075202p:plain
  2. Key Vault のアクセス ポリシーで、シークレットに対する参照権限を App Service のマネージド ID に付与する
    f:id:horihiro:20210314075348p:plain
  3. Key Vault に適当なシークレットを保存する
    f:id:horihiro:20210314075301p:plain
  4. App Service のアプリケーション設定に、Key Vault 参照を追加する
    正常に参照されると下のように表示される
    f:id:horihiro:20210314075917p:plain
  5. 確認用の Web ページとして下記 php ファイルを App Service のホーム (D:\home\site\wwwroot or /home/site/wwwroot ) に置いて Web ブラウザーで開く。
    f:id:horihiro:20210314080357p:plain
    いろいろ表示されるが、その中に環境変数一覧のセクションがあるので、ここに Key Vault 参照として設定したアプリケーション設定が表示されるか確認する。
  6. Key Vault シークレット側に新バージョンを追加し、1 日放置しておく。
    f:id:horihiro:20210314100006p:plain

とある日のお昼時に新バージョンのシークレットを追加してみると、翌朝にはアプリケーションから参照している値が更新されていることを確認できた。

App Service かどうかに関係なく、環境変数の更新にはプロセスの再起動が必要な気がするので、アプリケーションの再起動が発生していたか確認してみる。
# 今回確認したかったのはこの「再起動の有無」。

Kudu の Process Explorerw3sp.exe の情報を見る。

  1. Key Vault 参照設定直後
    f:id:horihiro:20210311171829p:plain
  2. シークレットへの新バージョン追加から 1 日後
    f:id:horihiro:20210311171841p:plain

プロセス ID が変わっているし、start time も更新されている。やはりアプリケーション自体に再起動がかかっている。

ドキュメントには 1 日以内 と書いてあるけど、start time の時刻からは、今回は半日ほどで更新されたみたい。

念のため [問題の診断と解決] の "Web App Restarted" から再起動の有無を確認してみると、キチンと「環境変数が変わったから再起動したからね!」って言っている。

f:id:horihiro:20210311171854p:plain

というわけで、おおむね予想通りの挙動。納得。

注意点

参照先のシークレットの新バージョンを検知すると自動的に再起動するので、再起動の数をできるだけ減らしたい人は設定しない方がいいかもしれない。
とはいっても、App Service 基盤は月に 1 回くらい更新していて、その反映のために (Key Vault 参照の設定に関係なく) 再起動は自動的にされるので、スタートアップ時間を短縮する、とか再起動が起こっても影響がないようにアプリケーションを実装しておくことがお勧め。

余談

実は、バージョンを省略した Key Vault 参照文字列で、最新バージョンを参照する挙動は、以前から非公式で動いていた。
今回は、それに自動更新の仕組み (定期的な最新バージョン チェックと自動再起動) が追加され、きちんとドキュメントに記載された (公式の使用法になった) という話でした。

おわり。

VS code extension に初トライ!!

f:id:horihiro:20210307163813p:plain

初めての VS code extension 開発

Hello World extension しか作ったことがなかったので、普通に使うもの(自分が、ね)を想定してチャレンジ。
主に下の教科書と公式サイトを行ったり来たりしてた感じ。
# 今年の2月からは、本業 (VS code とは関係ないけども) で著者の方々に大変お世話になっています。 www.amazon.co.jp

作った VS code extension

denofunc (Azure Functions のカスタム ハンドラーdeno で作りデプロイする CLI ツール) のコマンドを、VS code のコマンド パレットから操作するもの。

基本的に denofunc のラッパーなので、できることはこれ (だけ) 。

  • プロジェクト フォルダーの初期化
  • テンプレートからの関数追加
  • Azure Functions へのデプロイ

# えぇ、ターミナルでできるんですけどね。

なお Tree View とか Webview といった、extensin 感あるやつは一切使っていない。

ハマりポイント

初めての VS code extension 開発で少しハマったポイントをご紹介。

通知は await しない方がよさげ

右下のやつ。 f:id:horihiro:20210307190121p:plain

通知を出すのは、vscode.window 名前空間の下記メソッドたち。

  • showInformationMessage
  • showWarningMessage
  • showErrorMessage

戻り値の型が Thenable なので、盲目的に await を付けて使っていたけど、それをやると当然 resolve するまで処理が返ってこない。

通知の振る舞いとしてはその挙動は違和感があったので、await なしの方がいい気がする。
モーダルなダイアログを出したければ、await して 第二引数に {modal: true} を入れるとよい。

GitHub のブランチ名は注意

README.md 内で画像などのリンクを相対パスで書くと (![](relative/path/to/image.jpg) みたいに) 、vsix パッケージの生成処理 (vsce packagevsce publish の実行時) 内で、 package.json 内の repository を使って絶対パスに書き直される。

気を付けないといけないのが、上記絶対パス内のブランチ名として、既定では master が使用される。
なので、何も考えずにパッケージを作ったり publish すると master ブランチにリンクされるが、最近は main ブランチで作ることが流行りっぽいので、ブランチ自体が存在せず画像のリンクが切れる。

この README.md を修正するためにバージョン アップするのは (patch とはいえ) あまりやりたくない作業なので、気を付けた方がいい。

ちなみに最近ソースコード上では、master から HEAD に変わった様子。

github.com

ただ、この PR は vsce の最新バージョン (2021/3/7 現在 1.85.1) にまだ含まれていない (タッチの差)。
なので、 vsce の最新バージョンではまだ master が既定なので --githubBranch オプションで GitHub リポジトリのブランチ名を指定することを強く推奨。

(3/12 追記)
----
修正バージョン 1.86.0 がリリースされてた。

www.npmjs.com f:id:horihiro:20210312080821p:plain
----

OutputChannel に出す文字に色が付けられない

OutputChannel は、ログなどを出せるパネル。

f:id:horihiro:20210307203129p:plain

通常、コマンドプロンプトbash などでは、標準出力/標準エラー出力にこういう色が付けられる。
f:id:horihiro:20210307192941p:plain

一方で VS code の OutputChannel はまだカラー対応していないらしい。探すと issue がいっぱい出てくるが例えばこれ。

github.com

上記と同じ出力をそのまま VS code extension から OutputChannel に出力すると、ANSI color code? escape code? がそのままでてくる。
f:id:horihiro:20210307193110p:plain
# 最初、何が出ているのか理解できなかった。

結局正規表現ANSI code を削除している。苦肉の策。

ついでのメモ

今回作った extension では child_processspawn を使って denofuncaz を実行している。
この時、spawn の引数に {shell: true} を渡してあげないと、指定したコマンド(denoaz)が見つからず、ENOENT が発生する。Windows (コマンド プロンプト) と WSL2 (bash) で確認。

最後に

公開した extension の Visual Studio Marketplace のページ、GitHub リポジトリーはこちら。

marketplace.visualstudio.com

github.com

Azure Functions で Top-Level await は使えるのか?

f:id:horihiro:20210212080153p:plain

ことのいきさつ

AWS Lambda が Node.js v14 をサポートしたらしいです。
そのニュースを見て、「そういえば Azure Functions はどのバージョンが動いているんだっけ?」と確認してみると、既に Node.js v14.15.4 が最新のようでした。

でも Node.js v14 って何ができるんだっけ?と思い、また調べてみると、Top-Level await のサポートがあるんですね。

で、ここで表題の疑問、

「Azure Functions の関数コードで Node.js の Top-Level await は使えるのか?」

が湧いた訳です。

結論

使える。

ちゃんと書く

Top-Level await については、下記 Qiita の記事がわかりやすいと思うのでお任せして。

qiita.com qiita.com

確認したいことは、関数コードで module.exports の外で await が使えるかどうか、です。
# 単なる技術的興味なので、何に使えるかはこの際忘れましょう。

こんな感じ。await する関数 sleep は、指定のミリ秒数待機する単純な例です。

[index.js]

const sleep = (time) => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, time);
    });
}

const now = Date.now();
let responseMessage = `before await at ${Date.now() - now}
`;
await sleep(60000); // <-- このコードは動くのか?
responseMessage +=`after await at ${Date.now() - now}`;

module.exports = async function (context, req) {
    context.res = {
        // status: 200, /* Defaults to 200 */
        body: responseMessage
    };
}

実行してみます。

f:id:horihiro:20210211204555p:plain

やっぱり「await は async 関数の中だけ。」と怒られます。

Top-Level await は、ES modules の仕様なので、関数コードが ES modules として扱われる必要があります。

github.com

Azure Functions の Node.js Worker では、デフォルト (ファイル拡張子が .mjs 以外) だと関数コードを ( import ではなく) require しているので、CommonJS として使用することが想定されています。

[https://github.com/Azure/azure-functions-nodejs-worker/blob/v2.x/src/FunctionLoader.ts#L42:embed:cite] f:id:horihiro:20210212073027p:plain

なので、この関数コードを何とかして ES modules として読ませてやればよさそうです。
具体的には下記 2 つの変更をします。

  1. ファイル拡張子を .mjs に変更
    index.js -> index.mjs
  2. エクスポートの書式を ES module 化
    module.exports = -> export default

[index.mjs]

const sleep = (time) => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, time);
    });
}

const now = Date.now();
let responseMessage = `before await at ${Date.now() - now}
`;
await sleep(60000); // <-- こいつ・・・動くぞ!
responseMessage +=`after await at ${Date.now() - now}`;

export default async function (context, req) {
    context.res = {
        // status: 200, /* Defaults to 200 */
        body: responseMessage
    };
}

実行してみると、きちんとエクスポートした関数内から、関数の外で await した (60000 ミリ秒経過) 結果を参照することができています。

f:id:horihiro:20210211213054p:plain

await 中、例えば、20000 ミリ秒経過後に上記の HTTP 関数に対してアクセスすると、残りの 40000 ミリ秒待機した後にレスポンスが返却されます。

ちなみに、ES modules としてモジュールをロードする方法として、 package.json 内に "type": "module" を追加する方法もあります。 しかし、上記の通り Azure Functions の Node.js Worker は、ファイル拡張子が .mjs の時のみ ES modules として import する実装なので、上記のような拡張子の変更が必須です。

TypeScript は?

Azure Functions Core Tools では TypeScript で書かれた関数コードのテンプレートも作ってくれます。

この TypeScript のコードは、デプロイ時に tsc でトランスパイルされるので、実質 JavaScript で実行されます。

なので、TypeScript でも Top-Level await が使えそうですが、結果的には下記 2 つの追加作業が必要です。

  1. トランスパイルの設定の変更
    tsc で Top-Level await を通すよう tsconfig.json を以下のように修正します。
    f:id:horihiro:20210212121313p:plain
  2. tsc の出力結果の修正
    tsc の出力結果は、デフォルトでは ./dist/<関数名>/index.js に出力されますが、上記のとおり .js のままだと ES modules としてロードされないので、.mjs に拡張子を変更して、function.json の中の scriptFile の値も mjs ファイルを変更します。

ただ 2. は tsc を実行するたびにやらないといけないです。ちょっと面倒ですね。。。

結局何に使えるのか。

パッと思いつく使い方としては、エクスポートした関数の外で (つまり当該関数コード ファイルのロード時 1 回だけ) node-fetchaxios などで外部リソースを await で取得し、関数コード内で取得した外部リソースを再利用する、てことはできそうです。
# 副作用までは追いかけていませんが。

Azure Functions の関数コード用テンプレートはここで公開されているので、需要があるなら issue や PR を挙げてもよいかもしれませんね。

github.com

Azure Functions on KEDA on AKS

この記事は、Microsoft Azure Tech Advent Calendar 2020 の 17 日目の記事です。

qiita.com

KEDA を使って、Azure Functions を動かしてみました。

KEDA

KEDA (Kubernetes Event-Driven Autoscaling) は、Kubernetes をベースに、処理すべきイベント量に応じて自動でスケーリングしてくれる仕組みです (ほぼ名前を直訳) 。

keda.sh

名前の通り、Kubernetes が必要なようです。
Kubernetes によるコンテナー オーケストレーションを、イベント ベースにやってくれる、というのが雑な理解。
Kubernetes 自体、名前くらいしか知らないんですが。

Azure Functions

ご存じ、Azure が提供する FaaS (Functions as a Service)です。

docs.microsoft.com

作成した関数を、HTTP や Azure の各サービスからの通知をトリガーから実行でき、出力結果についてもほかのサービスと連携が可能なので、使い勝手の良いサービスです。

Azure Functions は、関数を実行するランタイム部分と、スケーリングを制御する部分で構成される Azure 上のサービスで、このうち後者のスケーリング制御は KEDA/Kubernetes に置き換えることができます。
KEDA/Kubernetes 上で動作させることができるので、オンプレミス環境のような Azure プラットフォームの外で Azure(?) Functions が利用できます。

ちなみにランタイム部分はオープンソースとして GitHub で公開 されており、issue や Pull Request を通して開発者と直接コンタクトを取ることが可能ですので、興味がある方はどうぞ。

やってみる

実際に動かしてみます。

前準備

以下 3 つのコマンド ツールをローカル環境にインストールしておきます。

構築本番

Kubernetes が動くオンプレ環境が準備できなかったので、今回は Azure の Kubernetes サービス、AKS を使って試してみます。
# 結局 Azure 上なのであまり KEDA を使うメリットはわからないですが、あくまで「試し」です。

1. AKS リソースをデプロイ

ポータルからポチポチしたらデプロイできるので、難しいことはないです。

2. kubectl の設定

AKS 上の Kubernetes の認証情報をローカルの kubectl の設定ファイル ($HOME/.kube/config) に保存します。

az aks get-credentials --resource-group $RG_NAME --name $AKS_NAME

このコマンドを実行すると、kubectl から AKS 上の Kubernetes の操作が可能になります。
※以降の手順では、一切 kubectl を実行していませんが、func が内部的に使用しています。

Azure ポータルからもコマンドの確認が可能です。 f:id:horihiro:20201129103328p:plain

3. KEDA コンポーネントのインストール

AKS 上の Kubernetes名前空間を作成しつつ、KEDA コンポーネントをインストールします。

func kubernetes install --namespace $CLUSTER_NAMESPACE

4. デプロイする関数の準備

ローカルの開発環境に Function App 用のプロジェクトを作り、次に Queue トリガー関数を作ります。 関数の実装言語はお好みのものを。

func init --docker --javascript
func new --javascript --template "Azure Queue Storage trigger" --name queuetrigger1

最終的に Docker コンテナーとしてデプロイするので、Dockerfile を生成する --docker をつけています。
既に実装中のプロジェクトがある場合は、下記コマンドで Dockerfile を生成するといいでしょう。

func init --docker-only

AKS 上で起動が確認できればいいだけなので、作成した関数はほぼテンプレのまま、メッセージを受信してから 5 秒スリープするだけです。

const sleep = (time) => {
    return new Promise((res) => {
        setTimeout(() => {
            res();
        }, time);
    })
}
module.exports = async function (context, myQueueItem) {
    await sleep(5000);
    context.log('JavaScript queue trigger function processed work item', myQueueItem);
};

その他、function.json や Dockerfile はこちら。

{
  "bindings": [
    {
      "name": "myQueueItem",
      "type": "queueTrigger",
      "direction": "in",
      "queueName": "<QUEUE_NAME>",
      "connection": "<APPSETTING_NAME_FOR_QUEUE_STORAGE>"
    }
  ]
}
# To enable ssh & remote debugging on app service change the base image to the one below
# FROM mcr.microsoft.com/azure-functions/node:3.0-appservice
FROM mcr.microsoft.com/azure-functions/node:3.0

ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true

COPY . /home/site/wwwroot

RUN cd /home/site/wwwroot && \
    npm install

5. 関数のデプロイ

AKS の KEDA 上に関数をデプロイします。

その前に、local.settings.json で定義している値をデプロイ環境用のものに変更しておきます。
デプロイの時点で Function App のアプリケーション設定に相当する情報は、local.settings.json からインポートされ、KEDA の Secret として保存されますので、例えば、ローカルでの開発に Storage Emulator を使って UseDevelopmentStorage=true などを設定している場合は、デプロイ前に正しいストレージ アカウントへの接続文字列へ変更しておきます。

デプロイ自体のコマンドはこれだけです。

func kubernetes deploy --name $NAME_IMAGE --registry $USERNAME_REGISTRY --namespace $CLUSTER_NAMESPACE

このコマンドを実行すると、

  1. docker build でコンテナー イメージを作成
  2. docker push で Registry に登録
  3. $HOME/.kube/config の認証情報に基づいて、Kubernetes に反映

が一気に実行され、デプロイ完了です。

動作確認

実際に関数を動かした時に、KEDA でスケールするのか、確認してみました。

KEDA に乗せた関数は Queue トリガーで実行されるので、キューにメッセージを追加する HTTP トリガー関数をローカルで、0.1 毎に実行してみました。
その様子がこちらです。

最初のポッドが起動するまでややかかりますが、その後はたまったキュー メッセージを処理するためにどんどんポッドが追加され、スケール アウトしていく様子がわかります。
ほおっておけば、 0 までスケール インします。

まとめ

Azure Functions 用の関数を (AKS 上であったものの) KEAD 上で動作させ、メッセージ量に応じてポッドがスケーリングがされることが確認できました。

今回、Queue トリガーを使いましたが、KEDA では以下のトリガーもサポートされています。

  • Azure Service Bus キュー
  • Azure Event/IoT Hubs
  • Apache Kafka
  • RabbitMQ キュー

HTTP でのスケーリングはサポートされていないようで、別途 Prometheus とかいうのが必要です。 dev.to

最後に公式ドキュメントをご紹介。 docs.microsoft.com

Bot Framework v4 を Azure Functions で使ってみる

仕事の Teams にボットでも作ろうかと思い、初めて Bot Framework SDK を使ってみました。

実は Teams のボットは Outgoing Webhook でも作れます。
しかし、Outgoing Webhook に対するボット アプリからのメッセージは、その Webhook リクエストに対するレスポンスで返す必要があること、そしてそのレスポンスは 5 秒以内に返す必要がある、など制限があります。
これをAzure Functions の HTTP トリガー関数で実装すると、関数処理自体が 5 秒以内に限定されてしまいますし、利用プランによってはコールド スタートするのでもっと処理時間は短くなり (最悪、処理を開始する前にタイムアウトする)、あまり相性がいいとは言えません。

なので、今回は Bot Framework SDK を使ったボット作成にチャレンジしました。

docs.microsoft.com

今のバージョンは v4 らしいです。

とりあえず Web 検索すると、下記の Qiita の記事がヒットします。

qiita.com

「Function Bot」というリソースからテンプレートがダウンロードできそうなので Azure デプロイしてみます。





。。。



もうないみたいです orz f:id:horihiro:20201031145013p:plain

上の記事が書かれた当時は Bot Framework が v3 のようですが、FUnction Bot は v3 のみに対応してて、そして v3 は 2019 年 8 月にディスコンになったようです😥

blog.botframework.com

ないものはしょうがないので、試しに Web App Bot をデプロイして、Web App Bot のテンプレートをダウンロードしてみます。

所詮 HTTP サーバー。そんな大変なことはないはず。

メインの index.js を開いてみると、express ベースみたいです。

const express = require('express');

// Import required bot services.
// See https://aka.ms/bot-services to learn more about the different parts of a bot.
const { BotFrameworkAdapter } = require('botbuilder');

// Import bot definitions
const { BotActivityHandler } = require('./botActivityHandler');

// (略)

// Create adapter.
// See https://aka.ms/about-bot-adapter to learn more about adapters.
const adapter = new BotFrameworkAdapter({
    appId: process.env.BotId,
    appPassword: process.env.BotPassword
});

// (略)

// Create HTTP server.
const server = express();
const port = process.env.port || process.env.PORT || 3978;
server.listen(port, () => 
    console.log(`\Bot/ME service listening at http://localhost:${port}`)
);

// Listen for incoming requests.
server.post('/api/messages', (req, res) => {
    adapter.processActivity(req, res, async (context) => {
        // Process bot activity
        await botActivityHandler.run(context);
    });
});

最後の方の BotFrameworkAdapter#adapter にリクエストとレスポンスをセットで渡せればいい感じです。

何も考えず、HTTP トリガーに置き換えてみます。typescript です。

// (いろいろ省略)

const httpTrigger: AzureFunction = async function (context:Context) {
  await adapter.processActivity(context.req, context.res, async (context:TurnContext) => {
    // Process bot activity
    await botActivityHandler.run(context);
  });
};

export default httpTrigger;

f:id:horihiro:20201031161106p:plain

はい、怒られました。型が違うようです。さすが typescript。

ここでようやくリファレンスの登場です。

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

やっぱり Express (か Restify) ライクなオブジェクトが必要とのこと。

この azure-function-express を使えば、Azure Functions でも Express が使えます。
でも正直ルーティングを必要とするほど HTTP トリガー関数を複数作るわけではないので express の組み込みまではしたくありません。

そこで、azure-function-express からリクエスト/レスポンスのラッパー オブジェクトのみを拝借&BotFrameworkAdapter#adapter が受け取れるよう微修正したテンプレート リポジトリを公開しました。
Express は一切使用しておりません。

github.com

実際にボットとしての動作確認は、Azure で Bot Channels Registration をデプロイして、Teams/LINE で確認しました。

docs.microsoft.com docs.microsoft.com docs.microsoft.com