From 24bd50a53cb203becba526c9ea40db6116ef1055 Mon Sep 17 00:00:00 2001 From: Josh Siviour Date: Wed, 24 Jun 2026 21:54:35 +1000 Subject: [PATCH] feat: ecs-node AMI (alpine + containerd + ecs-agent baked) (#415) Adds the spinifex-ecs-node AMI variant: alpine 3.21.7 + containerd + runc + cni-plugins + ca-certificates with the ecs-agent binary baked to /usr/local/bin and three OpenRC services wired in dependency order (ecs-ca-install -> containerd -> ecs-agent). - manifest.conf drives build-system-image.sh (no host mounts). - ecs-ca-install.initd: first-boot oneshot installing the cloud-init delivered gateway CA into the system trust; no-op when absent. - ecs-agent.initd: sources /etc/spinifex-ecs/agent.env, backgrounds the agent after net + containerd. - 10-bridge.conflist: CNI bridge for task networking (Sprint 4d). - setup.sh: exec bits, /opt/cni/bin symlink, content-store dirs, ttyS0 console reorder + fast boot menu. - Makefile: build-ecs-node-image / import-ecs-node-image targets. --- Makefile | 8 +++- scripts/images/ecs-agent/10-bridge.conflist | 21 ++++++++++ scripts/images/ecs-agent/ecs-agent.initd | 33 ++++++++++++++++ scripts/images/ecs-agent/ecs-ca-install.initd | 33 ++++++++++++++++ scripts/images/ecs-agent/manifest.conf | 35 +++++++++++++++++ scripts/images/ecs-agent/setup.sh | 38 +++++++++++++++++++ 6 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 scripts/images/ecs-agent/10-bridge.conflist create mode 100644 scripts/images/ecs-agent/ecs-agent.initd create mode 100644 scripts/images/ecs-agent/ecs-ca-install.initd create mode 100644 scripts/images/ecs-agent/manifest.conf create mode 100644 scripts/images/ecs-agent/setup.sh diff --git a/Makefile b/Makefile index 237b882e3..b8ef6c55c 100644 --- a/Makefile +++ b/Makefile @@ -94,6 +94,12 @@ import-eks-node-image: ## Build + register the eks-node AMI (requires a running publish-eks-node-image: ## Build + publish the eks-node AMI to Cloudflare R2 (needs R2_ENDPOINT + AWS_* env) ./scripts/publish-system-image.sh scripts/images/eks-node/manifest.conf --build +build-ecs-node-image: ## Build the spinifex-ecs-node AMI (Alpine + containerd + ecs-agent; IMPORT=1 to register) + $(MAKE) build-system-image IMAGE=ecs-agent + +import-ecs-node-image: ## Build + register the ecs-node AMI (requires a running cluster) + $(MAKE) build-system-image IMAGE=ecs-agent IMPORT=1 + MICROVM_OUT_DIR := build/microvm MICROVM_ARTIFACTS := $(MICROVM_OUT_DIR)/vmlinuz $(MICROVM_OUT_DIR)/initramfs.cpio.gz MICROVM_INPUTS := scripts/build-microvm-image.sh $(MICROVM_OUT_DIR)/init.sh $(MICROVM_OUT_DIR)/inittab bin/lb-agent @@ -291,7 +297,7 @@ distro-arm64: distro-clean: rm -rf dist/ -.PHONY: build build-ui build-installer build-lb-agent build-ecs-agent build-system-image build-eks-node-image import-eks-node-image publish-eks-node-image build-microvm-image install-microvm go_build preflight test test-cover test-race diff-coverage bench test-actions test-harness manifest-check manifest-lint manifest-lint-update \ +.PHONY: build build-ui build-installer build-lb-agent build-ecs-agent build-system-image build-eks-node-image import-eks-node-image publish-eks-node-image build-ecs-node-image import-ecs-node-image build-microvm-image install-microvm go_build preflight test test-cover test-race diff-coverage bench test-actions test-harness manifest-check manifest-lint manifest-lint-update \ deploy reinstall clean \ install-system install-go install-aws quickinstall \ lint fix govulncheck \ diff --git a/scripts/images/ecs-agent/10-bridge.conflist b/scripts/images/ecs-agent/10-bridge.conflist new file mode 100644 index 000000000..f55c6b2b1 --- /dev/null +++ b/scripts/images/ecs-agent/10-bridge.conflist @@ -0,0 +1,21 @@ +{ + "cniVersion": "1.0.0", + "name": "ecs-bridge", + "plugins": [ + { + "type": "bridge", + "bridge": "ecs0", + "isGateway": true, + "ipMasq": true, + "ipam": { + "type": "host-local", + "ranges": [ + [{ "subnet": "172.31.0.0/16" }] + ], + "routes": [{ "dst": "0.0.0.0/0" }] + } + }, + { "type": "portmap", "capabilities": { "portMappings": true } }, + { "type": "firewall" } + ] +} diff --git a/scripts/images/ecs-agent/ecs-agent.initd b/scripts/images/ecs-agent/ecs-agent.initd new file mode 100644 index 000000000..09cee953d --- /dev/null +++ b/scripts/images/ecs-agent/ecs-agent.initd @@ -0,0 +1,33 @@ +#!/sbin/openrc-run +# ecs-agent — Spinifex ECS container-instance agent. Resolves host identity from +# IMDSv2, connects to the cluster NATS bus, registers + heartbeats on the Layer-2 +# subjects, and pulls task images through containerd. +# +# Static config (gateway URL, CA, region, cluster, NATS, containerd socket) comes +# from /etc/spinifex-ecs/agent.env, dropped by cloud-init user-data at launch. + +description="Spinifex ECS container-instance agent" +command="/usr/local/bin/ecs-agent" +command_background="yes" +pidfile="/run/ecs-agent.pid" +output_log="/var/log/ecs-agent.log" +error_log="/var/log/ecs-agent.log" + +start_stop_daemon_args="--wait 2000" + +depend() { + need net containerd + after firewall ecs-ca-install +} + +start_pre() { + # Cloud-init drops the agent config here; OpenRC injects it into the agent's + # environment. Absent file is tolerated — the agent falls back to its + # built-in defaults (cluster=default, local NATS, default IMDS). + if [ -f /etc/spinifex-ecs/agent.env ]; then + # shellcheck disable=SC1091 + . /etc/spinifex-ecs/agent.env + export ECS_GATEWAY_URL ECS_GATEWAY_CA ECS_REGION ECS_CLUSTER \ + ECS_NATS_URL ECS_IMDS_BASE ECS_CONTAINERD_SOCKET ECS_HEARTBEAT_INTERVAL + fi +} diff --git a/scripts/images/ecs-agent/ecs-ca-install.initd b/scripts/images/ecs-agent/ecs-ca-install.initd new file mode 100644 index 000000000..5ef64b2f4 --- /dev/null +++ b/scripts/images/ecs-agent/ecs-ca-install.initd @@ -0,0 +1,33 @@ +#!/sbin/openrc-run +# ecs-ca-install — first-boot oneshot that installs the cloud-init-delivered +# gateway CA into the system trust store, so containerd trusts the internal ECR +# registry's TLS when pulling task images. Ordered before containerd + ecs-agent. +# +# The CA is delivered per-instance (not baked) at /etc/spinifex-ecs/gateway-ca.pem +# by cloud-init user-data, mirroring how EKS delivers EKS_GATEWAY_CA. No-op when +# the file is absent — containerd then relies on the stock system trust. + +description="Install Spinifex gateway CA into the system trust (first boot)" + +GATEWAY_CA="/etc/spinifex-ecs/gateway-ca.pem" +TRUST_DST="/usr/local/share/ca-certificates/spinifex-gateway.crt" + +depend() { + before containerd ecs-agent + after localmount +} + +start() { + if [ ! -f "${GATEWAY_CA}" ]; then + einfo "no gateway CA at ${GATEWAY_CA}; using stock system trust" + return 0 + fi + ebegin "Installing gateway CA into system trust" + install -D -m 0644 "${GATEWAY_CA}" "${TRUST_DST}" + update-ca-certificates >/dev/null 2>&1 + eend $? +} + +stop() { + return 0 +} diff --git a/scripts/images/ecs-agent/manifest.conf b/scripts/images/ecs-agent/manifest.conf new file mode 100644 index 000000000..48393a6d3 --- /dev/null +++ b/scripts/images/ecs-agent/manifest.conf @@ -0,0 +1,35 @@ +IMAGE_NAME="ecs-agent" +IMAGE_DESCRIPTION="Spinifex ECS container instance — Alpine + containerd + runc + cni-plugins + ecs-agent" +DISTRO="alpine" +ALPINE_VERSION="3.21.7" + +# 4G root: containerd content store (pulled task images) lives under +# /var/lib/containerd on the root disk. Instances can attach a larger root EBS at +# RunInstances; this is the baked floor. +IMAGE_SIZE="4G" + +# containerd-openrc ships the OpenRC service for containerd (the containerd apk +# itself has no init script). cni-plugins + the baked bridge conflist ready the +# image for Sprint 4d task networking. curl is used by the first-boot CA hook and +# IMDS probes. +APK_PACKAGES="containerd containerd-openrc runc cni-plugins ca-certificates curl openrc" + +# ecs-agent is the only built binary (static, FIPS boot like the host build). +BUILD_COMMANDS="CGO_ENABLED=0 GOFIPS140=v1.0.0 go build -ldflags \"-s -w\" -o bin/ecs-agent ./cmd/ecs-agent" + +INSTALL_BINARIES="bin/ecs-agent:/usr/local/bin/ecs-agent" + +INSTALL_FILES="\ +scripts/images/ecs-agent/ecs-agent.initd:/etc/init.d/ecs-agent \ +scripts/images/ecs-agent/ecs-ca-install.initd:/etc/init.d/ecs-ca-install \ +scripts/images/ecs-agent/10-bridge.conflist:/etc/cni/net.d/10-bridge.conflist" + +# ecs-ca-install (first boot) installs the cloud-init gateway CA into the system +# trust before containerd starts; containerd then trusts the ECR registry TLS. +# ecs-agent registers + heartbeats on the NATS bus once net + containerd are up. +ENABLE_SERVICES="containerd ecs-ca-install ecs-agent" + +SETUP_SCRIPT="scripts/images/ecs-agent/setup.sh" + +AMI_NAME="spinifex-ecs-node" +SYSTEM_TAG="spinifex:managed-by=ecs" diff --git a/scripts/images/ecs-agent/setup.sh b/scripts/images/ecs-agent/setup.sh new file mode 100644 index 000000000..307d667ac --- /dev/null +++ b/scripts/images/ecs-agent/setup.sh @@ -0,0 +1,38 @@ +#!/bin/sh +set -eu + +# setup.sh — guest customisation for the spinifex-ecs-node AMI. +# +# Runs inside the libguestfs appliance (via virt-customize --run) under +# build-system-image.sh, after packages, binaries, and INSTALL_FILES are placed. +# Sets exec bits on the OpenRC services, wires the CNI bin path containerd's CRI +# plugin expects, and applies the standard Alpine-cloud serial-console + fast +# boot-menu tweaks so orchestrator-captured ttyS0 logs work. + +# INSTALL_FILES land 0644; OpenRC requires 0755 on init scripts. +chmod 0755 /etc/init.d/ecs-agent /etc/init.d/ecs-ca-install + +# cni-plugins (alpine) installs to /usr/libexec/cni; containerd's CRI plugin +# defaults its bin dir to /opt/cni/bin. Symlink so the baked bridge conflist +# resolves its plugins when Sprint 4d wires task networking. +mkdir -p /opt/cni +ln -sf /usr/libexec/cni /opt/cni/bin + +# Content-store + agent config dirs. +mkdir -p /var/lib/containerd /etc/spinifex-ecs + +# Bind /dev/console to the serial port so userspace boot output (OpenRC service +# starts, cloud-init, ecs-agent registration) reaches ttyS0, which the +# orchestrator captures host-side. Stock Alpine lists console=tty0 last and Linux +# makes the last console= the controlling /dev/console — reorder so ttyS0 wins. +sed -i \ + 's|console=ttyS0,115200n8 console=ttyAMA0,115200n8 console=tty0|console=tty0 console=ttyAMA0,115200n8 console=ttyS0,115200n8|' \ + /etc/update-extlinux.conf /boot/extlinux.conf + +# Cut the boot-menu countdown from 10s to ~1s (a fixed tax on every VM start). +# Patch the generator config (seconds) and the rendered output (1/10s) so a +# regenerate keeps the short value; a small nonzero keeps the menu interruptible. +sed -i 's/^timeout=.*/timeout=1/' /etc/update-extlinux.conf +sed -i 's/^TIMEOUT[[:space:]].*/TIMEOUT 10/' /boot/extlinux.conf + +echo "[ecs-node-setup] done"