Configurando Akka.NET com .NET Aspire
Quando usar esta skill
Use esta skill quando:
- Configurar um novo projeto Akka.NET com orquestração .NET Aspire
- Configurar o Akka.Cluster com bootstrapping e descoberta de cluster
- Integrar o Akka.Persistence com SQL Server
- Configurar o Akka.Management para gerenciamento de cluster
- Configurar sistemas de atores com múltiplas réplicas em desenvolvimento local
- Implantar aplicações Akka.NET no Kubernetes com Aspire
Skills relacionadas
akka-net-management- Aprofundamento no Akka.Management, Cluster Bootstrap e provedores de descoberta (Kubernetes, Azure, Config)microsoft-extensions-configuration- Padrões IValidateOptions para validação de configuraçãoakka-net-best-practices- Abstrações de modo cluster/local para sistemas de atores testáveisaspire-integration-testing- Teste de aplicações Aspire com infraestrutura real
Princípios fundamentais
- Configuração via Microsoft.Extensions.Configuration - Use classes de configuração fortemente tipadas vinculadas ao appsettings.json (veja a skill
microsoft-extensions-configuration) - Akka.Hosting para integração com DI - Use a biblioteca Akka.Hosting para integração perfeita com ASP.NET Core
- Aspire para Orquestração - Deixe o Aspire gerenciar dependências de serviço, rede e configuração de ambiente
- Health Checks - Sempre configure health checks para clustering, persistência e prontidão
- Separação de responsabilidades - Mantenha definições de atores, configuração e orquestração Aspire em camadas separadas
- Validar configuração na inicialização - Use
IValidateOptions<T>e.ValidateOnStart()para falhar rapidamente em caso de configuração incorreta
Estrutura do projeto
YourSolution/
├── src/
│ ├── YourApp.Actors/ # Definições de atores e lógica de negócios
│ │ ├── YourActor.cs
│ │ └── YourApp.Actors.csproj
│ ├── YourApp/ # Aplicação web ASP.NET Core
│ │ ├── Config/
│ │ │ ├── AkkaConfiguration.cs # Métodos de extensão para configuração Akka
│ │ │ └── AkkaSettings.cs # Modelo de configuração
│ │ ├── Program.cs
│ │ ├── appsettings.json
│ │ └── YourApp.csproj
│ └── YourApp.AppHost/ # Orquestração Aspire
│ ├── Program.cs
│ ├── AkkaManagementExtensions.cs
│ └── YourApp.AppHost.csproj
Pacotes NuGet necessários
Para o projeto de atores (YourApp.Actors.csproj)
<ItemGroup>
<PackageReference Include="Akka.Cluster.Hosting" />
<PackageReference Include="Akka.Streams" />
</ItemGroup>
Para a aplicação web (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>
Para o 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>
Modelo de configuração (AkkaSettings.cs)
Crie uma classe de configuração fortemente tipada:
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($"Arquivo de certificado não encontrado em: {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
}
Métodos de extensão de configuração 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("A ConnectionString 'DefaultConnection' está ausente");
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]");
});
// Adicione seus atores aqui
// Exemplo: 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>();
// Aplica configuração TLS se habilitado
if (settings.TlsSettings is { Enabled: true })
{
ConfigureRemoteOptionsWithTls(settings);
}
builder.WithRemoting(settings.RemoteOptions);
if (settings.AkkaManagementOptions is { Enabled: true })
{
// Limpa nós seed ao usar 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("A string de conexão do table storage do AkkaManagement [AkkaManagementAzure] está ausente");
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 está habilitado mas nenhum certificado pôde ser carregado");
remoteOptions.EnableSsl = true;
remoteOptions.Ssl = new SslOptions
{
X509Certificate = certificate,
SuppressValidation = !tlsSettings.ValidateCertificates
};
// Atualiza nós seed para usar o protocolo 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();
}
}
}
Integração com Program.cs
using YourApp.Config;
using Petabridge.Cmd.Host;
using Petabridge.Cmd.Cluster;
var builder = WebApplication.CreateBuilder(args);
// Adiciona serviços
builder.Services.AddRazorPages(); // ou o que sua aplicação precisar
// Configura Akka.NET
builder.Services.ConfigureAkka(builder.Configuration,
(configurationBuilder, provider) =>
{
var options = provider.GetRequiredService<AkkaSettings>();
// Adiciona comandos Petabridge.Cmd
configurationBuilder.AddPetabridgeCmd(cmd =>
{
cmd.Host = options.PbmOptions.Host;
cmd.Port = options.PbmOptions.Port;
}, [typeof(ClusterCommands)]);
// Aqui você pode adicionar atores, etc.
});
var app = builder.Build();
// Configura pipeline de requisição
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();