feat: add Debian 13 golden image Packer pipeline and cloud-init bootstrap

This commit is contained in:
deytenit committed 2026-08-30 19:48:22 +03:00
1 parent b72b961968
commit 6123c3f6b4
12 files changed
+645 -6

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
#cloud-config
# ==============================================================================
# Cloud-Init User-Data Template for apex.ermnvldmr.com Node
# Compatible with generic cloud-init and virtualized environments (QEMU/KVM)
#
# Usage:
# 1. Copy template: cp ci/cloud-init/user-data.template.yaml user-data.yaml
# 2. Replace the public SSH key for adam.
# 3. Replace the private SSH deploy key for GitHub access.
# 4. Adjust APEX_REPO_NAME, APEX_REPO_URL, and tier devices in /etc/apex/bootstrap.env.
# 5. Pass user-data.yaml as user-data during VM creation.
# ==============================================================================
preserve_hostname: false
hostname: daedalus.a1.apex.ermnvldmr.com
users:
- name: adam
sudo: ALL=(ALL) NOPASSWD:ALL
groups: [sudo, docker]
shell: /bin/bash
ssh_authorized_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI_REPLACE_WITH_YOUR_PUBLIC_SSH_KEY adam@workstation"
write_files:
# ----------------------------------------------------------------------------
# 1. SSH Deploy Key for GitHub Submodule & Repo Access
# ----------------------------------------------------------------------------
- path: /home/adam/.ssh/id_ed25519
owner: adam:adam
permissions: '0600'
content: |
-----BEGIN OPENSSH PRIVATE KEY-----
# REPLACE WITH YOUR ED25519 DEPLOY PRIVATE KEY
-----END OPENSSH PRIVATE KEY-----
# ----------------------------------------------------------------------------
# 2. Node Bootstrap Parameters
# ----------------------------------------------------------------------------
- path: /etc/apex/bootstrap.env
owner: root:root
permissions: '0600'
content: |
APEX_REPO_NAME="daedalus.apex.ermnvldmr.com"
APEX_REPO_URL="git@github.com:deytenit/daedalus.apex.ermnvldmr.com.git"
APEX_TIER1_DEVICE="/dev/vdb"
APEX_TIER2_DEVICE="/dev/vdc"
APEX_TIER3_DEVICE=""
runcmd:
- /usr/local/bin/apex-bootstrap
+75
View File
@@ -0,0 +1,75 @@
packer {
required_plugins {
qemu = {
version = ">= 1.1.0"
source = "github.com/hashicorp/qemu"
}
}
}
variable "debian_cloud_image_url" {
type = string
default = "https://cloud.debian.org/images/cloud/trixie/daily/latest/debian-13-genericcloud-amd64-daily.qcow2"
}
variable "debian_cloud_image_checksum" {
type = string
default = "none"
}
variable "vm_name" {
type = string
default = "com-ermnvldmr-apex-debian-13-amd64.qcow2"
}
variable "accelerator" {
type = string
default = "kvm"
}
source "qemu" "apex_debian" {
iso_url = var.debian_cloud_image_url
iso_checksum = var.debian_cloud_image_checksum
disk_image = true
disk_size = "10G"
format = "qcow2"
output_directory = "output-qemu"
shutdown_command = "echo 'packer' | sudo -S shutdown -P now"
accelerator = var.accelerator
cd_files = ["ci/packer/cidata/user-data", "ci/packer/cidata/meta-data"]
cd_label = "cidata"
ssh_username = "debian"
ssh_password = "packer"
ssh_timeout = "10m"
vm_name = var.vm_name
net_device = "virtio-net"
disk_interface = "virtio"
headless = true
qemuargs = [
["-m", "2048M"],
["-smp", "2"]
]
}
build {
sources = ["source.qemu.apex_debian"]
provisioner "shell" {
execute_command = "echo 'packer' | sudo -S sh -c '{{ .Vars }} {{ .Path }}'"
scripts = [
"ci/packer/scripts/01-base-system.sh",
"ci/packer/scripts/02-storage-docker.sh",
"ci/packer/scripts/03-bootstrap-helper.sh",
"ci/packer/scripts/04-cleanup.sh"
]
}
post-processor "shell-local" {
inline = [
"echo 'Compressing and optimizing qcow2 image with 2M cluster size...'",
"qemu-img convert -f qcow2 -O qcow2 -o cluster_size=2M -c output-qemu/${var.vm_name} output-qemu/com-ermnvldmr-apex-debian-13-optimized.qcow2",
"mv output-qemu/com-ermnvldmr-apex-debian-13-optimized.qcow2 output-qemu/${var.vm_name}"
]
}
}
+2
View File
@@ -0,0 +1,2 @@
instance-id: apex-golden-image-build
local-hostname: apex-builder
+14
View File
@@ -0,0 +1,14 @@
#cloud-config
ssh_pwauth: true
disable_root: false
chpasswd:
list: |
debian:packer
root:packer
expire: false
users:
- default
- name: debian
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
lock_passwd: false
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
echo "=== [1/6] Configuring Base Packages ==="
apt-get update
apt-get install -y --no-install-recommends \
curl wget tcpdump socat rsync bind9-dnsutils iproute2 net-tools traceroute ca-certificates \
jq sed gawk tar gzip bzip2 xz-utils unzip htop iotop strace lsof sysstat \
parted fdisk pciutils usbutils smartmontools tmux vim less tree bash-completion \
git python3 python3-minimal sudo rsyslog cron qemu-guest-agent
echo "=== [2/6] Configuring Sudo & Passwordless Escalation ==="
cat << 'EOF_SUDO' > /etc/sudoers.d/99-apex
%sudo ALL=(ALL:ALL) NOPASSWD: ALL
EOF_SUDO
chmod 0440 /etc/sudoers.d/99-apex
visudo -cf /etc/sudoers.d/99-apex
echo "=== [3/6] Configuring VirtIO Kernel Modules ==="
cat << 'EOF_VIRTIO' > /etc/modules-load.d/virtio.conf
virtio
virtio_net
virtio_blk
virtio_pci
virtio_scsi
virtiofs
EOF_VIRTIO
echo "=== [4/6] Configuring GRUB Serial Console & Predictable Interfaces ==="
sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT=.*/GRUB_CMDLINE_LINUX_DEFAULT="console=tty0 console=ttyS0,115200n8 net.ifnames=0 biosdevname=0"/' /etc/default/grub
sed -i 's/^#GRUB_TERMINAL=.*/GRUB_TERMINAL="console serial"/' /etc/default/grub
if ! grep -q "GRUB_SERIAL_COMMAND" /etc/default/grub; then
echo 'GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"' >> /etc/default/grub
fi
update-grub
mkdir -p /etc/systemd/system/serial-getty@ttyS0.service.d
cat << 'EOF_GETTY' > /etc/systemd/system/serial-getty@ttyS0.service.d/autologin.conf
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud 115200,38400,9600 %I $TERM
EOF_GETTY
systemctl enable serial-getty@ttyS0.service || true
systemctl enable qemu-guest-agent 2>/dev/null || true
echo "=== [5/6] Configuring Cloud-Init Datasources & Service Enablement ==="
mkdir -p /etc/cloud /etc/cloud/cloud.cfg.d
cat << 'EOF_DS' > /etc/cloud/ds-identify.cfg
policy: enabled
EOF_DS
cat << 'EOF_CLOUD_CFG' > /etc/cloud/cloud.cfg.d/99-cloud-init.cfg
datasource_list: [ NoCloud, ConfigDrive, OpenStack, Ec2, None ]
disable_vm_template: false
EOF_CLOUD_CFG
# Cloud-init services are static and enabled by generator via ds-identify
systemctl enable cloud-init-local.service cloud-init.service cloud-config.service cloud-final.service 2>/dev/null || true
echo "=== [6/6] Configuring Predictable Network DHCP ==="
mkdir -p /etc/systemd/network
cat << 'EOF_NET' > /etc/systemd/network/10-eth.network
[Match]
Name=eth* en*
[Network]
DHCP=yes
EOF_NET
systemctl enable systemd-networkd systemd-resolved || true
mkdir -p /etc/network/interfaces.d
cat << 'EOF_IF' > /etc/network/interfaces.d/eth0
auto eth0
iface eth0 inet dhcp
EOF_IF
echo "=== Base System Configuration Complete ==="
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
echo "=== [1/5] Configuring Debian Repositories for OpenZFS DKMS ==="
# Enable contrib and non-free-firmware for zfs-dkms
sed -i 's/Components: main/Components: main contrib non-free-firmware/g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \
sed -i 's/main$/main contrib non-free-firmware/g' /etc/apt/sources.list 2>/dev/null || true
apt-get update
apt-get install -y --no-install-recommends linux-headers-amd64 zfsutils-linux zfs-dkms
# Pre-compile DKMS modules during bake time so first boot has zero compilation delay
systemctl enable zfs-import-cache zfs-import.target zfs.target || true
echo "=== [2/5] Installing Docker CE from Official Debian Repository ==="
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
apt-get remove -y "$pkg" 2>/dev/null || true
done
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install -y --no-install-recommends \
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
mkdir -p /srv/docker
cat << 'EOF_DOCKER' > /etc/docker/daemon.json
{
"data-root": "/srv/docker",
"log-driver": "json-file",
"log-opts": {
"max-size": "20m",
"max-file": "3"
}
}
EOF_DOCKER
systemctl enable docker.service containerd.service
echo "=== [3/5] Installing and Configuring UFW + ufw-docker ==="
apt-get install -y --no-install-recommends ufw iptables
curl -fsSL https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker -o /usr/local/bin/ufw-docker
chmod +x /usr/local/bin/ufw-docker
# Pre-configure firewall baseline (keep port 22 open during build for Packer SSH session)
ufw default deny incoming
ufw default allow outgoing
ufw default allow routed
ufw allow 22/tcp comment 'SSH default for builder'
ufw allow 2222/tcp comment 'SSH for Apex'
ufw --force enable
# Set up ufw-docker routing in after.rules (now that UFW is active)
/usr/local/bin/ufw-docker install || true
# Symlink into adam ~/.local/bin for engine compatibility
mkdir -p /etc/skel/.local/bin
ln -sf /usr/local/bin/ufw-docker /etc/skel/.local/bin/ufw-docker
echo "=== [4/5] Hardening SSH Configuration ==="
mkdir -p /etc/ssh/sshd_config.d
cat << 'EOF_SSH' > /etc/ssh/sshd_config.d/99-apex.conf
Port 2222
PasswordAuthentication no
PermitEmptyPasswords no
KbdInteractiveAuthentication no
X11Forwarding no
AllowUsers adam
EOF_SSH
echo "=== [5/5] Installing CrowdSec ==="
# PackageCloud does not publish a dedicated 'trixie' suite yet; bookworm suite works seamlessly on Debian 13
export os=debian
export dist=bookworm
if curl -fsSL https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | bash; then
apt-get update
apt-get install -y --no-install-recommends crowdsec crowdsec-firewall-bouncer-iptables
else
echo "PackageCloud script failed, falling back to Debian native repository..."
rm -f /etc/apt/sources.list.d/crowdsec*.list
apt-get update
apt-get install -y --no-install-recommends crowdsec crowdsec-firewall-bouncer-iptables || \
apt-get install -y --no-install-recommends crowdsec
fi
echo "=== Docker, ZFS & Security Stack Complete ==="
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
set -euo pipefail
echo "=== Baking /usr/local/bin/apex-bootstrap Runner ==="
cat << 'BOOTSTRAP_EOF' > /usr/local/bin/apex-bootstrap
#!/usr/bin/env bash
set -euo pipefail
MARKER="/var/lib/apex-bootstrapped"
if [ -f "$MARKER" ]; then
echo "[apex-bootstrap] Node has already been bootstrapped ($MARKER exists). Exiting."
exit 0
fi
echo "============================================================"
echo " Starting apex.ermnvldmr.com Node Bootstrap "
echo "============================================================"
CONFIG_ENV="/etc/apex/bootstrap.env"
if [ ! -f "$CONFIG_ENV" ]; then
echo "[apex-bootstrap] ERROR: $CONFIG_ENV not found!" >&2
echo "[apex-bootstrap] Provide APEX_REPO_NAME, APEX_REPO_URL via cloud-init write_files." >&2
exit 1
fi
source "$CONFIG_ENV"
if [ -z "${APEX_REPO_NAME:-}" ] || [ -z "${APEX_REPO_URL:-}" ]; then
echo "[apex-bootstrap] ERROR: APEX_REPO_NAME or APEX_REPO_URL is empty in $CONFIG_ENV" >&2
exit 1
fi
echo "[apex-bootstrap] Target repository: $APEX_REPO_NAME ($APEX_REPO_URL)"
# 1. Dynamic Swap Allocation
if ! swapon --show | grep -q "/swapfile"; then
echo "[apex-bootstrap] Configuring dynamic swapfile..."
TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
ROOT_AVAIL_KB=$(df -k / | awk 'NR==2 {print $4}')
SWAP_SIZE_MB=2048
if [ "$TOTAL_MEM_KB" -gt 4194304 ]; then
SWAP_SIZE_MB=4096
fi
if [ "$ROOT_AVAIL_KB" -gt $((SWAP_SIZE_MB * 1024 * 2)) ]; then
echo "[apex-bootstrap] Allocating ${SWAP_SIZE_MB}MB swap..."
fallocate -l "${SWAP_SIZE_MB}M" /swapfile || dd if=/dev/zero of=/swapfile bs=1M count="$SWAP_SIZE_MB"
chmod 0600 /swapfile
mkswap /swapfile
swapon /swapfile
if ! grep -q "/swapfile" /etc/fstab; then
echo "/swapfile none swap sw 0 0" >> /etc/fstab
fi
else
echo "[apex-bootstrap] WARNING: Insufficient free space on rootfs for swapfile. Skipping swap."
fi
fi
# 2. Storage Tiers & ZFS Configuration
mkdir -p /srv
for TIER_NUM in 1 2 3; do
TIER_PATH="/srv/tier-${TIER_NUM}.${APEX_REPO_NAME}"
DEV_VAR="APEX_TIER${TIER_NUM}_DEVICE"
DEV="${!DEV_VAR:-}"
if [ -n "$DEV" ] && [ -b "$DEV" ]; then
echo "[apex-bootstrap] Checking mapped tier $TIER_NUM on device: $DEV"
POOL_NAME="tier-${TIER_NUM}"
# Check if pool already exists on the device or in zfs
if zpool list -H -o name 2>/dev/null | grep -qw "$POOL_NAME"; then
echo "[apex-bootstrap] ZFS pool $POOL_NAME is already imported."
elif zpool import 2>/dev/null | grep -qw "$POOL_NAME"; then
echo "[apex-bootstrap] Importing existing ZFS pool $POOL_NAME..."
zpool import -f "$POOL_NAME"
else
echo "[apex-bootstrap] Creating new ZFS pool $POOL_NAME on $DEV..."
zpool create -f -o ashift=12 \
-O compression=lz4 \
-O atime=off \
-O mountpoint="$TIER_PATH" \
"$POOL_NAME" "$DEV"
fi
zfs set mountpoint="$TIER_PATH" "$POOL_NAME" || true
else
echo "[apex-bootstrap] Tier $TIER_NUM device unmapped. Using rootfs directory: $TIER_PATH"
mkdir -p "$TIER_PATH"
fi
done
# 3. User Directories & Permissions
mkdir -p /srv/docker
mkdir -p /home/adam/.local/bin /home/adam/.ssh
ln -sf /usr/local/bin/ufw-docker /home/adam/.local/bin/ufw-docker
echo "[apex-bootstrap] Setting permissions on /srv and home directory..."
chown -R adam:adam /srv /home/adam/.local /home/adam/.ssh
chmod 0700 /home/adam/.ssh
if [ -f /home/adam/.ssh/id_ed25519 ]; then
chmod 0600 /home/adam/.ssh/id_ed25519
fi
# 4. SSH Host Key Verification for GitHub
echo "[apex-bootstrap] Registering GitHub SSH host keys..."
ssh-keyscan -t ed25519,ecdsa,rsa github.com >> /home/adam/.ssh/known_hosts 2>/dev/null || true
chown adam:adam /home/adam/.ssh/known_hosts
chmod 0644 /home/adam/.ssh/known_hosts
# 5. Clone Repository
REPO_DIR="/srv/${APEX_REPO_NAME}"
if [ ! -d "$REPO_DIR/.git" ]; then
echo "[apex-bootstrap] Cloning repository $APEX_REPO_URL as user adam..."
sudo -u adam git clone "$APEX_REPO_URL" "$REPO_DIR"
else
echo "[apex-bootstrap] Repository already cloned at $REPO_DIR."
fi
# 6. Execute ./init.sh
INIT_SCRIPT="$REPO_DIR/init.sh"
if [ -f "$INIT_SCRIPT" ]; then
echo "[apex-bootstrap] Running ./init.sh from $REPO_DIR as user adam..."
sudo -u adam bash -c "cd '$REPO_DIR' && bash ./init.sh"
echo "[apex-bootstrap] ./init.sh completed successfully."
else
echo "[apex-bootstrap] No ./init.sh found in $REPO_DIR. Skipping."
fi
# 7. Complete & Mark Bootstrapped
touch "$MARKER"
echo "============================================================"
echo " [apex-bootstrap] Node bootstrap finished successfully! "
echo " Next steps: SSH as adam on port 2222, fill secrets & run: "
echo " apex tiers/link <tier1> <tier2> <tier3> "
echo " apex tiers/useradd && apex tiers/chown "
echo " apex configure && apex compose up "
echo "============================================================"
BOOTSTRAP_EOF
chmod 0755 /usr/local/bin/apex-bootstrap
echo "=== /usr/local/bin/apex-bootstrap baked successfully ==="
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
echo "=== [1/4] Purging APT Caches and Temp Files ==="
apt-get autoremove -y --purge
apt-get clean
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
echo "=== [2/4] Resetting Machine Identity for Clones ==="
truncate -s 0 /etc/machine-id
rm -f /var/lib/dbus/machine-id
ln -sf /etc/machine-id /var/lib/dbus/machine-id
mkdir -p /etc/systemd/system/ssh.service.d
cat << 'EOF_SSH_HOST' > /etc/systemd/system/ssh.service.d/10-generate-host-keys.conf
[Service]
ExecStartPre=-/usr/bin/ssh-keygen -A
EOF_SSH_HOST
ssh-keygen -A
echo "=== [3/4] Resetting Cloud-Init State & Bash History ==="
ufw delete allow 22/tcp 2>/dev/null || true
cloud-init clean --logs --seed || true
rm -rf /var/lib/cloud/*
rm -f /root/.bash_history /home/*/.bash_history
echo "=== [4/4] Syncing Filesystems ==="
sync
echo "=== Image Sanitization Complete ==="