はじめに
本記事は Microsoft Azure Tech Advent Calendar 2024 12/09 の記事です。
本記事では先日 GA された Azure App Service で Linux アプリのサイドカー コンテナー機能を用いて Dapr を利用してみます。
サイドカー機能については、下記記事も参考にしていただけますと幸いです。
上記記事では、サイドカーで ManagedId が利用できることや、環境変数 Application Setting の継承、App Service Storage が利用できることを確認しています。
Daprサイドカーをどうやって起動するか
Dapr のホスティングオプションとしては
以下の 3 つがあります。
- Run Dapr in self-hosted mode
- Deploy and run Dapr in Kubernetes mode
- Run Dapr in a serverless offering
App Service 上で動作させるには、1つ目の self-hosted モードとして動作させる必要があります。なお、serverless offering として Azure Container Apps が Dapr の動作をサポートしています。(そっちを使おう)
App Service Linux の仕組みとしてはコンテナの起動方法にユーザーが作用することはできません。ゆえに、dapr run
コマンドでメインとなるコンテナと共に dapr サイドカーを起動させるといったこともできません。
そのため、Run Dapr in self-hosted mode without Docker にあるように、daprd
を実行するコンテナをサイドカーとして自力で用意する必要があります。
dapr コンテナの構成
こちら を参考に、daprd
が動作するコンテナを用意します。
FROM alpine:latest
RUN apk add --no-cache wget bash
RUN wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash
# Install daprd
RUN dapr init --slim
COPY components/ /root/.dapr/components/
COPY sshd_config /etc/ssh/
COPY entrypoint.sh ./
# Start and enable SSH
RUN apk add openssh \
&& echo "root:Docker!" | chpasswd \
&& chmod +x ./entrypoint.sh \
&& cd /etc/ssh/ \
&& ssh-keygen -A
# 3500: Dapr HTTP port, 50001: Dapr gRPC port, 2223: SSH port, 9999: Dapr dashboard port
EXPOSE 3500 50001 2223
ENTRYPOINT [ "./entrypoint.sh" ]
entrypoint.sh
では、--app-id ${APPID} --app-port ${APPPORT}
でメインとなるアプリケーションコンテナを指定して daprd
を開始します。
#!/bin/sh
set -e
cat >/etc/motd <<EOL
_________________
Dapr container
_________________
EOL
cat /etc/motd
# Get environment variables to show up in SSH session
# This will replace any \ (backslash), " (double quote), $ (dollar sign) and ` (back quote) symbol by its escaped character to not allow any bash substitution.
(printenv | sed -n "s/^\([^=]\+\)=\(.*\)$/export \1=\2/p" | sed 's/\\/\\\\/g' | sed 's/"/\\\"/g' | sed 's/\$/\\\$/g' | sed 's/`/\\`/g' | sed '/=/s//="/' | sed 's/$/"/' >> /etc/profile)
/usr/sbin/sshd
# APPID should come from environment variable, if it not set, use default value as "main"
APPID=${APPID:-main}
APPPORT=${APPPORT:-9000}
DAPR_LOGLEVEL=${DAPR_LOGLEVEL:-debug}
# nohup dapr dashboard -p 9999 &
/root/.dapr/bin/daprd --resources-path /root/.dapr/components/ --log-level ${DAPR_LOGLEVEL} --enable-api-logging --log-as-json --app-id ${APPID} --app-port ${APPPORT} #--enable-metrics --enable-app-health-check --app-health-check-path /.internal/healthz
今回コンテナ内では daprd
の起動のみを行っています。
通常 dapr run
で開始した際に合わせて起動される placement
などは用意していません。
Dapr initialization includes:
Running a Redis container instance to be used as a local state store and message broker.
Running a Zipkin container instance for observability.
Creating a default components folder with component definitions for the above.
Running a Dapr placement service container instance for local actor support.
Running a Dapr scheduler service container instance for job scheduling.
つまり、Actor などこれらに依存する component は利用できないことが見込まれます。(未検証ですが個別に全部用意してあげれば動くとは思います。。。)
ssh接続用のセットアップについては下記を参照してください。
daprで有効にするcomponentsの設定
今回は Binding コンポーネントを 2 種類用意してみました。
1つ目は cron インプットバインディングとし、10秒ごとに /.internal/batch
エンドポイントを呼び出します。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: batch
spec:
type: bindings.cron
version: v1
metadata:
- name: schedule
value: "@every 10s" # valid cron schedule
- name: direction
value: "input"
- name: route
value: "/.internal/batch"
もう一方は Blob Storage アウトプットバインディングとし、マネージドIDを利用して(accountKeyを指定せずに)構成します。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: blob
spec:
type: bindings.azure.blobstorage
version: v1
metadata:
- name: accountName
value: toshidasandboxstorage
- name: containerName
value: container1
Azure Functions のタイマートリガーで Blob アウトプットバインディングを利用するみたいなものですね。
今回はイメージビルド時に COPY components/ /root/.dapr/components/
として決めてしまっていますが、$home
配下に配置したファイルなどを利用するのもいいかもしれません。
App Service の 環境変数(Application Settings) を利用したい場合は、entryPoint.sh 内で sed
等を利用する必要がありそうです。
アプリケーション側コード
Dockerfile自体は特にDaprを使うためになにかをする必要はありません。
1点注意点としては、メインコンテナとサイドカーコンテナがどういった順番で起動されるかがはっきりしません。
そのため、アプリケーションコード側で DaprClientを作成したタイミングでサイドカーコンテナが起動していないとエラーとなることがありました。
そこで、entrypoint.sh 内で サイドカーコンテナの起動が確認できたのちに、アプリケーションプロセスを開始するといったことをやっています。
#!/bin/sh
set -e
cat >/etc/motd <<EOL
_________________
Main container
_________________
EOL
cat /etc/motd
CONTAINER_INFO_FROM_CGROUP=$(cat /proc/self/cgroup | head -1| rev | cut -d "/" -f 1 | rev)
logevent() {
echo $(date "+%Y-%m-%dT%H:%M:%S.%3N%z") "[EntryPoint.backend] CONTAINER_INFO_FROM_CGROUP:${CONTAINER_INFO_FROM_CGROUP}, Message: $1"
}
handler15() {
logevent "'SIGTERM received"
}
trap handler15 15 # SIGTERM
logevent "log environment vars with printenv"
printenv
# Get environment variables to show up in SSH session
# This will replace any \ (backslash), " (double quote), $ (dollar sign) and ` (back quote) symbol by its escaped character to not allow any bash substitution.
(printenv | sed -n "s/^\([^=]\+\)=\(.*\)$/export \1=\2/p" | sed 's/\\/\\\\/g' | sed 's/"/\\\"/g' | sed 's/\$/\\\$/g' | sed 's/`/\\`/g' | sed '/=/s//="/' | sed 's/$/"/' >> /etc/profile)
logevent "Starting sshd" "START"
/usr/sbin/sshd
# Wait dapr sidecar is up, if not wait for 5 seconds
logevent "Waiting for dapr container to be up..."
while ! nc -z localhost 3500; do
logevent "Waiting for dapr container to be up... sleep 5 sec"
sleep 5
done
logevent "dapr container is up"
logevent "Starting node process"
node /app/dist/index.js
Cronバインディングから呼び出されるエンドポイント
let batchLastCalledTime: string = "00:00:00";
app.post('/.internal/batch', async (c) => {
batchLastCalledTime = new Date().toLocaleTimeString();
if (!client) {
client = new DaprClient({daprHost, daprPort: daprGrpcPort, communicationProtocol});
}
const bindingName = "blob";
const bindingOperation = "create";
const data = batchLastCalledTime;
const metadata = {
"blobName": `batch_${Date.now()}.txt`,
};
await client.binding.send(bindingName, bindingOperation, data, metadata);
return c.text('Hello Internal batch');
});
app.get('/batchstatus', (c) => {
return c.text('batchLastCalledTime:' + batchLastCalledTime);
});
今回の例では実施していませんが、 /.internal
としている理由は、このエンドポイントはパブリックにはアクセスできないことを想定しているためです。
こちらの記事で試したように、Envoy 等をフロントプロキシとして用意することで、制御できる想定です。
結果
無事 Blob コンテナ側にバッチで動作したファイルが作成されていることが確認できました。
サンプルコード
所感
- 抽象化レイヤーとして用いるのはありかなと思いました。すべて DaprClientで実装できるのはうれしい。個別のサービスのSDK等をアプリケーション側に依存関係として持つ必要がなくなります。
- 一方で記事を作成するにあたり、サイドカーの効用を調べたり、いろいろ試しましたが、それ Container Apps でいいのでは、Azure Functions でできるのでは?といったことがままありました。
無理にやらなくても、マネージドサービス側で任せられるところは任せていいのではないかなと思います。
例えば、Secrets や Configuration コンポーネントについては、App Service ではすでに App Settings が KeyVaults 参照、App Configuration 参照が利用できるため、あえてサイドカーの dapr でやる必要性はありません。
サービスバインディングについては、Azure Functions で実装できるものは Azure Functions で実装してもいいかなと思いました。
マイクロサービス間の service invocation については、わざわざ App Service 内で複数のマイクロサービスを立てる需要もなさそう。。
参考