After five and a half years building production platforms, I've learned that a good architecture design can save your team months of firefighting. I've seen systems collapse under load, security incidents that could have been prevented, and bills that spiraled out of control—all because the architecture review was superficial or skipped entirely. Today I want to share my AWS architecture review checklist: the questions I ask before approving a design, whether it's from a junior engineer or a principal architect.
Security — Least privilege, private subnets, encryption and audit logs
Security isn't negotiable in production. When I review an architecture, I'm looking for defense in depth. The first question I always ask: Can this service reach the internet without needing to?
Most of the time, the answer should be no. Your RDS database should live in a private subnet. Your Lambda functions processing internal events shouldn't have internet access. Your application servers should sit behind ALBs, not directly exposed. I've seen too many architectures where developers made everything publicly accessible "for convenience" and later realized they'd opened a door to attackers.
Here's what I look for in security:
- IAM roles are scoped tightly. Not
AdministratorAccessor evenPowerUserAccess. If a Lambda needs to write to one S3 bucket and read from DynamoDB, it gets exactly those permissions. No more. - Encryption at rest and in transit. RDS encryption enabled, S3 bucket encryption enabled, API endpoints use TLS. This is table stakes.
- Private subnets for databases and internal services. Public subnets only for load balancers and bastion hosts (if you still use them). NAT gateways for outbound traffic from private subnets when needed.
- Audit logging is centralized. CloudTrail logs everything, VPC Flow Logs capture network traffic, ALB access logs go to S3, application logs stream to CloudWatch Logs. If something breaks at 2 AM, you need the data to investigate.
Here's an example of a secure IAM policy for a Lambda that processes S3 events and writes results to DynamoDB:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "arn:aws:s3:::my-input-bucket/*"
},
{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/ProcessedEvents"
}
]
}
Notice what's not there: no S3 bucket wildcards, no DynamoDB wildcard actions like dynamodb:*, no other tables or buckets. This is least privilege in practice.
Reliability — Multi-AZ, health checks, backups and tested restores
A system that works 99% of the time is still down for 3.5 days per year. When I review reliability, I'm asking: What happens when one availability zone goes down? Because it will, eventually.
I've deployed applications across multiple AZs that still failed when one went down because the team never actually tested it. They assumed the load balancer would fail over correctly, but they didn't verify the database replication was working, or they didn't realize their NAT gateway was only in one AZ.
My reliability checklist:
- Multi-AZ deployment for stateful services. RDS with Multi-AZ enabled (automatic failover). ElastiCache in cluster mode. Load balancers span multiple AZs.
- Health checks are intelligent. ALB health checks shouldn't just ping the root path; they should hit an endpoint that verifies database connectivity. CloudWatch alarms trigger on elevated unhealthy host counts.
- Backups are automated and tested. RDS automated backups enabled with a retention period that matches your RPO (recovery point objective). More importantly: you've actually restored from a backup in a non-production environment to verify it works.
- Failover is tested regularly. Kill a database, promote the read replica, see what breaks. Simulate an AZ failure in a staging environment. Document the runbook.
A quick example: I once reviewed an architecture where the team set RDS backups to 7 days but never tested a restore. When we actually tried restoring, we discovered the backup was corrupted. The automated snapshots looked fine in the console, but the restore failed silently during development. Testing saved them weeks of potential downtime.
Operations — IaC, CI/CD with OIDC, dashboards and alarms
If you're clicking buttons in the AWS console to deploy infrastructure, you've already lost. Operations maturity separates teams that move fast from teams that move fast and safely.
Here's what I verify:
- Everything is Infrastructure as Code. CloudFormation, Terraform, CDK—I don't care which tool, but the infrastructure must be version controlled. No manual changes. No console drift.
- CI/CD pipelines use OIDC, not IAM access keys. OIDC (OpenID Connect) eliminates the need to store AWS credentials in GitHub Secrets. GitHub Actions, GitLab CI, or Jenkins can assume a role directly. Credentials never touch your CI system.
- Dashboards exist for the metrics that matter. Latency, error rate, CPU, memory, connection pools, queue depths. Not just "is it up," but "is it healthy?"
- Alarms are actionable and routed correctly. An alarm that fires at 3 AM should go to an on-call engineer, not Slack's general channel. Use SNS to fanout to PagerDuty or OpsGenie.
Here's a minimal Terraform example that sets up OIDC for GitHub Actions:
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
resource "aws_iam_role" "github_actions" {
name = "github-actions-deploy-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRoleWithWebIdentity"
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
}
}]
})
}
In your GitHub Actions workflow, you then assume this role without storing keys anywhere.
Cost — Right-sized, tagged, budgets set, NAT traffic checked
I've seen AWS bills double overnight because nobody was watching. Cost isn't glamorous, but it's the difference between a sustainable project and one that gets killed because finance pulls the plug.
My cost review questions:
- Are instances and databases right-sized? An
t3.largerunning at 5% CPU is wasting money. Use CloudWatch metrics to find these. - All resources are tagged consistently. Tags by environment, team, cost center, application name. Without consistent tagging, you can't attribute costs to projects or teams.
- AWS Budgets are set up with alerts. When you hit 50% of your monthly budget, you want an alert. 80% should escalate urgently.
- NAT traffic costs are understood. Each GB of data flowing through a NAT gateway costs money. I've seen teams pay thousands per month because they didn't know this. Consider whether an S3 Gateway Endpoint (free) or Interface Endpoint might be better.
- Reserved Instances or Savings Plans are used for predictable workloads. If you know you'll run a database 24/7 for the next year, a one-year RI saves 30-40%. But only commit to what you're sure about.
A real scenario: I reviewed an architecture where developers were constantly pulling large datasets from S3 through a NAT gateway to a private Lambda. 40 GB per day at $0.045/GB = $1,800 per month wasted. Adding an S3 Gateway Endpoint took 30 minutes and saved them $21,600 per year.
Common mistakes
After doing this for years, I see the same patterns:
- Assuming multi-AZ works without testing. Teams enable Multi-AZ on RDS but never simulate a failure. When it happens in production, they panic.
- Over-engineering for day one. Building a globally distributed architecture when you have 100 users creates operational complexity you don't need. Start simple, scale when metrics demand it.
- Skipping the cost estimate. A well-architected system that costs $50k/month isn't well-architected if the business can only afford $5k/month. Always estimate.
- Treating security review as a checkbox. Approving an architecture because the IAM policy exists, without verifying it's actually least privilege. Read the JSON.
- No runbooks for failure scenarios. Everyone understands the happy path. What happens when the database is unavailable? When a Lambda concurrency limit is hit? Document it.
Key takeaways
- Security means least privilege in IAM, private subnets for internal services, encryption everywhere, and centralized audit logging.
- Reliability requires Multi-AZ deployments, intelligent health checks, automated backups that you've actually tested, and regular failover drills.
- Operations maturity means IaC, OIDC-based CI/CD, actionable dashboards, and routed alarms—not console clicks and manual deployments.
- Cost discipline means right-sizing, consistent tagging, AWS Budgets with alerts, and awareness of costs like NAT gateway traffic.
- Every architecture review should end with a question: Can I wake someone up at 2 AM to handle an incident on this system? If not, iterate until you can.






Comments
Loading comments…