RDSのクロスアカウントアクセスを "VPC間の接続無しに" 実現したいです。
実現したい背景としては、以下を想定しています。
- AWSのマルチアカウント管理を始めている
- 「新規ワークロードアカウント(APPアカウント)」から「別アカウント(DBアカウント)のデータベース」へアクセスしたい
- ただしセキュリティやネットワーク的制約でVPC Peering, Transit Gateway等の接続が難しい
実現のために、本ブログでは PrivateLink と Network Load Balancer(NLB) を使ったアクセスを試してみます。
構成図は以下のとおり。
作ってみる
今回は主にCloudFormationを使って作成します。
以下のようなステップで作成します。
- [前提] RDS等の準備
- PrivateLink エンドポイントサービス等の作成 at DBアカウント
- PrivateLink エンドポイントの作成 at APPアカウント
以降、各ステップの構築詳細を記載します。
1. [前提] RDS等の準備
前提として以下環境(VPC, Subnet, RDS など) は作成済みとします。
本ブログでは RDS for MySQL のDBインスタンスをターゲットとします。
2. PrivateLink エンドポイントサービス等の作成 at DBアカウント
ターゲットRDSがあるアカウント(DBアカウント)上で、 以下リソースを作成します。
- RDSをターゲットとした Network Load Balancer(NLB)
- NLBをターゲットとした PrivateLink エンドポイントサービス
ここで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
- NLBターゲットグループ:
- PrivateLinkエンドポイントサービス :
EndpointService
- サービスのプリンシパル許可:
EndpointServicePermission
- サービスのプリンシパル許可:
作成後のアウトプット EndpointServiceName
(エンドポイントサービスのサービス名)をメモしておきます。 次ステップで使います。
3. PrivateLink エンドポイントの作成 at APPアカウント
接続元のアカウント(APPアカウント)上で、 以下リソースを作成します。
- PrivateLink エンドポイント
展開に使用した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アカウントの [エンドポイントサービス > エンドポイント接続] にて 「接続リクエスト」が来ているはずです。
それを承諾しましょう。
承諾後、APPアカウントのエンドポイントのステータスが 使用可能
となっていればOKです。
接続テスト
APPアカウント上にEC2インスタンスを建てて、接続します。
接続にはエンドポイントのドメイン名が必要です。 事前にメモしておきます。
以下のように、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_idle
や tcp_keepalives_interval
、 tcp_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
のパラメータで 接続ブロックのしきい値を変えることが出来ます。
IPアドレス変更(フェイルオーバー時など)への対応について
フェイルオーバー時などでプライマリインスタンスが変わった場合は、 NLBターゲットグループの設定を更新する必要があります。 ターゲットを「プライマリインスタンスの IPアドレス 」で指定しているためです。
IPアドレス更新を自動化する仕組みも作成可能です。 以下AWSブログに紹介している通り、フェイルオーバーイベントをトリガーに Lambdaを使ってターゲットを更新しています。
– 画像: 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 案を検討するのが良さそうです。
以上、参考になれば幸いです。
参考
- Access Amazon RDS across VPCs using AWS PrivateLink and Network Load Balancer | AWS Database Blog
- Use Amazon RDS Proxy to provide access to RDS databases across AWS accounts | AWS Database Blog
- AWS CloudFormationでVPCEndpoint + NLB + ALB + EC2の構成を作ってみる | DevelopersIO
- ターゲットグループのヘルスチェック - Elastic Load Balancing
- MySQL 8.0 リファレンスマニュアル: 5.1.12.3 DNS ルックアップとホストキャッシュ
- 19.3. 接続と認証 | 日本PostgreSQLユーザ会
- Amazon RDS for MySQLでIPブロックが発生してしまった場合の対処 | DevelopersIO
- Amazon RDS for MySQL のパラメーターを設定するためのベストプラクティス。パート 3: セキュリティ、操作管理性、および接続タイムアウトに関連するパラメーター | Amazon Web Services ブログ