A practical EC2 module — SSM-only by design. No SSH keys are created, stored, or attached; you reach the instance through AWS Session Manager.
Requirements:
- Terraform >= 1.15.0
- Trivy >= 0.68.2
Trivy can be installed via Homebrew on macOS with the command:
brew install aquasecurity/trivy/trivy- No SSH key pair — access is via SSM Session Manager. No private key is generated, written to disk, or stored in Terraform state.
- IMDSv2 required; root EBS volume encrypted
- Instance role limited to
AmazonSSMManagedInstanceCoreplus an optional bucket-scoped S3 policy - Ingress and egress are both caller-controlled. Ingress opens only the
ports you pass (examples open 80/443, never 22). Egress defaults to open
because SSM and OS updates need it, and can be narrowed via
egress_rules. - Scanned with Trivy and gitleaks; integration-tested with Terratest across both networking paths (module-created and caller-supplied)
- Provisions an EC2 instance accessed exclusively via AWS SSM Session Manager — no SSH key pair is created or attached
- IMDSv2 required and root EBS volume encrypted by default
- Dynamically creates ingress security group rules from a caller-supplied list
- Configurable egress rules, defaulting to the outbound access SSM and OS updates require
- Optionally provisions a minimal public VPC, or drops into an existing network you supply
- Optionally creates a public Route 53 DNS record for the instance
- Optionally passes a user data script into instance creation
- Optionally attaches a bucket-scoped S3 access policy to the instance role
Self-contained (creates a minimal public VPC) — see
examples/complete:
provider "aws" {
region = "us-east-1"
}
module "instance" {
source = "RussellGilmore/red-instance/aws"
project_name = "my-project"
instance_name = "web"
ingress_rules = [{
description = "HTTPS from anywhere"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}]
}Connect with: aws ssm start-session --target <instance_id>.
To place the instance in an existing network (e.g. red-network), set
create_vpc = false and pass vpc_id/subnet_id — see
examples/with-network.
To use with red-network, set create_vpc = false and pass
vpc_id/subnet_id — below is an example configuration.
variable "region" {
type = string
default = "us-east-1"
}
variable "project_name" {
type = string
default = "red-networked"
}
provider "aws" {
region = var.region
}
module "network" {
source = "RussellGilmore/red-network/aws"
version = "~> 3.0"
project_name = var.project_name
vpc_name = "${var.project_name}-vpc"
vpc_cidr = "10.0.0.0/16"
subnets = {
public-1a = {
name = "${var.project_name}-public-1a"
cidr_block = "10.0.1.0/24"
availability_zone = "${var.region}a"
type = "public"
}
}
}
module "red_instance" {
source = "RussellGilmore/red-instance/aws"
version = "~> 3.0"
project_name = var.project_name
instance_name = "red-networked"
# Consume the network red-network built.
create_vpc = false
vpc_id = module.network.vpc_id
subnet_id = module.network.public_subnet_ids[0]
# SSM-only access; open just the public service ports.
ingress_rules = [
{
description = "HTTPS from anywhere"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
]
# egress_rules is omitted, so it defaults to unrestricted outbound
# (required for SSM connectivity and OS updates).
additional_tags = {
Environment = "example"
}
}
output "instance_id" {
value = module.red_instance.instance_id
}
output "public_ip" {
value = module.red_instance.public_ip
}
output "vpc_id" {
value = module.red_instance.vpc_id
}| Name | Version |
|---|---|
| terraform | >= 1.15.0 |
| aws | >= 6.47.0 |
| Name | Version |
|---|---|
| aws | >= 6.47.0 |
No modules.
| Name | Type |
|---|---|
| aws_eip.red_instance_eip | resource |
| aws_iam_instance_profile.red_instance_profile | resource |
| aws_iam_role.red_role | resource |
| aws_iam_role_policy.s3_bucket_policy | resource |
| aws_iam_role_policy_attachment.red_ssm_policy_attachment | resource |
| aws_instance.red-instance | resource |
| aws_internet_gateway.igw | resource |
| aws_route53_record.red_instance_dns | resource |
| aws_route_table.public | resource |
| aws_route_table_association.public | resource |
| aws_security_group.red_sg | resource |
| aws_subnet.public | resource |
| aws_vpc.main | resource |
| aws_ami.red_ami | data source |
| aws_route53_zone.zone | data source |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| additional_tags | Additional tags to apply to all resources created by this module. | map(string) |
{} |
no |
| allocate_eip | Controls whether an Elastic IP should be allocated. | bool |
true |
no |
| ami_name | The name (or name pattern) of the AMI to use for the instance. | string |
"ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-arm64-server-20250610" |
no |
| ami_owner | The owner account ID of the AMI. | string |
"099720109477" |
no |
| apex_domain | Apex domain whose hosted zone holds the DNS record. | string |
"" |
no |
| availability_zone | Availability zone for the created subnet. Leave empty for default placement. | string |
"" |
no |
| create_vpc | Create a minimal public VPC (single subnet + IGW) for the instance. Set false to place the instance in an existing VPC via vpc_id/subnet_id. | bool |
true |
no |
| disable_api_stop | Controls whether API stop is disabled. | bool |
false |
no |
| disable_api_termination | Controls whether API termination is disabled. | bool |
false |
no |
| dns_name | FQDN for the public DNS record. | string |
"" |
no |
| egress_rules | List of egress rules for the instance security group. Defaults to unrestricted outbound, which SSM Session Manager and OS package updates require. Narrow this if your environment provides SSM VPC endpoints. | list(object({ |
[ |
no |
| enable_public_dns | Create a public Route53 A record pointing at the instance's EIP. | bool |
false |
no |
| enable_s3_bucket_policy | Attach an S3 access policy (scoped to s3_bucket_name) to the instance role. | bool |
false |
no |
| ingress_rules | List of ingress rules for the instance security group. Access is via SSM Session Manager by default; only open inbound ports you actually serve (e.g. 80/443). | list(object({ |
n/a | yes |
| instance_name | The name of the instance. | string |
n/a | yes |
| instance_tags | Tags to apply only to the EC2 instance resource. | map(string) |
{} |
no |
| instance_type | The instance type to use for the instance. | string |
"t4g.small" |
no |
| project_name | Project name used for naming and the Project tag. | string |
n/a | yes |
| s3_bucket_name | Name of the S3 bucket the instance role may access. | string |
"" |
no |
| subnet_id | ID of an existing subnet to use when create_vpc is false. | string |
"" |
no |
| user_data_script_path | Path to a user data script to run on first boot. | string |
"" |
no |
| volume_size | The size of the root volume in GB. | number |
30 |
no |
| vpc_id | ID of an existing VPC to use when create_vpc is false. | string |
"" |
no |
| Name | Description |
|---|---|
| instance_id | The ID of the EC2 instance. |
| public_dns | The public DNS name of the instance. |
| public_ip | The public IP address of the instance (EIP when allocated). |
| security_group_id | The ID of the instance security group. |
| subnet_id | The subnet ID the instance is deployed in — created by the module when create_vpc is true, or the supplied subnet_id when false. |
| vpc_id | The VPC ID the instance is deployed in — created by the module when create_vpc is true, or the supplied vpc_id when false. |