The aws:ec2/vpcEndpoint:VpcEndpoint resource, part of the Pulumi AWS provider, creates VPC endpoints that enable private connections to AWS services and third-party PrivateLink services without internet gateways. This guide focuses on four capabilities: Gateway endpoints for S3 and DynamoDB, Interface endpoints with private DNS, custom IP addressing, and third-party PrivateLink integration.
VPC endpoints attach to existing VPCs, subnets, security groups, and route tables. Third-party endpoints may require Route 53 configuration. The examples are intentionally small. Combine them with your own VPC infrastructure and access policies.
Connect to S3 via a Gateway endpoint
Most deployments begin with Gateway endpoints for S3 or DynamoDB, enabling 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 your VPC’s route tables rather than creating network interfaces. When vpcEndpointType is omitted, it defaults to Gateway.
Access EC2 API through an Interface endpoint
Interface endpoints create elastic network interfaces in your subnets for 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 subnets. The securityGroupIds control which resources can reach the endpoint. When privateDnsEnabled is true, AWS creates a Route 53 private hosted zone automatically, allowing services to use standard AWS API hostnames that resolve to the endpoint’s private IPs.
Pin specific IP addresses to endpoint interfaces
Some architectures require predictable IP addresses for firewall rules 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 subnet’s network interface. Each configuration must reference a subnet also listed in subnetIds. This gives you control over endpoint addressing for static firewall rules or application configuration.
Connect to third-party services via PrivateLink
VPC endpoints support third-party services exposed through AWS PrivateLink, enabling private connectivity to SaaS platforms.
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 (
"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}
For third-party services, the serviceName comes from the service provider’s PrivateLink configuration. Setting privateDnsEnabled to false means you manage DNS manually. The dnsEntries output provides the endpoint’s DNS name, which you can reference in Route 53 records to create custom hostnames for the service.
Beyond these examples
These snippets focus on specific VPC endpoint features: Gateway and Interface endpoint types, private DNS and custom IP addressing, and third-party PrivateLink integration. They’re intentionally minimal rather than full networking solutions.
The examples reference pre-existing infrastructure such as VPCs, subnets, security groups, route tables, and Route 53 private hosted zones or third-party PrivateLink service configurations. They focus on configuring the endpoint rather than provisioning the surrounding network.
To keep things focused, common endpoint patterns are omitted, including:
- Endpoint policies for access control (policy property)
- Route table associations for Gateway endpoints (routeTableIds)
- Gateway Load Balancer and VPC Lattice endpoint types
- Cross-region service access (serviceRegion)
- DNS options and IP address type configuration
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
Resource Associations & Conflicts
routeTableIds or subnetIds) and standalone VPC Endpoint Association resources. Choose one approach: manage associations via the VPC Endpoint’s attributes, or use standalone association resources, but not both.Endpoint Types & Selection
Gateway (default, for S3/DynamoDB), Interface (for most AWS services via ENI), GatewayLoadBalancer (for appliances), Resource (for VPC Lattice Resource Configuration), and ServiceNetwork (for VPC Lattice Service Network). The type determines routing behavior and available configuration options.subnetIds when creating Interface endpoints.Network & DNS Configuration
privateDnsEnabled set to true for Interface endpoints to allow services within the VPC to automatically use the endpoint. It defaults to false, so you need to explicitly enable it.securityGroupIds, the VPC’s default security group is automatically associated with the endpoint.subnetConfigurations to specify IPv4 and/or IPv6 addresses for each subnet, along with subnetIds.dnsEntry output is a list of maps. Use lookup and [] syntax for interpolation, such as dnsEntries[0].dns_name.Service Configuration
com.amazonaws.<region>.<service>. The SageMaker AI Notebook service is an exception, using aws.sagemaker.<region>.notebook.serviceName for standard AWS services, resourceConfigurationArn for VPC Lattice Resource Configuration endpoints, or serviceNetworkArn for VPC Lattice Service Network endpoints.region to your VPC’s region and serviceRegion to the service’s region. This is applicable for Interface endpoints connecting to services in different regions.Immutability & Limitations
vpcEndpointType, vpcId, serviceName, serviceNetworkArn, resourceConfigurationArn, and serviceRegion. Additionally, changing privateDnsEnabled on non-Interface endpoint types forces replacement.vpcEndpointType is anything other than Interface, changing privateDnsEnabled forces a new resource to be created.Using a different cloud?
Explore networking guides for other cloud providers: