Build a Multi-Tier AWS Architecture

Deploy a highly available multi-tier AWS architecture across multiple AZs.

Introduction

30 Second Summary

Every time you stream a show, check your bank balance, or order food online, you're relying on infrastructure designed to keep working even when servers fail. That resilience comes from a multi-tier architecture spread across multiple data centers.

In this project, you will build a production-grade multi-tier architecture on AWS, deploying 15+ core services that map directly to all four SAA-C03 exam domains. You'll go from an empty account to a fully connected system spanning compute, networking, databases, storage, serverless, and monitoring.

What You'll Build

A highly available web application infrastructure with a custom VPC, Auto Scaling EC2 instances behind an Application Load Balancer, a Multi-AZ RDS database, S3 static hosting with CloudFront CDN, event-driven Lambda processing, and CloudWatch monitoring.

By the end of this project, you'll have:

  • A custom VPC with public and private subnets across two Availability Zones, secured by Security Groups implementing defense-in-depth.
  • An Auto Scaling Group of EC2 instances behind an Application Load Balancer, with a Multi-AZ RDS database for automatic failover.
  • S3 static hosting fronted by a CloudFront distribution, plus a Lambda function triggered by S3 upload events and CloudWatch alarms with SNS notifications.
  • Secret Mission: Add a VPC Gateway Endpoint for S3 to eliminate NAT Gateway data processing charges, verified with VPC Flow Logs.

Are there any prerequisites?

You need an AWS account with a budget of approximately $5-10 for same-day resource usage. No free tier is required.

This project assumes familiarity with cloud architecture concepts. If you have a GCP background, you'll see AWS equivalents called out along the way.

Set Your Region and Verify Access

Before building any infrastructure, you need to confirm your AWS Console access and set up a billing safety net.

This project provisions multiple paid resources. A billing alarm acts as your financial guardrail, sending you an email if your AWS charges approach $10 USD.

In this step, get ready to:
  • Set your AWS Console region to US East (N. Virginia).
  • Optionally verify AWS CLI access.
  • Create a CloudWatch billing alarm with email notifications.
Log into the AWS Console and set your region

All the resources in this project will live in a single AWS region. Using us-east-1 (US East N. Virginia) keeps pricing lowest and ensures every service we need is available.

  • Open your browser and navigate to the AWS Management Console at https://console.aws.amazon.com.
  • Sign in with your AWS account credentials.
  • In the top-right corner of the console, click the region dropdown.
  • Select US East (N. Virginia) from the list.
  • Confirm the region dropdown now displays N. Virginia.

Now confirm you can access the VPC dashboard, where you will build the project's networking in the next step.

  • In the search bar at the top of the console, type VPC and press Enter.
  • You should see the VPC dashboard load, showing sections like Your VPCs, Subnets, and Route tables.

Don't see the VPC dashboard?

Make sure you are signed into an account with IAM permissions for VPC. If you see an "Access Denied" or blank page, your IAM user or role may not have the required policies attached.

help me troubleshoot why I can't access the VPC dashboard in AWS Console

Verify AWS CLI access (optional)

The AWS CLI is not required for this project. However, if you have it installed, a quick identity check confirms your credentials are configured correctly for any optional verification commands later.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), then typing Terminal and pressing Enter.
  • Verify your identity by running this command:
aws sts get-caller-identity

What does this command do?

This calls the AWS Security Token Service to return your account ID, user ARN, and user ID. If it works, your CLI credentials are properly configured.

✔️ I see my account ID and ARN

Your CLI is configured. You can use it for optional verification steps throughout the project.

ⓧ I see an error or command not found

No worries. The AWS CLI is optional for this project. All core steps use the AWS Management Console. You can skip this and continue to the next substep.

If you want to install the CLI later, visit the official AWS CLI install guide.

Create a billing alarm

A billing alarm watches your total estimated AWS charges and emails you when they cross a threshold. This is your financial safety net for the project.

Before creating the alarm, you need to enable billing alerts in the Billing console. This tells AWS to start publishing the EstimatedCharges metric to CloudWatch.

  • In the console search bar, type Billing and select Billing and Cost Management.
  • In the left navigation pane, click Billing Preferences.
  • Next to Alert preferences, click Edit.
  • Check the box for Receive CloudWatch Billing Alerts.
  • Click Save preferences.

Why enable billing alerts first?

AWS does not publish the EstimatedCharges metric to CloudWatch by default. Enabling this preference tells AWS to start collecting and sending your charge data several times per day. Once enabled, this cannot be turned off (but you can always delete any alarms you create).

Now create the alarm itself in CloudWatch.

  • In the console search bar, type CloudWatch and open the service.
  • Confirm your region is still set to US East (N. Virginia) in the top-right corner. Billing metric data is stored exclusively in this region.
  • In the left navigation pane, click Alarms, then click All alarms.
  • Click Create alarm.

You are now on the metric selection page. Select the billing metric.

  • Click Select metric.
  • In the Browse tab, click Billing, then click Total Estimated Charge.
  • Select the checkbox next to the EstimatedCharges metric (Currency: USD).
  • Click Select metric.

Don't see the Billing metric?

If you do not see Billing or Total Estimated Charge in the metric list, check two things. First, make sure you enabled Receive CloudWatch Billing Alerts in the Billing Preferences step above. Second, confirm your region is set to US East (N. Virginia). The billing metric only appears in this region.

help me find the EstimatedCharges billing metric in CloudWatch

Now configure the alarm threshold and conditions.

  • For Statistic, choose Maximum.
  • For Period, choose 6 hours.
  • Under Conditions, for Threshold type, choose Static.
  • For Whenever EstimatedCharges is..., choose Greater.
  • For than..., enter 10 (this is $10 USD).
  • Click Next.

Now configure the notification action using SNS (Simple Notification Service).

  • Under Notification, confirm In alarm is selected as the alarm state trigger.
  • Select Create new topic.
  • Enter a topic name such as saa-billing-alerts.
  • Enter your email address in the email endpoint field.
  • Click Create topic.
  • Click Next.

Finally, name your alarm and create it.

  • Under Name and description, enter a name such as saa-billing-alarm-10usd.
  • Click Next.
  • Review the settings on the Preview and create page.
  • Click Create alarm.

What does this alarm do?

CloudWatch checks your total estimated charges every 6 hours. If the maximum exceeds $10 USD, the alarm enters the ALARM state and sends a notification to your SNS topic, which emails you.

The alarm only triggers on actual accumulated charges for the current month. It does not project future costs.

You now need to confirm the SNS email subscription. Without this, the alarm will fire but you will not receive the email.

  • Check your email inbox for a message from AWS Notifications with the subject "AWS Notification - Subscription Confirmation".
  • Open the email and click Confirm subscription.
  • Return to the CloudWatch Alarms page. Your alarm may initially display as Insufficient data. This is normal because the EstimatedCharges metric only updates every 6 hours.

Don't see the confirmation email?

Check your spam or junk folder. The email comes from no-reply@sns.amazonaws.com. If it does not arrive within a few minutes, go back to the SNS console, find your topic, and re-send the subscription confirmation.

help me confirm my SNS email subscription for CloudWatch billing alerts

Your safety net is in place. Next up, you will build the VPC foundation with subnets across two Availability Zones.

Build the VPC Foundation

Your billing alarm is in place, so you have a safety net. Now it is time to build the network foundation for your entire architecture.

Every AWS resource you deploy needs a network to live in. A VPC (Virtual Private Cloud) is your own isolated section of the AWS cloud where you define IP address ranges, subnets, routing, and security rules. In this step, you will build a production-style VPC with public and private subnets, routing, and layered security groups.

In this step, get ready to:
  • Create a custom VPC with four subnets across two Availability Zones.
  • Set up routing with an Internet Gateway and NAT Gateway.
  • Create security groups that implement defense-in-depth.
Create the VPC and subnets

Your VPC needs a CIDR block that defines its total IP address space. You will then carve that space into smaller subnets spread across two Availability Zones for redundancy.

What is a CIDR block?

A CIDR block like 10.0.0.0/16 defines a range of IP addresses. The /16 means the first 16 bits are fixed, giving you 65,536 possible addresses to split across your subnets.

  • In the AWS Console, type VPC in the top search bar and select VPC from the results.
  • On the VPC dashboard, click Create VPC.
  • For Resources to create, select VPC only.
  • Enter saa-project-vpc in the Name tag field.
  • For IPv4 CIDR block, choose IPv4 CIDR manual input and enter 10.0.0.0/16.
  • Leave IPv6 CIDR block set to No IPv6 CIDR block.
  • Click Create VPC.

You should see a success banner confirming your VPC was created.

Now create the four subnets. Two will be public (for resources that face the internet) and two will be private (for resources that should not be directly accessible).

  • In the VPC console left navigation, click Subnets.
  • Click Create subnet.
  • Under VPC ID, select saa-project-vpc.
  • Under Subnet settings, configure the first subnet with Subnet name as public-subnet-1a, Availability Zone as us-east-1a, and IPv4 CIDR block as 10.0.1.0/24.
  • Click Add new subnet to add the second subnet: name it public-subnet-1b, set Availability Zone to us-east-1b, and IPv4 CIDR block to 10.0.2.0/24.
  • Click Add new subnet again for the third subnet: name it private-subnet-1a, set Availability Zone to us-east-1a, and IPv4 CIDR block to 10.0.3.0/24.
  • Click Add new subnet one more time for the fourth subnet: name it private-subnet-1b, set Availability Zone to us-east-1b, and IPv4 CIDR block to 10.0.4.0/24.
  • Click Create subnet.

You should see all four subnets listed in the Subnets table. Now enable auto-assign public IP on the two public subnets so that instances launched in them get public addresses.

  • Select public-subnet-1a by clicking its checkbox.
  • Click Actions, then Edit subnet settings.
  • Check the box for Enable auto-assign public IPv4 address.
  • Click Save.
  • Repeat the same steps for public-subnet-1b.

Why two Availability Zones?

Availability Zones are physically separate data centers within a region. By spreading subnets across two AZs, your application keeps running even if one data center has an outage. This is the foundation of high availability on AWS and maps directly to SAA-C03 Domain 2 (Resilient Architectures).

Don't see your VPC in the subnet dropdown?

Make sure you are still in the us-east-1 region. Check the region dropdown in the top-right corner of the console. If you created the VPC in a different region, it will not appear.

Help me find my VPC in the subnet creation form.

Set up routing with an Internet Gateway and NAT Gateway

Subnets alone cannot reach the internet. You need an Internet Gateway for public subnets and a NAT Gateway for private subnets. Route tables control which traffic goes where.

  • In the VPC console left navigation, click Internet gateways.
  • Click Create internet gateway.
  • Enter a name tag (for example saa-project-igw) and click Create internet gateway.
  • On the banner at the top, click Attach to a VPC.
  • Select saa-project-vpc and click Attach internet gateway.

The state should change to Attached. Now create a public route table that directs internet-bound traffic to this gateway.

  • In the left navigation, click Route tables.
  • Click Create route table.
  • Enter a name (for example saa-public-rt), select saa-project-vpc as the VPC, and click Create route table.
  • On the route table detail page, click the Routes tab, then click Edit routes.
  • Click Add route. Enter 0.0.0.0/0 for Destination and select the Internet Gateway for Target.
  • Click Save changes.
  • Click the Subnet associations tab, then click Edit subnet associations.
  • Select both public-subnet-1a and public-subnet-1b, then click Save associations.

Now set up the NAT Gateway so private subnets can reach the internet for software updates without being directly exposed.

  • In the left navigation, click NAT gateways.
  • Click Create NAT gateway.
  • Enter a name (for example saa-nat-gw).
  • For Subnet, select public-subnet-1a.
  • For Connectivity type, leave the default Public selected.
  • Click Allocate Elastic IP to assign a public IP address to the NAT Gateway.
  • Click Create NAT gateway.

Why does the NAT Gateway live in a public subnet?

The NAT Gateway needs its own internet access (through the Internet Gateway) to relay outbound traffic from private instances. Placing it in a public subnet with a route to the IGW gives it that access. Private instances then route through the NAT Gateway instead of accessing the internet directly.

This is the AWS equivalent of GCP Cloud NAT. The key difference is that AWS NAT Gateways charge $0.045/hr plus data processing fees.

Wait for the NAT Gateway status to change from Pending to Available (1-2 minutes). Then create the private route table.

  • In the left navigation, click Route tables.
  • Click Create route table.
  • Enter a name (for example saa-private-rt), select saa-project-vpc as the VPC, and click Create route table.
  • On the Routes tab, click Edit routes.
  • Click Add route. Enter 0.0.0.0/0 for Destination and select the NAT Gateway for Target.
  • Click Save changes.
  • Click the Subnet associations tab, then click Edit subnet associations.
  • Select both private-subnet-1a and private-subnet-1b, then click Save associations.

NAT Gateway still showing Pending?

NAT Gateways typically take 1-2 minutes to provision. If it stays in Pending for more than 5 minutes, check that the Elastic IP was allocated successfully and that public-subnet-1a has auto-assign public IP enabled.

Help me troubleshoot my NAT Gateway stuck in Pending state.

Create security groups for defense-in-depth

Security Groups act as virtual firewalls for your resources. You will create three groups that form a chain: the load balancer accepts traffic from the internet, passes it to web servers, which then talk to the database. Each layer only accepts traffic from the layer before it.

Security Groups vs NACLs

Security Groups are stateful. If you allow inbound traffic on port 80, the return traffic is automatically allowed. Network ACLs (NACLs) are stateless, meaning you must explicitly allow both inbound and outbound. For the SAA exam, remember that Security Groups operate at the instance level and NACLs operate at the subnet level.

  • In the VPC console left navigation under Security, click Security groups.
  • Click Create security group.
  • Enter alb-sg for Security group name and add a description like Allow HTTP from internet.
  • For VPC, select saa-project-vpc.
  • Under Inbound rules, click Add rule. Set Type to HTTP, which auto-fills port 80. Set Source to 0.0.0.0/0 (anywhere).
  • Click Create security group.

Now create the web server security group that only accepts traffic from the ALB.

  • Click Create security group again.
  • Enter web-sg for Security group name and add a description like Allow HTTP from ALB only.
  • For VPC, select saa-project-vpc.
  • Under Inbound rules, click Add rule. Set Type to HTTP (port 80). For Source, start typing alb-sg and select the alb-sg security group from the dropdown.
  • Click Create security group.

Finally, create the database security group that only accepts traffic from the web servers.

  • Click Create security group once more.
  • Enter db-sg for Security group name and add a description like Allow MySQL from web servers only.
  • For VPC, select saa-project-vpc.
  • Under Inbound rules, click Add rule. Set Type to MYSQL/Aurora (port 3306). For Source, start typing web-sg and select the web-sg security group from the dropdown.
  • Click Create security group.

What does this chain of security groups accomplish?

Each security group references the one before it as its source. The ALB accepts HTTP from anywhere. Web servers only accept HTTP from the ALB. The database only accepts MySQL connections from web servers. An attacker who breaches one layer still cannot reach the layers behind it directly. This pattern is called defense-in-depth.

Can't find alb-sg when creating web-sg?

Make sure you selected saa-project-vpc as the VPC for the new security group. Security groups from other VPCs will not appear in the source dropdown. Also confirm you created alb-sg in saa-project-vpc (not the default VPC).

Help me fix my security group source reference.

Your VPC foundation is complete. You have a production-style network with public and private subnets, proper routing, and layered security. Next up, you will launch EC2 instances behind an Auto Scaling Group and load balancer to make this architecture truly resilient.

Launch EC2 with Auto Scaling

Your VPC network is ready with secure traffic paths and defense-in-depth security groups. Now it's time to deploy the compute layer that actually runs your web application.

In this step, you'll create a Launch Template that defines how your EC2 instances are configured, put an Application Load Balancer in front of them, and wire up an Auto Scaling Group that automatically adjusts capacity based on demand.

In this step, get ready to:
  • Create a Launch Template with a User Data script that boots a web server.
  • Deploy an Application Load Balancer with a Target Group.
  • Create an Auto Scaling Group that maintains two healthy instances across Availability Zones.
Create a Launch Template

A Launch Template stores the configuration for your EC2 instances so you don't have to re-enter it every time you launch one. The Auto Scaling Group uses this template to know exactly how to spin up new instances.

  • In the AWS Console, navigate to EC2 using the search bar at the top.
  • In the left navigation pane, under Instances, click Launch Templates.
  • Click Create launch template.
  • For Launch template name, enter saa-web-lt.
  • Under Application and OS Images (Amazon Machine Image), click Quick Start and select Amazon Linux.
  • Choose the Amazon Linux 2023 AMI (the default selection at the top).
  • For Instance type, select t3.micro.
  • For Key pair (login), select Don't include in launch template.

Why no key pair?

You won't SSH directly into these instances. Instead, you'll use AWS Systems Manager (SSM) Session Manager for secure shell access without opening port 22. This is a best practice for production environments and a common SAA exam topic.

  • Under Network settings, for Firewall (security groups), click Select existing security group.
  • From the dropdown, select web-sg (the security group you created in the previous step that allows HTTP from the ALB only).
  • Scroll down to Advanced details and expand that section.
  • Scroll to the very bottom and find the User data text box.
  • Paste the following script into the User data field:
#!/bin/bash
# Update packages and install Apache web server
dnf update -y
dnf install -y httpd

# Start and enable the web server
systemctl start httpd
systemctl enable httpd

# Retrieve instance metadata using IMDSv2 (token-based)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
AZ=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/availability-zone)

# Create a simple web page displaying instance info
cat > /var/www/html/index.html <<EOF
<html>
<body>
<h1>Hello from EC2!</h1>
<p>Instance ID: $INSTANCE_ID</p>
<p>Availability Zone: $AZ</p>
</body>
</html>
EOF

What does this User Data script do?

This script runs automatically when each EC2 instance first boots. It installs Apache (httpd), starts the web server, then queries the instance metadata service to discover its own Instance ID and Availability Zone.

The script uses IMDSv2 (Instance Metadata Service version 2), which requires a session token for security. Amazon Linux 2023 enforces IMDSv2 by default to prevent SSRF attacks.

Finally, it writes a simple HTML page so you can verify which instance is responding when you hit the load balancer.

  • Click Create launch template.
  • You should see a success message confirming that saa-web-lt was created.

Don't see your security group in the dropdown?

Make sure the VPC shown in the Network settings section matches saa-project-vpc. Security groups are VPC-specific, so if the wrong VPC is selected, web-sg won't appear.

Ask NextWork: Why can't I find my web-sg security group in the launch template?

Create an Application Load Balancer and Target Group

An Application Load Balancer distributes incoming HTTP requests across multiple EC2 instances. It sits in your public subnets and routes traffic to instances in your private subnets through the Target Group.

  • In the EC2 left navigation pane, scroll down to Load Balancing and click Load Balancers.
  • Click Create load balancer.
  • Under Application Load Balancer, click Create.
  • For Load balancer name, enter saa-alb.
  • Leave Scheme set to Internet-facing.
  • Under Network mapping, select saa-project-vpc from the VPC dropdown.
  • Check the boxes for both us-east-1a and us-east-1b Availability Zones.
  • For each AZ, select the corresponding public subnet: public-subnet-1a for us-east-1a, and public-subnet-1b for us-east-1b.
  • Under Security groups, remove the default security group and select alb-sg.

Why does the ALB go in public subnets?

The ALB needs to be reachable from the internet so users can access your application. Your EC2 instances stay in private subnets where they can't be directly accessed. Traffic flows from the internet through the ALB, which forwards only to healthy targets registered in the Target Group.

Now you need to create a Target Group for the ALB to forward traffic to.

  • Under Listeners and routing, find the Default action row for the HTTP:80 listener.
  • Click the Create target group link (this opens a new tab).
  • For Target type, keep Instances selected.
  • For Target group name, enter saa-web-tg.
  • Leave Protocol as HTTP and Port as 80.
  • Select saa-project-vpc as the VPC.
  • Under Health checks, confirm the health check path is set to /.
  • Click Next.
  • Skip registering targets (the Auto Scaling Group will handle this automatically).
  • Click Create target group.
  • Switch back to the load balancer creation tab in your browser.
  • Click the refresh icon next to the Default action dropdown, then select saa-web-tg.
  • Click Create load balancer.
  • You should see a success message confirming saa-alb was created. The ALB state will show as Provisioning for a few minutes before transitioning to Active.

What is a Target Group?

A Target Group is a collection of compute resources (EC2 instances, in this case) that the ALB routes traffic to. The health check path / means the ALB pings the root URL of each instance every few seconds. If an instance stops responding, the ALB stops sending it traffic.

Can't find saa-web-tg in the listener dropdown?

Make sure you clicked the refresh icon (circular arrow) next to the Forward to dropdown after creating the target group. The target group was created in a separate tab, so the ALB creation page needs to reload the list.

Ask NextWork: I created my target group but it's not showing in the ALB listener dropdown.

Create an Auto Scaling Group

An Auto Scaling Group automatically launches and terminates EC2 instances based on demand. It uses your Launch Template to know how to configure each instance and registers them with the Target Group so the ALB can route traffic to them.

  • In the EC2 left navigation pane, scroll down to Auto Scaling and click Auto Scaling Groups.
  • Click Create Auto Scaling group.
  • For Auto Scaling group name, enter saa-web-asg.
  • For Launch template, select saa-web-lt.
  • Click Next.
  • Under Network, select saa-project-vpc.
  • For Availability Zones and subnets, select private-subnet-1a and private-subnet-1b.
  • Click Next.

Why private subnets for EC2 instances?

Placing your EC2 instances in private subnets means they have no direct internet exposure. Users reach them only through the ALB (which sits in public subnets). When the instances need to download packages, they route through the NAT Gateway. This is the AWS equivalent of GCP's internal-only VM + Cloud NAT pattern.

  • Under Load balancing, select Attach to an existing load balancer.
  • Select Choose from your load balancer target groups.
  • From the dropdown, select saa-web-tg.
  • Under Health checks, check the box for Turn on Elastic Load Balancing health checks.
  • Click Next.
  • Under Group size, set Desired capacity to 2.
  • Set Min desired capacity to 1.
  • Set Max desired capacity to 4.
  • Under Automatic scaling, select Target tracking scaling policy.
  • Leave the Metric type as Average CPU utilization.
  • Set the Target value to 70.
  • Click Next through the remaining pages (notifications, tags) without changes.
  • On the Review page, confirm your settings and click Create Auto Scaling group.

What does Target Tracking at 70% mean?

Target Tracking tells AWS to automatically add instances when average CPU goes above 70%, and remove instances when it drops below. With min 1 and max 4, you'll always have at least one instance running and never more than four. This keeps your application responsive under load while controlling costs.

Now verify that both instances launched and registered as healthy.

  • In the left navigation pane, under Load Balancing, click Target Groups.
  • Click saa-web-tg.
  • Click the Targets tab at the bottom.
  • Wait 2-3 minutes, then click the refresh icon. You should see two targets with a Health status of healthy.

Targets showing unhealthy?

Give the instances 3-5 minutes to fully boot and run the User Data script. The status will initially show initial before transitioning to healthy.

If targets remain unhealthy after 5 minutes, check that web-sg allows inbound HTTP on port 80 from alb-sg. Also verify that the private subnets have a route to the NAT Gateway (the instances need internet access to download httpd during boot).

Ask NextWork: My target group targets are showing unhealthy. How do I debug this?

Your compute layer is live. Two EC2 instances are running behind the ALB, automatically scaling with demand. Next up, you'll deploy an RDS database in the private subnets with Multi-AZ failover for resilience.

Deploy RDS Multi-AZ Database

Your compute tier is up and running. Two healthy EC2 instances behind an Application Load Balancer can serve traffic across both Availability Zones.

Now you need a data tier that matches that resilience. In this step, you will deploy an RDS MySQL database with Multi-AZ enabled. Multi-AZ creates a synchronous standby replica in a different AZ so that if the primary fails, AWS promotes the standby automatically.

In this step, get ready to:
  • Create a DB Subnet Group that tells RDS which private subnets to use.
  • Launch a Multi-AZ RDS MySQL database in your VPC.
  • Verify database connectivity from an EC2 instance using Session Manager.
Create a DB Subnet Group

A DB Subnet Group tells RDS which subnets inside your VPC are available for placing database instances. It must span at least two Availability Zones so RDS can place the primary and standby in different AZs.

  • Return to the AWS Management Console and search for RDS in the top search bar.
  • Open the Amazon RDS service.
  • In the left navigation pane, click Subnet groups.
  • Click Create DB Subnet Group.
  • For Name, enter saa-db-subnets.
  • For Description, enter Private subnets for RDS.
  • For VPC, select saa-project-vpc.
  • In the Add subnets section, select Availability Zones us-east-1a and us-east-1b.
  • For Subnets, select the two private subnets: 10.0.3.0/24 (private-subnet-1a) and 10.0.4.0/24 (private-subnet-1b).
  • Click Create.

Your new DB subnet group appears in the list. Click it to verify both private subnets are associated.

What is a DB Subnet Group?

A DB Subnet Group is a collection of subnets (typically private) designated for your RDS instances. RDS picks one subnet for the primary instance and, in Multi-AZ mode, another subnet in a different AZ for the standby.

If you are familiar with GCP, this is analogous to choosing the VPC network and region for a Cloud SQL HA instance. The key AWS difference is that subnets are AZ-specific, so you must explicitly name which AZs are eligible.

Don't see your VPC in the dropdown?

Make sure you are still in the us-east-1 region. The region selector is in the top-right corner of the console. If you switched regions accidentally, your VPC will not appear.

Why can't I see my VPC in the RDS dropdown?

Launch the RDS MySQL Multi-AZ database

With the subnet group ready, you can now create the database itself. RDS Multi-AZ will provision a primary instance and a synchronous standby in a different AZ.

  • In the RDS left navigation pane, click Databases.
  • Click Create database.
  • Select Standard create.
  • For Engine type, choose MySQL.
  • In the Templates section, select Dev/Test (this unlocks Multi-AZ options at a lower default instance size).
  • In the Availability and durability section, select Multi-AZ DB instance (one standby).
  • In the Settings section, set DB instance identifier to saa-project-db.
  • Set Master username to admin.
  • Enter a Master password and confirm it. Choose something memorable for later verification.
  • In Instance configuration, select Burstable classes and choose db.t3.micro.
  • In Storage, set Allocated storage to 20 GiB.
  • For Storage type, select gp3.
  • In the Connectivity section, for Virtual Private Cloud (VPC), select saa-project-vpc.
  • For DB subnet group, select saa-db-subnets.
  • For Public access, select No.
  • For VPC security group (firewall), select Choose existing.
  • Select db-sg from the list. Remove any default security group if one is pre-selected.
  • Leave other settings at their defaults and click Create database.

Why Multi-AZ?

Multi-AZ creates a synchronous standby in a different AZ. If the primary fails, RDS promotes the standby automatically with minimal downtime. The standby is not readable. It exists solely for failover.

This costs roughly 2x a Single-AZ instance (approximately $0.034/hr for db.t3.micro Multi-AZ) but is one of the most heavily tested topics on the SAA-C03 exam under Domain 2: Design Resilient Architectures.

Your database will show a status of Creating. This takes 10-20 minutes. You can continue to the next task while it provisions.

Use the wait time wisely

While the database is creating, note the Connectivity & security tab label. Once the status changes to Available, you will find the database endpoint there. Keep this browser tab open.

Create database button greyed out?

Double-check that you selected a VPC, a DB subnet group, and a security group in the Connectivity section. All three are required before RDS will allow you to proceed.

Help me fix the greyed out Create database button.

Verify database connectivity from an EC2 instance

The final proof that your security groups and subnet routing are configured correctly is connecting to the database from one of your EC2 instances. Because you chose "no key pair" for your instances, you will use Session Manager to get a shell without SSH.

  • Wait for the RDS instance status to change to Available. This can take 10-20 minutes.
  • Click the saa-project-db database name to open its details.
  • In the Connectivity & security tab, locate and copy the Endpoint value. It looks like saa-project-db.xxxxxxxxxxxx.us-east-1.rds.amazonaws.com.
  • Open the EC2 console by searching for EC2 in the top search bar.
  • In the left navigation pane, click Instances.
  • Select one of the running instances from your Auto Scaling Group.
  • Click Connect.
  • Select the Session Manager tab.
  • Click Connect.

What is Session Manager?

Session Manager is a fully managed AWS Systems Manager capability. It gives you a browser-based shell on your EC2 instances without needing SSH keys, open inbound ports, or a bastion host.

Amazon Linux 2023 instances come with the SSM Agent pre-installed, so your instances are ready to connect immediately.

  • Install the MySQL client by running this command in the Session Manager shell:
sudo dnf install mariadb105 -y
  • Connect to your RDS instance by running the following command, replacing the endpoint with the one you copied:
mysql -h [[RDS_ENDPOINT="saa-project-db.xxxxxxxxxxxx.us-east-1.rds.amazonaws.com"]] -u admin -p
  • When prompted, enter the master password you set during database creation.
  • You should see the MySQL welcome prompt. Type exit to disconnect.

A successful connection proves that your security groups and subnet routing work correctly. Traffic flows from the EC2 instance to the RDS instance because db-sg allows inbound MySQL port 3306 from web-sg.

Session Manager Connect button greyed out?

Your EC2 instances need an IAM instance profile with the AmazonSSMManagedInstanceCore policy attached. If your Launch Template did not assign an IAM role, the SSM Agent cannot register the instance.

Open Systems Manager in a new tab, then click Quick Setup in the left pane. Create a Host Management configuration to attach the required instance profile. Wait 2-3 minutes, then retry.

Help me connect to my EC2 instance via Session Manager.

MySQL connection timed out?

A timeout means traffic is not reaching the database. Check these two things:

  • Confirm db-sg has an inbound rule allowing port 3306 from web-sg. The source must reference the security group ID, not a CIDR block.
  • Verify the RDS instance is in the same VPC (saa-project-vpc) as your EC2 instances.

Help me debug my MySQL connection timeout.

Your data tier is complete. You have a MySQL database running across two Availability Zones with automatic failover, securely tucked inside private subnets where only your web servers can reach it.

Next up, you will add an S3 bucket and CloudFront distribution to serve static assets with global edge caching.

Configure S3 and CloudFront

Your compute and database layers are deployed and healthy. Now you need a way to serve static assets (HTML, images, CSS) to users with low latency across the globe.

In this step, you will create an S3 bucket for static content, front it with a CloudFront CDN distribution, and lock down direct access so content is only served through CloudFront. This covers SAA-C03 Domain 3: High-Performing Architectures.

In this step, get ready to:
  • Create an S3 bucket with static website hosting enabled.
  • Create a CloudFront distribution using Origin Access Control (OAC).
  • Verify CloudFront serves content while direct S3 access is blocked.
Create an S3 bucket and enable static website hosting

S3 buckets store objects (files) in the cloud. By enabling static website hosting, you tell S3 to serve an HTML page when visitors request the bucket's root URL.

  • In the AWS Management Console, type S3 in the top search bar and select S3.
  • Click Create bucket.
  • For Bucket name, enter saa-project-assets- followed by your 12-digit AWS account ID (e.g. saa-project-assets-123456789012). Bucket names must be globally unique, so appending your account ID guarantees uniqueness.
  • Confirm the AWS Region is set to US East (N. Virginia) us-east-1.
  • Leave all other settings at their defaults and click Create bucket.

Why append your account ID?

S3 bucket names are globally unique across all AWS accounts. Using your account ID as a suffix ensures no naming collision with other AWS users.

Now upload a sample HTML file so the bucket has content to serve.

  • On your local computer, create a file named index.html with the following content:
<!DOCTYPE html>
<html>
<head>
    <title>SAA Project</title>
</head>
<body>
    <h1>Hello from S3 and CloudFront!</h1>
    <p>This page is served via Amazon CloudFront CDN.</p>
</body>
</html>
  • Back in the S3 console, click your bucket name (saa-project-assets-<your-account-id>) to open it.
  • Click Upload.
  • Click Add files and select the index.html file you just created.
  • Click Upload at the bottom of the page.
  • Once the upload completes, click Close.

Next, enable static website hosting on this bucket.

  • Click the Properties tab on your bucket's page.
  • Scroll down to Static website hosting and click Edit.
  • Select Enable.
  • For Index document, enter index.html.
  • Click Save changes.

What is static website hosting?

When you enable static website hosting, S3 generates a website endpoint for your bucket. Requests to that endpoint return your index.html file by default. You will NOT use this endpoint directly because CloudFront will serve the content instead.

Don't see Static website hosting?

Make sure you are on the Properties tab (not Permissions or Objects). The Static website hosting section is near the bottom of the page.

Help me find the Static website hosting option.

Create a CloudFront distribution with Origin Access Control

CloudFront is a Content Delivery Network (CDN) that caches your content at edge locations worldwide. Origin Access Control (OAC) ensures that only CloudFront can read objects from your S3 bucket. Direct S3 URLs will return a 403 Forbidden error.

  • In the top search bar, type CloudFront and select CloudFront.
  • Click Create distribution.
  • For Origin domain, click the dropdown and select your S3 bucket (saa-project-assets-<your-account-id>.s3.us-east-1.amazonaws.com).
  • Under Origin access, select Origin access control settings (recommended).
  • Click Create new OAC.
  • In the dialog that appears, leave the default name and Sign requests (recommended) setting. Click Create.
  • Scroll down to the Default root object field and enter index.html.
  • Leave all other settings at their defaults.
  • Scroll to the bottom and click Create distribution.

What is Origin Access Control?

OAC tells CloudFront to sign every request it sends to your S3 bucket using AWS Signature Version 4 (SigV4). Because the bucket policy only allows signed requests from your specific distribution, nobody can bypass CloudFront and access S3 directly.

This is a common SAA-C03 exam topic. Security Groups protect EC2 instances. OAC protects S3 origins behind CloudFront.

After creating the distribution, a yellow banner appears at the top of the page prompting you to update the S3 bucket policy. You need to apply this policy so CloudFront can access your bucket.

  • Click Copy policy in the yellow banner at the top of the distribution page.
  • Open the S3 console in a new tab (or navigate back to S3).
  • Click your bucket name to open it.
  • Click the Permissions tab.
  • Under Bucket policy, click Edit.
  • Paste the policy you copied from CloudFront into the Policy editor, replacing any existing content.
  • Click Save changes.

What does this bucket policy do?

The policy grants the cloudfront.amazonaws.com service principal permission to call s3:GetObject on your bucket. A condition restricts this permission to only your specific CloudFront distribution ARN. No other principal can read objects.

Don't see the yellow banner?

If you navigated away from the distribution page, go back to CloudFront, click your distribution ID, then click the Origins tab. Select your S3 origin, click Edit, and look for the Copy policy button under the Origin access control section.

Help me find the bucket policy for my CloudFront OAC.

Verify CloudFront serves content and direct S3 access is blocked

CloudFront distributions take 2-5 minutes to deploy globally. Once deployed, you will confirm that CloudFront serves your content while direct S3 access is denied.

  • Navigate back to the CloudFront console and click Distributions in the left navigation.
  • Wait until the Last modified column changes from Deploying to a date and time. This confirms the distribution is live.
  • Copy the Domain name value (it looks like d1234abcdef.cloudfront.net).
  • Open a new browser tab and paste the CloudFront domain name. You should see your "Hello from S3 and CloudFront!" page.

Now verify that direct S3 access is blocked.

  • Go back to the S3 console, open your bucket, and click on index.html.
  • Copy the Object URL shown on the object detail page.
  • Paste it into a new browser tab. You should see a 403 Forbidden (AccessDenied) response.

Why does direct S3 access return 403?

Your bucket policy only allows the CloudFront service principal to read objects. Any other request (including a browser going directly to the S3 URL) is denied. This is exactly what OAC is designed to do.

On the SAA-C03 exam, remember: CloudFront is the AWS equivalent of GCP Cloud CDN. OAC is the recommended way to restrict S3 origin access (replacing the older Origin Access Identity, or OAI).

Still seeing an error on the CloudFront URL?

If you see a 403 error on the CloudFront domain, the bucket policy may not be applied correctly. Go to your S3 bucket's Permissions tab, check the Bucket policy section, and confirm the policy contains your distribution's ARN.

Also confirm the distribution has finished deploying (the Last modified column should show a timestamp, not "Deploying").

Help me troubleshoot my CloudFront 403 error.

Your static content is now served globally through CloudFront edge locations, with the S3 origin locked down via OAC. Next up, you will add a Lambda function that processes S3 upload events automatically.

Add Lambda and Event-Driven Processing

Your S3 bucket is live and serving content through CloudFront. Now you'll add serverless compute that reacts to file uploads automatically.

In this step, you'll create a Lambda function that runs every time someone uploads a file to your S3 bucket. There are no servers to manage, no capacity to plan, and no idle costs. S3 fires an event, Lambda runs your code, and you only pay for the milliseconds of execution.

In this step, get ready to:
  • Create a Lambda function with Python 3.12 runtime.
  • Connect an S3 event trigger to invoke the function on file uploads.
  • Verify the end-to-end pipeline by uploading a test file and checking CloudWatch Logs.
Create the Lambda function

Lambda lets you run code without provisioning or managing servers. You define a function, upload your code, and AWS handles the rest. If you're familiar with GCP, Lambda is the AWS equivalent of Cloud Functions.

  • In the AWS Console search bar, type Lambda and select the Lambda service.
  • Click Create function.
  • Select Author from scratch.
  • For Function name, enter saa-s3-processor.
  • For Runtime, choose Python 3.12.
  • Leave Architecture set to x86_64.
  • Under Permissions, leave the default option (Create a new role with basic Lambda permissions). Lambda creates an execution role that can write to CloudWatch Logs.
  • Click Create function.

Lambda creates a default file named lambda_function.py with placeholder code. You'll replace it with your own.

  • In the Code source pane, double-click lambda_function.py to open it in the editor.
  • Select all the existing code and replace it with the following:
import json

def lambda_handler(event, context):
    # Loop through each record in the S3 event
    for record in event['Records']:
        # Extract the bucket name from the event
        bucket = record['s3']['bucket']['name']
        # Extract the object key (file path) from the event
        key = record['s3']['object']['key']
        # Extract the file size in bytes
        size = record['s3']['object']['size']

        print(f"New file uploaded to {bucket}")
        print(f"Object key: {key}")
        print(f"File size: {size} bytes")

    return {
        'statusCode': 200,
        'body': json.dumps('File processed successfully')
    }

What does this code do?

When S3 triggers this function, it passes an event containing details about the uploaded object. The code loops through each record in that event and prints the bucket name, object key (the file's path), and file size.

In a real production system, this is where you'd add logic like generating thumbnails, scanning for malware, or indexing metadata. For this project, logging the details is enough to prove the pipeline works.

  • In the Code source pane, click Deploy to save your function code.
  • You should see a green banner confirming the function was updated successfully.

Don't see the Deploy button?

Make sure you're on the Code tab of your function page. The Deploy button appears in the Code source pane above the editor.

Help me find the Deploy button in the Lambda console

Add an S3 trigger to the function

Your function exists, but nothing invokes it yet. You need to connect an event source so that S3 notifies Lambda every time a new object appears in a specific prefix.

  • On your saa-s3-processor function page, scroll up to the Function overview pane.
  • Click Add trigger.
  • In the trigger configuration dropdown, select S3.
  • Under Bucket, select your bucket (saa-project-assets-<your-account-id>).
  • Under Event types, confirm that All object create events is selected.
  • Under Prefix, enter uploads/. This ensures only objects uploaded to the uploads/ folder trigger the function.
  • Under Recursive invocation, select the checkbox to acknowledge the warning about using the same bucket for input and output.
  • Click Add.

Why use a prefix filter?

Without a prefix, every single object uploaded to the bucket would trigger your function. That includes the index.html file you uploaded earlier. The uploads/ prefix scopes the trigger to only new files in that specific folder.

This pattern also prevents recursive invocation loops where a function writes an output file to the same bucket and accidentally triggers itself.

You should see the S3 trigger appear in the Function overview diagram as a connected event source.

Seeing a permissions error when adding the trigger?

If you see a message about conflicting event notification configurations, confirm that your bucket doesn't already have a notification for the same prefix and event type. Go to S3, open your bucket, click Properties, scroll to Event notifications, and remove any duplicate configurations.

Help me resolve this permissions error

Test the event-driven pipeline

The trigger is wired. Now you'll upload a file to the uploads/ prefix and confirm that Lambda picks it up automatically.

Why is this event-driven?

Traditional architectures use a polling service that constantly checks S3 for new files. Event-driven architectures flip this: S3 pushes a notification to Lambda only when something happens. There's zero cost when nothing is happening.

This pattern is a growing focus area on the SAA-C03 exam under Domain 3, Task 5 (data ingestion and transformation). The exam tests whether you understand when to use event-driven triggers vs. scheduled polling.

  • In the AWS Console search bar, type S3 and open the S3 service.
  • Click on your bucket (saa-project-assets-<your-account-id>).
  • Click Create folder.
  • Enter uploads as the folder name and click Create folder.
  • Open the uploads/ folder.
  • Click Upload.
  • Click Add files and select any small file from your computer (a text file works fine).
  • Click Upload.

Your upload just triggered the Lambda function. Now verify it ran by checking the logs.

  • In the AWS Console search bar, type CloudWatch and open the CloudWatch service.
  • In the left sidebar, click Logs, then Log groups.
  • Find the log group named /aws/lambda/saa-s3-processor and click on it.
  • Click the most recent log stream (the one at the top).
  • Look for log entries containing your file name, the object key, and the file size in bytes.

What should I see?

You should see log lines like:

  • New file uploaded to saa-project-assets-123456789012
  • Object key: uploads/yourfile.txt
  • File size: 1234 bytes

The exact values depend on your account ID and the file you uploaded. The important thing is that Lambda executed without errors and logged the S3 object details.

Don't see a log group for your function?

The log group is created the first time the function executes. If there's no log group, the trigger might not have fired. Wait 30 seconds and refresh. If it still doesn't appear, go back to your Lambda function, check that the S3 trigger shows as Enabled in the Configuration tab under Triggers, and confirm you uploaded the file into the uploads/ folder (not the bucket root).

Help me troubleshoot my S3 to Lambda trigger

Your serverless event-driven pipeline is working. S3 detects the upload, fires a notification, and Lambda processes it instantly. Next up, you'll bring everything together with monitoring and cost optimization.

Set Up Monitoring and Notifications

You have a full multi-tier architecture running across multiple Availability Zones. But how do you know if something goes wrong at 2 AM?

In this step, you will build an observability layer using CloudWatch. You will create a dashboard for at-a-glance health monitoring and set up alarms that notify you before problems escalate. This maps directly to SAA-C03 Domain 4: Design Cost-Optimized Architectures (20% of the exam).

In this step, get ready to:
  • Create a CloudWatch dashboard with widgets for your ALB, EC2, and RDS metrics.
  • Set up CloudWatch alarms that send SNS notifications when thresholds are breached.
  • Review your architecture from a cost optimization lens (per-hour vs per-use billing).
Create a CloudWatch dashboard

A dashboard gives you a single pane of glass to see the health of your entire architecture. Instead of clicking between individual services, you can see ALB traffic, CPU usage, and database storage all in one view.

  • In the AWS Console search bar, type CloudWatch and select the service.
  • In the left navigation pane, click Dashboards.
  • Click Create dashboard.
  • Enter saa-project-dashboard as the dashboard name.
  • Click Create dashboard.

An Add widget dialog appears. You will add four metric widgets to this dashboard.

  • Select Line as the widget type and click Next.
  • Click Metrics.
  • In the Browse tab, select ApplicationELB, then Per AppELB Metrics.
  • Find saa-alb and select the RequestCount metric. Choose Sum as the statistic.
  • Click Create widget.

Now add the second widget for EC2 CPU utilization.

  • Click the + symbol to add another widget.
  • Select Line and click Next.
  • Click Metrics.
  • Select EC2, then By Auto Scaling Group.
  • Search for saa-web-asg and select the CPUUtilization metric. Keep the statistic as Average.
  • Click Create widget.

Add the third widget for RDS CPU utilization.

  • Click the + symbol to add another widget.
  • Select Line and click Next.
  • Click Metrics.
  • Select RDS, then Per-Database Metrics.
  • Search for saa-project-db and select the CPUUtilization metric.
  • Click Create widget.

Add the fourth and final widget for RDS free storage.

  • Click the + symbol to add another widget.
  • Select Line and click Next.
  • Click Metrics.
  • Select RDS, then Per-Database Metrics.
  • Search for saa-project-db and select the FreeStorageSpace metric.
  • Click Create widget.
  • Click Save dashboard in the top right corner.

What is a CloudWatch dashboard?

CloudWatch dashboards are customizable pages that display metric graphs and alarm statuses for your AWS resources. They are global, meaning they are not restricted to a single region.

In GCP terms, this is the equivalent of a custom monitoring dashboard in Cloud Monitoring. The key difference is that CloudWatch dashboards are explicitly created per-account and shared across regions.

You should now see all four widgets displaying metrics on your dashboard. Some graphs may show minimal data since the architecture was just deployed.

Not seeing your metrics?

CloudWatch metrics can take a few minutes to populate for newly created resources. If a widget shows no data, wait 5 minutes and refresh the page. Make sure you are in the us-east-1 region.

Help me troubleshoot my CloudWatch dashboard showing no data for my ALB or EC2 resources.

Create CloudWatch alarms for CPU and storage

Dashboards show you what is happening right now. Alarms proactively tell you when something crosses a threshold you care about. You will create two alarms that send email notifications via the SNS topic you set up in Step 1.

Start with an alarm on your Auto Scaling Group's CPU utilization.

  • In the CloudWatch navigation pane, click Alarms, then All alarms.
  • Click Create alarm.
  • Click Select metric.
  • On the All metrics tab, select EC2, then By Auto Scaling Group.
  • Search for saa-web-asg and select the CPUUtilization metric.
  • Click Select metric.

Now configure the alarm conditions.

  • Under Statistic, confirm Average is selected.
  • Set the Period to 5 minutes.
  • Under Conditions, for Threshold type, select Static.
  • For Whenever CPUUtilization is, choose Greater than the threshold.
  • Enter 80 as the threshold value.
  • Click Additional configuration. For Datapoints to alarm, enter 2 out of 2. This means two consecutive 5-minute periods must breach the threshold.
  • Click Next.

Configure the notification action.

  • Under Notification, select In alarm.
  • For Send a notification to, select the SNS topic you created in Step 1 (the same one used for your billing alarm).
  • Click Next.
  • Enter a descriptive alarm name, such as saa-asg-cpu-high.
  • Click Next, review the settings, then click Create alarm.

Why 80% for 2 consecutive periods?

Setting a threshold at 80% with 2 consecutive 5-minute periods means the alarm only fires after 10 minutes of sustained high CPU. This prevents false alarms from brief traffic spikes.

Your ASG already has a Target Tracking policy at 70% that will scale out automatically. This 80% alarm serves as a warning that your architecture is under heavy load and may need human attention.

Now create the second alarm for RDS storage.

  • Click Create alarm again.
  • Click Select metric.
  • Select RDS, then Per-Database Metrics.
  • Search for saa-project-db and select the FreeStorageSpace metric.
  • Click Select metric.
  • Under Statistic, select Average.
  • Set the Period to 5 minutes.
  • Under Conditions, for Threshold type, select Static.
  • For Whenever FreeStorageSpace is, choose Lower than the threshold.
  • Enter 5368709120 as the threshold value (this is 5 GiB in bytes).
  • Click Next.
  • Under Notification, select In alarm and choose the same SNS topic.
  • Click Next.
  • Enter a name such as saa-rds-storage-low.
  • Click Next, review your settings, then click Create alarm.

Why monitor FreeStorageSpace?

RDS stores the FreeStorageSpace metric in bytes. When your database runs out of storage, writes fail and your application breaks. Alerting at 5 GiB gives you time to increase allocated storage before hitting the limit.

On the SAA exam, expect questions about which CloudWatch metric to monitor for database storage. FreeStorageSpace is the correct metric for RDS.

You should now see your two new alarms listed alongside the billing alarm you created earlier. All three alarms will show a status of Insufficient data initially, because CloudWatch needs time to collect enough data points for evaluation.

Alarm stuck in Insufficient data?

This is normal for newly created alarms. CloudWatch needs at least 2 data points (10 minutes for a 5-minute period alarm) before it can evaluate the alarm state. Wait 10-15 minutes and the alarm should transition to OK.

Help me understand why my CloudWatch alarm is stuck in Insufficient data state.

Review your architecture from a cost optimization perspective

Before wrapping up, take a moment to understand the cost profile of the architecture you built. This mental model is critical for SAA exam questions on cost optimization.

Your resources fall into two billing categories.

Per-hour vs per-use billing

Resources that charge per-hour (regardless of traffic):

  • NAT Gateway: $0.045 per hour + $0.045 per GB of data processed.
  • Application Load Balancer: $0.0225 per hour + LCU charges.
  • RDS Multi-AZ (db.t3.micro): approximately $0.034 per hour.
  • EC2 instances (2x t3.micro): $0.0104 per hour each.

Resources that charge per-use (only when invoked or accessed):

  • Lambda: free tier of 1M requests/month and 400,000 GB-seconds.
  • S3: $0.023 per GB-month of storage.
  • CloudFront: charges for data transfer out to viewers.
  • Data transfer: charges apply for traffic leaving AWS to the internet.

The total per-hour cost of this architecture is approximately $0.12/hour. If you tear down today, your total cost stays well under $10.

Why does this matter for the SAA exam?

Domain 4 questions often ask you to identify the most cost-effective architecture or reduce costs for a given workload. The key insight is that per-hour resources (NAT Gateway, ALB, RDS) drive your baseline bill whether you have 0 or 1000 requests.

Per-use services like Lambda and S3 scale to zero cost when idle. The SAA exam frequently tests this distinction in scenarios like choosing between ALB + EC2 vs API Gateway + Lambda for low-traffic workloads.

Secret mission

Add a VPC Gateway Endpoint for S3

Your private EC2 instances access S3 through the NAT Gateway, which charges $0.045 per GB of data processed. In this secret mission, you'll eliminate that cost entirely by creating a free VPC Gateway Endpoint for S3, then prove the traffic rerouted using VPC Flow Logs.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses multiple per-hour billed resources, so leaving them running will accumulate charges.

Cost warning

Your architecture costs approximately $0.12/hr (~$2.90/day) while running. The biggest per-hour charges come from the NAT Gateway ($0.045/hr), RDS Multi-AZ ($0.034/hr), and ALB ($0.0225/hr).

If you are done experimenting, delete all resources now to avoid unexpected charges. Even overnight, you could accumulate $1-2 in unnecessary costs.

Resources you used:

  • VPC (saa-project-vpc) with 4 subnets, route tables, Internet Gateway, and security groups.
  • NAT Gateway with Elastic IP.
  • Application Load Balancer (saa-alb) and Target Group (saa-web-tg).
  • Auto Scaling Group (saa-web-asg) with Launch Template (saa-web-lt).
  • RDS Multi-AZ instance (saa-project-db) and DB Subnet Group (saa-db-subnets).
  • S3 bucket (saa-project-assets-<account-id>).
  • CloudFront distribution.
  • Lambda function (saa-s3-processor) and its IAM execution role.
  • VPC Gateway Endpoint for S3.
  • VPC Flow Logs and CloudWatch Log Group.
  • CloudWatch dashboard (saa-project-dashboard) and alarms.
  • SNS topic and email subscription.

Keep everything running

No action needed. Choose this if you are still actively experimenting with your architecture or want to test additional configurations.

  • Your resources will continue to cost approximately $0.12/hr (~$2.90/day).
  • Your billing alarm at $10 will notify you if charges approach that threshold.
  • Come back to the Delete tab when you are finished.

Pause - I'll come back to this later

Reduce costs by stopping compute resources while keeping your network infrastructure intact. This does not eliminate all charges, but significantly reduces the hourly rate.

  • Scale the Auto Scaling Group to zero: navigate to EC2 > Auto Scaling Groups > select saa-web-asg > Edit > set desired, min, and max all to 0.
  • Stop the RDS instance: navigate to RDS > Databases > select saa-project-db > Actions > Stop temporarily. The instance will auto-restart after 7 days.
  • NAT Gateway ($0.045/hr) and ALB ($0.0225/hr) cannot be paused. They continue to charge per hour even with no traffic.
  • Remaining hourly cost while paused: approximately $0.07/hr (~$1.60/day) from the NAT Gateway and ALB alone.

Why not just stop the EC2 instances directly?

The Auto Scaling Group would detect stopped instances and launch replacements. Setting the ASG capacity to zero is the correct way to stop compute without triggering replacements.

Delete - I don't want to use this again

Remove all project resources completely. Follow this order to avoid dependency errors.

1. Disable and delete the CloudFront distribution:

  • Navigate to CloudFront > Distributions.
  • Select your distribution, click Disable, and wait until the status changes to Disabled (2-5 minutes).
  • Once disabled, select it again and click Delete.

2. Delete the Auto Scaling Group (this terminates EC2 instances):

  • Navigate to EC2 > Auto Scaling Groups.
  • Select saa-web-asg, click Delete, and confirm.
  • Wait for the instances to terminate (check under EC2 > Instances).

3. Delete the ALB and Target Group:

  • Navigate to EC2 > Load Balancers.
  • Select saa-alb, click Actions > Delete load balancer, and confirm.
  • Navigate to EC2 > Target Groups, select saa-web-tg, click Actions > Delete.

4. Delete the RDS instance:

  • Navigate to RDS > Databases.
  • Select saa-project-db, click Actions > Delete.
  • Uncheck Create final snapshot to avoid storage costs for the snapshot.
  • Check the acknowledgement box and confirm deletion. This takes a few minutes.
  • After the instance is deleted, navigate to RDS > Subnet groups, select saa-db-subnets, and delete it.

5. Delete the NAT Gateway and release the Elastic IP:

  • Navigate to VPC > NAT gateways.
  • Select your NAT Gateway, click Actions > Delete NAT gateway, type delete to confirm.
  • Wait for the state to change to Deleted (1-2 minutes).
  • Navigate to VPC > Elastic IP addresses, select the released IP, click Actions > Release Elastic IP address.

6. Delete the Lambda function:

  • Navigate to Lambda > Functions.
  • Select saa-s3-processor, click Actions > Delete, and confirm.

7. Empty and delete the S3 bucket:

  • Navigate to S3 > Buckets.
  • Select your bucket, click Empty, type permanently delete to confirm.
  • After emptying, select the bucket again and click Delete, type the bucket name to confirm.

8. Delete the VPC (this removes subnets, route tables, security groups, Internet Gateway, and the S3 Gateway Endpoint):

  • Navigate to VPC > Your VPCs.
  • Select saa-project-vpc, click Actions > Delete VPC.
  • The console will list all resources being deleted (subnets, route tables, security groups, Internet Gateway, Gateway Endpoint). Confirm the deletion.

9. Delete CloudWatch resources:

  • Navigate to CloudWatch > Dashboards, select saa-project-dashboard, and click Delete.
  • Navigate to CloudWatch > Alarms > All alarms. Select all three alarms (ASG CPU, RDS Storage, Billing), click Actions > Delete.
  • Navigate to CloudWatch > Logs > Log groups. Delete the log groups for VPC Flow Logs and the Lambda function.

10. Delete the SNS topic:

  • Navigate to SNS > Topics.
  • Select your topic, click Delete, and confirm.

11. Clean up IAM roles:

  • Navigate to IAM > Roles.
  • Search for the Lambda execution role (it starts with saa-s3-processor-role), select it, and click Delete.
  • Also delete the Launch Template: navigate to EC2 > Launch Templates, select saa-web-lt, click Actions > Delete template.

Verify everything is gone

After completing all steps, check your billing dashboard in 24 hours. Navigate to Billing and Cost Management > Bills and confirm no unexpected charges are accumulating.

That's a Wrap!

That's a Wrap!

You just built a production-grade multi-tier architecture on AWS spanning all four SAA-C03 exam domains!

You now have hands-on muscle memory for 15+ core services connected in a real-world pattern.

What you accomplished:

  • Designed a secure VPC foundation with public and private subnets across two Availability Zones, Security Groups implementing defense-in-depth, and IAM roles with least-privilege access.
  • Built a resilient multi-tier architecture with an Auto Scaling Group behind an Application Load Balancer, plus an RDS Multi-AZ database with automatic failover.
  • Configured high-performing content delivery with a CloudFront CDN distribution fronting S3, event-driven Lambda processing on uploads, and CloudWatch monitoring with SNS notifications.
  • Secret Mission: Added a VPC Gateway Endpoint for S3 to eliminate NAT Gateway data processing charges, then verified traffic routing with VPC Flow Logs.

Ready to quiz yourself?