FDE 캡스톤: 창고가 같은 주문을 세 번 받았다 · 설치 전에 막을 수 있던 실패 · 이론
설치는 성공했는데 포트는 이미 남의 것이었다
한 줄 요약
사전 점검(preflight)은 설치 전에 고객 호스트가 요구사항을 만족하는지 읽기만 해서 판정하고, 그 결과를 사람이 아니라 다음 단계의 자동화가 읽을 수 있는 보고서와 종료 코드로 남기는 일이다.
왜 이게 필요했나
고객 현장의 설치 창은 대개 한 번뿐이다. 목요일 밤 22시에 인프라팀이 문을 열어 주고, 자정이면 닫는다. 이런 자리에서 설치 스크립트가 "완료" 로 끝났다는 사실은 거의 아무것도 보장하지 않는다. 파일은 제자리에 복사됐지만 에이전트가 쓰려던 포트는 사내 프록시가 이미 쥐고 있어서 뜨자마자 죽을 수 있고, 고객이 건넨 인증서는 설치 3주 전에 만료돼 있을 수 있다. 둘 다 설치 뒤에 로그를 뒤져야 알게 되지만, 설치 전에 5분이면 알 수 있는 사실이다.
FDE 가 고객에게 인도하는 것은 코드가 아니라 동작하는 결과다. 그래서 설치 절차 앞에 "이 호스트에서 설치가 성공할 조건이 갖춰졌는가" 를 따로 묻는 단계를 둔다. 이 단계를 사람의 체크리스트로 두면 매번 누군가 빠뜨린다. 스크립트로 만들고 결과를 파일로 남기면, 고객에게 "설치를 막는 것은 이 두 가지이고 나머지는 경고" 라고 근거를 붙여 말할 수 있다.
어떻게 동작하나
점검기는 요구사항 명세를 입력으로 받고, 점검마다 pass·warn·fail 중 하나와 관측값을 낸다. 관측값이 중요하다. "포트 실패" 만 적힌 보고서는 반박할 수 없지만 검증할 수도 없다. observed: in_use 와 잰 경로·여유 MiB·notAfter 시각이 함께 있어야 고객 담당자가 자기 눈으로 다시 확인한다.
종료 코드는 모니터링 플러그인 관례를 빌리면 설명이 쉽다. [Monitoring Plugins 개발 지침](https://www.monitoring-plugins.org/doc/guidelines.html)은 0 OK, 1 Warning, 2 Critical, 3 Unknown 을 쓰고, 3 은 인자가 잘못됐거나 점검기 자신이 돌지 못한 경우로 한정한다. 이름 해석 실패나 소켓 시간 초과 같은 상위 오류는 Unknown 으로 올리지 말라고 적어 둔 점이 눈여겨볼 대목이다. 호스트 이름이 안 풀리는 것은 "모르겠다" 가 아니라 설치를 막는 사실이기 때문이다.
점검 하나하나에는 흔한 함정이 있다.
- 포트. 비었는지 보려면 실제로 bind 해 본다. 그런데 옵션 없이 bind 하면 방금 끝난 연결이 TIME_WAIT 로 남은 포트도 사용 중으로 나온다. 파이썬 [socket 문서](https://docs.python.org/3/library/socket.html)는
create_server()설명에서 TIME_WAIT 로 남은 주소를 곧바로 다시 쓰려고 POSIX 에서SO_REUSEADDR를 켠다고 적는다. 실습 이미지에서 실측하니, 듣는 소켓이 있는 포트는 이 옵션을 켜도 EADDRINUSE 였고 TIME_WAIT 만 남은 포트는 옵션을 켜야 bind 가 됐다. 켜지 않은 점검기는 멀쩡한 설치를 막는다. - 디스크. [shutil.disk_usage](https://docs.python.org/3/library/shutil.html)는 total·used·free 를 바이트로 준다. 실측에서 free 는 statvfs 의
f_bavail(권한 없는 사용자가 쓸 수 있는 블록) 에 조각 크기를 곱한 값과 같았다. 아직 없는 설치 경로는 만들지 말고 존재하는 가장 가까운 부모를 재고, 잰 경로를 보고서에 적는다. - 인증서. 파이썬 [ssl 모듈](https://docs.python.org/3/library/ssl.html)에는 PEM 파일을 읽어 notAfter 를 꺼내는 공개 함수가 없다. 대신
ssl.cert_time_to_seconds()가"%b %d %H:%M:%S %Y %Z"모양의 notAfter 문자열을 epoch 초로 바꿔 준다. 문자열은 [openssl x509](https://docs.openssl.org/3.0/man1/openssl-x509/)의-enddate로 얻는다. 같은 문서의-checkend는 주어진 초 안에 만료되면 0 이 아닌 값으로 끝나지만, 남은 일수를 보고서에 적으려면 날짜를 직접 계산하는 편이 낫다. - 파이썬 판. "3.12.3" 과 "3.9" 를 문자열로 비교하면 3.12 가 더 작다. 정수 튜플로 비교한다.
- 쓰기 권한. [os 문서](https://docs.python.org/3/library/os.html)는
os.access()로 확인한 뒤 여는 방식이 확인과 사용 사이의 틈을 만든다며 EAFP, 곧 실제로 해 보고 예외를 받는 방식을 권한다. 읽기 전용 마운트처럼 권한 비트가 말해 주지 않는 이유도 있으니, 임시 파일을 실제로 만들고 곧바로 지운다.
명세(spec.json) ──▶ preflight.py ──▶ report.json (점검마다 status + observed) │ └──▶ 종료 코드 0 · 1 · 2 (명세를 못 읽으면 3, 보고서 없음)현장에서 만나는 모습
가장 자주 보는 실수는 점검기가 고쳐 주는 것이다. 데이터 디렉터리가 없으면 만들고, 포트를 쥔 프로세스를 죽이고, 만료 인증서를 자체 서명으로 바꿔 넣는다. 그 순간 점검 결과는 "원래 이 호스트가 어땠는가" 를 잃는다. 게다가 디렉터리 소유권, 포트를 쥐고 있던 사내 프록시, 인증서 발급 절차는 전부 고객 조직의 결정이다. 사전 점검은 사실을 드러내고, 고치는 일은 담당자가 합의한 절차로 한다. 이번 고객 메모에도 작업 디렉터리는 인프라팀이 만들어 주기로 했다고 적혀 있다.
두 번째는 경고와 실패를 섞는 것이다. 여유 공간이 공급사 최소치는 넘지만 운영팀 기준보다 적다면 설치는 가능하다. 이것까지 실패로 올리면 설치 창을 날리고, 아예 안 알리면 두 달 뒤 디스크가 찬다. 경고 등급을 따로 두는 이유다.
실무에서 진짜 중요한 것
- 보고서는 다시 잴 수 있어야 한다. 판정만 적지 말고 관측값과 잰 대상을 함께 남긴다.
- 점검기는 호스트를 바꾸지 않는다. 쓰기 시험의 임시 파일까지 지운다.
- 종료 코드는 약속이다. 설치 자동화가 1 은 진행, 2 는 중단으로 읽게 만든다.
- 보고서가 낡으면 근거가 아니다. 설치 직전에 다시 돌리고, 판정 기록에 어떤 보고서였는지 해시를 남긴다.
다음 실습에서 할 것
고객 메모를 명세로 옮기고, 포트·TIME_WAIT·디스크·인증서·파일·호스트·쓰기 점검을 차례로 구현한다. 채점기는 매번 다른 포트와 기준값, 직접 만든 임박·만료 인증서로 여러분의 점검기를 실행해 판정과 관측값을 대조한다. 마지막에는 고객 명세로 실제 판정을 내리고 go / no-go 기록을 남긴다.