feat: enable n8n AI assistant AZ-PRM-1
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
./atrocore.nix
|
||||
./excalidraw.nix
|
||||
./kestra.nix
|
||||
./n8n-sandbox.nix
|
||||
./online3dviewer.nix
|
||||
./semaphore.nix
|
||||
./stirling-pdf.nix
|
||||
@@ -15,5 +16,17 @@
|
||||
if ! /run/current-system/sw/bin/podman network exists web-dev; then
|
||||
/run/current-system/sw/bin/podman network create web-dev --subnet=10.89.1.0/24 --internal
|
||||
fi
|
||||
# Routed egress network: unlike web/web-dev (isolated), netavark sets up
|
||||
# a default gateway plus NAT/masquerade for attached containers —
|
||||
# outbound via host, incl. the NetBird overlay (wt0, 100.91.0.0/16).
|
||||
if ! /run/current-system/sw/bin/podman network exists vpn-egress; then
|
||||
/run/current-system/sw/bin/podman network create vpn-egress --subnet=10.89.9.0/24
|
||||
fi
|
||||
# n8n AI-Assistant sandbox stack (n8n-sandbox.nix): routed, NOT --internal
|
||||
# — the privileged DinD runner must pull its sandbox images from ghcr.io.
|
||||
# DNS between sandbox-api/sandbox-runner-1 via netavark/aardvark.
|
||||
if ! /run/current-system/sw/bin/podman network exists n8n-sandbox; then
|
||||
/run/current-system/sw/bin/podman network create n8n-sandbox --subnet=10.89.10.0/24
|
||||
fi
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
{config, ...}: let
|
||||
sandboxApiPort = config.m3ta.ports.get "n8n-sandbox-api";
|
||||
tlsDir = "/var/lib/n8n-sandbox/tls";
|
||||
apiImage = "ghcr.io/n8n-io/n8n-sandbox-service-api:1.2.0";
|
||||
runnerImage = "ghcr.io/n8n-io/n8n-sandbox-service-runner-dind:1.2.0";
|
||||
sandboxImage = "ghcr.io/n8n-io/n8n-sandbox-service-sandbox:latest";
|
||||
in {
|
||||
virtualisation.oci-containers.containers = {
|
||||
sandbox-api = {
|
||||
image = apiImage;
|
||||
environmentFiles = [config.age.secrets.n8n-sandbox-env.path];
|
||||
environment = {
|
||||
SANDBOX_API_GRPC_TLS_CERT_FILE = "/tls/api/grpc-server.crt";
|
||||
SANDBOX_API_GRPC_TLS_KEY_FILE = "/tls/api/grpc-server.key";
|
||||
SANDBOX_API_GRPC_TLS_CLIENT_CA_FILE = "/tls/api/ca.crt";
|
||||
SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CA_FILE = "/tls/api/ca.crt";
|
||||
SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CERT_FILE = "/tls/api/control-grpc-api-client.crt";
|
||||
SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_KEY_FILE = "/tls/api/control-grpc-api-client.key";
|
||||
SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_SERVER_NAME = "sandbox-runner-1";
|
||||
};
|
||||
volumes = ["${tlsDir}:/tls:ro"];
|
||||
ports = ["127.0.0.1:${toString sandboxApiPort}:8080"];
|
||||
extraOptions = ["--network=n8n-sandbox"];
|
||||
};
|
||||
|
||||
sandbox-runner-1 = {
|
||||
image = runnerImage;
|
||||
environmentFiles = [config.age.secrets.n8n-sandbox-env.path];
|
||||
environment = {
|
||||
SANDBOX_RUNNER_API_GRPC_ADDR = "sandbox-api:9090";
|
||||
SANDBOX_RUNNER_HTTP_BASE_URL = "http://sandbox-runner-1:8080";
|
||||
SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR = ":9091";
|
||||
SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR = "sandbox-runner-1:9091";
|
||||
SANDBOX_RUNNER_ID = "runner-1";
|
||||
SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE = sandboxImage;
|
||||
SANDBOX_RUNNER_REGISTRATION_GRPC_CA_FILE = "/tls/runner/ca.crt";
|
||||
SANDBOX_RUNNER_REGISTRATION_GRPC_CERT_FILE = "/tls/runner/grpc-client.crt";
|
||||
SANDBOX_RUNNER_REGISTRATION_GRPC_KEY_FILE = "/tls/runner/grpc-client.key";
|
||||
SANDBOX_RUNNER_REGISTRATION_GRPC_SERVER_NAME = "sandbox-api";
|
||||
SANDBOX_RUNNER_CONTROL_GRPC_TLS_CERT_FILE = "/tls/runner/control-grpc-server.crt";
|
||||
SANDBOX_RUNNER_CONTROL_GRPC_TLS_KEY_FILE = "/tls/runner/control-grpc-server.key";
|
||||
SANDBOX_RUNNER_CONTROL_GRPC_TLS_CLIENT_CA_FILE = "/tls/runner/ca.crt";
|
||||
};
|
||||
volumes = ["${tlsDir}:/tls:ro"];
|
||||
dependsOn = ["sandbox-api"];
|
||||
extraOptions = ["--network=n8n-sandbox" "--privileged"];
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.n8n-sandbox-certs = {
|
||||
description = "n8n sandbox: mTLS certificate bootstrap";
|
||||
wantedBy = ["multi-user.target"];
|
||||
after = ["network-online.target"];
|
||||
wants = ["network-online.target"];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ConditionPathExists = "!${tlsDir}/ca.crt";
|
||||
};
|
||||
path = [config.virtualisation.podman.package];
|
||||
preStart = ''
|
||||
mkdir -p ${tlsDir}
|
||||
'';
|
||||
script = ''
|
||||
podman run --rm \
|
||||
--name n8n-sandbox-certs \
|
||||
--user 0:0 \
|
||||
-e NUM_RUNNERS=1 \
|
||||
-v ${tlsDir}:/tls \
|
||||
--entrypoint sh \
|
||||
${apiImage} \
|
||||
-c 'bootstrap-mtls.sh --out-dir /tls --api-san sandbox-api --control-san-prefix sandbox-runner --world-readable && chown -R sandbox-api:sandbox-api /tls/api && chmod -R a+rX /tls'
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services."podman-sandbox-api" = {
|
||||
after = ["n8n-sandbox-certs.service"];
|
||||
requires = ["n8n-sandbox-certs.service"];
|
||||
preStart = ''
|
||||
${config.virtualisation.podman.package}/bin/podman rm -f sandbox-api 2>/dev/null || true
|
||||
'';
|
||||
};
|
||||
systemd.services."podman-sandbox-runner-1" = {
|
||||
after = ["n8n-sandbox-certs.service"];
|
||||
requires = ["n8n-sandbox-certs.service"];
|
||||
preStart = ''
|
||||
${config.virtualisation.podman.package}/bin/podman rm -f sandbox-runner-1 2>/dev/null || true
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -7,8 +7,6 @@
|
||||
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";
|
||||
@@ -16,26 +14,24 @@ in {
|
||||
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"
|
||||
];
|
||||
# Public DNS: the corporate DNS behind aardvark answers AAAA queries for
|
||||
# galaxy.ansible.com but not A queries — with no IPv6 route in the web
|
||||
# network ansible-galaxy downloads failed (EAI_AGAIN / empty responses).
|
||||
# Bypass with public resolvers; git.az-gruppe.com is publicly resolvable,
|
||||
# targets connect by IP (same pattern as librechat/homarr on AZ-CLD-1).
|
||||
extraOptions = ["--network=web" "--ip=10.89.0.17" "--add-host=postgres:10.89.0.1" "--dns=8.8.8.8" "--dns=8.8.4.4"];
|
||||
extraOptions = [
|
||||
"--network=web:ip=10.89.0.17"
|
||||
"--network=vpn-egress"
|
||||
"--add-host=postgres:10.89.0.1"
|
||||
"--dns=8.8.8.8"
|
||||
"--dns=8.8.4.4"
|
||||
];
|
||||
};
|
||||
|
||||
# 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).
|
||||
networking.firewall.extraForwardRules = ''
|
||||
iifname "vpn-egress" oifname "wt0" accept
|
||||
'';
|
||||
|
||||
systemd.services.semaphore-db-init = {
|
||||
description = "Provision semaphore role/database from agenix secret";
|
||||
after = ["postgresql.service"];
|
||||
@@ -49,8 +45,6 @@ in {
|
||||
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}"
|
||||
|
||||
Reference in New Issue
Block a user