diff --git a/hosts/AZ-PRM-1/secrets.nix b/hosts/AZ-PRM-1/secrets.nix index 833f6d7..549011d 100644 --- a/hosts/AZ-PRM-1/secrets.nix +++ b/hosts/AZ-PRM-1/secrets.nix @@ -63,6 +63,12 @@ in { file = ../../secrets/pgadmin-pw.age; owner = "pgadmin"; }; + semaphore-env = { + file = ../../secrets/semaphore-env.age; + owner = "postgres"; + group = "postgres"; + mode = "0400"; + }; pg-cert = { file = ../../secrets/server.crt.age; owner = "postgres"; diff --git a/hosts/AZ-PRM-1/services/containers/default.nix b/hosts/AZ-PRM-1/services/containers/default.nix index 687c076..c05d657 100644 --- a/hosts/AZ-PRM-1/services/containers/default.nix +++ b/hosts/AZ-PRM-1/services/containers/default.nix @@ -5,6 +5,7 @@ ./excalidraw.nix ./kestra.nix ./online3dviewer.nix + ./semaphore.nix ./stirling-pdf.nix ]; system.activationScripts.createPodmanNetworkWeb = lib.mkAfter '' diff --git a/hosts/AZ-PRM-1/services/containers/semaphore.nix b/hosts/AZ-PRM-1/services/containers/semaphore.nix new file mode 100644 index 0000000..20d1fc4 --- /dev/null +++ b/hosts/AZ-PRM-1/services/containers/semaphore.nix @@ -0,0 +1,86 @@ +{ + config, + lib, + ... +}: let + serviceName = "semaphore"; + servicePort = config.m3ta.ports.get serviceName; +in { + virtualisation.oci-containers.containers."${serviceName}" = { + # Image contains the Semaphore server plus ansible, terraform, + # opentofu and terragrunt runners. + image = "docker.io/semaphoreui/semaphore:latest"; + environment = { + SEMAPHORE_DB_DIALECT = "postgres"; + SEMAPHORE_DB_HOST = "postgres"; + SEMAPHORE_DB_PORT = "5432"; + SEMAPHORE_PLAYBOOK_PATH = "/tmp/semaphore/"; + }; + # SEMAPHORE_DB (name), SEMAPHORE_DB_USER, SEMAPHORE_DB_PASS and the + # admin bootstrap vars (SEMAPHORE_ADMIN*) come from the agenix secret + # (podman env-file). + environmentFiles = [config.age.secrets.semaphore-env.path]; + ports = ["127.0.0.1:${toString servicePort}:3000"]; + volumes = [ + "semaphore_data:/var/lib/semaphore" + ]; + extraOptions = ["--network=web" "--ip=10.89.0.17" "--add-host=postgres:10.89.0.1"]; + }; + + # Idempotent provisioning of the semaphore role/database on the host + # PostgreSQL 17 (same pattern as atrocore-db-init: initialScript cannot + # be used, the cluster is already initialized and the password must + # never land in the Nix store). + systemd.services.semaphore-db-init = { + description = "Provision semaphore role/database from agenix secret"; + after = ["postgresql.service"]; + before = ["podman-${serviceName}.service"]; + wants = ["postgresql.service"]; + wantedBy = ["multi-user.target"]; + serviceConfig = { + Type = "oneshot"; + User = "postgres"; + Group = "postgres"; + RemainAfterExit = true; + }; + path = [config.services.postgresql.package]; + # Env-file format: plain KEY=value lines (no quotes), same file the + # container consumes. + script = '' + set -euo pipefail + ENV_FILE="${config.age.secrets.semaphore-env.path}" + get_val() { + grep -m1 "^$1=" "$ENV_FILE" | head -n1 | cut -d= -f2- | tr -d '\r' + } + db_name="$(get_val SEMAPHORE_DB)" + db_user="$(get_val SEMAPHORE_DB_USER)" + db_pass="$(get_val SEMAPHORE_DB_PASS)" + if [ -z "$db_name" ] || [ -z "$db_user" ] || [ -z "$db_pass" ]; then + echo "semaphore-db-init: SEMAPHORE_DB/SEMAPHORE_DB_USER/SEMAPHORE_DB_PASS missing in $ENV_FILE" >&2 + exit 1 + fi + psql -v ON_ERROR_STOP=1 -d postgres \ + -v db_name="$db_name" -v db_user="$db_user" -v db_pass="$db_pass" <<'SQL' + SELECT format('CREATE ROLE %I LOGIN', :'db_user') + WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'db_user') \gexec + ALTER ROLE :"db_user" WITH LOGIN PASSWORD :'db_pass'; + SELECT format('CREATE DATABASE %I OWNER %I', :'db_name', :'db_user') + WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = :'db_name') \gexec + ALTER DATABASE :"db_name" OWNER TO :"db_user"; + SQL + echo "semaphore-db-init: role/database '$db_name' ready" + ''; + }; + + # Traefik configuration specific to semaphore + services.traefik.dynamicConfigOptions.http = { + services.${serviceName}.loadBalancer.servers = [{url = "http://localhost:${toString servicePort}/";}]; + + routers.${serviceName} = { + rule = "Host(`sem.l.az-gruppe.com`)"; + tls = {certResolver = "ionos";}; + service = serviceName; + entrypoints = "websecure"; + }; + }; +} diff --git a/hosts/AZ-PRM-1/services/monitoring/blackbox.nix b/hosts/AZ-PRM-1/services/monitoring/blackbox.nix index 45abc1a..8bca38b 100644 --- a/hosts/AZ-PRM-1/services/monitoring/blackbox.nix +++ b/hosts/AZ-PRM-1/services/monitoring/blackbox.nix @@ -39,6 +39,7 @@ in { "https://g.l.az-gruppe.com" "https://wf.l.az-gruppe.com" "https://k.l.az-gruppe.com" + "https://sem.l.az-gruppe.com" "https://git.az-gruppe.com" "https://ping.az-gruppe.com" "https://llm.az-gruppe.com" diff --git a/hosts/AZ-PRM-1/services/postgres.nix b/hosts/AZ-PRM-1/services/postgres.nix index 9cd1c97..145d19c 100644 --- a/hosts/AZ-PRM-1/services/postgres.nix +++ b/hosts/AZ-PRM-1/services/postgres.nix @@ -53,6 +53,7 @@ host baserow baserow 10.89.0.0/24 scram-sha-256 host kestra kestra 10.89.0.0/24 scram-sha-256 host atrocore atrocore 10.89.0.0/24 scram-sha-256 + host semaphore semaphore 10.89.0.0/24 scram-sha-256 # Grafana (local socket / localhost only) host grafana grafana 127.0.0.1/32 scram-sha-256 @@ -66,7 +67,7 @@ services.postgresqlBackup = { enable = true; startAt = "03:10:00"; - databases = ["atrocore" "baserow" "kestra" "n8n" "netbox"]; + databases = ["atrocore" "baserow" "kestra" "n8n" "netbox" "semaphore"]; }; networking.firewall = { extraCommands = '' diff --git a/hosts/common/ports.nix b/hosts/common/ports.nix index e28106e..8ac160a 100644 --- a/hosts/common/ports.nix +++ b/hosts/common/ports.nix @@ -22,6 +22,7 @@ phishboard = 3055; homarr = 3057; atrocore = 3058; + semaphore = 3059; metabase = 3013; baserow = 3050; diff --git a/secrets.nix b/secrets.nix index e0b39dd..eb98b91 100644 --- a/secrets.nix +++ b/secrets.nix @@ -67,4 +67,5 @@ in { "secrets/monitoring-loki-htpasswd.age".publicKeys = [AZ-PRM-1] ++ users; "secrets/monitoring-loki-remote-env.age".publicKeys = [AZ-CLD-1] ++ users; "secrets/ntfy-grafana-webhook.age".publicKeys = systems ++ users; + "secrets/semaphore-env.age".publicKeys = [AZ-PRM-1] ++ users; } diff --git a/secrets/semaphore-env.age b/secrets/semaphore-env.age new file mode 100644 index 0000000..561bfb1 Binary files /dev/null and b/secrets/semaphore-env.age differ