A les dues lliçons anteriors vam deixar botiga-web, api-reserves i postgres-reserves funcionant en producció. Els manifests són correctes, les sondes responen, les còpies es restauren. Falta la pregunta que fa que tot això sigui sostenible: com hi arriba el codi que escriu un desenvolupador un dimarts al matí?
Aquesta lliçó recorre aquesta distància completa: des del git push fins al pod que atén peticions a rutas-norte-pro. No és una lliçó sobre GitOps, que ja vam veure a 10-05, ni sobre signatura d'imatges, que vam veure a 08-05: és la lliçó on totes aquestes peces es connecten en una canalització que funciona, i on s'explica per què la canalització de Rutas Norte mai no executa kubectl apply.
Al final mesurarem si tot això serveix per a alguna cosa, perquè una canalització elaborada que no redueix el temps entre escriure codi i veure'l funcionar és un projecte d'enginyeria interna disfressat de millora.
Contingut
- El flux complet de Rutas Norte
- Integració contínua i lliurament continu: per què separar-los
- La canalització d'integració, pas a pas
- Proves d'integració contra un clúster kind efímer
- El lliurament: actualitzar el repositori de manifests
- Promoció a
prei aproamb aprovació humana - Entorns efímers per petició de canvi
- Credencials sense secrets de llarga vida
- Verificació posterior al desplegament i reversió automàtica
- Mètriques DORA: els números de Rutas Norte abans i després
- El flux complet de Rutas Norte
Hi ha dos repositoris, i aquesta separació és l'eix de tota la resta:
| Repositori | Conté | Qui el modifica |
|---|---|---|
rutasnorte/api-reserves |
Codi font, Dockerfile, proves, definició de la canalització |
Desenvolupament, a mà |
rutasnorte/manifests |
Chart de Helm, superposicions de Kustomize, Application d'Argo CD |
La canalització (dev) i persones (pre/pro) |
graph TB
DEV[Desenvolupador<br/>git push a branca] --> PR[Petició de canvi]
PR --> CI
subgraph CI["Integració contínua · GitHub Actions"]
T1[1 Lint + proves unitàries]
T2[2 Construcció multietapa<br/>amb memòria cau de capes]
T3[3 Trivy: falla si CRITICAL]
T4[4 Push al registre<br/>etiqueta = SHA del commit]
T5[5 Cosign: signatura sense claus]
T6[6 Obtenir digest]
T7[7 Proves en clúster kind]
T1 --> T2 --> T3 --> T4 --> T5 --> T6 --> T7
end
CI --> EF[Entorn efímer<br/>ns rutas-norte-pr-1842]
T7 --> MERGE{Fusió a main}
MERGE --> BOT[Petició de canvi automàtica<br/>a rutasnorte/manifests<br/>digest a overlays/dev]
BOT --> AUTO[Fusió automàtica]
AUTO --> ACD[Argo CD]
ACD --> DEVENV[rutas-norte-dev]
DEVENV --> HUMO1[Proves de fum]
HUMO1 --> PROMO_PRE[PC de promoció a pre<br/>aprovació: desenvolupament]
PROMO_PRE --> ACD2[Argo CD] --> PREENV[rutas-norte-pre]
PREENV --> CARGA[k6 + proves de regressió]
CARGA --> PROMO_PRO[PC de promoció a pro<br/>aprovació: plataforma + producte]
PROMO_PRO --> ACD3[Argo CD] --> PROENV[rutas-norte-pro]
PROENV --> VERIF[Verificació posterior<br/>reversió automàtica si falla]
Fixa't en una cosa important: la fletxa que entra als namespaces sempre surt d'Argo CD, mai de la canalització. La canalització arriba fins al repositori de manifests i allà s'atura.
- Integració contínua i lliurament continu: per què separar-los
S'anomenen junts i es confonen constantment, però responen a preguntes diferents i fallen de maneres diferents.
| Integració contínua | Lliurament continu | |
|---|---|---|
| Pregunta que respon | Aquest canvi és correcte? | És aquest artefacte als entorns? |
| Entrada | Un commit | Un artefacte verificat (digest) |
| Sortida | Una imatge signada i el seu digest | Un estat del clúster |
| On viu la lògica | GitHub Actions | Repositori de manifests + Argo CD |
| Naturalesa | Imperativa: passos en ordre | Declarativa: estat desitjat |
| Si falla | No surt artefacte; ningú no se n'assabenta fora de l'equip | Els entorns divergeixen; sí que es nota |
| Freqüència | Cada push | Cada canvi del repositori de manifests |
2.1. Per què la canalització no executa kubectl apply
És la decisió de disseny més important d'aquesta lliçó, i convé entendre-la bé perquè contradiu el que fa molta gent.
Amb kubectl apply des de la canalització:
- El corredor de la canalització necessita credencials d'escriptura sobre producció. Qualsevol que pugui modificar el fitxer de la canalització pot modificar producció.
- L'estat real del clúster depèn de quines execucions han passat i en quin ordre. Si dues se solapen, el resultat és indeterminat.
- Un canvi manual amb
kubectl edita les tres de la matinada durant un incident no es reverteix sol, i ningú no s'assabenta que el clúster ja no coincideix amb Git. - Saber què hi ha desplegat exigeix mirar el clúster, no el repositori.
- Reconstruir l'entorn després d'un desastre significa tornar a executar canalitzacions antigues, si és que encara funcionen.
Amb GitOps (10-05):
- La canalització només necessita permís per obrir una petició de canvi en un repositori de Git. Zero credencials de clúster.
- L'estat desitjat és una revisió de Git: reproduïble, revisable, amb historial i amb autor.
- La deriva es corregeix sola gràcies a
selfHeal, i queda visible com un esdeveniment de desincronització. - La reversió és
git revert. - L'auditoria de "qui va canviar què en producció i quan" és el registre de Git, no un rastre dispers de treballs de CI.
La regla, en una frase: la integració produeix un digest; el lliurament decideix en quin entorn viu aquest digest; només Git uneix totes dues coses.
- La canalització d'integració, pas a pas
Fitxer .github/workflows/integracio.yml del repositori rutasnorte/api-reserves, complet i comentat.
name: Integració contínua
on:
push:
branches: [main]
pull_request:
# Sense secrets de llarga vida: id-token permet demanar un token OIDC
# efímer per autenticar-se contra el núvol i signar amb Cosign.
permissions:
contents: read
packages: write
id-token: write
pull-requests: write
env:
REGISTRE: registry.rutasnorte.example
IMATGE: rutasnorte/api-reserves
# Cancel·la execucions antigues de la mateixa branca: no gastem corredors
# validant commits que ja han quedat obsolets.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# ---------------------------------------------------------------
# 1. Proves unitàries: ràpides, sense xarxa, sense base de dades real.
# ---------------------------------------------------------------
proves:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
- name: Instal·lar dependències exactes
# npm ci respecta el fitxer de bloqueig: reproduïble, a diferència de npm install.
run: npm ci
- name: Anàlisi estàtica
run: npm run lint
- name: Proves unitàries amb cobertura
run: npm test -- --coverage --coverageThreshold='{"global":{"lines":80}}'
- name: Auditoria de dependències
run: npm audit --audit-level=high
# ---------------------------------------------------------------
# 2-6. Construcció, escaneig, publicació i signatura.
# ---------------------------------------------------------------
imatge:
needs: proves
runs-on: ubuntu-latest
outputs:
# El digest és la sortida que consumeixen els treballs següents
# i, finalment, el repositori de manifests.
digest: ${{ steps.construir.outputs.digest }}
etiqueta: ${{ steps.meta.outputs.etiqueta }}
steps:
- uses: actions/checkout@v4
- name: Calcular etiqueta a partir del commit
id: meta
run: |
# SHA curt: identificador únic, traçable i ordenable.
ETIQUETA="$(git rev-parse --short=12 HEAD)"
echo "etiqueta=${ETIQUETA}" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v3
- name: Autenticar-se al registre per OIDC
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRE }}
username: ${{ github.actor }}
# Token efímer de la mateixa execució, no una credencial desada.
password: ${{ secrets.GITHUB_TOKEN }}
- name: Construir imatge multietapa
id: construir
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ env.REGISTRE }}/${{ env.IMATGE }}:${{ steps.meta.outputs.etiqueta }}
# Memòria cau de capes al mateix registre: una construcció típica
# baixa de 4 min a uns 50 s reutilitzant node_modules.
cache-from: type=registry,ref=${{ env.REGISTRE }}/${{ env.IMATGE }}:cache
cache-to: type=registry,ref=${{ env.REGISTRE }}/${{ env.IMATGE }}:cache,mode=max
provenance: true # atestació de procedència SLSA
sbom: true # inventari de components
- name: Escaneig amb Trivy
uses: aquasecurity/[email protected]
with:
image-ref: ${{ env.REGISTRE }}/${{ env.IMATGE }}@${{ steps.construir.outputs.digest }}
format: table
# Trenca la construcció només amb CRITICAL. Les HIGH es registren
# i es revisen setmanalment: si trenques amb HIGH, l'equip aprèn
# a saltar-se el control en lloc d'arreglar-lo.
severity: CRITICAL
exit-code: '1'
ignore-unfixed: true # sense pedaç disponible no hi ha res a fer avui
- name: Instal·lar Cosign
uses: sigstore/cosign-installer@v3
- name: Signar sense claus amb la identitat del flux de treball
# No hi ha clau privada enlloc: Cosign obté un certificat
# efímer de Fulcio a partir del token OIDC d'aquesta execució i
# registra la signatura al log públic Rekor. La identitat signant és
# el mateix flux de treball, verificable a la política d'admissió.
run: |
cosign sign --yes \
"${REGISTRE}/${IMATGE}@${{ steps.construir.outputs.digest }}"
- name: Resum de l'execució
run: |
{
echo "### Imatge publicada"
echo "- Etiqueta: \`${{ steps.meta.outputs.etiqueta }}\`"
echo "- Digest: \`${{ steps.construir.outputs.digest }}\`"
echo "- Signada per: \`${{ github.workflow_ref }}\`"
} >> "$GITHUB_STEP_SUMMARY"3.1. Les decisions que importen
Etiquetatge per hash de commit i no per versió semàntica. L'etiqueta a3f91c2b8e04 és única, immutable i respon a "quin codi exacte és això?" sense ambigüitat. Les versions semàntiques es reserven per a les publicacions anunciades al negoci, que es creen com a etiqueta addicional sobre un digest ja existent.
El digest com a sortida real. L'etiqueta serveix per a les persones. El que viatja al repositori de manifests és el digest, perquè una etiqueta es pot reescriure i un digest no. És el que sosté la referència per digest dels Deployments d'11-01.
Trivy amb severity: CRITICAL i ignore-unfixed: true. És una decisió calibrada. Trencar amb HIGH genera tantes falses alarmes que l'equip acaba afegint excepcions per sistema; i fallar per una vulnerabilitat sense pedaç disponible no aporta res, perquè no hi ha acció possible. Les HIGH es revisen a la reunió setmanal de plataforma.
Signatura sense claus. No existeix cap clau privada per rotar, custodiar o filtrar. El certificat és efímer i la identitat signant és el flux de treball concret. La política d'admissió de 08-05 exigeix exactament aquesta identitat:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: exigir-signatura-rutasnorte
spec:
validationFailureAction: Enforce
rules:
- name: verificar-signatura
match:
any:
- resources:
kinds: [Pod]
namespaces: ["rutas-norte-*"]
verifyImages:
- imageReferences: ["registry.rutasnorte.example/rutasnorte/*"]
attestors:
- entries:
- keyless:
subject: "https://github.com/rutasnorte/*/.github/workflows/integracio.yml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"L'efecte pràctic és contundent: una imatge construïda al portàtil d'algú, per molt correcta que sigui, no es pot executar en cap namespace de Rutas Norte.
3.2. La construcció multietapa amb memòria cau
# syntax=docker/dockerfile:1.7
# --- Etapa 1: dependències --------------------------------------
FROM node:22-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
# La memòria cau d'npm es munta, no es copia: no engreixa cap capa.
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
# --- Etapa 2: construcció ---------------------------------------
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
# --- Etapa 3: imatge final --------------------------------------
FROM gcr.io/distroless/nodejs22-debian12:nonroot
WORKDIR /app
# Només l'imprescindible: sense compiladors, sense shell, sense gestor de paquets.
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER 10001:10001
EXPOSE 8080 9090
CMD ["dist/server.js"]Copiar primer package.json i package-lock.json i després la resta del codi no és un caprici d'estil: fa que la capa de dependències només s'invalidi quan canvien les dependències. Com que el 95 % dels commits només toquen codi, la memòria cau encerta gairebé sempre. La imatge final resultant pesa 118 MB davant dels 1,1 GB d'una construcció d'una sola etapa sobre node:22, i no conté shell, cosa que redueix dràsticament el que un atacant pot fer dins del contenidor.
- Proves d'integració contra un clúster kind efímer
Les proves unitàries no detecten el que trenca de debò: un ConfigMap amb una clau mal escrita, una sonda que apunta a un port que no existeix, un chart de Helm amb un valor obligatori sense definir. Això només ho detecta desplegar de debò.
La canalització aixeca un clúster kind (10-01) dins del mateix corredor, desplega el chart i executa proves de fum.
integracio:
needs: imatge
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Aixecar clúster kind
uses: helm/kind-action@v1
with:
cluster_name: ci-rutasnorte
# Un node n'hi ha prou: provem funcionament correcte, no alta disponibilitat.
config: .github/kind/cluster.yaml
wait: 120s
- name: Desplegar dependències reals
# PostgreSQL i Redis de debò, no simuladors: volem detectar
# errors d'SQL i de connexió, que és on fallen aquestes coses.
run: |
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install pg bitnami/postgresql \
--set auth.database=reserves \
--set auth.username=app_reserves \
--set auth.password=proves \
--set primary.persistence.enabled=false \
--wait --timeout 5m
helm install redis bitnami/redis \
--set auth.enabled=false \
--set master.persistence.enabled=false \
--wait --timeout 5m
- name: Carregar la imatge acabada de construir a kind
run: |
IMG="${REGISTRE}/${IMATGE}@${{ needs.imatge.outputs.digest }}"
docker pull "$IMG"
kind load docker-image "$IMG" --name ci-rutasnorte
- name: Desplegar el chart de Rutas Norte
run: |
helm upgrade --install api-reserves ./chart \
--values ./chart/valors-ci.yaml \
--set image.digest='${{ needs.imatge.outputs.digest }}' \
--set postgres.host=pg-postgresql \
--set redis.url=redis://redis-master:6379 \
--wait --timeout 5m
- name: Migracions d'esquema
run: kubectl wait --for=condition=complete job/api-reserves-migracions --timeout=3m
- name: Proves de fum
run: |
kubectl port-forward svc/api-reserves 8080:80 &
sleep 5
npm run proves:fum -- --base-url http://localhost:8080
- name: Diagnòstic si alguna cosa falla
if: failure()
run: |
kubectl get pods -o wide
kubectl describe pods -l app.kubernetes.io/name=api-reserves
kubectl logs -l app.kubernetes.io/name=api-reserves --tail=200 --all-containers
kubectl get events --sort-by=.lastTimestamp | tail -40Les proves de fum són deliberadament poques i molt significatives:
// proves/fum.spec.js — s'executen a kind i també després de cada desplegament real
describe('fum api-reserves', () => {
test('respon a la sonda de preparació', async () => {
const r = await fetch(`${base}/preparat`);
expect(r.status).toBe(200);
});
test('exposa mètriques en format Prometheus', async () => {
const t = await (await fetch(`${base}/metriques`)).text();
expect(t).toMatch(/^api_reserves_peticions_total/m);
});
test('consulta horaris contra la base de dades real', async () => {
const r = await fetch(`${base}/horaris?linia=BIL-SAN&data=2026-08-14`);
expect(r.status).toBe(200);
expect(Array.isArray((await r.json()).sortides)).toBe(true);
});
test('crea una reserva i és idempotent', async () => {
const cos = { linia: 'BIL-SAN', data: '2026-08-14', places: 2 };
const cap = { 'Idempotency-Key': 'prova-fum-001', 'Content-Type': 'application/json' };
const r1 = await fetch(`${base}/reserves`, { method: 'POST', headers: cap, body: JSON.stringify(cos) });
const r2 = await fetch(`${base}/reserves`, { method: 'POST', headers: cap, body: JSON.stringify(cos) });
expect(r1.status).toBe(201);
expect((await r2.json()).id).toBe((await r1.json()).id); // no duplica
});
});El cost d'aquest treball és d'uns tres minuts per execució. A canvi, en l'últim any ha detectat abans de fusionar: dues claus de ConfigMap reanomenades sense actualitzar el codi, una migració que no era reversible, un targetPort erroni després d'un canvi de port, i una sonda de preparació que retornava 200 abans que el pool de base de dades estigués a punt. Qualsevol d'aquestes quatre hauria estat un incident a dev com a mínim, i l'última podria haver arribat a producció.
- El lliurament: actualitzar el repositori de manifests
Quan la fusió a main acaba amb tot en verd, l'últim treball obre una petició de canvi a rutasnorte/manifests.
actualitzar-manifests:
needs: [imatge, integracio]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Clonar el repositori de manifests
uses: actions/checkout@v4
with:
repository: rutasnorte/manifests
# Token d'una app de GitHub limitada a aquest repositori,
# no un token personal amb accés a tota l'organització.
token: ${{ secrets.TOKEN_MANIFESTS }}
- name: Escriure el nou digest a la superposició de dev
run: |
cd overlays/dev
kustomize edit set image \
"api-reserves=${REGISTRE}/${IMATGE}@${{ needs.imatge.outputs.digest }}"
- name: Obrir petició de canvi i fusionar-la automàticament
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.TOKEN_MANIFESTS }}
branch: auto/api-reserves-${{ needs.imatge.outputs.etiqueta }}
commit-message: "dev: api-reserves ${{ needs.imatge.outputs.etiqueta }}"
title: "dev: api-reserves ${{ needs.imatge.outputs.etiqueta }}"
body: |
Actualització automàtica des de `rutasnorte/api-reserves`.
- Commit: ${{ github.sha }}
- Digest: `${{ needs.imatge.outputs.digest }}`
- Trivy: sense vulnerabilitats crítiques
- Signatura: verificada (sense claus)
- Proves d'integració a kind: correctes
labels: automatic,devA dev, aquesta petició es fusiona sola perquè una regla del repositori ho permet quan l'únic canvi afecta overlays/dev/kustomization.yaml. Tan bon punt es fusiona, Argo CD detecta el canvi i sincronitza. Entre el git push del desenvolupador i el pod nou corrent a rutas-norte-dev passen uns onze minuts.
Per què una petició de canvi i no un commit directe: deixa rastre revisable, permet que les regles de protecció de branca s'apliquin igual, i fa que revertir sigui un git revert amb context en lloc d'un commit orfe d'un bot.
- Promoció a
pre i a pro amb aprovació humana
pre i a pro amb aprovació humanaLa promoció és canviar el mateix digest de superposició. No es reconstrueix res: l'artefacte que es va validar a dev és exactament el que arriba a producció, byte a byte.
# Executat per una persona (o per un botó que fa això mateix)
cd manifests
DIGEST=$(yq '.images[] | select(.name=="api-reserves") | .digest' overlays/dev/kustomization.yaml)
git switch -c promocio/api-reserves-pre
cd overlays/pre
kustomize edit set image "api-reserves=registry.rutasnorte.example/rutasnorte/api-reserves@${DIGEST}"
git commit -am "pre: promocionar api-reserves ${DIGEST:0:19}"
gh pr create --title "pre: promocionar api-reserves" --body-file ../.github/plantilles/promocio.mdCada porta comprova coses diferents, i això és el que fa que la promoció sigui alguna cosa més que prémer un botó:
| Porta | Qui aprova | Què ha de comprovar abans |
|---|---|---|
dev → pre |
Un membre de l'equip desenvolupament |
Aplicació desplegada a dev sense reinicis durant 30 min; proves de fum en verd; sense alertes noves; migracions aplicades sense error |
pre → pro |
Un membre de plataforma i un de producte |
Proves de regressió completes a pre; prova de càrrega k6 amb el perfil del pont de maig dins dels llindars; migracions provades contra una còpia restaurada de producció; pla de reversió escrit; finestra de canvi oberta (no hi ha congelació activa) |
La plantilla de la petició de canvi de promoció a pro obliga a emplenar:
## Promoció a producció — api-reserves
- **Digest:** `sha256:...`
- **Canvis inclosos:** (enllaços a les peticions de canvi de codi)
- **Inclou migració d'esquema?** Sí / No — si sí, és compatible cap enrere?
- **Resultat de k6 a pre:** p95 = ___ ms (llindar 300 ms), errors = ___ % (llindar 0,1 %)
- **Risc estimat:** baix / mitjà / alt — justificació:
- **Pla de reversió:** `git revert <sha>` + temps estimat ___ min
- **Finestra:** hi ha congelació activa? Sí / No
- **Aprovacions:** plataforma @____ · producte @____La pregunta sobre compatibilitat cap enrere de la migració no és retòrica: és la que fa possible la reversió. Una migració que esborra una columna fa que revertir el codi sigui impossible sense restaurar la base de dades. A 11-04 veurem el patró d'expandir i contraure que ho resol.
- Entorns efímers per petició de canvi
Cada petició de canvi oberta a rutasnorte/api-reserves obté un namespace propi, amb la seva base de dades, el seu Ingress i la seva URL, i desapareix en tancar-la.
entorn-efimer:
if: github.event_name == 'pull_request'
needs: imatge
runs-on: ubuntu-latest
environment:
name: pr-${{ github.event.number }}
url: https://pr-${{ github.event.number }}.dev.rutasnorte.example
steps:
- uses: actions/checkout@v4
- name: Autenticar-se al clúster de desenvolupament per OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/ci-rutasnorte-efimers
aws-region: eu-west-1
- run: aws eks update-kubeconfig --name rutasnorte-dev
- name: Desplegar l'entorn
env:
NS: rutas-norte-pr-${{ github.event.number }}
run: |
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f -
# Etiquetes que governen quotes, polítiques i neteja automàtica.
kubectl label namespace "$NS" --overwrite \
rutasnorte.example/efimer=true \
rutasnorte.example/pr=${{ github.event.number }} \
rutasnorte.example/creat=$(date +%Y-%m-%d) \
pod-security.kubernetes.io/enforce=restricted
helm upgrade --install api-reserves ./chart \
--namespace "$NS" \
--values ./chart/valors-efimer.yaml \
--set image.digest='${{ needs.imatge.outputs.digest }}' \
--set ingress.host=pr-${{ github.event.number }}.dev.rutasnorte.example \
--wait --timeout 8m
- name: Comentar la URL a la petició de canvi
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.number }}
body: |
Entorn de proves a punt: https://pr-${{ github.event.number }}.dev.rutasnorte.example
Namespace: `rutas-norte-pr-${{ github.event.number }}`
netejar-efimer:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- run: kubectl delete namespace "rutas-norte-pr-${{ github.event.number }}" --ignore-not-foundAquest és l'únic punt de tota la canalització on s'executa kubectl contra un clúster, i està acotat deliberadament: només el clúster de desenvolupament, només namespaces amb el prefix rutas-norte-pr-, i amb un rol que no pot tocar res més.
La xarxa de seguretat: un CronJob nocturn esborra els namespaces efímers de més de set dies, perquè sempre hi ha peticions de canvi que es queden obertes i treballs de neteja que fallen.
apiVersion: batch/v1
kind: CronJob
metadata:
name: netejar-namespaces-efimers
namespace: plataforma
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: netejador-efimers
restartPolicy: OnFailure
containers:
- name: netejar
image: registry.rutasnorte.example/utils/kubectl:1.30
command:
- /bin/sh
- -c
- |
LIMIT=$(date -d '7 days ago' +%Y-%m-%d)
kubectl get ns -l rutasnorte.example/efimer=true \
-o jsonpath='{range .items[*]}{.metadata.name} {.metadata.labels.rutasnorte\.example/creat}{"\n"}{end}' |
while read -r NS CREAT; do
[ "$CREAT" \< "$LIMIT" ] && kubectl delete ns "$NS"
donePer què són la millor inversió d'un equip. Costa uns dos dies muntar això bé, i canvia la dinàmica de treball sencera:
- La revisió de codi passa de llegir un diff a usar la funcionalitat. Producte i disseny opinen sobre una cosa real, no sobre una captura.
- Els errors d'integració es detecten a la petició de canvi, no a
devdesprés de fusionar. - Desapareix la cua per l'entorn compartit: cinc canvis en paral·lel tenen cinc entorns.
- Es proven els manifests, no només el codi. Un chart mal parametritzat falla aquí.
El cost cal acotar-lo: quota estricta per namespace (03-04), una sola rèplica de cada cosa, base de dades petita sense persistència, i l'esborrat automàtic. A Rutas Norte, amb una mitjana de sis entorns efímers vius, això suposa uns 130 euros al mes: menys del que costa mitja hora de reunió per coordinar qui fa servir l'entorn de proves.
- Credencials sense secrets de llarga vida
L'objectiu és que no existeixi cap credencial permanent als secrets de la canalització. Cada accés s'autentica amb un token efímer emès per a aquella execució concreta.
sequenceDiagram participant W as Flux de treball participant G as Emissor OIDC de GitHub participant A as AWS STS participant K as Clúster EKS dev W->>G: Sol·licita token OIDC (permissions.id-token) G-->>W: JWT amb sub=repo:rutasnorte/api-reserves:ref:refs/heads/main W->>A: AssumeRoleWithWebIdentity(JWT) A->>A: Valida emissor, audiència i condició sobre sub A-->>W: Credencials temporals (1 h) W->>K: kubectl amb aquestes credencials K->>K: RBAC del rol mapejat: només ns rutas-norte-pr-*
La política de confiança del rol és on hi ha la seguretat real:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:rutasnorte/api-reserves:pull_request"
}
}
}]
}La condició sobre sub és el que impedeix que un altre repositori de l'organització, o una branca qualsevol, assumeixi aquest rol. Escriure-la com a repo:rutasnorte/*:* —error molt freqüent— equival a donar el rol a tota l'organització.
I l'RBAC del costat del clúster, mínim (08-01):
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: ci-efimers
rules:
# Pot crear i esborrar namespaces (necessari per als efímers)...
- apiGroups: [""]
resources: [namespaces]
verbs: [get, list, create, delete, patch]
# ...i gestionar càrregues dins d'ells.
- apiGroups: ["", apps, networking.k8s.io, batch, autoscaling]
resources: ["*"]
verbs: [get, list, create, update, patch, delete]
# Explícitament NO: nodes, clusterroles, clusterrolebindings,
# persistentvolumes ni secrets d'altres namespaces.Comparació de la superfície de risc:
| Enfocament | Què es filtra si es compromet el corredor | Caducitat |
|---|---|---|
| Kubeconfig desat com a secret | Accés permanent al clúster | Cap |
| Clau d'accés del núvol desada | Accés permanent al compte | Cap |
| Token personal d'un desenvolupador | Tot el que aquella persona pot fer | Mesos |
| OIDC federat | Un token d'1 hora, acotat per la condició sub |
1 hora |
- Verificació posterior al desplegament i reversió automàtica
Que Argo CD digui Synced i Healthy significa que els pods van arrencar, no que l'aplicació funcioni. La verificació posterior és la capa que falta.
# Hook de sincronització d'Argo CD: s'executa DESPRÉS de sincronitzar.
apiVersion: batch/v1
kind: Job
metadata:
name: fum-api-reserves
namespace: rutas-norte-pro
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 2
activeDeadlineSeconds: 300
template:
spec:
restartPolicy: Never
containers:
- name: fum
image: registry.rutasnorte.example/rutasnorte/proves-fum@sha256:e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4
env:
- name: BASE_URL
value: https://api.rutasnorte.example
- name: MODE
value: nomes-lectura # a pro no creem reserves realsSi el Job falla, Argo CD marca la sincronització com a fallida i, amb aquesta política, reverteix sola:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: api-reserves-pro
namespace: argocd
spec:
source:
repoURL: https://github.com/rutasnorte/manifests
path: overlays/pro
targetRevision: main
destination:
server: https://kubernetes.default.svc
namespace: rutas-norte-pro
syncPolicy:
automated:
prune: true
selfHeal: true
retry:
limit: 2
backoff: { duration: 30s, factor: 2, maxDuration: 3m }
revisionHistoryLimit: 20La reversió automàtica té dos nivells i convé distingir-los:
- Reversió de la càrrega:
kubectl rollout undosobre el Deployment. És immediata però deixa el clúster desincronitzat amb Git, així que Argo CD ambselfHealtornaria a posar la versió dolenta. Només serveix com a pedaç d'emergència acompanyat del pas 2. - Reversió a Git:
git revertdel commit que va canviar el digest. És la correcta, triga el que trigui la sincronització (segons si es força) i deixa el sistema coherent.
Automatització a Rutas Norte: si el hook de verificació falla a pro, un flux de treball obre i fusiona automàticament el git revert de l'últim commit d'overlays/pro, i avisa el canal de guàrdia. El temps mitjà des de la fallada fins a la versió anterior corrent és de 3 minuts i 40 segons.
# El flux de reversió automàtica, en essència
gh workflow run revertir.yml -f entorn=pro -f motiu="fum PostSync fallit"
# ...que fa:
git revert --no-edit "$(git log -1 --format=%H -- overlays/pro)"
git push origin main
argocd app sync api-reserves-pro --prune
argocd app wait api-reserves-pro --health --timeout 300Un matís important: la reversió automàtica només s'activa si la petició de canvi de promoció va declarar que no hi ha migració d'esquema incompatible. Si n'hi ha, la reversió ha de ser humana, perquè revertir codi sobre un esquema ja migrat pot corrompre dades.
- Mètriques DORA: els números de Rutas Norte abans i després
Tota aquesta enginyeria s'ha de justificar amb números, o és afició. Les quatre mètriques DORA són la manera estàndard de mesurar-ho.
| Mètrica | Què mesura | Com s'obté a Rutas Norte |
|---|---|---|
| Freqüència de desplegament | Quantes vegades es porta alguna cosa a producció | Commits a overlays/pro per setmana |
| Termini de lliurament del canvi | Del commit fusionat a ser en producció | Marca de temps del commit → esdeveniment de sincronització d'Argo CD |
| Taxa de fallada dels canvis | Quin percentatge de desplegaments causa un problema | Desplegaments seguits de reversió o incident / total |
| Temps de restauració del servei | Quant es triga a recuperar-se després d'una fallada | Obertura de l'incident → resolució |
10.1. Els números
Situació de partida, el novembre de 2025: construcció manual d'imatges des dels portàtils, etiqueta latest, kubectl apply des de l'equip de la persona que desplegava, desplegaments els dijous a la tarda "quan toca".
| Mètrica | Abans (nov. 2025) | Després (jul. 2026) | Referència d'alt rendiment |
|---|---|---|---|
Freqüència de desplegament a pro |
1 cada 2 setmanes | 4-6 per setmana | Sota demanda |
Termini de lliurament (fusió → pro) |
9 dies | 4 h 20 min | < 1 dia |
| Taxa de fallada dels canvis | 31 % | 7 % | 0-15 % |
| Temps de restauració | 3 h 40 min | 12 min | < 1 hora |
10.2. Què va moure cada número
- Freqüència: va pujar pels entorns efímers i per la promoció automàtica a
dev. Quan desplegar deixa de fer mal, es desplega més. - Termini de lliurament: de nou dies a poc més de quatre hores. El gruix d'aquests nou dies no era tècnic: era esperar que s'alliberés l'entorn compartit i que hi hagués lloc a la finestra del dijous.
- Taxa de fallada: va baixar del 31 % al 7 % principalment per dues coses, les proves d'integració a kind i el fet que el que es prova a
preés exactament el mateix digest que va apro. La majoria de les fallades anteriors eren diferències entre el que es provava i el que es desplegava. - Temps de restauració: de gairebé quatre hores a dotze minuts, gràcies a la reversió automàtica i al fet que revertir és
git reverten lloc de reconstruir una imatge antiga que ja ningú no sap com es construïa.
10.3. Com mesurar-les sense muntar un projecte
# Freqüència de desplegament a pro en els últims 30 dies
git log --since='30 days ago' --oneline -- overlays/pro | wc -l
# Termini de lliurament: diferència entre el commit de codi i el de promoció
git log --since='30 days ago' --format='%H %ct' -- overlays/proLes dues primeres surten de Git. La taxa de fallada i el temps de restauració surten del registre d'incidències, que veurem a 11-06. La trampa clàssica és mesurar només les dues fàcils: la velocitat sense estabilitat no és rendiment, és risc acumulant-se.
Errors Comuns i Consells
- Executar
kubectl applydes de la canalització. Obliga a desar credencials de producció al sistema de CI, fa l'estat dependent de l'ordre d'execucions i elimina la traçabilitat. Amb GitOps la canalització només escriu a Git. - Desplegar per etiqueta mòbil en lloc de per digest. L'artefacte provat i el desplegat deixen de ser el mateix, i tota la cadena de verificació perd sentit.
- Reconstruir la imatge en promocionar de
preapro. És l'error conceptual més car: es promociona un artefacte, no un commit. Reconstruir invalida totes les proves anteriors. - Trencar la construcció amb
HIGHo amb vulnerabilitats sense pedaç. L'equip aprèn a afegir excepcions per rutina i el control deixa de detectar res. - Condició OIDC massa àmplia (
repo:org/*:*). Concedeix el rol a tota l'organització. La condició ha de fixar repositori i, quan sigui possible, branca o tipus d'esdeveniment. - Entorns efímers sense quota ni esborrat automàtic. Es converteixen en la partida de cost que ningú no entén. Quota, rèplica única, sense persistència i neteja nocturna.
- Reversió automàtica amb migracions incompatibles. Pot corrompre dades. Tota migració s'ha de declarar compatible cap enrere o bloquejar la reversió automàtica.
- Consell: fes que les proves de fum s'executin a kind i també després de cada desplegament real. El mateix codi, dos moments: valida abans de fusionar i verifica després de desplegar.
- Consell: publica les quatre mètriques DORA en un panell visible. Si només es miren les de velocitat, la qualitat es degrada sense que ningú no ho noti fins a l'incident.
Exercicis
Exercici 1: trobar les fallades d'una canalització
Un equip de Rutas Norte proposa aquesta canalització simplificada per a un servei nou:
on: [push]
permissions: write-all
jobs:
desplegar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t registry.rutasnorte.example/rutasnorte/promos:latest .
- run: docker push registry.rutasnorte.example/rutasnorte/promos:latest
- run: echo "${{ secrets.KUBECONFIG_PRO }}" > /tmp/kc
- run: KUBECONFIG=/tmp/kc kubectl -n rutas-norte-pro apply -f k8s/
- run: KUBECONFIG=/tmp/kc kubectl -n rutas-norte-pro rollout restart deploy/promosIdentifica almenys cinc problemes seriosos i proposa la correcció de cadascun.
Exercici 2: dissenyar la porta de promoció
api-reserves incorpora un canvi que afegeix la columna codi_promocional a la taula reserves i comença a fer-la servir. Dissenya la seqüència de promoció a producció indicant: en quants desplegaments es divideix, què conté cadascun, en quin ordre s'apliquen migració i codi, i què permet que la reversió sigui possible en cada punt.
Exercici 3: interpretar les mètriques DORA
Tres mesos després de les millores, Rutas Norte mesura: freqüència de desplegament 11 per setmana (va pujar), termini de lliurament 2 h 10 min (va baixar), taxa de fallada 22 % (va pujar des del 7 %) i temps de restauració 14 min (estable). Interpreta el conjunt, proposa dues hipòtesis sobre la causa i digues què mesuraries per distingir-les.
Solucions
Solució 1. Problemes i correccions:
| # | Problema | Correcció |
|---|---|---|
| 1 | permissions: write-all |
Permisos mínims: contents: read, packages: write, id-token: write |
| 2 | Etiqueta latest |
Etiquetar per SHA curt i desplegar per digest |
| 3 | Sense proves, sense lint, sense escaneig, sense signatura | Afegir treball de proves, Trivy amb CRITICAL i signatura Cosign sense claus |
| 4 | Kubeconfig de producció desat com a secret | Eliminar-lo; la canalització no ha de tocar pro. Substituir per petició de canvi al repositori de manifests |
| 5 | kubectl apply directe a pro des de cada push a qualsevol branca |
GitOps: escriure el digest a overlays/dev; pre i pro per promoció aprovada |
| 6 | rollout restart com a mecanisme de desplegament |
Innecessari si el digest canvia; el canvi de digest ja provoca el desplegament |
| 7 | on: [push] sense filtre de branca |
Restringir a main el que publica, i fer servir pull_request per validar |
Solució 2. Tres desplegaments, aplicant expandir i contraure:
- Desplegament 1 (expandir): migració que afegeix
codi_promocionalcom a columna anul·lable amb valor per defecte nul. El codi desplegat la ignora completament. Reversió possible: revertir el codi no trenca res, la columna sobra però és innòcua. - Desplegament 2 (usar): codi nou que escriu i llegeix la columna, amb lectura tolerant al valor nul per a les files antigues. Sense canvis d'esquema. Reversió possible: la versió anterior continua funcionant perquè ignora la columna.
- Desplegament 3 (contraure), setmanes després i només si cal: afegir restricció
NOT NULL, índexs o eliminar columnes antigues substituïdes. A partir d'aquí la reversió a la versió 1 ja no és segura, per això se separa en el temps i es fa quan hi ha confiança plena.
Ordre general: la migració s'aplica abans que el codi que la necessita, i sempre de manera que la versió anterior del codi continuï funcionant amb l'esquema nou. Això és el que fa possible el RollingUpdate (dues versions convivint) i la reversió.
Solució 3. Interpretació: la velocitat ha millorat però l'estabilitat s'ha degradat seriosament; gairebé un de cada quatre desplegaments causa problema. El temps de restauració estable indica que la reversió automàtica continua funcionant bé, cosa que esmorteeix l'impacte però no l'elimina. El conjunt suggereix que s'està desplegant més de pressa del que la verificació pot validar.
Hipòtesi A: s'han relaxat les portes de qualitat (menys proves de regressió a pre, aprovacions per rutina, llindars de k6 no revisats). Hipòtesi B: l'augment de freqüència ve de canvis més petits però més nombrosos en zones del sistema mal cobertes per proves, o d'un component nou amb menys maduresa.
Per distingir-les: (a) desglossar la taxa de fallada per component —si es concentra en un, és la hipòtesi B; si està repartida, l'A—; (b) mesurar la cobertura i el temps de les proves de pre en els últims tres mesos i comprovar si alguna porta s'ha desactivat; (c) revisar les peticions de canvi de promoció fallides i classificar la causa arrel de cadascuna. Acció probable: no reduir la freqüència, sinó reforçar la verificació que es va saltar, i considerar el desplegament canari d'11-04 perquè un canvi defectuós només afecti un percentatge petit d'usuaris.
Conclusió
Hem recorregut la canalització completa de Rutas Norte, del git push a producció. Vam veure per què la integració i el lliurament se separen, i per què amb GitOps la canalització mai no executa kubectl apply: produeix un digest signat i verificat, i escriu aquest digest al repositori de manifests, i deixa que Argo CD faci la resta. Vam recórrer el fitxer d'integració complet —proves, construcció multietapa amb memòria cau, etiquetatge per commit, Trivy, signatura sense claus, digest—, les proves d'integració reals contra un clúster kind aixecat dins de la mateixa canalització, la promoció per entorns amb portes que comproven coses diferents, els entorns efímers per petició de canvi com la millor inversió que pot fer un equip, les credencials federades sense secrets de llarga vida, la verificació posterior al desplegament amb reversió automàtica en menys de quatre minuts, i les mètriques DORA que demostren que tot això va servir per a alguna cosa real.
La idea central: es promociona un artefacte, no un commit. El mateix digest que va passar les proves a dev i la càrrega a pre és el que atén els clients a pro, sense reconstruir res pel camí.
Fins i tot amb tot això, cada desplegament a producció continua sent un salt: la versió nova substitueix l'anterior a tots els pods i, si alguna cosa es va escapar a les proves, la descobreixen tots els usuaris alhora. A la lliçó següent, Estratègies de Desplegament: Blue-Green i Canary, veurem com eliminar aquest salt: desplegar sense comprometre's, validar amb trànsit real, exposar la versió nova a un percentatge petit d'usuaris i deixar que Prometheus decideixi automàticament si es promociona o s'avorta.
Curs de Kubernetes
Mòdul 1: Introducció a Kubernetes
- Què és Kubernetes?
- Arquitectura de Kubernetes
- Conceptes i Terminologia Clau
- Configuració d'un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objectes, Manifests YAML i el Model Declaratiu
- El Projecte del Curs: la Plataforma Rutas Norte
Mòdul 2: Components Principals de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualitzacions, Rollbacks i Estratègies de Desplegament
- Serveis
- Namespaces
- Etiquetes, Selectors i Anotacions
Mòdul 3: Gestió de Configuració i Secrets
- ConfigMaps
- Secrets
- Variables d'Entorn
- Quotes i Límits de Recursos
- LimitRanges i Classes de Qualitat de Servei (QoS)
- ServiceAccounts i Accés a l'API des dels Pods
Mòdul 4: Xarxes a Kubernetes
- Xarxes de Clúster
- Tipus de Serveis
- DNS Intern i Descobriment de Serveis
- Controladors d'Ingress
- TLS i Gestió de Certificats amb cert-manager
- Polítiques de Xarxa
Mòdul 5: Emmagatzematge a Kubernetes
- Volums
- Volums Persistents
- Reclamacions de Volums Persistents
- Classes d'Emmagatzematge
- Aprovisionament Dinàmic, Expansió i Snapshots
- Còpies de Seguretat i Restauració de Dades
Mòdul 6: Conceptes Avançats de Kubernetes
- StatefulSets
- DaemonSets
- Treballs i CronJobs
- Init Containers, Sidecars i Patrons Multicontenidor
- Planificació: Afinitat, Taints i Toleracions
- Definicions de Recursos Personalitzats (CRDs)
- Operadors i el Patró Controlador
Mòdul 7: Monitoratge i Registre
- Verificacions de Salut i Sondes
- Servidor de Mètriques i kubectl top
- Monitoratge amb Prometheus
- Visualització i Alertes amb Grafana i Alertmanager
- Registre Centralitzat amb Elasticsearch, Fluentd i Kibana (EFK)
- Depuració d'Aplicacions i Esdeveniments del Clúster
Mòdul 8: Seguretat a Kubernetes
- Control d'Accés Basat en Rols (RBAC)
- Contextos de Seguretat i Enduriment del Contenidor
- Polítiques de Seguretat de Pods i Pod Security Standards
- Seguretat de Xarxa
- Seguretat d'Imatges
- Auditoria, Escaneig i Gestió de Vulnerabilitats
Mòdul 9: Escalat i Rendiment
- Autoescalat Horitzontal de Pods
- Autoescalat Vertical de Pods
- Autoescalat de Clúster
- Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA
- Alta Disponibilitat: PodDisruptionBudgets i Topologia
- Ajust de Rendiment
Mòdul 10: Ecosistema i Eines de Kubernetes
- Minikube i Entorns Locals amb kind
- Kubeadm
- Helm
- Kustomize
- GitOps amb Argo CD i Flux
- Kubernetes Gestionat: EKS, AKS i GKE
Mòdul 11: Estudis de Cas i Aplicacions del Món Real
- Desplegament d'una Aplicació Web
- Execució d'Aplicacions amb Estat
- CI/CD amb Kubernetes
- Estratègies de Desplegament: Blue-Green i Canary
- Gestió Multi-Clúster
- Operació en Producció: Incidències, Runbooks i Costos
