Azure Functions の Pythonコードから Azure Access Token を取得してみました


概要

Microsoft Graph API を利用するために必須となる アクセストークン を Azure Functions から取得するためための 関数作成手順です。
この記事 のオンプレPythonプログラムを Azure Functions のコードとして定期的に実行するために、TimerTrigger をトリガーにして実装してみました。
オンプレシステムのクラウド移行において、バッチプログラム等を Azure Functions に移行するためのヒントになればと思っています。

ローカル実行環境

macOS Big Sur 11.1
python 3.8.3

ローカルでの事前準備

Azure Functionsをデプロイするためには、「Azure CLI」と「Azure Functions Core Tools」が必要になります。この記事 等を参考に事前にインストールしておきます。

今回デプロイするにあたって利用したバージョンは以下となります。

# Azure CLI のバージョン
$ az --version
azure-cli                         2.20.0

# Azure Functions Core Tools のバージョン
$ func --version                              
3.0.3388

Azure上での事前準備

この記事 等を参考にAzureのクラウド環境を準備しておきます。
上記記事の「3.FunctionAppの作成」を実施します。今回は以下の値で設定しています。

項目
Functions App name iturufunctest
Publish Code
Runtime stack Python
Version 3.8
Region Japan East
Storage Account storageituru
Operating System Linux
Plan type Consumption

ローカルでのAzure Functionsの実装

以下の順で実装していきます。
1.作業実施のための任意のディレクトリを作成
2.Python仮想環境の定義
3.Functionプロジェクトの作成
4.Functionの作成

Functionのプロジェクトディレクトリの作成

$ mkdir GraphAPI   
$ cd GraphAPI                

Python仮想環境の定義

$ python -m venv .venv            
$ source .venv/bin/activate 
$ ls -l
total 0
drwxr-xr-x  3 ituru  staff   96  3 22 14:22 ./
drwxr-xr-x  5 ituru  staff  160  3 22 14:21 ../
drwxr-xr-x  6 ituru  staff  192  3 22 14:22 .venv/

Functionのプロジェクトの作成

$ func init AAD --python
Found Python version 3.8.3 (python3).
Writing requirements.txt
Writing .funcignore
Writing .gitignore
Writing host.json
Writing local.settings.json
Writing /Users/ituru/MyDevelops/AzureFunction/GraphAPI/AAD/.vscode/extensions.json

$ ls -l
total 0
drwxr-xr-x  4 ituru  staff  128  3 22 14:24 ./
drwxr-xr-x  5 ituru  staff  160  3 22 14:21 ../
drwxr-xr-x  6 ituru  staff  192  3 22 14:22 .venv/
drwxr-xr-x  8 ituru  staff  256  3 22 14:24 AAD/

Functionで利用するテンプレートリストの表示(利用はPythonの中から)

$ cd AAD 
$ func templates list
C# Templates:
  Azure Blob Storage trigger
  Azure Cosmos DB trigger
  Durable Functions activity
  Durable Functions HTTP starter
  Durable Functions orchestrator
  Azure Event Grid trigger
  Azure Event Hub trigger
  HTTP trigger
  IoT Hub (Event Hub)
  Azure Queue Storage trigger
  RabbitMQ trigger
  SendGrid
  Azure Service Bus Queue trigger
  Azure Service Bus Topic trigger
  SignalR negotiate HTTP trigger
  Timer trigger
    :
    中略
    :
Python Templates:
  Azure Blob Storage trigger
  Azure Cosmos DB trigger
  Durable Functions activity
  Durable Functions HTTP starter
  Durable Functions orchestrator
  Azure Event Grid trigger
  Azure Event Hub trigger
  HTTP trigger
  Azure Queue Storage trigger
  RabbitMQ trigger
  Azure Service Bus Queue trigger
  Azure Service Bus Topic trigger
  Timer trigger
    :
    後略
    :

Functionの作成

$ func new           
Select a number for template:
1. Azure Blob Storage trigger
2. Azure Cosmos DB trigger
3. Durable Functions activity
4. Durable Functions HTTP starter
5. Durable Functions orchestrator
6. Azure Event Grid trigger
7. Azure Event Hub trigger
8. HTTP trigger
9. Azure Queue Storage trigger
10. RabbitMQ trigger
11. Azure Service Bus Queue trigger
12. Azure Service Bus Topic trigger
13. Timer trigger
Choose option: 13
Timer trigger
Function name: [TimerTrigger] 
Writing /Users/ituru/MyDevelops/AzureFunction/GraphAPI/AAD/TimerTrigger/readme.md
Writing /Users/ituru/MyDevelops/AzureFunction/GraphAPI/AAD/TimerTrigger/__init__.py
Writing /Users/ituru/MyDevelops/AzureFunction/GraphAPI/AAD/TimerTrigger/function.json
The function "TimerTrigger" was created successfully from the "Timer trigger" template.

Function作成されたものの確認

# デフォルトで作成されたディレクトリと各種ファイルの構成
$ tree -a
.
├── .funcignore
├── .gitignore
├── .python_packages
├── .vscode
│   └── extensions.json
├── Makefile
├── TimerTrigger
│   ├── __init__.py
│   ├── __pycache__
│   │   └── __init__.cpython-38.pyc
│   ├── function.json
│   └── readme.md
├── host.json
├── local.settings.json
└── requirements.txt

# TimeTrigger でデフォルトで定義されているスケジュールの確認(5分間隔での実行)
$ cat TimerTrigger/function.json 
{
  "scriptFile": "__init__.py",
  "bindings": [
    {
      "name": "mytimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 */5 * * * *"
    }
  ]
}                                                                                                              

Azure上でのクレデンシャル情報の定義

アクセストークン取得のために事前に必要なクレデンシャル情報については、この記事 を参考にして取得しました。
その取得したクレデンシャル情報(「CLIENT_ID」「CLIENT_KEY」「TENANT_ID」)を Azure Functionsの「関数アプリ」 ー 「構成」 ー 「+新しいアプリケーション設定」 で追加定義しておきます(保存を忘れずに)。

実行されるコードを以下のように変更してます。

__init__.py

import json
import os
import requests
import argparse
import time
from datetime import datetime, timezone, timedelta
from logging import getLogger, INFO
import azure.functions as func

# log出力
logger = getLogger()
logger.setLevel(INFO)

# Microsoft GraphAPI Info
TENANT_ID = os.environ['TENANT_ID']
CLIENT_ID = os.environ['CLIENT_ID']
CLIENT_KEY = os.environ['CLIENT_KEY']

# Azureアクセスのためのアクセストークンの取得
def get_azure_access_token() -> str:

    # access_token を取得するためのヘッダ情報
    headers = {
        'Accept': 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    payload = {
        'client_id': CLIENT_ID,
        'scope': 'https://graph.microsoft.com/.default',
        'grant_type': 'client_credentials',
        'client_secret': CLIENT_KEY
    }

    # access_token を取得するためのURLを生成
    TokenGet_URL = "https://login.microsoftonline.com/" + \
        TENANT_ID + "/oauth2/v2.0/token"
    # print(TokenGet_URL)

    # Microsoft Graphを実行し、その結果を取得
    response = requests.get(
        TokenGet_URL,
        headers=headers,
        data=payload
    )
    # requrest処理のクローズ
    response.close

    jsonObj = json.loads(response.text)

    return jsonObj["access_token"]


def main(mytimer: func.TimerRequest):
    # 日時の取得
    tz_jst = timezone(timedelta(hours=9))
    today = datetime.now(tz=tz_jst)

    start = time.time()
    access_token = get_azure_access_token()
    generate_time = time.time() - start

    logger.info(f"today: {today}")
    logger.info("取得時間:{0}".format(generate_time) + " [sec]")
    logger.info("取得アクセストークン:")
    logger.info(access_token)
requirements.txt
# Do not include azure-functions-worker as it may conflict with the Azure Functions platform

azure-functions
cerberus
requests
argparse

ローカルでコードを実行してみます

$ func host start
Found Python version 3.8.3 (python3).

Azure Functions Core Tools
Core Tools Version:       3.0.3388 Commit hash: fb42a4e0b7fdc85d74d509122ae 
Function Runtime Version: 3.0.15371.0

Missing value for AzureWebJobsStorage in local.settings.json. This is required for all triggers other than httptrigger, kafkatrigger. You can run 'func azure functionapp fetch-app-settings <functionAppName>' or specify a connection string in local.settings.json.

「local.settings.json」に「AzureWebJobsStorage」が定義されてないと怒られました、、、、
メッセージに従い、コマンドを実行します

$ func azure functionapp fetch-app-settings iturufunctest
App Settings:
Loading APPINSIGHTS_INSTRUMENTATIONKEY = *****
Loading APPLICATIONINSIGHTS_CONNECTION_STRING = *****
Loading AzureWebJobsStorage = *****
Loading CLIENT_ID = *****
Loading CLIENT_KEY = *****
Loading FUNCTIONS_EXTENSION_VERSION = *****
Loading FUNCTIONS_WORKER_RUNTIME = *****
Loading TENANT_ID = *****

再度、ローカルでコードを実行してみます

$ func host start
Found Python version 3.8.3 (python3).

Azure Functions Core Tools
Core Tools Version:       3.0.3388 Commit hash: fbdc8bd0bcfc8d743ff7d509ae 
Function Runtime Version: 3.0.15371.0

Functions:
    TimerTrigger: timerTrigger

For detailed output, run func with --verbose flag.
[2021-03-22T07:37:15.334Z] Worker process started and initialized.
[2021-03-22T07:37:20.508Z] Host lock lease acquired by instance ID '0000000000000000000000002CF30641'.
[2021-03-22T07:40:00.059Z] Executing 'Functions.TimerTrigger' (Reason='Timer fired at 2021-03-22T16:40:00.0196180+09:00', Id=29c7-323-497-8ca-ff4d505bd)
[2021-03-22T07:40:00.330Z] today: 2021-03-22 16:40:00.103535+09:00
[2021-03-22T07:40:00.330Z] 取得時間:0.21979808807373047 [sec]
[2021-03-22T07:40:00.331Z] 取得アクセストークン:
[2021-03-22T07:40:00.331Z] eyJ0eXAiOiJKV1QiLCJub25・・・中略・・・dFGuvWN-JdTy_-A
[2021-03-22T07:40:00.354Z] Executed 'Functions.TimerTrigger' (Succeeded,Id=29c7-323-497-8ca-ff4d505bd, Duration=326ms)

問題なく、ローカルで実行を確認できました。

Azureへのデプロイ

ローカルで動作確認したコードをAzureにデプロイします。

$ func azure functionapp publish iturufunctest
Getting site publishing info...
Creating archive for current directory...
Performing remote build for functions project.
Uploading 5.18 KB [###############################################################################]
Remote build in progress, please wait...
    :
    中略
    :
Uploading built content /home/site/artifacts/functionappartifact.squashfs for linux consumption function app…
Resetting all workers for iturufunctest.azurewebsites.net
Deployment successful.
Remote build succeeded!
Syncing triggers...
Functions in iturufunctest:
    TimerTrigger - [timerTrigger]

AzurePortalで確認してみます。問題なくコードが作成されています。

Azureでのコードの実行確認

今回は TimerTrigger でコードを作成(デフォルトの5分間隔で実行定義)しているため、デプロイ完了した時点から実行されています。 実行結果は以下で確認できました。

よもやの、、、、

数時間後の結果です。 5分間隔で実行されていません、、、、、どうしましょ。。。。
この記事に にありますように、Azure Portal から明示的にコードのRestart(無効化 → 有効化)を 10:50 頃に行いましたが結果は同じでした。やはり、syncfunctiontriggers を実行する必要があるようです。

参考情報

以下の情報を参考にさせていただきました。感謝申し上げます。
【備忘録】初めてのAzureFunctionsのデプロイ
Azure Functions での環境変数の切り替え
AzureCLIでAzureFunctionsの構築