AWS PrivateLink: Private Service Connectivity Without VPC Peering or Public Internet

When you need to share a service across multiple VPCs or AWS accounts, the naive approaches create problems. VPC peering connects entire networks, which means every resource in both VPCs can potentially reach every other resource. You cannot easily restrict which specific service is accessible. And as accounts multiply, the peering mesh grows into something no one can reason about.

AWS PrivateLink solves this with surgical precision. You expose a single service through an endpoint service. Consumers in other VPCs create an interface endpoint that connects to that specific service. Traffic stays entirely within the AWS network. The consumer VPC has no routing relationship with the provider VPC. In this article I will walk through setting up a PrivateLink service with Terraform and the access control patterns that matter in production.

Creating a VPC Endpoint Service

You expose your service through a Network Load Balancer. PrivateLink creates an endpoint service on top of the NLB that consumer accounts can connect to.

resource "aws_lb" "privatelink" {
  name               = "privatelink-nlb"
  internal           = true
  load_balancer_type = "network"
  subnets            = var.private_subnet_ids

  enable_deletion_protection = true
  enable_cross_zone_load_balancing = true

  tags = {
    Environment = "production"
    Purpose     = "privatelink"
  }
}

resource "aws_lb_listener" "privatelink" {
  load_balancer_arn = aws_lb.privatelink.arn
  port              = 443
  protocol          = "TCP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.service.arn
  }
}

resource "aws_vpc_endpoint_service" "main" {
  acceptance_required        = true
  network_load_balancer_arns = [aws_lb.privatelink.arn]

  allowed_principals = [
    "arn:aws:iam::${var.consumer_account_id_1}:root",
    "arn:aws:iam::${var.consumer_account_id_2}:root"
  ]

  private_dns_name = "service.yourdomain.com"

  tags = {
    Name        = "internal-api-endpoint-service"
    Environment = "production"
  }
}

acceptance_required = true means connection requests from consumers require explicit approval before traffic can flow. This is the correct setting for production. Without it, any principal in your allowed_principals list can connect without any approval workflow. With it, you review each connection request and approve or reject it, giving you full visibility and control over which specific consumer VPCs have connected to your service.

The private_dns_name enables consumers to access your service using a friendly DNS name rather than the endpoint-specific DNS that PrivateLink generates. After domain verification, consumers who create an interface endpoint to your service can resolve service.yourdomain.com to the private endpoint IP in their VPC.

Creating an Interface Endpoint (Consumer Side)

resource "aws_vpc_endpoint" "service" {
  provider = aws.consumer_account

  vpc_id              = var.consumer_vpc_id
  service_name        = "com.amazonaws.vpce.us-east-1.vpce-svc-xxxxxxxxx"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.consumer_private_subnet_ids
  security_group_ids  = [aws_security_group.endpoint.id]
  private_dns_enabled = true

  tags = {
    Name        = "internal-api-endpoint"
    Environment = "production"
  }
}

resource "aws_security_group" "endpoint" {
  provider = aws.consumer_account
  name     = "privatelink-endpoint-sg"
  vpc_id   = var.consumer_vpc_id

  ingress {
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [var.application_sg_id]
    description     = "HTTPS from application tier only"
  }

  tags = { Name = "privatelink-endpoint-sg" }
}

The interface endpoint creates elastic network interfaces in each specified subnet. Traffic from your application to the service name resolves to these ENI IPs, which are in your VPC’s address space. No traffic leaves your VPC to reach the provider service. The security group on the endpoint controls which resources in the consumer VPC can use it, which is a second layer of access control on top of the allowed_principals on the provider side.

Approving Connection Requests

resource "aws_cloudwatch_event_rule" "endpoint_connection_request" {
  name        = "privatelink-connection-request"
  description = "Alert when a new PrivateLink connection is requested"

  event_pattern = jsonencode({
    source      = ["aws.ec2"]
    detail-type = ["AWS API Call via CloudTrail"]
    detail = {
      eventSource = ["ec2.amazonaws.com"]
      eventName   = ["CreateVpcEndpoint"]
    }
  })
}

resource "aws_cloudwatch_event_target" "notify_team" {
  rule = aws_cloudwatch_event_rule.endpoint_connection_request.name
  arn  = aws_sns_topic.security_alerts.arn
}

With acceptance_required = true, the approval workflow is manual by default. You receive a notification, review the requesting account, and approve or reject via the console or CLI. For organizations with many consumer accounts, build an automation that receives the connection request event, validates the requesting account against an approved list, and calls AcceptVpcEndpointConnections automatically for approved accounts. This scales the approval process without requiring manual review for every new deployment.

Closing Thoughts

PrivateLink is the right connectivity pattern for sharing services across account boundaries. It gives you the isolation of separate VPCs with the convenience of direct private connectivity, without the routing complexity of VPC peering meshes. The producer controls exactly who can connect. The consumer controls which resources in their VPC can use the endpoint. Traffic never leaves the AWS network.

Use PrivateLink when you need to share a specific service across accounts. Use VPC peering when you need full network connectivity between trusted environments like production and shared services. Use Transit Gateway when you need to connect many VPCs in a hub-and-spoke topology. The right answer depends on what connectivity you actually need rather than what is easiest to set up initially.

Enjoy the cloud.

Osama


#AWS #PrivateLink #CloudSecurity #NetworkEngineering #CloudArchitecture #Terraform #InfrastructureAsCode #AmazonWebServices #SolutionsArchitect #CloudComputing #VPC #CrossAccount #ServiceConnectivity #TechBlog #CloudInfrastructure #ZeroTrust #Microservices #DevOps #SRE #CloudNetworking

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.