# 배포 정의 이 디렉터리는 **배포될 대상**을 정의한다. 빌드·이미지·배포 실행 방식은 사내 표준 CI/CD가 담당하며 이 저장소가 정하지 않는다. ## Helm Chart [helm/mcp-server/](helm/mcp-server/)가 유일한 배포 정의다. values는 두 축으로 나뉜다. | 파일 | 소유하는 것 | |---|---| | `values.yaml` | **배포 토폴로지.** 어떤 MCP가 어떤 Tool Service를 보는가, 공개 path, 가용성 등급 | | `values-{dev,test,prod}.yaml` | **환경 차이.** namespace, 이미지, 공개 host·허용 CIDR, 등급별 replica·PDB, 리소스 | 설치할 때 두 번째 축을 `-f`로, 첫 번째 축에서 고를 배포 하나를 `--set deploymentKey=`로 지정한다. ```bash helm upgrade --install processing-critical-mcp helm/mcp-server -f helm/mcp-server/values-dev.yaml --set deploymentKey=processing-critical -n ``` `deploymentKey`에는 기본값이 없다. 지정을 빠뜨리면 렌더링 단계에서 멈춘다. 엉뚱한 배포가 조용히 설치되는 것보다 낫다. ### 공유 host와 배포별 path [ADR-0009](../docs/decisions/ADR-0009-container-handles-public-mcp-path.md)에 따라 한 환경은 하나의 공개 host를 사용하고, 각 Helm release는 고유 path의 OpenShift Route를 만든다. Route는 Service만 선택하고 공개 path를 그대로 전달하며, 컨테이너가 같은 path를 직접 처리한다. ```text https://mcp-dev.apps.example.internal/mcp/processing-critical -> processing-critical-mcp:8080/mcp/processing-critical https://mcp-dev.apps.example.internal/mcp/information-standard -> information-standard-mcp:8080/mcp/information-standard ``` 공개 URL은 각각 독립된 MCP다. Agent Builder는 URL별로 등록하고 initialize하며, 한 Route나 MCP Pod의 장애가 다른 path의 Deployment로 전파되지 않는다. ### MCP 하나는 Tool Service 하나만 본다 [ADR-0007](../docs/decisions/ADR-0007-one-mcp-per-tool-service.md)의 결정이다. 대상을 늘리는 방법은 bundle 목록을 늘리는 것이 아니라 **배포를 하나 더 만드는 것**이다. ```yaml deployments: processing-critical: name: processing-critical-mcp service: processing-critical-tools # ← 이름만. 주소는 template이 만든다 namePrefix: "processing." # ← 업무 단위. 등급을 넣지 않는다 tier: critical publicPath: /mcp/processing-critical # ← 환경 host 안에서 유일 ``` 배포가 10개든 20개든 **파일 수는 늘지 않는다.** 전체 매핑을 한 화면에서 검토할 수 있고, `--set`으로 고르는 값 하나만 배포마다 달라진다. MCP Server와 Tool Service는 같은 namespace에 배포하므로 values에는 서비스 이름만 적고 주소는 template이 조립한다. 환경마다 URL을 반복하지 않으므로 오타로 다른 대상을 호출할 수 없다. `identity`도 `{배포 이름}-{global.env}`로 template이 조립한다. 현재 Redis cache 구현이 이 값을 사용하지만, key namespace와 공유 정책은 아직 확정되지 않았다. ### 가용성 등급 배포를 업무 × 중요도로 나누는 목적은 **중요 등급에만 비용을 쓰기 위해서**다. ```yaml tiers: critical: { replicas: 3, podDisruptionBudget: true, spreadAcrossNodes: true } standard: { replicas: 2, podDisruptionBudget: false, spreadAcrossNodes: false } ``` test와 prod의 `critical`은 **replica 2 이상, PodDisruptionBudget, 노드 분산 설정이 필수**다. replica가 1이면 rolling update 중 반드시 공백이 생기고, PDB가 없으면 노드 drain이 마지막 Pod을 내릴 수 있다. `HelmDeploymentContractTest`는 values와 template의 정적 규칙을 검사한다. dev는 배포마다 Pod 1개로 운영하므로 이 검사 대상이 아니다. 정적 테스트는 Helm 렌더러를 실행하지 않는다. 실제 배포 파이프라인은 사용하는 환경과 등급별로 `helm lint`와 `helm template`을 실행해 병합된 values와 생성 YAML을 확인해야 한다. ```bash helm lint helm/mcp-server -f helm/mcp-server/values-prod.yaml --set deploymentKey=processing-critical helm template processing-critical-mcp helm/mcp-server -f helm/mcp-server/values-prod.yaml --set deploymentKey=processing-critical helm template processing-standard-mcp helm/mcp-server -f helm/mcp-server/values-prod.yaml --set deploymentKey=processing-standard ``` **나누는 것만으로 가용성이 생기지는 않는다.** 같은 노드 배치, namespace 쿼터, 공통 Redis·클러스터 장애는 분할로 막히지 않는다. 남은 작업은 [extension-points.md](../docs/extension-points.md)의 "운영 적용 전 필수 보완"에서 관리한다. ### dev에서 MCP에 연결되지 않을 때 **먼저 Tool Service가 떠 있는지 확인한다.** readiness가 usable snapshot을 요구하므로, Tool Service가 없으면 MCP Pod은 Ready가 되지 못하고 Service endpoint에서 빠진다. dev는 배포마다 Pod 1개라 그 순간 그 MCP로는 아예 연결되지 않는다. "MCP가 죽었다"가 아니라 "읽을 Tool이 없다"는 뜻이다. ```bash kubectl get pod -l app=<배포 이름> # 0/1 Ready이면 이 경우다 kubectl describe pod # Readiness probe 실패 사유 kubectl port-forward 9090:9090 # /actuator/toolBundles로 bundle 상태 확인 ``` Tool Service가 뜨면 다음 refresh 주기(기본 30초) 안에 스스로 Ready가 된다. 재기동할 필요가 없다. `/actuator/toolBundles`는 management 포트라 NetworkPolicy가 관제 namespace로 제한하므로, 개발자는 위처럼 `port-forward`로 본다. ## 확정 전 임시값 `values.yaml`의 Tool Service 이름·이미지 경로와 `values-{env}.yaml`의 namespace·공개 host·Route 허용 CIDR은 자리표시자다. 각 파일의 `TODO` 주석을 참고해 확정 시 교체하고, 존재하지 않는 배포는 `deployments`에서 삭제한다. ## 배포 시 알아야 할 앱 제약 아래는 이 애플리케이션의 동작에서 나온 사실이다. 각 항목의 정본은 링크한 문서이며 값을 여기 옮겨 적지 않는다. | 제약 | 정본 | |---|---| | readiness·liveness는 management 포트에서 제공한다 | [architecture.md](../docs/architecture.md) | | readiness는 첫 Tool 조회 시도 완료 후 usable snapshot이 있을 때만 UP이다 | [architecture.md](../docs/architecture.md) | | `terminationGracePeriodSeconds`는 Spring drain보다 길어야 한다 | [architecture.md의 요청 시간 예산](../docs/architecture.md#요청-시간-예산) | | **NetworkPolicy는 필수다. 비활성화 스위치를 두지 않았다** | [ADR-0006](../docs/decisions/ADR-0006-no-authentication-in-mcp.md) | | 공개 path는 Route와 컨테이너 endpoint가 동일하게 사용한다 | [ADR-0009](../docs/decisions/ADR-0009-container-handles-public-mcp-path.md) | | Redis key·TTL·공유 정책은 확정 전이다 | [extension-points.md](../docs/extension-points.md#운영-적용-전-필수-보완) | | bundle은 정확히 하나다 | [ADR-0007](../docs/decisions/ADR-0007-one-mcp-per-tool-service.md) | Route IP allowlist와 NetworkPolicy는 특히 중요하다. 이 서버는 인증·인가를 하지 않으므로 `/mcp`에 도달할 수 있다는 것이 곧 인가다. Route는 Agent Builder 고정 egress CIDR만 받고, NetworkPolicy는 Route backend인 ingress controller와 명시한 Agent Builder namespace만 업무 포트에 허용한다. ## 미확정 항목 배포 정의를 이 저장소가 어디까지 소유하는지, namespace·registry 명명 규칙은 아직 확정되지 않았다. [docs/extension-points.md](../docs/extension-points.md)에서 관리한다.