Weather App
A production-grade serverless weather application that fetches real-time conditions, a 7-day forecast, and the EPA Air Quality Index for any city in the world. The OpenWeatherMap API key never touches the browser — Lambda retrieves it from AWS Secrets Manager at runtime, with automated rotation on a 90-day cadence. Results are cached in DynamoDB for 15 minutes to reduce API calls and cost, replicated cross-region via DynamoDB Global Tables. The entire stack is defined in CloudFormation, deployed through CodePipeline, served globally via CloudFront, and fails over automatically to a warm standby in us-west-2 if the primary region goes down.
Stack: AWS Lambda (Python 3.11) · API Gateway HTTP API · DynamoDB (Global Tables) · Secrets Manager · Route 53 (failover routing) · CloudFront · S3 · ACM · CodePipeline · CodeBuild · CodeCommit · CloudFormation · OpenWeatherMap Air Pollution API | Type: Serverless web app — 100% AWS-native, zero third-party CI/CD, multi-region active-passive
How it works
Cache-first request flow — When a user searches for a city, the browser calls API Gateway, which proxies to Lambda. Lambda checks DynamoDB first. On a cache hit (within 15 minutes), it returns the stored result immediately — no outbound API call, sub-30 ms response. On a miss, it fetches the API key from Secrets Manager, calls OpenWeatherMap and the Air Pollution API concurrently, writes the result to DynamoDB with a TTL, and returns the response (~150–250 ms).
Secure secret handling — The OpenWeatherMap API key is stored in AWS Secrets Manager, encrypted with a dedicated KMS CMK. Lambda fetches it on cold start and caches it in the execution context. The key never appears in environment variables, source code, or any browser-side resource.
Static frontend — HTML, CSS, and JavaScript are deployed to a private S3 bucket. CloudFront serves them globally using Origin Access Control (OAC) — the bucket has no public access policy and is unreachable directly. A CloudFront Function rewrites directory index paths so /about/ resolves to about/index.html.
Architecture

Compute & API
| Service | Role |
|---|---|
| AWS Lambda (us-east-1, primary) | Python 3.11 handler — input validation, cache check, concurrent OpenWeatherMap + Air Pollution API calls, EPA AQI calculation, response mapping |
| AWS Lambda (us-west-2, standby) | Warm passive-region replica of the primary handler — no Provisioned Concurrency; a cold start is negligible against the failover RTO |
| API Gateway HTTP API v2 | Regional endpoints in both us-east-1 and us-west-2, CORS restricted to https://weather.craftingnewtech.com |
| Lambda Reserved Concurrency | Capped at 10 concurrent executions per region — prevents runaway API costs |
Frontend hosting
| Service | Role |
|---|---|
| Amazon S3 | Private static hosting — HTML, CSS, JS, and 18 custom SVG weather icons |
| Amazon CloudFront | Global CDN — HTTPS termination, security headers policy, directory index function |
| CloudFront Function | Rewrites /path/ to /path/index.html at the edge |
DNS & TLS
| Service | Role |
|---|---|
| Route 53 | ALIAS records — weather.craftingnewtech.com and apex craftingnewtech.com pointing to CloudFront |
| Route 53 Failover Routing | api.weather.craftingnewtech.com health-checked every 30s against a dedicated GET /health endpoint; fails over to us-west-2 automatically, no manual intervention |
| AWS Certificate Manager | TLS certificate provisioned in us-east-1 (required for CloudFront) |
Data & secrets
| Service | Role |
|---|---|
| Amazon DynamoDB | WeatherCache table — On-Demand billing, TTL attribute for automatic 15-min expiry, replicated cross-region as a Global Table for failover continuity |
| AWS Secrets Manager | OpenWeatherMap API key — custom 4-stage rotation Lambda on a 90-day schedule, dedicated KMS CMK, native cross-region replication to the standby region |
Observability
| Service | Role |
|---|---|
| Amazon CloudWatch | Structured Lambda logs, 5-widget dashboard, 4 alarms (errors, duration, API 4xx, API 5xx) |
| Amazon SNS | Alarm topic — CloudWatch breaches publish to SNS, triggering email alerts |
CI/CD pipeline
| Service | Role |
|---|---|
| AWS CodeCommit | Git repository — main branch triggers pipeline on push |
| AWS CodePipeline | Two-stage pipeline: Source (CodeCommit) → Build (CodeBuild) |
| AWS CodeBuild | Runs cfn-lint, checkov, pip-audit, pytest gates before deploying |
| EventBridge | Push-triggered pipeline — no polling, instant invocation on git push aws main |
Recent improvements
Multi-region active-passive failover
The app now survives a full regional AWS outage — motivated by three real us-east-1 outages (Nov 2021, Dec 2021, Jun 2023) that would have taken a single-region deployment fully offline.
Route 53 failover routing on api.weather.craftingnewtech.com health-checks the primary region every 30 seconds against a dedicated GET /health endpoint. On failure, DNS fails over automatically to a fully warm standby Lambda + API Gateway deployment in us-west-2 — no manual intervention, no data loss (RTO ~60–90s, RPO under 1s). DynamoDB Global Tables replicate the weather cache cross-region, and Secrets Manager’s native cross-region replication keeps the standby’s OpenWeatherMap key in sync.
The design is deliberately active-passive, not active-active — at portfolio-scale traffic, active-active’s added cost and complexity (dual-write conflict resolution, 2x steady-state compute, traffic-splitting logic) isn’t justified. The standby region runs with zero Provisioned Concurrency, since a Lambda cold start is a rounding error against the recovery time objective. Failover and failback were verified with a real, live drill, not just a smoke test.
EPA Air Quality Index widget
A collapsible dial-gauge widget shows the real US EPA Air Quality Index (0–500 scale, 6 categories from Good to Hazardous) for the searched city — not OpenWeatherMap’s simpler native 1–5 scale.
The backend implements the official EPA AQI linear-interpolation formula against the EPA breakpoint tables (40 CFR Part 58 Appendix G) for all six criteria pollutants — PM2.5, PM10, O3, CO, SO2, and NO2 — including unit conversion (µg/m³ → ppm/ppb) and dominant-pollutant selection. The Air Pollution API call runs concurrently with the existing forecast call via a Python ThreadPoolExecutor, so the widget adds no extra page-load latency.
The gauge is a hand-built SVG matching the AirNow.gov reference design — no charting library — constructed entirely via document.createElementNS/setAttribute, per the app’s existing “never innerHTML” convention. It’s non-fatal by design: if the air-quality fetch fails, the rest of the page is unaffected and the widget simply doesn’t render. Marginal cost is $0 — bundled into the existing free-tier OpenWeatherMap key.
Secrets Manager migration
The OpenWeatherMap API key moved from SSM Parameter Store to AWS Secrets Manager with a custom automated rotation schedule. This is more than a service swap: the original SSM-based design relied on manual key rotation that, in practice, never actually happened — there was no reminder mechanism. Secrets Manager now runs a custom 4-stage rotation Lambda on a fixed 90-day cadence and notifies an operator via SNS. Because OpenWeatherMap has no self-service key-generation API, full end-to-end automation isn’t possible — but a forcing function now exists where none did before.
The secret is encrypted with a dedicated KMS CMK rather than an AWS-managed default key, consistent with the project’s practice elsewhere. This migration also enabled the multi-region work above — Secrets Manager’s native cross-region replication keeps the standby region’s key in sync, something the old SSM approach couldn’t do without custom pipeline scripting.
Security design
No secrets in the browser — The OpenWeatherMap key lives exclusively in AWS Secrets Manager, encrypted with a dedicated KMS CMK. The frontend JavaScript only knows the API Gateway URL. There is no way for a user to extract the key from the browser.
Automated rotation with a human forcing function — A custom 4-stage rotation Lambda runs on a 90-day schedule. OpenWeatherMap has no self-service key-generation API, so full end-to-end automation isn’t possible — instead, the rotation holds open and notifies an operator via SNS rather than expiring silently. RotationEnabled/LastRotatedDate give a queryable, auditable record that rotation is actually happening.
Least-privilege IAM — Five separate IAM roles cover Lambda, CodeBuild, CodePipeline, CloudFormation, and EventBridge. Each role has only the permissions required for its function. No shared credentials, no wildcard resource ARNs.
S3 + OAC — The frontend bucket blocks all public access. CloudFront uses Origin Access Control (OAC) to sign requests to S3 with SigV4. Direct S3 object URLs return 403. The bucket policy permits only the CloudFront distribution’s service principal.
CORS enforcement — API Gateway allows only https://weather.craftingnewtech.com. Cross-origin calls from other domains or localhost are rejected at the API layer.
Input validation — The city name parameter is validated against a whitelist regex (^[a-zA-Z0-9\s,\-\.]{1,100}$) before any downstream call is made. SQL injection and path traversal patterns are rejected immediately.
Content Security Policy — All pages serve a strict CSP (default-src 'self', no 'unsafe-inline'). All styles are in an external stylesheet — no inline <style> blocks or style= attributes.
Pipeline security gates — cfn-lint and checkov validate CloudFormation templates. pip-audit scans Lambda dependencies for CVEs. pytest enforces ≥ 80% test coverage. Any gate failure aborts the build with on-failure: ABORT.
CI/CD pipeline
git push aws main
└─▶ CodeCommit ──▶ EventBridge ──▶ CodePipeline
│
┌──────▼──────┐
│ CodeBuild │
│ pre_build │
│ cfn-lint │
│ checkov │
│ pip-audit │
│ pytest │
└──────┬───────┘
│ (all gates pass)
┌────────────▼────────────────┐
│ build phase │
│ cfn package (Lambda zip) │
│ cfn deploy (nested stacks) │
│ s3 sync (frontend) │
│ cloudfront invalidate /* │
└─────────────────────────────┘
The CloudFront invalidation uses --invalidation-batch with CallerReference=${CODEBUILD_BUILD_ID}, making it idempotent — re-running the same build never creates duplicate invalidations.
Key design decisions
CloudFormation over Terraform — The entire project uses AWS-native IaC only. No third-party state backend, no Terraform Cloud, no external lock table. CloudFormation nested stacks organized by concern (IAM, storage, CDN, database, backend, API, pipeline, monitoring) keep each template focused and independently deployable.
DynamoDB On-Demand — No minimum capacity cost. At portfolio scale, the cache table costs effectively nothing. On-demand scales automatically if traffic spikes without capacity planning.
Lambda reserved concurrency — Capped at 10 concurrent executions. This prevents a sudden traffic spike or misconfigured client from sending thousands of calls to OpenWeatherMap and blowing through the API quota.
Vanilla JS frontend — No React, no build step for the frontend. The HTML, CSS, and JS are deployed directly to S3 as static files. This eliminates a node_modules dependency tree, reduces the attack surface for supply-chain CVEs, and makes the frontend trivially cacheable.
18 custom SVG weather icons — OpenWeatherMap’s raster PNG icons depend on an external CDN call, which the strict CSP blocks. Inline SVG icons built from OpenWeatherMap’s condition codes serve from the same origin with no external dependencies.
DynamoDB TTL for cache expiry — No cron job, no Lambda sweeper. DynamoDB TTL automatically deletes expired records. The 15-minute window balances freshness against API call volume — weather data does not change meaningfully in 15 minutes for most use cases.
Infrastructure layout
infrastructure/cloudformation/
├── 01-iam.yml # least-privilege roles
├── 02-storage.yml # WebsiteBucket + ArtifactsBucket
├── 03-cdn.yml # CloudFront + OAC + CloudFront Function
├── 04-database.yml # DynamoDB WeatherCache table (Global Table replica in us-west-2)
├── 05-backend.yml # Lambda function + log group (deployed per region)
├── 06-api.yml # API Gateway HTTP API + CORS (deployed per region)
├── 07-secrets.yml # Secrets Manager secret + rotation Lambda + KMS CMK
├── 08-pipeline.yml # CodeCommit + CodeBuild + CodePipeline + EventBridge
├── 09-monitoring.yml # CloudWatch alarms + dashboard + SNS topic
├── 10-failover-dns.yml # Route 53 health check + failover routing policy
└── master.yml # Root nested stack — deploys to us-east-1 (primary) and us-west-2 (standby)
Live application

The app is deployed at weather.craftingnewtech.com.
Try searching for any city — London, Tokyo, São Paulo, or New York. Use the °C / °F toggle to switch units. The About page documents how the app works and the privacy model.