AWS VPC Endpoints and PrivateLink with Terraform: Private Access to AWS Services

· 2 min read · Terraform & OpenTofu Tutorials

Reach AWS services without going through the Internet #

By default, an instance in a private subnet reaches S3 or SQS through a NAT gateway and the public Internet endpoint of the service. A VPC endpoint gives a private path inside the AWS network. It improves security (traffic never leaves AWS), allows subnets without Internet access, and can reduce NAT gateway data processing charges, which are significant for S3 traffic.

There are two types, both based on AWS PrivateLink technology:

Gateway endpoint Interface endpoint
Services S3 and DynamoDB only Most AWS services and third-party services
How it works A route in the route table A network interface (ENI) with a private IP in your subnets
Cost Free Hourly charge per endpoint per AZ plus data processed

Gateway endpoint for S3 and DynamoDB #

endpoints.tf
data "aws_region" "current" {}

resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.${data.aws_region.current.name}.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = [for rt in aws_route_table.private : rt.id]
}

resource "aws_vpc_endpoint" "dynamodb" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.${data.aws_region.current.name}.dynamodb"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = [for rt in aws_route_table.private : rt.id]
}

Terraform adds a route for the service prefix list to the private route tables. There is nothing else to configure in the application, and since it is free you should create these two in almost every VPC. Adjust aws_route_table.private to the names in your configuration.

You can restrict what the endpoint allows with an endpoint policy, for example only to your buckets.

Interface endpoint #

endpoints.tf
resource "aws_security_group" "endpoints" {
  name   = "ditwl-pro-endpoints"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [aws_vpc.main.cidr_block]
  }
}

resource "aws_vpc_endpoint" "secretsmanager" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.${data.aws_region.current.name}.secretsmanager"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = aws_subnet.private[*].id
  security_group_ids  = [aws_security_group.endpoints.id]
  private_dns_enabled = true
}

With private_dns_enabled = true, the normal service name (secretsmanager.eu-west-1.amazonaws.com) resolves to the private IPs of the endpoint inside the VPC, so no application change is needed. This requires enable_dns_support and enable_dns_hostnames on the VPC (VPC tutorial). The security group must allow HTTPS from your instances.

Common interface endpoints: ssm, ssmmessages, ec2messages (for Session Manager without Internet), ecr.api, ecr.dkr (for ECS and ECR), logs, sqs, kms, secretsmanager, sts.

Several interface endpoints at once #

endpoints.tf
locals {
  interface_services = ["ssm", "ssmmessages", "ec2messages", "logs", "kms"]
}

resource "aws_vpc_endpoint" "interface" {
  for_each = toset(local.interface_services)

  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.${data.aws_region.current.name}.${each.key}"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = aws_subnet.private[*].id
  security_group_ids  = [aws_security_group.endpoints.id]
  private_dns_enabled = true
}

Use for_each so each endpoint is independent.

You can offer an application running behind a Network Load Balancer to other VPCs or accounts without peering:

privatelink.tf
resource "aws_vpc_endpoint_service" "app" {
  acceptance_required        = true
  network_load_balancer_arns = [aws_lb.nlb.arn]
}

The consumers create an interface endpoint with service_name = aws_vpc_endpoint_service.app.service_name. It exposes one service and not the whole network, which solves overlapping CIDR blocks. Compare with peering and Transit Gateway.

Costs #

Interface endpoints are billed per hour per Availability Zone, so ten endpoints in three AZs are 30 billed units. Create only those you need, and in as many AZs as the workloads use. Gateway endpoints are free. Check the current PrivateLink pricing and estimate with Infracost.

#AWS #VPC #Network #Terraform #OpenTofu