Infra

[Infra] 실무에서 사용해본 유용한 Docker Swarm 기능들

immersive 2025. 6. 26. 12:17

Docker Swarm

Docker Swarm이란?

단일 컨테이너로 시작한 애플리케이션도 비즈니스 요구사항이 증가함에 따라 점차 마이크로서비스 아키텍처(MSA)를 도입하게 되고, 이에 따라 컨테이너 수도 함께 증가하게 됩니다. 컨테이너 내부에서 실행되는 애플리케이션의 특성에 따라 각기 다른 관리 전략이 필요해지고, 이는 운영 복잡도를 높입니다. 컨테이너 오케스트레이션 도구가 등장하기 전에는 인프라 관리자가 모든 컨테이너를 직접 배포하고 모니터링하며 운영을 책임져야 했습니다.

 

하지만, Docker Swarm을 비롯한 다양한 오케스트레이션 도구의 등장으로 이러한 작업들이 자동화되었고, 인프라를 코드로 체계적으로 관리할 수 있는 기반이 마련되었습니다. Docker의 공식 컨테이너 오케스트레이션 도구인 Docker Swarm을 한 문장으로 정리하자면 "여러 노드에서 실행 중인 Docker 호스트들을 하나의 가상 호스트처럼 묶어 관리할 수 있도록 해주는 클러스터링 솔루션"입니다. 이번 블로그에서는 Docker Swarm 기반의 프로젝트를 구축 및 운영하면서 파악한 Docker Swarm의 작동 원리와 유용한 기능들을 소개해드리겠습니다. 

굳이 Kubernetes 대신 Docker Swarm을 사용하는 이유

Docker Swarm과 쿠버네티스 keyword 분석량

Docker Swarm은 현재 활발히 개발되고 있는 오픈 소스 프로젝트는 아닙니다. 위 그래프에서 보이는 것처럼 컨테이너 오케스트레이션 툴에서의 사용량은 Kubernetes가 압도적입니다. 저도 이러한 사실을 알고 현재 회사에서 담당하고 있는 프로젝트에 Kubernetes를 적용하는 것을 건의하는 생각도 했었습니다. 하지만, 실제로 Docker Swarm 기반으로 시스템을 운영해보니 Docker Swarm이 Kubernetes에 비해 다음과 같은 이점들이 있습니다.

 

첫 번째로 Swarm 모드는 Docker Engine에 내장된 기능으로 별도 패키지나 외부 도구 설치 없이 Docker만 설치되어 있다면 "docker swarm init" 명령어로 Swarm을 활성화하여 클러스터를 구성할 수 있습니다. 또한 Worker Node를 추가할 때도 "docker swarm join" 명령어로 손쉽게 클러스터에 포함시킬 수 있습니다. 이는 Kubernetes의 kubeadm, kubectl, kubelet, CNI 설정 등 복잡한 초기 구성 과정에 비해 훨씬 빠르고 직관적입니다.

 

두 번째로 Docker Swarm은 익숙한 Docker Compose 형식으로 컨테이너를 배포할 수 있습니다. 즉, 기존에 사용하던 Docker Compose 파일을 그래도 활용할 수 있습니다. docker-compose.yml 파일을 "docker stack deploy" 명령어로 Swarm 클러스터 환경에서 Service를 구성할 수 있습니다. 기존에 Docker를 사용하는 조직의 경우 적은 학습 비용으로도 클러스터 환경으로 자연스럽게 확장할 수 있다는 장점이 있습니다.

 

마지막으로, Docker Swarm은 네트워킹 전략, 서비스 운영 전략, 배포 전략 측면에서 실무에 바로 적용할 수 있는 유용한 기능들을 기본으로 제공합니다. 먼저 네트워킹 전략 측면에서 보면, Swarm은 별도의 설정 없이도 클러스터 내의 컨테이너들은 서비스 이름을 통해 서로를 인식하고 통신할 수 있습니다. 서비스 운영 전략 측면에서는, 동일한 Service에 속한 여러 컨테이너 간에 로드밸런싱이 자동으로 적용되어 트래픽이 균등하게 분산됩니다. 마지막으로 배포 전략 측면에서도 Swarm은 별도 구성 없이도 Rolling Update, 헬스 체크 기반 재기동, 노드 라벨 기반 배치 전략 등을 지원하므로, 복잡한 배포 파이프라인 없이도 실무 환경에서 안정적으로 서비스를 운영할 수 있습니다. Kubernetes에서는 이와 같은 기능들을 활용하기 위해 ClusterIP 타입의 Service 객체, Ingress 설정, CNI 플러그인 구성, Deployment 오브젝트 세팅 등 복잡한 설정을 수반해야 하는 반면, Docker Swarm은 이러한 기능들을 기본값으로 제공함으로써 보다 빠르고 단순하게 마이크로서비스 환경을 구성할 수 있습니다.

 

이 블로그에서는 실무에서 시스템을 운영하면서 유용하다고 느낀 Docker Swarm의 세 가지 핵심 전략— 서비스 운영 전략, 네트워킹 전략 그리고 배포 전략—에 대해 자세히 소개하고자 합니다. 

Docker Swarm의 서비스 (Replicas, Restart Policy)

Docker Swarm에서의 최소 배포 단위는 Service입니다. 이는 Kubernetes의 Pod와 유사한 개념으로 이해할 수 있습니다. Docker Swarm은 개별 컨테이너를 직접 다루지 않고 Service라는 추상화된 객체를 통해 컨테이너를 관리합니다.

web-service:
    image: nginx:alpine
    deploy:
      replicas: 2
      placement:
        constraints:
          - node.labels.zone == frontend
    ports:
      - "8080:80"

 

Docker Swarm에는 기본 타입인 Replicated ServiceGlobal Service라는 두 가지 서비스 타입이 존재합니다.

 

Replicated 타입의 Service는 사용자가 설정한 replicas 수만큼 컨테이너를 실행하며, Swarm은 이 수를 항상 유지하도록 관리합니다.
예를 들어, 위의 예시에서는 replicas: 2로 설정되어 있어 nginx 컨테이너가 총 2개 실행되며, Swarm은 이 컨테이너들이 항상 살아 있도록 클러스터 내에서 적절히 분산 배포하고 장애 시에는 자동으로 복구합니다. 보통 일반적인 애플리케이션을 Replicated 서비스 타입으로 배포합니다.

 

또한 placement 설정을 통해 특정 조건을 만족하는 노드에만 컨테이너를 배포할 수도 있습니다. 예시에서는 "node.labels.zone == frontend" 라는 제약 조건을 통해, zone 라벨이 frontend인 노드에만 컨테이너가 배포되도록 하는 설정입니다.

 

Global 타입을 설정하면 클러스터의 모든 노드에 정확히 하나의 컨테이너가 배포되며, 노드가 추가되거나 제거될 때도 자동으로 컨테이너 수가 조정됩니다. node-exporter나 cAdvisor처럼 모든 노드에서 실행되어야 하는 모니터링용 애플리케이션의 경우에는 Global 타입의 Service를 사용하는 것이 적절합니다. 

web-service:
    image: nginx:alpine
    deploy:
      replicas: 2
      placement:
        constraints:
          - node.labels.zone == frontend
      
      restart_policy:
        condition: on-failure
        max_attempts: 3
        delays: 10s
        window: 30s
      
    ports:
      - "8080:80"

 

Docker Swarm에서 서비스를 배포할 때 restart_policy를 설정하면, 컨테이너의 상태에 따라 Swarm이 컨테이너를 자동으로 재기동할지 여부를 제어할 수 있습니다.

 

condition: on-failure 옵션은 컨테이너가 비정상 종료(exit code 1 이상) 했을 때만 재시작하도록 지정합니다.
예를 들어, 배치 작업을 처리하는 컨테이너가 작업을 완료하고 정상 종료(exit code 0) 하는 경우에는 Swarm이 컨테이너를 다시 시작하지 않습니다. 이를 통해 정상 종료된 컨테이너들의 불필요한 재시작을 방지할 수 있습니다.

 

max_attempts: 3은 Swarm이 최대 3회까지 재시작을 시도한다는 의미이며,delay: 10s는 각 재시작 시도 간에 10초 간격을 두고 수행하겠다는 설정입니다.

 

여기서 특히 중요한 개념이 window입니다. window: 30s는 Swarm이 재시작 시도 횟수를 30초라는 시간 범위 내에서 측정하겠다는 의미입니다. 예를 들어, 컨테이너가 일시적인 장애로 한두 번 실패한 뒤 30초 이상 정상적으로 동작한다면, Swarm은 이를 "정상 상태로 복구되었다"고 판단하고 재시작 시도 횟수를 0으로 초기화합니다. 이를 통해 일시적인 장애 이후에도 max_attempts에 걸려 재기동이 멈추는 일을 방지할 수 있습니다.

 

처음에는 이 window라는 개념이 다소 낯설었지만, 이 기능을 통해 인프라의 안정성을 더 높여 결과적으로 더 견고하고 자가 복구 가능한 서비스 환경을 구축할 수 있음을 알게 되었습니다. 현재 모든 서비스에 windows 옵션을 추가해서 Service를 운영하고 있습니다.

Docker Swarm의 네트워킹 전략 (Service Discovery, Overlay 네트워크)

Docker Swarm 환경에서 여러 Service들이 존재하며, Service가 관리하는 컨테이너들이 서로 통신해야 되는 상황이 생기기 마련입니다. 이때, 컨테이너 끼리 통신할 수 있는 수단이 필요합니다. Docker Swarm에서는 서비스에 속한 컨테이너들은 서비스명으로 도메인이름을 가지며, 클러스터 내부의 컨테이너들은 서비스명으로 통신할 수 있습니다. 그러면 어떻게, 서로 다른 서비스가 통신할 수 있는지 알아보겠습니다.

web-service:
 image: nginx:alpine
 deploy:
   replicas: 2
   placement:
     constraints:
       - node.labels.zone == frontend
   
   restart_policy:
     condition: on-failure
     max_attempts: 3
     delay: 10s
     window: 30s
   
 ports:
   - "8080:80"
 networks:
   - frontend-network

networks:
 frontend-network:
   driver: overlay
   attachable: true

 

먼저, 서비스를 위의 예시처럼 "frontend-network"와 같은 사용자 정의 오버레이 네트워크에 배포하면, 서비스들끼리는 동일한 오버레이 네트워크 상에서 서로 통신할 수 있는 구조가 됩니다. 이때, 각 서비스는 추상화된 객체이기 때문에 사용자 정의 네트워크 대역 내의 VIP(가상 IP)를 부여받게 됩니다. 또한, 사용자 정의 오버레이 네트워크에서는 서비스 이름을 통한 DNS 기반 서비스 디스커버리가 기본으로 제공됩니다. 오버레이 네트워크의 개념과 구현 원리에 대해서는 다음 블로그에서 좀 더 자세히 다룰 예정입니다.

컨테이너 통신 흐름

 

구체적인 동작 원리는 다음과 같습니다.

  1. 컨테이너 A에서, 컨테이너 B가 속한 서비스 이름(예: api-service)으로 요청을 보냅니다.
  2. 컨테이너 A 내부의 Docker 내장 DNS 서버(127.0.0.11:53)가, 목적지 서비스명을 VIP(예: 172.20.0.166)로 변환합니다.
  3. Swarm의 로드밸런서는 이 VIP에 연결된 실제 컨테이너들의 IP 정보를 알고 있습니다.
  4. Swarm은 요청을 VIP를 통해 수신한 뒤, 내부적으로 해당 서비스에 속한 실제 컨테이너 중 하나로 트래픽을 전달합니다.
    (여러 컨테이너가 존재할 경우, 로드밸런싱이 적용됩니다.)

각 컨테이너는 Docker의 내장 DNS 서버(127.0.0.11)를 통해, 같은 오버레이 네트워크 내에 있는 다른 서비스의 이름을 IP로 해석할 수 있습니다. 또한, Swarm Manager는 각 서비스의 VIP와 실제 컨테이너 IP 간의 매핑 정보를 유지합니다. 서비스명은 항상 고정된 VIP로 해석되며, Swarm의 내장 로드밸런서가 해당 VIP로 전달된 트래픽을 적절한 컨테이너로 라우팅합니다. 추가적으로 Swarm은 헬스체크 상태를 기반으로, 비정상 상태의 컨테이너에는 트래픽을 전달하지 않습니다.

 

이러한 구조는 Docker Swarm의 서비스 메시(Service Mesh) 구조의 핵심입니다. 컨테이너 A는 실제 컨테이너 B의 IP를 몰라도 되고, 컨테이너 B가 종료되더라도 트래픽이 다른 replica로 자동 라우팅됩니다. 또한, 서비스가 스케일링되면 새로운 컨테이너가 로드밸런싱 풀에 자동으로 추가되므로, 개발자는 서비스명만 알고 있으면 되고, 복잡한 인프라 구성과 네트워크 처리 로직은 Swarm이 추상화하여 자동으로 처리해줍니다.

Docker Swarm의 서비스 배포 전략 (Rolling Update, Rollback)

새로운 버전의 서비스로 업데이트할 때, Docker Swarm은 기본적으로 롤링 업데이트로 서비스를 배포 합니다. 

 

주요 설정 옵션은 다음과 같습니다.

  • parallelism: 동시에 업데이트할 컨테이너 수를 지정합니다. 기본값은 1이며, 이는 한 번에 하나씩 업데이트함을 의미합니다.
  • delay: 각 업데이트 그룹 간의 대기 시간을 설정합니다. 예를 들어 10s는 10초 대기를 의미합니다.
  • failure_action: 업데이트 실패 시 취할 행동을 정의합니다. pause(일시정지), continue(계속), rollback(롤백) 중 선택할 수 있습니다.
  • monitor: 업데이트된 태스크가 성공적으로 실행 중인지 모니터링할 시간을 설정합니다.
  • max_failure_ratio: 실패를 허용할 최대 비율을 설정합니다. 0.1은 10% 실패까지 허용함을 의미합니다. 기본값은 0입니다.
  • order: 업데이트 순서를 지정합니다. stop-first(기존 컨테이너 종료 후 새 컨테이너 시작) 또는 start-first(새 컨테이너 시작 후 기존 컨테이너 종료) 중 선택합니다.
web-service:
 image: nginx:alpine
 deploy:
   replicas: 10
   placement:
     constraints:
       - node.labels.zone == frontend
   
   restart_policy:
     condition: on-failure
     max_attempts: 3
     delay: 10s
     window: 30s
   
   update_config:
     parallelism: 1
     delay: 15s
     failure_action: rollback
     monitor: 30s
     max_failure_ratio: 0.2
     order: start-first
   
   rollback_config:
     parallelism: 1
     delay: 10s
     failure_action: pause
     monitor: 20s
     max_failure_ratio: 0.1
     order: stop-first
     
 ports:
   - "8080:80"
 networks:
   - frontend-network

networks:
 frontend-network:
   driver: overlay
   attachable: true

 

롤링 업데이트를 수행할 때는 한 번에 하나의 컨테이너만 업데이트하며(parallelism: 1), 새 버전의 컨테이너를 먼저 시작한 후 기존 컨테이너를 종료합니다(order: start-first). 컨테이너 간에는 15초의 간격을 두고 순차적으로 업데이트가 진행됩니다(delay: 15s). 이를 통해 새 버전의 컨테이너가 정상적으로 기동되는지 확인할 시간을 확보하여, 전체 서비스의 가용성을 유지할 수 있습니다.

업데이트된 컨테이너는 30초 동안 모니터링되며(monitor: 30s), 이 기간 동안 비정상적인 동작이 감지되면 실패로 간주됩니다. 만약 업데이트 중 실패 비율이 20%를 초과할 경우(max_failure_ratio: 0.2), 서비스의 모든 컨테이너를 이전 버전으로 롤백합니다(failure_action: rollback). 예를 들어 10개의 컨테이너 중 3개 이상이 실패하면 롤백이 수행되지만, 1~2개 정도만 실패한 경우에는 롤백되지 않고 그 컨테이너만 기존 버전(V1)으로 남게 됩니다.

 

롤백 시에도 한 번에 하나의 컨테이너만 되돌리며(rollback_config.parallelism: 1), 10초 간격으로 순차적으로 롤백이 이루어집니다(rollback_config.delay: 10s). 이때는 기존 컨테이너를 먼저 중지한 후 이전 버전 컨테이너를 기동합니다(rollback_config.order: stop-first). 롤백 중에도 컨테이너는 20초 동안 모니터링되며(rollback_config.monitor: 20s), 실패 비율이 10%를 넘는 경우 롤백 작업은 일시 중지됩니다(rollback_config.failure_action: pause, rollback_config.max_failure_ratio: 0.1).

 

이러한 설정을 통해 서비스 중단 없이 안전하고 점진적인 업데이트를 수행할 수 있으며, 문제 발생 시 자동으로 이전 버전으로 롤백하는 등의 보호 메커니즘을 구현할 수 있습니다.

롤링 업데이트

 

 

Docker Swarm이 기본적으로 제공하는 롤링 업데이트를 사용하면 배포하는 도중에 동시에 두 개 버전의 컨테이너가 가동된다는 단점이 있습니다. 필요에 따라서 모든 배포가 완료되면 트래픽 흐름을 바꿔주는 Blue-Green 배포나, 테스트 목적으로 트래픽을 점진적으로 늘려주는 Canary 배포를 사용해야 하는 일이 있을 수 있습니다. Docker Swarm이 Kubernetes와 다르게 이러한 고급 배포는 지원하지 않습니다. 이러한 고급 배포 설정을 해야하는 경우, Nginx나 Traefik과 같은 외부 로드밸런서를 도입하여 트래픽 흐름을 조절해줘야 하는 한계가 있습니다.

마무리

회사에서 Docker Swarm 기반으로 시스템을 운영한다고 했을 때, 처음에는 “지금 트렌드에 맞지 않는 기술을 사용하는 건 아닐까?” 하는 의문이 들기도 했습니다. 하지만, 직접 Docker Swarm을 활용해 컨테이너들을 오케스트레이션해보면서, 인프라 관점에서 많은 것을 배울 수 있었습니다.

예를 들어, 다중 컨테이너 환경에서 컨테이너를 어떤 노드에 배치하는 것이 가장 효율적일지 고민하게 되었고, 고정된 노드에 컨테이너가 상주하지 않기 때문에 노드 위치와 무관하게 데이터를 영구 저장하고 공유할 수 있는 볼륨 스토리지는 어떻게 구성해야 할까에 대한 고민도 자연스럽게 따라왔습니다.

결국 이러한 경험 덕분에 인프라에 대한 이해도가 높아졌고, 이후 Kubernetes를 학습할 때도 "이 문제를 Kubernetes는 어떻게 풀고 있을까?"라는 관점으로 접근할 수 있었습니다. 덕분에 단순한 기능 학습을 넘어, 더 깊이 있는 학습 경험을 할 수 있었습니다.