PrivateLink Endpoints for PostGIS Access
A tile renderer in one account needs the parcels database in another. The obvious route is VPC peering, and it works — at the cost of joining two routing domains, requiring non-overlapping address space, and giving every workload in each VPC a network path to every workload in the other. PrivateLink offers a narrower alternative: expose one service, consume it as a single endpoint, and share no routing at all. For a shared spatial database consumed by several teams or several accounts, that difference is the whole point. This guide extends VPC Routing for Tile Servers within Network Security and Access Control.
When PrivateLink is the right answer
Peering and PrivateLink solve different problems and the choice is usually clear once framed correctly.
Peering joins networks. Both VPCs’ address spaces become mutually routable, subject to route tables and security groups. It is the right choice when two environments genuinely need broad mutual access — a renderer fleet and a database it effectively owns, in the same organisational boundary. It requires non-overlapping CIDRs, and its blast radius is a network, not a service.
PrivateLink exposes a service. The consumer gets an endpoint in its own subnets that resolves to one service in the provider’s VPC. Traffic is unidirectional by construction: the consumer can reach the service, and the service cannot reach back into the consumer. Address spaces may overlap freely, because no routing is shared. It is the right choice when the relationship is “several consumers use one database” rather than “two networks are one network”.
The cost difference matters at spatial-workload volumes. PrivateLink charges per endpoint-hour and per gigabyte processed; peering charges only for cross-zone or cross-region transfer. A renderer pulling large geometry result sets continuously will pay noticeably more through an endpoint, and that should be modelled rather than discovered — the approach in Cost Estimation Frameworks applies directly.
Prerequisites and environment assumptions
Terraform 1.6 or later with hashicorp/aws pinned at ~> 5.60. A PostGIS cluster in the provider account, reachable from a network load balancer in the same VPC — PrivateLink fronts a network load balancer, not a database directly, so the NLB with an IP target group pointed at the database endpoint is a required intermediate. Consumer-side subnets in at least two availability zones, since an endpoint is provisioned per zone and a single-zone endpoint is a single point of failure for a database connection.
Be aware of one behaviour that surprises everyone the first time: the load balancer sees the endpoint’s network interface as the source, so the database sees connections from the load balancer, not from the consumer. Source-based authorisation therefore does not distinguish consumers, and per-consumer authorisation must happen at the database — separate roles per consumer — or at the endpoint service’s allowlist.
Step-by-step implementation
-
Front the database with a network load balancer. Internal, TCP on 5432, with an IP target group holding the database endpoint’s addresses. Health-check the port, and be aware the addresses behind a managed database endpoint can change on failover, which is why a periodic reconciliation of the target group is worth automating.
-
Create the endpoint service and set acceptance to manual. Automatic acceptance means anyone who learns the service name and is in the allowlist connects without a human step. Manual acceptance makes each new consumer a deliberate decision, which for a database holding authoritative geometry is the right default.
-
Allowlist the consuming principals explicitly. The endpoint service permission list names which accounts or roles may even request a connection. Combined with manual acceptance this is two independent gates.
-
Create the endpoint in the consumer VPC, in every zone the workload runs in. Attach a security group permitting 5432 from the renderer’s security group only, and enable private DNS so the consumer’s application uses a stable hostname rather than the endpoint’s generated one.
-
Give each consumer its own database role. Because the database cannot distinguish consumers by source address through the endpoint, the identity must come from the credential. One role per consumer, read-only, sourced from a secrets manager as described in Secrets Management for Spatial Pipelines.
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.60" }
}
}
# ---- Provider side ----
resource "aws_lb" "postgis" {
name = "postgis-privatelink"
internal = true
load_balancer_type = "network"
subnets = var.provider_subnet_ids
enable_cross_zone_load_balancing = true
}
resource "aws_lb_target_group" "postgis" {
name = "postgis-tg"
port = 5432
protocol = "TCP"
vpc_id = var.provider_vpc_id
# IP targets: a managed database is reached by address, and those addresses
# can change on failover — reconcile this target group periodically.
target_type = "ip"
health_check {
protocol = "TCP"
port = "5432"
interval = 10
healthy_threshold = 2
unhealthy_threshold = 2
}
}
resource "aws_vpc_endpoint_service" "postgis" {
acceptance_required = true # a human step per consumer, deliberately
network_load_balancer_arns = [aws_lb.postgis.arn]
tags = {
Service = "postgis-read"
Owner = "geospatial-platform"
}
}
# Two independent gates: the allowlist decides who may ask, manual acceptance
# decides whether they are connected.
resource "aws_vpc_endpoint_service_allowed_principal" "tile_team" {
vpc_endpoint_service_id = aws_vpc_endpoint_service.postgis.id
principal_arn = var.tile_team_account_arn
}
# ---- Consumer side ----
resource "aws_vpc_endpoint" "postgis" {
vpc_id = var.consumer_vpc_id
service_name = aws_vpc_endpoint_service.postgis.service_name
vpc_endpoint_type = "Interface"
# One endpoint network interface per zone: a single-zone endpoint is a single
# point of failure for every database connection the renderer makes.
subnet_ids = var.consumer_subnet_ids
security_group_ids = [aws_security_group.postgis_endpoint.id]
private_dns_enabled = true
}
resource "aws_security_group" "postgis_endpoint" {
name = "postgis-endpoint"
vpc_id = var.consumer_vpc_id
ingress {
description = "PostGIS 5432 from the renderer fleet only"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [var.renderer_sg_id]
}
}
variable "provider_vpc_id" { type = string }
variable "provider_subnet_ids" { type = list(string) }
variable "consumer_vpc_id" { type = string }
variable "consumer_subnet_ids" { type = list(string) }
variable "tile_team_account_arn" { type = string }
variable "renderer_sg_id" { type = string }
Verification
From a renderer task in the consumer VPC, resolve the private DNS name and confirm it returns an address inside the consumer’s own subnets — that is the signal that private DNS is working and that traffic is not leaving through some other path. Then connect with psql using sslmode=verify-full and run SELECT postgis_full_version(). Verification of the certificate matters here specifically because the endpoint introduces an intermediary, and require alone would accept any endpoint that answers.
Confirm the direction of the relationship by attempting a connection from the provider VPC into a consumer service; it must fail, because PrivateLink is unidirectional and that property is a large part of why it was chosen. Confirm zone coverage by checking there is an endpoint network interface in every subnet the renderer runs in. Finally check the endpoint’s processed-bytes metric after a day of real traffic and compare it against the cost model, since this is the number that decides whether the architecture stays affordable.
Preventing recurrence
- Automate target-group reconciliation. A managed database’s addresses can change on failover, and a stale IP target group produces a database that is up and unreachable.
- Keep acceptance manual and review the allowlist quarterly. A consumer that no longer exists should not retain a connection path to authoritative geometry.
- Alarm on endpoint processed bytes. It is both a cost signal and a behavioural one — a sudden rise usually means a consumer started pulling far more geometry than it used to.
- Give every consumer its own role and rotate on the same schedule. Since the network cannot distinguish consumers, the credential is the only boundary, and a shared credential collapses it entirely.
Frequently Asked Questions
Can I use PrivateLink to reach a managed database directly?
Not directly — it fronts a network load balancer, so an internal NLB with an IP target group pointed at the database endpoint is a required intermediate. That indirection is also why the database sees the load balancer as the source rather than the consumer.
Is PrivateLink cheaper than peering?
Usually not, for a high-volume database connection. Peering charges only cross-zone or cross-region transfer, while an endpoint charges per hour and per gigabyte processed. PrivateLink is chosen for its isolation properties, and the cost should be modelled deliberately rather than assumed to be neutral.
Does the consumer's traffic ever leave the private network?
No. Endpoint traffic stays on the provider network and never traverses the public internet, which is the security argument for the architecture. Encrypt it anyway with verify-full, because the intermediary makes server verification more important, not less.
What happens on a database failover?
The endpoint is unaffected, but the target group may be pointing at addresses that no longer serve the primary. Automating target reconciliation, or using a target that follows the endpoint rather than an address, is what keeps a failover from becoming an outage on the consumer side.
Related
- VPC Routing for Tile Servers — the parent topic on network paths for spatial workloads
- Terraform VPC Peering for Distributed GeoServer — the peering alternative and when it fits better
- Troubleshooting Blackholed Routes Between Tile Server VPCs — diagnosing the routing failures PrivateLink avoids
- Security Group Hardening — the group rules on both ends of the endpoint