실습으로 배우는 Docker 입문 | 6. Docker Engine의 구조를 체험하자

이번에는 Docker Engine의 구조에 대해 클라이언트 서버 모델의 관점에서 설명하겠다. Docker Engine API를 호출하거나 원격으로 연결하는 등, Docker Engine의 구조를 체험적으로 이해하고 가자.

Docker Engine 개요

Docker Engine은 Docker의 핵심 소프트웨어이다. 지금까지 종종 등장했던 docker rundocker build 등의 명령과 그 명령에 의해 실행되는 이미지 빌드, 컨테이너 실행하는 등의 다양한 작업을 할 것을 정리하여 Docker Engine이라고 한다.

단순히 Docker라고 하면 이 Docker Engine을 가리키는 경우도 있고, Docker Hub와 같은 주변 서비스 및 도구를 포함한 Docker Platform을 가리키는 경우도 있다. Docker Platform처럼 넓은 의미에서 Docker과 구별 할 때 Docker Engine이라는 말은 사용된다.

공식 문서에 따르면, Docker Engine은 주로 다음의 3가지 요소로 구성되어 있다.

  1. Docker CLI
  2. Docker Engine API
  3. Docker 데몬

Docker CLIdocker rundocker build 등의 Docker 명령을 실행하는 커멘드 라인 도구이다. Docker CLI는 입력된 Docker 명령에 따라 Docker Engine API를 호출한다. Docker 데몬은 Linux 데몬 프로세스에서 Docker Engine API가 호출되는 것을 기다리고 있다가, 호출된 Docker Engine API에 따라 이미지의 빌드 및 컨테이너의 시작 등을 실행한다.

이처럼 Docker Engine은 클라이언트 서버 모델의 응용 프로그램이라 할 수 있다. 클라이언트인 Docker CLI가 Docker Engine API를 통해 서버인 Docker 데몬에 처리를 요청하고 응답을 받는다.

Docker Engine 개요

Unix 소켓 통신과 TLS 통신

위의 그림과 같이 Docker CLI가 Docker 호스트에 있는지 외부에 있는지에 따라 Docker 데몬과 통신 방식은 다르다.

Docker CLI가 Docker 호스트에 있는 경우는 Unix 도메인 소켓(이하, Unix 소켓)을 이용하여 Docker 데몬과 통한다. Unix 소켓은 동일한 시스템에 있는 프로세스끼리 통신을 할 수 있는 구조이다. 마지막은 docker-machine ssh명령을 사용하여 Docker 호스트에 로그인 한 후 Docker 호스트에 있는 Docker CLI 명령을 실행하고 있었으므로, Unix 소켓 통신을 하고 있었던 거다.

Docker CLI가 Docker 호스트의 외부에 있는 경우 TCP 소켓을 사용하여 Docker 데몬과 통신한다. TCP 소켓의 경우 HTTP를 그대로 이용하는 것이 아니라, 어떠한 보안 대책을 실시하는 것이 추천되고 있다. 그 중 하나가 TLS 이다. Docker Engine은 TLS를 사용하여 HTTP를 암호화 된 HTTPS 통신을 실시하는 것과 동시에 클라이언트와 서버를 신뢰할 수있는 것으로 한정 할 수 있다.

이런 TLS 통신을 하기 위해서는 인증서 만들어야 하는 등의 다양한 작업이 필요하지만 Docker Machine을 사용하여 Docker 호스트를 작성한 경우에는 그 작업은 자동으로 이루어진다. 여기에서도 이 Docker Machine 자동 설정을 이용하여 TLS 통신을 한다. (참고 : 자동 설정을 사용하는 이유는 쉽게 TLS 통신을 체험 할 수 있기 때문이고, 높은 안전성이 보장되기 때문은 아니다.)

사전 준비

이상으로 설명한 것에 대해 여기에서 실제로 Docker Engine을 사용해 보고 더 깊게 이해를 보도록 하자. 개요는 다음과 같다.

  • 사전 준비
  • curl에 따르면 API 호출 (Unix 소켓 통신)
  • API 호출에 의한 “Hello World”(Unix 소켓 통신)
  • Docker CLI 설치
  • Docker CLI가 원격 연결 (TLS 통신)
  • 참고 : 프록시 인증서를 다시 작성

우선 이전과 마찬가지로 사전 준비로 Windows 10 , VirtualBox , PowerShell 을 준비한다.

또한 다음과 같이 Docker Machine을 사용하여 Docker 호스트를 작성한다.

PS > docker-machine create myhost

이 create명령을 실행하면 Docker 호스트 중 Docker 버전은 최신이다. 그러나 필자가 이용한 Docker 버전은 “v17.09.0-ce"이므로, 혹시 Docker 버전이 다르기 때문에 다음에 소개하는 내용이 수행 할 수 없을지도 모른다. 이 경우 다음과 같이 create명령의 --virtualbox-boot2docker-url 옵션에 버전을 지정하여 Docker 호스트를 작성한다.

PS> $url = 'https://github.com/boot2docker/boot2docker/releases/download/v17.09.0-ce/boot2docker.iso'
PS> docker-machine create --virtualbox-boot2docker-url $url myhost

Docker 호스트가 생성되면 로그인한다.

PS> docker-machine ssh myhost

로그인하면 Docker CLI를 사용하여 “Hello World"가있는 것을 확인합시다.

docker@myhost:~$ docker run hello-world
(중간 생략)
Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

여기까지는 이전과 같다.

다시 로그를 읽어 보면, Docker CLI (Docker client)와 Docker 데몬 (Docker daemon)의 처리에 대해 차례로 설명되어 있다. 이 로그에는 적혀 있지 않지만, Docker CLI는 Docker 데몬과 상호 작용하는 Docker Engine API를 호출한다. Docker Engine API는 Docker CLI 않아도 호출 할 수 있다. 그래서 다음 curl을 사용하여 Docker Engine API를 호출하자.

curl에 따르면 API 호출 (Unix 소켓 통신)

curl은 널리 알려진 HTTP 등 다양한 통신을 할 수있는 도구이다. curl은 처음부터 Docker 호스트에 설치되어 있지만, 만약을 위해 curl 버전을 확인하자.

docker@myhost:~$ curl --version
curl 7.49.1 (x86_64-pc-linux-gnu) libcurl/7.49.1 OpenSSL/1.0.2h zlib/1.2.8
Protocols: dict file ftp ftps gopher http https imap imaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: AsynchDNS Largefile NTLM NTLM_WB SSL libz TLS-SRP UnixSockets

curl 버전 7.40.0 이상이면 된다. curl 버전 7.40.0에서 Unix 소켓을 사용할 수 있다. 다음과 같이 Unix 소켓을 사용하여 Docker Engine API를 호출하자.

docker@myhost:~$ curl --unix-socket /var/run/docker.sock http:/version
{
    "Version":  "17.09.0-ce",
    "ApiVersion":  "1.32",
    "MinAPIVersion":  "1.12",
    "GitCommit":  "afdb6d4",
    "GoVersion":  "go1.8.3",
    "Os":  "linux",
    "Arch":  "amd64",
    "KernelVersion":  "4.4.89-boot2docker",
    "BuildTime":  "2017-09-26T22:45:38.000000000+00:00"
}

--unix-socket 옵션에 지정되어 있는 /var/run/docker.sock이 Docker 소켓의 경로이다. http:/version는 Docker 버전 정보를 받아오는 API이다. 버전 정보는 JSON 형식으로 출력되고 있다. "ApiVersion": "1.32"라는 것이 API의 버전이다. 버전 1.32의 문서에는 다양한 API가 샘플과 함께 설명되어 있다. 그 내용들을 참고로 하면서, 다음은 “Hello World"를 출력해 보자.

API 호출에 의한 “Hello World”(Unix 소켓 통신)

먼저, 컨테이너를 만든다.

curl --unix-socket /var/run/docker.sock \
    -H "Content-Type: application/json" \
    -d '{"Image": "hello-world", "Tty": true}' \
    -X POST http:/containers/create
{"Id":"469a115ce858fc7ae41639dc3ec7bf354e88015cc98b2540a4c8a7598e01445e","Warnings":null}

이미지 이름이 "hello-world"이고 -d '{"Image": "hello-world", ...와 같은 식으로 JSON 형식으로 지정되어 있다. -X POST는 HTTP의 POST 메서드를 사용한다는 의미이다. http:/containers/create는 컨테이너를 생성하는 API이다.

출력 결과의 {"Id":"469a11...는 생성된 컨테이너의 ID이다. 이 ID를 이용하여 컨테이너를 시작한다.

docker@myhost:~$ curl --unix-socket /var/run/docker.sock \
    -X POST http:/containers/469a11/start

http:/containers/469a11/start이 컨테이너를 시작하는 API이다. 시작하는 컨테이너는 이러한 컨테이너 ID 앞에 숫자 문자(469a11)로 지정할 수 있다.

작성한 “hello-world"이미지 컨테이너는 시작할 때 로그를 출력하고 즉시 중지하는 컨테이너이다. 성공적으로 중지했는지 확인하자.

docker@myhost:~$ curl --unix-socket /var/run/docker.sock -X POST http:/containers/469a11/wait
{"StatusCode":0}

http:/containers/469a11/wait 컨테이너가 멈출 때까지 기다린 후 종료 코드를 StatusCode로서 돌려주는 API이다. StatusCode0이므로 컨테이너의 처리는 성공하였다.

컨테이너의 로그를 확인하자.

docker@myhost:~$ curl --unix-socket /var/run/docker.sock http:/containers/469a11/logs?stdout=1

Hello from Docker!
This message shows that your installation appears to be working correctly.
(이하 생략)

docker run 명령에 “Hello World"를 실행했을 때와 같은 로그가 출력되어 있아. 호출 API는 조금 다르지만 docker run 명령도 이와 같이 Docker Engine API를 통해 Docker 데몬과 상호 작용을 하고 있다.

Docker CLI 설치

자, 다음은 Docker CLI가 원격 연결(TLS 통신)을 수행하기 위해 Windows에 Docker CLI를 설치한다. 설치는 Docker Machine뿐만 아니라 바이너리 파일을 다운로드하여 Path 설정을 하기만 하면 된다.

Docker CLI의 Windows 64 비트 버전의 바이너리 파일은 여기에서 다운로드 할 수 있는데, 만약 표시가 연결되지 않는다면, 공식 문서를 참조해라.

Docker CLI 버전은 Docker 호스트 Docker 버전이 되도록 맞춘다. 버전이 다르면 정상적으로 작동하지 않을 가능성이 높다. 버전이 v17.09.0-ce는 파일 이름이 “docker-17.09.0-ce.zip"이다.

zip 파일을 다운로드 한 후 압축을 풀고 안에 들어있는 docker.exe 파일을 환경 변수 Path에 추가한다. PowerShell에서 Path 설정 방법에 대해서는 이전을 참고해라.

Path를 설정 한 후 다음 명령을 실행한다.

PS> docker version
Client:
 Version:      17.09.0-ce
 API version:  1.32
 Go version:   go1.8.3
 Git commit:   afdb6d4
 Built:        Tue Sep 26 22:40:09 2017
 OS/Arch:      windows/amd64
error during connect: Get https://%2F%2F.%2Fpipe%2Fdocker_engine/v1.32/version:
open //./pipe/docker_engine: The system cannot find the file specified.
In the default daemon configuration on Windows, the docker client must be run elevated to connect.
This error may also indicate that the docker daemon is not running.

출력된 내용중 상단에 `Client:에 Docker CLI 버전 정보가 출력되어 있으면 설치가 완료된거다.

하단에는 오류 메시지가 출력되고 있다. 다음 원격 연결을 설정하는 것으로, 하단에는 Docker 호스트 측의 정보가 출력되게 된다.

Docker CLI가 원격 연결 (TLS 통신)

원격 접속을하기 위해서는 대상 Docker 호스트 정보를 설정해야 한다. 다음은 설정 방법을 2가지 소개한다.

(1) docker 옵션을 설정하는 방법

PowerShell에서 다음 Docker Machine 명령을 실행하여 보자.

PS> docker-machine config myhost
--tlsverify
--tlscacert="C:\\Users\\Taro\\.docker\\machine\\machines\\myhost\\ca.pem"
--tlscert="C:\\Users\\Taro\\.docker\\machine\\machines\\myhost\\cert.pem"
--tlskey="C:\\Users\\Taro\\.docker\\machine\\machines\\myhost\\key.pem"
-H=tcp://192.168.99.100:2376

이 config명령은 원격 연결을 위한 docker 옵션을 출력하는 명령이다. --tls로 시작하는 옵션은 Docker Machine이 자동으로 생성된 인증서의 경로 등이 지정되어 있다. -H=tcp://192.168.99.100:2376는 TLS 통신을 허용 “myhost"의 IP 주소와 포트 번호이다.

이 옵션을 복사하고 다음과 같이 docker version명령에 추가하고 실행하자. (각 행의 끝에 붙어있는 “`“는 백틱(역 따옴표)에서 PowerShell에서는 긴 명령을 여러 줄로 나누어 쓰고 싶을 때 사용한다.)

PS> docker --tlsverify `
--tlscacert="C:\\Users\\kimkc\\.docker\\machine\\machines\\myhost\\ca.pem" `
--tlscert="C:\\Users\\kimkc\\.docker\\machine\\machines\\myhost\\cert.pem" `
--tlskey="C:\\Users\\kimkc\\.docker\\machine\\machines\\myhost\\key.pem" `
-H=tcp://192.168.99.100:2376 version

Client:
 Version:      17.09.0-ce
 API version:  1.32
 Go version:   go1.8.3
 Git commit:   afdb6d4
 Built:        Tue Sep 26 22:40:09 2017
 OS/Arch:      windows/amd64

Server:
 Version:      17.09.0-ce
 API version:  1.32 (minimum version 1.12)
 Go version:   go1.8.3
 Git commit:   afdb6d4
 Built:        Tue Sep 26 22:45:38 2017
 OS/Arch:      linux/amd64
 Experimental: false

원격 연결에 성공하여 “Server :“정보가 출력되었다.

그러나 docker 명령을 실행할 때마다 매번 같은 긴 옵션을 붙이는 것은 가급적 피해 싶다. 그래서 다음과 같이 환경 변수를 설정하자.

(2) 환경 변수를 설정하는 방법

원격 연결에 필요한 환경 변수는 Docker Machine과 PowerShell을 사용하여 다음과 같이 한 줄로 설정할 수 있다.

PS> docker-machine env myhost | Invoke-Expression

이 명령을 실행 한 후 계속 같은 PowerShell에서 docker version및 버튼 docker run hello-world을 실행하여 보자. 버전 정보가 출력되고 “Hello World"를 실행할 수 있어야 한다.

그러면, 이 명령은 도대체 무엇을 하고있는 것일까요? 분할하여 살펴 보자.

먼저 다음과 같이 파이프 라인 기호 " |“왼쪽의 명령을 실행한다.

PS> docker-machine env myhost
$Env:DOCKER_TLS_VERIFY = "1"
$Env:DOCKER_HOST = "tcp://192.168.99.100:2376"
$Env:DOCKER_CERT_PATH = "C:\Users\kimkc\.docker\machine\machines\myhost"
$Env:DOCKER_MACHINE_NAME = "myhost"
$Env:COMPOSE_CONVERT_WINDOWS_PATHS = "true"
# Run this command to configure your shell:
# & "C:\Users\kimkc\Scripts\docker-machine.exe" env myhost | Invoke-Expression 

$Env:문자열이 몇 줄 출력되어 있다. $Env:는 이전에 언급 된 바와 PowerShell에서 환경 변수이다. 변수 이름과 설정 값에서 알 수 있듯이 이들은 TLS 통신에 대한 환경 변수이다. 이렇게 Docker Machine의 env명령은 원격 연결하려는 Docker 호스트에 대한 환경 변수를 문자열로 출력해 준다.

단, env명령은 단순히 문자열을 출력 해주는 것에 지나지 않는다. 출력된 문자열을 PowerShell 명령으로 실행하지 않으면 환경 변수는 설정되지 않는다.

문자열을 실행 해주는 것이 PowerShell의 Invoke-Expression 명령(약어 iex)이다. 즉, docker-machine env myhost | Invoke-Expression이 Docker Machine의 env명령에서 출력된 문자열을 PowerShell의 Invoke-Expression 명령으로 실행하여 원격 연결에 필요한 환경 변수를 설정하고 있었던 것이다.

이처럼 Docker CLI는 환경 변수에 의해 연결을 전환 할 수 있다. 이번에 작성한 “myhost"이외에도 여러 Docker 호스트를 만들어 시험해 보면 좋을 것이다.

이상에서 Docker CLI가 원격 연결 (TLS 통신) 할 수 있게 되었다. 이하에서는 추가로 원격 연결 곤란할 때의 대처법을 2가지를 소개하겠다.

보충 1 : 프록시

env 명령을 사용하여 환경 변수를 설정해도 다음과 같이 오류가 발생할 수 있다.

PS> docker-machine env myhost | Invoke-Expression
PS> docker version
(중략)
error during connect: Get https://192.168.99.100:2376/v1.32/version: Service Unavailable

원인의 하나로서, 회사나 학교의 HTTP 프록시하여 Docker 호스트의 IP 주소(192.168.99.100)까지 도달할 수 없을 때 발생한다. 이 경우 다음과 같이 --no-proxy 옵션을 추가하자.

PS> docker-machine env --no-proxy myhost
$Env:DOCKER_TLS_VERIFY = "1"
$Env:DOCKER_HOST = "tcp://192.168.99.100:2376"
$Env:DOCKER_CERT_PATH = "C:\Users\kimkc\.docker\machine\machines\myhost"
$Env:DOCKER_MACHINE_NAME = "myhost"
$Env:COMPOSE_CONVERT_WINDOWS_PATHS = "true"
$Env:NO_PROXY = "192.168.99.100"
# Run this command to configure your shell:
# & "C:\Users\kimkc\Scripts\docker-machine.exe" env --no-proxy myhost | Invoke-Expression

--no-proxy 옵션을 추가하여 아래에서 세 번째 줄 $Env:NO_PROXY = "192.168.99.100"이 추가로 출력 되었다. $Env:NO_PROXY 프록시를 경유하지 않는 IP 주소를 설정하는 환경 변수이다. 이렇게 $Env:NO_PROXY에 “myhost"의 IP 주소를 지정하면 Docker CLI는 프록시를 경유하지 않고 “myhost” 원격 연결을 시도한다.

보충 2 : 인증서를 다시 작성

Docker Machine을 계속 사용하면 가끔 다음과 같은 오류 메시지가 출력될 수 있다.

PS> docker-machine env myhost | Invoke-Expression
Error checking TLS connection: Error checking and/or regenerating the certs:
There was an error validating certificates for host "192.168.99.101:2376":
x509: certificate is valid for 192.168.99.100, not 192.168.99.101
You can attempt to regenerate them using 'docker-machine regenerate-certs [name]'.
Be advised that this will trigger a Docker daemon restart which might stop running containers.

원인의 하나로서 인증서를 생성 할 때(Docker 호스트를 작성했을 때)의 IP 주소와 이번 Docker 호스트를 시작할 때 IP 주소가 다른 것을 생각할 수 있다. Docker Machine은 Docker 호스트를 시작할 때 IP 주소를 동적으로 할당하는 Docker 호스트를 여러 개 생성하고 이러한 오류가 자주 발생한다.

이 경우 오류 메시지에 표시되어 있는대로 regenerate-certs 명령으로 인증서를 다시 만들어 보다.

PS> docker-machine regenerate-certs myhost
Regenerate TLS machine certs?  Warning: this is irreversible. (y/n): y
Regenerating TLS certificates
Waiting for SSH to be available...
Detecting the provisioner...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...

이제 다시 원격 접속을 할 수있게 될 것이다.

결론 : Docker Engine과 “assemble”

이번에는 Docker Engine의 구조에 대해 설명하였다. API 호출과 원격 연결을 통해 클라이언트 서버 모델이라고도 말할 수 Docker Engine의 구조를 체험 할 수 있었던 것이 아닐까 생각된다.

최후에 최근 Docker 사의 동향과 Docker Engine의 관계에 대해 간략하게 소개하고, 정리하려고 한다.

Docker Engine이라는 명칭이 명시적으로 사용된 것은 2014년 6 월 DockerCon이 처음이라고 말해도 좋을 것이다. 이 DockerCon는 libcontainer 과 libswarm 등 현재 개발되어 있는 구성 요소의 원류가 되는 요소도 발표되고 있다. 분명히 지금까지 “Docker"로 일괄로 부른 것을, 이 무렵부터 조금씩 구성 요소로 명확하게 구분 가려고 했던 것 같다.

이번에 소개한 Docker Engine의 세 가지 구성 요소도 각각 Docker의 진화와 함께 구성 요소로 모양을 명확하게 해 온 것이다. 그중 Docker 데몬은 또한 몇 가지 구성 요소로 나누어져 있다. 예를 들어 containerd는 Docker 데몬의 컨테이너에 대한 처리를 구성 요소로 잘라낸 것이다. 현재 containerd는 Cloud Native Computing Foundation(CNCF)라는 조직의 프로젝트로 개발이 진행되고 있다.

이러한 구성 요소화의 흐름은 2017년 4월에 DockerCon 2017에서 발표된 Moby 프로젝트에 따라 새로운 단계에 들어 섰다고 말할 수 있겠다.

Moby는 말하자면 나름대로의 Docker를 만들기 위한 프레임워크이다. Moby 프로젝트의 홈페이지에는 다음과 같이 적혀 있다.

An open framework to assemble specialized container systems without reinventing the wheel.

이 글에서 주목해야 할 것은 “assemble"라는 단어이다. “assemble"은 “수집하다”, ‘‘조립하다"라는 뜻이다. Moby는 다양한 구성 요소를 모아 Docker 같은 컨테이너 시스템을 조립하는 오픈 프레임워크라는 것이다.

Docker사는 향후 다양한 컨테이너 시스템을 “assemble"하는 수단으로 Moby를 제공하고 Moby을 활용해 뛰어난 형태로"assemble"한 컨테이너 시스템의 하나로서 Docker를 제공하고 가자라고 하고 있다. Docker를 중심으로하는 컨테이너 기술은 변화가 심하고 계속해서 새로운 제품이 등장해 오는데, 그 컴포넌트인가, 컴포넌트를 “assemble"한 성과인가의 관점에서 바라 보면 이해가 되지 않을까 생각된다.