Repository navigation
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

컨트롤 플레인에서 본 cilium status. agent·operator·Hubble이 모두 OK이고, 클러스터의 파드 전부를 Cilium이 관리한다
compose 시절에는 EC2 한 대에 backend·AI 워커·Redis·수집 에이전트가 같이 떠 있었다. 배포는 CI가 SSM으로 접속해 docker compose up을 실행하는 방식이었다.

Kubernetes 이전(docker compose 시절) AWS 구성. 인스턴스마다 compose로 컨테이너를 띄웠고, DB·큐·스토리지는 처음부터 관리형이었다
이 구조에서 걸리던 점은 다음과 같다.
- 서버를 손으로 만지는 단계가 남아 있었다. 환경변수를 바꾸려면 서버의 env 파일을 직접 고쳐야 했고, 그 변경은 어디에도 이력이 남지 않았다.
- CI가 서버 접근 권한을 들고 있어야 했다. 밖에서 명령을 밀어 넣는 push 방식이었기 때문이다.
- 확장 단위가 인스턴스였다. 서비스를 늘리려면 인스턴스와 compose 파일이 같이 늘어난다.
- 팀이 Kubernetes를 실전에서 다뤄 보고 싶었다. 학습 자체도 목표였다.
관리형 EKS는 컨트롤 플레인 고정비(월 약 $73)가 이 규모에선 부담이라 뺐다. 대신 기존 EC2 위에 클러스터를 직접 세웠다.
처음에는 경량 배포판인 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 | 바꾼 이유 |
|---|---|---|
| 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 노드에는 taint를 걸어 일반 파드가 올라가지 않게 하고, GPU 워크로드만 toleration과 nodeSelector로 배치한다. 노드를 등록할 때 라벨과 taint를 kubelet 인자로 넣어 두어서, 노드가 다시 등록돼도 같은 값이 붙는다.
# GPU 워크로드 쪽 (개념 예시)
nodeSelector:
workload: etl
tolerations:
- key: workload
value: etl
effect: NoSchedule
resources:
limits:
nvidia.com/gpu: 1
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
- Secrets Manager → k8s Secret: ESO가 폴링한다(실시간 아님). 급하면 ExternalSecret의 어노테이션을 바꿔 바로 동기화한다(명령은 런북에 있다).
- k8s Secret/ConfigMap → 파드: 파드는 시작할 때 읽은 값을 계속 쓴다. ConfigMap은 차트가 내용 해시를 파드 어노테이션으로 붙여 두어서 바뀌면 자동으로 롤링 재시작되고, Secret은 Reloader가 재시작을 건다.
Important
새 비밀값은 값을 먼저, 코드를 나중에 넣는다. Secrets Manager에 키 추가 → ExternalSecret에 키 추가 → 차트에서 참조 순서다. 코드가 먼저 나가면 새 파드가 값을 못 찾아 바로 죽는다.
ECR 이미지 pull 토큰은 12시간마다 만료된다. EKS가 아니라서 자동 갱신 플러그인이 없고, 그래서 ESO의 ECR 토큰 생성기로 주기적으로 다시 발급한다. 이 Secret을 손으로 만들면 덮어써진다.
AWS 자격증명은 파드 단위로 나뉘지 않고 노드의 EC2 인스턴스 롤을 그대로 쓴다. EKS의 IRSA 같은 파드 단위 권한 분리가 없어서, 같은 노드의 파드는 같은 권한을 가진다. 새 워커를 추가하면 같은 권한의 롤을 붙여야 한다.
배포는 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로 배포 완료·실패 알림
-
레포를 나눴다. 소스 레포는 "어떻게 빌드하나"(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이 가장 빠른 길이다.
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/: 클러스터 밖 중앙 모니터링 스택(클러스터 대시보드·알림 룰 포함)
- CNI를 Flannel에서 Calico를 거쳐 Cilium으로 전환
- 클러스터 상태가 레포 밖에 있어서 잃어버린 것들
- Helm 3과 4의 렌더링 차이를 리소스 단위 대조로 잡고 애드온을 Argo CD로 이전
- GitOps 이전 중에 드러난 Traefik 서비스 타입 오류와 외부에 열린 actuator
- GitHub App 키를 지워 dev 배포가 멈추고 조직 시크릿으로 통합
- 배포는 초록불인데 컨테이너가 계속 재시작되던 일
- 타깃은 UP인데 컨테이너 메트릭이 비어 있던 일 (compose 시절 컨테이너 메트릭 수집)
Notion 팀 문서(비공개)
- k8s (전환 단계 Phase 0~8)
- k8s 현재 상태
- k3s 결정 이유 / kubeadm
- CNI 변경 / Cilium
- DNS 문제
- 운영 배선 (etcd 백업 · 인증서 만료)
- GPU / GPU 트러블 슈팅
- ArgoCD / secret manager - ESO
관련 위키: Kubernetes 런북 · 결정 로그
성능 · 부하테스트
데이터 · API 계약
- 500 144건과 포괄 예외 핸들러
- 평점이 항상 0점
- 검수 대기 숫자 불일치
- 프론트 계약 드리프트
- core 스키마 통일
- 리포트 무음 정지
- OpenAPI camelCase 드리프트
- OpenAPI 상태코드 드리프트
인증 · 메시징
배포 · 관측성
Kubernetes
Kubernetes
레퍼런스