Skip to content

RĂ©sumĂ© Infra EKS — fastapi-eks-project

Contexte projet

Portfolio DevOps — FastAPI + PostgreSQL sur AWS EKS. GitLab : gitlab.com/yk-devops/fastapi-eks-project Dev Container : VS Code avec AWS CLI, Terraform, kubectl, Helm.


Stack technique

App         : FastAPI + PostgreSQL + Alembic + PyJWT + bcrypt
Cloud       : AWS eu-west-3 (Paris)
IaC         : Terraform 1.15
Container   : Docker → ECR → EKS
CI/CD       : GitLab CI/CD (runner self-hosted Ubuntu 22.04 ESXi)
Routing     : Envoy Gateway (Gateway API)
TLS         : cert-manager + Let's Encrypt (DNS-01 via Cloudflare)
DNS         : ExternalDNS (provider Cloudflare, source gateway-httproute, policy sync)
Domaine     : devopsyouss.com (Cloudflare)

Architecture AWS

Internet (HTTPS — api.devopsyouss.com)
    ↓
NLB (créé par Envoy Gateway — ports 80 + 443, #70 ADR 017)
    ↓
Envoy Proxy Pod (TLS terminĂ© ici — cert rempli par cert-manager + Let's Encrypt)
    ↓
Service ClusterIP (fastapi)
    ↓
FastAPI Pod (namespace: fastapi)
    ↓
RDS PostgreSQL (subnet privé)

Terraform — 2 workspaces sĂ©parĂ©s

persistent/ (jamais détruit)

ECR : fastapi-eks/fastapi
IAM : gitlab_ci (ECR push) + gitlab_ci_infra (terraform)
Secrets Manager : fastapi-eks/app (DB_PASSWORD + SECRET_KEY) → consommĂ© par ESO (#33)
State S3 : yk-devops-terraform-state/fastapi-eks/persistent/

ephemeral/ (apply matin, destroy soir)

VPC : 10.0.0.0/16, 2 subnets publics + 2 privés
NAT Gateway : obligatoire pour nodes EKS
EKS : fastapi-eks-cluster, version 1.32, node t3.medium
RDS : PostgreSQL 16, db.t3.micro, subnet privé
IRSA ESO (#33) : provider OIDC du cluster + rĂŽle fastapi-eks-eso-irsa (lecture du secret fastapi-eks/app)
State S3 : yk-devops-terraform-state/fastapi-eks/ephemeral/

Scripts quotidiens

./aws-start.sh   # matin → terraform apply + kubectl config
./aws-stop.sh    # soir  → kubectl delete gateway + terraform destroy

Pipeline CI/CD GitLab

.gitlab-ci.yml (app pipeline)

Stages : test → security → build → scan-image → promote → deploy (write-back GitOps)

test          : pytest + PostgreSQL service
security      : SAST, Secret Detection, Trivy fs, tfsec, kube-linter
build         : Kaniko build le Dockerfile multi-stage (python:3.12-slim pinnĂ© par digest, deps prod only) → ECR (tag: candidate-SHA-PIPELINEID, unique car repo IMMUTABLE). Cf ADR 011
scan-image    : Trivy image scan depuis ECR (sur le candidate)
promote       : retag par digest aws-cli (batch-get-image + put-image → tag SHA). Pas de rebuild, pas de latest. Image dĂ©ployĂ©e == image scannĂ©e (INC-050)
update-image-tag : GitOps write-back (#72, ADR 013 ; recible env dev #139). kustomize edit set image dans k8s/overlays/{fastapi,frontend}/dev/kustomization.yaml + git commit [skip ci] + push sur develop. ArgoCD reconcilie l'env dev (pull-based). staging/prod n'avancent PAS ici. develop only.

Déclenché sur : push, MR, develop, main
Déploiement   : pull-based via ArgoCD (write-back automatique sur develop, pas d'input manuel)

Promotion multi-env (dev → staging → prod, #139 ADR 029 D2)

Le tag d'image vit par overlay (k8s/overlays/<workload>/<env>/kustomization.yaml, champ images[].newTag), plus dans base. Chemin de promotion :

  • dev = automatique. À chaque merge sur develop, update-image-tag bumpe overlays/{fastapi,frontend}/dev. ArgoCD dĂ©ploie dev seul. Un merge ne touche QUE dev.
  • staging puis prod = MR de promotion. Promouvoir = recopier le newTag validĂ© de l'overlay amont vers l'aval (dev → staging, puis staging → prod), sur une branche, puis MR vers develop. Rien d'autre ne change (mĂȘme image, dĂ©jĂ  scannĂ©e).
# Exemple : promouvoir le back de dev vers staging
TAG=$(grep newTag k8s/overlays/fastapi/dev/kustomization.yaml | awk '{print $2}')
cd k8s/overlays/fastapi/staging && kustomize edit set image \
  fastapi=199167114788.dkr.ecr.eu-west-3.amazonaws.com/fastapi-eks/fastapi:$TAG
# puis: branche + MR vers develop (staging->prod idem, review CODEOWNERS requise)

Gate prod : .gitlab/CODEOWNERS exige 1 approbation sur toute MR modifiant overlays/*/prod/. Effectif seulement si develop (protégée) a "Require approval from code owners" activé (réglage projet GitLab). E2E staging vert = condition It.4 (#140).

.gitlab-ci-infra.yml (infra pipeline)

Stages : infra → bootstrap → teardown → destroy

infra-status  : aws eks list-clusters, rds, ecr...
infra-start   : terraform apply + kubectl get nodes
bootstrap     : Ansible — Envoy Gateway + metrics-server + cert-manager + ClusterIssuers + External Secrets Operator + ExternalDNS (Cloudflare, gateway-httproute) + ArgoCD v3.4.3 (chart 9.5.20, app-of-apps root Application) + exposition (GatewayClass + Gateway + Certificate) + RBAC user CI (ESO + HTTPRoute) (action=start)
infra-stop    : aws-stop.sh (cleanup K8s + terraform destroy)

Déclenché sur : Run Pipeline (web) + Schedule 20h
Inputs        : action = status | start | stop

Sérialisation : les jobs mutants (infra-start, bootstrap, infra-stop,
                teardown) partagent resource_group: infra → un pipeline
                start et le teardown programmé ne tournent jamais en
                parallÚle (évite la corruption mid-bootstrap). #132

Variables GitLab CI/CD

APP PIPELINE :
AWS_ACCESS_KEY_ID      → gitlab_ci (ECR push)
AWS_SECRET_ACCESS_KEY  → gitlab_ci (ECR push)
AWS_DEFAULT_REGION     → eu-west-3
ECR_REGISTRY           → 199167114788.dkr.ecr.eu-west-3.amazonaws.com
ECR_REPOSITORY         → fastapi-eks/fastapi
SECRET_KEY             → JWT secret

INFRA PIPELINE :
AWS_INFRA_ACCESS_KEY_ID     → gitlab_ci_infra
AWS_INFRA_SECRET_ACCESS_KEY → gitlab_ci_infra
TF_VAR_db_password          → <REDACTED - stored in GitLab CI as Masked variable>
CLOUDFLARE_API_TOKEN        → token API Cloudflare (Masked + Protected, Zone > Zone Read + DNS Edit)

Note ESO (#33) : TF_VAR_db_password alimente aussi le secret Secrets Manager persistent (mĂȘme valeur que RDS). SECRET_KEY est gĂ©nĂ©rĂ© par Terraform (random_password) et stockĂ© dans Secrets Manager. Depuis MR-D, le job deploy ne crĂ©e plus le Secret (ESO le synchronise) : la variable CI SECRET_KEY est devenue inutile cĂŽtĂ© deploy et peut ĂȘtre supprimĂ©e des settings GitLab.


Kubernetes manifests (k8s/)

k8s/base/  (réconcilié par ArgoCD via kustomize, #72 ; le tag image est commité dans kustomization.yaml par le write-back CI)
├── serviceaccount.yaml             → SA dĂ©diĂ© fastapi, automountServiceAccountToken false
├── configmap.yaml                  → DB_HOSTNAME, DB_PORT, DB_NAME...
├── deployment.yaml                 → FastAPI pods, probes /healthz, resources, securityContext
├── service.yaml                    → ClusterIP port 80→8080
├── hpa.yaml                        → min:1, max:5, cpu:70%, mem:80%
├── pdb.yaml                        → PodDisruptionBudget minAvailable 1
├── networkpolicy-default-deny.yaml → deny-all ingress + egress
├── networkpolicy-fastapi.yaml      → ingress 8080 ; egress 5432 (RDS), 443 (AWS), 53 (DNS)
├── networkpolicy-postgres.yaml     → rĂšgles pour le pod postgres (prĂ©sent en local-kind)
├── secretstore.yaml                → ESO SecretStore (AWS Secrets Manager, auth IRSA contrîleur)
├── externalsecret.yaml             → ESO ExternalSecret → Secret fastapi-secrets (DB_PASSWORD, SECRET_KEY)
├── httproute.yaml                  → Gateway API HTTPRoute (app : route api.devopsyouss.com, ownĂ© par le deploy CI, #60 ; rule /metrics → 404 directResponse, #81)
└── httproutefilter-deny-metrics.yaml → HTTPRouteFilter Envoy GW : directResponse 404 sur /metrics (coupe l'expo publique, scrape interne intact, #81 ADR 018)

k8s/overlays/  (multi-env, #134 ADR 029 D1 — un overlay par workload et par env)
├── fastapi/{dev,staging,prod}/kustomization.yaml   → resources:[../../../base] + patchs : namespace fastapi-<env>, host <env>.api.devopsyouss.com, bornes HPA, parentRef Gateway partagĂ© (ns fastapi)
└── frontend/{dev,staging,prod}/kustomization.yaml  → resources:[../../../frontend] + patchs : namespace frontend-<env>, host <env>.app.devopsyouss.com, replicas
(instanciĂ©s par les ApplicationSet fastapi/frontend ; tag image encore hĂ©ritĂ© du base, reciblage write-back → overlays/dev = #139)

Le namespace fastapi héberge désormais le Gateway partagé + le RBAC user CI (ns
plateforme, un seul NLB pour les 3 envs) ; les workloads applicatifs vivent dans
fastapi-<env>/frontend-<env>. Ces namespaces (+ labels PSA enforce restricted) ne
sont PAS dans les overlays :
ils sont créés par le bootstrap Ansible (cluster-admin). Le user CI deploy en
least-privilege ne peut pas patcher un objet cluster-scoped (INC-045).

✅ Les NetworkPolicy sont enforced sur EKS depuis #55 : vpc-cni dĂ©clarĂ© en addon
EKS managé avec enableNetworkPolicy=true (résout INC-046). Validé par test négatif
(egress port 80 → timeout) + test positif (/healthz/ready → 200). Voir validation-runbook.md.

✅ L'exposition (GatewayClass + Gateway + Certificate) est posĂ©e par le bootstrap
Ansible, pas par le deploy CI (#60, ADR 010). Raison : GatewayClass cluster-scoped +
CRDs Gateway API / cert-manager non couverts par "edit" (mĂȘmes classes qu'INC-045/047/048).
Le HTTPRoute reste owné par l'app (k8s/base) avec son binding RBAC explicite
fastapi-deploy-httproute, pour faire évoluer les routes (canary, blueprints) sans re-bootstrap.

k8s/platform/  (exposition + GitOps — appliquĂ©s par le bootstrap Ansible, cluster-admin)
├── gateway.yaml                    → EnvoyProxy nlb-config (annotation NLB, #70 ADR 017) + GatewayClass envoy (parametersRef) + Gateway fastapi-gateway (listeners 80/443, TLS Terminate, #60 ; allowedRoutes from:All pour routes cross-ns, #76)
├── certificate.yaml                → Certificate wildcard *.devopsyouss.com (cert-manager DNS-01, secret wildcard-devopsyouss-tls, #76)
├── argocd-apps/                    → App-of-apps ArgoCD (#72, ADR 013)
│   ├── root-app.yaml               → Application "apps" (root, surveille argocd-apps/, auto-sync)
│   ├── fastapi-appset.yaml         → ApplicationSet "fastapi" (gĂ©nĂ©rateur liste dev/staging/prod → 3 Applications sur k8s/overlays/fastapi/<env>, #134 ADR 029)
│   ├── frontend-appset.yaml        → ApplicationSet "frontend" (idem → k8s/overlays/frontend/<env>, #134)
│   ├── kube-prometheus-stack.yaml  → Application Helm multi-source, ServerSideApply, sync-wave 0 (#74, ADR 014)
│   ├── monitoring.yaml             → Application kustomize (ServiceMonitor + dashboards + datasource Loki), sync-wave 1 (#74/#75)
│   ├── loki.yaml                   → Application Helm Loki SingleBinary, ns logging, sync-wave 1 (#75, ADR 015)
│   └── alloy.yaml                  → Application Helm Grafana Alloy DaemonSet, ns logging, sync-wave 2 (#75, ADR 015)
├── monitoring/                     → ObservabilitĂ© mĂ©triques as-code (ns monitoring, #74, ADR 014)
│   ├── values.yaml                 → values kube-prometheus-stack (rĂ©tention 24h, no persistence, control-plane off)
│   ├── servicemonitor-fastapi.yaml → scrape /metrics du Service fastapi (interne ClusterIP, #81)
│   ├── datasource-loki.yaml        → datasource Loki pour Grafana (sidecar datasources, #75)
│   ├── secretstore.yaml            → ESO SecretStore ns monitoring (#76)
│   ├── externalsecret-grafana.yaml → Secret K8s grafana-admin depuis Secrets Manager (#76)
│   ├── httproute-grafana.yaml      → expose grafana.devopsyouss.com via le Gateway partagĂ© (#76)
│   ├── externalsecret-alertmanager-slack.yaml → Secret K8s alertmanager-slack (webhook Slack) depuis Secrets Manager (#77)
│   ├── prometheusrules-fastapi.yaml → 4 rùgles d'alerte FastAPI (target down, crashloop, 5xx, latence p99) (#77)
│   ├── kustomization.yaml          → ServiceMonitor + datasource + ESO + HTTPRoute + dashboards + alerting
│   └── dashboards/fastapi-red.json → dashboard FastAPI RED, provisionnĂ© par le sidecar Grafana
└── logging/                        → Logs as-code (ns logging, #75, ADR 015)
    ├── loki-values.yaml            → values Loki SingleBinary (filesystem, rĂ©tention 24h, no cache/gateway/minio)
    └── alloy-values.yaml           → values Alloy (DaemonSet, tail /var/log/pods, push vers Loki, labels low-cardinality)

k8s/overlays/
├── ingress-alb/        → ALB Ingress Controller (option A, Ă©cartĂ©e #34)
└── local-kind/         → lab local (postgres en pod + ns baseline), dev only (#46)

✅ L'observabilitĂ© (#74, ADR 014) est dĂ©ployĂ©e en GitOps par ArgoCD, pas par le bootstrap : kube-prometheus-stack (Prometheus Operator + Grafana + Alertmanager + node-exporter + kube-state-metrics) via une Application Helm multi-source (chart upstream + values dans le repo). Le ServiceMonitor scrape /metrics en interne (Service ClusterIP, jamais public, #81). Dashboards as-code (FastAPI RED + mixins cluster du chart). RĂ©tention 24h sans PVC (cluster Ă©phĂ©mĂšre). Grafana en port-forward (exposition publique = #76). Pas d'ELB créé : teardown via cascade du root-app, rien d'anti-INC-016 Ă  ajouter.

✅ Les logs (#75, ADR 015) sont centralisĂ©s via Loki (SingleBinary, filesystem, rĂ©tention 24h, ns logging) + Grafana Alloy (DaemonSet, tail /var/log/pods, push vers Loki, labels low-cardinality namespace/pod/container). Alloy remplace Promtail (dĂ©prĂ©ciĂ©). Datasource Loki dĂ©clarĂ©e cĂŽtĂ© Grafana via le sidecar datasources (ConfigMap labellisĂ©e dans ns monitoring). CorrĂ©lation mĂ©triques ↔ logs depuis le dashboard RED via Grafana Explore. DĂ©ployĂ© en GitOps (2 Applications ArgoCD), pas par le bootstrap.

✅ Mot de passe admin Grafana gĂ©rĂ© via ESO (#76) : secret dĂ©diĂ© fastapi-eks/grafana dans Secrets Manager (gĂ©nĂ©rĂ© par Terraform), lu par un ExternalSecret (ns monitoring) → Secret K8s grafana-admin, consommĂ© par grafana.admin.existingSecret. Remplace le mot de passe par dĂ©faut du chart (prĂ©requis avant l'exposition publique). Le rĂŽle IRSA de l'ESO est Ă©tendu en least-privilege au seul ARN supplĂ©mentaire.

✅ Grafana exposĂ© sur https://grafana.devopsyouss.com (#76) en rĂ©utilisant TOUTE la stack d'exposition du Sprint 3 (preuve de rĂ©utilisabilitĂ©, ADR 010) : une HTTPRoute (ns monitoring) attachĂ©e au Gateway partagĂ© via allowedRoutes.from:All (routage cross-namespace), un Certificate wildcard *.devopsyouss.com (cert-manager DNS-01 Cloudflare, couvre api + grafana + futurs sous-domaines), ExternalDNS qui crĂ©e le CNAME automatiquement. La route est owned par les manifests (ADR 010), le Gateway/Certificate par le bootstrap. Teardown : HTTPRoute supprimĂ©e avant l'uninstall d'Envoy (anti-INC-016).

✅ Alerting (#77, ADR 016) : 4 PrometheusRules FastAPI (target down, CrashLoop, taux de 5xx, latence p99) dĂ©couvertes par Prometheus, routĂ©es par Alertmanager vers Slack selon la sĂ©vĂ©ritĂ© (critical rĂ©pĂ©tĂ© plus souvent, inhibition critical → warning, Watchdog vers un receiver null pour ne pas spammer). L'URL du webhook Slack n'est jamais en clair dans Git : secret dĂ©diĂ© fastapi-eks/alertmanager-slack (Secrets Manager) → ESO → Secret K8s alertmanager-slack montĂ© dans Alertmanager, lu via api_url_file. RĂŽle IRSA de l'ESO Ă©tendu en least-privilege au seul ARN supplĂ©mentaire (3e secret). Tout en GitOps (rules + values), zĂ©ro modif bootstrap.


IAM — Least Privilege

gitlab_ci :
→ ECR push uniquement (app pipeline)
→ ecr:GetAuthorizationToken, BatchCheck, InitiateUpload...

gitlab_ci_infra :
→ terraform_ci policy (ec2:*, eks:*, rds:*, iam:limited + OIDC provider + secretsmanager read pour IRSA #33)
→ infra_status policy (read-only EKS, EC2, RDS, ECR, ELB)

Coûts AWS (ephemeral 4h/jour)

NAT Gateway  : 0.045$/h × 4 = 0.18$/jour
EKS Control  : 0.10$/h  × 4 = 0.40$/jour
t3.medium    : 0.047$/h × 4 = 0.19$/jour
RDS micro    : 0.02$/h  × 4 = 0.08$/jour
Total        : ~0.85$/jour → ~17$/mois (20 jours)

Persistent (toujours) :
ECR          : ~0.01$/mois
IAM          : gratuit

État Sprint 3

✅ Terraform persistent/ephemeral
✅ Kubernetes manifests (base + overlays)
✅ Envoy Gateway opĂ©rationnel
✅ FastAPI + RDS validĂ© sur EKS
✅ Pipeline build/scan/promote
✅ Pipeline infra start/stop/status
✅ IAM Least Privilege (2 users)
✅ Schedule nightly destroy
✅ Stage deploy CI/CD validĂ© end-to-end via kustomize (#52)
✅ PSA enforce restricted effectif sur EKS (#48, INC-045)

✅ NetworkPolicy enforced via VPC CNI managĂ© (#55, INC-046)
❌ ALB Ingress Controller (#34) — Ă©cartĂ©, direction gateway-api actĂ©e pour portabilitĂ© multi-cloud
✅ HTTPS public api.devopsyouss.com — cert-manager + Let's Encrypt DNS-01 Cloudflare (#28)
🚧 ExternalDNS Cloudflare (#66) — code livrĂ©, validation au prochain aws-start (CNAME api.devopsyouss.com automatique)
🚧 Secrets Manager + ESO (#33) — MR-A→D livrĂ©es (secret persistent fastapi-eks/app, provider OIDC + rĂŽle IRSA ephemeral, install ESO via bootstrap, manifests SecretStore/ExternalSecret, deploy sans Secret impĂ©ratif). Sync ESO de fastapi-secrets validĂ©e live le 2026-05-29. Reste : valider le deploy app end-to-end via le pipeline develop, puis fermer #33

Commandes utiles

# AWS
awslogin                                    # ouvrir session aws-vault
aws sts get-caller-identity                 # vérifier l'identité
aws eks list-clusters                       # lister clusters EKS
aws ecr list-images --repository-name fastapi-eks/fastapi

# Kubernetes
kubectl get nodes
kubectl get all -n fastapi
kubectl logs -n fastapi -l app=fastapi -f
kubectl port-forward -n fastapi deployment/fastapi 8080:8080

# Terraform
cd terraform/ephemeral && terraform output  # voir outputs
cd terraform/persistent && terraform state list

# Pipeline infra
./aws-start.sh   # démarrer l'infra
./aws-stop.sh    # arrĂȘter l'infra