xenvman: ambientes flexíveis de teste de microsserviços (e mais)

Olá pessoal!


Eu gostaria de falar um pouco sobre o projeto em que trabalho nos últimos seis meses. Eu faço o projeto no meu tempo livre, mas a motivação para a sua criação veio das observações feitas no trabalho principal.


Em um projeto de trabalho, usamos a arquitetura de microsserviços, e um dos principais problemas que se manifestaram ao longo do tempo e o aumento do número desses serviços está sendo testado. Quando um determinado serviço depende de cinco a sete outros serviços, além de algum outro banco de dados (ou mesmo vários) para inicializar, testá-lo de forma "ativa", por assim dizer, é muito inconveniente. Você precisa colocar mokas de todos os lados com tanta força que nem consegue fazer o teste. Bem, ou de alguma forma, organize um ambiente de teste em que todas as dependências possam realmente ser lançadas.


Na verdade, para facilitar a segunda opção, apenas me sentei para escrever xenvman . Em poucas palavras, isso é algo como um híbrido de docker para composição e teste de contêineres , apenas sem ligação ao Java (ou qualquer outra linguagem) e com a capacidade de criar e configurar dinamicamente ambientes por meio da API HTTP.


xenvman escrito em Go e implementado como um servidor HTTP simples, que permite usar toda a funcionalidade disponível em qualquer idioma que possa falar esse protocolo.


A principal coisa que o xenvman pode fazer é:


  • Descreva de forma flexível o conteúdo do ambiente com scripts JavaScript simples
  • Crie imagens em tempo real
  • Crie o número certo de contêineres e combine-os em uma única rede isolada
  • Encaminhar portas internas do ambiente para fora, para que os testes possam alcançar os serviços necessários, mesmo de outros hosts
  • Altere dinamicamente a composição do ambiente (pare, inicie e adicione novos contêineres) em movimento, sem interromper o ambiente de trabalho.

Meio ambiente


O personagem principal do xenvman é o meio ambiente. Esse é um balão isolado no qual todas as dependências necessárias (empacotadas em contêineres do Docker) do seu serviço são iniciadas.



A figura acima mostra o servidor xenvman e os ambientes ativos nos quais diferentes serviços e bancos de dados estão em execução. Cada ambiente foi criado diretamente a partir do código de teste de integração e será excluído após a conclusão.


Padrões


O que diretamente faz parte do ambiente é determinado por modelos, que são pequenos scripts em JS. O xenvman possui um intérprete interno dessa linguagem e, quando recebe uma solicitação para criar um novo ambiente, simplesmente executa os modelos especificados, cada um dos quais adiciona um ou mais contêineres à lista para execução.


O JavaScript foi escolhido para permitir alterar / adicionar modelos dinamicamente sem a necessidade de reconstruir o servidor. Além disso, os modelos geralmente usam apenas os recursos básicos e os tipos de dados da linguagem (o bom e velho ES5, sem DOM, React e outras mágicas); portanto, trabalhar com modelos não deve causar dificuldades especiais, mesmo para quem conhece completamente o JS.


Os modelos são parametrizáveis, ou seja, podemos controlar completamente a lógica do modelo passando certos parâmetros em nossa solicitação HTTP.


Crie imagens em tempo real


Um dos recursos mais convenientes do xenvman, na minha opinião, é a criação de imagens do Docker no processo de configuração do ambiente. Por que isso pode ser necessário?
Bem, por exemplo, em nosso projeto, para obter uma imagem de um serviço, você precisa confirmar as alterações em uma ramificação separada, iniciar e aguardar até que o Gitlab CI colete e preencha a imagem.
Se apenas um serviço foi alterado, pode levar de 3 a 5 minutos.


E se estivermos vendo ativamente novos recursos em nosso serviço, ou tentando entender por que ele não funciona, adicionando o bom e velho fmt.Printf para frente e para trás, ou alterando o código frequentemente de alguma forma, mesmo um atraso de 5 minutos será ótimo para extinguir o desempenho ( nossos como redatores de código). Em vez disso, podemos simplesmente adicionar toda a depuração necessária ao código, compilá-lo localmente e simplesmente anexar o binário finalizado à solicitação HTTP.


Depois de receber essa aprovação, o modelo pegará esse binário e criará uma imagem temporária em movimento, a partir da qual já podemos iniciar o contêiner como se nada tivesse acontecido.


Em nosso projeto, no modelo principal de serviços, por exemplo, verificamos se o binário está presente nos parâmetros e, se houver, coletamos a imagem em movimento, caso contrário, apenas baixamos a versão latest recente do ramo dev . O código adicional para a criação de contêineres é idêntico para as duas opções.


Um pequeno exemplo


Para maior clareza, vejamos o micro-exemplo.


Digamos que escrevamos algum tipo de servidor milagroso (vamos chamá-lo de wut ), que precisa de um banco de dados para armazenar tudo lá. Bem, como base, escolhemos o MongoDB. Portanto, para testes completos, precisamos de um servidor Mongo que funcione. Obviamente, você pode instalá-lo e executá-lo localmente, mas, por simplicidade e visibilidade do exemplo, assumimos que, por algum motivo, isso é difícil de fazer (com outras configurações mais complexas em sistemas reais, será mais parecido com a verdade).


Então, tentaremos usar o xenvman para criar um ambiente com o Mongo em execução e nosso servidor wut .


Primeiro de tudo, precisamos criar um diretório base no qual todos os modelos serão armazenados:


$ mkdir xenv-templates && cd xenv-templates


Em seguida, crie dois modelos, um para Mongo e outro para nosso servidor:


$ touch mongo.tpl.js wut.tpl.js


mongo.tpl.js


Abra o mongo.tpl.js e escreva o seguinte:


 function execute(tpl, params) { var img = tpl.FetchImage(fmt("mongo:%s", params.tag)); var cont = img.NewContainer("mongo"); cont.SetLabel("mongo", "true"); cont.SetPorts(27017); cont.AddReadinessCheck("net", { "protocol": "tcp", "address": '{{.ExternalAddress}}:{{.Self.ExposedPort 27017}}' }); } 

O modelo deve ter uma função execute () com dois parâmetros.
A primeira é uma instância do objeto tpl através do qual o ambiente está configurado. O segundo argumento (params) é apenas um objeto JSON com o qual iremos parametrizar nosso modelo.


Em linha


 var img = tpl.FetchImage(fmt("mongo:%s", params.tag)); 

pedimos ao xenvman para baixar a imagem do docker mongo:<tag> , onde <tag> é a versão da imagem que queremos usar. Em princípio, essa linha é equivalente ao docker pull mongo:<tag> , com a única diferença de que todas as funções do objeto tpl são essencialmente declarativas, ou seja, a imagem será efetivamente baixada somente após o xenvman executar todos os modelos especificados na configuração do ambiente.


Depois de termos a imagem, podemos criar um contêiner:


 var cont = img.NewContainer("mongo"); 

Novamente, o contêiner não será criado instantaneamente neste local; simplesmente declaramos a intenção de criá-lo, por assim dizer.


Em seguida, colocamos um rótulo em nosso contêiner:


 cont.SetLabel("mongo", "true"); 

Os atalhos são usados ​​para que os contêineres possam se encontrar em um ambiente, por exemplo, para inserir o endereço IP ou o nome do host no arquivo de configuração.


Agora precisamos desligar a porta Mongo interna (27017). Isso é facilmente feito assim:


  cont.SetPorts(27017); 

Antes que o xenvman relate a criação bem-sucedida do ambiente, seria ótimo garantir que todos os serviços não estejam apenas em execução, mas estejam prontos para aceitar solicitações. O Xenvman tem verificações de prontidão para isso.
Adicione um desses para o nosso contêiner mongo:


  cont.AddReadinessCheck("net", { "protocol": "tcp", "address": '{{.ExternalAddress}}:{{.Self.ExposedPort 27017}}' }); 

Como podemos ver, aqui na barra de endereços existem stubs nos quais os valores necessários serão substituídos dinamicamente imediatamente antes do lançamento dos contêineres.


Em vez de {{.ExternalAddress}} endereço externo do host executando o xenvman será substituído e, em vez de {{.Self.ExposedPort 27017}} porta externa que foi encaminhada para o 27017 interno será substituída.


Leia mais sobre interpolação aqui .


Como resultado de tudo isso, podemos nos conectar ao Mongo em execução no ambiente, mesmo fora, por exemplo, do host no qual executamos nosso teste.


wut.tpl.js


Então, c, tendo lidado com o monga, escreveremos outro modelo para o nosso servidor wut .
Como queremos coletar a imagem em movimento, o modelo será um pouco diferente:


 function execute(tpl, params) { var img = tpl.BuildImage("wut-image"); img.CopyDataToWorkspace("Dockerfile"); // Extract server binary var bin = type.FromBase64("binary", params.binary); img.AddFileToWorkspace("wut", bin, 0755); // Create container var cont = img.NewContainer("wut"); cont.MountData("config.toml", "/config.toml", {"interpolate": true}); cont.SetPorts(params.port); cont.AddReadinessCheck("http", { "url": fmt('http://{{.ExternalAddress}}:{{.Self.ExposedPort %v}}/', params.port), "codes": [200] }); } 

Como estamos BuildImage() imagem aqui, usamos BuildImage() vez de FetchImage() :


  var img = tpl.BuildImage("wut-image"); 

Para montar a imagem, precisaremos de vários arquivos:
Dockerfile - na verdade, instruções sobre como montar uma imagem
config.toml - arquivo de configuração para o nosso servidor wut


Usando o img.CopyDataToWorkspace("Dockerfile"); copiamos o Dockerfile do diretório de dados do modelo para um diretório de trabalho temporário .


O diretório de dados é um diretório no qual podemos armazenar todos os arquivos necessários para o funcionamento do nosso modelo.


No diretório de trabalho temporário, copiamos os arquivos (usando img.CopyDataToWorkspace ()) que entram na imagem.


O seguinte é o seguinte:


  // Extract server binary var bin = type.FromBase64("binary", params.binary); img.AddFileToWorkspace("wut", bin, 0755); 

Passamos o binário do nosso servidor diretamente nos parâmetros, na forma codificada (base64). E no modelo, simplesmente decodificamos e salvamos a string resultante no diretório de trabalho como um arquivo sob o nome wut .


Em seguida, crie um contêiner e monte o arquivo de configuração nele:


  var cont = img.NewContainer("wut"); cont.MountData("config.toml", "/config.toml", {"interpolate": true}); 

Uma chamada para MountData() significa que o arquivo config.toml do diretório de dados será montado dentro do contêiner sob o nome /config.toml . O sinalizador de interpolate informa ao xenvman ao servidor que todos os stubs devem ser substituídos antes da montagem no arquivo.


Aqui está a aparência da configuração:


 {{with .ContainerWithLabel "mongo" "" -}} mongo = "{{.Hostname}}/wut" {{- end}} 

Aqui, procuramos o contêiner com o rótulo mongo e substituímos o nome do host, independentemente do que estiver neste ambiente.


Após a substituição, o arquivo pode se parecer com:


 mongo = “mongo.0.mongo.xenv/wut” 

Em seguida, publicamos novamente a porta e iniciamos uma verificação de prontidão, desta vez HTTP:


 cont.SetPorts(params.port); cont.AddReadinessCheck("http", { "url": fmt('http://{{.ExternalAddress}}:{{.Self.ExposedPort %v}}/', params.port), "codes": [200] }); 

Nossos modelos estão prontos para isso e podemos usá-los no código de teste de integração:


 import "github.com/syhpoon/xenvman/pkg/client" import "github.com/syhpoon/xenvman/pkg/def" //  xenvman  cl := client.New(client.Params{}) //      env := cl.MustCreateEnv(&def.InputEnv{ Name: "wut-test", Description: "Testing Wut", // ,      Templates: []*def.Tpl{ { Tpl: "wut", Parameters: def.TplParams{ "binary": client.FileToBase64("wut"), "port": 5555, }, }, { Tpl: "mongo", Parameters: def.TplParams{"tag": “latest”}, }, }, }) //      defer env.Terminate() //     wut  wutCont, err := env.GetContainer("wut", 0, "wut") require.Nil(t, err) //      mongoCont, err := env.GetContainer("mongo", 0, "mongo") require.Nil(t, err) //    wutUrl := fmt.Sprintf("http://%s:%d/v1/wut/", env.ExternalAddress, wutCont.Ports[“5555”]) mongoUrl := fmt.Sprintf("%s:%d/wut", env.ExternalAddress, mongoCont.Ports["27017"]) // !      ,            ,   

Pode parecer que os modelos de escrita levarão muito tempo.
No entanto, com o design correto, essa é uma tarefa única e, em seguida, os mesmos modelos podem ser reutilizados cada vez mais (e até para idiomas diferentes!) Simplesmente ajustando-os, passando determinados parâmetros. Como você pode ver no exemplo acima, o código de teste em si é muito simples, devido ao fato de colocarmos todas as cascas na configuração do ambiente em modelos.


Neste pequeno exemplo, longe de todos os recursos do xenvman, um guia passo a passo mais detalhado está disponível aqui.


Clientes


Atualmente, existem clientes para dois idiomas:


Go
Python


Mas adicionar novos não é difícil, pois a API fornecida é muito, muito simples.


Interface da web


Na versão 2.0.0, uma interface da web simples foi adicionada com a qual você pode gerenciar seus ambientes e visualizar os modelos disponíveis.





Qual a diferença entre o xenvman e o docker-compose?


Claro, existem muitas semelhanças, mas o xenvman me parece uma abordagem um pouco mais flexível e dinâmica, em comparação com a configuração estática no arquivo.
Aqui estão as principais características distintivas, na minha opinião:


  • Absolutamente todo o controle é realizado através da API HTTP, portanto, podemos criar ambientes a partir do código de qualquer linguagem que entenda HTTP
  • Como o xenvman pode ser executado em um host diferente, podemos usar todos os seus recursos, mesmo em um host no qual o docker não esteja instalado.
  • A capacidade de criar imagens dinamicamente em tempo real
  • A capacidade de alterar a composição do ambiente (adição / parada de contêineres) durante sua operação
  • Código de clichê reduzido, composição aprimorada e capacidade de reutilizar o código de configuração através do uso de modelos parametrizáveis

Referências


Página de projeto do Github
Exemplo detalhado passo a passo, em inglês.


Conclusão


Só isso. Num futuro próximo, pretendo adicionar a oportunidade
chame modelos de modelos e, assim, permita combiná-los com maior eficiência.


Tentarei responder a quaisquer perguntas e ficarei feliz se mais alguém achar esse projeto útil.

Source: https://habr.com/ru/post/pt439236/


All Articles