15. VPC Endpoints / PrivateLink
プライベートサブネットの中から S3 や DynamoDB、SSM などの AWS API を NAT Gateway 経由ではなく プライベートに直接呼ぶ仕組み。セキュリティとコスト両面のメリット。
なぜ VPC Endpoint を使うか
プライベートサブネットの EC2 から S3 にアップロードしたい時、NAT Gateway 経由でインターネットを通って S3 に到達する のが標準ルート。これには:
- コスト: NAT Gateway は時間 $0.045 + データ処理量 $0.045/GB
- セキュリティ: トラフィックがインターネットに出る(再度 AWS 内に戻るとはいえ経路上で)
- パフォーマンス: NAT Gateway を経由する分のレイテンシ
VPC Endpoint を使えば VPC 内から AWS API へ直接プライベートに接続。NAT 不要、データ転送料は VPC 内(ほぼゼロ)、セキュリティ上もインターネットを通らない。
2 種類の Endpoint
| Gateway endpoint | Interface endpoint | |
|---|---|---|
| 対応サービス | S3 と DynamoDB のみ | ほとんどの AWS サービス |
| 仕組み | ルートテーブルにルート追加 | ENI を作って DNS を上書き |
| 料金 | 無料 | $0.01/時間/AZ + データ処理 $0.01/GB |
| 用途 | S3 / DynamoDB の VPC 内利用 | SSM、ECR、Logs、Secrets Manager 等 |
Gateway endpoint(S3 / DynamoDB)
必ず使うべき。無料で得しかない。
# リージョン名を取得(service_name に埋め込むため)
data "aws_region" "current" {}
# S3 への Gateway Endpoint(無料)
# 指定したルートテーブルに「S3 宛 = この endpoint」のルートが自動追加される
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"
# private 用 RT に加えて、public RT もリストにして concat で結合
route_table_ids = concat(
[aws_route_table.private.id],
[for rt in aws_route_table.public : rt.id]
)
tags = { Name = "s3-gateway" }
}
# DynamoDB への Gateway Endpoint(無料)
resource "aws_vpc_endpoint" "dynamodb" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${data.aws_region.current.name}.dynamodb"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.private.id]
}
Interface endpoint
各 AZ にプライベート ENI(Elastic Network Interface)を作成し、Route 53 DNS で AWS の API ドメインを ENI に向けます。
# Interface Endpoint 用の SG(VPC 内からの 443 を許可)
resource "aws_security_group" "vpc_endpoint" {
name_prefix = "vpc-endpoint-"
vpc_id = aws_vpc.main.id
}
# VPC CIDR からの HTTPS のみ許可(AWS API は全部 443)
resource "aws_vpc_security_group_ingress_rule" "endpoint_https" {
security_group_id = aws_security_group.vpc_endpoint.id
cidr_ipv4 = aws_vpc.main.cidr_block
from_port = 443
to_port = 443
ip_protocol = "tcp"
}
# 必要そうな endpoint をまとめて
# サービスごとに 1 つずつ書くと冗長なので、リスト + for_each で展開
locals {
interface_endpoints = [
"ssm", # SSM Session Manager
"ssmmessages", # SSM Session Manager
"ec2messages", # SSM
"ecr.api", # ECR API
"ecr.dkr", # ECR Docker registry
"logs", # CloudWatch Logs
"secretsmanager", # Secrets Manager
"kms", # KMS
"sts", # STS(IAM ロール用)
]
}
# 上のリストから 9 個の interface endpoint を生成
resource "aws_vpc_endpoint" "interface" {
for_each = toset(local.interface_endpoints)
vpc_id = aws_vpc.main.id
# サービス名は com.amazonaws.{region}.{service} の形
service_name = "com.amazonaws.${data.aws_region.current.name}.${each.key}"
vpc_endpoint_type = "Interface"
# 各 AZ の private subnet に ENI を作る(高可用性のため複数 AZ 推奨)
subnet_ids = [for s in aws_subnet.private : s.id]
security_group_ids = [aws_security_group.vpc_endpoint.id]
# AWS の DNS 名を VPC 内 ENI に向ける(アプリ側のコード変更不要)
private_dns_enabled = true # AWS の DNS 名で接続できる
tags = { Name = each.key }
}
private_dns_enabled = true がポイント。これで secretsmanager.ap-northeast-1.amazonaws.com のような AWS の標準ドメインが VPC 内 ENI の IP に解決されるようになる。アプリ側のコード変更は不要。
コスト比較
例: プライベートサブネットの ECS タスクが S3 と Secrets Manager と CloudWatch Logs を使う場合(月)。
| NAT Gateway 経由 | VPC Endpoint 経由 | |
|---|---|---|
| NAT GW 時間料 | $33 / 1 GW | 不要 (or 縮小) |
| NAT データ処理 | 10 GB なら $0.45 | 0 |
| S3 endpoint | — | 無料 |
| Interface endpoint × 3 | — | 3 × $7.2 = $21.6 (1 AZ) / $43.2 (2 AZ) |
| 合計(2 AZ 想定) | ~$66 | ~$43.2 |
NAT Gateway を完全廃止できれば月 $66 セーブ。ただし「外向きインターネットが必要なケース(パッケージ更新等)」が無いか確認が必要。
PrivateLink
VPC Endpoint を 第三者のサービス(Datadog、Snowflake、自社の別 VPC のサービス)に向ける場合は PrivateLink を使う。
# 3rd party SaaS への PrivateLink 接続例
# service_name は相手側が提供する vpce-svc-xxxx を埋め込む
resource "aws_vpc_endpoint" "datadog" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.vpce.ap-northeast-1.vpce-svc-xxxxxxxxx"
vpc_endpoint_type = "Interface"
subnet_ids = [for s in aws_subnet.private : s.id]
security_group_ids = [aws_security_group.vpc_endpoint.id]
}