English Version

피플펀드는 (당연히) 서버를 운영하고 있고 이 서버는 배포된지 1년이 넘었습니다. 그 중 몇몇 서비스는 로컬에서 테스트 환경을 쉽게 구축할 수 있고 문서화가 되어있지만, 모든 서비스가 잘 문서화 되어있지는 않았습니다. 개발 부서가 점점 커지고 계속 (필자를 포함해) 새 인력이 증가함에 따라, 더 이상 로컬에 서버 테스트 환경을 구축하는 방법에 대한 문서화를 미룰 수가 없었고 이 기회에 Docker를 사용해 보기로 했습니다.

What is Docker?

Docker는 세계 최고의 소프트웨어 컨테이너 플랫폼입니다(what-is-docker). 컨테이너는 소프트웨어가 실행되는 데 필요한 모든 부품들을 포함한 하나의 객체이며, 각각의 컨테이너들은 서로 격리된 환경에서 작동합니다. 따라서 컨테이너화 된 소프트웨어는 구동되는 시스템 환경에 관계 없이 모두 동일한 모습으로 실행됩니다.

alt text Image Source

이렇게 보면 가상 머신 (VM)과 비슷하게 보이지만 VM과 달리 Docker는 하드웨어가 아니라 운영 시스템 (OS)을 가상화합니다. 따라서 컨테이너는 OS 커널을 다른 컨테이너와 공유 할 수 있으며 각 컨테이너는 사용자 공간에서 격리 된 프로세스로 실행됩니다. 따라서 컨테이너는 (용량과 구동시간 측면에서 볼 때) VM보다 더 포터블하고 효율적입니다.

alt text Image Source

하지만 그렇다고 컨테이너가 VM보다 무조건 좋은 것은 아닙니다 (containers-vs-virtual-machines). 첫째로 컨테이너는 보안상 더 취약할 수 있습니다. 만약 사용자가 컨테이너내에서 superuser 권한을 가지고 있으면 이론적으로 그 사용자는 컨테이너를 아래에 있는 기본 OS에도 superuser의 권한을 갖고 문제를 일으킬 수 있습니다. 따라서 컨테이너 역시 다른 서버 응용 프로그램들과 동일한 방식으로 다루는 것이 좋습니다 (가능하다면 서비스를 root권한 없이 실행). 또한 컨테이너는 응용 프로그램 구동 환경을 열어볼 필요가 없는 금고로 만들기 때문에 개발자가 응용 프로그램을 쉽게 개발할 수 있게 되지만, 반대로 개발자는 금고의 내용을 더욱 이해하기 어려워집니다. 즉, 앱을 쉽게 배포 할 수는 있지만 앱이 엉뚱한 컨테이너 위에 배포될 가능성이 늘어납니다 (예를 들어 nginx 이미지를 이용해 컨테이너를 만들어 앱을 올렸지만, 이 컨테이너가 올바른 버전의 TCP Load Balancing이 포함되어 있는지 모르고 사용할 수 있음). 마지막으로 배포할 시스템을 더 작은 (작동 가능한) 부분들로 나누는 것은 현명한 방법이지만, 반대로 그만큼 관리해야하는 부분들이 더 늘어나게 됩니다.

위와 같은 이유들과 함게 Docker가 VM을 지원하므로 (애초에 Docker 자체가 xhyve나 Hyper-V 그리고 예전 버전은 virtual box를 이용해 VM 위에서 구동 됨) 컨테이너와 VM은 공존할 수 있으며, 상황에 따라 컨테이너와 VM을 적절히 사용하는 것이 좋습니다. 언제 어떤 방식을 사용할지 잘 모르겠으면, 같은 응용 프로그램을 여러개 복사해서 실행해야 하는 시스템에서는 컨테이너를, 단일 시스템에서 다른 여러 응용 프로그램들을 실행해야 하는 상황에선 VM을 사용하는 것이 좋습니다.

alt text Image Source

Docker는 새로운 기술을 제시하지는 않았지만 사용자의 사용성을 위해 기존 기술들을 잘 결합한 케이스입니다. 예를 들어 Docker는 UnionFS(Union Files System)를 사용하여 Docker의 레이어 시스템을 만들었습니다. UnionFS는 폴더와 파일들을 “branch”로 그룹화 시키고 각 branch들은 다른 branch 위에 stack으로 쌓일 수 있게 합니다. 같은 경로를 갖고 있는 폴더들은 서로 병합되며 이렇게 stack된 branch들은 하나의 파일 시스템을 사용하는 것처럼 작동하게 됩니다 (병합되는 과정에서 같은 이름의 파일은 미리 정의된 정책에 따라 겹쳐 쓰여집니다). 이 UnionFS통해 Docker는 단일 시스템 이미지를 여러 (읽기 전용) 이미지들의 레이어로 이루었습니다. 이렇게 단일 시스템 이미지를 여러개의 중간 단계의 이미지들을 통해 생성함으로써 Docker는 이미지의 version control을 가능하게 해주고 (각 이미지들은 이름과 tag를 통해 version을 명명할 수 있음) 또한 이미지들을 재사용 할 수 있게 합니다 (각 중간 단계의 이미지들 역시 하나의 단일 시스템 이미지이며 이들을 이용하여 컨테이너를 생성하는 것이 가능함).

alt text Image from Subicura’s Blog

또한 unionFS 덕분에 Docker는 용량도 효율적으로 운영할 수 있게 되었습니다. 컨테이너가 이미지에서 만들어지면, Docker는 기반이 되는 이미지 위에 (쓰기 가능한) 얇은 레이어를 생성합니다 (단 이 레이어는 컨테이너가 없어지면 같이 삭제됨). 이 레이어 시스템 덕분에 컨테이너에서의 변경 사항은 기반 이미지에 영향을 주지 않아 이 이미지를 다른 곳에서 다시 사용할 수 있게 해줍니다. 따라서 여러 컨테이너들이 하나의 기본 이미지를 공유함으로써 용량을 효율적으로 사용할 수 있습니다.

alt text Image Source

이미지들은 Docker hub에 무료로 저장할 수 있으며 (공개 이미지는 무한정으로 저장할 수 있으나 비공개 이미지는 하나만 무료로 저장 가능), Docker hub에는 이미 많은 오픈 소스 소프트웨어들이 공식 개발자들에 의해 관리되어 올라가 있습니다.

How to Use Docker?

Docker는 Mac, Windows, Linux, AWS 및 Azure를 비롯한 여러 플랫폼을 지원합니다. 각 플랫폼에 맞춰 Docker를 호스트 OS에 설치하면 Docker를 바로 사용할 수 있게됩니다. 이렇게 설치된 Docker는 일반 응용 프로그램처럼 보이지만 실제로는 VM 위에서 구동됩니다. 하지만 사용자의 편의성을 위해 일반 응용 프로그램을 사용하는 것처럼 느껴지게 설계되었습니다 (실제로는 각 command를 원격 VM 시스템에 전송해서 처리함). Docker의 모든 작업은 command line interface를 기반으로합니다. 가능한 모든 command는 “–help”옵션을 사용하면 출력됩니다.

docker --help

이미지에서 컨테이너를 생성할때는 아래의 command를 사용하게 되며,

docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]

모든 옵션들에 대한 정보는 “–help”나 reference page (docker-run)를 통해 찾을 수 있습니다. 예를 들어 Ubuntu 이미지를 기반으로 컨테이너를 실행하면

docker run -it ubuntu

Ubuntu (Docker hub에 있는 최신버전으로) 컨테이너가 생성되고 shell이 시작됩니다 (만약 “-it” 옵션을 넣지 않으면 shell이 상호작용할 대상이 없기 때문에 바로 종료되고 컨테이너 역시 종료됨).

Building a Docker image

이렇게 생성된 shell을 통해 필요 패키지들을 설치하는 등 일반적인 원격 Ubuntu 컴퓨터에 연결한 것 처럼 사용할 수 있습니다. 이렇게 패키지를 설치하거나 설정을 수정한 후에는 컨테이너를 commit함으로써 새로운 이미지를 생성할 수 있습니다.

docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

이처럼 이미지는 컨테이너를 수정하고 commit해서 만들 수도 있지만 시스템의 문서화 측면에서 보면 컨테이너를 통해 이미지를 만드는 것은 좋지 않을 수 있습니다 (Docker hub를 이용하지 않을 시에는 이미지가 기가 바이트 단위의 크기를 갖을 수 있기 때문에 다른 사람들과 공유하는 측면에서 볼 때도 별로 좋은 방식이 아님). 따라서 Dockerfile을 이용하여 이미지를 만드는 것이 권장합니다. Dockerfile은 기반 이미지에서 새로운 이미지를 빌드하기 위해 필요한 일련의 명령들을 포함하고 있는 파일입니다.

아래는 Django 이미지를 빌드하기위한 Dockerfie의 예시입니다.

# Start from the Ubuntu 14.04 image
FROM ubuntu:14.04

# Image labels
LABEL com.example.vendor="Peoplefund"
LABEL version="1"
LABEL description="My custom docker image"
LABEL maintainer="Peoplefund"

# To remove debconf build warnings
# ARG DEBIAN_FRONTEND=noninteractive

# Change locale to fix encoding error on mail-parser install
# Install needed default locale
RUN echo 'en_US.UTF-8 UTF-8' >> /var/lib/locales/supported.d/local \
    && echo 'ko_KR.UTF-8 UTF-8' >> /var/lib/locales/supported.d/local \
    && locale-gen
# Set default locale for the environment
ENV LC_ALL en_US.UTF-8
ENV LANG ko_KR.UTF-8

# Change the timezone
RUN mv /etc/localtime /etc/localtime.old \
    && ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime

# Install required packages and clean up the apt cache
RUN apt-get update \
    && apt-get install --no-install-suggests -y \
    sudo \
    python3-pip \
    libmysqlclient-dev \
    libssl-dev \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean

# Install requirements
RUN mkdir /temp
COPY ["requirements/develop.txt", "requirements/production.txt", "/temp/"]
RUN pip3 install -r /temp/develop.txt
RUN rm -rf /temp

# Set environment variable
RUN mkdir /src_code
ENV SRC_HOME /src_code

# Copy a startup script
COPY ["docker_inapi_init.sh", "/"]

# Add non root user
RUN adduser --disabled-password --gecos '' ubuntu
# Allows user to sudo
RUN echo "ubuntu ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ubuntu \
    && chmod 0440 /etc/sudoers.d/ubuntu

# Set a working directory (every code below this will have a root directory as SRC_HOME)
WORKDIR $SRC_HOME
# Work with a non-root user for security
USER ubuntu
# Use a shell script as an initial process
ENTRYPOINT ["/docker_inapi_init.sh"]
# Below command is run if no command is given on a "docker run" else it is over-written
CMD ["runserver", "0.0.0.0:8000"]

위의 Dockerfile에서 볼 수 있듯이 Dockerfile은 설정값들과 설치된 패키지들이 명확하게 문서화되어 있습니다. 이렇게 Dockerfile을 만든 후 “docker build” command를 실행하면 Dockerfile에서 이미지가 생성됩니다.

docker build [OPTIONS] PATH | URL | -

Running multiple containers at once

지금까지 이미지를 만들고 이미지에서 컨테이너를 실행하는 방법을 알아봤습니다. 하지만 서버가 여러 응용 프로그램으로 구성되어 있는 경우 (예: 서버가 여러 SQL 프로그램으로 구성된 경우) 수동으로 각 응용 프로그램 컨테이너를 생성하고 실행하고 연결하는 것은 매우 귀찮은 일입니다. 이런 문제를 해결해주기 위해 Docker는 docker-compose라는 command를 제공합니다. docker-compose.yml이란 파일을 생성하고 “docker-compose up” command를 실행하면 모든 필요 작업들이 docker-compose.yml 파일에 따라 자동으로 수행됩니다.

아래는 docker-compose 파일의 예시입니다.

version: '3'

services:
    # create a nginx container
    nginx:
        # create a container from an image
        image: nginx
        # forward localhost:8080 to a container's port 80
        ports:
            - "8080:80"
        # share a host folder with a container
        volumes:
            - ./src_code/:/www/src_code/
        # a network to connect this service to
        networks:
            - nginx-php-network

    php-fpm:
        # build an image from a Dockerfile in php folder
        build:
            context: ./php
        # share a host folder with a container
        volumes:
            - ./src_code/:/www/src_code/
        # a network to connect this service to
        networks:
            - nginx-php-network

networks:
    # create a new network for this services
    nginx-php-network:
        driver: bridge

Docker 이용하여 서버를 구성하게 되면 각 컨테이너에게 매번 다른 ip가 할당될 수 있습니다. 이 문제를 해결하기 위해 Docker는 IP 주소 대신 서비스 이름을 대신 사용하라고 권장합니다. 예를 들어 위의 docker-compose 파일을 사용하는 경우 nginx 설정 파일에 “php-fpm:9000”을 통해 nginx 컨테이너가 php-fpm 컨테이너와 통신할 수 있게 만들 수 있습니다.

Conclusion

Docker는 쉽게 완전히 동일한 로컬 테스트 환경을 세팅할 수 있게 해줍니다. 따라서 개발자가 원한다면 테스트 시스템을 쉽게 뒤엎을 수 있습니다. 뿐만 아니라, 자동으로 테스트 환경 시스템을 문서화하므로 개발자는 테스트 환경의 구성 요소를 쉽게 확인할 수 있습니다. 또한 Docker는 개발자가 실제로 운영할 서비스를 쉽게 배포하고 관리 할 수 ​​있게 도와줍니다. 비록 현재 저희는 로컬 테스트 환경을 위해서만 Docker를 사용하고 있지만 가까운 미래에는 Docker를 이용하여 실제 Peoplefund 서비스를 배포했으면 하는 바램이 있습니다.