VPC peering and Transit Gateway connect whole networks. PrivateLink exposes one service on one port, so consumers can have overlapping CIDRs and see nothing else in your VPC. It is the cleanest way for a platform team to offer an internal API to many accounts.
How the pieces fit
Consumer VPC Provider VPC
app -> interface endpoint (ENI) ==PrivateLink==> NLB -> targets (EC2/ECS/IP)
DNS: api.internal.example.com endpoint service: com.amazonaws.vpce.<region>.vpce-svc-xxxx
Traffic always starts from the consumer side. The provider cannot open connections back through the endpoint.
Step 1: the provider NLB
Use an internal NLB in the same AZs your consumers use.
aws elbv2 create-load-balancer --name nlb-orders-api --type network --scheme internal \
--subnets subnet-a subnet-b subnet-c
aws elbv2 create-target-group --name tg-orders-api --protocol TCP --port 8443 \
--vpc-id vpc-provider --target-type ip --health-check-protocol HTTPS --health-check-path /health
aws elbv2 create-listener --load-balancer-arn $NLB --protocol TCP --port 443 \
--default-actions Type=forward,TargetGroupArn=$TG
NLBs can have security groups now; if yours does, allow the consumer endpoint traffic (it appears from the NLB's perspective as coming from the PrivateLink side; check the "Enforce inbound rules on PrivateLink traffic" setting).
Step 2: the endpoint service
aws ec2 create-vpc-endpoint-service-configuration \
--network-load-balancer-arns $NLB --acceptance-required \
--private-dns-name api.internal.example.com \
--supported-ip-address-types ipv4
aws ec2 modify-vpc-endpoint-service-permissions --service-id vpce-svc-0abc \
--add-allowed-principals arn:aws:iam::222222222222:root arn:aws:iam::333333333333:root
You can also allow a whole organisation with an aws:PrincipalOrgID-style principal pattern where supported; check the current docs. Keep --acceptance-required on so new consumers are a deliberate decision.
Private DNS name verification
To let consumers use api.internal.example.com instead of the long vpce DNS name, prove you own the domain:
aws ec2 describe-vpc-endpoint-service-configurations --service-ids vpce-svc-0abc \
--query "ServiceConfigurations[].PrivateDnsNameConfiguration"
# create the TXT record it shows in the PUBLIC hosted zone for example.com, then:
aws ec2 start-vpc-endpoint-service-private-dns-verification --service-id vpce-svc-0abc
Step 3: the consumer endpoint
In the consumer account:
aws ec2 create-vpc-endpoint --vpc-endpoint-type Interface \
--vpc-id vpc-consumer --service-name com.amazonaws.vpce.eu-west-1.vpce-svc-0abc \
--subnet-ids subnet-x subnet-y --security-group-ids sg-endpoint \
--private-dns-enabled
Back in the provider account, accept it:
aws ec2 describe-vpc-endpoint-connections --filters Name=service-id,Values=vpce-svc-0abc \
--query "VpcEndpointConnections[].[VpcEndpointId,VpcEndpointOwner,VpcEndpointState]" --output table
aws ec2 accept-vpc-endpoint-connections --service-id vpce-svc-0abc --vpc-endpoint-ids vpce-0consumer
AZ alignment
An endpoint can only be created in AZs where the provider NLB has a subnet. AZ names differ between accounts; AZ IDs (euw1-az1) do not. Compare IDs:
aws ec2 describe-availability-zones --query "AvailabilityZones[].[ZoneName,ZoneId]" --output table
Enable cross-zone load balancing on the NLB if consumers in one AZ must reach targets in others.
Debugging
| Symptom | Cause | Fix |
|---|---|---|
Endpoint stuck pendingAcceptance | Provider has not accepted | Accept, or add consumer to auto-accept list |
InvalidServiceName when creating endpoint | Consumer account not in allowed principals, or wrong region | Add principal; endpoint services are regional |
| Connection timeout | Endpoint SG blocks the port, or NLB targets unhealthy | Allow 443 from app SG on sg-endpoint; check target health |
| Private DNS not resolving | Verification pending, --private-dns-enabled off, or VPC DNS hostnames disabled | Check verification state; enable enableDnsHostnames and enableDnsSupport |
| Works in one AZ only | Provider not in the consumer's AZ ID | Add NLB subnet in that AZ ID or enable cross-zone |
| Targets see NLB IPs, not clients | Expected with PrivateLink | Enable Proxy Protocol v2 on the target group and read the TLV with the endpoint ID |
aws elbv2 describe-target-health --target-group-arn $TG --output table
aws elbv2 modify-target-group-attributes --target-group-arn $TG --attributes Key=proxy_protocol_v2.enabled,Value=true
Prevent it next time
- Publish an onboarding page: service name, port, AZ IDs supported, how to request access.
- Monitor NLB
UnHealthyHostCountand endpoint service metrics; alert the provider team, not the consumers. - Use Proxy Protocol v2 to log which consumer endpoint each request came from.