Create an AWS PrivateLink Connection to YugabyteDB Aeon with Terraform

The YugabyteDB Aeon documentation shows how to create an AWS Private Service Endpoint using the Aeon UI or the ybm CLI.

But can the same thing be automated with Terraform?

Yes.

The YugabyteDB Aeon Terraform provider includes a native ybm_private_service_endpoint resource. The resource also returns the AWS PrivateLink service name, allowing Terraform to pass it directly to the standard AWS aws_vpc_endpoint resource.

Engineering validation in progress: This tip is currently under review by YugabyteDB Engineering to validate the Terraform configuration, resource behavior, and recommended deployment approach. The content may be updated based on that review.

That means the entire connection can be managed declaratively:

				
					YugabyteDB Aeon Private Service Endpoint
                     |
                     | service_name
                     v
          AWS Interface VPC Endpoint
                     |
                     v
              Application VPC
				
			
Important update: You do not need a Terraform null_resource, a local-exec provisioner, or an external ybm CLI command when using a current version of the YugabyteDB Aeon Terraform provider.

What Are We Creating?

This demo creates:

  • ● A Private Service Endpoint on an existing YugabyteDB Aeon cluster
  • ● An AWS security group for the interface endpoint
  • ● An ingress rule for YSQL
  • ● An optional ingress rule for YCQL
  • ● An AWS interface VPC endpoint
  • ● Terraform outputs containing the private host and AWS service name

The key connection between the two Terraform providers is:

				
					service_name = ybm_private_service_endpoint.aws.service_name
				
			

The service name does not need to be copied manually from the Aeon UI or parsed from the output of a CLI command.

Prerequisites

You will need:

  • ● A YugabyteDB Aeon dedicated cluster deployed on AWS
  • ● The Aeon cluster ID
  • ● A YugabyteDB Aeon API key
  • ● An application VPC in AWS
  • ● One or more subnets for the AWS interface endpoint
  • ● The security group used by the application
  • ● AWS credentials available to Terraform
  • ● Terraform installed

For AWS PrivateLink, YugabyteDB Aeon creates the endpoint service and your AWS account creates the corresponding interface VPC endpoint. A PSE is created for each Aeon cluster region that requires private connectivity.

Create the Terraform Configuration

Create a file named main.tf:

				
					terraform {
  required_version = ">= 1.5.0"

  required_providers {
    ybm = {
      source  = "yugabyte/ybm"
      version = "~> 1.0"
    }

    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

###############################################################################
# Variables
###############################################################################

variable "ybm_api_key" {
  description = "YugabyteDB Aeon API key."
  type        = string
  sensitive   = true
}

variable "ybm_cluster_id" {
  description = "ID of the YugabyteDB Aeon cluster."
  type        = string
}

variable "aws_region" {
  description = "AWS region containing the Aeon cluster and application VPC."
  type        = string
  default     = "us-east-1"
}

variable "application_vpc_id" {
  description = "ID of the AWS VPC containing the application."
  type        = string
}

variable "endpoint_subnet_ids" {
  description = "Subnet IDs where the AWS interface endpoint will be created."
  type        = set(string)
}

variable "application_security_group_id" {
  description = "Security group used by the application."
  type        = string
}

variable "enable_ycql" {
  description = "Create an ingress rule for YCQL port 9042."
  type        = bool
  default     = false
}

###############################################################################
# Providers
###############################################################################

provider "ybm" {
  host            = "cloud.yugabyte.com"
  use_secure_host = true
  auth_token      = var.ybm_api_key
}

provider "aws" {
  region = var.aws_region
}

###############################################################################
# Discover the AWS Account ID
###############################################################################

data "aws_caller_identity" "current" {}

###############################################################################
# Create the Private Service Endpoint in YugabyteDB Aeon
###############################################################################

resource "ybm_private_service_endpoint" "aws" {
  cluster_id = var.ybm_cluster_id
  region     = var.aws_region

  security_principals = [
    "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
  ]
}

###############################################################################
# Security Group for the AWS Interface Endpoint
###############################################################################

resource "aws_security_group" "yugabyte_endpoint" {
  name_prefix = "yugabyte-aeon-endpoint-"
  description = "Security group for the YugabyteDB Aeon PrivateLink endpoint"
  vpc_id      = var.application_vpc_id

  tags = {
    Name = "yugabyte-aeon-private-endpoint"
  }
}

###############################################################################
# Allow YSQL from the Application
###############################################################################

resource "aws_vpc_security_group_ingress_rule" "ysql" {
  security_group_id            = aws_security_group.yugabyte_endpoint.id
  referenced_security_group_id = var.application_security_group_id

  description = "Allow YSQL connections from the application"
  ip_protocol = "tcp"
  from_port   = 5433
  to_port     = 5433
}

###############################################################################
# Optionally Allow YCQL from the Application
###############################################################################

resource "aws_vpc_security_group_ingress_rule" "ycql" {
  count = var.enable_ycql ? 1 : 0

  security_group_id            = aws_security_group.yugabyte_endpoint.id
  referenced_security_group_id = var.application_security_group_id

  description = "Allow YCQL connections from the application"
  ip_protocol = "tcp"
  from_port   = 9042
  to_port     = 9042
}

###############################################################################
# Create the AWS Interface VPC Endpoint
###############################################################################

resource "aws_vpc_endpoint" "yugabyte" {
  vpc_id            = var.application_vpc_id
  vpc_endpoint_type = "Interface"

  service_name = ybm_private_service_endpoint.aws.service_name

  subnet_ids = var.endpoint_subnet_ids

  security_group_ids = [
    aws_security_group.yugabyte_endpoint.id
  ]

  private_dns_enabled = true

  tags = {
    Name = "yugabyte-aeon-private-endpoint"
  }
}

###############################################################################
# Outputs
###############################################################################

output "yugabyte_pse_endpoint_id" {
  description = "ID of the YugabyteDB Aeon Private Service Endpoint."
  value       = ybm_private_service_endpoint.aws.endpoint_id
}

output "yugabyte_pse_state" {
  description = "State of the YugabyteDB Aeon Private Service Endpoint."
  value       = ybm_private_service_endpoint.aws.state
}

output "yugabyte_pse_host" {
  description = "Private YugabyteDB host name."
  value       = ybm_private_service_endpoint.aws.host
}

output "yugabyte_pse_service_name" {
  description = "AWS PrivateLink service name published by Aeon."
  value       = ybm_private_service_endpoint.aws.service_name
}

output "yugabyte_pse_availability_zones" {
  description = "Availability zones exposed by the Aeon endpoint service."
  value       = ybm_private_service_endpoint.aws.availability_zones
}

output "aws_vpc_endpoint_id" {
  description = "ID of the AWS interface VPC endpoint."
  value       = aws_vpc_endpoint.yugabyte.id
}

output "aws_vpc_endpoint_state" {
  description = "State of the AWS interface VPC endpoint."
  value       = aws_vpc_endpoint.yugabyte.state
}
				
			

The Aeon provider requires an API key, a host, and optionally the secure-host setting. The PSE resource requires the cluster ID, region, and permitted security principals. It exposes the service name, host, state, endpoint ID, and availability zones as computed values.

Supply the Environment-Specific Values

Create terraform.tfvars:

				
					ybm_cluster_id = "00000000-1111-2222-3333-444444444444"

aws_region = "us-east-1"

application_vpc_id = "vpc-0123456789abcdef0"

endpoint_subnet_ids = [
  "subnet-0123456789abcdef0"
]

application_security_group_id = "sg-0123456789abcdef0"

enable_ycql = false
				
			

Do not store the Aeon API key directly in terraform.tfvars.

Export it as a Terraform variable:

				
					export TF_VAR_ybm_api_key="<your-aeon-api-key>"
				
			

Export the database password:

				
					export TF_VAR_ysql_password="<your-ysql-password>"
				
			

Initialize Terraform

				
					terraform init
				
			

When upgrading an existing configuration:

				
					terraform init -upgrade
				
			

Confirm the selected providers:

				
					terraform providers
				
			

Review the Terraform Plan

				
					terraform plan -out=tfplan
				
			

The important dependency is created by this reference:

				
					service_name = ybm_private_service_endpoint.aws.service_name
				
			

Terraform knows that the Aeon PSE must exist before it can create the AWS interface endpoint.

Apply the Configuration

				
					terraform apply tfplan
				
			

Display the outputs:

				
					terraform output
				
			

Example output:

				
					aws_vpc_endpoint_id = "vpce-0123456789abcdef0"
aws_vpc_endpoint_state = "available"
yugabyte_pse_endpoint_id = "11111111-2222-3333-4444-555555555555"
yugabyte_pse_host = "pse-us-east-1.example.aws.yugabyte.cloud"
yugabyte_pse_service_name = "com.amazonaws.vpce.us-east-1.vpce-svc-0123456789abcdef0"
yugabyte_pse_state = "ACTIVE"
				
			

Verify the AWS Endpoint

Check the endpoint state:

				
					aws ec2 describe-vpc-endpoints \
  --vpc-endpoint-ids "$(terraform output -raw aws_vpc_endpoint_id)" \
  --query 'VpcEndpoints[0].State' \
  --output text
				
			

Expected result:

				
					available
				
			

Check private DNS:

				
					aws ec2 describe-vpc-endpoints \
  --vpc-endpoint-ids "$(terraform output -raw aws_vpc_endpoint_id)" \
  --query 'VpcEndpoints[0].PrivateDnsEnabled' \
  --output text
				
			

Expected result:

				
					True
				
			

The AWS Terraform provider exposes private_dns_enabled on interface endpoint resources. YugabyteDB’s AWS PrivateLink procedure directs users to create the application-side interface endpoint after creating the Aeon PSE.

Verify Private DNS

Run this from an EC2 instance, container, or application host inside the application VPC:

				
					PSE_HOST="$(terraform output -raw yugabyte_pse_host)"

nslookup "${PSE_HOST}"
				
			

You can also use dig:

				
					dig "${PSE_HOST}"
				
			

The hostname should resolve through the interface endpoint to private addresses.

Connect with ysqlsh

Download the cluster CA certificate and connect from a host inside the application VPC:

				
					PSE_HOST="$(terraform output -raw yugabyte_pse_host)"

PGSSLMODE=verify-full \
PGSSLROOTCERT=./root.crt \
ysqlsh \
  -h "${PSE_HOST}" \
  -p 5433 \
  -U <database_user> \
  -d yugabyte
				
			

Verify the connection:

				
					SELECT version();
				
			

Match the Availability Zones

The PSE resource exposes the availability zones supported by the endpoint service:

				
					ybm_private_service_endpoint.aws.availability_zones
				
			

The selected AWS endpoint subnets must be compatible with those zones.

Do not assume that an availability zone name such as us-east-1a represents the same physical zone in every AWS account. Compare the corresponding AWS availability zone IDs when necessary.

Create One PSE Per Aeon Region

For a multi-region Aeon cluster, create a PSE in every region where applications need private access. Each PSE has a corresponding interface endpoint in the application VPC.

A multi-region Terraform pattern can use for_each:

				
					variable "pse_regions" {
  type = set(string)

  default = [
    "us-east-1",
    "us-west-2"
  ]
}

resource "ybm_private_service_endpoint" "aws" {
  for_each = var.pse_regions

  cluster_id = var.ybm_cluster_id
  region     = each.value

  security_principals = [
    "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
  ]
}
				
			

The application-side AWS endpoint resources would also need to be created in the corresponding regions.

Why the Native Terraform Resource Is Better

Capability YBM CLI with local-exec Native Terraform Resource
Terraform state Endpoint is not fully represented Endpoint attributes are stored in state
Service name Must be parsed or copied Available as a computed output
Dependencies Managed through scripts and triggers Handled by the dependency graph
Destroy Requires separate cleanup logic Handled by Terraform
Drift detection Limited Visible during terraform plan

Destroy the Connection

				
					terraform destroy
				
			

Terraform removes the AWS interface endpoint before removing the Aeon PSE because the dependency is represented directly in the configuration.

Final Takeaway

AWS PrivateLink can be managed natively with Terraform. Use ybm_private_service_endpoint to create the YugabyteDB Aeon side of the connection. Pass its computed service_name directly to aws_vpc_endpoint. The result is a declarative PrivateLink deployment with state tracking, dependency management, reusable outputs, drift detection, and clean resource destruction.

Resources

Have Fun!

As we sort through old boxes for our move to Dallas, we came across our daughter’s beloved childhood teddy bear... along with this sweet photo of her as a baby cuddling it! The good news is that Teddy will soon be reunited with her nearly 30 years later, since she lives in Dallas too. ❤️🧸