skyhub_terraform repo as ~16 reusable modules under modules/, composed per-environment by a thin root module under environments/<env>/. Each environment is a self-contained Terraform working directory with its own S3 remote-state key, its own provider/backend block, and its own var-file. You never run Terraform from the repo root — you cd into an environment directory and run init/plan/apply there.
This page is the operational entry point for running Terraform. For what the resulting resources actually are, see AWS Infrastructure Overview, ECS Fargate Services, and VPC, WireGuard Jumphost & nginx Routing. For the gateway’s runtime env vars, see Gateway Environment Variables and AWS Production Configuration.
Repository layout
skyhub_terraform/
module blocks at the same shared code via source = "../../modules/<name>" (see environments/prod/main.tf:14), so a change to a module affects every environment on its next apply. The environment directory only supplies inputs and wiring — which modules to instantiate, and what values to feed them.
The S3 remote state backend
State is stored remotely in one shared S3 bucket, keyed per environment. The backend is declared inside theterraform{} block of each environment’s provider_config.tf:
environments/prod/provider_config.tf
| Environment | State bucket | State key | Region |
|---|---|---|---|
dev | skyhub-terraform-environment-states | skyhub-dev/terraform.tfstate | eu-central-1 |
prod | skyhub-terraform-environment-states | skyhub-prod/terraform.tfstate | eu-central-1 |
<aws-account-id> (eu-central-1). Provider versions are pinned identically across environments — aws = 5.55.0 and kreuzwerker/docker = 3.0.2 — and locked in each env’s .terraform.lock.hcl.
settings.tfvars and variable inputs
Each environment declares its variables ininputs.tf and supplies values through a --var-file. The prod environment ships a committed settings.tfvars:
environments/prod/settings.tfvars
environment and client_name drive the resources_tag = "${var.client_name}-${var.environment}" local (e.g. skyhub-prod), which prefixes the name of nearly every AWS resource and forms the Cloud Map DNS namespace skyhub-prod.internal. aws_region sets the provider region. github_token and slack_webhook are consumed by the CodeBuild/alarms modules.
The dev environment has no committed settings.tfvars — you must create one before running (the README shows the template). Dev’s inputs.tf is far larger than prod’s and carries dev-friendly defaults for the whole gateway config surface (jwt_secret_key, mail_*, otel_*, remote_docker_*, sitl_*, Janus/WHIP tuning), so a dev plan needs only the five client/environment/region vars in a hand-written settings.tfvars.
The init / plan / apply workflow
Run every command from inside the environment directory so Terraform picks up that env’s backend key and.tf files.
Configure AWS credentials
Terraform uses your AWS CLI credentials (account
<aws-account-id>, region eu-central-1). Ensure they are exported / in your profile before init — the S3 backend and the aws_ecr_authorization_token data source both need them.terraform init
Downloads the pinned providers and For dev, first create
modules/, and configures the S3 backend for this environment.settings.tfvars (prod already has one committed):terraform apply
Provision / update the environment.On apply, module dependencies force the ECS cluster and Cloud Map namespace up first (
depends_on = [module.our_ecs] throughout main.tf), then the services; the WireGuard jumphost’s nginx config is (re)written by a remote-exec provisioner. The frontend module’s CodeBuild ultimately syncs the Angular dist/ into the skyhub-<env>-ui-bucket S3 bucket.terraform destroy --var-file=settings.tfvars.
Two distinct “deploy” triggers
There are two independent deploy mechanisms, and it is easy to conflate them:- Infrastructure deploys are manual.
terraform applyis run by hand from an environment directory. There is no pipeline that runs Terraform for you — nothing applies infrastructure on a git push. - Application-image deploys are automatic, driven by GitHub webhooks on the per-service repos (gateway, dashboard, janus, whip, ws_proxy, drone). The webhook filter is environment-aware:
environments/prod/main.tf (and dev/provider_config.tf)
| Environment | Trigger | Git ref pattern |
|---|---|---|
prod | Push a version tag | ^refs/tags/v_.*$ (e.g. v_1.4.0) |
dev | Push to the develop branch | ^refs/heads/develop$ |
:latest image to ECR, then forces a new ECS deployment (aws ecs update-service --force-new-deployment). This webhook config is passed into every CI/CD-bearing module (module.our_api, our_janus, our_whip, our_ws_proxy, our_frontend, our_user_vpn) via the shared webhook_config input, so all services in an environment share one trigger convention. (The our_drone module’s CodeBuild webhooks are commented out and not wired to webhook_config, so drone images are not auto-deployed by this trigger.) Full pipeline mechanics live in CI/CD: CodeBuild, ECR & Frontend Deploy.
dev and prod root modules have diverged
The two root modules are not parameter-for-parameter identical, which matters when auditing or promoting a change between them.Prod's our_api block omits required variables (a prod plan errors as-is)
Prod's our_api block omits required variables (a prod plan errors as-is)
modules/api/inputs.tf declares ~50 variables, and roughly two dozen of them — jwt_secret_key, mail_server/port/username/password, aws_access_key_id/secret, otel_exporter_otlp_*, remote_docker_enabled/host, docker_host_ip, user_sitl_max_count, sitl_cpu/memory_limit, janus_url, whip_server_url, sitl_video_stream_drone_state, log_level, socket_ip, socketio_ping_* — have no default. The dev module "our_api" block passes all of them (environments/dev/main.tf:246-268); the prod module "our_api" block does not (environments/prod/main.tf:213-250). As captured on this branch, a prod terraform plan would therefore error demanding those required variables. The prod api.tf also still hardcodes several of these as literal task-def env values. A refactor must reconcile the two paths — see AWS Production Configuration.Other dev/prod differences
Other dev/prod differences
docker_host_location: prod<office-docker-host>, dev<office-host>(the on-prem/office Docker host the gateway uses for SITL + rosbridge).- Frontend
build_configuration: prod"production"(plusrepo_branch = "development"), dev"aws-dev"— the Angular build profile the frontend CodeBuild uses. module.our_lambdasis instantiated only in devmain.tf, not prod.module.our_alarmsis instantiated only in prodmain.tf, not dev; dev’sinputs.tf/main.tfare generally more parameterized and current.local.frontend_domainis"dev.skyhub.ai"in bothprovider_config.tffiles — a leftover dev value in prod (the user-facing prod domain isprod.skyhub.ai).
Adding a new environment
Create the environment directory
Copy an existing env (dev is the more complete template) to
environments/<new-env>/ and keep the four core files: provider_config.tf, inputs.tf, main.tf, vpc_configuration.tf (plus outputs.tf and wireguard/).Set a unique backend state key
In
provider_config.tf, change the backend key to skyhub-<new-env>/terraform.tfstate. Keep the same bucket = "skyhub-terraform-environment-states" and region. A duplicate key would clobber another environment’s state.Write settings.tfvars
Set
client_name, client_description, environment=<new-env>, environment_description, aws_region. Set environment correctly — it flips the CodeBuild trigger (prod → tags v_*, anything else → develop) and forms resources_tag.Check VPC CIDR / region for collisions
vpc_configuration.tf hardcodes CIDR 172.31.0.0/16, a single AZ, and one NAT gateway. If the new environment shares an account/region and must peer, give it a non-overlapping CIDR.Where things live — quick reference
| Concern | Location |
|---|---|
| Remote state | S3 skyhub-terraform-environment-states, key skyhub-<env>/terraform.tfstate |
| Backend + provider pins | environments/<env>/provider_config.tf |
| Committed secrets (prod) | environments/prod/settings.tfvars (GitHub PAT, Slack webhook) |
| Committed secrets (dev defaults) | environments/dev/inputs.tf (jwt_secret_key, mail_password, OTLP token) |
| Module wiring / composition | environments/<env>/main.tf |
| Shared module code | modules/<name>/ |
| App-deploy trigger (prod) | git tag v_* → CodeBuild → ECR → ECS |
| App-deploy trigger (dev) | push to develop → CodeBuild → ECR → ECS |
| Infra deploy | manual terraform apply --var-file=settings.tfvars |

