Automate EC2 with Python and Boto3
Script EC2 launches, fleet reports, and backups using Python and Boto3.
Introduction
30 Second Summary
Every company running on AWS has servers that need to be launched, monitored, and shut down. Doing this by clicking through the console works for one or two servers, but it falls apart when you are managing a fleet.
In this project, you will write Python scripts that automate EC2 instance management using Boto3, the AWS SDK for Python. You will launch instances, generate fleet reports, batch-stop servers by tag, and create backups from your terminal.
What You'll Build
You run a single Python script and watch it launch three tagged servers, print a clean status report of your fleet, stop an entire environment with one command, and snapshot your volumes for backup.
By the end of this project, you'll have:
- A launch script that creates three tagged t2.micro instances (web, database, dev) in one command.
- A formatted fleet report that lists every instance with its ID, type, state, and tags printed in a clean table.
- Tag-based batch operations that stop or start groups of instances by their Environment tag.
- An EBS snapshot function that backs up volumes for running instances.
- Secret Mission: A security group audit that flags any rule open to the entire internet.
Are there any prerequisites?
This project assumes you have completed the "Python Fundamentals for Cloud Engineers" project, which sets up Python and VS Code on your machine. You also need an existing AWS account.
Before We Start
Before you start installing tools and writing code, take a moment to lock in what you are building and why it matters.
Install AWS CLI and Boto3
You have Python and VS Code ready from the previous project. Your Python cannot talk to AWS yet, though.
The AWS CLI is a command-line tool that stores your AWS credentials on your machine. Boto3 is a Python library that reads those credentials and uses them to call AWS APIs on your behalf. In this step, you will install both.
In this step, get ready to:
- Install and configure the AWS CLI with your credentials.
- Install the Boto3 Python library.
- Create your project folder and verify everything works together.
Install the AWS CLI
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
- Install the AWS CLI by running this command:
irm https://awscli.amazonaws.com/v2/install.ps1 | iex
What does this command do?
irm downloads the official AWS CLI install script from Amazon's servers. The | iex part tells PowerShell to immediately run the downloaded script.
This installs the AWS CLI for your current user. No administrator privileges are needed.
- Wait for the installer to finish. You will see progress messages in your terminal.
- Close your PowerShell window completely.
- Reopen PowerShell by pressing the Windows key, typing PowerShell, and pressing Enter.
Why close and reopen?
The installer adds the aws command to your system path. Your current terminal session loaded the old path when it opened. Closing and reopening forces PowerShell to pick up the new path.
- Verify the install worked by running this command:
aws --version
✔️ I see a version number
The output shows something like aws-cli/2.x.x. Your AWS CLI is installed and ready for the next substep.
ⓧ Command not found
PowerShell does not recognize the aws command yet. Try these fixes:
- Make sure you closed and reopened PowerShell after the install finished.
- Run the install command again. If it was interrupted, the CLI may not have been fully installed.
- Restart your computer, then reopen PowerShell and try aws --version again.
Configure the AWS CLI with your credentials
Boto3 reads credentials that the AWS CLI stores on your machine. You need to run the configuration command once so those credentials are available to every Python script you write.
- The aws configure command asks for four values one at a time. Run this command to start the configuration:
aws configure
- When prompted for AWS Access Key ID, paste your access key.
- When prompted for AWS Secret Access Key, paste your secret access key.
- When prompted for Default region name, enter us-east-1.
- When prompted for Default output format, enter json.
Where to find your access keys
You created your AWS account in a previous project. To find or create access keys, sign in to the AWS console, click your username in the top-right corner, and select Security credentials. Scroll down to the Access keys section.
If you do not have an access key yet, click Create access key. Copy both the Access Key ID and Secret Access Key immediately. You will not be able to see the secret key again after closing the dialog.
What are these credentials for?
The AWS CLI stores your Access Key ID and Secret Access Key in a file on your machine. Every time Boto3 makes an API call to AWS, it reads this file to prove you are authorized. You never have to paste your credentials into your Python scripts.
Getting an error during configuration?
Double-check that you pasted the full access key without any extra spaces. If you see a message about invalid credentials later, run aws configure again and re-enter all four values.
help me fix my aws configure setup
Install Boto3 and create the project folder
The AWS CLI is configured. Now you need Boto3, the Python library that translates your Python function calls into AWS API requests.
- Install Boto3 by running this command:
pip install boto3
What is Boto3?
Boto3 is the official AWS SDK for Python. When you call a Boto3 method like run_instances(), Boto3 translates that into an HTTPS request to the EC2 API, signs it with your credentials, and returns the response as a Python dictionary.
Seeing pip is not recognized?
If PowerShell cannot find pip, try running python -m pip install boto3 instead. This uses Python to call pip directly.
help me fix pip not being recognized in my terminal
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir ec2-automation
cd ec2-automation
- Press the Windows key to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the ec2-automation folder, then click Select Folder.
VS Code opens with the ec2-automation folder name visible in the Explorer sidebar on the left.
- Open the VS Code terminal by pressing Ctrl+` (backtick key, usually above Tab).
Before you run the next command, what do you expect to see if Boto3 was installed correctly?
- Verify Boto3 is installed by running this command in the VS Code terminal:
python -c "import boto3; print(boto3.__version__)"
You should see a version number like 1.43.72 printed in the terminal. The exact number may differ, but any version number means Boto3 is installed and Python can import it.
Seeing a ModuleNotFoundError?
This means pip install boto3 installed Boto3 for a different Python installation than the one VS Code is using. Try running python -m pip install boto3 in the VS Code terminal, then run the verify command again.
help me fix ModuleNotFoundError for boto3 in VS Code
Your tools are locked in. Next up, you will write Python code that launches EC2 instances with a single command.
Launch Tagged EC2 Instances
Your Python environment is connected to AWS. Boto3 is installed, your credentials are configured, and your project folder is open in VS Code.
In this step, you'll write a script that launches three EC2 instances from your terminal. Each instance gets tags at launch time so you can filter and manage groups of instances in later steps.
In this step, get ready to:
- Write a function that finds the latest Amazon Linux AMI automatically.
- Launch three tagged t2.micro instances (web-server, db-server, dev-server) with a single script.
- Verify the instances are running in the AWS console.
Write the AMI lookup function
Before you can launch an instance, you need an AMI ID. AMI IDs change by region and over time, so hardcoding one would break for anyone in a different region.
- In VS Code, create a new file called ec2_launcher.py inside your ec2-automation folder.
- Add the imports and the AMI lookup function by copying this code:
import boto3
import json
def get_amazon_linux_ami(ec2_client):
"""Find the latest free-tier-eligible Amazon Linux 2023 AMI."""
response = ec2_client.describe_images(
Owners=["amazon"],
Filters=[
{"Name": "name", "Values": ["al2023-ami-2023.*-x86_64"]},
{"Name": "state", "Values": ["available"]},
{"Name": "architecture", "Values": ["x86_64"]},
],
)
images = sorted(response["Images"], key=lambda x: x["CreationDate"], reverse=True)
return images[0]["ImageId"]
What does this code do?
- describe_images searches AWS for AMIs matching your filters. The filters narrow results to Amazon Linux 2023, x86_64 architecture, and available state.
- The sorted() call orders the results by creation date so you always get the latest AMI.
- The function returns just the ImageId string, which is the ID you pass to the launch call.
- Save ec2_launcher.py.
- Verify the function loads without syntax errors by running this command in your terminal:
python -c "from ec2_launcher import get_amazon_linux_ami; print('AMI function loaded')"
You should see AMI function loaded printed in the terminal.
Seeing a SyntaxError or ModuleNotFoundError?
- Make sure your terminal is inside the ec2-automation folder. Run cd ~/Desktop/ec2-automation if needed.
- Check that every bracket and quote in your code is properly closed. The nested filter dictionaries are easy to mistype.
Help me fix a SyntaxError in my ec2_launcher.py AMI lookup function.
Add the launch function
The AMI lookup handles finding the right image. Now you need a function that actually launches an instance with that AMI and applies tags to it.
- In ec2_launcher.py, add the launch_instance function below your get_amazon_linux_ami function:
def launch_instance(ec2_client, ami_id, name, environment):
"""Launch a single t2.micro instance with Name and Environment tags."""
response = ec2_client.run_instances(
ImageId=ami_id,
InstanceType="t2.micro",
MinCount=1,
MaxCount=1,
TagSpecifications=[
{
"ResourceType": "instance",
"Tags": [
{"Key": "Name", "Value": name},
{"Key": "Environment", "Value": environment},
{"Key": "Project", "Value": "ec2-automation"},
],
}
],
)
instance_id = response["Instances"][0]["InstanceId"]
state = response["Instances"][0]["State"]["Name"]
print(f"Launched {name} ({environment}): {instance_id} [{state}]")
return instance_id
What does this code do?
- run_instances is the Boto3 call that launches an EC2 instance. MinCount and MaxCount are both 1 because you want exactly one instance per call.
- TagSpecifications applies tags at launch time. Each instance gets three tags: Name (what the instance is), Environment (Prod or Dev), and Project (always ec2-automation so you can find and clean up everything later).
- The response contains an Instances list. You grab the first (and only) instance's ID and state to print a confirmation line.
What is t2.micro?
t2.micro is a small instance type with 1 vCPU and 1 GiB of RAM. For AWS accounts created before July 15, 2025, t2.micro is free-tier eligible for 750 hours per month.
For accounts created on or after July 15, 2025, t3.micro is the free-tier option instead. Either way, you will terminate all instances at the end of this project, so costs stay at $0.
Launch three instances and verify in the console
Your two functions are ready. Now you need a main() function that ties them together: look up the AMI, then call launch_instance three times with different tags.
- Add the main() function at the bottom of ec2_launcher.py:
def main():
ec2 = boto3.client("ec2")
print("Finding latest Amazon Linux AMI...")
ami_id = get_amazon_linux_ami(ec2)
print(f"Using AMI: {ami_id}")
print("\nLaunching instances...")
launch_instance(ec2, ami_id, "web-server", "Prod")
launch_instance(ec2, ami_id, "db-server", "Prod")
launch_instance(ec2, ami_id, "dev-server", "Dev")
print("\nAll instances launched. Run ec2_manager.py report to see your fleet.")
main()
What does this code do?
- boto3.client("ec2") creates an EC2 client that uses the credentials you configured with aws configure in the previous step.
- The three launch_instance calls create web-server (Prod), db-server (Prod), and dev-server (Dev). Each gets its own Name and Environment tag.
- main() is called at the bottom of the file so the script runs when you execute it from the terminal.
- Save ec2_launcher.py.
Before you run this, what do you think will happen? Will you see all three instance IDs printed immediately, or will the script take time to reach AWS and get a response?
- Launch your instances by running this command:
python ec2_launcher.py
The script takes a few seconds to reach AWS. You should see output like this:
Finding latest Amazon Linux AMI...
Using AMI: ami-0abcdef1234567890
Launching instances...
Launched web-server (Prod): i-0abc123def456 [pending]
Launched db-server (Prod): i-0def789abc012 [pending]
Launched dev-server (Dev): i-0ghi345jkl678 [pending]
All instances launched. Run ec2_manager.py report to see your fleet.
Why does it say pending?
pending means AWS accepted your request and the instance is booting up. It transitions to running within 30 seconds or so. Your AMI ID and instance IDs will be different from the example above.
Now confirm that all three instances are running in the AWS console.
- Wait about 30 seconds for the instances to finish booting.
- Open the EC2 Instances page in your AWS console at https://console.aws.amazon.com/ec2/home#Instances:.
- Confirm you see three instances named web-server, db-server, and dev-server with the state Running.
Not seeing your instances?
- Make sure the region in the top right corner of the console says US East (N. Virginia). Your CLI is configured for us-east-1, so instances launched from your script appear there.
- If you see an AuthFailure or UnauthorizedOperation error in your terminal, your AWS CLI credentials may be incorrect. Run aws configure again and double-check your Access Key ID and Secret Access Key.
- If the script hangs or times out, check your internet connection. Boto3 needs network access to reach the AWS API.
Help me debug why my ec2_launcher.py script is not launching instances.
✔️ Awesome, I've got everything!
Great. Double check you've saved ec2_launcher.py and that all three instances show up in the AWS console.
ⓧ I'd like to double check the full code
Your ec2_launcher.py should contain the following three sections, in order. Scroll up and confirm each one is present:
- Imports (boto3 and json) and the get_amazon_linux_ami function that searches for the latest Amazon Linux 2023 AMI.
- The launch_instance function that calls run_instances with TagSpecifications for Name, Environment, and Project tags.
- The main() function that creates an EC2 client, calls get_amazon_linux_ami, launches three instances (web-server, db-server, dev-server), and a main() call at the very bottom of the file.
You just launched three EC2 instances from your terminal with a single command. Next up, you'll build a formatted fleet report that shows your entire fleet at a glance.
Build a Formatted Fleet Report
You have three EC2 instances running in AWS. But if you want to check on them right now, your only option is clicking through the AWS console.
The Boto3 describe_instances API can pull all of your instance data in one call. The problem is that it returns a deeply nested dictionary that is hard to scan. In this step, you will build a clean report that shows which servers are running, which are stopped, and what environment they belong to.
In this step, get ready to:
- Create a get_fleet_report function that queries AWS for your project instances.
- Add a print_report function that displays your fleet in a formatted table.
- Run the script to confirm all three instances appear with their correct tags.
Create the fleet report data function
Every cloud automation script follows the same pattern: describe, extract, format. You call an AWS API to describe your resources, extract the fields you care about, and format them for display.
- In VS Code, create a new file called ec2_manager.py inside your ec2-automation folder.
- Add the imports and the get_fleet_report function by copying this code into the file:
import boto3
import sys
def get_fleet_report(ec2_client):
"""Get all instances tagged with Project=ec2-automation."""
response = ec2_client.describe_instances(
Filters=[{"Name": "tag:Project", "Values": ["ec2-automation"]}]
)
instances = []
for reservation in response["Reservations"]:
for instance in reservation["Instances"]:
if instance["State"]["Name"] == "terminated":
continue
tags = {}
for tag in instance.get("Tags", []):
tags[tag["Key"]] = tag["Value"]
instances.append(
{
"InstanceId": instance["InstanceId"],
"InstanceType": instance["InstanceType"],
"State": instance["State"]["Name"],
"Name": tags.get("Name", "No tag"),
"Environment": tags.get("Environment", "No tag"),
}
)
return instances
What does this code do?
- describe_instances with a tag:Project filter asks AWS to return only the instances tagged with Project=ec2-automation. This way, the script never touches instances from other projects in your account.
- The response nests instances inside Reservations, so the code loops through both levels. This nested structure is a common gotcha for beginners who expect a flat list.
- if instance["State"]["Name"] == "terminated" skips instances that have already been terminated. AWS keeps terminated instances visible for about an hour after deletion.
- tags.get("Name", "No tag") safely handles instances that have no tags at all. Without .get(), accessing a missing key would crash the script with a KeyError.
Before you add the formatting, test that the function can actually pull data from AWS.
- Add these temporary test lines at the bottom of ec2_manager.py:
# Temporary test - you will replace this soon
ec2 = boto3.client("ec2")
print(get_fleet_report(ec2))
- Save ec2_manager.py.
- Run the script by entering this command in your terminal:
python ec2_manager.py
You should see a list of dictionaries printed in your terminal, one for each of your three instances. The output is a wall of text with keys like InstanceId, State, and Name all jammed together.
Seeing an empty list or an error?
- If you see [], your instances may have been terminated. Open the EC2 console and check that all three instances show a running state. If they were terminated, re-run ec2_launcher.py to launch fresh instances.
- If you see a credentials error, make sure your AWS CLI is still configured by running aws sts get-caller-identity in your terminal. If that fails, re-run aws configure.
- Make sure your region matches the region where you launched your instances (us-east-1).
Why is my fleet report returning an empty list?
Format and print the fleet table
The raw dictionary output proves your function works, but it is unreadable at a glance. A formatted table with aligned columns turns that wall of text into something you can actually scan.
- In ec2_manager.py, delete the three temporary test lines at the bottom of the file (the comment, the ec2 = line, and the print line).
- Add the print_report function and the main function below get_fleet_report by copying this code:
def print_report(instances):
"""Print a formatted fleet report."""
if not instances:
print("No instances found.")
return
print(f"\n{'Instance ID':<22} {'Name':<16} {'Type':<12} {'State':<12} {'Env':<8}")
print("-" * 70)
for inst in instances:
print(
f"{inst['InstanceId']:<22} {inst['Name']:<16} {inst['InstanceType']:<12} {inst['State']:<12} {inst['Environment']:<8}"
)
running = sum(1 for i in instances if i["State"] == "running")
stopped = sum(1 for i in instances if i["State"] == "stopped")
print("-" * 70)
print(f"Total: {len(instances)} | Running: {running} | Stopped: {stopped}")
def main():
ec2 = boto3.client("ec2")
instances = get_fleet_report(ec2)
print_report(instances)
main()
What does this code do?
- The :<22 syntax in the f-strings is Python's format specification. It left-aligns each value in a column that is 22 characters wide. This keeps every row lined up neatly.
- sum(1 for i in instances if i["State"] == "running") counts how many instances are in each state. The summary line gives you a quick picture of your fleet health without scanning every row.
- main() at the bottom calls your main function when you run the script. It creates the Boto3 client, gets the fleet data, and passes it to print_report.
- Save ec2_manager.py.
Before you run the script, what do you think the output will look like? How many instances do you expect, and what state should they all be in?
- Run the script by entering this command in your terminal:
python ec2_manager.py
You should see a formatted table with headers for Instance ID, Name, Type, State, and Env. All three instances (web-server, db-server, dev-server) should appear with a state of running and the correct Environment tags (Prod or Dev). The summary line at the bottom shows the total count and how many are running vs stopped.
Table columns not lining up?
- Check that the f-string inside print_report uses the exact same column widths as the header line. The header uses <22, <16, <12, <12, and <8. The data row must match.
- If you see a NameError mentioning get_fleet_report, make sure print_report and main are defined below get_fleet_report in the file, not above it.
- Make sure you deleted the three temporary test lines from the previous substep. Leftover code at the bottom can cause the script to run twice or throw errors.
Why is my fleet report not printing correctly?
✔️ Awesome, I've got everything!
Great. Double check you have saved ec2_manager.py.
ⓧ I'd like to double check the full code
Your ec2_manager.py should contain these sections in order. Scroll up and confirm each one is present:
- import boto3 and import sys at the top.
- The get_fleet_report function that calls describe_instances with the tag:Project filter, loops through Reservations and Instances, skips terminated instances, and extracts the five key fields using .get() for tags.
- The print_report function that prints a header row, a dashed line, each instance as an aligned row, and a summary line with running and stopped counts.
- The main function that creates the Boto3 client, calls get_fleet_report, and passes the result to print_report.
- main() on the last line to run the script.
Make sure the three temporary test lines are gone. Only the functions and main() should remain.
What is the describe-filter-extract pattern?
The script you just wrote follows the same three-step pattern that every cloud automation script uses. First, you describe your resources by calling an AWS API. Then, you filter the response to the subset you care about. Finally, you extract the specific fields and format them for display or further processing.
You will see this pattern repeated in the next steps when you add stop, start, and snapshot commands. The only thing that changes is what you do with the data after extracting it.
Your fleet report gives you a clear snapshot of every instance you own. Next up, you will use tag-based filtering to stop and start entire groups of instances with a single command.
Stop and Start Instances by Tag
Your fleet report gives you a clean snapshot of every instance, its state, and its tags. You can see exactly what is running.
But seeing is not managing. If you want to stop all Dev instances at the end of the day, you would have to copy each instance ID from the report and stop them one by one. That is the same manual work as clicking through the console. The real power of automation is batch operations based on tags.
In this step, get ready to:
- Add a tag filter that finds instances by their Environment tag.
- Build batch stop and start functions that act on entire groups at once.
- Wire everything to command-line arguments so you can manage your fleet with one terminal command.
Add a tag filter function
To manage instances by group, you first need a way to find them by tag. The filter_by_tag function will query AWS for instances matching a specific Environment tag and return their IDs as a list.
- In ec2_manager.py, add the following function below the print_report function:
def filter_by_tag(ec2_client, environment):
"""Get instance IDs filtered by Environment tag."""
response = ec2_client.describe_instances(
Filters=[
{"Name": "tag:Project", "Values": ["ec2-automation"]},
{"Name": "tag:Environment", "Values": [environment]},
{"Name": "instance-state-name", "Values": ["running", "stopped", "pending"]},
]
)
instance_ids = []
for reservation in response["Reservations"]:
for instance in reservation["Instances"]:
instance_ids.append(instance["InstanceId"])
return instance_ids
What does this code do?
This function calls describe_instances with three filters stacked together. All three must match for an instance to be returned.
- tag:Project scopes results to only instances this project created, so you never accidentally touch other resources in your account.
- tag:Environment narrows to the environment you pass in, like Dev or Prod.
- instance-state-name excludes terminated instances so you only get instances you can actually act on.
The function loops through the nested Reservations/Instances structure (the same pattern you used in get_fleet_report) and returns a flat list of instance IDs.
- Save ec2_manager.py.
- Run your script to confirm there are no syntax errors by running:
python ec2_manager.py
You should see the same fleet report as before. The new function is defined but not called yet, so the output has not changed.
Seeing a syntax error?
- Check that filter_by_tag is defined at the same indentation level as print_report. It should be a top-level function, not nested inside another function.
- Make sure there is a blank line between the end of print_report and the start of filter_by_tag.
- Verify the Filters list has three dictionaries, each with matching brackets and commas.
Still stuck? Help me debug a syntax error in my filter_by_tag function in ec2_manager.py.
Add batch stop and start functions
With filter_by_tag returning a list of instance IDs, you can now pass that entire list to stop_instances or start_instances to act on the whole group at once.
- In ec2_manager.py, add these two functions below filter_by_tag:
def stop_by_tag(ec2_client, environment):
"""Stop all running instances with the given Environment tag."""
instance_ids = filter_by_tag(ec2_client, environment)
if not instance_ids:
print(f"No instances found with Environment={environment}")
return
ec2_client.stop_instances(InstanceIds=instance_ids)
print(f"Stopping {len(instance_ids)} instance(s) with Environment={environment}")
def start_by_tag(ec2_client, environment):
"""Start all stopped instances with the given Environment tag."""
instance_ids = filter_by_tag(ec2_client, environment)
if not instance_ids:
print(f"No instances found with Environment={environment}")
return
ec2_client.start_instances(InstanceIds=instance_ids)
print(f"Starting {len(instance_ids)} instance(s) with Environment={environment}")
What does this code do?
Both functions follow the same pattern: call filter_by_tag to get matching instance IDs, check if the list is empty, and then call the Boto3 stop_instances or start_instances method with the full list of IDs.
The if not instance_ids guard prevents calling stop_instances with an empty list, which would cause an AWS API error.
- Save ec2_manager.py.
- Run your script again to confirm there are no syntax errors by running:
python ec2_manager.py
You should still see the fleet report. The new functions are defined but the current main function does not call them yet.
Seeing a NameError?
- Make sure stop_by_tag and start_by_tag are placed after filter_by_tag in the file. Python reads top to bottom, so a function must be defined before it is called.
- Check that the function name filter_by_tag inside stop_by_tag matches exactly. A typo like filter_by_tags will cause a NameError.
Still stuck? Help me debug a NameError in my stop_by_tag or start_by_tag function.
Wire functions to command-line arguments
Right now the main function always runs the fleet report. You need a way to tell the script which command to run from the terminal. Python's sys.argv list captures everything you type after python on the command line.
What is sys.argv?
When you run python ec2_manager.py stop Dev, Python splits that into a list: sys.argv[0] is the script name, sys.argv[1] is stop, and sys.argv[2] is Dev.
This turns your script into a mini CLI tool. Instead of editing the code every time you want a different action, you just change the command you type in the terminal.
- In ec2_manager.py, find the existing main function and the main() call at the bottom of the file:
def main():
ec2 = boto3.client("ec2")
instances = get_fleet_report(ec2)
print_report(instances)
main()
- Replace the entire main function and the main() call with this updated version:
def main():
ec2 = boto3.client("ec2")
if len(sys.argv) < 2:
print("Usage: python ec2_manager.py <command> [environment]")
print("Commands: report, stop, start")
return
command = sys.argv[1]
if command == "report":
instances = get_fleet_report(ec2)
print_report(instances)
elif command == "stop":
environment = sys.argv[2]
stop_by_tag(ec2, environment)
elif command == "start":
environment = sys.argv[2]
start_by_tag(ec2, environment)
else:
print(f"Unknown command: {command}")
main()
What does this code do?
- if len(sys.argv) < 2 checks whether the learner typed a command. If not, it prints usage instructions and exits.
- command = sys.argv[1] grabs the first argument after the script name (e.g. report, stop, or start).
- The if/elif chain routes each command to the right function. The stop and start commands also read sys.argv[2] for the environment name.
- Save ec2_manager.py.
Before you run this, think about what you expect to happen when you run python ec2_manager.py stop Dev. How many instances should be affected?
- Stop your Dev instance by running:
python ec2_manager.py stop Dev
You should see: Stopping 1 instance(s) with Environment=Dev. One command stopped the dev-server without you needing to look up its instance ID.
- Confirm the state changed by running the fleet report:
python ec2_manager.py report
The dev-server row should now show a state of stopping or stopped, while the two Prod instances remain running.
All instances still showing as running?
- EC2 state changes take a few seconds to propagate. Wait 10-15 seconds and run the report command again.
- Make sure you typed Dev with a capital D. Tag values are case-sensitive, so dev will not match the Dev tag you set in the launcher.
- Check that your filter_by_tag function uses tag:Environment as the filter name, not just Environment. The tag: prefix is required by the AWS API.
Still stuck? Help me debug why my stop command is not changing instance states.
✔️ Awesome, I've got everything!
Great. Make sure you have saved ec2_manager.py.
ⓧ I'd like to double check the full code
Your ec2_manager.py should now have these sections in this order. Scroll through your file and confirm each one is present.
- import boto3 and import sys at the top.
- get_fleet_report function (from the previous step).
- print_report function (from the previous step).
- filter_by_tag function with three Filters.
- stop_by_tag function.
- start_by_tag function.
- Updated main function with sys.argv handling for report, stop, and start commands.
- main() call at the very bottom of the file.
You just turned your fleet report into a management tool. One command stops or starts an entire environment. Next up, you will add EBS snapshots to back up your instance volumes from code.
Create EBS Snapshots for Backup
In the last step, you automated batch start and stop operations with tag-based filtering. Your toolkit can now report on your fleet, and stop or start entire groups of instances with a single command.
Stopping and starting instances saves on compute costs, but it does not protect against data loss. If an instance's volume gets corrupted or accidentally deleted, the data is gone. EBS snapshots create point-in-time backups of your volumes. Creating them from code is a common cloud automation task that shows up in cloud engineer interviews.
In this step, get ready to:
- Add a snapshot function that backs up EBS volumes for tagged instances.
- Wire snapshot and cleanup commands into your CLI tool.
- Test the full lifecycle from backup to complete resource cleanup.
Add the create_snapshots function
Every EC2 instance has at least one EBS volume attached to it. This volume holds the operating system and any data on the instance. A snapshot captures the entire state of that volume at a single point in time, stored safely in S3.
- In ec2_manager.py, find the start_by_tag function and scroll below it.
- Add the following create_snapshots function below start_by_tag:
def create_snapshots(ec2_client, environment):
"""Create EBS snapshots for all instances with the given Environment tag."""
response = ec2_client.describe_instances(
Filters=[
{"Name": "tag:Project", "Values": ["ec2-automation"]},
{"Name": "tag:Environment", "Values": [environment]},
{"Name": "instance-state-name", "Values": ["running", "stopped", "pending"]},
]
)
snapshot_count = 0
for reservation in response["Reservations"]:
for instance in reservation["Instances"]:
instance_id = instance["InstanceId"]
for block_device in instance.get("BlockDeviceMappings", []):
volume_id = block_device["Ebs"]["VolumeId"]
snap = ec2_client.create_snapshot(
VolumeId=volume_id,
Description=f"Backup of {volume_id} from {instance_id}",
TagSpecifications=[
{
"ResourceType": "snapshot",
"Tags": [
{"Key": "Project", "Value": "ec2-automation"},
{"Key": "SourceInstance", "Value": instance_id},
],
}
],
)
print(f"Snapshot {snap['SnapshotId']} created for volume {volume_id} ({instance_id})")
snapshot_count += 1
print(f"\nTotal snapshots created: {snapshot_count}")
What does this code do?
This function calls describe_instances directly instead of reusing filter_by_tag. That is because filter_by_tag only returns instance IDs, but here you need the full instance objects to access BlockDeviceMappings.
- BlockDeviceMappings is a list of all EBS volumes attached to an instance. Each entry contains the volume ID under block_device["Ebs"]["VolumeId"].
- create_snapshot takes the volume ID and creates a point-in-time copy. The function tags each snapshot with Project=ec2-automation so the cleanup command can find them later.
- snapshot_count tracks how many snapshots were created across all instances and volumes, then prints the total at the end.
- Save ec2_manager.py.
Wire the snapshot command and run it
The function exists, but your CLI tool does not know about it yet. You need to add a snapshot command to main().
- In ec2_manager.py, scroll down to the main() function.
- Find this line:
print("Commands: report, stop, start")
- Replace it with:
print("Commands: report, stop, start, snapshot")
- Now find the elif command == "start": block. Add the following elif block directly below it, before the else: line:
elif command == "snapshot":
environment = sys.argv[2]
create_snapshots(ec2, environment)
What does this code do?
This follows the same pattern as your stop and start commands. It reads the environment name from sys.argv[2] and passes it to create_snapshots.
- Save ec2_manager.py.
Before you run this, how many snapshots do you think it will create? Think about how many Prod instances you launched and how many volumes each one has.
- Test the snapshot command by running:
python ec2_manager.py snapshot Prod
You should see a snapshot ID and volume ID printed for each Prod instance. You launched two Prod instances (web-server and db-server), each with one volume, so you should see two snapshots created.
- Open the AWS Console in your browser.
- Navigate to EC2, then click Snapshots in the left sidebar under Elastic Block Store.
- Confirm you see the two snapshots listed with their volume IDs matching the terminal output.
Not seeing any snapshots?
- Make sure the region in the top-right corner of the AWS Console matches us-east-1. Snapshots only appear in the region where they were created.
- Check that your Prod instances are still running or stopped (not terminated). Run python ec2_manager.py report to verify their state.
- If you see a NameError for create_snapshots, make sure the function is defined above main() in your file.
Why aren't my EBS snapshots showing up when I run the snapshot command?
Add the cleanup_all function
Your toolkit can now launch, report, stop, start, and snapshot instances. The last piece is a cleanup command that tears everything down so you leave zero resources behind in your AWS account.
- In ec2_manager.py, add the following cleanup_all function below create_snapshots:
def cleanup_all(ec2_client):
"""Terminate all project instances and delete all project snapshots."""
# Find all Prod and Dev instances for this project
instance_ids = filter_by_tag(ec2_client, "Prod") + filter_by_tag(ec2_client, "Dev")
if instance_ids:
ec2_client.terminate_instances(InstanceIds=instance_ids)
print(f"Terminating {len(instance_ids)} instance(s)...")
else:
print("No instances to terminate.")
# Find and delete all snapshots tagged with this project
snapshots = ec2_client.describe_snapshots(
Filters=[{"Name": "tag:Project", "Values": ["ec2-automation"]}],
OwnerIds=["self"],
)
for snap in snapshots["Snapshots"]:
ec2_client.delete_snapshot(SnapshotId=snap["SnapshotId"])
print(f"Deleted snapshot {snap['SnapshotId']}")
print("Cleanup complete.")
What does this code do?
- filter_by_tag(ec2_client, "Prod") + filter_by_tag(ec2_client, "Dev") gathers all project instance IDs by combining the Prod and Dev lists. This reuses the function you already built.
- terminate_instances permanently shuts down the instances and releases their resources. Terminated instances cannot be restarted.
- describe_snapshots with OwnerIds=["self"] finds only your own snapshots tagged with Project=ec2-automation. Without OwnerIds, the call could return public snapshots from other accounts.
Now wire cleanup_all into your command handler the same way you wired the other commands.
- In the main() function, find this line:
print("Commands: report, stop, start, snapshot")
- Replace it with:
print("Commands: report, stop, start, snapshot, cleanup_all")
- Find the elif command == "snapshot": block you just added. Add the following elif block directly below it, before the else: line:
elif command == "cleanup_all":
cleanup_all(ec2)
- Save ec2_manager.py.
Before you run cleanup, what do you think will happen to your three instances and two snapshots?
- Run the cleanup command:
python ec2_manager.py cleanup_all
You should see output confirming that 3 instances are being terminated and both snapshots are deleted, followed by "Cleanup complete." on the last line.
- Confirm your fleet is empty by running:
python ec2_manager.py report
You should see "No instances found." printed. Your AWS account is clean.
Seeing instances still listed after cleanup?
- Instances take a few seconds to fully terminate. Wait 10-15 seconds and run the report again.
- If you see instances stuck in a "shutting-down" state, they are still terminating. Run the report again after a minute.
- If the snapshots were not deleted, check that the OwnerIds=["self"] parameter is present in your describe_snapshots call.
Why is my cleanup_all command not terminating my instances or deleting my snapshots?
✔️ Awesome, I've got everything!
Great. Double check you have saved ec2_manager.py.
ⓧ I'd like to double check the full code
Scroll up through your ec2_manager.py and confirm each of these sections is present, in order:
- Imports: import boto3 and import sys at the top.
- get_fleet_report function (from Step 3).
- print_report function (from Step 3).
- filter_by_tag function (from Step 4).
- stop_by_tag function (from Step 4).
- start_by_tag function (from Step 4).
- create_snapshots function (added in this step).
- cleanup_all function (added in this step).
- main() with commands: report, stop, start, snapshot, and cleanup_all.
- main() call at the very bottom of the file.
Your automation toolkit is complete. You can launch, report, stop, start, snapshot, and clean up EC2 instances, all from your terminal. Next up, there is an optional Secret Mission that extends your toolkit with a security group audit.
Secret mission
Security Group Audit
Can you add a security audit to your automation toolkit? Open security groups are one of the most common cloud misconfigurations. Build a function that scans every security group in your account and flags any rule that allows traffic from the entire internet.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project created EC2 instances and EBS snapshots in your AWS account, which can incur charges if your free tier has expired.
Cost warning
EC2 instances consume your free tier's 750-hour monthly budget while running. If your 12-month free tier period has expired, running t2.micro instances will incur hourly charges.
EBS snapshots have a small per-GB-month storage cost that can accumulate over time. If you are done with this project, the Delete option is recommended.
Resources you used:
- 3 EC2 t2.micro instances (web-server, db-server, dev-server) tagged with Project=ec2-automation.
- EBS snapshots tagged with Project=ec2-automation.
- Local ec2-automation project folder on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building or plan to continue shortly.
- Your EC2 instances remain running and continue consuming your free tier's 750-hour monthly budget.
- EBS snapshots remain stored in your account with a small per-GB-month storage cost.
- Your local ec2-automation folder and Python scripts stay on your Desktop.
Pause - I'll come back to this later
Stop your running instances to eliminate compute charges while keeping your files and snapshots intact.
- Stop your Prod instances by running:
python ec2_manager.py stop Prod
- Stop your Dev instance by running:
python ec2_manager.py stop Dev
- Confirm all instances are stopped by running:
python ec2_manager.py report
All three instances should show a state of stopped. Stopped instances do not incur compute charges. EBS volumes attached to stopped instances still have minimal storage costs.
Delete - I don't want to use this again
Remove all project resources and start fresh. This is the recommended option after completing the project.
- Terminate all EC2 instances and delete all EBS snapshots by running:
python ec2_manager.py cleanup_all
You should see a message confirming the instances are terminating and each snapshot is deleted. If you already ran this command during Step 5, it will print that there are no instances to terminate.
- Verify the cleanup in the AWS Console by opening the EC2 service, clicking Instances, and confirming all three project instances show a state of terminated.
- Delete the local project folder. On macOS or Linux, run:
rm -rf ~/Desktop/ec2-automation
- On Windows (PowerShell), run:
Remove-Item -Recurse -Force ~\Desktop\ec2-automation
- Confirm the folder is gone by running:
ls ~/Desktop
You should no longer see ec2-automation in the output.
Nice Work!
Nice Work!
You just automated EC2 management with Python and Boto3. You went from zero cloud automation experience to running a full fleet management toolkit from your terminal.
What you learned:
- Launched EC2 instances programmatically with tags, replacing manual console clicks with a single Python command.
- Built a formatted fleet report using the describe-filter-extract pattern that every cloud automation script follows.
- Automated batch stop/start operations by tag and created EBS snapshots for backup, all driven by sys.argv commands in your terminal.
- Secret Mission: Audited security groups for overly permissive inbound rules open to 0.0.0.0/0, a real-world security task that cloud engineers perform regularly.
Ready to quiz yourself?