What is Amazon EC2?
Amazon Elastic Compute Cloud (EC2) is a core AWS service that provides resizable, secure compute capacity in the cloud. In simple terms, EC2 lets you rent virtual servers — called instances — running on Amazon's physical infrastructure. You choose the operating system, CPU, memory, storage, and networking configuration, then launch the instance and connect to it just like any physical server. You pay only for the compute time you actually consume.
EC2 underpins virtually every modern cloud workload. From simple web hosting to massive distributed data processing clusters, from development and test environments to production microservices orchestrations, EC2 is the foundational building block. It eliminates the need to procure, rack, cable, and maintain physical hardware, compressing what used to take weeks into minutes.
Core Concepts You Must Understand
Before diving into setup, grasp these fundamental EC2 building blocks:
- AMI (Amazon Machine Image) — A pre-configured template containing an operating system, application server, and applications. Think of it as the "golden image" from which your instance is born.
- Instance Type — Defines the virtual hardware: vCPUs, memory, network performance, and instance storage. Types range from general purpose (t3, m5) to compute-optimized (c5), memory-optimized (r5), storage-optimized (i3), and GPU-equipped (g5, p4).
- Key Pair — A public-private key pair used for secure SSH (Linux) or RDP (Windows) authentication. You download the private key once at creation time.
- Security Group — A virtual firewall that controls inbound and outbound traffic at the instance level. Rules are permissive (allow rules), not restrictive (deny rules).
- EBS (Elastic Block Store) — Persistent block storage volumes that survive instance termination. They can be detached and reattached to other instances.
- VPC (Virtual Private Cloud) — The isolated network boundary in which your instance lives. Every EC2 instance must be launched into a VPC subnet.
- Elastic IP — A static public IPv4 address that you can remap to different instances, useful for failover scenarios.
Why EC2 Matters: Key Use Cases and Benefits
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →EC2 isn't just another server rental service — it's the elastic, programmable backbone of cloud-native architecture. Here's why it matters:
- Elastic Scale — Using Auto Scaling groups, your application can automatically grow from one instance to hundreds during a traffic spike, then shrink back down, all in minutes. Traditional data centers simply cannot match this.
- Global Reach — Launch instances in any AWS region worldwide, placing compute close to your users for lower latency.
- Flexible Pricing Models — On-Demand (pay by the second), Reserved Instances (significant discount for commitment), Savings Plans, and Spot Instances (up to 90% off spare capacity) let you optimize cost for any workload pattern.
- Deep Integration — EC2 works natively with IAM for security, CloudWatch for monitoring, S3 for storage, Lambda for event-driven compute, and hundreds of other AWS services.
- Bare Metal Option — For workloads that need direct hardware access (licensing requirements, certain HPC workloads), EC2 offers bare metal instances with no hypervisor overhead.
- Compliance and Governance — AWS's extensive compliance certifications (HIPAA, PCI DSS, FedRAMP, GDPR) flow through to EC2, simplifying your own compliance posture.
Complete Setup and Configuration Walkthrough
This section walks through launching, connecting to, and configuring an EC2 instance from scratch — using the AWS CLI, which is the preferred tool for repeatable, automated setups. We'll cover both the CLI approach and critical Console steps.
Prerequisites
Ensure you have these before starting:
- AWS CLI installed and configured with credentials (
aws configurewith Access Key, Secret Key, region, and output format) - An existing VPC with at least one public subnet (or let AWS create one for you)
- Permissions via IAM to launch EC2 instances, create security groups, and manage key pairs
Step 1: Create a Key Pair
Key pairs are your primary authentication mechanism for Linux instances. Generate one using the CLI:
# Create a new key pair named 'my-app-keypair' and save the private key locally
aws ec2 create-key-pair \
--key-name my-app-keypair \
--key-type rsa \
--key-format pem \
--query "KeyMaterial" \
--output text > my-app-keypair.pem
# Set correct permissions on the private key file (CRITICAL)
chmod 400 my-app-keypair.pem
# Verify the key pair exists
aws ec2 describe-key-pairs --key-names my-app-keypair
Important: The .pem file is your only copy of the private key. AWS does not store it. Lose it, and you lose access to instances launched with this key pair. Store it securely — consider AWS Secrets Manager or a hardware security module for production.
Step 2: Create a Security Group
Security groups define allowed inbound and outbound traffic. Let's create one that permits SSH from your IP and HTTP/HTTPS from anywhere:
# Create the security group in your default VPC (or specify --vpc-id)
aws ec2 create-security-group \
--group-name web-server-sg \
--description "Security group for web server - allows SSH, HTTP, HTTPS" \
--vpc-id vpc-xxxxxxxxxxxxx # Replace with your actual VPC ID
# Get the security group ID for later use
SG_ID=$(aws ec2 describe-security-groups \
--filters Name=group-name,Values=web-server-sg \
--query "SecurityGroups[0].GroupId" \
--output text)
echo "Security Group ID: $SG_ID"
# Add inbound rules
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 22 \
--cidr 203.0.113.25/32 # Replace with YOUR actual IP address
# Use --cidr $(curl -s ifconfig.me)/32 to dynamically fetch your IP
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
# Optional: add ICMP for ping debugging
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol icmp \
--port -1 \
--cidr 203.0.113.25/32
Production note: Never use 0.0.0.0/0 for SSH (port 22). Always restrict SSH to known IP ranges — VPN CIDRs, office egress IPs, or bastion host IPs. For HTTP/HTTPS, 0.0.0.0/0 is expected for public-facing web servers.
Step 3: Launch the EC2 Instance
Now launch an instance using a publicly available AMI. We'll use Amazon Linux 2023, a t3.micro (free tier eligible), attach our security group and key pair:
# Find the latest Amazon Linux 2023 AMI ID for your region
AMI_ID=$(aws ec2 describe-images \
--owners amazon \
--filters "Name=name,Values=al2023-ami-2023.*-kernel-6.1-x86_64" \
"Name=state,Values=available" \
"Name=architecture,Values=x86_64" \
"Name=virtualization-type,Values=hvm" \
--query "Images | sort_by(@, &CreationDate)[-1].ImageId" \
--output text)
echo "Using AMI: $AMI_ID"
# Launch the instance
INSTANCE_ID=$(aws ec2 run-instances \
--image-id $AMI_ID \
--instance-type t3.micro \
--key-name my-app-keypair \
--security-group-ids $SG_ID \
--subnet-id subnet-xxxxxxxxxxxxx # Replace with your subnet ID
--associate-public-ip-address \
--block-device-mappings '[
{
"DeviceName": "/dev/xvda",
"Ebs": {
"VolumeSize": 20,
"VolumeType": "gp3",
"DeleteOnTermination": true,
"Encrypted": true
}
}
]' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=web-server-prod}]' \
--metadata-options 'HttpTokens=required,HttpPutResponseHopLimit=2' \
--query "Instances[0].InstanceId" \
--output text)
echo "Instance ID: $INSTANCE_ID"
# Wait for the instance to reach 'running' state
aws ec2 wait instance-running --instance-ids $INSTANCE_ID
# Get the public IP address
PUBLIC_IP=$(aws ec2 describe-instances \
--instance-ids $INSTANCE_ID \
--query "Reservations[0].Instances[0].PublicIpAddress" \
--output text)
echo "Public IP: $PUBLIC_IP"
Let's break down what's happening in this launch command:
- block-device-mappings: We're specifying a 20 GB gp3 EBS root volume with encryption enabled. gp3 gives consistent 3000 IOPS and 125 MB/s baseline throughput regardless of volume size.
- associate-public-ip-address: Ensures the instance gets a public IP. In production, you might omit this and use an Elastic IP or load balancer instead.
- tag-specifications: Tags are critical for cost allocation, automation, and organization. Always tag instances with at least a Name tag.
- metadata-options: Setting
HttpTokens=requiredenforces IMDSv2 (Instance Metadata Service v2), which protects against SSRF vulnerabilities. This is a crucial security configuration.
Step 4: Connect to Your Instance via SSH
With the public IP and key pair, connect securely:
# Basic SSH connection
ssh -i my-app-keypair.pem ec2-user@$PUBLIC_IP
# More explicit connection with host key checking
ssh -i my-app-keypair.pem \
-o StrictHostKeyChecking=accept-new \
-o IdentitiesOnly=yes \
ec2-user@$PUBLIC_IP
# If you need to troubleshoot connection issues, use verbose mode
ssh -vvv -i my-app-keypair.pem ec2-user@$PUBLIC_IP
Connection troubleshooting checklist:
- Is the instance in
runningstate? Check withaws ec2 describe-instance-status - Does the security group allow inbound port 22 from your IP? Verify with
aws ec2 describe-security-groups - Are you using the correct username? Amazon Linux =
ec2-user, Ubuntu =ubuntu, RHEL =ec2-userorroot, Debian =admin - Is the key file permission correct? Must be
400or600 - Is the instance in a public subnet with an Internet Gateway route?
Step 5: Initial System Configuration
Once connected, perform essential baseline configuration. Here's a script that covers security hardening, package updates, and basic monitoring:
# Inside the EC2 instance, run these commands
# 1. Update all packages (Amazon Linux 2023 uses dnf)
sudo dnf update -y
sudo dnf upgrade -y
# 2. Install essential tools
sudo dnf install -y \
amazon-ec2-utils \
amazon-cloudwatch-agent \
htop \
iotop \
jq \
git \
docker
# 3. Start and enable Docker if needed
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker ec2-user # Allow ec2-user to run docker without sudo
# 4. Configure automatic security updates
sudo dnf install -y dnf-automatic
sudo systemctl enable dnf-automatic.timer
sudo systemctl start dnf-automatic.timer
# 5. Set hostname (persistent across reboots)
sudo hostnamectl set-hostname web-server-prod
# 6. Configure timezone
sudo timedatectl set-timezone UTC
# 7. Enable and start the CloudWatch agent
sudo systemctl enable amazon-cloudwatch-agent
sudo systemctl start amazon-cloudwatch-agent
# 8. Disable unused services (harden the system)
sudo systemctl disable --now rpcbind.socket 2>/dev/null || true
sudo systemctl disable --now rpcbind.service 2>/dev/null || true
# 9. Set up a basic firewall with iptables or nftables
# Amazon Linux 2023 uses nftables by default
sudo dnf install -y iptables-services
# Note: Security Groups handle most filtering; OS-level firewall adds defense in depth
Step 6: Deploy a Sample Application (Nginx + Simple Web App)
Let's deploy a real application to make this instance useful. We'll set up Nginx with a simple Node.js backend proxied behind it:
# Install Nginx and Node.js
sudo dnf install -y nginx nodejs npm
# Create a simple Node.js application directory
sudo mkdir -p /opt/webapp
sudo chown ec2-user:ec2-user /opt/webapp
cd /opt/webapp
# Create package.json and app.js
cat > package.json << 'EOF'
{
"name": "simple-webapp",
"version": "1.0.0",
"description": "EC2 demo application",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
EOF
# Install dependencies
npm install
# Create the application file
cat > app.js << 'EOF'
const express = require('express');
const os = require('os');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({
message: 'Hello from EC2!',
instanceId: process.env.INSTANCE_ID || 'local',
hostname: os.hostname(),
uptime: Math.floor(os.uptime()),
timestamp: new Date().toISOString(),
requestHeaders: req.headers
});
});
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
EOF
# Create a systemd service file for the Node.js app
sudo tee /etc/systemd/system/webapp.service > /dev/null << 'EOF'
[Unit]
Description=Simple Web Application
After=network.target
[Service]
Type=simple
User=ec2-user
WorkingDirectory=/opt/webapp
ExecStart=/usr/bin/node /opt/webapp/app.js
Restart=always
RestartSec=10
Environment=PORT=3000
Environment=INSTANCE_ID=from-metadata
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd, enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable webapp.service
sudo systemctl start webapp.service
# Verify it's running
sudo systemctl status webapp.service
curl http://localhost:3000/health
Now configure Nginx as a reverse proxy:
# Configure Nginx
sudo tee /etc/nginx/conf.d/webapp.conf > /dev/null << 'EOF'
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Proxy to Node.js backend
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Health check endpoint (no caching)
location /health {
proxy_pass http://127.0.0.1:3000/health;
proxy_cache_bypass 1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
}
EOF
# Remove default server block if it conflicts
sudo rm -f /etc/nginx/conf.d/default.conf /etc/nginx/default.d/default.conf 2>/dev/null || true
# Test and reload Nginx
sudo nginx -t
sudo systemctl enable nginx
sudo systemctl reload nginx
# Test the full stack
curl http://localhost/
curl http://localhost/health
At this point, if you navigate to http://PUBLIC_IP in a browser (assuming your security group allows port 80), you'll see the JSON response from the Node.js application.
Step 7: Configure CloudWatch Monitoring and Logs
Production instances need proper monitoring. Configure the CloudWatch agent to collect system metrics and application logs:
# Create CloudWatch agent configuration file
sudo tee /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json > /dev/null << 'EOF'
{
"agent": {
"metrics_collection_interval": 60,
"run_as_user": "root"
},
"metrics": {
"append_dimensions": {
"InstanceId": "${aws:InstanceId}",
"ImageId": "${aws:ImageId}"
},
"metrics_collected": {
"mem": {
"measurement": [
{"name": "mem_used_percent", "rename": "MemoryUtilization"}
]
},
"disk": {
"measurement": [
{"name": "disk_used_percent", "rename": "DiskUtilization"}
],
"resources": ["/"]
},
"netstat": {
"measurement": [
{"name": "tcp_connection_count", "rename": "TCPConnections"}
]
}
}
},
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/var/log/nginx/access.log",
"log_group_name": "/ec2/webapp/nginx/access",
"log_stream_name": "{instance_id}",
"timezone": "UTC"
},
{
"file_path": "/var/log/nginx/error.log",
"log_group_name": "/ec2/webapp/nginx/error",
"log_stream_name": "{instance_id}",
"timezone": "UTC"
},
{
"file_path": "/var/log/messages",
"log_group_name": "/ec2/webapp/system/messages",
"log_stream_name": "{instance_id}",
"timezone": "UTC"
}
]
}
}
}
}
EOF
# Start the CloudWatch agent with the configuration
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config \
-m ec2 \
-c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json \
-s
# Verify the agent is running
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a status
Now you can view instance metrics (CPU, memory, disk, network) in CloudWatch Metrics and application logs in CloudWatch Logs.
Production-Grade Configuration Patterns
Pattern 1: User Data for Bootstrapping
Instead of manually running setup commands, embed them in User Data — a script that runs at first boot. This is the foundation of immutable infrastructure:
# User Data script (paste into --user-data when launching, or specify via file)
cat > user-data.sh << 'EOF'
#!/bin/bash
set -e
# Update system
dnf update -y
# Install packages
dnf install -y nginx nodejs npm git htop
# Create application
mkdir -p /opt/webapp
cat > /opt/webapp/app.js << 'APPEOF'
const express = require('express');
const app = express();
app.get('/', (req, res) => res.json({ status: 'ok', instance: process.env.INSTANCE_ID || 'unknown' }));
app.listen(3000);
APPEOF
cd /opt/webapp && npm init -y && npm install express
# Create systemd service
cat > /etc/systemd/system/webapp.service << 'SERVEOF'
[Unit]
Description=Web Application
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/webapp
ExecStart=/usr/bin/node /opt/webapp/app.js
Restart=always
Environment=PORT=3000
[Install]
WantedBy=multi-user.target
SERVEOF
systemctl daemon-reload
systemctl enable webapp.service
systemctl start webapp.service
# Configure nginx
cat > /etc/nginx/conf.d/default.conf << 'NGXEOF'
server {
listen 80;
location / { proxy_pass http://127.0.0.1:3000; }
}
NGXEOF
systemctl enable nginx
systemctl start nginx
echo "Bootstrap complete at $(date)" >> /var/log/user-data.log
EOF
# Launch with user data
aws ec2 run-instances \
--image-id $AMI_ID \
--instance-type t3.micro \
--key-name my-app-keypair \
--security-group-ids $SG_ID \
--user-data file://user-data.sh \
--subnet-id subnet-xxxxxxxxxxxxx
Pattern 2: IAM Instance Profile for Secure AWS Access
Never hardcode AWS credentials on an EC2 instance. Instead, attach an IAM role (instance profile) that grants the necessary permissions:
# Create an IAM role with an assume role policy for EC2
aws iam create-role \
--role-name ec2-webapp-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# Attach policies (example: S3 read access and CloudWatch write)
aws iam attach-role-policy \
--role-name ec2-webapp-role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
aws iam attach-role-policy \
--role-name ec2-webapp-role \
--policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
# Create instance profile
aws iam create-instance-profile \
--instance-profile-name ec2-webapp-profile
aws iam add-role-to-instance-profile \
--instance-profile-name ec2-webapp-profile \
--role-name ec2-webapp-role
# Launch instance with the instance profile
aws ec2 run-instances \
--image-id $AMI_ID \
--instance-type t3.micro \
--key-name my-app-keypair \
--security-group-ids $SG_ID \
--iam-instance-profile Name=ec2-webapp-profile \
--subnet-id subnet-xxxxxxxxxxxxx
Now from within the instance, the AWS CLI and SDKs automatically retrieve temporary credentials via the instance metadata service — no configuration needed.
Pattern 3: Elastic IP for Persistent Public Address
When you stop and restart an instance, its public IP changes (unless it's in a VPC with a persistent configuration). An Elastic IP gives you a static address:
# Allocate a new Elastic IP
aws ec2 allocate-address \
--domain vpc \
--tag-specifications 'ResourceType=elastic-ip,Tags=[{Key=Name,Value=web-server-eip}]'
# Associate it with your instance
aws ec2 associate-address \
--instance-id $INSTANCE_ID \
--allocation-id eipalloc-xxxxxxxxxxxxx
# Now the instance is reachable at the Elastic IP permanently
# Note: Elastic IPs are free while associated with a running instance;
# you're charged for unassociated or remapped EIPs
Best Practices for EC2
1. Security Hardening
- Always enforce IMDSv2 — Set
HttpTokens=requiredon every instance. IMDSv1 is vulnerable to SSRF attacks that can exfiltrate credentials. - Use instance profiles, not access keys — IAM roles provide temporary, automatically rotated credentials. Access keys embedded in code are a major breach vector.
- Restrict security group rules — Never allow 0.0.0.0/0 to SSH, RDP, or database ports. Use specific CIDRs or reference other security groups.
- Encrypt EBS volumes by default — Enable EBS encryption by default in your AWS account settings. This ensures all new volumes are encrypted without manual specification.
- Patch regularly — Use
dnf-automatic(Amazon Linux) orunattended-upgrades(Ubuntu) for automated security patches.
2. High Availability and Resilience
- Spread across Availability Zones — Never run critical workloads in a single AZ. Use Auto Scaling groups spanning at least two AZs.
- Use load balancers — Place instances behind an Application Load Balancer (ALB) or Network Load Balancer (NLB) rather than exposing individual instance IPs.
- Implement health checks — Define meaningful health check endpoints (like the
/healthroute we created) and configure both load balancer and Auto Scaling group health checks. - Design for failure — Assume any instance can terminate at any time. Use stateless application design, externalize state to RDS/ElastiCache/DynamoDB.
3. Cost Optimization
- Right-size instances — Use CloudWatch metrics and AWS Compute Optimizer to identify over-provisioned instances. A t3.medium might handle what you're running on an m5.large.
- Purchase Reserved Instances or Savings Plans — For steady-state workloads (dev/test environments, production baseline), commit to 1- or 3-year terms for 30-60% savings.
- Use Spot Instances for fault-tolerant workloads — Batch jobs, CI/CD runners, stateless web workers — anything that can handle interruption — should run on Spot for up to 90% discount.
- Schedule non-production instances — Use AWS Instance Scheduler to automatically stop dev/test instances during nights and weekends.
- Delete unused EBS snapshots and volumes — Orphaned resources accumulate cost silently. Implement lifecycle policies.
4. Monitoring and Observability
- Install the CloudWatch agent — Default EC2 metrics only include CPU, disk, and network at 5-minute granularity. The agent adds memory, disk utilization, and custom metrics at 1-minute granularity.
- Set up CloudWatch Alarms — Create alarms for CPU > 80% for 5 minutes, memory > 85%, disk utilization > 90%, and status check failures. Route these to SNS for notification.
- Centralize logs — Send application logs, system logs, and audit logs to CloudWatch Logs (or a centralized logging solution). Never rely on SSH-ing into instances to tail logs.
- Track instance lifecycle events — Use AWS Health Dashboard and EC2 reboot/retirement notifications to proactively handle hardware degradation events.
5. Infrastructure as Code
- Never manually create production resources — Use Terraform, CloudFormation, or CDK for all EC2 infrastructure. Manual clicks in the Console are unrepeatable and error-prone.
- Version control everything — AMI build scripts, User Data scripts, CloudFormation templates — all should live in Git with peer-reviewed changes.
- Bake AMIs for faster bootstrapping — Instead of installing packages via User Data on every launch, pre-bake AMIs with your application stack using tools like Packer. This reduces boot time from minutes to seconds.
Advanced Configuration: Auto Scaling Group Setup
Individual instances are fragile. For production, wrap your instances in an Auto Scaling group (ASG) that automatically replaces failed instances and scales based on demand:
# Create a launch template (modern replacement for launch configurations)
aws ec2 create-launch-template \
--launch-template-name webapp-launch-template \
--version-description "v1 - Amazon Linux 2023 with Node.js" \
--launch-template-data '{
"ImageId": "'"$AMI_ID"'",
"InstanceType": "t3.micro",
"KeyName": "my-app-keypair",
"SecurityGroupIds": ["'"$SG_ID"'"],
"IamInstanceProfile": {"Name": "ec2-webapp-profile"},
"UserData": "'"$(base64 -w0 user-data.sh)"'",
"BlockDeviceMappings": [{
"DeviceName": "/dev/xvda",
"Ebs": {
"VolumeSize": 20,
"VolumeType": "gp3",
"Encrypted": true,
"DeleteOnTermination": true
}
}],
"MetadataOptions": {
"HttpTokens": "required",
"HttpPutResponseHopLimit": 2
},
"TagSpecifications": [{
"ResourceType": "instance",
"Tags": [{"Key": "Name", "Value": "webapp-asg-instance"}]
}]
}'
# Create the Auto Scaling group
aws autoscaling create-auto-scaling-group \
--auto-scaling-group-name webapp-asg \
--launch-template LaunchTemplateName=webapp-launch-template,Version='$Latest' \
--min-size 2 \
--max-size 10 \
--desired-capacity 2 \
--vpc-zone-identifier "subnet-aaaa,subnet-bbbb,subnet-cccc" \
--health-check-type ELB \
--health-check-grace-period 300 \
--tags "Key=Name,Value=webapp-asg,PropagateAtLaunch=true" \
--default-cooldown 300
# Attach the ASG to a target group (assuming ALB and target group exist)
aws autoscaling attach-load-balancer-target-groups \
--auto-scaling-group-name webapp-asg \
--target-group-arns arn:aws:elasticloadbalancing:region:account:targetgroup/webapp-tg/xxxxxxxx
# Create scaling policies
aws autoscaling put-scaling-policy \
--auto-scaling-group-name webapp-asg \
--policy-name scale-out-cpu \
--policy-type TargetTrackingScaling \
--target-tracking-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ASGAverageCPUUtilization"
}
}'
With this configuration, the ASG maintains at least 2 healthy instances, scales up to 10 when CPU averages exceed 70%, and automatically replaces any instance that fails health checks. The launch template ensures every new instance is identically configured.
Cleaning Up Resources
When experimenting, remember to clean up to avoid unexpected charges:
# Terminate the instance
aws ec2 terminate-instances --instance-ids $INSTANCE_ID
# Delete the security group (after removing all instances using it)
aws ec2 delete-security-group --group-id $SG_ID
# Delete the key pair
aws ec2 delete-key-pair --key-name my-app-keypair
rm -f my-app-keypair.pem
# Release Elastic IP if you allocated one
aws ec2 release-address --allocation-id eipalloc-xxxxxxxxxxxxx
# Delete Auto Scaling group and launch template
aws autoscaling delete-auto-scaling-group \
--auto-scaling-group-name webapp-asg \
--force-delete
aws ec2 delete-launch-template \
--launch-template-name webapp-launch-template
# Delete IAM resources
aws iam remove-role-from-instance-profile \
--instance-profile