1. 서론
신한DS SW Academy에서 진행한 2차 프로젝트는 웹 어플리케이션이다. 프론트엔드는 React, 백엔드는 Spring boot 모듈로 구현되었다. 해당 프로젝트에서 배포를 위해 DevOps 체계를 구축했는데, DevOps 체계에서 CI(Continuous Integration)와 CD(Continuous Deployment)의 자동화는 불가피한 요소이다. 바로 이 자동화의 중심에 Docker라는 리눅스 컨테이너 기술이 있다. 쉽게 말하면, Docker는 이런 자동화 과정에서 필요한 것을 만들고, 올리고, 내릴 수 있는 일꾼이라고 생각하면 편하다. Docker의 기본적인 개념부터 살펴보도록 하자.
2. 가상화란?
Wikipedia에서 정의하는 정돈된 정의는 다음과 같다.
가상화(假像化, virtualization)는 컴퓨터에서 컴퓨터 리소스의 추상화를 일컫는 광범위한 용어이다. "물리적인 컴퓨터 리소스의 특징을 다른 시스템, 응용 프로그램, 최종 사용자들이 리소스와 상호 작용하는 방식으로부터 감추는 기술"로 정의할 수 있다. 이것은 다중 논리 리소스로서의 기능을 하는 것처럼 보이는 서버, 운영 체제, 응용 프로그램, 또는 저장 장치와 같은 하나의 단일 물리 리소스를 만들어 낸다. 아니면 단일 논리 리소스처럼 보이는 저장 장치나 서버와 같은 여러 개의 물리적 리소스를 만들어 낼 수 있다.
쉽게 말해, 실제로는 하나의 자원만을 가지고 있지만 원래부터 여러 개인 것처럼 자원을 분할해서 사용하거나, 또는 그 반대로 원래는 분리돼 있는 자원을 하나의 자원인 것처럼 사용하는 것을 '가상화'라고 하는 것이다. 사용자의 경험 상으로는 '실재'하는 자원으로 인식하지만, 실제로는 다른 '실체'를 가진 컴퓨팅 자원인 것이다.
예를 들어 구글이나 네이버, AWS 등에서 제공하는 여러 클라우드 서비스가 가상화의 대표적인 예이다. 이러한 클라우드 서비스를 이용할 때 우리는 실물을 배송받는가? 아니다. 실제로는 해당 기업의 자원을 가상화시킨 형태로 나누어 주기 때문에 제공하고 회수하는 것이 빠르고 간편하며, 비용과 관리 측면에서 많은 이점을 취할 수 있다. 이에 더해 이 가상화 자원들은 본질적으로 하나의 자원이더라도(물리적으로는 하나의 자원) 서로 완벽하게 격리되어 있기 때문에(논리적으로는 격리된 자원들), 전체 시스템 그리고 서로에게 영향을 미치지 않는다. 따라서 가상화는 컴퓨팅 자원을 활용할 때 유용성과 편리성을 수반할 수 있게하는 아주 중요한 개념이다.
3. Docker는 왜 필요한가?
가상화 기술 등장 이전에는 하나의 서버에 하나의 어플리케이션만 구동시켰다고 한다. 이런 체제에서는 서버에서 어플리케이션 외의 남는 자원들이 많다보니 서버는 리소스를 낭비하게 된다. 그렇다고 여러 어플리케이션을 한 서버에 올리면, 그 안정성에 문제가 생기게 된다. 예를 들어 한 어플리케이션의 과부하가 전체 시스템에 악영향을 미칠 수 있다. 이를 해결하기 위해 가상화 기술이 적용되기 시작한 것이다.
어플리케이션 가상화 방식에는 대표적으로 VM(Virtual Machine) 가상화와 Docker(리눅스 컨테이너 기술 기반) 가상화가 있다. VM 가상화의 경우, Host OS 위에 가상화를 위한 Hypervisor 엔진과 그 위에 Guest OS를 올려 사용한다. 이로 인해 각 어플리케이션들이 Host OS와 완전히 분리되고, 더 높은 격리 레벨을 지원하므로 보안적인 측면에서 우수하다. 또한 이 경우 커널을 공유하지 않기 때문에 멀티 OS를 가능하게 한다는 장점이 있다. 하지만 OS 위에 다시 OS를 올리는 방식이기 때문에 전체적으로 무겁고 느려질 수 밖에 없다는 단점이 존재한다.
컨테이너 기반의 가상화(ex) Docker)는 Host OS 위에 어플리케이션 실행에 필요한 바이너리/라이브러리만 올라가고 Docker Engine은 이를 관리한다. 또한 Host의 커널을 공유하기 때문에 I/O의 처리에 있어서 성능적인 부분에서 효율성을 높일 수 있다. 따라서 컨테이너 구조는 가상머신을 생성하지 않고도 Host가 사용하는 자원을 분리하여 다양한 환경을 만들 수 있도록 한다.
컨테이너 기반 가상화가 VM 기반 가상화보다 우등한 기술이라는 것을 말하고 싶은 것이 아니다. 각 가상화 기술은 뚜렷한 장단점을 가지고 있다. 다만 Docker 컨테이너 기술은 가상화 사용 방식에 차별화되는 강점을 가지면서 장래가 유망한 기술이다. 실제로 2015년 이래로 리눅스 컨테이너 기술 부분에서 Docker는 업계 표준이 되었다고 해도 과언이 아니다.
정리하자면 Docker 컨테이너 기술은 VM 가상화에 비해 보안성이 떨어지고 멀티 OS가 불가능하다는 단점이 있으나, 가볍고 빠른 성능과 뛰어난 이식성과 Scale-Out(서버를 여러 대 추가하는 것, 확장성)에 대한 막강한 유연성을 가지고 있다.
따라서 우리가 Docker를 사용하는 이유는 이러한 컨테이너 기술을 통해 어플리케이션 배포와 관리를 쉽고 빠르게 하기 위함이다.
4. Docker란? + Docker의 구성요소
컨테이너는 개별 어플리케이션 내지는 소프트웨어의 실행에 요구되는 실행 환경을 독립적으로 가지고, Host OS나 다른 환경과의 간섭을 막고 실행의 독립성이 확보돼 있는 격리 기술을 뜻한다. 즉, 어플리케이션들이 컨테이너화 되었다는 것은, 서로 간섭하지 않으면서도 각자의 실행이 보장된 상태에서 독립적으로 운용된다는 뜻이다.
Docker는 이러한 리눅스 컨테이너 기술을 이용해 만든 컨네이너 기술 중 하나이다. Docker을 이용해 쉽고 빠른 가상화를 만족시킬 수 있다. 하나의 Host OS 안에서도 여러 어플리케이션을 분리해 컨테이너로 추상화시켜 각각을 동시에 실행 및 운용할 수 있게 된다.
정리하자면, Docker는 어플리케이션 패키징, 배포, 실행을 위한 플랫폼이다. 여기서 어플리케이션 패키징이란 라이브러리, 파일 시스템, 실행 환경을 하나로 묶어서 하나의 어플리케이션으로 만드는 것을 의미한다.
Docker 아키텍처는 위와 같은데, Docker Client와 Docker Daemon이 내부적으로 Rest API를 사용해서 통신하는 구조이다. Docker는 Docker Client, Docker Daemon, Docker Registries, Docker Objects로 구성된다.
- Docker Client : 정체를 말하자면 CLI 명령어 도구이다. 사용자(우리)가 docker run ... / docker build ... 등의 CLI 명령어를 실행시키면 Docker Client는 이 명령어를 Rest API call로 변환시켜 Docker Daemon에 송신한다. 즉, 인간과 소프트웨어(Docker Daemon) 사이를 매개해주는 다리 역할을 한다.
- Docker Daemon : Docker Daemon은 상기한 Rest API call을 수신해 명령어에 해당하는 작업을 수행한다. 이를 통해 Docker Objects를 관리한다. 모든 실제 동작은 Docker Daemon에서 이루어진다.
- Docker Registries : Docker Image를 저장하는 저장소이다. Docker Hub는 Public한 Registry이며 Docker는 레지스트리를 기본 값으로 이 Docker Hub로 둔다. 물론 사용자는 Private한 Registry를 개인적으로 구축해 사용 가능하다. 예를 들어 Docker Daemon이 docker pull ... 또는 docker run ... 등의 요청을 받으면, Daemon은 이 Docker Registries에서 Image를 다운 받거나 컨터이너를 실행시키는 것이다.
- Docker Objects : Docker Objects는 Image, Container, Network, Volume과 같은 컨테이너 기술을 이루는 핵심 Objects를 총칭한다.
- Image : Container 실행에 필요한 파일이나 설정 값등이 포함되어 있다. Image는 수정될 수 없으며, Container를 생성하기 위한 필요한 절차가 기록된 설계도라고 봐도 무방하다. 이런 수정될 수 없다는 일관성은 Docker의 큰 특징이자 장점 중 하나로, 개발자가 일관된 환경에서 안정적으로 Software를 테스트하고 실험할 수 있다. Docker Image는 파일 시스템에서 Layer를 중첩해 쌓는 방식으로 생성되며, 이미 빌드된 Image의 중간 Layer는 캐싱되므로 추후에 다른 Image를 빌드할 때 재활용해 용량을 효율적으로 관리할 수 있다.
- Container : Image가 설계도라면 Container는 이 설계도를 토대로 실제로 실행되는 프로그램이다. Container는 Image를 실행한 결과로 구동되며, 사용자는 Docker Client로 이를 관리할 수 있다. 각 Container는 Host 환경에서 완전히 격리되는 프로세스지만, Network나 Volume을 통해 일부 연결되는 것이 가능하다. 또한 Container는 수정이 가능하다.
위 아키텍처 그림을 토대로 Docker의 작동 과정을 정리해보자.
- 사용자가 Docker를 설치하면, 해당 Host 시스템(쉽게 말하면 PC)에 Docker Daemon이 설치되고 해당 Host 시스템에 '떠' 있게 된다.(백그라운드에서 계속 작동 중임.)
- 사용자가 CLI 또는 전용 프론트엔드(Docker Desktop 등)를 통해 Docker Client에게 명령어를 전달한다.
- Docker Client는 이를 Rest API call로 변환시켜 Docker Daemon에게 전달한다.
- Docker Daemon은 이 명령어대로 동작을 수행한다.(Registry에서 Image를 가져오거나, Image를 토대로 Container를 띄우거나, Dockerfile에 명시된대로 Image를 build 하는 등)
따라서 우리는 이러한 Docker 컨테이너 기술을 배포에 활용할 것이다.
5. Dockerfile
우리가 어떤 어플리케이션을 이용할 때, 그 어플리케이션의 실체는 도커 컨테이너이다.(도커 플랫폼을 이용했다는 전제 하에) 컨테이너는 도커 이미지를 통해 만들어지고 구동된다. 그렇다면 도커 이미지는 어떻게 만들어지는가? 상술했듯이 도커 레지스트리에서 이미 만들어둔 여러 이미지를 가져올 수 있지만, 필자와 같이 프로그래밍 언어를 통해 새로운 프로그램을 만들었을 경우 레지스트리에만 의존해서는 우리만의 컨테이너를 구동시킬 수 없다. 즉, 우리가 직접 도커 이미지를 만들 수 있어야 한다.
Dockerfile은 이를 가능하게 한다. Dockerfile은 도커 이미지를 생성하기 위한 설정 파일이다. 이미지가 컨테이너를 만들고 구동시키기 위한 설계도였다면, Dockerfile은 이미지를 만들기 위한 설계도라고 생각하면 쉽다. 개발자는 Docker의 build 기능을 통해 Dockerfile에 명시된 여러가지 명령어를 수행시킬 수 있고, 이를 토대로 이미지를 만들 수 있다.
따라서 Dockerfile은 컨테이너 구동의 시작점이라고 봐도 무방하다. Dockerfile에는 이미지가 어떻게 만들어졌는지 명시돼 있다. 이를 통해 개발자는 자기의 어플리케이션을 통해 필요한 부분을 수정 내지는 추가해가며 과정을 기록할 수 있다. 또한 도커 이미지는 환경에 종속되지 않은 일관성을 지니므로, 배포 시 Dockerfile만을 배포하더라도 해당하는 이미지를 배포하는 것과 동일하다. 따라서 용량이 큰 이미지 파일을 직접적으로 공유하지 않고도 하나의 스크립트 파일만을 통해 간단한 배포가 가능하다. 이를 사용하는 사람은 build만 하더라도 해당 이미지를 얻을 수 있기 때문이다. 개발자는 이러한 Dockerfile 스크립트를 통해 자기만의 이미지 또는 컨테이너를 구축할 수 있게 된다.
Dockerfile
# 베이스 이미지를 뜻한다. 어느 이미지를 기반으로 만들어지는 것인지 의미한다.
# 해당 Spring boot 프로젝트는 jdk17 기반으로 실행되어야 하므로 버전과 함께 명시한다.(태그를 이용)
FROM openjdk:17
# 인자 값을 설정한다. build/libs 위치에 존재하는 -SNAPSHOT.jar로 끝나는 파일의 경로를 JAR_FILE에 set한다.
# 현재 Dockerfile이 위치하는 파일 시스템 기준이다.
ARG JAR_FILE=build/libs/*-SNAPSHOT.jar
# ${} 형태로 설정된 인자 값을 불러올 수 있다.
# 불러온 인자를 app.jar의 이름으로 복사해서 저장한다는 뜻이다.
# 경로가 생략되었으므로 이미지 내부의 root 디렉토리에 저장된다.
# 즉 COPY 명령어의 왼쪽은 현재 Dockerfile이 위치하는 파일 시스템 기준이고,
# 오른쪽은 이미지 내부의 파일 시스템 기준이다. 다시 말해 오른쪽이 이미지 내부이다.
COPY ${JAR_FILE} app.jar
# 이미지가 컨테이너로 구동될 때, 항상 실행되는 커맨드이다.
# 띄어쓰기를 기준으로 구분되는 것이며, app.jar를 java 명령어로 실행시켜
# 결과적으로 Spring boot 어플리케이션이 실행된다.
ENTRYPOINT ["java","-jar","/app.jar"]
위는 실제 필자의 Spring boot 어플리케이션을 도커 이미지로 만들기 위해 작성된 Dockerfile이다. 후일 또 다루겠지만 여기서 Dockerfile의 작성 요령과 함께 짚고 넘어가보자.
Docker는 이미지를 빌드할 때 Dockerfile이라는 파일명을 가진 파일을 자동으로 인식하고 이를 빌드시킨다. 따라서 실제 파일명이 Dockerfile이어야 한다.
Dockerfile 명령어
- FROM <이미지>(:<태그>) : 도커 이미지는 베이스 이미지를 시작으로 기존 이미지에 새로운 이미지를 중첩하며 여러 단계의 Layer를 쌓아가며 만들어지는데, FROM 명령어를 통해 베이스 이미지를 지정한다. 우리가 커스텀할 이미지의 근간을 이루는 시작점이라고 보면 된다. 가령, 파이썬으로 구현된 어플리케이션의 경우 베이스 이미지로는 python 이미지가 필요할 것이고, 위와 같이 자바로 구현된 어플리케이션의 경우, jdk 이미지를 베이스로 사용하면 되는 것이다.
- WORKDIR <이동할 경로> : shell의 cd(change directory) 명령어와 유사하다. 이미지 내부에서 어떤 경로(위치)에서 작업을 할 것인지 명시할 수 있다. 생략하면 /(루트 디렉토리) 경로로 자동 설정되며, 설정할 경우 이후 실행하는 명령어가 이미지 내부에서는 WORKDIR 경로를 기준으로 실행된다. 어플리케이션을 위한 소스들은 WORKDIR를 명시해 관리하는 것이 권장된다.
- RUN ["<커맨드>", "<파라미터 1>", "<파라미터 2>", ...] : 리눅스 시스템의 shell에서 사용자가 커맨드를 실행하는 것처럼 이미지 내부에서 실행되어야 하는 커맨드를 실행하기 위해 사용된다. 보통은 이미지 안에 특정한 패키지 내지는 소프트웨어를 설치하기 위해 사용되는 명령어이다.
- ENTRYPOINT ["<커맨드>", "<파라미터 1>", "<파라미터 2>", ...] : 이미지가 컨테이너로 구동될 때, 무조건 실행되는 커맨드를 지정할 때 사용한다. 이 명령어를 통해 이미지가 마치 하나의 실행 파일처럼 사용된다. 이유는 컨테이너가 구동 후, 에러 등의 이유로 ENTRYPOINT로 지정한 프로세스가 멈추게 되면 컨테이너도 이에 의존해 구동을 중지하기 때문이다. 이미지를 구동할 때 ENTRYPOINT로 지정한 명령어는 컨테이너 구동 시(docker run ...) 추가 인자로 변경되지 않는다.
- CMD ["<커맨드>", "<파라미터 1>", "<파라미터 2>", ...] : ENTRYPOINT와 유사하게 이미지를 컨테이너로 구동 시 디폴트로 해당 커맨드를 실행한다. 다만 ENTRYPOINT와 달리 컨테이너 구동 시(docker run ...) 추가 인자로 명령어를 덮어 씌울 수 있다. 따라서 CMD 명령어를 통해 디폴트 파라미터를 지정하고, 필요 시 덮어쓰는 방식으로 유연하게 컨테이너를 구동시킬 수 있다.
- EXPOSE <포트>(/<프로토콜>) : 해당 컨테이너로 접근하기 위한 포트를 개방시키기 위해 사용된다. 프로토콜은 지정하지 않을 시 TCP가 기본이며, 명시할 경우 TCP 또는 UDP를 선택할 수 있다. 컨테이너 기준임을 유의해야 한다. 컨테이너 내부에서만 개방된 포트이므로 Host에서 이 포트에 바로 접근할 수 없다. 추후 이 내용에 대해 다룰 것이다.
- COPY <Dockerfile이 실행되는 공간에서의 디렉토리> <이미지 내부 디렉토리> : Dockerfile이 실행되는 공간에 있는 파일이나 디렉토리를 이미지 내부 디렉토리로 복사하는 명령어이다. 즉 이미지 내부에 우리가 목적으로 하는 파일 또는 디렉토리를 존재하게 할 수 있다.
- ADD <Dockerfile이 실행되는 공간에서의 디렉토리> <이미지 내부 디렉토리> : COPY와 거의 유사한데, 압축 파일이나 네트워크 상의 파일과 같은 특수한 파일을 대상으로도 사용할 수 있다. 이처럼 특수한 경우가 아니라면 COPY를 사용하는 것이 권장된다.
- ENV <키>(=)<값> : 환경 변수를 지정할 때 사용한다. 이 환경 변수는 이미지 빌드 시, 컨테이너 구동 후 어플리케이션 내부 두 경우 모두 접근할 수 있다.
- ARG <이름>(=<기본값>) : 이미지 빌드 시에만 사용될 변수를 지정할 수 있다. 지정하지 않을 경우 docker build 명령어에서 --build-arg 옵션에서 값을 지정할 수 있으며, 기본 값을 지정해 빌드하는 것도 가능하다. 설정된 변수는 ${<이름>}의 형태로 사용할 수 있다.
6. 마무리
본 게시에서 Docker가 지향하는 가상화 개념과 이를 토대로 한 컨테이너 기술, Docker의 핵심 구성요소 및 개념을 다루어 보았다. Docker 명령어와 Docker와 관련된 다른 요소 및 과정의 경우에는, 앞으로 이러한 구성 요소를 가지고 직접 이미지와 컨테이너를 다루며 흐름에 맞게 다뤄보려 한다.
References
- https://khj93.tistory.com/entry/Docker-Docker-%EA%B0%9C%EB%85%90
- https://velog.io/@kdaeyeop/%EB%8F%84%EC%BB%A4-Docker-%EC%99%80-VM%EC%9D%98-%EC%B0%A8%EC%9D%B4
- https://ko.wikipedia.org/wiki/%EA%B0%80%EC%83%81%ED%99%94
- https://selog.tistory.com/entry/%EA%B0%80%EC%83%81%ED%99%94-Virtualization%EA%B0%80%EC%83%81%ED%99%94-%EA%B0%9C%EB%85%90-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0
- https://hstory0208.tistory.com/entry/kernel-%EC%9D%B4%EB%9E%80-%EC%89%BD%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90
- https://dev-coco.tistory.com/143
- https://velog.io/@geunwoobaek/%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%B0%8F-%EB%8F%84%EC%BB%A4-%EA%B0%9C%EB%85%90%EC%A0%95%EB%A6%AC
- https://velog.io/@koo8624/Docker-%EB%B2%88%EC%97%AD-%EB%8F%84%EC%BB%A4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-Docker-Architecture-Overview
- https://www.daleseo.com/dockerfile/
'Docker' 카테고리의 다른 글
[Docker] Spring boot App 배포 (4) - 서버에 배포하기 (0) | 2024.11.28 |
---|---|
[Docker] Spring boot App 배포 (3) - Web App 구동 및 빌드 (1) | 2024.11.26 |
[Docker] Spring boot App 배포 (2) - EC2 기본 + Docker 설치 (1) | 2024.11.23 |