En este artículo, aprenderemos qué significa lanzar una aplicación en producción y cómo hacerlo automáticamente, veremos Docker y GitHub Actions para eso. Dado que estamos usando Docker, no dedicaré demasiado tiempo a las tecnologías utilizadas para escribir la aplicación, sino más bien a lo que hacer con la imagen de Docker herself. La premisa aquí significa un servidor simple, sea un simple VPS (Servidor Privado Virtual), un servidor dedicado, o cualquier servidor donde tenga SSH para usarlo. Debemos realizar acciones, preparar el servidor para recibir y ejecutar la app, y configurar el pipeline de despliegue. Para la parte venidera de este artículo, consideraré que tienes un servidor Ubuntu
Vamos a preparar el servidor
La primera cosa que hacer es algunas configuraciones únicas en el servidor, el objetivo es prepararlo para los pasos siguientes. Para este propósito, debemos considerar los siguientes temas:
Docker Compose para ejecutar la aplicación y sus dependencias
Docker Compose es una herramienta que ejecuta muchas aplicaciones de Docker como servicios y permite que se comuniquen entre sí, así como otras características como volúmenes para el almacenamiento de archivos. Primero instalaremos Docker y Docker Compose en el servidor; puedes seguir la guía de instalación oficial aquí: docs.docker.com/engine/install/ubuntu. Después de esto, es importante ejecutarlo como un usuario no root; aquí dice lo siguiente la documentación oficial: docs.docker.com/engine/security/rootless.
Configura las credenciales de AWS ECR & CLI.
Es importante notar que puedes lograr el mismo objetivo con otro registro si deseas, estoy usando AWS aquí porque es más sencillo para mí.
Dado que estamos usando Docker, las imágenes tendrán que ser almacenadas y recuperadas desde algún lugar. Para este propósito, estoy utilizando AWS ECR (Amazon Web Services Elastic Container Registry). Es un registro de Docker dentro de una cuenta de AWS. Es muy económico de usar y fácil de configurar. También puedes usar Docker Hub para crear un repositorio privado para tus imágenes. Todo comienza creando un registro ECR privado en la cuenta de AWS. Hacerás clic en “Crear repositorio” y llenar el nombre del repositorio.
Después de crear un repositorio puedes copiar el URI del repositorio y guardarlo para más tarde. Tiene el siguiente formato AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/
NOMBRE_REPOSITORIO
.
También necesitarás configurar credenciales de IAM de AWS que tengan el derecho de extraer/subir a/from este repositorio. Vamos al servicio IAM, haz clic en nuevo usuario y adjunta la siguiente política a él: AmazonEC2ContainerRegistryFullAccess
, no necesitas habilitar el acceso a la consola de AWS para él. Al final de este proceso, recibes 2 claves de AWS, una clave secreta
y un identificador de clave secreta
, guárdalas aparte, las necesitaremos para el trabajo venidero.
De vuelta en nuestro servidor, necesitamos instalar AWS CLI. La forma oficial de instalarlo está disponible aquí. https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions. Después de esto, puedes probar la instalación ejecutando el comando aws --version
. En este paso, tendrás que ejecutar el comando aws configure
y responder a las preguntas proporcionando las claves generadas previamente en aws, la clave secreta
y el id de la clave secreta
. También se te pedirá que elijas un formato de salida, simplemente JSON, y proporciones una región predeterminada, es mejor elegir la región donde creaste el registro ECR anteriormente.
Configura el script del ejecutor de la aplicación.
En mi flujo de trabajo, he escrito un pequeño script de shell que realiza ciertas acciones, es la parte clave de este proceso, se conecta al registro, descarga la imagen y reinicia el servicio docker correspondiente, simplemente lo llamo redeploy.sh
y lo guardo en una carpeta desde donde quiero ejecutar mi aplicación, aquí está el contenido:
#!/bin/bash
# Retrieve AWS ECR login command
aws ecr get-login-password --region [SWS_REGION] | docker login --username AWS --password-stdin [AWS_REGION].dkr.ecr.us-west-2.amazonaws.com
# Associating repositories with identifiers
declare -A repositories=(
["web"]="[REGISTRY_NAME]:latest"
)
# Check if service identifier is provided as a command line argument
if [ -z "$1" ]; then
echo "Please provide a service identifier as a command line argument."
exit 1
fi
service_identifier=$1
# Check if the provided service identifier exists in the repositories array
if [ -z "${repositories[$service_identifier]}" ]; then
echo "Invalid service identifier. Available identifiers: ${!repositories[@]}"
exit 1
fi
# pull the new image from the registry
repository=${repositories[$service_identifier]}
echo "Pulling [AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/$repository"
docker pull "[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/$repository"
# Change directory to [APP_FOLDER]
cd /home/ubuntu/[APP_FOLDER] || {
echo "Failed to change directory to /home/ubuntu/[APP_FOLDER]"
exit 1
}
# stop and restart the service, this wil force docker compose to redownload the lates image
echo "Re-running service $service_identifier"
docker compose stop "$service_identifier"
docker compose up --no-deps "$service_identifier" -d
# Remove old and un-used docker images
echo "Removing unused Docker images"
docker image prune -fa
echo "Removed Dangling Images"
El primer paso de este script consiste en iniciar sesión en la cuenta de AWS con AWS CLI para obtener un token que docker utilizará al recuperar la imagen docker, recuerda que el registro es privado, no podemos simplemente extraerla sin estar autenticados.
Luego declararemos una lista de repositorios y los asociaremos con algún identificador, el identificador especificado se utilizará como argumento de línea de comandos, más sobre esto después. Después de esto, verificamos si el usuario ha proporcionado un argumento que corresponda a un identificador de servicio existente, queremos que escriba algo como ./redeploy web
por ejemplo, el script asociará el argumento web
al repositorio web
como en el segundo paso.
Después de tener el identificador de servicio creamos la URL del repositorio dinámicamente y realizamos un docker pull con ella. Esto asegura que la imagen de docker se descargue en nuestro sistema.
El script ahora cambiará al directorio de la aplicación, /home/ubuntu/[APP_FOLDER]
esto asume que está ejecutando todo bajo el usuario ubuntu
y que su carpeta HOME
se llama ubuntu
, APP_FOLDER
contiene toda la configuración.
El siguiente paso consiste en detener y reiniciar el servicio después del cual simplemente eliminamos las imágenes antiguas y no utilizadas con el comando docker image prune -fa
puedes aprender más aquí: https://docs.docker.com/reference/cli/docker/system/prune/.
El archivo Docker compose
Compose es la utilidad que ejecuta todo nuestro sistema, necesita un archivo llamado docker-compose.yml
donde definirás todo, supongamos que nuestra aplicación necesita un servicio de redis
y postgres
para ejecutarse, así es cómo se verá:
version: '3.9'
services:
web:
image: "[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/myapp:latest"
ports:
- 8080:8080
depends_on:
- redis
- db
env_file:
- .env
redis:
image: 'redis:alpine'
ports:
- '6379:6379'
db:
image: 'postgres:14'
restart: always
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
PGDATA: /var/lib/postgresql/data/pgdata
healthcheck:
test: [ "CMD", "pg_isready", "-q", "-d", "postgres", "-U", "postgres" ]
timeout: 45s
interval: 10s
retries: 10
ports:
- '5437:5432'
volumes:
- ./opt/postgres/data:/var/lib/postgresql/data
Tu volumen ./opt/postgres/data:/var/lib/postgresql/data
mapeará el contenido del servidor Postgres al disco local para que no se pierda cuando el contenedor docker deje de ejecutarse. Aprende más sobre cómo ejecutar Postgres con docker-compose aquí https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216. He utilizado una directiva llamada env_file
que permite a docker-compose leer un archivo y cargar su contenido en el contenedor docker en tiempo de ejecución, lo hice porque usualmente, el archivo docker-compose se compromete a un VCS, por lo que no quiero mantener la variable de entorno directamente a través de la directiva environment
en el servicio. Nota que nuestro servicio se llama web
aquí, anteriormente escribimos un archivo redeploy.sh
y pretendemos ejecutarlo así:
./redeploy.sh web
El argumento web
está vinculado al nombre de nuestro servicio, ese archivo simplemente mapea el argumento a un nombre de servicio en el archivo docker.
Configura un servicio Linux para mantener todo en ejecución
A este paso, debemos crear un servicio Linux que asegure que la aplicación se inicie cada vez que el servidor se inicie o nuestra aplicación se detenga. El siguiente script te ayudará a hacerlo:
[Unit]
Description=[APP_NAME] service executed by docker compose
PartOf=docker.service
After=docker.service
After=network.target
[Service]
Type=oneshot
RemainAfterExit=true
WorkingDirectory=/home/ubuntu/[APP_FOLDER]
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
[Install]
WantedBy=multi-user.target
Vamos a analizarlo !!!
-
La sección
Unit
describe nuestro servicio y especifica qué servicio es parte de nuestra unidad, en este caso, es el servicio docker, esto asegurará que nuestro servicio siempre se ejecute cuando el servicio docker también esté en ejecución. -
La sección
Service
describe cómo ejecutar nuestro servicio, las partes interesantes sonWorkingDirectory
,ExecStart
y el comandoExecStop
, se utilizarán según lo que indique su nombre, por ejemplo, si el servicio se llamamyapp
cuando escribas el comandosystemctl start myapp
se ejecutará el comandoExecStart
. Puedes aprender más sobre los servicios de Linux aquí https://www.redhat.com/sysadmin/systemd-oneshot-service. Aprende más sobre cómo ejecutar el servicio docker consystemd
aquí: https://bootvar.com/systemd-service-for-docker-compose/
Este servicio debe ser instalado de tal manera que el sistema lo ejecute cuando sea necesario, tendrás que guardarlo en un archivo con un nombre, por ejemplo: myapp.service
touch myapp.service
# open it
nano myapp.service
# paste the previous scrip in it
cp myapp.service /etc/systemd/system/myapp.serivce
A este punto es reconocido como un servicio de Linux, puedes ejecutar systemctl start myapp
para iniciarlo. El siguiente comando requerido es
systemctl enable myapp.service
Esto asegurará que el servicio sea ejecutado automáticamente por el servidor en cada reinicio. Puedes aprender más aquí: https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6
El servidor web
Usé Nginx para esta tarea, es pequeño y potente, es ampliamente utilizado y puede actuar como balanceador de carga, servidor de archivos estáticos, proxy inverso y mucho más. Lo primero que hay que hacer es instalarlo.
sudo apt-get install nginx
En esta etapa, la imagen de docker supuestamente se está ejecutando, supongamos que contiene una aplicación que se ejecuta en el puerto 8080
, y que ese puerto está enlazado al servidor a través del archivo docker-compose. Necesitamos configurar una configuración de proxy inverso entre Nginx y nuestro puerto. Aquí está la configuración necesaria:
upstream app_backend {
server localhost:8080; # the appliction port
}
server {
listen 80;
server_name [DOMAIN_NAME];
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
}
}
Llamemos a esta configuración myapp.conf
y guárdelo en un directorio donde nginx lo encuentre, esa carpeta entre otras se llama /etc/nginx/conf.d/
.
sudo touch /etc/nginx/conf.d/myapp.conf
sudo nano /etc/nginx/conf.d/myapp.conf
# paste the content there
Jetzt tutto lo que necesitamos es probarlo y reiniciar el servicio NGINX con los siguientes comandos
sudo nginx -t # test if the config is valid
sudo nginx -s reload # reload the nginx service so it will consider it
Esta configuración instruirá a nginx para que escuche el tráfico en el puerto 80
y con el nombre de dominio [DOMAIN_NAME]
y lo envíe a su servidor de aplicaciones en el puerto 8080
a través de la directiva proxy_pass
, la línea location / {
simplemente significa capturar todas las solicitudes que comiencen con /
y realizar las acciones escritas bajo el bloque location
. Más información aquí https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/How-to-setup-Nginx-reverse-proxy-servers-by-example.
La Pipeline de Construcción
Después de configurar el servidor, ahora tenemos que establecer la pipeline de construcción, que principalmente consta de 1 paso, escribir un archivo de pipeline de Github Action y agregarlo al proyecto, vamos a empezar.
Configuración de GitHub Actions
GitHub action se utilizará para construir la imagen de Docker desde nuestro código fuente y enviarla al registro desde donde se extrae y ejecuta la imagen en el servidor. Tomaré un ejemplo de Dockerfile para este ejemplo, pero en la práctica, tendrás que escribir tu propio Dockerfile. Para una aplicación de express.js, el archivo Docker sería así:
# Fetching the minified node image on apline linux
FROM node:slim
# Declaring env
ENV NODE_ENV production
# Setting up the work directory
WORKDIR /express-docker
# Copying all the files in our project
COPY . .
# Installing dependencies
RUN npm install
# Installing pm2 globally
RUN npm install pm2 -g
# Exposing server port
EXPOSE 8080
# Starting our application
CMD pm2 start process.yml && tail -f /dev/null
Construir y ejecutar este archivo Docker iniciará nuestra aplicación en el puerto 8000, pero en nuestra configuración, tendremos que ejecutarlo con docker-compose.
Lo siguiente es configurar la pipeline de GitHub actions. Para eso, simplemente crea una carpeta .github/workflows
en la raíz del proyecto y crea un archivo llamado docker-build.yml
, escribiremos nuestra pipeline en él.
name: Build, Push to ECS and Deploy to Server
on:
push:
branches: ['deploy/main']
jobs:
build:
name: Build Web Image
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: [AWS_REGION]
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to Amazon ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: [REPOSITORY_NAME]
IMAGE_TAG: latest
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
- name: Restart the service via SSH
uses: appleboy/[email protected]
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
script: /home/ubntu/[APP_DIRECTORY]/redeploy.sh web
Hay varios pasos en los que hay que mirar aquí:
-
Configurar credenciales de AWS
: aquí el sistema cargará la clave de aws que creaste anteriormente, tendrás que registrarlas en los secretos de tu cuenta de GitHub -
Construir, etiquetar y enviar la imagen a Amazon ECR
: este paso ejecutará el comandodocker build
ydocker push
para crear la imagen de Docker -
Reiniciar el servicio mediante SSH
esta etapa se conectará al servidor y reiniciará toda la aplicación de una vez.
Esta pipeline se ejecutará cada vez que se fusione una solicitud de extracción contra la rama deploy/main
.
on:
push:
branches: ['deploy/main']
A este punto, todo el sistema está en su lugar y atado, ahora es posible editar y aplicarlo a su caso específico. En un artículo futuro, compartiré el proceso de construcción de la aplicación en sí para producción y ejecución en un archivo docker.
Conclusión
Este artículo intenta describir el proceso que uso para configurar un VPS para automatizar la implementación. Describe cómo establecer el proceso de ejecución de la aplicación dentro del servidor y el proceso de construcción de la aplicación, cada parte puede hacerse con otra herramienta, por ejemplo, puedes cambiar nginx por Treafik si quieres, y puedes reemplazar el servicio systemd
con un programa en supervisor
y más. Este proceso no cubre cosas adicionales como respaldar el servidor o cerrar los puertos predeterminados en los servidores, esas se explicarán en artículos futuros. No dudes en hacer una pregunta si deseas adaptar esto a tu flujo. En otro artículo me centraré en cómo configurar una aplicación para que esté lista para producción en términos de implementación, esa es la parte del proceso que precede la construcción de la Imagen Docker.
Espero que disfrutara de la lectura.
Estoy aquí para ayudarlo a implementar esto dentro de su empresa o equipo, permitiéndole keskarse en sus tareas principales y ahorrar dinero antes de aprovechar el vasto potencial de Mastodon.