Módulo 2: Pods Deployments And Services
7. Manos a la obra: exponiendo `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 a través de una dirección que no depende de ningún Pod específico. Vas a declarar status-api-service —el mismo nombre, literal, que aws-serverless-and-containers-guide dejó documentado en ECS y nunca corrió—, confirmar que encuentra tus dos Pods, y hacerle curl de verdad con kubectl port-forward. También vas a encontrarte, de forma honesta y documentada, con el límite real de este punto de la guía: /health funciona perfecto, pero /shipments/<id> todavía no tiene ningún dato al cual conectarse — y esta lección explica exactamente por qué, sin fingir un resultado que no existe todavía.
Conexión con el módulo
Esta lección confirma, con evidencia real, todo lo que la lección 6 explicó en teoría. También es la primera vez que ves, en producción real dentro de esta guía, la distinción entre "el Pod corre" y "el servicio está completo" — una distinción que se vuelve central en el Módulo 3, cuando ConfigMap/Secret le den a andes-cargo-status-api la configuración que le falta, y que se mantiene abierta durante el resto de esta guía: la capa de datos (DynamoDB, vía LocalStack) queda fuera de su alcance $0, así que /shipments/<id> sigue siendo un camino representativo, no ejecutado.
Paso 1 — El Service: service.yaml
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: status-api-service
namespace: andes-cargo
spec:
type: ClusterIP
selector:
app: andes-cargo-status-api
ports:
- port: 80
targetPort: 8080
protocol: TCP
Tres decisiones, cada una explicada a fondo en la lección 6, ahora aplicadas al caso real: type: ClusterIP (interno al clúster, el tipo que esta guía usa de forma consistente); selector: app: andes-cargo-status-api (exactamente la misma etiqueta que declaraste en deployment.yaml, lección 5 — sin esta coincidencia exacta, el Service no encontraría ningún Pod); y port: 80 con targetPort: 8080 (el Service escucha en el puerto HTTP estándar, mientras el contenedor real —heredado del Dockerfile de aws-serverless-and-containers-guide— sigue escuchando en 8080, sin ningún cambio).
kubectl apply -f service.yaml
Qué esperar:
service/status-api-service created
Paso 2 — Verifica: el Service y sus Endpoints
kubectl get service -n andes-cargo
Qué esperar (CLUSTER-IP es tu valor variable — asignada por Kubernetes de un rango interno cada vez que se crea el Service; el resto es literal):
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
status-api-service ClusterIP 10.96.78.1 <none> 80/TCP 0s
Ahora confirma la pieza que la lección 6 nombró pero no mostró todavía: la lista de Endpoints que Kubernetes mantiene automáticamente, con las IPs reales de los Pods que coinciden con el selector:
kubectl get endpoints -n andes-cargo
Qué esperar (las dos IPs listadas son tus valores variables — corresponden a los dos Pods que dejó la lección 5; el puerto 8080 es literal, coincide con targetPort):
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
NAME ENDPOINTS AGE
status-api-service 10.244.1.3:8080,10.244.2.4:8080 0s
La advertencia sobre v1 Endpoints es informativa, no un error: es Kubernetes avisando que el objeto Endpoints clásico está en proceso de reemplazo por EndpointSlice, una versión más escalable del mismo concepto — para lo que esta guía necesita, kubectl get endpoints sigue funcionando perfectamente y es más simple de leer de un vistazo. Dos IPs, dos puertos 8080 — exactamente los dos Pods que confirmaste en la lección 5. Esta es la prueba directa de que el selector del Service sí está encontrando a tus Pods; si esta lista saliera vacía, sabrías de inmediato que hay un problema de coincidencia de etiquetas, el error común que la lección 6 ya anticipó.
Paso 3 — port-forward: un túnel temporal hacia el Service
kubectl port-forward abre un túnel entre un puerto de tu máquina y un objeto dentro del clúster —en este caso, el Service, no un Pod específico—, sin necesidad de exponer nada permanentemente. Es la herramienta correcta para probar algo rápido durante desarrollo; el Módulo 4 va a reemplazar esta necesidad con un Ingress real, expuesto de forma permanente.
kubectl port-forward svc/status-api-service -n andes-cargo 8080:80
Qué esperar (literal — el comando queda corriendo en primer plano, sin devolver el control de la terminal; déjalo así y abre una terminal nueva para los siguientes pasos, o ejecútalo en segundo plano con & si prefieres una sola terminal):
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Fíjate en la sintaxis del comando: 8080:80 significa "puerto 8080 de mi máquina, hacia el puerto 80 del Service" — el mismo port que declaraste en service.yaml, no el targetPort del contenedor. kubectl port-forward habla con el Service, y es el Service quien decide, por debajo, a cuál de sus Pods reenviar la conexión.
Paso 4 — Verifica: /health responde, de verdad
Con el port-forward corriendo, abre una segunda terminal (o usa la bandera & para dejarlo en segundo plano) y confirma el primer endpoint:
curl -i -s http://localhost:8080/health
Qué esperar (literal, ejecutado — Date es tu valor variable):
HTTP/1.1 200 OK
Server: Werkzeug/3.1.8 Python/3.13.15
Date: Fri, 14 Aug 2026 19:34:54 GMT
Content-Type: application/json
Content-Length: 51
Connection: close
{"service":"andes-cargo-status-api","status":"ok"}
200 OK, el mismo cuerpo exacto que ya viste correr en docker run en aws-serverless-and-containers-guide, ahora sirviéndose desde dentro de un Pod de Kubernetes, a través de un Service, a través de un port-forward — cuatro capas de indirección, cero cambios en el resultado. Esto confirma algo importante: el Pod arrancó correctamente, el proceso Flask está sano, y el camino de red completo (tu máquina → kube-apiserver → kubelet → Pod) funciona de punta a punta.
Paso 5 — El límite honesto de este punto de la guía: /shipments/<id>
Aquí es donde esta lección se detiene a explicar algo con toda la honestidad que esta guía promete desde su diseño. Prueba el segundo endpoint, el que consulta la tabla Shipments:
curl -i -s http://localhost:8080/shipments/4471
Qué esperar (literal, ejecutado — este resultado no es un error de tu laboratorio, es el comportamiento correcto y esperado en este punto exacto de la guía):
HTTP/1.1 500 INTERNAL SERVER ERROR
Server: Werkzeug/3.1.8 Python/3.13.15
Date: Fri, 14 Aug 2026 19:34:54 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 265
Connection: close
<!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>
500, sin ningún detalle en la respuesta HTTP —el comportamiento estándar de Flask fuera de modo debug, exactamente como advirtió aws-serverless-and-containers-guide sobre el servidor de desarrollo—. Para entender la causa real, sin adivinar, revisa los logs del Pod que atendió la solicitud:
kubectl get pods -n andes-cargo -l app=andes-cargo-status-api
Qué esperar (nombres variables — usa el tuyo):
NAME READY STATUS RESTARTS AGE
andes-cargo-status-api-56856576d4-7ztk9 1/1 Running 0 81s
andes-cargo-status-api-56856576d4-vjc9k 1/1 Running 0 94s
kubectl logs andes-cargo-status-api-56856576d4-vjc9k -n andes-cargo --tail=40
Qué esperar (literal — el nombre exacto del Pod es tu valor variable, pero el traceback completo, si tu Pod es el que atendió la solicitud, es idéntico):
127.0.0.1 - - [14/Aug/2026 19:34:54] "GET /health HTTP/1.1" 200 -
[2026-08-14 19:34:54,789] ERROR in app: Exception on /shipments/4471 [GET]
Traceback (most recent call last):
File "/usr/local/lib/python3.13/site-packages/flask/app.py", line 1511, in wsgi_app
response = self.full_dispatch_request()
File "/usr/local/lib/python3.13/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
File "/app/app.py", line 27, in get_shipment
response = dynamodb.get_item(
TableName=TABLE_NAME,
Key={"shipmentId": {"S": shipment_id}},
)
File "/usr/local/lib/python3.13/site-packages/botocore/client.py", line 569, in _api_call
return self._make_api_call(operation_name, kwargs)
File "/usr/local/lib/python3.13/site-packages/botocore/auth.py", line 423, in add_auth
raise NoCredentialsError()
botocore.exceptions.NoCredentialsError: Unable to locate credentials
127.0.0.1 - - [14/Aug/2026 19:34:54] "GET /shipments/4471 HTTP/1.1" 500 -
La causa exacta, sin rodeos: app.py —el mismo código heredado, sin ningún cambio, de aws-serverless-and-containers-guide, Módulo 6— crea su cliente de boto3 leyendo DYNAMODB_ENDPOINT_URL, AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY de variables de entorno. El Deployment que declaraste en la lección 5 de este módulo no define ninguna de esas variables — a propósito, porque configurarlas todavía no era el tema de este módulo. Sin AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, boto3 ni siquiera intenta alcanzar la red: falla antes, al momento de firmar la solicitud, con NoCredentialsError. Y aunque las hubiera, no hay ningún LocalStack corriendo dentro de andes-cargo-cluster al cual esas credenciales pudieran conectarse, ni lo va a haber en ningún módulo posterior de esta guía — la capa de datos (DynamoDB) queda fuera del alcance $0 de este laboratorio de Kubernetes; /shipments/<id> se documenta como el camino representativo que un backing store real completaría, no como algo que esta guía vaya a instalar.
Esto no es un fallo de esta lección — es exactamente el punto en el que esta guía está hoy, y se mantiene así hasta el capstone. El Pod corre. La aplicación Flask arrancó sin errores (docker logs/kubectl logs lo confirman). El endpoint /health, que no depende de ningún dato externo, responde perfecto. Lo único que falta es la capa de datos: ni la configuración (ConfigMap/Secret, Módulo 3) ni un backend real de DynamoDB existen en este laboratorio — y no van a existir, porque instalar LocalStack (o conectar una cuenta AWS real) queda fuera del alcance $0 de esta guía de Kubernetes. Anota este resultado —NoCredentialsError, 500— porque el Módulo 3 lo va a resolver parcialmente (agregando la configuración, aunque siga sin haber ningún backend al cual conectarse), y porque esta misma condición se mantiene, documentada, hasta el Módulo 8.
QUÉ FUNCIONA HOY, Y QUÉ SE MANTIENE ASÍ (honestidad del M2)
curl /health ──▶ 200 OK (no depende de ningún dato externo)
curl /shipments/4471 ──▶ 500 (depende de DynamoDB, que no existe
en el clúster y queda fuera del
alcance $0 de esta guía)
Lo que sí llega, y cuándo:
- ConfigMap/Secret con el endpoint y credenciales ──▶ Módulo 3
Lo que se queda representativo (no ejecutado en esta guía):
- Backend real de DynamoDB / LocalStack en el clúster
Detén el port-forward
Antes de cerrar esta lección, detén el túnel con Ctrl+C en la terminal donde lo dejaste corriendo (o kill del proceso, si lo pusiste en segundo plano). El Service y los Pods siguen corriendo sin ningún cambio — port-forward es solo un túnel temporal desde tu máquina, no parte del estado del clúster.
Errores comunes
Interpretar el 500 de /shipments/<id> como un problema del laboratorio, y tratar de "arreglarlo" antes de tiempo (de expectativa, el error más importante de prevenir en esta lección específica). Qué pasa: alguien ve el 500 y empieza a buscar qué está mal en deployment.yaml o en el Service, sin darse cuenta de que el comportamiento es exactamente el esperado en este punto exacto de la guía. Por qué pasa: un código de error 5xx casi siempre significa "algo está roto y hay que arreglarlo" — un reflejo correcto en la mayoría de los contextos, pero no en este, donde esta misma lección lo documenta como esperado. Cómo detectarlo: si intentas agregar variables de entorno de AWS al Deployment de la lección 5 por tu cuenta, antes de llegar al Módulo 3. Cómo corregirlo: no hay nada que arreglar en este punto — la sección "El límite honesto de este punto de la guía" de esta lección explica la causa exacta (NoCredentialsError) y en qué módulos se resuelve cada parte del problema. Configurar la conexión completa ahora sería adelantarte a un tema que el Módulo 3 (ConfigMap/Secret) enseña con la profundidad que merece.
Confundir el puerto de port-forward (8080:80) con el targetPort del contenedor, y usar el número equivocado del lado izquierdo (de configuración). Qué pasa: alguien escribe kubectl port-forward svc/status-api-service -n andes-cargo 8080:8080, en vez de 8080:80, y recibe un error de que el puerto 8080 no existe en el Service. Por qué pasa: es fácil asumir que el número del lado derecho del port-forward debe coincidir con el puerto real del contenedor (8080), en vez del puerto en el que el Service escucha (80, el valor de port en service.yaml). Cómo detectarlo: el mensaje de error de kubectl port-forward menciona explícitamente que no pudo encontrar el puerto solicitado en el Service. Cómo corregirlo: recuerda la distinción de la lección 6 — kubectl port-forward svc/<nombre> <puerto-local>:<puerto-del-service> siempre usa el port del Service (el número de la izquierda de los dos puntos en service.yaml), nunca el targetPort del contenedor, del lado derecho de ese :80 en este caso.
Dejar el port-forward corriendo en segundo plano y olvidarlo, confundiendo puertos ocupados en lecciones futuras (de flujo). Qué pasa: alguien pone el port-forward de esta lección en segundo plano con &, sigue trabajando, y en una lección posterior de este módulo (o de un módulo futuro) intenta correr otro port-forward en el mismo puerto 8080, y recibe un error de "dirección ya en uso". Por qué pasa: kubectl port-forward no se detiene solo — sigue corriendo hasta que lo interrumpes explícitamente o cierras la terminal que lo originó. Cómo detectarlo: un error de bind: address already in use al intentar un port-forward nuevo. Cómo corregirlo: lsof -i :8080 (macOS/Linux) muestra qué proceso tiene ocupado ese puerto; termina ese proceso (kill <PID>) antes de abrir un port-forward nuevo, o simplemente recuerda cerrar cada port-forward con Ctrl+C en cuanto termines de usarlo, como recomienda el cierre de esta lección.
Ejercicios
Ejercicio 1 — Reconstruye el flujo completo de memoria. Sin volver a la lección, enumera los cuatro pasos, en orden, desde declarar service.yaml hasta confirmar que /health responde 200.
Ver solución
- Declarar
service.yamlcontype: ClusterIP,selector: app: andes-cargo-status-api,port: 80/targetPort: 8080, y aplicarlo conkubectl apply -f. - Verificar con
kubectl get service -n andes-cargo(la direcciónCLUSTER-IPasignada) ykubectl get endpoints -n andes-cargo(confirmar que elselectorsí encontró los Pods). - Abrir un túnel con
kubectl port-forward svc/status-api-service -n andes-cargo 8080:80. curl -i http://localhost:8080/health, confirmando200 OKcon el cuerpo JSON esperado.
Ejercicio 2 — Explica el NoCredentialsError a un colega sin usar la palabra "error". Un colega que no leyó esta lección te pregunta por qué /shipments/4471 no funciona todavía. Explícaselo en dos o tres frases, enfocándote en qué le falta al sistema en este punto exacto, no en que "algo esté roto".
Ver solución
Una explicación razonable: "El Pod está corriendo perfecto, y el endpoint /health lo confirma — el problema no es que algo esté roto, es que a este servicio le faltan dos cosas para consultar datos reales: las credenciales para hablarle a una base de datos (eso lo agregamos en el Módulo 3, con ConfigMap y Secret), y una base de datos real a la cual conectarse — y esa segunda pieza queda fuera del alcance de esta guía: instalar LocalStack o conectar una cuenta AWS real es trabajo de infraestructura de datos, no de Kubernetes, así que /shipments/<id> se documenta como representativo en toda la guía. El resto del sistema ya funciona."
Ejercicio 3 — Predice qué pasaría con las credenciales, pero sin LocalStack. Si el Módulo 3 le agregara a este Deployment las variables AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (con valores dummy, como test/test, el patrón que ya conoces de aws-serverless-and-containers-guide), pero sin ninguna instancia de LocalStack corriendo todavía en ningún lado, ¿esperarías que /shipments/4471 funcionara? Justifica tu respuesta con lo que aprendiste sobre el traceback de esta lección.
Ver solución
No funcionaría — cambiaría el tipo de error, pero seguiría fallando. Con credenciales presentes (aunque sean dummy), boto3 ya no fallaría en el paso de firmar la solicitud (NoCredentialsError desaparecería), pero como DYNAMODB_ENDPOINT_URL seguiría sin apuntar a ningún servicio real que responda, la solicitud fallaría en el siguiente paso: un error de conexión (tiempo de espera agotado, o conexión rechazada, dependiendo de a qué endpoint intentara conectarse por defecto sin esa variable configurada). El traceback de esta lección muestra que el fallo ocurre antes de intentar la conexión de red —en la firma de la solicitud—; agregar solo credenciales movería el punto de fallo un paso más adelante en la misma cadena, pero no lo resolvería, porque sigue faltando la pieza que ninguna configuración por sí sola puede resolver: un backend real de DynamoDB corriendo en algún lado. Esa pieza queda fuera del alcance $0 de esta guía de Kubernetes — el Módulo 3 resuelve la configuración, no el backend.
Resumen y siguiente paso
En esta lección andes-cargo-status-api recibió, por primera vez en esta guía, tráfico real a través de una dirección estable: declaraste status-api-service (ClusterIP), confirmaste que su selector encontró correctamente los dos Pods de la lección 5 (con kubectl get endpoints), y le hiciste curl de verdad con kubectl port-forward. /health respondió 200 OK, confirmando que todo el camino de red funciona de punta a punta. /shipments/4471 respondió 500, y en vez de esconder ese resultado, esta lección lo documentó con el traceback completo: falta la configuración de credenciales (que el Módulo 3 sí agrega) y falta, sobre todo, un backend real de DynamoDB — algo que ningún módulo de esta guía instala, porque la capa de datos queda fuera de su alcance $0. Ese es el comportamiento correcto, no un error tuyo, y se mantiene así hasta el capstone.
Antes de avanzar deberías poder: explicar la diferencia entre port y targetPort con el ejemplo real de esta lección; leer un traceback de Python dentro de kubectl logs para identificar la causa exacta de un 500; y describir, sin adivinar, exactamente qué le falta a andes-cargo-status-api para que /shipments/<id> funcione, y cuál de esas piezas esta guía sí resuelve (Módulo 3) y cuál queda fuera de su alcance por diseño.
Siguiente lección: proyecto de este módulo, andes-cargo-status-api con N réplicas. Ahí escalas el Deployment a tres réplicas, confirmas que las tres responden a través del mismo Service (el balanceo de carga que la lección 6 explicó en teoría), y repites el experimento de borrar un Pod a propósito — esta vez con el sistema completo de este módulo funcionando junto.
Recursos
- Kubernetes — Service — la misma referencia de la lección 6, ahora confirmada con evidencia real.
- Kubernetes — Use Port Forwarding to Access Applications in a Cluster — la guía oficial de
kubectl port-forward, el comando central de esta lección. - Boto3 — Credentials — referencia oficial de cómo
boto3exige credenciales antes de firmar cualquier solicitud, la causa raíz delNoCredentialsErrorde esta lección. aws-serverless-and-containers-guide(NIEVA), Módulo 6, lección 5 — el origen exacto deapp.py, con el mismo comportamiento de lectura de variables de entorno que esta lección confirma sin configurar todavía.