Kubernetes 기본개념
1. 철학과 개념
선언적 관리 (Declarative Management)
정의: 애플리케이션의 "원하는 상태(desired state)"를 파일로 선언하면, 쿠버네티스가 그 상태를 자동으로 만들고 유지하는 관리 방식이다.
핵심 포인트:
- 쿠버네티스의 주요 특징은 선언적으로 애플리케이션을 관리한다는 것
- 컨테이너의 이상적인 상태를 YAML 또는 JSON 형식의 매니페스트 설정 파일로 선언
- 파일 기반이므로 Git으로 버전 관리 가능 → 과거 상태로 되돌리기 용이
매니페스트 (Manifest)
정의: 쿠버네티스 리소스의 "원하는 상태"를 정의한 설정 파일이다.
핵심 포인트:
- YAML 또는 JSON 형식으로 작성
- 용어 유래: 원래 화물 목록(적하목록)이라는 뜻. "이 안에 뭐가 있어야 한다"를 선언하는 문서에서 차용
- 쿠버네티스만의 용어가 아니라 Android, Java JAR, Windows 등에서도 사용
컨트롤러 (Controller)
정의: API를 참조/조작해서 사용자가 정의한 이상적인 상태를 유지하도록 구체적인 관리 작업을 하는 컴포넌트이다. 컨트롤 플레인에 존재한다.
핵심 포인트:
- 쿠버네티스는 관리 정보를 HTTP API로 공개
- 사용자는 API로 이상적인 상태를 선언
- 컨트롤러가 컨테이너 실행 개수 유지 등 구체적인 관리 작업을 수행
Reconciliation Loop (조정 루프)
정의: 컨트롤러가 원하는 상태(desired state)와 현재 상태(current state)를 주기적으로 비교하여 차이가 있으면 현재 상태를 원하는 상태로 수렴시키는 무한 루프이다.
핵심 포인트:
- 모든 쿠버네티스 컨트롤러의 핵심 패턴이며, Operator, KEDA, ArgoCD 등 확장 컨트롤러도 동일한 패턴을 사용
- 동작: Watch(변경 감지) → Compare(상태 비교) → Act(차이 해소) → 반복
- 예: ReplicaSet Controller가 "replicas: 3"을 선언했는데 현재 Pod가 2개이면, 1개를 추가 생성하여 3개로 수렴
- 멱등성(Idempotency): 같은 조정 루프를 여러 번 실행해도 결과가 동일 → 안전한 재시도 보장
CGV 적용: KEDA ScaledObject가 Prometheus 메트릭을 Watch하여 대기열 크기가 threshold(1000)를 초과하면 Pod 수를 조정하는 것도 Reconciliation Loop의 구현이다.
2. 클러스터 아키텍처
2.1 컨테이너와 쿠버네티스 개념
- 자바실행과정
- JAR = 앱코드( .java를 javac가 컴파일한 .class를 jar로 패키징한 파일) + 라이브러리
- JRE(JVM + 표준 라이브러리) = 런타임
- 컨테이너: 앱 코드 + 실행 환경(런타임, 라이브러리, 설정)을 통째로 포장해서, 호스트 커널을 공유하며 프로세스 단위로 격리 실행하는 경량 패키지.
- 컨테이너 이미지 = JAR 파일 + JRE(JVM 포함) + 설정
- Dockerfile로 JAR, JRE, 설정들을 레이어로 쌓아 docker build 명령어로 컨테이너 이미지를 생성
- 도커 빌드를 하면 RO 레이어는 로컬 캐시에 저장됨
- 컨테이너 이미지는 docker run 명령어를 통해 컨테이너(격리된 프로세스)로 실행
- 이미지 RO 레이어는 새 RW레이어와 통합되어 통합된 디렉터리를 루트 디렉터리로 가지는 컨테이너로 시작됨
- VM처럼 OS 전체를 가상화하는 게 아니라, 호스트 OS의 커널을 공유하면서 프로세스 수준으로 격리하기 때문에 수초내에 시작되고 메모리 오버헤드가 거의 없다.
- "내 PC에서는 되는데 서버에서 안 돼"를 해결하는 기술 — 어디서 실행하든 동일한 환경이 보장된다.
- 쿠버네티스 = 컨테이너를 자동으로 관리하는 오케스트레이션 시스템/플랫폼
- 오케스트레이션 = 여러 컨테이너를 자동으로 조율해서 하나의 시스템으로 운영하는 것
- 클러스터 = 쿠버네티스를 실행하면 만들어지는 환경/인스턴스 (마스터 노드 + 워커 노드의 묶음)
클러스터는 여러 대의 컴퓨터를 묶어서 하나의 시스템처럼 동작하게 하는 것이다(예: 3개의 EC2 인스턴스).
쿠버네티스에서의 클러스터는 1개 이상의 컨트롤 플레인 + 1개 이상의 워커 노드를 묶은 하나의 운영 단위이다.
쿠버네티스를 설치한다는 것은 클러스터를 구성하는 도구를 써서 필요한 컴포넌트들을 세팅하는 것이다.
클러스터 종류
| 종류 | 도구/서비스 | 특징 |
|---|---|---|
| 로컬 클러스터 | minikube, kind, Docker Desktop | 개발용, 내 PC에서 실행 |
| 온프레미스 클러스터 | kubeadm 등 | 실제 서버에 직접 구축 |
| 매니지드 클러스터 | EKS(AWS), GKE(GCP), AKS(Azure) | 컨트롤 플레인을 클라우드가 관리 |
클러스터 아키텍처
쿠버네티스 클러스터
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 컨트롤 플레인 (마스터 노드) │
│ 클러스터 전체의 관리를 담당 │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ kube-apiserver 모든 통신의 중심 (REST API) │ │
│ │ ↕ │ │
│ │ etcd 모든 상태/설정 저장 (유일한 저장소) │ │
│ │ ↕ │ │
│ │ kube-scheduler Pod를 어떤 노드에 배치할지 결정 │ │
│ │ ↕ │ │
│ │ kube-controller-manager 원하는 상태 ↔ 현재 상태 맞춤 │ │
│ │ │ │
│ │ [노드 컴포넌트] kubelet / kube-proxy / 컨테이너 런타임 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↑ │
│ │ API 서버를 통해 통신 │
│ ↓ │
│ 워커 노드 1 워커 노드 2 │
│ 실제 Pod가 실행되는 머신 실제 Pod가 실행되는 머신 │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ │ │ │ │
│ │ kubelet │ │ kubelet │ │
│ │ 컨트롤 플레인 명령 수신 │ │ 컨트롤 플레인 명령 수신 │ │
│ │ Pod 관리 + 상태 보고 │ │ Pod 관리 + 상태 보고 │ │
│ │ │ │ │ │
│ │ kube-proxy │ │ kube-proxy │ │
│ │ 네트워크/로드밸런싱 │ │ 네트워크/로드밸런싱 │ │
│ │ │ │ │ │
│ │ 컨테이너 런타임 │ │ 컨테이너 런타임 │ │
│ │ (containerd 등) │ │ (containerd 등) │ │
│ │ │ │ │ │
│ │ ┌─Pod A─┐ ┌─Pod B─┐ │ │ ┌─Pod C─┐ ┌─Pod D─┐ │ │
│ │ │컨테이너│ │컨테이너│ │ │ │컨테이너│ │컨테이너│ │ │
│ │ └───────┘ └───────┘ │ │ └───────┘ └───────┘ │ │
│ └────────────────────────┘ └────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
↑
│ REST API 요청
↓
┌──────────────────────────────────────┐
│ kubectl (외부 CLI 도구, 로컬에 설치) │
│ ~/.kube/config (클러스터 접속 정보) │
└──────────────────────────────────────┘
kubectl
정의: 쿠버네티스 클러스터를 명령줄(CLI)에서 제어하는 도구이다. 로컬에 설치한다.
주요 기능:
- 클러스터에 리소스(Pod, Service, Deployment 등) 생성/조회/수정/삭제
- 매니페스트 파일을 클러스터에 적용
- 클러스터 상태 모니터링
- Pod 안에 접속(exec), 로그 확인 등
~/.kube/config: kubectl이 어떤 클러스터에 접속할지 알려주는 로컬에 저장되는 설정 파일
kubectl 작동 흐름:
kubectl 명령 실행
↓
~/.kube/config 파일에서 클러스터 접속 정보 확인
↓
해당 클러스터의 kube-apiserver에 REST API 요청 전송
↓
apiserver가 인증/인가 확인 후 처리
↓
결과를 kubectl로 응답
2.2 컨트롤 플레인 컴포넌트
컨트롤 플레인은 클러스터 전체의 관리를 담당하는 노드이다. 관리용 컴포넌트들이 실행된다.
노드 컴포넌트(kubelet, kube-proxy, 컨테이너 런타임)도 함께 실행된다.
kube-apiserver
정의: 모든 통신의 중심이 되는 컴포넌트이다. kubectl, kubelet, controller-manager, scheduler 모두 API 서버를 통해 통신한다.
핵심 포인트:
- REST API로 리소스 CRUD 제공
- 수평 확장 가능 (여러 인스턴스 실행)
- 유일하게 etcd와 직접 통신하는 컴포넌트 — 다른 모든 컴포넌트는 반드시 apiserver를 거쳐야 etcd에 접근
요청 처리 파이프라인:
API 요청 수신
↓
1. 인증 (Authentication): "누구인가?" — 인증서, 토큰, OIDC 등
↓
2. 인가 (Authorization): "권한이 있는가?" — RBAC, ABAC 등
↓
3. Admission Control: "정책 위반 없는가?"
├── Mutating Admission: 요청 변경 (사이드카 주입, 기본값 추가 등)
├── 스키마 검증 (OpenAPI validation)
└── Validating Admission: 요청 거부/허용 (latest 태그 금지 등)
↓
4. etcd에 저장
etcd
정의: 쿠버네티스의 유일한 상태 저장소이다 (분산 key-value store).
핵심 포인트:
- 모든 리소스 정보(Pod, Service, ConfigMap 등)가 여기에 저장
- Raft 합의 알고리즘으로 데이터 일관성 보장 — 과반수(quorum)가 동의해야 쓰기가 확정됨
- 프로덕션에서는 3대 이상으로 이중화 (홀수 유지: 3대면 1대 장애 허용, 5대면 2대 장애 허용)
- etcd가 죽으면 클러스터 전체가 먹통 → 가장 중요한 백업 대상
- 성능: 수천 개 Watch를 동시 처리, 읽기는 빠르지만 쓰기는 Raft 합의 필요로 상대적으로 느림
kube-scheduler
정의: 새로 생성된 Pod를 어떤 노드에 배치할지 결정하는 컴포넌트이다.
핵심 포인트:
- 2단계 프로세스: 필터링(Filtering) → 스코어링(Scoring)
- 필터링: 조건에 맞지 않는 노드 제외 (리소스 부족, Taint 불일치, nodeSelector 불일치 등)
- 스코어링: 남은 노드에 점수를 매겨 최적 선택 (리소스 균형, affinity 충족도 등)
- 스케줄러는 기존 노드 중에서 배치할 곳을 고르는 역할. 배치 가능한 노드가 없으면 Pod는 Pending 상태
- Karpenter와의 차이: 스케줄러는 기존 노드에 배치, Karpenter는 Pending Pod를 감지하여 새 노드를 생성
kube-controller-manager
정의: 여러 컨트롤러를 하나의 바이너리로 실행하는 컴포넌트이다.
핵심 포인트:
- 각 컨트롤러는 독립적인 Reconciliation Loop를 실행 (→ 섹션 1)
- 하나의 프로세스 안에서 여러 컨트롤러가 별도의 goroutine으로 동작
주요 컨트롤러:
| 컨트롤러 | 역할 |
|---|---|
| Deployment Controller | Deployment의 원하는 상태를 유지 (ReplicaSet 관리) |
| ReplicaSet Controller | 지정된 수의 Pod 유지 |
| Node Controller | 노드 상태 모니터링, 응답 없는 노드의 Pod를 다른 노드로 재스케줄링 |
| Job Controller | Job 완료 추적, 실패 시 재시도 관리 |
| ServiceAccount Controller | 네임스페이스마다 default ServiceAccount 자동 생성 |
| Endpoint Controller | Service와 Pod를 연결하는 Endpoint 객체 관리 |
2.3 워커 노드 컴포넌트
워커 노드는 실제 Pod(컨테이너)가 실행되는 노드(머신)이다.
kubelet
정의: 각 노드에서 실행되는 에이전트이다. API 서버로부터 Pod 스펙을 받아 실제 컨테이너를 관리한다.
핵심 포인트:
- 주요 역할: 컨테이너 생성/삭제, 프로브 실행(liveness/readiness/startup), 리소스 모니터링, 노드 상태 보고
- CRI(Container Runtime Interface)를 통해 컨테이너 런타임(containerd)과 통신
- kubelet 자체는 컨테이너가 아닌 시스템 프로세스로 노드에서 직접 실행됨
kube-proxy
정의: 각 노드에서 실행되며, Service의 가상 IP(ClusterIP)로 들어온 트래픽을 실제 Pod IP로 포워딩하는 네트워크 프록시이다.
핵심 포인트:
- DaemonSet: 모든 워커 노드에 하나씩 반드시 실행됨
- Packet Routing: Service IP는 실제 인터페이스가 없는 가상 IP이다. kube-proxy가 설정한 규칙이 이 가상 IP 패킷을 가로채서(DNAT) 실제 Pod IP로 목적지를 바꾼다
동작 모드 (Mode):
| 모드 | iptables (Legacy/Default) | IPVS (Performance) |
|---|---|---|
| 구현 기술 | 리눅스 netfilter의 iptables 규칙 | 리눅스 커널 레벨 L4 로드밸런서 |
| 알고리즘 | 랜덤, 라운드로빈 (순차 탐색 O(N)) | 라운드로빈, 최소 연결 등 (해시 테이블 O(1)) |
| 성능 | Service 5천 개 이상이면 규칙 갱신/조회가 느려짐 | Service 수만 개여도 성능 저하 거의 없음 |
| 용도 | 일반적인 규모의 클러스터 | 대규모 트래픽/서비스 환경 |
NetworkPolicy와의 관계: kube-proxy는 연결을 담당하고, 트래픽 차단/허용(방화벽)은 CNI 플러그인(Calico, AWS VPC CNI 등)이 담당한다. 기술적으로 둘 다 iptables를 조작하므로 호환성이 중요하다.
CRI (Container Runtime Interface)
정의: kubelet과 컨테이너 런타임 사이의 표준 인터페이스이다.
핵심 포인트:
- kubelet이 특정 런타임에 종속되지 않도록 추상화 계층 제공
- 대표 런타임: containerd (Docker에서 분리된 런타임, 현재 사실상 표준), CRI-O (Red Hat 기반, OCI 전용 경량 런타임)
- Docker는 K8s 1.24부터 지원 중단 (dockershim 제거). Docker가 내부적으로 containerd를 사용하므로, Docker를 제거하고 containerd를 직접 사용하는 것
관련 표준:
| 표준 | 설명 |
|---|---|
| OCI (Open Container Initiative) | 컨테이너 이미지와 런타임의 표준 규격 |
| CRI (Container Runtime Interface) | kubelet ↔ 런타임 통신 인터페이스 |
| CNI (Container Network Interface) | 컨테이너 네트워크 설정 인터페이스 |
CoreDNS (Cluster DNS)
정의: 쿠버네티스 클러스터 내부의 DNS 서버 역할을 하는 필수 애드온이다. Service 이름을 IP로 변환(Resolve)한다.
핵심 포인트:
- Service Discovery의 핵심: Pod가 다른 Service를 찾을 때 IP가 아닌 서비스명으로 찾게 해주는 주체
- Deployment 형태: kube-system 네임스페이스에 보통 2개 이상의 레플리카로 실행 → 고가용성
- ConfigMap (Corefile): DNS 설정을 관리하며, 외부 도메인에 대한 포워딩 설정도 가능
DNS 레코드 형식:
<service-name>.<namespace>.svc.cluster.local
예: cgv-api-service.cgv-prod.svc.cluster.local
prometheus-kube-prometheus-prometheus.monitoring.svc.cluster.local
DNS 질의 과정 (Resolution Flow):
1. Pod A가 curl http://cgv-api-service 요청
2. Pod A의 /etc/resolv.conf → nameserver 10.100.0.10 (CoreDNS Service IP)
3. 요청이 CoreDNS Pod로 전달
4. CoreDNS가 메모리 내 테이블에서 ClusterIP를 찾아 반환
(cgv-api-service.default.svc.cluster.local → 10.100.x.x)
5. Pod A는 반환받은 ClusterIP로 패킷 전송
ndots 설정과 DNS 검색 순서:
- Pod의
/etc/resolv.conf에ndots:5가 기본 설정됨 - 도메인에 점(.)이 5개 미만이면 search 도메인을 순차 시도:
<ns>.svc.cluster.local→svc.cluster.local→cluster.local→ 외부 DNS - 외부 도메인(예:
api.example.com)은 점이 2개이므로 내부 search를 먼저 시도 → 불필요한 DNS 쿼리 발생 - 최적화: 외부 도메인 호출 시 FQDN 끝에 점(.)을 붙이면 search 단계를 건너뜀 (
api.example.com.)
Pod DNS Policy:
| 정책 | 동작 |
|---|---|
| ClusterFirst | 기본값. 클러스터 내부 DNS(CoreDNS)를 먼저 조회, 없으면 upstream |
| Default | 노드의 DNS 설정을 상속 |
| None | Pod spec의 dnsConfig만 사용 (완전 수동 설정) |
Upstream Nameserver: CoreDNS가 클러스터 내부에서 해석하지 못하는 외부 도메인은 노드의 /etc/resolv.conf에 설정된 upstream DNS (예: VPC DNS 서버 10.0.0.2)로 포워딩한다.
CGV 적용: 모든 서비스 간 통신에서 IP 하드코딩 없이 서비스명을 사용.
Prometheus FQDN prometheus-kube-prometheus-prometheus.monitoring.svc:9090도 CoreDNS가 해석한다
2.4 전체 요청 흐름
kubectl apply -f deployment.yaml을 실행했을 때 클러스터 내부에서 일어나는 전체 흐름이다.
사용자: kubectl apply -f deployment.yaml
↓
kube-apiserver
├── 인증 (Authentication): 누구인가?
├── 인가 (Authorization): 권한이 있는가? (RBAC)
├── Admission Control: 정책 위반 없는가?
└── etcd에 저장
↓
kube-controller-manager
└── Deployment Controller가 변경 감지 (Watch)
└── ReplicaSet 생성/수정
└── Pod 생성 요청
↓
kube-scheduler
└── Pod를 어떤 노드에 배치할지 결정
├── 필터링: 조건에 맞지 않는 노드 제외
└── 스코어링: 남은 노드 중 최적 선택
↓
kubelet (해당 노드)
└── CRI를 통해 컨테이너 런타임에 컨테이너 생성 요청
└── 컨테이너 실행
↓
kube-proxy
└── Service에 대한 네트워크 규칙 설정
└── 트래픽 라우팅 준비 완료
2.5 EKS에서의 컨트롤 플레인 [EKS 전용]
정의: Amazon EKS(Elastic Kubernetes Service)는 컨트롤 플레인을 AWS가 완전 관리하는 매니지드 쿠버네티스 서비스이다.
핵심 포인트:
- AWS가 관리하는 것: kube-apiserver, etcd, kube-scheduler, kube-controller-manager, etcd 백업/복구, 컨트롤 플레인 고가용성(Multi-AZ)
- 사용자가 관리하는 것: 워커 노드(EC2), Pod, Deployment, Service, Helm Chart, 모니터링 등
- EKS 컨트롤 플레인은 AWS 전용 VPC에서 실행되며 사용자 VPC와 분리됨
- etcd 이중화, apiserver 수평 확장, 자동 버전 패치를 AWS가 처리 → 운영 부담 감소
EKS API Server Endpoint:
| 설정 | 접근 경로 | 보안 |
|---|---|---|
| Public (기본값) | 인터넷 → EKS API Server | 퍼블릭 IP 노출, CIDR 제한 권장 |
| Private | VPC 내부에서만 접근 | 가장 안전, VPN/Direct Connect 필요 |
| Public + Private | 노드는 Private, 관리자는 Public 사용 | CGV 권장 — 노드 트래픽은 VPC 내부, 관리 접근은 외부 허용 |
EKS 아키텍처:
AWS 관리 VPC (사용자 접근 불가) 사용자 VPC
┌──────────────────────────┐ ┌────────────────────────┐
│ kube-apiserver (Multi-AZ)│ ◄──ENI──► │ 워커 노드 1 │
│ etcd (자동 백업) │ │ ├── kubelet │
│ kube-scheduler │ │ ├── kube-proxy │
│ kube-controller-manager │ │ └── Pod A, Pod B │
└──────────────────────────┘ │ │
│ 워커 노드 2 │
│ ├── kubelet │
│ ├── kube-proxy │
│ └── Pod C, Pod D │
└────────────────────────┘
ENI(Elastic Network Interface)를 통해 AWS 관리 VPC의 apiserver와 사용자 VPC의 워커 노드가 프라이빗 네트워크로 통신한다.
CGV 적용: EKS 클러스터에서 컨트롤 플레인은 AWS 관리, 사용자는 Karpenter + Managed Node Group으로 워커 노드를 관리
2.6 컴포넌트 간 통신 정리
컨트롤 플레인 내부:
scheduler, controller-manager → apiserver (API 호출) → etcd (상태 저장)
* 모든 컴포넌트는 반드시 apiserver를 통해서만 etcd에 접근
노드 ↔ 컨트롤 플레인:
kubelet → apiserver (Pod 스펙 수신, 상태 보고)
apiserver → kubelet (exec, logs 등 직접 연결)
노드 내부:
kubelet → CRI → 컨테이너 런타임 → 컨테이너
kube-proxy → iptables/IPVS 규칙 설정
Pod 간 통신:
같은 노드: Pod → veth → cbr0(Linux bridge) → 대상 Pod
다른 노드: Pod → veth → cbr0 → 노드 NIC → 대상 노드 → 대상 Pod
Service 경유: Pod → kube-proxy(iptables/IPVS) → ClusterIP → 대상 Pod
DNS 해석:
Pod → CoreDNS → Service 이름을 ClusterIP로 변환
면접 대비 정리
쿠버네티스 구성요소는?
쿠버네티스 클러스터는 컨테이너 그룹을 실행하는 머신들의 집합이다. 구성 요소로는 크게 컨트롤 플레인과 워커 노드로 구성된다.
- 컨트롤 플레인(클러스터 전체 관리 노드):
- API 서버(모든 통신의 중심이자 etcd와 유일하게 직접 통신),
- etcd(Raft 합의 기반 분산 저장소),
- 스케줄러(필터링→스코어링 2단계로 Pod 배치 결정),
- 컨트롤러 매니저(Reconciliation Loop로 원하는 상태와 현재 상태 비교/수렴)
- 워커 노드(실제 Pod 실행 노드):
- kubelet(CRI를 통해 컨테이너 런타임과 통신하여 Pod 관리),
- kube-proxy(iptables/IPVS로 Service 가상 IP를 Pod IP로 라우팅),
- 컨테이너 런타임(containerd가 사실상 표준)
- CoreDNS: Service 이름을 ClusterIP로 해석하는 클러스터 내부 DNS
- 클러스터를 외부에서 조작하기 위해 kubectl CLI 도구를 사용하며, 항상 API 서버를 통해서만 클러스터와 통신
쿠버네티스는 어떻게 돌아가나?
쿠버네티스의 핵심은 선언형 관리이다. 사용자가 매니페스트에 원하는 상태를 선언하면, 쿠버네티스가 그 상태를 자동으로 유지한다.
- "nginx를 3개 실행해달라"는 Deployment를 적용하면,
- kubectl이 API 서버에 요청 → 인증/인가/Admission Control → etcd에 저장
- 컨트롤러 매니저가 감지 → ReplicaSet과 Pod 생성 요청 → 스케줄러가 노드 배치 결정 → kubelet이 CRI를 통해 containerd로 컨테이너 실행
- 이후 컨트롤러 매니저가 Reconciliation Loop를 통해 계속 감시하며, Pod가 죽어서 2개가 되면 자동으로 1개를 추가해서 3개를 유지 (Self-healing)
3. Pod
"쿠버네티스는 컨테이너를 직접 실행하지 않고, 반드시 Pod라는 단위로 감싸서 실행한다."
3.1 Pod 기본 개념
정의: 하나 이상의 컨테이너를 한꺼번에 묶어서 관리할 수 있는 배포 단위이다.
클러스터
├── 노드 1
│ ├── Pod A
│ │ └── 컨테이너 1
│ ├── Pod B
│ │ ├── 컨테이너 2
│ │ └── 컨테이너 3 (사이드카)
│ └── Pod C
│ └── 컨테이너 4
│
└── 노드 2 (컨트롤 플레인)
└── (시스템 Pod들: apiserver, etcd 등)
핵심 포인트:
- 파드 1개에 컨테이너 1개만 포함 가능, 파드 1개에 여러 컨테이너들도 포함 가능
- 하나의 파드에 포함된 컨테이너 그룹은 동일한 노드에 배포되고 네트워크 인터페이스와 스토리지 할당 등을 공유
- 파드 내부의 컨테이너는 localhost로 서로 통신 가능
- 컨테이너 그룹은 그 내부에서 동작하는 관련성이 깊은 애플리케이션 모음으로 간주
일회성 (Ephemeral)
- Pod는 죽으면 사라짐. 같은 Pod가 부활하는 게 아니라 새 Pod가 생성됨
- 새 Pod는 새 IP를 받음
- 직접 만들면 죽었을 때 아무도 안 살려줌
- 그래서 워크로드 리소스(Deployment 등)로 관리하는 것
디자인 패턴
파드에 컨테이너를 배치하는 방식:
| 패턴 | 설명 |
|---|---|
| 사이드카 (Sidecar) | 메인 컨테이너 기능을 확장하는 컨테이너를 동일한 파드에 배치 |
| 앰배서더 (Ambassador) | 메인 컨테이너에 프록시 기능을 제공 |
| 어댑터 (Adapter) | 서로 다른 컨테이너에서 통일된 데이터를 파드 외부에 제공 |
레이블과 애너테이션
파드와 관리 대상이 수천 개 이상으로 많아질 때, 이런 대상을 그룹화하고 추가 정보를 부여하는 기능이다.
레이블 (Label): 쿠버네티스 리소스에 붙이는 태그
- 레이블을 부여한 관리 대상은 셀렉터(selector)를 사용해서 대상을 선별
- 각 파드에 레이블을 부여해서, 해당 레이블로 구성된 파드를 모아 하나의 디플로이먼트 작성이 가능
애너테이션 (Annotation): 레이블처럼 관리 대상에 부여할 수 있는 키와 값의 쌍이지만, 셀렉터를 지정할 수 없음
- 주로 쿠버네티스를 구성하는 컴포넌트와 주변 도구용 추가 정보를 부여하는 데 사용
- 예:
alb.ingress.kubernetes.io/target-type: ip(ALB Controller가 읽는 설정)
3.2 프로브 (Probe)
정의: 쿠버네티스가 Pod의 상태를 확인하는 헬스 체크 메커니즘이다. 3가지 종류가 있다.
프로브가 필요한 이유:
- 프로브 없이 Pod가 Running 상태지만 앱이 아직 초기화되지 않은 경우 → 트래픽 수신 → 에러 발생
- 프로브 없이 Pod가 Running 상태지만 앱이 데드락에 걸린 경우 → 아무도 인지 못함 → 장애
- 프로브가 있으면 시작 대기, 자동 복구, 트래픽 차단 등이 가능
startupProbe -- 시작 완료 확인
- 앱이 시작될 때까지 다른 프로브(liveness, readiness)를 비활성화
- 시작이 느린 앱(Spring Boot, JVM 등)에 반드시 필요
- 실패하면 컨테이너 재시작
- CGV 적용:
/health/ready-- DB 연결 포함 초기화 완료 확인
livenessProbe -- 프로세스 생존 확인
- 주기적으로 확인, 실패하면 컨테이너 재시작
- 데드락, 무한루프 등 "살아있지만 동작 안 하는" 상태를 감지
- CGV 적용:
/health/live-- 프로세스만 생존 확인 (가벼운 체크)
readinessProbe -- 서비스 투입 가능 여부
- 실패하면 Service의 Endpoint에서 제거 (트래픽 안 보냄)
- 컨테이너를 죽이진 않음. 준비되면 다시 트래픽을 받음
- CGV 적용:
/health/ready-- DB + Redis 연결 확인 (무거운 체크)
프로브 YAML 예시 (CGV 프로젝트)
spec:
containers:
- name: cgv-api
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 180 # JVM 시작 대기
periodSeconds: 5
failureThreshold: 30 # 최대 180 + 5x30 = 330초 대기
livenessProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 10
failureThreshold: 3 # 30초간 실패하면 재시작
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 5
failureThreshold: 3 # 15초간 실패하면 트래픽 차단
프로브 동작 흐름
startupProbe 성공 전: liveness/readiness 비활성
startupProbe 성공 후: liveness/readiness 활성화
├── liveness 실패 → 컨테이너 재시작
└── readiness 실패 → 트래픽 차단 (재시작 안 함)
프로브 비교
| 항목 | startupProbe | livenessProbe | readinessProbe |
|---|---|---|---|
| 확인 시점 | 시작 시에만 | 시작 후 주기적 | 시작 후 주기적 |
| 실패 시 동작 | 컨테이너 재시작 | 컨테이너 재시작 | 트래픽 차단 |
| 확인 대상 | 초기화 완료 여부 | 프로세스 생존 여부 | 서비스 투입 가능 |
| CGV 엔드포인트 | /health/ready | /health/live | /health/ready |
프로브 체크 방식: httpGet(HTTP 요청), tcpSocket(TCP 연결), exec(명령어 실행), grpc(gRPC 호출)
3.3 Requests & Limits (리소스 요청과 제한)
정의: Pod가 사용할 CPU/메모리를 정의하는 설정이다. 스케줄링과 안정성의 핵심이다.
- requests: 스케줄러가 노드를 고를 때 기준 ("이만큼은 보장해줘")
- limits: 최대 사용량 ("이 이상 쓰면 제한/OOM Kill")
왜 필요한가:
- requests/limits 없으면 한 Pod가 CPU/메모리를 독점 → 같은 노드의 다른 Pod가 자원 부족 → 장애
- 스케줄러가 노드 자원 파악 불가 → 과밀 배치 → 전체 노드 장애
- requests가 있으면 스케줄러가 "이 노드에 500m CPU 여유 있으니 배치 가능" 판단
- limits가 있으면 "최대 1코어까지만 써" → 다른 Pod 자원 보호
YAML 예시
resources:
requests:
cpu: 500m # 0.5 코어 보장
memory: 1Gi # 1GB 보장
limits:
cpu: 1000m # 최대 1코어
memory: 2Gi # 최대 2GB (초과 시 OOM Kill)
CPU vs Memory -- 초과 시 동작 차이
| 리소스 | limits 초과 시 | 상세 |
|---|---|---|
| CPU | Throttling (느려짐, 죽진 않음) | CPU 시간을 제한해서 속도가 느려지는 것. 요청 응답 시간이 길어질 수 있음 |
| Memory | OOM Kill (즉시 종료) | 커널이 프로세스를 강제 종료. Pod가 CrashLoopBackOff 상태로 빠짐. 메모리는 CPU와 달리 "나중에 돌려줘"가 안 되기 때문 |
CGV 적용 예시
| 환경 | requests (cpu / memory) | limits (cpu / memory) |
|---|---|---|
| dev | 200m / 512Mi | 500m / 1Gi |
| prod | 500m / 1Gi | 1000m / 2Gi |
dev는 작게, prod는 크게. requests는 "최소 보장", limits는 "최대 허용"이다 (→ 2.3 섹션 9.1).
requests와 limits 비교
| 항목 | requests | limits |
|---|---|---|
| 역할 | 최소 보장량 (스케줄링 기준) | 최대 허용량 |
| CPU 초과 | - | Throttling (느려짐) |
| Memory 초과 | - | OOM Kill (즉시 종료) |
| 미설정 시 | 스케줄러가 무조건 배치 | 무제한 사용 가능 (위험) |
3.4 Pod Lifecycle (생애주기)
정의: Pod는 생성부터 종료까지 Phase(단계)를 거친다.
Phase
| Phase | 의미 |
|---|---|
| Pending | 스케줄링 대기 중 또는 이미지 Pull 중 |
| Running | 최소 1개 컨테이너가 실행 중 |
| Succeeded | 모든 컨테이너가 정상 종료 (exit 0) -- Job에서 주로 사용 |
| Failed | 최소 1개 컨테이너가 비정상 종료 (exit != 0) |
| Unknown | 노드와 통신 불가 (노드 다운 등) |
일반적인 흐름:
Pending → Running → (Succeeded 또는 Failed)
Pod 생성 요청
↓
Pending: 스케줄러가 노드 선택 중 → 이미지 Pull 중
↓
Running: 컨테이너 실행됨 → Probe 시작
↓
종료: Succeeded(정상) 또는 Failed(비정상)
restartPolicy
컨테이너가 종료되면 어떻게 할 것인지에 대한 정책이다.
| 정책 | 동작 |
|---|---|
| Always | 항상 재시작 (Deployment 기본값) |
| OnFailure | 실패 시에만 재시작 (Job 기본값) |
| Never | 절대 재시작 안 함 |
3.5 Init Container
정의: 메인 컨테이너가 시작되기 전에 실행되는 초기화 컨테이너이다. 초기화 작업 완료 후 종료되면 메인 컨테이너가 시작된다.
핵심 포인트:
- 순차 실행: init-1 완료 → init-2 완료 → 메인 컨테이너 시작
- 하나라도 실패하면 재시도
- 초기화 로직을 메인 앱에서 분리하면, 초기화 실패 시 앱 시작 방지 → 안전
- DB 마이그레이션, 설정 파일 다운로드, 의존 서비스 대기 등에 사용
YAML 예시
spec:
initContainers:
- name: wait-for-db
image: busybox
command: ['sh', '-c', 'until nc -z mysql-service 3306; do sleep 2; done']
# DB가 준비될 때까지 대기
- name: init-config
image: busybox
command: ['sh', '-c', 'cp /config/default.conf /app/config/']
# 설정 파일 복사
containers:
- name: app
image: my-app:1.0
# init 컨테이너들이 모두 완료된 후에야 시작
사용 사례:
- DB 마이그레이션 실행
- 의존 서비스(DB, Redis) 준비 상태 대기
- 설정 파일 다운로드/생성
- 볼륨 권한 변경
3.6 Sidecar Container
정의: 메인 컨테이너와 함께 실행되면서 보조 기능을 제공하는 컨테이너이다. 메인 앱의 기능을 분리/확장하는 패턴이다.
핵심 포인트:
- 일반 Pod는 컨테이너 1개(메인 앱만), Sidecar는 2개 이상(메인 앱 + 보조 컨테이너)
- 같은 Pod이므로 localhost로 통신, 볼륨 공유 가능
대표적인 Sidecar 패턴
| 패턴 | 역할 |
|---|---|
| 로그 수집기 | 앱 로그 → 파일 → Sidecar가 수집/전송 |
| 프록시 (Envoy) | 서비스 메시 트래픽 관리 |
| 설정 동기화 | 외부 설정 변경 감지 → 자동 반영 |
| 모니터링 에이전트 | 앱 메트릭 수집/내보내기 |
K8s 1.28+ 네이티브 Sidecar
기존에는 일반 컨테이너로 Sidecar를 구현했으나, Pod 종료 시 순서 보장이 안 됐다. 1.28+에서는 restartPolicy: Always인 initContainer가 네이티브 Sidecar로 동작한다.
spec:
initContainers:
- name: log-collector
image: fluentd
restartPolicy: Always # ← 이것이 네이티브 Sidecar 선언
# Init 단계에서 시작되지만 종료되지 않고 메인 컨테이너와 함께 실행
containers:
- name: app
image: my-app:1.0
네이티브 Sidecar의 장점: 메인 컨테이너보다 먼저 시작/나중에 종료 → 시작/종료 순서 보장
3.7 Ephemeral Container
정의: 이미 실행 중인 Pod에 임시로 컨테이너를 추가해서 디버깅하는 기능이다. Pod를 재시작하지 않고 문제를 조사할 수 있다.
왜 필요한가:
kubectl exec로 접속하려 해도 앱 이미지에 sh, curl, dig 같은 도구가 없으면 디버깅 불가- distroless 이미지(최소 이미지)에는 쉘조차 없음
- Ephemeral Container는 디버깅 도구가 있는 이미지를 Pod에 주입 → 네트워크/프로세스 공유 → Pod 재시작 없이 실시간 디버깅
# 사용 방법
kubectl debug -it my-pod --image=nicolaka/netshoot --target=app
# netshoot 이미지: curl, dig, tcpdump, ss, ping 등 네트워크 도구 포함
# --target=app: app 컨테이너의 프로세스 네임스페이스 공유
특징:
- Pod spec에 미리 정의하지 않음 → 런타임에 동적 추가
- Resource limits 설정 불가 (디버깅 전용)
- Pod가 삭제되면 함께 사라짐
- Probe 적용 안 됨
3.8 QoS 클래스 (Quality of Service)
정의: 쿠버네티스가 Pod에 자동으로 부여하는 서비스 품질 등급이다. requests와 limits 설정에 따라 결정되며, 메모리 부족(OOM) 시 어떤 Pod를 먼저 퇴거(evict)할지의 기준이 된다.
QoS 클래스 3가지
| QoS 클래스 | 조건 | 안정성 |
|---|---|---|
| Guaranteed | 모든 컨테이너의 requests == limits (CPU, Memory 모두) | 가장 높음 (마지막에 퇴거) |
| Burstable | requests < limits (일부 컨테이너라도) | 중간 |
| BestEffort | requests/limits가 전혀 없음 | 가장 낮음 (먼저 퇴거) |
결정 예시
# Guaranteed — requests와 limits가 동일
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 500m
memory: 1Gi
# Burstable — requests < limits
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 1000m
memory: 2Gi
# BestEffort — resources 블록 자체가 없음
# (resources: 생략)
메모리 부족 시 Eviction 순서
노드의 메모리가 부족(memory pressure)하면 kubelet이 Pod를 퇴거시킨다:
1. BestEffort → 가장 먼저 퇴거 (보장받는 자원이 없으므로)
2. Burstable → requests 초과 사용량이 큰 Pod부터 퇴거
3. Guaranteed → 가장 마지막에 퇴거 (limits까지만 쓰므로 가장 안전)
Burstable 내에서의 순서: requests 대비 실제 사용량의 비율이 높은 Pod가 먼저 퇴거 대상이 된다. 예를 들어 requests 512Mi인데 900Mi를 쓰는 Pod가, requests 1Gi인데 1.2Gi를 쓰는 Pod보다 먼저 퇴거된다.
CGV 적용
CGV prod Pod는 requests(500m/1Gi) < limits(1000m/2Gi)이므로 Burstable QoS이다.
OOM Kill 순서에서 infra Pod(Prometheus 등, Guaranteed)보다 먼저 대상이 되지만, ResourceQuota로 네임스페이스별 총 자원량을 제한하여 노드 전체가 메모리 부족에 빠지는 것을 방어한다
Burstable을 선택한 이유: 평상시에는 requests만큼만 사용하다가 티켓팅 트래픽 폭주 시 limits까지 버스트(burst)할 수 있어야 하므로, Guaranteed(requests == limits)보다 비용 효율적이다.
3.9 Graceful Shutdown
정의: Pod가 종료될 때 진행 중인 작업을 안전하게 마무리하고 종료하는 과정이다. 갑작스러운 종료로 인한 요청 유실, WebSocket 연결 끊김, 데이터 손실을 방지한다.
Pod 종료 시퀀스 (전체 흐름)
1. API Server가 Pod를 Terminating 마킹
2. 동시에 두 가지가 병렬로 발생:
├── a) Endpoint Controller가 Service의 Endpoint에서 해당 Pod를 제거
│ → 새로운 트래픽이 더 이상 이 Pod로 라우팅되지 않음
└── b) kubelet이 Pod 종료 프로세스 시작:
├── 3. preStop Hook 실행 (정의된 경우)
├── 4. SIGTERM 전송 → 앱이 graceful 종료 처리
├── 5. terminationGracePeriodSeconds 동안 대기 (기본 30초)
└── 6. 시간 초과 시 SIGKILL 강제 종료
핵심 타이밍 문제: 2a(Endpoint 제거)와 2b(preStop 시작)는 병렬로 일어난다. Endpoint 제거가 kube-proxy/Ingress Controller에 전파되기까지 약간의 지연이 있으므로, preStop에서 짧은 sleep을 넣어 이미 라우팅된 요청이 도착할 시간을 벌어준다.
preStop Hook
Pod 종료 전 정리 작업을 수행하는 Hook이다.
- 진행 중인 HTTP 요청 완료 대기
- WebSocket 연결 정리 및 클라이언트에게 재연결 알림
- Redis Pub/Sub 구독 해제
- 리더 선출(Leader Election) 해제
spec:
terminationGracePeriodSeconds: 25 # 전체 종료 허용 시간
containers:
- name: cgv-api
image: cgv-api:1.0
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
# 1. Endpoint 제거가 전파될 시간 확보 (새 트래픽 차단 대기)
sleep 5
# 2. 앱에 종료 준비 신호 전송
curl -s -X POST http://localhost:8080/actuator/shutdown || true
# preStop 완료 후 SIGTERM이 전송됨
terminationGracePeriodSeconds
SIGTERM 전송 후 SIGKILL까지 대기하는 시간이다. preStop + SIGTERM 처리 시간의 합이 이 값을 초과하면 안 된다.
terminationGracePeriodSeconds: 25
┌─────────────────────────────────────────────┐
│ preStop (5초) │ SIGTERM 처리 (최대 20초) │ SIGKILL
│ sleep 5 │ 진행중 요청 완료 │ (강제 종료)
│ + shutdown │ + 연결 정리 │
└─────────────────────────────────────────────┘
0초 5초 25초
CGV 설정: terminationGracePeriodSeconds: 25 = preStop 5초 + SIGTERM 처리 20초
deregistration_delay
정의: ALB(Application Load Balancer)가 Pod로의 새 트래픽 전송을 중지하고, 기존에 이미 전달된 요청이 완료되기를 기다리는 시간이다.
ALB Pod (Terminating)
│ │
│ ── 기존 요청 계속 처리 ──► │ ← deregistration_delay 동안
│ ✕ 새 요청 전송 중지 │ 기존 요청만 처리
│ │
│ deregistration_delay 만료 │
│ ── Target Group에서 제거 ─ │
# Ingress annotation으로 설정
metadata:
annotations:
alb.ingress.kubernetes.io/target-group-attributes: >-
deregistration_delay.timeout_seconds=30
CGV 설정: deregistration_delay: 30초. Pod의 terminationGracePeriodSeconds(25초)보다 약간 길게 설정하여 ALB가 Pod 종료 전까지 기존 요청을 처리할 시간을 보장한다.
CGV 적용: KEDA 스케일다운 시 안전한 Pod 종료
KEDA가 트래픽 감소를 감지하여 Pod 수를 축소(스케일다운)할 때, 각 Pod는 위 Graceful Shutdown 시퀀스를 거친다.
핵심은 진행 중인 대기열 세션 처리와 WebSocket 연결 정리를 안전하게 수행하는 것이다.
KEDA: "현재 대기열 크기가 threshold 이하 → Pod 8개 → 4개로 축소"
↓
대상 Pod 4개에 종료 시퀀스 시작
├── 1. Endpoint에서 제거 → 새 사용자는 나머지 4개 Pod로
├── 2. preStop: sleep 5초 (라우팅 전파 대기)
├── 3. SIGTERM: @PreDestroy에서 WebSocket 연결 정리, Pub/Sub 해제
├── 4. 진행 중인 요청 완료 (최대 20초)
└── 5. Pod 종료
이 과정에서 사용자가 체감하는 서비스 중단은 없다. WebSocket 클라이언트는 연결이 끊기면 자동 재연결하여 다른 Pod에 접속한다
4. 워크로드 리소스
Pod는 쿠버네티스의 최소 실행 단위이지만, Pod를 직접 만들면 죽었을 때 복구되지 않는다. 워크로드 리소스는 Pod를 "어떻게 실행하고 유지할 것인가"를 정의한다.
4.1 Deployment
정의: Stateless 애플리케이션을 관리하는 워크로드 리소스이다. 내부적으로 ReplicaSet을 생성하여 Pod의 개수를 유지한다.
핵심 포인트:
- Pod를 직접 만들면 죽었을 때 복구 불가 → Deployment가 대신 관리
- 롤링 업데이트: 새 버전 배포 시 Pod를 점진적으로 교체 (무중단 배포)
- 셀프 힐링: Pod가 죽으면 자동으로 새 Pod 생성
- 스케일링:
replicas수를 변경하면 Pod 개수 자동 조정 - ReplicaSet은 Deployment가 내부적으로 생성하므로 직접 만들지 않음
apiVersion: apps/v1
kind: Deployment
metadata:
name: cgv-api
spec:
replicas: 3
selector:
matchLabels:
app: cgv-api
template:
metadata:
labels:
app: cgv-api
spec:
containers:
- name: cgv-api
image: cgv-api:1.0
ports:
- containerPort: 8080
CGV 적용: cgv-dev/cgv-prod 네임스페이스에서 API 서버를 Deployment로 관리. prod 환경에서는 Argo Rollouts로 카나리 배포 적용
4.2 ReplicaSet
정의: 지정된 수의 Pod 레플리카가 항상 실행되도록 보장하는 리소스이다. Deployment의 내부 메커니즘으로 동작한다.
핵심 포인트:
- Deployment가 새 버전을 배포할 때 새 ReplicaSet을 생성하고, 이전 ReplicaSet의 레플리카를 점진적으로 0으로 줄임
- 이전 ReplicaSet은 삭제되지 않고 남아 있어 롤백 시 재활용됨
kubectl rollout history로 리비전 확인,kubectl rollout undo로 롤백 가능- 직접 생성하지 않는다 — 항상 Deployment를 통해 관리
Deployment (v2 배포)
├── ReplicaSet-v2 (replicas: 3) ← 새 버전, Pod 점진 증가
│ ├── Pod-v2-abc
│ ├── Pod-v2-def
│ └── Pod-v2-ghi
└── ReplicaSet-v1 (replicas: 0) ← 이전 버전, 롤백용 보존
4.3 배포 전략 (Deployment Strategy)
Deployment는 .spec.strategy로 Pod 교체 방식을 결정한다.
Rolling Update (기본값)
정의: 기존 Pod를 점진적으로 새 버전으로 교체하는 전략이다. 서비스 중단 없이 배포가 가능하다.
핵심 파라미터:
| 파라미터 | 설명 | 기본값 | 예시 |
|---|---|---|---|
| maxSurge | 원하는 replicas 수 초과로 생성 가능한 Pod 수 | 25% | 1 또는 25% |
| maxUnavailable | 업데이트 중 사용 불가능한 최대 Pod 수 | 25% | 0 또는 25% |
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 최대 1개 추가 Pod 허용
maxUnavailable: 0 # 항상 원하는 수만큼 가용 보장
maxSurge: 1, maxUnavailable: 0 조합: 새 Pod가 Ready된 후 기존 Pod 제거. 가장 안전하지만 배포 속도 느림, replicas+1 리소스 필요.
Recreate
정의: 모든 기존 Pod를 먼저 삭제한 후 새 버전 Pod를 생성하는 전략이다.
spec:
strategy:
type: Recreate
배포 중 다운타임 발생. 구/신 버전 동시 실행이 불가능한 경우(DB 스키마 변경 등)에만 사용.
Canary 배포 (Argo Rollouts)
쿠버네티스 기본 Deployment에는 Canary 전략이 없다.
Argo Rollouts라는 CRD(Custom Resource Definition)를 사용하여 구현한다.
CGV 적용: prod 환경에서 Canary 배포를 사용한다
- 단계: 10% → 50% → 100%
- 각 단계에서 AnalysisTemplate으로 에러율 5% 이하를 검증
- 검증 실패 시 자동 롤백
# Argo Rollouts Canary (개념 예시 — Deployment가 아닌 Rollout CRD 사용)
apiVersion: argoproj.io/v1alpha1
kind: Rollout
spec:
strategy:
canary:
steps:
- setWeight: 10 # 10% → pause → 50% → pause → 100%
- pause: { duration: 5m }
- setWeight: 50
- pause: { duration: 5m }
analysis:
templates:
- templateName: error-rate # 에러율 5% 초과 시 자동 롤백
배포 전략 비교:
| 전략 | 다운타임 | 리소스 사용 | 롤백 속도 | 사용 시점 |
|---|---|---|---|---|
| Rolling Update | 없음 | 일시적 추가 | kubectl rollout undo | 일반 Stateless 앱 |
| Recreate | 있음 | 동일 | 재배포 필요 | 스키마 변경 등 동시 실행 불가 시 |
| Canary (Argo) | 없음 | 일시적 추가 | 자동 롤백 | 프로덕션 안전 배포 |
4.4 StatefulSet
정의: Stateful 애플리케이션을 관리하는 워크로드 리소스이다. 각 Pod에 고정된 이름과 고유한 스토리지를 부여한다.
핵심 포인트:
- 고정 이름: Pod 이름이
pod-0,pod-1,pod-2형태로 순번 부여 - 순차 시작/종료: 0번부터 순서대로 시작, 역순으로 종료
- 고유 PVC: 각 Pod에 개별 PersistentVolumeClaim이 바인딩됨
- Pod가 재시작되어도 같은 이름과 같은 볼륨을 유지
- Headless Service 필수 — ClusterIP: None인 Service를 통해 각 Pod에 고유 DNS가 부여됨
- DB, Redis, Kafka 등 상태를 가진 앱에 사용
Deployment와의 차이:
| 항목 | Deployment | StatefulSet |
|---|---|---|
| Pod 이름 | 랜덤 (pod-abc123) | 고정 순번 (pod-0, pod-1) |
| 네트워크 ID | 없음 (Service로 묶임) | 고유 DNS (pod-0.svc-name) |
| 스토리지 | 공유 또는 없음 | Pod별 고유 PVC |
| 시작 순서 | 동시 | 순차 |
| 용도 | Stateless (API 서버 등) | Stateful (DB, 캐시 등) |
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: redis-headless # Headless Service 필수
replicas: 3
selector:
matchLabels: { app: redis }
template:
metadata:
labels: { app: redis }
spec:
containers:
- name: redis
image: redis:7
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates: # Pod별 고유 PVC 자동 생성
- metadata: { name: data }
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gp3
resources: { requests: { storage: 10Gi } }
배포 결과: redis-0, redis-1, redis-2 순차 생성, 각각 data-redis-0, data-redis-1, data-redis-2 PVC 바인딩.
CGV 적용: CGV는 Redis를 ElastiCache(관리형)로 사용하므로 StatefulSet으로 직접 운영하지 않는다. dev 환경에서는 Redis Pod를 Deployment로 단일 인스턴스 실행
4.5 DaemonSet
정의: 모든(또는 특정) 노드에 Pod를 1개씩 실행하는 워크로드 리소스이다.
핵심 포인트:
- 노드가 추가되면 자동으로 해당 노드에 Pod 생성
- 노드가 제거되면 해당 Pod도 삭제
- 로그 수집(Fluentd/Fluent Bit), 모니터링 에이전트(Prometheus Node Exporter), 네트워크 플러그인(VPC CNI) 등에 사용
nodeSelector나affinity로 특정 노드에만 배포 가능
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
containers:
- name: node-exporter
image: prom/node-exporter:v1.7.0
ports:
- containerPort: 9100
tolerations:
- operator: Exists # system/infra 노드에도 배포
CGV 적용: monitoring 네임스페이스에서 Prometheus Node Exporter를 DaemonSet으로 배포하여 모든 노드의 CPU/메모리/디스크 메트릭 수집
4.6 Job
정의: 1회성 작업을 실행하는 워크로드 리소스이다. 작업이 성공적으로 완료되면 재시작하지 않는다.
핵심 포인트:
- completions: 성공해야 하는 Pod 수 (기본값 1)
- parallelism: 동시에 실행할 Pod 수 (기본값 1)
- 성공 시 Pod는 Completed 상태로 남음 (삭제되지 않음)
- 실패 시 backoffLimit까지 재시도 (기본값 6)
- activeDeadlineSeconds: 전체 Job 타임아웃. DB 마이그레이션, 배치 처리 등에 사용
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
backoffLimit: 3
activeDeadlineSeconds: 600 # 10분 타임아웃
template:
spec:
containers:
- name: migrate
image: cgv-api:1.0
command: ["java", "-jar", "app.jar", "--migrate"]
restartPolicy: OnFailure
4.7 CronJob
정의: 주기적으로 Job을 생성하여 반복 작업을 실행하는 워크로드 리소스이다.
핵심 포인트:
- cron 표현식으로 스케줄링 (
분 시 일 월 요일) - 내부적으로 Job을 생성하고, Job이 Pod를 생성
- concurrencyPolicy:
Allow(동시 실행),Forbid(스킵),Replace(교체)
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-cleanup
spec:
schedule: "0 3 * * *" # 매일 새벽 3시
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
containers:
- name: cleanup
image: cgv-batch:1.0
command: ["./cleanup.sh"]
restartPolicy: OnFailure
4.8 워크로드 리소스 비교
| 리소스 | 용도 | Pod 수명 | 예시 |
|---|---|---|---|
| Deployment | Stateless 앱 | 계속 실행 | API 서버, 웹 앱 |
| StatefulSet | Stateful 앱 (고정 이름/PVC) | 계속 실행 | DB, Redis, Kafka |
| DaemonSet | 노드당 1개 | 계속 실행 | 로그 수집, 모니터링 |
| Job / CronJob | 1회성 / 주기적 작업 | 완료 시 종료 | DB 마이그레이션, 배치 |
5. 설정과 스토리지
애플리케이션 코드와 설정/데이터를 분리하는 것이 컨테이너 운영의 기본 원칙이다.
설정은 ConfigMap/Secret으로, 영구 데이터는 PV/PVC로 관리한다.
5.1 ConfigMap
정의: 비밀이 아닌 설정값을 키-값 쌍으로 저장하는 리소스이다. 환경변수, 설정파일 등을 Pod와 분리하여 관리한다.
핵심 포인트:
- 이미지를 다시 빌드하지 않고 설정만 변경 가능
- Pod에 환경변수(env)로 주입하거나 Volume mount로 파일 형태로 주입
- 네임스페이스 단위로 관리
- 암호화되지 않으므로 민감한 데이터는 Secret을 사용
- ConfigMap 변경 시 Pod 자동 재시작되지 않음 —
kubectl rollout restart또는 Reloader 등 별도 메커니즘 필요
주입 방식: envFrom/env(환경변수, 변경 시 재시작 필요) vs Volume mount(파일, subPath 미사용 시 변경 감지 가능).
apiVersion: v1
kind: ConfigMap
metadata:
name: cgv-config
data:
# 단순 키-값 (환경변수로 주입)
APP_ENV: "production"
LOG_LEVEL: "info"
MAX_TOTAL_SESSIONS: "5000"
SESSION_TIMEOUT: "600"
# 파일 형태 (Volume mount로 주입)
application.yml: |
server:
port: 8080
spring:
profiles:
active: prod
Pod에서 사용하는 방법:
spec:
containers:
- name: cgv-api
envFrom:
- configMapRef:
name: cgv-config # 모든 키를 환경변수로 주입
volumeMounts:
- name: config-volume
mountPath: /app/config # 파일로 마운트
volumes:
- name: config-volume
configMap:
name: cgv-config
CGV 적용: 환경별(dev/prod) ConfigMap으로 Spring 프로필, Redis 엔드포인트, SESSION_TIMEOUT(dev 300초/prod 600초), MAX_TOTAL_SESSIONS(dev 100/prod 5000) 등을 분리 관리
5.2 Secret
정의: 민감한 데이터(패스워드, 토큰, 인증서 등)를 저장하는 리소스이다. base64로 인코딩되어 저장된다.
핵심 포인트:
- ConfigMap과 사용 방법은 동일 (env 주입, Volume mount)
- base64 인코딩은 암호화가 아님 → 디코딩하면 평문 노출
- 메모리(tmpfs)에 저장되어 디스크에 남지 않음
- 보안 강화: RBAC(접근 제한), etcd 암호화(저장 시 암호화), External Secrets(외부 서비스 동기화)
Secret 유형:
| 유형 | 용도 | 예시 |
|---|---|---|
| Opaque | 일반 키-값 데이터 (기본값) | DB 패스워드, API 키 |
| kubernetes.io/tls | TLS 인증서 (cert + key) | Ingress TLS 종료 |
| kubernetes.io/dockerconfigjson | Docker 레지스트리 인증 정보 | Private Registry 접근 |
apiVersion: v1
kind: Secret
metadata:
name: cgv-secret
type: Opaque
data:
DB_PASSWORD: cGFzc3dvcmQxMjM= # echo -n 'password123' | base64
REDIS_PASSWORD: cmVkaXMxMjM=
stringData 필드를 사용하면 평문으로 작성하고 적용 시 자동 base64 인코딩된다.
External Secrets Operator (ESO)
정의: AWS Secrets Manager, HashiCorp Vault 등 외부 비밀 관리 서비스의 값을 쿠버네티스 Secret으로 자동 동기화하는 오퍼레이터이다.
핵심 포인트:
- ExternalSecret CRD로 어떤 외부 비밀을 어떤 K8s Secret으로 동기화할지 선언
- SecretStore/ClusterSecretStore로 외부 서비스 연결 정보 정의
- 주기적 동기화 → 외부 비밀 변경 시 자동 반영, Git에 Secret 커밋 불필요 (GitOps 적합)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
spec:
refreshInterval: 1h
secretStoreRef: { name: aws-secrets-manager, kind: ClusterSecretStore }
target: { name: cgv-db-secret }
data:
- secretKey: DB_PASSWORD
remoteRef: { key: cgv/production/db, property: password }
CGV 적용: AWS Secrets Manager + ESO로 DB 패스워드, JWT 시크릿 자동 동기화
5.3 PersistentVolume (PV) / PersistentVolumeClaim (PVC)
Pod에 스토리지를 제공하는 Volume 중에서, PV/PVC는 Pod와 독립적인 라이프사이클을 가진 영구 스토리지이다. 임시 공유 용도의 emptyDir, 노드 종속적인 hostPath와 구분된다.
PersistentVolume (PV)
정의: 클러스터에 프로비저닝된 실제 스토리지 리소스이다. 관리자가 생성하거나 StorageClass로 동적 생성된다.
핵심 포인트:
- 클러스터 수준 리소스 (네임스페이스에 속하지 않음)
- 실제 물리 스토리지(EBS, NFS, EFS 등)를 추상화
- Pod와 독립적인 라이프사이클
PersistentVolumeClaim (PVC)
정의: 사용자(Pod)가 스토리지를 요청하는 리소스이다. PV와 바인딩되어 Pod에서 사용한다.
핵심 포인트:
- 네임스페이스 수준 리소스
- 원하는 용량, AccessMode를 지정하면 조건에 맞는 PV와 자동 바인딩
- Pod는 PVC를 통해서만 PV를 사용
PV/PVC 연결 흐름
PV 생성 → PVC 생성(용량/모드 요청) → 바인딩(PV↔PVC) → Pod에서 마운트
# PVC 정의 (동적 프로비저닝 — StorageClass가 PV를 자동 생성)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cgv-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: gp3
---
# Pod에서 PVC 사용
spec:
containers:
- name: app
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: cgv-pvc
AccessModes: RWO(ReadWriteOnce, 단일 노드 읽기/쓰기, EBS), ROX(ReadOnlyMany, 다중 노드 읽기전용), RWX(ReadWriteMany, 다중 노드 읽기/쓰기, EFS 필요).
Reclaim Policy: Retain(PV+데이터 보존, 프로덕션 권장) vs Delete(PV+스토리지 삭제, 동적 프로비저닝 기본값).
5.4 StorageClass
정의: 동적 프로비저닝을 위한 스토리지 "등급"을 정의하는 리소스이다. PVC 생성 시 StorageClass를 지정하면 자동으로 PV가 생성된다.
핵심 포인트:
- provisioner: 어떤 스토리지 드라이버를 사용할지 지정 (예:
ebs.csi.aws.com) - parameters: 볼륨 타입, IOPS, 암호화 등 세부 설정
- reclaimPolicy: PVC 삭제 시 PV 처리 방식
- volumeBindingMode: PVC 생성 시 즉시 바인딩(
Immediate) vs Pod 스케줄링 시 바인딩(WaitForFirstConsumer)
정적 프로비저닝(관리자가 PV 수동 생성)은 운영 부담이 높다. 동적 프로비저닝(StorageClass가 PV 자동 생성)이 프로덕션 표준.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3
provisioner: ebs.csi.aws.com
parameters: { type: gp3, fsType: ext4, encrypted: "true" }
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer # Pod 스케줄 AZ에서 볼륨 생성
[EKS 전용] EBS CSI Driver
정의: EKS에서 EBS 볼륨을 PersistentVolume으로 사용하기 위한 CSI 드라이버이다.
핵심 포인트:
- EKS 1.23+에서 in-tree
aws-ebsprovisioner deprecated → EBS CSI Driver 필수 - EKS Add-on 또는 Helm Chart로 설치, IRSA로 Pod 수준 IAM 권한 부여
- gp3 지원 (gp2 대비 비용 20% 절감, IOPS/처리량 독립 설정)
WaitForFirstConsumer가 중요한 이유: EBS는 단일 AZ에 존재하므로, Pod 스케줄링 전에 볼륨을 만들면 AZ 불일치로 마운트 실패. 이 모드는 Pod가 배치된 AZ에서 볼륨을 생성한다
CGV 적용: EBS CSI Driver + gp3 StorageClass로 Prometheus/Grafana 데이터 PVC 동적 프로비저닝
6. 네트워킹
쿠버네티스 네트워킹의 기본 원칙: 모든 Pod는 NAT 없이 고유 IP를 갖고, 모든 Pod가 서로 직접 통신할 수 있다.
이 위에 Service, Ingress, NetworkPolicy로 접근 제어와 로드밸런싱을 구축한다.
6.1 Service
정의: Pod에 고정된 접근점(IP, DNS)을 제공하고, 여러 Pod에 대한 로드밸런싱을 수행하는 리소스이다.
왜 필요한가:
- Pod IP는 Pod가 재생성될 때마다 변경됨
- Service는 고정 IP(ClusterIP)와 DNS 이름을 제공
selector로 대상 Pod를 선택하여 트래픽을 분배
Service 유형
| 유형 | 설명 | 접근 범위 |
|---|---|---|
| ClusterIP | 클러스터 내부 가상 IP (기본값) | 내부 |
| NodePort | 노드 포트(30000-32767)로 외부 노출 | 외부 |
| LoadBalancer | 클라우드 LB 자동 생성 | 외부 |
| ExternalName | 외부 서비스에 CNAME 부여 | 외부 → 내부 |
| Headless | ClusterIP: None, 개별 Pod IP 직접 반환 | 내부 |
# ClusterIP (기본값) — 내부 통신용
apiVersion: v1
kind: Service
metadata:
name: cgv-api-service
spec:
type: ClusterIP
selector:
app: cgv-api
ports:
- port: 80 # Service 포트
targetPort: 8080 # Pod 포트
---
# Headless — StatefulSet용, 개별 Pod DNS 제공
apiVersion: v1
kind: Service
metadata:
name: redis-headless
spec:
clusterIP: None
selector:
app: redis
ports:
- port: 6379
Headless Service는 StatefulSet에서 각 Pod에 개별 DNS를 부여할 때 사용
CGV 적용: API 서버는 ClusterIP Service로 내부 통신, Ingress를 통해 외부 노출. Redis(dev)는 ClusterIP Service로 접근
6.2 Ingress
정의: URL 경로나 도메인 기반으로 HTTP/HTTPS 트래픽을 클러스터 내부 Service로 라우팅하는 L7 리소스이다.
핵심 포인트:
- LoadBalancer Service는 서비스마다 LB 1개(L4, 비용 높음) → Ingress는 LB 1개로 여러 서비스를 경로/호스트 기반 라우팅(L7, 비용 절감)
- Ingress Controller가 필수 — Ingress 리소스 자체는 "규칙 정의"일 뿐, 실제 트래픽 처리는 Controller가 담당
- TLS 종료, 경로 기반 라우팅, 호스트 기반 라우팅 지원
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cgv-ingress
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
spec:
rules:
- host: api.cgv.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: cgv-api-service
port: { number: 80 }
- path: /ws
pathType: Prefix
backend:
service:
name: cgv-websocket-service
port: { number: 80 }
tls:
- hosts: [api.cgv.com]
secretName: cgv-tls-cert
[EKS 전용] AWS Load Balancer Controller
정의: 쿠버네티스 Ingress/Service 리소스를 감지하여 실제 AWS ALB(Application Load Balancer) 또는 NLB(Network Load Balancer)를 프로비저닝하고 설정하는 컨트롤러이다.
핵심 포인트:
- IngressClass:
alb를 지정하면 이 컨트롤러가 동작 - TargetGroupBinding: 쿠버네티스 Service와 AWS Target Group 간의 연결을 관리하는 CRD
- IP Mode vs Instance Mode:
| 모드 | 트래픽 흐름 | 장점 | 단점 |
|---|---|---|---|
| Instance | ALB → NodePort → kube-proxy → Pod | 설정 간단 | 추가 홉, 지연 증가 |
| IP | ALB → Pod IP 직접 | 홉 감소, 지연 최소화 | VPC CNI 필수 |
동작 흐름: Ingress 배포 → Controller가 Watch → AWS API로 ALB/Target Group 생성 → Pod IP 등록 (IP Mode) → Route53 연동.
주요 annotation: scheme(internet-facing/internal), target-type(ip/instance), certificate-arn(ACM 인증서), healthcheck-path, target-group-attributes(deregistration_delay.timeout_seconds=30) 등.
CGV 적용: target-type: ip로 ALB→Pod 직접 트래픽 전달. deregistration_delay 30초로 Graceful Shutdown 연동
참고 — Gateway API는 Ingress의 차세대 표준으로, HTTP/TCP/UDP/gRPC를 지원하고 GatewayClass→Gateway→HTTPRoute 3단계 역할 분리를 제공한다. CGV는 현재 Ingress + ALB Controller를 사용한다.
6.3 NetworkPolicy
정의: Pod 간 네트워크 통신을 제어하는 방화벽 역할의 리소스이다.
핵심 포인트:
- 기본 동작: NetworkPolicy가 없으면 모든 Pod 간 통신이 허용 (all allow)
- NetworkPolicy를 적용하면 해당 Pod에 대해 명시적으로 허용한 트래픽만 통과
- ingress (들어오는 트래픽)와 egress (나가는 트래픽) 규칙을 각각 설정
- 네임스페이스, Pod 레이블, IP 블록(CIDR)으로 대상 지정
- CNI 플러그인이 NetworkPolicy를 지원해야 동작 (모든 CNI가 지원하진 않음)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-policy
namespace: cgv-prod
spec:
podSelector:
matchLabels:
app: cgv-api
policyTypes: [Ingress, Egress]
ingress:
- from: # 같은 네임스페이스
- namespaceSelector:
matchLabels: { name: cgv-prod }
ports: [{ port: 8080 }]
- from: # monitoring (메트릭 수집)
- namespaceSelector:
matchLabels: { name: monitoring }
ports: [{ port: 8080 }]
egress:
- to: # Redis (DB Private Subnet)
- ipBlock: { cidr: 10.0.30.0/24 }
ports: [{ port: 6379 }]
- to: # DNS (필수 — 없으면 서비스 디스커버리 불가)
- namespaceSelector: {}
ports: [{ port: 53, protocol: UDP }, { port: 53, protocol: TCP }]
주의: egress 규칙을 설정할 때 DNS(포트 53) 허용을 잊으면 Pod가 Service 이름을 해석하지 못해 모든 내부 통신이 실패한다.
[EKS 전용] VPC CNI NetworkPolicy
정의: EKS의 VPC CNI 플러그인이 자체적으로 NetworkPolicy를 적용하는 기능이다. 별도의 Calico 설치 없이 NetworkPolicy를 사용할 수 있다.
핵심 포인트:
- EKS VPC CNI v1.14.0+에서 지원
- eBPF 기반으로 동작 — iptables 대비 높은 성능
- 별도 CNI(Calico, Cilium)를 추가 설치하지 않아도 됨
- EKS Add-on 설정에서
"enableNetworkPolicy": "true"로 활성화
CGV 적용: VPC CNI NetworkPolicy로 cgv-prod 네임스페이스의 API Pod는 같은 네임스페이스 + monitoring에서만 ingress 허용. DB Private Subnet CIDR로 Redis/RDS egress 제한
6.4 DNS와 서비스 디스커버리
CoreDNS
정의: 쿠버네티스 클러스터 내부의 DNS 서버 역할을 하는 필수 애드온이다. Service 이름을 ClusterIP로 해석(resolve)한다.
핵심 포인트:
- Service Discovery의 핵심: Pod가 다른 서비스를 IP가 아닌 이름으로 찾게 해주는 주체
- Deployment 형태: kube-system 네임스페이스에 보통 2개 이상의 레플리카로 실행 (고가용성)
- 모든 Pod의
/etc/resolv.conf에 CoreDNS의 ClusterIP가 nameserver로 설정됨
FQDN 구조
쿠버네티스 내부 DNS는 <service>.<namespace>.svc.cluster.local 형식을 따른다.
Service DNS 해석 예시:
같은 네임스페이스: curl cgv-api-service → 자동 확장
다른 네임스페이스: curl cgv-api-service.cgv-prod → namespace 명시
FQDN 전체: curl cgv-api-service.cgv-prod.svc.cluster.local. → "." 으로 검색 도메인 무시
ndots 설정
Pod의 /etc/resolv.conf에는 nameserver(CoreDNS IP), search(검색 도메인 목록), ndots:5가 설정된다.
ndots:5 의미: 쿼리 이름에 점(.)이 5개 미만이면 search 도메인을 순서대로 붙여서 시도한다. cgv-api-service(점 0개)는 첫 번째 search에서 hit하지만, google.com(점 1개)은 3회 불필요한 쿼리 후 외부 DNS에서 해석된다. 외부 도메인 조회가 많으면 dnsConfig로 ndots를 낮추거나 FQDN 끝에 .을 추가한다.
Corefile 구성
CoreDNS의 설정은 kube-system 네임스페이스의 coredns ConfigMap에 Corefile 형태로 저장된다.
.:53 {
kubernetes cluster.local { ... } # K8s Service/Pod DNS 레코드
forward . /etc/resolv.conf # 외부 DNS 포워딩 (EKS: VPC DNS)
cache 30 # DNS 캐시 30초
prometheus :9153 # 메트릭 노출
health / ready / errors / loop / reload / loadbalance
}
Headless Service와 DNS
ClusterIP: None인 Headless Service는 로드밸런싱용 가상 IP를 생성하지 않는다. 대신 DNS 쿼리 시 개별 Pod의 IP를 직접 반환한다.
# 일반 Service: ClusterIP 1개 반환
dig cgv-api-service.cgv-prod.svc.cluster.local → 10.100.45.123
# Headless Service: 모든 Pod IP 반환 (로드밸런싱 없음)
dig redis-headless.cgv-prod.svc.cluster.local → 10.0.10.5, 10.0.20.8, 10.0.10.12
# StatefulSet 개별 Pod: 특정 Pod IP만 반환
dig redis-0.redis-headless.cgv-prod.svc.cluster.local → 10.0.10.5
StatefulSet + Headless Service에서 각 Pod는 <pod-name>.<headless-service>.<namespace>.svc.cluster.local 형태의 고유 DNS를 갖는다. Pod 재시작으로 IP가 변경되어도 DNS 이름은 유지된다.
DNS 질의 흐름 (전체)
Pod A: curl cgv-api-service
→ /etc/resolv.conf (nameserver: CoreDNS IP, ndots:5)
→ CoreDNS: kubernetes 플러그인으로 cgv-api-service.cgv-prod.svc.cluster.local 해석
├── K8s Service → ClusterIP 반환
└── 매칭 안 되면 forward → VPC DNS (AWS 서비스 도메인: RDS, ElastiCache 등)
→ Pod A: ClusterIP로 패킷 전송 → kube-proxy가 실제 Pod IP로 DNAT
CGV 적용:
- 내부 서비스: Pod 간 통신은 CoreDNS가 자동 해석.
cgv-api-service로 접근하면 ClusterIP가 반환됨 - 외부 서비스: Redis(ElastiCache), RDS 등 AWS 관리형 서비스는 ConfigMap에 호스트명(
xxx.cache.amazonaws.com,xxx.rds.amazonaws.com)을 저장하고, CoreDNS의forward플러그인이 VPC DNS를 통해 해석 - Prometheus가 ServiceMonitor로 대상 Pod를 찾을 때도 CoreDNS를 통한 서비스 디스커버리를 사용
6.5 Namespace
정의: 클러스터 내 리소스를 논리적으로 분리하는 가상의 구획이다.
- 팀/환경/프로젝트별 리소스 격리. 리소스 이름은 네임스페이스 내에서만 유일하면 됨
- ResourceQuota, LimitRange, NetworkPolicy와 함께 사용하여 자원 제한/네트워크 격리
- 기본 네임스페이스:
default,kube-system,kube-public,kube-node-lease
CGV 적용: kube-system(CoreDNS, VPC CNI, EBS CSI), argocd(GitOps), monitoring(Prometheus, Grafana, Loki), keda(KEDA), argo-rollouts(카나리), cgv-dev(개발), cgv-prod(운영) — 7개 네임스페이스.
7. 스케줄링과 리소스 관리
Pod를 어떤 노드에 배치하고, 리소스를 어떻게 제한/분배할 것인지를 결정하는 메커니즘이다. 노드에는 Taint/Label을 설정하고, 나머지(Affinity, TopologySpread, PriorityClass 등)는 Pod(Deployment) 스펙에서 선언한다.
7.1 nodeSelector
정의: 가장 단순한 Pod 스케줄링 제약 조건이다. Pod에 nodeSelector를 지정하면 해당 레이블을 가진 노드에만 배치된다.
핵심 포인트:
- 노드에 레이블을 붙이고, Pod의
nodeSelector에 동일 레이블을 지정 - 조건에 맞는 노드가 없으면 Pod는 Pending 상태로 대기
- 가장 간단하지만, 복잡한 조건(OR, 선호도 등)은 표현 불가 → Node Affinity 사용
apiVersion: v1
kind: Pod
metadata:
name: cgv-api
spec:
nodeSelector:
role: app # role=app 레이블이 있는 노드에만 배치
containers:
- name: api
image: cgv-api:latest
CGV 적용: role=infra 노드에 모니터링/ArgoCD 배치, role=app 노드에 API 서버 배치 (→ 2.1 섹션 10.9).
7.2 Node Affinity
정의: nodeSelector의 확장판으로, 더 풍부한 표현식으로 노드 배치 조건을 지정하는 기능이다.
두 가지 규칙:
| 규칙 | 약칭 | 동작 |
|---|---|---|
| requiredDuringSchedulingIgnoredDuringExecution | Hard | 반드시 조건 충족 노드에 배치. 조건 맞는 노드 없으면 Pending |
| preferredDuringSchedulingIgnoredDuringExecution | Soft | 가능하면 조건 충족 노드에 배치, 없으면 다른 노드에도 배치 |
사용 가능 연산자: In, NotIn, Exists, DoesNotExist, Gt, Lt
nodeSelector vs Node Affinity 비교:
| 항목 | nodeSelector | Node Affinity |
|---|---|---|
| 표현력 | key=value 일치만 | In, NotIn, Exists, Gt, Lt 등 |
| 필수/선호 | 필수만 | requiredDuring... (필수) + preferredDuring... (선호) |
| weight 지정 | 불가 | preferred에 weight(1-100) 부여 가능 |
| 용도 | 단순한 노드 지정 | 복잡한 조건/우선순위 필요 시 |
apiVersion: v1
kind: Pod
metadata:
name: cgv-api
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # Hard: 반드시 prod 노드
nodeSelectorTerms:
- matchExpressions:
- key: environment
operator: In
values:
- prod
preferredDuringSchedulingIgnoredDuringExecution: # Soft: 가능하면 AZ-a 선호
- weight: 1
preference:
matchExpressions:
- key: zone
operator: In
values:
- ap-northeast-2a
containers:
- name: api
image: cgv-api:latest
CGV 적용: prod Pod는 environment=prod 노드 그룹에 Hard Affinity로 반드시 배치. Karpenter NodePool의 requirements도 동일한 레이블 기반으로 노드를 생성한다
7.3 Taint & Toleration
정의: Taint는 노드에 "거부 표시"를 붙이는 것이고, Toleration은 Pod에 "해당 Taint를 감수하겠다"고 선언하는 것이다.
Taint Effect 종류:
| Effect | 동작 |
|---|---|
| NoSchedule | 새 Pod 배치 금지. 기존 Pod에는 영향 없음 |
| PreferNoSchedule | 가능하면 배치하지 않음. 다른 선택지가 없으면 배치 허용 |
| NoExecute | 새 Pod 배치 금지 + 기존 Pod도 퇴거(Evict). tolerationSeconds로 유예 시간 지정 가능 |
nodeSelector/Affinity vs Taint/Toleration 비교:
| 항목 | nodeSelector / Affinity (파드) | Taint(노드) / Toleration(파드) |
|---|---|---|
| 방향 | Pod -> Node ("이 노드에 가고 싶다") | Node -> Pod ("이 노드에 오지 마라") |
| 설정 위치 | Pod 스펙 | Taint는 노드, Toleration은 Pod |
| 목적 | 특정 노드에 Pod 유인 | 특정 노드에서 Pod 배제 |
왜 둘 다 필요한가:
- Taint만 설정하면 → Toleration이 있는 Pod는 들어오지만, 그 Pod가 Taint가 없는 다른 노드에도 자유롭게 배치됨
- nodeSelector만 설정하면 → 그 Pod가 해당 노드에는 가지만, 다른 Pod(노드 셀렉터가 없는)가 해당 노드에 침범하는 것을 막지 못함
- Taint(출입 금지) + nodeSelector(목적지 지정) = 완벽한 격리
# 노드에 Taint 추가 (kubectl 명령)
# kubectl taint nodes system-node dedicated=system:NoSchedule
# Pod에 Toleration 추가
apiVersion: v1
kind: Pod
metadata:
name: monitoring-agent
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "system"
effect: "NoSchedule"
nodeSelector:
role: system # Taint + nodeSelector 세트로 사용
containers:
- name: agent
image: prometheus-agent:latest
CGV 적용: system 노드에 dedicated=system:NoSchedule Taint를 설정하여 일반 워크로드 배치를 방지하고, 시스템 컴포넌트(Karpenter, CoreDNS)만 Toleration으로 배치
7.4 Pod Affinity / Anti-Affinity
정의: Pod 간의 관계를 기반으로 스케줄링을 제어하는 기능이다. 특정 Pod와 같은 위치(Affinity) 또는 다른 위치(Anti-Affinity)에 배치한다.
핵심 포인트:
- Pod Affinity: 특정 Pod가 있는 노드/존에 함께 배치 → 레이턴시 최소화
- Pod Anti-Affinity: 특정 Pod가 있는 노드/존을 피해서 배치 → 고가용성
topologyKey로 범위 지정:kubernetes.io/hostname= 노드 수준 (같은 노드 / 다른 노드)topology.kubernetes.io/zone= AZ 수준 (같은 AZ / 다른 AZ)
- Node Affinity와 동일하게 required(Hard) / preferred(Soft) 지원
apiVersion: apps/v1
kind: Deployment
metadata:
name: cgv-api
spec:
replicas: 3
selector:
matchLabels:
app: cgv-api
template:
metadata:
labels:
app: cgv-api
spec:
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cgv-redis
topologyKey: topology.kubernetes.io/zone # Redis와 같은 AZ 선호
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cgv-api
topologyKey: kubernetes.io/hostname # API Pod끼리 다른 노드 필수
containers:
- name: api
image: cgv-api:latest
CGV 적용: API Pod는 같은 AZ에 Redis와 함께 배치(Pod Affinity → 레이턴시 최소화), 같은 Deployment의 API Pod끼리는 다른 노드에 분산 배치(Pod Anti-Affinity → 노드 장애 시 전멸 방지)
7.5 TopologySpreadConstraints
정의: Pod를 토폴로지 도메인(AZ, 노드 등)에 균등하게 분산 배치하는 제약 조건이다.
핵심 포인트:
maxSkew: 도메인 간 Pod 수의 최대 허용 차이whenUnsatisfiable:DoNotSchedule(조건 미충족 시 Pending) vsScheduleAnyway(최대한 균등하되, 불가능하면 허용)- Pod Anti-Affinity와의 차이:
- Anti-Affinity: "같은 곳을 피한다" (이진법: 있다/없다)
- TopologySpreadConstraints: "균등하게 나눈다" (숫자로 제어: maxSkew)
- Pod 10개를 여러 노드/AZ에 퍼뜨려야 할 때 Anti-Affinity보다 세밀한 분산 제어가 가능
apiVersion: apps/v1
kind: Deployment
metadata:
name: cgv-api
spec:
replicas: 6
selector:
matchLabels:
app: cgv-api
template:
metadata:
labels:
app: cgv-api
spec:
topologySpreadConstraints:
- maxSkew: 1 # AZ 간 Pod 수 차이 최대 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: cgv-api
containers:
- name: api
image: cgv-api:latest
CGV 적용: 2개 AZ에 API Pod를 균등 분산하여 AZ 장애 시 영향 최소화. 6개 Pod면 각 AZ에 3개씩 배치
7.6 PriorityClass / Preemption
정의: Pod에 우선순위를 부여하는 리소스이다. 리소스가 부족할 때 낮은 우선순위 Pod를 Preemption(선점, 강제 퇴거)하고 높은 우선순위 Pod를 먼저 배치한다.
핵심 포인트:
value: 우선순위 숫자 (클수록 높은 우선순위, 범위: -2147483648 ~ 1000000000)preemptionPolicy:PreemptLowerPriority(기본값, 낮은 우선순위 Pod 퇴거) vsNever(선점 안 함, 대기만)globalDefault: true면 priorityClassName을 지정하지 않은 모든 Pod에 이 PriorityClass 적용- 시스템 기본 PriorityClass:
system-cluster-critical(2000000000),system-node-critical(2000001000)
Preemption 동작 순서:
- 스케줄러가 고우선순위 Pod를 배치할 노드를 찾지 못함
- 각 노드에서 "어떤 저우선순위 Pod를 제거하면 이 Pod가 들어가는지" 계산
- 최소한의 Pod를 제거(Evict)하여 고우선순위 Pod를 배치
- 제거된 Pod는 다른 노드에 재스케줄링 (가능한 경우)
# PriorityClass 정의
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: cgv-critical
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "CGV API 서버 등 핵심 워크로드용"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: cgv-batch
value: 100
globalDefault: false
preemptionPolicy: Never # 다른 Pod를 쫓아내지 않고 대기
description: "CGV 배치 작업용 (낮은 우선순위)"
---
# Pod에서 사용
apiVersion: v1
kind: Pod
metadata:
name: cgv-api
spec:
priorityClassName: cgv-critical
containers:
- name: api
image: cgv-api:latest
CGV 적용: API 서버는 cgv-critical, 배치 작업(데이터 처리 등)은 cgv-batch로 지정하여 리소스 경합 시 API 서버가 우선 배치. Karpenter 환경에서는 Pending Pod 감지 시 새 노드를 프로비저닝하므로 Preemption보다 노드 추가가 먼저 발생하지만, 노드 프로비저닝 한도(limits.cpu)에 도달하면 PriorityClass가 작동한다 (→ 2.1 섹션 10.9).
7.7 스케줄링 전체 흐름 (CGV 예시)
CGV Prod Pod(toleration env=prod) 1개를 배치한다고 가정하자. K8s 스케줄러가 순서대로 필터링한다.
노드 5개가 있다:
Node A: system (Karpenter, CoreDNS)
Node B: infra (ArgoCD, Prometheus)
Node C: dev (dev Backend, dev Redis)
Node D: prod (prod Backend 2개 이미 있음)
Node E: prod (비어있음)
Step 1: Taint/Toleration — "갈 수 있는 노드가 어디야?"
질문: "문 잠긴 노드는 어디?"
Node A: Taint dedicated=system → toleration 없으면 -- 출입 금지
Node B: Taint role=infra → toleration 없으면 -- 출입 금지
Node C: Taint env=dev → toleration 없으면 -- 출입 금지
Node D: Taint env=prod → toleration env=prod 있음 -> 출입 가능
Node E: Taint env=prod → toleration env=prod 있음 -> 출입 가능
결과: [D, E] 만 후보로 남음
역할: 소거법. "못 가는 곳"을 먼저 제거
Step 2: nodeSelector / nodeAffinity — "가고 싶은 노드가 어디야?"
질문: "남은 후보 중에 내가 원하는 노드는?"
nodeSelector: env=prod
Node D: env=prod -> 일치
Node E: env=prod -> 일치
결과: [D, E] 유지
역할: 지목. "가고 싶은 곳"을 지정
※ Taint만 있고 nodeSelector 없으면?
→ toleration으로 D,E 갈 수 있지만, Taint 없는 다른 노드에도 갈 수 있음
→ 그래서 둘 다 써야 완벽
Step 3: podAntiAffinity — "같이 있기 싫은 Pod가 있어?"
질문: "이미 있는 Pod와의 관계는?"
antiAffinity: app=cgv-api와 같은 hostname 피해
Node D: cgv-api Pod 2개 있음 → 피하고 싶음
Node E: cgv-api Pod 없음 → 깨끗
결과: [E] 선택
역할: 분산. 같은 Pod끼리 다른 노드로 흩어짐
Step 4: TopologySpreadConstraints — "균등하게 퍼져있어?"
질문: "AZ별로 균형 맞나?"
AZ-a: Node D (Pod 2개)
AZ-c: Node E (Pod 0개)
maxSkew: 1 → 차이가 2면 초과
결과: Node E (AZ-c)에 배치해서 균형 맞춤
역할: 균등 분배. AntiAffinity보다 세밀한 숫자 제어
Step 5: PriorityClass — "자리가 없으면 누굴 쫓아내?"
질문: "모든 노드가 꽉 찼다면?"
cgv-api (priority: 1000000) vs batch-job (priority: 100)
→ batch-job을 쫓아내고 cgv-api 배치
역할: 비상 시 우선순위. 평소엔 작동 안 함
전체 흐름 ASCII:
Pod 배치 요청
|
v
(1) Taint/Toleration --- "못 가는 노드 제거" [A,B,C,D,E] -> [D,E]
|
v
(2) nodeSelector ------- "가고 싶은 노드 지정" [D,E] -> [D,E]
|
v
(3) podAntiAffinity ---- "같은 Pod 피하기" [D,E] -> [E]
|
v
(4) TopologySpread ----- "AZ 균등 분배" [E] -> [E]
|
v
(5) PriorityClass ------ "자리 없으면 쫓아내기" (평소엔 안 씀)
|
v
배치 완료: Node E
비유 정리:
(1) Taint/Toleration = 건물 출입증 "이 출입증 없으면 이 건물 못 들어감"
(2) nodeSelector = 지원서 "나는 이 건물에서 일하고 싶습니다"
(3) podAntiAffinity = 사내 규칙 "같은 팀원끼리 같은 층 금지 (장애 시 전멸 방지)"
(4) TopologySpread = 인사팀 배치 "각 층마다 인원 비슷하게 맞춰"
(5) PriorityClass = 직급 "자리 부족하면 인턴부터 재배치"
(1)(2)는 항상 세트로 사용 (막고 + 보내고)
(3)(4)는 고가용성이 필요할 때
(5)는 리소스 부족 비상 시
7.8 ResourceQuota
정의: 네임스페이스별 리소스 사용량의 상한(총량)을 설정하는 리소스이다.
핵심 포인트:
- CPU, 메모리, Pod 수, Service 수, PVC 수 등 네임스페이스 전체 합계 제한
- 한 팀/환경이 클러스터 자원을 독점하는 것을 방지
- ResourceQuota가 설정된 네임스페이스에서는 모든 Pod가 requests/limits를 명시해야 함 (미명시 시 생성 거부 → LimitRange로 기본값 설정 필요)
- 여러 ResourceQuota를 하나의 네임스페이스에 적용 가능 (합산)
제한 가능 항목:
| 카테고리 | 항목 예시 |
|---|---|
| Compute | requests.cpu, requests.memory, limits.cpu, limits.memory |
| Object Count | pods, services, configmaps, secrets, persistentvolumeclaims |
| Storage | requests.storage, .storageclass.storage.k8s.io/requests.storage |
apiVersion: v1
kind: ResourceQuota
metadata:
name: cgv-dev-quota
namespace: cgv-dev
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "20"
services: "10"
persistentvolumeclaims: "5"
# prod는 더 큰 쿼타
apiVersion: v1
kind: ResourceQuota
metadata:
name: cgv-prod-quota
namespace: cgv-prod
spec:
hard:
requests.cpu: "16"
requests.memory: 32Gi
limits.cpu: "32"
limits.memory: 64Gi
pods: "50"
CGV 적용: cgv-dev는 작은 쿼타(CPU 4코어, 메모리 8Gi), cgv-prod는 큰 쿼타(CPU 16코어, 메모리 32Gi)로 환경별 자원 관리.
3중 방어: Karpenter limits(노드 총량) + ResourceQuota(네임스페이스 총량) + LimitRange(개별 Pod 상한)
7.9 LimitRange
정의: 네임스페이스 내 개별 Pod/Container의 기본 리소스 제한을 설정하는 리소스이다.
핵심 포인트:
- ResourceQuota가 "네임스페이스 전체 합계"를 제한한다면, LimitRange는 "개별 Pod/Container"를 제한
default: 기본 limits 값 (미지정 시 자동 적용)defaultRequest: 기본 requests 값 (미지정 시 자동 적용)min: 최소 허용 리소스 (이보다 작으면 생성 거부)max: 최대 허용 리소스 (이보다 크면 생성 거부)- requests/limits를 지정하지 않은 Pod에 기본값이 자동 적용 → ResourceQuota가 있을 때 필수 동반자
apiVersion: v1
kind: LimitRange
metadata:
name: cgv-limit-range
namespace: cgv-prod
spec:
limits:
- type: Container
default: # limits 기본값
cpu: 500m
memory: 512Mi
defaultRequest: # requests 기본값
cpu: 100m
memory: 128Mi
min: # 최소값
cpu: 50m
memory: 64Mi
max: # 최대값
cpu: "2"
memory: 2Gi
ResourceQuota vs LimitRange 비교:
| 항목 | ResourceQuota | LimitRange |
|---|---|---|
| 제한 대상 | 네임스페이스 전체 합계 | 개별 Pod/Container |
| 설정 범위 | CPU, 메모리, Pod 수, Service 수 등 | CPU, 메모리의 기본값/최소/최대 |
| 목적 | 전체 자원 독점 방지 | 개별 Pod의 과도한 자원 요청 방지 |
| 기본값 적용 | 불가 | 가능 (requests/limits 미지정 시 자동 적용) |
| 생성 거부 | 총량 초과 시 | min 미만 또는 max 초과 시 |
CGV 리소스 관리 3중 방어 체계:
[1단계] Karpenter NodePool limits
- 노드 레벨: CPU 총 1000코어 이하로 노드 생성 제한
- "클러스터 전체에서 인프라 비용 폭주 방지"
[2단계] ResourceQuota
- 네임스페이스 레벨: cgv-dev CPU 8코어 / cgv-prod CPU 32코어
- "한 환경이 전체 클러스터를 독점하는 것 방지"
[3단계] LimitRange
- 컨테이너 레벨: 최대 CPU 2코어, 메모리 2Gi
- "하나의 Pod가 네임스페이스 쿼타를 독식하는 것 방지"
CGV 적용:
- Karpenter limits → 과도한 노드 프로비저닝 차단
- ResourceQuota → dev/prod 네임스페이스별 자원 총량 통제
- LimitRange → requests/limits 누락 Pod에 기본값 자동 주입, 개별 상한 강제.
8. 보안
쿠버네티스의 보안은 인증(Authentication) → 인가(Authorization) → 어드미션 컨트롤(Admission Control) 3단계로 구성된다. 이 섹션에서는 Pod의 신원 관리, AWS 권한 연동, RBAC, Pod 보안 정책을 다룬다.
8.1 ServiceAccount
정의: Pod에 ID(신원)를 부여하는 리소스이다. Pod가 쿠버네티스 API 서버에 접근할 때 인증 수단으로 사용된다.
핵심 포인트:
- 네임스페이스마다 기본
defaultServiceAccount가 자동 생성 - Pod에 별도 ServiceAccount를 지정하지 않으면
default사용 automountServiceAccountToken: false로 불필요한 토큰 마운트를 방지하여 보안 강화- ServiceAccount별로 RBAC 권한을 다르게 부여 가능
- K8s 1.24+에서는 토큰이 시간 제한(bound) 토큰으로 자동 발급 (기존 무기한 Secret 토큰 폐지)
apiVersion: v1
kind: ServiceAccount
metadata:
name: cgv-api-sa
namespace: cgv-prod
automountServiceAccountToken: true
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cgv-api
namespace: cgv-prod
spec:
template:
spec:
serviceAccountName: cgv-api-sa # 전용 ServiceAccount 지정
containers:
- name: api
image: cgv-api:latest
CGV 적용: 각 앱별로 전용 ServiceAccount를 생성하여 최소 권한 원칙(Principle of Least Privilege)을 적용. cgv-api-sa, keda-operator-sa, argo-rollouts-sa 등 역할별 분리
8.2 IRSA (IAM Roles for Service Accounts) [EKS 전용]
정의: 쿠버네티스 ServiceAccount와 AWS IAM Role을 연결하여 Pod별로 세밀한 AWS 권한을 부여하는 메커니즘이다.
핵심 포인트:
- 노드의 IAM Role을 공유하는 대신, Pod별로 독립적인 AWS 권한 부여 가능
- 최소 권한 원칙 구현에 핵심
- 임시 토큰 방식 → 토큰이 자동 갱신되며, 유출 시에도 시간 제한으로 피해 최소화
동작 원리:
1. EKS 클러스터에 OIDC Provider 설정 (Terraform으로 자동 생성)
2. IAM Role의 Trust Policy에 OIDC Provider + ServiceAccount 조건 추가
3. ServiceAccount에 IAM Role ARN 애너테이션 추가
4. Pod 생성 시 EKS Webhook이 AWS STS 임시 토큰을 자동 주입
5. Pod 내 AWS SDK가 임시 토큰으로 AWS API 호출
┌───────────────┐
│ AWS STS │
│ (임시 토큰 발급) │
└───────┬───────┘
│ 3. 임시 토큰 반환
1. OIDC 토큰 제시 │
┌──────────┐ ┌─────┴──────┐ ┌──────────────┐
│ Pod │──────────│ EKS OIDC │──────────│ IAM Role │
│ (SA 연결) │ │ Provider │ │ (Trust Policy)│
└──────────┘ └────────────┘ └──────────────┘
│
│ 4. 임시 토큰으로 AWS API 호출
v
┌──────────────┐
│ AWS Service │
│ (ECR, S3 등) │
└──────────────┘
apiVersion: v1
kind: ServiceAccount
metadata:
name: cgv-api-sa
namespace: cgv-prod
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/cgv-api-role
CGV 적용 IRSA Role 목록:
| ServiceAccount | IAM Role | 용도 |
|---|---|---|
| cgv-api-sa | cgv-api-role | SQS 접근 (Dead Letter Queue) |
| keda-operator-sa | cgv-keda-role | CloudWatch/Prometheus 메트릭 조회 |
| aws-load-balancer-controller-sa | cgv-alb-controller-role | ALB 생성/관리 |
| external-dns-sa | cgv-external-dns-role | Route53 레코드 관리 |
| gitlab-runner-sa | cgv-gitlab-runner-ecr-role | ECR Push/Pull |
8.3 Role / RoleBinding (RBAC - namespace)
정의: RBAC(Role-Based Access Control)의 핵심 리소스이다.
*RBAC : "누가 K8s API에서 무엇을 할 수 있는지를 정의"
Role은 "네임스페이스 내"에서 "무엇을 할 수 있는지" 권한을 정의하고,
RoleBinding은 "누가" 해당 Role을 가지는지 연결한다.
핵심 포인트:
- Role: resources(대상 리소스) + verbs(허용 동작)로 권한 정의
- RoleBinding: subjects(사용자/그룹/ServiceAccount)와 Role을 연결
- verbs 종류:
get,list,watch,create,update,patch,delete - apiGroups:
""= core API (Pod, Service, ConfigMap),"apps"= apps API (Deployment, StatefulSet)
# Role 정의: cgv-prod 네임스페이스에서 Pod 읽기 전용
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: cgv-prod
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"] # 로그 조회도 허용
---
# RoleBinding: cgv-api-sa에 pod-reader Role 연결
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: cgv-prod
subjects:
- kind: ServiceAccount
name: cgv-api-sa
namespace: cgv-prod
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
8.4 ClusterRole / ClusterRoleBinding (RBAC - Cluster)
정의: Role의 클러스터 전체 버전이다. 네임스페이스에 속하지 않는 리소스(Node, PersistentVolume 등)에 대한 권한을 정의하거나, 모든 네임스페이스에 걸친 권한을 부여할 때 사용한다.
Role vs ClusterRole 비교:
| 항목 | Role / RoleBinding | ClusterRole / ClusterRoleBinding |
|---|---|---|
| 범위 | 특정 네임스페이스 내 | 클러스터 전체 |
| 대상 리소스 | 네임스페이스 리소스 (Pod, Service 등) | 비-네임스페이스 리소스 (Node, PV 등) 포함 |
| 용도 | 특정 네임스페이스 내 권한 관리 | 클러스터 관리자 권한, 전체 조회 등 |
| 조합 | RoleBinding + Role | ClusterRoleBinding + ClusterRole |
특수 조합: RoleBinding + ClusterRole
- ClusterRole을 RoleBinding으로 특정 네임스페이스에 바인딩 가능
- 공통 ClusterRole을 정의해두고, 각 네임스페이스에서 RoleBinding으로 재사용 → 중복 Role 생성 방지
# ClusterRole: 모든 네임스페이스의 노드 조회
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: node-viewer
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch"]
---
# ClusterRoleBinding: monitoring-sa에 node-viewer 연결
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: monitoring-node-view
subjects:
- kind: ServiceAccount
name: prometheus-sa
namespace: monitoring
roleRef:
kind: ClusterRole
name: node-viewer
apiGroup: rbac.authorization.k8s.io
CGV 적용: Prometheus ServiceAccount에 ClusterRole로 모든 네임스페이스의 Pod/Service/Endpoint 조회 권한 부여 (모니터링 메트릭 수집에 필수)
8.5 Pod Security Standards (PSS)
정의: K8s 1.25+에서 PodSecurityPolicy(PSP)를 대체하는 내장 보안 기능이다.
네임스페이스 단위로 Pod의 보안 프로파일을 강제한다.
핵심 포인트:
- 별도 설치 불필요 — K8s 빌트인 Admission Controller로 동작
- PodSecurityPolicy(PSP)는 1.25에서 완전 제거됨 → PSS가 공식 후속
3가지 프로파일:
| 프로파일 | 설명 | 제한 수준 |
|---|---|---|
| privileged | 제한 없음. 모든 Pod 허용 | 없음 (시스템 컴포넌트용) |
| baseline | 알려진 위험 설정만 차단 (hostNetwork, hostPID, privileged 컨테이너 등) | 중간 |
| restricted | 최대 보안. 현재 Pod 보안 권장사항을 모두 강제 | 최고 |
3가지 적용 모드 (namespace label):
| 모드 | 동작 | label 키 |
|---|---|---|
| enforce | 위반 Pod 생성 거부 | pod-security.kubernetes.io/enforce |
| warn | 위반 시 경고 메시지 출력 (생성은 허용) | pod-security.kubernetes.io/warn |
| audit | 위반 이벤트를 감사 로그에 기록 (생성은 허용) | pod-security.kubernetes.io/audit |
restricted 프로파일 주요 요구사항:
# restricted를 충족하는 Pod 예시
apiVersion: v1
kind: Pod
metadata:
name: cgv-api
spec:
containers:
- name: api
image: cgv-api:latest
securityContext:
runAsNonRoot: true # root 실행 금지
allowPrivilegeEscalation: false # 권한 상승 금지
capabilities:
drop:
- ALL # 모든 Linux capabilities 제거
seccompProfile:
type: RuntimeDefault # seccomp 기본 프로파일 적용
readOnlyRootFilesystem: true # 루트 파일시스템 읽기 전용
네임스페이스에 PSS 적용:
apiVersion: v1
kind: Namespace
metadata:
name: cgv-prod
labels:
pod-security.kubernetes.io/enforce: restricted # 위반 시 거부
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/warn: restricted # 경고도 함께
pod-security.kubernetes.io/audit: restricted # 감사 로그도 기록
---
apiVersion: v1
kind: Namespace
metadata:
name: cgv-dev
labels:
pod-security.kubernetes.io/enforce: baseline # dev는 baseline으로 완화
pod-security.kubernetes.io/warn: restricted # 단, restricted 위반 시 경고
baseline vs restricted 차이 요약:
| 항목 | baseline | restricted |
|---|---|---|
| privileged 컨테이너 | 금지 | 금지 |
| hostNetwork/hostPID | 금지 | 금지 |
| runAsNonRoot | 선택 | 필수 |
| capabilities drop ALL | 선택 | 필수 |
| allowPrivilegeEscalation | 선택 | false 필수 |
| seccompProfile | 선택 | RuntimeDefault 필수 |
CGV 적용: cgv-prod=restricted (최대 보안 강제), cgv-dev=baseline (개발 편의성 확보, restricted 위반은 warn으로 표시). monitoring 네임스페이스는 일부 시스템 컴포넌트(node-exporter 등)가 hostNetwork를 사용하므로 baseline 적용
8.6 SecurityGroupPolicy [EKS 전용]
정의: Pod 단위로 AWS Security Group을 적용하는 CRD이다. EKS VPC CNI 플러그인에서 제공한다.
핵심 포인트:
- 일반적으로 Node의 Security Group이 모든 Pod에 적용됨
- SecurityGroupPolicy를 사용하면 Pod별로 다른 SG를 할당 가능
- 내부 동작: EKS가 ENI를 Pod에 직접 할당(Branch ENI)하고, 해당 ENI에 지정된 SG를 연결
- 전제 조건: VPC CNI의
ENABLE_POD_ENI=true설정 필요 - Nitro 인스턴스에서만 지원 (t3, m5, c5 등)
apiVersion: vpcresources.k8s.aws/v1beta1
kind: SecurityGroupPolicy
metadata:
name: cgv-api-sgp
namespace: cgv-prod
spec:
podSelector:
matchLabels:
app: cgv-api
securityGroups:
groupIds:
- sg-0123456789abcdef0 # API Pod 전용 SG
- sg-0fedcba9876543210 # 공통 SG
Node SG vs SecurityGroupPolicy 비교:
| 항목 | Node SG | SecurityGroupPolicy |
|---|---|---|
| 적용 단위 | 노드의 모든 Pod | 특정 label의 Pod만 |
| 설정 방식 | EC2 인스턴스 SG | CRD로 선언 |
| ENI | 노드의 기본 ENI 공유 | Branch ENI 별도 할당 |
| 용도 | 단일 테넌트, 동일 보안 요구사항 | 멀티 테넌트, Pod별 네트워크 격리 |
CGV 적용: 단일 테넌트이므로 Node SG Chain(ALB -> Node -> RDS/ElastiCache)으로 충분. SecurityGroupPolicy는 멀티테넌트 전환 시(예: 여러 영화관 체인이 같은 클러스터를 공유) 검토. 현재는 불필요한 복잡성을 추가하지 않는다
8.7 OPA / Gatekeeper
정의: OPA(Open Policy Agent)는 범용 정책 엔진이고, Gatekeeper는 OPA의 K8s 네이티브 구현이다. Admission Webhook으로 동작하여 리소스 생성/수정 시 정책을 강제한다.
핵심 포인트:
- OPA: CNCF graduated 프로젝트. Rego 언어로 정책을 작성하는 범용 엔진
- Gatekeeper: OPA를 K8s CRD로 래핑. ConstraintTemplate + Constraint 패턴
- PSS와의 차이:
- PSS: 네임스페이스-level 일괄 정책 (privileged/baseline/restricted 3단계)
- Gatekeeper: 세밀한 커스텀 정책 (특정 레지스트리만 허용, 특정 label 필수 등)
- ValidatingAdmissionPolicy (K8s 1.26+): Gatekeeper 없이도 CEL 표현식으로 간단한 정책 가능 (K8s 네이티브)
동작 구조:
kubectl apply → API Server → Admission Webhook → Gatekeeper
|
v
ConstraintTemplate (정책 틀)
+
Constraint (적용 규칙)
|
허용 / 거부
# ConstraintTemplate: "특정 레지스트리에서만 이미지 허용" 정책 틀
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
openAPIV3Schema:
type: object
properties:
repos:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not startswith(container.image, input.parameters.repos[_])
msg := sprintf("image '%v' is not from an allowed repository", [container.image])
}
---
# Constraint: cgv-prod에서 ECR 이미지만 허용
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: cgv-prod-allowed-repos
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces: ["cgv-prod"]
parameters:
repos:
- "123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/"
PSS vs Gatekeeper 비교:
| 항목 | PSS | Gatekeeper |
|---|---|---|
| 설치 | K8s 내장 (설치 불필요) | CRD + Operator 설치 필요 |
| 정책 범위 | Pod 보안 설정 (3단계 프로파일) | 무제한 (이미지, 라벨, 리소스, 네이밍 등) |
| 적용 단위 | 네임스페이스 전체 | 네임스페이스, Kind, label 등 세밀한 매칭 |
| 정책 언어 | 없음 (프로파일 선택) | Rego (OPA 전용 언어) |
| 적합 상황 | "Pod 보안 기본선" | "커스텀 조직 정책 강제" |
CGV 적용: PSS로 기본 보안 라인을 잡고, Gatekeeper로 추가 정책(ECR 이미지만 허용, 필수 label 강제 등)을 적용 (→ 2.3 섹션 9.3).
RBAC 전체 흐름 정리:
사용자/Pod 요청
|
v
[인증] ServiceAccount 토큰 검증 (+ IRSA로 AWS 임시 토큰 주입)
|
v
[인가] RBAC: Role/ClusterRole에 정의된 권한 확인
|
v
[어드미션] PSS (Pod 보안 프로파일) + Gatekeeper (커스텀 정책) 검증
|
v
허용 / 거부
정리
┌───────────────────┬───────────┬────────┬─────────────────────────┐
│ │ 언제 │ 어디서 │ 뭘 확인 │
├───────────────────┼───────────┼────────┼─────────────────────────┤
│ 인증 │ 요청 시 │ K8s │ "누구야?" │
│ │ │ API │ │
├───────────────────┼───────────┼────────┼─────────────────────────┤
│ 인가(RBAC) │ 요청 시 │ K8s │ "K8s에서 이거 할 권한 │
│ │ │ API │ 있어?" │
├───────────────────┼───────────┼────────┼─────────────────────────┤
│ 어드미션(PSS/OPA) │ 요청 시 │ K8s │ "정책 위반 아니야?" │
│ │ │ API │ │
├───────────────────┼───────────┼────────┼─────────────────────────┤
│ IRSA │ Pod 실행 │ AWS │ "AWS에서 이거 할 권한 │
│ │ 중 │ API │ 있어?" │
└───────────────────┴───────────┴────────┴─────────────────────────┘
9. 오토스케일링
트래픽 변화에 따라 Pod 수(수평) 또는 Pod 리소스(수직)를 자동으로 조정하고, 서비스 가용성을 보장하는 메커니즘이다.
9.1 Metrics Server
정의: 클러스터 전체의 리소스 사용량(CPU, Memory) 데이터를 수집하는 경량 애드온이다.
핵심 포인트:
- In-Memory: 데이터를 디스크에 저장하지 않고 메모리에만 보관. 장기 보존용이 아님 (→ 장기 메트릭은 Prometheus 역할)
- 수집 경로: 각 노드의 kubelet(cAdvisor) → Metrics Server → Metrics API (
metrics.k8s.io) - 수집 주기: 기본 60초
- 용도:
kubectl top nodes/kubectl top pods명령어 수행- HPA가 스케일링 결정을 내리는 기준 데이터 소스
장애 시 영향:
- Metrics Server가 죽으면
kubectl top이 안 되고, HPA가 현재 부하를 알 수 없어 오토스케일링이 멈춘다 → 서비스 장애로 이어질 수 있음 - 단, KEDA의 Prometheus trigger를 사용하는 경우 Metrics Server와 무관하게 동작 (KEDA는 Prometheus에서 직접 메트릭을 조회)
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ kubelet │ │ kubelet │ │ kubelet │
│ (cAdvisor) │ │ (cAdvisor) │ │ (cAdvisor) │
│ Node A │ │ Node B │ │ Node C │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────────┼───────────────────┘
│
┌──────┴──────┐
│ Metrics │
│ Server │
└──────┬──────┘
│
┌──────┴──────┐
│ metrics.k8s │
│ .io API │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
┌────┴────┐ ┌───┴───┐ ┌────┴─────┐
│ HPA │ │kubectl│ │Dashboard │
│ │ │ top │ │ │
└─────────┘ └───────┘ └──────────┘
9.2 HPA (Horizontal Pod Autoscaler)
정의: CPU/메모리 사용률 또는 커스텀 메트릭 기반으로 Pod 수를 자동으로 조정하는 리소스이다.
핵심 포인트:
minReplicas: 최소 Pod 수 (0은 불가, 최소 1)maxReplicas: 최대 Pod 수metrics: 스케일링 기준 (CPU, 메모리, 커스텀 메트릭)- Metrics Server가 설치되어 있어야 CPU/메모리 메트릭 수집 가능
- 스케일링 주기: 기본 15초마다 메트릭 확인
- 스케일아웃은 즉시, 스케일다운은 안정화 대기 (기본 5분) → 잦은 축소/확장(flapping) 방지
HPA 계산 공식:
desiredReplicas = ceil(현재 Pod 수 × (현재 메트릭 값 / 목표 메트릭 값))
예시: 현재 3개 Pod, CPU 사용률 90%, 목표 70%
desiredReplicas = ceil(3 × (90 / 70)) = ceil(3.86) = 4
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: cgv-api-hpa
namespace: cgv-prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: cgv-api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # 전 Pod 평균 CPU 70% 초과 시 스케일아웃
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior: # v2에서 추가된 세밀한 제어
scaleUp:
stabilizationWindowSeconds: 0 # 스케일아웃은 즉시
policies:
- type: Pods
value: 4 # 한 번에 최대 4개 추가
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300 # 스케일다운은 5분 안정화 후
policies:
- type: Percent
value: 10 # 한 번에 최대 10% 제거
periodSeconds: 60
HPA의 한계:
| 한계 | 설명 |
|---|---|
| 메트릭 제한 | 기본은 CPU/메모리만. 커스텀 메트릭은 Prometheus Adapter 등 별도 설치 필요 |
| 0으로 축소 불가 | minReplicas 최소 1. 트래픽 없을 때도 리소스 소비 |
| 반응 속도 | CPU 사용률은 이미 부하가 걸린 후 올라감 → 선제적 대응 불가 |
| 외부 이벤트 | Redis 큐 길이, Kafka lag 등 외부 메트릭에 네이티브 대응 불가 |
CGV 적용: CGV는 HPA 대신 KEDA를 사용. Redis 대기열 길이(Prometheus Gauge) 기반으로 이벤트 드리븐 스케일링을 구현하므로, HPA를 별도로 설정하지 않는다. values.yaml에서 hpa.enabled: false로 비활성화 (→ 2.2 섹션 8, 2.3 섹션 7.2).
9.3 VPA (Vertical Pod Autoscaler)
정의: Pod의 CPU/메모리 requests와 limits를 자동으로 조정하는 수직 스케일링 리소스이다.
핵심 포인트:
- HPA가 Pod "수"를 조정한다면, VPA는 Pod "크기(리소스)"를 조정
- 별도 설치 필요 (K8s 내장이 아님)
- 3가지 컴포넌트: Recommender(추천) + Updater(적용) + Admission Controller(주입)
VPA 모드:
| 모드 | 동작 |
|---|---|
| Off | 추천값만 제공, 실제 적용 안 함 (모니터링 전용) |
| Initial | Pod 생성 시에만 추천값 적용, 실행 중 변경 안 함 |
| Auto | 실행 중인 Pod도 재시작하여 추천값 적용 (주의: Pod 재시작 발생) |
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: cgv-api-vpa
namespace: cgv-prod
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: cgv-api
updatePolicy:
updateMode: "Off" # 추천값만 확인, 자동 적용 안 함
resourcePolicy:
containerPolicies:
- containerName: api
minAllowed:
cpu: 100m
memory: 128Mi
maxAllowed:
cpu: "2"
memory: 4Gi
HPA vs VPA 비교:
| 항목 | HPA | VPA |
|---|---|---|
| 방향 | 수평 (Pod 수 증감) | 수직 (Pod 리소스 증감) |
| 설치 | K8s 내장 | 별도 설치 필요 |
| Pod 재시작 | 불필요 | Auto 모드 시 재시작 필요 |
| 동시 사용 | - | CPU 기준 HPA와 동시 사용 금지 (충돌) |
| 적합 상황 | Stateless 워크로드 (API 서버) | Stateful 워크로드 (DB, 캐시), 적정 리소스 탐색 |
VPA + HPA 동시 사용 주의:
- VPA가 CPU requests를 올림 → HPA가 CPU 사용률이 낮아진 것으로 판단 → 스케일다운 → VPA가 다시 조정 → 무한 루프
- 해결: VPA는 memory만, HPA는 CPU만 담당하도록 분리하거나, VPA를 Off 모드로 추천값만 확인
CGV 적용: VPA를 Off 모드로 설정하여 추천 리소스값을 확인하고, 이를 참고하여 Helm values.yaml의 requests/limits를 수동 조정. KEDA와 VPA는 충돌하지 않지만(KEDA는 Pod 수, VPA는 Pod 크기), Auto 모드의 Pod 재시작이 서비스에 영향을 줄 수 있으므로 Off 모드 권장.
9.4 KEDA (Kubernetes Event-Driven Autoscaler)
정의: 이벤트 기반 오토스케일러. HPA를 확장하여 외부 메트릭 소스(Prometheus, Redis, CloudWatch 등)를 트리거로 사용한다.
핵심 포인트:
- HPA는 CPU/메모리만 기본 지원 → KEDA는 50+ 외부 스케일러 지원
- ScaledObject CRD: 어떤 Deployment를, 어떤 메트릭으로, 얼마나 스케일할지 정의
- 내부 동작: KEDA가 ScaledObject를 감지 → 내부적으로 HPA를 자동 생성/관리 → 따라서 별도 HPA를 만들면 충돌
- 별도 설치 필요: KEDA Operator + CRD를 Helm으로 설치
KEDA 아키텍처:
┌──────────────────────────────────────────────────────┐
│ KEDA Operator │
│ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Scaler │────│ Metrics │ │
│ │ (Prometheus,│ │ Adapter │ │
│ │ Redis, │ │ (external │ │
│ │ SQS 등) │ │ metrics API)│ │
│ └──────┬──────┘ └──────┬───────┘ │
│ │ │ │
│ │ 메트릭 조회 │ 메트릭 제공 │
│ v v │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ External │ │ HPA │ ← KEDA가 자동 │
│ │ Metric Source│ │ (자동 생성) │ 생성/관리 │
│ │ (Prometheus) │ └──────┬───────┘ │
│ └──────────────┘ │ │
│ v │
│ ┌──────────────┐ │
│ │ Deployment │ │
│ │ (cgv-api) │ │
│ │ replicas │ │
│ │ 조정 │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────┘
ScaledObject 주요 설정:
| 필드 | 설명 | CGV 값 |
|---|---|---|
pollingInterval |
메트릭 조회 주기 (초) | 15 |
cooldownPeriod |
스케일다운 대기 시간 (초) | 60 |
minReplicaCount |
최소 Pod 수 | 2 |
maxReplicaCount |
최대 Pod 수 | 10 |
idleReplicaCount |
이벤트 없을 때 Pod 수 (0 가능 = 서버리스) | 미사용 (항상 min 2 유지) |
triggers |
메트릭 소스와 임계값 정의 | Prometheus, threshold 1000 |
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: cgv-api-scaledobject
namespace: cgv-prod
spec:
scaleTargetRef:
name: cgv-api # 스케일 대상 Deployment
pollingInterval: 15 # 15초마다 메트릭 확인
cooldownPeriod: 60 # 스케일다운 전 60초 대기
minReplicaCount: 2 # 최소 2개 Pod 유지
maxReplicaCount: 10 # 최대 10개 Pod
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-kube-prometheus-prometheus.monitoring.svc:9090
query: max(cgv_queue_waiting_count{namespace="cgv-prod"})
threshold: "1000" # 대기열 1000명 초과 시 스케일아웃
HPA vs KEDA 비교:
| 항목 | HPA | KEDA |
|---|---|---|
| 메트릭 소스 | CPU/메모리 (기본), custom metrics API | 50+ 외부 스케일러 (Prometheus, Redis, CloudWatch, SQS 등) |
| 0으로 축소 | 불가 (최소 1) | 가능 (idleReplicaCount: 0) |
| 설치 | K8s 내장 | CRD + Operator 설치 필요 |
| 동작 방식 | 직접 HPA 생성 | ScaledObject → 내부 HPA 자동 생성 |
| 충돌 | - | HPA와 동시 사용 시 충돌 (KEDA 사용 시 HPA 비활성화 필수) |
| 선제적 대응 | CPU 사용률 상승 후 반응 (후행) | 이벤트(큐 길이 등) 기반 선제 대응 (선행) |
주의: sum() vs max() 함정 — 양성 피드백 루프:
이 부분은 CGV 프로젝트에서 실제로 중요한 설계 결정이므로 자세히 설명한다.
상황: 모든 Pod가 동일한 Redis ZCARD 값을 Prometheus에 노출
Pod 1: cgv_queue_waiting_count = 5000
Pod 2: cgv_queue_waiting_count = 5000
Pod 3: cgv_queue_waiting_count = 5000
sum()을 쓰면:
sum(cgv_queue_waiting_count) = 15000 (실제 대기열은 5000명인데!)
threshold 1000 → 15000/1000 = 15개 Pod 필요 → 과도한 스케일아웃
→ Pod가 늘어나면 sum 값도 증가 → 더 스케일아웃 → 양성 피드백 루프!
max()를 쓰면:
max(cgv_queue_waiting_count) = 5000 (실제 대기열 크기 정확히 반영)
threshold 1000 → 5000/1000 = 5개 Pod 필요 → 정확한 스케일링
결론: 모든 Pod가 같은 전역 값을 읽는 메트릭은 반드시 max()를 사용
KEDA Trigger 종류 (CGV 관련):
| Trigger | 용도 | 메트릭 소스 |
|---|---|---|
| prometheus | Prometheus 쿼리 결과 기반 스케일링 | Prometheus 서버 |
| redis | Redis List/Stream 길이 기반 | Redis 서버 직접 |
| aws-cloudwatch | CloudWatch 메트릭 기반 | AWS CloudWatch |
| cron | 시간 기반 예약 스케일링 | 크론 표현식 |
# 참고: cron trigger 예시 (예매 오픈 시간에 미리 스케일아웃)
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: cgv-api-cron-scaledobject
namespace: cgv-prod
spec:
scaleTargetRef:
name: cgv-api
minReplicaCount: 2
maxReplicaCount: 10
triggers:
- type: cron
metadata:
timezone: Asia/Seoul
start: 55 19 * * * # 매일 19:55 (예매 오픈 5분 전)
end: 30 20 * * * # 매일 20:30
desiredReplicas: "8" # 미리 8개로 확장
- type: prometheus
metadata:
serverAddress: http://prometheus-kube-prometheus-prometheus.monitoring.svc:9090
query: max(cgv_queue_waiting_count{namespace="cgv-prod"})
threshold: "1000"
CGV 적용: Redis 대기열 길이(Prometheus Gauge cgv_queue_waiting_count)를 max() 함수로 조회하여 이벤트 드리븐 스케일링. threshold 1000명 초과 시 스케일아웃, cooldown 60초. values.yaml에서 hpa.enabled: false로 HPA 충돌 방지 (→ 2.2 섹션 8, 2.3 섹션 7.2).
9.5 PodDisruptionBudget (PDB)
정의: 자발적 중단(Voluntary Disruption) 시 동시에 중단 가능한 Pod 수를 제한하여 서비스 가용성을 보장하는 리소스이다.
핵심 포인트:
minAvailable: 최소 유지해야 하는 Pod 수 (숫자 또는 비율)maxUnavailable: 최대 중단 허용 Pod 수 (숫자 또는 비율)- 둘 중 하나만 지정 (동시 지정 불가)
- 노드 업그레이드, 클러스터 오토스케일링 축소,
kubectl drain시 서비스 무중단 보장 - 비자발적 중단(하드웨어 장애, OOM Kill 등)에는 적용되지 않음
자발적 vs 비자발적 중단:
| 구분 | 자발적 중단 (Voluntary) | 비자발적 중단 (Involuntary) |
|---|---|---|
| 예시 | 노드 드레인, K8s 업그레이드, Karpenter Consolidation | 하드웨어 장애, 커널 패닉, OOM Kill |
| PDB 적용 | O (PDB가 보호) | X (PDB 무시) |
| 대응 방법 | PDB로 최소 가용성 보장 | replicas + Anti-Affinity로 이중화 |
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: cgv-api-pdb
namespace: cgv-prod
spec:
minAvailable: 2 # 최소 2개 Pod 항상 유지
selector:
matchLabels:
app: cgv-api
# 비율로 지정하는 방식
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: cgv-api-pdb-percent
namespace: cgv-prod
spec:
maxUnavailable: "25%" # 전체의 25%까지만 동시 중단 허용
selector:
matchLabels:
app: cgv-api
PDB 동작 시나리오:
상황: cgv-api Pod 4개 실행 중, PDB minAvailable: 2
노드 업그레이드를 위해 kubectl drain 실행
1. Node A의 Pod 1 퇴거 요청 → 남은 Pod 3개 >= 2 → 허용
2. Node B의 Pod 2 퇴거 요청 → 남은 Pod 2개 >= 2 → 허용
3. Node C의 Pod 3 퇴거 요청 → 남은 Pod 1개 < 2 → 거부! (대기)
4. Pod 1이 Node D에서 재시작 → 남은 Pod 3개 >= 2 → Pod 3 퇴거 허용
PDB 설정 시 주의사항:
| 상황 | 문제 | 해결 |
|---|---|---|
| minAvailable = replicas | 모든 Pod가 필수 → 드레인 불가능 (데드락) | minAvailable < replicas로 여유 확보 |
| maxUnavailable: 0 | 어떤 Pod도 중단 불가 → 드레인 불가능 | 최소 1 이상 허용 |
| PDB 없이 drain | 모든 Pod가 한꺼번에 퇴거 → 서비스 중단 | PDB 필수 설정 |
CGV 적용: API 서버 PDB minAvailable: 2로 설정하여 노드 업그레이드, Karpenter Consolidation 중에도 최소 2개 Pod를 유지, 서비스 가용성을 보장. Karpenter의 disruption.consolidationPolicy: WhenUnderutilized와 함께 사용하면 비용 최적화와 가용성을 동시에 달성 (→ 2.3 섹션 5).
9.6 오토스케일링 전체 구조 (CGV)
트래픽 유입
|
v
┌──────────────┐
│ ALB │
│ (Ingress) │
└──────┬───────┘
|
v
┌──────────────┐
│ cgv-api │ ← Pod 수평 스케일링
│ Deployment │
│ (2~10 Pod) │
└──────┬───────┘
|
┌─────────────┼──────────────┐
| | |
v v v
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Redis │ │Prometheus│ │ Metrics │
│ │ │ │ │ Server │
└──────────┘ └────┬─────┘ └──────────┘
|
┌────────────┼────────────┐
| | |
v v v
┌──────────┐ ┌──────────┐ ┌──────────┐
│ KEDA │ │ HPA │ │ VPA │
│(실제사용) │ │(비활성화)│ │(Off모드) │
│ │ │ │ │추천값만 │
└────┬─────┘ └──────────┘ └──────────┘
|
v
ScaledObject
query: max(cgv_queue_waiting_count)
threshold: 1000
|
v
KEDA 내부 HPA 자동 생성
|
v
Pod 수 조정 (2 ~ 10)
|
v
Karpenter: Pending Pod 감지 → 노드 자동 프로비저닝
CGV 오토스케일링 의사결정 요약:
| 결정 | 선택 | 근거 |
|---|---|---|
| Pod 스케일링 | KEDA (HPA 아님) | Redis 대기열 길이라는 외부 메트릭 기반 선제적 대응이 필요 |
| KEDA Trigger | Prometheus (Redis 아님) | Prometheus에 이미 수집된 메트릭 재활용, KEDA가 Redis에 직접 접속하지 않아 부하 분리 |
| 쿼리 함수 | max() (sum() 아님) | 모든 Pod가 같은 ZCARD를 노출 → sum()은 양성 피드백 루프 유발 |
| HPA | 비활성화 | KEDA가 내부 HPA를 자동 생성하므로 별도 HPA 충돌 방지 |
| VPA | Off 모드 | 추천값 참고용. Auto 모드의 Pod 재시작은 대기열 시스템에 부적합 |
| 노드 스케일링 | Karpenter | Pending Pod 감지 시 수십 초 내 최적 인스턴스 프로비저닝 |
| PDB | minAvailable: 2 | 노드 드레인/Consolidation 시에도 최소 2개 Pod로 서비스 유지 |
10. 노드 관리
10.1 노드 기본 개념
정의: Node는 쿠버네티스 클러스터에서 실제로 Pod를 실행하는 워커 머신이다. 물리 서버일 수도 있고, 클라우드의 가상 머신(EC2 인스턴스 등)일 수도 있다.
핵심 포인트:
- 쿠버네티스 클러스터는 컨트롤 플레인 노드와 워커 노드로 구성된다
- 워커 노드에는 kubelet, kube-proxy, 컨테이너 런타임이 실행된다
- 각 노드는 API 서버에 자신의 상태를 주기적으로 보고한다 (kubelet heartbeat)
- 노드가 일정 시간 응답하지 않으면 Node Controller가 해당 노드를 NotReady로 표시하고, Pod를 다른 노드로 재스케줄링한다
노드 상태 확인:
# 노드 목록 조회
kubectl get nodes
# 노드 상세 정보 (Conditions, Capacity, Allocatable 등)
kubectl describe node <node-name>
Node Conditions (상태 조건):
| Condition | 의미 | True일 때 |
|---|---|---|
| Ready | 노드가 정상적으로 Pod를 실행할 수 있는 상태 | 정상 |
| DiskPressure | 디스크 용량이 부족한 상태 | 비정상 — 새 Pod 스케줄링 중단 |
| MemoryPressure | 메모리가 부족한 상태 | 비정상 — OOM 위험 |
| PIDPressure | 프로세스 ID가 부족한 상태 | 비정상 — 컨테이너 생성 불가 |
| NetworkUnavailable | 네트워크 설정이 올바르지 않은 상태 | 비정상 — 통신 불가 |
Node Labels (노드 레이블):
- 노드에 key=value 형태의 메타데이터를 부여하는 메커니즘
nodeSelector,nodeAffinity에서 Pod를 특정 노드에 배치할 때 사용한다- 쿠버네티스가 자동으로 부여하는 레이블(Well-Known Labels)도 있다
# 노드에 레이블 추가
kubectl label node worker-01 role=prod-base
# 레이블 기반으로 노드 필터링
kubectl get nodes -l role=prod-base
주요 Well-Known Labels:
| 레이블 | 예시 값 | 용도 |
|---|---|---|
kubernetes.io/os |
linux | OS 기반 스케줄링 |
kubernetes.io/arch |
amd64, arm64 | 아키텍처 기반 스케줄링 |
topology.kubernetes.io/zone |
ap-northeast-2a | AZ 기반 스케줄링 (Anti-Affinity) |
node.kubernetes.io/instance-type |
m5.xlarge | [EKS 전용] 인스턴스 타입 |
10.2 노드 운영
프로덕션 환경에서 노드 유지보수(OS 패치, 커널 업데이트 등)를 수행할 때, Pod를 안전하게 이동시키는 3가지 명령이 있다.
cordon
정의: 해당 노드에 새로운 Pod의 스케줄링을 금지한다. 이미 실행 중인 Pod에는 영향을 주지 않는다.
# 노드를 스케줄링 불가 상태로 변경
kubectl cordon worker-01
# 결과: worker-01 상태가 Ready,SchedulingDisabled로 변경
# 내부적으로는 노드에 spec.unschedulable: true를 설정하는 것
사용 시점: 노드 상태를 먼저 확인하거나 점진적으로 트래픽을 줄일 때.
drain
정의: cordon(스케줄링 금지) + 기존 Pod를 다른 노드로 Evict(퇴거)한다. 노드를 완전히 비우는 명령이다.
# 노드의 모든 Pod를 안전하게 퇴거
kubectl drain worker-01 --ignore-daemonsets --delete-emptydir-data
# --ignore-daemonsets: DaemonSet Pod는 퇴거하지 않음 (모든 노드에 떠야 하므로)
# --delete-emptydir-data: emptyDir 볼륨 데이터 삭제 허용
핵심 포인트:
- drain은 PodDisruptionBudget(PDB)을 준수한다. PDB
minAvailable: 2이면 2개 미만이 될 Evict는 대기한다 - Evict된 Pod는 ReplicaSet Controller에 의해 다른 노드에 재생성된다
- 단독 Pod(Deployment 없이 직접 생성한 Pod)는 drain 시 삭제되고 재생성되지 않는다 —
--force필요
사용 시점: 노드 유지보수, OS 패치, 노드 교체 시.
uncordon
정의: cordon 상태를 해제하여 다시 새 Pod를 스케줄링할 수 있게 한다.
# 노드를 다시 스케줄링 가능 상태로 복원
kubectl uncordon worker-01
# 결과: worker-01 상태가 Ready로 복원
노드 유지보수 전체 흐름:
1. kubectl cordon worker-01 # 새 Pod 스케줄링 차단
2. kubectl drain worker-01 ... # 기존 Pod 안전 퇴거
3. (OS 패치, 커널 업데이트 등 수행)
4. kubectl uncordon worker-01 # 스케줄링 재개
10.3 Node Group 개념
정의: 같은 사양/역할의 노드를 묶어서 관리하는 단위이다.
핵심 포인트:
- 쿠버네티스 자체 개념이 아닌 클라우드 프로바이더/관리 도구의 개념
- 레이블로 그룹을 구분하고, Taint/Toleration + NodeAffinity로 워크로드 배치를 제어
- 네임스페이스(논리적 분리) + 노드 그룹(물리적 분리)을 함께 사용하여 완전한 워크로드 격리 달성
EKS Managed Node Group [EKS 전용]
정의: AWS가 노드의 프로비저닝과 라이프사이클을 관리하는 노드 그룹이다.
핵심 포인트:
- AMI 업데이트, 노드 교체를 AWS가 처리
- Auto Scaling Group(ASG) 기반으로 동작
- 장점: 운영 부담 감소, AWS 서비스와의 통합이 용이
- 단점: 유연성 제한 (인스턴스 타입 조합에 제약)
CGV 적용: system 노드 그룹은 Managed Node Group으로 구성하여 안정적인 시스템 컴포넌트(Karpenter Controller, CoreDNS 등) 실행
10.4 Cluster Autoscaler (CA)
정의: 쿠버네티스 공식 클러스터 오토스케일러이다. Pending 상태의 Pod를 감지하여 ASG(Auto Scaling Group)의 Desired Count를 증가시켜 새 노드를 추가한다.
핵심 포인트:
- 동작 원리: Pod가 Pending(스케줄 불가) → CA가 감지 → ASG에 "노드 수를 N+1로 늘려라" 요청 → ASG가 새 EC2 인스턴스 생성 → 노드 등록 → Pending Pod 스케줄링
- Scale Down: 노드 사용률이 낮으면(기본 50% 미만) 해당 노드의 Pod를 다른 노드로 이동 후 노드 삭제
- 한계점:
- ASG에 미리 정의한 인스턴스 타입만 사용 가능 (유연성 부족)
- ASG를 경유하므로 반응 속도가 느림 (수 분 소요)
- 인스턴스 타입 최적화 없이 미리 정한 타입만 생성 (비용 비효율)
- Node Group마다 별도 ASG 구성 필요 (관리 복잡도 증가)
# Cluster Autoscaler Deployment 설정 (핵심 부분)
spec:
containers:
- name: cluster-autoscaler
command:
- ./cluster-autoscaler
- --cloud-provider=aws
- --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/my-cluster
- --scale-down-utilization-threshold=0.5 # 50% 미만이면 축소 대상
- --scale-down-delay-after-add=5m # 노드 추가 후 5분간 축소 금지
10.5 Karpenter [EKS 전용]
정의: AWS에서 시작된 오픈소스 Just-in-Time 노드 오토스케일러이다. 스케줄러가 처리하지 못한 Pending Pod를 감지하여, 해당 Pod의 요구사항에 딱 맞는 인스턴스를 즉시 프로비저닝한다.
핵심 포인트:
- Scheduler Bypass: 기존 CA(Cluster Autoscaler)와 달리 ASG(Auto Scaling Group)를 거치지 않고 EC2 Fleet API를 직접 호출하여 노드를 생성한다 (수 분 → 수십 초로 단축)
- Bin Packing (테트리스): Pod들의 Resource Request 합계를 계산하여, 낭비되는 공간 없이 가장 꽉 채울 수 있는 인스턴스 타입(c5, m5 등)을 자동으로 선택한다
- Consolidation (통합/정리): 노드 사용률이 낮아지면 실행 중인 Pod를 다른 노드로 몰아넣고(Evict), 비싼 노드를 삭제하거나 더 저렴한 노드로 교체(Replace)하여 비용을 최적화한다
- Drift Detection: 노드 스펙(AMI, 보안그룹 등)이 변경되면, 기존 노드를 자동으로 최신 스펙으로 교체한다
Cluster Autoscaler(CA) vs Karpenter 비교:
| 항목 | Cluster Autoscaler (Legacy) | Karpenter (Modern) |
|---|---|---|
| 기반 기술 | AWS Auto Scaling Group (ASG) | AWS EC2 Fleet API 직접 제어 |
| 확장 트리거 | "ASG야, 인스턴스 개수(Desired) +1 해줘" | "EC2야, 이 Pod들 태울 수 있는 거 하나 줘" |
| 인스턴스 타입 | ASG에 미리 지정한 타입만 사용 | 모든 인스턴스 타입 중 최적/최저가 자동 선택 |
| 스케일링 속도 | 느림 (ASG 쿨타임, 부팅 대기 등) | 매우 빠름 (즉시 API 호출) |
| 비용 최적화 | 부족할 때만 늘림 (축소에 소극적) | 적극적인 축소 및 교체 (Consolidation) |
| 노드 교체 | 수동 또는 ASG Rolling Update | Drift Detection으로 자동 교체 |
Karpenter CRD
Karpenter는 2개의 CRD로 노드 프로비저닝 정책을 선언적으로 정의한다.
NodePool — 스케줄링 제약조건 정의:
- requirements: 인스턴스 타입 필터 (capacity-type, architecture, instance-category 등)
- taints: 해당 NodePool로 생성된 노드에 자동 부여할 Taint
- limits: NodePool 전체의 CPU/Memory 총량 상한 — 비용 폭주 방지
- disruption: 통합 정책 (consolidationPolicy) 및 노드 만료 기한 (expireAfter)
EC2NodeClass — AWS 인프라 설정 정의:
- amiFamily: AMI 종류 (AL2, Bottlerocket 등)
- subnetSelectorTerms: 노드가 생성될 서브넷 필터 (태그 기반)
- securityGroupSelectorTerms: 노드에 부여할 보안그룹 필터
- instanceProfile: 노드에 부여할 IAM Instance Profile
- blockDeviceMappings: 루트 볼륨 크기, 타입(gp3 등) 설정
NodePool과 EC2NodeClass는 nodeClassRef로 1:1 매핑된다.
NodePool이 "어떤 조건의 노드를 만들 것인가"를 정의
# NodePool 예시 (prod-burst: Spot 인스턴스, 이벤트 트래픽 대응)
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: prod-burst
spec:
template:
metadata:
labels:
role: prod-burst # Pod의 nodeSelector에서 매칭
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"] # Spot 인스턴스만 사용 (비용 절감)
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m"] # 컴퓨팅/범용 인스턴스 계열
taints:
- key: role
value: prod-burst
effect: NoSchedule # Toleration 없는 Pod는 배치 금지
nodeClassRef:
name: default # EC2NodeClass 참조
limits:
cpu: "64" # 이 NodePool 전체 CPU 상한
memory: 128Gi # 이 NodePool 전체 메모리 상한
disruption:
consolidationPolicy: WhenEmpty # 노드가 빈 경우에만 통합
consolidateAfter: 30s # 빈 상태 30초 후 삭제
EC2NodeClass가 "AWS에서 어떤 설정으로 만들 것인가"를 정의
# EC2NodeClass 예시 (AWS 인프라 설정)
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: AL2 # Amazon Linux 2 AMI
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: cgv-eks # 서브넷 태그로 필터
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: cgv-eks # 보안그룹 태그로 필터
instanceProfile: KarpenterNodeInstanceProfile-cgv-eks
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 50Gi
volumeType: gp3
deleteOnTermination: true
CGV NodePool 구조 — 5개 그룹 :
| 그룹 | 관리 방식 | 인스턴스 타입 | 역할 |
|---|---|---|---|
| system | EKS Managed Node Group | On-Demand (t3.medium) | Karpenter Controller, CoreDNS 등 시스템 컴포넌트 |
| infra | Karpenter NodePool | On-Demand x 2 | Prometheus, Grafana, ArgoCD 등 인프라 도구 |
| dev | Karpenter NodePool | Spot/On-Demand | 개발 환경 워크로드 |
| prod-base | Karpenter NodePool | On-Demand | 프로덕션 기본 트래픽 (상시 운영) |
| prod-burst | Karpenter NodePool | Spot | 이벤트 트래픽 대응 (티켓팅 오픈 시) |
CGV 적용:
- 티켓 예매 오픈 시: 트래픽 폭주로 Pod가 수백 개 늘어날 때, CA는 ASG 반응 속도가 느려 Pending이 길어진다. Karpenter는 수십 초 내에 대용량 노드를 다수 생성하여 빠르게 대응한다.
- 새벽 시간: 접속자가 줄어들면 흩어진 Pod들을 소수의 노드로 모으고, 남는 노드는 삭제하여 클라우드 비용을 절감한다.
- system 노드 그룹은 Managed Node Group: Karpenter Controller 자체가 Karpenter 노드에 뜨면 자기 자신을 삭제할 위험이 있으므로, system 컴포넌트는 MNG에서 안정적으로 실행한다.
11. 확장 메커니즘
11.1 CRD (Custom Resource Definition)
정의: 쿠버네티스 API를 확장하여 사용자 정의 리소스를 등록하는 메커니즘이다.
핵심 포인트:
- 기본 리소스(Pod, Service 등)처럼 kubectl로 CRUD 가능
- 쿠버네티스를 "플랫폼을 만드는 플랫폼"으로 만드는 핵심 메커니즘
- CRD를 등록하면 API 서버에 새로운 엔드포인트가 생성된다 (
/apis/<group>/<version>/<resource>) - CRD 자체는 "데이터 구조 정의"일 뿐이다. 실제 동작은 Custom Controller가 담당한다
- 예: Karpenter의 NodePool(노드 오토스케일러), ArgoCD의 Application, KEDA의 ScaledObject(파드 오토스케일러)
# CRD 등록 예시 — NodePool이라는 새로운 리소스 타입을 K8s API에 등록
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: nodepools.karpenter.sh
spec:
group: karpenter.sh # API 그룹
versions:
- name: v1beta1
served: true # API 서버에서 제공
storage: true # etcd에 저장
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
template:
type: object
scope: Cluster # 클러스터 레벨 리소스 (네임스페이스 없음)
names:
plural: nodepools # kubectl get nodepools
singular: nodepool # kubectl get nodepool
kind: NodePool # YAML의 kind 값
CRD 등록 후에는 기본 리소스처럼 사용 가능:
kubectl get nodepools # 목록 조회
kubectl describe nodepool prod-burst # 상세 조회
kubectl apply -f nodepool.yaml # 생성/수정
kubectl delete nodepool prod-burst # 삭제
11.2 Operator Pattern
정의: CRD + Custom Controller의 조합으로, 사람 운영자(Operator)의 운영 지식을 코드로 자동화하는 패턴이다.
핵심 포인트:
- Custom Controller가 Reconciliation Loop(조정 루프)를 실행: 원하는 상태(Desired State)와 현재 상태(Current State)를 비교하여 차이를 해소
- 동작 흐름: CRD 생성 → Controller가 Watch → 변경 감지 → Reconcile 함수 실행 → 원하는 상태로 수렴
CRD (Custom Resource) Custom Controller
┌──────────────────┐ ┌───────────────────┐
│ kind: NodePool │ │ Karpenter │
│ spec: │ Watch │ │
│ limits: │ ──────→ │ Reconcile Loop: │
│ cpu: "64" │ │ 현재 상태 확인 │
│ memory: 128Gi│ ◀────── │ 차이 감지 → 조정 │
│ │ Update │ │
└──────────────────┘ └───────────────────┘
주요 Operator 예시:
| Operator | CRD | 자동화하는 운영 지식 |
|---|---|---|
| Karpenter | NodePool, EC2NodeClass | "Pod가 Pending이면 적절한 노드를 즉시 생성하라" |
| Prometheus Operator | ServiceMonitor, PrometheusRule | "이 Service의 메트릭을 자동으로 수집하라" |
| ArgoCD | Application | "Git과 클러스터 상태를 동기화하라" |
| KEDA | ScaledObject | "이벤트 소스 메트릭에 따라 Pod를 스케일하라" |
| Argo Rollouts | Rollout, AnalysisTemplate | "Canary 배포를 자동으로 진행/롤백하라" |
11.3 Admission Webhook
정의: API 서버에 요청이 들어올 때, 요청을 가로채서 검증(Validating)하거나 수정(Mutating)하는 확장 메커니즘이다.
핵심 포인트:
- Mutating Admission Webhook: 요청을 변경 (예: 기본 레이블 자동 추가, 사이드카 자동 주입)
- Validating Admission Webhook: 요청을 검증하여 거부/허용 (예: latest 태그 금지, 리소스 제한 미설정 거부)
- 동작 순서: API 요청 → 인증/인가 → Mutating Webhook → 스키마 검증 → Validating Webhook → etcd 저장
kubectl apply -f pod.yaml
│
▼
API Server
├─ 인증 (Authentication)
├─ 인가 (Authorization / RBAC)
├─ Mutating Admission Webhook ← 요청 수정 (레이블 추가, 사이드카 주입 등)
├─ Object Schema Validation ← YAML 구조 검증
├─ Validating Admission Webhook ← 정책 검증 (latest 금지 등)
└─ etcd 저장 ← 최종 저장
사용 예시:
| 도구 | Webhook 종류 | 동작 |
|---|---|---|
| Istio | Mutating | Pod 생성 시 Envoy 사이드카 자동 주입 |
| OPA/Gatekeeper | Validating | 정책 위반 리소스 생성 차단 |
| Karpenter | Validating | NodePool 스펙 유효성 검증 |
11.4 주요 CRD 예시 (CGV 프로젝트)
CGV 대기열 시스템에서 사용하는 CRD들을 정리한다. CRD가 설치되면 kubectl로 관리할 수 있는 새로운 리소스 타입이 추가된다.
| CRD | 프로젝트/Operator | 용도 | 참조 |
|---|---|---|---|
| ScaledObject | KEDA | 이벤트 드리븐 오토스케일링 | → 섹션 9 |
| NodePool | Karpenter | 노드 프로비저닝 제약조건 | → 섹션 10.5 |
| EC2NodeClass | Karpenter | AWS 인프라 설정 (AMI, Subnet, SG) | → 섹션 10.5 |
| Application | ArgoCD | GitOps 배포 대상 정의 | → 섹션 12.2 |
| Rollout | Argo Rollouts | Canary/Blue-Green 배포 | → 섹션 12.3 |
| AnalysisTemplate | Argo Rollouts | 배포 검증 메트릭 정의 | → 섹션 12.3 |
| ServiceMonitor | Prometheus Operator | 메트릭 수집 대상 정의 | → 섹션 11.5, 2.4 |
| PrometheusRule | Prometheus Operator | 알림 규칙 정의 | → 섹션 11.5, 2.4 |
| IngressClassParams | ALB Controller | ALB Ingress 설정 | → 2.1 |
핵심: 이 CRD들은 모두 Operator Pattern을 따른다. CRD(데이터 정의) + Controller(조정 루프)가 한 쌍으로 동작하며, kubectl apply로 선언하면 Controller가 자동으로 원하는 상태를 실현한다.
11.5 ServiceMonitor / PrometheusRule
Prometheus Operator가 제공하는 핵심 CRD 2가지이다. Prometheus 설정 파일을 직접 수정하지 않고, CRD로 선언적으로 관리한다.
ServiceMonitor
정의: Prometheus가 어떤 Service의 어떤 포트에서 메트릭을 수집할지 선언적으로 정의하는 CRD이다.
핵심 포인트:
selector로 대상 Service를 지정한다 (레이블 매칭)endpoints로 수집 포트(port), 경로(path), 간격(interval)을 설정한다- Prometheus Operator가 ServiceMonitor를 Watch → Prometheus 설정(
prometheus.yml)에 scrape target을 자동 추가 - scrape interval 우선순위: ServiceMonitor 레벨
interval: 15s> Prometheus 글로벌 기본값scrape_interval: 30s
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: cgv-api-monitor
namespace: cgv-prod
labels:
release: prometheus # Prometheus Operator가 이 레이블로 ServiceMonitor를 발견
spec:
selector:
matchLabels:
app: cgv-api # 이 레이블을 가진 Service를 대상으로 수집
endpoints:
- port: http # Service에서 정의한 포트 이름
path: /actuator/prometheus # 메트릭 노출 경로 (Spring Boot Actuator)
interval: 15s # 15초마다 수집 (글로벌 기본 30초 대신)
namespaceSelector:
matchNames:
- cgv-prod
동작 흐름:
ServiceMonitor 생성
│
▼
Prometheus Operator (Watch)
│
▼
Prometheus 설정 자동 갱신 (scrape_configs에 target 추가)
│
▼
Prometheus가 15초마다 cgv-api Service의 /actuator/prometheus에서 메트릭 수집
PrometheusRule
정의: Prometheus의 알림 규칙(alerting rules)을 CRD로 선언적으로 관리하는 리소스이다.
핵심 포인트:
expr: PromQL 쿼리로 조건 정의for: 조건이 지속되어야 하는 시간 (flapping 방지)labels.severity: 심각도 (critical, warning, info)annotations: 알림 메시지, 대시보드 링크 등- Prometheus Operator가 PrometheusRule을 Watch → Prometheus의 alerting rules에 자동 추가
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: cgv-queue-alerts
namespace: monitoring
labels:
release: prometheus
spec:
groups:
- name: cgv-queue
rules:
- alert: QueueWaitingTooLong
expr: cgv_queue_waiting_count > 50000 # 대기자 5만 명 초과
for: 2m # 2분 이상 지속
labels:
severity: warning
annotations:
summary: "대기열 과부하 ({{ $value }}명 대기 중)"
description: "대기열이 50,000명을 2분 이상 초과. Pod 스케일링 확인 필요."
- alert: ActiveSessionCritical
expr: cgv_active_session_count > 4500 # Active 세션 4,500 이상 (5,000 한계)
for: 1m
labels:
severity: critical
annotations:
summary: "Active 세션 임계치 도달 ({{ $value }}/5000)"
ServiceMonitor vs PrometheusRule 비교:
| 항목 | ServiceMonitor | PrometheusRule |
|---|---|---|
| 역할 | 메트릭 수집 대상 정의 | 알림 규칙 정의 |
| Prometheus 설정 | scrape_configs 자동 생성 |
alerting.rules 자동 생성 |
| 입력 | Service endpoint (port, path) | PromQL 쿼리 + 조건 |
| 출력 | 수집된 메트릭 데이터 | AlertManager로 알림 발송 |
CGV 적용: ServiceMonitor 15초 간격으로 메트릭 수집, PrometheusRule 22개 + KEDA ScaledObject 2개로 총 24개의 알림/스케일링 규칙 운영
12. 운영 도구
12.1 Helm
정의: 쿠버네티스의 패키지 매니저이다. 여러 매니페스트를 하나의 패키지(Chart)로 묶어서 관리한다. "apt/yum의 쿠버네티스 버전"이다.
핵심 개념:
| 개념 | 설명 |
|---|---|
| Chart | 쿠버네티스 리소스 매니페스트들의 패키지. Deployment, Service, ConfigMap 등을 한 묶음으로 관리 |
| Values | Chart의 설정값을 정의하는 파일 (values.yaml). 환경별 값을 다르게 적용 가능 |
| Release | Chart를 클러스터에 설치한 인스턴스. 같은 Chart를 여러 번 설치하면 각각 별도 Release |
| Template | Go 템플릿 문법으로 작성된 매니페스트. Values의 값이 주입되어 최종 매니페스트 생성 |
| Repository | Chart를 저장하고 공유하는 저장소 |
Chart 디렉토리 구조:
mychart/
├── Chart.yaml # Chart 메타데이터 (이름, 버전)
├── values.yaml # 기본 설정값
├── values-dev.yaml # dev 환경 오버라이드
├── values-prod.yaml # prod 환경 오버라이드
├── templates/ # 매니페스트 템플릿
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ └── _helpers.tpl # 공통 헬퍼 함수
└── charts/ # 의존 Chart (subchart)
주요 명령어:
| 명령어 | 설명 |
|---|---|
helm install <release> <chart> |
Chart를 클러스터에 설치 |
helm upgrade <release> <chart> |
기존 Release를 업데이트 |
helm rollback <release> <revision> |
이전 버전으로 롤백 |
helm uninstall <release> |
Release 삭제 |
helm template <chart> |
렌더링된 매니페스트 미리 보기 (클러스터에 적용하지 않음) |
Values 오버라이드 우선순위 (낮은 → 높은):
# 1. values.yaml (Chart 기본값) — 가장 낮은 우선순위
# 2. -f 파일 (환경별 오버라이드)
# 3. --set 플래그 (커맨드라인 오버라이드) — 가장 높은 우선순위
helm upgrade cgv-prod ./platform \
-f values.yaml \
-f values-prod.yaml \
--set image.tag=v1.2.3
Template 문법 기본:
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-api # Release 이름 참조
spec:
replicas: {{ .Values.replicaCount }} # values.yaml의 값 주입
template:
spec:
containers:
- name: api
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
resources:
{{- toYaml .Values.resources | nindent 12 }} # YAML 블록 주입
CGV 적용: platform/ 디렉토리에 Helm Chart로 Deployment, Service, ConfigMap 등을 템플릿화. 환경별로 values-dev.yaml, values-prod.yaml로 분리하여 동일한 Chart에서 Dev/Prod 환경을 관리
12.2 ArgoCD
정의: GitOps 기반 Continuous Delivery 도구이다. Git 리포지토리의 매니페스트와 클러스터 상태를 자동으로 동기화한다.
핵심 포인트:
- GitOps 원칙: Git이 Single Source of Truth(유일한 진실의 원천). 클러스터 상태를 수동으로(
kubectl apply) 변경하지 않고, Git에 커밋하면 ArgoCD가 자동으로 클러스터에 적용한다 - Application CRD: 배포 대상(Git repo + path + target namespace)을 선언적으로 정의하는 Argo의 핵심 리소스
- Web UI 제공: 배포 상태, 리소스 트리, 동기화 상태를 시각적으로 확인 가능
Sync 전략:
| 전략 | 설명 |
|---|---|
| Automated Sync | Git 변경 감지 시 자동으로 클러스터에 적용 |
| Manual Sync | 수동 승인 후 적용 (Prod 환경에서 사용) |
| Self-Heal | 누군가 kubectl로 직접 변경하면 Git 상태로 되돌림 |
| Auto-Prune | Git에서 삭제된 리소스는 클러스터에서도 자동 삭제 |
ArgoCD 동작 흐름:
개발자: Git Push (values-prod.yaml 변경)
│
▼
ArgoCD Application Controller (3분마다 Git 폴링 또는 Webhook)
│
▼
변경 감지 → "OutOfSync" 상태
│
├─ Automated Sync: 즉시 적용
└─ Manual Sync: UI/CLI에서 승인 후 적용
│
▼
helm template → 렌더링된 매니페스트 → kubectl apply
│
▼
Health Check → "Synced + Healthy" 상태
helm install vs ArgoCD:
| 항목 | helm install/upgrade | ArgoCD |
|---|---|---|
| 실행 위치 | 로컬 머신에서 명령 실행 | 클러스터 내부에서 자동 실행 |
| 상태 추적 | Helm Release로 추적 | Git 커밋 해시로 추적 |
| 드리프트 감지 | 없음 (kubectl로 변경하면 모름) | Self-Heal로 자동 복원 |
| 롤백 | helm rollback (Release 번호) |
Git revert → 자동 적용 |
| 감사(Audit) | 누가 실행했는지 추적 어려움 | Git 커밋 히스토리 = 감사 로그 |
# Application CRD — ArgoCD가 배포할 대상을 선언
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cgv-prod
namespace: argocd # ArgoCD 자체 네임스페이스
spec:
project: default
source:
repoURL: https://gitlab.com/cgv/cgv-queue-system.git
targetRevision: main # Git 브랜치
path: platform # Helm Chart 경로
helm:
valueFiles:
- values-prod.yaml # Prod 환경 Values
destination:
server: https://kubernetes.default.svc # 현재 클러스터
namespace: cgv-prod # 배포 대상 네임스페이스
syncPolicy:
automated: # 자동 Sync 활성화
prune: true # Git에서 삭제된 리소스 자동 삭제
selfHeal: true # 수동 변경 시 Git 상태로 복원
syncOptions:
- CreateNamespace=true # 네임스페이스 자동 생성
Rollback (롤백):
ArgoCD에서 롤백은 Git을 통해 수행한다. 이전 버전으로 돌아가려면:
git revert <commit>→ Git에 되돌리는 커밋 생성- ArgoCD가 변경 감지 → 자동으로 이전 상태를 클러스터에 적용
- 또는 ArgoCD UI/CLI에서 직접 이전 Git 커밋으로 Sync 가능
CGV 적용: dev는 Automated Sync(자동 배포), prod는 Manual Sync(수동 승인 후 배포). Self-Heal을 활성화하여 실수로 kubectl 변경 시 자동 복원
12.3 Argo Rollouts
정의: Kubernetes Deployment를 대체하는 고급 배포 컨트롤러이다. Canary, Blue-Green 배포 전략을 지원하며, 자동 분석을 통해 배포를 진행하거나 롤백한다.
핵심 포인트:
- 기본 Deployment는 Rolling Update만 지원한다 → Rollouts는 트래픽 비율 제어 + 자동 분석이 가능하다
- Rollout CRD: Deployment와 동일한 Pod spec을 가지면서,
strategy필드에 Canary/Blue-Green 전략을 추가로 정의한다 - 기존 Deployment YAML에서
kind: Deployment→kind: Rollout으로 변경하고,strategy블록을 추가하면 마이그레이션 완료
Canary 배포
정의: 새 버전을 소수의 트래픽(예: 10%)에만 먼저 노출한 후, 문제가 없으면 단계적으로 트래픽을 늘려가는 배포 전략이다. "카나리아 새"에서 유래 (광산에서 유독 가스를 먼저 감지하는 역할).
단계별 흐름:
Step 1: setWeight 10% → 새 버전에 10% 트래픽 전송
Step 2: pause 2m → 2분 관찰
Step 3: analysis → Prometheus 메트릭으로 에러율 확인
├─ 에러율 < 5%: 통과 → 다음 단계
└─ 에러율 >= 5%: 실패 → 자동 롤백
Step 4: setWeight 50% → 50% 트래픽
Step 5: pause 5m → 5분 관찰
Step 6: analysis → 에러율 재확인
Step 7: setWeight 100% → 전체 트래픽 전환 완료
AnalysisTemplate / AnalysisRun
- AnalysisTemplate CRD: 배포 검증에 사용할 메트릭을 미리 정의한다 (Prometheus 쿼리, 성공 조건, 실패 조건)
- AnalysisRun: AnalysisTemplate의 실행 인스턴스이다. Rollout의 analysis step에 도달하면 자동으로 생성되며, 성공/실패 결과에 따라 Rollout이 진행 또는 롤백된다
# Rollout CRD — Deployment 대체, Canary 전략 적용
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: cgv-api
namespace: cgv-prod
spec:
replicas: 10
selector:
matchLabels:
app: cgv-api
template:
metadata:
labels:
app: cgv-api
spec:
containers:
- name: cgv-api
image: 123456789.dkr.ecr.ap-northeast-2.amazonaws.com/cgv-api:v1.2.3
ports:
- containerPort: 8080
strategy:
canary:
steps:
- setWeight: 10 # 10% 트래픽을 새 버전으로
- pause: { duration: 2m } # 2분 관찰
- analysis:
templates:
- templateName: error-rate-check # AnalysisTemplate 참조
- setWeight: 50 # 50% 트래픽
- pause: { duration: 5m } # 5분 관찰
- analysis:
templates:
- templateName: error-rate-check
- setWeight: 100 # 100% 전환
# AnalysisTemplate CRD — 배포 검증 메트릭 정의
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: error-rate-check
namespace: cgv-prod
spec:
metrics:
- name: error-rate
interval: 30s # 30초마다 쿼리 실행
count: 3 # 총 3번 측정
successCondition: result[0] < 0.05 # 에러율 5% 미만이면 성공
failureLimit: 1 # 1번이라도 실패하면 전체 실패 → 롤백
provider:
prometheus:
address: http://prometheus-kube-prometheus-prometheus.monitoring.svc:9090
query: |
sum(rate(http_server_requests_seconds_count{status=~"5..",namespace="cgv-prod"}[2m]))
/
sum(rate(http_server_requests_seconds_count{namespace="cgv-prod"}[2m]))
Deployment vs Argo Rollouts 비교
| 항목 | Deployment | Argo Rollouts |
|---|---|---|
| 배포 전략 | Rolling Update, Recreate | Canary, Blue-Green, A/B 테스트 |
| 트래픽 제어 | 불가 (Pod 비율로만 간접 제어) | 가중치 기반 트래픽 분배 (10%→50%→100%) |
| 자동 분석 | 없음 | AnalysisTemplate으로 메트릭 기반 검증 |
| 자동 롤백 | 없음 (수동 kubectl rollout undo) |
분석 실패 시 자동 롤백 |
| 관찰 구간 | 없음 | pause로 단계별 관찰 시간 확보 |
| 설치 | K8s 내장 | CRD + Controller 별도 설치 필요 |
| 사용 시점 | Dev 환경, 빠른 배포 | Prod 환경, 안전한 점진적 배포 |
Rolling Update의 한계 — 왜 Canary가 필요한가:
Rolling Update는 "Pod를 하나씩 교체"하지만 트래픽 비율을 제어하지 못한다.
10개 Pod 중 3개가 새 버전이면 약 30%의 트래픽이 새 버전으로 가지만, 이 비율은 Pod 수에 의존하며 정밀한 제어가 불가능하다. 또한 새 버전에 문제가 있어도 자동으로 감지하고 롤백하는 메커니즘이 없다.
Canary 배포는 이 두 가지(정밀한 트래픽 제어 + 자동 분석/롤백)를 제공한다.
kubectl로 Rollout 상태 확인:
# Rollout 상태 조회
kubectl argo rollouts get rollout cgv-api -n cgv-prod
# Rollout 수동 승인 (pause 단계에서)
kubectl argo rollouts promote cgv-api -n cgv-prod
# Rollout 수동 롤백
kubectl argo rollouts abort cgv-api -n cgv-prod
CGV 적용: dev는 Rolling Update(Deployment), prod는 Canary(Argo Rollouts). 10%→50%→100% 3단계로 진행하며, 각 단계에서 AnalysisTemplate으로 에러율 5% 초과 시 자동 롤백.
Prometheus 주소: prometheus-kube-prometheus-prometheus.monitoring.svc:9090
12.4 ArgoCD + Argo Rollouts 통합 흐름
실제 운영에서 ArgoCD와 Argo Rollouts는 함께 사용된다. 전체 배포 파이프라인을 정리한다.
개발자: Git Push (image.tag: v1.2.3 → v1.2.4)
│
▼
CI Pipeline (GitLab CI)
├─ 빌드 + 테스트
├─ Docker 이미지 빌드 (Kaniko)
└─ ECR에 이미지 Push
│
▼
ArgoCD (CD)
├─ Git 변경 감지 ("OutOfSync")
├─ Dev: Automated Sync → Deployment (Rolling Update)
└─ Prod: Manual Sync 승인
│
▼
Argo Rollouts (Canary)
├─ Step 1: 10% 트래픽 → 새 버전
├─ Step 2: AnalysisRun (에러율 확인)
│ ├─ 성공 → 50% → 분석 → 100%
│ └─ 실패 → 자동 롤백 (0%로 복원)
└─ 완료: "Synced + Healthy"
각 도구의 역할 분담:
| 도구 | 역할 | 담당 범위 |
|---|---|---|
| GitLab CI | Continuous Integration | 코드 빌드, 테스트, 이미지 빌드/Push |
| ArgoCD | Continuous Delivery | Git→클러스터 동기화, 배포 트리거 |
| Argo Rollouts | Progressive Delivery | Canary 트래픽 제어, 자동 분석/롤백 |
| Prometheus | 메트릭 수집 | AnalysisTemplate의 데이터 소스 |
| KEDA | 이벤트 드리븐 스케일링 | 대기열 메트릭 기반 Pod 오토스케일링 |
CGV 적용: GitLab CI(빌드) → ArgoCD(배포 트리거) → Argo Rollouts(Canary 진행) → Prometheus(검증 메트릭) 순서로 전체 배포 파이프라인이 동작한다
'Kubernetes' 카테고리의 다른 글
| Kubernetes 배포판과 k3s (0) | 2026.03.08 |
|---|