Weather App

Weather App — AWS Serverless Architecture

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

Request flow — cache miss vs cache hit

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

Weather App — Multi-Region Active-Passive Architecture

Weather App — AWS Architecture

Compute & API

ServiceRole
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 v2Regional endpoints in both us-east-1 and us-west-2, CORS restricted to https://weather.craftingnewtech.com
Lambda Reserved ConcurrencyCapped at 10 concurrent executions per region — prevents runaway API costs

Frontend hosting

ServiceRole
Amazon S3Private static hosting — HTML, CSS, JS, and 18 custom SVG weather icons
Amazon CloudFrontGlobal CDN — HTTPS termination, security headers policy, directory index function
CloudFront FunctionRewrites /path/ to /path/index.html at the edge

DNS & TLS

ServiceRole
Route 53ALIAS records — weather.craftingnewtech.com and apex craftingnewtech.com pointing to CloudFront
Route 53 Failover Routingapi.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 ManagerTLS certificate provisioned in us-east-1 (required for CloudFront)

Data & secrets

ServiceRole
Amazon DynamoDBWeatherCache table — On-Demand billing, TTL attribute for automatic 15-min expiry, replicated cross-region as a Global Table for failover continuity
AWS Secrets ManagerOpenWeatherMap API key — custom 4-stage rotation Lambda on a 90-day schedule, dedicated KMS CMK, native cross-region replication to the standby region

Observability

ServiceRole
Amazon CloudWatchStructured Lambda logs, 5-widget dashboard, 4 alarms (errors, duration, API 4xx, API 5xx)
Amazon SNSAlarm topic — CloudWatch breaches publish to SNS, triggering email alerts

CI/CD pipeline

ServiceRole
AWS CodeCommitGit repository — main branch triggers pipeline on push
AWS CodePipelineTwo-stage pipeline: Source (CodeCommit) → Build (CodeBuild)
AWS CodeBuildRuns cfn-lint, checkov, pip-audit, pytest gates before deploying
EventBridgePush-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 gatescfn-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

CI/CD pipeline — build gates and deploy phases

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

Weather App — live frontend

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.