Auto Stop/Start EC2 Instances Using Schedule Tags with Python
Write a Python boto3 script that reads the AutoStop=true tag on EC2 instances and automatically stops them at 8 PM and starts them at 8 AM — with detailed explanation of every command.
Cost-saving automation for non-production EC2 instances (dev/test/staging) that don't need to run 24/7.
Problem Statement
Your team has 20 dev/staging EC2 instances that run 24/7 but are only used during business hours (8 AM – 8 PM). Each instance costs ~$0.10/hour. Running them overnight wastes $0.10 × 12 hours × 20 instances = $24/day — nearly $730/month in idle compute.
Goal: Write a Python script that:
- Finds all instances tagged
AutoStop=true - Stops them at 8 PM every day
- Starts them at 8 AM every day
- Logs all actions so you can audit what happened
Prerequisites Setup
Step 1 — Install dependencies
pip install boto3 schedule
| Package | Purpose |
|---|---|
boto3 | AWS SDK for Python — talks to EC2, S3, Lambda, etc. |
schedule | Lightweight job scheduler — runs functions at set times |
Step 2 — Configure AWS credentials
# Option A — AWS CLI (recommended for local use)
aws configure
# AWS Access Key ID: AKIA...
# AWS Secret Access Key: xxxxxxxx
# Default region: ap-south-1
# Default output format: json
# Option B — Environment variables (for CI/CD or Docker)
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=xxxxxxxx
export AWS_DEFAULT_REGION=ap-south-1
# Option C — IAM Role (best for EC2/Lambda — no credentials needed)
# Attach an IAM role with the right permissions to the instance running this script
Step 3 — Tag your EC2 instances
# Tag an instance via AWS CLI
aws ec2 create-tags \
--resources i-0abc123def456789 \
--tags Key=AutoStop,Value=true
# Or tag multiple instances at once
aws ec2 create-tags \
--resources i-0abc123def456789 i-0def456789abc123 \
--tags Key=AutoStop,Value=true
# Verify the tag
aws ec2 describe-tags \
--filters "Name=resource-id,Values=i-0abc123def456789"
Step 4 — Required IAM permissions
Create an IAM policy and attach it to the user or role running this script:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EC2ScheduleControl",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeTags",
"ec2:StartInstances",
"ec2:StopInstances"
],
"Resource": "*"
}
]
}
The Complete Script
# ec2_scheduler.py
"""
EC2 Auto Stop/Start Scheduler
Stops instances tagged AutoStop=true at 8 PM.
Starts them at 8 AM.
"""
import boto3
import schedule
import time
import logging
from datetime import datetime
from botocore.exceptions import ClientError, NoCredentialsError
# ── Logging setup ────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[
logging.StreamHandler(), # Print to console
logging.FileHandler("ec2_scheduler.log"), # Also write to file
],
)
log = logging.getLogger(__name__)
# ── Configuration ─────────────────────────────────────────────────
REGION = "ap-south-1" # Change to your AWS region
TAG_KEY = "AutoStop" # The tag key we look for
TAG_VALUE = "true" # The tag value we look for
STOP_TIME = "20:00" # 8 PM — 24-hour format
START_TIME = "08:00" # 8 AM — 24-hour format
# ── boto3 client ──────────────────────────────────────────────────
def get_ec2_client():
"""
Create an EC2 client for the specified region.
boto3.client() creates a low-level service client.
- 'ec2' : the AWS service name
- region_name: which AWS region to connect to
Credentials are picked up automatically from:
1. Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
2. ~/.aws/credentials file (set by `aws configure`)
3. IAM role attached to the EC2/Lambda running this script
"""
return boto3.client("ec2", region_name=REGION)
# ── Helper: find tagged instances ─────────────────────────────────
def get_tagged_instances(ec2, desired_state: str) -> list[dict]:
"""
Return EC2 instances that have AutoStop=true AND are in the desired_state.
Parameters
----------
ec2 : boto3 EC2 client
desired_state : 'running' to find instances to stop,
'stopped' to find instances to start
How describe_instances works
----------------------------
- Filters is a list of dicts with 'Name' and 'Values' keys.
- 'tag:AutoStop' matches the tag KEY named AutoStop.
- 'instance-state-name' filters by the current lifecycle state.
- The API returns a paginated response — Reservations is the top-level list.
Each Reservation can contain multiple Instances (from a single launch command).
"""
try:
response = ec2.describe_instances(
Filters=[
{
"Name": f"tag:{TAG_KEY}", # Filter by tag key
"Values": [TAG_VALUE], # Tag value must be 'true'
},
{
"Name": "instance-state-name",
"Values": [desired_state], # Only running or stopped
},
]
)
except NoCredentialsError:
log.error("AWS credentials not found. Run 'aws configure' or set env vars.")
return []
except ClientError as e:
log.error(f"AWS API error: {e.response['Error']['Message']}")
return []
# Flatten the nested Reservations → Instances structure into a flat list
instances = [
instance
for reservation in response["Reservations"]
for instance in reservation["Instances"]
]
return instances
# ── Helper: get a human-readable instance name ─────────────────────
def get_instance_name(instance: dict) -> str:
"""
Extract the 'Name' tag value from the instance's Tags list.
Tags is a list of {'Key': '...', 'Value': '...'} dicts.
We use next() with a default so it doesn't crash if 'Name' tag is absent.
"""
tags = instance.get("Tags", [])
name = next((t["Value"] for t in tags if t["Key"] == "Name"), "Unnamed")
return name
# ── STOP action ───────────────────────────────────────────────────
def stop_instances():
"""
Find all running instances tagged AutoStop=true and stop them.
Called automatically at STOP_TIME (8 PM).
"""
log.info("=" * 55)
log.info(f"[STOP JOB] Starting at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
ec2 = get_ec2_client()
instances = get_tagged_instances(ec2, desired_state="running")
if not instances:
log.info("[STOP JOB] No running instances with AutoStop=true found.")
return
# Extract just the instance IDs — that's what start/stop APIs need
instance_ids = [inst["InstanceId"] for inst in instances]
log.info(f"[STOP JOB] Found {len(instance_ids)} instance(s) to stop:")
for inst in instances:
log.info(f" - {inst['InstanceId']} ({get_instance_name(inst)})")
try:
"""
ec2.stop_instances() sends a stop signal to each instance.
- InstanceIds: list of instance ID strings (e.g. ['i-0abc...', 'i-0def...'])
- The API call is asynchronous — it returns immediately.
The instance transitions: running → stopping → stopped.
- DryRun=True can be used to test without actually stopping.
- StopInstances does NOT terminate (delete) the instance.
Data on the EBS root volume is preserved.
"""
response = ec2.stop_instances(InstanceIds=instance_ids)
# Log the new state reported by AWS for each instance
for item in response["StoppingInstances"]:
prev = item["PreviousState"]["Name"]
curr = item["CurrentState"]["Name"]
iid = item["InstanceId"]
log.info(f" ✓ {iid}: {prev} → {curr}")
log.info(f"[STOP JOB] Stop signal sent to {len(instance_ids)} instance(s).")
except ClientError as e:
error_code = e.response["Error"]["Code"]
error_msg = e.response["Error"]["Message"]
log.error(f"[STOP JOB] Failed to stop instances. Code: {error_code} — {error_msg}")
# Common errors:
# UnsupportedOperation: instance store-backed instances can't be stopped
# IncorrectInstanceState: instance is already stopping/terminated
# ── START action ──────────────────────────────────────────────────
def start_instances():
"""
Find all stopped instances tagged AutoStop=true and start them.
Called automatically at START_TIME (8 AM).
"""
log.info("=" * 55)
log.info(f"[START JOB] Starting at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
ec2 = get_ec2_client()
instances = get_tagged_instances(ec2, desired_state="stopped")
if not instances:
log.info("[START JOB] No stopped instances with AutoStop=true found.")
return
instance_ids = [inst["InstanceId"] for inst in instances]
log.info(f"[START JOB] Found {len(instance_ids)} instance(s) to start:")
for inst in instances:
log.info(f" - {inst['InstanceId']} ({get_instance_name(inst)})")
try:
"""
ec2.start_instances() boots stopped EBS-backed instances.
- InstanceIds: list of instance ID strings.
- The API call is asynchronous.
The instance transitions: stopped → pending → running.
- A new public IP is assigned (unless an Elastic IP is attached).
- Instance store data is NOT preserved across stop/start cycles.
EBS data IS preserved.
"""
response = ec2.start_instances(InstanceIds=instance_ids)
for item in response["StartingInstances"]:
prev = item["PreviousState"]["Name"]
curr = item["CurrentState"]["Name"]
iid = item["InstanceId"]
log.info(f" ✓ {iid}: {prev} → {curr}")
log.info(f"[START JOB] Start signal sent to {len(instance_ids)} instance(s).")
except ClientError as e:
error_code = e.response["Error"]["Code"]
error_msg = e.response["Error"]["Message"]
log.error(f"[START JOB] Failed to start instances. Code: {error_code} — {error_msg}")
# ── Scheduler setup ───────────────────────────────────────────────
def setup_schedule():
"""
Register stop and start jobs with the schedule library.
schedule.every().day.at("HH:MM") registers a recurring daily job.
- Times use 24-hour format.
- The scheduler runs in the LOCAL timezone of the machine.
If you need UTC or a specific timezone, use pytz:
import pytz
tz = pytz.timezone("Asia/Kolkata")
now = datetime.now(tz)
"""
schedule.every().day.at(STOP_TIME).do(stop_instances)
schedule.every().day.at(START_TIME).do(start_instances)
log.info(f"Scheduler active — Stop: {STOP_TIME} | Start: {START_TIME}")
log.info(f"Region: {REGION} | Tag filter: {TAG_KEY}={TAG_VALUE}")
log.info("Waiting for next scheduled job... (Ctrl+C to exit)")
# ── Entry point ───────────────────────────────────────────────────
if __name__ == "__main__":
setup_schedule()
"""
schedule.run_pending() checks if any registered job is due and runs it.
It does NOT block — it returns immediately if no job is due.
We wrap it in an infinite loop with time.sleep(60) so we check
every 60 seconds. This is lightweight (almost no CPU usage while sleeping).
The loop runs forever until:
- You press Ctrl+C (raises KeyboardInterrupt)
- The process is killed (systemd, Docker, etc.)
"""
try:
while True:
schedule.run_pending()
time.sleep(60) # Check every minute — fine-grained enough for HH:MM scheduling
except KeyboardInterrupt:
log.info("Scheduler stopped by user.")
Running the Script
Option A — Run directly (for testing)
# Run and watch the logs
python ec2_scheduler.py
# 2025-01-20 07:59:00 INFO Scheduler active — Stop: 20:00 | Start: 08:00
# 2025-01-20 07:59:00 INFO Region: ap-south-1 | Tag filter: AutoStop=true
# 2025-01-20 08:00:00 INFO ═══════════════════════════════════════════════════════
# 2025-01-20 08:00:00 INFO [START JOB] Starting at 2025-01-20 08:00:00
# 2025-01-20 08:00:00 INFO Found 3 instance(s) to start:
# 2025-01-20 08:00:00 INFO - i-0abc123 (dev-api-server)
# 2025-01-20 08:00:00 INFO - i-0def456 (staging-db)
# 2025-01-20 08:00:00 INFO - i-0ghi789 (test-worker)
# 2025-01-20 08:00:01 INFO ✓ i-0abc123: stopped → pending
# 2025-01-20 08:00:01 INFO ✓ i-0def456: stopped → pending
# 2025-01-20 08:00:01 INFO ✓ i-0ghi789: stopped → pending
Option B — Run as a Linux systemd service (production)
# /etc/systemd/system/ec2-scheduler.service
[Unit]
Description=EC2 Auto Stop/Start Scheduler
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/ec2-scheduler
ExecStart=/opt/ec2-scheduler/venv/bin/python ec2_scheduler.py
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable ec2-scheduler
sudo systemctl start ec2-scheduler
sudo systemctl status ec2-scheduler
journalctl -u ec2-scheduler -f # Follow live logs
Option C — Run as a cron job (alternative)
# Edit crontab
crontab -e
# Add these two lines:
# Stop at 8 PM (20:00) every day
0 20 * * * /usr/bin/python3 /opt/ec2-scheduler/ec2_scheduler_once.py stop >> /var/log/ec2-stop.log 2>&1
# Start at 8 AM (08:00) every day
0 8 * * * /usr/bin/python3 /opt/ec2-scheduler/ec2_scheduler_once.py start >> /var/log/ec2-start.log 2>&1
# ec2_scheduler_once.py — for cron (runs once, stops/starts, then exits)
import sys
from ec2_scheduler import start_instances, stop_instances
if __name__ == "__main__":
action = sys.argv[1] if len(sys.argv) > 1 else "stop"
if action == "stop":
stop_instances()
elif action == "start":
start_instances()
Option D — AWS Lambda + EventBridge (serverless, no server needed)
# lambda_handler.py — deploy this as a Lambda function
import boto3
from botocore.exceptions import ClientError
TAG_KEY = "AutoStop"
TAG_VALUE = "true"
REGION = "ap-south-1"
def lambda_handler(event, context):
"""
EventBridge triggers this Lambda on a cron schedule.
event['action'] is set by the EventBridge rule's Input field.
"""
action = event.get("action", "stop")
ec2 = boto3.client("ec2", region_name=REGION)
state = "running" if action == "stop" else "stopped"
response = ec2.describe_instances(
Filters=[
{"Name": f"tag:{TAG_KEY}", "Values": [TAG_VALUE]},
{"Name": "instance-state-name", "Values": [state]},
]
)
instance_ids = [
inst["InstanceId"]
for r in response["Reservations"]
for inst in r["Instances"]
]
if not instance_ids:
return {"status": "no instances found", "action": action}
if action == "stop":
ec2.stop_instances(InstanceIds=instance_ids)
else:
ec2.start_instances(InstanceIds=instance_ids)
return {
"status": "success",
"action": action,
"instances": instance_ids,
"count": len(instance_ids),
}
# EventBridge rules (AWS Console or CLI)
# Stop rule — every day at 8 PM UTC
aws events put-rule \
--name ec2-stop-rule \
--schedule-expression "cron(0 14 * * ? *)" \
--state ENABLED
# Start rule — every day at 8 AM UTC
aws events put-rule \
--name ec2-start-rule \
--schedule-expression "cron(0 2 * * ? *)" \
--state ENABLED
Key Commands Explained
| Command | What it does |
|---|---|
boto3.client("ec2", region_name=REGION) | Creates an EC2 API client for the specified region |
ec2.describe_instances(Filters=[...]) | Lists instances matching the tag + state filter |
response["Reservations"] | Top-level grouping returned by EC2 describe — each holds 1+ instances |
ec2.stop_instances(InstanceIds=[...]) | Sends stop signal; instance goes running → stopping → stopped |
ec2.start_instances(InstanceIds=[...]) | Sends start signal; instance goes stopped → pending → running |
schedule.every().day.at("20:00").do(fn) | Registers fn to run daily at 20:00 in local time |
schedule.run_pending() | Executes any jobs that are due — called in the event loop |
item["PreviousState"]["Name"] | Reports what the instance state was before the API call |
item["CurrentState"]["Name"] | Reports the transitional state immediately after the API call |
Cost Savings Estimate
| Scenario | Instances | Hours saved/day | Cost/hr | Daily saving | Monthly saving |
|---|---|---|---|---|---|
| Small team | 5 | 12 | $0.10 | $6 | ~$180 |
| Medium team | 20 | 12 | $0.10 | $24 | ~$720 |
| Large team | 50 | 12 | $0.10 | $60 | ~$1,800 |
The script pays for itself (Lambda cost: ~$0/month on free tier) the first day it runs.
Common Issues & Fixes
NoCredentialsError — AWS credentials not configured. Run aws configure or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY environment variables.
UnauthorizedOperation — The IAM user/role doesn’t have ec2:StopInstances or ec2:StartInstances permission. Attach the policy from Step 4.
UnsupportedOperation: You may not stop instance-store instances — Instance-store backed AMIs cannot be stopped, only terminated. Only EBS-backed instances support stop/start.
Instances not found — Double-check the tag spelling: AutoStop (capital A and S) with value true (lowercase). Tags are case-sensitive in AWS.
Wrong timezone — schedule uses the machine’s local time. If your server is in UTC and you want 8 PM IST (UTC+5:30), set STOP_TIME = "14:30" (20:00 − 5:30 = 14:30 UTC).
🔍 Line-by-Line Code Walkthrough
Imports
| Line | Why It’s Used & How It Works |
|---|---|
import boto3 | AWS SDK for Python. Every AWS API call goes through this library. Without it you cannot talk to EC2. Install: pip install boto3 |
import schedule | Lightweight job scheduler. Lets you call a function at “20:00 every day”. Install: pip install schedule |
import time | Python standard library. We use time.sleep(60) to pause the event loop for 60 seconds between scheduler checks |
import logging | Standard library for writing structured log output to both console and file |
from datetime import datetime | Used to format the current timestamp inside log messages (e.g., "2025-01-20 08:00:00") |
from botocore.exceptions import ClientError, NoCredentialsError | AWS SDK error classes. ClientError covers all AWS API errors (wrong permissions, wrong state, etc.). NoCredentialsError fires when no AWS credentials are found anywhere |
Configuration Constants
| Line | What It Does & Why |
|---|---|
REGION = "ap-south-1" | Which AWS region to call. EC2 is regional — instances in ap-south-1 can only be managed through the ap-south-1 endpoint |
TAG_KEY = "AutoStop" | The EC2 tag key we search for. Stored as a constant so changing the tag policy means changing one line |
TAG_VALUE = "true" | The expected tag value. Only instances tagged AutoStop=true are touched — anything else is left alone |
STOP_TIME = "20:00" | 8 PM in 24-hour format. The schedule library parses this string directly |
START_TIME = "08:00" | 8 AM in 24-hour format |
logging.basicConfig(...)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[
logging.StreamHandler(),
logging.FileHandler("ec2_scheduler.log"),
],
)
| Line | Explanation |
|---|---|
level=logging.INFO | Sets the minimum severity level to log. INFO logs everything except DEBUG. Set to logging.DEBUG when troubleshooting |
format="%(asctime)s %(levelname)-8s %(message)s" | Log line template. %(asctime)s = timestamp, %(levelname)-8s = level padded to 8 chars, %(message)s = the log text |
datefmt="%Y-%m-%d %H:%M:%S" | Timestamp format. Without this, the time includes milliseconds |
logging.StreamHandler() | Sends log lines to stdout (the terminal). Required to see logs when running interactively or in Docker |
logging.FileHandler("ec2_scheduler.log") | Also writes log lines to a file on disk. Both handlers run together — every log line goes to both |
get_ec2_client()
def get_ec2_client():
return boto3.client("ec2", region_name=REGION)
| Line | Explanation |
|---|---|
boto3.client("ec2") | Creates a low-level EC2 client. “ec2” is the AWS service name. The client exposes individual API methods like describe_instances, stop_instances, etc. |
region_name=REGION | Every boto3 EC2 client is region-scoped. Instances in ap-south-1 will not be visible to a client pointed at us-east-1 |
| (credentials auto-discovered) | boto3 finds credentials automatically in this order: environment variables → ~/.aws/credentials file → IAM role attached to the current EC2 instance or Lambda |
get_tagged_instances(ec2, desired_state)
response = ec2.describe_instances(
Filters=[
{"Name": f"tag:{TAG_KEY}", "Values": [TAG_VALUE]},
{"Name": "instance-state-name", "Values": [desired_state]},
]
)
| Line | Explanation |
|---|---|
ec2.describe_instances(Filters=[...]) | Calls the EC2 DescribeInstances API. Without filters it returns ALL instances — the Filters list narrows results on the server side (faster, cheaper) |
{"Name": f"tag:{TAG_KEY}", "Values": [TAG_VALUE]} | tag:AutoStop is a special filter key. The tag: prefix tells EC2 to filter by tag key name. Values: ["true"] — only instances where AutoStop tag equals “true” |
{"Name": "instance-state-name", "Values": [desired_state]} | Filters by lifecycle state. "running" to find instances to stop; "stopped" to find instances to start. Other states: pending, stopping, terminated |
response["Reservations"] | EC2 groups instances into Reservations (a Reservation = one launch command). Each Reservation has an "Instances" list |
for reservation in response["Reservations"]: for instance in reservation["Instances"] | Double loop to flatten: Reservations → Instances → flat list |
get_instance_name(instance)
tags = instance.get("Tags", [])
name = next((t["Value"] for t in tags if t["Key"] == "Name"), "Unnamed")
| Line | Explanation |
|---|---|
instance.get("Tags", []) | Gets the Tags list. .get(key, default) returns the default if the key is missing — instances with no tags would otherwise raise KeyError |
next((...), "Unnamed") | next() returns the first item from the generator expression. The second argument is the default if the generator yields nothing (i.e., no “Name” tag found) |
t["Key"] == "Name" | Tags are stored as a list of {"Key": "...", "Value": "..."} dicts — not as a dict. We must search linearly |
stop_instances() — Core Stop Logic
response = ec2.stop_instances(InstanceIds=instance_ids)
for item in response["StoppingInstances"]:
prev = item["PreviousState"]["Name"]
curr = item["CurrentState"]["Name"]
| Line | Explanation |
|---|---|
ec2.stop_instances(InstanceIds=instance_ids) | Sends a graceful stop signal to all instances in the list. The OS is signalled to shut down cleanly (like pressing the power button). Data on EBS volumes is preserved |
InstanceIds=instance_ids | Must be a list of instance ID strings: ["i-0abc123", "i-0def456"]. Stops all of them in one API call — more efficient than a loop |
response["StoppingInstances"] | Each element is a dict with InstanceId, PreviousState, and CurrentState |
item["PreviousState"]["Name"] | State before the API call. Will be "running" |
item["CurrentState"]["Name"] | State immediately after. Will be "stopping" — the instance is not yet stopped at this point (stop is async) |
except ClientError as e: | Catches AWS API errors. e.response["Error"]["Code"] contains the error type string |
start_instances() — Core Start Logic
response = ec2.start_instances(InstanceIds=instance_ids)
for item in response["StartingInstances"]:
prev = item["PreviousState"]["Name"] # "stopped"
curr = item["CurrentState"]["Name"] # "pending"
| Line | Explanation |
|---|---|
ec2.start_instances(InstanceIds=instance_ids) | Sends a boot signal to all stopped instances. The instance powers on, the OS boots, and services start |
response["StartingInstances"] | List of status objects — same structure as StoppingInstances |
"pending" (CurrentState) | The instance is transitioning to running. It takes ~30–90 seconds to become fully reachable |
| Instance gets a new public IP | Unless an Elastic IP is attached, the public IP changes on every stop/start cycle |
Scheduler Setup
schedule.every().day.at(STOP_TIME).do(stop_instances)
schedule.every().day.at(START_TIME).do(start_instances)
| Line | Explanation |
|---|---|
schedule.every() | Returns a Job object. The chain of calls builds the schedule rule |
.day | Specifies the interval unit: “run every 1 day” |
.at("20:00") | Sets the exact time within the day. Uses the local machine timezone — important if your server is in UTC but you want a different timezone |
.do(stop_instances) | Registers stop_instances (without calling it) as the function to execute when the time arrives. Note: no parentheses — we pass the function object, not its return value |
Event Loop
while True:
schedule.run_pending()
time.sleep(60)
| Line | Explanation |
|---|---|
while True: | Infinite loop — keeps the script running forever until you press Ctrl+C or kill the process |
schedule.run_pending() | Checks if any registered job is due RIGHT NOW and runs it. Returns immediately if nothing is due. Does NOT block |
time.sleep(60) | Pauses the loop for 60 seconds. Since our schedule uses HH:MM precision, checking every 60 seconds is more than fine. This keeps CPU usage near zero |
except KeyboardInterrupt: | Catches Ctrl+C gracefully — prints a shutdown message instead of an ugly traceback |
- boto3 EC2 client
- Tag-based filtering with Filters
- Instance lifecycle (stop/start)
- Scheduling with Python schedule library
- Error handling for AWS API calls
Have a similar scenario to share?
Production incidents are the best teachers. Submit your real-world scenario and help others learn.
Open Google FormRelated Scenarios
Clean Up Unused AWS Resources — EBS Volumes, EIPs, Old AMIs with Cost Report
Resource Cost Overview Resource Approx. Cost When it wastes money EBS gp3 volume $0.08/GB/month When not attached to any instance Elastic IP …
Create CloudWatch Alarms for All EC2 Instances (CPU, Memory, Disk)
Problem Statement Your team gets paged at 3 AM when an EC2 instance runs out of disk space — but only after the application has already …
EBS Snapshot Manager — Auto Backup & Retention Cleanup
Problem Statement Your team’s EC2 instances hold critical application data on EBS volumes. Without automated snapshots, a failed …