EC2, NGINX, CloudFront, WAF, S3, SES, auto-scaling, and blue-green deployments — every service explained with config context.
Q: Why not use Lambda or ECS instead of EC2?
A: WebSocket connections require persistent, long-lived TCP connections — Lambda's 15-min max invocation time and ECS Fargate's cold start overhead make them unsuitable. EC2 gives us full control over the process lifecycle, connection pool warmup, and PM2-based cluster mode. For stateless APIs, Lambda would be fine, but the real-time WebSocket layer needs EC2.
# /etc/nginx/sites-available/app
upstream nodejs {
server 127.0.0.1:3000;
keepalive 64; # keep upstream connections alive — avoids TCP handshake per req
}
server {
listen 443 ssl http2;
server_name api.zoober.in;
# SSL termination here — Node.js handles plain HTTP internally
ssl_certificate /etc/letsencrypt/live/api.zoober.in/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.zoober.in/privkey.pem;
# Gzip compression for JSON responses
gzip on;
gzip_types application/json text/plain;
gzip_min_length 1000;
# Rate limiting (token bucket) — 100 req/s per IP, burst 200
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;
location /api/ {
limit_req zone=api burst=200 nodelay;
proxy_pass http://nodejs;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # WebSocket upgrade
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400; # WebSocket long-lived connections
}
# Static files served directly by NGINX — bypasses Node.js entirely
location /public/ {
root /var/www/app;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Q: What does keepalive 64 do in the upstream block?
A: Without keepalive, NGINX opens a new TCP connection to Node.js for every proxied request (connect → request → close). With keepalive 64, NGINX maintains a pool of 64 persistent connections to the upstream. This eliminates TCP handshake overhead (~1-3ms) on every request, directly improving P95 latency.
Cache-Control: max-age=300. Reduces backend load during traffic spikes./api/* → TTL=0 (no cache), forward all headers, forward cookies/public/* → TTL=31536000s (1 year), compress=true/catalog/* → TTL=300s (5 min), Vary: Accept-EncodingWhen a store updates their catalog, the backend calls CloudFront.createInvalidation({ paths: ['/catalog/store-id/*'] }). Wildcard invalidations cost $0.005 each, so they're triggered only on catalog updates, not on every order.
// Generate a pre-signed URL for a private S3 object via CloudFront
const { getSignedUrl } = require('@aws-sdk/cloudfront-signer');
const url = getSignedUrl({
url: `https://cdn.zoober.in/uploads/${filename}`,
keyPairId: process.env.CF_KEY_PAIR_ID,
privateKey: process.env.CF_PRIVATE_KEY,
dateLessThan: new Date(Date.now() + 15 * 60 * 1000), // 15min expiry
});
Q: Why have both WAF rate limiting AND NGINX rate limiting?
A: Defense in depth. WAF (CloudFront layer) blocks abuse before it reaches our EC2 instances — this protects against DDoS at the CDN edge. NGINX rate limiting is the second layer at the instance level, for cases where traffic bypasses CloudFront (e.g., direct IP access). Two layers means an attacker can't take down the origin even if they find the EC2 IP.
zoober-public-assets: product images, store logos — public read via CloudFront OAC (Origin Access Control), no direct public URLzoober-private-uploads: rider documents, payment receipts — private, accessed only via pre-signed URLszoober-logs: ALB access logs, CloudFront logs — lifecycle policy: move to S3 Glacier after 30 days// Client requests upload URL from backend
// Backend generates presigned URL (never exposes S3 credentials to client)
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const url = await getSignedUrl(s3Client, new PutObjectCommand({
Bucket: 'zoober-private-uploads',
Key: `riders/${riderId}/${filename}`,
ContentType: 'image/jpeg',
}), { expiresIn: 300 }); // 5 min
// Client uploads directly to S3 using presigned URL — zero load on our server
// Backend then updates DB with S3 key after client confirms upload
v=spf1 include:amazonses.com ~allv=DMARC1; p=quarantine; rua=mailto:dmarc@zoober.inQ: What triggers an automatic rollback?
A: ALB monitors the Green target group's health check endpoint (/health returning 200). If more than 20% of health checks fail within 2 minutes of the traffic shift, the deployment pipeline triggers a rollback by shifting the ALB listener rule back to Blue. The whole rollback takes <30 seconds — faster than any manual intervention.
Q: What's in the smoke test?
A: Basic end-to-end checks: (1) GET /health returns 200, (2) MongoDB connection is alive, (3) Redis is reachable, (4) A known store's catalog returns data, (5) Auth endpoint issues a valid JWT. These run against the Green environment before any real traffic hits it.