Skip to content

Kubernetes

donghoong2 edited this page Oct 28, 2026 · 3 revisions

Kubernetes

develop 04c0e10 (2026-10-27) · 클러스터 기준 2026-10-14 기록

요약

  • dev 환경의 앱을 EC2 위 docker compose에서 kubeadm으로 직접 세운 Kubernetes 클러스터(CNI는 Cilium)로 옮겼다. prod는 아직 compose다.
  • 배포는 Argo CD가 매니페스트 레포를 보고 맞추는 GitOps 방식이고, 비밀값은 Secrets Manager에서 External Secrets Operator(ESO)가 가져온다.
  • 운영 중 확인 절차와 장애 대응은 Kubernetes 런북에 따로 모았다.

이 페이지는 클러스터를 어떻게 구성했고 배포가 어떻게 흐르는지, 그리고 왜 그렇게 정했는지를 정리한다. 클러스터 매니페스트는 별도 private 레포에 있어서, 여기서는 구조와 판단 근거 위주로 적는다.

Note

지금 Kubernetes로 운영하는 건 dev 환경뿐이다. prod는 아직 EC2 + docker compose(deploy/prod/)로 배포하고, prod 클러스터는 따로 세울 계획이다.

한눈에 보기

flowchart TB
    USER([클라이언트]) --> ALB[ALB<br/>TLS 종료]
    subgraph K8S[kubeadm 클러스터 · Cilium eBPF]
        CP[control-plane<br/>단일 노드 · etcd 암호화 · 일일 스냅샷]
        subgraph APPNODE[앱 워커]
            TR[Traefik<br/>NodePort]
            BE[backend<br/>8080 / 9292]
            RP[report 워커<br/>SQS 소비]
            RD[(Redis<br/>StatefulSet)]
            PLAT[Argo CD · ESO · Reloader<br/>모니터링 에이전트]
        end
        subgraph GPUNODE[GPU 워커 · taint로 격리]
            ETL[약관 ETL 워크로드<br/>Ollama · vLLM · chunker]
        end
    end
    subgraph AWS[클러스터 밖 · AWS 관리형]
        RDS[(RDS PostgreSQL)]
        SQS[SQS]
        SM[Secrets Manager]
        ECR[ECR]
    end
    MON[중앙 모니터링 EC2<br/>Prometheus · Grafana · Loki · Tempo]

    ALB --> TR
    TR -->|/actuator| BE
    TR -->|그 외| BE
    BE --> RD
    BE --> RDS
    RP --> RDS
    RP --> SQS
    SM -->|ESO 동기화| PLAT
    ECR --> APPNODE
    PLAT -->|remote_write · 로그| MON
Loading

cilium status 출력: Cilium·Operator·Envoy·Hubble Relay가 모두 OK이고 클러스터 파드 19개를 Cilium이 관리하는 모습

컨트롤 플레인에서 본 cilium status. agent·operator·Hubble이 모두 OK이고, 클러스터의 파드 전부를 Cilium이 관리한다

왜 필요했나

docker compose에서 옮긴 이유

compose 시절에는 EC2 한 대에 backend·AI 워커·Redis·수집 에이전트가 같이 떠 있었다. 배포는 CI가 SSM으로 접속해 docker compose up을 실행하는 방식이었다.

Kubernetes 이전 AWS 구성: VPC 프라이빗 서브넷에 frontend(Next.js), backend·report-worker, ollama·ocr-worker, grafana 인스턴스가 있고 RDS PostgreSQL·SQS·S3·ECR, ALB·NAT·IGW·Route53, Discord 알림이 붙어 있다

Kubernetes 이전(docker compose 시절) AWS 구성. 인스턴스마다 compose로 컨테이너를 띄웠고, DB·큐·스토리지는 처음부터 관리형이었다

이 구조에서 걸리던 점은 다음과 같다.

  • 서버를 손으로 만지는 단계가 남아 있었다. 환경변수를 바꾸려면 서버의 env 파일을 직접 고쳐야 했고, 그 변경은 어디에도 이력이 남지 않았다.
  • CI가 서버 접근 권한을 들고 있어야 했다. 밖에서 명령을 밀어 넣는 push 방식이었기 때문이다.
  • 확장 단위가 인스턴스였다. 서비스를 늘리려면 인스턴스와 compose 파일이 같이 늘어난다.
  • 팀이 Kubernetes를 실전에서 다뤄 보고 싶었다. 학습 자체도 목표였다.

관리형 EKS는 컨트롤 플레인 고정비(월 약 $73)가 이 규모에선 부담이라 뺐다. 대신 기존 EC2 위에 클러스터를 직접 세웠다.

k3s에서 kubeadm으로 바꾼 이유

처음에는 경량 배포판인 k3s로 클러스터를 세웠다. 레포의 이전 마이그레이션 문서가 k3s를 전제로 쓰인 것도 그 때문이다. 컷오버 전에 kubeadm으로 다시 설치했고, 이유는 두 가지였다.

  • HA 전환 비용. k3s 단일 서버(SQLite)는 나중에 컨트롤 플레인을 늘리려면 사실상 다시 세워야 한다. kubeadm은 처음에 controlPlaneEndpoint만 잡아 두면 컨트롤 플레인 노드 추가가 문서화된 join 절차다.
  • 학습 깊이. k3s는 etcd·CNI·인증서·컨트롤 플레인 부팅을 감춘다. kubeadm은 이것을 전부 직접 조립하게 만든다.

실데이터가 올라가기 전이라 갈아타는 비용이 가장 싼 시점이었다. 소프트웨어 비용 차이는 없고, 대신 CNI·인증서·etcd 백업을 직접 챙겨야 하는 운영 부담을 졌다.

어떻게 동작하나

클러스터 구성

항목 구성 왜
부트스트랩 kubeadm, 버전 고정 + 패키지 hold 업그레이드는 계획된 시점에만 한다
컨트롤 플레인 전용 인스턴스 1대, 기본 taint로 앱 파드는 올리지 않는다 모니터링 노드와 합치면 장애 때 관측까지 같이 잃는다
워커 앱 워커 1대(구 compose 호스트에서 Docker를 걷어 내고 조인), GPU 워커 기존 인스턴스를 재활용했다
CNI Cilium(eBPF, kube-proxy 대체, Hubble) 아래 "CNI 변천" 참고
Ingress Traefik(NodePort) ALB 타깃 그룹을 서비스마다 만들지 않으려고
스토리지 local-path-provisioner kubeadm엔 기본 StorageClass가 없다
시크릿 at-rest etcd EncryptionConfiguration(aescbc) 설치 전에 켰다. 나중에 켜려다 k3s에서 세 번 실패했다
접속 노드 접근은 SSM Session Manager 전용 인바운드 SSH는 열지 않았다

CNI 변천: Flannel → Calico → Cilium

시기 CNI 바꾼 이유
kubeadm 초기 Flannel k3s 때와 같은 VXLAN이라 보안그룹을 그대로 쓸 수 있었다
이후 Calico(eBPF 데이터플레인) Flannel은 NetworkPolicy를 지원하지 않는다(적용해도 조용히 무시된다). dev/prod 네트워크 격리가 요건이 되면서 탈락했다
현재 Cilium(eBPF, kube-proxy 대체) Hubble로 플로우 단위 네트워크 관측이 필요해졌다

전환 과정과 부작용은 사례 기록 CNI를 Flannel에서 Calico를 거쳐 Cilium으로 전환에 있다.

Warning

CNI를 바꿀 때마다 노드 간 보안그룹 포트가 달라진다(VXLAN 포트, 헬스체크, 메트릭 포트). kube-proxy DaemonSet은 지우지 않고 스케줄만 막아 두었다. 롤백을 한 줄로 되돌리기 위해서다.

노드 배치와 GPU 노드

GPU 노드에는 taint를 걸어 일반 파드가 올라가지 않게 하고, GPU 워크로드만 toleration과 nodeSelector로 배치한다. 노드를 등록할 때 라벨과 taint를 kubelet 인자로 넣어 두어서, 노드가 다시 등록돼도 같은 값이 붙는다.

# GPU 워크로드 쪽 (개념 예시)
nodeSelector:
  workload: etl
tolerations:
  - key: workload
    value: etl
    effect: NoSchedule
resources:
  limits:
    nvidia.com/gpu: 1

nvidia-smi 출력: NVIDIA L4 GPU 한 장, 드라이버와 CUDA 버전, VRAM 약 23GB가 잡힌 모습

GPU 인스턴스에서 드라이버·CUDA가 제대로 잡혔는지 확인한 nvidia-smi 출력(초기 GPU 검증 당시). 노드가 여기까지 돼야 device plugin이 GPU를 클러스터에 알려 줄 수 있다

  • Kubernetes는 GPU를 기본으로 모른다. NVIDIA device plugin(DaemonSet)과 containerd의 nvidia 런타임 등록이 있어야 nvidia.com/gpu 리소스가 잡힌다. kubeadm에선 이것을 전부 손으로 해야 하고, 가장 흔한 실패 지점이다.
  • GPU 한 장을 나눠 쓴다. device plugin의 time-slicing으로 나누는데 VRAM은 격리되지 않아서, 워크로드끼리 메모리 몫을 직접 정해야 한다.
  • GPU 인스턴스는 상시 가동하지 않는다. 같은 인스턴스를 껐다 켜는 방식(조인은 한 번, 이후엔 stop/start)을 택했고, 모델 캐시는 EBS 기반 볼륨에 둔다(인스턴스 스토어는 stop하면 사라진다). 꺼진 동안 파드가 Pending으로 보이는 건 정상이다.
  • 자동 프로비저닝은 검토만 했다. Karpenter는 EKS 전용이고, Cluster Autoscaler는 매번 새 볼륨이라 모델을 다시 받아야 해서 이 규모엔 맞지 않았다.

워크로드 배치

구분 워크로드 비고
앱 backend(Spring Boot) Deployment. 8080 API, 9292 actuator
앱 report 워커(FastAPI, AI 레포) HTTP 서버 없는 SQS 소비자라 Service·프로브 없이 Deployment만 있다
앱 chatbot(AI 레포) 앱 코드가 아직 없어서 replica 0이다
데이터 Redis Bitnami 차트 StatefulSet, local-path PVC
ETL 약관 chunker, corpus 워커, Ollama, vLLM GPU 노드에 고정
플랫폼 Argo CD, Traefik, External Secrets, Reloader 각자 네임스페이스
플랫폼 Cilium, kube-prometheus-stack, Alloy, NVIDIA device plugin 관측 데이터는 중앙 모니터링 EC2로 보낸다

앱과 Redis는 아직 default 네임스페이스에 함께 있고, 인프라 컴포넌트만 각자 네임스페이스를 쓴다. 아직 나눌 이유가 없어서이고, 서비스가 늘면 나눈다.

DB(RDS)·S3·SQS는 계속 AWS 관리형으로 둔다. PII를 다루는 서비스라 자체 호스팅하면 백업·내구성 리스크가 절감 효과보다 크다고 봤다.

중앙 모니터링 스택은 클러스터 밖 EC2에 그대로 있다. 클러스터 안의 kube-prometheus-stack이 메트릭을 모아 중앙 Prometheus로 remote_write하고, Alloy DaemonSet이 파드 로그를 Loki로 보낸다.

설정과 시크릿

원칙은 하나다. 비밀값은 레포에 두지 않고, 비밀이 아닌 설정은 레포에 둔다.

  • ConfigMap: DB 호스트, 큐 이름, AI 서버 주소처럼 비밀이 아닌 값이다. 매니페스트 레포의 환경별 values에 있다.
  • Secret: 비밀번호, 서명 키, 암호화 키다. 원본은 AWS Secrets Manager에 있고, External Secrets Operator(ESO) 가 주기적으로 읽어 k8s Secret으로 만든다. 레포에는 "어떤 시크릿의 어떤 키를 가져올지"만 적는다.

깔끔해서 나눈 게 아니다. Secret만 etcd 암호화 대상이 되고 RBAC도 따로 걸 수 있어서다.

값이 바뀌었을 때 파드까지 반영되는 길은 두 단계라 헷갈리기 쉽다.

flowchart LR
    SM[Secrets Manager] -->|1. ESO 폴링<br/>실시간 아님| KS[k8s Secret]
    VAL[매니페스트 레포 values] -->|Argo CD Sync| CM[ConfigMap]
    KS -->|2. Reloader가 롤링 재시작| POD[파드]
    CM -->|2. 내용 해시 어노테이션이 바뀌어 롤링 재시작| POD
Loading
  1. Secrets Manager → k8s Secret: ESO가 폴링한다(실시간 아님). 급하면 ExternalSecret의 어노테이션을 바꿔 바로 동기화한다(명령은 런북에 있다).
  2. k8s Secret/ConfigMap → 파드: 파드는 시작할 때 읽은 값을 계속 쓴다. ConfigMap은 차트가 내용 해시를 파드 어노테이션으로 붙여 두어서 바뀌면 자동으로 롤링 재시작되고, Secret은 Reloader가 재시작을 건다.

Important

새 비밀값은 값을 먼저, 코드를 나중에 넣는다. Secrets Manager에 키 추가 → ExternalSecret에 키 추가 → 차트에서 참조 순서다. 코드가 먼저 나가면 새 파드가 값을 못 찾아 바로 죽는다.

ECR 이미지 pull 토큰은 12시간마다 만료된다. EKS가 아니라서 자동 갱신 플러그인이 없고, 그래서 ESO의 ECR 토큰 생성기로 주기적으로 다시 발급한다. 이 Secret을 손으로 만들면 덮어써진다.

AWS 자격증명은 파드 단위로 나뉘지 않고 노드의 EC2 인스턴스 롤을 그대로 쓴다. EKS의 IRSA 같은 파드 단위 권한 분리가 없어서, 같은 노드의 파드는 같은 권한을 가진다. 새 워커를 추가하면 같은 권한의 롤을 붙여야 한다.

배포 파이프라인 (GitOps)

배포는 Argo CD가 맡는다. CI는 이미지를 만들고 매니페스트 레포의 이미지 태그만 커밋한다. 클러스터에 쓰는 주체는 클러스터 안의 Argo CD 하나뿐이라, CI는 클러스터 자격증명을 들고 있지 않다.

sequenceDiagram
    participant Dev as 개발자
    participant CI as GitHub Actions
    participant ECR as ECR
    participant MF as 매니페스트 레포
    participant Argo as Argo CD
    participant API as dev API
    Dev->>CI: develop 머지
    CI->>ECR: 이미지 빌드·push (태그 = 커밋 해시)
    CI->>MF: dev values의 이미지 태그 커밋
    Argo->>MF: 주기적으로 확인
    Argo->>Argo: 새 파드 readiness 통과 후 옛 파드 종료
    CI->>API: /actuator/info의 app.commit이 바뀌었는지 폴링
    CI->>Dev: Discord로 배포 완료·실패 알림
Loading
  • 레포를 나눴다. 소스 레포는 "어떻게 빌드하나"(Dockerfile, application.yml의 자리표시자)를, 매니페스트 레포는 "어떤 값으로 어떻게 돌리나"(차트, 환경별 values, Argo CD Application)를 가진다. 매니페스트가 소스 레포에 있으면 태그 갱신 커밋이 다시 CI를 돌린다. 이 레포에 k8s/ 같은 디렉터리가 없는 것도 같은 이유다.
  • 환경은 브랜치가 아니라 폴더로 나눈다. 매니페스트 레포는 main 하나만 쓰고 dev/prod는 디렉터리로 나눈다. 환경별 브랜치는 승격할 때 머지가 꼬인다.
  • 이미지 태그는 커밋 해시다. 덮어쓰는 dev 태그를 쓰면 매니페스트가 한 글자도 안 바뀌어 Argo CD가 아무것도 하지 않는다. 해시가 숫자로만 되면 YAML이 숫자로 읽으니 항상 따옴표로 감싼다.
  • app-of-apps 구조다. 클러스터에는 root Application 하나만 손으로 넣고, 나머지 Application은 레포의 디렉터리에서 생긴다. 새 서비스는 파일 하나 커밋하면 된다.
  • 배포 확인은 바깥에서 한다. 빌드할 때 커밋 해시를 이미지에 굽고 /actuator/info의 app.commit으로 내보낸다. CI가 ALB와 Traefik을 거쳐 이 값을 확인하므로, 파드는 멀쩡한데 Ingress가 깨진 경우도 잡힌다. 이 단계를 넣은 경위는 배포는 초록불인데 컨테이너가 계속 재시작되던 일에 있다.
  • 롤백은 Git으로 한다. 해당 태그 커밋을 revert하는 PR을 머지한다. Argo CD UI의 Rollback은 자동 Sync 앱에선 막혀 있고, 쓰더라도 Git과 클러스터가 어긋난다.

Sync 정책은 리소스 성격에 따라 다르게 뒀다.

대상 정책 이유
앱(backend 등) 자동 Sync + selfHeal + prune 일반적인 경우
Redis prune 끔 데이터가 있는 리소스라 Git에서 빠져도 지우지 않는다
ESO, Traefik, 모니터링 자동 Sync, prune 끔 CRD가 지워지면 그 CRD로 만든 리소스가 줄줄이 지워진다
Cilium, Argo CD 자신 수동 Sync만 깨지면 Argo CD가 스스로 되돌릴 수 없다(네트워크를 잃거나 고칠 주체가 사라진다)

Tip

selfHeal이 켜진 앱은 kubectl로 직접 고쳐도 몇 분 안에 Git 상태로 돌아간다. 급한 변경이라도 매니페스트 레포 PR이 가장 빠른 길이다.

헬스 프로브와 Actuator

backend는 포트를 둘로 나눠 쓴다. 8080은 API, 9292는 actuator 전용 관리 포트다(MANAGEMENT_SERVER_PORT). 프로브와 메트릭 수집이 API 트래픽과 섞이지 않게 하려는 것이다.

# application.yml (발췌)
management:
  server:
    port: ${MANAGEMENT_SERVER_PORT:8080}   # dev/prod는 9292
  endpoint:
    health:
      show-details: when-authorized
      probes:
        enabled: true   # /actuator/health/liveness, /actuator/health/readiness
  • liveness는 /actuator/health/liveness, readiness는 /actuator/health/readiness(둘 다 9292)를 본다. 롤링 업데이트는 새 파드의 readiness가 통과해야 옛 파드를 내리므로, readiness가 무중단 배포의 기준선이다.
  • 외부에는 /actuator/health와 /actuator/info만 연다. Traefik이 이 두 경로는 9292로, 나머지는 8080으로 보내서 ALB는 포트 하나만 알면 된다. 메트릭(/actuator/prometheus) 같은 나머지 엔드포인트는 내부망 전용이다. 처음엔 /actuator 전체를 관리 포트로 보내 메트릭이 밖에 열려 있었고, 그 경위는 GitOps 이전 중에 드러난 Traefik 서비스 타입 오류와 외부에 열린 actuator에 있다.
  • 익명 요청에는 UP/DOWN 요약만 보이고, DB·Redis 상세는 인증된 경우에만 보인다.
  • JVM은 Dockerfile의 MaxRAMPercentage로 컨테이너 limit을 따라간다. requests/limits는 꼭 지정한다. 없으면 BestEffort로 분류돼 가장 먼저 쫓겨난다.

결정 기록

결정 상태 맥락 검토한 대안 결과·트레이드오프
dev 배포를 EC2 docker compose에서 Kubernetes로 옮긴다 채택 서버 수동 수정에 이력이 없고, CI가 서버 접근 권한을 들고 push 배포했고, 확장 단위가 인스턴스였다 compose 유지 배포·설정이 Git에 남는다. 대신 클러스터 운영(CNI·인증서·etcd)을 직접 떠안는다. prod는 아직 compose
관리형 EKS 대신 기존 EC2 위에 클러스터를 직접 세운다 채택 이 규모에선 컨트롤 플레인 고정비(월 약 $73)가 부담 EKS 비용을 아꼈다. 대신 IRSA·ECR 자동 인증·Karpenter 같은 EKS 전용 기능이 없다
k3s로 클러스터를 세운다 대체됨(→ kubeadm 재설치) 경량 배포판으로 빠르게 시작 — 단일 서버(SQLite)라 HA 전환이 사실상 재구축이고, etcd·CNI·인증서를 감춰 학습 깊이가 얕았다
컷오버 전에 kubeadm으로 다시 설치한다 채택 실데이터가 올라가기 전이라 갈아타는 비용이 가장 쌌다 k3s 유지 controlPlaneEndpoint만 잡아두면 컨트롤 플레인 추가가 문서화된 join 절차다. 대가는 CNI·인증서·etcd 백업 직접 관리
CNI로 Flannel을 쓴다 대체됨(→ Calico) k3s 때와 같은 VXLAN이라 보안그룹을 그대로 쓸 수 있었다 — NetworkPolicy를 지원하지 않아(적용해도 조용히 무시) dev/prod 격리 요건을 못 맞춘다
CNI를 Calico(eBPF 데이터플레인)로 바꾼다 대체됨(→ Cilium) NetworkPolicy가 필요해졌다 Flannel 유지 정책은 해결됐지만 플로우 단위 네트워크 관측이 부족했다
CNI를 Cilium(eBPF, kube-proxy 대체)으로 바꾼다 채택 Hubble로 플로우 단위 관측이 필요해졌다 Calico 유지 관측을 얻었다. CNI가 바뀔 때마다 노드 간 보안그룹 포트가 달라지고, 홉이 늘어 IMDS 경로가 깨진 일도 있었다(런북). kube-proxy DaemonSet은 롤백용으로 스케줄만 막아 둔다
etcd 시크릿 암호화를 클러스터 설치 전에 켠다 채택 k3s에서 나중에 켜려다 세 번 실패했다 설치 후 활성화 Secret이 at-rest 암호화된다
Ingress는 Traefik을 NodePort로 둔다 채택 ALB 타깃 그룹을 서비스마다 만들고 싶지 않았다 서비스별 ALB 타깃 그룹 ALB는 포트 하나만 안다. /actuator 경로 분기도 Traefik이 맡는다
DB(RDS)·S3·SQS는 AWS 관리형으로 둔다 채택 PII를 다루는 서비스라 자체 호스팅의 백업·내구성 리스크가 절감 효과보다 크다 클러스터 안 자체 호스팅 관리형 비용은 남지만 데이터 내구성을 AWS에 맡긴다
GPU 노드는 같은 인스턴스를 stop/start한다 채택 GPU는 상시 가동할 만큼 쓰지 않는다 Karpenter(EKS 전용), Cluster Autoscaler(매번 새 볼륨이라 모델 재다운로드) 조인은 1회, 모델 캐시는 EBS에 남는다. 꺼진 동안 GPU 파드 Pending은 정상
배포를 Argo CD GitOps로 바꾼다. CI는 매니페스트 레포의 이미지 태그만 커밋한다 채택(#332) CI가 서버·클러스터 자격증명을 들고 밀어 넣는 구조를 없애고 싶었다 CI에서 SSM으로 compose 실행(이전 방식), CI에서 클러스터에 직접 apply 클러스터에 쓰는 주체가 Argo CD 하나다. 급한 변경도 PR을 거쳐야 하고, kubectl 수정은 selfHeal로 되돌아간다
매니페스트는 소스 레포와 분리된 레포에 두고, 환경은 폴더로 나눈다 채택 태그 갱신 커밋이 다시 CI를 돌리면 안 되고, 환경별 브랜치는 승격 때 머지가 꼬인다 소스 레포 안 k8s/ 디렉터리, 환경별 브랜치 레포가 하나 늘었다. 이 레포에는 클러스터 매니페스트가 없다
이미지 태그는 커밋 해시로 하고, /actuator/info의 app.commit으로 배포를 바깥에서 확인한다 채택(#332) 덮어쓰는 dev 태그는 매니페스트가 안 바뀌어 Argo CD가 반응하지 않는다 고정 dev 태그 ALB·Traefik까지 포함해 실제로 새 이미지가 떴는지 확인된다
Sync 정책을 리소스 성격별로 다르게 둔다 채택 데이터·CRD·네트워크를 가진 리소스는 자동 prune·Sync가 위험하다 전부 자동 Sync + prune Cilium·Argo CD 자신은 수동 Sync라 사람이 챙겨야 한다
비밀값은 Secrets Manager에 두고 ESO로 동기화한다 채택 비밀값을 레포에 두지 않으면서 k8s Secret으로 써야 했다 레포에 암호화해 두기, Secret 수동 생성 레포엔 참조만 남는다. 폴링이라 반영이 실시간이 아니다
ECR pull 토큰은 ESO의 ECR 토큰 생성기로 주기 발급한다 채택 토큰이 12시간마다 만료되고 EKS가 아니라 자동 갱신 플러그인이 없다 수동 생성(만료 방치 사고가 있었다, 클러스터 상태가 레포 밖에 있어서 잃어버린 것들) 손으로 만든 Secret은 덮어써진다
actuator를 관리 포트 9292로 분리한다 채택 프로브·메트릭 수집이 API 트래픽과 섞이지 않게 API 포트 공용 외부엔 health·info만 열고 나머지는 내부망 전용이다

결정을 추가하면 결정 로그에도 한 줄 추가한다.

주의할 점

작업할 때

  • selfHeal이 켜진 앱은 kubectl로 고쳐도 몇 분 안에 Git 상태로 돌아간다. replica 조정 같은 급한 변경도 매니페스트 레포 값을 바꿔야 한다.
  • 새 비밀값은 값을 먼저, 코드를 나중에 넣는다. 순서가 뒤집히면 새 파드가 바로 죽는다.
  • 노드를 추가하거나 CNI를 건드릴 때는 현재 CNI 기준 보안그룹 포트 목록부터 확인한다.
  • 같은 노드의 파드는 같은 AWS 권한을 가진다. 새 워커에는 같은 권한의 롤을 붙여야 한다.
  • requests/limits가 없는 파드는 BestEffort로 분류돼 가장 먼저 쫓겨난다.

알려진 한계와 남은 과제

  • 일반 워커가 한 대뿐이다. 앱·Redis·Traefik·Argo CD가 모두 한 노드에 몰려 있어서 그 노드가 곧 단일 장애점이고, 롤링 업데이트 순간엔 메모리 여유가 빠듯하다. 워커 추가를 검토하고 있다.
  • 컨트롤 플레인도 한 대다. 이미 떠 있는 파드는 계속 돌지만, 새 배치와 자동 복구는 멈춘다.
  • prod로 가기 전에 알림 스윕러의 중복 실행을 막아야 한다. 롤링 업데이트 중에는 옛 파드와 새 파드가 잠깐 같이 떠서 스케줄러 작업이 두 번 돌 수 있다. 스윕러가 생긴 경위는 AI 워커가 남긴 상태를 소비하지 않아 리포트가 무음으로 멈춤에 있다.
  • dev/prod가 같은 EC2 롤을 공유한다. prod 클러스터를 만들 때 워커 전용 롤을 분리할 계획이다.
  • 일부 컴포넌트는 아직 레포 밖에 있다. metrics-server, local-path 같은 것들이다. 빈 클러스터에서 레포만으로 다시 세우는 리허설이 필요하다.
  • dev/prod 격리용 NetworkPolicy는 아직 작성 전이다.
  • Redis는 local-path PVC라 노드에 묶여 있다. 다른 노드로 옮기면 데이터를 잃는다.
  • etcd 스냅샷은 매일 뜨지만 복구 리허설은 아직 안 했다. kubeadm 인증서는 1년짜리라 만료 관리가 필요하다(점검 절차는 런북에 있다).

관련 코드

클러스터 매니페스트(차트·values·Argo CD Application)는 별도 private 레포에 있다. 이 레포에서 클러스터와 맞물리는 곳은 다음과 같다.

  • .github/workflows/deploy-dev.yml: 이미지 빌드·push, 매니페스트 레포 태그 커밋, app.commit 폴링
  • .github/workflows/deploy-prod.yml, deploy/prod/: prod는 아직 SSM + docker compose
  • src/main/resources/application.yml: management.server.port(MANAGEMENT_SERVER_PORT), 프로브, 노출 엔드포인트
  • Dockerfile: MaxRAMPercentage 등 JVM 옵션
  • src/main/java/com/soma/backend/global/security/SecurityConfig.java: actuator 공개 범위
  • deploy/monitoring/: 클러스터 밖 중앙 모니터링 스택(클러스터 대시보드·알림 룰 포함)

관련 사례 기록

더 읽을거리

Notion 팀 문서(비공개)

  • k8s (전환 단계 Phase 0~8)
  • k8s 현재 상태
  • k3s 결정 이유 / kubeadm
  • CNI 변경 / Cilium
  • DNS 문제
  • 운영 배선 (etcd 백업 · 인증서 만료)
  • GPU / GPU 트러블 슈팅
  • ArgoCD / secret manager - ESO

관련 위키: Kubernetes 런북 · 결정 로그

Clone this wiki locally