feat(infra): add K3s GitOps infrastructure and Packer golden image build (#25)

### Summary of Changes

This PR introduces a declarative, GitOps-managed cloud infrastructure
for the monorepo using K3s, Flux v2, and HashiCorp Packer:

1. **Packer Golden Image Builder (`ci/packer/`)**:
- Automated Debian 12 minimal base OS image creation using QEMU builder,
UEFI firmware, GPT partition scheme, and 2M cluster size `.qcow2`
conversion.
- Cloud-init, `virtio` kernel drivers, and serial console (`ttyS0`,
115200 baud).
- Pre-installed K3s binary (`v1.31.2+k3s1`) with isolated first-boot
token and cert generation.
   - Full identity sanitization (`machine-id` and SSH host key resets).

2. **First-Boot Cloud-Init Bootstrapping
(`ci/cloud-init/user-data.yaml`)**:
- Configures K3s with `servicelb` disabled and TLS SANs for custom
domains and static IP.
- Waits for API readiness, initializes `flux-system` namespace, injects
`sops-age` secret, and triggers Flux sync.

3. **SOPS & GitOps Root Orchestration (`.sops.yaml`,
`ci/k8s/clusters/production/`)**:
- SOPS encryption configuration targeting Kubernetes secrets with `age`
key recipient.
- Flux v2 `GitRepository` and `Kustomization` manifests tracking `next`.

4. **Infrastructure & Networking (`ci/k8s/base/`,
`ci/k8s/infrastructure/`)**:
   - Dedicated `com-ermnvldmr` namespace.
- `cert-manager` HTTP-01 `ClusterIssuer` (`letsencrypt-prod` &
`letsencrypt-staging`).
- Traefik Ingress routing for `ermnvldmr.com`, `www.ermnvldmr.com`,
`docs.ermnvldmr.com`, and `analytics.ermnvldmr.com`.

5. **PostgreSQL & Applications (`ci/k8s/apps/`)**:
- PostgreSQL 16 StatefulSet on 5Gi `local-path` persistent storage with
low-memory tuning.
- Deployments for `www` (pinned `sha-6d33b71`), `docs` (pinned
`sha-91f5245`), and `umami` (pinned `postgresql-v2.14.0`) with
zero-downtime rolling update strategy.

6. **CI/CD Workflows (`.github/workflows/`)**:
- `ci - next/build-golden-image`: Workflow to build and upload
`com-ermnvldmr-debian-12-k3s-amd64-<tag>.qcow2` to static S3
`/dev/images/`.
- Updated `services-www-releases-dispatch-release.yml` and
`services-docs-next-required.yml` with GitOps deployment steps.
This commit is contained in:
deytenit authored and GitHub committed 2026-08-23 02:11:54 +03:00
1 parent 6d33b71405
commit 710873e72c
38 files changed
+1106 -28

No files matched your search

+156
View File
@@ -0,0 +1,156 @@
#cloud-config
preserve_hostname: false
hostname: com-ermnvldmr
ssh_pwauth: false
write_files:
- path: /etc/ssh/sshd_config.d/99-security-hardening.conf
owner: root:root
permissions: '0600'
content: |
PasswordAuthentication no
PermitRootLogin prohibit-password
KbdInteractiveAuthentication no
X11Forwarding no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
- path: /etc/sysctl.d/99-k3s-hardening.conf
owner: root:root
permissions: '0644'
content: |
# Network SYN Flood Protection
net.ipv4.tcp_syncookies = 1
# Disable ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
# Disable Source Routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
# Anti-spoofing (Reverse Path Filtering)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Required for K8s pod networking
net.ipv4.ip_forward = 1
# System Hardening
kernel.kptr_restrict = 2
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 512
- path: /etc/rancher/k3s/config.yaml
owner: root:root
permissions: '0600'
content: |
disable:
- servicelb
tls-san:
- "51.250.0.1" # Replace with actual static VPS IP upon provisioning
- "ermnvldmr.com"
- "www.ermnvldmr.com"
- "docs.ermnvldmr.com"
- "umami.ermnvldmr.com"
write-kubeconfig-mode: "0644"
- path: /usr/local/bin/bootstrap-cluster.sh
owner: root:root
permissions: '0755'
content: |
#!/usr/bin/env bash
set -euo pipefail
echo "=== [Bootstrap] Applying Sysctl Hardening ==="
sysctl --system
echo "=== [Bootstrap] Reloading Hardened SSH Configuration ==="
systemctl reload sshd || systemctl restart ssh || true
echo "=== [Bootstrap] Configuring UFW Firewall ==="
ufw default deny incoming
ufw default allow outgoing
ufw default allow routed
ufw allow 22/tcp comment 'SSH'
ufw allow 80/tcp comment 'HTTP Traefik'
ufw allow 443/tcp comment 'HTTPS Traefik'
# Allow Kubernetes Pod & Service traffic
ufw allow in on cni0 || true
ufw allow in on flannel.1 || true
ufw allow from 10.42.0.0/16 comment 'K3s Pod CIDR'
ufw allow from 10.43.0.0/16 comment 'K3s Service CIDR'
ufw --force enable
echo "=== [Bootstrap] Enabling Fail2ban ==="
systemctl enable --now fail2ban || true
echo "=== [Bootstrap] Starting K3s Service ==="
systemctl enable --now k3s.service
echo "=== [Bootstrap] Waiting for K3s API server ready ==="
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
until kubectl get nodes >/dev/null 2>&1; do
echo "Waiting for Kubernetes API..."
sleep 3
done
echo "=== [Bootstrap] Cluster node is online ==="
kubectl get nodes
echo "=== [Bootstrap] Creating flux-system namespace ==="
kubectl create namespace flux-system --dry-run=client -o yaml | kubectl apply -f -
echo "=== [Bootstrap] Injecting SOPS age decryption key ==="
if [ -f /root/.config/sops/age/keys.txt ]; then
kubectl create secret generic sops-age \
--namespace=flux-system \
--from-file=age.agekey=/root/.config/sops/age/keys.txt \
--dry-run=client -o yaml | kubectl apply -f -
echo "sops-age secret created."
else
echo "WARNING: /root/.config/sops/age/keys.txt not found. Inject key manually via kubectl."
fi
echo "=== [Bootstrap] Installing Flux Operator / Custom Resources ==="
kubectl apply -k https://github.com/fluxcd/flux2/manifests/install?ref=v2.4.0 || true
echo "=== [Bootstrap] Applying initial GitOps root sync ==="
kubectl apply -f - << 'EOF_GITOPS'
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: ermnvldmr-repo
namespace: flux-system
spec:
interval: 1m
url: https://github.com/deytenit/ermnvldmr.com.git
ref:
branch: next
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: cluster-sync
namespace: flux-system
spec:
interval: 5m
targetNamespace: flux-system
sourceRef:
kind: GitRepository
name: ermnvldmr-repo
path: ./ci/k8s/clusters/production
prune: true
wait: true
timeout: 2m
decryption:
provider: sops
secretRef:
name: sops-age
EOF_GITOPS
echo "=== [Bootstrap] First-Boot Bootstrap Sequence Complete ==="
runcmd:
- /usr/local/bin/bootstrap-cluster.sh
+48
View File
@@ -0,0 +1,48 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: docs
namespace: com-ermnvldmr
spec:
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: docs
template:
metadata:
labels:
app: docs
spec:
containers:
- name: docs
image: ghcr.io/deytenit/ermnvldmr.com/docs:sha-91f5245
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
cpu: 100m
memory: 128Mi
readinessProbe:
httpGet:
path: /en/
port: 80
initialDelaySeconds: 3
periodSeconds: 5
timeoutSeconds: 2
livenessProbe:
httpGet:
path: /en/
port: 80
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
+9
View File
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: com-ermnvldmr
resources:
- deployment.yaml
- service.yaml
images:
- name: ghcr.io/deytenit/ermnvldmr.com/docs
newTag: sha-91f5245
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: docs-service
namespace: com-ermnvldmr
spec:
type: ClusterIP
selector:
app: docs
ports:
- name: http
port: 80
targetPort: 80
+21
View File
@@ -0,0 +1,21 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-config
namespace: com-ermnvldmr
data:
postgresql.conf: |
# Low-memory tuned configuration for single-node K3s VM
listen_addresses = '*'
max_connections = 50
shared_buffers = 64MB
effective_cache_size = 128MB
maintenance_work_mem = 16MB
checkpoint_completion_target = 0.9
wal_buffers = 2MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = 4MB
min_wal_size = 80MB
max_wal_size = 512MB
+9
View File
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: com-ermnvldmr
resources:
- configmap.yaml
- secret.template.yaml
- pvc.yaml
- statefulset.yaml
- service.yaml
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: com-ermnvldmr
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 5Gi
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: Secret
metadata:
name: postgres-secrets
namespace: com-ermnvldmr
type: Opaque
stringData:
POSTGRES_DB: "umami"
POSTGRES_USER: "umami"
POSTGRES_PASSWORD: "CHANGE_ME_IN_ENCRYPTED_SOPS_SECRET"
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: postgres-service
namespace: com-ermnvldmr
spec:
type: ClusterIP
selector:
app: postgres
ports:
- name: postgres
port: 5432
targetPort: 5432
+83
View File
@@ -0,0 +1,83 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: com-ermnvldmr
spec:
serviceName: postgres-service
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-secrets
key: POSTGRES_DB
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secrets
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secrets
key: POSTGRES_PASSWORD
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- name: postgres
containerPort: 5432
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: config
mountPath: /etc/postgresql/postgresql.conf
subPath: postgresql.conf
readinessProbe:
exec:
command:
- pg_isready
- -U
- $(POSTGRES_USER)
- -d
- $(POSTGRES_DB)
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
exec:
command:
- pg_isready
- -U
- $(POSTGRES_USER)
- -d
- $(POSTGRES_DB)
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 5
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-pvc
- name: config
configMap:
name: postgres-config
+8
View File
@@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: umami-config
namespace: com-ermnvldmr
data:
DATABASE_TYPE: "postgresql"
CLIENT_IP_HEADER: "X-Forwarded-For"
+53
View File
@@ -0,0 +1,53 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: umami
namespace: com-ermnvldmr
spec:
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: umami
template:
metadata:
labels:
app: umami
spec:
containers:
- name: umami
image: ghcr.io/umami-software/umami:postgresql-v2.14.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 3000
envFrom:
- configMapRef:
name: umami-config
- secretRef:
name: umami-secrets
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
readinessProbe:
httpGet:
path: /api/heartbeat
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /api/heartbeat
port: 3000
initialDelaySeconds: 20
periodSeconds: 20
timeoutSeconds: 3
+11
View File
@@ -0,0 +1,11 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: com-ermnvldmr
resources:
- configmap.yaml
- secret.template.yaml
- deployment.yaml
- service.yaml
images:
- name: ghcr.io/umami-software/umami
newTag: postgresql-v2.14.0
+9
View File
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: umami-secrets
namespace: com-ermnvldmr
type: Opaque
stringData:
APP_SECRET: "CHANGE_ME_IN_ENCRYPTED_SOPS_SECRET"
DATABASE_URL: "postgresql://umami:CHANGE_ME@postgres-service.com-ermnvldmr.svc.cluster.local:5432/umami"
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: umami-service
namespace: com-ermnvldmr
spec:
type: ClusterIP
selector:
app: umami
ports:
- name: http
port: 3000
targetPort: 3000
+48
View File
@@ -0,0 +1,48 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: www
namespace: com-ermnvldmr
spec:
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: www
template:
metadata:
labels:
app: www
spec:
containers:
- name: www
image: ghcr.io/deytenit/ermnvldmr.com/www:sha-6d33b71
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
cpu: 100m
memory: 128Mi
readinessProbe:
httpGet:
path: /en/
port: 80
initialDelaySeconds: 3
periodSeconds: 5
timeoutSeconds: 2
livenessProbe:
httpGet:
path: /en/
port: 80
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
+9
View File
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: com-ermnvldmr
resources:
- deployment.yaml
- service.yaml
images:
- name: ghcr.io/deytenit/ermnvldmr.com/www
newTag: sha-6d33b71
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: www-service
namespace: com-ermnvldmr
spec:
type: ClusterIP
selector:
app: www
ports:
- name: http
port: 80
targetPort: 80
@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: com-ermnvldmr
labels:
name: com-ermnvldmr
@@ -0,0 +1,30 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: ermnvldmr-repo
namespace: flux-system
spec:
interval: 1m
url: https://github.com/deytenit/ermnvldmr.com.git
ref:
branch: next
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: cluster-sync
namespace: flux-system
spec:
interval: 5m
targetNamespace: flux-system
sourceRef:
kind: GitRepository
name: ermnvldmr-repo
path: ./ci/k8s/clusters/production
prune: true
wait: true
timeout: 3m
decryption:
provider: sops
secretRef:
name: sops-age
@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gotk-sync.yaml
@@ -0,0 +1,12 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- flux-system
- ../../base/namespaces
- ../../infrastructure/cert-manager
- ../../infrastructure/networking
- ../../apps/postgres
- ../../apps/umami
- ../../apps/www
- ../../apps/docs
@@ -0,0 +1,29 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@ermnvldmr.com
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
class: traefik
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@ermnvldmr.com
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- http01:
ingress:
class: traefik
@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- cluster-issuer.yaml
@@ -0,0 +1,63 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: main-ingress
namespace: com-ermnvldmr
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd
spec:
ingressClassName: traefik
tls:
- hosts:
- ermnvldmr.com
- www.ermnvldmr.com
secretName: ermnvldmr-tls
- hosts:
- docs.ermnvldmr.com
secretName: docs-ermnvldmr-tls
- hosts:
- umami.ermnvldmr.com
secretName: umami-ermnvldmr-tls
rules:
- host: ermnvldmr.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: www-service
port:
number: 80
- host: www.ermnvldmr.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: www-service
port:
number: 80
- host: docs.ermnvldmr.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: docs-service
port:
number: 80
- host: umami.ermnvldmr.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: umami-service
port:
number: 3000
@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ingress.yaml
+75
View File
@@ -0,0 +1,75 @@
packer {
required_plugins {
qemu = {
version = ">= 1.1.0"
source = "github.com/hashicorp/qemu"
}
}
}
variable "debian_iso_url" {
type = string
default = "https://cdimage.debian.org/cdimage/archive/12.8.0/amd64/iso-cd/debian-12.8.0-amd64-netinst.iso"
}
variable "debian_iso_checksum" {
type = string
default = "file:https://cdimage.debian.org/cdimage/archive/12.8.0/amd64/iso-cd/SHA512SUMS"
}
variable "vm_name" {
type = string
default = "com-ermnvldmr-debian-12-k3s-amd64.qcow2"
}
source "qemu" "debian_k3s" {
iso_url = var.debian_iso_url
iso_checksum = var.debian_iso_checksum
output_directory = "output-qemu"
shutdown_command = "echo 'packer' | sudo -S shutdown -P now"
disk_size = "10G"
format = "qcow2"
accelerator = "kvm"
http_directory = "ci/packer/http"
ssh_username = "root"
ssh_password = "packer"
ssh_timeout = "25m"
vm_name = var.vm_name
net_device = "virtio-net"
disk_interface = "virtio"
efi_boot = true
qemuargs = [
["-m", "2048M"],
["-smp", "2"],
["-serial", "mon:stdio"]
]
boot_wait = "5s"
boot_command = [
"<wait><wait><wait>c<wait>",
"linux /install.amd/vmlinuz auto=true priority=critical preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg --- quiet<enter>",
"initrd /install.amd/initrd.gz<enter>",
"boot<enter>"
]
}
build {
sources = ["source.qemu.debian_k3s"]
provisioner "shell" {
scripts = [
"ci/packer/scripts/01-base-system.sh",
"ci/packer/scripts/02-install-k3s.sh",
"ci/packer/scripts/03-cleanup.sh"
]
}
post-processor "shell-local" {
inline = [
"echo '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/debian-12-k3s-optimized.qcow2",
"mv output-qemu/debian-12-k3s-optimized.qcow2 output-qemu/${var.vm_name}"
]
}
}
+54
View File
@@ -0,0 +1,54 @@
d-i debian-installer/locale string en_US.UTF-8
d-i keyboard-configuration/xkb-keymap select us
d-i netcfg/choose_interface select auto
d-i netcfg/get_hostname string debian-k3s
d-i netcfg/get_domain string local
d-i mirror/country string manual
d-i mirror/http/hostname string deb.debian.org
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string
d-i passwd/root-login boolean true
d-i passwd/root-password password packer
d-i passwd/root-password-again password packer
d-i passwd/make-user boolean false
d-i clock-setup/utc boolean true
d-i time/zone string UTC
d-i partman-auto/method string regular
d-i partman-auto/disk string /dev/vda
d-i partman-auto/choose_recipe select gpt-boot-root
d-i partman-auto/expert_recipe string \
gpt-boot-root :: \
512 512 512 fat32 \
$primary{ } \
$bootable{ } \
method{ efi } \
format{ } \
. \
1024 10000 -1 ext4 \
$primary{ } \
method{ format } format{ } \
use_filesystem{ } filesystem{ ext4 } \
mountpoint{ / } \
.
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i apt-setup/non-free-firmware boolean true
d-i apt-setup/non-free boolean true
d-i apt-setup/contrib boolean true
tasksel tasksel/first multiselect standard, ssh-server
d-i pkgsel/include string sudo curl ca-certificates qemu-guest-agent cloud-init
d-i pkgsel/upgrade select full-upgrade
d-i grub-installer/only_debian boolean true
d-i grub-installer/bootdev string default
d-i finish-install/reboot_in_progress note
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
echo "=== Configuring Base System for Cloud Image ==="
# Install essential kernel modules and utilities
apt-get update
apt-get install -y --no-install-recommends \
cloud-init \
qemu-guest-agent \
linux-image-amd64 \
socat \
conntrack \
ipset \
iptables \
ufw \
fail2ban \
unattended-upgrades \
curl \
ca-certificates \
gnupg \
tar \
gzip
# Ensure virtio kernel modules are loaded on boot
cat << 'EOF' > /etc/modules-load.d/virtio.conf
virtio
virtio_net
virtio_blk
virtio_pci
virtiofs
EOF
# Configure GRUB for Serial Console (ttyS0 / COM1) and predictable network names
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
echo 'GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"' >> /etc/default/grub
update-grub
# Enable serial console getty
systemctl enable serial-getty@ttyS0.service || true
# Configure cloud-init to use NoCloud and ConfigDrive datasources
cat << 'EOF' > /etc/cloud/cloud.cfg.d/99-cloud-init.cfg
datasource_list: [ NoCloud, ConfigDrive, None ]
datasource:
NoCloud:
fs_label: cidata
EOF
# Ensure DHCP on eth0
cat << 'EOF' > /etc/network/interfaces.d/eth0
auto eth0
iface eth0 inet dhcp
EOF
echo "=== Base System Configuration Complete ==="
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
echo "=== Installing K3s Binary (First-Boot Isolation) ==="
export INSTALL_K3S_SKIP_ENABLE="true"
export INSTALL_K3S_SKIP_START="true"
export INSTALL_K3S_VERSION="v1.31.2+k3s1"
# Download official K3s installer and install binary/systemd unit without starting
curl -sfL https://get.k3s.io | sh -
# Verify k3s binary is located in /usr/local/bin
which k3s
k3s --version
# Ensure systemd service is NOT running
systemctl stop k3s.service || true
systemctl disable k3s.service || true
# Create configuration directory
mkdir -p /etc/rancher/k3s
echo "=== K3s Pre-installation Complete ==="
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
echo "=== Sanitizing Machine Identity & Temporary Files ==="
# Clean APT caches and logs
apt-get autoremove -y --purge
apt-get clean
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Reset machine-id for unique DHCP lease acquisition on clone
truncate -s 0 /etc/machine-id
rm -f /var/lib/dbus/machine-id
ln -sf /etc/machine-id /var/lib/dbus/machine-id
# Remove generated SSH host keys (re-created by cloud-init on first boot)
rm -f /etc/ssh/ssh_host_*
# Clean cloud-init logs and state
cloud-init clean --logs || true
rm -rf /var/lib/cloud/instances/*
# Clean bash history
rm -f /root/.bash_history /home/*/.bash_history
# Sync filesystem
sync
echo "=== Cleanup Complete ==="
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
set -euo pipefail
SERVICE=$1
TAG=$2
if [ -z "$SERVICE" ] || [ -z "$TAG" ]; then
echo "Usage: $0 <service: www|docs|umami> <tag>"
exit 1
fi
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
APP_DIR="$ROOT_DIR/ci/k8s/apps/$SERVICE"
if [ ! -d "$APP_DIR" ]; then
echo "Error: Application directory '$APP_DIR' does not exist."
exit 1
fi
echo "Updating Kustomize image tag for '$SERVICE' to '$TAG'..."
cd "$APP_DIR"
kustomize edit set image "ghcr.io/deytenit/ermnvldmr.com/${SERVICE}=ghcr.io/deytenit/ermnvldmr.com/${SERVICE}:${TAG}"
cd "$ROOT_DIR"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add "ci/k8s/apps/${SERVICE}/kustomization.yaml"
if git diff --staged --quiet; then
echo "No manifest changes to commit for $SERVICE."
else
git commit -m "chore(release): deploy ${SERVICE} ${TAG} [skip ci]"
git push origin next
echo "Successfully deployed $SERVICE ($TAG) via GitOps."
fi