Hackaton Sup De Vinci 2023
Sommaire
Réalisé dans le cadre de mon année de Bachelor, le Hackaton à pour but de proposer une solution informatique en une semaine. Un jury juge ensuite la solution la plus convaincante et désigne les vainqueurs.
L’ensemble du projet que j’ai réalisé est disponible en open-source
sur GitLab
Introduction #
Lors de ce hackathon, mon équipe à choisi pour sujet Solution Libre
. Il s’agissait de :
- développer le frontend d’une application à destination de formateurs et de leurs élèves
- développer l’architecture qui permettra in fine d’héberger l’application, son backend et sa base de données
- proposer une CI/CD permettant de mettre en place une livraison et une intégration continue de l’applicatif et son infrastructure
Le back-end étant fourni par l’école, mon travail ici se limitait à son intégration ainsi qu’a celle du front-end.
Contexte et travail d’équipe #
L’ensemble des étudiants de l’école sont réunis en équipe de 10, tout niveaux et spécialités confondues. Toute la difficulté étant de coordonner l’ensemble de l’équipe afin de livrer un produit fonctionnel.
Limites et difficultés #
Malheureusement, l’équipe dont j’ai fais partie n’as pas sû délivrer un front-end dans les temps impartis. Cela dit, de mon côté j’ai pu architecturer l’ensemble de l’infrastructure ainsi que la pipeline CI/CD et obtenir un POC fonctionnel. Il ne me manquais plus qu’un front-end afin d’obtenir le résultat demandé, dommage 😥!
Cependant, j’ai pu tester le fonctionnement du back-end sur l’architecture ainsi déployée. C’est d’ailleurs l’objet de cet article : présenter ma solution sur la partie DevOps 😎.
Schéma d’architecture #
J’ai réalisé un petit brouillon de l’architecture que j’ai souhaité mettre en place sur le cloud Azure :
On à donc:
- Un cluster Kubernetes managé sur le cloud Azure (AKS)
- 3 namepsaces : un pour le front, un pour le back et le dernier pour le monitoring et la gestion des logs
pods
K8S au sein du même namespace
par manque de temps lors du
développement de la partie Terraform. La partie monitoring est aussi inachevée et ne figure pas sur le repo. Cela dit,
il n’est pas exclu que je m’y penche dans le cadre d’un article de blog dans un futur proche.En l’état, l’architecture disponible sur le lien GitLab donnée en préambule permet uniquement de requêter l’API du
backend. De plus, comme indiqué sur le schéma la base de donnée utilisée est hébergée dans un pod
. J’aurais préféré
mettre en place une base de donnée managée mais n’étant pas encore très à l’aise avec Kubernetes sur Azure, j’ai préféré
aller au plus simple. Le but de cet article reste pour moi l’occasion de garder une sorte de documentation sur le projet
réalisé. A ne pas utiliser en production donc 😉.
Infrastructure As Code #
Contexte #
L’enjeu étant de provisioner tout cela as code, j’ai déployé cette architecture avec Terraform. Le repo contenant l’infrastructure est construit comme ceci:
└── terraform
├── aks.tf
├── environment
│ └── dev
│ └── variables.tfvars
├── kubernetes.tf
├── main.tf
├── modules
│ └── kubernetes
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
├── outputs.tf
├── rg.tf
├── variables.tf
└── vpc.tf
Cette architecture est dès le départ conçue pour être en mesure de déployer des environments ISO
dev/pprd/prod.
Cela permet au développeur d’être en mesure (en théorie) de tester son code en amont sur des architectures identiques
avant de déployer l’applicatif en production.
Backend et providers #
Dans le fichier main.tf
, en début de code on retrouve la déclaration du backend
, ainsi que les providers
requis:
terraform {
backend "azurerm" {
resource_group_name = "backend-terraform-rg"
storage_account_name = "backendhackatonsdv"
container_name = "terraform-state"
key = "terraform.tfstate"
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "3.63.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "2.21.1"
}
}
}
provider "azurerm" {
features {}
skip_provider_registration = true
}
provider "kubernetes" {
host = module.aks.host
client_certificate = base64decode(module.aks.client_certificate)
client_key = base64decode(module.aks.client_key)
cluster_ca_certificate = base64decode(module.aks.cluster_ca_certificate)
}
Le backend
, permettant de stocker le fichier tfstate
consiste en un compte de stockage Azure hébergeant ce même
fichier. Dans le cas de ce lab, j’ai utilisé un compte de stockage préalablement existant sur mon compte Azure.
J’utilise les providers azure_rm
et kubernetes
permettant de respectivement :
- intéragir avec mon compte Azure
- intéragir avec le cluster Kubernetes déployé
apply
mon infrastructure en raison d’erreurs de droits. L’option
skip_provider_registration = true
m’as permis de débloquer la situation.Le provider kubernetes
nécessite une configuration permettant de se connecter au cluster afin d’y déployer les
ressources. Ici, je fournis les valeurs dynamiquement à partir du module aks
que je decris plus loin dans l’article.
Terraform et modules #
Pour une meilleure lisibilité, je préfère séparer chaque ressources déployées en un fichier distinct. Cela évite de se retrouver avec un gros fichier de plusieurs centaines de lignes devenant vite illisible. Terraform permet ensuite d’organiser son code en modules. Un module est un ensemble de fichiers Terraform stockés dans un dossier. Cela permet d’éviter les répétitions dans le code et la méthode de fonctionnement peut être comparable à une fonction dans un programme informatique. Ici j’ai donc :
aks.tf
-> Crée un cluster Kubernetes managé via le moduleAzure/aks/azurerm
kubernetes.tf
-> Intéragit avec le cluster Kubernetes. Ce fichier appelle un module que j’ai développé et stocké dans le répertoiremodules
rg.tf
-> Le groupe de ressource Azure contenant l’ensemble des instancesvpc.tf
-> La configuration réseau de l’infrastructure
Pour finir, dans le fichier variables.tf
, je déclare les variables qui seront utilisées pour déployer l’infrastructure.
Ces variables sont fournies par le fichier variables.tfvars
, qui diffère en fonction de l’environement de production
choisi. Ainsi, au plan
ou apply
il suffira de rajouter le flag -var-file=environment/dev/variables.tfvars
afin de
définir l’environment choisi. Dans ce lab, il s’agit de l’environment de dev
.
CI/CD #
Containerisation #
Pour être en mesure de déployer l’infrastructure sur Kubernetes, il m’a fallu au préalable containairiser le back-end dans une image Docker prête à être stocké sur un registre d’images (ici, j’utilise celui de GitLab). La méthode aurais été similaire pour le front-end. J’ai donc écris un Dockerfile :
# Base Golang Image
FROM golang:latest
# Setup working directory
WORKDIR /usr/src/osf-core
# Copy source code to
COPY . /usr/src/osf-core
# Install Git and NodeJS
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash -
RUN apt-get install -y nodejs npm
# Install NPM dependencies
RUN npm install -g @marp-team/marp-core \
&& npm install -g markdown-it-include \
&& npm install -g markdown-it-container \
&& npm install -g markdown-it-attrs
# Install Go Library & Swagger
RUN cd /usr/src/osf-core && go get golang.org/x/text/transform \
&& go get golang.org/x/text/unicode/norm \
&& go install github.com/swaggo/swag/cmd/swag@v1.8.12
# Init Swagger
RUN cd /usr/src/osf-core && swag init --parseDependency --parseInternal
# Export ports
EXPOSE 8000/tcp
EXPOSE 443/tcp
EXPOSE 80/tcp
# Launch the API
CMD ["go", "run", "/usr/src/osf-core/main.go"]
J’ai tenté d’utiliser les fichiers packages.json
et go.sum
/go.mod
afin d’installer les dépendances directement
depuis ces fichiers, mais la génération de mon image plantait. De plus, j’ai du forcer la version de swagger
en 1.8.12
car un problème de compatibilité m’empêchais de générer la documentation Swagger.
Ce Dockerfile est utilisé dans la CI afin de générer dynamiquement l’image destiné à être poussée dans le cluster K8S.
CI applicative #
La CI applicative est très basique pour ce lab mais il y a quelques spécificités. J’utilise une image docker in docker
,
car pour builder l’image applicative il est nécessaire d’executer des commandes docker dans docker
(plus d’infos ici). Pour fonctionner correctement, le build
d’une image Docker par le runner nécessite d’ajouter ces lignes en début de CI :
image: docker:20.10.16
services:
- docker:20.10.16-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
Sans cela, docker serais incapable d’accéder à Internet à l’intérieur du container.
La CI consiste en 3 stages :
- test
- deploy
- trigger_deploy_to_terraform
2 variables sont à renseigner manuellement : ENV
et TAG
:
ENV:
value: "dev"
description: "On wich env the image should be deployed"
TAG:
value: "latest"
description: "Version of the image"
Ces variables sont utilisées par la suite pour définir sur quel environment déployer l’image, et permet à Terraform de récupérer le chemin vers l’image à pousser sur le cluster. Le script qui va créer l’image est très simple :
deploy:
stage: deploy
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_TOKEN $DOCKER_REGISTRY_URL
- docker build -t registry.gitlab.com/sdv-open-course-factory/ocf-core/${ENV}-backend:${TAG} .
- docker push registry.gitlab.com/sdv-open-course-factory/ocf-core/${ENV}-backend:${TAG}
Le nom et le chemin de l’image sera renseigné automatiquement en fonction des données entrées par le dev.
Pour finir, le dernier stage déclenche la CI situées sur le repo Terraform, en poussant la variable TAG
permettant
à Terraform de pousser la bonne image du backend sur le cluster Kubernetes:
New job to trigger the other project's CI
trigger_deploy_to_terraform:
image: curlimages/curl
stage: trigger_deploy_to_terraform
script:
- curl -X POST --fail -F token=$CI_TRIGGER_TOKEN -F "ref=main" -F "variables[TAG]=$TAG" https://gitlab.com/api/v4/projects/47370418/trigger/pipeline
needs:
- deploy
TAG
est envoyée à Terraform, permettant de retrouver l’image.CI infrastructure #
La CI d’infrastructure peut soit être déclenchée par le job trigger
de la CI applicative, soit manuellement. On retrouve
les stages classiques :
- validate
- plan
- apply
- destroy
J’utilise ici l’image hashicorp/terraform:latest
, m’évitant d’installer Terraform à chaque déploiement sur le runner.
Au niveau des variables :
variables:
TF_ROOT: ${CI_PROJECT_DIR}/terraform/
TF_ENVIRONMENT: "dev" # Définissez l'environnement souhaité ici (par exemple, dev, preprod, prod)
TF_DESTROY:
description: Destroy Terraform resources
value: "false"
dev
dans mon labLa variable TF_DESTROY
me permet de détruire l’infrastructure via la CI. Le stage destroy
ne s’execute seulement
ci la valeur est changée pour true
:
terraform_destroy:
stage: destroy
script: terraform destroy -auto-approve -var-file=environment/${TF_ENVIRONMENT}/variables.tfvars -var="img_tag=${TAG}"
rules:
- if: $TF_DESTROY == "true"
when: always
La partie plan
contient un simple script bash
qui va tester l’existence de la variable TAG
récupérée depuis la CI
applicative :
if [ -n "$TAG" ]; then
terraform plan -var-file=environment/${TF_ENVIRONMENT}/variables.tfvars -var "img_tag=${TAG}"
else
echo "Nothing to plan"
fi
J’utilise le flag -var-file=environment/${TF_ENVIRONMENT}/variables.tfvars
afin de déterminer dynamiquement sur
quel environenment déployer. Puis le flag -var "img_tag=${TAG}
pour déterminer l’image Docker à utiliser sur le même
principe.
Le flag -var "img_tag=${TAG}
permet de définir la variable Terraform contenue dans le variables.tf
à la racine ainsi
que dans le module Kubernetes:
variable "img_tag" {
description = "Image tag"
type = string
}
L’adresse permettant de pointer vers l’image est ensuite reconstruite comme ceci lors de la création du déploiement Kubernetes:
spec {
container {
image = "registry.gitlab.com/sdv-open-course-factory/ocf-core/${var.env}-backend:${var.img_tag}"
name = "ocf-core-backend"
port {
container_port = 80
}
port {
container_port = 443
}
port {
container_port = 8000
}
Conclusion #
J’ai eu au final à peu près 4 jours pour réaliser toute cette architecture. C’est assez court, mais le POC est fonctionnel
et permet d’accéder à la doc swagger
du backend et de manipuler l’API. Dans l’idée, pour vraiment déployer ce lab en production il faudrais:
- Bien sûr y intégrer un front-end
- Retravailler la CI pour intégrer l’image du front-end
- Intégrer toute la partie monitoring et gestion des logs, c’est super important
- Utiliser une BDD managée Azure au lieu d’une image
PostegrSQL
dans le cluster - Séparer chaque services sur des namespaces différents
- Niveau sécurité, placer le loadbalancer sur un subnet séparé.
- Mettre en place un bastion sur un VPC différent afin d’être en mesure de requêter l’API K8S via
kubectl
. Ici, ⚠️ l’API est exposée au web ⚠️
Je pense ré-utiliser mon architecture pour tenter de réaliser le front-end. J’en profiterais pour consolider le tout
par rapport à la liste ci-dessus. Cela fais aussi quelque temps que je suis intéressé
par les technologies de dev orienté front-end (React
) et n’ayant pas ou très peu d’expérience en JavaScript
, ce serais
l’occasion. Affaire à suivre donc…