[AI 시대에 개발자가 알아야 할 인프라 구성 배포 with 클로드코드]스터디 4주차

2026. 7. 26. 22:18·ETC
반응형

 

https://www.yes24.com/product/goods/191693950

스터디 3주차 까지는 캐시 서버 & 시크릿 & 카나리 배포 전략을 테스트했었는데 7장부터는 SMB에서 엔터프라이즈 급으로 규모가 확장되었을 때 어떤 것들을 고려해야하는지, 고도화를 위해서는 어떤 서비스를 추가해서 어떻게 운영해야하는지 실습해보도록 한다.

  • 1주차: https://canaryrelease.tistory.com/122     
  • 2주차: https://canaryrelease.tistory.com/123    
  • 3주차: https://canaryrelease.tistory.com/124

7장. 규모 확장

7.1 성장통: SMB 구조의 한계

지금까지 Notiflex는 단일 노드풀(default-pool)에 모든 워크로드가 섞여 있고, 테넌트는 smb 하나뿐이다.

 

관측 스택, ArgoCD, Valkey, 앱이 같은 노드를 두고 CPU를 경쟁한다. 6장에서 CSI/WI DaemonSet이 추가되며 파드가 13시간 Pending에 걸린 것도 이 구조의 한계였다.

 

대형 고객사를 받으려면 두 가지가 필요하다. 워크로드를 물리적으로 분리해 서로 영향을 주지 않게 하는 것(노드풀 분리), 그리고 고객사별로 환경을 격리하는 것(멀티테넌시). 7장은 이 둘을 인프라와 배포 구조 양쪽에서 해결한다.

 

7.2 워크로드별 노드 배치: 멀티 노드풀

클로드 코드에게 노드 분리 방법을 물으면 nodeSelector, Taint/Toleration, Node Affinity 세 가지를 제시한다. 실습에서는 이 중 역할별 노드풀 + nodeSelector 조합을 골랐다. Taint는 "이 노드에 아무나 못 오게" 막는 강한 격리이고, nodeSelector는 "이 워크로드를 이 노드로 보내는" 단순 지정이다. 실습 규모에서는 nodeSelector 한 줄이면 충분하고 설정도 간단하다.

 

노드풀은 아래 세 개를 만든다.

노드풀 머신 타입 용도
api-pool e2-medium API 워크로드(smb·enterprise)
worker-pool e2-standard-2 8.1 Kafka 등 무거운 워커
ops-pool e2-small 8.2 Tempo·8.3 CronJob 등 운영 도구

 

책 가이드와 다르게 진행한 부분: 가이드는 gcloud container node-pools create로 노드풀을 만들지만, 나는 앞서서 테라폼으로 클러스터와 노드를 생성했기 때문에 local에 node_pools 맵을 생성하고 terraform apply로 생성했다.
# terraform/gcp/gke/03-locals.tf
node_pools = {
  default-pool = { ... node_count = 3 ... }
  api-pool     = { machine_type = "e2-medium",   disk_type = "pd-standard", spot = true, workload_metadata = "GKE_METADATA" }
  worker-pool  = { machine_type = "e2-standard-2", ... }
  ops-pool     = { machine_type = "e2-small",     ... }
}

 

테라폼으로 작업 시 주의점이 몇 가지 있었다. 모든 노드풀에 workload_metadata = "GKE_METADATA"를 넣어야 한다. 6.2에서 Workload Identity를 켰기 때문에, 이 옵션이 없으면 해당 노드의 파드가 GCP Secret Manager에 접근하지 못한다.

 

그리고 디스크는 기본값인 pd-ssd를 쓰면 리전당 SSD 쿼터(300GB)를 초과할 수 있어 pd-standard(HDD)로 지정했다.

 

배치는 nodeSelector로 지정한다. 키는 GKE가 자동으로 붙이는 cloud.google.com/gke-nodepool만 쓴다.

# k8s/smb/manifests/rollout.yaml
spec:
  template:
    spec:
      nodeSelector:
        cloud.google.com/gke-nodepool: api-pool

 

role: api나 workload: api 같은 커스텀 키를 쓰면, 노드에 그 라벨이 없어서 파드가 영구 Pending에 빠진다. GKE 자동 라벨만 쓰면 안전하다. 배포 후 확인하니 smb와 enterprise 파드가 모두 api-pool 노드로 갔다.

$ kubectl get pods -A -o wide | grep notiflex-api
enterprise  notiflex-api-...  gke-notiflex-cluster-api-pool-0ebdaf95-46r8
notiflex    notiflex-api-...  gke-notiflex-cluster-api-pool-0ebdaf95-46r8

 

7.3 다수 앱 관리: App of Apps 패턴 + Sync Wave

앱이 늘어나면 ArgoCD Application을 하나씩 만들고 관리하는 게 번거로워진다. App of Apps 패턴은 부모 Application 하나가 자식 Application들을 자동으로 관리하게 한다. 클로드 코드는 이 패턴과 ApplicationSet을 함께 제시하는데, 이미 6장에서 App of Apps 구조로 재편해둔 상태였다.

 

우리 구조는 k8s/<app>/application.yaml에 각 앱의 Application CR을 두고, 부모 root-app이 이들을 include로 수집한다. 자식 CR은 k8s/<app>/에 있고 실제 리소스는 manifests/ 하위에 있어 자기참조가 생기지 않는다.

 

7.3에서 새로 더한 것은 Sync Wave다. App of Apps가 동작하면 자식 앱들이 한꺼번에 sync되는데, 의존성이 있으면 순서가 필요하다. Argo Rollouts 컨트롤러(CRD)가 먼저 있어야 smb의 Rollout이 배포되고, 관측 스택이 앱보다 먼저 떠야 자연스럽다. 각 Application에 sync-wave 어노테이션을 붙여 순서를 지정했다.

metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "0"   # argo-rollouts (CRD)
    # "1" 관측 스택(kube-prometheus/loki/fluent-bit)
    # "2" 앱(smb/enterprise/valkey)

숫자가 낮은 wave부터 sync되고, 앞 wave가 Healthy가 되어야 다음 wave가 시작된다.

 

적용 후 확인하니 아래처럼 wave가 앱별로 정확히 붙었다.

$ kubectl get app -n argocd
notiflex-argo-rollouts     0   Synced   Healthy
notiflex-kube-prometheus   1   Synced   Healthy
notiflex-loki              1   Synced   Healthy
notiflex-smb               2   Synced   Healthy
notiflex-enterprise        2   Synced   Healthy

 

ArgoCD UI의 notiflex-root 앱을 두고 부모 하나가 자식 Application 7개(smb, enterprise, valkey, argo-rollouts, loki, fluent-bit, kube-prometheus)를 트리로 관리한다.

 

7.4 멀티 테넌시: 네임스페이스 격리

멀티테넌시 방법으로 클로드 코드는 Namespace 분리, 단일 namespace 라벨 격리, vCluster를 제시한다. 실습에서는 Namespace 분리 + 테넌트별 Rollout을 골랐다. 네임스페이스로 나누면 RBAC, 리소스, 네트워크 경계가 자연스럽게 생기고, App of Apps와도 잘 결합되기 때문.

 

enterprise 테넌트를 위해 k8s/enterprise/를 만들었다. smb 매니페스트를 복제하되 네임스페이스만 enterprise로 바꾸고, api-pool에 배치하고, ArgoCD Application을 root-app이 관리하도록 했다.

 

[주의 사항]
1. Valkey가 notiflex 네임스페이스에 있어 enterprise 앱은 cross-namespace로 접근해야 해 FQDN(valkey-primary.notiflex.svc.cluster.local:6379)을 써야 하고, 이걸 빠뜨리면 localhost로 붙으려다 CrashLoopBackOff에 빠진다.
2. 책 가이드는 enterprise에 valkey-secret.yaml로 비번을 하드코딩하고 "실제 Valkey 비번과 반드시 일치시켜라"라고 경고한다. 하지만 앞선 실습에서 Google Secret Manager 를 사용하도록 구성했기 때문에 enterprise도 같은 GSM 파일을 CSI로 읽게 했다. 하드코딩이 없으니 불일치가 원천적으로 불가능하다. 나의 경우 enterprise 전용 KSA(enterprise-api)에 GSM 접근 권한을 아래처럼 Terraform으로 부여했다.
# k8s/enterprise/manifests/rollout.yaml (핵심만)
spec:
  template:
    spec:
      serviceAccountName: enterprise-api
      nodeSelector:
        cloud.google.com/gke-nodepool: api-pool
      containers:
        - env:
            - name: VALKEY_ADDR
              value: valkey-primary.notiflex.svc.cluster.local:6379   # cross-ns FQDN
            - name: VALKEY_PASSWORD_FILE
              value: /mnt/secrets/valkey-password                     # GSM CSI 파일

 

 

배포 후 enterprise 테넌트에서 /id를 호출하니 정상 응답했다. notiflex 테넌트와 같은 Valkey 카운터를 공유하므로, 두 테넌트를 통틀어 전역 순차 ID가 유지된다.

$ curl http://notiflex-api.enterprise.svc.cluster.local/id
{"generated_by":"notiflex-api-...","id":5}

 

전체 8개 앱(root + 자식 7개)이 Synced/Healthy로 수렴했고, enterprise가 정식 테넌트로 편입됐다.

enterprise 테넌트의 리소스 트리. 별도 네임스페이스에 Rollout·Service·SecretProviderClass·전용 KSA가 배포됨. Valkey는 notiflex 네임스페이스의 것을 cross-namespace로 공유함

 

7.5 마무리: settings.local.json으로 권한 분리 체험

7장 마무리는 AI에게 권한 경계를 설정하는 체험인데 지금까지는 CLAUDE.md의 자연어 규칙("리소스 삭제 시 확인받아라")으로 AI 행동을 유도했는데, 자연어는 강제력이 없기 때문에 .claude/settings.local.json을 사용한다. 이 설정은 harness가 직접 강제하는 권한 경계이고 자연어 규칙과 달리 AI가 우회할 수 없다.

{
  "permissions": {
    "deny": ["Bash(kubectl delete *)", "Bash(kubectl apply *)"],
    "ask":  ["Bash(gcloud container node-pools delete *)"]
  }
}

deny에 걸린 명령은 AI가 아예 실행하지 못하고, ask에 걸린 명령은 실행 전 사용자 승인을 요구한다. deny로 kubectl delete/apply를 막으면 클러스터 직접 변경이 차단되어 GitOps 경유가 강제되고, ask로 노드풀 삭제를 걸면 비용·파괴적 작업에 승인 단계가 생긴다. 실무에서 위험 작업을 확실히 막으려면 자연어 규칙만으로는 부족하고, harness 레벨의 권한 경계가 필요하다.

 

8장. 고도화

8.1 이벤트 드리븐: 카프카

지금까지 Notiflex의 /id는 요청이 오면 그 자리에서 ID를 만들어 응답하는 동기 처리 형식의 API 였다. 트래픽이 몰리면 후처리(알림 발송, 집계 등)까지 요청 안에서 모두 처리하기 때문에 처리 속도가 느려진다. 이벤트를 큐에 넣고 비동기로 처리하면 요청은 빨리 응답하고 후처리는 따로 돌 수 있다.

 

클로드 코드에게 메시지 큐를 물으면 Kafka, RabbitMQ, NATS, Redis Streams를 제시하는데 실습에서는 Kafka(Strimzi)를 선택했다. 이벤트 드리븐의 사실상 표준이라 학습 가치가 높고, Strimzi operator가 Kafka를 CRD로 선언해 ArgoCD로 관리할 수 있다. 또한, KRaft 모드로 ZooKeeper 없이 단일 브로커를 돌릴 수 있어 실습 환경에도 맞는다. Redis Streams는 이미 Valkey가 있어 추가 설치가 없다는 장점이 있지만 전용 브로커 대비 기능이 제한적이고, NATS는 더 가볍지만 업계 채택률이 낮아 학습 투자 대비 효과가 떨어진다.

 

Strimzi operator는 ArgoCD Application(Helm 차트 1.1.0, 카프카 버전 4.3.0)으로, Kafka 클러스터/토픽은 순수 매니페스트로 만들어 worker-pool에 배치했다. Kafka는 KRaft 단일 브로커(broker+controller)로 설치했다.

# k8s/kafka/manifests/kafka-cluster.yaml (핵심만)
apiVersion: kafka.strimzi.io/v1
kind: Kafka
metadata:
  annotations:
    strimzi.io/kraft: "enabled"
    strimzi.io/node-pools: "enabled"
spec:
  kafka:
    version: "4.3.0"
    metadataVersion: "14.0"

 

앱에서는 Producer/Consumer를 추가했다. /id 요청 시 생성된 ID를 notifications 토픽에 발행하고, 백그라운드 goroutine이 그 토픽을 구독해 로그로 출력한다. 라이브러리는 sarama v1.60.0(Kafka 4.x 상수 지원)를 사용했는데 /id를 호출해도 Consumer가 메시지를 못 받았는 문제가 있었다. 토픽이 partitions 3인데 Consumer가 파티션 0만 구독했고, sarama Producer는 메시지를 파티션 1과 2에 보냈던 것이다.

notifications:0:0   ← 파티션 0은 비어있음
notifications:1:2
notifications:2:1

 

consumer.Partitions()로 전체 파티션 목록을 받아 각각 goroutine으로 구독하게 고치니 정상 수신됐다.

kafka consumer 시작: topic=notifications partitions=[0 1 2]
kafka 수신: {"generated_by":"notiflex-api-...","id":9}
kafka 수신: {"generated_by":"notiflex-api-...","id":10}

 

8.2 분산 트레이싱: 템포

Kafka로 흐름이 비동기가 되면 "이 요청이 어디를 거쳐 어디서 느려졌는가"를 추적하기 어려워진다. 분산 트레이싱은 요청 하나가 만드는 여러 span(작업 단위)을 묶어 흐름을 보여준다.

 

클로드 코드는 Tempo, Jaeger, Zipkin을 제시한다. 실습에서는 Grafana Tempo를 선택했는데, 4장에서 이미 Grafana를 운영하고 있으니 메트릭 / 로그에 이어 트레이스까지 한 UI에서 연결된다. Jaeger는 독립 UI가 장점이지만 이미 Grafana가 있는데 또 다른 UI를 띄우는 건 불필요하다. 참고로 Tempo는 단일 바이너리 모드로 가볍고 OTLP를 기본 지원한다.

 

Tempo는 ArgoCD Application(grafana/tempo 1.24.4)으로 ops-pool에 배치하고 OTLP gRPC receiver(4317)를 켰다. Grafana에는 Tempo datasource를 ConfigMap으로 등록했다(isDefault: false / Prometheus가 이미 default). 4장에서 Loki를 false로 넣어둔 덕에 default 충돌은 없었다.

 

앱에는 OpenTelemetry Go SDK를 붙였다. /id 핸들러 전체를 span으로 감싸고, 그 안의 Valkey INCR과 Kafka 발행을 하위 span으로 만들었다. TracerProvider가 OTLP gRPC로 트레이스를 Tempo에 보낸다.

ctx, span := tracer.Start(r.Context(), "idHandler")
defer span.End()
// valkey.incr, kafka.publish가 하위 span으로 이어진다

 

여기서 걸린 건 서비스 이름이었다. helm releaseName이 ArgoCD Application 이름을 따라가 Tempo 서비스가 tempo가 아니라 notiflex-tempo로 떴다. datasource URL과 앱의 OTEL_EXPORTER_OTLP_ENDPOINT를 notiflex-tempo.monitoring.svc.cluster.local로 수정했다.

 

/id를 호출하고 Grafana Explore에서 Tempo를 조회하니 notiflex-api의 트레이스가 나왔다.

Grafana Explore의 Tempo datasource. notiflex-api의 idHandler 트레이스가 2~17ms 지속시간과 함께 나열된다. 메트릭·로그와 같은 UI에서 조회된다.

 

트레이스 하나를 열면 idHandler 아래 valkey.incr·kafka.publish span이 시간축으로 펼쳐진다. 요청 하나가 거친 작업 흐름이 그대로 보인다.

 

8.3 배치 자동화: 크론잡

주기적으로 돌아야 하는 작업(헬스체크, 정리 배치 등)은 CronJob으로 만들면 편리하다. 외부 cron이나 Argo Workflows도 있지만, 쿠버네티스 네이티브라 별도 스케줄러 없이 클러스터가 관리하고 ArgoCD 매니페스트로 선언할 수 있다.

 

5분마다 notiflex-api의 /health를 호출하는 헬스체크 CronJob을 만들어 ops-pool에 배치했다. 200이면 성공 로그, 그 외엔 exit 1로 실패 처리한다. history는 3개만 유지한다.

spec:
  schedule: "*/5 * * * *"
  successfulJobsHistoryLimit: 3
  jobTemplate:
    spec:
      template:
        spec:
          nodeSelector:
            cloud.google.com/gke-nodepool: ops-pool

 

이 CronJob은 smb 앱의 manifests에 파일 하나 추가한 것이라 App of Apps가 자동으로 관리한다(root-app include 변경 불필요). 5분마다 Job이 생성돼 완료됐다.

$ kubectl get jobs -n notiflex
notiflex-healthcheck-29748095   Complete   1/1   12s
notiflex-healthcheck-29748100   Complete   1/1   6s
notiflex-healthcheck-29748105   Complete   1/1   6s
$ kubectl logs job/notiflex-healthcheck-... -n notiflex
healthcheck OK (200)

 

 

8.4 마무리: command-guardrails/로 위험 작업 절차 정리

시스템이 복잡해지면 위험한 작업도 늘어난다. Kafka 토픽 삭제, CronJob 수동 실행, 테넌트 Namespace 삭제 같은 것들이다. 이런 작업을 매번 즉흥적으로 하면 실수가 나온다. command-guardrails/에 "사전 확인 → 실행 → 사후 검증" 3단 절차서를 만들어 git에 누적한다.

 

7장의 settings.local.json과 대비된다. settings.local.json이 harness가 명령을 강제로 차단하는 실행 통제라면, command-guardrails는 어떻게 안전하게 작업할지를 담은 누적 지식이다. 

 

참고로 이 절차서는 우리 구조에 맞게 조정한 부분이 있다. 예를 들어 테넌트 Namespace 삭제 절차는 kubectl delete 대신 root-app의 include 목록에서 테넌트를 빼고 git push하는 GitOps 경유를 명시했고(selfHeal 충돌 방지), GSM IAM 바인딩을 Terraform에서 정리하는 단계도 넣었다. 절차서가 실제 우리 아키텍처를 반영해야 쓸모가 있다.

command-guardrails/
├── kafka-topic-delete.md
├── cronjob-manual-run.md
└── tenant-namespace-delete.md

이후 이런 작업을 요청하면 클로드 코드가 해당 절차서를 참조해 사전 확인부터 순서대로 안내하게 된다.

 

9장. GitAIOps, 살아있는 운영 표준의 탄생

그래서 GitAIOps란 무엇인가. Git, AI, Ops 세 요소가 각자의 역할로 맞물린 것이다.

  • Git: 코드·매니페스트·Terraform·JOURNEY·ADR이 한 저장소에 누적. 의사결정이 코드와 같이 버전 관리된다.
  • AI: 자연어 요청을 받아 옵션 제시·코드 생성·문제 진단을 하고, 그 결정을 JOURNEY에 기록한다.
  • Ops: ArgoCD가 Git 상태를 클러스터에 동기화. 사람이 kubectl로 직접 안 바꾼다.

핵심은 이게 일회성 결합이 아니라 루프라는 점이다.

자연어 요청
   ↓
[탐색]→[비교]→[실행]
   ↓      ↓       ↓
JOURNEY  ADR   Git push → ArgoCD sync → 클러스터
   ↓      ↓       ↓
   CLAUDE.md·claude-context·command-guardrails 누적
   ↓
다음 요청 시 AI가 누적 자산 자동 참조 (돌수록 빨라진다)

한 사이클이 다음 사이클을 강화한다. 첫 사이클은 탐색·비교에 시간이 걸리지만, 두 번째부터 AI가 이전 결정과 규칙을 알고 있어 곧장 실행으로 간다. 책 전반의 "탐색 → 비교 → 실행" 3-프롬프트 패턴이 바로 이 루프 한 바퀴이고, 가드레일이 쌓일수록 탐색·비교가 압축되고 실행만 남는다.

 

GitOps와의 차이가 여기 있다. GitOps는 "Git이 진실의 원천, 자동 동기화"까지다. GitAIOps는 그 루프에 AI가 들어가, 무엇을 Git에 넣을지(결정)와 그 결정의 기록(문서)까지 루프 안에서 생성·누적한다. Ops가 자동화되는 걸 넘어, 결정과 지식이 자동으로 축적되는 것이다.

작위적으로 들릴 수 있지만, 실제로 이 저장소를 만든 방식이 정확히 이랬다. 매 챕터가 자연어 요청 → AI 실행 → Git 커밋 → ArgoCD 반영 → JOURNEY 기록의 반복이었고, 진행할수록 같은 작업이 빨라졌다.

 

9.5 마무리: 다음 단계

책에서는 프로덕션으로 가려면 다음과 같은 추가 영역을 검토하는 것을 제안한다. 실제로 지금 회사에서 적용하고 있는 것들도 있고 적용을 검토하는 부분들도 있는데 기왕 구글 크레딧이 남은 김에 아래 내용을 포함해 내가 테스트 해보고 싶은 것들도 남는 시간에 하나씩 찬찬히 테스트 해볼 생각이다.

  1. 보안 강화: NetworkPolicy로 namespace 간 트래픽 제한, RBAC을 SA 단위로 축소, OPA/Kyverno 정책 강제. (지금은 변경 통제는 있으나 런타임 네트워크 격리는 없음)
  2. 스케일링: HPA, Cluster Autoscaler, KEDA(Kafka 큐 기반). (지금은 replicas 고정)
  3. 비용 최적화: requests 정밀화, Autopilot 검토, Budget Alert. (Spot은 이미 전면 적용)
  4. 멀티 클러스터/리전: ApplicationSet 다중 sync, 리전 분리. (지금은 단일 Zonal)
  5. 관측 심화: SLO/SLI, Burn Rate 알림, 트레이스 샘플링(지금은 전량 수집).
저작자표시 비영리 동일조건 (새창열림)

'ETC' 카테고리의 다른 글

Hands-On LLM Serving and Optimization 책 스터디 2주차  (0) 2026.08.16
Hands-On LLM Serving and Optimization 책 스터디 1주차  (1) 2026.08.02
[AI 시대에 개발자가 알아야 할 인프라 구성 배포 with 클로드코드]스터디 3주차  (0) 2026.07.20
AI 시대에 개발자가 알아야 할 인프라 구성 배포 with 클로드코드 스터디: CH03-04  (2) 2026.07.12
[AI 시대에 개발자가 알아야 할 인프라 구성 배포 with 클로드코드]스터디 1주차  (0) 2026.07.05
'ETC' 카테고리의 다른 글
  • Hands-On LLM Serving and Optimization 책 스터디 2주차
  • Hands-On LLM Serving and Optimization 책 스터디 1주차
  • [AI 시대에 개발자가 알아야 할 인프라 구성 배포 with 클로드코드]스터디 3주차
  • AI 시대에 개발자가 알아야 할 인프라 구성 배포 with 클로드코드 스터디: CH03-04
Canary_카나리아
Canary_카나리아
어쩌다 데브옵스 엔지니어가 된 문과생의 기록용 블로그
    반응형
  • Canary_카나리아
    release: canary
    Canary_카나리아
  • 전체
    오늘
    어제
    • 분류 전체보기 (126)
      • DevOps (67)
      • Programming (28)
      • Certifications (3)
      • ETC (26)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    백엔드공부
    Grafana
    클라우드자격증
    클로드코드
    Prometheus
    AIOps
    Java
    백엔드스쿨
    GitAIOps
    제로베이스
    AI
    백엔드
    네이버클라우드서비스
    그라파나예제
    terraform
    클라우드엔지니어자격증
    java개념
    ncp서비스
    한빛출판네트워크
    자바개념
    바이브코딩
    AI 시대에 개발자가 알아야 할 인프라 구성 배포
    백엔드개발
    kops사용법
    kops란
    프로메테우스예제
    프로메테우스
    argocd
    개발자
    spring
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
Canary_카나리아
[AI 시대에 개발자가 알아야 할 인프라 구성 배포 with 클로드코드]스터디 4주차
상단으로

티스토리툴바