내가 원하던 배포 도구 만들기

6 days ago 4
  • Deptool은 DNS와 웹서버 설정을 직접 운영하기 위해 만든 배포 도구로, 변경 계획을 먼저 보여주고 확인 후 대상 호스트에 적용함
  • 전체 클러스터 설정을 미리 렌더링해 Git으로 관리하고, 호스트별 /var/lib/deptool 아래에 commit별 디렉터리를 둔 뒤 current 심볼릭 링크를 바꿔 버전을 원자적으로 전환함
  • 배포 전 각 호스트에서 락을 잡고 로컬이 알고 있는 commit과 실제 배포 상태를 비교해 stale 계획을 중단하며, 영향받는 모든 호스트의 락을 확보한 경우에만 진행함
  • 서비스는 systemd 유닛으로 실행되며 설정 변경 시 재시작하고, 시작 실패 시 이전 known-good 버전으로 링크를 되돌린 뒤 다시 재시작해 밀리초 단위 자동 롤백을 수행함
  • 원격 실행은 SSH를 전송 계층으로만 쓰는 정적 에이전트 방식이며, Flatcar Linux처럼 Python과 패키지 관리자가 없는 환경에서도 coreutils만으로 자동 설치 가능함

Deptool을 만든 배경

  • 유럽 디지털 주권에 대한 글을 미국 호스팅과 미국 통제 하이퍼스케일러 위에 올리는 모순을 피하려고 블로그를 유럽으로 옮기는 작업에서 출발함
  • DNS도 Cloudflare에 의존하고 있었기 때문에 DNS 서버를 직접 운영할 필요가 생김
  • 기존 웹서버는 작은 VM에서 Nginx와 인증서 갱신용 Lego를 실행하고, Nginx 설정은 Nix로 생성한 뒤 작은 Python 스크립트로 서버에 복사하고 Nginx를 재시작하는 방식이었음
  • DNS 서버를 운영하려면 최소 두 대의 서버, 더 많은 systemd 유닛, 설정 파일, zonefile이 필요해져 기존 스크립트로는 부족해짐
  • NixOS로 전환하는 선택지도 있었지만, 최소한의 기본 OS와 읽기 전용 chroot에서 필요한 바이너리만 포함해 서비스를 실행하는 현재 방식을 유지하고 새 배포 도구를 만들기로 함

Deptool의 사용 모습

  • Deptool은 클러스터 설정 변경 계획을 먼저 보여주고, 확인을 받은 뒤 대상 호스트에 적용함
  • DNS 레코드 업데이트 예시는 deptool deploy 실행 후 s4.ruuda.nl과 s5.ruuda.nl에서 nsd 설정 파일 변경과 nsd.service 재시작을 계획으로 보여줌
  • 배포 실패 시 자동 롤백이 적용되며, 예시에서는 prod 클러스터의 2개 호스트에 적용할지 확인한 뒤 0.99초 만에 성공함
  • 출력은 대상 호스트, 변경 애플리케이션, 변경 파일, 재시작할 systemd 유닛을 분리해 보여줘 배포 전에 실제 수행될 작업을 확인할 수 있게 함

원하는 배포 도구의 조건

  • 빠름

    • 설정 업데이트는 1초 미만이어야 하며, 대서양 횡단 ping도 100ms 수준이므로 본질적으로 더 느려야 할 이유가 없다고 봄
  • 예측 가능함

    • 도구는 수행할 작업을 먼저 보여주고 그대로 실행해야 함
    • OpenTofu처럼 계획(plan)적용(apply) 단계를 분리하는 방식을 원함
    • Ansible의 check mode는 명령형 단계가 실행된 뒤에야 연쇄 변경이 드러날 수 있고, check와 실제 실행 사이에 호스트 상태가 바뀌는 것도 막지 못해 신뢰하기 어렵다고 봄
  • 안전함

    • Nginx 설정이 깨져도 웹서버가 몇 분 동안 내려가 있지 않도록, 도구가 밀리초 단위로 자동 롤백해야 함
  • 단순함

    • 필요한 것은 노트북에서 서버로 설정 파일을 복사하고 몇 개의 systemd 유닛을 재시작하는 것뿐임
    • 모든 배포 문제를 해결하거나 제어 흐름, 임의 코드 실행을 제공할 필요는 없음
    • 설정 파일 템플릿 처리는 별도 도구로 할 수 있으며, YAML 템플릿에 대한 문제의식은 generate별도 파일 생성 도구로 분리됨
  • 선언적이어야 함

    • 설정에서 파일이나 애플리케이션을 제거하면 서버에서도 제거되어야 함
    • 명시적인 정리 단계를 추가하지 않아도 되고, 잊어버려서 drift나 남은 파일이 생기지 않아야 함
  • 초기 설정이 없어야 함

    • 서버를 프로비저닝한 직후부터 관리할 수 있어야 함
    • 에이전트, 데몬, 의존성을 수동 설치하거나 호스트를 등록하는 절차가 필요하면, 그 절차를 다시 자동화해야 하는 문제가 생김

설정 생성과 배포의 분리

  • 핵심 아이디어는 설정 생성배포를 분리하는 것임
  • 직장에서 David가 만든 Unsible은 Ansible playbook을 단계별로 실행하지 않고, 로컬에서 tarball을 만든 뒤 호스트로 보내 파일을 배치하는 방식임
  • 기존의 단순 배포 스크립트도 외부에서 설정을 빌드하고 스크립트는 파일 복사에 가까운 역할을 했음
  • NixOS도 이 아이디어를 로컬 시스템에 적용한 것으로 볼 수 있으며, Nix에서 배울 수 있는 점은 생성된 산출물을 여러 버전이 공존할 수 있는 위치에 저장하고, system administration의 명령형 부분을 몇 개의 심볼릭 링크를 바꾸는 작은 활성화 단계로 제한하는 것임
  • 이 설계는 패키지 관리와 시스템 설정 모두에 잘 맞음

Deptool의 동작 방식

  • 전체 클러스터 설정을 미리 렌더링함

    • 전체 클러스터의 설정 파일을 미리 생성해 디스크의 디렉터리에 저장함
    • 디렉터리 트리는 두 단계 깊이이며, 최상위에는 대상 호스트별 디렉터리, 그 아래에는 애플리케이션별 디렉터리가 있음
  • Git 저장소에 넣음

    • 설정 디렉터리를 Git 저장소에 넣으면 버전 간 차이를 비교할 수 있고, 전체 클러스터에서 무엇이 바뀌었는지 확인 가능함
    • diffstat으로 영향을 받는 호스트와 변경된 앱을 알 수 있으며, 각 설정 파일의 정확한 diff를 볼 수 있음
  • 호스트의 격리된 디렉터리에 파일을 실체화함

    • 모든 파일은 /var/lib/deptool 아래에 두어 다른 요소와 간섭하지 않게 함
    • 배포할 commit 이름으로 디렉터리를 만들기 때문에 여러 버전이 디스크에 공존 가능함
    • current 심볼릭 링크가 배포된 버전을 가리키게 해 버전을 원자적으로 바꿀 수 있음
    • 삭제된 파일은 다음 버전에 실체화되지 않으므로 남은 파일이 생기지 않음
    • 특정 위치에 파일을 요구하는 애플리케이션에는 파일시스템의 필요한 위치에서 /var/lib/deptool로 향하는 심볼릭 링크를 만들 수 있음
    • 심볼릭 링크 생성·삭제는 원자적이지 않지만, 파일 내용 수정 때가 아니라 링크 추가·삭제 때만 필요함
    • 이후 배포 버전에 해당 심볼릭 링크가 포함되지 않으면 diff를 통해 삭제해야 함을 알 수 있어 파일이 남지 않음
  • 원격 추적 ref로 배포 상태를 기록함

    • 운영자 노트북에서 각 호스트에 배포된 commit을 추적함
    • 배포 상태는 클러스터 전체 속성이 아니라 호스트별 속성
    • 어떤 변경이 특정 호스트에 영향을 주지 않으면 그 호스트에 새 commit을 배포할 필요가 없음
    • 이 정보로 클러스터 diff를 오프라인에서 계산할 수 있고, 이 diff가 배포 계획이 되어 밀리초 단위로 표시 가능함
  • 배포 전 대상 호스트에서 락을 획득함

    • SSH로 연결해 락 요청을 보내며, 요청에는 해당 호스트에 배포됐다고 생각하는 commit을 포함함
    • 락을 획득하면 계획이 유효했고, 락을 해제할 때까지 다른 배포가 해당 호스트에서 진행될 수 없어 계획이 계속 유효함
    • 변경의 영향을 받는 모든 호스트의 락을 보유한 경우에만 배포가 진행됨
    • ref가 오래되어 다른 무언가가 호스트에 배포된 상태라면 계획은 stale 상태이므로 중단함
    • 이후 로컬 ref를 갱신하면 다음 실행에서 최신 계획을 볼 수 있음
  • systemd 유닛을 재시작함

    • 모든 서비스는 systemd 유닛으로 실행되고 빠르게 시작되므로, 확실하지 않을 때는 재시작하는 쪽을 택함
    • 애플리케이션 설정이 바뀌면 영향받는 systemd 유닛을 재시작함
    • 유닛 시작이 실패하면 심볼릭 링크를 이전 known-good 버전으로 되돌리고 다시 재시작해 밀리초 단위 자동 롤백이 가능함

낙관적 동시성 모델

  • Deptool 배포에는 낙관적 동시성 요소가 있음
  • 현재 클러스터 상태를 알고 있다고 가정하고 계획을 세우며, 그 가정이 틀렸다면 다시 시도해야 함
  • 경합이 없을 때는 매우 빠르며, 개인 인프라를 한 사람이 같은 노트북에서 배포하는 경우가 여기에 해당함
  • 여러 사람이 계속 배포를 시도하는 환경에서는 한 명만 성공하고 나머지는 재시도해야 하므로 성능이 매우 나빠질 수 있음
  • 이 모델은 git push와 같으며, 수백 명이나 수천 대 서버 규모로 확장되지는 않지만 개인 인프라 용도에는 충분함
  • 직접 도구를 만들면 자신의 정확한 사용 사례에 맞게 최적화할 수 있음

에이전트 구축

  • Flatcar Linux와 초기 호스트 제약

    • 웹서버는 Flatcar Linux에서 실행됨
    • Flatcar Linux는 이미지 기반 OS이며 userspace가 매우 작고, coreutils와 Bash는 있지만 패키지 관리자와 Python은 없음
    • 공격 표면을 줄이는 데는 좋지만 무언가를 설치하기에는 불리함
    • 도구가 작동하려고 먼저 무언가를 설치해야 한다면, 그 설치 과정을 자동화해야 하는 새 문제가 생김
  • SSH를 전송 계층으로만 사용함

    • 새 호스트를 바깥에서 관리해야 하므로 SSH와 passwordless sudo를 사용할 수 있음
    • 명령을 SSH로 직접 실행하는 방식은 handshake가 느릴 뿐 아니라 argv가 SSH 경계를 안전하게 넘지 못하고, shell-over-SSH의 word splitting과 escaping 문제를 다뤄야 함
    • Deptool은 예측 가능한 위치에 있는 인자 없는 단일 프로그램을 실행하고, 이 프로그램을 에이전트로 사용함
    • 에이전트는 stdin에서 메시지를 읽고 stdout으로 응답함
    • SSH는 소켓처럼 전송 수단으로만 쓰이며, 사용자 제어 입력이 SSH나 shell 명령에 들어가지 않아 escaping 문제를 피함
  • 정적 바이너리를 사용함

    • 에이전트는 정적 바이너리로 빌드함
    • 커널 외에 무엇이 있는지 가정하지 않고, 유용한 일을 하기 전에 수 MB 코드를 파싱해야 하는 인터프리터도 필요 없음
    • Ansible은 최악의 결함을 mitigate한 뒤에도 연결할 때마다 수 MB의 Python 모듈을 전송하며, Flatcar에는 Python도 없음
  • commit 기반 경로에 바이너리를 둠

    • 에이전트 바이너리는 빌드된 commit을 포함한 경로에 저장함
    • 양쪽 연결 끝이 같은 버전을 실행하도록 보장해 프로토콜 호환성 문제가 생기지 않게 함
    • 경로는 /var/lib/deptool/bin/deptool-<version>-<commit> 형식임
  • 먼저 바이너리가 있다고 가정함

    • SSH handshake는 비용이 크므로, 프로브나 멱등 설치 단계에 낭비하지 않음
    • 에이전트 바이너리는 약 1.6MB로 전송이 금지될 정도로 크지는 않지만 무료도 아님
    • 클러스터 설정 변경은 Deptool 업데이트보다 훨씬 자주 일어나므로, 보통은 바이너리가 이미 존재한다고 봄
  • 실행 실패 시 바이너리를 설치함

    • 바이너리 시작에 실패하면 두 번째 SSH 연결로 설치를 수행함
    • 실행 명령은 다음과 같음
uname -sm && sudo mkdir -p /var/lib/deptool/{bin,apps,store} && sudo dd status=none of=<remote_bin_path> && sudo chmod +x <remote_bin_path> && sudo sha256sum <remote_bin_path>
  • 먼저 stdout에서 한 줄을 읽어 uname 출력을 얻고, 이를 통해 OS와 CPU 아키텍처를 확인해 해당 플랫폼용 에이전트 바이너리를 보냄
  • 바이너리를 stdin에 쓰면 원격의 dd가 디스크에 기록함
  • 마지막으로 stdout에서 한 줄을 더 읽어 원격에서 계산된 shasum을 확인하고, 전송 성공 여부를 검증함
  • 이 과정은 표준화된 coreutils 프로그램에만 의존함
  • 이후 에이전트 실행을 다시 시도하면 성공해야 하며, 에이전트는 디스크가 가득 차지 않도록 오래된 버전을 정리함

에이전트 방식의 효과와 비용

  • 원격 호스트에서 에이전트를 실행하고 통신하는 방법을 얻음
  • 원격 호스트에 coreutils 외의 요구사항 없이 자동 설치가 가능함
  • 양쪽이 같은 버전을 실행하므로 프로토콜 호환성이 구조적으로 보장됨
  • 사용자 제어 입력은 SSH 기반 소켓을 통해서만 전달되고 SSH나 shell 명령에 들어가지 않아 escaping 문제와 길이 제한을 피함
  • 일반적인 경우에는 SSH handshake 한 번만 필요해 지연 시간이 작음
  • 새 머신에 배포하거나 도구 업데이트 뒤처럼 흔하지 않은 경우에는 추가 연결 2번과 1회성 1.6MB 전송이 필요함
  • ControlMaster를 쓰면 이후 연결 오버헤드 대부분을 건너뛸 수 있어 전체 비용은 몇 초 수준임
  • 이 경우에는 1초 미만 배포는 아니지만 Ansible보다는 낫다고 봄
  • 설정을 배포하고 조금 수정한 뒤 다시 배포하는 흐름에서는 SSH가 기본 연결을 유지할 수 있어 배포가 즉각적으로 느껴짐

사용 결과와 공개

  • Deptool은 지난 한 달 동안 개인 인프라 관리에 사용됨
  • 연결하기 전에 정확한 계획을 즉시 볼 수 있고 자동 롤백이 있다는 점이 좋지만, 가장 큰 변화는 1초 미만 배포
  • 올바른 배포 방식이 몇 분 걸리면 피드백 루프를 줄이기 위해 서버에서 직접 파일을 편집하고 싶어지지만, Deptool에서는 로컬에서 수정하고 배포하는 편이 SSH로 서버에 접속해 편집기를 여는 것보다 빠름
  • 마찰이 가장 적은 방식이 올바른 방식이 되며, 적용된 모든 수정은 Git 히스토리에 기록됨
  • 무언가를 깨뜨려도 깨졌다는 사실을 인식하기 전에 Deptool이 롤백함
  • Deptool은 개인 문제를 정확히 해결하기 위해 만들어졌고, 모든 사람의 모든 배포 문제를 풀려 하지 않는 점이 해당 사용 사례에서 빛나게 하는 요소임
  • 특히 이미지 기반 운영체제에서 유용할 수 있으며, CodebergGitHub에 공개되어 있고 자세한 manual도 제공됨
Read Entire Article