La lección anterior terminó con un problema que ni Helm ni Kustomize resuelven. Ambas herramientas solo actúan cuando alguien ejecuta una orden. Si un compañero hace kubectl edit en rutas-norte-pro a las tres de la madrugada para salir de un apuro, ninguna se entera. Si el portátil de quien despliega se estropea, nadie sabe desplegar. Y si la canalización ci-rutasnorte necesita credenciales de administrador del clúster para ejecutar kubectl apply, hemos abierto un agujero de seguridad que contradice el RBAC mínimo que tanto cuidamos en 08-01.
GitOps es la respuesta, y es la conclusión natural de todo lo que llevamos de curso. La idea es sencilla de enunciar y transformadora en la práctica: un agente que vive dentro del clúster, mira un repositorio de Git de forma continua, y se encarga de que el clúster se parezca a lo que dice ese repositorio. Siempre. Sin que nadie ejecute nada.
En esta lección montaremos ese sistema para Rutas Norte con Argo CD, veremos su equivalente en Flux, resolveremos por fin el conflicto entre el HPA y el campo replicas anunciado en 09-01, y cerraremos el problema de los secretos en el repositorio que quedó pendiente desde 03-02.
Contenido
- Qué es GitOps y sus cuatro principios
- Modelo de envío frente a modelo de extracción
- Deriva de configuración y autocorrección
- Argo CD: arquitectura e instalación
- El recurso Application campo a campo
- App of apps y ApplicationSet
- Ondas de sincronización y hooks
- Estados de salud y sincronización
- Flux: los controladores y sus recursos
- Argo CD frente a Flux
- El problema de los secretos
- Campos que cambian solos: ignoreDifferences
- Estructura de repositorios y promoción entre entornos
- Errores comunes y consejos
- Ejercicios
- Conclusión
- Qué es GitOps y sus cuatro principios
GitOps es un modelo operativo en el que el estado deseado del sistema vive en Git y un agente automatizado lo hace realidad de forma continua. El grupo de trabajo OpenGitOps lo formalizó en cuatro principios:
- Declarativo: todo el sistema se describe por lo que debe existir, no por los pasos a ejecutar. Ya lo tenemos desde 01-06: Kubernetes es declarativo por diseño.
- Versionado e inmutable: el estado deseado se almacena de forma que el historial no se pueda alterar. Git es la implementación obvia. La pregunta "¿qué cambió en producción el martes?" pasa de ser una investigación forense a
git log. - Aplicado automáticamente: los cambios aprobados se aplican sin intervención manual. Nadie ejecuta
kubectl applynihelm upgrade: se fusiona una petición de cambio y el sistema converge. - Reconciliado de forma continua: y este es el que lo cambia todo. El agente no se limita a aplicar cuando hay un commit nuevo: comprueba continuamente que el estado real coincide con el deseado y corrige las diferencias.
flowchart LR
G[(Repositorio<br/>de manifiestos)] -->|el agente observa| A[Agente GitOps<br/>en el clúster]
A -->|compara| K[(Estado real<br/>del clúster)]
K -.->|si difiere| A
A -->|corrige| K
style A fill:#e8f4ff
Ese bucle no para nunca. Cada tres minutos (configurable), el agente se pregunta: "¿el clúster es como dice Git?". Si no lo es, actúa.
| Problema actual de Rutas Norte | Cómo lo resuelve GitOps |
|---|---|
| "Nadie sabe qué versión está en producción" | El repositorio, en la rama main, es la respuesta |
| "Se aplica a mano desde un portátil" | Nadie tiene ni necesita credenciales de despliegue |
| "Los cambios del VPA se llevan al manifiesto a mano y se olvidan" | Si no está en Git, no existe: se revierte solo |
| "¿Quién cambió esto y por qué?" | git log, git blame, la PR con su discusión |
| "Se ha perdido el clúster" | Se recrea y el agente lo repuebla |
- Modelo de envío frente a modelo de extracción
Esta es la diferencia arquitectónica clave, y tiene implicaciones de seguridad serias.
flowchart LR
subgraph push["Modelo de ENVÍO (hoy)"]
D1[Desarrollo] --> G1[(Git)] --> CI["ci-rutasnorte"]
CI -->|"kubectl apply<br/>con credenciales"| K1[(rutas-norte-pro)]
end
subgraph pull["Modelo de EXTRACCIÓN (GitOps)"]
D2[Desarrollo] --> G2[(Git)]
CI2["ci-rutasnorte"] -->|"commit de<br/>la etiqueta"| G2
G2 -.->|"el agente TIRA<br/>(solo lectura)"| A2[Agente<br/>en el clúster]
A2 --> K2[(Objetos)]
end
style CI fill:#ffe8e8
style A2 fill:#e8ffe8
En el modelo de envío, la canalización ejecuta kubectl apply contra el clúster y para eso necesita un kubeconfig con permisos. El problema, en detalle:
- Ese kubeconfig es un secreto almacenado fuera del clúster, a menudo gestionado por otro equipo o por un proveedor externo.
- Para poder desplegar cualquier cosa en cualquier namespace, suele acabar teniendo
cluster-admin. Es lo más fácil, y por eso es lo que hay en la mayoría de las empresas. - Cualquiera que pueda modificar la definición de la canalización —un fichero YAML del repositorio de código— puede ejecutar órdenes arbitrarias con esas credenciales. Una petición de cambio maliciosa a un fichero de CI equivale a acceso de administrador a producción.
- El apiserver tiene que ser alcanzable desde el exterior.
- Las credenciales hay que rotarlas, auditarlas y revocarlas, y con tres clústeres, tres juegos.
El RBAC mínimo que definimos para ci-rutasnorte en 08-01 mitiga el problema, pero no lo elimina: la canalización sigue teniendo una credencial permanente hacia dentro del clúster.
| Aspecto | Modelo de envío | Modelo de extracción |
|---|---|---|
| Quién tiene credenciales del clúster | El sistema de CI, permanentemente | Nadie fuera del clúster |
| Dirección de la conexión | CI → clúster (entrante) | Clúster → Git (saliente) |
| ¿El apiserver accesible desde fuera? | Sí | No |
| Permisos que necesita CI | Desplegar en el clúster | Solo escribir en un repositorio Git |
| Superficie de ataque si CI se compromete | El clúster entero | Un repositorio (revisable, revertible) |
| Detección de cambios manuales | Ninguna | Continua |
| Clústeres en redes privadas | Complicado (túneles, VPN) | Natural: solo necesita salida a Git |
El cambio es profundo: la credencial más peligrosa desaparece. Si alguien roba el token de escritura en Git, puede hacer un commit... que quedará registrado, será revisable y podrá revertirse.
- Deriva de configuración y autocorrección
La deriva de configuración es la diferencia entre lo que dicen tus ficheros y lo que hay realmente en el sistema. Se acumula sin que nadie se dé cuenta. Casos reales de Rutas Norte:
- Durante un incidente del puente de mayo, alguien hizo
kubectl scale deploy/api-reservas --replicas=12. Funcionó. Nadie lo llevó al manifiesto. Tres semanas después, un despliegue rutinario devolvió el Deployment a 4 réplicas y el servicio se saturó. - Se cambió un límite de memoria con
kubectl editpara probar una hipótesis. Se quedó así seis meses. - Se añadió una anotación al Ingress para ajustar un tiempo de espera. Cuando se recreó el clúster de
pre, desapareció y nadie supo por qué el comportamiento cambió.
Con GitOps, kubectl scale deploy/api-reservas --replicas=12 en producción tiene otro desenlace. Tres minutos después:
level=info msg="Detected out-of-sync resource" application=rutas-norte-pro
kind=Deployment name=api-reservas
level=info msg="Initiating self-heal"
level=info msg="Sync successful"Ha vuelto a 4. Y eso es exactamente lo que queremos. No es que la máquina sea tozuda: es que si 12 réplicas era la decisión correcta, tiene que estar en Git, donde queda revisada, documentada y presente la próxima vez que se recree el entorno.
El cambio cultural que esto provoca es más importante que la tecnología: la única forma de cambiar producción es a través de una petición de cambio. Al principio genera fricción; a los dos meses, nadie quiere volver atrás.
La autocorrección es una decisión, no una obligación. Argo CD permite activar
selfHealpor aplicación. Enrutas-norte-devpuede convenir dejarlo desactivado para que la gente experimente; enrutas-norte-prodebe estar activado.
- Argo CD: arquitectura e instalación
flowchart TB
U["Usuario<br/>(web / CLI)"] --> API["**Servidor de API**<br/>autenticación, RBAC,<br/>interfaz web"]
API --> REPO["**Servidor de repositorios**<br/>clona Git, ejecuta<br/>kustomize build /<br/>helm template, cachea"]
API --> CTRL["**Controlador de aplicaciones**<br/>compara deseado vs real,<br/>sincroniza, evalúa salud"]
REPO -.-> G[(Repositorio Git)]
CTRL --> K8S[(API de Kubernetes)]
CTRL --> REPO
| Componente | Responsabilidad | Síntoma cuando falla |
|---|---|---|
argocd-server |
Interfaz web, API, autenticación, RBAC de Argo CD | No entras en la web, pero las aplicaciones se siguen sincronizando |
argocd-repo-server |
Clona Git y renderiza los manifiestos | "failed to generate manifests"; nada se sincroniza |
argocd-application-controller |
El bucle de reconciliación | Las aplicaciones se quedan congeladas |
| Redis | Caché de manifiestos y estado | Lentitud; hay que rerenderizar todo |
| ApplicationSet controller | Genera Applications desde plantillas | Los ApplicationSet no producen nada |
Detalle clave: el controlador de aplicaciones es el único que necesita permisos sobre los objetos del clúster. El servidor de API solo gestiona recursos de Argo CD. Esa separación importa para el RBAC.
La instalación coherente con lo aprendido es por Helm (10-03), con valores versionados:
helm repo add argo https://argoproj.github.io/argo-helm
helm upgrade --install argocd argo/argo-cd -n argocd --create-namespace \
--version 7.6.12 -f plataforma/argocd/valores-pro.yaml --atomic --timeout 10m# plataforma/argocd/valores-pro.yaml
global: { domain: argocd.rutasnorte.example }
configs:
params: { server.insecure: true } # TLS lo termina el Ingress
cm: { timeout.reconciliation: 180s }
redis-ha: { enabled: true }
controller:
replicas: 2
resources: { requests: {cpu: 250m, memory: 1Gi}, limits: {memory: 2Gi} }
repoServer: { replicas: 2 }
server:
replicas: 2
ingress:
enabled: true
ingressClassName: nginx
hostname: argocd.rutasnorte.example
annotations: { cert-manager.io/cluster-issuer: letsencrypt-produccion }
tls: true# Contraseña inicial, cambiarla y borrar el Secret
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
argocd login argocd.rutasnorte.example --username admin
argocd account update-password
kubectl -n argocd delete secret argocd-initial-admin-secretEn producción, el usuario admin se desactiva y se usa un proveedor de identidad (OIDC, SSO corporativo). El RBAC de Argo CD se configura aparte del de Kubernetes, en el ConfigMap argocd-rbac-cm:
p, role:desarrollo, applications, get, */*, allow
p, role:desarrollo, applications, sync, rutas-norte/rutas-norte-dev, allow
p, role:plataforma, applications, *, */*, allow
p, role:soporte, applications, get, */*, allow
g, rutasnorte:desarrollo, role:desarrollo
g, rutasnorte:plataforma, role:plataforma
g, rutasnorte:soporte, role:soporte
policy.default: role:readonlyY se registra el repositorio con una credencial de solo lectura: Argo CD nunca escribe en el repositorio de manifiestos. Es el privilegio mínimo de 08-01 aplicado aquí.
argocd repo add [email protected]:plataforma/k8s.git \
--ssh-private-key-path ~/.ssh/argocd-rutasnorte-lectura
- El recurso Application campo a campo
Application es un CRD (06-06). Cada instancia dice: "de este repositorio, esta ruta, a este clúster y namespace, con esta política".
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: rutas-norte-pro
namespace: argocd
# El finalizador hace que borrar la Application borre también los objetos
# que gestionaba. SIN él, quedan todos huérfanos en el clúster.
finalizers: [resources-finalizer.argocd.argoproj.io]
spec:
# Proyecto: agrupa aplicaciones y restringe qué pueden desplegar y dónde
project: rutas-norte
source:
repoURL: [email protected]:plataforma/k8s.git
# Apuntamos a la superposición de Kustomize que construimos en 10-04
path: k8s/entornos/pro
# Para producción, una ETIQUETA o un commit fijo da más control
# que 'main', que despliega cualquier cosa que se fusione.
targetRevision: main
kustomize:
commonAnnotations: { rutasnorte.example/desplegado-por: argocd }
destination:
# 'kubernetes.default.svc' es el propio clúster donde corre Argo CD
server: https://kubernetes.default.svc
namespace: rutas-norte-pro
syncPolicy:
automated:
prune: true # borra del clúster lo que ya no está en Git
selfHeal: true # revierte los cambios manuales: LA AUTOCORRECCIÓN
# allowEmpty en false protege de un error de ruta que borraría todo
allowEmpty: false
syncOptions:
- CreateNamespace=true
- ServerSideApply=true # (01-06) y necesario para CRDs grandes
- RespectIgnoreDifferences=true
- PruneLast=true
retry:
limit: 5
backoff: { duration: 10s, factor: 2, maxDuration: 5m }
ignoreDifferences: # ver el apartado 12
- group: apps
kind: Deployment
jsonPointers: ["/spec/replicas"]
revisionHistoryLimit: 20El AppProject
Un AppProject restringe qué puede hacer un grupo de aplicaciones. Es una capa de seguridad esencial cuando varios equipos comparten Argo CD.
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata: { name: rutas-norte, namespace: argocd }
spec:
# Solo desde NUESTRO repositorio: nadie puede apuntar a uno externo
sourceRepos: ["[email protected]:plataforma/k8s.git"]
# Solo a NUESTROS namespaces: nadie puede desplegar en kube-system
destinations:
- { server: https://kubernetes.default.svc, namespace: rutas-norte-dev }
- { server: https://kubernetes.default.svc, namespace: rutas-norte-pre }
- { server: https://kubernetes.default.svc, namespace: rutas-norte-pro }
# Prohibido crear objetos de ámbito de clúster
clusterResourceWhitelist: []
namespaceResourceWhitelist: [{ group: '*', kind: '*' }]
namespaceResourceBlacklist:
- { group: '', kind: ResourceQuota }
- { group: '', kind: LimitRange }
# Ventana de despliegue: nada en producción los viernes por la tarde
syncWindows:
- kind: deny
schedule: "0 15 * * 5"
duration: 9h
applications: [rutas-norte-pro]
manualSync: true # se puede forzar si hay una urgenciaEse clusterResourceWhitelist: [] es importante: significa que ni un error en un manifiesto puede crear un ClusterRole o una StorageClass. Sin él, quien pueda escribir en el repositorio puede escalar privilegios en el clúster.
Origen con Helm
spec:
source:
repoURL: https://charts.jetstack.io
chart: cert-manager
targetRevision: v1.16.1 # versión del CHART
helm:
releaseName: cert-manager
valuesObject:
crds: { enabled: true, keep: true }
replicaCount: 2
syncPolicy:
syncOptions: [CreateNamespace=true, ServerSideApply=true]Argo CD ejecuta internamente helm template, así que no crea releases de Helm: no verás nada en helm list. El estado lo lleva Argo CD, no Helm. Es coherente con GitOps (el estado está en Git), pero sorprende la primera vez.
Un patrón muy útil es el de múltiples orígenes: el chart viene de un repositorio público y los valores del repositorio de la empresa, referenciado con ref: valores y valueFiles: [$valores/plataforma/.../valores-pro.yaml].
- App of apps y ApplicationSet
Con veinte aplicaciones, crear cada Application a mano y aplicarla con kubectl nos devuelve al problema original. La solución: una Application que gestiona un directorio lleno de Applications.
k8s-gitops/
├── raiz/kustomization.yaml <- la Application raíz apunta aquí
├── aplicaciones/ <- rutas-norte-{dev,pre,pro}.yaml,
│ cert-manager.yaml, ingress-nginx.yaml,
│ kube-prometheus-stack.yaml, keda.yaml...
└── proyectos/ <- rutas-norte.yaml, plataforma.yaml# La ÚNICA Application que se aplica a mano, una sola vez en la vida
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: raiz
namespace: argocd
finalizers: [resources-finalizer.argocd.argoproj.io]
spec:
project: default
source:
repoURL: [email protected]:plataforma/k8s-gitops.git
path: raiz
targetRevision: main
destination: { server: https://kubernetes.default.svc, namespace: argocd }
syncPolicy:
automated: { prune: true, selfHeal: true }Esa es la última vez que alguien ejecuta kubectl apply en Rutas Norte. A partir de ahí, añadir una aplicación nueva es añadir un fichero al directorio aplicaciones/ y fusionar la petición de cambio.
ApplicationSet: los tres entornos desde una sola definición
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata: { name: rutas-norte-entornos, namespace: argocd }
spec:
generators:
# Generador de LISTA: el más explícito y legible
- list:
elements:
- { entorno: dev, namespace: rutas-norte-dev, revision: main,
selfHeal: "false" } # en dev dejamos experimentar
- { entorno: pre, namespace: rutas-norte-pre, revision: main,
selfHeal: "true" }
- { entorno: pro, namespace: rutas-norte-pro, revision: release-2.4,
selfHeal: "true" } # producción va por etiqueta
template:
metadata:
name: 'rutas-norte-{{.entorno}}'
labels: { entorno: '{{.entorno}}' }
finalizers: [resources-finalizer.argocd.argoproj.io]
spec:
project: rutas-norte
source:
repoURL: [email protected]:plataforma/k8s.git
path: 'k8s/entornos/{{.entorno}}'
targetRevision: '{{.revision}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{.namespace}}'
syncPolicy:
automated: { prune: true, selfHeal: '{{.selfHeal}}' }
syncOptions: [CreateNamespace=true, ServerSideApply=true]
ignoreDifferences:
- { group: apps, kind: Deployment, jsonPointers: ["/spec/replicas"] }Una definición, tres aplicaciones. Hay más generadores, y algunos abren posibilidades notables:
| Generador | Qué hace |
|---|---|
list |
Elementos explícitos, como arriba |
git.directories |
Una Application por subdirectorio: añadir un entorno = crear una carpeta |
git.files |
Lee ficheros de configuración del repositorio y usa su contenido como parámetros |
clusters |
Una Application por clúster registrado que case con la etiqueta (base del multi-clúster, 11-05) |
matrix |
Producto cartesiano: 3 entornos × 4 clústeres = 12 Applications |
pullRequest |
Una Application efímera por cada PR abierta: entornos de vista previa automáticos |
Ese último es espectacular en la práctica: abres una PR con la etiqueta vista-previa y aparece un entorno completo desplegado; la cierras y desaparece solo.
- Ondas de sincronización y hooks
Argo CD aplica los objetos en un orden por tipo (Namespace, CRDs, ConfigMaps, Secrets y al final las cargas). Cuando necesitas un orden propio, usas ondas: argocd.argoproj.io/sync-wave: "-5". Van de menor a mayor, y Argo CD espera a que todos los objetos de una onda estén sanos antes de pasar a la siguiente.
| Onda | Objetos | Por qué en ese momento |
|---|---|---|
-10 |
Namespace, ResourceQuota, LimitRange | Todo lo demás vive dentro |
-5 |
ServiceAccount, Role, RoleBinding, NetworkPolicy | Los pods los necesitan al arrancar |
-3 |
ExternalSecret, ConfigMap | La configuración antes que los pods |
-1 |
Job de migración de esquema | Antes del código nuevo |
0 |
postgres-reservas, redis-cache |
La API depende de ellos |
1 |
api-reservas, worker-notificaciones |
Dependen de la base de datos |
2 |
tienda-web |
Depende de la API |
3 |
Service, Ingress | Publicar solo cuando todo esté listo |
5 |
HPA, ScaledObject, PDB | Cuando el objetivo ya existe |
Los hooks son el equivalente de los de Helm (10-03): PreSync, Sync, PostSync, SyncFail y Skip.
apiVersion: batch/v1
kind: Job
metadata:
name: migracion-esquema
annotations:
argocd.argoproj.io/hook: PreSync # antes de aplicar nada
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: "-1"
spec:
backoffLimit: 2
activeDeadlineSeconds: 900
template:
spec:
restartPolicy: Never
containers:
- name: migrador
image: registry.rutasnorte.example/api-reservas-migraciones:2.4.0
command: ["/app/migrar", "--hasta", "2.4.0"]
env:
- name: BD_PASSWORD
valueFrom:
secretKeyRef: { name: api-reservas-credenciales, key: bd-password }Las políticas de borrado son HookSucceeded, HookFailed (no la uses: pierdes los logs) y BeforeHookCreation (recomendada). Y un PostSync con un curl --fail a /salud/listo es una prueba de humo excelente: si falla, la aplicación queda Degraded y Argo CD te avisa aunque los pods estén arrancados.
- Estados de salud y sincronización
Argo CD maneja dos estados independientes. Confundirlos es el error conceptual más común.
- Estado de sincronización — ¿el clúster coincide con Git?:
Synced,OutOfSync,Unknown. - Estado de salud — ¿la aplicación funciona?:
Healthy,Progressing,Degraded,Suspended,Missing,Unknown.
Son ortogonales. Las cuatro combinaciones importantes:
| Sync | Health | Qué significa |
|---|---|---|
Synced |
Healthy |
Todo perfecto |
Synced |
Degraded |
Git dice la verdad, pero la aplicación está rota. Problema de la aplicación, no del despliegue: la imagen no existe, la sonda falla, faltan recursos |
OutOfSync |
Healthy |
Funciona, pero no es lo que dice Git: deriva manual o commit pendiente |
OutOfSync |
Degraded |
La peor: ni coincide ni funciona |
Argo CD sabe evaluar la salud de los tipos estándar (Deployment, Service, Ingress, StatefulSet, Job, PVC). Para CRDs se escriben evaluadores propios en Lua, en el ConfigMap argocd-cm:
resource.customizations.health.keda.sh_ScaledObject: |
hs = {}
if obj.status ~= nil and obj.status.conditions ~= nil then
for i, condition in ipairs(obj.status.conditions) do
if condition.type == "Ready" and condition.status == "True" then
hs.status = "Healthy"; return hs
end
end
end
hs.status = "Progressing"; return hsargocd app get rutas-norte-pro
argocd app diff rutas-norte-pro # Git vs clúster
argocd app sync rutas-norte-pro --dry-run
argocd app sync rutas-norte-pro --resource apps:Deployment:api-reservas
argocd app history rutas-norte-pro
argocd app rollback rutas-norte-pro 12
argocd app wait rutas-norte-pro --health --timeout 600Ojo con argocd app rollback: revierte el clúster, pero no revierte Git. Con selfHeal activo, en tres minutos el agente devolverá el clúster a lo que dice Git y tu rollback desaparecerá. El rollback de Argo CD es una medida de emergencia; el rollback de verdad en GitOps es git revert seguido de una petición de cambio, y Argo CD sincroniza solo.
- Flux: los controladores y sus recursos
Flux tiene una filosofía distinta: en vez de una aplicación con interfaz web, es un conjunto de controladores especializados, cada uno con su CRD.
flowchart TB
SC["**source-controller**<br/>clona Git, descarga charts"] --> KC["**kustomize-controller**<br/>construye y aplica"]
SC --> HC["**helm-controller**<br/>releases de Helm"]
IRC["**image-reflector**<br/>escanea registros"] --> IAC["**image-automation**<br/>hace commit de la etiqueta"]
KC --> K[(Clúster)]
HC --> K
IAC --> G[(Git)]
curl -s https://fluxcd.io/install.sh | sudo bash
flux check --pre
flux bootstrap git --url=ssh://[email protected]/plataforma/k8s-gitops.git \
--branch=main --path=clusters/rutas-norte-pro --private-key-file=~/.ssh/fluxEse bootstrap es una decisión de diseño interesante: Flux se instala a sí mismo mediante GitOps. Escribe sus propios manifiestos en el repositorio y luego se gestiona desde ahí. Actualizar Flux es hacer un commit.
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata: { name: rutas-norte, namespace: flux-system }
spec:
interval: 1m
url: ssh://[email protected]/plataforma/k8s.git
ref: { branch: main }
secretRef: { name: flux-clave-ssh }
---
# Ojo: esta Kustomization es de Flux, NO el kustomization.yaml de Kustomize
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata: { name: rutas-norte-pro, namespace: flux-system }
spec:
interval: 5m
path: ./k8s/entornos/pro
prune: true # equivale al prune de Argo CD
wait: true
timeout: 10m
sourceRef: { kind: GitRepository, name: rutas-norte }
# DEPENDENCIAS EXPLÍCITAS: equivalen a las ondas, pero más claras
dependsOn: [{ name: infraestructura }]
healthChecks:
- { apiVersion: apps/v1, kind: Deployment, name: api-reservas, namespace: rutas-norte-pro }
postBuild:
substitute: { entorno: pro } # lo más parecido a plantillas que tiene FluxPara los charts de terceros están HelmRepository y HelmRelease, y a diferencia de Argo CD, Flux sí crea releases de Helm de verdad (aparecen en helm list), con install.remediation y upgrade.remediation como equivalente de --atomic.
Automatización de imágenes
Capacidad que Flux tiene de serie y Argo CD solo mediante un proyecto aparte:
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata: { name: api-reservas, namespace: flux-system }
spec:
imageRepositoryRef: { name: api-reservas }
policy:
semver: { range: "~2.4.0" } # 2.4.0, 2.4.1... pero NUNCA 2.5.0Y en el manifiesto se marca qué campo actualizar: newTag: 2.4.1 # {"$imagepolicy": "flux-system:api-reservas:tag"}. Flux detecta una etiqueta nueva en el registro, hace un commit en Git actualizándola, y el ciclo GitOps normal la despliega. El estado sigue en Git: la automatización escribe en Git, no en el clúster.
flux get kustomizations
flux reconcile kustomization rutas-norte-pro --with-source
flux suspend / resume kustomization rutas-norte-pro
flux diff kustomization rutas-norte-pro --path ./k8s/entornos/pro
- Argo CD frente a Flux
| Criterio | Argo CD | Flux |
|---|---|---|
| Interfaz web | Excelente: mapa visual, logs, diffs | No tiene (Weave GitOps como añadido) |
| CLI | Buena | Excelente, pensada para trabajar sin interfaz |
| Modelo mental | Una aplicación = un recurso Application |
Varios controladores, cada uno con su CRD |
| Curva de aprendizaje | Más suave: se ve lo que pasa | Más abrupta: hay que entender la cadena |
| Multi-inquilino | AppProject con RBAC propio |
Namespaces + RBAC de Kubernetes |
| Helm | helm template; no crea releases |
Releases reales, con helm list |
| Automatización de imágenes | Proyecto aparte (Image Updater) | Integrada y madura |
| Multi-clúster | Desde un Argo CD central hacia N clústeres | Un Flux por clúster |
| Consumo de recursos | Mayor (web, Redis, API) | Menor: solo controladores |
| Dependencias entre aplicaciones | Ondas (anotaciones) | dependsOn (explícito) |
| Despliegue progresivo | Argo Rollouts | Flagger |
| Generación masiva | ApplicationSet con muchos generadores |
Menos flexible |
Elige Argo CD si quieres que desarrollo y soporte puedan ver el estado del despliegue sin aprender kubectl (la interfaz web es una herramienta de comunicación, no un capricho), gestionas varios clústeres desde un punto central, o necesitas la flexibilidad de ApplicationSet.
Elige Flux si prefieres una herramienta ligera operada por CLI y por Git, quieres la automatización de imágenes de serie, necesitas releases de Helm de verdad, o tienes muchos clústeres pequeños y autónomos.
Para Rutas Norte elegimos Argo CD, por dos razones concretas: el equipo de soporte necesita ver el estado de los despliegues sin ser experto en Kubernetes, y ApplicationSet genera los tres entornos desde una definición. Ninguna elección es irreversible: ambas leen los mismos manifiestos de Kustomize y los mismos charts de Helm, así que cambiar afecta a los ficheros de k8s-gitops/, no a k8s/.
- El problema de los secretos
Aquí llega la objeción que aparece siempre: si todo va en Git, ¿dónde pongo la contraseña de postgres-reservas?
En 03-02 dijimos que los Secrets están codificados en base64, no cifrados, y que versionarlos requiere una herramienta específica. Ha llegado el momento. Y recuerda: un secreto que ha estado en Git está comprometido para siempre, aunque hagas commit para borrarlo. El historial lo conserva, y cualquiera que haya clonado el repositorio lo tiene.
Solución A: SOPS (cifrado en el fichero)
SOPS cifra los valores de un YAML dejando las claves legibles, usando una clave de un KMS, age o PGP.
# .sops.yaml en la raíz del repositorio
creation_rules:
- path_regex: k8s/entornos/pro/secretos.*\.yaml$
age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8pstringData:
# La CLAVE se lee; el VALOR está cifrado
bd-password: ENC[AES256_GCM,data:8f2a91bc4d7e...,iv:3c1a...,tag:9e4f...,type:str]Ventaja enorme: los diffs siguen siendo legibles, ves qué clave cambió aunque no su valor. Flux trae soporte nativo (decryption: { provider: sops, secretRef: ... }); en Argo CD hace falta un plugin o ksops, lo que es un punto en contra.
Solución B: Sealed Secrets (cifrado asimétrico por clúster)
Un controlador en el clúster genera un par de claves. Tú cifras con la pública; solo ese clúster puede descifrar.
kubectl create secret generic api-reservas-credenciales \
--from-literal=bd-password='contrasena-super-secreta' \
--namespace=rutas-norte-pro --dry-run=client -o yaml > /tmp/en-claro.yaml
kubeseal --format yaml < /tmp/en-claro.yaml > k8s/entornos/pro/secreto-sellado.yaml
rm /tmp/en-claro.yaml # ¡importante!El SealedSecret resultante, con su encryptedData, es seguro en un repositorio público. Contrapartidas: el diff no dice nada (todo el bloque cambia aunque cambies una letra), el secreto está atado a un namespace y nombre concretos, y hay que respaldar la clave maestra del controlador o perderás la capacidad de descifrar si recreas el clúster.
Solución C: External Secrets Operator (referencia, sin cifrado)
La más limpia conceptualmente: el secreto no está en Git de ninguna forma, ni cifrado. En Git hay solo una referencia.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata: { name: api-reservas-credenciales, namespace: rutas-norte-pro }
spec:
# Recomprobar cada hora: si el secreto rota en Vault, el Secret
# de Kubernetes se actualiza solo.
refreshInterval: 1h
secretStoreRef: { name: vault-rutasnorte, kind: ClusterSecretStore }
target: { name: api-reservas-credenciales, creationPolicy: Owner }
data:
- secretKey: bd-password
remoteRef: { key: rutas-norte/pro/postgres, property: password }
- secretKey: pasarela-token
remoteRef: { key: rutas-norte/pro/pagos, property: token }| Criterio | SOPS | Sealed Secrets | External Secrets |
|---|---|---|---|
| Dónde vive el secreto | Cifrado en Git | Cifrado en Git | Fuera de Git |
| Diffs legibles | Sí | No | Sí (no hay secreto) |
| Rotación | Recifrar y commit | Resellar y commit | Automática |
| Infraestructura extra | Un KMS o clave age | Un controlador | Vault/KMS + operador |
| Riesgo si Git se filtra | Bajo | Bajo | Nulo |
| Descifrar en varios clústeres | Sí | No (uno por clúster) | Sí |
| Integración Argo CD / Flux | Regular / Nativa | Buena / Buena | Buena / Buena |
| Auditoría de accesos | No | No | Sí |
Recomendación para Rutas Norte: External Secrets Operator con Vault. La plataforma maneja datos personales de clientes y credenciales de la pasarela de pagos; la rotación automática y la auditoría de accesos no son lujos. Sealed Secrets es una alternativa razonable para empezar sin infraestructura extra.
Regla que no cambia con ninguna de las tres: si un secreto ha llegado a Git en claro, hay que rotarlo. Siempre.
- Campos que cambian solos: ignoreDifferences
Y llegamos al problema que dejamos anunciado en 09-01.
api-reservas tiene un HPA que ajusta spec.replicas entre 4 y 20 según la carga. El manifiesto en Git dice replicas: 4. Cuando llega el puente de mayo, el HPA sube a 15. Sin nada más:
14:22:11 Detected out-of-sync: Deployment/api-reservas
Live: spec.replicas = 15 | Desired: spec.replicas = 4
14:22:13 Sync successfulArgo CD ha bajado a 4 réplicas en plena hora punta. Treinta segundos después, el HPA vuelve a subirlas. Y a los tres minutos, Argo CD las baja otra vez. Es un bucle de guerra entre dos controladores, y el que sufre es el cliente que intenta comprar un billete.
La solución tiene dos partes. Primera: no declarar replicas en el manifiesto, como ya hicimos en 10-04. Segunda: ignoreDifferences, porque aunque el campo no esté en Git, Argo CD compara el objeto vivo con el generado y lo detecta igualmente.
spec:
ignoreDifferences:
# 1. El HPA gobierna las réplicas
- group: apps
kind: Deployment
jsonPointers: ["/spec/replicas"]
# 2. El VPA modifica los recursos (09-02). jqPathExpressions es
# más expresivo que jsonPointers cuando hay condiciones.
- group: apps
kind: Deployment
name: worker-notificaciones
jqPathExpressions:
- '.spec.template.spec.containers[] | select(.name == "worker") | .resources'
# 3. cert-manager inyecta el CA bundle en los webhooks (04-05)
- group: admissionregistration.k8s.io
kind: ValidatingWebhookConfiguration
jqPathExpressions: ['.webhooks[]?.clientConfig.caBundle']
# 4. El controlador de Service asigna clusterIP y nodePort
- group: ""
kind: Service
jsonPointers: ["/spec/clusterIP", "/spec/ports/0/nodePort"]
# 5. Ignorar TODO lo que gestione otro controlador. La opción más
# robusta cuando se usa server-side apply.
- group: apps
kind: Deployment
managedFieldsManagers: [kube-controller-manager, vpa-updater]Flux lo resuelve de otra forma: quitando el campo del objeto deseado antes de aplicar, con un patch de op: remove sobre /spec/replicas, de modo que server-side apply no reclama su propiedad y el HPA queda libre.
La lista de campos que cambian solos en Rutas Norte
| Campo | Quién lo cambia | Lección |
|---|---|---|
Deployment.spec.replicas |
HPA / KEDA | 09-01, 09-04 |
containers[].resources |
VPA en modo Auto | 09-02 |
Service.spec.clusterIP, .nodePort |
Controlador de Services | 04-02 |
PVC.spec.volumeName |
Aprovisionador dinámico | 05-05 |
webhooks[].clientConfig.caBundle |
cert-manager | 04-05 |
metadata.finalizers |
Varios operadores | 06-07 |
Consejo de operación: cuando una aplicación aparece OutOfSync de forma persistente y no entiendes por qué, argocd app diff te lo dice en dos segundos. Casi siempre es un campo de esta lista.
- Estructura de repositorios y promoción entre entornos
git.rutasnorte.example/
├── aplicaciones/ <- código fuente + Dockerfile + pruebas
└── plataforma/
├── k8s/ <- manifiestos (base + entornos, de 10-04)
└── k8s-gitops/ <- Applications, AppProjects, ApplicationSetsCinco razones para separar código y manifiestos:
| Razón | Explicación |
|---|---|
| Ciclos distintos | Un cambio de código dispara construcción y pruebas; uno de manifiesto, solo despliegue |
| Bucle infinito | Si la canalización hace commit de la etiqueta en el repositorio del código, se dispara a sí misma |
| Permisos distintos | Desarrollo escribe en el código; solo plataforma aprueba cambios en k8s/entornos/pro |
| Auditoría limpia | git log de k8s/ responde "qué cambió en producción" sin ruido de código |
| Acceso de Argo CD | El agente solo necesita leer los manifiestos, nunca el código fuente |
El flujo completo
sequenceDiagram
participant D as Desarrollo
participant CI as ci-rutasnorte
participant R as registry
participant CM as Repo k8s
participant A as Argo CD
participant K as Clúster
D->>CI: fusiona el código en main
CI->>CI: construye, prueba, escanea (Trivy, 08-06)
CI->>R: publica api-reservas:2.4.1@sha256:... y firma con Cosign (08-05)
CI->>CM: commit automático en k8s/entornos/dev
CM-->>A: el agente detecta el commit
A->>K: despliega en rutas-norte-dev + prueba de humo PostSync
D->>CM: PR: promocionar a pre (MISMO digest, 1 aprobación)
CM-->>A: sincroniza -> rutas-norte-pre
Note over K: pruebas de carga con k6 (09-06)
D->>CM: PR: promocionar a pro (2 aprobaciones + ventana)
CM-->>A: sincroniza -> rutas-norte-pro
El último paso de la canalización, que solo toca dev:
git clone --depth 1 [email protected]:plataforma/k8s.git /tmp/k8s
cd /tmp/k8s/k8s/entornos/dev
kustomize edit set image \
"registry.rutasnorte.example/api-reservas=registry.rutasnorte.example/api-reservas:${VERSION}@${DIGEST}"
git -c user.name=ci-rutasnorte -c [email protected] \
commit -am "dev: api-reservas ${VERSION} (${DIGEST:0:19})"
git push origin mainSe promociona el digest, no la etiqueta. Es la garantía de que lo que se probó en pre es bit a bit lo que va a pro. La petición de cambio que promociona a producción es literalmente copiar tres líneas de pre/ a pro/: un diff revisable en diez segundos.
| Cambio | Quién propone | Quién aprueba | Automático |
|---|---|---|---|
Imagen nueva en dev |
ci-rutasnorte |
Nadie | Sí |
Configuración de dev |
Desarrollo | Desarrollo | No |
Promoción a pre |
Desarrollo | Plataforma | No |
Promoción a pro |
Plataforma | Plataforma + responsable de producto | No |
Cambio en k8s/base o en k8s-gitops |
Cualquiera / Plataforma | Plataforma | No |
| Revertir producción | Cualquiera | 1 aprobación (vía rápida) | No |
Esa última fila es importante: revertir tiene que ser rápido. Si el procedimiento de vuelta atrás es tan pesado como el de despliegue, la gente evitará revertir y arreglará a mano, que es lo que queríamos eliminar.
Las comprobaciones obligatorias en toda PR son las de 10-04: kustomize build de las tres superposiciones, kubeconform contra el esquema, kyverno apply de las políticas (08-03) y argocd app diff comentado automáticamente en la PR.
El primer día que alguien borra algo
Esta es la anécdota que convence a los escépticos, y ocurre en todas las empresas que adoptan GitOps. Una persona de soporte, depurando un problema, ejecuta kubectl delete deployment api-reservas -n rutas-norte-pro. Se le hiela la sangre.
14:31:02 Deployment/api-reservas is Missing → Auto-sync (selfHeal)
14:31:04 Deployment/api-reservas created
14:31:39 Application rutas-norte-pro: HealthyTreinta y siete segundos. Los pods vuelven, la configuración es exactamente la correcta, y hay un registro de lo que pasó.
Ese día el equipo entiende que el repositorio no es una copia de la documentación del clúster: es el clúster. El clúster es solo la proyección actual, y es reemplazable. La prueba definitiva, que conviene ensayar una vez al año: destruir rutas-norte-pre, crear un clúster nuevo, instalar Argo CD y aplicar una Application, la raíz. En veinte minutos está reconstruido entero: namespaces, aplicaciones, políticas, monitorización, certificados. Los datos se restauran aparte con Velero (05-06), pero la plataforma se reconstruye sola.
Errores Comunes y Consejos
1. Olvidar el finalizador resources-finalizer.argocd.argoproj.io. Sin él, borrar una Application deja todos sus objetos huérfanos en el clúster, sin nadie que los gestione.
2. Activar prune: true sin entender el alcance. Si te equivocas en path y apunta a un directorio vacío, Argo CD borrará todo lo que gestionaba. Protégete con allowEmpty: false y prueba siempre en dev primero.
3. selfHeal peleando con el HPA. El bucle del apartado 12. Quita replicas del manifiesto y añade ignoreDifferences. Ambas cosas.
4. targetRevision: HEAD o main en producción. Cualquier commit a main se despliega inmediatamente en rutas-norte-pro. Usa una etiqueta o un commit fijo, y promociona conscientemente.
5. Poner secretos en el repositorio "de momento, ya lo arreglaremos". No se arregla, y el secreto queda en el historial para siempre. Monta SOPS, Sealed Secrets o External Secrets antes del primer despliegue.
6. Dar a la credencial de Argo CD permisos de escritura en Git. Solo necesita leer.
7. No configurar AppProject y dejar todo en default. El proyecto default permite desplegar cualquier cosa en cualquier namespace desde cualquier repositorio: quien pueda escribir en el repositorio de manifiestos puede escalar a administrador del clúster.
8. Confundir Synced con Healthy. Synced + Degraded significa que el despliegue funcionó y la aplicación está rota: mira los pods, no el repositorio.
9. Usar argocd app rollback creyendo que resuelve el problema. Revierte el clúster, no Git. Con selfHeal, en tres minutos vuelve el estado malo. El rollback real es git revert.
10. Todo en una sola Application gigante. Cuando algo falla, toda la aplicación queda Degraded y no sabes qué. Divide por componente o por capa, con ondas para el orden.
11. Editar en producción "solo esta vez, es una urgencia". Se revertirá solo. Si el cambio es correcto, la vía rápida es una PR con una aprobación, que tarda dos minutos. Prepara ese procedimiento antes de necesitarlo.
12. No monitorizar el propio Argo CD. Si el controlador está caído, nada se sincroniza y no te enteras. Alerta sobre argocd_app_info{sync_status="OutOfSync"} sostenido y sobre la salud de los pods de argocd (07-04).
13. Ondas de sincronización sin comprobaciones de salud. Argo CD pasa a la onda siguiente cuando la anterior está sana; si el objeto no tiene evaluador de salud (un CRD sin resource.customizations), pasa inmediatamente y el orden no sirve de nada.
Ejercicios
Ejercicio 1: ApplicationSet con políticas por entorno
Escribe un ApplicationSet que genere las tres aplicaciones de Rutas Norte con estas diferencias: dev sincroniza automáticamente desde main sin selfHeal; pre desde main con selfHeal y poda; pro desde la etiqueta release-2.4 con selfHeal, poda y server-side apply. Los tres deben ignorar spec.replicas de los Deployments. Añade el AppProject que restrinja el despliegue a los tres namespaces y prohíba crear recursos de ámbito de clúster.
Ejercicio 2: diagnosticar y resolver una deriva persistente
rutas-norte-pro lleva dos días en OutOfSync aunque nadie ha tocado nada. Describe el procedimiento completo de diagnóstico con las órdenes exactas, identifica al menos tres causas posibles con lo estudiado en el curso, y escribe el ignoreDifferences que las resolvería.
Ejercicio 3: ordenar el despliegue con ondas y hooks
Diseña las anotaciones necesarias para que un despliegue ocurra en este orden: (1) namespace y cuotas, (2) ExternalSecrets y ConfigMaps, (3) migración de esquema antes que nada más, (4) postgres-reservas y redis-cache, (5) api-reservas, (6) tienda-web, (7) Ingress, (8) HPA y PDB, y una prueba de humo final que marque la aplicación como degradada si falla. Explica qué pasa si la migración falla.
Soluciones
Solución 1
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata: { name: rutas-norte, namespace: argocd }
spec:
sourceRepos: ["[email protected]:plataforma/k8s.git"]
destinations:
- { server: https://kubernetes.default.svc, namespace: rutas-norte-dev }
- { server: https://kubernetes.default.svc, namespace: rutas-norte-pre }
- { server: https://kubernetes.default.svc, namespace: rutas-norte-pro }
clusterResourceWhitelist: [] # prohibido crear objetos de clúster
namespaceResourceWhitelist: [{ group: '*', kind: '*' }]
---
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata: { name: rutas-norte-entornos, namespace: argocd }
spec:
generators:
- list:
elements:
- { entorno: dev, rev: main, selfHeal: "false", prune: "false", ssa: "false" }
- { entorno: pre, rev: main, selfHeal: "true", prune: "true", ssa: "false" }
- { entorno: pro, rev: release-2.4, selfHeal: "true", prune: "true", ssa: "true" }
template:
metadata:
name: 'rutas-norte-{{.entorno}}'
labels: { entorno: '{{.entorno}}' }
finalizers: [resources-finalizer.argocd.argoproj.io]
spec:
project: rutas-norte
source:
repoURL: [email protected]:plataforma/k8s.git
path: 'k8s/entornos/{{.entorno}}'
targetRevision: '{{.rev}}'
destination:
server: https://kubernetes.default.svc
namespace: 'rutas-norte-{{.entorno}}'
syncPolicy:
automated: { prune: '{{.prune}}', selfHeal: '{{.selfHeal}}', allowEmpty: false }
syncOptions: ['CreateNamespace=true', 'ServerSideApply={{.ssa}}']
ignoreDifferences:
- { group: apps, kind: Deployment, jsonPointers: ["/spec/replicas"] }Solución 2
argocd app get rutas-norte-pro # ¿qué objeto está desincronizado?
argocd app diff rutas-norte-pro # ¿en qué campo? LA ORDEN DECISIVA
kubectl get deploy api-reservas -n rutas-norte-pro \
--show-managed-fields -o yaml | yq '.metadata.managedFields' # ¿quién lo gestiona?
kubectl get events -n rutas-norte-pro --sort-by=.lastTimestamp | tail -20
kubectl logs -n argocd deploy/argocd-repo-server --tail=100 | grep -i error| Causa | Campo | Solución |
|---|---|---|
| HPA escalando (09-01) | Deployment.spec.replicas |
Quitar de Git + ignoreDifferences |
| VPA en modo Auto (09-02) | containers[].resources |
jqPathExpressions |
| cert-manager inyectando el CA (04-05) | webhooks[].clientConfig.caBundle |
jqPathExpressions |
ignoreDifferences:
- { group: apps, kind: Deployment, jsonPointers: ["/spec/replicas"] }
- group: apps
kind: Deployment
name: worker-notificaciones
jqPathExpressions: ['.spec.template.spec.containers[] | select(.name=="worker") | .resources']
- group: admissionregistration.k8s.io
kind: ValidatingWebhookConfiguration
jqPathExpressions: ['.webhooks[]?.clientConfig.caBundle']Solución 3
| Objeto | Anotación |
|---|---|
| Namespace, ResourceQuota | sync-wave: "-10" |
| ExternalSecret, ConfigMap | sync-wave: "-5" |
| Job de migración | hook: PreSync, hook-delete-policy: BeforeHookCreation |
postgres-reservas, redis-cache |
sync-wave: "0" |
api-reservas / tienda-web |
sync-wave: "1" / "2" |
| Service, Ingress | sync-wave: "3" |
| HPA, PDB | sync-wave: "5" |
| Job de prueba de humo | hook: PostSync |
Si la migración falla: el hook PreSync no completa, así que Argo CD aborta la sincronización sin aplicar ningún manifiesto. El clúster queda exactamente como estaba, sirviendo la versión anterior. La aplicación aparece Sync Failed y el Job sigue existiendo (por BeforeHookCreation), de modo que kubectl logs job/migracion-esquema -n rutas-norte-pro da el motivo. Es el mismo comportamiento protector que los hooks de Helm de 10-03.
Conclusión
GitOps cierra el círculo que abrimos al principio del módulo. Rutas Norte ha pasado de 120 ficheros aplicados a mano desde un portátil a un sistema donde el repositorio es la fuente de verdad y el clúster converge hacia él continuamente.
- Los cuatro principios —declarativo, versionado e inmutable, aplicado automáticamente, reconciliado de forma continua— son el contrato. El cuarto es el que lo cambia todo.
- El modelo de extracción elimina la credencial más peligrosa:
ci-rutasnorteya no necesita acceso al clúster, solo escribir en un repositorio Git, y el apiserver deja de tener que ser accesible desde fuera. - La deriva de configuración se detecta y se corrige sola. El cambio cultural que provoca —"la única forma de cambiar producción es una petición de cambio"— vale más que la tecnología.
- Argo CD con su
Application, susAppProjectque limitan el alcance, el patrón app of apps que reduce el trabajo manual a un únicokubectl applyen toda la vida del clúster, y losApplicationSetque generan los tres entornos desde una definición. Las ondas y los hooks ordenan el despliegue y protegen la migración de esquema. - Flux ofrece lo mismo con controladores especializados, releases de Helm reales y automatización de imágenes integrada. La elección no es irreversible: leen los mismos manifiestos.
- Los secretos tienen tres soluciones prácticas: SOPS (diffs legibles), Sealed Secrets (sin infraestructura extra) y External Secrets Operator (el secreto nunca llega a Git, con rotación automática). Para Rutas Norte, la tercera.
- Y el conflicto entre el HPA y
replicasque arrastrábamos desde 09-01 queda resuelto: fuera del manifiesto eignoreDifferencesen la Application, junto con toda la familia de campos que otros controladores modifican.
Nos queda una pieza del ecosistema. Todo lo que hemos montado tiene que correr en algún sitio, y en 10-02 vimos lo que cuesta operar un clúster propio. En la siguiente lección, Kubernetes Gestionado: EKS, AKS y GKE, veremos qué te ahorra un proveedor de nube y qué sigue siendo tuyo: el modelo de responsabilidad compartida, la comparación honesta de los tres grandes, la federación de identidad que dejamos pendiente en 03-06, el uso de instancias interrumpibles para worker-notificaciones e informes-ocupacion, y el criterio de decisión final para Rutas Norte.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
