Profile picture

[Docker Swarm] 도커 스웜(Docker Swarm) 알아보기

JaehyoJJAng2024년 04월 01일

▶︎ Docker Swarm

image

도커는 단일 호스트 안에서 컨테이너 기반 애플리케이션을 관리할 때 매우 유용한 도구이다.

하지만 단일 호스트로 구성된 환경은 확장성(Scalability), 가용성(Availability), 장애 허용성(Fault Tolerance) 측면에서 많은 한계점을 가지고 있다.

만약 서비스의 트래픽이 늘어나게 되면서 호스트의 가용 시스템 자원이 바닥나게 된다면?

혹은 어떠한 이유로 호스트 서버가 일시적으로 작동이 불가능하다면?

서비스가 바로 중단되는 비극적인 상황을 맞이하게 될 것이다. 따라서 개인 블로그 또는 포트폴리오용 정도의 작은 규모가 아닌 이상,
대부분의 상황에서는 여러 대의 호스트를 함께 운영하여 이러한 상황에 대비하게 된다.


그러나 여러 대의 호스트에서 컨테이너를 관리하려니, 도커 자체만으로는 해갈하기 어려운 또 다른 문제를 마주하게 된다.

  • 서로 다른 각각의 호스트(서버)들을 어떻게 연결할 것인가?
  • 어떤 컨테이너를 어느 호스트에 배치하여 구동시킬 것인가?
  • 각기 다른 호스트에 배치된 컨테이너들의 상호 통신은 어떻게 제어되는가?

위와 같은 문제들을 해결하기 위해 나타난 것이 "컨테이너화된 애플리케이션에 대한 자동화된 설정" 즉, **컨테이너 오케스트레이션(Container Orchestration)**이다.

도커 역시 자체적으로 컨테이너 오케스트레이션을 위한 도구를 만들어놨다. 바로 이번에 공부할 **도커 스웜(Docker Swarm)**이다.


‣ 왜 도커스웜?

현재 도커 스웜은 개발 단계가 아닌 유지보수 단계에 접어들었다.

클라우드 컴퓨팅 회사 미란티스(Mirantis)가 2019년 도커의 엔터프라이즈 플랫폼 사업을 인수한 이래로 도커 스웜의 장래가 불투명해졌다.

쿠버네티스가 컨테이너 오케스트레이션에 관한 사실상 표준 기술로 자리 잡은 지금 상태에서 도커 스웜은 더 이상 매력적인 선택지가 아니다.

그럼에도 불구하고, 나는 아래의 장점들이 마음에 들어 도커 스웜을 공부해보고 싶어졌다.

  • 여러 대의 호스트로 구성된 중소 규모의 클러스터에서 컨테이너 기반 애플리케이션 구동을 제어하기에 충분한 기능을 가지고 있음.
  • 도커 엔진(Docker Engine)이 설치된 환경이라면 **별도의 구축 비용 없이 스웜 모드(Swarm Mode)**를 활성화 하는 것만으로 시작 가능
  • 도커 컴포즈(Docker Compose)를 사용해봤다면 도커 스웜(Docker Swarm)의 스택(Stack)을 이용한 애플리케이션 운영에 적응이 쉬움.
  • 클러스터 관리와 배포가 가능한 단일 노드 클러스터를 만들 수 있음.
    • 최소한의 자원으로 컨테이너 오케스트레이션 환경을 테스트해볼 수 있음

‣ 주요 용어

도커 스웜에서 흔하게 사용되는 주요 용어들의 개념을 먼저 살펴보도록 하자.

용어 설명
노드(Node) 클러스터를 구성하는 개별 도커 서버를 의미
매니저 노드(Manager Node) 클러스터 관리와 컨테이너 오케스트레이션을 담당함.
쿠버네티스의 마스터 노드(Master Node)와 같은 역할
워커 노드(Worker Node) 컨테이너 기반 서비스(Service)들이 실제 구동되는 노드를 의미
쿠버네티스와 다른점이 있다면, 도커 스웜에서는 매니저 노드(Manager Node)도 기본적으로 워커 노드(Worker Node)의 역할을 같이 수행할 수 있다는 점이다.
스케줄링을 임의로 막는 것도 가능하다.
스택(Stack) 하나 이상의 서비스(Service)로 구성된 다중 컨테이너 애플리케이션 묶음을 의미함.
도커 컴포즈와 유사한 양식의 YAML 파일로 스택 배포를 진행
서비스(Service) 노드에서 수행하고자 하는 작업들을 정의해놓은 것으로 클러스터 안에서 구동시킬 컨테이너 묶음을 정의한 객체
도커 스웜에서는 기본적인 배포 단위로 취급된다.
하나의 서비스는 하나의 이미지를 기반으로 구동되며, 이들 각각이 전체 애플리케이션 구동에 필요한 개벌적인 마이크로서비스(Microservce)로 기능한다.
태스트(Task) 클러스터를 통해 서비스를 구동시킬 때, 도커 스웜은 해당 서비스의 요구 사항에 맞춰 실제 마이크로서비스가 동작할 도커 컨테이너를 구성하여 노드에 분배한다 이것을 태스크(Task)라고 한다.
하나의 서비스는 지정된 복제본(replica) 수에 따라 여러 개의 태스크를 가질 수 있으며, 각각의 태스크에는 하나씩의 컨테이너가 포함된다.
스케줄링(Scheduling) 도커 스웜에서의 스케줄링은 서비스 명세에 따라 태스크(컨테이너)를 노드에 분배하는 작업을 의미한다.
2022년 8월 기준으로 도커 스웜에서는 오직 균등 분배(spread)방식만 지원하고 있다.
물론 노드별 설정 변경 또는 라벨링(Labeling)을 통해 스케줄링 가능한 노드의 범위 제한도 가능하다.

‣ 스웜 모드 구조

스웜 모드는 매니저 노드와 워커 노드로 구성된다.

워커 노드는 실제로 컨테이너가 생성되고 관리되는 도커 서버이고,

매니저 노드는 워커 노드를 관리하기 위한 서버이다. 매니저 노드에도 컨테이너가 생성될 수 있으며 워커 노드의 역할을 포함한다.
image


매니저 노드는 1개 이상이 필수이다. 반면에 워커노드는 없을 수도 있다.

매니저 노드가 워커 노드의 역할도 포함하고 있기 때문에 매니저 노드 만으로 스웜 클러스터 구성이 가능하다는 애기이다.

그러나 워커노드와 매니저 노드는 구분하는 것을 권장한다.

또한 매니저 노드는 안정성을 위해 한 개 이상으로 다중화하는 것을 권장하고 있다.

매니저 노드에 문제가 생기게 되면 매니저 노드가 복구될 때까지 클러스터 운영이 중단되기 때문에 서비스에 큰 지장이 생긴다.


▶︎ 스웜 모드 서비스

도커 명령어의 제어 단위는 컨테이너 기준이다. 그러나 스웜에서는 Service 단위로 제어한다.

서비스는 같은 이미지에서 생성된 컨테이너의 집합이며, 서비스를 제어하면 서비스 내의 컨테이너에 같은 명령이 수행된다.

서비스 내에 컨테이너는 1개 이상 존재할 수 있으며, 이러한 컨테이너들을 태스크(Task)라고도 한다.


image
예를 들어, 위 그림처럼 컨테이너 수를 4개로 설정했다고 가정해보자.

스웜 스케줄러는 서비스의 정의에 따라 컨테이너를 할당할 적합한 노드를 선정하고, 해당 노드에 컨테이너를 분산해서 할당한다.

이처럼 함께 생성된 컨테이너를 레플리카(Replica)라고 하며, 서비스에 설정된 레플리카 수 만큼의 컨테이너가 스웜 클러스터 내에 존재해야 한다.

스웜은 서비스의 컨테이너들에 대한 상태를 계속 모니터링하고 있다가 서비스 내에 정의된 레플리카의 수 만큼 컨테이너가 스웜 클러스터에 존재하지 않으면 새로운 컨테이너 레플리카를 생성한다.

또한 서비스 내의 컨테이너 중 일부가 작동을 멈춰 정지한 상태로 있다면, 이 또한 레플리카의 수를 충족하지 못하는 것으로 판단하여

스웜 매니저는 새로운 컨테이너를 클러스터에 새롭게 생성한다.

이를 그림으로 표현한다면 아래와 같다.
image


▶︎ 스웜 네트워크

스웜 모드의 네트워크는 도커 일반 네트워크랑은 조금 다른 구조로 사용된다.

스웜 모드는 여러 개의 도커 엔진에 같은 컨테이너를 분산해서 할당하기 때문에 각 도커 데몬의 네트워크가 하나로 묶인 네트워크 풀이 필요하다.

이뿐만 아니라 서비스를 외부로 노출했을 때 어느 노드로 접근하더라도 해당 서비스의 컨테이너에 접근할 수 있게 라우팅 처리가 되어야 한다.

이러한 네트워크 기능은 스웜모드가 자체적으로 지원하는 네트워크 드라이버를 통해 사용할 수 있다.

다음 명령어를 확인해보자.

docker network ls

NETWORK ID     NAME              DRIVER    SCOPE
46e6b65cedf0   bridge            bridge    local
9fa6e4cc536b   docker_gwbridge   bridge    local
42e30a18bcc5   host              host      local
rmg029v282bu   ingress           overlay   swarm
b974107b89a8   none              null      local

기존 네트워크에 docker_gwbridgeingress 네트워크가 생성된 것을 볼 수 있다.

docker_gwbridge 네트워크는 스웜에서 오버레이 네트워크를 사용할 때 사용되고,

ingress 네트워크는 로드 밸런싱과 라우팅 메시(Routing Mesh)에 사용된다.


‣ ingress 네트워크

ingress 네트워크는 스웜 클러스터를 생성하면 자동으로 등록되는 네트워크로, 스웜모드를 사용할 때에만 유효하다.

docker network ls 명령어를 입력하면 SCOPE 확인이 가능하다.

docker network ls | grep 'ingress'

rmg029v282bu   ingress           overlay   swarm

image
ingress 네트워크는 어떤 스웜 노드에 접근하더라도 서비스 내의 컨테이너에 접근할 수 있게 설정하는 라우팅 메시를 구성하고,

서비스 내의 컨테이너에 대한 접근을 라운드 로빈(round robin)방식으로 분산하는 로드밸런싱을 담당한다.


예를 들어, 아래와 같은 하나의 앱을 실행했다고 가정해보자.

docker service create --name test-app \
-p 80:80 \
--replicas=4 \
nginx:latest

4개의 컨테이너가 있는 환경에서 docker ps 명령으로 컨테이너의 ID를 확인해보면 아래와 같다.

# manager
docker ps --format "table {{.ID}}\t{{.Status}}\t{{.Image}}"

CONTAINER ID   STATUS              IMAGE
a5c0973f5b98   Up About a minute   nginx:latest
# worker1
docker ps --format "table {{.ID}}\t{{.Status}}\t{{.Image}}"

CONTAINER ID   STATUS         IMAGE
6c41ce64de1b   Up 2 minutes   nginx:latest
c3ce8ae5ae30   Up 2 minutes   nginx:latest
# worker2
CONTAINER ID   STATUS         IMAGE
be7b7a006213   Up 2 minutes   nginx:latest

각각의 컨테이너는 매니저에 1개 worker1에 2개 그리고 worker2에 1개씩 할당 되었다.

컨테이너 로그를 켜놓고 브라우저로 접근해서 확인해보면, 각각의 컨테이너로 접속이 분산되어 가는 것을 확인해 볼 수 있을거다.

하지만 이렇게 ingress로만 네트워크를 외부로 노출할 수 있는 것은 아니다.

docker service create \
--publish mode=host, target=80, published=8888, protocol=tcp \
--name nginx \
nginx:latest

다음과 같이 직접 컨테이너의 80번 포트에 연결할 수도 있다.

그러나 ingress 네트워크를 사용하지 않고 노출할 경우, 어느 호스트에서 컨테이너가 생성될지 알 수 없기에

포트 및 서비스 관리가 어렵다는 단점이 존재한다. 따라서 가급적 ingress 네트워크를 사용하도록 하자.


‣ overlay 네트워크

image
ingress 네트워크는 오버레이 네트워크를 사용한다.

오버레이 네트워크는 여러 개의 도커 데몬을 하나의 네트워크 풀로 만드는 네트워크 가상화 기술의 하나로서,

도커에 오버레이 네트워크를 적용하면 여러 도커 데몬에 존재하는 컨테이너가 서로 통신할 수 있다.

즉, 여러 개의 스웜 노드에 할당된 컨테이너는 오버레이 네트워크의 서브넷에 해당하는 IP 대역을 할당받고

해당 IP를 통해 서로 통신할 수 있는 것이다.


• 사용자 정의 오버레이 네트워크

스웜 모드는 자체 키-값 저장소를 갖고 있으므로 별도의 구성 없이 사용자 정의 오버레이 네트워크를 생성할 수 있다.

docker network create \
--subnet 172.24.0.0/24 \
-d overlay \
test-overlay

docker network ls로 확인해보면 다음과 같다.

docker network ls | grep 'test-overlay'

yqnmcoury7ku   test-overlay      overlay   swarm

‣ 서비스 디스커버리

같은 컨테이너가 여러 개 있을 때 가장 쟁점이 되는 부분은 새로 생성된 컨테이너의 발견, 없어진 컨테이너 감지 이 두 가지라고 볼 수 있다.

스웜 모드는 이를 주키퍼 같은 분산 코디네이터 없이 자체적으로 지원한다.

예를 들어, A 서비스가 B 서비스의 컨테이너를 사용할 때 scale out을 하여 컨테이너를 2개에서 3개로 늘렸다고 가정해보자.

그럼 A 서비스는 새로 추가된 컨테이너를 어떻게 추적할 수 있을까?

결론부터 말하면 스웜모드에서는 B라는 이름으로 서비스 B의 컨테이너에 모두 접근할 수 있다.

즉, 컨테이너의 IP를 알 필요없이 서비스의 이름만 알면 되는 것이다.

각 서비스 이름은 그러면 어떻게 IP로 변환되는 걸까?

서비스 이름이 각각의 컨테이너에 할당된 IP를 가지는 것이 아니라

서비스의 VIP(Virtual IP)를 가지는 것이다.

VIP는 다음 명령어로 확인 가능하다.

docker service inspect <service-name> --format {{.Endpoint.VirtualIPs}}

[{rmg029v282bu56mlymcinz41c 10.0.0.10/24} {yqnmcoury7kurmi4wsa6uk6dr 172.24.0.2/24}]

test-app 서비스의 VIP는 172.24.0.2이다.

스웜 모드가 활성화된 도커 엔진의 내장 DNS 서버는 test-app라는 호스트 이름을 172.24.0.2라는 IP로 변환한다.

그리고 이 IP는 컨테이너의 네트워크 네임스페이스 내부에서 실제 test-app 서비스의 컨테이너 IP로 포워딩된다.

VIP 방식이 아닌 도커 내장 DNS 서버를 기반으로 라운드 로빈을 사용할 수도 있지만,

이는 캐시 문제로 인해 서비스 발견이 정상적으로 동작하지 않을 가능성이 높으니 권장하지는 않는다.


▶︎ 스웜 볼륨

도커 데몬 명령어 중 run 명령어에서 -v 플래그를 사용할 때 호스트와 디렉토리를 공유하는 경우와 볼륨을 사용하는 경우에 대한 구분은 딱히 있지 않았다.

# 호스트 디렉토리와 bind 하는 경우
docker run -d -it --name nginx \
-v "$(pwd)/nginx-log:/var/log/nginx" \
nginx:latest

# 도커 볼륨 사용하는 경우
docker run -d -it --name nginx \
-v "nginx-log:/var/log/nginx" \
nginx:latest

이러한 run 명령어의 -v 옵션과 같은 기능을 스웜 모드에서도 사용 가능하다.

그러나 스웜 모드에서는 도커 볼륨을 사용할지, 호스트와 디렉토리를 공유할지를 조금 더 명확히 구분해 볼륨을 사용한다.


‣ bind 타입의 볼륨 생성

바인드 타입은 호스트와 디렉토리를 공유할 때 사용된다.

볼륨 타입과는 달리 공유될 호스트의 디렉토리를 설정해야 하므로 source 옵션이 필수다

docker service create --name nginx \
--mount type=bind,source=/home/manager/dockerSwarm/nginx-log,target=/var/log/nginx \
--replicas=4 \
nginx:latest

• bind source path does not exist

매니저 노드에서 바인드 타입으로 볼륨을 매핑 시 아래와 같은 에러가 발생할거다.

docker service create --name nginx \
--mount type=bind,source=/home/manager/dockerSwarm/nginx-log,target=/var/log/nginx \
--replicas=4 \
nginx:latest

overall progress: 1 out of 4 tasks
1/4: invalid mount config for type "bind": bind source path does not exist: /ho…
2/4: invalid mount config for type "bind": bind source path does not exist: /ho…
3/4: running   [==================================================>]
4/4: invalid mount config for type "bind": bind source path does not exist: /ho…

이는 할당받은 노드에서는 해당 경로의 바인드 폴더가 없기 때문에 이러한 에러가 생기는 것이다.

문제를 해결하려면 모든 노드에 bind에 매핑해둔 source 경로와 동일한 폴더를 생성해주어야 한다.


‣ volume 타입의 볼륨 생성

스웜 모드에서는 도커 볼륨을 사용하는 서비스를 생성하려면 서비스를 생성할 때 --mount 옵션의 type 값에 volume을 지정해주면 된다.

docker service create --name nginx \
--mount type=volume,source=nginx-log,target=/var/log/nginx \
nginx:latest

도커 데몬에 이미 source에 해당하는 이름의 볼륨이 존재하면 해당 볼륨을 사용하고, 없는 경우 새로 생성한다.

이 때 source 옵션을 명시하지 않으면 임의의 16진수로 구성된 익명의 이름을 가진 볼륨을 생성한다.

docker volume ls
DRIVER.        VOLUME NAME
local          f3432432fdsfdffdf...

서비스의 컨테이너에서 볼륨에 공유할 디렉토리에 파일이 이미 존재한다면 이 파일은 볼륨에 복사되고,

호스트에서 별도의 공간을 차지하게 된다.

그러나 서비스를 생성할 때 volume-nocopy 옵션을 추가하면 컨테이너의 파일들이 볼륨에 복사되지 않도록 설정할 수도 있다.


‣ 스웜에서의 볼륨 한계점

스웜 클러스터에서 볼륨을 사용하기란 매우 까다롭다.

서비스를 할당받을 수 있는 모든 노드가 볼륨 데이터를 가지고 있어야 하기 때문이다. 위의 bind source path does not exist 예시처럼 ..

스웜 매니저에 내장된 스케줄러가 컨테이너를 할당할 때 어느 노드에 할당해도 서비스에 정의된 볼륨을 사용할 수 있어야 한다.

따라서 여러 개의 도커 데몬을 관리해야하는 스웜모드에서는 도커 볼륨, 또는 호스트와의 볼륨 사용이 적합하지 않은 기능일 수도 있다.
image

서비스의 컨테이너가 각 노드에 할당될 때 persistence 스토리지를 마운트하여 사용하면 노드에 볼륨을 생성하지 않아도 되며,

컨테이너가 어느 노드에 할당되더라도, 컨테이너에 필요한 파일을 자유롭게 읽고 쓸 수 있다.

또는, 각 노드에 라벨을 붙여 서비스에 제한을 주는 방법도 있다.

노드에 라벨을 설정해 특정 서비스의 동작에 필요한 볼륨이 존재하는 노드에만 컨테이너를 할당할 수 있도록 설정하는 것이다. 하지만 근본적인 해결책은 아니다.


▶︎ 노드 Availability 변경하기

구축한 클러스터의 노드를 확인하면 다음과 같은 결과를 볼 수 있다.

docker node ls

ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
bt2mnzckkkkidq9d9gw42upk9 *   manager    Ready     Active         Leader           26.1.3
v79vd9oveczfi4qw6l64ftjij     worker1    Ready     Active                          26.1.3
yoqa9ii8f6sjw1rn4kpjogws9     worker2    Ready     Active                          26.1.3

‣ Active

Active 상태는 새로운 노드가 스웜 클러스터에 추가되면 기본적으로 설정되는 상태로서 노드가 서비스의 컨테이너를 할당 받을 수 있음을 의미한다.


다음과 같이 availabilty 변경이 가능하다.

docker node update \
--avilability active \
worker1

‣ Drain

이 상태에서는 스웜 매니저의 스케줄러는 컨테이너를 해당 노드에 할당하지 않는다.

일반적으로 매니저 노드에 설정하는 상태이지만, 노드에 문제가 생겨 일시적으로 사용하지 않는 상태로 설정해야 할 때에도 자주 사용된다.


다음과 같이 availabilty 변경이 가능하다.

docker node update \
--avilability drain \
worker1

‣ Pause

Pause 상태는 서비스의 컨테이너를 더는 할당받지 않는다. Drain과 다르게 실행 중인 컨테이너가 중지되지 않는다.


다음과 같이 availabilty 변경이 가능하다.

docker node update \
--avilability pause \
worker1

▶︎ 노드 라벨 추가

라벨은 key-value 형태로 존재한다.

특정 노드에 라벨을 추가하면 서비스를 할당할 때 컨테이너를 생성할 노드의 그룹을 선택하는 것이 가능하다.

예시로 swarm-worker1은 HDD 스토리지를 사용하고 있어 storage=hdd라는 라벨을,

swarm-worker2는 SSD 스토리지를 사용하고 있어 storage=ssd라는 라벨을 설정할 수도 있다.

이러한 라벨을 이용해서 서비스를 생성할 때 storage=hdd로 설정해 swarm-worker1에만 컨테이너를 할당 해줄 수 있다.


‣ 서비스 제약 설정

1. node.labels 제약 조건

docker service create name label_nginx \
--constraint 'node.labels.storage == hdd' \
--replicas=5 \
nginx:latest

위 명령어로 storage키의 값이 hdd로 설정된 노드에 서비스 컨테이너를 할당한다.

Label에 매칭되는 키를 찾지 못할 경우 서비스는 생성되지 않는다.


2. node.id 제약 조건

# worker2 노드의 id 출력
docker node ls | grep worker2

yoqa9ii8f6sjw1rn4kpjogws9     worker2    Ready     Active                          26.1.3
docker service create --name label_nginx2 \ 
--constraint 'node.id == yoqa9ii8f6sjw1rn4kpjogws9' \
--replicas=5 \
nginx:latest

노드의 ID를 명시해 서비스의 컨테이너를 할당할 노드를 선택한다.

그 외에도 hostname이나 role 등으로도 제약조건 설정이 가능하다.


Loading script...