Azure Container AppsでDaprによる定期実行を実装する

### はじめに

Azure Container Apps はコンテナ化したアプリをサーバーレスプラットフォームで実行できるフルマネージド環境です。  
マイクロサービス構築に適した Dapr にも対応しており、マイクロサービス間の抽象化された呼び出しや Pub/Sub、イベントバインドなどを使用することができます。
Azure Container Apps で定期処理を実装するために Dapr のイベントバインドを使用したところ、[Microsoft のドキュメント](https://learn.microsoft.com/ja-jp/azure/container-apps/microservices-dapr-bindings?pivots=nodejs)だけではなかなか理解できず、躓いたところがあったので記事にしてみました。

### 定期処理プログラム

定期処理の検証に使うプログラムは、Node.js + TypeScript で記述しています。  
定期処理として実行するのは単純な日時のログ出力です。  
サンプルコードを転記していますが、Microsoft のドキュメントで紹介されていたサンプルをさらにシンプルにした内容になっています。

<details><summary>サンプルコード</summary>

```Typescript
/*
Copyright 2021 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
     http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/*
dapr run --app-id batch-sample --app-port 5002 --resources-path ../components -- node ..
*/

import { DaprServer } from "dapr-client";

const cronBindingName = "samplecron";

const serverHost = "127.0.0.1";
const serverPort = process.env.SERVER_PORT || "5002";

const server = new DaprServer({
  serverHost,
  serverPort,
});

async function start() {
  await server.binding.receive(cronBindingName, processBatch);
  await server.start();
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

async function processBatch() {
  console.log(`Now is ${new Date()}`);
  return 0;
}

```

</details>

主要なファイルの構成は下記の通りです。

```
├─components
|  └─binding-cron.yaml
├─dist
   └─index.js
├─node_modules
└─src
   └─index.ts
package.json
```

<details><summary>package.json抜粋</summary>

```json
  "main": "dist/index.js",
  "scripts": {
    "build": "npx tsc",
    "start:prod": "node .",
    ...
```

</details>

binding-cron.yaml には、イベントバインドを定義します。  
10 秒毎にイベントが発行されるようにしています。

<details><summary>binding-cron.yaml</summary>

```yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: samplecron
spec:
  type: bindings.cron
  version: v1
  metadata:
    - name: schedule
      value: "@every 10s" # valid cron schedule
```

</details>

フォーマットの詳細は下記を参照ください。

https://docs.dapr.io/reference/components-reference/supported-bindings/cron/

### Dapr CLI のインストール

私は最初にここで躓きました。  
Windows11 で Git bash を使っており、Dapr CLI も最初は linux の手順で実施しようとしましたがうまく行かず、Windows 版も msi がウィルス対策ソフトに実行拒否されたりしましたが、最終的には、公式サイト記載の PowerShell のコマンドを実行し、その後、`dapr init`(私は最小構成の`dapr init --slim`で初期化しました)を実行することでインストールできました。

https://docs.dapr.io/getting-started/install-dapr-cli/

### ローカル実行

ルートフォルダで下記のような dapr コマンドを実行することで、定期処理が開始されます。

```
dapr run --app-id batch-sample --app-port 5002 --resources-path ./components -- node .
```

実行すると、下記のように 10 秒毎にログが出力されます。  
途中に出力されているポート 9411 への接続失敗ログは、監視用の Zipkin サーバをローカルでは起動していないために出力されていると思われます。

```
== APP == Now is Wed May 17 2023 08:05:01 GMT+0900 (日本標準時)
2023/05/17 08:05:07 request to http://localhost:9411/api/v2/spans failed: Post "http://localhost:9411/api/v2/spans": dial tcp [::1]:9411: connectex: No connection could be made because the target machine actively refused it.
== APP == Now is Wed May 17 2023 08:05:11 GMT+0900 (日本標準時)
2023/05/17 08:05:17 request to http://localhost:9411/api/v2/spans failed: Post "http://localhost:9411/api/v2/spans": dial tcp [::1]:9411: connectex: No connection could be made because the target machine actively refused it.
== APP == Now is Wed May 17 2023 08:05:21 GMT+0900 (日本標準時)
```

### Azure Container Apps へのデプロイ

Dockerfile を作成し、ACR にイメージをアップロード後、Azure Container Apps のコンテナーアプリとしてデプロイしてみます。

<details><summary>Dockerfile</summary>

```Dockerfile
# Node 18.x
ARG NODE_VERSION=18

# Build phase
FROM node:$NODE_VERSION AS builder

# Create app directory
WORKDIR /app

# A wildcard is used to ensure both package.json AND package-lock.json are copied
COPY package*.json ./

# Install app dependencies
RUN npm install

COPY . .

RUN npm run build

FROM node:$NODE_VERSION

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/dist ./dist

CMD [ "npm", "run", "start:prod" ]
```

</details>

デプロイする際に Dapr コンポーネントとしての設定が必要です。  
Azure ポータルで設定する場合は、まずコンテナーアプリの Dapr 設定を行います。

![スクリーンショット 2023-05-17 081257.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104149/44b0ee8c-9e67-051d-f063-38a5870b9bf4.png)

その後、Container Apps 環境側で Dapr コンポーネントを追加します。  
下記のような項目の設定が必要です。

```
名前: 任意
コンポーネントの種類: bindings.cron
バージョン: v1
メタデータ: 下記を追加
  schedule, 手動エントリ,任意のスケジュール("@every 10s"など)
アプリID: コンテナーアプリ側で設定したアプリID
```

ここで何を設定すればいいのかも躓きポイントでしたが、binding-cron.yaml で設定していた内容を反映するとよいようです。

設定が完了し、プロビジョニングが完了すると、定期処理が行われていることがログから確認できました。

```
2023-05-16T23:22:55.990033004Z > node .
2023-05-16T23:22:55.990037204Z
2023-05-16T23:22:57.155780266Z 2023-05-16T23:22:57.154Z INFO [HTTPServer, HTTPServer] Listening on 5002
2023-05-16T23:22:57.156322674Z 2023-05-16T23:22:57.156Z INFO [HTTPServer, HTTPServer] Registering 0 PubSub Subscriptions
2023-05-16T23:22:57.168941870Z 2023-05-16T23:22:57.168Z INFO [HTTPClient, HTTPClient] Sidecar Started
2023-05-16T23:23:07.009585460Z Now is Tue May 16 2023 23:23:07 GMT+0000 (Coordinated Universal Time)
2023-05-16T23:23:17.002580442Z Now is Tue May 16 2023 23:23:17 GMT+0000 (Coordinated Universal Time)
2023-05-16T23:23:27.001924053Z Now is Tue May 16 2023 23:23:27 GMT+0000 (Coordinated Universal Time)
```