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.
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
- Lint
- Unit and database tests
- terraform fmt and validate
- Checkov security scan
- Plan posted on the PR
After merge
- ECR scan gate and cost tags
- Deploy to dev
Waiting for a human to approve prod
Prod
- ECR scan gate and cost tags
- Deploy the same image to prod
A payday peak of 50 requests a second, 25 times a normal day. Auto scaling went from 2 to 5 tasks and back down.
8 minutes of k6 against prod, no alarm fired.
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: stable 21 minutes after the restore was requested, and the newest restorable point was under 5 minutes old.
Torn down with terraform destroy on 17 October 2026. AWS Config was 76% of the bill, because it bills for every recorded change while infrastructure is created and destroyed.
Terraform modules
- network
- database
- api
- messaging
- worker
- kms
- observability
- security
- governance
The whole bill: USD 9.20
AWS Config, USD 6.98 Everything else, USD 2.22
Spend on 18 October
USD 0.00
Loads from YouTube when you press play. Watch on YouTube.



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.