DevContainerでAzure Durable Functionsを試す

以下のQuickStartに従い、PythonでDurable Functionsを作ってみる。最近はDev Containerに興味があるので、こちらを使ってみる。

https://learn.microsoft.com/ja-jp/azure/azure-functions/durable/quickstart-python-vscode?tabs=linux%2Cvs-code-set-indexing-flag&pivots=python-mode-decorators

# 環境設定
## AzureでFunctionsを作成する
これはAzure上で従量課金プランでAzure Functionsのリソースを作成するだけ。特に何も特別なこともしていないのでスキップ。

durablefunという名前で作りました。

## Dev Containerの準備
PythonでAzure Functionsを開発するためのDev Containerを立ち上げる。しばやんさんのイメージを使う。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/8d6bbdf2-30c1-4760-854f-06d8e27caef3.png)
Pythonを選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/20b3bcda-a3ba-061b-0ee5-84c3d0e737da.png)
Azure Functionsの最新サポートバージョンは3.11なのでそれにする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/a6c30955-b289-db85-7712-2733e42e54b1.png)
初回は１０分ほどかかった。こんな風になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/550bf059-68bc-1c46-1a38-49ff1f6e29b3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/531af060-1a42-3b6a-6620-b751d8af9cf8.png)

feature、拡張機能は以下のようなものが入っていた。
```devcontainer.json
  "features": {
    "ghcr.io/jlaundry/devcontainer-features/azure-functions-core-tools:1": {},
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-azuretools.vscode-azurefunctions",
        "ms-azuretools.vscode-docker",
        "ms-python.python"
      ]
    }
  },
```

## Python環境
Durable Functionsの開発のために必要なものを入れる。
```Requirements.txt
azure-functions
azure-functions-durable
```

```bash
vscode ➜ /workspaces/DurableFunction $ python -m venv venv
vscode ➜ /workspaces/DurableFunction $ venv/bin/activate
```

以下の記事に従って、venvとvscodeをcacheするために、mountsを追加。

https://qiita.com/smr1/items/137b912de86c4947ead0

```devcontainer.json
{
  "name": "Azure Functions (Python 3)",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
  "forwardPorts": [
    7071,
    10000,
    10001,
    10002
  ],
  "otherPortsAttributes": {
    "onAutoForward": "ignore"
  },
  "features": {
    "ghcr.io/jlaundry/devcontainer-features/azure-functions-core-tools:1": {},
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-azuretools.vscode-azurefunctions",
        "ms-azuretools.vscode-docker",
        "ms-python.python"
      ]
    }
  },
    "mounts": [
    "source=vscode-extensions,target=/vscode/.vscode-server/extensions,type=volume",
    {
      "type": "volume",
      "source": "myprj-cache-vscode",
      "target": "/home/vscode/.cache"
    },
    {
      "type": "volume",
      "source": "myprj-venv",
      "target": "${containerWorkspaceFolder}/.venv"
    }
  ]
}
```
devcontainer.jsonを更新したので、Rebuilld Dev Containerをして、次は10秒程度で起動するようになりました。

# 開発
拡張機能でCreate new Azure Functionsを選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/984c4169-62cc-8472-7eb3-49dd73b4b434.png)
HTTPトリガーで作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/8e0ab691-b703-ab4a-f8cf-1f9cef678444.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/d2942463-96aa-0fc7-9814-b2da7c3a76b6.png)

Add new setting
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/87d04956-4c57-f971-c868-fcf8de05acdb.png)

Azureにサインイン

流れで環境変数をAzure側にも作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/40e66679-8cca-3886-b38e-2dea31207f46.png)

```function_app.py
import azure.functions as func
import azure.durable_functions as df

myApp = df.DFApp(http_auth_level=func.AuthLevel.FUNCTION)

# An HTTP-triggered function with a Durable Functions client binding
@myApp.route(route="orchestrators/{functionName}")
@myApp.durable_client_input(client_name="client")
async def http_start(req: func.HttpRequest, client):
    function_name = req.route_params.get('functionName')
    instance_id = await client.start_new(function_name)
    response = client.create_check_status_response(req, instance_id)
    return response

# Orchestrator
@myApp.orchestration_trigger(context_name="context")
def hello_orchestrator(context):
    result1 = yield context.call_activity("hello", "Seattle")
    result2 = yield context.call_activity("hello", "Tokyo")
    result3 = yield context.call_activity("hello", "London")

    return [result1, result2, result3]

# Activity
@myApp.activity_trigger(input_name="city")
def hello(city: str):
    return f"Hello {city}"
```

# デバッグ

そのままローカル上でデバッグしようとしたところ、以下のエラーになった。Storage Accountに接続できていないらしい。

```
[2025-01-25T02:58:09.754Z] A host error has occurred during startup operation '5690128b-8db5-4f7b-bc11-69c78fb57d31'.
[2025-01-25T02:58:09.774Z] Microsoft.Azure.WebJobs.Extensions.DurableTask: Unable to resolve the Azure Storage connection named 'Storage'.
Value cannot be null. (Parameter 'provider')
[2025-01-25T02:58:09.808Z] Host startup operation has been canceled
[2025-01-25T02:58:09.836Z] Exception encountered while listening to EventStream
[2025-01-25T02:58:09.836Z] System.Private.CoreLib: The request stream was aborted. The HTTP/2 connection faulted.
[2025-01-25T02:58:09.889Z] The Azure Storage connection string is either empty or invalid. Unable to record diagnostic events, so the diagnostic logging service is being stopped.
[2025-01-25T02:58:09.889Z] Azure.Data.Tables: Value cannot be null. (Parameter 'connectionString').
[2025-01-25T02:58:09.890Z] Unable to get table reference. Aborting write operation.
```

が、しばやんさんが作ってくれたDev Containerのイメージでは、Azuriteの設定はしてくれている。
```docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    platform: linux/amd64
    volumes:
      - ../..:/workspaces:cached
    command: sleep infinity
    network_mode: service:azurite

  azurite:
    image: mcr.microsoft.com/azure-storage/azurite
    restart: unless-stopped
```
調べてみたところ、Azure FunctionsをデバッグするときはVSCodeの設定もしてやる必要がある。localsettings.jsonで設定の追加が必要らしい。
```local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "AzureWebJobsDashboard": "UseDevelopmentStorage=true"
  }
}
```

正常にデバッグされ、エンドポイントが表示された。

```
Functions:

        http_start:  http://localhost:7071/api/orchestrators/{functionName}

        hello: activityTrigger

        hello_orchestrator: orchestrationTrigger
```

Postmanでたたいてみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/60887f8d-18b9-e247-73ae-7a09bc6bdb1a.png)

```
{
    "id": "583f5b37ee2a4c4b829e728a6741c220",
    "statusQueryGetUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/583f5b37ee2a4c4b829e728a6741c220?taskHub=TestHubName&connection=Storage&code=Avb4lMcPpaQnisWEc5fm6JzHLEkPnDGgtGESzDchauzqAzFu5eA6wA==",
    "sendEventPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/583f5b37ee2a4c4b829e728a6741c220/raiseEvent/{eventName}?taskHub=TestHubName&connection=Storage&code=Avb4lMcPpaQnisWEc5fm6JzHLEkPnDGgtGESzDchauzqAzFu5eA6wA==",
    "terminatePostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/583f5b37ee2a4c4b829e728a6741c220/terminate?reason={text}&taskHub=TestHubName&connection=Storage&code=Avb4lMcPpaQnisWEc5fm6JzHLEkPnDGgtGESzDchauzqAzFu5eA6wA==",
    "rewindPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/583f5b37ee2a4c4b829e728a6741c220/rewind?reason={text}&taskHub=TestHubName&connection=Storage&code=Avb4lMcPpaQnisWEc5fm6JzHLEkPnDGgtGESzDchauzqAzFu5eA6wA==",
    "purgeHistoryDeleteUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/583f5b37ee2a4c4b829e728a6741c220?taskHub=TestHubName&connection=Storage&code=Avb4lMcPpaQnisWEc5fm6JzHLEkPnDGgtGESzDchauzqAzFu5eA6wA==",
    "restartPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/583f5b37ee2a4c4b829e728a6741c220/restart?taskHub=TestHubName&connection=Storage&code=Avb4lMcPpaQnisWEc5fm6JzHLEkPnDGgtGESzDchauzqAzFu5eA6wA==",
    "suspendPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/583f5b37ee2a4c4b829e728a6741c220/suspend?reason={text}&taskHub=TestHubName&connection=Storage&code=Avb4lMcPpaQnisWEc5fm6JzHLEkPnDGgtGESzDchauzqAzFu5eA6wA==",
    "resumePostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/583f5b37ee2a4c4b829e728a6741c220/resume?reason={text}&taskHub=TestHubName&connection=Storage&code=Avb4lMcPpaQnisWEc5fm6JzHLEkPnDGgtGESzDchauzqAzFu5eA6wA=="
}
```

statusQueryGetUriをたたいてみると処理結果が表示された。
```
{
    "name": "hello_orchestrator",
    "instanceId": "583f5b37ee2a4c4b829e728a6741c220",
    "runtimeStatus": "Completed",
    "input": null,
    "customStatus": null,
    "output": [
        "Hello Seattle",
        "Hello Tokyo",
        "Hello London"
    ],
    "createdTime": "2025-01-25T03:21:19Z",
    "lastUpdatedTime": "2025-01-25T03:21:34Z"
}
```

## VSCodeの設定
pip installでライブラリをインストールしているのに、import <ライブラリ>の下に黄色い波線が表示され、インテリセンスも効かないときがあった。そういう時はDev ContainerのVSCodeの設定で、Pylance拡張機能の設定を更新したところ直った。
"Pylance" を検索し、"Python > Analysis: Type Checking Mode" を "basic" に設定。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/49a79bda-6e13-0d6b-e039-9837e240ffc8.png)


# Azureにデプロイしてみる
拡張機能が入って入っているのでサインイン。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/0933a965-2267-11dd-7de2-2454c5d81c25.png)
拡張機能でデプロイ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/801ff4a8-9cb2-b702-9956-0c69d7539124.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/73a67ea0-923a-5f90-a77c-e7ad95d331ca.png)

Azure上のものを叩いて、正常にレスポンスが返ってくることをかくに
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654538/4f399146-a739-f6e3-46d5-56574456f3ff.png)