Creando una cadena CI/CD y automatizando el trabajo con Docker
Escribí mis primeros sitios web a finales de los 90. Entonces fue muy fácil ponerlos en condiciones de funcionar. Había un servidor Apache en algún alojamiento compartido, se podía acceder a este servidor a través de FTP escribiendo en la línea del navegador algo como ftp://ftp.example.com. Luego era necesario ingresar un nombre y contraseña y subir los archivos al servidor. Hubo otros tiempos, entonces todo era más sencillo que ahora.
Las cosas han cambiado mucho en las últimas dos décadas. Los sitios se han vuelto más complejos, deben ensamblarse antes de lanzarse a producción. Un único servidor se ha convertido en muchos servidores que se ejecutan detrás de balanceadores de carga y el uso de sistemas de control de versiones se ha vuelto común.
Para mi proyecto personal, tenía una configuración especial. Y sabía que necesitaba la capacidad de implementar un sitio en producción, realizando solo una acción: escribir código en una rama. master en GitHub. También sabía que no quería administrar un enorme clúster de Kubernetes, ni utilizar la tecnología Docker Swarm, ni mantener un parque de servidores con pods, agentes y todo tipo de otras complejidades para ejecutar mi pequeña aplicación web. Para lograr el objetivo de facilitar al máximo el trabajo, necesitaba familiarizarme con CI/CD.
Si tiene un proyecto pequeño (en nuestro caso, un proyecto Node.js) y le gustaría aprender cómo automatizar la implementación de este proyecto, mientras se asegura de que lo que está almacenado en el repositorio coincida exactamente con lo que funciona en producción, creo Quizás te interese este artículo.
Prerrequisitos
Se espera que el lector de este artículo tenga conocimientos básicos de línea de comandos y scripting Bash. Además, necesitará cuentas. Travis CI и Centro acoplable.
Objetivos
No diré que este artículo pueda llamarse incondicionalmente una "guía de formación". Este es más bien un documento en el que hablo sobre lo que aprendí y describo el proceso que más me conviene para probar e implementar código en producción, realizado en una sola pasada automatizada.
Así es como terminó luciendo mi flujo de trabajo.
Para código enviado a cualquier rama del repositorio que no sea master, se realizan las siguientes acciones:
Comienza la construcción del proyecto en Travis CI.
Se realizan todas las pruebas unitarias, de integración y de extremo a extremo.
Sólo para el código que termina en master, se hace lo siguiente:
Todo lo anterior, más...
Creación de una imagen de Docker basada en el código, la configuración y el entorno actuales.
Alojar la imagen en Docker Hub.
Conexión al servidor de producción.
Subir una imagen desde Docker Hub al servidor.
Detenga el contenedor actual e inicie uno nuevo basado en la nueva imagen.
Si no sabes absolutamente nada sobre Docker, imágenes y contenedores, no te preocupes. Te lo contaré todo.
¿Qué es CI/CD?
La abreviatura CI/CD significa "integración continua/implementación continua" - "integración continua/implementación continua".
▍Integración continua
La integración continua es el proceso mediante el cual los desarrolladores se comprometen con el repositorio principal de código fuente de un proyecto (normalmente una rama master). Al mismo tiempo, la calidad del código se garantiza mediante pruebas automatizadas.
▍Implementación continua
La implementación continua es la implementación automatizada frecuente de código en producción. La segunda parte de la abreviatura CI / CD a veces se identifica como "entrega continua" ("entrega continua"). Esto es básicamente lo mismo que la "implementación continua", pero la "entrega continua" implica que los cambios deben confirmarse manualmente antes de iniciar el proceso de implementación del proyecto.
Primeros pasos
La aplicación en la que dominé todo esto se llama Tomar nota. Este es un proyecto web en el que estoy trabajando para tomar notas. Primero intenté hacer JAMstack-proyecto, o simplemente una aplicación front-end sin servidor, para aprovechar las opciones estándar de alojamiento e implementación de proyectos que ofrece Netlify. A medida que crecía la complejidad de la aplicación, necesitaba crear su back-end, lo que significaba que tendría que formular mi propia estrategia para la integración y la implementación automatizadas del proyecto.
En mi caso, la aplicación es un servidor Express que se ejecuta en un entorno Node.js, sirve una aplicación React de una sola página y admite una API segura del lado del servidor. Esta arquitectura sigue una estrategia que se puede encontrar en este Guía de autenticación de pila completa.
Consulté con otro, que es un experto en automatización, y le pregunté qué debo hacer para que todo funcione como quiero. Me dio una idea de cómo debería ser el flujo de trabajo automatizado descrito en la sección Objetivos de este artículo. Establecer objetivos como este significaba que necesitaba descubrir cómo usar Docker.
Docker
Docker es una herramienta que, gracias a la tecnología de contenedorización, facilita la distribución de aplicaciones, así como su implementación y ejecución en el mismo entorno, incluso si la propia plataforma Docker se ejecuta en entornos diferentes. Primero, necesitaba tener en mis manos las herramientas de línea de comandos (CLI) de Docker. Instrucción La guía de instalación de Docker no es muy clara, pero puedes aprender de ella que para dar el primer paso de la instalación, necesitas descargar Docker Desktop (para Mac o Windows).
Docker Hub es casi lo mismo que GitHub para repositorios git o registro npm para paquetes de JavaScript. Este es un repositorio en línea para imágenes de Docker. Aquí es donde se conecta Docker Desktop.
Entonces, para comenzar con Docker, debes hacer dos cosas:
Después de eso, puede verificar si la CLI de Docker está funcionando ejecutando el siguiente comando para verificar la versión de Docker:
docker -v
A continuación, inicie sesión en Docker Hub ingresando su nombre de usuario y contraseña cuando se le solicite:
docker login
Para utilizar Docker, debes comprender los conceptos de imágenes y contenedores.
▍Imágenes
Una imagen es una especie de plano que contiene instrucciones para construir un contenedor. Esta es una instantánea inmutable del sistema de archivos y la configuración de la aplicación. Los desarrolladores pueden compartir imágenes fácilmente.
# Вывод сведений обо всех образах
docker images
Este comando generará una tabla con el siguiente título:
REPOSITORY TAG IMAGE ID CREATED SIZE
---
A continuación, consideraremos algunos ejemplos de comandos en el mismo formato: primero hay un comando con un comentario y luego un ejemplo de lo que puede generar.
▍Contenedores
Un contenedor es un paquete ejecutable que contiene todo lo necesario para ejecutar una aplicación. Una aplicación con este enfoque siempre funcionará igual, independientemente de la infraestructura: en un entorno aislado y en el mismo entorno. Estamos hablando de que se lanzan instancias de la misma imagen en diferentes entornos.
# Перечисление всех контейнеров
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
---
▍Etiquetas
Una etiqueta es una indicación de una versión específica de una imagen.
▍Referencia rápida para los comandos de Docker
A continuación se ofrece una descripción general de algunos comandos de Docker utilizados habitualmente.
Sé cómo ejecutar una aplicación de producción localmente. Tengo una configuración de paquete web para crear una aplicación React terminada. A continuación, tengo un comando que inicia un servidor basado en Node.js en el puerto 5000. Se parece a esto:
npm i # установка зависимостей
npm run build # сборка React-приложения
npm run start # запуск Node-сервера
Cabe señalar que no tengo una aplicación de ejemplo para este material. Pero aquí, para experimentos, cualquier aplicación de Nodo simple servirá.
Para utilizar el contenedor, deberá darle instrucciones a Docker. Esto se hace a través de un archivo llamado Dockerfileubicado en el directorio raíz del proyecto. Este expediente, al principio, parece bastante incomprensible.
Pero lo que contiene sólo describe, en comandos especiales, algo así como configurar un entorno de trabajo. Éstos son algunos de esos comandos:
DESDE — Este comando inicia un archivo. Especifica la imagen base a partir de la cual se construye el contenedor.
COPIA - Copiar archivos de una fuente local a un contenedor.
DIR.TRABAJO - Configurar el directorio de trabajo para los siguientes comandos.
# Загрузить базовый образ
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
Dependiendo de la imagen base que elija, es posible que necesite instalar dependencias adicionales. El hecho es que algunas imágenes base (como Node Alpine Linux) están diseñadas para ser lo más compactas posible. Como resultado, es posible que no incluyan algunos de los programas que espera.
▍Construir, etiquetar y ejecutar un contenedor
El montaje local y la puesta en servicio del contenedor se realiza, una vez que hayamos Dockerfilelas tareas son bastante simples. Antes de enviar una imagen a Docker Hub, es necesario probarla localmente.
▍Montaje
Primero necesitas recolectar imagen, especificando un nombre y, opcionalmente, una etiqueta (si no se especifica ninguna etiqueta, el sistema asignará automáticamente una etiqueta a la imagen latest).
# Сборка образа
docker build -t <image>:<tag> .
Después de ejecutar este comando, puede ver cómo Docker construye la imagen.
Sending build context to Docker daemon 2.88MB
Step 1/9 : FROM node:12-alpine
---> ...выполнение этапов сборки...
Successfully built 123456789123
Successfully tagged <image>:<tag>
La construcción puede llevar un par de minutos; todo depende de cuántas dependencias tenga. Una vez completada la compilación, puede ejecutar el comando docker images y echa un vistazo a la descripción de tu nueva imagen.
REPOSITORY TAG IMAGE ID CREATED SIZE
<image> latest 123456789123 About a minute ago x.xxGB
▍Lanzamiento
La imagen ha sido creada. Y esto significa que, sobre esta base, puede ejecutar un contenedor. Como quiero poder acceder a la aplicación que se ejecuta en el contenedor en localhost:5000, i, en el lado izquierdo del par 5000:5000 en el siguiente conjunto de comandos 5000. En el lado derecho está el puerto de contenedores.
# Запуск с использованием локального порта 5000 и порта контейнера 5000
docker run -p 5000:5000 <image>:<tag>
Ahora que el contenedor está creado y ejecutándose, puede usar el comando docker ps para ver información sobre este contenedor (o puede usar el comando docker ps -a, que muestra información sobre todos los contenedores, no solo los que se están ejecutando).
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
Si ahora vas a localhost:5000 - puede ver la página de la aplicación en ejecución, que tiene exactamente el mismo aspecto que la página de la aplicación que se ejecuta en el entorno de producción.
▍Asignación y publicación de etiquetas
Para utilizar una de las imágenes creadas en el servidor de producción, debemos poder descargar esta imagen desde Docker Hub. Esto significa que primero debes crear un repositorio para el proyecto en Docker Hub. Posteriormente tendremos a nuestra disposición un lugar donde podrás enviar la imagen. Es necesario cambiar el nombre de la imagen para que su nombre comience con nuestro nombre de usuario de Docker Hub. Esto debe ir seguido del nombre del repositorio. Se puede colocar cualquier etiqueta al final del nombre. A continuación se muestra un ejemplo de cómo nombrar imágenes según este esquema.
Ahora puede crear la imagen con un nuevo nombre asignado y ejecutar el comando docker push para enviarlo al repositorio de 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
Si todo va bien, la imagen estará disponible en Docker Hub y podrá cargarse fácilmente en el servidor o compartirse con otros desarrolladores.
Próximos pasos
Hasta ahora, hemos verificado que la aplicación, en forma de contenedor Docker, se está ejecutando localmente. Hemos subido el contenedor a Docker Hub. Todo esto significa que ya hemos avanzado mucho hacia nuestro objetivo. Ahora necesitamos resolver dos preguntas más:
Configuración de una herramienta de CI para probar e implementar código.
Configurar el servidor de producción para que pueda descargar y ejecutar nuestro código.
En nuestro caso, como solución CI / CD, utilizamos Travis CI. Como servidor - Océano Digital.
Cabe señalar que aquí puedes utilizar otra combinación de servicios. Por ejemplo, en lugar de Travis CI, puedes usar CircleCI o Github Actions. Y en lugar de DigitalOcean, AWS o Linode.
Decidimos trabajar con Travis CI y ya tengo algo configurado en este servicio. Por lo tanto, ahora hablaré brevemente sobre cómo prepararlo para el trabajo.
Travis CI
Travis CI es una herramienta para probar e implementar código. No quiero entrar en detalles sobre la configuración de Travis CI, ya que cada proyecto es único y no será de mucha utilidad. Pero cubriré los conceptos básicos para que pueda comenzar si decide utilizar Travis CI. Elija lo que elija: Travis CI, CircleCI, Jenkins u otra cosa, se aplicarán métodos de configuración similares en todas partes.
Para comenzar con Travis CI, vaya a sitio del proyecto y crear una cuenta. Luego integre Travis CI con su cuenta de GitHub. Al configurar el sistema, deberá especificar el repositorio que desea automatizar y habilitar el acceso a él. (Uso GitHub, pero estoy seguro de que Travis CI puede integrarse con BitBucket, GitLab y otros servicios similares).
Cada vez que se inicia Travis CI, se inicia un servidor que ejecuta los comandos especificados en el archivo de configuración, incluida la implementación de las ramas apropiadas del repositorio.
▍Ciclo de vida del trabajo
Archivo de configuración de Travis CI llamado .travis.yml y almacenado en el directorio raíz del proyecto, admite el concepto de eventos ciclo vital tareas. Aquí están los eventos, enumerados en el orden en que ocurren:
apt addons
cache components
before_install
install
before_script
script
before_cache
after_success или after_failure
before_deploy
deploy
after_deploy
after_script
▍Pruebas
En el archivo de configuración, voy a configurar un servidor Travis CI local. Elegí el nodo 12 como idioma y le dije al sistema que instalara las dependencias necesarias para usar Docker.
Todo lo que figura en .travis.yml, se ejecutará en todas las solicitudes de extracción en todas las ramas del repositorio, a menos que se especifique lo contrario. Esta es una característica útil ya que significa que podemos probar todo el código que ingresa al repositorio. Esto le permite saber si el código está listo para escribirse en la sucursal. mastery si interrumpirá el proceso de construcción del proyecto. En esta configuración global, instalo todo localmente, ejecuto el servidor de desarrollo Webpack en segundo plano (esta es una característica de mi flujo de trabajo) y ejecuto las pruebas.
Si desea que su repositorio muestre íconos de cobertura de código, aquí Puede encontrar un tutorial rápido sobre cómo utilizar Jest, Travis CI y Coveralls para recopilar y mostrar esta información.
Así que aquí está el contenido del archivo. .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
Aquí es donde terminan las acciones que se realizan para todas las ramas del repositorio y para las solicitudes de extracción.
▍Implementación
Partiendo del supuesto de que todas las pruebas automatizadas se han completado correctamente, opcionalmente podemos implementar el código en el servidor de producción. Ya que sólo queremos hacer esto para el código de sucursal master, le damos al sistema las instrucciones apropiadas en la configuración de implementación. Antes de intentar utilizar el código que veremos a continuación en su proyecto, me gustaría advertirle que debe tener un script real llamado para su implementación.
deploy:
# Собрать Docker-контейнер и отправить его на Docker Hub
provider: script
script: bash deploy.sh
on:
branch: master
El script de implementación hace dos cosas:
Construir, etiquetar y enviar la imagen a Docker Hub usando una herramienta CI (en nuestro caso es Travis CI).
Cargando la imagen en el servidor, deteniendo el contenedor antiguo e iniciando uno nuevo (en nuestro caso, el servidor se ejecuta en la plataforma DigitalOcean).
Primero, debe configurar un proceso automático para crear, etiquetar y enviar la imagen a Docker Hub. Todo esto es muy similar a lo que ya hicimos manualmente, excepto que aquí necesitamos una estrategia para asignar etiquetas únicas a las imágenes y automatizar el inicio de sesión. Tuve dificultades con algunos detalles del script de implementación, como la estrategia de etiquetado, el inicio de sesión, la codificación de claves SSH y el establecimiento de una conexión SSH. Pero afortunadamente mi novio es muy bueno con bash, además de con muchas otras cosas. Él me ayudó a escribir este guión.
Entonces, la primera parte del script es enviar la imagen a Docker Hub. Hacer esto es bastante simple. El esquema de etiquetado que he usado implica combinar el hash de git y la etiqueta de git, si existe. Esto garantiza que la etiqueta sea única y facilita la identificación del conjunto en el que se basa. DOCKER_USERNAME и DOCKER_PASSWORD son variables de entorno definidas por el usuario que se pueden configurar mediante la interfaz Travis CI. Travis CI procesará automáticamente datos confidenciales para que no caigan en las manos equivocadas.
Aquí tenéis la primera parte del guión. 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}
Lo que será la segunda parte del script depende completamente del host que esté utilizando y de cómo esté organizada la conexión. En mi caso como uso Digital Ocean los comandos sirven para conectarme al servidor doctor. Cuando se trabaja con Aws, se utilizará la utilidad. awsetc.
Configurar el servidor no fue particularmente difícil. Entonces, configuré una gota basada en la imagen base. Cabe señalar que el sistema que elegí requiere una instalación manual de Docker por única vez y un inicio manual de Docker por única vez. Usé Ubuntu 18.04 para instalar Docker, así que si también estás usando Ubuntu puedes seguir esta orientación sencilla.
No me refiero aquí a comandos específicos para el servicio, ya que este aspecto puede variar mucho en diferentes casos. Solo daré un plan de acción general a realizar después de conectarnos vía SSH al servidor donde se implementará el proyecto:
Debe encontrar el contenedor que se está ejecutando actualmente y detenerlo.
Luego necesitas iniciar un nuevo contenedor en segundo plano.
Deberá configurar el puerto local del servidor en 80 - esto le permitirá ingresar al sitio en la dirección del formulario example.com, sin especificar un puerto, en lugar de utilizar una dirección como example.com:5000.
Y finalmente, debe eliminar todos los contenedores e imágenes antiguos.
Aquí tenéis la continuación del guión.
# Найти 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
Algunas cosas a tener en cuenta
Es posible que al conectarte al servidor vía SSH desde Travis CI, veas un aviso que no te permitirá continuar con la instalación, ya que el sistema esperará la respuesta del usuario.
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)?
Aprendí que una clave de cadena se puede codificar en base64 para almacenarla en una forma en la que se pueda trabajar con ella de manera conveniente y confiable. En la etapa de instalación, puede decodificar la clave pública y escribirla en un archivo. known_hosts para deshacerse del error anterior.
El mismo enfoque se puede utilizar con una clave privada al establecer una conexión, ya que es posible que necesite una clave privada para acceder al servidor. Cuando trabaje con una clave, solo necesita asegurarse de que esté almacenada de forma segura en una variable de entorno de Travis CI y que no se muestre en ninguna parte.
Otra cosa a tener en cuenta es que es posible que deba ejecutar todo el script de implementación como una sola línea, por ejemplo, con doctl. Esto puede requerir un esfuerzo adicional.
doctl compute ssh <droplet> --ssh-command "все команды будут здесь && здесь"
TLS/SSL y equilibrio de carga
Después de hacer todo lo anterior, el último problema que tuve fue que el servidor no tenía SSL. Como estoy usando un servidor Node.js, para forzar работать proxy inverso Nginx y Let's Encrypt, necesitas jugar mucho.
Realmente no quería hacer todas estas configuraciones de SSL manualmente, así que solo creé un balanceador de carga y registré información al respecto en DNS. En el caso de DigitalOcean, por ejemplo, crear un certificado autofirmado de renovación automática en el balanceador de carga es un procedimiento simple, gratuito y rápido. Este enfoque tiene el beneficio adicional de hacer que sea muy fácil configurar SSL en varios servidores que se ejecutan detrás de un balanceador de carga si es necesario. Esto permite que los propios servidores no "piensen" en absoluto en SSL, pero al mismo tiempo utilicen, como de costumbre, el puerto 80. Por lo tanto, configurar SSL en un balanceador de carga es mucho más fácil y conveniente que los métodos de configuración SSL alternativos.
Ahora puede cerrar todos los puertos del servidor que aceptan conexiones entrantes, excepto el puerto 80, utilizado para comunicarse con el equilibrador de carga y el puerto 22 para SSH. Como resultado, cualquier intento de contactar directamente con el servidor en cualquier puerto distinto de estos dos fallará.
resultados
Después de hacer todo lo que hablé en este artículo, ni la plataforma Docker ni el concepto de cadenas CI/CD automatizadas me asustaron más. Pude configurar una cadena de integración continua, durante la cual el código se prueba antes de entrar en producción y el código se implementa automáticamente en el servidor. Todo esto todavía es relativamente nuevo para mí y estoy seguro de que hay formas de mejorar mi flujo de trabajo automatizado y hacerlo más eficiente. Entonces, si tienes alguna idea sobre esto, dale me saber. Espero que este artículo te haya ayudado en tus esfuerzos. Quiero creer que al leerlo aprendiste tanto como yo aprendí mientras yo lidiaba con todo lo que en él contaba.
PS En nuestro mercado hay una imagen Docker, que se instala con un solo clic. Puedes comprobar el funcionamiento de los contenedores. VPS. Todos los nuevos clientes reciben 3 días de prueba de forma gratuita.
Estimados lectores! ¿Utilizas tecnologías CI/CD en tus proyectos?