Ravindra BagaleCourses & study guides मराठी Track your progress

How to Build a 3-Tier Architecture in AWS Step by Step (VPC, ALB, Auto Scaling, RDS)

How to Build a 3-Tier Architecture in AWS with VPC, NAT Gateway and RDS explained the idea of three tiers with single servers. This chapter is the full production-style build, click by click: a custom VPC with eight subnets in two Availability Zones, an internet-facing Application Load Balancer, nginx web servers and PHP app servers that run in Auto Scaling groups from your own AMIs, an internal load balancer between them, and Amazon RDS MySQL at the bottom. Every step says what to click, what to type, what you should see, how to check it, and why it is done. The problems you are likely to hit are inside the steps, with the reason and the fix.

What you'll learn in this class

  • What highly available, fault tolerant, low latency and disaster recovery mean for a real app
  • Why the build goes bottom to top and the security rules go top to bottom
  • The /20 address plan for eight subnets in two AZs, worked out
  • Five chained security groups, and why each source is a security group and not an IP
  • A DB subnet group and RDS MySQL with no public access, reached through a jump server
  • Development servers, AMIs and launch templates: immutable servers
  • Two Auto Scaling groups with an internal and an internet-facing ALB, health checks and target tracking
  • Balanced best effort, warmup, grace period and instance refresh
  • Secrets Manager, leaked keys on GitHub, Windows key permissions, vCPU quotas and 502/503/504
  • A full troubleshooting checklist and the clean-up that stops the bill

1. The four goals of this design

Why: a single server is one point of failure. If it crashes, or its data centre has a problem, the whole app is down. Each of these four goals answers one "what if":

  1. Highly available (HA): "what if a whole data centre goes down?" We use two Availability Zones (AZs). An AZ is one or more separate data centres in a Region, with its own power and network. Every tier has servers in both AZs. Worked example: the app needs 2 web servers to carry its traffic. Put 2 in AZ a and 2 in AZ b, 4 in total. If AZ a fails, AZ b still has the 2 you need. In this lab we start with 1 per AZ (desired 2) to keep the bill small; the Auto Scaling group replaces a lost server in the healthy AZ.
  2. Fault tolerant: "what if one server breaks?" The load balancer stops sending traffic to a server that fails its health check, and the Auto Scaling group terminates it and launches a fresh copy from the AMI. Nobody has to wake up at night.
  3. Low latency: "what if users are far away?" The time a request takes grows with distance. Users in India get faster answers from the Mumbai Region than from a Region in another continent, so you pick the Region near your users (and, for users in many countries, deploy in more than one Region).
  4. Disaster recovery (DR): "what if the data is lost or the Region is unusable?" RDS takes automated backups and you can take snapshots; for bigger systems you copy backups or run a copy of the stack in a second Region. This chapter builds the HA and fault-tolerant parts; DR is the next layer on top.

Classroom line

Understand this, and you have understood 3-tier architecture.

2. The design, top to bottom

What: one request travels down five layers and the answer comes back up the same way.

Layer Lives in Runs Accepts traffic only from
Internet-facing ALB PublicSubnet1, PublicSubnet2 AWS-managed load balancer the internet, port 80
Web tier WebSubnet1, WebSubnet2 (private) nginx, from webami, in WebTierASG the internet-facing ALB
Internal ALB AppSubnet1, AppSubnet2 AWS-managed load balancer, no public address the nginx servers
App tier AppSubnet1, AppSubnet2 (private) PHP + php-fpm, from phpami, in AppTierASG the internal ALB
Database tier DBSubnet1, DBSubnet2 (private) Amazon RDS MySQL the PHP servers, port 3306

Why the web servers are private too: users never talk to an nginx server directly. They talk to the internet-facing ALB, and only the ALB needs a route to the internet gateway. So the public subnets hold just the ALB, the JumpServer and the short-lived development servers. The three tiers (web, app, database) all sit in private subnets, and an attacker can't even send a packet to them from outside.

Why an internal load balancer between web and app: the PHP servers come and go with Auto Scaling, so their IPs keep changing. nginx needs one fixed name to send PHP requests to. The internal ALB gives that name and spreads the requests over the healthy PHP servers in both AZs.

The 3-tier app we build, top to bottomUsers (browser)HTTP :80 from the internetThreeTierVpc 10.0.0.0/16AZ a (ap-northeast-2a)AZ b (ap-northeast-2b)Public subnets (route 0.0.0.0/0 → ThreeTierIGW)PublicSubnet1 10.0.0.0/20PublicSubnet2 10.0.16.0/20Internet-facing ALB (WebAlbSG)the only public entry for usersJumpServer (JumpSG), setup onlydev servers, deleted after AMIs:80Web tier: private Web subnetsWebSubnet1 10.0.32.0/20WebSubnet2 10.0.48.0/20nginx (WebEc2SG)webaminginx (WebEc2SG)webamiWebTierASG: desired 2, min 1, max 4:80, only .php requestsInternal ALB (AppAlbSG)no public address:80App tier: private App subnetsAppSubnet1 10.0.64.0/20AppSubnet2 10.0.80.0/20PHP + php-fpmAppEc2SG · phpamiPHP + php-fpmAppEc2SG · phpamiAppTierASG: desired 2, min 1, max 4:3306Database tier: private DB subnetsDBSubnet1 10.0.96.0/20DBSubnet2 10.0.112.0/20RDS MySQLprimary (DbSG)Public access: NoStandbyMulti-AZ in productionautomatic failoverThreeTierDBSubnetGroup = DBSubnet1 + DBSubnet2Each tier accepts traffic only from the tier above it.Spare: 10.0.128.0 to 10.0.255.255 is free for later subnets.

Figure 1. The whole build from top to bottom: users, the internet-facing ALB in the public subnets, nginx in the private Web subnets, the internal ALB, PHP in the private App subnets, and RDS MySQL in the private DB subnets, in two AZs.

Whiteboard drawing of a 3-tier AWS design with public, Web, App and Db rows in two Availability Zones, an internet-facing load balancer, an internal load balancer, Web and App Auto Scaling groups with nginx and PHP AMIs, and RDS with standby and read replicas

Whiteboard: the 3-tier design in two AZs: public subnets with the internet-facing load balancer, NAT and jump server; Web, App and DB rows; a Web and an App Auto Scaling group, each from its own AMI; an internal load balancer between Web and App; RDS with a standby and read replicas.

One form submit, top to bottomBrowseruser fills forms.htmlInternet-facing ALBpublic subnets · WebAlbSGnginx web serverprivate Web subnet · WebEc2SGInternal ALBAppAlbSGPHP app serverprivate App subnet · AppEc2SGAmazon RDS MySQLprivate DB subnet · DbSG1. The browser sends POST /submit.php to the ALB's DNS nameon port 80.2. The ALB picks a healthy nginx server in a private Web subnet.3. nginx serves .html itself; .php matches location ~ \.php$ andgoes to the internal ALB.4. The internal ALB picks a healthy PHP server in a private Appsubnet.5. PHP hashes the password and inserts the row into insta.userson port 3306.6. The answer travels back up the same chain to the browser.7. Every hop is allowed only from the security group just aboveit.

Figure 2. Animation: one form submit travels down the chain and the answer comes back up.

Classroom line

Think about it: why is it like this?

3. Build bottom to top, write the rules top to bottom

Why bottom to top: every tier needs an address from the tier below it before it can be finished.

  1. RDS first. It gives you the database endpoint.
  2. Then the PHP servers. submit.php needs the RDS endpoint written in it before you bake the PHP AMI.
  3. Then the internal ALB. It is created together with the App Auto Scaling group and gives you its DNS name.
  4. Then nginx. Its proxy_pass line needs the internal ALB's DNS name before you bake the web AMI.
  5. Last, the internet-facing ALB in front of the web Auto Scaling group.

If you went top to bottom, you would launch the web servers with nothing to point at, then log in to every one of them later to fix the config. With 2 servers that is annoying; with 100 servers launched by an Auto Scaling group it is impossible, and against the whole idea of identical servers from one AMI.

Why the security rules read top to bottom: each tier trusts only the tier directly above it. The rule for a tier is written as "accept port X only from the security group of the tier above". So you read and check them from the internet downwards: WebAlbSG, WebEc2SG, AppAlbSG, AppEc2SG, DbSG.

Why all security groups are created first: a rule can point only to a security group that already exists. Create the five groups empty, then add the rules. The servers are still built bottom to top.

The security group chainInternet0.0.0.0/0WebAlbSGinbound 80 from 0.0.0.0/0on the internet-facing ALBWebEc2SGinbound 80 from WebAlbSGon nginx servers (web launch template)AppAlbSGinbound 80 from WebEc2SGon the internal ALBAppEc2SGinbound 80 from AppAlbSGon PHP servers (app launch template)DbSGinbound 3306 from AppEc2SGon the RDS databaseJumpSGsetup only: DbSG also allows 3306 from JumpSGbuild order: bottom to toprules: top to bottom

Figure 3. The security group chain, read top to bottom. The servers are built bottom to top.

4. Immutable servers: why nobody logs in to a live server

Why it came up. While drawing the public subnets, the question was: why put a jump server and a NAT gateway there at all, if all the real servers are private?

Background. In the old way, an admin logs in to each live server and runs updates by hand. Over time every server becomes a little different ("configuration drift"), and nobody knows exactly what is on it. In the immutable way you never change a running server. You change a development server, test it, bake a new AMI, put it in a new launch template version, and let the Auto Scaling group replace the old servers with an instance refresh. If something breaks you roll back to the previous version.

How it links to the 3-tier build. This is exactly the build below: phpdevelopmentserver and nginxdevelopmentserver are built by hand in a public subnet, turned into phpami and webami, and then deleted. The JumpServer is only for the database setup and for reading logs when you debug. A NAT gateway is needed only if the private servers must reach the internet themselves, for example to install packages or call an outside API. Our private servers get everything from the AMI and talk only to the ALB and RDS, so this build needs no NAT gateway. When you do need one, it is covered in the concept chapter.

Immutable servers: change the image, not the server1. Development serverpublic subnet, temporary, you SSH in2. Install, configure, testyum install · edit config · browser test3. Enable services at bootsudo systemctl enable nginx php-fpm4. Create image (AMI)phpami / webami5. Launch template, new versionAMI + type + key + security group6. Auto Scaling grouplaunches identical servers in private subnets7. Instance refreshreplaces old servers with the new versionnext change: start again at step 1Nobody logs in to live servers; every change is a new AMI.

Figure 4. Immutable servers: every change goes development server → AMI → new launch template version → instance refresh.

5. The address plan: eight /20 subnets

Why eight: 4 kinds of subnet (public, web, app, DB) × 2 AZs = 8. Each tier gets one subnet in each AZ, so losing an AZ never takes a whole tier away. Why the AZs alternate (1 in AZ a, 2 in AZ b): if both subnets of a tier were in the same AZ, that tier would not be highly available at all.

Worked example: the /20 math

  1. The VPC is 10.0.0.0/16: 32 − 16 = 16 host bits, so 2^16 = 65,536 addresses, 10.0.0.0 to 10.0.255.255.
  2. A /20 has 32 − 20 = 12 host bits: 2^12 = 4,096 addresses.
  3. 4,096 = 16 × 256, so each /20 covers 16 values of the third octet. The next /20 starts 16 higher: 10.0.0.0, 10.0.16.0, 10.0.32.0, and so on.
  4. AWS keeps 5 addresses in every subnet (network address, VPC router, DNS, one for future use, and the last address), so 4,096 − 5 = 4,091 usable.
  5. A /16 holds 65,536 ÷ 4,096 = 16 subnets of /20. We use 8, which ends at 10.0.127.255. 10.0.128.0 to 10.0.255.255 stays free for later.
Name AZ CIDR Range Kind
PublicSubnet1 ap-northeast-2a 10.0.0.0/20 10.0.0.0 – 10.0.15.255 public
PublicSubnet2 ap-northeast-2b 10.0.16.0/20 10.0.16.0 – 10.0.31.255 public
WebSubnet1 ap-northeast-2a 10.0.32.0/20 10.0.32.0 – 10.0.47.255 private
WebSubnet2 ap-northeast-2b 10.0.48.0/20 10.0.48.0 – 10.0.63.255 private
AppSubnet1 ap-northeast-2a 10.0.64.0/20 10.0.64.0 – 10.0.79.255 private
AppSubnet2 ap-northeast-2b 10.0.80.0/20 10.0.80.0 – 10.0.95.255 private
DBSubnet1 ap-northeast-2a 10.0.96.0/20 10.0.96.0 – 10.0.111.255 private
DBSubnet2 ap-northeast-2b 10.0.112.0/20 10.0.112.0 – 10.0.127.255 private

The examples use the Seoul Region (ap-northeast-2). Any Region works, for example Mumbai (ap-south-1, AZs ap-south-1a and ap-south-1b). Just keep everything in one Region. The binary method for CIDRs is in What Is AWS VPC and How to Calculate CIDR IP Ranges.

6. The security group plan

What: five groups make the chain, and two small helper groups are only for setup.

Security group Attach it to Inbound rule Why this source
WebAlbSG the internet-facing ALB HTTP 80 from 0.0.0.0/0 users come from anywhere on the internet
WebEc2SG nginx servers (web launch template) HTTP 80 from WebAlbSG nobody can skip the ALB and hit nginx directly
AppAlbSG the internal ALB HTTP 80 from WebEc2SG only nginx servers may send PHP requests
AppEc2SG PHP servers (app launch template) HTTP 80 from AppAlbSG only the internal ALB (and its health checks) reaches PHP
DbSG the RDS database MySQL/Aurora 3306 from AppEc2SG only PHP servers may open a database connection
JumpSG JumpServer SSH 22 from My IP you, and only you, log in for setup
DevSG the two development servers SSH 22 and HTTP 80 from My IP you SSH in and test in your browser

During setup DbSG also allows 3306 from JumpSG, so the JumpServer can create the database. You remove that rule when the app works.

Why a security group as the source, not an IP: Auto Scaling servers get new private IPs every time they are replaced. A rule "from WebEc2SG" means "from any server that carries WebEc2SG", so it keeps working however many servers come and go. An IP rule would break after the first replacement.

Why one group per role, and never shared: if the ALB and the nginx servers shared one group that allows 80 from anywhere, a user who learns a server's address could skip the load balancer. Separate groups with chained sources make each hop explicit.

Two facts that save debugging time:

  1. Security groups are stateful: when a request is allowed in, its reply is allowed out automatically. You only write inbound rules here; the default outbound rule (all traffic) stays.
  2. A rule is checked on the destination's group. "nginx can't reach PHP" is fixed on AppAlbSG/AppEc2SG, not on WebEc2SG.

More on security groups and NACLs: Security Group vs NACL in AWS.

7. The Auto Scaling numbers, worked out

Why: both tiers run in Auto Scaling groups so that the number of servers follows the load and broken servers are replaced. These are the values used in the lab and what each one does.

Desired 2, min 1, max 4

  1. Desired 2: at the start the group runs 2 servers. With 2 subnets in 2 AZs, that is one per AZ.
  2. Min 1: the policy may scale in to 1 when there is no traffic, but never to 0, so the app never disappears.
  3. Max 4: the policy may never go above 4. This is your bill limit: a traffic spike or a bug can't start 50 servers.
  4. Example: you set desired to 6 by hand. The console refuses, because desired must be between min and max.

Target tracking on average CPU, target 20%

Target tracking keeps the group's average CPU near the target by changing desired capacity, roughly in proportion:

  1. 2 servers at 50% average CPU: 2 × 50 ÷ 20 = 5 servers needed, but max is 4, so the group grows to 4.
  2. 4 servers at 10%: 4 × 10 ÷ 20 = 2, so the group shrinks back to 2.
  3. 2 servers at 4%: 2 × 4 ÷ 20 = 0.4, rounded up to 1, and min is 1, so it shrinks to 1. This is why, with no traffic at all, you may see the group remove a server on its own.
  4. Scale-out reacts within a few minutes; scale-in waits longer (about 15 minutes of low CPU) so a short dip does not remove servers.
  5. Why 20: it is low on purpose so you can watch scaling happen in a lab with no real users. For production pick a realistic value such as 50 to 70.

Instance warmup 60 s: for 60 s after a new server starts, its CPU is not counted in the group average. A fresh server is busy booting; counting it would make the policy add even more servers.

Health check grace period 60 s: for 60 s after a server launches, the group ignores failed health checks, so it does not kill a server that is still starting nginx. Our AMIs boot and start nginx in well under a minute; if your app needs longer to start, raise it.

ELB health checks on: by default the group only checks that the EC2 instance is running. With ELB health checks on, a server whose web page fails the load balancer's health check is also treated as unhealthy and replaced. That is what makes the tier fault tolerant.

Availability Zone distribution: Balanced best effort

Normally the group keeps the same number of servers in each AZ. Sometimes an AZ temporarily runs out of a particular instance type. It happens on peak days such as Diwali, Dussehra, Eid, Christmas or 31 December, when many companies scale out at the same time.

  1. Balanced only: the group keeps trying to launch in the AZ that has no capacity. The launch keeps failing and your app runs short of servers exactly on the busiest day.
  2. Balanced best effort: if the launch fails in one AZ, the group launches in the other healthy AZ instead. The split is uneven for a while (for example 3 + 1), but the app gets the capacity it needs, and the group rebalances later.
  3. So keep Balanced best effort.

Instance maintenance policy: No policy. This decides what happens during replacements such as an instance refresh: launch the new server before terminating the old one (keeps capacity, costs a little more for a moment) or terminate first (saves money, briefly fewer servers). "No policy" keeps the default behaviour, which is fine for the lab; for production, "Launch before terminating" keeps full capacity.

Group metrics in CloudWatch: ticking it sends group metrics such as desired, in-service and pending instances to CloudWatch every minute, so you can graph what the group did.

More on launch templates and min, max and desired: What Is Auto Scaling in AWS? Min, Max and Desired Explained.

Classroom line

The fun is not in doing it, the fun is in understanding it.

8. Passwords: hardcoded vs Secrets Manager

Why a password in the code is a problem

Why it came up. When you create the RDS database you choose how its master password is managed, and the PHP code needs that password to connect.

Background. In this lab the password is written inside submit.php and baked into phpami. That is fine for learning, but think of a real app with 1,000 PHP servers: every server holds the password; if one server is hacked, or a developer who knew it leaves, the database is exposed; and changing the password means a new AMI and an instance refresh, about an hour during which some servers still have the old password and their database calls fail.

How it links to the 3-tier build. AWS Secrets Manager stores the password as a secret (a key-value pair, encrypted with AWS KMS). The PHP code keeps only the secret's name and reads the value at runtime with the AWS SDK (GetSecretValue), using an IAM role on the server. Change the password once in Secrets Manager and every server gets the new value on its next read. Secrets Manager can also rotate the password on a schedule (as often as every 4 hours), so no human ever needs to know it. When you create RDS, the option Managed in AWS Secrets Manager does this for the master password (rotation every 7 days by default). Two things it needs: an IAM role with secretsmanager:GetSecretValue, and a network path from the private servers to Secrets Manager (a VPC interface endpoint or a NAT gateway).

Whiteboard sketch of AWS Secrets Manager: a key named mypassword with its value, the AWS SDK on the app fetching it, RDS on the right, and password rotation

Whiteboard: AWS Secrets Manager: the code holds only the key name, the AWS SDK fetches the value at runtime, and the password can be rotated without touching the servers.

Classroom line

As soon as the password changes here, the same new password should automatically reach the app.

Secrets pushed to GitHub

Why it came up. Right after the hardcoded password: the same mistake happens when code with a password or an AWS access key is pushed to a public GitHub repository.

Background. Bots scan public repositories all the time and find keys within minutes; stolen AWS keys are typically used to start many large servers for crypto-mining, which lands on your bill. AWS also scans public GitHub for exposed access keys; when it finds one it attaches a quarantine policy (AWSCompromisedKeyQuarantine) to that user to limit what the key can do, and emails you. Deleting the commit is not enough, because the key stays in the history and in forks. The fix is to deactivate and rotate the key at once, then check your account for anything you didn't create.

How it links to the 3-tier build. Never commit submit.php with a real password to a public repository. Keep secrets out of code (Secrets Manager, or at least a config file listed in .gitignore), and turn on GitHub secret scanning and push protection. On servers, use an IAM role instead of access keys, so there is no key to leak.

9. Hands-on lab: the whole build, step by step

Why this order: Parts A to D build the network and all security rules first, because every later resource asks for a VPC, a subnet or a security group in its form. Parts E to G build the database and then the app tier on top of it, so each tier is tested against a working tier below it. Parts H to J turn the tested servers into Auto Scaling groups behind load balancers. Part K tests and locks down, Part L shows how to change servers safely, and Part M removes everything.

Before you start

  • An AWS account you can sign in to, and about three hours. Load balancers, RDS and the servers cost a little money while they run; do Part M the same day.
  • One Region for everything. The lab uses Asia Pacific (Seoul) ap-northeast-2 with AZs 2a and 2b; Mumbai ap-south-1 works the same with 1a and 1b.
  • A terminal with OpenSSH (PowerShell on Windows 10/11, Terminal on Mac or Linux).
  • Values in angle brackets, such as <rds-endpoint>, are yours to copy from the console.
Whiteboard list of the five network steps

Whiteboard: the five network steps in order: create the custom VPC, create 8 subnets, create and attach the internet gateway, create a route table with the internet route, associate it with the public subnets

Part A: the Region and the VPC

  1. Choose one Region for the whole build

    • Do: Sign in to the AWS console. Top right, open the Region menu and pick Asia Pacific (Seoul) ap-northeast-2 (or the Region you chose).
    • Check: The Region name shows top right on every page you open from now on.
    • Why: Every resource here (VPC, subnets, security groups, RDS, AMIs, load balancers) belongs to one Region and can only be used with resources in the same Region. Mixing Regions is the most common reason something "is not in the list".
  2. Open the Create VPC page

    • Do: Search VPC > Your VPCs > Create VPC.
    • Why: The VPC is your own private network in AWS; everything else in this build lives inside it.
  3. Resources to create: VPC only

    • Do: Select VPC only.
    • Why: "VPC and more" would also create subnets, route tables and gateways with its own names and sizes. Building each piece yourself is how you learn what each one does, and it gives exactly the eight subnets of our plan.
  4. Name tag: ThreeTierVpc

    • Do: Type ThreeTierVpc in Name tag.
    • Why: Every account already has a default VPC in each Region. A clear name stops you from picking the default VPC by mistake in the 20 dropdowns that follow.
  5. IPv4 CIDR: IPv4 CIDR manual input, 10.0.0.0/16

    • Do: Pick IPv4 CIDR manual input and type 10.0.0.0/16.
    • Why: A /16 gives 65,536 private addresses, room for sixteen /20 subnets. 10.0.0.0/8 is a private range (RFC 1918): it is never routed on the internet, so it can't clash with public addresses.
  6. IPv6 CIDR block: No IPv6 CIDR block

    • Do: Leave No IPv6 CIDR block selected.
    • Why: The app works on IPv4 only. Every extra address family is one more set of routes and rules to secure.
  7. Tenancy: Default

    • Do: Leave Default and click Create VPC.
    • You should see: A green banner "You successfully created vpc-… / ThreeTierVpc".
    • Why: Default runs your servers on shared hardware, still fully isolated from other customers. Dedicated tenancy puts every instance on single-customer hardware and costs much more.
  8. Look at the Resource map

    • Do: On the VPC page open the Resource map tab.
    • You should see: Subnets (0), Route tables (1).
    • Why: AWS created a main route table with the VPC (plus a default network ACL and a default security group). That route table has only the local route, so anything that uses it is private. Knowing this explains why new subnets start private.
  9. Name the main route table ThreeTierMainRouteTable

    • Do: Route tables: find the row with VPC ThreeTierVpc and Main: Yes, click the pencil in Name, type ThreeTierMainRouteTable, Save.
    • Why: Soon this VPC has two route tables, and the account has more from other VPCs. Unnamed route tables all look like rtb-0abc…, and choosing the wrong one makes a subnet public or private by mistake.

Part B: eight subnets

  1. Open Create subnet and pick VPC ID: ThreeTierVpc

    • Do: VPC > Subnets > Create subnet, VPC ID = ThreeTierVpc.
    • Why: This dropdown decides where all eight subnets go. If you leave it on the default VPC, all eight are created there and nothing later will find them.
    • If you see the subnets under the default VPC: the VPC dropdown was not changed. Delete those subnets (Actions > Delete subnet) and create them again in ThreeTierVpc.
  2. Subnet 1: PublicSubnet1, ap-northeast-2a, 10.0.0.0/20

    • Do: Subnet name PublicSubnet1, Availability Zone ap-northeast-2a, IPv4 VPC CIDR block 10.0.0.0/16, IPv4 subnet CIDR block 10.0.0.0/20.
    • Why: The first /20 block of the VPC, in AZ a. It will hold the internet-facing ALB, the JumpServer and the development servers.
  3. Add new subnet: PublicSubnet2, ap-northeast-2b, 10.0.16.0/20

    • Do: Click Add new subnet and fill in these values.
    • Why: An internet-facing ALB needs a subnet in at least two AZs, and the second public subnet in AZ b gives it that. The block starts at 16 because a /20 covers 16 values of the third octet.
  4. WebSubnet1, ap-northeast-2a, 10.0.32.0/20

    • Do: Add new subnet with these values.
    • Why: Private home of the nginx servers in AZ a.
  5. WebSubnet2, ap-northeast-2b, 10.0.48.0/20

    • Do: Add new subnet with these values.
    • Why: The web tier in AZ b, so the web tier survives the loss of AZ a.
  6. AppSubnet1, ap-northeast-2a, 10.0.64.0/20

    • Do: Add new subnet with these values.
    • Why: Private home of the PHP servers and the internal ALB in AZ a.
  7. AppSubnet2, ap-northeast-2b, 10.0.80.0/20

    • Do: Add new subnet with these values.
    • Why: The app tier in AZ b.
  8. DBSubnet1, ap-northeast-2a, 10.0.96.0/20

    • Do: Add new subnet with these values.
    • Why: The database's subnet in AZ a.
  9. DBSubnet2, ap-northeast-2b, 10.0.112.0/20, then Create subnet

    • Do: Add the last subnet and click Create subnet.
    • You should see: "You have successfully created 8 subnets".
    • Why: RDS requires subnets in at least two AZs, even for a single instance, so it can place a standby or move the database there later.
    • If you see "Failed to request credentials" or "NetworkError when attempting to fetch resource": the console session expired or one request failed while the long form was open. Refresh the page, sign in again if asked, check the Subnets list for half-created subnets, and fill the form again. See section 10.
  10. Check the eight subnets

    • Do: Subnets, filter VPC = ThreeTierVpc.
    • Check: 8 subnets, 4 in ap-northeast-2a and 4 in ap-northeast-2b, CIDRs as in the table in section 5, Available IPv4 addresses 4091 each.
    • Why: One wrong CIDR or AZ now breaks the ALB, the subnet group or the Auto Scaling groups much later, where it is far harder to see.
  11. Resource map: all eight use the main route table

    • Do: Your VPCs > ThreeTierVpc > Resource map.
    • You should see: All 8 subnets connected to ThreeTierMainRouteTable.
    • Why: A new subnet is automatically associated with the main route table. Right now that table has only the local route, so all eight subnets are private. That is a safe default: nothing is public until you make it public.
AWS console Create subnet form

Console: Create subnet: VPC ThreeTierVpc, subnet name, Availability Zone and the /20 IPv4 subnet CIDR block inside 10.0.0.0/16

Part C: internet gateway and route tables

  1. Create the internet gateway ThreeTierIGW

    • Do: VPC > Internet gateways > Create internet gateway, Name tag ThreeTierIGW, Create internet gateway.
    • You should see: State: Detached.
    • Why: The internet gateway is the VPC's door to the internet. Without it the internet-facing ALB can't receive a single request.
  2. Attach it: Actions > Attach to VPC > ThreeTierVpc

    • Do: Actions > Attach to VPC, choose ThreeTierVpc, Attach internet gateway.
    • You should see: State: Attached.
    • Why: A new internet gateway is not connected to any VPC. A detached gateway can't be picked as a route target.
  3. Create ThreeTierPublicRouteTable

    • Do: Route tables > Create route table, Name ThreeTierPublicRouteTable, VPC ThreeTierVpc, Create route table.
    • Why: The public subnets need their own route table. The main table stays private, so any subnet you add later is private until you decide otherwise. A route table is not public because of its name; it becomes public only when it has a route to an internet gateway.
  4. Add the route 0.0.0.0/0 → ThreeTierIGW

    • Do: Routes tab > Edit routes > leave the local row as it is > Add route: Destination 0.0.0.0/0, Target Internet Gateway > ThreeTierIGW > Save changes.
    • You should see: Two routes: 10.0.0.0/16 local and 0.0.0.0/0 igw-…, both Active.
    • Why: 10.0.0.0/16 → local carries all traffic inside the VPC (ALB to nginx, PHP to RDS) and must stay. 0.0.0.0/0 means "every other address", and sending it to the internet gateway is what makes a subnet public.
    • If you see an empty list under Internet Gateway: the gateway is still detached. Cancel, do the attach step, then come back.
  5. Associate only the two public subnets

    • Do: Subnet associations tab > Edit subnet associations > tick PublicSubnet1 and PublicSubnet2 only > Save associations.
    • Why: A subnet uses exactly one route table. Moving PublicSubnet1 and PublicSubnet2 onto the public table makes those two public; the other six stay on the private main table.
  6. Check the Resource map

    • Do: Your VPCs > ThreeTierVpc > Resource map.
    • Check: ThreeTierPublicRouteTable → PublicSubnet1, PublicSubnet2 → ThreeTierIGW; ThreeTierMainRouteTable → the 6 Web, App and DB subnets.
    • Why: This picture is the network plan of section 2. If it looks different now, every later step will misbehave.
AWS console Edit routes with a 0.0.0.0/0 route to the internet gateway

Console: Edit routes of ThreeTierPublicRouteTable: the local route stays as it is, and 0.0.0.0/0 goes to the internet gateway

AWS console Edit subnet associations with the two public subnets selected

Console: Edit subnet associations: only PublicSubnet1 and PublicSubnet2 are ticked for the public route table

AWS console VPC resource map

Console: Resource map of ThreeTierVpc: eight subnets in two AZs, the main and the public route table, and the internet gateway

Classroom line

Naming a subnet "public" does not make it public; the route to the internet gateway does.

Part D: all security groups first, then the rules

  1. Create WebAlbSG (no rules yet)

    • Do: EC2 > Security Groups > Create security group: Name WebAlbSG, Description Internet-facing ALB, VPC ThreeTierVpc, no inbound rules > Create security group.
    • Why: A security group belongs to one VPC and can only be attached to resources in that VPC. Creating all groups first lets every rule point to a group that already exists.
  2. Create WebEc2SG

    • Do: Same page: Name WebEc2SG, Description nginx web servers, VPC ThreeTierVpc, Create.
    • Why: The nginx servers get their own group, separate from the ALB, so the ALB can't be bypassed.
  3. Create AppAlbSG

    • Do: Name AppAlbSG, Description Internal ALB, VPC ThreeTierVpc, Create.
    • Why: The internal ALB is its own hop in the chain and needs its own group.
  4. Create AppEc2SG

    • Do: Name AppEc2SG, Description PHP app servers, VPC ThreeTierVpc, Create.
    • Why: The PHP servers' group. DbSG will trust exactly this group.
  5. Create DbSG

    • Do: Name DbSG, Description RDS MySQL, VPC ThreeTierVpc, Create.
    • Why: The database's group, chosen when you create RDS.
  6. Create JumpSG with SSH 22 from My IP

    • Do: Name JumpSG, Description Jump server, VPC ThreeTierVpc, Add rule: Type SSH, Source My IP > Create.
    • Why: Only your own public IP may open SSH to the JumpServer. Port 22 open to 0.0.0.0/0 is scanned by bots all day and invites password and key attacks.
  7. Create DevSG with SSH 22 and HTTP 80 from My IP

    • Do: Name DevSG, Description Development servers, VPC ThreeTierVpc, rules: SSH from My IP, HTTP from My IP > Create.
    • Why: The two development servers need SSH for setup and HTTP so you can test pages in your browser, but only from you.
  8. WebAlbSG inbound: HTTP 80 from Anywhere-IPv4 0.0.0.0/0

    • Do: Select WebAlbSG > Inbound rules > Edit inbound rules > Add rule: Type HTTP, Source Anywhere-IPv4 > Save rules.
    • Why: The public ALB is the one thing that must accept every user on the internet.
  9. WebEc2SG inbound: HTTP 80 from WebAlbSG

    • Do: WebEc2SG > Edit inbound rules > Add rule: Type HTTP, Source Custom, start typing WebAlbSG and pick it from Security Groups > Save rules.
    • Why: nginx accepts traffic only from the internet-facing ALB, including its health checks. A user who finds a server's address still can't reach it.
  10. AppAlbSG inbound: HTTP 80 from WebEc2SG

    • Do: AppAlbSG > Edit inbound rules > HTTP, Source Custom WebEc2SG > Save rules.
    • Why: Only nginx servers may send PHP requests to the internal ALB.
  11. AppEc2SG inbound: HTTP 80 from AppAlbSG

    • Do: AppEc2SG > Edit inbound rules > HTTP, Source Custom AppAlbSG > Save rules.
    • Why: PHP servers accept only the internal ALB. Its health checks also come from AppAlbSG, so they pass too.
  12. DbSG inbound: MySQL/Aurora 3306 from AppEc2SG, and from JumpSG for setup

    • Do: DbSG > Edit inbound rules > Add rule: Type MYSQL/Aurora, Source Custom AppEc2SG; Add rule: MYSQL/Aurora, Source Custom JumpSG > Save rules.
    • Why: Only PHP servers may connect to the database. The JumpSG rule exists only so you can create the database and table, and you remove it at the end. Port 3306 is never opened to 0.0.0.0/0.
  13. Read the chain once, top to bottom

    • Do: Security Groups, filter VPC ID = ThreeTierVpc, open each group's Inbound rules.
    • Check: WebAlbSG ← 0.0.0.0/0:80, WebEc2SG ← WebAlbSG:80, AppAlbSG ← WebEc2SG:80, AppEc2SG ← AppAlbSG:80, DbSG ← AppEc2SG:3306 and JumpSG:3306, exactly as in Figure 3.
    • Why: Two minutes of checking here saves an hour of chasing timeouts later.
AWS console Edit inbound rules with a security group as source

Console: Edit inbound rules: HTTP 80 with the source chosen from the list of security groups, not typed as an IP

Part E: the DB subnet group and RDS MySQL

  1. Create the DB subnet group ThreeTierDBSubnetGroup

    • Do: Search RDS (Aurora and RDS) > Subnet groups > Create DB subnet group: Name ThreeTierDBSubnetGroup, Description ThreeTierDBSubnetGroup, VPC ThreeTierVpc.
    • Why: RDS does not take a subnet directly; it takes a subnet group, the list of subnets it may place the database in. The group is created in the VPC you pick, and the database can only be created in that VPC.
  2. AZs ap-northeast-2a, ap-northeast-2b; subnets DBSubnet1 and DBSubnet2 only, then Create

    • Do: Availability Zones: tick both. Subnets: tick DBSubnet1 (10.0.96.0/20) and DBSubnet2 (10.0.112.0/20) > Create.
    • You should see: threetierdbsubnetgroup in the list (RDS stores the name in lower case), Status Complete.
    • Why: The subnet list shows only subnets in the AZs you tick. Picking only the two DB subnets guarantees the database can never land in a public or app subnet.
    • If you see your DB subnets missing from the list: the wrong VPC is selected, or their AZ is not ticked. Fix the VPC or AZ choice above the list.
  3. Databases > Create database: Standard create, engine MySQL

    • Do: Choose Standard create, Engine type MySQL, keep the default engine version.
    • Why: Standard create shows every network setting. The quick option fills them with defaults, and the default VPC is exactly what we must avoid.
  4. Templates: Free tier

    • Do: Select Free tier.
    • Why: It limits the choices to a small free-tier instance so the lab does not cost money.
  5. Availability: Single-AZ DB instance

    • Do: Keep Single-AZ DB instance deployment (1 instance).
    • Why: Multi-AZ keeps a standby copy in the second AZ with synchronous replication and fails over automatically, usually in one to two minutes. That is what a real highly available app needs, but it roughly doubles the database cost, so the lab uses one instance. In a company, production databases run Multi-AZ.
  6. Identifier database-1, master username root

    • Do: DB instance identifier database-1, Master username root.
    • Why: The identifier names the database in the console and starts its endpoint name. The master username is what you type in mysql -u and in submit.php; the console suggests admin, and any allowed name works as long as you use the same one everywhere.
  7. Credentials management: Self managed, type the password twice

    • Do: Select Self managed, fill Master password and Confirm master password. Write it down; you need it three more times.
    • Why: Self managed keeps the lab simple: you type the password and put it in submit.php. RDS master passwords are 8 to 41 characters and may not contain /, ', ", @ or a space. For real apps choose Managed in AWS Secrets Manager (section 8).
    • If you see "contains invalid characters": remove @, /, quotes and spaces from the password.
  8. Instance class and storage: keep the free-tier defaults

    • Do: Leave Instance configuration and Storage as they are.
    • Why: db.t3.micro or db.t4g.micro with 20 GiB of storage is plenty for a sign-up table and stays inside the free tier.
  9. Compute resource: Don't connect to an EC2 compute resource

    • Do: Under Connectivity select Don't connect to an EC2 compute resource.
    • Why: "Connect to an EC2 compute resource" ties the database to one particular instance by creating security group rules for it. Our app servers are created and replaced by an Auto Scaling group, so there is no single instance to bind to; DbSG already trusts the whole AppEc2SG group.
  10. VPC ThreeTierVpc, DB subnet group threetierdbsubnetgroup

    • Do: Network type IPv4, Virtual private cloud (VPC) ThreeTierVpc, DB subnet group threetierdbsubnetgroup.
    • Why: The VPC of a database can't be changed after it is created. If it ends up in the wrong VPC, the only fix is to delete it and create it again.
  11. Public access: No

    • Do: Select No.
    • Why: With No, the database gets no public IP and its endpoint resolves only to a private address, so it can be reached only from inside the VPC.
  12. Security group: Choose existing, only DbSG

    • Do: VPC security group (firewall): Choose existing, remove default, add DbSG. Availability Zone: No preference. Database port (Additional configuration): 3306.
    • Why: The VPC's default security group would be attached otherwise. DbSG is the last link of our chain.
  13. Create database and wait for Available

    • Do: Click Create database. Close the add-ons pop-up if one appears.
    • You should see: Banner "Creating database database-1"; Status Creating → Backing-up → Available, usually 5 to 15 minutes.
    • Why: RDS needs several minutes to create the instance, its storage and the first backup.
    • If you see "NetworkError when attempting to fetch resource": one console request failed. Open Databases; if database-1 is not there, go back, check every setting above and click Create database again.
  14. Copy the endpoint

    • Do: Databases > database-1 > Connectivity & security: copy Endpoint (database-1.xxxxxxxxxxxx.ap-northeast-2.rds.amazonaws.com); Port 3306.
    • Why: The endpoint is a DNS name that always points to the current primary database, even after a Multi-AZ failover. An IP address would break; the endpoint never does.
AWS console Create DB subnet group subnet selection

Console: Create DB subnet group: both AZs ticked and only the two DB subnets selected

AWS console RDS credentials settings

Console: Credentials settings: master username root and Self managed password

Classroom line

For real high availability you need Multi-AZ; we skip it here only because it is paid.

Part F: the JumpServer, the database and the table

  1. Launch JumpServer: Amazon Linux 2023, t3.micro

    • Do: EC2 > Instances > Launch instances: Name JumpServer, AMI Amazon Linux 2023 (64-bit x86), Instance type t3.micro.
    • Why: The database is private, so your laptop can't reach it. A small server in the public subnet, inside the same VPC, can. Amazon Linux 2023 uses yum/dnf; t3.micro is free-tier eligible in most accounts and offered in every AZ of most Regions.
  2. Key pair: Create new key pair ThreeTierKey, RSA, .pem

    • Do: Create new key pair: Name ThreeTierKey, Type RSA, Format .pem > Create key pair. The browser downloads ThreeTierKey.pem.
    • Why: AWS keeps only the public half of the key; the downloaded .pem file is the only copy of the private half. .pem works with OpenSSH on Windows 10/11, Mac and Linux.
  3. Network settings: ThreeTierVpc, PublicSubnet1, Auto-assign public IP: Enable, security group JumpSG

    • Do: Network settings > Edit: VPC ThreeTierVpc, Subnet PublicSubnet1, Auto-assign public IP Enable, Firewall Select existing security group > JumpSG.
    • Why: A server is reachable from the internet only when all three are true: its subnet routes to the internet gateway, it has a public IP, and its security group allows the port. Leave out the public IP and you can't SSH in from your laptop.
  4. Launch instance and copy its public IP

    • Do: Launch instance > open JumpServer in Instances.
    • You should see: Instance state Running, Status check 2/2 checks passed, a Public IPv4 address.
    • Why: You SSH to this address in the next steps.
  5. Windows: give the key file read access for you only

    • Do: Right-click ThreeTierKey.pem > Properties > Security > Advanced > Disable inheritance > Remove all inherited permissions from this object > Add > Select a principal > type your Windows user name (the name after C:\Users\ in PowerShell) > Check Names > OK > tick only Read > OK > Apply > OK.
    • Command:
      icacls .\ThreeTierKey.pem /inheritance:r
      icacls .\ThreeTierKey.pem /grant:r "$($env:USERNAME):(R)"
    • Check: The same result from PowerShell in the Downloads folder, with the two icacls commands above.
    • Why: OpenSSH refuses a private key that other accounts on the computer can read. A downloaded file inherits the Downloads folder's permissions, which include other users and groups. This is the Windows version of chmod 400 on Mac and Linux: read for the owner, nothing for anyone else.
  6. Mac or Linux: chmod 400

    • Command:
      chmod 400 ~/Downloads/ThreeTierKey.pem
    • Why: 4 = read for the owner, 0 for group and others: exactly what SSH wants for a private key.
  7. SSH to the JumpServer

    • Command:
      ssh -i .\Downloads\ThreeTierKey.pem ec2-user@<JumpServer-public-IP>
    • You should see: Type yes to the fingerprint question; then the Amazon Linux 2023 banner and the prompt [ec2-user@ip-10-0-… ~]$.
    • Why: -i tells SSH which private key to use; ec2-user is the default user on Amazon Linux.
    • If you see "WARNING: UNPROTECTED PRIVATE KEY FILE!" or "Permission denied (publickey)": the key file is readable by others; redo the permission step, and check you used ec2-user.
    • If you see "Connection timed out": your public IP changed since you made JumpSG (home and mobile internet IPs change), or the server has no public IP or is not in a public subnet. Edit JumpSG and pick My IP again.
  8. Install the MySQL client only: mariadb105

    • Command:
      sudo yum install mariadb105 -y
    • You should see: Complete! at the end.
    • Why: The database server runs on RDS; the JumpServer only needs the client program. Without it, mysql gives "command not found". The mariadb105-server package would also install a database server you don't need.
  9. Connect to RDS

    • Command:
      mysql -h <rds-endpoint> -u root -p
    • You should see:
      Enter password:
      Welcome to the MariaDB monitor.  Commands end with ; or \g.
      Server version: 8.4.x Source distribution
      MySQL [(none)]>
    • Why: -h gives the host; without it mysql tries the local machine, where no database runs. -p asks for the password without showing it.
    • If you see it hangs and then "ERROR 2002" or "ERROR 2003 Can't connect": DbSG has no 3306 rule from JumpSG, or RDS was created with another security group. Fix the rule (Part D) or the database's security group (Modify).
    • If you see "Access denied for user": wrong user name or password. You can set a new one: RDS > database-1 > Modify > New master password > Continue > Apply immediately.
  10. create database insta;

    • Command:
      create database insta;
    • You should see:
      Query OK, 1 row affected
    • Why: One database for the app. Its name goes into submit.php.
  11. use insta;

    • Command:
      use insta;
    • You should see:
      Database changed
    • Why: Every following command works inside this database.
  12. Create the users table

    • Command:
      create table users (id int primary key auto_increment, name varchar(50), mobile varchar(20), password varchar(255));
    • You should see:
      Query OK, 0 rows affected
    • Why: id numbers every row by itself (auto_increment) and identifies it (primary key). mobile is text, not a number: numbers lose a leading 0 or +, and 10 digits overflow int. password stores a hash, never the real password; PHP's password_hash() produces 60 characters today and the PHP manual recommends room for 255.
  13. Check with desc users; and exit

    • Command:
      desc users;
      exit
    • You should see:
      | id       | int          | NO   | PRI | NULL | auto_increment |
      | name     | varchar(50)  | YES  |     | NULL |                |
      | mobile   | varchar(20)  | YES  |     | NULL |                |
      | password | varchar(255) | YES  |     | NULL |                |
      4 rows in set
      Bye
    • Why: Confirms the columns before the PHP code depends on them. The database tier is now finished.
Windows Advanced Security Settings dialog for the key file

Console: Advanced Security Settings of the .pem file: Disable inheritance, then "Remove all inherited permissions from this object"

Classroom line

What we do with chmod 400 on Linux is what we are doing here.

Terminal with the MySQL client connected to RDS

Console: the MySQL client on the JumpServer connected to the RDS endpoint

Part G: the PHP development server and phpami

  1. Launch phpdevelopmentserver in ThreeTierVpc

    • Do: Launch instances: Name phpdevelopmentserver, Amazon Linux 2023, t3.micro, key pair ThreeTierKey; Network settings > Edit: VPC ThreeTierVpc, Subnet PublicSubnet1, Auto-assign public IP Enable; Select existing security group: DevSG and AppEc2SG > Launch instance.
    • Check: Instance details show VPC ThreeTierVpc and both security groups.
    • Why: The App Auto Scaling group needs a launch template, and the template needs a ready AMI. This temporary server is where you build and test that AMI. Check the VPC twice: servers in two different VPCs can't talk to each other by default, so a PHP server in the default VPC can never reach RDS.
    • If you see the instance in the default VPC: an instance can't be moved to another VPC. Terminate it and launch again with the right VPC.
  2. Why two security groups on this server

    • Do: Nothing to click; read before you continue.
    • Why: AppEc2SG makes this server look exactly like a future app server to DbSG, so the database test here proves the real path. DevSG adds SSH and HTTP from your IP only, for the setup. The two rule sets simply add up.
  3. SSH in

    • Command:
      ssh -i .\Downloads\ThreeTierKey.pem ec2-user@<php-dev-public-IP>
    • Why: All software for the app tier is installed by hand once, here.
  4. Install nginx, PHP, php-fpm and the MySQL driver

    • Command:
      sudo yum install nginx php php-fpm php-mysqlnd -y
    • You should see: Complete!
    • Why: nginx receives the HTTP request from the internal ALB. nginx can't run PHP itself, so it hands .php files to php-fpm, which runs them. php-mysqlnd is the driver that lets PHP's mysqli talk to MySQL. Type package names with Tab completion instead of guessing the spelling.
    • If you see "No match for argument": refresh the package list with sudo yum update -y and type the name again with Tab completion.
  5. Start nginx

    • Command:
      sudo service nginx start
    • You should see:
      Redirecting to /bin/systemctl start nginx.service
    • Why: Installing a service does not start it.
  6. Start php-fpm

    • Command:
      sudo service php-fpm start
    • Why: Without php-fpm running, nginx has nobody to pass PHP to and returns 502.
  7. Enable nginx at boot

    • Command:
      sudo systemctl enable nginx
    • You should see:
      Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service → /usr/lib/systemd/system/nginx.service.
    • Why: Servers that the Auto Scaling group launches from this AMI boot with nobody logging in. A service that is started but not enabled is off after every boot, so every new server would fail its health check.
  8. Enable php-fpm at boot

    • Command:
      sudo systemctl enable php-fpm
    • Why: Same reason: nginx up but php-fpm down after boot means every PHP request returns 502.
  9. Check both services

    • Command:
      systemctl is-enabled nginx php-fpm
      systemctl is-active nginx php-fpm
    • You should see:
      enabled
      enabled
      active
      active
    • Why: A wrong state here is copied into every server made from the AMI.
  10. Go to the web root

    • Command:
      cd /usr/share/nginx/html
    • Why: On Amazon Linux nginx serves files from /usr/share/nginx/html.
  11. Test PHP alone with index.php

    • Do: Run the command, type <?php phpinfo(); ?>, save with Ctrl+O, Enter, Ctrl+X. Open http://<php-dev-public-IP>/index.php in your browser.
    • Command:
      sudo nano index.php
    • You should see: A purple "PHP Version 8.x" page.
    • Why: Test one link at a time. If phpinfo() shows, nginx → php-fpm works, and any later problem is in the app code or the database, not here.
    • If you see the file downloads, or "502 Bad Gateway": php-fpm is not running; start it and reload the page.
  12. Create health.php

    • Do: Type <?php http_response_code(200); echo "ok"; and save.
    • Command:
      sudo nano health.php
      curl -s http://localhost/health.php
    • You should see:
      ok
    • Why: The internal ALB needs one small page that returns 200 when PHP works. It must not touch the database: if the database had a short hiccup, a database-dependent health check would mark every PHP server unhealthy and the group would replace them all.
  13. Create submit.php

    • Do: Paste the code, put in your endpoint and password, save.
    • Command:
      sudo nano submit.php
    • Why: It reads the three form fields and inserts one row. $host is the RDS endpoint, not localhost, because the database runs on RDS. The prepared statement (? placeholders) stops SQL injection, and password_hash() stores a hash. It refuses anything that is not a POST before touching the database.
  14. The submit.php code

    • Command:
      <?php
      // submit.php: saves one sign-up from forms.html into RDS
      $host = "database-1.xxxxxxxxxxxx.ap-northeast-2.rds.amazonaws.com"; // your RDS endpoint
      $user = "root";
      $pass = "YOUR_DB_PASSWORD";
      $db   = "insta";
      if ($_SERVER["REQUEST_METHOD"] !== "POST") {
          http_response_code(405);
          exit("Send the form with POST");
      }
      try {
          $conn   = new mysqli($host, $user, $pass, $db);
          $name   = trim($_POST["name"] ?? "");
          $mobile = trim($_POST["mobile"] ?? "");
          $hash   = password_hash($_POST["password"] ?? "", PASSWORD_DEFAULT);
          $stmt = $conn->prepare("INSERT INTO users (name, mobile, password) VALUES (?, ?, ?)");
          $stmt->bind_param("sss", $name, $mobile, $hash);
          $stmt->execute();
          echo "User registered successfully!";
          $stmt->close();
          $conn->close();
      } catch (mysqli_sql_exception $e) {
          http_response_code(500);
          echo "Database error";
      }
    • Why: Every line here is explained in the step above; keep the file exactly in this order.
  15. Create a temporary forms.html for the test

    • Command:
      sudo nano forms.html
    • Why: submit.php reads POST fields. Opening its URL in a browser sends a GET with no fields, so you can't test it that way; a form sends name, mobile and password. (In companies, testers do this with written test cases.)
  16. The forms.html code

    • Command:
      <!DOCTYPE html>
      <html>
      <head><meta charset="utf-8"><title>Sign up</title></head>
      <body>
      <h2>Create your account</h2>
      <form action="submit.php" method="post">
        <p><label>Name <input type="text" name="name" required></label></p>
        <p><label>Mobile <input type="tel" name="mobile" required></label></p>
        <p><label>Password <input type="password" name="password" required></label></p>
        <p><button type="submit">Register</button></p>
      </form>
      </body>
      </html>
    • Why: action="submit.php" and method="post" send the three named fields to submit.php; the name attributes must match $_POST["name"], $_POST["mobile"] and $_POST["password"].
  17. Submit the form

    • Do: Open http://<php-dev-public-IP>/forms.html, fill the fields, click Register. In the terminal you can test the same thing with curl.
    • Command:
      curl -s -X POST -d "name=Test&mobile=9999999999&password=Test@1234" http://localhost/submit.php
    • You should see: "User registered successfully!"
    • Why: This proves PHP → RDS works through DbSG before you bake anything.
    • If you see the page spinning for about a minute and then "504 Gateway Time-out" or "Database error": DbSG does not allow 3306 from AppEc2SG, or this server does not carry AppEc2SG. The security group drops the connection packets silently, PHP waits for an answer that never comes, and nginx gives up with 504. Fix the DbSG rule or add AppEc2SG to the instance (Actions > Security > Change security groups).
    • If you see "Database error" at once: the endpoint, user, password or database name in submit.php is wrong; compare them with Part E and Part F.
  18. Check the row from the JumpServer

    • Command:
      mysql -h <rds-endpoint> -u root -p -e "select id, name, mobile, left(password,4) from insta.users;"
    • You should see: One row with your name and $2y$ in the last column.
    • Why: Proves the row really reached RDS and that the password is stored as a hash (it starts with $2y$).
  19. Remove forms.html and index.php from the PHP server

    • Command:
      sudo rm /usr/share/nginx/html/forms.html /usr/share/nginx/html/index.php
      ls /usr/share/nginx/html
    • Check: The list shows health.php and submit.php (plus nginx's own default files).
    • Why: HTML belongs to the web tier; the app servers only run PHP. phpinfo() shows your PHP configuration to anyone who opens it, which helps attackers. Keep submit.php and health.php.
  20. Create the AMI phpami

    • Do: Instances > select phpdevelopmentserver > Actions > Image and templates > Create image: Image name phpami, Image description nginx + php-fpm + submit.php + health.php, keep Reboot instance ticked > Create image.
    • You should see: Images > AMIs: phpami, Status Pending → Available after a few minutes.
    • Why: The AMI freezes this tested server so the Auto Scaling group can stamp out identical copies. Leave the reboot on: AWS stops writes for a moment so the disk snapshot is consistent. A clear name matters because you will have two AMIs and must pick the right one in each template.
    • If you see "Failed to load image creation. Try reloading the page.": a console glitch; reload the page and check the AMIs list before creating it again.
Browser showing the phpinfo page

Console: phpinfo() on the PHP development server: PHP runs through nginx and php-fpm

Part H: the app tier (launch template, Auto Scaling group, internal ALB)

  1. Create the launch template AppTierLaunchTemplate

    • Do: EC2 > Launch templates > Create launch template: Name AppTierLaunchTemplate, Template version description v1, tick Provide guidance to help me set up a template that I can use with EC2 Auto Scaling.
    • Why: A launch template is the recipe the Auto Scaling group follows every time it starts a server: AMI, instance type, key pair and security group. Ticking the Auto Scaling guidance makes the console warn you about settings a group can't use.
  2. AMI: My AMIs > phpami; type t3.micro; key pair ThreeTierKey

    • Do: Application and OS Images > My AMIs > Owned by me > phpami; Instance type t3.micro; Key pair ThreeTierKey.
    • Why: phpami already has the software, the files and the enabled services, so a new server is ready the moment it boots. The key pair lets you SSH in for debugging later.
  3. Network: Subnet: Don't include in launch template, security group AppEc2SG

    • Do: Network settings: Subnet Don't include in launch template; Firewall Select existing security group > AppEc2SG; leave Advanced network configuration empty > Create launch template.
    • You should see: "Successfully created AppTierLaunchTemplate".
    • Why: The Auto Scaling group chooses the subnets, one per AZ. A subnet fixed inside the template would pin every server to one AZ. The security group belongs in the template because every server of this tier needs the same one.
  4. Create the Auto Scaling group AppTierASG from AppTierLaunchTemplate

    • Do: EC2 > Auto Scaling groups > Create Auto Scaling group: Name AppTierASG, Launch template AppTierLaunchTemplate, Version Default (1) > Next.
    • Why: The group keeps the right number of servers running, spreads them over AZs, replaces broken ones and adds or removes servers with the load. You never launch these servers by hand.
  5. Network: ThreeTierVpc, subnets AppSubnet1 and AppSubnet2

    • Do: VPC ThreeTierVpc, Availability Zones and subnets AppSubnet1 and AppSubnet2.
    • Why: Two subnets in two AZs: if one AZ fails, the group starts servers in the other one.
    • If you see "The requested instance type is not supported in the requested Availability Zone": not every instance type exists in every AZ. Keep t3.micro, or pick another type in a new template version (section 10).
  6. AZ distribution: Balanced best effort

    • Do: Availability Zone distribution: Balanced best effort > Next.
    • Why: Balanced only waits and retries in the same AZ when that AZ has no capacity. Balanced best effort launches in the other AZ instead, so the app gets its server now and the group rebalances later. When everyone launches servers at the same time, like shopping sites in a festival sale, one AZ can run out of a type; best effort keeps you serving.
  7. Attach to a new load balancer: Application Load Balancer AppTierInternalALB, Internal

    • Do: Load balancing: Attach to a new load balancer, Application Load Balancer, Load balancer name AppTierInternalALB, Load balancer scheme Internal.
    • Why: The wizard creates the load balancer, its listener and its target group in one go and registers the group's servers in it. Internal means the load balancer gets only private IPs and its DNS name resolves only inside the VPC: only the web tier can reach it.
  8. Listener HTTP 80, Create a target group AppTierTG

    • Do: Listeners and routing: Protocol HTTP, Port 80, Default routing Create a target group, New target group name AppTierTG.
    • Why: The listener is the port the load balancer accepts; the target group is the list of servers it forwards to and health-checks.
  9. Turn on Elastic Load Balancing health checks, grace period 60

    • Do: Health checks: tick Turn on Elastic Load Balancing health checks, Health check grace period 60 > Next.
    • Why: By default the group replaces a server only when the EC2 machine itself is broken. With ELB health checks on, it also replaces a server whose app has stopped answering. The grace period gives a new server 60 seconds to boot before its checks count.
  10. Group size: Desired 2, Min 1, Max 4

    • Do: Desired capacity 2; Min desired capacity 1; Max desired capacity 4.
    • Why: Desired 2 gives one server per AZ. Min 1 is the floor scaling can never go under; Max 4 caps the bill and your vCPU quota.
  11. Scaling: Target tracking, Average CPU utilization 20, warmup 60

    • Do: Automatic scaling: Target tracking scaling policy, Metric type Average CPU utilization, Target value 20, Instance warmup 60.
    • Why: Target tracking adds servers when the average CPU of the group goes above 20% and removes them when it falls well below. 20 is low on purpose so you can see scaling in a lab; production values are often 50 to 70. Warmup tells the group not to count a new server's CPU for its first 60 seconds.
  12. Maintenance policy: No policy; tick Enable group metrics collection

    • Do: Instance maintenance policy No policy; Additional settings: tick Enable group metrics collection within CloudWatch > Next > Next (no notifications).
    • Why: No policy keeps the default replacement behaviour. Group metrics show the number of servers over time in the Monitoring tab, which is how you watch scaling happen.
  13. Tag Name = AppTierServer, then Create Auto Scaling group

    • Do: Add tag: Key Name, Value AppTierServer > Next > review > Create Auto Scaling group.
    • You should see: Two AppTierServer instances, one in each AZ, after a minute or two.
    • Why: Without the tag every server of the group shows an empty name in the Instances list and you can't tell the tiers apart. "Tag new instances" is ticked by default.
    • If you see "You have requested more vCPU capacity than your current vCPU limit" in the group's Activity tab: the account's On-Demand vCPU quota is full. Terminate servers you no longer need, or request more (section 10).
  14. Give AppTierInternalALB its own security group

    • Do: EC2 > Load balancers > AppTierInternalALB > Security tab > Edit: remove every group, add AppAlbSG > Save changes.
    • Why: The wizard attaches the launch template's security group to the new load balancer. Our chain needs AppAlbSG on the load balancer, otherwise the server security group, which trusts only the load balancer's group, blocks every forwarded request.
  15. Set the health check of AppTierTG to /health.php

    • Do: EC2 > Target groups > AppTierTG > Health checks tab > Edit: Health check path /health.php; Advanced health check settings: Healthy threshold 2, Unhealthy threshold 2, Timeout 2, Interval 5, Success codes 200 > Save changes.
    • Why: The target group checks / by default. /health.php proves the part of this tier that matters answers with 200. Healthy 2 and Unhealthy 2 at a 5-second interval mean a server joins or leaves the target group within about 10 seconds; the timeout (2 s) must be shorter than the interval.
  16. Wait for two Healthy targets in AppTierTG

    • Do: AppTierTG > Targets tab, refresh.
    • You should see: 2 Healthy, one target in each AZ.
    • Why: Only healthy targets receive traffic. If both are healthy, this tier works end to end through its load balancer.
    • If you see Unhealthy, "Health checks failed" or "Request timed out": the server security group does not allow port 80 from the load balancer's security group, or the load balancer still has the wrong group (previous step).
    • If you see "Health checks failed with these codes: [404]": /health.php is not in the AMI. Fix the file on a dev server, make a new AMI and a new template version (Part L).
    • If you see targets that are Unhealthy and get replaced again and again: nginx or php-fpm was not enabled before the AMI was made, so nothing listens after boot. Enable the services on a dev server and bake a new AMI.
  17. Copy the DNS name of AppTierInternalALB

    • Do: Load balancers > AppTierInternalALB > Details > DNS name (copy icon).
    • You should see: internal-AppTierInternalALB-…ap-northeast-2.elb.amazonaws.com, Scheme Internal.
    • Why: The web tier needs this name in its nginx proxy.
AWS console Auto Scaling AZ distribution options

Console: Create Auto Scaling group: Availability Zone distribution set to Balanced best effort

AWS console Auto Scaling attach to a new load balancer

Console: Load balancing: Attach to a new load balancer, Application Load Balancer

AWS console Auto Scaling group size

Console: Group size and scaling limits: desired 2, min 1, max 4

AWS console target tracking policy metric list

Console: Target tracking scaling policy with the metric Average CPU utilization

Part I: the nginx development server and webami

  1. Terminate phpdevelopmentserver

    • Do: Check AMIs: phpami is Available. Then Instances > phpdevelopmentserver > Instance state > Terminate (delete) instance.
    • Why: Its work ended when phpami became Available; the app servers now come from the AMI. It also frees 2 vCPUs of your quota for the next server.
  2. Launch nginxdevelopmentserver with DevSG and WebEc2SG

    • Do: Launch instances: Name nginxdevelopmentserver, Amazon Linux 2023, t3.micro, ThreeTierKey; VPC ThreeTierVpc, Subnet PublicSubnet1, Auto-assign public IP Enable; security groups DevSG and WebEc2SG > Launch instance.
    • Why: Same idea as the PHP server: WebEc2SG makes it look like a real web server to AppAlbSG, so the proxy test proves the real path; DevSG lets you in for setup.
    • If you see "You have requested more vCPU capacity than your current vCPU limit of 16 allows": the running servers already use your On-Demand quota. Terminate servers you don't need, or lower the App group to Desired 1 for now, or request an increase in Service Quotas (section 10).
  3. SSH in and install nginx only

    • Command:
      ssh -i .\Downloads\ThreeTierKey.pem ec2-user@<nginx-dev-public-IP>
      sudo yum install nginx -y
    • You should see: Complete!
    • Why: The web tier serves HTML and forwards PHP; it runs no PHP itself and never talks to the database.
  4. Start and enable nginx

    • Command:
      sudo service nginx start
      sudo systemctl enable nginx
      systemctl is-enabled nginx
    • You should see:
      enabled
    • Why: Enable before baking webami, or every web server starts with nginx off and the web load balancer returns 502.
  5. Put forms.html in the web root

    • Do: Paste the same forms.html code as in Part G and save.
    • Command:
      sudo nano /usr/share/nginx/html/forms.html
      curl -s http://localhost/forms.html | head -3
    • You should see:
      <!DOCTYPE html>
      <html>
      <head><meta charset="utf-8"><title>Sign up</title></head>
    • Why: This is the page users see. Its action="submit.php" is relative, so the browser posts to the same load balancer, and nginx forwards it to the app tier.
  6. Back up nginx.conf and open it

    • Command:
      sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
      sudo nano /etc/nginx/nginx.conf
    • Why: A copy lets you go back in one command if the edit breaks the file.
  7. Add the .php proxy inside the server { … } block

    • Do: Below the listen 80; and root /usr/share/nginx/html; lines of the first server block, paste the block below with your internal ALB DNS name, save.
    • Command:
          location ~ \.php$ {
              resolver 10.0.0.2 valid=30s;
              set $app_alb "http://internal-AppTierInternalALB-xxxxxxxx.ap-northeast-2.elb.amazonaws.com";
              proxy_pass $app_alb;
              proxy_set_header Host $host;
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          }
    • Why: Every request ending in .php goes to the internal ALB; everything else (forms.html) is served here. resolver 10.0.0.2 is the VPC's DNS server (VPC base address + 2). Putting the name in a variable makes nginx look it up again every 30 seconds; with a plain proxy_pass name nginx resolves it once at start, and when the ALB's IP addresses change (they do) requests start failing with 502.
  8. Test the config and reload

    • Command:
      sudo nginx -t
      sudo service nginx reload
    • You should see:
      nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
      nginx: configuration file /etc/nginx/nginx.conf test is successful
    • Why: nginx -t finds typos before they take the server down; reload applies the change without dropping open connections.
    • If you see "unexpected }" or "directive is not allowed here": the block is outside server { } or a brace is missing. Fix it, or restore with sudo cp /etc/nginx/nginx.conf.bak /etc/nginx/nginx.conf.
  9. Test the whole chain from the dev server

    • Do: Then open http://<nginx-dev-public-IP>/forms.html, fill it and click Register.
    • Command:
      curl -s http://localhost/health.php
    • You should see: ok in the terminal, then "User registered successfully!" in the browser.
    • Why: health.php exists only on the app servers, so getting ok here proves nginx → internal ALB → PHP works. The form test then proves PHP → RDS too.
    • If you see "504 Gateway Time-out": AppAlbSG does not allow port 80 from WebEc2SG, or this server lacks WebEc2SG, or the internal ALB still has the wrong security group.
    • If you see "502 Bad Gateway": the name in $app_alb is wrong or the resolver line is missing; check with sudo tail /var/log/nginx/error.log.
    • If you see "503 Service Temporarily Unavailable": the internal ALB has no registered targets: check that AppTierASG is running two servers.
  10. Create the AMI webami

    • Do: Instances > nginxdevelopmentserver > Actions > Image and templates > Create image: Image name webami, Description nginx + forms.html + php proxy > Create image.
    • You should see: webami Available in AMIs after a few minutes.
    • Why: Freezes the tested web server: nginx enabled, forms.html and the proxy config.

Part J: the web tier (launch template, Auto Scaling group, internet-facing ALB)

  1. Create the launch template WebTierLaunchTemplate

    • Do: EC2 > Launch templates > Create launch template: Name WebTierLaunchTemplate, Template version description v1, tick Provide guidance to help me set up a template that I can use with EC2 Auto Scaling.
    • Why: A launch template is the recipe the Auto Scaling group follows every time it starts a server: AMI, instance type, key pair and security group. Ticking the Auto Scaling guidance makes the console warn you about settings a group can't use.
  2. AMI: My AMIs > webami; type t3.micro; key pair ThreeTierKey

    • Do: Application and OS Images > My AMIs > Owned by me > webami; Instance type t3.micro; Key pair ThreeTierKey.
    • Why: webami already has the software, the files and the enabled services, so a new server is ready the moment it boots. The key pair lets you SSH in for debugging later.
  3. Network: Subnet: Don't include in launch template, security group WebEc2SG

    • Do: Network settings: Subnet Don't include in launch template; Firewall Select existing security group > WebEc2SG; leave Advanced network configuration empty > Create launch template.
    • You should see: "Successfully created WebTierLaunchTemplate".
    • Why: The Auto Scaling group chooses the subnets, one per AZ. A subnet fixed inside the template would pin every server to one AZ. The security group belongs in the template because every server of this tier needs the same one.
  4. Create the Auto Scaling group WebTierASG from WebTierLaunchTemplate

    • Do: EC2 > Auto Scaling groups > Create Auto Scaling group: Name WebTierASG, Launch template WebTierLaunchTemplate, Version Default (1) > Next.
    • Why: The group keeps the right number of servers running, spreads them over AZs, replaces broken ones and adds or removes servers with the load. You never launch these servers by hand.
  5. Network: ThreeTierVpc, subnets WebSubnet1 and WebSubnet2

    • Do: VPC ThreeTierVpc, Availability Zones and subnets WebSubnet1 and WebSubnet2.
    • Why: Two subnets in two AZs: if one AZ fails, the group starts servers in the other one.
    • If you see "The requested instance type is not supported in the requested Availability Zone": not every instance type exists in every AZ. Keep t3.micro, or pick another type in a new template version (section 10).
  6. AZ distribution: Balanced best effort

    • Do: Availability Zone distribution: Balanced best effort > Next.
    • Why: Balanced only waits and retries in the same AZ when that AZ has no capacity. Balanced best effort launches in the other AZ instead, so the app gets its server now and the group rebalances later. When everyone launches servers at the same time, like shopping sites in a festival sale, one AZ can run out of a type; best effort keeps you serving.
  7. Attach to a new load balancer: Application Load Balancer WebTierPublicALB, Internet-facing

    • Do: Load balancing: Attach to a new load balancer, Application Load Balancer, Load balancer name WebTierPublicALB, Load balancer scheme Internet-facing.
    • Why: The wizard creates the load balancer, its listener and its target group in one go and registers the group's servers in it. Internet-facing gives the load balancer public IPs and a public DNS name: this is the one address users open. For an internet-facing load balancer the wizard asks for its own subnets: pick PublicSubnet1 and PublicSubnet2.
  8. Listener HTTP 80, Create a target group WebTierTG

    • Do: Listeners and routing: Protocol HTTP, Port 80, Default routing Create a target group, New target group name WebTierTG.
    • Why: The listener is the port the load balancer accepts; the target group is the list of servers it forwards to and health-checks.
  9. Turn on Elastic Load Balancing health checks, grace period 60

    • Do: Health checks: tick Turn on Elastic Load Balancing health checks, Health check grace period 60 > Next.
    • Why: By default the group replaces a server only when the EC2 machine itself is broken. With ELB health checks on, it also replaces a server whose app has stopped answering. The grace period gives a new server 60 seconds to boot before its checks count.
  10. Group size: Desired 2, Min 1, Max 4

    • Do: Desired capacity 2; Min desired capacity 1; Max desired capacity 4.
    • Why: Desired 2 gives one server per AZ. Min 1 is the floor scaling can never go under; Max 4 caps the bill and your vCPU quota.
  11. Scaling: Target tracking, Average CPU utilization 20, warmup 60

    • Do: Automatic scaling: Target tracking scaling policy, Metric type Average CPU utilization, Target value 20, Instance warmup 60.
    • Why: Target tracking adds servers when the average CPU of the group goes above 20% and removes them when it falls well below. 20 is low on purpose so you can see scaling in a lab; production values are often 50 to 70. Warmup tells the group not to count a new server's CPU for its first 60 seconds.
  12. Maintenance policy: No policy; tick Enable group metrics collection

    • Do: Instance maintenance policy No policy; Additional settings: tick Enable group metrics collection within CloudWatch > Next > Next (no notifications).
    • Why: No policy keeps the default replacement behaviour. Group metrics show the number of servers over time in the Monitoring tab, which is how you watch scaling happen.
  13. Tag Name = WebTierServer, then Create Auto Scaling group

    • Do: Add tag: Key Name, Value WebTierServer > Next > review > Create Auto Scaling group.
    • You should see: Two WebTierServer instances, one in each AZ, after a minute or two.
    • Why: Without the tag every server of the group shows an empty name in the Instances list and you can't tell the tiers apart. "Tag new instances" is ticked by default.
    • If you see "You have requested more vCPU capacity than your current vCPU limit" in the group's Activity tab: the account's On-Demand vCPU quota is full. Terminate servers you no longer need, or request more (section 10).
  14. Give WebTierPublicALB its own security group

    • Do: EC2 > Load balancers > WebTierPublicALB > Security tab > Edit: remove every group, add WebAlbSG > Save changes.
    • Why: The wizard attaches the launch template's security group to the new load balancer. Our chain needs WebAlbSG on the load balancer, otherwise the server security group, which trusts only the load balancer's group, blocks every forwarded request.
  15. Set the health check of WebTierTG to /forms.html

    • Do: EC2 > Target groups > WebTierTG > Health checks tab > Edit: Health check path /forms.html; Advanced health check settings: Healthy threshold 2, Unhealthy threshold 2, Timeout 3, Interval 5, Success codes 200 > Save changes.
    • Why: The target group checks / by default. /forms.html proves the part of this tier that matters answers with 200. Healthy 2 and Unhealthy 2 at a 5-second interval mean a server joins or leaves the target group within about 10 seconds; the timeout (3 s) must be shorter than the interval.
  16. Wait for two Healthy targets in WebTierTG

    • Do: WebTierTG > Targets tab, refresh.
    • You should see: 2 Healthy, one target in each AZ.
    • Why: Only healthy targets receive traffic. If both are healthy, this tier works end to end through its load balancer.
    • If you see Unhealthy, "Health checks failed" or "Request timed out": the server security group does not allow port 80 from the load balancer's security group, or the load balancer still has the wrong group (previous step).
    • If you see "Health checks failed with these codes: [404]": /forms.html is not in the AMI. Fix the file on a dev server, make a new AMI and a new template version (Part L).
    • If you see targets that are Unhealthy and get replaced again and again: nginx or php-fpm was not enabled before the AMI was made, so nothing listens after boot. Enable the services on a dev server and bake a new AMI.
  17. Copy the DNS name of WebTierPublicALB

    • Do: Load balancers > WebTierPublicALB > Details > DNS name (copy icon).
    • You should see: WebTierPublicALB-…ap-northeast-2.elb.amazonaws.com, Scheme Internet-facing.
    • Why: This is the address of the whole app.
AWS console load balancer scheme Internet-facing

Console: the web tier load balancer: scheme Internet-facing, VPC ThreeTierVpc, one public subnet in each AZ

AWS console Edit health check settings

Console: Edit health check settings of the web target group: path /forms.html

AWS console list of two load balancers

Console: both load balancers Active: one with scheme internal for the app tier, one internet-facing for the web tier (your names are the ones you typed in the wizard)

Part K: test the whole app and lock it down

  1. Open the app through the public load balancer

    • Do: Open http://<WebTierPublicALB-DNS>/forms.html, fill the form, Register.
    • You should see: "User registered successfully!"
    • Why: This is the real user path: browser → WebTierPublicALB → web server → AppTierInternalALB → app server → RDS. No server is reached directly.
    • If you see the page not loading at all for a few minutes: a new load balancer's DNS name takes a few minutes to start resolving. Wait and retry.
  2. Check the new row in RDS

    • Command:
      mysql -h <rds-endpoint> -u root -p -e "select id, name, left(password,4) from insta.users;"
    • You should see: The new row, with $2y$ as the password start.
    • Why: Proves the request crossed all three tiers.
  3. Self-healing test: terminate one WebTierServer

    • Do: Instances > one WebTierServer > Terminate (delete) instance. Keep reloading the form page. Then open WebTierASG > Activity.
    • You should see: The page keeps loading; Activity shows "Terminating EC2 instance" and then "Launching a new EC2 instance"; after a minute or two WebTierTG has 2 Healthy targets again.
    • Why: This shows why the group exists: it notices the missing server through its health checks and launches a replacement from webami with no help from you, while the other server keeps the app up.
  4. Terminate nginxdevelopmentserver

    • Do: Instances > nginxdevelopmentserver > Terminate (delete) instance.
    • Why: Its job ended with webami. A dev server with SSH and HTTP open is attack surface and costs money.
  5. Remove the JumpSG rule from DbSG

    • Do: Security groups > DbSG > Inbound rules > Edit inbound rules > Delete the MYSQL/Aurora rule whose source is JumpSG > Save rules. Stop or terminate JumpServer if you no longer need it.
    • Check: DbSG has one inbound rule: MYSQL/Aurora 3306, source AppEc2SG.
    • Why: It was needed only to create the database and table. Now the only way to port 3306 is from AppEc2SG: exactly the clean design.
  6. Optional: point your domain at the load balancer with a CNAME

    • Do: In your domain provider's DNS records: Add record, Type CNAME, Name www, Value the WebTierPublicALB DNS name, TTL default > Save. In Route 53 you can use an Alias A record instead.
    • Check: nslookup www.yourdomain.com shows the ELB name as canonical name; then http://www.yourdomain.com/forms.html opens the form.
    • Why: Users should type www.yourdomain.com, not a long ELB name. A CNAME says "this name is another name for that name". A load balancer's IPs change, so you never point a domain at an IP; you point it at the DNS name.
    • If you see "Server Not Found" in the browser: the browser could not resolve the name, so the request never reached AWS. Wait for DNS to propagate, check the record name, or try another network or DNS server. It is never a security group problem.

Part L: change servers safely (new template version + instance refresh)

  1. Create a new version of the launch template

    • Do: Launch templates > AppTierLaunchTemplate > Actions > Modify template (Create new version): Template version description v2, AMI phpami-v2 > Create template version.
    • Why: A launch template can't be edited in place: every change is a new numbered version, so you can always see and go back to what ran before. Example: you baked phpami-v2 with a fix.
  2. Point the group at Latest

    • Do: AppTierASG > Details > Launch template > Edit > Version Latest > Update.
    • Why: With Default (1) the group keeps launching version 1. Latest makes every future launch use the newest version.
  3. Start an instance refresh

    • Do: AppTierASG > Instance refresh tab > Start instance refresh: Minimum healthy percentage 50 and Maximum healthy percentage 100 (one of the two servers is replaced at a time while the other serves), Instance warmup 60 > Start instance refresh.
    • You should see: Status InProgress, then Successful; the instances list shows new instance IDs.
    • Why: Changing the template does not touch servers that are already running. An instance refresh replaces them a few at a time, waiting for each new one to pass health checks, so the app stays up during the rollout.

Part M: clean up so nothing keeps billing

  1. Delete both Auto Scaling groups first

    • Do: Auto Scaling groups > select WebTierASG and AppTierASG > Actions > Delete > type delete.
    • Why: If you terminate the servers first, the groups simply launch new ones. Deleting a group terminates its servers.
  2. Delete both load balancers, then both target groups

    • Do: Load balancers > both > Actions > Delete load balancer > type confirm. Target groups > WebTierTG, AppTierTG > Actions > Delete.
    • Why: Load balancers bill every hour even with no traffic. A target group in use by a listener can't be deleted, so the load balancers go first.
  3. Delete the RDS database

    • Do: RDS > Databases > database-1 > Actions > Delete: untick Create final snapshot, untick Retain automated backups, tick the acknowledgement, type delete me > Delete.
    • Why: The database bills by the hour and for its storage.
  4. Terminate any remaining instances

    • Do: Instances > select the remaining ones > Instance state > Terminate (delete) instance.
    • Why: JumpServer and any dev server you kept still cost money.
  5. Deregister both AMIs and delete their snapshots

    • Do: AMIs > phpami, webami (and any -v2) > Actions > Deregister AMI. Snapshots > select their snapshots > Actions > Delete snapshot.
    • Why: An AMI stores its disk as an EBS snapshot, and the snapshot keeps costing storage even after the AMI is deregistered.
  6. Delete the launch templates and the DB subnet group

    • Do: Launch templates > both > Actions > Delete template. RDS > Subnet groups > threetierdbsubnetgroup > Delete.
    • Why: They cost nothing, but leftovers clutter the account and confuse the next build.
  7. Delete the VPC last

    • Do: VPC > Your VPCs > ThreeTierVpc > Actions > Delete VPC > type delete > Delete.
    • You should see: The VPC disappears from the list; EC2 > Instances, Load balancers and RDS > Databases are empty.
    • Why: Deleting the VPC also deletes its subnets, route tables, internet gateway and security groups, but only once nothing else uses them.
    • If you see "has dependencies and cannot be deleted": something still uses the VPC, usually a load balancer or RDS still deleting, or an ENI. Wait a few minutes and try again.

10. Side topics that came up during the build

Each of these came up because something in the lab needed it. Each one says why it came up, the background, and how it fits the 3-tier build.

Windows key permissions are chmod 400

Why it came up. SSH to the JumpServer failed from Windows with "UNPROTECTED PRIVATE KEY FILE".

Background. A private key proves who you are, so OpenSSH refuses to use one that other users of the computer could copy. On Linux and Mac you remove everyone else's access with chmod 400 (owner read, group 0, others 0). Windows has no chmod; its files carry access lists (ACLs) inherited from the folder. Disabling inheritance, removing the inherited entries and granting only your user Read is the same 400, written in Windows terms. icacls file /inheritance:r /grant:r "%USERNAME%:R" does the same from the command line.

How it links to the 3-tier build. Every server in this build (JumpServer, both dev servers, and any Auto Scaling server you debug) is reached with the same ThreeTierKey.pem, so this fix is needed once per computer.

AWS console Auto Scaling groups page with a NetworkError message

Console: the Auto Scaling groups page showing "NetworkError when attempting to fetch resource" after the console session had been idle; reloading the page fixes it

Console "NetworkError" and session timeouts

Why it came up. Pages such as Auto Scaling groups or Create database showed "NetworkError when attempting to fetch resource" or "Failed to load" in the middle of the build.

Background. The console is a web app that calls AWS APIs from your browser. If one call fails (Wi-Fi drop, VPN, a browser extension, or a sign-in session that expired; console sessions end after a set time, 1 to 12 hours depending on how you signed in), the page shows this error. Your resources are not affected. Reload the page, sign in again if asked, and look at the list before you click Create again, so you don't create the same thing twice.

How it links to the 3-tier build. This build has long wizards (RDS, two Auto Scaling groups). If an error appears after you click Create, check whether the resource exists before you repeat the step; a second RDS instance or Auto Scaling group doubles the cost.

vCPU quota errors

Why it came up. Launching the nginx development server failed with "You have requested more vCPU capacity than your current vCPU limit" while both Auto Scaling groups were running.

Background. Every account has a Service Quota called Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances, counted in vCPUs per Region, not in instances. New accounts often start low. t3.micro has 2 vCPUs, so a limit of 16 is reached by 8 servers. Worked example: two groups at their max of 4 = 8 servers = 16 vCPUs, plus a JumpServer (2) and a dev server (2) = 20. Fixes: terminate servers you don't need, lower Max while learning, or open Service Quotas > AWS services > Amazon EC2 > Running On-Demand Standard … > Request increase at account level; approval can take hours to days.

How it links to the 3-tier build. A 3-tier build with two Auto Scaling groups is the first lab that runs many servers at once, and scaling out needs headroom in the quota: a group that can't launch can't heal or scale.

Instance types are not offered in every AZ

Why it came up. While picking subnets for an Auto Scaling group, an AZ can reject an instance type with "not supported in the requested Availability Zone".

Background. Each AZ has its own hardware, and newer or special families are not in every AZ. Check before you build: EC2 > Instance types > select the type > Networking shows the AZs, or run aws ec2 describe-instance-type-offerings --location-type availability-zone --filters Name=instance-type,Values=t3.micro. Separate from this, an AZ can be temporarily out of capacity for a type ("InsufficientInstanceCapacity"), which is what Balanced best effort handles.

How it links to the 3-tier build. High availability needs the same instance type in both AZs. A type offered in only one AZ quietly turns a two-AZ design into a one-AZ design.

502, 503 and 504 from a load balancer

Why it came up. Each of these appeared at some point while the tiers were being connected, and each points to a different broken link.

Background. The code tells you where to look:

Code What the load balancer (or nginx) means Usual cause in this build
502 Bad Gateway I reached a target, but it refused the connection or sent a broken answer nginx or php-fpm not running (not enabled before the AMI was baked); wrong proxy target in nginx.conf
503 Service Unavailable I have no registered targets to send to the Auto Scaling group has 0 servers, or is attached to a different target group
504 Gateway Timeout I sent the request and no answer came in time a security group silently drops the packets (WebEc2SG → AppAlbSG, or AppEc2SG → DbSG), or the app waits on the database

One more rule: if every target is unhealthy, an ALB "fails open" and sends traffic to all of them anyway, so a page may still load (or fail in odd ways) while the console shows all targets Unhealthy. Fix the health check; don't rely on this.

How it links to the 3-tier build. There are two load balancers and four hops in this build. Reading the code first tells you which hop to check instead of changing everything.

Health checks decide which servers get replaced

Why it came up. In the self-healing test a terminated web server came back by itself, and servers baked without enabled services kept being replaced.

Background. The target group checks each server every 5 seconds; after 2 failures it marks the server Unhealthy and stops sending it traffic. Because the Auto Scaling group uses ELB health checks, it then terminates that server and launches a new one from the launch template, after the 60-second grace period. If the AMI itself is broken, every replacement fails the same way and you see a loop of launches and terminations in Activity; the fix is a new AMI and template version, never repairing a server by hand.

How it links to the 3-tier build. This is why /health.php must test only PHP and why the services must be enabled before baking: the health check is what turns a broken server into a fresh one.

"Server Not Found" is a DNS problem on your side

Why it came up. After adding the www CNAME, the browser can show "Server Not Found" (Firefox) or "This site can't be reached / DNS_PROBE_FINISHED_NXDOMAIN" (Chrome).

Background. These messages mean the browser could not turn the name into an IP address, or could not connect at all, so the request never reached AWS. Security groups, target groups and nginx are not involved. Check the record (Name www, Value the ELB name, no http://), wait for the TTL, run nslookup www.yourdomain.com, and try another network or a public DNS server such as 8.8.8.8. Compare: a 502/503/504 page proves the request did reach the load balancer.

How it links to the 3-tier build. It is the last link of the chain, above WebTierPublicALB. Knowing which side of AWS a problem is on saves you from changing working security groups.

EC2 Instance Connect needs port 22 too

Why it came up. Connecting from the browser with Connect > EC2 Instance Connect is quicker than SSH from Windows, but it failed on servers whose security group allowed SSH only from My IP.

Background. EC2 Instance Connect pushes a one-time key and then opens SSH to the instance from AWS's own address range, not from your laptop. So the security group needs port 22 from the managed prefix list com.amazonaws.ap-northeast-2.ec2-instance-connect (pick it in the Source box), and the instance needs a public IP. For private servers, use an EC2 Instance Connect Endpoint or the JumpServer.

How it links to the 3-tier build. JumpSG and DevSG allow SSH from My IP only. Add the prefix-list rule only if you prefer the browser, and remove it with the other setup rules in Part K.

Why this build has no NAT gateway

Why it came up. How to Build a 3-Tier Architecture in AWS with VPC, NAT Gateway and RDS used a NAT gateway so the private app server could install software. Here the private servers never install anything.

Background. A NAT gateway gives private servers outbound internet access only, and it bills every hour plus per GB. In this build all software is installed on the development servers in the public subnet and baked into AMIs, so web and app servers boot ready and need no outbound access. Add a NAT gateway only if private servers must reach the internet, for example to download updates or call an outside API.

How it links to the 3-tier build. Immutable AMIs (section 4) are what make the NAT gateway optional, and that saves money every hour the app runs.

Clean up so nothing keeps billing

Why it came up. Part M deletes everything in a fixed order, right after testing.

Background. Load balancers and RDS bill per hour whether or not anyone uses them; public IPv4 addresses bill per hour; EBS snapshots behind AMIs bill per GB-month. The order matters because AWS protects dependencies: Auto Scaling groups first (otherwise they relaunch servers), then load balancers and target groups, then RDS, instances, AMIs and snapshots, templates and the DB subnet group, and the VPC last. Afterwards, check Billing and Cost Management > Bills the next day, and set an AWS Budgets alert, for example at 5 USD.

How it links to the 3-tier build. A 3-tier build is the most expensive lab so far because it runs two load balancers, a database and six or more servers at once.

11. Troubleshooting checklist

Work from the top of the chain down. Each row is one link.

You see Link to check Fix
"Server Not Found", DNS_PROBE error your DNS, before AWS check the CNAME (Name www, Value ELB DNS name), wait for TTL, nslookup
Page never loads on the ALB DNS name WebAlbSG, public subnets WebAlbSG HTTP 80 from 0.0.0.0/0; the ALB in PublicSubnet1/2, whose route table has 0.0.0.0/0 → igw
503 on the web ALB WebTierASG the group has servers and is attached to WebTierTG
502 on the web ALB web servers nginx enabled in webami; WebTierTG health check /forms.html
forms.html loads, submit gives 504 WebEc2SG → AppAlbSG, AppAlbSG on the ALB AppAlbSG HTTP 80 from WebEc2SG; Security tab of the internal ALB shows AppAlbSG
Submit gives 502 nginx proxy resolver 10.0.0.2, the $app_alb name, sudo nginx -t, /var/log/nginx/error.log
Submit gives 503 AppTierASG servers running, targets registered in AppTierTG
Submit gives 504 or "Database error" after a long wait AppEc2SG → DbSG DbSG 3306 from AppEc2SG; RDS has DbSG
"Database error" at once submit.php endpoint, user, password, database insta, table users
Targets Unhealthy and replaced in a loop the AMI services enabled before baking; the health check file is in the AMI; new AMI + template version + instance refresh
Group shows "vCPU limit" Service Quotas terminate spare servers, lower Max, request an increase
SSH "UNPROTECTED PRIVATE KEY FILE" your key file Windows: owner Read only; Mac/Linux: chmod 400
SSH times out JumpSG / DevSG, public IP My IP up to date; the server has a public IP in a public subnet
mysql: command not found the JumpServer sudo yum install mariadb105 -y
"ERROR 2003 Can't connect" to RDS DbSG 3306 from JumpSG during setup; RDS uses DbSG
VPC won't delete leftovers load balancers, RDS or ENIs still deleting; wait and retry

Ravindra Bagale's Tip

When something breaks, don't change three things at once. Read the error code, find the one link it points to in the checklist, test that link alone with curl from the tier above it, and only then change one rule.

12. The whole class on one page

  1. Why: one server is one point of failure; two AZs, load balancers and Auto Scaling give high availability, fault tolerance and self-healing.
  2. Design: users → internet-facing ALB (public subnets) → nginx (Web subnets) → internal ALB → PHP (App subnets) → RDS (DB subnets).
  3. Order: build bottom to top (network, database, app, web) so every tier can be tested against a working tier below it; write security rules top to bottom.
  4. Network: ThreeTierVpc 10.0.0.0/16, eight /20 subnets (4,096 addresses each), an IGW, and a public route table used only by the two public subnets.
  5. Security groups: WebAlbSG (80 from anywhere) → WebEc2SG (80 from WebAlbSG) → AppAlbSG (80 from WebEc2SG) → AppEc2SG (80 from AppAlbSG) → DbSG (3306 from AppEc2SG). 3306 is never open to 0.0.0.0/0.
  6. Database: DB subnet group of the two DB subnets; RDS MySQL with no public access; one database insta, table users with password varchar(255) for hashes.
  7. Servers: install on a development server, enable the services, test, bake an AMI (phpami, webami); servers are replaced, never patched.
  8. Scaling: launch templates without a subnet; Auto Scaling groups with Balanced best effort, desired 2 / min 1 / max 4, target tracking at 20% CPU, ELB health checks.
  9. Load balancers: set each ALB's own security group after the wizard; health checks /health.php and /forms.html.
  10. Changes: new template version → group on Latest → instance refresh.
  11. Errors: Server Not Found = DNS; 502 = target refused; 503 = no targets; 504 = timeout, usually a security group.
  12. Clean up in order, and keep a budget alert.

For the concepts behind this build, go back to How to Build a 3-Tier Architecture in AWS with VPC, NAT Gateway and RDS.

13. Try at home

Try at home

Task 1: on paper

  1. Split 10.0.0.0/16 into eight /20 subnets and write the first and last usable address of each (AWS keeps 5 addresses in every subnet).
  2. Write the five security groups with their inbound rule and source, top to bottom.

Task 2: in the console

  1. Build the whole lab in a different Region (for example Mumbai, ap-south-1) with two AZs.
  2. Run the self-healing test on an app server and watch AppTierASG > Activity.
  3. Watch scaling: SSH to one app server through the JumpServer, run sudo yum install stress -y and stress --cpu 2 --timeout 600, and see the group grow above 2 in Monitoring, then shrink after the load stops.

Task 3: change and clean up

  1. Change health.php to print ok v2, bake phpami-v2, create template version 2 and run an instance refresh.
  2. Delete everything in the order of Part M and check the bill the next day.

Recap

In short

  1. Two AZs, two load balancers and two Auto Scaling groups make the app highly available and self-healing.
  2. Build bottom to top; chain five security groups top to bottom, each source a security group.
  3. RDS sits in private DB subnets with no public access; only AppEc2SG reaches 3306.
  4. Servers come from tested AMIs with services enabled; changes go through a new template version and an instance refresh.
  5. Health checks, Balanced best effort and target tracking keep the right number of healthy servers running.
  6. Read the error (DNS, 502, 503, 504) to find the broken link, and clean up in order when you finish.

Practice Tasks

About this scenario

Singapore Freight is a logistics business in Singapore. Wei Ling Tan looks after its IT and cloud work. They count money in SGD and run their AWS resources in the ap-southeast-1 Region. In the tasks below you do real jobs for them using what this lesson teaches. All names, account IDs, IP addresses and numbers are made up for practice.

0 / 12

Easy How many addresses in a /16?

Why: Picking a VPC size needs this count.

What to do: Singapore Freight plans the VPC 10.123.0.0/16. How many IP addresses does it hold in total?

How:
  1. Write down the numbers the task gives you.
  2. Work it out step by step on paper.
  3. Type only the final answer in the box.
  4. Click Check answer.

Easy Create a VPC

Why: A VPC is your own private network in AWS. Everything else lives inside it.

What to do: Create a VPC with the CIDR block 10.7.0.0/16.

How:
  1. Read the task and decide which AWS service it is about.
  2. Write aws, then the service name, then the operation (the action word).
  3. Add each needed flag (--name) followed by its value from the task.
  4. Flags can be in any order. Click Check answer.

Easy Create an internet gateway

Why: Without an internet gateway, nothing in the VPC can reach the internet.

What to do: Create a new internet gateway.

How:
  1. Read the task and decide which AWS service it is about.
  2. Write aws, then the service name, then the operation (the action word).
  3. Add each needed flag (--name) followed by its value from the task.
  4. Flags can be in any order. Click Check answer.

Easy Find the database endpoint

Why: Your app connects using the endpoint name, not an IP.

What to do: Describe DB instance sgfreight-db.

How:
  1. Read the task and decide which AWS service it is about.
  2. Write aws, then the service name, then the operation (the action word).
  3. Add each needed flag (--name) followed by its value from the task.
  4. Flags can be in any order. Click Check answer.

Medium Create a subnet in one AZ

Why: Subnets split the VPC range and each one lives in exactly one AZ.

What to do: In vpc-0201ee521cdeadbee, create subnet 10.7.1.0/24 in ap-southeast-1a.

How:
  1. Read the task and decide which AWS service it is about.
  2. Write aws, then the service name, then the operation (the action word).
  3. Add each needed flag (--name) followed by its value from the task.
  4. Flags can be in any order. Click Check answer.

Medium Add the internet route

Why: A subnet is "public" only when its route table sends 0.0.0.0/0 to an internet gateway.

What to do: In route table rtb-01a201dd521abb22, send all internet traffic (0.0.0.0/0) to igw-0a201dd521aaa11.

How:
  1. Read the task and decide which AWS service it is about.
  2. Write aws, then the service name, then the operation (the action word).
  3. Add each needed flag (--name) followed by its value from the task.
  4. Flags can be in any order. Click Check answer.

Medium Find the VPC router address

Why: AWS reserves base+1 for the router.

What to do: Which address does AWS reserve for the VPC router in subnet 10.123.4.0/24?

How:
  1. Write down the numbers the task gives you.
  2. Work it out step by step on paper.
  3. Type only the final answer in the box.
  4. Click Check answer.

Medium Usable IPs in an AWS /24 subnet

Why: AWS keeps 5 addresses in every subnet.

What to do: How many addresses can Singapore Freight use for servers in subnet 10.123.1.0/24 on AWS?

How:
  1. Write down the numbers the task gives you.
  2. Work it out step by step on paper.
  3. Type only the final answer in the box.
  4. Click Check answer.

Medium Link a subnet to a route table

Why: Until you associate it, a subnet uses the main route table.

What to do: Associate subnet subnet-01a201dd521abb1 with route table rtb-01a201dd521abb22.

How:
  1. Read the task and decide which AWS service it is about.
  2. Write aws, then the service name, then the operation (the action word).
  3. Add each needed flag (--name) followed by its value from the task.
  4. Flags can be in any order. Click Check answer.

Medium Attach the internet gateway

Why: A gateway does nothing until it is attached to a VPC.

What to do: Attach igw-0a201dd521aaa11 to vpc-0201ee521cdeadbee.

How:
  1. Read the task and decide which AWS service it is about.
  2. Write aws, then the service name, then the operation (the action word).
  3. Add each needed flag (--name) followed by its value from the task.
  4. Flags can be in any order. Click Check answer.

Hard Pick the smallest prefix

Why: Right-sizing saves address space.

What to do: A subnet must hold 100 servers on AWS. What is the largest prefix number (smallest subnet) that fits? Answer like /25.

How:
  1. Write down the numbers the task gives you.
  2. Work it out step by step on paper.
  3. Type only the final answer in the box.
  4. Click Check answer.

Hard Split a /24 into two

Why: Public and private subnets often split one block.

What to do: Split 10.123.8.0/24 into two equal subnets. Write both CIDRs separated by a comma.

How:
  1. Write down the numbers the task gives you.
  2. Work it out step by step on paper.
  3. Type only the final answer in the box.
  4. Click Check answer.

Quick revision cards

Revise this chapter in two minutes. Save the cards on your phone or download them as PNG files. The QR code on each card opens its lesson.

3-tier subnet plan for 10.0.0.0/16: eight /20 subnets, Public 10.0.0.0 and 10.0.16.0, Web 10.0.32.0 and 10.0.48.0, App 10.0.64.0 and 10.0.80.0, DB 10.0.96.0 and 10.0.112.0, across AZs a and b.
Card 1 of 4: 3-tier subnet plan · Download PNG
NAT gateway: sits in a public subnet with an Elastic IP; private route 0.0.0.0/0 to the NAT lets private servers go out but blocks incoming connections.
Card 2 of 4: NAT gateway · Download PNG
Public vs private subnet: a public subnet has a route 0.0.0.0/0 to the internet gateway; a private subnet does not.
Card 3 of 4: Public vs private subnet · Download PNG
Internet gateway vs NAT gateway: IGW is two-way for public subnets and free; NAT is outbound-only for private subnets and billed hourly.
Card 4 of 4: IGW vs NAT gateway · Download PNG
Comparison card: Hardcoded password vs AWS Secrets Manager. Hardcoded password: The password written inside the code. AWS Secrets Manager: An AWS service that stores the password encrypted.
Comparison: Hardcoded password vs AWS Secrets Manager · Download PNG
Revision card: Chained security groups in 3-tier. Server IPs change with Auto Scaling. Rules based on IPs break. Each tier's SG allows traffic only from the SG of the tier in front of it. Set the Source of each rule to a security group, not an IP. Five links:
Revision card: Chained security groups in 3-tier · Download PNG
Revision card: Immutable servers with AMIs. Fixing servers by hand makes each one a little different. Bugs appear on some servers only. Immutable = never change a running server. Build a new AMI and replace the servers. Dev server › test › create AMI › new launch template version › instance refresh. App v2: ami-0app2 in template v2; the group replaces all servers.
Revision card: Immutable servers with AMIs · Download PNG
Revision card: Warmup, grace period and instance refresh. A new server needs time to boot. Judging it too early kills healthy servers. Grace period: health checks wait. Warmup: its metrics are ignored for scaling. Refresh: replaces servers in rolling steps. Set them in the Auto Scaling group; start a refresh from the Instance refresh tab. Grace 300 s, warmup 120 s, refresh with minimum healthy 90%.
Revision card: Warmup, grace period and instance refresh · Download PNG
Comparison: Internet-facing ALB vs Internal ALB. Internet-facing ALB: A load balancer with public IPs in public subnets. Internal ALB: A load balancer with only private IPs in private subnets.
Comparison: Internet-facing ALB vs Internal ALB · Download PNG
Interview card: Why use a security group as the source instead of an IP?. A SG source allows any server that has that SG, whatever its IP. Auto Scaling keeps changing IPs; IP rules would break or be too wide. db-sg allows 3306 from app-sg only.
Interview card: Why use a security group as the source instead of an IP? · Download PNG
Interview card: What do 502, 503 and 504 from an ALB mean?. 502: target sent a bad reply. 503: no healthy targets. 504: target too slow. The code tells you where to look first. 503 › check target group health; 502 › app logs; 504 › slow DB query.
Interview card: What do 502, 503 and 504 from an ALB mean? · Download PNG
Interview card: Where should a 3-tier app keep its database password?. In AWS Secrets Manager, read by the app at runtime through its IAM role. Passwords in code end up on GitHub and get stolen in minutes. Secret prod/db read with GetSecretValue.
Interview card: Where should a 3-tier app keep its database password? · Download PNG
Interview card: What is the difference between high availability and fault tolerance?. HA: the app recovers quickly with a short break. Fault tolerant: users see no break at all. Fault tolerance needs more spare capacity, so it costs more. RDS Multi-AZ failover 1-2 min = HA. 2 AZs each able to carry full load = fault tolerant.
Interview card: What is the difference between high availability and fault tolerance? · Download PNG

More cards: Ravi Sir's Cards: AWS VPC and Subnets.