도커 컨테이너 CI/CD 맛보기

이 장에서는 도커 컨테이너를 이용해 지속적인 품질 관리와 애플리케이션 배포 자동화를 구현해 봅니다. 지속적인 통합/배포(CI/CD)의 대표 도구인 젠킨스Jenkins와 함께 깃Git, 도커 허브Docker Hub를 이용한 컨테이너 배포를 실습해 봅니다.



이 장의 내용

  • Git, 젠킨스Jenkins, 도커 허브Docker Hub를 이용해 지속적인 통합/배포 환경을 실습합니다.



여기서 실습할 도커 컨테이너를 이용한 CI/CD 개발 환경은 깃 리포지터리Git Repository를 준비하고 소스가 Push될 때마다 젠킨스의 Event hook을 통해 커밋commit 된 소스 코드를 컨테이너와 함께 배포합니다. 전체 아키텍처는 다음과 같습니다.


먼저 간단한 웹 서버를 구성합니다. node.js의 express 프레임워크를 이용해 애플리케이션을 작성하고, 이를 도커 이미지로 만든 후 컨테이너로 실행합니다.


깃 리포지터리 생성

먼저 node.js 웹 서버 소스 코드를 저장할 깃 리포지터리git repository를 생성합니다. 개인의 깃허브Github 계정에 빈 리포지터리를 만듭니다.

● 깃허브 (https://github.com)에 접속하고 개인 계정으로 로그인합니다.

● [Start a projcet] 버튼을 클릭합니다.


● 리포지터리 이름에는 awskrug-docker을 입력합니다.

● Public을 선택한 후 [Create repository]를 클릭합니다.



EC2 콘솔창을 열고 home으로 이동합니다.

$ cd ~


자신의 로컬 PC에서 다음 명력을 실행해 앞서 생성한 리포지터리를 동기화합니다.

만약 Username과 Password를 물어보면 입력창에 자신의 깃 계정 정보를 입력합니다.

$ git clone https://github.com/<개인 git ID>/awskrug-docker.git

Cloning into 'awskrug-docker'...
Username for 'https://github.com': <개인 git ID>
Password for 'https://github.com': <개인 git PW>
warning: You appear to have cloned an empty repository.



Node.js 소스 코드 작성

그다음 깃과 동기화된 해당 디렉터리로 이동해서 node 라는 디렉터리를 만듭니다. 그리고 node.js 관련 소스 코드를 이 안에 생성합니다.

$ cd awskrug-docker
$ mkdir node
$ cd ./node


node 디렉터리 안에서 애플리케이션 의존성 관련 package.json 파일을 만듭니다.

$ vi package.json


package.json 파일 안에 다음 코드를 넣고 저장합니다.

{
   "name": "docker_web_app",
   "version": "1.0.0",
   "description": "Node.js on Docker",
   "author": "Your name <your_email@gmail.com>",
   "main": "server.js",
   "scripts": {
       "start": "node server.js"
   },
   "dependencies": {
       "express": "^4.16.1"
   }
}


server.js 파일을 같은 디렉토리 안에 생성합니다.

$ vi server.js


server.js 파일에 Hello world를 출력하는 다음의 웹 서버 코드를 입력하고 저장합니다.

'use strict';
const express = require('express');

const PORT = 8000;
const HOST = '0.0.0.0';

const app = express();
app.get('/', (req, res) => {
 res.send('Hello world!\n');
});

app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);


Dockerfile 생성

node 디렉터리 밖으로 나와 awskrug-docker 디렉터리에 들어갑니다. 그 안에 Dockerfile 파일을 생성 생성합니다.

$ cd /home/ec2-user/awskrug-docker
$ vi Dockerfile


Dockerfile 파일에 다음 코드를 넣습니다. node.js 공식 이미지를 기본 웹 서버가 될 node.js 소스 코드를 추가해 컨테이너 이미지로 빌드합니다.

FROM node:carbon

# 앱 디렉터리 생성
WORKDIR /usr/src/app

# 앱 의존성 설치
COPY ./node/package*.json ./
RUN npm install

# 앱 소스 추가
COPY ./node/*.js ./

EXPOSE 8000
CMD [ "npm", "start" ]


Dockerfile 과 같은 디렉터리 안에 .dockerignore 파일을 만들고 특정 context를 도커를 빌드할 때 제외합니다. 이는 도커 이미지의 로컬 NPM 모듈과 디버깅 로그 복사를 막기 위함입니다.

$ vi .dockerignore


.dockerignore 파일 안에 다음 두 줄의 코드를 넣고 저장합니다.

node_modules
npm-debug.log


깃에 push할 디렉터리 구조는 다음과 같습니다.

awskrug-docker

  ├ Dockerfile

  ├ .dockerignore

  └ node

     └ package.json

     └ server.js


여기까지 CI/CD를 위한 Dockerfile과 node.js 소스 코드를 작성했습니다. 다음으로는 도커 이미지가 정상적으로 빌드되는지 테스트하겠습니다.


도커 이미지 빌드 테스트

콘솔창에서 앞서 작성한 Dockerfile이 있는 디렉터리로 이동한 후 다음 명령을 실행합니다.

Dockerfile 에 명시된 대로 기본 nodejs 이미지를 기반으로, NPM을 이용해 모듈을 추가하고 server.js를 실행하는 이미지를 <docker hub username>/node-web-app라는 이름의 Tag를 붙여 생성합니다.

$ docker build -t <docker hub username>/node-web-app .


생성된 이미지를 확인합니다.

$ docker images
REPOSITORY                      TAG ID CREATED SIZE
node                            carbon 1934b0b038d1 5 days ago 673MB
<your username>/node-web-app    latest d64d3505b0d2 1 minute ago 676MB


우리가 빌드한 이미지는 node:carbon 이미지를 기반으로 생성했기 때문에 도커 이미지 목록에 node:carbon 이미지가 있습니다. 빌드한 이미지와 node:carbon 이미지 용량은 600MB가 넘지만 실제로는 node:carbon 이미지만 로컬 레지스트리의 600MB가 넘는 공간을 차지하고 있습니다. 우리가 만든 이미지는 5MB도 되지 않습니다. 이는 도커가 AUFS 파일 구조로 이미지를 쌓아서 다른 이미지를 만들기 때문입니다.


컨테이너 생성 테스트

컨테이너를 생성해 웹 서버가 제대로 동작하는지 확인해 봅시다. 빌드된 이미지로 컨테이너를 생성합니다. 호스트의 8001 포트를 컨테이너의 8000 포트로 바인딩합니다.

$ docker run -p 8001:8000 -d <your username>/node-web-app
$ docker ps


웹 브라우저 주소 창에 http://<public host IP>:8001을 입력해 웹 서버가 정상 작동하는지 확인합니다.


컨테이너가 정상적으로 동작한다면, 다음으로는 젠킨스를 설치합니다.



  [TIP] 만약 다음과 같은 에러가 발생한다면?

docker: Error response from daemon: driver failed programming external connectivity on endpoint optimistic_pasteur (<Container ID>): Bind for 0.0.0.0:8000 failed: port is already allocated.


실행되고 있는 컨테이너 중 8000 포트를 이미 점유해서 발생한 에러입니다. 실습을 위해 도커 호스트의 8000 포트를 사용하고 있는 컨테이너를 중단합니다.

$ docker stop <Container name>



젠킨스 컨테이너 설치

여기서는 젠킨스를 컨테이너로 설치합니다.

젠킨스 도커 이미지는 도커 허브의 공식 리포지터리로 제공되고 있습니다.


젠킨스 최신 버전을 지금까지 사용해온 EC2 호스트에 설치합니다. 이 호스트는 앞으로 젠킨스 마스터Jenkins master 역할을 할 것입니다.

다음으로는 젠킨스 슬레이브Jenkins slave 로 사용할 EC2 인스턴스를 하나 더 생성합니다. 슬레이브는 마스터의 명령을 전달받아 실제 깃으로부터 소스 코드를 내려받고 도커 로컬 레지스트리에 이미지를 받아 컨테이너를 생성하는 역할을 합니다.


젠킨스 마스터 컨테이너 설치

젠킨스 마스터 역할이 부여된 컨테이너를 생성하면 몇 초만에 컨테이너가 실행될 것입니다. 젠킨스 컨테니어가 삭제되더라도 설정이 영구 보관되도록 로컬에 디렉터리를 만들고 -v 옵션을 이용해 호스트의 jenkins_home과 연결합니다.

젠킨스 컨테이너의 jenkins user(1000)가 로컬 디렉터리를 사용할 수 있도록 소유권도 바꿔줍니다.

$ cd ~
$ mkdir ~/jenkins_home
$ sudo chown -R 1000:1000 ~/jenkins_home/


다음 명령으로 젠킨스 최신 버전으로 컨테이너를 만듭니다.

$ docker run -d --name my_jenkins -p 8080:8080 -p 50000:50000 -v ~/jenkins_home:/var/jenkins_home jenkins:latest


정상적으로 컨테이너가 실행되면 docker ps 명령으로 my_jenkins라는 이름의 컨테이너를 확인할 수 있습니다. 젠킨스 컨테이너는 host의 8080 포트로 매핑돼 있으므로 웹 브라우저에서 http://<public host IP>:8080로 접속할 수 있습니다.


젠킨스 설치 초기에는 관리자 키를 묻습니다. 다음 두 명령을 통해 컨테이너 내부의 password 를 확인합니다.

$ docker exec my_jenkins cat /var/jenkins_home/secrets/initialAdminPassword

또는

$ sudo cat ~/jenkins_home/secrets/initialAdminPassword


initialAdminPassword 파일의 텍스트를 복사한 후 Administrator password에 입력합니다.


다음 [suggested plugins]를 선택합니다.


제안된 설정으로 플러그인을 설치합니다. 플러그인 설치에는 약 5분이 소요됩니다.



젠킨스 배포 구성

젠킨스 첫 화면에서 계정을 생성합니다.


그러면 젠킨스 메인화면이 나타납니다. 먼저 [새로운 Item] 버튼을 클릭합니다. item name에 docker_cicd를 입력합니다. 다음 [Freestyle project]를 선택하고 하단의 [OK] 버튼을 누릅니다.


docker_cicd 라는 아이템이 만들어졌습니다. 여기서 하고자 하는 것은 awskrug-docker 깃 리포지터리에 push가 오면 젠킨스에서 트리거trigger가 작동해 push된 소스 코드를 이미지와 함께 컨테이너로 생성해 배포하는 것입니다.

docker_cicd 아이템 구성 화면에서 소스 코드 관리 탭으로 이동한 후 체크박스에 Git을 체크합니다.

그리고 Repository URL에는 앞서 만든 깃허브 리포지터리 주소를 입력합니다.

https://github.com/west0706/awskrug-docker.git



참고로 깃허브의 awskrug-docker로 가면 리포지터리 주소를 복사할 수 있습니다.


하단의 [Add] 버튼을 클릭하고 젠킨스를 선택하면 Credentials 입력 화면이 나옵니다. 여기에서 Kind 를 Username with password로 그대로 지정하고, 아래에 자신의 깃허브 계정을 입력합니다.

모두 입력했다면 [Add] 버튼을 클릭해 저장합니다.


다시 Credentials 체크 박스를 펼치고 방금 등록한 계정을 선택합니다.


아래 빌드유발 탭으로 이동한 다음 [GitHub hook trigger for GITScm polling]을 체크합니다.


마지막 Build 탭으로 이동해 [Add build step] 버튼 클릭한 다음 [Execute shell]을 선택합니다.


Build 단계에서 Command 입력창에 다음 스크립트를 복사하고 붙여넣은 후 저장합니다. 이때 입력창이 너무 좁으면 입력창 하단을 드레그해 늘립니다. 코드에서 <YOUR ID>, <YOUR PW>는 자신의 도커 허브 계정 정보를 입력해야 하는 점에 주의하기 바랍니다.

도커 로그인 정보는 향후 설정 파일로 빼는 게 보안상 좋습니다.

ID=<YOUR ID>
PW=<YOUR PW>

#login to docker hub
docker login -u $ID -p $PW

# build image
docker build -t $ID/node_app:${BUILD_NUMBER} ./

# push image to docker hub
docker push $ID/node_app:${BUILD_NUMBER}

# stop, remove old container
echo "If containers exist, stop an remove the old container."

CONTAINER_NAME="node_app"
OLD="$(docker ps -aq --filter name="$CONTAINER_NAME")"

if [ -n "OLD" ]; then
 docker stop $OLD && docker rm $OLD
fi

# create node_app container
docker run -d --name node_app-v${BUILD_NUMBER} -p 8000:8000 \
$ID/node_app:${BUILD_NUMBER}


마침내 젠킨스 배포 설정이 완료됐습니다. 하단의 저장 버튼을 클릭해 설정을 마무리합니다.


깃허브 트리거 설정

지금부터 할 일은 깃으로 push가 되면 젠킨스 배포가 이루어지도록 트리커trigger를 설정하는 것입니다.

자신의 awskrug-docker 깃 리포지터리에서 [Settings] 탭에 들어갑니다.


왼쪽의 [Intergrations & services] 메뉴를 선택하고 오른쪽의 [Add service] 버튼을 클릭해 [Jenkins(GitHub plugin)]를 선택합니다.

그다음 Jenkins hook url 에 다음과 같이 젠킨스가 있는 EC2 호스트의 외부 IP를 포함한 URL을 입력합니다. URL의 끝에 슬래시(/)까지 넣어야 하는 점에 주의합니다.

예) http://<Jenkins public IP>:8080/github-webhook/


입력했다면 [Add service] 버튼을 클릭합니다.

이제 깃허브에서 push가 발생하면 jenkins의 /github-webhook path의 플러그인을 통해 Hook이실행됩니다.

 

깃허브와 젠킨스 연동이 끝났습니다.


젠킨스 슬레이브 구성

다음으로는 젠킨스 컨테이너가 명령을 내릴 수 있는 젠킨스 슬레이브를 구성해 보겠습니다. 젠킨스 마스터가 있는 VPC, Subnet 안에 EC2를 하나 더 띄우고 젠킨스 마스터 설치와 마찬가지로 도커를 설치합니다. keypair는 기존 것을 그대로 사용합니다.

Slave용 EC2 생성


젠킨스 슬레이브용 EC2 설정(차후에는 Swarm Manager로 사용합니다)

VPC

Default VPC

Subnet

Default subnet

Public IP

Enable (기본)

Name

awskrug-docker-jenkins-slave

AMI

Amazon Linux AMI 2018.03.0 (HVM)

Instance Type

t2.medium

Security group

default

keypair

awskrug-docker-default-keypair (기존 keypair 사용)


젠킨스 슬레이브 노드에 터미널로 접속해서 도커와 깃을 설치합니다.

$ sudo yum update -y
$ sudo yum install -y docker git
$ sudo usermod -a -G docker $USER (적용을 위해 터미널 나갔다 다시 접속)
$ sudo service docker start


다음 젠킨스 슬레이브 워킹 디렉터리를 생성합니다.

$ mkdir ~/jenkins


● 젠킨스 마스터 화면에서 [Jenkins 관리] → [노드 관리] → [신규노드]를 클릭합니다.

● 노드명에 worker01를 입력하고 [Permanent Agent]를 체크하고 [OK] 버튼을 클릭합니다.


Node 설정은 다음과 같이 입력하고 [SAVE]를 클릭합니다.

Name worker01
Remote root directory

/home/ec2-user/jenkins

Labels docker



노드 목록에 보면 슬레이브slave 노드에서 아직 agent를 실행하지 않아 상태가 'x' 로 나옵니다.


노드 목록에서 worker01 노드를 클릭하면 slave.jar 파일 실행 커맨드가 나옵니다.

슬레이브 서버 안에서 터미널을 통해 wget 명령어로 slave.jar 파일을 HOME 폴더에 내려받습니다.

$ wget http://<Jenkins master IP>:8080/jnlpJars/slave.jar

 

주의할 것은 젠킨스는 마스터와 슬레이브 연동 시 서로의 자바 버전이 일치해야 한다는 점입니다. 현재 젠킨스 마스터는 컨테이너로 돌고 있고, 컨테이너 안에 들어가 확인해 보면 자바 버전은 1.8이고, 젠킨스 슬레이브 EC2의 자바 버전은 1.7입니다. (최신 Jenkins 컨테이너는 JAVA 1.8 / EC2 Amazon Linux 는 JAVA 1.7)

따라서 슬레이브 서버의 자바 버전을 1.8.0으로 올립니다.

$ sudo yum install -y java-1.8.0


$ sudo update-alternatives --config java

There are 2 programs which provide 'java'.
 Selection Command
-----------------------------------------------
*+ 1        /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/java
  2      /usr/lib/jvm/jre-1.8.0-openjdk.x86_64/bin/java

Enter to keep the current selection[+], or type selection number: 2


앞의 터미널과 같은 메시지가 나타나면 2번 jre-1.8.0을 입력해 설치합니다.

다음 슬레이브 EC2의 자바 버전을 확인합니다. 마스터와 동일한 1.8.0이 설치됐습니다.

$ java -version
openjdk version "1.8.0_171"
OpenJDK Runtime Environment (build 1.8.0_171-b10)
OpenJDK 64-Bit Server VM (build 25.171-b10, mixed mode)


마지막으로 터미널의 slave.jar 파일이 있는 위치에서 worker01 에이전트 실행 명령을 복사해 입력하고 실행합니다 이때 다음과 같이 명령어 앞에 반드시 sudo를 붙이며, 백그라운드로 실행되도록 끝에는 &를 붙입니다. (-secret 뒤에 키값은 자신의 키를 입력합니다.)


$ sudo java -jar slave.jar -jnlpUrl http://<Jenkins master IP>:8080/computer/worker01/slave-agent.jnlp -secret <자신의 SECRET KEY> &


정상 연결되면 다음과 같은 메시지가 출력됩니다.


다시 젠킨스 노드 목록을 확인해 봅시다. 노드 상태에서 x 표시가 사라지고 worker01 노드가 정상적으로 실행되고 있을 겁니다.



  [TIP] 만약 다음과 같은 에러가 발생한다면?

java.io.IOException: Failed to load http://13.124.153.147:8080/computer/worker01/slave-agent.jnlp?encrypt=true: 404 Not Found
at hudson.remoting.Launcher.parseJnlpArguments(Launcher.java:400)
at hudson.remoting.Launcher.run(Launcher.java:248)
at hudson.remoting.Launcher.main(Launcher.java:218)
Waiting 10 seconds before retry


이 경우 secret key 값을 확인해야 합니다.



이제 [Jenkins] → [docker_cicd 아이템] → [구성] → [General] 탭으로 가서 빌드할 노드를 worker01로 지정합니다.

다음 그림과 같이 [Restrict where this project can be run]을 체크하고 [Label Expression]에 docker를 입력하고 저장합니다.


지금부터는 처음 awskrug-docker-default 서버로 가서 Dockerfile과 node.js 소스 코드를 git에 Push하고 트리거가 제대로 작동되는지 확인하겠습니다.

먼저 깃 계정 설정을 합니다. 자신의 계정으로 바꿉니다.

$ git config --global user.email "west0706@gmail.com"
$ git config --global user.name "west0706"


git clone된 폴더로 이동합니다.

$ cd ~/awskrug-docker
$ git add .
$ git commit -m "push container"
$ git push origin master (github Username, Password 입력)


깃으로 Push가 되면 젠킨스 화면 왼쪽 Build History에 빌드가 실행될 것입니다. 콘솔 출력으로 들어가 보면 깃에서 Dockerfile과 node.js 소스 코드를 받고 컨테이너를 빌드하는 과정이 출력됩니다.

컨테이너가 생성될 때 같은 이름의 기존 컨테이너가 있다면 지우고 만들어 줍시다.




젠킨스 슬레이브 서버에서 docker ps 명령으로 실행중인 컨테이너 목록을 출력해 컨테이너 생성을 확인합니다. 젠킨스 빌드넘버를 컨테이너의 Tag로 붙였기 때문에 생성된 이미지는 est0706/node_app:1이고 컨테이너 이름은 node_app-v1입니다.


웹 브라우저로 배포된 컨테이너에 접속해 봅니다. 주소창에 http://<Slave Host public IP>:8000를 입력합니다. 그러면  Node.js 컨테이너가 Hello World를 출력합니다.

node.js에서 Hello World 출력 문구를 바꿔서 다시 git push를 수행하고, 바뀐 컨테이너가 잘 동작하는지 확인해 봅니다. 웹브라우저가 캐쉬를 하고 있을 수도 있으므로 새로운 탭을 열어 확인하거나 다른 브라우저에서 접속해 봅니다.

또 깃허브에 가서 소스 코드가 정상적으로 올라갔는지도 확인합니다.



 [한빛미디어 도커Docker 도서 보러가기]