엔지니어링 노트설치·운영과 품질

설치 전 점검은 호스트를 읽기만 하고, 고칠 명령은 사람에게 넘긴다

구축형 OwlAI는 고객이 고른 호스트 한 대에 Docker Compose로 올라갑니다. 배포판과 커널, 도커 판, 보안 모듈, 인증서는 고객마다 다릅니다. 설치 날 compose를 올린 뒤 dockerd가 뜨지 않거나, 문서 생성이 전부 거부되거나, 데스크톱 앱이 서버에 붙지 못하면 원인이 호스트에 있는지 제품에 있는지부터 가려야 합니다. 증상은 제품 쪽에서 보이지만 원인은 대개 설치 명령과 상관없는 호스트 조건이고, 미리 알았다면 전날 맞춰 둘 수 있었습니다.

이 글은 그 판정을 설치 전으로 당기는 설치 전 점검(preflight) 스크립트를 다룹니다. 점검은 호스트를 읽기만 하고, 항목마다 통과·경고·실패 판정 한 줄과 고칠 명령을 냅니다. 같은 점검이 배포와 업데이트 때마다 컨테이너를 바꾸기 전에 다시 돌기 때문에, 설치 뒤에 바뀐 호스트 조건도 다음 배포 전에 걸러집니다.

증상은 제품에서 보이고 원인은 호스트에 있다

설치 날 멈추는 조건과 그때 보이는 증상, 점검 항목을 표로 모았습니다. 증상이 나는 자리와 원인이 있는 자리가 다르다는 점이 공통입니다.

호스트 조건 설치 날 보이는 증상 점검 항목
RHEL/Rocky 10 클라우드 이미지에 docker-ce를 설치하고 재부팅하지 않음 dockerd가 iptables 규칙을 넣지 못해 뜨지 않음. 도커 규칙이 쓰는 netfilter 모듈이 실행 중인 커널에 없음 kernel.netfilter
RHEL/Rocky 8의 8.6 기본 커널 스크립트 실행 사이드카는 뜨지만 문서 생성 스크립트가 전부 거부됨 kernel.el8
Ubuntu 24.04·26.04에서 동봉 AppArmor 프로파일을 적재하지 않음 위와 같음 apparmor.studio_profile
설치 디렉터리에 config.yaml이 없음 도커가 그 자리에 디렉터리를 만들고 백엔드가 설정을 읽지 못해 멈춤 bind.config
rootless 엔진의 기본 cgroup 위임(memory·pids만) rootless podman에서는 CPU 상한이 걸린 서비스가 만들어지지 않고, rootless Docker에서는 상한이 무시된 채 뜸 rootless.cgroup_delegation
자체 서명 인증서 웹 푸시가 등록되지 않고 데스크톱 앱이 접속을 거절함 tls.self_signed
ARM64 호스트 이미지가 x86-64 한 가지라 기동에서 실패 arch
FIPS 모드 호스트 이미지의 암호 모듈이 FIPS 검증 모듈이 아님 fips

RHEL 8 계열의 커널 하한은 4.18.0-425(8.7), 권장은 8.10입니다. 격리가 커널과 보안 모듈에 따라 왜 갈리는지는 「스크립트 샌드박스는 실행마다 세우고, 그 서버에서 띄워 보고 판정한다」에서 다룹니다.

인증서는 사용자가 기능을 처음 쓰는 순간에야 드러납니다. 브라우저 마이크는 보안 컨텍스트(HTTPS 또는 localhost)에서만 열려서, 평문으로 IP에 접속하면 음성 입력 버튼이 나타나지 않습니다. 자체 서명 인증서여도 마이크까지는 되지만, 웹 푸시는 다릅니다. Chromium은 서비스 워커 스크립트를 받을 때 인증서 오류가 있으면 등록을 실패시키므로, 사용자 PC가 그 인증서의 CA를 신뢰해야 합니다. 데스크톱 앱은 인증서 오류를 무시하지 않으므로, 사내 CA로 서명한 서버라면 그 CA를 Windows의 「신뢰할 수 있는 루트 인증 기관」 저장소에 넣습니다. 그래서 점검은 인증서의 만료(30일 전부터 경고), 키 짝, SAN에 사용자가 실제로 치는 주소가 들었는지, 자체 서명 여부를 봅니다.

설치 문서만으로는 그 호스트의 상태를 알 수 없다

이 조건들을 설치 문서의 요건 표로만 알리면, 설치 담당자가 커널 빌드 번호나 cgroup 위임 상태를 명령으로 확인해 표와 하나씩 대조해야 합니다. 대조는 설치 날 한 번으로 끝나고, 그 뒤에 도커나 커널을 올리거나 인증서가 만료돼도 문서는 알려 주지 않습니다.

설치 스크립트가 찾은 문제를 바로 고치는 방법도 있습니다. 주소 풀이 겹치면 daemon.json을 고치고, 공유 네트워크가 없으면 만들고, sysctl 값이 틀리면 바꾸는 식입니다. 손이 덜 간다는 장점은 분명하지만, 고치는 일에는 그 호스트의 맥락이 필요합니다. 도커 주소 풀을 어느 대역으로 옮길지는 사내망을 아는 사람만 정할 수 있고, 커널을 올린 뒤 언제 재부팅할지는 그 서버를 함께 쓰는 사람이 정합니다. 고객의 보안 담당자가 설치 전 점검을 돌려도 되느냐고 물었을 때 「읽기만 합니다」라고 답할 수 있어야 하는데, 고치는 기능이 한 군데라도 섞이면 그 답이 성립하지 않습니다.

그래서 점검은 호스트를 읽어 판정만 하고, 고칠 명령은 사람에게 넘깁니다.

점검은 호스트를 읽기만 하고 고칠 명령은 출력에 적는다

점검 스크립트는 파일을 쓰지 않고, 네트워크를 만들지 않고, sysctl과 데몬 설정을 바꾸지 않으므로 몇 번을 돌려도 결과가 같습니다. root가 필요한 고칠 명령은 사람이 root로 실행합니다. 점검을 root가 아닌 계정으로 돌리면 root만 읽을 수 있는 확인(AppArmor 프로파일 목록, SELinux 모듈 목록)은 판정할 수 없음으로 남습니다. 설치 계정으로 한 번, root로 한 번 돌리면 빠지는 항목이 없습니다.

예외는 스크립트 실행 격리를 보는 항목(studio.isolation) 하나입니다. 격리가 서는지는 커널과 보안 모듈의 조합이 정해서, 설정값을 읽는 것만으로는 판정할 수 없습니다. 그래서 이 항목은 반입한 사이드카 이미지로 일회용 컨테이너를 운영과 같은 보안 옵션으로 띄워, 그 안에서 격리를 한 번 세워 봅니다. 이 컨테이너에는 네트워크가 없고, 호스트 경로를 붙이지 않으며, 이미지를 받아 오지도 않습니다. 쓰기 확인에 쓰는 볼륨도 일회용으로 만들고, 시간 초과(90초)나 중단을 포함해 어떤 경우에도 끝나면 컨테이너와 볼륨을 함께 지웁니다. 첫 설치처럼 사이드카 이미지가 아직 호스트에 없으면 이 항목은 판정할 수 없음으로 남고, 배포 뒤의 헬스 체크가 판정합니다.

점검은 비밀값을 출력하지 않고 .env는 변수 이름만 말하므로, 결과를 지원 창구에 그대로 붙여 넣어도 됩니다. openssl·ss 같은 도구가 없으면 그 도구가 필요한 항목만 판정할 수 없음으로 남깁니다.

항목마다 판정 한 줄과 고칠 명령이 나온다

점검 항목은 일흔 개가 넘고, 배포판·커널, 도커 엔진과 compose, 네트워크와 프록시, 설치 디렉터리의 .env·설정·이미지·인증서, 이미 떠 있는 스택의 상태까지 봅니다. 항목마다 한 줄이 나오고, 상태는 넷 중 하나입니다.

  • PASS: 통과
  • WARN: 동작은 하지만 위험하거나 검증한 조합에서 벗어남
  • FAIL: 이대로 배포하면 안 됨
  • SKIP: 판정할 수단이 없음(필요한 명령이나 권한이 없음)

FAIL이 하나라도 있으면 종료 코드는 1입니다. 고칠 것이 있는 항목에는 fix: 줄이 붙습니다. RHEL 8.6 호스트에 자체 서명 인증서를 둔 설치라면 이렇게 나옵니다.

[FAIL] kernel.el8               RHEL/Rocky 8 kernel 4.18.0-372.9.1.el8.x86_64 is older than 4.18.0-425 (8.7): studio's sandbox fails on 8.6 ('bwrap: Can't open source /usr: Function not implemented')
                                fix: update the kernel to 8.7 or newer (8.10 recommended; a host pinned to 8.6/EUS must move to a later minor release) and reboot
[WARN] tls.self_signed          self-signed certificate: every browser and the desktop app must trust it (web push refuses certificate errors)
                                fix: distribute the CA to clients (Windows: Trusted Root Certification Authorities)

항목 ID는 점으로 묶은 고정 이름이고, 설치 요건 문서의 배포판별 표에도 요건마다 같은 ID를 적었습니다. 점검 결과에서 문서로, 문서에서 점검 항목으로 ID 하나로 건너갈 수 있습니다.

판정 기준값은 스크립트 머리에 상수로 모으고 근거를 함께 적었습니다. 디스크라면 동시에 도는 턴(질문 하나를 받아 답을 내기까지의 한 번의 질의응답) 72개가 각자 대화 저장 상한 512 MiB씩 받아 놓은 상태에 이미지 5 GiB와 여유를 더한 45 GiB가 FAIL 기준이고, 턴 폴더가 모두 1 GiB까지 자란 최악치인 80 GiB가 WARN 기준입니다.

배포와 업데이트는 컨테이너를 바꾸기 전에 같은 점검을 돌린다

배포 스크립트는 컨테이너를 하나라도 바꾸기 전에 이 점검을 돌리고, FAIL이 있으면 아무것도 바꾸지 않고 멈춥니다. 빠진 비밀값, config.yaml 자리의 디렉터리, 포트 충돌, 사내 대역과 겹치는 도커 네트워크는 교체 도중에 터지면 반쯤 바뀐 스택이 남습니다. 교체 전에 멈추면 이전 판이 그대로 돕니다. WARN은 배포를 막지 않고 배포 로그에 남습니다. 관리 화면에서 적용하는 업데이트도 적용 단계에서 같은 배포 스크립트를 부르므로, 업데이트마다 같은 점검이 먼저 돕니다.

같은 설치 전 점검이 설치 전·배포 전·업데이트 적용 전에 돈다
그림 1. 같은 설치 전 점검이 설치 전·배포 전·업데이트 적용 전에 돈다

배포 안에서는 DNS·레지스트리 조회를 건너뛰는 --offline으로 돌립니다. 이미지를 받는 일은 바로 다음 단계가 하고, 폐쇄망 설치에는 조회할 레지스트리가 없기 때문입니다.

사내 CA·프록시 교체, 시계 어긋남, 도커를 올리며 내려간 파일 디스크립터 한도처럼 업데이트 사이에 바뀌는 조건은 백엔드와 워커가 기동할 때 보고 경고 로그로 남깁니다. 대부분은 언젠가 어떤 호출이 실패할 수 있다는 위험이지 지금 확정된 장애가 아니고, 기동을 막으면 재시작이 되풀이되면서 원인이 더 가려지기 때문입니다. 외부 HTTP 클라이언트를 하나도 만들 수 없게 하는 NO_PROXY 값처럼 확정된 장애만 기동을 멈추고 이유를 적습니다.

설계에서 고른 것

고치는 기능 대신 읽기 전용을 골랐습니다. 그 대가로 설치 담당자는 fix: 줄의 명령을 읽고 직접 실행해야 합니다. 그 대신 점검은 보안 검토에서 「읽기만 한다」는 한 문장으로 설명되고, 배포와 업데이트 안에서 몇 번을 돌아도 호스트 설정을 바꾸지 않습니다. 읽기만으로 판정할 수 없는 스크립트 실행 격리 하나만 일회용 컨테이너로 띄워 보고, 그 컨테이너와 볼륨은 어떤 경우에도 지웁니다.

판정은 검증한 조합만 통과시키는 쪽으로 골랐습니다. RHEL 8 계열은 VM에서 실제로 설치해 판정한 커널 하한(4.18.0-425) 밑을 FAIL로 막습니다. 8.6 기본 커널과 하한 사이의 확인하지 않은 빌드도 여기에 듭니다. 배포판 기본이 아닌 커널이나 시험하지 않은 도커 판(25~28)에는 WARN을 냅니다. 모르는 조합을 통과시키면 더 많은 호스트에서 설치를 시작할 수는 있지만, 그 조합이 깨지는 시점은 설치 날입니다. 이미지는 x86-64 한 가지로 굽고 서명해 고객이 받는 이미지가 검증한 이미지와 같게 했습니다. 그래서 ARM64 호스트와, 이미지의 암호 모듈이 FIPS 검증 모듈이 아니어서 빠지는 FIPS 모드 호스트는 첫 기동에서 깨지는 대신 점검 결과 첫 화면에서 FAIL로 드러납니다.

점검은 알려진 실패 조건을 판정하고, 처음 보는 실패는 배포 뒤의 헬스 체크가 받습니다. 스크립트 실행 사이드카는 실제 실행과 같은 명령으로 격리를 세워 보고 결과를 헬스 체크에 싣습니다(「스크립트 샌드박스는 실행마다 세우고, 그 서버에서 띄워 보고 판정한다」). 그래서 점검이 모르는 조합이 격리를 막아도 배포 판정에서 드러납니다.

고객이 고른 호스트에 설치하는 제품이라면, 설치 문서의 요건마다 그 요건을 읽어 판정하는 항목을 하나씩 두고, 그 점검을 배포 스크립트가 교체 전에 다시 돌리게 하는 것부터 시작할 만합니다.

어떤 문제를 해결하나요?

설치·운영과 품질의 다른 글