Deployment

How to stand up a new app in infra.

Demo (preview)

For a demo app the whole platform already exists in the development account, so onboarding is straightforward.

1. Add the repo to repos.hcl

# infra/repos.hcl
demo_repos = [
  "demo-example",
  "demo-newthing",   # <- the list entry IS the GitHub repo name
]

That single line does three things via existing fan-out units:

  • creates the GitHub repo (demo_reposcatalog/github/demo-repos, open-push, no branch protection),

  • makes it a preview app (it joins the preview_repos union → Cloudflare Access app + OIDC trust),

  • sets its CI vars (PREVIEW_AWS_ROLE_ARN via preview_repo_vars, CODEARTIFACT_* via codeartifact_repo_vars).

2. Merge

Make an infra PR, get a review, merge.

On merge, everything will automatically happen.

3. Commit two files to the app repo

preview.toml at the repo root:

[preview]
app               = "demo-newthing"           # DNS slug + ECR image-tag prefix
worker_entrypoint = "tracker.queue.main:main" # the pgq worker's module:function
db_name           = "tracker"                 # Postgres DB name
base_branches     = ["main"]                  # branches that get a persistent trunk

.github/workflows/preview.yml — copy demo-example’s verbatim.


Production

A production app has a fully highly available stack.

1. Add the repo to repos.hcl

# infra/repos.hcl
prod_repos = [
  "example",
  "sales-pipeline",
  "new-locality",   # <- the live/internal/<dir> name; its env.hcl supplies app_name
]

2. Vend the AWS account (live/management)

# infra/live/management/terragrunt.stack.hcl
unit "account_new_locality" {
  source = "${get_repo_root()}/catalog/aws/account"
  path   = "account-new-locality"

  values = {
    name   = "new-locality"
    ou     = "internal" # or "customers" for a customer account
    client = true
  }
}

3. Create the environment config (live/internal/new-locality/)

This is “the specific environment configuration.” Two files, and the only one you edit is env.hcl.

env.hcl — the three knobs at the top are all you set; the rest are org defaults plus the few keys other files read. Purely-derived values (github_repo, customer) live in the shared terragrunt.stack.hcl, not here:

# infra/live/internal/new-locality/env.hcl
locals {
  # ── Per-app knobs: the only lines you change for a new app ───────────────────
  environment       = "new-locality"        # dir name = AWS account = customer label
  app_name          = "new-locality"        # GitHub repo under FSHTech/, ECR/ECS name prefix
  worker_entrypoint = "app.queue.main:main" # the pgq worker's module:function

  # ── Org defaults / contract keys other files read; leave as-is ───────────────
  aws_region        = "us-east-2"
  secondary_regions = ["us-west-2"]
  deploy_role       = "OrganizationAccountAccessRole"
  github_owner      = "FSHTech"
  customer_zone     = "${local.environment}.app.efesaitch.com"

  tags = {
    Environment = local.environment
    ManagedBy   = "terragrunt"
  }
}
cp live/internal/example/terragrunt.stack.hcl live/internal/new-locality/terragrunt.stack.hcl

That will give you a starting point; example has a full production deployment.

4. Delegate the DNS subzone (live/domain)

Gives the app’s own Route 53 zone (new-locality.app.efesaitch.com) authority, so its ACM certs can validate. The unit name must be <environment>-subzone-delegation — that’s the exact path env.hcl’s derived acm_delegation_path points at.

# infra/live/domain/terragrunt.stack.hcl
unit "new_locality_subzone_delegation" {
  source = "${get_repo_root()}/catalog/aws/route53-delegation"
  path   = "new-locality-subzone-delegation"

  values = {
    aws_region       = local.env.aws_region
j    parent_zone_path = "../route53-zone"
    subzone_name     = "new-locality.${local.env.delegated_zone}"
    child_zone_path  = "${get_repo_root()}/live/internal/new-locality/.terragrunt-stack/shared/.terragrunt-stack/route53-zone"
  }
}

5. Register the repo + web vars (live/source-code)

Two units: the GitHub repo itself, and the FE (S3/CloudFront) deploy vars. The web_vars unit reuses the prod_web_vars["<dir>"] map that’s derived automatically from prod_repos + env.hcl, so you just reference it by name.

# infra/live/source-code/terragrunt.stack.hcl
unit "new_locality" {
  source = "${get_repo_root()}/catalog/github/repo"
  path   = "new-locality"
  values = {
    repo_name          = "new-locality"
    description        = "New locality app"
    teams              = local.repo_teams
    push_allowed_teams = local.push_allowed_teams
    code_owner_teams   = local.code_owner_teams
    merge_bypass_teams = ["engineers-fte"]
    depends_on         = local.team_units
  }
}

unit "new_locality_web_vars" {
  source = "${get_repo_root()}/catalog/github/web-deploy-vars"
  path   = "new-locality-web-vars"
  values = local.prod_web_vars["new-locality"]
}

The CODEARTIFACT_* and (if you also want previews) PREVIEW_AWS_ROLE_ARN repo vars are already handled — they fan out from the derived codeartifact_consumer_repos / preview_repos sets. The BE deploy-role + deploy-repo-vars are part of the per-app stack you copied in step 3.

6. Grant human access (roster.hcl)

Add the new env to each role’s aws_access map that should reach it:

# infra/roster.hcl  (inside locals.roles)
engineers-fte  = { aws_access = { ..., "new-locality" = "Admin" } }
operations-fte = { aws_access = { ..., "new-locality" = "AppMaintainStaging" } }

7. Merge

Make an infra PR, get a review, merge.

On merge, everything will automatically happen.