10. 사내 frontend·backend와 연결하기
이 장에서 처음 나오는 말4개
SSAServer-Side Apply- 전체 manifest를 보내면 API server가 field 소유자를 추적하며 병합하는 apply 방식이다.
ServiceAccount- Pod 안의 workload가 Kubernetes API를 호출할 때 쓰는 신원이다.
informer- resource 변경을 watch하며 로컬 캐시를 최신으로 유지하는 클라이언트 패턴이다.
A2AAgent-to-Agent- Agent를 외부에서 호출하고 응답을 stream으로 받는 표준 protocol이다.
8~9장이 kagent가 무엇인지였다면 이 장은 이미 온프렘 Kubernetes에 올려 둔 우리 frontend·backend가 kagent와 어디서 만나는가다. 사내 상황을 다음으로 가정한다.
- frontend와 backend(Node/TypeScript)가 같은 온프렘 cluster에 이미 떠 있다.
- 사용자·부서의 단일 원본은 Keycloak 또는 Entra ID다.
- 사용자는 포털에서 Agent를 만들고, 다른 부서 사람과 공유한다.
- 사용자에게
kubectl이나 kubeconfig를 주지 않는다.
dashboard를 어디까지 쓸 것인가
섹션 제목: “dashboard를 어디까지 쓸 것인가”kagent dashboard는 8장에서 봤듯 controller의 내부 API를 통해 resource-derived 상태와 chat을 보여 주는 운영 도구다. 자체 포털을 만들면 역할이 이렇게 갈린다.
| kagent dashboard | 사내 포털 | |
|---|---|---|
| 대상 | 플랫폼 팀 | 전체 임직원 |
| 보는 범위 | cluster의 모든 Agent | 내가 권한을 가진 Agent |
| 쓰는 곳 | 설치 검증, 장애 진단, resource 상태 확인 | 생성·공유·호출·감사 |
| 배포 | 내부망에서 운영자에게만 노출 | 사내 표준 인증 뒤 |
dashboard를 없앨 필요는 없다. 다만 최종 사용자에게 열지 않는다. dashboard는 Agent별 권한을 판단하지 않으므로, 링크를 아는 사람이 다른 부서 Agent를 보고 호출할 수 있다.
통합 지점을 무엇으로 잡을 것인가
섹션 제목: “통합 지점을 무엇으로 잡을 것인가”backend가 kagent에 말을 거는 방법은 목적에 따라 둘이다. dashboard 내부 API와 CLI shell은 어느 쪽의 계약도 아니다.
| 목적 | 계약 | 쓰지 않을 것 |
|---|---|---|
| 배포·수정·삭제·상태 관찰 | Kubernetes API의 CRD schema·status | dashboard 내부 HTTP API, CLI shell |
| Agent Card·task·stream 호출 | kagent-controller:8083의 /api/a2a/{namespace}/{agent-name}/ | Agent Pod의 임의 Service, CLI shell |
관리 경로에 CRD를 고르면 Agent 배포가 Kubernetes resource 조작이 되므로 RBAC·감사 로그·GitOps 같은 기존 온프렘 운영 자산을 그대로 쓴다. 호출 경로는 A2A JSON-RPC·stream·task 상태를 우리 invoke contract로 번역한다. 두 client의 credential·timeout·retry·관측값도 분리한다.
일반 Agent와 MCP는 전용 workload를 만든다
섹션 제목: “일반 Agent와 MCP는 전용 workload를 만든다”먼저 관리·배포 경로다. 가운데 CR은 API server에 저장되는 선언과 상태이고 Pod가 아니다.
kagent controller는 Agent CR을 watch해 Agent engine의
Deployment·Service를 만든다. backend는 이 controller Pod를 관리 API처럼 쓰지 않고 Kubernetes API로 CR을
apply·list/watch한다.
MCP는 두 갈래다. RemoteMCPServer는 이미 떠 있는 endpoint를 가리키므로 그 CR 자체가 Pod를 만들지 않는다.
반면 kmcp의 MCPServer는 kmcp controller가 Deployment·Service와
MCP server Pod로 번역한다. 포털에 보여 줄 Agent·tool 목록은 이 runtime inventory를 그대로 노출하지 않고,
DB의 Publication·Grant·승인된 tool 목록과 합쳐 만든다.
이미 배포된 Agent를 호출할 때는 Kubernetes API를 지나지 않는다. controller는 A2A 입구와 route를 맡고, 대화 loop와 실제 MCP tool call은 Agent engine Pod에서 실행된다. 따라서 backend는 호출할 때만 controller Service를 지나며, Agent Pod나 MCP Pod의 임의 Service를 제품 계약으로 직접 호출하지 않는다.
SandboxAgent와 AgentHarness는 Agent Substrate로 간다
섹션 제목: “SandboxAgent와 AgentHarness는 Agent Substrate로 간다”SandboxAgent와 AgentHarness는 일반 Agent처럼 Agent별 Deployment Pod를 만들지 않는다. kagent controller가
Agent Substrate에 actor 실행을 요청하면, Substrate가 공유 WorkerPool의 gVisor worker Pod에 actor를 배치하고
idle 상태를 object storage에 snapshot한다.
SandboxAgent는 일반 Agent와 비슷한 spec의 Go
Declarative Agent를 sandbox actor로 실행한다. AgentHarness는
별도 kind이며 OpenClaw·Hermes 같은 coding agent의 장기 실행 환경을 만든다. kagent는 harness마다
ActorTemplate을 만들고 첫 chat 때 shared actor를 시작하며, A2A 요청을 actor 안의 acp-shim에 ACP로
bridge한다. 둘 다 Agent별 상주 Pod 대신 WorkerPool Pod를 공유하지만 actor state와 snapshot은 분리된다.
두 흐름 — 배포와 호출
섹션 제목: “두 흐름 — 배포와 호출”포털에는 성격이 다른 두 흐름이 있다. 1장에서 나눈 그대로다.
Agent를 만들 때
섹션 제목: “Agent를 만들 때”핵심은 두 가지다. 원본은 우리 DB에 먼저 저장하고, CR은 그 원본을 번역해 만든다.
그리고 사용자가 보낸 값을 그대로 apply하지 않는다 —
9장의 필드 구분표대로
namespace·security context·allowedNamespaces는 서버가 채운다.
controller A2A route로 Agent를 호출할 때
섹션 제목: “controller A2A route로 Agent를 호출할 때”호출 경로가 backend를 지나야 하는 이유는 세 가지다 — 권한 검사, 감사 기록, 그리고 tool 승인 같은 대화 중 이벤트를 우리 UI 규격으로 번역하는 일이다.
backend 구현 — Node/TypeScript
섹션 제목: “backend 구현 — Node/TypeScript”관리에는 공식 @kubernetes/client-node, 호출에는 controller A2A client를 둔다. 두 client를 한 class의
숨은 분기로 합치지 말고 deploy/status port와 invoke port로 나눈다.
신원 — in-cluster ServiceAccount
섹션 제목: “신원 — in-cluster ServiceAccount”backend Pod에 ServiceAccount를 붙이고 loadFromCluster()로 자격을 읽는다. 사용자 개인의 Kubernetes 권한을
쓰지 않는다. 사용자는 애초에 cluster 신원이 없고, 권한 판단은 backend가 DB의 Grant로 한다.
import { KubeConfig, KubernetesObjectApi, makeInformer } from '@kubernetes/client-node';
const kc = new KubeConfig();kc.loadFromCluster();const objects = KubernetesObjectApi.makeApiClient(kc);ServiceAccount에는 kagent.dev API group의 agents·modelconfigs·remotemcpservers에 대한 권한만
준다. 대상 namespace도 목록으로 제한한다. SandboxAgent나 AgentHarness publication을 실제로 열 때만 별도
policy와 함께 sandboxagents·agentharnesses 권한을 추가한다. backend가 탈취됐을 때의 blast radius가
여기서 정해진다.
쓰기 — server-side apply
섹션 제목: “쓰기 — server-side apply”CR 생성·수정은 create/update 분기 대신 server-side apply 하나로 통일한다. 같은 요청을 여러 번 보내도
같은 결과가 되고, fieldManager를 지정하면 우리가 소유한 필드가 무엇인지 API server가 추적한다.
누군가 dashboard에서 손대도 다음 apply가 우리 상태로 되돌린다.
const FIELD_MANAGER = 'agent-portal';
// manifest는 사용자 입력이 아니라 DB의 AgentVersion에서 생성한다const manifest = buildAgentManifest(agentVersion, target);
// apply는 Content-Type: application/apply-patch+yaml 과 fieldManager로 보낸다.// 정확한 인자 형태는 설치한 client-node 버전의 example을 따른다.await applyResource(objects, manifest, FIELD_MANAGER);CR 이름은 사용자가 붙인 표시 이름이 아니라 우리 ID에서 결정적으로 만든 이름을 쓴다.
표시 이름은 바뀌고 한글·공백이 들어가지만 Kubernetes 이름은 DNS 규칙을 지켜야 하고 바뀌면 안 되기 때문이다.
반대 방향 조회를 위해 deploymentId label을 함께 넣고, DB에는
Deployment.providerRef로
cluster·namespace·이름·UID를 기록한다.
읽기 — informer로 status를 받는다
섹션 제목: “읽기 — informer로 status를 받는다”배포 상태를 사용자 요청마다 Kubernetes API에 물어보면 API server가 포털 트래픽을 그대로 받는다. informer로 watch해 두고 status 변경을 DB에 반영한 뒤, 포털 화면은 DB만 읽게 한다.
const path = '/apis/kagent.dev/v1alpha2/namespaces/agents-hr/agents';const informer = makeInformer(kc, path, listFn);
informer.on('update', (obj) => syncDeploymentStatus(obj));informer.on('error', (err) => scheduleRestart(err));informer는 끊긴다. error 핸들러에서 재시작을 예약하지 않으면 조용히 죽고, 화면의 상태가 영원히
Pending에 머문다. backend replica가 여러 개면 어느 replica가 DB에 쓸지도 정해야 한다.
호출 — controller A2A route
섹션 제목: “호출 — controller A2A route”in-cluster URL은 controller Service DNS와 Agent의 namespace·resource name으로 결정한다.
const url = new URL( `/api/a2a/${namespace}/${agentName}/`, 'http://kagent-controller.kagent.svc.cluster.local:8083',);
const response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(toA2AMessageSend(platformInvocation)), signal: AbortSignal.timeout(120_000),});const result = normalizeA2AResponse(await response.json());실제 adapter는 먼저 /.well-known/agent.json을 검증하고 request ID·session owner·timeout·stream reconnect를
기록한다. Agent가 SandboxAgent로 바뀌어도 이 URL shape와 응답 정규화는 같아야 한다. 실행 가능한
server-side apply·condition wait·raw A2A 예제는
kagent-lab 7장에서 Node/TypeScript로 관통한다.
무엇을 어디에 저장하는가
섹션 제목: “무엇을 어디에 저장하는가”가장 자주 틀리는 부분이다. Kubernetes를 데이터베이스로 쓰지 않는다.
| 데이터 | 원본 | 이유 |
|---|---|---|
| Agent 정체성·소유자·설명 | 우리 DB | provider를 바꿔도 남아야 한다 |
| version 이력 | 우리 DB | CR을 덮어쓰면 이전 version 조사가 불가능 |
| knowledge source·version·binding | 우리 DB | source revision·ACL·index lineage가 runtime보다 오래 남아야 한다 |
| 부서·개인 권한(Grant) | 우리 DB | kagent에 대응 개념이 없다 |
| 승인 이력·감사 event | 우리 DB | 보존 기간이 cluster 수명과 다르다 |
| 실행 중인 Agent 상태 | Kubernetes | controller가 만드는 사실 |
| Pod·Service·condition | Kubernetes | 진단용 |
| 대화 이력 | 결정 필요 | 아래 참조 |
대화 이력은 기본적으로 kagent engine 쪽 session에 있다. 사내에서 감사·검색·품질 개선에 쓰려면 backend가 stream을 중계하는 김에 우리 DB에도 남기는 편이 낫다. cluster를 재설치하거나 runtime을 교체해도 남고, Langfuse 같은 LLM 관측 도구로 보낼 때도 우리 쪽 trace가 원본이 된다.
부서별 접근 통제 두 경로
섹션 제목: “부서별 접근 통제 두 경로”여기가 kagent를 그냥 쓰면 안 되는 이유다. 검토 기준인 kagent v0.9.9는 oauth2-proxy 기반 OIDC 인증을 지원하지만, 공식 release note는 application access control이 아직 구현되지 않았다고 명시한다. 로그인한 사용자가 누구인지 아는 것과 Agent별 실행·수정 권한을 판단하는 것은 다르다.
그래서 막아야 할 문이 둘이다.
- 관리 경로 — Agent 생성·수정·삭제. backend가 DB의 소유권·부서를 검사한 뒤 자기 ServiceAccount로 대행한다. 사용자에게 kubeconfig를 주지 않으므로 우회 경로 자체가 없다.
- 호출 경로 — 문서화된 A2A 표면은
kagent-controllerService의8083이다. 사내망에서 이 port에 직접 닿을 수 있으면 backend의 Grant 검사가 무의미해진다. controller Pod의8083ingress를 backend·승인된 gateway·운영 UI에서만 허용해야 통제가 완성된다.
namespace는 부서 단위로 나누는 편이 단순하다. ResourceQuota로 부서별 자원을 나누고, NetworkPolicy와 egress 허용 목록을 namespace 단위로 관리하고, 사고 시 격리 범위도 부서로 떨어진다. 사용자마다 namespace를 만드는 방식은 수가 금방 수천이 되고 quota·policy 관리가 어려워진다.
drift와 실패 다루기
섹션 제목: “drift와 실패 다루기”CR을 apply했다고 끝이 아니다. 포털이 감당해야 할 상태 차이가 셋 있다.
| 상황 | 증상 | 대응 |
|---|---|---|
| apply는 성공, runtime은 실패 | DB는 Deployed, Pod는 CrashLoopBackOff이거나 actor restore 실패 | condition과 Pod·actor·A2A health를 함께 읽어 정규화 |
| cluster에서 직접 수정 | dashboard·kubectl로 누군가 spec 변경 | 주기적 재조정 job이 DB 기준으로 다시 apply |
| CR은 있는데 DB에 없음 | 삭제 실패나 수동 생성으로 생긴 고아 resource | label 기준으로 조회해 관리자에게 보고 |
정규화된 상태 값은 backend가 정한다. 사용자에게 CrashLoopBackOff를 그대로 보여 주지 않고,
“시작 실패 — 모델 연결을 확인하세요” 같은 판단으로 바꾼다. 이 정규화 규칙은
11장 adapter에 정리한다.
10장 요약
섹션 제목: “10장 요약”- dashboard는 운영자 콘솔로 남기고 최종 사용자에게 열지 않는다.
- 관리 계약은 CRD·status, 호출 계약은 controller A2A다. dashboard 내부 API·CLI·Agent Pod Service는 쓰지 않는다.
- backend는 in-cluster ServiceAccount로 server-side apply하고 informer로 status를 받아 DB에 반영한다.
- 정체성·version·Grant·감사는 우리 DB가 원본이고, 실행 상태만 Kubernetes가 원본이다.
- 관리 경로는 ServiceAccount 대행으로, 호출 경로는 controller
8083ingress 제한으로 막아야 통제가 완성된다. - 부서 단위 namespace가 quota·정책·격리 범위를 함께 정리해 준다.
참고 자료
섹션 제목: “참고 자료”- kagent v0.9 release notes — OIDC 인증 지원과 access control 미구현 경계
- kagent API reference — backend가 생성할 CR의 schema
- kagent A2A 예제 — controller
8083의 Agent Card·task route - Kubernetes Server-Side Apply — field manager와 소유권 추적
- @kubernetes/client-node — Node 클라이언트의 apply·informer 예제
- CKA NetworkPolicy와 CNI — 기본 deny와 ingress 제한 작성법
- kagent 실습 — backend와 보안 경계 — Node probe와 controller 우회 차단 acceptance test