Convido a todos a conhecer a transcrição da apresentação de Alexander Sigachev da Inventos "O Processo de Desenvolvimento e Teste com Docker + Gitlab CI"
Aqueles que estão apenas começando a implementar o processo de desenvolvimento e teste baseado em Docker + Gitlab CI costumam fazer perguntas básicas. Por onde começar? Como organizar? Como testar?
Esta apresentação é boa porque explica de maneira estruturada o processo de desenvolvimento e teste utilizando Docker e Gitlab CI. A própria apresentação é de 2017. Acredito que dela podemos extrair fundamentos, metodologias, ideias e experiências de uso.

Para quem está interessado, convido a continuar lendo.
Meu nome é Alexander Sigachev. Trabalho na empresa Inventos. Vou compartilhar minha experiência com o Docker e como estamos progressivamente implementando em nossos projetos.
Tema da apresentação: O Processo de Desenvolvimento com Docker e Gitlab CI.

Esta é a minha segunda apresentação sobre Docker. Na época da primeira apresentação, usávamos Docker apenas em Development nas máquinas dos desenvolvedores. O número de funcionários que utilizavam Docker era de cerca de 2-3 pessoas. Gradualmente, acumulamos experiência e avançamos um pouco mais. Link para nossa .
O que será abordado nesta apresentação? Vamos compartilhar nossa experiência sobre os desafios que encontramos e como resolvemos cada problema. Nem tudo foi perfeito, mas nos permitiu avançar.
Nosso lema: dockerize tudo que nossas mãos alcançam.

Quais problemas estamos resolvendo?
Quando há várias equipes na empresa, o programador se torna um recurso compartilhado. Existem momentos em que o programador é retirado de um projeto e alocado temporariamente em outro.
Para que o programador consiga se adaptar rapidamente, é necessário que ele faça o download do código-fonte do projeto e inicie o ambiente o mais rápido possível, o que permitirá que ele siga avançando nas tarefas daquele projeto.
Em geral, se começarmos do zero, a documentação do projeto é escassa. As informações sobre como configurar estão disponíveis apenas para os veteranos. Os funcionários configuram seu ambiente de trabalho em um ou dois dias. Para acelerar isso, utilizamos o Docker.
A próxima razão é a padronização das configurações em Desenvolvimento. Pela minha experiência, os desenvolvedores sempre tomam a iniciativa. Em um caso a cada cinco, um domínio personalizado é introduzido, como vasya.dev. Ao lado, está o vizinho Petya, que tem o domínio petya.dev. Eles estão desenvolvendo um site ou algum componente do sistema, usando esse nome de domínio.
Quando o sistema se expande e esses nomes de domínio começam a aparecer nas configurações, ocorre um conflito nos ambientes de Desenvolvimento e o caminho do site é reescrito.
A mesma coisa acontece com as configurações do banco de dados. Alguém não se preocupa com a segurança e trabalha com a senha root vazia. Em outro caso, durante a instalação, o MySQL pediu uma senha e a senha era apenas 123. Muitas vezes ocorre que as configurações do banco de dados mudam constantemente, dependendo do commit do desenvolvedor. Alguém corrigiu, alguém não corrigiu a configuração. Havia estratégias em que nós externalizávamos alguma configuração de teste para .gitignore e cada desenvolvedor tinha que instalar o banco de dados. Isso complicava o processo de início. Além de tudo, é preciso lembrar do banco de dados. O banco de dados precisa ser inicializado, é necessário definir a senha, definir o usuário, criar a tabela e assim por diante.
Outro problema é as diferentes versões das bibliotecas. Muitas vezes, o desenvolvedor trabalha em diferentes projetos. Há um projeto legado que começou há cinco anos (a partir de 2017 - nota do editor). No início, começou-se com o MySQL 5.5. Existem também projetos modernos, onde tentamos implementar versões mais recentes do MySQL, como 5.7 ou superiores (em 2017 - nota do editor).
Quem trabalha com MySQL sabe que essas bibliotecas trazem dependências. É bastante problemático executar 2 bancos de dados juntos. Pelo menos, é problemático conectar clientes antigos a um novo banco de dados. Isso, por sua vez, gera vários problemas.
O próximo problema é quando o desenvolvedor trabalha em uma máquina local, ele usa recursos locais, arquivos locais, e RAM local. Toda a interação durante o desenvolvimento das soluções ocorre no que funciona em uma única máquina. Um exemplo disso é quando temos 3 servidores backend em produção, e o desenvolvedor salva arquivos no diretório raiz, de onde o nginx pega os arquivos para responder às solicitações. Quando esse código vai para produção, percebe-se que o arquivo está presente em um dos 3 servidores.
Atualmente, está em ascensão a abordagem de microsserviços. Quando dividimos nossas grandes aplicações em pequenos componentes que interagem entre si. Isso permite escolher tecnologias específicas para o conjunto de tarefas requerido. Também permite dividir o trabalho e a área de responsabilidade entre os desenvolvedores.
O desenvolvedor frontend, ao trabalhar com JS, praticamente não influencia o backend. O desenvolvedor backend, por sua vez, desenvolve, neste caso, em Ruby on Rails e não interfere no frontend. A interação ocorre através de APIs.
Como bônus, com o Docker conseguimos otimizar os recursos no staging. Cada projeto, devido à sua especificidade, requer configurações específicas. Precisava-se fisicamente alocar um servidor virtual para cada um e configurá-los separadamente, ou então dividir algum ambiente variável, onde os projetos poderiam, dependendo da versão das bibliotecas, influenciar uns aos outros.

Ferramentas. O que estamos usando?
- Diretamente o Docker em si. No Dockerfile, são descritas as dependências de uma aplicação.
- O Docker-compose é a conexão que reúne nossos vários aplicativos Docker.
- Usamos o GitLab para armazenar o código fonte.
- Usamos o GitLab-CI para integração contínua.

A apresentação consiste em duas partes.
A primeira parte abordará como iniciamos o Docker nas máquinas dos desenvolvedores.
A segunda parte discutirá como interagir com o GitLab, como executamos os testes e como fazemos o deploy no Staging.

O Docker é uma tecnologia que permite (usando uma abordagem declarativa) descrever os componentes necessários. Este é um exemplo de Dockerfile. Aqui declaramos que estamos herdando da imagem oficial do Docker Ruby:2.3.0. Isso contém o Ruby versão 2.3 instalado. Instalamos as bibliotecas necessárias para compilação e o NodeJS. Descrevemos que estamos criando um diretório. /appDesignamos o diretório app como o diretório de trabalho. Neste diretório, colocamos o necessário Gemfile e Gemfile.lock. Em seguida, realizamos a construção dos projetos que instalam essa imagem de dependências. Especificamos que o contêiner estará pronto para ouvir na porta externa 3000. O último comando é aquele que executa diretamente o nosso aplicativo. Se executarmos o comando de inicialização do projeto, o aplicativo tentará ser executado e lançará o comando especificado.

Este é um exemplo mínimo de um arquivo docker-compose. Neste caso, mostramos a comunicação entre dois contêineres. Isto se refere diretamente ao serviço do banco de dados e ao serviço web. Nossos aplicativos web, na maioria das vezes, requerem um banco de dados como backend para armazenamento de dados. Como estamos utilizando o MySQL, o exemplo com MySQL é adequado, mas nada impede o uso de outro banco de dados (PostgreSQL, Redis).
Estamos pegando uma imagem do MySQL 5.7.14 do Docker Hub sem alterações. A imagem que contém nosso aplicativo web é construída a partir do diretório atual. Durante a primeira execução, ela cria a imagem para nós. Após isso, executa o comando que estamos realizando aqui. Se voltarmos um pouco, veremos que foi definida uma command de inicialização através do Puma. Puma é um serviço escrito em Ruby. No segundo caso, estamos redefinindo. Este comando pode ser arbitrário, dependendo de nossas necessidades ou tarefas.
Também descrevemos que precisamos redirecionar a porta na máquina host do desenvolvedor da porta 3000 para a porta 3000 do contêiner. Isso é realizado automaticamente por meio do iptables e do seu mecanismo que está diretamente embutido no Docker.
O desenvolvedor pode também, como antes, acessar qualquer endereço IP disponível, por exemplo, 127.0.0.1 local ou o endereço IP externo da máquina.
A última linha indica que o contêiner web depende do contêiner db. Quando solicitamos a inicialização do contêiner web, o docker-compose iniciará o banco de dados primeiro. Após a inicialização do banco de dados (na verdade — após a inicialização do contêiner! A prontidão do BD não é garantida), o aplicativo, nosso backend, será iniciado.
Isso evita erros quando o banco de dados não está ativo e ajuda a economizar recursos quando paramos o contêiner de banco de dados, liberando assim recursos para outros projetos.

O que a utilização da dockerização de banco de dados nos proporciona no projeto. Registramos a versão do MySQL para todos os desenvolvedores. Isso ajuda a evitar alguns erros que podem ocorrer devido a discrepâncias de versões, quando o syntaxe, configuração e as definições padrão mudam. Isso permite especificar nomes de host comuns para o banco de dados, login e senha. Estamos nos afastando do caos de nomes e conflitos em arquivos de configuração que tínhamos anteriormente.
Temos a possibilidade de usar uma configuração mais otimizada para o ambiente de Desenvolvimento, que será diferente da configuração padrão. O MySQL, por padrão, é configurado para máquinas fracas e sua performance de prateleira é muito baixa.

O Docker permite usar o interpretador Python, Ruby, NodeJS, PHP na versão necessária. Nos livramos da necessidade de usar um gerenciador de versões. Anteriormente, para o Ruby, usávamos um pacote rpm que permitia mudar de versão conforme o projeto. Além disso, isso permite que, graças ao contêiner Docker, migremos o código suavemente e o versionemos junto com suas dependências. Não temos problemas em entender a versão tanto do interpretador quanto do código. Para atualizar a versão, é necessário parar o contêiner antigo e iniciar um novo. Se algo der errado, podemos derrubar o novo contêiner e levantar o contêiner antigo.
Após a construção da imagem, os contêineres serão idênticos tanto em Desenvolvimento quanto em Produção. Isso é especialmente relevante para grandes instalações.
No Frontend, utilizamos JavaScript e NodeJS.
Atualmente, nosso último projeto está em ReactJS. O desenvolvedor iniciou todos os contêineres e estava desenvolvendo usando hot-reload.
Em seguida, uma tarefa de construção de JavaScript é iniciada e o código, compilado em estática, é entregue através do nginx, economizando recursos.

Aqui está o esquema do nosso último projeto.
Quais problemas enfrentamos? Tivemos a necessidade de construir um sistema que interage com dispositivos móveis. Eles recebem dados. Uma das funcionalidades é enviar notificações push para esses dispositivos.
O que fizemos para isso?
Dividimos o aplicativo em componentes como: a parte administrativa em JS, o backend, que opera através de uma interface REST sob Ruby on Rails. O backend interage com o banco de dados. O resultado gerado é entregue ao cliente. A parte administrativa se comunica com o backend e o banco de dados via interface REST.
Também tivemos a necessidade de enviar notificações Push. Antes disso, tivemos um projeto em que foi implementado um mecanismo responsável pela entrega de notificações em plataformas móveis.
Desenvolvemos um esquema onde o operador interage com a interface administrativa pelo navegador, e a interface administrativa interage com o backend, estabelecendo a tarefa de enviar notificações Push.
As notificações Push interagem com outro componente que foi implementado em NodeJS.
Filas são construídas e, em seguida, o envio das notificações ocorre por seu próprio mecanismo.
Aqui estão representados dois bancos de dados. No momento, estamos utilizando 2 bancos de dados independentes com a ajuda do Docker, que não estão relacionados entre si. O único ponto em comum é que fazem parte de uma rede virtual compartilhada, enquanto os dados físicos estão armazenados em diretórios diferentes na máquina do desenvolvedor.

O mesmo, mas em números. Aqui, a reutilização de código é importante.
Se anteriormente falamos sobre reutilização de código na forma de bibliotecas, neste exemplo nosso serviço responsável pelas notificações Push é reutilizado como um servidor completo. Ele fornece uma API, com a qual nossa nova aplicação interage.
Naquela época estávamos utilizando a versão 4 do NodeJS. Atualmente (em 2017 — nota do editor), em novos desenvolvimentos, utilizamos a versão 7 do NodeJS. Não há problemas em novas componentes incorporarem novas versões de bibliotecas.
Se necessário, podemos realizar um refatoramento e atualizar a versão do NodeJS no serviço de notificações Push.
Se conseguirmos manter a compatibilidade da API, poderemos substituí-lo em outros projetos utilizados anteriormente.

O que é necessário para adicionar o Docker? Adicionamos um Dockerfile ao nosso repositório, que descreve as dependências necessárias. Neste exemplo, os componentes estão divididos logicamente. Este é o conjunto mínimo para um desenvolvedor backend.
Ao criar um novo projeto, criamos um Dockerfile, descrevendo o ecossistema necessário (Python, Ruby, NodeJS). O docker-compose descreve as dependências necessárias — o banco de dados. Especificamos que precisamos de um banco de dados de tal versão, para armazenar dados em determinados locais.
Utilizamos um terceiro contêiner separado com nginx para servir os arquivos estáticos. A possibilidade de carregar imagens está garantida. O backend as coloca em um volume preparado antecipadamente, que também está montado no contêiner com nginx, que serve os arquivos estáticos.
Para armazenar a configuração do nginx e do mysql, adicionamos uma pasta Docker, onde guardamos as configurações necessárias. Quando um desenvolvedor faz um git clone do repositório em sua máquina, ele já tem um projeto pronto para o desenvolvimento local. Não há dúvidas sobre qual porta ou quais configurações aplicar.

Em seguida, temos alguns componentes: admin, inform-API, notificações push.
Para executar tudo isso, criamos outro repositório que chamamos de dockerized-app. Atualmente, usamos vários repositórios para cada componente. Eles diferem apenas logicamente — no GitLab, isso aparece como uma pasta, e na máquina do desenvolvedor, como uma pasta para um projeto específico. Abaixo estão os componentes que serão integrados.

Este é um exemplo do conteúdo do dockerized-app. Nós também destacamos aqui o diretório Docker, onde organizamos as configurações necessárias para a interação de todos os componentes. Há um README.md que descreve brevemente como iniciar o projeto.
Aqui aplicamos dois arquivos docker-compose. Isso foi feito para permitir o lançamento em etapas. Quando um desenvolvedor está trabalhando com o núcleo e não precisa de notificações push, ele simplesmente inicia o arquivo docker-compose, economizando assim recursos.
Se houver necessidade de integração com notificações push, são iniciados o docker-compose.yaml e o docker-compose-push.yaml.
Como o docker-compose.yaml e o docker-compose-push.yaml estão na mesma pasta, é criada automaticamente uma rede virtual unificada.

Descrição dos componentes. Este é um arquivo mais detalhado que gerencia a coleta de componentes. O que é notável aqui? Introduzimos o componente balanceador.
Este é um Docker image pronto, onde o nginx e o aplicativo que escuta o socket do Docker são lançados. Ele regenera dinamicamente a configuração do nginx à medida que os contêineres são iniciados e desligados. A interação com os componentes é distribuída através de nomes de domínio de terceiro nível.
Para o ambiente de Desenvolvimento, usamos o domínio .dev — api.informer.dev. Aplicações com o domínio .dev estão disponíveis na máquina local do desenvolvedor.
Em seguida, as configurações são passadas para cada projeto e todos os projetos são iniciados simultaneamente.

Se representarmos graficamente, o cliente é nosso navegador ou algum instrumento com o qual realizamos solicitações ao balanceador.
O balanceador determina, pelo nome do domínio, a qual contêiner deve ser feita a solicitação.
Isso pode ser o nginx, que fornece o JS do painel administrativo. Pode ser o nginx que fornece a API ou arquivos estáticos que são entregues pelo nginx na forma de upload de imagens.
O diagrama mostra que os contêineres estão conectados a uma rede virtual e escondidos atrás de um proxy.
Na máquina do desenvolvedor, é possível acessar o contêiner conhecendo o IP, mas, em princípio, não fazemos isso. Praticamente não há necessidade de acesso direto.

Qual exemplo olhar para dockerizar sua aplicação? Na minha opinião, um bom exemplo é a imagem oficial do docker para MySQL.
É bastante complicado. Existem muitas versões. Mas sua funcionalidade atende a várias necessidades que podem surgir no decorrer do desenvolvimento. Se você dedicar tempo para entender como tudo isso interage, não deve ter problemas ao implementá-lo de forma autônoma.
No hub.docker.com, geralmente há links para github.com, onde estão os dados brutos a partir dos quais você pode construir a imagem por conta própria.
Além disso, este repositório contém o script docker-endpoint.sh, que é responsável pela inicialização primária e pelo processamento subsequente do lançamento da aplicação.
Este exemplo também oferece a possibilidade de configuração por meio de variáveis de ambiente. Ao definir a variável de ambiente ao iniciar um contêiner único ou através do docker-compose, podemos especificar que precisamos configurar uma senha vazia para o docker para o root no MySQL ou alguma outra que desejamos.
Há a opção de criar uma senha aleatória. Dizemos que precisamos de um usuário, que precisamos definir uma senha para o usuário e que precisamos criar um banco de dados.
Em nossos projetos, unificamos um pouco o Dockerfile, que é responsável pela inicialização. Nós o ajustamos para atender às nossas necessidades, ampliando os direitos do usuário que a aplicação utiliza. Isso permitiu criar um banco de dados a partir do console da aplicação. Em aplicações Ruby, há comandos para criar, modificar e eliminar bancos de dados.

Este é um exemplo de como uma versão específica do MySQL aparece no github.com. O Dockerfile pode ser aberto para ver como a instalação ocorre.
O script docker-endpoint.sh é responsável pelo ponto de entrada. Durante a inicialização inicial, algumas ações de preparação são necessárias e todas essas ações estão descritas no script de inicialização.

Vamos para a segunda parte.
Para armazenar os códigos fonte, mudamos para o gitlab. É um sistema poderoso que possui uma interface visual.
Um dos componentes do Gitlab é o Gitlab CI. Ele permite descrever a sequência de comandos que serão usados para organizar a entrega de código ou para a execução de testes automatizados.
Apresentação sobre Gitlab CI 2 — apresentação do Ruby Russia club — é bastante detalhada e pode interessá-lo.

Agora, vamos ver o que é necessário para ativar o Gitlab CI. Para iniciar o Gitlab CI, precisamos simplesmente colocar o arquivo .gitlab-ci.yml na raiz do projeto.
Aqui descrevemos o que queremos executar em uma sequência de estados do tipo teste, implantação.
Executamos scripts que chamam diretamente a construção do nosso aplicativo com docker-compose. Este é um exemplo do backend.
Em seguida, dizemos que é necessário executar as migrações para a alteração do banco de dados e executar os testes.
Se os scripts forem executados corretamente e não retornarem códigos de erro, o sistema passará à segunda fase da implantação.
A fase de implantação, neste momento, é implementada para staging. Não organizamos uma reinicialização sem interrupções.
Forçamos a parada de todos os contêineres e depois levantamos todos os contêineres novamente, construídos na primeira etapa durante os testes.
Executamos novamente as migrações do banco de dados para o ambiente variável atual que foram escritas pelos desenvolvedores.
Há uma nota de que isso deve ser aplicado apenas para a branch master.
Quando outras branches são alteradas, não serão executadas.
Há uma possibilidade de organizar versões por branches.

Para organizar isso, precisamos instalar o Gitlab Runner.
Esta ferramenta é escrita em Golang. É um único arquivo, como é comum no mundo Golang, e não requer dependências.
Ao iniciar, registramos o Gitlab Runner.
Obtivemos a chave no web interface do Gitlab.
Então chamamos o comando de inicialização no terminal.
Configuramos o Gitlab Runner em modo de diálogo (Shell, Docker, VirtualBox, SSH)
O código no Gitlab Runner será executado a cada commit, dependendo da configuração do .gitlab-ci.yml.

Como isso é visualmente apresentado no GitLab na interface web. Depois de conectar o GitLab CI, aparece uma bandeira que mostra em que estado o build está no momento.
Podemos ver que há 4 minutos foi feito um commit que passou por todos os testes sem causar problemas.

Podemos olhar mais detalhadamente os builds. Aqui vemos que dois estados já foram passados: o estado de teste e o estado de implantação no staging.
Se clicarmos em um build específico, haverá a saída do console dos comandos que foram executados no processo de acordo com o .gitlab-ci.yml.

Assim é como a história do nosso produto se apresenta. Vemos que houve tentativas bem-sucedidas. Quando os testes falham, o código não avança para o próximo passo e não há atualização no staging.

Quais problemas estávamos resolvendo no staging quando implementamos o Docker? Nosso sistema é composto por componentes e surgiu a necessidade de reiniciar apenas parte dos componentes que foram atualizados no repositório, em vez de reiniciar todo o sistema.
Para isso, tivemos que organizar tudo em pastas separadas.
Depois que fizemos isso, surgiu um problema em que o Docker-compose cria um espaço de rede separado para cada pasta e não vê os componentes vizinhos.
Para contornar isso, criamos manualmente uma rede no Docker. No Docker-compose, especificamos que este projeto usaria essa rede.
Assim, cada componente que inicia com essa rede pode ver os componentes em outras partes do sistema.
O próximo problema é a separação do staging entre vários projetos.
Para que tudo isso parecesse bonito e o mais próximo possível do production, é bom usar a porta 80 ou 443, que é amplamente utilizada na WEB.

Como resolvemos isso? Designamos um único GitLab Runner para todos os grandes projetos.
O GitLab permite iniciar vários GitLab Runners distribuídos que simplesmente pegam todas as tarefas em ordem aleatória.
Para que não houvesse confusão, limitamos nosso grupo de projetos a um único GitLab Runner, que consegue lidar bem com nossos volumes.
Retiramos o nginx-proxy para um script de execução separado e nele especificamos as redes de todos os projetos.
Nosso projeto tem uma rede única, enquanto o balanceador tem várias redes com os nomes dos projetos. Ele pode proxy para outros domínios.
Recebemos solicitações pelo domínio na porta 80, que são gerenciadas por um grupo de contêineres que atende a esse domínio.

Quais foram os outros problemas? O fato é que, por padrão, todos os contêineres são executados como o usuário root. Este root não é o mesmo root do sistema host.
No entanto, se você entrar no contêiner, estará como root, e o arquivo que criamos nesse contêiner recebe permissões de root.
Se um desenvolvedor entrou no contêiner e executou alguns comandos que geraram arquivos, depois saiu do contêiner, então em seu diretório de trabalho ele terá um arquivo ao qual não tem acesso.
Como podemos resolver isso? Podemos adicionar usuários que estarão dentro do contêiner.
Quais problemas surgiram quando adicionamos o usuário?
Ao criar um usuário, frequentemente não coincidimos o ID do grupo (UID) com o ID do usuário (GID).
Para resolver esse problema, dentro do contêiner usamos usuários com ID 1000.
No nosso caso, isso coincidiu com o fato de que praticamente todos os desenvolvedores usam o sistema operacional Ubuntu. E no Ubuntu, o primeiro usuário tem o ID 1000.

Quais são nossos planos?
Ler a documentação do Docker novamente. O projeto está em constante desenvolvimento, a documentação está mudando. As informações obtidas há dois ou três meses já estão começando a ficar desatualizadas.
Parte dos problemas que resolvemos pode já ter sido solucionada com ferramentas padrões.
Estamos ansiosos para avançar e nos concentrar diretamente na orquestração.
Um dos exemplos é o mecanismo integrado ao Docker chamado Docker Swarm, que vem pronto para uso. Gostaríamos de implementar algo em produção usando a tecnologia Docker Swarm.
A criação de contêineres dificultou o trabalho com logs. Atualmente, os logs estão isolados. Eles estão espalhados pelos contêineres. Um dos objetivos é facilitar o acesso aos logs através de uma interface web.

Fonte: habr.com
