Aquesta lliçó tanca una promesa que el curs arrossega des del mòdul 7. A la lliçó 07-06 vam traçar una frontera explícita: allà parlàvem d'integració contínua —comprovar automàticament que el que s'integra funciona— i vam deixar per aquí com aquest codi arriba als usuaris. A la 07-04 vam ajornar la mecànica dels entorns i la promoció. I a la 05-05, en crear l'etiqueta anotada v1.4.0, vam dir que les etiquetes serien la peça que dispara un desplegament.
Tot convergeix aquí.
El canvi de perspectiva és gran. Fins ara, Git ha estat una eina que fan servir persones: l'Ana escriu codi, en Bruno revisa, la Carla integra. A partir d'aquest punt, Git és també —i sobretot— una eina que fan servir màquines. Canonades que clonen, llegeixen etiquetes, comparen hashos, construeixen artefactes i publiquen en producció sense que ningú premi res. En una organització amb desplegament continu, la immensa majoria dels git clone del dia els executa un servidor, no una persona.
I això canvia el pes de cada operació. Quan git push origin main significa "això serà a producció d'aquí a quatre minuts", el rigor deixa de ser una virtut professional i passa a ser un requisit operatiu.
Contingut
- Git com a font única de veritat
- Integració, lliurament i desplegament continus
- Què dispara un desplegament i com es lliga a Git
- L'artefacte s'identifica pel hash del commit
- Infraestructura com a codi
- GitOps: el repositori com a estat desitjat
- Secrets en un món automatitzat
- Versionatge i changelog automàtics
- Reversió en producció
- Que la canonada no depengui d'un humà
- Git com a font única de veritat
La idea de fons de DevOps, pel que fa a Git, és una ampliació del que ja saps: el repositori deixa de contenir només codi.
Històricament, un repositori contenia l'aplicació. Tota la resta —com es configura el servidor, quina versió de la base de dades hi ha, quines variables d'entorn existeixen, com es desplega— vivia al cap d'algú, en un document, en una consola web o en un guió a l'escriptori d'un administrador.
El canvi consisteix a ficar tot això també a Git:
gestor-tasques/
├── index.html
├── estils.css
├── app.js
├── README.md
├── .github/workflows/ <- definició de les canonades
│ ├── ci.yml
│ ├── lliurament.yml
│ └── desplegament.yml
├── infra/ <- infraestructura com a codi
│ ├── produccio.tf
│ ├── preproduccio.tf
│ └── moduls/
├── entorns/ <- configuració per entorn
│ ├── produccio.yaml
│ ├── preproduccio.yaml
│ └── proves.yaml
├── Containerfile <- com es construeix l'artefacte
└── migracions/ <- canvis d'esquema versionats
├── 001-crear-tasques.sql
└── 002-afegir-oculta.sqlQuè s'hi guanya
| Propietat | Què significa a la pràctica |
|---|---|
| Historial | "Quan va canviar el límit de memòria del servei?" és un git log sobre infra/produccio.tf |
| Revisió | Un canvi d'infraestructura passa pel mateix procés de petició d'extracció i revisió que el codi (07-02) |
| Reversió | Tornar a una configuració anterior és git revert |
| Reproductibilitat | Un entorn nou s'aixeca des del repositori, sense coneixement tàcit |
| Traçabilitat | Cada canvi en producció té autor, data, missatge i tiquet |
| Atomicitat | Un commit pot canviar el codi i la configuració que necessita, alhora |
Aquesta última fila és la més valuosa i la que més se subestima. Si app.js comença a necessitar una variable d'entorn nova, el commit que introdueix el codi i el que declara la variable són el mateix commit. Mai no existeix una revisió del repositori on el codi demani alguna cosa que la configuració no dona.
La regla
Si alguna cosa pot trencar producció, ha d'estar a Git.
Configuració, infraestructura, migracions de base de dades, definició de les canonades, polítiques d'accés. Tot allò que sigui un fitxer de text i afecti el comportament del sistema.
I el corol·lari, que és igual d'important:
Si alguna cosa és a Git, no s'ha de canviar fora de Git.
Modificar la configuració de producció des d'una consola web crea una deriva: el repositori diu una cosa i la realitat en diu una altra. Tan bon punt la deriva existeix, el repositori deixa de ser la font de veritat i tot l'edifici cau. És el problema que GitOps ataca directament (apartat 6).
L'únic que no va a Git
Amb una excepció absoluta, ja establerta a la lliçó 08-05: els secrets, mai. Contrasenyes, claus d'API, certificats privats, tokens. Van en un gestor de secrets i s'injecten en el moment del desplegament. Hi tornarem a l'apartat 7.
- Integració, lliurament i desplegament continus
Tres termes que es confonen constantment, i la diferència dels quals és exactament on és el botó humà.
flowchart LR
A["Commit<br/>en una branca"] --> B["Compilar<br/>i provar"]
B --> C["Integrar<br/>a main"]
C --> D["Construir<br/>l'artefacte"]
D --> E["Publicar al<br/>registre"]
E --> F{"Aprovació<br/>humana"}
F --> G["Desplegar a<br/>producció"]
subgraph CI["Integració contínua (07-06)"]
A
B
C
end
subgraph CD1["Lliurament continu"]
D
E
F
end
subgraph CD2["Desplegament continu"]
G
end
| Integració contínua | Lliurament continu | Desplegament continu | |
|---|---|---|---|
| Què garanteix | El que s'integra compila i passa les proves | El que hi ha a main es pot desplegar en qualsevol moment |
El que hi ha a main està desplegat |
| On acaba | A main, en verd |
En un artefacte publicat i llest | A producció |
| Botó humà | No n'hi ha | Sí: algú aprova el desplegament | No n'hi ha |
| Disparador | push a una branca, petició d'extracció |
Integració a main |
Integració a main |
| Freqüència típica | Desenes al dia | Desenes al dia | Desenes al dia |
| Què cal | Proves ràpides i fiables | Artefactes reproduïbles, entorns de preproducció | Tot l'anterior + confiança total en les proves + reversió ràpida |
| Risc si falla | Es bloqueja la petició d'extracció | Es bloqueja la publicació | Producció trencada |
La diferència entre lliurament i desplegament és exactament un clic. I la decisió de treure aquest clic no és tècnica sinó de confiança: es treu quan el conjunt de proves és tan bo i la reversió tan ràpida que un humà mirant la pantalla no aporta seguretat, només latència.
Què cal per arribar al desplegament continu
- Proves en què es confia de debò. Si l'equip executa proves manuals "per si de cas" abans de desplegar, no està a punt.
- Reversió en minuts. L'assegurança no és "no fallar mai", sinó "arreglar-ho abans que importi".
- Desplegament progressiu. Treure la versió nova a un percentatge del trànsit i observar abans de completar.
- Observabilitat. Mètriques i alertes que detectin un problema abans que els usuaris.
- Canvis petits. És la connexió amb Trunk Based Development (lliçó 07-05): com més petit és el canvi, menor el risc i més fàcil identificar-ne la causa.
Aquest últim punt mereix èmfasi, perquè és on Git i DevOps es toquen directament. El desplegament continu obliga a commits petits i ben acotats. Si integres un canvi de 2.000 línies i producció es trenca, no saps quina de les 2.000 va ser. Tota la disciplina del mòdul 8 —commits atòmics, missatges que expliquen el perquè, historial llegible— passa de ser una bona pràctica a ser una eina d'operació.
- Què dispara un desplegament i com es lliga a Git
Hi ha tres mecanismes, i convé entendre quin model mental implica cadascun.
Mecanisme 1: per branca
name: Desplegar a preproducció
on:
push:
branches: [main]
jobs:
desplegar:
runs-on: ubuntu-latest
environment: preproduccio
steps:
- uses: actions/checkout@v4
- name: Construir l'artefacte
run: |
docker build -t registre.exemple.cat/gestor-tasques:${{ github.sha }} .
docker push registre.exemple.cat/gestor-tasques:${{ github.sha }}
- name: Desplegar
run: ./infra/desplegar.sh preproduccio ${{ github.sha }}Model mental: "la branca main és el que hi ha a preproducció". La branca deixa de ser només una línia de desenvolupament i passa a ser el punter a un entorn.
És el mecanisme natural per a entorns no productius. Cada integració a main actualitza preproducció, sense cerimònia.
Mecanisme 2: per etiqueta anotada
Aquí es tanca el que vam prometre a la lliçó 05-05.
name: Desplegar a producció
on:
push:
tags: ['v*.*.*']
jobs:
desplegar:
runs-on: ubuntu-latest
environment: produccio
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Verificar que l'etiqueta és anotada i està signada
run: |
TIPUS=$(git cat-file -t "${{ github.ref_name }}")
if [ "$TIPUS" != "tag" ]; then
echo "ERROR: ${{ github.ref_name }} és una etiqueta lleugera."
echo "Producció només accepta etiquetes anotades."
exit 1
fi
git tag --verify "${{ github.ref_name }}"
- name: Verificar que l'etiqueta és a main
run: |
git merge-base --is-ancestor "${{ github.ref_name }}" origin/main \
|| { echo "ERROR: l'etiqueta no és a main"; exit 1; }
- name: Desplegar l'artefacte ja construït
run: |
SHA=$(git rev-list -n1 "${{ github.ref_name }}")
./infra/desplegar.sh produccio "$SHA"Model mental: "l'etiqueta v1.4.0 és una versió publicable, i publicar-la és desplegar-la".
Tres detalls que importen molt:
1. S'exigeix etiqueta anotada, no lleugera. Recorda de la lliçó 05-05 que una etiqueta lleugera és només una referència a un commit, mentre que una d'anotada és un objecte propi amb autor, data, missatge i possibilitat de signatura. Per a producció, això és exactament el que vols: un registre de qui va declarar aquella versió i quan. La comprovació git cat-file -t retorna tag per a les anotades i commit per a les lleugeres.
2. Es verifica la signatura. git tag --verify comprova la signatura GPG (lliçó 08-05). Així, crear una etiqueta de producció requereix una clau privada, no només permisos d'escriptura.
3. Es comprova que l'etiqueta és a main. git merge-base --is-ancestor impedeix desplegar una etiqueta posada sobre una branca que mai no es va integrar. És una salvaguarda contra l'error d'etiquetar des de la branca equivocada, que passa més del que sembla.
Molt important: el pas de desplegament no reconstrueix res. Recupera l'artefacte que ja es va construir quan aquell commit va entrar a main. Hi tornarem a l'apartat següent, perquè és una de les regles d'or del lliurament.
Mecanisme 3: aprovació manual sobre un commit concret
name: Desplegar a producció (manual)
on:
workflow_dispatch:
inputs:
commit:
description: 'Hash complet del commit a desplegar'
required: true
jobs:
desplegar:
runs-on: ubuntu-latest
environment: produccio # amb revisors obligatoris configurats
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.commit }}
fetch-depth: 0
- name: Comprovar que el commit és a main i té artefacte
run: |
git merge-base --is-ancestor "${{ inputs.commit }}" origin/main \
|| { echo "ERROR: aquest commit no és a main"; exit 1; }
docker manifest inspect \
"registre.exemple.cat/gestor-tasques:${{ inputs.commit }}" > /dev/null \
|| { echo "ERROR: no hi ha artefacte per a aquest commit"; exit 1; }
- name: Desplegar
run: ./infra/desplegar.sh produccio "${{ inputs.commit }}"Model mental: "qualsevol commit de main és candidat; algú decideix quin i quan".
És el mecanisme més flexible i el que cal per tornar enrere ràpid, com veurem a l'apartat 9.
Taula comparativa
| Per branca | Per etiqueta | Manual sobre commit | |
|---|---|---|---|
| Ús típic | Preproducció, entorns de prova | Producció amb versions | Producció amb desplegament continu; reversions |
| Qui decideix | El procés d'integració | Qui crea l'etiqueta | Qui llança l'execució |
| Traçabilitat | Bona | Excel·lent: objecte amb autor, data, missatge i signatura | Bona, si es registra l'execució |
| Reversió | Difícil: cal revertir el commit | Fàcil: desplegar l'etiqueta anterior | Molt fàcil: desplegar el commit anterior |
| Risc de desplegament accidental | Alt: qualsevol integració desplega | Baix: cal crear l'etiqueta expressament | Molt baix |
| Encaixa amb | GitHub Flow, Trunk Based | Git Flow, productes amb versions | Trunk Based amb desplegament continu |
El muntatge habitual de gestor-tasques
Els tres mecanismes combinats, cadascun al seu lloc:
flowchart TD
A["Petició d'extracció GT-231"] -->|"CI: proves"| B["Integració a main"]
B -->|"automàtic<br/>per branca"| C["Preproducció"]
C -->|"validació"| D["git tag -a -s v1.4.0"]
D -->|"automàtic<br/>per etiqueta"| E["Producció"]
E -.->|"si alguna cosa falla:<br/>desplegament manual<br/>de v1.3.0"| F["Reversió"]
- L'artefacte s'identifica pel hash del commit
Aquesta és probablement la pràctica més important de tota la lliçó, i la que més equips fan malament.
La regla
Cada artefacte construït —imatge de contenidor, paquet, binari— s'etiqueta amb el hash complet del commit del qual va sortir.
SHA=$(git rev-parse HEAD)
docker build -t "registre.exemple.cat/gestor-tasques:$SHA" .
docker push "registre.exemple.cat/gestor-tasques:$SHA"
# I a més, etiquetes llegibles que APUNTEN al mateix artefacte
docker tag "registre.exemple.cat/gestor-tasques:$SHA" \
"registre.exemple.cat/gestor-tasques:v1.4.0"
docker tag "registre.exemple.cat/gestor-tasques:$SHA" \
"registre.exemple.cat/gestor-tasques:main"El hash és l'identificador canònic; v1.4.0 i main són àlies llegibles que hi apunten. Exactament la mateixa relació que a Git entre un hash de commit i les referències que l'apunten (lliçó 03-01): les etiquetes es mouen, el hash no.
Per què
1. Elimina l'ambigüitat. gestor-tasques:latest no significa res: és una etiqueta mòbil que apunta a l'últim que algú va construir. gestor-tasques:4f8a2e6c9d3b... identifica un contingut exacte i reproduïble.
2. Dona traçabilitat completa. Davant d'un error en producció, la cadena es recorre sencera i sense preguntar a ningú:
# 1. Què hi ha desplegat?
kubectl get deployment gestor-tasques -o jsonpath='{.spec.template.spec.containers[0].image}'
# registre.exemple.cat/gestor-tasques:4f8a2e6c9d3b1a5e7f2c8b0d4a6e9f1c3b5d7a0e
# 2. Quin commit és?
git show 4f8a2e6c
# 3. Què ha canviat respecte a la versió anterior?
git log --oneline 9e2f7a4c..4f8a2e6c
# 4. Qui i per què?
git log -1 --format='%an <%ae>%n%n%B' 4f8a2e6c
# 5. Quin tiquet?
git log -1 --format=%B 4f8a2e6c | grep -oE 'GT-[0-9]+'Del contenidor en producció al tiquet, en cinc ordres i sense obrir cap interfície.
3. Permet verificar la coherència. Està desplegat el que ens pensem?
DESPLEGAT=$(kubectl get deployment gestor-tasques \
-o jsonpath='{.spec.template.spec.containers[0].image}' | cut -d: -f2)
git merge-base --is-ancestor "$DESPLEGAT" origin/main \
&& echo "OK: el que està desplegat és a main" \
|| echo "ALERTA: s'ha desplegat una cosa que no és a main"
# Quants commits van per davant de producció?
git rev-list --count "$DESPLEGAT"..origin/main4. Fa possible el desplegament idempotent. Desplegar el mateix hash dues vegades produeix exactament el mateix resultat. Amb latest, no hi ha manera de saber-ho.
El corol·lari: construir una vegada, desplegar moltes
L'artefacte es construeix UNA sola vegada i aquest mateix artefacte recorre tots els entorns.
flowchart LR
C["commit 4f8a2e6"] --> B["Construcció<br/>ÚNICA"]
B --> A["Artefacte<br/>:4f8a2e6"]
A --> E1["Proves"]
A --> E2["Preproducció"]
A --> E3["Producció"]
Si cada entorn reconstrueix l'artefacte, el que has provat no és el que desplegues. Entre una construcció i una altra poden canviar les versions de les dependències, la imatge base o la versió del compilador. L'artefacte que va passar les proves i el que arriba a producció serien dues coses diferents, i tota la validació prèvia deixa de significar res.
El que sí que canvia entre entorns és la configuració, que s'injecta en el moment del desplegament: variables d'entorn, secrets, adreces de serveis. L'artefacte és el mateix binari; l'entorn el parametritza.
Incrustar la procedència a l'artefacte
Convé que el mateix artefacte sàpiga d'on va venir:
FROM node:22-alpine
ARG COMMIT_SHA
ARG VERSION
ARG DATA_CONSTRUCCIO
LABEL org.opencontainers.image.revision="${COMMIT_SHA}"
LABEL org.opencontainers.image.version="${VERSION}"
LABEL org.opencontainers.image.created="${DATA_CONSTRUCCIO}"
ENV COMMIT_SHA=${COMMIT_SHA}
COPY . /app
WORKDIR /app
RUN npm ci --omit=dev
CMD ["node", "app.js"]docker build \
--build-arg COMMIT_SHA="$(git rev-parse HEAD)" \
--build-arg VERSION="$(git describe --tags --always --dirty)" \
--build-arg DATA_CONSTRUCCIO="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-t "registre.exemple.cat/gestor-tasques:$(git rev-parse HEAD)" .Aquest git describe --tags --always --dirty és una joia per a això. Produeix cadenes com:
| Sortida | Què significa |
|---|---|
v1.4.0 |
Exactament l'etiqueta v1.4.0 |
v1.4.0-7-g4f8a2e6 |
7 commits després de v1.4.0, al commit 4f8a2e6 |
v1.4.0-7-g4f8a2e6-dirty |
El mateix, però amb canvis sense confirmar: no hauria d'arribar a producció |
Aquest sufix -dirty és una alarma valuosa. Un artefacte marcat com a dirty es va construir des d'una còpia de treball bruta, cosa que significa que no és reproduïble des del repositori. La canonada l'hauria de rebutjar:
if git describe --always --dirty | grep -q -- '-dirty$'; then
echo "ERROR: la còpia de treball té canvis sense confirmar."
exit 1
fiI a l'aplicació, exposar aquesta informació:
// app.js
app.get('/version', (req, res) => {
res.json({
commit: process.env.COMMIT_SHA,
version: process.env.VERSION,
construit: process.env.DATA_CONSTRUCCIO
});
});Un punt d'accés /version que diu el hash exacte que s'està executant resol per si sol la pregunta més freqüent durant una incidència: "què hi ha realment aquí?".
- Infraestructura com a codi
La infraestructura —servidors, xarxes, bases de dades, balancejadors, permisos— es declara en fitxers de text versionats, i una eina s'encarrega que la realitat coincideixi amb el que s'ha declarat.
# infra/produccio.tf
resource "servei_web" "gestor_tasques" {
nom = "gestor-tasques"
imatge = "registre.exemple.cat/gestor-tasques:${var.commit_sha}"
repliques = 4
memoria_mb = 512
cpu = "500m"
variables = {
ENTORN = "produccio"
NIVELL_LOG = "warn"
}
# El secret NO és aquí: és una referència al gestor de secrets
secrets = {
BD_PASSWORD = "gestor-secrets://produccio/bd/password"
}
}Què hi guanya Git
Tot el que Git sap fer amb el codi s'aplica ara a la infraestructura:
# Quan i per què vam pujar a 4 rèpliques?
git log -p --follow infra/produccio.tf | grep -B 20 'repliques'
# Qui va canviar el límit de memòria?
git blame infra/produccio.tf
# Què canvia aquesta petició d'extracció respecte a producció?
git diff origin/main...HEAD -- infra/
# Tornar a la configuració d'abans de l'incident
git revert 4f8a2e6cAquest git blame sobre un fitxer d'infraestructura és una capacitat que fa deu anys no existia. La pregunta "per què aquest servei té 512 MB i no 256?" passa de ser arqueologia a ser una ordre.
El patró de pla i aplicació
La pràctica estàndard consisteix a calcular el pla de canvis a la petició d'extracció —perquè es pugui revisar— i aplicar-lo només després de la integració:
name: Infraestructura
on:
pull_request:
paths: ['infra/**']
push:
branches: [main]
paths: ['infra/**']
jobs:
pla:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Calcular el pla
run: |
terraform init
terraform plan -no-color -out=pla.bin | tee pla.txt
- name: Publicar el pla com a comentari a la petició d'extracció
run: ./eines/comentar-pla.sh pla.txt
aplicar:
if: github.event_name == 'push'
runs-on: ubuntu-latest
environment: produccio
steps:
- uses: actions/checkout@v4
- name: Aplicar
run: |
terraform init
terraform apply -auto-approvePublicar el pla a la petició d'extracció és el que converteix la revisió d'infraestructura en una cosa real: qui revisa no llegeix un fitxer declaratiu i s'imagina l'efecte, llegeix l'efecte: "es destruiran 2 recursos, se'n crearà 1 i se'n modificaran 3".
Les migracions de base de dades
Menció a part, perquè és on el model es trenca si no es va amb compte. Els fitxers de migració es versionen com tota la resta:
Però hi ha una asimetria fonamental: el codi es reverteix fàcilment; les dades, no. Un git revert del commit que va afegir una columna no torna les dades que aquella columna contenia.
D'aquí la regla d'or de les migracions, que condiciona com s'escriuen els commits:
Tota migració ha de ser compatible cap enrere durant almenys un desplegament. La versió antiga del codi ha de continuar funcionant amb l'esquema nou.
A la pràctica, això converteix "reanomenar una columna" en una seqüència de tres desplegaments:
- Afegir la columna nova; el codi escriu a totes dues i llegeix de la vella.
- Migrar les dades; el codi llegeix de la nova.
- Eliminar la columna vella.
Tres commits, tres desplegaments, i en cap moment un estat en què revertir el codi trenqui l'aplicació. És un exemple perfecte de com les restriccions d'operació es tradueixen en com es parteixen els commits.
- GitOps: el repositori com a estat desitjat
GitOps és un model operatiu amb una premissa molt concreta:
L'estat desitjat del sistema està declarat en un repositori Git. Un agent que corre dins del sistema compara contínuament l'estat real amb el declarat i reconcilia les diferències.
La diferència amb l'anterior és subtil però important: no és la canonada la que empeny els canvis al sistema, sinó un agent dins del sistema que estira d'ells.
flowchart TD
subgraph git["Repositori d'estat"]
A["entorns/produccio/<br/>servei.yaml<br/>imatge: :4f8a2e6"]
end
subgraph cluster["Sistema en producció"]
B["Agent<br/>reconciliador"]
C["Estat real"]
end
A -->|"l'agent observa<br/>(pull)"| B
B -->|"compara"| C
B -->|"aplica les diferències"| C
C -.->|"informa de l'estat<br/>i de la deriva"| A
Model d'empenta enfront de model d'estirada
| Empenta (la canonada desplega) | Estirada (GitOps) | |
|---|---|---|
| Qui aplica els canvis | La canonada de CI | Un agent dins del sistema |
| Credencials de producció | Les té la canonada | Les té l'agent; la canonada no |
| Deriva de configuració | No es detecta | Es detecta i es corregeix sola |
| Estat després d'un canvi manual | Es queda així fins al pròxim desplegament | Es reverteix automàticament |
| Superfície d'atac | La canonada és un objectiu valuós | Menor: res extern no entra al sistema |
| Auditoria | Registres de la canonada | L'historial de Git és l'auditoria |
| Desplegar | Un push a la branca que dispara |
Un commit al repositori d'estat |
L'avantatge de seguretat és contundent i mereix explicar-se: en el model d'empenta, el teu sistema de CI necessita credencials d'escriptura sobre producció. Això converteix la CI en l'objectiu més valuós de l'organització. A GitOps, la canonada només necessita permís per escriure en un repositori Git; l'agent, que viu dins del sistema, estira dels canvis. Res extern no té accés a producció.
Com es veu un desplegament a GitOps
Desplegar és literalment fer un commit:
# La canonada, després de construir i publicar l'artefacte:
git clone https://git.exemple.cat/gestor-tasques-estat.git
cd gestor-tasques-estat
# Actualitzar la referència de la imatge a l'entorn corresponent
sed -i "s|imatge: .*|imatge: registre.exemple.cat/gestor-tasques:$SHA|" \
entorns/preproduccio/servei.yaml
git add entorns/preproduccio/servei.yaml
git commit -m "deploy: preproduccio a ${SHA:0:7}
Origen: gestor-tasques@$SHA
Tiquet: $(git -C ../gestor-tasques log -1 --format=%B "$SHA" | grep -oE 'GT-[0-9]+' | head -1)"
git push origin mainL'agent detecta el commit en qüestió de segons i reconcilia.
I la promoció a producció és igual de simple:
# Promocionar exactament el que hi ha a preproducció
IMATGE=$(grep 'imatge:' entorns/preproduccio/servei.yaml)
sed -i "s|imatge: .*|$IMATGE|" entorns/produccio/servei.yaml
git commit -am "deploy: promou a produccio allo validat a preproduccio"
git pushFixa't en l'elegància: la promoció entre entorns és copiar una línia d'un fitxer a un altre. No es reconstrueix res, no es torna a etiquetar res. És la materialització literal de "construir una vegada, desplegar moltes".
Què implica per als permisos i les branques protegides
Si un commit al repositori d'estat desplega en producció, aleshores el control d'accés al repositori ÉS el control d'accés a producció. Això obliga a prendre's seriosament el que vam veure a la lliçó 07-06:
| Mesura | Per què és obligatòria aquí |
|---|---|
Branca protegida a main del repositori d'estat |
Un push directe seria un desplegament sense revisió |
| Revisió obligatòria, i de més d'una persona per a producció | És l'única barrera humana que queda |
CODEOWNERS sobre entorns/produccio/ |
Qui aprova producció ha de ser qui correspon (10-01) |
| Prohibir l'enviament forçat | Un push --force reescriuria l'historial d'auditoria |
| Commits signats (08-05) | L'agent pot exigir signatura abans d'aplicar |
| Historial lineal | L'auditoria s'ha de poder llegir sense ambigüitat |
I una conseqüència organitzativa: la separació entre "repositori d'aplicació" i "repositori d'estat" deixa de ser una mania i es converteix en una decisió de seguretat. Al primer es desenvolupa; al segon es desplega. Cadascun amb els seus permisos.
La deriva
La propietat més valuosa de GitOps és la detecció de deriva. Si algú canvia alguna cosa a mà en producció —per "arreglar-ho ràpid" a les tres de la matinada—, l'agent detecta que l'estat real no coincideix amb el declarat i ho reverteix.
És incòmode la primera vegada que et passa. I és exactament el que ha de passar: obliga que tot canvi passi per Git, que és el que fa que el repositori continuï sent la font de veritat. Sense aquesta reconciliació, "tot és a Git" és una afirmació que es degrada silenciosament fins a ser falsa.
- Secrets en un món automatitzat
Reprenem la lliçó 08-05 amb el matís de l'automatització.
La regla, sense excepcions
Els secrets mai no van a Git. Ni xifrats amb una contrasenya feble, ni "només aquest de proves", ni "és un repositori privat".
La raó, que ja coneixes bé: a Git, esborrar no elimina. Un secret confirmat és a l'historial per sempre llevat que es reescrigui i es coordini amb tothom, i tot i així ja ha estat exposat a tothom que clonés.
Què va a Git i què no
| Va a Git | No va a Git |
|---|---|
El nom de la variable (BD_PASSWORD) |
El seu valor |
Una referència al secret (gestor-secrets://produccio/bd/password) |
El secret |
| La política de qui el pot llegir | La clau que el desbloqueja |
| Certificats públics | Claus privades |
| Secrets xifrats amb una clau que no és a Git (patró acceptable) | Secrets xifrats amb una clau que sí que hi és |
Com arriba el secret al procés
name: Desplegar
on:
push:
tags: ['v*.*.*']
jobs:
desplegar:
runs-on: ubuntu-latest
environment: produccio
permissions:
id-token: write # per a autenticació federada, sense secrets de llarga vida
contents: read
steps:
- uses: actions/checkout@v4
- name: Obtenir credencials temporals del gestor de secrets
uses: proveidor/autenticar@v2
with:
rol: rol-desplegament-produccio
- name: Desplegar
run: ./infra/desplegar.sh produccio "${{ github.sha }}"
env:
# Injectat pel sistema, mai escrit en cap fitxer
BD_PASSWORD: ${{ secrets.BD_PASSWORD_PRODUCCIO }}Aquest bloc permissions: id-token: write mereix explicació, perquè és la pràctica moderna: en lloc de guardar una credencial de llarga vida al sistema de CI, la canonada obté un token de curta durada demostrant la seva identitat. Si algú roba aquest token, caduca en minuts i només serveix per a aquell repositori i aquella branca.
Quan cal versionar secrets: xifratge amb clau externa
Hi ha un patró acceptable, molt utilitzat a GitOps, on els secrets sí que viuen al repositori però xifrats amb una clau que no hi és:
# entorns/produccio/secrets.enc.yaml
apiVersion: v1
kind: Secret
metadata:
name: gestor-tasques-bd
data:
password: ENC[AES256_GCM,data:8fK2mN...,type:str]
sops:
kms:
- arn: 'arn:proveidor:kms:regio:compte:clau/id-de-la-clau'El valor xifrat és a Git; la clau per desxifrar-lo viu en un servei de gestió de claus al qual només l'agent de desplegament té accés. Avantatges: els secrets es versionen, es revisen i s'auditen com tota la resta. Requisit: que la clau mai no entri al repositori, i que rotar-la sigui un procediment provat.
Defensa en profunditat
Com a la lliçó 08-05, tres capes:
- name: Buscar secrets a la petició d'extracció
run: |
# Anàlisi de l'històric complet de la branca
gitleaks detect --source . --log-opts="origin/main..HEAD" --verbose
- name: Comprovar que no hi ha fitxers de configuració amb credencials
run: |
git diff --name-only origin/main...HEAD | while read -r f; do
case "$f" in
*.env|*.pem|*.key|*credencials*|*secret*)
echo "ERROR: $f no hauria de ser al repositori"; exit 1 ;;
esac
doneMés el hook de servidor (06-01) i el .gitignore (08-03). I el procediment per si tot i així passa, que comença sempre pel mateix: revocar el secret primer, netejar l'historial després.
- Versionatge i changelog automàtics
Aquí es cobra la inversió de la lliçó 08-01. Els Conventional Commits no eren només una convenció estètica: són dades estructurades que una màquina pot llegir.
Dels commits a la versió
| Tipus de commit | Efecte en SemVer (05-05) |
|---|---|
fix: |
Incrementa PATCH (1.3.4 → 1.3.5) |
feat: |
Incrementa MINOR (1.3.4 → 1.4.0) |
feat!: o peu BREAKING CHANGE: |
Incrementa MAJOR (1.3.4 → 2.0.0) |
docs:, chore:, test:, refactor:, style: |
No incrementa res |
# Què hi ha des de l'última versió publicada
ULTIMA=$(git describe --tags --abbrev=0)
git log "$ULTIMA"..main --format='%s'feat(tasques): GT-231 filtrar les tasques ocultes del llistat fix(interficie): GT-238 corregir el comptador després d'eliminar una tasca docs: GT-240 documentar el filtre d'ocultes chore: actualitzar dependències
Un feat i un fix sense canvis incompatibles: la versió següent és 1.4.0.
Calcular-ho automàticament
#!/bin/bash
# eines/seguent-versio.sh
set -euo pipefail
ULTIMA=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
COMMITS=$(git log "$ULTIMA"..HEAD --format='%s%n%b')
IFS=. read -r MAJOR MENOR PEDAC <<< "${ULTIMA#v}"
if echo "$COMMITS" | grep -qE '^(BREAKING CHANGE|BREAKING-CHANGE):|^[a-z]+(\(.+\))?!:'; then
MAJOR=$((MAJOR + 1)); MENOR=0; PEDAC=0
elif echo "$COMMITS" | grep -qE '^feat(\(.+\))?!?:'; then
MENOR=$((MENOR + 1)); PEDAC=0
elif echo "$COMMITS" | grep -qE '^fix(\(.+\))?!?:'; then
PEDAC=$((PEDAC + 1))
else
echo "Sense canvis que justifiquin una versió nova." >&2
exit 1
fi
echo "v$MAJOR.$MENOR.$PEDAC"Generar el changelog
#!/bin/bash
# eines/changelog.sh
ULTIMA=$(git describe --tags --abbrev=0)
NOVA=$1
{
echo "## $NOVA — $(date +%Y-%m-%d)"
echo
echo "### Novetats"
git log "$ULTIMA"..HEAD --format='%s' \
| grep -E '^feat' \
| sed -E 's/^feat(\(([^)]+)\))?!?: /- (\2) /' \
| sed 's/- () /- /'
echo
echo "### Correccions"
git log "$ULTIMA"..HEAD --format='%s' \
| grep -E '^fix' \
| sed -E 's/^fix(\(([^)]+)\))?!?: /- (\2) /' \
| sed 's/- () /- /'
echo
if git log "$ULTIMA"..HEAD --format='%B' | grep -q 'BREAKING CHANGE:'; then
echo "### Canvis incompatibles"
git log "$ULTIMA"..HEAD --format='%B' \
| grep -A 5 'BREAKING CHANGE:' | sed 's/^/ /'
echo
fi
echo "### Detalls"
echo "- Commits: $(git rev-list --count "$ULTIMA"..HEAD)"
echo "- Rang: \`$ULTIMA..$NOVA\`"
} > /tmp/changelog-nou.md
# Anteposar al fitxer existent
cat /tmp/changelog-nou.md CHANGELOG.md > /tmp/c && mv /tmp/c CHANGELOG.mdLa canonada de publicació completa
name: Publicar versió
on:
workflow_dispatch:
jobs:
publicar:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # imprescindible: describe necessita les etiquetes
- name: Calcular la versió
id: version
run: echo "v=$(./eines/seguent-versio.sh)" >> "$GITHUB_OUTPUT"
- name: Generar el changelog
run: ./eines/changelog.sh "${{ steps.version.outputs.v }}"
- name: Confirmar el changelog i crear l'etiqueta anotada
run: |
git config user.name "Canonada de publicació"
git config user.email "[email protected]"
git add CHANGELOG.md
git commit -m "chore(release): ${{ steps.version.outputs.v }}"
git tag -a "${{ steps.version.outputs.v }}" \
-m "$(sed -n '/^## /,/^## /p' CHANGELOG.md | head -n -1)"
git push origin main
git push origin "${{ steps.version.outputs.v }}"Aquest git push origin "$VERSION" del final és el que dispara la canonada de desplegament a producció de l'apartat 3. El circuit es tanca: dels missatges de commit surt la versió, de la versió surt l'etiqueta, i de l'etiqueta surt el desplegament.
És la millor justificació possible per a la disciplina del mòdul 8. Un missatge mal escrit ja no és només lleig: produeix una versió incorrecta i un changelog que menteix.
- Reversió en producció
Alguna cosa falla. Què es fa?
Les dues opcions
Opció A: git revert i tornar a desplegar.
Opció B: redesplegar l'artefacte anterior.
Per què gairebé sempre és la B
git revert + desplegar |
Redesplegar l'artefacte anterior | |
|---|---|---|
| Temps fins a la recuperació | Minuts: cal construir, provar i desplegar | Segons: l'artefacte ja existeix |
| Risc | El commit de reversió és codi nou sense provar en producció | Zero: aquell artefacte ja va estar funcionant |
| Pot fallar la reversió? | Sí: pot haver-hi conflictes, o revertir de més | No |
| Estat de l'historial | Net i explícit | main continua contenint el canvi dolent |
| Dependència de la CI | Total: si la CI està caiguda, no pots revertir | Cap |
| Interacció amb migracions | Perillosa si la migració no és reversible | Igual de perillosa, però més ràpida |
La fila decisiva és la segona: un git revert genera un commit que mai no ha estat en producció. Estàs desplegant codi nou per arreglar un incident. Redesplegar l'artefacte anterior és tornar a un estat que saps amb certesa que funcionava, perquè va estar funcionant fins fa deu minuts.
I la primera fila és la que decideix a la pràctica: durant un incident, la diferència entre segons i minuts és la diferència entre un ensurt i una postmortem.
El procediment correcte
flowchart TD
A["Alerta:<br/>producció falla"] --> B["1. REDESPLEGAR<br/>la versió anterior"]
B --> C["Servei restablert"]
C --> D["2. Investigar<br/>sense pressa"]
D --> E{"S'arregla<br/>ràpid?"}
E -->|sí| F["3a. Corregir cap endavant:<br/>commit fix + desplegar"]
E -->|no| G["3b. git revert a main,<br/>investigar amb calma"]
F --> H["4. Postmortem"]
G --> H
Primer es restableix el servei. Després s'arregla el codi. Són dues activitats diferents i barrejar-les allarga la incidència.
I el pas 3 no és opcional: si només redesplegues la versió anterior, main continua contenint el canvi dolent, i el pròxim desplegament de qualsevol persona el tornarà a portar a producció. Cal deixar main en un estat sa, ja sigui amb un revert o amb una correcció.
Com es veu a GitOps
Encara més simple, perquè revertir el desplegament és revertir un commit del repositori d'estat:
cd gestor-tasques-estat
git revert HEAD # desfà el commit que va canviar la imatge
git push origin main
# L'agent reconcilia en segonsAquí sí que és un git revert, però fixa't en la diferència: es reverteix el commit de desplegament, no el commit de codi. El resultat és que el fitxer d'estat torna a apuntar a l'artefacte anterior, que ja existeix i ja funcionava. És l'opció B expressada com un commit, amb la traçabilitat de l'opció A.
Preparar la reversió abans de necessitar-la
#!/bin/bash
# eines/revertir-produccio.sh
set -euo pipefail
ACTUAL=$(kubectl get deployment gestor-tasques \
-o jsonpath='{.spec.template.spec.containers[0].image}' | cut -d: -f2)
# L'etiqueta anterior a la que correspon al que està desplegat
ETIQUETA_ACTUAL=$(git tag --points-at "$ACTUAL" | head -1)
ANTERIOR=$(git tag --sort=-v:refname | grep -A1 "^$ETIQUETA_ACTUAL$" | tail -1)
SHA_ANTERIOR=$(git rev-list -n1 "$ANTERIOR")
echo "Desplegat ara: $ACTUAL ($ETIQUETA_ACTUAL)"
echo "Es revertirà a: $SHA_ANTERIOR ($ANTERIOR)"
echo
echo "Canvis que es desfaran:"
git log --oneline "$SHA_ANTERIOR".."$ACTUAL"
echo
read -rp "Ho confirmes? (escriu SI): " R
[ "$R" = "SI" ] || exit 1
./infra/desplegar.sh produccio "$SHA_ANTERIOR"Un guió així, provat i amb permisos clars, és la diferència entre una reversió de trenta segons i una de vint minuts buscant a l'historial del terminal a les tres de la matinada. Escriu-lo i prova'l un dimarts al matí, no durant un incident.
Quan el revert sí que és el correcte
- La fallada no és urgent: un error visual, alguna cosa que afecta poca gent.
- L'artefacte anterior no és desplegable: hi va haver una migració de dades irreversible pel mig.
- El canvi dolent es detecta abans d'arribar a producció, a preproducció.
- Es vol que l'historial reflecteixi explícitament que aquell canvi es va desfer.
- Que la canonada no depengui d'un humà
Recopilació final de pràctiques, cadascuna amb el seu motiu.
- Tot al repositori, res a la interfície
Si la canonada està definida en un fitxer YAML versionat, els seus canvis es revisen, es reverteixen i tenen historial. Si està configurada fent clic en un web, no hi ha historial, no hi ha revisió i ningú no sap qui va canviar què.
- Fixar les versions de tot
# Fràgil: "v4" pot canviar sota els teus peus
- uses: actions/checkout@v4
# Reproduïble: un hash concret
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11És exactament el mateix principi que regeix tot el curs: identificar per contingut, no per nom mòbil. Una etiqueta es pot moure; un hash, no.
- Idempotència
Executar la canonada dues vegades sobre el mateix commit ha de produir el mateix resultat. Si la segona execució falla perquè "l'etiqueta ja existeix" o "l'artefacte ja està publicat", la canonada no és idempotent i qualsevol reintent es converteix en una intervenció manual.
- Que falli ràpid i amb un missatge clar
# Dolent
terraform apply -auto-approve
# Bo
if ! git merge-base --is-ancestor "$COMMIT" origin/main; then
echo "ERROR: el commit $COMMIT no és a main."
echo "Només es desplega el que està integrat. Integra la petició primer."
exit 1
fiEl missatge ha de dir què ha passat i què fer. Un error que només diu exit 1 obliga a llegir el guió.
- Registrar el perquè, no només el què
git commit -m "deploy: produccio a v1.4.0
Commit: 4f8a2e6c9d3b1a5e7f2c8b0d4a6e9f1c3b5d7a0e
Tiquets: GT-231, GT-238
Aprovat per: Ana Ferrer
Execució: https://ci.exemple.cat/execucions/8842"Un desplegament anotat així s'investiga en un minut sis mesos després.
- Comprovacions prèvies explícites
Abans de tocar producció, verificar:
# El commit és a main?
git merge-base --is-ancestor "$COMMIT" origin/main
# La còpia de treball està neta?
git describe --always --dirty | grep -q -- '-dirty$' && exit 1
# L'etiqueta és anotada i està signada?
[ "$(git cat-file -t "$ETIQUETA")" = "tag" ] || exit 1
git tag --verify "$ETIQUETA"
# Existeix l'artefacte?
docker manifest inspect "registre.exemple.cat/gestor-tasques:$COMMIT" >/dev/null
- Les mateixes ordres en local i a la canonada
Si la CI executa npm test i tu també, reproduir una fallada és trivial. Si la CI executa dotze passos escrits al YAML que no existeixen en cap guió, no pots reproduir res. Extreu la lògica a guions del repositori i que la canonada només els invoqui.
- Branques protegides de debò
La protecció de main (07-06) és l'última barrera. Amb desplegament continu, un push --force a main pot desplegar qualsevol cosa. Prohibit, sense excepcions ni "comptes d'emergència".
Errors Habituals i Consells
Error 1: reconstruir l'artefacte a cada entorn. El que proves deixa de ser el que desplegues. Construir una vegada, desplegar moltes; el que canvia entre entorns és la configuració injectada.
Error 2: fer servir latest com a identificador. No identifica res. El hash complet del commit com a etiqueta canònica, i els noms llegibles com a àlies.
Error 3: git revert com a primera reacció a un incident. Desplega codi nou sense provar per arreglar un problema urgent. Primer redesplegar l'anterior, després arreglar main.
Error 4: oblidar fetch-depth: 0 quan la canonada fa servir git describe o compara branques. És l'error de la lliçó 10-04: amb un clon superficial no hi ha etiquetes ni base de fusió. I en repositoris grans, fetch-depth: 0 + filter: blob:none.
Error 5: secrets al repositori "perquè és privat". Un repositori privat té desenes de clons, rèpliques, còpies de seguretat i sistemes de CI amb accés. La regla no admet matisos.
Error 6: etiquetes lleugeres per a producció. Sense autor, sense data, sense missatge, sense signatura. I es poden moure amb git tag -f, cosa que un objecte d'etiqueta anotada i signada fa molt més difícil de dissimular.
Error 7: canviar producció a mà. Crea deriva i el repositori deixa de ser la font de veritat. Si un canvi urgent és inevitable, el commit corresponent es fa immediatament després, sense excepció.
Error 8: migracions de base de dades no compatibles cap enrere. Fan impossible la reversió. És la restricció operativa que més condiciona com es parteixen els commits.
Consell 1: exposa /version a l'aplicació. Amb el hash del commit. Respon a "què hi ha realment a producció?" sense accés al sistema de desplegament.
Consell 2: escriu el guió de reversió abans de necessitar-lo. I prova'l. Un dimarts al matí.
Consell 3: fes servir git describe --dirty per detectar construccions no reproduïbles. El sufix -dirty significa que aquell artefacte no es pot reconstruir des del repositori: rebutja'l.
Consell 4: separa el repositori d'aplicació del d'estat. Permisos diferents, historial d'auditoria separat, i menys possibilitats que un canvi de codi toqui producció per accident.
Consell 5: un plafó que compari el que està desplegat amb main. git rev-list --count $DESPLEGAT..origin/main diu quants commits esperen. És la mètrica de "quant valor està parat".
Consell 6: signa les etiquetes de producció. Així crear una versió requereix una clau privada, no només permisos d'escriptura al repositori.
Exercicis
Exercici 1: dissenyar la canonada de desplegament de gestor-tasques
L'equip vol:
- Cada integració a
maindesplega automàticament a preproducció. - Producció es desplega només en crear una etiqueta anotada i signada
vX.Y.Z. - L'artefacte es construeix una vegada i es reutilitza.
- Ha de ser possible revertir producció en menys d'un minut.
Escriu la canonada (o canonades) en YAML, indicant quines comprovacions de Git inclous a cada punt i per què. Explica el flux complet d'un canvi, des de la petició d'extracció fins a producció.
Exercici 2: traçabilitat inversa des d'un incident
Són les 03:12. Alerta: gestor-tasques retorna error 500 en producció. L'única cosa que saps és que el contenidor desplegat és:
Escriu la seqüència d'ordres per a:
- Identificar quina versió i quins canvis conté.
- Determinar si el problema va entrar amb l'últim desplegament.
- Trobar el commit sospitós.
- Revertir producció.
- Deixar
mainen un estat sa.
Explica en quin ordre ho faries i per què.
Exercici 3: detectar problemes en una canonada aliena
Revisa aquesta canonada i assenyala tots els problemes, ordenats per gravetat, explicant què pot fallar i com ho corregiries:
name: Desplegar
on:
push:
branches: [main]
jobs:
desplegar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@main
- name: Construir
run: |
docker build -t gestor-tasques:latest .
docker push registre.exemple.cat/gestor-tasques:latest
- name: Etiquetar la versió
run: |
VERSION=$(cat VERSION)
git tag $VERSION
git push origin $VERSION
- name: Desplegar a producció
run: |
ssh [email protected] \
"docker pull registre.exemple.cat/gestor-tasques:latest && \
DB_PASS=Estiu2026! docker compose up -d"
- name: Notificar
run: curl -X POST https://chat.exemple.cat/hook -d "Desplegat"Solucions
Solució 1
Canonada 1: integració (ja existeix, de la lliçó 07-06). Proves i linter a cada petició d'extracció, main protegida.
Canonada 2: construcció i desplegament a preproducció.
name: Construir i desplegar a preproducció
on:
push:
branches: [main]
jobs:
construir:
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.info.outputs.sha }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # describe necessita les etiquetes
- id: info
name: Recopilar la procedència
run: |
SHA=$(git rev-parse HEAD)
DESC=$(git describe --tags --always --dirty)
# COMPROVACIÓ: res de construccions no reproduïbles
case "$DESC" in
*-dirty) echo "ERROR: còpia de treball bruta"; exit 1 ;;
esac
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "desc=$DESC" >> "$GITHUB_OUTPUT"
- name: Construir l'artefacte UNA sola vegada
run: |
docker build \
--build-arg COMMIT_SHA="${{ steps.info.outputs.sha }}" \
--build-arg VERSION="${{ steps.info.outputs.desc }}" \
--build-arg DATA_CONSTRUCCIO="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-t "registre.exemple.cat/gestor-tasques:${{ steps.info.outputs.sha }}" .
docker push "registre.exemple.cat/gestor-tasques:${{ steps.info.outputs.sha }}"
preproduccio:
needs: construir
runs-on: ubuntu-latest
environment: preproduccio
steps:
- name: Desplegar l'artefacte ja construït
run: ./infra/desplegar.sh preproduccio "${{ needs.construir.outputs.sha }}"
- name: Comprovació de vida
run: |
sleep 10
DESPLEGAT=$(curl -sf https://pre.exemple.cat/version | jq -r .commit)
[ "$DESPLEGAT" = "${{ needs.construir.outputs.sha }}" ] \
|| { echo "ERROR: s'esperava un altre commit a /version"; exit 1; }Comprovacions incloses i per què:
| Comprovació | Motiu |
|---|---|
fetch-depth: 0 |
git describe necessita les etiquetes; un clon superficial les omet (10-04) |
Rebutjar -dirty |
Un artefacte construït des d'una còpia bruta no és reproduïble |
| Etiquetar amb el hash complet | Identificador canònic i no ambigu |
Incrustar procedència com a LABEL i ENV |
Permet el /version i l'auditoria de l'artefacte |
Comprovació de vida contra /version |
Verifica que es va desplegar el que es creia, no una cosa de la memòria cau |
Canonada 3: producció per etiqueta.
name: Desplegar a producció
on:
push:
tags: ['v*.*.*']
jobs:
desplegar:
runs-on: ubuntu-latest
environment: produccio # amb revisors obligatoris
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Verificacions prèvies
id: verif
run: |
ETIQUETA="${{ github.ref_name }}"
# 1. Ha de ser anotada, no lleugera
[ "$(git cat-file -t "$ETIQUETA")" = "tag" ] \
|| { echo "ERROR: $ETIQUETA és lleugera. Producció exigeix anotada."; exit 1; }
# 2. Ha d'estar signada
git tag --verify "$ETIQUETA" \
|| { echo "ERROR: signatura no vàlida a $ETIQUETA"; exit 1; }
# 3. Ha de ser a main
git fetch origin main
git merge-base --is-ancestor "$ETIQUETA" origin/main \
|| { echo "ERROR: $ETIQUETA no és a main"; exit 1; }
# 4. L'artefacte ha d'existir JA (no es reconstrueix)
SHA=$(git rev-list -n1 "$ETIQUETA")
docker manifest inspect "registre.exemple.cat/gestor-tasques:$SHA" >/dev/null \
|| { echo "ERROR: no hi ha artefacte per a $SHA"; exit 1; }
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
- name: Desplegar
run: ./infra/desplegar.sh produccio "${{ steps.verif.outputs.sha }}"
- name: Verificar i registrar
run: |
sleep 15
DESPLEGAT=$(curl -sf https://gestor-tasques.exemple.cat/version | jq -r .commit)
[ "$DESPLEGAT" = "${{ steps.verif.outputs.sha }}" ] || exit 1
echo "Producció: ${{ github.ref_name }} (${{ steps.verif.outputs.sha }})"
git log --oneline "$(git describe --tags --abbrev=0 '${{ github.ref_name }}^')"..'${{ github.ref_name }}'Canonada 4: reversió ràpida (el requisit de "menys d'un minut").
name: Revertir producció
on:
workflow_dispatch:
inputs:
etiqueta:
description: 'Etiqueta a la qual revertir (p. ex. v1.3.0)'
required: true
jobs:
revertir:
runs-on: ubuntu-latest
environment: produccio-urgent # sense revisors: és una emergència
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Desplegar l'artefacte EXISTENT d'aquesta etiqueta
run: |
SHA=$(git rev-list -n1 "${{ inputs.etiqueta }}")
docker manifest inspect "registre.exemple.cat/gestor-tasques:$SHA" >/dev/null \
|| { echo "ERROR: no existeix artefacte per a aquesta etiqueta"; exit 1; }
./infra/desplegar.sh produccio "$SHA"És ràpida precisament perquè no construeix res: l'artefacte ja existeix des del dia en què aquell commit va entrar a main.
Flux complet d'un canvi:
- L'Ana crea
GT-231-filtre-ocultes, treballa, obre la petició d'extracció. - La CI executa proves i linter (07-06). En Bruno revisa i aprova.
- S'integra a
main(branca protegida, condensat). - La canonada 2 construeix l'artefacte
:4f8a2e6...una vegada i desplega a preproducció. - L'equip valida a preproducció.
- Quan toca publicar:
./eines/seguent-versio.shcalculav1.4.0des dels Conventional Commits, es genera el changelog i es creagit tag -a -s v1.4.0. - El
pushde l'etiqueta dispara la canonada 3, que verifica i desplega l'artefacte ja construït al pas 4. - Si alguna cosa falla: canonada 4 amb
v1.3.0, en segons. Després,git revertamainamb calma.
Solució 2
L'ordre importa moltíssim, i és contraintuïtiu: primer es restableix el servei, després s'investiga. Però amb una excepció: els passos 1 a 3 costen segons i donen la informació necessària per revertir amb criteri.
# ========== FASE 1: SITUAR-SE (30 segons) ==========
# 1. Quin commit és i quina versió?
git show --stat 4f8a2e6c
git tag --points-at 4f8a2e6c
git describe --tags 4f8a2e6c# 2. Què ha canviat respecte a la versió anterior?
ANTERIOR=$(git describe --tags --abbrev=0 4f8a2e6c^)
echo "Versió anterior: $ANTERIOR"
git log --oneline "$ANTERIOR"..4f8a2e6cVersió anterior: v1.3.0 4f8a2e6 feat(tasques): GT-231 filtrar les tasques ocultes del llistat 8a1f6c3 fix(interficie): GT-238 corregir el comptador després d'eliminar 2e9f4c7 chore: actualitzar dependències
# 3. Quan es va desplegar? Coincideix amb l'inici de l'alerta?
git log -1 --format='%ci' 4f8a2e6c
git for-each-ref --format='%(refname:short) %(creatordate:iso)' refs/tags/v1.4.0Si l'etiqueta es va crear a les 03:05 i l'alerta va saltar a les 03:12, el problema va entrar amb aquest desplegament. Aquesta correlació és el que justifica revertir en lloc d'investigar.
# ========== FASE 2: RESTABLIR (immediat) ==========
# 4. Revertir a la versió anterior, JA
./eines/revertir-produccio.sh
# o directament:
./infra/desplegar.sh produccio "$(git rev-list -n1 v1.3.0)"
# 5. Verificar
curl -sf https://gestor-tasques.exemple.cat/version | jqPer què aquí i no després: cada minut d'investigació és un minut de servei caigut. L'artefacte de v1.3.0 va estar funcionant fins fa vint minuts: és l'opció de risc zero.
Per què NO fer git revert ara: generaria un commit nou, caldria construir-lo (minuts), provar-lo i desplegar-lo, i aquell artefacte mai no ha estat en producció. És exactament el que no es vol durant un incident.
# ========== FASE 3: INVESTIGAR (amb el servei restablert) ==========
# 6. Només tres commits sospitosos. Veure què fa cadascun
git log -p "$ANTERIOR"..4f8a2e6c
# 7. Si no és evident, bisect entre les dues versions (lliçó 06-02)
git bisect start 4f8a2e6c v1.3.0
git bisect run ./proves/reproduir-error-500.sh
# 8. Quan aparegui el culpable, entendre la intenció (lliçó 06-03)
git log -1 --format=%B <commit-culpable>
git blame -L 40,60 app.jsAmb només tres commits, bisect és exagerat; es faria servir en un rang de cinquanta. Aquí, llegir els tres diffs és més ràpid.
# ========== FASE 4: DEIXAR main SA ==========
# 9a. Si la causa és clara i és petita: corregir cap endavant
git switch -c GT-245-corregir-error-500 main
# ... arreglar ...
git commit -m "fix(tasques): GT-245 corregeix l'error 500 en filtrar tasques ocultes
El filtre introduït a GT-231 no contemplava el cas de tasques sense
la marca 'oculta' definida, cosa que provocava una excepció no capturada
a renderitzaTasques().
S'afegeix una comprovació explícita i una prova de regressió.
Refs: GT-231, GT-245"
# Petició d'extracció, revisió, integració, versió nova v1.4.1
# 9b. Si no està clara o hi ha pressa: revertir el commit problemàtic
git revert 4f8a2e6c
git push origin mainPer què el pas 9 és obligatori: si només es va redesplegar v1.3.0, main continua contenint la fallada. La pròxima persona que publiqui una versió la portarà un altre cop a producció sense saber-ho. La reversió del desplegament restableix el servei; només el pas 9 arregla el problema.
# ========== FASE 5: APRENDRE ==========
# Per què les proves no ho van detectar? -> afegir la prova que falta.
# Per què no ho vam veure a preproducció? -> falten dades representatives?
# Quant va trigar la reversió? -> si va ser molt, millorar el guió.Solució 3
Vuit problemes, de més greu a menys.
1. CRÍTIC — Secret en text clar al guió.
La contrasenya de la base de dades de producció és en un fitxer versionat, a l'historial per sempre, visible per a tothom qui cloni, i probablement als registres de cada execució de la canonada. Viola la lliçó 08-05 de la manera més directa possible.
Correcció: revocar la contrasenya ara mateix (abans que res), desar-la al gestor de secrets, injectar-la com a variable d'entorn del sistema de CI i, si el repositori és públic o ha estat clonat, netejar l'historial amb git filter-repo (08-05).
2. CRÍTIC — Desplegament a producció sense cap barrera.
Qualsevol integració a main va directa a producció, sense environment, sense aprovació, sense comprovacions prèvies, sense preproducció. Un canvi d'una línia mal revisat arriba als usuaris en dos minuts.
Correcció: separar en dues canonades (integració a preproducció per branca; producció per etiqueta anotada i signada), i declarar environment: produccio amb revisors obligatoris.
3. GREU — latest com a identificador.
No identifica res. Impossible saber què s'està executant, impossible revertir a una versió concreta, impossible reproduir un problema. I una condició de cursa evident: si dues integracions passen seguides, la segona pot sobreescriure latest mentre la primera desplega.
Correcció: etiquetar amb el hash complet del commit i fer servir els noms llegibles només com a àlies.
4. GREU — Es reconstrueix a cada desplegament i no hi ha separació construcció/desplegament.
No hi ha entorn de preproducció, així que el que arriba a producció no s'ha provat mai desplegat. I com que es construeix aquí mateix, no hi ha manera de garantir que l'artefacte de producció sigui el mateix que va passar les proves.
Correcció: construir una vegada amb el hash, desplegar aquest mateix artefacte a preproducció i després a producció.
5. GREU — La versió surt d'un fitxer VERSION i l'etiqueta és lleugera.
Tres problemes en tres línies:
- Etiqueta lleugera (
git tagsense-a): sense autor, sense data, sense missatge, sense signatura. - Falla si la versió no ha canviat:
git tagsobre una etiqueta existent dona error i trenca la canonada. No és idempotent. - El fitxer
VERSIONs'actualitza a mà: algú se n'oblidarà.
Correcció: calcular la versió des dels Conventional Commits (apartat 8) i crear etiqueta anotada i signada:
6. MITJÀ — actions/checkout@main.
Referència a una branca mòbil: el que la canonada executa pot canviar sense que ningú ho decideixi. És un risc de cadena de subministrament i de reproductibilitat.
Correcció: fixar per hash. I afegir fetch-depth: 0 si es faran servir etiquetes o comparacions.
7. MITJÀ — Sense comprovacions ni verificació posterior.
No comprova res abans de tocar producció, i no verifica res després. La notificació diu "Desplegat" passi el que passi, perquè el curl s'executa igualment: no hi ha if: success(), ni es comprova que el servei respongui.
Correcció: comprovacions prèvies (commit a main, còpia neta, artefacte existent) i una comprovació de vida contra /version que confirmi el hash desplegat.
8. MENOR — ssh amb docker compose com a mecanisme de desplegament.
Sense control de versions de l'estat, sense reversió, sense desplegament progressiu, sense registre. Si la connexió SSH es talla a mitges, l'estat queda indeterminat.
Correcció: un guió de desplegament versionat al repositori (infra/desplegar.sh) que sigui idempotent i registri el que fa; i a mitjà termini, valorar GitOps (apartat 6), que a més elimina la necessitat que la canonada tingui credencials de producció.
Resum de la correcció: aquesta canonada fa tot en una sola tasca sense barreres, amb un secret en clar i sense identificació reproduïble de l'artefacte. La reescriptura correcta és la de l'exercici 1: separar construcció de desplegament, identificar pel hash, exigir etiqueta anotada i signada per a producció, injectar secrets des del gestor, i afegir verificació abans i després.
Conclusió
Git deixa de ser una eina de desenvolupadors i passa a ser la peça central de l'operació.
- El repositori es converteix en la font única de veritat: no només codi, també infraestructura, configuració per entorn, migracions i la definició de les mateixes canonades. La regla: si alguna cosa pot trencar producció, ha d'estar a Git; i si és a Git, no s'ha de canviar fora de Git. Amb una excepció absoluta: els secrets.
- Integració, lliurament i desplegament continus es distingeixen per on és el botó humà: la integració garanteix que el que entra funciona, el lliurament que el que hi ha a
maines pot desplegar, i el desplegament que està desplegat. Treure el botó és una decisió de confiança que exigeix proves fiables, reversió en minuts i canvis petits. - Un desplegament es dispara per branca (preproducció), per etiqueta anotada i signada (producció, tancant la 05-05) o per aprovació manual sobre un commit. Les verificacions que importen són de Git pur:
git cat-file -tper exigir etiqueta anotada,git tag --verifyper a la signatura, igit merge-base --is-ancestorper exigir que sigui amain. - L'artefacte s'identifica pel hash complet del commit, i es construeix una vegada i es desplega moltes. És la mateixa idea que sosté tot Git: identificar per contingut, no per un nom que es mou. D'aquí surt la traçabilitat completa —del contenidor al tiquet en cinc ordres— i el
git describe --dirtyque detecta construccions no reproduïbles. - La infraestructura com a codi dona als servidors tot el que Git sap fer: historial,
blame, revisió,revert. I les migracions de base de dades imposen la restricció que més condiciona els commits: han de ser compatibles cap enrere durant almenys un desplegament, perquè el codi es reverteix i les dades no. - GitOps inverteix el sentit: un agent dins del sistema estira de l'estat declarat i reconcilia. Guanya en seguretat (la canonada no té credencials de producció), detecta i corregeix la deriva, i converteix l'historial de Git en el registre d'auditoria. A canvi, el control d'accés al repositori és el control d'accés a producció, i les branques protegides deixen de ser una recomanació.
- Els secrets s'injecten, mai no es versionen; i la pràctica moderna substitueix les credencials de llarga vida per tokens de curta durada obtinguts per identitat federada.
- El versionatge i el changelog automàtics cobren la inversió de la 08-01: els Conventional Commits són dades estructurades de les quals surten la versió SemVer, el changelog i —via etiqueta— el desplegament.
- I la reversió en producció és gairebé sempre redesplegar l'artefacte anterior, no
git revert: l'artefacte anterior va estar funcionant fa deu minuts i està llest en segons, mentre que unrevertgenera codi nou sense provar que cal construir i desplegar. Primer es restableix el servei; després es deixamainsa, cosa que no és opcional.
La idea que travessa tota la lliçó:
En una organització amb desplegament automatitzat,
git pushdeixa de significar "he desat la meva feina" i passa a significar "això arribarà als usuaris". Tot el rigor del curs —commits atòmics, missatges que expliquen el perquè, historial net, etiquetes anotades, branques protegides— deixa de ser una virtut professional i es converteix en un requisit operatiu.
El que ve
Ja saps fer servir Git en tots els contextos que existeixen avui: sol, en equip, en projectes enormes i en producció automatitzada. Queda una última pregunta, que és la més honesta que es pot fer al final d'un curs tècnic: quant d'això continuarà sent cert d'aquí a deu anys?
Git no està quiet. Hi ha coses que ja existeixen i pots fer servir avui encara que gairebé ningú no les conegui: un format de referències nou que substitueix refs/ i packed-refs, un git que pot funcionar amb SHA-256 en lloc de SHA-1, els clons parcials i l'índex dispers que acabem de veure, i la consolidació de switch i restore enfront del vell checkout. I hi ha coses que són tendència o especulació, que convé distingir amb claredat de les anteriors.
També hi ha alguna cosa que gairebé amb seguretat no canviarà, i és precisament el que vas aprendre al mòdul 1: el model de dades adreçable per contingut, el graf dirigit acíclic de commits i la naturalesa distribuïda. Per això el que portes après no caduca.
La lliçó 10-06: El Futur de Git tanca el curs: què està madur i què és promesa, cap a on apunten les eines, com continuar aprenent pel teu compte, i una recapitulació completa del camí recorregut.
Dominant Git: De Principiant a Avançat
Mòdul 1: Introducció a Git
- Què és Git?
- Instal·lant Git
- Terminologia Bàsica de Git
- El Model de Dades de Git
- Configurant Git
- Configuració Inicial
Mòdul 2: Operacions Bàsiques de Git
- Creant un Repositori
- Clonant un Repositori
- Flux de Treball Bàsic de Git
- Preparant i Confirmant Canvis
- Inspeccionant Canvis amb git diff
- Visualitzant l'Historial de Confirmacions
Mòdul 3: Branques i Fusió
- Entenent les Branques
- Creant i Canviant Branques
- Fusionant Branques
- Estratègies de Fusió
- Resolent Conflictes de Fusió
- Gestió de Branques
Mòdul 4: Treballant amb Repositoris Remots
- Entenent els Repositoris Remots
- Afegint un Repositori Remot
- Autenticació amb Repositoris Remots
- Obtenint i Baixant Canvis
- Enviant Canvis
- Rastrejant Branques
Mòdul 5: Operacions Avançades de Git
- Rebase
- Rebase Interactiu
- Cherry-Picking de Confirmacions
- Desant Canvis Temporals
- Etiquetant Confirmacions
- Revertint Confirmacions
Mòdul 6: Eines i Tècniques de Git
- Usant Git Hooks
- Git Bisect
- Git Blame
- Git Log i Àlies
- Submòduls de Git
- Múltiples Còpies de Treball amb git worktree
Mòdul 7: Estratègies de Col·laboració i Flux de Treball
- Forks i Pull Requests
- Revisions de Codi amb Git
- Flux de Treball Git Flow
- GitHub Flow
- Trunk Based Development
- Integració Contínua amb Git
Mòdul 8: Bones Pràctiques i Consells de Git
- Escrivint Bons Missatges de Confirmació
- Mantenint un Historial Net
- Ignorant Fitxers amb .gitignore
- Atributs de Fitxer amb .gitattributes
- Bones Pràctiques de Seguretat
- Consells de Rendiment
Mòdul 9: Resolució de Problemes i Depuració
- Problemes Habituals de Git
- Desfent Canvis
- Resolent Divergències amb el Remot
- Recuperant Confirmacions Perdudes
- Tractant amb Repositoris Corruptes
- Tècniques Avançades de Depuració
