Environment Consolidation: Promote Staging to Production
Context
Verdict: Feasible β Recommended
No hard blockers. The main work is DNS migration, SES reconfiguration, and CDK config updates. Data stays in place β no migration needed.
Key Consequences
What You Gain
- ~$150/month savings (~$1,800/year) by not running production account
- Zero data migration β staging already has all production data
- Simpler operations β one environment to monitor, patch, and maintain
What You Lose
- No pre-production validation environment β deploys go straight to users
- No safe space for DB migration testing β Flyway runs against real data
- Stack names stay
BATbern-staging-*β cosmetic only, but visible in AWS Console
Mitigations
- Your 4-layer test suite (shell, Bruno, Playwright, CDK) runs in CI before deploy
- ECS rolling deployments with health checks provide automatic rollback
- RDS automated snapshots (increase to 14 days) enable point-in-time recovery
- On-demand ephemeral staging: spin up temp services in dev account for risky changes via CDK
Implementation Progress
Phase 1: Preparation β COMPLETE β
- β SES sandbox status checked β already out of sandbox
- β DNS TTL lowered
- β RDS snapshot taken
- β DNS records documented
Phase 2: DNS Migration β COMPLETE β (2026-03-11)
- β
Created
batbern.chhosted zone in staging account (Z08825557YYLWVHISLPY) - β Copied all DNS records from production hosted zone
- β
Updated domain NS records via management account (510187933511, profile
batbern-mgmt)- Domain registered via Gandi SAS (Route53 backend registrar for .ch TLDs)
- NS delegation now points to staging accountβs hosted zone
- β
DNS propagation verified (2026-03-11 β confirmed via
dig @a.nic.ch batbern.ch NS) - Old hosted zone in production account β delete after 30 days
Phase 3: CDK Config Updates β COMPLETE β
All infrastructure code updated to use isProduction flag pattern.
Files modified:
| File | Change | Status |
|---|---|---|
infrastructure/lib/config/environment-config.ts |
Added isProduction?: boolean to interface |
β |
infrastructure/lib/config/staging-config.ts |
Domain β batbern.ch, isProduction: true, RDS hardening (14-day backup, deletion protection), GDPR tag |
β |
infrastructure/lib/config/prod-config.ts |
Added @deprecated notice |
β |
infrastructure/bin/batbern-infrastructure.ts |
Domain derivation from config instead of envName | β |
infrastructure/lib/stacks/cognito-stack.ts |
isProdTraffic from isProduction flag; email/domain/callback conditionals |
β |
infrastructure/lib/stacks/inbound-email-stack.ts |
isProdTraffic for reply domain (replies@batbern.ch) |
β |
infrastructure/lib/stacks/network-stack.ts |
Domain from config instead of envName | β |
infrastructure/lib/stacks/api-gateway-stack.ts |
CORS origins from isProduction flag |
β |
infrastructure/lib/stacks/api-gateway-service-stack.ts |
APP_ENVIRONMENT=production when isProduction |
β |
infrastructure/lib/stacks/event-management-stack.ts |
SES from-domain from isProduction flag |
β |
infrastructure/lib/stacks/database-stack.ts |
Pass isProduction to RdsCluster construct |
β |
infrastructure/lib/stacks/storage-stack.ts |
isProduction flag for CDN/S3 config |
β |
infrastructure/lib/stacks/frontend-stack.ts |
isProduction flag |
β |
infrastructure/lib/stacks/cluster-stack.ts |
isProduction flag |
β |
infrastructure/lib/stacks/monitoring-stack.ts |
isProduction flag |
β |
infrastructure/lib/stacks/cicd-stack.ts |
isProduction flag |
β |
infrastructure/lib/constructs/domain-service-construct.ts |
isProduction flag |
β |
infrastructure/lib/constructs/ecs-service.ts |
isProduction flag |
β |
infrastructure/lib/constructs/rds-cluster.ts |
isProduction flag |
β |
infrastructure/lib/constructs/cognito-user-sync-triggers.ts |
isProduction flag |
β |
infrastructure/lib/constructs/ecs-service-alarms.ts |
isProduction flag |
β |
infrastructure/test/unit/inbound-email-stack.test.ts |
Updated assertion: replies@batbern.ch |
β |
Phase 4: SES & Email Reconfiguration β COMPLETE β
- β
batbern.chSES domain identity auto-validates via hosted zone - β
InboundEmailStack uses
replies@batbern.ch(viaisProductionflag) - β
Cognito CustomEmailSender uses
noreply@batbern.chandhttps://www.batbern.ch(viaisProductionflag)
Phase 5: Application Layer β COMPLETE β
- β
APP_ENVIRONMENT=productioninjected via CDK whenisProduction: true(api-gateway-service-stack.ts) - β ConfigController.java returns correct production URLs β no code change needed
Phase 6: CI/CD Pipeline β COMPLETE β
| File | Change | Status |
|---|---|---|
.github/workflows/deploy-staging.yml |
Updated test URLs: www.batbern.ch, api.batbern.ch |
β |
.github/workflows/deploy-production.yml |
Added DEPRECATED notice (targets decommissioned account) | β |
.github/workflows/deploy-code-staging.yml |
Updated smoke test URLs to production domains | β |
.github/workflows/build.yml |
Commented out production ECR push (old account 422940799530) | β |
.github/workflows/security-scan.yml |
Updated ZAP scan URLs to production domains | β |
Phase 7: Documentation β COMPLETE β
| File | Change | Status |
|---|---|---|
README.md |
Updated AWS references, deployment environments, deploy instructions | β |
CLAUDE.md |
Updated deployment section, AWS profiles, log group labels | β |
infrastructure/README.md |
Replaced 3-environment table with consolidated model | β |
infrastructure/CLAUDE.md |
Updated DNS architecture, account mapping, profiles, GitHub Actions roles, deployment modes | β |
docs/architecture/01-system-overview.md |
No changes needed (no account references) | β |
docs/architecture/02-infrastructure-deployment.md |
Updated environment strategy, AWS accounts, DNS diagram, CI/CD diagram, CORS, CDN, budget alerts, backup retention, service sizing | β |
docs/deployment/cicd-pipeline-guide.md |
Rewrote pipeline architecture, deployment process, secrets, image tagging | β |
docs/architecture/08-operations-security.md |
Removed staging SLA tier, updated CSP CDN domains | β |
docs/architecture/tech-stack.md |
Removed cdn.staging.batbern.ch reference |
β |
docs/testing/e2e-testing-guide.md |
All URLs updated to production domains | β |
docs/plans/environment-consolidation-plan.md |
This file β tracks full plan and progress | β |
Phase 8: Database URL Migration β COMPLETE β (2026-03-11)
Migrated 1,030 rows across 8 tables from old CDN URLs to cdn.batbern.ch:
| Table | Column | Rows Updated |
|---|---|---|
companies |
logo_url |
113 |
events |
theme_image_url |
6 |
event_photos |
display_url |
297 |
event_teaser_images |
image_url |
3 |
logos |
cloudfront_url |
135 |
speakers |
profile_picture_url |
1 |
user_profiles |
profile_picture_url |
183 |
session_materials |
cloudfront_url |
292 |
Two URL patterns replaced:
cdn.staging.batbern.chβcdn.batbern.ch(~738 rows)dhndjchovz1zp.cloudfront.netβcdn.batbern.ch(~292 rows)
Verified: brute-force scan of ALL text/varchar columns in ALL tables confirmed zero remaining old URLs.
Phase 9: CDK Deploy & Verification β COMPLETE β (2026-03-11)
Certificate Workaround (Cross-Region Export Writer)
CDKβs CrossRegionExportWriter blocked updating ACM certificate exports because downstream stacks (Storage, Frontend) still consumed the old values. CloudFormation processes the DNS stack first and the export writer unconditionally rejects value changes for existing export keys.
Fix applied:
- Pre-created ACM certificates manually in us-east-1:
www.batbern.ch:arn:aws:acm:us-east-1:188701360969:certificate/ef93631c-8241-4a2c-b84b-74615d5394cdcdn.batbern.ch:arn:aws:acm:us-east-1:188701360969:certificate/4aece095-d46f-4266-ac3a-c25736d59c08
- Added DNS validation CNAME records to Route53 hosted zone
- Added
cdnCertificateArntoDomainConfiginterface and both ARNs tostaging-config.ts - Modified
dns-stack.tsto useCertificate.fromCertificateArn()when pre-created ARNs are provided - This eliminated the CrossRegionExportWriter entirely β cert ARNs are now static config values
Stack Deployment Status
| Layer | Stack | Status | Notes |
|---|---|---|---|
| L0 | CICD | β deployed | |
| L0 | DNS | β deployed | Pre-created cert ARNs, no CrossRegionExportWriter |
| L0 | Monitoring | β deployed | |
| L0 | SES | β deployed | |
| L1 | Network | β deployed | New API cert for api.batbern.ch (config ARN preferred) |
| L1 | Secrets | β deployed | |
| L1 | EventBus | β deployed | |
| L2 | Database | β deployed | RDS deletion protection on, 14-day backup retention |
| L2 | Storage | β deployed | cdn.batbern.ch CNAME active on CloudFront |
| L2 | Bastion | β³ pending | Not included in deploy (optional) |
| L3 | Cognito | β deployed | noreply@batbern.ch, www.batbern.ch callbacks |
| L3 | Cluster | β deployed | |
| L4 | InboundEmail | β deployed | replies@batbern.ch, SES identity created |
| L4 | EventManagement | β deployed | Recreated fresh (Cloud Map conflict resolved) |
| L4 | SpeakerCoordination | β deployed | Recreated fresh |
| L4 | PartnerCoordination | β deployed | Recreated fresh |
| L4 | AttendeeExperience | β deployed | Recreated fresh |
| L4 | CompanyManagement | β deployed | Recreated fresh |
| L4 | ApiGatewayService | β deployed | APP_ENVIRONMENT=production |
| L5 | ApiGateway | β deployed | api.batbern.ch custom domain active |
| L5 | Frontend | β deployed | www.batbern.ch CNAME active |
Blockers & Fixes (All Resolved)
1. CloudFront CNAME conflicts (Storage, Frontend) β
The old production account (422940799530) had CloudFront distributions with cdn.batbern.ch, www.batbern.ch, and batbern.ch as alternate domain names. CloudFront CNAMEs are globally unique.
Fix: Destroyed all stacks in old production account. CNAMEs released.
2. API Gateway domain zombie (ApiGateway) β
The api.batbern.ch custom domain was stuck in a phantom state after rapid create/delete cycles.
Fix: Destroying old prod account released the domain. Staging ApiGateway deployed successfully with api.batbern.ch.
3. Network β ApiGateway cross-stack export (Network) β
The Network stack exports the API certificate ARN to the ApiGateway stack. Changing the cert changes the export value, which CloudFormation blocks.
Fix applied: Changed batbern-infrastructure.ts to prefer config.domain.apiCertificateArn over networkStack.apiCertificate.certificateArn. This is now the permanent design β static cert ARNs eliminate cross-stack export issues.
4. CDK global CLI version mismatch β
Global cdk was v2.1030.0, library needs β₯2.1107.0.
Fix: Using npx cdk for all deploys (uses local v2.1109.0).
5. Missing VPC log group (Network) β
Log group /aws/lambda/BATbern-staging/vpc-restrict-default-sg was externally deleted.
Fix: Manually recreated via aws logs create-log-group.
6. Cloud Map service discovery conflict (EventManagement) β ECS service replacement failed because the existing Cloud Map registration blocked the new service from registering with the same namespace. Fix: Destroyed all L4 service stacks and recreated them from scratch. These stacks are stateless (ECS services, task defs, ALBs) β no data loss.
7. CDK Bootstrap version too old β
InboundEmail deploy required bootstrap toolkit v30+; installed was v29.
Fix: Ran cdk bootstrap to update CDKToolkit stack.
8. Orphaned SES email identity (InboundEmail) β
batbern.ch SES domain identity existed outside CloudFormation, blocking InboundEmail stack from creating it.
Fix: Deleted orphaned identity via aws sesv2 delete-email-identity. CDK will recreate it as a managed resource.
Verification Checklist
-
https://www.batbern.ch -
https://batbern.ch -
https://api.batbern.ch/api/v1/configreturnsenvironment: "production" -
https://cdn.batbern.ch - Password reset email comes from
noreply@batbern.ch - Inbound email to
replies@batbern.ch
Apex domain migration: Old coming-soon CloudFront distribution (E2UQ8ZRVTQ4RLV) in prod account disabled and CNAME removed. New ACM cert (434c81ef-...) covers both www.batbern.ch and batbern.ch. Frontend CloudFront distribution now serves both domains.
Note: Frontend stack has a benign DELETE_FAILED on WebsiteBucketAutoDeleteObjectsCustomResource β old Lambda lacks s3:GetBucketTagging permission. Does not affect functionality; will self-resolve on next deploy.
Post-Deploy Fixes (2026-03-11)
9a. Service Connect inter-service communication failure β
All 6 ECS services were created simultaneously. Envoy sidecar proxies cached DNS failure state because upstream services werenβt registered when the proxy started.
Fix: force-new-deployment on all 6 ECS services. Fresh Envoy proxies resolved Service Connect DNS correctly.
9b. Frontend staging URL references β
Service workers cached old api.staging.batbern.ch URLs from the previous frontend bundle.
Fix: Removed all staging.batbern.ch references from 3 frontend source files:
web-frontend/src/config/runtime-config.tsβ removed staging hostname check fromgetApiUrl()web-frontend/src/config/amplify.tsβ changed staging Cognito redirects towww.batbern.chweb-frontend/src/hooks/useFileUpload/useFileUpload.tsβ removed staging CDN URL, always returnscdn.batbern.ch
9c. CORS for apex domain (https://batbern.ch) β
Requests from https://batbern.ch were blocked β only https://www.batbern.ch was in CORS allowed origins.
Fix: Added https://batbern.ch to both layers:
infrastructure/lib/stacks/api-gateway-stack.tsβ AWS HTTP API Gateway CORS configapi-gateway/src/main/java/ch/batbern/gateway/config/SecurityConfig.javaβ Spring Boot CORS config Deployed viacdk deploy BATbern-staging-ApiGateway BATbern-staging-ApiGatewayService.
9d. Staging subdomain HTTPS redirect β
S3 redirect buckets only serve HTTP. Users visiting https://staging.batbern.ch got a TLS error.
Fix: Created CloudFront distribution (EKKYTX97ETYNC, d37mzqchrwvyov.cloudfront.net) in front of the S3 redirect bucket, using existing ACM cert ff0df8d1-... (staging.batbern.ch). Updated DNS CNAME from S3 website endpoint to CloudFront. Both http:// and https://staging.batbern.ch now 301 redirect to https://www.batbern.ch.
Note: api.staging.batbern.ch and www.staging.batbern.ch still use direct S3 redirect (HTTP only). No ACM certs exist for these subdomains. Low priority β unlikely to receive user traffic.
Phase 10: Certificate & Resource Cleanup β PENDING β³
After all stacks deployed and CloudFront distributions updated:
- Delete old ACM certificates (no longer referenced by CloudFront):
β now in use by staging redirect CloudFront (EKKYTX97ETYNC)arn:aws:acm:us-east-1:188701360969:certificate/ff0df8d1-94c4-4cef-bbda-39af8c816dba(staging.batbern.ch)arn:aws:acm:us-east-1:188701360969:certificate/79870b01-d094-4d38-81f0-aceed0d3b041(cdn.staging.batbern.ch)
- Delete orphaned SSM parameters in eu-central-1 (if still present):
/cdk/exports/BATbern-staging-Storage/BATbernstagingDNSuseast1RefCdnCertificateE363B99BAEC53CEB/cdk/exports/BATbern-staging-Frontend/BATbernstagingDNSuseast1RefCertificate4E7ABB08423E170F
- Delete DNS validation CNAME records for old staging.batbern.ch certs (if any remain)
- Delete old SES identities:
staging.batbern.ch(no longer needed)
Phase 11: Decommission Old Production Account β COMPLETE β (2026-03-11)
Moved up from βafter 30 daysβ because old prod CloudFront CNAMEs blocked staging deploys.
- β
Destroyed all 21 stacks in production account (422940799530) via
cdk destroy --all- 19 stacks destroyed cleanly on first pass
- Database stack required disabling RDS deletion protection, then destroyed on retry
- Remaining stacks (Network, Secrets) destroyed after Database unblocked
- Skipped resources (retained by CDK policy): S3 buckets (4), ECR repos (7), Cognito UserPool, KMS key
- β³ Optionally close production AWS account
- β³ Delete retained S3 buckets and ECR repos manually (if closing account)
Development Workflow After Consolidation
- Local dev uses production Cognito/S3 β risk is low since local DB is read-only mirror
- Testing runs against production user pool β use
test-*@example.comconvention - Risky changes (major DB migrations, auth changes): spin up ephemeral staging in dev account via CDK, test, tear down
Resolved Questions
- Domain registrar:
batbern.chregistered via Gandi SAS (Route53 backend for .ch TLDs) in the management account (510187933511, profilebatbern-mgmt). NS records updated successfully viaaws route53domains update-domain-nameservers. - SES sandbox: Staging account already out of sandbox β no action needed.
- Maintenance window: Executed 2026-03-11.
- Development account: Decide later β focus on stagingβproduction first.
Total Files Modified
Infrastructure code: 23 files (+ api-gateway-stack.ts CORS update) Application code: 3 files (frontend staging URL cleanup + SecurityConfig.java CORS) CI/CD workflows: 5 files Documentation: 11 files watchOS app: 3 files (API URLs updated to batbern.ch) Database: 8 tables (1,030 rows migrated) AWS manual resources: 1 CloudFront distribution (staging redirect EKKYTX97ETYNC) Total: 45 files + database migration + manual CloudFront