Dans cet article, nous allons apprendre ce que signifie pousser une application en production et comment le faire automatiquement, nous verrons Docker et GitHub Actions pour cela. Puisque nous utilisons Docker, je ne vais pas passer trop de temps sur les technologies utilisées pour écrire l’application, mais plutôt sur ce qu’il faut faire avec l’image Docker elle-même. La prémisse signifie ici un simple serveur, qu’il s’agisse d’un simple VPS (Virtual Private Server), un serveur dédié ou tout serveur où vous avez SSH pour l’utiliser. Nous devons effectuer des actions, préparer le serveur pour recevoir et exécuter l’application, et configurer le pipeline de déploiement. Pour la partie à venir de cet article, je vais considérer que vous avez un serveur Ubuntu
Lisons préparer le serveur
La toute première chose à faire pour effectuer une configuration unique sur le serveur, l’objectif étant de le préparer pour les étapes suivantes. Pour ce faire, nous devons considérer les sujets suivants:
Docker Compose pour exécuter l’application et ses dépendances
Docker Compose est un outil qui exécute de nombreuses applications Docker en tant que services et leur permet de communiquer entre eux, ainsi que d’autres fonctionnalités comme les volumes pour le stockage de fichiers. Installons d’abord Docker et Docker Compose sur le serveur ; vous pouvez suivre le guide d’installation officiel ici : docs.docker.com/engine/install/ubuntu. Après cela, il est important de l’exécuter en tant qu’utilisateur non-root ; voici ce que dit la documentation officielle : docs.docker.com/engine/security/rootless.
Mettez en place les credentials pour AWS ECR & CLI.
Notez que vous pouvez atteindre le même objectif avec un autre registre si vous le souhaitez, j’utilise AWS ici car c’est plus simple pour moi.
Comme nous utilisons Docker, les images devront être stockées et récupérées depuis un endroit. Pour cela, j’utilise AWS ECR (Amazon Web Services Elastic Container Registry). C’est un registre Docker dans un compte AWS. C’est très économique à utiliser et facile à configurer. Vous pouvez également utiliser Docker Hub pour créer un dépôt privé pour vos images. Tout commence par la création d’un registre ECR privé dans le compte AWS. Vous cliquerez sur « Créer un dépôt » et remplirez le nom du dépôt.
Après avoir créé un référentiel, vous pouvez copier l’URI du référentiel et le conserver pour plus tard. Il a le format suivant AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/
REPOSITORY_NAME
.
Vous devrez également configurer les informations d’identification AWS IAM qui ont le droit de tirer/envoyer de/vers ce référentiel. Dirigeons-nous vers le service IAM, cliquons sur nouvel utilisateur et attachons-lui la politique suivante : AmazonEC2ContainerRegistryFullAccess
, vous n’avez pas besoin d’activer l’accès à la console AWS pour lui. A la fin de ce processus, vous recevrez 2 clés d’AWS, une secret key
et un secret key id
, gardez-les de côté nous en aurons besoin pour le travail à venir.
Revenir sur notre serveur, nous devons installer AWS CLI. La manière officielle d’installer est disponible ici. https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions. Après cela, vous pouvez tester l’installation en exécutant la commande aws --version
. À cette étape, vous devrez exécuter la commande aws configure
et répondre aux questions en fournissant les clés générées précédemment sur aws, la clé secrète
et l’identifiant de clé secrète
. Vous serez également invité à choisir un format de sortie, simplement JSON, et à fournir une région par défaut, il est préférable de choisir la région où vous avez créé le registre ECR plus tôt.
Mettre en place le script de lancement de l’application.
Dans mon flux de travail, j’ai écrit un petit script shell qui effectue certaines actions, c’est la partie clé de ce processus, il se connecte au registre, télécharge l’image et redémarre le service docker correspondant, je l’appelle simplement redeploy.sh
et je le sauvegarde dans un dossier d’où je veux exécuter mon application, voici le contenu:
#!/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"
La première étape de ce script consiste à se connecter au compte AWS avec AWS CLI pour obtenir un jeton que docker utilisera lors de la récupération de l’image docker, souvenez-vous que le registre est privé, nous ne pouvons pas tout simplement la télécharger sans être authentifié.
Ensuite, nous déclarons une liste de dépôts et les associons à un identifiant, l’identifiant spécifié sera utilisé comme argument de ligne de commande, nous en reparlerons plus tard. Après cela, nous vérifions si l’utilisateur a fourni un argument correspondant à un identifiant de service existant, nous voulons qu’il tape quelque chose comme ./redeploy web
par exemple, le script associera l’argument web
au dépôt web
comme dans la deuxième étape.
Après avoir l’identifiant de service, nous créons l’URL du dépôt dynamiquement et effectuons un docker pull
avec elle. Cela garantit que l’image Docker est téléchargée sur notre système.
Le script va maintenant se déplacer dans le dossier de l’application, /home/ubuntu/[APP_FOLDER]
, cela suppose que vous exécutez tout sous l’utilisateur ubuntu
et que son dossier HOME
est nommé ubuntu
, APP_FOLDER
contient tout le déploiement.
La prochaîne étape consiste à arrêter et redémarrer le service après quoi nous supprimons simplement les anciennes images inutilisées avec la commande docker image prune -fa
, vous pouvez en savoir plus ici : https://docs.docker.com/reference/cli/docker/system/prune/.
Le fichier Docker compose
Compose est l’utilitaire qui fait fonctionner tout notre système, il a besoin d’un fichier nommé docker-compose.yml
où vous allez définir tout, supposons que notre application a besoin d’un service redis
et postgres
pour fonctionner, voici à quoi cela ressemblera :
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
Votre volume ./opt/postgres/data:/var/lib/postgresql/data
va mapper le contenu du serveur Postgres vers le disque local afin qu’il ne soit pas perdu lorsque le conteneur Docker arrête de fonctionner. En savoir plus sur l’utilisation de Postgres avec docker-compose ici https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216. J’ai utilisé une directive nommée env_file
qui permet à docker-compose de lire un fichier et de charger son contenu dans le conteneur Docker lors de l’exécution, je l’ai fait parce que généralement, le fichier docker-compose est validé dans un VCS et je ne veux pas conserver les variables d’environnement directement via la directive environment
dans le service. Notez que notre service est nommé web
ici, plus tôt nous avons écrit un fichier redeploy.sh
et nous avons l’intention de l’exécuter comme ceci :
./redeploy.sh web
L’argument web
est lié au nom de notre service, ce fichier mapping l’argument à un nom de service dans le fichier Docker.
Configurer un service Linux pour maintenir tout en cours d’exécution
À cette étape, nous devons créer un service Linux qui s’assurera de démarrer l’application à chaque fois que le serveur démarre ou que notre application s’arrête. Le script suivant vous aidera à le faire :
[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
Analysons-le !!!
-
La section
Unit
décrit notre service et spécifie à quel service notre unité appartient, dans ce cas, c’est le service Docker, cela garantira que notre service s’exécute toujours lorsque le service Docker fonctionne également. -
La section
Service
décrit comment exécuter notre service, les parties intéressantes sontWorkingDirectory
,ExecStart
etExecStop
commandes, elles seront utilisées selon ce que signifie leur nom, par exemple, si le service est nommémyapp
lorsque vous tapez la commandesystemctl start myapp
, la commandeExecStart
sera exécutée. Vous pouvez en savoir plus sur les services Linux ici https://www.redhat.com/sysadmin/systemd-oneshot-service. En savoir plus sur comment exécuter le service docker avecsystemd
ici : https://bootvar.com/systemd-service-for-docker-compose/
Ce service doit être installé de manière à ce que le système le lance lorsque cela est nécessaire, vous devrez l’enregistrer dans un fichier avec un nom par exemple : myapp.service
touch myapp.service
# open it
nano myapp.service
# paste the previous scrip in it
cp myapp.service /etc/systemd/system/myapp.serivce
À ce stade, il est reconnu comme un service Linux, vous pouvez exécuter systemctl start myapp
pour le démarrer. La commande suivante requise est
systemctl enable myapp.service
Cela garantira que le service est exécuté automatiquement par le serveur à chaque redémarrage. Vous pouvez en savoir plus ici : https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6
Le serveur web
J’ai utilisé Nginx pour cette tâche, c’est petit et puissant, il est largement utilisé et peut agir comme un équilibreur de charge, un serveur de fichiers statiques, un proxy inverse, et bien plus encore. La première chose à faire est de l’installer.
sudo apt-get install nginx
À cette étape, l’image Docker est supposément en cours d’exécution, supposons qu’elle contient une application s’exécutant sur le port 8080
, et que ce port est lié au serveur via le fichier docker-compose. Nous devons configurer une configuration de proxy inverse entre Nginx et notre port. Voici la configuration nécessaire :
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;
}
}
nommons cette configuration myapp.conf
et enregistrons-la dans un répertoire où Nginx pourra la trouver, ce dossier, entre autres, s’appelle /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 müssen wir nur noch testen et redémarrer le service NGINX avec les commandes suivantes
sudo nginx -t # test if the config is valid
sudo nginx -s reload # reload the nginx service so it will consider it
Cette configuration va instructer Nginx d’écouter le trafic sur le port 80
et avec le nom de domaine [DOMAIN_NAME]
et de l’envoyer à votre serveur d’applications sur le port 8080
via la directive proxy_pass
, la ligne location / {
signifie simplement capturer toutes les demandes commençant par /
et effectuer les actions écrites sous le bloc location
. En savoir plus ici https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/How-to-setup-Nginx-reverse-proxy-servers-by-example.
La Pipeline de Construction
Après avoir configuré le serveur, nous devons maintenant mettre en place la pipeline de construction, elle se compose principalement d’une étape, écrire un fichier de pipeline Github Action et l’ajouter au projet, c’est parti.
Mise en place de GitHub Actions
GitHub action sera utilisé pour construire l’image Docker à partir de notre code source et l’envoyer au registre à partir duquel l’image est extraite et exécutée sur le serveur. Je prendrai un exemple de Dockerfile pour cet exemple, mais dans la pratique, vous devrez écrire votre propre Dockerfile. Pour une application express.js, le fichier Docker serait comme celui-ci :
# 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
La construction et l’exécution de ce fichier Docker démarrera notre application sur le port 8000, mais dans notre configuration, nous devrons l’exécuter avec docker-compose.
La prochaine chose à faire est de configurer la pipeline GitHub actions. Pour cela, créez simplement un dossier .github/workflows
à la racine du projet et créez un fichier nommé docker-build.yml
, nous écrirons notre pipeline dedans.
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
Il y a plusieurs étapes à examiner ici :
-
Configurer les crédentiels AWS
: ici, le système chargera la clé AWS que vous avez créée précédemment, vous devrez les enregistrer dans les secrets de votre compte GitHub -
Construire, taguer et pousser l'image vers Amazon ECR
: cette étape exécutera la commandedocker build
etdocker push
pour créer l’image Docker -
Redémarrer le service via SSH
cette étape se connectera au serveur et redémarrera l’application entière en une fois.
Cette pipeline s’exécutera chaque fois qu’il y a une requête de traction fusionnée contre la branche deploy/main
.
on:
push:
branches: ['deploy/main']
À ce stade, tout le système est en place et attaché, il est maintenant possible de le modifier et de l’appliquer à votre cas spécifique. Dans un article futur, je partagerai le processus de construction de l’application elle-même pour la production et son exécution dans un fichier Docker.
Conclusion
Cet article tente de décrire le processus que j’utilise pour configurer un VPS pour l’automatisation lors de la déploiement. Il décrit comment définir le processus d’exécution de l’application à l’intérieur du serveur et le processus de construction de l’application, chaque partie peut être effectuée avec un autre outil, par exemple, vous pouvez remplacer nginx par Treafik si vous le souhaitez, et vous pouvez remplacer le service systemd
par un programme dans supervisor
et plus. Ce processus ne couvre pas les éléments supplémentaires comme la sauvegarde du serveur ou la fermeture des ports par défaut sur les serveurs, ceux-ci seront expliqués dans des articles futurs. N’hésitez pas à poser une question si vous souhaitez l’adapter à votre flux. Dans un autre article, je me concentrerai sur comment configurer une application pour qu’elle soit prête pour la production en termes de déploiement, c’est la partie du processus qui précède la construction de l’Image Docker.
J’espère que vous avez apprécié la lecture.
J’suis là pour vous aider à mettre en œuvre cela au sein de votre entreprise ou équipe, vous permettant de vous concentrer sur vos tâches principales et d’économiser de l’argent avant de toucher au potentiel immense de Mastodon.