Up.oN
#28/03/20225

Workflow Ci/Cd

TO DO: vitejs, git et les hook, template, le processe cote prod, SSL

L'objectif de cette feuille de route est de créer un projet web via Django et de permettre un déploiement des mises à jours automatisé via les hooks de git.
J'en profite également pour détailler l'intégration de BootstrapCSS via ViteJS.

Cela me permettra par la suite, de me servir de cette page comme référence afin d'implémenter des scripts afin d'automatiser ces différentes étapes.

Ce qui j'entend par Workflow Ci/Cd pour un projet web, c'est de pouvoir facilement build des assets et mettre à jour le contenu du projet via une simple commande.

Requirement: avoir un serveur web (NGinx), une connexion ssh, un nom de domaine (sujets non traités ici).

Starter

Sur le poste de DEV, on va créer un répertoire de travail, ajouter git, l'environnement virtuel, Django et créer notre page "Hello World".

Creation et activation de l'environnement virtuel:

py -m venv venv

#linux 
source venv/bin/activate

# Windows 
./venv/scripts/activate

On install django, on créer les fichiers de bases et la db (sqlite):

pip install Django

# créer le projet
django-admin startproject nom_du_projet

cd ./nom_du_projet

# créer le repertoire des fichiers static
mkdir static

# DB
                
py manage.py makemigrations
py manage.py migrate

On va régler Django pour fonctionner par défaut en production et ajouter la configuration de développement dans un fichier séparé. On utilisera une variable d'environnement "activer" le "mode" DEV.

# éditer settings.py
# vérifier/ajouter les éléments suivants
import os

DEBUG = False

ALLOWED_HOSTS = ['domain.tld','localhost']

LANGUAGE_CODE = 'fr-fr'

TIME_ZONE = 'Europe/Paris'

USE_I18N = True

USE_TZ = True

USE_I18N = True  # Active la traduction des textes
USE_L10N = True  # Active la localisation des formats (dates, nombres)
USE_THOUSAND_SEPARATOR = True  # Active le séparateur des milliers

STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / 'static']

# adapter suivant l'architecture de production
STATIC_ROOT = os.path.join('/var/www/nom_du_projet/public')

### My spec here

# DEV MOD
# win : set DJANGO_DEVELOPMENT=true
#     : $env:DJANGO_DEVELOPMENT="true"
# Linux : export DJANGO_DEVELOPMENT=true
# inspired by: https://stackoverflow.com/a/34891731/21281469

if os.getenv('DJANGO_DEVELOPMENT') == 'true':
    from .dev_settings import *
else:
    pass

On créer le fichier pour les settings en mode DEV: touch dev_settings.py

# dev_settings.py
DEBUG = True

ALLOWED_HOSTS = ['*']

Enfin on vérifie si le serveur DEV fonctionne:

# on active 'le mode' DEV - ici sous Windows
$env:DJANGO_DEVELOPMENT="true"

On lance le serveur: http://127.0.0.1:8000
py manage.py runserver
Si tout s'est bien déroulé la page affiche "L’installation s’est déroulée avec succès. Félicitations"

On ajoute une app Django: pages:

py manage.py startapp pages

On l'ajoute dans settings.py:

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'pages',
]

On créer notre vue dans /nom_du_projet/pages/templates/pages/index.html

# création des répertoires
mkdir -p ./pages/templates/pages

# création de la vue
touch ./pages/templates/pages/index.html

On complète notre vue:

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Test - Hello World</title>
</head>
<body>
    <p>Hello World</p>
</body>
</html>

Il maintenant implémenter le Controller et configurer l'url au niveau du routing.
On va éditer le fichier /nom_du_projet/pages/views.py

# editer le fichier /nom_du_projet/pages/views.py

def index(request):
    return render(request,'pages/index.html')
On définit également notre route "principale" au niveau du routing:
# editer le fichier /nom_du_projet/nom_du_projet/urls.py
from pages import views

  ...

urlpatterns = [
  path('', views.index),
]
Le site doit maintenant afficher "Hello World".

On va maintenant créer notre repo de Dev local à ce niveau /répertoire_de_travail/nom_du_projet, un peut coup de: git init

On en profite pour ajouter le fichier .gitignore

# création du fichier .gitignore

touch .gitignore
Dans lequel on ajoute différentes répertoires:
# python env
venv/
__pycache__/

# django dev files
dev-requirements.txt
upon/dev_settings.py

# vitejs
static/.vite
static/vitejs/*

Configuration du serveur

Avant d'aller plus loin dans la mise en place du projet Web. On va, configurer la partie Serveur.

Via Git et les hooks, on va déployer l'application. Puis on mettra en place les processus pour la Prod. Enfin, on configura Nginx pour servir les fichiers "static".

...
Déploiement via Git et son hook: post-receive

Création du repo pour la Prod

Une fois connecté en ssh au serveur, choisissez un répertoire où créer vos repos (ex: /var/repo), puis initialiser un repo pour notre site: git init --bare

# se rendre dans le repertoire des repos puis...

sudo mkdir nom_du_projet 
sudo chown user_name:www-data nom_du_projet 

cd nom_du_projet
git init --bare

La branche par défaut doit être main et non master, penser à configurer git si besoin: git: git config --global init.defaultBranch main

On va ensuite créer un hook "post-receive" (adapter les chemins suivant vos besoins):

# nano ./hooks/post-receive

#!/bin/bash
while read oldrev newrev ref
do
if [[ $ref =~ .*/main$ ]];
then
echo "Master ref received.  Deploying master branch to production..."
git --work-tree=/var/www/nom_du_projet --git-dir=/var/repo/nom_du_projet checkout -f
            
# Collect static files
#sudo /var/www/nom_du_projet/venv/bin/python /var/www/nom_du_projet/manage.py collectstatic --noinput
            
# Restart Gunicorn
#sudo systemctl restart nom_du_projet_gunicorn
            
else
echo "Ref $ref successfully received.  Doing nothing: only the master branch may be deployed on this server."
fi
done

On rend le script executable: sudo chmod +x ./hooks/post-receive

Remarque: les commandes sudo sont commentées pour le moment (normal).

On créer ensuite le répertoire qui va "accueillir" notre application, adapter les chemins suivant vos besoins:

# creation du répertoire
mkdir /var/www/nom_du_projet

chown username:www-data /var/www/nom_du_projet

Sur le repo de Dev ajout le remote

Si ce n'est pas déjà fait on créer notre fichier "requirements.txt" (/repertoire_de_travail/nom_du_projet).
Votre environnement virtuel doit-être activé: pip freeze > requirements.txt

Afin de déployer une premier fois l'application, on va ajouter notre repo Prod au remote et pusher:

# sur le repo Dev local
git remote add origin "ssh://username@domain.tld:port/path"

# pour éditer en cas d'erreur
git remote set-url origin "..."

# et on push 
git status
git add  . 
git commit -m 'initial commit'

git push prod main

Process et Socket (Gunicorn)

...
Serveur Web WSGI via Gunicorn, Nginx en proxy reverse et systemD pour gérer les process

Dans un premier temps on va déployer Django via le serveur de Test afin de vérifier son bon fonctionnement.

On va créer l'environnement virtuel et installer les dépendances du projet:

# au niveau du repertoire du projet ex: /var/www/nom_du_projet
# au besoin créer le répertoire 'static'
mkdir static

# création de l'environnement virtuel
python3 -m venv venv

# activation 
source venv/bin/activate

# installation des dépendances du projets
pip install -r requirements.txt

# installation de Gunicorn
pip install gunicorn

# démarre le serveur test 
python3 manage.py runserver 0.0.0.0:8000

Tester si cela fonctionne dans le navigateur (adapter suivant la situation): http://domain.tld ou http://sub.domain.tld:8000

On s'occuppe des fichiers "static" puis on test de nouveau via Gunicorn:

# traite les fichiers statics 

python3 manage.py collectstatic --noinput

# on lance gunicorn
gunicorn --bind 0.0.0.0:8000 myproject.wsgi

Tester si cela fonctionne dans le navigateur (adapter suivant la situation): http://domain.tld ou http://sub.domain.tld:8000

On désactive l'environnement virtuel, nous allons créer le processus:

# désactivation de l'environnement virtuel
deactivate

Création du socket:

# sudo nano /etc/systemd/system/project_name_gunicorn.socket

[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/run/project_name_gunicorn.sock

[Install]
WantedBy=sockets.target

On créer ensuite le service:

# sudo nano /etc/systemd/system/project_name_gunicorn.service

[Unit]
Description=gunicorn daemon for project: project_name
Requires=project_name_gunicorn.socket
After=network.target
            
[Service]
User=username
Group=www-data
WorkingDirectory=/path/to/your/project
ExecStart=/path/to/your/project/venv/bin/gunicorn \
          --access-logfile - \
          --workers 3 \
          --bind unix:/run/project_name_gunicorn.sock \
          project_name.wsgi:application
            
[Install]
WantedBy=multi-user.target

On active ensuite le socket:

#activation du socket
sudo systemctl start project_name_gunicorn.socket
sudo systemctl enable project_name_gunicorn.socket
>> Created symlink /etc/systemd/system/sockets.target.wants/project_name_gunicorn.socket → /etc/systemd/system/project_name_gunicorn.socket.

# vérifie le socket 
sudo systemctl status project_name_gunicorn.socket
>>      Loaded: loaded (/etc/systemd/system/project_name_gunicorn.socket; enabled; preset: enabled)
        Active: active (listening) since Thu 2025-03-27 07:30:05 UTC; 2min 26s ago

# vérifie le sock file 
file /run/project_name_gunicorn.sock
>> /run/project_name_gunicorn.sock: socket

Configuration Nginx

On va maintenant configurer Nginx en tant que reverse proxy et pour servir les fichiers static:

# sudo nano /etc/nginx/sites-available/nom_du_projet

server {
  listen 80;
  server_name domain.tld;
          
  location = /favicon.ico { access_log off; log_not_found off; }
          
  location /static/ {
      alias /var/www/project_name/public/;
  }
          
  location / {
      include proxy_params;
      proxy_pass http://unix:/run/project_name_gunicorn.sock;
  }
}             

On créer le lien symbolique, on test er redémarre Nginx:

# création du lien symbolique 
sudo ln -s /etc/nginx/sites-available/domain.tld /etc/nginx/sites-enabled

# on test la configuration Nginx 
sudo nginx -t
>> nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
   nginx: configuration file /etc/nginx/nginx.conf test is successful

# on redémarre le service
sudo systemctl restart nginx

Tester si cela fonctionne dans le navigateur: http://domain.tld

On va maintenant installer un certificat SSL via certbot de Let's Encrypt afin d'activer https.
On passe par Snaps (snapcraft.io - installing-snapd):

sudo apt install snapd
            
# pour tester que c'est OK
sudo snap install hello-world

On install certbot et on génère les certifs. :

# retire d'éventuelle ancienne installations
sudo apt-get remove certbot

# install certbot
sudo snap install --classic certbot

# on prépare Certbot afin qu'il puisse executer des commandes
sudo ln -s /snap/bin/certbot /usr/bin/certbot

# on demande à certbot de générer un certif. automatiquement
sudo certbot --nginx
> Successfully received certificate. ...
   Congratulations! You have successfully enabled HTTPS>

# ou on souhaite le faire manuellement, dans ce cas on demande juste le certificat
sudo certbot certonly --nginx

# enfin on test la procédure de renouvellement des certifs
sudo certbot renew --dry-run

Le site doit maintenant être accéssible en https.

Si ce n'est déjà fait décommenter les lignes sudo dans le hook: nano ./hooks/post-receive

On peut tester le workflow: réaliser un commit et en pushant.

Il ne reste plus qu'a intégrer BootstrapCSS via viteJS et inclure notre premier template de "base".

Gabarit et ViteJS

On va intégrer ViteJS au projet afin de personaliser BootstrapCSS. On implémentera un helper qui va aller lire le fichier manifest.json afin d'intégrer les derniers assets buildés.

Je reprends ici, ce que je fais avec php et ViteJS depuis des années: vitejs.fr - backend-integration.

...
Struture des répertoires du projets

C'est un choix personel, j'ai pour habitude de créer les ressources nécessaires au front-end dans un répertoire nommé rsc où l'on y trouve BootstrapCSS "personalisé".

ViteJS est configuré pour délivrer les assets dans le répertoire /project_name/static/builder

Ensuite le helper vite_utils va lire le manifest afin que le Controller puisse transmettre les chemins des assets à la vue.

Helpers: vite_utils.py

Dans un premier temps on va intégrer le package helper. Pour le moment, je me contente de copier/coller. J'imagine par la suite surement un autre moyen plus automatique.

# /répertoire de travail/project_name/helpers/vite_utils.py

import json
from pathlib import Path
from django.conf import settings

def get_vite_assets():
    static_dir = settings.STATICFILES_DIRS[0]
    manifest_path = Path(static_dir) / 'builder/.vite/manifest.json'
    with open(manifest_path, 'r') as file:
        manifest = json.load(file)

    asset_paths = {}
    for key, value in manifest.items():
        if value.get('isEntry'):
            asset_paths['js'] = value.get('file')
            asset_paths['css'] = value.get('css', [])

    return asset_paths

On oublie pas d'ajouter le fichier: __init__.py

Au niveau du controller, on ajoutera les variables au context:

# views.py
# exemple d'implémentation
from django.shortcuts import render
from helpers.vite_utils import get_vite_assets

LIVE_MODE=False

def index(request):

  asset_paths = get_vite_assets()

  context={
      'live_mode': LIVE_MODE,
      'css_styles': 'builder/' + asset_paths['css'][0],
      'main_js': 'builder/' + asset_paths['js'],
      'current_page': 'index',
      'title': 'Project Name',
  }        
  return render(request,'pages/index.html', context)

Le template

On va donc créer un répertoire /repertoire_de_travail/project_name/templates. Dans lequel, on va mettre nos fichiers composant le template: hreader.html, base.html, [menu.html], [header.html].
Penser à régler dans settings.py, le chemin pour ce template:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

ibre d'adapter suivant le projet.

# header.html
            
{% load static %}
            
<!DOCTYPE html>
<html lang="fr">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            
        {% if live_mode %}
            <script type="module" src="http://localhost:5173/@vite/client"></script>
            <script type="module" src="http://localhost:5173/rsc/tpl_1/main.js"></script>
        {% else %}
            <link rel="stylesheet" href="{% static css_styles %}" />
            <script type="module" src="{% static main_js %}"></script>
        {% endif %}
            
        <title>{% if title %} {{title}} {% else %} Up.oN {% endif %}</title>
    </head>
<body>
# base.html
          
        {% include "header.html" %}
          
        {% include "menu.html" %}
          
        {% block content %}
        {% endblock %}
  </body>
</html>

Au niveau de la vue:

{% extends 'base.html' %}

  {% block content %}
    
  {% endblock %}            

Installation, Configuration: ViteJS

Voici mon organisation. Vous devais avoir NodeJs sur votre machine de Dev. Aussi, je vous invite les docs de Bootstrap, ViteJS et éventuellement NodeJS pour adapter la configuration à vos bessoins.

Je créer le repertoire: /repertoire_de_travail/vitejs:

# création du répertoire 
mkdir vitejs 

cd vitejs

# on initialise un projet NodeJs
npm init -y

# installation de vitejs, Bootstrap, Bootstrap Icons et Sass 
npm i -D vite && npm i --save bootstrap @popperjs/core && npm i bootstrap-icons && npm i --save-dev sass

Pour le confort et par habitude, j'ajoute dans le fichier package.json les commandes dev et build:

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",              
  "dev": "vite",
  "build": "vite build"
},

Ainsi pour dev le front-end, lancer le server live: npm run dev et pour builder les assets: npm run build.

Ensuite, j'ajoute mon fichier de configuration vite.config.mjs

# création du fichier
touch vite.config.mjs

# contenu du fichier
import { defineConfig } from "vite"
import path from "path"

export default defineConfig({
    base: './',
    server:{
        port: '5173',
        origin: 'http://localhost:5173',
    },
    build: {
      copyPublicDir:false,
      outDir: '../project_name/static/builder',
      //assetsDir:'assets',
      emptyOutDir: true,
      manifest: true,
      rollupOptions: {
        input: 'rsc/tpl_1/main.js',
      },
    },
  }) 

Ensuite, je place mes templates dans le repertoire rsc et j'adapte en conséquence. Par exemple, ici pour le template tpl_1, je vais créer un fichier /vitejs/rsc/tpl_1/main.js

# /vitejs/rsc/tpl_1/main.js

// pour le fonts => https://fontsource.org/
import '@fontsource/nom_de_la_font';

// Import configured BS's CSS 
import './bs_config.scss'

// Import all of Bootstrap's JS
import * as bootstrap from 'bootstrap'

//import './bs_color_mode_toggler'

import "./main.css"

Personnalisation de BootstrapCSS

Pour personaliser bootstrap, cela passe par un fichier "principale", qu'on adapte suivant les besoins. Ici, pleins de cas sont possible. Je mets un exemple bs_config.scss, juste pour la forme.

// Custom.scss
// Option A: Include all of Bootstrap
            
// Include any default variable overrides here (though functions won't be available)
            
//@import "bootstrap/scss/bootstrap";
            
// Option B: Include parts of Bootstrap
            
// 1. Include functions first (so you can manipulate colors, SVGs, calc, etc)
// Customize some defaults
$primary: #3572A0;  
$secondary: #6c8599;  
            
// Configuration
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
            
            
            
@import "bs_custom_variables";
            
@import "bootstrap/scss/variables-dark";
            
@import "bs_custom_colors";
            
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
            
@import "bs_custom_it";
            
@import "bootstrap/scss/utilities";
            
//@import "bs_custom_utilities";
            
// Layout & components
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/type";
@import "bootstrap/scss/images";
@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/tables";
@import "bootstrap/scss/forms";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/transitions";
@import "bootstrap/scss/dropdown";
@import "bootstrap/scss/button-group";
@import "bootstrap/scss/nav";
@import "bootstrap/scss/navbar";
@import "bootstrap/scss/card";
@import "bootstrap/scss/accordion";
@import "bootstrap/scss/breadcrumb";
@import "bootstrap/scss/pagination";
@import "bootstrap/scss/badge";
@import "bootstrap/scss/alert";
@import "bootstrap/scss/progress";
@import "bootstrap/scss/list-group";
@import "bootstrap/scss/close";
@import "bootstrap/scss/toasts";
@import "bootstrap/scss/modal";
@import "bootstrap/scss/tooltip";
@import "bootstrap/scss/popover";
@import "bootstrap/scss/carousel";
@import "bootstrap/scss/spinners";
@import "bootstrap/scss/offcanvas";
@import "bootstrap/scss/placeholders";
            
// Helpers
@import "bootstrap/scss/helpers";
            
// Utilities
@import "bootstrap/scss/utilities/api";
            
// Icons BS
@import "bootstrap-icons/font/bootstrap-icons.css";            

Et voilà! Maintenant pour pusher les modifications du projet un simple git push prod main suffit.

Je pense automatiser le processus, que ce soit coté dev ou prod. Car, pour mettre en place ce Workflow beaucoup d'étapes sont nécessaires et une erreur dans un fichier de config. peut faire perdre des heures de recherches. C'est pour cela que j'ai condencé le tout ici afin d'avoir une ligne rouge à suivre lors de la création de scripts d'automatisation.