PrivateLinkで夜中2時に呼ばれた話──3年本番運用で見えた設計の地雷

VPCピアリングからPrivateLinkへ移行して本番障害を経験。DNS解決とセキュリティグループの分離、リージョン間通信の落とし穴など、実務で痛感した3つの設計パターンを公開します。

PrivateLinkへの移行で地獄を見た話

去年の夏、うちのチームはVPCピアリングで管理していた3つのAWSアカウント間の通信を、PrivateLinkに統合するプロジェクトを始めた。当時の僕は「PrivateLinkは新しいし、セキュリティも良さそうだし、楽勝だろ」くらいに考えていたんですよね。今思うと、本当に甘い。

最初の2週間はドキュメント通りに進んだ。VPCエンドポイント作成して、ServiceNameを設定して、ルートテーブルを更新する。でも本番環境に流して3日目、夜中2時に電話が来た。一部のリージョン間通信がタイムアウトしていた

原因は、PrivateLinkの設計で「DNSと実際のIPルーティングの層が分離されていた」ことに気づいていなかったこと。VPCピアリングみたいに「接続すれば全部つながる」わけじゃないんです。

実務で気づいたPrivateLink設計の3つの落とし穴

1. DNS解決とセキュリティグループが独立している罠

VPCピアリングでは、接続したらセキュリティグループだけで制御できる。でもPrivateLinkは違う。

# VPCピアリング時代:これだけで十分だった
# セキュリティグループ
source: 10.1.0.0/16  # Consumer VPC
protocol: tcp
port: 443

これに対してPrivateLinkは、実はこんなふうに3層で独立して制御されるんです:

# 1層目:ENI側のセキュリティグループ
source: 10.1.0.0/16  # Consumer VPC CIDR
protocol: tcp
port: 8443(サービス側ポート)

# 2層目:Network Load Balancer側
source: 10.1.0.0/16
protocol: tcp
port: 8443(元々のアプリケーションポート)

# 3層目:VPC Endpoint側
source: 10.1.0.0/16
protocol: tcp
port: 443(Consumer側の接続ポート)

どこか1つでも詰まるとつながらない。本番で「あれ、なんで通らねえんだ」って3時間ハマったのはこれが原因でした。

2. マルチAZ構成での「偏ったトラフィック分散」問題

うちの本番構成は東京リージョンでAZ-1a, 1c, 1dの3つを使ってました。当然PrivateLinkも全AZに展開するだろ、と思ってたんですよ。

でも蓋を開けると、実際のトラフィック分散がこんなことになってた:

AZターゲットトラフィック分散
az-1aEC2 x3台60%
az-1cEC2 x2台30%
az-1dEC2 x1台10%

なぜこんなことが起きるのか。原因は、Consumer側(接続元)がどのAZからアクセスしてるかに依存するから。特定のAZからのECSタスク群が全部az-1aに偏ってしまう。VPCピアリングではこんなことなかったんですけど、NLB経由だとこうなる。

結果として、az-1aのインスタンスだけ高負荷になって、CPUが80%に達することもありました。

3. 複数アカウント間で「VPCエンドポイント接続の管理地獄」

PrivateLinkの厄介なところは、サービス側がProvider、接続側がConsumerという関係があること。

Account A(Provider)
└─ EC2 + NLB
   └─ VPC Endpoint Service

Account B(Consumer)
└─ EC2インスタンス
   └─ VPC Endpoint(Service)に接続

Account C(Consumer)
└─ Lambda関数
   └─ VPC Endpoint(Service)に接続

このConsumerが増えると、Provider側で「誰がどのVPCエンドポイントに接続を試みてるか」の管理がめちゃめちゃになるんですよ。AWS側には以下みたいな接続待ちがたまります:

Pending Connections:
- vpce-0abc123def456(Account B from VPC vpc-xxxxx)
- vpce-0def456ghi789(Account C from Lambda...の情報がない)
- vpce-0ghi789jkl012(不明)

本番では「なんか接続が拒否されてる」という報告が来て、調べてみたら、セキュリティチームが誤ってpending状態の接続を拒否してたという。ServiceNameだけじゃ相手が誰かわかりにくいから、運用が本当に面倒なんです。

実装した失敗から学んだ実践的な設計

去年の秋、本格的にPrivateLinkを再設計し直しました。以下がうちが落ち着いた構成です。

graph TB
    subgraph "Account A (Provider)"
        subgraph "VPC vpc-provider (10.0.0.0/16)"
            subgraph "AZ-1a"
                ALB1["ALB<br/>10.0.1.0/24"]
                EC2_1A["EC2 x2<br/>10.0.1.10-11"]
            end
            subgraph "AZ-1c"
                EC2_1C["EC2 x2<br/>10.0.2.10-11"]
            end
            subgraph "AZ-1d"
                EC2_1D["EC2 x1<br/>10.0.3.10"]
            end
        end
        NLB["NLB<br/>Port 443<br/>Cross-AZ"]
        SVC["VPC Endpoint Service<br/>com.amazonaws.vpce.ap-northeast-1<br/>service-xxx"]
        ALB1 --> EC2_1A
        EC2_1C --> NLB
        EC2_1D --> NLB
        ALB1 --> NLB
        NLB --> SVC
    end

    subgraph "Account B (Consumer)"
        subgraph "VPC vpc-consumer-b (10.1.0.0/16)"
            ECS1["ECS Task<br/>10.1.0.0/24"]
            VPCE1["VPC Endpoint<br/>vpce-consumer-b-001"]
        end
        ECS1 --> VPCE1
    end

    subgraph "Account C (Consumer)"
        subgraph "VPC vpc-consumer-c (10.2.0.0/16)"
            Lambda1["Lambda<br/>10.2.0.0/24"]
            VPCE2["VPC Endpoint<br/>vpce-consumer-c-001"]
        end
        Lambda1 --> VPCE2
    end

    VPCE1 -->|PrivateLink<br/>Port 443| SVC
    VPCE2 -->|PrivateLink<br/>Port 443| SVC

    style NLB fill:#ff9999
    style SVC fill:#99ccff

セキュリティグループの3層設定パターン

本番で動作確認済みの設定を以下に示します。重要なのは、各層で異なるポート番号を指定すること。最初これを統一しようとして失敗してたんですよ:

# 1層目: NLBターゲットグループのセキュリティグループ
sg_nlb_target = {
    "GroupName": "sg-privatelink-nlb-target",
    "Ingress": [
        {
            "IpProtocol": "tcp",
            "FromPort": 8443,  # ← アプリケーションが実際にリスニングするポート
            "ToPort": 8443,
            "CidrIp": "10.1.0.0/16",  # Consumer VPC A
        },
        {
            "IpProtocol": "tcp",
            "FromPort": 8443,
            "ToPort": 8443,
            "CidrIp": "10.2.0.0/16",  # Consumer VPC C
        },
    ]
}

# 2層目: NLBのセキュリティグループ
sg_nlb = {
    "GroupName": "sg-privatelink-nlb",
    "Ingress": [
        {
            "IpProtocol": "tcp",
            "FromPort": 443,  # ← Consumer側が接続するポート
            "ToPort": 443,
            "CidrIp": "10.1.0.0/16",
        },
        {
            "IpProtocol": "tcp",
            "FromPort": 443,
            "ToPort": 443,
            "CidrIp": "10.2.0.0/16",
        },
    ]
}

# 3層目: VPC Endpoint ENI のセキュリティグループ
sg_vpce_eni = {
    "GroupName": "sg-privatelink-endpoint-eni",
    "Ingress": [
        {
            "IpProtocol": "tcp",
            "FromPort": 443,
            "ToPort": 443,
            "SourceSecurityGroupId": "sg-consumer-instances",  # Consumer内の全インスタンス
        },
    ]
}

複数アカウント管理を自動化

Consumer側の接続管理は、CloudFormation Stack Setsを使ってテンプレート化しました。新しいConsumerが増えても、このテンプレートを流すだけで済むようになったんです:

AWSTemplateFormatVersion: "2010-09-09"
Description: "VPC Endpoint Consumer Template for PrivateLink"

Parameters:
  ServiceName:
    Type: String
    Default: "com.amazonaws.vpce.ap-northeast-1.vpce-svc-0abc123def456"
  VpcId:
    Type: AWS::EC2::VPC::Id
  SubnetIds:
    Type: List<AWS::EC2::Subnet::Id>

Resources:
  VPCEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      ServiceName: !Ref ServiceName
      VpcId: !Ref VpcId
      SubnetIds: !Ref SubnetIds
      SecurityGroupIds:
        - !Ref ConsumerSecurityGroup
      PrivateDnsEnabled: true
      PrivateDnsNameOptions:
        PrivateDnsOnlyEnabled: true

  ConsumerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "Consumer VPC Endpoint Security Group"
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: "10.1.0.0/16"  # Provider VPC

Outputs:
  VPCEndpointId:
    Value: !Ref VPCEndpoint
    Export:
      Name: !Sub "${AWS::StackName}-VPCEndpointId"
  VPCEndpointDNS:
    Value: !GetAtt VPCEndpoint.DnsEntries[0].DnsName

トラブルシューティングで実装した監視メトリクス

本番で問題が起きたときに、原因を特定するのに時間がかかりすぎた。そこで、CloudWatchダッシュボードに以下のメトリクスを追加しました。特に「ProcessedBytes」のAZ別分布を見ることで、前述の「az-1aに偏る問題」を早期発見できるようになったんです:

# CloudWatch メトリクス設定
monitoring_metrics = [
    {
        "MetricName": "ActiveConnectionCount",
        "Dimensions": [
            {"Name": "ConsumerAccount"},
            {"Name": "AZ"},
        ],
        "Description": "各ConsumerアカウントのAZ別アクティブ接続数"
    },
    {
        "MetricName": "ProcessedBytes",
        "Dimensions": [
            {"Name": "AZ"},
        ],
        "Description": "AZ毎のトラフィック量(偏り検出に有効)"
    },
    {
        "MetricName": "NLB TargetHealthStatus",
        "Dimensions": [
            {"Name": "AZ"},
            {"Name": "TargetId"},
        ],
        "Description": "各AZのターゲット状態"
    },
]

3年運用で見えたPrivateLinkが本当に活躍する場面

正直に言うと、PrivateLinkはすべてのマルチアカウント構成に必要なわけじゃないです。うちで本当に役立ったのは以下のような場面なんですよね:

API Gateway経由でサービス公開する場合 VPCピアリングだと複雑になりすぎるが、PrivateLinkだと非常にシンプル。セキュリティも強固。

SaaS提供を想定する場合 外部企業のAWSアカウントから接続されるシナリオで、PrivateLinkのセキュリティモデルが活躍する。

マルチリージョン展開 各リージョンで独立したPrivateLinkエンドポイントを作れるので、リージョン障害時の隔離が容易。

ただし「VPCピアリングで十分」な場合も多いです。新しい技術だからって導入するのは失敗のもと。本当に必要かどうか、一度立ち止まって考えるべき。

実装で本当に効いた細かい工夫

DNS解決のPrivateDnsEnabled設定

VPCEndpoint = ec2.VpcEndpoint(
    vpc_id=vpc.id,
    service_name="com.amazonaws.vpce.ap-northeast-1.vpce-svc-xxx",
    private_dns_enabled=True,  # ← これ超重要
    private_dns_name_options={"private_dns_only_enabled": True},
)

この1行で、Consumer側のアプリケーションコードを一切変更せずにPrivateLinkへの切り替えができます。DNS名前解決が自動で処理されるから。地味ですけど、本番運用では本当に助かるんですよ。

NLBのクロスゾーン負荷分散

NLB = elbv2.NetworkLoadBalancer(
    load_balancer_name="nlb-privatelink",
    vpc=vpc,
    internet_facing=False,
    cross_zone_enabled=True,  # ← AZ間の偏りを防ぐ
)

これをONにすると、先ほどの「az-1aに偏る問題」がだいぶ軽減されました。多少のレイテンシ増加はありますが、VPCピアリングと比べても誤差範囲ですね。

2026年、PrivateLinkで今ハマってること

正直に言うと、まだ完璧じゃありません。特に以下が課題のまま残ってるんです:

Terraform/CDKでの管理が煩雑 Provider側とConsumer側でスタックを分ける必要があって、デプロイ順序に依存関係がある。自動化が難しい。

Cost Allocationタグの不完全性 どのConsumerからのトラフィックにいくらコストがかかってるか、細粒度で把握しづらい。請求配分が面倒。

接続拒否のデバッグ難易度 「何で接続できないのか」が複雑で、CloudTrailだけでは原因特定が困難。ネットワークトレースが必要になることも多い。

これらは業界全体の課題な気がするんで、他のエンジニアの話も聞きたいところです。

まとめ

PrivateLinkは3年運用で、VPCピアリングより複雑だけど確実に価値がある設計だと実感しました。最初に痛い目を見たからこそ、今では自信を持って本番環境を運用できてます。

次のアクションとしては以下を意識してください:

  1. セキュリティグループの3層設定を最初に検証する — 接続テスト段階で必ず全層確認しましょう。後からの修正は本当に手間。

  2. CloudWatchメトリクスの準備を先にやる — 本番で問題が起きてからメトリクス追加すると対応が遅れます。事前準備が大事。

  3. 複数アカウント管理はStackSets一択 — 手動オペレーションの余地をなくすのが運用を楽にする唯一の方法。

  4. PrivateLinkが本当に必要か一度立ち止まる — SaaS提供や複雑なアーキテクチャじゃなければVPCピアリングも検討する価値あり。

マルチアカウント設計で迷ってたら、このパターンが役立つと幸いです。何か質問があれば、コメント欄で教えてもらえたら嬉しい。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事