Aller au contenu principal

TaskHorizon

Contexte​

TaskHorizon est une application de gestion de tâches Kanban que j'ai développée seul, sans ambition produit. Son rôle est de servir de terrain d'expérimentation pour un ensemble de compétences DevOps et backend rarement réunies dans un même projet professionnel : conception d'API, modélisation de données, infrastructure as code, orchestration Kubernetes multi-environnement et automatisation CI/CD. Chaque décision (stack, stratégie de déploiement, gestion des secrets, résilience) est prise et assumée de bout en bout, sans contrainte organisationnelle héritée.

L'API REST (FastAPI + PostgreSQL) et l'interface React constituent le support fonctionnel qui rend le projet concret. Le périmètre décrit ci-dessous porte sur ce qui l'entoure.

Stack technique​

Backend
Python, FastAPI, SQLAlchemy, Alembic, Pydantic
Frontend
React, TypeScript, Vite
Données
PostgreSQL
Conteneurisation & orchestration
Docker, Kubernetes, Helm
Infrastructure as code
Terraform, modules terraform-aws-modules
Cloud (AWS)
EKS, VPC, RDS, S3, IAM / IRSA
CI/CD
GitHub Actions
Qualité
pytest, Vitest, Ruff, Black, pre-commit

Modèle de données et logique métier​

Le schéma repose sur quatre entités : User, Column, Task, Label. Les identifiants sont des UUID générés côté application plutôt que des séquences auto-incrémentées, pour ne jamais exposer d'information sur le volume de données via un identifiant public.

La couche API sépare strictement les schémas Pydantic (validation et sérialisation) des modèles SQLAlchemy : un endpoint ne retourne jamais un objet ORM directement, ce qui évite d'exposer un champ interne ou une relation lazy-loaded par accident. La session SQLAlchemy est injectée par requête via le système de dépendances FastAPI, ouverte et fermée dans le même cycle de vie de la requête.

Le déplacement d'une tâche (POST /tasks/{id}/move) est l'opération la plus sensible du modèle : elle recalcule les positions des tâches adjacentes, dans la colonne d'origine et dans la colonne de destination, à l'intérieur d'une seule transaction. L'objectif est d'éviter un état incohérent, deux tâches à la même position, si l'opération échoue à mi-chemin.

Les migrations de schéma sont gérées par Alembic, découplées de l'initialisation applicative : au démarrage, l'application se contente d'insérer les données de référence (les trois colonnes par défaut, le compte administrateur) si elles sont absentes, sans jamais toucher au schéma lui-même.

Authentification​

L'authentification repose sur des JWT signés en HS256, avec un mot de passe haché en bcrypt. Le jeton porte l'identifiant utilisateur, l'email et le statut administrateur, avec une expiration de 24 heures. Une dépendance FastAPI dédiée (require_admin) restreint certains endpoints aux comptes administrateur, en s'appuyant sur la même vérification de jeton que l'authentification standard plutôt que sur une logique dupliquée.

Le secret de signature JWT et le mot de passe du compte administrateur ne sont jamais définis en dur : ils sont injectés par variable d'environnement, elle-même alimentée par le pipeline de déploiement.

Déploiement Kubernetes​

Un chart Helm unique gère trois environnements, chacun déployé comme une release distincte (taskhorizon-test, taskhorizon-staging, taskhorizon-prod). Les pipelines installent test dans le namespace taskhorizon et staging dans taskhorizon-staging, mais le chart ne compte pas sur cette séparation : chaque ressource porte le nom de la release en préfixe, et les selectorLabels incluent app.kubernetes.io/instance. Sans cette discipline, deux releases installées dans le même namespace entreraient en conflit de noms, et un Service d'un environnement pourrait router du trafic vers les pods d'un autre.

La divergence la plus structurante entre environnements porte sur la persistance des données et la disponibilité :

EnvironnementBase de donnéesRéplicasParticularité
testPostgreSQL dans le cluster, sans volume1données perdues au redémarrage, état volontairement jetable
stagingRDS provisionnée par Terraform, endpoint injecté au déploiement2 (API et web)PodDisruptionBudget actif (minAvailable: 1)
prodRDS externe (Secret préexistant)API 3 à 10, web 2 à 5, pilotés par HPApas de replicas fixe dans le Deployment, Ingress TLS via cert-manager

Le chart conserve un mode intermédiaire, un StatefulSet PostgreSQL adossé à un PVC, activable par une seule valeur (postgres.pvc.enabled). Il a servi de persistance pour staging avant la bascule vers RDS, qui aligne staging sur la topologie de production.

En production, le Deployment ne fixe pas de nombre de réplicas dès que le HorizontalPodAutoscaler est activé (seuil CPU à 70 %) : le template n'émet le champ replicas que si l'HPA est désactivé. Définir replicas et activer un HPA simultanément produit un conflit de contrôle : à chaque helm upgrade, la valeur du chart écrase celle calculée par l'HPA, qui la réajuste ensuite.

Le mot de passe de connexion à la base ne transite jamais par un ConfigMap : il est lu depuis un Secret Kubernetes et substitué au runtime par le kubelet via $(DB_PASSWORD). En staging, ce mot de passe est fourni au moment du déploiement via --set, depuis les secrets GitHub Actions, sans jamais résider dans un fichier de values versionné. En production, le chart ne crée aucun Secret de base de données : il référence par son nom un Secret provisionné hors bande, cohérent avec le fait que l'instance RDS elle-même est hors du contrôle du chart applicatif.

Le frontend nginx proxifie /api/ vers le service API interne, dont l'adresse dépend du nom de la release (taskhorizon-test-api:8000, taskhorizon-prod-api:8000, etc.). Cette configuration est générée dans un ConfigMap, et le Deployment web porte une annotation de checksum sur ce ConfigMap : toute modification de la configuration nginx déclenche un rolling restart automatique, sans intervention manuelle.

Infrastructure AWS​

Le cluster tourne sur AWS EKS (région eu-west-3), provisionné par Terraform plutôt que créé manuellement dans la console. Le réseau comprend un VPC dédié avec quatre sous-réseaux répartis sur deux zones de disponibilité (deux publics, deux privés), condition requise pour la haute disponibilité d'EKS et de RDS. Les sous-réseaux publics portent les tags kubernetes.io/role/elb et les privés kubernetes.io/role/internal-elb, qui indiquent au contrôleur de Load Balancer AWS où créer les répartiteurs de charge externes ou internes. Une NAT Gateway unique permet aux nœuds situés en sous-réseau privé de sortir vers Internet sans y être exposés directement.

L'accès aux avatars utilisateurs illustre le choix d'éviter les identifiants statiques : ils sont stockés dans un bucket S3 dont l'accès public est bloqué par les quatre réglages S3 Block Public Access (blocage et ignorance des ACL publiques, blocage des policies publiques, restriction des buckets publics), et dont la policy n'autorise qu'un unique rôle IAM. Ce rôle est assumé par les pods de l'API via IRSA (IAM Roles for Service Accounts) : le ServiceAccount Kubernetes de l'API est fédéré à ce rôle par le fournisseur OIDC du cluster, ce qui permet aux pods d'obtenir des permissions S3 sans qu'aucune clé d'accès ne soit stockée dans le cluster.

L'instance RDS est provisionnée de façon conditionnelle (enable_rds), chiffrée au repos, avec une politique prevent_destroy pour éviter une suppression accidentelle par un terraform apply mal ciblé.

Observabilité​

EKS Auto Mode gère les nœuds, mais pas la supervision des applications : sans configuration, CloudWatch ne voyait aucun pod. J'ai décrit l'observabilité du cluster en Terraform. L'add-on amazon-cloudwatch-observability collecte les métriques Container Insights et les logs des conteneurs, avec un rôle IAM obtenu par EKS Pod Identity plutôt que par le rôle des nœuds. J'ai corrigé deux réglages par défaut. Les groupes de logs sont créés par Terraform avant l'add-on, avec une rétention fixée, pour ne pas être conservés indéfiniment. Application Signals est désactivé : il aurait instrumenté l'API sans que je le demande, avec une facturation à la requête.

Côté application, l'API écrit désormais une ligne JSON par requête (méthode, gabarit de route, statut, durée), à un niveau qui dépend du statut. Un metric filter compte les erreurs de l'API et alimente une alarme, avec une autre sur l'absence de pod en service, une sur la mémoire et une sur le stockage RDS, toutes envoyées sur un topic SNS. Un dashboard versionné complète l'ensemble, avec la latence p95 par route calculée par Logs Insights. Le détail est dans l'article AWS : CloudWatch sur EKS.

Ce volet est écrit et validé hors ligne (terraform validate, tests de l'API), mais il n'a pas été appliqué : le compte AWS du projet n'a plus de crédits.

Intégration et déploiement continus​

Trois workflows GitHub Actions couvrent le cycle du commit à la production :

pr-checks s'exécute sur chaque pull request : hooks pre-commit (Ruff, Black), tests de l'API avec couverture (pytest), vérification qu'aucune migration Alembic n'est en attente contre une instance PostgreSQL éphémère, puis côté web, vérification de type (tsc), tests (Vitest) et build. Chaque étape publie son résultat en commentaire sur la pull request plutôt que de forcer à consulter les logs bruts de la CI.

deploy-test se déclenche sur chaque push vers master : construction et publication des images Docker taguées :main, application Terraform sur l'environnement de test, puis déploiement Helm. C'est un flux continu, sans porte d'entrée : tout ce qui atteint master finit sur l'environnement de test.

deploy-staging se déclenche sur un tag sémantique (vX.Y.Z) : construction des images versionnées, application Terraform sur l'environnement staging, récupération de l'endpoint RDS en sortie Terraform, puis déploiement Helm avec injection des secrets (JWT, mot de passe administrateur, mot de passe de base de données) via --set. Le passage par un tag plutôt qu'un déclenchement automatique sur master fait de la promotion vers staging un acte volontaire.

Il n'existe pas de workflow deploy-prod : le déploiement en production reste manuel, ce qui est cohérent avec le choix de provisionner l'instance RDS et les secrets de production hors du pipeline plutôt que de les faire transiter par GitHub Actions.

Résultats​

  • Deux environnements livrés automatiquement sur AWS EKS : test à chaque push sur master, staging à chaque tag sémantique, infrastructure Terraform et release Helm comprises.
  • Un chart unique pour trois topologies, de la base jetable de test à la production autoscalée sur RDS, sans collision possible entre releases.
  • Aucune clé d'accès statique ni secret versionné : accès S3 par IRSA, secrets injectés au déploiement ou provisionnés hors bande.
  • Des pull requests vérifiées avant fusion (tests, migrations en attente, types, build), avec un résultat lisible directement sur la pull request.
  • Une vingtaine d'articles du blog tirés des concepts AWS, Terraform, Kubernetes et CI/CD mis en pratique sur le projet.

Liens​

Articles liés

21 articles du blog détaillent des notions mises en pratique dans ce projet.

  1. Cloud

    AWS : CloudWatch sur EKS

    Observabilité CloudWatch du cluster EKS décrite en Terraform : add-on avec Pod Identity, groupes de logs à rétention fixée, Application Signals désactivé, alarmes SNS et dashboard. L'API écrit des logs JSON par requête, avec le gabarit de route.

  2. CI/CD

    GitHub Actions : pipeline CI/CD de bout en bout vers EKS

    Chaîne Publish puis Deploy Test : images de l'API et du frontend publiées sur GHCR, puis terraform apply et déploiement Helm sur l'environnement de test à chaque push sur master.

  3. Infrastructure as Code

    Terraform : multi-environnements

    Configuration racine unique pour les environnements test et staging, sélectionnés par un fichier -backend-config et un fichier -var-file par environnement, avec l'instance RDS activée par la variable enable_rds.

  4. Infrastructure as Code

    Terraform : count, for_each, locals et expressions

    Interrupteur enable_rds (count = var.enable_rds ? 1 : 0) sur l'instance RDS et son subnet group, et création des quatre subnets du VPC par for_each sur une map déclarée dans locals.tf, chaque entrée portant son CIDR, l'index de son AZ et son niveau (public ou privé).

  5. Infrastructure as Code

    Terraform : depends_on et lifecycle

    prevent_destroy sur l'instance RDS, ignore_changes sur les tags du bucket S3 des avatars, et depends_on explicites : Elastic IP et NAT Gateway après l'internet gateway, politique du bucket après son bloc d'accès public.

  6. Infrastructure as Code

    Terraform : modules

    Cluster EKS provisionné par le module du registry terraform-aws-modules/eks/aws, avec une contrainte de version ~> 20.0, alimenté par le VPC et les subnets déclarés dans la configuration racine.

  7. Infrastructure as Code

    Terraform : state remote avec S3

    Backend S3 en configuration partielle, avec une clé de state par environnement injectée par le pipeline de déploiement au terraform init.

  8. Cloud

    AWS : EKS

    Cluster EKS provisionné par Terraform en eu-west-3, dont les pods de l'API accèdent au bucket S3 des avatars via IRSA, sans clé d'accès stockée dans le cluster.

  9. Infrastructure as Code

    Terraform : data sources

    Fichier data.tf regroupant aws_availability_zones, qui répartit les subnets sur deux AZs sans nom codé en dur, et aws_caller_identity, dont l'identifiant de compte préfixe le nom du bucket S3 des avatars et désigne le compte dans la politique d'approbation d'un rôle IAM.

  10. Infrastructure as Code

    Terraform

    Provisionnement par Terraform du bucket S3 des avatars, du VPC et de l'instance RDS, dont les outputs (endpoint RDS) sont injectés dans le déploiement Helm par le pipeline GitHub Actions.

  11. Scripting

    Python : CRUD et authentification avec FastAPI

    API REST de l'application avec schémas Pydantic séparés des modèles, authentification JWT signée en HS256, mots de passe hachés en bcrypt et dépendance require_admin pour les endpoints réservés aux administrateurs.

  12. Scripting

    Python : SQLAlchemy

    Modèles SQLAlchemy des entités User, Column, Task et Label, session injectée par requête via les dépendances FastAPI et déplacement d'une tâche recalculé dans une seule transaction.

  13. Orchestration

    Kubernetes : déploiements sans interruption et optimisation des ressources

    Rolling restart automatique du frontend nginx déclenché par une annotation de checksum sur son ConfigMap, dans un chart Helm unique qui déploie les environnements test, staging et production.

  14. Orchestration

    Kubernetes : PodDisruptionBudget et haute disponibilité

    PodDisruptionBudget minAvailable: 1 en staging et HorizontalPodAutoscaler en production (API de 3 à 10 réplicas, web de 2 à 5, seuil CPU à 70 %), sans champ replicas fixe lorsque l'HPA est actif.

  15. Cloud

    AWS : VPC

    VPC dédié provisionné par Terraform en eu-west-3, avec deux sous-réseaux publics et deux privés sur deux zones de disponibilité, tags kubernetes.io/role/elb et kubernetes.io/role/internal-elb, et NAT Gateway unique pour la sortie des nœuds privés.

  16. Cloud

    AWS : RDS, S3 et EBS

    Instance RDS PostgreSQL chiffrée au repos et protégée par prevent_destroy, provisionnée par Terraform pour l'environnement de staging, et bucket S3 d'avatars aux quatre réglages Block Public Access activés, dont la policy n'autorise qu'un rôle IAM assumé par les pods de l'API.

  17. Orchestration

    Kubernetes : kubectl

    Création idempotente du namespace et du secret de pull du registre dans les workflows de déploiement, par génération d'un manifeste (kubectl create ... --dry-run=client -o yaml) transmis à kubectl apply -f -.

  18. Scripting

    Python : Pydantic

    Schémas Pydantic de l'API FastAPI, séparés des modèles SQLAlchemy : validation des requêtes et sérialisation des réponses sans jamais retourner un objet ORM.

  19. Orchestration

    Kubernetes : composants de base

    Chart Helm composé de deux Deployments (API et frontend web) exposés par des Services, et d'un StatefulSet PostgreSQL adossé à un PVC et à un Service headless, activable en option.

  20. Orchestration

    Kubernetes : Secrets et ConfigMaps

    Configuration nginx du frontend générée dans un ConfigMap, avec une annotation de checksum sur le Deployment qui déclenche un rolling update à chaque modification, et mot de passe de la base lu depuis un Secret, jamais depuis un ConfigMap.

  21. Réseau

    Nginx

    Nginx en conteneur frontend : service de la SPA avec repli try_files vers index.html, reverse proxy de /api/ vers le service API interne avec en-têtes X-Forwarded-*, et en-têtes de cache par type de ressource, configuration générée dans un ConfigMap Helm.