Akka.NET을 .NET Aspire로 구성하기
이 스킬을 사용해야 할 때
다음과 같은 상황에서 이 스킬을 사용하세요:
- .NET Aspire 오케스트레이션을 사용하여 새로운 Akka.NET 프로젝트를 설정할 때
- 클러스터 부트스트래핑 및 디스커버리를 사용하여 Akka.Cluster를 구성할 때
- Akka.Persistence를 SQL Server와 통합할 때
- 클러스터 관리를 위해 Akka.Management를 설정할 때
- 로컬 개발에서 다중 복제본 액터 시스템을 구성할 때
- Aspire를 사용하여 Kubernetes에 Akka.NET 애플리케이션을 배포할 때
관련 스킬
akka-net-management- Akka.Management, Cluster Bootstrap 및 디스커버리 제공자(Kubernetes, Azure, Config)에 대한 심층 분석microsoft-extensions-configuration- 구성 검증을 위한IValidateOptions패턴akka-net-best-practices- 테스트 가능한 액터 시스템을 위한 클러스터/로컬 모드 추상화aspire-integration-testing- 실제 인프라로 Aspire 애플리케이션 테스트
핵심 원칙
- Microsoft.Extensions.Configuration을 통한 구성 - appsettings.json에서 바인딩된 강력한 형식의 설정 클래스 사용(
microsoft-extensions-configuration스킬 참조) - DI 통합을 위한 Akka.Hosting - 원활한 ASP.NET Core 통합을 위해 Akka.Hosting 라이브러리 사용
- 오케스트레이션을 위한 Aspire - Aspire가 서비스 종속성, 네트워킹 및 환경 구성을 관리하도록 함
- 상태 확인(Health Checks) - 클러스터링, 영속성 및 준비 상태에 대한 상태 확인 항상 구성
- 관심사 분리 - 액터 정의, 구성 및 Aspire 오케스트레이션을 별도 계층으로 유지
- 시작 시 구성 검증 -
IValidateOptions<T>및.ValidateOnStart()를 사용하여 잘못된 구성에 대해 빠르게 실패
프로젝트 구조
YourSolution/
├── src/
│ ├── YourApp.Actors/ # 액터 정의 및 비즈니스 로직
│ │ ├── YourActor.cs
│ │ └── YourApp.Actors.csproj
│ ├── YourApp/ # ASP.NET Core 웹 애플리케이션
│ │ ├── Config/
│ │ │ ├── AkkaConfiguration.cs # Akka 설정 확장 메서드
│ │ │ └── AkkaSettings.cs # 구성 모델
│ │ ├── Program.cs
│ │ ├── appsettings.json
│ │ └── YourApp.csproj
│ └── YourApp.AppHost/ # Aspire 오케스트레이션
│ ├── Program.cs
│ ├── AkkaManagementExtensions.cs
│ └── YourApp.AppHost.csproj
필요한 NuGet 패키지
액터 프로젝트용(YourApp.Actors.csproj)
<ItemGroup>
<PackageReference Include="Akka.Cluster.Hosting" />
<PackageReference Include="Akka.Streams" />
</ItemGroup>
웹 애플리케이션용(YourApp.csproj)
<ItemGroup>
<PackageReference Include="Akka.Hosting" />
<PackageReference Include="Akka.Cluster.Hosting" />
<PackageReference Include="Akka.Persistence.Sql.Hosting" />
<PackageReference Include="Akka.Management" />
<PackageReference Include="Akka.Management.Cluster.Bootstrap" />
<PackageReference Include="Akka.Discovery.KubernetesApi" />
<PackageReference Include="Akka.Discovery.Azure" />
<PackageReference Include="Akka.Discovery.Config.Hosting" />
<PackageReference Include="Petabridge.Cmd.Host" />
<PackageReference Include="Petabridge.Cmd.Cluster" />
</ItemGroup>
AppHost용(YourApp.AppHost.csproj)
<Sdk Name="Aspire.AppHost.Sdk" Version="$(AspireVersion)" />
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
<PackageReference Include="Aspire.Hosting.Azure.Storage" />
<PackageReference Include="Aspire.Hosting.SqlServer" />
</ItemGroup>
구성 모델(AkkaSettings.cs)
강력한 형식의 구성 클래스 생성:
using System.Net;
using System.Security.Cryptography.X509Certificates;
using Akka.Cluster.Hosting;
using Akka.Remote.Hosting;
using Petabridge.Cmd.Host;
namespace YourApp.Config;
public class AkkaSettings
{
public string ActorSystemName { get; set; } = "YourSystem";
public bool LogConfigOnStart { get; set; } = false;
public RemoteOptions RemoteOptions { get; set; } = new()
{
PublicHostName = Dns.GetHostName(),
HostName = "0.0.0.0",
Port = 8081
};
public ClusterOptions ClusterOptions { get; set; } = new()
{
SeedNodes = [$”akka.tcp://YourSystem@{Dns.GetHostName()}:8081”],
Roles = ["your-role"]
};
public ShardOptions ShardOptions { get; set; } = new();
public AkkaManagementOptions? AkkaManagementOptions { get; set; }
public PetabridgeCmdOptions PbmOptions { get; set; } = new()
{
Host = "0.0.0.0",
Port = 9110
};
public TlsSettings? TlsSettings { get; set; }
}
public class TlsSettings
{
public bool Enabled { get; set; } = false;
public string? CertificatePath { get; set; }
public string? CertificatePassword { get; set; }
public bool ValidateCertificates { get; set; } = true;
public X509Certificate2? LoadCertificate()
{
if (string.IsNullOrWhiteSpace(CertificatePath))
return null;
if (!File.Exists(CertificatePath))
throw new FileNotFoundException($”Certificate file not found at: {CertificatePath}");
return !string.IsNullOrWhiteSpace(CertificatePassword)
? X509CertificateLoader.LoadPkcs12FromFile(CertificatePath, CertificatePassword)
: X509CertificateLoader.LoadCertificateFromFile(CertificatePath);
}
}
public class AkkaManagementOptions
{
public bool Enabled { get; set; }
public string? Hostname { get; set; }
public int Port { get; set; } = 8558;
public string ServiceName { get; set; } = "your-service";
public string PortName { get; set; } = "management";
public int RequiredContactPointsNr { get; set; } = 1;
public bool FilterOnFallbackPort { get; set; } = true;
public DiscoveryMethod DiscoveryMethod { get; set; } = DiscoveryMethod.Config;
}
public enum DiscoveryMethod
{
Config,
Kubernetes,
AzureTableStorage,
AwsEcsTagBased,
AwsEc2TagBased
}
Akka 구성 확장 메서드(AkkaConfiguration.cs)
using Akka.Cluster.Hosting;
using Akka.Discovery.Azure;
using Akka.Discovery.Config.Hosting;
using Akka.Discovery.KubernetesApi;
using Akka.Hosting;
using Akka.Management;
using Akka.Management.Cluster.Bootstrap;
using Akka.Persistence.Sql.Config;
using Akka.Persistence.Sql.Hosting;
using Akka.Remote.Hosting;
using LinqToDB;
namespace YourApp.Config;
public static class AkkaConfiguration
{
public static IServiceCollection ConfigureAkka(
this IServiceCollection services,
IConfiguration configuration,
Action<AkkaConfigurationBuilder, IServiceProvider> additionalConfig)
{
var akkaSettings = BindAkkaSettings(services, configuration);
var connectionString = configuration.GetConnectionString("DefaultConnection");
if (connectionString is null)
throw new Exception("DefaultConnection ConnectionString is missing");
const string roleName = "your-role";
services.AddAkka(akkaSettings.ActorSystemName, (builder, provider) =>
{
builder.ConfigureNetwork(provider)
.WithAkkaClusterReadinessCheck()
.WithActorSystemLivenessCheck()
.WithSqlPersistence(
connectionString: connectionString,
providerName: ProviderName.SqlServer2022,
databaseMapping: DatabaseMapping.SqlServer,
tagStorageMode: TagMode.TagTable,
deleteCompatibilityMode: true,
useWriterUuidColumn: true,
autoInitialize: true,
journalBuilder: journalBuilder =>
{
journalBuilder.WithHealthCheck(name: "Akka.Persistence.Sql.Journal[default]");
},
snapshotBuilder: snapshotBuilder =>
{
snapshotBuilder.WithHealthCheck(name: "Akka.Persistence.Sql.SnapshotStore[default]");
});
// 여기에 액터 추가
// 예: builder.WithActors((system, registry) => { ... });
additionalConfig(builder, provider);
});
return services;
}
public static AkkaSettings BindAkkaSettings(IServiceCollection services, IConfiguration configuration)
{
var akkaSettings = new AkkaSettings();
configuration.GetSection(nameof(AkkaSettings)).Bind(akkaSettings);
services.AddSingleton(akkaSettings);
return akkaSettings;
}
public static AkkaConfigurationBuilder ConfigureNetwork(
this AkkaConfigurationBuilder builder,
IServiceProvider serviceProvider)
{
var settings = serviceProvider.GetRequiredService<AkkaSettings>();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
// TLS 구성이 활성화된 경우 적용
if (settings.TlsSettings is { Enabled: true })
{
ConfigureRemoteOptionsWithTls(settings);
}
builder.WithRemoting(settings.RemoteOptions);
if (settings.AkkaManagementOptions is { Enabled: true })
{
// Akka.Management 사용 시 시드 노드 제거
var clusterOptions = settings.ClusterOptions;
clusterOptions.SeedNodes = [];
builder
.WithClustering(clusterOptions)
.WithAkkaManagement(setup =>
{
setup.Http.HostName = settings.AkkaManagementOptions.Hostname?.ToLower();
setup.Http.Port = settings.AkkaManagementOptions.Port;
setup.Http.BindHostName = "0.0.0.0";
setup.Http.BindPort = settings.AkkaManagementOptions.Port;
})
.WithClusterBootstrap(options =>
{
options.ContactPointDiscovery.ServiceName = settings.AkkaManagementOptions.ServiceName;
options.ContactPointDiscovery.PortName = settings.AkkaManagementOptions.PortName;
options.ContactPointDiscovery.RequiredContactPointsNr =
settings.AkkaManagementOptions.RequiredContactPointsNr;
options.ContactPointDiscovery.ContactWithAllContactPoints = true;
options.ContactPointDiscovery.StableMargin = TimeSpan.FromSeconds(5);
options.ContactPoint.FilterOnFallbackPort =
settings.AkkaManagementOptions.FilterOnFallbackPort;
}, autoStart: true);
ConfigureDiscovery(builder, settings, configuration);
}
else
{
builder.WithClustering(settings.ClusterOptions);
}
return builder;
}
private static void ConfigureDiscovery(
AkkaConfigurationBuilder builder,
AkkaSettings settings,
IConfiguration configuration)
{
switch (settings.AkkaManagementOptions!.DiscoveryMethod)
{
case DiscoveryMethod.Kubernetes:
builder.WithKubernetesDiscovery();
break;
case DiscoveryMethod.AzureTableStorage:
var connectionString = configuration.GetConnectionString("AkkaManagementAzure");
if (connectionString is null)
throw new Exception("AkkaManagement table storage connection string [AkkaManagementAzure] is missing");
builder
.WithAzureDiscovery(options =>
{
options.ServiceName = settings.AkkaManagementOptions.ServiceName;
options.ConnectionString = connectionString;
options.HostName = settings.RemoteOptions.PublicHostName?.ToLower() ?? "localhost";
options.Port = settings.AkkaManagementOptions.Port;
})
.AddHocon(AzureDiscovery.DefaultConfiguration(), HoconAddMode.Append);
break;
case DiscoveryMethod.Config:
builder.WithConfigDiscovery(options =>
{
options.Services.Add(new Service
{
Name = settings.AkkaManagementOptions.ServiceName,
Endpoints =
[
$”{settings.AkkaManagementOptions.Hostname}:{settings.AkkaManagementOptions.Port}”
]
});
});
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private static void ConfigureRemoteOptionsWithTls(AkkaSettings settings)
{
var tlsSettings = settings.TlsSettings!;
var remoteOptions = settings.RemoteOptions;
var certificate = tlsSettings.LoadCertificate();
if (certificate is null)
throw new InvalidOperationException("TLS is enabled but no certificate could be loaded");
remoteOptions.EnableSsl = true;
remoteOptions.Ssl = new SslOptions
{
X509Certificate = certificate,
SuppressValidation = !tlsSettings.ValidateCertificates
};
// 시드 노드를 akka.ssl.tcp:// 프로토콜로 업데이트
if (settings.ClusterOptions.SeedNodes?.Length > 0)
{
settings.ClusterOptions.SeedNodes = settings.ClusterOptions.SeedNodes
.Select(node => node.Replace("akka.tcp://", "akka.ssl.tcp://"))
.ToArray();
}
}
}
Program.cs 통합
using YourApp.Config;
using Petabridge.Cmd.Host;
using Petabridge.Cmd.Cluster;
var builder = WebApplication.CreateBuilder(args);
// 서비스 추가
builder.Services.AddRazorPages(); // 또는 앱에 필요한 다른 서비스
// Akka.NET 구성
builder.Services.ConfigureAkka(builder.Configuration,
(configurationBuilder, provider) =>
{
var options = provider.GetRequiredService<AkkaSettings