Gateway 型 VPC エンドポイントで S3 バケットにプライベート接続する環境の構築手順について説明します。
目次
今回の構成
次のような環境を想定します。
AWS Region: ap-northeast-1
VPC
└── 10.0.0.0/16
├── Private Subnet A
│ ├── 10.0.1.0/24
│ ├── EC2 A
│ └── Private Route Table A
│
├── S3 Gateway VPC Endpoint
│
└── NAT Gateway
構成図

VPCエンドポイントがない場合(NAT Gateway 経由で接続する場合)
プライベートサブネットの EC2 は、通常、直接 Internet Gateway へ通信できません。
そのためS3へアクセスするには、一般的には次の経路を通ります。
EC2
↓
Private Route Table
↓
NAT Gateway
↓
Internet Gateway
↓
Amazon S3のパブリックエンドポイント
経路をもう少し正確に書くと次のようになります。
EC2
10.0.1.10
↓
送信先: S3のIPアドレス
↓
ルートテーブルを検索
↓
S3専用ルートが存在しない
↓
0.0.0.0/0 → NAT Gateway
↓
S3
ここで重要なのは、EC2 が接続する S3 のホスト名です。
S3 は AWS サービスですが、通常の S3 API エンドポイントはパブリック IP アドレスへ名前解決されます。そのため、Gateway エンドポイントがない状態では、ルートテーブル上のデフォルトルートである、
0.0.0.0/0 → NAT Gateway
が選択されます。
Gateway VPC エンドポイントを作成した場合
Gateway VPC エンドポイントを作成し、プライベートサブネットのルートテーブルと関連付けると、ルートテーブルに S3 向けのルートが自動追加されます。
送信先 ターゲット
10.0.0.0/16 local
pl-xxxxxxxx vpce-xxxxxxxx
0.0.0.0/0 nat-xxxxxxxx
それぞれの意味は次のとおりです。
| 送信先 | ターゲット | 意味 |
|---|---|---|
| 10.0.0.0/16 | local | 同じVPC内への通信 |
| pl-xxxxxxxx | vpce-xxxxxxxx | S3宛ての通信 |
| 0.0.0.0/0 | NAT Gateway | その他の外部通信 |
AWS は、Gateway エンドポイントをルートテーブルに関連付けると、S3 の AWS 管理プレフィックスリストを送信先、VPC エンドポイントをターゲットとするルートを自動追加します。
pl-xxxxxxxx とは何か
pl-xxxxxxxxは、AWS 管理プレフィックスリストです。AWS 管理プレフィックスリストとは、たとえば東京リージョンの S3 が利用している IP アドレス範囲を、AWS側でまとめて管理したものです。
具体的には次のような内容です。
pl-xxxxxxxx
├── 3.xxx.xxx.0/24
├── 52.xxx.xxx.0/24
├── 54.xxx.xxx.0/24
└── その他のS3用IPアドレス範囲
ただし、利用者が個別にIPアドレスを管理する必要はありません。
AWS が S3 の IP アドレス範囲を変更した場合も、AWS 管理プレフィックスリストが更新されます。
したがって、ルートテーブルでは、
S3のIPアドレス群 → S3 Gateway Endpoint
というルーティングを、次のように表現できます。
pl-xxxxxxxx → vpce-xxxxxxxx
Gatewayエンドポイントはサブネットに置かれない
ここは Interface 型との大きな違いです。
Gateway 型 VPC エンドポイントは、特定のサブネット内に ENI を作成しません。
つまり、次のようなリソースは作られません。
・ENI
・プライベートIPアドレス
・エンドポイント用セキュリティグループ
Gatewayエンドポイントは、論理的に次の位置付けです。
ルートテーブル
↓
Gateway VPC Endpoint
↓
S3
そのためTerraform でも Gatewayエンドポイントを構築する場合、Interface 型で使用する次の設定は使いません。
subnet_ids
security_group_ids
private_dns_enabled
代わりに重要なのが、
route_table_ids
です。
Gateway エンドポイントは S3 と DynamoDB 向けのルートテーブルベースの仕組みであり、Interface 型とは異なり、AWS PrivateLink を使用しません。
ENI(Elastic Network Interface)とは
ENI(Elastic Network Interface)は、AWS 上の仮想ネットワークカードです。EC2 などの AWS リソースを VPC ネットワークに接続するために使われます。
たとえば物理サーバーなら、LAN ケーブルを挿す「NIC(ネットワークインターフェースカード)」があります。AWS では、その仮想版が ENI です。
ENI には主に次の情報が紐づきます。
- プライベートIPアドレス
- セキュリティグループ
- MACアドレス
- サブネット
- 必要に応じてパブリックIPやElastic IP
- 追加のセカンダリIPアドレス
サブネットではなくルートテーブルに関連付ける
たとえば次の構成だとします。
Private Subnet A
└── Private Route Table A
両方のサブネットから S3 Gateway エンドポイントを利用したい場合、両方のルートテーブルを関連付けます。
Terraform コード
route_table_ids = [
aws_route_table.private_a.id,
]
private_aは、次のような構成になります。
Private Subnet A
→ Gateway Endpoint経由
VPC エンドポイントは VPC に作成されますが、実際に利用できるかどうかは、関連付けられたルートテーブルで決まります。
DNS名は変更しない
Gateway エンドポイントを作成しても、EC2 側で使用する S3 の URL は変更しません。
引き続き通常どおり、以下のエンドポイントを使用します。
S3サービスエンドポイント:s3.ap-northeast-1.amazonaws.com
仮想ホスト形式のエンドポイント:example-app-bucket.s3.ap-northeast-1.amazonaws.com
AWS CLI もアプリケーションコードも変更不要です。
名前解決結果自体を VPC エンドポイントのプライベート IP へ変更する仕組みではありません。
Gateway エンドポイントでは、
DNS名
↓
S3のIPアドレスへ名前解決
↓
ルートテーブルでS3プレフィックスリストに一致
↓
Gateway Endpointへ送信
という動作になります。
Gateway エンドポイントを作成しても既存の S3 パブリック DNS 名をそのまま使用でき、変更されるのは通信経路です。
サービスエンドポイントとは
サービスエンドポイントとは、AWSの特定サービスへ接続するための「接続先」を指します。
S3の場合は、以下になります。
s3.ap-northeast-1.amazonaws.com
これは、東京リージョンの Amazon S3 サービスに接続するためのエンドポイントです。AWS サービスが公開している接続先です。
仮想ホスト形式のエンドポイントとは
仮想ホスト形式のエンドポイントとは、S3 のバケット名を、URL のパスではなくホスト名の一部として表す形式です。「仮想ホスト」と呼ばれるのは、バケットごとに別々のホスト名を持っているように見えるためです。ただし、バケットごとに専用サーバーが存在する、という意味ではありません。
例:
example-app-bucket.s3.ap-northeast-1.amazonaws.com
仮想ホスティングとは、単一のウェブサーバーから複数のウェブサイトにサービスを提供することです。
パス形式の例
仮想ホスティング形式の URI では、バケット名は URL のドメイン名の一部です。
仮想ホスト形式
example-app-bucket.s3.ap-northeast-1.amazonaws.com/images/logo.png
パス形式
s3.ap-northeast-1.amazonaws.com/example-app-bucket/images/logo.png
仮想ホスト形式URL
URL:https://example-app-bucket.s3.ap-northeast-1.amazonaws.com/images/logo.png
パス形式URL
URL:https://s3.ap-northeast-1.amazonaws.com/example-app-bucket/images/logo.png
Gateway エンドポイントを通る通信の流れの例
EC2から次を実行したとします。ローカルの test.txt ファイルを Gateway エンドポイント経由で example-app-bucket バケットにコピーしたとします。
aws s3 cp test.txt s3://example-app-bucket/test.txt
手順1:AWS CLIがS3のDNS名を解決
example-app-bucket.s3.ap-northeast-1.amazonaws.com
を DNS で名前解決します。S3 の IP アドレスが返されます。
手順2:EC2がルートテーブルを確認
EC2 は、名前解決された S3 の IP アドレスへの送信ルートを探します。
Private Route Table A
10.0.0.0/16 → local
pl-xxxx → vpce-xxxx ← S3のIPアドレスはpl-xxxxに含まれているため、これが選択される。
0.0.0.0/0 → nat-xxxx
S3 の Gateway VPC エンドポイントがない場合、S3 の通常のエンドポイントは公開 IP アドレスに名前解決されます。その通信は通常、0.0.0.0/0 のルートを使用します。
なお、Gateway エンドポイントがないうえに、NAT Gateway や Internet Gateway へのルートもない場合は、S3 へ接続できません。
手順3:Gatewayエンドポイントへ送信
通信が S3 Gateway VPC エンドポイントへ送られます。
EC2
↓
Private Route Table
↓
S3 Gateway Endpoint
手順4:エンドポイントポリシーを評価
Gateway エンドポイントに設定されたエンドポイントポリシーで、次が確認されます。
・誰がアクセスするか
・どのS3操作を実行するか
・どのバケットへアクセスするか
エンドポイントポリシーとは、VPC エンドポイントに設定する、そのエンドポイントを通って AWS サービスへアクセスする際の許可範囲です。
S3 Gateway エンドポイントの場合、簡単にいうと、この Gateway エンドポイント経由では、誰が、どの S3 操作を、どのバケットに対して実行してよいかを制限するポリシーです。VPC エンドポイントにアタッチするリソースベースのポリシーです。
エンドポイントポリシーの例
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::example-app-bucket",
"arn:aws:s3:::example-app-bucket/*"
]
}
]
}
権限について
エンドポイントポリシーだけで、EC2 の IAM ロールに S3 権限が付与されるわけではありません。
例えば、EC2 から S3 オブジェクトを取得するには、少なくとも次の両方で許可される必要があります。
EC2のIAMロール
→ s3:GetObjectを許可
Gatewayエンドポイントポリシー
→ s3:GetObjectと対象バケットを許可
権限の4パターン
| EC2のIAMロール | エンドポイントポリシー | 結果 |
|---|---|---|
| 許可あり | 通過を許可 | アクセス可能 |
| 許可なし | 通過を許可 | アクセス不可 |
| 許可あり | 対象外・拒否 | アクセス不可 |
| 許可なし | 対象外・拒否 | アクセス不可 |
結論を言えば、IAM ロールのポリシーとエンドポイントポリシー両方の許可がなければアクセスはできません。
ちなみにデフォルトのエンドポイントポリシーは、広く許可しています。
{
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
手順5:IAMとS3バケットポリシーを評価
次に通常の AWS 認可が行われます。(手順4 ですでにエンドポイントポリシーで評価済みの前提)
EC2 IAMロール
S3バケットポリシー
SCP
Permissions Boundary
その他のIAM制御
EC2 の IAM ロールですでに許可されている場合、バケットポリシーにも同じ Allow を必ず書かなければならないわけではありません。同一アカウントでは、IAM ポリシーまたはリソースベースポリシーのいずれかの Allow によって許可され、どこかに明示的 Deny があれば拒否されるのが基本です。
したがって、より正確な流れは次のとおりです。
1. EC2のIAMロールなどに、S3操作を許可するAllowがあるか
2. Gatewayエンドポイントポリシーの許可範囲に入っているか
3. バケットポリシーやSCPなどに明示的Denyがないか
4. すべての条件を満たせばアクセス可能
一言で表すと、IAM ロールは「何をしてよいか」という権限を与え、エンドポイントポリシーは「その権限をこの経路でどこまで使わせるか」を制限します。
手順6:S3へアクセス
すべての認可を通過した場合、S3 オブジェクトの取得や保存が行われます。以上の手順で S3 バケットへのアクセスが行われます。
通信経路とアクセス権限は別
Gateway エンドポイントを作っただけでは、EC2 から S3 へアクセスできるとは限りません。
最低でも次の2種類を分けて考える必要があります。
通信経路
→ Gateway VPC Endpoint
AWS権限
→ IAMポリシー
整理すると次のようになります。
| 設定 | 役割 |
|---|---|
| ルートテーブル | S3への通信経路を決める |
| VPCエンドポイントポリシー | エンドポイントを通過できる操作を制限する |
| EC2のIAMロール | EC2が実行できるS3操作を許可する |
| S3バケットポリシー | バケット側でアクセス元を制限する |
| セキュリティグループ | EC2からのHTTPS送信を許可する |
| NACL | サブネットレベルで通信を許可する |
VPC エンドポイントポリシーは IAM ポリシーの代わりではなく、エンドポイントを通過するリクエストに追加の制限をかけるポリシーです。
Terraformの全体構成
次のリソースを作成します。
・VPC
・Private Subnet A
・Private Route Table A
・EC2用セキュリティグループ
・S3バケット
・S3 Gateway VPC Endpoint
・EC2用IAMロール
・S3アクセス用IAMポリシー
Providerと共通データ
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
data "aws_region" "current" {}
data "aws_caller_identity" "current" {}
Terraform の Data Sources について
Data Sources とは、簡単に言うと AWS 上に何かを作るのではなく、現在 Terraform を実行している AWS 環境の情報を取得する ための設定です。
data "aws_region" "current" {}
data "aws_caller_identity" "current" {}
例:
data "aws_region" "current" {}
output "region" {
value = data.aws_region.current.name
}
結果は例えばこうです。
ap-northeast-1
これは、今 Terraform が対象にしている AWS リージョンを取得するという動きです。
data "aws_caller_identity" "current" {}は、現在 Terraform を実行している AWS 認証情報の情報を取得します。
具体的には、主に次のような情報が取れます。
AWSアカウントID
IAMユーザーまたはロールのARN
User ID
例:
data "aws_caller_identity" "current" {}
output "account_id" {
value = data.aws_caller_identity.current.account_id
}
結果は例えばこうなります。
123456789012
つまりこれは、今 Terraform を実行している AWS アカウントがどれかを取得するという動きです。
VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "example-vpc"
}
}
プライベートサブネット
resource "aws_subnet" "private_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = false
tags = {
Name = "example-private-subnet-a"
}
}
プライベートルートテーブル
まずルートテーブル本体を作成します。
resource "aws_route_table" "private_a" {
vpc_id = aws_vpc.main.id
tags = {
Name = "example-private-route-table-a"
}
}
サブネットと関連付けます。
resource "aws_route_table_association" "private_a" {
subnet_id = aws_subnet.private_a.id
route_table_id = aws_route_table.private_a.id
}
S3バケット
# S3 バケット作成
resource "aws_s3_bucket" "application" {
bucket = "example-app-bucket-${data.aws_caller_identity.current.account_id}"
tags = {
Name = "example-app-bucket"
}
}
# パブリックアクセスブロック
resource "aws_s3_bucket_public_access_block" "application" {
bucket = aws_s3_bucket.application.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# 暗号化
resource "aws_s3_bucket_server_side_encryption_configuration" "application" {
bucket = aws_s3_bucket.application.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
最小構成のS3 Gateway VPC Endpoint
最もシンプルな Terraform コードは次のとおりです。
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id # VPC ID を指定
# 東京リージョンの場合 com.amazonaws.ap-northeast-1.s3 になる
service_name = "com.amazonaws.${data.aws_region.current.name}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [
aws_route_table.private_a.id, # このルートテーブルに S3 向けルートが自動追加される
]
tags = {
Name = "example-s3-gateway-endpoint"
}
}
Gateway エンドポイント本体とルートテーブル関連付けを分離する書き方の例
上記の例では、エンドポイントの中でroute_table_idsでルートテーブルを指定していますが、逆にルートテーブルの方で指定する書き方もあります。
エンドポイント例
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${data.aws_region.current.name}.s3"
vpc_endpoint_type = "Gateway"
policy = data.aws_iam_policy_document.s3_endpoint.json
tags = {
Name = "example-s3-gateway-endpoint"
}
}
ルートテーブルとの関連付け例
resource "aws_vpc_endpoint_route_table_association" "private_a" {
vpc_endpoint_id = aws_vpc_endpoint.s3.id
route_table_id = aws_route_table.private_a.id
}
作成後のルートテーブル
Terraform 適用後、自動的に以下のルートが追加されます。
Destination Target
10.0.0.0/16 local
pl-xxxxxxxx vpce-xxxxxxxx
エンドポイントポリシーを追加する
ポリシーを指定しない場合、デフォルトでは比較的広いエンドポイントポリシーが設定されます。
本番環境では、対象バケットを限定しましょう。
data "aws_iam_policy_document" "s3_endpoint" {
statement {
sid = "AllowApplicationBucket"
effect = "Allow"
principals {
type = "*"
identifiers = ["*"]
}
actions = [
"s3:ListBucket",
"s3:GetBucketLocation",
]
resources = [
aws_s3_bucket.application.arn,
]
}
statement {
sid = "AllowApplicationBucketObjects"
effect = "Allow"
principals {
type = "*"
identifiers = ["*"]
}
actions = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
]
resources = [
"${aws_s3_bucket.application.arn}/*",
]
}
}
VPC エンドポイントへ設定します。
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${data.aws_region.current.name}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [
aws_route_table.private_a.id,
]
# ポリシーエンドポイントポリシーを割り当てる
policy = data.aws_iam_policy_document.s3_endpoint.json
tags = {
Name = "example-s3-gateway-endpoint"
}
}
※上記で説明したように Gateway エンドポイント本体とルートテーブル関連付けを分離する書き方もあります。
EC2用IAMロール
VPCエンドポイントポリシーだけでは、EC2にS3権限は付与されません。
EC2側にもIAMロールが必要です。
IAMロール
# IAM ロール作成
resource "aws_iam_role" "ec2" {
name = "example-ec2-s3-role"
assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json
}
# IAM ポリシー作成
resource "aws_iam_policy" "ec2_s3_access" {
name = "example-ec2-s3-access"
policy = data.aws_iam_policy_document.ec2_s3_access.json
}
# IAMロールへアタッチ
resource "aws_iam_role_policy_attachment" "ec2_s3_access" {
role = aws_iam_role.ec2.name
policy_arn = aws_iam_policy.ec2_s3_access.arn
}
# インスタンスプロファイル作成
resource "aws_iam_instance_profile" "ec2" {
name = "example-ec2-s3-instance-profile"
role = aws_iam_role.ec2.name
}
信頼ポリシー
data "aws_iam_policy_document" "ec2_assume_role" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = [
"ec2.amazonaws.com",
]
}
actions = [
"sts:AssumeRole",
]
}
}
S3アクセス権限(IAMロールにアタッチするポリシーの中身)
data "aws_iam_policy_document" "ec2_s3_access" {
statement {
sid = "ListApplicationBucket"
effect = "Allow"
actions = [
"s3:ListBucket",
"s3:GetBucketLocation",
]
resources = [
aws_s3_bucket.application.arn,
]
}
statement {
sid = "AccessApplicationBucketObjects"
effect = "Allow"
actions = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
]
resources = [
"${aws_s3_bucket.application.arn}/*",
]
}
}
セキュリティグループ(EC2)
Gateway エンドポイント自体にはセキュリティグループを付けません。ただし、EC2 側のセキュリティグループでは、S3 向けの HTTPS アウトバウンド通信を許可する必要があります。
シンプルな例です。
# セキュリティグループの作成
resource "aws_security_group" "ec2" {
name = "example-ec2-sg"
description = "Security group for application EC2"
vpc_id = aws_vpc.main.id
tags = {
Name = "example-ec2-sg"
}
}
# アウトバウンドルールの作成
resource "aws_vpc_security_group_egress_rule" "ec2_https" {
security_group_id = aws_security_group.ec2.id
ip_protocol = "tcp"
from_port = 443
to_port = 443
cidr_ipv4 = "0.0.0.0/0"
description = "Allow outbound HTTPS"
}
S3 プレフィックスリストに限定する
EC2 からのアクセスを S3 プレフィックスリストだけに限定することもできます。ただし、プレフィックスリストだけに限定すると、EC2 が SSM、CloudWatch Logs、外部 API などにアクセスできなくなるので、別途許可が必要になります。
# プレフィックスリストを取得する。
data "aws_prefix_list" "s3" {
name = "com.amazonaws.${data.aws_region.current.name}.s3"
}
# アウトバウンドルールを作成する。
resource "aws_vpc_security_group_egress_rule" "ec2_s3_https" {
security_group_id = aws_security_group.ec2.id
ip_protocol = "tcp"
from_port = 443
to_port = 443
prefix_list_id = data.aws_prefix_list.s3.id
description = "Allow HTTPS to Amazon S3"
}
S3バケット側からVPCエンドポイントを限定する
より厳密にする場合、S3バケットポリシーで、指定したVPCエンドポイント以外からのアクセスを拒否できます。
data "aws_iam_policy_document" "s3_bucket_policy" {
statement {
sid = "DenyAccessOutsideVpcEndpoint"
effect = "Deny"
principals {
type = "*"
identifiers = ["*"]
}
actions = [
"s3:*",
]
resources = [
aws_s3_bucket.application.arn,
"${aws_s3_bucket.application.arn}/*",
]
condition {
test = "StringNotEquals"
variable = "aws:SourceVpce"
values = [
aws_vpc_endpoint.s3.id,
]
}
}
}
resource "aws_s3_bucket_policy" "application" {
bucket = aws_s3_bucket.application.id
policy = data.aws_iam_policy_document.s3_bucket_policy.json
}
※S3バケットの Deny は強力なので注意が必要です。
設定後は、次のようなアクセスも拒否される可能性があるので、しっかりと検証してから導入しましょう。
・自分のPCからAWS CLIでアクセス
・AWSコンソールからオブジェクトを閲覧
・別VPCのEC2からアクセス
・Terraform実行環境からアクセス
・別アカウントからアクセス
・バックアップ処理からアクセス
EC2 の作成例
EC2 作成
# AMIはSSM Parameter StoreからAmazon Linux 2023の最新AMIを取得する
data "aws_ssm_parameter" "al2023_ami" {
name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}
# EC2を作成する
resource "aws_instance" "application_a" {
ami = data.aws_ssm_parameter.al2023_ami.value
instance_type = "t3.micro"
subnet_id = aws_subnet.private_a.id
vpc_security_group_ids = [
aws_security_group.ec2.id,
]
iam_instance_profile = aws_iam_instance_profile.ec2.name
associate_public_ip_address = false
tags = {
Name = "example-application-a"
}
}
まとめ
S3 Gateway VPC エンドポイントは、VPC 内に S3 のサーバーやプライベート IP を作るものではありません。実際に行っていることは、ルートテーブルへ「S3宛ての通信は、このGatewayエンドポイントへ送る」という専用ルートを追加することです。
全体を一行で表すと次のようになります。
EC2
↓
通常のS3 DNS名
↓
S3のIPアドレスへ名前解決
↓
ルートテーブルのS3プレフィックスリストに一致
↓
S3 Gateway VPC Endpoint
↓
Amazon S3
そして、通信できるかどうかと、S3 操作を実行できるかどうかは別です。
通信経路
→ ルートテーブル+Gateway Endpoint
アクセス権限
→ IAM+Endpoint Policy+Bucket Policy
この2つを分けて考えると、S3 Gateway VPC エンドポイントの仕組みがかなり整理しやすくなります。