🚧

CI/CD per Svogliati

La guida alle pipeline che non volevi configurare ma che ti salveranno dal deploy manuale alle 3 di notte.

Da "git push e prego" a "la pipeline fa tutto da sola"

Alla fine CI/CD non è poi così terribile, basta che qualcuno te lo spieghi senza il tono da "DevOps Engineer con 47 badge su Credly".

Capitolo 01

Che Cavolo è CI/CD?

Spoiler: è solo un modo per smettere di deployare a mano

🍔 Analogia per Svogliati

Immagina di lavorare in una fabbrica di biscotti. Ogni volta che inventi una ricetta nuova (codice), devi: impastare, cuocere, assaggiare, confezionare e spedire. CI/CD è il nastro trasportatore automatico: tu butti l'impasto, e lui fa tutto il resto. Se il biscotto è brutto (test fallito), il nastro si ferma prima di spedire porcherie ai clienti.

CI — Continuous Integration

Ogni volta che fai push, il codice viene compilato e testato automaticamente. Se qualcosa si rompe, lo sai subito — non dopo 3 settimane quando il QA ti manda un'email passivo-aggressiva.

💡 In pratica: push → build → test → risultato in 5 minuti. Se è verde vai avanti, se è rosso hai rotto qualcosa.

🚀 CD — Continuous Delivery/Deployment

Delivery: il codice testato è pronto per andare in produzione (ma qualcuno preme il bottone). Deployment: va in produzione da solo. Sì, da solo. Terrificante? Un po'. Ma funziona.

⚠️ Delivery ≠ Deployment. Con Delivery decidi tu quando rilasciare. Con Deployment il merge su main va dritto in prod. Scegli in base a quanto ti fidi dei tuoi test.

💔 Senza CI/CD

Scrivi codice → "funziona da me" → zip via email → FTP sul server → 500 Internal Server Error → panico → "chi ha toccato il server?" → il capo ti guarda male

VS

💚 Con CI/CD

Push su Git → la pipeline builda → i test passano → deploy automatico → tutto funziona → tu vai a prendere un caffè → il capo pensa che sei un genio

🚧 Il Flusso CI/CD — Dal Codice alla Produzione
💻 Code git push 📦 Build Compila / Bundle 🧪 Test Unit + Integration 🔎 Scan Security / Lint 🚗 Stage Ambiente di test 🎯 PROD CI inizia qui CI finisce qui CD inizia qui CD finisce qui CONTINUOUS INTEGRATION CONTINUOUS DELIVERY
Capitolo 02

I Concetti Base

Il vocabolario minimo per non fare figure di merda

🚧

Pipeline

L'intero processo automatizzato dal push al deploy. Come una catena di montaggio: ogni pezzo fa il suo lavoro.

📌

Stage

Un gruppo di job che gira insieme. Es: "build", "test", "deploy". Gli stage vanno in ordine sequenziale.

⚙️

Job

Un singolo compito dentro uno stage. Es: "esegui i test unitari", "builda l'immagine Docker". I job nello stesso stage possono girare in parallelo.

📦

Artifact

Un file prodotto da un job e passato al successivo. Es: il JAR compilato, il binario, l'immagine Docker, il report dei test.

🏃

Runner

La macchina (vera o virtuale) che esegue i job. Può essere ospitata dal provider (GitHub/GitLab) o self-hosted (il tuo server triste in cantina).

🔌

Trigger

L'evento che fa partire la pipeline. Push, merge request, cron, tag, o il tuo collega che clicca "Run pipeline" per sbaglio.

📑 Come si Relazionano

Ecco la gerarchia, dal più grande al più piccolo:

gerarchia-cicd
# La gerarchia CI/CD
Pipeline          # L'intero processo (1 per push/evento)
  → Stage        # Fase logica (build, test, deploy...)
      → Job     # Singolo compito (compila, linta, testa...)
         → Step # Singola istruzione dentro un job

# Esempio concreto:
Pipeline "Deploy App"
  Stage: Build
      Job: build-frontend    # npm run build
      Job: build-backend     # go build
  Stage: Test
      Job: unit-tests        # pytest
      Job: integration-tests # cypress
  Stage: Deploy
      Job: deploy-staging    # kubectl apply
      Job: deploy-prod       # solo se staging OK
Capitolo 03

GitHub Actions

Il CI/CD di GitHub — quello che usi se il repo è su GitHub (duh)

📁 Dove Mettere i File

GitHub Actions cerca i workflow in .github/workflows/. Ogni file YAML lì dentro è un workflow separato.

struttura-progetto
il-tuo-progetto/
 ├── .github/
 │   └── workflows/
 │       ├── ci.yml          # pipeline principale
 │       ├── deploy.yml      # deploy in prod
 │       └── nightly.yml     # test notturni
 ├── src/
 ├── package.json
 └── ...

📄 Il Primo Workflow — Commentato Riga per Riga

Ecco un workflow completo per un progetto Node.js. Ogni riga ha il suo perché.

.github/workflows/ci.yml
# Nome del workflow (appare nella tab Actions di GitHub)
name: CI Pipeline

# QUANDO gira questo workflow?
on:
  push:
    branches: [ main, develop ]     # solo su questi branch
  pull_request:
    branches: [ main ]              # PR verso main

# I JOB che compongono il workflow
jobs:
  # --- JOB 1: Build e Test ---
  build-and-test:
    runs-on: ubuntu-latest          # su quale macchina gira

    strategy:
      matrix:
        node-version: [18, 20, 22]   # testa su 3 versioni di Node

    steps:
      # Step 1: Scarica il codice
      - uses: actions/checkout@v4    # action ufficiale di GitHub

      # Step 2: Installa Node.js
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'              # cache le dipendenze

      # Step 3: Installa dipendenze
      - run: npm ci                  # ci = clean install (più veloce)

      # Step 4: Linting
      - run: npm run lint

      # Step 5: Test
      - run: npm test

      # Step 6: Build
      - run: npm run build

      # Step 7: Salva gli artifact
      - uses: actions/upload-artifact@v4
        with:
          name: build-output-${{ matrix.node-version }}
          path: dist/

🚀 Workflow con Deploy

Un workflow più completo: build, test, e deploy su staging/produzione.

.github/workflows/deploy.yml
name: Build & Deploy

on:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test

  build-image:
    needs: test                      # aspetta che test finisca
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: |
          docker build -t myapp:${{ github.sha }} .
          docker push myregistry/myapp:${{ github.sha }}

  deploy-staging:
    needs: build-image
    runs-on: ubuntu-latest
    environment: staging              # protezione ambiente
    steps:
      - run: |
          kubectl set image deployment/myapp \
            myapp=myregistry/myapp:${{ github.sha }}

  deploy-prod:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production           # richiede approvazione manuale
    steps:
      - run: |
          kubectl set image deployment/myapp \
            myapp=myregistry/myapp:${{ github.sha }}
💡 Tip da Svogliato: La keyword needs definisce le dipendenze tra job. Senza needs, i job girano in parallelo. Con needs: test, il job aspetta che "test" finisca con successo.

🔌 Le Actions Più Usate

actions-essenziali
# Le action che userai nel 90% dei workflow

actions/checkout@v4        # clona il repo
actions/setup-node@v4      # installa Node.js
actions/setup-python@v5    # installa Python
actions/setup-go@v5        # installa Go
actions/setup-java@v4      # installa Java
actions/cache@v4           # cache dipendenze (npm, pip, ecc)
actions/upload-artifact@v4 # salva file tra job
actions/download-artifact@v4 # scarica artifact da job precedente
docker/build-push-action@v5 # build + push immagine Docker
docker/login-action@v3     # login a Docker registry
Capitolo 04

GitLab CI

L'alternativa — un solo file .gitlab-ci.yml nella root e via

📄 Il Primo .gitlab-ci.yml

GitLab CI è più lineare: un file, stage ordinati, job che li popolano.

.gitlab-ci.yml
# Definisci l'ordine degli stage
stages:
  - build
  - test
  - deploy

# Variabili globali
variables:
  NODE_VERSION: "20"

# Immagine Docker di default per tutti i job
default:
  image: node:${NODE_VERSION}

# --- STAGE: BUILD ---
build:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:                         # passa i file allo stage successivo
    paths:
      - dist/
    expire_in: 1 hour

# --- STAGE: TEST ---
unit-test:
  stage: test
  script:
    - npm ci
    - npm test
  coverage: '/Statements\s*:\s*(\d+\.?\d*)%/'

lint:
  stage: test                        # stesso stage = parallelo a unit-test
  script:
    - npm ci
    - npm run lint

# --- STAGE: DEPLOY ---
deploy-staging:
  stage: deploy
  script:
    - echo "Deploying to staging..."
    - kubectl apply -f k8s/staging/
  environment:
    name: staging
    url: https://staging.example.com
  only:
    - develop

deploy-prod:
  stage: deploy
  script:
    - echo "Deploying to production..."
    - kubectl apply -f k8s/production/
  environment:
    name: production
    url: https://example.com
  only:
    - main
  when: manual                       # richiede click manuale

⚖️ GitHub Actions vs GitLab CI — Rosetta Stone

Concetto GitHub Actions GitLab CI
File config .github/workflows/*.yml .gitlab-ci.yml
Trigger on: push/pull_request only/rules
Macchina runs-on: ubuntu-latest image: node:20
Passi steps: [run, uses] script: [...]
Dipendenze needs: [job-name] stage: (ordine implicito)
Secrets ${{ secrets.TOKEN }} $TOKEN (CI/CD Variables)
Cache actions/cache@v4 cache: paths: [...]
Manuale workflow_dispatch when: manual
Capitolo 05

Trigger & Events

Quando far partire la pipeline (e quando NO)

🔌 I Trigger Più Comuni (GitHub Actions)

triggers.yml
on:

  # Push su branch specifici
  push:
    branches: [ main, develop ]
    paths:                           # SOLO se questi file cambiano
      - 'src/**'
      - 'package.json'
    paths-ignore:                    # ignora questi
      - 'docs/**'
      - '**.md'

  # Pull request
  pull_request:
    branches: [ main ]
    types: [ opened, synchronize ]   # solo apertura e nuovi push

  # Manuale (bottone "Run workflow")
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

  # Cron (schedulato)
  schedule:
    - cron: '0 3 * * 1-5'          # lun-ven alle 3:00 UTC

  # Quando viene creato un tag
  push:
    tags:
      - 'v*'                        # v1.0.0, v2.3.1, ecc.

  # Quando un altro workflow finisce
  workflow_run:
    workflows: [ "CI Pipeline" ]
    types: [ completed ]
⚠️ Trappola classica: se usi paths e paths-ignore insieme, paths-ignore vince. Usane solo uno dei due per non impazzire.
Capitolo 06

Secrets & Variables

Come NON committare la password del database nel repo

🔐 Dove Mettere i Segreti

MAI, MAI, MAI nel codice. MAI nel YAML. MAI in un commento "tanto nessuno lo legge". I segreti vanno nei secret manager della piattaforma.

GitHub

Settings → Secrets and variables → Actions → New repository secret

GitLab

Settings → CI/CD → Variables → Add variable (Flag "Masked" + "Protected")

💻 Usare i Secrets nel Workflow

secrets-example.yml
# GitHub Actions
steps:
  - name: Deploy
    run: |
      echo "Deploying..."
      kubectl apply -f k8s/
    env:
      KUBE_TOKEN: ${{ secrets.KUBE_TOKEN }}        # dal secret store
      REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }}

  - name: Login Docker
    uses: docker/login-action@v3
    with:
      username: ${{ secrets.DOCKER_USER }}
      password: ${{ secrets.DOCKER_PASS }}

# GitLab CI (più semplice, sono già env vars)
deploy:
  script:
    - docker login -u $DOCKER_USER -p $DOCKER_PASS
    - kubectl --token=$KUBE_TOKEN apply -f k8s/
🚨 PERICOLO MORTALE: I secrets non appaiono nei log (vengono mascherati con ***). Ma se fai echo $SECRET | base64 o li metti in un artifact, li esponi comunque. Non fare il furbo.
Capitolo 07

Docker in CI/CD

Build dell'immagine, push al registry, deploy — tutto automatico

🐋 Build & Push con GitHub Actions

.github/workflows/docker.yml
name: Docker Build & Push

on:
  push:
    branches: [ main ]
    tags: [ 'v*' ]

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Login al registry
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # Genera tag intelligenti
      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha

      # Build e push
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
💡 Tip: cache-from/cache-to: type=gha usa la cache di GitHub Actions per i layer Docker. La build successiva sarà molto più veloce se il Dockerfile non è cambiato tanto.
Capitolo 08

Deploy Strategies

Come mandare in produzione senza distruggere tutto

💣 Big Bang

Togli il vecchio, metti il nuovo. Tutto insieme. Downtime garantito.

rischio alto

Quando usarlo: mai in prod. Forse in dev se sei pigro.

🔄 Rolling Update

Sostituisci le istanze una alla volta. Zero downtime. Default in K8s.

rischio basso

Quando usarlo: il 90% delle volte. È il default, fai bene a usarlo.

🐦 Blue/Green

Due ambienti identici. Uno live (blue), uno con la nuova versione (green). Switch istantaneo del traffico.

rischio medio

Quando usarlo: quando vuoi rollback istantaneo (basta tornare a blue).

🐧 Canary

Mandi il 5% del traffico alla nuova versione. Se va tutto bene, aumenti gradualmente a 100%.

rischio basso

Quando usarlo: per cambiamenti rischiosi. Vuoi vedere se esplode prima di mandare tutto.

🔄 Rolling Update vs Blue/Green vs Canary
Rolling Update v1 v1 v1 v2 Una alla volta → zero downtime Blue/Green BLUE (live) GREEN (new) Switch istantaneo del traffico Canary v1 (95%) v2 (5%) Traffico graduale → meno rischio Esempio nel tempo Rolling t1 t2 t3 ✓ Completo Blue/Green LIVE prep idle LIVE ↓ switch! ✓ Istantaneo Canary 95% 5% 50% 50% 100% v2 ✓ Graduale
Capitolo 09

Troubleshooting

Quando la pipeline è rossa e non sai perché

🔴 "Error: Process completed with exit code 1" +

L'errore più generico dell'universo. Significa solo "qualcosa è fallito".

fix
# 1. Leggi il log INTERO, non solo l'ultima riga
# 2. Cerca la PRIMA riga rossa/di errore
# 3. Cause comuni:
- Test fallito             # leggi quale test e perché
- npm ci fallito           # package-lock.json non aggiornato?
- Comando non trovato      # manca un tool? Installalo nello step prima
- Permessi                 # chmod +x script.sh
🔴 "Error: Container action is only supported on Linux" +

Stai usando runs-on: macos-latest o windows-latest con un'action che richiede Docker.

fix
# Cambia il runner a Linux:
runs-on: ubuntu-latest
🔴 "Resource not accessible by integration" +

Il GITHUB_TOKEN non ha i permessi giusti.

fix
# Aggiungi i permessi nel workflow:
permissions:
  contents: read
  packages: write
  pull-requests: write
🔴 La pipeline è lentissima (15+ minuti) +

Di solito il colpevole è npm install senza cache o Docker build senza layer cache.

fix
# 1. Cache npm/pip/yarn
- uses: actions/setup-node@v4
  with:
    cache: 'npm'

# 2. Cache Docker layers
- uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

# 3. Usa npm ci invece di npm install
# 4. Parallelizza i job (togli needs se non serve)
# 5. Usa path filters per skippare pipeline inutili
🔴 "Your account has been flagged" / Runner non parte +

Su GitHub, i runner gratuiti hanno dei limiti. Se li superi o GitHub ti segnala per mining:

fix
# Verifica i limiti: Settings → Billing → Actions
# Free: 2000 min/mese (Linux)
# Soluzione: usa self-hosted runner o ottimizza le pipeline

# Self-hosted runner (il tuo server):
runs-on: self-hosted
Capitolo 10

Cheat Sheet

Copia, incolla, funziona

📌 GitHub Actions — I Pattern Più Usati

Cosa Vuoi Fare Come
Runnare solo su main on: push: branches: [main]
Skippare se cambi solo docs paths-ignore: ['docs/**', '**.md']
Runnare manualmente on: workflow_dispatch
Runnare su cron on: schedule: - cron: '0 3 * * *'
Job B dopo Job A needs: [job-a]
Usare un secret ${{ secrets.MY_SECRET }}
Cachare npm setup-node con cache: 'npm'
Continuare se un job fallisce continue-on-error: true
Runnare solo su tag on: push: tags: ['v*']
Matrice di versioni strategy: matrix: node: [18, 20, 22]
Condizione if if: github.ref == 'refs/heads/main'
Timeout per job timeout-minutes: 10

📌 GitLab CI — I Pattern Più Usati

Cosa Vuoi Fare Come
Runnare solo su main only: [main] oppure rules: - if: $CI_COMMIT_BRANCH == "main"
Deploy manuale when: manual
Passare file tra stage artifacts: paths: [dist/]
Cache dipendenze cache: paths: [node_modules/]
Job che può fallire allow_failure: true
Variabile d'ambiente variables: MY_VAR: "value"
Job solo su merge request only: [merge_requests]
Timeout timeout: 10 minutes

🔧 Template Pronti — Copia e Adatta

Python + pytest

.github/workflows/python-ci.yml
name: Python CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'
      - run: pip install -r requirements.txt
      - run: pytest --cov=app tests/

Go

.github/workflows/go-ci.yml
name: Go CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.22'
      - run: go test ./...
      - run: go build -o app .

Static Site (HTML/CSS/JS)

.github/workflows/pages.yml
name: Deploy to GitHub Pages
on:
  push:
    branches: [ main ]
permissions:
  pages: write
  id-token: write
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/configure-pages@v4
      - uses: actions/upload-pages-artifact@v3
        with:
          path: '.'
      - uses: actions/deploy-pages@v4
        id: deployment
Capitolo 11

🚨 SOS alle 3 di Notte

La pipeline è rotta, il deploy è fallito, il capo chiama. Cosa fai?

🆙 Checklist di Emergenza

1

🚫 STOP — Non Toccare Niente

Respira. Non fare push random "per provare". Non fare git push --force. Non fare niente finché non hai capito cos'è rotto.

2

🔎 Leggi i Log

Vai nella tab Actions/Pipelines. Apri il job fallito. Leggi dall'inizio, non solo l'ultima riga. Il primo errore è quasi sempre la causa, il resto sono conseguenze.

3

🔬 È Colpa Tua o dell'Infrastruttura?

Colpa tua: il codice non compila, test falliti, typo nel YAML. Infrastruttura: runner non disponibile, timeout, rate limit di npm/Docker Hub, disco pieno.

4

🔄 Rollback se Necessario

Se il deploy è andato male, rollback prima, debug dopo.

rollback-rapido
# Kubernetes
kubectl rollout undo deployment/myapp

# Docker Compose
docker compose up -d --force-recreate myapp:previous-tag

# Git: torna al commit precedente e re-deploy
git revert HEAD && git push
5

✍️ Fix e Re-run

Fixxa il problema, pusha, aspetta che la pipeline passi. Se non sai cos'è rotto, prova a ri-runnare il job — a volte sono errori transienti (network timeout, runner pieno).

💦 Comandi di Sopravvivenza

comandi-emergenza
# === GITHUB ACTIONS (con gh CLI) ===

# Vedi l'ultimo run
gh run list --limit 5

# Vedi i log di un run fallito
gh run view <run-id> --log-failed

# Re-run un workflow fallito
gh run rerun <run-id> --failed

# Disabilita un workflow (emergenza)
gh workflow disable ci.yml

# Triggera manualmente un workflow
gh workflow run deploy.yml -f environment=staging

# === GITLAB CI ===

# Vedi le pipeline recenti
glab ci list

# Vedi i log di un job
glab ci trace <job-id>

# Retrigga una pipeline
glab ci retry <pipeline-id>
⚠️ Regola d'Oro delle 3AM: Se non è in fiamme, può aspettare domattina. Se è in fiamme, rollback prima, debug dopo. Mai debuggare in produzione alle 3 di notte con un occhio chiuso.