Deployment¶
The platform runs on Scaleway (containers, PostgreSQL, object storage, secrets,
VMs) and Cloudflare (Pages, Workers, DNS). Infrastructure is OpenTofu in
infra/terraform; each service repo's .gitlab-ci.yml builds and deploys it.
Environments: shared (cross-environment resources), dev, stg, prod.
Topology¶
graph TB
Browser[Browser]
subgraph "Cloudflare"
Portal[Portal<br/>Cloudflare Pages]
Realtime[Realtime Worker<br/>+ GatewayRoom DO]
end
subgraph "Scaleway, per environment"
Nexus[nexus container]
Authz[authz container]
Bucket[(OTA release bucket)]
Relays[Relay VMs]
end
subgraph "Scaleway, shared"
RDB[(Shared RDB<br/>one database per env)]
Zitadel[Zitadel container<br/>auth.digilan-edge.com]
ZDB[(Zitadel RDB)]
end
Agent[Edge agent<br/>OpenBSD]
Browser --> Portal
Portal --> Nexus
Portal <--> Realtime
Portal --> Zitadel
Agent --> Nexus
Agent <--> Realtime
Agent --> Bucket
Agent <--> Relays
Nexus --> RDB
Nexus --> Authz
Nexus --> Zitadel
Nexus --> Bucket
Authz --> RDB
Realtime <--> Nexus
Relays --> Nexus
Zitadel --> ZDB
What runs where¶
| Runtime | What | Source |
|---|---|---|
Scaleway Serverless Containers, namespace digilan-<env> |
nexus-<env>, authz-<env>; radius-api-<env> and Monsieur Wifi where enabled |
modules/environment |
| Scaleway Serverless Container (shared) | Zitadel | environments/shared/zitadel.tf |
Scaleway Managed PostgreSQL digilan-shared (PostgreSQL 17) |
One database per environment (db_name), used by nexus and authz; radius-<env> and mrwifi-<env> where enabled |
environments/shared/database.tf, modules/database |
Scaleway Managed PostgreSQL zitadel (PostgreSQL 16) |
Zitadel's database | environments/shared/zitadel.tf |
Scaleway Container Registry rg.fr-par.scw.cloud/digilan |
nexus, authz, zitadel, radius-api, radius-freeradius images |
environments/shared/main.tf |
| Scaleway Object Storage | digilan-agent-releases-<env>, private |
modules/environment/ota.tf |
| Scaleway Instances | Relays (relay_count), FreeRADIUS VM (where enabled) |
modules/relay, modules/environment |
| Scaleway Secret Manager | JWT keypair, release publisher key, OTA signing key | modules/environment, environments/shared/ota-signing.tf |
| Scaleway Transactional Email | Zitadel mail | environments/shared/email.tf |
| Cloudflare Pages | digilan-portal-<env> (and digilan-radius-<env>) |
modules/dns |
| Cloudflare Workers | digilan-realtime-{dev,stg,production} |
platform/realtime/wrangler.toml |
Cloudflare DNS, zone digilan-edge.com |
portal, nexus, authz, auth CNAMEs; relay host records; realtime custom domain | modules/dns, modules/relay, wrangler.toml |
All serverless containers default to min_scale = 0, so the first request
after an idle period pays a cold start.
Hostnames¶
| Service | dev | stg | prod |
|---|---|---|---|
| Portal | dev.portal.digilan-edge.com |
stg.portal.digilan-edge.com |
portal.digilan-edge.com |
| Nexus | dev.nexus.digilan-edge.com |
stg.nexus.digilan-edge.com |
prod.nexus.digilan-edge.com |
| Authz | dev.authz.digilan-edge.com |
stg.authz.digilan-edge.com |
authz.digilan-edge.com |
| Realtime | dev.realtime.digilan-edge.com |
stg.realtime.digilan-edge.com |
realtime.digilan-edge.com |
| Zitadel | auth.digilan-edge.com (shared) |
Prod nexus keeps the prod. prefix; the other prod services do not.
How services are released¶
| Component | Trigger | dev | stg / prod |
|---|---|---|---|
| nexus | git tag | builds :dev, :stg, :prd images; deploy_dev runs automatically |
deploy_stg / deploy_prd manual |
| authz | git tag | builds all three images; deploy_dev manual |
manual |
| portal | git tag | deploy_dev automatic |
deploy_stg / deploy_prd manual |
| realtime | git tag | deploy_dev automatic |
deploy_stg / deploy_prd manual |
| edge agent | git tag | publishes to beta in dev and prod |
promote:stable:<env>:<arch> manual (dev, prod) |
| radius-ctrl | git tag | API and SPA deploy to dev | not deployed |
Merging to main builds and deploys nothing in any of these repos; a merge
request runs checks only. Details: Nexus, Portal,
Agent releases.
Standing up an environment¶
- Shared first.
environments/sharedcreates the registry, the shared RDB, Zitadel and its database, mail, the OTA signing key and the nexus service user. Zitadel's SMTP, login policy and service user needzitadel_pat(an instance-level admin PAT) and a second apply. - Environment tfvars. Fill
environments/<env>/terraform.tfvarsfromtfvars.example, including theshared_rdb_*values from the shared outputs,zitadel_url,zitadel_patandnexus_zitadel_service_token. - Apply the environment.
tofu applyinenvironments/<env>creates the database, container namespace, containers, OTA bucket and keys, Pages project, DNS and Zitadel applications. Containers stay inerroruntil their images exist. - Images. Tag
platform/nexusandplatform/authz, then run their deploy jobs for the environment. - Realtime secret. Set the Worker's
INTERNAL_API_KEYfrom theinternal_api_keyoutput (see Terraform → Secrets), then run the realtimedeploy_<env>job. - Portal. Set the portal CI variables
<ENV>_ZITADEL_CLIENT_IDand<ENV>_ZITADEL_PROJECT_IDfrom theportal_client_idandzitadel_project_idoutputs, then run the portaldeploy_<env>job. - OTA. Set the edge-gateway CI variables listed in Agent releases and tag the agent.
- First admin. Insert the first
PLATFORM_ADMINgrant (Authz); everything after that is done from the portal. - Relays (dev and prod). Set
relay_count,relay_zone_suffixandrelayd_package_url(token asTF_VAR_relayd_package_token) and apply. See Relay Fleet.