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

설치 키트에 넣을 파일은 사람이 아니라 코드가 고른다

구축형 제품의 저장소에는 고객에게 줄 파일과 사내에만 둘 파일이 함께 있습니다. 운영 compose와 설치 문서는 고객에게 가고, 사내 테스트 환경 설정과 사내 빌드 절차 문서는 사내에 남아야 합니다. 사람이 디렉터리를 골라 tar로 묶으면 두 방향으로 틀립니다. 작업 트리에 있던 .env나 사내 서버 주소가 적힌 문서가 함께 묶여 모든 고객에게 가고, 배포 스크립트가 부르는 스크립트 하나가 빠진 키트는 고객 호스트에서 설치나 업데이트가 멈춘 뒤에야 드러납니다. 묶는 순간에는 어느 쪽도 보이지 않습니다.

구축형 OwlAI의 설치 키트(폐쇄망 고객이 이미지 번들 옆에 받는 compose·운영 스크립트·문서 묶음, 「배포와 폐쇄망 적재는 태그 대신 다이제스트로 확인한다」)는 코드가 고릅니다. 후보는 git이 추적하는 파일뿐이고, 넣을 파일은 사유가 붙은 규칙으로 정하며, 규칙이 아무것도 고르지 못하거나 분류되지 않은 문서가 있으면 빌드가 멈춥니다. 고른 파일에는 머지 리퀘스트마다 도는 사내 좌표 검사를 똑같이 다시 겁니다. 이 글은 이 네 가지가 각각 무엇을 막는지 설명합니다.

키트 파일을 고르는 단계와 단계마다 빌드가 멈추는 조건
그림 1. 키트 파일을 고르는 단계와 단계마다 빌드가 멈추는 조건

디렉터리째 묶으면 넣을 것과 뺄 것을 사람이 기억해야 한다

흔한 방법은 필요한 디렉터리를 통째로 묶고 뺄 파일을 제외 목록에 적는 것입니다. 파일을 하나하나 정하지 않아도 되고, 새 스크립트를 만들면 저절로 들어갑니다. 대신 이 방법은 뺄 것을 사람이 기억할 때만 맞게 돕니다. 누군가 작업 트리에 접속 정보 파일을 만들고 제외 목록에 올리지 않으면 그 파일은 다음 키트에 실립니다. 사내 전용 운영 문서도 같은 디렉터리에 있으면 새로 쓴 문서가 제외 목록에 오르기 전까지 고객에게 갑니다.

빠지는 쪽도 같은 구조입니다. 스크립트를 다른 경로로 옮기고 묶는 명령을 고치지 않으면 그 스크립트는 키트에서 소리 없이 사라집니다. 사내 테스트 환경은 저장소 전체를 체크아웃해 배포하므로 빠진 파일을 알아채지 못합니다. 키트만 받는 고객 호스트에서 배포 스크립트가 그 파일을 부르는 순간 처음으로 드러납니다.

후보는 git이 추적하는 파일뿐이다

키트 생성 도구는 후보 목록을 git ls-files에서 가져옵니다. 작업 트리에만 있는 .env, 로컬 config.yaml, 접속 정보를 담은 환경 파일은 처음부터 후보에 없습니다. 비밀 파일을 만든 사람이 무엇을 기억하든 도구는 그 파일을 보지 않습니다.

키트에 들어갈 파일 가운데 하나라도 작업 트리에서 고친 상태면 빌드가 멈춥니다. 키트의 내용은 커밋의 내용과 같아야 하기 때문입니다. 2026년 11월 3일 개발 브랜치 기준으로 추적 파일 6,722개 가운데 키트에 드는 파일은 32개이고, 여기에 도구가 만드는 파일 목록·체크섬·README 세 개가 더해집니다.

릴리스 파이프라인은 이미지 번들보다 키트를 먼저 만듭니다. 키트는 몇 초면 끝나므로, 검사에 걸리면 기가바이트 단위의 이미지 번들을 굽기 전에 파이프라인이 멈춥니다.

넣을 파일은 규칙으로 정하고, 정하지 못하면 빌드가 멈춘다

추적 파일 가운데 무엇을 넣을지는 세 목록이 정합니다. 세 목록의 모든 항목에는 사유가 한 줄씩 붙어 있습니다.

목록 항목 하는 일
넣을 파일 규칙 29개 경로나 패턴으로 키트에 넣을 파일을 고른다
사내 운영 문서 목록 10개 운영 문서 디렉터리에서 고객에게 주지 않을 문서를 이름으로 적는다
차단 목록 6개 어떤 규칙으로도 키트에 들면 안 되는 파일이다. 넣을 파일 규칙이 이 파일을 고르면 빌드가 멈춘다

넣을 파일 규칙에는 운영 compose, .env.example과 config.example.yaml, 배포·점검·이미지 적재·백업·업데이트 스크립트, 스크립트 실행 격리 프로파일(AppArmor·SELinux·seccomp), DB 초기화 SQL, 설치·운영 문서가 있습니다. 파일은 저장소와 같은 배치로 들어갑니다. 배포 스크립트와 설치 전 점검(「설치 전 점검은 호스트를 읽기만 하고, 고칠 명령은 사람에게 넘긴다」)은 자기 위치에서 두 단계 위를 설치 디렉터리로 보고, 운영 compose는 초기화 SQL과 seccomp 프로파일을 상대 경로로 마운트하기 때문입니다.

운영 문서는 패턴으로 넣지 않고 이름으로 하나씩 넣습니다. 운영 문서 디렉터리의 문서 20개 가운데 10개가 넣을 파일 규칙에, 10개가 사내 운영 문서 목록에 이름으로 올라 있습니다. 새 문서가 들어왔는데 두 목록 어디에도 이름이 없으면 빌드가 멈춥니다. 그래서 사내 문서는 좌표가 적혀 있지 않아도 이 분류를 거치지 않고는 키트에 들어가지 못합니다.

차단 목록에는 사내 좌표가 든 설정이나 사내 테스트 스택 정의 같은 사내 전용 파일과, 어느 경로에 있든 비밀값·접속 정보 파일이 있습니다. 이 목록을 「넣지 않을 파일」의 약속으로 두면 넣을 파일 규칙의 패턴이 넓어지는 순간 아무도 모르게 깨집니다. 그래서 들어오면 빌드가 실패하는 조건으로 정했습니다.

빠지는 파일은 두 겹으로 잡습니다. 넣을 파일 규칙 가운데 아무 파일도 고르지 못한 것이 있으면 빌드가 멈춥니다. 파일이 옮겨졌는데 규칙을 고치지 않은 경우입니다. 단위 테스트는 배포·점검·업데이트 스크립트가 부르는 스크립트와, 호스트 준비 스크립트가 불러 읽는(source) 공통 파일이 모두 키트에 드는지 확인합니다. 저장소 전체로 배포하는 테스트 환경이 잡지 못하는 누락을 여기서 잡습니다.

항목마다 사유를 다는 이유는 다음 사람이 그 규칙을 지워도 되는지 판단할 수 있게 하려는 것입니다. 「이 문서는 무해해 보이니 넣자」는 판단은 규칙을 만든 사람이 무엇을 걱정했는지 모르면 틀리기 쉽습니다. 사유는 그 파일이 고객에게 가면 무엇이 달라지는지를 적어 둔 기록입니다.

키트에 든 파일은 고객 전달물로 다시 검사한다

저장소에는 고객 전달물에 사내 좌표(사내 IP 대역, 사내 도메인, 사내 DB 이름)가 들어가는 것을 막는 검사가 따로 있고, 머지 리퀘스트마다 돕니다. 사내 값은 대개 그 순간 편해서 들어옵니다. 사내 GPU 서버 주소를 예제 설정에, 사내 계정을 기본값에 넣는 식입니다. 한 번 청소해도 다음 변경에 다시 들어오므로 변경마다 검사합니다.

이 검사는 평소 운영 스크립트와 문서 디렉터리를 보지 않습니다. 사내에서만 쓰는 파일이 많고, 거기에는 사내 좌표가 있는 것이 정상이기 때문입니다. 그런데 키트에 드는 순간 그 디렉터리의 일부 파일은 고객 전달물이 됩니다. 그래서 두 도구가 서로를 불러 씁니다. 키트 생성 도구는 사내 좌표 검사의 규칙과 허용 목록을 불러 키트의 모든 파일에 겁니다. 사내 좌표 검사는 거꾸로 키트 생성 도구의 파일 선택을 불러, 키트에 드는 파일을 평소 검사 대상에 더합니다.

이 연결 덕분에 고객용 운영 문서에 사내 주소를 적은 변경은 릴리스 때가 아니라 그 머지 리퀘스트에서 걸립니다. 키트에 들 파일 목록은 키트 생성 도구 한 곳에만 있어서 두 검사가 보는 파일이 갈라지지 않습니다.

허용 목록에는 판단의 근거 기록만 올립니다. 어떤 값을 왜 골랐는지 남긴 실측 출처나 회귀 사례가 그렇습니다. 지우면 근거가 사라지므로 파일 단위로 사유를 달아 통과시킵니다. 목록이 길어지는 것 자체가 리뷰할 신호입니다. 새 항목이 올라오면 정말 근거 기록인지, 새로 들어온 사내 값인지를 묻습니다.

DB 초기화 SQL에는 검사가 하나 더 걸립니다. 이 스크립트는 빈 볼륨에서 PostgreSQL이 처음 뜰 때 그대로 실행됩니다. 슈퍼유저나 비밀번호를 박아 둔 역할을 만드는 문장이 있으면 모든 고객 설치에 같은 계정이 생깁니다. 키트 생성 도구는 초기화 스크립트에서 그런 문장을 찾으면 빌드를 멈춥니다. 설치된 시스템에서는 설치 전 점검이 같은 위험을 다시 봅니다. 메타데이터 DB의 관리 역할 말고 로그인할 수 있는 슈퍼유저가 있으면 FAIL로 판정합니다(「설치 전 점검은 호스트를 읽기만 하고, 고칠 명령은 사람에게 넘긴다」).

키트 밖의 고객 전달물도 같은 검사를 거칩니다. 이미지 번들 안의 이미지 이름은 사내 레지스트리 주소 대신 localhost/owlai/<이미지>로 붙이고, 이미지 저장소가 받아 온 레지스트리 주소를 적어 두는 주석은 봉인 단계에서 걷어 냅니다. 공개 피드에 올릴 릴리스 메타데이터는 같은 사내 좌표 검사를 통과해야 게시되고, 여기에는 허용 목록을 적용하지 않습니다. 게시물에는 근거 기록이 있을 자리가 없기 때문입니다.

같은 커밋에서는 같은 바이트가 나온다

키트 생성 도구는 tar의 모든 항목에서 빌드 환경에 따라 달라지는 값을 고정합니다.

값 고정하는 방법
파일 순서 경로 순으로 정렬한다
수정 시각 모든 항목을 커밋 시각으로 적는다. gzip 머리의 시각도 커밋 시각이다
소유자 uid·gid 0, 사용자 이름은 비운다
권한 실행 비트가 있으면 755, 없으면 644

그래서 같은 커밋으로 두 번 만든 키트는 바이트까지 같습니다. 단위 테스트가 이것을 확인합니다. 고객이 받은 키트가 그 릴리스 커밋에서 나왔는지는 같은 커밋으로 다시 만들어 비교하면 알 수 있습니다.

키트 안에는 파일 목록(MANIFEST.tsv — 경로, 권한, 크기, sha256, 원본 커밋), 체크섬 파일(SHA256SUMS), README가 함께 들어갑니다. 고객은 반입한 뒤 sha256sum -c SHA256SUMS 한 줄로 파일이 온전한지 확인합니다. 고객 전달물이라 도구가 만드는 파일은 영어로 씁니다.

설계에서 고른 것

운영 문서는 패턴 대신 이름으로 넣는 쪽을 골랐습니다. 패턴으로 넣으면 고객용 문서가 생겨도 손댈 곳이 없지만, 사내 문서가 분류되기 전에 고객에게 갈 틈이 생깁니다. 이름으로 넣으면 운영 문서를 하나 쓸 때마다 목록 한 줄을 고쳐야 합니다. 그 번거로움이 대가이고, 대신 문서를 쓴 사람이 그 자리에서 고객에게 줄지 정합니다.

사내 좌표 검사는 제품명 대신 좌표만 잡습니다. 도메인·IP·사내 DB 이름은 모양이 정해져 있어 코드가 매번 같은 판정을 냅니다. 제품명과 엔진 이름은 정상 코드에도 흔히 나와서, 그것까지 막으면 정상 코드가 걸려 허용 목록이 불어나고 검사는 사람들이 무시하는 목록이 됩니다. 문서 속 사람 이름이나 문장으로 적힌 내부 사정처럼 모양이 없는 정보는 코드 리뷰가 맡습니다. 그쪽이 새지 않는 것은 리뷰어가 얼마나 꼼꼼히 읽는지에 달려 있습니다.

키트 쪽에 사내 좌표 규칙을 한 벌 더 적는 대신 두 도구가 서로를 불러 쓰게 했습니다. 규칙을 두 곳에 적으면 언젠가 갈라지고, 갈라진 쪽은 아무도 모르게 느슨해집니다. 서로 불러 쓰면 한쪽을 고칠 때 다른 쪽이 함께 바뀝니다. 대가는 두 도구가 서로의 위치를 알아야 한다는 것이고, 한쪽 파일을 옮기면 다른 쪽이 읽지 못해 바로 멈춥니다.

고객 전달물과 사내 산출물을 한 저장소에서 관리하는 설치형 제품이라면 이 구성을 그대로 옮길 수 있습니다. 출발점은 후보를 git ls-files로 좁히는 것과, 아무것도 고르지 못한 규칙에 빌드를 멈추는 것입니다. 두 가지 모두 코드 몇 줄이고, 키트에 무엇이 섞이고 무엇이 빠지는지를 고객 호스트가 아니라 빌드에서 알게 됩니다.