ECS Exec 세션 로그가 한 줄도 안 남을 때 — 이미지에 script 가 있는지부터 본다
Alpine 컨테이너에서 2년 넘게 비어 있던 감사 로그. 클러스터 logging 을 세 번 바꿔도 안 남던 이유와 한 줄 수정
ECS Exec 으로 본번 컨테이너에 붙어서 한 일이 어디에도 안 남고 있었다. 이 글을 읽으면 세션 로그가 비어 있을 때 어디부터 봐야 하는지(이미지에 script 가 있는가), 클러스터의 logging 설정이 실제로 정하는 게 무엇인지, Alpine 이미지에서 한 줄로 고치는 법을 알 수 있다.
ECS Exec 은 aws ecs execute-command 로 돌고 있는 컨테이너에 셸을 여는 기능이다. 컨테이너 안에 SSM agent 가 같이 떠서 세션을 중계한다.
누가 붙었는지는 남는데, 뭘 했는지는 안 남았다
CloudTrail 에는 ExecuteCommand 호출이 남는다. 누가 언제 어느 태스크에 붙었는지는 안다. 그런데 붙은 다음 rails console 에서 무엇을 조회하고 고쳤는지는 흔적이 없었다.
평소엔 불편할 일이 없다. 개인정보 유출이 의심될 때 문제가 된다. 「몇 건이 열람됐나」 에 「모른다」 밖에 답할 게 없다.
첫 번째 헛발 — 설정을 컨테이너 정의에 쓰고 있었다
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 어디서도 걸리지 않았다. 코드를 읽는 사람은 당연히 설정돼 있다고 믿는다.
그래서 클러스터에 제대로 넣었다. 그래도 로그는 안 남았다.
두 번째 헛발 — 제일 시끄러운 에러를 쫓았다
컨테이너 안의 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 를 한참 의심했다. 셋 다 멀쩡했다.
원인은 첫 줄에 있었다. transcript 를 못 만들었다는 그 줄이다. 버킷 에러는 그 뒤에 따라 나온 2차 증상이었다. 로그는 시간순으로, 맨 처음 난 에러부터 본다. 눈에 제일 띄는 에러가 범인이라는 보장은 없다.
진짜 원인 — 이미지에 /usr/bin/script 가 없었다
같은 클러스터에 컨테이너가 둘 있었다. 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 로 올리는 코드까지 가지 않고 돌아간다. 바이너리가 없으면 두 번 다 실패한다.
ECS 문서에도 「세션 로그를 S3 · CloudWatch Logs 로 올리려면 이미지에 script 와 cat 이 있어야 한다」 고 한 줄 적혀 있다. 그 한 줄을 못 보고 지나갔다.
지금 agent 소스는 transcript 를 못 만들면 거기서 바로 돌아간다. 우리 로그에 버킷 에러가 뒤따라 찍힌 건 컨테이너에 든 agent 버전이 달라서로 보이는데, 버전별로 쫓아 보진 않았다.
logging 설정은 목적지만 정한다
클러스터의 logging 은 값이 셋이다 (문서).
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 판도 바탕이 같다.
고치기 — 한 줄
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로 태스크를 갈아야 한다. 이걸 몰라서 한 바퀴 더 돌았다--configuration '{}'로는 안 지워졌다. 되돌리려고 terraform 에서 블록을 지웠는데 AWS 쪽은 그대로였다.'{"executeCommandConfiguration":{}}'처럼 키를 적어 넘기니 지워졌다storedBytes는 믿지 않는다. CloudWatch Logs 로그 스트림의storedBytes는 2019-06-17 부터 늘 0 이다(API 문서). 0 이라고 로그가 없는 게 아니다.get-log-events로 직접 읽는다스트림이 있다고 로그가 있는 건 아니다.
ecs-execute-command-*스트림이 30개 넘게 있었는데 열어 보니 전부 비어 있었다보존 기간. DEFAULT 는 앱 로그 그룹의 보존 기간을 따라간다. 7일이면 몇 주 뒤에 알게 된 사고에는 쓸모가 없다. 세션 출력에는 접속 정보나 토큰이 그대로 흐르니, 감사용은 전용 그룹으로 빼서 길게 두고 읽을 수 있는 사람도 좁힌다
가벼운 이미지를 고를 때 하나 더
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 에 올라간다