About

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のバージョンが違うからだと思うが、バージョンごとには追っていない。

1. Is a log file made? 2. Where does it go? Session I/O what you typed ipcTempFile.log raw I/O on disk script -c cat needs /usr/bin/script → transcript logging NONE DEFAULT OVERRIDE CloudWatch S3 Alpine image: no /usr/bin/script "Failed to generate transcript" nothing to send, whatever step 2 says

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)の出力形式だった。

同じ症状ならこの順番で

  1. クラスターに設定が入っているか — aws ecs describe-clusters --clusters <cluster> --include CONFIGURATIONS。タスク定義に書いたものは無視される
  2. コンテナにscriptがあるか — aws ecs execute-command ... --command "ls -l /usr/bin/script"。たいていここで片がつく
  3. 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 から聞いてほしい。

動作フロー

1

セッション中、agentが入出力をコンテナ内のファイル(ipcTempFile.log)にためる

2

セッション終了時にscript -c "cat ..."でtranscriptを作る — /usr/bin/scriptがなければここで止まる

3

クラスターのlogging(NONE・DEFAULT・OVERRIDE)が送り先を決める

4

CloudWatch Logsのecs-execute-command-<セッションID>ストリームかS3に上がる

ユースケース

ECS Execのセッションログが空、またはストリームだけあって中身がないとき AlpineベースのイメージのECS・Fargateサービスの監査ログ点検 個人情報を扱う本番コンテナに人が入る運用
← RDSをMulti-AZにする前に知っておくこと — 料金は2倍、スタンバイは読めない、順番が大事 AWSでメンテナンスページを出す3つの方法 — ALBルール・WAF・DNS切替を比べて、DNSをやめた理由 →