Development Environment Scale Up / Scale Down Runbook

One command to raise or rest the staging tier, what it does that flipping dev_service_count by hand does not, and how to tell whether it actually worked.
Verified · 2026-09-15 · Phenom AI Agent
Source: Verified by running a full down/up cycle against the live account on 2026-09-15: scripts/dev-env-scale.sh down --apply then up --apply, with scripts/dev-env-status.sh confirming each end state. RDS resize to db.t4g.small observed reaching status=modifying. Service counts, RDS class and pending modifications read from describe-services / describe-db-instances, not from terraform output.
Signed stamp image pending re-generation

Companion to Development Environments & Workflow, which describes the topology. This page is how you turn it on and off.

Tooling lives in phenom-infra: scripts/dev-env-scale.sh and scripts/dev-env-status.sh.

The staging tier rests at zero to save roughly $63/month of Fargate, and is raised when someone needs to validate against it.

The command

cd phenom-infra
export CLOUDFLARE_API_TOKEN=...        # from wherever you keep secrets

scripts/dev-env-scale.sh up            # dry run: shows the plan, changes nothing
scripts/dev-env-scale.sh up --apply    # does it
scripts/dev-env-scale.sh down --apply
scripts/dev-env-scale.sh status        # report only

Dry run is the default. --apply is required to touch anything, and a dry run reverts its own edits before exiting.

If CLOUDFLARE_API_TOKEN is unset the script prompts for it at a terminal, and fails immediately in CI rather than hanging. It is required because the Cloudflare provider is configured in this environment; without it Terraform builds an incomplete plan that only fails minutes later at apply.

After up, re-run deploy-hasura-development on phenom-backend. The script prints this reminder and deliberately does not do it: the Hasura engine is one of the services that was at zero, so migrations could not apply while the tier rested.

Commit the Terraform edits in a PR afterwards. --apply is the break-glass path; dev-infra-ci auto-applying on merge is the normal one.

Checking without changing anything

scripts/dev-env-status.sh
Exit Meaning
0 Environment matches what Terraform believes
1 Drift, or a service failing to place
2 Could not determine (no credentials, missing tooling)

The third code exists so a missing credential is never reported as healthy. Safe to wire into CI or a cron.

Why a command rather than a checklist

dev_service_count in environments/development/locals.tf looks like the environment switch. It is one line that scales four ECS services, and treating it as the whole story has failed three distinct ways:

It does not resize the database. db_instance_class is a separate value in main.tf. Flipping dev_service_count brings services up against a resting database, so “scaled up” and “usable” diverge with nothing reconciling them.

It does not cover phenom-dev-nest-ops. That service was created out-of-band on 2026-04-06, is absent from Terraform state, and is invisible to any plan.

It reports desiredCount, not runningCount. On 2026-09-08 the environment read as fully up while three of five services sat at runningCount: 0. Terraform was satisfied, nothing was running, and it billed for seventeen days.

That last one is the whole design principle: desiredCount is what Terraform asked for, runningCount is what happened, and only the second is the environment.

What the command does, in order

# Step Why it is in that position
1 Edit db_instance_class and dev_service_count together Separate values; changing one without the other is the 2026-08-22 failure
2 Plan, refuse unexpected destroys Scaling removes only the CloudWatch alarms count-gated on dev_service_count. Anything else is not scaling
3 terraform apply All AWS mutation goes through Terraform
4 aws ecs update-service for phenom-dev-nest-ops The one exception: no Terraform resource to drive
5 Wait for placement, verify runningCount See above
6 Print the migration reminder Lives in phenom-backend’s CI, not here

The RDS resize goes first because services that come up against a db.t4g.micro run badly, and the symptom looks like an application bug rather than a sizing one.

A failed apply does not abort the run. Terraform applies in dependency order and does not roll back, so a mid-apply failure leaves the tier partially scaled. The script says so plainly, finishes the remaining steps rather than leaving it half-done, verifies, and exits non-zero regardless of what the probe reports.

Instance sizes

State Class vCPU / RAM ~$/mo
Resting db.t4g.micro 2 burstable / 1 GB 13
Raised db.t4g.small 2 burstable / 2 GB 25
Production, for contrast db.m5.large 2 / 8 GB 130

Staging deliberately does not match production. It serves at most a handful of people testing.

apply_immediately = true is set for development only, so a resize starts at once with the brief interruption that implies. Production keeps the default and resizes in a planned window. Without it the resize sits in PendingModifiedValues while terraform apply reports success and the account keeps running the old class.

What stays up on purpose

Resource ~$/mo Why
phenom-dev-alb 22 Preserves DNS and listener config; recreating changes hostnames
NAT ×2 in phenom-dev-vpc 66 phenom-staging-chat-user-provisioner is VPC-attached and needs egress
RDS (downsized) 13 Stopping loses the data; downsizing keeps it at about a tenth the cost

If a service will not place

running=0 against desired=1 is a placement failure, not a slow start:

aws ecs describe-services --cluster phenom-dev-cluster \
  --services <name> --query 'services[0].events[0:3].[createdAt,message]' --output text
  • CannotPullContainerError / 401 — the task pulls from a registry it cannot authenticate to. phenom-staging-synapse pulls a private Docker Hub image while production pulls the same app from ECR. Fix the image reference, not the credential.
  • Starts then dies — crash loop. Events say “unable to consistently start tasks”; the reason is in the task’s CloudWatch log group.

Known open problems

Live as of 2026-09-15. The probe flags all of these, so a non-zero exit today is expected rather than a broken script:

  • phenom-dev-storage fails to place. Tracked in phenom-infra#246.
  • phenom-staging-synapse / -admin fail to place on the image-pull 401 above.
  • phenom-dev-file-validator has been Inactive since 2026-06-06: “AWS Lambda does not have permission to access the provided code artifact.” Its S3 notification still fires into a function that cannot start, so staging uploads are never validated or tagged. It also fails every terraform apply against this environment, which is why the scale command has to survive a failed apply.