Files
dap-was-dapms/deploy/README.md
koseokmin 1e6fa8f22f Portal을 endpoint 원천으로 확정하고 route 단위 실패 격리를 적용한다
내부망 운영은 route↔Tool Service 매핑을 Portal이 소유하고, MCP 배포 하나가 N개
route를 서비스하며, route 하나에 N개 Tool Service가 붙을 수 있다. 이 판단을
ADR-0010으로 남기고 ADR-0007 전체와 ADR-0009 결정 4를 대체한다.

계약
- Portal-MCP registry 조회 계약 v0.1과 예제 JSON을 docs/contracts/portal-mcp/에
  신설한다. 지금까지 이 경로에는 정본이 없었다.
- 예제를 PortalToolRegistryClient의 실제 파싱 경로에 태우는 계약 테스트를 추가해
  문서와 구현이 따로 표류하지 않게 한다.

실패 격리
- fetchAllTools()의 실패 전파를 route 단위로 격리한다. 계약 v0.2의 "aggregate는
  전부 아니면 전무"는 카탈로그 하나를 전제한 규칙인데, route가 N개가 되면서 전
  route로 확대돼 있었다. Tool Service 하나의 장애가 cold start에서 Pod 전체를
  내리고 steady state에서 모든 route의 갱신을 멈추던 동작을 없앤다.
- 제거 판단의 원천을 ToolRegistryClient.knownRoutes()로 분리한다. 조회 결과를
  기준으로 지우면 이번 주기에 실패한 route의 정상 snapshot까지 사라져 "어떤
  실패도 목록을 비우지 않는다" 불변식이 깨진다.
- readiness는 최소 1개 route로 UP을 유지한다. 모든 route를 요구하면 정상 route까지
  트래픽에서 빠져 위 격리를 되돌리기 때문이다. 대신 routesWithoutSnapshot을
  health detail로 노출해 관제가 부분 상태를 감지하게 한다.

설정과 기동
- warm start가 route별 Redis key를 읽도록 확장하고, 읽을 key를 알기 위해 기동
  preload 순서를 registry 조회 → warm start → manifest 조회로 바꾼다.
- 어떤 코드도 읽지 않던 mcp.portal.route-key를 제거한다. route key는 요청 URI에서만
  결정되며, 설정으로 보정하면 잘못된 단일 진입점 호출이 조용히 성공한다.

정리
- ToolRegistryService.java의 이중 인코딩으로 깨져 있던 한글 Javadoc 33줄을 코드
  동작에 맞춰 다시 쓰고, replaceSnapshot 위에 겹쳐 있던 고아 Javadoc 블록을 지운다.
- Helm chart는 mcp.bundles 구성에서 계속 유효하므로 삭제하지 않고, 내부망 운영
  대상이 아니라는 사실을 deploy/README.md와 values.yaml에 명시한다.

검증: 이 환경은 loopback이 막혀 gradlew check를 실행하지 못했다. CodeStyleContract가
보는 항목(줄바꿈, 탭, 행말 공백, 파일 끝 개행, 미사용 import, import 순서)은 변경된
Java 13개 파일에 대해 따로 재현해 확인했다. 컴파일과 테스트 실행은 미확인이다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 18:10:34 +09:00

141 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 배포 정의
이 디렉터리는 **배포될 대상**을 정의한다. 빌드·이미지·배포 실행 방식은 사내 표준 CI/CD가 담당하며
이 저장소가 정하지 않는다.
> **내부망 운영은 이 Chart를 사용하지 않는다**([ADR-0010](../docs/decisions/ADR-0010-portal-owns-route-and-endpoint-registry.md) 결정 7).
> 여기 정의된 토폴로지는 배포 하나가 Tool Service 하나를 보는 `mcp.bundles` 구성(ADR-0007/0009)을 전제한다.
> 내부망 운영은 endpoint 목록과 route 매핑의 원천을 Portal로 옮겼고, 배포 하나가 N개 route를 서비스한다.
>
> Chart를 지우지 않는 이유는 `mcp.bundles` 구성이 코드에서 사라지지 않았고 local 검증과 1:1 배포가
> 필요한 환경에서 그대로 유효하기 때문이다. **다만 아래 `deployments` 목록의 업무 이름은 예시이며
> 실제 배포 대상이 아니다.** Portal 구성으로 갈 환경에 이 Chart를 적용하면 route가 하나로 고정된다.
## 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 <namespace>
```
`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 <pod> # Readiness probe 실패 사유
kubectl port-forward <pod> 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)에서 관리한다.