Skip to content
Back to blog AWS PrivateLink with Terraform

AWS PrivateLink with Terraform

AWSTerraform

Overview

AWS PrivateLink enables secure, private connectivity between VPCs and services hosted on AWS without exposing traffic to the public internet. It’s essential for secure service-to-service communication across accounts or VPCs.

In this post, we’ll implement a working PrivateLink setup with Terraform: one VPC exposing a service (via NLB), and another VPC accessing it (via interface endpoint). We’ll use two Terraform providers to simulate cross-account setup.


Architecture

[ Service Provider VPC ]                  [ Service Consumer VPC ]
[ EC2 (httpd) ] -> [ NLB ] -> [ Endpoint Service ] <- [ VPC Endpoint ] <- [ EC2 ]

The service is hosted on a private EC2 behind a Network Load Balancer. An Endpoint Service is created from the NLB. The consumer VPC accesses it through an Interface VPC Endpoint.


Prerequisites

  • Terraform ≥ 1.0
  • Two AWS profiles or roles simulating service provider and consumer
  • Basic networking knowledge (subnets, NLB, SGs)

Step 1: Service Provider VPC with NLB and Endpoint Service

provider "aws" {
  alias  = "provider"
  region = "us-east-1"
}

resource "aws_vpc" "provider" {
  provider   = aws.provider
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "provider_subnet" {
  provider          = aws.provider
  vpc_id            = aws_vpc.provider.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "us-east-1a"
}

resource "aws_security_group" "provider_sg" {
  provider = aws.provider
  vpc_id   = aws_vpc.provider.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "provider_instance" {
  provider        = aws.provider
  ami             = "ami-0c94855ba95c71c99"
  instance_type   = "t2.micro"
  subnet_id       = aws_subnet.provider_subnet.id
  security_groups = [aws_security_group.provider_sg.id]
  user_data       = <<-EOF
                #!/bin/bash
                echo "Hello from PrivateLink Service Provider" > index.html
                nohup python -m SimpleHTTPServer 80 &
                EOF
}

resource "aws_lb" "nlb" {
  provider           = aws.provider
  name               = "privatelink-nlb"
  internal           = true
  load_balancer_type = "network"
  subnets            = [aws_subnet.provider_subnet.id]
}

resource "aws_lb_target_group" "tg" {
  provider    = aws.provider
  name        = "privatelink-tg"
  port        = 80
  protocol    = "TCP"
  vpc_id      = aws_vpc.provider.id
  target_type = "instance"
}

resource "aws_lb_target_group_attachment" "tg_attachment" {
  provider         = aws.provider
  target_group_arn = aws_lb_target_group.tg.arn
  target_id        = aws_instance.provider_instance.id
  port             = 80
}

resource "aws_lb_listener" "listener" {
  provider          = aws.provider
  load_balancer_arn = aws_lb.nlb.arn
  port              = 80
  protocol          = "TCP"

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

resource "aws_vpc_endpoint_service" "service" {
  provider                    = aws.provider
  acceptance_required        = true
  network_load_balancer_arns = [aws_lb.nlb.arn]
}

Step 2: Service Consumer VPC with VPC Endpoint

provider "aws" {
  alias  = "consumer"
  region = "us-east-1"
}

resource "aws_vpc" "consumer" {
  provider   = aws.consumer
  cidr_block = "10.1.0.0/16"
}

resource "aws_subnet" "consumer_subnet" {
  provider          = aws.consumer
  vpc_id            = aws_vpc.consumer.id
  cidr_block        = "10.1.1.0/24"
  availability_zone = "us-east-1a"
}

resource "aws_security_group" "consumer_sg" {
  provider = aws.consumer
  vpc_id   = aws_vpc.consumer.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["10.1.0.0/16"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_vpc_endpoint" "consumer_endpoint" {
  provider           = aws.consumer
  vpc_id             = aws_vpc.consumer.id
  service_name       = aws_vpc_endpoint_service.service.service_name
  vpc_endpoint_type  = "Interface"
  subnet_ids         = [aws_subnet.consumer_subnet.id]
  security_group_ids = [aws_security_group.consumer_sg.id]
}

Testing

  • Accept the VPC Endpoint connection from the Provider side.
  • SSH into an EC2 in the Consumer VPC and use:
curl http://<interface-endpoint-dns-name>

You should see:

Hello from PrivateLink Service Provider

Security Notes

  • Ensure correct SGs between NLB and Endpoint
  • Use IAM condition keys or resource policies if needed
  • Enable VPC Flow Logs for visibility

Summary

  • We implemented AWS PrivateLink with real infra using Terraform
  • Service is exposed internally via NLB and Interface Endpoint
  • Setup is secure, scalable, and avoids internet exposure

Next steps? Add DNS, IAM auth, or cross-region routing!


Found this helpful?

Comments