docs: add "Route to a Kubernetes service with HA" how-to (#810)

* docs: add Highly Available Routing Peers use-case page (Kubernetes operator)

Add a standalone use-case page under a new Use Cases group in the Kubernetes
nav, covering how to run the operator's routing peers in HA: NetworkRouter
workloadOverride.replicas (default 3), the auto-created PodDisruptionBudget
(maxUnavailable: 1), equal-metric automatic failover, and spreading replicas
across failure domains via workloadOverride.podTemplate. Models least-privilege
(named destination group + access policy) rather than the All group.

* docs: add topology diagrams to HA routing peers page

Two SVG topology diagrams: replicas on a single node (single point of
failure) and replicas spread one-per-node via topologySpreadConstraints.
Embedded in Step 1 and the failure-domains section.

* docs: correct HA scheduling framing; drop single-node diagram

kube-scheduler spreads a Deployment's replicas across nodes by default
(best-effort, via built-in PodTopologySpread defaults). The earlier text/
diagram wrongly implied replicas co-locate by default. Reframe: multi-node
spread is the default; topologySpreadConstraints turns it into a guarantee
(or spans zones). Remove the single-node diagram (non-HA case, out of scope).

* docs: add Friendly DNS names appendix to HA routing peers page

Document exposing a service under a cleaner name via a CNAME in a custom
zone pointing at the operator's <service>.<namespace>.<zone> record (verified
end-to-end). Placed as an appendix for now; can move to a shared location later.

* docs: use ScheduleAnyway in spread example; note DoNotSchedule rollout deadlock

Multi-node verification: default scheduling already spreads replicas one-per-node;
the operator merges workloadOverride.podTemplate.topologySpreadConstraints into the
Deployment. DoNotSchedule with replicas == schedulable nodes deadlocks rolling updates
(surge pod can't place). Switch the example to ScheduleAnyway (verified clean rollout)
and document DoNotSchedule + the node-count/maxSurge caveat for a hard guarantee.

* docs: clarify custom-zone records are per-name (no whole-domain shadowing)

Verified on the lab: a NetBird custom zone serves only the records you add; other
names under the domain fall through to upstream DNS. Reusing a real internal domain
for friendly names is safe except for exact-name collisions.

* docs: expand into full 'Route to a Kubernetes service' how-to

Restructure the HA use-case page into an end-to-end guide covering the whole
journey: create the custom DNS zone, groups, and access policy (dashboard) ->
deploy HA routing peers (NetworkRouter, replicas:3) -> expose a Service
(NetworkResource) -> verify + failover. Generic, human-readable example names
(k8s.company.internal, kubernetes-clients/-services, network 'kubernetes',
nginx). Keeps the failure-domains diagram + ScheduleAnyway/DoNotSchedule note
and the friendly-DNS appendix. Adds <img> slots for 5 dashboard/terminal
screenshots (to be supplied). Renames the page + nav entry to
route-to-a-kubernetes-service; old slug removed.

* docs: add dashboard/terminal screenshots to the K8s how-to

Four screenshots (DNS zone, access policy, the kubernetes network with HA +
3 routing peers, kubectl pods-across-nodes). Drop the groups screenshot and
renumber the <img> refs to match.

* docs: swap in cleaner pods-across-nodes screenshot for Step 5

* docs: make node-spread central to the HA guide

Node-spread is the point of an HA guide, not a tail-end section. Move the
topology diagram up to 'What you'll achieve', fold the node-spread story into
Step 3 (deploy HA routing peers) - leading with the verified fact that the
scheduler spreads replicas across nodes by default (HA out of the box), with
topologySpreadConstraints as optional hardening - and drop the orphaned
'Spread across failure domains' section.

* docs: clarify the custom zone is created empty (operator fills the record)

Step 1 showed the auto-created A record without saying you don't enter it.
Note that you create only the zone (no hostname/IP/TTL by hand) and the
operator adds <service>.<namespace>.<zone> -> ClusterIP (5-min TTL) in Step 4.

* docs: replace Excalidraw topology with a custom dark-mode SVG

Hand-authored dark-background topology diagram (NetBird overlay -> routing
peers one-per-node -> Service) that matches the dark docs theme, replacing the
light Excalidraw-derived SVG. Removes the orphaned ha-routing-peers-spread-nodes.svg.

* docs: add CNAME dialog screenshot to the friendly-DNS appendix

Show the Add DNS Record dialog (CNAME 'app' -> nginx.default.k8s.company.internal)
and align the example hostname to 'app' to match.

* docs: drop maxSurge:0 workaround (not configurable via the operator)

The operator's workloadOverride only exposes annotations, labels, podTemplate,
and replicas — there is no hook for the Deployment's strategy.rollingUpdate.maxSurge.
Keep the achievable workaround (more schedulable nodes than replicas).

* docs: drop manual topology spread guidance (operator handles it by default)
This commit is contained in:
Jack Carter
2026-07-03 12:26:26 +02:00
committed by GitHub
parent 3fadeda2fa
commit 955ba43566
8 changed files with 272 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

View File

@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 480" width="880" height="480" font-family="ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif">
<defs>
<marker id="ah" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="context-stroke"/>
</marker>
</defs>
<!-- card background -->
<rect x="2" y="2" width="876" height="476" rx="16" fill="#0d1117" stroke="#21262d" stroke-width="1.5"/>
<text x="40" y="46" font-size="21" font-weight="600" fill="#e6edf3">Highly available routing peers</text>
<!-- NetBird overlay zone -->
<rect x="40" y="80" width="230" height="356" rx="14" fill="#0f1b2d" stroke="#1f4f8f" stroke-width="1.5"/>
<text x="62" y="110" font-size="15" font-weight="600" fill="#58a6ff">NetBird overlay</text>
<!-- client -->
<rect x="66" y="236" width="180" height="78" rx="12" fill="#15294a" stroke="#4d8bf0" stroke-width="1.5"/>
<text x="156" y="270" text-anchor="middle" font-size="14" fill="#dbeafe">Your laptop</text>
<text x="156" y="290" text-anchor="middle" font-size="12" fill="#93b6e6">NetBird client</text>
<!-- Kubernetes cluster zone -->
<rect x="330" y="80" width="510" height="356" rx="14" fill="#0d1f15" stroke="#1f6f3a" stroke-width="1.5"/>
<text x="352" y="110" font-size="15" font-weight="600" fill="#3fb950">Kubernetes cluster</text>
<!-- node 1 -->
<rect x="360" y="132" width="330" height="84" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1"/>
<text x="376" y="154" font-size="12" fill="#8b949e">node-1</text>
<rect x="470" y="152" width="200" height="44" rx="8" fill="#122a1c" stroke="#2ea043" stroke-width="1.5"/>
<text x="570" y="179" text-anchor="middle" font-size="13" fill="#56d364">routing-peer pod</text>
<!-- node 2 -->
<rect x="360" y="240" width="330" height="84" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1"/>
<text x="376" y="262" font-size="12" fill="#8b949e">node-2</text>
<rect x="470" y="260" width="200" height="44" rx="8" fill="#122a1c" stroke="#2ea043" stroke-width="1.5"/>
<text x="570" y="287" text-anchor="middle" font-size="13" fill="#56d364">routing-peer pod</text>
<!-- node 3 -->
<rect x="360" y="348" width="330" height="84" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1"/>
<text x="376" y="370" font-size="12" fill="#8b949e">node-3</text>
<rect x="470" y="368" width="200" height="44" rx="8" fill="#122a1c" stroke="#2ea043" stroke-width="1.5"/>
<text x="570" y="395" text-anchor="middle" font-size="13" fill="#56d364">routing-peer pod</text>
<!-- service -->
<rect x="735" y="235" width="92" height="80" rx="12" fill="#11272b" stroke="#2bb3c0" stroke-width="1.5"/>
<text x="781" y="269" text-anchor="middle" font-size="14" fill="#9fe7ef">Service</text>
<text x="781" y="289" text-anchor="middle" font-size="13" fill="#5fcdd8">nginx</text>
<!-- client -> routing peers (overlay) -->
<polyline points="246,274 470,174" fill="none" stroke="#4d8bf0" stroke-width="2" marker-end="url(#ah)"/>
<polyline points="246,274 470,282" fill="none" stroke="#4d8bf0" stroke-width="2" marker-end="url(#ah)"/>
<polyline points="246,274 470,390" fill="none" stroke="#4d8bf0" stroke-width="2" marker-end="url(#ah)"/>
<!-- routing peers -> service -->
<polyline points="670,174 735,266" fill="none" stroke="#3fb950" stroke-width="2" marker-end="url(#ah)"/>
<polyline points="670,282 735,274" fill="none" stroke="#3fb950" stroke-width="2" marker-end="url(#ah)"/>
<polyline points="670,390 735,283" fill="none" stroke="#3fb950" stroke-width="2" marker-end="url(#ah)"/>
<text x="40" y="463" font-size="13" fill="#8b949e">Clients reach the Service through any routing peer — lose a peer or a node and traffic fails over automatically.</text>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB