본문으로 건너뛰기

환경과 히스토리

「여러 사람이 함께 쓸 때」로 돌아가기


4. 환경 구분

4.1 recipe의 환경 개념 — 개정 전후

개정 전 recipe에는 환경 개념이 없었습니다. 지금은 ${NAME}--env가 그 자리입니다.

무엇개정 전지금
${NAME} 환경변수 치환연결 문자열에만 적용되었습니다recipe 전체. 값에만 적용되고, 파싱된 문서에 적용되므로 따옴표·역슬래시가 든 값도 그대로 들어갑니다
Path · SheetsId · IncludeWorkbooks치환되지 않았습니다치환됩니다
없는 변수오류이고, 없는 것을 전부 모아 recipe 안의 자리와 함께 보고합니다
연결 문자열타깃이 실행될 때 해석그대로 유지했습니다 — §4.5
산출물의 환경 표기없었습니다--env가 정하고 SummaryRun.Environment에 기록됩니다

TargetSide는 환경 구분이 아닙니다. 클라이언트/서버를 가르는 설정이며 dev/live와 무관합니다. 이름이 가장 유사하므로 오용될 여지가 있어 명시합니다.

4.2 사고가 발생하는 경로

경로차단 여부
라이브 DB에 dev 데이터를 적재ConnectionString${NAME}을 요구하면 차단됩니다 — 변수가 없으면 오류
라이브 클라이언트가 dev 데이터를 로드암호화·MAC 키를 라이브에만 두면 차단됩니다 — §4.3
dev 산출물이 라이브 경로에 쓰임환경이 경로에 들어가면 갈라집니다${TABBIT_ENV}가 출력 폴더를 나누므로 --env dev가 라이브 폴더에 쓸 수 없습니다
dev 시트를 라이브 빌드가 읽음같습니다 — SheetsId나 소스 경로를 ${TABBIT_ENV}로 적으면 한 낱말이 입력과 출력을 함께 정합니다
산출물의 라벨과 실제가 어긋남구조상 불가능합니다 — 라벨과 경로가 같은 낱말에서 나오고, TABBIT_ENV가 다른 값이면 거부합니다 (§4.5)

남는 것은 사람이 --env를 틀리게 적는 경우 하나입니다. 그것은 §4.3의 키가 차단합니다 — --env live로 적어도 라이브 키가 없으면 라이브가 로드할 수 있는 파일이 나오지 않습니다.

4.3 지금 실효가 있는 장치 — 키와 권한의 분리

도구를 수정하지 않고 오늘 적용할 수 있으며, 가장 강한 차단입니다.

라이브에만 키를 발급합니다. EncryptionKeyVariable·MacKeyVariable은 키가 아니라 키가 있는 환경변수의 이름을 받습니다.

{ "Type": "binary", "Path": "./build/data",
"EncryptionKeyVariable": "TABBIT_TCB_KEY",
"MacKeyVariable": "TABBIT_TCB_MAC_KEY" }

이 두 키는 라이브 recipe에만 적고, 그 변수는 CI의 라이브 잡에만 존재합니다. 결과는 다음과 같습니다.

  • 라이브 키를 보유하지 않은 사람은 라이브가 로드할 수 있는 파일을 생성할 수 없습니다. 경로를 잘못 지정하더라도 그 파일은 라이브 클라이언트에서 MAC 검증에 실패합니다.
  • 같은 규칙이 DB에도 적용됩니다. 라이브 비밀번호를 개인 셸에 두지 않으면 라이브 적재는 변수 부재로 중단됩니다.

이것이 현재 유일하게 사람의 주의에 의존하지 않는 장치입니다. 아래 §4.4·§4.5는 이 위에 얹는 것입니다.

dev는 평문으로 둡니다. 처음에는 dev에도 별도 키를 두는 안이었는데, 그 키가 더해 주는 차단이 없습니다 — 차단은 「라이브 키의 부재」에서 나오는 것이지 dev 키에서 나오는 것이 아닙니다. 남는 것은 비용뿐입니다: clone 직후의 첫 변환이 키 준비에 막히고, 산출물 확인에 복호화가 끼어듭니다. 암호화 경로 자체의 동작은 회귀 스위트가 이미 확인합니다.

4.4 원본의 분리와 승격

구글 스프레드시트는 조회 시점의 상태만 반환합니다. 리비전 이력은 문서 쪽에 있고 도구는 그것을 지정해 읽지 않으므로, 구글 시트를 직접 읽는 라이브 빌드는 재현되지 않습니다 — 같은 커밋으로 어제와 오늘 변환한 결과가 다를 수 있고, 그 차이의 출처가 기록에 남지 않습니다.

구성은 다음과 같습니다.

단계원본recipe비고
개발구글 시트를 직접 조회recipe.dev.jsonc기획이 편집 중인 상태를 그대로 확인합니다
승격워크북을 .xlsx로 내려받아 커밋이 커밋이 승인 지점입니다
라이브커밋된 워크북 폴더recipe.live.jsoncXlsx 소스. 태그된 커밋에서만 실행합니다

승격을 커밋으로 두면 3가지가 함께 성립합니다 — 재현성(같은 태그는 같은 바이트), 승인 지점(PR과 리뷰가 적용됩니다), 귀속(누가 무엇을 승격하였는지가 git에 남습니다).

이전 소규모 코퍼스가 이미 이 형태입니다. 원본은 구글 스프레드시트이고 저장소의 xlsx는 내보낸 사본입니다.

이 절차의 비용 2가지를 함께 기재합니다.

  • 저장소 비대화. 워크북은 수십 MB 단위이고 git은 바이너리의 차분을 압축하지 못하므로, 승격마다 그 크기가 히스토리에 누적됩니다. 워크북이 작으면 같은 저장소에 두어도 되고 — 분리는 크기가 강제할 때 하는 것입니다 — 커지면 스냅샷 전용 저장소로 분리합니다. 그 저장소의 과거는 보존 대상이 아닙니다: 계보는 태그와 히스토리 DB가 이미 들고 있으므로, 부담이 되면 저장소를 재시작해도 잃는 것이 없습니다.
  • 승격의 수작업화. 내려받아 커밋하고 태그를 붙이는 일을 사람이 반복하면 언젠가 누락되거나 순서가 어긋납니다. 승격을 CI의 수동 트리거 잡 1개로 둡니다 — 실행 1번이 내려받기·커밋·태그를 순서대로 수행합니다. §6.5의 서비스 계정이 이 잡의 전제입니다.

규칙 하나를 추가합니다 — 시트가 정본인 라인에서 스냅샷은 편집하지 않습니다. 수정은 시트를 고쳐 다시 승격하는 것이지 스냅샷 xlsx를 여는 것이 아닙니다. 직접 고치면 시트와 스냅샷이 어긋나고, 다음 승격이 그 수정을 되돌립니다. 예외는 §3.3의 옛 릴리스 브랜치 하나입니다 — 거기서는 스냅샷이 정본이므로 스냅샷을 고치고, 같은 수정을 시트에 반영합니다.

4.5 도구 쪽 — 됨

recipe 2개로 운영하면 Targets 목록 전체와 시트 필터가 두 파일에 중복되고, 한쪽만 수정된 상태가 발생합니다. recipe 1개와 환경변수 세트 2개가 되도록 두 가지를 넣었습니다.

무엇어떻게
전역 ${NAME} 치환됨. recipe의 모든 문자열 값에 적용됩니다. 없는 변수는 오류이고 전부 모아 각각의 자리와 함께 보고합니다 — 기계를 세팅하는 사람은 변수를 전부 설정해야 하므로 하나씩 알려주면 변수 수만큼 실행하게 됩니다
환경 라벨됨. --env <이름>SummaryRun.Environment에 기록되고, 동시에 ${TABBIT_ENV}가 그 값이 됩니다
recipe 상속(Extends)하지 않습니다. 위 둘로 recipe가 1개가 되므로 상속할 대상이 남지 않습니다

--env가 라벨과 경로를 함께 정하는 것이 이 설계의 요점입니다. 라벨용 플래그와 경로용 변수를 따로 두면 둘이 어긋날 수 있고, 어긋난 결과는 「개발 시트로 만들어 놓고 live라고 적힌 산출물」입니다 — 라벨이 없는 것보다 나쁩니다. TABBIT_ENV가 다른 값으로 이미 설정되어 있으면 거부합니다. 이름은 경로에 들어가므로 영문자·숫자·.·_·-로 제한합니다.

연결 문자열만 예외로 두었습니다. ConnectionStringValidation.Connections의 값은 그 타깃이 실제로 실행될 때 해석되던 동작을 유지합니다 — recipe에 있지만 이번에 돌리지 않는 데이터베이스 타깃 때문에 --validate-only가 멈추면, PR 검사를 돌리는 사람이 라이브 비밀번호를 갖고 있어야 합니다.

혼자 쓰는 구성은 아무 영향도 받지 않습니다. ${NAME}을 쓰지 않으면 치환도 일어나지 않고 설정할 변수도 없습니다.


5. 히스토리 — 단일 서버와 단일 기록자

5.1 결론

히스토리는 공용 서버 1대에 기록하고, 기록하는 주체는 CI 하나여야 합니다. 각자가 자기 DB를 보유하는 구성은 도구의 설계가 이미 거부하고 있습니다.

5.2 근거 — 개인 기록이 성립하지 않는 이유

히스토리는 스냅샷의 사슬이며, 각 스냅샷은 직전 스냅샷과 대조되어 변경이 산출됩니다 (설계). 그 사슬은 단일한 기록자를 전제합니다.

장치무엇을 차단하는가개인 기록에서 발생하는 것
RecordDirty: false커밋되지 않은 워킹카피의 기록개인 실행은 대부분 dirty이므로 기록 자체가 거부됩니다
AllowOutOfOrder: false브랜치 head보다 뒤진 커밋의 기록각자 다른 커밋에서 변환하면 뒤진 쪽의 변경이 되돌림으로 기록됩니다
브랜치별 이름 락같은 브랜치의 두 커밋을 동시에 변환하는 것락이 없으면 이후의 모든 diff가 임의의 한쪽을 기준으로 산출됩니다
followsParent건너뛴 구간의 표시변환하지 않은 커밋의 변경이 다음 스냅샷에 병합되어 그 작성자에게 귀속됩니다

백필은 하지 않습니다. 따라서 「나중에 CI를 붙여 채운다」가 성립하지 않고, 기록되지 않은 구간은 영구히 뭉친 채로 남습니다.

5.3 구성

역할누가설정
기록CI 1개 잡. 워크북이 바뀐 커밋마다 1회history 타깃 + --commit · --branch
조회--serve 1대읽기 전용 계정. 외부 노출 시 TABBIT_SERVE_TOKEN 필수 — 없으면 기동이 거부됩니다
개인 실행기록하지 않습니다개인용 recipe에서 history 타깃을 제외합니다
  • ProjectKey는 환경이 아니라 데이터의 계보입니다. 값을 변경하면 이어지지 않고 새 히스토리가 시작되므로, dev와 live를 다른 ProjectKey로 분리하면 승격 시점에서 계보가 단절됩니다. ProjectKey는 1개로 유지하고 --branch로 구분합니다 — 사슬이 브랜치별로 관리되며 락도 브랜치 단위입니다.
  • OnFailurewarn을 유지합니다. 기록용 DB의 일시적 장애로 데이터 빌드를 중단할 이유가 없습니다.
  • 개인이 조회만 하는 경우 recipe의 연결 문자열이 읽기 전용 계정을 가리키게 합니다. API는 전부 GET이며 쓰기 경로는 변환뿐입니다.

5.4 이 구성이 산출하는 것

히스토리는 공동 작업에서 발생하는 질문을 직접 해소합니다 — 이 값을 누가 언제 바꾸었는가가 셀 단위로 기록되고, 구글 시트라면 그 셀로 가는 URL이 함께 기록됩니다. 그리고 deployment 판정이 이 구간을 내보내려면 데이터 패치와 코드 배포 중 무엇이 필요한지를 산출합니다. 기록자가 분산되면 이 3가지가 모두 성립하지 않습니다.

한계 하나를 같은 자리에 기재합니다. 승격이 여러 편집을 커밋 하나로 묶으므로, 라이브 히스토리의 작성자는 편집자가 아니라 승격을 실행한 주체입니다. 편집 단위의 「누가」는 개발 단계에서 구글 스프레드시트의 리비전 기록이 담당하고, tabbit 히스토리는 태그 사이에 무엇이 바뀌었고 무엇을 배포해야 하는가를 담당합니다. 귀속의 해상도가 더 필요하면 도구를 고치는 것이 아니라 승격 주기를 좁힙니다 — xlsx가 바이너리인 이상 커밋 단위가 귀속의 상한이라는 사실은 도구 쪽에 이미 기재되어 있습니다.