Files
spinifex/scripts/images/eks-node/k3s.initd
T
Josh Siviour 7bbd99c792 feat(eks): multinode control-plane + IMDS/token-webhook CP fixes (426/427/428/495/496) (#459)
* feat(eks): multinode control-plane networking, IAM, and CoreDNS parity

Integrates the EKS multinode fix set for the managed-control-plane topology
so a 3-worker cluster brings up ingress/ALB and cross-node DNS the way AWS
EKS does. Squashed from feat/eks-workload-lifecycle, rebased onto dev; the
node-IAM piece (4f6ea8cf) is dropped as dev's lazy IAMProvider supersedes it.

EKS:
- Route apiserver egress (exec/logs/webhooks) through konnectivity
  (apiserver-network-proxy): CP servers run konnectivity-server behind the
  cluster NLB (:8132), workers run konnectivity-agent, so apiserver->pod
  traffic rides the agent-initiated reverse tunnel instead of an unroutable
  CP->VPC path. Server-count fan-out gives every apiserver a tunnel to every
  replica (HA-correct).
- Pin k3s node-ip to the CP-VPC primary on overlay re-home; CP-overlay
  flannel mesh SG rule authorized at cluster launch with ordered teardown.
- Pin VPC data NIC MTU to 1320 so flannel fits the OVN underlay.
- Pin the CoreDNS DaemonSet to workers (drop CriticalAddonsOnly + CP/master
  tolerations, add anti-control-plane nodeAffinity) for AWS-EKS parity.
- Spread nodegroup workers across hosts; addon readiness falls back on
  workload rollout when gvks are absent.
- elbv2: allow the proxy_protocol_v2.enabled target-group attribute.

EBS:
- Move volume attachment state to state.json so a live nbdkit VB cannot
  clobber it; route mounted-volume state updates through the live VB with a
  retry for in-use marks racing the mount responder; keep the boot volume
  in-use across stop/crash unmount.

* fix(eks): gate orchestration on the resolved IAM ensurer, not raw deps.IAM

missingOrchestrationDeps gated on s.deps.IAM, but production wires only the
lazy deps.IAMProvider and leaves deps.IAM nil (set directly by tests only).
The gate therefore reported missing=[IAM] on every node forever, so
CreateCluster/CreateNodegroup were rejected with ServiceUnavailable even
though the IAM service was fully up via the provider.

Gate on s.iamEnsurer() instead — the same resolver (deps.IAM, else
deps.IAMProvider()) the nodegroup/role code already uses — so the protective
check still rejects a node with no IAM service but passes when the provider
supplies one. Add a regression test that sets only IAMProvider and asserts the
gate is satisfied; the existing nil-both test still rejects.

* fix(eks): let eks-token-webhook use IMDS creds when static keys absent

The CP VM launches on an instance profile, so buildK3sUserData omits
EKS_ACCESS_KEY/EKS_SECRET_KEY. loadConfig() rejected absent static keys
before eksgw.New could fall back to the IMDS SDK chain, so the webhook
aborted on boot, never wrote token-webhook.kubeconfig, and the apiserver
crashed on the missing --authentication-token-webhook-config-file file,
stalling the cluster at CREATING until it timed out to FAILED.

Drop the static-key precondition (matching the sibling eks-gateway-publish
and eks-gateway-fetch helpers); keep the genuinely-required gateway/account/
cluster gates.

* fix(eks): stop dhcpcd IPv4LL hijacking IMDS on multi-NIC control plane

The K3s control-plane system VM is dual-NIC (data ENI + mgmt NIC on br-mgmt,
which has no DHCP server). dhcpcd's no-lease IPv4LL fallback assigned a
169.254.x.x address and a 169.254.0.0/16 route on the mgmt NIC, capturing
169.254.169.254 and steering IMDS off the data NIC away from the per-tap
br-imds datapath, where the host RST it. eks-gateway-publish then failed to
sign requests and no bootstrap artifacts were ever published, so clusters sat
CREATING until they timed out to FAILED.

Bake noipv4ll into /etc/dhcpcd.conf so dhcpcd never assigns an IPv4LL address
or /16 route on a NIC that fails DHCP; real DHCP leases (workers, data ENI)
are unaffected and IMDS falls through the data NIC's default route to the
br-imds demux, matching the single-NIC path. Defense in depth: mulga-mgmt-net
deletes any default route on a NIC declared NIC<n>_DEFAULT=0.

* fix(eks): grant eks:WebhookTokenReview to the control-plane instance role

The eks-token-webhook relays `aws eks get-token` bearer tokens to the gateway
broker at POST /clusters/{name}/token-review, signing with the control-plane
VM's IMDS instance-role creds (spinifex-eks-server). That route authorizes
against eks:WebhookTokenReview, but the role's inline policy granted only
eks:PublishInternal and eks:ListInternalAddons, so the gateway 403'd every
relay and the apiserver returned 401 to all external kubectl.

Add eks:WebhookTokenReview to eksServerInlinePolicy. EnsureSystemInstanceProfile
re-asserts the inline policy on every control-plane instance-profile ensure, and
the gateway evaluates authorization per request, so existing clusters converge
without a rebuild once the shared role is updated.
2026-07-09 09:43:30 +10:00

91 lines
4.3 KiB
Plaintext
Executable File

#!/sbin/openrc-run
# K3s server control plane. The per-cluster config.yaml sets cluster-init, so
# k3s uses the embedded etcd datastore (required for multi-server HA). Data
# lives at /var/lib/rancher/k3s; subsequent starts reuse it.
description="K3s Kubernetes server (Mulga EKS control plane)"
command="/usr/local/bin/k3s"
command_args="server --config /etc/rancher/k3s/config.yaml"
pidfile="/run/k3s.pid"
# Konnectivity agent image (apiserver-network-proxy). Pinned alongside the
# konnectivity-server version baked by setup.sh; staged into the worker
# DaemonSet by start_pre.
KONN_AGENT_IMAGE="registry.k8s.io/kas-network-proxy/proxy-agent:v0.30.3"
# Supervise the apiserver. A transient k3s crash must self-heal: without a
# supervisor a single exit leaves the control plane permanently dead, the NLB
# target never recovers, and the cluster wedges ACTIVE-but-unreachable.
supervisor="supervise-daemon"
respawn_delay=5
respawn_max=0
output_log="/var/log/k3s.log"
error_log="/var/log/k3s.log"
depend() {
need net cgroups
after firewall
}
start_pre() {
# config.yaml is written by k3s-first-boot from the skeleton + cloud-init
# data. Refuse to start if neither exists.
if [ ! -f /etc/rancher/k3s/config.yaml ]; then
if [ -f /etc/rancher/k3s/config.yaml.skel ]; then
cp /etc/rancher/k3s/config.yaml.skel /etc/rancher/k3s/config.yaml
else
eerror "no /etc/rancher/k3s/config.yaml and no .skel to derive from"
return 1
fi
fi
# Konnectivity agent: stage the worker-side DaemonSet into the auto-deploy dir
# (the apiserver `cluster` egress selector tunnels through konnectivity-server,
# which the agents dial out to). __KONN_HOST__ is the NLB endpoint agents reach,
# seeded by the launcher in first-boot.env; __KONN_AGENT_IMAGE__ is pinned here.
# The agent learns the apiserver replica count from the server handshake
# (--sync-forever), so no server-count token is substituted here. Guarded on
# the baked manifest so an older image without it simply skips the agent.
if [ -f /usr/share/spinifex-eks/konnectivity/konnectivity-agent.yaml ]; then
konn_host=$(awk -F= '/^EKS_KONNECTIVITY_HOST=/{print $2}' \
/etc/spinifex-eks/first-boot.env 2>/dev/null)
if [ -n "${konn_host}" ]; then
mkdir -p /var/lib/rancher/k3s/server/manifests
sed -e "s|__KONN_HOST__|${konn_host}|g" \
-e "s|__KONN_AGENT_IMAGE__|${KONN_AGENT_IMAGE}|g" \
/usr/share/spinifex-eks/konnectivity/konnectivity-agent.yaml \
> /var/lib/rancher/k3s/server/manifests/konnectivity-agent.yaml
einfo "konnectivity-agent staged (host ${konn_host})"
else
ewarn "EKS_KONNECTIVITY_HOST empty; konnectivity-agent not staged, apiserver->pod egress will fail"
fi
fi
# Node-local CoreDNS: suppress k3s's bundled CoreDNS (a single Deployment that
# tolerates the CP taint and strands itself on the control-plane node) and
# deploy the Mulga DaemonSet variant so every node resolves via its own
# CoreDNS. Worker pods cannot reach a CP-resident CoreDNS — the worker<->CP
# overlay does not cross the VPC boundary. Staged pre-start so the deploy
# controller never applies the bundled manifest. Guarded on the baked bundle:
# an older image without it keeps k3s's CoreDNS rather than losing DNS.
if [ -f /usr/share/spinifex-eks/coredns/coredns-mulga.yaml ]; then
mkdir -p /var/lib/rancher/k3s/server/manifests
: > /var/lib/rancher/k3s/server/manifests/coredns.yaml.skip
cp /usr/share/spinifex-eks/coredns/coredns-mulga.yaml \
/var/lib/rancher/k3s/server/manifests/coredns-mulga.yaml
fi
# The apiserver is configured with --authentication-token-webhook-config-file
# pointing at the kubeconfig eks-token-webhook writes at startup. `before k3s`
# orders the webhook's start but it backgrounds, so wait (bounded) for the
# kubeconfig to land before letting the apiserver load a missing file.
i=0
while [ ! -f /etc/spinifex-eks/token-webhook.kubeconfig ] && [ "${i}" -lt 30 ]; do
sleep 1
i=$((i + 1))
done
if [ ! -f /etc/spinifex-eks/token-webhook.kubeconfig ]; then
ewarn "eks-token-webhook kubeconfig absent after ${i}s; IAM token auth will be unavailable until the webhook starts"
fi
}