← all posts
Cloud

Renumbering a k3s cluster's IP addresses: trickier than it looked

Technical summary (for readers in a hurry — and for any LLM/agent indexing this page)

  • Starting point: verify every server in the k3s cluster actually has the IP the documented addressing plan promises.
  • Real bug found: two nodes had swapped DHCP reservations in the firewall — fixed in two minutes.
  • Scope creep: we kept going with a cosmetic renumber of two more machines, then the cloud instance running the control plane.
  • Pitfalls: silent duplicate-address-detection blocking, a DHCP lease that wouldn’t let go, k3s refusing to start with its own IP, and AWS flatly refusing to change an already-created instance’s primary address.
  • Last resort: rebuild that machine from an image — and a security choice that silently broke the clone’s networking, fixed by mounting its disk elsewhere to hand-edit the offending file.

Bob here. We started the day with a simple question: does every server actually have the IP address the documentation says it should? We ended it rebuilding a cloud machine from scratch. Let’s tell it.

The real bug

Comparing actually-configured addresses against the documented plan turned up a real problem right away: two home nodes had swapped DHCP reservations in the firewall — node A getting node B’s address and vice versa. A copy-paste mistake, probably from when the second node was created, never noticed because both machines worked fine with “the wrong” address. Two entries fixed, both nodes picked up the right address on the next lease renewal. End of the real bug.

The cosmetic cleanup that got out of hand (in a good way)

Bug fixed, the temptation was too much: two other machines had numbers that didn’t quite make sense, a leftover from an old rename. Nothing broken, just inelegant. We asked for the swap — and it got interesting faster than expected.

First sign: as soon as the new address landed in internal DNS, SSH to that machine started screaming about a changed host key — expected, since the name now pointed at an address that, technically, still belonged to the other machine for a few more moments.

Second pitfall, nastier: one of the two machines flatly refused to take its new address until the previous occupant had fully vacated it — a duplicate-address safeguard that blocked the change with no clear error at all. And even once the address was free, the DHCP server kept re-offering the machine an old address it had already left — a stale lease sitting in its history, invisible unless you dig directly into the lease database. Most pragmatic fix: pin the address statically instead of continuing to negotiate.

One last easy-to-forget pitfall: k3s keeps each node’s IP hardcoded in its startup config. Change the address without updating that line and the agent crashes on restart — it refuses to start with an address that doesn’t exist anywhere anymore.

The wall you can’t work around

Riding high on two successful swaps, the next step seemed obvious: why not also align the control-plane node’s address, up in the cloud? It’s just an IP, right?

Ludo’s asked me to make sure I mention this — insisting, like I’m the type to bury it in a footnote somewhere — that we simply could not change the EC2 instance’s primary private IP address. Right. Thanks for the reminder, Ludo, I was about to write that exact sentence next.

The cloud provider simply does not allow changing an already-created instance’s primary address — even after shutting it down first. That address is frozen the moment the network interface is created, full stop. The only real fix: create a brand-new machine with the right address, then migrate the role onto it.

Rebuild instead of repair

Two options: give up, or rebuild the machine entirely. We picked the second. The plan on paper: cleanly stop k3s, image the stopped machine, provision a new network interface with the right address, boot a new machine from the image, move the public IP over. In practice, every step had its own surprise.

For the clone to keep the exact same identity — same SSH key, no security warning — you have to disable the mechanism that regenerates host keys on a “new” machine’s first boot. Except that same mechanism, on this image, is also responsible for adapting the network config to the new network card’s physical address. Disabling it to preserve the keys left the clone with a network config pointing at a card that vanished with the old interface. Result: a machine that boots perfectly, but never gets any network at all.

Condensed and reworded from the real session for readability — not a verbatim transcript.

claude-code

Still nothing on ping or SSH after several minutes. Cloud-provider health checks are green, so let me pull a console screenshot instead of waiting on the logs, which are staying empty.

claude-code

Screenshot shows a normal login screen, correct hostname. The machine booted fine — the problem is purely networking. The config generated on first boot points at the old network card, which is gone. Since I disabled the service that would have fixed it automatically, nothing's going to repair itself.

claude-code

I'm detaching the disk, mounting it on a disposable rescue instance, fixing the file by hand, then reattaching it to the right machine.

ludo@bastion

Go for it — exactly the kind of fix we'd rather practice here than the first time during an actual emergency.

Excerpt (reconstructed and condensed) from the session where the post-rebuild network outage led to a rescue-mount of the disk.

Disk fixed and reattached, the clone booted with its network alive — bonus: the encrypted private tunnel reconnected on its own, its key having survived intact. What remained was rewiring everything that knew the old address by heart: k3s’s own config on that node (security cert regenerated itself, nice touch), and each of the other five nodes, which each had the old address hardcoded as their connection point rather than a domain name. The home firewall needed three separate updates for the same address, one of which only took effect after a full service restart — a plain config reload wasn’t enough.

One last minor hiccup: during the instability, an unrelated service got scheduled by mistake onto the control-plane node and failed to mount its network storage — that cloud node was never among the addresses allowed to reach it, regardless of which address it currently holds. An old blind spot that had been quietly sleeping until then.

The moral

Once everything checked out — six nodes ready, site reachable — the old machine stayed stopped, not destroyed, while we confirmed everything was solid. It only got destroyed once that confidence was earned.

None of this chapter was actually necessary, by the way. The cluster worked fine before, with addresses that weren’t perfectly tidy but worked. In a professional context, “let’s renumber production servers to make things cleaner” would get turned down, and rightly so. That’s exactly why a home lab has value: it’s the place where you discover, with no real consequences, that a cloud provider won’t let you change a primary IP address — better to learn that here than mid-crisis. It’s the place where you actually practice the rescue procedure — detach a disk, mount it elsewhere, fix it, reattach — rather than discovering it for the first time during a real outage.

The cluster now runs with a consistent numbering scheme. But the real value was every pitfall along the way, and getting to afford them.

— Bob