skyhub_terraform repo and applies to the skyhub-prod environment in eu-central-1 (account <aws-account-id>).
Three facts frame the whole page:
- Postgres is the only durable datastore with backups. It runs as an ECS Fargate task on an EFS volume (not RDS) and is protected by an AWS Backup daily plan. See ECS Fargate Services for the task definition.
- Redis has no persistence and no backup — it is a pure cache / pub-sub relay. Treat it as disposable.
- S3 holds everything else: the built frontend, user/drone assets, per-user VPN configs, Terraform state, and CI build artifacts. There is no cross-region replication.
Storage map
S3 buckets
Every bucket below is created ineu-central-1 with resources_tag = skyhub-prod. Bucket names are infrastructure facts (not secrets).
| Bucket | Defined in | Access | Versioning / lifecycle | Purpose |
|---|---|---|---|---|
skyhub-prod-ui-bucket | modules/frontend/frontend.tf | Public read (bucket policy s3:GetObject / Principal: *, all public-access-blocks off) | Versioning Suspended | Static Angular SPA; website config with index.html + 404→index.html. Fronted by CloudFront and by the jumphost nginx. |
skyhub-prod-ui-build-bucket | modules/frontend/frontend_build_deploy.tf | Private | Lifecycle stash-old-builds: transition to GLACIER after 7 days | CodeBuild frontend build artifacts (frontend-build.zip). |
skyhub-prod-frontend-build-cache | modules/frontend/frontend_build_deploy.tf | Private | Lifecycle cleanup-old-cache: expire after 7 days | CodeBuild S3 cache (/root/.cache/pip, .angular/). |
skyhub-prod-assets | modules/assets/assets.tf | Private (ACL private, ownership BucketOwnerPreferred) | None; CORS allows GET/PUT/POST/HEAD from any origin | Drone/user/mission assets. Read by the gateway’s AssetService via the ASSET_BUCKET env var; the UI reads from skyhub-prod-assets.s3.eu-central-1.amazonaws.com/drone. |
skyhub-prod-user-vpn | modules/user_vpn/user_vpn.tf | Private | Versioning Enabled; CORS for uploads | Per-user WireGuard configs. Referenced by the gateway as VPN_BUCKET. See User VPN & Network Isolation. |
skyhub-terraform-environment-states | backend block (environments/prod/provider_config.tf) | Private | — | Terraform remote state; prod key skyhub-prod/terraform.tfstate. Detailed in Environments, State & Terraform Workflow. |
skyhubcore | external (written to by modules/drone/drone_build.tf:190) | Private | — | Target for the generated drone docker-compose.installer.yml (prod: docker-compose.prod.yml). Not created by this repo. See CI/CD: CodeBuild, ECR & Frontend Deploy. |
Live prod also contains buckets that are not managed by this Terraform, seen only in an AWS inventory:
skyhub-dashboard-assets, skyhub-3d-assets (3D models), and skyhub.ai. Treat them as out-of-band — a terraform apply will neither create nor track them.ASSET_BUCKET and VPN_BUCKET from its task environment; see Gateway Environment Variables for the full list.
EFS-backed Postgres & AWS Backup
Postgres persistence is an EFS file system (modules/database/postgre.tf), not an EBS volume or RDS:
- File system
creation_token = skyhub-prod-postgres-efs, taggedName = skyhub-prod-PostgresEFS. - A mount target in the first private subnet; an access point rooted at
/var/lib/postgresql/data(uid/gid0). - The ECS task mounts it as the
postgres-datavolume with transit encryption enabled. desired_count = 1, no read replicas — durability rests entirely on EFS + AWS Backup.
modules/database/backup.tf. A single backup vault (skyhub-prod-postgres-backup-vault, KMS key aws/backup) holds all recovery points, and one backup plan (skyhub-prod-postgres-backup-plan) carries two rules:
| Rule | Schedule | Retention |
|---|---|---|
daily-efs-backups | cron(0 0 * * ? *) — daily 00:00 UTC | 14 days |
daily-ec2-backups | cron(0 1 * * ? *) — daily 01:00 UTC | 7 days |
postgres_efs_backup_selection→ resources taggedName = skyhub-prod-PostgresEFS(the EFS file system).jumphost_ec2_backup_selection→ resources taggedBackup = daily(the WireGuard jumphost EC2 — see VPC, WireGuard Jumphost & nginx Routing).
The AWS Backup selections use
db_task_role as their iam_role_arn — the same role the Postgres ECS task runs under. Its trust policy includes backup.amazonaws.com, and its inline policy is Action: "*" / Resource: "*". That works, but it is far broader than AWS Backup needs; a least-privilege pass should give backup its own role.user-vpn and ui buckets rely only on S3 versioning where enabled), and CloudWatch logs (the API and CodeBuild log groups set retention_in_days = 7).
Durability & recovery matrix
| Store | Persistence | Backup | Recovery window |
|---|---|---|---|
Postgres (database.skyhub-prod.internal) | EFS, transit-encrypted | AWS Backup (EFS) | 14 days (+7-day 2nd point) |
| Jumphost EC2 | EBS root (25 GB gp3) | AWS Backup (EC2, tag Backup=daily) | 7 days (+14-day 2nd point) |
| Redis | None | None | Not recoverable |
skyhub-prod-assets | S3 | None | Live objects only |
skyhub-prod-user-vpn | S3, versioned | None | Prior object versions |
skyhub-prod-ui-bucket | S3, versioning suspended | None | Live objects only (rebuildable from CI) |
S3 gateway VPC endpoint
modules/vpc_endpoints/main.tf provisions a single S3 Gateway VPC endpoint (com.amazonaws.eu-central-1.s3, type Gateway) so S3 traffic from the private subnet does not egress through the single NAT gateway — a deliberate cost optimization for a one-NAT topology.
ecr.dkr, ecr.api) are commented out — the code note explains they are only worth their per-hour cost under frequent image pulls; today ECR pulls still traverse the NAT gateway.
The module receives
subnet_ids but the endpoint resource never uses it, and no route_table_ids are wired. A gateway endpoint only routes traffic once it is associated with the route tables serving those subnets — confirm that association exists before assuming S3 traffic is actually bypassing NAT. This is a good candidate for a future cleanup.Alerting: SNS + Slack + email
Operational alarms funnel through one SNS topic and fan out to email inboxes and a Slack channel. The pipeline lives inmodules/alarms/main.tf; the topic ARN is exported as alarm_sns_topic and consumed by the API module.
Fan-out targets (all subscribe to skyhub-prod-ecs-alerts-topic):
| Subscription | Protocol | Endpoint |
|---|---|---|
todor_alerts_mail | <engineer-email> | |
yanko_alerts_mail | [email protected] | |
karthi_alerts_mail | <engineer-email> | |
slack_lambda_subscription | lambda | skyhub-prod-slack_alert_lambda |
slack_notifier.js, packaged as modules/alarms/slack_notifier.zip) reads SLACK_WEBHOOK_URL from its env (set to the checked-in slack_webhook var), then for each SNS record POSTs a :rotating_light: *ALARM* block carrying the alarm subject and message to Slack, tagging the on-call trio. Its role is the AWS-managed AWSLambdaBasicExecutionRole plus an SNS invoke permission. SMS subscriptions are present but commented out (sandbox/verification friction).
slack_webhook is committed in plaintext in environments/prod/settings.tfvars and flows into the Lambda’s environment. Rotating it means editing the tfvars and re-applying. A refactor should move it to SSM/Secrets Manager.Current alarm consumers
Today only the API (gateway) module publishes alarms, and all three target the SNS topic (alarm_actions = [var.alarm_topic_arn]). They exist to catch redeploy/restart loops — a real risk because ECS runs at 100% Fargate Spot (see ECS Fargate Services).
| Alarm | Metric | Fires when | Defined in |
|---|---|---|---|
skyhub-prod-API-Pending-Tasks-High | PendingTaskCount (ECS/ContainerInsights) | > 0.3 avg over 3×5 min (~15 min) | modules/api/api.tf:367 |
skyhub-prod-API-Multiple-Deployments | DeploymentCount (ECS/ContainerInsights) | > 1.4 avg over 4×2 min (~8 min) | modules/api/api.tf:386 |
skyhub-prod-API-Shutting-Down-Log-Spike | API-ShuttingDownCount (log metric filter on Shutting down) | Sum > 4 over 3×4 min (~12 min) | modules/api/api.tf:421 |
Wiring up a new alarm
Define the metric or log filter
Add an
aws_cloudwatch_metric_alarm in the relevant service module. For log-derived signals, first add an aws_cloudwatch_log_metric_filter against that service’s log group (the Shutting down filter in modules/api/api.tf:406 is the pattern to copy), then alarm on the emitted metric.Route it to the SNS topic
Set
alarm_actions = [var.alarm_topic_arn] on the alarm so it publishes to skyhub-prod-ecs-alerts-topic and reaches both email and Slack.Pass the topic ARN into the module
Thread
module.our_alarms.alarm_sns_topic into the module as an input (the API module does this via alarm_topic_arn = module.our_alarms.alarm_sns_topic in environments/prod/main.tf). Declare a matching variable in the module’s inputs.tf.Add a recipient (optional)
To page a new person, add an
aws_sns_topic_subscription with protocol = "email" in modules/alarms/main.tf. Email subscriptions require the recipient to click the AWS confirmation link before they receive anything.Apply
Run
terraform plan --var-file=settings.tfvars then apply from environments/prod. See Environments, State & Terraform Workflow.Related pages
ECS Fargate Services
The Postgres/Redis Fargate tasks, EFS mount, and Spot capacity model behind these backups and alarms.
Environments, State & Terraform
The
skyhub-terraform-environment-states bucket, state keys, and the apply workflow.CI/CD: CodeBuild, ECR & Frontend Deploy
How the build/cache buckets and the
skyhubcore installer bucket get populated.S3 Assets, HLS Video & Archives
How the gateway’s AssetService reads and writes
skyhub-prod-assets.
