A Small AWS Deployment, With the Awkward Parts Included

A diagram with CloudFront, a load balancer, some containers, and a database is a reasonable start.
It does not tell us whether the checkout page can cache someone else's order, whether the containers can download their image, or who is allowed to restore the database after a bad migration.
Those details are where I would spend the second pass. The boxes are familiar enough. Their permissions and failure behavior deserve a little more attention.
For this article, assume an illustrative React application with a Node backend, PostgreSQL, and a few background workers. This is a proposed deployment shape, not a claim about a system I have operated or a measured monthly bill.
Give each component a limited job
One possible layout is:
public DNS -> CloudFront -> static assets in S3 -> application routes through ALB -> ECS/Fargate application tasks -> RDS PostgreSQL -> optional ElastiCache queue -> separate worker tasks
CloudFront is the public edge. ALB distributes application traffic. ECS manages tasks; Fargate avoids operating the underlying servers directly. RDS runs the database. Redis is optional and should have the explicit job we discussed earlier.
I would not add Redis simply to complete the diagram, or Kubernetes simply to make the deployment feel serious. A managed application platform may also be sufficient. The point of this layout is understanding responsibility, not demonstrating the largest collection of AWS abbreviations.
A cache policy is part of authorization safety
Static, content-hashed assets are good candidates for long-lived caching. A signed-in customer's order page is not the same kind of object.
CloudFront distinguishes which values form the cache key from which values are forwarded to the origin. Sending a session cookie to the backend does not, by itself, guarantee responses are isolated correctly in the cache.
For private application responses, start with caching disabled and explicitly configure authentication forwarding. For public cacheable responses, include the dimensions that actually change the representation. Test with two separate users, not twice with the same session.
There is a particularly useful detail in the CloudFront cache behavior documentation: a minimum TTL above zero can force caching even when the origin sends private, no-store, or no-cache.
That is not a theoretical distinction. The application and infrastructure can each look reasonable in isolation while their combined policy is wrong.
Path-pattern ordering matters too. A broad public behavior matching before a narrower protected behavior can apply the wrong rule. I would review edge configuration like application code, including tests for authenticated and unauthenticated variants.
Private does not mean disconnected
Application tasks and databases should not be needlessly reachable from the public internet. But tasks still need outbound paths: pull images, obtain secrets, write logs, and contact payment or email providers.
Putting a task in a private subnet without arranging those paths produces a deployment that cannot start or cannot finish its normal work.
NAT provides general outbound connectivity. VPC endpoints can provide private access to supported AWS services. They have different setup, availability, and cost implications; neither is a universal instruction to copy without checking the task's actual dependencies.
For a small deployment, NAT, load balancers, logs, and minimum database capacity can be a meaningful part of the bill even when application traffic is low. I would estimate those standing costs before optimizing the price of one container.
Do not publish the database merely because it makes a first connectivity test easier. Use a controlled administrative path and keep the production network model intact.
Security groups express specific relationships
Rather than allowing the whole subnet to reach every component, define who needs to talk to whom.
The application's security group can accept traffic from the ALB group on the application port. The database can accept its database port from the application and worker groups that need it. Other callers do not acquire access simply by sharing a VPC.
Also consider origin bypass. If clients can call the public ALB directly, they may bypass restrictions applied only at CloudFront. Depending on the design, use a supported private-origin arrangement or restrict the public origin with appropriate network and request controls. The application must still enforce authentication and authorization itself.
These controls compose. A hidden origin is not an authorization system, and an authenticated route is not a reason to leave every network port open.
The task has two different identities
ECS distinguishes an execution role from a task role.
The execution role supports platform work such as pulling images and delivering configured logs or secrets. The task role is the identity the application uses when calling AWS APIs.
Giving the execution role permission to read an application bucket does not necessarily give the running code that access. Conversely, copying a broad deployment role into the task gives every application bug more authority than it needs.
Keep the roles narrow and separate. Use workload credentials rather than embedding long-lived AWS keys in the image. When access fails, identify which role made the request before adding permissions until the error goes away.
That last approach does work remarkably well at producing permissions nobody can later explain.
Fargate tasks still need a resource budget
Managed compute does not remove per-process connection pools or memory behavior. Two tasks with twenty PostgreSQL connections each are a different load from twenty tasks with the same setting.
Deployment overlap counts too. New tasks start before old tasks drain. The database must tolerate that temporary peak, or the release can run out of connections while average CPU remains comfortable.
RDS Proxy may help pool and manage connections, but it is not a cure for long transactions or unlimited concurrency. Application behavior still determines how long connections remain occupied.
Set a maximum scale consistent with downstream capacity. Separate workers from HTTP tasks so increasing export throughput does not quietly change the web service's deployment and resource profile.
Managed availability is not application continuity
Multi-AZ database deployment can improve availability during infrastructure failure. A read replica addresses a different concern and may lag. Neither undoes an accidental deletion that the database correctly accepted.
Redis failover also has semantics. If cached product descriptions disappear, the database can refill them. If sessions or coordination state disappear, the product consequence is different.
I would name those outcomes before choosing service options. "Highly available" is a useful infrastructure property, but it does not mean an order can be repeated without duplication or that a replica provides the latest permission change.
Backups and recovery get their own article, The Backup Exists. The Restore Still Needs Work. They belong in the initial infrastructure plan even if the first restore exercise comes later.
Deploy an artifact, not a moving tag
A controlled release records an image digest, configuration revision, migration step, and deployment outcome. Infrastructure as code records the intended topology so another environment can reproduce it without reconstructing yesterday's dashboard clicks.
Use an additive migration phase, deploy compatible tasks, observe health and business outcomes, then perform destructive cleanup in a later release. Give long-lived connections a drain policy and old browser assets a retention policy.
A rollback should name which pieces return to their previous state. The database does not automatically move backwards because ECS deploys the previous image.
Finally, add alarms with an owner: application error rate, latency, failed deployments, queue age, database saturation, and spend. Budgets are notifications unless explicit controls say otherwise; they are not a promise that AWS will stop the bill at an exact amount.
I would rather deploy this modest layout with known limits than a more elaborate one nobody can safely change. The diagram can stay small. The operating agreement needs enough detail to survive the first inconvenient event.
The next article examines load balancing and scaling, including why adding healthy replicas can make a shared database less healthy.
When reviewing an AWS layout, which relationship takes the longest to explain: network access, IAM, cache policy, or recovery? That is usually a useful place for the next design conversation.
About the author
Maksym Kuzmitskyi is a Senior Software Developer focused on React and TypeScript. His work includes enterprise applications, shared frontend components, Node.js integration, testing and accessibility. Alongside frontend work, he designs AI-assisted delivery workflows and has developed personal review tooling.
More than a blog post
I share frontend news and the reasoning behind it throughout the day. Pick the language that feels natural to you.