Sequel Ace(MySQL)や pgAdmin(PostgreSQL)など、ローカルPCのGUIクライアントからプライベートサブネットのRDS / Auroraに接続する方法をまとめる。

目次

  1. 方式
  2. 前提条件
  3. --target の形式
  4. コマンドの流れ
  5. 汎用スクリプト
  6. IAMポリシー(接続する人に付与)
  7. GUIクライアントの設定
  8. ローカルポートの決め方
  9. TLS(SSL)接続について
  10. 注意点・運用のベストプラクティス
  11. トラブルシューティング

方式

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側

  • 対象の 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 ではない)
Host127.0.0.1
Portローカルポート(例 3307)
Username / PasswordDBユーザー

pgAdmin(PostgreSQL)

項目値
Host name/address127.0.0.1(後述の hosts 設定時は RDSエンドポイント名)
Portローカルポート(例 5442)
Maintenance databasepostgres など
Username / PasswordDBユーザー
SSH Tunnel タブ使わない

ローカルポートの決め方

  • 手元で動いているDBの既定ポートと被らない番号にする
    • MySQL 3306 → 3307 や 13306
    • PostgreSQL 5432 → 5442 や 15432
    • MongoDB 27017 → 手元で mongod を動かすなら 27018 など
  • 複数のトンネルを同時に使うなら、スクリプトごとにローカルポートを重複させない
  • 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通り:
    1. sslmode=verify-ca(または require)で妥協する
    2. /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接続について」を参照

参考

0件のページ