Módulo 4: Networking Ingress And Networkpolicy

5. Manos a la obra: `Ingress` para `status-api-service`

Descripción

Esta es la lección donde, por primera vez en toda esta guía, le hablas a andes-cargo-status-api sin kubectl port-forward. Vas a declarar status-api-ingress, confirmar que ingress-nginx —instalado y sano en la lección 4— lo detectó, y hacerle curl real contra el mismo host y puerto que cualquier cliente externo usaría. También vas a confirmar, otra vez con honestidad, que /shipments/<id> sigue sin tener datos reales a los que conectarse — ese límite no lo resuelve Ingress, y esta lección explica por qué antes de que lo confundas con un error nuevo.

Conexión con el módulo

Esta lección junta todo lo que las lecciones 2-4 construyeron: el modelo de red que hace posible que ingress-nginx alcance cualquier Pod (lección 2), el objeto Ingress explicado en teoría (lección 3), y el controlador real corriendo en el nodo correcto (lección 4). Es la primera vez que ves, con evidencia real, la promesa completa del módulo: una puerta HTTP permanente, sin depender de ninguna terminal abierta.


Paso 1 — El objeto Ingress: ingress.yaml

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: status-api-ingress
  namespace: andes-cargo
spec:
  ingressClassName: nginx
  rules:
    - host: andes-cargo.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: status-api-service
                port:
                  number: 80

Cinco campos merecen explicación, cada uno conectado directo con lo que ya construiste:

  • ingressClassName: nginx — el valor exacto que el manifiesto de la lección 4 creó como IngressClass. Sin este campo, un clúster con más de un Ingress controller instalado (el caso del Módulo 7, con el AWS Load Balancer Controller) no sabría a cuál de los dos le pertenece esta regla.
  • host: andes-cargo.local — el nombre que un cliente tiene que usar en la cabecera Host de su solicitud HTTP para que esta regla aplique. No es un dominio público real ni requiere ningún DNS configurado —esta lección va a resolverlo directo contra 127.0.0.1 en el Paso 3—, es solo el valor que ingress-nginx usa para decidir "esta solicitud es para Andes Cargo".
  • path: / con pathType: Prefix — cualquier ruta que empiece con / (es decir, todas) se enruta hacia el mismo backend. andes-cargo-status-api no necesita reglas de enrutamiento distintas por ruta todavía —/health y /shipments/<id> van al mismo lugar—, así que un único path: / alcanza.
  • backend.service.name: status-api-service — exactamente el Service del Módulo 2, sin ningún cambio. Como ya confirmó la lección 3, un Ingress nunca apunta directo a Pods, siempre a través de un Service existente.
  • port.number: 80 — el mismo port (no targetPort) que ya usaste con kubectl port-forward en el Módulo 2 — Ingress habla con el Service en su puerto declarado, y es el Service quien decide, por debajo, a cuál targetPort de contenedor reenviar.
kubectl apply -f ingress.yaml

Qué esperar:

ingress.networking.k8s.io/status-api-ingress created

Paso 2 — Verifica: el Ingress y su estado

kubectl get ingress -n andes-cargo

Qué esperar (ADDRESS puede tardar unos segundos en aparecer; en kind se llena con localhost, a diferencia del EXTERNAL-IP: <pending> del Service de la lección 4 — el mecanismo por el cual ingress-nginx calcula este valor es interno del controlador, no relevante para el resto de esta lección):

NAME                 CLASS   HOSTS               ADDRESS     PORTS   AGE
status-api-ingress   nginx   andes-cargo.local   localhost   80      0s

Confirma que la regla se registró completa, con el backend correcto y sus Endpoints reales:

kubectl describe ingress status-api-ingress -n andes-cargo

Qué esperar (las tres IPs de la última línea son tus valores variables — corresponden a los tres Pods del Deployment; el resto es literal):

Name:             status-api-ingress
Labels:           <none>
Namespace:        andes-cargo
Address:          
Ingress Class:    nginx
Default backend:  <default>
Rules:
  Host               Path  Backends
  ----               ----  --------
  andes-cargo.local  
                     /   status-api-service:80 (10.244.2.2:8080,10.244.1.2:8080,10.244.2.3:8080)
Annotations:         <none>
Events:
  Type    Reason  Age   From                      Message
  ----    ------  ----  ----                      -------
  Normal  Sync    0s    nginx-ingress-controller  Scheduled for sync

La línea Backends es la confirmación más importante de esta lección: ingress-nginx ya resolvió status-api-service hasta las IPs reales de los tres Pods —el mismo mecanismo de Endpoints que ya conoces del Módulo 2— y las tiene listas para enrutar tráfico, sin que hayas hecho ningún curl todavía.


Paso 3 — curl real, sin port-forward

Este es el momento central de la lección. kind-config.yaml (lección 4) ya reenvía los puertos 80/443 de tu máquina hacia andes-cargo-cluster-control-plane, donde corre ingress-nginx. Lo único que falta es decirle a curl que resuelva andes-cargo.local hacia tu propia máquina, sin necesidad de editar /etc/hosts:

curl -i -s --resolve andes-cargo.local:80:127.0.0.1 http://andes-cargo.local/health

Qué esperar (literal, ejecutado — Date es tu valor variable; fíjate en algo importante: ningún kubectl port-forward corriendo en ninguna terminal):

HTTP/1.1 200 OK
Date: Fri, 14 Aug 2026 20:38:49 GMT
Content-Type: application/json
Content-Length: 51
Connection: keep-alive

{"service":"andes-cargo-status-api","status":"ok"}

--resolve andes-cargo.local:80:127.0.0.1 le dice a curl: "cuando alguien te pida andes-cargo.local en el puerto 80, resuélvelo hacia 127.0.0.1, sin consultar ningún DNS real" — el mismo resultado que editar /etc/hosts, sin necesidad de permisos de administrador ni de dejar un cambio permanente en tu sistema. La cabecera Host: andes-cargo.local que curl genera automáticamente a partir de la URL es la pieza que ingress-nginx lee para decidir qué regla aplicar — exactamente el campo host que declaraste en ingress.yaml.

Compara este resultado con la lección 7 del Módulo 2: el mismo cuerpo JSON, el mismo código 200, pero esta vez sin ningún túnel temporal de por medio — el camino completo (tu máquina → Docker → andes-cargo-cluster-control-planeingress-nginxstatus-api-service → un Pod real) funciona de punta a punta, de forma permanente, mientras el clúster siga corriendo.


Paso 4 — El límite honesto, otra vez: /shipments/<id>

curl -i -s --max-time 30 --resolve andes-cargo.local:80:127.0.0.1 http://andes-cargo.local/shipments/4471

Qué esperar (literal, ejecutado — la solicitud tarda varios segundos por la misma razón que ya documentó el Módulo 3; este resultado sigue siendo el comportamiento correcto y esperado en este punto de la guía):

HTTP/1.1 500 INTERNAL SERVER ERROR
Date: Fri, 14 Aug 2026 20:43:24 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 265
Connection: keep-alive

<!doctype html>
<html lang=en>
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>

Nada de esto es un error de Ingress — es el mismo NameResolutionError/EndpointConnectionError que el Módulo 3 diagnosticó a fondo, viajando ahora a través de una capa adicional (ingress-nginx) que no cambia en nada la causa: andes-cargo-status-api sigue sin ningún LocalStack corriendo dentro del clúster al cual conectarse. Ingress resuelve cómo llega el tráfico hasta el Pod — nunca resuelve de dónde salen los datos que ese Pod necesita.

              QUÉ CAMBIÓ ENTRE EL MÓDULO 2 Y ESTA LECCIÓN

  Módulo 2, lección 7        curl vía port-forward     /health → 200
                                                         /shipments/<id> → 500
                                                         (NoCredentialsError)

  Módulo 3, lección 4        curl vía port-forward     /health → 200
                                                         /shipments/<id> → 500
                                                         (NameResolutionError)

  Módulo 4, lección 5        curl vía Ingress,          /health → 200
  (esta lección)             SIN port-forward            /shipments/<id> → 500
                                                         (misma causa: sin LocalStack)

  Lo que falta, y por qué se queda faltando:
    - LocalStack corriendo dentro del propio clúster   ──▶  fuera del alcance $0 de esta guía

Paso 5 — Un vistazo al error de "cabecera incorrecta"

Vale la pena ver, aunque sea una vez, qué pasa cuando la cabecera Host no coincide con ninguna regla de Ingress — porque el mensaje de error es distinto a cualquiera que hayas visto en esta guía, y reconocerlo te ahorra tiempo de diagnóstico:

curl -i -s --max-time 8 http://localhost:80/health

Qué esperar (literal, ejecutado — sin --resolve, curl envía Host: localhost, que ninguna regla de status-api-ingress reconoce):

HTTP/1.1 404 Not Found
Date: Fri, 14 Aug 2026 20:38:49 GMT
Content-Type: text/html
Content-Length: 146
Connection: keep-alive

<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>

Este 404 no viene de andes-cargo-status-api —Flask nunca llegó a recibir la solicitud— viene del propio ingress-nginx, actuando como "backend por defecto" cuando ninguna regla coincide con la cabecera Host recibida. Es la prueba directa de que Ingress enruta por host, no solo por puerto: llegar al puerto 80 correcto no es suficiente si la cabecera Host no coincide con ninguna regla declarada.


Analogía: el visitante que llega a la recepción, pidiendo por nombre

Retomando la analogía del edificio: --resolve andes-cargo.local:80:127.0.0.1 es el equivalente a decirle a un taxi "llévame a la dirección del edificio, y una vez ahí, pregunta por 'Andes Cargo'" — el taxi (la conexión TCP) te deja en la puerta correcta gracias a extraPortMappings (lección 4), pero el recepcionista (ingress-nginx) todavía necesita escuchar el nombre exacto (Host: andes-cargo.local) para saber a qué piso dirigirte. Preguntar por un nombre que no está en su lista de empresas registradas (Host: localhost, Paso 5) te devuelve, con toda razón, un "no hay nadie aquí con ese nombre" — el 404 que generó el propio recepcionista, sin que ninguna empresa del edificio se enterara siquiera de que preguntaste.


Errores comunes

Olvidar --resolve (o no editar /etc/hosts) y recibir un 404 inesperado, sin entender por qué (el error más común de esta lección, ya adelantado en el Paso 5 a propósito). Qué pasa: alguien corre curl http://andes-cargo.local/health directo, sin --resolve ni ninguna entrada en /etc/hosts, y curl falla con un error de resolución de DNS —no llega ni siquiera a recibir el 404 del Paso 5, porque andes-cargo.local no es un dominio real que ningún DNS público conozca—. Cómo detectarlo: el error de curl menciona "could not resolve host". Cómo corregirlo: usa --resolve andes-cargo.local:80:127.0.0.1 en cada comando (el patrón de esta lección), o agrega una línea 127.0.0.1 andes-cargo.local a /etc/hosts si prefieres no repetir la bandera en cada comando.

Confundir el 404 de ingress-nginx (Paso 5) con un 404 de la aplicación Flask (de diagnóstico). Qué pasa: alguien ve un 404 y empieza a revisar las rutas de app.py, asumiendo que Flask no reconoce la URL solicitada. Cómo detectarlo: fíjate en el cuerpo de la respuesta — el 404 de ingress-nginx trae <center>nginx</center> en el HTML; un 404 real de Flask tendría un formato completamente distinto (el mismo que verías si pidieras una ruta que no existe en app.py, como /no-existe). Cómo corregirlo: si ves el HTML genérico de nginx, el problema es de enrutamiento de Ingress (cabecera Host incorrecta, o ninguna regla que coincida) — la solicitud nunca llegó a andes-cargo-status-api.

Esperar que /shipments/<id> funcione ahora que hay Ingress (de continuidad, ya anticipado en la lección 1 de este módulo). Qué pasa: alguien, al ver curl funcionando por primera vez sin port-forward, espera que también el segundo endpoint responda con datos reales. Cómo detectarlo: si te sorprende ver el mismo 500 que dejaron los módulos 2 y 3. Cómo corregirlo: Ingress resolvió el "cómo llega" el tráfico — la causa raíz de /shipments/<id> (sin LocalStack corriendo en el clúster) no cambió, y no va a cambiar en ningún módulo de esta guía: la capa de datos queda fuera de su alcance $0. /health es la señal de red completa; /shipments/<id> se mantiene representativo hasta el capstone.


Ejercicios

Ejercicio 1 — Reconstruye el flujo completo de memoria. Sin volver a la lección, enumera los pasos, en orden, desde declarar ingress.yaml hasta confirmar /health con 200 sin port-forward.

Ver solución
  1. Declarar ingress.yaml con ingressClassName: nginx, host: andes-cargo.local, path: / (Prefix), backend status-api-service:80, y aplicarlo.
  2. Verificar con kubectl get ingress -n andes-cargo (el ADDRESS asignado) y kubectl describe ingress (confirmar los Backends reales).
  3. curl -i --resolve andes-cargo.local:80:127.0.0.1 http://andes-cargo.local/health, confirmando 200 OK sin ningún kubectl port-forward corriendo.

Ejercicio 2 — Explica el 404 de ingress-nginx a un colega sin usar la palabra "error". Un colega ve un 404 con <center>nginx</center> en el cuerpo y te pregunta qué significa. Explícaselo en dos o tres frases, enfocándote en qué revisó ingress-nginx antes de rechazar la solicitud.

Ver solución

Una explicación razonable: "Ese 404 no viene de nuestra aplicación — viene directo del recepcionista (ingress-nginx), que revisó la cabecera Host de tu solicitud y no encontró ninguna regla registrada para ese nombre. Es como llegar a la recepción de un edificio y preguntar por una empresa que no está en su directorio — te dicen que no hay nadie con ese nombre, sin necesidad de llamar a ningún piso."

Ejercicio 3 — Predice qué pasaría con un segundo Ingress, mismo host, path distinto. Si agregaras un segundo objeto Ingress con el mismo host: andes-cargo.local, pero path: /admin apuntando a un Service distinto (imaginario, no lo crees), ¿esperarías que ingress-nginx combine ambas reglas, o que la segunda reemplace a la primera? Justifica tu respuesta con lo que sabes del campo path.

Ver solución

Las combinaría — ingress-nginx puede tener múltiples reglas para el mismo host, cada una con un path distinto, y las evalúa todas: una solicitud a andes-cargo.local/health seguiría yendo a status-api-service (coincide con path: /), mientras que una solicitud a andes-cargo.local/admin iría al Service imaginario del segundo Ingress. Esto es, de hecho, exactamente el patrón que hace útil a Ingress frente a exponer cada Service por separado —la tabla de la lección 3 lo anticipó—: varias reglas de enrutamiento, todas detrás de la misma puerta de entrada.


Resumen y siguiente paso

En esta lección andes-cargo-status-api recibió, por primera vez en esta guía, tráfico real sin ningún túnel temporal: declaraste status-api-ingress, confirmaste que ingress-nginx resolvió los Backends reales, y le hiciste curl de verdad contra http://andes-cargo.local/health, con 200 OK de respuesta — el mismo cuerpo exacto que ya viste en el Módulo 2, ahora servido a través de una puerta HTTP permanente. /shipments/4471 siguió respondiendo 500, por la misma causa exacta que documentó el Módulo 3 (sin LocalStack en el clúster), y viste, de paso, cómo se ve un 404 generado por el propio ingress-nginx cuando la cabecera Host no coincide con ninguna regla.

Antes de avanzar deberías poder: explicar cada campo de ingress.yaml sin mirarlo; distinguir un 404 de ingress-nginx de un 404 real de Flask por el cuerpo de la respuesta; y confirmar curl real contra tu clúster, sin ningún kubectl port-forward corriendo.

Siguiente lección: NetworkPolicy, por defecto todos hablan con todos — y por qué eso no dura. La mitad de "tráfico entrante" de este módulo está resuelta. La lección 6 abre la segunda mitad: quién, dentro del clúster, puede hablarle a status-api-service — y por qué, hoy, la respuesta es "cualquiera".

Recursos

  1. Kubernetes — Ingress — la misma referencia de la lección 3, ahora confirmada con evidencia real, incluida la sintaxis completa de rules/pathType.
  2. curl — --resolve — referencia oficial de la bandera usada en el Paso 3, la alternativa sin privilegios a editar /etc/hosts.
  3. ingress-nginx — Custom errors — documentación oficial del comportamiento de "backend por defecto" del Paso 5, cuando ninguna regla coincide.
  4. kubernetes-and-eks-in-production-guide (NIEVA), Módulo 3, lección 4 — el traceback completo de NameResolutionError/EndpointConnectionError que esta lección confirma sin cambios.