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日