Docker 컨테이너에서 systemd 실행하기: 도커의 한계에 도전하다

1

보통 도커 컨테이너 내부에서는 단일 프로세스를 실행하는 것이 일반적입니다. 하지만 저는 도커의 한계에 도전해보려 합니다. 컨테이너 내부에서 Systemd를 실행하고, core dump를 생성하며 GDB 디버깅을 하고, strace를 사용하여 시스템 호출을 추적하고, NIC(Network Interface Controller) 등의 하드웨어 리소스에 접근할 수 있어야 합니다.

이 모든 것을 가능하게 만들기 위해 Docker 컨테이너에서 Systemd를 실행하는 방법을 정리해 보겠습니다.

도커와 Systemd로 검색해보면 관련된 velog 글이 2개 정도밖에 안 나오는 것 같은데, 본격적으로 나름 고독한 삽질을 한 오리지널한 내용에 대해서 다룬다는 자부심을 가지고 글을 써보겠습니다. 언젠가 누군가에게 도움이 되기를...


1. 도커에서 Systemd를 실행하는 것이 비권장되는 이유

도커는 원래 경량 컨테이너 환경을 제공하기 위해 설계되었습니다. 기본적으로 도커 컨테이너는 하나의 프로세스를 실행하는 용도로 사용되며, init 시스템(Systemd, SysVinit 등)이 필요 없는 구조로 동작합니다.

하지만 Systemd는 기본적으로 PID 1을 차지하며 여러 데몬을 관리하는 역할을 합니다. 이를 컨테이너 내부에서 실행하면 몇 가지 문제가 발생할 수 있습니다.

  • Systemd는 cgroups를 활용하지만, 도커의 기본 cgroups 환경과 충돌할 수 있습니다.
  • 도커는 기본적으로 단일 프로세스 모델을 따르기 때문에, Systemd를 사용하면 컨테이너 종료/재시작 시 예상과 다른 동작이 발생할 수 있습니다.
  • Systemd는 일반적으로 privileged mode(특권 모드)를 요구하는데, 이는 보안상 위험 요소가 될 수 있습니다.

그럼에도 왜 굳이 이러는 걸까요?

Why you do this?
Because I can.

그러나 산이 저기에 있기 때문에 등산하듯이,
저는 도커의 한계를 시험하고 싶으므로, Systemd 실행을 가능하게 만들 방법을 찾아볼 것입니다. 방법을 찾으면 언젠가 쓸모가 있겠지요...? 그게 비록 아무도 가지 않는 위험한 길일 지라도...


2. Docker 컨테이너에서 Systemd 실행하기

2.1. Systemd 실행을 위한 Dockerfile 작성

일반적인 컨테이너 배포 방식과 다르게, Systemd를 실행하려면 몇 가지 설정이 필요합니다.

✅ 사용할 베이스 이미지 선택

기본적으로 Ubuntu, CentOS, Arch Linux 같은 배포판은 Systemd를 사용할 수 있습니다. 저는 Arch Linux를 선호하지만, 이번에는 Rocky8를 사용하겠습니다.

Rocky Linux 8에서 Systemd를 실행하려면 공식 Docker 문서에서 제공하는 설정을 참고해야 합니다.

참고: Docker Hub Official rockylinux와, rockylinux 재단의 rockylinux/rockylinux 이미지는 서로 다릅니다. rockylinux의 깃헙 이슈글을 보면 후자가 더 관리가 잘 되어있다고 하므로, 후자를 사용하겠습니다.

다음과 같은 Dockerfile을 작성합니다.

FROM rockylinux/rockylinux:8.4

ENV container=docker

RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \
systemd-tmpfiles-setup.service ] || rm -f $i; done); \
rm -f /lib/systemd/system/multi-user.target.wants/*; \
rm -f /etc/systemd/system/*.wants/*; \
rm -f /lib/systemd/system/local-fs.target.wants/*; \
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
rm -f /lib/systemd/system/basic.target.wants/*; \
rm -f /lib/systemd/system/anaconda.target.wants/*;

VOLUME [ "/sys/fs/cgroup" ]

CMD ["/usr/sbin/init"]

설정 설명

  • ENV container=docker → 컨테이너 환경에서 Systemd가 실행 중임을 알립니다.
  • 불필요한 Systemd 서비스 제거 → 컨테이너 내에서 사용되지 않는 서비스를 정리하여 불필요한 리소스 소비를 줄입니다.
  • VOLUME [ "/sys/fs/cgroup" ] → Systemd가 제대로 동작하려면 cgroups 마운트가 필요합니다.
  • CMD ["/usr/sbin/init"] → 컨테이너 시작 시 Systemd 실행.

컨테이너 실행

Systemd를 제대로 실행하려면 몇 가지 필수 옵션이 필요합니다.

docker run --rm -it \
  --privileged \
  --volume /sys/fs/cgroup:/sys/fs/cgroup:ro \
  ubuntu-systemd

Rockylinux에서 실행하려면 ro로 충분한데, WSL Ubuntu host에서 실행하면 rw를 필요로 하는 것 같습니다. 이유는 좀 더 공부해봐야 알 것 같습니다.

옵션 설명

  • --privileged: Systemd는 여러 시스템 리소스(Cgroups, DBus 등)에 접근해야 하므로 특권 모드가 필요합니다.
  • --volume /sys/fs/cgroup:/sys/fs/cgroup:ro: Cgroups를 올바르게 인식시키기 위해 이 볼륨을 마운트합니다.
  • --cgroupns=host: 만약 cgroupv2를 사용하는 현대 OS라면 이 설정이 필요할 수 있다고 합니다. v2로 넘어올 때 private이 기본값이 되었는데, 이러면 서로 다른 namespace를 가져서 서로 못 알아본다고 하네요.

이제 컨테이너 내부에서 systemctl 명령을 실행해 보면 Systemd가 정상적으로 작동하는 것을 확인할 수 있습니다.

root@container# systemctl status

3. Core Dump 생성 및 GDB 디버깅

3.1. Core Dump 활성화

컨테이너 내부에서 core dump를 생성하려면 몇 가지 설정을 변경해야 합니다.

echo "kernel.core_pattern=core.%e.%p.%t" | sudo tee /etc/sysctl.d/99-core.conf
sysctl --system
ulimit -c unlimited

ulimit -c unlimited를 실행하면 core dump 크기 제한이 사라집니다.

3.2. Core Dump 생성 및 분석

다음과 같은 단순한 C 프로그램을 작성해 core dump를 생성할 수 있습니다.

#include <stdio.h>

int main() {
    char *ptr = NULL;
    *ptr = 'A';  // Segmentation fault 발생
    return 0;
}

컴파일 후 실행하면 core dump가 생성됩니다.

gcc -o segfault segfault.c
./segfault

이제 GDB로 core dump를 분석할 수 있습니다.

gdb ./segfault core.segfault.<PID>.<TIME>

4. strace를 이용한 시스템 호출 추적

컨테이너 내부에서 실행 중인 프로세스를 strace로 추적할 수 있습니다.

strace -p <PID>

특정 바이너리를 실행하면서 시스템 호출을 분석할 수도 있습니다.

strace -o output.log ./segfault

이렇게 하면 실행되는 모든 시스템 호출이 output.log에 기록됩니다.


5. NIC(Network Interface Controller) 접근

Docker 컨테이너에서 네트워크 인터페이스를 직접 제어하려면 --network host 옵션을 사용하거나, macvlan, ipvlan 네트워크를 설정해야 합니다.

5.1. 컨테이너에서 호스트 네트워크 사용

docker run --rm -it --network host ubuntu-systemd

이렇게 하면 컨테이너가 호스트와 동일한 네트워크 인터페이스를 사용할 수 있습니다.

5.2. 특정 NIC 인터페이스를 컨테이너에 전달

docker run --rm -it --privileged --network bridge \
  --device /dev/net/tun \
  ubuntu-systemd

이렇게 하면 NIC 인터페이스에 직접 접근할 수 있다고 합니다. 저는 ipvlan, macvlan을 사용하는 방식을 채택했습니다.


결론

이번 실험을 통해, 도커의 일반적인 설계 철학과 다르게 Systemd를 실행하고, core dump를 생성하며, GDB 디버깅과 strace 분석을 수행하고, NIC 인터페이스에 접근하는 다양한 방법을 확인해 보았습니다.

정리하자면:
1. Systemd 실행 가능: 특권 모드(--privileged)와 Cgroups 볼륨을 사용하면 컨테이너 내에서 Systemd를 실행할 수 있습니다.
2. Core Dump 및 디버깅 가능: ulimit -c unlimited 설정과 GDB를 사용하면 core dump 분석이 가능합니다.
3. strace를 통한 시스템 호출 추적 가능: 특정 프로세스를 strace로 추적하며 내부 동작을 분석할 수 있습니다.
4. NIC 인터페이스 접근 가능: --network host 또는 --device /dev/net/tun을 활용하면 네트워크 장치를 컨테이너에서 직접 사용할 수 있습니다.

이 방법들은 권장되지는 않는다고 하지만, 컨테이너 환경에서 더 깊이 있는 시스템 분석을 수행하는 데 유용할 수 있습니다. 🚀

다른 방식으로는 systemd-nspawn이나 lxd, lxc를 사용해도 좋다고 하는데, 일단 docker가 가장 간편하고 쉬워서요. ㅎㅎ podman도 결국 rootful 모드로 동작하는 듯 하고... qemu는 좀 무겁고...

종종 상황에 따라 필요할 때가 있으니까요... 더 좋은 방법을 알고 계신다면 댓글 부탁드립니다!!

0개의 댓글

관련 채용 정보