【テクニカル・上級編】PulumiでAzure Container AppsとDaprを組み合わせたマイクロサービス基盤のコード化手順 – インフラ構成管理(IaC)活用バイブル

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つを極めた時、あなたの組織のデプロイ速度は次元の違う領域へと到達する。
さあ、エディターを開き、完璧なインフラストラクチャをコンパイルしよう。

タイトルとURLをコピーしました