AWS SSM Web (Services System Manager), is a way to manage your EC2 instances via the SSM tool, this provides a number of different features, the one I commonly use is SSM for reaching the console of an EC2 instance; without needing SSH. With SSM you can get to the console and a command line, all without having to provide Internet access to the EC2 instance, and that is what this article is about.
SSM requires that the EC2 instance (assuming it has the SSM Agent installed) to be able to reach the SSM endpoints to register and therefore be remotely controlled (if you are using the console access). If your EC2 instance can reach the Internet this is no problem, but what if your EC2 instance is on a private subnet with no Internet access, what then?
Well, you can use a VPC Endpoint and publish the various needed endpoints directly within the VPC (and any subnets), what this means is that when the EC2 instance’s SSM Agent attempts to register, it will resolve the endpoint names but instead of attempting to reach these via the Internet, it will “short-circuit” its way directly from the VPC to the endpoint. Thus allowing it to work without Internet access.
Here’s the Terraform for how to enable the VPC Endpoint for SSM:
locals {
example_services2 = {
"ec2messages" : {
"name" : "com.amazonaws.${var.region}.ec2messages"
},
"ssm" : {
"name" : "com.amazonaws.${var.region}.ssm"
},
"ssmmessages" : {
"name" : "com.amazonaws.${var.region}.ssmmessages"
}
}
}
resource "aws_vpc_endpoint" "example_ssm_endpoint2" {
for_each = local.example_services2
vpc_id = aws_vpc.test_workload_2.id
service_name = each.value.name
vpc_endpoint_type = "Interface"
security_group_ids = [aws_security_group.sg_test_workload_2-Instance-1.id]
private_dns_enabled = true
ip_address_type = "ipv4"
subnet_ids = [aws_subnet.test_workload_2-Private-Subnet-A.id, aws_subnet.test_workload_2-Private-Subnet-B.id]
}
You’ll notice in that you need to create a Security Group to govern the traffic that can reach the SSM endpoints and also specify which subnets the VPC Endpoints will be published into.
For reference, here is a more complete example:
// Security Group ---------------------------------------------------------------------------------------------------------------------------
resource "aws_security_group" "sg_test_workload_2-Instance-1" {
name = "sg_test_workload_2-Instance-1"
vpc_id = aws_vpc.test_workload_2.id
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
lifecycle {
create_before_destroy = true
}
}
// EC2 Instances ----------------------------------------------------------------------------------------------------------------------------
# User data: install nginx, make self-signed cert, HTTPS server block + redirect
locals {
example_user_data2 = <<-EOF
#!/bin/bash
set -eux
dnf -y install httpd
cat >/var/www/html/index.html <<'HTML'
<!doctype html>
<html><head><meta charset="utf-8"><title>Hello from EC2</title></head>
<body style="font-family: system-ui; margin: 3rem">
<h1>It works 🎉</h1>
<p>Served by EC2 directly with nothing else!</p>
</body></html>
HTML
systemctl enable --now httpd
EOF
}
// EC2 Instance 1
resource "aws_instance" "test_workload_2-Instance-1" {
ami = "ami-008ea0202116dbc56"
instance_type = "t2.micro"
tenancy = "default"
availability_zone = "eu-west-2a"
key_name = ""
subnet_id = aws_subnet.test_workload_2-Private-Subnet-A.id
vpc_security_group_ids = [aws_security_group.sg_test_workload_2-Instance-1.id]
iam_instance_profile = aws_iam_instance_profile.ec2_profile2.name
user_data = local.example_user_data2
tags = {
Name = "test_workload_2-Instance-1"
}
}
// EC2 IAM Role -----------------------------------------------------------------------------------------------------------------------------------------
resource "aws_iam_role" "ec2_role2" {
name = "ec2-ssm-role2"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = ""
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
},
]
})
}
resource "aws_iam_role_policy_attachment" "custom2" {
role = aws_iam_role.ec2_role2.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ec2_profile2" {
name = "ec2-ssm-profile2"
role = aws_iam_role.ec2_role2.name
}
# # // VPC Endpoint (for SSM) -------------------------------------------------------------------------------------------------------
locals {
example_services2 = {
"ec2messages" : {
"name" : "com.amazonaws.${var.region}.ec2messages"
},
"ssm" : {
"name" : "com.amazonaws.${var.region}.ssm"
},
"ssmmessages" : {
"name" : "com.amazonaws.${var.region}.ssmmessages"
}
}
}
resource "aws_vpc_endpoint" "example_ssm_endpoint2" {
for_each = local.example_services2
vpc_id = aws_vpc.test_workload_2.id
service_name = each.value.name
vpc_endpoint_type = "Interface"
security_group_ids = [aws_security_group.sg_test_workload_2-Instance-1.id]
private_dns_enabled = true
ip_address_type = "ipv4"
subnet_ids = [aws_subnet.test_workload_2-Private-Subnet-A.id, aws_subnet.test_workload_2-Private-Subnet-B.id]
}