Skip to content

feat(infra): a stable cross-cluster address for the Core and its three surfaces - #417

Merged
beyondnetPeru merged 2 commits into
developfrom
feat/kind-cross-cluster-core-address
Aug 4, 2026
Merged

feat(infra): a stable cross-cluster address for the Core and its three surfaces#417
beyondnetPeru merged 2 commits into
developfrom
feat/kind-cross-cluster-core-address

Conversation

@beyondnetPeru

Copy link
Copy Markdown
Contributor

Core and Tracker are separate products in separate repositories that talk over HTTP. Validating them in one cluster would hand them in-cluster DNS and hide the defects that only appear across a network boundary — a malformed base URL, a missing header, a timeout, CORS. Two clusters is the honest topology and already the de-facto one: the Tracker's manifest deploys into its own evolith-tracker cluster.

What was missing was the link. Everything was reached with kubectl port-forward, which is right inside one cluster and wrong between two: it is a host process nothing in another cluster can point at, and it dies on every rollout of the target — so redeploying the Core breaks the Tracker and the symptom looks like a Tracker defect.

Two addresses, not interchangeable

Consumer Address Path
Another cluster (the Tracker) http://<cluster>-control-plane:3008x Shared kind Docker network → NodePort. Does not use the host mapping
This machine (browser, curl, CLI) http://localhost:3008x extraPortMappings, bound to 127.0.0.1

Because the host mapping is on loopback it cannot serve the cross-cluster case — which is exactly why both exist.

host.docker.internal does not work and was the first thing written here: inside a kind pod it fails with Could not resolve host, because the pod resolves through CoreDNS in the node and never sees the Docker Desktop entry. The comments say so, since the wrong URL is the plausible one.

All three surfaces, not just the Core

core-api 30080 · mcp 30081 · agent-runtime 30082. A consumer that reaches one of three has not exercised the product, and the docs would have implied a parity never tested.

The probe needed fixing three times

Each version produced a confident red pointing at the wrong layer, and each is now pinned in a comment:

  1. It picked a stopped peer cluster, failed, and blamed the Docker network — the node container had been dead for two days.
  2. It matched "status":"OK" literally, so mcp and agent-runtime — which answer with a bare "status":"ok" rather than core-api's ADR-0073 envelope — were reported unreachable while serving fine.
  3. It used kubectl run -i --rm, which attaches after creating the pod and loses the stdout of a container that already exited. Two consecutive runs disagreed about which surface was reachable.

Verification — measured, not asserted

Real images, throwaway cluster pairs, then torn down:

Check Result
url from this machine 3/3 surfaces OK
url from a second kind cluster 3/3 surfaces OK
Three consecutive runs Identical, 3/3 both paths
After rollout restart of all three All six checks still OK
ci-runner.mjs governance ✅ 17 scripts
gitleaks dir . ✅ exit 0

Not done, stated rather than dropped

  • The up/apps-up path (Docker Desktop k8s, no kind) inherits the NodePort values and was not exercised.
  • Port mappings are fixed at cluster creation and cannot be added later, so kind_create now says when the existing cluster predates the config instead of leaving a never-bound port to be found as a refused connection.
  • The three surfaces disagree on the /health shape. That is a contract question, not an infra one, and is untouched here.

🤖 Generated with Claude Code

beyondnetPeru and others added 2 commits August 3, 2026 21:53
Core and Tracker are separate products in separate repositories that talk
over HTTP, so validating them in ONE cluster would hand them in-cluster
DNS and hide the defects that only appear across a network boundary. Two
clusters is the honest topology and already the de-facto one — the
Tracker's manifest deploys into its own `evolith-tracker` cluster.

What was missing was the link. Everything was reached with
`kubectl port-forward`, which is right inside one cluster and wrong
between two: it is a host process nothing in another cluster can point
at, and it dies on every rollout of the target — so redeploying the Core
breaks the Tracker and the symptom looks like a Tracker defect.

There are TWO addresses now, and they are not interchangeable:

  FROM ANOTHER CLUSTER  http://<cluster>-control-plane:30080
  FROM THIS MACHINE     http://localhost:30080

The cross-cluster one does not use the host mapping at all. Every kind
cluster joins the same `kind` Docker network, so the consumer's pod
reaches the node container by name and hits the NodePort directly. The
host mapping serves the other consumer — browser, curl, CLI — and is
bound to 127.0.0.1, which is also precisely why it cannot serve the first
case.

`host.docker.internal` was the first thing written here and it is WRONG:
it fails with `Could not resolve host` inside a kind pod, which resolves
through CoreDNS in the node and never sees the Docker Desktop entry. The
comments say so, because the wrong URL is the plausible one.

Measured on a throwaway pair of clusters with the real core-api image,
then torn down: a pod in a second cluster got `{"status":"OK"}` by name
and by IP, the host port answered, and both survived `rollout restart`.

`local-test.sh url` proves BOTH paths on demand rather than asserting
them — they fail independently, so one check would cover for the other.
It picks a RUNNING peer: the first version picked a stopped cluster,
reported the network as broken, and the real cause was a node container
dead for two days.

The port mapping is fixed at cluster creation and cannot be added later,
so `kind_create` says when the existing cluster predates the config
instead of leaving a port that was never bound to be discovered by a
refused connection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the probe three times

"Evolith Core and its interfaces" is REST, MCP and the agent runtime. The
first version gave a stable address to core-api alone, which would have
let a consumer reach one surface of three while the docs implied parity —
so mcp (30081) and agent-runtime (30082) now carry pinned node ports too,
and the kind config maps all three.

The probe that proves it needed fixing three times, each a different way
of reporting a defect that was not there:

  - it picked a STOPPED peer cluster, failed, and blamed the Docker
    network; the node container had been dead for two days;
  - it matched `"status":"OK"` literally, so mcp and agent-runtime — which
    answer with a bare `"status":"ok"` rather than the ADR-0073 envelope
    core-api uses — were reported unreachable while serving fine;
  - it used `kubectl run -i --rm`, which attaches AFTER creating the pod
    and loses the stdout of a container that already exited. Two
    consecutive runs disagreed about which surface was reachable.

All three are pinned in the comments, because each produced a confident
red that pointed at the wrong layer.

Measured, not asserted: three clean clusters' worth of runs with the real
images. Three consecutive `url` invocations agreed 3/3 on both paths, and
all six checks held after `rollout restart` of all three deployments.
Test clusters torn down; `evolith-cluster` untouched (it has been stopped
for 47h, exit 137).

Worth a look separately: the three surfaces disagree on the /health
shape. core-api returns the ADR-0073 envelope, the other two a bare
object. Not changed here — that is a contract question, not an infra one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@beyondnetPeru
beyondnetPeru requested a review from a team as a code owner August 4, 2026 03:18
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

📊 Bilingual Coverage Impact

PR Changes

  • Paired EN/ES files modified: 0
  • New EN files needing ES translation: 0

Repository Coverage

Metric Value
Total EN files 526
Total ES files 500
Paired files 0
Coverage 0%

Good: All EN changes have ES counterparts.


Generated by GitHub Actions

@beyondnetPeru
beyondnetPeru merged commit eb08326 into develop Aug 4, 2026
32 checks passed
@beyondnetPeru
beyondnetPeru deleted the feat/kind-cross-cluster-core-address branch August 4, 2026 03:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant