AWS PrivateLinkで安全な接続を実現
IBMの2024年版データ侵害調査では、グローバル平均の被害額が約4.88百万ドル(約数億円規模)に達したと報告されています[1]。実務では、開発者体験や連携要件を優先するあまり、NAT越え(NAT: プライベートIPをパブリックに変換して外部へ出る仕組み)やパブリックエンドポイント経由の設計が黙認されることが少なくありません。主要クラウドの機能群を俯瞰すると、トラフィックを外に出さないこと自体をアーキテクチャで担保する手段が、最小攻撃面・コンプライアンス適合・運用単純化の面で優位に立つ局面が増えています。AWS PrivateLinkはこの要件に正面から応える仕組みで、VPCエンドポイント(Interface型。ENI[Elastic Network Interface: 仮想NIC]を作成)を介して、インターネット・NAT・VPNを経由せずに、他VPCやSaaSのサービスへプライベート到達性を提供します[2,4,5]。
なぜPrivateLinkなのか:インターネット非経由という設計の意味
PrivateLinkの価値は一言でいえば到達性の逆転にあります。従来の公開エンドポイントは、世界に対して晒し、そこから防御する思想でした。PrivateLinkでは、消費側VPCにInterface型のVPCエンドポイントENIを生成し、そこから提供側のNetwork Load Balancer(NLB。L4ロードバランサ)にプライベート接続します[2,3]。ルーティングはAWS内部ネットワークで完結し、IGW(インターネットゲートウェイ)やNAT、VPNや専用線の有無に依らずに閉域でレイヤ4(TCP/UDP)相当の到達性を維持します[4,5]。認可は二段階で決まります。サービス提供側はエンドポイントサービスに対して利用アカウントやARN単位で許可を与え[6]、同時に消費側は自身のVPCエンドポイントにポリシーを付与して呼び出し元・宛先の条件を縛れます[7]。これにより、相互に意図しない第三者流入を防ぐ、ゼロトラストの考え方に沿ったモデルを実現しやすくなります。
しばしば比較されるVPC PeeringやTransit Gatewayは、L3(IPルーティング)での経路共有が主眼で、広域なVPC間通信やフルメッシュ・スター型のハブアンドスポークで力を発揮します。一方でPrivateLinkはサービス単位の最小公開に向いています。経路は共有せず、NLB配下の特定ポートだけに絞るため、ネットワーク境界が明確で、セキュリティレビューが通りやすいのが実務上の利点です。SaaSとつなぐ場合にも、提供側が自らのアカウントにNLBとエンドポイントサービスを持ち、消費側がVPCエンドポイントを作成するだけで、相互にCIDRやルートテーブルを晒す必要がありません[2]。
アーキテクチャ比較と設計判断:Peering・TGW・Direct Connectとの住み分け
判断軸は三つに集約できます。まず到達範囲の粒度です。ネットワーク全体の疎通が必要であればPeeringやTGWが適任ですが、APIやDBプロキシ、メッセージングといった特定ポートの公開ならPrivateLinkが過剰な経路共有を避けられます。次にセキュリティ境界の明確さです。PeeringやTGWはセグメンテーション設計とNACL・SG運用の巧拙が問われます。PrivateLinkはNLB配下のサービス境界に制約されるため、監査や委託先接続で説明しやすいことが多いと考えます。最後にコスト構造です。PrivateLinkの費用はエンドポイントの時間課金とデータ処理(GB)課金の組み合わせで決まり[9]、NAT Gatewayも同様に時間+データ処理で課金されます[8]。実際の優劣はリージョンやトラフィックプロファイルで異なるため、ピーク/オフピークの流量、AZ数、エンドポイント数を含めたモデルで試算するのが現実的です。対外SaaSがPrivateLinkに対応しているなら、セキュリティだけでなくデータ転送料の観点でも有利になり得ます。もちろんDirect Connect(専用線)やVPNと組み合わせたハイブリッド構成でも、オンプレから消費側VPCへ入り、その先はPrivateLinkでサービスへ、という多段の閉域経路を構築できます[4]。
実装ガイド:提供側・消費側・DNS/TLSを一気通貫で構築
提供側(サービスプロバイダ)をCDKで構築:NLB + エンドポイントサービス
提供側ではNLBをマルチAZで構築し、TLS終端(サーバ証明書をNLBで管理)またはTLSパススルー(バックエンドでTLSを維持)を選択します。mTLS(相互TLS)を使う場合は、NLBでTLSパススルーにしてバックエンドで相互認証を実装する構成が現実的です。以下はTypeScriptのCDK抜粋です(要点のみ)。
import * as cdk from 'aws-cdk-lib';
import { Stack, StackProps, CfnOutput, Duration } from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as certificatemanager from 'aws-cdk-lib/aws-certificatemanager';
import * as iam from 'aws-cdk-lib/aws-iam';
class ProviderStack extends Stack {
constructor(scope: cdk.App, id: string, props?: StackProps) {
super(scope, id, props);
const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { isDefault: false, vpcId: 'vpc-xxxxxxxx' });
const nlb = new elbv2.NetworkLoadBalancer(this, 'NLB', {
vpc,
crossZoneEnabled: true,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});
// NLBでTLS終端する場合の例(サーバ証明書はACMで管理)
const cert = certificatemanager.Certificate.fromCertificateArn(
this, 'Cert', 'arn:aws:acm:ap-northeast-1:111122223333:certificate/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
);
const listener = nlb.addListener('TLSListener', { port: 443, certificates: [cert] });
const tg = new elbv2.NetworkTargetGroup(this, 'Tg', {
vpc,
port: 8443,
protocol: elbv2.Protocol.TCP,
healthCheck: { port: '8443', healthyThresholdCount: 3, interval: Duration.seconds(10) },
});
listener.addTargetGroups('Attach', { targetGroups: [tg] });
// エンドポイントサービスの公開と許可(接続は承認制)
const service = new ec2.VpcEndpointService(this, 'EndpointService', {
vpcEndpointServiceLoadBalancers: [nlb],
acceptanceRequired: true,
allowedPrincipals: [new iam.ArnPrincipal('arn:aws:iam::444455556666:root')],
});
new CfnOutput(this, 'ServiceName', { value: service.vpcEndpointServiceName });
}
}
ターゲットはEC2の固定ポートや、ECS/Fargateで稼働するアプリに合わせます。TLS終端をNLBで行う場合は、アプリ側はプレーンTCPで受け、証明書の運用はACMで自動更新できます。TLSパススルーを選ぶときはバックエンドでTLSを維持し、ポリシーや暗号スイートの統制をアプリに寄せます。
消費側(クライアントVPC)をTerraformで作成:Interface VPC Endpoint
消費側ではInterfaceエンドポイントを作成します。セキュリティグループはエンドポイントENIにつく点に注意が必要で、クライアント(アプリ)からエンドポイントENIへのインバウンド許可が必要です。Private DNSは、提供側がエンドポイントサービスのPrivate DNS名を提供している場合は有効化するだけでドメイン名が自動でVPCEに解決されます。提供側がPrivate DNS名を提供していない場合は無効にし、Route 53のプライベートゾーンで名前解決を制御します[4,5]。
provider "aws" {
region = "ap-northeast-1"
}
resource "aws_security_group" "vpce" {
name = "vpce-sg"
description = "Allow app to reach VPCE"
vpc_id = "vpc-yyyyyyyy"
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"] # アプリ側からの到達を許可
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_vpc_endpoint" "svc" {
vpc_id = "vpc-yyyyyyyy"
service_name = "com.amazonaws.vpce.ap-northeast-1.vpce-svc-0123456789abcdef0"
vpc_endpoint_type = "Interface"
private_dns_enabled = false
subnet_ids = ["subnet-a1", "subnet-b2"]
security_group_ids = [aws_security_group.vpce.id]
policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Principal = "*",
Action = "*",
Resource = "*"
}]
})
}
エンドポイント作成後、提供側が接続要求を承認するまで待機します。提供側の許可モデルを厳密にしたい場合は、サービス権限に消費者アカウントのARNを追加し、都度承認にします[6,7]。
AWS CLIでの承認と権限付与、ポリシー更新
運用ではIaCだけでなく、権限や承認の例外対応をCLIで行う場面があります。以下はサービス提供側アカウントでの承認と、利用可能プリンシパルの追加例です[6]。
# 提供側: 接続要求の承認
aws ec2 accept-vpc-endpoint-connections \
--service-id vpce-svc-0123456789abcdef0 \
--vpc-endpoint-ids vpce-0abc1def234567890
# 提供側: 利用可能プリンシパルの追加
aws ec2 modify-vpc-endpoint-service-permissions \
--service-id vpce-svc-0123456789abcdef0 \
--add-allowed-principals arn:aws:iam::444455556666:root
# 消費側: エンドポイントポリシーを更新(例)
aws ec2 modify-vpc-endpoint \
--vpc-endpoint-id vpce-0abc1def234567890 \
--policy-document file://vpce-policy.json
承認を自動化したい場合は、CloudTrailのCreateVpcEndpointConnectionリクエストをEventBridgeで検出し、Lambdaで事前登録済みアカウントのみ承認するワークフローに落とし込むと運用が安定します。
DNSと名前解決:Route 53プライベートゾーンで意図したFQDNを提供
Private DNSをオンにすると、提供側が設定した検証済みのドメイン名に対して透過的にVPCEへ解決されます[4,5]。自社ドメインやSaaS固有ドメインで提供する場合は、プライベートホストゾーンにCNAMEやALIASでVPCE固有のDNS名へ向けます。アベイラビリティゾーン(AZ)ごとに異なるDNS名が払い出されるため、レイテンシやフェイルオーバー戦略に応じて解決戦略を決めます。以下はRoute 53の変更バッチ例です。
{
"Comment": "Map internal API to VPCE",
"Changes": [
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "api.internal.example.local",
"Type": "CNAME",
"TTL": 30,
"ResourceRecords": [
{ "Value": "vpce-0123456789abcdef0-xxxx.vpce-svc-zz.ap-northeast-1.vpce.amazonaws.com" }
]
}
}
]
}
DNSが正しく切り替わったかはdigやnslookupでVPCEのプライベートIPへ解決されるかを確認します。証明書はNLBで終端する場合、SANに自社FQDNを含めた証明書をACMで管理します。自己署名やプライベートCAを使う場合は、クライアントに信頼ルートを配布し、検証失敗時のフォールバックをアプリ側で設計します。
アプリケーションからの接続:タイムアウトとリトライ戦略
アプリ側は通常のTLS接続のままで構いません。Private DNSが有効な場合は既存のエンドポイント名をそのまま使えます。プライベートCAを使うときは検証用のルート証明書を配布し、タイムアウト・リトライはネットワーク揺らぎを前提に指数バックオフで設計します。
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retries = Retry(total=5, backoff_factor=0.3, status_forcelist=[500, 502, 503, 504])
session.mount("https://", HTTPAdapter(max_retries=retries))
# プライベートCAを使う場合は verify にルート証明書を指定
VERIFY_PATH = "/etc/ssl/certs/private-ca.pem"
try:
resp = session.get(
"https://api.internal.example.local/health",
timeout=(2.0, 5.0),
verify=VERIFY_PATH
)
resp.raise_for_status()
print("OK", resp.text)
except requests.exceptions.SSLError as e:
# 証明書検証エラー。配布やSANを確認
print("TLS verify failed", e)
except requests.exceptions.ConnectTimeout:
# VPCEのSG/ルートやNLBのヘルスチェックを確認
print("Connect timeout")
except requests.exceptions.ReadTimeout:
print("Read timeout")
except requests.RequestException as e:
print("HTTP error", e)
この例では接続と読み取りのタイムアウトを分け、サーバエラーは再試行する構成にしています。アプリケーションレベルでもサーキットブレーカを併用すると、障害時の復帰が安定します。
運用・パフォーマンス・コスト:現実解としての最適点を探る
パフォーマンス特性とベンチマーク手法
InterfaceエンドポイントはAZごとにENIを持ち、トラフィックはAWSのプライベートネットワーク内を流れます[4,5]。レイテンシは同一リージョン・同一AZ内では小さく、適切にチューニングされた環境では、アプリの処理を除くネットワークの追加オーバーヘッドが一桁ミリ秒台に収束するケースが報告されています。もっとも、バックエンドの実装やTLSハンドシェイク、アイドル切断の扱いで差が出ます。計測はワークロードに即したプロファイルで行い、TLSの再利用、キープアライブ、コネクションプールの有無を明示して比較します。以下はwrkによる例です。
# 事前にulimitやカーネルパラメータでFD/ポート枯渇を避ける
wrk -t8 -c256 -d60s --timeout 5s https://api.internal.example.local/echo
# 参考出力(例)
# Running 1m test @ https://api.internal.example.local/echo
# 8 threads and 256 connections
# Thread Stats Avg Stdev Max +/- Stdev
# Latency 2.10ms 0.80ms 12.0ms 95.00%
# Req/Sec 8.5k 0.3k 9.1k 75.0%
# 30,000,000 requests in 60.00s, 3.6GB read
# Non-2xx or 3xx responses: 0
# Socket errors: connect 0, read 0, write 0, timeout 0
出力は環境で大きく変動します。計測の再現性を高めるため、同一AZ・同一インスタンスタイプ・固定AMIで比較し、NLBのクロスゾーン有効化の有無と、VPCEの配置AZが一致しているかを確認してください。
コストの捉え方:データ処理と時間課金、NATとの対比
PrivateLinkの費用はエンドポイントの時間単価とデータ処理量に依存します[9]。NAT Gatewayも時間とGB課金があります[8]。高トラフィックなサービスほどデータ単価の影響が支配的になります。費用試算では、アクティブAZ数とエンドポイント数、時間帯別のピーク流量、TLS終端の有無によるCPU負荷(バックエンド規模への波及)を織り込み、月次でのアロケーションをタグで可視化します。ブレークイーブンはワークロード依存のため、両案(NAT経由とPrivateLink)の単価表[8,9]を前提に、実測トラフィックでの比較が実務的です。
監視・運用:ヘルスチェック、ログ、障害切り分け
NLBのTargetResponseTimeやHealthyHostCountなどのメトリクスを監視し、5xx発生時にはアプリログと関連付けます。Interfaceエンドポイント自体の専用メトリクスは限定的ですが、エンドポイントENIのNetwork*メトリクスや、クライアントアプリのエラーレートで異常を検知できます。障害切り分けでは、まずセキュリティグループの向き(エンドポイントENI側でのインバウンド許可)を再確認し、次にNLBリスナーとターゲットのプロトコル齟齬、証明書のSAN不一致、プライベートDNSの競合順を追って潰します。証明書のローテーションはACMに寄せ、更新ウィンドウ内にデプロイを当て込むと人的作業を減らせます。
mTLSや細粒度制御:さらに攻撃面を縮める
クライアント証明書による相互TLSを採用すると、盗難クレデンシャルや踏み台からの誤用を抑制できます。PrivateLink経路でmTLSを行う場合は、NLBをTLSパススルー(TCPリスナー)とし、バックエンドで相互認証を実施します。証明書の配布と失効リストの反映は自動化し、短命証明書とローテーションで鍵の露出時間を最小化します。さらに、エンドポイントポリシーでVPCやプリンシパル、条件(ソースENIなど)に基づいて許可を絞ると、ネットワークとアイデンティティの多層でガードできます[7]。
トラブルシューティングの実例:よくある落とし穴と対策
DNS循環と名前解決の優先順位
プライベートゾーンの同名ホストが複数のVPCに存在し、オーソリティが想定と異なるケースでは、クライアントVPCに関連付けられたゾーンが優先されます。このため、SaaS接続で自社ドメインと衝突しない命名規則を最初に決めておくと、将来のマルチアカウント展開での競合を避けられます。踏み込み調査では、VPC内のリゾルバログをCloudWatchに出し、対象FQDNのクエリ分布を時系列で見て、どのゾーンが回答しているかを判別します。
もう一つの典型は証明書検証の失敗です。NLB終端で提供するFQDNのSANに、クライアントが解決する最終名が含まれていないと、ハンドシェイクが失敗します。リネームやゾーン切替を伴う移行では、CNAMEチェーンの最終名を常にSANに含める運用ルールを設けると事故が減ります。
セキュリティグループの向きとNLBヘルス
Interfaceエンドポイントのセキュリティグループは、エンドポイントENIへ入ってくるトラフィックを許す設定である点を忘れがちです。クライアントからの443番到達を許す一方で、バックエンドのターゲットグループ側では、NLBからのヘルスチェックポートを許可する必要があります。NLBはセキュリティグループを持たないため、ターゲット側のセキュリティグループやOSレベルのフィルタで、NLBのサブネットCIDRからのトラフィックを許可します。
検証用バックエンド(Flask)とNLB設定のミニマム例
最後に、簡易なバックエンドを立ち上げてNLB経由でPrivateLinkを疎通させる最小構成を示します。アプリはTLS終端をNLBに任せ、アプリは8443/TCPでプレーンに受けます。
from flask import Flask, jsonify
from socket import gethostname
app = Flask(__name__)
@app.get('/health')
def health():
return jsonify(status='ok', host=gethostname())
@app.get('/echo')
def echo():
return jsonify(message='hello', host=gethostname())
if __name__ == '__main__':
# gunicorn等の本番サーバ推奨。ここでは簡易サーバ。
app.run(host='0.0.0.0', port=8443)
EC2に配置する場合は上記ポートを開放し、NLBのターゲットグループをTCP:8443に合わせます。ECS/Fargateで運用する際はターゲットタイプをIPにし、サービスディスカバリやオートスケーリングと組み合わせると、負荷に応じた拡縮に自然に追随できます。
まとめ:安全と速さとコストの「三方良し」を現場で形にする
PrivateLinkは、設計段階でインターネット経路を断つという決断により、攻撃面を最小化しながら開発者の接続体験を損なわないアーキテクチャを提供します。ネットワーク全体の共有をやめ、サービス単位の最小公開に収斂させることで、監査容易性と変更影響の小ささを同時に獲得できます。提供側はNLBとエンドポイントサービス、消費側はInterfaceエンドポイントと適切なDNS/TLSの運用という明快な分担により、責任境界もクリアになります。あなたのシステムで、パブリック経由の連携が当たり前になっている箇所はどこでしょうか。影響範囲の小さな一箇所から、閉域化の効果を数字で確かめる。まずはステージングの一点突破で導入し、ベンチとコスト実測を回し、次回の運用レビューに結果を持ち込む。そんな小さな一歩が、組織全体の接続設計を刷新する起点になります。
参考文献
- IBM Newsroom. IBM report: Escalating data breach disruption pushes costs to new highs (2024-07-30). https://newsroom.ibm.com/2024-07-30-ibm-report-escalating-data-breach-disruption-pushes-costs-to-new-highs
- AWS Architecture Blog. Building SaaS Services for AWS Customers with PrivateLink. https://aws.amazon.com/blogs/architecture/building-saas-services-for-aws-customers-with-privatelink/
- AWS Architecture Blog. Building SaaS Services for AWS Customers with PrivateLink – Traffic flows. https://aws.amazon.com/blogs/architecture/building-saas-services-for-aws-customers-with-privatelink/#:~:text=Traffic%20that%20flows%20to%20the
- AWS Documentation. Access AWS services through AWS PrivateLink. https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-aws-services.html
- AWS Documentation. Access AWS services through AWS PrivateLink – without using an internet gateway. https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-aws-services.html#:~:text=without%20using%20an%20internet%20gateway
- AWS Documentation. Configure an endpoint service. https://docs.aws.amazon.com/vpc/latest/privatelink/configure-endpoint-service.html
- AWS Documentation. Control access to services using endpoint policies. https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-access.html
- AWS Pricing. Amazon VPC pricing (includes NAT Gateway). https://aws.amazon.com/en/vpc/pricing/
- AWS Pricing. AWS PrivateLink pricing. https://aws.amazon.com/privatelink/pricing/