본문 바로가기

잡다/docker

[오늘의삽질] 도커와 커널: 아주 구 버전의 이미지는 호환되지 않을 수 있다

ubuntu 10.04를 docker 환경에서 돌리려는 시도에 실패하다.

 리눅스에 존재했던 Set-UID와 관련된 보안 약점을 실습하는 과제를 하고 있었다. 그런데 최근에 사용되고 있는 wsl2 환경의 ubuntu 환경의 경우 해당 약점에 대한 대응 패치가 이미 진행되었으므로, 상당히 오래된 버전의 리눅스가 필요했다.

 이때 현재 컴퓨터에 오래된 리눅스 버전을 설치하는 것은 거부감이 들었기 때문에, docker을 이용하여 격리된 환경에서 구 버전의 운영체제를 이용하여 과제를 진행하는 것을 목표로 docker hub에서 여러 이미지를 찾았다.

 docker hub 상의 ubuntu 버전 중 가장 오래된 것은 ubuntu 10.04 LTS 버전이었다. 우분투의 버전은 해당 버전이 등장한 년도와 달을 의미한다는 것을 고려하면 대략 출시한지 13년 지난 운영체제라고 볼 수 있으며, 실습을 수행할 수 있을 만큼 충분히 오래되었다고 판단되어 pull 한 후 컨테이너를 생성해보기로 했다.

이미지 주소: https://hub.docker.com/_/ubuntu/tags?page=1&ordering=-last_updated&name=10.04 

docker pull ubuntu:10.04
docker run -it --name my_ubuntu ubuntu:10.04 /bin/sh

위 명령의 각 라인은 다음과 같은 의미를 가지고 있다.

  1. docker pull ubuntu:10.04: docker hub로부터 ubuntu 이미지의 10.04 태그 버전을 pull한다.
  2. docker run -it --name my_ubuntu ubuntu:10.04 /bin/sh
    : ubuntu:10.04 이미지를 my_ubuntu라는 이름으로 만들되, 상호작용이 가능하도록 텍스트 출력(t), 상호작용 플래그(i)를 세운다. 기본 명령은 /bin/sh로 오버라이딩한다. 참고로 sh (쉘)을 켜지 않으면 운영체제가 바로 꺼진다.

도커가 설치된 환경에서 위와 같은 명령을 입력하면 엄청 간단하게 생긴 쉘 화면으로 진입한다.

쉘 화면에 진입하여 여러가지 명령을 입력한 모습

 그런데 참 이상하게도 권한이 필요할만한 기능은 죄다 segmentation fault가 발생한다. 이런 현상이 13년 전에도 발생했을까? 그건 명백히 아닐 것이다. 과거에도 사람들은 su, sudo와 같은 명령을 잘만 사용했기 때문이다.

실패하는 원인 분석

 구체적인 원인을 찾기 위해 탐색한 결과, 다음과 같은 글들을 발견했다.

 위 글들의 요지는 docker에서 동작하는 컨테이너들은 호스트 커널의 기능을 사용한다는 것이다. 이에 대해 설명하기 전에 가상 머신과 docker 컨테이너 사이의 차이에 대해 알고 가자.

 가상 머신과 docker라는 기술 둘 다 기존 환경과 새로운 환경을 격리하기는 하지만 둘은 동작 방식이 다르다. 가상 머신의 경우 호스트 운영체제 상에서 각각의 하드웨어에 대응되는 부품들을 가상으로 만들고 해당 부품들 위에 별개의 운영체제를 올려 동작시킨다. 따라서 호스트의 운영체제와 가상 머신 상에서 동작하는 운영체제는 서로 다른 커널 상에서 동작하는 셈이 된다. 가상 머신 상의 운영체제는 자신에게 맞는 가상의 하드웨어를 이용하기 때문이다.

https://www.citrix.com/solutions/vdi-and-daas/what-is-a-virtual-machine.html

 반면 docker의 컨테이너는 프로세스에 대한 자원 격리를 통해 기존 환경과의 격리를 수행한다. 리눅스에는 namespaces와 cgroup이라는 기능이 존재한다.

  • namespaces: 프로세스의 리소스를 추상화하여 실행할 수 있도록 도와주는 커널 기능으로, 동일 네임스페이스에 있는 프로세스들끼리는 자원을 공유할 수 있으나 다른 네임스페이스 상에 존재하는 경우 서로 격리되어 보이지 않는다. 즉, 네임스페이스 단위로 커널 리소스를 격리하는 기능이다.
  • cgroups: 프로세스의 자원 사용을 제한하고 격리하는 커널 기능으로, 동일 그룹으로 묶어 설정된 수치만큼 하드웨어를 사용할 수 있게 만들 수 있다.

 namespaces는 프로세스, 컨테이너 및 파일 시스템 등 추상적인 격리를, cgroups는 리소스 사용량의 격리를 수행한다. docker은 이러한 커널 기능들을 활용하여 사용자가 좀 더 쉽게 실행 환경을 격리할 수 있도록 도와주는 프로그램이므로 설령 컨테이너 단위로 격리된다고 해도 각 컨테이너들은 루트의 커널 환경을 사용한다. 이러한 동작을 고려하면 사실 docker의 컨테이너들은 사실 가상화된다기보다는 격리된 프로세스에 가깝다.


 도커와 ubuntu10.04 사이의 커널 차이

그래서 결론이 뭐냐 물어본다면, 현재 문제가 발생하는 원인은 docker와 ubuntu 10.04 버전의 커널 차이에 의해 발생하는 것일 가능성이 매우 높다는 것이다. docker은 3.10 이상의 권한을 요구하는 반면 ubuntu 10.04 버전은 2.6버전의 커널을 이용한다.

 커널 차이로 인해 문제가 발생했을 가능성이 높다고 생각한 이유는 ubuntu 12.04 버전의 컨테이너를 동작한 이후부터다. ubuntu 12.04는 3.2 버전의 커널에 기반하고 있으므로 docker의 권장사항과 일치한다. 실제로 docker hub에서 해당 버전을 설치하여 passwd, sudo처럼 ubuntu 10.04 버전에서 segmentation fault을 발생시키던 명령들을 실행해 보았다.

정상적으로 명령이 수행되는 모습


결론

 흔히들 도커가 호환성이 좋다고 말한다. 실제로 나도 팀 프로젝트를 진행할 때 로컬과 EC2 환경 사이의 설정 차이를 고려하여 도커를 이용한 적이 있다. 실제로 도커를 사용하는 프로젝트는 호환성이 좋다. 그러나 이러한 호환성은 각 컨테이너가 별개의 커널을 지원하면서 발생하는 것이 아니라 루트의 커널 수준에서 프로세스를 격리하기 때문에 실현되는 것이다. 따라서 도커가 공식적으로 지원하는 커널보다 낮은 버전을 사용하는 구식 기능을 사용하는 경우에는 해당 동작이 동작하지 않을 수도 있다는 사실을 알아두자.