Pontos Principais
-
O CQRS e o Event Sourcing requerem suporte de infraestrutura específico para armazenamento (o Event Store) e transmissão (o Messaging Hub) dos comandos, consultas e eventos;
-
A variedade de padrões de mensagens usada para dar suporte ao CQRS e ao Event Sourcing pode ser viabilizada por uma combinação de ferramentas de middleware existentes, como Kafka e AMQP, mas as soluções completas, como o Axon Server, são uma excelente opção;
-
A melhor implantação e utilização do Axon Server é feita de maneira a tornar a instalação "stateless", isto quando dividimos as definições de configuração em fixas e específicas do ambiente;
-
O Axon Server Enterprise Edition adiciona a capacidade de executar um cluster, mas os nós individuais podem ter identidades bastante específicas com grandes diferenças nos serviços fornecidos, afetando diretamente as estratégias de implantação;
-
É muito fácil incluir controles de acessos (tokens e contas de usuário) e TLS ao Axon Server.
CQRS e Event Sourcing no Java
As aplicações modernas de envio de mensagens e as orientadas a eventos têm requisitos que diferem significativamente das aplicações corporativas tradicionais. Um exemplo simples é a mudança de foco da entrega garantida de mensagens individuais, onde a responsabilidade pela entrega está principalmente no middleware, para o "smart endpoints / dumb pipes", onde a responsabilidade por monitorar a entrega e a pontualidade é totalmente da aplicação. O resultado é que os requisitos de qualidade de serviço na infraestrutura de mensagens tem mais a ver com taxas de transferência do que garantias de entrega. Uma mensagem que não chega ao seu destino é responsabilidade integral tanto do remetente quanto do destinatário, pois ambos são fundamentalmente responsáveis pelos requisitos de negócios afetados por essa falha e são também os melhores entes para definirem uma resposta adequada.
Outra mudança que está acontecendo é uma consequência direta da diminuição constante dos custos de armazenamento e do poder de processamento, bem como do aumento da flexibilidade na atribuição desses recursos aos componentes da aplicação, que é o Command and Query Responsibility Segregation, abreviada para CQRS, e Event Sourcing. Ao invés de ter um único componente no cenário das aplicações gerenciando o "disco de ouro" para atualizações e consultas, essas duas responsabilidades são separadas, e várias fontes de consulta são fornecidas. Os componentes de comando, que geralmente são de baixa frequência da equação, podem otimizar a validação e o armazenamento. As alterações validadas são anunciadas ao restante da empresa usando eventos, múltiplos componentes de consulta os utilizam para criar modelos otimizados. O aumento do uso de caches de encaminhamento e cópias em lote foram sintomas de que esse padrão arquitetural era desesperadamente necessário e os modelos de consulta com Event Stores reproduzíveis formalizam muitas das soluções necessárias aqui. O Event Sourcing avança por este caminho, definindo o estado atual de uma entidade usando a sequência de eventos que a levaram até lá. Isso significa que, ao invés de manter um registro atualizável dos eventos, usamos um armazenamento de eventos que somente adiciona novos registros, permitindo usar a semântica Write-Once, obtendo assim, uma trilha de auditoria incorruptível.
Para dar suporte a essas mudanças, vemos ambos componentes tradicionais como bancos de dados e middleware orientado a mensagens estendido com a funcionalidade necessária, além de novos componentes de infraestrutura criados especificamente com este objetivo. No mundo do desenvolvimento de software Java e Kotlin, o Open Source Axon Framework fornece a principal implementação dos paradigmas CQRS e Event Sourcing, mas possui solução apenas para os módulos individuais da aplicação. Se mantivermos a aplicação unida em uma configuração monolítica, que, reconhecidamente, fornece a maneira mais rápida de iniciar e executar um projeto greenfield, parece um desperdício não poder tirar proveito do suporte para uma arquitetura mais distribuída. Por si só, a arquitetura de uma aplicação baseada no Axon se presta a ser dividida com bastante facilidade, ou "estrangulada", utilizando o termo mais popular. A questão é como iremos dar suporte às implementações de armazenamento de mensagens e eventos.
A arquitetura de uma aplicação baseada no CQRS
As aplicações CQRS típicas têm componentes intercambiando comandos e eventos, com persistência dos agregados manipulados por meio de comandos explícitos, e modelos de consulta otimizados para uso e criados a partir dos eventos que relatam o estado do agregado. A camada de persistência agregada nesta configuração pode ser construída nas camadas de armazenamento RDBMS ou nos componentes de documentos NoSQL, e os repositórios padrão baseados em JPA/JDBC estão incluídos no núcleo do framework. O mesmo vale para o armazenamento dos modelos de consulta.
A comunicação para troca de mensagens pode ser resolvida com a maioria dos componentes básicos de mensagens, mas os padrões de uso favorecem implementações específicas para os diferentes cenários. Podemos usar praticamente qualquer solução moderna de mensagens para o padrão publish-subscribe, contanto que possamos garantir que nenhuma mensagem seja perdida, porque queremos que os modelos de consulta representem fielmente o estado do agregado. Para os comandos, precisamos estender o sistema de mensagens unidirecional básico em um padrão de request-replay, apenas para garantir que possamos detectar a indisponibilidade dos manipuladores de comando. Outras respostas podem ser o estado resultante do agregado ou um relatório detalhado de falha de validação se a atualização foi vetada. No lado da consulta, padrões simples de request-replay não são suficientes para uma arquitetura de microservices distribuídos, pois também queremos examinar os padrões scatter/gather e first-in, bem como os resultados transmitidos com atualizações contínuas.
Para o Event Sourcing, a camada de persistência agregada pode ser substituída por uma camada Write-Once-Read-Many que captura todos os eventos resultantes dos comandos e fornece suporte de repetição para agregados específicos. Essas mesmas repetições podem ser usadas para os modelos de consulta, permitindo que sua reimplementação possa ser feita usando armazenamentos de memória ou fornecendo ressincronização, se suspeitarmos de inconsistência de dados. Uma melhoria importante é o uso de snapshots, para que possamos evitar a necessidade de reproduzir um histórico de alterações possivelmente longo, e o Axon Framework fornece uma implementação padrão de snapshots para os agregados.
Se examinarmos o que coletamos nos componentes de infraestrutura para nossa aplicação, iremos precisar do seguinte:
- Uma implementação de persistência "padrão" para agregados armazenados em estado e modelos de consulta;
- Uma solução de persistência Write-Once-Read-Many para agregados do Event Sourcing;
- Uma solução de mensagens para:
- Request-reply;
- Publish-subscribe com entrega at-least-once;
- Publish-subscribe com repetição;
- Scatter-gather;
- Request-streaming-replay;
O Axon Framework fornece módulos adicionais para integrar uma gama de produtos open source, como soluções baseadas em Kafka e AMQP para distribuição de eventos. No entanto, não é nenhuma surpresa que o Axon Server da AxonIQ também pode ser usado como uma solução all-in-one. Esta série de artigos fala sobre o que é preciso fazer para instalá-lo e executá-lo, começando com uma instalação local simples e avançando para instalações baseadas no Docker (incluindo docker-compose e Kubernetes) e VMs "na nuvem".
Configurando o teste
Para começar, vamos considerar um pequeno programa para demonstrar os componentes que estamos adicionando a arquitetura. Usaremos o Spring Boot para facilitar a configuração, já que a Axon tem um iniciador de Spring Boot que verificará as notações que usamos. Como primeira iteração, manteremos apenas uma aplicação simples que envia um comando que causa um evento. Para que isso funcione, precisamos manipular o comando:
@CommandHandler
public void processCommand(TestCommand cmd) {
log.info("handleCommand(): src = '{}', msg = '{}'.",
cmd.getSrc(), cmd.getMsg());
final String eventMsg = cmd.getSrc() + " says: " + cmd.getMsg();
eventGateway.publish(new TestEvent(eventMsg));
}
O comando e o evento aqui são objetos de valor simples, o primeiro especificando a origem e uma mensagem, o outro apenas uma mensagem. A mesma classe também define o manipulador de eventos que receberá o evento publicado acima:
@EventHandler
public void processEvent(TestEvent evt) {
log.info("handleEvent(): msg = '{}'.", evt.getMsg());
}
Para concluir a aplicação, precisamos adicionar um "inicializador" que envia o comando:
@Bean
public CommandLineRunner getRunner(CommandGateway gwy) {
return (args) -> {
gwy.send(new TestCommand("getRunner", "Hi there!"));
SpringApplication.exit(ctx);
};
}
Para esta primeira versão, também precisamos de um pouco de código de suporte para compensar a falta de um agregado real como CommandHandler, porque o Axon Framework deseja que todo comando tenha algum tipo de identificação que permita a correlação de comandos subsequentes para o mesmo receptor. O código completo está disponível para download no GitHub e, além da parte citada acima, o código contém as classes TestCommand e TestEvent, e configura uma estratégia de roteamento baseada em chaves aleatórias, o que faz a Axon não se incomodar com isso. Isso precisa ser configurado na implementação do CommandBus, e aqui temos que começar a analisar as implementações dos componentes arquiteturais.
Se executarmos as aplicação sem nenhuma implementação específica de barramento tanto de comando quanto de eventos, o tempo de execução do Axon assumirá uma configuração distribuída baseada no Axon Server e tentará se conectar a ele. O Axon Server Standard Edition está disponível gratuitamente sob a licença Open Source da AxonIQ, e um pacote pré-compilado do Axon Framework e Server pode ser obtido no site da AxonIQ. Se colocar o arquivo JAR executável no diretório e executá-lo usando o Java 11, ele começará a usar padrões razoáveis. Observe que as execuções abaixo usam a versão "4.3", portanto a situação pode variar dependendo de quando for feito o download.
$ unzip AxonQuickStart.zip
…
$ mkdir server-se
$ cp axonquickstart-4.3/AxonServer/axonserver-4.3.jar server-se/axonserver.jar
$ cp axonquickstart-4.3/AxonServer/axonserver-cli-4.3.jar server-se/axonserver-cli.jar
$ cd server-se
$ chmod 755 *.jar
$ java -version
openjdk version "11.0.6" 2020-01-14
OpenJDK Runtime Environment (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1, mixed mode, sharing)
$ ./axonserver.jar
_ ____
/ \ __ _____ _ __ / ___| ___ _ ____ _____ _ __
/ _ \ \ \/ / _ \| '_ \\___ \ / _ \ '__\ \ / / _ \ '__|
/ ___ \ > < (_) | | | |___) | __/ | \ V / __/ |
/_/ \_\/_/\_\___/|_| |_|____/ \___|_| \_/ \___|_|
Standard Edition Powered by AxonIQ
version: 4.3
2020-02-20 11:56:33.761 INFO 1687 --- [ main] io.axoniq.axonserver.AxonServer : Starting AxonServer on arrakis with PID 1687 (/mnt/d/dev/AxonIQ/running-axon-server/server/axonserver.jar started by bertl in /mnt/d/dev/AxonIQ/running-axon-server/server)
2020-02-20 11:56:33.770 INFO 1687 --- [ main] io.axoniq.axonserver.AxonServer : No active profile set, falling back to default profiles: default
2020-02-20 11:56:40.618 INFO 1687 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8024 (http)
2020-02-20 11:56:40.912 INFO 1687 --- [ main] A.i.a.a.c.MessagingPlatformConfiguration : Configuration initialized with SSL DISABLED and access control DISABLED.
2020-02-20 11:56:49.212 INFO 1687 --- [ main] io.axoniq.axonserver.AxonServer : Axon Server version 4.3
2020-02-20 11:56:53.306 INFO 1687 --- [ main] io.axoniq.axonserver.grpc.Gateway : Axon Server Gateway started on port: 8124 - no SSL
2020-02-20 11:56:53.946 INFO 1687 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8024 (http) with context path ''
2020-02-20 11:56:53.948 INFO 1687 --- [ main] io.axoniq.axonserver.AxonServer : Started AxonServer in 21.35 seconds (JVM running for 22.317)
Concluindo esta etapa, nossa aplicação teste é iniciada, se conecta ao Axon Server e executa o teste.
...handleCommand(): src = "getRunner", msg = "Hi there!".
...handleEvent(): msg = "getRunner says: Hi there!".
Para obter uma boa medida, execute-o algumas vezes e, se tiver sorte, poderá ver mais de um evento tratado. Caso contrário, adicione um "Thread.sleep(10000)" entre o envio do comando e a chamada para "SpringApplication.exit()" e tente novamente. Este pequeno teste mostra que tivemos a aplicação cliente (vamos chamá-la assim, já que é um cliente do Axon Server e é para onde estamos indo) conectando-se ao Axon Server e a usamos para enviar o comando ao manipulador, que logo após a envia de volta ao cliente para manipulação. O manipulador enviou um evento, que seguiu a mesma rota, embora no EventBus ao invés do CommandBus. Este evento foi armazenado no Event Store do Axon Server e o manipulador de eventos exibirá todos quando ele se conectar inicialmente. Na verdade, se anexarmos a data e hora, simplesmente anexando um "new Date()" à mensagem, veremos que os eventos são de fato bem ordenados no momento que chegam.
Da perspectiva do Axon Framework, existem dois tipos de processadores de eventos: Assinatura e Rastreamento. Um Processador de Eventos de Assinatura irá inscrever a si mesmo em um fluxo de eventos, começando no momento da assinatura. Um processador de eventos de rastreamento, no entanto, rastreia o próprio progresso no fluxo e, por padrão, começa solicitando uma repetição de todos os eventos. Também é possível observar isto como a obtenção de um push nos eventos (para os processadores de inscrição de eventos) em comparação a um pull de eventos (para os processadores de eventos de rastreamento), e a implementação no framework realmente funciona dessa maneira. As duas aplicações mais importantes dessa diferença a serem observadas são para a construção de modelos de consulta e para agregados do Event Sourced. Isso ocorre porque é lá que desejamos ter o histórico completo dos eventos. Não entraremos em detalhes neste momento, mas podemos ler sobre isso na documentação da Axon clicando aqui.
Em nosso programa de teste, podemos adicionar uma configuração para selecionar o tipo de processador de eventos:
@Profile("subscribing")
@Component
public class SubscribingEventProcessorConfiguration {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Autowired
public void configure(EventProcessingConfigurer config) {
log.info("Setting using Subscribing event processors.");
config.usingSubscribingEventProcessors();
}
}
Com isso, se iniciarmos a aplicação com o perfil do Spring "assinando" ativo, veremos apenas um evento processado, que será enviado nesta execução. Se iniciarmos o programa sem esse perfil teremos o modo padrão, que é o rastreamento, e todos os eventos anteriores (incluindo aqueles enviados no modo de assinatura) estarão lá novamente.
Executando e configurando o Axon Server
Agora que temos um cliente pronto para ser usado, vamos dar uma olhada com um pouco mais de detalhe no lado do servidor Axon, pois esperamos lidar com o manuseio de mensagens e o armazenamento de eventos. Na seção anterior, criamos um diretório chamado "axonserver-se" e colocamos os dois arquivos JAR nele. Agora, enquanto estiver em execução, veremos que ele gerou um arquivo "PID", contendo o ID do processo e um diretório "data" com um arquivo de banco de dados e o armazenamento de eventos em um diretório chamado "padrão". No momento, tudo que o histórico de armazenamento possui é um arquivo para armazenamento de eventos e outro para snapshots. Esses arquivos parecerão muito grandes, mas são "esparsos" no sentido de que foram pré-alocados para garantir a disponibilidade de espaço, enquanto ainda contêm apenas alguns eventos. O que esperávamos, mas não vimos, é um arquivo de log com uma cópia da saída do Axon Server e é a primeira coisa que iremos remediar.
O Axon Server é uma aplicação baseada no Spring Boot, que permite adicionar facilmente configurações de registro. O nome padrão do arquivo de propriedades é "axonserver.properties", logo, se criarmos um arquivo com esse nome e o colocarmos no diretório onde o Axon Server será executado, as configurações serão selecionadas. O Spring Boot também analisa um diretório chamado "config" no diretório de trabalho atual, portanto, se quisermos criar uma configuração com script, podemos colocar um arquivo com configurações comuns no diretório de trabalho pretendido, deixando "config/axonserver.properties" para customizações. As propriedades mais simples necessárias para o log são aquelas fornecidas por todas as aplicações do Spring-boot:
logging.file=./axonserver.log
logging.file.max-history=10
logging.file.max-size=10MB
Usando estas linhas, após o banner inicial, o log será enviado para "axonserver.log" e manterá no máximo 10 arquivos com no máximo 10MB de tamanho, o que ajuda a deixar tudo muito bem limpo. A seguir, identificamos outras propriedades que podemos definir como "comuns":
axoniq.axonserver.event.storage=./events
Isso dará ao armazenamento de eventos seu próprio diretório, pois podemos esperar que ele cresça continuamente e não queremos que outras aplicações sejam impactadas por uma possível situação de "disco cheio". Podemos usar discos montados ou links simbólicos para permitir que esse local aponte para o volume em que realmente queremos usar o espaço em disco, mas isso nos dá a possibilidade de pelo menos tornar a configuração comum.
axoniq.axonserver.snapshot.storage=./events
Para reduzir o número de eventos em uma repetição, podemos criar snapshots. Por padrão, serão armazenados no mesmo local que os próprios eventos, mas podemos separá-los, se quisermos. No entanto, como estão intimamente ligados, vamos mantê-los juntos.
axoniq.axonserver.controldb-path=./data
Vamos deixar o ControlDB no local padrão e podemos usar montagens ou links simbólicos para colocá-lo em um volume separado. O ControlDB geralmente não ocupa muito espaço para que possamos fornecer sua própria localização, sem nos preocuparmos muito com o uso do disco.
axoniq.axonserver.pid-file-location=./events
Como vimos, o arquivo PID é gerado por padrão no diretório de trabalho atual do Axon Server. Ao alterá-lo para a mesma localização do ControlDB, temos um único local para arquivos relativamente pequenos, enquanto o atual diretório de trabalho é essencialmente Read-Only.
logging.file=./data/axonserver.log
Esse é altamente dependente do quão rigorosamente desejamos separar os arquivos de log dos demais, pois também podemos optar por atribuir ao Axon Server um diretório em/var/log
e adicionar configurações para rotação de logs ou até mesmo usar algo como "logging.config=logback-spring.xml
" e obter configurações mais detalhadas.
axoniq.axonserver.replication.log-storage-folder=./log
Essa é uma configuração do Axon Server Enterprise Edition para o log de replicação, que armazena o registro de alterações dos dados distribuídos para os outros nós em um cluster. A quantidade de dados aqui é configurável no sentido de que podemos definir o intervalo de limpeza, quando todas as alterações confirmadas serão removidas do log.
Com essas configurações, estruturamos a maneira como o Axon Server irá utilizar o espaço em disco e o configurar para que possamos usar o armazenamento em rede ou na nuvem, de forma que estamos preparados para implantá-lo em um pipeline de CI/CD. No repositório, também adicionaremos scripts de inicialização e desligamento que executam o Axon Server em segundo plano.
Protegendo nossa configuração
Como definitivamente "precisamos de proteção", configuraremos o controle de acesso e o TLS no servidor. O controle de acesso criará um token necessário para solicitações aos pontos de extremidade REST e gRPC, e a interface do usuário exigirá uma conta. Além disso, alguns recursos exigem funções específicas, onde o Enterprise Edition tem um conjunto mais elaborado de funções e permite adicionalmente especificar funções por contexto. Para começar com a Standard Edition, podemos ativar o controle de acesso, definindo um sinalizador no arquivo de propriedades e fornecendo o token:
axoniq.axonserver.accesscontrol.enabled=true
axoniq.axonserver.accesscontrol.token=my-token
Podemos usar ferramentas de linha de comando, como uuidgen, para gerar tokens aleatórios, que serão usados para autenticação. Agora, se iniciarmos o Axon Server com eles, não apenas precisaremos especificar o token para a ferramenta CLI, mas também a interface do usuário exigirá que efetuemos o login, mesmo que ainda não tenhamos criado os usuários. Podemos resolver esse problema facilmente usando a ferramenta CLI:
$ ./axonserver-cli.jar register-user -t my-token -u admin -p test -r ADMIN
$ ./axonserver-cli.jar users -t my-token
Name
admin
$
Com isso, poderemos fazer login novamente. Além disso, se desejamos facilitar a vida para o CLI, podemos criar um diretório chamado "security" e copiar o token para um arquivo chamado ".token". O CLI verificará esse diretório e o arquivo em relação ao diretório de trabalho atual:
$ mkdir security
$ echo my-token > security/.token
$ chmod 400 security/.token && chmod 500 security
$ ./axonserver-cli.jar users
Name
Admin
$
No lado do cliente, também precisamos especificar o token:
$ axonserver-quicktest-4.3-SNAPSHOT-exec.jar
2020-04-23 09:46:10.914 WARN 1438 --- [ main] o.a.a.c.AxonServerConnectionManager : Connecting to AxonServer node [localhost]:[8124] failed: PERMISSION_DENIED: No token for io.axoniq.axonserver.grpc.control.PlatformService/GetPlatformServer
**********************************************
* *
* !!! UNABLE TO CONNECT TO AXON SERVER !!! *
* *
* Are you sure it's running? *
* Don't have Axon Server yet? *
* Go to: https://axoniq.io/go-axon *
* *
**********************************************
To suppress this message, you can
- explicitly configure an AxonServer location,
- start with -Daxon.axonserver.suppressDownloadMessage=true
2020-04-23 09:46:10.943 WARN 1438 --- [.quicktester]-0] o.a.e.TrackingEventProcessor : Fetch Segments for Processor 'io.axoniq.testing.quicktester' failed: No connection to AxonServer available. Preparing for retry in 1s
2020-04-23 09:46:10.999 WARN 1438 --- [ main] o.a.c.gateway.DefaultCommandGateway : Command 'io.axoniq.testing.quicktester.msg.TestCommand' resulted in org.axonframework.axonserver.connector.command.AxonServerCommandDispatchException(No connection to AxonServer available)
$ AXON_AXONSERVER_TOKEN=my-token axonserver-quicktest-4.3-SNAPSHOT-exec.jar
2020-04-23 09:46:48.287 INFO 1524 --- [mandProcessor-0] i.a.testing.quicktester.TestHandler : handleCommand(): src = "QuickTesterApplication.getRunner", msg = "Hi there!".
2020-04-23 09:46:48.352 INFO 1524 --- [.quicktester]-0] i.a.testing.quicktester.TestHandler : handleEvent(): msg = "QuickTesterApplication.getRunner says: Hi there!".
$
Diante disso, a próxima etapa é adicionar o TLS e podemos fazer isso com um certificado autoassinado enquanto estivermos executando localmente. Podemos usar o conjunto de ferramentas "openssl" para gerar um certificado X509 no formato PEM para proteger a conexão gRPC e, em seguida, empacotar a chave e o certificado em um keystore no formato PKCS12 para a porta HTTP. Os próximos passos serão:
- Gerar uma solicitação de assinatura de certificado usando um arquivo de configuração de estilo INI, que permite o funcionamento sem a interação do usuário. Isso também gerará uma chave privada não protegida com 2048 bits de comprimento, usando o algoritmo RSA.
- Usar esta solicitação para gerar e assinar o certificado, que será válido por 365 dias.
- Ler a chave e o certificado e armazená-los em um keystore PKCS12, sob o alias "axonserver". Como não podemos proteger este local, fornecemos a senha "axonserver".
$ cat > csr.cfg <<EOF
[ req ]
distinguished_name="req_distinguished_name"
prompt="no"
[ req_distinguished_name ]
C="NL"
ST="Province"
L="City"
O="My Company"
CN="laptop.my-company.nl"
EOF
$ openssl req -config csr.cfg -new -newkey rsa:2048 -nodes -keyout tls.key -out tls.csr
Generating a 2048 bit RSA private key
.......................................+++
....................................................................................+++
writing new private key to 'tls.key'
-----
$ openssl x509 -req -days 365 -in tls.csr -signkey tls.key -out tls.crt
Signature ok
subject=/C=NL/ST=Province/L=City/O=My Company/CN=laptop.mycompany.nl
Getting Private key
$ openssl pkcs12 -export -out tls.p12 -inkey tls.key -in tls.crt -name axonserver -passout pass:axonserver
$
Agora temos:
- tls.csr: A solicitação de assinatura do certificado, da qual não precisamos mais;
- tls.key: A chave privada no formato PEM;
- tls.crt: O certificado no formato PEM;
- tls.p12: O keystore no formato PKCS12.
Para configurá-los no Axon Server, usamos:
# SSL for the HTTP port
server.ssl.key-store-type=PKCS12
server.ssl.key-store=tls.p12
server.ssl.key-store-password=axonserver
server.ssl.key-alias=axonserver
security.require-ssl=true
# SSL enabled for gRPC
axoniq.axonserver.ssl.enabled=true
axoniq.axonserver.ssl.cert-chain-file=tls.crt
axoniq.axonserver.ssl.private-key-file=tls.key
A diferença entre as duas abordagens decorre do suporte de tempo de execução usado: A porta HTTP é fornecida pelo Spring-boot usando as propriedades prefixadas nativas do "server" e requer um keystore PKCS12. A porta gRPC, no entanto, é configurada usando as bibliotecas do Google, que desejam certificados codificados por PEM. Com isso adicionado ao "axonserver.properties", podemos reiniciar o Axon Server que agora deve anunciar "Configuration initialized with SSL ENABLED and access control ENABLED". No lado do cliente, precisamos dizer a ele para usar SSL e, como estamos usando um certificado autoassinado, temos que passá-lo também:
axon.axonserver.ssl-enabled=true
axon.axonserver.cert-file=tls.crt
Observe que adicionamos "axonserver.megacorp.com" como nome do host ao arquivo "hosts" do sistema, para que outras aplicações possam encontrá-lo e o nome corresponder ao nome do certificado. Com isso, nosso testador rápido pode se conectar usando TLS (removendo os carimbos de data e hora e outros mais que houverem):
...Connecting using TLS...
...Requesting connection details from axonserver.megacorp.com:8124
...Reusing existing channel
...Re-subscribing commands and queries
...Creating new command stream subscriber
...Worker assigned to segment Segment[0/0] for processing
...Using current Thread for last segment worker: TrackingSegmentWorker{processor=io.axoniq.testing.quicktester, segment=Segment[0/0]}
...Fetched token: null for segment: Segment[0/0]
...open stream: 0
...Shutdown state set for Processor 'io.axoniq.testing.quicktester'.
...Processor 'io.axoniq.testing.quicktester' awaiting termination...
...handleCommand(): src = "QuickTesterApplication.getRunner", msg = "Hi there!".
...handleEvent(): msg = "QuickTesterApplication.getRunner says: Hi there!".
...Released claim
...Worker for segment Segment[0/0] stopped.
...Closed instruction stream to [axonserver]
...Received completed from server.
Então, e o Axon Server EE?
Do ponto de vista das operações, a execução do Axon Server Enterprise Edition não é tão diferente da Standard Edition. As diferenças mais importantes são:
- Podemos ter várias instâncias trabalhando como um cluster;
- O cluster suporta mais de um contexto (no SE, temos apenas o "padrão");
- O controle de acesso tem um conjunto mais detalhado das funções;
- As aplicações obtém seus próprios tokens e autorizações.
No lado da conectividade, obtemos uma porta gRPC extra usada para comunicação entre os nós no cluster, cujo padrão é a porta 8224.
Um cluster de nós do Axon Server fornecerá vários pontos de conexão para aplicações clientes (baseadas no Axon Framework) e, portanto, compartilhará a carga de gerenciamento da entrega de mensagens e armazenamento de eventos. Todos os nós que atendem a um contexto específico mantêm uma cópia completa, com um "líder de contexto" no controle da transação distribuída. O líder é determinado por eleições, seguindo o protocolo da RAFT. Neste artigo não vamos nos aprofundar nos detalhes da RAFT e em como ela funciona, mas uma consequência importante tem a ver com essas eleições: Os nós precisam ser capazes de vencê-las ou, pelo menos, sentir o apoio de uma forte maioria. Portanto, embora um cluster do Axon Server não precise ter um número ímpar de nós, todos os contextos individuais precisam, para impedir a possibilidade de empate em uma eleição. Isso também vale para o contexto interno denominado "_admin", usado pelos nós administrativos que armazenam os dados da estrutura do cluster. Como consequência, a maioria dos clusters terá um número ímpar de nós e continuará funcionando enquanto a maioria (para um contexto específico) estiver respondendo e armazenando eventos.
Axon Server Clustering
Um nó em um cluster do Axon Server pode ter funções diferentes em um contexto:
- Um nó "PRIMARY" é um membro totalmente funcional (e que pode votar) desse contexto. Para que um contexto esteja disponível para aplicações clientes, é necessário que a maioria dos nós sejam primary;
- Um membro "MESSAGING_ONLY" não fornecerá armazenamento de eventos e (como não está envolvido nas transações) é um membro sem direito a voto do contexto;
- Um nó "ACTIVE_BACKUP" é um membro com poder de voto que fornece um armazenamento de eventos, mas não fornece os serviços de mensagens, para que os clientes não se conectem a ele. Observe que devemos ter pelo menos um nó de active_backup, se desejarmos garantir que os backups estejam atualizados;
- Por fim, um "PASSIVE_BACKUP" fornecerá um armazenamento de eventos, mas não participará de transações ou mesmo das eleições, nem fornecerá serviços de mensagens. A ativação ou desativação nunca influenciará a disponibilidade do contexto e o líder enviará todos os eventos acumulados durante a manutenção, assim que voltar a ficar online.
Da perspectiva de uma estratégia de backup, o backup ativo pode ser usado para manter uma cópia externa sempre atualizada. Se tivermos dois nós de backup ativos, poderemos parar o Axon Server em um deles para fazer um backup dos arquivos de armazenamento de eventos, enquanto o outro continuará recebendo as atualizações. O nó de backup passivo fornece uma estratégia alternativa, na qual o líder do contexto envia atualizações de forma assíncrona. Embora isso não ofereça a garantia de que tudo esteja sempre atualizado, os eventos aparecerão em algum momento e, mesmo com uma única instância de backup, podemos desativar o Axon Server e fazer os backups de arquivos sem afetar a disponibilidade do cluster. Quando voltar a ficar online, o líder começará imediatamente a enviar os novos dados.
Uma consequência do suporte a vários contextos e funções diferentes, cada um configurável por nó, é que esses nós individuais podem ter grandes diferenças nos serviços que fornecem as aplicações clientes. Nesse caso, aumentar o número de nós não tem o mesmo efeito em todos os contextos: Embora a carga de mensagens seja compartilhada por todos os nós que suportam um contexto, o Event Store precisa distribuir os dados para um nó adicional e a maioria precisa confirmar o armazenamento antes que o cliente possa continuar. Outra coisa a ser lembrada é que os papéis "ACTIVE_BACKUP" e "PASSIVE_BACKUP" têm significados bastante específicos, embora os nomes possam sugerir interpretações diferentes do mundo da Alta Disponibilidade. Em geral, a função de um nó do Axon Server não muda apenas para resolver um problema de disponibilidade. O cluster pode continuar funcionando enquanto a maioria dos nós estiver disponível para um contexto, mas se essa maioria for perdida para o contexto "_admin", as alterações na configuração do cluster também não poderão ser confirmadas.
Para a execução em um cluster local, precisamos fazer algumas adições ao nosso conjunto "comum" de propriedades, das quais as mais importantes dizem respeito à inicialização do cluster: Quando um nó é iniciado, ele ainda não sabe se irá se tornará o núcleo de um novo cluster ou se será adicionado a um cluster existente com uma função específica. Portanto, se iniciarmos o Axon Server EE e começarmos a conectar imediatamente as aplicações clientes, receberemos uma mensagem de erro indicando que não há um cluster inicializado disponível. Se desejarmos apenas um cluster com todos os nós registrados como "PRIMARY", podemos adicionar as propriedades autocluster:
axoniq.axonserver.autocluster.first=axonserver-1.megacorp.com
axoniq.axonserver.autocluster.contexts=_admin,default
Com isso adicionado, o nó cujo nome do host e porta interna do cluster corresponde à configuração "first", sem nenhuma porta especificada, ou seja, usando a porta padrão 8224, inicializará os contextos "default" e "_admin", se necessário. Os outros nós usarão o nome do host e a porta especificados para se registrar no cluster e solicitar a inclusão nos contextos fornecidos. Uma solução típica para iniciar um cluster de vários nós em um único host é usar as propriedades da porta para que elas se exponham uma à outra. O segundo nó usaria:
server.port=8025
axoniq.axonserver.port=8125
axoniq.axonserver.internal-port=8225
axoniq.axonserver.name=axonserver-2
axoniq.axonserver.hostname=localhost
O terceiro pode usar 8026, 8126 e 8226. Na próxima parcela, veremos as implantações do Docker e também personalizaremos o nome do host usado para a comunicação interna do cluster.
Controle de acesso para a interface do usuário e aplicações clientes
Talvez seja necessária uma pequena explicação sobre a ativação e o controle de acesso, especialmente da perspectiva do cliente. Como mencionado acima, o efeito é que as aplicações clientes devem fornecer um token ao se conectar ao Axon Server. Esse token é usado para conexões HTTP e gRPC, e o Axon Server usa um cabeçalho HTTP personalizado chamado "AxonIQ-Access-Token" para isso. Para a Standard Edition, existe um único token para os dois tipos de conexão, enquanto o Enterprise Edition mantém uma lista de aplicações e gera UUIDs como token para cada uma. A porta interna do cluster usa outro token, que precisa ser configurado no arquivo de propriedades usando " axoniq.axonserver.internal-token
".
Um tipo separado de autenticação possível é o uso do nome de usuário e senha, que funcionam apenas para a porta HTTP. Isso geralmente é usado para a interface do usuário, que mostra uma tela de login, se ativada, mas também pode ser usada para chamadas REST usando autenticação BASIC:
$ curl -u admin:test http://localhost:8024/v1/public/users
[{"userName":"admin","password":null,"roles":["ADMIN@*"]}]
$
A CLI também é um tipo de aplicação cliente, mas apenas através da API REST. Como vimos anteriormente, podemos usar o token para conectarmos quando o controle de acesso estiver ativado, mas se tentarmos isso com o Axon Server EE, perceberemos que este caminho está fechado. O motivo é a substituição do token único de todo o sistema pelos tokens específicos da aplicação. Na verdade, ainda existe um token para a CLI, mas agora é local por nó e gerado pelo Axon Server, e é armazenado em um arquivo chamado "security/.token", relativo ao diretório de trabalho do nó. Também encontramos esse arquivo quando analisamos o fornecimento do token para a CLI. Voltaremos a isso na parte dois, quando analisarmos o Docker e o Kubernetes e apresentaremos um segredo para isso.
Fim da primeira parte
Isso encerra a primeira parte desta série sobre a execução do Axon Server. Na parte dois, seguiremos para o Docker, para o docker-composite e o Kubernetes, e nos divertiremos com as diferenças que eles nos trazem em relação ao gerenciamento de volumes. Vejo você no próximo artigo!
Sobre o autor
Bert Laverman é arquiteto e desenvolvedor sênior de software na AxonIQ, com mais de 25 anos de experiência, focado no Java nos últimos anos. Ele foi co-fundador da Internet Access Foundation, uma organização sem fins lucrativos que foi fundamental para desbloquear a internet para o norte e o leste da Holanda. Começou como desenvolvedor, nos anos 90 mudou-se para a Arquitetura de Software, com uma breve passagem como consultor estratégico e arquiteto corporativo em uma seguradora. Agora na AxonIQ, a startup por trás do Axon Framework e do Axon Server, trabalha no desenvolvimento de produtos, com foco na arquitetura de software e DevOps, além de ajudar clientes.