Back to Blog

Automating EC2 Instance Management with AWS Lambda: A Step-by-Step Guide

Automating EC2 Instance Management with AWS Lambda: A Step-by-Step Guide

Manually starting and stopping EC2 instances is a routine but error-prone task. Instances left running outside business hours incur unnecessary costs, and manual termination workflows carry the risk of data loss if a backup step is skipped. This guide walks through building a fully serverless EC2 management dashboard using AWS Lambda and Python — covering the architecture, the IAM considerations, and the exact steps to deploy it yourself.

The complete, working source code for every file referenced in this guide is available on GitHub:

https://github.com/nabinphoenix/ec2-lambda-portal

Clone or download it alongside this guide — each section below links directly to the specific file being discussed, so you always have the current, working version rather than a copy that can drift out of date.

What You Will Build

  • A function that lists every EC2 instance in your account, so a dropdown menu always shows the current, up-to-date list
  • A function that starts a chosen instance
  • A function that stops a chosen instance
  • A function that creates an AMI backup of an instance and only then terminates it, so you never lose data by accident
  • An optional function that schedules a start or stop for a future time in Nepal local time
  • A single-page HTML dashboard that ties all of the above together

Prerequisites

  • Access to an AWS account or AWS Academy/Vocareum lab account
  • An existing IAM role your Lambda functions can use (in a lab environment this is usually called LabRole)
  • At least one EC2 instance already running or stopped, so there is something to test against
  • Git installed, if you want to clone the repository rather than download files individually

Step 0: Understand the IAM Role Before You Touch Any Code

Every Lambda function needs an execution role — a set of AWS permissions that says what the function is allowed to do. Before creating anything, open the IAM console and check which role you have available:

  1. Go to the IAM console → Roles
  2. Look for a role you are permitted to use (in an AWS Academy lab, this is almost always LabRole)
  3. Click into it and note its Role ARN — you will need it later if you set up scheduling

In a standard personal AWS account, best practice is to create a dedicated, narrowly scoped role per function. In a restricted lab account where iam:CreateRole is disabled, every function in this guide instead reuses the same existing role. That single decision is what makes the whole project deployable without any extra IAM setup.

Step 1: Deploy the Instance-Listing Function

This function is called first by the dashboard, to populate a dropdown with every EC2 instance currently in your account — so no instance ID is ever hardcoded anywhere in the system.

  1. Open the Lambda console → Create function
  2. Choose Author from scratch, name it dropdownFunction
  3. Runtime: Python 3.x
  4. Execution role: Use an existing role → select your role (e.g. LabRole)
  5. Click Create function
  6. Open the code file and replace its contents with:
    https://github.com/nabinphoenix/ec2-lambda-portal/blob/main/lambdas/dropdown_function.py
  7. Click Deploy

What it does: calls ec2.describe_instances(), walks through every returned reservation and instance, extracts the Name tag where present, and returns a JSON array of {instance_id, name, state} objects. Because it queries live data on every call rather than using a fixed list, it automatically scales to however many instances your account has.

Enable a Function URL for this function (Configuration → Function URL → Create, Auth type NONE) and copy the URL — you will need it in Step 7.

Step 2: Deploy the Start Function

Create a new function named start-ec2-instance, same runtime and execution role as above, then replace its code with:

https://github.com/nabinphoenix/ec2-lambda-portal/blob/main/lambdas/start_ec2_instance.py

What it does: reads instance_id from the JSON request body, validates that it was actually provided (returning a clean 400 error if not), then calls ec2.start_instances(InstanceIds=[instance_id]). Both the input parsing and the AWS API call are wrapped in their own try/except blocks, so any failure — a malformed request or an AWS-side error — comes back as a readable JSON error instead of an unhandled crash.

Deploy, then enable a Function URL exactly as in Step 1.

Step 3: Deploy the Stop Function

Create a function named stop-ec2-instance, same role, and replace its code with:

https://github.com/nabinphoenix/ec2-lambda-portal/blob/main/lambdas/stop_ec2_instance.py

What it does: mirrors the start function exactly, calling ec2.stop_instances() instead. Same input validation, same error handling pattern. Deploy and enable its Function URL.

Step 4: Deploy the Backup-Before-Terminate Function

This is the function with real stakes — it guarantees an instance is never terminated without a backup existing first. Create a function named backupBeforeTerminate, same role, and replace its code with:

https://github.com/nabinphoenix/ec2-lambda-portal/blob/main/lambdas/backup_before_terminate.py

What it does: after validating instance_id, it builds a unique AMI name in the form backup-<instance_id>-<timestamp>, calls ec2.create_image(..., NoReboot=True) to snapshot the instance without restarting it, and only after that call succeeds calls ec2.terminate_instances(). If the backup step raises an exception for any reason, execution jumps to the except block and termination never happens — that ordering is the entire safety guarantee of this function.

Deploy, then enable its Function URL. The resulting AMI will be visible under EC2 → AMIs → Owned by me.

Step 5 (Optional): Deploy the Scheduler Function

If you want to queue a start or stop for a future time rather than running it immediately, create a function named scheduleEc2Action, same role, and replace its code with:

https://github.com/nabinphoenix/ec2-lambda-portal/blob/main/lambdas/scheduler_function.py

What it does: accepts an instance_id, an action (start or stop), and a schedule_time in Nepal local time, then calls EventBridge Scheduler's create_schedule() with ScheduleExpressionTimezone="Asia/Kathmandu", so AWS handles the UTC conversion internally rather than requiring manual timezone math. ActionAfterCompletion="DELETE" ensures the schedule cleans itself up after firing once.

This function needs three environment variables set (Configuration → Environment variables): START_LAMBDA_ARN and STOP_LAMBDA_ARN (the Function ARNs of the two functions from Steps 2 and 3, not their URLs), and SCHEDULER_ROLE_ARN (your execution role's ARN).

A real limitation to know about: EventBridge Scheduler must be able to assume the role in SCHEDULER_ROLE_ARN, which requires that role's trust policy to explicitly list scheduler.amazonaws.com. In a shared lab role, that trust policy usually cannot be edited. If scheduling fails with a role-assumption error, this is why — every other feature in this project works independently of it.

Step 6: Enable CORS on Every Function URL

This step trips up almost every beginner, so read it carefully. Your browser will not let a webpage call these Lambda URLs unless each Function URL explicitly allows it. For every function created above:

  1. Go to Configuration → Function URL → Edit
  2. Check Configure CORS
  3. Allow origin: *
  4. Allow methods: POST (or GET for dropdownFunction)
  5. Allow headers: content-type
  6. Save

The single most common mistake in this project: do not also return an Access-Control-Allow-Origin header from inside your Lambda code if CORS is already configured on the Function URL. AWS merges both, the header ends up duplicated, and browsers reject the response outright with a generic "Failed to fetch" error — even though the underlying logic is completely correct. Configure CORS in exactly one place, the Function URL settings. If you look at the code in the repository, none of the response headers include Access-Control-Allow-Origin for this exact reason.

Step 7: Deploy the Frontend

The dashboard is a single static HTML file — no build step, no framework:

https://github.com/nabinphoenix/ec2-lambda-portal/blob/main/frontend/index.html

Open the file, find the CONFIG object near the top of the <script> tag, and paste in each Function URL you copied from Steps 1–5. Save and open the file directly in a browser — no server required. The dropdown populates automatically on load by calling dropdownFunction, and every button (Start, Stop, Back up & terminate, Schedule) sends a POST request with the selected instance ID as JSON to the matching Function URL.

Step 8: Test Everything End to End

  1. Open the dashboard and confirm the dropdown populates with your real instances
  2. Select an instance and click Start — confirm its state changes to running in the EC2 console
  3. Click Stop on the same instance — confirm it changes to stopped
  4. On a disposable test instance only, try Back up & terminate, then confirm the new AMI appears under EC2 → AMIs → Owned by me before confirming the instance itself is gone

Common Errors and How to Read Them

  • "Failed to fetch" in the browser: almost always CORS — check it is configured in exactly one place, per Step 6.
  • A 400 error, "instance_id is required": the frontend did not send an instance ID, or the dropdown selection was empty when the button was clicked.
  • A 500 error with an AWS error message: this is the function's own error handling working as intended — the error field in the response will name the exact problem, most often an invalid instance ID or a missing permission on the execution role.

Conclusion

Every file referenced in this guide lives in the repository linked at the top, kept up to date independently of this article. That separation is intentional: the blog explains the reasoning, the architecture, and the mistakes worth avoiding, while the repository remains the single source of truth for the actual, working code. If you improve or extend the project, the repository is also where those changes belong.

The result is a complete EC2 management tool — instance discovery, on-demand start/stop, backup-and-terminate, and timezone-aware scheduling — built entirely from five small Lambda functions and one static HTML page, with no servers to patch and no cost incurred while idle.


Want Help Building Your Own AWS Automation?

If you would like guidance building serverless tools like this for your own AWS environment — whether for cost control, operational safety, or general automation — contact us today for a personalized consultation and let's design something tailored to your setup.