kubectl debug와 ephemeral container로 죽은 파드 들여다보기 - 쉘 없는 distroless 파드 디버깅과 --copy-to
쉘이 없는 distroless 이미지의 파드는 kubectl exec로 들어갈 수 없고, 죽어 있는 파드는 더더욱 들여다보기 어렵습니다. 이 글에서는 kubectl debug가 기존 파드에 임시 컨테이너(ephemeral container)를 붙이는 방법과 --target의 효과, CrashLoopBackOff 파드에서는 임시 컨테이너가 붙지 않아 --copy-to로 복제해야 하는 이유, 디버그 프로파일과 노드 디버깅, 필요한 RBAC 권한을 로컬 k3s 클러스터(Kubernetes v1.35.9, kubectl v1.34.1)에서 직접 실행해 정리합니다.
먼저 전체 그림입니다. 쉘이 없는 app 컨테이너에는 kubectl exec가 실패하지만, kubectl debug는 같은 파드에 도구가 든 임시 컨테이너를 붙이고, 이 컨테이너가 네트워크와 프로세스, 파일을 함께 들여다봅니다.

먼저 알아둘 용어
| 용어 | 뜻 |
|---|---|
| 파드 (Pod) | 쿠버네티스에서 컨테이너를 실행하는 가장 작은 단위입니다. 한 파드 안의 컨테이너들은 같은 네트워크(localhost)를 씁니다. |
| distroless 이미지 | 앱 실행에 필요한 것만 담고 쉘과 ls 같은 기본 도구도 뺀 최소 이미지입니다. 공격 면적이 작은 대신 kubectl exec로 들어가도 쓸 명령이 없습니다. 만드는 방법은 Docker 멀티스테이지 빌드 글에서 다뤘습니다. |
| 임시 컨테이너 (ephemeral container) | 이미 떠 있는 파드에 나중에 끼워 넣는 디버깅용 컨테이너입니다. 자동으로 재시작되지 않으며 앱을 만드는 용도가 아닙니다. Kubernetes v1.25부터 stable입니다. |
--target |
임시 컨테이너가 지정한 컨테이너와 프로세스 공간을 공유하도록 하는 옵션입니다. 이것을 줘야 ps에 대상 앱의 프로세스가 보입니다. |
| CrashLoopBackOff | 컨테이너가 시작하자마자 죽는 일이 반복되어, 쿠버네티스가 재시작 간격을 점점 늘리며 기다리는 상태입니다. |
| 디버그 프로파일 (profile) | kubectl debug가 만드는 컨테이너에 줄 권한 설정(securityContext)의 묶음입니다. general, baseline, restricted, netadmin, sysadmin이 있습니다. |
왜 필요한가
운영 이미지를 distroless로 줄이면 보안상 이득이지만, 장애가 났을 때 파드 안에서 확인할 수단이 사라집니다. 쉘이 없으니 kubectl exec가 막히고, 도구를 넣은 디버그용 이미지로 파드를 다시 만들면 문제가 났던 상태 자체가 사라집니다. 그래서 파드는 그대로 두고 도구만 잠깐 붙이는 방법이 필요합니다. 쿠버네티스 공식 문서도 임시 컨테이너의 용도로 컨테이너가 죽었거나 이미지에 디버깅 도구가 없어 kubectl exec로는 부족한 경우를 듭니다.
실습 환경 만들기
이 Mac에는 클러스터가 없어서 Docker 안에서 k3s(경량 쿠버네티스)를 임시로 띄웠습니다. k3s의 kubeconfig를 꺼내 서버 주소만 호스트에서 접근할 포트(16443)로 바꿔 씁니다. kubectl은 1.34이므로 버전 차이가 한 단계인 k3s v1.35.9를 골랐습니다.
$ docker run -d --name k3s-demo --privileged -p 16443:6443 rancher/k3s:v1.35.9-k3s1 server --disable=traefik --disable=metrics-server --disable=servicelb --tls-san=127.0.0.1
$ docker exec k3s-demo cat /etc/rancher/k3s/k3s.yaml | sed 's#https://127.0.0.1:6443#https://127.0.0.1:16443#' > kubeconfig
$ export KUBECONFIG=$PWD/kubeconfig
$ kubectl version
Client Version: v1.34.1
Kustomize Version: v5.7.1
Server Version: v1.35.9+k3s1
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
50473a59c9d0 Ready control-plane 2s v1.35.9+k3s1 172.17.0.2 <none> K3s v1.35.9+k3s1 6.12.54-linuxkit containerd://2.2.7-k3s1
여기서 봐야 할 부분: 서버가 v1.35.9이고 컨테이너 런타임은 containerd입니다. 아래 출력에서 노드 이름과 긴 ID는 <node>, <id>로 줄여 적었습니다.
실험에 쓸 파드 두 개입니다. web은 정상적으로 도는 파이썬 웹 서버이고, crash는 ConfigMap을 /etc/app에 마운트한 뒤 /etc/app/settings.json을 읽다가 죽습니다. 둘 다 쉘이 없는 distroless 파이썬 이미지입니다.
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: app
image: gcr.io/distroless/python3-debian12:latest
args: ["-m", "http.server", "8000", "--directory", "/tmp"]
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
config.json: '{"mode": "prod"}'
---
apiVersion: v1
kind: Pod
metadata:
name: crash
spec:
containers:
- name: app
image: gcr.io/distroless/python3-debian12:latest
args: ["-c", "import json; print(json.load(open('/etc/app/settings.json')))"]
volumeMounts:
- name: cfg
mountPath: /etc/app
volumes:
- name: cfg
configMap:
name: app-config
$ kubectl apply -f web.yaml -f crash.yaml
pod/web created
configmap/app-config created
pod/crash created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
crash 0/1 Error 2 (19s ago) 39s
web 1/1 Running 0 39s
1. kubectl exec가 막히는 모습
$ kubectl exec web -- sh
error: Internal error occurred: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "<id>": OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH
$ kubectl exec web -- ls /tmp
error: Internal error occurred: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "<id>": OCI runtime exec failed: exec failed: unable to start container process: exec: "ls": executable file not found in $PATH
여기서 봐야 할 부분: sh뿐 아니라 ls도 없습니다. 이 이미지에는 python3.11 실행 파일 외에 셸 도구가 없기 때문입니다.
2. 임시 컨테이너 붙이기와 --target의 효과
먼저 --target 없이 붙여 봅니다. -c는 새 컨테이너 이름이고, -- 뒤가 그 컨테이너에서 실행할 명령입니다. 임시 컨테이너는 실행이 끝나면 종료되므로 결과는 kubectl logs -c 이름으로 봅니다.
$ kubectl debug web -c dbg-noshare --image=busybox:1.36 -- sh -c 'ps; echo ---; ls /'
--profile=legacy is deprecated and will be removed in the future. It is recommended to explicitly specify a profile, for example "--profile=general".
$ kubectl logs web -c dbg-noshare
PID USER TIME COMMAND
1 root 0:00 sh -c ps; echo ---; ls /
7 root 0:00 ps
---
bin
dev
etc
home
lib
lib64
proc
root
sys
tmp
usr
var
여기서 봐야 할 부분: ps 결과의 PID 1이 임시 컨테이너 자신의 sh이고 web의 파이썬 프로세스는 보이지 않습니다. 컨테이너마다 프로세스 공간이 분리되어 있기 때문입니다. 또 --profile을 주지 않으면 legacy 프로파일이 쓰이고 지원 중단 예정이라는 경고가 나옵니다. 이후에는 --profile=general을 명시합니다.
이번에는 --target=app을 줍니다.
$ kubectl debug web -c dbg --image=busybox:1.36 --target=app -- sh -c 'ps; echo ---; ls /proc/1/root/tmp; echo ---; ls /proc/1/root/usr/bin | head -3; echo ---; wget -qO- http://localhost:8000/ | head -5; echo ---; cat /proc/1/cmdline | tr "\0" " "; echo'
Targeting container "app". If you don't see processes from this container it may be because the container runtime doesn't support this feature.
--profile=legacy is deprecated and will be removed in the future. It is recommended to explicitly specify a profile, for example "--profile=general".
$ kubectl logs web -c dbg
PID USER TIME COMMAND
1 root 0:00 /usr/bin/python3.11 -m http.server 8000 --directory /tmp
19 root 0:00 sh -c ps; echo ---; ls /proc/1/root/tmp; echo ---; ls /proc/1/root/usr/bin | head -3; echo ---; wget -qO- http://localhost:8000/ | head -5; echo ---; cat /proc/1/cmdline | tr "\0" " "; echo
25 root 0:00 ps
---
---
python
python3
python3.11
---
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /</title>
---
/usr/bin/python3.11 -m http.server 8000 --directory /tmp
여기서 봐야 할 부분: 세 가지입니다.
- PID 1이
/usr/bin/python3.11 -m http.server 8000, 즉app의 프로세스로 바뀌었습니다. 임시 컨테이너가app과 프로세스 공간을 공유하기 때문입니다. /proc/1/root를 통해app의 파일 시스템을 읽을 수 있습니다./usr/bin의python3.11이 보입니다.- 같은 파드라서
localhost:8000으로app의 웹 서버에 바로 접속됩니다.
두 경우의 차이를 그림으로 비교하면 다음과 같습니다.

임시 컨테이너의 제약도 확인했습니다. 한 번 붙인 임시 컨테이너는 파드에 남고, 같은 이름으로는 다시 만들 수 없습니다.
$ kubectl get pod web -o jsonpath='{range .status.ephemeralContainerStatuses[*]}{.name}{"\t"}{.state.terminated.reason}{"\t"}exit={.state.terminated.exitCode}{"\n"}{end}'
dbg Completed exit=0
dbg-noshare Completed exit=0
$ kubectl get pod web -o jsonpath='{.spec.ephemeralContainers[*].name}'
dbg-noshare dbg
$ kubectl debug web -c dbg --image=busybox:1.36 --target=app --profile=general -- true
Targeting container "app". If you don't see processes from this container it may be because the container runtime doesn't support this feature.
The Pod "web" is invalid: spec.ephemeralContainers: Forbidden: existing ephemeral containers "dbg" may not be changed
여기서 봐야 할 부분: 끝난 임시 컨테이너(Completed)도 파드의 spec.ephemeralContainers에 계속 남고, 같은 이름(dbg)으로 다시 만들면 may not be changed로 거부됩니다. 공식 문서도 임시 컨테이너는 추가한 뒤 바꾸거나 지울 수 없고, kubectl edit로 추가할 수도 없다고 설명합니다. 반복해서 디버깅한다면 이름을 매번 다르게 줍니다.
3. CrashLoopBackOff 파드는 복제해서 본다
이제 crash 파드입니다. 로그와 종료 상태는 다음과 같습니다.
$ kubectl logs crash
Traceback (most recent call last):
File "<string>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: '/etc/app/settings.json'
$ kubectl get pod crash
NAME READY STATUS RESTARTS AGE
crash 0/1 CrashLoopBackOff 5 (13s ago) 3m18s
$ kubectl get pod crash -o jsonpath='{.status.containerStatuses[0].lastState.terminated}'
{"containerID":"containerd://<id>","exitCode":1,"finishedAt":"2026-10-29T00:53:30Z","reason":"Error","startedAt":"2026-10-29T00:53:30Z"}
여기서 봐야 할 부분: 로그가 /etc/app/settings.json 파일이 없다는 것까지는 알려 주지만, 파일이 왜 없는지는 알려 주지 않습니다. 마지막 종료 상태의 종료 코드 1은 앱이 오류로 끝났다는 뜻입니다(종료 코드는 Docker 메모리·CPU 제한 직접 확인하기 글의 6번에 정리했습니다). 마운트한 디렉터리 안을 봐야 원인을 알 수 있으니 임시 컨테이너를 붙여 봅니다.
3-1. --target을 주면 임시 컨테이너가 시작되지 못합니다.
$ kubectl debug crash -c dbg --image=busybox:1.36 --target=app --profile=general -- sh -c 'ls -l /proc/1/root/etc/app 2>&1 | head; echo exit=$?'
$ kubectl get pod crash -o jsonpath='{.status.ephemeralContainerStatuses[?(@.name=="dbg")].state.waiting.reason}'
CreateContainerError
$ kubectl get pod crash -o jsonpath='{.status.ephemeralContainerStatuses[?(@.name=="dbg")].state.waiting.message}'
failed to generate container "<id>" spec: failed to generate spec opts: invalid target container: container "<id>" is not running - in state CONTAINER_EXITED
여기서 봐야 할 부분: 대상 컨테이너가 종료된 상태(CONTAINER_EXITED)여서 프로세스 공간을 공유할 수 없다는 invalid target container 오류로 CreateContainerError에 머뭅니다. 앱이 죽어 있는 동안에는 --target 방식이 통하지 않습니다.
3-2. --target 없이 붙이면 실행은 되지만 원하는 것이 보이지 않습니다.
$ kubectl debug crash -c dbg3 --image=busybox:1.36 --profile=general -- sh -c 'ps; echo ---; ls /etc/app 2>&1; echo exit=$?'
$ kubectl logs crash -c dbg3
PID USER TIME COMMAND
1 root 0:00 sh -c ps; echo ---; ls /etc/app 2>&1; echo exit=$?
7 root 0:00 ps
---
ls: /etc/app: No such file or directory
exit=1
여기서 봐야 할 부분: 임시 컨테이너는 실행되었지만 /etc/app이 없습니다. kubectl debug가 만드는 임시 컨테이너에는 기본적으로 파드의 볼륨이 마운트되지 않기 때문입니다.
3-3. --copy-to로 파드를 복제합니다. 원본은 그대로 두고, 같은 설정의 새 파드를 만들되 이미지를 busybox로 바꾸고 명령을 sleep 3600으로 바꿔 죽지 않게 합니다. 복제본은 같은 ConfigMap 볼륨을 마운트합니다.
$ kubectl debug crash --copy-to=crash-debug -c app --set-image=app=busybox:1.36 --profile=general -- sleep 3600
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
crash 0/1 CrashLoopBackOff 4 (58s ago) 2m42s
crash-debug 1/1 Running 0 2s
web 1/1 Running 0 2m42s
$ kubectl exec crash-debug -- ls -l /etc/app
total 0
lrwxrwxrwx 1 root root 18 Oct 29 00:55 config.json -> ..data/config.json
$ kubectl exec crash-debug -- cat /etc/app/config.json
{"mode": "prod"}
$ kubectl get pod crash-debug -o jsonpath='{.spec.containers[0].image}{" | "}{.spec.containers[0].command}{" | "}{.spec.containers[0].args}{"\n"}'
busybox:1.36 | ["sleep","3600"] |
여기서 봐야 할 부분: 복제본 crash-debug는 Running 상태이고, /etc/app 안에 settings.json이 아니라 config.json이 있습니다. 앱은 settings.json을 읽는데 ConfigMap의 키 이름은 config.json이라서 파일이 없던 것입니다. 해결은 앱이 읽는 경로나 ConfigMap의 키 이름 중 하나를 맞추는 것입니다. 로그만으로는 알기 어려운 원인을 복제본에서 직접 찾았습니다.
세 가지 시도를 한 장으로 정리하면 다음과 같습니다.

복제본은 트래픽을 받지 않습니다. 원본에 라벨을 붙이고 복제해 비교했습니다.
$ kubectl label pod crash app=demo tier=backend
$ kubectl debug crash --copy-to=crash-debug2 -c app --set-image=app=busybox:1.36 --profile=general -- sleep 3600
$ kubectl get pod crash crash-debug2 --show-labels
NAME READY STATUS RESTARTS AGE LABELS
crash 0/1 CrashLoopBackOff 6 (92s ago) 7m23s app=demo,tier=backend
crash-debug2 1/1 Running 0 4s <none>
여기서 봐야 할 부분: 복제본에는 라벨이 하나도 없습니다. --keep-labels 옵션의 기본값이 false이기 때문입니다. 그래서 라벨로 파드를 고르는 Service의 대상에 들어가지 않아, 복제본이 실제 요청을 받을 걱정은 없습니다. 반대로 라벨을 유지하려면 --keep-labels를 줍니다. 확인이 끝나면 복제본은 kubectl delete pod crash-debug로 지웁니다.
4. 디버그 프로파일: 어떤 권한이 붙는가
임시 컨테이너에 줄 권한은 --profile로 고릅니다. 공식 문서의 설명은 다음과 같습니다.
| 프로파일 | 공식 문서 설명 |
|---|---|
| legacy | 1.22 동작과 호환되는 속성 (기본값이지만 지원 중단 예정) |
| general | 디버깅 전반에 쓰는 무난한 속성 |
| baseline | Pod Security Standard의 baseline 정책과 호환되는 속성 |
| restricted | Pod Security Standard의 restricted 정책과 호환되는 속성 |
| netadmin | 네트워크 관리자 권한을 포함하는 속성 |
| sysadmin | 시스템 관리자(root) 권한을 포함하는 속성 |
프로파일마다 실제로 어떤 차이가 생기는지 같은 파드에 하나씩 붙여 컨테이너가 가진 권한(CapEff)을 읽어 봤습니다.
$ for p in general baseline restricted netadmin sysadmin; do kubectl debug web -c p-$p --image=busybox:1.36 --target=app --profile=$p --quiet -- sh -c 'grep CapEff /proc/self/status; id -u'; done
$ kubectl logs web -c p-general
CapEff: 00000000a80c25fb
0
$ kubectl logs web -c p-restricted
Error from server (BadRequest): container "p-restricted" in pod "web" is waiting to start: CreateContainerConfigError
$ kubectl get pod web -o jsonpath='{.spec.ephemeralContainers[?(@.name=="p-restricted")].securityContext}'
{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}}
$ kubectl get pod web -o jsonpath='{.status.ephemeralContainerStatuses[?(@.name=="p-restricted")].state.waiting.message}'
container has runAsNonRoot and image will run as root (pod: "web_default(<uid>)", container: p-restricted)
| 프로파일 | CapEff (유효 권한) | 생성된 securityContext | 결과 |
|---|---|---|---|
| general | 00000000a80c25fb |
capabilities.add: [SYS_PTRACE] |
실행됨 |
| baseline | 00000000a80425fb |
비어 있음 (기본 권한) | 실행됨 |
| netadmin | 00000000a80435fb |
capabilities.add: [NET_ADMIN, NET_RAW] |
실행됨 |
| sysadmin | 000001ffffffffff |
privileged: true |
실행됨 (모든 권한) |
| restricted | - | runAsNonRoot: true, capabilities.drop: [ALL] 등 |
시작 실패 (CreateContainerConfigError) |
여기서 봐야 할 부분: 네 가지입니다.
general은baseline보다CapEff값이0x80000커졌고, 이는SYS_PTRACE(다른 프로세스를 추적하는 권한) 한 가지가 더해진 것입니다. 프로세스를 들여다보는 디버깅에 맞춘 구성입니다.netadmin은NET_ADMIN이 더해져 값이0x1000커졌습니다.NET_RAW는 기본 권한에 이미 포함되어 있었습니다.sysadmin은privileged: true라서 모든 권한 비트가 켜졌습니다. 꼭 필요할 때만 씁니다.restricted는 root가 아닌 사용자로 실행하라고 요구하는데 busybox 이미지는 root로 실행되어 시작하지 못했습니다. restricted를 쓰려면 non-root로 실행되는 디버그 이미지가 필요합니다.
5. 노드를 디버깅하기
노드에 SSH로 들어갈 수 없을 때는 kubectl debug node/노드이름으로 그 노드에서 도는 파드를 만들 수 있습니다.
$ kubectl debug node/<node> --image=busybox:1.36 --profile=general -- sh -c 'echo hostname=$(hostname); ls /host | head -5; echo ---; grep PRETTY_NAME /host/etc/os-release; ls /host/etc/rancher/k3s | head -3; id -u'
Creating debugging pod node-debugger-<node>-4lmgl with container debugger on node <node>.
$ kubectl logs node-debugger-<node>-4lmgl
hostname=<node>
bin
dev
etc
lib
proc
---
PRETTY_NAME="K3s v1.35.9+k3s1"
k3s.yaml
0
$ kubectl get pod node-debugger-<node>-4lmgl -o jsonpath='hostPID={.spec.hostPID} hostNetwork={.spec.hostNetwork} hostIPC={.spec.hostIPC} volume={.spec.volumes[0].hostPath.path} mount={.spec.containers[0].volumeMounts[0].mountPath}'
hostPID=true hostNetwork=true hostIPC=true volume=/ mount=/host
여기서 봐야 할 부분: 이 파드는 hostPID, hostNetwork, hostIPC가 모두 true이고, 노드의 루트 디렉터리(/)가 컨테이너의 /host에 마운트됩니다. 그래서 /host/etc/os-release처럼 노드의 파일을 읽을 수 있고, hostname도 노드 이름이 나옵니다. 노드 전체가 보이므로 쓰고 나면 파드를 반드시 지웁니다.
6. 필요한 권한(RBAC)
kubectl debug로 임시 컨테이너를 붙이는 것은 파드의 ephemeralcontainers라는 하위 리소스를 수정하는 일입니다. 파드 조회와 exec 권한만 준 서비스 어카운트로 확인했습니다.
$ kubectl debug web -c v1 --image=busybox:1.36 --profile=general --as=system:serviceaccount:default:viewer -- true
Error from server (Forbidden): pods "web" is forbidden: User "system:serviceaccount:default:viewer" cannot patch resource "pods/ephemeralcontainers" in API group "" in the namespace "default"
$ kubectl create role pod-debug --verb=patch,update --resource=pods/ephemeralcontainers
$ kubectl create rolebinding viewer-debug --role=pod-debug --serviceaccount=default:viewer
$ kubectl debug web -c v1 --image=busybox:1.36 --profile=general --as=system:serviceaccount:default:viewer -- true
$ kubectl get pod web -o jsonpath='{.status.ephemeralContainerStatuses[?(@.name=="v1")].state.terminated.reason}'
Completed
여기서 봐야 할 부분: 권한이 없으면 cannot patch resource "pods/ephemeralcontainers"로 거부됩니다. 이 하위 리소스에 patch(와 update)를 허용하자 임시 컨테이너가 실행되어 Completed가 되었습니다. exec 권한과는 별개이므로, 디버깅 권한은 따로 주고 따로 감사해야 합니다.
상황별로 무엇을 쓰는가
| 상황 | 쓰는 방법 |
|---|---|
| 컨테이너에 쉘과 도구가 있다 | kubectl exec -it 파드 -- sh |
| 쉘이 없지만 앱은 실행 중이다 | kubectl debug 파드 -it --image=busybox --target=컨테이너 --profile=general |
| 앱이 죽었다 살아나기를 반복한다 (CrashLoopBackOff) | kubectl debug 파드 --copy-to=복제이름 -c 컨테이너 --set-image=컨테이너=busybox -- sleep 3600 후 exec |
| 노드 자체를 봐야 한다 | kubectl debug node/노드이름 -it --image=busybox --profile=general |
트레이드오프와 주의점
- 임시 컨테이너는 지울 수 없습니다. 파드가 사라질 때까지
spec에 남고 이름도 다시 못 씁니다. 그래서 같은 파드를 오래 디버깅하면 기록이 쌓입니다. - 복제본은 원본과 완전히 같지 않습니다. 이 글의 실험에서는 라벨이 제거되었고, 명령과 이미지도 바꿨습니다. 증상이 원본의 런타임 상태에만 있는 문제는 재현되지 않을 수 있습니다. 복제본은 쓰고 나서 지웁니다.
- 디버그 이미지를 받을 수 있어야 합니다. 인터넷이 막힌 환경에서는 사내 레지스트리에 디버그용 이미지를 미리 올려 두고
--image로 지정합니다. - 권한을 통제합니다. 임시 컨테이너는 대상 파드의 네트워크와 (
--target이면) 프로세스, 파일에 접근하고,sysadmin프로파일이나 노드 디버깅은 훨씬 넓은 권한을 줍니다.pods/ephemeralcontainers권한은 필요한 사람에게만 줍니다. - 스태틱 파드에는 쓸 수 없습니다. 쿠버네티스 문서에 임시 컨테이너는 static pod를 지원하지 않는다고 적혀 있습니다.
정리
- distroless 같은 쉘 없는 파드는
kubectl exec가 실패하지만,kubectl debug로 busybox 같은 임시 컨테이너를 붙이면 확인할 수 있습니다. --target을 주면ps에서 대상 앱이 PID 1로 보이고/proc/1/root로 그 파일을 읽을 수 있었습니다.- CrashLoopBackOff 파드에는
--target임시 컨테이너가 붙지 않았고(invalid target container), 임시 컨테이너에는 볼륨이 기본 마운트되지 않았습니다. 이 경우--copy-to로 복제해 명령을 바꿔 살려 두고 확인했습니다. - 복제본은 기본으로 라벨이 제거되어 Service 트래픽을 받지 않았습니다.
--profile로 권한을 고르며general은SYS_PTRACE가 추가된 구성이고sysadmin은 privileged입니다.restricted는 root 이미지로는 시작하지 못했습니다.- 임시 컨테이너는
pods/ephemeralcontainers권한이 필요하고, 한 번 붙이면 지울 수 없습니다.
체크리스트
- 디버그할 때는
--profile을 명시합니다. 생략하면 지원 중단 예정인legacy가 쓰입니다. - 죽는 파드는
--copy-to로 복제해서 봅니다. 쓰고 나면 복제본을 지웁니다. - 임시 컨테이너 이름은 매번 다르게 줍니다. 같은 이름은 다시 쓸 수 없습니다.
- 필요 이상으로 넓은 프로파일을 쓰지 않습니다. 대부분은
general이면 충분합니다.
요약
쉘이 없는 파드는 kubectl debug로 도구 컨테이너를 붙여 들여다봅니다. --target을 주면 대상 앱의 프로세스와 파일까지 보입니다. 다만 앱이 죽어 있는 CrashLoopBackOff 상태에서는 --target이 통하지 않고 임시 컨테이너에는 볼륨도 기본으로 붙지 않으므로, --copy-to로 파드를 복제해 명령을 바꿔 살려 놓고 확인하는 것이 방법입니다. 프로파일과 pods/ephemeralcontainers 권한으로 어디까지 허용할지 통제합니다.
참고사항
'DevOps > Kubernetes' 카테고리의 다른 글
| Kubernetes 1.37 HPA 스케일 투 제로(Beta) - 워커를 0개까지 줄였다가 다시 켜는 법 (0) | 2026.10.16 |
|---|---|
| Helm 차트 기초 정리 - Kustomize와 무엇이 다른가 (0) | 2026.10.02 |
| Kustomize 정리 - base와 overlay로 환경별 매니페스트 관리하기 (0) | 2026.09.30 |
| kubectl 생산성 도구 모음 - krew로 설치하는 실전 플러그인 5가지 (0) | 2026.09.30 |
| Dynamic Resource Allocation(DRA) 정리 - device plugin을 대체하는 쿠버네티스 하드웨어 할당 표준 (0) | 2026.09.30 |
