2026-09-07 (월) 개발일지
오늘의 회고 — 설치 자동화 프로그램 두 가지 이슈
오늘은 로그프레소 설치 자동화 프로그램에서 센트리 인증서 동기화 실패와 설치 단계 재시작 로직의 모호함을 잡았다. 첫 번째는 암호 키가 3개로 갈라져 있던 걸 정리한 케이스, 두 번째는 “단계를 중간부터 다시 돌려도 안전한가”라는 질문에서 출발해 설계를 단순 번호 비교로 정리한 이야기다.
센트리 ↔ 전달 노드 TLS — 인증서 서명 검증 실패의 진짜 원인
전달 노드에 센트리를 재설치(base=<INTERNAL_IP> = VIP .55)하고 웹콘솔에 등록했는데, 로그에서 Certificate signature validation failed 가 계속 떴다. 처음엔 전달 노드가 CA 인증서를 제대로 안 받은 줄 알았다. 마스터 노드의 /data/logpresso/conf/ca/ 를 열어보니 CA/RPC/웹 인증서는 정상 발급돼 있었다. 문제는 전달 노드가 RPC 인증서를 못 받은 게 아니라, 받았는데 서명이 달랐다.
/data/logpresso/conf/cert/rpc/ 를 분석 노드 2대·전달 노드에서 각각 비교했다. 놀랍게도 KEK(Key Encryption Key)가 노드마다 3가지로 갈라져 있었다. 즉, 같은 CA가 서명했어도 각 노드가 서로 다른 KEK로 자신의 키를 암호화해 저장하고 있었고, 센트리 ↔ 전달 노드 TLS 핸드셰이크에서 상대방의 KEK로 복호화를 시도하다가 서명 불일치가 발생한 거였다.
결정적 발견:
cluster.py의_sync_certs()에서 KEK를 수집할 때, 현재 노드의 KEK만 푸시하고 나머지 노드의 KEK는 수집 후 정렬을 안 하고 있었다. KEK 목록에 중복·누락이 섞여 들어가면서 노드 간 동기화가 깨졌다.
원인을 파고들자 더 근본적인 문제가 보였다. versions.py 에 KEK 생성 로직이 generate_kek() 하나뿐인데, 이 함수가 설치 단계마다 호출될 때마다 새로운 KEK를 생성하고 있었다. guid 단계에서 한 번, master 단계에서 또 한 번 — 설치 도중 재시작이 들어가면 KEK가 덮어써지거나 갈라지는 구조였다.
해법: KEK 생성을 preflight 단계로 고정하고, 이후 단계에서는 생성된 KEK를 읽기만 하게 변경했다. _sync_certs() 에는 KEK 수집 후 정렬 로직(sorted(set()))을 추가해 중복을 제거하고 순서를 일정하게 맞췄다.
바뀐 점
| 항목 | before | after |
|---|---|---|
| KEK 생성 시점 | guid, master 등 여러 단계 |
preflight 단계에서 1회만 |
| KEK 동기화 | 현재 노드 KEK만 밀어넣음 | 전 노드 KEK 수집 → 정렬 → 균등 분배 |
검증: 전달 노드에서 curl -k --cert /data/logpresso/conf/cert/rpc/rpc.crt --key /data/logpresso/conf/cert/rpc/rpc.key 로 센트리 TLS 연결을 찔렀더니 200 OK. Certificate signature validation failed 가 사라졌다.
남은 과제: 이미 KEK가 갈라진 상태로 설치된 환경은 재설치가 필요한데, 업그레이드 스크립트에서 KEK 재정렬을 지원할지 결정해야 한다.
실행 탭 시작 단계 — 중간 재시작 안전성에 대한 오해
설정 화면의 시작 단계 드롭다운은 preflight→osprep→mariadb→pkg→federation→guid→master→ha→verify 순서로 되어 있다. 초기 구현은 단순했다 — 시작 단계에서 고른 단계부터 끝까지 순차 실행. 그런데 “verify 까지 갔다가 중간에서 실패하면 처음부터 다시 해야 하나요?” 라는 질문을 받고 내가 당황했다.
처음엔 “당연히 중간부터 다시 시작할 수 있다”고 답하려다가, 실제 코드를 다시 보니 문제가 있었다. engine.py의 _run_stage() 는 각 단계가 멱등성(idempotent)을 가정하고 있었지만, 그 가정을 증명하는 코드가 없었다. 예를 들어 mariadb 단계가 이미 설치된 DB를 또 깔려고 하면 포트 충돌이 났다.
가설을 세웠다: 모든 단계를 여러 번 돌려도 결과가 같게 만들면 중간 재시작이 안전하다. 증명은 각 단계의 진입점에 상태 검사를 넣는 걸로 시작했다.
mariadb.py를 열었다. setup_mariadb() 안에 if os.path.exists('/var/lib/mysql/mysql.sock') 분기를 추가해, 이미 DB가 떠 있으면 dpkg -i 를 건너뛰고 설정 파일만 비교·갱신하게 했다. pkg 단계는 /opt/logpresso/logpresso 경로가 존재하면 압축 풀기를 생략. guid 단계는 이미 할당된 GUID가 있으면 /data/logpresso/conf/guid 파일을 덮어쓰지 않게.
가장 까다로웠던 건 federation 단계였다. 이 단계는 노드 간 암호·열쇠를 교환하는데, 한 번 교환된 키를 다시 밀어넣으면 노드 간 혼선이 생긴다. _check_federation_done() 을 추가해, /data/logpresso/conf/federation/peers 에 이미 상대 노드의 지문이 등록되어 있으면 federation 단계를 스킵하게 했다.
해법: 각 단계에 “이미 이 단계가 완료됐는가” 를 묻는 _check_<stage>_done() 을 추가하고, 완료 시 skip. engine.py의 _run_stage() 는 skip 여부를 로그에 남기고 다음 단계로 넘어간다.
핵심 전환점: “중간부터 다시 시작해도 된다”고 말하려면, “다시 돌려도 결과가 같다”는 증명이 필요했다. 증명은 각 단계 진입점에서 상태를 체크하는 코드 한 줄에서 시작됐다.
검증: preflight → verify 까지 전 단계를 돌린 뒤, 시작 단계를 mariadb 로 설정하고 다시 실행했다. 로그: [SKIP] mariadb — already installed → [SKIP] pkg — binary exists → … → [RUN] verify. verify 만 실제로 실행됐고, 기존 설치를 건드리지 않았다.
남은 과제: 08-쉬운-설명서.md 에 이 로직을 초등학생 비유로 풀어썼다. “번호 비교 하나뿐”이라는 문장으로 추상화했는데, 실제 사용자가 드롭다운에서 단계를 선택할 때 “이 단계가 이미 끝났는지”를 시각적으로 표시하지는 않는다. v2에서 각 단계 옆에 ✅/⏳ 아이콘을 붙이는 게 맞을 것 같다.
오늘의 한줄
“중간부터 다시 시작해도 안전하다”는 말을 코드로 증명하려면, 각 단계가 멱등성을 가져야 한다 — 그리고 멱등성은 상태 검사라는 단순한 if 문에서 시작된다.