Skip to content

appvia/terraform-aws-private-endpoints

Github Actions

Terraform AWS Private Endpoints


AWS Private Endpoints

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.

AWS References

Usage

## Provision the endpoints and resolvers
module "endpoints" {
  source = "../.."

  name = "endpoints"
  tags = var.tags
  endpoints = {
    "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 resolvers
module "endpoints" {
  source = "../.."

  name = "endpoints"
  tags = var.tags
  endpoints = {
    "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:

  1. Make changes to the .terraform-docs.yml file
  2. Fetch the terraform-docs binary (https://terraform-docs.io/user-guide/installation/)
  3. Run terraform-docs markdown table --output-file ${PWD}/README.md --output-mode inject .

Requirements

Name Version
terraform >= 1.0.7
aws >= 5.0.0

Providers

Name Version
aws >= 5.0.0

Modules

Name Source Version
dns_security_group terraform-aws-modules/security-group/aws 5.2.0
ram_share ./modules/ram_share n/a
vpc appvia/network/aws 0.3.1

Resources

Name Type
aws_ram_resource_association.endpoints resource
aws_ram_resource_share.endpoints resource
aws_route53_resolver_endpoint.outbound resource
aws_route53_resolver_rule.endpoints resource
aws_route53_resolver_rule.endpoints_single resource
aws_security_group.this resource
aws_vpc_endpoint.this resource
aws_vpc_security_group_egress_rule.allow_https_egress resource
aws_vpc_security_group_ingress_rule.allow_https_ingress resource
aws_route53_resolver_endpoint.outbound data source

Inputs

Name Description Type Default Required
name The name of the environment string n/a yes
network 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
})
n/a yes
region The region to deploy the resources string n/a yes
resolvers The resolvers to provision
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)
})
})
n/a yes
tags The tags to apply to the resources map(string) n/a yes
endpoints 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)
}))
{
"ec2": {
"service": "ec2"
},
"ec2messages": {
"service": "ec2messages"
},
"ssm": {
"service": "ssm"
},
"ssmmessages": {
"service": "ssmmessages"
}
}
no
sharing 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")
})
{
"principals": []
}
no

Outputs

Name Description
endpoints Array containing the full resource object and attributes for all endpoints created
outbound_resolver_endpoint_id The id of the outbound resolver if we created one
outbound_resolver_ip_addresses The ip addresses of the outbound resolver if we created one
private_subnet_attributes_by_az The attributes of the private subnets
resolver_security_group_id The id of the security group we created for the endpoints if we created one
rt_attributes_by_type_by_az The attributes of the route tables
transit_gateway_attachment_id The id of the transit gateway we used to provision the endpoints
vpc_attributes The attributes of the vpc we created
vpc_id The id of the vpc we used to provision the endpoints