Azure Functions から定期的に Microsoft Graph にアクセスする
Azure Functions って知ってますか ?
一言でいえば、Azure Automation の”開発屋さん版”です(って、適当すぎて怒られそうw)。
Azure Automation は PowerShell スクリプトをクラウド上でジョブ化することができますが、Azure Functions は C#、F#、Node.js、Python、PHP Java などで書いたプログラムを WebJob として登録することができます。いずれも、サーバーを置くことなくジョブを実行することができる、いわゆるサーバーレスを実現するためのコンポーネントです。
PowerShell 好きの私的には Automation が好みではありますが、悔しいことに Functions のほうが高機能であることは否めません。何がいいって、入力や出力の定義が超簡単。これはAutomationにはない部分です。詳しくは以下の手順で。
Azure Functions の概要
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-overview
ためしに使ってみようかってことで、ここでは、Microsoft Graph にアクセスしてユーザー一覧を取得し、これを Azure Blob に保存するジョブを作成してみることにします。
Microsoft Graph にアクセスするので Azure AD 側の設定も必要になります、そこも含めて手順を書いておきますので参考にしてください。
1. Azure Functions アカウントの作成
Azure Portal の Marketplace で Function App を検索し、Function App をクリックしたら「作成」。
アプリ名等を入力して「作成」。アプリ名(ここでは GraphFromFunction.azurewebsites.net)は後でAzure AD にアプリ登録するときに使用します。
作成した Function App に移動すると、以下のようにApp Service(Functions の本体)、ストレージ、App Service プランが作成されていることがわかります。
App Service をクリックすると、Function App の簡易作成画面(クイックスタート)が表示されます。
今回は定期的に Microsoft Graph を実行するので「タイマー」を選択。言語は C#。
選択したら「この関数を作成する」をクリック。
初期画面が表示されたら、ためしに「実行」をクリックしてみる。
ログに以下のような出力がされれば、ひとまず正常です。太字部分はコード内の log.info() によって出力された文字列です。
2017-01-26T13:45:00.009 Function started (Id=db7524a6-c10b-4d9a-9e44-b61e86898aa0)
2017-01-26T13:45:00.009 C# Timer trigger function executed at: 1/26/2017 1:45:00 PM
2017-01-26T13:45:00.009 Function completed (Success, Id=db7524a6-c10b-4d9a-9e44-b61e86898aa0)
この関数はタイマーにバインドされているので、規定時間(5分)に1回、自動的に実行されます。
タイマーの間隔を変更するには、左側のメニューの「統合」をクリックします。
右下に「スケジュール」と書かれたフィールドがありますが、これが間隔を示しています。
設定の方法は以下を参照してください。
0 */5 * * * *
Azure Functions におけるタイマー トリガー
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-timer
2. Azure AD にアプリ登録する
Function App からいったん離れて、今度は Auzre AD 側の設定をします。実は、AppService の認証/承認の設定からでもできるのですが、今回は何が行われているのかを理解してもらうために面倒な手順で進めてみます。
なお、Azure AD について詳しく知りたい方は以下のリンクがおすすめです。
Azure AD ディレクトリの管理
https://github.com/Microsoft/azure-docs.ja-jp/blob/master/articles/active-directory/active-directory-administer.md
Azure AD は旧ポータルで操作するのがセオリーですが(ほんとかよ)、ここはあえて新ポータルでいってみましょう。
ポータルで「Azure Active Directory」を開いてください。
「プロパティ」をクリックすると「ディレクトリ ID」が表示されるので、これを控えておきます。あとから、ソースコード内で「Tenant ID」として使用します。
TenantID = c9687145-521f-42e6-xxxx-xxxxxxxxxxxx
次に「アプリの登録」をクリックすると、指定したテナントに関連づけられているアプリケーションの一覧が表示されます。
「追加」をクリックします。「アプリケーションの種類」は「Webアプリ/API」を選択。「サインオンURI」には、先ほど作成した Function App の URL(https://graphgromgunction.azurewebsites.net) を指定して「作成」をクリック。
先ほどのアプリ一覧に、今作成したアプリが追加されるので、クリックして設定画面を開きます。
「プロパティ」をクリックして表示された情報から「アプリケーション ID」を控えておきます。これはあとから「Client ID」としてソースコード内で使用します。
ClientID = e67fff1a-d912-4bf4-xxxx-xxxxxxxxxxxx
今度は「キー」をクリックします。
既定ではキーは作成されていません。
「機関」からキーの有効期限を選択し(ここでは「期限なし」)、「キーの説明」を適当に入力して「保存」をクリック。保存が完了すると「値」にキーが表示されるので、これを控えておきます。これがコード内で ClientSecret となります。キーが表示されたら、それを控えるまではページを移動しないでください。移動すると、安全のため二度と表示できなくなります。もしキーを忘れてしまったら、既存のものを消して新しくキーを作成すれば OK です。
ClientSecret = b/dmwx0WUnmljqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
この作業で以下の3つの情報を収集することができました。
<Tenant ID> c9687145-521f-42e6-xxxx-xxxxxxxxxxxx
<Client ID> = e67fff1a-d912-4bf4-xxxx-xxxxxxxxxxxx
<Client Secret> = b/dmwx0WUnmljqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
これらの値を、この後の作業で Function App 内に埋め込みます。
Azure AD の最後の作業として、アプリケーションにディレクトリへのアクセス権を与えます。
アクセス権が無いと、正しいトークンを取得しても以下のようなエラーが発生します。
2017-01-27T04:29:02.895 {
"error": {
"code": "Authorization_RequestDenied",
"message": "Insufficient privileges to complete the operation.",
"innerError": { "request-id": "96e44229-5f66-4057-b9ea-f864b5013097",
"date": "2017-01-27T04:29:01"
}
}
}
「必要なアクセス許可」をクリックしてください。既定では以下のようになっています。
アクセス権について詳しく解説はしませんが、このままではアクセスすることができません。
アクセス許可スコープ | Graph API 概念
https://msdn.microsoft.com/ja-jp/library/azure/ad/graph/howto/azure-ad-graph-api-permission-scopes
今回は、Function App がユーザーのログオンを介さずに直接 Graph API にアクセスします。そのため、アプリケーションに対して直接権限を与える必要があります。規定では「委任されたアクセス許可」なので、ユーザーがログオンしないとアクセスすることができないのです。
API 一覧にある「Windows Azure Active Directory」をクリックすると、以下のように権限一覧画面が表示されます。「アプリケーションのアクセス許可」の中にある「Read directory data」を選択してください。既定で与えられている「Sign in and read user profile」は必要ないので外してしまいましょう。アプリケーションには余計な権限を与えてはいけません。「保存」をクリックして保存しましょう。
以上で Azure AD 側の設定は完了です。
3. Function App の作成
Function App の設定画面に戻ります。
既存のコードを以下で上書きして下さい。コード内の Tenant ID、Client ID、Client Secret に先ほど控えた値を埋め込みます。Client Secret を生のまま埋め込むのはアレなので、本当はApp Service のパラメタに登録するのがよいのですが、ひとまずここでは生埋め込みでいっちゃいましょう。パラメタ登録についてはあとで説明します。
#r "Newtonsoft.Json"
using System.Net;
using System.Net.Http.Headers;
using Newtonsoft.Json;
using Microsoft.IdentityModel.Clients.ActiveDirectory;public static async Task<String> Run(TimerInfo myTimer, TraceWriter log)
{
log.Info($"C# Timer trigger function executed at: {DateTime.Now}");string resourceId = "https://graph.microsoft.com";
string tenantId = "<Tenant ID>";
string authString = "https://login.microsoftonline.com/" + tenantId;
string clientId = " <Client ID> ";
string clientSecret = " <Client Secret> ";
//string clientSecret = ConfigurationManager.AppSettings["clientSecret"];log.Verbose("ClientSecret=" + clientSecret);
log.Verbose("authString=" + authString);var authenticationContext = new AuthenticationContext(authString, false);
//アクセストークン取得;
ClientCredential clientCred = new ClientCredential(clientId, clientSecret);
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenAsync(resourceId,clientCred);
string token = authenticationResult.AccessToken;
log.Verbose("token=" + token);var responseString = String.Empty;
using (var client = new HttpClient())
{
string requestUrl = "https://graph.microsoft.com/v1.0/users";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
log.Verbose(request.ToString());HttpResponseMessage response = client.SendAsync(request).Result;
responseString = response.Content.ReadAsStringAsync().Result;
log.Verbose(responseString);
}
return responseString;
}
※ こちらからダウンロードできます https://github.com/junichia/Functions-Graph
残念ながら、このままでは動作しません。
ちょっと説明が面倒なのですが、今回は Microsoft Graph にアクセスするため、必要なモジュールを追加しなければなりません。モジュールによっては、コードの先頭に以下のように書くことで、自動的に Nuget から追加されます。
#r "Newtonsoft.Json"
これに関する詳細が知りたい方は、以下の「外部アセンブリの参照」を参照してください。
Azure Functions C# developer reference (Azure Functions C# 開発者向けリファレンス)
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-reference-csharp
残念ながら Microsoft Graph に必要なモジュール(Microsoft.IdentityModel.Clients.ActiveDirectory)は #r では追加することができません。
そこで、特別なファイルを作成してアップロードしてあげる必要があります。以下の JSON ファイルを作成して、Project.json というファイル名で保存してください。3.13.8 は現時点での安定最新バージョンです。
Active Directory Authentication Library 3.13.8
https://www.nuget.org/packages/Microsoft.IdentityModel.Clients.ActiveDirectory/
Project.json
{
"frameworks": {
"net46":{
"dependencies": {
"Microsoft.IdentityModel.Clients.ActiveDirectory": "3.13.8"
}
}
}
}
ファイルを作成して保存したら、これを Function App にアップロードします。
画面右上の「ファイルの表示」をクリックしてください。
以下のように3つのファイルが存在していることがわかります。現在のコードは「run.csx」です。
「アップロード」をクリックして、先ほど作成した Project.json をアップロードしてください。
アップロードが完了すると、Nuget から必要なモジュールが自動的にダウンロードされて組み込まれます。
これで動くようになったはずです。
コード画面の「実行」ボタンをクリックしてみてください。
ログに、以下の赤枠で囲んだようなJSON形式のデータが出力されていれば成功です。
今回は、Graph の URL を以下のようにユーザー一覧を取得するように設定したので、Azure AD に登録されているユーザー一覧が取得できています。
https://graph.microsoft.com/v1.0/users
出力結果を JSON エディタ等で見ていただくと、以下のようなデータがユーザーの数だけ出力されています。
{
"id":"deb57951-92af-4bc4-xxxx-xxxxxxxxxxxx","businessPhones":["+81 9099999999"],
"displayName":"admin",
"givenName":"admin",
"jobTitle":"\u30a8\u30d0\u30f3\u30b8\u30a7\u30ea\u30b9\u30c8",
"mail":"admin@pharaojp.onmicrosoft.com",
"mobilePhone":"+81 9017xxxxxx",
"officeLocation":"???-???????",
"preferredLanguage":"ja-JP",
"surname":"user",
"userPrincipalName":admin@pharaojp.onmicrosoft.com
}
以上でGraphへのアクセス部分の作成は完了です。
4. Azure Blob Storage への出力
Blob ストレージへの出力は簡単です。コードを書く必要は一切ありません。
「統合」メニューをクリックしてください。
画面右上の「新しい出力」をクリックすると出力先一覧が表示されるので、「Azure BLOB ストレージ」を「選択」します。
「Blobパラメーター名」に「$Return」を指定します。これは、コードの最後に書かれた「 Return responseString; 」に関連しています。responseString 変数に格納された値が、Return されます。
「ストレージアカウント接続」にはストレージアカウントを指定してください。既存のものでも新規に作成することもできます。
パスはストレージアカウント内にファイルを保存するときのフルパスです。コンテナは事前に作成しておく必要はありません。指定したコンテナが自動的に作成されます。{rand-guid}は保存するたびにランダムなGUIDを生成することを意味しています。「.txt」を付けておくと開くときに便利です。
以上で作業は完了です。
放っておけば5分に1回、Graph API が呼び出されてBlobストレージにその時点のユーザー一覧のスナップショットがとられます(役に立つかどうかは不明ですが。。。)