aws_codebuild_project defined in Terraform under skyhub_terraform/modules/*/…_build_deploy.tf. A GitHub push-webhook triggers the build; the build produces an image (or a static bundle), pushes it, and rolls the target.
The gateway repo’s own GitHub Actions (
.github/workflows/pytest_unit_tests.yml, ruff.yml) only run pytest + Ruff — they never build or push an image. All image build/push and deploy happens in CodeBuild. For the gateway’s Docker build itself (Dockerfile.ecr, gunicorn runtime) see /deployment/gateway-build-runtime.The universal pipeline shape
Almost every service pipeline follows the same four-step recipe: buildDockerfile.ecr → push to ECR :latest → aws ecs update-service --force-new-deployment. The pipeline runs a docker-in-docker daemon inside a privileged CodeBuild container, logs in to ECR, builds with layer caching, pushes, and forces the Fargate service to pull the new :latest and replace its task.
The canonical example is the gateway (api) pipeline in skyhub_terraform/modules/api/api_build_deploy.tf:
modules/api/api_build_deploy.tf (buildspec, abridged)
Build environment
Compute
BUILD_GENERAL1_SMALL, privileged_mode = true (docker-in-docker). Most builds use the ARM image aws/codebuild/amazonlinux2-aarch64-standard:3.0 — matching the ARM64 Fargate tasks. Janus is the exception: it builds x86 on amazonlinux2-x86_64-standard:5.0.Caching & logs
cache { type = LOCAL, modes = [LOCAL_DOCKER_LAYER_CACHE, LOCAL_SOURCE_CACHE] } plus --cache-from :latest. Logs go to CloudWatch group /aws/codebuild/<project> with 7-day retention.api_build_deploy.tf explains it avoids taxing the single NAT gateway with dependency downloads (ingress is free). The two exceptions that must be in-VPC are the DB migration build (needs to reach database.skyhub-prod.internal) and the user-VPN build (SSHes to the jumphost private IP).
Trigger model: what fires a build
The webhook filter is environment-aware; the checked-out branch (source_version) is not. The single filter is defined once per environment in environments/prod/main.tf (and dev) and passed into every module as webhook_config:
environments/prod/main.tf
| Environment | Fires on | Example |
|---|---|---|
| prod | push of a git tag matching ^refs/tags/v_.*$ | git tag v_1.4.2 && git push origin v_1.4.2 |
| dev | push to the develop branch (^refs/heads/develop$) | git push origin develop |
The pipelines
Every project name is prefixed with the resource tag (skyhub-prod-…). Sources are all under github.com/ID-Robots/.
| Pipeline (CodeBuild project) | Source repo | source_version | Deploy action |
|---|---|---|---|
skyhub-prod-api-build-and-deploy | skyhub_gateway_service | develop | push :latest → force-new-deployment (gateway) |
skyhub-prod-api-migrate | skyhub_gateway_service | prod | build image, docker run … flask db upgrade (no ECS deploy; in-VPC, no webhook) |
skyhub-prod-frontend-build-and-deploy | skyhub_dashboard | development (repo_branch) | yarn build → sync dist/ to skyhub-prod-ui-bucket (no ECS) |
skyhub-prod-janus-build-and-deploy | skyhub_janus | development | push :latest → force-new-deployment (janus). x86, --no-cache |
skyhub-prod-whip-build-and-deploy | skyhub_whip | development | push :latest → force-new-deployment (whip) |
skyhub-prod-ws-proxy-build-and-deploy | skyhub_ws_proxy | main | push :latest → force-new-deployment (ws_proxy) |
skyhub-prod-vpn-build-and-deploy | skyhub_user_vpn | main | push :latest → SSH into jumphost, run deploy.sh (docker-compose) |
skyhub-prod-drone-build + layered builds | skyhub_core | main / cowboy/video | build Core images / template docker-compose.installer.yml → S3 (no webhooks — manual) |
CodeBuild builds and ECR pushes are the source of the redeploy-loop alarms on the gateway: a service that keeps replacing tasks trips the “pending tasks / deployment count / ‘Shutting down’ log spike” CloudWatch alarms in
modules/api/api.tf. See /deployment/storage-backup-alarms.Shared CodeBuild primitives
modules/codebuild/codebuild.tf defines the pieces every pipeline reuses:
aws_codebuild_source_credential— a GitHubPERSONAL_ACCESS_TOKEN(usertodor943) that authorizes CodeBuild to clone the privateID-Robots/*repos. The token itself comes fromvar.github_tokeninsettings.tfvars(a committed secret — rotate and move to SSM/Secrets Manager).aws_ssm_parameter/skyhub/prod/slack_webhook— the Slack incoming-webhook URL passed to builds asSLACK_WEBHOOK_URLfor start/finish notifications.permissive_sg— an all-traffic security group reused by in-VPC builds.
DB migration build (separate, manual)
Schema migrations are not part of the deploy.modules/api/api_migrations.tf defines an independent skyhub-prod-api-migrate project that builds the same Dockerfile.ecr image and then runs Alembic against the live database:
modules/api/api_migrations.tf (build/run, abridged)
- It runs inside the VPC (
vpc_configwith the API ECS security group) so it can reachdatabase.skyhub-prod.internal:5432. - Its
source_versionisprod, and it has no webhook — you start it manually (aws codebuild start-build --project-name skyhub-prod-api-migrate) when a release includes migrations. - The gateway’s own dev shortcut (
db.create_all()whenAPP_ENVIRONMENT=dev) is not used here — production always goes through Alembic. See /gateway/data/migrations-connection.
Frontend build (Angular → S3)
The dashboard has no runtime container — it is a static SPA.modules/frontend/frontend_build_deploy.tf clones skyhub_dashboard, builds it with yarn, and overwrites the UI bucket:
Install & notify
Pin Node with
n 24.11.1, post a Slack “build started”, then yarn install --frozen-lockfile.Build
yarn run build --configuration production (the build_configuration input; dev uses aws-dev). Produces dist/.skyhub-prod-ui-build-bucket (zipped build artifacts, transitioned to Glacier after 7 days) and skyhub-prod-frontend-build-cache (S3 cache, expired after 7 days). The prod webhook still applies, so a v_* tag on skyhub_dashboard rebuilds and republishes the SPA. Browsers reach that bucket through the jumphost nginx / CloudFront — see /deployment/networking-jumphost. For the Angular build configs themselves, see /dashboard/build-and-config.
Drone (Core) image pipelines
The on-drone images are built fromskyhub_core as a layered chain, each stage a separate CodeBuild project writing to its own ECR repo:
The final skyhub-prod-drone-build project doesn’t build an app image at all — it sed-templates the ECR image URLs, WHIP server, and Redis host into docker-compose.installer.yml and uploads it to s3://skyhubcore/ (prod writes docker-compose.prod.yml, dev writes docker-compose.yml, chosen by var.stage). That compose file is exactly what the gateway hands a physical drone on activation via an S3 presigned URL. See the activation flow in /gateway/api/drones-and-actions and drone-side pull auth in /gateway/security/vpn-middleware-jumphost.
user-VPN build (build + remote deploy)
The VPN service doesn’t run on ECS either — it runs as a container on the jumphost.modules/user_vpn/user_vpn_build_deploy.tf builds and pushes the image, templates docker-compose.aws.yml/deploy.sh (DB creds, VPN bucket, external IP), uploads them to s3://skyhub-prod-user-vpn/, then SSHes in and runs the deploy:
modules/user_vpn/user_vpn_build_deploy.tf (post_build, abridged)
3377, pulling the SSH key from SSM. See /ecosystem/user-vpn.
ECR registry conventions
All images land in the private registry<aws-account-id>.dkr.ecr.eu-central-1.amazonaws.com.
| Property | Value |
|---|---|
| App repos | skyhub-prod-{api,janus,whip,ws-proxy,vpn}-image |
| Drone repos | skyhub-prod-drone-{base,basebuild,ros2,rtsp,mavp2p,ws}-image |
| Tag | :latest (mutable) — every build overwrites it; ECS pulls :latest |
| Scanning | scan_on_push = true |
| Lifecycle | untagged images expire after 7 days |
| Repo policy | broad (Principal: "*", ecr:*) — see security notes below |
:latest tag, there is no image-digest history to roll back to in ECR itself — a rollback means rebuilding a previous ref. force-new-deployment is what makes ECS re-pull the tag (the tag string doesn’t change, so without the force flag Fargate would not know to redeploy).
Gotchas a future editor must preserve
CI (GitHub Actions) does not build images
CI (GitHub Actions) does not build images
The gateway’s
.github/workflows/* only run pytest + Ruff. If you expect a merge to auto-deploy via GitHub Actions, it won’t — deploys come from CodeBuild webhooks on tags (prod) or develop (dev).source_version vs webhook ref diverge
source_version vs webhook ref diverge
Hard-coded
source_version (develop/main/development/prod) only governs manually-started builds. Don’t assume a manual start-build deploys the same commit a tag push would.Migrations and drone/VPN builds are manual
Migrations and drone/VPN builds are manual
skyhub-prod-api-migrate and every drone pipeline have no webhook; the VPN build deploys by SSH, not ECS. Removing the manual step from a release runbook will ship code against an un-migrated DB or stale drone compose file.Committed secrets in the build path
Committed secrets in the build path
The GitHub PAT and Slack webhook live in
settings.tfvars; JWT_SECRET_KEY/MAIL_PASSWORD are literals in modules/api/api.tf. Build IAM roles and ECR repo policies are wildcard (ecs:*, ecr:*, ec2:*, Principal: "*"). Any hardening pass should move these to SSM/Secrets Manager and scope the roles. See /deployment/production-config.Related pages
Gateway Build & Runtime
Dockerfile.ecr, gunicorn/gevent worker model, entrypoint SSH tunnel.
ECS Fargate Services
What
force-new-deployment rolls: task defs, CPU/mem, Cloud Map DNS.Environments & Terraform State
Where
webhook_config and settings.tfvars come from; init/plan/apply.Storage, Backups & Alerting
Build/artifact buckets and the redeploy-loop CloudWatch alarms.

