Build a Highly Available Claims Portal

Deploy a multi-tier insurance app on AWS with ALB, Auto Scaling, and RDS.

Introduction

30 Second Summary

When natural disasters strike, insurance companies get flooded with claims all at once. If their systems go down at that exact moment, policyholders are left stranded.

In this project, you will design and deploy a professional Insurance Claims Portal on AWS that stays available even when servers fail. You'll build the kind of multi-tier architecture that companies rely on for business-critical workloads.

What You'll Build

A Node.js/Express web application running behind an Application Load Balancer with Auto Scaling across two Availability Zones, backed by an RDS MySQL database in isolated private subnets.

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

  • A custom VPC with public and private subnets spanning two Availability Zones for fault tolerance.
  • An Application Load Balancer and Auto Scaling Group that keep your portal online even if a server fails.
  • A managed RDS MySQL database isolated in private subnets with security group chaining for defense-in-depth.
  • Secret Mission: Add CloudWatch monitoring with SNS alerts that notify you when your infrastructure scales up or down.

Are there any prerequisites?

You'll need a macOS virtual machine running in UTM with internet access, a web browser, and a credit or debit card for creating an AWS account. AWS Free Tier credits cover all costs for this project.

Create Your AWS Account and Install the AWS CLI

To build a highly available insurance claims portal, you need a place to host it. AWS provides the cloud infrastructure you need: load balancers, auto-scaling servers, and managed databases.

In this step, you'll create your AWS account, set up an IAM user for programmatic access, and install the AWS CLI so you can manage resources from your terminal.

In this step, get ready to:
  • Create an AWS account and choose the Paid plan.
  • Create an IAM user with AdministratorAccess and generate access keys.
  • Install and configure the AWS CLI on your macOS VM.
Create your AWS account

AWS offers two plans at sign-up: Free and Paid. The Free plan limits which services you can access. Since this project uses services like NAT Gateways that the Free plan restricts, you need the Paid plan.

Will the Paid plan cost me money?

New accounts created after July 15, 2025 receive $100 in credits at sign-up. The Paid plan gives you access to all AWS services, and your Free Tier credits automatically apply to any charges.

You will need a credit or debit card to create the account. AWS uses it to verify your identity, but you will not be charged beyond your credits as long as you clean up resources at the end.

  • Open your web browser and navigate to https://aws.amazon.com/free/.
  • Click Create a Free Account.
  • Enter your email address in the Root user email address field.
  • Enter an account name in the AWS account name field (for example, securelife-project).
  • Click Verify email address.
  • Enter the verification code sent to your email inbox.
  • Create a strong root user password.
  • Click Continue.
  • Choose Personal for the account type and fill in your contact information.
  • Enter your credit or debit card details.
  • Complete the phone verification step.
  • When asked to choose a plan, select Paid plan.
  • Select the Basic (Free) support plan.
  • Click Complete sign up.
  • Wait for the activation email (usually a few minutes) and then sign in to the AWS Management Console.

Account activation taking a while?

Activation usually takes a few minutes but can sometimes take up to 24 hours. Check your spam folder for the confirmation email. If you are still waiting after 24 hours, contact AWS Support.

help me troubleshoot my AWS account activation

Create an IAM user with AdministratorAccess

Your root account has unlimited power over your AWS resources. Best practice is to create a separate IAM user for day-to-day work. This user gets the permissions needed to complete the project while keeping your root credentials safe.

  • In the AWS Management Console, search for IAM in the top search bar.
  • Select IAM from the results.
  • In the left navigation pane, click Users.
  • Click Create user.
  • Enter cli-admin as the user name.
  • Click Next.
  • On the permissions page, select Attach policies directly.
  • In the search box under Permissions policies, type AdministratorAccess.
  • Select the checkbox next to the AdministratorAccess policy.
  • Click Next.
  • Click Create user.

Now create access keys so the AWS CLI can authenticate as this user.

  • Click the user name cli-admin to open the user details page.
  • Select the Security credentials tab.
  • Scroll down to the Access keys section.
  • Click Create access key.
  • On the Access key best practices & alternatives page, select Other as the use case.
  • Click Next.
  • Click Create access key.
  • Click Download .csv file and save it to a safe location.

Save your credentials now

This is your only opportunity to view or download the secret access key. If you lose it, you will need to delete the access key and create a new one. Keep the downloaded .csv file in a safe location and do not share it.

Why an IAM user instead of root?

The root account can do anything in AWS, including closing the account and changing billing. An IAM user with AdministratorAccess can manage all AWS services but cannot modify account-level settings.

This limits the damage if credentials are ever compromised.

Install and configure the AWS CLI

The AWS CLI lets you manage AWS resources from your terminal. You will use it throughout this project to create VPCs, launch instances, and verify your infrastructure.

  • Open Terminal on your macOS VM by pressing Cmd+Space to open your search bar.
  • Type Terminal and press Enter to open it.
  • Download the AWS CLI installer by running this command:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"

What does this command do?

This uses curl to download the official AWS CLI v2 package installer from Amazon's servers. The -o flag saves it as AWSCLIV2.pkg in your current directory.

  • Install the AWS CLI by running:
sudo installer -pkg AWSCLIV2.pkg -target /

What does this command do?

The macOS installer utility installs the package system-wide. You will be prompted for your macOS password because sudo runs the command with administrator privileges.

  • Verify the installation succeeded by running:
aws --version

You should see output starting with aws-cli/2. followed by a version number and platform details.

✔️ I see aws-cli/2.x.x

The AWS CLI v2 is installed and ready. Continue below to configure it with your credentials.

ⓧ I see aws-cli/1.x.x

You have an older version of the AWS CLI installed. This project requires version 2. Remove the old version and install v2.

  • Check where the old CLI is installed by running:
which aws
  • If it was installed via pip, uninstall it by running:
pip uninstall awscli
  • Then re-run the install commands above (the curl download and sudo installer commands).
  • Close your terminal, open a new one, and run aws --version again.

help me upgrade from AWS CLI v1 to v2 on macOS

ⓧ Command not found

The installation may not have completed successfully. Try these steps:

  • Close your terminal and open a new one (the PATH may not have updated).
  • Run the install command again:
sudo installer -pkg AWSCLIV2.pkg -target /
  • If the file is not found, confirm it was downloaded:
ls AWSCLIV2.pkg
  • If the file does not appear, re-run the curl download command from above.

help me install the AWS CLI on macOS

Now connect the CLI to your IAM user credentials.

  • Configure the AWS CLI with your credentials by running:
aws configure

The CLI will prompt you for four values. Enter each one and press Enter:

  • AWS Access Key ID: paste the Access Key ID from your downloaded .csv file.
  • AWS Secret Access Key: paste the Secret Access Key from your downloaded .csv file.
  • Default region name: enter us-east-1.
  • Default output format: enter json.

Why us-east-1?

The us-east-1 region (N. Virginia) has the broadest service availability and is where most AWS tutorials and documentation examples are based. It also typically receives new features first.

  • Test that the CLI can reach AWS and authenticate as your IAM user by running:
aws sts get-caller-identity

You should see JSON output containing your UserId, Account number, and Arn that includes cli-admin.

What should I see?

The output confirms three things: your 12-digit AWS account number, the unique user ID, and the ARN (Amazon Resource Name) of your IAM user. If you see cli-admin in the Arn field, your CLI is correctly configured and authenticated.

Seeing an authentication error?

Check that you pasted your Access Key ID and Secret Access Key correctly. Open the .csv file you downloaded and compare the values character by character.

If they look wrong, re-run the configure command:

aws configure

Re-enter your credentials carefully and try aws sts get-caller-identity again.

help me fix my AWS CLI authentication error

Your AWS account is live, your IAM user is set up, and your CLI is authenticated. Next up, you will step into the role of a solutions architect and design the architecture for SecureLife Insurance's claims portal.

Design the Architecture

Your AWS account is set up and your CLI is configured. Before you create any resources, you need to design the architecture that will support them.

Real solutions architects never jump straight into building. They start with business requirements, map each one to a specific AWS service, and document the trade-offs. That's exactly what you'll do in this step for SecureLife Insurance's claims portal.

In this step, get ready to:
  • Map business requirements to AWS service choices.
  • Design the three-tier network architecture.
  • Document architectural trade-offs and decisions.
Map business requirements to AWS services

SecureLife Insurance is a mid-size insurer that needs a customer-facing claims portal. Before picking services, you need to understand what the business actually requires.

Here are the four core requirements from the client brief:

  • High availability — the portal must remain available if any single component fails. Customers filing claims after a natural disaster can't afford downtime.
  • Data isolation — customer PII must be network-isolated from public internet access per insurance compliance requirements.
  • Auto-scaling — the portal must scale automatically during peak claims periods, such as after natural disasters when claim volume spikes.
  • Cost efficiency — resources should be right-sized so the company pays only for what it uses.

Now map each requirement to the AWS service that satisfies it:

  • High availability maps to a multi-AZ Application Load Balancer (ALB) paired with an Auto Scaling Group. The ALB distributes traffic across Availability Zones, and the ASG replaces failed instances automatically.
  • Data isolation maps to private subnets inside a VPC combined with Security Groups chained together. The database lives in a subnet that has no route to the internet.
  • Auto-scaling maps to an Auto Scaling Group with scaling policies. The ASG adds EC2 instances when demand rises and removes them when demand drops.
  • Cost efficiency maps to right-sized instances (t3.micro), a single NAT Gateway instead of one per AZ, and Auto Scaling that avoids paying for idle capacity.

Why start with requirements?

Every component you deploy costs money and adds operational complexity. Starting with business requirements forces you to justify each service choice.

This is the same approach tested on the AWS Solutions Architect Associate (SAA-C03) exam. Questions give you a business scenario and ask you to pick the architecture that satisfies the requirements.

Design the three-tier network

The architecture follows a three-tier network model. Each tier serves a specific purpose and only accepts traffic from the tier directly above it.

  • Public tier (ALB) — the Application Load Balancer sits in public subnets. It has a public IP address and receives traffic from the internet. This is the only tier that faces the public internet.
  • Private application tier (EC2) — your Node.js application runs on EC2 instances in private subnets. These instances have no public IP. They only accept traffic from the ALB's security group.
  • Private data tier (RDS) — your RDS MySQL database sits in its own pair of private subnets. It only accepts connections from the application tier's security group.

Security group chaining is what enforces traffic isolation between tiers. Here is how the chain works:

  • ALB Security Group allows inbound on port 80 from 0.0.0.0/0 (the internet).
  • Application Security Group allows inbound on port 3000 only from the ALB security group ID (not a CIDR block, but the security group itself).
  • Database Security Group allows inbound on port 3306 only from the application security group ID.

Why reference security group IDs instead of CIDRs?

Referencing a security group ID means the rule adapts automatically when instances are added or removed. You never have to update IP ranges manually.

This is the principle of least privilege applied at the network layer. Even if an attacker compromises the ALB, they cannot reach the database directly because the DB security group only trusts the application security group.

The complete request flow through this architecture looks like this:

  • A user's browser sends an HTTP request to the ALB's public DNS name.
  • The ALB (in a public subnet) receives the request and forwards it to a healthy EC2 instance in a private application subnet.
  • The EC2 instance processes the request and queries the RDS MySQL database in the private data subnet.
  • The database returns the result to the EC2 instance, which builds the HTTP response.
  • The response travels back through the ALB to the user's browser.
Document architectural trade-offs

Every architectural decision involves trade-offs. Documenting them proves you understand not just what to build, but why one option fits better than another.

These are the same trade-off questions you will face on the SAA-C03 exam. The exam tests four domains: Design Secure Architectures (30%), Design Resilient Architectures (26%), Design High-Performing Architectures (24%), and Design Cost-Optimized Architectures (20%).

Why ALB over NLB?

An Application Load Balancer operates at Layer 7 (HTTP/HTTPS). It can route based on URL paths, perform HTTP health checks on a specific endpoint like /health, and inspect request content.

A Network Load Balancer (NLB) operates at Layer 4 (TCP/UDP). It excels at ultra-low latency and millions of connections per second, but it cannot make routing decisions based on HTTP content. For a web application that needs path-based health checks, the ALB is the right fit.

Why RDS over self-managed MySQL on EC2?

RDS is a managed database service. AWS handles automated backups, patch management, and multi-AZ failover. With self-managed MySQL on EC2, you would need to handle all of this yourself.

For an insurance claims portal storing customer PII, automated backups and failover are non-negotiable. RDS delivers both without custom scripting or operational burden.

Why a single NAT Gateway?

In production, you would deploy one NAT Gateway per Availability Zone so that a single AZ failure does not cut off outbound internet access for the other AZ's private instances.

For this learning project, a single NAT Gateway saves cost (approximately $0.045/hr). The trade-off is that if the NAT Gateway's AZ fails, instances in the other AZ temporarily lose outbound access. This is acceptable for a non-production environment.

Why Auto Scaling over fixed instances?

Fixed instances mean you either overpay during low traffic or crash during spikes. An Auto Scaling Group adjusts capacity based on demand.

For an insurance claims portal that sees traffic spikes after natural disasters, auto-scaling is essential. It handles the surge automatically and scales back down when claims subside, keeping costs proportional to actual usage.

Mapping to exam domains

Security group chaining and private subnets map to Domain 1 (Secure Architectures, 30%). Multi-AZ and Auto Scaling map to Domain 2 (Resilient Architectures, 26%). ALB traffic distribution maps to Domain 3 (High-Performing Architectures, 24%). Right-sized instances and the single NAT Gateway trade-off map to Domain 4 (Cost-Optimized Architectures, 20%).

Your architecture design is complete. You know exactly what you are building, why each component exists, and what trade-offs you have accepted. Next up, you will start creating the actual VPC and network infrastructure on AWS.

Create the VPC and Network Architecture

In the previous step, you mapped SecureLife Insurance's business requirements to specific AWS services and designed a three-tier architecture on paper.

Now it's time to build the network foundation. Every resource in your architecture needs a network to live on, and that network needs to enforce the isolation rules you designed. You'll create a custom VPC (Virtual Private Cloud) with carefully segmented subnets across two Availability Zones to guarantee high availability and security.

In this step, get ready to:
  • Create a custom VPC with public and private subnets using the AWS wizard.
  • Verify that route tables correctly direct traffic through the Internet Gateway and NAT Gateway.
  • Add isolated database-tier subnets for compliance-grade network separation.
Create the VPC using the wizard

The AWS VPC Console provides a streamlined wizard called "VPC and more" that creates your VPC, subnets, route tables, gateways, and endpoints in a single operation.

  • Open your web browser and navigate to the AWS Management Console at https://console.aws.amazon.com.
  • Sign in with the IAM user credentials you created in Step 1.
  • Confirm the region in the top-right corner is set to US East (N. Virginia) us-east-1.
  • In the search bar at the top, type VPC and select VPC from the results.
  • On the VPC dashboard, click Create VPC.

The VPC creation page opens with two options. You'll use the full wizard to create all networking resources at once.

  • For Resources to create, select VPC and more.
  • For Name tag auto-generation, enter securelife.
  • For IPv4 CIDR block, enter 10.0.0.0/16.
  • For Number of Availability Zones (AZs), select 2.
  • Expand Customize AZs and confirm the first AZ is us-east-1a and the second is us-east-1b.
  • For Number of public subnets, select 2.
  • For Number of private subnets, select 2.
  • For NAT gateways ($), select In 1 AZ.
  • For VPC endpoints, select S3 Gateway.
  • Leave all other settings at their defaults.
  • Review the Preview pane on the right side of the page to see a diagram of the resources that will be created.
  • Click Create VPC.

AWS creates all the resources. This takes about 30 seconds to a minute. Wait for the green success banner before continuing.

What did the wizard create?

The "VPC and more" wizard created 10+ resources in one operation: the VPC itself, 2 public subnets, 2 private subnets, an Internet Gateway, a NAT Gateway with an Elastic IP, route tables for each subnet type, and an S3 Gateway endpoint.

The /16 CIDR gives you 65,536 IP addresses for future growth. Two Availability Zones provide fault tolerance against an entire data centre failure. If us-east-1a goes down, your application continues running in us-east-1b.

Why a single NAT Gateway instead of one per AZ?

In production, AWS recommends a NAT Gateway in every AZ for maximum resilience. For this learning project, a single NAT Gateway keeps costs low while still demonstrating the pattern. If the NAT Gateway's AZ goes down, private subnet instances in the other AZ temporarily lose outbound internet access.

This is a classic cost-vs-resilience trade-off that appears on the SAA-C03 exam.

Verify the route tables and network topology

Route tables control where network traffic goes. You need to confirm that public subnets can reach the internet directly and private subnets route outbound traffic through the NAT Gateway.

  • Click View VPC on the success page (or navigate to Your VPCs in the left navigation pane and select securelife-vpc).
  • Click the Resource map tab to see a visual diagram of your VPC topology.
  • In the left navigation pane, click Route tables.

You should see multiple route tables created by the wizard. You need to check two types.

  • Find the route table associated with one of the public subnets (securelife-rtb-public). Click it, then click the Routes tab.
  • Confirm you see a route with destination 0.0.0.0/0 pointing to the Internet Gateway (target starts with igw-).
  • Find a route table associated with a private subnet (name contains private). Click it, then click the Routes tab.
  • Confirm you see a route with destination 0.0.0.0/0 pointing to a NAT Gateway (target starts with nat-).

What do these routes mean?

The 0.0.0.0/0 route is a default route. It catches all traffic that doesn't match a more specific rule. Public subnets send unknown traffic to the Internet Gateway (direct internet access). Private subnets send it to the NAT Gateway (outbound-only internet access).

You'll also see a 10.0.0.0/16 route targeting local. This means traffic between subnets within the VPC stays internal and never leaves the network.

Don't see route tables or subnets?

Make sure the VPC creation completed successfully. Check the Your VPCs page and confirm securelife-vpc shows a State of Available. If the NAT Gateway shows as Pending, wait another minute for it to become active.

Help me troubleshoot my VPC creation

Create the database-tier private subnets

Your architecture design specified three tiers: public, application, and data. The wizard created the public and application subnets. Now you need to add a separate pair of subnets for the database tier.

Why separate subnets for the database?

In real insurance and banking environments, compliance frameworks like PCI-DSS and SOC 2 require network isolation between application and data tiers. Even if an application server is compromised, the attacker cannot directly reach the database network.

These DB subnets will have no internet route at all. The database only needs to communicate with the application tier through the VPC's internal network.

  • In the left navigation pane, click Subnets.
  • Click Create subnet.
  • For VPC ID, select securelife-vpc from the dropdown.

Now configure the first database subnet.

  • For Subnet name, enter securelife-subnet-db1-us-east-1a.
  • For Availability Zone, select US East (N. Virginia) / us-east-1a.
  • For IPv4 subnet CIDR block, enter 10.0.128.0/24.
  • Click Add new subnet to add a second subnet in the same operation.

Now configure the second database subnet.

  • For Subnet name, enter securelife-subnet-db2-us-east-1b.
  • For Availability Zone, select US East (N. Virginia) / us-east-1b.
  • For IPv4 subnet CIDR block, enter 10.0.129.0/24.
  • Click Create subnet.

Both subnets should appear in the subnets list with a State of Available.

Now you need to ensure these DB subnets have no internet route. You'll create a dedicated route table for them.

  • In the left navigation pane, click Route tables.
  • Click Create route table.
  • For Name, enter securelife-rtb-db.
  • For VPC, select securelife-vpc.
  • Click Create route table.

The new route table only has the default 10.0.0.0/16 local route. No internet route. Now associate both DB subnets with it.

  • With the securelife-rtb-db route table selected, click the Subnet associations tab.
  • Click Edit subnet associations.
  • Select both securelife-subnet-db1-us-east-1a and securelife-subnet-db2-us-east-1b.
  • Click Save associations.

What does this route table enforce?

The DB route table only allows local traffic within the VPC. The database instances in these subnets can communicate with your application servers (because they're all inside 10.0.0.0/16), but they cannot reach the internet in any direction. This is network-level isolation.

Don't see your DB subnets in the association list?

Make sure you selected securelife-vpc as the VPC when creating the subnets. If the subnets are in a different VPC, they won't appear. Also confirm the CIDR blocks 10.0.128.0/24 and 10.0.129.0/24 don't overlap with existing subnets.

Help me find my database subnets

Your network foundation is now complete. You have public subnets for the load balancer, private subnets for the application servers, and isolated subnets for the database. Next up, you'll deploy the RDS MySQL database into those isolated subnets.

Set Up the RDS MySQL Database

Your network architecture is complete. You have public subnets, private application subnets, and private database subnets all wired up with the right route tables.

Now it is time to deploy the data layer. SecureLife Insurance handles customer PII (personally identifiable information), so the database must be completely isolated from public internet access. You will create an RDS MySQL instance inside your dedicated database subnets, protected by a security group that only allows traffic from the application tier.

In this step, get ready to:
  • Create a DB subnet group and database security group.
  • Launch an RDS MySQL instance in private subnets with no public access.
  • Connect to the database and apply the schema.
Create the DB subnet group, security group, and RDS instance

RDS needs a DB subnet group to know which subnets it can place the database in. You will also create a security group that restricts database access to only your application-tier subnets.

  • In the AWS console search bar, type RDS and open the RDS console.
  • In the left navigation panel, click Subnet groups.
  • Click Create DB subnet group.
  • For Name, enter securelife-db-subnet-group.
  • For Description, enter Subnet group for SecureLife RDS database.
  • For VPC, select securelife-vpc.
  • Under Add subnets, select Availability Zones us-east-1a and us-east-1b.
  • For subnets, select 10.0.128.0/24 (us-east-1a) and 10.0.129.0/24 (us-east-1b). These are the dedicated database subnets you created in the previous step.
  • Click Create.

Why a separate DB subnet group?

RDS uses the DB subnet group to determine which subnets it can place the database instance in. By giving RDS only the dedicated database subnets (10.0.128.0/24 and 10.0.129.0/24), you guarantee the database is network-isolated from both the public internet and the application tier at the subnet level.

In insurance and banking environments, compliance frameworks like PCI-DSS and SOC 2 require this kind of network segmentation between application and data tiers.

Next, create the security group that controls which traffic can reach the database.

  • In the AWS console search bar, type VPC and open the VPC console.
  • In the left navigation, click Security groups.
  • Click Create security group.
  • For Security group name, enter securelife-db-sg.
  • For Description, enter Allow MySQL access from app tier only.
  • For VPC, select securelife-vpc.
  • Under Inbound rules, click Add rule.
  • Set Type to MySQL/Aurora (port 3306).
  • Set Source to Custom and enter 10.0.2.0/24.
  • Click Add rule again to add a second inbound rule.
  • Set Type to MySQL/Aurora (port 3306).
  • Set Source to Custom and enter 10.0.3.0/24.
  • Leave the Outbound rules as the default (all traffic allowed).
  • Click Create security group.

What is security group chaining?

The two CIDR ranges you just allowed (10.0.2.0/24 and 10.0.3.0/24) are your private application subnets. This means only EC2 instances running in those subnets can reach the database on port 3306.

Even if someone gains access to the public subnets or misconfigures another security group, the database remains unreachable from any other network segment. This is defense-in-depth.

Now launch the RDS MySQL instance.

  • Return to the RDS console. In the left navigation, click Databases.
  • Click Create database.
  • Select Standard create.
  • For Engine type, select MySQL.
  • For Engine version, choose MySQL 8.0 (the latest 8.0.x available).
  • Under Templates, select Free tier.
  • Under Settings, for DB instance identifier, enter securelife-db.
  • For Master username, enter admin.
  • For Master password, enter a strong password and note it down somewhere safe. You will need it later to connect your application.
  • Under Instance configuration, confirm the instance class is db.t3.micro (Free Tier eligible).
  • Under Connectivity, for Virtual private cloud (VPC), select securelife-vpc.
  • For DB subnet group, select securelife-db-subnet-group.
  • For Public access, select No.
  • For VPC security group, choose Choose existing and select securelife-db-sg. Remove the default security group if it was auto-selected.
  • Expand Additional configuration.
  • For Initial database name, enter insurance.
  • Uncheck Enable automated backups (to simplify cleanup later).
  • Click Create database.

What does No public access mean?

Setting Public access to No means the RDS instance does not get a public IP address. Even if the security group rules were accidentally opened to 0.0.0.0/0, the database would still be unreachable from the internet because there is no public IP to connect to.

This is a critical defense-in-depth measure for any database holding customer data.

The RDS instance takes 5-10 minutes to launch. Wait until the status changes to Available on the Databases page before proceeding.

Database stuck in Creating?

RDS provisioning can take up to 10 minutes. If it stays in Creating for longer than 15 minutes, check that your VPC has the correct subnets and that the DB subnet group references the right Availability Zones.

help me troubleshoot why my RDS instance is stuck in Creating status

  • Once the status shows Available, click on securelife-db to open its details.
  • Under Connectivity & security, find the Endpoint value. It looks like securelife-db.xxxxxxxxxxxx.us-east-1.rds.amazonaws.com.
  • Copy this endpoint value and save it somewhere. You will need it when configuring the application later.
Connect to the database and apply the schema

Your database is running but has no tables yet. You need to connect to it and create the claims table that the application expects.

Since the RDS instance has no public access, you cannot connect directly from your local machine. You will launch a temporary EC2 instance in the private application subnet, connect to it via Session Manager, and run the SQL commands from there.

  • In the AWS console search bar, type EC2 and open the EC2 console.
  • Click Launch instance.
  • For Name, enter securelife-bastion-temp.
  • Under Application and OS Images, select Amazon Linux 2023 AMI.
  • For Instance type, select t3.micro.
  • For Key pair, select Proceed without a key pair (you will use Session Manager instead of SSH).
  • Click Edit next to Network settings.
  • For VPC, select securelife-vpc.
  • For Subnet, select securelife-subnet-private1-us-east-1a (10.0.2.0/24).
  • For Auto-assign public IP, select Disable.
  • Under Firewall (security groups), select Create security group. Uncheck all inbound rules (you do not need SSH since you are using Session Manager).
  • Expand Advanced details.
  • For IAM instance profile, you need a role with the AmazonSSMManagedInstanceCore policy. If you do not have one yet, see the callout below to create one before continuing.
  • Click Launch instance.

How to create an IAM role for Session Manager

If you do not already have an IAM instance profile with the AmazonSSMManagedInstanceCore policy, create one now:

  • Open the IAM console in a new browser tab.
  • Click Roles in the left navigation.
  • Click Create role.
  • Select AWS service as the trusted entity.
  • Choose EC2 as the use case and click Next.
  • Search for and attach AmazonSSMManagedInstanceCore.
  • Click Next.
  • Name the role securelife-ssm-role and click Create role.
  • Go back to the EC2 console and select your bastion instance.
  • Click Actions.
  • Select Security, then Modify IAM role.
  • Select securelife-ssm-role and click Update IAM role.

The SSM Agent is preinstalled on Amazon Linux 2023 AMIs, so no extra software is needed. The NAT Gateway in your VPC provides the outbound internet access the agent needs to reach the Systems Manager service.

  • Wait for the instance to show Running status and the Status check to pass (about 2-3 minutes).
  • Select the securelife-bastion-temp instance and click Connect.
  • Choose the Session Manager tab and click Connect.

A browser-based terminal session opens. Now install the MySQL client and connect to RDS.

  • Install the MySQL command-line client by running:
sudo dnf install mariadb105 -y

Why mariadb105?

Amazon Linux 2023 provides the MySQL-compatible client through the mariadb105 package. MariaDB is a fork of MySQL that uses the same wire protocol. The mysql command it installs connects to MySQL and MariaDB databases identically.

  • Connect to your RDS instance by running the following command, replacing the endpoint with the one you copied earlier:
mysql -h [[RDS_ENDPOINT="securelife-db.xxxxxxxxxxxx.us-east-1.rds.amazonaws.com"]] -u admin -p
  • When prompted, enter the master password you set during RDS creation.
  • You should see the MySQL prompt (MySQL [(none)]>). Run the following SQL to create the claims table:
CREATE DATABASE IF NOT EXISTS insurance;
USE insurance;

CREATE TABLE IF NOT EXISTS claims (
    id INT AUTO_INCREMENT PRIMARY KEY,
    policy_number VARCHAR(50) NOT NULL,
    claim_type VARCHAR(20) NOT NULL,
    description TEXT NOT NULL,
    amount DECIMAL(10, 2) NOT NULL,
    status VARCHAR(20) DEFAULT 'Pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

What does this schema do?

  • Creates the insurance database if it does not already exist.
  • Switches to the insurance database.
  • Creates a claims table with columns for policy number, claim type, a text description, a decimal amount, a status field (defaulting to Pending), and an auto-generated timestamp.

This table is what your Node.js application reads from and writes to when users submit insurance claims.

  • Verify the table was created by running:
SHOW TABLES;

You should see claims listed in the output.

  • Type exit to leave the MySQL prompt.
  • Close the Session Manager browser tab.

Why use a bastion instead of connecting directly?

The RDS instance lives in a private subnet with no public IP. There is no way to reach it from the internet. The only path in is from within the VPC itself.

By launching a temporary EC2 instance in the application subnet and connecting via Session Manager, you follow the same network path your production application will use. In production, schema migrations would be automated via CI/CD, but the network routing is identical.

Cannot connect to the database?

If the mysql command hangs or times out, check these common causes:

  • Verify the bastion EC2 instance is in subnet 10.0.2.0/24 or 10.0.3.0/24. The DB security group only allows traffic from these CIDRs.
  • Double-check that securelife-db-sg has two inbound rules for port 3306 from 10.0.2.0/24 and 10.0.3.0/24.
  • Make sure the RDS endpoint you pasted is correct. Copy it again from the RDS console.

help me debug why my bastion EC2 instance cannot connect to my RDS MySQL instance

  • After confirming the schema is in place, go back to the EC2 console.
  • Select the securelife-bastion-temp instance.
  • Click Instance state.
  • Select Terminate instance. This removes the temporary bastion since you no longer need it.

Your data layer is locked down and ready. The database sits in its own isolated subnets, protected by a security group that only allows connections from your application tier. Next up, you will build the Node.js application and create the launch template that deploys it to your private application subnets.

Prepare the Application and Launch Template

Your network and database infrastructure are in place. Now you need the actual application that will run on EC2 instances behind the load balancer.

In this step, you will write the Insurance Claims Portal application code locally, create the IAM role and Launch Template that EC2 instances use to bootstrap themselves, and wire up security groups so each tier only talks to the tier directly above or below it.

In this step, get ready to:
  • Write the Node.js/Express application (server, frontend, and package manifest).
  • Create an IAM Role and EC2 Launch Template with a User Data bootstrap script.
  • Configure security group chaining across all three tiers.
Write the Node.js application code

The Insurance Claims Portal is a Node.js/Express application that connects to your RDS MySQL database. You will write three files locally to understand them before embedding them in the Launch Template's User Data script.

  • Create a new folder on your Desktop called insurance-claims-portal by running these commands in your terminal:
cd ~/Desktop
mkdir insurance-claims-portal
cd insurance-claims-portal
mkdir public
  • Create a file called package.json inside insurance-claims-portal and paste the following code:
{
  "name": "insurance-claims-portal",
  "version": "1.0.0",
  "description": "Insurance Claims Portal - AWS Solutions Architect Portfolio Project",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "4.19.2",
    "mysql2": "3.22.3"
  }
}

What does this file do?

The package.json manifest tells Node.js about your project and its dependencies.

  • express 4.19.2 is the web framework that handles HTTP routing.
  • mysql2 3.22.3 is the MySQL driver with built-in promise/await support and connection pooling.
  • Save package.json.
  • Create a file called server.js in the same folder and paste the following imports, middleware, and database connection pool:
const express = require('express');
const mysql = require('mysql2/promise');
const path = require('path');

const app = express();
const PORT = 3000;

// Parse incoming JSON and form data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Serve static files (the frontend) from the public folder
app.use(express.static(path.join(__dirname, 'public')));

// Create a connection pool to reuse database connections efficiently
const pool = mysql.createPool({
  host: process.env.DB_HOST || 'localhost',
  user: process.env.DB_USER || 'admin',
  password: process.env.DB_PASSWORD || 'password',
  database: process.env.DB_NAME || 'insurance',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

What does this code do?

This sets up the Express application and creates a MySQL connection pool that reuses connections instead of opening a new one for every request.

  • The database credentials come from environment variables (DB_HOST, DB_USER, DB_PASSWORD, DB_NAME). The fallback values after || are only used during local development.
  • connectionLimit: 10 caps how many simultaneous database connections the pool opens. This protects your db.t3.micro from being overwhelmed.
  • Below the connection pool code in server.js, paste the route handlers and the server listener:
// Health check endpoint for the Application Load Balancer
app.get('/health', async (req, res) => {
  try {
    await pool.query('SELECT 1');
    res.status(200).json({ status: 'healthy' });
  } catch (error) {
    res.status(500).json({ status: 'unhealthy', error: error.message });
  }
});

// Serve the frontend HTML page
app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// Retrieve all claims from the database, newest first
app.get('/api/claims', async (req, res) => {
  try {
    const [rows] = await pool.query('SELECT * FROM claims ORDER BY created_at DESC');
    res.json(rows);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Submit a new insurance claim
app.post('/api/claims', async (req, res) => {
  const { policy_number, claim_type, description, amount } = req.body;
  try {
    const [result] = await pool.query(
      'INSERT INTO claims (policy_number, claim_type, description, amount, status) VALUES (?, ?, ?, ?, ?)',
      [policy_number, claim_type, description, amount, 'Pending']
    );
    res.status(201).json({ id: result.insertId, message: 'Claim submitted successfully' });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Start the server on all network interfaces
app.listen(PORT, '0.0.0.0', () => {
  console.log(`Insurance Claims Portal running on port ${PORT}`);
});

What do these routes do?

  • /health runs SELECT 1 against MySQL. The ALB pings this endpoint every few seconds. If the query fails, the ALB marks the instance as unhealthy and stops sending traffic.
  • GET /api/claims fetches all claims sorted by newest first. The frontend calls this on page load.
  • POST /api/claims inserts a new claim with a default status of Pending. The ? placeholders prevent SQL injection.
  • 0.0.0.0 means the server listens on all network interfaces, which is required for the ALB to reach it on port 3000.
  • Save server.js.
  • Create a file called index.html inside the public subfolder and paste the following frontend code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SecureLife Insurance - Claims Portal</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #333; }
        .header { background: linear-gradient(135deg, #1a237e, #283593); color: white; padding: 20px 40px; display: flex; justify-content: space-between; align-items: center; }
        .header h1 { font-size: 1.5rem; }
        .header .badge { background: #4caf50; padding: 4px 12px; border-radius: 12px; font-size: 0.8rem; }
        .container { max-width: 1200px; margin: 30px auto; padding: 0 20px; }
        .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
        .stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .stat-card h3 { font-size: 0.85rem; color: #666; text-transform: uppercase; margin-bottom: 8px; }
        .stat-card .value { font-size: 1.8rem; font-weight: 700; color: #1a237e; }
        .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }
        .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 24px; }
        .card h2 { margin-bottom: 20px; color: #1a237e; font-size: 1.2rem; }
        .form-group { margin-bottom: 16px; }
        .form-group label { display: block; margin-bottom: 6px; font-weight: 500; font-size: 0.9rem; }
        .form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.95rem; }
        .form-group textarea { height: 80px; resize: vertical; }
        .btn { background: #1a237e; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 1rem; width: 100%; }
        .btn:hover { background: #283593; }
        .claims-list { max-height: 400px; overflow-y: auto; }
        .claim-item { border: 1px solid #eee; border-radius: 6px; padding: 14px; margin-bottom: 10px; }
        .claim-item .claim-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
        .claim-item .policy { font-weight: 600; }
        .claim-item .status { padding: 3px 10px; border-radius: 10px; font-size: 0.8rem; font-weight: 500; }
        .status-pending { background: #fff3e0; color: #e65100; }
        .claim-item .details { font-size: 0.85rem; color: #666; }
        .message { padding: 12px; border-radius: 6px; margin-bottom: 16px; display: none; }
        .message.success { display: block; background: #e8f5e9; color: #2e7d32; }
        .message.error { display: block; background: #ffebee; color: #c62828; }
        @media (max-width: 768px) { .grid { grid-template-columns: 1fr; } }
    </style>
</head>
<body>
    <div class="header">
        <h1>SecureLife Insurance</h1>
        <span class="badge">Claims Portal</span>
    </div>
    <div class="container">
        <div class="stats">
            <div class="stat-card"><h3>Total Claims</h3><div class="value" id="totalClaims">0</div></div>
            <div class="stat-card"><h3>Pending</h3><div class="value" id="pendingClaims">0</div></div>
            <div class="stat-card"><h3>Total Amount</h3><div class="value" id="totalAmount">$0</div></div>
        </div>
        <div class="grid">
            <div class="card">
                <h2>Submit New Claim</h2>
                <div id="message" class="message"></div>
                <form id="claimForm">
                    <div class="form-group">
                        <label for="policy_number">Policy Number</label>
                        <input type="text" id="policy_number" name="policy_number" placeholder="e.g. POL-2024-001" required>
                    </div>
                    <div class="form-group">
                        <label for="claim_type">Claim Type</label>
                        <select id="claim_type" name="claim_type" required>
                            <option value="">Select type...</option>
                            <option value="Auto">Auto</option>
                            <option value="Home">Home</option>
                            <option value="Health">Health</option>
                            <option value="Life">Life</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label for="description">Description</label>
                        <textarea id="description" name="description" placeholder="Describe the incident..." required></textarea>
                    </div>
                    <div class="form-group">
                        <label for="amount">Claim Amount ($)</label>
                        <input type="number" id="amount" name="amount" placeholder="0.00" step="0.01" min="0" required>
                    </div>
                    <button type="submit" class="btn">Submit Claim</button>
                </form>
            </div>
            <div class="card">
                <h2>Recent Claims</h2>
                <div class="claims-list" id="claimsList"></div>
            </div>
        </div>
    </div>
    <script>
        async function loadClaims() {
            try {
                const response = await fetch('/api/claims');
                const claims = await response.json();
                const list = document.getElementById('claimsList');
                const totalEl = document.getElementById('totalClaims');
                const pendingEl = document.getElementById('pendingClaims');
                const amountEl = document.getElementById('totalAmount');

                totalEl.textContent = claims.length;
                pendingEl.textContent = claims.filter(c => c.status === 'Pending').length;
                const total = claims.reduce((sum, c) => sum + parseFloat(c.amount), 0);
                amountEl.textContent = '$' + total.toLocaleString('en-US', { minimumFractionDigits: 2 });

                if (claims.length === 0) {
                    list.innerHTML = '<p style="color:#666;text-align:center;padding:20px;">No claims submitted yet.</p>';
                    return;
                }
                list.innerHTML = claims.map(claim => `
                    <div class="claim-item">
                        <div class="claim-header">
                            <span class="policy">${claim.policy_number}</span>
                            <span class="status status-pending">${claim.status}</span>
                        </div>
                        <div class="details">${claim.claim_type} - $${parseFloat(claim.amount).toLocaleString('en-US', { minimumFractionDigits: 2 })} - ${claim.description}</div>
                    </div>
                `).join('');
            } catch (error) {
                console.error('Error loading claims:', error);
            }
        }

        document.getElementById('claimForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            const messageEl = document.getElementById('message');
            const formData = new URLSearchParams(new FormData(e.target));
            try {
                const response = await fetch('/api/claims', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: formData
                });
                const data = await response.json();
                if (response.ok) {
                    messageEl.className = 'message success';
                    messageEl.textContent = 'Claim submitted successfully!';
                    e.target.reset();
                    loadClaims();
                } else {
                    messageEl.className = 'message error';
                    messageEl.textContent = 'Error: ' + data.error;
                }
            } catch (error) {
                messageEl.className = 'message error';
                messageEl.textContent = 'Network error. Please try again.';
            }
            setTimeout(() => { messageEl.className = 'message'; }, 5000);
        });

        loadClaims();
    </script>
</body>
</html>

What does the frontend do?

This single HTML page is the entire user interface for the Claims Portal. It includes:

  • A stats dashboard showing total claims, pending count, and total dollar amount.
  • A form to submit new claims (policy number, type, description, amount).
  • A real-time claims list that refreshes after each submission by calling GET /api/claims.
  • Save public/index.html.
  • Confirm your folder structure by running:
ls
ls public

You should see package.json, server.js, and public in the first listing, and index.html inside public.

Missing a file?

Make sure you created the public subfolder with mkdir public before creating index.html inside it. Double-check that index.html is inside public/, not in the root of insurance-claims-portal.

help me verify my project folder structure is correct for the insurance claims portal

✔️ Awesome, I've got everything!

Great. Make sure all three files are saved. You will embed this code in the Launch Template next.

ⓧ I'd like to double check the full code

Here is the complete server.js for reference:

const express = require('express');
const mysql = require('mysql2/promise');
const path = require('path');

const app = express();
const PORT = 3000;

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));

const pool = mysql.createPool({
  host: process.env.DB_HOST || 'localhost',
  user: process.env.DB_USER || 'admin',
  password: process.env.DB_PASSWORD || 'password',
  database: process.env.DB_NAME || 'insurance',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

app.get('/health', async (req, res) => {
  try {
    await pool.query('SELECT 1');
    res.status(200).json({ status: 'healthy' });
  } catch (error) {
    res.status(500).json({ status: 'unhealthy', error: error.message });
  }
});

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

app.get('/api/claims', async (req, res) => {
  try {
    const [rows] = await pool.query('SELECT * FROM claims ORDER BY created_at DESC');
    res.json(rows);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/claims', async (req, res) => {
  const { policy_number, claim_type, description, amount } = req.body;
  try {
    const [result] = await pool.query(
      'INSERT INTO claims (policy_number, claim_type, description, amount, status) VALUES (?, ?, ?, ?, ?)',
      [policy_number, claim_type, description, amount, 'Pending']
    );
    res.status(201).json({ id: result.insertId, message: 'Claim submitted successfully' });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`Insurance Claims Portal running on port ${PORT}`);
});
Create the IAM Role and Launch Template

EC2 instances need an IAM role to interact with AWS services securely. The Launch Template then acts as a blueprint that tells Auto Scaling exactly how to create identical instances every time one is needed.

  • In the AWS Console search bar, type IAM and open the IAM service.
  • Click Roles in the left sidebar.
  • Click Create role.
  • For Trusted entity type, select AWS service.
  • For Use case, select EC2, then click Next.
  • In the search box, type AmazonSSMManagedInstanceCore.
  • Check the box next to AmazonSSMManagedInstanceCore, then click Next.
  • Enter securelife-ec2-role as the Role name.
  • Click Create role.

Why AmazonSSMManagedInstanceCore?

This policy lets you connect to your EC2 instances through Session Manager without needing SSH keys or a bastion host. It gives you secure shell access to instances in private subnets for troubleshooting.

When you create a role for EC2 through the console, AWS automatically creates an instance profile with the same name. The instance profile is what gets attached to the Launch Template.

Now create the Launch Template that Auto Scaling will use to launch identical application instances.

  • In the AWS Console search bar, type EC2 and open the EC2 service.
  • Click Launch Templates in the left sidebar under Instances.
  • Click Create launch template.
  • Enter securelife-launch-template as the Launch template name.
  • Under Application and OS Images, click Quick Start and select Amazon Linux. Choose the Amazon Linux 2023 AMI (Free tier eligible).
  • For Instance type, select t3.micro.
  • Under Key pair, select Don't include in launch template. You will use Session Manager instead of SSH.
  • Under Network settings, click Select existing security group. Leave this empty for now. You will create the security group in the next substep and update the template.
  • Expand Advanced details.
  • For IAM instance profile, select securelife-ec2-role.
  • Scroll down to the User data field and paste the following bootstrap script:
#!/bin/bash
yum update -y
yum install -y nodejs npm git

mkdir -p /home/ec2-user/app
cd /home/ec2-user/app

# Write the package manifest
cat > package.json << 'PKGJSON'
{
  "name": "insurance-claims-portal",
  "version": "1.0.0",
  "description": "Insurance Claims Portal",
  "main": "server.js",
  "scripts": { "start": "node server.js" },
  "dependencies": { "express": "4.19.2", "mysql2": "3.22.3" }
}
PKGJSON

# Write the Express server
cat > server.js << 'SERVERJS'
const express = require('express');
const mysql = require('mysql2/promise');
const path = require('path');
const app = express();
const PORT = 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER || 'admin',
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME || 'insurance',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});
app.get('/health', async (req, res) => {
  try { await pool.query('SELECT 1'); res.status(200).json({ status: 'healthy' }); }
  catch (error) { res.status(500).json({ status: 'unhealthy', error: error.message }); }
});
app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); });
app.get('/api/claims', async (req, res) => {
  try { const [rows] = await pool.query('SELECT * FROM claims ORDER BY created_at DESC'); res.json(rows); }
  catch (error) { res.status(500).json({ error: error.message }); }
});
app.post('/api/claims', async (req, res) => {
  const { policy_number, claim_type, description, amount } = req.body;
  try {
    const [result] = await pool.query('INSERT INTO claims (policy_number, claim_type, description, amount, status) VALUES (?, ?, ?, ?, ?)', [policy_number, claim_type, description, amount, 'Pending']);
    res.status(201).json({ id: result.insertId, message: 'Claim submitted successfully' });
  } catch (error) { res.status(500).json({ error: error.message }); }
});
app.listen(PORT, '0.0.0.0', () => { console.log(`Insurance Claims Portal running on port ${PORT}`); });
SERVERJS

# Write the frontend
mkdir -p public
cat > public/index.html << 'INDEXHTML'
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SecureLife Insurance - Claims Portal</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #333; }
        .header { background: linear-gradient(135deg, #1a237e, #283593); color: white; padding: 20px 40px; display: flex; justify-content: space-between; align-items: center; }
        .header h1 { font-size: 1.5rem; }
        .header .badge { background: #4caf50; padding: 4px 12px; border-radius: 12px; font-size: 0.8rem; }
        .container { max-width: 1200px; margin: 30px auto; padding: 0 20px; }
        .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
        .stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .stat-card h3 { font-size: 0.85rem; color: #666; text-transform: uppercase; margin-bottom: 8px; }
        .stat-card .value { font-size: 1.8rem; font-weight: 700; color: #1a237e; }
        .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }
        .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 24px; }
        .card h2 { margin-bottom: 20px; color: #1a237e; font-size: 1.2rem; }
        .form-group { margin-bottom: 16px; }
        .form-group label { display: block; margin-bottom: 6px; font-weight: 500; font-size: 0.9rem; }
        .form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 0.95rem; }
        .form-group textarea { height: 80px; resize: vertical; }
        .btn { background: #1a237e; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 1rem; width: 100%; }
        .btn:hover { background: #283593; }
        .claims-list { max-height: 400px; overflow-y: auto; }
        .claim-item { border: 1px solid #eee; border-radius: 6px; padding: 14px; margin-bottom: 10px; }
        .claim-item .claim-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
        .claim-item .policy { font-weight: 600; }
        .claim-item .status { padding: 3px 10px; border-radius: 10px; font-size: 0.8rem; font-weight: 500; }
        .status-pending { background: #fff3e0; color: #e65100; }
        .claim-item .details { font-size: 0.85rem; color: #666; }
        .message { padding: 12px; border-radius: 6px; margin-bottom: 16px; display: none; }
        .message.success { display: block; background: #e8f5e9; color: #2e7d32; }
        .message.error { display: block; background: #ffebee; color: #c62828; }
        @media (max-width: 768px) { .grid { grid-template-columns: 1fr; } }
    </style>
</head>
<body>
    <div class="header">
        <h1>SecureLife Insurance</h1>
        <span class="badge">Claims Portal</span>
    </div>
    <div class="container">
        <div class="stats">
            <div class="stat-card"><h3>Total Claims</h3><div class="value" id="totalClaims">0</div></div>
            <div class="stat-card"><h3>Pending</h3><div class="value" id="pendingClaims">0</div></div>
            <div class="stat-card"><h3>Total Amount</h3><div class="value" id="totalAmount">$0</div></div>
        </div>
        <div class="grid">
            <div class="card">
                <h2>Submit New Claim</h2>
                <div id="message" class="message"></div>
                <form id="claimForm">
                    <div class="form-group">
                        <label for="policy_number">Policy Number</label>
                        <input type="text" id="policy_number" name="policy_number" placeholder="e.g. POL-2024-001" required>
                    </div>
                    <div class="form-group">
                        <label for="claim_type">Claim Type</label>
                        <select id="claim_type" name="claim_type" required>
                            <option value="">Select type...</option>
                            <option value="Auto">Auto</option>
                            <option value="Home">Home</option>
                            <option value="Health">Health</option>
                            <option value="Life">Life</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label for="description">Description</label>
                        <textarea id="description" name="description" placeholder="Describe the incident..." required></textarea>
                    </div>
                    <div class="form-group">
                        <label for="amount">Claim Amount ($)</label>
                        <input type="number" id="amount" name="amount" placeholder="0.00" step="0.01" min="0" required>
                    </div>
                    <button type="submit" class="btn">Submit Claim</button>
                </form>
            </div>
            <div class="card">
                <h2>Recent Claims</h2>
                <div class="claims-list" id="claimsList"></div>
            </div>
        </div>
    </div>
    <script>
        async function loadClaims() {
            try {
                const response = await fetch('/api/claims');
                const claims = await response.json();
                const list = document.getElementById('claimsList');
                const totalEl = document.getElementById('totalClaims');
                const pendingEl = document.getElementById('pendingClaims');
                const amountEl = document.getElementById('totalAmount');
                totalEl.textContent = claims.length;
                pendingEl.textContent = claims.filter(c => c.status === 'Pending').length;
                const total = claims.reduce((sum, c) => sum + parseFloat(c.amount), 0);
                amountEl.textContent = '$' + total.toLocaleString('en-US', { minimumFractionDigits: 2 });
                if (claims.length === 0) {
                    list.innerHTML = '<p style="color:#666;text-align:center;padding:20px;">No claims submitted yet.</p>';
                    return;
                }
                list.innerHTML = claims.map(claim => `
                    <div class="claim-item">
                        <div class="claim-header">
                            <span class="policy">${claim.policy_number}</span>
                            <span class="status status-pending">${claim.status}</span>
                        </div>
                        <div class="details">${claim.claim_type} - $${parseFloat(claim.amount).toLocaleString('en-US', { minimumFractionDigits: 2 })} - ${claim.description}</div>
                    </div>
                `).join('');
            } catch (error) {
                console.error('Error loading claims:', error);
            }
        }
        document.getElementById('claimForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            const messageEl = document.getElementById('message');
            const formData = new URLSearchParams(new FormData(e.target));
            try {
                const response = await fetch('/api/claims', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: formData
                });
                const data = await response.json();
                if (response.ok) {
                    messageEl.className = 'message success';
                    messageEl.textContent = 'Claim submitted successfully!';
                    e.target.reset();
                    loadClaims();
                } else {
                    messageEl.className = 'message error';
                    messageEl.textContent = 'Error: ' + data.error;
                }
            } catch (error) {
                messageEl.className = 'message error';
                messageEl.textContent = 'Network error. Please try again.';
            }
            setTimeout(() => { messageEl.className = 'message'; }, 5000);
        });
        loadClaims();
    </script>
</body>
</html>
INDEXHTML

# Set database environment variables
export DB_HOST=[[RDS_ENDPOINT="your RDS endpoint hostname (e.g. securelife-db.xxxx.us-east-1.rds.amazonaws.com)"]]
export DB_USER=admin
export DB_PASSWORD=your-db-password-here
export DB_NAME=insurance

# Install dependencies and start the server
npm install
node server.js &

What does the User Data script do?

User Data runs automatically as root when an EC2 instance first boots. This script turns a blank Amazon Linux 2023 instance into a running application server in about 60 seconds.

  • It installs Node.js and npm from the Amazon Linux 2023 package repos.
  • It writes the application files using heredocs (the same code you wrote locally).
  • It exports the database connection details as environment variables so the app can reach your RDS instance.
  • It runs npm install and starts the server in the background on port 3000.

This is the immutable infrastructure pattern. Every instance Auto Scaling launches is identical because it runs the same script. No manual configuration needed.

Replace the RDS endpoint and password

In the User Data script above, replace the DB_HOST value with the RDS endpoint you noted in the previous step. Replace your-db-password-here with the master password you set when creating the RDS instance.

  • Click Create launch template.

You should see a success banner confirming the launch template was created.

Launch template creation failed?

Make sure you selected Amazon Linux 2023 (not Amazon Linux 2) for the AMI. Verify that securelife-ec2-role appears in the IAM instance profile dropdown. If it does not appear, go back to IAM and confirm the role was created successfully.

help me troubleshoot my EC2 Launch Template creation error

Configure security groups with chaining

Security group chaining is how you implement defense-in-depth. Each tier only accepts traffic from the tier directly above it, and each rule references a security group ID instead of a CIDR block. This means if new instances join a tier, they are automatically trusted without changing any rules.

Why security group references instead of CIDRs?

When you reference another security group as the source, the rule dynamically applies to any instance attached to that security group. As Auto Scaling adds or removes instances, the rules adapt automatically.

With CIDR-based rules, you would need to know the IP addresses in advance. That does not work with dynamically scaling infrastructure.

First, create the ALB security group that allows public internet traffic on port 80.

  • In the EC2 console left sidebar, click Security Groups under Network & Security.
  • Click Create security group.
  • Enter securelife-alb-sg as the Security group name.
  • Enter ALB security group - allows HTTP from internet as the Description.
  • For VPC, select securelife-vpc.
  • Under Inbound rules, click Add rule.
  • Set Type to HTTP, Source to Anywhere-IPv4 (0.0.0.0/0).
  • Click Create security group.

Next, create the application-tier security group that only accepts traffic from the ALB.

  • Click Create security group again.
  • Enter securelife-app-sg as the Security group name.
  • Enter App tier - allows port 3000 from ALB only as the Description.
  • For VPC, select securelife-vpc.
  • Under Inbound rules, click Add rule.
  • Set Type to Custom TCP, Port range to 3000.
  • For Source, type securelife-alb-sg in the search field. Select the security group that appears (not a CIDR).
  • Click Create security group.

Finally, update the database security group to reference the app security group instead of CIDR blocks.

  • In the Security Groups list, find and click on securelife-db-sg.
  • Click the Inbound rules tab, then click Edit inbound rules.
  • Delete the existing CIDR-based rules (the ones referencing 10.0.2.0/24 and 10.0.3.0/24).
  • Click Add rule.
  • Set Type to MYSQL/Aurora (port 3306).
  • For Source, type securelife-app-sg in the search field and select it.
  • Click Save rules.

How does the full security chain work?

  • Internet traffic enters on port 80 through securelife-alb-sg.
  • The ALB forwards to EC2 on port 3000. Only traffic from securelife-alb-sg is allowed by securelife-app-sg.
  • EC2 connects to RDS on port 3306. Only traffic from securelife-app-sg is allowed by securelife-db-sg.

Even if an attacker reaches the ALB, they cannot directly connect to the database. They would need to compromise an application instance first. This is defense-in-depth.

Now attach the new app security group to your Launch Template.

  • Go back to Launch Templates in the EC2 sidebar.
  • Select securelife-launch-template.
  • Click Actions, then Modify template (Create new version).
  • Under Network settings, click Select existing security group and choose securelife-app-sg.
  • Click Create template version.
  • Back on the template details page, click Actions, then Set default version.
  • Select the newest version number and click Set as default version.

Your Launch Template now references the correct security group. Any instance launched from it will only accept traffic from the ALB on port 3000.

Don't see securelife-app-sg in the dropdown?

Make sure you select securelife-vpc in the Network settings section first. Security groups are VPC-specific. If the VPC is set to the default VPC, your custom security groups will not appear.

help me fix my Launch Template so it uses the correct security group in securelife-vpc

Your application code, IAM role, Launch Template, and security group chain are all in place. Next up, you will deploy the Application Load Balancer and Auto Scaling Group to bring everything online.

Deploy the Load Balancer and Auto Scaling Group

Your infrastructure blueprint is ready. The launch template knows how to create identical application servers, and the security groups enforce strict traffic rules between tiers.

Now you need the two components that make this architecture highly available: an Application Load Balancer to distribute incoming traffic across multiple servers, and an Auto Scaling Group to ensure those servers stay running even if one fails.

In this step, get ready to:
  • Create an Application Load Balancer and target group to route internet traffic to your app servers.
  • Launch an Auto Scaling Group that maintains two instances across both Availability Zones.
  • Verify the full data flow by submitting a test insurance claim through the portal.
Create the Application Load Balancer and target group

The ALB lives in the public subnets because it needs a public IP to receive internet traffic. It then forwards requests to your EC2 instances in private subnets. The ALB bridges the gap between the public internet and your isolated application tier.

Before creating the ALB itself, you need a target group. This tells the ALB where to send traffic and how to check whether each target is healthy.

  • In the AWS Console, navigate to EC2 from the search bar.
  • In the left navigation pane, scroll down to Load Balancing and click Target Groups.
  • Click Create target group.
  • For Choose a target type, select Instances.
  • Enter securelife-tg as the Target group name.
  • Set Protocol to HTTP and Port to 3000.
  • Select securelife-vpc as the VPC.
  • Under Health checks, set Health check path to /health.
  • Click Next.
  • Do not register any targets manually. The Auto Scaling Group will handle this automatically.
  • Click Create target group.

Why /health as the health check path?

Your server.js defines a /health endpoint that queries the database with SELECT 1. If the app can reach the database, it returns a 200 status.

The ALB pings this path every 30 seconds. If an instance fails, the ALB stops sending traffic to it. This is how your architecture self-heals without human intervention.

Now create the load balancer itself.

  • In the left navigation pane, click Load Balancers (under Load Balancing).
  • Click Create load balancer.
  • Under Application Load Balancer, click Create.
  • Enter securelife-alb as the Load balancer name.
  • For Scheme, select Internet-facing.
  • Under Network mapping, select securelife-vpc as the VPC.
  • Select the us-east-1a checkbox and choose securelife-subnet-public1-us-east-1a from the dropdown.
  • Select the us-east-1b checkbox and choose securelife-subnet-public2-us-east-1b from the dropdown.
  • Under Security groups, remove the default security group and select securelife-alb-sg.
  • Under Listeners and routing, keep the protocol as HTTP and port as 80.
  • For Default action, select securelife-tg from the Forward to dropdown.
  • Click Create load balancer.

Why does the ALB need public subnets?

The ALB sits in the public subnets because it needs public IP addresses to accept traffic from the internet. Your EC2 instances stay safely in private subnets with no public IPs.

The ALB bridges these two worlds. Internet users reach the ALB's public address, and the ALB forwards requests to private instances on port 3000. From the outside, nobody knows your actual servers exist.

Don't see your VPC or subnets?

Make sure you are in the us-east-1 region (check the top-right corner of the console). Your VPC and subnets only appear in the region where you created them.

Help me troubleshoot my ALB not showing the correct VPC or subnets

Create the Auto Scaling Group

The Auto Scaling Group uses your launch template to create and manage EC2 instances automatically. It ensures you always have enough capacity running, even if an instance or an entire Availability Zone fails.

  • In the EC2 left navigation pane, scroll down to Auto Scaling and click Auto Scaling Groups.
  • Click Create Auto Scaling group.
  • Enter securelife-asg as the Auto Scaling group name.
  • Under Launch template, select securelife-launch-template.
  • Click Next.
  • Under Network, select securelife-vpc as the VPC.
  • For Availability Zones and subnets, select securelife-subnet-private1-us-east-1a and securelife-subnet-private2-us-east-1b.
  • Click Next.
  • Under Load balancing, select Attach to an existing load balancer.
  • Select Choose from your load balancer target groups and pick securelife-tg.
  • Under Health checks, check the box for Turn on Elastic Load Balancing health checks.
  • Click Next.
  • Set Desired capacity to 2.
  • Set Min desired capacity to 1.
  • Set Max desired capacity to 4.
  • Click Next through the remaining pages (notifications, tags) without changes.
  • On the Review page, click Create Auto Scaling group.

What do these capacity numbers mean?

Desired 2 means the ASG launches one instance in each AZ from the start. If one entire AZ goes down, you still have one healthy instance serving traffic.

Min 1 prevents a total outage. Even under extreme circumstances, at least one instance stays alive. Max 4 caps your cost during unexpected traffic spikes, so you never accidentally launch dozens of instances.

Auto Scaling Group stuck at 0 instances?

If your instances never launch, check that the private app subnets have a route to the NAT Gateway. Without internet access (via NAT), the User Data script cannot download Node.js or npm packages.

Also verify that securelife-launch-template is selected and the IAM role securelife-ec2-role is attached.

Help me debug why my Auto Scaling Group isn't launching instances

Access the portal and submit a test claim

Your instances need a few minutes to boot, install Node.js, download npm packages, and start the application. Once they pass the ALB health check, the portal is live.

  • Return to Load Balancers in the EC2 left navigation pane.
  • Select securelife-alb.
  • Copy the DNS name from the Description tab (it looks like securelife-alb-123456789.us-east-1.elb.amazonaws.com).
  • Open a new browser tab and paste the DNS name into the address bar.

How long until the portal loads?

Instances typically take 3-5 minutes to pass the initial health check. The ALB only routes traffic to healthy targets.

To check progress, go to Target Groups, select securelife-tg, and check the Targets tab. You want to see at least one target in healthy status before the portal responds.

Once the SecureLife Insurance Claims Portal loads, test the full data flow from browser to database and back.

  • In the Policy Number field, enter POL-2024-001.
  • Select Auto from the Claim Type dropdown.
  • Enter Fender bender in parking lot in the Description field.
  • Enter 2500 in the Claim Amount ($) field.
  • Click Submit Claim.

You should see a green success message and the claim appear in the Recent Claims section on the right. The stats at the top update to show 1 total claim.

What just happened end to end?

Your browser sent an HTTP request to the ALB's public DNS name. The ALB received it in a public subnet and forwarded it to one of your EC2 instances in a private subnet on port 3000.

The Express app on that instance processed the request and wrote the claim to the RDS MySQL database in an even deeper private subnet. The response flowed back through the same chain. Three network tiers, all working together.

Seeing a 502 Bad Gateway or timeout?

A 502 means the ALB reached your instance but the app is not responding on port 3000. Check these common causes:

  • The User Data script may still be running. Wait 3-5 minutes and try again.
  • The app security group securelife-app-sg must allow inbound port 3000 from securelife-alb-sg. Verify this in the security group's inbound rules.
  • The database connection may be failing. Ensure the RDS endpoint is correct in the User Data environment variables and that securelife-db-sg allows port 3306 from securelife-app-sg.

Help me debug a 502 error on my Application Load Balancer

Your multi-tier architecture is fully operational. Next up, you'll test what happens when things go wrong and prove that this architecture truly self-heals.

Test High Availability and Validate the Architecture

Your Insurance Claims Portal is live and serving traffic through the Application Load Balancer. But how do you know it can survive a failure?

In this step, you will deliberately break your infrastructure by terminating an EC2 instance, then watch it heal itself automatically. You will also map every component you built to the four SAA-C03 exam domains to see how this architecture translates directly into exam knowledge.

In this step, get ready to:
  • Terminate an EC2 instance and observe Auto Scaling self-healing.
  • Verify the application stays accessible during the failure.
  • Map the completed architecture to the four SAA-C03 exam domains.
Terminate an EC2 instance and observe self-healing

The real test of high availability is what happens when something breaks. In production, hardware fails, OS crashes happen, and entire Availability Zones go offline. Your architecture should handle this without human intervention.

  • In the AWS Management Console, navigate to the EC2 service.
  • Select Instances from the left navigation pane.
  • You should see 2 running instances created by your Auto Scaling Group securelife-asg.
  • Select the checkbox next to one of the running instances.
  • Click Instance state in the top menu.
  • Select Terminate instance.
  • Click Terminate to confirm.

Why terminate an instance on purpose?

This simulates exactly what happens during hardware failures, OS crashes, or AZ outages. The Auto Scaling Group detects that the instance count has dropped below the desired capacity of 2 and automatically launches a replacement.

The architecture self-heals without any human intervention. This is the resilience pattern that the SAA-C03 exam tests extensively.

Verify the application stays online during recovery

While the replacement instance launches, the ALB routes all traffic to the remaining healthy instance. Your users never see downtime.

  • Switch back to your browser tab with the Insurance Claims Portal (the ALB DNS name).
  • Refresh the page. The portal should still load and display your test claim from earlier.
  • Submit another test claim to confirm the full data flow still works (browser to ALB to remaining EC2 instance to RDS and back).

What is happening behind the scenes?

The ALB performs health checks on the /health endpoint. When the terminated instance stops responding, the ALB marks it as unhealthy and stops sending traffic to it. All requests go to the surviving instance.

Meanwhile, the Auto Scaling Group detects that only 1 instance is running (below the desired capacity of 2). It launches a new instance using your launch template securelife-launch-template, which bootstraps the application via the User Data script.

  • Return to the EC2 console.
  • Select Auto Scaling Groups from the left navigation pane.
  • Click on securelife-asg.
  • Click the Activity tab.
  • You should see activity entries showing the terminated instance and the new instance being launched as a replacement.

Give it a few minutes

The replacement instance takes 2-3 minutes to launch, install dependencies via User Data, and pass the ALB health check. Refresh the Activity tab periodically until you see the new instance reach Successful status.

  • Once the replacement instance is healthy, click Instances in the left navigation pane again.
  • Confirm you see 2 running instances once more. The architecture has self-healed.

Replacement instance not launching?

Check the Activity tab for error messages. Common causes include:

  • The launch template references a security group or subnet that was deleted. Verify securelife-app-sg still exists.
  • The IAM instance profile securelife-ec2-role was detached. Check the launch template configuration.
  • Help me troubleshoot why my Auto Scaling Group isn't launching a replacement instance after I terminated one.
Map the architecture to SAA-C03 exam domains

Every component you built maps directly to one of the four domains tested on the AWS Solutions Architect Associate (SAA-C03) exam. This is the same reasoning you would present in a solutions architect interview.

What is the SAA-C03 exam?

The AWS Solutions Architect Associate (SAA-C03) exam tests your ability to design architectures on AWS. It has four scored domains that together cover security, resilience, performance, and cost optimization.

Here is how your architecture maps to each domain:

Domain 1: Design Secure Architectures (30%)

  • VPC network isolation separates your application from the public internet using private subnets.
  • Security group chaining (ALB SG to App SG to DB SG) implements the principle of least privilege. Each tier only accepts traffic from the tier directly above it.
  • Private subnets for application and data tiers ensure EC2 instances and the RDS database have no direct internet access.
  • IAM roles (securelife-ec2-role) grant EC2 instances only the permissions they need, with no hardcoded credentials.
  • No public database access means even if security groups were misconfigured, the RDS instance has no public IP and is unreachable from the internet.

Domain 2: Design Resilient Architectures (26%)

  • Multi-AZ deployment across us-east-1a and us-east-1b protects against an entire data centre failure.
  • Auto Scaling Group automatically replaces failed instances and maintains desired capacity. You just proved this by terminating an instance.
  • ALB health checks on the /health endpoint detect unhealthy instances and stop routing traffic to them.
  • Self-healing architecture requires no human intervention when failures occur.

Domain 3: Design High-Performing Architectures (24%)

  • ALB traffic distribution spreads requests across multiple instances, preventing any single server from becoming a bottleneck.
  • Connection pooling in the application (connectionLimit: 10) efficiently manages database connections across requests.
  • Auto Scaling elasticity (min 1, max 4) allows the infrastructure to grow with demand during peak claims periods.

Domain 4: Design Cost-Optimized Architectures (20%)

  • Right-sized instances (t3.micro for both EC2 and RDS) match the workload without over-provisioning.
  • Single NAT Gateway trade-off reduces cost compared to one-per-AZ. This is acceptable for non-production workloads where the NAT Gateway is not a critical availability requirement.
  • Free Tier usage covers EC2, RDS, and ALB within the $100 credit allowance for new accounts.
  • Cleanup procedures ensure resources are deleted when no longer needed, preventing ongoing charges.

Why this architecture wins SA interviews

Solutions architect interviews ask you to justify trade-offs, not just list services. You can now explain WHY you chose a single NAT Gateway (cost savings vs. resilience), WHY security group chaining uses SG references instead of CIDRs (dynamic adaptation), and WHY the database lives in separate subnets (compliance isolation).

Each of these decisions has a counter-argument. Knowing both sides is what separates a junior engineer from a solutions architect.

Secret mission

Add CloudWatch Monitoring with SNS Alerts

Your architecture self-heals, but right now nobody knows when it does. In production, SRE teams get paged the moment infrastructure degrades. Add CloudWatch alarms that send you an email notification whenever your Auto Scaling group loses capacity or CPU spikes.

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. Your AWS Free Tier credits cover most of these resources, but the NAT Gateway charges approximately $0.045 per hour regardless of Free Tier status.

Cost warning

Your NAT Gateway costs approximately $0.045 per hour even when no traffic flows through it. If you leave all resources running overnight (8 hours), that is about $0.36. Over a full week, it would be around $7.56.

Your $100 AWS sign-up credits cover this comfortably for a short testing period. However, if you are done with the project, deleting everything is the safest way to avoid unexpected charges.

Resources you used:

  • Auto Scaling Group (securelife-asg) with EC2 instances.
  • Application Load Balancer (securelife-alb) and Target Group (securelife-tg).
  • RDS MySQL instance (securelife-db).
  • NAT Gateway with Elastic IP.
  • VPC (securelife-vpc) including subnets, route tables, Internet Gateway, security groups, and S3 Gateway endpoint.
  • Launch Template (securelife-launch-template) and IAM Role (securelife-ec2-role).
  • CloudWatch Alarms (securelife-low-instances, securelife-high-cpu) and SNS Topic (securelife-alerts).
  • DB Subnet Group (securelife-db-subnet-group).
  • Local project files (server.js, package.json, public/index.html, schema.sql, userdata.sh).
  • AWS account.

Keep everything running

No action needed. Choose this if you are still actively testing your architecture or want to experiment further.

  • Your $100 AWS sign-up credits cover the NAT Gateway, EC2 instances, ALB, and RDS for several hours of testing.
  • Monitor your remaining credits in the AWS Billing and Cost Management console.
  • AWS CLI v2 remains installed on your macOS VM. It does not consume resources when idle.

Pause - I'll come back to this later

Shut down running compute resources to reduce costs while preserving your configuration. You can restart later without rebuilding everything.

  • In the EC2 console, select **Auto Scaling Groups** in the left sidebar.
  • Select securelife-asg and click **Edit**.
  • Set **Desired capacity** to 0 and **Minimum capacity** to 0. Click **Update**. This terminates all EC2 instances.
  • In the RDS console, select **Databases** in the left sidebar.
  • Select securelife-db, click **Actions**, then **Stop temporarily**.
  • Note that RDS automatically restarts the instance after 7 days. Set a reminder to stop it again or delete it before then.

What about the NAT Gateway?

The NAT Gateway cannot be paused. It charges approximately $0.045 per hour as long as it exists. If you plan to come back within a few hours, the cost is negligible against your credits. If you are pausing for days, consider deleting the NAT Gateway and recreating it when you return.

Delete - I don't want to use this again

Remove all project resources completely. Follow these steps in order. Some resources depend on others being deleted first.

Delete CloudWatch Alarms and SNS Topic
  • In the CloudWatch console, select **Alarms** from the left sidebar.
  • Select both securelife-low-instances and securelife-high-cpu.
  • Click **Actions**, then **Delete**. Confirm the deletion.
  • In the SNS console, select **Topics** from the left sidebar.
  • Select securelife-alerts and click **Delete**. Type delete me to confirm.
Delete the Auto Scaling Group and EC2 instances
  • In the EC2 console, select **Auto Scaling Groups** in the left sidebar.
  • Select securelife-asg and click **Delete**.
  • Type delete to confirm. This terminates all managed EC2 instances automatically.
  • Wait for the instances to terminate before proceeding. You can check in **Instances** in the left sidebar.
Delete the Application Load Balancer and Target Group
  • In the EC2 console, select **Load Balancers** in the left sidebar.
  • Select securelife-alb, click **Actions**, then **Delete load balancer**.
  • Type confirm and click **Delete**.
  • Select **Target Groups** in the left sidebar.
  • Select securelife-tg, click **Actions**, then **Delete**. Confirm.
Delete the RDS instance
  • In the RDS console, select **Databases** in the left sidebar.
  • Select securelife-db, click **Actions**, then **Delete**.
  • Uncheck **Create final snapshot**. Check the acknowledgement box.
  • Type delete me and click **Delete**.
  • Wait for the RDS instance to fully delete before proceeding. This can take several minutes.
Delete the DB Subnet Group
  • In the RDS console, select **Subnet groups** in the left sidebar.
  • Select securelife-db-subnet-group and click **Delete**. Confirm.
Delete the NAT Gateway and release the Elastic IP
  • In the VPC console, select **NAT gateways** in the left sidebar.
  • Select the NAT Gateway associated with securelife-vpc and click **Actions**, then **Delete NAT gateway**.
  • Type delete to confirm.
  • Wait for the NAT Gateway status to change to **Deleted**. This takes 1-2 minutes.
  • Select **Elastic IP addresses** in the left sidebar.
  • Select the Elastic IP that was associated with your NAT Gateway (it will show as unassociated now).
  • Click **Actions**, then **Release Elastic IP addresses**. Confirm.
Delete the VPC
  • In the VPC console, select **Your VPCs** in the left sidebar.
  • Select securelife-vpc, click **Actions**, then **Delete VPC**.
  • This will also delete all associated subnets, route tables, the Internet Gateway, security groups, and the S3 Gateway endpoint.
  • Type delete to confirm and click **Delete**.
Delete the Launch Template and IAM Role
  • In the EC2 console, select **Launch Templates** in the left sidebar.
  • Select securelife-launch-template, click **Actions**, then **Delete template**. Type Delete to confirm.
  • In the IAM console, select **Roles** in the left sidebar.
  • Search for securelife-ec2-role and select it.
  • Click **Delete**. Type the role name to confirm and click **Delete**.
Delete local project files
  • In your terminal, navigate to the folder that contains your project files and delete them by running:
cd ~/Desktop
rm -rf insurance-claims-portal
Close your AWS account (optional)
  • If you only created this AWS account for this project, you can close it.
  • Click your account name in the top-right corner of the AWS console, then select **Account**.
  • Scroll to the bottom of the Account Settings page and expand **Close Account**.
  • Check all acknowledgement boxes and click **Close Account**.

Why this specific order?

AWS resources have dependencies. The VPC cannot be deleted while a NAT Gateway or RDS instance exists inside it. The RDS instance cannot be deleted while it depends on the DB Subnet Group. Deleting in reverse order of creation ensures you never hit a dependency conflict.

That's a Wrap!

That's a Wrap!

Nice work! You just architected and built enterprise-grade infrastructure from scratch. You took a set of business requirements from SecureLife Insurance and translated them into a fully deployed, highly available claims portal on AWS.

You've learned how to:

  • Design and implement a custom VPC with multi-tier network segmentation (public, private application, and private data subnets) across two Availability Zones.
  • Deploy an Application Load Balancer with an Auto Scaling Group that self-heals and distributes traffic automatically across multiple Availability Zones.
  • Build and deploy a professional Insurance Claims Portal backed by a managed RDS MySQL database in network-isolated private subnets, with security group chaining enforcing defense-in-depth.
  • Added CloudWatch monitoring with SNS notifications that alert you when your infrastructure scales or degrades.

Ready to quiz yourself?