S3 is not inside your VPC. Its address is a public one, so a private instance with no NAT gateway cannot reach it at all: aws s3 ls waits and times out.
A VPC endpoint is a private way to a service. There are two kinds:
| Kind | For | How |
|---|---|---|
| gateway | S3 and DynamoDB | a route in your route tables; free |
| interface | most other services, such as Session Manager | an address for the service inside your subnets; billed by the hour |
A gateway endpoint for S3
aws ec2 create-vpc-endpoint --vpc-id $VPC --service-name com.amazonaws.us-east-1.s3 --vpc-endpoint-type Gateway --route-table-ids $RTThe service name is com.amazonaws., the region, and the service. --route-table-ids names the tables that get the route; here $RT holds the private table's id (RT=$(aws ec2 describe-route-tables --filters Name=tag:Name,Values=private --query "RouteTables[0].RouteTableId" --output text)).
The new route's destination is not a block but a prefix list, pl-63a5400a: AWS's own list of S3's addresses in the region. Traffic to S3 now takes that route and never touches the internet. The instance still needs permission (its role) to read the bucket; the endpoint only provides the path.
Even with a NAT gateway, an S3 endpoint is worth adding: the path stays inside AWS, and a gateway endpoint costs nothing while NAT charges for every gigabyte.