AWS CDK Web App Deployment: VPC, Fargate, Aurora, and CloudFront Wired Together
Most CDK tutorials stop at 'it deployed.' This one covers the full aws cdk web application deployment stack - VPC isolation, Fargate on private subnets, Aurora with KMS, CloudFront, Secrets Manager rotation - and the six operational gaps that will wake you up at 2am if you skip them.
You deployed a CDK stack, the pipeline went green, and you called it production. Three weeks later, at 2am, your on-call gets paged because a credential expired, and it turns out Secrets Manager rotation had been silently failing for a month because someone quietly removed a security group rule during a refactor. That is the story this post is about. Not the happy path, the aws cdk web application deployment fargate aurora stack that survives contact with production traffic, expired secrets, and a bill that did not surprise you.
Who Is This For
If you are an engineer who has shipped CDK stacks before and you know your way around VPCs, this is for you. Not the "what is a subnet" crowd. I assume you know why we have private subnets and why you do not put RDS in a public one.
Specifically, you are probably in one of these spots:
- You are building a production web app on AWS and the tutorials all stop at "VPC plus ALB plus Fargate, good luck."
- You inherited a stack that is "basically production-ready" and you want to know what is actually missing.
- You have wired some of these services together before but never all of them in one CDK app, and you want to understand the dependency graph before you discover it at 2am.
Problem and Why It Matters
Every CDK tutorial hands you the same three services and calls it done. VPC. Fargate. ALB. "You are production-ready." For a demo, you are. Real production also needs encrypted RDS with credential rotation, a CDN layer that does not leak your S3 bucket to the public internet, an auto scaling policy that does not serve degraded traffic during cold starts, and a networking config that does not silently break when someone touches the route tables.
The dangerous part is not that these things are hard. It is that most of them fail quietly. Secrets Manager rotation does not throw an alarm when it fails. It just stops rotating. You find out when the 90-day credential expires and suddenly no tasks can connect to Aurora. That is your 2am call.
Then there is cost, and NAT Gateway is where teams get it most. A NAT Gateway is $0.045/hour, about $32.85/month per gateway at 730 hours, plus $0.045/GB processed (VPC pricing, checked June 2026). Run one per AZ for HA across three AZs and the base is roughly $99/month before you move a single application byte; the $0.045/GB on top is what makes it balloon under real traffic. I have watched engineers deploy three NAT Gateways and not notice the line for two months. The base alone is real money, and the per-GB charge is the part that grows without anyone watching.
The point: the hard part of this stack is not any individual service. It is wiring them so that every dependency edge - VPC endpoint routes, KMS key policies, security group rules - is owned explicitly in code. If it is not in code, it drifts.
When This Approach Fits
This stack is the right call when you have containerized workloads you do not want to manage EC2 for, a relational data model you are not rewriting soon, variable traffic that needs auto scaling, and a security posture that requires encryption at rest and secret rotation.
If you have three users and a startup budget, Aurora provisioned at roughly $150 to $200/month minimum is the wrong choice right now. Look at Aurora Serverless v2, or run RDS Single-AZ until you have real traffic. This stack is for when you are past "prove the idea" and you need the thing to work under load, under audits, and under on-call pressure.
One more thing: if you are not ready to own the operational surface this creates, wait. The EventBridge global endpoint failover in this stack is genuinely useful and genuinely dangerous if you do not understand what it alarms on. More on that in the failure section.
Architecture Overview
Here is the stack, layer by layer.
At the bottom: a custom VPC with three subnet tiers. Public subnets get the ALB and NAT Gateways. Private subnets get the Fargate tasks. Isolated subnets, no internet route at all, get Aurora. This is not just a best-practice checkbox. Isolated subnets mean that even if someone misconfigures a security group, there is no route for Aurora to call home. The VPC itself enforces the isolation.
Above that: an Application Load Balancer in the public subnets, ACM-terminated HTTPS, forwarding to ECS Fargate tasks pulling images from ECR. The Fargate tasks live in private subnets. They reach out through NAT, but nothing external reaches them directly.
Aurora sits in those isolated subnets with KMS encryption. Credentials get vended through Secrets Manager, with rotation configured via a Lambda rotator in the same VPC. S3 plus CloudFront handles static asset delivery with origin access control (OAC, not the old origin access identity pattern), so S3 is never publicly accessible.
Auto Scaling watches ALB RequestCountPerTarget and adjusts the Fargate task count. CloudWatch collects logs and alarms across the stack. And then the interesting bit: an EventBridge global endpoint with Route 53 health checks for cross-region failover. Most CDK examples skip this. Its presence here suggests a team building toward multi-region active-active, not just a DR checkbox.
For a deeper look at how the Fargate networking and VPC endpoint wiring works, I covered that on the ECS Fargate networking and VPC endpoint setup post.
Step-by-Step Implementation
Here is the actual shape of the CDK code. I walk through the Fargate plus ALB wiring since that is the core of the stack; the Aurora setup and EventBridge failover logic deserve their own context because the failure modes are involved.
The VPC construct goes first. Always. Everything depends on it:
const vpc = new ec2.Vpc(this, 'AppVpc', {
maxAzs: 3,
natGateways: 3, // one per AZ - yes this costs money, yes it's worth it
subnetConfiguration: [
{ name: 'Public', subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
{ name: 'Private', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, cidrMask: 24 },
{ name: 'Isolated', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, cidrMask: 24 },
],
});
Then the ECS cluster and Fargate service:
const cluster = new ecs.Cluster(this, 'AppCluster', { vpc });
const taskDef = new ecs.FargateTaskDefinition(this, 'AppTask', {
memoryLimitMiB: 1024,
cpu: 512,
});
taskDef.addContainer('AppContainer', {
image: ecs.ContainerImage.fromEcrRepository(repo, 'latest'),
portMappings: [{ containerPort: 8080 }],
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'app' }),
});
const service = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'AppService', {
cluster,
taskDefinition: taskDef,
publicLoadBalancer: true,
listenerPort: 443,
certificate: acmCert,
taskSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});
And the auto scaling policy:
const scaling = service.service.autoScaleTaskCount({ maxCapacity: 10, minCapacity: 2 });
scaling.scaleOnRequestCount('RequestScaling', {
requestsPerTarget: 1000,
targetGroup: service.targetGroup,
});
That requestsPerTarget number is the one to watch. Too low and you spin up tasks constantly. Too high and you are degraded before scaling kicks in. Start conservative, watch your metrics for two weeks, then tune. I have over-tuned this in both directions and lost a deploy window each time.
Trade-offs
NAT Gateway vs VPC endpoints. Three NAT Gateways for HA is roughly $99/month base before data transfer, and the $0.045/GB on top is what actually scales. You can cut a lot of that by using VPC Interface Endpoints for ECR, Secrets Manager, and CloudWatch Logs, the services your Fargate tasks actually need. The trade-off is construct complexity; each endpoint needs its own security group and DNS config. If you are doing this from scratch, NAT Gateway cost optimization in CDK is where I would start.
Aurora provisioned vs Serverless v2. The provisioned Aurora cluster here runs roughly $150 to $200/month minimum regardless of whether you have one user or ten thousand. That is the right call for consistent baseline traffic; the performance predictability is real. If your traffic is spiky or you are early stage, Aurora Serverless v2 cuts idle cost toward zero. Treat these as estimates and check current RDS pricing for your instance class and region.
Fargate vs EC2. Fargate costs roughly 20 to 30 percent more per vCPU-hour than equivalent EC2. At 5 tasks you do not care. At 50 tasks running 24/7 it is a different conversation. Skipping AMI patching, ECS agent management, and instance right-sizing is worth the premium at low task counts. At high counts, run the math.
What Happens When This Stack Breaks at 2am
The Secrets Manager rotation failure is the one worth time, because it is genuinely sneaky.
Rotation requires a Lambda rotator deployed into the same VPC as your Aurora cluster, with a route to the RDS endpoint and a security group rule allowing the connection. If that security group rule gets removed during a refactor, maybe someone was cleaning up security groups and did not realize the rule was load-bearing, rotation silently fails. Not loudly. Your CloudWatch dashboard looks fine. Your tasks are healthy. Then 90 days after rotation started failing, the credential expires and every task in the cluster throws a connection error at once.
The EventBridge global endpoint failure mode is equally fun. The health alarm uses Invocations as its signal, with treatMissingData: NOT_BREACHING. So if your application legitimately goes quiet, say an overnight maintenance window with no events, the alarm treats missing data as healthy. That is the right default for a consistently event-driven system. But for low-volume applications you can go days without an invocation legitimately, and then when something actually breaks the alarm does not fire because it is used to seeing nothing. You need a dead-man's-switch pattern here, or a more precise metric.
Cold start lag during scaling spikes is the third. ALB RequestCountPerTarget is reactive: you are already under load before the scaling policy sees it. A Fargate task takes 30 to 60 seconds to start. That gap means your first burst hits your existing task count, possibly at full saturation. Predictive scaling or a pre-warm strategy helps, but this is inherent to reactive scaling.
For a deeper look at these patterns, the AWS CDK multi-region architecture patterns post covers the EventBridge failover design.
Security and Operational Considerations
A few things that are not optional.
KMS CMK for Aurora adds about $1/month per key plus $0.03 per 10,000 API calls. That is not the concern. The concern is key policy drift. If the Lambda rotation function's kms:Decrypt permission gets removed from the key policy, again, a quiet refactor drops a statement someone did not recognize, rotation breaks, and you do not find out until the credential expires.
S3 with OAC (origin access control) means your bucket policy only permits CloudFront's service principal, not the deprecated OAI ARN pattern. This is the right setup. If you see a tutorial using OriginAccessIdentity instead of OriginAccessControl, that is the old way. OAC is more secure and works with SSE-S3 and SSE-KMS buckets.
For Secrets Manager rotation specifically, make sure you have a Secrets Manager rotation failure mode alarm in place. AWS does not give you one by default. You have to explicitly alarm on rotation failure events in CloudWatch. Most teams do not add this until after the first incident.
Cost Reality
Real numbers so you are not surprised. Estimates, us-east-1, baseline load:
| Service | Monthly Estimate |
|---|---|
| NAT Gateway x3 (~$33 base each) + data | ~$99 base, plus $0.045/GB |
| Aurora Provisioned (db.t3.medium) | ~$150-200 |
| Fargate (2 tasks baseline) | ~$30-50 |
| CloudFront + S3 | ~$5-20 |
| ALB | ~$20 |
| KMS CMK | ~$1-2 |
| Secrets Manager | ~$1 |
| Total baseline | ~$350-450/month plus NAT data transfer |
Two levers stand out. NAT is the first: replace two of the three NAT Gateways with VPC endpoints for ECR, Secrets Manager, and CloudWatch (the three services Fargate actually needs) and you cut both base gateway hours and the per-GB data processing. Whether that is worth the construct complexity depends on your traffic volume.
Aurora is the other. If your traffic profile justifies Serverless v2, that $150 to $200 baseline drops toward zero at idle and scales with usage.
What I'd Do Differently
Three things.
First, VPC endpoints from day one. I would set up Interface Endpoints for ECR API, ECR DKR, Secrets Manager, and CloudWatch Logs before deploying a single Fargate task. Endpoints cost about $7.30/month per AZ; four endpoints across three AZs is roughly $88/month, cheaper than three NAT Gateways once data processing is in play, and it removes NAT as a single-bandwidth bottleneck for high-throughput workloads.
Second, add an explicit CloudWatch alarm on Secrets Manager rotation failure events on day one. Not "I will add monitoring later." Day one. The failure mode is too quiet and too catastrophic to leave unmonitored.
Third, decide whether you actually need the EventBridge global endpoint before adding it. It is sophisticated and it adds real operational complexity, especially the CDK bootstrap ordering for cross-region stacks. If you are not actively building toward multi-region active-active, a Route 53 health check plus failover record might be all you need, and it is far simpler to reason about.
Failure Modes
The deployment path has three failure modes I have seen in production.
First, Fargate task health checks. If your container takes longer than the ALB health check grace period to start serving, the task gets killed and replaced in a loop. I have seen this with JVM apps that need 45 seconds to warm up against a grace period set to 30. The fix is embarrassingly simple: match the grace period to your actual startup time, not the default.
Second, Aurora failover during a deploy. If Aurora fails over while Fargate is rolling out a new task definition, you get a window where new tasks connect to the old writer endpoint that is now a reader. Connection errors spike, the circuit breaker trips, and your deploy looks failed even though the code is fine. The mitigation is a retry-aware connection using the cluster endpoint, not the instance endpoint.
Third, CloudFront cache invalidation timing. You push a new version, Fargate picks it up, CloudFront still serves the old version from cache. Users see stale content and file bugs. The fix: invalidate on deploy (adds 30 to 60 seconds and costs money) or use versioned asset paths so old and new coexist.
Where to Start
If one thing sticks, make it this: wiring services together in CDK means you own every dependency edge explicitly. That is the value of doing it in code, and also the trap, because when an edge breaks - a security group rule, a key policy statement, a route table entry - it breaks quietly, and you find out at credential expiry, not deploy time. Before you ship, add the Secrets Manager rotation alarm, confirm your Fargate health check grace period matches real startup time, and trace where your NAT traffic is actually going. Those three close most of the 2am gaps in this stack.
Next step
Have the same problem on your stack?
Send the architecture, AWS bill concern, deploy pain, or GenAI reliability issue. I'll find the first real bottleneck and propose a small, reversible fix.
Related reading

Rahul Ladumor
Principal Cloud & AI Platform Architect. AWS Professional certified, 4x AWS Community Builder. I work with teams that have real users, real AWS bills, and real production pressure.
About Rahul →