이번 Section을 공부하기 앞서 알아야 할 것이 있는데 Docker는 Docker 위에서 Docker를 사용하는 것을 권장하지 않는다는 것이다.
이게 어떤 의미냐면 Docker Container 내부에 새로운 Container를 생성하여 Container 내에서 직접 Docker 명령어를 실행시키는 것을 추천하지 않는다는 것이다.
docker의 Old Version에서는 docker client와 docker-engine이 확실하게 분리되어 있었다.
이는 Docker Server와 Docker Client가 확실하게 분리되어 있어 Client가 dockerd에 명령을 전달하는 방법을 고민하게 했으며, 이 과정에서 Container 내부에 Conatiner를 생성하고 내부나 외부 Container를 Docker Server로 사용하는 경우를 만들었다.
결국 Docker Container 내에서 직접 Docker 명령어를 실행시키게 되었으며 이를 막기 위해 최신 버전에서는 docker-engine과 docker client를 통합시켰다.
하지만 CI Tool에서는 상황이 조금 다르다.
Jenkins Agent가 Docker 관련 Task를 진행해야 하는 Container이고 Docker Daemon은 이미 Host Machine에 동작중인 상태이다.
즉, Docker Client는 Jenkins가 담당하게 되며 Docker Daemon은 이미 jenkins와는 다른 Container로써 동작하고 있는 것이다.
이를 위해 Jenkins Agent는 이미 Host Machine에서 동작중인 dockerd에 명령을 전달하는 방법을 고민해야 했으며 이를 위해 DooD나 DinD 방식으로 Docker Container 구조를 만들어 CI Tool Agent가 Docker Client 역할을 수행할 수 있도록 한 것이다.
Docker in Docker라는 말에서 어느 정도 예측 할 수 있듯이 Docker Host Machine 내부에 있는 Container에 또다시 격리된 Docker Container를 생성하는 방식을 의미한다.
Docker Container에서 Docker 바이너리를 설정하고 컨테이너 내부의 격리된 dockerd를 실행하여 명령을 처리함으로써 내부 Docker Container가 Docker Server 역할을 수행하는 것이다.
CI Tool(Jenkins)측면에서 생각해보자.
위 사진에서 Host Machine 위에 존재하는 빨간색 Docker Container(Outer Container)가 Jenkins Agent이다.
Jenkin Agent 내부에 회색 Container(Inner Container)가 존재함을 볼 수 있다.
Jenkins Agent측에서 Docker 명령을 실행시킬 경우 Inner Container의 dockerd가 요청한 Docker 명령을 실행하기 때문에 매우 손쉽게 Docker 명령을 처리할 수 있게 된다.
설명하기 앞서 예전에는 무조건 Privileged Mode를 통해 Docker Container를 생성해야 DinD를 사용할 수 있었으나 최근에는 Sysbox와 같은 Rootless Container와 도구를 활용해 DinD를 구축할 수 있게 되어 이 단점이 거의 사라졌다.
Privileged Mode를 사용하게 되면 Docker Container Host Machine이 할 수 있는 모든 작업을 수행할 수 있게 된다.
즉, Docker Container가 자신을 실행시키는 Host Machine에 대한 대부분의 권한을 가져야지 DinD 방식을 사용할 수 있게 되는 것이다. 그리고 당연히 이는 큰 보안 위험성을 초래할 수 있을 것이다.
Inner Docker는 Outer Docker와 거의 동일한 권한을 가지기 때문에 Inner Docker 측면에서도 Outer Docker에 대한 Setting을 수행할 수 있게 된다.
이 경우 Inner Docker 보안만 뚫으면 Outer Docker도 같이 뚫리기 때문에 해커 입장에서도 편하고 Inner Docker와 Outer Docker 사이에 충돌이 발생할 수도 있을 것이다.
외부 Docker는 일반 파일 시스템(EXT5, BTRFS 등)에서 실행되지만 내부 도커는 Copy-on-write System에서 운영된다는 차이가 존재한다. 그런데 일반 파일 시스템과 Copy-on-write System 조합 중 작동하지 않거나 문제가 생기는 조합이 많이 존재한다.
외부 Docker가 AUFS를 사용한다면 Copy-on-write System으로는 AUFS를 사용할 수 없다는 것이 가장 대표적인 예시일 것이다.
이런 문제는 곧 특정 파일 시스템을 사용할 수 없다는 문제로 이어지고 당연히 서버 설정 등에 대한 난이도의 증가로 이어질 것이다.
Inner Docker의 데이터를 Caching 하고 싶을 경우 Outer Docker의 var/lib/docker에 접근하여 저장하게 된다.
그런데 DinD에서는 위 사진에서 볼 수 있다시피 1개의 Outer Docker에 여러 개의 Inner Docker를 만들 수가 있다.
이 말은 무슨 의미냐, 여러 개의 Inner Docker는 단 1개의 /var/lib/docker에 접근해서 데이터를 저장해야 한다는 것이다.
즉, DinD에서는 빌드를 할 때 마다 var/lib/docker에 존재하고 있던 Cache를 모두 날려버리게 되며 이는 Data Corruption 위험성을 가진다.
DooD에는 DinD와는 반대로 "Out"이라는 단어가 들어가 있다는 것을 알면 개념 이해도 쉬울 것이다.
DooD는 Docker Container 외부 Docker를 활용하는 Architecture를 의미한다.
DinD와는 목적은 동일하나 완전히 반대 방식을 활용하는 것이다.
Docker Container가 동작하고 있다는 의미는 곧 Container를 관리하는 Host Machine이 동작하고 있다는 의미와 동일하다.
그렇다면 이미 동작중인 Host Machine을 통해 Docker Client 명령을 처리할 순 없을까?
이런 발상으로 나온 Architecture가 바로 DooD이다.
DooD에서는 위 사진에서 볼 수 있듯 내부 Container를 만드는 것이 아닌 Sibling Container, 즉 Container와 동등한 위치에 새로운 Container를 만들고 이를 Docker Server로써 활용하는 방식이다.
DooD에서는 Docker Container의 Socket과 Host Machine의 Socket이 공유된다. 즉, Docker Container는 Host Machine의 소켓을 활용한다.
우리는 이전 Section에서 Docker Client에서 Docker Server로 REST API를 통해 통신을 하는 과정에서 Unix Socket이나 TCP Socket을 활용한다고 배웠다.
이에 비추어보았을 때 소켓을 공유한다는 의미는 곧 Docker Client에서 Docker Server로 명령문을 보낼 때 원래라면 Docker Container의 소켓을 활용하여 Docker Server에 명령문을 보내야 했으나 이 상황에선 Host Machine 측에서 Docker Server에 명령문을 보낼 수 있게 된다는 것이다.
이제 CI Tool(Jenkins)에 빗대어 생각해보자.
Jenkins Agent(Docker CLI Container)는 Docker 명령어를 Container에 입력할 것이다. 그리고 Docker는 이 명령어를 Socket을 활용해 Docker Server에 보낼 것이다.
DooD를 사용하는 상황에서는 Socket이 Host Machine에 속해있으므로 사실상 Host Machine이 Docker 명령어를 Docker Server에 보내게 되는 것이다.
Host machine 위에 존재하는 Sibligng Container에 명령문을 보내고 Sibling Container의 docerkd에서 받은 명령문을 처리함으로써 Sibling Container가 Docker Server의 역할을 하는 것이다.
결국 Jenkins Agent(Docker Container) → Host Machine(부모) → 자식 Container(Sibling Container) 순서로 명령문이 전달되며 최종적으로 명령문이 전달된 Sibling Container의 dockerd에서 명령문을 처리하는 과정을 거치게 되는 것이다.
DooD는 확실히 DinD보다는 안전하지만 그렇다고 해서 취약점이 존재하지 않는 것은 아니다.
아래 3가지 취약점을 설명하겠지만, 결국은 공통적인 취약점이다.
DooD는 Host Machine을 사용하기 때문에 Host Machine이 미리 사용하고 있던 값들을 사용할 수 없다는 문제가 있다.
예를 들어 Host Machine이 8080 Port를 활용하고 있는 상태라면 Docker Client 역할을 하는 Docker CLI Container는 8080 Port를 활용하지 못하는 것이다.
하지만 이러한 문제점들은 Docker 명령문을 작성하기 전 한 번 더 사용하고 있는 Port Number나 Host Machine 경로를 확인해봄으로써 쉽게 해결할 수 있는 문제이며 큰 보안적 취약점까지는 아니다.
이러한 이유로 Docker측에서는 DooD 방법을 더 권장하고 있다.