도커 구조

도커 구조

도커 초기에는 LXC(LinuX Container)를 기반으로 구현하였습니다. 그리고 0.9 버전부터 LXC를 대신한는 libcontainer를 개발하여 사용하고 있습니다.

다음 그림은 도커 0.9 버전의 구조입니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2c21284c-197c-4393-8448-a00d37533ecc/docker-execdriver-diagram.png

출처 : https://www.docker.com/blog/docker-0-9-introducing-execution-drivers-and-libcontainer/

도커 1.11 버전부터 containerdrunC 를 컨테이너 런타임으로 사용합니다. 앞서 사용한 libcontainer 프로젝트는 OCI에 기부되었고, runc 라는 이름으로 바뀌게 됩니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5af65736-5226-47ae-9c67-af23bc193744/docker-containerd.png

dockerd

dockerd 는 컨테이너를 지속적으로 관리하는 데몬 프로세스로서, docker CLI 같은 클라이언트가 사용할 수 있는 RESTful API를 제공하고 있습니다. 흔히 명령어로 사용하는 docker 실행 파일이 docker CLI 입니다. 사용자가 입력한 docker 명령어는, 이 dockerd 로 전달되고, 실행됩니다.

dockerd는 unix, tcp, fd의 세 가지 소켓 유형을 통해, 도커 API 요청을 수신할 수 있습니다.

containerd

containerd는 이미지를 push 하고 pull 하고, 스토리지를 관리하고, 네트워크 기능을 정의할 수 있는 독립 실행형 고수준(high-level) 컨테이너 런타임입니다. runc 같은 저수준(low-level)의 컨테이너 런타임에 해당 명령을 전달하여, 컨테이너를 실행하는 등의 라이프사이클을 관리합니다.

도커에 의해 빠르게 확산되고 있던 컨테이너 환경에서, 컨테이너 런타임을 특정 벤더에 의존하지 않고, 중립적인 입장에서 컨테이너 표준에 맞게 구현하는 것을 목적으로 만들어졌습니다.

Docker Inc는 2016 년 12 월 컨테이너 런타임 부분을 독립적인 오픈 소스 프로젝트인 containerd 로 분리하여, 마이크로 소프트, Google, AWS, IBM 등과 공동으로 개발하기로 발표하였습니다.

그리고, 2017년 3월에는 CNCF (Cloud Native Computing Foundation)에 기부되었고, 이후 이를 담당해왔습니다.

도커는 1.11 이후 버전부터 containerd를 컨테이너 런타임으로 사용하고 있습니다.

containerd-shim

containerd-shimrunc를 실행하고, 컨테이너 프로세스를 제어하는 경량 데몬입니다. 컨테이너와 containerd 의 모든 통신은 containerd-shim 을 통해서 이루어 집니다.

containerd-shim 은 보통 다음과 같은 역할을 담당합니다.

  • 컨테이너의 stdout 및 stderr의 스트림을 제공해 주고 있습니다. 그래서 containerd 가 재시작 중에도 문제가 발생하지 않습니다. containerd 는 stdout 및 stderr의 스트림을 받아서 로그 파일로 저장을 할 수 있습니다.
  • runc 는 컨테이너 프로세스를 실행(fork)한 다음, 포그라운드 프로세스를 종료하여, 컨테이너 프로세스를 의도적으로 데몬화 합니다. 이렇게 되면, 컨테이너 프로세스는 호스트의 init 프로세스가 담당하게 되어서, 컨테이너의 관리가 어려워집니다. 이 문제를 해결하기 위해 shim 프로세스를 subreaper로 만들어서, 컨테이너 프로세스를 shim 프로세스가 관리하도록 합니다.

runc

OCI 런타임 스펙을 구현하고 있는 저수준 컨테이너 런타임입니다. 저수준 컨테이너 런타임이라고 부르는 이유는, 오직 실행 중인 컨테이너 관리에만 그 범위를 집중시키고 있기 때문입니다. 리눅스 커널의 네임스페이스와 cgroups 을 사용하여 격리시키는 기능을 제공합니다. 컨테이너를 생성(spawning)과 실행(running) 할 수있는 CLI로 구현되어 있습니다.

runc 는 도커 프로젝트(이전 이름은 libcontainer)에서 나와, OCI에 기부되었고, 이후 이를 담당해 왔습니다.

dockerdcontainerd 확인해 보기

도커가 설치된 호스트에서 프로세스를 조회해 보면, dockerdcontainerd 가 작동중인 것을 확인할 수 있습니다.

다음은 우분투 18.04 버전에 설치한 도커 19.03.12 버전인 경우 조회해 본 결과입니다.

dockerd 를 조회해 보았습니다.

sudo systemctl status docker.service
● docker.service - Docker Application Container Engine
   Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
   Active: active (running) since Sat 2020-07-25 06:31:07 UTC; 3h 11min ago
     Docs: <https://docs.docker.com>
 Main PID: 3375 (dockerd)
    Tasks: 8
   CGroup: /system.slice/docker.service
           └─3375 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.242010835Z" level=warning msg="Your kernel does not support swap memory lim
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.242240538Z" level=warning msg="Your kernel does not support cgroup rt perio
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.242342040Z" level=warning msg="Your kernel does not support cgroup rt runti
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.242520743Z" level=info msg="Loading containers: start."
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.430146872Z" level=info msg="Default bridge (docker0) is assigned with an IP
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.580818585Z" level=info msg="Loading containers: done."
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.630878720Z" level=info msg="Docker daemon" commit=48a66213fe graphdriver(s)
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.636328410Z" level=info msg="Daemon has completed initialization"
Jul 25 06:31:07 magi systemd[1]: Started Docker Application Container Engine.
Jul 25 06:31:07 magi dockerd[3375]: time="2020-07-25T06:31:07.677294494Z" level=info msg="API listen on /var/run/docker.sock"

containerd 를 조회해 보았습니다.

sudo systemctl status containerd.service
containerd.service - containerd container runtime
   Loaded: loaded (/lib/systemd/system/containerd.service; enabled; vendor preset: enabled)
   Active: active (running) since Sat 2020-07-25 06:31:05 UTC; 3h 11min ago
     Docs: <https://containerd.io>
 Main PID: 3118 (containerd)
    Tasks: 9
   CGroup: /system.slice/containerd.service
           └─3118 /usr/bin/containerd

Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.738189803Z" level=info msg="loading plugin "io.containerd.grpc.v1.images
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.738198104Z" level=info msg="loading plugin "io.containerd.grpc.v1.leases
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.738208904Z" level=info msg="loading plugin "io.containerd.grpc.v1.namesp
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.738217304Z" level=info msg="loading plugin "io.containerd.internal.v1.op
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.744531308Z" level=info msg="loading plugin "io.containerd.grpc.v1.snapsh
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.744556608Z" level=info msg="loading plugin "io.containerd.grpc.v1.tasks"
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.744565509Z" level=info msg="loading plugin "io.containerd.grpc.v1.versio
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.744574009Z" level=info msg="loading plugin "io.containerd.grpc.v1.intros
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.745122618Z" level=info msg=serving... address="/run/containerd/container
Jul 25 06:31:05 magi containerd[3118]: time="2020-07-25T06:31:05.745135418Z" level=info msg="containerd successfully booted in 0.165150s"

dockerdcontainerd 가 분리되어 있기 때문에, 도커 버전을 올릴 때 재시작 하여도, 컨테이너의 재시작 없이 사용할 수 있습니다.

컨테이너 실행 과정 살펴보기

pstree 명령어를 이용하여, 프로세스를 트리 모양으로 확인해 보겠습니다.

sudo pstree

실행중인 컨테이너가 없다면, 다음과 같은 결과를 확인할 수 있습니다.

systemd─┬─accounts-daemon───2*[{accounts-daemon}]
        ├─atd
        ├─containerd───8*[{containerd}]
        ├─cron
...

nginx 컨테이너를 실행해 보겠습니다.

sudo docker run -d --name nginx nginx:latest

docker ps 명령어를 실행하여, 실행중인 컨테이너를 확인할 수 있습니다.

sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
79c0a9468509        nginx:latest        "/docker-entrypoint.…"   5 seconds ago       Up 4 seconds        80/tcp              nginx

pstree 명령어를 이용하여, 프로세스를 트리 모양으로 확인해 보겠습니다.

sudo pstree

containerd의 자식 프로세스로 containerd-shim 이 생성된 것을 알 수 있습니다.

systemd─┬─accounts-daemon───2*[{accounts-daemon}]
        ├─atd
        ├─containerd─┬─containerd-shim─┬─nginx───nginx
        │            │                 └─9*[{containerd-shim}]
        │            └─8*[{containerd}]
        ├─cron
        ├─dbus-daemon

앞서 살펴본, docker run 을 이용한 nginx 컨테이너의 실행 과정을 정리하면 다음과 같습니다.

  • docker 명령어, 즉 docker CLI 를 실행하면, dockerd 로 요청을 전달합니다.
  • dockerd는 gRPC를 통해 containerd 요청을 전달합니다.
  • containerdexec를 통해, containerd-shim 을 자식으로 생성합니다.
  • containerd-shimrunc 를 이용하여, 컨테이너를 생성하고 실행합니다.
  • runc 는 컨테이너가 정상적으로 실행되면 종료됩니다.
  • containerd-shim 은 컨테이너에서 실행되는 프로세스의 부모가 됩니다.

도커에는 컨테이너 안에 프로세스를 새로 실행할 수 있는 docker exec 라는 명령어가 있습니다.

다음 명령어를 실행하여, nginx 컨테이너에 bash 쉘을 추가로 실행해 보겠습니다.

sudo docker exec -it nginx bash

nginx 컨테이너 bash 쉘이 실행되고, -it 옵션을 사용했기 때문에 터미널에서 쉘을 이용할 수 있습니다.

다른 터미널을 열어서, pstree 를 실행해 보겠습니다.

pstree
systemd─┬─accounts-daemon───2*[{accounts-daemon}]
        ├─atd
        ├─containerd─┬─containerd-shim─┬─bash
        │            │                 ├─nginx───nginx
        │            │                 └─9*[{containerd-shim}]
        │            └─8*[{containerd}]
        ├─cron
...

containerd-shim 프로세스의 자식 프로세스로 bash 프로세스가 추가된 것을 확인할 수 있습니다.

참고 문서

Kubernetes Container Timezone

쿠버네티스 컨테이너 타임존 변경하기

컨테이너를 이용하여 애플리케이션을 실행시킬 때 타임존을 변경해야 하는 경우가 생길 수 있습니다. 예를 들어, 로그를 남길 때 시간을 기록하게 되는데, 이 시간이 자신의 타임존과 다르다면 많이 불편할 것입니다. 이럴 경우 컨테이너의 타임존을 자신이 속한 타임존으로 변경해주면, 개발자는 좀더 효과적으로 로그를 확인할 수 있습니다.

이 글에서는 컨테이너의 타임존을 변경하는 방법을 이용하여, 쿠버티스 포드(POD)를 실행할때 타임존은 변경해보도록 하겠습니다

기본 타임존 (UTC +0)

먼저 busybox 컨테이너 이미지를 이용하여 POD 를 생성해 보겠습니다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: default-timezone
spec:
  containers:
  - image: busybox
    name: busybox
    args:
    - sleep
    - "100000"
EOF

생성한 default-timezone 포드가 정상적으로 작동되면, 실행중인 컨테이너의 shell에 접속해서 date 명령어를 실행해 보겠습니다.

kubectl exec -it default-timezone sh
/ # date
Tue Jun  9 11:55:56 UTC 2020

/ # strings /etc/localtime
TZif2
TZif2
UTC0

/ # exit

서울 타임존 (UTC +9)

이제 타임존을 UTC에서 한국의 서울(Asia/Seoul)로 변경해 보겠습니다.

새로운 타임존을 컨테이너에 주입하기 위하여, volumeMountsvolumes 을 사용하였습니다. volumes에서 hostPath 를 이용하여 호스트 노드에 있는 /usr/share/zoneinfo/Asia/Seoul 을 포드에 추가합니다. 그런 다음 컨테이너에서 volumeMounts 필드를 이용하여 추가한 볼륨을 컨테이너의 /etc/localtime 로 마운트합니다. 다시 말해서, 호스트 노드에 있는 /usr/share/zoneinfo/Asia/Seoul 을 컨테이너의 /etc/localtime 로 마운트하는 것입니다.

다음은 타임존을 서울로 설정한 포드 예제입니다. 예제를 실행하면, seoul-timezone 포드가 생성됩니다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name : seoul-timezone
spec:
  containers:
  - image: busybox
    name: busybox
    args:
    - sleep
    - "100000"
    volumeMounts:
    - name: tz-seoul
      mountPath: /etc/localtime
  volumes:
    - name: tz-seoul
      hostPath:
        path: /usr/share/zoneinfo/Asia/Seoul
EOF

생성한 seoul-timezone 포드가 정상적으로 작동되면, 실행중인 컨테이너의 shell에 접속해서 date 명령어를 실행해 보겠습니다.

kubectl exec -it seoul-timezone sh
/ # date
Tue Jun  9 20:58:01 KST 2020

/ # strings /etc/localtime
TZif2
5qx
TZif2
KST-9

/ # exit

타임존 환경변수 설정

대부분의 경우 앞서 설명한 방법만으로 충분하지만, 혹시 안되는 경우가 있다면 다음과 같이 TZ 라는 환경 변수에 타임존을 직접 지정해 줄 수 있습니다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name : seoul-timezone
spec:
  containers:
  - image: busybox
    name: busybox
    env:
    - name: TZ
      value: Asia/Tokyo
    args:
    - sleep
    - "100000"
    volumeMounts:
    - name: tz-seoul
      mountPath: /etc/localtime
  volumes:
    - name: tz-seoul
      hostPath:
        path: /usr/share/zoneinfo/Asia/Seoul
EOF

SpringBoot(자바) 애플리케이션에서 타임존 설정

다음은 SpringBoot 애플리케이션에 타임존 변경을 추가한 디플로이먼트 매니페스트 예제입니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: api-server
    app.kubernetes.io/component: server
    app.kubernetes.io/part-of: example
  name: api-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: api-server
      app.kubernetes.io/component: server
      app.kubernetes.io/part-of: example
  template:
    metadata:
      labels:
        app.kubernetes.io/name: api-server
        app.kubernetes.io/component: server
        app.kubernetes.io/part-of: example
    spec:
      containers:
        - env:
            - name: JAVA_TOOL_OPTIONS
              value: "-Xms1024m -Xmx2048m"
            - name: SPRING_PROFILES_ACTIVE
              value: develop
            - name: TZ
              value: Asia/Seoul
          image: kangwoo/api-server
          imagePullPolicy: Always
          name: api-server
          ports:
            - containerPort: 8080
              name: http
              protocol: TCP
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /actuator/health
              port: 8089
              scheme: HTTP
            initialDelaySeconds: 60
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          livenessProbe:
            failureThreshold: 3
            httpGet:
              path: /actuator/info
              port: 8089
              scheme: HTTP
            initialDelaySeconds: 60
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          volumeMounts:
            - name: tz-seoul
              mountPath: /etc/localtime
          resources:
            limits:
              cpu: "1"
              memory: 2Gi
            requests:
              cpu: "1"
              memory: 2Gi
      dnsPolicy: ClusterFirst
      volumes:
        - name: tz-seoul
          hostPath:
            path: /usr/share/zoneinfo/Asia/Seoul
      restartPolicy: Always
      terminationGracePeriodSeconds: 30

참고로, 자바 애플리케이션의 경우 TZ라는 환경 변수 대신에, system property로 다음과 같이 추가할 수도 있습니다.

-Duser.timezone=Asia/Seoul

docker 명령어

도커 이미지 관련 명령어

docker login [repository] : 저장소(repository)에 로그인한다. 저장소 주소를 적지 않으면 Docker Hub repository 로 로그인한다.

docker create [image] : 해당 이미지로부터 새로운 컨테이너를 생성한다.

docker pull [image] : 이미지를 저장소로부터 가져온다.

docker push [image] : 이미지를 저장소에 올린다.

docker tag [source] [target] : 원본 이미지 새로운 태그를 부여한다.

docker search [term] : 해당 단어로 저장소에 있는 이미지를 검색한다.

docker images : 로컬 시스템에 저장되어 있는 이미지 목록을 보여준다.

docker history [image] : 해당 이미지의 히스토리를 보여준다.

도커 컨테이너 관련 명령어

docker ps : 현재 실행중인 컨테이너 목록을 보여준다.

docker run [image] : 해당 이미지로 도커 컨테이너를 실행한다.

docker start [container] : 도커 컨테이너를 시작한다.

docker stop [container] : 도커 컨테이너를 중지한다. (SIGTERM -> SIGKILL)

docker stop $(docker ps -q) : 현재 작동하는 모든 도커 컨테이너를 중지한다.

docker kill [container] : 도커 컨테이너를 강제로 중지한다. (SIGKILL)

docker inspect [container] : 컨테이너의 상세 정보를 보여준다.

docker rm [container] : 중지된 도커 컨테이너를 삭제한다.

docker rm $(docker ps -a -q) : 중지된 모든 도커 컨테이너를 삭제한다.

docker exec -it [container] [command] : 대상 도커 컨테이너에 명령어를 실행한다.

기타 명령어

docker info : 도커 상세 정보를 보여준다.

docker version : 도커 버전을 보여준다.

docker stats : 현재 도커 컨테이너들의 상태로 보여준다.

Docker 로그 관리

도커(docker)는 로깅 드라이버(logging driver) 통해, 로그를 남기게 되어 있습니다. 로깅 드라이버의 기본 값을 json-file입니다. 즉, 로그를 json 형식으로 파일로 저장하게 됩니다.

아래 명령어를 실행하면, 해당 도커의 로깅 드라이버가 뭔지 알 수 있습니다.

$ docker info --format ''
json-file

json-file 로깅 드라이버를 사용하는 경우, 시간이 지날 수록 로그 파일이 쌓이기 때문에 주기적으로 파일을 삭제해줘야합니다. 주기적으로 파일을 삭제하는 방법은, 도커 데몬의 설정을 변경하거나, logrotate를 이용하는 것입니다.

도커 데몬 설정 파일 변경하기

/etc/docker/ 디렉토리에 있는 daemon.json 파일에 아래와 같은 내용을 추가해 주면 됩니다.

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3" 
  }
}

도커 재시작하면 변경 사항이 반영됩니다.

logrotate 이용하기

logrotate는 로그를 관리하기 위해 사용되는 범용툴입니다. 서버에 설치가 안되어 있다면, 설치가 필요합니다. 아래와 같이 컨테이너 로그를 정리하는 설정 파일을 추가해 주면 됩니다.

cat > /etc/logrotate.d/container << EOF
/var/lib/docker/containers/*/*.log {
    rotate 100
    copytruncate
    missingok
    notifempty
    compress
    maxsize 100M
    maxage 30
    daily
    dateext
    dateformat -%Y%m%d-%s
    create 0644 root root
}
EOF

참고 문서