시작하며
로컬 개발 환경에서 Kind(Kubernetes in Docker)를 사용하던 중, 갑자기 모든 Pod들이 계속 재시작하는 문제를 만났습니다. kubectl get pods를 실행하면 RESTARTS 숫자가 103, 217, 166... 끊임없이 올라가고 있었죠.
NAME READY STATUS RESTARTS
app-stack-api-gateway-69f8ccb8f6-4cs74 1/2 CrashLoopBackOff 103
app-stack-inventory-service-9c4f4cf8d-pcvnd 1/2 CrashLoopBackOff 217
app-stack-user-service-86bc8b87fb-n92n9 1/2 CrashLoopBackOff 166
처음에는 당연히 메모리 부족이라고 생각했습니다. 하지만 kubectl describe pod를 확인해보니 전혀 예상치 못한 에러 메시지가 보였습니다:(
너무 계속 죽어서 리소스가 문제인지 , 뭐가 문제인지 전혀 파악하지 못했답니다..
그러던 중
Normal SandboxChanged 55s kubelet Pod sandbox changed, it will be killed and re-created.
"샌드박스가 변경되었다고? 무슨 말이지?" 처음 보는 에러였습니다. 오늘은 이 문제를 해결하면서 배운 내용을 공유해보려고 합니다.
SandboxChanged 에러란?
Pod Sandbox의 개념
Kubernetes에서 Pod Sandbox는 Pod가 실행되는 격리된 환경을 의미합니다. 이 샌드박스는 다음과 같은 요소들로 구성됩니다:
- 네트워크 인터페이스
- 스토리지 마운트
- cgroup 관리 (리소스 제어)
여기서 핵심은 cgroup입니다. Cgroup(Control Groups)은 Linux에서 프로세스의 CPU, 메모리, I/O 등의 리소스를 제어하는 메커니즘입니다.
에러가 발생하는 이유
Kubernetes의 kubelet은 Pod의 cgroup에 특정 컨트롤러(CPU, 메모리 등을 제어하는 스위치)가 안정적으로 존재하는지 확인합니다. 만약 이 컨트롤러들이 사라지거나 변경되면, kubelet은 "샌드박스가 변경되었다"고 판단하고 Pod를 강제로 재시작합니다.
문제는 systemd와 containerd가 같은 cgroup을 서로 관리하려고 할 때 발생합니다:
- Containerd가 cgroup에 컨트롤러를 설정
- Systemd가 "이건 내가 관리하는 게 아니네?" 하고 컨트롤러를 제거
- Kubelet이 "컨트롤러가 없어졌네? Pod 재시작!"
- 무한 반복...
문제 상황 분석
제 환경 구성
저는 Kind를 사용해 로컬 Kubernetes 클러스터를 구성하고 있었습니다. 특히 private registry를 연동하기 위해 Kind 노드의 containerd 설정을 커스터마이징했었죠.
# setup-kind.sh 스크립트 중 일부
docker exec "${node}" sh -c "cat > /etc/containerd/config.toml <<'CONFEOF'
version = 2
[plugins]
[plugins.\"io.containerd.grpc.v1.cri\"]
[plugins.\"io.containerd.grpc.v1.cri\".registry]
# ... registry 설정만 있음
CONFEOF
"
문제는 이 설정에서 가장 중요한 한 줄이 빠져있었다는 것입니다.
증상 확인
다음 명령어로 문제를 확인할 수 있었습니다:
# Pod 상태 확인
kubectl describe pod postgresql-0 | grep -i sandbox
# 결과:
# Normal SandboxChanged 55s kubelet Pod sandbox changed, it will be killed and re-created.
모든 Pod들이 이 메시지와 함께 끊임없이 재시작되고 있었습니다.
해결 방법: SystemdCgroup 설정 추가
핵심 해결책
문제의 원인은 containerd가 systemd에게 cgroup 관리를 위임하지 않았기 때문입니다. 해결책은 간단합니다. containerd 설정에 SystemdCgroup = true를 추가하는 것이죠.
수정된 설정
version = 2
[plugins]
[plugins."io.containerd.grpc.v1.cri"]
# =============================================================================
# Containerd 런타임 설정
# =============================================================================
[plugins."io.containerd.grpc.v1.cri".containerd]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
# ⚠️ 이 설정이 핵심입니다!
# systemd에게 cgroup 관리를 위임합니다
SystemdCgroup = true
# =============================================================================
# Private Registry 설정
# =============================================================================
[plugins."io.containerd.grpc.v1.cri".registry]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."kind-registry:5000"]
endpoint = ["http://kind-registry:5000"]
[plugins."io.containerd.grpc.v1.cri".registry.configs]
[plugins."io.containerd.grpc.v1.cri".registry.configs."kind-registry:5000"]
[plugins."io.containerd.grpc.v1.cri".registry.configs."kind-registry:5000".tls]
insecure_skip_verify = true
Containerd 플러그인 이해하기
설정을 보면 [plugins."io.containerd.grpc.v1.cri"]와 같은 긴 경로가 보이는데요, 이게 무엇일까요?
Containerd는 플러그인 아키텍처로 동작합니다. 마치 레고 블록처럼, 필요한 기능들을 플러그인 형태로 조립해서 사용하는 거죠. 주요 플러그인들은 다음과 같습니다:
- io.containerd.grpc.v1.cri: CRI (Container Runtime Interface) 플러그인
- Kubernetes가 containerd와 통신하는 인터페이스
- Pod와 컨테이너를 생성/관리하는 역할
- io.containerd.runc.v2: 런타임 플러그인
- 실제로 컨테이너를 실행하는 저수준 런타임
- runc를 사용해 Linux 컨테이너를 생성
우리가 수정한 설정은 CRI 플러그인 내부의 런타임 옵션입니다. 즉, "Kubernetes가 containerd를 통해 컨테이너를 실행할 때, runc를 어떻게 사용할 것인가"를 정의하는 것이죠.
설정 구조 시각화:
Containerd
└── CRI 플러그인 (Kubernetes 인터페이스)
├── Registry 설정 (이미지 저장소)
└── Containerd Runtime
└── runc 런타임
└── options
└── SystemdCgroup = true ← 여기!
설정 파일로 표현하면:
[plugins."io.containerd.grpc.v1.cri"] # CRI 플러그인
[plugins."io.containerd.grpc.v1.cri".containerd]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] # runc 런타임
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true # runc가 systemd cgroup을 사용
왜 이 설정이 중요한가?
SystemdCgroup = true 설정은 다음을 의미합니다:
- Containerd가 직접 cgroup을 관리하지 않음
- 대신 systemd에게 cgroup 관리를 위임
- Systemd가 공식적으로 cgroup을 관리하므로 충돌이 발생하지 않음
이것은 단순한 설정 하나지만, systemd 기반 시스템에서 Kubernetes를 운영할 때 필수적인 설정입니다.
간단히 정리하면:
- 플러그인: containerd가 제공하는 기능 모듈 (CRI, 런타임, 스냅샷터 등)
- CRI 플러그인: Kubernetes와 containerd를 연결하는 다리
- 런타임 옵션: 실제 컨테이너를 어떻게 실행할지 정의
- SystemdCgroup: 누가 리소스를 관리할지 결정하는 핵심 설정
실제 적용하기
기존 클러스터에 바로 적용하는 방법
클러스터를 재생성하지 않고 바로 적용하려면 다음과 같이 하세요:
# 1. 모든 Kind 노드에 설정 적용
for node in $(kind get nodes --name k8s-lab); do
docker exec "${node}" sh -c "cat > /etc/containerd/config.toml <<'CONFEOF'
version = 2
[plugins]
[plugins.\"io.containerd.grpc.v1.cri\"]
[plugins.\"io.containerd.grpc.v1.cri\".containerd]
[plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes]
[plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes.runc]
runtime_type = \"io.containerd.runc.v2\"
[plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes.runc.options]
SystemdCgroup = true
CONFEOF
"
# 2. Containerd 재시작
docker exec "${node}" systemctl restart containerd
echo "✅ ${node} 설정 완료"
done
# 3. 기존 Pod들 삭제 (자동으로 재생성됨)
kubectl delete pod --all -n default
# 4. 상태 확인
kubectl get pods -w
스크립트에 반영하기
Kind 클러스터를 생성하는 스크립트가 있다면, 다음과 같이 수정하세요:
#!/bin/bash
# setup-kind.sh
# ... (클러스터 생성 부분은 동일)
# Containerd 설정 적용
for node in $(kind get nodes --name ${CLUSTER_NAME}); do
echo "Configuring node: ${node}"
docker exec "${node}" sh -c "cat > /etc/containerd/config.toml <<'CONFEOF'
version = 2
[plugins]
[plugins.\"io.containerd.grpc.v1.cri\"]
[plugins.\"io.containerd.grpc.v1.cri\".containerd]
[plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes]
[plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes.runc]
runtime_type = \"io.containerd.runc.v2\"
[plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes.runc.options]
SystemdCgroup = true
[plugins.\"io.containerd.grpc.v1.cri\".registry]
# ... registry 설정
CONFEOF
"
docker exec "${node}" systemctl restart containerd
done
검증 및 확인
정상 동작 확인
설정을 적용한 후 다음과 같이 확인하세요:
# 1. Pod 상태 확인
kubectl get pods
# 예상 결과: 모든 Pod가 Running 상태, RESTARTS가 더 이상 증가하지 않음
NAME READY STATUS RESTARTS
app-stack-api-gateway-69f8ccb8f6-4cs74 2/2 Running 0
app-stack-inventory-service-9c4f4cf8d-h5nrf 2/2 Running 0
postgresql-0 2/2 Running 0
# 2. SandboxChanged 이벤트가 없는지 확인
kubectl describe pod postgresql-0 | grep -i sandbox
# 예상 결과: 아무 메시지도 나오지 않거나, 과거 이벤트만 보임
# 3. 일정 시간 후에도 RESTARTS가 증가하지 않는지 확인
watch -n 2 'kubectl get pods'
문제가 지속될 경우
만약 여전히 문제가 발생한다면:
# 1. Containerd 설정이 제대로 적용되었는지 확인
for node in $(kind get nodes --name k8s-lab); do
echo "=== ${node} ==="
docker exec "${node}" cat /etc/containerd/config.toml | grep -A 3 "SystemdCgroup"
done
# 2. Containerd가 정상 실행 중인지 확인
for node in $(kind get nodes --name k8s-lab); do
echo "=== ${node} ==="
docker exec "${node}" systemctl status containerd
done
# 3. Kubelet 로그 확인
kubectl logs -n kube-system -l component=kubelet --tail=50
배운 점과 인사이트
1. 설정 파일을 덮어쓸 때는 신중하게
저는 registry 설정을 추가하기 위해 containerd 설정 파일 전체를 덮어썼습니다. 하지만 기본 설정 중 중요한 부분을 누락했죠. 설정을 커스터마이징할 때는 다음을 기억해야 합니다:
- 기존 기본 설정이 왜 있는지 이해하기
- 공식 문서나 기본 설정 파일 참고하기
- 가능하면 필요한 부분만 추가하기
2. systemd와 친해지기
Kubernetes를 운영하다 보면 systemd를 피할 수 없습니다. 특히 cgroup 관리에서 systemd의 역할은 매우 중요합니다. **"누가 cgroup을 관리하는가?"**를 명확히 하는 것이 많은 문제를 예방합니다.
3. 에러 메시지를 있는 그대로 읽기
"SandboxChanged"라는 메시지를 처음 봤을 때, 저는 당황했습니다. 하지만 메시지를 있는 그대로 받아들이면 **"샌드박스가 변경되었다"**는 의미입니다. 이것은 cgroup 설정이 불안정하다는 신호였죠.
에러 메시지를 두려워하지 말고, 그 안에서 힌트를 찾아보세요.
다른 환경에서도 발생할 수 있는 문제
Amazon Linux 2023 (AL2023)
AL2023에서는 nodeadm이라는 도구가 부팅 시 containerd를 설정합니다. 만약 containerd가 nodeadm보다 먼저 시작되면, 잘못된 기본 설정으로 실행되어 같은 문제가 발생할 수 있습니다.
해결책은 systemd unit 순서를 조정하는 것입니다:
# nodeadm이 먼저 실행되도록 설정
systemctl edit containerd.service
# 다음 내용 추가:
[Unit]
After=nodeadm.service
cgroupv2 환경
cgroupv2를 사용하는 최신 Linux 배포판에서는 특히 이 문제가 더 민감하게 나타날 수 있습니다. cgroupv2는 컨트롤러 관리가 더 엄격하기 때문입니다.
트러블슈팅 체크리스트
SandboxChanged 에러를 만났다면 다음을 확인하세요:
1단계: Containerd 설정 확인
# SystemdCgroup 설정이 있는지 확인
cat /etc/containerd/config.toml | grep -i systemd
2단계: Containerd 재시작 여부 확인
# 설정 변경 후 재시작했는지 확인
systemctl status containerd
3단계: Cgroup 컨트롤러 모니터링
# Pod의 cgroup 컨트롤러가 안정적인지 확인
watch -n 1 'cat /sys/fs/cgroup/kubepods.slice/kubepods-besteffort.slice/*/cgroup.controllers'
4단계: 다른 Pod의 영향 확인
# 같은 QoS 클래스에 다른 Pod가 있는지 확인
systemd-cgls | grep kubepods
마치며
처음 SandboxChanged 에러를 봤을 때는 정말 막막했습니다. "샌드박스가 뭐지?", "왜 변경된 거지?"라는 의문만 가득했죠. 하지만 문제를 하나씩 파고들다 보니, 결국은 systemd와 containerd가 cgroup 관리 권한을 놓고 다투는 간단한 문제였습니다.
이 글이 같은 문제로 고생하시는 분들께 도움이 되길 바랍니다. 특히 Kind로 로컬 개발 환경을 구성하시는 분들이라면, 처음부터 SystemdCgroup = true 설정을 꼭 확인하시길 권장합니다!
3~4일은 이것 저것 확인했네요..
혹시 다른 환경에서 비슷한 문제를 겪으셨거나, 다른 해결 방법을 아신다면 댓글로 공유해주세요. 함께 배우는 것이 가장 빠른 성장의 길이니까요. 😊
참고 자료
- SandBoxChanged Error in k8s (kind)
- Containerd 공식 문서 - Systemd Cgroup
- Kubernetes cgroup 관리 문서
- Kind 공식 문서
Fixing SandboxChanged Errors in Kubernetes Runtimes
SandboxChanged errors in Kubernetes often stem from cgroup misconfigurations. Learn how to debug and resolve these elusive pod restart issues.
edera.dev
'Network' 카테고리의 다른 글
| AWS SAA-CO3 합격 후기 (0) | 2026.03.07 |
|---|---|
| Terraform으로 EC2 관리하면서 겪은 AMI 필터링과 SSM 연결 이슈 (0) | 2026.02.25 |
| Istio로 대규모 트래픽 관리하기: 실전 적용 사례와 확장 전략 (2) | 2026.01.10 |
| EKS에서 ArgoCD로 GitOps 구축하기: 순환 의존성 해결부터 Single Sync까지 (0) | 2026.01.05 |
| 실전 Kubernetes 보안: 4C 모델을 프로덕션 클러스터에 적용하며 배운 것들 (0) | 2026.01.04 |