ECS Execのセッションログが1行も残らないとき — まずイメージにscriptがあるかを見る
Alpineコンテナで2年以上空だった監査ログ。クラスターのloggingを3回変えても残らなかった理由と、1行の修正
ECS Execで本番コンテナに入って作業した記録が、どこにも残っていなかった。この記事を読むと、セッションログが空のときにまずどこを見るか(イメージにscriptがあるか)、クラスターのlogging設定が実際に決めているもの、Alpineイメージを1行で直す方法がわかる。
ECS Execはaws ecs execute-commandで動いているコンテナにシェルを開く機能だ。コンテナの中でSSM agentが一緒に動き、セッションを中継している。
誰が入ったかは残るのに、何をしたかは残っていなかった
CloudTrailにはExecuteCommandの呼び出しが残る。誰がいつどのタスクに入ったかはわかる。ところが入ったあとrails consoleで何を調べて何を直したかは、跡がなかった。
普段は困らない。困るのは個人情報の漏えいが疑われたときだ。「何件閲覧されたか」に「わからない」としか答えられない。
1つ目の空振り — 設定をコンテナ定義に書いていた
terraformにexecuteCommandConfigurationはあった。ただしタスク定義のcontainer_definitionsの中に入っていた。
container_definitions = jsonencode([{
name = "app"
executeCommandConfiguration = { # ここにこの項目はない
logging = "DEFAULT"
}
}])
この設定はクラスターに付くものだ。ECS APIでもExecuteCommandConfigurationはクラスター設定の一部になっている。
resource "aws_ecs_cluster" "app" {
configuration {
execute_command_configuration {
logging = "OVERRIDE"
log_configuration { ... }
}
}
}
コンテナ定義に書いたほうは、aws ecs describe-task-definitionで見るとキーごと消えていた。エラーも出さずに捨てられていたわけだ。container_definitionsはJSON文字列なので、terraform validate・plan・applyのどこでも引っかからない。コードを読んだ人は、当然設定されていると思う。
それでクラスター側にちゃんと入れた。それでもログは残らなかった。
2つ目の空振り — いちばんうるさいエラーを追った
コンテナ内のagentログ(/var/log/amazon/ssm/amazon-ssm-agent.log)にはこんな行があった(短くして載せる)。
ERROR [DataBackend] unable to generate log data: Failed to generate transcript with the following errors: ...
WARN [DataBackend] Signed HeadBucket request failed, continuing to fallback logic
ERROR [DataBackend] ... InvalidBucketName: The specified bucket is not valid.
バケット名のところに{{s3BucketName}}が置換されないまま出ていたので、S3の設定、IAM、KMSをしばらく疑った。3つとも問題なかった。
原因は1行目にあった。transcriptを作れなかったという、あの行だ。バケットのエラーはそのあとに続いて出た二次的な症状だった。ログは時系列で、最初に出たエラーから読む。いちばん目立つエラーが犯人とは限らない。
本当の原因 — イメージに/usr/bin/scriptがなかった
同じクラスターにコンテナが2つあった。Alpineベースのアプリコンテナはセッションログ0件。Debianベースの別サービスのコンテナは、設定を何も触っていないのにセッションの中身が全部残っていた。設定が同じで結果が分かれるなら、残るのはイメージの違いだ。
SSM agentのソース(agent/session/shell/shell_unix.go)を読むと理由が見える。セッションが終わるとagentはこういうコマンドを実行する。
script <ログファイル> -c "cat <セッションの入出力をためたファイル>"
script(1)は端末の入出力をファイルに書き出すコマンドだ。これが失敗するとagentは-cなしでもう一度試し、それもだめならFailed to generate transcriptを出して、S3・CloudWatchへ送るコードまで行かずに戻る。バイナリがなければ2回とも失敗する。
ECSのドキュメントにも「コマンドログをS3・CloudWatch Logsに送るには、イメージにscriptとcatが入っている必要がある」と1文書いてある。その1文を読み飛ばしていた。
今のagentのソースは、transcriptを作れないとその場で戻る。うちのログでバケットのエラーが後に続いていたのは、コンテナに入っていたagentのバージョンが違うからだと思うが、バージョンごとには追っていない。
logging設定が決めるのは送り先だけ
クラスターのloggingは値が3つある(ドキュメント)。
NONE— 送らないDEFAULT— タスク定義のawslogsロググループ、つまりアプリのログと同じ場所に送るOVERRIDE—log_configurationに書いた専用のCloudWatchロググループ・S3に送る
scriptはその手前、送るファイルができるかどうかを決める。手前が詰まっていれば、後ろをどう変えても0件だ。実際に変えて確かめた。
| クラスター設定 | Alpineのアプリ(scriptなし) | Debianの別サービス(scriptあり) |
|---|---|---|
| 設定を入れる前 | 0件 | 全部残る |
| OVERRIDE + KMS + 専用グループ・バケット | 0件 | — |
| DEFAULT + KMS | 0件 | — |
| DEFAULT | 0件 | — |
| DEFAULT、イメージにscriptを追加 | 残る(コマンドの出力まで) | — |
最後の行がポイントだ。scriptさえ入れれば、クラスター設定に触らなくてもデフォルトのままアプリのロググループにセッションの記録が残り始める。
Alpineにだけない — 実際に動かした
公式イメージをDockerで起動して確かめた(2026-11-09)。
$ docker run --rm alpine:3.20 ls -l /usr/bin/script
ls: /usr/bin/script: No such file or directory
$ docker run --rm debian:bookworm-slim sh -c 'ls -l /usr/bin/script; dpkg -S /usr/bin/script'
-rwxr-xr-x 1 root root 133288 Nov 21 2024 /usr/bin/script
bsdutils: /usr/bin/script
Alpineは3.16・3.20どちらもなく、busyboxにもscriptアプレットがない。Debianはbsdutilsが必須(essential)パッケージなので、slimイメージにも最初から入っている。だからこの落とし穴はAlpineベースのイメージでだけ、警告ひとつなく起きる。ruby:*-alpineのような言語公式イメージのAlpine版も土台は同じだ。
直し方 — 1行
RUN apk add --no-cache util-linux-misc
Alpine 3.16ならutil-linux-misc-2.38-r1(インストールサイズ4,148 KiB)、3.20なら2.40.1-r1(6,200 KiB)が入る。バージョンによってパッケージの分け方が変わることがあるので、入れたあとapk info -W /usr/bin/scriptでどのパッケージのものか確かめておく。
入れてからagentと同じコマンドを動かすとこうなる。
$ script -q -c "cat /tmp/ipc.log" /tmp/out.txt < /dev/null
$ cat /tmp/out.txt
Script started on 2026-11-09 10:54:36+00:00 [COMMAND="cat /tmp/ipc.log" <not executed on terminal>]
id
uid=0(root)
Script done on 2026-11-09 10:54:36+00:00 [COMMAND_EXIT_CODE="0"]
CloudWatchに上がってくるセッションログも、このScript started on ...・Script done on ...の形そのままだ。最初はこのヘッダーがなぜ付くのか不思議だったが、script(1)の出力形式だった。
同じ症状ならこの順番で
- クラスターに設定が入っているか —
aws ecs describe-clusters --clusters <cluster> --include CONFIGURATIONS。タスク定義に書いたものは無視される - コンテナに
scriptがあるか —aws ecs execute-command ... --command "ls -l /usr/bin/script"。たいていここで片がつく - agentログの最初のERRORを見る —
Failed to generate transcriptが先に出ていれば、後ろのS3のエラーは追わなくていい
一緒に踏んだもの
クラスター設定は新しく起動したタスクから効いた。 動いていたタスクはそのままだった。確かめるには
aws ecs update-service --force-new-deploymentでタスクを入れ替える。これを知らずにもう1周した--configuration '{}'では消えなかった。 元に戻そうとterraformでブロックを消したら、AWS側は残ったままだった。'{"executeCommandConfiguration":{}}'のようにキーを書いて渡したら消えたstoredBytesは信じない。 CloudWatch LogsのログストリームのstoredBytesは2019-06-17から常に0だ(APIドキュメント)。0でもログがないとは限らない。get-log-eventsで直接読むストリームがあってもログがあるとは限らない。
ecs-execute-command-*のストリームが30個以上あったが、開いてみたら全部空だった保存期間。 DEFAULTはアプリのロググループの保存期間に従う。7日なら、数週間後に気づいた事故には役に立たない。セッションの出力には接続情報やトークンがそのまま流れるので、監査用は専用グループに分けて長く持ち、読める人も絞る
軽いイメージを選ぶときに、もう1つ
Alpineを選んだのはイメージを小さくするためだった。その選択が監査ログを2年以上、静かに止めていた。イメージサイズの比較表にこういうものは出てこない。
JSON文字列だから検査できないterraform、知らないキーを捨てるAPI、バイナリがなくても黙っているagent。どれも単体で見れば納得できる設計なのに、重なると誰も気づかなかった。監査のように「普段は見ない機能」は、一度わざと開けて見ておきたい。
ECS・Fargateの監査ログや、terraformと実際の状態がずれた設定の整理で困っているなら、https://www.negabaro.com/contact から聞いてほしい。
動作フロー
セッション中、agentが入出力をコンテナ内のファイル(ipcTempFile.log)にためる
セッション終了時にscript -c "cat ..."でtranscriptを作る — /usr/bin/scriptがなければここで止まる
クラスターのlogging(NONE・DEFAULT・OVERRIDE)が送り先を決める
CloudWatch Logsのecs-execute-command-<セッションID>ストリームかS3に上がる