Módulo 7: Eks Specifics For Production
8. Proyecto: el plan de migración de Andes Cargo, `kind` → EKS real
Descripción
Este es el único documento de todo el Módulo 7 con evidencia ejecutada — no porque hable de EKS de forma distinta al resto del módulo, sino porque el trabajo real de este proyecto no requiere ninguna llamada a AWS: es un análisis, recurso por recurso, de los diez manifiestos reales que ya existen en andes-cargo-k8s/, el repositorio que Gitea sirve y ArgoCD sincroniza de verdad desde el Módulo 5. Cada fila de la tabla de este proyecto está construida contra el contenido literal de esos archivos —los mismos que corrieron contra andes-cargo-cluster en kind—, no contra documentación externa ni contra ninguna suposición genérica de "así suele funcionar una migración a EKS".
Conexión con el módulo
Las lecciones 2-7 de este módulo construyeron, pieza por pieza, el vocabulario y la sintaxis representativa de EKS: plano de control gestionado, node groups, Karpenter, IRSA/Pod Identity, el AWS Load Balancer Controller, y un cluster.yaml completo. Este proyecto no agrega ningún concepto nuevo — toma todo eso y lo aplica, con precisión quirúrgica, al repositorio real que ya existe, para responder una única pregunta honesta: de los diez archivos que hoy hacen correr andes-cargo-status-api en kind, ¿cuántos sobreviven sin tocar una línea, y cuántos necesitan cambiar?
Punto de partida: el repositorio real, tal como quedó al cerrar el Módulo 5
andes-cargo-k8s/, servido por Gitea (andes-cargo/andes-cargo-k8s), sincronizado por ArgoCD desde el Módulo 5, contiene exactamente diez manifiestos — nueve heredados de los Módulos 1-4 (confirmado en el Módulo 5, lección 3: "un repositorio... con los nueve manifiestos heredados de los Módulos 1-4"), más application.yaml, agregado en el propio Módulo 5 como el objeto autorreferencial de ArgoCD:
andes-cargo-k8s/ — REPOSITORIO REAL, 10 archivos
namespace.yaml M1 — el namespace andes-cargo
deployment.yaml M2 — el Deployment andes-cargo-status-api
service.yaml M2 — el Service status-api-service
configmap.yaml M3 — endpoint de LocalStack + región + tabla
secret.yaml M3 — AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY dummy
hpa.yaml M3 — el HorizontalPodAutoscaler
ingress.yaml M4 — la regla de Ingress (ingressClassName: nginx)
networkpolicy-default-deny.yaml M4 — política default-deny del namespace
networkpolicy-allow-ingress-nginx.yaml M4 — permiso explícito desde ingress-nginx
application.yaml M5 — el Application de ArgoCD (autorreferencial)
Este proyecto no incluye gatekeeper/ ni kyverno/ (Módulo 6) en la tabla principal, porque esos dos directorios ya son, por diseño, independientes del clúster subyacente: un ConstraintTemplate/Constraint de Gatekeeper o una ClusterPolicy de Kyverno no referencian ningún detalle específico de kind ni de EKS — corren igual, sin ningún cambio, contra el mismo kube-apiserver en cualquiera de los dos. Se retoman al final de este proyecto, en la sección de infraestructura del laboratorio, con esa misma conclusión confirmada por escrito.
La tabla completa: qué cambia, qué no, y por qué — recurso por recurso
| # | Archivo | Cambia | Razón exacta |
|---|---|---|---|
| 1 | namespace.yaml | No | Un namespace es un objeto puramente lógico de Kubernetes — no tiene ningún campo que dependa de la nube subyacente. |
| 2 | deployment.yaml | Sí | Dos campos cambian: (a) image: — pasa del registro local localhost:5001/andes-cargo-status-api:<tag> (integrado a kind, Módulo 1) a un repositorio real de ECR (000000000000.dkr.ecr.us-east-1.amazonaws.com/andes-cargo-status-api:<tag>); (b) se agrega spec.template.spec.serviceAccountName: status-api-service-account, el único campo nuevo que la lección 7 de este módulo confirmó. Réplicas, resources.limits/requests, y las tres probes del Módulo 3 no cambian. |
| 3 | service.yaml | No | Un Service de tipo ClusterIP no referencia ningún detalle de infraestructura de nube — sigue resolviendo tráfico interno entre Pods exactamente igual. |
| 4 | configmap.yaml | Sí | De las tres claves reales (DYNAMODB_ENDPOINT_URL, AWS_REGION, SHIPMENTS_TABLE_NAME), solo una se elimina: DYNAMODB_ENDPOINT_URL apuntaba a localstack.localstack.svc.cluster.local:4566 — en un EKS real hablando con DynamoDB real, boto3 no necesita ningún endpoint_url explícito, usa el endpoint regional real por defecto. AWS_REGION: "us-east-1" y SHIPMENTS_TABLE_NAME: "Shipments" permanecen idénticos — son los mismos valores reales que aws-core-services-guide fijó desde el origen del caso. |
| 5 | secret.yaml | Se vuelve innecesario | Las dos claves de este archivo (AWS_ACCESS_KEY_ID: "test", AWS_SECRET_ACCESS_KEY: "test") son credenciales dummy que existen únicamente para autenticar contra LocalStack. Con IRSA o EKS Pod Identity (Módulo 7.5) inyectando credenciales temporales automáticamente al ServiceAccount, andes-cargo-status-api nunca necesitaría leer ningún AWS_ACCESS_KEY_ID de un Secret — el SDK de AWS detecta las credenciales federadas por su cuenta, vía la cadena de credenciales por defecto. Este archivo, literalmente, dejaría de aplicarse. |
| 6 | hpa.yaml | No | El HorizontalPodAutoscaler reacciona a métricas de Pods (metrics.k8s.io), sin ningún campo específico de la nube subyacente — el mismo mecanismo exacto, ejecutado en kind en el Módulo 3, funciona igual en EKS. Lo único que cambiaría es la fuente del CPU real reportado (el kubelet de un nodo EC2 real en vez de un contenedor Docker), invisible para el YAML en sí. |
| 7 | ingress.yaml | Sí | ingressClassName: nginx → ingressClassName: alb (Módulo 7.6), y se agregan anotaciones específicas del controlador (alb.ingress.kubernetes.io/scheme, alb.ingress.kubernetes.io/target-type). host, path, pathType, y backend.service — los campos que definen el enrutamiento en sí — no cambian. |
| 8 | networkpolicy-default-deny.yaml | No | podSelector: {} con una política default-deny de tráfico entrante no referencia ningún detalle de infraestructura — el mismo bloqueo total por defecto aplica igual en cualquier clúster de Kubernetes conformante. |
| 9 | networkpolicy-allow-ingress-nginx.yaml | Sí | Esta es la más sutil de la tabla: su regla actual usa namespaceSelector: kubernetes.io/metadata.name: ingress-nginx — permite tráfico desde los Pods del controlador ingress-nginx, que corre dentro del clúster. Con el AWS Load Balancer Controller, no hay ningún Pod de Ingress corriendo dentro del clúster que enviar tráfico desde ahí: el ALB, un recurso fuera del clúster, envía tráfico directo a la IP del Pod (con target-type: ip) desde las interfaces de red que AWS asigna dentro de la VPC. Un namespaceSelector no puede identificar ese origen —no hay ningún namespace de Kubernetes que lo represente—, así que la regla tendría que cambiar a un bloque ipBlock, con el rango CIDR real de las subredes donde vive el ALB. |
| 10 | application.yaml | No | El Application de ArgoCD (source/destination/syncPolicy) es agnóstico del clúster subyacente por diseño — el mismo mecanismo de watch y convergencia funciona igual sin importar si destination.server apunta a kind o a un clúster EKS real. Solo cambiaría, en la práctica, el kubeconfig/contexto que ArgoCD usa para llegar a ese clúster — un detalle de despliegue de ArgoCD, no del propio objeto application.yaml. |
El número final, verificado dos veces
andes-cargo-k8s/ — DIEZ ARCHIVOS, CATEGORIZADOS
SIN CAMBIOS (5 de 10):
namespace.yaml · service.yaml · hpa.yaml
networkpolicy-default-deny.yaml · application.yaml
CAMBIAN, EN CAMPOS ESPECÍFICOS (4 de 10):
deployment.yaml → image: + serviceAccountName
configmap.yaml → elimina DYNAMODB_ENDPOINT_URL (2 de 3 claves quedan)
ingress.yaml → ingressClassName + anotaciones ALB
networkpolicy-allow-ingress-nginx.yaml → namespaceSelector → ipBlock
SE VUELVEN INNECESARIOS (1 de 10):
secret.yaml → reemplazado por IRSA/EKS Pod Identity
5 + 4 + 1 = 10 ✓
El hallazgo central de este proyecto, dicho sin adornos: el 50% de los manifiestos de aplicación de Andes Cargo no cambiaría ni una línea al migrar de kind a EKS. De los cinco que sí cambian o desaparecen, ninguno lo hace por una razón de "Kubernetes es distinto en EKS" — cada uno cambia porque hay infraestructura real de AWS alrededor que antes era simulada o inexistente (un registro de contenedores real, credenciales federadas reales, un balanceador real, una VPC real). Es la confirmación, con evidencia y no con una promesa genérica, de la tesis central de esta guía completa: lo que aprendiste contra kind transfiere sin traducción a EKS — la traducción que sí hace falta es acotada, nombrable, y cabe en una sola tabla.
Lo que se agrega alrededor de andes-cargo-k8s/ (no dentro del repositorio)
La migración no es solo "editar cinco archivos" — hay piezas nuevas que este módulo ya mostró, representativas, que tendrían que existir antes de que el repositorio de arriba pudiera aplicarse contra un clúster real:
| Pieza nueva | De qué lección | Reemplaza, en kind, a |
|---|---|---|
cluster.yaml (eksctl ClusterConfig) | Módulo 7.7 | kind-config.yaml (Módulo 1) |
IAMServiceAccount (status-api-service-account / StatusApiTaskRole) | Módulo 7.5 y 7.7 | Nada — este mecanismo de identidad no existe en kind |
Managed node group (andes-cargo-managed-ng) | Módulo 7.3 y 7.7 | Los dos contenedores andes-cargo-cluster-worker/worker2 de kind |
Repositorio ECR (andes-cargo-status-api) | Implícito en la fila 2 de la tabla | El registro local registry:3 integrado a kind (Módulo 1) |
AWS Load Balancer Controller instalado (con su propio IAMServiceAccount) | Módulo 7.6 | ingress-nginx (Módulo 4) |
IngressClass alb | Módulo 7.6 y 7.7 | El IngressClass nginx que ingress-nginx creó automáticamente |
La infraestructura del laboratorio: qué sigue, qué se vuelve innecesario
Los namespaces de infraestructura real del propio laboratorio —gitea, argocd, gatekeeper-system, kyverno— nunca fueron "recursos de Andes Cargo" (la propia arquitectura del Módulo 1 ya los documentó así); esta sección cierra, con la misma honestidad de toda la guía, qué pasa con cada uno en una migración real. Se agrega, aparte, la pieza que este laboratorio nunca instaló:
- La capa de datos (DynamoDB, vía
/shipments) — nunca instalada en este laboratorio, es donde/shipments/<id>pasaría de representativo a real. Es la pieza más honesta de esta sección: ningún módulo de esta guía instaló LocalStack (ni ningún namespacelocalstack) dentro deandes-cargo-cluster— por eso/shipments/<id>se mantuvo representativo desde el Módulo 2. En un EKS real, con una cuenta AWS real disponible, un equipo cablearíaandes-cargo-status-apidirecto contra la tablaShipmentsreal de DynamoDB (quitandoDYNAMODB_ENDPOINT_URLdelConfigMap, fila 4 de la tabla de arriba) — no haría falta simular nada, porque ya hay AWS real disponible. Es, precisamente, el punto de esta migración donde/shipments/<id>dejaría de ser representativo. gitea(Helm chart, Módulo 5) — sigue siendo válido, sin cambios obligatorios. Gitea es un servidor Git autónomo, sin ninguna dependencia de LocalStack ni dekind— el mismo Helm chart oficial corre igual dentro de un clúster EKS. Un equipo real podría mantenerlo (si prefiere Git autoalojado) o migrar el contenido a un proveedor administrado (GitHub, GitLab) — ninguna de las dos es obligatoria por el hecho de migrar a EKS en sí.argocd(manifiesto oficial, Módulo 5) — sin cambios. ArgoCD no tiene ninguna dependencia dekind— el mismo mecanismo de sincronización pull-based corre igual, apuntando al mismo repositorio de Gitea (o a donde el equipo decida moverlo), contra undestination.serverdistinto.gatekeeper-system/kyverno(Módulo 6) — sin cambios. Confirmado al abrir este proyecto: ni Gatekeeper ni Kyverno tienen ningún campo dependiente de la nube subyacente — los mismosConstraintTemplate/Constraintde Gatekeeper y la mismaClusterPolicyde Kyverno se aplicarían, sin editar una sola línea, contra un clúster EKS real.
LA INFRAESTRUCTURA DEL LABORATORIO, DESPUÉS DE MIGRAR
gitea ✓ SIGUE VÁLIDO — sin cambios obligatorios
argocd ✓ SIGUE VÁLIDO — sin cambios
gatekeeper-system ✓ SIGUE VÁLIDO — sin cambios
kyverno ✓ SIGUE VÁLIDO — sin cambios
Los cuatro namespaces de infraestructura real de este laboratorio
sobreviven la migración sin ningún ajuste. La capa de datos (LocalStack)
nunca fue un quinto namespace de este laboratorio para "retirar" —
nunca se instaló — así que no hay nada que dar de baja ahí: hay,
en cambio, una conexión real por cablear por primera vez.
Errores comunes
Asumir que "migrar a EKS" significa reescribir el repositorio andes-cargo-k8s/ desde cero (de sobreestimación del esfuerzo). Qué pasa: alguien, al escuchar "migración a producción real", asume un esfuerzo de reescritura completa. Cómo detectarlo: si tu estimación de esfuerzo para esta migración no distingue "editar cinco archivos con cambios puntuales" de "reescribir diez archivos desde cero". Cómo corregirlo: la tabla de este proyecto demuestra, con evidencia contra el contenido real de cada archivo, que el 50% no cambia, y que ninguno de los cinco que sí cambian requiere más que un puñado de campos — esta es exactamente la evidencia que corrige esa sobreestimación.
Olvidar secret.yaml en el conteo final, tratándolo como "un archivo que cambia" en vez de "un archivo que desaparece" (de categorización). Qué pasa: alguien clasifica secret.yaml en la misma categoría que deployment.yaml o ingress.yaml ("cambia"), sin notar que la conclusión real es más fuerte: ese archivo deja de aplicarse por completo. Cómo detectarlo: si tu propio resumen dice "6 de 10 archivos cambian" en vez de "4 cambian, 1 se vuelve innecesario, 5 no cambian". Cómo corregirlo: vuelve a la fila 5 de la tabla — la razón exacta (IRSA/Pod Identity reemplaza por completo la necesidad de credenciales estáticas) es más fuerte que un simple "cambio de valor": es la eliminación completa de la categoría de objeto, no un ajuste de contenido.
Pensar que networkpolicy-allow-ingress-nginx.yaml no necesita ningún cambio porque "las NetworkPolicy no dependen de la nube" (generalización incorrecta a partir de la fila 8 de la misma tabla). Qué pasa: alguien, al ver que networkpolicy-default-deny.yaml (fila 8) no cambia, generaliza esa conclusión a la otra NetworkPolicy del mismo módulo. Cómo detectarlo: si tu plan de migración trata ambas políticas de red de forma idéntica. Cómo corregirlo: la diferencia no es si una NetworkPolicy "depende de la nube" en general —ninguna lo hace, como objeto—, sino si su selector específico referencia algo que solo existe en la topología de kind con ingress-nginx corriendo dentro del clúster. default-deny usa podSelector: {}, sin ninguna referencia externa — sobrevive intacta. allow-ingress-nginx selecciona explícitamente un namespace que no tendría ningún Pod real corriendo ahí en la topología de EKS con AWS Load Balancer Controller — esa selección específica es la que falla, no el concepto de NetworkPolicy en general.
Ejercicios
Ejercicio 1 — Recita el conteo final de memoria. Sin mirar la sección "El número final" de este proyecto, escribe: ¿cuántos de los diez archivos de andes-cargo-k8s/ no cambian, cuántos cambian en campos específicos, y cuántos se vuelven innecesarios? Nombra cada archivo en su categoría correcta.
Ver solución
Sin cambios (5): namespace.yaml, service.yaml, hpa.yaml, networkpolicy-default-deny.yaml, application.yaml. Cambian (4): deployment.yaml (imagen + serviceAccountName), configmap.yaml (elimina una de tres claves), ingress.yaml (ingressClassName + anotaciones), networkpolicy-allow-ingress-nginx.yaml (namespaceSelector → ipBlock). Se vuelven innecesarios (1): secret.yaml. Si tu recuento sumó 5+4+1=10 sin necesitar releer la tabla, dominas el resultado central de este proyecto.
Ejercicio 2 — Explica por qué secret.yaml desaparece, sin usar la palabra "IRSA" ni "Pod Identity". Un colega que todavía no llegó a la lección 5 de este módulo te pregunta por qué secret.yaml "se vuelve innecesario" en vez de solo "cambiar de valores". Explícaselo en dos frases, sin nombrar ningún mecanismo específico de AWS.
Ver solución
Una explicación completa suena, más o menos, así: "Ese archivo existe hoy solo para darle a la aplicación una credencial de AWS que pueda leer, porque no hay ninguna otra forma de que un Pod en kind demuestre quién es ante un servicio de AWS simulado. En un clúster EKS real, el propio clúster puede darle a ese Pod una identidad temporal de forma automática, sin que ningún archivo tenga que guardar ni una clave ni un secreto — la aplicación simplemente deja de necesitar leer ninguna credencial de ningún lado."
Ejercicio 3 — Predice qué pasaría si alguien aplicara networkpolicy-allow-ingress-nginx.yaml sin cambios contra un clúster EKS con el AWS Load Balancer Controller. Basándote en la fila 9 de la tabla de este proyecto, predice: si ese archivo se aplicara sin ningún cambio, ¿el tráfico del ALB hacia andes-cargo-status-api pasaría, o quedaría bloqueado? Justifica con el mecanismo exacto.
Ver solución
Quedaría bloqueado. La política default-deny (fila 8, sin cambios) sigue rechazando todo el tráfico entrante por defecto; la única excepción que el namespace andes-cargo tendría sería la regla de allow-ingress-nginx, que permite tráfico específicamente desde Pods con la etiqueta de namespace ingress-nginx. Como el tráfico del ALB no se origina en ningún Pod de ese namespace —ni de ningún namespace del clúster, porque el ALB vive fuera del clúster— ese namespaceSelector nunca haría match, y el tráfico real del balanceador quedaría bloqueado por la política default-deny de base, exactamente como si no existiera ninguna regla de excepción. Es la razón exacta por la que la fila 9 de la tabla marca este archivo como "cambia", no como "sin cambios".
Resumen y siguiente paso
Este proyecto construyó el único documento con evidencia ejecutada de todo el Módulo 7: una tabla, recurso por recurso, contra los diez manifiestos reales de andes-cargo-k8s/ — el mismo repositorio que Gitea sirve y ArgoCD sincroniza de verdad desde el Módulo 5. El resultado, verificado dos veces: 5 de 10 archivos no cambian, 4 cambian en campos específicos y acotados, 1 (secret.yaml) se vuelve innecesario por completo, reemplazado por IRSA/EKS Pod Identity. Confirmaste también qué piezas nuevas se agregan alrededor del repositorio (cluster.yaml, un IAMServiceAccount, un managed node group, un repositorio ECR, el AWS Load Balancer Controller), y qué pasa con la infraestructura del propio laboratorio: Gitea, ArgoCD, Gatekeeper y Kyverno sobreviven la migración sin ningún ajuste, mientras que la capa de datos —nunca instalada como LocalStack en este laboratorio— es donde /shipments/<id> dejaría de ser representativo y pasaría a responder con datos reales de DynamoDB.
Antes de cerrar este módulo deberías poder: recitar el conteo final (5/4/1) de memoria, con cada archivo en su categoría correcta; explicar por qué secret.yaml desaparece sin nombrar ningún mecanismo específico; y describir qué pasaría si networkpolicy-allow-ingress-nginx.yaml se aplicara sin el cambio que la fila 9 de este proyecto identificó.
Con este proyecto se cierra el Módulo 7 — el módulo más honesto de esta guía sobre lo que cuesta dinero de verdad, y el único que dejó, a propósito, la mayor parte de su contenido sin ejecutar. El Módulo 8, el capstone final, retoma andes-cargo-cluster en kind —el sistema completo, con Pods, red, GitOps y guardrails de runtime, todo ejecutado— para un recorrido end-to-end final: un cambio que cruza el gate completo, y uno que el gate detiene.
Recursos
kubernetes-and-eks-in-production-guide(NIEVA), Módulo 5, lección 3 — la fuente exacta del conteo original ("nueve manifiestos heredados de los Módulos 1-4") que este proyecto extiende a diez conapplication.yaml.kubernetes-and-eks-in-production-guide(NIEVA), Módulo 3, lección 4 — el contenido literal deconfigmap.yaml/secret.yaml, la base de las filas 4 y 5 de la tabla de este proyecto.kubernetes-and-eks-in-production-guide(NIEVA), Módulo 4, lección 7 — el contenido literal denetworkpolicy-allow-ingress-nginx.yaml, la base de la fila 9.kubernetes-and-eks-in-production-guide(NIEVA), Módulo 7, lecciones 3, 5, 6 y 7 — el vocabulario y la sintaxis representativa (node groups, IRSA/Pod Identity, AWS Load Balancer Controller,cluster.yaml) que este proyecto aplica al repositorio real.- Amazon EKS — What is Amazon EKS? — referencia general del servicio de destino de esta migración.