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

  1. Qué es GitOps y sus cuatro principios
  2. Modelo de envío frente a modelo de extracción
  3. Deriva de configuración y autocorrección
  4. Argo CD: arquitectura e instalación
  5. El recurso Application campo a campo
  6. App of apps y ApplicationSet
  7. Ondas de sincronización y hooks
  8. Estados de salud y sincronización
  9. Flux: los controladores y sus recursos
  10. Argo CD frente a Flux
  11. El problema de los secretos
  12. Campos que cambian solos: ignoreDifferences
  13. Estructura de repositorios y promoción entre entornos
  14. Errores comunes y consejos
  15. Ejercicios
  16. Conclusión

  1. 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:

  1. 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.
  2. 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.
  3. Aplicado automáticamente: los cambios aprobados se aplican sin intervención manual. Nadie ejecuta kubectl apply ni helm upgrade: se fusiona una petición de cambio y el sistema converge.
  4. 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

  1. 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:

  1. Ese kubeconfig es un secreto almacenado fuera del clúster, a menudo gestionado por otro equipo o por un proveedor externo.
  2. 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.
  3. 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.
  4. El apiserver tiene que ser alcanzable desde el exterior.
  5. 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? 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.

  1. 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 edit para 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 selfHeal por aplicación. En rutas-norte-dev puede convenir dejarlo desactivado para que la gente experimente; en rutas-norte-pro debe estar activado.

  1. 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-secret

En 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:readonly

Y 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

  1. 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: 20

El 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 urgencia

Ese 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].

  1. 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 }
kubectl apply -f raiz-application.yaml

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.

  1. 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.

  1. 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 hs
argocd 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 600

Ojo 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.

  1. 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/flux

Ese 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 Flux

Para 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.0

Y 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

  1. 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/.

  1. 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: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
sops --encrypt --in-place k8s/entornos/pro/secretos.yaml
stringData:
  # 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 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 No (uno por clúster)
Integración Argo CD / Flux Regular / Nativa Buena / Buena Buena / Buena
Auditoría de accesos No No

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.

  1. 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 successful

Argo 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.

  1. 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, ApplicationSets

Cinco 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 main

Se 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
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: Healthy

Treinta 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-rutasnorte ya 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, sus AppProject que limitan el alcance, el patrón app of apps que reduce el trabajo manual a un único kubectl apply en toda la vida del clúster, y los ApplicationSet que 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 replicas que arrastrábamos desde 09-01 queda resuelto: fuera del manifiesto e ignoreDifferences en 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

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados