The diagram above is a high level representation of the module and the resources it creates; note in this design we DO NOT create an inbound resolver, as its not technically required
Description
The following module provides a AWS recommended pattern for sharing private endpoint services across multiple VPCs, interconnected via a transit gateway. The intent is to retain as much of the traffic directed to AWS services, private and off the internet. Used in combination with the terraform-aws-connectivity.
How it works
A shared vpc called var.name is created and attached to the transit gateway. Note, this module does not perform any actions on the transit gateway, it is assumed the correct settings to enable connectivity between the var.name vpc and the spokes is in place.
Inside the shared vpc the private endpoints are created, one for each service defined in var.endpoints. The default security groups permits all https traffic from 10.0.0.0/8 to ingress.
Optionally, depending on the configuration of the module, a outbound resolver is created. The outbound resolver is used to resolve the AWS services, against the default VPC resolver (VPC+2 ip)
Route53 resolver rules are created for each of the shared private endpoints, allowing the consumer to pick and choose which endpoints they want to resolve to the shared vpc.
The endpoints are shared using AWS RAM to the all the principals defined in the var.sharing.principals list e.g. a collection of organizational units.
The spoke vpc's are responsible for associating the resolver rules with their vpc.
These rules intercept the DNS queries and route them to the shared vpc resolvers, returning the private endpoint ip address located within them.
Traffic from the spoke to the endpoints once resolved, is routed via the transit gateway.
## Provision the endpoints and resolversmodule"endpoints" {
source="../.."name="endpoints"tags=var.tagsendpoints={
"s3"= {
service ="s3"
},
"ec2"= {
service ="ec2"
},
"ec2messages"= {
service ="ec2messages"
},
"ssm"= {
service ="ssm"
},
"ssmmessages"= {
service ="ssmmessages"
},
"logs"= {
service ="logs"
},
"kms"= {
service ="kms"
},
"secretsmanager"= {
service ="secretsmanager"
}
}
sharing={
principals =values(var.ram_principals)
}
resolvers={
outbound = {
create =true
ip_address_offset =12
}
}
network={
# Name of the network to create
name ="endpoints"# Number of availability zones to create subnets in
private_netmask =24# The transit gateway to connect
transit_gateway_id = var.transit_gateway_id
# The cider range to use for the VPC
vpc_cidr ="10.20.0.0/21"
}
}
Reuse Existing Network
In order to reuse and existing network (vpc), we need to pass the vpc_id and the subnets ids where the outbound resolver will be provisioned (assuming you are not reusing an existing resolver as well).
## Provision the endpoints and resolversmodule"endpoints" {
source="../.."name="endpoints"tags=var.tagsendpoints={
"ec2"= {
service ="ec2"
},
"ec2messages"= {
service ="ec2messages"
},
"ssm"= {
service ="ssm"
},
"ssmmessages"= {
service ="ssmmessages"
},
}
sharing={
principals =values(var.ram_principals)
}
resolvers={
outbound = {
create =true
ip_address_offset =10
}
}
network={
## The vpc_cidr of the network we are reusing
vpc_cidr =<VPC_CIDR>## Reuse the network we created above
vpc_id =<VPC_ID>## Reuse the private subnets we created above i.e subnet-id => cidr
private_subnet_cidrs_by_id = module.network.private_subnet_cidrs_by_id
## Do not create a new network
create =false
}
}
Update Documentation
The terraform-docs utility is used to generate this README. Follow the below steps to update:
The network to use for the endpoints and optinal resolvers
object({ availability_zones = optional(number, 2) # Whether to use ipam when creating the network create = optional(bool, true) # Indicates if we should create a new network or reuse an existing one enable_default_route_table_association = optional(bool, true) # Whether to associate the default route table enable_default_route_table_propagation = optional(bool, true) # Whether to propagate the default route table ipam_pool_id = optional(string, null) # The id of the ipam pool to use when creating the network private_netmask = optional(number, 24) # The subnet mask for private subnets, when creating the network i.e subnet-id => 10.90.0.0/24 private_subnet_cidr_by_id = optional(map(string), {}) # The ids of the private subnets to if we are reusing an existing network transit_gateway_id = optional(string, "") ## The transit gateway id to use for the network vpc_cidr = optional(string, "") # The cidrws range to use for the VPC, when creating the network vpc_id = optional(string, "") # The vpc id to use when reusing an existing network vpc_netmask = optional(number, null) # When using ipam this the netmask to use for the VPC vpc_dns_resolver = optional(string, "") # The ip address to use for the vpc dns resolver })
object({ # Indicates we create a single resolver rule, rather than one per service_type create_single_resolver_rule = optional(bool, false) # The configuration for the outbound resolver outbound = object({ # Whether to create the resolver create = optional(bool, true) # If creating the outbound resolver, the address offset to use i.e if 10.100.0.0/24, offset 10, ip address would be 10.100.0.10 ip_address_offset = optional(number, 10) # The protocols to use for the resolver protocols = optional(list(string), ["Do53", "DoH"]) # When not creating the resolver, this is the name of the resolver to use use_existing = optional(string, null) }) })
The private endpoints to provision within the shared vpc
map(object({ # Whether to enable private dns private_dns_enabled = optional(bool, true) # The route table ids to use for the endpoint, assuming a gateway endpoint route_table_ids = optional(list(string), null) # service_type of the endpoint i.e. Gateway, Interface service_type = optional(string, "Interface") # The security group ids to use for the endpoint, else create on the fly security_group_ids = optional(list(string), null) # The AWS service we are creating a endpoint for service = string # The IAM policy associated to the endpoint policy = optional(string, null) }))
The configuration for sharing the resolvers to other accounts
object({ ## The principals to share the resolvers with principals = optional(list(string), null) # The preifx to use for the shared resolvers share_prefix = optional(string, "resolvers") })