<?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: Hashir Saud Khan</title>
    <description>The latest articles on DEV Community by Hashir Saud Khan (@hashirsaudkhan).</description>
    <link>https://dev.to/hashirsaudkhan</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%2F1256353%2F88066382-91b2-4814-a05e-284da6bed06b.jpeg</url>
      <title>DEV Community: Hashir Saud Khan</title>
      <link>https://dev.to/hashirsaudkhan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hashirsaudkhan"/>
    <language>en</language>
    <item>
      <title>Understanding AWS Route 53 Routing Policies</title>
      <dc:creator>Hashir Saud Khan</dc:creator>
      <pubDate>Fri, 07 Aug 2026 08:10:32 +0000</pubDate>
      <link>https://dev.to/hashirsaudkhan/understanding-aws-route-53-routing-policies-43b5</link>
      <guid>https://dev.to/hashirsaudkhan/understanding-aws-route-53-routing-policies-43b5</guid>
      <description>&lt;h1&gt;
  
  
  aws #route53 #dns #networking
&lt;/h1&gt;

&lt;h2&gt;
  
  
  AWS Route 53 Routing Policies Explained
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;INTRO&lt;/strong&gt;&lt;br&gt;
Route 53 is AWS's &lt;strong&gt;DNS service&lt;/strong&gt;. When someone types &lt;code&gt;www.example.com&lt;/code&gt; into their browser, Route 53 is what decides which server or IP address that request actually gets sent to.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;routing policy&lt;/strong&gt; is simply the rule Route 53 follows to answer one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Which server or location should this user be sent to?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are eight routing policies, and each one answers that question a different way. Let's go through them one at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. SIMPLE ROUTING POLICY&lt;/strong&gt;&lt;br&gt;
The most basic option. You have one domain, &lt;code&gt;myapp.com&lt;/code&gt;, and one server, &lt;code&gt;10.0.0.1&lt;/code&gt;. Route 53 sends every single user to that same server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User → myapp.com → Server 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this when you only have one server and don't need any special routing logic at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Easy way to remember it:&lt;/strong&gt; "Send everyone to the same place."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. WEIGHTED ROUTING POLICY&lt;/strong&gt;&lt;br&gt;
Here, traffic is split across servers based on a &lt;strong&gt;percentage/weight&lt;/strong&gt; you assign.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server 1 → Weight 80
Server 2 → Weight 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Out of 100 users, roughly 80 land on Server 1 and 20 land on Server 2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real example:&lt;/strong&gt; You're testing a new version of your app. You send 90% of traffic to the old version and 10% to the new one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Users
  ↓
Route 53
 ↙    ↘
90%    10%
Old    New
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the new version holds up well, you can gradually shift the split to 50/50, then eventually 100% new version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Easy way to remember it:&lt;/strong&gt; "Divide traffic by percentage."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. LATENCY ROUTING POLICY&lt;/strong&gt;&lt;br&gt;
Route 53 sends the user to whichever server gives them the &lt;strong&gt;lowest latency&lt;/strong&gt; — the fastest response time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pakistan  → Karachi
USA       → Virginia
Europe    → Frankfurt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user connecting from Pakistan would generally be routed to the Karachi endpoint, if that's the server offering the lowest latency for them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pakistan User
    ↓
Route 53
    ↓
Karachi Server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user in the USA gets routed differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USA User
    ↓
Route 53
    ↓
Virginia Server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Easy way to remember it:&lt;/strong&gt; "Send the user to whichever server is fastest for them."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. FAILOVER ROUTING POLICY&lt;/strong&gt;&lt;br&gt;
This one exists for &lt;strong&gt;backup servers&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Primary Server   → Working
Secondary Server → Backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normally, all traffic goes to the primary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User → Primary Server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if a health check detects the primary is down, traffic automatically shifts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Primary ❌
   ↓
Route 53
   ↓
Secondary ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Real example:&lt;/strong&gt; Your main website server is in Karachi. Your backup server sits in Singapore. If the Karachi server goes down, Route 53 automatically starts sending users to the Singapore server instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Easy way to remember it:&lt;/strong&gt; "Primary down? Send them to the backup."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. GEOLOCATION ROUTING POLICY&lt;/strong&gt;&lt;br&gt;
This routes users based on their &lt;strong&gt;actual location&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pakistan user → Pakistan server
USA user      → USA server
UK user       → UK server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Say you run &lt;code&gt;example.com&lt;/code&gt; and you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pakistani users to see the Pakistan-specific site&lt;/li&gt;
&lt;li&gt;US users to see the US-specific site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Geolocation routing is exactly built for that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important distinction:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt; picks whichever server is &lt;em&gt;fastest&lt;/em&gt;, regardless of where the user actually is&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geolocation&lt;/strong&gt; picks a server based on the user's &lt;em&gt;country&lt;/em&gt;, regardless of which one is technically fastest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Easy way to remember it:&lt;/strong&gt; "Where is the user coming from?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. GEOPROXIMITY ROUTING POLICY&lt;/strong&gt;&lt;br&gt;
This is also location-based, but here you can actually &lt;strong&gt;adjust the size of the geographic area&lt;/strong&gt; each server covers.&lt;/p&gt;

&lt;p&gt;Say you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS Lahore
AWS Singapore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could decide that most of Pakistan's traffic should go to Lahore, while Singapore and nearby regions go to the Singapore server. You can also apply a &lt;strong&gt;bias&lt;/strong&gt; — for example, if you want the Lahore server to cover a larger geographic area than it normally would, you increase its bias to expand its coverage zone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Easy way to remember it:&lt;/strong&gt; "Control the geographic boundary to steer traffic."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. MULTIVALUE ANSWER ROUTING POLICY&lt;/strong&gt;&lt;br&gt;
Here, Route 53 can return &lt;strong&gt;multiple healthy IP addresses&lt;/strong&gt; in a single response instead of just one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server 1 → 10.0.0.1
Server 2 → 10.0.0.2
Server 3 → 10.0.0.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Route 53's DNS response might include all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DNS Response:
10.0.0.1
10.0.0.2
10.0.0.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client then picks one of those addresses to actually connect to. If health checks are enabled, any unhealthy endpoint gets excluded from that list automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Easy way to remember it:&lt;/strong&gt; "Return several healthy server IPs instead of just one."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. IP-BASED ROUTING POLICY&lt;/strong&gt;&lt;br&gt;
This routes traffic based on the &lt;strong&gt;user's source IP address&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User IP: 10.x.x.x → Server A
User IP: 20.x.x.x → Server B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can map specific IP ranges directly to specific endpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Company employees' IP range → Internal Server
Everyone else                → Public Server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a good fit for IP-based routing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Easy way to remember it:&lt;/strong&gt; "Check the user's IP, then route accordingly."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QUICK REFERENCE TABLE&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;Policy&lt;/th&gt;
&lt;th&gt;Simple meaning&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simple&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Everyone gets the same server&lt;/td&gt;
&lt;td&gt;Everyone → Server A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Weighted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Split by percentage&lt;/td&gt;
&lt;td&gt;80% → A, 20% → B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fastest/lowest-latency server&lt;/td&gt;
&lt;td&gt;Pakistan → Karachi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Failover&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Primary down → backup takes over&lt;/td&gt;
&lt;td&gt;A ❌ → B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geolocation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Based on the user's location&lt;/td&gt;
&lt;td&gt;Pakistan → Pakistan server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geoproximity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Based on geographic area/bias&lt;/td&gt;
&lt;td&gt;Lahore gets a larger coverage area&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multivalue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Returns multiple healthy IPs&lt;/td&gt;
&lt;td&gt;A + B + C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP-based&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Based on the user's source IP&lt;/td&gt;
&lt;td&gt;IP range A → Server A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;SHORTCUT FOR REMEMBERING THESE&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simple = Same&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weighted = Percentage&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency = Fastest&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failover = Backup&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Geolocation = Where the user is&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Geoproximity = Geographic area control&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multivalue = Multiple IPs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP-based = User's IP&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're studying this for an AWS exam, the distinction between &lt;strong&gt;Latency vs. Geolocation vs. Geoproximity&lt;/strong&gt; is the one worth spending the most time on — they all sound similar but solve genuinely different problems.&lt;/p&gt;

&lt;h1&gt;
  
  
  AWS #Route53 #DNS #Networking #CloudComputing
&lt;/h1&gt;

</description>
    </item>
    <item>
      <title>Scaling and Load Balancing Your Architecture on AWS ⚖️</title>
      <dc:creator>Hashir Saud Khan</dc:creator>
      <pubDate>Thu, 06 Aug 2026 07:22:33 +0000</pubDate>
      <link>https://dev.to/hashirsaudkhan/scaling-and-load-balancing-your-architecture-on-aws-5cbo</link>
      <guid>https://dev.to/hashirsaudkhan/scaling-and-load-balancing-your-architecture-on-aws-5cbo</guid>
      <description>&lt;p&gt;&lt;strong&gt;INTRO&lt;/strong&gt;&lt;br&gt;
This lab takes a single web server and turns it into a properly scalable, load-balanced setup. You start with one EC2 instance running a web app, and by the end, you have a load balancer spreading traffic across multiple instances, an Auto Scaling group automatically adding or removing instances based on CPU load, and CloudWatch alarms watching it all in the background.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starting Arcitecture&lt;/strong&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%2Fvt24sg3ocyh2doucf687.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%2Fvt24sg3ocyh2doucf687.png" alt="starting arcitecture" width="799" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final Arcitecture&lt;/strong&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%2Fvxm1qdkqyg5l1a1z7gjd.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%2Fvxm1qdkqyg5l1a1z7gjd.png" alt="Final Arcitecture" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KEY TERMS YOU'LL SEE&lt;/strong&gt;&lt;br&gt;
Before jumping into steps, here's every term this lab throws at you, defined once so nothing feels unfamiliar later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AMI (Amazon Machine Image)&lt;/strong&gt; — a saved snapshot of an instance's boot disk. Once you have one, you can launch as many identical instances from it as you want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load Balancer (specifically an Application Load Balancer)&lt;/strong&gt; — a service that sits in front of your instances and spreads incoming traffic across them, so no single instance gets overwhelmed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target Group&lt;/strong&gt; — the list of instances a load balancer is actually allowed to send traffic to. The load balancer doesn't talk to instances directly; it talks to a target group, and the target group tracks which instances are in it and whether they're healthy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Listener&lt;/strong&gt; — a rule on the load balancer that checks for connection requests on a specific port and protocol (like HTTP on port 80) and forwards matching traffic to a target group. It's the piece that actually connects "traffic coming in" to "which target group handles it."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Launch Template&lt;/strong&gt; — a saved configuration (AMI, instance type, security group, etc.) that tells AWS exactly how to launch a new instance. Auto Scaling doesn't guess what a new instance should look like — it reads this template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto Scaling Group (ASG)&lt;/strong&gt; — a group of instances that AWS automatically keeps within a size range you define (a minimum, a desired count, and a maximum), launching or terminating instances as needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch Alarm&lt;/strong&gt; — a watcher that tracks a metric (like average CPU) and flips into an "In alarm" state when a threshold is crossed, which is what actually triggers Auto Scaling to act.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep these seven in mind — every task below is really just building one of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 1: CREATE AN AMI FROM THE EXISTING WEB SERVER&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the EC2 console and go to &lt;strong&gt;Instances&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;Web Server 1&lt;/strong&gt; instance (it should be in a &lt;strong&gt;Running&lt;/strong&gt; state)&lt;/li&gt;
&lt;li&gt;From the &lt;strong&gt;Actions&lt;/strong&gt; dropdown, choose &lt;strong&gt;Image and templates → Create image&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Set:

&lt;ul&gt;
&lt;li&gt;Image name: &lt;code&gt;Web Server AMI&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Image description: &lt;code&gt;Lab AMI for Web Server&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Create image&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The confirmation screen gives you the new AMI's ID — you'll need this AMI later when setting up the launch template.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; Auto Scaling needs a repeatable template for new instances, and that template starts with an AMI — a frozen copy of a working server, ready to be cloned on demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 2: CREATE A LOAD BALANCER&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the EC2 console, go to &lt;strong&gt;Load Balancing → Load Balancers&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Create load balancer&lt;/strong&gt;, then under &lt;strong&gt;Application Load Balancer&lt;/strong&gt;, choose &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under Basic configuration, set the Load balancer name: &lt;code&gt;LabELB&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Under Network mapping:

&lt;ul&gt;
&lt;li&gt;VPC: &lt;strong&gt;Lab VPC&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Mappings: select both Availability Zones&lt;/li&gt;
&lt;li&gt;First Availability Zone: &lt;strong&gt;Public Subnet 1&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Second Availability Zone: &lt;strong&gt;Public Subnet 2&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Under Security groups: remove the default security group, then attach &lt;strong&gt;Web Security Group&lt;/strong&gt; (already created for you, permits HTTP)&lt;/li&gt;
&lt;li&gt;Under Listeners and routing, choose &lt;strong&gt;Create target group&lt;/strong&gt; — this opens a new tab&lt;/li&gt;
&lt;li&gt;In that new tab, configure the target group:

&lt;ul&gt;
&lt;li&gt;Target type: &lt;strong&gt;Instances&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Target group name: &lt;code&gt;lab-target-group&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Next&lt;/strong&gt;, then on the Register targets page, choose &lt;strong&gt;Create target group&lt;/strong&gt; (don't register any instances yet — the Auto Scaling group will do that automatically later)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Close that tab and return to the load balancer tab&lt;/li&gt;
&lt;li&gt;Next to the Forward to dropdown under Default action, choose the refresh icon, then select &lt;strong&gt;lab-target-group&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Create load balancer&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Once created, choose &lt;strong&gt;View load balancer&lt;/strong&gt;, then copy the load balancer's &lt;strong&gt;DNS name&lt;/strong&gt; into a text editor — you'll need it soon to actually open the app in a browser&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; A load balancer without a target group has nowhere to send traffic — the target group is what actually tracks which instances exist and whether they're healthy enough to receive requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 3: CREATE A LAUNCH TEMPLATE&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the EC2 console, go to &lt;strong&gt;Instances → Launch Templates&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Create launch template&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Set:

&lt;ul&gt;
&lt;li&gt;Launch template name: &lt;code&gt;lab-app-launch-template&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Template version description: &lt;code&gt;A web server for the load test app&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check &lt;strong&gt;Provide guidance to help me set up a template that I can use with EC2 Auto Scaling&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Under Application and OS Images, go to the &lt;strong&gt;My AMIs&lt;/strong&gt; tab — &lt;strong&gt;Web Server AMI&lt;/strong&gt; (the one you created in Task 1) should already be selected&lt;/li&gt;
&lt;li&gt;Under Instance type, choose &lt;strong&gt;t3.micro&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under Key pair (login), leave it set to &lt;strong&gt;Don't include in launch template&lt;/strong&gt; — you won't need to SSH into these instances directly&lt;/li&gt;
&lt;li&gt;Under Network settings, set Security groups to &lt;strong&gt;Web Security Group&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Create launch template&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; This is the recipe Auto Scaling reads every time it needs to launch a new instance — AMI, instance type, and security group all bundled together so nothing has to be configured manually per-instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 4: CREATE AN AUTO SCALING GROUP&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;From the launch template you just created, choose &lt;strong&gt;Actions → Create Auto Scaling group&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Auto Scaling group name: &lt;code&gt;Lab Auto Scaling Group&lt;/code&gt;, then choose &lt;strong&gt;Next&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under Network:

&lt;ul&gt;
&lt;li&gt;VPC: &lt;strong&gt;Lab VPC&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Availability Zones and subnets: select &lt;strong&gt;Private Subnet 1 (10.0.1.0/24)&lt;/strong&gt; and &lt;strong&gt;Private Subnet 2 (10.0.3.0/24)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Next&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;On the advanced options page:

&lt;ul&gt;
&lt;li&gt;Under Load balancing, choose &lt;strong&gt;Attach to an existing load balancer&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Choose from your load balancer target groups&lt;/strong&gt;, then select &lt;strong&gt;lab-target-group | HTTP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under Health checks, set Health check type to &lt;strong&gt;ELB&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Next&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;On the group size and scaling policies page:

&lt;ul&gt;
&lt;li&gt;Desired capacity: &lt;code&gt;2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Minimum capacity: &lt;code&gt;2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Maximum capacity: &lt;code&gt;4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Under Scaling policies, choose &lt;strong&gt;Target tracking scaling policy&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Metric type: &lt;strong&gt;Average CPU utilization&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Target value: &lt;code&gt;50&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Next&lt;/strong&gt; through the notifications page (nothing to configure)&lt;/li&gt;
&lt;li&gt;On the Add tags page, add a tag:

&lt;ul&gt;
&lt;li&gt;Key: &lt;code&gt;Name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Value: &lt;code&gt;Lab Instance&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Next&lt;/strong&gt;, then &lt;strong&gt;Create Auto Scaling group&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note: instances launch into &lt;strong&gt;private subnets&lt;/strong&gt; — they're not directly reachable from the internet, only through the load balancer sitting in the public subnets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; This single group definition replaces manually launching, monitoring, and terminating instances yourself — you just declare the size range and the CPU target, and AWS keeps reality matching that declaration on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 5: VERIFY LOAD BALANCING IS WORKING&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to EC2 → &lt;strong&gt;Instances&lt;/strong&gt; — you should see two new instances named &lt;strong&gt;Lab Instance&lt;/strong&gt;, launched automatically by the Auto Scaling group&lt;/li&gt;
&lt;li&gt;Go to Load Balancing → &lt;strong&gt;Target Groups&lt;/strong&gt;, choose &lt;strong&gt;lab-target-group&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under Registered targets, both &lt;strong&gt;Lab Instance&lt;/strong&gt; entries should appear&lt;/li&gt;
&lt;li&gt;Wait until both show a &lt;strong&gt;Healthy&lt;/strong&gt; status (refresh as needed) — this confirms each instance is passing the load balancer's health check and is eligible to receive traffic&lt;/li&gt;
&lt;li&gt;Open a new browser tab, paste the load balancer's DNS name from Task 2, and press Enter — the &lt;strong&gt;Load Test&lt;/strong&gt; application should load&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; A healthy status is the load balancer's way of confirming an instance is actually ready for traffic — without this check, the load balancer might send requests to an instance that's still booting or broken, and users would see failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 6: TEST AUTO SCALING UNDER LOAD&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the CloudWatch console → &lt;strong&gt;Alarms → All alarms&lt;/strong&gt; — you'll see two alarms, created automatically by the Auto Scaling group (a "high CPU" alarm and a "low CPU" alarm), keeping the group's average CPU near the 50% target within its 2–4 instance range&lt;/li&gt;
&lt;li&gt;Check the alarm with &lt;strong&gt;AlarmHigh&lt;/strong&gt; in its name — it should currently show state &lt;strong&gt;OK&lt;/strong&gt; (CPU is low, nothing to react to yet)&lt;/li&gt;
&lt;li&gt;Go back to the &lt;strong&gt;Load Test&lt;/strong&gt; application tab, and choose &lt;strong&gt;Load Test&lt;/strong&gt; next to the AWS logo — this deliberately spikes CPU usage across the running instances, and the page auto-refreshes to keep the load going&lt;/li&gt;
&lt;li&gt;Return to the CloudWatch console — within about 5 minutes, watch for:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AlarmLow&lt;/strong&gt; flipping to &lt;strong&gt;OK&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AlarmHigh&lt;/strong&gt; flipping to &lt;strong&gt;In alarm&lt;/strong&gt;, once average CPU crosses 50% for more than 3 minutes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Once &lt;strong&gt;AlarmHigh&lt;/strong&gt; is &lt;strong&gt;In alarm&lt;/strong&gt;, go back to EC2 → &lt;strong&gt;Instances&lt;/strong&gt; — you should now see more than two &lt;strong&gt;Lab Instance&lt;/strong&gt; entries running, launched automatically in response to the alarm&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; This is the actual proof the whole setup works — not just that instances exist, but that real load causes CloudWatch to detect it and Auto Scaling to react to it, exactly as configured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 7: TERMINATE THE ORIGINAL WEB SERVER&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to EC2 → Instances, select only &lt;strong&gt;Web Server 1&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;From &lt;strong&gt;Instance state&lt;/strong&gt;, choose &lt;strong&gt;Terminate instance&lt;/strong&gt;, then confirm with &lt;strong&gt;Terminate&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; Web Server 1 already did its one job — becoming the AMI everything else is built from. Keeping it running afterward serves no purpose and just adds an untracked, unmanaged instance sitting outside your Auto Scaling group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OPTIONAL CHALLENGE: CREATE AN AMI USING THE AWS CLI&lt;/strong&gt;&lt;br&gt;
If there's time left, repeat Task 1's goal — but through the CLI instead of the console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect to one of your running EC2 instances using &lt;strong&gt;EC2 Instance Connect&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Configure your AWS CLI credentials&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;aws ec2 create-image&lt;/code&gt; command, supplying an AMI name and the instance ID you want to image&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; Every console action in this lab has a CLI equivalent — this challenge is a reminder that nothing here is console-exclusive; it's all scriptable and automatable the same way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHY THIS MATTERS OVERALL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A load balancer, target group, launch template, and Auto Scaling group aren't four separate features — they're one pipeline: template defines the instance, the group manages how many exist, and the target group is how the load balancer finds them&lt;/li&gt;
&lt;li&gt;Placing Auto Scaling instances in private subnets while the load balancer sits in public subnets is a real security pattern — the only path in is through the load balancer, nothing is directly internet-facing&lt;/li&gt;
&lt;li&gt;CloudWatch alarms are the trigger, not the mechanism — Auto Scaling still needs a target tracking policy telling it what to do once an alarm fires&lt;/li&gt;
&lt;li&gt;Terminating the original instance after imaging it is easy to forget, but leaving it running defeats the purpose of consolidating everything into a managed, scalable group&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AWS #LoadBalancing #AutoScaling #CloudWatch #EC2 #CloudComputing
&lt;/h1&gt;

</description>
    </item>
    <item>
      <title>Building a Serverless Sales Report with AWS Lambda, SNS, and CloudWatch 📊⚡</title>
      <dc:creator>Hashir Saud Khan</dc:creator>
      <pubDate>Thu, 06 Aug 2026 07:04:26 +0000</pubDate>
      <link>https://dev.to/hashirsaudkhan/building-a-serverless-sales-report-with-aws-lambda-sns-and-cloudwatch-12l5</link>
      <guid>https://dev.to/hashirsaudkhan/building-a-serverless-sales-report-with-aws-lambda-sns-and-cloudwatch-12l5</guid>
      <description>&lt;h1&gt;
  
  
  aws #lambda #serverless #tutorial
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Working with AWS Lambda
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;INTRO&lt;/strong&gt;&lt;br&gt;
This lab builds a small but complete serverless pipeline: a Lambda function that runs on a schedule, pulls sales data out of a MySQL database running on a Café web app, formats it into a report, and emails it to you — all without a single server you manage yourself. Along the way you'll create two Lambda functions, a Lambda layer for a shared library, an SNS topic for email delivery, and a CloudWatch schedule to trigger the whole thing automatically.&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%2F0yr5dc9q65icyvbge9qd.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%2F0yr5dc9q65icyvbge9qd.png" alt="flow diagram" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the flow, start to finish:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A CloudWatch schedule triggers &lt;code&gt;salesAnalysisReport&lt;/code&gt; every evening&lt;/li&gt;
&lt;li&gt;That function calls a second function, &lt;code&gt;salesAnalysisReportDataExtractor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The extractor queries the café database directly&lt;/li&gt;
&lt;li&gt;The result comes back to &lt;code&gt;salesAnalysisReport&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;salesAnalysisReport&lt;/code&gt; formats it into a message and publishes it to an SNS topic&lt;/li&gt;
&lt;li&gt;SNS emails the finished report to you&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything below builds one piece of that chain at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 1: LOOK AT THE IAM ROLES BEFORE TOUCHING ANYTHING&lt;/strong&gt;&lt;br&gt;
Before creating either function, you look at the two IAM roles they'll use, so you know exactly what each function is — and isn't — allowed to do.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;salesAnalysisReportRole&lt;/code&gt; (used by the report function):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AmazonSNSFullAccess&lt;/strong&gt; — lets it publish the report to SNS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AmazonSSMReadOnlyAccess&lt;/strong&gt; — lets it read the database credentials from Parameter Store&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWSLambdaBasicRunRole&lt;/strong&gt; — lets it write logs to CloudWatch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWSLambdaRole&lt;/strong&gt; — lets it invoke another Lambda function (the data extractor)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;salesAnalysisReportDERole&lt;/code&gt; (used by the data extractor function):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWSLambdaBasicRunRole&lt;/strong&gt; — same logging permission&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWSLambdaVPCAccessRunRole&lt;/strong&gt; — lets it create network interfaces so it can actually reach into the VPC where the database lives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; Every Lambda function is only as capable as the role attached to it — no role, no permission, no matter how correct the code is. Looking at these upfront means that later, if a function fails with an access error, you already know exactly which permission to check first instead of guessing blind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 2: CREATE A LAMBDA LAYER, THEN THE DATA EXTRACTOR FUNCTION&lt;/strong&gt;&lt;br&gt;
This task has five parts — a layer, a function, attaching the layer, importing code, and wiring up networking.&lt;/p&gt;

&lt;p&gt;Grab the two files you'll need for this task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://drive.google.com/file/d/1cTx-lCjedpWXCeDuaRLTNT_WlMygJgCU/view?usp=sharing" rel="noopener noreferrer"&gt;salesAnalysisReportDataExtractor-v3.zip&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://drive.google.com/file/d/1g7Lzg1c_kraQ876mS2_10t3LUQYsLFAA/view?usp=sharing" rel="noopener noreferrer"&gt;pymysql-v3.zip&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2.1 — Create the Lambda layer:&lt;/strong&gt;&lt;br&gt;
In Lambda → Layers → Create layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;pymysqlLibrary&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Description: &lt;code&gt;PyMySQL library modules&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Upload the &lt;code&gt;pymysql-v3.zip&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Compatible runtime: Python 3.9&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2.2 — Create the data extractor function:&lt;/strong&gt;&lt;br&gt;
In Functions → Create function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Author from scratch&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;salesAnalysisReportDataExtractor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Runtime: Python 3.9&lt;/li&gt;
&lt;li&gt;Execution role: use the existing &lt;code&gt;salesAnalysisReportDERole&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2.3 — Attach the layer to the function:&lt;/strong&gt;&lt;br&gt;
In the function's Layers section → Add a layer → Custom layers → &lt;code&gt;pymysqlLibrary&lt;/code&gt;, version 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.4 — Import the actual code:&lt;/strong&gt;&lt;br&gt;
In Runtime settings, set the handler to &lt;code&gt;salesAnalysisReportDataExtractor.lambda_handler&lt;/code&gt;, then upload &lt;code&gt;salesAnalysisReportDataExtractor-v3.zip&lt;/code&gt; as the code source. Take a minute to actually read through the code and its comments — notice it expects &lt;code&gt;dbURL&lt;/code&gt;, &lt;code&gt;dbName&lt;/code&gt;, &lt;code&gt;dbUser&lt;/code&gt;, and &lt;code&gt;dbPassword&lt;/code&gt; to be passed in as part of its input event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.5 — Configure networking:&lt;/strong&gt;&lt;br&gt;
Under Configuration → VPC, set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPC: Cafe VPC&lt;/li&gt;
&lt;li&gt;Subnet: Cafe Public Subnet 1&lt;/li&gt;
&lt;li&gt;Security group: CafeSecurityGroup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; A Lambda layer exists so you're not bundling the same library into every function that needs it — one shared package, reused anywhere. And the VPC step matters because Lambda functions run outside your VPC by default; without explicitly attaching it to the café's VPC, subnet, and security group, this function would have no network path to reach the database at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 3: TEST THE DATA EXTRACTOR — AND FIX WHAT BREAKS&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;3.1 — Run a first test:&lt;/strong&gt;&lt;br&gt;
Grab the four database connection values from Parameter Store (&lt;code&gt;/cafe/dbUrl&lt;/code&gt;, &lt;code&gt;/cafe/dbName&lt;/code&gt;, &lt;code&gt;/cafe/dbUser&lt;/code&gt;, &lt;code&gt;/cafe/dbPassword&lt;/code&gt;), then create a test event on the function with those values as the JSON input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dbUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;value of /cafe/dbUrl&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dbName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;value of /cafe/dbName&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dbUser"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;value of /cafe/dbUser&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dbPassword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;value of /cafe/dbPassword&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the test — it fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.2 — Read the failure:&lt;/strong&gt;&lt;br&gt;
The error says the task timed out after 3 seconds. That 3-second default timeout is barely enough time to even attempt a database connection, let alone get a response back — so this points squarely at something blocking the connection itself, not a code bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.3 — Find and fix the real cause:&lt;/strong&gt;&lt;br&gt;
MySQL listens on port &lt;strong&gt;3306&lt;/strong&gt;. Check the inbound rules on the security group attached to the database's EC2 instance — if 3306 isn't open to the Lambda function's security group, the connection attempt just hangs until it times out. Add that inbound rule, then run the test again. This time it succeeds, but the response body comes back empty — there's simply no order data in the database yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.4 — Put real data in, then test again:&lt;/strong&gt;&lt;br&gt;
Open the café website (&lt;code&gt;http://&amp;lt;publicIP&amp;gt;/cafe&lt;/code&gt;), place a couple of orders through the menu, then re-run the test. This time the response body actually contains real order data — quantities, product names, everything the report needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; This is a textbook case of "the code is fine, the network isn't" — a timeout error tells you almost nothing about &lt;em&gt;why&lt;/em&gt; by itself, but knowing MySQL's default port narrows the search immediately. And testing with an empty database first, then a populated one, proves two separate things: that the connection works, and that the query logic itself is correct — not just that the function didn't crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 4: SET UP THE EMAIL NOTIFICATION&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;4.1 — Create the SNS topic:&lt;/strong&gt;&lt;br&gt;
In SNS → Topics → Create topic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type: Standard&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;salesAnalysisReportTopic&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Display name: &lt;code&gt;SARTopic&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save the topic's ARN — you'll need it in the next task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.2 — Subscribe your email:&lt;/strong&gt;&lt;br&gt;
Create a subscription on that topic with Protocol: Email, and your own email as the endpoint. Confirm it from the email AWS sends you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; SNS is the delivery mechanism, not the report generator — the Lambda function doesn't know how to send email itself, it just knows how to publish a message to a topic. Confirming the subscription is a required step too: an unconfirmed subscription silently receives nothing, so skipping it means the whole pipeline "works" but nobody ever sees the result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 5: CREATE THE MAIN REPORT FUNCTION — THIS TIME VIA THE CLI&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;5.1 — Connect to the CLI host:&lt;/strong&gt;&lt;br&gt;
Use EC2 Instance Connect to log into the CLI host instance, which already has the AWS CLI and the Python code for this function pre-loaded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5.2 — Configure the CLI:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supply the access key, secret key, region (&lt;code&gt;us-west-2&lt;/code&gt; for this lab), and &lt;code&gt;json&lt;/code&gt; as output format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5.3 — Create the function via the CLI:&lt;/strong&gt;&lt;br&gt;
First confirm the code is where you expect it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd activity-files
ls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grab the ARN of &lt;code&gt;salesAnalysisReportRole&lt;/code&gt; from IAM, then create the function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws lambda create-function \
--function-name salesAnalysisReport \
--runtime python3.9 \
--zip-file fileb://salesAnalysisReport-v2.zip \
--handler salesAnalysisReport.lambda_handler \
--region &amp;lt;region&amp;gt; \
--role &amp;lt;salesAnalysisReportRoleARN&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5.4 — Configure the environment variable:&lt;/strong&gt;&lt;br&gt;
The code reads the SNS topic ARN from an environment variable called &lt;code&gt;topicARN&lt;/code&gt; — it's not hardcoded in the function, so you have to set it. In Configuration → Environment variables, add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Key: &lt;code&gt;topicARN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Value: the ARN of &lt;code&gt;salesAnalysisReportTopic&lt;/code&gt; from Task 4&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5.5 — Test it:&lt;/strong&gt;&lt;br&gt;
Create a test event (no input parameters needed — this function doesn't take any), run it, and check for a &lt;code&gt;statusCode: 200&lt;/code&gt; response. Then check your inbox — you should receive an actual "Daily Sales Analysis Report" email with the items you ordered earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5.6 — Add the daily trigger:&lt;/strong&gt;&lt;br&gt;
Add an &lt;strong&gt;EventBridge (CloudWatch Events)&lt;/strong&gt; trigger with a schedule expression written as a Cron expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cron(Minutes Hours Day-of-month Month Day-of-week Year)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For testing, set it 5 minutes ahead of the current UTC time so you can see it fire quickly. For production, you'd want something like &lt;code&gt;cron(0 20 ? * MON-SAT *)&lt;/code&gt; — 8 PM UTC, Monday through Saturday, matching the schedule described at the start of this lab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; Using the CLI here instead of the console is deliberate — it's the same underlying operation, just proving that Lambda functions can be created and deployed as part of a script or pipeline, not only by clicking through a UI. The environment variable step matters for a different reason: hardcoding an ARN into the code would mean editing and redeploying code every time the SNS topic changed, while an environment variable means you can update it in seconds without touching a single line of Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHY THIS MATTERS OVERALL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two functions calling each other, each with its own scoped IAM role, is a far safer pattern than one function with every permission bundled together&lt;/li&gt;
&lt;li&gt;A timeout error almost always means "something on the network side is blocking me," not "my code is broken" — check ports and security groups before you start rewriting logic&lt;/li&gt;
&lt;li&gt;Layers, environment variables, and VPC config all exist for the same underlying reason: keeping configuration and shared code &lt;em&gt;outside&lt;/em&gt; the function itself, so changes don't require a full redeploy&lt;/li&gt;
&lt;li&gt;Testing with empty data and then real data isn't redundant — it's how you separate "the connection works" from "the logic is correct"&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AWS #Lambda #Serverless #SNS #CloudWatch #IAM #Cron
&lt;/h1&gt;

</description>
    </item>
    <item>
      <title>Troubleshooting a Broken EC2 Launch Script (LAMP Stack Edition) 🐛🔧</title>
      <dc:creator>Hashir Saud Khan</dc:creator>
      <pubDate>Sun, 02 Aug 2026 10:48:56 +0000</pubDate>
      <link>https://dev.to/hashirsaudkhan/troubleshooting-a-broken-ec2-launch-script-lamp-stack-edition-5gbp</link>
      <guid>https://dev.to/hashirsaudkhan/troubleshooting-a-broken-ec2-launch-script-lamp-stack-edition-5gbp</guid>
      <description>&lt;h2&gt;
  
  
  Troubleshooting the Creation of an EC2 Instance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;INTRO&lt;/strong&gt;&lt;br&gt;
This one's less about learning a new AWS service and more about a skill every cloud engineer eventually needs: reading someone else's script, figuring out why it's failing, and fixing it without rewriting the whole thing. You're handed a shell script that's supposed to launch a full &lt;strong&gt;LAMP stack&lt;/strong&gt; instance (Linux, Apache, MariaDB, PHP) hosting a Café web app — except the script has two bugs baked into it on purpose. Your job is to find them the way you actually would in real life: by reading error messages, checking open ports, and following a log file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHAT THE SCRIPT IS SUPPOSED TO DO&lt;/strong&gt;&lt;br&gt;
Before touching anything broken, it's worth understanding what a &lt;em&gt;working&lt;/em&gt; run of this script does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Loops through every AWS region looking for a VPC named &lt;code&gt;Cafe VPC&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Once found, looks up the subnet, an existing key pair, and the latest Amazon Linux AMI ID&lt;/li&gt;
&lt;li&gt;Cleans up any leftover instance or security group from a previous run&lt;/li&gt;
&lt;li&gt;Creates a new security group&lt;/li&gt;
&lt;li&gt;Launches the EC2 instance, attaching a user data script that installs Apache, MariaDB, and PHP, then deploys the actual website files&lt;/li&gt;
&lt;li&gt;Waits for a public IP and prints it out&lt;/li&gt;
&lt;/ol&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%2Ffbqtu56zbx5ijuq2761d.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%2Ffbqtu56zbx5ijuq2761d.png" alt="LAMP STACK" width="707" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 1: CONNECT AND SET UP&lt;/strong&gt;&lt;br&gt;
You connect to a CLI host instance using EC2 Instance Connect, then configure the AWS CLI with your access key, secret key, and region — same as any other CLI-based lab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; Nothing else in this lab works without a properly authenticated CLI session. This is the one step that has to be boring and correct before anything interesting happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 2: READ THE SCRIPT BEFORE YOU RUN IT&lt;/strong&gt;&lt;br&gt;
Before running anything, you make a backup of the script and read through it in a text editor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; This is the habit that actually matters here. If you run a broken script blind and it fails, you're troubleshooting with zero context. If you read it first, you already have a mental model of what &lt;em&gt;should&lt;/em&gt; happen, so when it breaks, you know roughly where to look instead of starting from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 3: RUN IT — AND WATCH IT FAIL&lt;/strong&gt;&lt;br&gt;
You run the script, and it fails partway through with this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An error occurred (InvalidAMIID.NotFound) when calling the RunInstances operation:
The image id '[ami-xxxxxxxxxx]' does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;BUG #1: THE HARDCODED REGION&lt;/strong&gt;&lt;br&gt;
Here's the part of the script that creates the instance:&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="nv"&gt;instanceDetails&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 run-instances &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--image-id&lt;/span&gt; &lt;span class="nv"&gt;$imageId&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--count&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--instance-type&lt;/span&gt; &lt;span class="nv"&gt;$instanceType&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--subnet-id&lt;/span&gt; &lt;span class="nv"&gt;$subnetId&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--security-group-ids&lt;/span&gt; &lt;span class="nv"&gt;$securityGroup&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See it? Earlier in the script, it dynamically &lt;em&gt;finds&lt;/em&gt; the correct region by searching every region for the &lt;code&gt;Cafe VPC&lt;/code&gt; and stores it in a variable called &lt;code&gt;$region&lt;/code&gt;. But then, right here in the &lt;code&gt;run-instances&lt;/code&gt; command, it ignores that variable completely and hardcodes &lt;code&gt;--region us-east-1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;AMI IDs are region-specific — the exact same AMI does not exist under the same ID in every region. So the script looked up a perfectly valid AMI ID in, say, &lt;code&gt;us-west-2&lt;/code&gt;, and then tried to launch it in &lt;code&gt;us-east-1&lt;/code&gt;, where that ID means nothing. That's exactly why AWS says the image doesn't exist — from that region's point of view, it genuinely doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; swap the hardcoded value for the variable that was already found earlier in the script:&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="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the script again, and this time &lt;code&gt;run-instances&lt;/code&gt; succeeds — you get back a public IP address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this bug mattered:&lt;/strong&gt; This is a classic copy-paste trap. Somewhere along the way, someone probably tested the script against &lt;code&gt;us-east-1&lt;/code&gt; directly, hardcoded it to move fast, and forgot to swap it back to the dynamic variable. It's a one-line bug, but it perfectly explains a very confusing-looking error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 3 CONTINUED: THE WEBSITE STILL DOESN'T LOAD&lt;/strong&gt;&lt;br&gt;
With the instance launched, you try opening &lt;code&gt;http://&amp;lt;public-ip&amp;gt;&lt;/code&gt; in a browser. Nothing loads.&lt;/p&gt;

&lt;p&gt;The instance is running, it has a public IP — so what's left? This is where you stop guessing and start checking, one layer at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BUG #2: THE WRONG PORT IN THE SECURITY GROUP&lt;/strong&gt;&lt;br&gt;
Look at the part of the script that opens ports:&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Opening port 22 in the new security group"&lt;/span&gt;
aws ec2 authorize-security-group-ingress &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--group-id&lt;/span&gt; &lt;span class="nv"&gt;$securityGroup&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--port&lt;/span&gt; 22 &lt;span class="se"&gt;\&lt;/span&gt;
...

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Opening port 80 in the new security group"&lt;/span&gt;
aws ec2 authorize-security-group-ingress &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--group-id&lt;/span&gt; &lt;span class="nv"&gt;$securityGroup&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="se"&gt;\&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that carefully — the &lt;code&gt;echo&lt;/code&gt; line says &lt;strong&gt;port 80&lt;/strong&gt;, but the actual &lt;code&gt;--port&lt;/code&gt; flag right below it says &lt;strong&gt;8080&lt;/strong&gt;. The message lies about what the command actually does. The security group only ever opens SSH (22) and 8080 — never 80, which is the port a web server actually needs for a plain &lt;code&gt;http://&lt;/code&gt; request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How you'd actually catch this without reading the script:&lt;/strong&gt; install &lt;code&gt;nmap&lt;/code&gt; on the CLI host and scan the instance directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo yum install -y nmap
nmap -Pn &amp;lt;public-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scan shows you exactly which ports are open from the outside — and port 80 simply isn't one of them. That's your confirmation the security group, not the web server itself, is the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; change &lt;code&gt;--port 8080&lt;/code&gt; to &lt;code&gt;--port 80&lt;/code&gt; so it matches what the echo statement (and the actual website) needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this bug mattered:&lt;/strong&gt; This is why you never fully trust comments or echoed log messages — they describe &lt;em&gt;intent&lt;/em&gt;, not necessarily what the code underneath actually does. &lt;code&gt;nmap&lt;/code&gt; sidesteps that entirely by checking the real, observable state of the network instead of what the script claims it configured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CONFIRMING THE FIX WORKED&lt;/strong&gt;&lt;br&gt;
Reload &lt;code&gt;http://&amp;lt;public-ip&amp;gt;&lt;/code&gt; — you should now see &lt;strong&gt;"Hello From Your Web Server!"&lt;/strong&gt;. That confirms Apache itself is up and reachable.&lt;/p&gt;

&lt;p&gt;To confirm the &lt;em&gt;rest&lt;/em&gt; of the user data script ran correctly (MariaDB, PHP, the actual Café app files), tail the cloud-init log on the instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo tail -f /var/log/cloud-init-output.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're looking for clean installation messages and no errors — plus a line confirming the database setup script completed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; A web page loading doesn't automatically mean &lt;em&gt;everything&lt;/em&gt; worked — it only confirms the web server layer. The cloud-init log is where you'd catch a silent failure further down the stack, like the database script erroring out even though Apache came up fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 4: VERIFY THE ACTUAL WEBSITE WORKS&lt;/strong&gt;&lt;br&gt;
Last step — visit &lt;code&gt;http://&amp;lt;public-ip&amp;gt;/cafe&lt;/code&gt; and confirm the real Café web app loads, not just the placeholder message. From there, add a couple of items to an order, submit it, and check the Order History page shows both orders — proof that the database (MariaDB) is actually storing data, not just installed and idle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; This is the real end-to-end check. Ports open, Apache running, and the app loading are all necessary, but placing an order and seeing it persist is the only thing that proves the full LAMP stack — web server, PHP, and database — is genuinely working together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHY THIS MATTERS OVERALL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A confusing AWS error often has a boring, specific cause — &lt;code&gt;InvalidAMIID.NotFound&lt;/code&gt; sounds scary, but it almost always just means "you're in the wrong region for that ID"&lt;/li&gt;
&lt;li&gt;Never trust a hardcoded value sitting next to a variable that already exists for that exact purpose — that mismatch is one of the most common sources of "it worked yesterday" bugs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;echo&lt;/code&gt; statements and comments describe what a script &lt;em&gt;author intended&lt;/em&gt; — they're not proof of what the code &lt;em&gt;actually does&lt;/em&gt;. Always check the real flag, not the message next to it&lt;/li&gt;
&lt;li&gt;Tools like &lt;code&gt;nmap&lt;/code&gt; let you verify the actual state of a system from the outside, instead of trusting that your configuration commands did what you assumed they did&lt;/li&gt;
&lt;li&gt;Success at one layer (the web server responding) doesn't guarantee success at every layer underneath it (the database) — always confirm the deepest thing that actually matters, which here was placing a real order&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AWS #EC2 #CLI #Troubleshooting #LAMP #Nmap #CloudComputing
&lt;/h1&gt;

&lt;p&gt;📜 Click to view the full create-lamp-instance-v2.sh script&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="nv"&gt;DATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%d %H:%M:%S'&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"Running create-instance.sh on "&lt;/span&gt;&lt;span class="nv"&gt;$DATE&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;span class="c"&gt;# Hard coded values&lt;/span&gt;
&lt;span class="nv"&gt;instanceType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"t3.small"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Instance Type: "&lt;/span&gt;&lt;span class="nv"&gt;$instanceType&lt;/span&gt;
&lt;span class="nv"&gt;profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Profile: "&lt;/span&gt;&lt;span class="nv"&gt;$profile&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"Looking up account values..."&lt;/span&gt;
&lt;span class="c"&gt;# get vpcId&lt;/span&gt;
&lt;span class="nv"&gt;vpc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$vpc&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-regions | &lt;span class="nb"&gt;grep &lt;/span&gt;RegionName | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="nt"&gt;-f4&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;vpc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-vpcs &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values='Cafe VPC'"&lt;/span&gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;VpcId | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="nt"&gt;-f4&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1p &lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$vpc&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;fi
  done
done
&lt;/span&gt;&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"VPC: "&lt;/span&gt;&lt;span class="nv"&gt;$vpc&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Region: "&lt;/span&gt;&lt;span class="nv"&gt;$region&lt;/span&gt;
&lt;span class="nv"&gt;vpc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-vpcs &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values='Cafe VPC'"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;VpcId | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="nt"&gt;-f4&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1p&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VPC: "&lt;/span&gt;&lt;span class="nv"&gt;$vpc&lt;/span&gt;
&lt;span class="c"&gt;# get subnetId&lt;/span&gt;
&lt;span class="nv"&gt;subnetId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-subnets &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values='Cafe Public Subnet 1'"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Subnets[*]"&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;SubnetId | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="nt"&gt;-f4&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1p&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Subnet Id: "&lt;/span&gt;&lt;span class="nv"&gt;$subnetId&lt;/span&gt;
&lt;span class="c"&gt;# Get keypair name&lt;/span&gt;
&lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-key-pairs &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;KeyName | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="nt"&gt;-f4&lt;/span&gt; &lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Key: "&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;
&lt;span class="c"&gt;# Get AMI ID&lt;/span&gt;
&lt;span class="nv"&gt;imageId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ssm get-parameters &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--names&lt;/span&gt; &lt;span class="s1"&gt;'/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;ami- | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="nt"&gt;-f4&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 2p&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"AMI ID: "&lt;/span&gt;&lt;span class="nv"&gt;$imageId&lt;/span&gt;
&lt;span class="c"&gt;#check for existing cafe instance&lt;/span&gt;
&lt;span class="nv"&gt;existingEc2Instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-instances &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values=cafeserver"&lt;/span&gt; &lt;span class="s2"&gt;"Name=instance-state-name,Values=running"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| &lt;span class="nb"&gt;grep &lt;/span&gt;InstanceId | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="nt"&gt;-f4&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$existingEc2Instance&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo
  echo&lt;/span&gt; &lt;span class="s2"&gt;"WARNING: Found existing running EC2 instance with instance ID "&lt;/span&gt;&lt;span class="nv"&gt;$existingEc2Instance&lt;/span&gt;&lt;span class="s2"&gt;"."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"This script will not succeed if it already exists. "&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Would you like to delete it? [Y/N]"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;amp;gt;&amp;amp;gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;validResp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$validResp&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;do
      &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;answer
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$answer&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Y"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$answer&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"y"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
          &lt;/span&gt;&lt;span class="nb"&gt;echo
          echo&lt;/span&gt; &lt;span class="s2"&gt;"Deleting the existing instance..."&lt;/span&gt;
          aws ec2 terminate-instances &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &lt;span class="nv"&gt;$existingEc2Instance&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt;
          &lt;span class="c"&gt;#wait for confirmation it was terminated&lt;/span&gt;
          aws ec2 &lt;span class="nb"&gt;wait &lt;/span&gt;instance-terminated &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &lt;span class="nv"&gt;$existingEc2Instance&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt;
          &lt;span class="nv"&gt;validResp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;
      &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$answer&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"N"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$answer&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"n"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
          &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Ok, exiting."&lt;/span&gt;
          &lt;span class="nb"&gt;exit &lt;/span&gt;1
      &lt;span class="k"&gt;else
          &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Please reply with Y or N."&lt;/span&gt;
      &lt;span class="k"&gt;fi
  done
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;10 &lt;span class="c"&gt;#give it 10 seconds before trying to delete the SG this instance used.&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="c"&gt;#check for existing cafeSG security Group&lt;/span&gt;
&lt;span class="nv"&gt;existingMpSg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-security-groups &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"SecurityGroups[?contains(GroupName, 'cafeSG')]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;GroupId | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="nt"&gt;-f4&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$existingMpSg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo
  echo&lt;/span&gt; &lt;span class="s2"&gt;"WARNING: Found existing security group with name "&lt;/span&gt;&lt;span class="nv"&gt;$existingMpSg&lt;/span&gt;&lt;span class="s2"&gt;"."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"This script will not succeed if it already exists. "&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Would you like to delete it? [Y/N]"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;amp;gt;&amp;amp;gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;validResp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$validResp&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;do
      &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;answer
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$answer&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Y"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$answer&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"y"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
          &lt;/span&gt;&lt;span class="nb"&gt;echo
          echo&lt;/span&gt; &lt;span class="s2"&gt;"Deleting the existing security group..."&lt;/span&gt;
          aws ec2 delete-security-group &lt;span class="nt"&gt;--group-id&lt;/span&gt; &lt;span class="nv"&gt;$existingMpSg&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt;
          &lt;span class="nv"&gt;validResp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;
      &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$answer&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"N"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$answer&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"n"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
          &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Ok, exiting."&lt;/span&gt;
          &lt;span class="nb"&gt;exit &lt;/span&gt;1
      &lt;span class="k"&gt;else
          &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Please reply with Y or N."&lt;/span&gt;
      &lt;span class="k"&gt;fi
  done
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;10 &lt;span class="c"&gt;#give it 10 seconds before trying to recreate the SG&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="c"&gt;# CREATE a security group and capture the name of it&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"Creating a new security group..."&lt;/span&gt;
&lt;span class="nv"&gt;securityGroup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 create-security-group &lt;span class="nt"&gt;--group-name&lt;/span&gt; &lt;span class="s2"&gt;"cafeSG"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"cafeSG"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--group-name&lt;/span&gt; &lt;span class="s2"&gt;"cafeSG"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--vpc-id&lt;/span&gt; &lt;span class="nv"&gt;$vpc&lt;/span&gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;GroupId | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="nt"&gt;-f4&lt;/span&gt; &lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Security Group: "&lt;/span&gt;&lt;span class="nv"&gt;$securityGroup&lt;/span&gt;
&lt;span class="c"&gt;# Open ports in the security group&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"Opening port 22 in the new security group"&lt;/span&gt;
aws ec2 authorize-security-group-ingress &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--group-id&lt;/span&gt; &lt;span class="nv"&gt;$securityGroup&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--port&lt;/span&gt; 22 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--cidr&lt;/span&gt; 0.0.0.0/0 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Opening port 80 in the new security group"&lt;/span&gt;
aws ec2 authorize-security-group-ingress &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--group-id&lt;/span&gt; &lt;span class="nv"&gt;$securityGroup&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--cidr&lt;/span&gt; 0.0.0.0/0 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"Creating an EC2 instance in "&lt;/span&gt;&lt;span class="nv"&gt;$region&lt;/span&gt;
&lt;span class="nv"&gt;instanceDetails&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 run-instances &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--image-id&lt;/span&gt; &lt;span class="nv"&gt;$imageId&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--count&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--instance-type&lt;/span&gt; &lt;span class="nv"&gt;$instanceType&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--subnet-id&lt;/span&gt; &lt;span class="nv"&gt;$subnetId&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--security-group-ids&lt;/span&gt; &lt;span class="nv"&gt;$securityGroup&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--tag-specifications&lt;/span&gt; &lt;span class="s1"&gt;'ResourceType=instance,Tags=[{Key=Name,Value=cafeserver}]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--associate-public-ip-address&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--iam-instance-profile&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;LabInstanceProfile &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--user-data&lt;/span&gt; file://create-lamp-instance-userdata-v2.txt &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--key-name&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;#if the create instance command failed, exit this script&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"Instance Details...."&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$instanceDetails&lt;/span&gt; | python &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool
&lt;span class="c"&gt;# Extract instanceId&lt;/span&gt;
&lt;span class="nv"&gt;instanceId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$instanceDetails&lt;/span&gt; | python &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool | &lt;span class="nb"&gt;grep &lt;/span&gt;InstanceId | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1p | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="nt"&gt;-f4&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"instanceId="&lt;/span&gt;&lt;span class="nv"&gt;$instanceId&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting for a public IP for the new instance..."&lt;/span&gt;
&lt;span class="nv"&gt;pubIp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pubIp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;10&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;pubIp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-instances &lt;span class="nt"&gt;--instance-id&lt;/span&gt; &lt;span class="nv"&gt;$instanceId&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$region&lt;/span&gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$profile&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;PublicIp | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1p | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="nt"&gt;-f4&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"The public IP of your LAMP instance is: "&lt;/span&gt;&lt;span class="nv"&gt;$pubIp&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"Download the Key Pair from the lab console."&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"Then connect using this command (with .pem or .ppk added to the end of the keypair name):"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ssh -i path-to/"&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="s2"&gt;" ec2-user@"&lt;/span&gt;&lt;span class="nv"&gt;$pubIp&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"The website should also become available at"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"http://"&lt;/span&gt;&lt;span class="nv"&gt;$pubIp&lt;/span&gt;&lt;span class="s2"&gt;"/cafe/"&lt;/span&gt;
&lt;span class="nb"&gt;echo
&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%d %H:%M:%S'&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nb"&gt;echo
echo&lt;/span&gt; &lt;span class="s2"&gt;"Done running create-instance.sh at "&lt;/span&gt;&lt;span class="nv"&gt;$DATE&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
    </item>
    <item>
      <title>Hosting a Static Website on Amazon S3 Using the AWS CLI 🌐🪣</title>
      <dc:creator>Hashir Saud Khan</dc:creator>
      <pubDate>Thu, 30 Jul 2026 12:25:57 +0000</pubDate>
      <link>https://dev.to/hashirsaudkhan/hosting-a-static-website-on-amazon-s3-using-the-aws-cli-gih</link>
      <guid>https://dev.to/hashirsaudkhan/hosting-a-static-website-on-amazon-s3-using-the-aws-cli-gih</guid>
      <description>&lt;h2&gt;
  
  
  Creating a Website on S3
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;INTRO&lt;/strong&gt;&lt;br&gt;
This lab is about hosting a real static website — a Café &amp;amp; Bakery site — entirely on Amazon S3, using nothing but the AWS CLI from an EC2 instance. Along the way, you create a new IAM user with S3 access, upload the actual website files, and then build your own script so future updates take one command instead of repeating the whole upload process by hand.&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%2F4aifmdnno2tnv8k6hubs.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%2F4aifmdnno2tnv8k6hubs.png" alt="AWS S3 static website" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By the end, you'll have a real public URL you can open in a browser and see the site live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 1: CONNECT TO THE EC2 INSTANCE USING SSM&lt;/strong&gt;&lt;br&gt;
You start by connecting to the instance through &lt;strong&gt;Systems Manager Session Manager&lt;/strong&gt; — either from the EC2 console (select the instance, choose Connect, then the Session Manager tab) or through a session link if your environment provides one. Either way, this drops you straight into a terminal session on the instance, no SSH key involved.&lt;/p&gt;

&lt;p&gt;Once connected, switch to the &lt;code&gt;ec2-user&lt;/code&gt; account:&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="nb"&gt;sudo &lt;/span&gt;su &lt;span class="nt"&gt;-l&lt;/span&gt; ec2-user
&lt;span class="nb"&gt;pwd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; Every command in this lab needs to run from inside this instance, so this is just making sure you're actually in the right shell, as the right user, before doing anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 2: CONFIGURE THE AWS CLI&lt;/strong&gt;&lt;br&gt;
Amazon Linux comes with the AWS CLI already installed — unlike Red Hat, you don't need to download or unzip anything here. You just authenticate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then supply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access Key ID and Secret Access Key for your IAM user&lt;/li&gt;
&lt;li&gt;Region: &lt;code&gt;us-west-2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Output format: &lt;code&gt;json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; The CLI is just a tool — on its own it doesn't know &lt;em&gt;which&lt;/em&gt; AWS account it's allowed to touch. This step is what connects it to your actual lab account so every command after this actually does something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 3: CREATE AN S3 BUCKET&lt;/strong&gt;&lt;br&gt;
Bucket names have to be globally unique across all of AWS, so you pick something like your initials plus a few random numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api create-bucket &lt;span class="nt"&gt;--bucket&lt;/span&gt; &amp;lt;your-bucket-name&amp;gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="nt"&gt;--create-bucket-configuration&lt;/span&gt; &lt;span class="nv"&gt;LocationConstraint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-west-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful run returns a JSON response with the bucket's location.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; This bucket is going to &lt;em&gt;be&lt;/em&gt; the website — every file you upload later needs somewhere to live first. Specifying the region explicitly matters too, because S3 defaults to &lt;code&gt;us-east-1&lt;/code&gt; if you don't say otherwise, and this lab needs everything in &lt;code&gt;us-west-2&lt;/code&gt; to line up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 4: CREATE A NEW IAM USER WITH S3 ACCESS&lt;/strong&gt;&lt;br&gt;
Instead of doing everything as the root/admin lab user, you create a dedicated IAM user just for this task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-user &lt;span class="nt"&gt;--user-name&lt;/span&gt; awsS3user
aws iam create-login-profile &lt;span class="nt"&gt;--user-name&lt;/span&gt; awsS3user &lt;span class="nt"&gt;--password&lt;/span&gt; Training123!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you sign out of the console, sign back in as &lt;code&gt;awsS3user&lt;/code&gt; using the account ID, and try opening S3 — you'll get an access error, because this new user has zero permissions yet.&lt;/p&gt;

&lt;p&gt;Back in the terminal, you find the AWS-managed policy that grants full S3 access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam list-policies &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Policies[?contains(PolicyName,'S3')]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And attach it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam attach-user-policy &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::aws:policy/&amp;lt;policyYouFound&amp;gt; &lt;span class="nt"&gt;--user-name&lt;/span&gt; awsS3user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; This is the same principle you'd apply in any real environment — don't do everything as a god-mode account. You create a user scoped to exactly the service it needs (S3, and nothing else), and you can see the access error firsthand &lt;em&gt;before&lt;/em&gt; attaching the policy, so you actually witness IAM permissions in action rather than just taking it on faith.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 5: OPEN UP THE BUCKET'S PUBLIC ACCESS&lt;/strong&gt;&lt;br&gt;
By default, S3 buckets block all public access — good for security, but a website needs to be reachable by anyone. So in the bucket's Permissions tab, you turn off &lt;strong&gt;Block all public access&lt;/strong&gt;, then enable &lt;strong&gt;ACLs&lt;/strong&gt; under Object Ownership.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; A private bucket and a public website are contradictory by definition. This step is you deliberately telling AWS "yes, I want this specific bucket to be visible to the internet" — it's not a default AWS assumes for you, because most buckets shouldn't be public.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 6: EXTRACT THE WEBSITE FILES&lt;/strong&gt;&lt;br&gt;
Back in the terminal:&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="nb"&gt;cd&lt;/span&gt; ~/sysops-activity-files
&lt;span class="nb"&gt;tar &lt;/span&gt;xvzf static-website-v2.tar.gz
&lt;span class="nb"&gt;cd &lt;/span&gt;static-website
&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;index.html&lt;/code&gt; plus &lt;code&gt;css&lt;/code&gt; and &lt;code&gt;images&lt;/code&gt; folders — that's the actual site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; The lab ships the website as a compressed archive, so this step just unpacks it into a real folder structure you can upload, the same way you'd receive a client's website files as a zip and need to extract them before deploying anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 7: UPLOAD THE FILES AND ENABLE STATIC HOSTING&lt;/strong&gt;&lt;br&gt;
First, tell S3 which file is the homepage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 website s3://&amp;lt;my-bucket&amp;gt;/ &lt;span class="nt"&gt;--index-document&lt;/span&gt; index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then upload everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; /home/ec2-user/sysops-activity-files/static-website/ s3://&amp;lt;my-bucket&amp;gt;/ &lt;span class="nt"&gt;--recursive&lt;/span&gt; &lt;span class="nt"&gt;--acl&lt;/span&gt; public-read
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--recursive&lt;/code&gt; uploads every file in that folder and its subfolders. &lt;code&gt;--acl public-read&lt;/code&gt; makes each uploaded file individually viewable by anyone — remember, opening bucket access in Task 5 controls the &lt;em&gt;bucket&lt;/em&gt;, but this flag controls the &lt;em&gt;objects&lt;/em&gt; inside it.&lt;/p&gt;

&lt;p&gt;Confirm the upload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;ls&lt;/span&gt; &amp;lt;my-bucket&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then head to the bucket's &lt;strong&gt;Properties&lt;/strong&gt; tab, confirm &lt;strong&gt;Static website hosting&lt;/strong&gt; is Enabled, and open the &lt;strong&gt;Bucket website endpoint URL&lt;/strong&gt; — your Café &amp;amp; Bakery site is now live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; This is the actual deployment — everything before this was just setup. Notice you needed &lt;em&gt;two&lt;/em&gt; separate permissions here: the bucket-level public access (Task 5) and the object-level &lt;code&gt;public-read&lt;/code&gt; ACL (this task). Miss either one, and visitors get an access-denied error instead of your website.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 8: BUILD A SCRIPT SO YOU DON'T REPEAT YOURSELF&lt;/strong&gt;&lt;br&gt;
Re-uploading manually every time you change a file gets old fast, so you turn that upload command into a reusable script.&lt;/p&gt;

&lt;p&gt;Check your command history to find the exact &lt;code&gt;aws s3 cp&lt;/code&gt; line you ran:&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="nb"&gt;history&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create and edit a new file:&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="nb"&gt;cd&lt;/span&gt; ~
&lt;span class="nb"&gt;touch &lt;/span&gt;update-website.sh
vi update-website.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside, add:&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;
aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; /home/ec2-user/sysops-activity-files/static-website/ s3://&amp;lt;my-bucket&amp;gt;/ &lt;span class="nt"&gt;--recursive&lt;/span&gt; &lt;span class="nt"&gt;--acl&lt;/span&gt; public-read
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save with &lt;code&gt;Esc&lt;/code&gt;, then &lt;code&gt;:wq&lt;/code&gt;, then make it executable:&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="nb"&gt;chmod&lt;/span&gt; +x update-website.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now make an actual change to the site — edit &lt;code&gt;index.html&lt;/code&gt; and swap a couple of background colors — then run your new script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./update-website.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refresh the site in your browser and you'll see your change live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; This is the difference between a one-time task and a repeatable process. The first upload was you doing the work manually; this script means every future update is one command, run the same way, every time — no risk of forgetting a flag or mistyping a bucket name six months from now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OPTIONAL CHALLENGE: SWITCH FROM &lt;code&gt;cp&lt;/code&gt; TO &lt;code&gt;sync&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Here's the catch with the script you just built — &lt;code&gt;aws s3 cp --recursive&lt;/code&gt; re-uploads &lt;em&gt;every single file&lt;/em&gt;, every single time, even the ones that haven't changed at all.&lt;/p&gt;

&lt;p&gt;The fix is to swap it for &lt;code&gt;aws s3 sync&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;sync&lt;/span&gt; /home/ec2-user/sysops-activity-files/static-website/ s3://&amp;lt;my-bucket&amp;gt;/ &lt;span class="nt"&gt;--acl&lt;/span&gt; public-read
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make a small change to &lt;code&gt;index.html&lt;/code&gt;, run this instead of &lt;code&gt;cp&lt;/code&gt;, and refresh the site to confirm it still works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; &lt;code&gt;sync&lt;/code&gt; compares what's already in the bucket against what's on your local disk, and only uploads files that are new or changed — not everything, every time. For a small site with three files that barely matters, but imagine a real site with hundreds of images: &lt;code&gt;cp --recursive&lt;/code&gt; would re-upload all of them on every single deploy, while &lt;code&gt;sync&lt;/code&gt; would only push the one file you actually touched. Same result on screen, far less wasted bandwidth and time behind the scenes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHY THIS MATTERS OVERALL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 static hosting means you don't need a running server at all — no EC2 instance, no patching, just files sitting in a bucket, served directly to the browser&lt;/li&gt;
&lt;li&gt;Bucket-level public access and object-level ACLs are two separate switches — both need to be flipped for a public website to actually work&lt;/li&gt;
&lt;li&gt;Creating a scoped IAM user instead of using the account's root permissions is a habit worth carrying into real projects, not just this lab&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cp --recursive&lt;/code&gt; vs &lt;code&gt;sync&lt;/code&gt; is a small command swap with a real efficiency difference once your site is bigger than three files&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AWS #S3 #CLI #StaticWebsite #IAM #CloudComputing
&lt;/h1&gt;

</description>
    </item>
    <item>
      <title>Creating Amazon EC2 Instances: Console, CLI, and a Bastion Host or Jump Server 🖥️</title>
      <dc:creator>Hashir Saud Khan</dc:creator>
      <pubDate>Thu, 30 Jul 2026 10:16:51 +0000</pubDate>
      <link>https://dev.to/hashirsaudkhan/creating-amazon-ec2-instances-console-cli-and-a-bastion-host-or-jump-server-210h</link>
      <guid>https://dev.to/hashirsaudkhan/creating-amazon-ec2-instances-console-cli-and-a-bastion-host-or-jump-server-210h</guid>
      <description>&lt;h1&gt;
  
  
  aws #ec2 #cli #tutorial
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Creating Amazon EC2 Instances
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;INTRO&lt;/strong&gt;&lt;br&gt;
This lab is about the different ways you can launch an EC2 instance on AWS — through the console, and through the CLI. You'll launch one instance as a bastion host, then use that bastion host to launch a second instance (a web server) using the AWS CLI. By the end, you'll have a small real setup: one instance whose whole job is to give you a safe way in, and another instance doing the actual work.&lt;/p&gt;

&lt;p&gt;Quick note before we start — if you've worked in on-prem networks or with other cloud providers, you might know the term &lt;strong&gt;jump server&lt;/strong&gt; instead of &lt;strong&gt;bastion host&lt;/strong&gt;. They're the same thing, just different names depending on where you learned it. A bastion host/jump server is a hardened instance that sits at the edge of your network, and it's the &lt;em&gt;only&lt;/em&gt; way in — you connect to it first, then use it to reach other instances that aren't directly exposed. Same concept throughout this lab.&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%2Fo8bjiqqqvi9zx5w6vpl5.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%2Fo8bjiqqqvi9zx5w6vpl5.png" alt="AWS Bastion Host " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 1: LAUNCH THE BASTION HOST FROM THE CONSOLE&lt;/strong&gt;&lt;br&gt;
First, you launch an EC2 instance through the AWS Console — this one becomes your bastion host.&lt;/p&gt;

&lt;p&gt;Here's what you configure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;: &lt;code&gt;Bastion host&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AMI&lt;/strong&gt;: Amazon Linux 2 (from Quick Start)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instance type&lt;/strong&gt;: &lt;code&gt;t3.micro&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key pair&lt;/strong&gt;: Proceed without a key pair — you'll use EC2 Instance Connect instead, so no key pair is needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network&lt;/strong&gt;: Lab VPC, public subnet, public IP enabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security group&lt;/strong&gt;: new one named &lt;code&gt;Bastion security group&lt;/code&gt;, described as permitting SSH connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: default 8 GiB root volume&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM instance profile&lt;/strong&gt;: &lt;code&gt;Bastion-Role&lt;/code&gt; — this gives the instance permission to make its own calls to EC2 later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once everything's set, launch it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; You need a single, controlled entry point into your VPC before you do anything else. Instead of exposing every instance directly to the internet, you expose just this one — the bastion host — and everything else stays private, reachable only through it. The IAM role is the other half of this: without it, the bastion host would have no permission to talk to EC2 later in Task 3, no matter how you tried to connect to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 2: CONNECT TO THE BASTION HOST&lt;/strong&gt;&lt;br&gt;
With the instance running, select it in the console, choose &lt;strong&gt;Connect&lt;/strong&gt;, and use the &lt;strong&gt;EC2 Instance Connect&lt;/strong&gt; tab to connect.&lt;/p&gt;

&lt;p&gt;That's it — no key file, no terminal setup on your side. You're dropped straight into a shell on the bastion host, right from the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; This proves the bastion host is actually reachable and working before you rely on it to do anything else. It's also worth noticing &lt;em&gt;how&lt;/em&gt; you connected — no SSH key was ever generated or downloaded for this instance. EC2 Instance Connect handles the authentication behind the scenes through IAM, which is a cleaner story than passing PEM files around, especially for a machine whose whole purpose is being your access point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 3: LAUNCH A WEB SERVER USING THE AWS CLI&lt;/strong&gt;&lt;br&gt;
Now for the real task — from inside that bastion host session, you use the AWS CLI to launch a &lt;em&gt;second&lt;/em&gt; instance, this time as a web server. Unlike the console, the CLI doesn't guess anything for you — you have to supply every parameter yourself. So the first few steps are just about gathering those parameters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Get the latest AMI ID:&lt;/strong&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="nv"&gt;AZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://169.254.169.254/latest/meta-data/placement/availability-zone&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_DEFAULT_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;::-1&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;AMI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ssm get-parameters &lt;span class="nt"&gt;--names&lt;/span&gt; /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Parameters[0].[Value]'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$AMI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of hardcoding an AMI ID (which goes stale the moment AWS patches it), you pull the current one from Parameter Store — the same Parameter Store you'd use to store any other config value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Get the subnet ID:&lt;/strong&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="nv"&gt;SUBNET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-subnets &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s1"&gt;'Name=tag:Name,Values=Public Subnet'&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; Subnets[].SubnetId &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$SUBNET&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3 — Get the security group ID:&lt;/strong&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="nv"&gt;SG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-security-groups &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;group-name,Values&lt;span class="o"&gt;=&lt;/span&gt;WebSecurityGroup &lt;span class="nt"&gt;--query&lt;/span&gt; SecurityGroups[].GroupId &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$SG&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4 — Download the user data script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://aws-tc-largeobjects.s3.us-west-2.amazonaws.com/CUR-TF-100-RSJAWS-1-23732/171-lab-JAWS-create-ec2/s3/UserData.txt
&lt;span class="nb"&gt;cat &lt;/span&gt;UserData.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script installs the web server software and the web app itself, and it runs automatically the moment the instance boots — you never have to log into the new instance to set it up manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 — Launch the instance:&lt;/strong&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="nv"&gt;INSTANCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 run-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-id&lt;/span&gt; &lt;span class="nv"&gt;$AMI&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet-id&lt;/span&gt; &lt;span class="nv"&gt;$SUBNET&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-group-ids&lt;/span&gt; &lt;span class="nv"&gt;$SG&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user-data&lt;/span&gt; file:///home/ec2-user/UserData.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-type&lt;/span&gt; t3.micro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-specifications&lt;/span&gt; &lt;span class="s1"&gt;'ResourceType=instance,Tags=[{Key=Name,Value=Web Server}]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Instances[*].InstanceId'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 6 — Wait for it to be ready:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-instances &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Reservations[].Instances[].State.Name'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this again until it says &lt;code&gt;running&lt;/code&gt; instead of &lt;code&gt;pending&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7 — Test it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-instances &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; Reservations[].Instances[].PublicDnsName &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste that DNS name into a browser tab — if the web page loads, the instance launched, booted, and configured itself correctly, entirely through commands you typed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why we did this:&lt;/strong&gt; This is the whole point of the CLI — repeatability. Everything you just did could be saved as a script and run again to spin up an identical web server anytime, with zero clicking. That's the real difference between the console and the CLI: the console is great for a one-off instance, but the CLI is what you reach for when you need the exact same setup twice, or twenty times, without relying on memory or a screenshot of the steps.&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%2Fp62mrjys95bddekkmwe6.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%2Fp62mrjys95bddekkmwe6.png" alt="Web server" width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PICKING THE RIGHT METHOD&lt;/strong&gt;&lt;br&gt;
Worth remembering going forward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Console&lt;/strong&gt; — quick, one-off, temporary instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI / scripts&lt;/strong&gt; — repeatable, automatable, consistent deployments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFormation&lt;/strong&gt; — when you need multiple related resources launched and managed together as one unit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;WHY THIS MATTERS OVERALL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bastion host, jump server — same idea: one hardened, controlled entry point standing between the internet and everything else in your VPC&lt;/li&gt;
&lt;li&gt;EC2 Instance Connect swaps out "manage an SSH key" for "let IAM handle it" — one less credential to lose track of&lt;/li&gt;
&lt;li&gt;The CLI forces you to know every parameter that goes into launching an instance, which is exactly why it's worth learning even if the console feels faster day-to-day&lt;/li&gt;
&lt;li&gt;Pulling the AMI ID from Parameter Store instead of hardcoding it means your launch script never silently uses an outdated, unpatched image&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AWS #EC2 #BastionHost #JumpServer #CLI #CloudComputing
&lt;/h1&gt;

</description>
    </item>
    <item>
      <title>Using AWS Systems Manager (Lab): Fleet Manager, Inventory, Run Command, Documents &amp; Parameter Store</title>
      <dc:creator>Hashir Saud Khan</dc:creator>
      <pubDate>Wed, 29 Jul 2026 09:41:32 +0000</pubDate>
      <link>https://dev.to/hashirsaudkhan/using-aws-systems-manager-lab-fleet-manager-inventory-run-command-documents-parameter-store-20og</link>
      <guid>https://dev.to/hashirsaudkhan/using-aws-systems-manager-lab-fleet-manager-inventory-run-command-documents-parameter-store-20og</guid>
      <description>&lt;h1&gt;
  
  
  aws #systemsmanager #ansible #tutorial
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Using AWS Systems Manager
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;INTRO&lt;/strong&gt;&lt;br&gt;
This lab is about AWS Systems Manager — a service that lets you manage a whole fleet of servers from one central place. Instead of logging into each machine one by one, you use it to check what's installed on your instances, run commands across many of them at once, store configuration values in one spot, and even get a shell on a server — all without ever opening an SSH port.&lt;/p&gt;

&lt;p&gt;Now, if you've worked with Ansible before, a lot of this is going to feel familiar, just with different names. So alongside every step, I'll point out the Ansible equivalent — that way, instead of learning something brand new, you're really just learning AWS's word for something you probably already do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;THE BIG PICTURE FIRST — HOW THIS IS DIFFERENT FROM ANSIBLE&lt;/strong&gt;&lt;br&gt;
Before the steps, one important difference that's worth understanding upfront, because it explains &lt;em&gt;why&lt;/em&gt; AWS built things this way:&lt;/p&gt;

&lt;p&gt;Ansible is &lt;strong&gt;agentless&lt;/strong&gt;. It reaches out to a server over SSH, runs its tasks, and leaves. No software has to sit running on the target machine.&lt;/p&gt;

&lt;p&gt;Systems Manager works the opposite way. It's &lt;strong&gt;agent-based&lt;/strong&gt; — every managed instance runs the SSM Agent in the background. That agent constantly checks in with AWS, waiting for work to do. That's &lt;em&gt;why&lt;/em&gt; you never need to open an SSH port or manage keys with Systems Manager — the instance is calling home to AWS instead of AWS calling in to the instance.&lt;/p&gt;

&lt;p&gt;Keep that one idea in mind, and everything else in this lab makes a lot more sense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 1: FLEET MANAGER — LIKE RUNNING &lt;code&gt;ansible -m setup&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
In Ansible, when you want to know what's actually installed on a box — OS version, packages, running services — you gather facts. That's literally a module call: &lt;code&gt;ansible -m setup&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fleet Manager's Inventory feature does the same job, just continuously and automatically instead of on-demand.&lt;/p&gt;

&lt;p&gt;To set it up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Search &lt;strong&gt;Systems Manager&lt;/strong&gt; in the console, then go to &lt;strong&gt;Fleet Manager&lt;/strong&gt; on the left&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Account management&lt;/strong&gt;, choose &lt;strong&gt;Set up inventory&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name it &lt;code&gt;Inventory-Association&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Under targets, choose &lt;strong&gt;Manually selecting instances&lt;/strong&gt; and pick your &lt;strong&gt;Managed Instance&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Leave everything else default and choose &lt;strong&gt;Setup Inventory&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once it's running, click the &lt;strong&gt;Node ID&lt;/strong&gt;, then the &lt;strong&gt;Inventory&lt;/strong&gt; tab — you'll see every application installed on that instance. No SSH needed to find out what's running there. That's the whole point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 2: RUN COMMAND — LIKE RUNNING AN ANSIBLE PLAYBOOK&lt;/strong&gt;&lt;br&gt;
This is where the comparison gets really direct. In Ansible, you write a &lt;strong&gt;playbook&lt;/strong&gt; — a YAML file describing tasks to run — and point it at a group of hosts.&lt;/p&gt;

&lt;p&gt;In Systems Manager, the equivalent of a playbook is called an &lt;strong&gt;SSM Document&lt;/strong&gt;. &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%2Fdmnimw46us1xghy7da1r.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%2Fdmnimw46us1xghy7da1r.png" alt="aws_system_manager_inventery-document" width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's AWS's pre-packaged (or custom) definition of "here's what to run." Run Command is what actually &lt;em&gt;executes&lt;/em&gt; that document against your targets — same relationship as &lt;code&gt;ansible-playbook&lt;/code&gt; executing a playbook.&lt;/p&gt;

&lt;p&gt;In this task, you'll run a document that installs a small web app (Apache, PHP, the app itself) and starts the web server — basically a mini playbook run.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Node Management → Run Command → Run command&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Search documents by &lt;strong&gt;Owner → Owned by me&lt;/strong&gt;, and select the one described as &lt;em&gt;Install Dashboard App&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Under target selection, choose &lt;strong&gt;Choose instances manually&lt;/strong&gt; and pick your &lt;strong&gt;Managed Instance&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Under Output options, turn off &lt;strong&gt;Enable an S3 bucket&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Expand the &lt;strong&gt;AWS CLI command&lt;/strong&gt; section — this shows you the exact CLI command behind the button, which you could drop into a script instead of clicking through the console every time (same idea as saving a playbook instead of running one-off &lt;code&gt;ansible&lt;/code&gt; commands)&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Run&lt;/strong&gt;, wait a minute or two for the status to hit &lt;strong&gt;Success&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then grab the instance's public IP from the lab details panel and open it in a browser — you should see the &lt;strong&gt;Widget Manufacturing Dashboard&lt;/strong&gt; you just installed, all without ever logging into the box directly.&lt;/p&gt;

&lt;p&gt;One more parallel worth knowing: Ansible lets you target a whole group of hosts using &lt;strong&gt;inventory groups&lt;/strong&gt;. Systems Manager does the same thing using &lt;strong&gt;tags&lt;/strong&gt; — tag a batch of instances, and you can run one document against all of them at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 3: PARAMETER STORE — LIKE ANSIBLE VAULT / GROUP VARS&lt;/strong&gt;&lt;br&gt;
In Ansible, you keep configuration values and secrets outside your playbooks — in &lt;code&gt;group_vars&lt;/code&gt;, &lt;code&gt;host_vars&lt;/code&gt;, or encrypted with &lt;strong&gt;Ansible Vault&lt;/strong&gt; if it's sensitive. The playbook reads those values instead of having them hardcoded.&lt;/p&gt;

&lt;p&gt;Parameter Store is AWS's version of that same idea — a place to store config values and secrets, as plain text or encrypted, referenced by name instead of baked into your application.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In Systems Manager, go to &lt;strong&gt;Application Management → Parameter Store → Create parameter&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;/dashboard/show-beta-features&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Description: &lt;code&gt;Display beta features&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Leave Tier and Type as default&lt;/li&gt;
&lt;li&gt;Value: &lt;code&gt;True&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Create parameter&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Go back to the dashboard app in your browser and refresh it — a third chart appears. The app was already built to check for that parameter; it just needed the value to exist. This is the same pattern as "dark launching" a feature in Ansible-managed config — the feature is already deployed, just waiting on a variable to flip it on.&lt;/p&gt;

&lt;p&gt;(Optional: delete the parameter and refresh again — the chart disappears. That's the value being read live, not cached.)&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%2Fysmoux5x9cg4f8hmk4is.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%2Fysmoux5x9cg4f8hmk4is.png" alt="Beta Features" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TASK 4: SESSION MANAGER — THE PART ANSIBLE DOESN'T REALLY HAVE&lt;/strong&gt;&lt;br&gt;
This is where the comparison breaks a little, and that's worth calling out rather than forcing a fake parallel. Ansible doesn't really have an interactive shell feature — it's built to run tasks and exit, not to give you a live terminal.&lt;/p&gt;

&lt;p&gt;Session Manager, on the other hand, gives you a browser-based shell straight into the instance — no SSH key, no open port, no bastion host. It rides on the same SSM Agent connection that Fleet Manager and Run Command already use.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Node Management → Session Manager → Start session&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select your &lt;strong&gt;Managed Instance&lt;/strong&gt; and choose &lt;strong&gt;Start session&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A shell opens right in your browser. Try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls /var/www/html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the app files Run Command installed earlier.&lt;/p&gt;

&lt;p&gt;Then run this to pull instance details through the CLI, right from inside the session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AZ=`curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone`
export AWS_DEFAULT_REGION=${AZ::-1}
aws ec2 describe-instances
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second block grabs the region the instance is running in, then uses it to list EC2 instance details in JSON — all without you ever needing an SSH key for this box. You could even go check the instance's security group afterward and confirm port 22 isn't open at all. The access is happening entirely through IAM and the SSM Agent instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHY THIS MATTERS&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you already think in Ansible terms, don't relearn from scratch — map the concept: Document = Playbook, Run Command = &lt;code&gt;ansible-playbook&lt;/code&gt;, Inventory = fact-gathering, Parameter Store = vars/Vault, tags = inventory groups&lt;/li&gt;
&lt;li&gt;The one thing that doesn't map over is the connection model itself — Ansible pushes over SSH, Systems Manager relies on an agent pulling instructions. That single difference is why SSM never needs an open SSH port&lt;/li&gt;
&lt;li&gt;Session Manager plus closed SSH ports means every login is going through IAM permissions and gets logged in CloudTrail — that's a real audit trail, not just "someone SSH'd in with a shared key"&lt;/li&gt;
&lt;li&gt;None of these four tools work in isolation — Fleet Manager tells you what's there, Run Command changes it, Parameter Store configures it, and Session Manager lets you get hands-on when you need to&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AWS #SystemsManager #Ansible #DevOps #CloudComputing #Automation
&lt;/h1&gt;

</description>
    </item>
    <item>
      <title>Monitoring an EC2 Instance with CloudWatch and SNS</title>
      <dc:creator>Hashir Saud Khan</dc:creator>
      <pubDate>Fri, 24 Jul 2026 10:43:44 +0000</pubDate>
      <link>https://dev.to/hashirsaudkhan/monitoring-an-ec2-instance-with-cloudwatch-and-sns-5do7</link>
      <guid>https://dev.to/hashirsaudkhan/monitoring-an-ec2-instance-with-cloudwatch-and-sns-5do7</guid>
      <description>&lt;h1&gt;
  
  
  aws #cloudwatch #monitoring #tutorial
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Monitor an EC2 Instance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;INTRO&lt;/strong&gt;&lt;br&gt;
Ever wonder how you'd actually know if something was silently maxing out your server's CPU? That's basically what this lab is about. You set up an alarm that watches an EC2 instance's CPU, wire it to send you an email the moment it spikes, and then you deliberately spike it yourself to prove the whole thing works. It's a simple setup, but it's the same pattern behind real incident alerting — think of the CPU spike as a stand-in for something like malware taking over a box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHAT YOU'RE BUILDING&lt;/strong&gt;&lt;br&gt;
You'll set up three things that talk to each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;SNS topic&lt;/strong&gt; that emails you when triggered&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;CloudWatch alarm&lt;/strong&gt; that watches CPU usage and fires the SNS topic if it crosses 60%&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;stress test&lt;/strong&gt; on the EC2 instance to actually push CPU past that line and prove the alarm works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you'll also have a CloudWatch dashboard so you can glance at that CPU metric anytime without digging through menus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STEP 1: SET UP THE SNS TOPIC&lt;/strong&gt;&lt;br&gt;
First, you need somewhere for the alert to go. Head to SNS and create a topic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type: &lt;strong&gt;Standard&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;MyCwAlarm&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once it's created, go to the Subscriptions tab and add a subscription:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protocol: &lt;strong&gt;Email&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Endpoint: your actual email address&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'll get a confirmation email almost instantly — open it and confirm the subscription. Come back to the console and check that the status flipped to &lt;strong&gt;Confirmed&lt;/strong&gt;. If it still says "Pending confirmation," the alarm won't be able to reach you later, so don't skip this.&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%2Fx4d4ro8ub15x2va6vyxv.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%2Fx4d4ro8ub15x2va6vyxv.png" alt="AWS SNS" width="799" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STEP 2: CREATE THE CLOUDWATCH ALARM&lt;/strong&gt;&lt;br&gt;
Now you tell CloudWatch what to watch and when to yell about it.&lt;/p&gt;

&lt;p&gt;Jump into CloudWatch → Metrics → EC2 → Per-Instance Metrics, and find &lt;code&gt;CPUUtilization&lt;/code&gt; for your Stress Test instance. Right now it should be sitting near 0% — nothing's happened yet.&lt;/p&gt;

&lt;p&gt;Then go to Alarms → Create alarm, and pick that same CPUUtilization metric. Set it up like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Statistic: &lt;strong&gt;Average&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Period: &lt;strong&gt;1 minute&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Condition: &lt;strong&gt;Greater than 60&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the action, point it at the SNS topic you just made (&lt;code&gt;MyCwAlarm&lt;/code&gt;), and give the alarm a name like &lt;code&gt;LabCPUUtilizationAlarm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's it — the alarm now sits quietly in the background, watching.&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%2Fq8pyedqmgyzafwr8vxl5.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%2Fq8pyedqmgyzafwr8vxl5.png" alt="AWS SNS" width="799" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STEP 3: SPIKE THE CPU AND WATCH THE ALARM FIRE&lt;/strong&gt;&lt;br&gt;
This is the part that actually proves it works. Open a terminal into the Stress Test instance and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo stress --cpu 10 -v --timeout 400s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pins the CPU at 100% for about 400 seconds, then lets it drop back down on its own.&lt;/p&gt;

&lt;p&gt;Open a second terminal to the same instance and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;top
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;so you can watch the CPU load live while it's happening.&lt;/p&gt;

&lt;p&gt;Now flip back to the CloudWatch alarm page and refresh every minute or so. Give it a few minutes — you'll see the graph climb past the 60% line, and the alarm state will flip to &lt;strong&gt;In alarm&lt;/strong&gt;. Shortly after, check your inbox — you should have a fresh email from AWS Notifications telling you the alarm went off.&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%2Fconri1yeoog0576l38kk.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%2Fconri1yeoog0576l38kk.png" alt="Alarm alert" width="800" height="380"&gt;&lt;/a&gt;&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%2F7cg0tda2mx6th1cepaha.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%2F7cg0tda2mx6th1cepaha.png" alt="Email from SNS" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STEP 4: BUILD A DASHBOARD&lt;/strong&gt;&lt;br&gt;
Once you've seen the alarm work, it's worth having a permanent view of that metric instead of hunting for it every time.&lt;/p&gt;

&lt;p&gt;Go to CloudWatch → Dashboards → Create dashboard, name it &lt;code&gt;LabEC2Dashboard&lt;/code&gt;, and add a &lt;strong&gt;Line&lt;/strong&gt; widget using the same CPUUtilization metric for your Stress Test instance. Save it, and now you've got a one-click view of that instance's CPU health whenever you need it.&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%2Ffwbvmrd4h7qz41ytgiu5.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%2Ffwbvmrd4h7qz41ytgiu5.png" alt="aws cloudwatch dashboard" width="799" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHAT I TOOK AWAY FROM THIS&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An alarm is only as useful as the notification behind it — CloudWatch watches the metric, but SNS is what actually gets a human's attention&lt;/li&gt;
&lt;li&gt;A CPU spike by itself doesn't tell you &lt;em&gt;why&lt;/em&gt; it happened — it just tells you &lt;em&gt;that&lt;/em&gt; it happened. That's the trigger to go investigate, not the answer&lt;/li&gt;
&lt;li&gt;Confirming the SNS subscription is an easy step to forget, and if you skip it, the whole alert chain silently breaks&lt;/li&gt;
&lt;li&gt;Dashboards aren't just nice-to-have — they turn "let me go find that metric again" into "it's already right there"&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AWS #CloudWatch #SNS #EC2 #CloudSecurity #Monitoring
&lt;/h1&gt;

</description>
    </item>
    <item>
      <title>Skipping the Console: Talking to IAM Through the AWS CLI</title>
      <dc:creator>Hashir Saud Khan</dc:creator>
      <pubDate>Thu, 23 Jul 2026 11:14:23 +0000</pubDate>
      <link>https://dev.to/hashirsaudkhan/skipping-the-console-talking-to-iam-through-the-aws-cli-1akg</link>
      <guid>https://dev.to/hashirsaudkhan/skipping-the-console-talking-to-iam-through-the-aws-cli-1akg</guid>
      <description>&lt;h2&gt;
  
  
  Install and Configure the AWS CLI
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;INTRO&lt;/strong&gt;&lt;br&gt;
If you've only ever clicked around the AWS Console, this one's for you. I set up the AWS CLI from scratch on a Red Hat Linux EC2 instance — not Amazon Linux, which comes with it pre-installed, but Red Hat, which doesn't. So I had to install it, connect it to my AWS account, and then use it to actually talk to IAM. Here's exactly how I did it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHAT YOU'RE BUILDING&lt;/strong&gt;&lt;br&gt;
Here's the picture: you SSH into an EC2 instance sitting inside a VPC. That instance gets the AWS CLI installed on it. Once it's configured with an access key, it can reach out and talk to IAM directly from the terminal — no console needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHAT YOU'LL WALK AWAY WITH&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The AWS CLI installed and working&lt;/li&gt;
&lt;li&gt;The CLI connected to a real AWS account&lt;/li&gt;
&lt;li&gt;Comfort using the CLI to query IAM instead of clicking through the console&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%2Fmyapvbyl5er0p899jig2.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%2Fmyapvbyl5er0p899jig2.png" alt="AWS CLI version output showing successful installation in terminal" width="799" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STEP 1: SSH INTO THE INSTANCE&lt;/strong&gt;&lt;br&gt;
First thing, you need to get onto the EC2 instance itself.&lt;/p&gt;

&lt;p&gt;If you're on macOS or Linux, grab the PEM key, lock down its permissions, and connect:&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="nb"&gt;cd&lt;/span&gt; ~/Downloads
&lt;span class="nb"&gt;chmod &lt;/span&gt;400 labsuser.pem
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; labsuser.pem ec2-user@&amp;lt;ip-address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type &lt;code&gt;yes&lt;/code&gt; when it asks about the connection — you're using a key pair, so no password needed.&lt;/p&gt;

&lt;p&gt;If you're on Windows, you'll use PuTTY with the PPK file instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STEP 2: INSTALL THE AWS CLI&lt;/strong&gt;&lt;br&gt;
Once you're inside the instance, download and install the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"awscliv2.zip"&lt;/span&gt;
unzip &lt;span class="nt"&gt;-u&lt;/span&gt; awscliv2.zip
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./aws/install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check it worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something like &lt;code&gt;aws-cli/2.7.24 Python/3.8.8 Linux/...&lt;/code&gt; — exact version doesn't matter, just that it responds.&lt;/p&gt;

&lt;p&gt;Try &lt;code&gt;aws help&lt;/code&gt; too — if it opens up the help pager, you're good. Press &lt;code&gt;q&lt;/code&gt; to get out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STEP 3: LOOK AT THE IAM SETUP IN THE CONSOLE&lt;/strong&gt;&lt;br&gt;
Before jumping into the CLI, take a quick look at IAM in the console so you know what you're working with.&lt;/p&gt;

&lt;p&gt;Go to IAM → Users → your user. Under Permissions, open the attached policy and view it as JSON — that's the actual permission document controlling what you can and can't do.&lt;/p&gt;

&lt;p&gt;Then check the Security Credentials tab and find your access key ID. You'll need this (and the matching secret key) in the next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STEP 4: CONNECT THE CLI TO YOUR ACCOUNT&lt;/strong&gt;&lt;br&gt;
Back in the terminal, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It'll ask you four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access Key ID — paste it in&lt;/li&gt;
&lt;li&gt;Secret Access Key — paste it in&lt;/li&gt;
&lt;li&gt;Default region — I used &lt;code&gt;us-west-2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Default output format — &lt;code&gt;json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. Your terminal is now authenticated as your AWS user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STEP 5: TALK TO IAM FROM THE TERMINAL&lt;/strong&gt;&lt;br&gt;
Now for the fun part — test it out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam list-users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything's wired up right, you'll get back a JSON list of the IAM users in the account. No console, no clicking — just the CLI doing the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;THE CHALLENGE: PULL THE POLICY DOCUMENT USING ONLY THE CLI&lt;/strong&gt;&lt;br&gt;
Here's where it gets interesting. The task: grab that same IAM policy document you looked at earlier in the console — but this time, do it entirely through the CLI. No cheating by going back to the console.&lt;/p&gt;

&lt;p&gt;Two commands get you there. First, find the policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam list-policies &lt;span class="nt"&gt;--scope&lt;/span&gt; Local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--scope Local&lt;/code&gt; matters here — it filters down to customer-managed policies instead of dumping every AWS-managed policy at you.&lt;/p&gt;

&lt;p&gt;Once you've got the policy's ARN and version ID from that output, pull the actual JSON and save it to a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam get-policy-version &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::&amp;lt;account-id&amp;gt;:policy/lab_policy &lt;span class="nt"&gt;--version-id&lt;/span&gt; v1 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; lab_policy.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;&amp;gt;&lt;/code&gt; at the end is doing the heavy lifting — it takes whatever the command prints and writes it straight into a file instead of just showing it on screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHAT I TOOK AWAY FROM THIS&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CLI and the console aren't different tools — they're two doors into the same house. Everything you can click, you can also script&lt;/li&gt;
&lt;li&gt;Logging into the console needs a username and password. Talking to AWS through the CLI needs an access key and secret key instead — different door, different key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list-policies --scope Local&lt;/code&gt; is worth remembering — without it, you're wading through every AWS-managed policy just to find the one you actually own&lt;/li&gt;
&lt;li&gt;Piping CLI output into a file with &lt;code&gt;&amp;gt;&lt;/code&gt; is a small thing, but it's how you turn a one-off command into something you can actually keep and review later&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AWS #CLI #IAM #Linux #CloudComputing #DevOps
&lt;/h1&gt;

</description>
      <category>aws</category>
      <category>cli</category>
      <category>iam</category>
      <category>rhel</category>
    </item>
    <item>
      <title>The AWS Security Lifecycle and Its Key Services</title>
      <dc:creator>Hashir Saud Khan</dc:creator>
      <pubDate>Tue, 21 Jul 2026 08:04:32 +0000</pubDate>
      <link>https://dev.to/hashirsaudkhan/the-aws-security-lifecycle-and-its-key-services-b75</link>
      <guid>https://dev.to/hashirsaudkhan/the-aws-security-lifecycle-and-its-key-services-b75</guid>
      <description>&lt;h2&gt;
  
  
  Security Lifecycle: Prevention, Detection, Analysis, Response
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;INTRO&lt;/strong&gt;&lt;br&gt;
So I've been digging into how security actually works in practice, and here's the thing — it's not a one-time setup you finish and forget. It's a lifecycle. It loops: Prevention, Detection, Analysis, Response, and back again. Let me walk you through it the way I understood it, along with the exact AWS services tied to each part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;THE SECURITY LIFECYCLE&lt;/strong&gt;&lt;br&gt;
Picture four stages running in a continuous loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prevention&lt;/strong&gt; — you stop the incident before it even happens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detection&lt;/strong&gt; — if it does happen anyway, you catch it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analysis&lt;/strong&gt; — you figure out what went wrong and why&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response&lt;/strong&gt; — you act on it, then feed what you learned back into Prevention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice how it circles back? That's the whole point. Every response makes your next round of prevention stronger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PREVENTION — WHERE MOST OF YOUR WORK HAPPENS&lt;/strong&gt;&lt;br&gt;
This is the stage you'll spend the most time on. I broke it into five areas, and each one has a specific AWS service behind it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Network Hardening — Amazon Inspector&lt;/strong&gt;&lt;br&gt;
You use this for vulnerability assessment. It scans your environment for known issues like CVEs, so you can patch them before anyone else finds them first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. System Hardening — AWS Systems Manager&lt;/strong&gt;&lt;br&gt;
This is where Patch Manager comes in. Instead of logging into every single instance to update it, you patch your whole fleet from one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Data Security — AWS KMS&lt;/strong&gt;&lt;br&gt;
You'll use this to encrypt your data at rest with symmetric keys, managed through the AWS Encryption CLI. Even if someone gets to your data, it's useless without the key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Public Key Infrastructure — AWS ACM&lt;/strong&gt;&lt;br&gt;
This handles your SSL/TLS certificates using asymmetric public/private key pairs. It takes care of issuing and renewing them, so you're not the one finding out a cert expired at 2am.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Identity Management — AWS IAM&lt;/strong&gt;&lt;br&gt;
This is the one everything else depends on. You decide who — or what — gets access to which resource. If your IAM isn't locked down, none of the other four matter as much.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DETECTION, ANALYSIS, RESPONSE&lt;/strong&gt;&lt;br&gt;
Prevention is just where you start. The other three stages are what close the loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detection&lt;/strong&gt; — you're watching for suspicious activity or a policy violation as it happens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analysis&lt;/strong&gt; — you dig into the logs and findings to trace the root cause and understand the impact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response&lt;/strong&gt; — you contain it, fix it, and take what you learned straight back into Prevention&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AWS #Security #CloudSecurity #IAM #KMS #Inspector
&lt;/h1&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>cybersecurity</category>
      <category>security</category>
    </item>
    <item>
      <title>Deploying MySQL on RDS and Joining Tables Like It's Production</title>
      <dc:creator>Hashir Saud Khan</dc:creator>
      <pubDate>Sun, 19 Jul 2026 15:35:50 +0000</pubDate>
      <link>https://dev.to/hashirsaudkhan/deploying-mysql-on-rds-and-joining-tables-like-its-production-ck</link>
      <guid>https://dev.to/hashirsaudkhan/deploying-mysql-on-rds-and-joining-tables-like-its-production-ck</guid>
      <description>&lt;p&gt;Rds challenge lab devto post 🗄️🐬&lt;/p&gt;

&lt;h1&gt;
  
  
  aws #rds #database #tutorial
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Build Your DB Server and Interact With Your DB
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;INTRO&lt;/strong&gt;&lt;br&gt;
Did a hands-on AWS challenge lab on Amazon RDS. Task: spin up a managed database, connect from a Linux server, and run real SQL — create tables, insert data, join across tables. No hand-holding here, just requirements to figure out myself. Here's the walkthrough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SCENARIO&lt;/strong&gt;&lt;br&gt;
Service: Amazon RDS&lt;br&gt;
Role: Cloud/DB Admin&lt;br&gt;
Goal: Launch RDS under set constraints, connect via EC2, run SQL (create, insert, select, join)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ARCHITECTURE&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LinuxServer (EC2) sits in the Lab VPC — this is the client&lt;/li&gt;
&lt;li&gt;RDS instance (Aurora or MySQL) in the same VPC&lt;/li&gt;
&lt;li&gt;Security group lets LinuxServer talk to RDS&lt;/li&gt;
&lt;li&gt;Flow: LinuxServer -&amp;gt; MySQL client (port 3306) -&amp;gt; RDS -&amp;gt; tables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;STEP 1: LAUNCH THE RDS INSTANCE&lt;/strong&gt;&lt;br&gt;
Constraints for this lab:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Engine: Aurora (Provisioned) or MySQL — no serverless&lt;/li&gt;
&lt;li&gt;Template: Dev/Test or Free tier&lt;/li&gt;
&lt;li&gt;No standby instance (single-AZ only)&lt;/li&gt;
&lt;li&gt;Instance size: db.t3.micro to db.t3.medium&lt;/li&gt;
&lt;li&gt;Storage: gp2, up to 100 GB — no Provisioned IOPS&lt;/li&gt;
&lt;li&gt;Network: Lab VPC&lt;/li&gt;
&lt;li&gt;Security group must allow LinuxServer access&lt;/li&gt;
&lt;li&gt;MySQL only: turn off Enhanced Monitoring&lt;/li&gt;
&lt;li&gt;On-Demand only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These limits keep costs in check — Provisioned IOPS and Multi-AZ are the fastest ways to blow up an RDS bill.&lt;/p&gt;

&lt;p&gt;Noted the master username, password, and endpoint — needed next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STEP 2: CONNECT TO THE LINUX SERVER&lt;/strong&gt;&lt;br&gt;
Downloaded the PEM key, grabbed the LinuxServer address, connected over SSH:&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="nb"&gt;chmod &lt;/span&gt;400 labsuser.pem
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; labsuser.pem ec2-user@&amp;lt;LinuxServer-address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This box is just the SQL client — it needs network access to RDS, nothing more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STEP 3: INSTALL MYSQL CLIENT AND CONNECT&lt;/strong&gt;&lt;br&gt;
On the LinuxServer:&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="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install &lt;/span&gt;mysql &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect using the master credentials from Step 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-h&lt;/span&gt; &amp;lt;rds-endpoint&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &amp;lt;master-username&amp;gt; &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it hangs, it's almost always the security group — check port 3306 inbound.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STEP 4: CREATE THE RESTART TABLE&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;lab_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="n"&gt;lab_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;RESTART&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;StudentID&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;StudentName&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;RestartCity&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;GraduationDate&lt;/span&gt; &lt;span class="nb"&gt;DATETIME&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 5: INSERT 10 SAMPLE ROWS&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;RESTART&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StudentID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StudentName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RestartCity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GraduationDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Ahmed Khan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Karachi'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-15 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Sara Ali'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Lahore'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-02-10 11:30:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Bilal Hassan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Islamabad'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-03-05 09:15:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Ayesha Malik'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Karachi'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-20 14:00:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Usman Tariq'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Faisalabad'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-04-12 10:45:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Hina Shaikh'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Karachi'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-02-28 13:30:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Omar Farooq'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Multan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-03-18 12:00:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Zara Iqbal'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Lahore'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-30 15:20:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Danish Raza'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Karachi'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-04-02 09:50:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mahnoor Aslam'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Islamabad'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-02-15 11:10:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 6: SELECT ALL FROM RESTART&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;RESTART&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 7: CREATE THE CLOUD_PRACTITIONER TABLE&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;CLOUD_PRACTITIONER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;StudentID&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CertificationDate&lt;/span&gt; &lt;span class="nb"&gt;DATETIME&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 8: INSERT 5 SAMPLE ROWS&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;CLOUD_PRACTITIONER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StudentID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CertificationDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-05-01 10:00:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-05-03 11:00:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-05-05 09:30:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-05-07 14:15:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-05-09 10:20:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 9: SELECT ALL FROM CLOUD_PRACTITIONER&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;CLOUD_PRACTITIONER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STEP 10: INNER JOIN&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StudentID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StudentName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;C&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CertificationDate&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;RESTART&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;CLOUD_PRACTITIONER&lt;/span&gt; &lt;span class="k"&gt;C&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StudentID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;C&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StudentID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only students in &lt;em&gt;both&lt;/em&gt; tables show up — filters out anyone not yet certified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHY THIS MATTERS&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RDS constraints = real cost guardrails, not busywork&lt;/li&gt;
&lt;li&gt;Security groups are the #1 connection blocker&lt;/li&gt;
&lt;li&gt;Base table + event table + join = same pattern used in production apps&lt;/li&gt;
&lt;li&gt;Always capture before/after proof at each step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;TOOLS USED&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon RDS (Aurora / MySQL)&lt;/li&gt;
&lt;li&gt;Amazon EC2&lt;/li&gt;
&lt;li&gt;Amazon VPC + Security Groups&lt;/li&gt;
&lt;li&gt;MySQL client / SQL&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AWS #RDS #Database #MySQL #CloudComputing #SQL
&lt;/h1&gt;

</description>
      <category>aws</category>
      <category>database</category>
      <category>sql</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Blocking Malware with AWS Network Firewall (Bump-in-the-wire Design) 🛑🛡️</title>
      <dc:creator>Hashir Saud Khan</dc:creator>
      <pubDate>Fri, 17 Jul 2026 06:59:35 +0000</pubDate>
      <link>https://dev.to/hashirsaudkhan/blocking-malware-with-aws-network-firewall-bump-in-the-wire-design-1l6</link>
      <guid>https://dev.to/hashirsaudkhan/blocking-malware-with-aws-network-firewall-bump-in-the-wire-design-1l6</guid>
      <description>&lt;p&gt;Blocking Malware Downloads with AWS Network Firewall (Stateful Suricata Rules)&lt;/p&gt;

&lt;h2&gt;
  
  
  INTRO
&lt;/h2&gt;

&lt;p&gt;Ran a hands-on lab that simulates a real-world security incident: end users at a&lt;br&gt;
company kept accidentally downloading malware after visiting a specific website.&lt;br&gt;
IT had already identified the malicious URLs. My job was to harden the network&lt;br&gt;
perimeter using AWS Network Firewall so those files could never reach an internal&lt;br&gt;
host again. Here's a full walkthrough of what I did and why each step matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  SCENARIO
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Company: AnyCompany (lab scenario)&lt;/li&gt;
&lt;li&gt;Role: Security Engineer&lt;/li&gt;
&lt;li&gt;Problem: Users downloading malware from a known-bad site&lt;/li&gt;
&lt;li&gt;Given: Exact URLs hosting the malicious files&lt;/li&gt;
&lt;li&gt;Goal: Block access to those files at the network layer, without touching
every single endpoint individually&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ARCHITECTURE
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A pre-configured EC2 instance (TestInstance) sits in an isolated perimeter
subnet — used purely to simulate an end user's browser/download behavior&lt;/li&gt;
&lt;li&gt;An AWS Network Firewall (LabFirewall) sits between the VPC and the internet&lt;/li&gt;
&lt;li&gt;The firewall is driven by a Firewall Policy (LabFirewallPolicy), which
determines stateless vs. stateful packet handling&lt;/li&gt;
&lt;li&gt;A Stateful Rule Group written in Suricata syntax is attached to the policy
to do the actual content-based blocking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow: TestInstance -&amp;gt; Network Firewall (stateless check -&amp;gt; stateful rule&lt;br&gt;
group) -&amp;gt; Internet -&amp;gt; malicious site (now blocked)&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: CONFIRM THE PROBLEM IS REAL
&lt;/h2&gt;

&lt;p&gt;Before touching any config, I logged into TestInstance via Systems Manager&lt;br&gt;
Session Manager and reproduced the issue:&lt;/p&gt;

&lt;p&gt;wget &lt;a href="http://malware.wicar.org/data/js_crypto_miner.html" rel="noopener noreferrer"&gt;http://malware.wicar.org/data/js_crypto_miner.html&lt;/a&gt;&lt;br&gt;
  wget &lt;a href="http://malware.wicar.org/data/java_jre17_exec.html" rel="noopener noreferrer"&gt;http://malware.wicar.org/data/java_jre17_exec.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both returned HTTP 200 OK and downloaded successfully. This confirms the&lt;br&gt;
malicious files are currently reachable and the firewall isn't inspecting&lt;br&gt;
or blocking this traffic yet. Never skip this step in real environments —&lt;br&gt;
you need a documented "before" state to prove the fix worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 2: SWITCH THE FIREWALL POLICY TO STATEFUL INSPECTION
&lt;/h2&gt;

&lt;p&gt;By default, a lot of basic firewall configs only do stateless filtering&lt;br&gt;
(rules based on IP/port only, no context, no content awareness). To block&lt;br&gt;
based on URI content, traffic needs to go through the stateful rules engine.&lt;/p&gt;

&lt;p&gt;In the Firewall Policy (LabFirewallPolicy):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stateless default actions -&amp;gt; Edit&lt;/li&gt;
&lt;li&gt;Fragmented packets: "Use the same actions for all packets"&lt;/li&gt;
&lt;li&gt;Action: "Forward to stateful rule groups"&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stateless engine = fast, no context, evaluates each packet in isolation&lt;br&gt;
Stateful engine = slower, but understands traffic flow/direction and lets&lt;br&gt;
you write much more precise rules (and log everything)&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: WRITE A STATEFUL RULE GROUP (SURICATA SYNTAX)
&lt;/h2&gt;

&lt;p&gt;Created a new Stateful Rule Group:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rule group type: Stateful&lt;/li&gt;
&lt;li&gt;Format: Suricata compatible rule string&lt;/li&gt;
&lt;li&gt;Evaluation order: Action order&lt;/li&gt;
&lt;li&gt;Name: StatefulRuleGroup&lt;/li&gt;
&lt;li&gt;Capacity: 100&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rules used:&lt;/p&gt;

&lt;p&gt;drop http $HOME_NET any -&amp;gt; $EXTERNAL_NET 80 (msg:"MALWARE custom solution"; flow: to_server,established; classtype:trojan-activity; sid:2002001; content:"/data/js_crypto_miner.html"; http_uri; rev:1;)&lt;/p&gt;

&lt;p&gt;drop http $HOME_NET any -&amp;gt; $EXTERNAL_NET 80 (msg:"MALWARE custom solution"; flow: to_server,established; classtype:trojan-activity; sid:2002002; content:"/data/java_jre17_exec.html"; http_uri; rev:1;)&lt;/p&gt;

&lt;p&gt;What this actually does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;drop        -&amp;gt; silently block the packet (no reset sent back)&lt;/li&gt;
&lt;li&gt;http        -&amp;gt; only inspect HTTP traffic&lt;/li&gt;
&lt;li&gt;$HOME_NET -&amp;gt; $EXTERNAL_NET -&amp;gt; traffic leaving your VPC toward the internet&lt;/li&gt;
&lt;li&gt;flow: to_server, established -&amp;gt; only match on established outbound requests&lt;/li&gt;
&lt;li&gt;content: "..."; http_uri -&amp;gt; match specifically on the URI path, not
just the domain or IP — this is the key advantage over basic
IP/domain blocklists&lt;/li&gt;
&lt;li&gt;sid -&amp;gt; unique rule ID (required by Suricata)&lt;/li&gt;
&lt;li&gt;classtype: trojan-activity -&amp;gt; categorizes the threat for logging/alerting&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  STEP 4: ATTACH THE RULE GROUP TO THE FIREWALL
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Firewalls -&amp;gt; LabFirewall -&amp;gt; Associated firewall policy -&amp;gt; LabFirewallPolicy&lt;/li&gt;
&lt;li&gt;Stateful rule groups -&amp;gt; Add unmanaged stateful rule groups&lt;/li&gt;
&lt;li&gt;Select StatefulRuleGroup -&amp;gt; Add stateful rule group&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the step that actually activates the rules — until this point, the&lt;br&gt;
rule group existed but had zero effect on real traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 5: VALIDATE
&lt;/h2&gt;

&lt;p&gt;Back on TestInstance, re-ran the exact same commands:&lt;/p&gt;

&lt;p&gt;wget &lt;a href="http://malware.wicar.org/data/js_crypto_miner.html" rel="noopener noreferrer"&gt;http://malware.wicar.org/data/js_crypto_miner.html&lt;/a&gt;&lt;br&gt;
  wget &lt;a href="http://malware.wicar.org/data/java_jre17_exec.html" rel="noopener noreferrer"&gt;http://malware.wicar.org/data/java_jre17_exec.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time: "HTTP request sent, awaiting response..." and then it just hangs&lt;br&gt;
(had to Ctrl+C out of it) — no 200 OK, no file downloaded. The firewall is&lt;br&gt;
silently dropping matching requests before they ever complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHY THIS MATTERS (KEY TAKEAWAYS)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;URI-level blocking beats plain IP/domain blocklists. Attackers can move
files to a new path on the same domain — a rule matching http_uri content
catches the actual malicious payload path, not just the host&lt;/li&gt;
&lt;li&gt;Stateless vs. stateful matters. If your firewall policy defaults to
stateless-only, none of your Suricata content rules will ever fire&lt;/li&gt;
&lt;li&gt;This is a form of IDS/IPS at the network edge without needing a separate
third-party appliance — useful as one layer in a defense-in-depth strategy&lt;/li&gt;
&lt;li&gt;Always validate with a "before" and "after" test. Screenshots/logs of
both states are what actually prove the fix to stakeholders/auditors&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  TOOLS/SERVICES USED
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AWS Network Firewall&lt;/li&gt;
&lt;li&gt;AWS VPC&lt;/li&gt;
&lt;li&gt;Amazon EC2&lt;/li&gt;
&lt;li&gt;Suricata rule syntax (IDS/IPS)&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AWS #CyberSecurity #NetworkFirewall #CloudSecurity #InfoSec #Suricata
&lt;/h1&gt;

</description>
      <category>aws</category>
      <category>networking</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
