My production AWS starter architecture
After shipping products on AWS for years, I've noticed that most teams either over-engineer from day one or skip critical foundations and regret it later. My production AWS starter architecture sits in the middle: it's the baseline setup I'd recommend for any web product expecting real traffic, built on lessons learned from e-commerce platforms, trading systems, and ride-hailing apps that scaled from hundreds to millions of requests.
This isn't a "get started in 5 minutes" guide. This is what you should have in place before your first customer pays money, without the complexity of multi-region failover or advanced observability you don't need yet.
Accounts: Separate dev and prod, Identity Center for access
I learned the hard way that mixing dev and prod in a single AWS account is like storing your backup keys in the same safe. The first time someone deploys to prod during testing, or accidentally scales up the wrong database, you'll understand why this matters.
Start with at least two AWS accounts: one for development and one for production. I use AWS Organizations to manage them, and AWS Identity Center (formerly AWS SSO) to control access. Here's why this structure works:
- Blast radius control: A runaway Lambda in dev can't touch your prod database.
- Cost visibility: You can see dev and prod spend separately and set different budget thresholds.
- Access patterns: Junior developers get full access to dev; only a few engineers can deploy to prod.
- Compliance: Most SOCs want to see account separation before they'll sign the contract.
With Identity Center, I set up permission sets that match team roles. A backend engineer might have read-only access to prod logs and full access to the dev environment. A DevOps engineer gets broader permissions. The whole thing stays synchronized with your directory, so you're not managing IAM users manually.
Pro tip: Enable MFA on the root account, store the credentials in a physical safe or your company's vault, and never use the root account for day-to-day work. Set up a break-glass process for emergencies.
Network: VPC with public and private subnets across two AZs
Your network is the skeleton everything else hangs on. A poorly designed VPC forces you to refactor routing, security groups, and NAT gateways later. The right design scales from hundreds of requests to millions without changes.
I build every VPC the same way: two availability zones, each with a public subnet and a private subnet. Public subnets hold load balancers and NAT gateways. Private subnets hold application servers and databases. This gives you high availability out of the box, and it's simple enough that any engineer on the team can explain it in five minutes.
| Component | AZ-A | AZ-B | Purpose |
|---|---|---|---|
| Public subnet | 10.0.1.0/24 | 10.0.2.0/24 | Load balancer, NAT gateway |
| Private subnet | 10.0.11.0/24 | 10.0.12.0/24 | ECS tasks, RDS, ElastiCache |
For most startups, I also add a third private subnet in each AZ for databases and caches. This isolates data layer traffic from application layer traffic. Use security groups to enforce it: the app security group can only talk to the data security group on specific ports.
NAT gateways are expensive (roughly $32 per month per gateway), so I create one per AZ rather than per subnet. They sit in the public subnets and handle all outbound traffic from private subnets, which means your databases and containers never have public IPs.
Compute and data: ECS Fargate, RDS PostgreSQL Multi-AZ, ElastiCache
This is where your application lives. I use ECS Fargate for compute because it removes the operational burden of managing EC2 instances. Your team focuses on code, not patching kernels.
For a typical web service, I start with one ECS cluster and one Fargate service per environment. The service has a task definition that specifies CPU (0.25–2 vCPU) and memory (0.5–8 GB). Auto Scaling handles traffic spikes by launching additional tasks; CloudWatch triggers it based on CPU or request count.
aws ecs create-service \
--cluster prod-cluster \
--service-name api-service \
--task-definition api-task:1 \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-xxx,subnet-yyy],securityGroups=[sg-zzz],assignPublicIp=DISABLED}" \
--load-balancers targetGroupArn=arn:aws:elasticloadbalancing:...,containerName=api,containerPort=3000Pair this with RDS PostgreSQL Multi-AZ. Multi-AZ gives you automatic failover if the primary database goes down, with zero data loss. It costs roughly double a single-AZ instance, but the downtime you avoid is worth it. Enable automated backups (30-day retention) and enable the backup window during off-peak hours.
Add ElastiCache Redis for session storage and caching. A single cache node (cache.t4g.micro or larger, depending on load) handles most early-stage products. Failover isn't critical yet, so you can skip multi-AZ; if it fails, rebuild from the database. Store connection strings in AWS Secrets Manager and rotate them every 90 days.
Real scenario: I built a travel booking platform that stored everything in Postgres without caching. At 10,000 concurrent users, the database connection pool maxed out and the app crashed. Adding Redis took a day and cut database load by 70%. Start with caching in mind.
Edge: Route 53, CloudFront, ACM and WAF
Users are global, but your infrastructure isn't (yet). Route 53, CloudFront, and WAF form your edge layer: Route 53 directs traffic, CloudFront caches content closer to users, and WAF blocks common attacks.
Start by creating a Route 53 public hosted zone for your domain. Add an A record pointing to your Application Load Balancer (ALB). If your ALB is in us-east-1 and your user is in Sydney, traffic travels across the globe every time. CloudFront fixes that by caching responses at edge locations worldwide. Configure CloudFront with your ALB as the origin, enable gzip compression, and set a cache TTL of 3,600 seconds for dynamic content and 86,400 seconds for static assets.
SSL/TLS certificates come from AWS Certificate Manager (ACM), which is free. Request a certificate for your domain and any subdomains. Attach it to your ALB and CloudFront distribution. ACM handles renewal automatically, so you never have to think about expiration again.
WAF is simpler than it sounds. Create a Web ACL in AWS WAF and attach it to your CloudFront distribution. Use managed rule groups like AWSManagedRulesCommonRuleSet to block SQL injection, XSS, and other OWASP Top 10 attacks. Add rate limiting: block any IP that sends more than 2,000 requests in 5 minutes.
aws wafv2 create-web-acl \
--name prod-waf \
--scope CLOUDFRONT \
--default-action Block={} \
--rules file://rules.json \
--visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=prod-wafOperations: CDK, GitHub Actions with OIDC, CloudWatch alarms and budgets
Infrastructure as code keeps everything reproducible. I use AWS CDK (Cloud Development Kit) in TypeScript because it's expressive, type-safe, and easy for developers already familiar with Node.js to understand. Define your entire stack—VPC, ECS, RDS, CloudFront, WAF—in code, and redeploy with one command.
new ecs.Cluster(this, 'ProdCluster', {
vpc: prodVpc,
clusterName: 'prod-cluster',
containerInsights: true,
});
new rds.DatabaseInstance(this, 'ProdDatabase', {
engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_15_2 }),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.MEDIUM),
multiAz: true,
allocatedStorage: 100,
storageEncrypted: true,
});For deployments, I use GitHub Actions with OpenID Connect (OIDC). OIDC is a standard that lets GitHub securely assume an IAM role in AWS without storing long-lived access keys. Every deploy creates a short-lived credential, so you never have secrets sitting in your GitHub repository.
- name: Assume AWS role
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::PROD_ACCOUNT_ID:role/GitHubActionsRole
aws-region: us-east-1
role-session-name: GitHubActions
- name: Deploy with CDK
run: cdk deploy --require-approval neverCloudWatch alarms notify you when things break. Create alarms for RDS CPU usage above 80%, ALB target health falling below 50%, and ECS service task count dropping below desired count. Send them to SNS, which pipes them to Slack or email. Most teams need only 5–10 alarms to catch the majority of production issues.
Set a monthly budget for your AWS account and receive alerts at 50%, 80%, and 100%. It sounds paranoid, but a misconfigured NAT gateway or runaway EC2 instance can cost thousands in a week. A budget alarm catches it before the credit card bill arrives.
Common mistakes
- Single AZ: "It's just for testing, we'll add redundancy later." Later never comes, and your first prod outage happens during a maintenance window. Build it in from day one; the cost increase is minimal.
- Databases in public subnets: Easier to connect to locally, but now your database has a public IP and you're relying on security groups alone. Private subnets are non-negotiable.
- Skipping WAF: "We'll add it after launch." By then, you're already getting scanned by bots and you're reacting instead of preventing. Enable it before day one.
- Hardcoding secrets: Storing database passwords in code or environment files means they end up in logs, Git history, and container images. Use Secrets Manager or Parameter Store from the start.
- No budgets or billing alerts: I've seen startup teams get hit with surprise AWS bills because someone tested a load generator and forgot to stop it. Set a budget immediately.
- Manual deployments: If deployment requires logging into the console and clicking buttons, it will fail at 3 AM. Automate it with CDK and GitHub Actions.
Key takeaways
- Separate AWS accounts for dev and prod, managed through AWS Organizations and Identity Center, keep blast radius small and access controls tight.
- A VPC with public and private subnets across two AZs gives you high availability and security without overcomplicating the design.
- ECS Fargate removes operational overhead, RDS Multi-AZ removes the fear of data loss, and ElastiCache removes database pressure—the perfect trio for early stage.
- Route 53, CloudFront, ACM, and WAF form your edge layer: they speed up users globally, encrypt traffic, and block attackers before they reach your origin.
- CDK in code, GitHub Actions with OIDC for deployment, CloudWatch alarms for visibility, and budgets for cost control. Together, they let a small team sleep at night.
- This setup costs roughly $300–500 per month (dev and prod combined) and scales to millions of requests without architectural changes. Anything simpler is gambling; anything more complex is premature.






Comments
Loading comments…