AWS Builder Center
ClimateGuard: Building a Serverless Heat and Flood Risk Alert System on AWS

ClimateGuard: Building a Serverless Heat and Flood Risk Alert System on AWS

How I built ClimateGuard, a serverless web app that turns live weather into heat and flood risk scores and clear safety advice, using Lambda, API Gateway, DynamoDB, S3, CloudFront and CloudWatch, with CI/CD through GitHub Actions and OIDC.

Weather apps tell you it is 43°C or that 145 mm of rain has fallen. They rarely tell you whether that is dangerous, or what to do about it. For a hackathon track on heat and water, I built ClimateGuard to close that gap: you enter a city and get a heat risk score, a flood risk score, an overall level and plain advice.

What it does

  • Search any city and get a heat risk and flood risk, each scored 0–100
  • An overall level: LOW, MEDIUM, HIGH or CRITICAL
  • Recommendations such as "avoid outdoor activity 12 PM–4 PM" or "avoid low-lying roads"
  • A saved risk history per city, so you can see how conditions changed

How the risk is scored

The heat score uses the "feels like" temperature, which already accounts for humidity: 28°C scores 0 and 50°C scores 100. The flood score adds the rain from the last 24 hours to half of the rain forecast for the next 24 hours, with 150 mm scoring 100. The overall level is the higher of the two. Weather data comes from the open Open-Meteo API.
These thresholds are my own reasoned starting values, not official warning criteria, so ClimateGuard is guidance and not a replacement for advisories from the weather department.

Architecture

The request path looks like this:
  1. The dashboard is hosted in a private S3 bucket and served through CloudFront over HTTPS.
  1. The browser calls an API Gateway HTTP API:  GET /risk?city=Nagpur .
  1. API Gateway triggers a Lambda function (Node.js) that fetches the weather, runs the risk engine and builds the response.
  1. Lambda saves each check in DynamoDB and returns the last 10 checks for that city.
  1. CloudWatch collects logs and metrics.
Everything is serverless, so the app costs almost nothing when nobody is using it and scales on its own when many people check at once, as they would during a heat wave.

Building it

Lambda and API Gateway. The risk engine is plain functions with no network calls, so the same file runs in the browser, in unit tests and in Lambda. The handler wraps it with the weather lookup. I rate-limited the API at 5 requests per second (burst 10) and locked the CORS origin to my CloudFront domain, since a public endpoint should not be an open invitation.
DynamoDB. The history table uses  place  as the partition key and  checkedAt  as the sort key, with on-demand capacity. A TTL attribute deletes rows after 30 days, so I never wrote a cleanup job. History is a bonus feature: if the database call fails, the handler still returns the risk result.
S3 and CloudFront. The bucket blocks all public access, and CloudFront reads it using Origin Access Control. The CloudFront Free plan kept the cost at $0.
CloudWatch. Because the Lambda writes structured JSON log lines, one Logs Insights query can summarise checks by city and risk level, and I put the metrics on a dashboard. One lesson: my handler returns errors as normal responses, so Lambda's own Errors metric stays at zero. API Gateway's 5xx metric is the better signal for server failures.

CI/CD with GitHub Actions and OIDC

Every push runs the unit tests, builds a Docker image (the tests run inside the build) and validates the Terraform. A push to  main  then packages and updates the Lambda, syncs the site to S3 and clears the CloudFront cache. A full deploy takes about 13 seconds.
I wanted no long-lived AWS keys in GitHub, so I used OIDC federation: GitHub requests a short-lived token and assumes an IAM role.
The problem that cost me the most time: the deploy failed with  Not authorized to perform sts:AssumeRoleWithWebIdentity , and the error does not say which condition failed. The documented subject format is  repo:OWNER/REPO:ref:refs/heads/main . The token GitHub actually sent contained numeric IDs:
Plain text
repo:OWNER@OWNER_ID/REPO@REPO_ID:ref:refs/heads/main
My trust policy never matched. I found it by adding a temporary workflow step that printed the token's  iss ,  aud  and  sub  claims, then updated the trust policy to match. If your OIDC role will not assume, print the claims first and compare.

Docker and Terraform

The Dockerfile has two stages. The first runs  npm test , so an image cannot be built if the risk engine is broken. The second serves the dashboard with nginx.
The whole stack is also described in Terraform (22 resources: DynamoDB, Lambda and IAM, API Gateway, S3 and CloudFront, a CloudWatch dashboard and alarm). It uses its own name prefix so it never touches the live stack, and  terraform validate  runs in CI. I built the live version by hand in the console first to learn each service, then wrote the Terraform to capture it.

Lessons learned

  • Memory matters. My first Lambda test took about 7 seconds at 128 MB, close to the limit, because the AWS SDK loads on a cold start. Raising memory to 256 MB and the timeout to 15 seconds fixed it.
  • Make optional features fail safely. The risk result must never depend on the history table.
  • Read the real error, not the documented one. The OIDC problem came down to the actual token contents.

What's next

Official IMD thresholds for each region, SMS or push alerts through Amazon SNS when a city reaches HIGH, and local-language advice for the people who need it most.
If you are building something similar, I hope the OIDC note saves you an afternoon.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article