はじめに
プライベートサブネットにあるRDS/AuroraへローカルのDBクライアントから接続するとき、「パブリックサブネットに踏み台EC2を置き、22番ポートを開けてSSHトンネルで運用」していたのですが、現在はSession Managerを使用する運用が主流のようなので、今回はその接続手順をまとめてみました。
AWS Systems Manager(SSM)のSession Managerを使うと、インバウンドポートを一切開けず、SSH鍵も管理せずにEC2へ接続でき、そのままRDSへのポートフォワードもできます。
目次
- 従来の踏み台サーバー(SSH)のデメリット
- Session Managerの仕組みと前提条件
- 実際に運用してみた
- コスト・権限・ログまわりについて
1. 従来の踏み台サーバー(SSH)のデメリット
一般的な構成は以下のようになっているかと思います。
[ローカルPC] --SSH(22)--> [踏み台EC2(パブリックサブネット)] --3306/5432--> [RDS(プライベートサブネット)]
22番ポートがインターネットに露出する
踏み台のセキュリティグループで22番を開ける以上、常にスキャンやブルートフォースの標的になりえます。接続元IPを絞っても、以下のような問題が残ります。
- リモートワークやモバイル回線で接続元IPが固定できない
- 結局
0.0.0.0/0で開けてしまいがち。または許可接続元IPリストの追加・削除が日常業務になる - OpenSSH自体の脆弱性(例: regreSSHion/CVE-2024-6387)が出るたびに、パッチ当てを急ぐ必要がある
SSH鍵の管理が属人化する
- メンバーの加入・離脱のたびに
authorized_keysの追加・削除が必要 - 1本の鍵を複数人で使い回すと、誰が接続したのか分からなくなる
- 退職者の鍵を消し忘れると、それがそのまま侵入経路になる
- 鍵ファイル(
.pem)がSlackやメールで共有されがち
「誰が・いつ・何をしたか」を追いにくい
SSHのログはOS内(/var/log/secureなど)に残るだけです。
- IAMのユーザー/ロールと紐づかないため、AWS側の監査ログ(CloudTrail)と突き合わせられない
- インスタンスを作り直すとログも消える
- 操作内容まで残すには、自前で仕組みを用意する必要がある
認可がIAMと二重管理になる
AWSのアクセス権はIAM(SSO)で管理しているのに、踏み台だけはOSユーザーとSSH鍵という別の仕組みになります。権限の棚卸しが2か所に分かれ、IAMを剥がしてもSSH鍵が生きている、という状態が起こり得ます。
2. Session Managerの仕組みと前提条件
Session Managerとは
Session ManagerはAWS Systems Managerの機能の一つで、マネージドノード(EC2やオンプレサーバー)に対して、ブラウザやAWS CLIからシェル接続できるサービスです。シェル接続のほか、ポートフォワードもできます。
仕組み
[ローカルPC / マネジメントコンソール]
│ HTTPS (443)
▼
[AWS Systems Managerエンドポイント]
▲
│ HTTPS (443) ※EC2側からのアウトバウンド通信のみ
[EC2 (SSM Agent)]
ポイントは、EC2上のSSM AgentがAWS側へアウトバウンドで接続しにいくことです。
そのため、EC2側のセキュリティグループにインバウンドルールは不要になります。
従来のSSH踏み台との比較
| 項目 | SSH + 踏み台 | SSM Session Manager |
|---|---|---|
| インバウンドポート | 22番を開放 | 不要 |
| 接続元IPの管理 | 必要 | 不要 |
| 鍵管理 | SSH鍵の配布・ローテーションが必要 | 不要(IAM権限で制御) |
| アクセス制御 | セキュリティグループ / authorized_keys | IAMポリシー(SSOと統合) |
| 監査 | OS内のログのみ | CloudTrail + セッションログ(CloudWatch Logs / S3) |
| 中継用EC2 | 必要 | RDSへ中継する場合は必要(後述) |
私は最初「SSMなら踏み台が要らない」、と勘違いしていましたが、正確にはSSH用に公開する踏み台が要らない であって、RDS/Aurora自体にはSSM Agentが入らないため、RDSへポートフォワードするには中継役のEC2(マネージドノード)が1台必要になります。
ただしこのEC2はインバウンドを一切開けず、鍵も持たないので、従来の踏み台とは性質がまったく違います。
前提条件
SSMで接続するには、次の3つが揃っている必要があります。
a. SSM AgentがEC2にインストールされ、起動している
b. EC2にIAMロール(インスタンスプロファイル)がアタッチされている
c. EC2からSSMのエンドポイントへHTTPS(443)で到達できる
a. SSM Agent
Amazon Linux 2 / Amazon Linux 2023 / Ubuntu Server(公式 AMI)/ Windows Server(公式 AMI)などにはプリインストール済みです。
# Amazon Linux 2 / 2023
sudo systemctl status amazon-ssm-agent
# Ubuntu (snap でインストールされている場合)
sudo snap services amazon-ssm-agent
b. IAM ロール
EC2に、AWS管理ポリシーAmazonSSMManagedInstanceCoreを付与したIAMロールをアタッチします。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
アカウント単位でDefault Host Management Configuration(DHMC)を有効にすると、インスタンスごとに IAMロールを付けなくてもSSMの管理対象にできます(IMDSv2が必要)
c. ネットワーク
EC2から次のエンドポイントへ443番で通信できる必要があります。
ssm.<region>.amazonaws.comssmmessages.<region>.amazonaws.comec2messages.<region>.amazonaws.com
経路は大きく 3 通りあります。
| 経路 | 構成 | コスト感(東京リージョン目安) |
|---|---|---|
| パブリックサブネット + IGW | EC2にパブリックIPを付け、アウトバウンドのみ使う | パブリックIPv4 $0.005/時(起動中だけ。常時なら約 3.7/月) |
| プライベートサブネット + NAT Gateway | NAT経由で外へ出る | $0.062/時 + $0.062/GB(約 $45/月〜) |
| プライベートサブネット + VPC エンドポイント | 上記3つのInterface型エンドポイントを作る | $0.014/時×3個×AZ数(2AZで約$61/月) |
VPCエンドポイントを使う場合は、エンドポイントに付けるセキュリティグループでEC2からの443番のインバウンドを許可します。また、VPCの「DNS ホスト名」「DNS 解決」を有効にしておく必要があります。
「パブリックサブネットに置く」と聞くと身構えるかもしれませんが、インバウンドが空のセキュリティグループであれば、外から到達できる経路はありません。中継用EC2しか置かない用途なら、これが最も安価になります。
3. 実際に運用してみた
ここからは、実際に運用している構成を紹介します。
構成例: SSM専用踏み台でAurora PostgreSQLに接続する
全体像
[ローカルPC]
│ localhost:15432(ポートフォワード)
│ aws ssm start-session (HTTPS 443 / IAM認可)
▼
[SSMエンドポイント]
▲ HTTPS 443(アウトバウンドのみ)
│
[踏み台 EC2] ← パブリックサブネット / SGインバウンド: なし / キーペア: なし
│ 5432(踏み台SG → クラスタSGのみ許可)
▼
[Aurora PostgreSQL Serverless v2] ← プライベートサブネット(インターネット経路なし)
※SG:セキュリティグループ
設計のポイントは次のとおりです。
- Auroraはプライベートサブネットに置き、インターネットへの経路を一切持たせない(NAT Gatewayもなし)
- 踏み台は
パブリックサブネット + IGWに置く。SSMエンドポイントへ出るためだけに使い、インバウンドは空にする - 踏み台はSSHを使わない、キーペアなし、セキュリティグループのインバウンドなし
- DB への到達は
踏み台SG → クラスタSGの5432番のみ許可する - 認可はSSOロールのIAM(
ssm:StartSession)で一元管理する - 踏み台はt4g.nanoを普段は停止しておき、使うときだけ起動する(コスト最安)
アプリケーション(Lambda)からのDBアクセスとマイグレーションはRDS Data API(HTTPS + IAM)経由で行っており、踏み台は使用していません。踏み台は「人がGUIクライアントでDBを覗く」ためだけの経路です。用途を絞ることで、普段は停止しておく運用にできています。
合わせて読みたい記事:(AWS)RDS Data APIを使ってみた
CDK での定義
AWS CDKのBastionHostLinuxを使うと、SSM接続に必要なインスタンスロール(AmazonSSMManagedInstanceCore)を自動で付けてくれます。
import {
AmazonLinuxCpuType, BastionHostLinux, BlockDeviceVolume, InstanceClass,
InstanceSize, InstanceType, IpAddresses, MachineImage, SubnetType, Vpc,
} from 'aws-cdk-lib/aws-ec2'
const vpc = new Vpc(this, 'DbVpc', {
ipAddresses: IpAddresses.cidr('10.0.0.0/16'),
maxAzs: 2,
natGateways: 0, // NAT Gatewayは作らない
subnetConfiguration: [
// Aurora用。インターネット経路なし
{ name: 'db-isolated', subnetType: SubnetType.PRIVATE_ISOLATED, cidrMask: 24 },
// 踏み台用。SSMエンドポイントへのアウトバウンドのためだけに使う
{ name: 'bastion-public', subnetType: SubnetType.PUBLIC, cidrMask: 24 },
],
})
// const cluster = new DatabaseCluster(this, 'DbCluster', { vpc, vpcSubnets: { subnetType: SubnetType.PRIVATE_ISOLATED }, ... })
const bastion = new BastionHostLinux(this, 'Bastion', {
vpc,
subnetSelection: { subnetType: SubnetType.PUBLIC },
instanceName: 'myapp-dev-bastion',
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.NANO),
machineImage: MachineImage.latestAmazonLinux2023({
cpuType: AmazonLinuxCpuType.ARM_64,
// AMIをcdk.context.jsonに固定する(理由は後述)
cachedInContext: true,
}),
requireImdsv2: true,
blockDevices: [
{ deviceName: '/dev/xvda', volume: BlockDeviceVolume.ebs(8, { encrypted: true }) },
],
})
// 踏み台SG → クラスタSGの5432 番だけを許可する
cluster.connections.allowDefaultPortFrom(bastion, 'SSM port forwarding from bastion')
BastionHostLinuxはキーペアを設定せず、セキュリティグループのインバウンドも空のまま作成されます。
AMIはcachedInContext: trueで固定します。latestAmazonLinux2023()は既定ではSSMパラメータで最新AMIを解決します。そのため、新しいAMIが出るたびにcdk deployでインスタンスが置き換えになり、停止中の踏み台が勝手に起動した状態で作り直されます。
AMIを更新したいときは、cdk.context.jsonから該当キーを消して再synthします。
接続手順
前提:
- AWS CLI v2とSession Managerプラグインがインストール済み
# Session Manager プラグイン(macOS)
brew install --cask session-manager-plugin
session-manager-plugin --version
- 対象アカウントのSSOプロファイルでログイン済み(
aws sso login --profile <profile>)
-
踏み台のインスタンスIDとDBエンドポイントを確認する(CDKのOutputなどから確認できるようにしておくと楽)
-
踏み台が停止していれば起動する(SSMに登録されるまで1〜2分かかるかも)
aws ec2 start-instances --instance-ids i-0123456789abcdef0 # PingStatus が Online になれば接続可能 aws ssm describe-instance-information \ --query "InstanceInformationList[].[InstanceId,PingStatus]" --output table -
ポートフォワードを開始する(このターミナルは開いたままにする)
aws ssm start-session \ --target i-0123456789abcdef0 \ --document-name AWS-StartPortForwardingSessionToRemoteHost \ --parameters '{"host":["<クラスタのライターエンドポイント>"],"portNumber":["5432"],"localPortNumber":["15432"]}' -
DB クライアントから
localhost:15432に接続する(他の数字を設定した場合、後の手順を置き換える)psql "host=localhost port=15432 dbname=<DB名> user=<ユーザー名> sslmode=require"※パスワードはSecrets Managerに保存したDB認証情報から取得します。DBeaverなどのGUIクライアントを使う場合は、下記「DBeaverで接続する場合」を参照してください。
-
使い終わったらCtrl+Cでポートフォワードを止め、踏み台を停止する(コスト削減)
aws ec2 stop-instances --instance-ids i-0123456789abcdef0
DBeaverで接続する場合
GUIクライアントのDBeaverからも、手順3のポートフォワードを開いたままlocalhost:15432に接続するだけで使えます。
DBeaver側のSSHトンネル設定は使いません(トンネルはSession Managerが担当するため)。
-
DB認証情報をSecrets Managerから取得する
aws secretsmanager get-secret-value \ --secret-id <シークレット名またはARN> \ --query SecretString --output text \ --profile <profile> # => {"username":"...","password":"...", ...} -
DBeaverを起動し、メニューの「データベース(Database)」→「新しいデータベース接続(New Database Connection)」を開く
-
一覧から「PostgreSQL」を選んで「次へ(Next)」
-
「メイン(Main)」タブに接続情報を入力する
項目 設定値 接続方法(Connect by) Host ホスト(Host) localhostポート(Port) 15432(手順3のlocalPortNumberおそらく5432)データベース(Database) <DB名>認証(Authentication) Database Native ユーザー名 / パスワード 1で取得した username/password
- 「接続のテスト(Test Connection)」を押す(初回はPostgreSQLドライバーのダウンロードを求められるので、ダウンロードする)
- 「Connected」と表示されたら「完了(Finish)」で保存する
- ポートフォワードのターミナルを閉じる、または
Ctrl+Cで止めると、DBeaverの接続も切れます - Session Managerのセッションは一定時間操作がないと切断されます(アイドルタイムアウト、既定20分)。切れたら手順3を再実行し、DBeaverで再接続します
4. コスト・権限・ログまわりについて
コスト比較
Session Manager自体はEC2に対して追加料金なしで使えます。つまり「SSHかSSMか」だけで料金はほとんど変わりません。
接続するユーザー側のIAM権限管理
「誰がどのインスタンスに、どの方法で接続できるか」はIAMポリシーで制御します。次の例は、「Env=devタグが付いた踏み台に、RDS向けのポートフォワードだけを許可する」場合の例です。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ssm:StartSession",
"Resource": "arn:aws:ec2:ap-northeast-1:123456789012:instance/*",
"Condition": {
"StringEquals": { "ssm:resourceTag/Env": "dev" }
}
},
{
"Effect": "Allow",
"Action": "ssm:StartSession",
"Resource": "arn:aws:ssm:ap-northeast-1::document/AWS-StartPortForwardingSessionToRemoteHost"
},
{
"Effect": "Allow",
"Action": ["ssm:TerminateSession", "ssm:ResumeSession"],
"Resource": "arn:aws:ssm:*:*:session/${aws:userid}-*"
}
]
}
- SSMドキュメントを
AWS-StartPortForwardingSessionToRemoteHostに絞れば、シェルは開けず、ポートフォワードだけできる**権限になります -
TerminateSession/ResumeSessionのResourceを${aws:userid}-*に絞ることで、自分が開始したセッションだけを操作できるようにできます - SSOの権限セットに組み込めば、メンバーの離脱はSSOから外すだけで接続できなくなります。SSH鍵の消し忘れのような問題は起きません
セッションログの記録
- セッションの開始・終了はCloudTrailにIAMプリンシパル付きで記録されます。「誰が・いつ・どの踏み台に」接続したかは追加設定なしで追うことが可能です
- Session Managerの「設定」でシェル操作のログをCloudWatch Logs / S3に保存することもできます
ポートフォワードのセッションでは、DBに流れるSQLの中身までは記録されません。
クエリ単位の監査が必要な場合は、DB側の監査ログ(PostgreSQLのpgauditなど)を併用します。
おわりに
今回は、Session Managerを使って、ローカルのDBクライアントからプライベートサブネットのRDS/Auroraへ接続する構成を紹介しました。
導入して一番便利だと感じたのは、SSH鍵や接続元IPの管理が不要になった点です。
今回の構成では中継用EC2は引き続き必要ですが、用途をローカルからのDB接続に絞ることで、必要なときだけ起動する運用にできました(コスト削減)
最後まで読んでいただきありがとうございました!

