본 글은 시작하세요! 도커/쿠버네티스(용찬호 저, 위키북스)를 기반으로 정리한 글입니다.

도커는 컨테이너 기반의 오픈소스 가상화 플랫폼이다.
그렇다면 기존에 쓰이던 가상화 방법인 가상 머신과는 어떤 차이가 있을까?
- 가상 머신

기존의 가상화 기술은 하이퍼바이저를 이용해 여러 개의 운영체제를 하나의 호스트에서 생성해 사용하는 방식이었다.
이렇게 하이퍼바이저에 의해 생성되고 관리되는 운영체제를 Guest OS라고 하며, 각 Guest OS는 다른 Guest OS와는 완전히 독립된 공간과 시스템 자원을 할당받아 사용한다.
그러나 각종 시스템 자원을 가상화하고 독립된 공간을 생성하는 작업은 하이퍼바이저를 반드시 거치기 때문에 일반 호스트에 비해 성능의 손실이 발생한다. 그뿐만 아니라 가상 머신은 Guest OS를 사용하기 위한 라이브러리, 커널 등을 전부 포함하기 때문에 가상 머신을 배포하기 위한 이미지로 만들었을 때 이미지의 크기 또한 커진다.
즉, 가상 머신은 완벽한 운영체제를 생성할 수 있다는 장점이 있지만 일반 호스트에 비해 성능 손실이 있으며, 크기가 매우 큰 가상 머신 이미지를 애플리케이션으로 배포하기는 부담스럽다는 단점이 있다.
- 도커 컨테이너

도커 컨테이너는 가상화된 공간을 생성하기 위해 리눅스의 자체 기능인 chroot, namespace, cgroup을 사용함으로써 프로세스 단위의 격리 환경을 만들기 때문에 성능 손실이 거의 없다.
그리고 컨테이너에 필요한 커널은 호스트의 커널을 공유해서 사용하고, 컨테이너 안에는 애플리케이션을 구동하는 데 필요한 라이브러리와 실행 파일만 존재하기 때문에 컨테이너를 이미지로 만들었을 때 이미지의 크기가 가상 머신보다 훨씬 작다.
이로 인해 컨테이너를 이미지로 만들어 배포하는 시간이 가상 머신에 비해 빠르고, 가상화된 공간을 사용할 때의 성능 손실도 거의 없다.
도커 이미지와 컨테이너
이번에는 도커의 핵심이라고 할 수 있는 이미지와 컨테이너에 대해서 알아보도록 하자.
- 도커 이미지
도커 이미지는 컨테이너를 생성할 때 필요한 요소로, 여러 개의 계층으로 된 바이너리 파일로 존재하고, 컨테이너를 생성하고 실행할 때 읽기 전용으로 사용된다.
도커에서 사용하는 이미지의 이름은 기본적으로 [저장소 이름]/[이미지 이름]:[태그]의 형태로 구성된다.
- 도커 컨테이너
앞서 설명한 도커 이미지는 우분투, CentOS, MySQL, Nginx, 하둡 등 매우 다양한 종류를 가지고 있다. 이러한 이미지로 컨테이너를 생성하면 해당 이미지의 목적에 맞는 파일이 들어 있는 파일 시스템과 격리된 시스템 자원 및 네트워크를 사용할 수 있는 독립된 공간이 생성되는데, 이것이 바로 도커 컨테이너이다.

컨테이너는 이미지를 읽기 전용으로 사용하고, 이미지에서 변경된 사항만 컨테이너 계층에 저장하므로 컨테이너에서 무엇을 하든 원래 이미지는 영향을 받지 않는다.
이로써 도커 이미지와 도커 컨테이너에 대해 간단하게 알아보았는데, 조금 더 들어가보도록 하자.

도커 이미지는 도커 허브(Docker Hub) 라는 중앙 이미지 저장소에서 내려받을 수 있다. 그렇다면 도커 허브에서 이미지를 내려받아 컨테이너를 생성해보도록 하자.
다음은 ubuntu:14.04 이미지를 내려 받아 컨테이너를 생성하는 2가지 예시이다.
# docker run -ti --name test_container ubuntu:14.04
# docker create -ti --name test_container2 ubuntu:14.04
-ti( -t 옵션과 -i 옵션)는 컨테이너와 상호 입출력을 가능하게 하는 옵션으로, 해당 옵션과 함께 run을 실행하면 표준 입출력이 활성화된, 상호작용이 가능한 쉘 환경을 사용할 수 있게 된다.
이와 반대되는 옵션으로는 -d 옵션이 있는데, 해당 옵션은 Detached 모드로 컨테이너를 실행한다. Detached 모드는 컨테이너를 백그라운드에서 동작하는 애플리케이션으로써 실행하도록 설정한다.

run과 create 모두 도커 허브에서 이미지를 내려 받은 뒤, 컨테이너를 생성하는 것은 동일하다.
하지만 run은 추가적으로 컨테이너를 실행시키고, attach가 가능한 컨테이너라면 컨테이너 내부로 들어간다.
앞서 ubuntu:14.04 이미지를 사용하여 컨테이너를 생성해보았는데, 이번에는 생성한 컨테이너에 특정 개발 환경을 구축해서 나만의 이미지를 직접 생성해보도록 하자.
# docker run -ti --name my_container ubuntu:14.04 // 컨테이너 생성 및 내부로 진입
root@2a6ce44bfa2f:/# echo hello world >> hello.txt // hello.txt 파일 생성
root@2a6ce44bfa2f:/# exit // 컨테이너 종료
# docker commit -m "first commit" my_container my_image:0.0 // my_image:0.0 이미지 생성
위 예제는 hello.txt 파일을 가진(예제에서의 특정 개발 환경) 컨테이너를 이미지로 만든 것이다. 이처럼 컨테이너를 이미지로 만들 때에는 docker commit 명령어를 사용하면 되는데, 구체적인 형식은 아래와 같다.
# docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
이미지가 제대로 생성되었는지는 docker images 명령어를 통해 확인해볼 수 있다.
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
my_image 0.0 f0187f481560 5seconds ago 197MB
...
이로써 우리는 도커 이미지를 생성해보았다!
그렇다면 이러한 이미지는 어떻게 만들어지는 것일까?
도커 이미지의 자세한 정보를 확인하기 위해 도커 단위의 정보를 얻을 때 사용하는 docker inspect 명령어를 사용해보도록 하자.
# docker inspect ubuntu:14.04
...
"Layers": [
"sha256:f2fa9f4cf8fd0a521d40e34492b522cee3f35004047e617c75fadeb8bfd1e6b7",
"sha256:30d3c4334a2379748937816c01f5c972a8291a5ccc958d6b33d735457a16196e",
"sha256:83109fa660b2ed9307948505abd3c1f24c27c64009691067edb765bd3714b98d"
]
...
# docker inspect my_image:0.0
...
"Layers": [
"sha256:f2fa9f4cf8fd0a521d40e34492b522cee3f35004047e617c75fadeb8bfd1e6b7",
"sha256:30d3c4334a2379748937816c01f5c972a8291a5ccc958d6b33d735457a16196e",
"sha256:83109fa660b2ed9307948505abd3c1f24c27c64009691067edb765bd3714b98d",
"sha256:ed5f1b26203ca7f83374d17946209f1d39a24c594f7bfdfd2241940a21d20082"
]
...
docker inspect 명령어를 통해 ubuntu:14.04 이미지에 대한 정보와 앞서 만든 my_image:0.0 이미지에 대한 정보를 볼 수 있는데, 이 중 Layers라는 부분에 주목해보도록 하자.
ubuntu:14.04 이미지는 3개의 레이어를 갖고 있고, 이를 기반으로 만든 my_image:0.0 이미지는 4개의 레이어를 갖고 있다는 것을 알 수 있다.
이러한 구조의 원리는 3개의 레이어를 가진 ubuntu:14.04 이미지를 기반으로 생성한 my_container 컨테이너에 hello.txt라는 파일이 추가되었는데, 해당 컨테이너를 이미지로 만들 때 변경 사항인 hello.txt 파일이 하나의 레이어가 되어 my_image:0.0이 생성된 것이다.

즉, 이미지를 생성할 때는 컨테이너에서 변경된 사항만 새로운 레이어로 저장하고, 그 레이어를 포함해 새로운 이미지를 생성한다는 것을 알 수 있다.
도커 이미지 좀 더 쉽게 생성하기
앞서 도커 이미지에 대해 알아보았는데, 개발한 애플리케이션을 컨테이너화할 때 아래와 같은 과정을 가져야했다.
1. 아무것도 존재하지 않는 이미지(ubuntu, CentOS 등)로 컨테이너 생성
2. 애플리케이션을 위한 환경 설치
3. 해당 컨테이너를 이미지로 커밋(Commit)
하지만 위 과정은 수작업이 요구되기 때문에 어느 정도의 자동화가 필요하다고 느낄 수 있다. 도커는 이를 위해 위 과정을 손쉽게 기록하고 수행할 수 있는 빌드 명령어와 Dockerfile을 제공해준다.
다음은 앞서 my_image:0.0을 생성했던 과정과 이 과정을 Dockerfile로 수행해 본 예시이다.
# docker run -ti --name my_container ubuntu:14.04
root@2a6ce44bfa2f:/# echo hello world >> hello.txt
root@2a6ce44bfa2f:/# exit
# docker commit -m "first commit" my_container my_image:0.0
# vim Dockerfile
FROM ubuntu:14.04
RUN echo hello world >> hello.txt
:wq!
# docker build -t my_image:0.0 ./
FROM: 생성할 이미지의 베이스가 될 이미지를 설정
RUN: 이미지를 만들기 위해 컨테이너 내부에서 명령어를 실행
수작업이 요구되는 이미지를 생성하기 위해 실행해야 하는 일련의 과정을 Dockerfile에 기록하면, 이제는 build 명령어만을 통해 Dockerfile에 기록된 과정을 거쳐 이미지를 생성할 수 있게 된다.
참고로 build 명령어의 끝에는 Dockerfile이 위치한 경로를 지정하면 되는데, 위 예시에서는 ./(현재 디렉토리)를 지정하였다.
사실 위 예시는 텍스트 파일 하나를 추가하는 매우 간단한 과정이라 Dockerfile의 편리함을 느끼지 못할 수 있는데, 이번에는 웹 애플리케이션을 다뤄봄으로써 편리함을 느껴보도록 하자.
다음은 스프링 기반의 웹 애플리케이션을 컨테이너화하고, 실행시키는 과정이다.
# docker run -ti --name my_container ubuntu:18.04
root@ae6748b38fda:/# apt-get update
root@ae6748b38fda:/# apt-get upgrade
root@ae6748b38fda:/# apt-get install openjdk-11-jdk
root@ae6748b38fda:/# Ctrl + P + Q
# docker cp ./web_app/build/libs/*.jar my_container:/
# docker commit -m "web app test" my_container web_image:0.0
# docker run -d -p 8080:8080 --name web_container web_image:0.0 java -jar ./webapp-0.0.1.SNAPSHOT.jar
Ctrl + P + Q: 컨테이너를 종료하지 않고 빠져나오기
docker cp: 호스트에서 컨테이너로 파일을 복사하는 명령어
docker run -p [호스트의 포트]:[컨테이너의 포트]: 컨테이너의 포트를 호스트의 포트와 바인딩하는 옵션
언뜻 보기에도 꽤 많은 수작업이 필요하다는 것을 알 수 있고, 새로운 버전을 배포할 때마다 매번 위와 같은 과정을 가져야 한다면 많이 불편할 것이다.
그렇다면 위 과정을 Dockerfile을 작성하여 수행해보도록 하자.
# vim Dockerfile
FROM openjdk:11
COPY web_app/build/libs/*.jar ./
CMD ["java", "-jar", "./webapp-0.0.1-SNAPSHOT.jar"]
COPY: 파일을 이미지에 추가하는 명령어로, 파일은 Dockerfile이 위치한 디렉토리인 컨텍스트에서 가져온다.
# docker build -t web_image:0.1 ./
# docker run -d -p 8080:8080 --name web_container web_image:0.1
앞서 일일이 해주던 작업을 Dockerfile로 기록해두었고, build 명령어를 통해 컨테이너화하였다. Dockerfile을 작성하였으니 이제는 build 명령어만 사용하면 이미지를 생성할 수 있게 된다.
이처럼 Dockerfile을 이용하면 애플리케이션의 컨테이너화를 좀 더 손쉽게 진행할 수 있다.
Dockerfile에는 디렉토리를 바꾸는 WORKDIR, 환경 변수를 설정하는 ENV, 볼륨을 설정하는 VOLUME 등 다양한 명령어들을 제공한다. 애플리케이션에 맞는 Dockerfile을 작성하기 위해서 Dockerfile 공식문서를 참고해보도록 하자.
도커 컴포즈 - 여러 개의 컨테이너 다루기
앞서 Dockerfile로 이미지를 손쉽게 생성할 수 있는 방법까지 알아보았는데, 그렇다면 이렇게 생성한 여러 개의 이미지들로 컨테이너를 생성해야 한다고 하면 어떻게 해야할까?
각 이미지마다 run 명령어를 통해 컨테이너를 생성하는 방법이 있을 것이다.
하지만 이 방법은 컨테이너의 수가 많다면 번거로울 수 있고, 만약 다른 애플리케이션에 의존성이 있는 애플리케이션이 있다면 인적 실수로 인해 애플리케이션이 구동되지 않을 수도 있다.
이럴 때 사용할 수 있는 도구가 도커 컴포즈이다. 도커 컴포즈는 여러 개의 컨테이너를 하나의 프로젝트로써 다룰 수 있는 환경을 제공하고, 여러 개의 컨테이너의 옵션과 환경을 정의한 파일을 읽어 컨테이너를 순차적으로 생성해준다.
도커 컴포즈 설정 파일은 run 명령어의 옵션을 그대로 사용할 수 있으며, 각 컨테이너의 의존성, 네트워크, 볼륨 등을 함께 정의할 수 있다.
먼저 도커 컴포즈를 시작하기 위해 다음 명령어로 설치하도록 하자.
# curl -L https://github.com/docker/compose/releases/download/2.1.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
# chmod +x /usr/local/bin/docker-compose
# docker-compose -v
도커 컴포즈는 컨테이너의 설정이 정의된 YAML 파일을 읽어 도커 엔진을 통해 컨테이너를 생성한다. 따라서 도커 컴포즈를 사용하려면 가장 먼저 YAML 파일을 작성해야 한다.

다음은 docker-compose.yml 파일의 예시이다.
version: '3.8'
services:
client:
image: kykapple/playground-frontend:0.1
container_name: client
ports:
- "80:80"
depends_on:
- server
server:
image: kykapple/playground-backend:0.1
container_name: server
ports:
- "8080:8080"
위 YAML 파일을 설명해보자면 다음과 같다.
- kykapple/playground-frontend:0.1 이미지를 통해 client라는 이름을 가진 컨테이너를 생성하고, 호스트의 80번 포트와 컨테이너의 80번 포트를 연결한다. 그리고 depends_on 옵션을 통해 server 컨테이너가 실행된 후에 client 컨테이너가 실행되도록 한다.
- kykapple/playground-backend:0.1 이미지를 통해 server라는 이름을 가진 컨테이너를 생성하고, 호스트의 8080번 포트와 컨테이너의 8080번 포트를 연결한다.
이제는 docker-compose up -d 명령어를 통해 도커 컴포즈 설정 파일에 정의된 여러 컨테이너를 생성할 수 있게 되었다.
# docker-compose up -d
[+] Running 3/3
⠿ Network deploy-playground_default Created 0.1s
⠿ Container server Started 0.5s
⠿ Container client Started 1.8s
별도의 설정을 하지 않으면 도커 컴포즈는 현재 디렉토리에 있는 docker-compose.yml 파일을 읽어 도커 엔진에게 컨테이너 생성을 요청한다.
추가적으로 docker-compose up 명령어 끝에 서비스의 이름을 입력하면 특정 서비스의 컨테이너만 생성할 수도 있다.
# docker-compose up -d server // server 서비스의 컨테이너만 생성
또한 docker-compose down 명령어로 생성된 프로젝트를 삭제할 수도 있다. 프로젝트를 삭제하면 서비스의 컨테이너 또한 전부 정지된 뒤에 삭제된다.
# docker-compose down
[+] Running 3/3
⠿ Container client Removed 0.1s
⠿ Container server Removed 0.2s
⠿ Network deploy-playground_default Removed 0.1s
마지막으로 다음은 docker-compose.yml 파일에서 사용할 수 있는 일부 옵션들에 대한 설명이다.
- version - docker-compose.yml 파일의 규격 버전으로, 버전에 따라 지원하는 옵션들이 달라진다.
- services - 도커 컴포즈로 생성할 컨테이너 옵션을 정의한다.
- image - 서비스의 컨테이너를 생성할 때 쓰일 이미지의 이름을 설정한다.
- container_name - 생성할 컨테이너의 이름을 설정한다.
- ports - 서비스의 컨테이너를 개방할 포트를 설정한다.
- depends_on - 특정 컨테이너에 대한 의존 관계를 나타내며, 이 항목에 명시된 컨테이너가 먼저 생성되고 실행된다.
- command - 컨테이너가 실행될 때 수행할 명령어를 설정한다.
- build - build 항목에 정의된 Dockerfile에서 이미지를 빌드해 서비스의 컨테이너를 생성하도록 설정한다. 이때 새롭게 빌 드될 이미지의 이름은 image 항목에 정의하면 된다.
이 외에도 여러 가지 옵션이 존재하는데, 도커 컴포즈 공식문서를 참고하도록 하자.
'DevOps' 카테고리의 다른 글
Jenkins를 활용한 자동 배포 구축 (0) | 2022.05.13 |
---|---|
인덱스 컨디션 푸시다운(index_condition_pushdown) (0) | 2022.02.19 |
어댑티브 해시 인덱스(Adaptive Hash Index) (0) | 2022.02.02 |
도커 스웜(Docker Swarm) 알아보기 (0) | 2021.12.26 |