Create AWS VPC Endpoints

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.

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 FREE

Frequently Asked Questions

Resource Associations & Conflicts
Why am I getting conflicts when managing route tables or subnets for my VPC endpoint?
You can’t use the same resource ID in both a VPC Endpoint resource (with 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
What's the difference between Gateway, Interface, and other endpoint types?
VPC endpoints support five types: 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.
Why isn't my Interface endpoint working?
Interface endpoints cannot function without being assigned to a subnet. Always specify subnetIds when creating Interface endpoints.
Network & DNS Configuration
Should I enable private DNS for my Interface endpoint?
Most users want 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.
What security groups are used if I don't specify any?
For Interface endpoints, if you don’t specify securityGroupIds, the VPC’s default security group is automatically associated with the endpoint.
How do I specify custom IP addresses for an Interface endpoint?
Use subnetConfigurations to specify IPv4 and/or IPv6 addresses for each subnet, along with subnetIds.
How do I access the DNS name from the dnsEntries output?
The dnsEntry output is a list of maps. Use lookup and [] syntax for interpolation, such as dnsEntries[0].dns_name.
Service Configuration
What's the correct format for serviceName?
For AWS services, use com.amazonaws.<region>.<service>. The SageMaker AI Notebook service is an exception, using aws.sagemaker.<region>.notebook.
Which service identifier should I use: serviceName, resourceConfigurationArn, or serviceNetworkArn?
You must specify exactly one of these three. Use serviceName for standard AWS services, resourceConfigurationArn for VPC Lattice Resource Configuration endpoints, or serviceNetworkArn for VPC Lattice Service Network endpoints.
How do I create a cross-region VPC endpoint?
Set 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
What properties can't be changed after creating a VPC endpoint?
The following properties are immutable and force replacement if changed: vpcEndpointType, vpcId, serviceName, serviceNetworkArn, resourceConfigurationArn, and serviceRegion. Additionally, changing privateDnsEnabled on non-Interface endpoint types forces replacement.
Why does changing privateDnsEnabled recreate my endpoint?
If 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: