Connect to Private EC2 Instances Using AWS SSM Session Manager
Written on
Chapter 1: Understanding AWS SSM Session Manager
If you're not acquainted with AWS SSM Session Manager, you might find it helpful to explore my previous article: "What is AWS SSM Session Manager?". For our discussion here, think of AWS SSM Session Manager as a user-friendly alternative to SSH. It simplifies administration and eliminates the need for your target resource to have public access.
Motivation
In a perfect scenario, manual access to a remote machine would be unnecessary because everything would be automated and monitored effectively. Unfortunately, we don’t always live in such a scenario.
Eventually, you might find the need to access a remote machine. This could be due to debugging an issue that lacks visibility, needing to adjust configurations without the appropriate automation, or dealing with automation failures. As AWS CTO Werner Vogels aptly states, "everything fails, all the time." Thus, connecting directly to a private resource becomes a pragmatic necessity.
Cost Considerations
Cost is always a crucial factor. While it may not be your foremost concern, it should definitely be high on your priority list. Fortunately, maintaining low costs is achievable. Besides necessary free resources like IAM, here’s what you will need:
- Basic Networking: VPC, subnets, security groups, etc. (free)
- An EC2 instance (we'll opt for a t4.nano, costing around $3.00/month)
- AWS SSM Session Manager (free)
If your EC2 instance resides in a private subnet, you will also need one of the following:
- A NAT Gateway (minimum $32.85/month per AZ, $0.045/hour + $0.045/GB)
- A NAT instance like fck-nat (cost varies, minimum ~$3.00/month)
- 3 VPC Interface Endpoints (minimum $22/month per AZ; $0.01/hour + $0.01/GB)
For this guide, we will utilize the interface endpoint, bringing the total approximate monthly cost to $25, excluding data transfer costs. If you’re already working with an existing setup, you may find that some or all of these components are already in place, meaning additional costs could be negligible.
Caution: If you’re deploying this personally, keep the costs mentioned in mind. It’s advisable to dismantle any resources you create once you’re finished by deleting the stacks we generate.
Required Tools
To follow along, you’ll need a few essential tools:
- AWS CDK: We will use the AWS CDK with TypeScript to outline our infrastructure. If you're unfamiliar with it, the AWS CDK is an open-source framework for defining cloud infrastructure using modern programming languages, effectively providing a programmatic abstraction over CloudFormation. You can refer to AWS’s Getting Started guide for setup instructions.
(Optional) Granted: For actions involving the AWS CDK or CLI, ensure you’re using the correct AWS credentials. If you regularly work with multiple accounts and roles, consider using a tool like Granted to manage your profiles effectively.
Defining Our Infrastructure
Now that we have the necessary groundwork laid, let’s construct the infrastructure to see everything in action. All the resources in this section can also be found in the repository "Using-AWS-Session-Manager-to-Connect-to-Remote-Private-Resources."
Assumptions: The AWS CLI/CDK commands below assume you've configured your default AWS profile or assumed a profile in your shell.
Stack Composition
The structure of the stacks below aims to provide a modular example and may not represent the optimal distribution of resources across stacks.
Project Structure:
README.md
bin
└── using-session-manager.ts
cdk.json
jest.config.js
lib
├── stack.AuroraDB.ts
├── stack.BastionHost.ts
└── stack.Networking.ts
package-lock.json
package.json
test
└── using-session-manager.test.ts
tsconfig.json
To replicate this layout, you can run the cdk init --language="typescript" --generate-only command in an empty directory or clone the paired repository.
The Networking Stack
Before we can build additional components, we need a network. This requires defining a VPC and its associated elements, which can be complex. For convenience, we will delegate most of this complexity to the CDK’s provided VPC Construct.
At a high level, this construct will:
- Create a private network with a designated IP range (our VPC)
- Segment our network into subnets with varying levels of isolation
- Create an internet gateway to enable communication with the public internet
- Establish basic routing and controls within our network
For the purpose of this guide, we will not delve deeper into the specifics, as explaining the resources created warrants its own discussion.
// File: lib/stack.Networking.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
const VPC_CIDR = "10.0.0.0/16"; // Total of 65,536 IP addresses
const SUBNET_CIDR_MASK = 20; // 4096 IP addresses per subnet
const MAX_AZ_COUNT = 1; // Just one AZ for this example
export class NetworkStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.vpc = new ec2.Vpc(this, "VPC", {
ipAddresses: ec2.IpAddresses.cidr(VPC_CIDR),
maxAzs: MAX_AZ_COUNT,
natGateways: 0,
gatewayEndpoints: {
S3: {
service: ec2.GatewayVpcEndpointAwsService.S3,},
DynamoDB: {
service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,},
},
subnetConfiguration: [
{
cidrMask: SUBNET_CIDR_MASK,
name: "Public",
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: SUBNET_CIDR_MASK,
name: "Private",
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
{
cidrMask: SUBNET_CIDR_MASK,
name: "Isolated",
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});
}
}
You may observe that I've included two gateway endpoints in the definition above. They are free and facilitate connectivity between the specified AWS services from within your VPC. While the DynamoDB endpoint is not necessary for our example, the S3 Gateway endpoint is critical.
We also need to define our VPC Interface Endpoints, which will allow the SSM Agent to connect with the SSM service without needing public internet access:
// File: lib/stack.Networking.ts
this.vpc.addInterfaceEndpoint("ssm", {
service: ec2.InterfaceVpcEndpointAwsService.SSM,
});
this.vpc.addInterfaceEndpoint("ec2messages", {
service: ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES,
});
this.vpc.addInterfaceEndpoint("ssmmessages", {
service: ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES,
});
These interface endpoints enable secure connectivity from your VPC to the relevant parts of the SSM service, enhancing your security posture and being more cost-effective than utilizing a NAT Gateway.
The EC2 Instance
We will simplify our task by using the AWS CDK-provided construct to handle most of the heavy lifting for us. Specifically, we will utilize ec2.BastionHostLinux, which:
- Defines the EC2 instance along with its storage and networking
- Assigns IAM roles for SSM interaction
// File: lib/stack.BastionHost.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
interface BastionStackProps extends cdk.StackProps {
vpc: ec2.IVpc;
}
export class BastionStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: BastionStackProps) {
super(scope, id, props);
const bastionSecurityGroup = new ec2.SecurityGroup(this, "bastionSecurityGroup", {
vpc: props.vpc,
allowAllOutbound: true,
});
const bastion = new ec2.BastionHostLinux(this, "bastion", {
vpc: props.vpc,
subnetSelection: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
requireImdsv2: true,
securityGroup: bastionSecurityGroup,
instanceName: ${this.node.path}-bastion,
machineImage: ec2.MachineImage.latestAmazonLinux2023({
cpuType: ec2.AmazonLinuxCpuType.ARM_64,}),
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T4G,
ec2.InstanceSize.NANO
),
blockDevices: [
{
deviceName: "/dev/xvda",
volume: ec2.BlockDeviceVolume.ebs(8, {
encrypted: true,}),
},
],
});
}
}
A crucial aspect of this configuration is the IAM policy that permits the EC2 instance to interact with SSM. This policy allows the SSM agent running on the instance to communicate with the SSM service.
Assembling the CDK Application
Now, we need to create our CDK application, combining all the stacks we defined earlier.
#!/usr/bin/env node
// File: bin/using-session-manager.ts
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { NetworkStack } from '../lib/stack.Networking';
import { BastionStack } from '../lib/stack.BastionHost';
const app = new cdk.App();
const network_stack = new NetworkStack(app, 'UsingSessionManager-Network', {});
new BastionStack(app, 'UsingSessionManager-Bastion', {
vpc: network_stack.vpc
});
Deploying Our Infrastructure
If you haven’t used CDK in your account previously, you’ll need to bootstrap your environment first. After completing that, if you’ve cloned the repository and executed npm install, you can deploy the stacks with the following command:
npx cdk deploy UsingSessionManager-Network UsingSessionManager-Bastion
Video Demonstrations
To further enhance your understanding, check out these informative videos:
Go Bastionless - Access Private EC2 Instances using SSM Session Manager: This video provides a comprehensive overview of accessing private EC2 instances without a bastion host.
Connect to EC2 with Session Manager and EC2 Instance Connect: This tutorial demonstrates the process of connecting to EC2 instances using Session Manager and EC2 Instance Connect.
Connecting to Your Instance
Now, let's use AWS SSM Session Manager to connect to our private EC2 instance. You simply need to run the following command (replace <instance-id-here> with the actual output from CDK):
aws ssm start-session --target <instance-id-here>
If everything is successful, you should find yourself in a remote shell:
aws ssm start-session --target i-058ae85fd0329142a
Starting session with SessionId: [email protected]
sh-5.2$
You can also connect through the AWS Console by selecting your instance, clicking on "Connect," navigating to the “Session Manager” tab, and then clicking “Connect.”
Conclusion
In summary, our instance operates without internet access and permits no incoming connections. The connection to SSM is facilitated by an agent on the EC2 instance that establishes outgoing connections to the AWS SSM service.
Using interface endpoints enhances security and is more cost-effective than employing a NAT Gateway, which would otherwise route traffic through the public internet.
Thank you for reading! I hope you found this article informative. If you have any questions or feedback, feel free to reach out. Don’t forget to subscribe for more updates!