새 Python AI 마이크로서비스(threat-intel-ai-service — FastAPI + LangGraph + Qdrant, 별도 리포)를 GitHub 리포 생성부터 실제 클러스터에 트래픽이 흘르는 상태까지 올린 기록. target-tracking-service가 이미 깔아둔 CI/CD 관례(Docker Hub + GitHub Actions + ArgoCD Image Updater)를 그대로 재사용했고, 재사용 과정에서 이 파이프라인의 숨은 전제조건 하나를 새로 발견했다.

1. 클러스터에 엎기 전 먼저 한 것 — 리소스 여유 확인

배포하기 전에 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에 붙인다.

2. 매니페스트 설계

apps/threat-intel-ai-service/에 세 파일을 추가했다.

2.1 worker2로 스케쥴을 강제한 방법

노드 이름을 직접 하드코딩하는 대신, 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뿐이라, 결과적으로 노드 이름을 하드코딩한 것과 같은 효과를 얻으면서도 "왜 이 노드인지"가 라벨 기반으로 명시적으로 드러난다.

2.2 시크릿/컴피그맵을 새로 안 만들고 기존 것 재사용

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에는 안 올라간다).

2.3 배포 전 검증

실제로 push하기 전에 kubectl apply --dry-run=server -k apps/threat-intel-ai-service로 살아있는 클러스터의 API 서버에 대고 스키마 검증을 했다. 로컬 kubectl kustomize dry-run만으로는 CRD 존재 여부나 실제 API 버전 호환성까지는 못 잡아서, --dry-run=server로 한 번 더 확인하는 습관을 들이는 중이다.

3. ArgoCD 등록 — "git에 Application yaml이 있다고 자동 등록되는 게 아니다"

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