Módulo 1: Why Kubernetes And The Continuity Challenge

3. El reto de continuidad: recogiendo `andes-cargo-status-api` donde quedó

Descripción

Toda guía nueva de un ecosistema con un caso continuo enfrenta la misma pregunta antes de escribir un solo manifiesto: ¿qué parte del caso existente retoma, y qué construye desde cero? La respuesta fácil —inventar un componente nuevo, con nombre nuevo, para tener "algo propio"— es, casi siempre, la respuesta equivocada. Esta lección explica, con la misma honestidad que sostuvo todo el ecosistema hasta ahora, por qué esta guía hace exactamente lo contrario: recoge andes-cargo-status-api tal cual aws-serverless-and-containers-guide lo dejó, sin reescribir una sola línea, y le da lo único que le faltaba — un orquestador que de verdad lo corra.

Conexión con el módulo

Esta lección conecta el criterio de la lección 2 (por qué Kubernetes) con el caso concreto que vas a construir en el resto de la guía. Todo lo que instalas en las lecciones 4, 5 y 7 de este módulo —kind, kubectl, el clúster, la imagen cargada— existe para resolver exactamente el vacío que esta lección describe.


Recap honesto: qué pasó en aws-serverless-and-containers-guide, Módulos 6 y 7

Andes Cargo, hasta ese punto del ecosistema, tenía un solo componente que respondía a eventos: process-shipment-manifest, una función Lambda que procesa manifiestos subidos a S3. Pero los socios logísticos de Andes Cargo necesitaban algo distinto — una forma de consultar el estado de un envío en cualquier momento, sin esperar a que ocurra un evento. Ese patrón de uso —tráfico constante, disponibilidad continua— es exactamente el caso donde un contenedor siempre activo le gana a una función que solo reacciona (el criterio completo de decisión, "taxi vs. auto propio", que ya instalaste en esa guía).

El Módulo 6 de esa guía construyó, de verdad, la pieza física:

  • Un Dockerfile de cinco instrucciones sobre python:3.13-slim.
  • Un servicio Flask de dos endpoints: GET /health y GET /shipments/<shipment_id>, que consulta la tabla Shipments con boto3 y devuelve un contrato JSON de siete campos.
  • La imagen construida (docker build), corrida local (docker run) contra el LocalStack real de esa guía, y confirmada con curl — los tres envíos reales de Andes Cargo (4471, 4472, 4473) respondiendo con 200 y el contrato completo; un envío inexistente (9999) respondiendo con 404.
  • La imagen publicada en un registro local (registry:2), con docker tag/docker push/docker pull ejecutados de punta a punta.

Hasta ahí, todo ejecutado, real, $0. El Módulo 7 de esa guía fue un paso más allá — pero ahí es donde el camino se corta:

  • El security group andes-cargo-status-api-sg (Amazon EC2) sí se creó de verdad, porque EC2 está en el plan Hobby de LocalStack.
  • El cluster ECS andes-cargo-cluster, la task definition andes-cargo-status-api, y el service status-api-service con desiredCount: 2nunca se ejecutaron. Amazon ECS está marcado "Included in Plans: Base, Ultimate" en la documentación de LocalStack, y el plan Hobby gratuito no lo incluye en absoluto. Toda la sintaxis y la salida de esos tres recursos están verificadas contra la documentación oficial de AWS CLI —campos reales, no inventados—, pero ningún comando corrió contra un clúster real.
              LO QUE aws-serverless-and-containers-guide DEJÓ, EXACTAMENTE

  ┌─────────────────────────────────────────────────────────────────┐
  │  andes-cargo-status-api:1.0        (imagen — EJECUTADA, real)       │
  │  localhost:5000/andes-cargo-status-api:1.0  (registro — EJECUTADO)   │
  └──────────────────────────────┬──────────────────────────────────┘
                                    │
                                    │  ¿quién la corre de forma continua?
                                    ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  andes-cargo-cluster (ECS)          ── REPRESENTATIVO, nunca corrió   │
  │  status-api-service (ECS)           ── (LocalStack Hobby no cubre ECS)│
  │  EcsTaskExecutionRole / StatusApiTaskRole ── creados en JSON, nunca   │
  │                                          aplicados                    │
  └─────────────────────────────────────────────────────────────────┘

La prueba honesta que esa guía sí pudo dar —correr el mismo contenedor con docker run, en una red de Docker que imita el problema que awsvpc resuelve— confirmó que la imagen funciona. Nunca pudo confirmar que el orquestador funciona, porque el orquestador nunca llegó a existir.


Las tres opciones que se evaluaron para esta guía (y por qué se descartaron dos)

Antes de decidir "reutilizar tal cual", se consideraron dos alternativas — vale la pena entender por qué ninguna de las dos ganó, porque la razón revela algo importante sobre cómo diseñar continuidad honesta en un caso de estudio:

Opción descartada 1 — Contenerizar process-shipment-manifest como un servicio HTTP de larga duración. Rompería el caso de uso: esa función es event-driven por diseño (reacciona a un manifiesto subido a S3), no un servicio que atiende tráfico constante. Convertirla en HTTP de larga duración sin que el volumen de negocio lo justifique sería exactamente la decisión de arquitectura que aws-serverless-and-containers-guide enseñó a no tomar. Y, más importante todavía: ya existe un componente que sí es de larga duración por diseño y que ya está en Docker — inventar una segunda contenerización cuando la primera está esperando un orquestador sería trabajo redundante, no continuidad.

Opción descartada 2 — Introducir un componente nuevo (shipment-tracking-api, por ejemplo). Sería, literalmente, reconstruir andes-cargo-status-api con otro nombre: mismo propósito (servir el estado de Shipments), mismo patrón de acceso (lectura por shipmentId), mismo caso de uso de disponibilidad continua. Un componente nuevo con el mismo rol que uno que ya existe no es continuidad honesta del caso — es un caso nuevo disfrazado de continuidad.

La vía que usa esta guía — recoger andes-cargo-status-api tal cual. El Dockerfile no se reescribe (lo confirmas tú mismo en la lección 7). El nombre del clúster (andes-cargo-cluster) y el nombre del servicio (status-api-service) se reutilizan literalmente — no es casualidad, es el hilo narrativo explícito de esta guía: el clúster que solo existió en YAML mostrado ahora existe de verdad, con el mismo nombre. El rol StatusApiTaskRole —creado en JSON, nunca aplicado en la guía anterior porque ECS no era ejecutable— se retoma en el Módulo 7 de esta guía como el rol que, en un EKS real, un ServiceAccount de Kubernetes asumiría vía IRSA, cerrando en YAML representativo el círculo que la guía anterior dejó abierto en JSON representativo.


Un hilo abierto en el ecosistema, que esta guía deja anotado

Dos guías hermanas —cloud-security-and-guardrails-guide y finops-and-cost-guardrails-guide, ambas diseñadas después de aws-serverless-and-containers-guide— afirman en su sección de frontera algo que ya no es del todo preciso: "Andes Cargo no tiene contenedores en este ecosistema". Esa frase fue correcta en el momento en que se escribió esos diseños, pero dejó de serlo desde el Módulo 6 de aws-serverless-and-containers-guide: el contenedor existe, solo que sin orquestador ejecutado. La imprecisión no invalida ninguna de las dos guías —sus fronteras de alcance siguen siendo correctas: la seguridad de cadena de suministro de cloud-security-and-guardrails-guide es del .zip de process-shipment-manifest, no de una imagen; el rightsizing de finops-and-cost-guardrails-guide es de Lambda/DynamoDB/S3, no de Pods— pero la premisa literal debería decir "Andes Cargo tiene un componente en contenedor, sin orquestador ejecutado hasta kubernetes-and-eks-in-production-guide". Esta guía deja esa nota aquí, en su primer módulo, y la cierra formalmente en su capstone (Módulo 8) — no porque esta guía tenga autoridad para editar el diseño de otra, sino porque es la guía que, al ejecutar el orquestador real, hace que la imprecisión sea visible por primera vez.


Lo que sí cambia en esta guía: red y configuración, no la aplicación

Una aclaración importante antes de seguir: "recoger la imagen tal cual" no significa que absolutamente nada cambie alrededor de ella. Lo que sí es nuevo, siempre en inglés como el resto de identificadores de Kubernetes de esta guía:

  • andes-cargo-k8s/ — un repositorio Git nuevo (servido por Gitea, dentro del propio clúster desde el Módulo 5), la fuente de verdad que ArgoCD va a sincronizar. Va a contener, módulo a módulo, cada manifiesto que construyes: namespace.yaml, deployment.yaml, service.yaml, configmap.yaml/secret.yaml, hpa.yaml, ingress.yaml, networkpolicy.yaml, application.yaml.
  • El endpoint que andes-cargo-status-api leería para hablarle a DynamoDB cambia de forma —de host.docker.internal:4566 (Docker suelto, guía anterior) al patrón DNS interno de Kubernetes, localstack.localstack.svc.cluster.local:4566 (el ConfigMap del Módulo 3 configura ese valor)— pero el código de la aplicación no cambia ni una línea: sigue leyendo DYNAMODB_ENDPOINT_URL de una variable de entorno, exactamente como ya lo diseñó el Módulo 6 de aws-serverless-and-containers-guide. Ningún módulo de esta guía instala LocalStack dentro del clúster: la capa de datos queda fuera de su alcance $0, así que ese Service nunca llega a existir aquí — /health es la señal que valida el camino de red de punta a punta; /shipments/<id> queda representativo.
  • Las credenciales dummy test/test se siguen usando, ahora inyectadas vía un Secret de Kubernetes (Módulo 3) en vez de flags de docker run.

Lo que no se toca: process-shipment-manifest sigue siendo Lambda, .zip, event-driven; el bucket andes-cargo-shipment-docs, la tabla Shipments, y los roles IAM de las guías anteriores no se reescriben — esta guía lee Shipments desde andes-cargo-status-api, nunca la modifica.


Analogía: la casa heredada, nunca conectada a la corriente real

Piensa en andes-cargo-status-api como una casa completamente construida por el contratista anterior: paredes en pie, plomería instalada, cada habitación con su propósito definido — pero nunca conectada a la red eléctrica de verdad, porque el contratista solo tenía acceso a un generador de demostración limitado (el plan Hobby de LocalStack, sin ECS). Esta guía no derriba la casa para construir otra. Tampoco construye una casa nueva al lado, con otro nombre, para tener "algo propio" — eso sería desperdiciar el trabajo ya hecho, y confundir a cualquiera que intente entender el caso completo. Esta guía conecta la corriente real: el mismo cableado interno (el Dockerfile, sin tocar), la misma dirección (el nombre andes-cargo-cluster, reutilizado), ahora con electricidad de verdad corriendo por los cables.


Errores comunes

Intentar "mejorar" el Dockerfile al reconstruirlo en la lección 7 (de disciplina, el más tentador para quien ya sabe Docker). Qué pasa: alguien, al llegar a la lección 7 y ver el Dockerfile de cinco instrucciones, decide agregar una etapa multi-stage, cambiar la imagen base, o cualquier otra "mejora" técnica razonable en abstracto. Por qué pasa: es un reflejo natural para cualquiera con experiencia en Docker — ver una imagen simple y querer optimizarla. Cómo detectarlo: si tu Dockerfile de la lección 7 tiene una sola línea distinta al original de aws-serverless-and-containers-guide, Módulo 6, lección 5. Cómo corregirlo: la regla dura de esta guía es explícita — el Dockerfile se hereda sin reescribir, salvo que un módulo específico lo pida de forma explícita (nunca ocurre en esta guía). Cualquier optimización real de imágenes es contenido de docker-essentials-guide, no de esta.

Pensar que andes-cargo-cluster (Kubernetes) y andes-cargo-cluster (ECS, guía anterior) son "el mismo recurso" en algún sentido técnico (conceptual). Qué pasa: alguien asume que existe alguna relación técnica real —una migración, una importación— entre el clúster ECS nunca ejecutado y el clúster Kubernetes que vas a crear en la lección 5. Por qué pasa: comparten nombre literal, y es fácil leer eso como continuidad técnica en vez de continuidad narrativa. Cómo detectarlo: si buscas algún comando de "migración" de ECS a kind en esta guía. Cómo corregirlo: no existe tal migración porque no existe tal cosa que migrar — el clúster ECS nunca corrió. El nombre se reutiliza a propósito, como un hilo narrativo del caso, no porque haya algo técnico que trasladar de un sistema al otro.

Saltarse el recap de esta lección porque "ya me acuerdo de la guía anterior" (de flujo). Qué pasa: alguien que hizo aws-serverless-and-containers-guide hace poco asume que no necesita releer el contrato JSON de siete campos ni los nombres exactos de los recursos, y llega a la lección 7 sin tener claro qué exactamente va a reconstruir. Por qué pasa: "ya lo hice" se siente suficiente, pero los detalles exactos —nombres de campo, orden alfabético de las claves de Flask, el contrato del 404— son fáciles de olvidar en los detalles finos. Cómo detectarlo: si no puedes nombrar, de memoria, los siete campos del contrato JSON de /shipments/<id>. Cómo corregirlo: repasa la tabla del contrato más abajo en esta lección — son diez segundos que evitan confusión más adelante, cuando el Módulo 2 exponga este mismo contrato detrás de un Service de Kubernetes.


El contrato que no cambia: los siete campos de andes-cargo-status-api

Vas a volver a ver esta respuesta exacta, sin ningún cambio, cuando expongas el servicio detrás de un Service de Kubernetes en el Módulo 2 — el mismo contrato que aws-serverless-and-containers-guide, Módulo 6, ya estableció:

CampoTipoEjemplo (envío 4472)
shipmentIdstring"4472"
statusstring"MANIFEST_PROCESSED"
originCountrystring"Colombia"
destinationCountrystring"Ecuador"
carrierstring"AndesExpress"
weightKgnumber85
processedAtstring (ISO 8601)"2026-08-12T19:00:00Z"

Y el caso "no encontrado": 404, con cuerpo {"error": "shipment not found", "shipmentId": "<id>"} — nota que esta forma de error no coincide con la de get-shipment-status (la Lambda placeholder de aws-serverless-and-containers-guide, Módulo 5, que usa {"message": "..."}), un detalle de continuidad que esa misma guía ya documentó como una fricción real, no accidental.


Ejercicios

Ejercicio 1 — Reconstruye las dos opciones descartadas. Sin volver a leer la sección correspondiente, nombra las dos alternativas que se evaluaron antes de decidir "reutilizar andes-cargo-status-api tal cual", y la razón concreta por la que cada una se descartó.

Ver solución
  1. Contenerizar process-shipment-manifest como servicio HTTP de larga duración — descartada porque rompería su caso de uso event-driven por diseño, y porque ya existe un componente distinto que sí es de larga duración y ya está en Docker.
  2. Introducir un componente nuevo (por ejemplo, shipment-tracking-api) — descartada porque sería, en la práctica, reconstruir andes-cargo-status-api con otro nombre: mismo propósito, mismo patrón de acceso, mismo caso de uso.

Ejercicio 2 — Explica el hilo abierto del ecosistema a un colega. Un colega pregunta: "¿por qué dos guías hermanas dicen que Andes Cargo no tiene contenedores, si claramente sí tiene uno?". Respóndele en dos o tres frases, sin criticar el diseño de esas guías.

Ver solución

Una respuesta completa suena, más o menos, así: "Esas dos guías se diseñaron después de que aws-serverless-and-containers-guide construyera el contenedor de status-api, pero antes de que ninguna guía le diera un orquestador real — así que, en el momento en que se escribieron, la afirmación reflejaba la realidad operativa del caso, aunque técnicamente el contenedor ya existiera. No invalida el alcance de esas guías: la seguridad de cadena de suministro de una sigue siendo del .zip de Lambda, no de una imagen. Es una imprecisión de continuidad, no un error de diseño, y esta guía la deja anotada para que el ecosistema completo la corrija cuando se audite de punta a punta."

Ejercicio 3 — Predice qué NO va a cambiar en el Módulo 2. Con el contrato de siete campos de esta lección delante, predice: cuando expongas andes-cargo-status-api detrás de un Service status-api-service en el Módulo 2, ¿el curl contra /shipments/4472 va a devolver un JSON distinto al de esta lección? Justifica tu respuesta.

Ver solución

No, va a devolver exactamente el mismo JSON, con los mismos siete campos, en el mismo orden alfabético que Flask usa por defecto (carrier, destinationCountry, originCountry, processedAt, shipmentId, status, weightKg). La razón de fondo es la regla dura de esta guía: el código de la aplicación (app.py) no se toca en ningún módulo salvo que se pida explícitamente — lo único que cambia entre el Módulo 1 (docker run suelto, guía anterior) y el Módulo 2 de esta guía (un Pod real detrás de un Service) es quién orquesta el contenedor, no lo que el contenedor responde.


Resumen y siguiente paso

En esta lección hiciste un recap honesto de dónde quedó andes-cargo-status-api: imagen construida, corrida local, publicada — y un orquestador (ECS) que se quedó completo solo en documentación, porque el plan gratuito de LocalStack no cubre ese servicio. Viste las dos alternativas que se descartaron antes de decidir la vía que esta guía sigue —reutilizar el componente tal cual, sin inventar nada nuevo— y por qué esa decisión es, precisamente, la que hace honesta la continuidad del caso. Confirmaste el contrato de siete campos que no va a cambiar en ningún módulo de esta guía, y viste el hilo que dos guías hermanas dejaron abierto sobre "Andes Cargo no tiene contenedores", que esta guía anota aquí y cierra formalmente en su capstone.

Antes de avanzar deberías poder: explicar por qué las dos opciones descartadas no eran continuidad honesta; nombrar los siete campos del contrato de /shipments/<id> de memoria; y describir qué SÍ es nuevo en esta guía (el repositorio andes-cargo-k8s/, el endpoint de LocalStack vía DNS interno) frente a lo que no se toca.

Con el criterio y el reto de continuidad resueltos, las lecciones 4 y 5 instalan el laboratorio real: kind, kubectl, y tu primer clúster corriendo de verdad, con el nombre que ya conoces.

Recursos

  1. aws-serverless-and-containers-guide (NIEVA), Módulo 6, lección 5 — la construcción original de la imagen y el Dockerfile exacto que esta guía hereda.
  2. aws-serverless-and-containers-guide (NIEVA), Módulo 7, lecciones 4 y 7 — la task definition y el intento de despliegue representativo en ECS.
  3. cloud-security-and-guardrails-guide (NIEVA), DISENO.md — la premisa de frontera que esta lección anota como imprecisa.
  4. finops-and-cost-guardrails-guide (NIEVA), DISENO.md — la misma premisa, repetida en una guía distinta.
  5. Flask — Quickstart — referencia oficial del framework detrás de app.py, sin cambios en esta guía.