Module 1: Why Kubernetes And The Continuity Challenge
3. The continuity challenge: picking up `andes-cargo-status-api` where it left off
Description
Every new guide in an ecosystem with a continuous case faces the same question before writing a single manifest: which part of the existing case does it pick up, and what does it build from scratch? The easy answer — invent a new component, with a new name, to have "something of your own" — is, almost always, the wrong answer. This lesson explains, with the same honesty that's held up this whole ecosystem so far, why this guide does exactly the opposite: it picks up andes-cargo-status-api exactly as aws-serverless-and-containers-guide left it, without rewriting a single line, and gives it the one thing it was missing — an orchestrator that actually runs it.
Connection to the module
This lesson connects lesson 2's criteria (why Kubernetes) with the concrete case you're going to build throughout the rest of the guide. Everything you install in lessons 4, 5, and 7 of this module — kind, kubectl, the cluster, the loaded image — exists to resolve exactly the gap this lesson describes.
Honest recap: what happened in aws-serverless-and-containers-guide, Modules 6 and 7
Andes Cargo, up to that point in the ecosystem, had a single event-driven component: process-shipment-manifest, a Lambda function that processes manifests uploaded to S3. But Andes Cargo's logistics partners needed something different — a way to check a shipment's status at any moment, without waiting for an event to happen. That usage pattern — constant traffic, continuous availability — is exactly the case where an always-on container beats a function that only reacts (the full decision criteria, "taxi vs. owned car," you already installed in that guide).
Module 6 of that guide really built the physical piece:
- A five-instruction
Dockerfileon top ofpython:3.13-slim. - A two-endpoint Flask service:
GET /healthandGET /shipments/<shipment_id>, which queries theShipmentstable withboto3and returns a seven-field JSON contract. - The image built (
docker build), run locally (docker run) against that guide's real LocalStack, and confirmed withcurl— Andes Cargo's three real shipments (4471,4472,4473) responding with200and the full contract; a nonexistent shipment (9999) responding with404. - The image published to a local registry (
registry:2), withdocker tag/docker push/docker pullexecuted end to end.
Up to there, everything was executed, real, $0. Module 7 of that guide went one step further — but that's where the path breaks off:
- The
andes-cargo-status-api-sgsecurity group (Amazon EC2) really was created, because EC2 is on LocalStack's Hobby plan. - The
andes-cargo-clusterECS cluster, theandes-cargo-status-apitask definition, and thestatus-api-serviceservice withdesiredCount: 2— never actually ran. Amazon ECS is marked "Included in Plans: Base, Ultimate" in LocalStack's documentation, and the free Hobby plan doesn't include it at all. All the syntax and output for those three resources are verified against official AWS CLI documentation — real fields, not invented — but no command ever ran against a real cluster.
WHAT aws-serverless-and-containers-guide LEFT, EXACTLY
┌─────────────────────────────────────────────────────────────────┐
│ andes-cargo-status-api:1.0 (image — EXECUTED, real) │
│ localhost:5000/andes-cargo-status-api:1.0 (registry — EXECUTED) │
└──────────────────────────────┬──────────────────────────────────┘
│
│ who runs it continuously?
▼
┌─────────────────────────────────────────────────────────────────┐
│ andes-cargo-cluster (ECS) ── REPRESENTATIVE, never ran │
│ status-api-service (ECS) ── (LocalStack Hobby doesn't │
│ cover ECS) │
│ EcsTaskExecutionRole / StatusApiTaskRole ── created in JSON, never │
│ applied │
└─────────────────────────────────────────────────────────────────┘
The honest proof that guide could give — running the same container with docker run, on a Docker network mimicking the problem awsvpc solves — confirmed that the image works. It could never confirm that the orchestrator works, because the orchestrator never came to exist.
The three options evaluated for this guide (and why two were discarded)
Before deciding "reuse as-is," two alternatives were considered — it's worth understanding why neither one won, because the reason reveals something important about designing honest continuity in a case study:
Discarded option 1 — Containerize process-shipment-manifest as a long-running HTTP service. This would break the use case: that function is event-driven by design (it reacts to a manifest uploaded to S3), not a service that handles constant traffic. Turning it into long-running HTTP without the business volume to justify it would be exactly the architecture decision aws-serverless-and-containers-guide taught you not to make. And, more importantly: a component that's already long-running by design, and already in Docker, already exists — inventing a second containerization when the first one is waiting for an orchestrator would be redundant work, not continuity.
Discarded option 2 — Introduce a new component (shipment-tracking-api, for example). This would, literally, be rebuilding andes-cargo-status-api under another name: same purpose (serving Shipments status), same access pattern (read by shipmentId), same continuous-availability use case. A new component with the same role as one that already exists isn't honest continuity of the case — it's a new case disguised as continuity.
The path this guide takes — pick up andes-cargo-status-api as-is. The Dockerfile isn't rewritten (you confirm this yourself in lesson 7). The cluster name (andes-cargo-cluster) and the service name (status-api-service) are reused literally — it's not a coincidence, it's this guide's explicit narrative thread: the cluster that only ever existed as shown YAML now truly exists, under the same name. The StatusApiTaskRole role — created in JSON, never applied in the previous guide because ECS wasn't executable — is picked back up in Module 7 of this guide as the role that, on real EKS, a Kubernetes ServiceAccount would assume via IRSA, closing in representative YAML the circle the previous guide left open in representative JSON.
An open thread in the ecosystem, which this guide notes
Two sibling guides — cloud-security-and-guardrails-guide and finops-and-cost-guardrails-guide, both designed after aws-serverless-and-containers-guide — state in their scope-boundary section something that's no longer entirely accurate: "Andes Cargo has no containers in this ecosystem." That statement was correct at the moment those designs were written, but it stopped being true as of Module 6 of aws-serverless-and-containers-guide: the container exists, it just has no executed orchestrator. The inaccuracy doesn't invalidate either guide — their scope boundaries remain correct: the supply-chain security in cloud-security-and-guardrails-guide is about process-shipment-manifest's .zip, not an image; the rightsizing in finops-and-cost-guardrails-guide is about Lambda/DynamoDB/S3, not Pods — but the literal premise should read "Andes Cargo has a containerized component, with no executed orchestrator until kubernetes-and-eks-in-production-guide." This guide leaves that note here, in its first module, and closes it formally in its capstone (Module 8) — not because this guide has the authority to edit another guide's design, but because it's the guide that, by running the real orchestrator, makes the inaccuracy visible for the first time.
What does change in this guide: networking and configuration, not the application
An important clarification before moving on: "picking up the image as-is" doesn't mean absolutely nothing changes around it. What is new, always in English like the rest of this guide's Kubernetes identifiers:
andes-cargo-k8s/— a new Git repository (served by Gitea, inside the cluster itself starting in Module 5), the source of truth ArgoCD is going to sync. It's going to hold, module by module, each manifest you build:namespace.yaml,deployment.yaml,service.yaml,configmap.yaml/secret.yaml,hpa.yaml,ingress.yaml,networkpolicy.yaml,application.yaml.- The endpoint
andes-cargo-status-apiwould read to talk to DynamoDB changes shape — fromhost.docker.internal:4566(loose Docker, previous guide) to Kubernetes' internal DNS pattern,localstack.localstack.svc.cluster.local:4566(Module 3'sConfigMapsets that value) — but the application code doesn't change a single line: it still readsDYNAMODB_ENDPOINT_URLfrom an environment variable, exactly as Module 6 ofaws-serverless-and-containers-guidealready designed it. No module in this guide installs LocalStack inside the cluster: the data layer stays outside its $0 scope, so thatServicenever actually comes to exist here —/healthis the signal that validates the end-to-end network path;/shipments/<id>stays representative. - The dummy
test/testcredentials keep being used, now injected via a KubernetesSecret(Module 3) instead ofdocker runflags.
What does not get touched: process-shipment-manifest stays Lambda, .zip, event-driven; the andes-cargo-shipment-docs bucket, the Shipments table, and the IAM roles from previous guides aren't rewritten — this guide reads Shipments from andes-cargo-status-api, never modifies it.
Analogy: the inherited house, never connected to real power
Think of andes-cargo-status-api as a house fully built by the previous contractor: walls standing, plumbing installed, every room with its purpose defined — but never connected to the real power grid, because the contractor only had access to a limited demo generator (LocalStack's Hobby plan, no ECS). This guide doesn't tear down the house to build another one. It also doesn't build a new house next door, under another name, to have "something of its own" — that would waste the work already done, and confuse anyone trying to understand the complete case. This guide connects the real power: the same internal wiring (the Dockerfile, untouched), the same address (the name andes-cargo-cluster, reused), now with real electricity running through the wires.
Common mistakes
Trying to "improve" the Dockerfile while rebuilding it in lesson 7 (discipline, the most tempting one for anyone who already knows Docker). What happens: someone, arriving at lesson 7 and seeing the five-instruction Dockerfile, decides to add a multi-stage build, change the base image, or any other technically reasonable-in-the-abstract "improvement." Why it happens: it's a natural reflex for anyone with Docker experience — seeing a simple image and wanting to optimize it. How to spot it: if your lesson 7 Dockerfile has even one line different from the original in aws-serverless-and-containers-guide, Module 6, lesson 5. How to fix it: this guide's hard rule is explicit — the Dockerfile is inherited without rewriting, unless a specific module explicitly asks for it (never happens in this guide). Any real image optimization is docker-essentials-guide content, not this guide's.
Thinking andes-cargo-cluster (Kubernetes) and andes-cargo-cluster (ECS, previous guide) are "the same resource" in some technical sense (conceptual). What happens: someone assumes there's some real technical relationship — a migration, an import — between the never-executed ECS cluster and the Kubernetes cluster you're going to create in lesson 5. Why it happens: they share the literal name, and it's easy to read that as technical continuity instead of narrative continuity. How to spot it: if you look for some "migration" command from ECS to kind in this guide. How to fix it: no such migration exists because there's nothing to migrate — the ECS cluster never ran. The name is reused on purpose, as a narrative thread of the case, not because there's something technical to carry over from one system to the other.
Skipping this lesson's recap because "I already remember the previous guide" (workflow). What happens: someone who did aws-serverless-and-containers-guide recently assumes they don't need to reread the seven-field JSON contract or the exact resource names, and arrives at lesson 7 without a clear idea of exactly what they're going to rebuild. Why it happens: "I already did it" feels sufficient, but the exact details — field names, the alphabetical order of Flask's keys, the 404 contract — are easy to forget in the fine details. How to spot it: if you can't name, from memory, the seven fields of the /shipments/<id> JSON contract. How to fix it: review the contract table further down in this lesson — it takes ten seconds and prevents confusion later, when Module 2 exposes this same contract behind a Kubernetes Service.
The contract that doesn't change: andes-cargo-status-api's seven fields
You're going to see this exact response again, with no change whatsoever, when you expose the service behind a Kubernetes Service in Module 2 — the same contract aws-serverless-and-containers-guide, Module 6, already established:
| Field | Type | Example (shipment 4472) |
|---|---|---|
shipmentId | string | "4472" |
status | string | "MANIFEST_PROCESSED" |
originCountry | string | "Colombia" |
destinationCountry | string | "Ecuador" |
carrier | string | "AndesExpress" |
weightKg | number | 85 |
processedAt | string (ISO 8601) | "2026-08-12T19:00:00Z" |
And the "not found" case: 404, with body {"error": "shipment not found", "shipmentId": "<id>"} — note that this error shape does not match get-shipment-status's (the placeholder Lambda from aws-serverless-and-containers-guide, Module 5, which uses {"message": "..."}), a continuity detail that same guide already documented as real friction, not an accident.
Exercises
Exercise 1 — Reconstruct the two discarded options. Without looking back at the corresponding section, name the two alternatives evaluated before deciding to "reuse andes-cargo-status-api as-is," and the concrete reason each one was discarded.
See solution
- Containerizing
process-shipment-manifestas a long-running HTTP service — discarded because it would break its event-driven-by-design use case, and because a different component that's already long-running and already in Docker exists. - Introducing a new component (for example,
shipment-tracking-api) — discarded because it would, in practice, be rebuildingandes-cargo-status-apiunder another name: same purpose, same access pattern, same use case.
Exercise 2 — Explain the ecosystem's open thread to a colleague. A colleague asks: "why do two sibling guides say Andes Cargo has no containers, when it clearly has one?" Answer them in two or three sentences, without criticizing those guides' design.
See solution
A complete answer sounds roughly like this: "Those two guides were designed after aws-serverless-and-containers-guide built the status-api container, but before any guide gave it a real orchestrator — so, at the moment they were written, the statement reflected the case's operational reality, even though the container technically already existed. It doesn't invalidate either guide's scope: one's supply-chain security is still about the Lambda .zip, not an image. It's a continuity inaccuracy, not a design mistake, and this guide leaves it noted here so the full ecosystem can correct it during an end-to-end audit."
Exercise 3 — Predict what will NOT change in Module 2. With this lesson's seven-field contract in front of you, predict: when you expose andes-cargo-status-api behind a status-api-service Service in Module 2, will curl against /shipments/4472 return a different JSON than this lesson's? Justify your answer.
See solution
No, it's going to return exactly the same JSON, with the same seven fields, in the same alphabetical order Flask uses by default (carrier, destinationCountry, originCountry, processedAt, shipmentId, status, weightKg). The underlying reason is this guide's hard rule: the application code (app.py) isn't touched in any module unless explicitly requested — the only thing that changes between Module 1 (loose docker run, previous guide) and Module 2 of this guide (a real Pod behind a Service) is who orchestrates the container, not what the container responds.
Summary and next step
In this lesson you did an honest recap of where andes-cargo-status-api was left: image built, run locally, published — and an orchestrator (ECS) that stayed complete only in documentation, because LocalStack's free plan doesn't cover that service. You saw the two alternatives discarded before deciding the path this guide takes — reusing the component as-is, with nothing new invented — and why that decision is precisely what makes the case's continuity honest. You confirmed the seven-field contract that won't change in any module of this guide, and saw the thread two sibling guides left open about "Andes Cargo has no containers," which this guide notes here and closes formally in its capstone.
Before moving on you should be able to: explain why the two discarded options weren't honest continuity; name the seven fields of the /shipments/<id> contract from memory; and describe what IS new in this guide (the andes-cargo-k8s/ repository, the LocalStack endpoint via internal DNS) versus what isn't touched.
With the criteria and the continuity challenge resolved, lessons 4 and 5 install the real lab: kind, kubectl, and your first cluster actually running, under the name you already know.
Resources
aws-serverless-and-containers-guide(NIEVA), Module 6, lesson 5 — the original build of the image and the exact Dockerfile this guide inherits.aws-serverless-and-containers-guide(NIEVA), Module 7, lessons 4 and 7 — the task definition and the representative deployment attempt on ECS.cloud-security-and-guardrails-guide(NIEVA),DISENO.md— the boundary premise this lesson notes as inaccurate.finops-and-cost-guardrails-guide(NIEVA),DISENO.md— the same premise, repeated in a different guide.- Flask — Quickstart — official reference for the framework behind
app.py, unchanged in this guide.