【AWS】Gateway 型 VPC エンドポイントで S3 バケットにプライベート接続する環境の構築手順(Terraform)

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/16local同じVPC内への通信
pl-xxxxxxxxvpce-xxxxxxxxS3宛ての通信
0.0.0.0/0NAT 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 エンドポイントの仕組みがかなり整理しやすくなります。