새 Python AI 마이크로서비스(threat-intel-ai-service — FastAPI + LangGraph + Qdrant, 별도 리포)를 GitHub 리포 생성부터 실제 클러스터에 트래픽이 흘르는 상태까지 올린 기록. target-tracking-service가 이미 깔아둔 CI/CD 관례(Docker Hub + GitHub Actions + ArgoCD Image Updater)를 그대로 재사용했고, 재사용 과정에서 이 파이프라인의 숨은 전제조건 하나를 새로 발견했다.
배포하기 전에 kubectl top nodes/kubectl describe node로 실측한 결과, k3s-worker1(1 core/1.45GB)은 이미 memory limit 96%까지 차 있어서 새 워크로드를 얹기엔 빨막았고, k3s-worker2(1 core/1.7GB)는 memory request 15%만 쓰는 중이라 여유가 있었다. k3s-master는 control-plane taint로 애초에 배제. 결론: 새 서비스는 worker2에 붙인다.
apps/threat-intel-ai-service/에 세 파일을 추가했다.
deployment.yaml — FastAPI 앱 (containerPort 8000)qdrant.yaml — Qdrant Deployment + PVC(local-path, 1Gi) + Service. postgres.yaml과 동일하게 strategy: Recreate(RWO 볼륨이라 롤링 업데이트 시 두 pod가 동시 마운트하면 안 됨)service.yaml — 앱용 ClusterIP Service (포트 8000)노드 이름을 직접 하드코딩하는 대신, kafka.yaml/target-tracking-service/deployment.yaml이 이미 쓰고 있던 것과 같은 패턴을 재사용했다:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- target-tracking-service
- postgres
- redis
topologyKey: kubernetes.io/hostname
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
worker1에 이미 떠 있는 세 앱(target-tracking-service, postgres, redis)과 같은 노드에는 못 뜵게 하는 required anti-affinity. master는 taint로 이미 막혀 있어서 이 두 조건을 합치면 남는 노드가 worker2뿐이라, 결과적으로 노드 이름을 하드코딩한 것과 같은 효과를 얻으면서도 "왜 이 노드인지"가 라벨 기반으로 명시적으로 드러난다.
env:
- name: GEMINI_API_KEY
valueFrom:
secretKeyRef: { name: target-tracking-secrets, key: gemini-api-key }
- name: KAFKA_BOOTSTRAP_SERVERS
valueFrom:
configMapKeyRef: { name: target-tracking-config, key: kafka-bootstrap-servers }
target-tracking-secrets, target-tracking-config는 target-tracking-service용으로 이미 만들어져 있던 것들이다. 새 Secret/ConfigMap을 또 만드는 대신 그대로 참조했다 — 어차피 같은 Gemini 계정, 같은 Kafka 브로커를 두 서비스가 공유하는 게 맞는 그림이라 값을 이중으로 관리할 이유가 없었다 (이 Secret은 GitOps 대상이 아니라 클러스터에 kubectl create secret으로 직접 만들어진 것 — git에는 안 올라간다).
실제로 push하기 전에 kubectl apply --dry-run=server -k apps/threat-intel-ai-service로 살아있는 클러스터의 API 서버에 대고 스키마 검증을 했다. 로컬 kubectl kustomize dry-run만으로는 CRD 존재 여부나 실제 API 버전 호환성까지는 못 잡아서, --dry-run=server로 한 번 더 확인하는 습관을 들이는 중이다.
argocd/applications/threat-intel-ai-service.yaml을 만들어서 커밋했는데, push만으로는 아무 일도 안 일어났다. 이 리포에는 app-of-apps 패턴이 없다 — 기존 target-tracking-service Application도 처음엔 누군가 kubectl apply -f로 수동 등록했을 것이고, defense-api-gateway.yaml도 리포엔 파일이 있지만 실제로는 kubectl get applications에 안 잡히는 상태였다 (교차 확인함).
kubectl apply -f argocd/applications/threat-intel-ai-service.yaml