はじめに
本番やSTGの調査をするとき、ECS Execでコンテナに入ってbin/rails runnerを叩く場面は多いと思います。私も最近、ECS上で動いているRailsアプリのデータ調査を、AIエージェントにローカルから自動で進めてもらう形で行っていました。
その途中で、aws ecs execute-command経由で実行したbin/rails runnerの出力が、途中でCannot perform start session: EOFというエラーとともに切れてしまう現象にハマりました。軽いechoは普通に返ってくるのに、調査用のスクリプトだけ結果が得られないという状況です。
この記事では、そのときに試して安定した回避策をまとめます。同じような状況の方の参考になれば嬉しいです。
何が起きたか
次のように、ECS Execでbin/rails runnerを直接フォアグラウンドで実行しました。
aws ecs execute-command --interactive \
--cluster <cluster> --task "$TASK_ARN" --container rails \
--command "bin/rails runner '...'"
すると、セッションが途中でCannot perform start session: EOFを出して切断され、期待した標準出力が得られないことがありました。単純なechoなどの軽いコマンドは問題なく返ってきます。そのため、コマンドの実行時間や出力量に依存している模様です。
ちなみに、このエラー自体は「ターミナルではない場所からECS Execを実行したとき」にも出るという報告が見つかりました(expectのunbufferでラップするとよい、という話です)。私の場合は事情が少し違い、--interactiveで実行している点や、実行時間・出力量に左右される点から、別の要因も絡んでいそうでした。原因の完全な特定はできていないので、あくまで「こうすると安定した」という実践的な回避策としてお読みください。
回避策:バックグラウンド実行、ファイル出力、結果の読み出し
安定したのは、1回のexecute-commandの中で「バックグラウンドで処理を開始し、少し待ってから結果ファイルを読む」ところまで完結させる方法でした。
1. 診断スクリプトをコンテナ内に書き出す
まず、heredocでスクリプトをコンテナ内に書き出します。引用符のエスケープが複雑になりがちなので、この方が楽でした。
TASK_ARN=$(aws ecs list-tasks --cluster <cluster> --service-name <service> --profile <profile> | jq -r '.taskArns[0]')
aws ecs execute-command --profile <profile> --region ap-northeast-1 \
--cluster <cluster> --task "$TASK_ARN" --container rails --interactive \
--command "/bin/sh -c 'cat > /tmp/check.rb << \"EOF\"
# ここにRubyスクリプト本体
File.write(\"/tmp/result.log\", some_result.to_json)
EOF
echo WROTE_SCRIPT'"
スクリプトの結果は標準出力ではなく、File.writeでファイルに書き出すのがポイントです。
2. バックグラウンドで実行して、結果ファイルを読む
次に、setsid nohupでバックグラウンド実行し、数秒待ってから結果ファイルをcatします。
aws ecs execute-command --profile <profile> --region ap-northeast-1 \
--cluster <cluster> --task "$TASK_ARN" --container rails --interactive \
--command "/bin/sh -c 'setsid nohup bin/rails runner /tmp/check.rb > /tmp/stdout.log 2>&1 & sleep 15; cat /tmp/result.log; echo ---stdout---; cat /tmp/stdout.log'"
この方法で、期待した結果を取得できました。
ハマりどころと注意点
disownは付けない
最初はバックグラウンド化の定番としてdisownを付けていましたが、コンテナのシェル(busybox系の/bin/shなど)にはdisownが無いことがありました。setsidだけで十分に切り離せたので、disownは付けなくて大丈夫です。
sleepの秒数はRailsのboot時間も考慮する
sleepの秒数は処理量に応じて調整します。DB接続やSentryの初期化など、Railsのboot自体に数秒かかることも見込んでおく必要があります。
終了時のEOFは無害
セッション終了時に出るCannot perform start session: EOFは、この手順では無害でした。コマンド自体の出力は正常に得られているので、これが出ることを失敗の判定条件にしないようにしています。自動化する場合は特に注意したいところです。
後片付けを忘れない
調査用の一時ファイルは、最後に必ず消しておきます。
rm -f /tmp/check.rb /tmp/result.log /tmp/stdout.log
WORKDIRは決め打ちしない
コンテナのWORKDIRが/appとは限りません。私が扱ったアプリでは/llstockでした。DockerfileのWORKDIRを事前に確認しておくのが安全です。
代替策:base64でスクリプトを転送する
heredocの代わりに、ローカルで書いたRubyスクリプトをbase64エンコードして転送する方法もあります。複雑な引用符のネストを避けられます。
B64=$(base64 < local_check.rb | tr -d '\n')
aws ecs execute-command --profile <profile> --region ap-northeast-1 \
--cluster <cluster> --task "$TASK_ARN" --container rails --interactive \
--command "/bin/sh -c 'echo $B64 | base64 -d > /tmp/check.rb && cd /llstock && bin/rails runner /tmp/check.rb'"
別の調査では、この方式でフォアグラウンド実行しても、約622KB相当の出力でEOFによる切断は起きず、そのまま結果を取得できました。EOFの発生条件は実行時間や出力量に依存する「模様」としか言えないため、次の順で試すのが良さそうです。
- まずbase64転送とフォアグラウンド実行で試す
- EOFに遭遇したら、バックグラウンド実行とファイル出力の方式に切り替える
まとめ
ECS Exec経由のbin/rails runnerで出力が途切れたときは、「バックグラウンドで実行し、結果をファイルに書き出して、待ってから読む」という流れにすると安定しました。複雑な引用符はheredocかbase64で回避できます。
原因の特定までは至っていませんが、調査を自動化していると、こうした「出力が取れない」問題は地味に効いてきます。まずは軽い方式から試し、EOFが出たら切り替えるくらいの気持ちで使うのが良さそうです。同じような状況の方の参考になれば嬉しいです。