AWS PrivateLink with Terraform

2024-05-03

🔍 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!


Related Posts