RHEL 9 계열에 containerd 1.7.x 와 kubeadm 으로 Kubernetes v1.33 을 올리다가 두 단계에서 연달아 막힌 사례다. 앞 단계는 kubeadm init preflight 가 CRI 를 못 찾는 문제이고, 뒤 단계는 CRI 가 살아난 뒤에도 컨테이너 자체가 뜨지 않는 문제다. 두 증상은 원인이 전혀 다르므로 순서대로 갈라내야 한다.
kubeadm init 이 preflight 에서 다음 메시지를 내고 멈춘다.
failed to create new CRI runtime service: validate service connection:
validate CRI v1 runtime API for endpoint "unix:///run/containerd/containerd.sock":
rpc error: code = Unimplemented desc = unknown service runtime.v1.RuntimeService
이 메시지는 "containerd 는 떠 있는데 그 소켓이 CRI 서비스를 제공하지 않는다"는 뜻이다. containerd 프로세스가 죽어 있는 것과는 다르다. crictl info 로도 같은 에러가 재현되면 kubeadm 문제가 아니라 containerd 문제로 확정할 수 있다.
먼저 CRI 플러그인이 실제로 어떤 상태로 올라왔는지 본다.
ctr plugins ls | egrep 'cri|runtime'
io.containerd.grpc.v1.cri 행의 상태가 ok 가 아니라 error 이면 플러그인이 로드 도중 실패한 것이다. 실패 사유는 서비스 로그에 그대로 찍힌다.
journalctl -u containerd -n 200 --no-pager | egrep -i 'cri|plugin|failed'
원인은 크게 두 갈래다.
첫째, disabled_plugins = ["cri"] 로 CRI 가 아예 꺼져 있는 경우다. 이쪽은 별도 문서에서 다룬다.
둘째, disabled_plugins = [] 인데도 플러그인이 error 로 뜨는 경우다. 이번 사례가 여기에 해당했고, 로그에는 다음 취지의 메시지가 남았다.
systemd_cgroup only works for runtime io.containerd.runtime.v1.linux
containerd 설정에는 cgroup 드라이버를 지정하는 자리가 두 곳 있고, 둘은 서로 다른 런타임을 위한 것이다.
| 키 | 위치 | 대상 |
|---|---|---|
systemd_cgroup |
[plugins."io.containerd.grpc.v1.cri"] |
폐기된 io.containerd.runtime.v1.linux 런타임 전용 |
SystemdCgroup |
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] |
현행 io.containerd.runc.v2 런타임 |
현행 구성(runtime_type = "io.containerd.runc.v2")에서 systemd_cgroup = true 를 CRI 플러그인 최상단에 두면 플러그인 초기화가 통째로 실패한다. 결과적으로 소켓은 열려 있지만 RuntimeService 가 등록되지 않아 위 Unimplemented 에러가 난다.
올바른 형태는 다음과 같다.
[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
설정을 고친 뒤 재시작하고 플러그인 상태를 다시 본다.
systemctl restart containerd
ctr plugins ls | grep cri
crictl --runtime-endpoint unix:///run/containerd/containerd.sock info | head
crictl info 가 JSON 을 뱉으면 이 단계는 끝난 것이다.
CRI 가 정상인데도 파드 샌드박스 생성이 실패하는 단계다.
failed to create shim task: OCI runtime create failed: runc create failed:
unable to start container process: can't copy bootstrap data to pipe:
write init-p: broken pipe
runc 가 컨테이너 init 프로세스를 띄우자마자 그 프로세스가 죽어 파이프가 닫혔다는 뜻이다. Kubernetes 계층의 문제가 아니므로 kubeadm 은 잠시 잊고 런타임만으로 재현한다.
ctr --namespace k8s.io run --rm docker.io/library/busybox:latest bbtest sh -c 'echo OK; id'
이것이 실패하면 kubeadm 을 아무리 만져도 소용이 없다.
아래 순서로 원인을 좁힌다. 위쪽일수록 빠르고 확실하다.
dmesg 에서 커널이 무엇을 죽였는지 먼저 본다.
dmesg -T | tail -n 300 | egrep -i 'runc|containerd|overlay|seccomp|selinux|avc|denied|killed|oom|segfault|trap'
segfault 나 trap 이면 runc 바이너리와 커널·라이브러리 조합 문제, Killed process 나 OOM 이면 메모리, avc: denied 면 SELinux 정책, overlay 관련 오류면 스냅샷 파일시스템 쪽이다.
/run 과 /tmp 의 noexec 여부를 본다. 이 둘이 noexec 로 마운트돼 있으면 일부 환경에서 컨테이너 init 이 즉시 죽는다.
mount | egrep ' on /(run|tmp) '
mount -o remount,exec /run
RHEL 계열에서는 fapolicyd 가 SELinux 와 별개로 실행 파일을 차단한다. SELinux 가 Permissive 여도 fapolicyd 는 독립적으로 동작하므로 따로 확인한다.
systemctl is-active fapolicyd
ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recent | tail -n 80
runc 대신 crun 으로 바꿔 보면 runc 고유 문제인지 시스템 전역 문제인지 한 번에 갈린다. runc.options 에 BinaryName = "/usr/bin/crun" 만 추가해 재시작한 뒤 같은 ctr run 을 돌린다. crun 은 되고 runc 만 죽으면 runc 패키지 문제이고, 둘 다 죽으면 시스템 차단·커널·마운트 옵션 쪽이다.
스냅샷 파일시스템 요건도 확인한다. overlayfs 는 백킹 XFS 에 ftype=1 을 요구한다.
xfs_info /var/lib/containerd | grep ftype
위 항목 중 SELinux(Permissive), seccomp 프로파일, ftype=1, /run·/tmp 의 noexec, fapolicyd 는 모두 원인이 아니었다. 최종적으로는 이미지를 명시적으로 다시 받은 뒤 같은 명령이 그대로 통과했다.
ctr --namespace k8s.io images pull docker.io/library/busybox:latest
ctr --namespace k8s.io run --rm docker.io/library/busybox:latest bbtest sh -c 'echo OK; id'
CRI 플러그인이 error 상태이던 동안, 그리고 containerd 의 root 경로를 바꿔 가며 설정을 여러 번 갈아엎는 동안 이미지 unpack 과 스냅샷이 반쯤 만들어진 상태로 남아 있었고, 그 깨진 번들 때문에 runc 가 init 을 띄우기도 전에 실패했다는 것이 당시의 결론이다. 다만 깨진 스냅샷이라는 진단 자체는 직접 증거로 확인된 것이 아니라 정황 추론이므로 그대로 믿을 것은 아니다 (확인 필요). 확실한 것은 다음 두 가지다. 컨테이너 런타임 계층의 오류는 ctr run 으로 Kubernetes 와 분리해 재현해야 한다는 것, 그리고 containerd 설정과 데이터 디렉터리를 바꾼 뒤에는 이미지 캐시를 신뢰하지 말고 다시 받아 봐야 한다는 것이다.
containerd.io 패키지(docker-ce-stable 저장소)는 RHEL AppStream 의 runc 를 obsolete 로 선언한다. 그래서 두 저장소에서 각각 가져다 쓰면 dnf 가 충돌을 내고, --allowerasing 으로 밀어붙이면 버전 숫자는 맞아도 패키징이 다른 조합이 남는다. containerd 와 runc 는 같은 저장소 세트로 통일하는 편이 낫다.
dnf remove -y runc containerd.io
dnf install -y containerd.io --allowerasing
containerd --version
runc --version
kubeadm init 은 --kubernetes-version 을 주지 않으면 원격의 최신 버전을 조회한다. 원격이 너무 앞서 있으면 remote version is much newer ... falling back to: stable-1.33 처럼 임의로 떨어뜨리므로, 재현 가능한 설치에서는 버전을 반드시 명시한다.
containerd config default 로 설정을 재생성하면 SystemdCgroup = false 가 기본이다. kubelet 이 systemd cgroup 드라이버를 쓰는 기본 구성에서는 반드시 true 로 맞춰야 한다.
unknown service runtime.v1.RuntimeService 메시지의 다른 원인 — disabled_plugins = ["cri"].