설정 한 줄을 넣었더니 Ansible이 다시 돌아갔다.

[ssh_connection]
ssh_args = -C -o ControlMaster=no -o ControlPersist=no

기쁘다. 그런데 바로 다음 질문이 따라온다.

이 설정이 문제를 고친 걸까, 아니면 우연히 다른 길로 돌아간 걸까?

장시간 자동화를 다뤄본 사람이라면 이 질문이 낯설지 않다. 처음 몇 개 작업은 잘 되는데, 한참 뒤 파일을 복사하는 순간 연결이 끊긴다. 로그에는 SSH 오류가 남고, 사람들은 보통 SSH 설정을 더 붙이기 시작한다.

이번에 얻은 결론은 의외로 단순했다.

설정을 바꾸는 것과, 그 설정이 무엇을 바꾸는지 증명하는 것은 다른 일이다.

문제는 연결 하나가 아니었다

Ansible이 접근 제어 프록시를 거쳐 서버에 접속한다고 생각해보자.

Ansible
  → SSH client
  → access proxy
  → target server

겉으로는 SSH 연결 하나처럼 보인다. 하지만 안쪽에는 여러 시간이 흐른다.

  • Ansible은 다음 작업에도 기존 SSH 연결을 재사용할 수 있다.
  • 프록시는 접속할 때 받은 인증 상태를 들고 있을 수 있다.
  • copy 같은 파일 전송은 새 실행 채널이나 SFTP 채널을 열 수 있다.
  • 그 사이 인증 상태가 달라지면, 첫 명령은 성공했는데 다음 작업에서만 실패할 수 있다.

그래서 “명령이 한 번 성공했다”는 충분한 증거가 아니다. 다음 질문을 나눠야 한다.

  1. SSH client가 정말 기존 연결을 재사용했는가?
  2. 연결 재사용을 끄면 새 transport가 만들어지는가?
  3. 실제 프록시 경로에서 raw 명령뿐 아니라 파일 전송도 되는가?
  4. 인증 상태가 바뀐 뒤, 기존 연결과 새 연결은 다르게 행동하는가?

한 질문에 답했다고 나머지까지 답한 것은 아니다.

ControlMaster=no가 하는 일

SSH의 connection multiplexing은 이미 만들어진 연결을 여러 작업이 함께 쓰게 하는 기능이다. 버스를 빌려두고 다음 승객이 계속 올라타는 모습에 가깝다.

ControlMaster=auto
ControlPersist=60s

이 설정에서는 첫 연결이 master가 되고, 뒤의 명령은 그 버스에 올라탄다. 빠르고 편하다.

반대로 아래 설정은 버스를 매번 새로 부른다.

ControlMaster=no
ControlPersist=no

연결을 다시 맺는 비용은 늘어난다. 하지만 오래된 연결이 들고 있던 상태를 다음 작업이 이어받지 않는다.

중요한 점은 여기다.

이 설정은 인증 문제를 마법처럼 고치는 기능이 아니다. 기존 연결을 재사용하지 않게 하는 기능이다.

그래서 workaround를 검증할 때도 “명령이 성공했다”가 아니라 “기존 연결을 쓰지 않았고, 필요한 작업이 성공했다”를 같이 확인해야 한다.

raw만 통과하면 반쪽짜리다

처음에는 간단한 명령만 실행해 보기 쉽다.

ansible targets -m raw -a "printf 'hello\n'"

이 결과가 성공하면 마음이 놓인다. 하지만 실제 운영 작업에는 파일 배포가 많다.

raw command  → 원격에서 명령 실행
copy          → 파일 전달, 임시 파일, 권한 설정, 정리

두 작업은 SSH를 쓰지만, 연결 안에서 밟는 길이 완전히 같지 않을 수 있다. 그래서 재현 실험에는 둘 다 들어가야 한다.

확인할 작업 질문
raw 기본 exec 채널이 열리는가?
copy 파일 전송과 후속 정리까지 되는가?
mux trace 같은 SSH master를 재사용했는가?

이 세 개가 있어야 “연결 재사용을 껐더니 파일 복사까지 정상 동작했다”라고 말할 수 있다.

포트 하나가 만든 가짜 단서

이런 실험에서 가장 재미있던 함정은 SSH port였다.

접근 제어 시스템에 등록된 서버가 반드시 기본 port 22를 쓰는 것은 아니다. 그런데 Ansible inventory에 습관처럼 22를 넣으면, 프록시는 우리가 생각한 등록 대상과 다른 요청을 받게 된다.

그 결과는 꽤 그럴듯하다.

권한이 없는 것 같아요.
계정이 맞지 않는 것 같아요.
프록시가 custom target을 거절한 것 같아요.

하지만 실제 원인은 “등록된 서버의 port와 다른 port를 보냈다”일 수 있다.

이 경험은 아주 평범한 원칙을 다시 알려준다.

설정값은 증거가 아니다. 실제 클라이언트가 해석한 대상 정보가 증거다.

그래서 재현 도구는 임의의 기본값을 믿지 않고, 접근 가능한 서버 목록과 서버 상세 정보에서 host, port, account를 확인한 뒤 Ansible inventory를 만든다.

좋은 실험실은 남의 방을 어지르지 않는다

재현을 하다 보면 기존 테스트 계정, 로컬 데이터베이스, 이미 켜져 있는 서버를 건드리고 싶은 유혹이 생긴다. 빠르기 때문이다.

하지만 그건 실험실이 아니라 남의 책상에서 하는 실험이다.

이번에는 반대로 경계를 정했다.

  • SSH target, key, inventory, socket은 실험이 만들고 실험이 지운다.
  • CLI 상태는 임시 홈 디렉터리에 격리한다.
  • 기존 runtime과 기존 데이터는 초기화하지 않는다.
  • 결과에는 성공 여부와 재사용 여부만 남긴다.
  • 비밀번호, token, private key는 결과 파일에 남기지 않는다.

이런 규칙은 조금 느려 보인다. 하지만 cleanup이 끝난 뒤에도 다른 사람이 같은 환경을 믿고 쓸 수 있게 만든다.

좋은 테스트는 성공한 뒤에도 조용하다. 무엇을 만들었는지 알고, 무엇을 지웠는지도 안다.

크게 만들지 않고, 자주 쓰는 부분만 뽑았다

반복되는 절차를 발견하면 곧바로 거대한 설정 언어를 만들고 싶어진다.

proxy: ...
ssh: ...
ansible: ...
token: ...
magic: true

처음에는 멋있다. 하지만 다음 사건이 오면 DSL의 예외부터 추가하게 된다. 버그를 재현하려고 만든 도구가 먼저 새로운 버그가 된다.

그래서 이번에는 작은 공구만 만들었다.

  1. 임시 CLI 상태를 만들고 지우는 공구
  2. 등록된 SSH port를 읽는 공구
  3. ProxyCommand가 포함된 Ansible inventory를 만드는 공구
  4. rawcopy를 같은 방식으로 실행하는 공구
  5. return code와 연결 재사용 여부만 남기는 공구

사건의 맥락은 시나리오 코드에 남긴다. 반복되는 안전장치만 공용화한다.

이 방식의 장점은 다음 사람이 코드를 읽을 때도 분명하다.

무엇을 검증하는가?  → scenario
어떻게 안전하게 준비하는가? → helper
무엇을 근거로 통과라 하는가? → result

고객에게는 어디까지 말해야 할까

기술 검증은 정직할수록 좋다.

“SSH 재사용을 끄면 실제 프록시 경로에서 명령과 파일 전송이 정상 동작했다.”

여기까지는 말할 수 있다.

하지만 자연스러운 모든 인증 만료 타이밍과 모든 환경을 완전히 재현하지 않았다면, “제품 문제가 완전히 해결됐다”고 말하면 안 된다.

고객에게는 이렇게 안내하는 편이 정확하다.

장시간 Ansible 실행에서 후속 작업의 연결 종료를 피하기 위해, SSH connection reuse를 끄는 설정을 우선 적용해 주세요. 이 설정은 작업마다 새 연결을 생성하므로 실행 시간이 조금 늘어날 수 있습니다.

workaround는 해결 선언이 아니라, 확인한 범위 안에서 위험한 길을 피하는 운영 방법이다.

결론: 좋은 재현은 답보다 경계를 남긴다

이번 일에서 가장 중요한 산출물은 SSH 옵션 두 줄이 아니었다.

  • 무엇이 실제로 재사용되는지
  • 어떤 작업까지 검증했는지
  • 어디서부터는 아직 검증하지 못했는지
  • 실험이 무엇을 만들고 무엇을 지웠는지

이 네 가지를 다음 사람이 다시 확인할 수 있게 만든 것이 더 오래 남는다.

버그 재현은 “됐다”를 말하는 기술이 아니다.

“어디까지 됐고, 무엇은 아직 모르는지”를 같은 언어로 남기는 기술이다.