Criando uma cadeia CI/CD e automatizando o trabalho com Docker
Escrevi meus primeiros sites no final dos anos 90. Naquela época era muito fácil colocá-los em funcionamento. Havia um servidor Apache em alguma hospedagem compartilhada, você poderia fazer login neste servidor via FTP escrevendo algo como ftp://ftp.example.com. Então você teve que inserir seu nome e senha e enviar os arquivos para o servidor. Houve tempos diferentes, tudo era mais simples do que agora.
Nas duas décadas desde então, tudo mudou muito. Os sites se tornaram mais complexos; eles devem ser montados antes de serem lançados em produção. Um único servidor tornou-se muitos servidores rodando atrás de balanceadores de carga, e o uso de sistemas de controle de versão tornou-se comum.
Para o meu projeto pessoal tive uma configuração especial. E eu sabia que precisava implantar o site em produção executando apenas uma ação: escrever código em uma ramificação master no GitHub. Além disso, eu sabia que para garantir o funcionamento da minha pequena aplicação web, não queria gerenciar um enorme cluster Kubernetes, nem usar a tecnologia Docker Swarm, nem manter uma frota de servidores com pods, agentes e todo tipo de outros complexidades. Para atingir o objetivo de facilitar ao máximo o trabalho, precisei me familiarizar com CI/CD.
Se você tem um projeto pequeno (neste caso, um projeto Node.js) e gostaria de saber como automatizar a implantação deste projeto, garantindo ao mesmo tempo que o que está armazenado no repositório corresponde exatamente ao que funciona na produção, então eu acho que você pode estar interessado neste artigo.
Pré-requisitos
Espera-se que o leitor deste artigo tenha um conhecimento básico da linha de comando e da escrita de scripts Bash. Além disso, ele precisará de contas Travis C.I. и Hub do Docker.
Objetivos
Não direi que este artigo possa ser incondicionalmente chamado de “tutorial”. Este é mais um documento no qual falo sobre o que aprendi e descrevo o processo que me convém para testar e implantar código em produção, realizado em uma passagem automatizada.
Foi assim que meu fluxo de trabalho acabou sendo.
Para código postado em qualquer ramificação do repositório, exceto master, as seguintes ações são executadas:
A construção do projeto no Travis CI é iniciada.
Todos os testes unitários, de integração e ponta a ponta são realizados.
Somente para código que se enquadra master, é realizado o seguinte:
Tudo o que foi mencionado acima, mais...
Construindo uma imagem Docker com base no código, configurações e ambiente atuais.
Implantando a imagem no Docker Hub.
Conexão com o servidor de produção.
Fazendo upload de uma imagem do Docker Hub para o servidor.
Interrompendo o contêiner atual e iniciando um novo com base na nova imagem.
Se você não sabe absolutamente nada sobre Docker, imagens e containers, não se preocupe. Eu vou te contar tudo sobre isso.
O que é CI/CD?
A abreviatura CI/CD significa “integração contínua/implantação contínua”.
▍Integração contínua
A integração contínua é um processo no qual os desenvolvedores fazem commits no repositório de código-fonte principal do projeto (geralmente um branch master). Ao mesmo tempo, a qualidade do código é garantida através de testes automatizados.
▍Implantação contínua
A implantação contínua é a implantação frequente e automatizada de código na produção. A segunda parte da sigla CI/CD às vezes é escrita como “entrega contínua”. Isto é basicamente o mesmo que “implantação contínua”, mas “entrega contínua” implica a necessidade de confirmar manualmente as alterações antes de iniciar o processo de implantação do projeto.
Introdução
O aplicativo que usei para aprender tudo isso se chama Tome nota. Este é um projeto web em que estou trabalhando, projetado para fazer anotações. No começo eu tentei fazer JAMStack-project, ou apenas um aplicativo front-end sem servidor, para aproveitar as vantagens dos recursos padrão de hospedagem e implantação de projetos que ele oferece Netlify. À medida que a complexidade do aplicativo crescia, precisei criar sua parte de servidor, o que significava que precisaria formular minha própria estratégia para integração automatizada e implantação automatizada do projeto.
No meu caso, o aplicativo é um servidor Express rodando no ambiente Node.js, servindo um aplicativo React de página única e suportando uma API segura do lado do servidor. Esta arquitetura segue a estratégia que pode ser encontrada em esta Guia de autenticação de pilha completa.
eu consultei com другом, que é especialista em automação, e perguntei o que eu precisava fazer para que tudo funcionasse do jeito que eu queria. Ele me deu a ideia de como deveria ser um fluxo de trabalho automatizado, descrita na seção Metas deste artigo. Ter esses objetivos significava que eu precisava descobrir como usar o Docker.
Estivador
Docker é uma ferramenta que, graças à tecnologia de conteinerização, permite que aplicações sejam facilmente distribuídas, implantadas e executadas no mesmo ambiente, mesmo que a própria plataforma Docker seja executada em ambientes diferentes. Primeiro, eu precisava colocar as mãos nas ferramentas de linha de comando (CLI) do Docker. Instrução O guia de instalação do Docker não pode ser considerado muito claro e compreensível, mas com ele você pode aprender que para dar o primeiro passo de instalação, você precisa baixar o Docker Desktop (para Mac ou Windows).
Docker Hub é praticamente a mesma coisa que GitHub para repositórios git ou registro npm para pacotes JavaScript. Este é um repositório online para imagens Docker. É a isso que o Docker Desktop se conecta.
Portanto, para começar a usar o Docker, você precisa fazer duas coisas:
Depois disso, você pode verificar se o Docker CLI está funcionando executando o seguinte comando para verificar a versão do Docker:
docker -v
Em seguida, faça login no Docker Hub digitando seu nome de usuário e senha quando solicitado:
docker login
Para usar o Docker, você deve compreender os conceitos de imagens e contêineres.
▍Imagens
Uma imagem é algo como um projeto que contém instruções para montar o contêiner. Este é um instantâneo imutável do sistema de arquivos e das configurações do aplicativo. Os desenvolvedores podem compartilhar imagens facilmente.
# Вывод сведений обо всех образах
docker images
Este comando gerará uma tabela com o seguinte cabeçalho:
REPOSITORY TAG IMAGE ID CREATED SIZE
---
A seguir veremos alguns exemplos de comandos no mesmo formato - primeiro há um comando com um comentário e depois um exemplo do que ele pode gerar.
▍Contêineres
Um contêiner é um pacote executável que contém tudo o que é necessário para executar um aplicativo. Uma aplicação com essa abordagem funcionará sempre da mesma forma, independente da infraestrutura: em um ambiente isolado e no mesmo ambiente. A questão é que instâncias da mesma imagem são lançadas em ambientes diferentes.
# Перечисление всех контейнеров
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
---
▍Tags
Uma tag é uma indicação de uma versão específica de uma imagem.
▍Uma referência rápida aos comandos do Docker
Aqui está uma visão geral de alguns comandos do Docker comumente usados.
Eu sei como executar um aplicativo de produção localmente. Eu tenho uma configuração do Webpack projetada para construir um aplicativo React pronto. A seguir, tenho um comando que inicia um servidor baseado em Node.js na porta 5000. Se parece com isso:
npm i # установка зависимостей
npm run build # сборка React-приложения
npm run start # запуск Node-сервера
Deve-se observar que não tenho um exemplo de aplicação para este material. Mas aqui, para experimentos, qualquer aplicativo Node simples servirá.
Para usar o contêiner, você precisará fornecer instruções ao Docker. Isso é feito através de um arquivo chamado Dockerfile, localizado no diretório raiz do projeto. Este arquivo, a princípio, parece bastante incompreensível.
Mas o que ele contém apenas descreve, com comandos especiais, algo semelhante à configuração de um ambiente de trabalho. Aqui estão alguns desses comandos:
A PARTIR DE — Este comando inicia um arquivo. Ele especifica a imagem base na qual o contêiner é construído.
CÓPIA — Copiar arquivos de uma fonte local para um contêiner.
DIRTRABALHO — Configurando o diretório de trabalho para os comandos a seguir.
# Загрузить базовый образ
FROM node:12-alpine
# Скопировать файлы из текущей директории в директорию app/
COPY . app/
# Использовать app/ в роли рабочей директории
WORKDIR app/
# Установить зависимости (команда npm ci похожа npm i, но используется для автоматизированных сборок)
RUN npm ci --only-production
# Собрать клиентское React-приложение для продакшна
RUN npm run build
# Прослушивать указанный порт
EXPOSE 5000
# Запустить Node-сервер
ENTRYPOINT npm run start
Dependendo da imagem base escolhida, pode ser necessário instalar dependências adicionais. O fato é que algumas imagens base (como Node Alpine Linux) são criadas com o objetivo de torná-las o mais compactas possível. Como resultado, eles podem não ter alguns dos programas que você espera.
▍Construir, marcar e executar o contêiner
A montagem local e o lançamento do contêiner ocorrem depois de termos Dockerfile, as tarefas são bastante simples. Antes de enviar a imagem para o Docker Hub, você precisa testá-la localmente.
▍Montagem
Primeiro você precisa coletar imagem, especificando um nome e, opcionalmente, uma tag (se uma tag não for especificada, o sistema atribuirá automaticamente uma tag à imagem latest).
# Сборка образа
docker build -t <image>:<tag> .
Depois de executar este comando, você pode observar o Docker construir a imagem.
Sending build context to Docker daemon 2.88MB
Step 1/9 : FROM node:12-alpine
---> ...выполнение этапов сборки...
Successfully built 123456789123
Successfully tagged <image>:<tag>
A compilação pode levar alguns minutos - tudo depende de quantas dependências você possui. Assim que a compilação for concluída, você pode executar o comando docker images e veja a descrição da sua nova imagem.
REPOSITORY TAG IMAGE ID CREATED SIZE
<image> latest 123456789123 About a minute ago x.xxGB
▍Lançamento
A imagem foi criada. Isso significa que você pode executar um contêiner baseado nele. Porque quero poder acessar o aplicativo em execução no contêiner em localhost:5000, eu, no lado esquerdo do par 5000:5000 no próximo comando instalado 5000. Do lado direito está o porto de contêineres.
# Запуск с использованием локального порта 5000 и порта контейнера 5000
docker run -p 5000:5000 <image>:<tag>
Agora que o contêiner foi criado e em execução, você pode usar o comando docker ps para ver informações sobre este contêiner (ou você pode usar o comando docker ps -a, que exibe informações sobre todos os contêineres, não apenas sobre os que estão em execução).
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
987654321234 <image> "/bin/sh -c 'npm run…" 6 seconds ago Up 6 seconds 0.0.0.0:5000->5000/tcp stoic_darwin
Se você for agora para o endereço localhost:5000 — você pode ver uma página de um aplicativo em execução que se parece exatamente com a página de um aplicativo em execução em um ambiente de produção.
▍ Marcação e publicação
Para utilizar uma das imagens criadas no servidor de produção, precisamos conseguir baixar esta imagem do Docker Hub. Isso significa que primeiro você precisa criar um repositório para o projeto no Docker Hub. Depois disso, teremos um local para onde poderemos enviar a imagem. A imagem precisa ser renomeada para que seu nome comece com nosso nome de usuário Docker Hub. Isto deve ser seguido pelo nome do repositório. Qualquer tag pode ser colocada no final do nome. Abaixo está um exemplo de nomeação de imagens usando este esquema.
Agora você pode construir a imagem com um novo nome e executar o comando docker push para enviá-lo para o repositório Docker Hub.
docker build -t <username>/<repository>:<tag> .
docker tag <username>/<repository>:<tag> <username>/<repository>:latest
docker push <username>/<repository>:<tag>
# На практике это может выглядеть, например, так:
docker build -t user/app:v1.0.0 .
docker tag user/app:v1.0.0 user/app:latest
docker push user/app:v1.0.0
Se tudo correr bem, a imagem estará disponível no Docker Hub e poderá ser facilmente carregada no servidor ou transferida para outros desenvolvedores.
Próximos passos
Até agora verificamos que o aplicativo, na forma de um contêiner Docker, está sendo executado localmente. Carregamos o contêiner no Docker Hub. Tudo isto significa que já fizemos progressos muito bons em direcção ao nosso objectivo. Agora precisamos resolver mais duas questões:
Configurando uma ferramenta de CI para testar e implantar código.
Configurando o servidor de produção para que ele possa baixar e executar nosso código.
Deve-se notar que aqui você pode usar outra combinação de serviços. Por exemplo, em vez do Travis CI, você pode usar CircleCI ou Github Actions. E em vez de DigitalOcean - AWS ou Linode.
Decidimos trabalhar com o Travis CI, e já tenho algo configurado neste serviço. Portanto, agora falarei brevemente sobre como prepará-lo para o trabalho.
Travis C.I.
Travis CI é uma ferramenta para testar e implantar código. Não gostaria de entrar nos meandros da montagem do Travis CI, pois cada projeto é único e isso não trará muitos benefícios. Mas abordarei o básico para você começar, caso decida usar o Travis CI. Quer você escolha Travis CI, CircleCI, Jenkins ou qualquer outra coisa, métodos de configuração semelhantes serão usados em todos os lugares.
Para começar a usar o Travis CI, vá para site do projeto e crie uma conta. Em seguida, integre o Travis CI à sua conta GitHub. Ao configurar o sistema, você precisará especificar o repositório com o qual deseja automatizar o trabalho e permitir o acesso a ele. (Eu uso o GitHub, mas tenho certeza de que o Travis CI pode ser integrado ao BitBucket, GitLab e outros serviços semelhantes).
Cada vez que o Travis CI é iniciado, é iniciado um servidor que executa os comandos especificados no arquivo de configuração, incluindo a implantação das ramificações do repositório correspondentes.
▍Ciclo de vida do trabalho
Arquivo de configuração do Travis CI chamado .travis.yml e armazenado no diretório raiz do projeto, suporta o conceito de eventos ciclo da vida tarefas. Esses eventos estão listados na ordem em que ocorrem:
apt addons
cache components
before_install
install
before_script
script
before_cache
after_success или after_failure
before_deploy
deploy
after_deploy
after_script
▍Teste
No arquivo de configuração vou configurar o servidor Travis CI local. Selecionei o Nó 12 como idioma e disse ao sistema para instalar as dependências necessárias para usar o Docker.
Tudo o que está listado em .travis.yml, será executado quando todas as solicitações pull forem feitas para todas as ramificações do repositório, a menos que especificado de outra forma. Este é um recurso útil porque significa que podemos testar todo o código que chega ao repositório. Isso permite saber se o código está pronto para ser gravado na ramificação. mastere se isso interromperá o processo de construção do projeto. Nesta configuração global, instalo tudo localmente, executo o servidor de desenvolvimento Webpack em segundo plano (este é um recurso do meu fluxo de trabalho) e executo testes.
Se você quiser que seu repositório exiba ícones de cobertura de teste, aqui Você pode encontrar instruções breves sobre como usar Jest, Travis CI e Coveralls para coletar e exibir essas informações.
Então aqui está o conteúdo do arquivo .travis.yml:
# Установить язык
language: node_js
# Установить версию Node.js
node_js:
- '12'
services:
# Использовать командную строку Docker
- docker
install:
# Установить зависимости для тестов
- npm ci
before_script:
# Запустить сервер и клиент для тестов
- npm run dev &
script:
# Запустить тесты
- npm run test
É aqui que terminam as ações executadas para todas as ramificações do repositório e para solicitações pull.
▍Implantação
Com base na suposição de que todos os testes automatizados foram concluídos com êxito, podemos, o que é opcional, implantar o código no servidor de produção. Como queremos fazer isso apenas para o código do branch master, fornecemos instruções apropriadas ao sistema nas configurações de implantação. Antes de tentar usar o código que veremos a seguir em seu projeto, gostaria de avisar que você deve ter um script real chamado para implantação.
deploy:
# Собрать Docker-контейнер и отправить его на Docker Hub
provider: script
script: bash deploy.sh
on:
branch: master
O script de implantação resolve dois problemas:
Construa, marque e envie a imagem para o Docker Hub usando uma ferramenta de CI (no nosso caso, Travis CI).
Carregando a imagem no servidor, parando o container antigo e iniciando um novo (no nosso caso, o servidor roda na plataforma DigitalOcean).
Primeiro, você precisa configurar um processo automático para construir, marcar e enviar a imagem para o Docker Hub. Tudo isso é muito parecido com o que já fizemos manualmente, exceto que precisamos de uma estratégia para atribuir tags exclusivas às imagens e automatizar logins. Tive dificuldade com alguns detalhes do script de implantação, como estratégia de marcação, login, codificação de chave SSH, estabelecimento de conexão SSH. Mas felizmente meu namorado é muito bom com bash, assim como com muitas outras coisas. Ele me ajudou a escrever esse roteiro.
Portanto, a primeira parte do script é fazer o upload da imagem para o Docker Hub. Isso é muito fácil de fazer. O esquema de marcação que usei envolve a combinação de um hash git e uma tag git, se existir. Isso garante que a tag seja exclusiva e facilita a identificação do assembly no qual ela se baseia. DOCKER_USERNAME и DOCKER_PASSWORD são variáveis de ambiente do usuário que podem ser definidas usando a interface do Travis CI. O Travis CI processará automaticamente dados confidenciais para que não caiam em mãos erradas.
Aqui está a primeira parte do script deploy.sh.
#!/bin/sh
set -e # Остановить скрипт при наличии ошибок
IMAGE="<username>/<repository>" # Образ Docker
GIT_VERSION=$(git describe --always --abbrev --tags --long) # Git-хэш и теги
# Сборка и тегирование образа
docker build -t ${IMAGE}:${GIT_VERSION} .
docker tag ${IMAGE}:${GIT_VERSION} ${IMAGE}:latest
# Вход в Docker Hub и выгрузка образа
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
docker push ${IMAGE}:${GIT_VERSION}
O que será a segunda parte do script depende inteiramente de qual host você está usando e de como a conexão com ele está organizada. No meu caso, como utilizo o Digital Ocean, utilizo os comandos para conectar ao servidor documento. Ao trabalhar com AWS, o utilitário será usado awsetc.
Configurar o servidor não foi particularmente difícil. Então, configurei um droplet com base na imagem base. Deve-se observar que o sistema que escolhi requer uma instalação manual única do Docker e uma inicialização manual única do Docker. Usei o Ubuntu 18.04 para instalar o Docker, então se você também estiver usando o Ubuntu para fazer o mesmo, basta seguir este guia simples.
Não estou falando aqui de comandos específicos para o serviço, pois esse aspecto pode variar bastante em diferentes casos. Darei apenas um plano geral de ação a ser executado após a conexão via SSH ao servidor no qual o projeto será implantado:
Precisamos encontrar o contêiner que está em execução e interrompê-lo.
Então você precisa iniciar um novo contêiner em segundo plano.
Você precisará definir a porta local do servidor para 80 - isso permitirá que você entre no site em um endereço como example.com, sem especificar a porta, em vez de usar um endereço como example.com:5000.
Finalmente, você precisa excluir todos os contêineres e imagens antigos.
Aqui está a continuação do roteiro.
# Найти ID работающего контейнера
CONTAINER_ID=$(docker ps | grep takenote | cut -d" " -f1)
# Остановить старый контейнер, запустить новый, очистить систему
docker stop ${CONTAINER_ID}
docker run --restart unless-stopped -d -p 80:5000 ${IMAGE}:${GIT_VERSION}
docker system prune -a -f
Algumas coisas para prestar atenção
É possível que ao se conectar ao servidor via SSH do Travis CI, você veja um aviso que o impedirá de continuar com a instalação, pois o sistema aguardará a resposta do usuário.
The authenticity of host '<hostname> (<IP address>)' can't be established.
RSA key fingerprint is <key fingerprint>.
Are you sure you want to continue connecting (yes/no)?
Aprendi que uma chave de string pode ser codificada em base64 para salvá-la em um formato no qual possa ser trabalhada de maneira conveniente e confiável. Na fase de instalação, você pode decodificar a chave pública e gravá-la em um arquivo known_hosts para se livrar do erro acima.
A mesma abordagem pode ser usada com uma chave privada ao estabelecer uma conexão, já que você pode precisar de uma chave privada para acessar o servidor. Ao trabalhar com a chave, você só precisa garantir que ela esteja armazenada com segurança em uma variável de ambiente do Travis CI e que não seja exibida em nenhum lugar.
Outra coisa a observar é que pode ser necessário executar todo o script de implantação como uma linha, por exemplo - com doctl. Isso pode exigir algum esforço extra.
doctl compute ssh <droplet> --ssh-command "все команды будут здесь && здесь"
TLS/SSL e balanceamento de carga
Depois de fazer tudo o que foi mencionado acima, o último problema que encontrei foi que o servidor não tinha SSL. Como uso um servidor Node.js, para forçar работать proxy reverso Nginx e Let's Encrypt, você precisa mexer muito.
Eu realmente não queria fazer toda essa configuração SSL manualmente, então apenas criei um balanceador de carga e registrei seus detalhes no DNS. No caso da DigitalOcean, por exemplo, criar um certificado autoassinado com renovação automática no balanceador de carga é um procedimento simples, gratuito e rápido. Essa abordagem tem o benefício adicional de facilitar a configuração do SSL em vários servidores executados atrás de um balanceador de carga, se necessário. Isso permite que os próprios servidores não “pensem” em SSL, mas ao mesmo tempo usem a porta normalmente 80. Portanto, configurar SSL em um balanceador de carga é muito mais fácil e conveniente do que métodos alternativos de configuração de SSL.
Agora você pode fechar todas as portas do servidor que aceitam conexões de entrada - exceto a porta 80, usado para se comunicar com o balanceador de carga e a porta 22 para SSH. Como resultado, uma tentativa de acessar diretamente o servidor em qualquer porta diferente dessas duas falhará.
Resultados de
Depois de fazer tudo o que falei neste material, nem a plataforma Docker nem os conceitos de cadeias automatizadas de CI/CD me assustaram mais. Consegui configurar uma cadeia de integração contínua, durante a qual o código é testado antes de entrar em produção e é implantado automaticamente no servidor. Tudo isso ainda é relativamente novo para mim e tenho certeza de que existem maneiras de melhorar meu fluxo de trabalho automatizado e torná-lo mais eficiente. Então, se você tiver alguma idéia sobre esse assunto, por favor me avise. me saber. Espero que este artigo tenha ajudado você em seus empreendimentos. Quero acreditar que depois de lê-lo você aprendeu tanto quanto eu aprendi ao descobrir tudo o que falei nele.
PS В нашем Mercado há uma imagem Estivador, que é instalado com um clique. Você pode verificar os contêineres funcionam VPS. Todos os novos clientes recebem 3 dias de testes gratuitos.
Caros leitores! Você utiliza tecnologias CI/CD em seus projetos?