Right, let’s talk about VPC Endpoints. You’ve built your pristine VPC, locked your instances down in private subnets with no internet gateways, and you’re feeling pretty good about your security posture. Then you realize your app needs to save a file to S3. Panic sets in. How does it get there without a public IP? Do you really have to build a clunky NAT gateway and pay for all that egress data just to talk to another AWS service?

No, you don’t. This is exactly the problem VPC Endpoints solve. They’re essentially a way to create a private, internal-only connection from your VPC directly to an AWS service without sending your traffic over the public internet. It’s like AWS built a secret, high-speed tunnel from your VPC right into their service’s back-end. It’s more secure, and often a lot faster and cheaper.

There are two types, but here we’re focusing on the simpler one: Gateway Endpoints. Currently, they only support two services, but boy are they important ones: S3 and DynamoDB.

How a Gateway Endpoint Actually Works (It’s Weird)

Don’t let the name fool you; it’s not a gateway you deploy. It’s not an ENI. It’s a routing object. This is the part everyone gets wrong at first. When you create a Gateway Endpoint, you’re essentially telling your VPC’s route tables: “Hey, for traffic heading to this specific S3 prefix, don’t send it to the Internet Gateway or NAT. Send it to this special endpoint route instead.” AWS then handles the rest, magically routing that traffic internally within their network.

It’s a brilliantly simple solution from a configuration standpoint. You just create the endpoint, attach a policy controlling what it can do, and most importantly, you update your route tables. Let’s see what that looks like.

# Create the S3 Gateway Endpoint
aws ec2 create-vpc-endpoint \
    --vpc-id vpc-12345678 \
    --service-name com.amazonaws.us-east-1.s3 \
    --route-table-ids rtb-abcdefgh123456789 \
    --policy-document file://endpoint-policy.json

Where your endpoint-policy.json might look like this. This is a critical step—don’t just use the default full-access policy unless you’re absolutely sure you want that.

{
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::my-secure-bucket/*"
    }
  ]
}

And just like that, any instance in the subnets associated with that route table (rtb-abcdefgh123456789) can now talk to S3, but only for the actions you specified in the policy.

The Critical “Gotchas” and Best Practices

This is where I earn my keep. This stuff is simple until it isn’t.

  1. Routing is Everything: The endpoint must be associated with the route tables of your private subnets. If your instance’s route table doesn’t have the new route to the endpoint for the S3 prefix, traffic will still try to go out the NAT gateway (and probably fail). Always double-check your route tables after creation.

  2. The Bucket Policy Tango: This is the most common head-scratcher. You have an endpoint policy controlling what the endpoint can do. But you might also have a bucket policy on the S3 bucket itself. For a request from your VPC to succeed, both policies must allow it. I’ve seen people tear their hair out because their endpoint policy was wide open, but their bucket policy had a explicit Deny for requests not from a specific VPC. You need to sync them up. A common best practice is to use the aws:SourceVpc condition in your bucket policy to only allow requests coming from your specific VPC.

  3. Region is King: Gateway Endpoints are region-specific. Your endpoint in us-east-1 can only talk to S3 buckets that are also in us-east-1 using the internal AWS network. If your application in us-east-1 needs to talk to a bucket in eu-west-1, the traffic will still go over the public internet. Plan your architecture accordingly.

  4. DNS Doesn’t Change: This is a big one. Your instances will still resolve the public DNS name of the S3 bucket (e.g., my-bucket.s3.amazonaws.com). The magic happens at the network layer. The route table forces the traffic destined for that IP to go through the endpoint instead of out to the internet. It works seamlessly, but it means you can’t rely on DNS lookups to tell you if you’re using the endpoint.

Gateway Endpoints are one of those features that, once you understand them, you’ll use everywhere. They make your architecture cleaner, more secure, and cheaper. Just remember the policy dance and always, always check your route tables. It’s the difference between a elegant private connection and a frustrating debugging session.