ControlMaster를 이용한 SSH 연결 멀티플렉싱: 배포 및 자동화 속도 향상하기
요약
SSH 연결 시 발생하는 반복적인 핸드셰이크 비용을 줄이기 위해 ControlMaster를 활용하는 방법을 소개합니다. 하나의 마스터 연결을 생성하고 Unix 소켓을 통해 이후의 연결을 재사용함으로써 배포 및 자동화 속도를 획기적으로 높일 수 있습니다.
핵심 포인트
- SSH 핸드셰이크 과정의 오버헤드 이해
- ControlMaster를 통한 연결 멀티플렉싱 원리
- Unix 소켓을 이용한 세션 재사용 방법
- 배포 및 자동화 작업의 네트워크 지연 시간 단축
실행하는 모든 SSH 명령은 새로운 TCP 연결을 열고 전체 암호화 핸드셰이크 (cryptographic handshake)를 완료합니다. 여기서는 이를 한 번만 수행하고 수백 번 재사용하는 방법을 소개합니다.
50대의 서버를 대상으로 ansible을 실행하면, 각 태스크마다 새로운 SSH 연결이 열립니다. 서버당 10개의 태스크가 있다면 총 500번의 핸드셰이크가 발생합니다. 원격 개발 서버로 rsync를 빈번하게 실행한다면, 매번 연결 비용을 지불하게 됩니다. 배포 스크립트가 루프 내에서 ssh를 호출한다면, 반복할 때마다 그 비용을 치러야 합니다.
SSH의 연결 핸드셰이크는 공짜가 아닙니다. 빠른 로컬 네트워크에서는 인지할 수 없을 정도지만, 인터넷을 통하거나, Bastion을 거치거나, 부하가 걸린 상태에서는 이 비용이 누적되어 몇 초면 끝날 작업에 몇 분을 추가하기도 합니다.
ControlMaster는 OpenSSH의 내장된 연결 멀티플렉싱 (connection multiplexing) 기능입니다. 호스트당 하나의 SSH 연결을 생성하여 이후의 모든 작업이 이를 공유합니다. 첫 번째 ssh host가 핸드셰이크 비용을 지불하면, 그 이후의 모든 연결은 이미 수립된 소켓 (socket)을 재사용하여 밀리초(ms) 단위로 연결됩니다.
이 글에서는 ControlMaster가 어떻게 작동하는지, 어떻게 설정하는지, 배포와 자동화를 가속화하기 위해 어떻게 사용하는지, 그리고 반드시 알아야 할 예외 상황(edge cases)에 대해 설명합니다.
문제 이해하기: 모든 SSH 연결에서 발생하는 일
해결책을 보기 전에, 우리가 무엇을 제거하려고 하는지 이해할 가치가 있습니다.
모든 새로운 SSH 연결은 다음 과정을 수행합니다:
- TCP 핸드셰이크 (TCP handshake) — three-way SYN/SYN-ACK/ACK (~1 RTT)
- SSH 버전 교환 (SSH version exchange) — 클라이언트와 서버가 프로토콜 버전을 알림
- 키 교환 (Key exchange) — 공유 비밀을 설정하기 위한 Diffie-Hellman 또는 Curve25519 협상 (~1–2 RTTs)
- 호스트 키 검증 (Host key verification) — 서버가 자신의 신원을 증명
- 사용자 인증 (User authentication) — 키 서명 챌린지/응답 (~1 RTT)
- 채널 오픈 (Channel open) — 세션 채널 수립
지연 시간이 낮은 로컬 네트워크에서는 이 과정이 약 50ms 정도 걸립니다. RTT가 100ms인 VPN이나 Bastion을 통할 경우 약 300–500ms가 소요됩니다. 수백 개의 연결을 만드는 자동화 작업에서는 이 시간이 실제로 큰 차이를 만들어냅니다.
ControlMaster를 사용하면 2번째부터 N번째까지의 연결은 1~5단계를 완전히 건너뜁니다. 이 연결들은 기존의 멀티플렉싱 (Multiplexing) 된 연결 위에서 새로운 채널을 엽니다. 연결 시간은 네트워크 지연 시간 (Network latency)과 관계없이 한 자릿수 밀리초 (ms) 단위로 떨어집니다.
ControlMaster의 작동 원리
ControlMaster는 하나의 SSH 연결을 "마스터 (Master)"로 지정합니다. 마스터 연결은 로컬 머신에 Unix 소켓 (Unix socket)인 "컨트롤 소켓 (Control socket)"을 생성하고, 이후의 연결 요청을 대기합니다.
첫 번째 ssh 연결:
사용자 머신 ──[전체 핸드셰이크 (Full handshake)]──► 원격 서버
생성됨: ~/.ssh/sockets/ubuntu@server:22 (컨트롤 소켓)
...
마스터 연결은 이를 사용하는 모든 세션이 종료된 후에도 (설정 가능한 기간 동안) 계속 유지됩니다. 새로운 세션은 즉시 연결될 수 있습니다.
기본 설정
~/.ssh/config 설정
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h:%p
...
소켓 디렉토리를 생성합니다:
mkdir -p ~/.ssh/sockets
chmod 700 ~/.ssh/sockets
지시어(Directive) 상세 분석:
ControlMaster auto
yes— 항상 마스터로 동작합니다. 이미 마스터가 존재하면 연결을 거부합니다.auto— 사용 가능한 기존 마스터가 있으면 사용하고, 없으면 마스터가 됩니다. 이 설정을 권장합니다.no— 멀티플렉싱을 절대 사용하지 않습니다.ask— 기존 마스터를 사용할지 여부를 묻습니다.
ControlPath ~/.ssh/sockets/%r@%h:%p
컨트롤 소켓을 위한 파일 시스템 경로입니다. 토큰은 다음과 같이 확장됩니다:
%r— 원격 사용자 이름 (Remote username)%h— 원격 호스트 이름 (Remote hostname, 연결 명령에 지정된 대로)%p— 포트 번호 (Port number)
ssh ubuntu@web-01.example.com 실행 시 결과:
~/.ssh/sockets/ubuntu@web-01.example.com:22
ControlPersist 4h
모든 세션이 종료된 후 마스터 연결이 유지되는 시간입니다. 4h는 4시간을 의미합니다. 다음 값도 허용됩니다:
yes— 무기한 유지no— 마지막 세션이 종료될 때 함께 종료- 시간 값:
10m,1h,4h,1d
작동 확인
# 첫 번째 연결 — 전체 핸드셰이크 비용 발생
time ssh web-01 "echo hello"
# real 0m0.312s
...
그 차이는 전체 핸드셰이크 (handshake) 비용입니다. 지연 시간 (latency)이 높은 연결에서는 작업당 1~2초가 소요될 수 있습니다.
마스터가 실행 중인지 확인하려면:
ssh -O check web-01
# Master running (pid=12345)
ControlMaster 제어 명령 (Control Commands)
ControlMaster 세션은 ssh -O를 통해 전송된 제어 명령에 응답합니다:
# 마스터 실행 여부 확인
ssh -O check web-01
...
이 명령들은 자동화 및 스크립팅에 필수적입니다. 지속 시간 타이머 (persist timer)에 의존하는 대신 마스터의 생명 주기 (lifecycle)를 명시적으로 관리할 수 있습니다.
Ansible 가속화
Ansible은 모든 호스트의 모든 태스크 (task)에 대해 SSH를 사용합니다. 인벤토리 (inventory) 규모가 크고 태스크가 많을 경우, SSH 핸드셰이크 오버헤드 (overhead)가 실행 시간의 대부분을 차지하게 됩니다.
Ansible의 내장 SSH 파이프라이닝 (Pipelining)
먼저, ansible.cfg에서 SSH 파이프라이닝 (pipelining)을 활성화하십시오. 이것만으로도 하나의 SSH 세션 내에서 여러 작업을 전송함으로써 연결 횟수를 크게 줄일 수 있습니다:
# ansible.cfg
[defaults]
pipelining = True
관리 대상 호스트(managed hosts)에서는 /etc/sudoers 파일의 requiretty 설정을 주석 처리하십시오 (requiretty가 활성화되어 있으면 파이프라이닝이 작동하지 않습니다):
# 대상 서버에서:
sudo visudo
# 주석 처리 또는 제거:
...
Ansible을 위한 ControlMaster
Ansible은 ssh_args를 통해 ControlMaster를 지원합니다:
# ansible.cfg
[defaults]
pipelining = True
...
대화형 세션과의 소켓 (socket) 충돌을 피하기 위해 Ansible에는 별도의 ControlPath를 사용하십시오.
실질적인 영향
벤치마크 결과: Ansible 플레이북 (playbook), 서버 20대, 각 서버당 15개 태스크:
| 설정 | 시간 |
|---|---|
| 기본 설정 (파이프라이닝 미사용, 멀티플렉싱 미사용) | 4m 12s |
| ... |
멀티플렉싱 (Multiplexing)은 단순히 시간만 절약하는 것이 아닙니다. 수백 개의 핸드셰이크를 더 이상 처리할 필요가 없으므로 배스천 (bastion) 서버와 대상 서버의 부하를 줄여줍니다.
배포 스크립트 가속화
패턴: 배포 기간 동안 마스터 유지
일반적인 배포 패턴은 다음과 같습니다: 시작 시점에 마스터를 명시적으로 설정하고, 모든 작업을 수행한 후, 마지막에 명시적으로 마스터를 닫는 방식입니다.
#!/bin/bash
set -e
...
-S를 사용하여 소켓 경로 (socket path)를 명시적으로 지정하면, 주변 상태 (ambient state)에 의존하거나 ControlPersist 타이머에 의존할 필요 없이 완벽한 제어권을 가질 수 있습니다.
패턴: 반복적인 핸드셰이크 (Handshake) 없는 SSH 루프
이전:
# 매 반복마다: 전체 핸드셰이크 수행
for server in web-01 web-02 web-03; do
ssh ubuntu@$server "sudo systemctl restart nginx"
...
이후 — 먼저 마스터를 설정:
# 서버당 한 번의 핸드셰이크, 이후 즉시 실행
for server in web-01 web-02 web-03; do
ssh ubuntu@$server "echo" & # 마스터를 병렬로 오픈
...
또는 더 간단하게 — 만약 ControlMaster가 전역적으로 설정되어 있다면, 그냥 평소처럼 루프를 돌리면 됩니다. 설정 파일이 이를 처리합니다.
Bastion 호스트와 함께 사용하는 ControlMaster
ControlMaster는 ProxyJump와 자연스럽게 작동하며, 멀티플렉싱 (multiplexing)은 종단 간 (end-to-end) 연결에서 이루어집니다.
Host bastion
HostName bastion.example.com
User ubuntu
...
첫 번째 ssh db.internal: Bastion 경유 비용 + 대상 서버 핸드셰이크가 발생합니다.
두 번째 ssh db.internal: 즉시 실행됩니다. db.internal로 직접 연결된 기존의 멀티플렉싱된 연결을 재사용합니다.
Bastion 또한 마스터 연결을 갖게 되므로, Bastion을 통한 반복적인 연결은 Bastion의 TCP 연결도 공유하게 됩니다.
Bastion + 멀티플렉싱의 성능
RTT(Round Trip Time)가 150ms인 Bastion 뒤에 있는 대상 서버의 경우:
| 작업당 지연 시간 (Latency) | |
|---|---|
| 멀티플렉싱 미사용 | ~900ms (Bastion 경유 + 대상 핸드셰이크) |
| ... |
수십 번의 SSH 호출을 수행하는 배포 작업의 경우, Bastion을 통한 종단 간 멀티플렉싱은 혁신적인 변화를 가져옵니다.
CI/CD 파이프라인 통합
CI 파이프라인에서의 멀티플렉싱은 여러 명령어를 실행하는 배포 단계에서 SSH 오버헤드 (overhead)를 제거합니다.
GitHub Actions 예시
- name: Setup SSH multiplexing
run: |
mkdir -p ~/.ssh/sockets
...
정리 단계의 if: always()는 파이프라인이 실패하더라도 마스터가 닫히도록 보장하며, 이는 연결이 공중에 떠 있는 상태(dangling connections)로 남는 것을 방지하는 데 중요합니다.
예외 사례 및 주의사항
소켓 경로 길이 제한
Unix 소켓 경로(Unix socket paths)는 약 104자(OS에 따라 다름)의 최대 길이를 가집니다. 소켓 경로가 너무 길면, ControlMaster는 조용히 일반 연결(regular connections) 방식으로 전환됩니다.
호스트 이름이 길다면, 더 짧은 토큰 패턴을 사용하세요:
ControlPath ~/.ssh/sockets/%C
%C는 연결 파라미터의 해시(hash) 값으로, 호스트 이름에 관계없이 항상 고정된 길이를 유지합니다. 단점은 소켓 파일 이름의 가독성이 떨어진다는 점입니다.
만료된 소켓 (Stale Sockets)
SSH가 충돌하거나 마스터 연결이 예기치 않게 종료되면, 소켓 파일이 디스크에 남아 있을 수 있습니다. 이후의 연결들은 죽은 소켓에 연결을 시도하기 때문에 실패하게 됩니다.
탐지 및 정리:
ssh -O check web-01
# Error: No ControlPath (check errno: No such file or directory)
...
멀티플렉싱과 SSH 이스케이프 시퀀스 (SSH Escape Sequences)
SSH 이스케이프 시퀀스(~.로 연결 해제, ~#로 포워딩된 연결 목록 표시)는 멀티플렉싱된 세션에서 다르게 동작합니다. 이스케이프 시퀀스는 로컬 SSH 클라이언트 프로세스에 신호를 보내는데, 이 프로세스는 마스터가 아닌 현재의 채널(channel)만을 제어합니다. 따라서 ~.을 사용하여 세션을 종료하더라도 마스터는 계속 실행 상태를 유지합니다.
ControlPersist와 장시간 실행되는 마스터
ControlPersist yes 설정은 마스터를 무기한 유지합니다. 이는 편리하지만, 사용자가 잊어버린 후에도 SSH 마스터가 몇 시간 동안 실행되고 있을 수 있음을 의미합니다.
# 실행 중인 모든 SSH 마스터 목록 표시
ls ~/.ssh/sockets/
...
멀티플렉싱과 포트 포워딩 (Port Forwarding)
멀티플렉싱된 연결을 통한 포트 포워딩은 정상적으로 작동합니다. 기존 마스터에 터널을 추가할 수 있습니다:
# 기존 마스터 연결에 포트 포워딩 추가
ssh -O forward -L 5432:db.internal:5432 web-01
...
대화형 채널 vs 비대화형 채널 (Interactive vs. Non-Interactive Channels)
마스터 연결과 그 채널들은 독립적입니다. 하나의 채널은 대화형 셸(interactive shell)일 수 있고, 다른 채널은 명령어를 실행하거나 파일 전송을 수행할 수 있습니다. 이들은 TCP 연결을 공유하지만 독립적으로 작동합니다.
호스트별 설정 vs 전역 설정 (Per-Host vs. Global Configuration)
Global ControlMaster (Host * 내 설정)는 모든 호스트에 적용됩니다. 이는 보통 문제가 없지만, 특정 호스트를 제외하고 싶은 경우가 있습니다:
# 기본값: 모든 것을 멀티플렉싱함
Host *
ControlMaster auto
...
나중에 나오는 (더 구체적인) Host 블록은 동일한 지시어(directive)에 대해 이전 블록을 덮어쓰지 않습니다. SSH는 지시어당 첫 번째로 일치하는 값을 사용하기 때문입니다. 따라서 구체적인 재정의(override) 설정을 Host * 블록 앞에 배치하십시오:
# 구체적인 재정의를 먼저 배치
Host container-*
ControlMaster no
...
보안 고려 사항 (Security Considerations)
ControlMaster 소켓은 이해할 가치가 있는 보안상의 함의를 가집니다.
소켓 권한 (Socket permissions): 소켓 파일은 사용자의 소유이며 다른 사용자가 읽을 수 없습니다. 동일한 머신의 다른 사용자는 사용자의 마스터(master)에 연결할 수 없습니다. 잘 구성된 멀티 유저 시스템에서는 이 방식이 안전합니다.
공용 머신 (Shared machines): 모든 사용자 계정을 제어할 수 없는 머신(공용 점프 호스트, 여러 사용자가 접근하는 CI 러너 등)에서는 더 주의해야 합니다. 제한적인 권한을 가진 명시적인 소켓 경로를 사용하고, 작업이 끝나면 마스터를 명시적으로 종료하십시오.
ControlPersist 지속 시간 (ControlPersist duration): 마스터가 몇 시간 동안 지속된다는 것은 SSH 연결이 열려 있다는 의미입니다. 암호화되고 인증되었지만, 여전히 활성화된 연결입니다. 보안 수준이 높은 시스템에서는 지속 시간을 짧게 설정하거나 no(마지막 세션이 종료될 때 즉시 종료)를 사용하십시오.
세션 하이재킹(Session hijacking) 위험은 없음: 에이전트 포워딩 (Agent forwarding)과 달리, ControlMaster 소켓은 다른 시스템에 인증하는 용도로 사용될 수 없습니다. 이는 오직 기존의 멀티플렉싱된 연결 위에서 새로운 채널을 허용할 뿐이며, 해당 연결은 이미 특정 서버 하나에 대해 인증된 상태입니다.
완전한 프로덕션 설정 (Complete Production Configuration)
# ~/.ssh/config
# 소켓 디렉토리가 존재해야 합니다: mkdir -p ~/.ssh/sockets
...
빠른 참조 (Quick Reference)
# 특정 호스트에 대해 마스터가 실행 중인지 확인
ssh -O check hostname
...
요약 (The Bottom Line)
요약 (The Bottom Line)
ControlMaster는 제대로 작동할 때는 보이지 않지만, 없을 때는 매우 고통스럽게 느껴지는 기능 중 하나입니다. 일단 이 기능을 사용하고 나면, ControlMaster 없이 SSH를 실행하는 것은 클릭하는 링크마다 브라우저의 새 탭을 여는 것처럼 느껴질 것입니다.
설정은 단 다섯 줄이면 충분합니다. 자동화에 미치는 성능 영향은 눈에 띄는 수준에서부터 극적인 수준까지 다양합니다. 보안 측면의 트레이드오프 (trade-offs)는 에이전트 포워딩 (agent forwarding)과 같은 대안들에 비해 미미합니다.
오늘 바로 ~/.ssh/config에 이 설정을 추가하세요. 내일 다시 그 Ansible 플레이북을 실행해 보세요. 그 차이를 느끼게 될 것입니다.
더 많은 SSH 심층 분석을 위해 팔로우하세요. 이전 글: SSH 에이전트 포워딩 (SSH Agent Forwarding) vs ProxyJump, SSH 배스천 호스트 (SSH Bastion Host) 아키텍처.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기