Canvas 1 Layer 1

왜 굳이 도커(컨테이너)를 써야 하나요?
눈송이 서버의 한계를 넘어 컨테이너를 사용해야 하는 이유

들어가며: 왜 도커(Docker)를 써야하나요?

컨테이너는 (대충 말하자면) 애플리케이션을 환경에 구애 받지 않고 실행하는 기술입니다. 일례로 깃랩gitlab이라는 도구를 우분투에 설치하려면 깃랩 공식 문서에서는 다음과 같이 하라고 안내하고 있습니다.

sudo apt-get update
sudo apt-get install -y curl openssh-server ca-certificates
sudo apt-get install -y postfix
curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh | sudo bash
sudo EXTERNAL_URL="http://gitlab.example.com" apt-get install gitlab-ee

센트OSCentOS를 사용한다면 명령어가 조금 달라집니다.

sudo yum install -y curl policycoreutils-python openssh-server
sudo systemctl enable sshd
sudo systemctl start sshd
sudo firewall-cmd --permanent --add-service=http
sudo systemctl reload firewalld
sudo yum install postfix
sudo systemctl enable postfix
sudo systemctl start postfix
curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.rpm.sh | sudo bash
sudo EXTERNAL_URL="http://gitlab.example.com" yum install -y gitlab-ee

그러나 컨테이너 도구인 도커Docker가 설치되어 있다면 어느 환경이든 상관 없이 다음 명령어를 사용하여 깃랩을 실행할 수 있습니다. (이 명령도 간단해보이진 않겠지만, 그저 운영체제별로 존재하는 복잡한 설치 과정을 겪지 않는다는 점만 기억하시면 됩니다.)

$ docker run --detach \
    --hostname gitlab.example.com \
    --publish 443:443 --publish 80:80 --publish 22:22 \
    --name gitlab \
    --restart always \
    --volume /srv/gitlab/config:/etc/gitlab \
    --volume /srv/gitlab/logs:/var/log/gitlab \
    --volume /srv/gitlab/data:/var/opt/gitlab \
    gitlab/gitlab-ce:latest

이렇듯 편리함과 확장성을 지닌 컨테이너가 서버 세상을 변화시킨지 오랜 시간이 지났지만 여전히 컨테이너를 이야기하면 몇 가지 질문을 받곤 하는데, 그 중 저를 가장 고민하게 만든 질문은 다음과 같습니다.

도커 없이도 배포/운영하고 있는데, 우린 아무 불편을 느끼지 못합니다.
왜 도커를 써야 하죠?

저도 컨테이너를 접하고 나서 한동안 그 이점을 깨닫지 못하다가, 어느 순간 컨테이너를 좋아하게 되면서 컨테이너 정말 좋아요라는 말만 하고 있었는데, 이 글에서 그 이유를 정리해보려고 합니다. 그러면 자연스레 기존 서버 세상에 비해 도커가 나은 부분에도 어느정도 답할 수 있겠다 생각합니다. 도커 명령어 등의 ’활용법’은 이미 많이 알려졌기 때문에, 이 글에서는 서버를 코드로 구성하고 관리하는 방법으로써 도커가 지닌 장점을 이야기하려 합니다.

컨테이너 기술은 다양하지만, 가장 많이 알려지고 사용되는 도커Docker를 사용하여 설명하겠습니다.

운영하면서 만들어지는 눈송이 서버들(Snowflake Servers)

서버 운영을 오래 해 본 사람이라도, 처음 들어가는 서버에서는 마음 먹은 대로 문제를 해결하기가 어렵습니다. 이는 서버를 다루는 기술과는 별개로, 각 서버마다 운영 기록이 다르기 때문입니다. 똑같은 일을 하는 두 서버가 있다 해도, A 서버는 한 달 전에 구성했고 B 서버는 이제 막 구성했다면, 운영체제부터 컴파일러, 설치된 패키지까지 완벽하게 같기는 쉽지 않습니다. 그리고 이러한 차이점들이 장애를 일으키고 말죠. A 서버는 잘 되는데 B 서버는 왜 죽었지?와 같은 일(혹은 그 반대)이 벌어지는 겁니다. 이렇게 서로 모양이 다른 서버들이 존재하는 상황을 눈송이 서버Snowflakes Server이라고도 합니다. 모든 눈송이의 모양이 다르듯, 서버들도 서로 다른 모습이라는 말이죠.

모든 눈송이의 모양은 서로 다릅니다
모든 눈송이의 모양은 서로 다릅니다

A 서버와 B 서버에 이미지매직ImageMagick이라는 도구를 설치한다고 가정해봅시다. A 서버는 지난 달에 서버를 구성하면서 이미지매직을 설치했고, B 서버는 이제 막 구성한 상황이니 새로 이미지매직을 설치했습니다. 그리고 웹 서비스를 업데이트하여 각 서버에 배포합니다. A 서버에서 장애가 발생했습니다. 이 상황에서 장애 발생 원인은 대략 다음과 같이 추측할 수 있습니다.

장애 원인을 빨리 찾으려면 A 서버를 잘 아는 사람이 필요할텐데 마침 A 서버를 구성했던 사람이 팀을 옮겼거나 퇴사해서 옆에 없습니다. B 서버를 구성한 작업자는 A 서버가 구성되고 운영된 모든 과정을 파악하려고 애를 쓸 겁니다. 그래야 B 서버와의 차이점을 알아낼 수 있으니까요.

여기서는 두 서버 간 구성 시점이 한 달 밖에 차이나지 않지만, 막상 현업에서는 몇 년씩 차이나는 경우도 있습니다. 처음엔 각 서버 사이의 차이점이 눈송이 하나 만큼 작을지 모르지만, 서버를 오래 운영하다보면 점점 커져서 나중엔 집을 삼키는 거대한 눈덩이가 되어 있을 겁니다.

서버를 코드로 구성하고 관리하는 다양한 방법

이런 상황을 개선하고자 다양한 방식으로 서버 운영 기록을 저장해 두곤 합니다. 가장 흔하게는 서버에서 어떤 작업을 실행할 때마다 이를 사내 문서 도구에 기록해둔다거나, 여러 서버에 동시 접속해서 한꺼번에 명령을 실행하는 tmux-xpanes 같은 도구를 사용하기도 하죠.

tmux-xpanes
tmux-xpanes

하지만 문서에 적힌 대로 해봐도 잘 안 되는 경우가 생기고(그때는 맞고 지금은 틀림), 여러 서버를 한 번에 조작할 때도 특정 서버 하나만 문제를 일으키는 경우가 허다하죠. 따라서 서버의 운영 기록을 코드화하려는 다양한 시도들이 등장합니다. 베이그런트Vagrant나 셰프Chef, 퍼핏Puppet, 앤서블Ansible 등의 도구에는 모두 서버 운영 기록을 나중에도 재현하려는 의도가 담겨 있습니다. 이미지매직을 설치하는 앤서블 코드를 살펴봅시다. (내용을 이해할 필요는 없습니다.)

# imagemagick를 설치하는 Ansible 플레이북
# code by https://github.com/treetips/ansible-playbook-imagemagick/blob/master/main.yml
---
# ansible v2.0.0.2
- hosts: all
  become: no
  vars:
    - autoconf_dir_name: "autoconf-latest"
    - autoconf_archive_name: "{{ autoconf_dir_name }}.tar.gz"
    - autoconf_dl_url: "http://ftp.gnu.org/gnu/autoconf/{{ autoconf_archive_name }}"
    - imageMagick_dir_name: "ImageMagick-latest"
    - imageMagick_archive_name: "ImageMagick.tar.gz"
    - imageMagick_dl_url: "http://www.imagemagick.org/download/{{ imageMagick_archive_name }}"
  tasks:

(중략)

    - block:
      - name: downlaod imagemagick
        get_url: url={{ imageMagick_dl_url }}  dest=/usr/local/src/{{ imageMagick_archive_name }}

      - name: unarchive imagemagick archive
        shell: chdir=/usr/local/src mkdir -p {{ imageMagick_dir_name }} && tar xzvf {{ imageMagick_archive_name }} -C {{ imageMagick_dir_name }} --strip-components 1

      # see configure options http://www.imagemagick.org/script/advanced-unix-installation.php
      - name: configure imagemagick
        shell: chdir=/usr/local/src/{{ imageMagick_dir_name }} ./configure

      - name: make imagemagick
        shell: chdir=/usr/local/src/{{ imageMagick_dir_name }} make

      - name: make imagemagick
        shell: chdir=/usr/local/src/{{ imageMagick_dir_name }} make install
      when: imagemagick_archive.stat.exists == False
      become: yes

그리고 우리가 이야기할 도커가 있죠. 도커에서 사용하는 도커파일(Dockerfile)도 앞서 이야기한 서버 운영 기록을 코드화한 것입니다.

# Nginx 서버를 구성하는 도커 파일
FROM debian:stretch-slim

RUN apt-get update \
    && apt-get install -y \
    imagemagick  

이 도커 파일로 도커 이미지를 만들 수 있습니다. 도커 파일이 서버 운영 기록이라면, 도커 이미지는 운영 기록을 실행할 시점이라고 할 수 있습니다.

도커 파일 = 서버 운영 기록 코드화
도커 이미지 = 도커 파일 + 실행 시점

예를 들어 앞서 살펴본 앤서블의 플레이북으로 서버에 이미지매직을 설치한다고 합시다. 작업자가 1년 전에 이 플레이북으로 서버 A를 구성했고, 오늘 서버 B를 구성한다면, 두 서버에 대해 이미지매직이 설치된 시점은 1년의 차이가 발생합니다.

하지만 도커에서는 앞서 살펴본 도커 파일로 이미지를 만들어 두면, 서버가 구성되는 시점이 이미지를 만든 시점으로 고정됩니다. 이 이미지를 사용해 1년 전에 A 서버에 컨테이너를 배포하고, 오늘 B 서버에 컨테이너를 배포한다고 해도, 두 컨테이너 모두 이미지매직이 설치된 시점은 같습니다.

실행 시점에 상관 없이 구성 시점을 고정할 수 있는 도커
실행 시점에 상관 없이 구성 시점을 고정할 수 있는 도커

이것이 바로 도커가 여느 서버 구성 도구와 가장 다른 부분입니다. 다른 도구들은 모두 도구를 실행하는 시점에 서버의 상태가 결정되는 데 비해, 도커는 작업자가 그 시점을 미리 정해둘 수 있습니다. 덕분에 서버를 항상 똑같은 상태로 만들 수 있는 것이죠.

한편, 도커를 처음 접한 분들이 불편하게 여기는 부분 중 하나가 바로 도커 파일인데요. 명령어가 어려워서라기보다는 한 번 작성해서 이미지 빌드까지 성공하는 경우가 드물다보니, 수정해서 다시 빌드하는 과정을 반복해야 하기 때문입니다. 게다가 빌드하는 시간이 점점 길어지는데 그러다보면 마치 코드 작성 후 컴파일하는 시간처럼 느껴지기도 하죠(그 때는 틀리고 지금은 맞다?).

그런데 이 불편함을 다르게 바라보면 오히려 도커만의 장점이 될 수도 있습니다.

테스트 주도 개발의 관점에서 도커파일 바라보기

소프트웨어 작성에 도움을 주는 기법 중 하나인 테스트 주도 개발Test Driven Development은 이미 잘 아실 겁니다.

  1. TDD에서는 먼저 테스트를 작성하고,
  2. 테스트에 실패하고,
  3. 코드를 작성/수정한 후,
  4. 테스트를 성공합니다.
  5. 중복된 코드 등을 리팩터링합니다.
  6. 1번으로 돌아갑니다.
TDD 순환도
TDD 순환도

도커 파일 역시 이런 식으로 생각해볼 수 있습니다.

  1. 도커 파일을 만들고
  2. 도커 이미지 만들기에 실패하고,
  3. 도커 파일을 작성/수정한 후,
  4. 도커 이미지 만드는 데 성공합니다.
  5. 필요 없는 부분은 지우고 합칠 수 있는 명령은 합칩니다. (=효율화)
  6. 1번으로 돌아갑니다.
도커 파일 작성 순환도
도커 파일 작성 순환도

약간 이상한 소리일 수도 있지만 서버를 만들 때 미리 실패해보는 일은 대단히 중요합니다. 왜냐하면 지금 미리 겪은 실패는 약간의 기다림과 귀찮음 뿐이지만, 지금 겪지 않은 실패는 훗날 서비스 장애로 이어지기 때문입니다. 저는 서비스 장애 없이 서버를 수정할 수 있다는 도커의 장점과 TDD가 주는 안정감이 서로 맥락이 닿아 있다고 생각합니다. (둘 모두 먼저 맞는 매가 낫다는 속담이 잘 들어맞는 경우랄까요.)

이 과정을 한 번 경험해보고자, 앞서 보았던 이미지매직 설치용 도커 파일을 스스로 만들어보겠습니다. 먼저 기반 이미지를 우분투로 선택합니다.

# Dockerfile
FROM ubuntu:latest

그리고 이미지매직을 설치합니다.

# Dockerfile
FROM ubuntu:latest

RUN apt install imagemagick

이제 이미지를 빌드해봅니다.

$ docker build -t my-imagemagick .
Sending build context to Docker daemon  23.26MB
Step 1/2 : FROM ubuntu:latest
 ---> 93fd78260bd1
Step 2/2 : RUN apt install imagemagick
 ---> Running in 81d55446049c
Reading package lists...
Building dependency tree...
Reading state information...
E: Unable to locate package imagemagick
The command '/bin/sh -c apt-get install imagemagick' returned a non-zero code: 100

이미지매직 패키지를 찾지 못해서 실패했습니다. 패키지 목록을 업데이트하지 않았기 때문이죠. 이 과정을 추가합니다.

# Dockerfile
FROM ubuntu:latest

RUN apt update
RUN apt install imagemagick

다시 빌드해봅시다.

$ docker build -t my-imagemagick .
Sending build context to Docker daemon  23.26MB
Step 1/3 : FROM ubuntu:latest
 ---> 93fd78260bd1
Step 2/3 : RUN apt update
 ---> Running in b7d442841e90

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

Get:1 http://security.ubuntu.com/ubuntu bionic-security InRelease [83.2 kB]
... (중략) ...
Reading package lists...
Building dependency tree...
Reading state information...
4 packages can be upgraded. Run 'apt list --upgradable' to see them.
Removing intermediate container b7d442841e90
 ---> 5345ed18a95b
Step 3/3 : RUN apt install imagemagick
 ---> Running in f3cb19701d3f

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

Reading package lists...
Building dependency tree...
Reading state information...
The following additional packages will be installed:
  dbus fontconfig fontconfig-config fonts-dejavu-core fonts-droid-fallback
... (중략) ...
Need to get 36.3 MB of archives.
After this operation, 135 MB of additional disk space will be used.
Do you want to continue? [Y/n] Abort.
The command '/bin/sh -c apt install imagemagick' returned a non-zero code: 1

패키지를 설치할지 여부를 묻고 있는데 입력을 해주지 않았기 때문에 에러코드와 함께 셸이 종료되었습니다. 이를 해결하고자 apt install 부분에 -y 옵션을 추가합니다.

# Dockerfile
FROM ubuntu:latest

RUN apt update
RUN apt install -y imagemagick

이제 다시 빌드합니다.

$ docker build -t my-imagemagick .
... (중략) ...
Successfully tagged my-imagemagick:latest

이미지가 잘 만들어집니다. 이렇게 명령어를 추가하고 -> 실패하고 -> 수정하는 과정을 통해 도커 파일을 완성할 수 있습니다.*

* 한 가지 팁을 이야기하자면 기반 이미지로 컨테이너를 하나 실행한 다음 거기서 원하는 명령어들을 입력하고 원하는 결과가 나왔을 때 해당 명령어를 도커 파일로 옮기는 식으로 작업하면 실패 -> 수정 과정을 훨씬 더 빨리 반복할 수 있습니다.

마지막 과정에서 이미지를 만드는 데 성공했으므로, 이 이미지로 서버를 실행해봅시다.

$ docker run -d my-imagemagick
... (중략) ...

도커 이미지로 서버를 실행하면 도커 컨테이너가 만들어집니다. 앞서 이야기했지만, 컨테이너를 언제 실행하든 이미지가 변하지 않았다면 컨테이너의 내용도 완전히 똑같습니다. 드디어 배포(혹은 실행)하는 시점과 상관 없이 서버의 내용을 똑같게 만들려는 우리의 노력이 성공한 듯 합니다.

클래스와 인스턴스처럼 도커 이미지 바라보기

도커 이미지로는 언제든 똑같은 형태의 서버를 실행(=도커 컨테이너)할 수 있습니다. 그런데 코드나 도커 파일을 전혀 수정하지 않은 채 내일 도커 이미지를 빌드하면 어떨까요? 서버가 똑같기를 기대하겠지만 실제로는 달라질 수도 있습니다. 왜냐하면 서버에 설치하는 패키지가 보안 문제를 겪어서 하루 사이에 패치되었거나 할 수 있기 때문입니다. 그러면 도커를 사용하는 장점이 퇴색되는 걸까요?

지금까지 서버를 똑같이 만드는 데 노력을 기울였지만, 사실 서버에는 바뀌어야 할 부분도 있습니다. 일례로 한 컴퓨터에서 A라는 도커 컨테이너를 두 개 배포했다면, 이 둘을 어떻게 구분할까요? 도커에서는 내부 규칙에 따라 해시 값(=컨테이너 id)과 임의의 이름(=컨테이너 이름)을 붙입니다. 물론 IP도 다르고요. 도커에서는 이렇게 바뀌어야 할 부분을 환경변수에 넣고 관리하도록 권합니다. 이러한 도커 이미지의 특징은, 소프트웨어 분야의 클래스와 public 변수, private 변수에 비견할 수 있습니다.

소프트웨어와 비교해 본 도커 이미지
소프트웨어와 비교해 본 도커 이미지

도커 파일에 실행 시점을 더한 것이 도커 이미지라면, 도커 이미지에 실행 시점에 수정되어야 할 정보들을 더한 것이 도커 컨테이너입니다.

도커 파일 == 서버 운영 기록
도커 이미지 == 도커 파일 + 실행 시점
도커 컨테이너 == 도커 이미지 + 환경 변수

소프트웨어에서 숨길 부분은 숨기고 드러낼 부분은 드러낸 덕에 견고함과 유연성을 모두 얻을 수 있었듯, 이제 서버도 그렇게 사용할 수 있습니다. 방금 일부러 서버를 사용한다고 표현했습니다. 도커(정확히는 컨테이너 기술) 덕분에 서버를 설치하고 운영 기록을 별도로 관리하는 고단함 없이, 잘 만들어진 서버를 사용할 수 있습니다. 심지어 내가 만든 서버가 아니어도 말이죠. (도커 허브Docker Hub에 가면 다른 사람들이 만들어 둔 도커 이미지를 찾아볼 수 있습니다.)

결국 이 지점에서 서버와 소프트웨어는 크게 다르지 않습니다. 설치하기가 그렇게 까다롭던 레드마인Redmine이나 깃랩GitLab 등도 이미 도커 이미지로 관리되고 있으므로 손쉽게 사용할 수 있습니다. 그리고 소프트웨어에서 한 클래스로 만들 수 있는 인스턴스 갯수에 제한이 없듯, 한 도커 이미지로 생성할 수 있는 컨테이너 갯수에도 제한이 없습니다.

서버 코드화의 장점

지금까지 살펴본 내용이 모두 서버 코드화가 가져다 준 장점입니다.

  1. 서버 제작 과정에 견고함과 유연성을 더할 뿐 아니라
  2. 다른 이가 만든 서버를 소프트웨어 사용하듯 가져다 쓸 수 있고
  3. 여러 대에 배포할 수 있는 확장성

이런 점들 덕에 서버 지식이 부족한 저같은 백엔드 개발자도 큰 힘을 들이지 않고 서버를 운영할 수 있습니다.

마치며: 왜 도커를 써야 하나요? (반복)

이제 서두에서 언급한 질문에 답을 해볼까 합니다.

도커 없이도 배포/운영하고 있는데, 우린 아무 불편을 느끼지 못합니다.
왜 도커를 써야 하죠?

서버 배포와 운영에 도커를 꼭 써야만 하는 건 아닙니다. 하지만 지금 상황에 너무 익숙해져서 문제라고 느끼지 않는 문제는 없을까요? 수평적 확장이 자유롭나요? 서버의 견고함을 보장하면서도 동적으로 바꿀 수 있는 유연함이 존재하나요? 퇴사를 하거나 부서를 옮겨야 해서 다른 이에게 서버 운영 기록을 인계하려면 시간이 얼마나 걸릴까요?

도커가 아니어도 이미 다른 방식으로 문제를 해결할 수 있겠지만, 저에게는 아직까지 도커가 가장 편리한 해결 방법이었습니다. 고마워요 도커, 고마워요 컨테이너 세상.