Azure Container Apps によるマイクロサービス構築完全ガイド
Azure Container Apps によるマイクロサービス構築完全ガイド
はじめに
Azure Container Appsは、サーバーレスコンテナーの実行環境を提供する完全マネージドサービスです。Kubernetesの複雑さを意識することなく、マイクロサービスアーキテクチャを実現できます。本記事では、実践的なマイクロサービス構築方法を解説します。
Azure Container Appsの主要機能
1. 自動スケーリング
# スケーリング設定例
scale:
minReplicas: 1
maxReplicas: 100
rules:
- name: http-rule
http:
metadata:
concurrentRequests: 100
- name: cpu-rule
custom:
type: cpu
metadata:
type: Utilization
value: 70
2. リビジョン管理とトラフィック分割
// Bicepテンプレートでのトラフィック分割
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
name: appName
location: location
properties: {
configuration: {
ingress: {
traffic: [
{
revisionName: 'myapp--revision1'
weight: 80
}
{
revisionName: 'myapp--revision2'
weight: 20
label: 'canary'
}
]
}
}
}
}
マイクロサービスアーキテクチャの実装
1. サービス設計
// Order Service - ASP.NET Core Minimal API
using Microsoft.AspNetCore.Mvc;
using Dapr.Client;
var builder = WebApplication.CreateBuilder(args);
// Dapr Client の追加
builder.Services.AddSingleton<DaprClient>(new DaprClientBuilder().Build());
// Application Insights
builder.Services.AddApplicationInsightsTelemetry();
var app = builder.Build();
// Health check endpoint
app.MapGet("/health", () => Results.Ok(new { status = "Healthy" }));
// Order API endpoints
app.MapPost("/api/orders", async (
[FromBody] CreateOrderRequest request,
[FromServices] DaprClient daprClient) =>
{
try
{
// 在庫サービスの呼び出し
var inventoryResponse = await daprClient.InvokeMethodAsync<CheckInventoryRequest, CheckInventoryResponse>(
"inventory-service",
"check-inventory",
new CheckInventoryRequest { ProductIds = request.ProductIds }
);
if (!inventoryResponse.Available)
{
return Results.BadRequest(new { error = "在庫が不足しています" });
}
// 注文の作成
var order = new Order
{
Id = Guid.NewGuid().ToString(),
CustomerId = request.CustomerId,
Products = request.Products,
TotalAmount = request.TotalAmount,
Status = OrderStatus.Pending,
CreatedAt = DateTime.UtcNow
};
// State Store に保存
await daprClient.SaveStateAsync("statestore", order.Id, order);
// イベントの発行
await daprClient.PublishEventAsync(
"pubsub",
"order-created",
new OrderCreatedEvent
{
OrderId = order.Id,
CustomerId = order.CustomerId,
TotalAmount = order.TotalAmount
}
);
return Results.Created($"/api/orders/{order.Id}", order);
}
catch (Exception ex)
{
app.Logger.LogError(ex, "Failed to create order");
return Results.Problem("注文の作成に失敗しました");
}
});
app.MapGet("/api/orders/{orderId}", async (
string orderId,
[FromServices] DaprClient daprClient) =>
{
var order = await daprClient.GetStateAsync<Order>("statestore", orderId);
return order != null ? Results.Ok(order) : Results.NotFound();
});
app.Run();
// DTOs
public record CreateOrderRequest(
string CustomerId,
List<Product> Products,
decimal TotalAmount,
List<string> ProductIds
);
public record CheckInventoryRequest(List<string> ProductIds);
public record CheckInventoryResponse(bool Available);
public record Order
{
public string Id { get; init; }
public string CustomerId { get; init; }
public List<Product> Products { get; init; }
public decimal TotalAmount { get; init; }
public OrderStatus Status { get; init; }
public DateTime CreatedAt { get; init; }
}
public record Product(string Id, string Name, decimal Price, int Quantity);
public enum OrderStatus { Pending, Processing, Shipped, Delivered, Cancelled }
public record OrderCreatedEvent(
string OrderId,
string CustomerId,
decimal TotalAmount
);
2. Daprを使用したサービス間通信
# Dapr Component - State Store (Azure Cosmos DB)
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.azure.cosmosdb
version: v1
metadata:
- name: url
value: https://mycosmosaccount.documents.azure.com:443/
- name: masterKey
secretKeyRef:
name: cosmos-key
key: masterKey
- name: database
value: ordersdb
- name: collection
value: orders
- name: partitionKey
value: customerId
---
# Dapr Component - Pub/Sub (Azure Service Bus)
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.azure.servicebus
version: v1
metadata:
- name: connectionString
secretKeyRef:
name: servicebus-connection
key: connectionString
- name: consumerID
value: order-service
3. 通知サービスの実装
// Notification Service
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<DaprClient>(new DaprClientBuilder().Build());
builder.Services.AddSingleton<IEmailService, SendGridEmailService>();
var app = builder.Build();
// Subscribe to order events
app.MapPost("/order-created", async (
[FromBody] CloudEvent<OrderCreatedEvent> cloudEvent,
[FromServices] IEmailService emailService,
[FromServices] ILogger<Program> logger) =>
{
try
{
var orderEvent = cloudEvent.Data;
// 顧客情報の取得
var customer = await GetCustomerInfo(orderEvent.CustomerId);
// メール送信
await emailService.SendOrderConfirmationAsync(
customer.Email,
orderEvent.OrderId,
orderEvent.TotalAmount
);
logger.LogInformation(
"Order confirmation sent for order {OrderId} to {Email}",
orderEvent.OrderId,
customer.Email
);
return Results.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to process order created event");
return Results.Problem();
}
});
// Dapr subscription configuration
app.MapGet("/dapr/subscribe", () => new[]
{
new
{
pubsubname = "pubsub",
topic = "order-created",
route = "/order-created"
}
});
app.Run();
// Email Service Interface
public interface IEmailService
{
Task SendOrderConfirmationAsync(string email, string orderId, decimal amount);
}
// SendGrid Implementation
public class SendGridEmailService : IEmailService
{
private readonly ISendGridClient _client;
private readonly ILogger<SendGridEmailService> _logger;
public SendGridEmailService(IConfiguration configuration, ILogger<SendGridEmailService> logger)
{
_client = new SendGridClient(configuration["SendGrid:ApiKey"]);
_logger = logger;
}
public async Task SendOrderConfirmationAsync(string email, string orderId, decimal amount)
{
var msg = new SendGridMessage
{
From = new EmailAddress("noreply@enhanced.co.jp", "エンハンスド株式会社"),
Subject = $"ご注文確認 - 注文番号: {orderId}",
HtmlContent = GenerateOrderConfirmationHtml(orderId, amount)
};
msg.AddTo(new EmailAddress(email));
var response = await _client.SendEmailAsync(msg);
if (response.StatusCode != HttpStatusCode.Accepted)
{
_logger.LogError("Failed to send email. Status: {Status}", response.StatusCode);
throw new Exception("Email sending failed");
}
}
private string GenerateOrderConfirmationHtml(string orderId, decimal amount)
{
return $@"
<html>
<body>
<h2>ご注文ありがとうございます</h2>
<p>注文番号: <strong>{orderId}</strong></p>
<p>合計金額: <strong>¥{amount:N0}</strong></p>
<p>ご注文の詳細は、マイページからご確認いただけます。</p>
</body>
</html>
";
}
}
コンテナー化とデプロイメント
1. Dockerfile の最適化
# Multi-stage build for .NET 8
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy csproj and restore dependencies
COPY ["OrderService/OrderService.csproj", "OrderService/"]
RUN dotnet restore "OrderService/OrderService.csproj"
# Copy source code and build
COPY . .
WORKDIR "/src/OrderService"
RUN dotnet build "OrderService.csproj" -c Release -o /app/build
# Publish
FROM build AS publish
RUN dotnet publish "OrderService.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
EXPOSE 80
EXPOSE 443
# Add non-root user
RUN adduser --disabled-password --gecos '' appuser
# Copy published files
COPY --from=publish /app/publish .
RUN chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/health || exit 1
ENTRYPOINT ["dotnet", "OrderService.dll"]
2. GitHub Actions によるCI/CD
# .github/workflows/deploy-container-apps.yml
name: Deploy to Azure Container Apps
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
AZURE_CONTAINER_REGISTRY: myacr.azurecr.io
RESOURCE_GROUP: rg-microservices
CONTAINER_APP_ENV: cae-production
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Run tests
run: |
dotnet test ./tests/OrderService.Tests/OrderService.Tests.csproj \
--logger "trx;LogFileName=test-results.trx"
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Login to ACR
uses: azure/docker-login@v1
with:
login-server: ${{ env.AZURE_CONTAINER_REGISTRY }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Build and push images
run: |
# Build services
docker build -t ${{ env.AZURE_CONTAINER_REGISTRY }}/order-service:${{ github.sha }} \
-f ./src/OrderService/Dockerfile ./src
docker build -t ${{ env.AZURE_CONTAINER_REGISTRY }}/inventory-service:${{ github.sha }} \
-f ./src/InventoryService/Dockerfile ./src
docker build -t ${{ env.AZURE_CONTAINER_REGISTRY }}/notification-service:${{ github.sha }} \
-f ./src/NotificationService/Dockerfile ./src
# Push images
docker push ${{ env.AZURE_CONTAINER_REGISTRY }}/order-service:${{ github.sha }}
docker push ${{ env.AZURE_CONTAINER_REGISTRY }}/inventory-service:${{ github.sha }}
docker push ${{ env.AZURE_CONTAINER_REGISTRY }}/notification-service:${{ github.sha }}
- name: Deploy to Container Apps
uses: azure/CLI@v1
with:
inlineScript: |
# Deploy Order Service
az containerapp update \
--name order-service \
--resource-group ${{ env.RESOURCE_GROUP }} \
--image ${{ env.AZURE_CONTAINER_REGISTRY }}/order-service:${{ github.sha }} \
--revision-suffix ${{ github.run_number }}
# Deploy Inventory Service
az containerapp update \
--name inventory-service \
--resource-group ${{ env.RESOURCE_GROUP }} \
--image ${{ env.AZURE_CONTAINER_REGISTRY }}/inventory-service:${{ github.sha }} \
--revision-suffix ${{ github.run_number }}
# Deploy Notification Service
az containerapp update \
--name notification-service \
--resource-group ${{ env.RESOURCE_GROUP }} \
--image ${{ env.AZURE_CONTAINER_REGISTRY }}/notification-service:${{ github.sha }} \
--revision-suffix ${{ github.run_number }}
インフラストラクチャ as Code
1. Bicepテンプレート
// main.bicep - Container Apps Environment and Services
param location string = resourceGroup().location
param environmentName string
param acrName string
param serviceBusNamespace string
param cosmosAccountName string
// Container Apps Environment
resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = {
name: environmentName
location: location
properties: {
daprAIConnectionString: appInsights.properties.ConnectionString
zoneRedundant: true
workloadProfiles: [
{
name: 'Consumption'
workloadProfileType: 'Consumption'
}
]
}
}
// Log Analytics Workspace
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: '${environmentName}-logs'
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}
// Application Insights
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: '${environmentName}-insights'
location: location
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: logAnalytics.id
}
}
// Container Registry
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = {
name: acrName
location: location
sku: {
name: 'Premium'
}
properties: {
adminUserEnabled: true
dataEndpointEnabled: true
networkRuleSet: {
defaultAction: 'Allow'
}
}
}
// Service Bus for Pub/Sub
resource serviceBus 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = {
name: serviceBusNamespace
location: location
sku: {
name: 'Standard'
tier: 'Standard'
}
properties: {
zoneRedundant: true
}
}
// Cosmos DB for State Store
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
name: cosmosAccountName
location: location
kind: 'GlobalDocumentDB'
properties: {
databaseAccountOfferType: 'Standard'
consistencyPolicy: {
defaultConsistencyLevel: 'Session'
}
locations: [
{
locationName: location
failoverPriority: 0
isZoneRedundant: true
}
]
capabilities: [
{
name: 'EnableServerless'
}
]
}
}
// Container Apps
module orderService 'modules/container-app.bicep' = {
name: 'order-service-deployment'
params: {
name: 'order-service'
location: location
containerAppEnvironmentId: containerAppEnvironment.id
image: '${containerRegistry.properties.loginServer}/order-service:latest'
cpu: '0.5'
memory: '1Gi'
minReplicas: 2
maxReplicas: 10
env: [
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: appInsights.properties.ConnectionString
}
]
daprEnabled: true
daprAppId: 'order-service'
daprAppPort: 80
}
}
// Output values
output containerAppEnvironmentId string = containerAppEnvironment.id
output acrLoginServer string = containerRegistry.properties.loginServer
2. Container App モジュール
// modules/container-app.bicep
param name string
param location string
param containerAppEnvironmentId string
param image string
param cpu string = '0.25'
param memory string = '0.5Gi'
param minReplicas int = 1
param maxReplicas int = 3
param env array = []
param daprEnabled bool = false
param daprAppId string = ''
param daprAppPort int = 80
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
name: name
location: location
properties: {
managedEnvironmentId: containerAppEnvironmentId
configuration: {
activeRevisionsMode: 'Multiple'
ingress: {
external: true
targetPort: 80
transport: 'auto'
traffic: [
{
weight: 100
latestRevision: true
}
]
}
dapr: daprEnabled ? {
enabled: true
appId: daprAppId
appPort: daprAppPort
appProtocol: 'http'
} : null
secrets: []
registries: []
}
template: {
containers: [
{
image: image
name: name
resources: {
cpu: json(cpu)
memory: memory
}
env: env
probes: [
{
type: 'Liveness'
httpGet: {
path: '/health'
port: 80
}
initialDelaySeconds: 30
periodSeconds: 30
}
{
type: 'Readiness'
httpGet: {
path: '/health'
port: 80
}
initialDelaySeconds: 5
periodSeconds: 10
}
]
}
]
scale: {
minReplicas: minReplicas
maxReplicas: maxReplicas
rules: [
{
name: 'http-rule'
http: {
metadata: {
concurrentRequests: '100'
}
}
}
]
}
}
}
}
output fqdn string = containerApp.properties.configuration.ingress.fqdn
output latestRevisionName string = containerApp.properties.latestRevisionName
モニタリングとオブザーバビリティ
1. 分散トレーシングの実装
// OpenTelemetry Configuration
using OpenTelemetry.Trace;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
var builder = WebApplication.CreateBuilder(args);
// OpenTelemetry
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(serviceName: "order-service", serviceVersion: "1.0.0"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation()
.AddSource("Dapr.Client")
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri(builder.Configuration["Otlp:Endpoint"]);
}))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter("OrderService.Metrics")
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri(builder.Configuration["Otlp:Endpoint"]);
}));
// Custom metrics
builder.Services.AddSingleton<OrderMetrics>();
var app = builder.Build();
// Middleware for tracing
app.Use(async (context, next) =>
{
using var activity = Activity.Current;
activity?.SetTag("http.request.method", context.Request.Method);
activity?.SetTag("http.request.path", context.Request.Path);
try
{
await next();
activity?.SetTag("http.response.status_code", context.Response.StatusCode);
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
});
// Custom Metrics Class
public class OrderMetrics
{
private readonly Counter<long> _ordersCreated;
private readonly Histogram<double> _orderProcessingTime;
private readonly ObservableGauge<long> _pendingOrders;
public OrderMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("OrderService.Metrics");
_ordersCreated = meter.CreateCounter<long>(
"orders.created",
unit: "{orders}",
description: "Total number of orders created");
_orderProcessingTime = meter.CreateHistogram<double>(
"order.processing.duration",
unit: "ms",
description: "Time taken to process an order");
_pendingOrders = meter.CreateObservableGauge<long>(
"orders.pending",
() => GetPendingOrderCount(),
unit: "{orders}",
description: "Number of pending orders");
}
public void RecordOrderCreated(string customerId, decimal amount)
{
_ordersCreated.Add(1, new KeyValuePair<string, object?>("customer.id", customerId));
}
public void RecordOrderProcessingTime(double milliseconds)
{
_orderProcessingTime.Record(milliseconds);
}
private long GetPendingOrderCount()
{
// 実際の実装では、データストアから取得
return 42;
}
}
2. ログアグリゲーション
// Structured Logging with Serilog
using Serilog;
using Serilog.Enrichers.Span;
var builder = WebApplication.CreateBuilder(args);
// Serilog configuration
builder.Host.UseSerilog((context, services, configuration) => configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.Enrich.WithSpan()
.Enrich.WithProperty("Service", "order-service")
.Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName)
.WriteTo.Console(new JsonFormatter())
.WriteTo.ApplicationInsights(
services.GetRequiredService<TelemetryConfiguration>(),
TelemetryConverter.Traces));
// Correlation ID Middleware
app.Use(async (context, next) =>
{
var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
context.Items["CorrelationId"] = correlationId;
context.Response.Headers.Add("X-Correlation-ID", correlationId);
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await next();
}
});
// Structured logging in action
app.MapPost("/api/orders", async (CreateOrderRequest request) =>
{
using var activity = Activity.StartActivity("CreateOrder");
Log.Information("Creating order for customer {CustomerId} with {ProductCount} products",
request.CustomerId, request.Products.Count);
try
{
// Order processing logic
var orderId = Guid.NewGuid().ToString();
Log.Information("Order created successfully {OrderId} for customer {CustomerId}",
orderId, request.CustomerId);
return Results.Ok(new { orderId });
}
catch (Exception ex)
{
Log.Error(ex, "Failed to create order for customer {CustomerId}", request.CustomerId);
throw;
}
});
セキュリティベストプラクティス
1. 認証・認可の実装
// JWT Authentication with Azure AD
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
// Azure AD authentication
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration, "AzureAd");
// Authorization policies
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("OrderRead", policy =>
policy.RequireRole("Customer", "Admin")
.RequireClaim("permissions", "orders.read"));
options.AddPolicy("OrderWrite", policy =>
policy.RequireRole("Admin")
.RequireClaim("permissions", "orders.write"));
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Protected endpoints
app.MapGet("/api/orders", [Authorize("OrderRead")] async (
[FromServices] IOrderService orderService,
ClaimsPrincipal user) =>
{
var customerId = user.FindFirst("sub")?.Value;
var orders = await orderService.GetOrdersForCustomerAsync(customerId);
return Results.Ok(orders);
});
2. シークレット管理
# Container App with Key Vault references
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
template:
spec:
containers:
- name: order-service
env:
- name: COSMOS_CONNECTION_STRING
valueFrom:
secretKeyRef:
name: app-secrets
key: cosmos-connection
- name: SERVICEBUS_CONNECTION_STRING
valueFrom:
secretKeyRef:
name: app-secrets
key: servicebus-connection
パフォーマンス最適化
1. レスポンスキャッシング
// Redis Cache implementation
using StackExchange.Redis;
public class CachedOrderService : IOrderService
{
private readonly IOrderService _innerService;
private readonly IConnectionMultiplexer _redis;
private readonly ILogger<CachedOrderService> _logger;
public CachedOrderService(
IOrderService innerService,
IConnectionMultiplexer redis,
ILogger<CachedOrderService> logger)
{
_innerService = innerService;
_redis = redis;
_logger = logger;
}
public async Task<Order?> GetOrderAsync(string orderId)
{
var db = _redis.GetDatabase();
var cacheKey = $"order:{orderId}";
// Try cache first
var cached = await db.StringGetAsync(cacheKey);
if (cached.HasValue)
{
_logger.LogDebug("Cache hit for order {OrderId}", orderId);
return JsonSerializer.Deserialize<Order>(cached!);
}
// Fetch from service
var order = await _innerService.GetOrderAsync(orderId);
if (order != null)
{
// Cache for 5 minutes
await db.StringSetAsync(
cacheKey,
JsonSerializer.Serialize(order),
TimeSpan.FromMinutes(5)
);
}
return order;
}
public async Task InvalidateOrderCacheAsync(string orderId)
{
var db = _redis.GetDatabase();
await db.KeyDeleteAsync($"order:{orderId}");
_logger.LogDebug("Cache invalidated for order {OrderId}", orderId);
}
}
// Redis configuration in Program.cs
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")));
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.Decorate<IOrderService, CachedOrderService>();
2. 非同期処理の最適化
// Async batch processing
public class OrderBatchProcessor
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<OrderBatchProcessor> _logger;
private readonly Channel<Order> _orderChannel;
public OrderBatchProcessor(
IServiceProvider serviceProvider,
ILogger<OrderBatchProcessor> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
_orderChannel = Channel.CreateUnbounded<Order>();
}
public async ValueTask EnqueueOrderAsync(Order order)
{
await _orderChannel.Writer.WriteAsync(order);
}
public async Task StartProcessingAsync(CancellationToken cancellationToken)
{
const int batchSize = 100;
const int maxConcurrency = 5;
using var semaphore = new SemaphoreSlim(maxConcurrency);
var tasks = new List<Task>();
await foreach (var batch in _orderChannel.Reader
.ReadAllAsync(cancellationToken)
.Batch(batchSize)
.WithCancellation(cancellationToken))
{
await semaphore.WaitAsync(cancellationToken);
var task = Task.Run(async () =>
{
try
{
await ProcessBatchAsync(batch, cancellationToken);
}
finally
{
semaphore.Release();
}
}, cancellationToken);
tasks.Add(task);
// Clean up completed tasks
tasks.RemoveAll(t => t.IsCompleted);
}
await Task.WhenAll(tasks);
}
private async Task ProcessBatchAsync(
IEnumerable<Order> orders,
CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
try
{
await orderService.ProcessOrderBatchAsync(orders.ToList(), cancellationToken);
_logger.LogInformation("Processed batch of {Count} orders", orders.Count());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order batch");
// Handle failed orders individually or retry
}
}
}
// Extension method for batching
public static class AsyncEnumerableExtensions
{
public static async IAsyncEnumerable<IEnumerable<T>> Batch<T>(
this IAsyncEnumerable<T> source,
int batchSize)
{
var batch = new List<T>(batchSize);
await foreach (var item in source)
{
batch.Add(item);
if (batch.Count >= batchSize)
{
yield return batch.ToArray();
batch.Clear();
}
}
if (batch.Count > 0)
{
yield return batch.ToArray();
}
}
}
まとめ
Azure Container Appsを使用することで、Kubernetesの複雑さを意識することなく、スケーラブルなマイクロサービスアーキテクチャを実現できます。本記事で紹介した実装パターンを参考に、堅牢で保守性の高いシステムを構築してください。
重要なポイント:
- Daprを活用したサービス間通信の簡素化
- 包括的なモニタリングとオブザーバビリティ
- セキュリティファーストなアプローチ
- パフォーマンスを意識した実装
エンハンスド株式会社では、Azure Container Appsを活用したマイクロサービス構築の支援を行っています。お気軽にご相談ください。
#Azure #ContainerApps #Microservices #Dapr #Kubernetes #CloudNative #DevOps #.NET8 #マイクロサービス #コンテナ
執筆者: エンハンスド株式会社 技術チーム
公開日: 2024年12月21日