Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches:
- main
- dev
# Filet de sécurité anti-boucle : le commit du job "deploy" ne touche que ce fichier.
# (Un push fait avec GITHUB_TOKEN ne déclenche déjà aucun workflow.)
paths-ignore:
- '_k8s/overlays/*/commit-patch.yaml'
pull_request:
branches:
- main
Expand Down Expand Up @@ -81,3 +85,79 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

deploy:
name: Trigger ArgoCD redeploy
needs: build
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
permissions:
contents: write
concurrency:
group: deploy-${{ github.ref_name }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}

- name: Résoudre l'overlay cible
id: overlay
run: |
case "${{ github.ref_name }}" in
main) echo "path=_k8s/overlays/prod" >> "$GITHUB_OUTPUT" ;;
dev) echo "path=_k8s/overlays/dev" >> "$GITHUB_OUTPUT" ;;
*) echo "Branche non gérée : ${{ github.ref_name }}" >&2; exit 1 ;;
esac

- name: Écrire l'ENV COMMIT dans le patch kustomize
env:
PATCH: ${{ steps.overlay.outputs.path }}/commit-patch.yaml
BRANCH: ${{ github.ref_name }}
run: |
SHORT_SHA="${GITHUB_SHA::7}"
cat > "$PATCH" <<YAML
# Généré par la CI (job "deploy") à chaque push sur ${BRANCH}.
# La modification de cette ENV var change le pod template -> ArgoCD redéploie
# sans changer le tag de l'image.
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp
spec:
template:
spec:
containers:
- name: demoapp
env:
- name: COMMIT
value: "${SHORT_SHA}"
YAML
cat "$PATCH"

- name: Commit & push (sans redéclencher de workflow)
env:
PATCH: ${{ steps.overlay.outputs.path }}/commit-patch.yaml
BRANCH: ${{ github.ref_name }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

if git diff --quiet -- "$PATCH"; then
echo "COMMIT déjà à jour, rien à pousser."
exit 0
fi

git add "$PATCH"
git commit -m "[skip ci] [Claude] chore: COMMIT=${GITHUB_SHA::7} (${BRANCH})"

for attempt in 1 2 3; do
if git push origin "HEAD:${BRANCH}"; then
echo "Poussé (tentative ${attempt})."
exit 0
fi
echo "Push refusé, rebase puis nouvelle tentative..."
git pull --rebase origin "${BRANCH}"
done
echo "Échec du push après 3 tentatives." >&2
exit 1
19 changes: 16 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ cannot serve it.
returns 503 so Kubernetes pulls the pod out of rotation.
- Container runs as uid 10001 with `readOnlyRootFilesystem: true`. Any code writing to disk
breaks the deployment — keep `PYTHONDONTWRITEBYTECODE=1`.
- All config comes from env vars: `REDIS_URL`, `COUNTER_KEY`, `TZ`, `PORT`. No hardcoded values.
- All config comes from env vars: `REDIS_URL`, `COUNTER_KEY`, `TZ`, `PORT`, `COMMIT`. No
hardcoded values.
- `_k8s/overlays/*/commit-patch.yaml` is **CI-generated** — do not hand-edit it, the `deploy` job
overwrites the file on every push.

## Branches and deployment

| Branch | Image tag | Overlay | Namespace | Hostname |
|---|---|---|---|---|
| `dev` | `:dev` | `_k8s/overlays/dev` | `demoapp-dev` | `dev.demoapp.k8s.lab.ops.nc` |
| `dev` | `:dev` | `_k8s/overlays/dev` | `demoapp-dev` | `demoapp-dev.k8s.lab.ops.nc` |
| `main` | `:main` | `_k8s/overlays/prod` | `demoapp` | `demoapp.k8s.lab.ops.nc` |

Work on `dev`. Reach `main` through a PR. Hostnames are written literally in each overlay's
Expand All @@ -68,5 +71,15 @@ Never mark k8s or Docker work done without rendering the manifests or running th
## Known limitations

- Redis uses an `emptyDir`: the counter resets when the pod restarts.
- Image tags are mutable per branch, so a new build needs `kubectl rollout restart` to land.
- Image tags are mutable per branch. Redeployment is triggered by the `deploy` job bumping the
`COMMIT` env var, which changes the pod template and makes ArgoCD roll the pods. That commit
must never retrigger CI: it relies on `GITHUB_TOKEN` pushes not firing workflows, plus
`[skip ci]` and `paths-ignore`. Keep all three if you touch the workflow.

## Gotcha: commit messages

GitHub scans the whole commit message — subject *and* body — for the skip marker (`skip ci`
in square brackets). Never write that literal sequence in a commit message when describing
this mechanism, or your push silently produces no CI run. Referring to it inside files
(README, workflow YAML) is safe; only the commit message matters.
- `/api/info` returns the server date in the server locale; the browser reformats it in French.
42 changes: 28 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ compteur de vues persisté dans **Redis** (sans authentification).
| `COUNTER_KEY` | `demoapp:views` | Clé du compteur |
| `TZ` | `Pacific/Noumea` | Timezone affichée |
| `PORT` | `8000` | Port d'écoute |
| `COMMIT` | `unknown` | SHA court affiché sous l'IP ; réécrit par la CI à chaque déploiement |

## Lancer en local

Expand Down Expand Up @@ -64,8 +65,9 @@ Le workflow `.github/workflows/build.yml` se déclenche sur `dev` et `main` :
la branche comme tag** (plus un tag court de SHA) :
- branche `dev` → `ghcr.io/ops-nc/demoapp:dev`
- branche `main` → `ghcr.io/ops-nc/demoapp:main`
3. **Job `deploy`** — déclenche le redéploiement ArgoCD (voir ci-dessous).

Sur pull request, seul le job `test` est exécuté (pas de push d'image).
Sur pull request, seuls les jobs `test` s'exécutent (ni push d'image, ni commit).

---

Expand All @@ -78,7 +80,7 @@ deux overlays. Chaque overlay surcharge le hostname de l'HTTPRoute via un patch
```
_k8s/
├── base/ deployment · service · httproute · redis
├── overlays/dev/ → dev.demoapp.k8s.lab.ops.nc, ns demoapp-dev, image :dev, 1 replica
├── overlays/dev/ → demoapp-dev.k8s.lab.ops.nc, ns demoapp-dev, image :dev, 1 replica
├── overlays/prod/ → demoapp.k8s.lab.ops.nc, ns demoapp, image :main, 2 replicas
└── argocd/ Applications ArgoCD prêtes à appliquer
```
Expand All @@ -89,7 +91,7 @@ L'HTTPRoute est rattachée à la Gateway existante `main-gateway`
## Vérifier le rendu avant de déployer

```bash
kubectl kustomize _k8s/overlays/dev | grep -A1 hostnames # dev.demoapp.k8s.lab.ops.nc
kubectl kustomize _k8s/overlays/dev | grep -A1 hostnames # demoapp-dev.k8s.lab.ops.nc
kubectl kustomize _k8s/overlays/prod | grep -A1 hostnames # demoapp.k8s.lab.ops.nc
```

Expand All @@ -99,7 +101,7 @@ Les manifests fournis pointent chacun vers une branche et un overlay différents

| Application | Branche suivie | Path | Namespace | URL |
|---|---|---|---|---|
| `demoapp-dev` | `dev` | `_k8s/overlays/dev` | `demoapp-dev` | https://dev.demoapp.k8s.lab.ops.nc |
| `demoapp-dev` | `dev` | `_k8s/overlays/dev` | `demoapp-dev` | https://demoapp-dev.k8s.lab.ops.nc |
| `demoapp` | `main` | `_k8s/overlays/prod` | `demoapp` | https://demoapp.k8s.lab.ops.nc |

```bash
Expand All @@ -109,23 +111,35 @@ kubectl apply -f _k8s/argocd/application-prod.yaml

Les deux Applications sont en `automated` (prune + selfHeal) avec `CreateNamespace=true`.

## Cycle de déploiement
## Redéploiement automatique (job `deploy`)

1. Push sur `dev` → CI publie `ghcr.io/ops-nc/demoapp:dev` → ArgoCD resynchronise `demoapp-dev`.
2. Merge `dev` → `main` → CI publie `:main` → ArgoCD resynchronise `demoapp`.
Le tag d'image est fixe par branche : un nouveau push ne modifie donc aucun manifest, et ArgoCD
n'a rien à resynchroniser. Le job `deploy` résout ce problème **sans changer de tag** — il réécrit
l'ENV var `COMMIT` du Deployment avec le SHA court dans
`_k8s/overlays/<env>/commit-patch.yaml`, puis commite sur la branche courante.

Le tag d'image étant fixe par branche (`imagePullPolicy: Always`), forcer le redéploiement après
un nouveau build se fait via :
Modifier une ENV var change le pod template : ArgoCD voit la dérive, sync, et Kubernetes fait un
rolling update. Comme `imagePullPolicy: Always`, les nouveaux pods tirent l'image fraîche du
même tag. Le SHA déployé est affiché en petit sous l'IP sur la page.

```
push sur dev → test → build & push :dev → commit "COMMIT=<sha>" → ArgoCD sync → rollout
```

**Le commit de la CI ne redéclenche aucun workflow**, via trois protections :

1. Un push authentifié avec le `GITHUB_TOKEN` ne déclenche pas de workflow (garantie GitHub).
2. Le message de commit porte `[skip ci]`.
3. `on.push.paths-ignore` exclut `_k8s/overlays/*/commit-patch.yaml`.

Mapping branche → overlay patché : `dev` → `overlays/dev`, `main` → `overlays/prod`.

Redéploiement manuel si besoin :

```bash
kubectl rollout restart deploy/demoapp -n demoapp-dev
# ou, côté ArgoCD
argocd app actions run demoapp-dev restart --kind Deployment
```

> Pour un suivi automatique des nouvelles images, brancher **ArgoCD Image Updater** ou passer la
> CI en tag immuable (SHA) avec commit du tag dans l'overlay.

## Notes

- Le Redis déployé utilise un `emptyDir` : le compteur repart à zéro si le pod redémarre.
Expand Down
15 changes: 15 additions & 0 deletions _k8s/overlays/dev/commit-patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Généré par la CI (job "deploy") à chaque push sur dev.
# La modification de cette ENV var change le pod template -> ArgoCD redéploie
# sans changer le tag de l'image.
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp
spec:
template:
spec:
containers:
- name: demoapp
env:
- name: COMMIT
value: "2c008d5"
2 changes: 1 addition & 1 deletion _k8s/overlays/dev/httproute-patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ metadata:
name: demoapp
spec:
hostnames:
- dev.demoapp.k8s.lab.ops.nc
- demoapp-dev.k8s.lab.ops.nc
4 changes: 3 additions & 1 deletion _k8s/overlays/dev/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ replicas:
- name: demoapp
count: 1

# hostname de l'HTTPRoute : dev.demoapp.k8s.lab.ops.nc
# httproute-patch : hostname demoapp-dev.k8s.lab.ops.nc
# commit-patch : ENV COMMIT, réécrit par la CI pour forcer le redéploiement ArgoCD
patches:
- path: httproute-patch.yaml
- path: commit-patch.yaml
15 changes: 15 additions & 0 deletions _k8s/overlays/prod/commit-patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Généré par la CI (job "deploy") à chaque push sur main.
# La modification de cette ENV var change le pod template -> ArgoCD redéploie
# sans changer le tag de l'image.
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp
spec:
template:
spec:
containers:
- name: demoapp
env:
- name: COMMIT
value: "initial"
4 changes: 3 additions & 1 deletion _k8s/overlays/prod/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ replicas:
- name: demoapp
count: 2

# hostname de l'HTTPRoute : demoapp.k8s.lab.ops.nc
# httproute-patch : hostname demoapp.k8s.lab.ops.nc
# commit-patch : ENV COMMIT, réécrit par la CI pour forcer le redéploiement ArgoCD
patches:
- path: httproute-patch.yaml
- path: commit-patch.yaml
11 changes: 9 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

import redis
from asgiref.wsgi import WsgiToAsgi
from flask import Flask, jsonify, render_template
from flask import Flask, jsonify, make_response, render_template

REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
COUNTER_KEY = os.environ.get("COUNTER_KEY", "demoapp:views")
TZ_NAME = os.environ.get("TZ", "Pacific/Noumea")
COMMIT = os.environ.get("COMMIT", "unknown")

app = Flask(__name__)
redis_client = redis.Redis.from_url(REDIS_URL, decode_responses=True)
Expand Down Expand Up @@ -52,7 +53,7 @@ def index():
hostname, ip = get_host_info()
time_str, date_str, tz_label, offset = get_time()
views = incr_views()
return render_template(
page = render_template(
"index.html",
time=time_str,
date=date_str,
Expand All @@ -61,7 +62,12 @@ def index():
hostname=hostname,
ip=ip,
views=views,
commit=COMMIT,
)
# Le compteur doit s'incrémenter à chaque visite : pas de cache navigateur.
response = make_response(page)
response.headers["Cache-Control"] = "no-store"
return response


@app.route("/api/info")
Expand All @@ -77,6 +83,7 @@ def api_info():
hostname=hostname,
ip=ip,
views=get_views(),
commit=COMMIT,
)


Expand Down
11 changes: 11 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@
word-break: break-all;
}

.commit {
margin-top: 0.45rem;
font-size: 0.68rem;
letter-spacing: 0.04em;
color: #6b748c;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}

.commit span { color: #97a1b8; }

.badge {
display: inline-block;
margin-top: 0.9rem;
Expand Down Expand Up @@ -168,6 +178,7 @@ <h1>DemoApp</h1>
<div class="label">Machine</div>
<div class="value mono" id="hostname">{{ hostname }}</div>
<div class="sub" id="ip">{{ ip }}</div>
<div class="commit">commit <span id="commit">{{ commit }}</span></div>
<span class="badge">Adresse IP interne</span>
</section>

Expand Down