前提条件
- 第1回から第7回までの構築が完了していること
- これまでの作業を
terraform destroy
でリソースを削除していること(NAT Gatewayを使用しない方法で検証するため) - こちらからDockerコンテナでの実行環境が準備できていること
Terraform実行環境の起動
前回と同様に、Dockerコンテナ内で作業を進めます。
# コンテナを起動してbashに入る
docker-compose run --rm terraform
これ以降のコマンドは、すべてこのコンテナ内で実行します。
基礎知識
VPCエンドポイントは、VPCとAWSサービス間のプライベート接続を提供する機能です。インターネットゲートウェイやNAT Gatewayを経由せずに、AWSサービスにアクセスできます。
WEBアプリケーション開発者の視点から見ると
ローカル開発では、すべてのサービスがlocalhost内で通信しますが、クラウドでは通常インターネット経由でサービス間通信が発生します。VPCエンドポイントは、AWSサービスへのアクセスをlocalhost通信のように内部ネットワークで完結させる仕組みです。
エンドポイントの種類
VPCエンドポイントには2つのタイプがあります。
タイプ | 対象サービス | 料金体系 | ネットワーク構成 |
---|---|---|---|
Gateway Endpoint | S3、DynamoDB | 無料(データ転送料のみ) | ルートテーブルで制御 |
Interface Endpoint | ECR、CloudWatch等 | 時間課金+データ転送料 | ENI(Elastic Network Interface)を作成 |
なぜVPCエンドポイントが必要か
現在の構成では、ECSタスクがAWSサービスにアクセスする際の通信フローは以下のとおりです。
通信元 | 通信先 | 現在の経路 | 課題 |
---|---|---|---|
ECSタスク | ECR(イメージ取得) | NAT Gateway → Internet | 通信料金、レイテンシ |
ECSタスク | CloudWatch Logs | NAT Gateway → Internet | セキュリティリスク |
ECSタスク | S3(ログ保存等) | NAT Gateway → Internet | 帯域幅の制限 |
VPCエンドポイントを使用することで、これらの通信をVPC内で完結させることができます。
PrivateLinkとは
PrivateLinkは、VPCエンドポイントの基盤技術です。AWSサービスやサードパーティのサービスをプライベートIPアドレスでアクセス可能にします。
メリット
項目 | 説明 |
---|---|
セキュリティ向上 | インターネットを経由しないため、攻撃対象面が減少 |
パフォーマンス向上 | AWS内部ネットワークを使用するため低レイテンシ |
コスト削減 | NAT Gatewayのデータ処理料金が不要 |
簡単な設定 | Security GroupとRoute Tableで制御可能 |
今回作成するネットワーク構成
リソース | 説明 | 数量 |
---|---|---|
S3 Gateway Endpoint | S3へのプライベートアクセス | 1個 |
ECR API Interface Endpoint | ECR APIへのプライベートアクセス | 1個 |
ECR DKR Interface Endpoint | Dockerイメージ取得用 | 1個 |
CloudWatch Logs Interface Endpoint | ログ送信用 | 1個 |
Secrets Manager Interface Endpoint | シークレット取得用 | 1個 |
VPC Flow Logs | ネットワークトラフィックの可視化 | 1個 |
Security Group (VPC Endpoints) | エンドポイント用セキュリティグループ | 1個 |
Terraformコードの実装
Nat Gatewayの無効化
今回はNAT Gatewayを使用せずにプライベート通信を行うため、学習においてコスト面で厄介なNat Gatewayを無効化します。
# terraform/terraform.tfvars
~~~
enable_nat_gateway = false
変数の追加
VPCエンドポイント用の変数を追加します。
# terraform/variables.tf に追加
variable "enable_vpc_endpoints" {
description = "VPCエンドポイントを有効にするかどうか"
type = bool
default = true
}
variable "enable_vpc_flow_logs" {
description = "VPC Flow Logsを有効にするかどうか"
type = bool
default = true
}
variable "flow_logs_retention_days" {
description = "VPC Flow Logsの保持期間(日)"
type = number
default = 7
}
Security Group(VPCエンドポイント用)
Interface Endpointへのアクセスを制御するSecurity Groupを作成します。
# terraform/vpc_endpoints.tf
# VPC Endpoints用Security Group
resource "aws_security_group" "vpc_endpoints" {
name = "${var.project_name}-${var.environment}-vpc-endpoints-sg"
description = "Security group for VPC Endpoints"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTPS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
}
egress {
description = "All traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-${var.environment}-vpc-endpoints-sg"
}
}
Security Group設定の説明
パラメータ | 設定値 | 設定理由 |
---|---|---|
ingress from_port/to_port | 443 | Interface EndpointはHTTPS(443)で通信するため |
ingress cidr_blocks | VPC CIDR | VPC内からのみアクセスを許可してセキュリティを確保 |
egress | すべて許可 | エンドポイントからの戻り通信を許可 |
S3 Gateway Endpoint
S3へのプライベートアクセスを提供するGateway Endpointを作成します。
# terraform/vpc_endpoints.tf
# S3 Gateway Endpoint
resource "aws_vpc_endpoint" "s3" {
count = var.enable_vpc_endpoints ? 1 : 0
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.aws_region}.s3"
route_table_ids = concat(
aws_route_table.private[*].id,
[aws_route_table.public.id]
)
tags = {
Name = "${var.project_name}-${var.environment}-s3-endpoint"
}
}
S3 Gateway Endpoint設定の説明
パラメータ | 設定値 | 設定理由 |
---|---|---|
service_name | com.amazonaws.{region}.s3 | S3サービスを指定 |
route_table_ids | Private + Public | 両方のサブネットからS3にアクセス可能にする |
vpc_endpoint_type | Gateway(暗黙的) | S3はGateway型のみサポート |
ECR Interface Endpoints
ECRからDockerイメージを取得するための2つのエンドポイントを作成します。
# terraform/vpc_endpoints.tf
# ECR API Endpoint
resource "aws_vpc_endpoint" "ecr_api" {
count = var.enable_vpc_endpoints ? 1 : 0
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.aws_region}.ecr.api"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
tags = {
Name = "${var.project_name}-${var.environment}-ecr-api-endpoint"
}
}
# ECR DKR Endpoint (Docker Registry)
resource "aws_vpc_endpoint" "ecr_dkr" {
count = var.enable_vpc_endpoints ? 1 : 0
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.aws_region}.ecr.dkr"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
tags = {
Name = "${var.project_name}-${var.environment}-ecr-dkr-endpoint"
}
}
ECR Endpoints設定の説明
パラメータ | 設定値 | 設定理由 |
---|---|---|
service_name (API) | ecr.api | ECRのAPI操作(認証、メタデータ取得)用 |
service_name (DKR) | ecr.dkr | Dockerイメージのpull/push用 |
subnet_ids | Private Subnets | ECSタスクが配置されているサブネットに設置 |
private_dns_enabled | true | ECRのパブリックDNS名をプライベートIPに解決 |
security_group_ids | VPC Endpoints SG | HTTPS通信のみ許可するSGを適用 |
CloudWatch Logs Interface Endpoint
ログ送信のためのエンドポイントを作成します。
# terraform/vpc_endpoints.tf
# CloudWatch Logs Endpoint
resource "aws_vpc_endpoint" "logs" {
count = var.enable_vpc_endpoints ? 1 : 0
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.aws_region}.logs"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
tags = {
Name = "${var.project_name}-${var.environment}-logs-endpoint"
}
}
CloudWatch Logs Endpoint設定の説明
パラメータ | 設定値 | 設定理由 |
---|---|---|
service_name | logs | CloudWatch Logsサービスを指定 |
vpc_endpoint_type | Interface | CloudWatch LogsはInterface型のみサポート |
private_dns_enabled | true | logs.{region}.amazonaws.comをプライベート解決 |
Secrets Manager Interface Endpoint
シークレット取得のためのエンドポイントを作成します。
# terraform/vpc_endpoints.tf
# Secrets Manager Endpoint
resource "aws_vpc_endpoint" "secrets_manager" {
count = var.enable_vpc_endpoints ? 1 : 0
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.aws_region}.secretsmanager"
vpc_endpoint_type = "Interface"
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.vpc_endpoints.id]
private_dns_enabled = true
tags = {
Name = "${var.project_name}-${var.environment}-secrets-manager-endpoint"
}
}
VPC Flow Logs
ネットワークトラフィックを可視化するためのFlow Logsを設定します。
# terraform/vpc_endpoints.tf
# VPC Flow Logs用のCloudWatch Log Group
resource "aws_cloudwatch_log_group" "vpc_flow_logs" {
count = var.enable_vpc_flow_logs ? 1 : 0
# 削除時にログデータも強制削除
skip_destroy = false
name = "/aws/vpc/${var.project_name}-${var.environment}"
retention_in_days = var.flow_logs_retention_days
lifecycle {
ignore_changes = [name]
}
tags = {
Name = "${var.project_name}-${var.environment}-vpc-flow-logs"
}
}
# VPC Flow Logs用のIAM Role
resource "aws_iam_role" "vpc_flow_logs" {
count = var.enable_vpc_flow_logs ? 1 : 0
name = "${var.project_name}-${var.environment}-vpc-flow-logs-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "vpc-flow-logs.amazonaws.com"
}
}
]
})
tags = {
Name = "${var.project_name}-${var.environment}-vpc-flow-logs-role"
}
}
# VPC Flow Logs用のIAM Policy
resource "aws_iam_role_policy" "vpc_flow_logs" {
count = var.enable_vpc_flow_logs ? 1 : 0
name = "${var.project_name}-${var.environment}-vpc-flow-logs-policy"
role = aws_iam_role.vpc_flow_logs[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams"
]
Resource = "*"
}
]
})
}
# VPC Flow Logs
resource "aws_flow_log" "main" {
count = var.enable_vpc_flow_logs ? 1 : 0
iam_role_arn = aws_iam_role.vpc_flow_logs[0].arn
log_destination = aws_cloudwatch_log_group.vpc_flow_logs[0].arn
traffic_type = "ALL"
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-${var.environment}-vpc-flow-logs"
}
}
VPC Flow Logs設定の説明
パラメータ | 設定値 | 設定理由 |
---|---|---|
traffic_type | ALL | ACCEPT/REJECT両方のトラフィックを記録して完全な可視化 |
log_destination | CloudWatch Logs | ログの検索・分析が容易なため |
retention_in_days | 7 | 開発環境ではコスト削減のため短期間に設定 |
出力の追加
作成したリソースの情報を出力に追加します。
# terraform/outputs.tf に追加
# VPC Endpoints関連の出力
output "vpc_endpoint_s3_id" {
description = "S3 VPC Endpoint ID"
value = try(aws_vpc_endpoint.s3[0].id, "")
}
output "vpc_endpoint_ecr_api_id" {
description = "ECR API VPC Endpoint ID"
value = try(aws_vpc_endpoint.ecr_api[0].id, "")
}
output "vpc_endpoint_ecr_dkr_id" {
description = "ECR DKR VPC Endpoint ID"
value = try(aws_vpc_endpoint.ecr_dkr[0].id, "")
}
output "vpc_endpoint_logs_id" {
description = "CloudWatch Logs VPC Endpoint ID"
value = try(aws_vpc_endpoint.logs[0].id, "")
}
output "vpc_endpoint_secrets_manager_id" {
description = "Secrets Manager VPC Endpoint ID"
value = try(aws_vpc_endpoint.secrets_manager[0].id, "")
}
output "vpc_flow_logs_id" {
description = "VPC Flow Logs ID"
value = try(aws_flow_log.main[0].id, "")
}
output "vpc_flow_logs_group_name" {
description = "VPC Flow Logs CloudWatch Log Group名"
value = try(aws_cloudwatch_log_group.vpc_flow_logs[0].name, "")
}
リソースを作成
Terraformコンテナ内で以下のコマンドを実行します。
# terraformディレクトリへ移動
cd terraform
# 実行計画の確認
terraform plan
# リソースの作成
terraform apply
VPCエンドポイントの作成には2〜3分程度かかります。
注意: ECSタスクを動かすには、事前にECRにDockerイメージをプッシュしておく必要があります。詳細は第7回の記事の「ECRへのイメージプッシュ」セクションを参照してください。
作成結果の確認
# VPCエンドポイントの確認
terraform output vpc_endpoint_s3_id
terraform output vpc_endpoint_ecr_api_id
terraform output vpc_endpoint_ecr_dkr_id
terraform output vpc_endpoint_logs_id
動作確認
VPCエンドポイントの状態確認
AWS Management ConsoleでVPCエンドポイントが正しく作成されていることを確認します。
VPC > エンドポイント
エンドポイント | エンドポイントタイプ | ステータス |
---|---|---|
S3 | Gateway | 使用可能 |
ECR API | Interface | 使用可能 |
ECR DKR | Interface | 使用可能 |
CloudWatch Logs | Interface | 使用可能 |
Secrets Manager | Interface | 使用可能 |
VPC Flow Logsの確認
ネットワークトラフィックが正しくログに記録されていることを確認します。
# Flow Logsの確認
aws logs tail $(terraform output -raw vpc_flow_logs_group_name) \
--follow \
--format short
正常なFlow Logsのフォーマット例
# 送信方向
2025-07-30T15:10:35 2 123456789012 eni-xxxxxxxxxxxxxxxxx 10.0.1.8 54.xxx.xxx.10 443 51804 6 1 40 1753888235 1753888263 ACCEPT OK
# 受信方向
2025-07-30T15:10:35 2 123456789012 eni-xxxxxxxxxxxxxxxxx 54.xxx.xxx.10 10.0.1.8 51804 443 6 1 60 1753888235 1753888263 ACCEPT OK
通信経路の確認
VPCエンドポイント経由で通信していることを確認します。
# ECSタスクのENIを特定
TASK_ARN=$(aws ecs list-tasks \
--cluster $(terraform output -raw ecs_cluster_id) \
--service-name $(terraform output -raw ecs_service_name) \
--query 'taskArns[0]' \
--output text)
# タスクの詳細からENIを取得(ECRにイメージをプッシュしていること前提)
ENI_ID=$(aws ecs describe-tasks \
--cluster $(terraform output -raw ecs_cluster_id) \
--tasks $TASK_ARN \
--query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' \
--output text)
# ENIのプライベートIPを確認
aws ec2 describe-network-interfaces \
--network-interface-ids $ENI_ID \
--query 'NetworkInterfaces[0].PrivateIpAddress' \
--output text
コスト管理(料金比較)
項目 | NAT Gateway利用時 | VPCエンドポイント利用時 |
---|---|---|
NAT Gateway(2AZ) | $0.045 × 2 × 24時間 × 30日 = 約$65/月 | $0 |
NAT Gatewayデータ処理 | $0.045/GB | $0 |
Interface Endpoint(4個) | $0 | $0.01 × 4 × 24時間 × 30日 = 約$29/月 |
データ転送料金 | 標準料金 | 同一AZ内は$0.01/GB |
月間コスト削減額の例(100GB/月の通信量の場合)
コスト項目 | 削減額 |
---|---|
NAT Gateway固定費 | $65 |
データ処理料金(100GB) | $4.5 |
VPCエンドポイント費用 | -$29 |
実質削減額 | 約$40.5/月 |
次のステップ
次回は、フロントエンド用のS3とCloudFront構築を行います。
- S3バケットによる静的ウェブサイトホスティング
- CloudFrontディストリビューションの設定
- Origin Access Control(OAC)によるセキュリティ設定
- HTTPS化とカスタムドメイン設定(オプション)