PrivateLinkとNLBを使ったRDSクロスアカウントアクセスを試してみる

2023.05.09

RDSのクロスアカウントアクセスを "VPC間の接続無しに" 実現したいです。

img

実現したい背景としては、以下を想定しています。

  • AWSのマルチアカウント管理を始めている
  • 「新規ワークロードアカウント(APPアカウント)」から「別アカウント(DBアカウント)のデータベース」へアクセスしたい
  • ただしセキュリティやネットワーク的制約でVPC Peering, Transit Gateway等の接続が難しい

実現のために、本ブログでは PrivateLink と Network Load Balancer(NLB) を使ったアクセスを試してみます。

構成図は以下のとおり。

img

作ってみる

今回は主にCloudFormationを使って作成します。

以下のようなステップで作成します。

  1. [前提] RDS等の準備
  2. PrivateLink エンドポイントサービス等の作成 at DBアカウント
  3. PrivateLink エンドポイントの作成 at APPアカウント

img

以降、各ステップの構築詳細を記載します。

1. [前提] RDS等の準備

前提として以下環境(VPC, Subnet, RDS など) は作成済みとします。

img

本ブログでは RDS for MySQL のDBインスタンスをターゲットとします。

img

2. PrivateLink エンドポイントサービス等の作成 at DBアカウント

ターゲットRDSがあるアカウント(DBアカウント)上で、 以下リソースを作成します。

  • RDSをターゲットとした Network Load Balancer(NLB)
  • NLBをターゲットとした PrivateLink エンドポイントサービス

img

ここでNLBターゲットには「プライマリインスタンスのIPアドレス」を指定しないといけない点に注意してください (後述の "運用の考慮点" でも触れます)。 なお、プライマリインスタンスのIPアドレスは nslookup や dig を実行して確認できます。

$ dig test-db.chnkexample.ap-northeast-1.rds.amazonaws.com +short
172.31.81.180

CloudFormationで展開

以下テンプレートをDBアカウントへ展開しました。

provider.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: "Create a PrivateLink service for RDS."
Parameters:
  # リソース名のプレフィクス
  Prefix: { Type: String, Default: "exampleDB" }
  # 接続先データベースのポート番号
  DBPort: { Type: Number, Default: 3306 } # MySQL
  # 接続先データベースのIPアドレス
  DBIPAddress: { Type: String }
  # 接続先データベースのVPC
  DBVPC: { Type: "AWS::EC2::VPC::Id" }
  # NLBの配置サブネット(リスト)
  NLBSubnets: { Type: "List<AWS::EC2::Subnet::Id>"}
  # 接続元(APPアカウント)のAWSアカウントID
  AppAccountID: { Type: String }

Resources:
  ### NLBターゲットグループ
  NLBTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      # General
      Name: !Sub "${Prefix}-nlb-tg"
      Protocol: "TCP"
      Port: !Ref DBPort
      IpAddressType: "ipv4"
      VpcId: !Ref DBVPC
      # Healthcheck
      HealthCheckProtocol: TCP
      HealthCheckIntervalSeconds: 30 # 5 - 300 まで指定可能
      # Target
      TargetType: "ip"
      Targets:
        - Id: !Ref DBIPAddress
          Port: !Ref DBPort
      # Tags
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-nlb-tg"
  ### NLB
  NLB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Type: "network"
      Name: !Sub "${Prefix}-nlb"
      Scheme: "internal"
      IpAddressType: "ipv4"
      Subnets: !Ref NLBSubnets
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-nlb"
  ### NLBリスナー
  NLBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref NLB
      Protocol: "TCP"
      Port: !Ref DBPort
      DefaultActions:
        - Type: "forward"
          TargetGroupArn: !Ref NLBTargetGroup
  ### PrivateLink エンドポイントサービス
  EndpointService:
    Type: AWS::EC2::VPCEndpointService
    Properties:
      AcceptanceRequired: true
      NetworkLoadBalancerArns:
        - !Ref NLB
  EndpointServicePermission:
    Type: AWS::EC2::VPCEndpointServicePermissions
    Properties:
      ServiceId: !Ref EndpointService
      AllowedPrincipals:
        - !Sub "arn:aws:iam::${AppAccountID}:root"

Outputs:
  EndpointServiceName:
    Value: !Sub "com.amazonaws.vpce.${AWS::Region}.${EndpointService}"

このテンプレートで以下リソースが展開されます。

  • NLB : NLB
    • NLBターゲットグループ: NLBTargetGroup
    • NLBリスナー: NLBListener
  • PrivateLinkエンドポイントサービス : EndpointService
    • サービスのプリンシパル許可: EndpointServicePermission

作成後のアウトプット EndpointServiceName (エンドポイントサービスのサービス名)をメモしておきます。 次ステップで使います。

img

3. PrivateLink エンドポイントの作成 at APPアカウント

接続元のアカウント(APPアカウント)上で、 以下リソースを作成します。

  • PrivateLink エンドポイント

img

展開に使用したCloudFormationテンプレート

以下テンプレートをAPPアカウントへ展開しました。

consumer.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: "Create a PrivateLink endpoint for access RDS."
Parameters:
  # リソース名のプレフィクス
  Prefix: { Type: String, Default: "exampleDB" }
  # エンドポイント(接続先データベース)のポート番号
  DBPort: { Type: Number, Default: 3306 } # MySQL
  # エンドポイントサービスの名前
  EndpointServiceName: { Type: String }
  # エンドポイント作成先のVPC
  EndpointVPC: { Type: "AWS::EC2::VPC::Id" }
  # エンドポイント作成先のサブネット(リスト)
  EndpointSubnets: { Type: "List<AWS::EC2::Subnet::Id>" }
  # 「エンドポイントへアクセスするリソース(EC2等)」のセキュリティグループ
  AppSG: { Type: "AWS::EC2::SecurityGroup::Id" }

Resources:
  EndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${Prefix}-endpoint"
      GroupDescription: "VPC endpoint to access RDS"
      VpcId: !Ref EndpointVPC
      SecurityGroupIngress:
        - IpProtocol: "tcp"
          FromPort: !Ref DBPort
          ToPort: !Ref DBPort
          SourceSecurityGroupId: !Ref AppSG
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-endpoint"
  Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: "Interface"
      PrivateDnsEnabled: false
      ServiceName: !Ref EndpointServiceName
      VpcId: !Ref EndpointVPC
      SubnetIds: !Ref EndpointSubnets
      SecurityGroupIds:
        - !Ref EndpointSecurityGroup

このテンプレートで以下リソースが展開されます。

  • PrivateLinkエンドポイント : Endpoint
  • 上記エンドポイント用のセキュリティグループ : EndpointSecurityGroup

展開後にDBアカウントでやること

展開後にDBアカウントの [エンドポイントサービス > エンドポイント接続] にて 「接続リクエスト」が来ているはずです。

それを承諾しましょう。

img

承諾後、APPアカウントのエンドポイントのステータスが 使用可能 となっていればOKです。

接続テスト

APPアカウント上にEC2インスタンスを建てて、接続します。

接続にはエンドポイントのドメイン名が必要です。 事前にメモしておきます。

img

以下のように、DBアカウントのMySQLへ接続できることを確認できました。

db_user="example"
db_password="********"
db_endpoint="vpce-example-svxh5maq.vpce-svc-example.ap-northeast-1.vpce.amazonaws.com"
mysql --host=${db_endpoint} --user=${db_user} --password=${db_password}
# mysql: [Warning] Using a password on the command line interface can be insecure.
# Welcome to the MySQL monitor.  Commands end with ; or \g.
# Your MySQL connection id is 119
# Server version: 8.0.32 Source distribution
#  
# Copyright (c) 2000, 2023, Oracle and/or its affiliates.
#  
# Oracle is a registered trademark of Oracle Corporation and/or its
# affiliates. Other names may be trademarks of their respective
# owners.
#  
# Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
#  
# mysql>

運用の考慮点

実際には運用周りの考慮点が多くて大変です。 主にNLBを間に挟んでいることに起因します。

洗い出せた範囲の考慮点を以下に記載していきます。

NLBのTCPアイドルタイムアウトは350秒固定

TCP フローのアイドルタイムアウト値を 350 秒 に設定します。 この値は変更できません。 クライアントまたはターゲットは TCP キープアライブパケットを使用して、アイドルタイムアウトをリセットできます。

Network Load Balancer #接続のアイドルタイムアウト - Elastic Load Balancing

上記、注意が必要です。 非常に重たいクエリを実行した場合に、 NLB起因で接続が閉じられる可能性があります。

以下チェックポイントです。

☑ DB側のTCPキープアライブ設定

例えば PostgreSQL では tcp_keepalives_idletcp_keepalives_intervaltcp_keepalives_count のパラメータでTCPキープアライブの挙動を制御できます。

☑ DB側のタイムアウト時間

TCPキープアライブ設定と合わせて、DB側のタイムアウト時間も確認しておきましょう。 MySQL の wait_timeout 、 PostgreSQL の statement_timeout などです。

NLBヘルスチェック起因でDB接続が拒否される可能性がある

今回 RDS for MySQL で構築した際に、接続時に以下のようなエラーに出くわしました。

# 見やすさのために改行しています
ERROR 1129 (HY000):
  Host '172.31.88.155' is blocked because of many connection errors;
  unblock with 'mysqladmin flush-hosts'

これはNLBのヘルスチェックが「接続エラー」としてカウントされるためです。 ※ FLUSH HOSTS; を実行してリセットできます。

MySQLの場合、一定回数( max_connect_errors )を超えると、このようにブロックされます。 デフォルト設定だと、すぐに "ブロック状態" になっちゃいます。

以下チェックポイントです。

☑ NLBヘルスチェック間隔

NLBのヘルスチェック間隔( HealthCheckIntervalSeconds )を確認、調整しましょう。 5秒〜300秒の間で調整可能です。

☑ DB側の「接続エラー回数によるブロック」のしきい値

例えば MySQL の場合、 max_connect_errors のパラメータで 接続ブロックのしきい値を変えることが出来ます。

img

IPアドレス変更(フェイルオーバー時など)への対応について

フェイルオーバー時などでプライマリインスタンスが変わった場合は、 NLBターゲットグループの設定を更新する必要があります。 ターゲットを「プライマリインスタンスの IPアドレス 」で指定しているためです。

IPアドレス更新を自動化する仕組みも作成可能です。 以下AWSブログに紹介している通り、フェイルオーバーイベントをトリガーに Lambdaを使ってターゲットを更新しています。

img

– 画像: Access Amazon RDS across VPCs using AWS PrivateLink and Network Load Balancer | AWS Database Blog

【補足】他のアーキテクチャ案について

クロスアカウントのRDS接続を「VPC間の接続無しに」実現する方法は、 調べた限りでは以下 2つありそうでした。

  • RDS Proxy 案
  • PrivateLink + NLB 案 (本ブログで紹介)

本ブログでは触れないRDS Proxy 案について、ここで簡単に補足します。

RDS Proxy 案は、プロキシエンドポイントを別アカウントに作成して接続する方法です。 PrivateLink + NLB と比べて よりマネージドであり、 また特定の用途(サーバレス環境)などではパフォーマンス効率の恩恵があったりします。

以下AWSブログにも紹介されていますので、 詳細はこちらを参照ください。

ただし、いくつか制限があります。 そもそも「ターゲットが RDS Proxy でクロスアカウント接続が可能か」、 以下公式ドキュメントで調べる必要があります。

例えば 2023/05/08時点では、 DBインスタンスのリードレプリカは RDS Proxyターゲットにできません

おわりに

PrivateLink と NLB を使ってRDSクロスアカウントアクセスを試してみました。

所感としては、運用の考慮点が多いのが辛そうだと感じました。 他にも運用の考慮点が出てくる可能性はあるので、 その都度対応できる気合が必要そうです。

RDS Proxy のほうが、よりマネージドな感覚です。 「コスト(vCPU単位)」や「そもそもターゲットにできるか」あたりの考慮点はありますが、 運用負荷を下げることを重視するのであれば、 まずは RDS Proxy 案を検討するのが良さそうです。

以上、参考になれば幸いです。

参考