feat: huge rework

entire ~/.config directory now provisions; traefik configuration added as first step

+8 -6
butane/fcos.yml.tftpl
···
name: core
group:
name: core
-
-
# Quadlets dir
- path: /var/home/core/.config/containers
user:
name: core
group:
name: core
-
- path: /var/home/core/.config/containers/systemd
+
+
# Config dirs
+
%{ for _, path in directories ~}
+
- path: /var/home/core/.config/${path}
user:
name: core
group:
name: core
+
%{ endfor ~}
# Systemd user dir
- path: /var/home/core/.config/systemd
···
driver = "overlay"
rootless_storage_path = "/var/mnt/docker/$USER"
-
# Quadlets block
-
%{ for name, content in quadlet_files ~}
-
- path: /var/home/core/.config/containers/systemd/${name}
+
# Configs block
+
%{ for name, content in config_files ~}
+
- path: /var/home/core/.config/${name}
contents:
inline: |
${indent(10, content)}
+9
configs/traefik/file/oauth2-proxy.yml
···
+
http:
+
middlewares:
+
oauth2-proxy:
+
forwardAuth:
+
address: http://oauth2-proxy:4180/
+
trustForwardHeader: true
+
authResponseHeaders:
+
- X-Auth-Request-Access-Token
+
- Authorization
+8
configs/traefik/file/security-headers.yml
···
+
http:
+
middlewares:
+
security-headers:
+
headers:
+
stsSeconds: 31536000
+
stsIncludeSubdomains: true
+
forceSTSHeader: true
+
contentTypeNosniff: true
+16
configs/traefik/file/tcp.yml.tftpl
···
+
{{ $baseDomain := ".${base_domain}" }}
+
{{ $rules := dict "pve.${base_domain}" "${proxmox_ip}:8006" "truenas.${base_domain}" "${truenas_ip}:443" }}
+
tcp:
+
routers:
+
{{ range $key, $value := $rules }}{{ $name := trimSuffix $baseDomain $key }}{{ $name }}:
+
rule: HostSNI(`{{ $key }}`)
+
service: {{ $name }}
+
tls:
+
passthrough: true
+
{{ end }}
+
services:
+
{{ range $key, $value := $rules }}{{ trimSuffix $baseDomain $key }}:
+
loadBalancer:
+
servers:
+
- address: {{ $value }}
+
{{ end }}
+45
configs/traefik/traefik.yml
···
+
entryPoints:
+
web:
+
address: ":80"
+
http:
+
redirections:
+
entryPoint:
+
to: websecure
+
websecure:
+
address: ":443"
+
http:
+
middlewares:
+
- security-headers@file
+
tls:
+
certResolver: leresolver
+
metrics:
+
address: ":8082"
+
+
providers:
+
docker:
+
exposedByDefault: false
+
file:
+
directory: /etc/traefik/file
+
watch: true
+
+
certificatesResolvers:
+
leresolver:
+
acme:
+
email: ${email}
+
storage: /etc/traefik/acme/acme.json
+
dnsChallenge:
+
provider: cloudflare
+
keyType: EC256
+
+
api:
+
dashboard: true
+
+
metrics:
+
prometheus:
+
entryPoint: metrics
+
+
log:
+
format: json
+
+
accessLog:
+
format: json
+37 -20
fcos.tf
···
data "bitwarden_secret" "victoria_bearer_token" {
-
id = var.quadlets_secret_config.victoria_bearer_token
+
id = var.containers_secret_config.victoria_bearer_token
}
data "bitwarden_secret" "immich_map_key" {
-
id = var.quadlets_secret_config.immich_map_key
+
id = var.containers_secret_config.immich_map_key
}
locals {
// Add secrets into quadlets config
-
quadlets_config = merge(var.quadlets_config, {
-
secrets: {
-
victoria_bearer_token: data.bitwarden_secret.victoria_bearer_token.value
+
containers_config = merge(var.containers_config, {
+
email : var.containers_config.email,
+
proxmox_ip : var.proxmox_config.host,
+
truenas_ip : var.fcos_config.truenas_ip,
+
secrets : {
+
victoria_bearer_token : data.bitwarden_secret.victoria_bearer_token.value
immich_map_key = data.bitwarden_secret.immich_map_key.value
}
})
# Get a list of all files in the specified directory
-
quadlet_paths = fileset(path.module, "quadlets/**")
-
quadlet_files = {
-
for path in local.quadlet_paths :
-
replace(basename(path), ".tftpl", "") => templatefile(path, local.quadlets_config)
+
config_paths = fileset(path.module, "configs/**")
+
config_files = {
+
for path in local.config_paths :
+
replace(trimprefix(path, "configs/"), ".tftpl", "") => templatefile(path, local.containers_config)
}
+
// I use trimsuffix+basename instead of dirname because on Windows dirname replaces slashes with backslashes.
+
possible_paths = [
+
for path in fileset(path.module, "configs/**") :trimsuffix(trimprefix(path, "configs/"), "/${basename(path)}")
+
]
+
// Because Terraform/Tofu doesn't interacting with real filesystem, I cannot check if directory is actually directory.
+
directories = distinct([
+
for path in local.possible_paths : path if !strcontains(basename(path), ".")
+
])
+
butane_config = merge(var.fcos_config, {
-
quadlet_files = local.quadlet_files
+
config_files = local.config_files
+
directories = local.directories,
})
init_script_path = "${path.module}/scripts/init_fcos.sh.tftpl"
}
+
output "test" {
+
value = local.directories
+
}
+
data "ct_config" "fcos_ignition" {
content = templatefile("${path.module}/butane/fcos.yml.tftpl", local.butane_config)
strict = true
···
cpu {
cores = 8
-
type = "host"
+
type = "host"
}
memory {
···
}
network_device {
-
bridge = "vmbr0"
-
vlan_id = 100
+
bridge = "vmbr0"
+
vlan_id = 100
mac_address = var.fcos_config.mac_address
}
···
}
connection {
-
type = "ssh"
-
user = "core"
+
type = "ssh"
+
user = "core"
agent = true
-
host = var.fcos_config.ip
+
host = var.fcos_config.ip
}
provisioner "file" {
destination = "/tmp/init.sh"
content = templatefile(local.init_script_path, {
-
bws_access_token: var.bws_access_token
-
quadlet_files: local.quadlet_files
-
secrets: var.quadlets_secret_config
+
bws_access_token : var.bws_access_token
+
config_files : local.config_files
+
secrets : var.containers_secret_config
})
}
···
inline = ["sh /tmp/init.sh"]
on_failure = fail
}
-
}
+
}
+1 -1
main.tf
···
}
provider "proxmox" {
-
endpoint = var.proxmox_config.endpoint
+
endpoint = "https://${var.proxmox_config.host}:8006"
insecure = true
// Unfortunately Proxmox can execute a lot of actions only under root user...
quadlets/actual-budget.container.tftpl configs/containers/systemd/actual-budget.container.tftpl
-1
quadlets/bluesky-pds.container.tftpl configs/containers/systemd/bluesky-pds.container.tftpl
···
Label="traefik.http.routers.pds.entrypoints=websecure"
Label="traefik.http.routers.pds.tls.domains[0].main=pds.${base_domain}"
Label="traefik.http.routers.pds.tls.domains[0].sans=*.pds.${base_domain}"
-
Label="traefik.http.routers.pds.tls.options=http3@file"
Label="traefik.http.routers.pds.tls.certresolver=leresolver"
Volume=/var/mnt/docker/app_data/bluesky/pds:/pds:Z
quadlets/glance.container.tftpl configs/containers/systemd/glance.container.tftpl
quadlets/grafana-alloy.container.tftpl configs/containers/systemd/grafana-alloy.container.tftpl
quadlets/grafana.container.tftpl configs/containers/systemd/grafana.container.tftpl
quadlets/hoarder/hoarder-chrome.container.tftpl configs/containers/systemd/hoarder/hoarder-chrome.container.tftpl
quadlets/hoarder/hoarder-meilisearch.container.tftpl configs/containers/systemd/hoarder/hoarder-meilisearch.container.tftpl
quadlets/hoarder/hoarder-server.container.tftpl configs/containers/systemd/hoarder/hoarder-server.container.tftpl
quadlets/immich/immich-machine-learning.container.tftpl configs/containers/systemd/immich/immich-machine-learning.container.tftpl
quadlets/immich/immich-postgres.container.tftpl configs/containers/systemd/immich/immich-postgres.container.tftpl
quadlets/immich/immich-redis.container.tftpl configs/containers/systemd/immich/immich-redis.container.tftpl
quadlets/immich/immich-server.container.tftpl configs/containers/systemd/immich/immich-server.container.tftpl
quadlets/immich/immich-static-web-server.container.tftpl configs/containers/systemd/immich/immich-static-web-server.container.tftpl
quadlets/miniflux/miniflux-postgres.container.tftpl configs/containers/systemd/miniflux/miniflux-postgres.container.tftpl
quadlets/miniflux/miniflux-server.container.tftpl configs/containers/systemd/miniflux/miniflux-server.container.tftpl
quadlets/networks/hoarder.network configs/containers/systemd/networks/hoarder.network
quadlets/networks/immich.network configs/containers/systemd/networks/immich.network
quadlets/networks/miniflux.network configs/containers/systemd/networks/miniflux.network
quadlets/networks/outline.network configs/containers/systemd/networks/outline.network
quadlets/networks/reverse-proxy.network configs/containers/systemd/networks/reverse-proxy.network
quadlets/networks/victoria.network configs/containers/systemd/networks/victoria.network
quadlets/oauth2-proxy.container.tftpl configs/containers/systemd/oauth2-proxy.container.tftpl
quadlets/open-webui.container.tftpl configs/containers/systemd/open-webui.container.tftpl
quadlets/outline/outline-postgres.container.tftpl configs/containers/systemd/outline/outline-postgres.container.tftpl
quadlets/outline/outline-redis.container.tftpl configs/containers/systemd/outline/outline-redis.container.tftpl
quadlets/outline/outline-server.container.tftpl configs/containers/systemd/outline/outline-server.container.tftpl
quadlets/plex.container.tftpl configs/containers/systemd/plex.container.tftpl
quadlets/pocket-id.container.tftpl configs/containers/systemd/pocket-id.container.tftpl
quadlets/pods/hoarder.pod configs/containers/systemd/pods/hoarder.pod
quadlets/pods/immich.pod configs/containers/systemd/pods/immich.pod
quadlets/pods/miniflux.pod configs/containers/systemd/pods/miniflux.pod
quadlets/pods/outline.pod configs/containers/systemd/pods/outline.pod
quadlets/pods/victoria.pod configs/containers/systemd/pods/victoria.pod
quadlets/qbittorrent.container.tftpl configs/containers/systemd/qbittorrent.container.tftpl
quadlets/step-ca.container.tftpl configs/containers/systemd/step-ca.container.tftpl
+7 -3
quadlets/traefik.container.tftpl configs/containers/systemd/traefik.container.tftpl
···
User=1000:1000
UserNS=keep-id:uid=1000,gid=1000
+
# I use CNAMEs to point to my homelab;
+
# Variable name could be misleading, since overwise
+
# Lego tries to issue cert for you CNAME host.
Environment=LEGO_DISABLE_CNAME_SUPPORT=true
+
Secret=traefik-cf-dns-api-token,type=env,target=CF_DNS_API_TOKEN
Label="glance.name=Traefik"
Label="glance.icon=di:traefik"
···
Label="traefik.http.routers.dashboard-auth.rule=Host(`traefik.${base_domain}`) && PathPrefix(`/oauth2/`)"
Label="traefik.http.routers.dashboard-auth.service=oauth2-proxy"
-
Secret=traefik-cf-dns-api-token,type=env,target=CF_DNS_API_TOKEN
+
Volume=%E/traefik/traefik.yml:/etc/traefik/traefik.yml:Z
+
Volume=%E/traefik/file:/etc/traefik/file:Z
+
Volume=/var/mnt/docker/app_data/traefik/acme:/etc/traefik/acme:Z
-
Volume=/var/mnt/docker/app_data/traefik/traefik.yml:/etc/traefik/traefik.yml:Z
-
Volume=/var/mnt/docker/app_data/traefik/data:/data:Z
Volume=%t/podman/podman.sock:/var/run/docker.sock
Network=reverse-proxy.network
quadlets/victoria/victoria-logs.container.tftpl configs/containers/systemd/victoria/victoria-logs.container.tftpl
quadlets/victoria/victoria-metrics.container.tftpl configs/containers/systemd/victoria/victoria-metrics.container.tftpl
quadlets/victoria/victoria-vmauth.container.tftpl configs/containers/systemd/victoria/victoria-vmauth.container.tftpl
quadlets/volumes/immich-machine-learning.volume configs/containers/systemd/volumes/immich-machine-learning.volume
+5 -5
scripts/init_fcos.sh.tftpl
···
echo "Starting Quadlets..."
# Quadlets are "enabled" using their configurations, it's enough to just start them.
-
%{ for name, content in quadlet_files ~}
-
%{ if strcontains(name, ".container") && !strcontains(content, "\nPod=") ~}
-
systemctl --user start ${replace(name, ".container", "")}
+
%{ for path, content in config_files ~}
+
%{ if strcontains(basename(path), ".container") && !strcontains(content, "\nPod=") ~}
+
systemctl --user start ${replace(basename(path), ".container", "")}
%{ endif ~}
-
%{ if strcontains(name, ".pod") ~}
-
systemctl --user start ${replace(name, ".pod", "")}-pod
+
%{ if strcontains(basename(path), ".pod") ~}
+
systemctl --user start ${replace(basename(path), ".pod", "")}-pod
%{ endif ~}
%{ endfor ~}
+5 -5
variables.tf
···
variable "proxmox_config" {
description = "Proxmox credentials"
type = object({
-
endpoint = string
+
host = string
password_secret_id = string
})
}
-
# Just base domain for now
-
variable "quadlets_config" {
-
description = "Shared Quadlets configuration"
+
variable "containers_config" {
+
description = "Shared configuration"
type = object({
+
email = string
base_domain = string
})
}
-
variable "quadlets_secret_config" {
+
variable "containers_secret_config" {
description = "Quadlet Ssecret"
type = map(string)
default = {