Docker
백엔드를 공부하다보면 Docker라는 말을 한 번쯤은 들어보았을 것이다. Node.js의 2016년 설문조사에 의하면 응답자 중 45%가 컨테이너 기술을 Node.js와 함께 사용한다고 답했다. 웹 분야 뿐만 아니라 IoT 개발자 중 58%가 Node.js와 Docker를 함께 사용한다고 답했다. 즉, Node.js와 Docker는 함께 사용하기에 좋은 조합이라 말할 수 있다.(이는 MSA와 관련이 크다.)
1. Container & Image
Docker란 Container를 사용하여 응용프로그램을 더 쉽게 만들고, 배포하고, 실행할 수 있도록 설계된 도구이다. Docker를 이해하기 위해선 Container와 Image라는 개념을 알고 있어야 한다.
Container는 현실에서 물건을 담고 이동시키는 용도로 사용한다. Docker의 컨테이너는 프로그램 + 실행환경을 담아 쉽게 이동시키는 용도이다. 예를 들어 A가 Node.js 서버를 작성하여 컨테이너에 담아 동료인 B에게 전달하면 B는 A가 개발하던 환경과 똑같은 환경에서 이어 작업할 수 있다.
Container에 담긴 내용물(프로그램 + 실행환경)은 Image라 부른다. 이미지는 컨테이너 실행에 필요한 모든 것을 포함한 소프트웨어 패키지이다. 코드, 런타임, 시스템 도구, 시스템 라이브러리, 설정 등이 담겨있으며, 하나의 이미지로 여러 컨테이너를 만들 수 있다.
이미지는 컨테이너 시작시 실행될 명령어와 프로그램 스냅샷(프로그램을 실행하는데 필요한 파일 + 실행파일)로 구성되어있다. 이를 컨테이너화하면 컨테이너는 실행 명령어를 통해 실행파일을 실행시킨다. 이 때 컨테이너는 프로세스를 실행시킬 최소한의 리소스를 함께 포함하고 있다.
또한 이미지에 하나의 컨테이너가 대응되는 것이 아니라 옵션을 다르게 줌으로써 여러 컨테이를 만들 수 있다.
2. Docker의 장점
1) Install
개발을 할 때 은근 신경쓰이는 점이 개발환경 구축이다. 각 개발 언어마다 환경구축방법도 다르고, 사용자의 OS에 따라 다르기도 하다. 하지만 Docker를 이용하면 간단하다. 위 그림은 Docker로 node, redis, python 개발환경을 구축한 것이다. 모두 Docker run 이라는 명령 한 줄로 간단히 개발환경을 구축하고 사용할 수 있다.
2) Isolation
Docker로 여러 컨테이너를 실행시키면 위와 같은 형태이다. Host OS 위에 Docker 엔진이 있어 OS와 상관없이 여러 이미지를 실행시킬 수 있으며, 이들은 컨테이너로 격리되어있다. 따라서 서로 다른 프로그램 App A와 App B는 서로 영향을 주지 않는다.
예를들면 A와 B가 모두 4000번 포트를 쓰는 Node.js 서버라 하자. 이들을 동시에 실행시키기 위해선 둘 중 하나는 포트를 변경해야한다. 그러나 컨테이너로 둘을 격리시킨다면 코드를 변경시킬 필요가 없다. 이에 대해선 DockerFile 항목에서 설명하겠다.
2. Dockerising Node.js
Docker로 Node.js 서버를 개발하기 위해선 서버 코드 작성 > Dockerfile 작성 > 이미지 빌드/실행 과정을 거친다.
1) 서버 코드 작성
const express = require('express');
const PORT = 8080;
const app = express();
app.get('/', (req, res) => {
res.send('Hello World');
});
app.listen(PORT, HOST);
console.log(`Running on ${PORT}`);
서버 코드는 평소 하던대로 작성한다.
2) Dockerfile 작성
# 베이스 이미지
FROM node:12
# 앱 디렉터리 생성
WORKDIR /usr/src/app
# 앱 의존성 설치
# 가능한 경우(npm@5+) package.json과 package-lock.json을 모두 복사하기 위해
# 와일드카드를 사용
COPY package*.json ./
RUN npm install
# 프로덕션을 위한 코드를 빌드하는 경우
# RUN npm ci --only=production
# 앱 소스 추가
COPY . .
EXPOSE 8080
CMD [ "node", "server.js" ]
Dockerfile은 Docker Image를 만들기 위한 명령어를 순차적으로 적은 파일이다. 먼저 From 키워드를 통해 베이스 이미지를 지정하였다. 위 경우는 node 12버전을 사용하였으며, 배포를 위해서는 alpine 버전을 이용한다. alpine이란 크기를 최소화한 이미지로 npm, git, bash를 포함하지 않을 수 있다. 사실 배포할 때는 npm install이나 git 명령이 필요하지 않기 때문이다.
그 후에는 WORKDIR로 내 노드 서버가 위치할 작업 폴더를 지정하며, COPY로 package.json, package-lock.json을 해당 폴더에 먼저 복사한다. 이는 후에 npm install을 하기 위해 package.json이 필수이기 때문에 먼저 복사하는 것이다. RUN 명령어로 npm install을 하고 나면 나머지 앱 소스파일들을 복사한다.
이대로 Docker를 실행하고 8080포트에 접속하면 포트가 닫혀있다. 8080포트로 컨테이너 내의 서버에 접속하려면 컨테이너의 8080포트도 열어주고 이미지를 실행할 때 컨테이너의 8080포트와 서버의 8080포트를 연결해주어야 한다. 여기서 아까 App A와 B가 모두 4000포트를 열고 있을 때 동시에 실행가능하다는 말이 설명된다. 두 서버의 코드에서 모두 4000포트를 연다고 하더라도 컨테이너가 각자 다른 포트를 열고 서버의 4000포트와 연결시켜놓는다면 코드를 바꾸지 않더라도 두 서버 모두 띄울 수 있다.
Dockerfile은 package.json처럼 보통 프로젝트의 루트폴더에 위치하며, .dockerignore 파일을 만들어 Docker 이미지에 복사하고 싶지 않은 파일/폴더를 지정할 수 있다. 또한 개발용과 배포용 Dockerfile을 따로 두기도 하며 개발용의 경우 DorkerFile.dev를 사용하며 build할 때 -f 옵션을 이용하여 사용할 Dorkerfile을 지정해준다.
3) Dockerfile and Run
#현재 경로의 소스코드 DockerFile을 이용하여 이미지 생성 -> 이름 랜덤
docker run .
#태그를 지정하여 이미지 생성
docker build -t ihl/todos .
#DockerFile을 지정하여 이미지 생성
docker build . -f DockerFile.dev
#4000포트와 연결하여 이미지 실행
docker run -p 4000:4000 ihl/todos
#5000포트와 연결하여 이미지 실행
docker run -p 5000:4000 ihl/todos
#코드 변경이 바로 반영되도록 하는 개발환경을 위한 실행 명령어
docker run -d -p 4000:4000 -v /usr/src/app/nodemodules -v $(pwd):/usr/src/app ihl/todos
Dockerfile을 작성한 후에는 해당 파일을 빌드하여 이미지를 생성한 후 이미지를 실행(컨테이너)한다. 빌드하거나 실행할 때는 다양한 옵션이 있으므로 이를 알아보고 활용하자.
4. Docker Compose
Docker Compose는 DockerFile로 이미지를 컨테이너화할 때 경우에 따라 사용해야하는 옵션이 너무 많거나, 여러 Docker Container를 한 번에 실행시킬 때 사용한다. 예를 들면 다음과 같다.
docker run -e MYSQL_USER=root MYSQL_PASSWORD=secret MYSQL_DB=todos -p 13306:3306
위는 MySQL 컨테이너를 실행시키기위한 명령어이다. MySQL을 사용하기 위해선 최소 User, Password, DB 환경변수와 포트번호를 지정해주어야 한다. MySQL 컨테이너를 실행시킬 때마다 명령어에 옵션들을 지정해주어야 하는 것은 매우 번거롭다. docker-compose를 이용해보자.
version:'3'
services:
database:
image: mysql:5.7.27
ports:
-13306:3306
environment:
MYSQL_USER: root
MYSQL_PASSWORD: secret
MYSQL_DB: todos
redis:
image: redis: 5.0.5
port:
-16379: 6379
위는 mysql와 redis를 실행시키기 위한 docker-compose.yml 파일의 일부이다. 컨테이너를 실행할 때 주던 옵션들을 docker-compose 파일로 만든 후 docker-compose up 명령어만 입력하면 mysql에 옵션을 적용하여 실행할 수 있으며 동시에 redis도 실행할 수 있다.
5. DockerHub
npm install을 하면 NPM 저장소에서 모듈을 가져오는 것처럼 Docker의 이미지 파일들이 저장되어있는 저장소가 있다. DockerHub가 그것이다. DockerHub에서는 마치 GitHub처럼 나만의 저장소를 만들 수 있으며 내가 만든 이미지를 올리고, 다른 PC에서 당겨올 수 있다.
6. Deployment with DockerHub
Docker를 AWS EC2에 배포하기 위해선 이미지 DockerHub 업로드 > EC2에서 이미지 Pull > 이미지 실행 단계를 거친다.
1) DockerHub 업로드
#Docker Image를 DockerHub에 업로드
#DockerHub 로그인
docker login
#imhyelim1091/test 라는 저장소에 latest라는 태그로 올리기
docker push imhyelim1091/test imhyelim1091/test:latest
DockerHub에 이미지를 업로드 하기 위해선 DockerHub에 회원가입 및 로그인을 진행하고 저장소를 만든후 push라는 명령을 이용한다. 태그는 따로 지정하지 않으면 latest로 자동 지정된다.
2) EC2 Pull & Run
#저장소에서 이미지 가져오기
docker pull imhyelim1091/test:latest
저장소에 내가 만든 이미지가 올린 후에는 EC2에 접속하여 pull 명령을 사용하여 저장소에서 이미지를 가져온다. 그 후 이미지를 run 명령어로 실행하면 된다.
7. AutoBuild
DockerHub의 저장소는 Git허브와 연결하여 Git 허브에서 코드에 변화가 생겼을 때 자동으로 빌드하여 저장소에 업로드하는 AutoBuild 기능을 제공한다.
나의 저장소에 들어가면 Build라는 메뉴가 있다. 이를 클릭한다.
Build 페이지에는 GitHub 혹은 Bitbucket과 연결할 수 있는 선택지가 있다. 나는 GitHub를 이용할 것이기 때문에 GitHub를 클릭한 후 GitHub와 DockerHub를 연결해준다.
그 후에는 연결할 GitHUb 저장소와 빌드 옵션을 지정한다. 위 내용은 나의 lilakchal-server라는 GitHub 저장소의 master브랜치가 변경되면 Dockerfile을 이용하여 latest라는 태그로 빌드할 것이라는 의미이다.
설정 후 Trigger 버튼을 누르거나 GitHub 저장소에 변동이 생기면 자동으로 빌드가 진행(pending)되며, 성공하면 Success라는 메시지를 볼 수 있다.
빌드 기록도 DockerHub 내에서 볼 수 있다.
이것만으로도 개발-배포 과정이 상당히 편해졌지만 더 나아가 Docker 이미지를 EC2에 자동으로 업로드 시키기 위해선 Travis, Jenkins, AWS CodeBuild/Deploy/Pipeline 등의 도구를 추가적으로 사용할 수도 있다.
Travis는 public 레포만 무료로 사용할 수 있어 제외했으며(우리 프로젝트는 규정? 때문에 private여야했다) Jenkins는 Jenkins용 BuildServer가 따로 있어야 하며 설정이 상당히 까다롭고 소규모 프로젝트에는 적합하지 않아 제외. DockerHub를 쓰는 대신 AWS의 CodeBuild(Docerfile로 이미지 생성) + CodeDeploy + CodePipeline을 함께 사용하는 선택지도 있다. 안그래도 어려운 AWS의 서비스를 3개나 써야한다는 단점이 있는데 내가 직접 해본 건 CodeBuild 뿐이라 사실 쉬울 수도 있다. 지난 프로젝트에서는 프론트엔드 배포에만 CodeBuild를 사용하고 백엔드는 DockerHub만을 사용하였는데, 다음 기회가 있다면 백엔드도 AWS의 서비스를 이용하던가 Travis를 사용해보고 싶다.
docs.microsoft.com/ko-kr/visualstudio/docker/tutorials/use-docker-compose