Back

4_scaling_deploy

#!/usr/bin/env bash
# ================================================================
#  SCALABLE WEB CLUSTER — DEPLOY
#  Configure Auto Scaling Group + Application Load Balancer
#
#  AMI    : Latest Ubuntu Server LTS
#  TYPE   : t3.micro (Free Tier eligible)
#  USER   : ubuntu
#  RUN    : chmod +x 4_scaling_deploy.sh && ./4_scaling_deploy.sh
#  DELETE : ./4_scaling_cleanup.sh
#  STATE  : $HOME/aws_state_scaling.txt
# ================================================================
set -euo pipefail

REGION="ap-south-1"
AMI_ID=$(aws ec2 describe-images --owners 099720109477 \
  --filters "Name=name,Values=ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*" \
  --region "$REGION" --query "sort_by(Images, &CreationDate)[-1].ImageId" --output text)
INSTANCE_TYPE="t3.micro"
KEY_NAME="Practical-KeyPair"
STATE_FILE="$HOME/aws_state_scaling.txt"

# Resource Names
SG_NAME="Practical-Web-SG"
LT_NAME="Practical-Web-LT"
TG_NAME="Practical-Web-TG"
ALB_NAME="Practical-Web-ALB"
ASG_NAME="Practical-Web-ASG"

G='\033[0;32m'; B='\033[0;34m'; Y='\033[1;33m'; NC='\033[0m'
info()    { echo -e "${B}[INFO]${NC}  $*"; }
success() { echo -e "${G}[OK]${NC}    $*"; }
banner()  { echo -e "\n${Y}══════════════════════════════════════════${NC}"; \
            echo -e "${Y}  $*${NC}"; \
            echo -e "${Y}══════════════════════════════════════════${NC}\n"; }

banner "SCALABLE WEB CLUSTER — ASG + ALB"
echo "  AMI: $AMI_ID (Ubuntu 24.04 LTS) | Type: $INSTANCE_TYPE | Region: $REGION"

# ── Step 1: Key Pair ─────────────────────────────────────────
info "Step 1 — Key Pair: $KEY_NAME"
if aws ec2 describe-key-pairs --key-names "$KEY_NAME" --region "$REGION" >/dev/null 2>&1; then
  info "  Key Pair '$KEY_NAME' already exists. Using it."
else
  info "  Creating Key Pair: $KEY_NAME"
  aws ec2 create-key-pair --key-name "$KEY_NAME" --region "$REGION" \
    --query "KeyMaterial" --output text > "$HOME/${KEY_NAME}.pem"
  chmod 400 "$HOME/${KEY_NAME}.pem"
  success "Key Pair created → $HOME/${KEY_NAME}.pem"
fi

# ── Step 2: Networking (Independence & "From Scratch" logic) ───
info "Step 2 — Networking: VPC & Dual-AZ Subnets (Required for ALB)"
# Try to find Practical-VPC first, fallback to Default
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=Practical-VPC" --query "Vpcs[0].VpcId" --output text --region "$REGION" 2>/dev/null || echo "None")
if [[ "$VPC_ID" == "None" || "$VPC_ID" == "null" ]]; then
  VPC_ID=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" --region "$REGION" --query "Vpcs[0].VpcId" --output text 2>/dev/null || echo "None")
fi

if [[ "$VPC_ID" == "None" || "$VPC_ID" == "null" ]]; then
  info "  No suitable VPC found. Creating a dedicated 'Practical-Scaling-VPC'..."
  VPC_ID=$(aws ec2 create-vpc --cidr-block "10.40.0.0/16" --region "$REGION" --query "Vpc.VpcId" --output text)
  aws ec2 create-tags --resources "$VPC_ID" --region "$REGION" --tags Key=Name,Value="Practical-Scaling-VPC"
  aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-hostnames --region "$REGION"
  aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-support --region "$REGION"
  # IGW for public access
  IGW_ID=$(aws ec2 create-internet-gateway --region "$REGION" --query "InternetGateway.InternetGatewayId" --output text)
  aws ec2 attach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" --region "$REGION"
  # Route Table
  RT_ID=$(aws ec2 create-route-table --vpc-id "$VPC_ID" --region "$REGION" --query "RouteTable.RouteTableId" --output text)
  aws ec2 create-route --route-table-id "$RT_ID" --destination-cidr-block "0.0.0.0/0" --gateway-id "$IGW_ID" --region "$REGION" > /dev/null
fi

# Multi-AZ Subnets setup
AZS=$(aws ec2 describe-availability-zones --region "$REGION" --query "AvailabilityZones[0:2].ZoneName" --output text)
AZ1=$(echo "$AZS" | awk '{print $1}')
AZ2=$(echo "$AZS" | awk '{print $2}')

SUBNET_1=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" "Name=availability-zone,Values=$AZ1" --region "$REGION" --query "Subnets[0].SubnetId" --output text 2>/dev/null || echo "None")
if [[ "$SUBNET_1" == "None" || "$SUBNET_1" == "null" ]]; then
  info "  Creating subnet in $AZ1..."
  SUBNET_1=$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block "10.40.1.0/24" --availability-zone "$AZ1" --region "$REGION" --query "Subnet.SubnetId" --output text 2>/dev/null || \
             aws ec2 create-default-subnet --availability-zone "$AZ1" --region "$REGION" --query "Subnet.SubnetId" --output text)
fi

SUBNET_2=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" "Name=availability-zone,Values=$AZ2" --region "$REGION" --query "Subnets[0].SubnetId" --output text 2>/dev/null || echo "None")
if [[ "$SUBNET_2" == "None" || "$SUBNET_2" == "null" ]]; then
  info "  Creating subnet in $AZ2..."
  SUBNET_2=$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block "10.40.2.0/24" --availability-zone "$AZ2" --region "$REGION" --query "Subnet.SubnetId" --output text 2>/dev/null || \
             aws ec2 create-default-subnet --availability-zone "$AZ2" --region "$REGION" --query "Subnet.SubnetId" --output text)
fi

SUBNETS_CSV="$SUBNET_1,$SUBNET_2"
success "VPC: $VPC_ID | Subnets: $SUBNETS_CSV ($AZ1, $AZ2)"

# ── Step 3: Security Group ────────────────────────────────────
info "Step 3 — Security Group: $SG_NAME"
SG_ID=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=$SG_NAME" --region "$REGION" \
  --query "SecurityGroups[0].GroupId" --output text 2>/dev/null || echo "None")

if [[ "$SG_ID" == "None" || "$SG_ID" == "null" ]]; then
  info "  Creating Security Group..."
  SG_ID=$(aws ec2 create-security-group \
    --group-name "$SG_NAME" \
    --description "Web Security Group" \
    --vpc-id "$VPC_ID" \
    --region "$REGION" \
    --query "GroupId" --output text)
  aws ec2 create-tags --resources "$SG_ID" --region "$REGION" \
    --tags Key=Name,Value="$SG_NAME"
  success "Security Group created: $SG_ID"
else
  info "  Security Group already exists ($SG_ID)."
fi

info "  Adding inbound rules (80, 22)..."
aws ec2 authorize-security-group-ingress --group-id "$SG_ID" --protocol tcp --port 80 --cidr 0.0.0.0/0 --region "$REGION" 2>/dev/null || true
aws ec2 authorize-security-group-ingress --group-id "$SG_ID" --protocol tcp --port 22 --cidr 0.0.0.0/0 --region "$REGION" 2>/dev/null || true
success "Security Group rules checked."

# ── Step 4: Launch Template ──────────────────────────────────
info "Step 4 — Launch Template: $LT_NAME"
USER_DATA=$(base64 -w 0 <<'SCRIPT'
#!/bin/bash
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get install -y apache2
systemctl start apache2
systemctl enable apache2

# Wait for Metadata Service (IMDSv2)
TOKEN="None"
until [[ "$TOKEN" != "None" && -n "$TOKEN" ]]; do
  TOKEN=$(curl -s -f -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
  sleep 2
done

INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
AZ=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/availability-zone)

cat > /var/www/html/index.html <<HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CloudStack | High-Availability Cluster</title>
    <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
    <style>
        :root { --primary: #0061ff; --secondary: #60efff; --dark: #0f172a; }
        * { margin:0; padding:0; box-sizing:border-box; }
        body { font-family: 'Outfit', sans-serif; background: #f8fafc; color: var(--dark); line-height: 1.6; }
        .hero { background: linear-gradient(135deg, var(--dark) 0%, #1e293b 100%); color: white; padding: 100px 20px; text-align: center; position: relative; overflow: hidden; }
        .hero::after { content: ''; position: absolute; top:0; left:0; right:0; bottom:0; background: url('https://www.transparenttextures.com/patterns/cubes.png'); opacity: 0.1; }
        .container { max-width: 1200px; margin: 0 auto; position: relative; z-index: 1; }
        h1 { font-size: 3.5rem; font-weight: 600; margin-bottom: 20px; letter-spacing: -1px; background: linear-gradient(to right, #60efff, #0061ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
        .tagline { font-size: 1.25rem; color: #94a3b8; max-width: 700px; margin: 0 auto 40px; }
        .status-pill { display: inline-flex; align-items: center; background: rgba(16, 185, 129, 0.1); border: 1px solid #10b981; color: #10b981; padding: 6px 16px; border-radius: 999px; font-weight: 600; font-size: 0.875rem; margin-bottom: 20px; }
        .status-dot { width: 8px; height: 8px; background: #10b981; border-radius: 50%; margin-right: 8px; animation: pulse 2s infinite; }
        @keyframes pulse { 0% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(1.2); } 100% { opacity: 1; transform: scale(1); } }
        .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px; margin-top: -60px; padding: 0 20px; }
        .card { background: white; padding: 40px; border-radius: 20px; box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1); border: 1px solid #e2e8f0; transition: transform 0.3s ease; }
        .card:hover { transform: translateY(-10px); }
        .meta-label { font-size: 0.75rem; text-transform: uppercase; color: #64748b; font-weight: 600; margin-bottom: 8px; display: block; }
        .meta-value { font-size: 1.125rem; font-weight: 600; color: var(--dark); font-family: monospace; word-break: break-all; }
        .features { padding: 100px 20px; text-align: center; }
        .feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 40px; margin-top: 50px; }
        .feat h3 { margin-bottom: 10px; color: var(--primary); }
        footer { background: var(--dark); color: #64748b; padding: 40px 20px; text-align: center; font-size: 0.875rem; }
    </style>
</head>
<body>
    <section class="hero">
        <div class="container">
            <div class="status-pill">
                <span class="status-dot"></span> Cluster Operational
            </div>
            <h1>CloudStack High-Availability</h1>
            <p class="tagline">Enterprise-grade infrastructure automatically scaling to meet global demand. This request was served by a node in our distributed edge network.</p>
        </div>
    </section>

    <div class="container">
        <div class="grid">
            <div class="card">
                <span class="meta-label">Node Identifier</span>
                <span class="meta-value">$INSTANCE_ID</span>
                <p style="margin-top:20px; font-size:0.875rem; color:#64748b;">The unique ID of the virtual machine currently processing your request.</p>
            </div>
            <div class="card">
                <span class="meta-label">Availability Zone</span>
                <span class="meta-value">$AZ</span>
                <p style="margin-top:20px; font-size:0.875rem; color:#64748b;">The physical isolated partition within our regional data center.</p>
            </div>
        </div>
    </div>

    <section class="features container">
        <h2>Infrastructure Architecture</h2>
        <div class="feature-grid">
            <div class="feat">
                <h3>Auto Scaling</h3>
                <p>Dynamically provisions capacity based on real-time traffic spikes.</p>
            </div>
            <div class="feat">
                <h3>Load Balancing</h3>
                <p>Intelligent distribution across multi-AZ fault-tolerant zones.</p>
            </div>
            <div class="feat">
                <h3>Self-Healing</h3>
                <p>Automatic replacement of unhealthy nodes to maintain 99.99% uptime.</p>
            </div>
        </div>
    </section>

    <footer>
        &copy; 2026 CloudStack Pro. Powered by Amazon Web Services.
    </footer>
</body>
</html>
HTML
SCRIPT
)
USER_DATA_B64="$USER_DATA" # Renaming for clarity as per snippet, though it's already base64

LT_ID=$(aws ec2 describe-launch-templates --launch-template-names "$LT_NAME" --region "$REGION" \
  --query "LaunchTemplates[0].LaunchTemplateId" --output text 2>/dev/null || echo "NOT_FOUND")

if [[ "$LT_ID" == "NOT_FOUND" ]]; then
  info "  Creating Launch Template..."
  LT_ID=$(aws ec2 create-launch-template \
    --launch-template-name "$LT_NAME" \
    --version-description "v1" \
    --region "$REGION" \
    --launch-template-data "{
      \"NetworkInterfaces\": [{\"AssociatePublicIpAddress\": true, \"DeviceIndex\": 0, \"Groups\": [\"$SG_ID\"]}],
      \"ImageId\": \"$AMI_ID\",
      \"InstanceType\": \"$INSTANCE_TYPE\",
      \"KeyName\": \"$KEY_NAME\",
      \"UserData\": \"$USER_DATA_B64\",
      \"TagSpecifications\":[{
        \"ResourceType\":\"instance\",
        \"Tags\":[{\"Key\":\"Name\",\"Value\":\"Practical-Web-Instance\"}]
      }]}" \
    --query "LaunchTemplate.LaunchTemplateId" --output text)
  success "Launch Template created: $LT_ID ($LT_NAME)"
else
  info "  Launch Template already exists. Creating a new version with latest UserData..."
  aws ec2 create-launch-template-version \
    --launch-template-id "$LT_ID" \
    --version-description "Updated via script" \
    --region "$REGION" \
    --launch-template-data "{
      \"ImageId\": \"$AMI_ID\",
      \"InstanceType\": \"$INSTANCE_TYPE\",
      \"UserData\": \"$USER_DATA_B64\"
    }" > /dev/null
  success "Launch Template $LT_NAME updated to latest version."
fi

# ── Step 5: Target Group ──────────────────────────────────────
info "Step 5 — Target Group: $TG_NAME"
TG_ARN=$(aws elbv2 describe-target-groups --names "$TG_NAME" --region "$REGION" \
  --query "TargetGroups[0].TargetGroupArn" --output text 2>/dev/null || echo "None")

if [[ "$TG_ARN" == "None" || "$TG_ARN" == "null" ]]; then
  info "  Creating Target Group..."
  TG_ARN=$(aws elbv2 create-target-group \
    --name "$TG_NAME" \
    --protocol HTTP \
    --port 80 \
    --vpc-id "$VPC_ID" \
    --health-check-path "/" \
    --health-check-interval-seconds 30 \
    --healthy-threshold-count 2 \
    --unhealthy-threshold-count 3 \
    --target-type instance \
    --region "$REGION" \
    --query "TargetGroups[0].TargetGroupArn" --output text)
  success "Target Group created: $TG_ARN"
else
  info "  Target Group already exists ($TG_ARN). Using it."
fi

# ── Step 6: Application Load Balancer ─────────────────────────
info "Step 6 — Load Balancer: $ALB_NAME"
ALB_ARN=$(aws elbv2 describe-load-balancers --names "$ALB_NAME" --region "$REGION" \
  --query "LoadBalancers[0].LoadBalancerArn" --output text 2>/dev/null || echo "None")

if [[ "$ALB_ARN" == "None" || "$ALB_ARN" == "null" ]]; then
  info "  Creating ALB (External)..."
  ALB_ARN=$(aws elbv2 create-load-balancer \
    --name "$ALB_NAME" \
    --subnets "$SUBNET_1" "$SUBNET_2" \
    --security-groups "$SG_ID" \
    --scheme internet-facing \
    --type application \
    --region "$REGION" \
    --query "LoadBalancers[0].LoadBalancerArn" --output text)
  success "ALB created: $ALB_ARN"
else
  info "  ALB already exists ($ALB_ARN). Using it."
fi

ALB_DNS=$(aws elbv2 describe-load-balancers --load-balancer-arns "$ALB_ARN" --region "$REGION" \
  --query "LoadBalancers[0].DNSName" --output text)

info "  Setting up HTTP Listener..."
LISTENER_ARN=$(aws elbv2 describe-listeners --load-balancer-arn "$ALB_ARN" --region "$REGION" \
  --query "Listeners[?Port==\`80\`].ListenerArn" --output text 2>/dev/null || echo "None")

if [[ "$LISTENER_ARN" == "None" || "$LISTENER_ARN" == "null" || -z "$LISTENER_ARN" ]]; then
  info "  Creating HTTP Listener..."
  LISTENER_ARN=$(aws elbv2 create-listener \
    --load-balancer-arn "$ALB_ARN" \
    --protocol HTTP \
    --port 80 \
    --default-actions Type=forward,TargetGroupArn="$TG_ARN" \
    --region "$REGION" --query "Listeners[0].ListenerArn" --output text)
else
  info "  HTTP Listener already exists ($LISTENER_ARN). Using it."
fi
success "ALB Listener configured: $LISTENER_ARN"

# ── Step 7: Auto Scaling Group ───────────────────────────────
ASG_LENGTH=$(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names "$ASG_NAME" --region "$REGION" \
  --query "length(AutoScalingGroups)" --output text 2>/dev/null || echo "0")

if [[ "$ASG_LENGTH" == "0" ]]; then
  info "  Creating ASG (min=1, desired=2, max=4)..."
  aws autoscaling create-auto-scaling-group \
    --auto-scaling-group-name "$ASG_NAME" \
    --launch-template "LaunchTemplateId=$LT_ID,Version=\$Latest" \
    --min-size 1 --max-size 4 --desired-capacity 2 \
    --target-group-arns "$TG_ARN" \
    --vpc-zone-identifier "$SUBNETS_CSV" \
    --health-check-type ELB \
    --health-check-grace-period 300 \
    --region "$REGION" \
    --tags "Key=Name,Value=Practical-Web-Instance,PropagateAtLaunch=true"
  success "Auto Scaling Group '$ASG_NAME' created."
else
  info "  ASG already exists. Updating to use \$Latest Launch Template and 300s grace period..."
  aws autoscaling update-auto-scaling-group \
    --auto-scaling-group-name "$ASG_NAME" \
    --launch-template "LaunchTemplateId=$LT_ID,Version=\$Latest" \
    --health-check-grace-period 300 \
    --region "$REGION"
  success "Auto Scaling Group '$ASG_NAME' updated."
fi

# ── Step 8: Scaling Policy ───────────────────────────────────
info "Step 8 — Scaling Policy: CPU-Utilization-70"
aws autoscaling put-scaling-policy \
  --auto-scaling-group-name "$ASG_NAME" \
  --policy-name "ScalingPolicy-CPU" \
  --policy-type TargetTrackingScaling \
  --region "$REGION" \
  --target-tracking-configuration '{
    "PredefinedMetricSpecification":{"PredefinedMetricType":"ASGAverageCPUUtilization"},
    "TargetValue":70.0}' > /dev/null 2>&1 || true
success "CPU Scaling Policy created (target: 70% CPU)."

# ── Save State ────────────────────────────────────────────────
cat > "$STATE_FILE" <<EOF
REGION=$REGION
VPC_ID=$VPC_ID
SG_ID=$SG_ID
LT_ID=$LT_ID
TG_ARN=$TG_ARN
ALB_ARN=$ALB_ARN
LISTENER_ARN=$LISTENER_ARN
KEY_NAME=$KEY_NAME
ALB_DNS=$ALB_DNS
AMI_ID=$AMI_ID
EOF

# ── Step 9: Finalizing ───────────────────────────────────────
info "Step 9 — Deployment submitted. Custom enterprise site will be live shortly."

banner "SCALABLE WEB CLUSTER — COMPLETE SUMMARY"
cat <<SUMMARY
  ╔══════════════════════════════════════════════════════════════╗
  ║              RESOURCES DEPLOYED                              ║
  ╠═══════════════════════╦══════════════════════════════════════╣
  ║ Resource              ║ Value                                ║
  ╠═══════════════════════╬══════════════════════════════════════╣
  ║ Security Group        ║ $SG_ID                               ║
  ║ Launch Template       ║ $LT_ID                               ║
  ║ Target Group          ║ $TG_NAME                             ║
  ║ Load Balancer (ALB)   ║ $ALB_NAME                            ║
  ║ ALB DNS               ║ $ALB_DNS                             ║
  ║ ALB Listener          ║ HTTP:80 → Target Group               ║
  ║ Auto Scaling Group    ║ $ASG_NAME                            ║
  ║ Scaling Policy        ║ CPU > 70% → scale out                ║
  ║ Key Pair              ║ $KEY_NAME                            ║
  ║ AMI Used              ║ $AMI_ID                              ║
  ║ Region                ║ $REGION                              ║
  ╠═══════════════════════╩══════════════════════════════════════╣
  ║              DETAILS NEEDED TO CLEANUP                       ║
  ╠══════════════════════════════════════════════════════════════╣
  ║  ASG Name   : $ASG_NAME                            ║
  ║  ALB Name   : $ALB_NAME                            ║
  ║  TG Name    : $TG_NAME                             ║
  ║  LT ID      : $LT_ID                                         ║
  ║  SG ID      : $SG_ID                                         ║
  ║  State File : $STATE_FILE                                    ║ 
  ╠══════════════════════════════════════════════════════════════╣
  ║  Run: ./4_scaling_cleanup.sh (reads state file automatically)  ║
  ╚══════════════════════════════════════════════════════════════╝

  Test website: http://$ALB_DNS
  (Wait 2-3 min for health checks to pass, then refresh browser)

  AWS Console (Instances in ap-south-1):
  https://ap-south-1.console.aws.amazon.com/ec2/home?region=ap-south-1#Instances:search=Practical-Web-Instance

  Stress test (trigger scale-out):
  EC2 → connect to any WebCluster instance → EC2 Instance Connect
  sudo apt update && sudo apt install -y stress && stress --cpu 2 --timeout 300
SUMMARY