Tancàvem la lliçó anterior amb una conclusió: la inversió a App Engine no s'acumula fora d'App Engine, però la inversió en contenidors serveix a tot arreu. Kubernetes és l'estàndard de facto per orquestrar contenidors, i Google Kubernetes Engine és la implementació gestionada de Google —significativament, la casa on va néixer Kubernetes, derivat del sistema intern Borg.

Kubernetes té fama de complex, i en part és merescuda. Però aquesta complexitat respon a un problema real: quan tens desenes de contenidors repartits en diverses màquines, algú ha de decidir on s'executa cadascun, reiniciar els que fallen, repartir el trànsit, actualitzar sense tallar el servei i créixer quan arriba un pic. Si no ho fa Kubernetes, ho fas tu a mà.

En aquesta lliçó aprendràs Kubernetes des de zero amb el just i suficient, crearàs el clúster alpinashop-cluster, publicaràs la imatge del catàleg Flask a Artifact Registry i desplegaràs alpinashop-web amb manifestos complets, escalat automàtic, configuració, secrets i actualitzacions sense tall amb el seu rollback.

Contingut

  1. Kubernetes en deu minuts: els objectes que importen
  2. Nodes i pla de control
  3. Què hi afegeix GKE
  4. Autopilot davant de Standard
  5. Crear alpinashop-cluster i connectar kubectl
  6. Construir i publicar la imatge a Artifact Registry
  7. El Deployment d'alpinashop-web
  8. El Service: exposar l'aplicació
  9. Escalat: manual, HPA i escalat automàtic del clúster
  10. ConfigMaps i Secrets
  11. Actualitzacions sense tall i rollback
  12. Observabilitat bàsica del clúster

  1. Kubernetes en deu minuts: els objectes que importen

Kubernetes és un sistema declaratiu: tu descrius l'estat desitjat en fitxers YAML i el sistema treballa contínuament perquè la realitat coincideixi amb aquella descripció. Si demanes tres rèpliques i una mor, Kubernetes en crea una altra. No dónes ordres; declares objectius.

Els objectes imprescindibles:

Objecte Què és Analogia
Contenidor Un procés empaquetat amb totes les seves dependències L'aplicació i el seu entorn, en una caixa
Pod La unitat mínima que Kubernetes desplega: un o diversos contenidors que comparteixen xarxa i emmagatzematge Un "servidor lògic" d'un sol ús
ReplicaSet Garanteix que existeixin N pods iguals El que compta i reposa
Deployment Gestiona ReplicaSets i orquestra les actualitzacions El que tu escrius de debò
Service Un nom i una IP estables que reparteixen trànsit entre pods El balancejador intern
Ingress Encaminament HTTP(S) des de fora cap a diversos Services El proxy invers
Namespace Partició lògica del clúster Carpeta amb permisos i quotes
ConfigMap Configuració no sensible Fitxer de configuració
Secret Dades sensibles (amb matisos, apartat 10) Sobre tancat, no blindat
Node Una màquina (VM) que executa pods El servidor físic

Tres idees que cal interioritzar abans d'escriure un YAML:

  • Els pods són efímers i d'un sol ús. Neixen, moren i es recreen amb un altre nom i una altra IP. No et connectis mai a un pod per la seva IP: per a això hi ha el Service. És la mateixa lliçó de les instàncies del MIG a 02-01, portada a l'extrem.
  • Gairebé mai no crees pods directament. Crees un Deployment, que crea un ReplicaSet, que crea els pods. Cada capa afegeix una garantia.
  • Tot s'identifica per labels. Un Service no coneix els seus pods pel nom: selecciona els que porten una etiqueta determinada. Si el selector no coincideix amb les etiquetes dels pods, el Service existeix però no envia trànsit a ningú. És l'error número u dels principiants.
graph TD
    subgraph "Pla de control (gestionat per Google)"
        API[API Server]
        SCHED[Scheduler]
        CM[Controller Manager]
        ETCD[(etcd)]
    end

    subgraph "Nodes (VM de Compute Engine)"
        subgraph "Node 1"
            P1[Pod alpinashop-web]
            P2[Pod alpinashop-web]
        end
        subgraph "Node 2"
            P3[Pod alpinashop-web]
        end
    end

    DEP[Deployment<br/>alpinashop-web] --> RS[ReplicaSet]
    RS --> P1
    RS --> P2
    RS --> P3

    SVC[Service LoadBalancer<br/>alpinashop-web] --> P1
    SVC --> P2
    SVC --> P3

    KUBECTL[kubectl] --> API
    API --> SCHED
    API --> CM
    API --> ETCD
    INTERNET((Internet)) --> SVC

  1. Nodes i pla de control

Un clúster de Kubernetes té dues meitats:

El pla de control és el cervell. Conté l'API Server (l'única porta d'entrada: kubectl i tota la resta hi parlen), el Scheduler (decideix en quin node va cada pod), els Controller Managers (els bucles que comparen estat real amb estat desitjat i actuen) i etcd (la base de dades que desa tot l'estat del clúster).

Els nodes són les màquines que executen els pods. A GKE són VM de Compute Engine —les mateixes de la lliçó 02-01—, cadascuna amb el kubelet (l'agent que parla amb el pla de control i arrenca contenidors), un runtime de contenidors i kube-proxy (que implementa la xarxa dels Services).

Muntar i mantenir un pla de control pel teu compte és una feina considerable: alta disponibilitat d'etcd, certificats, actualitzacions coordinades, còpies de seguretat. Aquesta és exactament la part que GKE et treu.

  1. Què hi afegeix GKE

Sobre Kubernetes vainilla, GKE aporta:

  • Pla de control gestionat i amb SLA. Google el desplega, el replica, l'apedaça i l'actualitza. En mode regional, replicat en diverses zones.
  • Actualitzacions automàtiques de pla de control i nodes, amb canals de versió (rapid, regular, stable) per triar quanta novetat vols.
  • Reparació automàtica de nodes: un node que deixa de respondre es recrea.
  • Escalat automàtic de nodes: si no hi caben més pods, s'afegeixen nodes; si en sobren, es retiren.
  • Integració amb la xarxa de Google: els Services de tipus LoadBalancer creen balancejadors natius de Google Cloud, i els pods reben IP de la VPC (03-01).
  • Integració amb IAM i Workload Identity: els pods s'autentiquen davant de les API de Google Cloud amb una identitat pròpia, sense claus.
  • Cloud Logging i Cloud Monitoring integrats des del primer moment.
  • Autopilot: un mode on ni tan sols veus els nodes.

  1. Autopilot davant de Standard

És la primera decisió en crear un clúster, i condiciona el dia a dia.

Autopilot Standard
Qui gestiona els nodes? Google, del tot Tu: mida, nombre, imatge, pools
Veus les VM? No Sí, a Compute Engine
Facturació Per CPU, memòria i disc sol·licitats pels teus pods Per les VM dels nodes, s'utilitzin o no
Escalat de nodes Automàtic i invisible Escalador automàtic configurable per pool
Seguretat Endurit per defecte (sense privilegis, sense accés a l'amfitrió) Configurable, més permissiu
DaemonSets i accés a l'amfitrió Limitat Permès
GPU i maquinari especial Suportat amb restriccions Control total
Sobrecàrrega operativa Mínima Mitjana-alta
Quan triar-lo Cas general, equips petits, aplicacions estàndard Necessitats específiques de maquinari, agents amb privilegis, ajust fi de costos a gran escala

La diferència de facturació és la clau per entendre-ho. A Standard pagues els nodes: si tens tres VM e2-standard-4 i els teus pods n'utilitzen el 15 %, pagues el 100 %. A Autopilot pagues el que els teus pods sol·liciten: si un pod demana 250 mCPU i 512 MiB, això és el que es factura. Això té dues conseqüències directes: els requests dels teus manifestos deixen de ser una recomanació i passen a ser la teva factura, i no hi ha incentiu per "reomplir" nodes.

AlpinaShop tria Autopilot. La Marta és l'única responsable d'infraestructura d'una empresa de 40 persones; no té temps per dimensionar pools de nodes ni per ajustar el binpacking. L'aplicació és un contenidor web estàndard sense requisits especials. Autopilot elimina tota una categoria de feina a canvi d'un sobrecost per unitat que, a aquesta escala, és irrellevant davant de les hores de la Marta.

  1. Crear alpinashop-cluster i connectar kubectl

gcloud services enable container.googleapis.com artifactregistry.googleapis.com

gcloud container clusters create-auto alpinashop-cluster \
  --project=alpinashop-prod \
  --region=europe-west1 \
  --release-channel=regular \
  --labels=entorno=prod,equipo=plataforma,centro-coste=tienda,aplicacion=catalogo

Notes sobre la comanda:

  • create-auto crea un clúster Autopilot. Per a Standard seria create amb tota la configuració de pools de nodes.
  • --region (no --zone): els clústers Autopilot són sempre regionals, amb el pla de control replicat entre zones. És l'elecció correcta per a producció i coherent amb el raonament de 01-05.
  • --release-channel=regular: versions estables amb actualitzacions automàtiques. stable és més conservador i rapid dóna accés anticipat a versions noves.

La creació triga entre 5 i 10 minuts. Després cal dir-li a kubectl amb quin clúster ha de parlar:

# Instal.lar el plugin d'autenticacio si no hi es (Cloud Shell ja el porta)
gcloud components install gke-gcloud-auth-plugin

# Obtenir credencials: escriu la configuracio a ~/.kube/config
gcloud container clusters get-credentials alpinashop-cluster \
  --region=europe-west1 --project=alpinashop-prod

# Comprovar la connexio
kubectl cluster-info
kubectl get nodes
kubectl get namespaces

A Autopilot, kubectl get nodes pot retornar una llista buida al principi: els nodes apareixen quan hi ha pods per executar. És desconcertant la primera vegada i és exactament el comportament esperat.

Creem un namespace propi, en lloc de treballar a default:

kubectl create namespace tienda
kubectl config set-context --current --namespace=tienda

Els namespaces permeten separar entorns i equips dins d'un clúster, amb quotes de recursos i permisos RBAC propis. Utilitzar default per a tot és un mal hàbit que es paga quan el clúster creix.

  1. Construir i publicar la imatge a Artifact Registry

Kubernetes executa imatges de contenidor, així que cal empaquetar el catàleg Flask. Artifact Registry és el registre d'artefactes de Google Cloud —successor de Container Registry, que està retirat; si trobes documentació amb gcr.io, està desactualitzada.

gcloud artifacts repositories create alpinashop \
  --repository-format=docker \
  --location=europe-west1 \
  --description="Imatges de contenidor d'AlpinaShop"

# Configurar Docker perque s'autentiqui contra aquest registre
gcloud auth configure-docker europe-west1-docker.pkg.dev

El Dockerfile del catàleg:

# Imatge base slim: mes petita que la completa, amb menys superficie d'atac
FROM python:3.12-slim

# Evita que Python escrigui .pyc i forca sortida sense buffer,
# imprescindible perque els registres arribin a Cloud Logging en temps real.
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

# Copiar primer NOMES requirements.txt: si no canvia, Docker reutilitza
# la capa de dependencies en les construccions seguents. Es la
# optimitzacio de memoria cau mes rendible d'un Dockerfile.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Ara el codi, que canvia en cada commit
COPY . .

# Usuari sense privilegis: Autopilot REBUTJA contenidors que
# intentin executar-se com a root.
RUN useradd --create-home --uid 1000 alpina && chown -R alpina:alpina /app
USER 1000

EXPOSE 8080

# 2 workers i 4 fils: l'app espera molt la base de dades,
# aixi que els fils aprofiten be aquest temps mort.
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", \
     "--threads", "4", "--timeout", "60", "main:app"]

Construcció i publicació. Hi ha dos camins:

# Opcio A: construir en local amb Docker
IMATGE="europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:v1"
docker build -t "$IMATGE" .
docker push "$IMATGE"

# Opcio B: construir al nuvol amb Cloud Build (no requereix Docker local)
gcloud builds submit --tag "$IMATGE" .

L'opció B és especialment còmoda des de Cloud Shell i és el germen del pipeline d'integració contínua que muntarem a 06-01.

Verifica el que has publicat:

gcloud artifacts docker images list \
  europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop

# Analisi de vulnerabilitats (si esta activat al repositori)
gcloud artifacts docker images scan "$IMATGE"

Dues bones pràctiques d'etiquetatge: no utilitzis mai :latest en producció —no sabràs quina versió està corrent ni podràs fer rollback— i etiqueta amb el hash del commit (catalogo:a3f9c1d), cosa que fa traçable quin codi hi ha a cada pod. És la mateixa idea de noms immutables que vam aplicar als objectes de Storage a 02-02 i a les plantilles d'instància a 02-01.

  1. El Deployment d'alpinashop-web

Ara el manifest principal. Cada bloc està comentat:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: alpinashop-web
  namespace: tienda
  labels:
    app: alpinashop-web
    entorno: prod
spec:
  # Nombre de pods desitjat. L'HPA (apartat 9) el sobreescriura despres.
  replicas: 3

  # El Deployment governa els pods que coincideixen amb aquest selector.
  # HA DE coincidir amb les labels de la plantilla de sota.
  selector:
    matchLabels:
      app: alpinashop-web

  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1          # fins a 1 pod extra durant l'actualitzacio
      maxUnavailable: 0    # mai baixar del nombre desitjat: zero tall

  template:
    metadata:
      labels:
        app: alpinashop-web    # etiqueta que utilitzara el Service
        entorno: prod
    spec:
      # Compte de servei de Kubernetes vinculat per Workload Identity
      # a un compte de servei de Google Cloud (apartat 10).
      serviceAccountName: sa-catalogo

      containers:
        - name: catalogo
          image: europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:v1

          ports:
            - name: http
              containerPort: 8080

          # requests: el que el planificador reserva. A Autopilot, EL QUE PAGUES.
          # limits: el sostre. Si se supera la memoria, el contenidor mor (OOMKilled).
          resources:
            requests:
              cpu: "250m"          # 0,25 d'una vCPU
              memory: "512Mi"
              ephemeral-storage: "1Gi"
            limits:
              cpu: "500m"
              memory: "512Mi"      # igual que requests: evita sorpreses de memoria

          env:
            - name: BUCKET_CATALOGO
              value: "alpinashop-catalogo"
            - name: ENTORNO
              valueFrom:
                configMapKeyRef:
                  name: config-catalogo
                  key: entorno
            - name: INSTANCIA_SQL
              valueFrom:
                configMapKeyRef:
                  name: config-catalogo
                  key: instancia_sql
            - name: DB_USER
              valueFrom:
                configMapKeyRef:
                  name: config-catalogo
                  key: db_user
            - name: DB_PASS
              valueFrom:
                secretKeyRef:
                  name: secreto-catalogo
                  key: db_pass

          # Es viu el contenidor? Si falla, Kubernetes el REINICIA.
          livenessProbe:
            httpGet:
              path: /salud
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 20
            failureThreshold: 3

          # Esta llest per rebre transit? Si falla, se li RETIRA el transit
          # pero NO es reinicia. Es la sonda que evita servir errors durant
          # l'arrencada o una sobrecarrega puntual.
          readinessProbe:
            httpGet:
              path: /salud
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 2

          # Enduriment exigit per Autopilot
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]

      # Reparteix els pods entre zones: si cau europe-west1-b, la botiga continua.
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: alpinashop-web

Els punts que més s'equivoquen:

  • selector.matchLabels ha de coincidir amb template.metadata.labels. Si no, el Deployment no reconeix els seus propis pods.
  • requests davant de limits. requests és el que el planificador reserva i, a Autopilot, el que factures. limits és el sostre: superar el límit de memòria mata el contenidor amb OOMKilled. Igualar memòria de request i limit evita sorpreses.
  • Liveness i readiness no són el mateix. La primera reinicia; la segona només retira trànsit. Una liveness mal configurada —per exemple, apuntant a una ruta que consulta la base de dades— provoca reinicis en cadena quan la base de dades va lenta, empitjorant el problema. Mantén /salud lleugera i sense dependències externes.
  • maxUnavailable: 0 garanteix que la capacitat mai no baixa durant un desplegament.

Aplicar i comprovar:

kubectl apply -f deployment.yaml

kubectl get deployments
kubectl get pods -o wide          # mostra en quin node i zona cau cada pod
kubectl describe pod <nom-del-pod>
kubectl logs -f deployment/alpinashop-web

  1. El Service: exposar l'aplicació

Els pods tenen IP efímeres i canviants. Un Service ofereix un nom DNS i una IP virtual estables que reparteixen trànsit entre els pods que coincideixin amb el seu selector.

Tipus de Service Abast Ús
ClusterIP (per defecte) Només dins del clúster Comunicació entre serveis interns
NodePort Port a cada node Rar d'utilitzar directament
LoadBalancer IP pública externa Exposar un servei a internet
ExternalName Àlies DNS a un amfitrió extern Integració amb serveis de fora
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: alpinashop-web
  namespace: tienda
  annotations:
    # Balancejador natiu per IP de pod: el transit va directe al pod,
    # sense salt addicional pel node. Menys latencia i millor health checking.
    cloud.google.com/neg: '{"ingress": true}'
spec:
  type: LoadBalancer
  selector:
    app: alpinashop-web      # HA DE coincidir amb les labels dels pods
  ports:
    - name: http
      protocol: TCP
      port: 80               # port exposat a l'exterior
      targetPort: 8080       # port del contenidor
kubectl apply -f service.yaml

# La IP externa triga 1-2 minuts a apareixer
kubectl get service alpinashop-web --watch

IP=$(kubectl get service alpinashop-web -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -s "http://$IP/"

Un Service de tipus LoadBalancer a GKE crea automàticament un balancejador de xarxa de Google Cloud. És la manera més ràpida d'exposar alguna cosa, però per a una botiga real voldràs HTTPS, un domini propi, encaminament per ruta i protecció davant d'atacs. Això s'aconsegueix amb un Ingress o un Gateway davant d'un balancejador HTTP(S) global, juntament amb Cloud Armor i certificats gestionats: contingut de les lliçons 03-02, 03-05 i 03-07. Aquí parem deliberadament al LoadBalancer de tipus L4.

Dins del clúster, qualsevol pod pot cridar aquest servei pel seu nom DNS:

http://alpinashop-web.tienda.svc.cluster.local
http://alpinashop-web            # forma curta, dins del mateix namespace

  1. Escalat: manual, HPA i escalat automàtic del clúster

Kubernetes escala en dos nivells independents: nombre de pods i nombre de nodes.

Escalat manual de pods:

kubectl scale deployment alpinashop-web --replicas=5
kubectl get pods -w

HorizontalPodAutoscaler (HPA): ajusta les rèpliques automàticament segons una mètrica.

# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: alpinashop-web
  namespace: tienda
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: alpinashop-web

  minReplicas: 3
  maxReplicas: 20

  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60    # % sobre el REQUEST de CPU, no sobre el limit
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 75

  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30   # reacciona rapid als pics
      policies:
        - type: Percent
          value: 100                   # com a maxim, duplicar cada 30 s
          periodSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300  # baixa a poc a poc: evita oscil.lacions
      policies:
        - type: Pods
          value: 1
          periodSeconds: 60            # retirar com a molt 1 pod per minut
kubectl apply -f hpa.yaml
kubectl get hpa alpinashop-web --watch
kubectl describe hpa alpinashop-web    # mostra per que va escalar o no

El bloc behavior és el que separa un HPA que funciona d'un que oscil·la. L'asimetria és deliberada: pujar ràpid i baixar a poc a poc. Un pic de trànsit requereix capacitat immediata; retirar capacitat amb pressa provoca flapping, amb pods creant-se i destruint-se sense parar. La finestra de 300 segons a la baixa és una recomanació molt raonable com a punt de partida.

Compte amb un detall: el percentatge de l'HPA es calcula sobre el request, no sobre el límit. Amb requests.cpu: 250m i objectiu del 60 %, l'HPA escala quan l'ús mitjà supera 150 mCPU per pod. Un request mal posat descol·loca tot l'escalat automàtic.

Escalat automàtic del clúster. A Autopilot és automàtic i invisible: si els pods nous no hi caben, Google afegeix capacitat; si en sobra, la retira. No hi ha res a configurar, que és precisament la seva proposta de valor. A Standard cal configurar-lo per pool:

gcloud container clusters update alpinashop-cluster-std \
  --enable-autoscaling --min-nodes=1 --max-nodes=10 \
  --node-pool=pool-principal --region=europe-west1

Prova de càrrega ràpida per veure l'HPA en acció:

kubectl run generador-carga --rm -it --image=busybox:1.36 --restart=Never -- \
  /bin/sh -c "while true; do wget -q -O- http://alpinashop-web.tienda/; done"

En una altra terminal, kubectl get hpa --watch mostrarà pujar l'ús de CPU i, després d'uns segons, augmentar el nombre de rèpliques.

  1. ConfigMaps i Secrets

ConfigMap per a configuració no sensible:

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: config-catalogo
  namespace: tienda
data:
  entorno: "produccion"
  instancia_sql: "alpinashop-prod:europe-west1:alpinashop-pedidos"
  db_user: "app_catalogo"
  db_name: "tienda"
  productos_por_pagina: "24"
kubectl apply -f configmap.yaml
kubectl get configmap config-catalogo -o yaml

Secret per a dades sensibles:

kubectl create secret generic secreto-catalogo \
  --from-literal=db_pass='<contrasenya>' \
  --namespace=tienda

I aquí arriba l'advertiment més important d'aquest apartat: els Secrets de Kubernetes estan codificats en base64, no xifrats. Qualsevol amb permís per llegir secrets en aquell namespace els veu en clar:

kubectl get secret secreto-catalogo -o jsonpath='{.data.db_pass}' | base64 -d

Base64 no és xifratge, és una codificació. Un Secret de Kubernetes protegeix davant que la contrasenya aparegui en un kubectl describe o en un manifest del repositori, i res més.

El correcte en producció és Secret Manager (lliçó 03-06), integrat amb GKE mitjançant el complement Secret Manager CSI driver, que munta els secrets com a fitxers obtinguts en temps d'execució, amb rotació centralitzada i auditoria. La regla pràctica: els Secrets de Kubernetes valen per a desenvolupament i per a dades de baix impacte; les credencials reals de producció viuen a Secret Manager.

Workload Identity mereix un paràgraf propi perquè resol el problema d'arrel. Vincula un compte de servei de Kubernetes amb un de Google Cloud, de manera que els pods obtenen credencials de Google Cloud automàticament, sense fitxers de clau:

PROJECTE=alpinashop-prod

# 1. Compte de servei de Google Cloud
gcloud iam service-accounts create sa-catalogo-gke \
  --display-name="Cataleg a GKE"

gcloud projects add-iam-policy-binding $PROJECTE \
  --member="serviceAccount:sa-catalogo-gke@$PROJECTE.iam.gserviceaccount.com" \
  --role="roles/cloudsql.client"

gcloud storage buckets add-iam-policy-binding gs://alpinashop-catalogo \
  --member="serviceAccount:sa-catalogo-gke@$PROJECTE.iam.gserviceaccount.com" \
  --role="roles/storage.objectUser"

# 2. Compte de servei de Kubernetes
kubectl create serviceaccount sa-catalogo --namespace=tienda

# 3. Vincular tots dos
gcloud iam service-accounts add-iam-policy-binding \
  "sa-catalogo-gke@$PROJECTE.iam.gserviceaccount.com" \
  --role="roles/iam.workloadIdentityUser" \
  --member="serviceAccount:$PROJECTE.svc.id.goog[tienda/sa-catalogo]"

kubectl annotate serviceaccount sa-catalogo --namespace=tienda \
  iam.gke.io/gcp-service-account="sa-catalogo-gke@$PROJECTE.iam.gserviceaccount.com"

Amb això, el mateix storage.Client() i el mateix connector de Cloud SQL de les lliçons anteriors funcionen dins del pod sense cap credencial. És la continuació natural del que ja vas veure a Compute Engine i App Engine: la identitat la dóna la plataforma, no un fitxer.

  1. Actualitzacions sense tall i rollback

Amb strategy: RollingUpdate i maxUnavailable: 0, actualitzar la imatge substitueix els pods d'un en un sense baixar la capacitat:

# Publicar la versio nova
gcloud builds submit \
  --tag europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:v2 .

# Actualitzar el Deployment
kubectl set image deployment/alpinashop-web \
  catalogo=europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:v2

# Seguir el progres
kubectl rollout status deployment/alpinashop-web

# Historial de revisions
kubectl rollout history deployment/alpinashop-web

El procés, pas a pas: es crea un pod amb v2, s'espera que la seva readinessProbe doni el vistiplau, es retira un pod de v1, i es repeteix. Com que maxUnavailable: 0, sempre hi ha almenys tres pods llestos. Aquí es veu per què la readinessProbe és imprescindible: sense ella, Kubernetes donaria per bona la versió nova tan bon punt el contenidor arrenca, abans que gunicorn estigui acceptant peticions, i alguns clients rebrien errors.

Si la versió nova falla:

# Tornar a la revisio anterior
kubectl rollout undo deployment/alpinashop-web

# Tornar a una revisio concreta
kubectl rollout undo deployment/alpinashop-web --to-revision=3

# Pausar una actualitzacio en curs (canary manual)
kubectl rollout pause deployment/alpinashop-web
kubectl rollout resume deployment/alpinashop-web

Per protegir el servei també durant operacions de manteniment del clúster —actualitzacions de nodes, reescalats—, defineix un PodDisruptionBudget:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: alpinashop-web
  namespace: tienda
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: alpinashop-web

Això li diu a Kubernetes que mai no deixi menys de 2 pods disponibles quan ell decideix moure pods. Sense un PDB, una actualització de nodes podria buidar un node sencer i deixar la botiga amb menys capacitat de la necessària en el pitjor moment.

  1. Observabilitat bàsica del clúster

GKE envia mètriques i registres a Cloud Monitoring i Cloud Logging sense configuració addicional. Les comandes de diagnòstic de cada dia:

# Estat general
kubectl get all -n tienda
kubectl get events -n tienda --sort-by=.metadata.creationTimestamp

# Consum real de pods i nodes
kubectl top pods -n tienda
kubectl top nodes

# Registres
kubectl logs -f deployment/alpinashop-web
kubectl logs deployment/alpinashop-web --previous   # registres del contenidor que va morir

# Diagnostic d'un pod que no arrenca
kubectl describe pod <pod>          # la seccio Events diu gairebe sempre per que

# Obrir una shell dins del contenidor
kubectl exec -it <pod> -- /bin/sh

# Provar el servei sense exposar-lo
kubectl port-forward service/alpinashop-web 8080:80

Els estats de pod que veuràs amb més freqüència i què signifiquen:

Estat Causa habitual
Pending No hi ha recursos per planificar-lo; a Autopilot, s'està afegint capacitat
ImagePullBackOff La imatge no existeix o falta permís sobre Artifact Registry
CrashLoopBackOff El contenidor arrenca i mor en bucle. Mira kubectl logs --previous
OOMKilled Va superar limits.memory. Puja el límit o arregla la fuita
Running però no llest La readinessProbe falla; revisa la ruta i el port

A Cloud Monitoring hi ha taulers predefinits de GKE amb ús per clúster, namespace i càrrega de treball, i es poden definir alertes sobre reinicis de contenidor, pods no disponibles o latència. El tractament complet és a 06-04 i 06-06.

Errors habituals i consells

  • Selector del Service que no coincideix amb les labels dels pods. El Service existeix, no dóna error i no envia trànsit a ningú. Verifica-ho amb kubectl get endpoints alpinashop-web.
  • No definir requests i limits. A Autopilot s'apliquen valors per defecte que probablement no són els teus; a Standard, un pod sense límits pot ofegar els seus veïns.
  • Igualar la liveness probe a una ruta que consulta la base de dades. Si la base va lenta, Kubernetes reinicia tots els pods i empitjora l'incident.
  • Utilitzar l'etiqueta :latest. No saps quina versió corre ni pots fer rollback fiable.
  • Desar credencials reals en Secrets de Kubernetes. Base64 no és xifratge. Utilitza Secret Manager.
  • Executar com a root. Autopilot ho rebutja, i a Standard és un risc innecessari.
  • Escalar a la baixa massa ràpid a l'HPA. Provoca oscil·lacions. Utilitza una finestra d'estabilització àmplia.
  • Treballar sempre al namespace default. Separa per entorn i equip des del principi.
  • Oblidar el PodDisruptionBudget. El manteniment del clúster pot deixar-te sense capacitat.
  • Consell: kubectl describe abans que cap altra comanda quan alguna cosa no funciona; la secció Events sol donar la resposta.
  • Consell: etiqueta les imatges amb el hash del commit. Traçabilitat completa entre codi i pod.
  • Consell: utilitza kubectl apply -f sobre fitxers versionats a Git, mai kubectl edit en producció. El que no és a Git, no existeix.
  • Consell: kubectl port-forward et deixa provar un servei intern sense exposar-lo a internet.

Exercicis

Exercici 1: clúster, imatge i primer desplegament

  1. Crea un clúster Autopilot alpinashop-cluster-ej a europe-west1 i connecta kubectl.
  2. Crea un repositori d'Artifact Registry a europe-west1 i publica una imatge d'una aplicació Flask mínima que mostri el nom del pod (variable HOSTNAME) i respongui a /salud.
  3. Escriu el Deployment amb 2 rèpliques, requests i limits raonables, sondes de liveness i readiness, i securityContext sense root.
  4. Escriu el Service de tipus LoadBalancer i comprova amb curl que el repartiment entre pods funciona.
  5. Comprova amb kubectl get endpoints que el Service té pods associats.

Exercici 2: escalat i actualització sense tall

  1. Escala manualment a 4 rèpliques i comprova en quines zones cauen els pods.
  2. Crea un HPA de 2 a 8 rèpliques amb objectiu de CPU al 60 % i comportament asimètric.
  3. Genera càrrega i observa l'escalat.
  4. Publica una v2 de la imatge amb un canvi visible i actualitza el Deployment sense tall, comprovant amb curl en bucle que no hi ha cap error.
  5. Fes rollback a la versió anterior i verifica l'historial de revisions.

Exercici 3: configuració, secrets i diagnòstic

  1. Crea un ConfigMap amb l'entorn i el nombre de productes per pàgina, i un Secret amb una contrasenya fictícia.
  2. Injecta tots dos al Deployment com a variables d'entorn i verifica'n el valor dins del pod.
  3. Demostra que el Secret no està xifrat.
  4. Provoca deliberadament un CrashLoopBackOff (per exemple, amb una comanda d'arrencada errònia) i diagnostica'l pas a pas.
  5. Explica què utilitzaries en producció en lloc d'un Secret de Kubernetes i per què.

Solucions

Solució 1

gcloud container clusters create-auto alpinashop-cluster-ej \
  --region=europe-west1 --release-channel=regular

gcloud container clusters get-credentials alpinashop-cluster-ej --region=europe-west1
kubectl create namespace tienda
kubectl config set-context --current --namespace=tienda

gcloud artifacts repositories create alpinashop \
  --repository-format=docker --location=europe-west1
# main.py
import os
from flask import Flask

app = Flask(__name__)


@app.route("/")
def inici():
    return f"<h1>AlpinaShop</h1><p>Pod: {os.environ.get('HOSTNAME', 'local')}</p>"


@app.route("/salud")
def salut():
    return "ok", 200
FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd --create-home --uid 1000 alpina && chown -R alpina:alpina /app
USER 1000
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "main:app"]
IMG="europe-west1-docker.pkg.dev/$(gcloud config get-value project)/alpinashop/catalogo:v1"
gcloud builds submit --tag "$IMG" .
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: alpinashop-web
  namespace: tienda
spec:
  replicas: 2
  selector:
    matchLabels:
      app: alpinashop-web
  template:
    metadata:
      labels:
        app: alpinashop-web
    spec:
      containers:
        - name: catalogo
          image: europe-west1-docker.pkg.dev/PROJECTE/alpinashop/catalogo:v1
          ports:
            - containerPort: 8080
          resources:
            requests: { cpu: "250m", memory: "512Mi" }
            limits:   { cpu: "500m", memory: "512Mi" }
          livenessProbe:
            httpGet: { path: /salud, port: 8080 }
            initialDelaySeconds: 15
            periodSeconds: 20
          readinessProbe:
            httpGet: { path: /salud, port: 8080 }
            initialDelaySeconds: 5
            periodSeconds: 5
          securityContext:
            runAsNonRoot: true
            runAsUser: 1000
            allowPrivilegeEscalation: false
            capabilities: { drop: ["ALL"] }
kubectl apply -f deployment.yaml -f service.yaml

IP=$(kubectl get svc alpinashop-web -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
for i in $(seq 1 10); do curl -s "http://$IP/" | grep -o "Pod: [a-z0-9-]*"; done

# 5. Comprovar que el Service te endpoints
kubectl get endpoints alpinashop-web

Si kubectl get endpoints retorna <none>, el selector del Service no coincideix amb les labels dels pods, o cap pod no està passant la readinessProbe. És la primera comprovació davant d'un Service que no respon.

Solució 2

# 1. Escalat manual i repartiment per zones
kubectl scale deployment alpinashop-web --replicas=4
kubectl get pods -o custom-columns=\
NOM:.metadata.name,NODE:.spec.nodeName,ESTAT:.status.phase
# 2. hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: alpinashop-web
  namespace: tienda
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: alpinashop-web
  minReplicas: 2
  maxReplicas: 8
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 60 }
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300
kubectl apply -f hpa.yaml

# 3. Generar carrega i observar
kubectl run carga --rm -it --image=busybox:1.36 --restart=Never -- \
  /bin/sh -c "while true; do wget -q -O- http://alpinashop-web.tienda/ >/dev/null; done"
# en una altra terminal:
kubectl get hpa alpinashop-web --watch

# 4. Actualitzacio sense tall, comprovant que no hi ha errors
gcloud builds submit --tag "${IMG%:v1}:v2" .

while true; do
  curl -s -o /dev/null -w "%{http_code} " "http://$IP/"
  sleep 0.3
done &

kubectl set image deployment/alpinashop-web catalogo="${IMG%:v1}:v2"
kubectl rollout status deployment/alpinashop-web

# 5. Rollback
kubectl rollout undo deployment/alpinashop-web
kubectl rollout history deployment/alpinashop-web

Durant l'actualització, el bucle de curl ha de mostrar únicament codis 200. Si apareguessin 502 o 503, la causa gairebé sempre és una readinessProbe mal configurada o maxUnavailable més gran que zero.

Solució 3

# 1. ConfigMap i Secret
kubectl create configmap config-catalogo \
  --from-literal=entorno=produccion \
  --from-literal=productos_por_pagina=24

kubectl create secret generic secreto-catalogo \
  --from-literal=db_pass='ClauDeProva123'
# 2. Injeccio al Deployment
          env:
            - name: ENTORNO
              valueFrom:
                configMapKeyRef: { name: config-catalogo, key: entorno }
            - name: PRODUCTOS_POR_PAGINA
              valueFrom:
                configMapKeyRef: { name: config-catalogo, key: productos_por_pagina }
            - name: DB_PASS
              valueFrom:
                secretKeyRef: { name: secreto-catalogo, key: db_pass }
kubectl apply -f deployment.yaml
kubectl exec -it deployment/alpinashop-web -- env | grep -E "ENTORNO|PRODUCTOS|DB_PASS"

# 3. El Secret no esta xifrat
kubectl get secret secreto-catalogo -o jsonpath='{.data.db_pass}' | base64 -d; echo

# 4. Provocar i diagnosticar un CrashLoopBackOff
kubectl set image deployment/alpinashop-web catalogo=python:3.12-slim
kubectl get pods
kubectl describe pod <pod>              # Events: Back-off restarting failed container
kubectl logs <pod> --previous           # sortida del contenidor que va morir
kubectl rollout undo deployment/alpinashop-web

El diagnòstic correcte segueix sempre aquest ordre: kubectl get pods per veure l'estat, kubectl describe pod per llegir els Events (que indiquen si el problema és d'imatge, de recursos o de sondes) i kubectl logs --previous per veure què va imprimir el contenidor just abans de morir. En aquest cas, la imatge python:3.12-slim no té comanda d'arrencada útil i acaba immediatament, amb Kubernetes reintentant amb retrocés exponencial.

  1. En producció utilitzaria Secret Manager amb el complement CSI de GKE, o Workload Identity per eliminar directament la credencial. Raons: els Secrets de Kubernetes només estan codificats en base64 i són llegibles per qualsevol amb permisos de lectura al namespace; no tenen rotació automàtica; no deixen traça d'auditoria de qui hi va accedir; i si es versionen a Git, la credencial queda a l'historial per sempre. Secret Manager xifra en repòs, controla l'accés amb IAM granular, registra cada accés i permet rotar sense tornar a desplegar. S'estudia a 03-06.

Neteja:

kubectl delete namespace tienda
gcloud container clusters delete alpinashop-cluster-ej --region=europe-west1 --quiet

Un clúster oblidat és dels recursos més cars que pots deixar encesos. Esborra'l en acabar els exercicis.

Conclusió

Has recorregut Kubernetes des dels conceptes fins a una aplicació real en producció. Saps que és un sistema declaratiu on descrius l'estat desitjat i el sistema hi convergeix, i coneixes els objectes que importen: el pod com a unitat efímera, el ReplicaSet que compta i reposa, el Deployment que orquestra actualitzacions, el Service que dóna una identitat estable a un conjunt canviant de pods, i els namespaces que particionen el clúster. Has entès la separació entre pla de control i nodes, i per què el fet que Google gestioni el primer és el nucli del valor de GKE, juntament amb la reparació automàtica, l'escalat automàtic de nodes, les actualitzacions per canals i la integració amb la xarxa i amb IAM.

Has comparat Autopilot i Standard, i has vist que la diferència essencial és què es factura: els nodes a Standard, el que sol·liciten els teus pods a Autopilot. AlpinaShop va triar Autopilot perquè la Marta és una sola persona i l'aplicació no té requisits especials de maquinari. Has creat alpinashop-cluster regional, has connectat kubectl, has escrit un Dockerfile amb memòria cau de dependències i usuari sense privilegis, i has publicat la imatge a Artifact Registry amb etiquetes versionades i mai :latest. Has escrit el Deployment complet d'alpinashop-web amb requests i limits, sondes de liveness i readiness ben diferenciades, context de seguretat i repartiment entre zones; i el Service de tipus LoadBalancer, sabent que HTTPS, domini propi i encaminament avançat arribaran al mòdul 3.

Has escalat a mà, has configurat un HorizontalPodAutoscaler amb comportament asimètric —ràpid en pujar, lent en baixar— i has comprovat que el percentatge es calcula sobre el request. Has gestionat configuració amb ConfigMaps i has descobert que els Secrets de Kubernetes només estan codificats en base64, amb Secret Manager i Workload Identity com a resposta correcta. I has fet una actualització sense ni un sol error 500 i el seu rollback en una comanda, protegit a més amb un PodDisruptionBudget.

AlpinaShop ja té el seu còmput resolt en quatre nivells diferents i les seves dades transaccionals a Cloud SQL. Però no tot encaixa en una base de dades relacional: la cistella de la compra que canvia a cada clic, les sessions d'usuari, la telemetria de quins productes mira la gent. A 02-06, Bases de dades NoSQL: Firestore, Bigtable i Spanner, veurem per què existeixen altres models de dades —documental, columnar ample, relacional distribuït—, entendrem consistència i el teorema CAP sense dogmatisme, desarem la cistella d'AlpinaShop a Firestore i la seva telemetria de clics a Bigtable, situarem Spanner, Memorystore i BigQuery en un mapa comparatiu, i tancarem amb un arbre de decisió i el repartiment raonat de quina dada d'AlpinaShop viu a cada lloc.

Curs de Google Cloud Platform (GCP)

Mòdul 1: Introducció a Google Cloud Platform

Mòdul 2: Serveis principals de GCP

Mòdul 3: Xarxes i seguretat

Mòdul 4: Dades i anàlisi

Mòdul 5: Aprenentatge automàtic i IA

Mòdul 6: DevOps i monitoratge

Mòdul 7: Temes avançats de GCP

Mòdul 8: Projecte final

© Copyright 2026. Tots els drets reservats