ほりひログ

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

Azure Functions の関数内で Key を取得する

はじめに

需要があるのかないのか、怪しいエントリーです。
Function App 内にある HTTP トリガー関数の実行時に、別の関数を HTTP トリガーで実行するために必要なコードを取得してみます。
# ちょっと何言っているかわからないかもしれませんが。。。

本題

今回は、1 つの Function App に作成した、ある HTTP トリガー関数の実行中に、同じ Function App 内にある別の HTTP トリガー関数の実行を想定します。

必要な手順と参考にしたドキュメントは以下の通りです。

前準備

マネージド ID を有効にして、発行されたサービス プリンシパルに、予め対象の Function App に対する "Web サイト共同作成者" ロールを割り当てておきます。
docs.microsoft.com

これは実行時に、Azure の REST API を実行するために必要となります。

  1. Azure ポータルで Function App を開いて、"プラットフォーム機能" タブ内にある "ID" をクリックします。
    f:id:horihiro:20190831195103p:plain
  2. 表示された "識別" 画面内の "システム割り当て済み" タブ内の "状態" を "オン" にし、保存します
    f:id:horihiro:20190831195259p:plain
  3. "プラットフォーム機能" タブに戻り、"アクセス制御 (AIM)" をクリックします
    f:id:horihiro:20190831195408p:plain
  4. 表示された "Access Control" 画面内で、"+追加" -> "ロールの割り当ての追加" をクリックし、"役割" に "Web サイト共同作成者" 、"アクセスの割り当て先" に "Function App"、を選択し、今開いている Function App そのものを選択したうえで保存します。 f:id:horihiro:20190831195925p:plain
    "ロールの割り当て" タブ内で、先ほどのロールが "このリソース" に割り当てられていることを確認してください。
    f:id:horihiro:20190831200836p:plain

以上で前準備は終了です。

実行時

実行時に必要な処理は下記のとおりです。

  1. マネージド ID を使用して、Azure の REST API (ARM) 実行に必要なアクセス トークンを取得します。
    docs.microsoft.com
  2. 下記の Azure REST API を、1. で取得したアクセス トークンを使用して実行し、Function App の Admin Token を取得します。
    docs.microsoft.com
  3. Function Host の REST API を、2. で取得した Function App の Admin Token を使用して実行し、関数の実行に必要なキーを取得します。
    github.com

下記コードは、TypeScript でのサンプル実装です。
Windows 版の Function App のみで動作を確認しました。

import { AzureFunction, Context, HttpRequest } from "@azure/functions"
import fetch, { Response } from 'node-fetch';

const API_VERSION: String = '2017-09-01';
const RESOURCE_URL: String = 'https://management.azure.com/';
const FUNCTION_NAME_CALLEE: String = '<呼び出し先の関数名>';

const getMasterKey = async function(): Promise<String> {
  try {
    // step1: get access token using managed ID.
    // see https://docs.microsoft.com/ja-jp/azure/app-service/overview-managed-identity#using-the-rest-protocol
    let response:Response = await fetch(`${process.env.MSI_ENDPOINT}?api-version=${API_VERSION}&resource=${RESOURCE_URL}`, {
      headers: {
        secret: process.env.MSI_SECRET
      }
    });
    let json:any = await response.json();

    // set parameters from environment variables
    const [, subscriptionId] = process.env.WEBSITE_OWNER_NAME.match(/^([^+]*)+.*$/);
    const resourceGroup:String = process.env.WEBSITE_RESOURCE_GROUP;
    const functionApp:String = process.env.WEBSITE_SITE_NAME;

    // step2: get admin token for the function app using the access token
    // see https://docs.microsoft.com/ja-jp/rest/api/appservice/webapps/getfunctionsadmintoken
    response = await fetch(`https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${functionApp}/functions/admin/token?api-version=2016-08-01`, {
      headers: {
        Authorization: `Bearer ${json.access_token}`
      }
    });
    json = await response.json();

    // step3: get master key using the admin token
    // see https://github.com/Azure/azure-functions-host/wiki/Key-management-API#host-key-resource-adminhostkeyskeyname
    // # but wrong url /admin/host/keys/ in the document.
    response = await fetch(`https://${process.env.WEBSITE_HOSTNAME}/admin/functions/${FUNCTION_NAME_CALLEE}/keys/default`, {
      headers: {
        Authorization: `Bearer ${json}`
      }
    });
    json = await response.json();

    return json.value;
  } catch {
    return '';
  }
};

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
  context.log('HTTP trigger function processed a request.');
  const code:String = await getMasterKey();
  const response:Response = await fetch(`http${masterKey?'s':''}://${process.env.WEBSITE_HOSTNAME}/api/${FUNCTION_NAME_CALLEE}?code=${code}`);
  context.res = {
    status: 200, /* Defaults to 200 */
    body: await response.text()
  };
};

export default httpTrigger;

留意点

Azure の REST API (ARM) の URL に必要な、サブスクリプション ID やリソース グループ名、Function App 自体の名前は、今回は環境変数から取得していますが、Linux 版でこれが使えるのか、Windows 版でもこれが使い続けられるかは、正直わかりません。

(上記コードから抜粋)

    // set parameters from environment variables
    const [, subscriptionId]:Array<String> = process.env.WEBSITE_OWNER_NAME.match(/^([^+]*)+.*$/);
    const resourceGroup:String = process.env.WEBSITE_RESOURCE_GROUP;
    const functionApp:String = process.env.WEBSITE_SITE_NAME;

まとめ

Function App で、HTTP トリガー関数をネストする時、各関数のキーをハードコード、もしくは、アプリケーション設定などに保存しておく必要がありましたが、上記の方法で実行ごとに動的に取得することが可能になります。
# HTTP リクエストが 3 回増える&そもそもそういう設計するのか、というのはありますが。。。