.NET 8 によるクラウドネイティブ開発:Azure と AWS での実践ガイド
.NET 8 によるクラウドネイティブ開発:Azure と AWS での実践ガイド
はじめに
.NET 8 は、クラウドネイティブアプリケーション開発において画期的な進化を遂げました。本記事では、.NET 8 の新機能を活用しながら、Azure と AWS の両プラットフォームでエンタープライズグレードのアプリケーションを構築する方法を詳しく解説します。
.NET 8 の主要な新機能
Native AOT(Ahead-of-Time Compilation)
Native AOT により、.NET アプリケーションをネイティブコードにコンパイルし、起動時間とメモリ使用量を大幅に削減できます。
// Program.cs - Native AOT 対応の最小 API
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});
var app = builder.Build();
app.MapGet("/", () => new { Message = "Hello from Native AOT!" });
app.MapGet("/health", () => Results.Ok(new { Status = "Healthy" }));
app.Run();
// JSON シリアライゼーション用のコンテキスト
[JsonSerializable(typeof(WeatherForecast))]
[JsonSerializable(typeof(HealthStatus))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}
プロジェクト設定
<!-- .csproj ファイル -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
<StripSymbols>true</StripSymbols>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
</Project>
クラウドネイティブアーキテクチャ
Clean Architecture の実装
// Domain/Entities/Order.cs
namespace ECommerce.Domain.Entities;
public class Order : AggregateRoot
{
public Guid Id { get; private set; }
public string CustomerId { get; private set; }
public DateTime OrderDate { get; private set; }
public OrderStatus Status { get; private set; }
public Money TotalAmount { get; private set; }
private readonly List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
protected Order() { } // EF Core
public static Order Create(string customerId)
{
var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = customerId ?? throw new ArgumentNullException(nameof(customerId)),
OrderDate = DateTime.UtcNow,
Status = OrderStatus.Pending,
TotalAmount = Money.Zero
};
order.AddDomainEvent(new OrderCreatedEvent(order.Id, customerId));
return order;
}
public void AddItem(Product product, int quantity)
{
if (quantity <= 0)
throw new DomainException("Quantity must be greater than zero");
var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id);
if (existingItem != null)
{
existingItem.IncreaseQuantity(quantity);
}
else
{
_items.Add(new OrderItem(product.Id, product.Name, product.Price, quantity));
}
RecalculateTotal();
}
private void RecalculateTotal()
{
TotalAmount = new Money(_items.Sum(i => i.TotalPrice.Amount));
}
}
// Domain/ValueObjects/Money.cs
public record Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency = "JPY")
{
if (amount < 0)
throw new ArgumentException("Amount cannot be negative");
Amount = amount;
Currency = currency;
}
public static Money Zero => new(0);
public static Money operator +(Money left, Money right)
{
if (left.Currency != right.Currency)
throw new InvalidOperationException("Cannot add different currencies");
return new Money(left.Amount + right.Amount, left.Currency);
}
}
リポジトリパターンと Unit of Work
// Application/Interfaces/IOrderRepository.cs
public interface IOrderRepository : IRepository<Order>
{
Task<Order?> GetByIdWithItemsAsync(Guid orderId, CancellationToken cancellationToken = default);
Task<IEnumerable<Order>> GetByCustomerIdAsync(string customerId, CancellationToken cancellationToken = default);
Task<PagedResult<Order>> GetPagedAsync(int page, int pageSize, CancellationToken cancellationToken = default);
}
// Infrastructure/Persistence/Repositories/OrderRepository.cs
public class OrderRepository : Repository<Order>, IOrderRepository
{
private readonly ApplicationDbContext _context;
public OrderRepository(ApplicationDbContext context) : base(context)
{
_context = context;
}
public async Task<Order?> GetByIdWithItemsAsync(Guid orderId, CancellationToken cancellationToken = default)
{
return await _context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
}
public async Task<IEnumerable<Order>> GetByCustomerIdAsync(string customerId, CancellationToken cancellationToken = default)
{
return await _context.Orders
.Where(o => o.CustomerId == customerId)
.OrderByDescending(o => o.OrderDate)
.ToListAsync(cancellationToken);
}
public async Task<PagedResult<Order>> GetPagedAsync(int page, int pageSize, CancellationToken cancellationToken = default)
{
var query = _context.Orders.AsQueryable();
var totalCount = await query.CountAsync(cancellationToken);
var items = await query
.OrderByDescending(o => o.OrderDate)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return new PagedResult<Order>(items, totalCount, page, pageSize);
}
}
// Infrastructure/Persistence/UnitOfWork.cs
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
private readonly IMediator _mediator;
public IOrderRepository Orders { get; }
public IProductRepository Products { get; }
public UnitOfWork(ApplicationDbContext context, IMediator mediator)
{
_context = context;
_mediator = mediator;
Orders = new OrderRepository(_context);
Products = new ProductRepository(_context);
}
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// ドメインイベントの発行
var domainEvents = _context.ChangeTracker.Entries<AggregateRoot>()
.SelectMany(e => e.Entity.DomainEvents)
.ToList();
var result = await _context.SaveChangesAsync(cancellationToken);
// イベントの発行
foreach (var domainEvent in domainEvents)
{
await _mediator.Publish(domainEvent, cancellationToken);
}
return result;
}
}
Azure での実装
Azure Functions with .NET 8
// Functions/OrderProcessingFunction.cs
public class OrderProcessingFunction
{
private readonly IOrderService _orderService;
private readonly ILogger<OrderProcessingFunction> _logger;
public OrderProcessingFunction(IOrderService orderService, ILogger<OrderProcessingFunction> logger)
{
_orderService = orderService;
_logger = logger;
}
[Function("ProcessOrder")]
public async Task<IActionResult> ProcessOrder(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "orders/process")] HttpRequest req,
CancellationToken cancellationToken)
{
try
{
var orderRequest = await req.GetBodyAsync<ProcessOrderRequest>();
if (!orderRequest.IsValid())
{
return new BadRequestObjectResult("Invalid order request");
}
var result = await _orderService.ProcessOrderAsync(orderRequest, cancellationToken);
return new OkObjectResult(new
{
OrderId = result.OrderId,
Status = result.Status,
Message = "Order processed successfully"
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing order");
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
}
}
[Function("OrderStatusWebhook")]
public async Task OrderStatusWebhook(
[ServiceBusTrigger("order-events", "order-status-subscription")] OrderStatusChangedEvent orderEvent,
[CosmosDB(
databaseName: "OrdersDB",
containerName: "OrderStatusHistory",
Connection = "CosmosDBConnection")] IAsyncCollector<OrderStatusHistory> statusHistory,
CancellationToken cancellationToken)
{
_logger.LogInformation($"Processing order status change: {orderEvent.OrderId} -> {orderEvent.NewStatus}");
await statusHistory.AddAsync(new OrderStatusHistory
{
Id = Guid.NewGuid().ToString(),
OrderId = orderEvent.OrderId,
OldStatus = orderEvent.OldStatus,
NewStatus = orderEvent.NewStatus,
ChangedAt = orderEvent.Timestamp,
ChangedBy = orderEvent.UserId
});
}
}
// Startup configuration
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices((context, services) =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
// Azure サービスの設定
services.AddAzureClients(builder =>
{
builder.AddServiceBusClient(context.Configuration.GetConnectionString("ServiceBus"));
builder.AddBlobServiceClient(context.Configuration.GetConnectionString("Storage"));
builder.UseCredential(new DefaultAzureCredential());
});
// DI 設定
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(context.Configuration.GetConnectionString("SqlDatabase")));
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<IOrderService, OrderService>();
// MediatR
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
})
.Build();
host.Run();
Azure Service Bus Integration
// Infrastructure/Messaging/ServiceBusMessagePublisher.cs
public class ServiceBusMessagePublisher : IMessagePublisher
{
private readonly ServiceBusClient _client;
private readonly ILogger<ServiceBusMessagePublisher> _logger;
public ServiceBusMessagePublisher(ServiceBusClient client, ILogger<ServiceBusMessagePublisher> logger)
{
_client = client;
_logger = logger;
}
public async Task PublishAsync<T>(T message, string topicName, CancellationToken cancellationToken = default)
where T : IMessage
{
var sender = _client.CreateSender(topicName);
try
{
var serviceBusMessage = new ServiceBusMessage
{
Body = BinaryData.FromObjectAsJson(message),
ContentType = "application/json",
Subject = message.GetType().Name,
MessageId = Guid.NewGuid().ToString(),
CorrelationId = message.CorrelationId
};
// カスタムプロパティの追加
serviceBusMessage.ApplicationProperties.Add("MessageType", message.GetType().FullName);
serviceBusMessage.ApplicationProperties.Add("Timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds());
await sender.SendMessageAsync(serviceBusMessage, cancellationToken);
_logger.LogInformation("Message published to {TopicName}: {MessageType}", topicName, message.GetType().Name);
}
finally
{
await sender.DisposeAsync();
}
}
public async Task PublishBatchAsync<T>(IEnumerable<T> messages, string topicName, CancellationToken cancellationToken = default)
where T : IMessage
{
var sender = _client.CreateSender(topicName);
try
{
var batch = await sender.CreateMessageBatchAsync(cancellationToken);
foreach (var message in messages)
{
var serviceBusMessage = new ServiceBusMessage
{
Body = BinaryData.FromObjectAsJson(message),
ContentType = "application/json",
Subject = message.GetType().Name
};
if (!batch.TryAddMessage(serviceBusMessage))
{
// バッチが満杯の場合は送信して新しいバッチを作成
await sender.SendMessagesAsync(batch, cancellationToken);
batch = await sender.CreateMessageBatchAsync(cancellationToken);
batch.TryAddMessage(serviceBusMessage);
}
}
if (batch.Count > 0)
{
await sender.SendMessagesAsync(batch, cancellationToken);
}
}
finally
{
await sender.DisposeAsync();
}
}
}
AWS での実装
AWS Lambda with .NET 8
// Lambda/Functions/OrderProcessingLambda.cs
public class OrderProcessingLambda
{
private readonly IOrderService _orderService;
private readonly IAmazonSQS _sqsClient;
private readonly IAmazonDynamoDB _dynamoDbClient;
public OrderProcessingLambda()
{
// Lambda では DI コンテナの設定
var services = new ServiceCollection();
ConfigureServices(services);
var serviceProvider = services.BuildServiceProvider();
_orderService = serviceProvider.GetRequiredService<IOrderService>();
_sqsClient = serviceProvider.GetRequiredService<IAmazonSQS>();
_dynamoDbClient = serviceProvider.GetRequiredService<IAmazonDynamoDB>();
}
[LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]
public async Task<APIGatewayProxyResponse> ProcessOrderHandler(
APIGatewayProxyRequest request,
ILambdaContext context)
{
context.Logger.LogInformation($"Processing order request: {request.RequestContext.RequestId}");
try
{
var orderRequest = JsonSerializer.Deserialize<ProcessOrderRequest>(request.Body);
var result = await _orderService.ProcessOrderAsync(orderRequest);
// DynamoDB に注文履歴を保存
await SaveOrderHistory(result);
// SQS にメッセージを送信
await PublishOrderEvent(result);
return new APIGatewayProxyResponse
{
StatusCode = 200,
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" },
{ "X-Request-Id", context.RequestId }
},
Body = JsonSerializer.Serialize(new
{
success = true,
orderId = result.OrderId,
status = result.Status
})
};
}
catch (ValidationException ex)
{
return new APIGatewayProxyResponse
{
StatusCode = 400,
Body = JsonSerializer.Serialize(new { error = ex.Message })
};
}
catch (Exception ex)
{
context.Logger.LogError($"Error processing order: {ex}");
return new APIGatewayProxyResponse
{
StatusCode = 500,
Body = JsonSerializer.Serialize(new { error = "Internal server error" })
};
}
}
private async Task SaveOrderHistory(OrderResult result)
{
var putRequest = new PutItemRequest
{
TableName = Environment.GetEnvironmentVariable("ORDER_HISTORY_TABLE"),
Item = new Dictionary<string, AttributeValue>
{
["OrderId"] = new AttributeValue { S = result.OrderId },
["CustomerId"] = new AttributeValue { S = result.CustomerId },
["Timestamp"] = new AttributeValue { N = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString() },
["Status"] = new AttributeValue { S = result.Status },
["TotalAmount"] = new AttributeValue { N = result.TotalAmount.ToString() }
}
};
await _dynamoDbClient.PutItemAsync(putRequest);
}
private async Task PublishOrderEvent(OrderResult result)
{
var message = new SendMessageRequest
{
QueueUrl = Environment.GetEnvironmentVariable("ORDER_QUEUE_URL"),
MessageBody = JsonSerializer.Serialize(new OrderProcessedEvent
{
OrderId = result.OrderId,
CustomerId = result.CustomerId,
ProcessedAt = DateTime.UtcNow,
Status = result.Status
}),
MessageAttributes = new Dictionary<string, MessageAttributeValue>
{
["EventType"] = new MessageAttributeValue
{
DataType = "String",
StringValue = "OrderProcessed"
}
}
};
await _sqsClient.SendMessageAsync(message);
}
private void ConfigureServices(IServiceCollection services)
{
// AWS サービスの設定
services.AddDefaultAWSOptions(new AWSOptions
{
Region = RegionEndpoint.GetBySystemName(Environment.GetEnvironmentVariable("AWS_REGION"))
});
services.AddAWSService<IAmazonSQS>();
services.AddAWSService<IAmazonDynamoDB>();
services.AddAWSService<IAmazonS3>();
// アプリケーションサービス
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IOrderRepository, DynamoDbOrderRepository>();
// ロギング
services.AddLogging(builder =>
{
builder.AddLambdaLogger();
builder.SetMinimumLevel(LogLevel.Information);
});
}
}
Step Functions との統合
// StepFunctions/OrderWorkflow.cs
public class OrderWorkflowSteps
{
[LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]
public async Task<ValidateOrderResult> ValidateOrder(OrderRequest request, ILambdaContext context)
{
context.Logger.LogInformation($"Validating order for customer: {request.CustomerId}");
var validationResult = new ValidateOrderResult
{
OrderId = Guid.NewGuid().ToString(),
IsValid = true,
ValidationErrors = new List<string>()
};
// 在庫確認
foreach (var item in request.Items)
{
var inventoryAvailable = await CheckInventory(item.ProductId, item.Quantity);
if (!inventoryAvailable)
{
validationResult.IsValid = false;
validationResult.ValidationErrors.Add($"Insufficient inventory for product {item.ProductId}");
}
}
// 与信確認
var creditCheck = await CheckCustomerCredit(request.CustomerId, request.TotalAmount);
if (!creditCheck.Approved)
{
validationResult.IsValid = false;
validationResult.ValidationErrors.Add("Credit check failed");
}
return validationResult;
}
[LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]
public async Task<ProcessPaymentResult> ProcessPayment(PaymentRequest request, ILambdaContext context)
{
context.Logger.LogInformation($"Processing payment for order: {request.OrderId}");
// Stripe API の呼び出し
var stripe = new StripeClient(Environment.GetEnvironmentVariable("STRIPE_SECRET_KEY"));
try
{
var options = new ChargeCreateOptions
{
Amount = (long)(request.Amount * 100),
Currency = "jpy",
Source = request.PaymentToken,
Description = $"Order {request.OrderId}",
Metadata = new Dictionary<string, string>
{
["order_id"] = request.OrderId,
["customer_id"] = request.CustomerId
}
};
var charge = await stripe.Charges.CreateAsync(options);
return new ProcessPaymentResult
{
Success = true,
TransactionId = charge.Id,
ProcessedAt = DateTime.UtcNow
};
}
catch (StripeException ex)
{
context.Logger.LogError($"Payment failed: {ex.Message}");
return new ProcessPaymentResult
{
Success = false,
ErrorMessage = ex.Message
};
}
}
}
パフォーマンス最適化
Response Compression
// Program.cs
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
{
"application/json",
"application/xml",
"text/json"
});
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Optimal;
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Optimal;
});
メモリ管理とオブジェクトプーリング
// Infrastructure/ObjectPooling/HttpClientPool.cs
public class HttpClientPool
{
private readonly ObjectPool<HttpClient> _pool;
public HttpClientPool()
{
var provider = new DefaultObjectPoolProvider();
var policy = new HttpClientPooledObjectPolicy();
_pool = provider.Create(policy);
}
public async Task<T> ExecuteAsync<T>(Func<HttpClient, Task<T>> operation)
{
var client = _pool.Get();
try
{
return await operation(client);
}
finally
{
_pool.Return(client);
}
}
}
public class HttpClientPooledObjectPolicy : PooledObjectPolicy<HttpClient>
{
public override HttpClient Create()
{
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
MaxConnectionsPerServer = 10
};
return new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
}
public override bool Return(HttpClient obj)
{
// クリーンアップ
obj.DefaultRequestHeaders.Clear();
return true;
}
}
監視とロギング
統合ロギング
// Infrastructure/Logging/StructuredLogging.cs
public static class LoggerExtensions
{
public static void LogOrderProcessed(this ILogger logger, string orderId, string customerId, decimal amount)
{
logger.LogInformation("Order processed successfully. OrderId: {OrderId}, CustomerId: {CustomerId}, Amount: {Amount}",
orderId, customerId, amount);
}
public static void LogPerformanceMetric(this ILogger logger, string operation, double duration)
{
using (logger.BeginScope(new Dictionary<string, object>
{
["Operation"] = operation,
["Duration"] = duration,
["Timestamp"] = DateTime.UtcNow
}))
{
logger.LogInformation("Performance metric recorded");
}
}
}
// Application Insights / CloudWatch 統合
builder.Services.AddApplicationInsightsTelemetry();
builder.Services.AddSingleton<ITelemetryInitializer, CustomTelemetryInitializer>();
public class CustomTelemetryInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
telemetry.Context.GlobalProperties["Environment"] = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
telemetry.Context.GlobalProperties["ServiceName"] = "OrderService";
telemetry.Context.GlobalProperties["Version"] = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
}
}
セキュリティ実装
JWT 認証と認可
// Security/JwtConfiguration.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Auth:Authority"];
options.Audience = builder.Configuration["Auth:Audience"];
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogError("Authentication failed: {Error}", context.Exception.Message);
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Token validated for user: {User}", context.Principal?.Identity?.Name);
return Task.CompletedTask;
}
};
});
// Policy-based authorization
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("OrderManagement", policy =>
policy.RequireAuthenticatedUser()
.RequireClaim("permission", "orders:manage"));
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
});
まとめ
.NET 8 は、クラウドネイティブアプリケーション開発において優れた選択肢です。Native AOT、改善されたパフォーマンス、そして Azure と AWS の両プラットフォームでの優れたサポートにより、エンタープライズグレードのアプリケーションを効率的に構築できます。
重要なポイント:
- Native AOT による起動時間とメモリ使用量の削減
- Clean Architecture による保守性の高い設計
- クラウドサービスとの緊密な統合
- 包括的な監視とロギング
エンハンスド株式会社では、.NET 8 を活用したクラウドネイティブアプリケーション開発を支援しています。お気軽にお問い合わせください。
タグ: #dotNET8 #CloudNative #Azure #AWS #CSharp #Serverless
執筆者: エンハンスド株式会社 .NET開発部
公開日: 2024年12月20日