Sequel Ace(MySQL)や pgAdmin(PostgreSQL)など、ローカルPCのGUIクライアントからプライベートサブネットのRDS / Auroraに接続する方法をまとめる。
目次
- 方式
- 前提条件
--targetの形式- コマンドの流れ
- 汎用スクリプト
- IAMポリシー(接続する人に付与)
- GUIクライアントの設定
- ローカルポートの決め方
- TLS(SSL)接続について
- 注意点・運用のベストプラクティス
- トラブルシューティング
方式
SSM Session Manager のポートフォワーディング(AWS-StartPortForwardingSessionToRemoteHost)で、稼働中のECSタスクのコンテナを中継にしてトンネルを張る。
手元からDBへは直接届かないので、DBと同じVPCにいてDBへの接続を許可されているアプリのコンテナを中継に使う。経路は ECS Exec と同じ。
- 中継用のサーバーを別に用意しなくてよい
- インバウンドポートを一切開けない(SSHの鍵もポート22も使わない)
- 手元から外へ出る通信は SSM への HTTPS(443)だけ
- 認証・認可は IAM に一本化(aws-vault / SSO の一時クレデンシャル、MFA)
- 誰がいつ接続したかが CloudTrail に残る
通信の経路
Sequel Ace / pgAdmin
│ 127.0.0.1:<ローカルポート>
▼
session-manager-plugin(手元の Mac)
│ HTTPS(443)で AWS の SSM サービスへ出る ← SSH(22)ではない
▼
SSM サービス
│
▼
ECS タスクのコンテナ内の SSM Agent(ECS Exec 用)
│ VPC の中から TCP で接続
▼
RDS / Aurora
前提条件
ローカルPC
- AWS CLI v2
- Session Manager plugin(実際のトンネルはこれが張る)
# macOS (Homebrew)
brew install --cask session-manager-plugin
session-manager-plugin --version- 一時クレデンシャルで認証する(アクセスキーの長期保存は避ける)
- aws-vault:
aws-vault exec <プロファイル> -- aws ...で一時クレデンシャルを環境変数に入れて、後ろのコマンドを実行する。MFAを求められることがある。セッションはキャッシュされるので、続くコマンドで再入力は通常不要 - IAM Identity Center(SSO)を使う場合は
aws sso login --profile <プロファイル>
- aws-vault:
AWS側
- 対象の ECS サービスで ECS Exec(
enableExecuteCommand: true)が有効- 有効化後にデプロイされたタスクでないと SSM Agent が入らない
- Fargate の場合はプラットフォームバージョン 1.4.0 以降
- タスクロールに ECS Exec 用の権限(
ssmmessages:CreateControlChannel/CreateDataChannel/OpenControlChannel/OpenDataChannel)がある - タスクから SSM(ssmmessages)への 443 の経路がある(NAT Gateway、または
com.amazonaws.<region>.ssmmessagesのVPCエンドポイント) - RDS のセキュリティグループが、タスクのセキュリティグループからのDBポートを許可している(アプリがDBに接続できているなら通常満たしている)
ECS Exec が有効かの確認:
aws ecs describe-services --cluster <クラスター> --services <サービス> \
--query 'services[0].enableExecuteCommand'--target の形式
ECSタスクのコンテナを指定するときの target は次の形式になる。
ecs:<クラスター名>_<タスクID>_<コンテナのランタイムID>
タスクIDとランタイムIDはタスクが入れ替わるたびに変わるので、スクリプトで毎回取得してから組み立てる。
コマンドの流れ
① ECSタスクのARNを取る
TASK_ARN=$(aws-vault exec example-stg -- aws ecs list-tasks \
--cluster app-a-stg --service-name app-a-stg \
--query 'taskArns[0]' --output text)- サービスで動いているタスクのうち先頭の1つを取る(例:
arn:aws:ecs:ap-northeast-1:123…:task/app-a-stg/abc123def…) - 空か
Noneなら「タスクが見つかりませんでした」と出して終了する - タスクが複数あっても、どれを使っても転送先のDBは同じ
② タスクIDを取り出す
TASK_ID=$(echo "$TASK_ARN" | awk -F/ '{print $NF}')ARN を / で区切った最後の要素を取る。AWSへのアクセスはなく、手元の文字列処理だけ。
③ コンテナのランタイムIDを取る
RUNTIME_ID=$(aws-vault exec example-stg -- aws ecs describe-tasks \
--cluster app-a-stg --tasks "$TASK_ARN" \
--query "tasks[0].containers[?name=='app-a-stg'].runtimeId" --output text)- タスクの詳細から、指定した名前のコンテナの
runtimeIdを取る(例:abc123def…-1234567890) - 1つのタスクに複数のコンテナがあり得るので、どのコンテナを中継にするかをここで決める
- JMESPath の比較演算子は
==。文字列リテラルは'app-a-stg'(シングルクォート)で書くのが確実
④ 接続情報を表示する
GUIクライアントに入れる値(Host 127.0.0.1 / Port など)を echo で表示する。
⑤ ポートフォワーディングを始める(本体)
aws-vault exec example-stg -- aws ssm start-session \
--target "ecs:app-a-stg_${TASK_ID}_${RUNTIME_ID}" \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["shared-mysql.cluster-…"],"portNumber":["3306"],"localPortNumber":["3307"]}'| オプション | 意味 |
|---|---|
--target | 中継に使うECSコンテナ(ecs:<クラスター>_<タスクID>_<ランタイムID>) |
--document-name | リモートホストへのポートフォワーディング用のSSMドキュメント |
host | 転送先(DBのエンドポイント名)。名前解決はコンテナ側で行われる |
portNumber | 転送先のポート(MySQL なら 3306) |
localPortNumber | 手元で待ち受けるポート |
- 実際のトンネルは手元の session-manager-plugin が張る
- フォアグラウンドで動き続け、Ctrl+C で止まる
汎用スクリプト
①〜⑤を1本にまとめ、環境ごとの違いは引数で渡す。環境ごとにスクリプトをコピーするとコピペ由来の不具合が起きやすいので、1本にまとめるのがおすすめ。
#!/usr/bin/env bash
# db-tunnel-ecs.sh
# 使い方: ./db-tunnel-ecs.sh <aws-vaultプロファイル> <クラスター> <サービス> <コンテナ> <DBホスト> <DBポート> <ローカルポート>
set -euo pipefail
PROFILE="${1:?profile}"
CLUSTER="${2:?cluster}"
SERVICE="${3:?service}"
CONTAINER="${4:?container}"
DB_HOST="${5:?db host}"
DB_PORT="${6:?db port}"
LOCAL_PORT="${7:?local port}"
run() { aws-vault exec "$PROFILE" -- "$@"; }
# ローカルポートの重複チェック
if lsof -nP -iTCP:"$LOCAL_PORT" -sTCP:LISTEN >/dev/null 2>&1; then
echo "ローカルポート $LOCAL_PORT は使用中です" >&2
exit 1
fi
TASK_ARN=$(run aws ecs list-tasks --cluster "$CLUSTER" --service-name "$SERVICE" \
--desired-status RUNNING --query 'taskArns[0]' --output text)
if [[ -z "$TASK_ARN" || "$TASK_ARN" == "None" ]]; then
echo "タスクが見つかりませんでした" >&2
exit 1
fi
TASK_ID="${TASK_ARN##*/}"
RUNTIME_ID=$(run aws ecs describe-tasks --cluster "$CLUSTER" --tasks "$TASK_ARN" \
--query "tasks[0].containers[?name=='${CONTAINER}'].runtimeId | [0]" --output text)
if [[ -z "$RUNTIME_ID" || "$RUNTIME_ID" == "None" ]]; then
echo "コンテナ ${CONTAINER} のランタイムIDが取れませんでした(コンテナ名を確認)" >&2
exit 1
fi
echo "接続先: Host 127.0.0.1 / Port ${LOCAL_PORT} (-> ${DB_HOST}:${DB_PORT})"
run aws ssm start-session \
--target "ecs:${CLUSTER}_${TASK_ID}_${RUNTIME_ID}" \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "{\"host\":[\"${DB_HOST}\"],\"portNumber\":[\"${DB_PORT}\"],\"localPortNumber\":[\"${LOCAL_PORT}\"]}"呼び出し例:
./db-tunnel-ecs.sh example-stg app-a-stg app-a-stg app-a-stg shared-mysql.cluster-… 3306 3307よく使う環境はシェルのエイリアスにしておくと楽。
IAMポリシー(接続する人に付与)
ECS Exec の権限はそのままだと ecs execute-command でコンテナへのシェル操作も許してしまうので、ポートフォワーディング用ドキュメントに絞る。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "StartPortForwardingToEcsTask",
"Effect": "Allow",
"Action": "ssm:StartSession",
"Resource": [
"arn:aws:ecs:ap-northeast-1:123456789012:task/app-a-stg/*",
"arn:aws:ssm:ap-northeast-1::document/AWS-StartPortForwardingSessionToRemoteHost"
],
"Condition": {
"BoolIfExists": { "ssm:SessionDocumentAccessCheck": "true" }
}
},
{
"Sid": "FindTask",
"Effect": "Allow",
"Action": ["ecs:ListTasks", "ecs:DescribeTasks"],
"Resource": "*"
},
{
"Sid": "ManageOwnSessions",
"Effect": "Allow",
"Action": ["ssm:TerminateSession", "ssm:ResumeSession"],
"Resource": "arn:aws:ssm:*:*:session/${aws:userid}-*"
}
]
}ssm:SessionDocumentAccessCheckにより、許可したドキュメント以外でのセッション開始を拒否できるResourceのタスクARNはクラスター単位で絞る。本番クラスターへの権限は限られたロールのみに付与する
GUIクライアントの設定
トンネルはSSM側で張っているので、クライアント内蔵のSSHトンネル機能は使わない。
Sequel Ace(MySQL)
| 項目 | 値 |
|---|---|
| 接続タイプ | Standard(SSH ではない) |
| Host | 127.0.0.1 |
| Port | ローカルポート(例 3307) |
| Username / Password | DBユーザー |
pgAdmin(PostgreSQL)
| 項目 | 値 |
|---|---|
| Host name/address | 127.0.0.1(後述の hosts 設定時は RDSエンドポイント名) |
| Port | ローカルポート(例 5442) |
| Maintenance database | postgres など |
| Username / Password | DBユーザー |
| SSH Tunnel タブ | 使わない |
ローカルポートの決め方
- 手元で動いているDBの既定ポートと被らない番号にする
- MySQL 3306 →
3307や13306 - PostgreSQL 5432 →
5442や15432 - MongoDB 27017 → 手元で
mongodを動かすなら27018など
- MySQL 3306 →
- 複数のトンネルを同時に使うなら、スクリプトごとにローカルポートを重複させない
hostには DBのエンドポイントDNS名 を指定する。名前解決はコンテナ側で行われるため、フェイルオーバーでIPが変わっても追従する。Aurora なら書き込みはクラスターエンドポイント、参照は reader エンドポイント
TLS(SSL)接続について
トンネル越しでも DB接続自体のTLSは有効にしておく(SSMの区間は暗号化されるが、コンテナ→RDS間はTLSで守る)。
- RDSの CA バンドル(
global-bundle.pem)をダウンロードしてクライアントに設定 127.0.0.1に接続すると証明書のホスト名と一致しないため、sslmode=verify-fullだと失敗する。対処は2通り:sslmode=verify-ca(またはrequire)で妥協する/etc/hostsに127.0.0.1 mydb.cluster-xxxxxxxx.ap-northeast-1.rds.amazonaws.comを追記し、クライアントには RDS エンドポイント名で接続する →verify-fullが通る(使い終わったら hosts を戻すこと)
- PostgreSQL 側で
rds.force_ssl=1、MySQL 側でrequire_secure_transport=ONにしておくと平文接続を拒否できる
注意点・運用のベストプラクティス
- デプロイやスケールインでタスクが入れ替わるとトンネルが切れる。再実行すれば新しいタスクで張り直せる
- コンテナへのシェル操作を禁止:IAMでポートフォワーディング用ドキュメントのみ許可する
- セッションログ:Session Manager の設定で CloudWatch Logs / S3 へのログ出力、アイドルタイムアウト(例: 20分)、最大セッション時間を設定する
- 環境ごとに権限を分離:aws-vault のプロファイル(IAMロール)を環境ごとに分け、本番への接続は限られたロールのみ
- DBユーザーを個人単位で発行、あるいは IAMデータベース認証 を使い、共有の管理者ユーザーを日常的に使わない。読み取り用途なら read-only ユーザー + reader エンドポイント
- パスワードは Secrets Manager で管理し、ローカルにベタ書きしない
- ECS Exec の有効化・タスクロール・IAMポリシーは IaC(CDK / Terraform)でコード化する
トラブルシューティング
| 症状 | 確認ポイント |
|---|---|
SessionManagerPlugin is not found | ローカルに Session Manager plugin が入っていない |
TargetNotConnected | サービスの enableExecuteCommand、有効化後にデプロイされたタスクか、タスクロールの ssmmessages:* 権限、ssmmessages への経路 |
AccessDeniedException | 実行ユーザーの IAM(ssm:StartSession、タスクARN、ドキュメントARN) |
ランタイムIDが空 / None | コンテナ名の誤り、JMESPath の == 誤記、タスクが起動途中 |
| トンネルは開くがクライアントが接続できない / タイムアウト | RDS SG がタスクSGからのDBポートを許可しているか、DBエンドポイント名の誤り |
Address already in use | ローカルポートが使用中。lsof -i :<ポート> で確認し別ポートにする |
| しばらくすると切れる | Session Manager のアイドルタイムアウト、デプロイでのタスク入れ替わり |
| SSL エラー(ホスト名不一致) | 上記「TLS接続について」を参照 |