Deployment
Jenkins pipeline, ECS Fargate runtime, and CloudFront CDN
Deployment
Sentinel runs on AWS ECS Fargate with CloudFront CDN, Route 53 DNS, and an Application Load Balancer. The frontend is a Next.js standalone application packaged as a Docker container. Build, test, image push, ECS rollout, and CloudFront cache invalidation are all driven by the Jenkins pipeline defined in Jenkinsfile at the repository root.
Infrastructure Overview
graph TB
DNS["Route 53<br/>sentinel.centricitywealth.tech"] --> CF["CloudFront CDN<br/>SSL + Cache + WAF"]
CF --> ALB["ALB<br/>Health Checks"]
ALB --> ECS1["ECS Task 1<br/>Next.js Standalone"]
ALB --> ECS2["ECS Task N<br/>Next.js Standalone"]
ECS1 --> BFF["Studio Backend (BFF)"]
ECS2 --> BFF
classDef primary fill:#dbeafe,stroke:#3b82f6,color:#1e293b
classDef success fill:#d1fae5,stroke:#10b981,color:#1e293b
classDef dark fill:#1e293b,stroke:#334155,color:#f8fafc
class DNS,CF,ALB primary
class ECS1,ECS2 success
class BFF dark
Network Architecture
A detailed view of the VPC layout, showing how traffic flows from users through the CDN and load balancer into private ECS tasks:
graph TB
subgraph Internet
Users["Users / Browsers"]
DNS["Route 53<br/>sentinel.centricitywealth.tech"]
end
subgraph AWS["AWS ap-south-1"]
CF["CloudFront CDN<br/>SSL Termination + WAF"]
subgraph VPC["VPC 10.0.0.0/16"]
subgraph Public["Public Subnets"]
ALB["Application<br/>Load Balancer"]
end
subgraph Private["Private Subnets"]
ECS1["ECS Task 1<br/>Next.js :3000"]
ECS2["ECS Task 2<br/>Next.js :3000"]
end
end
ECR["ECR<br/>Container Registry"]
CW["CloudWatch<br/>Logs + Metrics"]
end
subgraph Backend["Backend Services"]
BFF["Studio BFF<br/>studio-backend-dev"]
end
Users --> DNS
DNS --> CF
CF --> ALB
ALB --> ECS1
ALB --> ECS2
ECS1 --> BFF
ECS2 --> BFF
ECS1 -.-> CW
ECS2 -.-> CW
ECR -.->|Pull image| ECS1
ECR -.->|Pull image| ECS2
classDef primary fill:#dbeafe,stroke:#3b82f6,color:#1e293b
classDef secondary fill:#e0e7ff,stroke:#6366f1,color:#1e293b
classDef neutral fill:#f1f5f9,stroke:#64748b,color:#1e293b
classDef dark fill:#1e293b,stroke:#334155,color:#f8fafc
class Users,DNS,CF,ALB primary
class ECS1,ECS2 secondary
class ECR,CW neutral
class BFF dark
Service Map
| Component | Resource | Details |
|---|---|---|
| DNS | Route 53 | sentinel.centricitywealth.tech (prod), sentinel-dev.centricitywealth.tech (dev) |
| CDN | CloudFront | Distribution EQEH6ZAHOVUNL, SSL termination, /* cache invalidation |
| Load Balancer | ALB | Health check on port 3000, target group with ECS tasks |
| Compute | ECS Fargate | 256 CPU / 512 MB memory, auto-scaling up to 2 (dev) or 10 (prod) |
| Container Registry | ECR | <account-id>.dkr.ecr.ap-south-1.amazonaws.com/cwt-dev/sentinel |
| Backend | Studio BFF | studio-backend-dev.centricitywealth.tech – all API calls route here |
Jenkins Pipeline
Jenkinsfile at the repo root is the source of truth for the release flow. The same image-build and deploy commands are also exposed locally as cwt-* Make targets so engineers can reproduce a stage on a workstation when debugging.
Branch Policy
| Branch | Target environment | Promotion |
|---|---|---|
dev |
cwt-dev |
Automatic on merge |
main |
cwt-prod |
Manual approval gate |
Pipeline Stages
graph LR
Checkout["Checkout"] --> Install["npm ci"]
Install --> Lint["Lint + Type Check"]
Lint --> Test["Playwright"]
Test --> Audit["npm audit<br/>+ outdated"]
Audit --> Build["Docker Build<br/>Multi-stage"]
Build --> Push["ECR Push"]
Push --> Deploy["ECS Force Deploy<br/>Rolling update"]
Deploy --> Invalidate["CloudFront<br/>Cache Invalidation"]
Invalidate --> Verify["services-stable wait"]
classDef primary fill:#dbeafe,stroke:#3b82f6,color:#1e293b
classDef success fill:#d1fae5,stroke:#10b981,color:#1e293b
classDef warning fill:#fef3c7,stroke:#f59e0b,color:#1e293b
class Checkout,Install,Lint,Test,Audit primary
class Build,Push,Deploy,Verify success
class Invalidate warning
The pipeline performs each stage in order; any failure short-circuits the run. ECR/ECS/CloudFront credentials are referenced by Jenkins credential IDs configured on the controller — no secrets are committed to the repository.
Local Make Targets
The Makefile mirrors the Jenkins stages so developers can run any step locally with the same command shape.
| Target | Purpose |
|---|---|
dev |
Start the Next.js dev server |
build |
Produce the Next.js standalone build |
test |
Run the Playwright suite (Chromium project) |
lint |
Run ESLint |
type-check |
Run tsc --noEmit strict type check |
audit |
Run dependency audits (npm audit, npm outdated) |
cwt-ecr-login |
Authenticate Docker against the shared CWT ECR registry |
cwt-docker-build |
Build the production image from the root Dockerfile |
cwt-docker-push |
Push tagged + latest image to ECR |
cwt-deploy |
Force a new ECS deployment and wait for services-stable |
Docker Build
The Dockerfile sits at the repository root and uses a multi-stage build optimised for Next.js standalone output. The Jenkins pipeline runs the equivalent of:
docker build --platform linux/amd64 \
--build-arg NEXT_PUBLIC_STUDIO_API_URL=https://studio-backend-dev.centricitywealth.tech \
--build-arg NEXT_PUBLIC_ENABLE_MOCKS=false \
-f Dockerfile -t cwt-dev/sentinel:latest .
Key details:
NEXT_PUBLIC_*variables are baked at build time into the Docker image (Next.js inlines them during compilation)- Server-only variables (
STUDIO_API_URL,SESSION_SECRET) are provided at runtime via the ECS task definition - The final image uses Next.js standalone output for a minimal container size
Environment Variables
Environment Variables Flow
Build-time variables are baked into the JavaScript bundle and cannot change after deployment. Runtime variables are read by the Node.js server at startup.
graph TD
subgraph BuildTime["Build Time (docker build)"]
NB["next build"]
NB --> BP1["NEXT_PUBLIC_STUDIO_API_URL<br/>Inlined into JS bundle"]
NB --> BP2["NEXT_PUBLIC_ENABLE_MOCKS<br/>Inlined into JS bundle"]
end
subgraph RunTime["Runtime (ECS Task Start)"]
TS["Task Definition<br/>Environment"]
TS --> RP1["STUDIO_API_URL<br/>Server-side only"]
TS --> RP2["SESSION_SECRET<br/>From Secrets Manager"]
TS --> RP3["NODE_ENV / PORT<br/>Process config"]
end
BP1 -->|"Shipped to browser"| Browser["Client Browser"]
RP1 -->|"Never exposed"| Server["Node.js Server"]
RP2 -->|"Never exposed"| Server
classDef warning fill:#fef3c7,stroke:#f59e0b,color:#1e293b
classDef success fill:#d1fae5,stroke:#10b981,color:#1e293b
classDef neutral fill:#f1f5f9,stroke:#64748b,color:#1e293b
classDef danger fill:#fee2e2,stroke:#ef4444,color:#1e293b
classDef dark fill:#1e293b,stroke:#334155,color:#f8fafc
class NB,BP1,BP2 warning
class TS,RP1,RP2,RP3 success
class Browser danger
class Server dark
Build-Time Variables (NEXT_PUBLIC_*)
These are embedded into the JavaScript bundle during next build and cannot be changed at runtime.
| Variable | Description | Dev Value |
|---|---|---|
NEXT_PUBLIC_STUDIO_API_URL |
Studio Backend URL for all client-side API calls | https://studio-backend-dev.centricitywealth.tech |
NEXT_PUBLIC_ENABLE_MOCKS |
Enable mock data mode | false |
Runtime Variables (ECS Task Definition)
These are available to the Node.js server at runtime and are never exposed to the browser.
| Variable | Description | Dev Value |
|---|---|---|
STUDIO_API_URL |
Studio Backend URL for server-side auth | https://studio-backend-dev.centricitywealth.tech |
SESSION_SECRET |
Cookie encryption key (32+ characters) | Stored in AWS Secrets Manager |
NODE_ENV |
Node environment | production |
PORT |
Container port | 3000 |
Production Gating
app/src/instrumentation.ts runs once on Next.js boot. When NODE_ENV=production or APP_ENV=prod, it fails fast if any of STUDIO_API_URL, NEXT_PUBLIC_STUDIO_API_URL, or SESSION_SECRET are missing or pointing at localhost. The container exits before serving traffic if the configuration is unsafe.
Health Checks
Container Health
The ALB performs HTTP GET requests to port 3000 on each ECS task. A healthy response (HTTP 200) keeps the task in the target group.
# Check target health
aws elbv2 describe-target-health \
--target-group-arn <TARGET_GROUP_ARN>
Service Health
# Check ECS service status
aws ecs describe-services \
--cluster cwt-dev-cluster \
--services sentinel
# List running tasks
aws ecs list-tasks \
--cluster cwt-dev-cluster \
--service-name sentinel
Application Health Endpoint
# Studio Backend health
curl https://studio-backend-dev.centricitywealth.tech/api/v1/health/
Local Reproduction Steps
To reproduce the pipeline against cwt-dev from a workstation (debugging only — production deploys are owned by Jenkins):
# 1. Install + checks
cd app && npm ci
npm run lint
npx tsc --noEmit
npx playwright test --project=chromium
# 2. Dependency audit
npm audit --audit-level=high
npm outdated
# 3. Build + push (uses Make targets)
cd ..
make cwt-ecr-login
make cwt-docker-build
make cwt-docker-push
# 4. Force ECS rollout
make cwt-deploy
Rollback
To roll back to a previous version, pin the ECS task definition to the previous image tag and re-run make cwt-deploy (or trigger the equivalent Jenkins job parameters):
# 1. List recent ECR images
aws ecr describe-images \
--repository-name cwt-dev/sentinel \
--query 'imageDetails | sort_by(@, &imagePushedAt) | [-5:]'
# 2. Update the ECS task definition to use a previous image tag,
# then force a new deployment
aws ecs update-service \
--cluster cwt-dev-cluster \
--service sentinel \
--force-new-deployment
Monitoring
CloudWatch Logs
# Stream logs in real time
aws logs tail /ecs/sentinel-frontend-dev --follow
# Search for errors
aws logs filter-log-events \
--log-group-name /ecs/sentinel-frontend-dev \
--filter-pattern "ERROR"
AWS Console Links
Security
| Practice | Implementation |
|---|---|
| Network isolation | ECS tasks run in private subnets |
| HTTPS only | CloudFront redirects HTTP to HTTPS |
| Server-only secrets | STUDIO_API_URL and SESSION_SECRET are never exposed to the browser |
| Cookie-based auth | Auth tokens in encrypted HttpOnly cookies, not localStorage |
| IAM least privilege | Task roles have minimal required permissions |
| Secrets management | SESSION_SECRET stored in AWS Secrets Manager; pulled by Jenkins via credential IDs |
| Security headers | X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy |
| Production gating | instrumentation.ts rejects missing or localhost values for STUDIO_API_URL, NEXT_PUBLIC_STUDIO_API_URL, and SESSION_SECRET |
Domains
| Stage | Domain | URL |
|---|---|---|
| Dev | sentinel-dev.centricitywealth.tech |
https://sentinel-dev.centricitywealth.tech |
| Prod | sentinel.centricitywealth.tech |
https://sentinel.centricitywealth.tech |