Create AWS VPC Endpoints

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.

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 FREE

Frequently Asked Questions

Common Errors & Pitfalls
Why am I getting conflicts when managing route tables or subnets?
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 inline with the VPC Endpoint, or use separate Association resources, but never both.
Why isn't my Interface endpoint working?
Interface endpoints require subnetIds to function. Always specify at least one subnet when creating Interface type endpoints.
How do I access DNS entries from the endpoint?
The 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
What are the VPC endpoint types and when should I use each?

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
Should I enable private DNS for Interface endpoints?
Most users want 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
How do I specify which service to connect to?

Provide exactly one of three options:

  • serviceName for AWS services (format: com.amazonaws.<region>.<service>)
  • resourceConfigurationArn for VPC Lattice Resource Configurations
  • serviceNetworkArn for VPC Lattice Service Networks
What's the service name format for AWS services?
Use com.amazonaws.<region>.<service> for most AWS services. Exception: SageMaker AI Notebook uses aws.sagemaker.<region>.notebook.
How do I connect to a service in a different region?
Set 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.
Can I connect to non-AWS services?
Yes. Create an Interface endpoint with privateDnsEnabled set to false, then create a Route53 record pointing to the endpoint’s DNS entry.
Networking & Security
What happens if I don't specify security groups for Interface endpoints?
The VPC’s default security group is automatically associated with the endpoint.
How do I assign specific IP addresses to my Interface endpoint?
Use subnetConfigurations to specify IPv4 or IPv6 addresses for each subnet.
Do Gateway endpoints support access policies?
Yes. All Gateway endpoints and some Interface endpoints support JSON policies to control service access. The default is full access.
Immutability & Lifecycle
What properties can't be changed after creation?
These properties are immutable: vpcId, vpcEndpointType, serviceName, serviceNetworkArn, resourceConfigurationArn, and serviceRegion.

Using a different cloud?

Explore networking guides for other cloud providers: