Skip to content

Core lesson

Public Web App On AWS

A highly available public web application with one public entrance, private application servers, managed state, and health-based scaling.

18 min read

After this, you will understand

Learn the reusable AWS pattern behind many AWS Solutions Architect Associate exam (SAA-C03) questions: one public entrance, replaceable private compute, and durable managed state.

Article guideprerequisites, mental models, and concepts

Article overview

foundationCloudCertificationNetworking

Three useful mental models

In plain terms

Users reach a public load balancer, the load balancer sends requests to healthy private application instances, and those instances use private databases and managed storage.

Decision pressure

One public server becomes the entry point, application, file store, database, credential store, and single failure boundary.

Exam-ready model

Separate the web edge, application tier, and data tier; spread capacity across Availability Zones; then use health checks, Auto Scaling, IAM roles, RDS, and S3 for their specific jobs.

Think before reading

If the application is public, which part actually needs to accept traffic from the internet?

Usually the internet-facing Application Load Balancer. The EC2 application instances and database can remain private behind it.

Connected learning

These lessons add useful context to the current core lesson.
  1. 1CloudFront WAF Protected Web EdgeAWS Scenario

Concepts Covered

  • Dynamic public web applications
  • Regions, Availability Zones, and subnet tiers
  • Internet-facing Application Load Balancers
  • Listeners, target groups, and health checks
  • Private EC2 instances in an Auto Scaling group
  • Stateless application servers
  • Private RDS Multi-AZ databases
  • S3 object storage and presigned uploads
  • EC2 instance roles and temporary credentials
  • Security group chaining
  • NAT gateways versus inbound application traffic
  • CloudWatch metrics, alarms, and logs
  • AWS Solutions Architect Associate exam (SAA-C03) recognition patterns

1. Situation

Your team is launching an online store. Unlike a purely static website, this application must run server-side code for each request.

For example, when a user opens /orders/42, the application must:

  1. identify the user;
  2. read order data from a relational database;
  3. apply business rules; and
  4. generate a response.

Users can also upload product images and receipts. Traffic is usually moderate, but promotions can create sudden spikes. The business cannot accept an outage every time one virtual machine or one Availability Zone has a problem.

The requirements are:

  • a public HTTPS domain;
  • no direct internet access to application servers or the database;
  • enough capacity when traffic grows;
  • continued service when one application instance fails;
  • continued service when one Availability Zone has a problem;
  • durable storage for relational data and uploaded files; and
  • no long-lived AWS access keys stored on servers.

This study develops the foundational AWS pattern:

public entry point -> private application tier -> private data tier

The goal is not to memorize a diagram. It is to understand which problem forces each layer to exist.

2. Naive Design

The team begins with one EC2 instance that has a public IP address:

flowchart LR
  User["Internet users"] --> Server["One public EC2 instance<br/>web app + files + database"]

The instance does everything:

  • accepts HTTPS requests;
  • runs the application;
  • stores uploaded files on its local disk;
  • runs the database;
  • stores user sessions in memory; and
  • keeps AWS access keys in a configuration file.

This resembles a familiar virtual private server. It is easy to understand, inexpensive at small scale, and reasonable for an early experiment.

Its simplicity comes from combining every responsibility into one failure boundary.

Adding a larger instance postpones that problem. It does not remove it.

3. What Breaks

Follow four failure stories.

Failure 1: the server stops

At 10:00, the instance fails a system status check. The application, database, uploaded files, and in-memory sessions all disappear from service together.

There is no second healthy application target and no component able to route around the failure.

The architecture needs multiple application instances and health-based routing.

Failure 2: a promotion creates a traffic spike

At 14:00, request volume increases fivefold. The single server reaches its CPU and connection limits. Requests become slow, then begin timing out.

Manually resizing the instance requires prediction and intervention. The architecture needs horizontal scaling: adding and removing application instances as demand changes.

Failure 3: the team adds a second server

The second instance helps with compute, but state now splits:

user A uploads image.jpg -> instance A local disk
next request             -> instance B cannot find image.jpg

The same problem affects in-memory sessions. Replaceable servers cannot be the only owners of durable or shared state.

The architecture needs state outside the application instances.

Failure 4: public exposure grows with every component

The server accepts user traffic, SSH, and database connections. Long-lived AWS keys sit on disk. A mistake in one security rule can expose far more than the web application.

The architecture needs a narrow public boundary and temporary workload credentials.

These failures lead to four design moves:

one public entrance
+ multiple replaceable app instances
+ managed durable state
+ identity without stored access keys

4. AWS Architecture

Build the architecture one responsibility at a time.

Step 1: create network boundaries across two Availability Zones

A VPC is the application's private network boundary in an AWS Region. An Availability Zone, or AZ, is an isolated location within that Region.

Use at least two AZs so one location is not the only place able to run the application.

Within each AZ, create subnet tiers:

flowchart LR
  subgraph VPC["VPC in one AWS Region"]
    subgraph AZA["Availability Zone A"]
      PublicA["Public subnet"]
      AppA["Private app subnet"]
      DBA["Private database subnet"]
    end

    subgraph AZB["Availability Zone B"]
      PublicB["Public subnet"]
      AppB["Private app subnet"]
      DBB["Private database subnet"]
    end
  end

A public subnet has a route to an internet gateway. A private subnet does not have a direct route to the internet gateway. The label comes from routing, not from the resource's name.

The public subnets will hold the internet-facing entry layer. Application and database resources will use the private tiers.

Step 2: add one public entry point

Create an internet-facing Application Load Balancer (ALB) in public subnets across at least two AZs.

The ALB understands HTTP and HTTPS. Its listener waits for traffic on a port such as 443. A listener rule forwards matching requests to a target group, which is the set of application backends eligible to receive them.

flowchart LR
  DNS["Route 53 alias<br/>app.example.com"] -.->|"DNS answer"| User["User"]
  User -->|"HTTPS"| ALB["Internet-facing ALB<br/>public subnets"]
  ALB --> Target["Target group"]
  Target --> App["Application target"]

Attach an ACM certificate to the HTTPS listener. Because an ALB is regional, its ACM certificate is created in the same Region as the load balancer.

Route 53 only answers DNS. It tells the browser how to find the ALB; it is not an HTTP hop through which every request passes.

Step 3: make the application tier private and replaceable

Launch EC2 application instances in the private app subnets through an Auto Scaling group.

The Auto Scaling group uses a launch template to describe each new instance: its machine image, instance type, security groups, IAM role, and startup configuration. It also maintains a chosen minimum, desired, and maximum capacity.

flowchart LR
  User["User"] --> ALB["Public ALB"]
  ALB --> TG["Target group<br/>health checks"]
  TG --> AppA["EC2 app<br/>private subnet, AZ A"]
  TG --> AppB["EC2 app<br/>private subnet, AZ B"]
  ASG["Auto Scaling group<br/>desired capacity"] -.->|"launches and replaces"| AppA
  ASG -.->|"launches and replaces"| AppB

The ALB sends traffic only to targets that pass the target group's health check. When Elastic Load Balancing health checks are enabled for the Auto Scaling group, the group can replace an instance that the load balancer reports as unhealthy.

These are related but different jobs:

  • the ALB routes around an unhealthy instance;
  • the Auto Scaling group restores capacity by launching a replacement; and
  • a scaling policy changes capacity when load changes.

The private EC2 instances do not need public IP addresses to receive requests from the ALB. The ALB reaches them over private VPC networking.

Step 4: move durable state out of EC2

Place the relational database in private database subnets through an RDS DB subnet group. When the availability requirement calls for it, use a Multi-AZ DB instance deployment with a synchronous standby in another AZ.

Store uploaded objects in S3 rather than on EC2 instance disks.

flowchart LR
  AppA["EC2 app<br/>AZ A"] --> RDS["RDS writer endpoint<br/>private database tier"]
  AppB["EC2 app<br/>AZ B"] --> RDS
  RDS ==>|"synchronous standby copy"| Standby["RDS standby<br/>another AZ"]
  AppA -->|"uploads and reads"| S3["S3 bucket<br/>durable objects"]
  AppB --> S3

Multi-AZ protects database availability; its standby is not the read-scaling target in this classic deployment. Backups and point-in-time recovery are still needed because a standby can receive bad data changes as well as good ones.

Once shared state lives in RDS and S3, any healthy application instance can handle the next request. This is the practical meaning of a stateless application tier: an instance is not the only durable owner of data needed by future requests.

For large uploads, the application can create a short-lived S3 presigned URL. The browser then uploads a specific object directly to S3 without receiving AWS credentials and without sending the file bytes through EC2.

Step 5: give the workload an identity

Attach an IAM role to the EC2 instances through an instance profile. The application obtains rotating temporary credentials for the role instead of reading long-lived access keys from a file.

Grant only the actions and resources the workload needs, such as access to one S3 bucket prefix or one Secrets Manager secret.

An IAM role answers:

what AWS APIs may this application call?

A security group answers:

which network connections may reach this resource?

Do not confuse identity permissions with network reachability. A role that permits s3:PutObject does not create a network route to S3, and a route to S3 does not grant permission to upload.

Step 6: add outbound access only where it is needed

Private application instances may need outbound access for operating system updates or external APIs. A public NAT gateway can provide outbound IPv4 access while preventing the internet from initiating connections to those instances.

NAT is not part of the user's inbound request path:

inbound user request: user -> ALB -> private EC2
outbound app request: private EC2 -> NAT gateway -> internet

For S3 and DynamoDB traffic, a gateway VPC endpoint can provide a private route and avoid sending that traffic through a NAT gateway.

The completed architecture is:

flowchart LR
  DNS["Route 53 alias"] -.->|"resolves domain"| User["User"]
  User -->|"HTTPS"| ALB["Internet-facing ALB<br/>public subnets, two AZs"]
  ACM["ACM certificate<br/>application Region"] -.->|"TLS identity"| ALB

  ALB --> AppA["EC2 app<br/>private subnet, AZ A"]
  ALB --> AppB["EC2 app<br/>private subnet, AZ B"]
  ASG["Auto Scaling group"] -.->|"maintains capacity"| AppA
  ASG -.->|"maintains capacity"| AppB

  AppA --> RDS["Private RDS<br/>Multi-AZ"]
  AppB --> RDS
  AppA -->|"instance role"| S3["S3 bucket<br/>uploaded objects"]
  AppB -->|"instance role"| S3

5. Request Or Data Flow

Learn three lifecycles instead of memorizing every arrow at once.

Lifecycle 1: normal application request

  1. Route 53 answers the DNS query for app.example.com with the ALB target.
  2. The browser opens an HTTPS connection to the ALB.
  3. The ALB listener evaluates its rules and selects the target group.
  4. The ALB sends the request to one healthy EC2 target over private networking.
  5. The application reads or writes relational data through the RDS endpoint.
  6. The application returns the response through the ALB to the user.
flowchart LR
  User["User"] -->|"1. HTTPS"| ALB["ALB"]
  ALB -->|"2. healthy target"| App["Private EC2 app"]
  App -->|"3. SQL"| RDS["Private RDS"]
  RDS --> App --> ALB --> User

Lifecycle 2: an application instance fails

  1. The target stops returning the expected health-check response.
  2. The ALB marks it unhealthy and stops routing new requests to it.
  3. Healthy targets in the other AZ continue receiving traffic.
  4. If ELB health checks are enabled on the Auto Scaling group, the group replaces the unhealthy instance.
  5. The replacement starts from the launch template, passes health checks, and enters service.

Health checks should test whether the application can serve useful traffic. A check that returns success while the application cannot operate gives the load balancer false confidence. Avoid making the check so dependent on every downstream system that one database issue causes every app target to be replaced simultaneously.

Lifecycle 3: a user uploads a file

For a simple upload, the application receives the file and writes it to S3 using its instance role.

For a larger direct upload:

sequenceDiagram
  participant User
  participant App as Private EC2 app
  participant S3

  User->>App: Ask permission to upload metadata
  App->>App: Authorize user and choose object key
  App-->>User: Return short-lived presigned URL
  User->>S3: Upload the object with that URL
  S3-->>User: Upload result

The application still decides who may upload and what object key is allowed. S3 carries the file bytes.

6. Security Controls

Chain security groups by tier

Use security group references to express which tier may call the next tier:

Security groupAllow inbound fromTypical purpose
ALB security groupInternet clients on HTTPSPublic application entry
Application security groupALB security group on the application portPrevent direct client access to EC2
Database security groupApplication security group on the database portPrevent direct client and ALB access to RDS

This is more precise than allowing the entire VPC CIDR at every layer.

Keep application and database resources private

Do not assign public IP addresses to normal application instances. Do not make RDS publicly accessible. The fact that the product is public does not make every component public.

Use roles for AWS API access

Attach a least-privilege instance role rather than storing access keys in the AMI, source code, user data, or environment files managed by hand.

Store application secrets separately

An EC2 role proves the application's AWS identity. A database password is still a secret. Store secrets in a managed service such as Secrets Manager and grant the role permission to retrieve only the required secret.

Protect administrative access

Avoid opening SSH to the world. Prefer Systems Manager Session Manager or tightly controlled administrative paths when interactive access is required.

Encrypt the intended paths

Use HTTPS from users to the ALB. Use encryption for RDS, S3, EBS, backups, and application-to-database connections when the security requirement calls for it.

7. Resilience Controls

Run real capacity in more than one AZ

Selecting two subnets is not enough if the Auto Scaling group runs only one instance. To tolerate one instance or AZ failure without losing all application capacity, maintain healthy instances across multiple AZs and enough spare capacity for the remaining AZ to carry the load.

Combine routing with replacement

The ALB removes unhealthy targets from service. Auto Scaling replaces failed capacity. Neither action repairs application bugs, so monitor whether a new release causes every target to fail the same health check.

Keep compute replaceable

Do not make local files or in-memory sessions the only copy of important state. Use RDS, S3, DynamoDB, ElastiCache, or another suitable shared service according to the data's durability and consistency requirements.

Protect the database separately

Use RDS Multi-AZ for managed failover when database availability matters. Use automated backups and point-in-time recovery for historical recovery. These solve different problems.

Review Highly Available RDS App for the database failover lifecycle and application reconnect behavior.

Avoid moving the failure boundary to NAT

If private instances require resilient internet egress, use a NAT design that matches the AZ failure requirement. A single NAT gateway in one AZ can become an availability dependency for outbound operations in other AZs.

Monitor the user path, not only the servers

Use CloudWatch metrics and alarms for signals such as ALB response time and 5XX errors, healthy target count, Auto Scaling capacity, EC2 resource pressure, and RDS connections and storage. Centralize application logs so a terminated instance does not take its only diagnostic history with it.

An alarm should describe an action-worthy symptom. For example, low healthy-target count reveals lost serving capacity more directly than an alarm that reports only one instance's CPU.

8. Performance Controls

Use a scaling metric that represents application pressure. Useful signals may include:

  • ALB request count per target;
  • CPU utilization for compute-heavy work;
  • queue depth for worker-style processing; or
  • a custom latency or concurrency metric.

CPU is not automatically the best metric. An application can run out of database connections or request workers while CPU remains moderate.

Set a realistic instance warm-up period so a newly launched instance has time to start before scaling decisions treat it as fully ready.

Move static assets and public downloads away from the application tier when useful. S3 and CloudFront can deliver those objects without consuming EC2 request capacity. The next study, CloudFront WAF Protected Web Edge, develops that global edge.

Tune the database only after identifying the bottleneck. Indexing, connection pooling, instance sizing, caching, and read replicas solve different pressures. A read replica helps read scale; it is not the classic automatic Multi-AZ failover answer.

9. Cost Controls

Set deliberate Auto Scaling minimum, desired, and maximum capacity. A minimum that is too low may violate availability; a minimum that is too high wastes money.

Right-size EC2 and RDS using real metrics. Use Savings Plans or reservations for predictable baseline usage, and use Spot Instances only for capacity that can tolerate interruption.

Watch NAT gateway processing and cross-AZ data-transfer patterns. Use an S3 gateway endpoint when it meets the access pattern so private S3 traffic does not need NAT.

Store uploaded files in S3 rather than growing every application instance's EBS volume. Apply lifecycle rules when older objects or logs can move to cheaper storage classes or expire.

Do not add Multi-AZ, read replicas, caches, CloudFront, and multiple NAT gateways merely because they appear in a reference diagram. Each adds value only when a requirement justifies its availability, performance, security, or operational benefit.

10. Exam Variants

If the question says...Think...Why
"Highly available web application"ALB + Auto Scaling targets across multiple AZsThe ALB routes around failures while Auto Scaling maintains capacity
"Only the load balancer should be public"Internet-facing ALB in public subnets; EC2 in private subnetsPublic ingress and private execution are separate tiers
"Instances need S3 access without stored credentials"EC2 instance roleThe workload receives temporary credentials automatically
"Database must not be internet accessible"Private database subnets + database security groupOnly the application tier needs database network access
"Automatic relational database failover"RDS Multi-AZA standby is maintained for availability
"Scale read-heavy database traffic"Read replicaRead scaling is different from Multi-AZ failover
"Uploaded files disappear when instances are replaced"Store objects in S3Application instances should not own the only durable copy
"Private instances need software updates from the internet"NAT gateway for outbound accessNAT permits initiated outbound connections, not inbound user traffic
"Private instances need only S3 or DynamoDB"Gateway VPC endpointThose service paths can avoid NAT
"Global users need cached content and edge protection"Add CloudFront and, when required, AWS WAFThis evolves the regional public edge

Reconstruct the base design from the requirements:

one public web entrance       -> internet-facing ALB
healthy dynamic compute      -> target group + EC2 Auto Scaling
no direct server exposure    -> private application subnets
relational durable state     -> private RDS
uploaded object state        -> S3
AWS API permissions          -> EC2 instance role
AZ failure tolerance         -> real capacity across multiple AZs

11. Common Traps

TrapBetter reasoning
"The app is public, so EC2 must have a public IP."The ALB is public and reaches EC2 over private VPC addresses.
Drawing user -> Route 53 -> ALB as the HTTP pathRoute 53 answers DNS; the user then connects to the ALB.
Sending inbound users through a NAT gatewayNAT provides outbound connectivity for private resources; it is not the public application entrance.
Running one instance while selecting two AZsSubnet selection alone does not create redundant application capacity.
Assuming the ALB replaces failed serversThe ALB routes around failure; Auto Scaling replaces capacity when configured to use the relevant health signal.
Saving uploads or sessions only on EC2Replaceable instances require shared or durable state outside the instance.
Storing AWS keys on the serverUse an instance role with temporary credentials.
Allowing the entire internet to reach the application portLet the app security group accept that port only from the ALB security group.
Using a read replica for automatic failoverClassic Multi-AZ and read replicas solve availability and read-scaling requirements respectively.
Treating Multi-AZ as a backupSynchronously copied mistakes still require backup recovery.
Making the health check depend on every downstream serviceA shared downstream failure can make all targets unhealthy and trigger harmful replacement churn.

Final Mental Model: One-Minute Review

Final Architecture Map

Public web application architecture showing Route 53, a public Application Load Balancer, private EC2 instances across two Availability Zones, RDS Multi-AZ, Auto Scaling, and S3.

Use this map to consolidate the boundaries: the ALB is public, the application instances are private and replaceable, and durable state lives outside EC2.

TermExact jobRemember it as
Route 53Answers DNS so the client can find the public entry pointThe address book
Application Load BalancerAccepts public HTTP/HTTPS traffic and routes it to healthy targetsThe reception desk
Target group and health checksTrack which application instances are ready to receive requestsThe healthy-worker roster
Auto Scaling groupMaintains and adjusts the number of EC2 application instancesThe staffing manager
Private EC2 instancesRun the server-side application without accepting direct internet trafficWorkers in private offices
RDS and S3Keep relational data and uploaded objects outside replaceable computeThe records room and warehouse
IAM roleGives the application temporary permission to call AWS APIsThe worker's job badge
Security groupsControl which network connections may enter each tierThe locks between rooms
flowchart LR
  DNS["Route 53<br/>DNS"] -.->|"Resolves the name"| User["User"]
  User -->|"HTTPS"| ALB["Public ALB<br/>reception desk"]
  ALB -->|"Healthy targets only"| App["Private EC2 apps<br/>replaceable workers across AZs"]
  ASG["Auto Scaling group<br/>staffing manager"] -.->|"Maintains capacity"| App
  App --> RDS["RDS<br/>relational records"]
  App --> S3["S3<br/>uploaded objects"]
  Role["IAM role<br/>temporary AWS permissions"] -.->|"Attached identity"| App

The core pattern is one public entrance, replaceable private compute, and durable state outside the instances. The ALB routes around failure; Auto Scaling replaces capacity; IAM roles authorize AWS API calls; security groups control network paths.

Review Public vs Private Subnets for the routing boundary, ALB vs NLB vs GWLB for load-balancer selection, Amazon EC2 Auto Scaling for capacity behavior, Amazon RDS for managed relational databases, Amazon S3 for durable object storage, and Amazon CloudWatch for metrics, alarms, and logs.

Continue to CloudFront WAF Protected Web Edge to evolve this regional architecture into a global cached and protected entry point.

Official AWS references:

Finished reading?

Your reading history is saved in this browser so you can continue later.

Recommended Next

CloudFront WAF Protected Web EdgeAWS Architecture Scenarios17 min read

This applies the foundation mental models to a real architecture decision instead of a service inventory.

Optional exploration

These links add context, but they do not replace the recommended next lesson.

Arcflow Plus is coming — review drills, research breakdowns, more AI. Get one email at launch.