도커 스웜 구성과 CI/CD 실습



앞서 젠킨스Jenkins, 깃Git, 도커 허브Docker Hub를 이용하여 지속적인 통합/배포(CI/CD) 환경을 구현했다면, 이 장에서는 CI/CD 개발환경을 도커 엔진 그룹을 단일 가상 도커 엔진으로 묶는 클러스터링 기술인 도커 스웜Docker Swarm으로 구축해 봅니다.



이 장의 내용

  • 도커 스웜을 이용한 CI/CD 개발 환경을 실습합니다.

  • 스웜 클러스터의 무중단 업데이트를 구현해 봅니다.



지금까지는 하나의 호스트에서 도커를 사용했습니다. 그러나 실제 운영환경에서는 하나의 호스트로 컨테이너를 운영하기는 어렵습니다. 1대의 호스트 서버를 운영하다가 자원(CPU, 메모리, 저장공간)이 부족해지면 해당 호스트를 업그레이드하면 됩니다. 그러나 그럴 경우 서버 다운타임을 가져야 하기 때문에 매번 그렇게 대응할 수는 없습니다.

만약, 호스트를 클러스터로 묶어 자원을 병렬로 확장하면 문제는 쉽게 해결됩니다. 이러한 구조는 호스트 자원이 부족할 때마다 한대씩 호스트를 클러스터에 붙이는 구조이므로 자원을 매우 효율적으로 사용할 수 있습니다. 우리가 말로만 듣던 컨테이너 오케스트레이션 도구는 이러한 고민을 해결해주기 위해 나왔다고  볼 수 있습니다. 여러 가지 오케스트레이션 도구 중 여기서는 도커에서 공식 제공하는 도커 스웜Docker Swarm을 살펴봅니다.


도커 스웜은 크게 두 가지로 나뉩니다. 하나는 버전 1.6 이후부터 사용 가능한 '컨테이너로서의 스웜'이고, 다른 하나는 1.12 이후 버전부터 사용할 수 있는 '도커 스웜 모드'입니다.

그중에서도 도커 스웜 모드는 호스트에 도커만 설치돼 있으면 에이전트를 설치할 필요 없어 세팅과 사용이 무척 쉽습니다. 그래서 여기서는 도커 스웜 모드로 실습을 합니다.

Swarm의 사전적 의미는 '군중'입니다. 도커 1.12 버전에서 스웜이 스웜 모드로 바뀌었지만 그냥 스웜이라고도 부릅니다. 스웜 클러스터 자체도 스웜이라고 합니다. 즉, "스웜을 만들다."와 "스웜에 가입하다."는 결국 "클러스터를 만들다."와 "클러스터에 가입하다."와 같은 의미인 것입니다.


  • 노드node: 스웜 클러스터에 속한 도커 서버의 단위입니다. 보통 한 서버에 하나의 도커 데몬만 실행하므로 서버가 곧 노드라고 보면 됩니다. 노드는 크게 매니저 노드와 워커 노드가 있습니다.

  • 매니저 노드manager node : 스웜 클러스터 상태를 관리하는 노드입니다. 매니저 노드는 곧 워커노드가 될 수 있으며, 스웜 명령어는 매니저 노드에서만 실행됩니다.

  • 워커 노드worker node : 매니저 노드의 명령을 받아 컨테이너를 생성하고 상태를 체크하는 노드입니다.

  • 서비스service : 기본 배포 단위입니다. 하나의 서비스는 하나의 이미지로 생성하고 동일한 컨테이너를 한개 이상 실행할 수 있습니다.

  • 테스크task : 컨테이너 배포 단위입니다. 하나의 서비스는 여러 개의 태스크를 실행할 수 있고, 각각의 테스크가 컨테이너를 관리합니다.


도커 스웜 설치를 위한 사전 작업

  • 각각의 노드에는 도커가 설치돼 있어야 합니다.

  • 스웜 매니저는 기본적으로 TCP 2377 포트를 사용합니다.

  • 각각의 노드는 통신을 위해 다음 포트들을 사용합니다.

    • TCP/UDP 7946 : Node 간의 통신에 사용합니다.

    • TCP/UDP 4789 : 오버레이 네트워크Overlay Network 트래픽에 사용합니다.


도커 스웜 클러스터 구성하기

우리가 구성할 도커 스웜 클러스터의 구조는 다음과 같습니다.



먼저, 이전 장에서 젠킨스 슬레이브로 사용했던 EC2를 스웜 매니저로 사용할 것입니다. 스웜 워커노드는 여러 대로 구성이 가능하나 여기서는 편의상 1대만 준비하겠습니다. 따라서 다음과 같이 스웜 워커노드가 될 EC2를 1대 더 준비합니다.


스웜 워커 노드용 EC2 생성

VPC

Default VPC

Subnet

Default subnet

Public IP

Enable (기본)

Name

awskrug-docker-swarm-worker

AMI

Amazon Linux AMI 2018.03.0 (HVM)

Instance Type

t2.medium

Security group

default

keypair

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


따라서 EC2는 총 3대가 되었습니다(젠킨스 마스터 / 젠킨스 슬레이브(스웜 마스터Swarm Master) / 스웜 워커Swarm Worker).


새로 추가한 스웜 워커 노드용 EC2에도 도커를 설치합니다.

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


도커 버전 확인하기

앞서 밝혔듯 스웜 모드는 에이전트가 필요 없지만 도커 버전이 1.12 이상이어야 사용할 수 있습니다.

$ docker -v
Docker version 17.12.1-ce, build 3dfb834


스웜 매니저 노드 정의

먼저 다음 명령으로 스웜 매니저 노드Swarm Manager node를 정의합니다. 다음 매니저 노드Manager node에서 생성된 토큰Token을 통해 워커 노드Worker node에서 매니저 노드로  조인join을 합니다. 이때 조인하기 위해서는 매니저 노드의 2377 TCP 포트가 열려 있어야 합니다.

# Manager node

$ docker swarm init --advertise-addr <Manager Node Private IP>
Swarm initialized: current node (mzs7v623fyb70tppgv19tksjz) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-347j70hctdutkn6osn0qk4yecxm97nfmtuqd7bng39x3s488mx-0ct8pc5u2t560cyxlg29md578 172.31.28.248:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions

# Worker node 01
Swarm Manager 노드에서 실행한 docker swarm join --token <토큰값> 커멘드를 복사해서 Swarm Worker


노드에서 다음 명령을 실행합니다.

$ docker swarm join --token SWMTKN-1-347j70hctdutkn6osn0qk4yecxm97nfmtuqd7bng39x3s488mx-0ct8pc5u2t560cyxlg29md578 172.31.28.248:2377

This node joined a swarm as a worker.


앞과 같은 메시지가 나오면 워커 노드가 정상적으로 조인한 것입니다. 이제 매니저 노드에서 워커 노드가 정상적으로 조인됐는지 확인해 봅니다.

# Manager node

$ docker node ls
ID                            HOSTNAME                     STATUS AVAILABILITY MANAGER STATUS
8wphdjm9weei0qh8ubf3oj2xa     ip-172-31-30-166.ap-northeast-2.compute.internal   Ready               Activ
mzs7v623fyb70tppgv19tksjz *   ip-172-31-28-248.ap-northeast-2.compute.internal   Ready               Active Leader


서비스 생성하기

스웜 모드에서는 컨테이너 제어 단위를 서비스별로 구분합니다. 서비스는 같은 이미지에서 생성된 컨테이너의 집합입니다. 서비스에 명령을 내리면 해당 서비스에 포함된 컨테이너에 같은 명령이 실행됩니다.

컨테이너는 워커 노드, 매니저 노드에 할당됩니다. 이런 컨테이너를 테스크Task라고 부릅니다.

nginx 이미지를 통해 서비스 컨테이너를 생성해 보겠습니다. 참고로 서비스 제어 도커 명령어는 모두 매니저 노드에서만 실행할 수 있습니다.

$ docker service create --name my_nginx -p 80:80 nginx:latest
t7cck0nssur0m81iyaofz7fha
overall progress: 1 out of 1 tasks
1/1: running   
verify: Service converged


앞의 명령을 실행하면 nginx 컨테이너 1대가 생성될 것입니다. 스웜 모드에서 서비스 컨테이너를 생성하려면 run이 아니라 service create 명령을 사용해야 합니다. --name 으로 서비스 명을 지정하고, -p 를 통해 호스트의 80번 포트를 서비스의 80번과 매핑합니다.

생성된 서비스를 확인해 봅시다. 서비스는 1개만 생성했으므로 my_nginx는 하나만 나옵니다.

$ docker service ls
ID              NAME MODE            REPLICAS IMAGE PORTS
t7cck0nssur0    my_nginx replicated      1/1 nginx:latest *:80->80/tcp


my_nginx 서비스에서 실행되고 있는 컨테이너는 다음 명령어로 확인할 수 있습니다. 서비스의 기본 정보 이외에도 이 컨테이너가 어떤 노드에서 실행되고 있는지도 알 수 있습니다.

$ docker service ps my_nginx
ID           NAME IMAGE        NODE DESIRED STATE  CURRENT STATE ERROR PORTS
rqqq798n9yzi my_nginx.1 nginx:latest ip-10-0-0-62  Running Running 7 minutes ago 

  

우선 먼저 띄워던 nginx 서비스와 컨테이너를 삭제합니다. 참고로 docker rm 명령은 컨테이너가 실행중이면 먼저 중단하고 삭제하지만, service rm은 바로 컨테이너를 삭제합니다.

$ docker service rm my_nginx


이번에는 --replicas <컨테이너 수> 옵션을 추가 해당 서비스에서 여러 개의 컨테이너를 띄워봅니다. nginx 컨테이너를 4개 생성해 봅니다.

$ docker service create --name my_nginx --replicas 4 -p 80:80 nginx:latest
ysygwq8v3x82y11egq85miufp

overall progress: 4 out of 4 tasks
1/4: running   [==================================================>]
2/4: running   [==================================================>]
3/4: running   [==================================================>]
4/4: running   [==================================================>]
verify: Service converged


각 EC2 호스트에서 확인하면 컨테이너가 호스트 서버마다 균일하게 생성돼 있습니다. my_nginx 서비스에서 실행된 컨테이너를 확인해 봅니다.

$ docker service ps my_nginx

ID              NAME IMAGE           NODE DESIRED STATE CURRENT STATE
mpkskd0o3z2m    my_nginx.1 nginx:latest    ip-10-0-0-193 Running Running 6 minutes.               wp5atdc2r5ud my_nginx.2 nginx:latest ip-10-0-0-62 Running         Running about a min..
rt3fbdjv9fdk    my_nginx.3 nginx:latest    ip-10-0-0-62 Running Running 6 minutes..           6vqc50yfn9ri my_nginx.4 nginx:latest ip-10-0-0-193  Running Running about a min..


서비스 확장하기

도커 스웜을 이용하면 서비스를 쉽게 확장할 수 있습니다. 이번에는 scale 명령을 이용해 컨테이너를 8개로 늘려봅니다. 스케일 인/아웃을 하는 다음 명령을 실행합니다.

$ docker service scale my_nginx=8



8개의 컨테이너가 각 호스트 노드에 균일하게 나뉘어져 생성됐습니다.


inspect 커맨드를 통해 스웜 서비스 정보를 확인합니다. --pretty 옵션을 주지 않으면 JSON 형태로 출력됩니다.

$ docker service inspect --pretty my_nginx



네트워크

도커 스웜 모드에서는 여러 개의 호스트가 클러스터로 묶여 있으며, 같은 컨테이너를 골고루 분산 배치해야 합니다. 이 때문에 각 호스트의 도커 데몬의 네트워크를 하나로 묶을 수 있는 네트워크 풀이 필요합니다. 또한 외부에 서비스를 노출했을 때 어떤 호스트 노드로 접근하더라도 같은 서비스 컨테이너에 접근하는 라우팅 기능도 필요합니다. 스웜 모드는 자체 네트워크 드라이버를 통해 이들 기능을 제공합니다.


먼저 매니저 노드에서 도커 스웜 네트워크를 확인합니다.

$ docker network ls
NETWORK ID       NAME             DRIVER           SCOPE
2d31ef818e63     bridge           bridge           local
9a840ecb203a     docker_gwbridge bridge           local
0dcd3cff5401     host             host             local
43nsbrhm74no     ingress          overlay          swarm
aaa0941791a0     none             null             local


docker_gwbridge 는 스웜 오버레이overlay 네트워크 사용 시 사용되고, ingress 네트워크는 로드밸런싱과 라우트 메시Rouing Mesh에 사용됩니다.

ingress 네트워크는 스웜 모드에서 클러스터 생성 시 모든 노드에 자동으로 생성됩니다. 사용자가 어떤 노드로 접근하더라도 서비스 내의 컨테이너에 접근하게 해주며 라운드 로빈 방식으로 클러스터를 로드밸런싱합니다.

앞서 생성한 my_nginx 서비스를 삭제해 컨테이너를 정리합니다.

$ docker service rm my_nginx


다음 명령에서 미리 만든 이미지는 nodejs로 현재의 서버 호스트의 이름을 보여주는 예제입니다. 이 이미지로 컨테이너 4대를 띄우고 웹 브라우저에서 호스트 이름이 로드밸런싱되는지 직접 확인해 봅시다.

웹 브라우저에 나오는 호스트 이름이 바뀐다면 제대로 동작하고 있는 것입니다.

$ docker service create --name my_host --replicas 4 -p 80:8000 west0706/node_app:hostname


웹 브라우저로 호스트 서버에 접속해보면 컨테이너가 로드밸런싱됨을 확인할 수 있습니다.

스웜의 ingress 오버레이overlay 네트워크 때문에 2개의 호스트 중 어디로 들어가도 4개의 컨테이너 호스트 이름이 라운드 로빈 방식에 의해 랜덤하게 출력됩니다.

     


서비스 롤링 업데이트

기존 컨테이너를 새로운 이미지로 교체하면서 컨테이너를 업데이트해야 할 때 기존에는 컨테이너를 중지하고 새로운 컨테이너를 띄워야만 했습니다. 이때 서비스는 잠깐씩 중단됩니다. 하지만 도커 스웜 모드는 롤링 업데이트를 자체적으로 지원해 무중단 업데이트가 가능합니다.

사용법은 매우 간단합니다. 현재 서비스 되고 있는 my_host 서비스의 컨테이너 이미지는 다음 그림 같이 node_app:hostimage 이미지로 업데이트해 보겠습니다.

$ docker service update --image west0706/node_app:hostimage my_host



롤링 업데이트가 되는 동안 웹 브라우저에 접속해서 F5 키를 마구 눌러 새로고침을 합니다. 그러면 전체 컨테이너가 무중단 상태로 하나하나씩 업데이트되는 것을 직접 확인할 수 있습니다.


도커 스웜 클러스터 무중단 롤링 업데이트

이제 앞서 구성한 젠킨스Jenkins 설정을 조금 바꿔서 깃에서 소스 코드가 변경돼 Push되면 변경된 node.js 웹 서버 이미지를 현재 실행되고 있는 도커 스웜 서비스의 my_host에 업데이트돼 스웜 클러스터 내의 컨테이너가 무중단으로 배포되도록 해보겠습니다.

구성은 다음과 같습니다.


앞서 만든 젠킨스의 docker_cicd 프로젝트로 이동합니다. 구성에서 [Build] 탭에서 Excute shell의 내용을 지우고 다음 코드를 붙여넣습니다. 여기서 <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}

# rolling update swarm cluster
docker service update --image $ID/node_app:${BUILD_NUMBER} my_host


Excute shell에 내용을 입력했다면 아래의 [저장] 버튼을 클릭합니다. 이제 처음 젠킨스Jenkins 마스터 컨테이너(awskrug-docker-default)가 있었던 EC2에 터미널로 접속합니다. 

맨 처음에 git clone을 했던 폴더로 이동합니다.

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


server.js 파일을 수정합니다.

$ cd node
$ vi server.js


기존 hello world 출력 소스 코드를 지우고 다음 코드를 넣은 후 저장합니다.

'use strict';

const express = require('express');
const os = require('os');
var hostname = os.hostname();
const PORT = 8000;
const HOST = '0.0.0.0';

const app = express();
app.get('/', (req, res) => {
 res.send("<html><head><h1>hostname : " + hostname +
    "</h1></head><body><img src='https://blog.docker.com/wp-content/uploads/Swarmnado-357x627-30-1.gif'></body></html>");
});

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


변경된 소스를 깃으로 다시 Push합니다.

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


젠킨스 빌드 히스토리 화면으로 이동해 빌드 상황을 확인합시다.


젠킨스 콘솔 출력에서 스웜 클러스터가 한대씩 롤링 업데이트되는 것을 직접 확인할 수 있습니다.


다음 화면을 예로 들면 스웜 클러스터의 실행중인 서비스의 이미지가 젠킨스 빌드 넘버 7인 node_app:7 이미지로 변경돼 실행되고 있습니다.

$ docker service ps my_host



웹 브라우저로 호스트 서버에 다시 접속하면 우리가 변경한 node.js 서버가 컨테이너에 반영돼 그림이 바뀐 것을 볼 수 있습니다.


도커 스웜 모드의 주요 명령어는 다음과 같습니다.

명령

옵션-- or 파라미터<>

설명

docker swarm init

--advertise-addr <매니저 노드 IP>

매니저 서버에서 스웜 클러스터를 시작

--advertise-addr 다른 도커 서버가 매니저 노드에 접근하기 위한 IP 주소

docker swarm join

--token <토큰 키> <매니저 노드 IP>

매니저 노드에서 init 명령 실행 시 결과값으로 나오는 명령.

워커 노드에서 이 명령을 실행하면 스웜 클러스터에 조인됨

docker swarm join-token


<manager | worker>

--rotate

매니저 노드에서 워커나 다른 매니저를 추가하기 위한 토큰을 받기 위해 실행

--rotate 옵션은 보안을 위해 토큰을 재발급함

docker swarm leave

--force

삭제하고 싶은 워커 노드에서 이 명령을 실행하면 클러스터에서 노드 상태를 Down으로 변경함. 워커 노드를 삭제하지는 않음

매니저 노드 삭제 시에는 --force 옵션 필요

docker node rm

<워커노드 ID or HOSTNAME>

매니저 노드에서 실행 시 워커노드 삭제

워커노드에서 먼저 leave 명령으로 Down 상태로 변경돼야 함

docker node ls


매니저 노드에서 스웜 클러스터로 묶여 있는 노드를 확인

목록중 *표가 붙어 있는 노드가 현재 서버임

docker node promote


워커 노드를 매니저 노드로 변경

docker node demote


매니저 노드를 워커 노드로 변경

매니저 노드가 1개일 때는 사용불가

매니저 노드가 2개 이상 일때 demote 명령 사용 시 다른 매니저 노드 중 새로운 리더Leader 노드 선출

docker service create

-p <서비스port:컨테이너port>

--name <서비스명>

--replicas <숫자>

--mode global

<도커이미지:Tag>

스웜 클러스터에 서비스 생성

--mode global 추가 시 클러스터의 사용 가능한 모든 노드에 컨테이너를 무조건 하나씩 생성(에이전트용 컨테이너 생성 시)

docker service ls


스웜 클러스터 내의 서비스 목록 출력

docker service ps

<서비스명>

서비스 내의 컨테이너 목록, 상태, 할당된 노드 위치 출력

docker service scale

<서비스명>=<숫자>

클러스터의 레플리카 수를 늘리거나 줄임

docker service update

--image <도커이미지:Tag>

<서비스명>

서비스 롤링 업데이트

docker service rm

<서비스명>

서비스 상태에 관계없이 서비스의 컨테이너 즉시 삭제

docker network ls


도커에서 사용하는 네트워크 목록 출력