The aws:ec2/vpcEndpoint:VpcEndpoint resource, part of the Pulumi AWS provider, creates VPC endpoints that enable private connectivity to AWS services and PrivateLink-enabled applications without internet gateways. This guide focuses on four capabilities: Gateway endpoints for S3 access, Interface endpoints with security groups and private DNS, custom IP addressing, and third-party PrivateLink integration.
VPC endpoints require an existing VPC and reference subnets, security groups, or route tables depending on endpoint type. The examples are intentionally small. Combine them with your own VPC infrastructure and access policies.
Connect to S3 via Gateway endpoint
Most deployments begin with Gateway endpoints for S3 or DynamoDB, allowing private access without internet gateways or NAT devices.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const s3 = new aws.ec2.VpcEndpoint("s3", {
vpcId: main.id,
serviceName: "com.amazonaws.us-west-2.s3",
});
import pulumi
import pulumi_aws as aws
s3 = aws.ec2.VpcEndpoint("s3",
vpc_id=main["id"],
service_name="com.amazonaws.us-west-2.s3")
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := ec2.NewVpcEndpoint(ctx, "s3", &ec2.VpcEndpointArgs{
VpcId: pulumi.Any(main.Id),
ServiceName: pulumi.String("com.amazonaws.us-west-2.s3"),
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var s3 = new Aws.Ec2.VpcEndpoint("s3", new()
{
VpcId = main.Id,
ServiceName = "com.amazonaws.us-west-2.s3",
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ec2.VpcEndpoint;
import com.pulumi.aws.ec2.VpcEndpointArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var s3 = new VpcEndpoint("s3", VpcEndpointArgs.builder()
.vpcId(main.id())
.serviceName("com.amazonaws.us-west-2.s3")
.build());
}
}
resources:
s3:
type: aws:ec2:VpcEndpoint
properties:
vpcId: ${main.id}
serviceName: com.amazonaws.us-west-2.s3
The serviceName identifies the AWS service using the format com.amazonaws.<region>.<service>. Gateway endpoints route traffic through the VPC’s route tables rather than creating network interfaces. When vpcEndpointType is omitted, it defaults to Gateway.
Access EC2 API through Interface endpoint
Interface endpoints create elastic network interfaces in your subnets, enabling private connections to AWS services that don’t support Gateway endpoints.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const ec2 = new aws.ec2.VpcEndpoint("ec2", {
vpcId: main.id,
serviceName: "com.amazonaws.us-west-2.ec2",
vpcEndpointType: "Interface",
securityGroupIds: [sg1.id],
privateDnsEnabled: true,
});
import pulumi
import pulumi_aws as aws
ec2 = aws.ec2.VpcEndpoint("ec2",
vpc_id=main["id"],
service_name="com.amazonaws.us-west-2.ec2",
vpc_endpoint_type="Interface",
security_group_ids=[sg1["id"]],
private_dns_enabled=True)
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := ec2.NewVpcEndpoint(ctx, "ec2", &ec2.VpcEndpointArgs{
VpcId: pulumi.Any(main.Id),
ServiceName: pulumi.String("com.amazonaws.us-west-2.ec2"),
VpcEndpointType: pulumi.String("Interface"),
SecurityGroupIds: pulumi.StringArray{
sg1.Id,
},
PrivateDnsEnabled: pulumi.Bool(true),
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var ec2 = new Aws.Ec2.VpcEndpoint("ec2", new()
{
VpcId = main.Id,
ServiceName = "com.amazonaws.us-west-2.ec2",
VpcEndpointType = "Interface",
SecurityGroupIds = new[]
{
sg1.Id,
},
PrivateDnsEnabled = true,
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ec2.VpcEndpoint;
import com.pulumi.aws.ec2.VpcEndpointArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var ec2 = new VpcEndpoint("ec2", VpcEndpointArgs.builder()
.vpcId(main.id())
.serviceName("com.amazonaws.us-west-2.ec2")
.vpcEndpointType("Interface")
.securityGroupIds(sg1.id())
.privateDnsEnabled(true)
.build());
}
}
resources:
ec2:
type: aws:ec2:VpcEndpoint
properties:
vpcId: ${main.id}
serviceName: com.amazonaws.us-west-2.ec2
vpcEndpointType: Interface
securityGroupIds:
- ${sg1.id}
privateDnsEnabled: true
Setting vpcEndpointType to Interface creates network interfaces in your VPC. The securityGroupIds control which resources can reach the endpoint. When privateDnsEnabled is true, AWS creates private DNS records so service calls automatically resolve to the endpoint rather than public IPs.
Pin specific IP addresses to endpoint interfaces
Some applications require predictable IP addresses for firewall rules or DNS configuration rather than accepting AWS-assigned addresses.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const ec2 = new aws.ec2.VpcEndpoint("ec2", {
vpcId: example.id,
serviceName: "com.amazonaws.us-west-2.ec2",
vpcEndpointType: "Interface",
subnetConfigurations: [
{
ipv4: "10.0.1.10",
subnetId: example1.id,
},
{
ipv4: "10.0.2.10",
subnetId: example2.id,
},
],
subnetIds: [
example1.id,
example2.id,
],
});
import pulumi
import pulumi_aws as aws
ec2 = aws.ec2.VpcEndpoint("ec2",
vpc_id=example["id"],
service_name="com.amazonaws.us-west-2.ec2",
vpc_endpoint_type="Interface",
subnet_configurations=[
{
"ipv4": "10.0.1.10",
"subnet_id": example1["id"],
},
{
"ipv4": "10.0.2.10",
"subnet_id": example2["id"],
},
],
subnet_ids=[
example1["id"],
example2["id"],
])
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := ec2.NewVpcEndpoint(ctx, "ec2", &ec2.VpcEndpointArgs{
VpcId: pulumi.Any(example.Id),
ServiceName: pulumi.String("com.amazonaws.us-west-2.ec2"),
VpcEndpointType: pulumi.String("Interface"),
SubnetConfigurations: ec2.VpcEndpointSubnetConfigurationArray{
&ec2.VpcEndpointSubnetConfigurationArgs{
Ipv4: pulumi.String("10.0.1.10"),
SubnetId: pulumi.Any(example1.Id),
},
&ec2.VpcEndpointSubnetConfigurationArgs{
Ipv4: pulumi.String("10.0.2.10"),
SubnetId: pulumi.Any(example2.Id),
},
},
SubnetIds: pulumi.StringArray{
example1.Id,
example2.Id,
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var ec2 = new Aws.Ec2.VpcEndpoint("ec2", new()
{
VpcId = example.Id,
ServiceName = "com.amazonaws.us-west-2.ec2",
VpcEndpointType = "Interface",
SubnetConfigurations = new[]
{
new Aws.Ec2.Inputs.VpcEndpointSubnetConfigurationArgs
{
Ipv4 = "10.0.1.10",
SubnetId = example1.Id,
},
new Aws.Ec2.Inputs.VpcEndpointSubnetConfigurationArgs
{
Ipv4 = "10.0.2.10",
SubnetId = example2.Id,
},
},
SubnetIds = new[]
{
example1.Id,
example2.Id,
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ec2.VpcEndpoint;
import com.pulumi.aws.ec2.VpcEndpointArgs;
import com.pulumi.aws.ec2.inputs.VpcEndpointSubnetConfigurationArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var ec2 = new VpcEndpoint("ec2", VpcEndpointArgs.builder()
.vpcId(example.id())
.serviceName("com.amazonaws.us-west-2.ec2")
.vpcEndpointType("Interface")
.subnetConfigurations(
VpcEndpointSubnetConfigurationArgs.builder()
.ipv4("10.0.1.10")
.subnetId(example1.id())
.build(),
VpcEndpointSubnetConfigurationArgs.builder()
.ipv4("10.0.2.10")
.subnetId(example2.id())
.build())
.subnetIds(
example1.id(),
example2.id())
.build());
}
}
resources:
ec2:
type: aws:ec2:VpcEndpoint
properties:
vpcId: ${example.id}
serviceName: com.amazonaws.us-west-2.ec2
vpcEndpointType: Interface
subnetConfigurations:
- ipv4: 10.0.1.10
subnetId: ${example1.id}
- ipv4: 10.0.2.10
subnetId: ${example2.id}
subnetIds:
- ${example1.id}
- ${example2.id}
The subnetConfigurations array lets you specify exact IPv4 addresses for each network interface. Each entry maps a subnetId to an ipv4 address. You must also list the same subnets in subnetIds. The specified addresses must be available in their respective subnets.
Connect to third-party services via PrivateLink
VPC endpoints support third-party services exposed through AWS PrivateLink, enabling private connectivity to SaaS applications without internet exposure.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const ptfeService = new aws.ec2.VpcEndpoint("ptfe_service", {
vpcId: vpcId,
serviceName: ptfeServiceConfig,
vpcEndpointType: "Interface",
securityGroupIds: [ptfeServiceAwsSecurityGroup.id],
subnetIds: [subnetIds],
privateDnsEnabled: false,
});
const internal = aws.route53.getZone({
name: "vpc.internal.",
privateZone: true,
vpcId: vpcId,
});
const ptfeServiceRecord = new aws.route53.Record("ptfe_service", {
zoneId: internal.then(internal => internal.zoneId),
name: internal.then(internal => `ptfe.${internal.name}`),
type: aws.route53.RecordType.CNAME,
ttl: 300,
records: [ptfeService.dnsEntries[0].dns_name],
});
import pulumi
import pulumi_aws as aws
ptfe_service = aws.ec2.VpcEndpoint("ptfe_service",
vpc_id=vpc_id,
service_name=ptfe_service_config,
vpc_endpoint_type="Interface",
security_group_ids=[ptfe_service_aws_security_group["id"]],
subnet_ids=[subnet_ids],
private_dns_enabled=False)
internal = aws.route53.get_zone(name="vpc.internal.",
private_zone=True,
vpc_id=vpc_id)
ptfe_service_record = aws.route53.Record("ptfe_service",
zone_id=internal.zone_id,
name=f"ptfe.{internal.name}",
type=aws.route53.RecordType.CNAME,
ttl=300,
records=[ptfe_service.dns_entries[0].dns_name])
package main
import (
"fmt"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/route53"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
ptfeService, err := ec2.NewVpcEndpoint(ctx, "ptfe_service", &ec2.VpcEndpointArgs{
VpcId: pulumi.Any(vpcId),
ServiceName: pulumi.Any(ptfeServiceConfig),
VpcEndpointType: pulumi.String("Interface"),
SecurityGroupIds: pulumi.StringArray{
ptfeServiceAwsSecurityGroup.Id,
},
SubnetIds: pulumi.StringArray{
subnetIds,
},
PrivateDnsEnabled: pulumi.Bool(false),
})
if err != nil {
return err
}
internal, err := route53.LookupZone(ctx, &route53.LookupZoneArgs{
Name: pulumi.StringRef("vpc.internal."),
PrivateZone: pulumi.BoolRef(true),
VpcId: pulumi.StringRef(vpcId),
}, nil);
if err != nil {
return err
}
_, err = route53.NewRecord(ctx, "ptfe_service", &route53.RecordArgs{
ZoneId: pulumi.String(internal.ZoneId),
Name: pulumi.Sprintf("ptfe.%v", internal.Name),
Type: pulumi.String(route53.RecordTypeCNAME),
Ttl: pulumi.Int(300),
Records: pulumi.StringArray{
pulumi.String(ptfeService.DnsEntries.ApplyT(func(dnsEntries []ec2.VpcEndpointDnsEntry) (interface{}, error) {
return dnsEntries[0].Dns_name, nil
}).(pulumi.Interface{}Output)),
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var ptfeService = new Aws.Ec2.VpcEndpoint("ptfe_service", new()
{
VpcId = vpcId,
ServiceName = ptfeServiceConfig,
VpcEndpointType = "Interface",
SecurityGroupIds = new[]
{
ptfeServiceAwsSecurityGroup.Id,
},
SubnetIds = new[]
{
subnetIds,
},
PrivateDnsEnabled = false,
});
var @internal = Aws.Route53.GetZone.Invoke(new()
{
Name = "vpc.internal.",
PrivateZone = true,
VpcId = vpcId,
});
var ptfeServiceRecord = new Aws.Route53.Record("ptfe_service", new()
{
ZoneId = @internal.Apply(@internal => @internal.Apply(getZoneResult => getZoneResult.ZoneId)),
Name = @internal.Apply(@internal => $"ptfe.{@internal.Apply(getZoneResult => getZoneResult.Name)}"),
Type = Aws.Route53.RecordType.CNAME,
Ttl = 300,
Records = new[]
{
ptfeService.DnsEntries.Apply(dnsEntries => dnsEntries[0].Dns_name),
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ec2.VpcEndpoint;
import com.pulumi.aws.ec2.VpcEndpointArgs;
import com.pulumi.aws.route53.Route53Functions;
import com.pulumi.aws.route53.inputs.GetZoneArgs;
import com.pulumi.aws.route53.Record;
import com.pulumi.aws.route53.RecordArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var ptfeService = new VpcEndpoint("ptfeService", VpcEndpointArgs.builder()
.vpcId(vpcId)
.serviceName(ptfeServiceConfig)
.vpcEndpointType("Interface")
.securityGroupIds(ptfeServiceAwsSecurityGroup.id())
.subnetIds(subnetIds)
.privateDnsEnabled(false)
.build());
final var internal = Route53Functions.getZone(GetZoneArgs.builder()
.name("vpc.internal.")
.privateZone(true)
.vpcId(vpcId)
.build());
var ptfeServiceRecord = new Record("ptfeServiceRecord", RecordArgs.builder()
.zoneId(internal.zoneId())
.name(String.format("ptfe.%s", internal.name()))
.type("CNAME")
.ttl(300)
.records(ptfeService.dnsEntries().applyValue(_dnsEntries -> _dnsEntries[0].dns_name()))
.build());
}
}
resources:
ptfeService:
type: aws:ec2:VpcEndpoint
name: ptfe_service
properties:
vpcId: ${vpcId}
serviceName: ${ptfeServiceConfig}
vpcEndpointType: Interface
securityGroupIds:
- ${ptfeServiceAwsSecurityGroup.id}
subnetIds:
- ${subnetIds}
privateDnsEnabled: false
ptfeServiceRecord:
type: aws:route53:Record
name: ptfe_service
properties:
zoneId: ${internal.zoneId}
name: ptfe.${internal.name}
type: CNAME
ttl: '300'
records:
- ${ptfeService.dnsEntries[0].dns_name}
variables:
internal:
fn::invoke:
function: aws:route53:getZone
arguments:
name: vpc.internal.
privateZone: true
vpcId: ${vpcId}
This configuration extends the Interface endpoint pattern with custom DNS. The endpoint creates network interfaces with DNS entries accessible via the dnsEntries output. Setting privateDnsEnabled to false prevents automatic DNS configuration, allowing you to create custom Route 53 records that point to the endpoint’s DNS name. This is common when connecting to third-party services that require specific DNS names.
Beyond these examples
These snippets focus on specific endpoint-level features: Gateway endpoints for S3 and DynamoDB, Interface endpoints with security groups and private DNS, and custom IP addressing and third-party PrivateLink services. They’re intentionally minimal rather than full networking solutions.
The examples may reference pre-existing infrastructure such as VPC, subnets, and security groups, Route 53 private hosted zones for custom DNS, and PrivateLink service configurations for third-party services. They focus on configuring the endpoint rather than provisioning everything around it.
To keep things focused, common endpoint patterns are omitted, including:
- Route table associations (routeTableIds for Gateway endpoints)
- Endpoint policies for access control
- Cross-region endpoint configuration (serviceRegion)
- VPC Lattice endpoints (Resource and ServiceNetwork types)
These omissions are intentional: the goal is to illustrate how each endpoint feature is wired, not provide drop-in networking modules. See the VPC Endpoint resource reference for all available configuration options.
Let's create AWS VPC Endpoints
Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.
Try Pulumi Cloud for FREEFrequently Asked Questions
Common Errors & Pitfalls
routeTableIds or subnetIds) and standalone VPC Endpoint Association resources. Choose one approach: manage associations inline with the VPC Endpoint, or use separate Association resources, but never both.subnetIds to function. Always specify at least one subnet when creating Interface type endpoints.dnsEntry output is a list of maps requiring special handling. Use lookup and bracket notation (e.g., dnsEntries[0].dns_name) to access values.Endpoint Types & Configuration
Five types are available:
- Gateway (default): For S3 and DynamoDB, routes traffic through route tables
- Interface: For most AWS services, creates ENIs in your subnets
- GatewayLoadBalancer: For Gateway Load Balancer endpoints
- Resource: For VPC Lattice Resource Configurations
- ServiceNetwork: For VPC Lattice Service Networks
privateDnsEnabled set to true (though it defaults to false) to allow services within the VPC to automatically use the endpoint without DNS configuration.Service Connection
Provide exactly one of three options:
serviceNamefor AWS services (format:com.amazonaws.<region>.<service>)resourceConfigurationArnfor VPC Lattice Resource ConfigurationsserviceNetworkArnfor VPC Lattice Service Networks
com.amazonaws.<region>.<service> for most AWS services. Exception: SageMaker AI Notebook uses aws.sagemaker.<region>.notebook.serviceRegion to the service’s region for Interface endpoints. Your endpoint will connect to the service in that region while remaining in your VPC’s region.privateDnsEnabled set to false, then create a Route53 record pointing to the endpoint’s DNS entry.Networking & Security
subnetConfigurations to specify IPv4 or IPv6 addresses for each subnet.Immutability & Lifecycle
vpcId, vpcEndpointType, serviceName, serviceNetworkArn, resourceConfigurationArn, and serviceRegion.Using a different cloud?
Explore networking guides for other cloud providers: