Docker Swarm é a solução de orquestração de containers contruída e mantida pela Docker. Através de uma camada adicional sobre a Docker Engine e com um subset de comandos próprios, Swarm entrega aos desenvolvedores e profissionais de infraestrutura um caminho simplificado para realizar algo nada trivial – orquestrar containers em modelo de cluster.
No último post que escrevi aqui no blog falei de maneira mais detalhada sobre o Swarm e apresentei o processo de criação/configuração de um cluster Swarm, já preparando quem acompanha o blog para este post de hoje, onde faremos de fato, a orquestração manual de containers usando Docker Swarm. Dessa forma, para que seja possível a você reproduzir os exemplos apresentados aqui, recomendo fortemente a leitura e execução dos passos apresentados no primeiro post, disponível aqui.
Criando a aplicação que será distribuída
Para que possamos distribuir uma aplicação através de containers com Swarm precisamos primeiro da aplicação, certo? Então começaremos com isso. Note, o foco aqui não é a aplicação em si mas sim, o processo de distribuição da mesma utilizando Swarm. Por isso, vamos criar uma aplicação ASP.NET Core MVC simples (estou assumindo que você possua .NET Core já instalado e configurado em sua máquina. Se este não é o caso, veja como fazer isso seguindo os passos descritos neste site). Depois iremos conteinerizá-la localmente (estou assumindo que você já possui o Docker instalado e configurado em sua máquina. Se este não é o caso, por favor, siga os procedimentos descritos neste site para obter o Docker). Depois iremos publicá-la no Azure Container Registry para só então realizarmos o processo de distribuição.
Criando a aplicação
Para criar uma nova aplicação ASP.NET Core hoje em dia, a maneira mais simples e rápida na minha opinião é através do Prompt de comandos, Powershell ou Bash. Criei minha aplicação através da linha comandos apresentada pela Figura 1.
Figura 1. Criando a aplicação “DockerDemoApp”
Para verificar se a aplicação está funcional, naveguei até a estrutura do arquivo *.csproj do projeto e executei o comando “dotnet run”. O resultado pode ser visualizado através da Figura 2.
Figura 2. Aplicação ASP.NET Core funcionando corretamente
Farei apenas duas customizações nesta aplicação:
- Modificarei o título (apenas para ter alguma personalização). Para isso, no projeto, naveguei até a pasta “Views” > “Shared”, editei o arquivo “_Layout.cshtml” e adicionei a linha “
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">Fabricio's App</a>
” a ele. - Alterei a porta através da qual o kestrel ouve os requests. Ao invés da porta 5000, utilizarei a porta 8080. Essa alteração deve ser realizada no arquivo “Program.cs”. Com isso o método “BuildWebHost” foi atualizado, conforme apresenta a Listagem 1.
Listagem 1. Ajustando a aplicação para que requests sejam ouvidos na porta 8080
Executando esta aplicação agora, temos o resultado apresentado pela Figura 3.
Figura 3. Aplicação ajustada para ouvir requests na porta 8080 funcionando adequadamente
Publicando e “dockerizando” a aplicação
Nosso próximo passo é claro – precisamos fazer com que esta aplicação seja executável em um container Docker, certo? Certo. Mas antes, precisamos publicar a aplicação de exemplo. Isso porque iremos utilizar uma imagem específica de produção (e portanto bem mais leve) do .NET Core. Como você deve imaginar, existem sim outras imagens de .NET Core disponíveis para diferentes fins (dev, build, etc.), entretanto, como nossa aplicação teoricamente já está pronta para a produção, utilizaremos esta imagem específica de produção como base para o container host.
Utilizei o comando “dotnet publish” (dotnet publish -o ./publish
) para publicar a aplicação localmente. No meu caso, requisitei ao publisher do .NET core que os arquivos resultantes deste processo sejam hospedados no diretório “publish” na raiz da aplicação, entretanto, você pode ordenar ao publisher que ele envie estes arquivos para qualquer diretório de sua preferência. A Figura 4 apresenta a estrutura de arquivos e diretórios da pasta “publish”.
Figura 4. Os arquivos gerados pela publicação
Agora que já temos “o que containerizar”, podemos então partir para esta fase. O primeiro passo nesse sentido consiste em se informar ao docker engine como ele deve configurar o container que hospedará nossa aplicação, e você sabe, o elemento primário para que se possa fazer isso é o famoso arquivo chamado “Dockerfile”. A Listagem 2 apresenta o conteúdo do mesmo.
Listagem 2. Dockerfile da aplicação “DockerDemoApp”
O que estamos fazendo aqui é:
- Informando ao docker engine qual imagem ele deverá utilizar como padrão.
- Informando ao docker engine que ele deverá criar uma pasta chamada “app” no diretório raiz do container e que este mesmo diretório será o padrão que hospedará a aplicação.
- Informando ao docker engine que, durante o processo de build, ele deverá copiar o conteúdo do diretório “publish” para o WORKDIR.
- Explicitando ao docker engine que o container da aplicação deverá o executar o comando “dotnet” contra a dll “DemoDockerApp”.
- Explicitando ao docker engine que a porta através da qual os requests serão ouvidos no contrainer é a “8080”.
Simples e direto. Agora, como próximo passo, podemos então gerar nosso container. Para isso, utilizaremos o comando “docker build” (docker build -t docker-demo-app-prod .
). Se tudo correu bem, você estará visualizando uma tela semelhante aquela apresentada pela Figura 5.
Figura 5. Processo de build gerado com sucesso
Agora já podemos testar este container rodando localmente. Para isso, execute o comando “docker run” (docker run -p 8080:8080 docker-demo-app-prod
). O resultado da aplicação sendo executada a partir do container pode ser visualizado através da Figura 6.
Figura 6. Aplicação rodando a partir do container
Azure Container Registry (ACR) como repositório da imagem
Done! Aplicação construída, containerizada e rodando localmente. Próximo passo? Publicar esta imagem em um repositório de imagens online, público ou privado. Precisamos necessariamente fazer isso. Por que? Para que possamos distribuir esta aplicação em tempo de execução quando formos utilizar o Swarm para orquestrar os containers.
Caso você esteja sendo apresentado a este conceito de repositório de imagens apenas agora e não sabe exatamente do que estamos falando, pense em um repositório de código (GitHub, por exemplo), onde você faz updates no código, faz versões e tudo mais. É basicamente o mesmo conceito aqui, só que para o contexto de imagens de containers. Felizmente, existem algumas boas opções para hospedar imagens de containers hoje em dia. A mais popular é da propria Docker – Docker Hub. Docker Hub é gratuíto e funcionaria perfeitamente para nossas necessidades aqui, entretanto, a imagem fica pública (qualquer um pode fazer pulling dela) e isso não é o que desejamos (ok, acabei de criar essa premissa 🙂 ).
Dessa maneira, optei por utilizar uma solução mais robusta e corporativa como meu repositório de imagens – O Azure Container Registry. Azure Container Registry é um serviço gerenciado de gestão de imagens baseado no open-source Docker Registry 2.0. Além de criar e manter diferentes versões para seus containers, ACR lhe disponibilizará ferramentas robustas para garantir a privacidade das imagens que estão sendo geradas/consumidas. Se você quiser saber mais sobre o ACR (e eu recomendo que o faça), por favor, siga este link.
Criando o ACR
Para criar o ACR estou utilizando o Azure CLI. Mas o procedimento também poderia ser realizado via Powershell ou Azure Portal. A linha de código que criou meu ACR foi: az acr create --resource-group swarm --name swarmfabricio --sku Basic
. Se tudo correu bem, você deverá ter visualizado uma mensagem de sucesso detalhes sobre o recurso recém criado, conforme apresenta a Figura 7.
Figura 7. Azure Container Registry criado com sucesso
Como próximo passo, para tornar o processo Seguro, você pode habilitar um usuário administrativo para seus repositórios. Eu fiz isso através do comando “az acr update” (az acr update -n swarmfabricio --admin-enabled true
). Seu usuário de acesso será o nome de seu ACR (no meu caso “swarmfabricio”) e a sua senha será gerada automaticamente. Para obtê-la, digite o comando “az acr credentials” (az acr credential show --name swarmfabricio --query "passwords[0].value"
).
Pronto. Agora já podemos fazer o push de nossa imagem para nosso repositório privado. Para isso, completei três passos:
- Me autentiquei no ACR utilizando o comando
docker login --username swarmfabricio --password {minha-senha} swarmfabricio.azur
eacr.io - “Tag-ei” a imagem local para atender a taxonomia do ACR. Fiz isso através do comando
docker tag docker-demo-app-prod:latest swarmfabricio.azureacr.io/docker-demo-app-prod:v1
- Fiz o push da imagem através do comando
docker push swarmfabricio.azureacr.io/docker-demo-app-prod:v1
A Figura 8 apresenta a imagem já em meu ACR.
Figura 8. Imagem da aplicação “DockerDemoApp” já no ACR
Pronto. Para testar se tudo estava funcionando adequadamente, utililizei o comando docker run na imagem que recém havia enviado para o Azure Container Registry e, advinhe? Tudo funcionando perfeitamente, como é possível visualizar na Figura 9.
Figura 9. “DockerDemoApp” rodando localmente após pulling a partir do ACR
All set. Próxima etapa, Swarm!
Orquestrando containers com Swarm
Finalmente chegamos ao ponto da orquestração dos containers que irão rodar as instâncias de nossa aplicação “DockerDemoApp”. Porém, antes de “sujarmos nossas mãos”, precisamos entender alguns conceitos fundamentais para se trabalhar com Docker Swarm. Vamos rapidamente discuti-los a seguir e depois, prática!
Para que seja possível ao Docker Swarm realizar a orquestração dos containers, existem basicamente quatro elementos através dos quais tudo acontece: “Tasks”, “Services”, “Nodes” e “Load Balancing”. Mencionei rapidamente algo sobre esses conceitos no primeiro post desta série de dois artigos, entretanto, hoje, vamos aprofundar um pouco mais sobre eles para seguirmos com o processo na prática.
Nodes
No contexto de um Swarm cluster, um node é uma instância física ou virtual que executa o docker engine em Swarm mode. Estas instâncias podem assumir basicamente três papéis: managers, workers ou ambos. Managers são os responsáveis por controlar todo o ciclo de vida dos containers, conciliação entre os estados, atribuição dos workloads para os elementos abaixo do cluster e muito mais. Workers são nodes que apenas recebem cargas de trabalho e as processam. Nodes que assumem ambas as funções priorizarão sempre a função de manager em relação a worker. A Figura 10 a seguir apresenta a visão geral de um Swarm cluster com 3 nodes managers/workers e 3 nodes workers. Apenas 1 manager está atuando como manager. Os demais dois nodes estão prontos para serem managers caso o manager ativo falhe por alguma razão. Enquanto isso não ocorre, eles atuam como workers apenas.
Figura 10. Visão geral dos nodes de um Swarm cluster
Tasks e Services
De maneira bem pragmática, podemos dizer que uma task é um ambiente de execução de um único container que “responde” para a gestão dos nodes managers do Swarm. Diferente de instâncias Docker standalone, tasks não possuem qualquer tipo de autonomia. Elas tem a configuração pré-definida pelos managers e vão sempre executar aquilo que lhes foi designado e pronto. Uma task será sempre criada e atribuída um um node worker pelo manager. Outro aspecto importante sobre tasks é que uma dada task nunca poderá iniciar sua execução em um node e terminar em outro. Swarm utiliza tasks como unidades atômicas de distribuição de workloads, portanto, no context do Swarm, não falaremos mais em containers – falaremos em tasks sendo executadas dentro de um dado node abaixo de um service. A Figura 11 apresenta uma visão geral das tasks distribuídas ao longo dos nodes.
Figura 11. Tasks sendo distribuídas ao longo dos nodes do cluster
Enquanto as tasks são os elementos através dos quais os containers são distribuídos ao longo dos nodes, é o service o elemento responsável por todas as definições relacionadas ao ambiente. Como veremos a seguir, quando criamos um service (e esta é a única maneira disponível para interagir com o Swarm) definimos o número de replicas para os nodes, imagem que o docker swarm deve considerar para deploy dos containers, comandos que serão executados dentro dos containers, camadas de redes, dentre tantas outras configurações. Portanto, esta distribuição de tarefas ocorre depois, quando o service já definiu quando tudo deverá funcionar. Veja a ilustração da Figura 12 para este conceito ficar mais claro.
Figura 12. Service definindo cada task a ser distribuída
Load Balancing
Este é outro conceito fundamental quando falamos sobre clusters. Ele é fundamental porque, no final das contas, é ele o elemento primário de distrubuição de carga entre as tasks sendo executadas em cada node.
Para cada node do cluster Swarm utiliza o método ingress load balancing para expor publicamente os serviços desejados ao acesso externo. O que precisa ser feito no entanto, é atribuir um valor de porta disponível ao parâmetro “PublishedPort”. Podemos definir este valor no momento em que criamos o serviço. Se não atribuirmos manualmente um valor, o swarm irá automaticamente atribuir uma porta disponível entre 30000-32767.
Se você está criando sua estrutura em uma nuvem publica e quiser fazer com que outros serviços associados a sua aplicação se comuniquem com suas tasks em execução no cluster Swarm, vá em frente. Conforme já mencionei, cada node do cluster trabalha com o modelo de external ingress. Basta acessar a “PublishedPort” correta.
Outro importante aspecto relacionado ao Swarm é que ele possui um componente de DNS interno que atribui automaticamente a cada serviço sendo executado uma entrada na tabela interna de DNS. Por que isso é importante? Porque internamente o load balancer utiliza estes DNS names para distribuir as cargas entre os diferentes serviços em execução. A Figura 13 irá clarificar este conceito.
Figura 13. Cluster Swarm com a visão de balanceamento de cargas
Agora que temos todos os conceitos de que precisamos, podemos avançar para o trabalho com Docker Swarm propriamente dito. Não coincidentemente, a Figura 13 reflete de maneira bem próxima o cluster Swarm com o qual iremos trabalhar, isto é, três máquinas managers (e ao mesmo tempo workers) e três maquinas workers. O processo para criação e configuração do cluster já foi coberto em um post anterior aqui no site.
Conforme mencionado anteriormente, toda nossa interação com o cluster se dará através do node manager eleito como líder. No meu caso, me refiro ao node “node-manager-1”. Dessa maneira, a primeira coisa a ser feita é acessar este ambiente e para evitar maiores dores de cabeça, já rodar o comando “sudo su
” para ter acesso de administrador ao ambiente como um todo. A Figura 14 apresenta meu ambiente manager já funcionando adequadamente.
Figura 14. Conectado à maquina “node-manager-1” que lidera o cluster Swarm
A primeira verificação que farei (apenas para fins didáticos) é visualizar quantos serviços estão sendo executados em meu cluster. Para isso, utilizarei o comando “docker service ls
“. O retorno da execução deste comando pode ser visualizado na Figura 15.
Figura 15. Nenhum serviço sendo executado no cluster
Ok. Nosso cluster está vazio, isto é, não temos nenhum serviço rodando e portanto, sabemos que não existe qualquer task atribuída aos nodes. Vamos iniciar nossas atividades aqui dando permissão ao manager para fazer pulling de nossa imagem hospedada no ACR, uma vez que nosso repositório é privado e fechado por usuário e senha. Para isso, novamente utilizarei o comando “docker login”. O resultado deste processo pode ser visualizado através da Figura 16.
Figura 16. Autenticado com sucesso junto ao ACR
Próximo passo? Criar nossa primeira distribuição de aplicação através da definição de um novo serviço. Para fazer isso, utilizarei o seguinte comando: docker service create --with-registry-auth --name servicev1 -p 8080:8080 --replicas 5 swarmfabricio.azurecr.io/docker-demo-app-prod:v1
. A Figura 17 apresenta a saída de tela após a execução deste comando ser concluída.
Figura 17. Service “servicev1” criado com sucesso
Antes de seguirmos com algumas verificações de sucesso, vamos primeiro entender o que estamos fazendo com a linha de comando utilizada para a criação de nosso service inicial.
- docker service create: estamos, obviamente, ordenando ao Swarm que crie um novo service definition.
- –with-registry-auth: como estamos utilizando um modelo de pulling descentralizado, isto é, as imagens serão distribuídas ao longo de outros nodes e não temos acesso a eles, precisamos informar ao Swarm quais credenciais de acesso ao repositório ele deve usar para fazer este processo de pulling. Ao utilizar este parâmetro, estamos passando adiante a mesma credencial de acesso utilizada pelo node manager líder.
- –name servicev1: apenas atribuindo um label ao service definition que está sendo criado.
- -p 8080:8080: lembra-se de quando falamos que cada serviço poderá ter uma porta aberta para comunicação externa? Então, para este serviço, a porta que explicitamente estamos abrindo é a 8080, que será mapeada para a porta 8080 de cada uma das tasks sendo executadas nos nodes. Dessa forma o tráfego se dará da seguinte maneira:
- Request chega na porta 8080 do service “servicev1”;
- Seguindo a estratégia de distribuição de carga do Swarm (ver Figura 13), este Request é então direcionado para o load balancer também externo de um dos nodes que está executando tasks geradas por “servicev1”.
- O processo de NAT é então concluído com o request chegando a porta 8080 do container abaixo da task selecionada para receber a carga do request.
- –replicas 5: estamos ordenando ao Swarm que crie 5 replicas (portanto, tasks) desta aplicação. Estas cinco replicas serão distribuídas ao longo dos nodes. Quem fará a avaliação de “qual task vai para qual node” será o manager líder.
- swarmfabricio.azurecr.io/docker-demo-app-prod:v1: estamos ordenando ao Swarm que faça o pulling da imagem da aplicação a partir de nosso Azure Container Registry.
Pronto. Aplicação teóricamente distribuída. Vamos primeiro testar se a mesma está funcionando, certo? Vamos então executar o comando “docker service ps <id>” para verificar quais nodes estão rodando nossas 5 replicas. Assim, poderemos chamar qualquer uma das máquinas executando a aplicação através de nosso navegador e a mesma deverá carregar. O resultado da execução do comando anterior pode ser visto através da Figura 18.
Figura 18. Listagem dos nodes executadando as tasks de nosso “servicev1”
Além de nos mostrar quais nodes estão rodando efetivamente as tasks (e portanto containers) com nossa aplicação, o retorno de tela nos dá também uma informação importante sobre “Desired State” versus “Current State”. Isso nos mostra a natureza resiliente do Docker Swarm. Quando uma task é criada e alocada para um dos nodes, o Swarm espera (Desired State) que esta task execute corretamente (running). Caso algo de errado ocorra que impeça este estado, o Swarm marcará esta task como “Exited”, indicando que o “Current State” está diferente do “Desired State”. Este fato irá disparar por parte do Swarm o comportamento de gerar uma nova task para substituir aquela que falhou anteriormente. Bacana, né?
Para testar se a aplicação está funcionando corretamente, basta abrir seu navegador e “chamar” a aplicação tanto pelo DNS name (se estiver disponível) quando pelo IP público do node “:” porta declarada como aberta (em nosso caso, 8080). A Figura 19 apresenta a aplicação funcionando corretamente.
Figura 19. Aplicação em pleno funcionando em “node-worker-2”
Tanto “node-worker-2”, “node-manager-2”, “node-worker-3”, “node-manager-1” e “node-manager-3” estão rodando tasks com nossa aplicação encapsulada. Dessa forma, poderia chamar qualquer um dos ips públicos para constatar que nossa aplicação está funcionando adequadamente. Para exemplificar, imprimi o resultado gerado pelo node “node-worker-2”.
Escalando a aplicação de exemplo
Vamos imaginar que a aplicação “DockerDemoApp” é um sucesso e que o número de acessos a ela vem crescendo rapidamente. Associando a isso, vale mencionar que estamos em um ambiente de cluster, o que automaticamente nos leva a pensar em alta disponibilidade e “escalabilidade”. Pois bem, a seguir quero mostrar o processo de se escalar containers utilizando Docker Swarm.
Digamos que as pessoas responsáveis pelo ambiente onde nossa aplicação está sendo executada identificaram que a nossa capacidade de resposta para a app tem que duplicar. Isso significa que precisaríamos sair de 5 tasks para 10, certo? Considerando este cenário, fui até o “node-manager-1” e digitei o seguinte comando: docker service scale servicev1=10
. O resultado gerado pela execução deste comando pode ser visualizado através da Figura 20.
Figura 20. “servicev1” escalado agora para 1o instâncias ao invés de apenas 5
Simples, rápido e direto. Duplicamos a capacidade de processamento e resposta de nossa aplicação. A aplicação continua respondendo em qualquer um dos nodes listados desde que chamada na porta 8080.
Outra observação importante é que o comando “docker service scale” trata-se apenas de um atalho para “docker service update –replicas”. Independente do comando utilizado aqui, o efeito será o mesmo, isto é, você irá escalar a capacidade de processamento de suas aplicações.
Atualizando a aplicação de exemplo
Evidentemente que, quanto mais a aplicação cresce em um ambiente de cluster, maior é a complexidade para se manter tal estrutura, certo? Neste contexto, atualizar vesões das aplicações certamente não será tarefa fácil. A boa notícia é que o Docker Swarm tem um processo simples e bem definido para que seja possível atualizar os tasks rodando as instâncias de uma dada aplicação já em execução. Nesta sessão, passaremos por este processo.
Para demonstrar essa capacidade, fiz algumas alterações na aplicação de exemplo (adicionei um novo ítem de menu principal e alterei a informação de rodapé) que estamos utilizando e enviei esta nova versão (push) para o nosso ACR com a versão “v2”, como é possível visualizar através da Figura 21. O processo de geração e envio da imagem para um repositório online já foi tratado neste post quando estávamos gerando a primeira versão da aplicação de exemplo.
Figura 21. Nova versão da aplicação já disponível em nosso ACR
O próximo passo então consiste em atualizarmos nosso serviço já em execução com a nova versão desta aplicação, certo? Para isso, utilizei o comando: docker service update --with-registry-auth --image swarmfabricio.azurecr.io/docker-demo-app-prod:v2 --update-parallelism 2 --update-delay 10s
. Vamos a explicação do que este comando está fazendo exatamente?
- docker service update: auto explicativo. Estamos ordenando ao Swarm que faça uma atualização em algum serviço ativo.
- –with-registry-auth: como estamos utilizando um modelo de pulling descentralizado, isto é, as imagens serão distribuídas ao longo de outros nodes e não temos acesso a eles, precisamos informar ao Swarm quais credenciais de acesso ao repositório ele deve usar para fazer este processo de pulling. Ao utilizar este parâmetro, estamos passando adiante a mesma credencial de acesso utilizada pelo node manager líder.
- –image swarmfabricio.azurecr.io/docker-demo-app-prod:v2: estamos explicitando para o Swarm onde ele deverá buscar a imagem da nova versão da aplicação para pulling.
- –update-parallelism 2: estamos ordenando ao Swarm que o processo de update seja realizado aos pares, ou seja, atualize duas tasks simultaneamente. Quando terminar, atualize mais duas. E assim por diante.
- –update-delay: estamos informando ao Swarm que adicione uma janela de tempo de 1o segundos entre um update de pares de tasks e outro, ou seja, quando uma dada atualização terminar, espere 1o segundos e então inicie a atualização de mais dois nodes.
O resultado do processo de atualização da aplicação pode ser visualizado através da Figura 22. No navegador, ao chamar qualquer dos nodes executando a nova versão da aplicação, será possível visualizer as alterações.
Figura 22. Tasks atualizadas com a versão mais recente da aplicação
Done!
Resumindo
Wow, quanto conteúdo vimos aqui hoje. Um resumo do que vimos pode ser encontrado abaixo.
- Criamos uma aplicação ASP.NET Core simples e a containerizamos.
- Criamos um repositório privado no Azure (serviço Container Registry) e enviamos a primeira versão da aplicação pra lá.
- Entendemos os conceitos fundamentais relacionados ao processo de distribuição e manutenção de aplicações em um cluster Docker Swarm.
- Criamos nosso primeiro serviço de distribuição e entendemos em detalhes o que cada parte dos comandos fazem.
- Escalamos nossa aplicação de 5 tasks para 10 em alguns segundos.
- Atualizamos nossas instâncias de aplicações (tasks) com uma versão recente (v2) do código fonte.
Existem ainda alguns outros conceitos relacionados aos clusters Swarm que poderiam ser discutidos aqui (networks, Docker Compose, etc), entretanto, fogem ao escopo deste post que é introdutório à tecnologia. Se você quiser aprofundar seus estudos sobre Docker Swarm e tecnologias relacionadas ao que fizemos aqui, disponibilizo a seguir um pequeno compilado de links que poderão lhe ajudar nesta jornada.
Links úteis
- Fundamentos de Docker: link
- Fundamentos de Docker Swarm: link
- Azure Container Registry: link
- Tutoriais sobre ASP.NET Core: link
- Criando e configurando um cluster de Docker Swarm: link
- Azure Container Services (ACS): link
Por hoje é isso. Até a próxima!
Facebook
Twitter
Instagram
LinkedIn
RSS