Download CV

Kestrel Loan Platform on AWS

A loan application platform built end to end with Terraform: an API on ECS Fargate, a Lambda scorer behind SQS, and a pipeline that deploys to prod only after a human approves.

When
October 2026
Context
A production style build for a fictional microfinance bank
Role
Solo build
  • ECS Fargate
  • RDS PostgreSQL
  • AWS Lambda
  • Amazon SQS
  • AWS WAF
  • AWS KMS
  • Terraform
  • GitHub Actions
  • Application Load Balancer
  • VPC endpoints
  • CloudWatch
  • k6

The problem

Kestrel Microfinance Bank, a fictional lender, needed customers to apply for a loan through an API and get a decision in seconds, not overnight. It had to be secure enough for bank data, survive a payday rush, recover from a lost database and stay under a USD 1,500 monthly budget. I built, tested, load tested, recovered and then tore it all down in 5 days.

The architecture, live

The real diagram, drawn with the official AWS icons. Each numbered step lights up in order, with traffic moving along the path it really takes.

Step 1 of 7. A customer sends their application over HTTPS, and AWS WAF filters it.Full size
Run it yourself

Five experiments from the build

The real results, replayed. Each one is sped up so it fits in a few seconds.

Prod gets the same tested image, only after a human approves it. GitHub signs in to AWS with OIDC, so no access keys exist.

Pull request

  1. Lint
  2. Unit and database tests
  3. terraform fmt and validate
  4. Checkov security scan
  5. Plan posted on the PR

After merge

  1. ECR scan gate and cost tags
  2. Deploy to dev

Waiting for a human to approve prod

Prod

  1. ECR scan gate and cost tags
  2. Deploy the same image to prod

Loads from YouTube when you press play. Watch on YouTube.

k6 summary of the prod load test: 19,602 checks, 100% succeeded, 0% of requests failed, p95 request duration 58.08 ms
The k6 payday load test against prod: 19,602 requests, 0 failed, p95 58 ms.
The kestrel-prod CloudWatch dashboard during the load test: requests climbing to about 3,000 a minute, a 5xx error rate of 0, latency far below the 1 second alarm line, and empty scoring and dead letter queues
The prod dashboard at peak: about 3,000 requests a minute, no errors, queues empty.
Cost Explorer daily spend from 14 to 18 October 2026, rising to under 4 US dollars a day and falling to zero on 18 October after the teardown
Daily AWS spend for the whole build: USD 9.20 in total, USD 0.00 after the teardown.

The application

  • Customers reach a Python API on ECS Fargate through an Application Load Balancer protected by AWS WAF, with common attack rules and a rate limit of 2,000 requests per IP per 5 minutes.
  • Applications are stored in PostgreSQL on RDS, Multi-AZ in prod, and queued on SQS.
  • A Lambda scorer reads the queue, scores each application and publishes the decision. Decisions are idempotent, so a retried message never creates a second decision.

Network and data protection

  • Everything runs in private subnets with no internet route. AWS services are reached through VPC endpoints instead of a NAT gateway.
  • HTTPS for customers and TLS to the database. Data at rest is encrypted with customer managed KMS keys.
  • The customer's BVN (bank verification number) is encrypted at field level, so it is never readable in the database.

Infrastructure as code

  • 9 reusable Terraform modules: network, database, api, messaging, worker, kms, observability, security and governance.
  • Dev and prod use the same modules with different settings. Prod runs 2 tasks across 2 availability zones with a standby database and auto scales up to 20 tasks.

CI/CD and security

  • A GitHub Actions pipeline runs lint, unit and database tests, terraform fmt and validate, a Checkov security scan, and posts the Terraform plan on every pull request.
  • After merge it deploys to dev, waits for a human approval, then deploys the same tested image to prod.
  • GitHub signs in to AWS with OIDC, so there are no long lived access keys anywhere, and no IAM user has an access key.
  • Each deploy stops on a critical container vulnerability (an ECR scan gate) and checks that every resource carries cost tags.
  • CloudTrail in every region to a tamper resistant encrypted bucket, plus GuardDuty, Security Hub (AWS Foundational Security Best Practices) and AWS Config.

Observability

  • A CloudWatch dashboard per environment and 6 alarms that email on fire and on recovery: API 5xx error rate, p95 latency, no healthy tasks, a stuck scoring queue, the dead letter queue and low database storage.
  • A runbook entry for every alarm: how to confirm it, the immediate fix and the permanent fix.

Decisions

VPC endpoints instead of a NAT gateway
Nothing in the private subnets has a route to the internet. The tasks and the scorer reach AWS services only through VPC endpoints.
Scorer on Lambda with idempotent decisions
The scorer reads from SQS, which can deliver a message more than once. Making each decision idempotent means a retry never creates a second decision.
Field level protection for the BVN
Disk encryption is not enough for a bank verification number. It is encrypted at field level, so it is never readable in the database.
A pipeline with a prod approval
Every merge deploys to dev first. Prod gets the same tested image only after a human approves it.

What went wrong, and what fixed it

  • GitHub's OIDC token used permanent owner and repository IDs in its subject, so AWS rejected it. I found the real subject in CloudTrail and fixed the role trust policy.
  • terraform destroy failed on large versioned log buckets with a provider error (bufio token too long). I emptied the buckets with the AWS CLI in batches and re-ran the destroy.
  • The rollback check originally passed too easily, so I rewrote it to pass only when ECS itself logs the failed deployment and the rollback.
  • AWS Config was 76% of the whole bill (USD 6.98 of USD 9.20), because it bills for every recorded change while infrastructure is created and destroyed.

The result

  • Load test with k6 against prod at a payday peak of 50 requests a second, 25 times a normal day: 19,602 requests in 8 minutes, 0 failed, and p95 latency of 58 ms against a 1 second target. Auto scaling went from 2 to 5 tasks and back down, and no alarm fired.
  • Automatic rollback proven: a deliberately broken image was deployed, and the ECS deployment circuit breaker rolled back to the working version by itself while customers kept being served.
  • Restore drill on prod: the service was stable 21 minutes after the restore was requested, against a 2 hour target, and the newest restorable point was under 5 minutes old, against a 15 minute target.
  • Estimated running cost at the client's scale: about USD 273 a month, against a USD 1,500 limit.
  • Actual AWS spend for the whole build: USD 9.20, confirmed in Cost Explorer. Everything was torn down with terraform destroy on 17 October 2026, and spend on 18 October was USD 0.00.
Hire meCV