Deployment¶
The pipeline in .github/workflows/deploy.yml builds, tests and deploys CloudSheets.
This page describes how it picks a target environment and how to move it from dev
to prod.
One switch, everything else derived¶
There is exactly one place where the target environment is chosen:
The ECR repository, the ECR stack and the names of all CDK stacks are derived from
that value — they are never configured separately. The mapping lives in
infra/hello-cdk/lib/environments.json, which both sides read:
- the workflow, through
.github/scripts/resolve-environment.mjs - the CDK app, through
infra/hello-cdk/lib/environment.ts
dev |
prod |
|
|---|---|---|
| ECR stack | EcrDevStack |
EcrStack |
| ECR repository | cloudsheets-backend-dev |
cloudsheets-backend |
| Database / VPC | BackendStack |
BackendStack-prod |
| S3 / CloudFront / Cognito | FrontendStack |
FrontendStack-prod |
| Backend service | EcsExpressStack |
EcsExpressStack-prod |
| Frontend bucket | cloudsheets-frontend-bucket |
cloudsheets-frontend-bucket-prod |
| ECS service | cloudsheets-backend |
cloudsheets-backend-prod |
| Cognito domain prefix | cloudsheets-auth-dev |
cloudsheets-auth-prod |
| SSM credentials | /cloudsheets/dev/db/* |
/cloudsheets/prod/db/* |
Why dev has no suffix
The dev stacks are already deployed under their unsuffixed names. Renaming them
would not move anything: CloudFormation would create a second set and leave the
RDS instance, the S3 bucket and the CloudFront distribution behind — orphaned but
still billing. prod is the environment that does not exist yet, so it is the one
that gets the suffix. The ECR stacks keep their historical names for the same
reason.
Promoting the pipeline from dev to prod¶
-
Change the one line in
.github/workflows/deploy.yml:Do not add
ECR_REPOSITORYorECR_STACKnext to it. Theresolve-environmentjob fails the run if either is set to anything other than the derived value. -
Open a pull request against
develop. The pull request already runsCDK synth (prod), which synthesizes the full prod app and asserts that it contains prod stacks only. A mismatch fails there, before any AWS call. -
Check
cdk difffor prod locally before merging:Everything should show up as new. If a resource shows as a modification, a name is still shared between the two environments and prod would take dev's resource over.
-
Merge, then approve. A push to
developormainruns everything up to the deploy job, which then waits for an approval on theprodGitHub environment — the reviewers configured there are the ones who can release it. The target environment is printed as a run annotation and in the job summary before the wait, so the approver can see what they are approving. -
Verify in AWS — the parameters carry the environment in their path:
Before the first prod deploy¶
- It creates a second set of everything. A second RDS instance, a second VPC with two NAT gateways (~$65/month, not free tier), a second CloudFront distribution. Nothing is shared with dev by design.
- The Cognito callback URLs change, because they are derived from the new CloudFront domain. Users of the dev pool do not exist in the prod pool.
prodremoval policies areRETAIN.cdk destroywill leave the database, the bucket, the ECR repository and the SSM parameters behind. That is intentional, but it means clean-up is manual.- Both environments can run at the same time. Deploying prod does not touch dev.
If the intention is to move rather than to add, destroy dev afterwards:
npx cdk destroy --all -c environment=dev.
What stops a mismatched deployment¶
Four independent guards, in the order they fire:
| Guard | Where | Catches |
|---|---|---|
resolve-environment.mjs |
resolve-environment job |
an unknown CDK_ENVIRONMENT, or a re-introduced ECR_STACK / ECR_REPOSITORY that disagrees with the mapping |
test/environment.test.ts |
test-infra job |
the mapping changing by accident, and the workflow starting to hardcode names again |
assert-environment-synth.mjs |
synth-environments job (dev and prod) |
a synthesized app that contains a stack, a repository or a bucket of the other environment |
resolveEnvironment() |
every cdk invocation |
-c environment=<typo>, which used to synthesize a half-configured app instead of failing |
Beyond that, bin/hello-cdk.ts only instantiates the stacks of the selected
environment. cdk deploy --all -c environment=dev therefore cannot create the prod
registry, and cdk deploy EcrStack -c environment=dev fails with "no stack found"
rather than pushing dev images into the prod registry.
Approval before a deploy¶
The deploy job is bound to a GitHub environment of the same name:
environment:
name: ${{ needs.resolve-environment.outputs.name }}
url: ${{ steps.endpoint.outputs.url }}
The environment's protection rules apply before the job's first step. With
required reviewers configured under Settings → Environments → dev / prod, a
merge does not deploy on its own: the run stops at "Review deployments" and waits for
someone to release it. Unapproved runs expire after 30 days, and who approved what is
kept in the run history.
Because the name is derived from CDK_ENVIRONMENT like everything else, switching to
prod also switches to the prod reviewers — and to the prod environment secrets, if
AWS_GITHUB_ACTIONS_ROLE_ARN is stored per environment rather than repository-wide.
An environment without reviewers is only a label
Creating the environment is not the gate. Without at least one required reviewer, the job runs straight through and the merge deploys immediately.
Which branches deploy¶
| Event | Runs | Deploys |
|---|---|---|
Pull request to develop or main |
all checks | no |
Push to develop |
all checks | after approval |
Push to main |
all checks | after approval |
workflow_dispatch |
all checks | only on develop / main, after approval |
Pushes to a feature branch trigger nothing — open a pull request to get CI.
Running the CDK app locally¶
cd infra/hello-cdk
npm ci
# dev is the default; prod has to be asked for explicitly
npx cdk synth --all
npx cdk synth --all -c environment=prod
# the frontend build has to exist first -- FrontendStack reads frontend/dist
cd ../../frontend && npm ci && npm run build
The same two scripts the pipeline uses run locally: