Pulumiで極めるAzure Container Apps + Dapr:プロダクショングレード・マイクロサービス基盤の完全自動化
著者: 伝説的SREアーキテクト
—
Kubernetesの複雑性(YAML地獄、CiliumやIstioの運用コスト、Nodeプールのサイジング地獄)に疲弊したすべてのインフラエンジニアに告ぐ。
今、我々が選択すべき究極の解は Azure Container Apps (ACA) と Dapr (Distributed Application Runtime) の融合だ。
しかし、Azure Portalや生半可なTerraformスクリプトでこれを構築してはならない。リソース間の暗黙の依存関係、Daprコンポーネントのライフサイクル管理、そして何より「真の冪等性」を担保するためには、プログラミング言語の表現力をフルに活かした Pulumi によるコード化が唯一にして絶対の正解である。
今回は、単なる「動くコード」ではない。プロダクション環境で容赦なく降りかかるトラフィック、セキュリティ要件、そしてコスト最適化を極限まで追求した、最高峰のPulumiインフラストラクチャコードを紐解く。
—
1. アーキテクチャの全貌と設計思想
今回構築する基盤は、以下の要件を完全に満たす。
1. 完全なネットワーク分離: イングレスはACA環境の内部ロードバランサーを経由し、外部からの直接アクセスを遮断。
2. Daprサイドカーによる横断的関心事の分離: サービス間通信は mTLS で暗号化し、ステート管理やPub/Subをアプリケーションコードから完全に切り離す。
3. インフラのモジュール化とゼロ・ハードコーディング: プレフィックスと環境変数による完全なマルチテナント対応。
[Internet]
│ (HTTPS)
▼
[Azure App Gateway / Front Door (Optional)]
│
▼
[Container Apps Environment (Internal vNet)]
├── Service A (App) + Dapr Sidecar ──(mTLS)──> Service B (App) + Dapr Sidecar
└── Dapr StateStore (CosmosDB / Redis)
これをTypeScript版Pulumiを用いて、ワンコマンドで無から有へ創り出す。
—
2. Pulumiコード実装:基盤の構築
まずは、リソースグループ、VNet、Log Analytics Workspace、そして Container Apps Environment を定義する。
ここで重要なのは、「リソース間の暗黙の依存関係に頼らず、明示的に `dependsOn` や入出力をバインドする」ことだ。
プロジェクト構成
.
├── Pulumi.yaml
├── Pulumi.prod.yaml
├── index.ts
└── package.json
`index.ts` (コアインフラストラクチャの定義)
import as pulumi from “@pulumi/pulumi”;
import as azure-native from “@pulumi/azure-native”;
// コンフィグの読み込み(マジックナンバーの排除)
const config = new pulumi.Config();
const environmentName = config.require(“environment”); // e.g., “prod”
const location = config.require(“location”); // e.g., “japaneast”
// 1. リソースグループの作成
const rg = new azure_native.resources.ResourceGroup(`rg-aca-${environmentName}`, {
resourceGroupName: `rg-microservices-${environmentName}`,
location: location,
tags: {
Environment: environmentName,
ManagedBy: “Pulumi”,
Architecture: “ACA-Dapr”,
},
});
// 2. セキュアなVNetおよびサブネットの構築(ACA内部環境用)
const vnet = new azure_native.network.VirtualNetwork(`vnet-${environmentName}`, {
resourceGroupName: rg.name,
location: rg.location,
virtualNetworkName: `vnet-microservices-${environmentName}`,
addressSpace: {
addressPrefixes: [“10.0.0.0/16”],
},
});
const acaSubnet = new azure_native.network.Subnet(`subnet-aca-${environmentName}`, {
resourceGroupName: rg.name,
virtualNetworkName: vnet.name,
subnetName: `subnet-aca`,
addressPrefix: “10.0.0.0/23”,
delegations: [{
name: “Microsoft.App.environments”,
serviceName: “Microsoft.App/environments”,
}],
});
// 3. ログ収集基盤 (Log Analytics Workspace)
const logAnalyticsWorkspace = new azure_native.operationalinsights.Workspace(`law-${environmentName}`, {
resourceGroupName: rg.name,
location: rg.location,
workspaceName: `law-microservices-${environmentName}`,
sku: {
name: “PerGB2018”,
},
retentionInDays: 30,
});
// 4. Container Apps Environment (内部VNet統合型)
const acaEnv = new azure_native.app.ManagedEnvironment(`aca-env-${environmentName}`, {
resourceGroupName: rg.name,
location: rg.location,
environmentName: `cae-microservices-${environmentName}`,
appLogsConfiguration: {
destination: “log-analytics”,
logAnalyticsConfiguration: {
customerId: logAnalyticsWorkspace.customerId,
sharedKey: logAnalyticsWorkspace.primarySharedKey,
},
},
vnetConfiguration: {
internal: true, // 内部ロードバランサーのみ(外部からの直接露出を防止)
infrastructureSubnetId: acaSubnet.id,
},
zoneRedundant: true, // 高可用性の担保(可用性ゾーンの活用)
}, { dependsOn: [acaSubnet] });
// エクスポート(後続のマイクロサービス定義で使用)
export const environmentId = acaEnv.id;
export const defaultDomain = acaEnv.defaultDomain;
—
3. Daprサイドカー統合マイクロサービスのコード化
次に、Daprサイドカーを有効化したマイクロサービス(例:注文処理サービス `order-service`)をデプロイする。
ACAでは、Container Appリソースのプロパティとして `dapr` 設定を埋め込むだけで、サイドカーのインジェクションとライフサイクル管理が完全に自動化される。
// 注文処理用マイクロサービスの定義
const orderService = new azure_native.app.ContainerApp(`order-service-${environmentName}`, {
resourceGroupName: rg.name,
containerAppName: `order-service`,
managedEnvironmentId: acaEnv.id,
configuration: {
// Daprの有効化
dapr: {
enabled: true,
appId: “order-service”,
appPort: 8080, // アプリケーションがリッスンしているポート
appProtocol: “http”, // http または gRPC
logLevel: “info”,
enableApiLogging: true,
},
// イングレス設定(内部通信のみ許可)
ingress: {
external: false, // 内部VNet内からのアクセスのみ
targetPort: 8080,
transport: “auto”,
allowInsecure: false,
},
},
template: {
// リソース制限の最適化(CPU/メモリの過不足を排除)
scale: {
minReplicas: 1, // コールドスタート対策として常時1台は維持。0へのスケールダウンは “minReplicas: 0”
maxReplicas: 10,
rules: [
{
name: “http-scaling-rule”,
http: {
metadata: {
concurrentRequests: “50”, // 1インスタンスあたり50リクエストでスケールアウト
},
},
},
],
},
containers: [
{
name: “order-processor”,
image: “mycontainerregistry.azurecr.io/order-service:v1.0.0”,
resources: {
cpu: 0.5,
memory: “1.0Gi”, // マイクロサービスにおける黄金比率の初期値
},
env: [
{
name: “ASPNETCORE_ENVIRONMENT”,
value: “Production”,
},
{
name: “DAPR_HTTP_PORT”,
value: “3500”, // デフォルトのDaprサイドカーポート
}
],
},
],
},
});
—
4. エキスパート向けハック:Daprコンポーネントの動的プロビジョニング
Daprの真価は、State StoreやPub/Subブローカー(Azure Service Busなど)をコードから抽象化できる点にある。
Pulumiを使って、Azure Service Busをプロビジョニングし、それをDaprのPub/SubコンポーネントとしてACA環境にバインドするコードを記述する。
import as servicebus from “@pulumi/azure-native/servicebus”;
// 1. Azure Service Bus Namespaceの作成
const sbNamespace = new azure_native.servicebus.Namespc(`sb-namespace-${environmentName}`, {
resourceGroupName: rg.name,
location: rg.location,
namespaceName: `sb-microservices-${environmentName}`,
sku: {
name: “Standard”,
tier: “Standard”,
},
});
// 2. 接続文字列の取得(ListKeys APIのラップ)
const sbAuthRule = azure_native.servicebus.listNamespaceKeysOutput({
resourceGroupName: rg.name,
namespaceName: sbNamespace.name,
authorizationRuleName: “RootManageSharedAccessKey”,
});
const connectionString = sbAuthRule.primaryConnectionString;
// 3. Dapr Pub/Subコンポーネントの定義
const daprPubSubComponent = new azure_native.app.ManagedEnvironmentComponent(`dapr-pubsub-${environmentName}`, {
resourceGroupName: rg.name,
environmentName: acaEnv.name,
componentName: “pubsub”, // アプリ側で指定するコンポーネント名
componentType: “pubsub.azure.servicebus.queues”,
version: “v1”,
metadata: [
{
name: “connectionString”,
secretRef: “sb-connection-string”, // セキュリティのためシークレットとして渡す
},
],
secrets: [
{
name: “sb-connection-string”,
value: connectionString,
},
],
}, { dependsOn: [acaEnv, sbNamespace] });
このコードにより、アプリケーションコード側では `http://localhost:3500/v1.0/publish/pubsub/orders` にPOSTするだけで、インフラ側の実装を一切意識せずにメッセージングが成立する。
—
5. 運用・保守の自動化とパフォーマンス最適化の極意
1. ゼロ・ダウンタイムデプロイメントの担保
Pulumiはデフォルトでリソースの更新時にプレビューと差分検知を行う。Container Appのイメージタグ(`v1.0.0` -> `v1.0.1`)が変更された場合、ACAは自動的にローリングアップデートを実行する。
しかし、大規模トラフィック環境下では、Daprサイドカーの初期化完了(ヘルスチェック)を待たずにトラフィックが流れるとエラーが発生する。
そのため、Container Appのテンプレートに `probes`(Liveness / Readiness / Startup)を必ず定義せよ。
// 抜粋: プローブの設定
probes: [
{
type: “Readiness”,
httpGet: {
path: “/health/ready”,
port: 8080,
},
initialDelaySeconds: 5,
periodSeconds: 10,
},
{
type: “Liveness”,
httpGet: {
path: “/health/live”,
port: 8080,
},
initialDelaySeconds: 15,
periodSeconds: 20,
}
]
2. メモリ消費とコールドスタートの最適化
- CPU / メモリ比率: ACAでは `CPU: 0.5 / Memory: 1.0Gi` がコストとパフォーマンスのスイートスポットである。これ未満にすると、Daprサイドカー(Go製で軽量だがメモリを食う)が原因でOOM Killerの餌食になる。
- 常時起動 (`minReplicas: 1`) の徹底: 本番環境において、コストをケチって `minReplicas: 0` に設定してはならない。Daprサイドカーの初期化(mTLS証明書の取得やサイドカーの立ち上がり)を含めると、コールドスタートに3〜5秒かかるため、レイテンシ要件の厳しいマイクロサービスでは致命傷となる。
—
結び:インフラを「コード」ではなく「ソフトウェア」として扱え
TerraformのHclによる静的な設定に別れを告げよ。
Pulumiを用いることで、条件分岐、ループ、型安全性、そしてテスト駆動インフラ(TDI)が現実のものとなる。
Azure Container AppsとDapr、そしてPulumi。この3つを極めた時、あなたの組織のデプロイ速度は次元の違う領域へと到達する。
さあ、エディターを開き、完璧なインフラストラクチャをコンパイルしよう。