メインコンテンツへスキップ

ECS Exec の stdin pipe もいいが SSM Port Forwarding はもっといい

· loading · loading ·
kiitosu
著者
kiitosu
aws community builder. 画像処理やデバイスドライバ、データ基盤構築からWebバックエンドまで、多様な領域に携わってきました。地図解析や地図アプリケーションの仕組みにも経験があり、幅広い技術を活かした開発に取り組んでいます。休日は草野球とランニングを楽しんでいます。
目次

はじめに
#

private subnet にある RDS PostgreSQL に、ローカルから dump.sql を流したい場面がありました。

踏み台候補を比較した結論として ECS Task を採用しました。

  • EC2: 停止忘れの課金と AMI パッチ管理が地味に痛い
  • CloudShell VPC environment: クォータ最大 2/region と社内ネットワーク調整が釣り合わない
  • ECS Task: タスク終了で勝手に止まる。コンテナイメージなので OS 管理も不要

そこで postgres:17-alpinesleep infinity で立てて、ECS Exec で psql を起動して stdin に dump.sql を流せばよいと素直に考えました。これが落とし穴です。

最終的に踏み台は ECS Task のまま、stdin pipe は捨てて SSM Port Forwarding に置き換えました。本記事はその顛末と、なぜそれが筋がよいかの話です。

結論:

  • ❌ ECS Exec の stdin pipe は信頼できません(session 中断かハング)
  • ✅ SSM Port Forwarding で TCP tunnel を張って、ローカルの psql に -f dump.sql で渡すだけで完走します

ECS Exec に dump を流して撃沈する
#

最初に試したコマンドはこれです。

aws ecs execute-command \
  --cluster <cluster-name> --task <task-arn> --container <container> \
  --interactive \
  --command "sh -c 'PGPASSWORD=... psql -h <rds-endpoint> -U postgres <db-name>'" \
  < dump.sql

なお PGPASSWORD 直書きは検証用です。実運用では Secrets Manager / SSM Parameter Store 経由が望ましいです。

期待した動きは「dump.sql が psql に流れて CREATE TABLE / COPY が走る」。 現実はこうでした。

Starting session with SessionId: ecs-execute-command-...
psql (17.9)
Type "help" for help.

<db-name>=# Cannot perform start session: EOF

psql のバナーが出た瞬間に EOF で session 終了です。CREATE TABLE 一個も走りません。

メッセージ文言は「Cannot perform start session」と書いていますが、Starting session with SessionId と psql バナーが出ている時点で session 開始自体は成功していることが分かります。落ちているのはその後の入力処理段階です。

なぜ EOF で落ちるのか
#

これは sfujiwara 記事 でも語られている session-manager-plugin の挙動です。

mainline ソースを辿ると、ローカル stdin が EOF に達した瞬間に handleKeyboardInput() がエラーを上げて、ValidateInputAndStartSession()Cannot perform start session: %v を出力して session を終了します(shellsession_unix.gosession.go の連携)。

ローカルで < dump.sql... | aws ecs ... を使えば、fd は file または pipe で、読み切ったら EOF を返します。plugin にとってローカル stdin の EOF は「session 続行不能」のシグナル、というデザインです。

ところが remote コマンド次第で見え方が変わります。

remote 起動コマンド 結果
cat(stdin を読み続けるだけ) ハングします(Ctrl-C で抜けるしかない)
sh -c 'psql ...'(対話 shell として居座る) Cannot perform start session: EOF

両方ともローカル stdin は EOF してるはずなのに結果が違うのは、plugin に session を終わらせる経路が複数あって、どちらが先勝ちかで見え方が変わるためです(remote プロセスが先に終了すれば os.Exit(0) 経路で抜けてエラー表示すら出ません)。psql のように対話で居座るプロセスでは「ローカル EOF → エラー表示」が先勝ち、cat のように EOF を待つだけのプロセスでは少なくとも私の観察では remote 側の stdin EOF として認識されず、cat が読み続けたまま session も閉じない状態になりました。

要は ECS Exec の stdin pipe は信頼して使えない。エラーが目に見える形で出るのは psql 系の対話プログラムを起動したときだけ、という具合です。

unbuffer や expect で頑張ればやれます。ただし overengineering
#

sfujiwara 記事は unbuffer で PTY を被せて EOF 問題を回避するアプローチを示しています。PTY なら明示的に閉じるまで EOF にならないので、plugin の error 経路に乗らない、という理屈です。

unbuffer 単独はうまくいかない
#

unbuffer -p aws ecs execute-command ... --command "sh -c 'psql ...'" < dump.sql

私の環境ではこれは即死でした。unbuffer -p が file の中身を一気に PTY に流したあと自分も exit するので、session 確立より前にデータが消えるレース、というのが原因と推測しています。

expect で psql の prompt と同期させれば動きます
#

expect で psql の prompt を待ってから次の行を送るスクリプトなら、ちゃんとデータが届きます。

expect -f - <<'EOF'
spawn aws ecs execute-command --cluster <cluster> --task <task-arn> --container <container> \
  --interactive \
  --command {sh -c 'PGPASSWORD=... psql -h 127.0.0.1 -U postgres mydb'}

expect "mydb=#"
send -- "DROP TABLE IF EXISTS t;\r"
expect "mydb=#"
send -- "CREATE TABLE t (id int, name text);\r"
expect "mydb=#"
send -- "COPY t (id, name) FROM stdin;\r"
expect ">> "
send -- "1\tfoo\r"
send -- "2\tbar\r"
send -- "3\tbaz\r"
send -- "\\.\r"
expect "mydb=#"
send -- "SELECT * FROM t;\r"
expect "mydb=#"
send -- "\\q\r"
expect eof
EOF

実機で COPY も含めて綺麗に通り、3 行 insert に成功します。

mydb=# SELECT * FROM t;
 id | name
----+------
  1 | foo
  2 | bar
  3 | baz
(3 rows)

ただし、これを毎回 dump 投入のたびに書くのは普通に重いです。

  • 通常 / 継続行 / transaction の 3 種類のプロンプトを正規表現で識別する
  • pg_dump が出す複数行 SQL や COPY ブロックを状態遷移込みで扱う
  • エラー検知 / バッファ溢れ / 特殊文字エスケープなどの運用面の作り込み

ECS Exec を踏み台にしたいだけなのに、対話 shell との同期 protocol を自前実装することになります。データ転送のためにこの仕組みを保守したくない、というのが本記事の判断です。

SSM Port Forwarding に切り替える
#

「ECS Exec で remote shell に喋る」のをやめて、「ローカルの psql から RDS に直接届く TCP tunnel」を作ります。

aws ssm start-session \
  --target ecs:<cluster-name>_<task-id>_<runtime-id> \
  --document-name AWS-StartPortForwardingSessionToRemoteHost \
  --parameters '{"host":["<rds-endpoint>"],"portNumber":["5432"],"localPortNumber":["15432"]}'

<task-id> は task ARN の末尾、<runtime-id>aws ecs describe-tasks ... --query "tasks[0].containers[?name=='<container>'].runtimeId | [0]" で取れます(複数コンテナ task でも安全な書き方)。

このコマンドが foreground で生きている間、127.0.0.1:15432 に来た TCP 接続が ECS Task 経由で <rds-endpoint>:5432 に forward されます。

別ターミナルで psql を実行します。

psql -h 127.0.0.1 -p 15432 -U postgres -d <db-name> -f /path/to/dump.sql

これで完走します。psql はローカルで動いて pgwire で RDS と直接喋る。間にいる SSM はバイトを中継しているだけです。

なぜ問題が消えるのか
#

経路の構造が違います。

経路 抽象化 中身
ECS Exec stdin pipe remote 対話 shell に喋る PTY 経由 / 同期必要 / 対話モード前提
SSM Port Forwarding TCP の中継 port → port のバイト中継、それだけ

ECS Exec stdin pipe で苦しんだ要素は、Port Forwarding では全部消えます。

要素 ECS Exec stdin pipe Port Forwarding
psql プロンプトの同期 必要(expect で書く) 不要(psql が pgwire で処理)
session 確立タイミング EOF と競合 無関係
対話モードの罠 あり なし

最後に
#

ECS Exec は対話のための機能で、データ転送には向きません

stdin pipe で渡すと session ごと EOF で吹き飛ぶか、ハングするか、どちらかです。expect で頑張れば動かせますが、それは「ECS Exec を経由する」という前提が間違っていて、その間違いを補正するために大きなコードを書いている状態です。

SSM Port Forwarding は「ECS Task を TCP tunnel の起点として使う」というシンプルな抽象で、ローカルの素の psql で -f dump.sql だけで完結します。SSM 有効な ECS Task は aws ssm start-session --target ecs:... でそのまま target として扱えるので、踏み台を別に立てる必要もありません。

「ECS Exec で対話する」と「ECS Task の private subnet 越しに TCP データを流す」は別問題で、後者は Port Forwarding が筋。これが今回の学びです。ECS Exec 自体は対話 shell やデバッグには便利な機能なので、用途に応じて使い分けるのが正解です。

参考
#

Reply by Email

関連記事

いまさら AWS AppConfig に触ってみた
· loading · loading
CloudWatch Pipelinesを使ってみた
Authenticatorアプリの仕組み — MFAの中のTOTPを自作する