Architecture¶
Overview¶
┌─────────────────────────────┐
│ Cloudflare (tagai.uk) │
│ Access (Zero Trust) │
│ + per-device Tunnels │
└──────────────┬───────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
<id>.tagai.uk <id>-nodered.tagai.uk <id>-ssh.tagai.uk
(dashboard, (Node-RED editor, (SSH, admins
same target) admins only) only)
│ │ │
└─────────────────┼─────────────────┘
│
┌──────────▼──────────┐
│ Raspberry Pi │
│ - cloudflared (svc) │
│ - Node-RED (svc) │
└──────────┬───────────┘
│ mTLS
┌──────────▼───────────┐
│ EMQX MQTT broker │
│ mqtts.tagai.xyz:8883│
└───────────────────────┘
cloudflared and Node-RED both run as native systemd services — there is no
Docker anywhere in the deployed fleet.
Source of truth: config/devices.yml¶
Every device that should exist is declared once in config/devices.yml,
grouped by environment:
environments:
production: { prefix: "rpi-prod", devices: { <12-hex-serial>: {ip_address, location} } }
staging: { prefix: "rpi-staging", devices: {...} }
development: { prefix: "rpi-dev", devices: {...} }
lite_environments:
production: { prefix: "rpi-lite-prod", devices: {...} }
staging: { prefix: "rpi-lite-staging", devices: {...} }
development: { prefix: "rpi-lite-dev", devices: {...} }
Device ID = {prefix}-{serial}, e.g. rpi-prod-1883c404a12f. "Lite" devices
run a different Node-RED project and have extra bootstrap steps but use the
same tunnel/Access/mTLS mechanics as standard devices.
terraform/main.tf reads this file (yamldecode) and creates one device
module instance per entry. Known inconsistency: the production
environment's device ID is hardcoded as "rpi-prod-${serial}" in
terraform/main.tf rather than using environments.production.prefix like
every other environment does — currently harmless since the prefix value is
also "rpi-prod", but it means renaming that prefix in devices.yml alone
would silently not change production device IDs. Tracked in
decisions/optimization-proposal.md.
Ansible has its own, manually-maintained inventory
(ansible/inventory/production/hosts.yml and hosts-lite.yml) that must be
kept in sync with devices.yml by hand (scripts/generate-ansible-inventory.sh
helps, but doesn't run automatically). This duplication is a known rough
edge — see the optimization proposal.
Per-device networking¶
terraform/modules/device/main.tf creates, per device:
- One Cloudflare Tunnel (
cloudflare_zero_trust_tunnel_cloudflared) - Three DNS CNAME records, all pointing at the same tunnel:
<id>.tagai.uk— dashboard (proxies to Node-RED's HTTP port; intended as the read-only entry point)<id>-nodered.tagai.uk— Node-RED editor (same HTTP port; gated to admins only at the Access layer, see below)<id>-ssh.tagai.uk— SSH (proxies to port 22 over the tunnel)
The tunnel's ingress rules on the device itself
(ansible/roles/cloudflared/templates/cloudflared-config.yml.j2) route both
<id>.tagai.uk and <id>-nodered.tagai.uk to localhost:1880 (Node-RED),
and <id>-ssh.tagai.uk to ssh://localhost:22.
Ansible's own SSH connectivity defaults to the device's LAN IP
(local_host in inventory) and only routes over the Cloudflare Tunnel when
invoked with -e use_tunnel=true (injects
ProxyCommand="cloudflared access ssh --hostname %h"). Day-to-day fleet
management assumes you're on the same network as the devices; the tunnel is
the path for remote/off-network access.
Access control (Cloudflare Zero Trust)¶
terraform/modules/access/main.tf creates three separate Access
applications, each with its own policy:
| Application | Domain pattern | Who | IdPs |
|---|---|---|---|
| Dashboard | *.tagai.uk |
Admins + Users | Google, email OTP, admin group |
| Node-RED | *-nodered.tagai.uk |
Admins only | GitHub org team, Google, admin group |
| SSH | *-ssh.tagai.uk |
Admins only | GitHub org team, Google, admin group |
Because the more-specific *-nodered.tagai.uk and *-ssh.tagai.uk
applications exist, they take precedence over the *.tagai.uk dashboard app
for those hostnames — the dashboard app is effectively the fallback for
plain <id>.tagai.uk requests. Full detail in
security-access.md.
MQTT / EMQX¶
Node-RED on each device connects to EMQX at mqtts.tagai.xyz:8883 using
per-device mTLS client certificates (2-year validity, generated by
task mqtt:cert / scripts/provision-mqtt-certs.sh). See
mqtt-emqx.md and
reference/emqx-certificate-auth-setup.md.
Node-RED deployment model¶
Flows are managed via Node-RED's Projects feature: each device
auto-clones ssh://git@github.com/mbTagai/gaisro-technika-nodered (or the
lite equivalent) on first boot and can be updated in place via
task deploy-nodered (ansible/playbooks/deploy-nodered-project.yml). See
reference/nodered-git-deployment.md
and reference/nodered-projects-setup.md.
Environments¶
Three tiers, promoted in order: development → staging → production
(plus a parallel lite variant of each). Deploy/health/restart tasks exist
per tier (task ansible:deploy-dev, health-staging, restart-prod, ...) —
see reference/ansible-environment-deployment.md.