도커로 올린 프로젝트를 도커스웜을 활용하여 다중 호스트 환경에서 배포한다.
도커스웜에서는 클러스터가 1개 이상의 매니저 노드를 가질 경우 리더로 선정된 매니저 노드가 클러스터를 실질적으로 관리한다. 이 경우 클러스터의 모든 변경사항은 리더 노드를 통해 전파되며, 나머지 노드들은 리더 노드와 동기화된 상태를 유지한다. 이는 뗏목 합의 알고리즘(feat. 정족수)에 기초한 것이기 때문에 매니저 노드 수를 홀수 개로 유지하는 것이 효율적이다. 왜 그런 것인지 궁금하다면 다음 유튜브 영상에 잘 설명되어 있으니 여기를 참고하자.
다른 작업들과 달리 노드를 제거할 때는 신중을 기해야 한다. 워커 노드를 바로 지워버렸는데 만약 가용한 다른 노드가 없다면 기존에 돌아가던 태스크들이 PENDING 상태로 멈춰버린다. 때문에 가급적이면 사전작업을 해주는 것이 좋다.
drain시킨 뒤, 해당 노드에 있던 태스크들이 다른 노드로 잘 옮겨졌는지 확인한다. (선택, 사전작업)docker service ps로 확인할 수 있다.docker swarm leave를 실행하여 노드와 클러스터의 연결을 끊는다.--force를 붙이면 1번 과정을 생략할 수 있다.docker swarm rm <node-id or hostname>을 실행한다.매니저 노드를 제거할 때는 워커 노드를 제거할 때보다도 훨씬 신중해야 한다. 사실 사전작업 하나만 추가하면 끝이라서 과정은 간단하다.
demote시킨다.(선택..?, 사전작업)매니저 노드를 제거할 때도 docker swarm leave --force를 사용하면 demote 과정까지 생략할 수 있다. 하지만 이 방법은 절대 지양해야 한다. 왜 그럴까?
클러스터 내부의 매니저 노드들 중 절반 이상이 죽어버리면 클러스터 전체가 관리 불가능해진다. demote를 생략하고 제거해버렸다가 이 정족수 커트라인에 걸리면 클러스터가 통째로 죽어버리는 것이다. 이 경우 클러스터를 초기화하는 수밖에 없다. 여기에 대해 자세히 궁금하다면 위에서 링크해둔 유튜브 영상을 꼭 시청해보자. 클러스터가 죽는 이유에 대해서도 자세히 다루고 있다.
도커스웜에서 매니저 노드로서 서비스 배포 관련한 여러 명령어를 사용할 수 있다.
# 서비스 생성
# --name이나 --replicas 등의 옵션을 부가할 수 있다.
docker service create <image_name>:<tag>
# 도커 스택(컴포즈) 파일을 기반으로 서비스 배포
docker stack deploy -c <compose_file> <service_name>
# 클러스터에서 구동중인 서비스 목록 조회
docker service ls
# 특정 서비스에 포함된 태스크 목록 조회
docker service ps <service-name or id>
# 서비스 로그 조회 (-f 옵션 사용 가능)
docker service logs <service-name or id or task-id>
# 서비스 제거
docker service rm <service-name or id>
# 서비스 스케일링
# 이미 배포된 서비스의 레플리카 수를 조정할 수 있다.
docker service scale <service-name or id>=<number-of-task>
이외의 자세한 내용은 아래 포스트를 참고하자.
Docker Swarm에서 서비스(Service) 생성하고 다루기
# 현재 호스트를 매니저 노드로 선정하여 도커스웜 클러스터 구축
docker swarm init
# 특정 IP를 가진 호스트를 매니저 노드로 선정하여 도커스웜 클러스터 구축
docker swarm init --advertise-addr <manager-node-ip>
# 현재 호스트로 특정 도커 스웜에 참여
docker swarm join --token <token>
# 도커 정보에서 스웜 활성화 여부 확인
# -A <line-count>를 추가하여 세부 정보 확인 가능
docker info | grep Swarm
# 도커 클러스터에 존재하는 노드 목록 조회
docker node ls
# 워커 노드 추가 명령어 요청(--quiet 사용 시 토큰 정보만 출력)
docker swarm join-token worker
# 매니저 노드 추가 명령어 요청(--quiet 사용 시 토큰 정보만 출력)
# 매니저 노드에서만 실행 가능
docker swarm join-token manager
# 특정 노드 상세정보 출력(--pretty 사용 시 출력이 보기 좋아짐)
# 실행하는 노드의 정보를 출력하려면 Node-ID 대신 self 사용
docker node inspect <node-id or hostname>
# 워커 노드를 매니저 노드로 승격(promote)
# 노드명을 여러 개 명시하여 여러 노드 동시 처리 가능
docker node promote <node-id or hostname>
# 매니저 노드를 워커 노드로 강등(demote)
# 노드명을 여러 개 명시하여 여러 노드 동시 처리 가능
docker node demote <node-id or hostname>
# <rule>에 manager 혹은 worker를 명시하여 역할 변경 가능
# promote, demote와 달리 한 번에 하나의 노드만 처리 가능
docker node update --role <rule> <node-id or hostname>
# 노드 상태 변경(active, pause, drain)
docker node update --availability <status> <node-id or hostname>
# 노드에 라벨 추가(--label-add <key or key=value>를 여러 개 사용 가능)
docker node update --label-add <key or key=value>
# 노드 정보에서 라벨 내용 확인(--pretty 사용 시 출력이 보기 좋아짐)
# 라벨 수에 따라 <line-count> 조절
docker node inspect <node-id or hostname> | grep -i Labels -A <line-count>
# 노드에서 라벨 제거
docker node update --label-rm <key or key=value>
도커스웜에서는 이전에 사용했던 도커 컴포즈 파일을 그대로 사용할 수 있다. 대신 배포 관련 설정을 추가해주기 위해 deploy-replicas 부분만 추가로 작성했다. 서비스 시작 시 db는 레플리카 1개, was는 레플리카 2개를 가진다.
version: "3.9"
services:
mysql:
build: ./mysql_docker
volumes:
- db:/var/lib/mysql
image: songsunkook/roomescape_db:latest
ports:
- 3307:3306
restart: unless-stopped
container_name: roomescape_db
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
networks:
- roomescape
deploy:
replicas: 1
spring:
depends_on:
- mysql
build: ./spring_docker
image: songsunkook/roomescape_was:latest
ports:
- 8081:8080
restart: unless-stopped
container_name: roomescape_was
networks:
- roomescape
deploy:
replicas: 2
volumes:
db: {}
networks:
roomescape: {}
도커가 설치된 ec2 두 개를 준비하고, 각각 아래의 인바운드 설정을 추가했다. 각 포트는 원활한 Swarm 동작을 위해 개방되어야 한다. 다만 이 포트들이 외부에 노출되면 위험하니 꼭 신뢰할 수 있는 IP에 대해서만 개방해야 한다! 여기서는 간편한 실습을 위해 모든 IP에 대해 개방했지만 가급적 지양하자.
이제 매니저 노드로 사용할 호스트에서 아래 명령어를 입력하여 도커스웜 클러스터를 구축한다.
docker swarm init
위 명령어를 입력하면 도커는 워커 노드로 사용할 호스트에서 사용해야 할 명령어를 제공해준다. 제공받은 토큰과 함께 워커 노드로 사용할 호스트에서 명령어를 입력한다. (init은 클러스터를 구축하는 명령어이기 때문에 워커노드에서는 사용하지 않는다)
docker swarm join --token <token>

워커 노드가 합류한 후 매니저 노드에서 docker node ls를 사용하니 새로운 워커 노드가 정상적으로 합류한 모습을 확인할 수 있다.
HOSTNAME을 예쁘게 바꾸고 싶어요
HOSTNAME을 바꾸고자 하는 호스트에서hostnamectl set-hostname <name>을 사용해서 바꿔줄 수 있다.
아직 서비스를 배포하기에 앞서 선행되어야 할 일이 있다. 바로 이미지를 푸시하는 것이다. 현재 Stack을 확인해보면 Dockerfile에서 공식 image에 추가 작업을 진행하여 새로운 이미지를 빌드한다. 도커스웜에서는 이 빌드된 이미지를 배포하는 것인데, 빌드는 (리더)매니저 노드만 진행하므로 이를 제외한 다른 노드는 빌드된 이미지가 로컬에 존재하지 않는다. 결국 배포가 정상적으로 이루어지지 못해 매니저 노드 한 곳에서만 모든 컨테이너가 올라가는 일이 발생한다.
이런 문제를 방지하려면 워커 노드에서는 이미지를 빌드하지 않고 바로 불러올 수 있어야 한다. 여기에 도커허브(Docker Hub)를 활용할 수 있다. 매니저 노드에서 빌드를 마친 이미지를 푸시(Push)한 후 워커 노드에서는 이미지를 풀(Pull)받아와서 실행하는 것이다.
적용 방법은 다음과 같다.
1. 도커 허브에 로그인한다.
2. 도커 컴포즈로 이미지를 빌드한다.
3. 도커 허브에 빌드한 이미지를 푸시한다.
4. Stack 파일의 image 속성을 도커 허브에 올린 이미지 이름 및 태그와 일치시킨다.
1. ex) image: songsunkook/roomescape_was:latest
5. 매니저 노드에서 서비스를 실행한다.
4번은 최초 1회만 진행하면 되지만 1~3번은 서비스를 실행할 때마다 사전작업으로 가져가야 한다. 이 과정이 번거롭기 때문에 다음 쉘 스크립트로 분리하여 서비스 시작 전 간단하게 실행할 수 있도록 정리했다.
# Docker Hub 로그인
docker login -u <dockerhub_username> -p <dockerhub_password>
# Docker Compose를 사용하여 이미지 빌드
docker-compose build
# 도커스웜에서 사용할 서비스 이미지 푸시
docker push songsunkook/roomescape_db:latest
docker push songsunkook/roomescape_was:latest

푸시된 이미지들이 개인 도커허브에 저장된 모습이다.
이제 준비가 완료되었으니 서비스를 배포해보자.
# 이미지 빌드 및 푸시(사전작업)
sh build.sh
# 서비스 시작
docker stack deploy -c <compose-file-name> <service-name>

서비스를 시작하니 Stack에 정의해둔 레플리카 수에 맞게 서비스가 정상적으로 시작된 것을 확인할 수 있다.

또한 각 노드에 레플리카가 분산되어 올라간 것을 확인할 수 있다.
docker service scale <service-name or id>=<number-of-task>을 사용하면 서비스 배포 이후에도 레플리카 수를 조절하여 서비스 스케일링을 진행할 수 있다.

기존에 spring 레플리카가 2개 올라가있던 초기 상황에서 스케일을 5로 늘리니 3개의 레플리카가 추가로 각 노드에 분산되어 올라간 것을 확인할 수 있다.
왜 매니저 노드에도 클러스터의 작업이 할당되는 건가요?
기본적으로 매니저 노드는 워커 노드의 역할을 병행할 수 있다. 하지만 매니저 노드의 안정성을 고려한다면 매니저 노드의 역할을 워커 노드와 분리하는 것도 가능하다. 이 경우 매니저 노드를Drain상태로 만들면 된다.
docker node update --availability drain <node-name or id>
Getting started with Swarm mode
뗏목 타고 합의 알고리즘 이해하기
Docker Swarm의 주요 용어, 활성화 방법 및 노드(Node) 관리법 살펴보기
도커스웜 기초 및 예제
Docker Swarm에서 서비스(Service) 생성하고 다루기