배포 증거 체인(delivery evidence chain) 구축 과정
요약
AI 에이전트의 빠른 개발 속도로 인해 발생하는 배포 신뢰성 문제를 해결하기 위한 '배포 증거 체인' 구축 과정을 다룹니다. Appaloft는 동결, 프리뷰, 프로모션, 검증의 4단계 과정을 통해 배포되는 콘텐츠의 무결성과 승인 주체를 명확히 증명하는 오픈소스 플랫폼을 제안합니다.
핵심 포인트
- AI 에이전트의 높은 처리량으로 인한 배포 관리의 어려움 해결
- 콘텐츠 주소 지정 방식(Content-addressed)을 통한 아티팩트 동결
- 실패 시 차단(Fail-closed) 원칙을 통한 배포 신뢰성 확보
- 정형화된 JSON 매니페스트와 SHA-256을 이용한 무결성 검증
AI 에이전트는 10분 만에 앱을 작성할 수 있습니다. 실제로 배포된 것이 무엇인지 증명하는 것은 더 어렵고 오래된 문제입니다. 저희는 Appaloft의 증거 체인을 어떻게 구축했는지, 그리고 어디에서 의도적으로 멈췄는지 설명합니다.
코딩 에이전트를 사용하는 모든 사람이 같은 벽에 부딪힙니다. 에이전트는 사용자 컴퓨터에서는 작동하는 무언가를 만들어냅니다. 그런 다음 누군가(대개는 사용자 본인) 파일들을 서버로 복사하고, 프로세스를 재시작하며, 로그 라인을 확인한 후 성공을 선언합니다. 2주 후에 프로덕션 환경에서 새벽 2시에 잘못된 빌드가 서비스되고 있을 때, 아무도 소프트웨어 배포 과정에서 가장 간단한 질문에 답할 수 없습니다: 정확히 무엇이 실행되고 있으며, 누가 승인했는가?
이 문제는 AI보다 오래되었습니다. 하지만 에이전트 때문에 두 가지 이유로 인해 문제가 심화됩니다:
- 처리량(Throughput). 인간은 한 번에 배포 가능한 변경 사항을 하나씩 작성하며, 보통 그 내용물을 기억합니다. 반면 에이전트는 오후 동안 20개의 후보를 만들어낼 수 있습니다. 사용자의 배포 메모리는 확장되지 않습니다.
- 신뢰(Trust). 만약 에이전트가 배포 과정을 직접 주도하도록 — SSH 키, Docker 소켓, 클라우드 자격 증명 등을 건네준다면 — 당신은 프로덕션 환경에 확신에 찬 어조를 가진 확률적 프로세스(stochastic process)의 접근 권한을 준 것입니다.
Appaloft는 하나의 아이디어를 중심으로 구축된 오픈소스 (Apache-2.0) 배포 플랫폼입니다: 프로모션은 명시적이어야 하고, 배포는 증거를 남겨야 합니다.
- 어떤 정확한 바이트(bytes)가 후보로서 동결(frozen)되었는가?
- 어떤 종류의 주체(principal)가, 언제, 그것들을 프로모션(promoting)하도록 승인했는가?
- 배포 후, 관찰된 실제 상태(실행 중인 컨테이너, 설정, 트래픽을 처리하는 경로)가 승인된 내용과 일치하는가?
해당 체인의 연결 고리 중 어느 하나라도 끊어지면, 시스템은 이유 코드(reason code)와 함께 이를 명시적으로 알립니다. 증거가 없거나 사용할 수 없는 경우에는 절대로 조용히 검사를 통과할 수 없습니다. 그러한 '실패 시 차단(fail-closed)' 속성이 이 시스템의 핵심입니다.
이 체인은 네 개의 연결 고리로 구성됩니다: 동결(freeze) → 프리뷰(preview) → 프로모션(promote) → 검증(verify).
연결 고리 1: 워크스페이스를 콘텐츠 주소 지정 아티팩트(content-addressed artifact)로 동결하기
코딩 에이전트(coding agent)는 샌드박스(sandbox)에서 작동합니다. 이는 기본적으로 네트워크를 차단하는(deny-by-default) 정책이 적용된 gVisor 격리 컨테이너입니다. 에이전트의 작업 결과가 양호해 보이면, 워크스페이스를 _소스 아티팩트(source artifact)_로 동결합니다.
가장 명확한 구현 방식은 "디렉토리를 tar로 묶고, 그 tar볼의 해시를 생성하는 것"입니다. 하지만 우리는 그렇게 하지 않았습니다. 아티팩트 다이제스트(artifact digest)는 정형화된 JSON 매니페스트(canonical JSON manifest)에 대한 SHA-256입니다. 이는 {path, 파일 바이트의 sha256, sizeBytes} 항목들을 정렬하고 중복을 제거한 목록입니다. 아카이브 자체(압축되지 않은 일반 ZIP)는 단지 전송 수단일 뿐입니다. 정체성(Identity)은 매니페스트에 존재합니다.
왜 그럴까요? 해시들의 매니페스트(manifest-of-hashes)는 일반적인 tar볼 해시가 가질 수 없는 다음과 같은 속성을 제공하기 때문입니다:
- 의미 있는 중복 제거(Dedupe). 에이전트의 "새로운" 출력이 이전의 것과 바이트 단위로 동일할 경우,
findArtifactByDigest를 통해 기존 아티팩트를 조회하고 반환할 수 있습니다. 이는 두 번의 동결 작업이 경합(race)하는 경우에도 마찬가지입니다(경합에서 패배한 쪽은 자신의 복사본을 삭제하며, 호출자는 어떤 경우에도 승자의 디스크립터(descriptor)를 받게 됩니다). - 변조 탐지(Mutation detection). 캡처 과정에서 각 파일을 읽은 후, 파일 크기를 다시 확인합니다. 만약 동결 도중에 워크스페이스가 변경되었다면, 캡처는 실패하며 _"캡처 중 소스 아티팩트가 변경되었습니다(Source Artifact changed during capture)."_라는 메시지를 띄웁니다.
- 경로 안전성(Path safety). 모든 항목은 소스 루트(source root)를 기준으로 검증됩니다. 심볼릭 링크(symlinks)와
..를 이용한 경로 이탈(escapes)은 즉시 거부됩니다.
언급할 가치가 있는 두 가지 설계 결정이 더 있습니다:
동결(Freezing)에는 정지 상태(quiescence)가 필요합니다. 해당 샌드박스(sandbox) 내에서 실행 중인 에이전트(agent)가 있다면 캡처(Capture)는 거부됩니다. 편집 도중에 워크스페이스(workspace)를 동결할 수는 없습니다. 이는 당연하게 들릴 수 있지만, 이를 단순히 문서화하는 대신 상태 머신(state machine)에서 강제함으로써 실제로 보장됩니다.
비밀 정보 스캐닝(Secret scanning)은 동결 시점에 수행되며, 실패 시 차단(fail-closed) 방식으로 동작합니다. 어떤 것이 불변(immutable) 상태가 되기 전에, 파일 이름 블랙리스트(.env* (문서화된 템플릿 제외), id_rsa*, credentials.json 등)와 콘텐츠 패턴(PEM 개인 키, AWS/GitHub/OpenAI 토큰 형태, password = … 스타일의 할당문)을 스캔합니다. 비밀 정보로 의심되는 것이 발견되면 동결은 실패합니다. 비밀 정보는 불변 후보(immutable candidate)에 포함될 수 없습니다. 왜냐하면 다운스트림(downstream)의 모든 과정(미리보기(preview), 승격(promotion), 배포(deployment))이 해당 아티팩트(artifact)를 신원 기반의 신뢰할 수 있는 것으로 취급하기 때문입니다.
링크 2: 동결한 내용을 정확하게 미리보기(Preview)
동결된 아티팩트는 _후보 미리보기(candidate preview)_로 전달됩니다. 이는 해당 빌드를 그대로 서빙하는 임시적이고 만료 가능한 URL입니다. 모든 미리보기 응답에는 x-appaloft-artifact-digest 헤더가 포함되며, 컨트롤 플레인(control plane)은 게이트웨이(gateway)가 예상하는 것과 동일한 previewId 및 artifactDigest를 에코(echo)하는지 확인합니다. 자신이 어떤 아티팩트를 서빙하고 있는지 증명할 수 없는 미리보기는 검증되지 않은 증거로 취급되어 승격(promotion)이 차단됩니다.
미리보기 토큰은 HMAC 해싱되어 timingSafeEqual로 비교되며, 취소 가능하고 GET/HEAD 요청만 허용됩니다. 의도적으로 TCB(신뢰 컴퓨팅 기반, Trusted Computing Base)를 작게 유지했습니다. 미리보기 게이트웨이는 프레임워크가 아니라 보안 관련 코드가 수백 줄에 불과한 독립형 서비스입니다.
링크 3: UI가 아닌 승인 게이트를 갖춘 상태 머신으로서의 승격(Promotion)
이것이 대부분의 도구가 틀리는 지점입니다. 대부분의 도구는 승인을 대시보드의 버튼으로 취급하기 때문입니다. 우리의 승인 게이트는 **도메인 계층(domain layer)에서의 주체 확인(principal check)**입니다.
승격 애그리거트(promotion aggregate)는 명시적인 상태 머신을 가집니다:
planned → accepted → creating-resource → deploying → verifying → completed
│ │
├── expired (30분 TTL) failed ──→ retry ──→ deploying
...
이를 명시적으로 만듦으로써 다음과 같은 몇 가지 속성이 도출됩니다:
플랜은 30분 후에 만료됩니다. 플랜은 다이제스트 (digest)와 검증된 프리뷰 (verified preview)를 참조합니다. 조치를 취하지 않으면 플랜은 만료되며, sandbox_promotion_plan_expired 오류가 발생합니다. 승인 결정은 어제의 후보가 아닌, 최신의 증거 (evidence)를 바탕으로 내려져야 합니다.
승인(Accept)에는 정확한 다이제스트가 필요합니다. acceptPromotion은 expectedArtifactDigest를 인자로 받습니다. 검토 시점 이후로 후보가 변경(drift)되었다면, sandbox_promotion_artifact_mismatch 오류가 발생합니다. 사용자는 "최신 것"이 아니라, 바로 "이 바이트 (these bytes)"를 승인하는 것입니다.
게이트(Gate)는 머신 주체(machine principals)를 거부합니다. requireExternalApprovalActor는 deploy-token 식별자에 대해 sandbox_agent_external_approval_required를 반환하며 거부합니다. 에이전트나 MCP 세션이 보유하는 것과 같은 샌드박스 범위(sandbox-scoped)의 식별자는 구조적으로 승인(promotion)을 수락할 수 없습니다. 이는 에이전트가 대화로 통과할 수 있는 권한 토글이 아닙니다. 작업 카탈로그 (operation catalog) 자체가 해당 주체 유형에 대해 그러한 기능을 제공하지 않기 때문입니다. 에이전트는 게이트 직전까지의 모든 것을 준비할 수 있지만, 최종 통과는 인간(또는 인간이 위임한 자격 증명으로 동작하는 외부 시스템)이 수행해야 합니다.
승인(accept)과 재시도(retry)에는 멱등성(Idempotency)이 내장되어 있습니다. 동일한 멱등성 키(idempotency key)로 다시 승인하는 것은 작업을 다시 큐에 넣는 무작정 동작(no-op)이므로, 불안정한 클라이언트가 중복 승인을 일으킬 수 없습니다. 재시도는 의도적으로 좁은 범위로 설계되었습니다. 동일한 리소스와 아티팩트 (artifact)를 재사용하고, 배포 ID (deployment ID)만 삭제한 뒤 새로운 시도를 수행합니다. recordResource는 이미 리소스 ID가 있는 승인 작업에 다른 리소스 ID를 연결하는 것을 거부합니다. 즉, 관리 연속성 (chain of custody)은 진행 도중에 조용히 재구성될 수 없습니다.
모든 중간 상태는 영속화됩니다. 비동기 워커 (reconcilePromotion, 중복 제거 키와 제한된 시도 횟수를 가진 데이터베이스 기반의 내구적 작업 큐(durable work queue)에 의해 구동됨)는 각 단계(리소스 생성, 배포 생성, 증거 읽기) 이후에 상태를 영속화합니다. 어디서든 충돌(crash)이 발생하더라도, 마지막으로 확인된 정직한 상태에서 재개할 수 있습니다.
링크 4: 증거가 종료 코드(exit codes)보다 강력하다
배포 도구에 관한 불편한 진실이 하나 있습니다. exit 0은 배포 _스크립트(script)_가 종료되었음을 증명할 뿐입니다. 녹색 상태의 헬스 체크(health check)는 어떤 컨테이너가 건강하다는 것을 증명합니다. 하지만 둘 다 트래픽을 처리하고 있는 대상이 당신이 승인한 바로 그 대상이라는 점은 증명하지 못합니다. 따라서 마지막 연결 고리는 deployments.proof로 노출되는 읽기 기반 검증 엔진(read-back verification engine)입니다.
증거(Proof)는 계획된(planned) 상태(불변의 승인 시점 스냅샷: 아티팩트 식별자, 구성 지문(configuration fingerprint), 예상 효과)를 대상으로부터 수집된 관찰된(observed) 증거와 비교함으로써 **읽기 시점에 계산(computed on read)**됩니다:
- 워크로드 식별자 (Workload identity). 대상에 SSH로 접속하여 이 배포의 ID가 라벨링된 컨테이너를
docker inspect한 다음, 이미지의sha256:다이제스트(digest)와 컨테이너의 생성 정보(generation)를 계획(plan)과 비교합니다. - 구성 지문 (Configuration fingerprint). 라벨 및 환경 변수 키 세트(key-set) 비교 — 드리프트(drift)된 환경 변수는 단순한 각주가 아니라 불일치 사항입니다.
- 상태 (Health). 컨테이너 실행 상태 및 Docker 헬스 상태(health status).
- 경로 식별자 (Route identity). 이 부분은 미묘하며 깊이 살펴볼 가치가 있습니다. 올바른 라벨을 가진 건강한 컨테이너가 있다고 해서 리버스 프록시(reverse proxy)가 실제로 서비스하고 있는 배포가 무엇인지에 대해서는 아무것도 증명하지 못합니다. 따라서 당사의 관리형 에지(managed edge)는 응답에 배포 식별 헤더(deployment-identity header)를 찍고, 검증 과정에서 퍼블릭 URL(raw TLS 클라이언트, 제한된 재시도, 리다이렉트 추적)을 가져와 찍힌 식별자가 계획된 배포 ID와 일치하는지 확인합니다. _"관리형 퍼블릭 경로가 Y 대신 배포 X를 서비스함"_은 일급 실패(first-class failure)로 간주됩니다.
판정 어휘(verdict vocabulary)는 의도적으로 정직하게 구성되어 있습니다: verified | partially-verified | unverified | stale | failed. 사용 불가능한 증거는 명시적이며, 절대로 필수 게이트(required gate)를 충족할 수 없습니다. 상태(health) 성공이나 액세스(access) 성공만으로는 결코 verified를 생성할 수 없습니다. 불일치(mismatch)가 발생하면 artifact_identity_mismatch, configuration_fingerprint_mismatch, access_route_workload_mismatch와 같이 기계가 읽을 수 있는 이유 코드(reason codes)가 함께 제공되며, 각 코드는 권장되는 복구 작업(remediation operations)을 포함합니다. 핵심은 판정 그 자체가 아니라, 속임수가 발생할 수 있는 모든 경로가 열거되어 있기 때문에 다운스트림 시스템(또는 지친 사람)이 verified를 신뢰할 수 있다는 점입니다.
오직 verified 증거만이 프로모션(promotion)을 completed 상태로 이동시킵니다. 프로모션 기술자(descriptor)는 말 그대로 해당 상태로부터 proofVerdict를 도출합니다. 검증되지 않은 배포(unverified deployment)가 성공적인 프로모션으로 읽히는 코드 경로란 존재하지 않습니다.
하부 원장(ledger): 감사 이벤트(audit events) 및 운영자 작업
체인 하부에는 일반 감사 싱크(audit sink)가 자리 잡고 있습니다. 감사 대상 도메인(프로젝트, 배포, 샌드박스, 자격 증명 등) 내의 모든 변경 작업(mutating operation)은 작업 키(예: sandboxes.promotions.accept), 영향을 받은 집합체(aggregate), 그리고 비식별화된 페이로드(redacted payload, 영구 저장 전 비밀값과 유사한 키 패턴에 매칭되는 모든 항목은 제거됨)와 함께 감사 이벤트(audit event)로 기록됩니다. 보존(retention), 정리(pruning), 자체 콘텐츠 다이제스트(content digests)를 포함한 불변 아카이브(immutable archives), 그리고 법적 보존(legal holds)은 별도의, 지루하지만 의도적인 하위 시스템들입니다(저장소의 내용을 함께 읽고 계신다면 ADR 048/057/058을 참조하세요).
appaloft audit-event list와 appaloft work list는 운영자에게 제공되는 읽기 경로(read paths)입니다. 새벽 2시에 무언가 잘못된 것처럼 보일 때, "무엇이, 어떤 순서로, 무엇에 의해 트리거되었는가"라는 질문은 고고학 프로젝트가 아닌 쿼리(query)의 영역이 됩니다.
경계가 설정되는 지점
솔직하게 말씀드리자면, 저희도 댓글을 읽기 때문에 추가하자면:
- Deploy & Verify는 안정적입니다. 폴더(Folders), Git 저장소(Git repos), zip 파일, 이미지(images), Compose 번들(Compose bundles), 정적 아티팩트(static artifacts), 상태 확인(health), 로그(logs), 롤백(rollback), 증거 재확인(proof readback) — 이 기능들은 현재 셀프 호스팅(self-hosted) 또는 당사의 클라우드에서 프로덕션 환경에 사용되고 있습니다.
- 에이전트 샌드박스(Agent sandboxes), 소스 아티팩트(source artifacts), 그리고 프로모션(promotion)은 프라이빗 프리뷰(private preview) 단계입니다. 도메인 모델(domain model), 상태 머신(state machine), 증거 엔진(proof engine), 그리고 운영 카탈로그(operation catalog)는 공개 저장소(public repo)에 있습니다. 관리형 캡처/프리뷰/전달 어댑터(managed capture/preview/delivery adapters)는 현재 당사의 클라우드 배포판으로 제공됩니다. README에는 역량 성숙도 표(capability maturity table)가 포함되어 있으며, 저희는 여러분이 이 기준을 엄격히 적용해 주시기를 바랍니다.
- 저렴한 VPS가 자동으로 샌드박스 기능을 갖추는 것은 아닙니다. gVisor 격리(isolation)를 위해서는 Docker에
runsc가 등록되어 있어야 합니다. 당사의 프로바이더는 이를 조사하며, 누락된 경우 보안을 위해 실행을 차단(fails closed)합니다. - 증거 체인(evidence chain)은 정답 여부를 논하는 논거가 아닙니다. 이것이 에이전트의 코드가 훌륭하다는 것을 알려주지는 않습니다. 대신 여러분이 승인한 코드가 실제로 실행되고 있는 코드임을 알려주며, 이를 지속적으로 증명합니다.
왜 이런 방식으로 구축하는가?
저희는 1년 더 일찍 "원클릭 AI 배포(one-click AI deploy)"를 출시할 수도 있었습니다. 시장에는 그런 제품들이 넘쳐나며, 그들은 모두 동일한 형태를 띠고 있습니다: 너무 많은 권한(credentials)을 가진 에이전트, 과도한 신뢰를 바탕으로 하는 배포 스크립트(deploy script), 그리고 증거는 거의 없는 성공 메시지 말입니다.
증거 체인에 담긴 베팅은 에이전트 시대에는 더 많은 편의성이 아니라 더 적은 신뢰가 필요하다는 것입니다. 에이전트는 통제된 운영(governed operations)을 통해 작동해야 합니다. 즉, 인간이 CLI, MCP, SDK, OpenAPI 또는 GitHub Action을 통해 사용하는 것과 동일한 운영 카탈로그(operation catalog)를 사용해야 하며, 주체 유형(principal type)에 의해 역량 경계(capability boundaries)가 강제되고, 콘텐츠 다이제스트(content digest)에 의해 아티팩트 정체성(artifact identity)이 강제되며, 성공은 "프로세스가 깔끔하게 종료됨"이 아니라 "관찰된 현실이 승인 사항과 일치함"으로 정의되어야 합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기