CI/CD
Objectifs
- Estimer son travail
- Ajouter des tests unitaires en Python
- Créer une CI/CD pipeline sur GitLab
Rendu
- GitHub Classroom : https://classroom.github.com/a/s6fNQH2c
- Rapport individuel en Markdown à rendre avant le prochain cours
- Nom du fichier :
report.md
à la racine du répertoire
- Devoir sur Cyberlearn : mettre le lien de la pull request GitLab dans le champ texte
- Délai: 2 semaines
Tâches
Estimer son travail
- Estimez le temps nécessaire pour réaliser ce laboratoire
- Découpez le travail en tâches pour faciliter l'estimation
- Lorsque vous aurez terminé le laboratoire, comparez le temps estimé avec le temps réellement passé
Tâche | Temps estimé | Temps réel | Commentaire |
---|---|---|---|
Estimation | 10m | 15m | ... |
... | ... | ... | ... |
Total | 2h | 1h30 | ... |
Git
- Reprenez votre projet sur GitLab du laboratoire précédent (HEIG-VD DevOps)
- Mettez tout votre travail sur une branche
feature/04-cicd
et faites une merge request (MR) surmain
en m'ajoutant comme reviewer - Séparez votre travail en commits cohérents avec des messages de commit clairs et concis
Tester le backend
- Ajoutez les dépendances de développement
poetry add -G dev pytest pytest-cov httpx
- Une dépendance de développement est une dépendance qui n'est pas nécessaire en production, par exemple uniquement pour les tests
pytest
est le framework de testpytest-cov
permet de générer un rapport de couverture de codehttpx
permet de faire des requêtes HTTP dans les tests
- Ajoutez/modifier les fichiers suivants (inspiré de cette documentation) :
- main.py
- test_main.py
/backend/backend/main.py
from os import getenv
from sys import modules
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
from . import models, schemas
from .database import SessionLocal, engine
if "pytest" not in modules:
models.Base.metadata.create_all(bind=engine)
app = FastAPI(root_path=getenv("ROOT_PATH"))
...
/backend/backend/tests/test_main.py
from random import choices, uniform
from string import ascii_letters
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from backend.database import Base
from backend.main import app, get_db
DATABASE_URL = "sqlite://"
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
def random_string(n=32):
return "".join(choices(ascii_letters, k=n))
def random_double():
return round(uniform(0.0, 100.0), 2)
product = {
"name": random_string(),
"description": random_string(512),
"price": random_double(),
}
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"Hello": "World"}
def test_read_empty_products():
response = client.get("/products/")
assert response.status_code == 200
assert response.json() == []
def test_create_product():
response = client.post(
"/products/",
json=product,
)
assert response.status_code == 200
assert response.json() == {"id": 1, **product}
def test_read_product():
response = client.get("/products/1")
assert response.status_code == 200
assert response.json() == {"id": 1, **product}
def test_read_products():
response = client.get("/products/")
assert response.status_code == 200
assert response.json() == [{"id": 1, **product}]
def test_delete_product():
response = client.delete("/products/1")
assert response.status_code == 200
assert response.json() == {"id": 1, **product}
response = client.get("/products/1")
assert response.status_code == 404
def test_read_deleted_empty_products():
response = client.get("/products/")
assert response.status_code == 200
assert response.json() == []
- Pour lancer les tests :
poetry run pytest --cov
GitLab CI/CD
Créez une pipeline sur GitLab CI/CD qui :
- a les 3 stages :
- build : vérifie que le code compile
- test :
- vérifie que les tests (du backend) passent
- Unit Test Reports
- Code Coverage
- Code Quality
- Dependency Scanning
- SAST
- Container Scanning
- deploy : met à jour les images Docker sur le registry
- est déclenchée à chaque push sur n'importe quelle branche
- le stage
deploy
n'est exécuté que surmain
- le stage
- Le frontend et le backend doivent être dans des jobs séparés et en parallèle
- Chacun est exécuté uniquement lorsqu'il y a des changements dans son dossier
Proposition
Vous allez devoir tester beaucoup de changements sur la pipeline, une manière d'éviter d'avoir plein de commit est d'en utiliser qu'un seul (ne pas le faire sur main
ou develop
!) : git commit --amend --all --no-edit && git push --force-with-lease
Commencez par le frontend (commencez vos scripts par cd frontend/
)
- Le job
build-frontend
utilise l'imagenode:lts
, exécutenpm ci
etnpm run build
- Le résultat du build est gardé dans un artéfact pour être utilisé par le job
deploy-frontend
- Ajoutez le cache
- Le résultat du build est gardé dans un artéfact pour être utilisé par le job
- Le job
deploy-frontend
utilise l'imagedocker
avec le servicedocker:dind
, exécutedocker build -t ${CI_REGISTRY_IMAGE}/frontend:latest .
etdocker push ${CI_REGISTRY_IMAGE}/frontend:latest
- Docker in Docker
- Docker login
echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin
- Docker Layer Caching
Solution .gitlab-ci.yml
build-frontend:
stage: build
image: node:lts
cache:
key:
files:
- frontend/package-lock.json
paths:
- frontend/.npm/
before_script:
- cd frontend/
script:
- npm ci --cache .npm --prefer-offline
- npm run build
artifacts:
paths:
- frontend/dist/
deploy-frontend:
stage: deploy
image: docker
services:
- docker:dind
dependencies:
- build-frontend
variables:
REGISTRY_IMAGE: ${CI_REGISTRY_IMAGE}/frontend
before_script:
- cd frontend/
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin
script:
- docker pull $REGISTRY_IMAGE:latest || true
- docker build --cache-from $REGISTRY_IMAGE:latest -t $REGISTRY_IMAGE:latest .
- docker push $REGISTRY_IMAGE:latest
Puis le backend (similairement au frontend)
- Le job
build-backend
utilise l'imagepython:3.11
, installe Poetry et les dépendances en les cachant pour les prochains jobs
build-backend:
stage: build
image: python:3.11
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
paths:
- .cache/pip
- backend/.venv/
before_script:
- cd backend/
- pip install poetry
- poetry config virtualenvs.in-project true
script:
- poetry install
- Le job
test-backend
reprend le cache du jobbuild-backend
et exécutepoetry run pytest --cov
test-backend:
stage: test
image: python:3.11
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
paths:
- .cache/pip
- backend/.venv/
before_script:
- cd backend/
- pip install poetry
- poetry config virtualenvs.in-project true
script:
- poetry run pytest --cov
- Ajoutez les éléments suivants
- Le job
deploy-backend
est très similaire au jobdeploy-frontend
Solution .gitlab-ci.yml
build-backend:
stage: build
image: python:3.11
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
paths:
- .cache/pip
- backend/.venv/
before_script:
- cd backend/
- pip install poetry
- poetry config virtualenvs.in-project true
script:
- poetry install
test-backend:
stage: test
image: python:3.11
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
paths:
- .cache/pip
- backend/.venv/
before_script:
- cd backend/
- pip install poetry
- poetry config virtualenvs.in-project true
script:
- poetry run pytest --cov --junitxml="rspec.xml" --cov-report term --cov-report xml:coverage.xml
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts:
paths:
- backend/rspec.xml
reports:
junit: backend/rspec.xml
coverage_report:
coverage_format: cobertura
path: backend/coverage.xml
deploy-backend:
stage: deploy
image: docker
services:
- docker:dind
variables:
REGISTRY_IMAGE: ${CI_REGISTRY_IMAGE}/backend
before_script:
- cd backend/
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin
script:
- docker pull $REGISTRY_IMAGE:latest || true
- docker build --cache-from $REGISTRY_IMAGE:latest -t $REGISTRY_IMAGE:latest .
- docker push $REGISTRY_IMAGE:latest
- N'effectuez le stage
deploy
uniquement sur la branchemain
- Transformez votre pipeline en Directed Acyclic Graph Pipelines
- Transformez votre pipeline en Parent-child pipelines
- Optimisez vos fichiers YAML
- Ajoutez et configurez