This guide provides a complete walkthrough for starting, stopping, rebooting, describing, and creating EC2 instances using Node.js and the AWS SDK for JavaScript v3. The examples are designed for Node 18+ and assume credentials configured in the AWS profile or environment variables.
Prerequisites
- An AWS account with permissions on EC2 (preferably via a least-privilege role).
- Node.js 18 or later installed.
- Credentials configured with
aws configure
or environment variablesAWS_ACCESS_KEY_ID
,AWS_SECRET_ACCESS_KEY
,AWS_REGION
.
Example of a least-privilege IAM policy for basic management
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": [
"ec2:DescribeInstances",
"ec2:DescribeInstanceStatus",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:RebootInstances"
], "Resource": "*" }
]
}
If you also need to create/terminate instances, add: ec2:RunInstances
, ec2:TerminateInstances
, ec2:CreateTags
, and permissions related to key pairs and security groups.
Installing packages
npm init -y
npm i @aws-sdk/client-ec2 @aws-sdk/credential-providers
Configuring the EC2 client
// ec2-client.js
import { EC2Client } from "@aws-sdk/client-ec2";
import { fromEnv } from "@aws-sdk/credential-providers";
export const ec2 = new EC2Client({
region: process.env.AWS_REGION || "eu-central-1",
credentials: process.env.AWS_ACCESS_KEY_ID
? fromEnv()
: undefined // will use local profile/role if available
});
Common operations on an existing instance
Suppose we have the instance ID, e.g. i-0123456789abcdef0
. Create reusable functions:
Describe state and details
// describe.js
import { DescribeInstancesCommand } from "@aws-sdk/client-ec2";
import { ec2 } from "./ec2-client.js";
export async function describeInstance(instanceId) {
const cmd = new DescribeInstancesCommand({ InstanceIds: [instanceId] });
const res = await ec2.send(cmd);
const reservation = res.Reservations?.[0];
const inst = reservation?.Instances?.[0];
if (!inst) throw new Error("Instance not found");
return {
id: inst.InstanceId,
state: inst.State?.Name,
type: inst.InstanceType,
az: inst.Placement?.AvailabilityZone,
publicIp: inst.PublicIpAddress,
privateIp: inst.PrivateIpAddress,
launchTime: inst.LaunchTime
};
}
Start, stop, reboot
// lifecycle.js
import {
StartInstancesCommand,
StopInstancesCommand,
RebootInstancesCommand
} from "@aws-sdk/client-ec2";
import { ec2 } from "./ec2-client.js";
export async function startInstance(instanceId) {
await ec2.send(new StartInstancesCommand({ InstanceIds: [instanceId] }));
}
export async function stopInstance(instanceId, hibernate = false) {
await ec2.send(new StopInstancesCommand({
InstanceIds: [instanceId],
Hibernate: hibernate
}));
}
export async function rebootInstance(instanceId) {
await ec2.send(new RebootInstancesCommand({ InstanceIds: [instanceId] }));
}
Waiting for state transitions (waiters)
Waiters can reliably wait until the instance is running or stopped before proceeding.
// waiters.js
import {
waitUntilInstanceRunning,
waitUntilInstanceStopped
} from "@aws-sdk/client-ec2";
import { ec2 } from "./ec2-client.js";
export async function waitRunning(instanceId, maxWaitSeconds = 300) {
const out = await waitUntilInstanceRunning(
{ client: ec2, maxWaitTime: maxWaitSeconds },
{ InstanceIds: [instanceId] }
);
if (out.state !== "SUCCESS") throw new Error("Timeout while waiting for running");
}
export async function waitStopped(instanceId, maxWaitSeconds = 300) {
const out = await waitUntilInstanceStopped(
{ client: ec2, maxWaitTime: maxWaitSeconds },
{ InstanceIds: [instanceId] }
);
if (out.state !== "SUCCESS") throw new Error("Timeout while waiting for stopped");
}
Creating a new instance
To create an instance you need: AMI, instance type, key pair (for SSH), security group, and subnet. Here’s a minimal example with tags and user data to install Nginx on Amazon Linux 2023.
// create-instance.js
import {
RunInstancesCommand,
CreateTagsCommand,
DescribeImagesCommand
} from "@aws-sdk/client-ec2";
import { ec2 } from "./ec2-client.js";
// Optional: fetch the latest Amazon Linux 2023 AMI (x86_64) for the region
async function getLatestAL2023Ami() {
const res = await ec2.send(new DescribeImagesCommand({
Owners: ["amazon"],
Filters: [
{ Name: "name", Values: ["al2023-ami-*-x86_64"] },
{ Name: "state", Values: ["available"] },
{ Name: "architecture", Values: ["x86_64"] },
{ Name: "root-device-type", Values: ["ebs"] }
]
}));
const images = res.Images ?? [];
images.sort((a, b) => new Date(b.CreationDate) - new Date(a.CreationDate));
if (!images[0]?.ImageId) throw new Error("AMI not found");
return images[0].ImageId;
}
export async function createInstance({
amiId,
instanceType = "t3.micro",
keyName,
securityGroupIds,
subnetId,
tags = { Project: "demo-node-ec2" }
}) {
const imageId = amiId || await getLatestAL2023Ami();
const userData = Buffer.from(`#!/bin/bash
dnf -y update
dnf -y install nginx
systemctl enable nginx
systemctl start nginx
`).toString("base64");
const run = new RunInstancesCommand({
ImageId: imageId,
InstanceType: instanceType,
KeyName: keyName,
SecurityGroupIds: securityGroupIds,
SubnetId: subnetId,
MinCount: 1,
MaxCount: 1,
TagSpecifications: [
{
ResourceType: "instance",
Tags: Object.entries(tags).map(([Key, Value]) => ({ Key, Value }))
}
],
UserData: userData
});
const out = await ec2.send(run);
const instanceId = out.Instances?.[0]?.InstanceId;
if (!instanceId) throw new Error("Instance creation failed");
// (Optional) add tags to the root volume
try {
const volumeId = out.Instances?.[0]?.BlockDeviceMappings?.[0]?.Ebs?.VolumeId;
if (volumeId) {
await ec2.send(new CreateTagsCommand({
Resources: [volumeId],
Tags: Object.entries(tags).map(([Key, Value]) => ({ Key, Value }))
}));
}
} catch {}
return instanceId;
}
Secure management of credentials and roles
- Prefer using IAM Roles (e.g., on an EC2/CodeBuild runner) instead of static keys.
- For local environments, use profiles in
~/.aws/credentials
andAWS_PROFILE
. - Limit permissions to the minimum necessary: separate the “read/describe” policy from the “lifecycle” one.
Operational best practices
- Idempotency: protect repeated commands (e.g., check the state before calling
StartInstances
/StopInstances
). - Retry and backoff: AWS APIs may respond with throttling; use exponential backoff retries if integrating more complex orchestrations.
- Waiters: rely on waiters for consistent states before executing next steps (IP allocation, tagging, health check).
- Tagging: apply consistent tags for cost allocation and search (
Project
,Env
,Owner
). - Security: protect private key pairs; restrict security groups (SSH only from your IP, HTTP/HTTPS as needed).
- Shutdown: automate stopping unused instances to contain costs (e.g., external cron or Lambda Scheduler).
Quick diagnostics
// utils-status.js
import { DescribeInstanceStatusCommand } from "@aws-sdk/client-ec2";
import { ec2 } from "./ec2-client.js";
export async function instanceChecks(instanceId) {
const res = await ec2.send(new DescribeInstanceStatusCommand({
InstanceIds: [instanceId],
IncludeAllInstances: true
}));
return res.InstanceStatuses?.[0] ?? null;
}
Conclusion
With the AWS SDK v3 and a few well-structured functions you can orchestrate the entire lifecycle of an EC2 instance directly from Node.js: from creation with user data to state checks, up to scheduled shutdown. Adapt the examples to your IAM rules, security groups, and your networking and observability needs.