GitOps 저장소도 ArgoCD Application도 아직 없어, 그때까지 이 저장소가 push 방식 파이프라인(.gitea/workflows/)을 임시로 소유한다. 무엇을 포기하는지와 넘길 때 할 일은 deploy/README.md에 적었다. Chart는 portal과 bundles 두 배포 모델을 모두 렌더링한다. ADR-0013이 ADR-0007을 대체했으므로 운영은 portal이 기준이지만, bundles 경로를 언제 삭제할지는 아직 정하지 않았다. - .gitea/workflows/ci.yaml, deploy-openshift.yaml - deploy/ci/render-manifests.sh, deploy/examples/ - Chart: mode 분기, selectedDeployment/tier helper, imagePullSecrets, toolService.apiKeySecret 참조 - extension-points.md에 미결 항목 9~12 추가 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
배포 정의
이 디렉터리는 배포될 대상과, 그것을 클러스터에 올리는 임시 경로를 정의한다. 빌드·배포 실행 방식의 정본은 원래 사내 표준 CI/CD이며 이 저장소가 정하지 않는다. 지금 여기 파이프라인이 있는 이유는 아래에 적었다.
내부망 운영은 이 Chart를 사용하지 않는다(ADR-0013 결정 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/가 유일한 배포 정의다.
배포 모델이 두 가지다
| mode | 무엇이 route↔Tool Service 매핑을 소유하는가 | 근거 |
|---|---|---|
portal (기본값) |
Portal. 배포 하나가 N개 route를 서비스하고 route key는 /mcp/{routeKey} URI에서만 온다 |
ADR-0013 |
bundles |
배포 정의. 배포 하나가 Tool Service 하나만 보고 매핑을 배포 시점에 못박는다 | ADR-0007 |
현재 애플리케이션이 실제로 도는 경로는 portal이다. bundles는 ADR-0013이 대체했지만 코드 경로가
남아 있어 1:1 검증과 격리 배포에 쓸 수 있다. 어느 쪽을 운영에 쓸지는 아직 확정되지 않았고
extension-points.md에서 관리한다.
mode를 바꾸면 ConfigMap의 Tool 원천이 통째로 바뀐다. 값 하나로 배포 성격이 달라지므로
설치 명령에 항상 명시한다.
values는 두 축으로 나뉜다
| 파일 | 소유하는 것 |
|---|---|
values.yaml |
배포 토폴로지. mode, portal 배포 정의, bundles 배포 목록, 등급 기준 |
values-{dev,test,prod}.yaml |
환경 차이. namespace, 공개 host·허용 CIDR, Portal registry 주소, 등급별 replica·PDB, 리소스 |
환경 파일은 토폴로지를 갖지 않는다. HelmDeploymentContractTest가 그 경계를 고정한다.
# portal 모드. 배포가 하나이므로 deploymentKey가 없다.
helm upgrade --install axhub-mcp helm/mcp-server -f helm/mcp-server/values-dev.yaml -n <namespace>
# bundles 모드. 설치할 배포 하나를 반드시 고른다.
helm upgrade --install processing-critical-mcp helm/mcp-server -f helm/mcp-server/values-dev.yaml \
--set mode=bundles --set deploymentKey=processing-critical -n <namespace>
deploymentKey에는 기본값이 없다. bundles에서 지정을 빠뜨리면 렌더링 단계에서 멈춘다.
엉뚱한 배포가 조용히 설치되는 것보다 낫다. 반대로 portal에서 deploymentKey를 주면 역시 멈춘다.
route를 배포 정의에 적기 시작하면 Portal을 원천으로 둔 이유가 사라지기 때문이다.
공개 host와 path
한 환경은 하나의 공개 host를 사용한다(ADR-0009). Route는 Service만 선택하고 공개 path를 그대로 전달하며, 컨테이너가 같은 path를 직접 처리한다.
portal 모드에서 Route path는 /mcp 하나다. OpenShift Route의 path는 prefix 매칭이므로
/mcp/{routeKey} 전체가 이 Route로 들어오고, route 구분은 컨테이너가 한다.
https://mcp-dev.apps.example.internal/mcp/cus -> axhub-mcp:8080/mcp/cus
https://mcp-dev.apps.example.internal/mcp/sal -> axhub-mcp:8080/mcp/sal
bundles 모드에서는 Route가 배포마다 하나씩 생기고 path가 배포별로 다르다.
https://mcp-dev.apps.example.internal/mcp/processing-critical -> processing-critical-mcp:8080/mcp/processing-critical
두 경우 모두 공개 URL은 각각 독립된 MCP다. Agent Builder는 URL별로 등록하고 initialize한다.
가용성 등급
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을 내릴 수 있다.
dev는 배포마다 Pod 1개로 운영하므로 이 검사 대상이 아니다.
portal 모드에서 등급별 물리 분리는 성립하지 않는다. 배포가 하나이므로 전 route가 같은
프로세스·같은 replica set을 공유한다(ADR-0013 전제 2).
tiers는 그 하나의 배포에 어떤 가용성 기준을 적용할지만 정한다.
렌더링 검증
HelmDeploymentContractTest는 values와 template의 정적 규칙만 본다. helper 오류, 조건 분기 실수,
들여쓰기는 실제로 렌더링해야 드러난다. 두 검사는 서로를 대신하지 못한다.
deploy/ci/render-manifests.sh # 환경 × 모드 전 조합 lint + template
CI가 매 push에서 같은 스크립트를 돌리고 결과를 rendered-manifests 아티팩트로 올린다.
GitOps 저장소가 없는 동안의 우회
Chart를 어디에 둘지, 배포를 무엇이 실행할지는 아직 확정되지 않았다. GitOps 저장소도 ArgoCD Application도 없다. 그동안 파이프라인을 멈춰 두지 않기 위해 아래 형태로 돌린다.
| 파일 | 트리거 | 하는 일 |
|---|---|---|
.gitea/workflows/ci.yaml |
main push, PR | gradlew check, Chart lint·template, 이미지 빌드·push |
.gitea/workflows/deploy-openshift.yaml |
수동 실행 | 고른 환경·모드로 helm upgrade --install |
.gitea/workflows/deploy.yaml |
main push | 기존 VM docker compose 배포 |
이 방식이 무엇을 포기하는가
숨기지 않고 적는다. GitOps로 넘어가는 판단의 근거가 되기 때문이다.
- 클러스터 상태가 저장소와 자동으로 맞춰지지 않는다. 누가
oc edit으로 고치면 그대로 남는다. - 배포 이력이 Helm release history에만 남는다.
git revert로 되돌릴 수 없고helm rollback을 써야 한다. - 파이프라인이 클러스터 자격증명을 들고 있어야 한다. 러너를 신뢰 경계 안에 두어야 한다.
- 어떤 이미지가 어느 환경에 떠 있는지 저장소만 봐서는 모른다. 수동 실행 이력을 봐야 한다.
이 때문에 OpenShift 배포에는 자동 트리거를 두지 않았다. 사람이 환경·모드·이미지 tag를 확인하고 실행한다.
GitOps 저장소가 생기면
deploy-openshift.yaml을 삭제한다. 클러스터 자격증명 secret도 회수한다.- ArgoCD Application이 이 Chart를 참조하게 하거나, Chart 자체를 배포 저장소로 옮긴다.
- CI의
rendered-manifests아티팩트가 인수인계 형태다. 그 시점에 무엇이 배포되고 있었는지가 거기 그대로 있으므로, 옮긴 뒤 diff로 대조한다. deploy.yaml의 VM compose 배포를 계속 쓸지 결정한다. 스크립트가 저장소 밖(러너의/home/ubuntu/apps/prd-dap-gateway/deploy.sh)에 있어 이 저장소가 내용을 모른다.
아직 필요한 secret
확정 전까지 CI는 이미지 빌드까지만 하고 push를 건너뛴다. 없는 secret 때문에 파이프라인 전체가 실패로 보이지 않게 하기 위해서다.
| secret | 쓰는 곳 | 없으면 |
|---|---|---|
REGISTRY_HOST·REGISTRY_USER·REGISTRY_PASSWORD·IMAGE_REPOSITORY |
CI 이미지 push | push 건너뜀 |
OCP_SERVER·OCP_TOKEN |
OpenShift 배포 | 배포 실패 |
OCP_CA_CERT |
API 인증서를 사내 CA가 서명했을 때 | 러너의 신뢰 저장소를 쓴다. 사내 CA면 TLS 검증 실패 |
OCP_NAMESPACE_DEV·OCP_NAMESPACE_TEST·OCP_NAMESPACE_PROD |
OpenShift 배포 | 배포 실패 |
dev에서 MCP에 연결되지 않을 때
먼저 Tool Service가 떠 있는지 확인한다. readiness가 usable snapshot을 요구하므로, Tool Service가 없으면 MCP Pod은 Ready가 되지 못하고 Service endpoint에서 빠진다. "MCP가 죽었다"가 아니라 "읽을 Tool이 없다"는 뜻이다.
portal 모드에서는 Portal registry 조회부터 확인한다. registry를 못 읽으면 route 자체가 등록되지 않아
/mcp/{routeKey} 호출이 route key 검증에서 거부된다.
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 주기 안에 스스로 Ready가 된다. 재기동할 필요가 없다.
/actuator/toolBundles는 management 포트라 NetworkPolicy가 관제 namespace로 제한하므로,
개발자는 위처럼 port-forward로 본다.
확정 전 임시값
values.yaml의 이미지 경로와 Tool Service 이름, values-{env}.yaml의 namespace·공개 host·Route 허용
CIDR·Portal registry 주소는 자리표시자다. 각 파일의 TODO 주석을 참고해 확정 시 교체하고,
존재하지 않는 배포는 deployments에서 삭제한다.
배포 시 알아야 할 앱 제약
아래는 이 애플리케이션의 동작에서 나온 사실이다. 각 항목의 정본은 링크한 문서이며 값을 여기 옮겨 적지 않는다.
| 제약 | 정본 |
|---|---|
| readiness·liveness는 management 포트에서 제공한다 | architecture.md |
| readiness는 첫 Tool 조회 시도 완료 후 usable snapshot이 있을 때만 UP이다 | architecture.md |
terminationGracePeriodSeconds는 Spring drain보다 길어야 한다 |
architecture.md의 요청 시간 예산 |
| NetworkPolicy는 필수다. 비활성화 스위치를 두지 않았다 | ADR-0006 |
| 공개 path는 Route와 컨테이너 endpoint가 동일하게 사용한다 | ADR-0009 |
| route↔Tool Service 매핑의 원천은 Portal이다 | ADR-0013 |
| Portal 응답 모양과 실패 처리 | Portal-MCP 계약 v0.1 |
| Redis key·TTL·공유 정책은 확정 전이다 | extension-points.md |
Route IP allowlist와 NetworkPolicy는 특히 중요하다. 이 서버는 인증·인가를 하지 않으므로 /mcp에
도달할 수 있다는 것이 곧 인가다. Route는 Agent Builder 고정 egress CIDR만 받고, NetworkPolicy는 Route
backend인 ingress controller와 명시한 Agent Builder namespace만 업무 포트에 허용한다.
portal 모드는 여기에 하나를 더한다. MCP는 Portal registry가 준 주소를 그대로 호출한다. Portal이
신뢰 경계 안에 있다는 전제가 깨지면 MCP의 outbound 대상이 통째로 바뀐다(ADR-0013 전제 4).
egress 제한은 아직 없으며 extension-points.md에서 관리한다.