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
- Kubernetes en deu minuts: els objectes que importen
- Nodes i pla de control
- Què hi afegeix GKE
- Autopilot davant de Standard
- Crear
alpinashop-clusteri connectarkubectl - Construir i publicar la imatge a Artifact Registry
- El Deployment d'
alpinashop-web - El Service: exposar l'aplicació
- Escalat: manual, HPA i escalat automàtic del clúster
- ConfigMaps i Secrets
- Actualitzacions sense tall i rollback
- Observabilitat bàsica del clúster
- 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
- 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.
- 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.
- 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.
- Crear
alpinashop-cluster i connectar kubectl
alpinashop-cluster i connectar kubectlgcloud 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=catalogoNotes sobre la comanda:
create-autocrea un clúster Autopilot. Per a Standard seriacreateamb 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 irapiddó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 namespacesA 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:
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.
- 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.devEl 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.
- El Deployment d'
alpinashop-web
alpinashop-webAra 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-webEls punts que més s'equivoquen:
selector.matchLabelsha de coincidir ambtemplate.metadata.labels. Si no, el Deployment no reconeix els seus propis pods.requestsdavant delimits.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 ambOOMKilled. 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
/saludlleugera i sense dependències externes. maxUnavailable: 0garanteix 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
- 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 contenidorkubectl 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
- 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:
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 minutkubectl apply -f hpa.yaml
kubectl get hpa alpinashop-web --watch
kubectl describe hpa alpinashop-web # mostra per que va escalar o noEl 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-west1Prova 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.
- 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"Secret per a dades sensibles:
kubectl create secret generic secreto-catalogo \
--from-literal=db_pass='<contrasenya>' \
--namespace=tiendaI 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:
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.
- 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-webEl 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-webPer 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-webAixò 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.
- 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:80Els 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
requestsilimits. 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 describeabans 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 -fsobre fitxers versionats a Git, maikubectl editen producció. El que no és a Git, no existeix. - Consell:
kubectl port-forwardet deixa provar un servei intern sense exposar-lo a internet.
Exercicis
Exercici 1: clúster, imatge i primer desplegament
- Crea un clúster Autopilot
alpinashop-cluster-ejaeurope-west1i connectakubectl. - Crea un repositori d'Artifact Registry a
europe-west1i publica una imatge d'una aplicació Flask mínima que mostri el nom del pod (variableHOSTNAME) i respongui a/salud. - Escriu el Deployment amb 2 rèpliques, requests i limits raonables, sondes de liveness i readiness, i
securityContextsense root. - Escriu el Service de tipus LoadBalancer i comprova amb
curlque el repartiment entre pods funciona. - Comprova amb
kubectl get endpointsque el Service té pods associats.
Exercici 2: escalat i actualització sense tall
- Escala manualment a 4 rèpliques i comprova en quines zones cauen els pods.
- Crea un HPA de 2 a 8 rèpliques amb objectiu de CPU al 60 % i comportament asimètric.
- Genera càrrega i observa l'escalat.
- Publica una
v2de la imatge amb un canvi visible i actualitza el Deployment sense tall, comprovant ambcurlen bucle que no hi ha cap error. - Fes rollback a la versió anterior i verifica l'historial de revisions.
Exercici 3: configuració, secrets i diagnòstic
- Crea un ConfigMap amb l'entorn i el nombre de productes per pàgina, i un Secret amb una contrasenya fictícia.
- Injecta tots dos al Deployment com a variables d'entorn i verifica'n el valor dins del pod.
- Demostra que el Secret no està xifrat.
- Provoca deliberadament un
CrashLoopBackOff(per exemple, amb una comanda d'arrencada errònia) i diagnostica'l pas a pas. - 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", 200FROM 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-webSi 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: 300kubectl 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-webDurant 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-webEl 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.
- 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 --quietUn 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
- Què és Google Cloud Platform?
- Configuració del teu compte de GCP
- Descripció general de la consola de GCP
- Projectes, jerarquia de recursos i facturació
- Regions, zones i model de responsabilitat compartida
- Cloud Shell i la CLI de gcloud
Mòdul 2: Serveis principals de GCP
- Compute Engine: màquines virtuals a Google Cloud
- Cloud Storage: emmagatzematge d'objectes
- Cloud SQL: bases de dades relacionals gestionades
- App Engine: plataforma com a servei
- Google Kubernetes Engine (GKE)
- Bases de dades NoSQL: Firestore, Bigtable i Spanner
- Com triar el servei de còmput adequat
Mòdul 3: Xarxes i seguretat
- Xarxes VPC
- Balanceig de càrrega al núvol
- Cloud CDN
- Gestió d'identitat i accés (IAM)
- Cloud Armor
- Secrets i xifratge: Secret Manager i Cloud KMS
- Cloud DNS, certificats TLS i publicació segura de serveis
Mòdul 4: Dades i anàlisi
- BigQuery: el magatzem de dades analític
- Cloud Dataflow: processament de dades per lots i en temps real
- Cloud Dataproc: Spark i Hadoop gestionats
- Cloud Pub/Sub: missatgeria asíncrona
- Cloud Data Fusion: integració de dades sense codi
- Orquestració de pipelines amb Cloud Composer i Workflows
- Govern de les dades i taulers amb Dataplex i Looker Studio
Mòdul 5: Aprenentatge automàtic i IA
- Vertex AI: la plataforma d'aprenentatge automàtic de GCP
- AutoML: models a mida sense escriure codi
- TensorFlow a GCP: entrenament i servei de models
- API de llenguatge natural
- API de visió
- IA generativa a Vertex AI: models Gemini i incrustacions
- MLOps: del model al producte amb Vertex AI Pipelines
Mòdul 6: DevOps i monitoratge
- Cloud Build: integració contínua a GCP
- Cloud Source Repositories i gestió del codi font
- Cloud Functions: funcions sense servidor
- Cloud Monitoring (abans Stackdriver): mètriques, taulers i alertes
- Cloud Deployment Manager i infraestructura com a codi nativa
- Cloud Logging i Cloud Trace: registres, traces i diagnòstic
- Terraform a GCP: infraestructura com a codi a la pràctica
Mòdul 7: Temes avançats de GCP
- Híbrid i multinúvol amb Anthos
- Computació sense servidor amb Cloud Run
- Xarxes avançades: VPC compartida, aparellament i connectivitat híbrida
- Bones pràctiques de seguretat
- Gestió i optimització de costos
- Fiabilitat: SLO, alta disponibilitat i recuperació de desastres
- Govern a escala: organització, polítiques i auditoria
