Compare commits

...
37 Commits
Author SHA1 Message Date
ahmza 0af896ab02 fix(lidarr): expand config volume to 10Gi 2026-10-23 09:45:41 +01:00
ahmza 4238d82b40 Add an import mount to lidarr for music imports 2026-10-23 07:52:00 +00:00
ahmza adb5b9524b Upgrade Authentik to 2026.8.5 2026-10-22 19:44:05 +00:00
ahmza 36ad140f66 Upgrade Authentik to 2026.8.4 2026-10-22 19:34:37 +00:00
ahmza 36aba16de3 Upgrade Authentik to 2026.5.6 2026-10-22 19:26:36 +00:00
ahmza 038b3396ab Use Recreate strategy for Authentik 2026-10-22 17:55:26 +00:00
ahmza ffd5bde344 Upgrade Authentik to 2026.2.3 2026-10-22 17:42:32 +00:00
ahmza 2f0e10a10b bump soulsync to 3.5.9, removed some media library types 2026-10-22 09:17:25 +00:00
ahmza f3c0cf7060 added download NFS mountpoints 2026-10-22 08:18:44 +00:00
ahmza b45f9a19a2 deploy lidarr 2026-10-22 08:08:53 +00:00
ahmza e9b199aca0 updated entry for auth.ahmza.com as well as info for ingress one being the gateway for netbird 2026-10-16 23:06:30 +00:00
ahmza c1437d9199 Complete Authentik Kubernetes cutover 2026-10-16 17:05:38 +00:00
ahmza 9b5dff602a Require encryption for PostgreSQL backups 2026-10-16 17:03:41 +00:00
ahmza 1f26627488 Begin Authentik final cutover 2026-10-16 16:50:43 +00:00
ahmza db3c8e4694 Enable production TLS for Authentik 2026-10-16 16:46:10 +00:00
ahmza a8431d15c6 Fix cert-manager checks with split DNS 2026-10-16 16:42:46 +00:00
ahmza 0663196fa0 Expose Authentik through staging HTTPS 2026-10-16 16:38:43 +00:00
ahmza 5101df740d Start isolated Authentik trial server 2026-10-16 16:27:46 +00:00
ahmza f56c6719da Stage Authentik database and backups 2026-10-16 16:03:02 +00:00
ahmza 019039126a Add CloudNativePG for Authentik 2026-10-16 15:50:56 +00:00
ahmza aa1bb9d838 scrape Longhorn metrics 2026-10-16 02:17:40 +00:00
ahmza faebef6c58 use internal DNS name for VictoriaMetrics endpoint 2026-10-16 01:39:57 +00:00
ahmza f79691f838 skip Talos localhost-only control-plane metric scrapes 2026-10-16 01:07:28 +00:00
ahmza c7fce94b04 add VictoriaMetrics cluster metrics collection 2026-10-16 00:56:05 +00:00
ahmza 6f43b5e343 add NetBird connectivity to Talos nodes 2026-10-16 00:51:47 +00:00
ahmza fc195c5379 remove temporary credential recovery job 2026-10-15 23:17:52 +00:00
ahmza e9574e3317 add temporary credential recovery job 2026-10-15 23:09:09 +00:00
ahmza 2ba48aa313 update soulsync to 3.5.4 2026-10-14 15:50:12 +00:00
ahmza fbae9b0fc3 changed volume names to fit standard 2026-10-09 13:33:22 +00:00
ahmza 029ff24479 added extra download paths for usenet and torrent to match indexer as well as download paths 2026-10-09 13:31:53 +00:00
ahmza 0b13c02899 fix type in nfs mount from Audiobooks to AudioBooks 2026-10-09 10:46:47 +00:00
ahmza 4ddc5e0151 added soulsync 3.5.3 2026-10-09 10:41:17 +00:00
gitea-actions[bot] 918435ac7c Deploy mcpacks 2026-09-11 20:24:08 +00:00
gitea-actions[bot] 2d6e6a69e5 Deploy mcpacks 2026-09-11 18:40:03 +00:00
gitea-actions[bot] 19ec6a37b2 Deploy mcpacks 2026-09-11 18:16:44 +00:00
ahmza 397c4ec5f1 move to letsencrypt-prod cert 2026-09-11 10:26:12 +00:00
ahmza 6e49cc860c Fix Bazarr NFS media mounts 2026-09-11 10:21:07 +00:00
33 changed files with 1069 additions and 63 deletions
+32
View File
@@ -0,0 +1,32 @@
# Authentik is deployed from its official Helm chart, while this repository's
# wrapper chart also owns its PostgreSQL cluster, backups, and persistent data.
# The database is created first with Authentik disabled so the existing Docker
# database can be restored before any new server or worker touches it.
#
# Deliberately NO resources-finalizer.argocd.argoproj.io here. This Application
# owns identity data, and deleting its Git entry must not cascade into deleting
# PostgreSQL or Authentik's uploaded files.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: authentik
namespace: argocd
spec:
project: default
source:
repoURL: https://tea.ahmza.com/ahmza.com/k8s-starter-pack.git
targetRevision: main
path: apps/authentik
destination:
server: https://kubernetes.default.svc
namespace: authentik
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
+16
View File
@@ -0,0 +1,16 @@
# The upstream Authentik chart is initially disabled. This wrapper first
# creates PostgreSQL, backup configuration, and persistent file storage so the
# existing installation can be restored before Authentik starts.
apiVersion: v2
name: authentik-app
description: Deploys Authentik with CloudNativePG and off-cluster backups
version: 0.1.0
type: application
dependencies:
- name: authentik
# Pin each release explicitly: Authentik database migrations require
# sequential, verified upgrades through every release train.
version: 2026.8.5
repository: https://charts.goauthentik.io
condition: authentikDeployment.enabled
+18
View File
@@ -0,0 +1,18 @@
# Authentik keeps uploaded icons and branding under /data. The PostgreSQL dump
# does not include these files, so this separate Longhorn volume is restored
# from the source VM's data/ directory before Authentik is enabled.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: authentik-data
namespace: {{ .Release.Namespace }}
annotations:
# Uploaded files are identity-service state. Never let Argo CD prune the
# volume automatically because a template was renamed or removed.
argocd.argoproj.io/sync-options: Prune=false
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
+103
View File
@@ -0,0 +1,103 @@
# Backups live outside the cluster so a Longhorn or cluster-wide failure does
# not remove both PostgreSQL and its recovery data. Credentials are produced by
# the encrypted SopsSecret in this chart.
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: authentik-backups
namespace: {{ .Release.Namespace }}
spec:
retentionPolicy: 30d
configuration:
destinationPath: s3://ahmza-talos-cnpg-backups/authentik/
endpointURL: https://s3.eu-central-003.backblazeb2.com
s3Credentials:
accessKeyId:
name: backblaze-b2-credentials
key: ACCESS_KEY_ID
secretAccessKey:
name: backblaze-b2-credentials
key: ACCESS_SECRET_KEY
region:
name: backblaze-b2-credentials
key: AWS_REGION
wal:
compression: gzip
# Require Backblaze's S3-compatible SSE-B2 encryption rather than
# relying only on the bucket's default encryption policy.
encryption: AES256
maxParallel: 2
data:
compression: gzip
encryption: AES256
jobs: 2
# Backblaze implements the S3 API but does not require every optional AWS
# checksum behavior. These settings avoid incompatible optional checksums.
instanceSidecarConfiguration:
env:
- name: AWS_REQUEST_CHECKSUM_CALCULATION
value: when_required
- name: AWS_RESPONSE_CHECKSUM_VALIDATION
value: when_required
---
# One PostgreSQL instance is enough initially: Longhorn replicates its volume
# across workers and can reattach it after a node failure. A second database
# instance can be added later if shorter failover time justifies another full
# copy of the data and WAL.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: authentik-postgres
namespace: {{ .Release.Namespace }}
annotations:
# Argo CD may update this database but must never delete it automatically
# merely because a manifest or parent Application was removed from Git.
argocd.argoproj.io/sync-options: Prune=false
spec:
instances: 1
imageName: >-
ghcr.io/cloudnative-pg/postgresql:16.13-202603020816-standard-trixie
bootstrap:
initdb:
database: authentik
owner: authentik
storage:
# The default StorageClass is Longhorn. Omitting storageClass here keeps the
# database on that established replicated storage path.
size: 10Gi
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
plugins:
- name: barman-cloud.cloudnative-pg.io
# CloudNativePG defaults this to true. Keeping it explicit prevents Argo
# CD from reporting harmless drift between Git and the live resource.
enabled: true
isWALArchiver: true
parameters:
barmanObjectName: authentik-backups
---
# CloudNativePG cron expressions have six fields; the leading zero is seconds.
# WAL is archived continuously, while this creates a full base backup nightly.
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: authentik-postgres-daily
namespace: {{ .Release.Namespace }}
spec:
schedule: "0 0 2 * * *"
immediate: false
backupOwnerReference: self
cluster:
name: authentik-postgres
method: plugin
pluginConfiguration:
name: barman-cloud.cloudnative-pg.io
@@ -0,0 +1,30 @@
apiVersion: isindir.github.com/v1alpha3
kind: SopsSecret
metadata:
name: authentik-secrets
namespace: authentik
spec:
secretTemplates:
- name: authentik-secrets
data:
AUTHENTIK_SECRET_KEY: ENC[AES256_GCM,data:xwd512Zty1VmOcc79UK2MxgLG5PaSApHhhwZRkqNz2w6HbLMSx+QvbODTfIZ0pq+HfQHxQMTY5FeX7t6TKR9toI5tG6B5202Bf85U9Lph4ssVfGC7fU/2WBsdELmtJIxxPjrzpcoQBfybOaR,iv:/J5e8Ej1KmuiEaOm8lZkikx7eStmScUA6lc0civVpdY=,tag:+gxczl/b3K4KnF8LcSkHKQ==,type:str]
- name: backblaze-b2-credentials
data:
ACCESS_KEY_ID: ENC[AES256_GCM,data:4FjHGgdeDwbA22ekQr1jv2qDn+5xxEMqgB1wWanL8nXvt8EV,iv:csE6cZ0ifFFC0bq12eMGBwSuchH0u1lqbXflywoR9FY=,tag:xMhjAvxicV1nUpfBfJAKBw==,type:str]
ACCESS_SECRET_KEY: ENC[AES256_GCM,data:XWHpbgZWbfl3REQDraRMUemcRvoACdaE2hjgkcYFgpZGkSZQ3jO7/fg4tDo=,iv:bGvTZy9N/ooxykqki0WkaUMI5qORbceSjprLnUu2k1A=,tag:67LlMIj+wvhj++Li/WtEAw==,type:str]
AWS_REGION: ENC[AES256_GCM,data:TL4IGaKv2/LD1/PTL1JY0l/J+N8=,iv:JZtM2V0nwimMJaR5o7qK9Rx1FC6jTuFbknU/2qBrwYs=,tag:tUgyYeAYjEGxTAlnky679w==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4WDgwRkExNHhXa05hSkcy
ZVVUam44a0pVR2hTZk5oOTdWdzkvSGlWODJVCnp6REpkUWVBV2g0MXNVKzdCVG1t
aE9YQ25icitBWGJqQ3NtUjd1cCtRUk0KLS0tIHNPeFVkeFVGUlYxT3Nhd2UrVnl2
NlhTZFV1YVRSM2JJYk1WRnRiUFppMXMKtqNp3OzKTeBKWXos0+wUmn6YGEDXubtc
UPX7lOkLI6qaKg8+3HZV3x9yKgtaT32WF9faMBQhvxFMrhRR3UTTlQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1udejf5qa66rd6yvj5r8dz0fjnqfh0quf4t339c3cpqr26mkx2ydq3zel07
encrypted_regex: ^(data|stringData)$
lastmodified: "2026-10-16T15:59:11Z"
mac: ENC[AES256_GCM,data:bqT+3/x/1DAgRRxjIXlxwNLiXT2f0rD7WxUk6RGBikOD0sZKBvCZVh4qTEVUIf2U6siZBgue39Gqa5eBlCCzx+rq/JxbKdsNd5klythOzmuaKSlOaIimSZLR9Ye1KJMGPkZI4G7eE5CAM17WJ5MMZDkhP9CtscRPEQ/AgHbVUSg=,iv:JqtI7n6uMkQ9lKr4oGGEA6czFtvoDrG510aLmltCdmI=,tag:1LU4a4Mlrxh8btQw4MY0eQ==,type:str]
version: 3.13.3
+135
View File
@@ -0,0 +1,135 @@
# The trial server is enabled only after the source database and files have
# been restored. Its worker and Ingress remain disabled until cutover, which
# prevents background jobs and production traffic reaching this clone.
authentikDeployment:
enabled: true
authentik:
global:
# Server and worker share one ReadWriteOnce Longhorn volume. Recreate stops
# the old Pods before replacements start, allowing the volume to move nodes
# instead of leaving a rolling update blocked by a multi-attach error.
deploymentStrategy:
type: Recreate
# CloudNativePG generates this connection Secret. Referencing individual
# keys avoids duplicating its generated database password in Git or SOPS.
env:
- name: AUTHENTIK_POSTGRESQL__HOST
valueFrom:
secretKeyRef:
name: authentik-postgres-app
key: host
- name: AUTHENTIK_POSTGRESQL__PORT
valueFrom:
secretKeyRef:
name: authentik-postgres-app
key: port
- name: AUTHENTIK_POSTGRESQL__NAME
valueFrom:
secretKeyRef:
name: authentik-postgres-app
key: dbname
- name: AUTHENTIK_POSTGRESQL__USER
valueFrom:
secretKeyRef:
name: authentik-postgres-app
key: username
- name: AUTHENTIK_POSTGRESQL__PASSWORD
valueFrom:
secretKeyRef:
name: authentik-postgres-app
key: password
# `require` encrypts traffic to PostgreSQL without depending on a public
# CA; CloudNativePG rotates its private in-cluster certificates itself.
- name: AUTHENTIK_POSTGRESQL__SSLMODE
value: require
# Authentik 2026.8 only honors forwarded HTTPS and client-address headers
# from explicit trusted proxies. This is the smallest aggregate covering
# all current node Pod subnets, so ingress-nginx stays trusted if moved.
- name: AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS
value: 10.244.0.0/21
# The same Longhorn claim carries three restored source directories. Each
# subPath is mounted where the upstream container expects it.
volumes:
- name: authentik-data
persistentVolumeClaim:
claimName: authentik-data
volumeMounts:
- name: authentik-data
mountPath: /data
subPath: data
- name: authentik-data
mountPath: /templates
subPath: custom-templates
- name: authentik-data
mountPath: /certs
subPath: certs
authentik:
# This SOPS-managed Secret contains the source installation's exact secret
# key, preserving encrypted configuration and existing identity semantics.
existingSecret:
secretName: authentik-secrets
# CloudNativePG supplies PostgreSQL; do not deploy the bundled database.
postgresql:
enabled: false
server:
# One server is sufficient for this homelab; Kubernetes recreates it if the
# Pod fails, while PostgreSQL and uploaded files live on persistent storage.
replicas: 1
# UID/GID 1000 matches the restored Docker files and satisfies this
# namespace's restricted Pod Security policy without running as root.
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
# Route the LAN hostname through the cluster's shared ingress-nginx IP.
# DNS-01 issuance and HTTPS routing were proven against Let's Encrypt's
# staging service before switching to this browser-trusted issuer.
ingress:
enabled: true
ingressClassName: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- auth.ahmza.com
tls:
- secretName: authentik-tls
hosts:
- auth.ahmza.com
worker:
# One worker handles scheduled jobs, source synchronization, email, and
# management of Kubernetes-native outposts.
replicas: 1
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
+16 -8
View File
@@ -37,20 +37,28 @@ spec:
volumeMounts: volumeMounts:
- name: bazarr-config - name: bazarr-config
mountPath: /config mountPath: /config
- name: cryo-media # Mount each media directory directly instead of using subPath.
# The NFS export root is not listable by root-squashed clients, so
# kubelet cannot safely prepare subPath mounts from that root.
- name: movies
mountPath: /movies mountPath: /movies
subPath: Movies - name: tv
- name: cryo-media
mountPath: /tv mountPath: /tv
subPath: TVShows - name: anime
- name: cryo-media
mountPath: /anime mountPath: /anime
subPath: Anime
volumes: volumes:
- name: bazarr-config - name: bazarr-config
persistentVolumeClaim: persistentVolumeClaim:
claimName: bazarr-data claimName: bazarr-data
- name: cryo-media - name: movies
nfs: nfs:
server: nas.ahmza.com server: nas.ahmza.com
path: /srv/media path: /srv/media/Movies
- name: tv
nfs:
server: nas.ahmza.com
path: /srv/media/TVShows
- name: anime
nfs:
server: nas.ahmza.com
path: /srv/media/Anime
+1 -1
View File
@@ -5,7 +5,7 @@ metadata:
name: bazarr name: bazarr
namespace: bazarr namespace: bazarr
annotations: annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging cert-manager.io/cluster-issuer: letsencrypt-prod
spec: spec:
tls: tls:
- hosts: - hosts:
+8
View File
@@ -7,3 +7,11 @@ cert-manager:
# separate manual step. `installCRDs` is the older, deprecated name for # separate manual step. `installCRDs` is the older, deprecated name for
# this same setting. # this same setting.
enabled: true enabled: true
# Technitium serves a private, split-horizon ahmza.com zone, while DNS-01
# challenge TXT records are created in the public Cloudflare zone. Use
# public resolvers for ACME self-checks so cert-manager sees the same TXT
# record Let's Encrypt sees; ordinary Pods still use normal cluster DNS.
extraArgs:
- --dns01-recursive-nameservers-only
- --dns01-recursive-nameservers=1.1.1.1:53,8.8.8.8:53
+34
View File
@@ -0,0 +1,34 @@
# CloudNativePG manages PostgreSQL databases as Kubernetes resources. The
# companion Barman Cloud plugin sends database backups and transaction logs to
# object storage; installing both here gives future apps one shared database
# operator without forcing them to share one PostgreSQL server.
#
# Deliberately NO resources-finalizer.argocd.argoproj.io here. PostgreSQL
# clusters managed by this operator hold application data, so removing this
# Application must not cascade into a surprise deletion of database machinery.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cloudnative-pg
namespace: argocd
spec:
project: default
source:
repoURL: https://tea.ahmza.com/ahmza.com/k8s-starter-pack.git
targetRevision: main
path: apps/cloudnative-pg
destination:
server: https://kubernetes.default.svc
namespace: cnpg-system
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
# Both dependencies install cluster-wide CRDs. Server-side apply avoids
# annotation-size limits and lets Kubernetes handle CRD fields safely.
- ServerSideApply=true
+19
View File
@@ -0,0 +1,19 @@
# One umbrella chart installs the PostgreSQL operator and its current backup
# plugin together. Applications create their own independent PostgreSQL
# Cluster resources; these shared controllers hold no application data.
apiVersion: v2
name: cloudnative-pg-app
description: >-
Installs CloudNativePG and the Barman Cloud object-storage backup plugin
version: 0.1.0
type: application
dependencies:
- name: cloudnative-pg
# Pin versions so an unrelated Git sync cannot silently upgrade the
# controller responsible for every PostgreSQL database in the cluster.
version: 0.29.1
repository: https://cloudnative-pg.github.io/charts
- name: plugin-barman-cloud
version: 0.8.1
repository: https://cloudnative-pg.github.io/charts
+27
View File
@@ -0,0 +1,27 @@
# This Argo CD Application deploys Lidarr to manage the music library.
# It uses Hotio's lightweight Lidarr container image.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: lidarr
namespace: argocd
# Omit the cascading finalizer so deleting this Application does not
# automatically delete Lidarr's config-bearing PersistentVolumeClaim.
spec:
project: default
source:
repoURL: https://tea.ahmza.com/ahmza.com/k8s-starter-pack.git
targetRevision: main
path: apps/lidarr
destination:
server: https://kubernetes.default.svc
namespace: lidarr
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+70
View File
@@ -0,0 +1,70 @@
# Lidarr image maintained by Hotio: ghcr.io/hotio/lidarr.
apiVersion: apps/v1
kind: Deployment
metadata:
name: lidarr
namespace: lidarr
spec:
# not critical infrastructure a single pod should be sufficient
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: lidarr
template:
metadata:
labels:
app: lidarr
spec:
containers:
- name: lidarr
image: ghcr.io/hotio/lidarr:release
imagePullPolicy: Always
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: UMASK
value: "002"
- name: TZ
value: "Europe/London"
ports:
- name: http
containerPort: 8686
protocol: TCP
volumeMounts:
- name: lidarr-config
mountPath: /config
# Mount each media directory directly instead of using subPath.
# The NFS export root is not listable by root-squashed clients, so
# kubelet cannot safely prepare subPath mounts from that root.
- name: music
mountPath: /music
- name: torrents
mountPath: /data/torrents
- name: usenet
mountPath: /data/usenet
- name: import
mountPath: /data/import
volumes:
- name: lidarr-config
persistentVolumeClaim:
claimName: lidarr-data
- name: music
nfs:
server: nas.ahmza.com
path: /srv/media/Music
- name: torrents
nfs:
server: nas.ahmza.com
path: /srv/media/torrents
- name: usenet
nfs:
server: nas.ahmza.com
path: /srv/media/usenet
- name: import
nfs:
server: nas.ahmza.com
path: /srv/media/import
+25
View File
@@ -0,0 +1,25 @@
# Route lidarr.ahmza.com through ingress-nginx (docs/11-ingress.md).
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: lidarr
namespace: lidarr
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- lidarr.ahmza.com
# cert-manager keeps this up to date. nothing to do by hand
secretName: lidarr-tls
rules:
- host: lidarr.ahmza.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: lidarr
port:
number: 8686
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: lidarr
+13
View File
@@ -0,0 +1,13 @@
# Store Lidarr's config on a Longhorn volume (see docs/06-longhorn.md).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: lidarr-data
namespace: lidarr
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
# Album artwork and the music database outgrew the original 2Gi volume.
storage: 10Gi
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: lidarr
namespace: lidarr
spec:
selector:
app: lidarr
ports:
- name: http
port: 8686
targetPort: http
+1 -1
View File
@@ -47,7 +47,7 @@ spec:
# The publishing workflow replaces this bootstrap tag with an # The publishing workflow replaces this bootstrap tag with an
# immutable OCI digest. Never deploy the mutable latest tag after # immutable OCI digest. Never deploy the mutable latest tag after
# bootstrap: recording the digest makes rollbacks and audits reliable. # bootstrap: recording the digest makes rollbacks and audits reliable.
image: tea.ahmza.com/ahmza.com/mcpacks-site@sha256:30a98b5835fb11ee183077cef0a638602a8b975c7e377abb125159f0eaed197e image: tea.ahmza.com/ahmza.com/mcpacks-site@sha256:c9983dd9c31562f6518388f4d66f308e00cc229264b248536db8b4be7e7845db
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- name: http - name: http
+26
View File
@@ -0,0 +1,26 @@
# This ArgoCD application deploys soulsync to manage media discovery.
# The Deployment pins a reviewed SoulSync release for repeatable GitOps syncs.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: soulsync
namespace: argocd
# skipping resources-finalizer.argocd.argoproj.io as soulsync will create configs
spec:
project: default
source:
repoURL: https://tea.ahmza.com/ahmza.com/k8s-starter-pack.git
targetRevision: main
path: apps/soulsync
destination:
server: https://kubernetes.default.svc
namespace: soulsync
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+117
View File
@@ -0,0 +1,117 @@
# Pin SoulSync so an upstream `latest` update cannot change the running app
# without a reviewed Git commit.
apiVersion: apps/v1
kind: Deployment
metadata:
name: soulsync
namespace: soulsync
spec:
# not critical infra, a single pod should suffice
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: soulsync
template:
metadata:
labels:
app: soulsync
spec:
containers:
- name: soulsync
image: boulderbadgedad/soulsync:3.5.9
imagePullPolicy: IfNotPresent
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: UMASK
value: "022"
- name: PYTHONPATH
value: "/app"
- name: SOULSYNC_CONFIG_PATH
value: "/app/config/config.json"
- name: TZ
value: "Europe/London"
ports:
# HTTPS terminates at ingress-nginx, which forwards the UI and both
# OAuth callback paths to SoulSync's main web server on this port.
- name: http
containerPort: 8008
protocol: TCP
# Kubernetes ignores the Docker image's HEALTHCHECK, so define probes
# here to keep traffic away until SoulSync has actually started and to
# restart it if its web server later stops responding.
startupProbe:
httpGet:
path: /
port: http
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 24
readinessProbe:
httpGet:
path: /
port: http
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /
port: http
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
# Match the upstream Compose defaults: scheduling reserves enough for
# normal operation while limits protect the rest of the cluster.
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
- name: soulsync-db
mountPath: /app/data
- name: soulsync-config
mountPath: /app/config
- name: soulsync-logs
mountPath: /app/logs
- name: usenet-downloads
mountPath: /data/usenet
- name: torrent-downloads
mountPath: /data/torrents
- name: downloads
mountPath: /app/downloads
- name: music
mountPath: /app/Transfer
volumes:
- name: soulsync-db
persistentVolumeClaim:
claimName: soulsync-db
- name: soulsync-config
persistentVolumeClaim:
claimName: soulsync-config
- name: soulsync-logs
persistentVolumeClaim:
claimName: soulsync-logs
- name: usenet-downloads
nfs:
server: nas.ahmza.com
path: /srv/media/usenet
- name: torrent-downloads
nfs:
server: nas.ahmza.com
path: /srv/media/torrents
- name: downloads
nfs:
server: nas.ahmza.com
path: /srv/media/torrents
- name: music
nfs:
server: nas.ahmza.com
path: /srv/media/Music
+28
View File
@@ -0,0 +1,28 @@
# routes for soulsync.ahmza.com through ingress-nginx (docs/11-ingress.md)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: soulsync
namespace: soulsync
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
# Select ingress-nginx explicitly rather than relying on whichever ingress
# controller happens to be configured as the cluster-wide default.
ingressClassName: nginx
tls:
- hosts:
- soulsync.ahmza.com
# cert-manager keeps this up to date. nothing to do by hand
secretName: soulsync-tls
rules:
- host: soulsync.ahmza.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: soulsync
port:
number: 8008
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: soulsync
+41
View File
@@ -0,0 +1,41 @@
# Keep SoulSync's databases and downloaded artwork on Longhorn so they survive
# Pod replacement (see docs/06-longhorn.md).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: soulsync-db
namespace: soulsync
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
# Configuration can contain connection settings and OAuth tokens, so persist it
# without putting those credentials in this public Git repository.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: soulsync-config
namespace: soulsync
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
# Persistent logs remain available when the container is recreated and make
# failures that happened before a restart easier to diagnose.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: soulsync-logs
namespace: soulsync
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: soulsync
namespace: soulsync
spec:
selector:
app: soulsync
ports:
- name: http
port: 8008
targetPort: http
+41
View File
@@ -0,0 +1,41 @@
# Cluster metrics collection for the external VictoriaMetrics and Grafana host.
# This Application installs only collectors and Kubernetes exporters: metrics
# are sent over NetBird to prd-vps-hel-01 and are not stored in this cluster.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: victoria-metrics
namespace: argocd
# The in-cluster pieces are stateless. Cascading deletion is safe because the
# durable metrics database lives on the external VPS, not in this namespace.
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://tea.ahmza.com/ahmza.com/k8s-starter-pack.git
targetRevision: main
path: apps/victoria-metrics
destination:
server: https://kubernetes.default.svc
namespace: monitoring
syncPolicy:
automated:
prune: true
selfHeal: true
# node-exporter reads host kernel and filesystem metrics through hostPath,
# hostNetwork, and hostPID. Scope the required privileged Pod Security
# profile to this dedicated monitoring namespace.
managedNamespaceMetadata:
labels:
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/warn: privileged
syncOptions:
- CreateNamespace=true
# VictoriaMetrics Operator CRDs are large. Server-side apply avoids the
# client-side last-applied annotation size limit and handles CRDs first.
- ServerSideApply=true
+13
View File
@@ -0,0 +1,13 @@
# Umbrella chart for the official VictoriaMetrics Kubernetes monitoring stack.
# Argo CD renders this dependency from Git just like the existing Longhorn and
# MetalLB wrapper charts.
apiVersion: v2
name: victoria-metrics-app
description: Forwards Kubernetes and node metrics to external VictoriaMetrics
version: 0.1.0
dependencies:
- name: victoria-metrics-k8s-stack
# Pin the exact chart version so monitoring upgrades are explicit and
# reviewable rather than changing during an unrelated Argo CD sync.
version: 0.96.0
repository: oci://ghcr.io/victoriametrics/helm-charts
@@ -0,0 +1,30 @@
# Scrapes Longhorn's own metrics (volume robustness, node and disk capacity)
# so the external Grafana can alert when a volume becomes degraded or faulted.
#
# Every longhorn-manager Pod serves /metrics on its `manager` port (9500)
# behind the longhorn-backend Service. A VMServiceScrape scrapes each Pod
# behind that Service, so metrics from managers on both storage workers are
# collected, not just whichever one a load-balanced request would hit.
#
# This lives in the victoria-metrics wrapper chart rather than the Longhorn
# app because it is monitoring configuration, and it depends on the
# VMServiceScrape CRD installed by this chart's operator. Sync wave "1" makes
# Argo CD apply it only after that CRD exists on a fresh install.
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMServiceScrape
metadata:
name: longhorn
namespace: monitoring
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
namespaceSelector:
matchNames:
- longhorn-system
selector:
matchLabels:
app: longhorn-manager
endpoints:
- port: manager
path: /metrics
interval: 30s
+89
View File
@@ -0,0 +1,89 @@
# Values for the upstream victoria-metrics-k8s-stack dependency. This cluster
# collects metrics only; durable storage and visualization run on the NetBird-
# connected VPS prd-vps-hel-01.ahmza.com (NetBird address 100.71.24.9).
victoria-metrics-k8s-stack:
# Identify this source if more clusters write to the same database later.
vmagent:
enabled: true
spec:
scrapeInterval: 30s
externalLabels:
cluster: gitops-starter
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 512Mi
external:
vm:
# Queries are served from the VictoriaMetrics base URL. Remote write uses
# its Prometheus-compatible ingestion endpoint. NetBird encrypts the path
# between the Talos nodes and VPS, so no public endpoint is involved.
#
# The name resolves only through internal Technitium DNS (10.10.10.10),
# where an authoritative zone maps it to the VPS's NetBird address. If
# that address changes, update Technitium rather than this file.
read:
url: http://prd-vps-hel-01.ahmza.com:8428
write:
url: http://prd-vps-hel-01.ahmza.com:8428/api/v1/write
# Storage and UI already live on prd-vps-hel-01; do not duplicate them here.
vmsingle:
enabled: false
vmcluster:
enabled: false
vmdistributed:
enabled: false
grafana:
enabled: false
# Alert delivery is a separate follow-up once notification destinations are
# chosen. Disabling it also avoids deploying rules with nowhere to notify.
vmalert:
enabled: false
alertmanager:
enabled: false
defaultRules:
enabled: false
defaultDashboards:
enabled: false
# One node-exporter Pod runs on every node, including tainted control planes.
# Talos exposes the host root filesystem read-only; mount propagation is not
# needed and can be rejected by immutable hosts, so keep it disabled.
prometheus-node-exporter:
enabled: true
hostRootFsMount:
enabled: true
mountPropagation: None
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
memory: 128Mi
# Talos binds the scheduler and controller-manager metrics endpoints to
# 127.0.0.1, and runs etcd as a host service rather than a Pod. Scraping them
# from vmagent would require exposing those control-plane ports on the LAN,
# so skip them; API server, kubelet, CoreDNS, and node metrics remain.
kubeScheduler:
enabled: false
kubeControllerManager:
enabled: false
kubeEtcd:
enabled: false
# kube-state-metrics reports desired/current Kubernetes object state (for
# example unavailable Deployment replicas), complementing host metrics.
kube-state-metrics:
enabled: true
resources:
requests:
cpu: 20m
memory: 64Mi
limits:
memory: 256Mi
+24 -3
View File
@@ -32,6 +32,22 @@ on every node.
| `10.10.10.204` | `talos-vba-ood` — worker02 (Proxmox VM on Titus) | 50000 | `https://10.10.10.204:50000` | | `10.10.10.204` | `talos-vba-ood` — worker02 (Proxmox VM on Titus) | 50000 | `https://10.10.10.204:50000` |
| `10.10.10.210` | Cluster VIP (use this one, not a node's own address) | 6443 | `https://10.10.10.210:6443` | | `10.10.10.210` | Cluster VIP (use this one, not a node's own address) | 6443 | `https://10.10.10.210:6443` |
## NetBird overlay
These addresses exist only inside the private NetBird overlay and are subject
to NetBird access policies. All Talos nodes use the official
`siderolabs/netbird` system extension.
| Address | What | Port(s) | URL(s) |
|---|---|---|---|
| `100.71.24.9` | `prd-vps-hel-01` — external Grafana and VictoriaMetrics host; internal name `prd-vps-hel-01.ahmza.com` (authoritative zone in Technitium) | 3000, 8428 | `http://prd-vps-hel-01.ahmza.com:3000`, `http://prd-vps-hel-01.ahmza.com:8428` |
| `100.71.236.121` | `ingress1` — Caddy HTTP origin gateway used by NetBird Reverse Proxy; the WireGuard overlay encrypts this leg | 80 | `http://100.71.236.121` |
| `100.71.121.67` | `talos-455-i3b` — control plane NetBird address | 50000 | `https://100.71.121.67:50000` |
| `100.71.157.217` | `talos-9xy-iuk` — control plane NetBird address | 50000 | `https://100.71.157.217:50000` |
| `100.71.242.40` | `talos-k1b-7j5` — control plane NetBird address | 50000 | `https://100.71.242.40:50000` |
| `100.71.3.210` | `talos-rss-v5l` — worker01 NetBird address | 50000 | `https://100.71.3.210:50000` |
| `100.71.29.51` | `talos-vba-ood` — worker02 NetBird address | 50000 | `https://100.71.29.51:50000` |
## MetalLB pool ## MetalLB pool
This dedicated service range is advertised to the MikroTik at This dedicated service range is advertised to the MikroTik at
@@ -49,6 +65,7 @@ This dedicated service range is advertised to the MikroTik at
| Address | What | Namespace | Port(s) | URL | Source | | Address | What | Namespace | Port(s) | URL | Source |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| `10.10.60.10` | Argo CD UI/API (`argocd-server`) | `argocd` | 443 | `https://10.10.60.10` | Patched to `LoadBalancer` by hand — see [`docs/05-metallb.md`](05-metallb.md) | | `10.10.60.10` | Argo CD UI/API (`argocd-server`) | `argocd` | 443 | `https://10.10.60.10` | Patched to `LoadBalancer` by hand — see [`docs/05-metallb.md`](05-metallb.md) |
| `10.10.60.11` | Reserved for the Authentik LDAP Outpost used by Jellyfin | `authentik` | 636 | `ldaps://10.10.60.11:636` | Planned Authentik deployment; verify the address and TLS certificate before migrating Jellyfin |
| `10.10.60.12` | ingress-nginx (shared, routes by hostname) | `ingress-nginx` | 80, 443 | `http://10.10.60.12`, `https://10.10.60.12` | [`apps/ingress-nginx/values.yaml`](../apps/ingress-nginx/values.yaml) — see [`docs/11-ingress.md`](11-ingress.md) | | `10.10.60.12` | ingress-nginx (shared, routes by hostname) | `ingress-nginx` | 80, 443 | `http://10.10.60.12`, `https://10.10.60.12` | [`apps/ingress-nginx/values.yaml`](../apps/ingress-nginx/values.yaml) — see [`docs/11-ingress.md`](11-ingress.md) |
Add a row here every time you add a new app with its own dedicated Add a row here every time you add a new app with its own dedicated
@@ -61,13 +78,17 @@ only when it's the protocol default (80 for `http://`, 443 for `https://`)
## Hostnames routed through ingress-nginx ## Hostnames routed through ingress-nginx
Every real DNS record pointing at ingress-nginx's shared address Every real hostname routed to ingress-nginx's shared address
(`10.10.60.12`), and which app it reaches. All hosted on Cloudflare. (`10.10.60.12`), and which app it reaches. Public DNS is hosted on
Cloudflare; internal overrides are served by Technitium where noted.
| Hostname | App | Namespace | TLS | Source | | Hostname | App | Namespace | TLS | Source |
|---|---|---|---|---| |---|---|---|---|---|
| `demo.ahmza.com` | Demo app | `demo` | Yes — `letsencrypt-prod`, real trusted cert (see [`docs/12-cert-manager.md`](12-cert-manager.md)) | [`apps/demo/ingress.yaml`](../apps/demo/ingress.yaml) | | `demo.ahmza.com` | Demo app | `demo` | Yes — `letsencrypt-prod`, real trusted cert (see [`docs/12-cert-manager.md`](12-cert-manager.md)) | [`apps/demo/ingress.yaml`](../apps/demo/ingress.yaml) |
| `auth.ahmza.com` | Authentik identity provider | `authentik` | Yes — `letsencrypt-prod`, trusted certificate and browser login verified; Technitium routes LAN clients to `10.10.60.12`. For public traffic, NetBird terminates public TLS and proxies HTTP through its encrypted WireGuard overlay to Caddy on `ingress1`; Caddy then uses certificate-validated HTTPS with `auth.ahmza.com` as SNI and the HTTP `Host` when connecting to ingress-nginx at `10.10.60.12:443` | [`apps/authentik/values.yaml`](../apps/authentik/values.yaml) |
| `mc.ahmza.com` | Packwiz modpack files | `mcpacks` | Yes — `letsencrypt-prod`, real trusted certificate | [`apps/mcpacks/ingress.yaml`](../apps/mcpacks/ingress.yaml) | | `mc.ahmza.com` | Packwiz modpack files | `mcpacks` | Yes — `letsencrypt-prod`, real trusted certificate | [`apps/mcpacks/ingress.yaml`](../apps/mcpacks/ingress.yaml) |
| `bazarr.ahmza.com` | Bazarr subtitle manager | `bazarr` | Yes — `letsencrypt-prod`, real trusted certificate; Technitium overrides Cloudflare's NetBird route on the LAN | [`apps/bazarr/ingress.yaml`](../apps/bazarr/ingress.yaml) |
| `soulsync.ahmza.com` | SoulSync media discovery | `soulsync` | Configured for `letsencrypt-prod`; verify the certificate after the first deployment; Technitium routes LAN clients to `10.10.60.12` | [`apps/soulsync/ingress.yaml`](../apps/soulsync/ingress.yaml) |
Add a row here every time you point a new hostname at this cluster (see Add a row here every time you point a new hostname at this cluster (see
[`docs/11-ingress.md`](11-ingress.md)). The DNS record itself lives in [`docs/11-ingress.md`](11-ingress.md)). The DNS record itself lives in
@@ -79,7 +100,7 @@ Addresses from the range above that aren't spoken for yet. Update this
alongside the table above so it's obvious at a glance what's safe to hand alongside the table above so it's obvious at a glance what's safe to hand
to the next app. to the next app.
- `10.10.60.11`, `10.10.60.13-10.10.60.250` - `10.10.60.13-10.10.60.250`
(`whoami`'s former `10.10.60.12` was reclaimed here after that app was (`whoami`'s former `10.10.60.12` was reclaimed here after that app was
removed as the docs/08 exercise — it now belongs to ingress-nginx instead. removed as the docs/08 exercise — it now belongs to ingress-nginx instead.
+49 -42
View File
@@ -5,18 +5,14 @@ This is the detailed, copy-pasteable companion to
first for the explanations — this one is here so you have every exact first for the explanations — this one is here so you have every exact
command in one place once you understand what they do. command in one place once you understand what they do.
Throughout, replace `<NODE1_IP>`, `<NODE2_IP>`, `<NODE3_IP>` with your three Throughout, use the three control-plane and two worker addresses recorded in
machines' real IP addresses, and `<VIP_IP>` with the shared address you [`docs/network.md`](../docs/network.md), plus the cluster's shared VIP. Give
picked (with Claude's help) for the cluster's VIP — see each machine a DHCP reservation (a fixed IP tied to its MAC address) before
[`docs/02-talos-bootstrap.md`](../docs/02-talos-bootstrap.md#the-vip-one-address-for-a-cluster-that-moves). installation so its address remains stable.
Give each machine a DHCP reservation (a fixed IP tied to its MAC address)
in your router or hypervisor before you start, so none of these addresses
change under the cluster. Talos bakes them into certificates during setup,
and changing one later means starting over for that node.
Steps 1–2 below run **once, total**. Steps 3 runs **once per node** (three Steps 1–2 below run **once, total**. Step 3 runs **once per node** (five
times). Steps 4–6 run **once, total**, against whichever node you like times, using the matching role configuration). Steps 4–6 run **once, total**;
(or, once it exists, the VIP). Talos API commands target a control-plane node, while Kubernetes uses the VIP.
All commands assume you're running them from the root of this repo. All commands assume you're running them from the root of this repo.
@@ -26,7 +22,10 @@ All commands assume you're running them from the root of this repo.
talosctl gen config gitops-starter https://<VIP_IP>:6443 \ talosctl gen config gitops-starter https://<VIP_IP>:6443 \
--output-dir _out \ --output-dir _out \
--config-patch-control-plane @talos/patch-controlplane.yaml \ --config-patch-control-plane @talos/patch-controlplane.yaml \
--config-patch-control-plane @talos/patch-longhorn.yaml --config-patch-control-plane @talos/patch-netbird.yaml \
--config-patch-worker @talos/patch-worker.yaml \
--config-patch-worker @talos/patch-longhorn.yaml \
--config-patch-worker @talos/patch-netbird.yaml
``` ```
Note the cluster endpoint here is the **VIP**, not any one node's address Note the cluster endpoint here is the **VIP**, not any one node's address
@@ -38,13 +37,12 @@ This writes three files into `_out/` (already covered by this repo's
`.gitignore` — they contain private keys and must never be committed): `.gitignore` — they contain private keys and must never be committed):
- `controlplane.yaml` — the machine configuration for a control-plane - `controlplane.yaml` — the machine configuration for a control-plane
node, with both patches baked in. Applied identically to all three node, with the Raspberry Pi/VIP and non-secret NetBird patches baked in.
nodes — there's no separate worker config in this setup, since every It is applied identically to all three control-plane nodes.
node is both. - `worker.yaml` — the machine configuration for the two Proxmox workers,
- `worker.yaml` — a plain worker config, generated automatically but with their role, Longhorn data-disk, and non-secret NetBird patches baked
unused here since every node in this repo's setup is both control plane in. The NetBird setup key is deliberately absent from both generated files;
and worker. Kept around for [`docs/09-growing-up.md`](../docs/09-growing-up.md), enrollment uses a short-lived key applied separately and revoked afterward.
where a fourth-and-beyond node uses exactly this file.
- `talosconfig` — *your* client credentials for talking to the Talos API on - `talosconfig` — *your* client credentials for talking to the Talos API on
this cluster (equivalent in spirit to a kubeconfig, but for Talos itself this cluster (equivalent in spirit to a kubeconfig, but for Talos itself
rather than Kubernetes). rather than Kubernetes).
@@ -68,34 +66,39 @@ separate.
## 3. Apply the configuration — once per node ## 3. Apply the configuration — once per node
Boot each machine from your custom Talos ISO (see Boot each machine from its architecture-appropriate custom Talos image (see
[docs/01-prerequisites.md](../docs/01-prerequisites.md)). Each boots into [docs/01-prerequisites.md](../docs/01-prerequisites.md)). Each boots into
"maintenance mode" — alive enough to accept a configuration, with nothing "maintenance mode" — alive enough to accept configuration, with nothing
installed yet. For **each** node in turn: installed yet. Inspect its disks first:
```sh ```sh
talosctl get disks --insecure --nodes <NODE_IP> talosctl get disks --insecure --nodes <NODE_IP>
``` ```
Confirm you see two disks: the one Talos will install itself to, and the For each Raspberry Pi control plane, confirm `/dev/mmcblk0` is the intended
≥900GB one `talos/patch-longhorn.yaml`'s `UserVolumeConfig` will claim for system disk, then apply the control-plane configuration:
Longhorn. Then:
```sh ```sh
talosctl apply-config --insecure \ talosctl apply-config --insecure \
--nodes <NODE_IP> \ --nodes <CONTROL_PLANE_IP> \
--file _out/controlplane.yaml --file _out/controlplane.yaml
``` ```
`--insecure` is required for this one command only: the node has no For each Proxmox worker, confirm `/dev/sda` is the Talos system disk and
certificates yet, so talosctl can't authenticate to it normally. This is `/dev/sdb` is the dedicated Longhorn disk selected by
the one moment during setup where you're trusting the network — do this on `talos/patch-longhorn.yaml`, then apply the worker configuration:
a network you control. The node will reboot, install Talos to its system
disk, format and mount its second disk for Longhorn, and come back up
running the configuration you gave it — now *with* certificates, so every
command after this uses the secure path.
Repeat this for `<NODE1_IP>`, `<NODE2_IP>`, and `<NODE3_IP>` before moving on. ```sh
talosctl apply-config --insecure \
--nodes <WORKER_IP> \
--file _out/worker.yaml
```
`--insecure` is required for this one command only: the node has no
certificates yet, so talosctl cannot authenticate to it normally. This is
the one moment during setup where you trust the local network. After applying
configuration, the node installs Talos and returns with authenticated API
access. Configure all three control planes and both workers before continuing.
## 4. Bootstrap etcd — once, total ## 4. Bootstrap etcd — once, total
@@ -115,12 +118,14 @@ existing one. Running it again against an existing cluster can corrupt it.
## 5. Get your kubeconfig ## 5. Get your kubeconfig
```sh ```sh
talosctl kubeconfig --nodes <VIP_IP> . talosctl kubeconfig --nodes <CONTROL_PLANE_IP> .
``` ```
Downloads a kubeconfig into the current directory (also gitignored — Downloads a kubeconfig through a control-plane node's Talos API into the
it grants full cluster access), pointed at the VIP rather than any one current directory (also gitignored — it grants full cluster access). The
node. Merge it with `~/.kube/config` or point `KUBECONFIG` at it directly: resulting Kubernetes configuration points at the cluster VIP rather than one
specific control plane. Merge it with `~/.kube/config` or point `KUBECONFIG`
at it directly:
```sh ```sh
export KUBECONFIG=$PWD/kubeconfig export KUBECONFIG=$PWD/kubeconfig
@@ -132,15 +137,17 @@ export KUBECONFIG=$PWD/kubeconfig
kubectl get nodes kubectl get nodes
``` ```
Expect to see **three** nodes, `Ready`, within a couple of minutes (it Expect to see **five** nodes, `Ready`, within a couple of minutes (it takes
takes a moment for the default CNI — Talos ships Flannel out of the box — a moment for the default CNI — Talos ships Flannel out of the box — to clear
to come up and clear each node's networking checks). each node's networking checks).
``` ```text
NAME STATUS ROLES AGE VERSION NAME STATUS ROLES AGE VERSION
talos-... Ready control-plane 90s v1.3x.x talos-... Ready control-plane 90s v1.3x.x
talos-... Ready control-plane 85s v1.3x.x talos-... Ready control-plane 85s v1.3x.x
talos-... Ready control-plane 80s v1.3x.x talos-... Ready control-plane 80s v1.3x.x
talos-... Ready worker 75s v1.3x.x
talos-... Ready worker 70s v1.3x.x
``` ```
If any node is stuck on `NotReady`, give it another minute before If any node is stuck on `NotReady`, give it another minute before
+6 -4
View File
@@ -12,9 +12,10 @@
# --- install image --- # --- install image ---
# All three control planes are Raspberry Pi 4 systems currently installed on # All three control planes are Raspberry Pi 4 systems currently installed on
# their microSD (`mmcblk0`). The Image Factory schematic contains the # their microSD (`mmcblk0`). The Image Factory schematic contains the
# `rpi_generic` ARM64 overlay needed to boot this hardware. Keep this custom # `rpi_generic` ARM64 overlay needed to boot this hardware and the NetBird
# installer reference during upgrades or the board-specific boot support may # system extension used for private overlay connectivity. Keep this custom
# be lost. # installer reference during upgrades or the board-specific boot support and
# NetBird binary may be lost.
# #
# --- vip --- # --- vip ---
# With three control-plane nodes, "the cluster's API server" isn't one # With three control-plane nodes, "the cluster's API server" isn't one
@@ -38,7 +39,8 @@ cluster:
machine: machine:
install: install:
disk: /dev/mmcblk0 disk: /dev/mmcblk0
image: factory.talos.dev/metal-installer/ee21ef4a5ef808a9b7484cc0dda0f25075021691c8c09a276591eedb638ea1f9:v1.13.8 image: "factory.talos.dev/metal-installer/\
d093bb68c464b87cc637765535dde5f68fbb040da44280617dd11a42d6c03988:v1.13.8"
network: network:
interfaces: interfaces:
# Raspberry Pi 4's built-in Ethernet interface is named `end0` by # Raspberry Pi 4's built-in Ethernet interface is named `end0` by
+20
View File
@@ -0,0 +1,20 @@
# Persistent, non-secret configuration for the official SideroLabs NetBird
# system extension. Apply this patch to every Talos node alongside its normal
# role-specific patches.
#
# Enrollment still requires NB_SETUP_KEY, but that short-lived credential must
# never be committed to this public repository. Supply it separately from a
# silent shell prompt during first enrollment, revoke it after all nodes join,
# then remove it from the live ExtensionServiceConfig.
apiVersion: v1alpha1
kind: ExtensionServiceConfig
name: netbird
environment:
# Both endpoints use this installation's combined self-hosted NetBird URL.
- NB_MANAGEMENT_URL=https://netbird.ahmza.com
- NB_ADMIN_URL=https://netbird.ahmza.com
# Talos mounts /etc read-only. Prevent NetBird from repeatedly trying to
# rewrite resolv.conf and the OpenSSH client configuration there. Overlay
# networking remains active; NetBird-managed DNS is deliberately disabled.
- NB_DISABLE_DNS=true
- NB_DISABLE_SSH_CONFIG=true
+5 -4
View File
@@ -8,9 +8,9 @@
# claiming the wrong virtual disk. # claiming the wrong virtual disk.
# #
# The custom Image Factory installer contains Longhorn's `iscsi-tools` and # The custom Image Factory installer contains Longhorn's `iscsi-tools` and
# `util-linux-tools` extensions plus `qemu-guest-agent`. Keep this schematic # `util-linux-tools` extensions plus `qemu-guest-agent` and NetBird. Keep this
# reference during future upgrades; switching to the stock installer would # schematic reference during future upgrades; switching to the stock installer
# silently remove those extensions. # would silently remove those extensions.
# #
# Both VMs use `ens18`. Their DHCP-reserved MAC addresses keep worker01 at # Both VMs use `ens18`. Their DHCP-reserved MAC addresses keep worker01 at
# 10.10.10.203 and worker02 at 10.10.10.204 while DHCP supplies their gateway # 10.10.10.203 and worker02 at 10.10.10.204 while DHCP supplies their gateway
@@ -26,7 +26,8 @@ machine:
node.longhorn.io/create-default-disk: "true" node.longhorn.io/create-default-disk: "true"
install: install:
disk: /dev/sda disk: /dev/sda
image: factory.talos.dev/metal-installer/88d1f7a5c4f1d3aba7df787c448c1d3d008ed29cfb34af53fa0df4336a56040b:v1.13.8 image: "factory.talos.dev/metal-installer/\
81fb1af7c03952593ba459c6c8b7fcc1268b6fe0cc80e6035ac629a741c183cc:v1.13.8"
network: network:
interfaces: interfaces:
- interface: ens18 - interface: ens18