docs(S15P11A705-96): define dev deployment approval contract - #32
docs(S15P11A705-96): define dev deployment approval contract#32tpals0409 wants to merge 1 commit into
Conversation
colosair
left a comment
There was a problem hiding this comment.
AI owner 이정헌입니다. 소스 실측 기준으로 §4 응답 표를 채웠습니다.
기준 커밋은 971e515이고 문서의 감사 기준 85f02f7과 다르지만, 아래 인용한 파일·라인은 두 커밋에서 동일합니다. 시크릿 값은 이름·형식만 적었습니다.
먼저 문서 정정 1건과 배포를 막을 수 있는 결함 3건을 앞에 둡니다.
정정 — §2.4의 preset version 서술
"preset version은 YAML 항목의
version또는 소스 기본 처리로 결정된다"
data/keyword_preset.yaml에 version 필드가 존재하지 않습니다. 파일 단위도 항목 단위도 없고, 헤더 주석이 "embedding·embedding_profile·version은 여기서 다루지 않습니다"라고 명시합니다. loader는 int(preset.get("version", 1))이므로 모든 행의 version이 항상 1입니다. 캐시도 max(p.version, default=1)이라 ai.context_keyword.preset_version에 저장되는 값은 실질 상수입니다.
"YAML의 version을 쓸 수도 있다"로 읽히면 향후 그 필드를 신뢰하게 되니, **"version은 현재 상수 1이며 개정 추적 기능이 없다"**로 고쳐 주시면 좋겠습니다. §2.4의 gate false 판정 자체는 옳습니다.
배포 전에 반드시 짚어야 할 결함 3건
① GMS_BASE_URL 형식 오류 시 비대칭 장애 — 형식 검증 코드가 없습니다
한 변수를 두 클라이언트가 다르게 소비합니다.
- 임베딩:
{GMS_BASE_URL}/embeddings(app/client/embedding_client.py:53) - 판정:
GMS_BASE_URL.split("/gmsapi/")[0] + "/gmsapi"로 파생한 root + Gemini 경로 (app/client/llm_client.py:74,78-81)
/gmsapi/ 세그먼트가 빠지면 임베딩은 정상 동작하고 judge만 조용히 실패합니다. 형식을 검증하는 코드가 없고, 기동 시 GMS로 요청을 보내지 않으며(main.py:38-49 — 클라이언트 객체 생성만), /health는 상수 응답이라 틀린 endpoint·key로도 서버는 정상 기동하고 첫 실사용 요청에서야 실패합니다.
→ 배포 직후 임베딩·judge 양쪽을 각각 1회씩 실호출하는 스모크 절차를 활성화 조건에 넣어 주세요. 형식 검증은 AI 파트가 후속 티켓으로 추가하겠습니다.
② /health를 readiness에 쓰면 죽은 인스턴스에 트래픽이 들어갑니다
핸들러가 return {"status": "ok"} 한 줄입니다(main.py:105-107). DB 세션 획득도 캐시 조회도 하지 않아 기동 후 DB가 끊기거나 커넥션 풀이 고갈돼도 계속 200입니다. lifespan이 보증하는 건 기동 시점 1회뿐입니다.
→ /health는 liveness 전용으로만 쓰고, readiness는 /ready 신설 전까지 연결하지 말아 주세요. 신설은 AI 파트 작업이며 app.state에 db·preset_cache가 이미 있어 DB ping + preset 건수 + 현재 profile 노출로 구현 가능합니다. 원하시는 경로명과 응답 스키마를 지정해 주시면 그 계약대로 만들겠습니다.
③ 배포 순서가 매니페스트 밖에서는 강제되지 않습니다
① back Flyway 마이그레이션 (V1 / V100 / V101 — ai.* 테이블, back 소관)
② python -m app.bootstrap.load_presets ← 1회, 서버와 동일한 env 전체 필요
③ ai 앱 기동
②를 건너뛰면 ③이 preset 0건 RuntimeError로 크래시 루프합니다(main.py:51-60). 이건 사후 감지이지 순서 보장이 아닙니다.
→ init container 방식을 권장합니다. 같은 이미지 + command 오버라이드 + 같은 env면 순서가 매니페스트 수준에서 서고, 실패 시 Pod이 Ready로 넘어가지 않습니다. 이미지가 COPY data ./data를 하고 pyyaml이 requirements.lock:55에 있어 런타임 이미지로 실행 가능합니다.
§4 Owner 응답 표
| 승인 항목 | Owner 응답 | 비밀값 없는 결정·근거 | 요청 gate |
|---|---|---|---|
dev-only, internal ClusterIP, no Ingress |
승인 | 노출 라우트는 /health, POST /internal/v1/search, POST /internal/v1/context/process 3개뿐. 뒤 둘은 X-Internal-Secret 필수(app/core/security.py:12,23-30), /health만 무인증이라 프로브가 헤더 없이 호출 가능. 컨테이너 포트 8000 고정(PORT env 미지원, CMD 하드코딩) |
true |
별도 pinlog_dev DB와 Secret-ref handoff |
승인 | ai는 마이그레이션을 실행하지 않습니다. 커넥션마다 SET search_path = ai, public을 걸고 core.* 접근이 없습니다(app/core/db.py). 롤 권한을 pinlog_dev의 ai 스키마로 한정해 주시면 됩니다. 단 ai.* 테이블은 back의 Flyway 소관이라 ①이 선행돼야 합니다 |
true (①선행 조건부) |
| source-derived env key names | 승인 | 표의 12개가 완전합니다. app/ 전체에서 os.environ/getenv를 직접 읽는 코드가 0건이고 진입점은 app/core/config.py:Settings 한 곳뿐입니다. rename·prefix 차이 없음(.env.example:10-11, README.md:62, tests/conftest.py:37-38 교차 확인). Dockerfile에 ENV 지시어 0건이라 12개 전부 런타임 주입이며 시크릿이 이미지에 구워질 위험은 없습니다. tools/의 OPENAI_* 폴백은 .dockerignore로 이미지에서 제외돼 배포와 무관합니다 |
true |
| source-default 4개 설정의 ConfigMap/default 배치 | 수정 필요 | 4개를 둘로 나눠 주세요. 명시 주입 필요 2개 — PROCESSING_EXPIRY_SEC는 Spring 재스캔 만료와 반드시 동일해야 하므로(코드 주석·.env.example이 "임의값 금지" 명시) back과 같은 원본에서 주입해야 하고, PINLOG_JUDGE_MODEL은 유일하게 기본값이 있어 누락 시 조용히 구 모델로 되돌아갑니다(embedding 4종의 "기본값 없음 → 기동 실패" 원칙과 비대칭). source default 수용 2개 — KEYWORD_CANDIDATE_TOP_K, SIMILARITY_FLOOR는 dev에서 기본값으로 두셔도 됩니다 |
2개 true, 2개 ConfigMap 확정 후 |
| GMS endpoint/API key handoff와 client 호환성 | 수정 필요 | 위 ① 참조. 인증 키는 하나이고 헤더만 다릅니다 — 임베딩 Authorization: Bearer …(embedding_client.py:54), 판정 x-goog-api-key(llm_client.py:99). 임베딩용/판정용 키를 분리 주입하는 경로가 코드에 없습니다(main.py:40-48이 동일 settings를 두 클라이언트에 주입). egress는 GMS 게이트웨이 호스트 1개만 허용하면 됩니다. 프록시 미지원(HTTP_PROXY 처리 코드 없음, httpx 기본 동작 의존) |
스모크 증거 확보 후 |
| embedding model/dimension/distance/profile | 승인 | text-embedding-3-small / 1536 / cosine / profile openai-text-embedding-3-small-1536-cosine-v1. 시크릿이 아니므로 ConfigMap 가능합니다. 기동 시 Settings._profile_consistency가 세 토큰의 profile 문자열 포함 여부를 검사해 불일치 시 기동 실패시킵니다(config.py:45-62) — 다만 단순 substring 검사라 순서·구분자·벤더 접두어는 검증하지 않습니다 |
true |
| judge model 및 compatibility ownership | 승인 | gemini-2.5-flash. judge model은 embedding profile과 별개 축이며 ai.context_keyword_analysis.model_profile에 기록됩니다(keyword_service.py:170-175). 따라서 judge 모델 교체는 재임베딩을 유발하지 않습니다(docs/spec/model-profile.md §4). 호환성 승인 주체는 AI owner입니다 |
true |
| preset bootstrap 실행·retry·provenance 증거 | 수정 필요 | command는 python -m app.bootstrap.load_presets가 맞습니다. 결정 요청 사항 — 실행 형태는 init container 권장(위 ③), retry는 backoffLimit ≥ 2 권장(로더에 retry/backoff 코드가 전혀 없어 재시도 책임이 오케스트레이터에 있습니다), 동시 실행 방어가 없으므로(advisory lock 없음) 단일 실행을 보장해 주세요. 중복 기동 시 데이터 손상은 없으나 임베딩 비용이 2배입니다. 성공 판정은 종료 코드 0 + stdout OK: 27 presets upserted. 부트스트랩 Job도 INTERNAL_SHARED_SECRET을 요구합니다(공용 Settings 강제) — dev에서는 감수하고 전용 설정 분리는 후속으로 남깁니다 |
실행 형태 확정 후 |
| preset exact-set/version provenance gap | 보류 (dev 수용) | gate false 판정에 동의합니다. 위 정정대로 version은 상수 1이고, YAML에서 항목을 지워도 DB 행이 is_active=true로 남으며, is_active가 코드에 True 하드코딩이라 수동 비활성화도 재실행하면 되살아납니다. 추가 위험 — _UPSERT의 conflict target이 id인데 DDL에 code VARCHAR(50) NOT NULL UNIQUE가 있어, YAML에서 id를 바꾸고 code를 유지하면 트랜잭션 전체가 실패합니다. dev는 27건 고정으로 진행하고 provenance 설계는 AI 파트 후속 티켓(back 스키마 협의 필요)으로 분리하겠습니다 |
false 유지 동의 |
| startup/readiness/liveness probe 계약 | 수정 필요 | 위 ② 참조. startup은 GMS 호출 0건이고 preset 27행×1536 float32 ≈ 166KB를 SELECT 1회로 읽을 뿐이라 지배 요인은 Python import와 풀 생성입니다. 콜드 스타트 2~5초로 추정하나 레포에 실측치가 없습니다 — initialDelaySeconds/failureThreshold 확정 전 dev 컨테이너에서 1회 측정이 필요하며 AI 파트가 측정해 회신할 수 있습니다(요청 주세요). Dockerfile에 HEALTHCHECK 없음 |
/ready 신설 후 |
/metrics 부재 수용 또는 앱 변경 요청 |
보류 (dev 수용) | dev는 로그 기반으로 가되 한계를 명시적으로 인지하고 진행해 주세요. embedding_client.py·llm_client.py에 로거 자체가 없어 GMS 성공률·지연·타임아웃 발생률을 어떤 경로로도 알 수 없고, stale PROCESSING 재선점이 무로그라 "신규 처리"와 "stale 회수"를 구분할 수 없습니다. app/ 전체에 log.error 호출이 0건이라 "ERROR 발생 시 알림" 룰을 걸면 아무것도 잡히지 않습니다 — dev 알림은 WARNING 기준으로 잡아 주세요. 계측 도입은 인프라 결정 후 AI가 구현하겠습니다 |
false 유지 동의 |
| 외부 API retry/error classification gap | dev 수용 · 수정 선행 아님 | S15P11A705-121 추적에 동의하며 수정을 배포 선행 조건으로 두지 않겠습니다. dev는 부하가 낮아 429 발생 가능성이 낮고, 일시 오류는 상태를 PROCESSING으로 둔 채 반환해 Spring 재스캔(만료 10분)이 회수하는 설계라 작업이 영구 유실되지 않습니다. 다만 관측 한계(위 항목) 때문에 발생 시 감지가 늦으니, dev 운영 중 judge 실패가 반복되면 즉시 알려 주세요 |
true (dev 한정) |
| resource/replica/rollout/rollback 운영값 | 수정 필요 — 값 제시 | replica 1을 요청합니다. uvicorn 단일 워커(--workers 0건)이고 PresetCache가 프로세스당 1개라 수평 확장 시 인스턴스마다 startup에서 각자 적재합니다. graceful shutdown 30초 + terminationGracePeriodSeconds 40초를 제안합니다 — POST /internal/v1/context/process가 202를 즉시 반환하고 실제 처리를 같은 프로세스의 BackgroundTasks로 수행하므로(app/api/internal/v1/context.py:24-32), 롤아웃 시 작업이 끊기면 해당 Context가 PROCESSING으로 남아 10분 만료 후에야 회수됩니다. CPU/메모리 실측치는 레포에 없어 제시하지 못합니다 — dev 관측 후 조정 요청드리겠습니다. 롤백 대상 지정은 태그가 commit sha 전용(latest·semver 없음)이라 인프라 결정 사항입니다 |
값 확정 후 |
승인 6 · 수정 필요 5 · dev 수용 보류 2
추가 정보 2건
PresetCache는 런타임 갱신 경로가 없습니다. startup 1회 적재 후 재적재 엔드포인트·시그널이 없어(app/cache/preset_cache.py, 재적재 호출부 0건) preset 변경·profile 전환 반영은 프로세스 재기동이 유일한 경로입니다. 배포 계약에 명시해 주세요.
.env 파일 방식 주입 시 UTF-8 BOM 함정이 있습니다. 첫 줄 key가 인식되지 않아 "missing"으로 기동 실패하며 재발 이력이 있습니다(docs/troubleshooting/2026-07-23-fastapi-local-verification.md T16). k8s Secret env 주입이면 해당 없습니다.
AI 파트가 착수할 후속 작업
우선순위를 지정해 주시면 그 순서로 진행하겠습니다.
| # | 내용 |
|---|---|
| A | /ready 신설 — DB ping + preset 건수 + 현재 profile 노출 (우선순위 1 권장) |
| B | GMS_BASE_URL의 /gmsapi/ 포함 형식 검증 추가 |
| C | /context/process 요청에 embeddingProfile 필드 추가 + 대조 (back 협의) |
| D | 영구 오류 로그 레벨 WARNING → ERROR 정정 (스펙 정합) |
| E | preset provenance 설계 — 적재 시각·source SHA (back 스키마 협의) |
A와 B는 위 ①②를 직접 메우므로 dev 배포 전에 넣는 것이 안전합니다. 일정에 넣을지 판단 부탁드립니다.
|
이정헌님, 소스 실측 기반 상세 답변 감사합니다. 정정과 결함 3건을 포함해 확인했습니다. 특히 preset version이 현재 상수 Infra 쪽에서는 key/token 값과 앱 호환성 설계를 AI owner 소유로 두고, 값을 열람하지 않은 채 암호화 Secret과 컨테이너만 배포하겠습니다. 아래 항목을 다음 handoff로 요청드립니다. AI 파트 요청사항1. GitHub Secret → 암호화 Secret handoffAI 소유 값은
이 구조/이름을 수정하고 싶으면 AI owner 기준안을 비밀값 없이 회신해 주세요. Infra는 확정된 opaque Secret interface만 소비하겠습니다. 2.
|
|
확인 감사합니다. 4번 지적이 맞습니다 — init container 제안을 철회합니다. Pod 재시작과 rolling overlap마다 반복 실행된다는 점을 놓쳤습니다. 그러면 매번 27건을 GMS에 다시 임베딩하고(임베딩 캐시 없음), 롤링 중 old/new Pod이 겹치면 제가 요청한 "단일 실행 보장"과도 정면으로 충돌합니다. versioned Argo PreSync Job이 옳습니다. 4. PreSync Job 적합성 — 가능합니다제시하신 조건 전부 현재 loader로 충족됩니다.
두 가지만 유의해 주세요. Job에도 서버와 동일한 env 전체가 필요합니다. config만 바뀐 sync에서도 Job이 돌면 27건이 재임베딩됩니다. 멱등하므로 안전하지만 GMS 호출 비용이 발생합니다. revision 단위로 한 번이면 dev에서는 감수할 만하다고 봅니다 — 실행 빈도가 예상보다 높아지면 알려 주시면 loader 쪽에 스킵 조건을 넣겠습니다. 1. Secret handoff — 진행하되 cert가 필요합니다
요청: kubeseal 암호화에 sealed-secrets controller의 public cert가 필요합니다. 클러스터 접근 없이 오프라인 암호화가 가능한 형식(
2.
|
| # | 항목 | 주체 | 상태 |
|---|---|---|---|
| 1 | SealedSecret 산출 workflow | AI | cert 수령 후 착수 |
| 2 | /ready + GMS_BASE_URL 검증 |
AI | 착수 (검증 fail-fast는 형식 확인 후) |
| 3 | smoke command | AI | 착수 |
| 4 | PreSync Job | Infra | 가능 확인 완료 |
구현 PR이 나오면 merge 후 immutable image source SHA와 digest를 이 이슈에 전달하겠습니다.
요청 2건 회신 부탁드립니다 — sealed-secrets public cert, 그리고 dev에 주입 예정인 GMS_BASE_URL이 /gmsapi/ 형식을 만족하는지 여부(값은 알려주지 않으셔도 됩니다, 형식 만족 여부만).
컨테이너 런타임 변경 관련
docker 대신 다른 방식으로 전환하신다고 들었습니다. AI 파트 쪽 변경은 필요하지 않은 것으로 확인했습니다.
Dockerfile이 표준 OCI 빌드만 씁니다 — # syntax= 지시어, --mount=type=cache, heredoc, 멀티스테이지가 모두 없어 어떤 OCI 빌더로도 그대로 빌드됩니다. CI의 manifest 조회도 Accept 헤더에 OCI와 docker manifest를 함께 명시하고 있어 형식 제약이 없습니다. 빌드 자체는 GitHub Actions runner에서 수행되므로 클러스터 런타임과 독립적입니다.
다만 두 가지만 확인 부탁드립니다.
① 기존 이미지를 그대로 쓰시는지, 재빌드하시는지. 빌드 도구가 바뀌면 같은 소스라도 digest가 달라집니다. 저희 CI는 기존 태그가 있으면 HEAD manifest 프리플라이트로 덮어쓰기를 거부하므로, 동일 SHA를 다른 빌더로 재빌드하시면 push가 실패합니다. 이미 발행된 이미지를 그대로 배포하시면 문제없습니다.
② imagePullSecret 경로. GHCR 패키지가 private이라 익명 pull이 되지 않습니다. 런타임 교체 시 인증 설정 경로가 달라지니 이 부분만 확인해 주세요.
|
이정헌님, 확인했습니다. init container 철회와 PreSync Job 적합성 확인 감사합니다. 아래 두 요청과 런타임 질문에 회신드립니다. 1. sealed-secrets public cert 제공현재
workflow에서 이 cert로 **strict scope의 2.
|
|
김세민님, 요청하신 §2 형식 검증은 유예 없이 fail-fast로 넣었습니다. 현재 live AI runtime Secret이 없어 기존 값과의 호환성 문제가 없다고 확인해 주신 대로입니다. 로컬 실측 (실 GMS + 실 pgvector, preset 27건):
배선 요청
주의 — smoke는 실호출이라 CI에 넣지 않았습니다. CI에서는 집계·종료 코드·값 미노출 규약만 스텁으로 검증합니다. GMS 가용성이 CI 성패에 들어오지 않게 하려는 판단이며, 실제 왕복 증명은 배포 절차의 이 명령이 담당합니다. 남은 deliverable 2건은 #33 병합 후 이 이슈에 전달하겠습니다.
리뷰에서 특히 확인 부탁드릴 지점은 |
deliverable ①·② 전달
immutable source SHA와 digest입니다.
구현 결과
응답은
구현 중 값 노출 경로를 하나 발견해 막았습니다. pydantic의 smoke — embedding과 judge를 각각 1회 실호출하고 한쪽이라도 실패하면 non-zero exit합니다. 성공 여부와 안전한 status만 출력하며 credential·endpoint·request 원문을 stdout·stderr에 남기지 않습니다. DB mutation은 없습니다. 실행에는 서버와 동일한 env 전체가 필요합니다( 남은 것
|
handoff ① 완료 — SealedSecret 산출 경로김세민님, 요청 ①(GitHub Secret → 암호화 Secret handoff)을 구현해 ② 산출 방식주신 인증서를 그대로 커밋했고 workflow가 매번 SHA-256 지문을 대조합니다( 평문 Secret YAML은 만들지 않습니다. 산출물은 업로드 전에 두 방향으로 검사합니다 — 모든 항목이 봉인 대상 7종 —
|
| 키 | 값 |
|---|---|
PINLOG_EMBEDDING_MODEL |
text-embedding-3-small |
PINLOG_EMBEDDING_DIMENSION |
1536 |
PINLOG_EMBEDDING_DISTANCE |
cosine |
PINLOG_EMBEDDING_PROFILE |
openai-text-embedding-3-small-1536-cosine-v1 |
PINLOG_JUDGE_MODEL |
gemini-2.5-flash (코드 기본값 있음, 미주입 가능) |
DATABASE_URL은 말씀대로 ai-db-credentials로 따로 두고 envFrom으로 합치는 것으로 이해했습니다.
배포 전 검사를 봉인 시점으로 앞당겼습니다
앱이 기동 시 fail-fast로 거르는 것과 같은 검사를 봉인할 때 미리 돌립니다. 배포하고 Pod이 죽어야 아는 상황을 줄이려는 것입니다.
GMS_BASE_URL에/gmsapi/포함PINLOG_EMBEDDING_DIMENSION이 정수PINLOG_EMBEDDING_PROFILE이 나머지 셋을 부분 문자열로 포함
특히 세 번째가 중요합니다. profile이 어긋난 채 배포되면 기존 임베딩이 조회 대상에서 전부 빠지는데, 검색 결과만 조용히 비어서 배포 후에는 알아채기 가장 어렵습니다.
확인 요청 — PINLOG_AI_INFRA_PR_TOKEN
요구 목록에 주신 값 중 이것만 성격이 다릅니다. AI 앱이 읽지 않습니다 — ai 레포 전체에서 참조가 0건이고 config.py에도 없습니다.
이름으로 보면 workflow가 GitOps 레포에 PR을 여는 토큰으로 읽히는데, 그렇다면 전달 방식이 지금의 artifact가 아니라 PR이 됩니다. 토큰은 권한을 정하지 않고 발급할 수 없어 넷을 여쭙습니다.
- 대상 레포 — 어느 GitOps 레포에 PR을 여는지
- 권한 범위 —
contents:write+pull_requests:write면 충분한지 (fine-grained PAT 기준) - 만료·회전 — 기간과 회전 주체
- 커밋 경로 — SealedSecret manifest를 그 레포의 어느 경로에 넣는지
답을 주시면 workflow를 artifact 방식에서 PR 방식으로 바꾸겠습니다. 그 전까지는 artifact로 받아 가실 수 있습니다.
아직 확인되지 않은 것
실제 봉인은 아직 돌리지 않았습니다. Actions Secret 등록이 선행이고, 등록 후 수동 트리거로 산출합니다. 지문·인증서 유효기간·kubeseal 릴리스·workflow 문법까지는 확인했지만, 봉인 결과를 controller가 실제로 복호화할 수 있는지는 클러스터에 적용해 보셔야 확정됩니다. 첫 적용에서 실패하면 바로 알려 주십시오.
이 PR(#32)도 main 대비 뒤처져 있는데, 계약 문서 확정이 남으신 것인지 확인 부탁드립니다.
|
AI owner Secret 7종 등록 후 봉인 workflow를 실제 실행했습니다. Secret 존재/형식 검증과 controller 인증서 지문 검증은 통과했지만, install 단계의 checksum 파일명 불일치로 실패했습니다.
|
고쳤습니다 — 재실행 부탁드립니다 (
|
⚠ 저희가 만든 충돌입니다 —
|
|
이 PR 의 base( AI 파트에 담당자가 추가 합류할 예정이라 통합 브랜치를 둔 것이고, 배포·봉인 경로는 변경하지 않았습니다. |
|
Summary
S15P11A705-96의 dev AI 배포를 위한 담당자 승인 요청 문서를 추가합니다.
/health동작과 readiness/liveness 구분 및/metrics부재 기록colosairreviewer 확인용 응답 표 추가Scope
Docs-only입니다. 서비스 코드, 설정, 테스트, CI, Dockerfile, Kubernetes/GitOps/live 리소스는 변경하지 않습니다.
Review requested
AI owner 이정헌: 문서의 Owner 응답 표를 비밀값 없이 작성하고 각 gate를 승인/수정 필요/보류로 응답해 주세요.
Reviewer candidate
colosair: owner 답변, source citation, fail-closed 조건을 확인해 주세요.Activation safety
credential handoff, model/profile 합의, bootstrap provenance/성공 증거가 없으면 관련 Infra gate는
false를 유지합니다. 이 PR은 live 변경 승인이 아닙니다.Verification
git diff --check