<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jingxi Deng</title>
    <description>The latest articles on DEV Community by Jingxi Deng (@jingxi_deng_1ab63ce12398e).</description>
    <link>https://dev.to/jingxi_deng_1ab63ce12398e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4177915%2F8aae5ea2-f070-4749-a41c-6d47ecb3bc1f.png</url>
      <title>DEV Community: Jingxi Deng</title>
      <link>https://dev.to/jingxi_deng_1ab63ce12398e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jingxi_deng_1ab63ce12398e"/>
    <language>en</language>
    <item>
      <title>Production-Ready Web Application Deployment on AWS</title>
      <dc:creator>Jingxi Deng</dc:creator>
      <pubDate>Mon, 12 Oct 2026 04:43:43 +0000</pubDate>
      <link>https://dev.to/jingxi_deng_1ab63ce12398e/production-ready-web-application-deployment-on-aws-6e5</link>
      <guid>https://dev.to/jingxi_deng_1ab63ce12398e/production-ready-web-application-deployment-on-aws-6e5</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;I deployed a secure, resilient, and highly available web application on AWS using EC2 using a VPC, auto load-balancer, auto scaling group across 2 availability zones.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stack:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;One VPC

&lt;code&gt;web-app-vpc&lt;/code&gt;

(10.0.0.0/16), 2 AZ (us-east-1a, us-east-1b), 4 subnets (2 public, 2 private)
- Internet Gateways and 1 NAT per AZ
- Application load balancer

&lt;code&gt;web-app-alb&lt;/code&gt;

in the public subnets, and an auto scaling group launching instances in the private subnets (one private subnet per AZ)
- Launch template with user-data boot strapped to it. No manual configs for each new instance launched by the scaling group.
- Instance profile

&lt;code&gt;LabInstanceProfile&lt;/code&gt;

(

&lt;code&gt;LabRole&lt;/code&gt;

), which lets instances use SSM and read Parameter Store.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Architecture
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwreu1r0ozt1o5a6kegd1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwreu1r0ozt1o5a6kegd1.jpg" alt=" " width="800" height="958"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network isolation:&lt;/strong&gt; instances only in private subnets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Least privilege perimeter:&lt;/strong&gt; app-sg ONLY takes port 80 from alb-sg.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No key pair on any instances:&lt;/strong&gt; Only SSM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No hard coded things:&lt;/strong&gt; nothing stored in user data of instances (all are read from a single source of truth that can be changed without editing instances).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self Healing:&lt;/strong&gt; ALB health checks re-routes traffic of failed instances to other instances. Auto scaling group replaces failed instances automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identical provisions:&lt;/strong&gt; every instance boots with the same launch template.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Network design
&lt;/h3&gt;

&lt;p&gt;The only things in public subnets are the load balancer and the NAT gateways. EC2 instances sit on the private subnets and don't have public IP.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Subnet&lt;/th&gt;
&lt;th&gt;AZ&lt;/th&gt;
&lt;th&gt;CIDR&lt;/th&gt;
&lt;th&gt;Route 0.0.0.0/0 -&amp;gt;&lt;/th&gt;
&lt;th&gt;Holds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;web-app-subnet-public1&lt;/td&gt;
&lt;td&gt;us-east-1a&lt;/td&gt;
&lt;td&gt;10.0.1.0/24&lt;/td&gt;
&lt;td&gt;Internet Gateway&lt;/td&gt;
&lt;td&gt;ALB node, NAT Gateway 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;web-app-subnet-public2&lt;/td&gt;
&lt;td&gt;us-east-1b&lt;/td&gt;
&lt;td&gt;10.0.2.0/24&lt;/td&gt;
&lt;td&gt;Internet Gateway&lt;/td&gt;
&lt;td&gt;ALB node, NAT Gateway 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;web-app-subnet-private1&lt;/td&gt;
&lt;td&gt;us-east-1a&lt;/td&gt;
&lt;td&gt;10.0.11.0/24&lt;/td&gt;
&lt;td&gt;NAT Gateway 1&lt;/td&gt;
&lt;td&gt;App instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;web-app-subnet-private2&lt;/td&gt;
&lt;td&gt;us-east-1b&lt;/td&gt;
&lt;td&gt;10.0.12.0/24&lt;/td&gt;
&lt;td&gt;NAT Gateway 2&lt;/td&gt;
&lt;td&gt;App instances&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Security
&lt;/h3&gt;

&lt;p&gt;The security group of instances only allow traffic from the load balancer&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Security group&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Source / destination&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;alb-sg&lt;/td&gt;
&lt;td&gt;Inbound&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;0.0.0.0/0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alb-sg&lt;/td&gt;
&lt;td&gt;Outbound&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;app-sg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;app-sg&lt;/td&gt;
&lt;td&gt;Inbound&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;alb-sg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;app-sg&lt;/td&gt;
&lt;td&gt;Outbound&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;0.0.0.0/0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;app-sg&lt;/td&gt;
&lt;td&gt;Outbound&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;0.0.0.0/0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Identity, access and secrets
&lt;/h3&gt;

&lt;p&gt;Instances only get creds from an IAM instance profile, and those creds are temporary and rotate automatically. No key pairs in the launch template and admin access is only through SSM so port 22 is never open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Since this was done in an AWS academy learner lab, I was blocked from creating IAM roles, so I defaulted to the "LabRole" that was provided. &lt;code&gt;LabRole&lt;/code&gt; is broader than needed, so a real prod app would use a new role with just the Session Manager permissions and &lt;code&gt;ssm:GetParameter&lt;/code&gt; scoped to &lt;code&gt;/webapp/*&lt;/code&gt;. &lt;code&gt;AmazonSSMManagedInstanceCore&lt;/code&gt; on its own isn't enough, since it lets the instance read every parameter in the account.&lt;/p&gt;

&lt;p&gt;A parameter called &lt;code&gt;/webapp/app_message&lt;/code&gt; is stored in Parameter Store. The instances' user-data fetches it at boot using the instance profile's creds. If the web app had secrets like API keys, they would go in Parameter Store as SecureString, so nothing is hardcoded in the user data of the launch template.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self Healing
&lt;/h3&gt;

&lt;p&gt;Every instance boots ready to serve with the setup instructions in the user-data of launch templates, so the auto scaling group can deploy new ones without human intervention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Launch Template:&lt;/strong&gt; &lt;code&gt;web-app-launch-template&lt;/code&gt; uses Amazon Linux 2023 x86, t3.micro, &lt;code&gt;app-sg&lt;/code&gt; security group (the group that only allows traffic from the load balancer), &lt;code&gt;LabInstanceProfile&lt;/code&gt;, IMDSv2 required so instances can know about themselves at runtime instead of needing to hard code credentials. Hop limit is set to 1 so it only reaches the instance itself and not any further.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target group:&lt;/strong&gt; HTTP 80, health check at &lt;code&gt;GET /health&lt;/code&gt;, every 10 seconds, unhealthy threshold at 2 times. So dead instances are removed in about 20 seconds. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto scaling group:&lt;/strong&gt; &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Subnets&lt;/td&gt;
&lt;td&gt;the two private subnets from each AZ in the VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Min / desired / max&lt;/td&gt;
&lt;td&gt;2 / 2 / 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health check type&lt;/td&gt;
&lt;td&gt;EC2 + ELB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling policy&lt;/td&gt;
&lt;td&gt;Target tracking, average CPU 50%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h1&gt;
  
  
  Trade-offs
&lt;/h1&gt;

&lt;p&gt;I went with one NAT gateway per AZ instead of a single shared one. It costs about double, but if one AZ goes down the private subnet in the other AZ still has internet, so the NAT isn't a single point of failure. For admin access I used SSM Session Manager instead of a bastion host, so there's no port 22, no SSH keys to lose, and no extra public instance, and the only catch is it depends on the SSM agent working. The instances use &lt;code&gt;LabRole&lt;/code&gt; because the learner lab won't let me create roles, which means they have more permissions than they need. In a real account I'd make a role scoped to just SSM and &lt;code&gt;/webapp/*&lt;/code&gt;. I kept config in Parameter Store instead of Secrets Manager since standard parameters are free, and Secrets Manager mostly adds auto rotation for about $0.40 per secret a month. The app only runs on HTTP because HTTPS needs a domain name for an ACM cert, which I didn't have for this project, so that's the first thing I'd add next. Last, I built everything in the AWS console because it was the fastest way to get it done in a time-limited lab, but the tradeoff is I can't rebuild it with one command. Next time I'd write it in Terraform.&lt;/p&gt;

&lt;h1&gt;
  
  
  Resilience Demonstration
&lt;/h1&gt;

&lt;p&gt;In this section I will demonstrate the resilience capability of my setup. The demo starts in its ideal healthy state, one instance in each of the two AZs, and the load balancer evenly distributing traffic between every instance. Then I will manually delete one of the instances.&lt;/p&gt;

&lt;p&gt;The list of instances in my project in their healthy state as shown in the target group:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkvaau4b1v94swdqn59cw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkvaau4b1v94swdqn59cw.png" alt=" " width="799" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my machine's terminal, I will run a command to continuously ping my project every second while showing which instance in which AZ the request was routed to. The screenshot shows the projects responding healthy 200 codes while being evenly routed between two different instances &lt;code&gt;i-0e6555acb4c09a6d3&lt;/code&gt; and &lt;code&gt;i-0ca5c7c0b17f67601&lt;/code&gt; in &lt;code&gt;us-east-1b&lt;/code&gt; and &lt;code&gt;us-east-1a&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fle6vc8g4jm3lezxhkk4u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fle6vc8g4jm3lezxhkk4u.png" alt=" " width="733" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I will intentionally destroy one of the two instances by navigating to EC2 and deleting the instance &lt;code&gt;i-0e6555acb4c09a6d3&lt;/code&gt; (the instance in us-east-1b). Since this was the only instance in &lt;code&gt;us-east-1b&lt;/code&gt;, this also kind of simulates losing a whole AZ, since that zone has no compute left at all.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faj5xiz9kf28py3r0sjd5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faj5xiz9kf28py3r0sjd5.png" alt=" " width="797" height="143"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Checking on my target group again I can see instance &lt;code&gt;i-0e6555acb4c09a6d3&lt;/code&gt; is gone and only one instance remains.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx2868b9bt7stkjl8447e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx2868b9bt7stkjl8447e.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While that was happening, the terminal showed some &lt;code&gt;000 FAIL&lt;/code&gt; lines for about 15 seconds. That's because for a little while the load balancer didn't know the instance was dead yet, so some requests still got sent to it and timed out after 3 seconds. Once the dead instance failed 2 health checks in a row (every 10 seconds), the load balancer pulled it out and sent everything to the healthy instance in &lt;code&gt;us-east-1a&lt;/code&gt;, and the FAILs stopped. (Side note: the instance ID printed next to each FAIL is just left over from the previous response, since a failed curl doesn't write a new one.)&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo0gzc3l5b7mxg17mr30m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo0gzc3l5b7mxg17mr30m.png" alt=" " width="397" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2 seconds after the auto scaling group noticed the old instance in &lt;code&gt;us-east-1b&lt;/code&gt; was gone, it launched a new one &lt;code&gt;in response to an unhealthy instance needing to be replaced&lt;/code&gt;. The launch itself took about 6 seconds (1:44:15 to 1:44:21), and after booting and running the user data the new instance showed up as healthy in the target group at around 1:46, so about 2 minutes from kill to fully recovered, with no clicks from me.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfu8btaw64we823fkwwj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfu8btaw64we823fkwwj.png" alt=" " width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the new instance was up the scaling group once again had 1 instance in each availability zone. The terminal now prints traffic being evenly rerouted between the newly created &lt;code&gt;i-0266c702496739b06&lt;/code&gt; in &lt;code&gt;us-east-1b&lt;/code&gt; and the old unaffected &lt;code&gt;i-0ca5c7c0b17f67601&lt;/code&gt; in &lt;code&gt;us-east-1a&lt;/code&gt;!&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flwhcl7247xd9pz2iawlp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flwhcl7247xd9pz2iawlp.png" alt=" " width="334" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From this demo we can see that the load balancer evenly distributes traffic between each instance. The load balancer detects when an instance goes unhealthy, and reroutes traffic to healthy instances gracefully, and the auto-scaling group detects the anomaly (instance count going below my specified minimum of 2) which triggered an immediate provisioning of a new instance at the correct availability zone.&lt;/p&gt;
&lt;h1&gt;
  
  
  Build Walkthrough
&lt;/h1&gt;

&lt;p&gt;Here's how I implemented the entire secure &amp;amp; highly available web app:&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Make a VPC with 2 availability zones
&lt;/h3&gt;

&lt;p&gt;Log into your AWS console and enter the VPC page. Click "Create VPC"&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl93uzmwvs6ewmqc10nxa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl93uzmwvs6ewmqc10nxa.png" alt=" " width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select the same settings as in the image and click "Create". Wait 30 seconds and we've just created a VPC with 2 availability zones, with a public and private subnet in each availability zone.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F641jkwdtbq2lnvmwscvm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F641jkwdtbq2lnvmwscvm.png" alt=" " width="800" height="1043"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Make a security group for the load balancer, and another for the instances that only take traffic from the load balancer
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgexxwpmtsjfpd54rt957.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgexxwpmtsjfpd54rt957.png" alt=" " width="800" height="256"&gt;&lt;/a&gt;&lt;br&gt;
Then at the top left corner type "EC2" and click on the EC2 icon to jump to the EC2 dashboard.&lt;/p&gt;

&lt;p&gt;On the left sidebar go to Network &amp;amp; Security -&amp;gt; Security Groups and click "Create security group".&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqqia12z4s5a6n8s5koyq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqqia12z4s5a6n8s5koyq.png" alt=" " width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First we make &lt;code&gt;alb-sg&lt;/code&gt; for the load balancer. Give it a name and make sure to switch the VPC to &lt;code&gt;web-app-vpc&lt;/code&gt; (it picks the default VPC on its own, which got me the first time). Add one inbound rule with type HTTP and source Anywhere-IPv4 (0.0.0.0/0). This is the only thing in the whole project that the internet can reach. Here's what it looks like once it's made:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp63ax717chhfi9q4jjhs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp63ax717chhfi9q4jjhs.png" alt=" " width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then make another one called &lt;code&gt;app-sg&lt;/code&gt; for the instances, also in &lt;code&gt;web-app-vpc&lt;/code&gt;. Add an inbound rule with type HTTP, source Custom, and pick &lt;code&gt;alb-sg&lt;/code&gt; from the dropdown (it shows up as its &lt;code&gt;sg-&lt;/code&gt; ID). Now only the load balancer can talk to the instances. Not the public subnet, not my IP, nothing else. And no SSH rule anywhere.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1rbokv1df2s39t05df54.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1rbokv1df2s39t05df54.png" alt=" " width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Parameter Store: avoid hardcoding data in instances
&lt;/h3&gt;

&lt;p&gt;Search "Systems Manager" in the top search bar, then on the left sidebar click Parameter Store -&amp;gt; "Create parameter". I named mine &lt;code&gt;/webapp/app_message&lt;/code&gt;, left it as Standard tier and String type, and typed in the message I want the web page to show. Every instance reads this when it boots, so I never have to put it in the launch template.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu15ect124fn2otd917j6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu15ect124fn2otd917j6.png" alt=" " width="799" height="612"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  4. IAM role for the instances (blocked in learner lab, so I used LabInstanceProfile)
&lt;/h3&gt;

&lt;p&gt;The plan was to make my own role &lt;code&gt;web-app-instances-role&lt;/code&gt; with &lt;code&gt;AmazonSSMManagedInstanceCore&lt;/code&gt; and a policy that can only read &lt;code&gt;/webapp/*&lt;/code&gt;. So I went to IAM -&amp;gt; Roles -&amp;gt; "Create role", picked AWS service -&amp;gt; EC2, and... the learner lab doesn't let me create roles:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10gzrxdwqihbnqpctk4m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10gzrxdwqihbnqpctk4m.png" alt=" " width="799" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I just used the &lt;code&gt;LabRole&lt;/code&gt; that the lab already gives you (it gets attached to instances through &lt;code&gt;LabInstanceProfile&lt;/code&gt;). It can do SSM and read Parameter Store, which is all I need, but it can do a lot more than that too. In a real account I'd use the smaller role I talked about in the Identity section.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frrt2gka8aekozrp31dt2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frrt2gka8aekozrp31dt2.png" alt=" " width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Launch template for the auto-scaling instances
&lt;/h3&gt;

&lt;p&gt;Go back to EC2 and click Instances -&amp;gt; Launch Templates -&amp;gt; "Create launch template". I named it &lt;code&gt;web-app-launch-template&lt;/code&gt; and picked Amazon Linux 2023 (64-bit x86).&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi968s8bxkgfjowondtfv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi968s8bxkgfjowondtfv.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instance type is t3.micro. For key pair pick "Don't include in launch template" since nobody is going to SSH into these. Subnet is also "Don't include" because the auto scaling group picks the subnet later. For security group pick &lt;code&gt;app-sg&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgdgxmcntuqpnq59toaqz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgdgxmcntuqpnq59toaqz.png" alt=" " width="800" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open up Advanced details and set the IAM instance profile to &lt;code&gt;LabInstanceProfile&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq7zk94r2rqf9ey4w77pu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq7zk94r2rqf9ey4w77pu.png" alt=" " width="799" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scroll down a bit and set metadata version to "V2 only (token required)" and hop limit to 1. Then paste the script below into User data. This script is basically the whole deployment. It installs nginx, grabs the instance ID and AZ, reads &lt;code&gt;/webapp/app_message&lt;/code&gt; from Parameter Store, writes the web page, and adds a &lt;code&gt;/health&lt;/code&gt; page for the load balancer to check. Then click "Create launch template".&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnfghb2yz2lplos4v9us.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnfghb2yz2lplos4v9us.png" alt=" " width="800" height="819"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx

&lt;span class="c"&gt;# Get instance ID and AZ via IMDSv2&lt;/span&gt;
&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sX&lt;/span&gt; PUT http://169.254.169.254/latest/api/token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-aws-ec2-metadata-token-ttl-seconds: 300"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
md&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-aws-ec2-metadata-token: &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://169.254.169.254/latest/meta-data/&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;md instance-id&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;AZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;md placement/availability-zone&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;AZ&lt;/span&gt;&lt;span class="p"&gt;%?&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Read config from Parameter Store (falls back if it fails)&lt;/span&gt;
&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ssm get-parameter &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; /webapp/app_message &lt;span class="nt"&gt;--query&lt;/span&gt; Parameter.Value &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Hello (param read failed)"&lt;/span&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /usr/share/nginx/html/index.html &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
&amp;lt;h1&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="sh"&gt;&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;Instance: &lt;/span&gt;&lt;span class="nv"&gt;$ID&lt;/span&gt;&lt;span class="sh"&gt;&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;AZ: &lt;/span&gt;&lt;span class="nv"&gt;$AZ&lt;/span&gt;&lt;span class="sh"&gt;&amp;lt;/p&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;ok &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /usr/share/nginx/html/health

systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Target group: health checks that decide which instances get traffic
&lt;/h3&gt;

&lt;p&gt;In EC2 go to Load Balancing -&amp;gt; Target Groups -&amp;gt; "Create target group". Pick Instances, name it &lt;code&gt;web-app-target-group&lt;/code&gt;, HTTP on port 80, and VPC &lt;code&gt;web-app-vpc&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn4f1f4jez4m2aoony9gq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn4f1f4jez4m2aoony9gq.png" alt=" " width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Set the health check path to &lt;code&gt;/health&lt;/code&gt;. Under Advanced health check settings I set healthy and unhealthy threshold to 2, timeout to 5 seconds, and interval to 10 seconds. So if an instance stops responding, it gets kicked out of rotation in about 20 seconds.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5h8liauylk39wzj7q3pj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5h8liauylk39wzj7q3pj.png" alt=" " width="800" height="821"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click next and don't register any targets. The auto scaling group will add the instances by itself. Then click "Create target group".&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpymx8z9xs35hifxc4jgg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpymx8z9xs35hifxc4jgg.png" alt=" " width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Application Load Balancer in the public subnets
&lt;/h3&gt;

&lt;p&gt;Go to Load Balancing -&amp;gt; Load Balancers -&amp;gt; "Create load balancer" and pick Application Load Balancer. Here's what I set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name &lt;code&gt;web-app-alb&lt;/code&gt;, Internet-facing, IPv4&lt;/li&gt;
&lt;li&gt;VPC &lt;code&gt;web-app-vpc&lt;/code&gt;, check both AZs and pick the public subnet in each one (&lt;code&gt;web-app-subnet-public1&lt;/code&gt; and &lt;code&gt;web-app-subnet-public2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Security group: remove &lt;code&gt;default&lt;/code&gt; and add &lt;code&gt;alb-sg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Listener HTTP:80 -&amp;gt; forward to &lt;code&gt;web-app-target-group&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn96wgrdmd1dqheq1s6ak.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn96wgrdmd1dqheq1s6ak.png" alt=" " width="800" height="1060"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click "Create load balancer" and wait a few minutes for it to spin up. Once it's active, copy the DNS name. That's the link to the app.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrex75xvuop4pxrwqqef.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrex75xvuop4pxrwqqef.png" alt=" " width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Auto Scaling Group in the private subnets
&lt;/h3&gt;

&lt;p&gt;Go to Auto Scaling -&amp;gt; Auto Scaling Groups -&amp;gt; "Create Auto Scaling group". I named it &lt;code&gt;web-app-asg&lt;/code&gt; and picked &lt;code&gt;web-app-launch-template&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fip5sgpeipqfbrjzlujv0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fip5sgpeipqfbrjzlujv0.png" alt=" " width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For network pick &lt;code&gt;web-app-vpc&lt;/code&gt; and both private subnets (&lt;code&gt;web-app-subnet-private1&lt;/code&gt; and &lt;code&gt;web-app-subnet-private2&lt;/code&gt;). This is the part that keeps the instances hidden (no public IP) and spreads them across both AZs. Leave AZ distribution on "Balanced best effort".&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb9tjtcgkkt4kk000n67l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb9tjtcgkkt4kk000n67l.png" alt=" " width="800" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For load balancing pick "Attach to an existing load balancer" -&amp;gt; "Choose from your load balancer target groups" -&amp;gt; &lt;code&gt;web-app-target-group&lt;/code&gt;. Then check "Turn on Elastic Load Balancing health checks". Don't skip this one. Without it the scaling group only checks if the machine is on, not if the app is actually working, so if nginx crashed it would never get replaced.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0sacmgpsciq4mkehs4y1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0sacmgpsciq4mkehs4y1.png" alt=" " width="800" height="770"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Set desired to 2, min to 2, and max to 4. I also added a target tracking policy on average CPU at 50 so it can add instances if traffic gets heavy. Skip notifications and tags and click "Create Auto Scaling group".&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faafzkzuqenlmba6sx2ux.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faafzkzuqenlmba6sx2ux.png" alt=" " width="799" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After about 3-5 minutes both instances boot up on their own and show up as healthy in the target group, one in each AZ. I didn't touch either of them:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20xi6cmdtzo88qrjvtam.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20xi6cmdtzo88qrjvtam.png" alt=" " width="799" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And curling the load balancer's DNS name gives back a 200 with my Parameter Store message and the instance that answered:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyq6vpxu8toj04euw72eu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyq6vpxu8toj04euw72eu.png" alt=" " width="799" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Tightening the instances' outbound rules
&lt;/h3&gt;

&lt;p&gt;At this point &lt;code&gt;app-sg&lt;/code&gt; still let the instances send any traffic out. They only really need to reach Parameter Store, SSM, and the package repos, so I went back to Security Groups -&amp;gt; &lt;code&gt;app-sg&lt;/code&gt; -&amp;gt; Outbound rules -&amp;gt; "Edit outbound rules" and swapped "All traffic" for just two rules: HTTPS 443 and HTTP 80 to 0.0.0.0/0. Security groups remember connections that came in, so traffic from the load balancer still works fine, and the app kept giving back 200s after the change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3lpf8htv02jewokwr1o2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3lpf8htv02jewokwr1o2.png" alt=" " width="800" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
    </item>
  </channel>
</rss>
