There are a few scenarios in which ssm can be deployed and break. All of this assumes you have the proper role attached to the vm.
resource aws_iam_role "ssm" {
name = "myssm"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
resource aws_iam_role_policy_attachment "ssm" {
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
role = aws_iam_role.ssm.id
}
resource aws_iam_instance_profile "ssm" {
name = "myssm"
role = aws_iam_role.ssm.id
}
resource "aws_instance" "private" {
ami = data.aws_ami.amzn2.id
instance_type = "t3.micro"
subnet_id = aws_subnet.private.id
vpc_security_group_ids = [aws_security_group.test-ssm.id]
iam_instance_profile = aws_iam_instance_profile.ssm.name
tags = {
Name = "session-manager-private"
}
}
public subnet with no public ip (internet access)

In this scenario even though you have a vm in a public subnet and outbound via the igw, there's no public ip on the vm so ssm will not work.
public subnet with public ip (internet access)

In this scenario, same as the previous only difference being I've added a public ip to the vm and ssm kicks into life.
private subnet with public ip (internet access)

In this scenario even though the vm is in a private subnet it has outbound internet access via a public nat gateway which in turn has outbound access via the internet gateway. The nat gateway picutred here has a public ip. When a nat gw with a public ip sits infront of a private subnet those vms use that pubic ip for internet outbound, so ssm works.

Well I wonder what's going to happen here? If you guessed absolutely nothing, you'd be right. There's no public ip no route out of any kind and no way in. It's a completely isolated tenant. So how do you get ssm working this scenario? Next diagram.
private subnet with no public ip (no internet access)

In this instance, you need to add vpc endpoints - unsurprisingly to the vpc - and then associate them with the private subnet you want to connect into. Endpoints are created at vpc level and then "associated". The ssm endpoints are of type "interface" so an eni is created in that subnet for each endpoint and a private dns zone is set up so that the vm sends traffic to the local ssm enis and not to the aws fabric globally.
Here's some terraform to do it, sg is allowing 443.
locals {
endpoints= toset([
"com.amazonaws.eu-west-2.ssm",
"com.amazonaws.eu-west-2.ssmmessages",
"com.amazonaws.eu-west-2.ec2messages"
])
}
resource aws_vpc_endpoint "endpoints" {
for_each = local.endpoints
vpc_id = data.aws_vpc.main.id
service_name = each.key
vpc_endpoint_type = "Interface"
security_group_ids = [
aws_security_group.test-ssm.id
]
private_dns_enabled = true
}
resource "aws_vpc_endpoint_subnet_association" "association" {
for_each = local.endpoints
vpc_endpoint_id = aws_vpc_endpoint.endpoints[each.key].id
subnet_id = aws_subnet.completely-private.id
}