----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/roadmap
----------------------------------------
# 가이드북 로드맵
> **마지막 업데이트**: 2026년 9월 11일
이 가이드북은 Linux 커널에서 시작해 컨테이너, Kubernetes, Amazon EKS, 네트워킹, 서비스 메시, 스토리지, 데이터베이스, 데이터 파이프라인, AI/ML, 그리고 보안·GitOps·플랫폼 엔지니어링·컨테이너 레지스트리·옵저버빌리티·운영까지 — 클라우드 네이티브 스택 전체를 하나의 서사로 다룹니다. 이 페이지는 전체 지도이자 추천 학습 경로입니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-roadmap-0.html)
## 도메인 지도
| 계층 | 도메인 | 시작점 | 한 줄 요약 |
|------|--------|--------|-----------|
| 기초 | Linux & Container | [Linux 기초](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md) | 커널, 네임스페이스, cgroup — 컨테이너의 실체 |
| 오케스트레이션 | Kubernetes 핵심 개념 | [Kubernetes 소개](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md) | 워크로드·스케줄링·오토스케일링까지 K8s 그 자체 |
| 오케스트레이션 | Amazon EKS | [EKS 소개](https://www.atomai.click/kubernetes-docs/llms/ko/eks/01-eks-introduction.md) | 클러스터 생성부터 하이브리드/Auto Mode까지 |
| 연결 | Networking | [네트워크 기초](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part1.md) | 프로토콜 25개부터 CNI(Cilium/Calico)까지 |
| 연결 | Service Mesh | [Istio](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/istio/README.md) | Istio/Linkerd/Cilium Mesh — mTLS 레이턴시 실측 포함 |
| 상태 | Storage | [Storage 개요](https://www.atomai.click/kubernetes-docs/llms/ko/storage/README.md) | EBS gp2 vs gp3 fio 실측 벤치마크 |
| 상태 | Database | [Database 개요](https://www.atomai.click/kubernetes-docs/llms/ko/database/README.md) | Operator 지형과 ClickHouse 1억 행 실측 |
| 데이터·AI | Data Pipeline | [Data on EKS 개요](https://www.atomai.click/kubernetes-docs/llms/ko/data-on-eks/README.md) | Kafka·Spark·Airflow·Flink 딥다이브 — Kafka RF3/gp3 ingest 상한 실측 포함 |
| 데이터·AI | AI/ML | [AI/ML 워크로드](https://www.atomai.click/kubernetes-docs/llms/ko/ai-ml/01-ai-ml-workloads.md) | vLLM·Ray·Kubeflow·MLflow on EKS |
| 횡단 | Security & Policy | [Kyverno](https://www.atomai.click/kubernetes-docs/llms/ko/security/01-kyverno-policy-management.md) | 인증/인가, 정책, 런타임 보안, 공급망 |
| 횡단 | GitOps | [GitOps](https://www.atomai.click/kubernetes-docs/llms/ko/gitops/README.md) | ArgoCD·Flux·Progressive Delivery |
| 횡단 | Platform Engineering | [개요](https://www.atomai.click/kubernetes-docs/llms/ko/platform-engineering/00-platform-engineering-overview.md) | ACK·KRO·Crossplane·Backstage |
| 횡단 | Container Registry | [개요](https://www.atomai.click/kubernetes-docs/llms/ko/container-registry/README.md) | ECR·Harbor·이미지 공급망 |
| 횡단 | Observability | [개요](https://www.atomai.click/kubernetes-docs/llms/ko/observability/README.md) | 메트릭·로그·트레이싱·알림 스택 |
| 횡단 | Operations Guide | [운영 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/ops/README.md) | 용량 계획·FinOps·업그레이드와 증상 기반 [트러블슈팅 플레이북](https://www.atomai.click/kubernetes-docs/llms/ko/ops/16-troubleshooting-playbook.md) |
## 실측 벤치마크 시리즈
기존 AWS 실행에서 보고한 수치를 담은 문서들입니다. 측정 환경·반복 횟수·캐시·원시 기록의 공개 범위와 한계를 먼저 확인하고 현재 용량 계획에는 재측정합니다:
- [Istio sidecar vs ambient 실측](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/istio/comparison/03-sidecar-vs-ambient.md) — mTLS 데이터플레인별 P50/P99 레이턴시와 rollout 중 503 비율
- [EBS gp2 vs gp3 실측 벤치마크](https://www.atomai.click/kubernetes-docs/llms/ko/storage/01-ebs-gp2-gp3-benchmark.md) — 같은 100GiB에서 IOPS 10배 차이와 gp2 버스트 크레딧 절벽
- [ClickHouse on EKS 실측 벤치마크](https://www.atomai.click/kubernetes-docs/llms/ko/database/01-clickhouse-on-eks.md) — 1억 행 ingest 처리량, 압축률, 쿼리 레이턴시
- [Kafka on EKS 실측 벤치마크](https://www.atomai.click/kubernetes-docs/llms/ko/data-on-eks/kafka/09-kafka-benchmark.md) — RF3 클러스터 ingest 상한 ≈130–135 MiB/s(= gp3 볼륨 1개 쓰기 캡)과 RF1 338 MiB/s, acks별 p99, 콜드 컨슈머가 프로듀서 처리량을 약 45% 깎는 현상
- [Pod 네트워크 실측 벤치마크](https://www.atomai.click/kubernetes-docs/llms/ko/networking/06-pod-network-benchmark.md) — 같은 노드 0.040 ms → 같은 AZ 0.339 ms → 다른 AZ 0.544 ms RTT 사다리, AZ와 무관한 단일 TCP 플로우 4.96 Gbps 상한과 8플로우 9.94 Gbps, `ndots:5`의 10쿼리/8 NXDOMAIN 증폭
## 다이어그램 공유하기 — LinkedIn·발표용 내보내기
이 가이드북의 인터랙티브 다이어그램은 `https://www.atomai.click/kubernetes-docs/archmaps/<이름>.html`에서 열리고, 뷰어 툴바의 **Export** 버튼(단축키 `E`)이 공유용 파일을 바로 만들어 줍니다. 사용 가능한 메뉴는 해당 뷰어 버전·브라우저 지원·선택 상태에 따라 다릅니다.
### Export 메뉴 구성
| 그룹 | 메뉴 항목 | 결과물 | 언제 쓰나 |
|------|-----------|--------|-----------|
| Share | **Share Card** / **Copy Share Card** | 1200×630 PNG (다운로드 / 클립보드) | LinkedIn·X 링크 미리보기, README, 릴리스 노트 |
| Share | **Route Share Card** | 1200×630 PNG (다운로드 전용) | Route Probe(`R`)로 두 노드 사이 경로를 추적한 뒤에만 나타남 |
| Share | **Reach Share Card** | 1200×630 PNG (다운로드 전용) | Semantic Passport에서 노드의 upstream/downstream 도달성을 조회한 뒤에만 나타남 |
| Share | **Copy diagram** | 전체 다이어그램 PNG를 클립보드로 | 슬라이드·문서에 바로 붙여넣기 |
| Image | **PNG** / **JPEG** / **WebP** | 전체 다이어그램 래스터 이미지 | 무손실이 필요하면 PNG, 용량이 중요하면 JPEG/WebP |
| Vector & motion | **SVG** | 라이트·다크 테마를 모두 담은 벡터 | 확대해도 깨지지 않는 발표 자료 |
| Vector & motion | **WebM** | 트레이스 애니메이션 6초 녹화 | 피드에서 흐름이 실제로 "움직이는" LinkedIn 포스트 |
내보내기 결과물에서는 뷰어 상태(가이드 패널, 렌즈, 검색창, 포커스, 경로, 스토리, 카메라 위치, 레이더, 프레젠테이션 모드, 임시 오버레이)가 모두 제거되고 다이어그램 본체만 남습니다. Share Card는 현재 테마와 비주얼 프리셋을 그대로 쓰되 다이어그램 전체를 잘림 없이 담습니다. WebM 녹화는 트레이스 애니메이션이 있는 다이어그램과 브라우저의 MediaRecorder 지원이 필요하며, 미지원 브라우저에서는 메뉴가 그렇게 알려줍니다.
### LinkedIn 포스팅용 30초 레시피
1. **다이어그램 열기** — 문서에 삽입된 다이어그램 아래의 "전체 화면으로 열기 ↗"(GitBook에서는 "🔍 인터랙티브 다이어그램 보기") 링크를 클릭합니다.
2. **트레이스 재생 확인** — 툴바의 **Live/Still** 토글이 Live인지 확인합니다. 화살표를 따라 흐르는 이 모션이 영상에 담기는 내용입니다. 발표 리허설이라면 **Presentation stage**(`F`)로 다이어그램에 화면 전체를 내어 주세요.
3. **Export → WebM**(움직이는 포스트) 또는 **Export → Share Card**(1200×630 정적 미리보기) — WebM은 "Recording 6 seconds of motion…" 표시 후 파일이 내려옵니다.
4. **포스트** — 대상 플랫폼의 현재 업로드 형식을 확인하고 필요하면 WebM을 지원 형식으로 변환합니다. Share Card는 이미지로 올리고 원문 문서 URL을 함께 붙입니다. 특정 노드·경로·스토리 장면을 짚어 주려면 Semantic Passport와 Route Probe의 **Copy link**, Story Beat의 **Copy moment**(스토리 챕터가 정의된 다이어그램에서만 보입니다)로 딥링크를 복사해 댓글이나 슬라이드에 넣으세요.
### 내보내기의 한계 — 정직하게 말하기
- 내보낸 파일은 **커뮤니케이션 자산**입니다. 아키텍처가 검증됐다는 증거가 아니며, 게시된 원본 HTML과 작성자의 검증 과정을 대신하지 않습니다. Share Card에도 "검증됨" 같은 표시는 붙지 않습니다.
- Route Share Card는 **작성자가 명시한 방향성 관계**만 따라 계산된 경로입니다. 도형이 가까이 있다는 이유로 경로를 추측하지 않고, 경로가 바뀌었거나 도달 불가능하면 내보내기를 거부합니다.
- Reach Share Card가 보여 주는 것은 *authored reachability*입니다. 영향 범위(impact), 장애 반경(blast radius), 장애 전파(breakage), 런타임 인과관계로 해석해 소개하지 마세요.
## 추천 학습 경로
### ① 인프라 입문 — "컨테이너부터 EKS까지"
Linux 기초 → 컨테이너 기술 → Kubernetes 소개 → 핵심 개념(파드/서비스/스토리지/구성) → EKS 클러스터 생성 → 네트워크 기초. 각 문서의 퀴즈로 이해를 점검하고, [실습 랩](https://www.atomai.click/kubernetes-docs/ko/labs/)을 병행하세요.
### ② 플랫폼/SRE — "운영 가능한 클러스터"
EKS 운영(업그레이드/문제 해결/복원력) → Networking(VPC CNI, Cilium) → Service Mesh 비교 가이드 → Security & Policy → Observability 스택 → GitOps → Operations Guide의 용량 계획/FinOps. 실측 벤치마크 시리즈가 이 경로의 판단 근거를 제공합니다.
### ③ 데이터·AI 플랫폼 — "상태와 데이터의 세계"
Storage → Database → Data Pipeline(Kafka → Spark → Airflow → Flink) → AI/ML(vLLM → Ray → Kubeflow). GPU/스케줄링이 필요하면 Kubernetes 핵심 개념의 Custom Scheduler 파트를 함께 보세요.
## AI와 함께 읽기
이 가이드북은 llms.txt 제안 형식의 색인과 원문을 제공합니다. AI 도구의 웹 가져오기 기능이나 MCP 연결이 필요하며, URL 하나만 제공한다고 전체 문서를 자동으로 읽거나 색인한다는 보장은 없습니다. 엔드포인트와 활용 예시는 [LLM과 함께 읽기](https://www.atomai.click/kubernetes-docs/llms/ko/llm-guide.md)에서 확인하세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/llm-guide
----------------------------------------
# LLM과 함께 읽기 — llms.txt와 MCP
> **마지막 업데이트**: 2026년 9월 11일
이 가이드북은 [llms.txt 제안 형식](https://llmstxt.org/)과 문서별 Markdown을 제공합니다. URL을 읽을 수 있는 AI 도구에는 색인을 전달하고, LLM Wiki나 RAG에는 문서 목록과 원문을 수집하며, 로컬 MCP 클라이언트에는 검색·본문 조회 도구를 연결할 수 있습니다. `llms.txt`가 존재한다고 모든 AI가 자동으로 발견하거나 검색하는 것은 아닙니다. 사용하는 도구에 웹 가져오기 기능이나 MCP 연결이 필요합니다.
## 엔드포인트
| URL | 내용 | 용도 |
|-----|------|------|
| [llms.txt](https://www.atomai.click/kubernetes-docs/llms.txt) | 본문 문서의 그룹·제목·요약과 순수 Markdown URL 색인 (퀴즈·랩은 `## Optional`의 목록 페이지 링크로) | LLM이 필요한 페이지만 골라 읽게 할 때 |
| [문서 manifest](https://www.atomai.click/kubernetes-docs/llms/manifest.json) | 문서 ID, 언어, 섹션, 제목, 소제목, 설명, 웹/Markdown URL, 갱신일, SHA-256, UTF-8 바이트 수 | LLM Wiki·RAG의 선택 수집과 변경 감지 |
| [llms-full-ko.txt](https://www.atomai.click/kubernetes-docs/llms-full-ko.txt) | 한국어 전체 본문 (마크다운) | 컨텍스트에 통째로 넣거나 RAG 인덱싱 |
| [llms-full-en.txt](https://www.atomai.click/kubernetes-docs/llms-full-en.txt) | 영어 전체 본문 (마크다운) | 영어 기반 도구/파이프라인 |
| `llms-full-<언어>-<섹션>.txt` (예: [llms-full-ko-networking.txt](https://www.atomai.click/kubernetes-docs/llms-full-ko-networking.txt)) | 사이드바 섹션 하나의 본문만 합친 파일. 전체 목록은 `llms.txt`의 `## Section bundles` 절에 | 한 섹션만 컨텍스트에 넣을 때 — 전체 파일은 한 번에 넣기엔 너무 큽니다 |
모든 파일과 문서별 Markdown은 사이트 빌드에서 같은 소스를 기준으로 생성됩니다. 성공한 배포가 반영된 뒤에 공개 콘텐츠와 일치하며, 로컬 변경이나 아직 배포되지 않은 main 변경이 즉시 공개되는 것은 아닙니다. `llms.txt`의 본문 링크는 `/llms/<언어>/<원본 경로>.md` 형식이며, VitePress HTML·사이드바·스크립트 없이 해당 문서의 Markdown만 반환합니다. 원문의 상대 링크는 모두 절대 URL로 바뀌어 있습니다 — 수집 범위의 문서 링크는 Markdown URL로, 퀴즈·랩·언어 루트는 웹페이지 URL로, 이미지 등 자산은 GitHub 원본 파일 URL로 — 그래서 LLM이 문서 하나만 받아도 참조를 그대로 따라갈 수 있습니다. Markdown을 제공하는 본문 HTML 페이지의 `
`에도 ``으로 같은 Markdown URL이 걸려 있어, 에이전트가 웹페이지 URL만 받아도 Markdown 원문을 찾아갈 수 있습니다. 퀴즈는 `## Optional` 절에 퀴즈 목록 페이지 링크(언어별 하나)로만 등장하고, 개별 퀴즈 페이지(정답 포함)는 색인과 full 파일 어디에도 들어가지 않습니다 — LLM 컨텍스트에 정답지를 섞지 않기 위해서입니다. 랩 가이드는 색인에서는 마찬가지로 목록 페이지 링크(언어별 하나)로만 나타나지만, full 파일에는 본문과 함께 포함됩니다.
## LLM Wiki의 자료 소스로 수집하기
여기서 LLM Wiki는 원문을 보관하고 AI가 주제별 지식 문서로 정리하는 방식을 뜻합니다. 이 사이트는 수집할 원문과 메타데이터를 제공합니다. Wiki의 생성·갱신이나 임베딩은 사용하는 수집 도구에서 수행합니다.
1. `llms/manifest.json`을 읽고 `locale`과 `section`으로 필요한 문서를 고릅니다. 한영 중복 수집을 피하려면 언어를 하나 선택합니다.
2. 각 항목의 `markdownUrl`에서 Markdown을 받고 `id`를 키로 원문을 보관합니다. `sha256`은 해당 Markdown의 UTF-8 바이트를 해시한 값이므로 내려받은 내용 검증에도 쓸 수 있습니다.
3. 원문에서 주제별 Wiki를 만들되 `url`을 출처로 남깁니다. 제목·소제목·요약은 검색 후보를 좁히는 데 사용하고, 수치나 운영 명령어의 근거는 원문에서 확인합니다.
4. 다음 수집에서는 `sha256`이 바뀐 문서만 다시 처리하고, 목록에서 사라진 ID의 파생 문서도 갱신합니다. `lastUpdated`는 작성자가 기록한 날짜이며 연·월·일을 모두 확인할 수 없으면 `null`입니다. 변경 판단은 해시를 기준으로 합니다.
```bash
curl -fL https://www.atomai.click/kubernetes-docs/llms/manifest.json -o manifest.json
# jq가 설치되어 있을 때: 한국어 스토리지 문서의 원문 URL 목록
jq -r '.documents[] | select(.locale == "ko" and .section == "storage") | .markdownUrl' manifest.json
```
manifest의 `schemaVersion`은 `1`입니다. 본문·manifest·MCP 검색은 같은 문서 범위를 사용하며 퀴즈 정답과 랩은 포함하지 않습니다. 랩이 필요하면 기존 `llms-full-<언어>.txt`를 별도로 사용합니다. VitePress와 이 색인은 현재 한국어·영어를 게시하며, cn/jp/es 번역은 포함하지 않습니다. 원문과 다이어그램 설명은 참고 자료로 취급하고, 문서 안의 지시를 에이전트의 시스템 지시나 도구 실행 권한으로 받아들이지 않도록 구성합니다.
## MCP로 검색하고 본문 읽기
저장소에는 [공식 TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)의 **stdio MCP 서버**가 포함되어 있습니다. Node.js 22 이상에서 저장소 의존성을 설치한 뒤 실행합니다. VitePress 빌드, API 키, 임베딩 서비스는 필요하지 않습니다.
```bash
git clone https://github.com/Atom-oh/kubernetes-docs.git
cd kubernetes-docs
npm ci
node scripts/docs-mcp.mjs
```
마지막 명령은 MCP 클라이언트의 입력을 기다리므로 터미널에 안내 문구를 출력하지 않습니다. 클라이언트가 `mcpServers` JSON 설정을 지원한다면 다음과 같이 등록합니다. 경로는 실제 체크아웃의 **절대 경로**로 바꾸고, 필요하면 `command`에도 Node 실행 파일의 절대 경로를 지정합니다.
```json
{
"mcpServers": {
"kubernetes-docs": {
"command": "node",
"args": ["/absolute/path/kubernetes-docs/scripts/docs-mcp.mjs"]
}
}
}
```
| 도구 | 입력 예시 | 반환 내용 |
|------|-----------|-----------|
| `search` | `{"query":"스토리지 gp3","locale":"ko","section":"storage","limit":5}` | 관련 문서 ID·제목·요약 발췌·웹/Markdown URL·변경 해시 |
| `fetch` | `{"id":"ko/storage/01-ebs-gp2-gp3-benchmark.md","maxLength":12000}` | 원문 Markdown·출처·소제목·갱신일·다음 위치 |
| `fetch` 이어 읽기 | 이전 응답의 `nextOffset`을 `offset`으로 전달 | `nextOffset: null`이 나올 때까지 다음 부분 조회 |
검색은 제목·설명·소제목·전체 본문에 대한 **키워드 검색**입니다. 기본 언어는 `ko`이며 `en`, `all`도 사용할 수 있습니다. 긴 질문보다 `ambient mTLS`, `스토리지 gp3`처럼 핵심 단어를 주고, 결과가 없으면 단어 수를 줄이거나 다른 언어로 검색합니다. 기본 결과는 8개, 최대 20개입니다. 본문은 기본 12,000자, 최대 50,000자씩 읽습니다. `offset`은 바이트가 아닌 JavaScript 문자열 위치이므로 계산해서 만들지 말고 반환된 `nextOffset`을 그대로 사용합니다.
서버는 시작할 때 로컬 `ko/`, `en/`과 `SUMMARY.md`를 읽습니다. **사이트를 실시간으로 가져오지는 않습니다.** 저장소 갱신 후 MCP 서버를 재시작해야 변경이 검색에 반영됩니다. 검색에 등록된 ID만 읽으며 임의의 파일 경로나 외부 URL은 조회하지 않습니다. 원문 URL은 공개 사이트를 가리키므로 아직 배포하지 않은 로컬 편집과 공개 문서에는 차이가 있을 수 있습니다.
GitHub Pages는 정적 파일을 제공하므로 이 사이트의 URL을 원격 MCP 주소로 등록할 수는 없습니다. 웹에서 연결하는 원격 MCP가 필요하면 별도 실행 환경에 [Streamable HTTP 전송](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports)을 구현하고 접근 제어와 운영 정책을 정해야 합니다. 이 저장소에 포함된 것은 로컬 stdio 서버입니다.
## 활용 예시
**대화형 AI에게 특정 주제 질문하기** — 색인을 주고 필요한 페이지만 읽게 합니다:
```text
https://www.atomai.click/kubernetes-docs/llms.txt 를 읽고,
Istio ambient 모드의 mTLS 레이턴시 실측 결과가 있는 문서를 찾아
sidecar와 비교해서 요약해줘.
```
**Claude Code / 코딩 에이전트에서** — 작업 컨텍스트로 주입:
```text
이 클러스터의 스토리지 클래스를 정리하려고 해.
근거 자료: https://www.atomai.click/kubernetes-docs/llms/ko/storage/01-ebs-gp2-gp3-benchmark.md
gp2 PVC를 gp3로 마이그레이션하는 계획을 세워줘.
```
**RAG 파이프라인 인덱싱** — full 파일 하나만 내려받아 청킹:
```bash
curl -fL https://www.atomai.click/kubernetes-docs/llms-full-ko.txt -o guidebook-ko.txt
# 각 문서는 "Source: " 구분자로 나뉘어 있어 문서 단위 청킹이 쉽습니다
```
## 형식 안내
- `llms.txt` — `# 제목` / `> 요약` / `## Machine-readable catalog` / `## Docs (한국어)` / `## Docs (English)` / `## Section bundles (…)` / `## Optional`로 구성된 색인입니다. 문서 항목은 `그룹 · 제목`, 순수 Markdown URL, 첫 본문 문단의 짧은 요약을 제공합니다.
```text
- [Kubernetes 핵심 개념 · 클러스터 아키텍처](https://www.atomai.click/kubernetes-docs/llms/ko/core/01-cluster-architecture.md): Kubernetes 컨트롤 플레인과 워커 노드의 구성 요소를 설명합니다.
```
- `llms/<언어>/<경로>.md` — 문서 한 개의 원본 Markdown입니다. 렌더링된 웹페이지의 전체 내비게이션을 함께 읽지 않아도 됩니다.
- `llms-full-*.txt` — 언어별 본문을 합친 파일입니다. 이미 색인 역할을 하는 루트 `README.md`는 제외되며, 각 문서 앞에는 아래 구분자 블록이 붙습니다:
```text
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/core/01-cluster-architecture
----------------------------------------
```
- 크기 주의: full 파일은 언어별로 수 MiB 규모입니다. 한 번의 프롬프트 컨텍스트에 다 넣기보다, 색인에서 요약을 보고 필요한 문서별 Markdown만 읽게 하는 편이 대부분의 도구에서 더 잘 동작합니다.
## 다이어그램은 사람에게 — 내보내기 링크
텍스트 기반 수집기는 iframe을 실행하거나 이미지 속 노드와 연결을 자동으로 해석하지 않습니다. Markdown의 alt 텍스트·주변 설명·Mermaid 코드가 텍스트 근거가 됩니다. 그림에만 있는 정보는 이미지 인식 도구로 확인하거나 본문 설명을 보강해야 하며, 짧은 alt 텍스트만으로 전체 구성을 추론해서는 안 됩니다.
full 파일과 문서별 Markdown은 원문을 그대로 담기 때문에, 각 다이어그램의 설명(alt 텍스트)과 인터랙티브 뷰어 URL(`https://www.atomai.click/kubernetes-docs/archmaps/<이름>.html`)도 텍스트로 들어 있습니다. LLM은 그 설명으로 다이어그램의 내용을 파악하고, 사람은 그 URL을 열어 뷰어의 **Export** 메뉴에서 PNG/JPEG/WebP, 라이트·다크 겸용 SVG, 트레이스 애니메이션 6초 WebM, 1200×630 Share Card를 바로 받을 수 있습니다. 메뉴 항목별 용도와 LinkedIn 포스팅 레시피는 [가이드북 로드맵](https://www.atomai.click/kubernetes-docs/llms/ko/roadmap.md)의 "다이어그램 공유하기 — LinkedIn·발표용 내보내기" 섹션에 정리해 두었습니다. 내보낸 파일은 커뮤니케이션 자산일 뿐, 아키텍처 검증 증거는 아닙니다.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/news/
----------------------------------------
# 소식
> **마지막 업데이트**: 2026년 9월 12일
Kubernetes, Amazon EKS와 CNCF 소식에 따른 문서 변경 이력입니다. GitHub Actions는 매주 월요일 09:00 KST에 갱신안을 만들고 품질 검사를 통과하면 PR을 엽니다. 실제 사이트에는 PR 검토·머지와 배포가 완료된 뒤 반영됩니다.
아래 주차는 로그에 기록한 주차이며 원문의 발표일과 다를 수 있습니다. 당시의 변경 기록이므로 현재 지원 버전·보안 패치·운영 조건은 연결된 문서와 공식 자료에서 확인하세요. “매칭 문서 없음”도 당시 자동 매칭 결과입니다.
## 갱신 로그
- 2026-W36: [core/01-cluster-architecture.md](https://www.atomai.click/kubernetes-docs/llms/ko/core/01-cluster-architecture.md) — Kubernetes v1.37 "Garhwal" 정식 릴리스(파드 인증서/ClusterTrustBundle Stable, Metrics API GA, kube-dns·IPVS 모드 사용 중단 등) 반영
- 2026-W36: [service-mesh/istio/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/istio/README.md) — Istio 1.30.4/1.29.7 보안 패치 릴리스(ISTIO-SECURITY-2026-006, Envoy CVE 13건) 반영
- 2026-W36: [gitops/argocd/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/gitops/argocd/README.md) — ArgoCD v3.5.2/v3.4.8 패치 릴리스 반영
- 2026-W36: [service-mesh/linkerd/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/linkerd/README.md) — Linkerd edge-26.8.4(TLSRoute API 버전 협상 등) 반영
- 2026-W36: 매칭 문서 없음 — Amazon EKS, 클러스터당 최대 10개의 외부 OIDC 자격 증명 공급자 연결 지원 ([원문](https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-eks-multiple-oidc-providers))
- 2026-W36: 매칭 문서 없음 — Kubernetes GPU 워크로드를 위한 예측형(predictive) 오토스케일링, CNCF 블로그 ([원문](https://www.cncf.io/blog/2026/08/28/scale-before-the-spike-predictive-autoscaling-for-gpu-workloads-on-kubernetes/))
- 2026-W36: 매칭 문서 없음 — Kubernetes 위에 AI 팩토리 구축하기, CNCF 블로그 ([원문](https://www.cncf.io/blog/2026/08/27/building-an-ai-factory-on-kubernetes/))
- 2026-W35: [gitops/argocd/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/gitops/argocd/README.md) — Amazon EKS 관리형 Argo CD 기능의 `argocd-cm` ConfigMap 기반 사용자 지정 구성 지원 반영
- 2026-W35: [core/01-cluster-architecture.md](https://www.atomai.click/kubernetes-docs/llms/ko/core/01-cluster-architecture.md) — Kubernetes v1.36.4/v1.35.8/v1.34.11 패치 릴리스 및 v1.37.0-rc.1 반영
- 2026-W35: [networking/cilium/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/README.md) — Cilium 1.20.1/1.19.7/1.18.13 패치 릴리스 반영
- 2026-W35: [autoscaling/02-karpenter.md](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/02-karpenter.md) — Karpenter v1.14.1 패치 릴리스 반영
- 2026-W35: [service-mesh/istio/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/istio/README.md) — Istio 1.31.0-rc.0 릴리스(1.31 RC 단계 진입) 반영
- 2026-W35: [observability/tracing/03-opentelemetry.md](https://www.atomai.click/kubernetes-docs/llms/ko/observability/tracing/03-opentelemetry.md) — 느린 SQL 쿼리를 OTel 스팬 파생 메트릭으로 정제하는 CNCF 블로그 가이드 반영
- 2026-W35: 매칭 문서 없음 — Amazon EKS, 클러스터 인증 기관(CA) 로테이션 및 자동 수명 주기 관리 지원 ([원문](https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-eks-certificate-authority-ca-rotation-automated-lifecycle-management))
- 2026-W35: 매칭 문서 없음 — Kubeflow, CNCF 졸업(graduated) 프로젝트 승격 ([원문](https://www.cncf.io/announcements/2026/08/17/cncf-announces-kubeflows-graduation-solidifying-the-standard-for-cloud-native-ai-operations/))
- 2026-W34: [gitops/argocd/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/gitops/argocd/README.md) — ArgoCD v3.5.0 정식 릴리스 및 v3.5.1/v3.4.7/v3.3.14 패치 릴리스 반영
- 2026-W34: [service-mesh/istio/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/istio/README.md) — Istio 1.31.0-beta.1 릴리스(1.31 베타 단계 진입) 반영
- 2026-W34: [service-mesh/linkerd/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/linkerd/README.md) — Linkerd edge-26.8.2(Gateway API 1.5.1 지원, 테스트 최대 k8s 1.36) 반영
- 2026-W34: 매칭 문서 없음 — Amazon EKS, Kubernetes 컨트롤 플레인 구성 파라미터(스케줄러/컨트롤러 매니저/API 서버 튜닝) 지원 ([원문](https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-eks-control-plane-configuration-parameters))
- 2026-W34: 매칭 문서 없음 — Cloud Native Buildpacks, CNCF 졸업(graduated) 프로젝트 승격 ([원문](https://www.cncf.io/announcements/2026/08/11/cncf-announces-graduation-of-cloud-native-buildpacks-advancing-the-standard-for-container-builds/))
- 2026-W34: 매칭 문서 없음 — KubeCon + CloudNativeCon North America 2026 일정 공개, AI Inference + Agentic 트랙 신설 ([원문](https://www.cncf.io/announcements/2026/08/10/cncf-reveals-kubecon-cloudnativecon-north-america-2026-schedule-adds-new-ai-inference-agentic-track/))
- 2026-W34: 매칭 문서 없음 — Kubernetes YAML을 KYAML로 예쁘게 출력하기, Kubernetes 블로그 ([원문](https://kubernetes.io/blog/2026/08/11/how-to-pretty-print-kubernetes-yaml-as-kyaml/))
- 2026-W33: [networking/04-gateway-api.md](https://www.atomai.click/kubernetes-docs/llms/ko/networking/04-gateway-api.md) — Gateway API v1.6(TCPRoute/UDPRoute Standard v1 승격, 채널별 deprecated API 제공 여부 변경) 반영
- 2026-W33: [gitops/argocd/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/gitops/argocd/README.md) — ArgoCD v3.5.0 GA(Helm 4 마이그레이션, 소스 무결성 검증 알파, ApplicationSet 개선) 반영
- 2026-W33: [networking/cilium/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/README.md) — Cilium 1.20.0 GA(Gateway API v1.6.1, KCNP, multi-pool IPAM 마이그레이션) 및 1.21.0-pre.0 반영
- 2026-W33: [core/01-cluster-architecture.md](https://www.atomai.click/kubernetes-docs/llms/ko/core/01-cluster-architecture.md) — Kubernetes v1.37 스니크 픽, 문서 프리즈 발효, v1.38.0-alpha.0 태그 반영
- 2026-W33: 매칭 문서 없음 — Amazon ECR, Docker push 이미지 레이어 최대 200GB 지원 ([원문](https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-ecr-image-layers/))
- 2026-W33: 매칭 문서 없음 — K8gb, CNCF 인큐베이팅 프로젝트 승격 ([원문](https://www.cncf.io/announcements/2026/08/05/k8gb-becomes-a-cncf-incubating-project/))
- 2026-W33: 매칭 문서 없음 — OpenCost 1.121.0, Kubernetes 추론(inference) 비용 추적 기능 추가 ([원문](https://www.cncf.io/blog/2026/08/05/opencost-1-121-0-first-of-a-kind-kubernetes-inference-cost-tracking/))
- 2026-W33: 매칭 문서 없음 — Kubernetes DRA가 HAMi를 대체할까?, CNCF 블로그 ([원문](https://www.cncf.io/blog/2026/08/07/does-kubernetes-dra-replace-hami/))
- 2026-W31: [core/01-cluster-architecture.md](https://www.atomai.click/kubernetes-docs/llms/ko/core/01-cluster-architecture.md) — Kubernetes v1.36.3/v1.35.7/v1.34.10 패치 릴리스 및 v1.37 코드 프리즈 발효 반영
- 2026-W31: [eks-auto-mode/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md) — EKS Auto Mode 노드 풀의 EFA·EC2 배치 그룹 지원 반영
- 2026-W31: [autoscaling/02-karpenter.md](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/02-karpenter.md) — Karpenter 노드 풀의 EFA·EC2 배치 그룹 지원 반영
- 2026-W31: [observability/metrics/01-prometheus.md](https://www.atomai.click/kubernetes-docs/llms/ko/observability/metrics/01-prometheus.md) — AMP 워크스페이스 한도 상향(활성 시계열 15억 개, 규칙 20만 개) 반영
- 2026-W31: [observability/tracing/03-opentelemetry.md](https://www.atomai.click/kubernetes-docs/llms/ko/observability/tracing/03-opentelemetry.md) — OpenTelemetry CNCF 졸업(graduation) 반영
- 2026-W31: [networking/calico/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/networking/calico/README.md) — Tigera의 Calico for VMs on Kubernetes 출시(eBPF 기반 VM+컨테이너 통합 네트워킹) 반영
- 2026-W31: [networking/cilium/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/README.md) — Cilium 1.20.0-rc.1 릴리스 후보 반영
- 2026-W31: 매칭 문서 없음 — Confidential Containers, CNCF 인큐베이팅 프로젝트 승격 ([원문](https://www.cncf.io/blog/2026/07/22/confidential-containers-becomes-a-cncf-incubating-project/))
- 2026-W31: 매칭 문서 없음 — Kubernetes CSI 드라이버 경로 순회(path traversal) CVE 2건 (CVE-2026-3864 NFS / CVE-2026-3865 SMB; csi-driver-nfs v4.13.1, csi-driver-smb v1.20.1에서 수정) ([원문](https://www.sentinelone.com/blog/mount-here-read-there-twin-path-traversal-cves-in-kubernetes-storage/))
- 2026-W30: [networking/cilium/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/README.md) — Cilium 1.19.6/1.18.12/1.17.18 패치 릴리스 및 CVE-2026-56743(ipBlock NetworkPolicy 이슈) 반영
- 2026-W30: [service-mesh/istio/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/istio/README.md) — Istio 1.30.3/1.29.6 패치 릴리스 반영
- 2026-W30: [service-mesh/linkerd/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/linkerd/README.md) — Linkerd edge-26.7.1(미정의 서비스 포트 요청 차단, breaking) 반영
- 2026-W30: [eks-auto-mode/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md) — EKS Auto Mode의 ARC zonal shift/autoshift 지원 반영
- 2026-W30: [ops/15-zonal-operations-guide.md](https://www.atomai.click/kubernetes-docs/llms/ko/ops/15-zonal-operations-guide.md) — EKS Auto Mode의 ARC zonal shift 지원 반영
- 2026-W30: [autoscaling/02-karpenter.md](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/02-karpenter.md) — Karpenter 구버전 라인의 패치 릴리스(v1.3.8~v1.11.3) 반영
- 2026-W30: [core/01-cluster-architecture.md](https://www.atomai.click/kubernetes-docs/llms/ko/core/01-cluster-architecture.md) — Kubernetes v1.37.0-beta.0 및 v1.37 릴리스 일정 반영
- 2026-W30: [gitops/argocd/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/gitops/argocd/README.md) — ArgoCon Japan 2026과 Argo CD 3.5 로드맵 공유 예정 소식 반영
- 2026-W30: [observability/metrics/01-prometheus.md](https://www.atomai.click/kubernetes-docs/llms/ko/observability/metrics/01-prometheus.md) — Kubernetes 블로그의 커스텀 메트릭 익스포터 작성 가이드 반영
- 2026-W30: 매칭 문서 없음 — HAMi, CNCF 인큐베이팅 프로젝트로 승격 ([원문](https://www.cncf.io/blog/2026/07/15/hami-becomes-a-cncf-incubating-project/))
- 2026-W30: 매칭 문서 없음 — Kubernetes에서 vLLM으로 셀프 호스팅 LLM 운영하기, CNCF 블로그 ([원문](https://www.cncf.io/blog/2026/07/16/running-a-self-hosted-llm-in-kubernetes-with-vllm/))
- 2026-W29: [security/10-cert-manager.md](https://www.atomai.click/kubernetes-docs/llms/ko/security/10-cert-manager.md) — ACM의 ACME 프로토콜 지원(cert-manager에서 ACM 퍼블릭 인증서 발급 가능) 반영
- 2026-W29: [observability/tracing/03-opentelemetry.md](https://www.atomai.click/kubernetes-docs/llms/ko/observability/tracing/03-opentelemetry.md) — NGINX + OpenTelemetry 기반 AI 에이전트 네트워크 경계 관측 패턴 반영
- 2026-W29: 매칭 문서 없음 — AI 네이티브 워크로드를 위한 플랫폼 엔지니어링의 진화, CNCF 블로그 ([원문](https://www.cncf.io/blog/2026/07/06/evolving-platform-engineering-for-ai-native-workloads/))
- 2026-07-11: [core/01-cluster-architecture.md](https://www.atomai.click/kubernetes-docs/llms/ko/core/01-cluster-architecture.md) — etcd v3.7.0 릴리스(RangeStream 등) 반영
- 2026-07-11: [eks-auto-mode/06-cost-management.md](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/06-cost-management.md) — EKS Auto Mode GPU 관리 요금 최대 60% 인하 반영
- 2026-07-11: [autoscaling/02-karpenter.md](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/02-karpenter.md) — Karpenter v1.14.0 릴리스(CapacityBuffers API 등) 반영
- 2026-07-11: [observability/metrics/04-cloudwatch-metrics.md](https://www.atomai.click/kubernetes-docs/llms/ko/observability/metrics/04-cloudwatch-metrics.md) — CloudWatch Application Signals Service Events 반영
- 2026-07-11: [gitops/argocd/README.md](https://www.atomai.click/kubernetes-docs/llms/ko/gitops/argocd/README.md) — ArgoCD v3.4.5 패치 릴리스 반영
- 2026-07-11: 매칭 문서 없음 — ingress-nginx 컨트롤러 은퇴(2026년 3월) 이후 대응 가이드, CNCF 블로그 ([원문](https://www.cncf.io/blog/2026/07/09/navigating-the-ingress-nginx-retirement/))
- 2026-07-11: 매칭 문서 없음 — CNCF 클라우드 네이티브 AI 데이터 스토리지 백서 공개 ([원문](https://www.cncf.io/report-whitepaper/2026/07/08/the-cncf-data-storage-in-cloud-native-ai-white-paper/))
- 2026-07-11: 매칭 문서 없음 — Amazon EMR on EKS, Apache Spark 트러블슈팅 에이전트 지원 ([원문](https://aws.amazon.com/about-aws/whats-new/2026/07/amazon-emr-eks-spark-troubleshooting/))
- 2026-07-11: 매칭 문서 없음 — AWS Systems Manager 하이브리드/멀티클라우드 노드 요금제 개편(Advanced Instances Tier 폐지) ([원문](https://aws.amazon.com/about-aws/whats-new/2026/06/aws-systems-manager-multicloud-vm/))
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/basics/01-linux-basics
----------------------------------------
# Linux 기초
> **지원 버전**: 검토한 예제 환경: Ubuntu 24.04 LTS, Debian 13, Amazon Linux 2023; 패키지/서비스 이름은 배포판별로 다름 **마지막 업데이트**: 2026년 9월 11일
Kubernetes와 컨테이너 기술을 이해하기 위해서는 Linux에 대한 기본적인 이해가 필수적입니다. 이 문서에서는 Kubernetes 환경에서 특히 중요한 Linux의 핵심 개념들을 다룹니다.
## 실습 환경 설정
이 문서의 예제를 따라하기 위해서는 다음과 같은 환경이 필요합니다:
### 필수 환경
* Linux 운영체제 (Ubuntu 24.04 LTS, Debian 13 또는 Amazon Linux 2023 권장)
* 터미널 액세스
* sudo 권한
### 클라우드 환경 설정 (선택 사항)
격리된 실습 VM을 사용합니다. AWS에서는 리전/아키텍처에 맞는 AL2023을 선택하며 고정 AMI ID는 다른 리전에서 재사용할 수 없습니다. AWS는 AL2 표준 지원이 2026-06-30 종료되었다고 안내합니다. 아래는 AMI 조회만 수행하므로 인스턴스 생성과 제한된 접근 경로를 별도로 준비한 후 기존 인스턴스에 접속합니다.
```bash
# Read-only AMI discovery; select a kernel-specific parameter when reproducibility is required.
aws ssm get-parameter --region us-east-1 \
--name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
--query Parameter.Value --output text
# SSH 접속
ssh -i your-key.pem ec2-user@your-instance-public-ip
```
### 로컬 환경 설정 (선택 사항)
로컬 환경에서 실습하려면 다음 중 하나를 사용할 수 있습니다:
* **VirtualBox + Vagrant**: 가상 머신 환경 구성
* **WSL2**: Windows에서 Linux 환경 사용
* **Docker**: 기본 셸 실습에 적합하지만 일반 컨테이너에는 전체 systemd 호스트나 호스트 네트워크/커널 변경 권한이 없음
## 목차
* [Linux 커널과 사용자 공간](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#linux-커널과-사용자-공간)
* [프로세스 관리](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#프로세스-관리)
* [네임스페이스](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#네임스페이스)
* [cgroups (Control Groups)](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#cgroups-control-groups)
* [파일 시스템](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#파일-시스템)
* [네트워킹 기초](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#네트워킹-기초)
* [보안 컨텍스트](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#보안-컨텍스트)
* [systemd와 서비스 관리](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#systemd와-서비스-관리)
* [커널 파라미터와 모듈](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#커널-파라미터와-모듈)
* [시스템 리소스 제한](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#시스템-리소스-제한)
* [로그 관리](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#로그-관리)
* [DNS와 네트워크 설정](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#dns와-네트워크-설정)
* [시간 동기화](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#시간-동기화)
* [패키지 관리](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#패키지-관리)
* [주요 Linux 명령어](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#주요-linux-명령어)
* [컨테이너 관련 Linux 기능](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md#컨테이너-관련-linux-기능)
## Linux 커널과 사용자 공간
### 커널의 역할
> **핵심 개념**: Linux 커널은 운영체제의 핵심으로, 하드웨어와 소프트웨어 사이의 중개자 역할을 합니다.
Linux 커널은 운영체제의 핵심으로, 하드웨어와 소프트웨어 사이의 중개자 역할을 합니다. 주요 기능은 다음과 같습니다:
* **프로세스 관리**: 프로세스 생성, 스케줄링, 종료
* **메모리 관리**: 가상 메모리, 물리적 메모리 할당
* **장치 관리**: 하드웨어 장치와의 통신
* **시스템 호출 인터페이스**: 사용자 공간 프로그램이 커널 서비스에 접근할 수 있는 방법 제공
### 사용자 공간
사용자 공간은 일반 응용 프로그램이 실행되는 메모리 영역입니다. 사용자 공간 프로그램은 시스템 호출을 통해 커널 서비스에 접근합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-01-linux-basics-0.html)
### 시스템 호출 예시
| 시스템 호출 | 설명 | 관련 명령어 |
| ---------- | ----------- | ------------------- |
| `fork()` | 새 프로세스 생성 | `ps`, `top` |
| `exec()` | 프로그램 실행 | `bash`, `sh` |
| `open()` | 파일 열기 | `cat`, `less` |
| `read()` | 파일에서 데이터 읽기 | `cat`, `grep` |
| `write()` | 파일에 데이터 쓰기 | `echo`, `tee` |
| `socket()` | 네트워크 소켓 생성 | `netstat`, `ss` |
| `clone()` | 태스크 생성 및 플래그에 따른 새 네임스페이스 | `unshare`, `docker` |
### 리눅스 커널 아키텍처

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-01-linux-basics-1.html)
## 프로세스 관리
### 프로세스와 스레드
* **프로세스**: 실행 중인 프로그램의 인스턴스로, 독립된 메모리 공간을 가짐
* **스레드**: 프로세스 내에서 실행되는 작업 단위로, 같은 프로세스의 스레드들은 메모리 공간을 공유
### 프로세스 상태
* **실행(Running)**: CPU에서 실행 중
* **대기(Waiting)**: I/O 완료 또는 이벤트 발생 대기
* **준비(Ready)**: 실행 가능하지만 CPU 할당 대기
* **좀비(Zombie)**: 종료되었지만 부모 프로세스가 상태를 확인하지 않은 상태
* **중단(Stopped)**: 일시 중지된 상태
### 주요 프로세스 관리 명령어
```bash
# 프로세스 목록 확인
ps aux
# 실시간 프로세스 모니터링
top
# 더 향상된 실시간 프로세스 모니터링
htop
# 프로세스 종료
kill
killall <프로세스명>
# 백그라운드 실행
command &
# 작업 관리
jobs
fg %<작업번호>
bg %<작업번호>
```
## 네임스페이스
네임스페이스는 Linux 커널의 기능으로, 프로세스 그룹을 격리하여 각 그룹이 시스템 자원을 독립적으로 볼 수 있게 합니다. 이는 컨테이너 기술의 핵심 요소입니다.
### 주요 네임스페이스 유형
* **PID 네임스페이스**: 프로세스 ID 격리, 컨테이너가 자체 PID 1(init)을 가질 수 있게 함
* **네트워크 네임스페이스**: 네트워크 스택 격리 (인터페이스, IP 주소, 라우팅 테이블, 방화벽 등), 컨테이너 네트워킹의 기반
* **마운트 네임스페이스**: 마운트 테이블을 격리하며 실제 파일 내용/루트 격리는 별도 마운트와 루트 구성 필요
* **UTS 네임스페이스**: 호스트명과 NIS 도메인명 격리(DNS 도메인은 아님), 각 컨테이너에 고유한 호스트 식별자 부여
* **IPC 네임스페이스**: 프로세스 간 통신 자원 격리 (공유 메모리, 세마포어, 메시지 큐 등), 마이크로서비스 아키텍처에서 서비스 간 격리에 중요
* **사용자 네임스페이스**: 사용자 및 그룹 ID 격리, 루트리스(rootless) 컨테이너 실행 지원으로 보안 강화
* **cgroup 네임스페이스**: cgroup 루트 디렉토리 격리, 컨테이너 내부에서 리소스 제한 가시성 제공
* **시간 네임스페이스**: CLOCK_MONOTONIC/CLOCK_BOOTTIME 오프셋 격리 (Linux 5.6+); 실제 날짜/시각 CLOCK_REALTIME은 격리하지 않음
### 네임스페이스 관련 명령어
```bash
# 프로세스의 네임스페이스 확인
ls -la /proc//ns/
# 새로운 네임스페이스에서 명령 실행
sudo unshare --mount --net --pid --fork --mount-proc bash
# 기존 프로세스의 네임스페이스에 진입
sudo nsenter --target --net --pid bash
# 네트워크 네임스페이스 생성 및 관리
ip netns add
ip netns exec
# 루트리스(rootless) 컨테이너 실행을 위한 사용자 네임스페이스 활용
unshare --user --map-root-user --mount --net bash
# 시간 네임스페이스 사용 (Linux 5.6+)
sudo unshare --time --fork bash
```
## cgroups (Control Groups)
cgroups는 프로세스 그룹의 자원 사용을 제한하고 격리하는 Linux 커널 기능입니다. 컨테이너의 자원 제한을 구현하는 데 사용됩니다. 클라우드 네이티브 환경과 Kubernetes에서 리소스 관리의 핵심 기술입니다.
### cgroups의 주요 기능
* **CPU 시간 제한**: 프로세스 그룹이 사용할 수 있는 CPU 시간 제한 및 CPU 코어 할당
* **메모리 제한**: 프로세스 그룹이 사용할 수 있는 메모리 양 제한 및 OOM(Out of Memory) 동작 제어
* **블록 I/O 제한**: 디스크 I/O 대역폭 제한 및 우선순위 설정
* **네트워크 트래픽 제어**: tc/eBPF와 cgroup 분류를 조합하며 cgroup v2 자체에는 독립적인 대역폭 제한 파일이 없음
* **장치 접근 제어**: 특정 장치에 대한 접근 제어 및 권한 관리
* **pids 제어**: 프로세스 생성 수 제한으로 fork 폭탄 방지
* **freezer**: 프로세스 그룹 일시 중지 및 재개 (컨테이너 일시 중지에 활용)
* **cpuset**: 특정 CPU 코어와 NUMA 노드에 프로세스 바인딩
### cgroups v1과 v2
* **cgroups v1**: 각 자원 유형별로 별도의 계층 구조, 레거시 시스템에서 여전히 사용
* **cgroups v2**: 통합된 단일 계층 구조로 더 일관된 관리 제공, 최신 배포판의 기본값
* **하이브리드 모드**: v1과 v2를 함께 사용하여 호환성 유지하면서 새 기능 활용
### cgroups 관련 명령어
```bash
# cgroups 확인
ls -la /sys/fs/cgroup/ # cgroups v2
ls -la /sys/fs/cgroup/cpu /sys/fs/cgroup/memory # cgroups v1
# systemd를 통한 cgroups 관리 (현대적인 방식)
sudo systemctl set-property --runtime <서비스명> CPUQuota=20%
sudo systemctl set-property --runtime <서비스명> MemoryMax=1G
sudo systemctl set-property --runtime <서비스명> IOWeight=500
# 프로세스의 cgroup 확인
cat /proc//cgroup
# Run only the example command inside a transient cgroup managed by systemd.
sudo systemd-run --scope -p CPUQuota=20% -p MemoryHigh=768M -p MemoryMax=1G sleep 60
# memory.max/high take one byte count or "max"; cpu.max takes quota and period.
# Do not move your shell into systemd-owned user.slice or edit its control files.
# 컨테이너 런타임과 cgroups
podman stats # 컨테이너 리소스 사용량 모니터링
docker run --cpus=0.5 --memory=512m nginx # 리소스 제한 설정
```
## 파일 시스템
### 파일 시스템 계층 구조
Linux는 단일 루트 디렉토리(`/`)에서 시작하는 계층적 파일 시스템 구조를 가집니다.
주요 디렉토리:
* `/bin`: 기본 명령어
* `/sbin`: 시스템 관리 명령어
* `/etc`: 시스템 구성 파일
* `/home`: 사용자 홈 디렉토리
* `/var`: 가변 데이터 (로그, 캐시 등)
* `/tmp`: 임시 파일
* `/usr`: 사용자 프로그램 및 데이터
* `/proc`: 프로세스 및 커널 정보 (가상 파일 시스템)
* `/sys`: 시스템 및 하드웨어 정보 (가상 파일 시스템)
### 파일 시스템 유형
* **ext4**: 널리 사용하는 Linux 파일 시스템이며 기본값은 배포판에 따라 다름
* **XFS**: 대용량 파일 시스템에 적합
* **Btrfs**: 스냅샷, 압축 등 고급 기능 제공
* **OverlayFS**: 여러 디렉토리를 겹쳐서 단일 디렉토리로 표현 (컨테이너에서 많이 사용)
* **tmpfs**: 메모리 기반 임시 파일 시스템이며 swap이 비활성화되지 않았다면 페이지가 swap될 수 있음
### 마운트와 볼륨
```bash
# 파일 시스템 마운트
mount -t <파일시스템유형> <소스> <마운트포인트>
# 마운트된 파일 시스템 확인
mount
df -h
# 파일 시스템 언마운트
umount <마운트포인트>
```
## 네트워킹 기초
### 네트워크 인터페이스
* **lo**: 루프백 인터페이스 (127.0.0.1)
* **eth0, ens3 등**: 물리적 네트워크 인터페이스
* **docker0, cni0 등**: 가상 브릿지 인터페이스 (컨테이너 네트워킹)
### 네트워크 구성 명령어
```bash
# 네트워크 인터페이스 확인
ip addr show
ifconfig
# 라우팅 테이블 확인
ip route
route -n
# 네트워크 연결 확인
netstat -tuln
ss -tuln
# 네트워크 패킷 분석
tcpdump -i <인터페이스>
```
### 네트워크 네임스페이스와 가상 인터페이스
```bash
# 네트워크 네임스페이스 생성
ip netns add <네임스페이스명>
# 가상 이더넷 페어 생성
ip link add type veth peer name
# 가상 인터페이스를 네임스페이스에 연결
ip link set netns <네임스페이스명>
```
## 보안 컨텍스트
### 사용자와 그룹
* **UID (User ID)**: 사용자 식별자
* **GID (Group ID)**: 그룹 식별자
* **root (UID 0)**: 관리자 권한을 가진 특별한 사용자
### 파일 권한
Linux 파일 권한은 소유자, 그룹, 기타 사용자에 대한 읽기(r), 쓰기(w), 실행(x) 권한으로 구성됩니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-01-linux-basics-2.html)
### 권한 관련 명령어
```bash
# 파일 권한 변경
chmod 755 <파일명> # rwxr-xr-x
chmod u+x <파일명> # 소유자에게 실행 권한 추가
# 파일 소유자 변경
chown <사용자>:<그룹> <파일명>
# 특수 권한
chmod 4755 <파일명> # setuid 설정
chmod 2755 <파일명> # setgid 설정
chmod 1755 <파일명> # sticky bit 설정
```
### SELinux와 AppArmor
* **SELinux (Security-Enhanced Linux)**: NSA에서 개발한 강제적 접근 제어 시스템
* **AppArmor**: 프로그램별 보안 프로필을 통한 접근 제어 시스템
```bash
# SELinux 상태 확인
getenforce
# 검토된 격리 실습에서만 사용: permissive는 시스템 전체 강제를 해제합니다.
# sudo setenforce 0
# 조사 후 원래 모드를 복구하며 일반적인 해결책으로 강제를 해제하지 않습니다.
# AppArmor 상태 확인
aa-status
# AppArmor 프로필 관리
aa-enforce /etc/apparmor.d/<프로필>
aa-complain /etc/apparmor.d/<프로필>
```
## systemd와 서비스 관리
systemd는 현대적인 Linux 시스템의 init 시스템이자 서비스 관리자입니다. Kubernetes 노드에서 kubelet, containerd 등 핵심 서비스를 관리하는 데 사용됩니다.
### systemd의 주요 기능
* **서비스 관리**: 시스템 서비스의 시작, 중지, 재시작, 활성화/비활성화
* **의존성 관리**: 서비스 간 의존성 자동 관리 및 병렬 시작
* **로깅**: journald를 통한 통합 로그 관리
* **타이머**: cron 대신 사용할 수 있는 타이머 유닛
* **리소스 관리**: cgroups를 통한 서비스별 리소스 제한
### systemd 유닛 타입
* **service**: 시스템 서비스 (예: kubelet.service, containerd.service)
* **socket**: 소켓 기반 활성화
* **target**: 유닛 그룹 (런레벨과 유사)
* **timer**: 예약 작업
* **mount**: 파일 시스템 마운트
* **device**: 장치 유닛
### systemd 명령어
```bash
# 서비스 상태 확인
systemctl status kubelet
systemctl status containerd
# 서비스 제어
systemctl start <서비스>
systemctl stop <서비스>
systemctl restart <서비스>
systemctl reload <서비스> # 설정 다시 읽기
# 부팅 시 자동 시작 설정
systemctl enable <서비스>
systemctl disable <서비스>
# 서비스 로그 확인
journalctl -u kubelet -f # 실시간 로그
journalctl -u kubelet --since "1 hour ago"
journalctl -u kubelet --no-pager
# 모든 서비스 목록
systemctl list-units --type=service
systemctl list-unit-files --type=service
# 실패한 서비스 확인
systemctl --failed
# systemd 설정 다시 읽기
systemctl daemon-reload
```
### systemd 유닛 파일 작성
작은 실습용 서비스로 유닛 구조를 설명합니다. kubelet은 systemctl cat kubelet으로 확인하고 배포판/kubeadm이 관리하는 유닛과 drop-in을 덮어쓰지 않습니다.
```ini
# /etc/systemd/system/linux-basics-demo.service
[Unit]
Description=Linux basics training service
Documentation=man:systemd.service(5)
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/usr/bin/sleep infinity
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
```
### systemd 리소스 제한
위 linux-basics-demo.service를 실습 VM에 저장하고 daemon-reload한 경우에만 아래 속성을 설정합니다. 운영 kubelet/containerd에 이 학습용 제한을 적용하지 않습니다.
```bash
# CPU 제한 (20%)
sudo systemctl set-property --runtime linux-basics-demo.service CPUQuota=20%
# 메모리 제한 (1GB)
sudo systemctl set-property --runtime linux-basics-demo.service MemoryMax=1G
# I/O 가중치 설정 (1-10000, 기본 100)
sudo systemctl set-property --runtime linux-basics-demo.service IOWeight=500
# 설정 확인
systemctl show linux-basics-demo.service | grep -E 'CPUQuota|MemoryMax|IOWeight'
```
## 커널 파라미터와 모듈
### sysctl을 통한 커널 파라미터 설정
sysctl은 실행 중인 커널의 파라미터를 조회하고 변경하는 도구입니다. Kubernetes 클러스터 구성 시 네트워크 및 시스템 파라미터 튜닝에 필수적입니다.
#### CNI별 sysctl 설정과 튜닝 예시
아래 값은 모든 Kubernetes 노드의 필수 기본값이 아닙니다. 선택한 IP 계열/CNI/서비스 프록시 요구를 확인합니다. bridge-nf 설정은 br_netfilter를 사용하는 구성에서만 필요합니다. 성능/ARP/conntrack 값은 측정 없이 운영 노드에 적용하지 않습니다. 먼저 현재 값을 기록하고 격리된 실습 VM에서만 변경합니다.
```bash
# IP 포워딩 활성화 (컨테이너 네트워킹에 필수)
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
# 브릿지 트래픽이 iptables를 통과하도록 설정 (bridge netfilter가 필요한 CNI 구성에 한함)
sudo sysctl -w net.bridge.bridge-nf-call-iptables=1
sudo sysctl -w net.bridge.bridge-nf-call-ip6tables=1
# 최대 파일 디스크립터 수 증가
sudo sysctl -w fs.file-max=2097152
# 네트워크 성능 튜닝
sudo sysctl -w net.core.somaxconn=32768
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=8192
sudo sysctl -w net.core.netdev_max_backlog=16384
# ARP 캐시 설정 (대규모 클러스터)
sudo sysctl -w net.ipv4.neigh.default.gc_thresh1=80000
sudo sysctl -w net.ipv4.neigh.default.gc_thresh2=90000
sudo sysctl -w net.ipv4.neigh.default.gc_thresh3=100000
# 현재 설정 확인
sysctl net.ipv4.ip_forward
sysctl -a | grep bridge-nf-call
# 영구 설정 (/etc/sysctl.conf 또는 /etc/sysctl.d/*.conf)
cat <
```
### 커널 버전 및 기능 확인
```bash
# 커널 버전 확인
uname -r
# 커널 컴파일 옵션 확인
cat /boot/config-$(uname -r) | grep OVERLAY
cat /boot/config-$(uname -r) | grep NETFILTER
# 사용 가능한 커널 기능 확인
cat /proc/filesystems # 지원되는 파일 시스템
cat /proc/sys/net/ipv4/ip_forward # IP 포워딩 상태
```
## 시스템 리소스 제한
### ulimit - 사용자별 리소스 제한
ulimit은 프로세스가 사용할 수 있는 시스템 리소스를 제한합니다. Kubernetes 노드에서 충분한 리소스를 확보하기 위해 조정이 필요할 수 있습니다.
```bash
# 현재 제한 확인
ulimit -a
# 주요 제한 항목
ulimit -n # 열 수 있는 파일 디스크립터 수
ulimit -u # 최대 프로세스 수
ulimit -m # RSS 제한; 최신 Linux에서는 강제되지 않음
ulimit -v # 가상 메모리 크기
# 제한 변경 (현재 세션)
ulimit -n 65536 # 파일 디스크립터를 65536으로 증가
# 영구 설정 (/etc/security/limits.conf)
sudo tee -a /etc/security/limits.conf </limits
# 특정 프로세스의 파일 디스크립터 확인
find /proc//fd -mindepth 1 -maxdepth 1 -printf '%f\n' | wc -l
```
## 로그 관리
### journald - systemd 통합 로깅
journald는 systemd의 로깅 시스템으로, Kubernetes 노드의 시스템 서비스 로그를 관리합니다.
```bash
# 전체 시스템 로그
journalctl
# 특정 서비스 로그
journalctl -u kubelet
journalctl -u containerd
journalctl -u docker
# 실시간 로그 (tail -f와 유사)
journalctl -u kubelet -f
# 시간 범위 지정
journalctl --since "2025-11-24 10:00:00"
journalctl --since "1 hour ago"
journalctl --since yesterday
journalctl --until "2025-11-24 12:00:00"
# 우선순위별 필터링
journalctl -p err # 에러 및 더 높은 심각도 (0-3)
journalctl -p warning # 경고 이상
journalctl -p debug # 디버그 포함 모두
# 출력 형식 변경
journalctl -u kubelet -o json # JSON 형식
journalctl -u kubelet -o json-pretty # Pretty JSON
journalctl -u kubelet -o cat # 메시지만
# 부팅 로그
journalctl -b # 현재 부팅 로그
journalctl -b -1 # 이전 부팅 로그
journalctl --list-boots # 부팅 목록
# 디스크 사용량 확인
journalctl --disk-usage
# 로그 정리
journalctl --vacuum-time=7d # 7일 이전의 보관된 journal 파일 삭제
journalctl --vacuum-size=1G # 보관된 journal 총량을 줄이도록 오래된 파일 삭제
```
### journald 설정
```bash
# journald 설정 파일
sudo vi /etc/systemd/journald.conf
# 주요 설정 옵션
# Storage=persistent # 디스크에 영구 저장
# SystemMaxUse=1G # 최대 디스크 사용량
# SystemKeepFree=500M # 최소 여유 공간
# MaxRetentionSec=1month # 최대 보관 기간
# 설정 적용
sudo systemctl restart systemd-journald
```
### 전통적인 syslog
일부 시스템에서는 여전히 syslog를 사용합니다.
```bash
# syslog 파일 위치
# /var/log/syslog # Debian/Ubuntu
# /var/log/messages # RHEL/CentOS
# 실시간 로그 확인
tail -f /var/log/syslog
# 로그 검색
grep "kubelet" /var/log/syslog
grep -i "error" /var/log/syslog
```
### 로그 로테이션
일반 애플리케이션 파일에 logrotate를 사용합니다. copytruncate는 복사/잘라내기 사이에 기록이 유실될 수 있으므로 애플리케이션이 지원하면 파일 재열기 방식을 선호합니다. CRI 컨테이너 로그 회전은 kubelet이 관리합니다.
```bash
# logrotate 설정
sudo vi /etc/logrotate.d/linux-basics-demo
# 파일 내용 (kubelet이 관리하지 않는 애플리케이션 텍스트 로그 전용):
```
```text
/var/log/linux-basics-demo/*.log {
daily
rotate 7
missingok
notifempty
compress
delaycompress
copytruncate
}
```
```bash
# 수동으로 로테이션 실행
sudo logrotate -f /etc/logrotate.d/linux-basics-demo
```
## DNS와 네트워크 설정
### DNS 설정
호스트의 resolv.conf는 NetworkManager/systemd-resolved 등이 관리할 수 있으므로 먼저 실제 설정을 확인합니다. 8.8.8.8 같은 공용 DNS는 cluster.local 서비스를 확인하지 못합니다. ClusterFirst Pod는 kubelet이 구성한 클러스터 DNS를 사용하며 호스트에 클러스터 검색 접미사를 추가하는 것만으로 연결되지 않습니다.
```bash
# On the Linux host
cat /etc/resolv.conf
cat /etc/hosts
# If a cluster is available, inspect its actual DNS Service address.
kubectl -n kube-system get service kube-dns
# Run inside an existing Pod with DNS utilities and ClusterFirst policy:
# nslookup kubernetes.default.svc.cluster.local
```
아래는 Pod resolver 파일의 **형식 예제**이며 IP/네임스페이스/클러스터 도메인을 실제 값으로 바꿔야 합니다. 호스트 파일에 복사하지 않습니다.
```text
nameserver
search .svc.cluster.local svc.cluster.local cluster.local
options ndots:5
```
### systemd-resolved
현대적인 Linux 배포판에서는 systemd-resolved를 사용합니다.
```bash
# systemd-resolved 상태 확인
systemctl status systemd-resolved
# DNS 서버 확인
resolvectl status
# DNS 캐시 통계
resolvectl statistics
# DNS 캐시 초기화
resolvectl flush-caches
```
### 네트워크 설정 파일
NetworkManager와 netplan 중 배포판이 사용하는 도구를 확인합니다. netplan의 YAML은 셸 명령이 아니며 /etc/netplan 아래 파일에 저장합니다. 원격 연결 중 주소/라우팅을 변경하기 전에 복구 경로를 준비하고 netplan try로 확인합니다.
```bash
nmcli connection show
nmcli device status
# On a netplan-based installation:
ls /etc/netplan
```
```yaml
# Example netplan file: replace eth0 with the actual interface name.
network:
version: 2
ethernets:
eth0:
dhcp4: true
```
```bash
sudo netplan generate
sudo netplan try
```
## 시간 동기화
시간 동기화는 분산 시스템에서 매우 중요합니다. Kubernetes 클러스터의 모든 노드는 정확한 시간을 유지해야 합니다.
### chronyd (권장)
chronyd는 네트워크 조건 변화에 대응하는 NTP 클라이언트/서버입니다. 동기화 성능은 클록, 시간 소스 및 설정에 따라 달라집니다.
```bash
# chronyd 설치 (RHEL/CentOS)
sudo yum install chrony
# chronyd 설치 (Ubuntu/Debian)
sudo apt install chrony
# 설치된 유닛 확인: RHEL/Amazon Linux는 chronyd, Debian/Ubuntu는 chrony.
systemctl status chronyd
# 시간 동기화 상태 확인
chronyc tracking
# NTP 서버 목록
chronyc sources
# 상세 정보
chronyc sourcestats
# 수동 시간 동기화
# Only during a reviewed maintenance window; stepping can disrupt time-sensitive workloads.
# sudo chronyc makestep
```
### chronyd 설정
RHEL 계열은 보통 /etc/chrony.conf, Debian/Ubuntu는 /etc/chrony/chrony.conf를 사용합니다. 배포판의 제공자 설정(EC2의 Amazon Time Sync Service 포함)을 먼저 확인하고 임의의 공용 서버로 덮어쓰지 않습니다. 다음은 파일 내용 예제입니다.
```text
# Choose an approved reachable time source.
server iburst
# Permit stepping only during the first three clock updates.
makestep 1.0 3
```
```bash
# Choose the unit actually installed on your distribution:
systemctl status chronyd.service # RHEL/Amazon Linux
systemctl status chrony.service # Debian/Ubuntu
chronyc tracking
chronyc sources
```
### timesyncd (배포판별 선택)
Ubuntu 25.10부터 기본 시간 동기화는 chrony이며 이전 릴리스/이미지에는 systemd-timesyncd가 사용될 수 있습니다. 활성 시간 서비스 하나를 사용합니다. show-timesync는 timesyncd 전용이며 chrony 상태는 chronyc로 확인합니다.
```bash
timedatectl status
# Only for installations using systemd-timesyncd:
timedatectl show-timesync --all
systemctl status systemd-timesyncd
```
```ini
# /etc/systemd/timesyncd.conf: use approved servers for this environment.
[Time]
NTP=
```
```bash
# After editing a timesyncd installation:
sudo systemctl restart systemd-timesyncd
```
### 시간대 설정
```bash
# 현재 시간 및 시간대 확인
timedatectl
# 시간대 목록
timedatectl list-timezones
# 시간대 변경
sudo timedatectl set-timezone Asia/Seoul
# 시간 수동 설정 (NTP 비활성화 시)
: "${LAB_TIME:?Set an intentional time for an isolated VM with NTP disabled}"
# sudo timedatectl set-time "$LAB_TIME"
# NTP 활성화/비활성화
sudo timedatectl set-ntp true
```
## 패키지 관리
Kubernetes와 관련 도구를 설치하고 관리하기 위한 패키지 관리자 사용법입니다.
### apt (Debian/Ubuntu)
```bash
# 패키지 목록 업데이트
sudo apt update
# 패키지 업그레이드
sudo apt upgrade
# 패키지 설치
sudo apt install <패키지명>
# 패키지 제거
sudo apt remove <패키지명>
sudo apt purge <패키지명> # 설정 파일도 함께 제거
# 패키지 검색
apt search <키워드>
# 패키지 정보 확인
apt show <패키지명>
# 설치된 패키지 목록
apt list --installed
# 저장소 추가 (Kubernetes 예시)
set -o pipefail
: "${KUBERNETES_MINOR:?Choose a supported cluster-compatible minor, for example v1.37}"
sudo apt install -y ca-certificates curl gnupg
sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL "https://pkgs.k8s.io/core:/stable:/${KUBERNETES_MINOR}/deb/Release.key" | \
sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/${KUBERNETES_MINOR}/deb/ /" | \
sudo tee /etc/apt/sources.list.d/kubernetes.list
# 불필요한 패키지 정리
sudo apt autoremove
sudo apt autoclean
```
### yum/dnf (RHEL/CentOS/Fedora)
```bash
# 패키지 설치
sudo yum install <패키지명>
sudo dnf install <패키지명> # Fedora/RHEL 8+
# 패키지 업데이트
sudo yum update
sudo dnf update
# 패키지 제거
sudo yum remove <패키지명>
sudo dnf remove <패키지명>
# 패키지 검색
yum search <키워드>
dnf search <키워드>
# 패키지 정보
yum info <패키지명>
dnf info <패키지명>
# 설치된 패키지 목록
yum list installed
dnf list installed
# 저장소 추가 (Kubernetes 예시)
: "${KUBERNETES_MINOR:?Choose a supported cluster-compatible minor, for example v1.37}"
cat < # 디렉토리 변경
pwd # 현재 디렉토리 확인
mkdir -p <경로> # 디렉토리 생성 (필요시 상위 디렉토리도 생성)
rm -rf <경로> # 파일/디렉토리 삭제
cp -r <소스> <대상> # 파일/디렉토리 복사
mv <소스> <대상> # 파일/디렉토리 이동 또는 이름 변경
find <경로> -name "<패턴>" # 파일 검색
```
### 텍스트 처리
```bash
cat <파일> # 파일 내용 출력
less <파일> # 파일 내용 페이지별 확인
grep "<패턴>" <파일> # 파일에서 패턴 검색
sed 's/<패턴>/<대체>/' <파일> # 텍스트 치환
awk '{print $1}' <파일> # 텍스트 처리
```
### 시스템 정보
```bash
uname -a # 커널 정보
lsb_release -a # 배포판 정보
free -h # 메모리 사용량
df -h # 디스크 사용량
du -sh <경로> # 디렉토리 크기
```
### 프로세스 및 서비스 관리
```bash
systemctl status <서비스> # 서비스 상태 확인
systemctl restart <서비스> # start/stop도 각각 별도 하위 명령으로 사용
journalctl -u <서비스> # 서비스 로그 확인
```
## 컨테이너 관련 Linux 기능
### OverlayFS
OverlayFS는 여러 디렉토리를 겹쳐서 단일 디렉토리로 표현하는 유니온 마운트 파일 시스템입니다. Docker와 같은 컨테이너 런타임에서 이미지 레이어를 구현하는 데 사용됩니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-01-linux-basics-11.html)
### 네트워크 브릿지와 NAT
Docker 기본 bridge 네트워크는 외부 통신에 브릿지/NAT를 사용합니다. Kubernetes CNI는 라우팅, 오버레이 또는 VPC 네이티브 네트워크를 사용할 수 있으며 모든 Pod 간 트래픽에 NAT가 적용되지는 않습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-01-linux-basics-10.html)
### 시스템 호출 필터링 (seccomp)
seccomp(Secure Computing Mode)는 프로세스가 사용할 수 있는 시스템 호출을 제한하는 Linux 커널 기능입니다. 컨테이너의 보안을 강화하는 데 사용됩니다.
### 기능(Capabilities) 제한
Linux 기능은 전통적인 root 권한을 더 작은 권한 단위로 나눈 것입니다. 컨테이너는 필요한 기능만 부여받아 보안을 강화합니다.
주요 기능:
* `CAP_NET_ADMIN`: 네트워크 설정 변경
* `CAP_SYS_ADMIN`: 시스템 관리 작업
* `CAP_CHOWN`: 파일 소유권 변경
* `CAP_DAC_OVERRIDE`: 파일 권한 무시
## 결론
Linux의 기본 개념과 기능은 Kubernetes와 컨테이너 기술을 이해하는 데 필수적입니다. 이 문서에서 다룬 주요 내용을 정리하면:
### 핵심 기술
* **네임스페이스와 cgroups**: 컨테이너 격리와 자원 관리의 기반
* **OverlayFS**: 컨테이너 이미지 레이어링의 핵심
* **systemd**: Kubernetes 노드 서비스 관리
### 운영 필수 지식
* **커널 파라미터 튜닝**: sysctl을 통한 네트워킹 및 시스템 최적화
* **모듈 관리**: CNI 플러그인과 스토리지 드라이버 지원
* **로그 관리**: journald를 통한 시스템 및 서비스 로그 분석
* **시간 동기화**: 분산 시스템의 일관성 유지
### 문제 해결
* **리소스 제한**: ulimit과 cgroups를 통한 리소스 관리
* **네트워킹**: DNS, 브릿지, iptables 설정
* **패키지 관리**: Kubernetes 구성 요소의 버전 관리
이러한 Linux 기초 지식을 바탕으로 Kubernetes 환경에서 발생하는 문제를 효과적으로 해결하고, 클러스터를 최적화하며, 안정적으로 운영할 수 있습니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [Linux 기초 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/basics/01-linux-basics-quiz)를 풀어보세요.
## 참고 자료
* [The Linux Documentation Project](https://tldp.org/)
* [Linux Kernel Documentation](https://www.kernel.org/doc/)
* [Linux Namespaces](https://man7.org/linux/man-pages/man7/namespaces.7.html)
* [Control Groups v2](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html)
## 검증 참고 자료
- https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html
- https://kubernetes.io/docs/concepts/architecture/cgroups/
- https://man7.org/linux/man-pages/man7/time_namespaces.7.html
- https://man7.org/linux/man-pages/man2/getrlimit.2.html
- https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html
- https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html
- https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html
- https://www.freedesktop.org/software/systemd/man/latest/journalctl.html
- https://kubernetes.io/docs/concepts/cluster-administration/logging/
- https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/
- https://ubuntu.com/about/release-cycle
- https://www.debian.org/releases/
- https://www.centos.org/centos-linux-eol/
- https://documentation.ubuntu.com/server/how-to/networking/timedatectl-and-timesyncd/
- https://aws.amazon.com/amazon-linux-2/faqs/
- https://docs.aws.amazon.com/linux/al2023/ug/ec2.html
- https://github.com/logrotate/logrotate/blob/main/logrotate.8.in
- https://github.com/linux-pam/linux-pam/blob/master/modules/pam_limits/limits.conf.5.xml
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/basics/02-linux-advanced
----------------------------------------
# Linux 운영 기술
> **지원 버전**: 지원 중인 Linux 배포판의 Bash; 사용 도구는 별도 설치 필요 **마지막 업데이트**: 2026년 9월 11일
이 문서는 Kubernetes 환경에서 효과적으로 작업하기 위한 필수 Linux 운영 기술을 다룹니다.
***
## 목차
1. [환경 변수와 쉘 설정](https://www.atomai.click/kubernetes-docs/llms/ko/basics/02-linux-advanced.md#1-환경-변수와-쉘-설정)
2. [쉘 스크립팅 기초](https://www.atomai.click/kubernetes-docs/llms/ko/basics/02-linux-advanced.md#2-쉘-스크립팅-기초)
3. [텍스트 처리 도구](https://www.atomai.click/kubernetes-docs/llms/ko/basics/02-linux-advanced.md#3-텍스트-처리-도구)
4. [SSH와 원격 접속](https://www.atomai.click/kubernetes-docs/llms/ko/basics/02-linux-advanced.md#4-ssh와-원격-접속)
5. [성능 모니터링 및 트러블슈팅](https://www.atomai.click/kubernetes-docs/llms/ko/basics/02-linux-advanced.md#5-성능-모니터링-및-트러블슈팅)
6. [스토리지 관리 기초](https://www.atomai.click/kubernetes-docs/llms/ko/basics/02-linux-advanced.md#6-스토리지-관리-기초)
7. [curl과 API 호출](https://www.atomai.click/kubernetes-docs/llms/ko/basics/02-linux-advanced.md#7-curl과-api-호출)
8. [실용적인 원라이너 모음](https://www.atomai.click/kubernetes-docs/llms/ko/basics/02-linux-advanced.md#8-실용적인-원라이너-모음)
***
## 1. 환경 변수와 쉘 설정
환경 변수는 Linux 시스템과 Kubernetes에서 설정을 관리하는 핵심 메커니즘입니다.
### 1.1 환경 변수 기초
```bash
env
echo "$HOME"
echo "$PATH"
printenv HOME
```
### 1.2 export 명령어
```bash
export MY_VAR="hello"
export DATABASE_URL="postgresql://localhost:5432/mydb"
export KUBECONFIG="/home/user/.kube/config"
```
### 1.3 source 명령어
```bash
cat > ~/my-env.sh << 'SCRIPT'
export APP_ENV="production"
export APP_PORT="8080"
alias k='kubectl'
SCRIPT
source ~/my-env.sh
```
### 1.4 .bashrc와 .bash\_profile
Bash의 대화형 비로그인 셸은 .bashrc를 읽으며 로그인 셸은 .bash_profile/.bash_login/.profile 중 첫 파일을 읽습니다. 로그인 설정에서 .bashrc를 명시적으로 읽을 수도 있습니다. 아래 블록은 한 번만 추가합니다.
```bash
cat >> ~/.bashrc << 'SCRIPT'
export KUBECONFIG=~/.kube/config
command -v kubectl >/dev/null && source <(kubectl completion bash)
alias k='kubectl'
SCRIPT
source ~/.bashrc
```
### 1.5 Kubernetes ConfigMap 연동
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
DATABASE_HOST: "mysql.default.svc.cluster.local"
DATABASE_PORT: "3306"
---
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: app
image: myapp:1.0
envFrom:
- configMapRef:
name: app-config
```
***
## 2. 쉘 스크립팅 기초
### 2.1 변수
```bash
#!/bin/bash
NAME="kubernetes"
NAMESPACE=${1:-default}
: "${REQUIRED_VAR:?REQUIRED_VAR must be set}"
```
### 2.2 조건문
```bash
if [ "$ENV" = "production" ]; then
echo "Production mode"
fi
case "$1" in
start) echo "Starting..." ;;
stop) echo "Stopping..." ;;
esac
```
### 2.3 반복문
```bash
for ns in default kube-system monitoring; do
kubectl get pods -n "$ns"
done
# Running phase does not imply readiness. A bounded wait propagates API errors/timeouts.
kubectl wait --for=condition=Ready pod/mypod --timeout=120s
```
### 2.4 함수
```bash
check_pod_exists() {
local pod_name=${1:?Pod name required}
local namespace=${2:-default}
# Nonzero also includes authorization/connection errors; inspect stderr.
kubectl get pod "$pod_name" -n "$namespace" -o name >/dev/null
}
```
### 2.5 Init Container 패턴
아래는 DNS 검색 대기의 유한 반복 예제입니다. DNS 성공은 데이터베이스 준비 완료가 아니므로 실제 애플리케이션은 연결 재시도/DB별 readiness 검사를 구현해야 합니다. Service mysql과 커스텀 myapp 이미지는 사전에 준비합니다. 실패 시 kubelet이 init 컨테이너를 다시 시작할 수 있으므로 전체 Pod 시작 기한과도 구분합니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-init
spec:
initContainers:
- name: wait-for-db-dns
image: busybox:1.37.0
command:
- sh
- -c
- |
for attempt in $(seq 1 60); do
nslookup mysql.default.svc.cluster.local >/dev/null 2>&1 && exit 0
sleep 2
done
echo "Database Service DNS did not become available" >&2
exit 1
containers:
- name: app
image: myapp:1.0
```
***
## 3. 텍스트 처리 도구
### 3.1 grep과 kubectl
```bash
kubectl get pods --field-selector=status.phase!=Running
kubectl logs nginx-pod | grep -i error
```
### 3.2 awk 필드 추출
```bash
kubectl get pods | awk 'NR>1 {print $1}'
kubectl get pods --no-headers | awk '$3 != "Running" {print $1, $3}'
```
### 3.3 sed 편집
```bash
# Text-only preview; this can match more than the intended YAML field.
sed 's/replicas: [0-9]*/replicas: 5/' deployment.yaml
# For a structured edit, use the yq example below.
```
### 3.4 jq로 JSON 파싱
```bash
kubectl get pod nginx -o json | jq '.metadata.name'
kubectl get pods -o json | jq -r '.items[].metadata.name'
```
### 3.5 yq로 YAML 파싱
이 예제는 Mike Farah yq v4 문법이며 Python yq와 옵션이 다릅니다.
```bash
yq '.metadata.name' deployment.yaml
yq -i '.spec.replicas = 5' deployment.yaml
```
***
## 4. SSH와 원격 접속
### 4.1 SSH 키 생성
```bash
ssh-keygen -t ed25519 -C "your_email@example.com"
```
### 4.2 SSH 터널링
```bash
ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:8080:localhost:80 user@server
ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:6443:kubernetes-api:6443 user@bastion
```
API 터널에서는 kubeconfig의 원래 CA와 TLS 서버 이름을 유지합니다. 127.0.0.1로 접속할 때 tls-server-name을 실제 API 인증서 이름으로 설정하며 TLS 검증을 끄지 않습니다.
### 4.3 Bastion 호스트 사용
```bash
ssh -J bastion user@internal-server
```
### 4.4 rsync
```bash
rsync -avzP ./local/ user@remote:/path/
```
***
## 5. 성능 모니터링 및 트러블슈팅
### 5.1 top과 htop
```bash
top -b -n 1 | head -20
```
### 5.2 vmstat와 iostat
```bash
vmstat 1 5
iostat -dx 1 5
```
### 5.3 free와 df
```bash
free -h
df -h
```
### 5.4 kubectl top
Metrics Server 등 metrics.k8s.io 제공자가 필요하며 kubectl top은 장기 모니터링 이력을 제공하지 않습니다.
```bash
kubectl top nodes
kubectl top pods --sort-by=memory
```
***
## 6. 스토리지 관리 기초
### 6.1 lsblk
```bash
lsblk -f
```
### 6.2 LVM
```bash
# Use only an explicitly selected empty training disk. These commands write storage metadata.
: "${LAB_DISK:?Set an unused lab block device after checking lsblk and backups}"
lsblk -f "$LAB_DISK"
sudo wipefs --no-act "$LAB_DISK"
# Stop if the disk contains data, mounted filesystems, or existing volume metadata.
sudo pvcreate "$LAB_DISK"
sudo vgcreate data_vg "$LAB_DISK"
sudo lvcreate -l 100%FREE -n data_lv data_vg
```
### 6.3 Kubernetes PV/PVC
아래 경로와 호스트명은 실제 파일 시스템이 마운트된 노드와 일치해야 합니다.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv
spec:
capacity:
storage: 100Gi
accessModes: [ReadWriteOnce]
storageClassName: local-storage
persistentVolumeReclaimPolicy: Retain
local:
path: /mnt/disks/vol1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [replace-with-actual-node-hostname]
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: local-pvc
spec:
storageClassName: local-storage
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi
```
***
로컬 볼륨은 노드 장애 시 다른 노드로 이동하지 않습니다. 위 StorageClass는 동적 디스크 생성을 하지 않으며 PVC는 소비 Pod가 스케줄될 때까지 Pending일 수 있습니다. PV 용량은 실제 파일 시스템 용량과 맞춰야 하며 값 자체가 디렉토리 사용량 제한을 만들지는 않습니다. Retain 볼륨 재사용/데이터 삭제는 별도 관리 작업입니다.
## 7. curl과 API 호출
### 7.1 HTTP 메서드
```bash
curl -X POST -H "Content-Type: application/json" -d '{"name":"John"}' https://api.example.com/users
```
### 7.2 Kubernetes API 호출
```bash
# Run inside a Pod with projected ServiceAccount credentials and curl installed.
set -e
SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
CACERT="$SERVICEACCOUNT/ca.crt"
NAMESPACE=$(cat "$SERVICEACCOUNT/namespace")
TOKEN=$(cat "$SERVICEACCOUNT/token")
curl --fail --silent --show-error --cacert "$CACERT" \
-H "Authorization: Bearer $TOKEN" \
"https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/pods"
unset TOKEN
```
ServiceAccount에는 해당 네임스페이스의 pods list RBAC 권한이 필요합니다. automountServiceAccountToken: false이면 이 경로가 없으며 projected 토큰은 갱신되므로 장기 실행 클라이언트는 파일을 다시 읽어야 합니다. 토큰을 로그에 출력하지 않습니다.
### 7.3 유용한 curl 옵션
```bash
curl --silent --show-error -o /dev/null -w "%{http_code}\n" https://api.example.com/health
```
***
## 8. 실용적인 원라이너 모음
### 8.1 Kubernetes 운영
```bash
kubectl get pods -A | awk '$4 != "Running" && NR>1 {print $1, $2, $4}'
kubectl get pods -A -o json | jq -r '.items[] | select(any((.status.containerStatuses // [])[]; .restartCount > 5)) | [.metadata.namespace, .metadata.name] | @tsv'
```
### 8.2 로그 분석
```bash
kubectl logs deploy/app --since=1h | grep -i error
```
### 8.3 네트워크 디버깅
```bash
nslookup kubernetes.default.svc.cluster.local
nc -zv service-name 80
```
***
## 결론
1. **환경 변수**: K8s ConfigMap/Secret의 기반
2. **쉘 스크립팅**: init container, health check에 필수
3. **텍스트 처리**: kubectl 출력 파싱의 핵심
4. **SSH**: 노드 디버깅에 중요
5. **성능 모니터링**: 트러블슈팅의 기초
***
[이전: Linux 기초](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md) | [다음: 컨테이너 기초](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md)
## 검증 참고 자료
- https://kubernetes.io/docs/concepts/storage/volumes/#local
- https://kubernetes.io/docs/concepts/storage/storage-classes/#local
- https://kubernetes.io/docs/tasks/run-application/access-api-from-pod/
- https://kubernetes.io/docs/concepts/configuration/configmap/
- https://kubernetes.io/docs/reference/kubectl/generated/kubectl_wait/
- https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
- https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html
- https://download.samba.org/pub/rsync/rsync.1
- https://github.com/mikefarah/yq
- https://busybox.net/downloads/BusyBox.html
- https://github.com/docker-library/official-images/blob/master/library/busybox
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/basics/03-container-technology
----------------------------------------
# 컨테이너 기술
> **지원 버전**: 유지 관리 중인 Linux Docker/CRI 릴리스; Kubernetes CRI v1; Node.js 24 빌드 예제 **마지막 업데이트**: 2026년 9월 11일
컨테이너는 애플리케이션과 그 종속성을 함께 패키징하여 다양한 환경에서 일관되게 실행할 수 있게 해주는 기술입니다. 이 문서에서는 컨테이너의 기본 개념, 작동 원리, 그리고 Kubernetes와의 관계에 대해 설명합니다.
## 목차
* [컨테이너란?](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md#컨테이너란)
* [컨테이너 vs 가상 머신](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md#컨테이너-vs-가상-머신)
* [컨테이너의 기술적 기반](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md#컨테이너의-기술적-기반)
* [컨테이너 런타임](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md#컨테이너-런타임)
* [컨테이너 이미지](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md#컨테이너-이미지)
* [Dockerfile](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md#dockerfile)
* [컨테이너 네트워킹](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md#컨테이너-네트워킹)
* [컨테이너 스토리지](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md#컨테이너-스토리지)
* [컨테이너 보안](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md#컨테이너-보안)
* [컨테이너 라이프사이클 관리](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md#컨테이너-라이프사이클-관리)
* [컨테이너 오케스트레이션](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md#컨테이너-오케스트레이션)
* [AWS에서의 컨테이너](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md#aws에서의-컨테이너)
> 아래 Linux 명령은 Linux Docker 호스트를 기준으로 합니다. Docker Desktop에서는 데몬이 VM 안에서 실행되므로 호스트 PID/파일 경로를 데스크톱 OS에서 그대로 조회할 수 없습니다. 이미지 실행은 CPU 아키텍처/OS/커널 호환성이 필요합니다. Kata/Fargate 같은 VM 기반 실행 환경은 여기서 설명하는 기본 프로세스 격리 모델과 구분합니다.
## 컨테이너란?
컨테이너는 애플리케이션과 그 실행에 필요한 모든 것(코드, 런타임, 시스템 도구, 시스템 라이브러리, 설정)을 포함하는 표준화된 소프트웨어 유닛입니다. 컨테이너는 호스트 운영체제의 커널을 공유하면서도 서로 격리된 환경에서 실행됩니다.
### 컨테이너의 주요 특징
1. **이식성**: 개발, 테스트, 프로덕션 환경 간에 일관된 실행 환경 제공
2. **경량성**: 가상 머신보다 적은 리소스 사용
3. **격리**: 다른 컨테이너 및 호스트 시스템과 격리된 실행 환경
4. **신속한 시작 및 종료**: 빠른 시작이 가능하지만 이미지 다운로드와 애플리케이션 초기화에 따라 준비 시간이 달라짐
5. **확장성**: 쉽게 복제하여 수평적 확장 가능
6. **버전 관리**: 이미지 버전 관리를 통한 애플리케이션 라이프사이클 관리
### 컨테이너 기술의 역사
* **2000년대 초**: Linux VServer, OpenVZ 등의 초기 컨테이너 기술 등장
* **2008년**: Linux 2.6.24 릴리스에 초기 cgroups 구현 포함
* **2008년**: LXC(Linux Containers) 프로젝트 시작
* **2013년**: Docker 출시, 컨테이너 기술 대중화
* **2015년**: Open Container Initiative(OCI) 설립, 컨테이너 표준화
* **2017년**: containerd가 CNCF 프로젝트로 기부됨
## 컨테이너 vs 가상 머신
### 가상 머신 아키텍처 vs 컨테이너 아키텍쳐

### 주요 차이점
아래는 구조 비교이며 측정한 성능/시작 시간 벤치마크가 아닙니다. 이미지 크기, 초기화 및 VM 복원 방식에 따라 결과가 달라집니다.
| 특성 | 컨테이너 | 가상 머신 |
| ------- | ---------------------- | ---------------------------- |
| 크기 | 애플리케이션/사용자 공간 레이어; 크기는 가변 | 게스트 OS와 애플리케이션; 크기는 가변 |
| 시작 시간 | 이미지가 로컬에 있으면 빠른 시작 가능 | 부팅/복원 방식에 따라 다름 |
| 격리 수준 | 프로세스 수준 격리 | 하드웨어 수준 격리 |
| OS | 호스트 OS 커널 공유 | 각 VM마다 전체 OS 필요 |
| 성능 | 거의 네이티브 | 약간의 오버헤드 |
| 보안 | 공유 커널이므로 보안 강화 필요 | 하이퍼바이저 경계가 추가되지만 보안 강화 필요 |
| 리소스 효율성 | 높음 | 중간 |
| 사용 사례 | 마이크로서비스, CI/CD, 개발/테스트 | 레거시 앱, 다양한 OS 요구사항, 높은 보안 요구 |
## 컨테이너의 기술적 기반
컨테이너는 Linux 커널의 여러 기능을 활용하여 구현됩니다. 이러한 기술들은 01-linux-basics.md에서 상세히 다루었으며, 여기서는 컨테이너와의 관계를 중심으로 설명합니다.
### 네임스페이스를 통한 격리
컨테이너는 Linux 네임스페이스를 사용하여 프로세스를 격리합니다. 네임스페이스 공유는 구성에 따라 다르며 같은 Kubernetes Pod의 컨테이너는 네트워크 네임스페이스를 공유합니다.
```bash
# 컨테이너의 네임스페이스 확인
docker inspect | grep -A 10 "Pid"
ls -la /proc//ns/
# 컨테이너 내부에서 프로세스 확인 (격리된 PID 네임스페이스)
docker exec ps aux
# 호스트에서 같은 프로세스 확인 (실제 PID)
ps aux | grep
```
**컨테이너가 사용하는 네임스페이스**:
* **PID**: 컨테이너는 자신만의 프로세스 트리를 가짐 (PID 1부터 시작)
* **Network**: 독립적인 네트워크 스택 (IP 주소, 라우팅 테이블, 포트)
* **Mount**: 독립적인 파일 시스템 뷰
* **UTS**: 독립적인 호스트네임
* **IPC**: 독립적인 프로세스 간 통신 공간
* **User**: 독립적인 사용자 ID 매핑 (선택적)
### cgroups를 통한 리소스 제한
컨테이너는 cgroups를 사용하여 리소스 사용량을 제한하고 모니터링합니다.
```bash
# CPU 제한이 있는 컨테이너 실행
docker run --cpus=0.5 --memory=512m nginx
# 컨테이너의 리소스 사용량 확인
docker stats
# 컨테이너의 cgroup 설정 확인
docker inspect | grep -A 20 "Cgroup"
# On a Linux cgroup v2 host, inspect a running container's actual path.
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' )
if [ "$CONTAINER_PID" -gt 0 ]; then
CGROUP_PATH=$(awk -F: '$1 == "0" {print $3}' "/proc/$CONTAINER_PID/cgroup")
cat "/sys/fs/cgroup$CGROUP_PATH/cpu.max"
cat "/sys/fs/cgroup$CGROUP_PATH/memory.max"
fi
```
**컨테이너가 사용하는 cgroup 리소스 제어**:
* **CPU**: CPU 시간 제한 및 CPU 코어 할당
* **Memory**: 메모리 사용량 제한 및 OOM 동작 제어
* **Block I/O**: 디스크 I/O 대역폭 제한
* **Network**: tc/eBPF와 연동한 트래픽 분류
* **PIDs**: 컨테이너 내 프로세스 수 제한
### OverlayFS를 통한 레이어 관리
Docker Engine 29.0 이상 새 설치는 containerd 이미지 저장소/snapshotter가 기본입니다. 기존 설치는 classic overlay2가 유지될 수 있으므로 docker info로 확인합니다. GraphDriver.Data 경로는 모든 설치에서 제공되지 않습니다.
OCI 이미지 레이어 형식은 저장 구현과 독립적입니다. OverlayFS는 흔한 Linux 백엔드이며 런타임은 다른 스토리지 드라이버/snapshotter도 사용할 수 있습니다.
```bash
# 이미지 레이어 확인
docker history
# 컨테이너의 파일 시스템 레이어 확인
docker info --format '{{.Driver}} {{json .DriverStatus}}'
docker image inspect --format '{{json .RootFS.Layers}}'
# OverlayFS 마운트 정보 확인
mount | grep overlay
```
**OverlayFS 구조**:
* **LowerDir**: 읽기 전용 이미지 레이어들 (하위 레이어 → 상위 레이어)
* **UpperDir**: 읽기/쓰기 가능한 컨테이너 레이어
* **WorkDir**: OverlayFS 작업 디렉토리
* **MergedDir**: 통합된 뷰 (컨테이너가 보는 파일 시스템)
### 실습: 컨테이너의 기술적 기반 이해하기
```bash
# 1. 간단한 컨테이너 실행
docker run -d --name test-container nginx
# 2. 컨테이너의 PID 확인
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' test-container)
echo "Container PID: $CONTAINER_PID"
# 3. 컨테이너의 네임스페이스 확인
ls -la /proc/$CONTAINER_PID/ns/
# 4. 컨테이너의 cgroup 확인
cat /proc/$CONTAINER_PID/cgroup
# 5. 컨테이너의 파일 시스템 레이어 확인
docker inspect test-container | jq '.[0].GraphDriver'
# 6. 정리
docker stop test-container
docker rm test-container
```
## 컨테이너 런타임
컨테이너 런타임은 컨테이너의 생명주기를 관리하는 소프트웨어입니다. 컨테이너 이미지를 실행하고, 컨테이너의 리소스 사용을 제한하며, 네트워킹과 스토리지를 설정합니다.
### 컨테이너 런타임 계층 구조
1. **저수준 런타임 (OCI 호환)**
* **runc**: Docker의 기본 런타임, OCI 표준 구현체
* **crun**: C로 작성된 경량 OCI 런타임
* **kata-containers**: 하드웨어 가상화를 사용한 보안 강화 런타임
* **gVisor**: 사용자 공간에서 커널 기능을 에뮬레이션하는 보안 런타임
2. **고수준 런타임**
* **containerd**: Docker에서 분리된 산업 표준 컨테이너 런타임
* **CRI-O**: Kubernetes를 위해 특별히 설계된 경량 런타임
* **Docker Engine**: 가장 널리 사용되는 컨테이너 플랫폼
### Kubernetes의 컨테이너 런타임 인터페이스 (CRI)
Kubernetes는 CRI(Container Runtime Interface)를 통해 다양한 컨테이너 런타임과 통합됩니다. CRI는 Kubernetes와 컨테이너 런타임 사이의 표준화된 인터페이스를 제공합니다.

Kubernetes 1.26 이상은 CRI v1을 요구합니다. 지원 중인 containerd/CRI-O 릴리스와 kubelet의 cgroup 드라이버를 맞춥니다. Docker Engine 자체는 CRI를 구현하지 않으며 내장 dockershim은 1.24에서 제거되었습니다. Docker Engine을 연결하려면 cri-dockerd 같은 별도 CRI 어댑터가 필요합니다. Docker로 만든 OCI 이미지는 계속 사용할 수 있습니다. CRI는 kubelet과 런타임의 API 규약이며 별도로 배포하는 중간 서비스 자체를 뜻하지 않습니다.
## 컨테이너 이미지
컨테이너 이미지는 애플리케이션과 그 종속성을 포함하는 불변의 템플릿입니다. 이미지는 여러 레이어로 구성되며, 각 레이어는 파일 시스템의 변경사항을 나타냅니다.
### 이미지 레이어
컨테이너 이미지는 여러 레이어의 스택으로 구성됩니다. 각 레이어는 이전 레이어에 대한 변경사항을 나타냅니다. 이 레이어 방식은 이미지 공유와 캐싱을 효율적으로 만듭니다.


[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-03-container-technology-0.html)
### 이미지 레지스트리
컨테이너 이미지는 레지스트리에 저장되고 공유됩니다. 주요 레지스트리는 다음과 같습니다:
* **Docker Hub**: 가장 큰 공개 레지스트리
* **Amazon ECR**: AWS의 컨테이너 레지스트리 서비스
* **Google Artifact Registry**: 현재 Google Cloud 레지스트리. Container Registry는 2025년 종료되었으며 Artifact Registry 기반 gcr.io 저장소는 별개
* **Azure Container Registry**: Microsoft Azure의 레지스트리
* **GitHub Container Registry**: GitHub의 컨테이너 레지스트리
* **Harbor**: 오픈소스 엔터프라이즈급 레지스트리
### 이미지 태그와 다이제스트
* **태그**: 사람이 읽을 수 있는 참조이며 레지스트리 불변성 정책이 없다면 다른 이미지로 재지정 가능 (예: `nginx:1.30.4`)
* **다이제스트**: 이미지 manifest 또는 다중 플랫폼 index의 콘텐츠 다이제스트(보통 SHA256)이며 config/layer 다이제스트를 참조 (예: `nginx@sha256:2834dc507516af02784808c5f48b7cbe38b8ed5d0f4837f16e78d00deb7e7767`)
## Dockerfile
Dockerfile은 컨테이너 이미지를 빌드하기 위한 지시사항을 포함하는 텍스트 파일입니다. 파일 시스템을 변경하는 지시문은 레이어를 만들 수 있지만 ENV/CMD 같은 메타데이터 지시문은 파일 시스템 diff를 추가하지 않습니다.
### 주요 Dockerfile 지시문
```dockerfile
FROM node:24-alpine
WORKDIR /app
ENV NODE_ENV=production
# Requires a committed package-lock.json matching package.json.
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .
RUN mkdir -p /app/data && chown node:node /app/data
USER node
EXPOSE 3000
VOLUME /app/data
CMD ["node", "server.js"]
```
Node.js 14는 지원이 종료되어 예제는 지원 중인 Node.js 24를 사용합니다. 네이티브 모듈은 Alpine의 musl 호환성을 확인합니다. .dockerignore를 준비하여 호스트 node_modules나 자격 증명을 이미지에 복사하지 않습니다. EXPOSE는 메타데이터이며 포트를 공개하지 않습니다. 포트 공개는 docker run -p 또는 오케스트레이터 설정이 필요합니다.
```text
# .dockerignore
node_modules
.git
.env
.env.*
npm-debug.log
```
### 다단계 빌드
다단계 빌드는 최종 이미지 크기를 줄이기 위해 여러 빌드 단계를 사용하는 기법입니다.
```dockerfile
FROM node:24 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.30.4-alpine
# This example assumes a static build written to dist/.
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
### 이미지 최적화 기법
1. **적절한 기본 이미지 선택**: Alpine과 같은 경량 이미지 사용
2. **다단계 빌드 사용**: 빌드 도구와 중간 파일 제외
3. **레이어 내용과 캐시 최적화**: 패키지 설치/정리를 하나의 RUN에서 처리하며 레이어 수만 줄여도 크기가 감소하는 것은 아님
4. **불필요한 파일 제외**: .dockerignore 파일 사용
5. **캐시 활용**: 자주 변경되는 레이어를 나중에 배치
## 컨테이너 네트워킹
컨테이너 네트워킹은 컨테이너 간, 그리고 컨테이너와 외부 세계 간의 통신을 가능하게 합니다.
### 네트워크 드라이버
Docker는 다양한 네트워크 드라이버를 제공합니다:
1. **bridge**: 기본 네트워크 드라이버, 동일한 호스트의 컨테이너 간 통신
2. **host**: 호스트 네트워크 네임스페이스를 공유하며 다른 컨테이너 격리는 유지
3. **overlay**: 다중 호스트 간 컨테이너 통신
4. **macvlan**: 컨테이너에 MAC 주소 할당, 물리 네트워크 장치처럼 보이게 함
5. **none**: 루프백만 남기고 외부 네트워크 연결 제거
### 포트 매핑
컨테이너의 내부 포트를 호스트의 포트에 매핑하여 외부에서 접근 가능하게 합니다.
```bash
# 호스트의 8080 포트를 컨테이너의 80 포트에 매핑
docker run -p 8080:80 nginx
```
포트 공개는 기본적으로 모든 호스트 주소에 바인딩합니다. 로컬 실습 전용이면 `-p 127.0.0.1:8080:80`을 사용합니다.
### 컨테이너 간 통신
1. **동일 네트워크**: 같은 사용자 정의 bridge의 컨테이너는 이름으로 확인할 수 있으며 기본 bridge에는 자동 이름 DNS가 없음
2. **링크**: 레거시 방식, 컨테이너 간 직접 링크 설정
3. **외부 네트워크**: 호스트 포트를 통한 통신
## 컨테이너 스토리지
Docker 컨테이너의 쓰기 레이어는 stop/start 후에도 유지되지만 컨테이너 삭제 시 제거됩니다. 영속 데이터는 볼륨 또는 외부 저장소에 보관합니다.
### 스토리지 유형
1. **임시 스토리지**: 컨테이너 내부 파일 시스템, 컨테이너 삭제 시 데이터 손실
2. **볼륨**: Docker가 관리하는 호스트 파일 시스템의 영역
3. **바인드 마운트**: 호스트의 특정 경로를 컨테이너에 마운트
4. **tmpfs 마운트**: 메모리 기반 임시 저장소이며 페이지가 디스크 swap에 기록될 수 있음
### 볼륨 사용 예시
```bash
# 볼륨 생성
docker volume create my-vol
# 볼륨을 사용하는 컨테이너 실행
docker run -v my-vol:/app/data nginx
# 바인드 마운트 사용
docker run -v /host/path:/container/path nginx
# 읽기 전용 마운트
docker run -v /host/path:/container/path:ro nginx
```
### 데이터 공유 패턴
1. **볼륨 공유**: 여러 컨테이너가 동일한 볼륨 사용
2. **데이터 볼륨 컨테이너**: 데이터만 포함하는 컨테이너 생성 후 공유
3. **외부 스토리지 통합**: AWS EBS, NFS 등 외부 스토리지 시스템 사용
## 컨테이너 보안
컨테이너 보안은 이미지, 컨테이너 런타임, 호스트 시스템 등 여러 계층에서 고려해야 합니다.
### 이미지 보안
1. **취약점 스캐닝**: Trivy, Clair 등의 도구로 이미지 취약점 검사
2. **신뢰할 수 있는 기본 이미지**: 공식 이미지 또는 검증된 이미지 사용
3. **최소 권한 원칙**: 필요한 패키지와 권한만 포함
4. **이미지 서명**: Cosign 등 유지 관리되는 이미지 서명 절차를 사용합니다. Docker Content Trust는 종료 예정이며 Docker Notary v1 서비스 종료 예정일은 2026-12-08입니다.
### 런타임 보안
1. **권한 제한**: 루트가 아닌 사용자로 컨테이너 실행
2. **기능(capabilities) 제한**: 필요한 Linux 기능만 부여
3. **seccomp 프로필**: 시스템 호출 제한
4. **AppArmor/SELinux**: 강제적 접근 제어 적용
5. **읽기 전용 파일 시스템**: 가능한 경우 파일 시스템을 읽기 전용으로 마운트
### 보안 모범 사례
1. **정기적인 업데이트**: 컨테이너 이미지와 호스트 시스템 정기 업데이트
2. **네트워크 분리**: 적절한 네트워크 정책으로 컨테이너 간 통신 제한
3. **시크릿 관리**: 플랫폼 시크릿 또는 외부 관리자를 사용합니다. Docker Swarm secrets는 서비스용이며 일반 docker run에는 적용되지 않고 Compose 파일 시크릿은 보장 범위가 다릅니다.
4. **리소스 제한**: CPU, 메모리 등 리소스 사용량 제한
5. **모니터링 및 로깅**: 컨테이너 활동 모니터링 및 로그 중앙화
## 컨테이너 라이프사이클 관리
컨테이너의 전체 라이프사이클을 이해하는 것은 효과적인 컨테이너 운영에 필수적입니다.
### 컨테이너 상태
컨테이너는 여러 상태를 가질 수 있습니다:
* **Created**: 컨테이너가 생성되었으나 아직 시작되지 않음
* **Running**: 컨테이너가 실행 중
* **Paused**: Linux 프로세스를 freezer cgroup으로 일시 중지
* **Restarting**: 컨테이너가 재시작 중
* **Exited**: 컨테이너가 종료됨
* **Removing**: 컨테이너 삭제 진행 중
* **Dead**: 일부만 제거된 비정상 컨테이너이며 재시작할 수 없어 정리 필요
```bash
# 컨테이너 상태 확인
docker ps -a
# 특정 컨테이너 상태 상세 정보
docker inspect | jq '.[0].State'
# 컨테이너 상태 전환
docker create nginx # Created 상태
docker start # Running 상태로 전환
docker pause # Paused 상태로 전환
docker unpause # Running 상태로 복귀
docker stop # Exited 상태로 전환
docker rm # 컨테이너 제거
```
### 컨테이너 생성 및 실행
```bash
# 컨테이너 생성만 (시작하지 않음)
docker create --name my-nginx nginx
# 컨테이너 시작
docker start my-nginx
# 컨테이너 생성 및 시작 (한 번에)
docker run --name my-nginx2 -d nginx
# 인터랙티브 모드로 실행
docker run -it ubuntu bash
# 백그라운드에서 실행
docker run -d nginx
# 컨테이너 종료 시 자동 제거
docker run --rm nginx
# 환경 변수와 함께 실행
docker run -e "DB_HOST=localhost" -e "DB_PORT=5432" myapp
# 포트 매핑과 함께 실행
docker run -p 8080:80 nginx
# 볼륨 마운트와 함께 실행
docker run -v /host/path:/container/path nginx
```
### 컨테이너 제어
```bash
# 실행 중인 컨테이너 목록
docker ps
# 모든 컨테이너 목록 (중지된 것 포함)
docker ps -a
# 설정한 종료 신호(기본 SIGTERM) 전송 후 제한 시간 초과 시 SIGKILL
docker stop
# 컨테이너 강제 종료 (SIGKILL)
docker kill
# 컨테이너 재시작
docker restart
# 컨테이너 일시 중지
docker pause
# 컨테이너 재개
docker unpause
# 실행 중인 컨테이너에 명령 실행
docker exec -it bash
docker exec ls -la /app
# 컨테이너에서 파일 복사
docker cp :/path/to/file /local/path
docker cp /local/path :/path/to/file
```
### 컨테이너 로깅 및 모니터링
```bash
# 컨테이너 로그 확인
docker logs
# 실시간 로그 스트리밍
docker logs -f
# 마지막 N개 로그 라인
docker logs --tail 100
# 타임스탬프와 함께 로그 출력
docker logs -t
# 특정 시간 이후 로그
docker logs --since "2025-11-24T10:00:00"
# 컨테이너 리소스 사용량 확인
docker stats
# 모든 컨테이너 리소스 사용량
docker stats
# 컨테이너 프로세스 확인
docker top
# 컨테이너 상세 정보
docker inspect
```
### 컨테이너 정리
```bash
# 중지된 모든 컨테이너 제거
docker container prune
# 중지된 컨테이너, 미사용 네트워크, dangling 이미지 및 빌드 캐시 제거; 볼륨 제외
docker system prune
# 추가로 미사용 익명 볼륨 정리; 실행 중인 리소스는 제거하지 않음
docker system prune --volumes
# 디스크 사용량 확인
docker system df
# 이미지 제거
docker rmi
# dangling 이미지 제거; -a는 모든 미사용 이미지 포함
docker image prune
# 볼륨 제거
docker volume rm
# 미사용 익명 볼륨 제거; --all은 미사용 이름 있는 볼륨도 포함
docker volume prune
# 네트워크 제거
docker network rm
# 사용하지 않는 네트워크 제거
docker network prune
```
### 헬스 체크
HEALTHCHECK는 Docker 상태를 기록합니다. 일반 Docker의 재시작 정책은 프로세스 종료에 반응하며 unhealthy만으로 재시작하지 않습니다. Kubernetes는 Dockerfile HEALTHCHECK 대신 liveness/readiness/startup probe를 사용합니다.
```dockerfile
FROM nginx:1.30.4-alpine
# Dockerfile에서 헬스 체크 정의
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1/ || exit 1
```
```bash
# 실행 시 헬스 체크 정의
docker run -d \
--health-cmd="wget -q -O /dev/null http://127.0.0.1/ || exit 1" \
--health-interval=30s \
--health-timeout=3s \
--health-retries=3 \
nginx:1.30.4-alpine
# 헬스 체크 상태 확인
docker inspect | jq '.[0].State.Health'
```
### 재시작 정책
컨테이너가 종료될 때 자동으로 재시작하도록 설정할 수 있습니다.
```bash
# 재시작 정책 옵션
# - no: 재시작하지 않음 (기본값)
# - on-failure: 실패 시에만 재시작
# - always: 종료 후 재시작; 수동 중지 후에는 데몬 재시작/명시적 start까지 억제
# - unless-stopped: 명시적으로 중지하지 않는 한 항상 재시작
# 실패 시 재시작 (최대 3회)
docker run -d --restart=on-failure:3 nginx
# 항상 재시작
docker run -d --restart=always nginx
# 명시적으로 중지하지 않는 한 재시작
docker run -d --restart=unless-stopped nginx
# 기존 컨테이너의 재시작 정책 변경
docker update --restart=always
```
### 컨테이너 디버깅
bash/ip/netstat/ps는 이미지에 설치되어 있어야 합니다. 최소 이미지에 없다면 호스트의 docker inspect/top 또는 승인된 디버그 이미지를 사용합니다. env/inspect 출력에는 시크릿이 포함될 수 있으므로 공유 로그에 그대로 남기지 않습니다.
```bash
# 컨테이너 내부 파일 시스템 탐색
docker exec -it bash
# 컨테이너의 환경 변수 확인
docker exec env
# 컨테이너의 네트워크 정보 확인
docker exec ip addr
docker exec netstat -tuln
# 컨테이너의 프로세스 확인
docker exec ps aux
# 컨테이너 이벤트 모니터링
docker events
# 특정 컨테이너 이벤트 필터링
docker events --filter container=
# 컨테이너 변경 사항 확인 (이미지와 비교)
docker diff
```
## 컨테이너 오케스트레이션
컨테이너 오케스트레이션은 다수의 컨테이너를 관리하고 조정하는 프로세스입니다. 주요 기능으로는 배포 관리, 확장, 네트워킹, 서비스 검색 등이 있습니다.
### 주요 오케스트레이션 도구
1. **Kubernetes**: 가장 널리 사용되는 컨테이너 오케스트레이션 플랫폼
2. **Docker Swarm**: Docker의 내장 오케스트레이션 도구, 간단한 설정
3. **Amazon ECS**: AWS의 컨테이너 오케스트레이션 서비스
4. **HashiCorp Nomad**: 컨테이너 및 비컨테이너 워크로드 모두 지원
### 오케스트레이션의 주요 기능
1. **자동 배포 및 롤백**: 선언적 구성을 통한 애플리케이션 배포 관리
2. **서비스 검색 및 로드 밸런싱**: 컨테이너 간 통신 및 부하 분산
3. **자동 확장**: 부하에 따른 컨테이너 수 조정
4. **자가 복구**: 실패한 컨테이너 자동 재시작
5. **구성 관리**: 애플리케이션 구성 및 시크릿 관리
6. **스토리지 오케스트레이션**: 영구 스토리지 관리
7. **배치 실행**: 일회성 작업 및 크론 작업 실행
## AWS에서의 컨테이너
AWS는 컨테이너 워크로드를 위한 다양한 서비스를 제공합니다.
### Amazon ECS (Elastic Container Service)
AWS의 자체 컨테이너 오케스트레이션 서비스로, EC2 인스턴스 또는 AWS Fargate에서 컨테이너를 실행할 수 있습니다.
**주요 특징**:
* AWS 서비스와의 긴밀한 통합
* 서버리스 컨테이너 실행 (Fargate)
* 간단한 설정 및 관리
* 자동 확장 및 로드 밸런싱
### Amazon EKS (Elastic Kubernetes Service)
AWS에서 관리하는 Kubernetes 서비스로, 표준 Kubernetes API를 사용하여 AWS 인프라에서 Kubernetes를 실행할 수 있습니다.
**주요 특징**:
* 관리형 Kubernetes 컨트롤 플레인
* 여러 가용 영역에 걸친 고가용성
* AWS 서비스와의 통합
* EC2 및 Fargate 지원
### AWS Fargate
서버리스 컨테이너 실행 환경으로, 서버를 관리하지 않고도 컨테이너를 실행할 수 있습니다.
**주요 특징**:
* 서버 관리 불필요
* ECS 태스크 또는 EKS Pod에 요청한 리소스 기준 과금이며 개별 애플리케이션 컨테이너당 별도 과금은 아님
* ECS 및 EKS와 통합
* 보안 격리
### Amazon ECR (Elastic Container Registry)
AWS의 관리형 컨테이너 이미지 레지스트리 서비스입니다.
**주요 특징**:
* 이미지 취약점 스캐닝
* IAM과의 통합
* 이미지 라이프사이클 관리
* 고가용성 및 확장성
## 용어집
| 용어 | 설명 |
| -------------- | ------------------------------------------------------------------------ |
| **컨테이너** | 애플리케이션과 그 종속성을 함께 패키징한 표준화된 소프트웨어 유닛으로, 어디서나 일관되게 실행할 수 있습니다. |
| **이미지** | 컨테이너를 생성하는 데 사용되는 읽기 전용 템플릿으로, 애플리케이션 코드, 라이브러리, 종속성, 도구 및 기타 파일을 포함합니다. |
| **Dockerfile** | 컨테이너 이미지를 빌드하기 위한 지시사항이 포함된 텍스트 파일입니다. |
| **레지스트리** | 컨테이너 이미지를 저장하고 배포하는 저장소입니다. (예: Docker Hub, Amazon ECR) |
| **컨테이너 런타임** | 컨테이너를 실행하는 소프트웨어입니다. (예: Docker, containerd, CRI-O) |
| **네임스페이스** | Linux 커널 기능으로, 프로세스가 시스템의 다른 부분을 볼 수 없도록 격리합니다. |
| **cgroups** | Linux 커널 기능으로, 프로세스 그룹의 리소스 사용(CPU, 메모리 등)을 제한하고 모니터링합니다. |
| **레이어** | 컨테이너 이미지는 여러 레이어로 구성되며, 레이어는 파일 시스템 diff이며 메타데이터 전용 지시문은 파일 시스템 레이어를 만들지 않습니다. |
| **볼륨** | 컨테이너의 데이터를 영구적으로 저장하기 위한 메커니즘입니다. |
| **오케스트레이션** | 여러 컨테이너의 배포, 관리, 확장, 네트워킹을 자동화하는 프로세스입니다. |
| **ECS** | Amazon Elastic Container Service의 약자로, AWS의 컨테이너 오케스트레이션 서비스입니다. |
| **ECR** | Amazon Elastic Container Registry의 약자로, AWS의 컨테이너 이미지 레지스트리 서비스입니다. |
| **Fargate** | AWS의 서버리스 컨테이너 실행 환경으로, 인프라 관리 없이 컨테이너를 실행할 수 있습니다. |
## 결론
컨테이너 기술은 애플리케이션 개발 및 배포 방식을 혁신적으로 변화시켰습니다. 이식성, 일관성, 효율성을 제공하여 개발자 생산성을 향상시키고 운영 복잡성을 줄였습니다. Kubernetes와 같은 오케스트레이션 도구와 결합하면 대규모 분산 애플리케이션을 효과적으로 관리할 수 있습니다.
컨테이너의 기본 개념과 작동 원리를 이해하는 것은 현대적인 클라우드 네이티브 애플리케이션을 개발하고 운영하는 데 필수적입니다. 이러한 지식은 Kubernetes를 효과적으로 활용하기 위한 기반이 됩니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [컨테이너 기술 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/basics/03-container-technology-quiz)를 풀어보세요.
## 참고 자료
* [Docker 공식 문서](https://docs.docker.com/)
* [OCI (Open Container Initiative)](https://opencontainers.org/)
* [containerd 프로젝트](https://containerd.io/)
* [Kubernetes 컨테이너 런타임 개요](https://kubernetes.io/docs/setup/production-environment/container-runtimes/)
* [AWS 컨테이너 서비스](https://aws.amazon.com/containers/)
## 검증 참고 자료
- https://kubernetes.io/docs/setup/production-environment/container-runtimes/
- https://docs.docker.com/reference/cli/docker/container/pause/
- https://docs.docker.com/reference/cli/docker/container/ls/
- https://docs.docker.com/engine/containers/start-containers-automatically/
- https://docs.docker.com/reference/cli/docker/system/prune/
- https://docs.docker.com/reference/cli/docker/volume/prune/
- https://docs.docker.com/reference/dockerfile/
- https://docs.docker.com/engine/network/drivers/bridge/
- https://docs.docker.com/engine/storage/containerd/
- https://docs.docker.com/engine/storage/tmpfs/
- https://docs.docker.com/engine/security/trust/
- https://docs.docker.com/engine/swarm/secrets/
- https://github.com/opencontainers/image-spec/blob/main/config.md
- https://github.com/opencontainers/image-spec/blob/main/manifest.md
- https://github.com/nodejs/Release/blob/main/schedule.json
- https://github.com/docker-library/official-images/blob/master/library/node
- https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md
- https://github.com/npm/cli/blob/latest/docs/lib/content/commands/npm-ci.md
- https://cloud.google.com/artifact-registry/docs/transition/transition-from-gcr
- https://man7.org/linux/man-pages/man7/cgroups.7.html
- https://github.com/torvalds/linux/releases/tag/v2.6.24
- https://docs.aws.amazon.com/eks/latest/userguide/fargate.html
- https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html
- https://aws.amazon.com/fargate/pricing/
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/basics/05-ebpf-fundamentals
----------------------------------------
# eBPF 기초와 Kubernetes 활용
> **지원 버전**: 프로그램별 커널/BTF/헬퍼 요구사항 및 도구/Kubernetes 호환성 표 확인
> **마지막 업데이트**: 2026년 9월 11일
eBPF는 Linux 커널 내에서 샌드박스화된 프로그램을 실행할 수 있게 해주는 혁신적인 기술입니다. 이 문서에서는 eBPF의 기본 개념부터 Kubernetes 환경에서의 활용까지 전반적인 내용을 다룹니다.
## 목차
* [1. eBPF 소개](#1-ebpf-소개)
* [2. eBPF 아키텍처](#2-ebpf-아키텍처)
* [3. eBPF 프로그램 유형](#3-ebpf-프로그램-유형)
* [4. eBPF 개발 도구](#4-ebpf-개발-도구)
* [5. eBPF와 Kubernetes 네트워킹](#5-ebpf와-kubernetes-네트워킹)
* [6. eBPF 기반 관찰성](#6-ebpf-기반-관찰성)
* [7. eBPF 기반 보안](#7-ebpf-기반-보안)
* [8. eBPF 실전 활용 예제](#8-ebpf-실전-활용-예제)
* [9. eBPF 제한 사항과 주의점](#9-ebpf-제한-사항과-주의점)
* [10. 다음 단계](#10-다음-단계)
## 실습 환경 설정
이 문서의 예제를 따라하기 위해서는 다음과 같은 환경이 필요합니다.
### 필수 환경
- 각 예제에 필요한 BTF/헬퍼/연결 유형을 지원하는 유지 관리 중인 배포판 커널
- bpftool, bcc-tools
- Kubernetes 클러스터 (선택 사항)
bpftrace 예제는 공식 0.27 언어 문법(args.field)을 기준으로 검토했습니다. 배포판 패키지가 더 오래되면 설치된 버전의 문법/기능을 확인합니다. tracepoint 필드는 `bpftrace -lv` 또는 tracefs의 format 파일로 검증하며 함수 kprobe/uprobes는 커널/라이브러리 버전과 아키텍처에 종속됩니다. 실제 trace/attach는 수행하지 않았습니다.
### 환경 설정
```bash
# Ubuntu/Debian에서 필요한 패키지 설치
sudo apt-get update
sudo apt-get install -y bpfcc-tools python3-bpfcc bpftrace
# Install bpftool for this distribution/kernel separately:
# Debian provides the bpftool package; Ubuntu uses matching linux-tools packages.
# 커널 버전 확인
uname -r
# eBPF 기능 지원 확인
sudo bpftool feature probe kernel
```
---
## 1. eBPF 소개
### 1.1 eBPF란 무엇인가?
**eBPF(extended Berkeley Packet Filter)**는 Linux 커널 내에서 안전하게 사용자 정의 프로그램을 실행할 수 있게 해주는 기술입니다. 원래 네트워크 패킷 필터링을 위해 설계되었던 BPF를 확장하여, 이제는 네트워킹, 보안, 추적, 성능 분석 등 다양한 영역에서 활용됩니다.
> **핵심 개념**: eBPF를 사용하면 커널 소스 코드를 수정하거나 커널 모듈을 로드하지 않고도 커널의 동작을 확장하고 관찰할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-05-ebpf-fundamentals-0.html)
### 1.2 전통적인 BPF에서 eBPF로의 진화
**초기 BPF (1992년)**:
- UC 버클리에서 개발
- 네트워크 패킷 캡처 및 필터링 전용
- 2개의 32비트 레지스터
- Linux classic BPF의 일반적 한도는4096개이며 모든 역사적 BPF 구현의 규격은 아님
**eBPF (2014년~)**:
- 64비트 아키텍처 지원
- 11개의 레지스터
- 맵(Maps)을 통한 상태 저장
- 다양한 훅 포인트 지원
- JIT 컴파일을 통한 네이티브 성능
| 특성 | 전통적 BPF | eBPF |
|------|-----------|------|
| 레지스터 | 2개 (32비트) | 11개 (64비트) |
| 명령어 제한 | 일반적 Linux 한도4096 | 커널/권한별 상이; 프로그램 크기와 검증 복잡도는 별개 |
| 맵 지원 | 없음 | 다양한 맵 유형 |
| 용도 | 패킷 필터링 | 범용 커널 프로그래밍 |
| 호출 기능 | 없음 | 헬퍼 함수, BPF-to-BPF 호출 |
| 영속 상태 | 영속 맵 없음(한 실행 내 scratch 저장소는 존재) | 맵을 통해 가능 |
### 1.3 eBPF가 혁신적인 이유
eBPF는 다음과 같은 이유로 혁신적입니다:
1. **커널 수정 없는 기능 확장**: 커널 소스 코드를 변경하지 않고도 커널 기능을 확장
2. **안전한 실행**: 검증기가 정의된 메모리/제어 흐름 안전 속성을 검사
3. **높은 성능**: JIT 컴파일로 네이티브 코드 수준의 성능
4. **동적 로딩**: 재부팅 없이 프로그램 로드/언로드 가능
5. **프로덕션 안정성**: 실행 경계 검사가 위험을 줄이지만 정책 정확성, 커널/JIT 버그 및 운영 영향은 별도 검증 필요

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-05-ebpf-fundamentals-1.html)
### 1.4 eBPF vs 커널 모듈 비교
| 측면 | eBPF | 커널 모듈 |
|------|------|----------|
| **안전성** | 검증 모델 범위의 안전성 검사 | 커널 크래시 가능 |
| **이식성** | CO-RE는 호환되는 커널 타입을 재배치하며 헬퍼/훅/의미/BTF 제약은 남음 | 커널 버전별 재컴파일 필요 |
| **로딩** | 동적 로드/언로드 | insmod/rmmod 필요 |
| **권한** | CAP_BPF/CAP_SYS_ADMIN 및 훅별 추가 권한 | root 권한 필요 |
| **디버깅** | 제한적 | 전체 커널 디버깅 가능 |
| **성능** | JIT 컴파일로 최적화 | 네이티브 성능 |
| **기능 범위** | 정해진 훅 포인트만 | 무제한 |
| **개발 난이도** | 상대적으로 쉬움 | 높은 전문성 필요 |
---
## 2. eBPF 아키텍처
### 2.1 eBPF 실행 흐름

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-05-ebpf-fundamentals-2.html)
### 2.2 검증기 (Verifier)
검증기는 eBPF의 핵심 보안 메커니즘입니다. 프로그램이 커널에서 실행되기 전에 다음 사항을 검증합니다:
**검증 항목**:
- 종료/제한된 제어 흐름 검사; 지원 커널에서는 bounded loop 사용 가능
- 범위를 벗어난 메모리 접근 없음
- 초기화되지 않은 변수 사용 없음
- 올바른 헬퍼 함수 호출
- 프로그램 종료 보장
```c
// XDP fragments; compile as separate programs with linux/bpf.h and bpf_helpers.h.
SEC("xdp")
int bad_example(struct xdp_md *ctx) {
unsigned char *data = (void *)(long)ctx->data;
// No data_end check: the verifier cannot prove this packet byte exists.
return data[0] == 0 ? XDP_DROP : XDP_PASS;
}
SEC("xdp")
int good_example(struct xdp_md *ctx) {
unsigned char *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
if ((void *)(data + 1) > data_end)
return XDP_PASS;
return data[0] == 0 ? XDP_DROP : XDP_PASS;
}
```
### 2.3 JIT 컴파일러
JIT(Just-In-Time) 컴파일러는 eBPF 바이트코드를 네이티브 머신 코드로 변환합니다:
```bash
# JIT 컴파일러 상태 확인
cat /proc/sys/net/core/bpf_jit_enable
# JIT 컴파일러 활성화 (0: 비활성화, 1: 활성화, 2: 디버그 모드)
echo 1 | sudo tee /proc/sys/net/core/bpf_jit_enable
```
CONFIG_BPF_JIT_ALWAYS_ON을 사용하는 커널에서는 이 sysctl의 존재/변경 가능 여부가 다릅니다. 디버그 모드2는 커널 로그를 출력하므로 프로덕션 기본값으로 사용하지 않습니다.
**JIT 컴파일 이점**:
- 원문의 4~5배 향상 수치는 출처가 제시되지 않았으며 실제 차이는 프로그램/아키텍처/커널에 따라 다름
- 네이티브 CPU 명령어로 직접 실행
- 아키텍처별 최적화 적용
### 2.4 eBPF 맵 (Maps)
eBPF 맵은 커널과 사용자 공간 간 데이터를 공유하고 상태를 저장하는 데이터 구조입니다.
**주요 맵 유형**:
| 맵 유형 | 설명 | 사용 사례 |
|---------|------|----------|
| `BPF_MAP_TYPE_HASH` | 해시 테이블 | 키-값 저장, 연결 추적 |
| `BPF_MAP_TYPE_ARRAY` | 고정 크기 배열 | 인덱스 기반 접근, 설정 값 |
| `BPF_MAP_TYPE_PERF_EVENT_ARRAY` | 이벤트 배열 | 사용자 공간으로 이벤트 전송 |
| `BPF_MAP_TYPE_RINGBUF` | 링 버퍼 | 고성능 이벤트 스트리밍 |
| `BPF_MAP_TYPE_LRU_HASH` | LRU 해시 | 캐시, 자동 항목 제거 |
| `BPF_MAP_TYPE_PERCPU_ARRAY` | CPU별 배열 | 통계 수집의 CPU 간 경합 감소 |
| `BPF_MAP_TYPE_LPM_TRIE` | LPM 트라이 | IP 주소 매칭, 라우팅 |
```c
// 해시 맵 정의 예제
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32); // 키: 프로세스 ID
__type(value, __u64); // 값: 카운터
} packet_count SEC(".maps");
```
### 2.5 헬퍼 함수 (Helper Functions)
eBPF 프로그램은 커널이 제공하는 헬퍼 함수를 통해 커널 기능에 접근합니다.
**주요 헬퍼 함수**:
아래는 API 역할을 설명하는 축약 표기입니다. 실제 프로그램에서는 libbpf의 bpf_helpers.h를 포함하며 이 선언들을 재정의하지 않습니다. 헬퍼 사용 가능 여부는 프로그램 유형/커널에 따라 다릅니다.
```text
// 맵 조작
void *bpf_map_lookup_elem(void *map, const void *key);
long bpf_map_update_elem(void *map, const void *key, const void *value, u64 flags);
long bpf_map_delete_elem(void *map, const void *key);
// 시간 관련
u64 bpf_ktime_get_ns(void); // 부팅 후 단조 시간(ns), suspend 제외; 실제 날짜/시각이 아님
// 패킷 조작
long bpf_skb_load_bytes(const void *skb, u32 offset, void *to, u32 len);
long bpf_xdp_adjust_head(struct xdp_md *xdp_md, int delta);
// 추적
long bpf_probe_read_kernel(void *dst, u32 size, const void *src);
long bpf_probe_read_user(void *dst, u32 size, const void *src);
long bpf_trace_printk(const char *fmt, u32 fmt_size, ...);
// 프로세스 정보
u64 bpf_get_current_pid_tgid(void); // PID/TGID 획득
u64 bpf_get_current_uid_gid(void); // UID/GID 획득
long bpf_get_current_comm(void *buf, u32 size); // 프로세스 이름
```
### 2.6 프로그램 라이프사이클

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-05-ebpf-fundamentals-3.html)
---
C 예제는 별도 프로그램/조각입니다. vmlinux.h 또는 필요한 UAPI 타입과 libbpf의 bpf_helpers.h, bpf_endian.h, bpf_tracing.h, bpf_core_read.h를 용도에 맞게 포함합니다. BPF_KPROBE/BPF_UPROBE는 올바른 대상 아키텍처 정의와 실제 attach 지점/ABI가 필요합니다. 로드/attach는 격리된 테스트 환경에서 검증해야 하며 이 감사에서는 수행하지 않았습니다. 경로 기반 LSM 예제는 읽기 오류에 fail-open하고 별칭/하드링크/다른 프로토콜까지 방어하지 않는 교육용입니다.
## 3. eBPF 프로그램 유형
### 3.1 XDP (eXpress Data Path)
XDP는 네트워크 드라이버 레벨에서 패킷을 처리하는 가장 빠른 방법입니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-05-ebpf-fundamentals-4.html)
**XDP 동작 모드**:
| 모드 | 설명 | 성능 |
|------|------|------|
| Native XDP | 지원 드라이버 수신 경로에서 실행 | 드라이버/워크로드에 따라 다름 |
| Offloaded XDP | 지원 NIC 하드웨어에서 실행 | 하드웨어/명령 제한 및 실제 측정 필요 |
| Generic XDP | 스택의 skb 기반 대체 경로 | 일반적으로 native보다 오버헤드 증가 |
```c
#include
#include
#include
#include
#include
#include
#include
// Demonstration only: untagged, non-fragmented IPv4 TCP.
// VLAN, IPv6 and fragments pass through; this is not a complete firewall.
static __always_inline int packet_action(void *data, void *data_end) {
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end || eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end || ip->version != 4 || ip->ihl < 5)
return XDP_PASS;
__u32 ihl = (__u32)ip->ihl * 4;
__u32 ip_len = bpf_ntohs(ip->tot_len);
if ((void *)ip + ihl > data_end || ip_len < ihl || (void *)ip + ip_len > data_end)
return XDP_PASS;
if (ip->protocol != IPPROTO_TCP || (bpf_ntohs(ip->frag_off) & 0x3fffU))
return XDP_PASS;
if (ip_len < ihl + sizeof(struct tcphdr))
return XDP_PASS;
struct tcphdr *tcp = (void *)ip + ihl;
if ((void *)(tcp + 1) > data_end || tcp->doff < 5)
return XDP_PASS;
__u32 tcp_len = (__u32)tcp->doff * 4;
if (ihl + tcp_len > ip_len || (void *)tcp + tcp_len > data_end)
return XDP_PASS;
return tcp->dest == bpf_htons(8080) ? XDP_DROP : XDP_PASS;
}
SEC("xdp")
int xdp_drop_port(struct xdp_md *ctx) {
return packet_action((void *)(long)ctx->data, (void *)(long)ctx->data_end);
}
char LICENSE[] SEC("license") = "GPL";
```
### 3.2 TC (Traffic Control)
TC 프로그램은 네트워크 스택의 트래픽 제어 계층에서 실행됩니다.
```bash
# TC 프로그램 연결 예제
set -e
: "${LAB_IFACE:?Select an isolated test veth interface, never a production interface}"
tc qdisc show dev "$LAB_IFACE"
# This assumes a fresh lab interface with no clsact qdisc.
sudo tc qdisc add dev "$LAB_IFACE" clsact
sudo tc filter add dev "$LAB_IFACE" ingress pref 49152 bpf da obj tc_prog.o sec classifier
sudo tc filter add dev "$LAB_IFACE" egress pref 49152 bpf da obj tc_prog.o sec classifier
# Cleanup only the filters created by this example, after the exercise:
# sudo tc filter del dev "$LAB_IFACE" ingress pref 49152
# sudo tc filter del dev "$LAB_IFACE" egress pref 49152
```
**TC vs XDP 비교**:
| 특성 | XDP | TC |
|------|-----|-----|
| 실행 위치 | 드라이버 레벨 | 네트워크 스택 |
| 성능 | 최고 | 높음 |
| SKB 접근 | 불가 | 가능 |
| 방향 | 수신만 | 송수신 모두 |
| 패킷 수정 | 제한적 | 자유로움 |
### 3.3 Kprobes/Uprobes
Kprobes와 Uprobes는 함수 호출을 동적으로 추적합니다.
```c
// Kprobe 예제: tcp_connect 함수 추적
SEC("kprobe/tcp_connect")
int BPF_KPROBE(trace_tcp_connect, struct sock *sk) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
// 목적지 IP 주소 획득
u32 daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
u16 dport = BPF_CORE_READ(sk, __sk_common.skc_dport);
bpf_printk("PID %d connecting to %pI4:%d\n", pid, &daddr, bpf_ntohs(dport));
return 0;
}
// Uprobe 예제: malloc 함수 추적
// The userspace loader must select the real libc path, PID and malloc symbol.
SEC("uprobe")
int BPF_UPROBE(trace_malloc, size_t size) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("PID %d malloc(%zu)\n", pid, size);
return 0;
}
```
### 3.4 Tracepoints
Tracepoints는 커널에 미리 정의된 정적 추적점입니다.
```bash
# 사용 가능한 tracepoints 확인
sudo ls /sys/kernel/tracing/events/
# 특정 카테고리의 tracepoints
sudo ls /sys/kernel/tracing/events/sched/
sudo ls /sys/kernel/tracing/events/syscalls/
```
```c
// Tracepoint 예제: 프로세스 시작 추적
SEC("tracepoint/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx) {
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
u32 pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("Process started: %s (PID: %d)\n", comm, pid);
return 0;
}
```
### 3.5 LSM (Linux Security Module) BPF
LSM BPF는 보안 정책을 동적으로 적용합니다.
```c
// LSM BPF 예제: 파일 열기 제한
SEC("lsm/file_open")
int BPF_PROG(restrict_file_open, struct file *file, int ret) {
if (ret != 0)
return ret;
char path[256];
if (bpf_d_path(&file->f_path, path, sizeof(path)) < 0)
return 0; // Demo fails open on unresolved paths; not a complete access policy.
// /etc/shadow 접근 차단
if (bpf_strncmp(path, 11, "/etc/shadow") == 0)
return -EACCES;
return 0;
}
```
### 3.6 Socket Filter
소켓 레벨에서 패킷을 필터링합니다.
```c
// Socket Filter 예제
SEC("socket")
int socket_filter(struct __sk_buff *skb) {
// IPv4 패킷만 허용
if (skb->protocol != bpf_htons(ETH_P_IP))
return 0; // 드롭
return skb->len; // 패킷 길이 반환 (허용)
}
```
### 3.7 Cgroup 프로그램
컨테이너의 리소스와 네트워크를 제어합니다.
```c
// Cgroup 소켓 프로그램 예제: 외부 연결 차단
SEC("cgroup/connect4")
int restrict_connect(struct bpf_sock_addr *ctx) {
// 로컬 네트워크가 아닌 연결 차단
__u32 dst = bpf_ntohl(ctx->user_ip4);
// 10.0.0.0/8 대역만 허용
if ((dst & 0xff000000U) != 0x0a000000U)
return 0; // 연결 거부
return 1; // 연결 허용
}
```
---
## 4. eBPF 개발 도구
### 4.1 bpftool
bpftool은 BPF 프로그램/맵을 관리합니다. 실습에서 만든 맵만 수정하며 실제 CNI/보안 맵 변경은 실행 중인 워크로드에 영향을 줍니다. 아래 hex 예제는 앞의 맵과 일치하는 little-endian u32 키/u64 값을 가정합니다.
```bash
# 로드된 eBPF 프로그램 목록
sudo bpftool prog list
# 프로그램 상세 정보
sudo bpftool prog show id
# 프로그램 덤프 (바이트코드)
sudo bpftool prog dump xlated id
# JIT 컴파일된 코드 덤프
sudo bpftool prog dump jited id
# 맵 목록
sudo bpftool map list
# 맵 내용 조회
sudo bpftool map dump id
# 맵에 값 추가
sudo bpftool map update id key hex 01 00 00 00 value hex ff 00 00 00 00 00 00 00
# 커널의 eBPF 기능 확인
sudo bpftool feature probe kernel
# BTF (BPF Type Format) 정보
sudo bpftool btf list
```
### 4.2 bpftrace
bpftrace는 DTrace 스타일의 고수준 추적 언어입니다.
```bash
# 설치
sudo apt-get install -y bpftrace
# 시스템 콜 카운트
sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
# 프로세스별 읽기 바이트 수
sudo bpftrace -e 'tracepoint:syscalls:sys_exit_read /args.ret > 0/ { @bytes[comm] = sum(args.ret); }'
# 파일 열기 추적
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s opened %s\n", comm, str(args.filename)); }'
# TCP 연결 추적
sudo bpftrace -e 'kprobe:tcp_connect { printf("%s -> %s\n", ntop(((struct sock *)arg0)->__sk_common.skc_rcv_saddr), ntop(((struct sock *)arg0)->__sk_common.skc_daddr)); }'
# 지연 시간 히스토그램
sudo bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; } kretprobe:vfs_read /@start[tid]/ { @ns = hist(nsecs - @start[tid]); delete(@start[tid]); }'
```
**유용한 bpftrace 원라이너**:
```bash
# CPU 사용량 상위 프로세스
sudo bpftrace -e 'profile:hz:99 { @[comm] = count(); }'
# 블록 I/O 지연 시간
sudo biolatency-bpfcc 1 10 # Maintained request correlation; avoids dev/sector collisions
# 새 프로세스 추적
sudo bpftrace -e 'tracepoint:sched:sched_process_exec { printf("%-10d %-16s\n", pid, comm); }'
# 메모리 할당 추적
sudo bpftrace -e 'tracepoint:kmem:kmalloc { @bytes = hist(args.bytes_alloc); }'
```
### 4.3 BCC (BPF Compiler Collection)
BCC는 BPF C 컴파일/로딩을 제공하며 일반적으로 Python 추적 도구에 BPF C를 포함하여 사용합니다.
```bash
# 설치
sudo apt-get install -y bpfcc-tools python3-bpfcc
# 포함된 도구들
dpkg -L bpfcc-tools | head -40
```
**주요 BCC 도구**:
| 도구 | 설명 |
|------|------|
| `execsnoop` | 새로운 프로세스 실행 추적 |
| `opensnoop` | 파일 열기 추적 |
| `biolatency` | 블록 I/O 지연 시간 |
| `tcpconnect` | TCP 연결 추적 |
| `tcpaccept` | TCP 수신 연결 추적 |
| `tcpretrans` | TCP 재전송 추적 |
| `runqlat` | CPU 실행 큐 지연 시간 |
| `profile` | CPU 프로파일링 |
| `funccount` | 함수 호출 횟수 |
| `trace` | 범용 함수 추적 |
```bash
# 사용 예제
sudo execsnoop-bpfcc # 프로세스 실행 추적
sudo tcpconnect-bpfcc # TCP 연결 추적
sudo biolatency-bpfcc # 디스크 I/O 지연 시간
sudo profile-bpfcc -F 99 10 # 10초간 CPU 프로파일링
```
### 4.4 libbpf와 CO-RE
libbpf는 eBPF 프로그램 로딩을 위한 C 라이브러리이며, CO-RE(Compile Once, Run Everywhere)를 지원합니다.
**CO-RE의 장점**:
- 컴파일된 eBPF 프로그램을 다양한 커널 버전에서 실행
- BTF(BPF Type Format)를 사용한 구조체 재배치
- 커널 헤더 의존성 감소
```c
// Independent tracing program. Generate vmlinux.h from the target kernel's BTF.
#include "vmlinux.h"
#include
#include
SEC("tracepoint/syscalls/sys_enter_openat")
int trace_openat(struct trace_event_raw_sys_enter *ctx) {
const char *filename = (const char *)BPF_CORE_READ(ctx, args[1]);
char fname[256];
if (bpf_probe_read_user_str(fname, sizeof(fname), filename) < 0)
return 0;
__u32 tgid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("TGID %u opened: %s", tgid, fname);
return 0;
}
char LICENSE[] SEC("license") = "GPL";
```
**BTF 생성 및 확인**:
```bash
# BTF 지원 확인
ls /sys/kernel/btf/vmlinux
# vmlinux.h 생성 (CO-RE 개발용)
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 프로그램의 BTF 정보 확인
bpftool prog show id --pretty
```
---
## 5. eBPF와 Kubernetes 네트워킹
### 5.1 Cilium: eBPF 기반 CNI
Cilium은 eBPF를 활용한 가장 대표적인 Kubernetes CNI(Container Network Interface)입니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-05-ebpf-fundamentals-5.html)
#### kube-proxy 대체
Cilium은 지원되는 구성에서 kube-proxy를 대체할 수 있습니다. 아래는 새 흐름의 백엔드 선택을 단순화한 그림이며 기존 흐름은 연결 추적을 사용할 수 있습니다. 실제 라우팅/터널링/NAT는 데이터 경로 설정에 따릅니다.
**기존 kube-proxy (iptables 모드)**:
```
패킷 → Netfilter → iptables 규칙 평가 → DNAT → 라우팅
```
**Cilium eBPF 모드**:
```
새 흐름 → eBPF 백엔드 조회 → 구성된 라우팅/터널링/NAT
```
```bash
# New, isolated self-managed lab only: configure the cluster for the selected
# CNI/proxy mode before bootstrap. Do not delete kube-proxy on a live cluster.
helm repo add cilium https://helm.cilium.io
helm repo update cilium
: "${CILIUM_CHART_VERSION:?Select a chart compatible with this Kubernetes/kernel}"
: "${CILIUM_VALUES_FILE:?Provide reviewed IPAM/routing/platform values}"
: "${API_SERVER_IP:?Set a directly reachable API endpoint, not the Service IP}"
: "${API_SERVER_PORT:?Set the API endpoint port}"
helm install cilium cilium/cilium --version "$CILIUM_CHART_VERSION" \
--namespace kube-system -f "$CILIUM_VALUES_FILE" \
--set kubeProxyReplacement=true \
--set k8sServiceHost="$API_SERVER_IP" --set k8sServicePort="$API_SERVER_PORT"
cilium status --wait
# Existing clusters require the Cilium migration procedure and a tested rollback plan.
```
#### 네트워크 정책
Cilium은 L3/L4에 eBPF를 사용하며 HTTP/L7 정책에는 Envoy 등 지원되는 프록시 처리가 필요합니다. DNS 관찰에는 DNS 프록시가 사용됩니다. Hubble의 HTTP/DNS 레코드는 해당 관찰 설정이 필요합니다.
```yaml
# Cilium 네트워크 정책 예제
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-http-only
spec:
endpointSelector:
matchLabels:
app: web
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: GET
path: "/api/.*"
```
#### 로드 밸런싱
```yaml
# Cilium LoadBalancer 서비스 예제
apiVersion: v1
kind: Service
metadata:
name: my-service
annotations:
lbipam.cilium.io/ips: "192.168.1.100"
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
```
위 요청 IP는 관리자가 소유한 CiliumLoadBalancerIPPool에 포함되어야 합니다. LB IPAM은 주소 할당만 담당하며 외부 도달성에는 BGP/L2 광고 또는 별도 로드 밸런서 구성이 필요합니다.
### 5.2 Calico eBPF 모드
Calico는 eBPF 데이터플레인을 지원합니다. 아래 패치는 호환되는 기존 Calico Operator 설치를 전제로 한 전환 절차의 일부입니다. 직접 API 접근을 구성하고 라우팅/복구를 검증한 뒤 Service 프록시를 변경합니다.
```bash
# Calico eBPF 모드 활성화
kubectl patch installation.operator.tigera.io default --type merge -p '{"spec":{"calicoNetwork":{"linuxDataplane":"BPF"}}}'
```
**Calico eBPF 모드 특징**:
- 소스 IP 보존
- 직접 서버 리턴 (DSR) 지원
- 호스트 엔드포인트 정책
- 별도로 구성하고 지원되는 경우 WireGuard 암호화; eBPF 선택만으로 자동 활성화되지 않음
### 5.3 성능 비교: iptables vs eBPF
| 측면 | iptables | eBPF |
|------|----------|------|
| **확장성** | O(n) - 서비스 수에 비례 | 해시 조회 평균 O(1); 맵 종류에 따라 다름 |
| **지연 시간** | 규칙 구조/워크로드에 따라 다름 | 맵 종류/워크로드/데이터 경로에 따라 다름 |
| **CPU 사용량** | 워크로드/설정에 따라 다름 | 워크로드/설정에 따라 다름 |
| **업데이트** | 현대 kube-proxy는 변경된 Service/엔드포인트 규칙 갱신 가능 | 구현에 따른 맵 갱신 |
| **관찰성** | 제한적 | Hubble 통합 |
| **메모리** | 규칙/엔드포인트/연결 추적 상태 | 맵/엔드포인트/연결 추적 상태 |
**벤치마크 결과** (1000개 서비스 기준):
원문에서 제시한 아래 수치는 테스트 출처, 하드웨어, 커널/CNI 버전 및 측정 방법이 제공되지 않았습니다. 재실행하지 않았으며 현재 성능이나 일반적 개선율로 사용할 수 없습니다. 비교를 재현하려면 원본 방법과 환경이 필요합니다.
```
| 지표 | iptables | eBPF | 개선율 |
|------------------|-------------|-----------|----------|
| 연결 설정 시간 | 2.5ms | 0.3ms | 8.3x |
| CPU 사용량 | 15% | 3% | 5x |
| 메모리 사용량 | 256MB | 32MB | 8x |
| 초당 연결 수 | 50,000 | 250,000 | 5x |
```
```bash
# Cilium 상태 확인
cilium status
# eBPF 맵 확인
kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg bpf lb list
kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg bpf ct list global
# 네트워크 정책 상태
kubectl get ciliumnetworkpolicies,ciliumclusterwidenetworkpolicies -A
```
---
## 6. eBPF 기반 관찰성 (Observability)
eBPF는 시스템과 애플리케이션의 동작을 심층적으로 관찰할 수 있게 해줍니다. 기존의 에이전트 기반 모니터링과 달리, eBPF는 커널 레벨에서 데이터를 수집하여 더 낮은 오버헤드로 더 풍부한 정보를 제공합니다.
### 6.1 Hubble: Cilium 네트워크 관찰성
Hubble은 Cilium 네트워크 관찰성을 제공합니다. 호환되는 Hubble CLI와 Relay를 준비하고 CLI 예제 전에 port-forward를 연결합니다. L7 관찰에는 해당 프록시 구성이 필요합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-05-ebpf-fundamentals-6.html)
```bash
# Use the installed, reviewed chart version; this is not a chart-version upgrade.
: "${CILIUM_CHART_VERSION:?Set the installed compatible chart version}"
# Hubble 설치
helm upgrade cilium cilium/cilium --version "$CILIUM_CHART_VERSION" \
--namespace kube-system \
--reuse-values \
--set hubble.enabled=true \
--set hubble.relay.enabled=true \
--set hubble.ui.enabled=true
# 먼저 별도 터미널에서 cilium hubble port-forward를 실행합니다.
# Hubble CLI 사용
hubble observe --pod my-pod
hubble observe --namespace default
hubble observe --protocol http
hubble observe --verdict DROPPED
# 특정 서비스 간 트래픽 관찰
hubble observe --from-pod default/frontend --to-pod default/backend
# 네트워크 플로우 실시간 모니터링
hubble observe -f --type trace
# 서비스 맵 생성
# Service maps are provided by Hubble UI; use the UI port-forward below.
```
**Hubble UI 접속**:
```bash
# 포트 포워딩
kubectl port-forward -n kube-system svc/hubble-ui 12000:80
# 브라우저에서 http://localhost:12000 접속
```
### 6.2 Pixie: 자동 계측 관찰성
Pixie는 eBPF를 사용하여 애플리케이션 코드 수정 없이 자동으로 텔레메트리를 수집합니다.
**Pixie 특징**:
- 자동 프로토콜 파싱 (HTTP, gRPC, MySQL, PostgreSQL, Kafka 등)
- 서비스 맵 자동 생성
- 분산 추적
- CPU 프로파일링
- 동적 로깅
```bash
# Pixie 설치
px deploy
# Pixie CLI 쿼리 예제
# HTTP 요청 지연 시간
px run px/http_data
# 서비스 간 트래픽
px run px/service_stats
# 느린 요청 분석
px run px/slow_http_requests --help
# Use the parameters advertised by the installed script bundle.
# Pod 리소스 사용량
px run px/pods
```
**PxL (Pixie Query Language) 예제**:
```python
# 느린 HTTP 요청 찾기
import px
df = px.DataFrame(table='http_events', start_time='-5m')
df.namespace = df.ctx['namespace']
df.pod = df.ctx['pod']
df = df[df.latency > 100000000] # 100ms 이상
df = df.groupby(['namespace', 'pod', 'req_path']).agg(
count=('latency', px.count),
avg_latency=('latency', px.mean),
latency_quantiles=('latency', px.quantiles)
)
df.p99_latency_ns = px.pluck_float64(df.latency_quantiles, 'p99')
px.display(df)
```
### 6.3 Coroot: "No-Code" 모니터링
Coroot는 eBPF를 사용하여 에이전트/저장소/권한/데이터 소스를 구성한 후 지원하는 애플리케이션을 자동 관찰합니다.
```bash
# Helm으로 Coroot 설치
helm repo add coroot https://coroot.github.io/helm-charts
# The old coroot/coroot chart is deprecated; use the operator and CE resource chart.
: "${COROOT_OPERATOR_VERSION:?Select a reviewed operator chart version}"
: "${COROOT_CE_VERSION:?Select a compatible CE chart version}"
helm install coroot-operator coroot/coroot-operator -n coroot --create-namespace \
--version "$COROOT_OPERATOR_VERSION"
helm install coroot coroot/coroot-ce -n coroot --version "$COROOT_CE_VERSION"
```
**Coroot 기능**:
- 서비스 자동 발견
- 의존성 맵 자동 생성
- SLO 모니터링
- 이상 탐지
- 근본 원인 분석
### 6.4 Kepler: 에너지 소비 모니터링
Kepler의 초기 버전은 eBPF를 사용했지만 **0.10.0부터 전면 재작성**되어 호스트 /proc·/sys 읽기와 RAPL/powercap 및 CPU 사용량 기반 전력 배분을 사용합니다. CAP_BPF가 더 이상 필요하지 않습니다. 따라서 현재 Kepler를 eBPF 계측의 필수 사례로 설명하면 부정확합니다. 0.9 이하 코드는 frozen legacy이며 현재 메트릭/배포 방법과 구분합니다.
하드웨어/VM에서 전력 센서가 제공되는지 먼저 확인합니다. 컨테이너/Pod 값은 직접 전력계를 달아 측정한 값이 아니라 노드 에너지의 추정 배분이며 중첩 RAPL zone을 합산하면 중복 계산할 수 있습니다. GPU/HWMon/플랫폼 전력 지원은 버전별 실험 기능 범위를 확인합니다.
```bash
: "${KEPLER_CHART_VERSION:?Select a reviewed current Kepler chart}"
helm install kepler oci://quay.io/sustainable_computing_io/charts/kepler \
--version "$KEPLER_CHART_VERSION" --namespace kepler --create-namespace
kubectl get pods -n kepler
# Run port-forward in a separate terminal; then query metrics from this machine.
kubectl port-forward -n kepler service/kepler 28282:28282
# curl --fail http://localhost:28282/metrics | grep kepler_node_cpu_watts
```
현재 CPU 메트릭 예: `kepler_node_cpu_joules_total`, `kepler_container_cpu_joules_total`, `kepler_pod_cpu_watts`. 실제 수집 가능 범위와 zone 레이블을 확인합니다.
### 6.5 기존 에이전트 vs eBPF 계측 비교
5–15%와 <1%는 원문의 출처 없는 수치를 보존한 것입니다. 검증한 오버헤드 범위가 아니며 eBPF도 사용자 공간 에이전트/버퍼/프로토콜 파서가 필요합니다. 모든 기존 에이전트가 코드 수정을 요구하거나 모든 eBPF 도구가 전체 시스템을 완전히 관찰하는 것은 아닙니다.
| 측면 | 기존 에이전트 | eBPF 계측 |
|------|-------------|-----------|
| **오버헤드** | 높음 (5-15%) | 낮음 (<1%) |
| **코드 수정** | SDK/에이전트 모델에 따라 다름 | 지원 데이터 소스에서는 대체로 불필요 |
| **커버리지** | 계측/에이전트에 따라 다름 | 지원 훅/프로토콜/가시성 범위; 자동으로 전체를 보장하지 않음 |
| **배포** | 앱/노드/수집기 형태에 따라 다름 | 보통 노드 에이전트; 앱 호환성은 여전히 필요 |
| **권한** | 에이전트별로 다름 | 프로그램/훅별 capability와 호스트 접근 필요 |
| **데이터 깊이** | 앱/호스트 계측에 따라 다름 | 커널 및 지원되는 사용자 공간 probe |
| **프로토콜 지원** | 도구별로 다름 | 지원되는 파서/라이브러리/가시성에서만 자동 파싱 |

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-05-ebpf-fundamentals-7.html)
---
## 7. eBPF 기반 보안
### 7.1 Tetragon: 런타임 보안
Tetragon은 Cilium 프로젝트에서 제공하는 eBPF 기반 런타임 보안 솔루션입니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-05-ebpf-fundamentals-8.html)
```bash
# Tetragon 설치
helm repo add cilium https://helm.cilium.io
: "${TETRAGON_CHART_VERSION:?Select a compatible reviewed chart version}"
helm install tetragon cilium/tetragon -n kube-system --version "$TETRAGON_CHART_VERSION"
# 이벤트 관찰
kubectl logs -n kube-system -l app.kubernetes.io/name=tetragon -c export-stdout -f | tetra getevents -o compact
```
ebpf-lab 네임스페이스와 app=ebpf-demo 테스트 Pod를 준비합니다. 아래 정책은 호스트 전체에 SIGKILL을 적용하지 않는 관찰 전용 Post 예제입니다. 예방적 차단은 지원되는 LSM/Override 동작을 별도 테스트해야 합니다.
**TracingPolicy 예제**:
```yaml
apiVersion: cilium.io/v1alpha1
kind: TracingPolicyNamespaced
metadata:
name: sensitive-file-access
namespace: ebpf-lab
spec:
kprobes:
- call: security_file_open
syscall: false
args:
- index: 0
type: file
selectors:
- matchArgs:
- index: 0
operator: Prefix
values:
- /etc/shadow
- /etc/passwd
- /etc/sudoers
matchActions:
- action: Post
podSelector:
matchLabels:
app: ebpf-demo
```
```yaml
apiVersion: cilium.io/v1alpha1
kind: TracingPolicyNamespaced
metadata:
name: observe-outbound
namespace: ebpf-lab
spec:
kprobes:
- call: tcp_connect
syscall: false
args:
- index: 0
type: sock
selectors:
- matchArgs:
- index: 0
operator: NotDAddr
values:
- 10.0.0.0/8
matchActions:
- action: Post
podSelector:
matchLabels:
app: ebpf-demo
```
### 7.2 Falco: eBPF 기반 이상 탐지
Falco는 CNCF 프로젝트로, eBPF를 사용하여 런타임 이상 동작을 탐지합니다.
```bash
# Falco 설치 (eBPF 드라이버)
helm repo add falcosecurity https://falcosecurity.github.io/charts
# Save the following Falco rule examples as ./ebpf-lab-rules.yaml before installation.
: "${FALCO_CHART_VERSION:?Select a compatible reviewed chart version}"
helm install falco falcosecurity/falco --version "$FALCO_CHART_VERSION" \
--namespace falco --create-namespace \
--set driver.kind=modern_ebpf \
--set-file 'customRules.ebpf-lab-rules\.yaml=./ebpf-lab-rules.yaml'
```
**Falco 규칙 예제**:
```yaml
# /etc/shadow 읽기 탐지
- rule: eBPF lab read sensitive file
desc: Detect reading of sensitive files
condition: >
open_read and
fd.name in (/etc/shadow, /etc/sudoers) and
not proc.name in (systemd, sudo, login)
output: >
Sensitive file opened (file=%fd.name user=%user.name
process=%proc.name container=%container.name)
priority: WARNING
# 컨테이너에서 셸 실행 탐지
- rule: eBPF lab shell in container
desc: Detect shell execution in container
condition: >
spawned_process and
container and
proc.name in (bash, sh, zsh, dash) and
proc.pname != containerd-shim
output: >
Shell spawned in container (container=%container.name
shell=%proc.name parent=%proc.pname)
priority: NOTICE
# 권한 상승 탐지
- rule: eBPF lab privilege escalation
desc: Detect privilege escalation attempts
condition: >
spawned_process and
proc.name in (sudo, su, doas) and
container
output: >
Privilege escalation attempt (user=%user.name
command=%proc.cmdline container=%container.name)
priority: WARNING
```
### 7.3 seccomp-bpf: 시스템 콜 필터링
seccomp 필터는 일반 eBPF 프로그램 헬퍼/맵이 아닌 classic BPF 사용자 API를 사용합니다. 컨테이너 런타임이 OCI JSON 프로필을 해석하여 syscall 필터를 구성합니다.
```yaml
# Kubernetes Pod에서 seccomp 프로필 적용
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
seccompProfile:
type: RuntimeDefault # 또는 Localhost
containers:
- name: app
image: nginx:1.30.4
```
**커스텀 seccomp 프로필**:
아래는 x86-64 최소 예제의 **형식 설명**이며 NGINX/일반 애플리케이션에 적용할 수 있는 프로필이 아닙니다. 기본 RuntimeDefault를 사용하고, custom allowlist는 실제 아키텍처/런타임/워크로드의 syscall을 관찰하여 회귀 테스트한 후 배포합니다. 광범위한 mount/reboot/module/BPF 허용 목록을 안전한 기본값으로 사용하지 않습니다.
```json
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64"
],
"syscalls": [
{
"names": [
"read",
"write",
"exit",
"exit_group",
"rt_sigreturn"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
```
### 7.4 LSM BPF: 동적 보안 정책
LSM BPF는 Linux Security Module과 eBPF를 결합하여 동적으로 보안 정책을 적용합니다.
```c
// LSM BPF 예제: 실행 파일 제한
SEC("lsm/bprm_check_security")
int BPF_PROG(restrict_exec, struct linux_binprm *bprm, int ret) {
if (ret != 0)
return ret;
char filename[256];
if (bpf_probe_read_kernel_str(filename, sizeof(filename), bprm->filename) < 0)
return 0; // Demo fails open on read error; define a real policy explicitly.
// /tmp에서 실행 차단
if (bpf_strncmp(filename, 5, "/tmp/") == 0)
return -EPERM;
return 0;
}
// LSM BPF 예제: 네트워크 소켓 제한
SEC("lsm/socket_connect")
int BPF_PROG(restrict_connect, struct socket *sock, struct sockaddr *address, int addrlen, int ret) {
if (ret != 0)
return ret;
if (addrlen < sizeof(struct sockaddr_in) || address->sa_family != AF_INET)
return 0; // This example handles IPv4 only.
struct sockaddr_in *addr = (struct sockaddr_in *)address;
// 특정 포트 연결 차단
if (bpf_ntohs(addr->sin_port) == 6666)
return -EACCES;
return 0;
}
```
---
## 8. eBPF 실전 활용 예제
### 8.1 bpftrace로 시스템 성능 분석하기
**TCP 연결 추적**:
```bash
# TCP 연결 추적
sudo bpftrace -e '
tracepoint:sock:inet_sock_set_state /args.protocol == 6 && args.newstate == 1/ {
if (args.family == 2) {
printf("IPv4 %s:%d -> %s:%d established\n", ntop(args.saddr), args.sport, ntop(args.daddr), args.dport);
} else if (args.family == 10) {
printf("IPv6 %s:%d -> %s:%d established\n", ntop(args.saddr_v6), args.sport, ntop(args.daddr_v6), args.dport);
}
}'
```
**시스템 콜 지연 시간 분석**:
```bash
# read 시스템 콜 지연 시간 히스토그램
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_read { @start[tid] = nsecs; }
tracepoint:syscalls:sys_exit_read /@start[tid]/ {
@latency = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
```
**디스크 I/O 분석**:
```bash
# 블록 I/O 요청 추적
sudo bpftrace -e '
tracepoint:block:block_rq_issue {
printf("%s %s %d\n",
comm,
str(args.rwbs),
args.nr_sector / 2);
}'
# I/O 지연 시간 히스토그램
sudo biolatency-bpfcc 1 10
```
### 8.2 Cilium Hubble로 네트워크 흐름 관찰
```bash
# 실시간 네트워크 플로우 관찰
hubble observe -f
# 특정 네임스페이스 트래픽
hubble observe --namespace production
# HTTP 트래픽만 필터링
hubble observe --protocol http
# 드롭된 패킷 분석
hubble observe --verdict DROPPED
# DNS 쿼리 추적
hubble observe --protocol dns
# 특정 Pod 간 트래픽
hubble observe --from-pod default/frontend --to-pod default/backend
# JSON 출력으로 상세 분석
hubble observe --namespace default -o json | jq '.flow.destination.pod_name'
# 보관된 흐름 관찰 이벤트 수이며 고유 연결 수나 전체 트래픽 통계가 아닙니다.
# Relay는 Hubble 인스턴스별로 지정 수만큼 반환합니다.
hubble observe --namespace default --last 1000 -o jsonpb | \
jq -r '.flow | "\(.source.pod_name // .source.identity) -> \(.destination.pod_name // .destination.identity)"' | \
sort | uniq -c | sort -rn | head -20
```
### 8.3 Tetragon으로 프로세스 보안 모니터링
```bash
# Tetragon 이벤트 실시간 모니터링
kubectl logs -n kube-system -l app.kubernetes.io/name=tetragon -c export-stdout -f | \
tetra getevents -o compact
# 프로세스 실행 이벤트만 필터링
kubectl logs -n kube-system -l app.kubernetes.io/name=tetragon -c export-stdout -f | \
tetra getevents -o compact --event-types PROCESS_EXEC
# 특정 네임스페이스 이벤트
kubectl logs -n kube-system -l app.kubernetes.io/name=tetragon -c export-stdout -f | \
tetra getevents -o json | jq 'select(.process_exec.process.pod.namespace == "default")'
```
**파일 접근 모니터링 정책**:
```yaml
apiVersion: cilium.io/v1alpha1
kind: TracingPolicyNamespaced
metadata:
name: file-access-monitor
namespace: ebpf-lab
spec:
kprobes:
- call: security_file_open
syscall: false
return: false
args:
- index: 0
type: file
selectors:
- matchArgs:
- index: 0
operator: Prefix
values:
- /etc/
- /var/run/secrets/
matchActions:
- action: Post
podSelector:
matchLabels:
app: ebpf-demo
```
### 8.4 eBPF를 사용한 지연 시간 분석
**함수, 연결 수립 및 이름 해석 지연 시간**:
```bash
# libc read() 함수 실행 시간이며 HTTP 요청 지연 시간 메트릭이 아님
sudo funclatency-bpfcc 'c:read' -i 1
# TCP 핸드셰이크 지연 시간
sudo tcpconnlat-bpfcc # Active TCP connection establishment latency
# DNS 조회 지연 시간
sudo gethostlatency-bpfcc # libc name-resolution latency; includes cache/NSS work
```
아래는 x86-64 glibc 경로 예시입니다. 대상 프로세스/라이브러리 경로를 먼저 확인하며 컨테이너 마운트 네임스페이스는 다를 수 있습니다. malloc/tcp_sendmsg 실행 시간은 함수 지연이며 전체 요청 지연이 아닙니다.
**애플리케이션 성능 분석 스크립트**:
```bash
#!/bin/bash
# app-latency-analysis.bt
sudo bpftrace -e '
BEGIN {
printf("Tracing application latency... Hit Ctrl-C to end.\n");
}
uprobe:/usr/lib/x86_64-linux-gnu/libc.so.6:malloc {
@malloc_start[tid] = nsecs;
}
uretprobe:/usr/lib/x86_64-linux-gnu/libc.so.6:malloc /@malloc_start[tid]/ {
@malloc_ns = hist(nsecs - @malloc_start[tid]);
delete(@malloc_start[tid]);
}
kprobe:tcp_sendmsg {
@send_start[tid] = nsecs;
}
kretprobe:tcp_sendmsg /@send_start[tid]/ {
@tcp_send_ns = hist(nsecs - @send_start[tid]);
delete(@send_start[tid]);
}
END {
printf("\n=== Malloc Latency ===\n");
print(@malloc_ns);
printf("\n=== TCP Send Latency ===\n");
print(@tcp_send_ns);
}
'
```
---
## 9. eBPF 제한 사항과 주의점
### 9.1 기술적 제한 사항
| 제한 사항 | 값 | 설명 |
|----------|-----|------|
| **스택 크기** | 512 bytes | 로컬 변수 저장 공간 제한 |
| **명령어 제한** | 권한/커널별 상이 | 프로그램 길이와 검증 중 처리 명령 수 한도는 별개이며 upstream 복잡도 한도는100만 |
| **최대 중첩 호출** | 8 레벨 | BPF-to-BPF 함수 호출 깊이 |
| **맵 항목 수** | 맵 유형별 상이 | 메모리 제한에 따름 |
| **프로그램 크기** | 커널/검증기/JIT 한도 | 맵 종류로 결정되지 않음 |
**스택 크기 제한 우회**:
```c
// 잘못된 예: 스택 크기 초과
int bad_function(void *ctx) {
volatile char buffer[1024] = {}; // 스택 크기 초과!
buffer[0] = 1;
return buffer[1023];
}
// 올바른 예: 맵 사용
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, char[1024]);
} buffer_map SEC(".maps");
int good_function(void *ctx) {
__u32 key = 0;
char *buffer = bpf_map_lookup_elem(&buffer_map, &key);
if (!buffer)
return 0;
// buffer 사용
return 0;
}
```
### 9.2 루프 제한
eBPF 검증기는 프로그램 종료를 보장하기 위해 루프를 제한합니다.
```c
// n의 작은 상한을 증명할 수 없다면 검증 복잡도 문제가 될 수 있음.
for (int i = 0; i < n; i++) { // 런타임 값도 증명 가능한 상한이 있을 수 있음
// ...
}
// 검증기가 허용: 제한된 루프 (커널 5.3+)
#pragma clang loop unroll(disable)
for (int i = 0; i < 100 && i < n; i++) { // 상한 명시
// ...
}
// 검증기가 허용: 컴파일 타임 언롤링
#pragma unroll
for (int i = 0; i < 10; i++) {
// ...
}
// bpf_loop 헬퍼 사용 (커널 5.17+)
static int callback(u32 index, void *ctx) {
// 반복 작업
return 0;
}
int main_prog(void *ctx) {
bpf_loop(1000, callback, NULL, 0);
return 0;
}
```
### 9.3 커널 버전 호환성
| 기능 | 최소 커널 버전 |
|------|--------------|
| 기본 eBPF | 3.18 |
| XDP | 4.8 |
| BTF | 4.18 |
| CO-RE | BTF와 호환 libbpf/기능 필요; 단일 최소 버전으로 보장 불가 |
| BPF 링 버퍼 | 5.8 |
| BPF 루프 | 5.3 |
| LSM BPF | 5.7 |
| bpf_loop 헬퍼 | 5.17 |
```bash
# 커널 버전 확인
uname -r
# eBPF 기능 지원 확인
sudo bpftool feature probe kernel
# BTF 지원 확인
ls /sys/kernel/btf/vmlinux
```
### 9.4 디버깅의 어려움
eBPF 프로그램 디버깅은 전통적인 방법과 다릅니다:
**디버깅 방법**:
```c
// bpf_printk (디버그용, 성능 영향)
bpf_printk("value = %d\n", value);
```
```bash
# tracefs 마운트/위치는 배포판별로 확인합니다.
sudo cat /sys/kernel/tracing/trace_pipe
```
```bash
# 검증기 로그 확인 (로드 실패 시)
sudo bpftool prog load my_prog.o /sys/fs/bpf/my_prog -d
# 프로그램 통계 확인
sudo bpftool -j prog show id | jq '.run_time_ns, .run_cnt'
# Runtime statistics require kernel.bpf_stats_enabled or a BPF stats FD; disabled by default and adds overhead.
# 맵 내용 덤프
sudo bpftool map dump id
```
### 9.5 권한 요구사항
| 권한 | 용도 |
|------|------|
| `CAP_BPF` | eBPF 프로그램 로드 (커널 5.8+) |
| `CAP_SYS_ADMIN` | 전통적인 eBPF 권한 |
| `CAP_PERFMON` | 성능 모니터링 이벤트 연결 |
| `CAP_NET_ADMIN` | XDP/TC 프로그램 연결 |
```bash
# 권한 확인
capsh --print
# 특정 권한으로 프로그램 실행
sudo setcap cap_bpf,cap_perfmon+ep ./my_bpf_loader
```
아래 Pod는 capability 필드 예시이며 실행 검증한 완성 에이전트가 아닙니다. 커널/BTF/프로그램 유형, seccomp의 bpf/perf_event_open 허용, LSM/lockdown, hostPath 마운트와 소유권, PSS 및 필요한 RBAC를 따로 확인합니다. capability만 추가해도 모든 프로그램이 로드되는 것은 아닙니다.
**Kubernetes에서의 권한 설정**:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: ebpf-pod
spec:
containers:
- name: ebpf-container
image: my-ebpf-app
securityContext:
capabilities:
add:
- BPF
- PERFMON
- NET_ADMIN
privileged: false
volumeMounts:
- name: bpf-maps
mountPath: /sys/fs/bpf
- name: debug
mountPath: /sys/kernel/debug
volumes:
- name: bpf-maps
hostPath:
path: /sys/fs/bpf
- name: debug
hostPath:
path: /sys/kernel/debug
```
### 9.6 보안 고려사항
eBPF는 강력한 도구이지만 보안 위험도 존재합니다:
- **정보 유출**: 민감한 데이터에 접근 가능
- **DoS 공격**: 성능 저하 유발 가능
- **권한 상승**: 잘못된 설정 시 취약점 발생 가능
**보안 모범 사례**:
```bash
# Inspect first. 0 enables unprivileged bpf(); 1 disables until reboot; 2 disables reversibly.
sysctl kernel.unprivileged_bpf_disabled
# On a kernel supporting value 2, disable only if currently enabled.
if [ "$(sysctl -n kernel.unprivileged_bpf_disabled)" = 0 ]; then
sudo sysctl -w kernel.unprivileged_bpf_disabled=2
fi
# Inspect the real JIT-hardening setting; choose changes through host configuration management.
sysctl net.core.bpf_jit_harden
```
---
## 10. 다음 단계
### 10.1 관련 퀴즈
이 문서의 내용을 확인하려면 다음 퀴즈를 풀어보세요:
- [eBPF 기초 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/basics/05-ebpf-fundamentals-quiz)
### 10.2 심화 학습 자료
**공식 문서 및 리소스**:
- [eBPF.io](https://ebpf.io) - 공식 eBPF 문서
- [Cilium Documentation](https://docs.cilium.io) - Cilium 공식 문서
- [BPF Performance Tools](https://www.brendangregg.com/bpf-performance-tools-book.html) - Brendan Gregg의 BPF 성능 도구 책
**실습 환경**:
- [eBPF Tutorial](https://github.com/lizrice/learning-ebpf) - Liz Rice의 eBPF 튜토리얼
- [BCC Tutorial](https://github.com/iovisor/bcc/blob/master/docs/tutorial.md) - BCC 공식 튜토리얼
- [bpftrace Tutorial](https://github.com/iovisor/bpftrace/blob/master/docs/tutorial_one_liners.md) - bpftrace 원라이너 튜토리얼
**커뮤니티**:
- [eBPF Summit](https://ebpf.io/events/?conference=eBPF%20Summit) - 연례 eBPF 컨퍼런스
- [Cilium Slack](https://slack.cilium.io/) - Cilium 커뮤니티
### 10.3 관련 문서
이 문서와 관련된 심화 내용은 다음 문서를 참고하세요:
| 주제 | 문서 링크 | 설명 |
|------|----------|------|
| Cilium 소개 | [Cilium 개요](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/01-introduction.md) | eBPF 기반 CNI 소개 |
| eBPF 심층 분석 | [eBPF 기술 심층 분석](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/02-ebpf.md) | 고급 eBPF 기술 |
| 네트워킹 | [Cilium 네트워킹](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/03-networking.md) | eBPF 네트워킹 구현 |
| 보안 | [Cilium 보안](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/06-security-visibility.md) | eBPF 기반 보안 |
| Kubernetes 네트워킹 | [서비스와 네트워킹](https://www.atomai.click/kubernetes-docs/llms/ko/core/03-services-networking.md) | 기본 네트워킹 개념 |
### 10.4 실습 체크리스트
eBPF 학습을 위한 실습 체크리스트:
```
[ ] bpftool을 사용하여 로드된 eBPF 프로그램 확인
[ ] bpftrace로 시스템 콜 추적 실행
[ ] BCC 도구로 네트워크 트래픽 분석
[ ] Cilium 설치 및 Hubble로 네트워크 관찰
[ ] Tetragon으로 보안 이벤트 모니터링
[ ] 간단한 XDP 프로그램 작성 및 로드
```
---
## 요약
eBPF는 Linux 커널의 동작을 안전하게 확장하고 관찰할 수 있게 해주는 혁신적인 기술입니다. 이 문서에서 다룬 핵심 내용을 정리하면:
1. **eBPF 기본 개념**: 커널 내에서 안전하게 실행되는 샌드박스 프로그램
2. **아키텍처**: 검증기, JIT 컴파일러, 맵, 헬퍼 함수로 구성
3. **프로그램 유형**: XDP, TC, Kprobes, Tracepoints, LSM BPF 등
4. **개발 도구**: bpftool, bpftrace, BCC, libbpf
5. **Kubernetes 활용**: Cilium, Calico eBPF 모드로 고성능 네트워킹
6. **관찰성**: Hubble, Pixie, Coroot를 통한 깊은 시스템 관찰
7. **보안**: Tetragon, Falco, seccomp-bpf를 통한 런타임 보안
8. **제한 사항**: 스택 크기, 루프, 커널 버전 호환성 고려 필요
eBPF는 클라우드 네이티브 환경에서 네트워킹, 보안, 관찰성의 미래를 이끌어가는 핵심 기술입니다.
> Falco 규칙은 기본 ruleset의 open_read/open_write/spawned_process/container 매크로를 먼저 로드해야 합니다. 추가 규칙 파일을 배포하는 방법은 설치한 Helm 차트의 customRules/falco.rules_files 설정으로 확인합니다. Falco는 탐지/알림 엔진이며 규칙만으로 접근을 차단하지 않습니다. container/Kubernetes 메타데이터는 조회 지연으로 없을 수 있고 정상적인 서비스 계정 토큰 읽기도 탐지되므로 허용 조건을 테스트합니다.
## 검증 참고 자료
- https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html
- https://www.kernel.org/doc/html/latest/admin-guide/sysctl/net.html
- https://github.com/torvalds/linux/blob/master/include/linux/bpf.h
- https://github.com/torvalds/linux/blob/master/include/linux/filter.h
- https://github.com/torvalds/linux/blob/master/include/uapi/linux/bpf.h
- https://github.com/torvalds/linux/blob/master/kernel/bpf/syscall.c
- https://docs.kernel.org/bpf/prog_lsm.html
- https://docs.kernel.org/userspace-api/seccomp_filter.html
- https://github.com/torvalds/linux/blob/master/include/trace/events/sock.h
- https://github.com/bpftrace/bpftrace/blob/v0.27.0/docs/language.md
- https://github.com/bpftrace/bpftrace/blob/v0.27.0/docs/stdlib.md
- https://packages.debian.org/trixie/arm64/bpfcc-tools/filelist
- https://github.com/iovisor/bcc/blob/master/tools/tcpconnlat.py
- https://github.com/iovisor/bcc/blob/master/tools/gethostlatency.py
- https://github.com/libbpf/bpftool/blob/main/docs/bpftool-map.rst
- https://github.com/cilium/cilium/blob/v1.20.1/Documentation/network/kubernetes/kubeproxy-free.rst
- https://github.com/cilium/cilium/blob/v1.20.1/Documentation/network/lb-ipam.rst
- https://github.com/cilium/cilium/blob/v1.20.1/install/kubernetes/cilium/values.yaml
- https://github.com/cilium/cilium/blob/v1.20.1/hubble/cmd/observe/observe.go
- https://github.com/cilium/cilium/blob/v1.20.1/hubble/pkg/printer/printer_test.go
- https://github.com/cilium/tetragon/blob/main/docs/content/en/docs/concepts/enforcement/_index.md
- https://github.com/cilium/tetragon/blob/main/docs/content/en/docs/concepts/tracing-policy/selectors.md
- https://github.com/cilium/tetragon/blob/main/pkg/k8s/apis/cilium.io/v1alpha1/tracing_policy_types.go
- https://github.com/cilium/tetragon/blob/main/cmd/tetra/getevents/getevents.go
- https://github.com/cilium/tetragon/blob/main/examples/tracingpolicy/lsm_file_open.yaml
- https://github.com/cilium/tetragon/blob/main/install/kubernetes/tetragon/crds-yaml/cilium.io_tracingpoliciesnamespaced.yaml
- https://github.com/sustainable-computing-io/kepler/blob/main/README.md
- https://github.com/sustainable-computing-io/kepler/blob/main/docs/user/metrics.md
- https://github.com/coroot/helm-charts/blob/main/charts/coroot/Chart.yaml
- https://github.com/coroot/helm-charts/blob/main/charts/operator/Chart.yaml
- https://github.com/coroot/helm-charts/blob/main/charts/coroot-ce/Chart.yaml
- https://docs.px.dev/reference/pxl/udf/quantiles/
- https://github.com/pixie-io/pixie/blob/main/src/pixie_cli/pkg/cmd/run.go
- https://github.com/pixie-io/pixie/blob/main/src/pxl_scripts/px/http_data/data.pxl
- https://falco.org/docs/reference/rules/supported-fields/
- https://github.com/falcosecurity/charts/blob/master/charts/falco/values.yaml
- https://github.com/falcosecurity/rules/blob/main/rules/falco_rules.yaml
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/kernel/
----------------------------------------
# Linux 커널 개요
> **지원 버전**: Linux 6.1 / 6.12 / 6.18 (Amazon Linux 2023), Kubernetes 1.33+ (Amazon EKS)
> **마지막 업데이트**: 2026년 9월 12일
## 이 섹션에서 다루는 것
- 컨테이너와 Kubernetes가 실제로 무엇에 올라타 있는가 — namespace, cgroup, netfilter, conntrack의 커널 기능들
- 패킷이 Pod에서 나가 NIC에 닿기까지 커널 안에서 지나는 경로와, 그 경로의 각 지점에서 무엇을 관측하고 조정할 수 있는가
- EKS 노드에서 커널 파라미터가 워크로드 성능·안정성에 미치는 영향과, 무엇을 건드려야 하고 무엇을 두어야 하는가
## 왜 이 섹션이 필요한가
Kubernetes 문서는 대부분 **선언적 API 위에서** 설명됩니다. Pod를 만들면 컨테이너가 뜨고, Service를 만들면 트래픽이 분산되고, resource limit을 걸면 컨테이너가 그만큼만 씁니다.
그런데 장애를 진단할 때 필요한 지식은 그 아래 계층에 있습니다.
| 현장에서 만나는 증상 | 커널 계층의 실체 |
|---|---|
| "Pod가 OOMKilled인데 컨테이너 메모리는 limit 아래였다" | cgroup v2의 `memory.current`에 page cache가 포함됨. RSS만 보면 안 됨 |
| "노드의 새 연결이 조용히 드롭된다" | 다른 packet drop 원인과 함께 conntrack count/max·insert/drop counter·kernel log 조사 |
| "CPU limit을 걸었더니 p99가 튄다" | CFS/EEVDF throttling. 사용률은 낮은데 주기마다 강제로 멈춤 |
| "같은 노드 Pod 간 통신이 유독 빠르다" | veth 쌍만 지나고 NIC를 거치지 않음 |
| "Service 규칙이 수천 개인데 지연이 늘었다" | iptables 모드 kube-proxy의 선형 룰 평가 |
이런 증상들은 **Kubernetes API 계층에서는 원인이 보이지 않습니다.** 이 섹션은 그 간극을 메우는 것이 목적입니다.
## 대상 독자와 전제
- EKS·Kubernetes 운영 경험이 있고, 리소스 제약과 네트워크 장애를 직접 진단해야 하는 인프라 담당자
- Linux 기본 명령과 프로세스 개념은 알고 있다고 전제합니다
- 커널 소스를 읽거나 모듈을 작성하는 것은 다루지 않습니다. **운영자가 관측하고 조정할 수 있는 범위**에 집중합니다
## 문서 구성
| # | 문서 | 다루는 질문 |
|---|------|------------|
| 1 | [컨테이너를 지탱하는 커널 기능](https://www.atomai.click/kubernetes-docs/llms/ko/kernel/01-container-primitives.md) | 컨테이너는 무엇으로 만들어지는가. cgroup v1과 v2의 차이가 왜 운영에 영향을 주는가 |
| 2 | [커널 네트워킹 스택](https://www.atomai.click/kubernetes-docs/llms/ko/kernel/02-network-stack.md) | 패킷이 socket에서 NIC까지 어떤 경로를 지나는가. 어디에 훅을 걸 수 있는가 |
| 3 | [EKS 노드 커널 튜닝](https://www.atomai.click/kubernetes-docs/llms/ko/kernel/03-eks-node-tuning.md) | 어떤 파라미터를 언제 건드려야 하는가. 기본값을 두는 게 정답인 경우는 언제인가 |
## 이 섹션을 읽는 순서
1번은 2번과 3번의 선행 개념입니다. cgroup과 namespace를 모르면 3번의 튜닝 항목이 왜 그 위치에 있는지 이해되지 않습니다.
네트워크 문제를 진단하러 오셨다면 **2번 → 3번의 네트워크 절**만 읽어도 됩니다. 리소스 제약(OOM, CPU throttling) 문제라면 **1번의 cgroup 절 → 3번의 메모리·CPU 절**이 경로입니다.
## 관련 문서
- [Linux 기초](https://www.atomai.click/kubernetes-docs/llms/ko/basics/01-linux-basics.md) / [Linux 운영 기술](https://www.atomai.click/kubernetes-docs/llms/ko/basics/02-linux-advanced.md) — 명령어와 기본 운영
- [컨테이너 기술](https://www.atomai.click/kubernetes-docs/llms/ko/basics/03-container-technology.md) — 컨테이너 런타임과 이미지 계층
- [eBPF 기초와 실무 활용](https://www.atomai.click/kubernetes-docs/llms/ko/basics/05-ebpf-fundamentals.md) — eBPF 프로그램 타입과 활용
- [네트워크 기초 4부작](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part1.md) — 계층 모델부터 클라우드까지
- [Pod 네트워크 실측 벤치마크](https://www.atomai.click/kubernetes-docs/llms/ko/networking/06-pod-network-benchmark.md) — 이 섹션의 이론에 대응하는 실측값
- [리소스 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/ops/10-resource-optimization.md) — request/limit 설계
- [VPC Lattice 커널 데이터패스](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/vpc-lattice/07-kernel-datapath.md) — link-local 인터셉트의 커널 계층
## 정확성에 대한 안내
커널 기능은 버전에 따라 동작이 바뀌고, 특히 **튜너블의 위치와 이름이 커널 버전 간에 이동합니다**(sysctl → debugfs 등). 이 섹션은 AL2023이 제공하는 커널 계열(6.1 / 6.12 / 6.18)을 기준으로 쓰되, 버전 의존적인 항목은 어느 버전 기준인지 명시했습니다.
공식 문서로 확인되지 않은 항목은 단정하지 않고 `확인 필요` 블록으로 표시했습니다. **운영 클러스터에 파라미터를 적용하기 전에 해당 노드의 커널 버전에서 실제 값을 직접 확인**하시기 바랍니다.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/kernel/01-container-primitives
----------------------------------------
# 컨테이너를 지탱하는 커널 기능
> **지원 버전**: Linux 6.1 / 6.12 / 6.18 (Amazon Linux 2023), Kubernetes 1.25+ (cgroup v2)
> **마지막 업데이트**: 2026년 9월 13일
## 이 문서에서 다루는 것
- 컨테이너는 커널의 어떤 기능들을 조합해 만들어지는가 — 그리고 "컨테이너"라는 커널 객체는 없다는 점
- cgroup v1에서 v2로 바뀌면서 운영에서 실제로 달라진 것 (특히 OOM 진단)
- netfilter와 conntrack이 Kubernetes 네트워킹의 어디에 끼어 있는가
## 먼저: 커널에 "컨테이너"는 없습니다
이것이 컨테이너를 이해하는 출발점입니다. 커널에는 `struct container` 같은 것이 없고, 컨테이너를 만드는 단일 시스템 콜도 없습니다.
컨테이너는 **여러 독립적인 커널 기능을 조합해 만든 관례**입니다. 런타임(containerd, runc)이 프로세스를 하나 띄우면서 다음을 함께 적용합니다.
| 목적 | 커널 기능 |
|---|---|
| **무엇을 볼 수 있는가** (격리) | namespace |
| **얼마나 쓸 수 있는가** (제한) | cgroup |
| **무엇을 할 수 있는가** (권한) | capabilities, seccomp, LSM (AppArmor/SELinux) |
| **파일시스템을 어떻게 합치는가** | overlayfs (union mount) |
| **트래픽을 어떻게 흘리는가** | veth, bridge/route, netfilter |
이 조합이라는 점에서 두 가지 실무적 결론이 나옵니다.
**첫째, 격리는 전부 또는 전무가 아닙니다.** 어떤 namespace는 공유하고 어떤 것은 격리할 수 있습니다. Kubernetes Pod가 정확히 그 예입니다 — 같은 Pod의 컨테이너들은 network·IPC namespace를 **공유하고** mount·PID namespace는 대개 **분리**합니다. 그래서 같은 Pod 안에서는 `localhost`로 서로를 부를 수 있고(network 공유), 파일시스템은 서로 안 보입니다(mount 분리).
**둘째, 빠뜨린 격리는 조용히 구멍이 됩니다.** 커널이 "컨테이너를 만들어라"를 모르므로, 런타임이 seccomp 프로필을 적용하지 않으면 그냥 적용되지 않은 상태로 돕니다. 컨테이너 보안이 런타임과 정책 설정의 문제인 이유입니다.
## Namespace — 무엇을 볼 수 있는가
namespace는 **커널 자원의 "이름 공간"을 분리**합니다. 같은 이름이나 번호가 namespace마다 다른 것을 가리키게 만드는 장치입니다.
| Namespace | 격리 대상 | Pod에서 |
|---|---|---|
| **mnt** | 마운트 지점 | 컨테이너별 분리 |
| **pid** | 프로세스 ID | 컨테이너별 분리 (`shareProcessNamespace: true`로 Pod 내 공유 가능) |
| **net** | 네트워크 인터페이스, 라우팅 테이블, netfilter 규칙, 소켓, 포트 | **Pod 단위로 공유** |
| **ipc** | System V IPC, POSIX 메시지 큐 | Pod 단위로 공유 |
| **uts** | hostname, domainname | Pod 단위로 공유 |
| **user** | UID/GID 매핑 | 기본 미사용 (아래 참고) |
| **cgroup** | cgroup 루트 경로 | 컨테이너별 분리 |
| **time** | 부팅 시각, 단조 시계 (5.6+) | 미사용 |
### net namespace가 Pod의 경계인 이유
Pod의 정체가 여기서 정해집니다. Kubernetes는 Pod마다 net namespace를 하나 만들고(pause 컨테이너가 보유), 그 Pod의 모든 컨테이너를 **같은 net namespace에 넣습니다.**
결과로 따라오는 것들:
- Pod 안의 컨테이너들은 **같은 IP, 같은 포트 공간**을 공유합니다 → 같은 Pod에서 두 컨테이너가 8080을 동시에 열 수 없습니다
- `localhost` 통신이 됩니다 → 사이드카 패턴의 기반
- **netfilter 규칙도 net namespace별입니다** → 사이드카 메시의 init container가 Pod의 net namespace 안에서 iptables를 심을 수 있는 이유이고, 그 규칙이 노드 전체에 영향을 주지 않는 이유입니다 ([VPC Lattice 커널 데이터패스](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/vpc-lattice/07-kernel-datapath.md) 참고)
- 라우팅 테이블도 분리됩니다 → Pod 안에서 `ip route`를 보면 노드의 것이 아닙니다
### user namespace — 왜 오래 기본이 아니었는가
User namespace는 컨테이너 UID/GID를 다른 host 범위로 매핑합니다. 여러 탈출 동작의 권한을 줄이지만 kernel 취약점이나 추가 권한 상승 경로까지 **봉쇄한다고 보장하지는 않습니다**.
그런데 오래 기본이 아니었습니다. 이유는 **파일 소유권**입니다. 볼륨의 파일이 호스트 UID로 기록되어 있는데 컨테이너가 다른 UID로 보면 권한이 맞지 않습니다. 이를 해결하려면 마운트 시점에 UID를 변환해야 하고(idmapped mounts, 커널 5.12+), 스토리지 드라이버와 CSI도 이를 지원해야 합니다.
### Kubernetes의 user namespace 지원 현황
[KEP-127](https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/127-user-namespaces/kep.yaml) 기준으로 성숙 단계는 다음과 같습니다.
| 단계 | 버전 |
|---|---|
| alpha | v1.25 |
| beta | v1.35 |
| **stable (GA)** | **v1.36** |
feature gate는 `UserNamespacesSupport`이며 kubelet과 kube-apiserver에 적용됩니다. **1.36부터는 GA이므로 feature gate를 켜지 않아도 `hostUsers: false`를 쓸 수 있습니다.**
::: warning 확인 필요
위 성숙도는 Kubernetes 업스트림 기준입니다. **EKS가 해당 버전을 제공하는지, 그리고 사용 중인 컨테이너 런타임과 CSI 드라이버가 idmapped mounts를 지원하는지는 별개**입니다. 도입 전에 EKS 지원 버전과 런타임·스토리지 조합을 확인하십시오.
:::
## cgroup — 얼마나 쓸 수 있는가
cgroup(control group)은 프로세스 그룹의 **자원 사용을 측정하고 제한**합니다. Kubernetes의 `requests`/`limits`가 최종적으로 도달하는 곳입니다.
### v1과 v2의 구조적 차이
| 항목 | cgroup v1 | cgroup v2 |
|---|---|---|
| **계층 구조** | 컨트롤러(cpu, memory, blkio…)마다 **별개의 트리** | **단일 통합 트리** |
| **프로세스 소속** | 컨트롤러별로 다른 그룹에 속할 수 있음 | 하나의 그룹에만 속함 |
| **메모리+IO 협조** | 어려움 (별도 트리라 연계 불가) | 가능 (같은 트리) |
| **압력 정보** | 없음 | **PSI** (`cpu.pressure`, `memory.pressure`, `io.pressure`) |
| **CPU 제한 표기** | `cpu.cfs_quota_us` / `cpu.cfs_period_us` | `cpu.max` (한 파일에 "quota period") |
| **메모리 제한 표기** | `memory.limit_in_bytes` | `memory.max`, 여기에 `memory.high`(소프트 압력) 추가 |
| **AL2023 EKS AMI** | — | **기본값** |
v1의 "컨트롤러마다 별개 트리"가 실제로 문제였던 지점은 **메모리 회수와 IO의 연계**입니다. 메모리가 부족해 page cache를 비워야 할 때, 그 회수 작업 자체가 디스크 IO를 유발하는데 v1에서는 두 컨트롤러가 서로를 몰랐습니다. v2의 통합 트리는 이를 같은 계층에서 다룹니다.
### 운영에서 가장 크게 달라진 것 — OOM 진단
cgroup v2에서 반드시 알아야 할 사실입니다.
> **`memory.current`는 page cache를 포함합니다.**
즉 애플리케이션이 실제로 붙잡고 있는 메모리(anon/RSS)가 limit보다 훨씬 낮은데도, 파일을 많이 읽어 page cache가 쌓이면 `memory.current`가 limit에 닿습니다.
여기서 중요한 구분이 있습니다. **page cache는 회수 가능(reclaimable)합니다.** 그래서 정상적인 경우 커널은 limit에 닿으면 page cache를 버려서 공간을 만들고, OOM은 나지 않습니다. 문제가 되는 것은 **회수 속도가 할당 속도를 못 따라갈 때**이고, 이때 OOM killer가 동작합니다.
실무적 함의:
| 오해 | 실제 |
|---|---|
| "`memory.current`가 limit 근처이면 OOM 직전" | 회수 가능한 file cache가 포함될 수 있으므로 구성을 가정하지 말고 anon/file/kernel 사용량·압력 확인 |
| "RSS만 보면 된다" | RSS가 낮아도 OOM이 날 수 있습니다 (회수 못 따라가는 경우) |
| "limit을 올리면 해결" | 원인이 회수 지연이면 올려도 재발합니다 |
**진단에 봐야 하는 값들:**
| 파일/값 | 의미 |
|---|---|
| `memory.current` | 현재 사용량 (page cache 포함) |
| `memory.stat`의 `anon` | 익명 메모리 — 애플리케이션이 실제 붙잡은 양 |
| `memory.stat`의 `file` | page cache |
| `memory.events`의 `oom` / `oom_kill` | OOM 발생·킬 횟수 |
| `memory.events`의 `high` / `max` | 소프트/하드 한계에 닿은 횟수 |
| `memory.pressure` (PSI) | 메모리 압박으로 **지연된 시간의 비율** |
**PSI가 특히 유용합니다.** 사용량(얼마나 쓰는가)이 아니라 **압박(그래서 얼마나 기다렸는가)**을 알려주기 때문입니다. `memory.pressure`의 `some avg10`이 올라가고 있으면 회수에 시간을 쓰고 있다는 뜻이고, 이는 사용량 그래프만으로는 보이지 않습니다.
### CPU limit과 throttling — 사용률이 낮은데 느린 이유
CPU limit은 **대역폭 제한**입니다. `cpu.max`가 `20000 100000`이면 "100ms 주기마다 20ms까지"를 뜻합니다.
여기서 직관에 반하는 일이 벌어집니다. 애플리케이션이 짧은 시간에 여러 스레드로 일하면, **주기 초반에 할당량을 다 쓰고 주기가 끝날 때까지 강제로 멈춥니다.** 평균 사용률은 20%로 낮게 보이는데 지연은 튑니다.
멀티스레드에서 더 심합니다. 4개 스레드가 동시에 돌면 20ms 할당량은 실제 시간 5ms에 소진됩니다. 나머지 95ms는 대기입니다.
**진단:** `cpu.stat`의 `nr_throttled`(throttling 당한 주기 수)와 `throttled_usec`(총 throttling 시간). `nr_periods`에 대한 `nr_throttled` 비율이 유의미하게 높으면 limit이 원인입니다.
**대응 방향** (자세한 request/limit 설계는 [리소스 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/ops/10-resource-optimization.md)):
- limit을 올리거나 제거 (단 노드 안정성과 트레이드오프)
- 애플리케이션의 스레드 수를 limit에 맞게 조정 (JVM의 `-XX:ActiveProcessorCount`, Go의 `GOMAXPROCS` 등) — **컨테이너가 인식하는 CPU 수와 실제 할당량이 다른 것이 근본 원인인 경우가 많습니다**
- `cpu.pressure` PSI로 실제 대기 시간 확인
## 권한 — 무엇을 할 수 있는가
격리(namespace)와 제한(cgroup)이 되어 있어도, 프로세스가 할 수 있는 **동작** 자체를 줄이는 것은 별개 계층입니다.
| 기능 | 무엇을 하는가 | Kubernetes에서 |
|---|---|---|
| **capabilities** | root 권한을 잘게 쪼갠 단위로 부여·제거 (`CAP_NET_ADMIN`, `CAP_SYS_ADMIN` 등) | `securityContext.capabilities.add/drop` |
| **seccomp** | 허용할 **시스템 콜** 목록 제한 | `securityContext.seccompProfile` (`RuntimeDefault` 권장) |
| **LSM** (AppArmor/SELinux) | 파일·네트워크 접근을 정책으로 통제 | `securityContext.appArmorProfile` 등 |
| **no_new_privs** | setuid 바이너리로 권한 상승 차단 | `allowPrivilegeEscalation: false` |
세 계층이 다른 질문에 답합니다 — capabilities는 "이 권한을 가졌는가", seccomp는 "이 시스템 콜을 부를 수 있는가", LSM은 "이 객체에 접근할 수 있는가". 그래서 **하나만으로는 부족하고 겹쳐 쓰는 것이 정석**입니다.
`CAP_NET_ADMIN`은 특별히 언급할 가치가 있습니다. 사이드카 메시의 init container가 iptables를 심으려면 이 권한이 필요하고, 그래서 메시 도입이 "왜 이 Pod가 NET_ADMIN을 갖고 있나"라는 보안 심의 질문을 만듭니다.
## netfilter와 conntrack — Kubernetes 네트워킹의 실체
### netfilter
netfilter는 커널 네트워크 스택의 정해진 지점에 **훅**을 제공하는 프레임워크입니다. `iptables`, `nftables`, `ipvs`는 모두 이 훅을 쓰는 사용자 공간 도구이거나 그 위의 구현입니다.
주요 훅 지점:
| 훅 | 언제 |
|---|---|
| `PREROUTING` | 패킷이 들어와 라우팅 결정 **전** — DNAT 지점 |
| `INPUT` | 로컬 프로세스로 향하는 패킷 |
| `FORWARD` | 통과하는 패킷 |
| `OUTPUT` | 로컬에서 나가는 패킷 |
| `POSTROUTING` | 라우팅 결정 **후** 나가기 직전 — SNAT/MASQUERADE 지점 |
Kubernetes에서 이 훅들이 쓰이는 곳:
- **Service의 ClusterIP → Pod IP 변환**: `PREROUTING`/`OUTPUT`에서 DNAT
- **Pod → 외부 통신의 출발지 변환**: `POSTROUTING`에서 MASQUERADE
- **NetworkPolicy**: CNI가 `FORWARD` 등에 규칙 삽입 (Calico의 iptables 데이터플레인)
- **사이드카 메시의 트래픽 인터셉트**: Pod net namespace 안의 `OUTPUT`/`PREROUTING` REDIRECT
### kube-proxy 모드 — iptables, IPVS, nftables
Service 구현 방식이 세 갈래이고, **2025~2026년에 지형이 바뀌었습니다.**
| 모드 | 룰 평가 | 상태 |
|---|---|---|
| **iptables** | Rule-chain 조회 비용은 배치에 따라 다르며 현재 kube-proxy는 갱신을 최적화 | 명시 변경하지 않은 환경의 기본값. 설치 구현 확인 |
| **IPVS** | 커널 L4 로드밸런서, 해시 기반 O(1) | **Kubernetes 1.35(2025년 12월)에서 deprecated**; 1.40 기본 비활성화·1.43 제거 계획 |
| **nftables** | O(1) 조회 + **증분 규칙 갱신** | **Kubernetes 1.33에서 GA** (1.29 alpha → 1.31 beta). 워커 노드에 **커널 5.13+** 필요 |
읽는 방법:
- **대규모 클러스터에서 iptables 모드의 병목은 룰 수와 갱신 비용**입니다. Service·Endpoint가 많을수록 kube-proxy의 동기화 시간이 늘고, 그 동안 규칙이 최신이 아닙니다.
- **IPVS를 쓰고 있다면 이전 계획이 필요합니다.** Upstream은 1.40 기본 비활성화와 1.43 제거를 계획하므로 이전 계획 시 최신 [KEP-5495 일정](https://github.com/kubernetes/enhancements/blob/master/keps/sig-network/5495-deprecate-ipvs-mode-in-kube-proxy/README.md)을 확인합니다. 권장 대체는 nftables 모드입니다.
- AL2023 노드는 커널 6.x라 nftables 모드의 커널 요건을 충족합니다.
- nftables가 GA여도 **기본값은 iptables**이므로 명시적으로 전환해야 합니다.
### conntrack — 가장 자주 사고를 내는 지점
netfilter가 NAT를 하려면 **연결을 기억해야** 합니다. 나갈 때 주소를 바꿨으면 돌아오는 패킷을 원래대로 되돌려야 하니까요. 이 기억을 담는 커널 테이블이 `nf_conntrack`입니다.
Kube-proxy netfilter mode의 Service NAT는 connection tracking에 의존하지만 **NAT 없는 트래픽도 추적될 수 있습니다**. Headless Service·외부 endpoint·eBPF 구현의 경로는 다르므로 모든 Kubernetes Service가 같은 DNAT 경로를 반드시 거친다고 보면 안 됩니다.
**포화되면 어떻게 되는가가 문제의 핵심입니다.** 에러 로그가 요란하게 나지 않습니다. 새 연결이 **조용히 드롭**되고, 애플리케이션은 연결 타임아웃이나 refused를 봅니다. 무엇이 원인인지 애플리케이션 쪽에서는 알 수 없습니다.
| 관측 지점 | 의미 |
|---|---|
| `/proc/sys/net/netfilter/nf_conntrack_count` | 현재 항목 수 |
| `/proc/sys/net/netfilter/nf_conntrack_max` | 상한 |
| `conntrack -S` → `insert_failed` | 삽입 실패. 단독으로 포화를 확정하지 말고 count/max·drop·kernel log와 함께 확인 |
| `conntrack -S`의 `drop` | 드롭된 패킷 |
| `dmesg`의 `nf_conntrack: table full, dropping packet` | 커널 경고 |
**EKS에서 주의할 점**이 하나 있습니다. kube-proxy도 conntrack 값을 관리하는데, **EKS에는 `kube-proxy-config` ConfigMap이 기본으로 존재하고 이것이 커맨드라인 인자보다 우선합니다.** 따라서 노드에서 sysctl만 올려놓고 kube-proxy가 다시 낮추는 상황이 생길 수 있습니다. 값을 바꾸려면 ConfigMap의 `conntrack.maxPerCore`·`conntrack.min`을 조정하고 kube-proxy DaemonSet을 재시작하는 것이 올바른 경로입니다.
`nf_conntrack_max`를 올리면 **노드 메모리 사용이 늘어납니다.** 항목당 메모리를 쓰므로 무한정 올릴 수 없고, 노드 크기에 맞춰야 합니다. 구체적 설정은 [EKS 노드 커널 튜닝](https://www.atomai.click/kubernetes-docs/llms/ko/kernel/03-eks-node-tuning.md)에서 다룹니다.
::: warning 실제 kube-proxy 설정 확인
과거 Bottlerocket 보고에는 kube-proxy가 node sysctl을 덮어쓴 사례가 있습니다. `--config` 사용 시 덮어써지는 CLI flag가 아니라 **활성 설정**의 `conntrack.maxPerCore`·`conntrack.min`을 수정합니다. 둘 다 0으로 설정하는 것은 node sysctl에 상한 관리를 맡기는 의도적 선택이므로 설치한 add-on/version의 동작과 실제 node 값을 검증하고 메모리 예산을 유지합니다.
:::
과거 issue만으로 모든 현재 Bottlerocket release가 같은 동작이라고 단정할 수 없습니다. Rollout 후 유효 설정과 실제 sysctl을 확인합니다.
### conntrack을 피하는 방향
conntrack 부하 자체를 줄이는 접근도 있습니다.
- Headless Service는 Service VIP DNAT를 피하지만 **conntrack을 본질적으로 우회하지는 않습니다**.
- Cilium은 kube-proxy/netfilter 기능을 eBPF map으로 대체할 수 있으며 자체 tracking/map 압력과 남은 netfilter 경로를 측정합니다.
- 연결 재사용은 churn을 줄이며 established 용량과 timeout 동작도 검증합니다.
## overlayfs — 이미지 계층이 합쳐지는 방식
컨테이너 이미지가 계층으로 되어 있고 그 계층들이 하나의 파일시스템으로 보이는 것은 **union mount**, 구체적으로는 `overlayfs`입니다.
구조는 세 부분입니다.
| 계층 | 역할 |
|---|---|
| **lowerdir** | 읽기 전용 — 이미지 계층들 (여러 개 겹칠 수 있음) |
| **upperdir** | 쓰기 가능 — 컨테이너의 변경분 |
| **merged** | 컨테이너가 보는 합쳐진 뷰 |
여기서 운영상 중요한 성질이 **copy-up**입니다. lowerdir의 파일을 수정하면 **파일 전체가 upperdir로 복사된 뒤** 수정됩니다. 그래서:
- **큰 파일을 조금 수정하는 것도 전체 복사 비용**을 냅니다. 1GB 파일의 1바이트 수정에 1GB 복사가 일어납니다
- 컨테이너 안에서 대용량 쓰기를 하면 노드 디스크를 먹습니다 (ephemeral storage)
- **쓰기가 많은 경로는 볼륨으로 빼는 것**이 정석입니다 — emptyDir, PVC 등
## 정리
- 커널에 "컨테이너"는 없습니다. namespace(격리) + cgroup(제한) + capabilities/seccomp/LSM(권한) + overlayfs(파일시스템) + netfilter(네트워크)의 **조합**입니다. 그래서 격리는 선택적이고, 빠뜨린 격리는 조용한 구멍이 됩니다.
- **net namespace가 Pod의 경계**입니다. 같은 IP·포트 공간, `localhost` 통신, Pod 범위의 netfilter 규칙이 모두 여기서 나옵니다.
- cgroup v2에서 **`memory.current`는 page cache를 포함**합니다. OOM 진단은 `memory.stat`의 `anon`과 `memory.events`, 그리고 **PSI(`memory.pressure`)**를 함께 봐야 합니다.
- CPU limit은 **대역폭 제한**이라 사용률이 낮아도 throttling으로 지연이 튑니다. `cpu.stat`의 `nr_throttled`가 증거입니다.
- kube-proxy는 **nftables가 1.33에서 GA, IPVS는 1.35에서 deprecated**이며 upstream은 IPVS의 1.40 기본 비활성화와 1.43 제거를 계획합니다. 기본값은 여전히 iptables입니다.
- Conntrack 포화는 신규 연결을 드롭할 수 있습니다. Count/max·drop/insert counter·log로 진단하고 상한 변경 전 유효 kube-proxy 설정을 확인합니다.
다음: [커널 네트워킹 스택](https://www.atomai.click/kubernetes-docs/llms/ko/kernel/02-network-stack.md)에서 패킷이 지나는 전체 경로를 봅니다.
## 참고 자료
- [Control Group v2 — Linux kernel documentation](https://docs.kernel.org/admin-guide/cgroup-v2.html)
- [PSI - Pressure Stall Information](https://docs.kernel.org/accounting/psi.html)
- [namespaces(7) — Linux manual](https://man7.org/linux/man-pages/man7/namespaces.7.html)
- [KEP-127: Support User Namespaces](https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/127-user-namespaces/README.md)
- [bottlerocket-os/bottlerocket#4221 — conntrack limit not applied](https://github.com/bottlerocket-os/bottlerocket/issues/4221)
- [NFTables mode for kube-proxy (Kubernetes Blog)](https://kubernetes.io/blog/2025/02/28/nftables-kube-proxy/)
- [KEP-5495: Deprecate IPVS mode in kube-proxy](https://github.com/kubernetes/enhancements/blob/master/keps/sig-network/5495-deprecate-ipvs-mode-in-kube-proxy/README.md)
- [Running kube-proxy in nftables Mode — EKS Best Practices](https://docs.aws.amazon.com/eks/latest/best-practices/nftables.html)
- [Increase nf_conntrack_max limit on EKS nodes](https://repost.aws/knowledge-center/eks-increase-nf-conntrack-max-limit)
- [Amazon EKS-Optimized Amazon Linux 2023 AMIs](https://aws.amazon.com/blogs/containers/amazon-eks-optimized-amazon-linux-2023-amis-now-available/)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/kernel/02-network-stack
----------------------------------------
# 커널 네트워킹 스택
> **지원 버전**: Linux 6.1 / 6.12 / 6.18 (Amazon Linux 2023)
> **마지막 업데이트**: 2026년 9월 12일
## 이 문서에서 다루는 것
- `send()` 한 번이 NIC의 전선에 닿기까지 커널 안에서 지나는 경로와, 각 지점이 무엇을 하는가
- 그 경로 어디에 훅을 걸 수 있는가 — XDP, TC, netfilter의 위치가 성능 차이를 만드는 이유
- Pod 간 통신이 같은 노드·같은 AZ·다른 AZ에서 실제로 다른 경로를 지나는 이유
## 왜 경로를 알아야 하는가
"네트워크가 느리다"는 진단할 수 없는 문장입니다. 커널 네트워크 경로에는 **각각 다른 이유로 지연과 손실이 생기는 지점이 여러 개** 있고, 어느 지점인지에 따라 대응이 완전히 달라집니다.
| 증상 | 실제 지점 |
|---|---|
| 처리량이 어느 선에서 막힌다 | 소켓 버퍼, 또는 단일 플로우 한도 |
| 트래픽 폭주 시에만 드롭된다 | qdisc 큐 오버플로 또는 NIC ring buffer |
| CPU 하나만 100%다 | RSS/RPS 미설정 — 인터럽트가 한 코어에 몰림 |
| 작은 요청이 유독 느리다 | 고정 오버헤드(시스템 콜, 컨텍스트 스위치) 비중 |
| 규칙이 많아지자 느려졌다 | netfilter 룰 평가 |
경로를 알면 이 표를 거꾸로 읽어 "어디를 볼지"가 나옵니다.
## 송신 경로 — send()에서 전선까지
```mermaid
graph TB
APP["애플리케이션 send / write"] --> SC["시스템 콜 진입 유저→커널 전환"]
SC --> SK["소켓 계층 sk_buff 할당 송신 버퍼 적재"]
SK --> L4["전송 계층 (TCP) 세그먼트 분할 혼잡 제어·재전송 큐"]
L4 --> L3["네트워크 계층 (IP) 라우팅 조회 헤더 구성"]
L3 --> NFO["netfilter OUTPUT / POSTROUTING NAT·필터"]
NFO --> TCE["TC egress eBPF 훅 지점"]
TCE --> QD["qdisc 큐잉·셰이핑 드롭 발생 지점"]
QD --> DRV["드라이버 ring buffer 적재 doorbell"]
DRV --> NIC["NIC DMA·체크섬·TSO 전선"]
style SC fill:#fff4e5,stroke:#d98324
style NFO fill:#fdecea,stroke:#d93025
style QD fill:#fdecea,stroke:#d93025
```
각 단계에서 실제로 무슨 일이 일어나는지가 진단의 근거입니다.
### ① 시스템 콜 진입 — 고정 오버헤드의 출처
`send()`는 시스템 콜이므로 유저 공간에서 커널로 전환됩니다. 이 전환 비용은 **전송하는 데이터 크기와 무관한 고정 비용**입니다.
그래서 **작은 요청이 많은 워크로드에서 이 비용의 비중이 커집니다.** 64바이트를 1만 번 보내는 것과 640KB를 한 번 보내는 것은 데이터양이 같아도 시스템 콜 횟수가 1만 배 차이납니다.
대응은 배치화입니다 — `sendmsg`/`sendmmsg`로 묶어 보내기, 애플리케이션 레벨에서 버퍼링, 또는 `io_uring`으로 제출 자체를 배치화.
### ② 소켓 계층 — sk_buff와 버퍼
커널은 패킷을 `sk_buff`(socket buffer) 구조체로 다룹니다. 데이터와 각 계층의 헤더 위치, 메타데이터를 담은 자료구조입니다. 경로 전체에서 이 구조체 포인터가 전달되며, **복사를 최소화하는 것이 설계 목표**입니다.
송신 버퍼가 차면 어떻게 되는가가 여기서 갈립니다.
- **블로킹 소켓**: `send()`가 대기합니다
- **논블로킹 소켓**: `EAGAIN`을 반환하고, 애플리케이션이 재시도해야 합니다
즉 소켓 버퍼 크기(`net.ipv4.tcp_wmem`)는 **애플리케이션이 얼마나 앞서 나갈 수 있는가**를 정합니다. BDP(대역폭 × 지연)보다 작으면 링크를 채우지 못합니다.
### ③ 전송 계층 (TCP) — 혼잡 제어가 사는 곳
TCP가 하는 일이 성능에 가장 크게 영향을 줍니다.
- 데이터를 MSS 단위 세그먼트로 나눕니다
- **혼잡 윈도(cwnd)**로 얼마나 앞서 보낼지 결정합니다
- 재전송 큐를 유지하고, ACK를 못 받으면 재전송합니다
혼잡 제어 알고리즘이 여기 있습니다. `cubic`이 오래 기본이었고, **`bbr`**이 대안입니다. 둘의 차이는 **혼잡을 무엇으로 판단하는가**입니다.
| 알고리즘 | 혼잡 신호 | 잘 맞는 환경 |
|---|---|---|
| **cubic** | **패킷 손실** | 손실이 혼잡을 의미하는 유선 환경 |
| **bbr** | **대역폭·RTT 추정** | 손실이 혼잡과 무관하게 생기는 환경(무선, 버퍼 얕은 경로), 긴 지연 경로 |
cubic의 전제는 "손실 = 혼잡"입니다. 그런데 손실이 다른 이유로 나는 경로에서는 cubic이 불필요하게 물러섭니다. bbr은 손실 대신 실측 대역폭과 최소 RTT로 판단해 이 문제를 피합니다.
VPC 내부 통신은 손실이 드문 품질 좋은 경로라 cubic으로도 대개 충분합니다. **리전 간이나 인터넷 경유처럼 지연이 길고 손실이 섞이는 경로에서 bbr의 이점이 나타납니다.**
### ④ 네트워크 계층 (IP) — 라우팅 조회
목적지로 가는 경로를 라우팅 테이블에서 찾습니다. **이 조회는 net namespace별**이라, Pod 안에서 보는 라우팅 테이블은 노드의 것이 아닙니다 ([컨테이너 커널 기능](https://www.atomai.click/kubernetes-docs/llms/ko/kernel/01-container-primitives.md)).
### ⑤ netfilter — 규칙이 평가되는 곳
`OUTPUT`과 `POSTROUTING` 훅에서 필터링과 NAT가 일어납니다. Kubernetes에서는 Service DNAT와 egress MASQUERADE가 이 지점입니다.
**여기가 규칙 수에 비례해 느려질 수 있는 지점입니다.** iptables 모드 kube-proxy에서 Service가 수천 개면 체인이 길어지고 선형 평가 비용이 붙습니다. nftables 모드와 eBPF 데이터플레인이 해결하려는 문제가 정확히 이것입니다.
### ⑥ qdisc — 드롭이 실제로 일어나는 곳
qdisc(queueing discipline)는 **패킷을 NIC로 보내기 전 큐에 넣고 순서와 속도를 정합니다.**
운영상 중요한 사실:
> Qdisc overflow는 burst drop의 가능한 원인 중 하나입니다. Qdisc·NIC/driver·stack·cloud-network counter를 함께 확인한 뒤 원인을 판단합니다.
qdisc 큐가 가득 차면 패킷을 버립니다. 이것은 NIC나 네트워크의 문제가 아니고 **노드 안에서 일어나는 드롭**입니다. 그래서 "네트워크가 패킷을 잃었다"고 생각하고 밖을 찾다가 시간을 버리기 쉽습니다.
**관측**: `tc -s qdisc show dev `의 `dropped` 카운터. `ip -s link`의 송신 드롭도 함께 봅니다.
qdisc 종류에 따라 성격이 다릅니다.
| qdisc | 성격 |
|---|---|
| `pfifo_fast` | 단순 FIFO(우선순위 3밴드). 오래된 기본값 |
| `fq_codel` | **버퍼블로트 완화** — 큐가 길어지면 능동적으로 드롭해 지연을 억제. 여러 배포판의 현대적 기본값 |
| `fq` | 플로우 공정 큐잉 + 페이싱. bbr과 함께 쓰기 좋음 |
| `mq` | 멀티큐 NIC에서 하드웨어 큐별 qdisc를 두는 래퍼 |
**버퍼블로트**는 이해할 가치가 있는 개념입니다. 큐를 크게 잡으면 드롭은 줄지만 **큐에서 기다리는 시간이 지연으로 나타납니다.** 처리량은 좋아 보이는데 지연이 나빠지는 상황입니다. `fq_codel`은 큐 지연을 감시해 일부러 드롭함으로써 TCP에게 "물러서라"는 신호를 빨리 주는 방식으로 이를 완화합니다.
### ⑦ 드라이버와 NIC — 오프로드
드라이버가 `sk_buff`를 ring buffer(디스크립터 링)에 넣고 NIC에 알립니다. NIC가 DMA로 메모리를 읽어 전송합니다.
NIC가 대신 해주는 일들이 **CPU 사용을 크게 줄입니다.**
| 오프로드 | 하는 일 |
|---|---|
| **TSO / GSO** | TSO는 지원 hardware에 segmentation을 위임하며 GSO는 kernel의 generic/software segmentation framework와 fallback입니다. 둘 다 NIC 전용 동작은 아닙니다 |
| **GRO** (Generic Receive Offload) | 수신 시 작은 패킷들을 **합쳐서** 스택에 올림 → 스택 통과 횟수 감소 |
| **체크섬 오프로드** | 체크섬 계산을 NIC가 수행 |
| **RSS** (Receive Side Scaling) | 수신 패킷을 **여러 큐/코어에 해시로 분산** |
TSO/GRO의 효과는 큽니다 — 스택을 통과하는 횟수를 줄이는 것이 곧 CPU 절약입니다. **관측**: `ethtool -k `로 현재 상태 확인.
## 수신 경로 — 인터럽트에서 애플리케이션까지
수신은 송신의 역순이지만 **인터럽트 처리라는 고유한 구조**가 있습니다.
```mermaid
graph TB
NIC2["NIC 패킷 수신·DMA"] --> IRQ["하드웨어 인터럽트 특정 CPU에 전달"]
IRQ --> NAPI["NAPI 폴링 인터럽트 끄고 배치 수거 softirq 컨텍스트"]
NAPI --> XDPH["Native/driver XDP sk_buff 할당 이전"]
XDPH --> SKB["sk_buff 구성 GRO 병합"]
SKB --> TCI["TC ingress eBPF 훅 지점"]
TCI --> NFP["netfilter PREROUTING DNAT·필터"]
NFP --> L3R["IP 계층 라우팅: 로컬 or 전달"]
L3R --> L4R["TCP 계층 순서 재조립·ACK"]
L4R --> SKR["소켓 수신 버퍼"]
SKR --> APP2["애플리케이션 recv / read"]
style XDPH fill:#e8f5e9,stroke:#1e8e3e
style TCI fill:#e8f5e9,stroke:#1e8e3e
style NFP fill:#fdecea,stroke:#d93025
```
### NAPI — 인터럽트 폭주를 막는 장치
패킷마다 인터럽트를 걸면 고부하에서 **인터럽트 처리만 하다 아무 일도 못 하는 상태**(livelock)가 됩니다.
NAPI가 이를 막습니다. 첫 인터럽트가 오면 **인터럽트를 끄고 폴링으로 전환**해 큐에 쌓인 패킷을 한 번에 여러 개 수거합니다. 큐가 비면 다시 인터럽트를 켭니다. 부하가 높을 때 자동으로 폴링 모드가 되는 구조입니다.
이것이 **고부하에서 오히려 효율이 좋아지는 이유**입니다. 배치가 커지면 패킷당 오버헤드가 내려갑니다.
### 인터럽트가 한 코어에 몰리는 문제
수신 인터럽트는 특정 CPU에 전달됩니다. 큐가 하나거나 분산이 설정되지 않으면 **그 코어만 100%가 되고 나머지는 놀게 됩니다.** 전체 CPU 사용률 그래프는 낮게 보이는데 처리량이 막힙니다.
해결 계층이 세 개입니다.
| 기능 | 계층 | 하는 일 |
|---|---|---|
| **RSS** | 하드웨어 | NIC가 해시로 여러 수신 큐에 분산, 각 큐를 다른 CPU가 처리 |
| **RPS** | 소프트웨어 | 커널이 수신 처리를 다른 CPU로 넘김 (RSS 없거나 큐가 적을 때) |
| **RFS** | 소프트웨어 | 해당 소켓을 **실제로 읽는 프로세스가 있는 CPU**로 보냄 → 캐시 지역성 향상 |
**진단**: `/proc/interrupts`로 인터럽트가 코어에 고르게 분포하는지, `mpstat -P ALL`로 특정 코어의 `%soft`(softirq)가 튀는지 확인합니다.
### 소켓 수신 버퍼와 백프레셔
애플리케이션이 `recv()`를 충분히 빠르게 부르지 않으면 수신 버퍼가 찹니다. TCP는 **수신 윈도를 줄여** 상대에게 "천천히 보내라"고 알립니다(백프레셔).
여기서 자주 오해되는 지점: **이 상황에서 지연이 늘어나는 원인은 네트워크가 아니라 애플리케이션입니다.** 애플리케이션이 처리를 못 따라가서 큐가 쌓인 것이고, 버퍼를 키우면 지연이 더 늘어납니다(버퍼블로트와 같은 구조). 근본 대응은 처리 능력을 늘리는 것입니다.
## 훅 지점 비교 — XDP, TC, netfilter
같은 "패킷을 가로채 처리한다"인데 **위치가 성능과 가능한 일을 결정합니다.**
| 항목 | **XDP** | **TC (eBPF)** | **netfilter** |
|---|---|---|---|
| **위치** | Native/driver XDP: `sk_buff` 이전. Generic XDP: skb 기반 | `sk_buff` 생성 후 ingress/egress | Stack hook |
| **방향** | ingress 중심 | ingress + egress | 전 방향 |
| **성능** | **가장 빠름** — 스택을 안 타고 즉시 드롭/전달 가능 | 빠름 | 상대적으로 느림 (규칙 수 영향) |
| **볼 수 있는 정보** | 원시 패킷 (메타데이터 제한적) | `sk_buff` 메타데이터 전체 | 연결 상태(conntrack) 포함 |
| **주 용도** | **DDoS 드롭**, 로드밸런싱, 패킷 리다이렉트 | 정책 집행, 관측, 리다이렉트 | NAT, 상태 기반 필터 |
| **Hardware offload** | 일부 driver/NIC 조합 | 일부 | 일부 nftables flowtable offload. 모든 rule/path는 아님 |
조기 drop의 장점은 skb 할당 이전의 **native/driver XDP** 설명입니다. Generic XDP에는 이미 skb가 있으며 실제 성능은 driver 지원과 프로그램 처리에 달려 있습니다. 한 mode의 설명을 보편적인 benchmark 결과로 사용하지 않습니다.
XDP가 모든 socket/stack 문맥을 자동으로 받는 것은 아니지만 BPF map으로 상태를 유지하고 지원 helper로 정보를 얻을 수 있습니다. **XDP의 상태 기반 처리가 본질적으로 불가능한 것은 아닙니다.** 실제 프로그램·kernel·verifier·driver 제약을 평가합니다.
Cilium이 두 훅을 함께 쓰는 이유가 여기 있습니다 — 가능한 것은 XDP에서 빠르게 처리하고, 상태나 L7 정보가 필요한 것은 TC 이후로 넘깁니다 ([Cilium eBPF](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/02-ebpf.md), [Cilium L2-L7 네트워킹](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/05-l2-l7-networking.md)).
## Pod 간 통신 — 경로가 왜 다른가
Kubernetes에서 Pod 간 통신은 배치에 따라 **실제로 다른 커널 경로**를 지납니다. 이것이 [Pod 네트워크 실측 벤치마크](https://www.atomai.click/kubernetes-docs/llms/ko/networking/06-pod-network-benchmark.md)에서 관측된 RTT 사다리(같은 노드 0.040 ms → 같은 AZ 0.339 ms → 다른 AZ 0.544 ms)의 원인입니다.
### 같은 노드의 Pod 간
```text
Pod A [net ns A] → veth A → (노드 net ns) → veth B → Pod B [net ns B]
```
그림의 일반 veth/routed 동일 노드 경로는 물리 NIC를 통과할 필요가 없습니다. 다른 dataplane·overlay·SR-IOV·policy/service 우회 경로는 달라질 수 있으며 가상 장치도 kernel driver 처리를 거칩니다.
벤치마크에서 같은 노드 단일 플로우가 **29.97 Gbps**까지 나온 것(다른 노드는 4.96 Gbps에서 EC2 단일 플로우 한도에 막힘)이 이 때문입니다. 병목이 네트워크가 아니라 **CPU**였습니다 — 클라이언트 코어 하나가 99.8%였습니다.
### 다른 노드의 Pod 간 (VPC CNI)
```text
Pod A → veth → 노드 net ns → ENI → VPC 네트워크 → 대상 ENI → veth → Pod B
```
Amazon VPC CNI에서 Pod는 **VPC의 실제 IP**를 받으므로 오버레이 캡슐화가 없습니다. 오버레이(VXLAN 등)를 쓰는 CNI 대비 캡슐화·역캡슐화 비용과 MTU 손실이 없다는 것이 VPC CNI의 구조적 이점입니다 ([VPC CNI](https://www.atomai.click/kubernetes-docs/llms/ko/networking/01-vpc-cni.md)).
대신 여기서는 송신 경로 전체(qdisc, 드라이버, NIC)를 지나고, **EC2 인스턴스의 네트워크 한도**를 받습니다 — 단일 플로우 상한, 인스턴스 총 대역폭, PPS 한도.
### AZ를 넘을 때
인용한 단일 flow 실험에서는 RTT +0.21 ms와 두 cross-node 배치의 약 4.96 Gbps를 관측했습니다. 해당 instance·부하·경로의 결과이며 모든 cross-AZ 워크로드의 처리량이 같다는 증명은 아닙니다.
### MTU와 단편화
패킷이 경로 최소 MTU보다 크면 단편화되거나 드롭됩니다. **점보 프레임(9001)**을 VPC 내부에서 쓸 수 있지만, 경로에 더 작은 MTU가 섞이면 문제가 됩니다.
특히 주의할 것이 **PMTUD(Path MTU Discovery)의 실패**입니다. 경로 MTU를 알려주는 ICMP가 차단되면 송신 측은 계속 큰 패킷을 보내고, 그것이 중간에서 드롭되면서 **연결이 멈춘 것처럼 보입니다.** "핸드셰이크는 되는데 데이터 전송에서 멈춘다"는 증상의 전형적 원인입니다 — 작은 패킷(핸드셰이크)은 통과하고 큰 패킷만 드롭되기 때문입니다.
## 관측 도구 정리
계층별로 봐야 할 것이 다릅니다.
| 계층 | 도구 | 무엇을 보는가 |
|---|---|---|
| 소켓 | `ss -tin` | 연결 상태, cwnd, RTT, 재전송 |
| TCP 전역 | `nstat` / `netstat -s` | 재전송, 순서 어긋남, 버퍼 오버런 |
| netfilter | `iptables-save`, `nft list ruleset` | 규칙 수와 내용 |
| conntrack | `conntrack -S` | **`insert_failed`** — 포화 증거 |
| qdisc | `tc -s qdisc show dev ` | **`dropped`** — 노드 내 드롭 |
| 인터페이스 | `ip -s link`, `ethtool -S ` | 인터페이스·NIC 카운터 |
| 오프로드 | `ethtool -k ` | TSO/GRO/체크섬 상태 |
| 인터럽트 | `/proc/interrupts`, `mpstat -P ALL` | 코어 편중, softirq 비중 |
| 경로 추적 | `tcpdump`, `ss`, eBPF 도구 | 실제 패킷 |
Drop counter부터 확인한 뒤 시각·interface/namespace·traffic·resource pressure와 대조합니다. Counter 증가는 조사할 증거이지 유일한 원인의 증명은 아니며 counter 부재가 다른 곳의 손실을 배제하지도 않습니다. 필요하면 RTT/cwnd·앱 지표·packet capture를 사용합니다.
## 정리
- 송신은 **시스템 콜 → 소켓 → TCP → IP → netfilter → TC → qdisc → 드라이버 → NIC** 순서입니다. 각 지점이 다른 이유로 문제를 만듭니다.
- **트래픽 폭주 시 드롭은 대개 qdisc에서** 일어납니다. 노드 안의 문제인데 네트워크 밖을 찾다가 시간을 버리기 쉽습니다.
- 수신은 **NAPI**가 인터럽트 폭주를 막고, 인터럽트가 한 코어에 몰리는 문제는 **RSS/RPS/RFS**로 분산합니다.
- 훅 지점의 성능 차이는 위치에서 나옵니다 — **XDP는 `sk_buff` 할당 전**이라 가장 빠르지만 conntrack 상태를 모릅니다.
- Pod 간 통신은 배치에 따라 **다른 경로**를 지납니다. 같은 노드는 veth만 지나 NIC를 건드리지 않고, 그래서 병목이 네트워크가 아니라 CPU입니다.
- **PMTUD 실패는 "핸드셰이크는 되는데 데이터에서 멈춘다"로 나타납니다.**
다음: [EKS 노드 커널 튜닝](https://www.atomai.click/kubernetes-docs/llms/ko/kernel/03-eks-node-tuning.md)에서 이 경로의 어느 파라미터를 언제 건드려야 하는지 봅니다.
## 참고 자료
- [Linux Networking Documentation — Kernel](https://docs.kernel.org/networking/index.html)
- [NAPI — Linux kernel documentation](https://docs.kernel.org/networking/napi.html)
- [Scaling in the Linux Networking Stack (RSS/RPS/RFS)](https://docs.kernel.org/networking/scaling.html)
- [XDP — eXpress Data Path](https://docs.kernel.org/networking/af_xdp.html)
- [BBR congestion control](https://datatracker.ietf.org/doc/draft-cardwell-iccrg-bbr-congestion-control/)
- [Pod 네트워크 실측 벤치마크](https://www.atomai.click/kubernetes-docs/llms/ko/networking/06-pod-network-benchmark.md)
- [eBPF 기초와 실무 활용](https://www.atomai.click/kubernetes-docs/llms/ko/basics/05-ebpf-fundamentals.md)
- [Linux segmentation offload](https://docs.kernel.org/networking/segmentation-offloads.html) — hardware TSO와 software GSO
- [Linux IP sysctl](https://docs.kernel.org/networking/ip-sysctl.html) — TCP buffer 크기와 socket override
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/kernel/03-eks-node-tuning
----------------------------------------
# EKS 노드 커널 튜닝
> **지원 버전**: Amazon Linux 2023 (커널 6.1 / 6.12 / 6.18), Kubernetes 1.33+ (Amazon EKS)
> **마지막 업데이트**: 2026년 9월 13일
## 이 문서에서 다루는 것
- EKS 노드에서 무엇을 건드려야 하고 무엇을 기본값으로 두어야 하는가 — 그리고 그 판단 기준
- 커널 파라미터를 EKS에서 실제로 적용하는 경로들과 각각의 함정
- AL2023의 커널 버전 전환(6.1 → 6.18)이 운영에 의미하는 것
## 먼저: 대부분은 건드리지 마십시오
이 문서의 가장 중요한 조언입니다.
커널 기본값은 **광범위한 워크로드에서 합리적으로 동작하도록** 정해져 있고, 상당수는 부하에 따라 커널이 자동 조정합니다(예: TCP 버퍼 자동 튜닝). 근거 없는 튜닝은 세 가지 방식으로 손해를 냅니다.
| 문제 | 예 |
|---|---|
| **재현 불가능한 구성** | 노드마다 값이 달라 장애 재현이 안 됨 |
| **커널 업그레이드 시 깨짐** | 6.1에서 유효했던 튜너블이 6.18에서 이름·위치가 바뀌거나 사라짐 |
| **자동 튜닝 방해** | Socket별 SO_RCVBUF/SO_SNDBUF 명시는 해당 socket 자동 크기 조정을 끄며 sysctl 범위와는 다른 제어 |
**튜닝의 전제 조건은 측정입니다.** 아래 순서를 지키십시오.
1. 증상을 특정한다 (지연? 드롭? 처리량?)
2. **드롭 카운터를 먼저 본다** — `conntrack -S`의 `insert_failed`, `tc -s qdisc`의 `dropped`, `ethtool -S`의 NIC 드롭
3. 그 카운터가 증가하는 지점의 파라미터만 건드린다
4. 변경 전후를 같은 조건으로 측정한다
5. 변경 사유와 근거를 코드로 남긴다 (아래 적용 경로)
측정 방법은 [Pod 네트워크 실측 벤치마크](https://www.atomai.click/kubernetes-docs/llms/ko/networking/06-pod-network-benchmark.md)의 픽스처를 참고하실 수 있습니다.
## 적용 경로 — EKS에서 커널 파라미터를 바꾸는 방법
바꾸는 방법이 여러 개이고 **각각 범위와 함정이 다릅니다.** 이것을 먼저 정리하는 것이 실무에서 더 중요합니다.
| 경로 | 범위 | 지속성 | 비고 |
|---|---|---|---|
| **노드 부트스트랩 스크립트** (User Data / `nodeadm`) | 노드 전체 | 노드 교체 시 재적용됨 | AL2023은 `nodeadm` 구성 사용. 가장 표준적 |
| **Bottlerocket 설정** (`settings.kernel.sysctl`) | 노드 전체 | 노드 설정으로 관리 | Bottlerocket은 불변 OS라 이 경로만 사용 |
| **Pod `securityContext.sysctls`** | **Pod의 net namespace만** | Pod 스펙 | **namespace 지원 sysctl만** 가능. `net.*` 다수가 해당 |
| **privileged 초기화 DaemonSet** | 노드 전체 | Pod 재시작 시 재적용 | 흔히 쓰이지만 privileged 필요 — 보안 심의 대상 |
| **`kube-proxy-config` ConfigMap** | conntrack 관련 | kube-proxy가 관리 | **EKS에서 이것이 CLI 인자보다 우선** |
| **Karpenter `EC2NodeClass`** | 노드 그룹 | 노드 프로비저닝 시 | User Data를 선언적으로 관리 |
### 놓치기 쉬운 두 가지
**첫째, namespace 지원 sysctl과 그렇지 않은 것의 구분입니다.** `net.*` 중 상당수는 net namespace별로 설정 가능해서 Pod `securityContext.sysctls`로 바꿀 수 있습니다. 반면 `vm.*`, `fs.*`, 그리고 **`net.netfilter.nf_conntrack_max` 같은 일부 값은 노드 전역**이라 Pod 스펙으로는 바꿀 수 없습니다.
또한 kubelet은 기본적으로 "안전하지 않은" sysctl을 거부합니다. 필요하면 `--allowed-unsafe-sysctls`로 명시적으로 허용해야 하고, 이것은 노드 설정입니다.
**둘째, conntrack은 kube-proxy가 덮어씁니다.** [컨테이너 커널 기능](https://www.atomai.click/kubernetes-docs/llms/ko/kernel/01-container-primitives.md)에서 언급한 내용인데, 실무에서 가장 자주 걸리는 함정이라 다시 씁니다 — EKS에는 `kube-proxy-config` ConfigMap이 기본 존재하고 **커맨드라인 인자보다 우선**합니다. 부트스트랩에서 sysctl로 올려놔도 kube-proxy가 자기 값으로 되돌릴 수 있습니다.
::: warning 확인 필요
Bottlerocket에서 `settings.kernel.sysctl`로 conntrack 상한을 올려도 적용되지 않는 이슈가 있습니다([bottlerocket-os/bottlerocket#4221](https://github.com/bottlerocket-os/bottlerocket/issues/4221), 2024년 9월 등록). 원인은 **kube-proxy 설정 파일(`/var/lib/kube-proxy-config/config`)이 커맨드라인 인자보다 우선**하기 때문입니다.
Kube-proxy 설정 파일을 사용하며 상한 관리를 node sysctl에 의도적으로 맡기려면 파일의 **`conntrack.maxPerCore`/`conntrack.min` 필드**를 변경합니다. 둘을 0으로 설정하더라도 `--config`가 무시하는 CLI flag에 의존하면 안 됩니다. Rollout 전 관리형 add-on 조정 동작과 메모리 여유를 확인합니다.
**이 이슈가 특정 Bottlerocket 릴리스에서 해결되었는지는 확인하지 못했습니다.** 어느 경로로 설정하든 적용 후 노드에서 실제 값을 직접 확인하십시오.
```bash
# 노드에서 실제 적용값 확인
cat /proc/sys/net/netfilter/nf_conntrack_max
cat /proc/sys/net/netfilter/nf_conntrack_count
conntrack -S | head
```
:::
## 커널 버전 — AL2023의 6.1 → 6.18 전환
운영상 지금 알아야 할 변화입니다.
| 시점 | 내용 |
|---|---|
| 2023년 3월 | AL2023 출시, 커널 **6.1** |
| 2025년 4월 | 커널 **6.12** 지원 추가 |
| **2026년 8월 17일** | **`al2023-ami-kernel-default` AMI의 기본 커널이 6.1 → 6.18로 변경** |
**함의가 두 갈래입니다.**
기본 kernel **AMI 계열**이 바뀌어도 교체 노드는 launch template/provisioning 정책이 선택한 AMI를 사용합니다. 고정 AMI ID의 kernel은 새 노드를 시작한다고 바뀌지 않습니다. Latest/default AMI를 다시 조회하면 새 kernel을 선택할 수 있으며 EKS-optimized AMI는 별도 release 선택을 확인해야 합니다.
특정 커널에 고정해야 하면 **버전 지정 AMI**(`al2023-ami-kernel-6.1-*` 등)를 명시적으로 쓰십시오.
**커널이 바뀔 때 점검할 것들:**
| 항목 | 이유 |
|---|---|
| 튜너블의 존재와 위치 | 커널 버전 간에 이름이 바뀌거나 sysctl → debugfs로 이동한 것이 있음 |
| 커널 모듈 의존 컴포넌트 | eBPF 프로그램, 특정 CNI 기능, GPU 드라이버, 커스텀 모듈 |
| 스케줄러 거동 | 6.6+ EEVDF (아래 참고) — 지연 민감 워크로드에서 체감될 수 있음 |
| 성능 회귀 | 벤치마크를 커널 버전별로 다시 측정 |
**권고**: 커널 전환은 Kubernetes 버전 업그레이드와 **같은 무게로 다루십시오.** 스테이징에서 같은 커널로 먼저 검증하고, 성능 기준선을 다시 측정한 뒤 프로덕션에 적용하는 것이 안전합니다.
## CPU — 스케줄러와 throttling
### EEVDF — 6.6에서 바뀐 것
Linux 6.6에서 CFS의 태스크 선택 로직이 **EEVDF**(Earliest Eligible Virtual Deadline First)로 교체되었습니다.
정확히 이해할 가치가 있는 지점은 **무엇이 바뀌고 무엇이 안 바뀌었는가**입니다.
| 바뀐 것 | 안 바뀐 것 |
|---|---|
| 다음에 실행할 태스크를 **고르는 방식** (가상 데드라인 기반) | vruntime 메커니즘, weight 계산 |
| 깨어난 태스크의 선점 판단 — 휴리스틱(`sched_wakeup_granularity_ns`) 대신 **데드라인 비교** | 그룹 스케줄링(cgroup cpu.weight), 로드 밸런싱 |
즉 **CFS를 통째로 갈아낸 것이 아니라 선택 로직을 교체한 진화**로 보는 것이 정확합니다. `fair_sched_class` 안에서의 변경입니다.
운영 관점의 의미: **지연 민감 워크로드의 깨우기 지연 특성이 달라질 수 있습니다.** 대개 개선 방향이지만, 커널 6.1에서 6.18로 넘어갈 때 p99가 바뀌면 이 변화가 후보 중 하나입니다.
### 튜너블의 실제 위치
커널 소스(`kernel/sched/debug.c`)에서 확인한 결과입니다.
| 항목 | 상태 |
|---|---|
| `sched_latency_ns` | **제거됨** — `kernel/sched/fair.c`에 참조가 남아 있지 않음 |
| `sched_wakeup_granularity_ns` | **제거됨** — 동일 |
| **`/sys/kernel/debug/sched/base_slice_ns`** | **현재의 대응 튜너블.** 내부 변수는 `sysctl_sched_base_slice`이고 debugfs에 `base_slice_ns`로 노출됨 |
즉 CFS 시절의 지연·선점 휴리스틱 튜너블은 사라지고, **기본 타임슬라이스 하나(`base_slice_ns`)**로 정리되었습니다. 이름에 `sched_` 접두어가 없다는 점에 주의하십시오 — 경로는 `/sys/kernel/debug/sched/base_slice_ns`입니다.
EEVDF는 이와 별개로 `sched_setattr()` 시스템 콜로 **태스크가 자기 타임슬라이스를 직접 요청**할 수 있게 했습니다. 지연 민감 애플리케이션에는 전역 튜너블을 건드리는 것보다 이 경로가 맞습니다.
**그래도 스케줄러 튜너블은 권장 튜닝 대상이 아닙니다.** debugfs는 커널 디버그 인터페이스라 프로덕션에서 마운트되지 않을 수 있고, 대부분의 경우 애플리케이션의 스레드 수 조정이나 cgroup limit 조정이 더 나은 답입니다.
### CPU limit — throttling이 진짜 문제인 경우
[컨테이너 커널 기능](https://www.atomai.click/kubernetes-docs/llms/ko/kernel/01-container-primitives.md)에서 다룬 대로, CPU limit은 대역폭 제한이라 사용률이 낮아도 지연을 만듭니다.
**진단:**
```bash
# cgroup v2 — 컨테이너의 cgroup 경로에서
cat cpu.stat
# nr_periods, nr_throttled, throttled_usec
```
`nr_throttled / nr_periods` 비율이 유의미하게 높으면 limit이 원인입니다.
**대응의 우선순위** (자세한 설계는 [리소스 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/ops/10-resource-optimization.md)):
1. **애플리케이션이 인식하는 CPU 수를 limit에 맞춥니다.** 가장 자주 놓치는 근본 원인입니다 — 컨테이너 안의 런타임이 노드의 전체 코어 수를 보고 그만큼 스레드를 만들면, 할당량을 순식간에 소진합니다. JVM `-XX:ActiveProcessorCount`, Go `GOMAXPROCS`, Node.js `UV_THREADPOOL_SIZE` 등을 limit에 맞춰 설정합니다
2. **limit을 올립니다** — 노드 안정성과의 트레이드오프
3. **지연에 극히 민감하면 limit 제거를 검토합니다** — 단 노이지 네이버 위험을 request와 노드 분리로 관리해야 합니다
4. `cpu.pressure` PSI로 실제 대기 시간을 확인합니다
**CPU Manager의 정적 정책**(전용 코어 할당)은 지연 민감 워크로드에 유효한 별개 수단입니다. 다만 노드 리소스 활용률이 내려가므로 근거가 필요합니다.
## 메모리 — OOM과 압박
### 무엇을 조정하고 무엇을 두는가
| 파라미터 | 권고 |
|---|---|
| `vm.swappiness` | Kubernetes는 전통적으로 swap 비활성을 전제. swap 지원이 성숙해 왔으나 **EKS에서 켜기 전에 지원 상태를 확인**해야 함 |
| `vm.overcommit_memory` | **기본값 유지 권장.** 바꾸면 컨테이너 할당 실패 양상이 예측하기 어려워짐 |
| `vm.min_free_kbytes` | 회수 여유 공간. 너무 낮으면 급격한 할당에서 OOM. **노드 메모리가 크고 버스트가 심할 때만** 검토 |
| `vm.max_map_count` | **Elasticsearch/OpenSearch 등에서 실제로 필요한 조정.** 기본값이 낮아 mmap 한도에 걸림 |
| `kernel.pid_max` | 고밀도 노드에서 PID 고갈 시 |
`vm.max_map_count`가 실제 사례로 자주 등장합니다 — OpenSearch 계열은 많은 파일을 mmap하므로 기본값에서 시작 실패합니다. 이것은 "근거 있는 튜닝"의 좋은 예입니다: 증상이 명확하고, 해당 파라미터가 직접 원인이며, 벤더 문서가 값을 제시합니다.
### kubelet의 예약 — 커널 파라미터보다 먼저
노드 안정성에서 커널 튜닝보다 효과가 큰 것이 **kubelet의 리소스 예약**입니다.
| 설정 | 용도 |
|---|---|
| `--system-reserved` | OS·시스템 데몬용 예약 |
| `--kube-reserved` | kubelet·컨테이너 런타임용 예약 |
| `--eviction-hard` | 이 임계에 닿으면 Pod 축출 |
예약이 부족하면 Pod가 노드 메모리를 다 먹고 **커널이나 kubelet 자체가 OOM에 걸립니다.** 이 경우 노드가 `NotReady`가 되고 그 위의 모든 Pod가 영향을 받습니다 — 개별 Pod OOM보다 훨씬 나쁜 결과입니다.
**축출이 OOM보다 낫습니다.** 축출은 Kubernetes가 통제된 방식으로 Pod를 옮기는 것이고, OOM killer는 커널이 프로세스를 갑자기 죽이는 것입니다. `--eviction-hard`를 적절히 설정해 커널 OOM 전에 Kubernetes가 개입하게 만드는 것이 목표입니다.
### PSI로 압박 관측
cgroup v2의 PSI가 사용량보다 나은 신호를 줍니다.
```bash
# 노드 전체
cat /proc/pressure/memory
cat /proc/pressure/cpu
cat /proc/pressure/io
# 특정 cgroup
: "${KERNEL_CGROUP_PATH:?조회할 cgroup directory를 지정하세요}"
cat "$KERNEL_CGROUP_PATH/memory.pressure"
```
`some avg10`은 최근 10초간 **최소 하나의 태스크가 그 자원 때문에 지연된 시간의 비율**입니다. 사용량 그래프가 평온한데 이 값이 올라가고 있으면 회수나 경합에 시간을 쓰고 있다는 뜻입니다.
## 네트워크 — 근거 있는 조정 항목
### conntrack
앞서 다룬 대로 **가장 자주 실제 장애를 만드는 항목**입니다.
| 항목 | 내용 |
|---|---|
| 증상 | 새 연결이 조용히 드롭. 애플리케이션은 타임아웃/refused만 봄 |
| 함께 확인할 증거 | `conntrack -S`의 **`insert_failed`** 증가. 이것만으로 table 포화를 확정하지 않음 |
| 보조 신호 | `dmesg`의 `nf_conntrack: table full`, `nf_conntrack_count` / `nf_conntrack_max` 비율 |
| 조정 경로 | **`kube-proxy-config` ConfigMap의 `conntrack.maxPerCore` / `conntrack.min`** (EKS에서 이것이 우선) |
| 비용 | 항목당 노드 메모리. 무한정 올릴 수 없음 |
| 근본 대응 | 연결 churn 감소와 dataplane/map 압력 조사. Headless DNS만으로 tracking이 꺼지지 않음 |
**`maxPerCore`를 쓰는 이유**를 알아둘 가치가 있습니다. 절대값이 아니라 코어당 값이라, 노드 크기가 달라도 같은 설정으로 비례 조정됩니다. 절대값(`nf_conntrack_max`)을 직접 박으면 작은 노드에서는 과다, 큰 노드에서는 부족해집니다.
타임아웃도 조정 대상입니다 — `nf_conntrack_tcp_timeout_established`(기본이 매우 길다)를 줄이면 항목이 빨리 회수됩니다. 단 정상적인 장수명 연결이 끊기지 않도록 주의해야 합니다.
### 소켓 버퍼와 큐
| 파라미터 | 언제 |
|---|---|
| `net.core.somaxconn` | **accept 큐 오버플로 시.** 연결 폭주를 받는 서버에서 흔한 조정 |
| `net.ipv4.tcp_max_syn_backlog` | SYN 폭주 시 |
| `net.core.netdev_max_backlog` | **수신 softirq가 못 따라갈 때** |
| `net.ipv4.tcp_rmem` / `tcp_wmem` | TCP 크기 범위/기본값. 변경 자체가 autotuning을 끄지는 않으며 BDP·메모리 실측 근거로만 조정 |
| `net.ipv4.ip_local_port_range` | **출발지 포트 고갈 시.** egress가 많은 노드에서 실제로 발생 |
| `net.ipv4.tcp_tw_reuse` | TIME_WAIT 누적 시. 거동을 이해하고 적용 |
**`somaxconn`과 `ip_local_port_range`가 실무에서 근거 있는 조정의 대표 사례**입니다. 전자는 accept 큐 오버플로 카운터(`nstat`의 `TcpExtListenOverflows`)로 증거를 잡을 수 있고, 후자는 포트 고갈이 연결 실패로 직접 나타납니다.
실측 근거가 없으면 적절한 `tcp_rmem`/`tcp_wmem` 범위를 유지합니다. Linux 문서는 명시적 **SO_RCVBUF/SO_SNDBUF socket 설정**이 해당 socket autotuning을 끈다고 설명합니다. 이것을 sysctl min/default/max 설정과 혼동하면 안 됩니다.
### qdisc
노드 내 드롭이 확인되면(`tc -s qdisc`의 `dropped`) 조정 대상입니다.
- **`fq_codel`**: 버퍼블로트 완화 — 지연이 문제일 때
- **`fq`**: 페이싱 — bbr과 함께 쓸 때
- 큐 길이(`txqueuelen`)를 늘리면 드롭은 줄지만 **지연이 늘어납니다.** 트레이드오프를 인지하고 결정해야 합니다
### 인터럽트 분산
`/proc/interrupts`에서 특정 코어 편중이 보이고 `mpstat -P ALL`의 `%soft`가 그 코어에서 튀면 RSS/RPS/RFS 설정을 봅니다. 다만 **최신 ENA 드라이버와 인스턴스 타입은 다중 큐와 RSS가 기본 구성**이라, 대개 문제가 되지 않습니다.
### kube-proxy 모드
노드 커널 파라미터는 아니지만 데이터패스 성능에 가장 큰 영향을 줍니다.
| 상황 | 권고 |
|---|---|
| Service 수가 많고 iptables 모드 | **nftables 모드 검토** — 1.33에서 GA, O(1) 조회 + 증분 갱신. 커널 5.13+ 필요(AL2023은 충족) |
| **IPVS 모드 사용 중** | **이전 계획 필요** — 1.35에서 deprecated; upstream은 1.40 기본 비활성화·1.43 제거 계획. 권장 대체는 nftables |
| 기본값 유지 | nftables가 GA여도 **기본은 여전히 iptables** — 전환은 명시적 결정 |
## 스토리지
| 파라미터 | 내용 |
|---|---|
| **I/O 스케줄러** | NVMe는 `none`(또는 `mq-deadline`)이 일반적. NVMe에서 복잡한 스케줄러는 이점이 적음 |
| `vm.dirty_ratio` / `dirty_background_ratio` | 쓰기 버퍼링 양. 쓰기 폭주 시 지연 특성에 영향 |
| **ephemeral storage** | 커널 파라미터보다 **overlayfs copy-up 비용**이 실제 문제 — 쓰기 많은 경로는 볼륨으로 분리 ([컨테이너 커널 기능](https://www.atomai.click/kubernetes-docs/llms/ko/kernel/01-container-primitives.md)) |
| **EBS 성능** | 커널이 아니라 볼륨 타입·IOPS·throughput 설정의 문제 ([EBS gp2 vs gp3 실측](https://www.atomai.click/kubernetes-docs/llms/ko/storage/01-ebs-gp2-gp3-benchmark.md)) |
## 워크로드별 정리
증상에서 출발하는 표입니다.
| 워크로드 | 자주 필요한 조정 | 근거 카운터 |
|---|---|---|
| **고연결 게이트웨이·프록시** | conntrack 상한, `somaxconn`, `ip_local_port_range` | `insert_failed`, `TcpExtListenOverflows`, 포트 고갈 |
| **지연 민감 (거래·실시간)** | CPU limit 재검토, CPU Manager 정적 정책, `fq_codel` | `cpu.stat` throttling, `cpu.pressure` |
| **대용량 처리 (배치·데이터)** | 버퍼 상한(장거리만), `netdev_max_backlog` | qdisc `dropped`, softirq 편중 |
| **검색·색인 (OpenSearch 등)** | **`vm.max_map_count`**, 파일 디스크립터 한도 | 시작 실패 로그 |
| **고밀도 노드** | `kernel.pid_max`, kubelet 예약, 축출 임계 | PID 고갈, 노드 `NotReady` |
| **블록체인 노드** | 파일 디스크립터, 디스크 IOPS, 소켓 버퍼 | [블록체인 노드 운영](https://www.atomai.click/kubernetes-docs/llms/ko/blockchain/02-nodes-on-eks.md) |
## 변경을 어떻게 관리할 것인가
튜닝 자체보다 **관리 방식이 장기적으로 더 중요합니다.**
| 원칙 | 이유 |
|---|---|
| **코드로 관리** (Karpenter `EC2NodeClass`, 시작 템플릿, Bottlerocket 설정) | 노드마다 값이 다른 상황을 막음 |
| **변경 사유를 주석으로 남김** | "왜 이 값인가"를 6개월 뒤에 알 수 없으면 아무도 되돌리지 못함 |
| **노드 그룹을 분리** | 워크로드 성격이 다르면 튜닝도 달라야 함. 한 프로필을 전체에 강요하지 않음 |
| **커널 버전을 고정하거나 전환을 계획** | `kernel-default` AMI는 조용히 커널이 바뀜 |
| **적용 후 실제 값 검증** | 특히 conntrack — 다른 주체가 덮어쓸 수 있음 |
| **변경 전후 같은 조건으로 측정** | 측정 없는 튜닝은 미신이 됨 |
## 정리
- **대부분은 기본값을 두십시오.** 커널은 부하에 따라 자동 조정하고 있고, 근거 없는 튜닝은 재현 불가능한 구성과 커널 업그레이드 시 파손을 만듭니다.
- 튜닝의 전제는 측정입니다. **드롭 카운터부터 보십시오** — `insert_failed`, qdisc `dropped`, NIC 드롭.
- 선택한 AMI와 실행 중 kernel을 확인합니다. 기본 AMI 계열은 바뀔 수 있지만 고정 AMI ID가 노드 교체 시 자동 변경되는 것은 아닙니다.
- CPU throttling의 근본 원인은 대개 **애플리케이션이 인식하는 CPU 수와 할당량의 불일치**입니다. `GOMAXPROCS`/`ActiveProcessorCount`부터 맞추십시오.
- 노드 안정성에는 커널 튜닝보다 **kubelet 예약과 축출 임계**가 효과적입니다. 축출이 커널 OOM보다 낫습니다.
- 근거 있는 조정의 대표 사례는 **conntrack 상한, `somaxconn`, `ip_local_port_range`, `vm.max_map_count`**입니다. 모두 직접적인 증거 카운터가 있습니다.
- TCP sysctl 범위와 socket별 autotuning override는 다르며 측정 근거로만 조정합니다.
- **IPVS 모드를 쓰고 있으면 이전 계획이 필요합니다** (1.35 deprecated; 1.40 기본 비활성화·1.43 제거 계획). Rollout 전에 아래 KEP-5495 링크에서 최신 일정을 확인합니다.
## 참고 자료
- [Amazon Linux 2023 — Updating the Linux Kernel](https://docs.aws.amazon.com/linux/al2023/ug/kernel-update.html)
- [Amazon EKS-Optimized Amazon Linux 2023 AMIs](https://aws.amazon.com/blogs/containers/amazon-eks-optimized-amazon-linux-2023-amis-now-available/)
- [Increase nf_conntrack_max limit on EKS nodes](https://repost.aws/knowledge-center/eks-increase-nf-conntrack-max-limit)
- [Running kube-proxy in nftables Mode — EKS Best Practices](https://docs.aws.amazon.com/eks/latest/best-practices/nftables.html)
- [KEP-5495: Deprecate IPVS mode in kube-proxy](https://github.com/kubernetes/enhancements/blob/master/keps/sig-network/5495-deprecate-ipvs-mode-in-kube-proxy/README.md)
- [EEVDF Scheduler — Linux kernel documentation](https://docs.kernel.org/scheduler/sched-eevdf.html)
- [kernel/sched/debug.c — debugfs 튜너블 정의](https://github.com/torvalds/linux/blob/master/kernel/sched/debug.c)
- [bottlerocket-os/bottlerocket#4221 — conntrack limit not applied](https://github.com/bottlerocket-os/bottlerocket/issues/4221)
- [PSI - Pressure Stall Information](https://docs.kernel.org/accounting/psi.html)
- [Reserve Compute Resources for System Daemons (Kubernetes)](https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/)
- [Using sysctls in a Kubernetes Cluster](https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/)
- [리소스 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/ops/10-resource-optimization.md) / [Pod 네트워크 실측 벤치마크](https://www.atomai.click/kubernetes-docs/llms/ko/networking/06-pod-network-benchmark.md)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/basics/04-kubernetes-introduction
----------------------------------------
# Kubernetes 소개
> **지원 버전**: Upstream Kubernetes 1.35, 1.36, 1.37; EKS 표준 지원 1.34–1.36 (2026-09-11) **마지막 업데이트**: 2026년 9월 11일
Kubernetes(K8s)는 컨테이너화된 애플리케이션의 배포, 확장 및 관리를 자동화하는 오픈소스 컨테이너 오케스트레이션 플랫폼입니다. 이 문서에서는 Kubernetes의 기본 개념, 아키텍처, 주요 구성 요소 및 기능에 대해 설명합니다.
이 문서의 매니페스트는 서로 독립적인 학습 예제이며 문법/스키마와 공식 문서를 기준으로 검토했습니다. 실제 클러스터에서 배포 검증한 프로덕션 구성은 아닙니다. 커스텀 이미지, 이름/레이블, TLS, IAM/RBAC, CNI 및 스토리지 요구를 대상 환경에서 확인하고 자리표시자를 바꿔야 합니다.
## 실습 환경 설정
이 문서의 예제를 따라하기 위해서는 다음과 같은 도구와 환경이 필요합니다:
### 필수 도구
* **kubectl**: Kubernetes 클러스터와 상호 작용하는 명령줄 도구
* **로컬 클러스터 드라이버**: minikube/kind가 지원하는 컨테이너 엔진/VM 드라이버; Kubernetes 노드는 CRI v1 런타임 사용
* **minikube** 또는 **kind**: 로컬 Kubernetes 클러스터 (개발 및 학습용)
### 설치 방법
**kubectl 설치**:
```bash
# macOS: use a kubectl version within one minor of the API server.
brew install kubectl
```
```bash
# Linux: select an explicit compatible version and architecture.
set -euo pipefail
: "${KUBECTL_VERSION:?Set a cluster-compatible version, e.g. v1.37.0}"
case "$(uname -m)" in
x86_64) KUBECTL_ARCH=amd64 ;;
aarch64|arm64) KUBECTL_ARCH=arm64 ;;
*) echo "Choose a supported kubectl architecture" >&2; exit 1 ;;
esac
curl --fail --location --output kubectl "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/$KUBECTL_ARCH/kubectl"
curl --fail --location --output kubectl.sha256 "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/$KUBECTL_ARCH/kubectl.sha256"
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
sudo install -m 0755 kubectl /usr/local/bin/kubectl
```
```powershell
$ErrorActionPreference = "Stop"
$KubectlVersion = Read-Host "Cluster-compatible kubectl version (vX.Y.Z)"
$KubectlArch = Read-Host "Architecture (amd64 or arm64)"
if ($KubectlVersion -notmatch '^v\d+\.\d+\.\d+$' -or $KubectlArch -notin @('amd64','arm64')) { throw "Invalid version/architecture" }
$BaseUrl = "https://dl.k8s.io/release/$KubectlVersion/bin/windows/$KubectlArch"
Invoke-WebRequest "$BaseUrl/kubectl.exe" -OutFile kubectl.exe
Invoke-WebRequest "$BaseUrl/kubectl.exe.sha256" -OutFile kubectl.exe.sha256
if ((Get-FileHash kubectl.exe -Algorithm SHA256).Hash -ne (Get-Content kubectl.exe.sha256).Trim()) { throw "Checksum mismatch" }
# Move the verified binary to a directory included in PATH.
```
**minikube 설치**:
minikube의 공식 시작 안내에서 OS/아키텍처/드라이버에 맞는 바이너리와 체크섬을 선택합니다. Linux와 Windows 명령은 각각 해당 셸에서 실행하며 검증한 바이너리를 PATH에 추가합니다. macOS 예시는 다음과 같습니다:
```bash
brew install minikube
minikube version
```
### 로컬 클러스터 시작
```bash
minikube start
```
## 목차
* [Kubernetes란?](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md#kubernetes란)
* [Kubernetes의 역사](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md#kubernetes의-역사)
* [Kubernetes 아키텍처](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md#kubernetes-아키텍처)
* [Kubernetes 주요 구성 요소](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md#kubernetes-주요-구성-요소)
* [Kubernetes 기본 객체](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md#kubernetes-기본-객체)
* [Kubernetes 워크로드 리소스](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md#kubernetes-워크로드-리소스)
* [Kubernetes 서비스와 네트워킹](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md#kubernetes-서비스와-네트워킹)
* [Kubernetes 스토리지](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md#kubernetes-스토리지)
* [Kubernetes 구성 및 보안](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md#kubernetes-구성-및-보안)
* [Kubernetes vs Amazon EKS](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md#kubernetes-vs-amazon-eks)
* [Kubernetes 시작하기](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md#kubernetes-시작하기)
## Kubernetes란?
Kubernetes는 그리스어로 '조타수' 또는 '파일럿'을 의미하며, 컨테이너화된 애플리케이션의 배포, 확장, 운영을 자동화하는 오픈소스 시스템입니다. Google에서 내부적으로 사용하던 Borg 시스템에서 영감을 받아 개발되었으며, 2014년에 오픈소스로 공개되었습니다.
### Kubernetes의 주요 기능
1. **서비스 디스커버리와 로드 밸런싱**: 컨테이너를 외부에 노출하고 트래픽을 분산
2. **스토리지 오케스트레이션**: 로컬 또는 클라우드 스토리지 시스템을 자동으로 마운트
3. **롤아웃과 롤백**: 애플리케이션을 점진적으로 업데이트하고 운영자/도구가 롤백할 수 있습니다. Deployment 실패만으로 자동 롤백하지는 않습니다.
4. **자동 빈 패킹**: 리소스 요구사항에 따라 컨테이너를 노드에 배치
5. **자가 복구**: 실패한 컨테이너를 재시작하고, 응답하지 않는 컨테이너를 교체
6. **시크릿과 구성 관리**: 민감한 정보를 저장하고 구성 정보를 업데이트할 수 있음
7. **수평적 확장**: 간단한 명령이나 UI를 통해 애플리케이션을 확장
8. **배치 실행**: 배치 및 CI 워크로드 관리
### Kubernetes가 해결하는 문제
* **컨테이너 오케스트레이션**: 수백, 수천 개의 컨테이너를 효율적으로 관리
* **고가용성**: 복제본, 배치, probe 및 용량을 구성하여 복원력 있는 애플리케이션 설계 지원
* **확장성**: 트래픽 증가에 따른 자동 확장
* **복구**: 실패한 워크로드를 조정하며 재해 복구에는 검증한 백업/복원 계획도 필요
* **리소스 효율성**: 하드웨어 리소스를 효율적으로 활용
* **선언적 구성**: 인프라를 코드로 관리
* **멀티 클라우드 및 하이브리드 클라우드**: 다양한 환경에서 일관된 배포 및 관리
## Kubernetes의 역사
### 탄생 배경
* **2003-2013**: Google은 내부적으로 Borg라는 컨테이너 오케스트레이션 시스템을 사용
* **2014년 6월**: Google이 Kubernetes 프로젝트를 오픈소스로 공개
* **2015년 7월**: Kubernetes 1.0 출시 및 Cloud Native Computing Foundation(CNCF)에 기부
* **2016-2017**: 주요 클라우드 제공업체들이 관리형 Kubernetes 서비스 출시
* **2018년 이후**: 컨테이너 오케스트레이션의 사실상 표준으로 자리매김
### 이름의 유래
Kubernetes(κυβερνήτης)는 그리스어로 '조타수' 또는 '파일럿'을 의미합니다. 이는 컨테이너화된 애플리케이션의 항해를 안내하는 역할을 상징합니다. K8s라는 약어는 'K'와 's' 사이에 8개의 문자가 있기 때문에 사용됩니다.
### 로고의 의미
Kubernetes의 로고는 7개의 스포크가 있는 항해용 방향타(helm)를 형상화했으며, 이는 컨테이너화된 애플리케이션의 항로를 안내하는 Kubernetes의 역할을 상징합니다.
## Kubernetes 아키텍처
Kubernetes는 마스터-노드 아키텍처를 따릅니다. 마스터 노드(컨트롤 플레인)는 클러스터를 관리하고, 워커 노드는 실제 애플리케이션 워크로드를 실행합니다.
### 컨트롤 플레인 (마스터) 구성 요소

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-04-kubernetes-introduction-0.html)
1. **kube-apiserver**: Kubernetes API를 노출하는 컨트롤 플레인의 프론트엔드
2. **etcd**: Kubernetes API 객체와 클러스터 상태(애플리케이션 볼륨 내용 제외)를 저장하는 일관성 있고 고가용성을 갖춘 키-값 저장소
3. **kube-scheduler**: 노드에 파드를 할당하는 구성 요소
4. **kube-controller-manager**: 컨트롤러 프로세스를 실행하는 구성 요소
* 노드 컨트롤러: 노드가 다운되었을 때 알림 및 대응
* 레플리케이션 컨트롤러: 파드 복제본의 올바른 수를 유지
* EndpointSlice 컨트롤러: Service 엔드포인트 정보 유지(기존 Endpoints는 deprecated)
* ServiceAccount 컨트롤러: 기본 계정 생성; 현대 Pod 토큰은 TokenRequest와 kubelet 갱신 사용
5. **cloud-controller-manager**: 클라우드별 컨트롤 로직을 포함하는 구성 요소
* 노드 컨트롤러: 클라우드 제공자에게 노드가 삭제되었는지 확인
* 라우트 컨트롤러: 클라우드 인프라에서 라우트 설정
* 서비스 컨트롤러: 클라우드 제공자 로드 밸런서 생성, 업데이트, 삭제
### 노드 구성 요소

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-04-kubernetes-introduction-1.html)
1. **kubelet**: 각 노드에서 실행되는 에이전트로, 파드 내 컨테이너가 실행되도록 관리
2. **kube-proxy**: 각 노드에서 실행되는 네트워크 프록시로, Kubernetes 서비스 개념의 구현을 담당
3. **컨테이너 런타임**: containerd/CRI-O 등 CRI v1 구현체; Docker Engine에는 별도 CRI 어댑터 필요
### 전체 아키텍처

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-04-kubernetes-introduction-2.html)
## Kubernetes 주요 구성 요소
### API 서버 (kube-apiserver)
API 서버는 Kubernetes API를 노출하는 컨트롤 플레인의 프론트엔드입니다. Kubernetes API 요청을 처리하며 애플리케이션 트래픽과 스토리지 I/O가 API 서버를 거치는 것은 아닙니다.
**주요 기능**:
* REST API 제공
* 인증 및 권한 부여
* 요청 검증
* etcd와의 통신
* 수평적 확장 가능
### etcd
etcd는 Kubernetes API 객체와 클러스터 상태(애플리케이션 볼륨 내용 제외)를 저장하는 일관성 있고 고가용성을 갖춘 키-값 저장소입니다.
**주요 특징**:
* 분산 시스템
* 강한 일관성
* 고가용성
* 안전한 데이터 저장
* 워치(watch) 기능으로 변경 사항 모니터링
### 스케줄러 (kube-scheduler)
스케줄러는 새로 생성된 파드를 실행할 노드를 선택하는 컨트롤 플레인 구성 요소입니다.
**스케줄링 과정**:
1. **필터링**: 파드를 실행할 수 있는 노드 식별
2. **스코어링**: 적합한 노드에 점수 부여
3. **바인딩**: 최적의 노드에 파드 할당
**고려 요소**:
* 리소스 요구사항 (CPU, 메모리)
* 하드웨어/소프트웨어/정책 제약 조건
* 어피니티/안티-어피니티 명세
* 데이터 지역성
* 워크로드 간섭
### 컨트롤러 매니저 (kube-controller-manager)
컨트롤러 매니저는 여러 컨트롤러 프로세스를 실행하는 컨트롤 플레인 구성 요소입니다.
**주요 컨트롤러**:
* **노드 컨트롤러**: 노드 상태 모니터링 및 대응
* **레플리케이션 컨트롤러**: 파드 복제본 수 유지
* **EndpointSlice 컨트롤러**: Service 엔드포인트 정보 유지(기존 Endpoints는 deprecated)
* **ServiceAccount 컨트롤러**: 기본 계정 생성; 현대 Pod 토큰은 TokenRequest와 kubelet 갱신 사용
* **잡 컨트롤러**: 일회성 작업 관리
* **크론잡 컨트롤러**: 예약된 작업 관리
* **DaemonSet 컨트롤러**: 각 적합한 노드의 Pod를 조정
* **스테이트풀셋 컨트롤러**: 상태 유지 애플리케이션 관리
* **PV 컨트롤러**: 영구 볼륨 관리
### 클라우드 컨트롤러 매니저 (cloud-controller-manager)
클라우드 컨트롤러 매니저는 클라우드별 컨트롤 로직을 포함합니다. 스토리지 생성/연결/마운트는 CSI sidecar/드라이버 및 kubelet/노드 플러그인의 역할이며 CCM 볼륨 컨트롤러의 역할이 아닙니다.
**주요 컨트롤러**:
* **노드 컨트롤러**: 클라우드 제공자 API를 통해 노드 상태 확인
* **라우트 컨트롤러**: 클라우드 환경에서 라우트 설정
* **서비스 컨트롤러**: 클라우드 로드 밸런서 생성, 업데이트, 삭제
### kubelet
kubelet은 각 노드에서 실행되는 에이전트로, 파드 내 컨테이너가 실행되도록 관리합니다.
**주요 기능**:
* PodSpec(파드 명세)에 따라 컨테이너 실행
* 컨테이너 상태 보고
* 컨테이너 헬스 체크 수행
* 컨테이너 라이프사이클 관리
* 노드 상태 보고
### kube-proxy
kube-proxy는 각 노드에서 실행되는 네트워크 프록시로, Kubernetes 서비스 개념의 구현을 담당합니다.
**주요 기능**:
* 서비스 IP 및 포트에 대한 네트워크 규칙 유지
* 연결 포워딩
* 로드 밸런싱 구현
**작동 모드**:
* **nftables 모드**: 1.33부터 stable이며 커널/네트워크 플러그인 호환성 확인
* **iptables 모드**: 리눅스 iptables를 사용한 NAT 구현 (기본)
* **IPVS 모드**: 1.35부터 deprecated이므로 전환 계획 필요. 과거 userspace 모드는 제거됨
## Kubernetes 기본 객체
Kubernetes 객체는 클러스터의 상태를 나타내는 영구적인 엔티티입니다. 이러한 객체는 클러스터에서 실행 중인 애플리케이션, 사용 가능한 리소스, 정책 등을 설명합니다.
### 파드 (Pod)
파드는 Kubernetes의 가장 작은 배포 단위로, 하나 이상의 컨테이너 그룹을 나타냅니다. Pod의 컨테이너는 네트워크와 명시적으로 마운트한 볼륨을 공유하고 같은 노드에서 실행됩니다. 루트 파일 시스템이 자동 공유되지는 않습니다.
**주요 특징**:
* 고유한 IP 주소 보유
* 공유 네트워크 네임스페이스 (동일한 IP 및 포트 공간)
* 공유 IPC 네임스페이스
* 공유 호스트네임
* 컨테이너 간 로컬호스트 통신 가능
**파드 예시**:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.30.4
ports:
- containerPort: 80
volumeMounts:
- name: logs
mountPath: /var/log/nginx
- name: log-sidecar
image: busybox:1.37.0
command:
- /bin/sh
- -c
- until [ -f /var/log/nginx/access.log ]; do sleep 1; done; tail -F /var/log/nginx/access.log
volumeMounts:
- name: logs
mountPath: /var/log/nginx
readOnly: true
volumes:
- name: logs
emptyDir: {}
```
### 네임스페이스 (Namespace)
네임스페이스는 단일 클러스터 내에서 리소스 그룹을 격리하는 방법을 제공합니다. 여러 팀/프로젝트 구분에 유용하지만 네임스페이스만으로 네트워크/권한 격리가 강제되지는 않습니다.
**기본 네임스페이스**:
* **default**: 기본 네임스페이스
* **kube-system**: Kubernetes 시스템에서 생성한 객체를 위한 네임스페이스
* **kube-public**: 공개 정보를 위한 관례적 네임스페이스이며 실제 객체 접근은 RBAC에 따름
* **kube-node-lease**: 노드 하트비트를 위한 네임스페이스
**네임스페이스 예시**:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: development
```
### 레이블 (Labels)과 셀렉터 (Selectors)
레이블은 객체에 연결된 키-값 쌍으로, 객체를 식별하고 선택하는 데 사용됩니다. 셀렉터는 레이블을 기반으로 객체를 필터링하는 방법을 제공합니다.
**레이블 예시**:
```yaml
metadata:
labels:
app: nginx
environment: production
tier: frontend
```
**셀렉터 유형**:
* **동등성 기반**: `=`, `!=`
* **집합 기반**: `in`, `notin`, `exists`
**셀렉터 예시**:
```yaml
selector:
matchLabels:
app: nginx
matchExpressions:
- {key: tier, operator: In, values: [frontend, middleware]}
- {key: environment, operator: NotIn, values: [dev]}
```
### 어노테이션 (Annotations)
어노테이션은 객체에 대한 비식별 메타데이터를 저장하는 키-값 쌍입니다. 어노테이션은 도구나 라이브러리에서 사용하는 정보를 저장하는 데 유용합니다.
**어노테이션 예시**:
```yaml
metadata:
annotations:
example.com/created-by: "admin"
example.com/last-modified: "2023-07-01T12:00:00Z"
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
```
### 노드 (Node)
노드는 Kubernetes 클러스터의 워커 머신으로, 파드를 실행합니다. 노드는 물리적 머신이나 가상 머신일 수 있습니다.
**노드 상태**:
* **주소**: 호스트 이름, 내부 IP, 외부 IP
* **컨디션**: Ready, DiskPressure, MemoryPressure, PIDPressure, NetworkUnavailable
* **용량**: CPU, 메모리, 최대 파드 수
* **정보**: 커널 버전, 컨테이너 런타임 버전, kubelet 버전
**Node 상태 예시(노드/컨트롤러가 보고하며 노드 생성용 매니페스트가 아님)**:
```yaml
apiVersion: v1
kind: Node
metadata:
name: worker-1
labels:
kubernetes.io/hostname: worker-1
node-role.kubernetes.io/worker: ""
topology.kubernetes.io/zone: us-east-1a
status:
capacity:
cpu: "4"
memory: 8Gi
pods: "110"
conditions:
- type: Ready
status: "True"
# ...
```
## Kubernetes 워크로드 리소스
워크로드 리소스는 파드를 관리하고 실행하는 데 사용되는 객체입니다. 이러한 리소스는 파드의 생성, 확장, 업데이트, 종료를 관리합니다.
### 레플리카셋 (ReplicaSet)
ReplicaSet은 원하는 Pod 객체 수를 조정하며 준비 상태는 용량, 올바른 설정 및 애플리케이션에 달려 있습니다. 파드가 실패하거나 삭제되면 레플리카셋은 자동으로 대체 파드를 생성합니다.
**주요 기능**:
* 지정된 수의 파드 복제본 유지
* 파드 템플릿 정의
* 셀렉터를 통한 파드 식별
**레플리카셋 예시**:
```yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx-replicaset
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.30.4
ports:
- containerPort: 80
```
### 디플로이먼트 (Deployment)
디플로이먼트는 레플리카셋을 한 단계 더 추상화하여 애플리케이션의 선언적 업데이트를 제공합니다. 디플로이먼트는 롤링 업데이트, 롤백, 스케일링 등의 기능을 제공합니다.
**주요 기능**:
* 선언적 애플리케이션 업데이트
* 롤링 업데이트 및 롤백
* 배포 이력 관리
* 스케일링
**디플로이먼트 예시**:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.30.4
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
```
아래 MySQL은 단일 인스턴스 영속 저장 예제입니다. StatefulSet이 복제/장애 조치/백업을 자동 구성하지 않습니다. mysql-secret의 password 키와 실제 StorageClass를 먼저 준비하고 자리표시자를 바꿉니다. 복제본만 늘리면 독립 데이터베이스가 생기므로 HA는 검증한 DB 오퍼레이터/복제 구성이 필요합니다.
### 스테이트풀셋 (StatefulSet)
스테이트풀셋은 상태 유지가 필요한 애플리케이션을 위한 워크로드 리소스입니다. 각 파드에 고유한 식별자를 부여하고, 안정적인 네트워크 식별자와 영구 스토리지를 제공합니다.
**주요 기능**:
* 안정적이고 고유한 네트워크 식별자
* 안정적이고 영구적인 스토리지
* 순차적인 배포 및 스케일링
* 순차적인 업데이트
**스테이트풀셋 예시**:
```yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None
selector:
app: mysql
ports:
- name: mysql
port: 3306
targetPort: 3306
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
serviceName: mysql
replicas: 1
template:
metadata:
labels:
app: mysql
role: db
spec:
containers:
- name: mysql
image: mysql:8.4
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
readinessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 10
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
storageClassName: replace-with-storage-class
resources:
requests:
storage: 10Gi
```
아래 Linux Fluent Bit 예제는 CRI 로그를 stdout으로 출력하는 실습용입니다. 자체 로그는 제외하여 재수집 루프를 막으며 상태 DB를 별도 경로에 유지합니다. 같은 로그를 다시 stdout으로 내보내는 다른 수집기와 함께 배포하지 않습니다. 운영 환경은 중앙 로그 저장소로 전달하고 호스트 경로/권한/PSS 예외를 검토합니다.
### 데몬셋 (DaemonSet)
DaemonSet은 각 적합한 노드에 Pod를 생성하며 nodeSelector, taint, 용량 및 admission 정책의 영향을 받습니다. 노드가 클러스터에 추가되면 파드가 자동으로 추가되고, 노드가 제거되면 파드도 제거됩니다.
**주요 사용 사례**:
* 로그 수집기 (Fluentd, Logstash)
* 모니터링 에이전트 (Prometheus Node Exporter)
* 네트워크 플러그인 (Calico, Cilium)
* 스토리지 데몬 (Ceph)
**데몬셋 예시**:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: intro-log-agent-config
namespace: kube-system
data:
fluent-bit.conf: |
[SERVICE]
Flush 5
Parsers_File /fluent-bit/etc/parsers.conf
[INPUT]
Name tail
Path /var/log/containers/*.log
Exclude_Path /var/log/containers/intro-log-agent-*_kube-system_fluent-bit-*.log
Parser cri
Tag kube.*
DB /var/lib/fluent-bit/tail.db
Mem_Buf_Limit 5MB
Skip_Long_Lines On
[OUTPUT]
Name stdout
Match *
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: intro-log-agent
namespace: kube-system
spec:
selector:
matchLabels:
app: intro-log-agent
template:
metadata:
labels:
app: intro-log-agent
spec:
automountServiceAccountToken: false
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: fluent-bit
image: cr.fluentbit.io/fluent/fluent-bit:5.1.2
securityContext:
runAsUser: 0
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
args:
- -c
- /fluent-bit/custom/fluent-bit.conf
resources:
requests:
cpu: 100m
memory: 100Mi
limits:
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: config
mountPath: /fluent-bit/custom
readOnly: true
- name: state
mountPath: /var/lib/fluent-bit
volumes:
- name: varlog
hostPath:
path: /var/log
type: Directory
- name: config
configMap:
name: intro-log-agent-config
- name: state
hostPath:
path: /var/lib/intro-log-agent
type: DirectoryOrCreate
nodeSelector:
kubernetes.io/os: linux
```
### 잡 (Job)
잡은 하나 이상의 파드를 생성하고 지정된 수의 파드가 성공적으로 종료될 때까지 실행을 계속합니다. 배치 처리 작업에 적합합니다.
**주요 기능**:
* 일회성 작업 실행
* 병렬 작업 실행
* 성공 완료 횟수를 추적하며 실패/기한 제한에 따라 Job이 실패할 수 있음
* 실패 시 재시도
**잡 예시**:
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: pi-calculator
spec:
completions: 5
parallelism: 2
backoffLimit: 3
template:
spec:
containers:
- name: pi
image: perl
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
```
Job은 실패/기한 제한으로 실패할 수 있고 동일 작업이 재실행될 수 있으므로 멱등성을 확보합니다. CronJob 스케줄도 정확히 한 번 실행을 보장하지 않으며 Forbid는 해당 CronJob의 겹치는 실행만 제어합니다.
### 크론잡 (CronJob)
크론잡은 지정된 일정에 따라 잡을 주기적으로 실행합니다. 리눅스 크론 작업과 유사한 방식으로 작동합니다.
**주요 기능**:
* 일정에 따른 작업 실행
* 크론 표현식 지원
* 동시성 정책 설정
* 이력 제한
**크론잡 예시**:
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: database-backup
spec:
timeZone: Etc/UTC
schedule: "0 2 * * *" # 매일 02:00에 실행
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: database-backup:v1
env:
- name: DB_HOST
value: "db.example.com"
restartPolicy: OnFailure
```
## Kubernetes 서비스와 네트워킹
Kubernetes의 네트워킹 모델은 호환되는 CNI가 Pod 네트워크를 제공하고 실제 통신은 NetworkPolicy/방화벽/토폴로지에 영향을 받는다는 것을 기본 전제로 합니다. 서비스는 파드 집합에 대한 안정적인 엔드포인트를 제공합니다.
### 서비스 (Service)
서비스는 파드 집합에 대한 단일 엔드포인트와 로드 밸런싱을 제공합니다. 파드는 동적으로 생성되고 삭제되므로, 서비스는 이러한 변화에도 불구하고 안정적인 네트워크 주소를 제공합니다.
**서비스 유형**:
* **ClusterIP**: 클러스터 내부에서만 접근 가능한 서비스 (기본값)
* **NodePort**: 각 노드의 IP와 특정 포트를 통해 외부에서 접근 가능
* **LoadBalancer**: 클라우드 제공자의 로드 밸런서를 사용하여 외부에서 접근 가능
* **ExternalName**: 외부 서비스에 대한 CNAME 레코드 생성

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-04-kubernetes-introduction-3.html)
**서비스 예시**:
```yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
type: ClusterIP
```
**NodePort 서비스 예시**:
```yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-nodeport
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
nodePort: 30080
type: NodePort
```
이 AWS 예제는 AWS Load Balancer Controller와 IAM/네트워크 사전 구성이 필요합니다. EKS Auto Mode는 다른 loadBalancerClass를 사용하며 로컬 클러스터는 자체 LoadBalancer 구현이 필요합니다.
**LoadBalancer 서비스 예시**:
```yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-lb
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
```
이 예제는 설치된 Traefik과 traefik IngressClass, app1/app2 Service, TLS Secret이 필요합니다. /app1과 /app2 경로를 그대로 전달하므로 백엔드가 해당 경로를 제공해야 합니다. Ingress 리소스만으로 컨트롤러가 설치되지는 않습니다.
### 인그레스 (Ingress)
인그레스는 클러스터 외부에서 클러스터 내부 서비스로의 HTTP 및 HTTPS 라우팅을 관리하는 API 객체입니다. 인그레스는 로드 밸런싱, SSL 종료, 이름 기반 가상 호스팅 등을 제공합니다.
**인그레스 컨트롤러**:
* **ingress-nginx(2026년 3월 종료)**: 과거 커뮤니티 컨트롤러이며 신규 설치는 유지 관리되는 컨트롤러를 선택합니다. F5 NGINX Ingress Controller는 별도 프로젝트입니다.
* **AWS Load Balancer Controller**: AWS Application Load Balancer 기반 인그레스 컨트롤러
* **Traefik**: 클라우드 네이티브 엣지 라우터
* **Istio Ingress**: 서비스 메시 기반 인그레스
**인그레스 예시**:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
spec:
ingressClassName: traefik
rules:
- host: example.com
http:
paths:
- path: /app1
pathType: Prefix
backend:
service:
name: app1-service
port:
number: 80
- path: /app2
pathType: Prefix
backend:
service:
name: app2-service
port:
number: 80
tls:
- hosts:
- example.com
secretName: example-tls
```
### 네트워크 정책 (NetworkPolicy)
네트워크 정책은 파드 간의 통신을 제어하는 방법을 제공합니다. 기본적으로 모든 파드는 서로 통신할 수 있지만, 네트워크 정책을 사용하면 이를 제한할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-04-kubernetes-introduction-4.html)
**주요 기능**:
* 파드 간 통신 제어
* 네임스페이스 간 통신 제어
* 인그레스(수신) 및 이그레스(송신) 트래픽 제어
* 포트 및 프로토콜 기반 필터링
**네트워크 정책 예시**:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-network-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
role: api
ports:
- protocol: TCP
port: 3306
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app: prometheus
ports:
- protocol: TCP
port: 9104
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
```
NetworkPolicy의 허용 규칙은 합산됩니다. 위 정책은 role=api에서 DB3306으로의 접근과 monitoring 네임스페이스의 app=prometheus에서 DB exporter9104로의 스크레이프를 허용합니다. 9104 exporter는 별도로 설치해야 하며 DB가 Prometheus9090으로 연결하는 규칙이 아닙니다. DNS egress 레이블은 실제 클러스터 DNS/NodeLocal DNS 구성에 맞춥니다.
### DNS
Kubernetes 배포판은 보통 CoreDNS로 서비스 검색을 제공합니다. ConfigMap 수정 시 배포판 관리 설정을 유지합니다. 아래 pods insecure는 Pod 존재를 검증하지 않는 레거시 IP 기반 레코드 모드이며 불필요하면 disabled, 검증이 필요하면 추가 watch/메모리 비용을 고려해 verified를 선택합니다.
**DNS 이름 형식**:
* **서비스**: `<서비스명>.<네임스페이스>.svc.cluster.local`
* **파드**: `<점-대신-하이픈을-쓴-Pod-IP>.<네임스페이스>.pod.cluster.local` (IPv4 레코드; CoreDNS pods 모드에 따라 다름)
**DNS 구성 예시**:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
```
### 서비스 메시 (Service Mesh)
서비스 메시는 마이크로서비스 간의 통신을 관리하는 인프라 레이어입니다. 서비스 메시는 트래픽 관리, 보안, 관찰성 등을 제공합니다.
**주요 서비스 메시**:
* **Istio**: 가장 널리 사용되는 서비스 메시
* **Linkerd**: 경량화된 서비스 메시
* **AWS App Mesh(2026-09-30 지원 종료 예정)**: 마이그레이션이 필요하며 신규 배포 권장 대상은 아님
**Istio 가상 서비스 예시**:
```yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: reviews-route
spec:
hosts:
- reviews
http:
- match:
- headers:
end-user:
exact: jason
route:
- destination:
host: reviews
subset: v2
- route:
- destination:
host: reviews
subset: v1
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: reviews-subsets
spec:
host: reviews
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
```
## Kubernetes 스토리지
Kubernetes는 컨테이너화된 애플리케이션에 다양한 스토리지 옵션을 제공합니다. 파드가 재시작되거나 재스케줄링되더라도 데이터를 유지할 수 있는 방법을 제공합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-04-kubernetes-introduction-5.html)
### 볼륨 (Volume)
볼륨은 파드 내의 컨테이너에 마운트할 수 있는 디렉토리로, 파드의 수명 주기 동안 데이터를 유지합니다. 볼륨은 파드 내의 컨테이너 간에 데이터를 공유하는 데도 사용됩니다.
**주요 볼륨 유형**:
* **emptyDir**: 빈 디렉토리로 시작하며, 파드가 삭제되면 함께 삭제됨
* **hostPath**: 호스트 노드의 파일 시스템에서 파드로 마운트
* **configMap**: ConfigMap을 볼륨으로 마운트
* **secret**: Secret을 볼륨으로 마운트
* **persistentVolumeClaim**: 영구 볼륨을 파드에 마운트
**emptyDir 볼륨 예시**:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: test-pd
spec:
containers:
- name: test-container
image: nginx:1.30.4
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
```
### 영구 볼륨 (PersistentVolume, PV)
영구 볼륨은 클러스터의 스토리지 리소스를 나타내는 API 객체입니다. 파드와 독립적으로 존재하며, 관리자가 정적으로 또는 provisioner가 동적으로 생성합니다.
**접근 모드**:
* **ReadWriteOnce (RWO)**: 단일 노드에서 읽기/쓰기 가능
* **ReadOnlyMany (ROX)**: 여러 노드에서 읽기 전용으로 마운트 가능
* **ReadWriteMany (RWX)**: 여러 노드에서 읽기/쓰기 가능
* **ReadWriteOncePod (RWOP)**: 지원하는 CSI 볼륨의 단일 Pod 접근; RWO만으로는 같은 노드의 여러 Pod 접근을 막지 않음
EBS CSI 드라이버와 IAM 권한을 먼저 구성합니다. 실제 기존 볼륨 ID와 AZ를 사용하며 다른 곳에서 사용 중인 볼륨을 중복 연결하지 않습니다. 이 스토리지 예제는 AWS용이며 로컬 클러스터는 자체 provisioner가 필요합니다.
**영구 볼륨 예시**:
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-example
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: ebs-gp3
csi:
driver: ebs.csi.aws.com
volumeHandle: vol-0123456789abcdef0
fsType: ext4
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- replace-with-volume-az
```
### 영구 볼륨 클레임 (PersistentVolumeClaim, PVC)
영구 볼륨 클레임은 사용자의 스토리지 요청을 나타내는 API 객체입니다. 파드는 PVC를 통해 PV에 접근합니다.
**영구 볼륨 클레임 예시**:
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-example
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: ebs-gp3
```
**PVC를 사용하는 파드 예시**:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: myfrontend
image: nginx:1.30.4
volumeMounts:
- mountPath: "/var/www/html"
name: mypd
volumes:
- name: mypd
persistentVolumeClaim:
claimName: pvc-example
```
### 스토리지 클래스 (StorageClass)
스토리지 클래스는 관리자가 제공하는 스토리지의 "클래스"를 설명합니다. 다양한 서비스 품질 수준, 백업 정책, 또는 클러스터 관리자가 결정한 임의의 정책을 제공할 수 있습니다.
**스토리지 클래스 예시**:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-gp3
provisioner: ebs.csi.aws.com
parameters:
type: gp3
csi.storage.k8s.io/fstype: ext4
encrypted: 'true'
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
```
### 동적 프로비저닝
동적 프로비저닝은 스토리지 클래스를 사용하여 PVC가 요청될 때 자동으로 PV를 생성하는 기능입니다.
**동적 프로비저닝 예시**:
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dynamic-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: ebs-gp3
```
### CSI (Container Storage Interface)
CSI는 Kubernetes와 스토리지 시스템 간의 표준 인터페이스를 제공합니다. 이를 통해 스토리지 제공업체는 Kubernetes 코드를 수정하지 않고도 자체 스토리지 드라이버를 개발할 수 있습니다.
**주요 CSI 드라이버**:
* **AWS EBS CSI Driver**: Amazon EBS 볼륨 관리
* **AWS EFS CSI Driver**: Amazon EFS 파일 시스템 관리
* **AWS FSx for Lustre CSI Driver**: FSx for Lustre 파일 시스템 관리
* **GCE PD CSI Driver**: Google Compute Engine 영구 디스크 관리
* **Azure Disk CSI Driver**: Azure 디스크 관리
**설치된 CSI 드라이버를 사용하는 StorageClass 예시**:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-sc
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: 'true'
csi.storage.k8s.io/fstype: ext4
volumeBindingMode: WaitForFirstConsumer
```
## Kubernetes 구성 및 보안
Kubernetes는 애플리케이션 구성과 보안을 관리하기 위한 다양한 객체와 메커니즘을 제공합니다.
### ConfigMap
ConfigMap은 키-값 쌍의 형태로 구성 데이터를 저장하는 API 객체입니다. 파드는 환경 변수, 명령줄 인수 또는 구성 파일로 ConfigMap의 데이터를 사용할 수 있습니다.
**ConfigMap 예시**:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
app.properties: |
app.name=MyApp
app.version=1.0.0
app.environment=production
log-level: INFO
max-connections: "100"
```
환경 변수는 자동 갱신되지 않아 변경 시 Pod를 재생성합니다. 볼륨 갱신은 지연될 수 있고 앱 reload가 필요하며 subPath 마운트는 갱신되지 않습니다.
**ConfigMap을 사용하는 파드 예시**:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: config-pod
spec:
containers:
- name: app
image: myapp:1.0
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: log-level
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: app-config
```
### Secret
Secret은 암호, 토큰, 키와 같은 민감한 정보를 저장하는 API 객체입니다. ConfigMap과 유사하지만, 민감한 데이터를 위해 설계되었습니다.
**Secret 유형**:
* **Opaque**: 임의의 사용자 정의 데이터 (기본값)
* **kubernetes.io/service-account-token**: 수동 요청하는 장기 레거시 토큰 Secret; TokenRequest/projected 토큰 권장
* **kubernetes.io/dockercfg**: 직렬화된 \~/.dockercfg 파일
* **kubernetes.io/dockerconfigjson**: 직렬화된 \~/.docker/config.json 파일
* **kubernetes.io/basic-auth**: 기본 인증을 위한 자격 증명
* **kubernetes.io/ssh-auth**: SSH 인증을 위한 자격 증명
* **kubernetes.io/tls**: TLS 클라이언트 또는 서버를 위한 데이터
data 필드는 base64 인코딩이며 암호화가 아닙니다. RBAC와 클러스터에 맞는 저장 암호화를 사용합니다. EKS는 1.28 이상에서 모든 Kubernetes API 데이터를 기본 암호화합니다. 아래 값은 설명용이며 실제 배포 시 교체해야 합니다.
**Secret 예시**:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: YWRtaW4= # base64 인코딩된 "admin"
password: cGFzc3dvcmQxMjM= # base64 인코딩된 "password123"
```
**Secret을 사용하는 파드 예시**:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: secret-pod
spec:
containers:
- name: db-client
image: db-client:1.0
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
```
### RBAC (Role-Based Access Control)
RBAC는 Kubernetes API에 대한 접근을 제어하는 메커니즘입니다. 역할(Role)과 역할 바인딩(RoleBinding)을 사용하여 사용자나 서비스 계정에 특정 권한을 부여합니다.
**주요 RBAC 객체**:
* **Role**: 네임스페이스 내에서 권한 집합을 정의
* **ClusterRole**: 클러스터/네임스페이스 리소스의 재사용 가능한 규칙이며 실제 적용 범위는 바인딩에 따름
* **RoleBinding**: 역할을 사용자, 그룹 또는 서비스 계정에 바인딩
* **ClusterRoleBinding**: 클러스터 역할을 사용자, 그룹 또는 서비스 계정에 바인딩
**Role 예시**:
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
```
**RoleBinding 예시**:
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
- kind: User
name: jane
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
```
### 서비스 계정 (ServiceAccount)
서비스 계정은 파드 내에서 실행되는 프로세스의 ID를 제공합니다. 파드는 서비스 계정을 사용하여 Kubernetes API와 통신합니다.
**서비스 계정 예시**:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: default
```
**서비스 계정을 사용하는 파드 예시**:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: sa-pod
spec:
serviceAccountName: app-sa
containers:
- name: app
image: myapp:1.0
```
### 네트워크 정책 (NetworkPolicy)
네트워크 정책은 파드 간의 통신을 제어하는 방법을 제공합니다. 기본적으로 모든 파드는 서로 통신할 수 있지만, 네트워크 정책을 사용하면 이를 제한할 수 있습니다.
**네트워크 정책 예시**:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-network-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
role: api
ports:
- protocol: TCP
port: 3306
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app: prometheus
ports:
- protocol: TCP
port: 9104
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
```
### Pod Security Admission과 SecurityContext
PodSecurityPolicy는 1.25에서 제거되었습니다. Pod Security Admission은 네임스페이스 레이블로 Pod Security Standards를 적용합니다. SecurityContext는 워크로드 자체 설정이며 admission 강제를 대신하지 않습니다.
**파드 보안 컨텍스트 예시**:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: security-context-pod
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:1.0
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
```
### 파드 보안 표준 (Pod Security Standards)
파드 보안 표준은 파드의 보안 요구사항을 정의하는 세 가지 정책 수준을 제공합니다:
1. **Privileged**: 제한 없음, 모든 기능 허용
2. **Baseline**: 알려진 권한 에스컬레이션 방지
3. **Restricted**: 강력한 제한으로 모범 사례 적용
**파드 보안 표준 적용 예시**:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: my-namespace
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
```
## Kubernetes vs Amazon EKS
Amazon EKS(Elastic Kubernetes Service)는 AWS에서 제공하는 관리형 Kubernetes 서비스입니다. EKS는 표준 Kubernetes API와 AWS 통합을 제공합니다. 아래 비교는 일반 EC2 노드 그룹 기준이며 Auto Mode/Fargate/Hybrid Nodes는 책임과 지원 기능이 다릅니다.
### 주요 차이점
| 특성 | 자체 관리형 Kubernetes | Amazon EKS |
| ---------- | ------------------- | -------------------------------- |
| 컨트롤 플레인 관리 | 사용자가 직접 관리 | AWS에서 관리 |
| 고가용성 | 사용자가 구성 필요 | 기본 제공 (여러 가용 영역에 걸쳐 배포) |
| 업그레이드 | 사용자가 직접 수행 | AWS가 컨트롤 플레인 업그레이드 관리; 노드/애드온은 별도 조정 |
| 보안 패치 | 사용자가 직접 적용 | AWS가 컨트롤 플레인 패치; 관리형 노드 AMI 배포는 사용자 책임(Auto Mode는 별도) |
| 인증 | 다양한 옵션 구성 필요 | AWS IAM과 통합 |
| 네트워킹 | CNI 플러그인 선택 및 구성 필요 | Amazon VPC CNI 기본 제공 |
| 로드 밸런싱 | 수동 구성 필요 | AWS Load Balancer Controller 통합 |
| 스토리지 | 스토리지 드라이버 구성 필요 | EBS, EFS, FSx CSI 드라이버 통합 |
| 모니터링 | 수동 설정 필요 | CloudWatch Container Insights 통합 |
| 비용 | 인프라 비용 및 운영 비용 | 컨트롤 플레인 비용 + 인프라 비용 |
### EKS의 추가 기능
1. **AWS IAM 통합**: Kubernetes RBAC와 AWS IAM의 통합
2. **AWS Load Balancer Controller**: ALB 및 NLB를 Kubernetes 서비스 및 인그레스와 통합
3. **EKS 관리형 노드 그룹**: 노드 수명 주기 관리 자동화
4. **Fargate 프로필**: 서버리스 Kubernetes 파드 실행
5. **VPC CNI 플러그인**: AWS VPC 네트워킹과의 통합
6. **CloudWatch Container Insights**: 컨테이너 모니터링 및 로깅
7. **AWS App Mesh**: 기존 연동; 2026-09-30 지원 종료 예정
8. **AWS Distro for OpenTelemetry**: 분산 추적 및 모니터링
9. **EKS 콘솔 및 CLI**: 관리 인터페이스 제공
10. **EKS 블루프린트**: 모범 사례 기반 클러스터 구성
### EKS 특화 구성 요소
1. **EKS 컨트롤 플레인**: 여러 가용 영역에 걸쳐 고가용성 보장
2. **EKS 노드 AMI**: AWS 제공 AL2023/Bottlerocket/Windows 옵션 및 Ubuntu 같은 별도 호환 AMI
3. **EKS 관리형 노드 그룹**: 노드 그룹 업데이트 지원; 워크로드 기반 노드 확장은 별도 autoscaler 필요
4. **EKS Fargate**: 서버리스 컨테이너 실행 환경
5. **EKS Connector**: 외부 Kubernetes 클러스터를 AWS 콘솔에 연결
6. **EKS Anywhere**: 온프레미스 환경에서 EKS 호환 클러스터 실행
7. **EKS Distro**: AWS에서 관리하는 Kubernetes 배포판
### AWS 서비스 통합
EKS는 다음과 같은 AWS 서비스와 통합됩니다:
1. **Amazon VPC**: 네트워킹 인프라
2. **AWS IAM**: 인증 및 권한 부여
3. **Amazon ECR**: 컨테이너 이미지 저장소
4. **AWS Load Balancer**: 애플리케이션 트래픽 분산
5. **Amazon EBS/EFS/FSx**: 영구 스토리지
6. **AWS CloudWatch**: 모니터링 및 로깅
7. **AWS CloudTrail**: AWS API 감사; Kubernetes API 감사에는 EKS audit 로깅 필요
8. **AWS KMS**: 암호화 키 관리
9. **AWS WAF**: ALB 등 지원되는 애플리케이션 진입점에 연결하며 EKS API 엔드포인트에 직접 연결하지 않음
10. **AWS Shield**: DDoS 보호
11. **AWS X-Ray**: 분산 추적
12. **AWS App Mesh**: 2026-09-30 지원 종료 예정; 기존 워크로드 마이그레이션 필요
13. **AWS SageMaker**: 기계 학습 워크로드
14. **AWS Bedrock**: 생성형 AI 워크로드
## Kubernetes 시작하기
Kubernetes를 시작하는 방법은 여러 가지가 있습니다. 여기서는 로컬 개발 환경과 AWS EKS에서 Kubernetes를 시작하는 방법을 간략히 소개합니다.
### 로컬 개발 환경
#### Minikube
Minikube는 로컬 Kubernetes 클러스터를 실행하며 단일/다중 노드 구성을 지원합니다.
**설치 및 시작**:
```bash
# 설치
brew install minikube
# 시작
minikube start
# 상태 확인
minikube status
# 워크로드 확인; 아래 유지 관리되는 Headlamp UI 절차 참고
kubectl get pods -A
```
#### Kind (Kubernetes in Docker)
Kind는 지원되는 Docker/Podman/nerdctl 제공자를 통해 컨테이너를 노드로 사용하는 로컬 클러스터 도구입니다.
**설치 및 시작**:
```bash
# 설치
brew install kind
# 클러스터 생성
kind create cluster --name my-cluster
# 클러스터 확인
kind get clusters
kubectl cluster-info --context kind-my-cluster
```
#### Docker Desktop
Docker Desktop은 Mac 및 Windows에서 Kubernetes를 쉽게 실행할 수 있는 기능을 제공합니다.
**설정**:
1. Docker Desktop 설치
2. 설정 > Kubernetes > "Enable Kubernetes" 체크
3. "Apply & Restart" 클릭
### AWS EKS
#### eksctl을 사용한 EKS 클러스터 생성
eksctl은 EKS 클러스터를 생성하고 관리하기 위한 간단한 CLI 도구입니다.
**설치 및 클러스터 생성**:
```bash
# Install a reviewed eksctl release from the official eksctl-io GitHub releases,
# verify eksctl_checksums.txt, and place the binary in PATH.
eksctl version
# Use an existing short-lived AWS login/SSO profile with required permissions.
aws sts get-caller-identity
# This example provisions real AWS resources. Choose the intended account/Region,
# supported EKS version, networking and IAM configuration before running it.
: "${EKS_VERSION:?Set a version supported by EKS, not upstream latest}"
eksctl create cluster \
--name my-cluster \
--region ap-northeast-2 \
--version "$EKS_VERSION" \
--nodegroup-name standard-workers \
--node-type t3.medium \
--node-ami-family AmazonLinux2023 \
--node-private-networking \
--nodes 3 --nodes-min 1 --nodes-max 4 --managed
kubectl get nodes
```
#### AWS Management Console을 사용한 EKS 클러스터 생성
AWS Management Console을 통해 EKS 클러스터를 생성할 수도 있습니다.
**단계**:
1. AWS Management Console에 로그인
2. EKS 서비스로 이동
3. "클러스터 생성" 클릭
4. 클러스터 이름, IAM 역할, VPC 및 서브넷 구성
5. 보안 그룹 구성
6. 로깅 옵션 구성
7. 클러스터 생성
8. 노드 그룹 추가
### kubectl 설치 및 구성
kubectl은 Kubernetes 클러스터와 상호 작용하기 위한 명령줄 도구입니다.
**설치**:
```bash
# macOS: use a kubectl version within one minor of the API server.
brew install kubectl
```
```bash
# Linux: select an explicit compatible version and architecture.
set -euo pipefail
: "${KUBECTL_VERSION:?Set a cluster-compatible version, e.g. v1.37.0}"
case "$(uname -m)" in
x86_64) KUBECTL_ARCH=amd64 ;;
aarch64|arm64) KUBECTL_ARCH=arm64 ;;
*) echo "Choose a supported kubectl architecture" >&2; exit 1 ;;
esac
curl --fail --location --output kubectl "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/$KUBECTL_ARCH/kubectl"
curl --fail --location --output kubectl.sha256 "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/$KUBECTL_ARCH/kubectl.sha256"
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
sudo install -m 0755 kubectl /usr/local/bin/kubectl
```
```powershell
$ErrorActionPreference = "Stop"
$KubectlVersion = Read-Host "Cluster-compatible kubectl version (vX.Y.Z)"
$KubectlArch = Read-Host "Architecture (amd64 or arm64)"
if ($KubectlVersion -notmatch '^v\d+\.\d+\.\d+$' -or $KubectlArch -notin @('amd64','arm64')) { throw "Invalid version/architecture" }
$BaseUrl = "https://dl.k8s.io/release/$KubectlVersion/bin/windows/$KubectlArch"
Invoke-WebRequest "$BaseUrl/kubectl.exe" -OutFile kubectl.exe
Invoke-WebRequest "$BaseUrl/kubectl.exe.sha256" -OutFile kubectl.exe.sha256
if ((Get-FileHash kubectl.exe -Algorithm SHA256).Hash -ne (Get-Content kubectl.exe.sha256).Trim()) { throw "Checksum mismatch" }
# Move the verified binary to a directory included in PATH.
```
**기본 명령어**:
```bash
# 클러스터 정보 확인
kubectl cluster-info
# 노드 목록 확인
kubectl get nodes
# 모든 네임스페이스의 파드 확인
kubectl get pods --all-namespaces
# 배포 생성
kubectl create deployment nginx --image=nginx:1.30.4
# 서비스 노출
kubectl expose deployment nginx --port=80 --type=ClusterIP
# Run port-forward in a separate terminal; stop it when finished.
kubectl port-forward service/nginx 8080:80
# 로그 확인
kubectl logs
# 파드 내 컨테이너에 명령 실행
kubectl exec -it -- /bin/bash
```
### Headlamp UI 사용
Kubernetes Dashboard는 보관되어 유지 관리되지 않습니다. Headlamp를 사용하고 사용자의 기존 RBAC 범위로 로그인합니다. 아래 Helm 예제는 자동 cluster-admin 바인딩을 생성하지 않으며 인증을 우회하는 서비스 계정 토큰 모드도 비활성화합니다. 필요한 경우 관리자가 별도로 최소 권한을 부여합니다.
```bash
helm repo add headlamp https://kubernetes-sigs.github.io/headlamp/
helm repo update headlamp
: "${HEADLAMP_CHART_VERSION:?Set a reviewed chart version}"
helm upgrade --install headlamp headlamp/headlamp \
--namespace kube-system --version "$HEADLAMP_CHART_VERSION" \
--set clusterRoleBinding.create=false \
--set config.unsafeUseServiceAccountToken=false
kubectl -n kube-system port-forward service/headlamp 8080:80
```
로컬 http://localhost:8080에서 설치한 Headlamp 버전의 로그인 절차를 따릅니다. 공개 인그레스로 노출하려면 TLS/인증 구성을 별도로 준비해야 합니다.
## 결론
Kubernetes는 컨테이너화된 애플리케이션의 배포, 확장 및 관리를 자동화하는 강력한 플랫폼입니다. 이 문서에서 다룬 핵심 내용을 정리하면:
### 핵심 아키텍처
* **컨트롤 플레인**: 클러스터의 두뇌 역할 (API Server, etcd, Scheduler, Controller Manager)
* **워커 노드**: 실제 애플리케이션을 실행하는 노드 (kubelet, kube-proxy, Container Runtime)
* **선언적 구성**: 원하는 상태를 정의하면 Kubernetes가 현재 상태를 원하는 상태로 맞춤
### 주요 객체 및 리소스
* **기본 객체**: Pod, Service, Volume, Namespace
* **워크로드 리소스**: Deployment, StatefulSet, DaemonSet, Job, CronJob
* **구성 및 보안**: ConfigMap, Secret, RBAC, ServiceAccount
* **네트워킹**: Service, Ingress, NetworkPolicy
* **스토리지**: PersistentVolume, PersistentVolumeClaim, StorageClass
### 학습 경로 권장사항
**1단계: 로컬 환경 구축**
* minikube 또는 kind로 로컬 클러스터 생성
* kubectl 명령어 익히기
* 기본 객체(Pod, Deployment, Service) 실습
**2단계: 핵심 개념 마스터**
* 워크로드 리소스 이해 및 실습
* ConfigMap과 Secret으로 구성 관리
* Service와 Ingress로 네트워킹 구성
* PV와 PVC로 스토리지 관리
**3단계: 고급 기능 학습**
* RBAC와 보안 정책
* 자동 확장 (HPA, VPA, Cluster Autoscaler)
* 모니터링 및 로깅 (Prometheus, Grafana)
* 서비스 메시 (Istio, Linkerd)
**4단계: 프로덕션 운영**
* Amazon EKS 또는 다른 관리형 Kubernetes 사용
* CI/CD 파이프라인 통합
* 재해 복구 및 백업 전략
* 비용 최적화 및 리소스 관리
### 다음 단계
* **EKS 심화 학습**: EKS 특화 기능 (Fargate, VPC CNI, ALB Controller)
* **고급 네트워킹**: CNI 플러그인 (Calico, Cilium)
* **옵저버빌리티**: 메트릭, 로그, 트레이싱
* **GitOps**: ArgoCD, Flux
* **보안 강화**: Pod Security Standards, Network Policies, OPA/Gatekeeper
Kubernetes는 계속 발전하고 있으며, 클라우드 네이티브 애플리케이션 개발 및 운영의 핵심 요소가 되었습니다. 이 문서가 Kubernetes 여정을 시작하는 데 도움이 되기를 바랍니다.
### 추가 학습 리소스
* **공식 문서**: [Kubernetes 공식 문서](https://kubernetes.io/docs/)는 가장 정확하고 최신의 정보를 제공합니다
* **인터랙티브 튜토리얼**: [Kubernetes Tutorials](https://kubernetes.io/docs/tutorials/)에서 실습 가능
* **커뮤니티**: [Kubernetes Slack](https://slack.k8s.io/), [Reddit r/kubernetes](https://reddit.com/r/kubernetes)
* **인증**: CKA(Certified Kubernetes Administrator), CKAD(Certified Kubernetes Application Developer)
* **한국 커뮤니티**: Kubernetes Korea User Group, AWS Korea User Group
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [Kubernetes 소개 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/basics/04-kubernetes-introduction-quiz)를 풀어보세요.
## 참고 자료
* [Kubernetes 공식 문서](https://kubernetes.io/docs/)
* [Amazon EKS 문서](https://docs.aws.amazon.com/eks/)
* [Kubernetes GitHub 저장소](https://github.com/kubernetes/kubernetes)
* [CNCF(Cloud Native Computing Foundation)](https://www.cncf.io/)
* [Kubernetes The Hard Way](https://github.com/kelseyhightower/kubernetes-the-hard-way)
* [Kubernetes Patterns](https://www.oreilly.com/library/view/kubernetes-patterns/9781492050278/)
## 검증 참고 자료
- https://kubernetes.io/releases/version-skew-policy/
- https://kubernetes.io/docs/tasks/tools/install-kubectl-windows/
- https://kubernetes.io/docs/setup/production-environment/container-runtimes/
- https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
- https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/
- https://kubernetes.io/docs/concepts/storage/persistent-volumes/
- https://kubernetes.io/docs/reference/networking/virtual-ips/
- https://kubernetes.io/docs/concepts/services-networking/network-policies/
- https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/
- https://coredns.io/plugins/kubernetes/
- https://github.com/fluent/fluent-bit/releases/tag/v5.1.2
- https://github.com/fluent/fluent-bit/blob/v5.1.2/conf/parsers.conf
- https://docs.aws.amazon.com/app-mesh/latest/userguide/what-is-app-mesh.html
- https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html
- https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions-standard.html
- https://docs.aws.amazon.com/eks/latest/userguide/envelope-encryption.html
- https://docs.aws.amazon.com/eks/latest/userguide/lbc-helm.html
- https://eksctl.io/installation/
- https://minikube.sigs.k8s.io/docs/tutorials/multi_node/
- https://kind.sigs.k8s.io/docs/user/quick-start/
- https://github.com/kubernetes/dashboard/blob/master/README.md
- https://headlamp.dev/docs/latest/installation/in-cluster/
- https://github.com/kubernetes-sigs/headlamp/blob/main/charts/headlamp/values.yaml
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/core/01-cluster-architecture
----------------------------------------
# 클러스터 아키텍처
> **지원 버전**: Kubernetes 1.35, 1.36, 1.37
> **마지막 업데이트**: 2026년 9월 9일
버전 헤더는 업스트림 Kubernetes 기준입니다. 2026년 9월 11일 기준 EKS 표준 지원 버전은 1.34–1.36이므로 버전 선택 전 [EKS 수명 주기](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html)를 확인하세요. 아래 구성 요소 명령은 자체 관리형 클러스터 예시이며 EKS 컨트롤 플레인은 AWS가 관리합니다. 이미지 태그와 인프라 ID는 예시이므로 호환되고 유지 관리되는 이미지와 실제 값으로 바꿔 사용하세요.
## 실습 환경 설정
이 문서의 개념을 실습하기 위해서는 다음과 같은 도구와 환경이 필요합니다:
### 필수 도구
- API 서버와 마이너 버전 차이가 1 이내인 kubectl
- 작동하는 Kubernetes 클러스터 (EKS, minikube, kind 등)
### 로컬 개발 환경 설정
```bash
# minikube 설치 (로컬 개발용)
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
# 클러스터 시작
minikube start
# 클러스터 상태 확인
kubectl cluster-info
# 컨트롤 플레인 구성 요소 확인
kubectl get pods -n kube-system
```
## 클러스터 아키텍처 개요
> **핵심 개념**: Kubernetes 클러스터는 컨트롤 플레인과 워커 노드로 구성되며, 각각 특정 역할을 담당하는 여러 구성 요소로 이루어져 있습니다.
Kubernetes 클러스터는 컨테이너화된 애플리케이션을 실행하기 위한 일련의 노드(가상 또는 물리적 머신)로 구성됩니다. 클러스터는 크게 컨트롤 플레인과 워커 노드로 나뉩니다.
### 클러스터 아키텍처 다이어그램

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-01-cluster-architecture-0.html)
**컨트롤 플레인 구성 요소**:
- **kube-apiserver**: Kubernetes API를 노출하는 프론트엔드
- **etcd**: Kubernetes API 상태를 저장하는 키-값 저장소
- **kube-scheduler**: 새로 생성된 파드를 실행할 노드 선택
- **kube-controller-manager**: 클러스터 상태를 관리하는 컨트롤러 실행
- **cloud-controller-manager**: 클라우드 제공업체 API와 상호 작용
**워커 노드 구성 요소**:
- **kubelet**: 각 노드에서 실행되는 에이전트, 컨테이너 실행 관리
- **kube-proxy**: 네트워크 규칙 유지 및 연결 포워딩
- **컨테이너 런타임**: 컨테이너 실행 (containerd, CRI-O 등)
## 컨트롤 플레인 구성 요소
컨트롤 플레인은 Kubernetes 클러스터의 "두뇌" 역할을 하며, 클러스터의 전반적인 상태를 관리하고 제어합니다. 컨트롤 플레인 구성 요소는 일반적으로 전용 머신에서 실행되며, 고가용성을 위해 여러 인스턴스로 복제될 수 있습니다.
### 컨트롤 플레인 구성 요소 상세 설명
| 구성 요소 | 주요 기능 | 통신 대상 | 고가용성 구성 |
|----------|----------|----------|-------------|
| **kube-apiserver** | - Kubernetes API 제공 - 인증 및 권한 부여 - API 요청 처리 | - 모든 구성 요소 - etcd | 여러 인스턴스로 수평 확장 |
| **etcd** | - 클러스터 데이터 저장 - 분산 키-값 저장소 - 일관성 보장 | - kube-apiserver | 다중 노드 클러스터 |
| **kube-scheduler** | - 파드 배치 결정 - 노드 리소스 평가 - 어피니티/안티-어피니티 적용 | - kube-apiserver | 액티브-스탠바이 구성 |
| **kube-controller-manager** | - 노드 컨트롤러 - 레플리케이션 컨트롤러 - 엔드포인트 컨트롤러 - 서비스 어카운트 컨트롤러 | - kube-apiserver | 액티브-스탠바이 구성 |
| **cloud-controller-manager** | - 클라우드 제공업체 통합 - 노드 라이프사이클 - 라우팅 및 로드 밸런싱 | - kube-apiserver - 클라우드 API | 액티브-스탠바이 구성 |
### 컨트롤 플레인 통신 흐름
1. 사용자 또는 컨트롤러가 kube-apiserver에 요청 전송
2. kube-apiserver가 인증, 권한 부여 및 승인 수행
3. kube-apiserver가 etcd에서 데이터 읽기/쓰기
4. 컨트롤러와 스케줄러가 kube-apiserver를 통해 클러스터 상태 감시
5. kubelet이 kube-apiserver에 노드 상태 보고
### kube-apiserver
kube-apiserver는 Kubernetes API를 노출하는 컨트롤 플레인의 프론트엔드입니다. 모든 내부 및 외부 요청은 이 API 서버를 통해 처리됩니다.
**주요 기능**:
- REST API 제공
- 인증 및 권한 부여
- 요청 검증 및 처리
- etcd와의 통신
- 수평적 확장 가능 (여러 인스턴스로 확장 가능)
**주요 플래그 및 구성 옵션**:
```bash
# 기본 구성 예시
kube-apiserver \
--advertise-address=192.168.1.10 \
--allow-privileged=true \
--authorization-mode=Node,RBAC \
--client-ca-file=/etc/kubernetes/pki/ca.crt \
--enable-admission-plugins=NodeRestriction \
--enable-bootstrap-token-auth=true \
--etcd-servers=https://127.0.0.1:2379 \
--etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt \
--etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt \
--etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key \
--kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt \
--kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key \
--service-account-key-file=/etc/kubernetes/pki/sa.pub \
--service-account-signing-key-file=/etc/kubernetes/pki/sa.key \
--service-account-issuer=https://kubernetes.default.svc.cluster.local \
--service-cluster-ip-range=10.96.0.0/12 \
--tls-cert-file=/etc/kubernetes/pki/apiserver.crt \
--tls-private-key-file=/etc/kubernetes/pki/apiserver.key
```
**API 서버 보안**:
- TLS 인증서를 통한 보안 통신
- 다양한 인증 방식 지원 (X.509 인증서, 서비스 계정 토큰, OIDC, 웹훅 등)
- RBAC(Role-Based Access Control)을 통한 권한 관리
- 어드미션 컨트롤러를 통한 요청 검증 및 변경
### etcd
etcd는 Kubernetes API 상태를 저장하는 일관성 있고 고가용성을 갖춘 키-값 저장소입니다. Kubernetes의 "소스 오브 트루스(source of truth)"로 작동합니다.
**주요 특징**:
- 분산 시스템
- 강한 일관성 (Raft 합의 알고리즘 사용)
- 고가용성 (여러 노드로 구성 가능)
- 안전한 데이터 저장
- 워치(watch) 기능으로 변경 사항 모니터링
**etcd 클러스터 구성**:
```bash
# etcd 클러스터 구성 예시 (3노드)
etcd \
--name etcd-1 \
--initial-advertise-peer-urls https://192.168.1.11:2380 \
--listen-peer-urls https://192.168.1.11:2380 \
--listen-client-urls https://192.168.1.11:2379,https://127.0.0.1:2379 \
--advertise-client-urls https://192.168.1.11:2379 \
--initial-cluster-token etcd-cluster \
--initial-cluster etcd-1=https://192.168.1.11:2380,etcd-2=https://192.168.1.12:2380,etcd-3=https://192.168.1.13:2380 \
--initial-cluster-state new \
--data-dir=/var/lib/etcd \
--cert-file=/etc/kubernetes/pki/etcd/server.crt \
--key-file=/etc/kubernetes/pki/etcd/server.key \
--trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt \
--client-cert-auth=true \
--peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt \
--peer-key-file=/etc/kubernetes/pki/etcd/peer.key \
--peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt \
--peer-client-cert-auth=true
```
**etcd 백업 및 복구**:
```bash
# etcd 백업
ETCDCTL_API=3 etcdctl snapshot save snapshot.db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key
# etcd 복구
etcdutl snapshot restore snapshot.db \
--bump-revision=1000000000 --mark-compacted \
--data-dir=/var/lib/etcd-restore \
--name=etcd-1 \
--initial-cluster=etcd-1=https://192.168.1.11:2380 \
--initial-cluster-token=etcd-cluster \
--initial-advertise-peer-urls=https://192.168.1.11:2380
```
**etcd 성능 최적화**:
- 디스크 I/O 최적화 (SSD 사용 권장)
- 적절한 메모리 할당
- 정기적인 압축 및 조각 모음
- 클러스터 크기에 따른 적절한 etcd 노드 수 설정 (일반적으로 3 또는 5)
#### 2026년 7월 업데이트: etcd v3.7.0 릴리스
2026년 7월 8일 SIG etcd가 etcd v3.7.0을 릴리스했습니다. 주요 변경 사항:
- **RangeStream**: 대용량 Range 응답 전체를 메모리에 버퍼링하지 않고 청크 단위로 스트리밍하는 기능 (오랫동안 요청되어 온 기능)
- **성능 개선**: keys-only Range 요청 최적화, 더 빠르고 안정적인 리스(lease) 처리
- 레거시 v2store 잔재 완전 제거 및 protobuf 전면 개편
- 핵심 의존성 bbolt v1.5.0, raft v3.7.0 포함
자세한 내용은 [공식 발표](https://kubernetes.io/blog/2026/07/08/announcing-etcd-3.7/)와 [etcd v3.7 체인지로그](https://github.com/etcd-io/etcd/blob/main/CHANGELOG/CHANGELOG-3.7.md)를 참고하세요.
### kube-scheduler
kube-scheduler는 새로 생성된 파드를 실행할 노드를 선택하는 컨트롤 플레인 구성 요소입니다.
**스케줄링 과정**:
1. **필터링**: 파드를 실행할 수 있는 노드 식별
- 리소스 요구사항 (CPU, 메모리)
- 노드 셀렉터, 노드 어피니티
- 테인트(taint)와 톨러레이션(toleration)
- 볼륨 제약 조건
2. **스코어링**: 적합한 노드에 점수 부여
- 리소스 사용률
- 파드 간 어피니티/안티-어피니티
- 데이터 지역성
- 노드 간 부하 분산
3. **바인딩**: 최적의 노드에 파드 할당
**스케줄러 구성**:
```bash
# 기본 구성 예시
kube-scheduler \
--kubeconfig=/etc/kubernetes/scheduler.conf \
--leader-elect=true \
--v=2
```
**스케줄러 프로필 및 플러그인**:
- 기본 스케줄러 프로필
- 사용자 정의 스케줄러 프로필
- 스케줄러 확장 포인트 (필터, 스코어, 바인드 등)
- 다중 스케줄러 지원
**스케줄링 정책**:
```yaml
# 스케줄링 정책 예시
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
pluginConfig:
- name: NodeResourcesFit
args:
scoringStrategy:
type: MostAllocated
resources:
- name: cpu
weight: 1
- name: memory
weight: 1
```
### kube-controller-manager
kube-controller-manager는 여러 컨트롤러 프로세스를 실행하는 컨트롤 플레인 구성 요소입니다. 각 컨트롤러는 클러스터의 특정 측면을 관리합니다.
**주요 컨트롤러**:
- **노드 컨트롤러**: 노드 상태 모니터링 및 대응
- **레플리케이션 컨트롤러**: 파드 복제본 수 유지
- **EndpointSlice 컨트롤러**: EndpointSlice에 Service 백엔드 기록
- **서비스 어카운트 & 토큰 컨트롤러**: 기본 ServiceAccount를 생성하고 명시적으로 요청한 레거시 토큰 Secret을 관리하며, 현재 파드는 단기 TokenRequest 토큰 사용
- **잡 컨트롤러**: 일회성 작업 관리
- **크론잡 컨트롤러**: 예약된 작업 관리
- **데몬셋 컨트롤러**: 모든 노드에 특정 파드 실행 보장
- **스테이트풀셋 컨트롤러**: 상태 유지 애플리케이션 관리
- **PV 컨트롤러**: 영구 볼륨 관리
- **네임스페이스 컨트롤러**: 네임스페이스 수명 주기 관리
- **가비지 컬렉터**: 종속성이 없는 객체 정리
**컨트롤러 매니저 구성**:
```bash
# 기본 구성 예시
kube-controller-manager \
--kubeconfig=/etc/kubernetes/controller-manager.conf \
--leader-elect=true \
--use-service-account-credentials=true \
--root-ca-file=/etc/kubernetes/pki/ca.crt \
--service-account-private-key-file=/etc/kubernetes/pki/sa.key \
--cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt \
--cluster-signing-key-file=/etc/kubernetes/pki/ca.key \
--controllers=*,bootstrapsigner,tokencleaner
```
**컨트롤러 동작 방식**:
1. 컨트롤러는 API 서버를 통해 클러스터 상태를 지속적으로 감시
2. 현재 상태와 원하는 상태 간의 차이 감지
3. 차이를 해소하기 위한 작업 수행
4. 상태 변경 사항을 API 서버에 보고
### cloud-controller-manager
cloud-controller-manager는 클라우드별 컨트롤 로직을 포함하는 컨트롤 플레인 구성 요소입니다. 이를 통해 Kubernetes 코어와 클라우드 제공업체의 API를 분리할 수 있습니다.
**주요 컨트롤러**:
- **노드 컨트롤러**: 클라우드 제공자 API를 통해 노드 상태 확인
- **라우트 컨트롤러**: 클라우드 환경에서 라우트 설정
- **서비스 컨트롤러**: 클라우드 로드 밸런서 생성, 업데이트, 삭제
클라우드 스토리지 프로비저닝·연결은 CSI 컨트롤러가, 마운트는 kubelet과 CSI 노드 플러그인이 담당합니다. 이는 cloud-controller-manager의 역할이 아닙니다.
**클라우드 제공업체별 구현**:
- AWS Cloud Controller Manager
- Azure Cloud Controller Manager
- GCP Cloud Controller Manager
- OpenStack Cloud Controller Manager
- vSphere Cloud Controller Manager
**클라우드 컨트롤러 매니저 구성**:
```bash
# AWS 클라우드 컨트롤러 매니저 예시
cloud-controller-manager \
--cloud-provider=aws \
--cloud-config=/etc/kubernetes/cloud-config \
--kubeconfig=/etc/kubernetes/cloud-controller-manager.conf \
--leader-elect=true
```
**클라우드 컨트롤러 매니저 장점**:
- 클라우드 제공업체별 코드와 Kubernetes 코어 분리
- 클라우드 제공업체가 자체 기능을 독립적으로 개발 가능
- Kubernetes 코어 변경 없이 클라우드 기능 추가 가능
## 노드 구성 요소
노드는 Kubernetes 클러스터에서 워커 머신으로, 컨테이너화된 애플리케이션을 실행합니다. 각 노드는 컨트롤 플레인에 의해 관리되며, 여러 구성 요소로 이루어져 있습니다.
### kubelet
kubelet은 각 노드에서 실행되는 에이전트로, 파드 내 컨테이너가 실행되도록 관리합니다. kubelet은 다양한 메커니즘을 통해 파드 스펙(PodSpec)을 받아 컨테이너가 해당 스펙에 따라 건강하게 실행되도록 보장합니다.
**주요 기능**:
- PodSpec에 따라 컨테이너 실행
- 컨테이너 상태 모니터링 및 보고
- 컨테이너 라이프사이클 관리
- 볼륨 마운트 관리
- 노드 상태 보고
- 컨테이너 헬스 체크 수행
**kubelet 구성**:
```bash
# 기본 구성 예시
kubelet \
--kubeconfig=/etc/kubernetes/kubelet.conf \
--config=/var/lib/kubelet/config.yaml \
--container-runtime-endpoint=unix:///run/containerd/containerd.sock
```
**kubelet 구성 파일 예시**:
```yaml
# /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
address: 0.0.0.0
authentication:
anonymous:
enabled: false
webhook:
cacheTTL: 2m0s
enabled: true
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
mode: Webhook
webhook:
cacheAuthorizedTTL: 5m0s
cacheUnauthorizedTTL: 30s
cgroupDriver: systemd
clusterDomain: cluster.local
cpuManagerPolicy: none
evictionHard:
memory.available: 100Mi
nodefs.available: 10%
nodefs.inodesFree: 5%
failSwapOn: true
healthzBindAddress: 127.0.0.1
healthzPort: 10248
```
**정적 파드**: 아래 매니페스트는 구성 일부이며 완전한 컨트롤 플레인 설치가 아닙니다. 호스트 네트워크, 인증서, 마운트, 전체 API 서버 구성이 추가로 필요합니다.
kubelet은 API 서버를 통하지 않고 직접 관리하는 정적 파드를 실행할 수 있습니다. 이는 주로 컨트롤 플레인 구성 요소를 실행하는 데 사용됩니다.
```yaml
# /etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
namespace: kube-system
spec:
containers:
- name: kube-apiserver
image: registry.k8s.io/kube-apiserver:v1.37.0
command:
- kube-apiserver
- --advertise-address=192.168.1.10
# ... 추가 플래그
```
### kube-proxy
kube-proxy는 각 노드에서 실행되는 네트워크 프록시로, Kubernetes 서비스 개념의 구현을 담당합니다. 노드의 네트워크 규칙을 유지하고 연결 포워딩을 수행합니다.
**주요 기능**:
- 서비스 IP 및 포트에 대한 네트워크 규칙 유지
- 연결 포워딩
- 로드 밸런싱 구현
- 서비스 디스커버리 지원
**작동 모드**:
1. **iptables**: Linux 기본 모드로 커널 패킷 처리 규칙 설정
2. **nftables**: v1.33부터 Stable이며 커널·CNI 호환성 확인 필요
3. **IPVS**: v1.35부터 사용 중단된 레거시 Linux 모드로 지원되는 대안으로 전환 필요
4. **kernelspace**: Windows 모드
기존 `userspace` 모드는 제거되었습니다. 일부 네트워크 구현은 kube-proxy를 완전히 대체합니다.
**kube-proxy 구성**:
```bash
# 기본 구성 예시
kube-proxy \
--config=/var/lib/kube-proxy/config.conf \
--hostname-override=node1
```
**kube-proxy 구성 파일 예시**:
```yaml
# /var/lib/kube-proxy/config.conf
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
bindAddress: 0.0.0.0
clientConnection:
acceptContentTypes: ""
burst: 10
contentType: application/vnd.kubernetes.protobuf
kubeconfig: /var/lib/kube-proxy/kubeconfig.conf
qps: 5
clusterCIDR: 10.244.0.0/16
configSyncPeriod: 15m0s
conntrack:
maxPerCore: 32768
min: 131072
tcpCloseWaitTimeout: 1h0m0s
tcpEstablishedTimeout: 24h0m0s
enableProfiling: false
healthzBindAddress: 0.0.0.0:10256
hostnameOverride: node1
iptables:
masqueradeAll: false
masqueradeBit: 14
minSyncPeriod: 0s
syncPeriod: 30s
ipvs:
excludeCIDRs: null
minSyncPeriod: 0s
scheduler: ""
syncPeriod: 30s
mode: "iptables"
```
**IPVS vs iptables 모드 비교**:
| 특성 | iptables 모드 | IPVS 모드 |
|------|--------------|-----------|
| 성능 | 서비스 수가 많을 때 성능 저하 | 대규모 클러스터에서 더 나은 성능 |
| 로드 밸런싱 알고리즘 | 기본적으로 백엔드를 무작위 선택 | 다양한 알고리즘 지원 (rr, lc, dh, sh, sed, nq) |
| 구현 | 네트워크 패킷 필터링 체인 | 해시 테이블 기반 |
| 커널 요구사항 | 기본 커널 모듈 | IPVS 커널 모듈 필요 |
### 컨테이너 런타임
컨테이너 런타임은 컨테이너를 실행하는 소프트웨어입니다. Kubernetes는 Container Runtime Interface(CRI)를 통해 다양한 컨테이너 런타임을 지원합니다.
**주요 컨테이너 런타임**:
1. **containerd**: 경량 컨테이너 런타임 (현재 가장 널리 사용됨)
2. **CRI-O**: Kubernetes를 위해 특별히 설계된 경량 런타임
3. **Docker Engine**: cri-dockerd 같은 외부 CRI 어댑터가 필요하며 내장 dockershim은 v1.24에서 제거되었습니다. Docker로 빌드한 OCI 이미지는 containerd/CRI-O에서도 실행됩니다.
**컨테이너 런타임 계층 구조**:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-01-cluster-architecture-1.html)
**containerd 1.x 구성 예시** (2.x는 플러그인 ID가 다르므로 설치 버전에 맞는 기본 구성을 생성하세요):
```toml
# /etc/containerd/config.toml
version = 2
[plugins]
[plugins."io.containerd.grpc.v1.cri"]
sandbox_image = "registry.k8s.io/pause:3.10"
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "runc"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
```
**CRI-O 구성 예시**:
```toml
# /etc/crio/crio.conf
[crio]
root = "/var/lib/containers/storage"
runroot = "/var/run/containers/storage"
storage_driver = "overlay"
storage_option = ["overlay.mountopt=nodev"]
[crio.runtime]
default_runtime = "runc"
conmon = "/usr/bin/conmon"
conmon_cgroup = "pod"
cgroup_manager = "systemd"
[crio.image]
pause_image = "registry.k8s.io/pause:3.10"
```
### 애드온 구성 요소
애드온은 Kubernetes 클러스터의 기능을 확장하는 추가 구성 요소입니다. 일부 중요한 애드온은 다음과 같습니다:
1. **CNI 네트워크 플러그인**: 파드 네트워킹 구현
- Calico, Cilium, Flannel 등
2. **DNS**: 클러스터 내 DNS 서비스 제공
- CoreDNS (기본)
3. **대시보드**: 웹 기반 UI 제공
- Headlamp (Kubernetes Dashboard는 보관 상태로 유지 관리 종료)
4. **인그레스 컨트롤러**: HTTP/HTTPS 라우팅 관리
- Traefik, HAProxy 등
5. **메트릭 서버**: 리소스 사용량 메트릭 수집
- Metrics Server
6. **로깅 및 모니터링**: 로그 수집 및 모니터링
- Prometheus, Grafana, Elasticsearch, Fluentd, Kibana 등
**CoreDNS 구성 예시**:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
```
**Calico CNI 구성 예시**:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: calico-config
namespace: kube-system
data:
calico_backend: "bird"
cni_network_config: |-
{
"name": "k8s-pod-network",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "calico",
"log_level": "info",
"datastore_type": "kubernetes",
"nodename": "__KUBERNETES_NODE_NAME__",
"mtu": __CNI_MTU__,
"ipam": {
"type": "calico-ipam"
},
"policy": {
"type": "k8s"
},
"kubernetes": {
"kubeconfig": "__KUBECONFIG_FILEPATH__"
}
},
{
"type": "portmap",
"snat": true,
"capabilities": {"portMappings": true}
}
]
}
```
## 클러스터 통신 경로
Kubernetes 클러스터 내에서는 여러 구성 요소 간의 통신이 이루어집니다. 이러한 통신 경로를 이해하는 것은 클러스터 설계, 보안 및 문제 해결에 중요합니다.
### 컨트롤 플레인 내부 통신

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-01-cluster-architecture-2.html)
컨트롤 플레인 구성 요소 간의 통신은 다음과 같습니다:
1. **kube-apiserver와 etcd**: kube-apiserver는 클러스터 상태를 저장하고 검색하기 위해 etcd와 통신합니다.
- 프로토콜: gRPC
- 포트: 2379/TCP
- 보안: TLS 인증서 기반 인증
2. **kube-scheduler와 kube-apiserver**: kube-scheduler는 파드 스케줄링을 위해 kube-apiserver와 통신합니다.
- 프로토콜: HTTPS
- 포트: 6443/TCP (kube-apiserver)
- 보안: TLS 인증서 기반 인증
3. **kube-controller-manager와 kube-apiserver**: 컨트롤러는 클러스터 상태를 감시하고 변경하기 위해 kube-apiserver와 통신합니다.
- 프로토콜: HTTPS
- 포트: 6443/TCP (kube-apiserver)
- 보안: TLS 인증서 기반 인증
4. **cloud-controller-manager와 kube-apiserver**: 클라우드 컨트롤러는 클러스터 상태를 감시하고 클라우드 리소스를 관리하기 위해 kube-apiserver와 통신합니다.
- 프로토콜: HTTPS
- 포트: 6443/TCP (kube-apiserver)
- 보안: TLS 인증서 기반 인증
### 컨트롤 플레인과 노드 간 통신

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-01-cluster-architecture-3.html)
컨트롤 플레인과 노드 간의 통신은 다음과 같습니다:
1. **kube-apiserver → kubelet**: API 서버는 로그, exec/attach, 포트 포워딩을 위해 kubelet API를 호출합니다.
- 프로토콜: HTTPS
- 포트: 10250/TCP (kubelet)
- 보안: TLS 인증서 기반 인증
2. **kubelet과 kube-apiserver**: kubelet은 할당된 PodSpec 감시, 노드 등록, 노드·파드 상태 및 이벤트 보고를 위해 kube-apiserver와 통신합니다.
- 프로토콜: HTTPS
- 포트: 6443/TCP (kube-apiserver)
- 보안: TLS 인증서 기반 인증
3. **kube-proxy와 kube-apiserver**: kube-proxy는 서비스 정보를 가져오기 위해 kube-apiserver와 통신합니다.
- 프로토콜: HTTPS
- 포트: 6443/TCP (kube-apiserver)
- 보안: TLS 인증서 기반 인증
### 노드 간 통신

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-01-cluster-architecture-4.html)
노드 간의 통신은 다음과 같습니다:
1. **파드 간 통신**: 파드는 CNI 플러그인이 제공하는 네트워크를 통해 서로 통신합니다.
- 프로토콜: 애플리케이션에 따라 다름 (TCP, UDP 등)
- 포트: 애플리케이션에 따라 다름
- 보안: 네트워크 정책으로 제어 가능
2. **노드 간 파드 통신**: 서로 다른 노드에 있는 파드 간의 통신은 CNI 플러그인에 의해 처리됩니다.
- 프로토콜: 애플리케이션에 따라 다름 (TCP, UDP 등)
- 포트: 애플리케이션에 따라 다름
- 보안: 네트워크 정책으로 제어 가능
### 외부 통신

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-01-cluster-architecture-5.html)
클러스터 외부와의 통신은 다음과 같습니다:
1. **클라이언트와 kube-apiserver**: 사용자 및 외부 시스템은 kube-apiserver를 통해 클러스터와 상호 작용합니다.
- 프로토콜: HTTPS
- 포트: 6443/TCP (kube-apiserver)
- 보안: TLS 인증서, 토큰, 사용자 인증 등
2. **외부 트래픽과 서비스**: 외부 트래픽은 NodePort, LoadBalancer 서비스 또는 인그레스를 통해 클러스터 내 애플리케이션에 접근합니다.
- 프로토콜: HTTP, HTTPS, TCP, UDP 등
- 포트: 서비스 구성에 따라 다름
- 보안: 인그레스 컨트롤러, 서비스 구성에 따라 다름
### 통신 보안
Kubernetes 클러스터 내 통신의 보안은 다음과 같은 방법으로 구현됩니다:
1. **TLS 인증서**: 모든 컨트롤 플레인 구성 요소 간의 통신은 TLS 인증서로 암호화됩니다.
2. **인증 및 권한 부여**: API 서버에 대한 모든 요청은 인증 및 권한 부여 과정을 거칩니다.
3. **네트워크 정책**: 파드 간 통신은 네트워크 정책을 통해 제한할 수 있습니다.
4. **암호화된 시크릿**: etcd에 저장되는 시크릿은 암호화할 수 있습니다.
**API 데이터 저장 시 암호화 예시** (API 서버에 `--encryption-provider-config`를 설정하고 기존 Secret도 다시 저장해야 암호화됨):
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret:
- identity: {}
```
### 고가용성 클러스터 구성
고가용성(HA) Kubernetes 클러스터는 단일 장애점을 제거하고 서비스 중단 없이 운영을 계속할 수 있도록 설계되었습니다.
### 컨트롤 플레인 고가용성
컨트롤 플레인의 고가용성은 다음과 같은 방법으로 구현됩니다:
1. **다중 컨트롤 플레인 노드**: 일반적으로 3개 또는 5개의 컨트롤 플레인 노드를 배포하여 중복성 제공
2. **etcd 클러스터**: 여러 etcd 인스턴스로 구성된 클러스터 배포 (일반적으로 3개 또는 5개)
3. **로드 밸런서**: API 서버 앞에 로드 밸런서를 배치하여 트래픽 분산
**고가용성 컨트롤 플레인 아키텍처**:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-01-cluster-architecture-6.html)
**etcd 클러스터 구성**:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-01-cluster-architecture-7.html)
### 워커 노드 고가용성
워커 노드의 고가용성은 다음과 같은 방법으로 구현됩니다:
1. **다중 워커 노드**: 여러 워커 노드에 워크로드 분산
2. **노드 자동 복구**: 클라우드 제공업체의 자동 복구 기능 활용
3. **자동 확장**: 클러스터 자동 확장기를 통한 노드 자동 확장
4. **다중 가용 영역**: 여러 가용 영역에 노드 배포
**워커 노드 분산 배포**:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-01-cluster-architecture-8.html)
### 애플리케이션 고가용성
애플리케이션의 고가용성은 다음과 같은 방법으로 구현됩니다:
1. **레플리카셋/디플로이먼트**: 여러 파드 복제본 실행
2. **파드 분산 규칙**: 파드 안티-어피니티를 통한 여러 노드에 파드 분산
3. **PodDisruptionBudget**: 계획된 중단 시 최소 가용성 보장
4. **서비스 및 로드 밸런싱**: 트래픽을 여러 파드에 분산
**파드 안티-어피니티 예시**:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
replicas: 3
selector:
matchLabels:
app: web-server
template:
metadata:
labels:
app: web-server
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-server
topologyKey: "kubernetes.io/hostname"
containers:
- name: web-server
image: nginx:1.30.4
```
**PodDisruptionBudget 예시**:
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-server-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: web-server
```
### 재해 복구 전략
Kubernetes 클러스터의 재해 복구 전략은 다음과 같은 방법으로 구현됩니다:
1. **etcd 백업 및 복구**: 정기적인 etcd 데이터 백업 및 복구 절차 수립
2. **다중 리전 배포**: 여러 리전에 클러스터 배포
3. **클러스터 페더레이션**: 여러 클러스터를 연합하여 관리
4. **지속적인 백업**: 애플리케이션 데이터의 지속적인 백업
**etcd 백업 스크립트 예시**:
```bash
#!/bin/bash
ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-snapshot-$(date +%Y%m%d-%H%M%S).db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key
```
**etcd 복구 절차 (자체 관리형 클러스터)**:
1. `etcdutl snapshot status`로 스냅샷을 확인하고 위 예시처럼 호환되는 `etcdutl`로 새 데이터 디렉토리에 복원합니다.
2. 데이터 디렉토리 전환 전에 클러스터 운영 절차에 따라 모든 API 서버와 해당 etcd 프로세스를 중지합니다. kubelet만 중지해도 기존 정적 파드 컨테이너는 종료되지 않습니다.
3. 여러 멤버를 복구할 때는 같은 스냅샷을 각 멤버의 고유 이름·피어 URL과 동일한 전체 `--initial-cluster` 목록으로 복원합니다. 위 단일 멤버 예시는 HA 복구 절차가 아닙니다.
4. Kubernetes watch 캐시를 위해 `--bump-revision`과 `--mark-compacted`를 사용합니다. 증가량은 스냅샷 이후 리비전을 초과하도록 정하며 위 값은 예시입니다.
5. etcd 정적 파드의 hostPath를 복원 디렉토리로 변경하고 etcd를 재시작합니다. 쿼럼·상태 확인 후 API 서버와 컨트롤러를 재시작하고 검증 완료까지 원본 데이터와 백업을 보관합니다.
[etcd 복구 문서](https://etcd.io/docs/v3.6/op-guide/recovery/)를 따르세요. EKS 사용자는 관리형 컨트롤 플레인의 etcd를 직접 운영하거나 복구하지 않습니다.
## 클러스터 네트워킹
Kubernetes 네트워킹은 파드, 서비스, 외부 세계 간의 통신을 가능하게 하는 핵심 구성 요소입니다. Kubernetes 네트워킹 모델은 모든 파드가 고유한 IP 주소를 가지며, NAT 없이 서로 통신할 수 있다는 것을 기본 전제로 합니다.
### 네트워킹 모델
Kubernetes 네트워킹 모델은 다음과 같은 요구사항을 가집니다:
1. **파드 간 통신**: 모든 파드는 NAT 없이 다른 모든 파드와 통신할 수 있어야 함
2. **노드와 파드 간 통신**: 노드 에이전트는 해당 노드의 파드와 통신할 수 있어야 함
3. **파드와 외부 간 통신**: 외부 연결은 라우팅, 필요한 NAT, 보안·egress 정책에 따라 달라지며 모든 파드의 인터넷 접근이 필수인 것은 아님
### CNI (Container Network Interface)
CNI는 Kubernetes에서 네트워킹을 구현하기 위한 표준 인터페이스입니다. 다양한 CNI 플러그인이 있으며, 각각 다른 기능과 성능 특성을 가집니다.
**주요 CNI 플러그인**:
1. **Calico**: BGP 기반 네트워킹, 네트워크 정책 지원
- 특징: 고성능, 네트워크 정책, 암호화, eBPF 지원
- 사용 사례: 대규모 클러스터, 보안이 중요한 환경
2. **Cilium**: eBPF 기반 네트워킹 및 보안
- 특징: L3-L7 보안 정책, 고성능, 관찰성
- 사용 사례: 마이크로서비스, 보안이 중요한 환경
3. **Flannel**: 간단한 오버레이 네트워크
- 특징: 설정 간단, 가벼움
- 사용 사례: 소규모 클러스터, 개발 환경
4. **Weave Net (역사적 예시)**: 2024년 6월에 프로젝트가 보관 상태로 전환되었으므로 새 배포에는 유지 관리되는 대안을 검토하세요.
**CNI 구성 예시 (Calico)**:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: calico-config
namespace: kube-system
data:
calico_backend: "bird"
cni_network_config: |-
{
"name": "k8s-pod-network",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "calico",
"log_level": "info",
"datastore_type": "kubernetes",
"nodename": "__KUBERNETES_NODE_NAME__",
"mtu": __CNI_MTU__,
"ipam": {
"type": "calico-ipam"
},
"policy": {
"type": "k8s"
},
"kubernetes": {
"kubeconfig": "__KUBECONFIG_FILEPATH__"
}
},
{
"type": "portmap",
"snat": true,
"capabilities": {"portMappings": true}
}
]
}
```
### 서비스 네트워킹
Kubernetes 서비스는 파드 집합에 대한 안정적인 엔드포인트를 제공합니다. 서비스는 ClusterIP, NodePort, LoadBalancer, ExternalName 등 여러 유형이 있습니다.
**서비스 네트워킹 구성 요소**:
1. **ClusterIP**: 클러스터 내부에서만 접근 가능한 가상 IP
2. **kube-proxy**: 서비스 IP에 대한 트래픽을 파드로 라우팅
3. **CoreDNS**: 서비스 디스커버리를 위한 DNS 서비스
**서비스 네트워킹 흐름**:
```
클라이언트 -> kube-proxy가 설정한 커널 규칙 (ClusterIP DNAT) -> 파드
```
**서비스 예시**:
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
type: ClusterIP
```
### 인그레스 네트워킹
인그레스는 클러스터 외부에서 클러스터 내부 서비스로의 HTTP 및 HTTPS 라우팅을 관리합니다. 인그레스 컨트롤러는 인그레스 리소스를 구현하는 역할을 합니다.
**주요 인그레스 컨트롤러**:
1. **유지 관리되는 Ingress/Gateway 컨트롤러**: 수명 주기와 Gateway API 지원 확인
2. **AWS Load Balancer Controller**: Ingress용 ALB 프로비저닝
3. **Traefik**: 클라우드 네이티브 엣지 라우터
4. **HAProxy Ingress**: HAProxy 기반 인그레스 컨트롤러
**인그레스 네트워킹 흐름**:
```
클라이언트 -> 인그레스 컨트롤러 -> 서비스 -> 파드
```
커뮤니티 ingress-nginx는 2026년 3월에 유지 관리가 종료되었습니다([공지](https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/)). 이 예시는 `traefik` IngressClass를 가진 Traefik 컨트롤러 설치가 필요하며 `/app` 경로를 그대로 전달합니다.
**인그레스 예시**:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
ingressClassName: traefik
rules:
- host: example.com
http:
paths:
- path: /app
pathType: Prefix
backend:
service:
name: my-service
port:
number: 80
```
### 네트워크 정책
네트워크 정책은 파드 간의 통신을 제어하는 방법을 제공합니다. 기본적으로 모든 파드는 서로 통신할 수 있지만, 네트워크 정책을 사용하면 이를 제한할 수 있습니다.
**네트워크 정책 예시**:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-network-policy
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 3306
egress:
- to:
- podSelector:
matchLabels:
role: monitoring
ports:
- protocol: TCP
port: 9090
```
### 네트워크 문제 해결
Kubernetes 네트워킹 문제를 해결하기 위한 일반적인 도구와 명령어:
1. **ping, traceroute**: 기본적인 네트워크 연결 테스트
2. **tcpdump**: 네트워크 패킷 캡처 및 분석
3. **netstat, ss**: 네트워크 연결 상태 확인
4. **nslookup, dig**: DNS 조회 테스트
5. **kubectl exec**: 파드 내에서 네트워크 명령 실행
**네트워크 디버깅 예시**:
```bash
# 파드 내에서 네트워크 연결 테스트
kubectl exec -it -- ping
# 파드 내에서 DNS 조회 테스트
kubectl exec -it -- nslookup
# 파드 내에서 네트워크 패킷 캡처
kubectl exec -it -- tcpdump -i eth0 -n
# 서비스 엔드포인트 확인
kubectl get endpointslices -l kubernetes.io/service-name=
```
## 클러스터 스토리지
Kubernetes 스토리지는 컨테이너화된 애플리케이션에 데이터 지속성을 제공합니다. Kubernetes는 다양한 스토리지 옵션과 추상화를 제공하여 애플리케이션이 스토리지를 효율적으로 사용할 수 있게 합니다.
### 스토리지 아키텍처
Kubernetes 스토리지 아키텍처는 다음과 같은 구성 요소로 이루어져 있습니다:
1. **볼륨**: 파드 내의 컨테이너에 마운트할 수 있는 디렉토리
2. **영구 볼륨 (PV)**: 클러스터의 스토리지 리소스
3. **영구 볼륨 클레임 (PVC)**: 사용자의 스토리지 요청
4. **스토리지 클래스**: 스토리지의 "클래스" 또는 유형을 정의
5. **CSI (Container Storage Interface)**: 스토리지 시스템과의 표준 인터페이스
**스토리지 아키텍처 흐름**:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-01-cluster-architecture-9.html)
### 볼륨 유형
Kubernetes는 다양한 유형의 볼륨을 지원합니다:
1. **임시 볼륨**:
- **emptyDir**: 빈 디렉토리로 시작하며, 파드가 삭제되면 함께 삭제됨
- **configMap**: ConfigMap을 볼륨으로 마운트
- **secret**: Secret을 볼륨으로 마운트
- **downwardAPI**: 파드 및 컨테이너 정보를 파일로 노출
2. **영구 볼륨**:
- **CSI 기반 클라우드 블록 스토리지**: AWS EBS, Azure Disk, GCE Persistent Disk (레거시 인트리 구현은 제거됨)
- **nfs**: NFS 볼륨
- **csi**: CSI 드라이버를 통한 볼륨
**볼륨 예시**:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: test-pd
spec:
containers:
- name: test-container
image: nginx
volumeMounts:
- mountPath: /test-pd
name: test-volume
volumes:
- name: test-volume
persistentVolumeClaim:
claimName: test-pvc
```
### 영구 볼륨 및 클레임
영구 볼륨(PV)은 관리자가 프로비저닝하거나 스토리지 클래스를 통해 동적으로 프로비저닝되는 클러스터의 스토리지 리소스입니다. 영구 볼륨 클레임(PVC)은 사용자의 스토리지 요청입니다.
먼저 IAM 권한을 갖춘 EBS CSI 드라이버를 설치하세요. 아래 정적 PV의 볼륨 ID와 가용 영역은 실제 기존 EBS 볼륨에 맞게 변경합니다. EBS는 Fargate에서 마운트할 수 없으며 EKS Auto Mode는 `ebs.csi.eks.amazonaws.com`을 사용합니다.
**영구 볼륨 예시**:
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-example
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: standard
csi:
driver: ebs.csi.aws.com
volumeHandle: vol-0123456789abcdef0
fsType: ext4
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: [ap-northeast-2a]
```
**영구 볼륨 클레임 예시**:
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-example
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: standard
```
### 스토리지 클래스
스토리지 클래스는 관리자가 제공하는 스토리지의 "클래스"를 설명합니다. 스토리지 클래스를 사용하면 PVC가 요청될 때 동적으로 PV를 프로비저닝할 수 있습니다.
**스토리지 클래스 예시**:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
provisioner: ebs.csi.aws.com
parameters:
type: gp3
csi.storage.k8s.io/fstype: ext4
encrypted: "true"
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
```
### CSI (Container Storage Interface)
CSI는 Kubernetes와 스토리지 시스템 간의 표준 인터페이스를 제공합니다. CSI를 통해 스토리지 제공업체는 Kubernetes 코드를 수정하지 않고도 자체 스토리지 드라이버를 개발할 수 있습니다.
**CSI 아키텍처**:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-01-cluster-architecture-10.html)
**CSI 드라이버 배포 예시**:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-sc
provisioner: ebs.csi.aws.com
parameters:
type: gp3
csi.storage.k8s.io/fstype: ext4
encrypted: "true"
volumeBindingMode: WaitForFirstConsumer
```
### 스토리지 모범 사례
Kubernetes 스토리지 사용에 대한 모범 사례:
1. **적절한 스토리지 유형 선택**: 워크로드 특성에 맞는 스토리지 유형 선택
2. **동적 프로비저닝 사용**: 스토리지 클래스를 통한 동적 프로비저닝 활용
3. **적절한 접근 모드 선택**: 워크로드 요구사항에 맞는 접근 모드 선택
4. **리소스 요청 및 제한 설정**: 적절한 스토리지 용량 요청
5. **백업 및 복구 전략 수립**: 중요 데이터에 대한 백업 및 복구 전략 마련
6. **스토리지 모니터링**: 스토리지 사용량 및 성능 모니터링
## 클러스터 확장성
Kubernetes 클러스터의 확장성은 클러스터가 증가하는 부하와 요구사항을 처리할 수 있는 능력을 의미합니다. 확장성은 수평적 확장(스케일 아웃)과 수직적 확장(스케일 업) 두 가지 방식으로 구현될 수 있습니다.
### 클러스터 규모 제한
업스트림 [대규모 클러스터 지침](https://kubernetes.io/docs/setup/best-practices/cluster-large/)은 노드 5,000개, 전체 파드 150,000개, 전체 컨테이너 300,000개, 노드당 파드 110개를 검증된 지원 범위로 설명합니다. 이 기준은 동시에 적용되며 보편적인 API 하드 리밋이 아닙니다. 일반적인 파드당 컨테이너 20개 제한은 없습니다. Service 용량은 주소 범위와 데이터 플레인에 따라 달라지고 클라우드 네트워킹 한도·할당량은 더 낮을 수 있습니다.
### 수평적 확장
수평적 확장은 더 많은 노드를 추가하여 클러스터의 용량을 늘리는 방식입니다.
**노드 자동 확장**:
Kubernetes 클러스터 자동 확장기(Cluster Autoscaler)는 워크로드 요구사항에 따라 노드 수를 자동으로 조정합니다.
```yaml
# AWS Auto Scaling Group 태그 예시
tags:
k8s.io/cluster-autoscaler/enabled: "true"
k8s.io/cluster-autoscaler/my-cluster: "owned"
```
**Cluster Autoscaler Deployment 일부 (v1.36 클러스터 예시)**: 클러스터 마이너 버전과 일치하는 패치 릴리스를 선택하세요. ServiceAccount, Kubernetes RBAC, 전용 IAM 역할이 추가로 필요하며 완전한 설치 매니페스트가 아닙니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cluster-autoscaler
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: cluster-autoscaler
template:
metadata:
labels:
app: cluster-autoscaler
spec:
containers:
- name: cluster-autoscaler
image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.36.0
command:
- ./cluster-autoscaler
- --cloud-provider=aws
- --nodes=2:10:my-asg-group
- --scale-down-unneeded-time=10m
```
**Karpenter**:
Karpenter는 AWS에서 개발한 새로운 노드 자동 확장 도구로, 더 빠르고 효율적인 노드 프로비저닝을 제공합니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default-class
limits:
cpu: 1000
memory: 1000Gi
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default-class
spec:
role: KarpenterNodeRole-my-cluster
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
```
### 수직적 확장
워크로드 수직 확장은 파드 CPU·메모리 요청량을 조정합니다. VPA는 기반 노드 크기를 변경하지 않으며 노드 용량은 별도로 확보해야 합니다.
**Vertical Pod Autoscaler (VPA)**:
VPA는 파드의 CPU 및 메모리 요청을 자동으로 조정합니다.
```yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-app-vpa
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: my-app
updatePolicy:
updateMode: "Recreate"
resourcePolicy:
containerPolicies:
- containerName: '*'
minAllowed:
cpu: 100m
memory: 50Mi
maxAllowed:
cpu: 1
memory: 500Mi
```
### 애플리케이션 확장
애플리케이션 수준에서의 확장은 파드 복제본 수를 조정하여 구현됩니다.
**Horizontal Pod Autoscaler (HPA)**:
HPA는 CPU 사용률이나 사용자 정의 메트릭에 따라 파드 복제본 수를 자동으로 조정합니다.
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80
```
**KEDA (Kubernetes Event-driven Autoscaling)**:
KEDA는 이벤트 기반 자동 확장을 제공하여 다양한 이벤트 소스에 기반한 확장을 가능하게 합니다.
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: my-app-scaledobject
spec:
scaleTargetRef:
name: my-app
minReplicaCount: 0
maxReplicaCount: 10
triggers:
- type: kafka
metadata:
bootstrapServers: kafka.svc:9092
consumerGroup: my-group
topic: my-topic
lagThreshold: "10"
```
### 확장성 모범 사례
Kubernetes 클러스터 확장성을 위한 모범 사례:
1. **리소스 요청 및 제한 설정**: 모든 파드에 적절한 리소스 요청 및 제한 설정
2. **노드 풀 전략**: 워크로드 특성에 맞는 여러 노드 풀 구성
3. **자동 확장 구성**: 클러스터 자동 확장기, HPA, VPA 적절히 구성
4. **효율적인 파드 배치**: 노드 어피니티, 파드 어피니티/안티-어피니티 활용
5. **클러스터 모니터링**: 리소스 사용량 및 성능 지속적 모니터링
6. **부하 테스트**: 확장 전략 검증을 위한 정기적인 부하 테스트
## 클러스터 보안
Kubernetes 클러스터 보안은 여러 계층에서 구현되어야 합니다. 이는 인증, 권한 부여, 네트워크 정책, 파드 보안 등을 포함합니다.
### 인증 (Authentication)
Kubernetes API 서버에 대한 접근을 인증하는 방법:
1. **X.509 인증서**: TLS 클라이언트 인증서를 사용한 인증
2. **서비스 계정 토큰**: 파드 내에서 API 서버 접근을 위한 토큰
3. **OpenID Connect (OIDC)**: 외부 ID 제공자를 통한 인증
4. **웹훅 토큰 인증**: 외부 인증 서비스를 통한 인증
5. **인증 프록시**: 인증 프록시를 통한 인증
**kubeconfig 예시**:
```yaml
apiVersion: v1
kind: Config
clusters:
- name: my-cluster
cluster:
certificate-authority-data:
server: https://api.my-cluster.example.com
users:
- name: admin
user:
client-certificate-data:
client-key-data:
contexts:
- name: my-context
context:
cluster: my-cluster
user: admin
current-context: my-context
```
### 권한 부여 (Authorization)
인증된 사용자의 작업을 제어하는 방법:
1. **RBAC (Role-Based Access Control)**: 역할 기반 접근 제어
2. **ABAC (Attribute-Based Access Control)**: 속성 기반 접근 제어
3. **Node Authorization**: 노드에 대한 특별한 권한 부여
4. **Webhook Authorization**: 외부 서비스를 통한 권한 부여
**RBAC 예시**:
```yaml
# 역할 정의
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
---
# 역할 바인딩
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
- kind: User
name: jane
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
```
### 네트워크 보안
클러스터 내 네트워크 트래픽을 보호하는 방법:
1. **네트워크 정책**: 파드 간 통신 제어
2. **암호화된 통신**: TLS를 통한 통신 암호화
3. **서비스 메시**: Istio, Linkerd 등을 통한 고급 네트워크 보안
**네트워크 정책 예시**:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
```
### 파드 보안
파드 수준에서의 보안 구현:
1. **파드 보안 컨텍스트**: 파드 및 컨테이너 수준의 보안 설정
2. **파드 보안 표준**: 파드 보안 요구사항 정의
3. **seccomp 프로필**: 시스템 호출 제한
4. **AppArmor/SELinux**: 강제적 접근 제어
**파드 보안 컨텍스트 예시**:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: security-context-pod
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
containers:
- name: app
image: myapp:1.0
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
```
### 시크릿 관리
민감한 정보를 안전하게 관리하는 방법:
1. **Kubernetes 시크릿**: 기본 시크릿 리소스 사용
2. **암호화된 etcd**: etcd에 저장된 시크릿 암호화
3. **외부 시크릿 관리**: HashiCorp Vault, AWS Secrets Manager 등 활용
**암호화된 etcd 구성 예시**:
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret:
- identity: {}
```
### 보안 모범 사례
Kubernetes 클러스터 보안을 위한 모범 사례:
1. **최소 권한 원칙**: 필요한 최소한의 권한만 부여
2. **정기적인 업데이트**: 클러스터 및 구성 요소 정기적 업데이트
3. **네트워크 분리**: 네트워크 정책을 통한 파드 간 통신 제한
4. **이미지 보안**: 신뢰할 수 있는 이미지만 사용, 취약점 스캐닝 구현
5. **감사 로깅**: 클러스터 활동에 대한 감사 로그 활성화
6. **보안 벤치마크**: CIS 벤치마크 등 보안 표준 준수
## 클러스터 업그레이드
Kubernetes 클러스터 업그레이드는 새로운 기능, 보안 패치, 버그 수정을 적용하기 위해 필요합니다. 업그레이드는 신중하게 계획하고 실행해야 합니다.
### 2026년 8월 업데이트: Kubernetes v1.37 "Garhwal" 정식 릴리스
2026년 8월 26일 [Kubernetes v1.37 "Garhwal"](https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release/)이 예정대로 정식 릴리스되었습니다. 이번 릴리스에는 총 67개의 개선 사항이 포함되었으며, 그중 16개가 Stable, 23개가 Beta로 승격되었고 나머지는 Alpha로 도입되었습니다. 주요 내용:
- **파드 인증서와 ClusterTrustBundle의 Stable 승격**: 서비스 어카운트 토큰 대신 워크로드에 X.509 인증서를 자동 발급·로테이션하는 PodCertificate 기능과 신뢰 앵커(trust anchor) 배포용 ClusterTrustBundle이 표준 기능이 되었습니다 ([상세 글](https://kubernetes.io/blog/2026/08/28/kubernetes-v1-37-pod-certificates-and-cluster-trust-bundles/))
- **Metrics API(metrics.k8s.io) GA**: `kubectl top`과 HPA가 사용하는 리소스 메트릭 API가 정식(stable) 버전으로 졸업했습니다 ([상세 글](https://kubernetes.io/blog/2026/08/27/kubernetes-v1-37-metrics-api-ga/))
- 그 외 **Stable**: DRA(Dynamic Resource Allocation) 기능 다수, watchcache 초기화 복원력 개선 등 / **Beta**: HPA scale-to-zero, 매니페스트 기반 어드미션 컨트롤 구성 등 / **Alpha**: 파드 수준 체크포인트·복원 등
- **사용 중단(deprecation)**: kube-dns와 `kubectl run --filename/-f`가 사용 중단되었고(`ipvs` 모드는 이미 v1.35부터 사용 중단),, 정적(static) 파드의 Secret/ConfigMap 참조가 금지되었습니다. cgroup v1 지원 제거도 계속 진행 중입니다.
업그레이드 전에는 [공식 릴리스 노트](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.37.md)에서 사용 중단·제거 항목을 반드시 확인하세요.
### 업그레이드 전략
Kubernetes 클러스터 업그레이드를 위한 전략:
1. **블루/그린 업그레이드**: 새 버전의 클러스터를 별도로 생성하고 워크로드 마이그레이션
2. **인플레이스 업그레이드**: 기존 클러스터를 직접 업그레이드
3. **카나리 업그레이드**: 일부 노드만 먼저 업그레이드하여 검증
### 업그레이드 순서
Kubernetes 클러스터 업그레이드의 일반적인 순서:
1. **컨트롤 플레인 업그레이드**: kube-apiserver, kube-controller-manager, kube-scheduler, etcd
2. **DNS 및 CNI 업그레이드**: CoreDNS, CNI 플러그인 등 주요 애드온 업그레이드
3. **워커 노드 업그레이드**: 워커 노드 순차적 업그레이드
**kubeadm 업그레이드 순서**:
[해당 버전의 kubeadm 업그레이드 문서](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/)에 따라 대상 마이너 버전의 `pkgs.k8s.io` 패키지 저장소를 설정합니다. 한 번에 한 마이너 버전만 업그레이드하세요.
1. etcd 백업과 애드온 호환성을 확인합니다. 첫 컨트롤 플레인 노드에서 `kubeadm`을 먼저 업그레이드하고 `kubeadm upgrade plan`, `kubeadm upgrade apply `을 실행합니다.
2. 추가 컨트롤 플레인 노드는 `kubeadm` 업그레이드 후 `kubeadm upgrade node`를 실행합니다.
3. 각 노드는 kubelet 업그레이드 전에 drain하고 선택한 패치로 kubelet/kubectl 패키지를 업데이트합니다. systemd 구성을 다시 읽고 kubelet을 재시작한 뒤 Ready 확인 후 uncordon합니다.
4. 워커는 `kubeadm` 업그레이드와 `kubeadm upgrade node` 후 drain/kubelet/uncordon 순서를 수행합니다. kubelet 버전은 API 서버보다 높으면 안 됩니다.
### 업그레이드 고려사항
Kubernetes 클러스터 업그레이드 시 고려해야 할 사항:
1. **API 변경사항**: 새 버전에서의 API 변경사항 확인
2. **기능 게이트**: 새로운 기능 게이트 및 기본값 변경 확인
3. **종속성**: CNI, CSI 등 종속 구성 요소의 호환성 확인
4. **다운타임**: 업그레이드 중 예상되는 다운타임 계획
5. **롤백 계획**: 문제 발생 시 롤백 계획 수립
### 업그레이드 모범 사례
Kubernetes 클러스터 업그레이드를 위한 모범 사례:
1. **테스트 환경에서 먼저 테스트**: 프로덕션 환경 업그레이드 전 테스트 환경에서 검증
2. **점진적 업그레이드**: 한 번에 한 마이너 버전씩 업그레이드
3. **백업**: 업그레이드 전 etcd 데이터 백업
4. **문서화**: 업그레이드 절차 및 결과 문서화
5. **모니터링**: 업그레이드 중 및 후 클러스터 상태 모니터링
6. **업그레이드 윈도우**: 트래픽이 적은 시간대에 업그레이드 수행
## Amazon EKS 클러스터 아키텍처
Amazon EKS(Elastic Kubernetes Service)는 AWS에서 제공하는 관리형 Kubernetes 서비스입니다. EKS는 Kubernetes의 기본 기능을 모두 제공하면서도 AWS 서비스와의 통합과 관리 편의성을 추가로 제공합니다.
### EKS 아키텍처 개요
EKS 클러스터는 다음과 같은 구성 요소로 이루어져 있습니다:
1. **EKS 컨트롤 플레인**: AWS에서 관리하는 Kubernetes 컨트롤 플레인
2. **EKS 노드**: 사용자가 관리하는 워커 노드 (EC2 인스턴스)
3. **EKS 관리형 노드 그룹**: AWS에서 관리하는 노드 그룹
4. **EKS Fargate 프로필**: 서버리스 컨테이너 실행 환경
5. **VPC 및 서브넷**: 클러스터 네트워킹을 위한 VPC 및 서브넷
**EKS 아키텍처 다이어그램**:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-01-cluster-architecture-11.html)
### EKS 컨트롤 플레인
EKS 컨트롤 플레인은 AWS에서 관리하며, 여러 가용 영역에 걸쳐 고가용성을 제공합니다.
**주요 특징**:
1. **관리형 서비스**: AWS에서 컨트롤 플레인 관리 및 업그레이드
2. **고가용성**: 여러 가용 영역에 걸쳐 배포
3. **자동 확장**: 부하에 따라 자동 확장
4. **보안**: AWS 보안 서비스와 통합
### EKS 노드 유형
EKS는 다양한 유형의 노드를 지원합니다:
1. **자체 관리형 노드**: 사용자가 직접 EC2 인스턴스 관리
2. **관리형 노드 그룹**: AWS에서 노드 수명 주기 관리
3. **Fargate**: 서버리스 컨테이너 실행 환경
4. **EKS Auto Mode**: AWS가 컴퓨팅과 통합 인프라 관리
5. **EKS Hybrid Nodes**: 고객이 관리하는 온프레미스 노드
Bottlerocket은 별도 컴퓨팅 관리 유형이 아니라 노드 운영체제입니다.
**관리형 노드 그룹 예시**:
```yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-cluster
region: ap-northeast-2
managedNodeGroups:
- name: ng-1
instanceType: m5.large
desiredCapacity: 3
minSize: 2
maxSize: 5
volumeSize: 80
privateNetworking: true
labels:
role: worker
tags:
nodegroup-role: worker
```
### EKS 네트워킹
EKS 네트워킹은 Amazon VPC를 기반으로 하며, 다음과 같은 구성 요소를 포함합니다:
1. **VPC CNI 플러그인**: AWS VPC 네트워킹과의 통합
2. **보안 그룹**: 노드 및 파드 수준의 네트워크 보안
3. **로드 밸런서 통합**: ELB, ALB, NLB와의 통합
4. **VPC 엔드포인트**: AWS 서비스와의 프라이빗 통신
**VPC CNI 애드온 구성 예시** (ConfigMap이 아닌 JSON 구성 값):
```json
{
"enableNetworkPolicy": "true",
"env": {
"WARM_IP_TARGET": "5",
"MINIMUM_IP_TARGET": "10"
}
}
```
설치된 애드온의 기존 설정과 병합하고 해당 버전의 스키마를 확인한 후 업데이트하세요. Pod ENI는 `ENABLE_POD_ENI` 환경 변수와 호환 노드, IAM 권한, SecurityGroupPolicy가 추가로 필요하며 ConfigMap 키만으로 활성화되지 않습니다. [네트워크 정책 구성 문서](https://docs.aws.amazon.com/eks/latest/userguide/cni-network-policy-configure.html)를 참고하세요.
### EKS 스토리지
EKS는 다양한 AWS 스토리지 서비스와 통합됩니다:
1. **EBS CSI 드라이버**: Amazon EBS 볼륨 관리
2. **EFS CSI 드라이버**: Amazon EFS 파일 시스템 관리
3. **FSx for Lustre CSI 드라이버**: FSx for Lustre 파일 시스템 관리
4. **S3**: 객체 스토리지
**EBS CSI 드라이버 예시**:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-sc
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: "true"
volumeBindingMode: WaitForFirstConsumer
```
### EKS 보안
EKS는 AWS의 보안 서비스와 통합되어 강력한 보안을 제공합니다:
1. **IAM 통합**: AWS IAM과 Kubernetes RBAC의 통합
2. **VPC 보안**: VPC 보안 그룹 및 네트워크 ACL
3. **AWS KMS**: 시크릿 암호화를 위한 KMS 통합
4. **AWS WAF**: 웹 애플리케이션 방화벽 통합
5. **AWS Shield**: DDoS 보호
**IAM 역할 서비스 계정 예시**:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: s3-reader
namespace: default
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/s3-reader-role
```
### EKS 모니터링 및 로깅
EKS는 AWS의 모니터링 및 로깅 서비스와 통합됩니다:
1. **CloudWatch Container Insights**: 컨테이너 모니터링
2. **CloudWatch Logs**: 로그 수집 및 분석
3. **X-Ray**: 분산 추적
4. **Prometheus 및 Grafana**: 오픈소스 모니터링 도구 통합
**CloudWatch Container Insights 예시**:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: amazon-cloudwatch
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: cloudwatch-agent
namespace: amazon-cloudwatch
spec:
selector:
matchLabels:
name: cloudwatch-agent
template:
metadata:
labels:
name: cloudwatch-agent
spec:
containers:
- name: cloudwatch-agent
image: amazon/cloudwatch-agent:1.247347.6b250880
# ... 추가 구성
```
### EKS 비용 최적화
EKS 클러스터의 비용을 최적화하는 방법:
1. **Spot 인스턴스**: 비용 효율적인 Spot 인스턴스 활용
2. **Fargate**: 서버리스 컨테이너 실행으로 유휴 리소스 비용 절감
3. **자동 확장**: 클러스터 자동 확장기를 통한 리소스 최적화
4. **Graviton 프로세서**: ARM 기반 Graviton 인스턴스 활용
5. **리소스 요청 최적화**: 적절한 리소스 요청 및 제한 설정
**Spot 인스턴스 노드 그룹 예시**:
```yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-cluster
region: ap-northeast-2
managedNodeGroups:
- name: spot-ng
instanceTypes: ["m5.large", "m5a.large", "m5d.large", "m5ad.large"]
spot: true
desiredCapacity: 3
minSize: 2
maxSize: 10
```
## 더 알아보기
이 문서에서 다룬 클러스터 아키텍처에 대한 이해를 더욱 깊게 하려면 다음 주제들을 참조하세요:
- [Kubernetes 소개](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md) - Kubernetes의 기본 개념과 역사
- [파드와 워크로드](https://www.atomai.click/kubernetes-docs/llms/ko/core/02-pods-and-workloads.md) - 클러스터에서 실행되는 워크로드 관리
- [서비스와 네트워킹](https://www.atomai.click/kubernetes-docs/llms/ko/core/03-services-networking.md) - 클러스터 내 네트워킹 구성
- [스케줄링, 선점 및 축출](https://www.atomai.click/kubernetes-docs/llms/ko/core/08-scheduling-preemption-eviction.md) - 노드에 파드를 배치하는 방법
- [클러스터 관리](https://www.atomai.click/kubernetes-docs/llms/ko/core/09-cluster-administration.md) - 클러스터 운영 및 관리
- [EKS 소개](https://www.atomai.click/kubernetes-docs/llms/ko/eks/01-eks-introduction.md) - Amazon EKS 서비스 개요
- [EKS 클러스터 생성](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part1.md) - EKS 클러스터 생성 방법
### 실습 및 심화 학습
- [Kubernetes 공식 튜토리얼](https://kubernetes.io/docs/tutorials/) - 실습을 통한 학습
- [Kubernetes The Hard Way](https://github.com/kelseyhightower/kubernetes-the-hard-way) - 수동으로 Kubernetes 클러스터 구축하기
- [Cilium 네트워킹](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/01-introduction.md) - 고급 네트워킹 및 보안 기능
## 결론
이 문서에서는 Kubernetes 클러스터의 아키텍처, 주요 구성 요소, 그리고 이들이 어떻게 함께 작동하는지에 대해 자세히 살펴보았습니다. 또한 클러스터의 네트워킹, 스토리지, 확장성, 보안, 업그레이드 등 중요한 측면들을 다루었으며, Amazon EKS 클러스터의 아키텍처에 대해서도 알아보았습니다.
Kubernetes 클러스터의 아키텍처를 이해하는 것은 효과적인 클러스터 설계, 배포, 운영을 위한 기반이 됩니다. 이 지식을 바탕으로 안정적이고 확장 가능하며 보안이 강화된 Kubernetes 환경을 구축할 수 있습니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [클러스터 아키텍처 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/core/01-cluster-architecture-quiz)를 풀어보세요.
## 참고 자료
- [Kubernetes 공식 문서](https://kubernetes.io/docs/)
- [Amazon EKS 문서](https://docs.aws.amazon.com/eks/)
- [Kubernetes The Hard Way](https://github.com/kelseyhightower/kubernetes-the-hard-way)
- [Kubernetes Patterns](https://www.oreilly.com/library/view/kubernetes-patterns/9781492050278/)
- [Kubernetes Up & Running](https://www.oreilly.com/library/view/kubernetes-up-and/9781492046523/)
- [Kubernetes Best Practices](https://www.oreilly.com/library/view/kubernetes-best-practices/9781492056461/)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/core/02-pods-and-workloads
----------------------------------------
# Kubernetes 파드와 워크로드
> **지원 버전**: Kubernetes 1.35, 1.36, 1.37
> **마지막 업데이트**: 2026년 2월 23일
이 문서에서는 Kubernetes의 기본 실행 단위인 파드(Pod)와 이를 관리하는 다양한 워크로드 리소스에 대해 자세히 설명합니다. 파드의 개념부터 시작하여 디플로이먼트, 스테이트풀셋, 데몬셋 등 다양한 워크로드 리소스의 특징과 사용 사례를 다룹니다.
## 실습 환경 설정
이 문서의 예제를 따라하기 위해서는 다음과 같은 도구와 환경이 필요합니다:
### 필수 도구
- API 서버와 마이너 버전 차이가 1 이내인 kubectl
- 작동하는 Kubernetes 클러스터 (EKS, minikube, kind 등)
### 예제 애플리케이션 배포
```bash
# 네임스페이스 생성
kubectl create namespace workloads-demo
# 간단한 디플로이먼트 생성
kubectl -n workloads-demo apply -f - < **핵심 개념**: 파드(Pod)는 Kubernetes의 가장 작은 배포 가능한 컴퓨팅 단위로, 하나 이상의 컨테이너 그룹으로 구성되며 스토리지와 네트워크를 공유합니다.
파드(Pod)는 Kubernetes의 가장 작은 배포 가능한 컴퓨팅 단위입니다. 파드는 하나 이상의 컨테이너 그룹으로, 스토리지와 네트워크를 공유하며 함께 스케줄링됩니다.
### 파드의 특징
1. **공유 컨텍스트**: 컨테이너는 파드 네트워크와 보통 IPC를 공유하며 프로세스 네임스페이스 공유에는 `shareProcessNamespace: true`가 필요합니다. 컨테이너 루트 파일시스템은 별개입니다.
2. **동일한 노드**: 파드의 모든 컨테이너는 항상 같은 노드에서 실행됩니다.
3. **고유한 IP 주소**: 각 파드는 클러스터 내에서 고유한 IP 주소를 가집니다.
4. **임시적(Ephemeral)**: 파드는 기본적으로 임시적이며, 장애 발생 시 새로운 파드로 대체될 수 있습니다.
5. **원자적 단위**: 파드는 배포, 스케줄링, 복제의 원자적 단위입니다.
### 파드 구조
파드는 다음과 같은 구성 요소로 이루어져 있습니다:
1. **컨테이너**: 파드 내에서 실행되는 하나 이상의 컨테이너
2. **볼륨**: 파드 내의 컨테이너가 공유하는 스토리지
3. **네트워크**: 파드에 할당된 IP 주소와 포트
4. **컨테이너 스펙**: 컨테이너 이미지, 환경 변수, 리소스 요구사항 등

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-02-pods-and-workloads-0.html)
### 파드 예제
```yaml
apiVersion: v1
kind: Pod
metadata:
name: multi-container-pod
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.30.4
ports:
- containerPort: 80
volumeMounts:
- name: shared-data
mountPath: /usr/share/nginx/html
- name: content-updater
image: alpine
command: ["/bin/sh", "-c"]
args:
- |
while true; do
echo "현재 시간: $(date)" > /content/index.html;
sleep 10;
done
volumeMounts:
- name: shared-data
mountPath: /content
volumes:
- name: shared-data
emptyDir: {}
```
### 실제 사용 예제: 웹 애플리케이션 파드
다음은 웹 애플리케이션과 사이드카 컨테이너를 포함하는 파드의 예제입니다:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: web-app
labels:
app: web
environment: production
spec:
containers:
- name: web-application
image: nginx:1.30.4
volumeMounts:
- name: log-volume
mountPath: /var/log/nginx
ports:
- containerPort: 80
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
- name: log-collector
image: fluentd:v1.14
volumeMounts:
- name: log-volume
mountPath: /var/log/nginx
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
volumes:
- name: log-volume
emptyDir: {}
```
이 예제는 다음과 같은 실제 시나리오를 보여줍니다:
- 주 컨테이너로 Nginx 웹 서버 실행
- 사이드카 컨테이너로 Fluentd 로그 수집기 실행
- 두 컨테이너 간에 로그 볼륨 공유
- 각 컨테이너에 대한 리소스 요청 및 제한 설정
이러한 구성은 마이크로서비스 아키텍처에서 로깅, 모니터링, 프록시 등의 기능을 분리하면서도 밀접하게 연결된 컨테이너를 실행하는 데 적합합니다.
이 로그 예시는 볼륨 연결 구조를 보여줍니다. Fluentd에는 별도 tail 입력과 출력 설정이 필요하며 디렉토리 마운트만으로 로그를 수집하지는 않습니다. 이미지와 애플리케이션 설정은 운영 버전 권장값이 아닌 예시입니다. 네이티브 사이드카는 `initContainers`에 `restartPolicy: Always`를 사용하며 v1.33부터 Stable입니다. 일반 다중 컨테이너 파드는 시작·종료 순서를 보장하지 않습니다.
### 파드 정의
파드는 YAML 또는 JSON 형식의 매니페스트 파일로 정의됩니다. 다음은 기본적인 파드 정의 예시입니다:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.30.4
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
```
### 단일 컨테이너 vs 다중 컨테이너 파드
**단일 컨테이너 파드**:
- 가장 일반적인 사용 사례
- 하나의 애플리케이션 컨테이너만 포함
- 간단하고 직관적인 구조
**다중 컨테이너 파드**:
- 밀접하게 결합된 여러 컨테이너를 포함
- 컨테이너 간 로컬 통신 가능 (localhost)
- 공유 볼륨을 통한 데이터 공유
- 함께 스케일링되고 배치됨
### 다중 컨테이너 파드 패턴
1. **사이드카 패턴**: 주 컨테이너의 기능을 확장하는 보조 컨테이너
- 예: 로그 수집기, 파일 동기화, 프록시
```yaml
apiVersion: v1
kind: Pod
metadata:
name: web-with-sidecar
spec:
containers:
- name: web
image: nginx:1.30.4
volumeMounts:
- name: logs
mountPath: /var/log/nginx
- name: log-collector
image: fluentd:v1.14
volumeMounts:
- name: logs
mountPath: /var/log/nginx
volumes:
- name: logs
emptyDir: {}
```
2. **앰배서더 패턴**: 외부 서비스에 대한 프록시 역할을 하는 컨테이너
- 예: 데이터베이스 프록시, 서비스 메시 사이드카
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-ambassador
spec:
containers:
- name: app
image: myapp:1.0
- name: ambassador
image: envoyproxy/envoy:v1.20.0
ports:
- containerPort: 9901
```
3. **어댑터 패턴**: 주 컨테이너의 출력을 표준화하는 컨테이너
- 예: 로그 포맷 변환, 메트릭 변환
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-adapter
spec:
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: app-logs
mountPath: /var/log/app
- name: adapter
image: adapter:1.0
volumeMounts:
- name: app-logs
mountPath: /var/log/app
volumes:
- name: app-logs
emptyDir: {}
```
4. **초기화 컨테이너 패턴**: 주 컨테이너 시작 전에 실행되는 컨테이너
- 예: 설정 파일 생성, 데이터베이스 마이그레이션, 권한 설정
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-init
spec:
initContainers:
- name: init-db
image: busybox:1.34
command: ['sh', '-c', 'until nslookup db; do echo waiting for db; sleep 2; done;']
containers:
- name: app
image: myapp:1.0
```
### 파드 네트워킹
파드 내의 컨테이너는 다음과 같은 네트워킹 특성을 가집니다:
1. **동일한 IP 주소**: 파드 내의 모든 컨테이너는 동일한 IP 주소를 공유합니다.
2. **포트 공유**: 파드 내의 컨테이너는 포트 공간을 공유하므로, 보통 같은 IP·프로토콜·포트 조합에 동시에 바인딩할 수 없습니다.
3. **localhost 통신**: 파드 내의 컨테이너는 localhost를 통해 서로 통신할 수 있습니다.
4. **파드 간 통신**: 각 파드는 고유한 IP 주소를 가지며, 다른 파드와 직접 통신할 수 있습니다.
### 파드 스토리지
파드는 다양한 유형의 볼륨을 사용하여 데이터를 저장하고 공유할 수 있습니다:
1. **emptyDir**: 파드가 생성될 때 생성되고 삭제될 때 함께 삭제되는 임시 볼륨
2. **hostPath**: 호스트 노드의 파일 시스템에서 파드로 마운트되는 볼륨
3. **persistentVolumeClaim**: 영구 스토리지를 요청하는 볼륨
4. **configMap**: ConfigMap을 볼륨으로 마운트
5. **secret**: Secret을 볼륨으로 마운트
6. **projected**: 여러 볼륨 소스를 동일한 디렉토리에 매핑
```yaml
apiVersion: v1
kind: Pod
metadata:
name: pod-with-volumes
spec:
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: data
mountPath: /data
- name: config
mountPath: /etc/config
volumes:
- name: data
emptyDir: {}
- name: config
configMap:
name: app-config
```
## 파드 라이프사이클
파드는 생성부터 종료까지 여러 단계의 라이프사이클을 거칩니다. 이러한 라이프사이클을 이해하는 것은 애플리케이션의 안정성과 가용성을 보장하는 데 중요합니다.
### 파드 단계(Phase)
파드는 다음과 같은 단계를 거칩니다:
1. **Pending**: 파드가 클러스터에 승인되었지만, 하나 이상의 컨테이너가 아직 설정되지 않은 상태
2. **Running**: 파드가 노드에 바인딩되고, 모든 컨테이너가 생성되었으며, 적어도 하나의 컨테이너가 실행 중이거나 시작/재시작 중인 상태
3. **Succeeded**: 파드의 모든 컨테이너가 성공적으로 종료되고, 재시작되지 않을 상태
4. **Failed**: 파드의 모든 컨테이너가 종료되었고, 적어도 하나의 컨테이너가 실패로 종료된 상태
5. **Unknown**: 어떤 이유로 파드의 상태를 얻을 수 없는 상태
### 컨테이너 상태
파드 내의 각 컨테이너는 다음과 같은 상태를 가질 수 있습니다:
1. **Waiting**: 컨테이너가 실행되기 전의 상태 (이미지 다운로드 중, 의존성 대기 등)
2. **Running**: 프로세스가 실행 중이며 이것만으로 앱의 정상 상태·준비 완료를 의미하지는 않음
3. **Terminated**: 컨테이너가 실행을 완료했거나 어떤 이유로 실패한 상태
### 파드 조건(Condition)
파드는 다음과 같은 조건을 통해 상태를 더 자세히 나타냅니다:
1. **PodScheduled**: 파드가 노드에 스케줄링되었는지 여부
2. **ContainersReady**: 파드의 모든 컨테이너가 준비되었는지 여부
3. **Initialized**: 일반 init 컨테이너는 완료되고 재시작 가능한 init 컨테이너(네이티브 사이드카)는 시작되었는지 여부
4. **Ready**: 파드가 요청을 처리할 수 있고 서비스의 로드 밸런싱 풀에 추가될 수 있는지 여부
### 컨테이너 프로브(Probe)
Kubernetes는 컨테이너의 상태를 확인하기 위해 다음과 같은 프로브를 제공합니다:
1. **livenessProbe**: 컨테이너가 살아있는지 확인하고, 실패 시 컨테이너를 재시작
2. **readinessProbe**: 컨테이너가 요청을 처리할 준비가 되었는지 확인하고, 실패 시 서비스 트래픽에서 제외
3. **startupProbe**: 컨테이너 내의 애플리케이션이 시작되었는지 확인하고, 성공할 때까지 다른 프로브를 비활성화
```yaml
apiVersion: v1
kind: Pod
metadata:
name: pod-with-probes
spec:
containers:
- name: app
image: myapp:1.0
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
startupProbe:
httpGet:
path: /startup
port: 8080
failureThreshold: 30
periodSeconds: 10
```
### 파드 종료 프로세스
파드가 종료될 때 다음과 같은 프로세스가 진행됩니다:
1. **API 서버에 삭제 요청**: 사용자 또는 컨트롤러가 파드 삭제 요청
2. **종료 기간 시작**: 기본 종료 기간(30초) 설정
3. **API 업데이트**: API 서버가 파드의 삭제 타임스탬프 업데이트
4. **엔드포인트 갱신**: EndpointSlice가 해당 엔드포인트를 terminating·not ready로 표시하며 전파는 노드 종료와 동시에 진행
5. **종료 신호**: kubelet은 유예 시간 안에서 preStop 훅을 실행한 뒤 런타임에 종료 신호 요청 (보통 SIGTERM, 이미지·컨테이너 설정으로 변경 가능)
6. **그레이스풀 종료 대기**: 애플리케이션이 그레이스풀하게 종료될 시간 제공
7. **SIGKILL 신호**: 종료 기간 후에도 컨테이너가 종료되지 않으면 SIGKILL 신호 전송
8. **리소스 정리**: kubelet이 파드 리소스 정리
### 초기화 컨테이너(Init Containers)
초기화 컨테이너는 파드의 앱 컨테이너가 시작되기 전에 실행되는 특수한 컨테이너입니다:
1. **순차적 실행**: 초기화 컨테이너는 정의된 순서대로 하나씩 실행됨
2. **선행 조건**: 각 초기화 컨테이너는 이전 컨테이너가 성공적으로 완료된 후에만 시작됨
3. **실패 시 재시작**: 초기화 컨테이너가 실패하면 파드의 재시작 정책에 따라 재시작됨
4. **용도**: 앱 컨테이너 시작 전 설정, 의존성 확인, 권한 설정 등
```yaml
apiVersion: v1
kind: Pod
metadata:
name: init-pod
spec:
initContainers:
- name: init-myservice
image: busybox:1.34
command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']
- name: init-mydb
image: busybox:1.34
command: ['sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 2; done;']
containers:
- name: app
image: myapp:1.0
```
### 파드 중단(Disruption)
파드 중단은 자발적(Voluntary) 또는 비자발적(Involuntary) 중단으로 나눌 수 있습니다:
1. **자발적 중단**: 클러스터 관리자 또는 자동화 도구에 의한 중단
- 노드 드레이닝(Draining)
- 디플로이먼트 업데이트
- 파드 삭제
2. **비자발적 중단**: 하드웨어 장애, 커널 패닉, 네트워크 분할 등으로 인한 중단
PodDisruptionBudget은 drain처럼 Eviction API를 사용하는 자발적 축출을 제한합니다. 직접 파드 삭제와 Deployment 롤링 업데이트는 이를 우회하므로 롤아웃 가용성을 별도로 설정해야 하며 비자발적 장애를 막지는 못합니다.
## 파드 설계 패턴
파드를 설계할 때 고려해야 할 여러 패턴과 모범 사례가 있습니다. 이러한 패턴을 이해하고 적용하면 애플리케이션의 안정성, 확장성, 유지보수성을 향상시킬 수 있습니다.
### 단일 책임 원칙
파드는 단일 책임 원칙(Single Responsibility Principle)을 따라야 합니다:
1. **하나의 주요 기능**: 각 파드는 하나의 주요 기능 또는 프로세스를 담당해야 함
2. **독립적인 확장**: 각 기능이 독립적으로 확장될 수 있도록 설계
3. **분리된 라이프사이클**: 각 기능이 자체 라이프사이클을 가질 수 있도록 설계
### 파드 템플릿
파드 템플릿은 워크로드 리소스(디플로이먼트, 스테이트풀셋 등)에서 파드를 생성하는 데 사용되는 명세입니다:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template: # 파드 템플릿 시작
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.30.4
ports:
- containerPort: 80
# 파드 템플릿 끝
```
### 파드 어피니티와 안티-어피니티
파드 어피니티와 안티-어피니티는 파드가 어떤 노드에 스케줄링될지를 제어하는 규칙입니다:
1. **파드 어피니티(Pod Affinity)**: 특정 파드와 같은 노드 또는 토폴로지 도메인에 스케줄링
2. **파드 안티-어피니티(Pod Anti-Affinity)**: 특정 파드와 다른 노드 또는 토폴로지 도메인에 스케줄링
```yaml
apiVersion: v1
kind: Pod
metadata:
name: web-pod
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cache
topologyKey: "kubernetes.io/hostname"
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web
topologyKey: "kubernetes.io/hostname"
containers:
- name: web
image: nginx:1.30.4
```
### 노드 어피니티
노드 어피니티는 파드가 특정 노드에 스케줄링되도록 제한하는 규칙입니다:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu
operator: In
values:
- "true"
containers:
- name: gpu-container
image: gpu-app:1.0
```
### 테인트(Taint)와 톨러레이션(Toleration)
테인트는 노드에 적용되어 특정 파드가 스케줄링되지 않도록 하고, 톨러레이션은 파드에 적용되어 테인트가 있는 노드에 스케줄링될 수 있도록 합니다:
```bash
# 노드에 테인트 적용
kubectl taint nodes node1 key=value:NoSchedule
```
```yaml
# 파드에 톨러레이션 적용
apiVersion: v1
kind: Pod
metadata:
name: tolerant-pod
spec:
tolerations:
- key: "key"
operator: "Equal"
value: "value"
effect: "NoSchedule"
containers:
- name: app
image: myapp:1.0
```
### 리소스 요청과 제한
파드의 컨테이너에 대한 리소스 요청과 제한을 설정하는 것은 클러스터 리소스를 효율적으로 사용하고 안정성을 보장하는 데 중요합니다:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: resource-pod
spec:
containers:
- name: app
image: myapp:1.0
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
```
### 파드 보안 컨텍스트
보안 컨텍스트는 파드 또는 컨테이너 수준에서 보안 설정을 정의합니다:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: security-pod
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
containers:
- name: app
image: myapp:1.0
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
```
### 파드 우선순위와 선점
파드 우선순위와 선점은 클러스터 리소스가 부족할 때 어떤 파드가 스케줄링되고 어떤 파드가 선점될지를 결정합니다:
```yaml
# 우선순위 클래스 정의
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for critical pods only."
---
# 우선순위 클래스를 사용하는 파드
apiVersion: v1
kind: Pod
metadata:
name: high-priority-pod
spec:
priorityClassName: high-priority
containers:
- name: app
image: myapp:1.0
```
## 워크로드 리소스 개요
Kubernetes는 파드를 관리하기 위한 다양한 워크로드 리소스를 제공합니다. 각 워크로드 리소스는 특정 사용 사례와 요구사항에 맞게 설계되었습니다.
### 워크로드 리소스 유형
Kubernetes의 주요 워크로드 리소스는 다음과 같습니다:
1. **레플리카셋(ReplicaSet)**: 지정된 수의 파드 복제본을 유지
2. **디플로이먼트(Deployment)**: 레플리카셋을 관리하여 선언적 업데이트 제공
3. **스테이트풀셋(StatefulSet)**: 상태 유지가 필요한 애플리케이션을 위한 리소스
4. **데몬셋(DaemonSet)**: 모든 노드에서 파드의 복사본을 실행
5. **잡(Job)**: 완료 후 종료되는 일회성 작업
6. **크론잡(CronJob)**: 일정에 따라 잡을 주기적으로 실행
### 워크로드 리소스 선택 기준
적절한 워크로드 리소스를 선택하기 위한 기준:
1. **상태 유지 여부**: 애플리케이션이 상태를 유지해야 하는지 여부
2. **실행 패턴**: 지속적으로 실행되는지, 일회성인지, 주기적인지 여부
3. **배포 요구사항**: 롤링 업데이트, 블루/그린 배포 등의 요구사항
4. **노드 커버리지**: 모든 노드에서 실행되어야 하는지 여부
5. **확장성 요구사항**: 수평적 확장이 필요한지 여부
## 레플리카셋
레플리카셋(ReplicaSet)은 지정된 수의 파드 복제본이 항상 실행되도록 보장합니다. 파드가 실패하거나 삭제되면 레플리카셋은 자동으로 대체 파드를 생성합니다.
### 레플리카셋의 주요 기능
1. **파드 복제본 유지**: 지정된 수의 파드 복제본을 유지
2. **파드 선택**: 레이블 셀렉터를 통해 관리할 파드 식별
3. **파드 생성**: 필요한 경우 새 파드 생성
4. **파드 삭제**: 초과 파드 삭제
### 레플리카셋 정의
```yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: frontend
labels:
app: guestbook
tier: frontend
spec:
replicas: 3
selector:
matchLabels:
tier: frontend
template:
metadata:
labels:
tier: frontend
spec:
containers:
- name: php-redis
image: gcr.io/google_samples/gb-frontend:v3
resources:
requests:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 80
```
### 레플리카셋 작동 방식
1. **레이블 셀렉터 매칭**: 레플리카셋은 레이블 셀렉터와 일치하는 파드를 식별
2. **현재 상태 확인**: 현재 실행 중인 파드 수 확인
3. **원하는 상태와 비교**: 현재 파드 수와 원하는 복제본 수 비교
4. **조정 작업**: 필요한 경우 파드 생성 또는 삭제
### 레플리카셋 vs 레플리케이션 컨트롤러
레플리카셋은 레플리케이션 컨트롤러의 후속 버전으로, 더 강력한 레이블 셀렉터를 제공합니다:
1. **레플리케이션 컨트롤러**: 등식 기반 셀렉터만 지원 (예: app=nginx)
2. **레플리카셋**: 집합 기반 셀렉터 지원 (예: app in (nginx, apache))
### 레플리카셋 사용 사례
레플리카셋은 일반적으로 직접 사용하기보다는 디플로이먼트를 통해 간접적으로 사용됩니다. 그러나 다음과 같은 경우에 직접 사용할 수 있습니다:
1. **단순한 복제**: 단순히 파드의 복제본을 유지하는 경우
2. **사용자 정의 업데이트**: 사용자 정의 업데이트 메커니즘이 필요한 경우
3. **레거시 지원**: 레거시 애플리케이션 지원
## 디플로이먼트
디플로이먼트(Deployment)는 레플리카셋을 관리하여 파드의 선언적 업데이트를 제공합니다. 디플로이먼트는 애플리케이션의 롤링 업데이트, 롤백, 스케일링 등을 관리합니다.
### 디플로이먼트의 주요 기능
1. **선언적 업데이트**: 원하는 상태를 선언하면 디플로이먼트가 현재 상태를 원하는 상태로 변경
2. **롤링 업데이트**: 다운타임 없이 애플리케이션 업데이트
3. **롤백**: 이전 버전으로 쉽게 롤백
4. **스케일링**: 애플리케이션 복제본 수 조정
5. **배포 이력**: 이전 배포 버전 기록 유지
### 디플로이먼트 정의
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.30.4
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 100Mi
limits:
cpu: 200m
memory: 200Mi
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
```
### 디플로이먼트 업데이트 전략
디플로이먼트는 두 가지 업데이트 전략을 제공합니다:
1. **RollingUpdate**: 점진적으로 파드를 업데이트하여 다운타임 없이 배포 (기본값)
- **maxSurge**: 원하는 파드 수 이상으로 생성할 수 있는 최대 파드 수
- **maxUnavailable**: 업데이트 중 사용할 수 없는 최대 파드 수
2. **Recreate**: 기존 파드를 모두 삭제한 후 새 파드 생성 (일시적인 다운타임 발생)
### 디플로이먼트 롤백
디플로이먼트는 이전 버전으로의 롤백을 지원합니다:
```bash
# 배포 이력 확인
kubectl -n workloads-demo rollout history deployment/nginx-deployment
# 특정 버전의 세부 정보 확인
kubectl -n workloads-demo rollout history deployment/nginx-deployment --revision=2
# 이전 버전으로 롤백
kubectl -n workloads-demo rollout undo deployment/nginx-deployment
# 특정 버전으로 롤백
kubectl -n workloads-demo rollout undo deployment/nginx-deployment --to-revision=2
```
### 디플로이먼트 스케일링
디플로이먼트는 쉽게 스케일링할 수 있습니다:
```bash
# 명령형 방식으로 스케일링
kubectl -n workloads-demo scale deployment/nginx-deployment --replicas=5
# 선언적 방식으로 스케일링 (YAML 파일 수정 후)
kubectl apply -f deployment.yaml
```
### 디플로이먼트 일시 중지 및 재개
디플로이먼트 롤아웃을 일시 중지하고 재개할 수 있습니다:
```bash
# 롤아웃 일시 중지
kubectl -n workloads-demo rollout pause deployment/nginx-deployment
# 여러 변경 사항 적용
kubectl -n workloads-demo set image deployment/nginx-deployment nginx=nginx:1.30.4-alpine
kubectl -n workloads-demo set resources deployment/nginx-deployment -c=nginx --limits=cpu=200m,memory=256Mi
# 롤아웃 재개
kubectl -n workloads-demo rollout resume deployment/nginx-deployment
```
### 디플로이먼트 상태
Deployment 조건에는 `Progressing`, `Available`, `ReplicaFailure`가 있습니다. 정체된 롤아웃은 `Progressing=False`, 사유 `ProgressDeadlineExceeded`를 보고할 수 있으며 Kubernetes가 자동 롤백하지는 않습니다. “Complete”는 조건 유형이 아니라 롤아웃 완료 상태를 설명하는 표현입니다.
## 스테이트풀셋
스테이트풀셋(StatefulSet)은 상태 유지가 필요한 애플리케이션을 위한 워크로드 리소스입니다. 각 파드에 고유한 식별자를 부여하고, 안정적인 네트워크 식별자와 영구 스토리지를 제공합니다.
### 스테이트풀셋의 주요 기능
1. **안정적이고 고유한 네트워크 식별자**: 파드의 이름과 호스트 이름이 재시작 후에도 유지됨
2. **안정적이고 영구적인 스토리지**: 파드가 재스케줄링되더라도 동일한 스토리지에 접근 가능
3. **순차적인 배포 및 스케일링**: 파드가 순서대로 생성, 업데이트, 삭제됨
4. **순차적인 자동 롤링 업데이트**: 순서대로 파드 업데이트
### 스테이트풀셋 정의
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
selector:
matchLabels:
app: nginx
serviceName: "nginx"
replicas: 3
updateStrategy:
type: RollingUpdate
podManagementPolicy: OrderedReady
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.30.4
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "standard"
resources:
requests:
storage: 1Gi
```
### 스테이트풀셋 파드 식별자
스테이트풀셋은 파드에 다음과 같은 형식의 고유한 식별자를 부여합니다:
```
<스테이트풀셋 이름>-<순서 인덱스>
```
예를 들어, `web` 스테이트풀셋은 `web-0`, `web-1`, `web-2`와 같은 파드를 생성합니다.
### 스테이트풀셋 헤드리스 서비스
스테이트풀셋은 일반적으로 헤드리스 서비스(clusterIP: None)와 함께 사용됩니다. 이를 통해 각 파드에 대한 DNS 레코드가 생성됩니다:
```yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
```
이렇게 하면 각 파드는 다음과 같은 DNS 이름을 가집니다:
```
<파드 이름>.<서비스 이름>.<네임스페이스>.svc.cluster.local
```
예: `web-0.nginx.default.svc.cluster.local`
### 스테이트풀셋 스토리지
스테이트풀셋은 `volumeClaimTemplates`를 사용하여 각 파드에 대한 영구 볼륨 클레임(PVC)을 자동으로 생성합니다. 이러한 PVC는 파드가 재스케줄링되더라도 유지됩니다.
### 스테이트풀셋 업데이트 전략
스테이트풀셋은 두 가지 업데이트 전략을 제공합니다:
1. **RollingUpdate**: 파드를 순서대로 업데이트 (기본값)
2. **OnDelete**: 파드가 삭제될 때만 업데이트
### 파드 관리 정책
스테이트풀셋은 두 가지 파드 관리 정책을 제공합니다:
1. **OrderedReady**: 순서대로 파드 생성 및 종료 (기본값)
2. **Parallel**: 병렬로 파드 생성 및 종료
### 스테이트풀셋 사용 사례
스테이트풀셋은 다음과 같은 애플리케이션에 적합합니다:
1. **데이터베이스**: MySQL, PostgreSQL, MongoDB 등
2. **분산 시스템**: Kafka, ZooKeeper, Elasticsearch 등
3. **메시지 큐**: RabbitMQ 등
4. **기타 상태 유지 애플리케이션**: 파일 서버, 세션 저장소 등
### 스테이트풀셋 예시: 영구 스토리지를 사용하는 MySQL 인스턴스
이 예시는 단일 MySQL 인스턴스의 안정적인 식별자와 PVC를 보여줍니다. 복제나 데이터베이스 자동 장애 조치는 구성하지 않으며 `replicas`만 늘리면 독립된 데이터베이스가 생성됩니다. 같은 네임스페이스에 `password` 키를 가진 `mysql-secret`과 기본 StorageClass를 준비하거나 적절한 클래스를 명시하세요. 검증된 MySQL 8.4 패치·다이제스트를 사용하고 자격 증명 변경은 Secret 수정뿐 아니라 DB 내부에서도 수행해야 합니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None
selector:
app: mysql
ports:
- name: mysql
port: 3306
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 1
selector:
matchLabels:
app: mysql
podManagementPolicy: OrderedReady
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.4
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
ports:
- name: mysql
containerPort: 3306
startupProbe:
tcpSocket:
port: mysql
periodSeconds: 10
failureThreshold: 60
readinessProbe:
exec:
command:
- sh
- -c
- 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -h 127.0.0.1 -u root -e "SELECT 1"'
periodSeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
memory: 2Gi
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi
```
복제와 리더 승격은 DB 오퍼레이터 또는 별도로 검증한 복제 운영 절차가 필요합니다. StatefulSet 자체는 이를 제공하지 않습니다. [업스트림 복제 튜토리얼](https://kubernetes.io/docs/tasks/run-application/run-replicated-stateful-application/)도 교육용 비보안 기본값을 사용하며 운영용 구성이 아니라고 명시합니다.
## 데몬셋
데몬셋(DaemonSet)은 모든 노드(또는 특정 노드)에서 파드의 복사본을 실행하도록 보장합니다. 노드가 클러스터에 추가되면 파드가 자동으로 추가되고, 노드가 제거되면 파드도 제거됩니다.
### 데몬셋의 주요 기능
1. **모든 노드에서 실행**: 클러스터의 모든 노드에서 파드 실행
2. **노드 선택**: 노드 셀렉터를 통해 특정 노드에서만 실행 가능
3. **자동 배포**: 새 노드가 추가되면 자동으로 파드 배포
4. **자동 정리**: 노드가 제거되면 자동으로 파드 정리
### 데몬셋 정의
```yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd-elasticsearch
namespace: kube-system
labels:
k8s-app: fluentd-logging
spec:
selector:
matchLabels:
name: fluentd-elasticsearch
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
template:
metadata:
labels:
name: fluentd-elasticsearch
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
containers:
- name: fluentd-elasticsearch
image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2
resources:
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
```
CRI 런타임에서는 Docker JSON 로그가 아니라 `/var/log/pods`의 CRI 로그(보통 `/var/log/containers`에서 링크)를 파싱하도록 수집기를 설정하세요. 수집기 입력·출력 구성과 RBAC도 별도로 필요합니다.
### 데몬셋 업데이트 전략
데몬셋은 두 가지 업데이트 전략을 제공합니다:
1. **RollingUpdate**: 파드를 순차적으로 업데이트 (기본값)
- **maxUnavailable**: 업데이트 중 사용할 수 없는 최대 파드 수
2. **OnDelete**: 파드가 삭제될 때만 업데이트
### 데몬셋 노드 선택
데몬셋은 특정 노드에서만 실행되도록 구성할 수 있습니다:
```yaml
spec:
template:
spec:
nodeSelector:
disk: ssd
```
### 데몬셋 테인트 톨러레이션
데몬셋은 테인트가 있는 노드에서도 실행되도록 톨러레이션을 설정할 수 있습니다:
```yaml
spec:
template:
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
```
### 데몬셋 사용 사례
데몬셋은 다음과 같은 용도로 사용됩니다:
1. **로그 수집기**: Fluentd, Logstash 등
2. **모니터링 에이전트**: Prometheus Node Exporter, Datadog Agent 등
3. **네트워크 플러그인**: Calico, Cilium 등
4. **스토리지 데몬**: Ceph, GlusterFS 등
5. **보안 에이전트**: Falco, Sysdig 등
### 데몬셋 예시: Prometheus Node Exporter
```yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
labels:
app: node-exporter
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
hostNetwork: true
hostPID: true
containers:
- name: node-exporter
image: prom/node-exporter:v1.3.1
args:
- --path.procfs=/host/proc
- --path.sysfs=/host/sys
- --path.rootfs=/host/root
- --web.listen-address=:9100
ports:
- containerPort: 9100
protocol: TCP
name: http
resources:
limits:
cpu: 250m
memory: 180Mi
requests:
cpu: 102m
memory: 180Mi
volumeMounts:
- name: proc
mountPath: /host/proc
readOnly: true
- name: sys
mountPath: /host/sys
readOnly: true
- name: root
mountPath: /host/root
readOnly: true
tolerations:
- operator: "Exists"
volumes:
- name: proc
hostPath:
path: /proc
- name: sys
hostPath:
path: /sys
- name: root
hostPath:
path: /
```
## 잡과 크론잡
잡(Job)과 크론잡(CronJob)은 일회성 또는 주기적인 작업을 실행하기 위한 워크로드 리소스입니다.
### 잡(Job)
잡은 하나 이상의 파드를 생성하고 지정된 수의 파드가 성공적으로 종료될 때까지 실행을 계속합니다.
#### 잡의 주요 기능
1. **완료 추적**: 성공한 파드를 추적하며 재시도 제한·기한 초과 시 Job 실패 가능
2. **병렬 실행**: 여러 파드를 병렬로 실행 가능
3. **재시도**: 실패한 파드 자동 재시도
4. **완료 후 정리**: 작업 완료 후 파드 정리 (선택적)
#### 잡 정의
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
completions: 5 # 성공적으로 완료해야 하는 파드 수
parallelism: 2 # 병렬로 실행할 파드 수
backoffLimit: 4 # 실패 시 재시도 횟수
activeDeadlineSeconds: 100 # 작업 시간 제한 (초)
ttlSecondsAfterFinished: 100 # 완료 후 삭제 시간 (초)
template:
spec:
containers:
- name: pi
image: perl:5.34
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
resources:
requests:
cpu: 100m
memory: 50Mi
limits:
cpu: 100m
memory: 100Mi
restartPolicy: Never # 또는 OnFailure
```
#### 잡 완료 모드
잡은 두 가지 완료 모드를 제공합니다:
1. **NonIndexed**: 일반적인 잡 모드로, 지정된 수의 파드가 성공적으로 완료되면 작업 완료
2. **Indexed**: 각 파드에 0부터 시작하는 인덱스가 할당되어, 특정 인덱스 범위의 작업 수행
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: indexed-job
spec:
completions: 5
parallelism: 3
completionMode: Indexed # Indexed 모드 활성화
template:
spec:
containers:
- name: worker
image: busybox:1.34
command: ["sh", "-c", "echo Processing item ${JOB_COMPLETION_INDEX}"]
restartPolicy: Never
```
#### 잡 사용 사례
잡은 다음과 같은 용도로 사용됩니다:
1. **배치 처리**: 데이터 처리, ETL 작업
2. **계산 작업**: 과학적 계산, 렌더링
3. **데이터베이스 마이그레이션**: 스키마 업데이트
4. **일회성 관리 작업**: 백업, 정리 작업
### 크론잡(CronJob)
크론잡은 지정된 일정에 따라 잡을 주기적으로 실행합니다. 리눅스 크론 작업과 유사한 방식으로 작동합니다.
#### 크론잡의 주요 기능
1. **일정에 따른 실행**: cron 표현식을 사용하여 실행 일정 지정
2. **잡 관리**: 일정에 따라 잡 생성
3. **동시성 정책**: 이전 작업이 아직 실행 중일 때의 동작 정의
4. **이력 제한**: 완료된 작업의 이력 제한
#### 크론잡 정의
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: hello
spec:
schedule: "*/1 * * * *" # 매분 실행
timeZone: "America/New_York" # 타임존 (Kubernetes 1.27부터 Stable)
concurrencyPolicy: Forbid # Allow, Forbid, Replace
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
startingDeadlineSeconds: 60
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox:1.34
command:
- /bin/sh
- -c
- date; echo Hello from the Kubernetes cluster
restartPolicy: OnFailure
```
#### 크론 표현식
크론 표현식은 다음과 같은 형식을 가집니다:
```
┌───────────── 분 (0 - 59)
│ ┌───────────── 시 (0 - 23)
│ │ ┌───────────── 일 (1 - 31)
│ │ │ ┌───────────── 월 (1 - 12)
│ │ │ │ ┌───────────── 요일 (0 - 6) (일요일부터 토요일까지; 일요일은 0 사용)
│ │ │ │ │
│ │ │ │ │
* * * * *
```
일반적인 크론 표현식 예시:
- `*/5 * * * *`: 5분마다
- `0 * * * *`: 매시간 정각
- `0 0 * * *`: 매일 자정
- `0 0 * * 0`: 매주 일요일 자정
- `0 0 1 * *`: 매월 1일 자정
- `0 0 1 1 *`: 매년 1월 1일 자정
#### 동시성 정책
크론잡은 세 가지 동시성 정책을 제공합니다:
1. **Allow**: 여러 잡이 동시에 실행될 수 있음 (기본값)
2. **Forbid**: 이전 잡이 아직 실행 중이면 새 잡을 건너뜀
3. **Replace**: 이전 잡이 아직 실행 중이면 새 잡으로 대체
#### 크론잡 사용 사례
크론잡은 다음과 같은 용도로 사용됩니다:
1. **정기 백업**: 데이터베이스 백업, 스냅샷 생성
2. **데이터 동기화**: 주기적인 데이터 동기화
3. **보고서 생성**: 일일/주간/월간 보고서 생성
4. **정리 작업**: 임시 파일 정리, 로그 로테이션
5. **알림 및 모니터링**: 상태 확인, 알림 전송
#### 크론잡 예시: 데이터베이스 백업
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: database-backup
spec:
schedule: "0 2 * * *"
timeZone: "Etc/UTC"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:14
env:
- name: PGHOST
value: postgres-service
- name: PGUSER
valueFrom:
secretKeyRef:
name: postgres-secret
key: username
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
command:
- /bin/sh
- -c
- |
set -eu
backup_file="/backup/db-$(date +%Y%m%d-%H%M%S).dump"
trap 'rm -f "$backup_file.partial"' EXIT
pg_dump -Fc > "$backup_file.partial"
pg_restore --list "$backup_file.partial" > /dev/null
mv "$backup_file.partial" "$backup_file"
find /backup -maxdepth 1 -type f -name 'db-*.dump' -mtime +7 -delete
volumeMounts:
- name: backup-volume
mountPath: /backup
restartPolicy: OnFailure
volumes:
- name: backup-volume
persistentVolumeClaim:
claimName: backup-pvc
```
CronJob 일정 실행은 정확히 한 번을 보장하지 않으므로 중복 실행을 견디도록 작성하세요. `Forbid`는 같은 CronJob이 만든 Job에만 적용됩니다. 이력 제한은 Job/Pod 객체를 삭제하며 백업 파일을 지우지 않습니다. 백업 예시는 UTC 02:00에 실행되며 `backup-pvc`, `postgres-secret`, 접근 가능한 DB가 필요합니다. `pg_dump` 메이저 버전은 서버 이상이어야 하며 DB 이름이 `PGUSER`와 다르면 `PGDATABASE`를 지정하세요. 실제 복원 검증은 별도로 수행해야 합니다.
## 결론
이 문서에서는 Kubernetes의 기본 빌딩 블록인 파드와 다양한 워크로드 리소스에 대해 살펴보았습니다. 파드의 개념부터 시작하여 디플로이먼트, 스테이트풀셋, 데몬셋, 잡, 크론잡 등 다양한 워크로드 리소스의 특성과 사용 사례를 알아보았습니다. 이러한 리소스들은 각각 고유한 목적과 기능을 가지고 있으며, 적절한 상황에서 활용하면 효율적인 애플리케이션 배포와 관리가 가능합니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [파드와 워크로드 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/core/02-pods-and-workloads-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/core/03-services-networking
----------------------------------------
# 서비스와 네트워킹
> **지원 버전**: Kubernetes 1.35, 1.36, 1.37
> **마지막 업데이트**: 2026년 2월 23일
Kubernetes에서 서비스는 포드 집합에 대한 단일 접점을 제공하는 추상화 계층입니다. 이 장에서는 다양한 서비스 유형, 인그레스, 네트워크 정책 등 Kubernetes의 네트워킹 개념에 대해 자세히 알아보겠습니다.
## 실습 환경 설정
이 문서의 예제를 따라하기 위해서는 다음과 같은 도구와 환경이 필요합니다:
### 필수 도구
- API 서버와 마이너 버전 차이가 1 이내인 kubectl
- 작동하는 Kubernetes 클러스터 (EKS, minikube, kind 등)
### 예제 애플리케이션 배포
```bash
# 네임스페이스 생성
kubectl create namespace networking-demo
# 간단한 애플리케이션 배포
kubectl -n networking-demo apply -f - < **핵심 개념**: Kubernetes 서비스는 포드 집합에 대한 안정적인 네트워크 엔드포인트를 제공하며, 다양한 유형을 통해 내부 및 외부 접근을 제어합니다.
Kubernetes는 다양한 유형의 서비스를 제공하여 애플리케이션을 노출하는 여러 방법을 지원합니다.
### 서비스 아키텍처

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-03-services-networking-0.html)
### 서비스 유형 비교
| 서비스 유형 | 접근 범위 | 외부 IP | 사용 사례 | 특징 |
|------------|----------|---------|----------|------|
| **ClusterIP** | 클러스터 내부 | 아니오 | 내부 마이크로서비스 통신 | 기본 서비스 유형, 클러스터 내부에서만 접근 가능 |
| **NodePort** | 클러스터 외부 | 아니오 | 개발 및 테스트 환경 | 모든 노드의 특정 포트(30000-32767)를 통해 접근 |
| **LoadBalancer** | 클러스터 외부 | 예 | 프로덕션 환경의 외부 서비스 | 클라우드 제공업체의 로드 밸런서 프로비저닝 |
| **ExternalName** | 클러스터 내부 | 아니오 | 외부 서비스에 대한 내부 별칭 | DNS CNAME 레코드를 통한 리디렉션 |
| **Headless** | 클러스터 내부 | 아니오 | 직접 포드 IP 접근이 필요한 경우 | ClusterIP가 없는 특수 서비스 |
### ClusterIP
ClusterIP는 가장 기본적인 서비스 유형으로, 클러스터 내부에서만 접근 가능한 고정 IP 주소를 제공합니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
type: ClusterIP # 기본값이므로 생략 가능
```
### NodePort
NodePort 서비스는 모든 노드의 특정 포트를 통해 서비스에 접근할 수 있게 합니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80 # 클러스터 내부에서 사용하는 포트
targetPort: 9376 # 포드의 포트
nodePort: 30007 # 노드에 노출되는 포트 (30000-32767)
type: NodePort
```
### LoadBalancer
LoadBalancer 서비스는 클라우드 제공업체의 로드 밸런서를 프로비저닝하여 서비스를 외부에 노출합니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: external
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
spec:
selector:
app: MyApp
ports:
- port: 80
targetPort: 9376
type: LoadBalancer
```
이 예시는 AWS Load Balancer Controller, IAM 권한, 적절한 서브넷, 라우팅 가능한 Pod IP가 필요하며 인터넷 공개 NLB를 생성합니다. 내부 로드 밸런서도 가능하고 AWS는 IP 대신 DNS 호스트 이름을 반환할 수 있습니다. EKS Auto Mode는 별도 컨트롤러·클래스와 설정을 사용합니다.
### ExternalName
ExternalName 서비스는 외부 서비스에 대한 별칭을 제공합니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: ExternalName
externalName: my.database.example.com
```
이 서비스는 DNS 이름 `my-service`를 `my.database.example.com`으로 매핑합니다.
### 헤드리스 서비스
헤드리스 서비스는 클러스터 IP가 없는 서비스로, 각 포드에 대한 DNS 레코드를 생성합니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
clusterIP: None # 헤드리스 서비스
selector:
app: MyApp
ports:
- port: 80
targetPort: 9376
```
이 서비스는 클러스터 IP를 할당하지 않고, 각 포드에 대한 DNS 레코드를 생성합니다.
### 외부 IP
`externalIPs`는 관리자가 이미 노드로 라우팅한 IP에서 이 Service를 노출하며 주소를 할당하거나 외부 백엔드를 선택하지 않습니다. v1.36부터 사용 중단되었으므로 새 구성은 지원되는 로드 밸런서나 Gateway 구현을 사용하세요. 아래 문서용 주소는 직접 관리하는 주소로 바꿔야 합니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- port: 80
targetPort: 9376
externalIPs:
- 198.51.100.32
```
## 인그레스(Ingress)
인그레스는 클러스터 외부에서 클러스터 내부 서비스로의 HTTP 및 HTTPS 경로를 노출하는 API 객체입니다. 인그레스는 로드 밸런싱, SSL 종료, 이름 기반 가상 호스팅을 제공합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-03-services-networking-1.html)
### 인그레스 컨트롤러
인그레스 리소스를 사용하려면 클러스터에 인그레스 컨트롤러가 실행되고 있어야 합니다. 다양한 인그레스 컨트롤러가 있습니다:
- AWS Load Balancer Controller
- GCE 인그레스 컨트롤러
- Traefik
- HAProxy
- Istio 인그레스
커뮤니티 ingress-nginx는 2026년 3월에 유지 관리가 종료되었습니다([공지](https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/)). 아래 일반 예시는 Traefik 컨트롤러와 `traefik` IngressClass가 설치되어 있다고 가정합니다. Ingress는 패킷이 통과하는 장치가 아닌 구성 API이며 컨트롤러가 실제 프록시·로드 밸런서를 설정합니다.
### 기본 인그레스
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
spec:
ingressClassName: traefik # 사용할 인그레스 컨트롤러 클래스
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: example-service
port:
number: 80
```
이 인그레스는 `example.com` 호스트의 모든 요청을 `example-service:80`으로 라우팅합니다.
### 경로 기반 라우팅
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-based-ingress
spec:
ingressClassName: traefik
rules:
- host: example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- path: /web
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
```
이 인그레스는 `example.com/api`로 시작하는 요청을 `api-service`로, `example.com/web`으로 시작하는 요청을 `web-service`로 라우팅합니다.
### 이름 기반 가상 호스팅
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: name-based-ingress
spec:
ingressClassName: traefik
rules:
- host: foo.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: foo-service
port:
number: 80
- host: bar.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: bar-service
port:
number: 80
```
이 인그레스는 `foo.example.com`으로 들어오는 요청을 `foo-service`로, `bar.example.com`으로 들어오는 요청을 `bar-service`로 라우팅합니다.
### TLS 설정
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-ingress
spec:
ingressClassName: traefik
tls:
- hosts:
- example.com
secretName: example-tls
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: example-service
port:
number: 80
```
이 인그레스는 `example-tls` 시크릿에 저장된 TLS 인증서를 사용하여 `example.com`에 대한 HTTPS 연결을 종료합니다.
TLS 시크릿 생성:
```bash
kubectl create secret tls example-tls --cert=path/to/cert.crt --key=path/to/key.key
```
### AWS Load Balancer Controller
AWS EKS에서는 AWS Load Balancer Controller를 사용하여 Application Load Balancer를 프로비저닝할 수 있습니다.
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: alb-ingress
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:region:account-id:certificate/certificate-id
spec:
ingressClassName: alb
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: example-service
port:
number: 80
```
이 인그레스는 AWS ALB를 사용하여 `example.com`에 대한 요청을 처리합니다.
## 엔드포인트(Endpoints)
레거시 `v1/Endpoints` API는 v1.33부터 사용 중단되었습니다. 새 통합에는 `discovery.k8s.io/v1` EndpointSlice를 사용하세요. 이 객체는 백엔드 정보를 표현하며 트래픽이 API 객체를 통과하지는 않습니다.
엔드포인트는 서비스가 가리키는 포드의 IP 주소와 포트를 저장하는 리소스입니다. 서비스의 셀렉터와 일치하는 포드가 있으면 Kubernetes는 자동으로 엔드포인트 객체를 생성하고 관리합니다.
```yaml
apiVersion: v1
kind: Endpoints
metadata:
name: my-service
subsets:
- addresses:
- ip: 192.168.1.1
ports:
- port: 9376
```
수동 백엔드는 **selector가 없는** `my-service` Service와 일치하는 포트가 필요합니다. selector가 있으면 컨트롤러가 백엔드 정보를 덮어쓸 수 있으므로 아래처럼 수동 EndpointSlice를 사용하는 편이 좋습니다.
### 엔드포인트슬라이스(EndpointSlice)
엔드포인트슬라이스는 엔드포인트의 확장 가능한 대안으로, 대규모 클러스터에서 더 나은 성능을 제공합니다.
```yaml
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: my-service-abc
labels:
kubernetes.io/service-name: my-service
endpointslice.kubernetes.io/managed-by: docs.example.com
addressType: IPv4
ports:
- name: ""
protocol: TCP
port: 9376
endpoints:
- addresses:
- "10.1.2.3"
conditions:
ready: true
hostname: pod-1
nodeName: node-1
zone: us-west-2a
```
## 서비스 디스커버리
Kubernetes는 두 가지 주요 서비스 디스커버리 방법을 제공합니다:
1. **환경 변수**: Kubernetes는 포드가 생성될 때 활성 서비스에 대한 환경 변수를 포드에 주입합니다.
2. **DNS**: Kubernetes는 클러스터 DNS 서버를 통해 서비스에 대한 DNS 레코드를 제공합니다.
### 환경 변수
`enableServiceLinks: true`이면 kubelet은 컨테이너 시작 시 같은 네임스페이스에 이미 있는 ClusterIP Service의 변수를 추가합니다. 변수는 자동 갱신되지 않으므로 나중에 생성되는 Service에는 DNS를 사용하세요. 예를 들어, `my-service`라는 서비스가 있으면 다음과 같은 환경 변수가 생성됩니다:
```
MY_SERVICE_SERVICE_HOST=10.0.0.11
MY_SERVICE_SERVICE_PORT=80
```
### DNS
Kubernetes DNS는 서비스에 대한 DNS 레코드를 생성합니다. 포드는 서비스 이름을 사용하여 서비스에 접근할 수 있습니다.
- 일반 서비스: `my-service.my-namespace.svc.cluster.local`
- 헤드리스 서비스의 포드: `pod-name.my-service.my-namespace.svc.cluster.local`
## CoreDNS
CoreDNS는 Kubernetes 클러스터의 DNS 서버로 사용되는 유연하고 확장 가능한 DNS 서버입니다.
### CoreDNS 구성
CoreDNS는 ConfigMap을 통해 구성됩니다:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
```
이 구성은 다음과 같은 기능을 제공합니다:
- `errors`: 오류 로깅
- `health`: 상태 확인 엔드포인트
- `ready`: 준비 상태 확인 엔드포인트
- `kubernetes`: Kubernetes 서비스 및 포드에 대한 DNS 레코드 제공
- `prometheus`: Prometheus 메트릭 노출
- `forward`: 외부 DNS 쿼리 전달
- `cache`: DNS 응답 캐싱
- `loop`: 루프 감지
- `reload`: 구성 파일 변경 시 자동 리로드
- `loadbalance`: 로드 밸런싱
### DNS 정책
포드의 DNS 정책은 `dnsPolicy` 필드를 통해 구성할 수 있습니다:
- `ClusterFirst`: 기본값으로, Kubernetes DNS 서버를 먼저 사용하고, 일치하는 항목이 없으면 업스트림 네임서버로 전달합니다.
- `Default`: 포드가 실행 중인 노드의 DNS 설정을 상속받습니다.
- `ClusterFirstWithHostNet`: `hostNetwork: true`로 설정된 포드에 권장되는 정책입니다.
- `None`: 모든 DNS 설정을 `dnsConfig` 필드를 통해 제공해야 합니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: custom-dns
spec:
containers:
- name: nginx
image: nginx
dnsPolicy: "None"
dnsConfig:
nameservers:
- 1.1.1.1
- 8.8.8.8
searches:
- ns1.svc.cluster.local
- my.dns.search.suffix
options:
- name: ndots
value: "2"
- name: edns0
```
## 네트워크 정책
네트워크 정책은 포드 간의 통신을 제어하는 방법을 제공합니다. 네트워크 정책을 사용하려면 네트워크 플러그인이 네트워크 정책을 지원해야 합니다(예: Calico, Cilium).

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-03-services-networking-2.html)
### 기본 네트워크 정책
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {} # 모든 포드에 적용
policyTypes:
- Ingress
```
이 정책은 해당 네임스페이스 파드의 수신을 격리합니다. 표준 NetworkPolicy는 허용 규칙의 합집합이므로 다른 정책이 허용한 트래픽은 계속 허용됩니다. 플러그인의 정책 집행이 필요하며 연결에는 출발지 egress와 목적지 ingress 양쪽의 허용이 필요합니다. 노드 트래픽에는 문서화된 예외가 있습니다.
### 특정 포드에 대한 인그레스 허용
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-nginx-ingress
spec:
podSelector:
matchLabels:
app: nginx
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
access: allowed
ports:
- protocol: TCP
port: 80
```
이 네트워크 정책은 `access: allowed` 레이블이 있는 포드에서 `app: nginx` 레이블이 있는 포드로의 TCP 포트 80에 대한 인그레스 트래픽을 허용합니다.
### 네임스페이스 기반 정책
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-prod-namespace
spec:
podSelector:
matchLabels:
app: db
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
purpose: production
```
이 네트워크 정책은 `purpose: production` 레이블이 있는 네임스페이스의 모든 포드에서 `app: db` 레이블이 있는 포드로의 인그레스 트래픽을 허용합니다.
### 이그레스 정책
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: limit-egress
spec:
podSelector:
matchLabels:
app: frontend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 8080
- to:
- namespaceSelector:
matchLabels:
purpose: monitoring
```
이 네트워크 정책은 `app: frontend` 레이블이 있는 포드에서 `app: api` 레이블이 있는 포드의 TCP 포트 8080으로의 이그레스 트래픽과 `purpose: monitoring` 레이블이 있는 네임스페이스의 모든 포드로의 이그레스 트래픽을 허용합니다.
위 egress 예시는 별도 허용 정책이 없으면 DNS도 차단합니다. Service 이름을 사용하는 앱에는 클러스터 DNS 엔드포인트의 TCP/UDP 53을 허용하고 NodeLocal DNS 사용 여부도 반영하세요.
### CIDR 기반 정책
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-external-traffic
spec:
podSelector:
matchLabels:
app: web
policyTypes:
- Ingress
ingress:
- from:
- ipBlock:
cidr: 192.168.1.0/24
except:
- 192.168.1.1/32
```
이 네트워크 정책은 `192.168.1.0/24` CIDR 블록(192.168.1.1 제외)에서 `app: web` 레이블이 있는 포드로의 인그레스 트래픽을 허용합니다.
## 서비스 메시
서비스 메시는 마이크로서비스 간의 통신을 관리하는 인프라 계층입니다. 서비스 메시는 서비스 디스커버리, 로드 밸런싱, 암호화, 인증, 권한 부여, 관찰 가능성 등의 기능을 제공합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-03-services-networking-3.html)
### Istio
Istio는 인기 있는 서비스 메시 구현 중 하나입니다. 다이어그램은 등록된 파드에 Envoy를 주입하는 Istio 사이드카 모드입니다. Istio는 다른 데이터 플레인의 ambient 모드도 제공합니다.
#### Istio 가상 서비스
```yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- match:
- headers:
end-user:
exact: jason
route:
- destination:
host: reviews
subset: v2
- route:
- destination:
host: reviews
subset: v1
```
이 가상 서비스는 `end-user: jason` 헤더가 있는 요청을 `reviews` 서비스의 `v2` 서브셋으로 라우팅하고, 다른 모든 요청을 `v1` 서브셋으로 라우팅합니다.
#### Istio 대상 규칙
```yaml
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: reviews
spec:
host: reviews
trafficPolicy:
loadBalancer:
simple: RANDOM
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
```
이 대상 규칙은 `reviews` 서비스에 대한 두 개의 서브셋(`v1`과 `v2`)을 정의하고, 각 서브셋에 대한 로드 밸런싱 정책을 설정합니다.
### Linkerd
Linkerd는 경량화된 서비스 메시로, 간단한 설치와 사용이 특징입니다.
#### Linkerd 서비스 프로필
다음은 레거시 예시입니다. Linkerd 2.16부터 Gateway API 유형이 ServiceProfile을 대체하며 ServiceProfile은 호환성 목적으로 유지됩니다([현재 문서](https://linkerd.io/2-edge/reference/service-profiles/)).
```yaml
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: nginx.default.svc.cluster.local
namespace: default
spec:
routes:
- name: GET /
condition:
method: GET
pathRegex: /
responseClasses:
- condition:
status:
min: 500
max: 599
isFailure: true
retryBudget:
retryRatio: 0.2
minRetriesPerSecond: 10
ttl: 10s
```
이 서비스 프로필은 `nginx` 서비스에 대한 경로와 재시도 정책을 정의합니다.
## CNI(Container Network Interface)
CNI 플러그인은 파드 네트워크 인터페이스와 IP 주소를 구성합니다. NetworkPolicy 집행은 플러그인이 지원해야 하는 별도 기능입니다.
## Cilium

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-03-services-networking-4.html)
[Cilium 세부](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/README.md)
### Cilium 소개
Cilium은 Linux 커널의 강력한 eBPF 기술을 활용하여 컨테이너화된 애플리케이션 간의 네트워크 연결, 보안, 관찰 가능성을 제공하는 오픈 소스 소프트웨어입니다. 현재 Kubernetes 통합은 CNI 플러그인과 컨트롤러로 네트워킹·보안·관찰 기능을 제공합니다.
#### 주요 특징
- **eBPF 기반**: 커널 내에서 프로그래밍 가능한 데이터 경로를 통해 고성능 네트워킹 및 보안 기능 제공
- **API 인식 네트워킹**: L3-L7 계층에서 API 인식 네트워크 보안 정책 지원
- **Kubernetes 통합**: Kubernetes CNI(Container Network Interface) 구현 제공
- **분산 로드 밸런싱**: 효율적인 서비스 간 통신을 위한 분산 로드 밸런싱
- **네트워크 가시성**: Hubble을 통한 네트워크 흐름 모니터링 및 문제 해결
- **멀티 클러스터 지원**: 클러스터 간 네트워킹 및 보안 정책 지원
#### Cilium의 차별화 포인트
Cilium은 다른 CNI 솔루션과 비교하여 여러 고유한 이점을 제공합니다.
**기술적 차별화**:
- **eBPF 활용**: 커널 내 프로그래밍 가능한 데이터 경로를 통해 고성능 및 유연성 제공
- **API 인식 네트워킹**: L7 계층까지 네트워크 정책 지원
- **XDP(eXpress Data Path)**: 패킷 처리 성능 최적화
- **Kube-proxy 대체**: 더 효율적인 서비스 로드 밸런싱
- **Hubble 통합**: 강력한 네트워크 관찰 가능성 도구
**사용 사례별 이점**:
- **마이크로서비스 아키텍처**: 세분화된 네트워크 정책 및 관찰 가능성
- **멀티 클러스터 배포**: 클러스터 간 원활한 네트워킹
- **보안 중심 환경**: 강력한 네트워크 보안 정책
- **고성능 요구 사항**: 최적화된 데이터 경로
- **서비스 메시 통합**: Istio와 같은 서비스 메시와의 통합
### eBPF 기술
eBPF(extended Berkeley Packet Filter)는 Linux 커널 내에서 안전하게 프로그램을 실행할 수 있는 기술입니다. Cilium은 eBPF를 활용하여 네트워킹, 보안 및 관찰 가능성 기능을 구현합니다.
#### eBPF의 주요 특징
1. **커널 내 실행**: eBPF 프로그램은 커널 내에서 직접 실행되어 높은 성능을 제공합니다.
2. **안전성**: 검증기는 로드 전에 프로그램 안전성 제약을 검사하지만 커널 버그나 잘못된 정책 로직까지 없다고 보장하지는 않습니다.
3. **동적 로딩**: 커널을 재부팅하지 않고도 eBPF 프로그램을 로드하고 언로드할 수 있습니다.
4. **맵**: eBPF 맵은 데이터를 저장하고 사용자 공간과 커널 공간 간에 데이터를 공유하는 데 사용됩니다.
#### Cilium에서의 eBPF 활용
Cilium은 다음과 같은 방식으로 eBPF를 활용합니다:
1. **네트워크 데이터 경로**: eBPF 프로그램은 네트워크 패킷을 처리하고 라우팅합니다.
2. **정책 시행**: eBPF 프로그램은 네트워크 정책을 시행합니다.
3. **로드 밸런싱**: eBPF 프로그램은 서비스에 대한 로드 밸런싱을 수행합니다.
4. **관찰 가능성**: eBPF 프로그램은 네트워크 흐름에 대한 메트릭을 수집합니다.
#### eBPF vs 기존 네트워킹 접근 방식
| 특성 | eBPF | 기존 접근 방식 (iptables) |
|------|------|------------------------|
| 성능 | 매우 높음 | 중간 |
| 확장성 | 매우 높음 | 제한적 |
| 프로그래밍 가능성 | 높음 | 제한적 |
| 관찰 가능성 | 높음 | 제한적 |
| 구현 복잡성 | 높음 | 중간 |
### Cilium 네트워킹 모델
Cilium은 다양한 네트워킹 모델을 지원하여 다양한 환경과 요구 사항에 맞게 구성할 수 있습니다.
#### 오버레이 네트워킹
Cilium은 기본적으로 VXLAN을 사용하여 오버레이 네트워킹을 구현하지만, Geneve와 같은 다른 캡슐화 프로토콜도 지원합니다.
**작동 방식**:
1. 소스 노드에서 패킷이 생성됩니다.
2. Cilium은 패킷을 캡슐화하여 원래 패킷을 캡슐화 헤더로 감쌉니다.
3. 캡슐화된 패킷은 물리적 네트워크를 통해 대상 노드로 전송됩니다.
4. 대상 노드에서 Cilium은 패킷을 캡슐 해제하여 원래 패킷을 추출합니다.
5. 추출된 패킷은 대상 컨테이너로 전달됩니다.
**장점**:
- 기존 네트워크 인프라와의 호환성
- 네트워크 토폴로지 독립성
- 언더레이와 독립된 Pod 주소 사용; 연결된 클러스터에는 여전히 호환되고 중복 없는 주소 계획 필요
**단점**:
- 캡슐화 오버헤드로 인한 성능 영향
- MTU 크기 감소
- 추가적인 CPU 사용량
#### 네이티브 라우팅
네이티브 라우팅은 캡슐화 없이 직접 라우팅을 사용하는 방식입니다. 이 모드에서는 기본 네트워크 인프라가 포드 IP 주소를 라우팅할 수 있어야 합니다.
**작동 방식**:
1. 각 노드는 해당 노드에서 실행 중인 포드의 CIDR 블록을 알립니다.
2. 라우팅 테이블은 각 포드 CIDR 블록을 해당 노드로 라우팅하도록 구성됩니다.
3. 패킷은 캡슐화 없이 직접 대상 노드로 라우팅됩니다.
**장점**:
- 캡슐화 오버헤드 없음
- 향상된 네트워크 성능
- 낮은 CPU 사용량
**단점**:
- 기본 네트워크 인프라에 대한 의존성
- 네트워크 토폴로지 제약
- IP 주소 관리 복잡성
#### 라우팅 모드 선택
배포에 맞는 캡슐화 또는 네이티브 라우팅 모드를 명시적으로 선택하세요. 네이티브 경로 실패 시 자동으로 터널로 전환된다고 가정하지 말고 사용 버전의 라우팅·마이그레이션 절차를 따르세요.
#### AWS ENI 모드
AWS EKS에서 Cilium은 AWS의 Elastic Network Interface(ENI)를 활용하여 포드에 네이티브 VPC IP 주소를 할당할 수 있습니다.
**주요 특징**:
- 포드에 VPC 네이티브 IP 주소 할당
- 오버레이 네트워크 없이 VPC 네이티브 네트워킹
- AWS 보안 그룹 및 네트워크 정책 통합
- 향상된 네트워크 성능
### Cilium 네트워크 정책
Cilium은 Kubernetes 네트워크 정책을 확장하여 L3-L7 계층에서 세분화된 네트워크 보안 정책을 제공합니다.
#### L3/L4 정책
Cilium은 표준 Kubernetes 네트워크 정책을 지원하여 IP 주소, 포트 및 프로토콜 기반의 정책을 정의할 수 있습니다.
```yaml
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "l3-l4-policy"
spec:
endpointSelector:
matchLabels:
app: myapp
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "80"
protocol: TCP
```
이 정책은 `app: frontend` 레이블이 있는 포드에서 `app: myapp` 레이블이 있는 포드로의 TCP 포트 80에 대한 인그레스 트래픽을 허용합니다.
#### L7 정책
Cilium은 eBPF와 사용자 공간 Envoy 프록시를 함께 사용해 HTTP 규칙 등의 L7 정책을 지원합니다. L7 검사는 전부 커널 안에서 수행되지 않으며 설치 버전의 프로토콜·암호화 제약을 확인해야 합니다.
```yaml
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "l7-policy"
spec:
endpointSelector:
matchLabels:
app: myapp
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: "GET"
path: "/api/v1/products"
```
이 정책은 `app: frontend` 레이블이 있는 포드에서 `app: myapp` 레이블이 있는 포드로의 HTTP GET 요청을 `/api/v1/products` 경로에 대해서만 허용합니다.
#### 클러스터 전체 정책
Cilium은 클러스터 전체 네트워크 정책을 지원하여 모든 포드에 적용되는 정책을 정의할 수 있습니다.
```yaml
apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
name: "cluster-wide-policy"
spec:
endpointSelector:
matchLabels: {} # 모든 포드에 적용
ingress:
- fromEndpoints:
- matchLabels:
io.kubernetes.pod.namespace: kube-system
```
이 정책은 `kube-system` 네임스페이스의 포드에서 모든 포드로의 인그레스 트래픽을 허용합니다.
### Hubble을 통한 네트워크 가시성
Hubble은 Cilium의 관찰 가능성 계층으로, eBPF를 활용하여 네트워크 흐름을 모니터링하고 문제를 해결하는 도구입니다.
#### Hubble의 주요 기능
1. **네트워크 흐름 모니터링**: 포드 간 통신을 실시간으로 모니터링합니다.
2. **서비스 의존성 매핑**: 서비스 간 의존성을 시각화합니다.
3. **보안 관찰**: 네트워크 정책 위반을 감지합니다.
4. **성능 분석**: 네트워크 지연 시간 및 처리량을 분석합니다.
5. **문제 해결**: 네트워크 연결 문제를 진단합니다.
#### Hubble 아키텍처
Hubble은 다음과 같은 구성 요소로 이루어져 있습니다:
1. **Hubble Server**: Cilium 에이전트에 내장된 서버로, 네트워크 흐름 데이터를 수집합니다.
2. **Hubble Relay**: 여러 Hubble Server의 데이터를 집계합니다.
3. **Hubble UI**: 네트워크 흐름을 시각화하는 웹 인터페이스를 제공합니다.
4. **Hubble CLI**: 명령줄에서 네트워크 흐름을 쿼리하는 도구를 제공합니다.
#### Hubble 사용 예시
```bash
# Hubble CLI 설치
curl -L --remote-name-all https://github.com/cilium/hubble/releases/latest/download/hubble-linux-amd64.tar.gz
sudo tar xzvfC hubble-linux-amd64.tar.gz /usr/local/bin
rm hubble-linux-amd64.tar.gz
# Hubble 활성화
cilium hubble enable
# Run in a separate terminal and keep it open
cilium hubble port-forward
# 네트워크 흐름 관찰
hubble observe
# HTTP 요청 관찰
hubble observe --protocol http
# 특정 포드의 네트워크 흐름 관찰
hubble observe --pod default/myapp-pod
# 네트워크 정책 위반 관찰
hubble observe --verdict DROPPED
```
### Amazon EKS에서 Cilium 구성
Amazon EKS에서 Cilium을 구성하는 방법은 다양합니다. 여기서는 몇 가지 일반적인 구성 방법을 살펴보겠습니다.
#### 설치 전에 CNI 모드 선택
폐기 가능한 EKS 테스트 클러스터에서 Kubernetes 버전과 호환되는 Cilium 릴리스를 선택하세요. [공식 EKS 설치 문서](https://docs.cilium.io/en/stable/installation/k8s-install-helm/)를 따르며 아래 코드는 완전한 설치·마이그레이션 명령이 아닌 Helm **values 일부**입니다.
**AWS VPC CNI 체이닝**은 IPAM으로 `aws-node`를 유지합니다:
```yaml
cni:
chainingMode: aws-cni
exclusive: false
enableIPv4Masquerade: false
routingMode: native
```
기존 파드에는 새 CNI 체인이 적용되지 않으므로 통제된 롤아웃으로 재생성해야 합니다. L7 정책이나 kube-proxy 대체를 활성화하기 전에 체이닝 기능 제약을 확인하세요.
**Cilium ENI IPAM**은 VPC CNI 대신 Cilium이 ENI를 관리합니다:
```yaml
eni:
enabled: true
ipam:
mode: eni
routingMode: native
```
이 모드에는 EC2 API 권한과 같은 노드를 `aws-node`가 관리하지 않도록 하는 공식 설치·마이그레이션 절차가 필요합니다. 이 절차 없이 실행 중인 VPC CNI 위에 적용하지 마세요. EKS Auto Mode와 Fargate는 네트워킹을 별도로 관리합니다.
설치 후 `cilium status --wait`와 격리된 테스트 클러스터의 연결성 테스트로 검증하세요. 차트 버전을 고정하고 검토한 values를 업그레이드에도 유지합니다.
#### Hubble 활성화
```bash
# Hubble 활성화
cilium hubble enable --ui
# Hubble UI 접근
kubectl port-forward -n kube-system svc/hubble-ui 12000:80
```
#### Cilium 네트워크 정책 예시
```yaml
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "eks-app-policy"
spec:
endpointSelector:
matchLabels:
app: api
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "8080"
protocol: TCP
rules:
http:
- method: "GET"
path: "/api/v1/.*"
egress:
- toEndpoints:
- matchLabels:
app: database
toPorts:
- ports:
- port: "3306"
protocol: TCP
```
이 정책은 `app: frontend` 레이블이 있는 포드에서 `app: api` 레이블이 있는 포드로의 HTTP GET 요청을 `/api/v1/` 경로에 대해서만 허용하고, `app: api` 레이블이 있는 포드에서 `app: database` 레이블이 있는 포드로의 TCP 포트 3306에 대한 이그레스 트래픽을 허용합니다.
#### EKS에서 Cilium 최적화
1. **노드 그룹 구성**:
- 충분한 ENI 및 IP 주소를 제공하는 인스턴스 유형 선택
- 적절한 최대 포드 수 구성
2. **성능 최적화**:
- 직접 라우팅 모드 사용
- XDP 가속 활성화
- BBR 혼잡 제어 알고리즘 활성화
3. **모니터링 및 로깅**:
- Hubble 활성화
- Prometheus 메트릭 수집
- CloudWatch와 통합
## 결론
이 장에서는 Kubernetes의 서비스와 네트워킹에 대해 알아보았습니다. 서비스는 포드 집합에 대한 안정적인 엔드포인트를 제공하고, 인그레스는 외부 트래픽을 클러스터 내부 서비스로 라우팅합니다. 네트워크 정책은 포드 간의 통신을 제어하고, 서비스 메시는 마이크로서비스 아키텍처에서 서비스 간 통신을 관리합니다. 또한 CNI와 Cilium을 통해 고급 네트워킹 기능을 구현하는 방법에 대해 살펴보았습니다.
Kubernetes의 네트워킹 기능을 이해하고 활용하면 안전하고 확장 가능한 애플리케이션을 구축할 수 있습니다.
다음 장에서는 Kubernetes의 스토리지 옵션에 대해 알아보겠습니다.
## 참고 자료
- [Kubernetes 공식 문서 - 서비스](https://kubernetes.io/docs/concepts/services-networking/service/)
- [Kubernetes 공식 문서 - 인그레스](https://kubernetes.io/docs/concepts/services-networking/ingress/)
- [Kubernetes 공식 문서 - 네트워크 정책](https://kubernetes.io/docs/concepts/services-networking/network-policies/)
- [Kubernetes 공식 문서 - DNS for Services and Pods](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/)
- [Istio 공식 문서](https://istio.io/latest/docs/)
- [Linkerd 공식 문서](https://linkerd.io/2-edge/overview/)
- [Cilium 공식 문서](https://docs.cilium.io/)
- [CNI 공식 문서](https://github.com/containernetworking/cni)
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [서비스와 네트워킹 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/core/03-services-networking-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/core/04-storage
----------------------------------------
# 스토리지
> **지원 버전**: Kubernetes 1.35, 1.36, 1.37
> **마지막 업데이트**: 2026년 2월 19일
Kubernetes에서 스토리지는 컨테이너화된 애플리케이션의 데이터를 저장하고 관리하는 중요한 부분입니다. 이 장에서는 볼륨, 퍼시스턴트 볼륨, 퍼시스턴트 볼륨 클레임, 스토리지 클래스 등 Kubernetes의 스토리지 개념에 대해 자세히 알아보겠습니다.
## 실습 환경 설정
이 문서의 예제를 따라하기 위해서는 다음과 같은 도구와 환경이 필요합니다:
### 필수 도구
- API 서버와 마이너 버전 차이가 1 이내인 kubectl
- 작동하는 Kubernetes 클러스터 (EKS, minikube, kind 등)
- 스토리지 프로비저너 (EKS의 경우 EBS CSI 드라이버)
첫 예시는 기본 StorageClass가 필요하며 없으면 설치된 클래스의 이름을 `storageClassName`에 지정하세요. 아래 EKS EBS 예시는 IAM 권한을 가진 표준 EBS CSI 드라이버와 EC2 노드 기준이며 Auto Mode는 `ebs.csi.eks.amazonaws.com`을 사용합니다. EBS는 Fargate·Hybrid Nodes에서 마운트할 수 없습니다. 이후 매니페스트는 각각 선행 조건이 필요한 독립 예시입니다.
### 스토리지 예제 설정
```bash
# 네임스페이스 생성
kubectl create namespace storage-demo
# 간단한 PVC 및 Pod 생성
kubectl -n storage-demo apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: data-pod
spec:
containers:
- name: data-container
image: busybox
command: ["sh", "-c", "while true; do echo $(date) >> /data/output.txt; sleep 5; done"]
volumeMounts:
- name: data-volume
mountPath: /data
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: data-pvc
EOF
# 스토리지 리소스 확인
kubectl -n storage-demo get pvc,pod
```
## 목차
1. [볼륨(Volume)](#볼륨volume)
2. [퍼시스턴트 볼륨(PersistentVolume)](#퍼시스턴트-볼륨persistentvolume)
3. [퍼시스턴트 볼륨 클레임(PersistentVolumeClaim)](#퍼시스턴트-볼륨-클레임persistentvolumeclaim)
4. [스토리지 클래스(StorageClass)](#스토리지-클래스storageclass)
5. [동적 프로비저닝](#동적-프로비저닝)
6. [볼륨 스냅샷](#볼륨-스냅샷)
7. [볼륨 확장](#볼륨-확장)
8. [Projected Volumes](#projected-volumes)
9. [Generic Ephemeral Volumes](#generic-ephemeral-volumes)
10. [Block Volume Mode](#block-volume-mode)
11. [Volume Cloning](#volume-cloning)
12. [Storage ResourceQuota](#storage-resourcequota)
13. [EKS에서의 스토리지 옵션](#eks에서의-스토리지-옵션)
## 볼륨(Volume)
> **핵심 개념**: Kubernetes 볼륨은 포드 내의 컨테이너가 데이터를 저장하고 공유할 수 있는 디렉토리로, 컨테이너의 재시작과 관계없이 데이터를 유지할 수 있습니다.
Kubernetes 볼륨은 포드 내의 컨테이너가 데이터를 저장하고 공유할 수 있는 디렉토리입니다. 파드의 마운트 수명과 실제 데이터의 보존 기간은 다릅니다. emptyDir 데이터는 파드와 함께 제거되지만 영구 스토리지는 파드보다 오래 유지될 수 있습니다.
### Kubernetes 스토리지 아키텍처

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-04-storage-0.html)
### 볼륨의 필요성
1. **컨테이너 재시작 시 데이터 유지**: 컨테이너가 재시작되면 파일 시스템이 초기화되지만, 볼륨을 사용하면 데이터를 유지할 수 있습니다.
2. **컨테이너 간 데이터 공유**: 같은 포드 내의 여러 컨테이너가 볼륨을 통해 데이터를 공유할 수 있습니다.
### 주요 볼륨 유형 비교
| 볼륨 유형 | 수명 주기 | 데이터 지속성 | 사용 사례 | 특징 |
|----------|----------|-------------|----------|------|
| **emptyDir** | 포드 | 임시 | 임시 데이터, 캐시, 체크포인트 | 포드가 삭제되면 데이터도 삭제됨 |
| **hostPath** | 노드 | 노드 수준 | 노드 파일 시스템 접근, 모니터링 | 보안 위험이 있으므로 주의 필요 |
| **configMap** | 구성 | 구성 데이터 | 애플리케이션 구성 | 구성 데이터를 볼륨으로 마운트 |
| **secret** | 구성 | 민감 데이터 | 인증서, 비밀번호 | 민감 데이터를 볼륨으로 마운트 |
| **persistentVolumeClaim** | 클러스터 | 영구적 | 데이터베이스, 파일 저장소 | 포드 재시작 및 재스케줄링 후에도 데이터 유지 |
### emptyDir
`emptyDir` 볼륨은 포드가 노드에 할당될 때 생성되고, 포드가 해당 노드에서 실행되는 동안 유지됩니다. 포드가 노드에서 제거되면 `emptyDir`의 데이터는 영구적으로 삭제됩니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: test-pd
spec:
containers:
- image: nginx
name: test-container
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
```
### hostPath
`hostPath` 볼륨은 노드의 파일 시스템에서 파일이나 디렉토리를 포드에 마운트합니다. 이는 노드의 파일 시스템에 접근해야 하는 포드에 유용하지만, 보안 위험이 있으므로 주의해서 사용해야 합니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: test-hostpath
spec:
containers:
- image: nginx
name: test-container
volumeMounts:
- mountPath: /test-pd
name: test-volume
volumes:
- name: test-volume
hostPath:
path: /data
type: Directory # DirectoryOrCreate, Directory, FileOrCreate, File, Socket, CharDevice, BlockDevice
```
```yaml
apiVersion: v1
kind: Pod
metadata:
name: test-pd
spec:
containers:
- image: nginx
name: test-container
volumeMounts:
- mountPath: /test-pd
name: test-volume
volumes:
- name: test-volume
hostPath:
path: /data
type: Directory
```
#### configMap
`configMap` 볼륨은 ConfigMap의 데이터를 포드에 마운트합니다. ConfigMap은 키-값 쌍의 형태로 구성 데이터를 저장하는 데 사용됩니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: configmap-pod
spec:
containers:
- name: test
image: busybox
volumeMounts:
- name: config-vol
mountPath: /etc/config
volumes:
- name: config-vol
configMap:
name: log-config
items:
- key: log_level
path: log_level
```
#### secret
`secret` 볼륨은 Secret의 데이터를 포드에 마운트합니다. Secret은 암호, 토큰, 키 등의 민감한 정보를 저장하는 데 사용됩니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: secret-pod
spec:
containers:
- name: test
image: busybox
volumeMounts:
- name: secret-vol
mountPath: /etc/secret
readOnly: true
volumes:
- name: secret-vol
secret:
secretName: mysecret
items:
- key: username
path: my-username
```
#### nfs
`nfs` 볼륨은 기존 NFS(Network File System) 공유를 포드에 마운트합니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: nfs-pod
spec:
containers:
- name: test
image: busybox
volumeMounts:
- name: nfs-vol
mountPath: /mnt/nfs
volumes:
- name: nfs-vol
nfs:
server: nfs-server.example.com
path: /share
```
#### persistentVolumeClaim
`persistentVolumeClaim` 볼륨은 PersistentVolumeClaim을 포드에 마운트합니다. 이는 가장 일반적으로 사용되는 볼륨 유형 중 하나입니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: pvc-pod
spec:
containers:
- name: test
image: busybox
volumeMounts:
- name: pvc-vol
mountPath: /mnt/pvc
volumes:
- name: pvc-vol
persistentVolumeClaim:
claimName: my-pvc
```
#### CSI(Container Storage Interface)
CSI 볼륨은 Kubernetes와 외부 스토리지 시스템 간의 표준 인터페이스를 제공합니다. CSI를 사용하면 스토리지 제공업체가 Kubernetes 코드를 수정하지 않고도 자체 스토리지 드라이버를 개발할 수 있습니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: csi-pod
spec:
containers:
- name: test
image: busybox
volumeMounts:
- name: csi-vol
mountPath: /mnt/csi
volumes:
- name: csi-vol
csi:
driver: csi-driver.example.com
volumeAttributes:
foo: bar
nodePublishSecretRef:
name: csi-secret
```
## 퍼시스턴트 볼륨(PersistentVolume)
퍼시스턴트 볼륨(PV)은 관리자가 프로비저닝하거나 스토리지 클래스를 사용하여 동적으로 프로비저닝된 클러스터의 스토리지입니다. PV는 포드와 독립적인 수명 주기를 가지며, 포드가 삭제되어도 PV는 유지됩니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-04-storage-1.html)
### PV 생성
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv0001
labels:
release: stable
environment: dev
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: slow
mountOptions:
- hard
- nfsvers=4.1
nfs:
path: /tmp
server: 172.17.0.2
```
### PV 액세스 모드
PV는 다음과 같은 액세스 모드를 지원합니다:
- **ReadWriteOnce(RWO)**: 볼륨은 단일 노드에 의해 읽기-쓰기로 마운트될 수 있습니다.
- **ReadOnlyMany(ROX)**: 볼륨은 여러 노드에 의해 읽기 전용으로 마운트될 수 있습니다.
- **ReadWriteMany(RWX)**: 볼륨은 여러 노드에 의해 읽기-쓰기로 마운트될 수 있습니다.
- **ReadWriteOncePod(RWOP)**: 볼륨은 단일 포드에 의해 읽기-쓰기로 마운트될 수 있습니다(CSI 전용, v1.29부터 Stable).
RWO는 읽기·쓰기 마운트를 한 **노드**로 제한하며 한 파드로 제한하지 않습니다. 같은 노드의 여러 파드가 공유할 수 있습니다. RWOP는 이를 지원하는 CSI 드라이버에서 한 파드만 사용하도록 제한합니다. 액세스 모드는 파일시스템 권한을 대체하지 않습니다.
### PV 회수 정책
PV는 다음과 같은 회수 정책을 가질 수 있습니다:
- **Retain**: PVC가 삭제되어도 PV와 데이터는 유지됩니다. 관리자가 수동으로 정리해야 합니다.
- **Delete**: PVC가 삭제되면 PV와 외부 스토리지 자산이 자동으로 삭제됩니다.
- **Recycle**: PVC가 삭제되면 PV의 데이터가 삭제되고 PV는 다시 사용 가능한 상태가 됩니다(사용 중단됨).
### PV 상태
PV는 다음과 같은 상태를 가질 수 있습니다:
- **Available**: 아직 클레임에 바인딩되지 않은 사용 가능한 리소스입니다.
- **Bound**: 클레임에 바인딩되었습니다.
- **Released**: 클레임이 삭제되었지만, 리소스는 아직 클러스터에 의해 회수되지 않았습니다.
- **Failed**: 자동 회수가 실패했습니다.
## 퍼시스턴트 볼륨 클레임(PersistentVolumeClaim)
퍼시스턴트 볼륨 클레임(PVC)은 사용자의 스토리지 요청입니다. PVC는 PV와 유사하지만, PVC는 사용자가 스토리지를 요청하는 방법이고, PV는 관리자가 스토리지를 제공하는 방법입니다.
### PVC 생성
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myclaim
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 8Gi
storageClassName: slow
selector:
matchLabels:
release: "stable"
matchExpressions:
- {key: environment, operator: In, values: [dev]}
```
### PVC와 PV 바인딩
PVC가 생성되면 Kubernetes는 PVC의 요구 사항(스토리지 크기, 액세스 모드, 스토리지 클래스, 셀렉터 등)을 충족하는 PV를 찾아 바인딩합니다. 일치하는 PV가 없으면 적절한 StorageClass가 동적 프로비저닝할 수 있습니다. 다만 위처럼 selector가 비어 있지 않은 PVC는 동적 프로비저닝할 수 없으므로 일치하는 정적 PV가 없으면 Pending입니다. WaitForFirstConsumer도 스케줄링까지 바인딩을 의도적으로 지연합니다.
### PVC 사용
PVC는 포드에서 볼륨으로 사용할 수 있습니다:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: myfrontend
image: nginx
volumeMounts:
- mountPath: "/var/www/html"
name: mypd
volumes:
- name: mypd
persistentVolumeClaim:
claimName: myclaim
```
## 스토리지 클래스(StorageClass)
스토리지 클래스는 관리자가 제공하는 스토리지의 "클래스"를 설명합니다. 스토리지 클래스는 PV를 동적으로 프로비저닝하는 데 사용됩니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-04-storage-2.html)
### 스토리지 클래스 생성
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
provisioner: ebs.csi.aws.com
parameters:
type: gp3
csi.storage.k8s.io/fstype: ext4
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
```
이 예제는 AWS EBS gp3 볼륨을 프로비저닝하는 스토리지 클래스를 생성합니다.
### 프로비저너
스토리지 클래스는 볼륨을 프로비저닝하는 데 사용되는 프로비저너를 지정합니다. 현재 CSI 프로비저너 예시는 다음과 같습니다:
- `ebs.csi.aws.com`: AWS EBS
- `efs.csi.aws.com`: AWS EFS
- `fsx.csi.aws.com`: FSx for Lustre
- `pd.csi.storage.gke.io`: Google Persistent Disk
- `disk.csi.azure.com` / `file.csi.azure.com`: Azure Disk/File
- `nfs.csi.k8s.io`: NFS CSI 드라이버 (기존 NFS 서버 필요)
레거시 인트리 클라우드 플러그인은 제거되거나 마이그레이션되었습니다. 해당 CSI 드라이버를 설치해야 하며 `kubernetes.io/nfs`라는 내장 동적 프로비저너는 없습니다.
### 볼륨 바인딩 모드
스토리지 클래스는 다음과 같은 볼륨 바인딩 모드를 지원합니다:
- **Immediate**: 기본값으로, PVC가 생성되면 바로 볼륨이 프로비저닝됩니다.
- **WaitForFirstConsumer**: 포드가 PVC를 사용하려고 할 때까지 볼륨 프로비저닝을 지연합니다. 이는 볼륨이 포드와 같은 영역에 프로비저닝되도록 하는 데 유용합니다.
### 기본 스토리지 클래스
클러스터에는 기본 스토리지 클래스를 설정할 수 있습니다. PVC에서 스토리지 클래스를 지정하지 않으면 기본 스토리지 클래스가 사용됩니다.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: "true"
volumeBindingMode: WaitForFirstConsumer
```
## 동적 프로비저닝
동적 프로비저닝은 PVC가 생성될 때 자동으로 PV를 생성하는 기능입니다. 이를 통해 관리자가 미리 PV를 생성할 필요 없이 사용자가 필요할 때 스토리지를 요청할 수 있습니다.
### 동적 프로비저닝 예제
1. 스토리지 클래스 생성:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "3000"
encrypted: "true"
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
```
2. PVC 생성:
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myclaim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
storageClassName: fast
```
3. 포드에서 PVC 사용:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: myfrontend
image: nginx
volumeMounts:
- mountPath: "/var/www/html"
name: mypd
volumes:
- name: mypd
persistentVolumeClaim:
claimName: myclaim
```
## 볼륨 스냅샷
Kubernetes는 볼륨 스냅샷을 지원하여 PV의 특정 시점 복사본을 생성할 수 있습니다. 이는 백업 및 복원 시나리오에 유용합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-04-storage-3.html)
스냅샷 CRD, 스냅샷 컨트롤러, 스냅샷을 지원하는 CSI 드라이버가 필요합니다. 아래는 EBS 예시이며 소스 PVC와 복원 StorageClass는 해당 드라이버를 사용해야 합니다. `readyToUse: true`를 기다리고 스냅샷 복원 크기 이상의 용량을 요청하세요. 스토리지 스냅샷만으로 DB 일관성을 보장하지 않으므로 쓰기를 중지하거나 DB 인식 백업을 사용하세요.
### 볼륨 스냅샷 클래스
```yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: ebs-snapclass
driver: ebs.csi.aws.com
deletionPolicy: Delete
```
### 볼륨 스냅샷 생성
```yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: new-snapshot
spec:
volumeSnapshotClassName: ebs-snapclass
source:
persistentVolumeClaimName: myclaim
```
### 스냅샷에서 PVC 생성
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: restore-pvc
spec:
storageClassName: standard
dataSource:
name: new-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
```
## 볼륨 확장
Kubernetes는 PVC의 크기를 확장하는 기능을 지원합니다. 이를 위해서는 스토리지 클래스에서 `allowVolumeExpansion: true`를 설정해야 합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-04-storage-4.html)
### PVC 확장
확장을 지원하는 EBS StorageClass의 기존 100Gi PVC라면 요청 용량만 변경합니다:
```bash
kubectl patch pvc myclaim --type merge -p '{"spec":{"resources":{"requests":{"storage":"120Gi"}}}}'
```
PVC의 네임스페이스와 기존 StorageClass를 사용하세요. 드라이버·파일시스템이 확장을 지원해야 하며 축소는 지원하지 않습니다. 바인딩된 PVC의 클래스를 바꾸거나 PV 용량을 직접 수정해 확장을 흉내 내지 마세요.
## Projected Volumes
Projected Volumes는 여러 볼륨 소스를 하나의 디렉토리에 마운트할 수 있는 기능입니다. secrets, configMaps, downwardAPI, serviceAccountToken을 단일 볼륨으로 결합할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-04-storage-5.html)
### Projected Volume 예제
```yaml
apiVersion: v1
kind: Pod
metadata:
name: projected-volume-pod
spec:
containers:
- name: app
image: busybox
command: ["sleep", "3600"]
volumeMounts:
- name: all-in-one
mountPath: /etc/credentials
readOnly: true
volumes:
- name: all-in-one
projected:
sources:
# Secret에서 데이터베이스 자격 증명
- secret:
name: db-credentials
items:
- key: username
path: db-username
- key: password
path: db-password
# ConfigMap에서 애플리케이션 구성
- configMap:
name: app-config
items:
- key: config.yaml
path: app-config.yaml
# Downward API에서 포드 메타데이터
- downwardAPI:
items:
- path: labels
fieldRef:
fieldPath: metadata.labels
- path: namespace
fieldRef:
fieldPath: metadata.namespace
# ServiceAccountToken
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: api
```
### 사용 사례
1. **통합 자격 증명 관리**: 여러 소스의 자격 증명을 단일 디렉토리에 마운트
2. **애플리케이션 구성**: 구성 파일과 시크릿을 함께 제공
3. **서비스 메시 통합**: ServiceAccount 토큰과 인증서를 함께 마운트
프로젝션 토큰은 갱신되므로 앱이 파일을 다시 읽어야 합니다. 명시한 `audience`는 검증 서비스와 일치해야 하며 Kubernetes API에 자동으로 유효한 값은 아닙니다.
## Generic Ephemeral Volumes
Generic Ephemeral Volumes는 PVC 기반의 임시 볼륨을 제공합니다. emptyDir과 달리 동적 프로비저닝과 스토리지 클래스의 모든 기능을 사용할 수 있습니다.
### emptyDir과의 비교
| 특성 | emptyDir | Generic Ephemeral Volume |
|------|----------|-------------------------|
| **프로비저닝** | 노드 로컬 디스크 | 동적 프로비저닝 (CSI) |
| **스토리지 클래스** | 지원 안 함 | 지원 |
| **용량 지정** | sizeLimit (소프트 제한) | 정확한 용량 요청 |
| **스냅샷** | 지원 안 함 | 지원 |
| **암호화** | 노드에 따라 다름 | 스토리지 클래스로 제어 |
| **IOPS/처리량** | 노드에 따라 다름 | 스토리지 클래스로 제어 |
| **수명 주기** | 포드와 함께 | 포드와 함께 |
### Generic Ephemeral Volume 예제
```yaml
apiVersion: v1
kind: Pod
metadata:
name: ephemeral-volume-pod
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: scratch
mountPath: /scratch
volumes:
- name: scratch
ephemeral:
volumeClaimTemplate:
metadata:
labels:
type: ephemeral
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gp3-fast
resources:
requests:
storage: 10Gi
```
### 사용 사례
1. **고성능 임시 스토리지**: CSI 드라이버의 고성능 스토리지를 임시로 사용
2. **대용량 캐시**: emptyDir의 노드 디스크 제한 없이 대용량 캐시 사용
3. **ML/AI 워크로드**: 모델 학습 중 체크포인트를 고성능 스토리지에 저장
4. **암호화된 임시 스토리지**: CSI 드라이버의 암호화 기능 활용
```yaml
# ML 학습용 고성능 임시 스토리지
apiVersion: v1
kind: Pod
metadata:
name: ml-training
spec:
containers:
- name: trainer
image: pytorch/pytorch:latest
volumeMounts:
- name: checkpoint
mountPath: /checkpoints
volumes:
- name: checkpoint
ephemeral:
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: io2-high-iops
resources:
requests:
storage: 100Gi
```
Generic ephemeral PVC는 파드가 소유하며 파드 삭제 시 가비지 수집됩니다. 실제 데이터 삭제는 PV 회수 정책을 따르므로 `Retain`이면 스토리지가 남고 수동 정리가 필요합니다. 파드 손실 이후에도 필요한 체크포인트는 별도 영구 저장소에 보관하세요.
## Block Volume Mode
Block Volume Mode는 파일시스템 대신 원시 블록 디바이스로 볼륨을 마운트할 수 있는 기능입니다. 이는 데이터베이스와 같이 파일시스템 오버헤드 없이 직접 블록 접근이 필요한 애플리케이션에 유용합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-04-storage-6.html)
### Block Volume 설정
```yaml
# PersistentVolume
apiVersion: v1
kind: PersistentVolume
metadata:
name: block-pv
spec:
capacity:
storage: 100Gi
volumeMode: Block # Block 모드 지정
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: block-storage
csi:
driver: ebs.csi.aws.com
volumeHandle: vol-0123456789abcdef0
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: [us-west-2a]
---
# PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: block-pvc
spec:
volumeMode: Block # Block 모드 지정
accessModes:
- ReadWriteOnce
storageClassName: block-storage
resources:
requests:
storage: 100Gi
```
### Block Volume 사용
```yaml
apiVersion: v1
kind: Pod
metadata:
name: block-volume-pod
spec:
containers:
- name: database
image: custom-database:latest
volumeDevices: # volumeMounts 대신 volumeDevices 사용
- name: data
devicePath: /dev/xvda # 디바이스 경로
volumes:
- name: data
persistentVolumeClaim:
claimName: block-pvc
```
### 사용 사례
1. **특수 스토리지 엔진**: 원시 블록 장치를 명시적으로 지원하는 소프트웨어만 사용하며 일반 MySQL/PostgreSQL 데이터 디렉토리는 파일시스템 필요
2. **NoSQL 데이터베이스**: Cassandra, ScyllaDB 등의 성능 최적화
3. **가상화**: VM 디스크 이미지 저장
4. **커스텀 파일시스템**: 애플리케이션이 자체 파일시스템 사용
정적 EBS 볼륨 ID와 nodeAffinity의 영역은 실제 볼륨과 해당 가용 영역으로 바꾸세요. `custom-database` 이미지는 원시 블록 장치를 지원하는 소프트웨어의 자리 표시자입니다.
## Volume Cloning
Volume Cloning은 기존 PVC의 데이터를 새 PVC로 복제하는 기능입니다. 스냅샷을 거치지 않고 직접 PVC-to-PVC 클론을 생성할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-04-storage-7.html)
#정적 EBS 볼륨 ID와 nodeAffinity의 영역은 실제 볼륨과 해당 가용 영역으로 바꾸세요. `custom-database` 이미지는 원시 블록 장치를 지원하는 소프트웨어의 자리 표시자입니다.
## Volume Cloning 예제
```yaml
# 소스 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: source-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: gp3
resources:
requests:
storage: 100Gi
---
# 클론 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cloned-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: gp3 # 동일한 스토리지 클래스
resources:
requests:
storage: 100Gi # 동일하거나 더 큰 크기
dataSource:
kind: PersistentVolumeClaim
name: source-pvc # 소스 PVC 참조
```
### CSI 드라이버 지원과 제약
EBS CSI 드라이버는 v1.51.0부터 PVC 복제를 지원합니다([버전 고정 예제](https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/v1.66.0/examples/kubernetes/clone/README.md)). 설치 버전과 IAM 권한을 확인하세요. `kubectl get csidriver`는 CSI RPC의 `CLONE_VOLUME` capability를 표시하지 않습니다. 해당 드라이버의 공식 기능 문서와 CSI `ControllerGetCapabilities`가 근거입니다.
FSx for Lustre CSI 드라이버는 현재 `CLONE_VOLUME`을 제공하지 않으며 EFS도 PVC 복제 지원을 가정하면 안 됩니다. 소스·대상은 같은 네임스페이스와 volumeMode를 사용해야 하고 대상 용량은 소스 이상이어야 합니다. StorageClass는 드라이버 호환 범위에서 달라도 됩니다. 소스는 바인딩되고 사용 중이 아닌 상태로 준비하고 DB 일관성을 확보하세요.
## Storage ResourceQuota
Storage ResourceQuota는 네임스페이스 단위로 스토리지 리소스 사용을 제한합니다. PVC 수와 총 스토리지 용량을 제어할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-04-storage-8.html)
### Storage ResourceQuota 예제
```yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: storage-quota
namespace: dev-team
spec:
hard:
# 총 PVC 수 제한
persistentvolumeclaims: "10"
# 총 스토리지 요청량 제한
requests.storage: "500Gi"
# 특정 스토리지 클래스별 제한
gp3.storageclass.storage.k8s.io/requests.storage: "200Gi"
gp3.storageclass.storage.k8s.io/persistentvolumeclaims: "5"
io2.storageclass.storage.k8s.io/requests.storage: "100Gi"
io2.storageclass.storage.k8s.io/persistentvolumeclaims: "3"
```
### 스토리지 클래스별 쿼터
```yaml
# 여러 팀을 위한 스토리지 할당
---
# 개발 팀
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-storage-quota
namespace: development
spec:
hard:
requests.storage: "200Gi"
persistentvolumeclaims: "20"
gp3.storageclass.storage.k8s.io/requests.storage: "150Gi"
io2.storageclass.storage.k8s.io/requests.storage: "50Gi"
---
# 프로덕션 팀
apiVersion: v1
kind: ResourceQuota
metadata:
name: prod-storage-quota
namespace: production
spec:
hard:
requests.storage: "2Ti"
persistentvolumeclaims: "50"
gp3.storageclass.storage.k8s.io/requests.storage: "1Ti"
io2.storageclass.storage.k8s.io/requests.storage: "500Gi"
fsx-lustre.storageclass.storage.k8s.io/requests.storage: "500Gi"
```
### 쿼터 사용량 확인
```bash
# ResourceQuota 상태 확인
kubectl describe resourcequota storage-quota -n dev-team
# 출력 예시:
# Name: storage-quota
# Namespace: dev-team
# Resource Used Hard
# -------- ---- ----
# gp3.storageclass.storage.k8s.io/persistentvolumeclaims 3 5
# gp3.storageclass.storage.k8s.io/requests.storage 75Gi 200Gi
# persistentvolumeclaims 5 10
# requests.storage 100Gi 500Gi
```
### LimitRange와 함께 사용
```yaml
# PVC 최소/최대 용량 제한; 요청 기본값을 주입하지 않음
apiVersion: v1
kind: LimitRange
metadata:
name: storage-limits
namespace: dev-team
spec:
limits:
- type: PersistentVolumeClaim
max:
storage: 100Gi
min:
storage: 1Gi
```
## EKS에서의 스토리지 옵션
Amazon EKS에서는 다양한 스토리지 옵션을 사용할 수 있습니다. 각 옵션은 서로 다른 사용 사례와 성능 특성을 가지고 있으므로, 애플리케이션의 요구 사항에 맞는 적절한 스토리지를 선택하는 것이 중요합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-04-storage-9.html)
### Amazon EBS
Amazon EBS(Elastic Block Store)는 EC2 인스턴스에 연결할 수 있는 블록 스토리지 볼륨을 제공합니다. EKS에서는 EBS CSI 드라이버를 사용하여 EBS 볼륨을 Kubernetes 포드에 마운트할 수 있습니다.
#### EBS CSI 드라이버 설치
[EKS 드라이버 설치 문서](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html)에 따라 호환되는 애드온·드라이버 버전과 IAM 역할, 노드 선행 조건을 준비한 후 PVC를 생성하세요. StorageClass만으로 드라이버가 설치되지는 않습니다.
#### EBS 스토리지 클래스
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-sc
provisioner: ebs.csi.aws.com
parameters:
type: gp3
csi.storage.k8s.io/fstype: ext4
encrypted: "true"
volumeBindingMode: WaitForFirstConsumer
```
#### EBS 볼륨 유형
Amazon EBS는 다양한 볼륨 유형을 제공합니다:
1. **gp3**: 범용 SSD 볼륨으로, 대부분의 워크로드에 적합합니다. 기본 3,000 IOPS와 125 MiB/s를 제공하며 현재 리전 볼륨 한도는 용량·IOPS 비율과 인스턴스 한도에 따라 최대 80,000 IOPS, 2,000 MiB/s입니다. Outposts 한도는 더 낮습니다([AWS 사양](https://docs.aws.amazon.com/ebs/latest/userguide/general-purpose.html)).
2. **io2**: 고성능 SSD 볼륨으로, 높은 IOPS가 필요한 워크로드에 적합합니다. 현재 io2 Block Express는 적합한 Nitro 인스턴스에서 볼륨·인스턴스 한도에 따라 최대 1,000 IOPS/GiB와 256,000 IOPS를 지원합니다([AWS 사양](https://docs.aws.amazon.com/ebs/latest/userguide/provisioned-iops.html)).
3. **st1**: 처리량 최적화 HDD 볼륨으로, 빅데이터, 데이터 웨어하우스, 로그 처리 등의 처리량 집약적 워크로드에 적합합니다.
4. **sc1**: 콜드 HDD 볼륨으로, 자주 액세스하지 않는 데이터에 적합합니다.
#### EBS 스토리지 클래스 예제 (gp3)
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-gp3
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "3000"
throughput: "125"
encrypted: "true"
kmsKeyId: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
volumeBindingMode: WaitForFirstConsumer
```
#### EBS 스토리지 클래스 예제 (io2)
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-io2
provisioner: ebs.csi.aws.com
parameters:
type: io2
iops: "10000"
encrypted: "true"
volumeBindingMode: WaitForFirstConsumer
```
### Amazon EFS
Amazon EFS(Elastic File System)는 여러 EC2 인스턴스에서 동시에 액세스할 수 있는 확장 가능한 파일 스토리지를 제공합니다. EFS는 ReadWriteMany 액세스 모드를 지원하므로 여러 포드에서 동일한 볼륨을 공유해야 하는 경우에 유용합니다.
#### EFS CSI 드라이버 설치
[EKS 드라이버 설치 문서](https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html)에 따라 호환되는 애드온·드라이버 버전과 IAM 역할, 노드 선행 조건을 준비한 후 PVC를 생성하세요. StorageClass만으로 드라이버가 설치되지는 않습니다.
#### EFS 파일 시스템 생성
EFS 파일 시스템을 생성하려면 AWS Management Console, AWS CLI 또는 AWS CloudFormation을 사용할 수 있습니다.
AWS CLI를 사용한 예제:
```bash
# EFS 파일 시스템 생성
aws efs create-file-system \
--creation-token eks-efs \
--performance-mode generalPurpose \
--encrypted \
--throughput-mode bursting \
--tags Key=Name,Value=EKS-EFS
# 파일 시스템 ID 저장
FS_ID=$(aws efs describe-file-systems \
--creation-token eks-efs \
--query "FileSystems[0].FileSystemId" \
--output text)
# 가용 영역당 마운트 타겟 하나 생성
aws efs create-mount-target \
--file-system-id "$FS_ID" \
--subnet-id subnet-0eabfaa81fb22bcaf \
--security-groups sg-068000ccf82dfba88
```
#### EFS 스토리지 클래스
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: efs-sc
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap
fileSystemId: fs-1234abcd
directoryPerms: "700"
```
#### EFS 액세스 포인트를 사용한 PV 및 PVC
```yaml
# 퍼시스턴트 볼륨
apiVersion: v1
kind: PersistentVolume
metadata:
name: efs-pv
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: efs-sc
csi:
driver: efs.csi.aws.com
volumeHandle: fs-1234abcd::fsap-0123456789abcdef
---
# 퍼시스턴트 볼륨 클레임
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: efs-pvc
spec:
accessModes:
- ReadWriteMany
storageClassName: efs-sc
resources:
requests:
storage: 5Gi
```
파일시스템·액세스 포인트 ID를 교체하고 클라이언트 보안 그룹에서 마운트 타겟으로 NFS TCP 2049를 허용하세요. 동적 프로비저닝은 기존 파일시스템에 액세스 포인트를 만들며 파일시스템 자체를 생성하지 않습니다. EFS PVC 용량은 바인딩용 값이며 PVC별 사용량 한도가 아니고 EFS는 탄력적으로 증가합니다. Fargate는 EFS 정적 프로비저닝만 지원합니다.
#### EFS 성능 모드
EFS는 두 가지 성능 모드를 제공합니다:
1. **General Purpose**: 대부분의 파일 시스템 워크로드에 권장되는 기본 모드입니다. 낮은 지연 시간을 제공합니다.
2. **Max I/O**: 작업당 지연 시간이 더 긴 이전 세대 모드이며 AWS는 새 워크로드에 General Purpose를 권장합니다. Max I/O는 Elastic 처리량과 함께 사용할 수 없습니다.
#### EFS 처리량 모드
EFS는 세 가지 처리량 모드를 제공합니다:
1. **Bursting**: 파일 시스템 크기에 따라 기본 처리량이 할당되고, 버스트 크레딧을 사용하여 일시적으로 더 높은 처리량을 제공합니다.
2. **Provisioned**: 파일 시스템 크기와 관계없이 지정된 처리량을 제공합니다.
3. **Elastic**: 워크로드에 따라 자동으로 처리량을 확장하고 축소합니다.
### Amazon FSx for Lustre
Amazon FSx for Lustre는 고성능 컴퓨팅 워크로드를 위한 고성능 파일 시스템을 제공합니다. FSx for Lustre는 대규모 데이터 처리, 기계 학습, 분석 등의 워크로드에 적합합니다.
#### FSx for Lustre CSI 드라이버 설치
[EKS 드라이버 설치 문서](https://docs.aws.amazon.com/eks/latest/userguide/fsx-csi-create.html)에 따라 호환되는 애드온·드라이버 버전과 IAM 역할, 노드 선행 조건을 준비한 후 PVC를 생성하세요. StorageClass만으로 드라이버가 설치되지는 않습니다.
#### FSx for Lustre 파일 시스템 생성
AWS CLI를 사용한 예제:
```bash
aws fsx create-file-system \
--file-system-type LUSTRE \
--storage-capacity 1200 \
--subnet-ids subnet-0eabfaa81fb22bcaf \
--lustre-configuration DeploymentType=SCRATCH_2
```
#### FSx for Lustre 스토리지 클래스
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fsx-sc
provisioner: fsx.csi.aws.com
parameters:
subnetId: subnet-0eabfaa81fb22bcaf
securityGroupIds: sg-068000ccf82dfba88
deploymentType: SCRATCH_2
dataCompressionType: "NONE"
weeklyMaintenanceStartTime: "7:09:00"
```
#### FSx for Lustre 배포 유형
FSx for Lustre는 scratch와 persistent 스토리지를 구분합니다:
- **SCRATCH_1 / SCRATCH_2**: 데이터 복제가 없는 임시 저장소입니다. 장애 파일 서버는 교체되지 않으며 해당 데이터가 손실될 수 있습니다. SCRATCH_2가 이를 자동 복구하지는 않습니다.
- **PERSISTENT_1 / PERSISTENT_2**: 데이터 복제와 장애 구성 요소 자동 교체를 제공합니다. 배포 유형별로 스토리지 클래스, 용량 증분, 처리량 옵션, 지원 리전이 다릅니다.
Scratch 기본 처리량은 200 MBps/TiB이며 최대 6배 버스트가 가능합니다. `PerUnitStorageThroughput`은 persistent SSD/HDD 설정용이며 SCRATCH_2용이 아닙니다. Persistent SSD 옵션은 PERSISTENT_1의 50/100/200 MBps/TiB와 PERSISTENT_2의 125/250/500/1000 등이 있습니다. Intelligent-Tiering은 용량·처리량 설정이 다르므로 크기 선택 전에 [배포 사양](https://docs.aws.amazon.com/fsx/latest/LustreGuide/using-fsx-lustre.html)과 API를 확인하세요.
### vLLM 워크로드를 위한 FSx for Lustre 구성
vLLM(LLM 추론·서빙 엔진)과 같은 대규모 AI 모델 워크로드는 높은 처리량과 낮은 지연 시간을 가진 스토리지가 필요합니다. FSx for Lustre는 이러한 요구 사항을 충족하는 이상적인 솔루션입니다.
#### vLLM을 위한 FSx for Lustre 스토리지 클래스
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fsx-lustre-vllm
provisioner: fsx.csi.aws.com
parameters:
subnetId: subnet-0eabfaa81fb22bcaf
securityGroupIds: sg-068000ccf82dfba88
deploymentType: PERSISTENT_1
perUnitStorageThroughput: "200"
dataCompressionType: "NONE"
reclaimPolicy: Retain
volumeBindingMode: Immediate
```
FSx CSI 드라이버는 StorageClass의 `storageCapacity` 파라미터가 아닌 PVC 요청에서 용량을 계산합니다. 서브넷·보안 그룹 ID를 교체하고 호환 Lustre 클라이언트·CSI 드라이버를 설치하며 선택한 배포 유형에 유효한 용량을 요청하세요. 아래 추론 이미지는 모델 서빙 명령을 제공해야 하는 자리 표시자입니다.
#### vLLM 워크로드를 위한 PVC
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vllm-model-storage
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 4800Gi
storageClassName: fsx-lustre-vllm
```
#### vLLM 배포 예제
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-inference
spec:
replicas: 1
selector:
matchLabels:
app: vllm-inference
template:
metadata:
labels:
app: vllm-inference
spec:
nodeSelector:
node.kubernetes.io/instance-type: g5.12xlarge
containers:
- name: vllm
image: vllm-inference:latest
resources:
limits:
nvidia.com/gpu: 4
requests:
nvidia.com/gpu: 4
memory: "64Gi"
cpu: "32"
volumeMounts:
- name: model-storage
mountPath: /models
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: vllm-model-storage
```
#### vLLM 성능 최적화 팁
1. **적절한 처리량 선택**: vLLM 워크로드의 경우 모델 로드 동시성과 데이터셋 접근을 측정해 처리량을 선택합니다.
2. **스토리지 용량 최적화**: 모델 크기와 데이터셋 크기를 고려하여 충분한 스토리지 용량을 할당합니다.
3. **네트워크 최적화**: FSx for Lustre 파일 시스템과 EKS 노드가 동일한 가용 영역에 있는지 확인합니다.
4. **인스턴스 유형 선택**: GPU 인스턴스(예: g5.12xlarge)를 사용하여 vLLM 워크로드의 성능을 최적화합니다.
5. **메모리 구성**: 모델 크기에 따라 충분한 메모리를 할당합니다.
6. **파일 시스템 마운트 옵션**: 최적의 성능을 위해 적절한 마운트 옵션을 사용합니다.
```bash
mount -t lustre -o noatime,flock fs-1234abcd.fsx.us-west-2.amazonaws.com@tcp:/fsx /mnt/fsx
```
### 스토리지 옵션 비교
| 스토리지 옵션 | 액세스 모드 | 사용 사례 | 성능 | 비용 | 확장성 |
|------------|----------|--------|------|-----|------|
| Amazon EBS | ReadWriteOnce | 단일 노드에 마운트하는 블록 스토리지 | 중간-높음 | 중간 | 제한적 (단일 노드) |
| Amazon EFS | ReadWriteMany | 여러 포드에서 공유하는 파일 스토리지 | 중간 | 중간-높음 | 높음 (여러 노드) |
| Amazon FSx for Lustre | ReadWriteMany | 고성능 컴퓨팅, 기계 학습, 분석 | 매우 높음 | 높음 | 매우 높음 (병렬 액세스) |
### EKS 스토리지 선택 가이드
1. **단일 노드에 마운트하는 블록 스토리지가 필요한 경우**: Amazon EBS
- 데이터베이스
- 상태 저장 애플리케이션
- 단일 노드에서 실행되는 워크로드
2. **여러 포드에서 공유하는 파일 스토리지가 필요한 경우**: Amazon EFS
- 웹 서버 콘텐츠
- 공유 구성 파일
- 중간 규모의 데이터 처리
3. **고성능 파일 스토리지가 필요한 경우**: Amazon FSx for Lustre
- 대규모 데이터 처리
- 기계 학습 및 AI 워크로드 (vLLM 등)
- 고성능 컴퓨팅 (HPC)
- 빅데이터 분석
## 결론
이 장에서는 Kubernetes의 스토리지 개념에 대해 알아보았습니다. 볼륨은 포드 내의 컨테이너가 데이터를 저장하고 공유할 수 있는 방법을 제공하고, 퍼시스턴트 볼륨과 퍼시스턴트 볼륨 클레임은 포드와 독립적인 수명 주기를 가진 스토리지를 제공합니다. 스토리지 클래스는 동적 프로비저닝을 통해 사용자가 필요할 때 스토리지를 요청할 수 있게 합니다.
EKS에서는 Amazon EBS, Amazon EFS, Amazon FSx for Lustre 등 다양한 스토리지 옵션을 사용할 수 있으며, 각 옵션은 서로 다른 사용 사례와 성능 특성을 가지고 있습니다. 특히 vLLM과 같은 대규모 AI 모델 워크로드의 경우, 높은 처리량과 낮은 지연 시간을 제공하는 FSx for Lustre가 이상적인 선택입니다. FSx for Lustre는 병렬 파일 시스템으로, 여러 노드에서 동시에 데이터에 액세스할 수 있어 대규모 모델 학습 및 추론 작업에 적합합니다.
애플리케이션의 요구 사항에 맞는 적절한 스토리지 옵션을 선택하는 것이 중요합니다. 단일 노드에 마운트하는 블록 스토리지가 필요한 경우 Amazon EBS를, 여러 포드에서 공유하는 파일 스토리지가 필요한 경우 Amazon EFS를, 고성능 파일 스토리지가 필요한 경우 Amazon FSx for Lustre를 선택하는 것이 좋습니다.
다음 장에서는 Kubernetes의 구성 및 시크릿에 대해 알아보겠습니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [스토리지 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/core/04-storage-quiz)를 풀어보세요.
## 참고 자료
- [Kubernetes 공식 문서 - 볼륨](https://kubernetes.io/docs/concepts/storage/volumes/)
- [Kubernetes 공식 문서 - 퍼시스턴트 볼륨](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)
- [Kubernetes 공식 문서 - 스토리지 클래스](https://kubernetes.io/docs/concepts/storage/storage-classes/)
- [Kubernetes 공식 문서 - 볼륨 스냅샷](https://kubernetes.io/docs/concepts/storage/volume-snapshots/)
- [AWS EBS CSI 드라이버](https://github.com/kubernetes-sigs/aws-ebs-csi-driver)
- [AWS EFS CSI 드라이버](https://github.com/kubernetes-sigs/aws-efs-csi-driver)
- [AWS FSx for Lustre CSI 드라이버](https://github.com/kubernetes-sigs/aws-fsx-csi-driver)
- [AWS 블로그 - Scaling your LLM inference workloads: Multi-node deployment with TensorRT-LLM and Triton on Amazon EKS](https://aws.amazon.com/ko/blogs/hpc/scaling-your-llm-inference-workloads-multi-node-deployment-with-tensorrt-llm-and-triton-on-amazon-eks/)
- [AWS 워크숍 - GenAI FSx EKS](https://catalog.workshops.aws/genaifsxeks/en-US/200-module2-genai/210-deploy)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/core/05-configuration-secrets
----------------------------------------
# 구성 및 시크릿
> **지원 버전**: Kubernetes 1.35, 1.36, 1.37
> **마지막 업데이트**: 2026년 2월 22일
Kubernetes에서 구성 관리는 애플리케이션의 설정을 코드와 분리하여 관리하는 중요한 부분입니다. 이 장에서는 컨피그맵(ConfigMap), 시크릿(Secret), 환경 변수, 볼륨을 통한 구성 마운트 등 Kubernetes의 구성 관리 방법에 대해 자세히 알아보겠습니다.
## 실습 환경 설정
이 문서의 예제를 따라하기 위해서는 다음과 같은 도구와 환경이 필요합니다:
### 필수 도구
- API 서버와 마이너 버전 차이가 1 이내인 kubectl
- 작동하는 Kubernetes 클러스터 (EKS, minikube, kind 등)
### 구성 예제 설정
```bash
# 네임스페이스 생성
kubectl create namespace config-demo
# ConfigMap 생성
kubectl -n config-demo create configmap app-config \
--from-literal=APP_ENV=production \
--from-literal=APP_DEBUG=false \
--from-literal=APP_PORT=8080
# Secret 생성
kubectl -n config-demo create secret generic app-secrets \
--from-literal=DB_USER=admin \
--from-literal=DB_PASSWORD=s3cr3t \
--from-literal=API_KEY=abcdef123456
# ConfigMap과 Secret을 사용하는 Pod 생성
kubectl -n config-demo apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: config-test-pod
spec:
containers:
- name: test-container
image: busybox
command: ["sh", "-c", 'test -n "$DB_PASSWORD" && echo "Secret available" && sleep 3600']
env:
- name: APP_ENV
valueFrom:
configMapKeyRef:
name: app-config
key: APP_ENV
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: DB_PASSWORD
restartPolicy: Never
EOF
# Pod 로그 확인
kubectl -n config-demo logs config-test-pod
```
## 한 눈에 보는 구성 관리

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-05-configuration-secrets-0.html)
## 목차
1. [컨피그맵(ConfigMap)](#컨피그맵configmap)
2. [시크릿(Secret)](#시크릿secret)
3. [환경 변수](#환경-변수)
4. [볼륨을 통한 구성 마운트](#볼륨을-통한-구성-마운트)
5. [구성 모범 사례](#구성-모범-사례)
6. [Amazon EKS에서의 구성 관리](#amazon-eks에서의-구성-관리)
## 컨피그맵(ConfigMap)
> **핵심 개념**: 컨피그맵은 키-값 쌍 형태로 구성 데이터를 저장하는 객체로, 애플리케이션 코드와 구성을 분리합니다.
컨피그맵은 키-값 쌍의 형태로 구성 데이터를 저장하는 API 객체입니다. 컨피그맵을 사용하면 컨테이너 이미지에서 구성 데이터를 분리하여 애플리케이션을 더 쉽게 이식할 수 있습니다.
### ConfigMap과 Secret 비교
| 특성 | ConfigMap | Secret |
|------|-----------|--------|
| **용도** | 일반 구성 데이터 | 민감한 구성 데이터 |
| **API 표현** | UTF-8 `data` 또는 base64 `binaryData` | Base64 `data`; 쓰기 시 `stringData` 허용 |
| **크기 제한** | 1 MiB | 1 MiB |
| **저장 시 암호화** | API 서버·플랫폼 구성에 따라 다름 | API 서버·플랫폼 구성에 따라 다름 |
| **볼륨 타입** | configMap | secret |
| **사용 사례** | 환경 변수, 설정 파일 | 비밀번호, 토큰, 인증서 |
| **자동 업데이트** | 볼륨 마운트 시 지연 가능 | 볼륨 마운트 시 지연 가능 |
### 컨피그맵 생성 방법
컨피그맵은 다양한 방법으로 생성할 수 있습니다:
1. **명령형 방식으로 생성**:
```bash
# 리터럴 값으로 생성
kubectl create configmap my-config --from-literal=key1=value1 --from-literal=key2=value2
# 파일에서 생성
kubectl create configmap my-config --from-file=config.properties
# 디렉토리에서 생성
kubectl create configmap my-config --from-file=config-dir/
```
2. **선언형 방식으로 생성**:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: my-config
data:
# 단순 키-값 쌍
database.host: "mysql"
database.port: "3306"
# 파일 형태의 구성
config.yaml: |
server:
port: 8080
logging:
level: INFO
features:
enabled: true
```
### 컨피그맵 사용 방법
컨피그맵은 다음과 같은 방법으로 사용할 수 있습니다:
1. **환경 변수로 사용**:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: config-env-pod
spec:
containers:
- name: app
image: nginx
env:
# 단일 키-값 참조
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: my-config
key: database.host
# 모든 키-값 참조
envFrom:
- configMapRef:
name: my-config
```

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-05-configuration-secrets-1.html)
### 컨피그맵 생성
컨피그맵은 여러 가지 방법으로 생성할 수 있습니다:
#### 명령형 방식
```bash
# 리터럴 값으로 생성
kubectl create configmap my-config --from-literal=key1=value1 --from-literal=key2=value2
# 파일에서 생성
kubectl create configmap my-config --from-file=config.properties
# 디렉토리에서 생성
kubectl create configmap my-config --from-file=config-dir/
```
#### 선언형 방식
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: my-config
data:
# 단순 키-값 쌍
key1: value1
key2: value2
# 파일과 같은 구성
config.properties: |
property1=value1
property2=value2
# JSON 구성
config.json: |
{
"property1": "value1",
"property2": "value2"
}
```
### 컨피그맵 사용
컨피그맵은 다음과 같은 방법으로 포드에서 사용할 수 있습니다:
##자체 관리형 클러스터는 `--encryption-provider-config`로 이 파일을 로드하고 암호화 키를 보호하며 기존 Secret도 다시 저장해야 합니다. `kubectl apply`로 생성하는 리소스가 아닙니다. EKS의 암호화는 서비스가 관리합니다(아래 참고).
## 환경 변수로 사용
```yaml
apiVersion: v1
kind: Pod
metadata:
name: configmap-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "env" ]
env:
# 단일 키-값 쌍 사용
- name: SPECIAL_KEY
valueFrom:
configMapKeyRef:
name: my-config
key: key1
# 모든 키-값 쌍을 환경 변수로 사용
envFrom:
- configMapRef:
name: my-config
restartPolicy: Never
```
#### 볼륨으로 마운트
```yaml
apiVersion: v1
kind: Pod
metadata:
name: configmap-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "ls /etc/config/" ]
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: my-config
restartPolicy: Never
```
#### 특정 키만 마운트
```yaml
apiVersion: v1
kind: Pod
metadata:
name: configmap-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "cat /etc/config/key1" ]
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: my-config
items:
- key: key1
path: key1
restartPolicy: Never
```
### 컨피그맵 업데이트
수정 가능한 ConfigMap·Secret의 전체 볼륨 마운트는 최종적으로 갱신되며 지연은 kubelet 동기화와 변경 감지·캐시 설정에 따라 다릅니다. 앱도 파일을 다시 읽거나 리로드해야 합니다. `subPath` 마운트는 갱신되지 않습니다. 실행 중 프로세스의 환경 변수는 바뀌지 않으므로 새 값을 사용하려면 Deployment 롤아웃 등으로 파드를 재생성하세요.
```bash
kubectl edit configmap my-config
```
또는
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: my-config
data:
key1: updated-value1
key2: value2
```
```bash
kubectl apply -f updated-configmap.yaml
```
## 시크릿(Secret)
시크릿은 암호, OAuth 토큰, SSH 키와 같은 민감한 정보를 저장하는 API 객체입니다. 시크릿은 컨피그맵과 유사하지만, 민감한 데이터를 저장하기 위한 추가적인 보안 기능을 제공합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-05-configuration-secrets-2.html)
### 시크릿 유형
Kubernetes는 다양한 유형의 시크릿을 제공합니다:
- **Opaque**: 기본 유형으로, 임의의 사용자 정의 데이터를 저장합니다.
- **kubernetes.io/service-account-token**: 서비스 계정 토큰을 저장합니다.
- **kubernetes.io/dockercfg**: `.dockercfg` 파일의 직렬화된 형태를 저장합니다.
- **kubernetes.io/dockerconfigjson**: `.docker/config.json` 파일의 직렬화된 형태를 저장합니다.
- **kubernetes.io/basic-auth**: 기본 인증을 위한 자격 증명을 저장합니다.
- **kubernetes.io/ssh-auth**: SSH 인증을 위한 자격 증명을 저장합니다.
- **kubernetes.io/tls**: TLS 인증서와 키를 저장합니다.
- **bootstrap.kubernetes.io/token**: 부트스트랩 토큰 데이터를 저장합니다.
### 시크릿 생성
시크릿은 여러 가지 방법으로 생성할 수 있습니다:
#### 명령형 방식
```bash
# 리터럴 값으로 생성
kubectl create secret generic my-secret --from-literal=username=admin --from-literal=password=secret
# 파일에서 생성
kubectl create secret generic my-secret --from-file=username=username.txt --from-file=password=password.txt
# TLS 시크릿 생성
kubectl create secret tls my-tls-secret --cert=path/to/cert.crt --key=path/to/key.key
# Docker 레지스트리 시크릿 생성
kubectl create secret docker-registry my-registry-secret \
--docker-server=DOCKER_REGISTRY_SERVER \
--docker-username=DOCKER_USER \
--docker-password=DOCKER_PASSWORD \
--docker-email=DOCKER_EMAIL
```
#### 선언형 방식
```yaml
apiVersion: v1
kind: Secret
metadata:
name: my-secret
type: Opaque
data:
# base64로 인코딩된 값
username: YWRtaW4= # admin
password: c2VjcmV0 # secret
```
또는 `stringData` 필드를 사용하여 인코딩되지 않은 값을 제공할 수 있습니다:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: my-secret
type: Opaque
stringData:
# 인코딩되지 않은 값
username: admin
password: secret
```
### 시크릿 사용
시크릿은 다음과 같은 방법으로 포드에서 사용할 수 있습니다:
##자체 관리형 클러스터는 `--encryption-provider-config`로 이 파일을 로드하고 암호화 키를 보호하며 기존 Secret도 다시 저장해야 합니다. `kubectl apply`로 생성하는 리소스가 아닙니다. EKS의 암호화는 서비스가 관리합니다(아래 참고).
## 환경 변수로 사용
```yaml
apiVersion: v1
kind: Pod
metadata:
name: secret-pod
spec:
containers:
- name: test-container
image: busybox
command: ["/bin/sh", "-c", 'test -n "$USERNAME" && echo "Secret available"']
env:
# 단일 키-값 쌍 사용
- name: USERNAME
valueFrom:
secretKeyRef:
name: my-secret
key: username
# 모든 키-값 쌍을 환경 변수로 사용
envFrom:
- secretRef:
name: my-secret
restartPolicy: Never
```
#### 볼륨으로 마운트
```yaml
apiVersion: v1
kind: Pod
metadata:
name: secret-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "ls /etc/secret/" ]
volumeMounts:
- name: secret-volume
mountPath: /etc/secret
volumes:
- name: secret-volume
secret:
secretName: my-secret
restartPolicy: Never
```
#### 이미지 풀 시크릿
```yaml
apiVersion: v1
kind: Pod
metadata:
name: private-image-pod
spec:
containers:
- name: private-image-container
image: private-registry.example.com/my-app:v1
imagePullSecrets:
- name: my-registry-secret
```
### 시크릿 보안 고려 사항
시크릿은 기본적으로 base64로 인코딩되어 있지만, 이는 암호화가 아닙니다. 시크릿의 보안을 강화하기 위해 다음과 같은 방법을 고려할 수 있습니다:
1. **etcd 암호화**: etcd에 저장된 시크릿을 암호화합니다.
2. **RBAC**: 시크릿에 대한 접근을 제한합니다.
3. **네트워크 정책**: 지원되는 환경에서 API·외부 저장소로의 네트워크 접근을 제한하며 Secret 객체 권한은 NetworkPolicy가 아닌 RBAC가 집행합니다.
4. **외부 시크릿 관리 도구**: AWS Secrets Manager, HashiCorp Vault 등의 외부 시크릿 관리 도구를 사용합니다.
여기 표시한 자격 증명은 교육용 더미 값입니다. 실제 값이나 base64 값을 로그·Git에 남기지 말고 리터럴 CLI 인수보다 보호된 입력 파일이나 외부 시크릿 저장소를 사용하세요. Secret을 사용하는 파드를 생성할 수 있는 사용자는 직접 Secret 읽기 권한 없이도 값을 얻을 수 있습니다.
#### etcd 암호화 구성
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret:
- identity: {}
```
자체 관리형 클러스터는 `--encryption-provider-config`로 이 파일을 로드하고 암호화 키를 보호하며 기존 Secret도 다시 저장해야 합니다. `kubectl apply`로 생성하는 리소스가 아닙니다. EKS의 암호화는 서비스가 관리합니다(아래 참고).
## 환경 변수
환경 변수는 컨테이너에 구성 정보를 전달하는 간단한 방법입니다. Kubernetes는 여러 가지 방법으로 환경 변수를 설정할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-05-configuration-secrets-3.html)
### 직접 설정
```yaml
apiVersion: v1
kind: Pod
metadata:
name: env-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "env" ]
env:
- name: ENVIRONMENT
value: "production"
- name: LOG_LEVEL
value: "INFO"
restartPolicy: Never
```
### 컨피그맵에서 설정
```yaml
apiVersion: v1
kind: Pod
metadata:
name: env-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "env" ]
env:
- name: ENVIRONMENT
valueFrom:
configMapKeyRef:
name: my-config
key: key1
restartPolicy: Never
```
### 시크릿에서 설정
```yaml
apiVersion: v1
kind: Pod
metadata:
name: env-pod
spec:
containers:
- name: test-container
image: busybox
command: ["/bin/sh", "-c", 'test -n "$DATABASE_PASSWORD" && echo "Secret available"']
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: my-secret
key: password
restartPolicy: Never
```
### 다운워드 API를 통한 설정
다운워드 API를 사용하면 포드 및 컨테이너 정보를 환경 변수로 노출할 수 있습니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: downward-api-pod
labels:
app: myapp
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "env" ]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: CONTAINER_CPU_REQUEST
valueFrom:
resourceFieldRef:
containerName: test-container
resource: requests.cpu
restartPolicy: Never
```
## 볼륨을 통한 구성 마운트
볼륨을 통해 구성 파일을 컨테이너에 마운트하는 방법은 환경 변수보다 더 유연한 구성 관리 방법을 제공합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-05-configuration-secrets-4.html)
### 컨피그맵 볼륨
```yaml
apiVersion: v1
kind: Pod
metadata:
name: configmap-volume-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "ls -la /etc/config" ]
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: my-config
restartPolicy: Never
```
### 시크릿 볼륨
```yaml
apiVersion: v1
kind: Pod
metadata:
name: secret-volume-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "ls -la /etc/secret" ]
volumeMounts:
- name: secret-volume
mountPath: /etc/secret
volumes:
- name: secret-volume
secret:
secretName: my-secret
restartPolicy: Never
```
### 특정 파일 마운트
```yaml
apiVersion: v1
kind: Pod
metadata:
name: specific-file-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "cat /etc/config/config.properties" ]
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: my-config
items:
- key: config.properties
path: config.properties
restartPolicy: Never
```
### 읽기 전용 마운트
```yaml
apiVersion: v1
kind: Pod
metadata:
name: readonly-mount-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "ls -la /etc/config" ]
volumeMounts:
- name: config-volume
mountPath: /etc/config
readOnly: true
volumes:
- name: config-volume
configMap:
name: my-config
restartPolicy: Never
```
### 서브패스 마운트
```yaml
apiVersion: v1
kind: Pod
metadata:
name: subpath-mount-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh", "-c", "cat /etc/config/config.properties" ]
volumeMounts:
- name: config-volume
mountPath: /etc/config/config.properties
subPath: config.properties
volumes:
- name: config-volume
configMap:
name: my-config
restartPolicy: Never
```
## 구성 모범 사례
Kubernetes에서 구성을 관리할 때 다음과 같은 모범 사례를 고려하세요:
### 1. 구성과 코드 분리
애플리케이션 코드와 구성을 분리하여 관리하세요. 이렇게 하면 구성을 변경할 때 애플리케이션을 다시 빌드하지 않아도 됩니다.
### 2. 환경별 구성 관리
개발, 테스트, 프로덕션 등 다양한 환경에 대한 구성을 별도로 관리하세요. 네임스페이스를 사용하여 환경을 분리하고, 환경별로 다른 컨피그맵과 시크릿을 사용할 수 있습니다.
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: my-config
namespace: development
data:
environment: development
log_level: DEBUG
---
apiVersion: v1
kind: ConfigMap
metadata:
name: my-config
namespace: production
data:
environment: production
log_level: INFO
```
### 3. 민감한 정보는 시크릿 사용
암호, API 키, 인증서 등의 민감한 정보는 항상 시크릿을 사용하여 저장하세요. 컨피그맵은 민감하지 않은 구성 데이터에만 사용하세요.
### 4. 불변성 유지
구성을 변경할 때는 새 버전을 생성하고, 기존 버전을 수정하지 마세요. 이렇게 하면 롤백이 쉬워지고, 구성 변경 이력을 추적할 수 있습니다.
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: my-config-v1
immutable: true
data:
log_level: INFO
# 구성 데이터
---
apiVersion: v1
kind: ConfigMap
metadata:
name: my-config-v2
immutable: true
data:
log_level: DEBUG
# 업데이트된 구성 데이터
```
### 5. 구성 변경 시 포드 재시작
환경 변수로 사용된 구성은 포드가 재시작되어야 업데이트됩니다. 디플로이먼트를 사용하여 포드를 롤링 업데이트하세요.
```bash
kubectl rollout restart deployment/my-deployment
```
### 6. 구성 검증
구성을 적용하기 전에 유효성을 검증하세요. 잘못된 구성은 애플리케이션 장애를 일으킬 수 있습니다.
### 7. 구성 문서화
구성 옵션과 그 영향을 문서화하세요. 이는 팀원들이 구성을 이해하고 관리하는 데 도움이 됩니다.
### 리소스 요청과 QoS
요청은 스케줄링과 실행 중 리소스 배분의 기준이며 앱이 반드시 그만큼 사용해야 한다는 뜻은 아닙니다. CPU 제한은 스로틀링하고 메모리 제한은 OOM 종료를 유발할 수 있습니다. 퀴즈의 컨테이너 수준 예시에서 Guaranteed는 모든 컨테이너의 CPU·메모리 요청이 각각 제한과 같아야 하며 BestEffort는 둘 다 없고 나머지는 Burstable입니다. 파드 수준 리소스도 QoS에 영향을 줄 수 있습니다. 노드 압력 축출에는 우선순위와 요청 대비 사용량도 반영됩니다. [리소스 관리](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)와 [파드 QoS](https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/)를 참고하세요.
## Amazon EKS에서의 구성 관리
Amazon EKS에서는 Kubernetes의 기본 구성 관리 기능 외에도 AWS의 다양한 서비스를 활용하여 구성과 시크릿을 관리할 수 있습니다. 이 섹션에서는 EKS에서 구성을 관리하는 다양한 방법과 AWS 서비스와의 통합에 대해 알아보겠습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-05-configuration-secrets-5.html)
### AWS Secrets Manager 통합
AWS Secrets Manager는 데이터베이스 자격 증명, API 키 및 기타 시크릿 정보를 안전하게 저장하고 관리할 수 있는 서비스입니다. External Secrets Operator는 Kubernetes Secret을 동기화합니다. ASCP와 Secrets Store CSI Driver는 외부 값을 파일로 마운트하며 Kubernetes Secret 동기화·자동 로테이션은 별도 선택 설정이 필요합니다.
#### External Secrets Operator 설치
```bash
# Helm을 사용하여 External Secrets Operator 설치
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
--namespace external-secrets \
--create-namespace
```
#### SecretStore 생성
```yaml
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: aws-secretsmanager
namespace: my-namespace
spec:
provider:
aws:
service: SecretsManager
region: us-west-2
auth:
jwt:
serviceAccountRef:
name: my-serviceaccount
```
#### ExternalSecret 생성
```yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: database-credentials
namespace: my-namespace
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secretsmanager
kind: SecretStore
target:
name: db-credentials
data:
- secretKey: username
remoteRef:
key: prod/db/credentials
property: username
- secretKey: password
remoteRef:
key: prod/db/credentials
property: password
```
#### IRSA(IAM Roles for Service Accounts) 설정
External Secrets Operator가 AWS Secrets Manager에 접근하려면 적절한 IAM 권한이 필요합니다. IRSA를 사용하여 Kubernetes 서비스 계정에 IAM 역할을 연결할 수 있습니다.
```bash
# OIDC 제공자 생성
eksctl utils associate-iam-oidc-provider \
--cluster my-cluster \
--approve
# IAM 역할 및 서비스 계정 생성
eksctl create iamserviceaccount \
--cluster my-cluster \
--namespace my-namespace \
--name my-serviceaccount \
--attach-policy-arn arn:aws:iam::123456789012:policy/ReadAppDatabaseSecret \
--approve
```
`ReadAppDatabaseSecret` 정책을 먼저 만들고 `secretsmanager:GetSecretValue`, `secretsmanager:DescribeSecret`을 `prod/db/credentials`의 전체 ARN으로 제한하세요. 고객 관리 키 사용 시 필요한 범위의 `kms:Decrypt`만 추가합니다. IAM 신뢰 정책은 클러스터·네임스페이스·ServiceAccount와 일치해야 합니다. ESO와 v1 CRD, 네임스페이스, ServiceAccount를 SecretStore보다 먼저 준비하세요.
### AWS Parameter Store 활용
AWS Systems Manager Parameter Store는 구성 데이터와 시크릿 값을 계층적으로 저장하고 관리할 수 있는 서비스입니다. 로테이션·수명 주기·접근 요구사항에 따라 Parameter Store와 Secrets Manager를 선택하세요. 요금은 파라미터 티어와 API 사용량에 따라 다릅니다.
#### ASCP(AWS Secrets and Configuration Provider) 설치
```bash
# ASCP 설치
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
--namespace kube-system
# AWS 제공자 설치
kubectl apply -f https://raw.githubusercontent.com/aws/secrets-store-csi-driver-provider-aws/main/deployment/aws-provider-installer.yaml
```
#### SecretProviderClass 생성
```yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: aws-parameters
namespace: my-namespace
spec:
provider: aws
parameters:
objects: |
- objectName: /my-app/config/log-level
objectType: ssmparameter
- objectName: /my-app/config/environment
objectType: ssmparameter
```
#### 포드에서 Parameter Store 값 사용
```yaml
apiVersion: v1
kind: Pod
metadata:
name: parameter-store-pod
namespace: my-namespace
spec:
serviceAccountName: parameter-reader
containers:
- name: app
image: my-app:latest
volumeMounts:
- name: parameters-store-volume
mountPath: "/mnt/parameters"
readOnly: true
volumes:
- name: parameters-store-volume
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: aws-parameters
```
`parameter-reader`에 두 파라미터 ARN의 `ssm:GetParameters` 및 필요한 범위의 KMS 복호화 권한을 가진 IRSA 역할 또는 Pod Identity 연결을 준비하세요. 앞의 Secrets Manager 역할에는 SSM 권한이 없습니다. ASCP에는 지원되는 노드·애드온 조합이 필요하며 Fargate에서는 이 CSI 마운트를 사용할 수 없습니다. Hybrid Nodes는 명시적으로 지원되는 애드온 버전과 자격 증명 구성이 필요합니다.
### AWS AppConfig를 사용한 동적 구성
AWS AppConfig는 애플리케이션 구성을 관리하고 배포하는 서비스입니다. AppConfig를 사용하면 애플리케이션을 재배포하지 않고도 구성을 동적으로 업데이트할 수 있습니다.
#### AppConfig Agent 사이드카 패턴
애플리케이션은 에이전트의 로컬 HTTP 엔드포인트에서 구성을 가져와 갱신·리로드해야 합니다. emptyDir 공유만으로 에이전트가 `/config/config.json`을 쓰지는 않습니다. 먼저 애플리케이션·환경·구성 프로필을 만들고 배포한 뒤 필요한 구성에 대한 `appconfig:StartConfigurationSession`, `appconfig:GetLatestConfiguration` 권한을 파드 ID에 부여하세요.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: my-namespace
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
serviceAccountName: appconfig-reader
containers:
- name: app
image: my-app:latest
env:
- name: CONFIG_URL
value: http://localhost:2772/applications/MyApp/environments/Production/configurations/MyConfig
- name: appconfig-agent
image: public.ecr.aws/aws-appconfig/aws-appconfig-agent:2.x
env:
- name: SERVICE_REGION
value: us-west-2
- name: POLL_INTERVAL
value: "45s"
- name: REQUEST_TIMEOUT
value: "15s"
- name: HTTP_PORT
value: "2772"
- name: HTTP_HOST
value: localhost
- name: PREFETCH_LIST
value: MyApp:Production:MyConfig
```
애플리케이션 이미지는 CONFIG_URL 조회와 사이드카 시작 중 재시도를 구현해야 합니다. 배포 전에 `appconfig-reader`와 제한된 AWS ID를 생성하고 운영에서는 검증된 에이전트 버전·다이제스트를 고정하세요. 위 값은 Lambda 확장 변수가 아닌 컨테이너 에이전트 설정입니다.
### EKS Fargate 프로파일을 사용한 구성
EKS Fargate를 사용하면 노드를 관리할 필요 없이 Kubernetes 포드를 실행할 수 있습니다. Fargate 프로파일을 사용하여 포드의 실행 환경을 구성할 수 있습니다.
Fargate 프로파일은 기본 Kubernetes 객체가 아니라 EKS API 리소스입니다. 다음 `eksctl` 구성에서 프라이빗 서브넷 ID와 실행 역할을 바꾼 후 `eksctl create fargateprofile -f fargate-profile.yaml`로 생성합니다:
```yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-cluster
region: us-west-2
fargateProfiles:
- name: my-profile
podExecutionRoleARN: arn:aws:iam::123456789012:role/my-pod-execution-role
selectors:
- namespace: my-namespace
labels:
environment: production
subnets:
- subnet-1234567890abcdef0
- subnet-0abcdef1234567890
```
### AWS KMS를 사용한 시크릿 암호화
Kubernetes 1.28 이상의 EKS는 AWS 소유 키로 Secret·ConfigMap을 포함한 [모든 Kubernetes API 데이터를 기본 봉투 암호화](https://docs.aws.amazon.com/eks/latest/userguide/envelope-encryption.html)합니다. 고객 관리 KMS 키는 선택 사항이며 암호화를 켜기 위해 반드시 필요한 것은 아닙니다.
지원되는 기존 클러스터에 고객 관리 키를 연결할 때는 `update-cluster-config`가 아닌 `associate-encryption-config`를 사용합니다. 적용 전에 키 리전·정책·권한과 연결 제약을 검토하세요. 아래 예시는 키 하나만 생성하고 반환된 ARN을 재사용합니다:
```bash
set -eu
KEY_ARN=$(aws kms create-key --region us-west-2 \
--description "EKS customer-managed encryption key" \
--query KeyMetadata.Arn --output text)
aws kms create-alias --region us-west-2 \
--alias-name alias/eks-secrets --target-key-id "$KEY_ARN"
aws eks associate-encryption-config --region us-west-2 \
--cluster-name my-cluster \
--encryption-config "resources=secrets,provider={keyArn=$KEY_ARN}"
```
반환된 업데이트 ID로 `aws eks describe-update` 완료 여부를 확인하고 `aws eks describe-cluster --name my-cluster --region us-west-2 --query cluster.encryptionConfig`로 구성을 확인하세요. 고객 관리 키 구성이 없어도 기본 암호화가 꺼진 것은 아닙니다.
### AWS IAM을 사용한 시크릿 접근 제어
IRSA(IAM Roles for Service Accounts)를 사용하여 Kubernetes 서비스 계정에 IAM 역할을 연결하면, 포드가 AWS 서비스에 안전하게 접근할 수 있습니다.
#### 서비스 계정 생성
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-service-account
namespace: my-namespace
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-iam-role
```
#### 포드에서 서비스 계정 사용
```yaml
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: my-namespace
spec:
serviceAccountName: my-service-account
containers:
- name: app
image: my-app:latest
```
### EKS 구성 모범 사례
EKS에서 구성을 관리할 때 다음과 같은 모범 사례를 고려하세요:
1. **워크로드 ID 사용**: 지원되는 컴퓨팅에서는 EKS Pod Identity 또는 IRSA로 권한을 제한하세요. Fargate 앱은 IRSA를 사용하며 파드 실행 역할은 인프라용이지 앱 자격 증명이 아닙니다.
2. **시크릿 암호화**: KMS를 사용하여 EKS 클러스터의 시크릿을 암호화하세요.
3. **외부 시크릿 관리**: 민감한 정보는 AWS Secrets Manager나 Parameter Store와 같은 외부 시크릿 관리 서비스를 사용하여 관리하세요.
4. **구성 버전 관리**: AWS AppConfig나 Parameter Store를 사용하여 구성 버전을 관리하세요.
5. **환경별 구성 분리**: 개발, 테스트, 프로덕션 환경에 대한 구성을 분리하여 관리하세요. Kubernetes 네임스페이스와 AWS 리소스 태그를 활용하세요.
6. **IAM 정책 최소화**: AWS 서비스에 접근할 때는 최소 권한 원칙을 따르세요.
7. **구성 자동화**: AWS CloudFormation, AWS CDK, Terraform 등의 도구를 사용하여 구성 관리를 자동화하세요.
### EKS 구성 관리 도구
EKS에서 구성을 관리하는 데 도움이 되는 도구들을 살펴보겠습니다:
#### AWS Controllers for Kubernetes(ACK)
ACK는 Kubernetes에서 AWS 리소스를 관리할 수 있는 도구입니다. ACK를 사용하면 Kubernetes 매니페스트를 통해 AWS 리소스를 생성하고 관리할 수 있습니다.
```yaml
apiVersion: secretsmanager.services.k8s.aws/v1alpha1
kind: Secret
metadata:
name: my-secret
annotations:
services.k8s.aws/deletion-policy: retain
spec:
name: my-secret
description: "My secret created via ACK"
recoveryWindowInDays: 30
```
ACK Secrets Manager 컨트롤러와 IAM 역할·CRD를 먼저 설치하세요. 이 매니페스트는 시크릿 컨테이너 메타데이터를 관리하며 비밀번호를 생성하거나 기본 Kubernetes Secret을 만들지 않습니다. 값은 통제된 시크릿 관리 절차로 입력하세요.
#### eksctl
eksctl은 EKS 클러스터를 생성하고 관리하는 명령줄 도구입니다. eksctl을 사용하여 클러스터 구성을 관리할 수 있습니다.
```yaml
# cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-cluster
region: us-west-2
secretsEncryption:
keyARN: arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab
```
```bash
eksctl create cluster -f cluster.yaml
```
#### AWS CDK
AWS CDK(Cloud Development Kit)는 프로그래밍 언어를 사용하여 AWS 리소스를 정의하는 도구입니다. CDK를 사용하여 EKS 클러스터와 관련 리소스를 정의할 수 있습니다.
다음 함수는 기존 CDK EKS 클러스터와 Secret 구성을 받아 해당 Secret에만 읽기 권한을 부여합니다. 네임스페이스가 이미 존재하고 클러스터에 호환 kubectl 공급자가 구성되어 있어야 합니다.
```typescript
import * as eks from 'aws-cdk-lib/aws-eks';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
export function addSecretReader(
cluster: eks.Cluster,
secret: secretsmanager.ISecret,
): eks.ServiceAccount {
const serviceAccount = cluster.addServiceAccount('SecretReader', {
name: 'my-service-account',
namespace: 'my-namespace',
});
secret.grantRead(serviceAccount);
return serviceAccount;
}
```
## 결론
이 장에서는 Kubernetes의 구성 관리 방법에 대해 알아보았습니다. 컨피그맵과 시크릿은 애플리케이션 구성을 관리하는 기본적인 방법을 제공하며, 환경 변수와 볼륨을 통해 이러한 구성을 컨테이너에 전달할 수 있습니다. 또한, 구성 관리의 모범 사례와 외부 구성 관리 도구에 대해서도 살펴보았습니다.
Amazon EKS 환경에서는 Kubernetes의 기본 구성 관리 기능과 함께 AWS의 다양한 서비스를 활용하여 더욱 강력하고 안전한 구성 관리가 가능합니다. AWS Secrets Manager, Parameter Store, KMS, IAM 등의 서비스를 통합하여 시크릿을 안전하게 관리하고, IRSA를 통해 포드에 최소 권한을 부여할 수 있습니다. 또한, AWS AppConfig를 사용하여 애플리케이션을 재배포하지 않고도 구성을 동적으로 업데이트할 수 있습니다.
효과적인 구성 관리는 Kubernetes 애플리케이션의 유지 관리성, 확장성 및 보안을 향상시키는 데 중요합니다. 애플리케이션의 요구 사항에 맞는 적절한 구성 관리 전략을 선택하고, 모범 사례를 따르는 것이 중요합니다. 특히 EKS 환경에서는 AWS 서비스와의 통합을 통해 더욱 강력한 구성 관리 솔루션을 구축할 수 있습니다.
다음 장에서는 Kubernetes의 보안에 대해 알아보겠습니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [구성 및 시크릿 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/core/05-configuration-secrets-quiz)를 풀어보세요.
## 참고 자료
- [Kubernetes 공식 문서 - ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/)
- [Kubernetes 공식 문서 - Secrets](https://kubernetes.io/docs/concepts/configuration/secret/)
- [Kubernetes 공식 문서 - Environment Variables](https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/)
- [Kubernetes 공식 문서 - Configure a Pod to Use a ConfigMap](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/)
- [Kubernetes 공식 문서 - Distribute Credentials Securely Using Secrets](https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/)
- [Helm 공식 문서](https://helm.sh/docs/)
- [Kustomize 공식 문서](https://kustomize.io/)
- [External Secrets Operator 공식 문서](https://external-secrets.io/latest/)
- [AWS Secrets Manager 공식 문서](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html)
- [AWS Systems Manager Parameter Store 공식 문서](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html)
- [AWS AppConfig 공식 문서](https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html)
- [EKS 공식 문서 - IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)
- [EKS 공식 문서 - Secrets Encryption](https://docs.aws.amazon.com/eks/latest/userguide/enable-kms.html)
- [AWS Controllers for Kubernetes(ACK) 공식 문서](https://aws-controllers-k8s.github.io/community/)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/core/06-security
----------------------------------------
# Kubernetes 보안
> **지원 버전**: Kubernetes 1.35, 1.36, 1.37
> **마지막 업데이트**: 2026년 2월 11일
Kubernetes에서 보안은 클러스터와 애플리케이션을 보호하기 위한 핵심 요소입니다. 이 장에서는 Kubernetes의 보안 개념, 인증 및 권한 부여 메커니즘, 네트워크 정책, 보안 컨텍스트, 그리고 Amazon EKS에서의 보안 강화 방법에 대해 알아보겠습니다.
## 실습 환경 설정
이 문서의 예제를 따라하기 위해서는 다음과 같은 도구와 환경이 필요합니다:
### 필수 도구
- API 서버와 마이너 버전 차이가 1 이내인 kubectl
- 작동하는 Kubernetes 클러스터 (EKS, minikube, kind 등)
- OpenSSL (인증서 생성용)
### 보안 예제 설정
```bash
# 네임스페이스 생성
kubectl create namespace security-demo
# 서비스 계정 생성
kubectl -n security-demo create serviceaccount demo-sa
# 역할 생성
kubectl -n security-demo apply -f - < **핵심 개념**: Kubernetes 보안은 다층 방어(Defense in Depth) 접근 방식을 따르며, 인프라, 클러스터, 워크로드 수준에서 여러 보안 메커니즘을 제공합니다.
Kubernetes 보안은 다음과 같은 주요 영역으로 구성됩니다:
### 보안 영역 비교
| 보안 영역 | 주요 구성 요소 | 책임자 | 보안 메커니즘 |
|----------|--------------|-------|-------------|
| **인프라 보안** | 호스트 OS, 컨테이너 런타임, 네트워크 | 클러스터 관리자 | 방화벽, OS 강화, 컨테이너 런타임 보안 |
| **클러스터 보안** | API 서버, etcd, kubelet | 클러스터 관리자 | 인증, 권한 부여, 어드미션 컨트롤, 암호화 |
| **워크로드 보안** | 파드, 컨테이너, 서비스 | 애플리케이션 개발자 | 보안 컨텍스트, 네트워크 정책, RBAC |
### 보안 원칙
1. **최소 권한 원칙**: 필요한 최소한의 권한만 부여
2. **심층 방어**: 여러 보안 계층을 통한 방어
3. **기본 거부**: 명시적으로 허용되지 않은 모든 것을 거부
4. **보안 강화**: 기본 설정보다 더 강력한 보안 설정 적용
5. **지속적인 모니터링**: 보안 이벤트 감지 및 대응
## 인증(Authentication)
Kubernetes API 서버에 접근하기 위해서는 인증 과정을 거쳐야 합니다. Kubernetes는 다양한 인증 방법을 지원합니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-06-security-1.html)
### X.509 인증서
Kubernetes는 TLS 인증서를 사용하여 클라이언트를 인증합니다. 이는 주로 클러스터 내부 통신과 관리자 인증에 사용됩니다.
```bash
# 인증서 기반 인증을 위한 kubeconfig 설정 예시
kubectl config set-credentials admin --client-certificate=admin.crt --client-key=admin.key
```
### 서비스 계정 토큰
서비스 계정은 포드 내에서 실행되는 프로세스가 API 서버와 통신할 때 사용하는 계정입니다. 현재 파드는 보통 TokenRequest API로 단기·파드 바인딩 프로젝션 토큰을 받습니다. kubelet이 토큰을 갱신하므로 앱도 토큰 파일을 다시 읽어야 합니다. v1.24부터 ServiceAccount 생성 시 장기 토큰 Secret이 자동 생성되지 않습니다. 아래 웹 서버처럼 API 자격 증명이 필요 없으면 `automountServiceAccountToken: false`를 설정하세요. 명시적인 단기 토큰은 `kubectl create token`으로 요청하며 장기 토큰 Secret은 레거시 예외입니다.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-service-account
namespace: default
```
```yaml
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
serviceAccountName: my-service-account
automountServiceAccountToken: false
containers:
- name: my-container
image: nginx:1.30.4
```
### OpenID Connect (OIDC)
외부 ID 제공자(예: Google, Microsoft Entra ID)를 통한 인증을 지원합니다. 이는 기업 환경에서 Single Sign-On(SSO)을 구현하는 데 유용합니다.
ID 제공자에 맞는 신뢰할 수 있는 client-go ExecCredential 로그인 플러그인을 설치하고 해당 로그인 절차를 완료하세요. 아래 kubeconfig 사용자 일부의 실행 파일은 자리 표시자이므로 설치한 플러그인과 문서화된 인수로 교체합니다. EKS IAM 인증은 `aws eks get-token` 등의 AWS 서명 토큰을 사용하며 IAM 자체가 일반 OIDC ID 제공자인 것은 아닙니다.
```yaml
users:
- name: oidc-user
user:
exec:
apiVersion: client.authentication.k8s.io/v1
command: oidc-login-helper
interactiveMode: IfAvailable
provideClusterInfo: true
```
### 웹훅 토큰 인증
외부 인증 서비스를 통해 토큰을 검증하는 방법입니다. API 서버는 토큰을 외부 서비스에 전달하고, 해당 서비스는 토큰의 유효성을 검증하고 사용자 정보를 반환합니다.
### 인증 프록시
API 서버 앞에 인증 프록시를 배치하여 사용자 인증을 처리하는 방법입니다. 프록시는 인증된 사용자의 정보를 HTTP 헤더에 포함하여 API 서버로 전달합니다.
## 권한 부여(Authorization)
인증이 "당신이 누구인가?"를 확인하는 과정이라면, 권한 부여는 "당신이 무엇을 할 수 있는가?"를 결정하는 과정입니다. Kubernetes는 다양한 권한 부여 모드를 지원합니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-06-security-2.html)
### RBAC(Role-Based Access Control)
RBAC는 Kubernetes에서 가장 널리 사용되는 권한 부여 메커니즘입니다. 역할(Role)과 역할 바인딩(RoleBinding)을 통해 사용자나 서비스 계정에 특정 리소스에 대한 권한을 부여합니다.
#### Role과 ClusterRole
Role은 네임스페이스 리소스이며 ClusterRole은 클러스터 범위에서 네임스페이스·클러스터 리소스 권한을 정의할 수 있습니다. 그 자체로 권한을 부여하지는 않습니다. RoleBinding은 해당 네임스페이스로 권한을 한정하고 ClusterRoleBinding은 클러스터 전체에 권한을 부여합니다.
```yaml
# 네임스페이스 내 Role 예시
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
```
```yaml
# 클러스터 전체 ClusterRole 예시
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: node-reader
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "watch", "list"]
```
#### RoleBinding과 ClusterRoleBinding
RoleBinding은 Role이나 ClusterRole을 특정 네임스페이스의 사용자, 그룹 또는 서비스 계정에 바인딩합니다. ClusterRoleBinding은 ClusterRole을 클러스터 전체의 사용자, 그룹 또는 서비스 계정에 바인딩합니다.
```yaml
# RoleBinding 예시
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
- kind: User
name: jane
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
```
```yaml
# ClusterRoleBinding 예시
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: read-nodes-global
subjects:
- kind: Group
name: node-viewers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: node-reader
apiGroup: rbac.authorization.k8s.io
```
### ABAC(Attribute-Based Access Control)
ABAC는 사용자 속성, 리소스 속성, 환경 속성 등을 기반으로 권한을 부여하는 방식입니다. Kubernetes에서는 JSON 파일을 통해 정책을 정의합니다. RBAC에 비해 유연하지만 관리가 복잡하여 덜 사용됩니다.
### Node 권한 부여
Node 권한 부여는 kubelet이 API 서버에 접근할 때 사용되는 특수한 권한 부여 모드입니다. kubelet은 자신이 실행 중인 노드에 관련된 리소스(포드, 노드 상태 등)에만 접근할 수 있습니다.
### 웹훅 권한 부여
외부 서비스를 통해 권한 부여 결정을 내리는 방식입니다. API 서버는 권한 부여 요청을 외부 서비스에 전달하고, 해당 서비스는 요청을 허용할지 거부할지 결정합니다.
## 보안 컨텍스트(Security Context)
보안 컨텍스트는 포드나 컨테이너 수준에서 보안 설정을 정의합니다. 이를 통해 권한, 액세스 제어, 기능 등을 세밀하게 제어할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-06-security-3.html)
### 포드 보안 컨텍스트
```yaml
apiVersion: v1
kind: Pod
metadata:
name: security-context-pod
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: security-context-container
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
```
위 예시에서:
- `runAsUser`: 컨테이너 프로세스가 실행될 사용자 ID
- `runAsGroup`: 컨테이너 프로세스가 실행될 그룹 ID
- `fsGroup`: 볼륨에 접근할 때 사용할 그룹 ID
- `allowPrivilegeEscalation`: 프로세스가 부모 프로세스보다 더 많은 권한을 얻을 수 있는지 여부
- `capabilities`: Linux 커널 기능을 추가하거나 제거
- `readOnlyRootFilesystem`: 루트 파일 시스템을 읽기 전용으로 마운트
### 포드 보안 표준(Pod Security Standards)
PodSecurityPolicy는 v1.25에서 제거되었습니다. v1.25에서 Stable이 된 Pod Security Admission이 네임스페이스 레이블로 Pod Security Standards를 집행할 수 있습니다. 표준은 정책 정의이며 `PodSecurityStandard` API 리소스가 아닙니다. 세 수준을 정의합니다:
1. **Privileged**: 제한 없음, 모든 권한 허용
2. **Baseline**: 알려진 권한 상승 경로 차단
3. **Restricted**: 강력하게 강화된 보안 정책
```yaml
# 네임스페이스에 포드 보안 표준 적용 예시
apiVersion: v1
kind: Namespace
metadata:
name: my-namespace
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
```
Restricted Linux 워크로드에는 `runAsNonRoot: true`, `allowPrivilegeEscalation: false`, 허용된 seccomp 프로필, capability 제거와 호스트 접근 제한 등이 필요합니다. `readOnlyRootFilesystem`은 유용한 강화 설정이지만 Restricted 자체의 필수 항목은 아닙니다. 정책 버전을 고정하려면 `*-version` 네임스페이스 레이블을 지정하세요.
## 네트워크 정책(Network Policy)
네트워크 정책은 포드 간의 통신을 제어하는 방법을 제공합니다. 기본적으로 Kubernetes 클러스터의 모든 포드는 서로 통신할 수 있지만, 네트워크 정책을 사용하면 이를 제한할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-06-security-4.html)
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-allow
namespace: default
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
```
위 예시에서:
- `app=api` 레이블이 있는 포드에 대한 네트워크 정책을 정의
- `app=frontend` 레이블이 있는 포드에서 8080 포트로의 인바운드 트래픽만 허용
- `app=database` 레이블이 있는 포드의 5432 포트로의 아웃바운드 트래픽만 허용
네트워크 정책을 사용하려면 클러스터의 네트워크 플러그인이 네트워크 정책을 지원해야 합니다. Calico, Cilium, Antrea 등의 CNI 플러그인은 네트워크 정책을 지원합니다.
이 podSelector는 `default`의 파드를 선택합니다. 정책은 허용 규칙의 합집합이므로 다른 정책이 더 많은 트래픽을 허용할 수 있으며 출발지 egress와 목적지 ingress 양쪽이 허용해야 합니다. 이 예시는 DNS를 포함하지 않으므로 Service 이름 조회가 필요하면 실제 클러스터 DNS의 TCP/UDP 53도 허용하세요.
## 시크릿 관리
Kubernetes 시크릿은 암호, API 키, 인증서 등의 민감한 정보를 저장하고 관리하는 데 사용됩니다. Secret API의 `data`는 base64를 사용하며 이 인코딩 자체는 암호화가 아닙니다. 저장 시 보호는 클러스터에 따라 다릅니다. 자체 관리형 클러스터는 암호화 설정이 필요하고 현재 EKS는 기본 봉투 암호화를 제공합니다. 두 경우 모두 RBAC와 안전한 앱 처리가 필요합니다.
### 시크릿 암호화
etcd에 저장된 시크릿을 암호화하려면 API 서버의 암호화 구성을 설정해야 합니다:
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret:
- identity: {}
```
자체 관리형 API 서버는 `--encryption-provider-config`로 이 파일을 로드해야 합니다. 키를 보호하고 기존 Secret도 다시 저장하세요. 이 파일은 kubectl로 적용할 Kubernetes 리소스가 아닙니다.
### 외부 시크릿 관리
보다 안전한 시크릿 관리를 위해 외부 시크릿 관리 시스템을 사용할 수 있습니다:
- HashiCorp Vault
- AWS Secrets Manager
- Azure Key Vault
- Google Secret Manager
- External Secrets Operator
## 이미지 보안
컨테이너 이미지 보안은 Kubernetes 보안의 중요한 부분입니다.
### 이미지 취약점 스캔
컨테이너 이미지의 취약점을 스캔하여 알려진 보안 문제를 식별하고 해결할 수 있습니다:
- Trivy
- Clair
- Anchore
- AWS ECR 스캔
- Docker Hub 스캔
### 이미지 서명 및 검증
이미지 서명을 통해 이미지의 출처와 무결성을 검증할 수 있습니다:
- Notary
- Cosign
- Portieris
- AWS Signer
- Connaisseur
### 이미지 정책
이미지 정책을 통해 신뢰할 수 있는 레지스트리에서만 이미지를 가져오도록 제한할 수 있습니다:
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ImagePolicyWebhook
configuration:
imagePolicy:
kubeConfigFile: /path/to/kubeconfig
allowTTL: 50
denyTTL: 50
retryBackoff: 500
defaultAllow: false
```
ImagePolicyWebhook에는 실행 중인 정책 백엔드와 자체 관리형 API 서버의 admission 설정이 필요하며 이 파일만으로 레지스트리 규칙이 집행되지 않습니다. EKS는 임의의 API 서버 플래그를 노출하지 않으므로 지원되는 admission 웹훅·정책 컨트롤러를 사용하세요.
## 감사(Audit)
Kubernetes 감사는 클러스터에서 발생하는 이벤트를 기록하고 분석하는 메커니즘을 제공합니다.
### 감사 정책
감사 정책은 어떤 이벤트를 기록할지 정의합니다:
```yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
resources:
- group: ""
resources: ["secrets", "serviceaccounts/token"]
- group: "authentication.k8s.io"
resources: ["tokenreviews"]
- level: Metadata
```
감사 수준:
- `None`: 이벤트를 기록하지 않음
- `Metadata`: 요청 메타데이터(사용자, 시간, 리소스 등)만 기록
- `Request`: 요청 메타데이터와 요청 본문을 기록
- `RequestResponse`: 요청 메타데이터, 요청 본문, 응답 본문을 기록
### 감사 로그 백엔드
감사 로그는 다양한 백엔드에 저장될 수 있습니다:
- 파일
- 웹훅
내장 백엔드는 파일/log와 webhook입니다. 수집기로 Elasticsearch/Loki에 전달할 수 있지만 이들은 기본 동적 audit 백엔드가 아닙니다. 위 예시는 메타데이터만 기록하므로 Secret·토큰 본문을 로그에 복사하지 않습니다. 자체 관리형 API 서버에는 정책·백엔드 설정이 필요하며 EKS 감사 로그는 컨트롤 플레인 로깅으로 활성화합니다.
## Amazon EKS 보안 강화
Amazon EKS는 Kubernetes의 기본 보안 기능 외에도 AWS의 보안 서비스와 통합하여 보안을 강화할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-06-security-5.html)
### IAM 역할 및 서비스 계정(IRSA)
IRSA(IAM Roles for Service Accounts)를 사용하면 Kubernetes 서비스 계정에 IAM 역할을 연결하여 AWS 서비스에 안전하게 접근할 수 있습니다.
```bash
# OIDC 제공자 생성
eksctl utils associate-iam-oidc-provider --cluster my-cluster --approve
# IAM 역할 생성 및 서비스 계정 연결
eksctl create iamserviceaccount \
--name my-service-account \
--namespace default \
--cluster my-cluster \
--attach-policy-arn arn:aws:iam::123456789012:policy/ReadApplicationBucket \
--approve
```
필요한 버킷·접두사에만 `s3:GetObject`를 허용하는 `ReadApplicationBucket` 정책을 만들고 목록 조회가 필요할 때만 `s3:ListBucket`을 추가하세요. 광범위한 관리형 정책으로 모든 버킷 권한을 주지 마세요. 지원되는 컴퓨팅에서는 EKS Pod Identity도 가능하며 Fargate 앱은 IRSA를 사용합니다.
### AWS KMS를 사용한 시크릿 암호화
EKS 1.28+는 AWS 소유 KMS 키로 모든 Kubernetes API 데이터를 기본 암호화합니다. 고객 관리 키는 선택 사항입니다. 올바른 연결 예시는 [구성 장](https://www.atomai.click/kubernetes-docs/llms/ko/core/05-configuration-secrets.md#aws-kms를-사용한-시크릿-암호화)을 참고하고 API의 base64 표현과 저장 시 암호화를 구분하세요.
### AWS Security Groups
EKS 클러스터의 노드와 포드에 AWS 보안 그룹을 적용하여 네트워크 트래픽을 제어할 수 있습니다.
```bash
# 보안 그룹 생성
SECURITY_GROUP_ID=$(aws ec2 create-security-group \
--vpc-id vpc-0123456789abcdef0 \
--group-name eks-client-access --description "EKS client access example" \
--query GroupId --output text)
# 인바운드 규칙 추가
aws ec2 authorize-security-group-ingress \
--group-id "$SECURITY_GROUP_ID" \
--protocol tcp \
--port 443 \
--cidr 10.0.0.0/16
```
환경에 맞는 VPC/CIDR로 바꾸고 해당 보안 그룹을 대상 리소스에 연결해야 합니다. 그룹 생성만으로 기존 노드·파드를 보호하지 않습니다. 파드 보안 그룹에는 지원되는 VPC CNI 설정과 SecurityGroupPolicy가 추가로 필요합니다.
### AWS WAF
AWS WAF는 연결된 ALB 또는 CloudFront를 통해 HTTP(S) 앱 트래픽을 보호하며 EKS API 서버·파드·NLB에 직접 연결하지 않습니다. 기본 동작이 `Allow`이고 규칙이 없는 Web ACL은 아무것도 차단하지 않습니다. 규칙을 구성·테스트한 후 같은 리전의 앱 ALB에 regional ACL을 연결합니다:
```bash
aws wafv2 associate-web-acl \
--web-acl-arn "$WEB_ACL_ARN" \
--resource-arn "$APPLICATION_ALB_ARN"
```
### AWS GuardDuty
AWS GuardDuty를 사용하여 EKS 클러스터의 보안 위협을 탐지하고 대응할 수 있습니다.
먼저 대상 계정·리전의 detector를 확인하세요. EKS 감사 로그 분석(`EKS_AUDIT_LOGS`)과 Runtime Monitoring(`RUNTIME_MONITORING`)은 별도 기능입니다. Runtime Monitoring은 지원 노드의 에이전트 적용도 필요하며 자동 EKS 에이전트 관리는 `EKS_ADDON_MANAGEMENT`를 사용합니다. 기존 `EKS_RUNTIME_MONITORING` 사용자는 두 런타임 기능을 동시에 켜지 말고 마이그레이션 절차를 따라야 합니다.
```bash
aws guardduty list-detectors
aws guardduty get-detector --detector-id "$DETECTOR_ID"
```
반환된 ID로 `DETECTOR_ID`를 설정하고 [Runtime Monitoring 구성](https://docs.aws.amazon.com/guardduty/latest/ug/runtime-monitoring-configuration.html)을 따른 뒤 적용 범위를 확인하세요. GuardDuty는 탐지 결과를 생성하며 자동 대응에는 별도로 구성한 워크플로가 필요합니다.
## 보안 모범 사례
Kubernetes 클러스터와 워크로드의 보안을 강화하기 위한 모범 사례를 소개합니다.
### 클러스터 보안
1. **최신 버전 유지**: Kubernetes와 모든 컴포넌트를 최신 버전으로 유지하여 알려진 취약점을 패치합니다.
2. **API 서버 접근 제한**: API 서버에 대한 접근을 제한하고, 필요한 경우에만 공개 접근을 허용합니다.
3. **etcd 암호화**: etcd에 저장된 데이터를 암호화하여 민감한 정보를 보호합니다.
4. **감사 로깅 활성화**: 클러스터 활동을 모니터링하고 분석하기 위해 감사 로깅을 활성화합니다.
5. **네트워크 정책 구현**: 포드 간 통신을 제한하기 위해 네트워크 정책을 구현합니다.
### 워크로드 보안
1. **최소 권한 원칙**: 포드와 컨테이너에 필요한 최소한의 권한만 부여합니다.
2. **비루트 사용자**: 컨테이너를 비루트 사용자로 실행합니다.
3. **읽기 전용 파일 시스템**: 가능한 경우 컨테이너의 루트 파일 시스템을 읽기 전용으로 마운트합니다.
4. **리소스 제한**: CPU와 메모리 리소스 제한을 설정하여 DoS 공격을 방지합니다.
5. **보안 컨텍스트 구성**: 포드와 컨테이너의 보안 컨텍스트를 적절히 구성합니다.
### 이미지 보안
1. **최소 베이스 이미지**: 최소한의 패키지만 포함된 베이스 이미지를 사용합니다.
2. **이미지 취약점 스캔**: 컨테이너 이미지의 취약점을 정기적으로 스캔합니다.
3. **이미지 서명 및 검증**: 이미지 서명을 통해 이미지의 출처와 무결성을 검증합니다.
4. **신뢰할 수 있는 레지스트리**: 신뢰할 수 있는 레지스트리에서만 이미지를 가져옵니다.
5. **최신 이미지 사용**: 이미지를 정기적으로 업데이트하여 알려진 취약점을 패치합니다.
#이 podSelector는 `default`의 파드를 선택합니다. 정책은 허용 규칙의 합집합이므로 다른 정책이 더 많은 트래픽을 허용할 수 있으며 출발지 egress와 목적지 ingress 양쪽이 허용해야 합니다. 이 예시는 DNS를 포함하지 않으므로 Service 이름 조회가 필요하면 실제 클러스터 DNS의 TCP/UDP 53도 허용하세요.
## 시크릿 관리
1. **외부 시크릿 관리**: 외부 시크릿 관리 시스템을 사용하여 시크릿을 안전하게 관리합니다.
2. **시크릿 암호화**: etcd에 저장된 시크릿을 암호화합니다.
3. **시크릿 순환**: 시크릿을 정기적으로 순환하여 보안을 강화합니다.
4. **최소 권한 접근**: 시크릿에 대한 접근을 필요한 포드로만 제한합니다.
5. **환경 변수 대신 볼륨 사용**: 환경 변수 대신 볼륨을 통해 시크릿을 마운트합니다.
## 결론
Kubernetes 보안은 여러 계층에서 구현되어야 하며, 클러스터 인프라, Kubernetes 컴포넌트, 애플리케이션 워크로드 등 모든 영역에서 보안을 고려해야 합니다. 인증, 권한 부여, 네트워크 정책, 보안 컨텍스트 등의 Kubernetes 기본 보안 기능과 함께, 이미지 보안, 시크릿 관리, 감사 로깅 등의 추가적인 보안 조치를 통해 클러스터와 워크로드의 보안을 강화할 수 있습니다.
Amazon EKS를 사용하는 경우, AWS의 다양한 보안 서비스와 통합하여 보안을 더욱 강화할 수 있습니다. IAM 역할 및 서비스 계정(IRSA), AWS KMS를 사용한 시크릿 암호화, AWS Security Groups, AWS WAF, AWS GuardDuty 등의 서비스를 활용하여 EKS 클러스터의 보안을 향상시킬 수 있습니다.
보안은 지속적인 과정이므로, 정기적인 보안 평가와 업데이트를 통해 클러스터와 워크로드의 보안 상태를 유지하는 것이 중요합니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [보안 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/core/06-security-quiz)를 풀어보세요.
## 참고 자료
- [Kubernetes 공식 문서 - 보안](https://kubernetes.io/docs/concepts/security/)
- [Kubernetes 공식 문서 - 인증](https://kubernetes.io/docs/reference/access-authn-authz/authentication/)
- [Kubernetes 공식 문서 - 권한 부여](https://kubernetes.io/docs/reference/access-authn-authz/authorization/)
- [Kubernetes 공식 문서 - RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
- [Kubernetes 공식 문서 - 네트워크 정책](https://kubernetes.io/docs/concepts/services-networking/network-policies/)
- [Kubernetes 공식 문서 - 보안 컨텍스트](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/)
- [Kubernetes 공식 문서 - 포드 보안 표준](https://kubernetes.io/docs/concepts/security/pod-security-standards/)
- [Kubernetes 공식 문서 - 시크릿](https://kubernetes.io/docs/concepts/configuration/secret/)
- [Kubernetes 공식 문서 - 감사](https://kubernetes.io/docs/tasks/debug-application-cluster/audit/)
- [Amazon EKS 공식 문서 - 보안](https://docs.aws.amazon.com/eks/latest/userguide/security.html)
- [Amazon EKS 공식 문서 - IAM 역할 및 서비스 계정](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)
- [Amazon EKS 공식 문서 - 시크릿 암호화](https://docs.aws.amazon.com/eks/latest/userguide/enable-kms.html)
- [Amazon EKS 보안 모범 사례](https://docs.aws.amazon.com/eks/latest/best-practices/security.html)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/core/07-policies
----------------------------------------
# Kubernetes 정책
> **지원 버전**: Kubernetes 1.35 - 1.37
> **마지막 업데이트**: 2026년 2월 22일
Kubernetes에서 정책은 클러스터와 워크로드의 동작을 제어하고 규제하는 규칙 집합입니다. 정책을 통해 보안, 리소스 사용, 네트워크 통신 등 다양한 측면을 관리할 수 있습니다. 이 장에서는 Kubernetes의 다양한 정책 유형과 이를 구현하는 방법, 그리고 Amazon EKS에서의 정책 관리에 대해 알아보겠습니다.
## 실습 환경 설정
이 문서의 예제를 따라하기 위해서는 다음과 같은 도구와 환경이 필요합니다:
### 필수 도구
- API 서버와 마이너 버전 차이가 1 이내인 kubectl
- 작동하는 Kubernetes 클러스터 (EKS, minikube, kind 등)
- Kyverno CLI (선택 사항)
- OPA Gatekeeper (선택 사항)
### 정책 예제 설정
```bash
# 네임스페이스 생성
kubectl create namespace policy-demo
# 리소스 쿼터 생성
kubectl -n policy-demo apply -f - < 0
msg := sprintf("missing required labels: %v", [missing])
}
```
```yaml
# Constraint 예시
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabelKeys
metadata:
name: require-app-label
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
labels: ["app", "owner"]
```
### Kyverno
Kyverno는 Kubernetes 네이티브 정책 엔진으로, YAML 기반의 정책을 사용하여 Kubernetes 리소스를 검증, 변경, 생성할 수 있습니다. Rego 언어를 배울 필요 없이 Kubernetes 리소스와 유사한 구문으로 정책을 작성할 수 있습니다.
```yaml
# Kyverno 정책 예시
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
spec:
rules:
- name: check-for-labels
match:
any:
- resources:
kinds:
- Pod
validate:
failureAction: Enforce
message: "The labels 'app' and 'owner' are required."
pattern:
metadata:
labels:
app: "?*"
owner: "?*"
```
Kyverno는 다음과 같은 정책 유형을 지원합니다:
1. **Validate**: 리소스가 특정 조건을 충족하는지 검증
2. **Mutate**: 리소스를 자동으로 수정
3. **Generate**: 리소스가 생성될 때 다른 리소스를 자동으로 생성
4. **Verify Images**: 이미지 서명을 검증
5. **Clean Up**: CleanupPolicy/ClusterCleanupPolicy로 조건에 맞는 리소스를 일정에 따라 삭제하며 소유자 참조 기반 가비지 수집과는 별개
### Kubewarden
Kubewarden은 WebAssembly 기반의 정책 엔진으로, 다양한 프로그래밍 언어로 정책을 작성할 수 있습니다. 정책은 WebAssembly 모듈로 컴파일되어 Kubewarden 정책 서버에서 실행됩니다.
```yaml
# Kubewarden 정책 예시
apiVersion: policies.kubewarden.io/v1
kind: ClusterAdmissionPolicy
metadata:
name: require-labels
spec:
module: "registry://ghcr.io/kubewarden/policies/safe-labels:"
mutating: false
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
operations:
- CREATE
- UPDATE
settings:
mandatory_labels:
- app
- owner
```
위 리소스보다 정책 엔진·CRD·정책 서버를 먼저 설치하세요. Gatekeeper 예시는 더 풍부한 라이브러리 `K8sRequiredLabels`와 충돌하지 않도록 `K8sRequiredLabelKeys`를 정의합니다. Kubewarden은 실제 배포된 검증 버전·다이제스트의 safe-labels와 `mandatory_labels` 설정을 사용하며 ``는 자리 표시자입니다.
## Amazon EKS에서의 정책 관리
Amazon EKS에서는 Kubernetes의 기본 정책 메커니즘과 함께 AWS의 다양한 서비스를 활용하여 정책을 관리할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-07-policies-6.html)
### AWS IAM과의 통합
Amazon EKS는 IAM 역할 및 서비스 계정(IRSA)을 통해 포드에 AWS 서비스에 대한 권한을 부여할 수 있습니다. 이를 통해 최소 권한 원칙을 적용할 수 있습니다.
```bash
# OIDC 제공자 생성
eksctl utils associate-iam-oidc-provider --cluster my-cluster --approve
# IAM 역할 생성 및 서비스 계정 연결
eksctl create iamserviceaccount \
--name my-service-account \
--namespace default \
--cluster my-cluster \
--attach-policy-arn arn:aws:iam::123456789012:policy/ReadApplicationBucket \
--approve
```
참조한 고객 관리 정책을 먼저 생성하고 필요한 버킷·접두사 읽기로 제한하세요. 지원 컴퓨팅에서는 EKS Pod Identity도 사용할 수 있습니다. IAM은 AWS API 접근을 제어하며 Kubernetes 리소스 인가와 구분됩니다.
### AWS Security Groups for Pods
Amazon EKS는 포드 수준에서 AWS 보안 그룹을 적용할 수 있는 기능을 제공합니다. 이를 통해 포드 간의 통신을 더 세밀하게 제어할 수 있습니다.
```yaml
apiVersion: vpcresources.k8s.aws/v1beta1
kind: SecurityGroupPolicy
metadata:
name: allow-db-access
namespace: default
spec:
podSelector:
matchLabels:
app: web
securityGroups:
groupIds:
- sg-0123456789abcdef0
```
클러스터 VPC의 실제 보안 그룹 ID로 바꾸고 앱·DNS에 필요한 규칙을 설정하세요. 파드 보안 그룹에는 지원되는 VPC CNI 설정·IAM 권한·호환 컴퓨팅이 필요하며 Windows·EKS Auto Mode는 지원하지 않습니다.
### AWS Config 및 AWS Organizations
AWS Config는 리소스 준수 여부를 평가하며 자체적으로 CreateCluster를 거부하지 않습니다. Organizations 서비스 제어 정책(SCP)은 적용되는 멤버 계정·OU에서 필수 요청 태그가 없는 생성을 거부할 수 있습니다:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "eks:CreateCluster",
"Resource": "*",
"Condition": {
"Null": {
"aws:RequestTag/Environment": "true"
}
}
}
]
}
```
### AWS Firewall Manager
Firewall Manager는 WAF, VPC 보안 그룹, Network Firewall, DNS Firewall 등의 AWS 보호 기능을 중앙 관리합니다. Kubernetes NetworkPolicy 객체를 동기화하는 서비스는 아니므로 Kubernetes·GitOps 정책 도구로 별도 관리하세요.
## 정책 모범 사례
Kubernetes 클러스터에서 정책을 효과적으로 관리하기 위한 모범 사례를 소개합니다.
### 정책 설계
1. **최소 권한 원칙**: 필요한 최소한의 권한만 부여하는 정책을 설계합니다.
2. **점진적 적용**: 정책을 한 번에 모두 적용하지 말고, 점진적으로 적용하여 영향을 최소화합니다.
3. **감사 모드**: 정책을 적용하기 전에 감사 모드에서 실행하여 영향을 평가합니다.
4. **명확한 문서화**: 각 정책의 목적과 영향을 명확하게 문서화합니다.
### 리소스 관리
1. **네임스페이스 분리**: 팀이나 프로젝트별로 네임스페이스를 분리하고, 각 네임스페이스에 적절한 리소스 쿼터를 설정합니다.
2. **기본 제한 설정**: LimitRange를 사용하여 모든 컨테이너에 기본 리소스 제한을 설정합니다.
3. **QoS 클래스 고려**: 워크로드의 중요도에 따라 적절한 QoS 클래스를 설정합니다.
### 네트워크 보안
1. **기본 거부 정책**: 기본적으로 모든 트래픽을 거부하고, 필요한 통신만 명시적으로 허용하는 정책을 설정합니다.
2. **세분화된 정책**: 포드 간의 통신을 세밀하게 제어하는 네트워크 정책을 설정합니다.
3. **정기적인 검토**: 네트워크 정책을 정기적으로 검토하고 업데이트합니다.
### 정책 자동화
1. **CI/CD 통합**: 정책 검증을 CI/CD 파이프라인에 통합하여 배포 전에 정책 위반을 감지합니다.
2. **정책 테스트**: 정책을 테스트 환경에서 먼저 테스트하고, 문제가 없을 때 프로덕션 환경에 적용합니다.
3. **정책 버전 관리**: 정책을 코드로 관리하고, 버전 관리 시스템을 사용하여 변경 사항을 추적합니다.
## 결론
Kubernetes 정책은 클러스터와 워크로드의 보안, 리소스 사용, 네트워크 통신 등을 제어하는 강력한 도구입니다. 기본 제공 정책 메커니즘(ResourceQuota, LimitRange, NetworkPolicy 등)과 타사 정책 엔진(OPA Gatekeeper, Kyverno 등)을 조합하여 조직의 요구 사항에 맞는 정책 프레임워크를 구축할 수 있습니다.
Amazon EKS를 사용하는 경우, AWS의 다양한 서비스(IAM, Security Groups, AWS Config, AWS Organizations, AWS Firewall Manager 등)를 활용하여 정책 관리를 더욱 강화할 수 있습니다. 이러한 서비스를 통합하여 클러스터와 워크로드의 보안, 규정 준수, 리소스 관리를 효과적으로 수행할 수 있습니다.
정책은 지속적으로 발전하는 영역이므로, 새로운 위협과 요구 사항에 대응하기 위해 정기적으로 정책을 검토하고 업데이트하는 것이 중요합니다. 또한, 정책을 코드로 관리하고 자동화하여 일관성과 효율성을 높이는 것이 좋습니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [정책 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/core/07-policies-quiz)를 풀어보세요.
## 참고 자료
- [Kubernetes 공식 문서 - 리소스 쿼터](https://kubernetes.io/docs/concepts/policy/resource-quotas/)
- [Kubernetes 공식 문서 - LimitRange](https://kubernetes.io/docs/concepts/policy/limit-range/)
- [Kubernetes 공식 문서 - 네트워크 정책](https://kubernetes.io/docs/concepts/services-networking/network-policies/)
- [Kubernetes 공식 문서 - 포드 보안 표준](https://kubernetes.io/docs/concepts/security/pod-security-standards/)
- [Kubernetes 공식 문서 - 포드 보안 어드미션](https://kubernetes.io/docs/concepts/security/pod-security-admission/)
- [OPA Gatekeeper 공식 문서](https://open-policy-agent.github.io/gatekeeper/website/docs/)
- [Kyverno 공식 문서](https://kyverno.io/docs/)
- [Kubewarden 공식 문서](https://docs.kubewarden.io/)
- [Amazon EKS 공식 문서 - IAM 역할 및 서비스 계정](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)
- [Amazon EKS 공식 문서 - 포드용 보안 그룹](https://docs.aws.amazon.com/eks/latest/userguide/security-groups-for-pods.html)
- [AWS Config 공식 문서](https://docs.aws.amazon.com/config/latest/developerguide/WhatIsConfig.html)
- [AWS Organizations 공식 문서](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_introduction.html)
- [AWS Firewall Manager 공식 문서](https://docs.aws.amazon.com/waf/latest/developerguide/fms-chapter.html)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/core/08-scheduling-preemption-eviction
----------------------------------------
# Kubernetes 스케줄링, 선점 및 축출
> **지원 버전**: Kubernetes 1.34 - 1.36 (Descheduler v0.36 예시)
> **마지막 업데이트**: 2026년 9월 9일
Kubernetes에서 스케줄링은 포드를 적절한 노드에 배치하는 과정입니다. 선점은 우선순위가 높은 포드를 위해 우선순위가 낮은 포드를 제거하는 과정이며, 축출은 파드를 종료하며 워크로드 컨트롤러가 생성한 대체 파드를 스케줄러가 별도로 배치할 수 있습니다. 이 장에서는 Kubernetes의 스케줄링 메커니즘, 노드 선택, 선점, 축출 등의 개념과 Amazon EKS에서의 스케줄링 최적화 방법에 대해 알아보겠습니다.
## 실습 환경 설정
이 문서의 예제를 따라하기 위해서는 다음과 같은 도구와 환경이 필요합니다:
### 필수 도구
- API 서버와 마이너 버전 차이가 1 이내인 kubectl
- 작동하는 Kubernetes 클러스터 (EKS, minikube, kind 등)
- 여러 노드가 있는 클러스터 (스케줄링 테스트용)
### 스케줄링 예제 설정
```bash
# 네임스페이스 생성
kubectl create namespace scheduling-demo
# 노드에 레이블 추가 (여러 노드가 있는 경우)
kubectl label nodes disktype=ssd
kubectl label nodes gpu=true
# 노드 어피니티를 사용하는 파드 생성
kubectl -n scheduling-demo apply -f - < **핵심 개념**: Kubernetes 스케줄러는 포드를 실행할 최적의 노드를 선택하는 컨트롤 플레인 컴포넌트로, 필터링과 스코어링 두 단계로 작동합니다.
### 스케줄링 프로세스
1. **필터링 단계 (Predicates)**
- 포드를 실행할 수 있는 적합한 노드 집합 식별
- 리소스 요구사항, 노드 셀렉터, 어피니티 규칙, 테인트/톨러레이션 등 고려
- 하나의 조건이라도 충족하지 못하면 노드 제외
2. **스코어링 단계 (Priorities)**
- 필터링을 통과한 노드에 점수 부여
- 리소스 사용률, 포드 간 분산, 어피니티 선호도 등 고려
- 가장 높은 점수를 받은 노드 선택
3. **바인딩 단계**
- 선택된 노드에 포드 할당
- API 서버에 바인딩 정보 업데이트
## 목차
1. [스케줄링 개요](#스케줄링-개요)
2. [스케줄러 작동 방식](#스케줄러-작동-방식)
3. [노드 선택](#노드-선택)
4. [포드 어피니티와 안티-어피니티](#포드-어피니티와-안티-어피니티)
5. [테인트와 톨러레이션](#테인트와-톨러레이션)
6. [노드 어피니티](#노드-어피니티)
7. [포드 우선순위와 선점](#포드-우선순위와-선점)
8. [포드 축출](#포드-축출)
9. [포드 중단 예산(PDB)](#포드-중단-예산pdb)
10. [노드 압력 축출](#노드-압력-축출)
11. [토폴로지 분배 제약 조건(TopologySpreadConstraints)](#토폴로지-분배-제약-조건topologyspreadconstraints)
12. [Pod Deletion Cost](#pod-deletion-cost)
13. [Descheduler](#descheduler)
14. [Amazon EKS에서의 스케줄링 최적화](#amazon-eks에서의-스케줄링-최적화)
15. [스케줄링 모범 사례](#스케줄링-모범-사례)
16. [결론](#결론)
## 스케줄링 개요
Kubernetes 스케줄러는 포드를 적절한 노드에 배치하는 컨트롤 플레인 컴포넌트입니다. 스케줄러는 다양한 요소를 고려하여 포드를 배치할 최적의 노드를 결정합니다:
1. **리소스 요구 사항**: 포드가 요청한 CPU, 메모리 등의 리소스
2. **하드웨어/소프트웨어/정책 제약 조건**: 노드 셀렉터, 노드 어피니티, 테인트 등
3. **어피니티/안티-어피니티 명세**: 다른 포드와의 배치 관계
4. **데이터 지역성**: 데이터에 가까운 곳에 포드 배치
5. **워크로드 간 간섭**: 다양한 워크로드 간의 간섭 최소화
6. **사용자 정의 목표**: 앱 데드라인·워크로드 간 간섭을 인식하는 배치는 별도 로직이 필요하며 기본 스케줄러가 앱 데드라인을 추론하지는 않음
### 스케줄링 프로세스
스케줄링 프로세스는 크게 두 단계로 나뉩니다:
1. **필터링(Filtering)**: 포드를 실행할 수 있는 노드 집합을 식별
- 리소스 요구 사항 충족 여부 확인
- 노드 셀렉터, 어피니티, 테인트 등의 제약 조건 확인
2. **스코어링(Scoring)**: 필터링된 노드에 점수를 매겨 최적의 노드 선택
- 리소스 사용률 균형
- 포드 간 어피니티/안티-어피니티
- 데이터 지역성
- 테인트/톨러레이션
## 스케줄러 작동 방식
Kubernetes 스케줄러는 다음과 같은 과정으로 작동합니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-08-scheduling-preemption-eviction-1.html)
1. **포드 큐 감시**: 스케줄러는 API 서버를 감시하여 스케줄링되지 않은 포드를 찾습니다.
2. **노드 필터링**: 포드를 실행할 수 있는 노드 집합을 식별합니다.
3. **노드 스코어링**: 필터링된 노드에 점수를 매깁니다.
4. **노드 선택**: 가장 높은 점수를 받은 노드를 선택합니다.
5. **바인딩**: 선택된 노드에 포드를 바인딩합니다.
### 스케줄링 플러그인
Kubernetes 스케줄러는 플러그인 아키텍처를 사용하여 확장 가능하게 설계되었습니다. 다양한 플러그인이 스케줄링 프로세스의 여러 단계에서 작동합니다:
1. **필터 플러그인**: 포드를 실행할 수 없는 노드를 필터링
- NodeResourcesFit: 노드의 리소스 용량 확인
- NodeName: 포드의 nodeName 필드 확인
- NodeUnschedulable: 노드의 스케줄 가능 여부 확인
- TaintToleration: 테인트와 톨러레이션 확인
2. **스코어 플러그인**: 노드에 점수 부여
- NodeResourcesBalancedAllocation: 리소스 사용 균형 고려
- ImageLocality: 이미지 지역성 고려
- InterPodAffinity: 포드 간 어피니티 고려
- NodeAffinity: 노드 어피니티 고려
### 다중 스케줄러
Kubernetes는 여러 스케줄러를 동시에 실행할 수 있습니다. 이를 통해 특정 워크로드에 대해 사용자 정의 스케줄링 로직을 구현할 수 있습니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: custom-scheduled-pod
spec:
schedulerName: my-custom-scheduler
containers:
- name: container
image: nginx
```
위 예시에서 `schedulerName` 필드를 사용하여 포드를 스케줄링할 스케줄러를 지정합니다.
## 노드 선택
Kubernetes는 포드를 특정 노드에 배치하기 위한 여러 메커니즘을 제공합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-08-scheduling-preemption-eviction-2.html)
### 노드 셀렉터(Node Selector)
노드 셀렉터는 포드를 특정 레이블이 있는 노드에만 배치하도록 제한하는 가장 간단한 방법입니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
nodeSelector:
gpu: "true"
containers:
- name: gpu-container
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
limits:
nvidia.com/gpu: 1
```
위 예시에서 포드는 `gpu=true` 레이블이 있는 노드에만 배치됩니다.
GPU 예시는 스케줄링 확인용입니다. 실제 GPU와 `nvidia.com/gpu`를 광고하는 동작 중인 장치 플러그인이 필요하며 `gpu=true` 레이블만으로 GPU를 할당하지는 않습니다.
### nodeName
`nodeName` 필드를 사용하여 포드를 특정 노드에 직접 배치할 수 있습니다. 이 방법은 스케줄러를 우회하므로 일반적으로 권장되지 않습니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: specific-node-pod
spec:
nodeName: worker-node-1
containers:
- name: container
image: nginx
```
위 예시에서 포드는 `worker-node-1`이라는 이름의 노드에 직접 배치됩니다.
## 포드 어피니티와 안티-어피니티
포드 어피니티와 안티-어피니티는 포드 간의 관계를 기반으로 포드를 배치하는 방법을 제공합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-08-scheduling-preemption-eviction-3.html)
### 포드 어피니티(Pod Affinity)
포드 어피니티는 특정 레이블을 가진 포드와 같은 노드 또는 토폴로지 도메인에 포드를 배치하도록 합니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: frontend
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cache
topologyKey: kubernetes.io/hostname
containers:
- name: frontend
image: nginx
```
위 예시에서 `frontend` 포드는 `app=cache` 레이블이 있는 포드와 같은 호스트에 배치됩니다.
### 포드 안티-어피니티(Pod Anti-Affinity)
포드 안티-어피니티는 특정 레이블을 가진 포드와 다른 노드 또는 토폴로지 도메인에 포드를 배치하도록 합니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: frontend
labels:
app: frontend
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- frontend
topologyKey: kubernetes.io/hostname
containers:
- name: frontend
image: nginx
```
위 예시에서 `frontend` 포드는 다른 `app=frontend` 레이블이 있는 포드와 다른 호스트에 배치됩니다. 이는 고가용성을 위해 같은 애플리케이션의 인스턴스를 여러 노드에 분산시키는 데 유용합니다.
### 어피니티 유형
포드 어피니티와 안티-어피니티는 두 가지 유형이 있습니다:
1. **requiredDuringSchedulingIgnoredDuringExecution**: 스케줄링 시 반드시 충족해야 하는 하드 요구 사항
2. **preferredDuringSchedulingIgnoredDuringExecution**: 가능하면 충족하는 것이 좋지만, 필수는 아닌 소프트 요구 사항
```yaml
# preferredDuringSchedulingIgnoredDuringExecution 예시
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cache
topologyKey: kubernetes.io/hostname
```
위 예시에서 `weight` 필드는 이 선호도의 가중치를 나타냅니다. 여러 선호도가 있을 경우 가중치가 높은 선호도가 더 중요하게 고려됩니다.
## 테인트와 톨러레이션
테인트(Taint)와 톨러레이션(Toleration)은 노드가 특정 포드를 거부할 수 있게 하는 메커니즘입니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-08-scheduling-preemption-eviction-4.html)
### 테인트(Taint)
테인트는 노드에 적용되어 포드가 해당 노드에 스케줄링되는 것을 제한합니다.
```bash
# 노드에 테인트 추가
kubectl taint nodes node1 key=value:NoSchedule
```
테인트 효과(Effect)는 세 가지가 있습니다:
1. **NoSchedule**: 톨러레이션이 없는 포드는 노드에 스케줄링되지 않음
2. **PreferNoSchedule**: 가능하면 톨러레이션이 없는 포드를 노드에 스케줄링하지 않음
3. **NoExecute**: 톨러레이션이 없는 포드는 노드에서 축출됨
### 톨러레이션(Toleration)
톨러레이션은 포드에 적용되어 테인트가 있는 노드에 스케줄링될 수 있게 합니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
tolerations:
- key: "key"
operator: "Equal"
value: "value"
effect: "NoSchedule"
containers:
- name: nginx
image: nginx
```
위 예시에서 포드는 `key=value:NoSchedule` 테인트가 있는 노드에 스케줄링될 수 있습니다.
### 사용 사례
테인트와 톨러레이션의 일반적인 사용 사례는 다음과 같습니다:
1. **전용 노드**: 특정 워크로드만 실행할 노드 지정
2. **특수 하드웨어**: GPU와 같은 특수 하드웨어가 있는 노드 관리
3. **노드 유지 관리**: 유지 관리 중인 노드에서 새 포드 스케줄링 방지
4. **노드 문제**: 문제가 있는 노드에서 포드 축출
### 기본 테인트
Kubernetes는 일부 노드에 기본 테인트를 적용합니다:
- **node.kubernetes.io/not-ready**: 노드가 준비되지 않음
- **node.kubernetes.io/unreachable**: 노드에 도달할 수 없음
- **node.kubernetes.io/memory-pressure**: 노드에 메모리 압력이 있음
- **node.kubernetes.io/disk-pressure**: 노드에 디스크 압력이 있음
- **node.kubernetes.io/pid-pressure**: 노드에 PID 압력이 있음
- **node.kubernetes.io/network-unavailable**: 노드의 네트워크가 사용 불가능함
- **node.kubernetes.io/unschedulable**: 노드가 스케줄 불가능함
## 노드 어피니티
노드 어피니티는 포드를 특정 노드 집합에 배치하는 보다 표현력이 풍부한 방법을 제공합니다. 노드 셀렉터보다 더 복잡한 조건을 지정할 수 있습니다.
### 노드 어피니티 유형
노드 어피니티는 두 가지 유형이 있습니다:
1. **requiredDuringSchedulingIgnoredDuringExecution**: 스케줄링 시 반드시 충족해야 하는 하드 요구 사항
2. **preferredDuringSchedulingIgnoredDuringExecution**: 가능하면 충족하는 것이 좋지만, 필수는 아닌 소프트 요구 사항
```yaml
apiVersion: v1
kind: Pod
metadata:
name: with-node-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-west-2a
- us-west-2b
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: another-node-label-key
operator: In
values:
- another-node-label-value
containers:
- name: with-node-affinity
image: nginx
```
위 예시에서 포드는 `topology.kubernetes.io/zone` 레이블이 `us-west-2a` 또는 `us-west-2b`인 노드에만 배치됩니다. 또한 가능하면 `another-node-label-key=another-node-label-value` 레이블이 있는 노드에 배치됩니다.
### 연산자
노드 어피니티는 다양한 연산자를 지원합니다:
- **In**: 레이블 값이 지정된 값 중 하나와 일치
- **NotIn**: 레이블 값이 지정된 값과 일치하지 않음
- **Exists**: 지정된 키를 가진 레이블이 존재
- **DoesNotExist**: 지정된 키를 가진 레이블이 존재하지 않음
- **Gt**: 레이블 값이 지정된 값보다 큼
- **Lt**: 레이블 값이 지정된 값보다 작음
## 포드 우선순위와 선점
Kubernetes는 포드 우선순위와 선점(Preemption) 기능을 통해 중요한 워크로드가 클러스터 리소스를 확보할 수 있도록 합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-08-scheduling-preemption-eviction-5.html)
### 우선순위 클래스(PriorityClass)
우선순위 클래스는 포드의 상대적 중요도를 정의합니다. 우선순위 값이 높을수록 포드의 중요도가 높습니다.
```yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "이 우선순위 클래스는 중요한 워크로드에 사용해야 합니다."
```
위 예시에서 `value` 필드는 우선순위 값을 나타냅니다. 값이 클수록 우선순위가 높습니다. `globalDefault` 필드가 `true`로 설정되면, 우선순위 클래스가 지정되지 않은 포드에 이 우선순위 클래스가 적용됩니다.
### 포드에 우선순위 클래스 적용
포드에 우선순위 클래스를 적용하려면 `priorityClassName` 필드를 사용합니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: high-priority-pod
spec:
priorityClassName: high-priority
containers:
- name: container
image: nginx
```
### 선점(Preemption)
선점은 우선순위가 높은 포드를 스케줄링하기 위해 우선순위가 낮은 포드를 제거하는 과정입니다. 스케줄러가 우선순위가 높은 포드를 스케줄링할 노드를 찾지 못하면, 우선순위가 낮은 포드를 선점하여 리소스를 확보합니다.
선점 과정:
1. 스케줄러가 우선순위가 높은 포드를 스케줄링할 노드를 찾지 못함
2. 스케줄러가 우선순위가 낮은 포드를 선점하여 제거할 노드를 선택
3. API를 통해 선택한 낮은 우선순위 파드 삭제를 요청하며 실제 종료는 kubelet·런타임이 수행
4. 포드가 정상적으로 종료되면 우선순위가 높은 포드를 해당 노드에 스케줄링
### 선점 고려 사항
선점을 사용할 때 고려해야 할 사항:
1. **그레이스풀 종료 기간**: 선점된 포드는 `terminationGracePeriodSeconds`에 지정된 시간 동안 정상 종료 과정을 거침
2. **PodDisruptionBudget**: 스케줄러가 위반을 피하려고 하지만 적절한 대상을 찾지 못하면 PDB를 위반하며 선점할 수 있음
3. **시스템 우선순위 클래스**: Kubernetes는 시스템 컴포넌트를 위한 우선순위 클래스를 제공
- `system-cluster-critical`: 클러스터 작동에 중요한 포드
- `system-node-critical`: 노드 작동에 중요한 포드
## 포드 축출
축출은 같은 파드를 이동시키는 것이 아니라 종료하는 작업입니다. 대체 여부는 컨트롤러, 용량, 스케줄링·스토리지 제약에 따라 달라집니다. 축출은 다양한 이유로 발생할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-08-scheduling-preemption-eviction-6.html)
### 축출 유형
1. **kube-controller-manager에 의한 축출**:
- taint-eviction-controller가 NoExecute 테인트를 처리하며 일반 파드는 기본 not-ready/unreachable 톨러레이션 300초를 받음. 실제 축출 시점은 톨러레이션 설정에 따름
- 노드가 Unreachable 상태일 때
2. **kubelet에 의한 축출**:
- 노드 리소스 부족(메모리, 디스크 등)
- 하드웨어 장애는 노드 사용 불가로 이어질 수 있지만 일반적인 kubelet 압력 축출 신호는 아님
3. **사용자에 의한 축출**:
- `kubectl drain` 명령 실행
- 노드 유지 관리 작업
### kubelet 축출 신호
kubelet은 다음과 같은 축출 신호를 모니터링합니다:
1. **memory.available**: 사용 가능한 메모리
2. **nodefs.available**: 노드 파일 시스템의 사용 가능한 공간
3. **nodefs.inodesFree**: 노드 파일 시스템의 사용 가능한 inode
4. **imagefs.available**: 이미지 파일 시스템의 사용 가능한 공간
5. **imagefs.inodesFree**: 이미지 파일 시스템의 사용 가능한 inode
6. **pid.available**: 사용 가능한 프로세스 ID
각 신호에 대해 소프트 임계값과 하드 임계값을 설정할 수 있습니다:
- **소프트 임계값**: 임계값을 초과하면 `grace-period` 후에 포드 축출
- **하드 임계값**: 임계값을 초과하면 즉시 포드 축출
```yaml
# kubelet 구성 예시
evictionHard:
memory.available: "100Mi"
nodefs.available: "10%"
nodefs.inodesFree: "5%"
imagefs.available: "15%"
imagefs.inodesFree: "5%"
evictionSoft:
memory.available: "200Mi"
nodefs.available: "15%"
evictionSoftGracePeriod:
memory.available: "1m"
nodefs.available: "2m"
evictionMaxPodGracePeriod: 30
evictionPressureTransitionPeriod: "30s"
```
### 축출 우선순위
kubelet은 요청 초과 사용 여부, 파드 우선순위, 요청 대비 사용량을 기준으로 대상을 정합니다. 모든 BestEffort → 모든 Burstable → 모든 Guaranteed라는 고정 순서는 아닙니다. 디스크·PID 압력은 리소스 계산도 다르므로 QoS를 보편적인 축출 순서로 해석하면 안 됩니다.
## 포드 중단 예산(PDB)
포드 중단 예산(Pod Disruption Budget, PDB)은 자발적 중단 중에도 애플리케이션의 가용성을 유지하기 위한 방법입니다. PDB는 동시에 중단될 수 있는 포드의 수를 제한합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-08-scheduling-preemption-eviction-7.html)
### PDB 정의
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: frontend-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: frontend
```
또는
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: frontend-pdb
spec:
maxUnavailable: 1
selector:
matchLabels:
app: frontend
```
위 예시에서:
- `minAvailable`: 항상 사용 가능해야 하는 최소 포드 수
- `maxUnavailable`: 동시에 사용 불가능할 수 있는 최대 포드 수
- `selector`: PDB가 적용될 포드를 선택하는 레이블 셀렉터
### PDB 작동 방식
1. 노드 드레인과 같은 자발적 중단이 발생하면, Kubernetes는 PDB를 확인
2. PDB 조건을 충족하면 포드 축출 진행
3. PDB 조건을 충족하지 않으면 포드 축출 거부
PDB는 일반 drain·descheduler 등의 Eviction API 요청을 제한합니다. 직접 파드 삭제, 컨트롤러 롤아웃, 노드 압력 축출은 이 검사를 우회합니다. `minAvailable: 2`와 `maxUnavailable: 1`은 원하는 복제본이 3개인 경우에만 같은 의미이며 대체 용량을 생성하지는 않습니다.
### PDB 모범 사례
1. **모든 중요한 워크로드에 PDB 설정**: 고가용성이 필요한 모든 워크로드에 PDB 설정
2. **적절한 값 선택**: 워크로드 특성에 맞는 `minAvailable` 또는 `maxUnavailable` 값 선택
3. **레플리카 수 고려**: `minAvailable`을 복제본 수와 같게 설정해 자발적 축출을 막을 수 있지만 유지 관리가 정체될 수 있으므로 허용 중단 수를 의도적으로 정함
4. **정기적인 테스트**: 노드 드레인 등의 작업으로 PDB 작동 테스트
## 노드 압력 축출
노드 압력 축출(Node Pressure Eviction)은 노드의 리소스 부족으로 인해 포드가 축출되는 메커니즘입니다.
### 노드 상태 조건
kubelet은 다음과 같은 노드 상태 조건을 보고합니다:
1. **MemoryPressure**: 노드의 메모리가 부족함
2. **DiskPressure**: 노드의 디스크 공간이 부족함
3. **PIDPressure**: 노드의 프로세스 ID가 부족함
이러한 조건이 발생하면 kubelet은 포드를 축출하여 리소스를 확보합니다.
### 축출 정책 구성
kubelet 구성에서 축출 정책을 설정할 수 있습니다:
```yaml
# kubelet 구성 예시
evictionHard:
memory.available: "100Mi"
nodefs.available: "10%"
nodefs.inodesFree: "5%"
imagefs.available: "15%"
imagefs.inodesFree: "5%"
evictionSoft:
memory.available: "200Mi"
nodefs.available: "15%"
evictionSoftGracePeriod:
memory.available: "1m"
nodefs.available: "2m"
evictionMinimumReclaim:
memory.available: "50Mi"
nodefs.available: "5%"
evictionMaxPodGracePeriod: 30
evictionPressureTransitionPeriod: "30s"
```
위 예시에서:
- `evictionMinimumReclaim`: 축출 후 최소한으로 확보해야 할 리소스 양
- `evictionPressureTransitionPeriod`: 압력 상태 전환 사이의 대기 시간
## 토폴로지 분배 제약 조건(TopologySpreadConstraints)
토폴로지 분배 제약 조건은 포드를 클러스터의 여러 토폴로지 도메인(노드, 영역, 리전 등)에 균등하게 분산시키는 기능입니다. 이는 고가용성을 보장하고 장애 도메인의 영향을 최소화하는 데 유용합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-08-scheduling-preemption-eviction-8.html)
### 주요 필드 설명
| 필드 | 설명 | 필수 여부 |
|------|------|----------|
| **maxSkew** | DoNotSchedule은 대상 도메인과 전역 최소값의 차이를 제한; ScheduleAnyway는 편차를 선호 점수에 반영 | 필수 |
| **topologyKey** | 토폴로지 도메인을 정의하는 노드 레이블 키 | 필수 |
| **whenUnsatisfiable** | 제약 조건을 충족할 수 없을 때 동작 (DoNotSchedule 또는 ScheduleAnyway) | 필수 |
| **labelSelector** | 계산할 파드 선택; 보통 자기 파드와 일치하도록 지정 | 선택 (null은 아무 파드도 선택하지 않음) |
| **minDomains** | 편차 계산을 위한 최소 적격 도메인 수 (v1.30부터 Stable) | 선택 |
| **matchLabelKeys** | 동일한 키의 레이블 값으로 그룹화 (Kubernetes 1.27+) | 선택 |
| **nodeAffinityPolicy** | 노드 어피니티/노드 셀렉터 고려 여부 (Kubernetes 1.26+) | 선택 |
| **nodeTaintsPolicy** | 노드 테인트 고려 여부 (Kubernetes 1.26+) | 선택 |
### EKS 가용 영역 분산 예제
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
namespace: production
spec:
replicas: 6
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
topologySpreadConstraints:
# 가용 영역 간 균등 분산 (하드 제약)
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
# 노드 간 균등 분산 (소프트 제약)
- maxSkew: 2
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: web
containers:
- name: web
image: nginx:1.30.4
resources:
requests:
cpu: 100m
memory: 128Mi
```
### minDomains 사용 (v1.30부터 Stable)
`minDomains`보다 적격 도메인이 적으면 전역 최소값을 0으로 계산합니다. `maxSkew: 1`일 때도 적격 도메인별 첫 파드는 배치할 수 있으며 추가 파드가 Pending이 될 수 있습니다. 모든 파드를 즉시 차단하는 설정은 아닙니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: zone-spread-app
spec:
replicas: 3
selector:
matchLabels:
app: zone-spread
template:
metadata:
labels:
app: zone-spread
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
minDomains: 3 # 적격 도메인이 3개 미만이면 전역 최소값을 0으로 계산
labelSelector:
matchLabels:
app: zone-spread
containers:
- name: app
image: nginx
```
### matchLabelKeys 사용 (Kubernetes 1.27+)
`matchLabelKeys`는 동일한 키의 레이블 값을 공유하는 포드끼리만 분산을 계산합니다. 이는 롤링 업데이트 시 새 버전과 이전 버전의 포드를 별도로 분산시키는 데 유용합니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: rolling-update-app
spec:
replicas: 6
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: myapp
matchLabelKeys:
- pod-template-hash # 같은 ReplicaSet의 포드끼리만 분산 계산
containers:
- name: app
image: myapp:v2
```
### Pod Anti-Affinity 대비 장점
| 특성 | TopologySpreadConstraints | Pod Anti-Affinity |
|------|--------------------------|-------------------|
| **분산 수준** | 균등 분산 (maxSkew로 제어) | 완전 분리 또는 없음 |
| **유연성** | 높음 (허용 편차 지정 가능) | 낮음 (all-or-nothing) |
| **스케일링** | 확장 시에도 균등 분산 유지 | 노드 수에 제한됨 |
| **성능** | 효율적 | 포드 수 증가 시 성능 저하 |
| **권장 사용** | 일반적인 고가용성 배포 | 동일 노드 배치 완전 금지 시 |
```yaml
# Anti-Affinity: 동일 노드에 절대 배치 불가 (레플리카 수 = 노드 수로 제한)
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostname
# TopologySpreadConstraints: 균등 분산 (더 유연함)
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
```
## Pod Deletion Cost
Pod Deletion Cost는 ReplicaSet 컨트롤러가 축소 시 사용하는 best-effort 선호값입니다. HPA는 원하는 복제본 수를 바꾸며 개별 삭제 파드를 직접 선택하지 않습니다. 이 어노테이션은 Job·StatefulSet을 보호하거나 축출을 막지 않으며 삭제 순서를 보장하지도 않습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-08-scheduling-preemption-eviction-9.html)
### 어노테이션 형식
```yaml
metadata:
annotations:
controller.kubernetes.io/pod-deletion-cost: "100"
```
- **값 범위**: -2147483648 ~ 2147483647 (32비트 정수)
- **기본값**: 0 (어노테이션이 없는 경우)
- **동작**: 낮은 값의 포드가 먼저 삭제됨
### 캐시 보호 패턴
CPU 사용률 HPA를 사용할 캐시는 명시적인 CPU 요청이 필요합니다. 아래는 스케줄링 예시이며 완전한 Redis 운영 구성이 아닙니다:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cache-service
spec:
replicas: 3
selector:
matchLabels:
app: cache
template:
metadata:
labels:
app: cache
spec:
automountServiceAccountToken: false
containers:
- name: cache
image: redis:7
resources:
requests:
cpu: 100m
memory: 128Mi
```
실제 캐시 상태를 측정한 뒤 권한 있는 운영자·컨트롤러가 축소 전에 선택한 ReplicaSet 소유 파드에 한 번 어노테이션을 설정할 수 있습니다:
```bash
kubectl -n default annotate pod "$CACHE_POD" \
controller.kubernetes.io/pod-deletion-cost="1000" --overwrite
```
`CACHE_POD`는 실제 캐시 파드 이름으로 설정합니다. 잦은 어노테이션 변경은 API 부하를 만듭니다. 사용자 정의 갱신기는 Redis·Kubernetes 클라이언트와 좁은 Pod patch 권한이 필요하며 단순 시간 경과는 캐시 워밍업의 증거가 아닙니다. 이 예시는 갱신기를 설치하지 않습니다.
### Job에는 적용되지 않음
Job 파드에 deletion-cost를 넣어도 보호되지 않습니다. 장시간 작업은 체크포인트, 재시도·멱등성, 정상 종료 처리를 설계하고 작업 특성에 맞게 중단 정책을 정하세요.
### HPA와 함께 사용
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: cache-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: cache-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 60
# HPA가 복제본 수를 줄이면 ReplicaSet이 deletion cost를 선호값으로 고려
```
## Descheduler
Descheduler는 실행 중인 클러스터에서 포드를 재분산시키는 도구입니다. 스케줄러는 새 포드를 배치할 때만 동작하지만, Descheduler는 이미 실행 중인 포드를 축출하여 더 나은 분산을 달성할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-08-scheduling-preemption-eviction-10.html)
### Helm을 사용한 설치
검증한 예시 릴리스는 Descheduler v0.36.0이며 Kubernetes v1.36과 이전 두 마이너 버전이 테스트 범위입니다. 다른 릴리스에는 호환성 표를 확인하세요. 검토한 `schedule`과 아래 정책의 `profiles`를 `deschedulerPolicy.profiles` 아래 넣어 `descheduler-values.yaml`로 저장합니다. 이전 `strategies.*.enabled` 값은 이 API를 구성하지 않습니다.
```bash
helm repo add descheduler https://kubernetes-sigs.github.io/descheduler/
helm upgrade --install descheduler descheduler/descheduler \
--version 0.36.0 --namespace kube-system \
--values descheduler-values.yaml
```
### DeschedulerPolicy 설정
```yaml
apiVersion: descheduler/v1alpha2
kind: DeschedulerPolicy
profiles:
- name: default
pluginConfig:
- name: DefaultEvictor
args:
nodeFit: true
- name: RemoveDuplicates
args:
excludeOwnerKinds: [StatefulSet]
- name: LowNodeUtilization
args:
thresholds:
cpu: 20
memory: 20
pods: 20
targetThresholds:
cpu: 50
memory: 50
pods: 50
- name: RemovePodsHavingTooManyRestarts
args:
podRestartThreshold: 100
includingInitContainers: true
- name: PodLifeTime
args:
maxPodLifeTimeSeconds: 86400
labelSelector:
matchLabels:
app.kubernetes.io/lifecycle: ephemeral
- name: RemovePodsViolatingNodeAffinity
args:
nodeAffinityType: [requiredDuringSchedulingIgnoredDuringExecution]
- name: RemovePodsViolatingTopologySpreadConstraint
args:
constraints: [DoNotSchedule]
plugins:
balance:
enabled:
- RemoveDuplicates
- LowNodeUtilization
- RemovePodsViolatingTopologySpreadConstraint
deschedule:
enabled:
- RemovePodsHavingTooManyRestarts
- PodLifeTime
- RemovePodsViolatingNodeAffinity
```
위 정책은 Descheduler 설정 파일이며 kubectl apply용 API 객체가 아닙니다. 그룹 재분배는 Balance, 파드별 결정은 Deschedule 플러그인을 사용합니다. LowNodeUtilization은 보통 실시간 CPU 사용률 대신 리소스 요청량을 평가하며 축출 후 다른 노드 배치를 보장하지 않습니다. 보호 설정을 검토하고 dry-run 검증 후 반복 축출을 활성화하세요.
### Descheduler CronJob 설정
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: descheduler
namespace: kube-system
spec:
schedule: "*/30 * * * *" # 30분마다 실행
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
serviceAccountName: descheduler
containers:
- name: descheduler
image: registry.k8s.io/descheduler/descheduler:v0.36.0
args:
- --policy-config-file=/policy-dir/policy.yaml
- --v=3
volumeMounts:
- name: policy-volume
mountPath: /policy-dir
restartPolicy: Never
volumes:
- name: policy-volume
configMap:
name: descheduler-policy
```
### PDB 존중
Descheduler는 기본적으로 PodDisruptionBudget(PDB)을 존중합니다. PDB가 설정된 포드는 PDB 제한 내에서만 축출됩니다.
```yaml
# PDB 설정 예시
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: web
---
# Descheduler가 이 PDB를 존중하여
# 허용 가능한 voluntary eviction만 진행 (장애 시 가용성 보장은 아님)
```
위 독립 CronJob은 Helm 설치와 중복 실행하지 않는 대안입니다. `descheduler` ServiceAccount·RBAC와 `policy.yaml` 키를 가진 `descheduler-policy` ConfigMap이 필요하므로 공식 차트·매니페스트로 선행 리소스를 구성하세요.
### 주의사항
1. **시스템 포드 보호**: 기본 critical Pod 보호를 전체 kube-system 제외와 혼동하지 말고 설치 버전의 DefaultEvictor와 선택 조건을 확인하세요.
2. **DaemonSet 포드**: DaemonSet 포드는 축출되지 않습니다.
3. **로컬 스토리지**: 보호 여부는 DefaultEvictor·차트 값에 따라 달라지며 로컬 데이터 손실을 검토해야 합니다.
4. **PDB 제한**: PDB 제한을 초과하여 포드를 축출하지 않습니다.
> 📚 **심화 학습**: 커스텀 스케줄러에 대한 자세한 내용은 다음을 참조하세요:
> - [Custom Scheduler Part 1: 기본 개념](https://www.atomai.click/kubernetes-docs/llms/ko/scheduling/01-custom-scheduler-part1.md)
> - [Custom Scheduler Part 2: 구현](https://www.atomai.click/kubernetes-docs/llms/ko/scheduling/02-custom-scheduler-part2.md)
> - [Custom Scheduler Part 3: 고급 기능](https://www.atomai.click/kubernetes-docs/llms/ko/scheduling/03-custom-scheduler-part3.md)
## Amazon EKS에서의 스케줄링 최적화
Amazon EKS에서는 Kubernetes 스케줄링 기능을 활용하여 워크로드를 최적화할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-08-scheduling-preemption-eviction-11.html)
### 노드 그룹 및 인스턴스 유형
EKS에서는 다양한 노드 그룹과 인스턴스 유형을 활용하여 워크로드에 맞는 리소스를 제공할 수 있습니다:
1. **다양한 인스턴스 유형**: 컴퓨팅 최적화, 메모리 최적화, 스토리지 최적화 등
2. **스팟 인스턴스**: 비용 효율적인 워크로드를 위한 스팟 인스턴스
3. **GPU 인스턴스**: AI/ML 워크로드를 위한 GPU 인스턴스
노드 레이블과 테인트를 사용하여 특정 워크로드를 특정 노드 그룹에 배치할 수 있습니다:
기존 클러스터와 리전, 지원 GPU 인스턴스·AMI, IAM 권한에 맞게 검토한 eksctl 구성을 사용하세요:
```yaml
# gpu-nodegroup.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-cluster
region: us-west-2
managedNodeGroups:
- name: gpu-nodes
instanceType: p3.2xlarge
desiredCapacity: 1
privateNetworking: true
labels:
workload-type: gpu
taints:
- key: gpu
value: "true"
effect: NoSchedule
```
```bash
eksctl create nodegroup --config-file=gpu-nodegroup.yaml
```
### 가용 영역 분산
EKS에서는 포드 안티-어피니티와 토폴로지 스프레드 제약 조건을 사용하여 워크로드를 여러 가용 영역에 분산시킬 수 있습니다:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
containers:
- name: web
image: nginx
```
위 예시에서 `topologySpreadConstraints`는 포드를 여러 가용 영역에 균등하게 분산시킵니다.
### Karpenter를 사용한 자동 스케일링
Amazon EKS에서는 Karpenter를 사용하여 워크로드에 맞는 노드를 자동으로 프로비저닝할 수 있습니다:
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default-class
limits:
cpu: 1000
memory: 1000Gi
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default-class
spec:
role: KarpenterNodeRole-my-cluster
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
```
Karpenter는 포드의 리소스 요구 사항에 맞는 최적의 인스턴스 유형을 선택하여 비용을 최적화합니다.
### 리소스 요청 및 제한 최적화
EKS에서 워크로드의 리소스 요청과 제한을 최적화하는 것이 중요합니다:
1. **Vertical Pod Autoscaler(VPA)**: 워크로드의 실제 리소스 사용량을 기반으로 리소스 요청 최적화
2. **Goldilocks**: VPA 권장 사항을 시각화하여 리소스 요청 최적화 지원
3. **리소스 쿼터**: 네임스페이스별 리소스 사용량 제한
```yaml
# VPA 예시
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: frontend-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: frontend
updatePolicy:
updateMode: "Recreate"
```
## 스케줄링 모범 사례
Kubernetes 및 EKS에서 스케줄링을 최적화하기 위한 모범 사례:
1. **적절한 리소스 요청 및 제한 설정**:
- 워크로드의 실제 리소스 사용량을 기반으로 리소스 요청 설정
- 중요한 워크로드에 적절한 리소스 제한 설정
- VPA를 사용하여 리소스 요청 자동 최적화
2. **워크로드 분산**:
- 포드 안티-어피니티를 사용하여 중요한 워크로드를 여러 노드에 분산
- 토폴로지 스프레드 제약 조건을 사용하여 워크로드를 여러 가용 영역에 분산
- 노드 어피니티를 사용하여 특정 워크로드를 특정 노드에 배치
3. **노드 리소스 최적화**:
- 다양한 인스턴스 유형을 사용하여 워크로드에 맞는 리소스 제공
- 스팟 인스턴스를 사용하여 비용 최적화
- Karpenter를 사용하여 워크로드에 맞는 노드 자동 프로비저닝
4. **PDB 설정**:
- 중요한 워크로드에 PDB 설정
- 워크로드 특성에 맞는 `minAvailable` 또는 `maxUnavailable` 값 선택
- 정기적으로 PDB 작동 테스트
5. **우선순위 및 선점 설정**:
- 중요한 워크로드에 높은 우선순위 클래스 설정
- 시스템 컴포넌트에 `system-cluster-critical` 또는 `system-node-critical` 우선순위 클래스 사용
- 선점 영향 이해 및 테스트
6. **노드 테인트 및 톨러레이션**:
- 특수 워크로드를 위한 전용 노드 설정
- 유지 관리 중인 노드에 테인트 적용
- 적절한 톨러레이션 설정
## 결론
Kubernetes의 스케줄링, 선점 및 축출 메커니즘은 클러스터 리소스를 효율적으로 관리하고 워크로드의 가용성을 유지하는 데 중요한 역할을 합니다. 이러한 기능을 이해하고 활용함으로써 Amazon EKS 클러스터에서 워크로드를 최적화하고 안정적으로 운영할 수 있습니다.
스케줄링 최적화는 지속적인 과정이며, 워크로드 특성과 클러스터 상태에 따라 지속적으로 조정해야 합니다. 모니터링 도구를 활용하여 클러스터 리소스 사용량을 추적하고, 필요에 따라 스케줄링 정책을 조정하는 것이 중요합니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [스케줄링, 선점 및 축출 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/core/08-scheduling-preemption-eviction-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/core/09-cluster-administration
----------------------------------------
# Kubernetes 클러스터 관리
> **버전 정보**: Kubernetes 1.34 - 1.36 (2026년 9월 11일 EKS 표준 지원 기준)
> **마지막 업데이트**: 2026년 2월 11일
Kubernetes 클러스터 관리는 클러스터의 설정, 유지 관리, 모니터링, 문제 해결 및 업그레이드를 포함하는 중요한 작업입니다. 이 장에서는 Kubernetes 클러스터 관리의 다양한 측면과 Amazon EKS에서의 클러스터 관리 모범 사례에 대해 알아보겠습니다.
자체 관리형 kubeadm 작업과 EKS 서비스 작업은 구분해야 합니다. EKS는 컨트롤 플레인 호스트, 정적 파드 매니페스트, etcd 직접 접근을 제공하지 않습니다. 아래 블록은 연속 실행하는 단일 스크립트가 아닌 별도 예시입니다. 실제 클러스터 버전의 업스트림 지원과 애드온 호환성을 확인하세요.
## 핵심 개념
- **클러스터 수명 주기 관리**: 클러스터 생성부터 폐기까지의 전체 과정
- **컨트롤 플레인 관리**: API 서버, 스케줄러, 컨트롤러 관리자 등의 핵심 구성 요소 관리
- **노드 관리**: 워커 노드의 추가, 제거, 유지 관리
- **리소스 할당**: CPU, 메모리, 스토리지 등의 리소스 할당 및 제한 설정
- **업그레이드 전략**: 다운타임 최소화를 위한 클러스터 및 애플리케이션 업그레이드 전략
## 목차
1. [클러스터 관리 개요](#클러스터-관리-개요)
2. [클러스터 구성요소 관리](#클러스터-구성요소-관리)
3. [리소스 관리](#리소스-관리)
4. [클러스터 네트워킹](#클러스터-네트워킹)
5. [인증 및 권한 관리](#인증-및-권한-관리)
6. [클러스터 업그레이드](#클러스터-업그레이드)
7. [백업 및 복구](#백업-및-복구)
8. [모니터링 및 로깅](#모니터링-및-로깅)
9. [문제 해결](#문제-해결)
10. [Amazon EKS 클러스터 관리](#amazon-eks-클러스터-관리)
11. [클러스터 관리 모범 사례](#클러스터-관리-모범-사례)
12. [결론](#결론)
## 환경 설정
클러스터 관리를 위해 다음 도구들이 필요합니다:
[공식 kubectl 설치 문서](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/)를 사용하고 API 서버와 마이너 버전 차이를 1 이내로 유지하세요. 자체 관리형 클러스터의 kubeadm/kubelet은 대상 마이너 버전의 `pkgs.k8s.io` 저장소에서 설치합니다. 이전 `1.x.y-00` 패키지 예시는 더 이상 유효하지 않으며 저장소에서 정확한 패키지 버전을 선택해야 합니다.
Helm과 k9s는 [Helm 공식 설치 지침](https://helm.sh/docs/intro/install/)과 [k9s 릴리스](https://github.com/derailed/k9s/releases)에서 아키텍처·체크섬을 확인해 설치하세요. EKS에는 인증된 AWS CLI와 호환되는 eksctl도 필요합니다.
```bash
kubectl version --client
helm version
k9s version
# 대상 마이너 저장소를 구성한 자체 관리형 노드에서만 확인:
apt-cache madison kubeadm
```
## 클러스터 관리 개요
Kubernetes 클러스터 관리는 클러스터의 전체 수명 주기를 관리하는 과정입니다. 이는 다음과 같은 주요 영역을 포함합니다:
1. **클러스터 설정 및 구성**: 클러스터 생성, 노드 추가, 네트워킹 설정, 스토리지 구성 등
2. **운영 관리**: 리소스 모니터링, 성능 최적화, 용량 계획, 문제 해결
3. **보안 관리**: 인증, 권한 부여, 네트워크 정책, 보안 컨텍스트 등
4. **업그레이드 및 패치**: 클러스터 버전 업그레이드, 보안 패치 적용
5. **백업 및 복구**: 클러스터 데이터 백업, 재해 복구 계획
다음 다이어그램은 Kubernetes 클러스터 관리의 주요 영역과 관련 도구를 보여줍니다:
## 클러스터 구성요소 관리
Kubernetes 클러스터는 컨트롤 플레인 구성요소와 노드 구성요소로 구성됩니다. 각 구성요소의 관리는 클러스터의 안정성과 성능에 중요합니다.
### 컨트롤 플레인 구성요소 관리

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-0.html)
#### API 서버 관리
API 서버는 Kubernetes API를 노출하는 컨트롤 플레인의 핵심 구성요소입니다.
```bash
# API 서버 로그 확인
kubectl logs -n kube-system kube-apiserver-
# API 서버 구성 확인 (kubeadm 클러스터)
sudo cat /etc/kubernetes/manifests/kube-apiserver.yaml
# API 서버 상태 확인
kubectl get --raw='/readyz?verbose'
```
#### etcd 관리
etcd는 Kubernetes API 상태를 저장하는 분산 키-값 저장소입니다.
```bash
# etcd 백업
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
snapshot save /backup/etcd-snapshot-$(date +%Y-%m-%d).db
# etcd 상태 확인
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
endpoint health
```
### 노드 관리
노드는 컨테이너화된 애플리케이션을 실행하는 워커 머신입니다.
```bash
# 노드 목록 확인
kubectl get nodes
# 노드 상세 정보 확인
kubectl describe node
# 노드 라벨 추가
kubectl label node environment=production
# 노드 유지보수 모드 설정
kubectl drain --ignore-daemonsets
# 유지보수 후 노드 복귀
kubectl uncordon
```
### 구성요소 상태 모니터링
```bash
# 컨트롤 플레인 구성요소 상태 확인
kubectl get --raw='/readyz?verbose'
# 시스템 파드 상태 확인
kubectl get pods -n kube-system
# 노드 리소스 사용량 확인
kubectl top nodes
```

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-1.html)
### 클러스터 관리 도구
Kubernetes 클러스터 관리를 위한 다양한 도구가 있습니다:
1. **kubectl**: Kubernetes 클러스터와 상호 작용하기 위한 명령줄 도구
2. **kubeadm**: Kubernetes 클러스터 생성 및 관리를 위한 도구
3. **kops**: Kubernetes 클러스터 생성, 업그레이드, 관리를 위한 도구
4. **eksctl**: Amazon EKS 클러스터 생성 및 관리를 위한 도구
5. **Helm**: Kubernetes 애플리케이션 패키지 관리자
6. **Headlamp**: Kubernetes 웹 UI (기존 Kubernetes Dashboard 프로젝트는 보관 상태)
7. **Prometheus & Grafana**: 모니터링 및 알림 도구
8. **Fluentd & Elasticsearch**: 로깅 도구
## 클러스터 구성요소 관리
Kubernetes 클러스터는 여러 구성요소로 이루어져 있으며, 이러한 구성요소를 효과적으로 관리하는 것이 중요합니다.
### 컨트롤 플레인 구성요소
컨트롤 플레인 구성요소는 클러스터의 전반적인 상태를 관리합니다:
1. **kube-apiserver**: Kubernetes API를 노출하는 컴포넌트
2. **etcd**: 클러스터 데이터를 저장하는 키-값 저장소
3. **kube-scheduler**: 포드를 노드에 스케줄링하는 컴포넌트
4. **kube-controller-manager**: 컨트롤러를 실행하는 컴포넌트
5. **cloud-controller-manager**: 클라우드 제공업체와 상호 작용하는 컴포넌트
다음 다이어그램은 Kubernetes 컨트롤 플레인 구성요소와 그 상호작용을 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-2.html)
#### 컨트롤 플레인 구성요소 모니터링
컨트롤 플레인 구성요소의 상태를 모니터링하는 것이 중요합니다:
```bash
# 컨트롤 플레인 구성요소 상태 확인
kubectl get --raw='/readyz?verbose'
# API 서버 로그 확인
kubectl logs -n kube-system kube-apiserver-
# etcd 상태 확인
kubectl exec -n kube-system etcd- -- etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
--key=/etc/kubernetes/pki/etcd/healthcheck-client.key endpoint health
```
#### 컨트롤 플레인 구성요소 구성
아래 매니페스트는 플래그·구성 일부입니다. 호스트 네트워크, 인증서 마운트와 기타 kubeadm 생성 설정을 생략했으므로 실행 중인 컨트롤 플레인 매니페스트를 이것으로 대체하지 마세요. 이미지는 클러스터 업그레이드 계획과 일치시킵니다.
컨트롤 플레인 구성요소의 구성을 관리하는 방법:
```yaml
# kube-apiserver 구성 예시
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --advertise-address=192.168.1.10
- --allow-privileged=true
- --authorization-mode=Node,RBAC
- --client-ca-file=/etc/kubernetes/pki/ca.crt
- --enable-admission-plugins=NodeRestriction
- --enable-bootstrap-token-auth=true
- --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
- --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
- --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
- --etcd-servers=https://127.0.0.1:2379
- --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
- --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --secure-port=6443
- --service-account-key-file=/etc/kubernetes/pki/sa.pub
- --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
- --service-account-issuer=https://kubernetes.default.svc.cluster.local
- --service-cluster-ip-range=10.96.0.0/12
- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
image: registry.k8s.io/kube-apiserver:v1.36.4
name: kube-apiserver
```
### 노드 구성요소
노드 구성요소는 각 노드에서 실행되며 포드를 관리합니다:
1. **kubelet**: 각 노드에서 실행되는 에이전트로, 포드와 컨테이너가 실행되도록 함
2. **kube-proxy**: 네트워크 규칙을 유지하고 연결 포워딩을 처리
3. **컨테이너 런타임**: 컨테이너를 실행하는 소프트웨어(containerd, CRI-O 또는 외부 CRI 어댑터를 사용하는 Docker Engine 등)
#### 노드 관리
노드 관리를 위한 주요 명령어:
```bash
# 노드 목록 확인
kubectl get nodes
# 노드 상세 정보 확인
kubectl describe node
# 노드 레이블 추가
kubectl label node key=value
# 노드 테인트 추가
kubectl taint node key=value:NoSchedule
# 노드 유지 관리 모드 설정
kubectl cordon
# 노드 드레인
kubectl drain --ignore-daemonsets
```
drain은 단독 파드, PDB, 로컬 데이터 때문에 중단될 수 있습니다. 기본적으로 `--force`·`--delete-emptydir-data`를 추가하지 말고 원인을 확인하세요. 후자는 emptyDir 데이터 손실을 명시적으로 허용합니다. drain 완료와 워크로드 상태를 확인한 뒤 유지 관리하세요.
#### 노드 문제 해결
노드 문제 해결을 위한 명령어:
```bash
# 노드 상태 확인
kubectl describe node | grep Conditions -A 10
# 노드 리소스 사용량 확인
kubectl top node
# kubelet 로그 확인
journalctl -u kubelet
# 컨테이너 런타임 상태 확인
systemctl status docker # Docker 사용 시
systemctl status containerd # containerd 사용 시
```
## 리소스 관리
Kubernetes 클러스터에서 리소스를 효과적으로 관리하는 것은 클러스터의 안정성과 성능을 유지하는 데 중요합니다.
### 리소스 쿼터
리소스 쿼터는 네임스페이스별로 리소스 사용량을 제한합니다:
```yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-resources
namespace: dev
spec:
hard:
requests.cpu: "1"
requests.memory: 1Gi
limits.cpu: "2"
limits.memory: 2Gi
pods: "10"
```
위 예시에서 `dev` 네임스페이스는 최대 10개의 포드, 1 CPU 및 1Gi 메모리 요청, 2 CPU 및 2Gi 메모리 제한을 가질 수 있습니다.
### 리밋 레인지
리밋 레인지는 네임스페이스 내의 개별 리소스에 대한 기본값과 제한을 설정합니다:
```yaml
apiVersion: v1
kind: LimitRange
metadata:
name: limit-range
namespace: dev
spec:
limits:
- default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 200m
memory: 256Mi
max:
cpu: 1
memory: 1Gi
min:
cpu: 100m
memory: 128Mi
type: Container
```
위 예시에서 `dev` 네임스페이스의 모든 컨테이너는 기본적으로 500m CPU 및 512Mi 메모리 제한, 200m CPU 및 256Mi 메모리 요청을 가지며, 최대 1 CPU 및 1Gi 메모리, 최소 100m CPU 및 128Mi 메모리를 가질 수 있습니다.
### 수평 포드 자동 확장(HPA)
HPA는 CPU 사용량이나 사용자 정의 메트릭을 기반으로 포드 수를 자동으로 조정합니다:
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: frontend-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: frontend
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80
```
위 예시에서 `frontend` 디플로이먼트는 요청 CPU 대비 평균 사용률 80%를 목표로 하며 허용 오차, 누락 메트릭, 안정화 구간과 스케일링 정책을 함께 적용합니다. 최소 2개, 최대 10개의 레플리카를 유지합니다.
### 수직 포드 자동 확장(VPA)
VPA는 포드의 CPU 및 메모리 요청을 자동으로 조정합니다:
```yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: frontend-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: frontend
updatePolicy:
updateMode: "Recreate"
```
위 예시에서 `frontend` 디플로이먼트의 포드는 실제 리소스 사용량을 기반으로 CPU 및 메모리 요청이 자동으로 조정됩니다.
## 클러스터 네트워킹
Kubernetes 클러스터 네트워킹은 포드, 서비스, 노드 간의 통신을 관리합니다.
### 클러스터 네트워크 모델
Kubernetes 네트워크 모델의 기본 요구 사항:
1. 모든 포드는 NAT 없이 다른 모든 포드와 통신할 수 있어야 함
2. 노드의 에이전트(kubelet)는 해당 노드의 모든 포드와 통신할 수 있어야 함
3. 외부 연결은 라우팅·egress 정책에 따라 달라지며 보편적인 Pod NAT 모드 요구사항은 없음
다음 다이어그램은 Kubernetes 네트워킹 구성요소와 통신 흐름을 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-3.html)
### CNI(Container Network Interface) 플러그인
Kubernetes는 CNI 플러그인을 통해 네트워킹을 구현합니다. 일반적인 CNI 플러그인:
1. **Calico**: 네트워크 정책 및 보안 기능이 강화된 CNI
2. **Flannel**: 간단한 오버레이 네트워크 제공
3. **Cilium**: eBPF 기반의 네트워킹 및 보안 솔루션
4. **AWS VPC CNI**: AWS VPC와 통합된 CNI
5. **Weave Net (역사적 예시)**: 보관 상태이며 새 설치는 유지 관리되는 대안을 선택
#### CNI 플러그인 설치 및 구성
CNI 플러그인 설치 예시(Calico):
CNI 하나 또는 문서화된 체이닝·마이그레이션 구성을 선택하세요. Calico·Flannel·Cilium 대안을 같은 실행 중 클러스터에 순서대로 설치하면 안 됩니다. 지원되는 버전을 고정하고 제공자별 지침을 따르며 EKS에서는 [네트워킹 장](https://www.atomai.click/kubernetes-docs/llms/ko/core/03-services-networking.md)의 VPC CNI 또는 대체 CNI 전환 절차를 사용하세요.
```bash
# 변경 전에 설치된 네트워킹 구성 요소 확인
kubectl get daemonsets -A
kubectl get pods -A -l k8s-app=calico-node
```
### 서비스 네트워킹
Kubernetes 서비스는 포드 집합에 대한 안정적인 엔드포인트를 제공합니다:
1. **ClusterIP**: 클러스터 내부에서만 접근 가능한 서비스
2. **NodePort**: 모든 노드의 특정 포트를 통해 접근 가능한 서비스
3. **LoadBalancer**: 외부 로드 밸런서를 통해 접근 가능한 서비스
4. **ExternalName**: 외부 서비스에 대한 CNAME 레코드 제공
#### 서비스 CIDR 구성
서비스 CIDR은 서비스 IP 주소 범위를 정의합니다:
```bash
# kube-apiserver 구성에서 서비스 CIDR 설정
--service-cluster-ip-range=10.96.0.0/12
```
### CoreDNS 관리
CoreDNS는 Kubernetes의 DNS 서비스를 제공합니다:
```bash
# CoreDNS 상태 확인
kubectl get pods -n kube-system -l k8s-app=kube-dns
# CoreDNS 구성 확인
kubectl get configmap -n kube-system coredns -o yaml
```
CoreDNS 구성 예시:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
```
### 네트워크 정책
네트워크 정책은 포드 간의 통신을 제어합니다:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-network-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 3306
egress:
- to:
- podSelector:
matchLabels:
role: monitoring
ports:
- protocol: TCP
port: 9090
```
위 예시에서 `role=db` 레이블이 있는 포드는 `role=frontend` 레이블이 있는 포드로부터의 TCP 3306 포트 인바운드 트래픽과 `role=monitoring` 레이블이 있는 포드로의 TCP 9090 포트 아웃바운드 트래픽만 허용합니다.
## 인증 및 권한 관리
Kubernetes의 인증 및 권한 관리는 클러스터 보안의 핵심 요소입니다.
다음 다이어그램은 Kubernetes의 인증 및 권한 부여 흐름을 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-4.html)
### 인증(Authentication)
Kubernetes는 다양한 인증 방법을 지원합니다:
1. **X.509 인증서**: 클라이언트 인증서를 사용한 인증
2. **서비스 계정 토큰**: 서비스 계정에 연결된 JWT 토큰
3. **OpenID Connect(OIDC)**: 외부 ID 제공자를 통한 인증
4. **웹훅 토큰 인증**: 외부 서비스를 통한 토큰 검증
5. **인증 프록시**: 인증 프록시를 통한 요청 처리
#### X.509 인증서 관리
X.509 인증서 생성 및 관리:
```bash
# 보호된 개인 키와 CSR 생성
umask 077
openssl genrsa -out user.key 2048
openssl req -new -key user.key -out user.csr -subj "/CN=user/O=group"
# CSR을 Kubernetes에 제출
cat < user.crt
```
#### OIDC 인증 구성
OIDC 인증 구성 예시:
```bash
# kube-apiserver 구성에 OIDC 플래그 추가
--oidc-issuer-url=https://accounts.google.com
--oidc-client-id=kubernetes
--oidc-username-claim=email
--oidc-groups-claim=groups
```
### 권한 부여(Authorization)
Kubernetes는 다양한 권한 부여 모드를 지원합니다:
1. **RBAC(Role-Based Access Control)**: 역할 기반 접근 제어
2. **ABAC(Attribute-Based Access Control)**: 속성 기반 접근 제어
3. **Node**: 노드 권한 부여
4. **Webhook**: 외부 서비스를 통한 권한 부여
#### RBAC 구성
RBAC는 가장 일반적인 권한 부여 메커니즘입니다:
```yaml
# Role 예시
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
# RoleBinding 예시
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
- kind: User
name: user
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
```
위 예시에서 `user`는 `default` 네임스페이스의 포드를 조회할 수 있는 권한을 가집니다.
#### ClusterRole 및 ClusterRoleBinding
클러스터 전체 리소스에 대한 권한을 관리합니다:
```yaml
# ClusterRole 예시
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: node-reader
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "watch", "list"]
# ClusterRoleBinding 예시
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: read-nodes
subjects:
- kind: User
name: user
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: node-reader
apiGroup: rbac.authorization.k8s.io
```
위 예시에서 `user`는 클러스터의 모든 노드를 조회할 수 있는 권한을 가집니다.
### 서비스 계정 관리
서비스 계정은 포드가 API 서버와 통신하는 데 사용됩니다:
```yaml
# 서비스 계정 생성
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-service-account
namespace: default
# 서비스 계정에 권한 부여
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: my-service-account-binding
namespace: default
subjects:
- kind: ServiceAccount
name: my-service-account
namespace: default
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
# 포드에서 서비스 계정 사용
---
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
serviceAccountName: my-service-account
containers:
- name: my-container
image: nginx
```
### 보안 컨텍스트
보안 컨텍스트는 포드 및 컨테이너의 권한과 접근 제어를 정의합니다:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: security-context-pod
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: security-context-container
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
```
위 예시에서 포드는 UID 1000, GID 3000으로 실행되며, 컨테이너는 권한 상승이 불가능하고, 모든 Linux 기능이 제거되며, 루트 파일 시스템이 읽기 전용으로 마운트됩니다.
## 클러스터 업그레이드
Kubernetes 클러스터 업그레이드는 새로운 기능, 성능 개선, 보안 패치를 적용하기 위해 필요합니다.
다음 다이어그램은 Kubernetes 클러스터 업그레이드 프로세스를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-5.html)
### 업그레이드 계획
클러스터 업그레이드를 계획할 때 고려해야 할 사항:
1. **버전 호환성**: Kubernetes 버전 간의 호환성 확인
2. **업그레이드 경로**: 지원되는 업그레이드 경로 확인
3. **다운타임**: 업그레이드 중 예상되는 다운타임 계획
4. **롤백 계획**: 문제 발생 시 롤백 계획 수립
5. **애플리케이션 영향**: 업그레이드가 애플리케이션에 미치는 영향 평가
### 컨트롤 플레인 업그레이드
kubeadm을 사용한 컨트롤 플레인 업그레이드:
[해당 버전의 kubeadm 업그레이드 절차](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/)를 따르세요. 대상 마이너 버전의 pkgs.k8s.io 저장소에서 실제 패키지 버전을 선택하고 한 번에 한 마이너 버전만 업그레이드합니다.
1. etcd 백업과 워크로드·애드온 호환성을 확인합니다. 첫 컨트롤 플레인 노드에서 kubeadm을 먼저 업그레이드한 뒤 `kubeadm upgrade plan`, `kubeadm upgrade apply `을 실행합니다.
2. 추가 컨트롤 플레인 노드는 kubeadm 업그레이드 후 `kubeadm upgrade node`를 실행합니다.
3. 각 노드의 kubelet 업그레이드 전에 drain하고 실패하면 절차를 중단해 원인을 해결합니다. 호환 kubelet/kubectl 패키지 설치, systemd 재로드, kubelet 재시작, Ready·워크로드 확인 후 관리자 클라이언트에서 uncordon합니다.
4. 워커도 kubeadm 업그레이드와 `kubeadm upgrade node` 후 drain/kubelet/검증/uncordon 절차를 수행합니다. 일반적인 전체 OS 업그레이드를 Kubernetes 버전별 업그레이드 절차 대신 사용하지 마세요.
명령은 명시한 노드 또는 관리자 클라이언트에서 실행합니다. 중첩된 `ssh` 명령을 나열한 것은 다중 노드 자동화 스크립트가 아닙니다.
### 워커 노드 업그레이드
워커 노드 업그레이드 과정:
[해당 버전의 kubeadm 업그레이드 절차](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/)를 따르세요. 대상 마이너 버전의 pkgs.k8s.io 저장소에서 실제 패키지 버전을 선택하고 한 번에 한 마이너 버전만 업그레이드합니다.
1. etcd 백업과 워크로드·애드온 호환성을 확인합니다. 첫 컨트롤 플레인 노드에서 kubeadm을 먼저 업그레이드한 뒤 `kubeadm upgrade plan`, `kubeadm upgrade apply `을 실행합니다.
2. 추가 컨트롤 플레인 노드는 kubeadm 업그레이드 후 `kubeadm upgrade node`를 실행합니다.
3. 각 노드의 kubelet 업그레이드 전에 drain하고 실패하면 절차를 중단해 원인을 해결합니다. 호환 kubelet/kubectl 패키지 설치, systemd 재로드, kubelet 재시작, Ready·워크로드 확인 후 관리자 클라이언트에서 uncordon합니다.
4. 워커도 kubeadm 업그레이드와 `kubeadm upgrade node` 후 drain/kubelet/검증/uncordon 절차를 수행합니다. 일반적인 전체 OS 업그레이드를 Kubernetes 버전별 업그레이드 절차 대신 사용하지 마세요.
명령은 명시한 노드 또는 관리자 클라이언트에서 실행합니다. 중첩된 `ssh` 명령을 나열한 것은 다중 노드 자동화 스크립트가 아닙니다.
### 업그레이드 검증
업그레이드 후 클러스터 상태 검증:
```bash
# 노드 버전 확인
kubectl get nodes
# 컴포넌트 상태 확인
kubectl get --raw='/readyz?verbose'
# 포드 상태 확인
kubectl get pods --all-namespaces
# 클러스터 기능 테스트
kubectl create deployment nginx --image=nginx
kubectl expose deployment nginx --port=80
kubectl get svc nginx
```
## 백업 및 복구
Kubernetes 클러스터의 백업 및 복구는 재해 복구 계획의 중요한 부분입니다.
다음 다이어그램은 Kubernetes 클러스터의 백업 및 복구 프로세스를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-6.html)
### etcd 백업
etcd는 Kubernetes 클러스터의 모든 상태 정보를 저장하므로 정기적인 백업이 중요합니다:
```bash
# etcd 스냅샷 생성
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
snapshot save /backup/etcd-snapshot-$(date +%Y-%m-%d-%H-%M-%S).db
# 스냅샷 상태 확인
etcdutl snapshot status --write-out=table /backup/etcd-snapshot-2023-01-01-12-00-00.db
```
### etcd 복구
etcd 스냅샷에서 복구:
자체 관리형 재해 복구는 배포판 운영 절차에 따라 모든 API 서버와 해당 etcd 프로세스를 먼저 중지합니다. kubelet만 중지해도 기존 정적 파드 컨테이너는 계속 실행됩니다. 호환되는 etcdutl로 새 디렉토리에 복원하고 검증 전까지 원본 데이터를 보관하세요. 아래는 단일 멤버 예시이며 HA 다중 멤버 복구 절차가 아닙니다:
```bash
etcdutl snapshot status "$SNAPSHOT_FILE" --write-out=table
etcdutl snapshot restore "$SNAPSHOT_FILE" \
--data-dir=/var/lib/etcd-restore \
--name=etcd-1 \
--initial-cluster=etcd-1=https://127.0.0.1:2380 \
--initial-cluster-token=restored-cluster \
--initial-advertise-peer-urls=https://127.0.0.1:2380 \
--bump-revision=1000000000 --mark-compacted
```
SNAPSHOT_FILE에는 검증한 스냅샷 경로를 지정하세요. HA는 같은 스냅샷을 각 멤버의 고유 이름·피어 URL과 동일한 전체 멤버 목록으로 복원합니다. 스냅샷 이후 변경을 초과하는 리비전 증가량을 선택하고 etcd 매니페스트·서비스의 경로·소유권·인증서를 맞춘 뒤 쿼럼·상태를 확인합니다. 이후 API 서버·컨트롤러를 시작하세요([공식 복구 문서](https://etcd.io/docs/v3.6/op-guide/recovery/)). EKS 관리형 컨트롤 플레인의 etcd는 사용자가 직접 복구하지 않습니다.
### 리소스 백업
다음 내보내기는 보호해야 할 인벤토리이며 완전한 이식형 복원 계획이 아닙니다. Secret이 포함되므로 제한된 권한·암호화가 필요하고 PV 데이터는 포함하지 않습니다. `umask 077`을 사용하고 각 명령 실패를 확인하세요. 복원 시 CRD·종속성 순서와 서버 소유 메타데이터 정리가 필요하며 `kubectl get all`은 일부 리소스 종류만 반환합니다.
Kubernetes 리소스를 YAML 파일로 백업:
```bash
# 목록 조회 가능한 리소스 내보내기 (민감한 Secret 포함)
set -eu
umask 077
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
mkdir -p /backup/resources/$ns
for resource in $(kubectl api-resources --verbs=list --namespaced=true -o name); do
kubectl get -n "$ns" "$resource" -o yaml > "/backup/resources/$ns/$resource.yaml"
done
done
# 클러스터 범위 리소스 백업
mkdir -p /backup/resources/cluster-scoped
for resource in $(kubectl api-resources --verbs=list --namespaced=false -o name); do
kubectl get "$resource" -o yaml > "/backup/resources/cluster-scoped/$resource.yaml"
done
```
### 백업 자동화
자체 관리형 kubeadm 전용 예시입니다. 호환 etcdctl이 포함된 검증 이미지로 교체하고 컨트롤 플레인 레이블·테인트·인증서 경로를 맞추며 백업 PVC를 먼저 생성하세요. 선택한 호스트는 표시한 루프백 주소에서 etcd에 접근 가능하고 PVC를 마운트할 수 있어야 합니다. EKS 관리형 컨트롤 플레인에서는 실행할 수 없습니다. 스냅샷 검증 후 보호된 외부 저장소로 복사해야 하며 클러스터 내부 PVC만으로 재해 복구가 되지는 않습니다.
백업 작업을 CronJob으로 자동화:
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: etcd-backup
namespace: kube-system
spec:
concurrencyPolicy: Forbid
schedule: "0 0 * * *" # 매일 자정에 실행
jobTemplate:
spec:
template:
spec:
hostNetwork: true
automountServiceAccountToken: false
nodeSelector:
node-role.kubernetes.io/control-plane: ""
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: etcd-backup
image: example.invalid/etcd-backup-tools:replace-me
command:
- /bin/sh
- -c
- |
set -eu
umask 077
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
--key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
snapshot save /backup/etcd-snapshot-$(date +%Y-%m-%d-%H-%M-%S).db
volumeMounts:
- name: etcd-ca
mountPath: /etc/kubernetes/pki/etcd/ca.crt
readOnly: true
- name: etcd-client-cert
mountPath: /etc/kubernetes/pki/etcd/healthcheck-client.crt
readOnly: true
- name: etcd-client-key
mountPath: /etc/kubernetes/pki/etcd/healthcheck-client.key
readOnly: true
- name: backup
mountPath: /backup
restartPolicy: OnFailure
volumes:
- name: etcd-ca
hostPath:
path: /etc/kubernetes/pki/etcd/ca.crt
type: File
- name: etcd-client-cert
hostPath:
path: /etc/kubernetes/pki/etcd/healthcheck-client.crt
type: File
- name: etcd-client-key
hostPath:
path: /etc/kubernetes/pki/etcd/healthcheck-client.key
type: File
- name: backup
persistentVolumeClaim:
claimName: etcd-backup-pvc
```
## 모니터링 및 로깅
효과적인 모니터링 및 로깅은 클러스터 관리의 핵심 요소입니다.
다음 다이어그램은 Kubernetes 클러스터의 모니터링 및 로깅 아키텍처를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-7.html)
### 모니터링 도구
Kubernetes 클러스터 모니터링을 위한 도구:
1. **Prometheus**: 메트릭 수집 및 저장
2. **Grafana**: 메트릭 시각화
3. **Alertmanager**: 알림 관리
4. **kube-state-metrics**: Kubernetes 객체 메트릭 생성
5. **metrics-server**: 리소스 사용량 메트릭 제공
#### Prometheus 및 Grafana 설치
Helm을 사용한 Prometheus 및 Grafana 설치:
```bash
# Helm 저장소 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# Prometheus 스택 설치
helm install prometheus prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace
```
#### 주요 모니터링 메트릭
모니터링해야 할 주요 메트릭:
1. **노드 메트릭**: CPU, 메모리, 디스크, 네트워크 사용량
2. **포드 메트릭**: CPU, 메모리 사용량, 재시작 횟수
3. **컨테이너 메트릭**: CPU, 메모리 사용량, 파일 시스템 사용량
4. **API 서버 메트릭**: 요청 지연 시간, 요청 수, 오류율
5. **etcd 메트릭**: 디스크 I/O, 리더 변경, 커밋 지연 시간
### 로깅 도구
Kubernetes 클러스터 로깅을 위한 도구:
1. **Elasticsearch**: 로그 저장 및 검색
2. **Fluentd/Fluent Bit**: 로그 수집 및 전달
3. **Kibana**: 로그 시각화
4. **Loki**: 로그 집계 시스템
5. **Grafana**: 로그 시각화
#### EFK(Elasticsearch, Fluentd, Kibana) 스택 설치
Helm을 사용한 EFK 스택 설치:
독립 Elastic Stack Helm 차트 저장소는 보관 상태입니다. 유지 관리되는 배포에는 Elastic Cloud on Kubernetes(ECK)를 사용하고 Elasticsearch·Kibana 리소스와 호환 로그 수집기를 별도로 정의하세요. 오퍼레이터 설치만으로 EFK 스택이 생성되지는 않습니다:
```bash
helm repo add elastic https://helm.elastic.co
helm upgrade --install elastic-operator elastic/eck-operator \
--namespace elastic-system --create-namespace \
--version "${ECK_CHART_VERSION:?Select a supported ECK chart version}"
```
대시보드는 ClusterIP·인증된 접근으로 보호하고 [ECK 문서](https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/install-using-helm-chart)에 따라 스토리지·TLS·자격 증명·수집기 파서·RBAC를 구성하세요.
#### 로그 수집 구성
이 예시는 Docker JSON을 가정하지 않고 CRI 로그 형식을 파싱합니다. 수집기 이미지에 메타데이터·출력 플러그인이 있어야 하며 노드 로그·쓰기 가능한 위치 파일 저장소를 마운트하고 제한된 메타데이터 RBAC를 부여해야 합니다. 실제 Elasticsearch의 TLS·인증을 설정하며 예시 호스트 이름만으로 ECK 통합이 완성되지는 않습니다. 부분·멀티라인 레코드는 선택한 수집기에 맞게 처리하세요.
Fluentd 구성 예시:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: fluentd-config
namespace: logging
data:
fluent.conf: |
@type tail
path /var/log/containers/*.log
pos_file /var/log/fluentd-containers.log.pos
tag kubernetes.*
read_from_head true
@type regexp
expression /^(?
@type kubernetes_metadata
kubernetes_url https://kubernetes.default.svc
bearer_token_file /var/run/secrets/kubernetes.io/serviceaccount/token
ca_file /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
@type elasticsearch
host elasticsearch-master
port 9200
logstash_format true
logstash_prefix k8s
```
## 문제 해결
Kubernetes 클러스터 문제 해결은 클러스터 관리의 중요한 부분입니다.
### 포드 문제 해결
포드 문제 해결을 위한 명령어:
```bash
# 포드 상태 확인
kubectl get pod -o wide
# 포드 상세 정보 확인
kubectl describe pod
# 포드 로그 확인
kubectl logs
kubectl logs -c # 다중 컨테이너 포드의 경우
kubectl logs --previous # 이전 컨테이너의 로그
# 포드 내 명령 실행
kubectl exec -it -- /bin/sh
```
### 노드 문제 해결
노드 문제 해결을 위한 명령어:
```bash
# 노드 상태 확인
kubectl get node -o wide
# 노드 상세 정보 확인
kubectl describe node
# 노드 리소스 사용량 확인
kubectl top node
# SSH로 노드에 접속
ssh
# 노드 시스템 로그 확인
journalctl -u kubelet
# 노드 리소스 사용량 확인
top
df -h
free -m
```
### 네트워킹 문제 해결
네트워킹 문제 해결을 위한 명령어:
```bash
# 서비스 상태 확인
kubectl get svc
# 서비스 상세 정보 확인
kubectl describe svc
# 엔드포인트 확인
kubectl get endpointslices -l kubernetes.io/service-name=
# DNS 확인
kubectl run -it --rm --restart=Never busybox --image=busybox -- nslookup
# 네트워크 연결 테스트
kubectl run -it --rm --restart=Never busybox --image=busybox -- wget -O- :
# 네트워크 정책 확인
kubectl get networkpolicy
kubectl describe networkpolicy
```
### 컨트롤 플레인 문제 해결
컨트롤 플레인 문제 해결을 위한 명령어:
```bash
# 컴포넌트 상태 확인
kubectl get --raw='/readyz?verbose'
# API 서버 로그 확인
kubectl logs -n kube-system kube-apiserver-
# 컨트롤러 매니저 로그 확인
kubectl logs -n kube-system kube-controller-manager-
# 스케줄러 로그 확인
kubectl logs -n kube-system kube-scheduler-
# etcd 로그 확인
kubectl logs -n kube-system etcd-
```
## Amazon EKS 클러스터 관리
Amazon EKS는 관리형 Kubernetes 서비스로, 클러스터 관리의 많은 부분을 자동화합니다.
다음 다이어그램은 Amazon EKS 클러스터 아키텍처와 관리 구성요소를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-8.html)
### EKS 클러스터 구성
엔드포인트 변경 전에 실제 관리자 CIDR을 정하고 프라이빗 접근 가능 여부도 확인하세요. 버전 업그레이드는 호환 애드온·노드를 준비한 다음 지원되는 마이너 버전으로 수행하며 다운그레이드나 마이너 건너뛰기에 사용하면 안 됩니다. 반환된 업데이트 ID를 확인하고 실패하면 후속 변경을 중단하세요.
EKS 클러스터 구성 관리:
```bash
# EKS 클러스터 정보 확인
aws eks describe-cluster --name my-cluster
# EKS 클러스터 업데이트
aws eks update-cluster-config \
--name my-cluster \
--resources-vpc-config "endpointPublicAccess=true,endpointPrivateAccess=true,publicAccessCidrs=${ADMIN_CIDR:?Set an approved administrator public CIDR}"
# EKS 클러스터 버전 업데이트
aws eks update-cluster-version \
--name my-cluster \
--kubernetes-version "${TARGET_VERSION:?Select the next EKS-supported minor version}"
```
### EKS 노드 그룹 관리
EKS 노드 그룹 관리:
```bash
# 노드 그룹 정보 확인
aws eks describe-nodegroup \
--cluster-name my-cluster \
--nodegroup-name my-nodegroup
# 노드 그룹 스케일링
aws eks update-nodegroup-config \
--cluster-name my-cluster \
--nodegroup-name my-nodegroup \
--scaling-config minSize=2,maxSize=10,desiredSize=5
# 노드 그룹 업데이트
aws eks update-nodegroup-version \
--cluster-name my-cluster \
--nodegroup-name my-nodegroup
```
### EKS 추가 기능 관리
`aws eks describe-cluster --name my-cluster --query cluster.version --output text`로 현재 버전을 확인하고 애드온 검색에 사용한 뒤 호환 버전을 고정하세요. create/update 전에 기존 설정·IAM을 검토하고 이미 관리 중인 애드온을 다시 생성하지 마세요. 삭제 예시는 `--preserve`로 CNI 실행을 유지하면서 EKS 관리만 제거하며 실행 중 네트워킹 삭제는 별도의 중단 작업입니다.
EKS 추가 기능 관리:
```bash
# 사용 가능한 추가 기능 확인
aws eks describe-addon-versions --addon-name vpc-cni \
--kubernetes-version "${CLUSTER_VERSION:?Set the actual cluster version}"
# 추가 기능 설치
aws eks create-addon \
--cluster-name my-cluster \
--addon-name vpc-cni \
--addon-version "${CNI_ADDON_VERSION:?Select a compatible pinned VPC CNI add-on version}"
# 추가 기능 업데이트
aws eks update-addon \
--cluster-name my-cluster \
--addon-name vpc-cni \
--addon-version "${CNI_ADDON_VERSION:?Select a compatible pinned VPC CNI add-on version}"
# 추가 기능 삭제
aws eks delete-addon \
--cluster-name my-cluster \
--addon-name vpc-cni --preserve
```
### EKS 클러스터 업그레이드
EKS 클러스터 업그레이드 과정:
1. **컨트롤 플레인 업그레이드**:
```bash
aws eks update-cluster-version \
--name my-cluster \
--kubernetes-version "${TARGET_VERSION:?Select the next EKS-supported minor version}"
```
2. **추가 기능 업그레이드**:
```bash
aws eks update-addon \
--cluster-name my-cluster \
--addon-name vpc-cni \
--addon-version "${CNI_ADDON_VERSION:?Select a compatible pinned VPC CNI add-on version}"
```
3. **노드 그룹 업그레이드**:
```bash
aws eks update-nodegroup-version \
--cluster-name my-cluster \
--nodegroup-name my-nodegroup
```
### EKS 클러스터 모니터링
컨트롤 플레인 로깅은 api/audit/authenticator/controllerManager/scheduler 로그를 내보냅니다. Container Insights에는 CloudWatch 에이전트·애드온과 제한된 텔레메트리 IAM 권한이 필요하며 update-cluster-logging으로 활성화되지 않습니다. CloudWatch 관측 애드온은 Prometheus/Grafana가 아닌 CloudWatch·Fluent Bit 구성 요소를 설치합니다([공식 설치 문서](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/install-CloudWatch-Observability-EKS-addon.html)).
EKS 클러스터 모니터링 도구:
1. **Amazon CloudWatch**: 메트릭, 로그, 알림
2. **AWS CloudTrail**: API 호출 로깅
3. **Amazon Managed Grafana**: 메트릭 시각화
4. **Amazon Managed Service for Prometheus**: 메트릭 수집 및 저장
EKS 컨트롤 플레인 로깅 활성화:
```bash
# EKS 컨트롤 플레인 로그 활성화
eksctl utils update-cluster-logging \
--enable-types all \
--cluster my-cluster \
--approve
```
## 클러스터 관리 모범 사례
Kubernetes 및 EKS 클러스터 관리를 위한 모범 사례:
### 클러스터 구성 모범 사례
1. **Infrastructure as Code(IaC)**: Terraform, AWS CDK, eksctl 등을 사용하여 클러스터 구성 관리
2. **버전 관리**: 클러스터 구성을 버전 관리 시스템에 저장
3. **다중 환경**: 개발, 스테이징, 프로덕션 환경 분리
4. **네트워크 분리**: 적절한 네트워크 분리 및 보안 그룹 구성
5. **최소 권한 원칙**: 필요한 최소한의 권한만 부여
### 운영 모범 사례
1. **정기적인 백업**: etcd 및 중요 리소스 정기 백업
2. **모니터링 및 알림**: 포괄적인 모니터링 및 알림 시스템 구축
3. **로깅 중앙화**: 로그 중앙화 및 분석
4. **자동화**: 반복 작업 자동화
5. **재해 복구 계획**: 명확한 재해 복구 계획 수립 및 테스트
### 보안 모범 사례
1. **정기적인 업데이트**: 클러스터 및 노드 정기 업데이트
2. **네트워크 정책**: 적절한 네트워크 정책 구성
3. **암호화**: 저장 데이터 및 전송 중 데이터 암호화
4. **보안 컨텍스트**: 적절한 보안 컨텍스트 구성
5. **이미지 스캐닝**: 컨테이너 이미지 취약점 스캐닝
### 리소스 관리 모범 사례
1. **리소스 요청 및 제한**: 모든 포드에 적절한 리소스 요청 및 제한 설정
2. **네임스페이스 분리**: 워크로드를 네임스페이스로 분리
3. **리소스 쿼터**: 네임스페이스별 리소스 쿼터 설정
4. **HPA 및 VPA**: 자동 스케일링 구성
5. **노드 어피니티 및 테인트**: 워크로드 배치 최적화
### EKS 특화 모범 사례
1. **관리형 노드 그룹**: 가능한 경우 관리형 노드 그룹 사용
2. **Fargate**: 서버리스 워크로드에 Fargate 사용
3. **EKS 추가 기능**: 공식 EKS 추가 기능 사용
4. **IAM 역할 서비스 계정(IRSA)**: 포드별 IAM 권한 관리
5. **VPC CNI 사용자 지정**: 네트워킹 요구 사항에 맞게 VPC CNI 구성
## 결론
Kubernetes 클러스터 관리는 클러스터의 안정성, 보안, 성능을 유지하는 데 중요한 역할을 합니다. 이 장에서는 클러스터 구성요소 관리, 리소스 관리, 네트워킹, 인증 및 권한 관리, 업그레이드, 백업 및 복구, 모니터링 및 로깅, 문제 해결 등 클러스터 관리의 다양한 측면을 다루었습니다.
Amazon EKS를 사용하면 Kubernetes 컨트롤 플레인 관리의 복잡성을 줄이고, AWS 서비스와의 통합을 통해 클러스터 관리를 간소화할 수 있습니다. 그러나 효과적인 클러스터 관리를 위해서는 여전히 Kubernetes의 기본 개념과 모범 사례를 이해하는 것이 중요합니다.
클러스터 관리는 지속적인 과정이며, 클러스터의 요구 사항과 워크로드 특성에 따라 지속적으로 조정해야 합니다. 모니터링 도구를 활용하여 클러스터 상태를 추적하고, 자동화를 통해 반복 작업을 최소화하며, 모범 사례를 따라 클러스터의 안정성과 보안을 유지하는 것이 중요합니다.
## 리소스 관리
Kubernetes에서 리소스 관리는 클러스터의 효율적인 운영을 위해 중요합니다. 이는 CPU, 메모리, 스토리지와 같은 컴퓨팅 리소스와 네임스페이스, 쿼터와 같은 논리적 리소스를 포함합니다.
### 네임스페이스 관리
네임스페이스는 클러스터 내에서 리소스를 논리적으로 분리하는 방법입니다.
```bash
# 네임스페이스 생성
kubectl create namespace admin-demo
# 특정 네임스페이스의 리소스 확인
kubectl get all -n admin-demo
# 실습용 네임스페이스만 정리 (안의 리소스도 삭제됨)
kubectl delete namespace admin-demo
```
### 리소스 쿼터 관리
리소스 쿼터는 네임스페이스별로 리소스 사용량을 제한합니다.
```yaml
# resource-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-quota
namespace: production
spec:
hard:
pods: "10"
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
```
```bash
# 리소스 쿼터 적용
kubectl apply -f resource-quota.yaml
# 리소스 쿼터 확인
kubectl describe resourcequota compute-quota -n production
```
### 리소스 요청 및 제한 설정
파드 수준에서 리소스 요청과 제한을 설정하여 리소스 사용량을 관리할 수 있습니다.
```yaml
# resource-limits.yaml
apiVersion: v1
kind: Pod
metadata:
name: frontend
spec:
containers:
- name: app
image: nginx
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
```
### 리소스 모니터링
```bash
# 노드 리소스 사용량 확인
kubectl top nodes
# 파드 리소스 사용량 확인
kubectl top pods -A
# 특정 네임스페이스의 파드 리소스 사용량 확인
kubectl top pods -n production
```
### 리소스 관리 모범 사례
1. 모든 컨테이너에 리소스 요청과 제한 설정
2. 네임스페이스별 리소스 쿼터 설정
3. 수평적 파드 자동 확장(HPA) 구성
4. 클러스터 자동 확장 설정
5. 정기적인 리소스 사용량 모니터링 및 최적화
## 클러스터 네트워킹
Kubernetes 클러스터 네트워킹은 파드 간 통신, 서비스 디스커버리, 외부 접근 등을 관리합니다.
### 네트워크 아키텍처

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-9.html)
### CNI 플러그인 관리
CNI(Container Network Interface) 플러그인은 Kubernetes 클러스터의 네트워킹을 담당합니다.
CNI 하나 또는 문서화된 체이닝·마이그레이션 구성을 선택하세요. Calico·Flannel·Cilium 대안을 같은 실행 중 클러스터에 순서대로 설치하면 안 됩니다. 지원되는 버전을 고정하고 제공자별 지침을 따르며 EKS에서는 [네트워킹 장](https://www.atomai.click/kubernetes-docs/llms/ko/core/03-services-networking.md)의 VPC CNI 또는 대체 CNI 전환 절차를 사용하세요.
```bash
# 변경 전에 설치된 네트워킹 구성 요소 확인
kubectl get daemonsets -A
kubectl get pods -A -l k8s-app=calico-node
```
### CNI 플러그인 비교
| CNI 플러그인 | 네트워크 모델 | 네트워크 정책 지원 | 성능 | 특징 |
|-------------|-------------|-----------------|------|------|
| **Calico** | BGP | 예 | 높음 | 네트워크 정책에 강점, 라우팅 기반 |
| **Flannel** | VXLAN/호스트-게이트웨이 | 아니오 | 중간 | 간단한 설정, 제한된 기능 |
| **Cilium** | eBPF | 예 | 매우 높음 | L3-L7 정책, 고성능 |
| **Weave Net** | VXLAN | 예 | 중간 | 암호화 지원, 멀티클러스터 |
| **AWS VPC CNI** | AWS VPC | 지원 버전·구성에서 가능 | 워크로드에 따라 다름 | EKS 네이티브 통합 |
### 네트워크 문제 해결
```bash
# 파드 네트워크 연결 테스트
kubectl run -it --rm network-test --image=busybox -- sh
# 컨테이너 내에서
ping
traceroute
wget -O-
# DNS 문제 해결
kubectl run -it --rm dns-test --image=busybox -- sh
# 컨테이너 내에서
nslookup kubernetes.default.svc.cluster.local
cat /etc/resolv.conf
# 서비스 엔드포인트 확인
kubectl get endpointslices -l kubernetes.io/service-name=
# 네트워크 정책 확인
kubectl describe networkpolicy -n
```
## 인증 및 권한 관리
Kubernetes의 인증 및 권한 관리는 클러스터 보안의 핵심 요소입니다. RBAC(Role-Based Access Control)을 통해 사용자와 서비스 계정의 권한을 관리합니다.
### 인증 방법
Kubernetes는 다양한 인증 방법을 지원합니다:
1. **X.509 인증서**: 클라이언트 인증서를 사용한 인증
2. **서비스 계정 토큰**: 파드 내에서 API 서버 접근에 사용
3. **OpenID Connect(OIDC)**: 외부 ID 제공자와 통합
4. **웹훅 토큰 인증**: 외부 인증 서비스와 통합
5. **인증 프록시**: 프록시를 통한 인증
### RBAC 구성
```yaml
# role.yaml - 네임스페이스 범위의 역할
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
```
```yaml
# rolebinding.yaml - 역할과 사용자 연결
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
- kind: User
name: jane
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
```
```yaml
# clusterrole.yaml - 클러스터 범위의 역할
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: namespace-reader
rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "watch", "list"]
```
```yaml
# clusterrolebinding.yaml - 클러스터 역할과 사용자 연결
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: read-namespaces-global
subjects:
- kind: Group
name: namespace-viewers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: namespace-reader
apiGroup: rbac.authorization.k8s.io
```
### 사용자 인증서 생성
자체 관리형 클라이언트 인증서는 CSR을 제출하고 권한 있는 승인자가 요청한 사용자·그룹을 검증해야 합니다. 클러스터 CA 개인 키를 배포하지 마세요. EKS 사용자 접근에는 IAM·액세스 항목을 사용합니다.
```bash
umask 077
openssl genrsa -out jane.key 2048
openssl req -new -key jane.key -out jane.csr -subj "/CN=jane/O=dev"
cat < jane.crt
kubectl config set-credentials jane --client-certificate=jane.crt --client-key=jane.key
kubectl config set-context jane-context --cluster=kubernetes --user=jane
```
### 서비스 계정 관리
```bash
# 서비스 계정 생성
kubectl create serviceaccount app-service-account
# 서비스 계정에 역할 바인딩
kubectl create rolebinding app-service-account-binding \
--role=pod-reader \
--serviceaccount=default:app-service-account
# ServiceAccount 메타데이터 확인 (프로젝션 토큰은 여기에 표시되지 않음)
kubectl describe serviceaccount app-service-account
```
### 권한 검증
```bash
# 사용자 권한 확인
kubectl auth can-i get pods --as jane
# 특정 네임스페이스에서 권한 확인
kubectl auth can-i create deployments --as jane --namespace production
```
## 클러스터 업그레이드
Kubernetes 클러스터 업그레이드는 새로운 기능, 보안 패치, 버그 수정을 적용하기 위해 필요합니다. 업그레이드는 신중하게 계획하고 실행해야 합니다.
### 업그레이드 계획

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-10.html)
### 업그레이드 전략 비교
| 전략 | 설명 | 장점 | 단점 | 적합한 환경 |
|------|------|------|------|------------|
| **인플레이스 업그레이드** | 기존 클러스터를 직접 업그레이드 | 리소스 효율적, 간단한 절차 | 롤백 복잡, 잠재적 다운타임 | 개발, 테스트 환경 |
| **블루/그린 배포** | 새 버전의 클러스터 생성 후 전환 | 안전한 롤백, 검증 가능 | 리소스 중복, 비용 증가 | 프로덕션 환경 |
| **카나리 배포** | 일부 워크로드만 새 클러스터로 이동 | 점진적 검증, 위험 감소 | 복잡한 관리, 이중 운영 | 중요 프로덕션 환경 |
### kubeadm을 사용한 업그레이드
[해당 버전의 kubeadm 업그레이드 절차](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/)를 따르세요. 대상 마이너 버전의 pkgs.k8s.io 저장소에서 실제 패키지 버전을 선택하고 한 번에 한 마이너 버전만 업그레이드합니다.
1. etcd 백업과 워크로드·애드온 호환성을 확인합니다. 첫 컨트롤 플레인 노드에서 kubeadm을 먼저 업그레이드한 뒤 `kubeadm upgrade plan`, `kubeadm upgrade apply `을 실행합니다.
2. 추가 컨트롤 플레인 노드는 kubeadm 업그레이드 후 `kubeadm upgrade node`를 실행합니다.
3. 각 노드의 kubelet 업그레이드 전에 drain하고 실패하면 절차를 중단해 원인을 해결합니다. 호환 kubelet/kubectl 패키지 설치, systemd 재로드, kubelet 재시작, Ready·워크로드 확인 후 관리자 클라이언트에서 uncordon합니다.
4. 워커도 kubeadm 업그레이드와 `kubeadm upgrade node` 후 drain/kubelet/검증/uncordon 절차를 수행합니다. 일반적인 전체 OS 업그레이드를 Kubernetes 버전별 업그레이드 절차 대신 사용하지 마세요.
명령은 명시한 노드 또는 관리자 클라이언트에서 실행합니다. 중첩된 `ssh` 명령을 나열한 것은 다중 노드 자동화 스크립트가 아닙니다.
### 업그레이드 후 검증
```bash
# 클러스터 버전 확인
kubectl version
# 노드 버전 확인
kubectl get nodes
# 컴포넌트 상태 확인
kubectl get --raw='/readyz?verbose'
# 워크로드 상태 확인
kubectl get pods -A
```
## 백업 및 복구
Kubernetes 클러스터의 백업 및 복구는 재해 복구 계획의 중요한 부분입니다. 주요 백업 대상은 etcd 데이터베이스, 영구 볼륨 데이터, 그리고 Kubernetes 리소스 정의입니다.
### etcd 백업 및 복구
etcd는 클러스터의 모든 상태 정보를 저장하는 핵심 구성 요소입니다.
자체 관리형 재해 복구는 배포판 운영 절차에 따라 모든 API 서버와 해당 etcd 프로세스를 먼저 중지합니다. kubelet만 중지해도 기존 정적 파드 컨테이너는 계속 실행됩니다. 호환되는 etcdutl로 새 디렉토리에 복원하고 검증 전까지 원본 데이터를 보관하세요. 아래는 단일 멤버 예시이며 HA 다중 멤버 복구 절차가 아닙니다:
```bash
etcdutl snapshot status "$SNAPSHOT_FILE" --write-out=table
etcdutl snapshot restore "$SNAPSHOT_FILE" \
--data-dir=/var/lib/etcd-restore \
--name=etcd-1 \
--initial-cluster=etcd-1=https://127.0.0.1:2380 \
--initial-cluster-token=restored-cluster \
--initial-advertise-peer-urls=https://127.0.0.1:2380 \
--bump-revision=1000000000 --mark-compacted
```
SNAPSHOT_FILE에는 검증한 스냅샷 경로를 지정하세요. HA는 같은 스냅샷을 각 멤버의 고유 이름·피어 URL과 동일한 전체 멤버 목록으로 복원합니다. 스냅샷 이후 변경을 초과하는 리비전 증가량을 선택하고 etcd 매니페스트·서비스의 경로·소유권·인증서를 맞춘 뒤 쿼럼·상태를 확인합니다. 이후 API 서버·컨트롤러를 시작하세요([공식 복구 문서](https://etcd.io/docs/v3.6/op-guide/recovery/)). EKS 관리형 컨트롤 플레인의 etcd는 사용자가 직접 복구하지 않습니다.
### Kubernetes 리소스 백업
```bash
# 선택한 리소스 내보내기이며 전체 클러스터 백업이 아님
set -eu
umask 077
mkdir -p /backup/resources/$(date +%Y-%m-%d)
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
kubectl -n $ns get all -o yaml > /backup/resources/$(date +%Y-%m-%d)/$ns-all.yaml
done
# 특정 리소스 유형 백업
for resource in deployments services configmaps secrets; do
kubectl get $resource -A -o yaml > /backup/resources/$(date +%Y-%m-%d)/$resource.yaml
done
```
### Velero를 사용한 백업 및 복구
공식 호환성 표로 Velero/AWS 플러그인 버전을 선택하세요. IRSA 예시는 클러스터 OIDC 제공자와 velero ServiceAccount를 신뢰하는 제한된 역할이 구성되어 있다고 가정합니다. 설치 전에 백업 버킷, 볼륨 스냅샷·파일 백업 지원, 암호화·복원 권한을 준비해야 하며 모든 PVC가 자동 보호되는 것은 아닙니다.
Velero는 Kubernetes 클러스터 리소스와 영구 볼륨을 백업하고 복구하는 도구입니다.
```bash
# Velero 설치 (AWS S3 백업 스토리지 사용)
velero install \
--provider aws \
--plugins "${VELERO_AWS_PLUGIN_IMAGE:?Select a plugin compatible with your Velero release}" \
--bucket velero-backup \
--backup-location-config region=us-west-2 \
--snapshot-location-config region=us-west-2 \
--no-secret \
--sa-annotations "eks.amazonaws.com/role-arn=${VELERO_ROLE_ARN:?Set the preconfigured IRSA role ARN}"
# 전체 클러스터 백업
velero backup create full-cluster-backup --include-namespaces '*'
# 특정 네임스페이스 백업
velero backup create production-backup --include-namespaces production
# 백업 상태 확인
velero backup describe full-cluster-backup
# 백업에서 복구
velero restore create --from-backup full-cluster-backup
```
### 백업 전략 비교
| 백업 방법 | 백업 대상 | 장점 | 단점 | 복구 시간 |
|----------|----------|------|------|----------|
| **etcd 스냅샷** | 클러스터 상태 | 내장 기능, 완전한 상태 보존 | 볼륨 데이터 미포함, 수동 프로세스 | 중간 |
| **리소스 YAML 백업** | Kubernetes 객체 | 간단한 구현, 선택적 복원 | 볼륨 데이터 미포함, 관계 복잡성 | 느림 |
| **Velero** | 리소스 및 볼륨 | 자동화, 스케줄링, 볼륨 스냅샷 | 추가 도구 설치 필요 | 빠름 |
| **클라우드 제공자 스냅샷** | 지원되는 디스크·파일시스템 | 백엔드 복구 지점 | Kubernetes/EKS 클러스터 전체를 캡처하지 않음 | 데이터·백엔드에 따라 다름 |
## 모니터링 및 로깅
효과적인 클러스터 관리를 위해서는 포괄적인 모니터링 및 로깅 시스템이 필요합니다. 이를 통해 문제를 조기에 발견하고 해결할 수 있습니다.
### 모니터링 아키텍처

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-11.html)
### Prometheus 및 Grafana 설치
```bash
# Helm을 사용한 Prometheus 및 Grafana 설치
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install prometheus prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
--set grafana.enabled=true \
--set prometheus.service.type=ClusterIP
# 서비스 확인
kubectl get svc -n monitoring
# Grafana 접근 (포트 포워딩 사용)
kubectl port-forward svc/prometheus-grafana 3000:80 -n monitoring
# 구성된 Grafana Secret에서 자격 증명을 확인하고 공개된 기본 비밀번호를 가정하지 마세요
```
### EFK 스택 설치 (Elasticsearch, Fluentd, Kibana)
독립 Elastic Stack Helm 차트 저장소는 보관 상태입니다. 유지 관리되는 배포에는 Elastic Cloud on Kubernetes(ECK)를 사용하고 Elasticsearch·Kibana 리소스와 호환 로그 수집기를 별도로 정의하세요. 오퍼레이터 설치만으로 EFK 스택이 생성되지는 않습니다:
```bash
helm repo add elastic https://helm.elastic.co
helm upgrade --install elastic-operator elastic/eck-operator \
--namespace elastic-system --create-namespace \
--version "${ECK_CHART_VERSION:?Select a supported ECK chart version}"
```
대시보드는 ClusterIP·인증된 접근으로 보호하고 [ECK 문서](https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s/install-using-helm-chart)에 따라 스토리지·TLS·자격 증명·수집기 파서·RBAC를 구성하세요.
### 주요 모니터링 메트릭
| 메트릭 유형 | 설명 | 주요 메트릭 | 모니터링 도구 |
|------------|------|------------|--------------|
| **노드 메트릭** | 노드 수준 리소스 사용량 | CPU, 메모리, 디스크, 네트워크 | node-exporter, Prometheus |
| **파드 메트릭** | 컨테이너 리소스 사용량 | CPU, 메모리 사용량, 제한 | cAdvisor, Prometheus |
| **클러스터 메트릭** | 클러스터 상태 및 리소스 | 파드 수, 노드·객체 상태, 원하는·현재 복제본 수 | kube-state-metrics |
| **애플리케이션 메트릭** | 사용자 정의 애플리케이션 메트릭 | 요청 수, 지연 시간, 오류율 | Prometheus 클라이언트 라이브러리 |
### 로그 수집 및 분석
```bash
# 특정 파드의 로그 확인
kubectl logs -n
# 이전 인스턴스의 로그 확인
kubectl logs -n --previous
# 특정 컨테이너의 로그 확인 (다중 컨테이너 파드)
kubectl logs -c -n
# 로그 스트리밍
kubectl logs -f -n
# 모든 파드의 로그 확인 (레이블 선택자 사용)
kubectl logs -l app=nginx -n
```
### 알림 구성
Prometheus Alertmanager를 사용하여 알림을 구성할 수 있습니다:
monitoring에 `url` 키를 가진 보호된 `slack-webhook` Secret을 생성한 뒤 다음 Helm values를 기존 kube-prometheus-stack 릴리스 설정에 병합하세요. 웹훅 URL은 Git에 저장하지 않습니다. 독립 ConfigMap만 생성해서는 오퍼레이터가 자동으로 사용하지 않습니다.
```yaml
# alertmanager-values.yaml
alertmanager:
alertmanagerSpec:
secrets:
- slack-webhook
config:
global:
resolve_timeout: 5m
slack_api_url_file: /etc/alertmanager/secrets/slack-webhook/url
route:
receiver: slack-notifications
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
group_by: [alertname, cluster, service]
receivers:
- name: slack-notifications
slack_configs:
- channel: '#alerts'
send_resolved: true
title: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}'
text: '{{ range .Alerts }}{{ .Annotations.description }}{{ end }}'
```
업그레이드 시 현재 차트 버전과 다른 설정을 유지하고 알림에 의존하기 전에 Alertmanager 재로드·상태를 확인하세요.
## 문제 해결
Kubernetes 클러스터 문제 해결은 시스템 관리자와 운영자에게 중요한 기술입니다. 효과적인 문제 해결을 위해 체계적인 접근 방식이 필요합니다.
### 문제 해결 방법론

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-12.html)
### 일반적인 문제 및 해결 방법
| 문제 유형 | 증상 | 진단 명령어 | 일반적인 해결 방법 |
|----------|------|------------|-----------------|
| **파드가 시작되지 않음** | 파드가 Pending 또는 ContainerCreating 상태 | `kubectl describe pod ` | 리소스 제약 확인, 이미지 가용성 확인, 볼륨 마운트 확인 |
| **서비스 연결 문제** | 서비스를 통해 파드에 접근할 수 없음 | `kubectl describe svc `, `kubectl get endpointslices -l kubernetes.io/service-name=` | 레이블 선택자 확인, 파드 상태 확인, 네트워크 정책 확인 |
| **노드 문제** | 노드가 NotReady 상태 | `kubectl describe node `, `kubectl get events` | kubelet 상태 확인, 시스템 리소스 확인, 네트워크 연결 확인 |
| **DNS 문제** | 서비스 이름으로 연결할 수 없음 | `kubectl exec -it -- nslookup kubernetes.default` | CoreDNS 파드 확인, kube-dns 서비스 확인, 네트워크 정책 확인 |
| **인증·인가 문제** | API 서버 접근 거부 | `kubectl auth can-i ` | RBAC 설정 확인, 인증서 유효성 확인, 서비스 계정 확인 |
### 파드 문제 해결
```bash
# 파드 상태 확인
kubectl get pod -o wide
# 파드 세부 정보 확인
kubectl describe pod
# 파드 로그 확인
kubectl logs
kubectl logs --previous # 이전 컨테이너의 로그
# 파드 내 명령 실행
kubectl exec -it -- /bin/sh
# 파드 이벤트 확인
kubectl get events --field-selector involvedObject.name=
```
### 노드 문제 해결
```bash
# 노드 상태 확인
kubectl get nodes
kubectl describe node
# 노드 리소스 사용량 확인
kubectl top node
# 노드 시스템 로그 확인 (SSH 접속 필요)
ssh 'sudo journalctl -u kubelet'
# kubelet 상태 확인 (SSH 접속 필요)
ssh 'sudo systemctl status kubelet'
```
### 네트워킹 문제 해결
```bash
# 서비스 및 엔드포인트 확인
kubectl get svc
kubectl get endpointslices -l kubernetes.io/service-name=
# DNS 문제 해결
kubectl run -it --rm dns-test --image=busybox -- sh
# 컨테이너 내에서
nslookup kubernetes.default.svc.cluster.local
cat /etc/resolv.conf
# 네트워크 연결 테스트
kubectl run -it --rm network-test --image=nicolaka/netshoot -- sh
# 컨테이너 내에서
ping
traceroute
curl :
```
## Amazon EKS 클러스터 관리
Amazon EKS(Elastic Kubernetes Service)는 AWS에서 관리하는 Kubernetes 서비스로, 컨트롤 플레인 관리를 AWS가 담당합니다. 노드 관리 책임은 관리형 노드 그룹, Auto Mode, Fargate, 자체 관리형 컴퓨팅에 따라 달라지며 워크로드 보안·구성은 고객 책임입니다.
### EKS 클러스터 아키텍처

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-09-cluster-administration-13.html)
### EKS 클러스터 생성
```bash
# eksctl을 사용한 클러스터 생성
eksctl create cluster \
--name my-cluster \
--version 1.36 \
--region us-west-2 \
--nodegroup-name standard-workers \
--node-type t3.medium \
--nodes 3 \
--nodes-min 1 \
--nodes-max 5 \
--managed
# 대안: AWS CLI로 컨트롤 플레인 생성 (위 eksctl 예시 이후 중복 실행하지 않음)
aws eks create-cluster \
--name my-cluster \
--role-arn arn:aws:iam::123456789012:role/eks-cluster-role \
--kubernetes-version 1.36 \
--resources-vpc-config "subnetIds=${EKS_SUBNET_IDS:?Set two or more appropriate subnets},securityGroupIds=${EKS_SECURITY_GROUP_ID:?Set the intended security group},endpointPrivateAccess=true,endpointPublicAccess=true,publicAccessCidrs=${ADMIN_CIDR:?Set an approved administrator public CIDR}"
```
### 노드 그룹 관리
```bash
# 관리형 노드 그룹 생성
eksctl create nodegroup \
--cluster my-cluster \
--region us-west-2 \
--name my-nodegroup \
--node-type t3.medium \
--nodes 3 \
--nodes-min 1 \
--nodes-max 5
# 노드 그룹 확장
eksctl scale nodegroup \
--cluster my-cluster \
--name my-nodegroup \
--nodes 5 \
--region us-west-2
# 노드 그룹 업데이트
aws eks update-nodegroup-version \
--cluster-name my-cluster \
--nodegroup-name my-nodegroup \
--region us-west-2
```
### EKS 클러스터 업그레이드
```bash
# 클러스터 버전 확인
aws eks describe-cluster --name my-cluster --query "cluster.version"
# 클러스터 컨트롤 플레인 업그레이드
aws eks update-cluster-version \
--name my-cluster \
--kubernetes-version "${TARGET_VERSION:?Select the next EKS-supported minor version}"
# 관리형 노드 그룹 업그레이드
aws eks update-nodegroup-version \
--cluster-name my-cluster \
--nodegroup-name my-nodegroup
```
### EKS 클러스터 인증 및 권한
클러스터 인증 모드는 `API` 또는 `API_AND_CONFIG_MAP`이어야 합니다. 기존 관리자·노드 접근을 보존하며 레거시 aws-auth 매핑 전환을 계획하세요. 다음은 조회용 역할에 default 네임스페이스만 허용하는 예시입니다:
```bash
aws eks describe-cluster --name my-cluster --query cluster.accessConfig.authenticationMode
aws eks create-access-entry --cluster-name my-cluster \
--principal-arn arn:aws:iam::123456789012:role/cluster-viewer --type STANDARD
aws eks associate-access-policy --cluster-name my-cluster \
--principal-arn arn:aws:iam::123456789012:role/cluster-viewer \
--policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy \
--access-scope type=namespace,namespaces=default
```
[EKS 액세스 항목 문서](https://docs.aws.amazon.com/eks/latest/userguide/access-entries.html)를 참고하세요. EKS 접근 항목을 관리하는 권한과 Kubernetes 워크로드 권한은 별개입니다.
### EKS 클러스터 모니터링
컨트롤 플레인 로깅은 api/audit/authenticator/controllerManager/scheduler 로그를 내보냅니다. Container Insights에는 CloudWatch 에이전트·애드온과 제한된 텔레메트리 IAM 권한이 필요하며 update-cluster-logging으로 활성화되지 않습니다. CloudWatch 관측 애드온은 Prometheus/Grafana가 아닌 CloudWatch·Fluent Bit 구성 요소를 설치합니다([공식 설치 문서](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/install-CloudWatch-Observability-EKS-addon.html)).
```bash
# EKS 컨트롤 플레인 로그 활성화
eksctl utils update-cluster-logging \
--enable-types all \
--cluster my-cluster \
--region us-west-2
# CloudWatch 관측 기능 설치 (Prometheus/Grafana가 아님)
aws eks create-addon \
--cluster-name my-cluster \
--addon-name amazon-cloudwatch-observability \
--addon-version "${CLOUDWATCH_ADDON_VERSION:?Select a compatible add-on version}"
```
## 클러스터 관리 모범 사례
효과적인 Kubernetes 클러스터 관리를 위한 모범 사례는 안정성, 보안, 성능을 보장하는 데 중요합니다.
### 클러스터 설정 모범 사례
1. **다중 가용 영역 구성**: 고가용성을 위해 노드를 여러 가용 영역에 분산
2. **적절한 크기 조정**: 워크로드에 맞는 노드 유형 및 수 선택
3. **자동 확장 구성**: 클러스터 자동 확장기 및 수평적 파드 자동 확장기 활성화
4. **네트워크 정책 적용**: 기본 거부 정책으로 시작하고 필요한 통신만 허용
5. **리소스 쿼터 설정**: 네임스페이스별 리소스 제한 설정
### 운영 모범 사례
1. **선언적 구성 사용**: 모든 리소스를 YAML 파일로 정의하고 버전 관리
2. **GitOps 채택**: Git을 단일 진실 소스로 사용하고 자동화된 배포 파이프라인 구축
3. **정기적인 백업**: etcd 데이터와 영구 볼륨 데이터 정기적 백업
4. **모니터링 및 알림**: 포괄적인 모니터링 시스템 구축 및 주요 메트릭에 대한 알림 설정
5. **로깅 중앙화**: 모든 로그를 중앙 로깅 시스템으로 수집하여 분석 용이성 확보
### 보안 모범 사례
1. **최소 권한 원칙**: RBAC를 사용하여 필요한 최소 권한만 부여
2. **네트워크 세분화**: 네트워크 정책을 사용하여 파드 간 통신 제한
3. **이미지 스캐닝**: 취약점 검사를 위한 컨테이너 이미지 스캐닝 구현
4. **시크릿 관리**: 외부 시크릿 관리 도구 사용 (예: AWS Secrets Manager, HashiCorp Vault)
5. **정기적인 보안 감사**: 클러스터 구성 및 권한에 대한 정기적인 감사 수행
### 업그레이드 모범 사례
1. **점진적 업그레이드**: 한 번에 모든 것을 업그레이드하지 않고 점진적으로 진행
2. **테스트 환경 먼저**: 프로덕션 환경 전에 테스트 환경에서 업그레이드 검증
3. **백업 생성**: 업그레이드 전 전체 백업 수행
4. **롤백 계획**: 문제 발생 시 이전 버전으로 롤백할 수 있는 계획 수립
5. **업그레이드 창 설정**: 사용량이 적은 시간대에 업그레이드 수행
### 비용 최적화 모범 사례
1. **적절한 노드 크기 선택**: 워크로드에 맞는 최적의 노드 유형 선택
2. **스팟 인스턴스 활용**: 비중요 워크로드에 스팟 인스턴스 사용
3. **자동 확장 구성**: 수요에 따라 자동으로 확장 및 축소하도록 구성
4. **리소스 요청 및 제한 최적화**: 실제 사용량에 기반한 리소스 요청 및 제한 설정
5. **유휴 리소스 식별**: 정기적으로 유휴 리소스를 식별하고 제거
### 문서화 모범 사례
1. **아키텍처 문서화**: 클러스터 아키텍처, 네트워킹, 보안 설정 문서화
2. **운영 절차 문서화**: 일반적인 운영 작업, 문제 해결 절차, 비상 대응 계획 문서화
3. **변경 관리**: 모든 클러스터 변경 사항 기록 및 추적
4. **런북 작성**: 일반적인 시나리오에 대한 단계별 가이드 제공
5. **지식 공유**: 팀 내 지식 공유 및 교육 세션 정기적 진행
## 결론
Kubernetes 클러스터 관리는 다양한 측면을 포함하는 복잡한 작업입니다. 클러스터의 설정부터 운영, 모니터링, 문제 해결, 업그레이드에 이르기까지 체계적인 접근 방식이 필요합니다.
효과적인 클러스터 관리를 위해서는 다음 핵심 영역에 집중해야 합니다:
1. **클러스터 구성요소 관리**: 컨트롤 플레인 및 노드 구성요소의 안정적인 운영
2. **리소스 관리**: 효율적인 리소스 할당 및 사용
3. **네트워킹**: 안전하고 효율적인 네트워크 구성
4. **보안**: 적절한 인증 및 권한 관리
5. **백업 및 복구**: 데이터 손실 방지 및 재해 복구 계획
6. **모니터링 및 로깅**: 클러스터 상태 및 성능 모니터링
7. **문제 해결**: 체계적인 문제 해결 접근 방식
특히 Amazon EKS와 같은 관리형 Kubernetes 서비스를 사용할 때는 서비스 제공자와 사용자 간의 책임 분담 모델을 이해하는 것이 중요합니다. AWS가 컨트롤 플레인을 관리하며 컴퓨팅 책임은 모드에 따라 달라집니다. 애플리케이션 구성·보안은 고객이 관리합니다.
모범 사례를 따르고 적절한 도구를 활용하면 안정적이고 안전하며 효율적인 Kubernetes 클러스터를 운영할 수 있습니다. 지속적인 학습과 개선을 통해 클러스터 관리 역량을 향상시키는 것이 중요합니다.
---
> **참고 자료**:
> - [Kubernetes 공식 문서: 클러스터 관리](https://kubernetes.io/docs/tasks/administer-cluster/)
> - [Amazon EKS 사용 설명서](https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html)
> - [Kubernetes 모범 사례: 클러스터 관리](https://kubernetes.io/docs/setup/best-practices/)
> - [etcd 문서: 백업 및 복구](https://etcd.io/docs/v3.5/op-guide/recovery/)
> - [Prometheus 문서](https://prometheus.io/docs/introduction/overview/)
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [클러스터 관리 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/core/09-cluster-administration-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/core/10-windows-in-kubernetes
----------------------------------------
# Windows in Kubernetes
> **검토한 upstream Kubernetes 버전**: Kubernetes 1.35, 1.36, 1.37
> **마지막 업데이트**: 2026년 9월 11일
Kubernetes는 원래 Linux 컨테이너를 위해 설계되었지만, 버전 1.14부터 Windows 컨테이너에 대한 프로덕션 지원이 추가되었습니다. 이 장에서는 Kubernetes에서 Windows 워크로드를 실행하는 방법, 아키텍처, 제한 사항, 그리고 Amazon EKS에서의 Windows 지원에 대해 알아보겠습니다.
## 목차
1. [Windows 컨테이너 개요](#windows-컨테이너-개요)
2. [Kubernetes의 Windows 지원 아키텍처](#kubernetes의-windows-지원-아키텍처)
3. [Windows 노드 제한 사항](#windows-노드-제한-사항)
4. [Windows 노드 설정](#windows-노드-설정)
5. [Windows 컨테이너 배포](#windows-컨테이너-배포)
6. [네트워킹](#네트워킹)
7. [스토리지](#스토리지)
8. [모니터링 및 로깅](#모니터링-및-로깅)
9. [보안](#보안)
10. [Amazon EKS에서의 Windows 지원](#amazon-eks에서의-windows-지원)
11. [모범 사례](#모범-사례)
12. [결론](#결론)
## Windows 컨테이너 개요
Windows 컨테이너는 Windows 운영 체제에서 실행되는 컨테이너로, Windows 애플리케이션을 컨테이너화하여 배포할 수 있게 해줍니다.
### Windows 컨테이너 유형
Windows에는 두 가지 격리 유형이 있지만 Kubernetes는 **프로세스 격리만 지원**합니다. 아래 Hyper-V 설명은 운영 체제 배경 지식이며 Kubernetes 배포 옵션이 아닙니다:
1. **Windows Server 컨테이너**: Linux 컨테이너와 유사하게 호스트 OS 커널을 공유합니다. 가볍고 빠르게 시작되지만, Microsoft가 지원하는 호스트/이미지 조합이 필요합니다.
2. **Hyper-V 격리 컨테이너**: 각 컨테이너가 경량 VM에서 실행되어 더 높은 수준의 격리를 제공합니다. 호스트와 다른 Windows 버전을 실행할 수 있지만, 더 많은 리소스를 사용합니다.
다음 다이어그램은 두 가지 Windows 컨테이너 유형의 아키텍처 차이를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-10-windows-in-kubernetes-0.html)
### Windows 컨테이너 이미지
Windows 컨테이너 이미지는 Microsoft에서 제공하는 기본 이미지를 기반으로 합니다:
1. **Windows Server Core**: 최소한의 Windows Server 환경을 제공하는 경량 이미지
2. **Nano Server**: 더 작은 공간을 차지하는 초경량 이미지
3. **Windows**: 더 넓은 Windows API를 제공하는 이미지이며 전체 데스크톱/GUI 서버는 아님
예시 Dockerfile:
```dockerfile
FROM mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2022
COPY website/ C:/inetpub/wwwroot/
EXPOSE 80
# Inherit the IIS image entrypoint (ServiceMonitor.exe).
```
## Kubernetes의 Windows 지원 아키텍처
Kubernetes에서 Windows 지원은 혼합 환경을 기반으로 합니다. 컨트롤 플레인 구성 요소는 항상 Linux에서 실행되며, 워커 노드는 Linux 또는 Windows일 수 있습니다.
### 아키텍처 개요
Kubernetes의 Windows 지원 아키텍처는 다음과 같습니다:
1. **Linux 컨트롤 플레인**: kube-apiserver, kube-controller-manager, kube-scheduler, etcd는 항상 Linux에서 실행됩니다.
2. **Linux 워커 노드**: 시스템 구성 요소(CoreDNS, metrics-server 등)를 실행합니다.
3. **Windows 워커 노드**: Windows 애플리케이션 워크로드를 실행합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-10-windows-in-kubernetes-1.html)
### Windows 노드 구성 요소
Windows 노드에서 실행되는 Kubernetes 구성 요소:
1. **kubelet**: 노드에서 포드 및 컨테이너 관리
2. **kube-proxy**: 네트워크 규칙 관리
3. **CNI 플러그인**: 네트워킹 구성
4. **CSI 플러그인**: 스토리지 관리
## Windows 노드 제한 사항
Kubernetes에서 Windows 노드를 사용할 때 알아야 할 몇 가지 제한 사항이 있습니다.
### 기능 제한 사항
1. `privileged`는 지원하지 않습니다. 노드 에이전트에는 hostProcess와 hostNetwork를 함께 설정한 **HostProcess 컨테이너**를 사용하며 호스트 권한을 신중하게 제한합니다.
2. 일반 Windows Pod는 hostNetwork를 지원하지 않습니다. HostProcess는 예외입니다.
3. `spec.os.name: windows`인 Pod는 runAsUser, fsGroup, seccomp, capabilities, readOnlyRootFilesystem 등 Linux 전용 필드를 설정할 수 없습니다.
4. OS별 이미지와 nodeSelector로 DaemonSet을 분리할 수 있습니다.
5. 메모리 기반 emptyDir, raw block volumeDevices, PIDPressure, Linux 방식 OOM eviction은 지원하지 않습니다.
6. CPU/메모리 제한은 Windows 방식으로 구현됩니다. Windows에는 Linux OOM killer가 없으며 메모리 부족 시 할당 실패나 페이징으로 성능이 저하될 수 있습니다.
### 네트워킹 제한 사항
Windows HNS와 CNI의 L2bridge/overlay 등 지원 모드를 확인합니다. 같은 Pod의 컨테이너는 네트워크와 localhost를 공유하지만 프로세스 네임스페이스와 루트 파일 시스템은 공유하지 않습니다. NetworkPolicy, 서비스 및 DSR 지원은 OS/CNI/클러스터 조합에 따라 확인해야 합니다.
### 운영 체제 버전 호환성
Kubernetes v1.37의 Windows 워커 지원 대상은 Windows Server 2022와 2025입니다. 이 장의 예제는 **Windows Server 2022 + ltsc2022** 조합을 사용합니다. Microsoft 호환성 표와 배포판 지원 범위를 함께 확인하고 월별 보안 패치를 적용합니다. Hyper-V 격리로 Kubernetes의 호환성 제한을 우회할 수 없습니다.
## Windows 노드 설정
Kubernetes 클러스터에 Windows 노드를 추가하는 과정을 알아보겠습니다.
### 사전 요구 사항
지원 중인 Kubernetes/Windows 조합, Linux 컨트롤 플레인, Windows 지원 CNI 및 CRI 호환 containerd가 필요합니다. Docker Engine 자체는 CRI를 제공하지 않으며 내장 dockershim은 Kubernetes 1.24에서 제거되었습니다. EKS 노드는 아래 EKS 절차를 사용합니다.
### Windows 노드 준비
관리자 PowerShell에서 Containers 기능을 활성화하고 필요한 재부팅을 완료합니다. 아래는 **자체 관리 kubeadm 워커**용입니다. 공식 sig-windows-tools의 `hostprocess/Install-Containerd.ps1`과 `hostprocess/PrepareNode.ps1`을 검토한 커밋에서 다운로드하고 체크섬을 확인한 후 실행합니다. 지원되는 containerd 패치와 클러스터 버전에 맞는 kubelet을 선택합니다. 설치 스크립트가 만든 방화벽 규칙도 검토하여 10250 접근을 필요한 컨트롤 플레인 소스로 제한합니다.
```powershell
$ErrorActionPreference = "Stop"
$ContainerdVersion = Read-Host "Validated containerd version (without v)"
$KubernetesVersion = Read-Host "Cluster-compatible Kubernetes version (vX.Y.Z)"
if (-not $ContainerdVersion -or -not $KubernetesVersion) { throw "Versions required" }
.\Install-Containerd.ps1 -ContainerDVersion $ContainerdVersion
.\PrepareNode.ps1 -KubernetesVersion $KubernetesVersion
```
### kubeadm을 사용한 Windows 노드 조인
Linux 컨트롤 플레인에서 조인 토큰 생성:
```bash
kubeadm token create --print-join-command
```
Windows 노드에서 조인 명령 실행:
```powershell
# kubeadm 조인 명령 실행
kubeadm join : --token --discovery-token-ca-cert-hash sha256:
```
### Windows 노드 레이블 설정
kubelet이 게시한 OS/아키텍처/빌드 레이블을 확인합니다. 잘못된 OS 레이블을 덮어써서 스케줄링을 강제하지 않습니다. `spec.os.name`은 OS를 명시하지만 스케줄러 선택자를 대신하지 않으므로 nodeSelector도 사용합니다.
```bash
kubectl get nodes -L kubernetes.io/os,kubernetes.io/arch,node.kubernetes.io/windows-build
```
## Windows 컨테이너 배포
Windows 컨테이너를 Kubernetes에 배포하는 방법을 알아보겠습니다.
### 노드 셀렉터 사용
Windows 워크로드를 배포할 때는 노드 셀렉터를 사용하여 Windows 노드에 스케줄링되도록 해야 합니다:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: iis-deployment
spec:
replicas: 2
selector:
matchLabels:
app: iis
template:
metadata:
labels:
app: iis
spec:
os:
name: windows
nodeSelector:
kubernetes.io/os: windows
containers:
- name: iis
image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2022
resources:
limits:
cpu: 1
memory: 800Mi
requests:
cpu: .1
memory: 300Mi
ports:
- containerPort: 80
```
### 리소스 요청 및 제한
Windows 컨테이너의 리소스 요청 및 제한은 Linux 컨테이너와 다르게 처리됩니다:
1. **CPU 제한**: Windows에서는 CPU 제한이 다르게 적용됩니다. 예를 들어, CPU 제한이 1이면 단일 CPU 코어의 100%를 사용할 수 있습니다.
2. **메모리 제한**: Windows 컨테이너는 메모리 제한을 준수하지만, 일부 시스템 프로세스로 인해 추가 오버헤드가 발생할 수 있습니다.
### 컨테이너 사용자 지정
Windows 컨테이너에서 사용자 지정 스크립트 실행:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: windows-custom-script
spec:
os:
name: windows
nodeSelector:
kubernetes.io/os: windows
containers:
- name: windows-container
image: mcr.microsoft.com/windows/servercore:ltsc2022
command:
- powershell.exe
- -Command
- |
while ($true) {
Write-Host "Hello from Windows container"
Start-Sleep -Seconds 10
}
```
### 다중 컨테이너 포드
Windows에서도 다중 컨테이너 포드를 지원하지만, 일부 제한 사항이 있습니다:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: windows-multi-container
spec:
os:
name: windows
nodeSelector:
kubernetes.io/os: windows
containers:
- name: writer
image: mcr.microsoft.com/windows/servercore:ltsc2022
command: [powershell.exe, -Command, 'while ($true) { Add-Content C:\shared-logs\app.log "Log at $(Get-Date)"; Start-Sleep 10 }']
volumeMounts:
- name: logs
mountPath: C:\shared-logs
- name: logger
image: mcr.microsoft.com/windows/servercore:ltsc2022
command: [powershell.exe, -Command, 'while (-not (Test-Path C:\shared-logs\app.log)) { Start-Sleep 2 }; Get-Content C:\shared-logs\app.log -Wait']
volumeMounts:
- name: logs
mountPath: C:\shared-logs
readOnly: true
volumes:
- name: logs
emptyDir: {}
```
## 네트워킹
Windows 노드의 네트워킹은 Linux 노드와 다른 특성을 가집니다.
다음 다이어그램은 Windows 노드와 Linux 노드가 혼합된 Kubernetes 클러스터의 네트워킹 아키텍처를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-10-windows-in-kubernetes-2.html)
### 지원되는 네트워크 플러그인
Windows 노드에서 지원되는 네트워크 플러그인:
1. **Flannel**: VXLAN 또는 host-gw 모드
2. **Calico**: VXLAN 모드
3. **Antrea**: OVS 기반 네트워킹
4. **Azure CNI**: Azure 환경에서 사용
5. **AWS VPC CNI**: AWS 환경에서 사용
### Flannel 설정 예시
Flannel의 Linux 매니페스트를 Windows DaemonSet으로 복사하면 동작하지 않습니다. Windows 바이너리, HNS, CNI 경로, RBAC 및 HostProcess 구성이 필요합니다. 클러스터 배포판의 Windows 지원 설치 절차를 사용하고 Linux 측 네트워크와 Windows 측 win-overlay/win-bridge 구성을 함께 맞춥니다. Windows Flannel VXLAN은 VNI 4096/UDP 4789 조건을 확인합니다. 일반 애플리케이션 Pod에 hostNetwork를 추가하는 방식으로 설치하지 않습니다.
### 서비스 노출
Windows 노드에서 서비스를 노출하는 방법:
```yaml
apiVersion: v1
kind: Service
metadata:
name: iis-service
spec:
selector:
app: iis
ports:
- port: 80
targetPort: 80
type: LoadBalancer
```
### 네트워크 정책
Windows 노드에서 네트워크 정책을 사용하려면 네트워크 정책을 지원하는 CNI 플러그인(예: Calico)이 필요합니다:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: default
spec:
podSelector:
matchLabels:
app: backend
os: windows
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 80
```
## 스토리지
Windows 노드에서 사용할 수 있는 스토리지 옵션을 알아보겠습니다.
다음 다이어그램은 Windows 노드에서 사용 가능한 다양한 스토리지 옵션을 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-10-windows-in-kubernetes-3.html)
### 지원되는 볼륨 유형
Windows 노드에서 지원되는 볼륨 유형:
1. **emptyDir**: 임시 스토리지 (메모리 기반 emptyDir은 지원되지 않음)
2. **hostPath**: 호스트 노드의 파일 시스템
3. **configMap**: 구성 데이터
4. **secret**: 민감한 데이터
5. **CSI/PVC**: Windows 호환 Azure Files, Azure Disk, EBS 또는 SMB CSI 드라이버와 파일 시스템 볼륨을 사용하며 OS/파일 시스템 지원을 확인합니다.
### emptyDir 볼륨 예시
```yaml
apiVersion: v1
kind: Pod
metadata:
name: windows-emptydir
spec:
os:
name: windows
nodeSelector:
kubernetes.io/os: windows
containers:
- name: windows-container
image: mcr.microsoft.com/windows/servercore:ltsc2022
volumeMounts:
- name: temp-volume
mountPath: C:\temp
command:
- powershell.exe
- -Command
- |
Set-Content -Path C:\temp\test.txt -Value "Hello from Windows"
while ($true) {
Get-Content -Path C:\temp\test.txt
Start-Sleep -Seconds 10
}
volumes:
- name: temp-volume
emptyDir: {}
```
### hostPath 볼륨 예시
```yaml
apiVersion: v1
kind: Pod
metadata:
name: windows-hostpath
spec:
os:
name: windows
nodeSelector:
kubernetes.io/os: windows
containers:
- name: windows-container
image: mcr.microsoft.com/windows/servercore:ltsc2022
volumeMounts:
- name: logs-volume
mountPath: C:\logs
command:
- powershell.exe
- -Command
- |
Set-Content -Path C:\logs\app.log -Value "Application log"
while ($true) {
Add-Content -Path C:\logs\app.log -Value "Log entry at $(Get-Date)"
Start-Sleep -Seconds 10
}
volumes:
- name: logs-volume
hostPath:
path: C:\k\logs
type: DirectoryOrCreate
```
### ConfigMap 및 Secret 볼륨 예시
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: windows-config
data:
config.json: |
{
"setting1": "value1",
"setting2": "value2"
}
---
apiVersion: v1
kind: Secret
metadata:
name: windows-secret
type: Opaque
data:
username: YWRtaW4= # admin
password: cGFzc3dvcmQ= # password
---
apiVersion: v1
kind: Pod
metadata:
name: windows-config-secret
spec:
os:
name: windows
nodeSelector:
kubernetes.io/os: windows
containers:
- name: windows-container
image: mcr.microsoft.com/windows/servercore:ltsc2022
volumeMounts:
- name: config-volume
mountPath: C:\config
- name: secret-volume
mountPath: C:\secret
readOnly: true
command:
- powershell.exe
- -Command
- |
Get-Content -Path C:\config\config.json
if (-not (Test-Path C:\secret\username) -or -not (Test-Path C:\secret\password)) { throw "Secret files missing" }
while ($true) { Start-Sleep -Seconds 10 }
volumes:
- name: config-volume
configMap:
name: windows-config
- name: secret-volume
secret:
secretName: windows-secret
```
### CSI 드라이버 사용
사전 요구 사항: Windows 호환 CSI 드라이버와 파일 시스템(예: NTFS 및 WaitForFirstConsumer를 사용하는 EBS CSI)으로 windows-csi StorageClass를 먼저 생성합니다. 이름만으로 드라이버가 설치되지 않습니다. EBS는 AZ에 종속되며 raw block 대신 파일 시스템 모드를 사용합니다.
예시:
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: windows-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: windows-csi
---
apiVersion: v1
kind: Pod
metadata:
name: windows-csi-pod
spec:
os:
name: windows
nodeSelector:
kubernetes.io/os: windows
containers:
- name: windows-container
image: mcr.microsoft.com/windows/servercore:ltsc2022
volumeMounts:
- name: data-volume
mountPath: C:\data
command:
- powershell.exe
- -Command
- |
Set-Content -Path C:\data\file.txt -Value "Persistent data"
while ($true) { Start-Sleep -Seconds 10 }
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: windows-pvc
```
## 모니터링 및 로깅
Windows 노드 및 컨테이너의 모니터링 및 로깅 방법을 알아보겠습니다.
### 모니터링
Windows 노드 모니터링을 위한 도구:
1. **Prometheus Windows Exporter**: Windows 노드 메트릭 수집
2. **metrics-server**: 기본 리소스 사용량 메트릭 제공
3. **Datadog, Dynatrace, New Relic**: 상용 모니터링 솔루션
Windows 노드에 Prometheus Windows Exporter 설치:
```powershell
# Download a supported release MSI, verify its checksum, then install locally.
$ExporterMsi = (Resolve-Path .\windows_exporter.msi).Path
Start-Process msiexec.exe -ArgumentList "/i `"$ExporterMsi`" ENABLED_COLLECTORS=cpu,memory,logical_disk,net,service,os,system REMOVE=FirewallException /quiet" -Wait
# Restrict any separately configured port 9182 firewall rule to Prometheus sources.
```
Prometheus 구성:
```yaml
scrape_configs:
- job_name: 'windows-nodes'
static_configs:
- targets: ['windows-node-1:9182', 'windows-node-2:9182']
```
### 로깅
Windows 컨테이너 로그 수집을 위한 도구:
1. **Fluent Bit**: 경량 로그 수집기
2. **Fluentd**: 로그 수집 및 전달
3. **Elasticsearch**: 로그 저장 및 검색
4. **Azure Monitor**: Azure 환경에서 사용
5. **CloudWatch Logs**: AWS 환경에서 사용
Windows 노드에 Fluent Bit 설치:
실제 Elasticsearch 주소/인증 및 신뢰할 CA를 구성합니다. 서비스 계정에는 Security 이벤트 로그 읽기 권한과 체크포인트 경로 쓰기 권한이 필요합니다.
```powershell
# Install a supported Windows Fluent Bit release, verify its checksum,
# and arrange bin/ and conf/ under C:\fluent-bit before continuing.
# 구성 파일 생성
@"
[SERVICE]
Flush 5
Daemon Off
Log_Level info
[INPUT]
Name winlog
Channels Application,System,Security
DB C:\fluent-bit\winlog.db
[OUTPUT]
Name es
Match *
Host elasticsearch-host
Port 9200
Index windows_logs
Suppress_Type_Name On
tls On
tls.verify On
"@ | Out-File -FilePath C:\fluent-bit\conf\fluent-bit.conf -Encoding ascii
# 서비스 등록
sc.exe create fluent-bit binPath= "C:\fluent-bit\bin\fluent-bit.exe -c C:\fluent-bit\conf\fluent-bit.conf"
Start-Service fluent-bit
```
### 애플리케이션 로그 수집
IIS의 파일/ETW/Event Log를 stdout으로 내보내려면 Microsoft LogMonitor를 애플리케이션 이미지에 통합하고 LogMonitorConfig.json에 실제 소스를 정의합니다. ServiceMonitor와 IIS 수명을 유지하는 진입점을 테스트합니다. 위 shared-file sidecar는 볼륨 공유를 설명하는 예제이며 파일 회전/재시작 중 중복·유실 처리를 제공하지 않습니다. 운영 수집기는 체크포인트와 회전을 처리해야 합니다. `kubectl logs`는 stdout/stderr만 보여 주며 IIS 파일을 자동 수집하지 않습니다.
## 보안
Windows 노드 및 컨테이너의 보안 고려 사항을 알아보겠습니다.
### Windows 노드 보안
Windows 노드 보안을 위한 권장 사항:
1. **최신 업데이트 적용**: Windows 보안 업데이트 정기적 적용
2. **방화벽 구성**: Windows Defender 방화벽 적절히 구성
3. **최소 권한 원칙**: 필요한 최소한의 권한만 부여
4. **안티바이러스 소프트웨어**: 적절한 안티바이러스 소프트웨어 설치
5. **그룹 정책**: 보안 강화를 위한 그룹 정책 적용
### Windows 컨테이너 보안
Windows 컨테이너 보안을 위한 권장 사항:
1. **최소 기본 이미지**: 가능한 작은 기본 이미지 사용(Nano Server 등)
2. **이미지 스캐닝**: 컨테이너 이미지 취약점 스캐닝
3. **파일 시스템 권한**: NTFS ACL 및 지원되는 읽기 전용 데이터 마운트를 사용합니다. Windows는 readOnlyRootFilesystem을 지원하지 않습니다.
4. **비특권 사용자**: 비특권 사용자로 애플리케이션 실행
5. **네트워크 정책**: 적절한 네트워크 정책 적용
### RunAsUsername
Windows 컨테이너에서는 `runAsUser` 대신 `securityContext.windowsOptions.runAsUserName`을 사용하여 컨테이너 내에서 실행할 사용자를 지정할 수 있습니다:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: windows-runasusername
spec:
os:
name: windows
nodeSelector:
kubernetes.io/os: windows
securityContext:
windowsOptions:
runAsUserName: "ContainerUser"
containers:
- name: windows-container
image: mcr.microsoft.com/windows/servercore:ltsc2022
command:
- powershell.exe
- -Command
- |
whoami
while ($true) { Start-Sleep -Seconds 10 }
```
### 그룹 관리 서비스 계정(gMSA)
`gmsaCredentialSpecName`은 Secret이 아닌 클러스터 범위 **GMSACredentialSpec**을 참조합니다. CRD, mutating/validating webhook 및 ServiceAccount의 `use` RBAC 권한이 필요합니다. 아래 도메인/호스트 이름은 실제 값으로 바꾸고 CredentialSpec 모듈로 AD의 SID/GUID/NetBIOS/DNS 범위를 생성합니다. 도메인 가입 호스트 방식의 예이며 지원되는 비도메인 호스트용 portable identity 구성은 별도 준비가 필요합니다.
`Get-KdsRootKey`로 기존 키를 확인합니다. 새 키가 필요하면 AD 관리자가 `Add-KdsRootKey -EffectiveImmediately` 후 복제 대기 시간(최대 10시간)을 확보합니다. 10시간 backdate는 단일 DC 테스트 환경 전용입니다. gMSA는 네트워크 인증 자격이며 컨테이너를 도메인에 가입시키거나 `whoami`를 gMSA 이름으로 바꾸지 않습니다. 실제 서비스의 Kerberos 인증과 `klist`로 검증합니다.
```powershell
# On an authorized AD administration host, after KDS readiness is confirmed:
Import-Module ActiveDirectory
New-ADGroup -Name 'WebAppHosts' -SamAccountName 'WebAppHosts' -GroupScope DomainLocal
Add-ADGroupMember -Identity 'WebAppHosts' -Members 'ContainerHost01$'
New-ADServiceAccount -Name WebApp1 -DNSHostName WebApp1.contoso.com -ServicePrincipalNames http/WebApp1.contoso.com -PrincipalsAllowedToRetrieveManagedPassword WebAppHosts
# Install/review the official CredentialSpec PowerShell module first.
Import-Module CredentialSpec
New-CredentialSpec -AccountName WebApp1 -Path C:\gmsa-credspec.json
$spec = Get-Content C:\gmsa-credspec.json -Raw | ConvertFrom-Json
@{ apiVersion='windows.k8s.io/v1'; kind='GMSACredentialSpec'; metadata=@{name='gmsa-cred-spec'}; credspec=$spec } |
ConvertTo-Json -Depth 20 | Set-Content C:\gmsa-resource.json -Encoding utf8
```
```bash
# Requires the GMSA CRD and mutating/validating webhooks installed by an administrator.
kubectl apply -f gmsa-resource.json
```
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: windows-app
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: use-webapp-gmsa
rules:
- apiGroups: [windows.k8s.io]
resources: [gmsacredentialspecs]
resourceNames: [gmsa-cred-spec]
verbs: [use]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: use-webapp-gmsa
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: use-webapp-gmsa
subjects:
- kind: ServiceAccount
name: windows-app
namespace: default
---
apiVersion: v1
kind: Pod
metadata:
name: windows-gmsa
namespace: default
spec:
os:
name: windows
serviceAccountName: windows-app
nodeSelector:
kubernetes.io/os: windows
securityContext:
windowsOptions:
gmsaCredentialSpecName: gmsa-cred-spec
runAsUserName: 'NT AUTHORITY\NETWORK SERVICE'
containers:
- name: windows-container
image: mcr.microsoft.com/windows/servercore:ltsc2022
command: [powershell.exe, -Command, 'whoami; Start-Sleep -Seconds 3600']
```
## Amazon EKS에서의 Windows 지원
Amazon EKS에서 Windows 워크로드를 실행하는 방법을 알아보겠습니다.
다음 다이어그램은 Amazon EKS에서의 Windows 지원 아키텍처를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-10-windows-in-kubernetes-4.html)
### EKS에서 Windows 지원 활성화
Windows IPAM은 EKS가 관리하는 VPC resource controller가 담당합니다. 예전 release-1.11 controller/webhook 매니페스트를 설치하지 않습니다. 클러스터 IAM 역할에 `AmazonEKSVPCResourceController` 권한을 부여하고 현재 AWS 절차에 따라 `kube-system/amazon-vpc-cni` ConfigMap의 `enable-windows-ipam: "true"`를 설정합니다. 기존 키를 보존하며 Helm/애드온 관리 설정과 충돌하지 않게 적용합니다.
CoreDNS용 Linux 노드 또는 지원되는 Fargate 구성이 필요합니다. Windows는 EKS Auto Mode, Fargate 워크로드, Hybrid Nodes, IPv6, 사용자 지정 네트워킹 및 Pod별 보안 그룹을 지원하지 않습니다. Windows 노드 역할의 access entry 유형은 `EC2_WINDOWS`이며, 레거시 aws-auth 구성에서는 `eks:kube-proxy-windows` 그룹을 확인합니다.
### Windows 노드 그룹 생성
eksctl을 사용하여 Windows 노드 그룹 생성:
```bash
eksctl create nodegroup \
--cluster my-cluster \
--region us-west-2 \
--name windows-ng \
--node-type t3.large \
--nodes 2 \
--nodes-min 1 \
--nodes-max 4 \
--managed \
--node-ami-family WindowsServer2022FullContainer
```
AWS Management Console을 사용하여 Windows 노드 그룹 생성:
1. EKS 콘솔에서 클러스터 선택
2. "컴퓨팅" 탭 선택
3. "노드 그룹 추가" 클릭
4. 노드 그룹 세부 정보 입력
5. AMI 유형으로 "Windows" 선택
6. 나머지 설정 구성 및 생성
### EKS에서 Windows 애플리케이션 배포
EKS에서 Windows 애플리케이션 배포 예시:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: windows-server-iis
spec:
selector:
matchLabels:
app: windows-server-iis
tier: backend
track: stable
replicas: 2
template:
metadata:
labels:
app: windows-server-iis
tier: backend
track: stable
spec:
os:
name: windows
nodeSelector:
kubernetes.io/os: windows
containers:
- name: windows-server-iis
image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2022
ports:
- name: http
containerPort: 80
resources:
limits:
cpu: 1
memory: 800Mi
requests:
cpu: .1
memory: 300Mi
---
apiVersion: v1
kind: Service
metadata:
name: windows-server-iis-service
labels:
app: windows-server-iis
spec:
ports:
- port: 80
protocol: TCP
selector:
app: windows-server-iis
type: LoadBalancer
```
### EKS에서 Windows 컨테이너 로깅
Windows 노드의 Container Insights는 CloudWatch Observability EKS 애드온 1.5.0 이상에서 지원합니다. 클러스터와 호환되는 애드온 버전, IAM 권한 및 Windows 노드용 에이전트 구성을 선택합니다. Windows Application Signals는 지원하지 않습니다.
Windows stdout/stderr는 kubelet의 CRI 로그 경로(일반적으로 `C:\var\log\pods` 및 `C:\var\log\containers`)에서 수집합니다. 실제 배포판 경로를 확인하며 Linux `/var/log`/Docker parser 설정을 그대로 복사하지 않습니다. EKS의 kubelet/kube-proxy 로그는 **EKS Windows** 이벤트 로그에 기록됩니다. 일반 컨테이너에 .evtx 파일만 마운트해도 winlog 입력이 호스트 이벤트 API를 읽게 되지는 않습니다. 호스트 서비스나 검토된 HostProcess 수집기를 사용합니다.
## 모범 사례
Kubernetes에서 Windows 워크로드를 실행하기 위한 모범 사례를 알아보겠습니다.
### 클러스터 설계 모범 사례
1. **혼합 노드 풀**: Linux 노드와 Windows 노드를 적절히 혼합하여 사용
2. **노드 레이블 및 테인트**: 적절한 노드 레이블 및 테인트를 사용하여 워크로드 분리
3. **버전 호환성**: Kubernetes 버전과 Windows 버전 간의 호환성 확인
4. **네트워크 플러그인 선택**: Windows를 지원하는 적절한 네트워크 플러그인 선택
5. **고가용성**: 중요한 워크로드에 대한 고가용성 구성
### 애플리케이션 설계 모범 사례
1. **컨테이너 이미지 최적화**: 작고 효율적인 컨테이너 이미지 사용
2. **리소스 요청 및 제한**: 적절한 리소스 요청 및 제한 설정
3. **상태 비저장 설계**: 가능한 상태 비저장 애플리케이션 설계
4. **로깅 및 모니터링**: 효과적인 로깅 및 모니터링 구성
5. **보안 강화**: 적절한 보안 컨텍스트 및 네트워크 정책 적용
### 운영 모범 사례
1. **정기적인 업데이트**: Windows 노드 및 컨테이너 이미지 정기적 업데이트
2. **자동화**: 배포 및 관리 작업 자동화
3. **백업 및 복구**: 중요한 데이터 정기적 백업
4. **문제 해결 도구**: 적절한 문제 해결 도구 및 프로세스 구축
5. **문서화**: 구성 및 절차 문서화
### EKS 특화 모범 사례
1. **관리형 노드 그룹**: 가능한 경우 관리형 노드 그룹 사용
2. **IAM 역할 서비스 계정(IRSA)**: 포드별 IAM 권한 관리
3. **VPC CNI 구성**: 네트워킹 요구 사항에 맞게 VPC CNI 구성
4. **보안 그룹**: 적절한 보안 그룹 구성
5. **비용 최적화**: 적절한 인스턴스 유형 및 크기 선택
## 결론
Kubernetes에서 Windows 지원은 계속 발전하고 있으며, 이제 프로덕션 환경에서 Windows 워크로드를 실행할 수 있습니다. Windows 노드는 Linux 노드와 함께 동일한 클러스터에서 실행될 수 있으며, 이를 통해 다양한 워크로드를 단일 Kubernetes 클러스터에서 관리할 수 있습니다.
Windows 컨테이너는 .NET Framework 애플리케이션, Windows 서비스, 기타 Windows 전용 워크로드를 컨테이너화하여 Kubernetes의 오케스트레이션 기능을 활용할 수 있게 해줍니다. 그러나 Linux 컨테이너와 비교하여 일부 제한 사항이 있으므로, 이러한 제한 사항을 이해하고 적절히 대응하는 것이 중요합니다.
Amazon EKS는 Windows 노드에 대한 관리형 서비스를 제공하여 Windows 워크로드를 쉽게 배포하고 관리할 수 있게 해줍니다. EKS의 Windows 지원을 활용하면 Windows 애플리케이션을 현대적인 컨테이너 환경으로 마이그레이션하는 과정을 간소화할 수 있습니다.
Windows in Kubernetes를 성공적으로 구현하려면 적절한 계획, 설계, 운영 모범 사례를 따르는 것이 중요합니다. 이를 통해 Windows 및 Linux 워크로드를 효율적으로 관리하고 Kubernetes의 모든 이점을 활용할 수 있습니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [Windows in Kubernetes 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/core/10-windows-in-kubernetes-quiz)를 풀어보세요.
## 검증 참고 자료
- https://kubernetes.io/docs/concepts/windows/intro/
- https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/adding-windows-nodes/
- https://kubernetes.io/docs/tasks/configure-pod-container/create-hostprocess-pod/
- https://kubernetes.io/docs/tasks/configure-pod-container/configure-gmsa/
- https://learn.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/version-compatibility
- https://github.com/microsoft/windows-container-tools/tree/main/LogMonitor
- https://docs.aws.amazon.com/eks/latest/userguide/windows-support.html
- https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/install-CloudWatch-Observability-EKS-addon.html
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/core/11-extending-kubernetes
----------------------------------------
# Kubernetes 확장
> **검토한 upstream Kubernetes 버전**: Kubernetes 1.35, 1.36, 1.37
> **마지막 업데이트**: 2026년 9월 11일
Kubernetes는 확장성을 고려하여 설계된 플랫폼으로, 다양한 방법으로 기능을 확장할 수 있습니다. 이 장에서는 Kubernetes를 확장하는 다양한 방법과 Amazon EKS에서의 확장 기능 활용 방법에 대해 알아보겠습니다.
## 목차
1. [Kubernetes 확장 개요](#kubernetes-확장-개요)
2. [커스텀 리소스](#커스텀-리소스)
3. [오퍼레이터 패턴](#오퍼레이터-패턴)
4. [어드미션 컨트롤러](#어드미션-컨트롤러)
5. [API 서버 확장](#api-서버-확장)
6. [스케줄러 확장](#스케줄러-확장)
7. [클라우드 컨트롤러 매니저](#클라우드-컨트롤러-매니저)
8. [CSI(Container Storage Interface)](#csicontainer-storage-interface)
9. [CNI(Container Network Interface)](#cnicontainer-network-interface)
10. [디바이스 플러그인](#디바이스-플러그인)
11. [Amazon EKS에서의 확장 기능](#amazon-eks에서의-확장-기능)
12. [모범 사례](#모범-사례)
13. [결론](#결론)
## Kubernetes 확장 개요
Kubernetes는 다양한 확장 지점을 제공하여 기본 기능을 확장하고 사용자 정의할 수 있습니다. 주요 확장 지점은 다음과 같습니다:
1. **커스텀 리소스**: 새로운 API 객체 유형 정의
2. **오퍼레이터**: 커스텀 리소스와 컨트롤러를 결합하여 복잡한 애플리케이션 관리
3. **어드미션 컨트롤러**: API 요청을 가로채고 수정하거나 검증
4. **API 서버 확장**: API 서버에 새로운 엔드포인트 추가
5. **스케줄러 확장**: 포드 스케줄링 로직 사용자 정의
6. **클라우드 컨트롤러 매니저**: 클라우드 제공업체별 기능 통합
7. **CSI(Container Storage Interface)**: 스토리지 시스템 통합
8. **CNI(Container Network Interface)**: 네트워킹 솔루션 통합
9. **디바이스 플러그인**: 특수 하드웨어 통합
다음 다이어그램은 Kubernetes의 주요 확장 지점을 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-11-extending-kubernetes-0.html)
### 확장 방법 선택
적절한 확장 방법을 선택하는 데 고려해야 할 사항:
1. **사용 사례**: 확장하려는 기능의 유형
2. **복잡성**: 구현 및 유지 관리의 복잡성
3. **성능 영향**: 확장이 클러스터 성능에 미치는 영향
4. **업그레이드 호환성**: Kubernetes 버전 업그레이드와의 호환성
5. **커뮤니티 지원**: 확장 방법에 대한 커뮤니티 지원 수준
## 커스텀 리소스
커스텀 리소스는 Kubernetes API를 확장하여 새로운 객체 유형을 정의하는 방법입니다.
다음 다이어그램은 커스텀 리소스의 작동 방식을 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-11-extending-kubernetes-1.html)
### 커스텀 리소스 정의(CRD)
CRD는 새로운 리소스 유형을 정의하는 가장 간단한 방법입니다:
```yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: backups.example.com
spec:
group: example.com
names:
kind: Backup
listKind: BackupList
plural: backups
singular: backup
shortNames:
- bk
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
source:
type: string
destination:
type: string
schedule:
type: string
required:
- source
- destination
status:
type: object
properties:
phase:
type: string
lastBackupTime:
type: string
format: date-time
subresources:
status: {}
additionalPrinterColumns:
- name: Status
type: string
jsonPath: .status.phase
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
```
위 예시에서 `Backup`이라는 새로운 리소스 유형을 정의하고, 해당 리소스의 스키마와 추가 프린터 열을 지정합니다.
### 커스텀 리소스 인스턴스 생성
CRD의 Established 조건을 확인한 후 인스턴스를 생성합니다. CRD는 데이터를 저장/검증할 뿐 컨트롤러 없이는 백업을 실행하지 않습니다.
```yaml
apiVersion: example.com/v1
kind: Backup
metadata:
name: daily-backup
spec:
source: /data
destination: s3://my-bucket/backups
schedule: "0 0 * * *"
```
### 커스텀 리소스 검증
CRD에서 OpenAPI v3 스키마를 사용하여 커스텀 리소스의 유효성을 검증할 수 있습니다:
```yaml
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
replicas:
type: integer
minimum: 1
maximum: 10
image:
type: string
minLength: 1
required:
- replicas
- image
```
위 예시에서 `replicas` 필드는 1에서 10 사이의 정수여야 하고, `image` 필드는 비어 있지 않아야 하며 이미지 존재/서명 검증은 별도 정책이 필요합니다.
### 버전 관리
CRD는 여러 버전을 지원하여 API 진화를 가능하게 합니다:
```yaml
versions:
- name: v1alpha1
served: true
storage: false
- name: v1beta1
served: true
storage: false
- name: v1
served: true
storage: true
```
위 예시에서 `v1alpha1`, `v1beta1`, `v1` 세 가지 버전이 제공되지만, 새 쓰기는 `v1` 형식으로 저장됩니다. 위 조각에는 버전별 구조적 스키마를 추가해야 합니다. 기존 객체가 자동 재작성되지는 않으므로 이전 storedVersions 항목을 제거하기 전에 저장 데이터를 마이그레이션해야 합니다.
### 변환 웹훅
서로 다른 버전 간의 변환을 처리하기 위해 변환 웹훅을 사용할 수 있습니다:
```yaml
# Merge this spec fragment into the complete CRD above.
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: default
name: example-conversion-webhook
path: /convert
caBundle:
conversionReviewVersions:
- v1
```
## 오퍼레이터 패턴
오퍼레이터 패턴은 커스텀 리소스와 컨트롤러를 결합하여 복잡한 애플리케이션의 운영 지식을 자동화하는 방법입니다.
다음 다이어그램은 오퍼레이터 패턴의 작동 방식을 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-11-extending-kubernetes-2.html)
### 오퍼레이터 개념
오퍼레이터는 다음 구성 요소로 이루어집니다:
1. **커스텀 리소스 정의(CRD)**: 관리할 리소스의 스키마 정의
2. **컨트롤러**: 커스텀 리소스의 상태를 모니터링하고 원하는 상태로 조정하는 로직
3. **Kubernetes API 클라이언트**: Kubernetes API와 상호 작용하기 위한 클라이언트
### 오퍼레이터 예시
데이터베이스 오퍼레이터 예시:
```yaml
# 커스텀 리소스 정의
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.example.com
spec:
group: example.com
names:
kind: Database
listKind: DatabaseList
plural: databases
singular: database
shortNames:
- db
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
engine:
type: string
enum:
- mysql
- postgresql
version:
type: string
storageSize:
type: string
replicas:
type: integer
minimum: 1
required:
- engine
- version
- storageSize
status:
type: object
properties:
phase:
type: string
endpoint:
type: string
subresources:
status: {}
```
```yaml
# 데이터베이스 인스턴스
apiVersion: example.com/v1
kind: Database
metadata:
name: my-db
spec:
engine: postgresql
version: "17"
storageSize: 10Gi
replicas: 3
```
### 오퍼레이터 개발 도구
오퍼레이터를 개발하기 위한 도구:
1. **Operator SDK**: Go, Ansible, Helm을 사용하여 오퍼레이터 개발
2. **KUDO(Kubernetes Universal Declarative Operator)**: 선언적 방식으로 오퍼레이터 개발
3. **Kubebuilder**: Go 기반 오퍼레이터 개발 프레임워크
4. **Metacontroller**: 웹훅 기반 오퍼레이터 개발
#### Operator SDK 예시
Operator SDK를 사용한 오퍼레이터 생성:
```bash
: "${OPERATOR_IMAGE:?Set a registry image tag or digest you control}"
# Install a reviewed supported Operator SDK release and verify its checksum first.
# 새 오퍼레이터 프로젝트 생성
operator-sdk init --domain example.com --repo github.com/example/database-operator
# API 생성
operator-sdk create api --group database --version v1 --kind Database --resource --controller
# 컨트롤러 구현 (main.go, controllers/database_controller.go 등)
# 오퍼레이터 빌드 및 배포
make docker-build docker-push IMG="$OPERATOR_IMAGE"
make deploy IMG="$OPERATOR_IMAGE"
```
### 인기 있는 오퍼레이터
많이 사용되는 오픈 소스 오퍼레이터:
1. **Prometheus Operator**: Prometheus 모니터링 스택 관리
2. **Elasticsearch Operator**: Elasticsearch 클러스터 관리
3. **CoreOS etcd Operator(보관됨)**: 과거 etcd 자동화 예시이며 현재 설치 권장 대상은 아님
4. **PostgreSQL Operator**: PostgreSQL 데이터베이스 관리
5. **OpenTelemetry Operator**: Jaeger v2 배포에 사용하며 기존 Jaeger Operator는 지원 종료된 v1 전용
6. **Strimzi Kafka Operator**: Apache Kafka 클러스터 관리
7. **Istio in-cluster Operator(1.24에서 제거)**: 과거 예시이며 지원되는 Helm/istioctl 설치 절차 사용
## 어드미션 컨트롤러
어드미션 컨트롤러는 Kubernetes API 서버에 대한 요청을 가로채고 수정하거나 검증하는 플러그인입니다.
다음 다이어그램은 어드미션 컨트롤러의 작동 방식을 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-11-extending-kubernetes-3.html)
### 어드미션 컨트롤러 유형
Kubernetes에는 두 가지 유형의 어드미션 컨트롤러가 있습니다:
1. **변형(Mutating) 어드미션 컨트롤러**: 리소스를 변경할 수 있음
2. **검증(Validating) 어드미션 컨트롤러**: 리소스를 검증만 할 수 있음
### 내장 어드미션 컨트롤러
Kubernetes에는 여러 내장 어드미션 컨트롤러가 있습니다:
1. **NamespaceLifecycle**: 삭제 중인 네임스페이스에 리소스 생성을 방지
2. **LimitRanger**: 포드 및 컨테이너에 기본 리소스 제한 설정
3. **ServiceAccount**: Pod의 서비스 계정 기본값/존재 여부를 확인하고 automount 설정에 따라 projected 토큰 볼륨을 추가합니다. 기본 계정 생성은 별도 컨트롤러 역할입니다.
4. **DefaultStorageClass**: PVC에 기본 스토리지 클래스 할당
5. **ResourceQuota**: 네임스페이스별 리소스 사용량 제한
6. **PodSecurity**: 네임스페이스 Pod Security Standards를 적용하며 PodSecurityPolicy는 1.25에서 제거되었습니다.
7. **NodeRestriction**: 노드가 수정할 수 있는 리소스 제한
### 웹훅 어드미션 컨트롤러
사용자 정의 로직을 구현하기 위해 웹훅 어드미션 컨트롤러를 사용할 수 있습니다:
```yaml
# 변형 웹훅 구성
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: pod-mutating-webhook
webhooks:
- name: pod-mutator.example.com
clientConfig:
service:
namespace: default
name: pod-mutating-webhook
path: "/mutate"
caBundle:
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
operations: ["CREATE"]
scope: "Namespaced"
admissionReviewVersions: ["v1"]
sideEffects: None
timeoutSeconds: 5
```
```yaml
# 검증 웹훅 구성
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: pod-validating-webhook
webhooks:
- name: pod-validator.example.com
clientConfig:
service:
namespace: default
name: pod-validating-webhook
path: "/validate"
caBundle:
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
operations: ["CREATE", "UPDATE"]
scope: "Namespaced"
admissionReviewVersions: ["v1"]
sideEffects: None
timeoutSeconds: 5
```
### 웹훅 서버 구현
아래 Go 조각은 하나의 파일에서 v1 AdmissionReview 핸들러를 구현합니다. /mutate와 /validate를 Service 이름과 일치하는 인증서/CA를 사용하는 HTTPS 서버에 연결합니다. 외부 부작용이 없어 dry-run에 안전하며 대상 네임스페이스를 제한해야 합니다. 태그 검사는 정책 예시로 이미지 참조 전체 구문이나 서명을 검증하지 않습니다.
```go
package main
import (
"encoding/json"
"net/http"
"strings"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func readPodReview(w http.ResponseWriter, r *http.Request) (*admissionv1.AdmissionRequest, *corev1.Pod, bool) {
if r.Method != http.MethodPost || r.Body == nil {
http.Error(w, "POST body required", http.StatusBadRequest)
return nil, nil, false
}
var review admissionv1.AdmissionReview
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 2<<20)).Decode(&review); err != nil {
http.Error(w, "Invalid AdmissionReview JSON", http.StatusBadRequest)
return nil, nil, false
}
req := review.Request
if review.APIVersion != "admission.k8s.io/v1" || review.Kind != "AdmissionReview" || req == nil || req.UID == "" {
http.Error(w, "AdmissionReview v1 request and UID required", http.StatusBadRequest)
return nil, nil, false
}
if req.Kind.Group != "" || req.Kind.Version != "v1" || req.Kind.Kind != "Pod" ||
(req.Operation != admissionv1.Create && req.Operation != admissionv1.Update) {
http.Error(w, "Only Pod CREATE/UPDATE is supported", http.StatusBadRequest)
return nil, nil, false
}
var pod corev1.Pod
if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
http.Error(w, "Invalid Pod JSON", http.StatusBadRequest)
return nil, nil, false
}
return req, &pod, true
}
func writeReview(w http.ResponseWriter, response admissionv1.AdmissionResponse) {
review := admissionv1.AdmissionReview{
TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"},
Response: &response,
}
data, err := json.Marshal(review)
if err != nil {
http.Error(w, "Response encoding failed", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(data)
}
func writePatch(w http.ResponseWriter, req *admissionv1.AdmissionRequest, patches []map[string]interface{}) {
response := admissionv1.AdmissionResponse{UID: req.UID, Allowed: true}
if len(patches) > 0 {
data, err := json.Marshal(patches)
if err != nil {
http.Error(w, "Patch encoding failed", http.StatusInternalServerError)
return
}
patchType := admissionv1.PatchTypeJSONPatch
response.PatchType, response.Patch = &patchType, data
}
writeReview(w, response)
}
func deny(w http.ResponseWriter, req *admissionv1.AdmissionRequest, message string) {
writeReview(w, admissionv1.AdmissionResponse{
UID: req.UID, Allowed: false,
Result: &metav1.Status{Status: "Failure", Reason: metav1.StatusReasonForbidden, Code: 403, Message: message},
})
}
func mutateHandler(w http.ResponseWriter, r *http.Request) {
req, pod, ok := readPodReview(w, r)
if !ok { return }
if pod.Labels["injected-by"] == "mutating-webhook" {
writePatch(w, req, nil)
return
}
if pod.Labels == nil { pod.Labels = map[string]string{} }
pod.Labels["injected-by"] = "mutating-webhook"
// Add the whole map, preserving existing labels. Works when labels was absent.
writePatch(w, req, []map[string]interface{}{{"op": "add", "path": "/metadata/labels", "value": pod.Labels}})
}
```
```go
func validateHandler(w http.ResponseWriter, r *http.Request) {
req, pod, ok := readPodReview(w, r)
if !ok { return }
images := []string{}
for _, c := range pod.Spec.Containers { images = append(images, c.Image) }
for _, c := range pod.Spec.InitContainers { images = append(images, c.Image) }
for _, c := range pod.Spec.EphemeralContainers { images = append(images, c.Image) }
for _, image := range images {
if strings.Contains(image, "@") { continue } // Digest references have no implicit latest tag.
last := image[strings.LastIndex(image, "/")+1:]
if !strings.Contains(last, ":") || strings.HasSuffix(last, ":latest") {
deny(w, req, "Use an explicit non-latest tag or digest for every container")
return
}
}
writeReview(w, admissionv1.AdmissionResponse{UID: req.UID, Allowed: true})
}
```
### 인기 있는 어드미션 컨트롤러 프로젝트
1. **OPA Gatekeeper**: Open Policy Agent를 사용한 정책 적용
2. **Kyverno**: YAML 기반 정책 엔진
3. **Istio**: 서비스 메시 사이드카 주입
4. **cert-manager**: TLS 인증서 관리
## API 서버 확장
API 서버 확장은 Kubernetes API 서버에 새로운 엔드포인트를 추가하는 방법입니다.
### 확장 API 서버
확장 API 서버는 Kubernetes API 서버와 별도로 실행되는 서버로, 커스텀 API를 제공합니다:
```yaml
# APIService 정의
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1.example.com
spec:
group: example.com
version: v1
groupPriorityMinimum: 1000
versionPriority: 15
service:
name: example-api
namespace: default
caBundle:
```
### 확장 API 서버 구현
확장 API 서버는 다음과 같은 구성 요소로 이루어집니다:
1. **API 서버**: Kubernetes API 서버와 유사한 인터페이스 제공
2. **리소스 핸들러**: 특정 리소스 유형에 대한 요청 처리
3. **스토리지 백엔드**: 리소스 데이터 저장
아래는 구현 개요이며 독립 실행 프로그램이 아닙니다. k8s.io 의존성에 맞는 공식 sample-apiserver에서 시작하여 TLS, 위임 인증/권한, 실제 example.com/v1 타입, 저장소 및 종료 컨텍스트를 구성합니다. APIService와 서버의 group/version이 일치해야 합니다.
```go
// 확장 API 서버 예시
func main() {
// 서버 구성
config := genericapiserver.NewRecommendedConfig(apiserver.Codecs)
config.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(
sampleopenapi.GetOpenAPIDefinitions,
openapi.NewDefinitionNamer(apiserver.Scheme),
)
config.EnableIndex = true
config.EnableDiscovery = true
// 서버 생성
server, err := config.Complete().New("sample-apiserver", genericapiserver.NewEmptyDelegate())
if err != nil {
log.Fatalf("Error creating server: %v", err)
}
// API 그룹 정보 설정
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(
samplev1.GroupName,
apiserver.Scheme,
metav1.ParameterCodec,
apiserver.Codecs,
)
// 스토리지 설정
apiGroupInfo.VersionedResourcesStorageMap["v1"] = map[string]rest.Storage{
"widgets": NewWidgetStorage(),
}
// API 그룹 설치
if err := server.InstallAPIGroup(&apiGroupInfo); err != nil {
log.Fatalf("Error installing API group: %v", err)
}
// 서버 실행
if err := server.PrepareRun().Run(stopCh); err != nil {
log.Fatalf("Error running server: %v", err)
}
}
```
### 애그리게이션 레이어
애그리게이션 레이어는 여러 API 서버를 단일 API 서버처럼 보이게 합니다:
```
+-----------------+
| |
| kube-apiserver |
| |
+-------+---------+
|
v
+--------------------+--------------------+
| |
| |
+-----------v-----------+ +------------v------------+
| | | |
| metrics-server | | example-apiserver |
| | | |
+-----------------------+ +-------------------------+
```
## 스케줄러 확장
스케줄러 확장은 Kubernetes 스케줄러의 동작을 사용자 정의하는 방법입니다.
### 스케줄러 프레임워크
Kubernetes 1.15부터 도입된 스케줄러 프레임워크는 플러그인을 통해 스케줄링 파이프라인의 다양한 단계를 확장할 수 있습니다:
1. **Queue Sort**: 스케줄링 큐의 포드 정렬
2. **Pre-filter**: 필터링 전 포드 및 클러스터 상태 검사
3. **Filter**: 포드를 실행할 수 없는 노드 필터링
4. **Post-filter**: 필터링 후 작업 수행
5. **Pre-score**: 점수 계산 전 작업 수행
6. **Score**: 노드에 점수 부여
7. **Normalize Score**: 점수 정규화
8. **Reserve**: 포드를 위한 리소스 예약
9. **Permit**: 포드 스케줄링 허용, 거부 또는 지연
10. **Pre-bind**: 바인딩 전 작업 수행
11. **Bind**: 포드를 노드에 바인딩
12. **Post-bind**: 바인딩 후 작업 수행
### 스케줄러 구성
스케줄러 구성 예시:
```yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: true
profiles:
- schedulerName: custom-scheduler
pluginConfig:
- name: NodeResourcesFit
args:
scoringStrategy:
type: MostAllocated
resources:
- name: cpu
weight: 1
- name: memory
weight: 1
```
### 사용자 정의 스케줄러
아래 Deployment는 빌드한 custom-scheduler 이미지, 앞의 config.yaml을 담은 custom-scheduler-config ConfigMap 및 스케줄링/Lease RBAC 권한이 있는 ServiceAccount가 필요합니다. in-cluster 인증을 사용하며 워커에서 관리형 컨트롤 플레인의 scheduler.conf를 마운트하지 않습니다. schedulerName은 Pod와 일치해야 합니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: custom-scheduler
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: custom-scheduler
template:
metadata:
labels:
app: custom-scheduler
spec:
serviceAccountName: custom-scheduler
nodeSelector:
kubernetes.io/os: linux
containers:
- name: custom-scheduler
image: example/custom-scheduler:REPLACE_WITH_TESTED_RELEASE
command: [/custom-scheduler, --config=/etc/scheduler/config.yaml]
volumeMounts:
- name: config
mountPath: /etc/scheduler
readOnly: true
volumes:
- name: config
configMap:
name: custom-scheduler-config
```
포드에서 사용자 정의 스케줄러 지정:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: custom-scheduled-pod
spec:
schedulerName: custom-scheduler
containers:
- name: container
image: nginx:1.30.4
```
## 클라우드 컨트롤러 매니저
클라우드 컨트롤러 매니저는 Kubernetes와 클라우드 제공업체 간의 인터페이스를 제공합니다.
### 클라우드 컨트롤러 매니저 구성 요소
클라우드 컨트롤러 매니저는 다음과 같은 컨트롤러로 구성됩니다:
1. **노드 컨트롤러**: 클라우드 제공업체 API를 통해 노드 정보 업데이트
2. **라우트 컨트롤러**: 클라우드 네트워크에 라우트 설정
3. **서비스 컨트롤러**: 클라우드 로드 밸런서 생성, 업데이트, 삭제
### AWS 클라우드 컨트롤러 매니저
AWS CCM은 AWS 위의 **자체 관리 Kubernetes**를 위한 외부 클라우드 컨트롤러입니다. Kubernetes 버전에 맞는 cloud-provider-aws 릴리스를 선택하고 공식 기존 클러스터 설치 절차의 ServiceAccount/RBAC, IAM, 클러스터 태그, 노드 이름 및 `--cloud-provider=external` 전환 조건을 확인합니다. 현재 이미지 경로는 `registry.k8s.io/provider-aws/cloud-controller-manager`입니다. VPC/서브넷 태그는 임의의 cloud.conf 키로 대신할 수 없습니다.
EKS의 AWS 관리 컨트롤 플레인에 이 DaemonSet을 설치하거나 scheduler.conf를 마운트할 수 없습니다. EKS에서는 서비스가 관리하는 클라우드 통합과 지원되는 AWS Load Balancer Controller 또는 Auto Mode 기능을 사용하며 동일 리소스를 여러 컨트롤러가 소유하지 않게 합니다.
## CSI(Container Storage Interface)
CSI는 Kubernetes와 스토리지 시스템 간의 표준 인터페이스를 제공합니다.
다음 다이어그램은 CSI의 아키텍처와 작동 방식을 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-11-extending-kubernetes-4.html)
### CSI 아키텍처
CSI는 다음과 같은 구성 요소로 이루어집니다:
1. **CSI 컨트롤러 플러그인**: 볼륨 생성, 삭제, 스냅샷 등의 작업 처리
2. **CSI 노드 플러그인**: 볼륨 마운트, 언마운트 등의 작업 처리
3. **CSI 드라이버**: 특정 스토리지 시스템과 통합하는 구현체
```
+-------------------+
| |
| Kubernetes |
| (External |
| Provisioner) |
| |
+--------+----------+
|
| gRPC
v
+--------+----------+
| |
| CSI Driver |
| |
+--------+----------+
|
| Storage Protocol
v
+--------+----------+
| |
| Storage System |
| |
+-------------------+
```
### CSI 드라이버 배포
아래는 드라이버 개발용 템플릿이며 완전한 설치 매니페스트가 아닙니다. 공급자 문서에 따라 드라이버 이미지/인수, 호환되는 sidecar 버전, ServiceAccount/RBAC 및 CSIDriver 등록을 준비합니다. NodePlugin은 Linux에서 실행되며 지정된 호스트 경로와 권한이 필요합니다.
```yaml
# CSI 컨트롤러 서비스
apiVersion: apps/v1
kind: Deployment
metadata:
name: csi-controller
spec:
replicas: 1
selector:
matchLabels:
app: csi-controller
template:
metadata:
labels:
app: csi-controller
spec:
serviceAccountName: csi-controller
nodeSelector:
kubernetes.io/os: linux
containers:
- name: csi-provisioner
image: registry.k8s.io/sig-storage/csi-provisioner:REPLACE_WITH_COMPATIBLE_RELEASE
args:
- "--csi-address=$(ADDRESS)"
- "--v=5"
env:
- name: ADDRESS
value: /var/lib/csi/sockets/pluginproxy/csi.sock
volumeMounts:
- name: socket-dir
mountPath: /var/lib/csi/sockets/pluginproxy/
- name: csi-attacher
image: registry.k8s.io/sig-storage/csi-attacher:REPLACE_WITH_COMPATIBLE_RELEASE
args:
- "--csi-address=$(ADDRESS)"
- "--v=5"
env:
- name: ADDRESS
value: /var/lib/csi/sockets/pluginproxy/csi.sock
volumeMounts:
- name: socket-dir
mountPath: /var/lib/csi/sockets/pluginproxy/
- name: csi-driver
image: example/csi-driver:v1.0.0
args:
- "--endpoint=$(CSI_ENDPOINT)"
- "--nodeid=$(NODE_ID)"
env:
- name: CSI_ENDPOINT
value: unix:///var/lib/csi/sockets/pluginproxy/csi.sock
- name: NODE_ID
valueFrom:
fieldRef:
fieldPath: spec.nodeName
volumeMounts:
- name: socket-dir
mountPath: /var/lib/csi/sockets/pluginproxy/
volumes:
- name: socket-dir
emptyDir: {}
---
# CSI 노드 서비스
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: csi-node
spec:
selector:
matchLabels:
app: csi-node
template:
metadata:
labels:
app: csi-node
spec:
serviceAccountName: csi-node
nodeSelector:
kubernetes.io/os: linux
hostNetwork: true
containers:
- name: csi-node-driver-registrar
image: registry.k8s.io/sig-storage/csi-node-driver-registrar:REPLACE_WITH_COMPATIBLE_RELEASE
args:
- "--csi-address=$(ADDRESS)"
- "--kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)"
- "--v=5"
env:
- name: ADDRESS
value: /csi/csi.sock
- name: DRIVER_REG_SOCK_PATH
value: /var/lib/kubelet/plugins/example.csi.k8s.io/csi.sock
volumeMounts:
- name: plugin-dir
mountPath: /csi
- name: registration-dir
mountPath: /registration
- name: csi-driver
image: example/csi-driver:v1.0.0
args:
- "--endpoint=$(CSI_ENDPOINT)"
- "--nodeid=$(NODE_ID)"
env:
- name: CSI_ENDPOINT
value: unix:///csi/csi.sock
- name: NODE_ID
valueFrom:
fieldRef:
fieldPath: spec.nodeName
securityContext:
privileged: true
volumeMounts:
- name: plugin-dir
mountPath: /csi
- name: pods-mount-dir
mountPath: /var/lib/kubelet/pods
mountPropagation: "Bidirectional"
volumes:
- name: plugin-dir
hostPath:
path: /var/lib/kubelet/plugins/example.csi.k8s.io
type: DirectoryOrCreate
- name: registration-dir
hostPath:
path: /var/lib/kubelet/plugins_registry
type: Directory
- name: pods-mount-dir
hostPath:
path: /var/lib/kubelet/pods
type: Directory
```
### 스토리지 클래스 및 PVC
CSI 드라이버를 사용하는 스토리지 클래스 및 PVC 예시:
```yaml
# 스토리지 클래스
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: example-csi
provisioner: example.csi.k8s.io
parameters:
type: ssd
csi.storage.k8s.io/fstype: ext4
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
---
# PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: example-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: example-csi
```
### 인기 있는 CSI 드라이버
1. **AWS EBS CSI 드라이버**: AWS EBS 볼륨 관리
2. **AWS EFS CSI 드라이버**: AWS EFS 파일 시스템 관리
3. **GCE PD CSI 드라이버**: Google Compute Engine 영구 디스크 관리
4. **Azure Disk CSI 드라이버**: Azure 디스크 관리
5. **Ceph RBD CSI 드라이버**: Ceph RBD 볼륨 관리
6. **NFS CSI 드라이버**: NFS 볼륨 관리
## CNI(Container Network Interface)
CNI는 Kubernetes와 네트워킹 솔루션 간의 표준 인터페이스를 제공합니다.
다음 다이어그램은 CNI의 아키텍처와 작동 방식을 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-11-extending-kubernetes-5.html)
### CNI 아키텍처
CNI는 다음과 같은 구성 요소로 이루어집니다:
1. **CNI 플러그인**: 컨테이너 네트워크 인터페이스 구성
2. **IPAM 플러그인**: IP 주소 할당 및 관리
3. **메타 플러그인**: 여러 플러그인을 조합하여 사용
```
+-------------------+
| |
| Kubernetes |
| CRI runtime |
| (via kubelet) |
+--------+----------+
|
| CNI Spec
v
+--------+----------+
| |
| CNI Plugin |
| |
+--------+----------+
|
| Network Configuration
v
+--------+----------+
| |
| Network |
| |
+-------------------+
```
### CNI 플러그인 구성
아래 단일 노드 bridge/host-local 예제는 CNI 규약을 설명합니다. 클러스터에는 노드별 고유 서브넷과 노드 간 라우팅이 필요합니다. 최신 kubelet이 직접 호출하는 것이 아니라 CRI 런타임이 CNI를 호출합니다.
```json
{
"cniVersion": "0.4.0",
"name": "example-network",
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.244.0.0/24",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}
```
### 인기 있는 CNI 플러그인
1. **Calico**: 네트워크 정책 및 보안 기능이 강화된 CNI
2. **Flannel**: 간단한 오버레이 네트워크 제공
3. **Cilium**: eBPF 기반의 네트워킹 및 보안 솔루션
4. **Weave Net(보관됨)**: 과거 멀티 호스트 네트워킹 프로젝트이며 유지 관리되는 대안 검토
5. **AWS VPC CNI**: AWS VPC와 통합된 CNI
6. **Azure CNI**: Azure 가상 네트워크와 통합된 CNI
7. **Antrea**: Open vSwitch 기반의 네트워킹 솔루션
### CNI 플러그인 설치
Calico CNI 플러그인 설치 예시:
```bash
# Use the official Calico installation guide for your distribution.
# Select a supported release and inspect the operator/custom-resources manifests.
# Do not install a second primary CNI over an existing cluster network.
kubectl get nodes -o wide
kubectl -n kube-system get daemonsets
```
## 디바이스 플러그인
디바이스 플러그인은 Kubernetes와 특수 하드웨어 간의 인터페이스를 제공합니다.
### 디바이스 플러그인 아키텍처
디바이스 플러그인은 다음과 같은 구성 요소로 이루어집니다:
1. **디바이스 플러그인 서버**: 디바이스 검색, 할당, 초기화 등의 작업 처리
2. **kubelet**: 디바이스 플러그인과 통신하여 포드에 디바이스 할당
```
+-------------------+
| |
| Kubernetes |
| (kubelet) |
| |
+--------+----------+
|
| Device Plugin API
v
+--------+----------+
| |
| Device Plugin |
| |
+--------+----------+
|
| Device Management
v
+--------+----------+
| |
| Hardware Device |
| |
+-------------------+
```
### NVIDIA GPU 디바이스 플러그인
NVIDIA GPU 디바이스 플러그인 배포 예시:
호환되는 NVIDIA 드라이버/Container Toolkit과 런타임을 먼저 구성합니다. 공식 NVIDIA device-plugin Helm 차트에서 검증한 버전을 고정하고 Linux GPU 노드만 선택합니다. 운영자가 이미 GPU Operator/Auto Mode로 플러그인을 관리하는 경우 중복 설치하지 않습니다.
### GPU 요청 포드
GPU를 요청하는 포드 예시:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
restartPolicy: Never
nodeSelector:
kubernetes.io/os: linux
containers:
- name: cuda-container
image: nvidia/cuda:REPLACE_WITH_DRIVER_COMPATIBLE_TAG
command: ["nvidia-smi"]
resources:
limits:
nvidia.com/gpu: 1
```
### 인기 있는 디바이스 플러그인
1. **NVIDIA GPU 디바이스 플러그인**: NVIDIA GPU 관리
2. **AMD GPU 디바이스 플러그인**: AMD GPU 관리
3. **FPGA 디바이스 플러그인**: FPGA 디바이스 관리
4. **InfiniBand 디바이스 플러그인**: InfiniBand 디바이스 관리
5. **SRIOV 네트워크 디바이스 플러그인**: SR-IOV 네트워크 디바이스 관리
## Amazon EKS에서의 확장 기능
EKS 버전 지원은 upstream과 다릅니다. 2026-09-11 기준 EKS 표준 지원은 1.34–1.36이며 애드온/컨트롤러의 개별 호환성도 확인합니다.
Amazon EKS는 다양한 확장 기능을 지원하여 Kubernetes 클러스터의 기능을 확장할 수 있습니다.
다음 다이어그램은 Amazon EKS의 확장 기능 아키텍처를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-core-11-extending-kubernetes-6.html)
### EKS 추가 기능
Amazon EKS는 다음과 같은 추가 기능을 제공합니다:
1. **Amazon VPC CNI**: AWS VPC와 통합된 네트워킹
2. **CoreDNS**: 클러스터 내 DNS 서비스
3. **kube-proxy**: 네트워크 프록시
4. **Amazon EBS CSI 드라이버**: EBS 볼륨 관리
5. **AWS Load Balancer Controller**: 지원되는 Helm/매니페스트로 별도 설치하며 모든 확장이 EKS 관리형 애드온이라고 가정하지 않음
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set cluster name}"
KUBERNETES_VERSION=$(aws eks describe-cluster --name "$CLUSTER_NAME" --query cluster.version --output text)
aws eks list-addons --cluster-name "$CLUSTER_NAME"
aws eks describe-addon-versions --addon-name amazon-ebs-csi-driver --kubernetes-version "$KUBERNETES_VERSION"
# Choose a compatible version and prepare the controller's scoped IAM role first.
: "${ADDON_VERSION:?Set reviewed compatible add-on version}"
: "${EBS_ROLE_ARN:?Set EBS CSI IRSA role ARN}"
aws eks create-addon --cluster-name "$CLUSTER_NAME" --addon-name amazon-ebs-csi-driver \
--addon-version "$ADDON_VERSION" --service-account-role-arn "$EBS_ROLE_ARN"
# For an existing installation, use update-addon instead of create-addon.
# To stop EKS management while retaining the workload (not uninstall it):
# aws eks delete-addon --cluster-name "$CLUSTER_NAME" --addon-name amazon-ebs-csi-driver --preserve
```
### AWS Controllers for Kubernetes(ACK)
ACK는 Kubernetes에서 AWS 리소스를 관리할 수 있게 해주는 오퍼레이터 모음입니다:
```bash
set -euo pipefail
: "${ACK_VERSION:?Set a reviewed S3 controller chart version}"
: "${AWS_REGION:?Set target service region}"
# First prepare ack-s3-controller ServiceAccount with scoped IRSA/Pod Identity permissions.
helm upgrade --install ack-s3-controller oci://public.ecr.aws/aws-controllers-k8s/s3-chart \
--version "$ACK_VERSION" --namespace ack-system --create-namespace \
--set aws.region="$AWS_REGION" --set serviceAccount.create=false \
--set serviceAccount.name=ack-s3-controller
# Creating a Bucket CR provisions a real AWS resource: review IAM, naming and retention first.
```
아래 Bucket 예제는 컨트롤러가 설치되어 있고 IAM 권한이 있으면 실제 AWS 리소스를 생성합니다. 전역적으로 고유한 이름으로 바꾸고 수명 주기/보존 정책을 검토합니다. Kubernetes 객체 삭제 시 버킷도 삭제될 수 있으므로 데이터 보존 요구에 맞는 컨트롤러 삭제 정책을 설정합니다.
```yaml
apiVersion: s3.services.k8s.aws/v1alpha1
kind: Bucket
metadata:
name: example-bucket
spec:
name: replace-with-your-globally-unique-bucket-name
```
### AWS Load Balancer Controller
AWS Load Balancer Controller는 Kubernetes 서비스 및 인그레스를 AWS 로드 밸런서와 통합합니다:
```yaml
# ALB 인그레스 예시
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
spec:
ingressClassName: alb
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: example-service
port:
number: 80
```
### IAM Roles for Service Accounts(IRSA)
IRSA는 Kubernetes 서비스 계정에 AWS IAM 역할을 연결하여 포드가 AWS 서비스에 안전하게 접근할 수 있게 합니다:
```bash
# OIDC 제공자 생성
: "${S3_READ_POLICY_ARN:?Set a customer-managed policy restricted to your bucket/prefix}"
eksctl utils associate-iam-oidc-provider \
--cluster my-cluster \
--approve
# IAM 역할 및 서비스 계정 생성
eksctl create iamserviceaccount \
--cluster my-cluster \
--namespace default \
--name my-service-account \
--attach-policy-arn "$S3_READ_POLICY_ARN" \
--approve
# 서비스 계정을 사용하는 포드
cat < **예제 기준 버전**: Kubernetes 1.35.8, Go 1.27.1
> **마지막 업데이트**: 2026년 9월 11일
Kubernetes 스케줄러는 포드를 어떤 노드에 배치할지 결정하는 중요한 구성 요소입니다. 기본 스케줄러는 대부분의 경우 잘 작동하지만, 특정 요구 사항이 있는 경우 커스텀 스케줄러를 구현할 수 있습니다. 이 장에서는 EKS에서 커스텀 스케줄러를 구현하는 방법을 알아보겠습니다.
## 실습 환경 설정
이 문서의 예제를 따라하기 위해서는 다음과 같은 도구와 환경이 필요합니다:
### 필수 도구
* 클러스터 API 서버와 마이너 버전 차이가 1 이내인 kubectl
* 아래 재현 예제용 Go 1.27.1 및 Python 3
* Linux 워커 노드가 있는 폐기 가능한 Kubernetes 1.35 실습 클러스터. EKS는 먼저 [AWS 버전 일정](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html)을 확인
이 문서의 프레임워크 인터페이스와 설정은 Kubernetes **1.35.8**을 기준으로 확인했습니다. 예제 기준 버전이며, 최신 Kubernetes 또는 EKS 버전이라는 뜻이 아닙니다. 스케줄러를 클러스터 마이너 버전에 맞추고 업그레이드 시 플러그인·기능 게이트·RBAC를 다시 검증하세요. Kubernetes 모듈의 staging 의존성은 `v0.0.0`을 사용하므로 `go get k8s.io/kubernetes`만으로는 독립 모듈의 의존성을 해결할 수 없습니다.
### 개발 환경 설정
```bash
mkdir -p custom-scheduler
cd custom-scheduler
# Generate a standalone module from the pinned upstream staging-module list.
python3 - <<'PY'
from pathlib import Path
import re
from urllib.request import urlopen
version = "v1.35.8"
staging_version = "v0.35.8"
url = f"https://raw.githubusercontent.com/kubernetes/kubernetes/{version}/go.mod"
with urlopen(url, timeout=30) as response:
upstream = response.read().decode()
modules = re.findall(r"^\s*(k8s\.io/[\w-]+) => \./staging/src/\1\s*$", upstream, re.M)
if not modules:
raise RuntimeError("No staging modules found; review upstream go.mod")
text = f"module example.com/custom-scheduler\n\ngo 1.27.1\n\nrequire k8s.io/kubernetes {version}\n\nreplace (\n"
text += "".join(f"\t{m} => {m} {staging_version}\n" for m in modules)
Path("go.mod").write_text(text + ")\n")
PY
```
## 스케줄링 개요
### Kubernetes 스케줄링 프로세스
Kubernetes 스케줄링 프로세스는 다음과 같은 단계로 이루어집니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-01-custom-scheduler-part1-10.html)
### 스케줄링 단계 상세 설명
1. **필터링 단계 (Filtering Phase)**
* 포드가 실행될 수 있는 적합한 노드를 찾는 단계
* 각 필터 플러그인은 노드가 포드를 호스팅할 수 있는지 여부를 결정
* 하나의 필터라도 실패하면 해당 노드는 후보에서 제외됨
2. **점수 매기기 단계 (Scoring Phase)**
* 필터링을 통과한 노드에 점수를 매기는 단계
* 각 점수 플러그인은 선택적인 정규화 이후 0–100 범위의 점수를 반환
* 가중치를 적용하여 최종 점수 계산
3. **바인딩 단계 (Binding Phase)**
* 최고 점수를 받은 노드에 포드를 할당하는 단계
* Kubernetes API를 통해 포드-노드 바인딩 정보 업데이트
## 커스텀 스케줄러가 필요한 경우
다음과 같은 경우에 커스텀 스케줄러를 고려할 수 있습니다:
1. **특수한 하드웨어 요구 사항**: GPU, FPGA, 특수 네트워크 장치 등
2. **복잡한 워크로드 배치 규칙**: 특정 노드 그룹에 특정 워크로드 배치
3. **비용 최적화**: 스팟 인스턴스와 온디맨드 인스턴스 간의 최적 배치
4. **지역성 요구 사항**: 데이터 지역성을 고려한 워크로드 배치
5. **다중 스케줄러 시나리오**: 다양한 워크로드 유형에 대해 여러 스케줄러 사용
### 실제 사용 사례
| 산업 | 사용 사례 | 커스텀 스케줄러 이점 |
| --- | ---------- | ----------------------------- |
| 금융 | 고주파 거래 시스템 | 지연 시간 최소화를 위한 네트워크 토폴로지 인식 배치 |
| 의료 | 의료 영상 처리 | GPU 배치와 데이터 지역성 선호 |
| 통신 | 5G 네트워크 기능 | 레이블이 있는 네트워크 장치에 대한 배치 제약 |
| 소매 | 계절적 트래픽 처리 | 비용 효율적인 스팟 인스턴스 활용 최적화 |
| 미디어 | 비디오 트랜스코딩 | 워크로드 특성에 따른 CPU/GPU 노드 선택 |
1. **필터링(Filtering)**: 포드를 실행할 수 있는 노드를 식별합니다. 이 단계에서는 리소스 요구 사항, 노드 선택기, 노드 어피니티, 테인트 및 톨러레이션 등을 고려합니다.
2. **점수 매기기(Scoring)**: 필터링된 노드에 점수를 매깁니다. 이 단계에서는 노드의 리소스 사용량, 포드 간 어피니티, 노드 어피니티 등을 고려합니다.
3. **바인딩(Binding)**: 가장 높은 점수를 받은 노드에 포드를 할당합니다.
코드를 작성하기 전에 device plugin, required/preferred affinity, taint/toleration, topology spread로 요구 사항을 표현할 수 있는지 확인하세요. 일반적인 GPU 요청에는 커스텀 스케줄러가 필요하지 않습니다.
### 기본 스케줄러의 한계
기본 스케줄러는 다음과 같은 한계가 있을 수 있습니다:
1. **특정 하드웨어 요구 사항**: GPU, FPGA 등 특수 하드웨어에 대한 고급 스케줄링 로직이 필요할 수 있습니다.
2. **복잡한 어피니티 규칙**: 기본 어피니티 규칙으로는 표현하기 어려운 복잡한 배치 제약 조건이 있을 수 있습니다.
3. **사용자 정의 메트릭**: 기본 스케줄러가 고려하지 않는 사용자 정의 메트릭을 기반으로 스케줄링해야 할 수 있습니다.
4. **특정 도메인 지식**: 특정 애플리케이션 도메인에 특화된 스케줄링 로직이 필요할 수 있습니다.
## 커스텀 스케줄러 구현 방법
커스텀 스케줄러를 구현하는 방법은 크게 세 가지가 있습니다:
1. **다중 스케줄러 접근 방식**: 기본 스케줄러와 함께 커스텀 스케줄러를 실행합니다.
2. **스케줄러 확장(Extender) 접근 방식**: 기본 스케줄러를 확장하여 추가 필터링 및 우선순위 기능을 제공합니다.
3. **스케줄러 프레임워크 플러그인**: Kubernetes 1.15부터 도입된 스케줄러 프레임워크를 사용하여 플러그인을 개발합니다.
### 다중 스케줄러 접근 방식
다중 스케줄러 접근 방식에서는 기본 스케줄러와 함께 커스텀 스케줄러를 실행합니다. 포드를 생성할 때 `schedulerName` 필드를 사용하여 어떤 스케줄러를 사용할지 지정할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-01-custom-scheduler-part1-11.html)
#### 커스텀 스케줄러 구현
upstream 스케줄러 명령을 별도 스케줄러의 기반으로 사용합니다. 그러면 `NodeResourcesFit`, `TaintToleration`, `NodeAffinity`, `VolumeBinding` 등 기본 플러그인을 유지할 수 있습니다. 첫 번째 Ready 노드를 골라 바인딩 API를 호출하는 방식은 이러한 검사를 우회합니다.
다음을 `main.go`로 저장합니다. 아직 사용자 정의 배치 정책은 추가하지 않습니다. 뒤의 점수 함수와 퀴즈에서 확장 지점을 다룹니다.
```go
package main
import (
"os"
"k8s.io/component-base/cli"
"k8s.io/kubernetes/cmd/kube-scheduler/app"
)
func main() {
os.Exit(cli.Run(app.NewSchedulerCommand()))
}
```
#### 커스텀 스케줄러 배포
아래 `Dockerfile`을 저장하고 로컬 빌드 명령을 실행한 다음, 승인된 레지스트리 절차로 이미지를 배포하세요. 매니페스트의 `registry.example.com/...`을 해당 이미지로 바꾸고 가능하면 digest로 고정합니다. 바이너리와 이미지 아키텍처는 워커 노드와 일치해야 합니다.
```dockerfile
FROM gcr.io/distroless/static-debian12:nonroot
COPY custom-scheduler /custom-scheduler
ENTRYPOINT ["/custom-scheduler"]
```
```bash
go mod tidy
CGO_ENABLED=0 go build -buildvcs=false -trimpath -o custom-scheduler .
# Build for the same architecture as the scheduler's worker nodes.
docker build -t custom-scheduler:v1.35.8-1 .
```
이 구성은 실습용이며 운영 환경에서 HA를 검증한 레시피가 아닙니다. 스케줄러 Pod 자체는 `default-scheduler`를 사용하므로 `custom-scheduler`가 없어도 시작할 수 있습니다. 두 복제본은 `scheduler-lab`의 **동일한 Lease**를 공유합니다. 다른 스케줄러 그룹에는 별도 Lease가 필요합니다. 선호 anti-affinity는 배치에 도움을 주지만 호스트·AZ 분리를 보장하지 않습니다.
관리자는 참조한 기본 RBAC 역할이 존재하는지 확인해야 합니다. `system:kube-scheduler`와 `system:volume-scheduler`는 바인딩·선점을 포함한 강력한 클러스터 전체 스케줄링 권한을 부여합니다. `schedulerName`은 보안 경계가 아닙니다. 별도 Role로 자체 Lease 권한을 추가하며 Kubernetes 기본 역할은 변경하지 않습니다. HTTPS 엔드포인트는 비공개로 유지하고 대상 클러스터에서 리소스 크기와 장애 동작을 검증하세요.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: scheduler-lab
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: custom-scheduler
namespace: scheduler-lab
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: scheduler-lab-scheduling
subjects:
- kind: ServiceAccount
name: custom-scheduler
namespace: scheduler-lab
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:kube-scheduler
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: scheduler-lab-volumes
subjects:
- kind: ServiceAccount
name: custom-scheduler
namespace: scheduler-lab
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:volume-scheduler
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: scheduler-lab-authentication
namespace: kube-system
subjects:
- kind: ServiceAccount
name: custom-scheduler
namespace: scheduler-lab
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: extension-apiserver-authentication-reader
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: custom-scheduler-leader-election
namespace: scheduler-lab
rules:
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["create"]
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
resourceNames: ["custom-scheduler"]
verbs: ["get", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: custom-scheduler-leader-election
namespace: scheduler-lab
subjects:
- kind: ServiceAccount
name: custom-scheduler
namespace: scheduler-lab
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: custom-scheduler-leader-election
---
apiVersion: v1
kind: ConfigMap
metadata:
name: custom-scheduler-config
namespace: scheduler-lab
data:
config.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: true
resourceLock: leases
resourceName: custom-scheduler
resourceNamespace: scheduler-lab
leaseDuration: 15s
renewDeadline: 10s
retryPeriod: 2s
profiles:
- schedulerName: custom-scheduler
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: custom-scheduler
namespace: scheduler-lab
spec:
replicas: 2
selector:
matchLabels:
app: custom-scheduler
template:
metadata:
labels:
app: custom-scheduler
spec:
serviceAccountName: custom-scheduler
securityContext:
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
nodeSelector:
kubernetes.io/os: linux
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: custom-scheduler
containers:
- name: custom-scheduler
image: registry.example.com/training/custom-scheduler:v1.35.8-1
args:
- --config=/etc/scheduler/config.yaml
- --cert-dir=/tmp
ports:
- name: https
containerPort: 10259
livenessProbe:
httpGet:
path: /healthz
port: https
scheme: HTTPS
initialDelaySeconds: 15
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 512Mi
volumeMounts:
- name: config
mountPath: /etc/scheduler
readOnly: true
- name: tmp
mountPath: /tmp
volumes:
- name: config
configMap:
name: custom-scheduler-config
- name: tmp
emptyDir: {}
```
#### 커스텀 스케줄러 사용
포드를 생성할 때 `schedulerName` 필드를 사용하여 커스텀 스케줄러를 지정합니다:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
schedulerName: custom-scheduler
containers:
- name: nginx
image: nginx:1.30.4
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
memory: 128Mi
```
## EKS에서의 커스텀 스케줄러 구현
Amazon EKS에서 커스텀 스케줄러를 구현할 때는 다음 사항을 고려합니다:
1. **Kubernetes 인증·인가**: 클러스터 내부 스케줄러는 ServiceAccount 토큰과 Kubernetes RBAC를 사용합니다. EC2·CloudWatch 같은 AWS API도 호출할 때만 추가 IAM 권한이 필요하며, 해당 워크로드 자격 증명에 필요한 권한만 부여합니다.
2. **관리형 컨트롤 플레인**: 자체 보조 스케줄러를 워커 노드에 실행합니다. 관리형 기본 스케줄러의 프로세스·플래그·플러그인 레지스트리를 수정할 수 있다고 가정하지 마세요.
3. **컴퓨팅 경계**: 이 예제는 EC2 워커 노드 대상입니다. [EKS Fargate는 AWS 관리형 스케줄링·어드미션 컨트롤러](https://docs.aws.amazon.com/eks/latest/userguide/fargate.html)를 사용합니다. Fargate 노드를 선택하거나 `custom-scheduler`를 지정한다고 Fargate 용량이 생성되지는 않습니다.
4. **토폴로지·인스턴스 레이블**: 기존 노드 레이블, 리소스 요청, affinity, topology spread를 먼저 활용합니다. 커스텀 스케줄러는 용량을 생성하거나 지연 시간을 보장하지 않습니다.
### EKS 커스텀 스케줄러 아키텍처
각 스케줄러는 Kubernetes API를 감시하고 일치하는 Pod를 위한 **자체 캐시와 큐**를 유지합니다. API 서버는 Pod 상태를 저장·제공하며 공용 스케줄링 큐를 소유하지 않습니다. EC2·CloudWatch 연동은 선택 사항이고, 느리거나 실패한 메트릭 조회에는 제한된 타임아웃과 명시적인 대체 정책이 필요합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-01-custom-scheduler-part1-12.html)
### EKS 특화 스케줄링 고려 사항
아래 함수는 `preferences` 패키지의 별도 파일에 저장하는 **선호도 점수 예제**입니다. 앞의 바이너리에 자동 등록되지 않습니다. 기본 필터를 통과한 뒤 검증된 프레임워크 `Score` 플러그인에서 호출하세요. **0점인 노드도 후보로 남습니다.** 필수 조건은 필터나 required affinity로 표현하고 점수는 0–100 범위를 유지합니다.
#### 1. 인스턴스 유형 인식 스케줄링
아래 c5/m5/r5 가중치는 레이블 기반 선호도를 설명하기 위한 값입니다. 실측 성능 순위나 해당 세대의 구매 권고가 아닙니다. 실제 워크로드는 적합한 인스턴스를 벤치마크한 뒤 선호도를 명시적으로 설정하세요.
```go
package preferences
import (
"strings"
v1 "k8s.io/api/core/v1"
)
// Illustrative weights, not measured performance or current purchase advice.
func ScoreInstanceType(node *v1.Node) int64 {
switch {
case strings.HasPrefix(node.Labels["node.kubernetes.io/instance-type"], "c5."):
return 100
case strings.HasPrefix(node.Labels["node.kubernetes.io/instance-type"], "m5."):
return 50
case strings.HasPrefix(node.Labels["node.kubernetes.io/instance-type"], "r5."):
return 30
default:
return 10
}
}
```
#### 2. 가용 영역 분산 스케줄링
기본 `PodTopologySpread` 플러그인과 `topologySpreadConstraints`를 먼저 사용하세요. 별도 점수가 필요하면 스케줄러 캐시에서 대상 워크로드와 모든 적격 AZ를 포함한 일관된 사이클 스냅샷을 만듭니다. Pod가 0개인 AZ도 포함하고 할당·assume된 Pod를 적절히 집계합니다. Running Pod만 세면 예약된 요청을 놓칩니다. 후보 노드마다 API list/get을 반복하거나 API 오류를 임의의 개수로 바꾸지 마세요.
```go
package preferences
import (
"fmt"
v1 "k8s.io/api/core/v1"
)
// counts is a consistent snapshot for one workload, including empty eligible zones.
// Build it once per scheduling cycle, not by making API calls for every node.
func ScoreAZ(node *v1.Node, counts map[string]int) (int64, error) {
zone := node.Labels["topology.kubernetes.io/zone"]
count, ok := counts[zone]
if zone == "" || !ok {
return 0, fmt.Errorf("missing eligible-zone snapshot for node %q", node.Name)
}
maxCount := 0
for _, n := range counts {
if n < 0 {
return 0, fmt.Errorf("negative pod count")
}
if n > maxCount {
maxCount = n
}
}
if maxCount == 0 {
return 100, nil
}
return int64(100 * (maxCount - count) / maxCount), nil
}
```
#### 3. 스팟 인스턴스 인식 스케줄링
[EKS 관리형 노드 그룹](https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html)은 `eks.amazonaws.com/capacityType`의 `SPOT` / `ON_DEMAND` 값을, [Karpenter](https://karpenter.sh/docs/concepts/nodepools/)는 `karpenter.sh/capacity-type`의 `spot` / `on-demand` / `reserved` 값을 사용합니다. `node.kubernetes.io/lifecycle`은 이에 해당하는 표준 레이블이 아닙니다. 아래 함수는 Pod의 명시적인 선호도만 적용하고 알 수 없는 레이블에는 중립 점수를 줍니다. 용량 유형이 필수이면 required affinity를 사용하세요. 점수만으로 Spot 가격이나 중단 허용 여부를 판단할 수 없습니다.
```go
package preferences
import v1 "k8s.io/api/core/v1"
func ScoreCapacityType(node *v1.Node, pod *v1.Pod) int64 {
preferred := pod.Labels["lifecycle-preference"]
if preferred != "spot" && preferred != "on-demand" {
return 50
}
actual := node.Labels["karpenter.sh/capacity-type"]
if actual == "" {
switch node.Labels["eks.amazonaws.com/capacityType"] {
case "SPOT":
actual = "spot"
case "ON_DEMAND":
actual = "on-demand"
}
}
if actual != "spot" && actual != "on-demand" && actual != "reserved" {
return 50
}
if actual == preferred {
return 100
}
return 0
}
```
#### 4. GPU 워크로드 스케줄링
device plugin이 GPU 리소스를 광고하고 어드미션을 거친 Pod가 해당 리소스를 요청해야 합니다. 확장 리소스에서 limit만 지정하면 동일한 request가 기본 설정되며, 0인 request는 GPU 수요가 아닙니다. `NodeResourcesFit`은 유효 요청량을 기존·assume된 요청을 제외한 allocatable과 비교합니다. `Capacity`만으로 남은 GPU를 알 수 없습니다.
이 함수는 GPU가 필요 없는 워크로드의 GPU 노드 사용을 낮게 평가할 뿐입니다. 버전에 맞는 리소스 도우미로 init/sidecar/overhead를 고려하며, API 기본값이 적용된 Pod를 전제로 합니다. GPU 적격성을 판정하거나 모든 DRA 리소스 모델을 처리하거나 기본 리소스 필터를 대체하지 않습니다.
```go
package preferences
import (
v1 "k8s.io/api/core/v1"
resourcehelper "k8s.io/component-helpers/resource"
)
// pod must be API-defaulted; limits-only extended resources acquire requests.
// NodeResourcesFit still decides whether the effective request fits.
func ScoreGPU(node *v1.Node, pod *v1.Pod) int64 {
requests := resourcehelper.PodRequests(pod, resourcehelper.PodResourcesOptions{})
gpuRequest := requests[v1.ResourceName("nvidia.com/gpu")]
if gpuRequest.Sign() > 0 {
return 50
}
gpuAllocatable := node.Status.Allocatable[v1.ResourceName("nvidia.com/gpu")]
if gpuAllocatable.Sign() > 0 {
return 0
}
return 100
}
```
## 결론
이 장에서는 Kubernetes 스케줄링 프로세스의 개요와 다중 스케줄러 접근 방식을 사용하여 커스텀 스케줄러를 구현하는 방법을 알아보았습니다. 또한 EKS 클러스터에서 커스텀 스케줄러를 구현할 때 고려해야 할 사항들을 살펴보았습니다.
다음 장에서는 스케줄러 확장(Extender) 접근 방식과 스케줄러 프레임워크 플러그인을 사용하여 커스텀 스케줄러를 구현하는 방법을 알아보겠습니다.
## 검증 범위와 참고 자료
Go 명령·플러그인·설정은 고정된 의존성 버전으로 로컬 검증합니다. 이번 감사에서 클러스터 배포, 이미지 push, AWS 호출, 배치 벤치마크 또는 HA 장애 전환 테스트는 실행하지 않았습니다.
* [다중 스케줄러 구성](https://kubernetes.io/docs/tasks/extend-kubernetes/configure-multiple-schedulers/) — 구조·RBAC 패턴 참고용이며 오래된 이미지·빌드 예제는 버전 지침으로 사용하지 않음
* [스케줄러 설정](https://kubernetes.io/docs/reference/scheduling/config/)
* [스케줄링 프레임워크](https://kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/)
* [Kubernetes 1.35.8 프레임워크 인터페이스](https://github.com/kubernetes/kubernetes/blob/v1.35.8/staging/src/k8s.io/kube-scheduler/framework/interface.go)
* [Kubernetes 1.35.8 기본 플러그인](https://github.com/kubernetes/kubernetes/blob/v1.35.8/pkg/scheduler/apis/config/v1/default_plugins.go)
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/scheduling/02-custom-scheduler-part1-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/scheduling/02-custom-scheduler-part2
----------------------------------------
# Part 2: 구현
> **예제 기준 버전**: Kubernetes 1.35.8, Go 1.27.1
> **마지막 업데이트**: 2026년 9월 11일
[Part1의 모듈 설정과 전체 보조 스케줄러 RBAC/Deployment](https://www.atomai.click/kubernetes-docs/llms/ko/scheduling/01-custom-scheduler-part1.md)를 사용합니다. 아래는 해당 스케줄러의 서로 다른 두 확장 방법입니다. 예제는 로컬에서 확인하며 EKS 배포·GPU 실행·TLS 롤아웃·운영 장애 전환은 검증하지 않았습니다. 다른 Kubernetes 마이너 버전의 Go 인터페이스 호환성을 가정하지 말고 다시 검증하세요.
## 스케줄러 확장(Extender) 접근 방식
스케줄러 확장 접근 방식은 기본 스케줄러의 기능을 확장하는 방법입니다. 이 접근 방식에서는 기본 스케줄러가 HTTP 요청을 통해 외부 서비스(스케줄러 확장)를 호출하여 추가 필터링 및 우선순위 기능을 제공합니다.
### 스케줄러 확장 아키텍처
다음 다이어그램은 스케줄러 확장 접근 방식의 아키텍처를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-02-custom-scheduler-part2-10.html)
### 스케줄러 확장 워크플로우
스케줄러 확장의 워크플로우는 다음과 같습니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-02-custom-scheduler-part2-0.html)
### 스케줄러 확장 구현
extender는 설정한 **filter**, **prioritize**, **preempt**, **bind** HTTP callback 중 필요한 것을 구현합니다. 이 예제는 filter와 prioritize만 제공합니다. extender의 `PreFilter`·`PreScore` HTTP hook은 없으며, 이는 프로세스 내부 프레임워크 확장 지점입니다. 기본 바인더를 유지하려면 `bindVerb`를 생략합니다.
extender 우선순위는 **0–10**, 프레임워크 점수는 정규화 이후 **0–100**입니다. 필수 조건은 Filter에서 검사하세요. 이 고정 버전의 스케줄러는 prioritize 요청이 실패하면 기록 후 해당 점수를 제외하므로, 점수만으로 필수 조건을 강제할 수 없습니다.
스케줄러는 API를 감시하고 자체 큐·캐시를 유지합니다. 기본 필터 다음에 extender 필터를 적용하고 프레임워크 점수와 extender 선호도를 합칩니다. API 서버가 Pod를 공용 스케줄링 큐에 밀어 넣는 구조가 아닙니다. 위 아키텍처 그림의 `/prefilter`·`/prescore` 표기는 HTTP extender 계약에 해당하지 않습니다.
이 예제는 upstream 리소스 검사를 유지하면서 **GPU별 메모리 선언 조건**을 추가합니다. GPU를 검색하거나 남은 VRAM을 측정하거나 장치 메모리를 예약하지 않습니다.
공통 정책은 API 기본값이 적용된 Pod, 설치된 device plugin, MIG·time-slicing이 없는 동종 GPU 노드, 각 적격 GPU의 최소 메모리를 나타내는 관리자 관리 노드 레이블 `training.example.com/gpu-memory-mib`를 전제로 합니다. 이 키는 **실습용 사용자 키**이며 NVIDIA 표준 탐색 레이블이 아닙니다. 하드웨어 목록을 검증한 뒤 레이블을 지정하고 이기종·공유 GPU에는 장치를 인식하는 할당 방식을 사용하세요. 노드 레이블만으로 나중에 할당될 장치 특성을 보장할 수 없습니다.
Part1 모듈에 다음 파일을 저장합니다.
**`gpupolicy/policy.go`**
```go
package gpupolicy
import (
"fmt"
"strconv"
v1 "k8s.io/api/core/v1"
resourcehelper "k8s.io/component-helpers/resource"
)
const (
MinMemoryAnnotation = "training.example.com/min-gpu-memory-mib"
NodeMemoryLabel = "training.example.com/gpu-memory-mib"
ScoreCeilingMiB = int64(80 * 1024)
)
// These are lab policy keys, not automatically populated NVIDIA labels.
// NodeMemoryLabel must describe the minimum memory per eligible GPU on a
// homogeneous, non-shared GPU node, not total or currently free VRAM.
type Requirement struct {
HasGPU bool
MinMemoryMiB int64
}
func FromPod(pod *v1.Pod) (Requirement, error) {
if pod == nil {
return Requirement{}, fmt.Errorf("missing pod")
}
requests := resourcehelper.PodRequests(pod, resourcehelper.PodResourcesOptions{})
gpu := requests[v1.ResourceName("nvidia.com/gpu")]
req := Requirement{HasGPU: gpu.Sign() > 0}
if raw, exists := pod.Annotations[MinMemoryAnnotation]; exists {
value, err := strconv.ParseInt(raw, 10, 64)
if err != nil || value <= 0 || value > 1024*1024 {
return req, fmt.Errorf("minimum GPU memory must be 1..1048576 MiB")
}
if !req.HasGPU {
return req, fmt.Errorf("minimum GPU memory requires a positive GPU request")
}
req.MinMemoryMiB = value
}
return req, nil
}
func nodeMemory(node *v1.Node) (int64, error) {
if node == nil {
return 0, fmt.Errorf("missing node")
}
value, err := strconv.ParseInt(node.Labels[NodeMemoryLabel], 10, 64)
if err != nil || value <= 0 || value > 1024*1024 {
return 0, fmt.Errorf("missing or invalid administrator GPU-memory label")
}
return value, nil
}
// FitsMemory checks only the declared memory label. The default scheduler
// filters must still check free GPU counts, taints, affinity, volumes, etc.
func FitsMemory(req Requirement, node *v1.Node) (bool, string) {
if req.MinMemoryMiB == 0 {
return true, ""
}
memory, err := nodeMemory(node)
if err != nil {
return false, err.Error()
}
if memory < req.MinMemoryMiB {
return false, "GPU memory label is below the required minimum"
}
return true, ""
}
// The 80-GiB ceiling is a chosen scoring scale, not a hardware maximum.
func Score100(req Requirement, node *v1.Node) int64 {
if !req.HasGPU {
return 0
}
memory, err := nodeMemory(node)
if err != nil {
return 0
}
if memory >= ScoreCeilingMiB {
return 100
}
return memory * 100 / ScoreCeilingMiB
}
```
**`extenderserver/handler.go`**
핸들러는 `nodeCacheCapable: false` 계약을 사용하고 실습 요청을 4 MiB·512개 노드로 제한하며 잘못되거나 누락된 입력을 거부합니다. 전달받은 후보의 부분집합만 반환합니다. 실제 클러스터는 제한값을 별도로 산정해야 합니다. 필수 메모리 레이블이 잘못되면 해당 노드를 제외하며, 다른 Pod를 선점해도 레이블 문제는 해결되지 않습니다.
```go
package extenderserver
import (
"encoding/json"
"errors"
"io"
"net/http"
"example.com/custom-scheduler/gpupolicy"
v1 "k8s.io/api/core/v1"
extender "k8s.io/kube-scheduler/extender/v1"
)
const maxBodyBytes = 4 << 20
func NewHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("POST /filter", filter)
mux.HandleFunc("POST /prioritize", prioritize)
return mux
}
func readArgs(w http.ResponseWriter, r *http.Request) (extender.ExtenderArgs, gpupolicy.Requirement, bool) {
var args extender.ExtenderArgs
body := http.MaxBytesReader(w, r.Body, maxBodyBytes)
defer body.Close()
decoder := json.NewDecoder(body)
if err := decoder.Decode(&args); err != nil {
var large *http.MaxBytesError
status := http.StatusBadRequest
if errors.As(err, &large) {
status = http.StatusRequestEntityTooLarge
}
http.Error(w, "invalid or oversized extender request", status)
return args, gpupolicy.Requirement{}, false
}
if err := decoder.Decode(new(any)); err != io.EOF {
http.Error(w, "expected exactly one JSON object", http.StatusBadRequest)
return args, gpupolicy.Requirement{}, false
}
if args.Pod == nil || args.Nodes == nil || args.NodeNames != nil {
http.Error(w, "Pod and Nodes required; nodeCacheCapable must be false", http.StatusBadRequest)
return args, gpupolicy.Requirement{}, false
}
if len(args.Nodes.Items) > 512 {
http.Error(w, "lab candidate-node limit exceeded", http.StatusRequestEntityTooLarge)
return args, gpupolicy.Requirement{}, false
}
seen := make(map[string]bool, len(args.Nodes.Items))
for _, node := range args.Nodes.Items {
if node.Name == "" || seen[node.Name] {
http.Error(w, "missing or duplicate node name", http.StatusBadRequest)
return args, gpupolicy.Requirement{}, false
}
seen[node.Name] = true
}
req, err := gpupolicy.FromPod(args.Pod)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return args, req, false
}
return args, req, true
}
func writeJSON(w http.ResponseWriter, value any) {
data, err := json.Marshal(value)
if err != nil {
http.Error(w, "response encoding failed", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(data)
}
func filter(w http.ResponseWriter, r *http.Request) {
args, req, ok := readArgs(w, r)
if !ok {
return
}
result := extender.ExtenderFilterResult{
Nodes: &v1.NodeList{Items: []v1.Node{}},
FailedAndUnresolvableNodes: extender.FailedNodesMap{},
}
for _, node := range args.Nodes.Items {
if fits, reason := gpupolicy.FitsMemory(req, &node); fits {
result.Nodes.Items = append(result.Nodes.Items, node)
} else {
// Preempting other Pods cannot change a hardware inventory label.
result.FailedAndUnresolvableNodes[node.Name] = reason
}
}
writeJSON(w, result)
}
func prioritize(w http.ResponseWriter, r *http.Request) {
args, req, ok := readArgs(w, r)
if !ok {
return
}
result := make(extender.HostPriorityList, 0, len(args.Nodes.Items))
for _, node := range args.Nodes.Items {
result = append(result, extender.HostPriority{
Host: node.Name, Score: gpupolicy.Score100(req, &node) / 10,
})
}
// The wire response is an array, not a hostPriorities wrapper object.
writeJSON(w, result)
}
```
**`cmd/extender/main.go`**
서버는 지정된 client CA가 서명한 클라이언트 인증서를 요구합니다. CA를 스케줄러 클라이언트용으로 제한하고 기존 Secret 관리 절차로 인증서를 교체하세요.
```go
package main
import (
"crypto/tls"
"crypto/x509"
"flag"
"log"
"net/http"
"os"
"time"
"example.com/custom-scheduler/extenderserver"
)
func main() {
certFile := flag.String("tls-cert", "/etc/extender-tls/tls.crt", "server certificate")
keyFile := flag.String("tls-key", "/etc/extender-tls/tls.key", "server private key")
caFile := flag.String("client-ca", "/etc/extender-tls/client-ca.crt", "trusted scheduler client CA")
flag.Parse()
caPEM, err := os.ReadFile(*caFile)
if err != nil {
log.Fatal(err)
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(caPEM) {
log.Fatal("client CA contains no certificates")
}
server := &http.Server{
Addr: ":8443",
Handler: extenderserver.NewHandler(),
ReadHeaderTimeout: 2 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 30 * time.Second,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: roots,
},
}
log.Fatal(server.ListenAndServeTLS(*certFile, *keyFile))
}
```
### 스케줄러 확장 배포
Part1과 같은 고정 모듈·Go 버전으로 `./cmd/extender`를 빌드하고 정적 바이너리를 non-root 이미지로 패키징한 뒤, 아래 레지스트리 자리표시자를 실제 이미지·digest로 바꿉니다. Pod는 매 요청으로 스케줄링 데이터를 받으므로 Kubernetes API 토큰이 필요하지 않습니다.
다음을 `Dockerfile.extender`로 저장하고 로컬 빌드합니다. 승인된 레지스트리를 선택하고 배포 이미지에는 게시한 결과를 지정하세요.
```dockerfile
FROM gcr.io/distroless/static-debian12:nonroot
COPY scheduler-extender /scheduler-extender
ENTRYPOINT ["/scheduler-extender"]
```
```bash
go mod tidy
CGO_ENABLED=0 go build -buildvcs=false -trimpath -o scheduler-extender ./cmd/extender
: "${REGISTRY:?Set the approved registry/repository prefix}"
docker build -f Dockerfile.extender -t "$REGISTRY/scheduler-extender:v1.35.8-1" .
```
`scheduler-lab`에 미리 필요한 항목은 `tls.crt`, `tls.key`, `client-ca.crt`가 있는 `extender-server-tls` Secret과, 스케줄러 클라이언트용 `tls.crt`, `tls.key`, 서버 검증용 `ca.crt`가 있는 `extender-client-tls` Secret입니다. 서버 인증서는 `scheduler-extender.scheduler-lab.svc`를 포함해야 합니다. 이 문서는 인증서나 Secret을 생성하지 않습니다. NetworkPolicy를 집행하는 CNI가 필요합니다. TCP readiness probe는 포트가 열렸는지만 확인하며 상호 TLS·스케줄링 성공을 검증하지 않습니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: scheduler-extender
namespace: scheduler-lab
spec:
replicas: 2
selector:
matchLabels:
app: scheduler-extender
template:
metadata:
labels:
app: scheduler-extender
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
nodeSelector:
kubernetes.io/os: linux
containers:
- name: extender
image: registry.example.com/training/scheduler-extender:v1.35.8-1
ports:
- name: https
containerPort: 8443
readinessProbe:
tcpSocket:
port: https
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
memory: 128Mi
volumeMounts:
- name: tls
mountPath: /etc/extender-tls
readOnly: true
volumes:
- name: tls
secret:
secretName: extender-server-tls
defaultMode: 0440
---
apiVersion: v1
kind: Service
metadata:
name: scheduler-extender
namespace: scheduler-lab
spec:
selector:
app: scheduler-extender
ports:
- name: https
port: 8443
targetPort: https
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: scheduler-extender
namespace: scheduler-lab
spec:
podSelector:
matchLabels:
app: scheduler-extender
policyTypes: [Ingress, Egress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: scheduler-lab
podSelector:
matchLabels:
app: custom-scheduler
ports:
- protocol: TCP
port: 8443
egress: []
```
### 스케줄러 구성
EKS에서도 **자체 보조 스케줄러**를 구성합니다. EKS는 관리형 기본 스케줄러의 설정 파일을 노출하지 않으며 EC2 워커의 `/etc/kubernetes/scheduler.conf`를 컨트롤 플레인 자격 증명처럼 마운트해서는 안 됩니다.
1. 아래 설정을 저장합니다. Part1의 프로필·별도 Lease와 기본 필터를 유지하고 extender 서버 인증서를 검증합니다. `ignorable: false`이면 filter 실패 시 해당 스케줄링 시도를 차단합니다. 앞서 설명한 prioritize 오류 동작은 바뀌지 않습니다. 이 extender는 남은 장치 수를 계산하지 않으므로 GPU에 `ignoredByScheduler: true`를 설정하지 마세요.
```yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: true
resourceLock: leases
resourceName: custom-scheduler
resourceNamespace: scheduler-lab
leaseDuration: 15s
renewDeadline: 10s
retryPeriod: 2s
profiles:
- schedulerName: custom-scheduler
extenders:
- urlPrefix: https://scheduler-extender.scheduler-lab.svc:8443
filterVerb: filter
prioritizeVerb: prioritize
weight: 1
enableHTTPS: true
tlsConfig:
caFile: /etc/extender-client/ca.crt
certFile: /etc/extender-client/tls.crt
keyFile: /etc/extender-client/tls.key
httpTimeout: 2s
nodeCacheCapable: false
ignorable: false
```
2. 대응하는 ConfigMap을 `extender-scheduler-config.yaml`로 저장합니다:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: extender-scheduler-config
namespace: scheduler-lab
data:
config.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: true
resourceLock: leases
resourceName: custom-scheduler
resourceNamespace: scheduler-lab
leaseDuration: 15s
renewDeadline: 10s
retryPeriod: 2s
profiles:
- schedulerName: custom-scheduler
extenders:
- urlPrefix: https://scheduler-extender.scheduler-lab.svc:8443
filterVerb: filter
prioritizeVerb: prioritize
weight: 1
enableHTTPS: true
tlsConfig:
caFile: /etc/extender-client/ca.crt
certFile: /etc/extender-client/tls.crt
keyFile: /etc/extender-client/tls.key
httpTimeout: 2s
nodeCacheCapable: false
ignorable: false
```
3. 아래는 독립 Deployment가 아닌 **strategic merge patch**입니다. `extender-scheduler-patch.yaml`로 저장합니다. Part1 Deployment의 설정 볼륨을 바꾸고 클라이언트 인증서를 추가하되 ServiceAccount·probe·리소스·명령은 유지합니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: custom-scheduler
namespace: scheduler-lab
spec:
template:
spec:
volumes:
- name: config
configMap:
name: extender-scheduler-config
- name: extender-client
secret:
secretName: extender-client-tls
defaultMode: 288
containers:
- name: custom-scheduler
volumeMounts:
- name: extender-client
mountPath: /etc/extender-client
readOnly: true
```
extender를 배포하고 이미지 자리표시자를 교체한 뒤 폐기 가능한 실습 환경에만 적용합니다:
```bash
kubectl -n scheduler-lab apply -f extender-scheduler-config.yaml
kubectl -n scheduler-lab patch deployment custom-scheduler --type=strategic --patch-file=extender-scheduler-patch.yaml
kubectl -n scheduler-lab rollout restart deployment/custom-scheduler
kubectl -n scheduler-lab rollout status deployment/custom-scheduler
```
스케줄러는 시작할 때 설정을 읽습니다. ConfigMap 파일이 갱신되는 것만으로 설정을 다시 읽지 않으며, `subPath` 마운트라면 일반적인 projected 파일 갱신도 적용되지 않습니다.
## 스케줄러 프레임워크 플러그인
Kubernetes 1.15부터 도입된 스케줄러 프레임워크는 플러그인 기반 아키텍처를 제공합니다. 이 접근 방식을 사용하면 스케줄링 파이프라인의 다양한 단계에 플러그인을 구현할 수 있습니다.
### 스케줄러 프레임워크 아키텍처
다음 다이어그램은 스케줄러 프레임워크의 아키텍처를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-02-custom-scheduler-part2-11.html)
### 스케줄러 프레임워크 플러그인 구성
다음 다이어그램은 스케줄러 프레임워크 플러그인의 구성을 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-02-custom-scheduler-part2-12.html)
### 스케줄링 프레임워크 확장 포인트
프레임워크에는 `PreEnqueue`, `QueueSort`, `PreFilter`, `Filter`, `PostFilter`, `PreScore`, `Score`와 선택적 `NormalizeScore`, `Reserve`/`Unreserve`, `Permit`, `PreBind`, `Bind`, `PostBind`가 있습니다.
스케줄링 사이클은 순차적으로 노드를 선택하고 바인딩 사이클은 중첩될 수 있습니다. Reserve는 assume 상태를 기록하며 이후 실패하면 Unreserve가 해제합니다. PostFilter는 적합한 노드가 없을 때 선점을 시도할 수 있습니다. PostBind는 성공한 바인딩 이후 실행되며 이를 거부할 수 없습니다. 버전별로 추가 메서드가 필요한 확장 지점이 있으므로 대상 버전으로 컴파일하세요.
### 스케줄러 플러그인 구현
다음을 `gpuplugin/plugin.go`로 저장합니다. 앞의 공통 정책을 사용하며 `framework.NodeInfo`를 직접 받습니다. CycleState에서 읽을 수 있는 기본 `NodeInfoKey`는 없습니다. 이 플러그인은 선언된 메모리 레이블만 검사하며 기본 필터가 GPU 개수·taint·affinity·볼륨을 계속 처리합니다.
```go
package gpuplugin
import (
"context"
"example.com/custom-scheduler/gpupolicy"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
fwk "k8s.io/kube-scheduler/framework"
)
const Name = "GPUScheduler"
type Plugin struct{}
var _ fwk.FilterPlugin = &Plugin{}
var _ fwk.ScorePlugin = &Plugin{}
func (*Plugin) Name() string { return Name }
func (*Plugin) Filter(_ context.Context, _ fwk.CycleState, pod *v1.Pod, info fwk.NodeInfo) *fwk.Status {
if info == nil || info.Node() == nil {
return fwk.NewStatus(fwk.Error, "missing node")
}
req, err := gpupolicy.FromPod(pod)
if err != nil {
return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, err.Error())
}
if ok, reason := gpupolicy.FitsMemory(req, info.Node()); !ok {
return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, reason)
}
return nil
}
func (*Plugin) Score(_ context.Context, _ fwk.CycleState, pod *v1.Pod, info fwk.NodeInfo) (int64, *fwk.Status) {
if info == nil || info.Node() == nil {
return 0, fwk.NewStatus(fwk.Error, "missing node")
}
req, err := gpupolicy.FromPod(pod)
if err != nil {
return 0, fwk.AsStatus(err)
}
return gpupolicy.Score100(req, info.Node()), nil
}
func (*Plugin) ScoreExtensions() fwk.ScoreExtensions { return nil }
func New(_ context.Context, _ runtime.Object, _ fwk.Handle) (fwk.Plugin, error) {
return &Plugin{}, nil
}
```
### 스케줄러 플러그인 등록
플러그인을 등록하려면 아래 명령처럼 스케줄러 바이너리에 컴파일해야 합니다. 이 설정은 등록된 플러그인을 **활성화**하며 YAML만으로 Go 코드를 로드하지 않습니다. PreFilter·PreScore 인터페이스를 구현하지 않았다면 해당 지점에 활성화하지 마세요.
```yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: true
resourceLock: leases
resourceName: custom-scheduler
resourceNamespace: scheduler-lab
leaseDuration: 15s
renewDeadline: 10s
retryPeriod: 2s
profiles:
- schedulerName: custom-scheduler
plugins:
filter:
enabled:
- name: GPUScheduler
score:
enabled:
- name: GPUScheduler
weight: 10
```
## EKS에서의 스케줄러 프레임워크 구현
EKS에서는 Part1의 ServiceAccount/RBAC와 자체 프로필·Lease로 EC2 워커에 커스텀 스케줄러를 실행합니다. 클러스터 내부 API는 Kubernetes 자격 증명을 사용하며 ECR 게시 또는 선택적인 AWS API 호출에는 별도의 적절한 IAM 권한이 필요합니다. EKS Fargate 스케줄링은 AWS가 관리하고 GPU를 지원하지 않습니다. Fargate 프로필이 선택하지 않는 실습 네임스페이스를 사용하세요.
관리형 컨트롤 플레인에 플러그인 이미지만 주입하는 방식이 아니라 **플러그인이 포함된 스케줄러 바이너리**를 빌드합니다. 노드 레이블은 device plugin 리소스를 보완하며 GPU 용량을 생성하지 않습니다.
### EKS 스케줄러 프레임워크 아키텍처
보조 스케줄러는 API에서 Pod·Node 상태를 감시하고 등록된 플러그인을 내부에서 실행합니다. ECR은 이미지를 제공하며 CloudWatch 연동은 선택 사항입니다. 이 예제는 `GPUScheduler`만 구현합니다. 구조도에 표시한 Spot·AZ 플러그인은 별도 코드·테스트를 준비한 뒤 등록·활성화해야 합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-02-custom-scheduler-part2-13.html)
### EKS 스케줄러 프레임워크 구현 단계
1. **커스텀 플러그인 등록** (`cmd/gpu-scheduler/main.go`). upstream 명령은 이미 기본 플러그인을 등록하므로 다시 등록하면 이름 중복으로 실패합니다.
```go
package main
import (
"os"
"example.com/custom-scheduler/gpuplugin"
"k8s.io/component-base/cli"
"k8s.io/kubernetes/cmd/kube-scheduler/app"
)
func main() {
// Upstream already registers all built-in plugins. Register only our addition.
command := app.NewSchedulerCommand(app.WithPlugin(gpuplugin.Name, gpuplugin.New))
os.Exit(cli.Run(command))
}
```
2. **이미지 빌드**. 공통 정책과 플러그인을 저장하고 `go mod tidy`를 실행합니다. 이미지 아키텍처를 스케줄러의 Linux 워커와 맞추고 릴리스 절차에서는 기반 이미지를 digest로 고정하세요.
```dockerfile
FROM golang:1.27.1 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -buildvcs=false -trimpath -o /out/gpu-scheduler ./cmd/gpu-scheduler
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/gpu-scheduler /gpu-scheduler
ENTRYPOINT ["/gpu-scheduler"]
```
3. **레지스트리 절차로 게시**. 아래 명령은 레지스트리를 명시적으로 선택해야 합니다. 이번 감사에서는 실행하지 않았습니다:
```bash
: "${REGISTRY:?Set the approved registry/repository prefix}"
docker build -t "$REGISTRY/gpu-scheduler:v1.35.8-1" .
docker push "$REGISTRY/gpu-scheduler:v1.35.8-1"
```
4. **`gpu-scheduler-config.yaml` 저장**. 구현한 인터페이스만 활성화하고 upstream 기본값을 유지합니다:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: gpu-scheduler-config
namespace: scheduler-lab
data:
config.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: true
resourceLock: leases
resourceName: custom-scheduler
resourceNamespace: scheduler-lab
leaseDuration: 15s
renewDeadline: 10s
retryPeriod: 2s
profiles:
- schedulerName: custom-scheduler
plugins:
filter:
enabled:
- name: GPUScheduler
score:
enabled:
- name: GPUScheduler
weight: 10
```
5. **Part1 Deployment 갱신**. 이미지 자리표시자를 바꾼 뒤 아래 strategic merge patch를 `gpu-scheduler-patch.yaml`로 저장합니다. extender 설정의 대안이며 두 예제가 있다고 두 정책이 자동으로 함께 활성화되지는 않습니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: custom-scheduler
namespace: scheduler-lab
spec:
template:
spec:
volumes:
- name: config
configMap:
name: gpu-scheduler-config
containers:
- name: custom-scheduler
image: registry.example.com/training/gpu-scheduler:v1.35.8-1
```
```bash
kubectl -n scheduler-lab apply -f gpu-scheduler-config.yaml
kubectl -n scheduler-lab patch deployment custom-scheduler --type=strategic --patch-file=gpu-scheduler-patch.yaml
kubectl -n scheduler-lab rollout restart deployment/custom-scheduler
kubectl -n scheduler-lab rollout status deployment/custom-scheduler
```
6. **스케줄러와 GPU 요청**. 아래 BusyBox Pod를 준비된 실습 환경에서 실행하면 예약·배치만 확인할 수 있으며 CUDA를 실행하지 않습니다. 실제 GPU smoke test에는 검증된 CUDA 애플리케이션 이미지·호환 드라이버·장치 검사가 필요합니다. GPU 노드에 taint가 있으면 적절한 toleration을 추가하세요.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
annotations:
training.example.com/min-gpu-memory-mib: "16384"
spec:
schedulerName: custom-scheduler
restartPolicy: Never
containers:
- name: gpu-reservation-smoke
image: busybox:1.37.0
command: ["sh", "-c", "sleep 60"]
resources:
requests:
cpu: 100m
memory: 64Mi
nvidia.com/gpu: 1
limits:
memory: 128Mi
nvidia.com/gpu: 1
```
## 결론
이 장에서는 스케줄러 확장(Extender) 접근 방식과 스케줄러 프레임워크 플러그인을 사용하여 Custom Scheduler를 구현하는 방법을 알아보았습니다. 또한 EKS 클러스터에서 스케줄러 프레임워크를 구현하는 방법도 살펴보았습니다.
다음 장에서는 EKS에서의 Custom Scheduler 구현 사례와 모니터링 방법을 알아보겠습니다.
## 참고 자료와 검증 한계
* [스케줄러 설정과 extender 필드](https://kubernetes.io/docs/reference/scheduling/config/)
* [Kubernetes 1.35.8 extender wire 타입](https://github.com/kubernetes/kubernetes/blob/v1.35.8/staging/src/k8s.io/kube-scheduler/extender/v1/types.go)
* [고정 버전 프레임워크 인터페이스](https://github.com/kubernetes/kubernetes/blob/v1.35.8/staging/src/k8s.io/kube-scheduler/framework/interface.go)
* [GPU 스케줄링](https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/)
* [EKS Fargate 스케줄링과 제한](https://docs.aws.amazon.com/eks/latest/userguide/fargate.html)
로컬 코드·설정 검사는 실제 하드웨어 레이블, 드라이버 호환성, 인증서 발급, 클러스터 배치·용량·운영 가용성을 검증하지 않습니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/scheduling/02-custom-scheduler-part2-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/scheduling/03-custom-scheduler-part3
----------------------------------------
# Part 3: 고급 기능
> **예제 기준 버전**: Kubernetes 1.35.8, Go 1.27.1; Python 클라이언트 API 35.0.0
> **마지막 업데이트**: 2026년 9월 11일
[Part1 보조 스케줄러](https://www.atomai.click/kubernetes-docs/llms/ko/scheduling/01-custom-scheduler-part1.md)와 [Part2 프레임워크 인터페이스](https://www.atomai.click/kubernetes-docs/llms/ko/scheduling/02-custom-scheduler-part2.md)를 이용한 구현 패턴 예제입니다. 운영 배포 사례나 최적화 실측 보고서가 아닙니다. 로컬 코드·스키마 검사는 GPU 실행, 애플리케이션 준비 상태, 인증서 발급 또는 운영 가용성을 검증하지 않습니다.
## EKS에서의 커스텀 스케줄러 구현 사례
이 섹션은 EKS 스케줄링 정책 예제를 발전시킵니다. 실제 용량·장치 목록·트래픽·장애 동작은 대상 클러스터에서 검증해야 합니다.
### 사례 1: GPU 워크로드 최적화 스케줄러
AI/ML 워크로드를 실행하는 EKS 클러스터에서는 GPU 리소스를 효율적으로 활용하는 것이 중요합니다. 다음은 GPU 워크로드를 최적화하는 커스텀 스케줄러의 구현 사례입니다.
#### GPU 워크로드 최적화 스케줄러 아키텍처
GPU 배치 정책과 선택적인 메트릭 연동을 표현한 설계 예시입니다. 아래 구현은 캐시의 요청량을 사용하며 그림의 사용률 수집기가 구현·검증되었다는 뜻이 아닙니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-03-custom-scheduler-part3-10.html)
#### GPU 워크로드 스케줄링 워크플로우
다음 다이어그램은 GPU 워크로드 스케줄링 워크플로우를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-03-custom-scheduler-part3-11.html)
#### 요구 사항
1. 검증된 노드 레이블과 required node affinity로 GPU 메모리·모델 조건을 표현합니다.
2. `NodeResourcesFit`을 유지하여 유효 GPU 요청과 이미 할당·assume된 요청을 고려합니다.
3. 스케줄러 스냅샷으로 적합한 노드의 GPU를 모아 사용하도록 선호도를 줍니다. 실시간 사용률 가중치는 이 예제에 구현되지 않은 별도 연동입니다.
4. GPU 공유에는 적합한 device plugin·MIG·time-slicing 또는 DRA 할당 정책이 필요합니다. 노드 점수 함수가 장치를 분할하거나 프로세스별 GPU 메모리를 강제하지 않습니다.
#### 구현 접근 방식
이 사례에서는 스케줄러 프레임워크 플러그인 접근 방식을 사용합니다.
1. **레이블 전에 실제 목록을 검증합니다.** 아래 `training.example.com` 키는 관리자 정의 실습 레이블이며 NVIDIA 탐색 레이블이 아닙니다. 메모리는 공유하지 않는 동종 GPU 노드에서 각 적격 장치의 최소 메모리를 나타내야 합니다. GPU 수량은 `status.allocatable`에서 확인하며 개수 레이블은 남은 용량이 아닙니다.
```bash
kubectl get nodes -o custom-columns='NAME:.metadata.name,GPUS:.status.allocatable.nvidia\.com/gpu'
# Set only after verifying the actual node and per-device inventory.
: "${NODE_NAME:?Select a verified GPU node}"
: "${GPU_MODEL:?Set the observed model label value}"
: "${GPU_MEMORY_MIB:?Set verified minimum memory per GPU in MiB}"
kubectl label node "$NODE_NAME" \
"training.example.com/gpu-model=$GPU_MODEL" \
"training.example.com/gpu-memory-mib=$GPU_MEMORY_MIB" --overwrite
```
2. **기본 필터를 대체하지 않고 점수 플러그인을 추가합니다.** 다음을 `packing/plugin.go`로 저장합니다. 전체 개수 레이블·실시간 사용률 대신 allocatable에서 기존·assume된 요청과 신규 요청을 뺀 GPU 수량을 사용합니다. 0점은 노드 제외를 뜻하지 않습니다.
```go
package packing
import (
"context"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
resourcehelper "k8s.io/component-helpers/resource"
fwk "k8s.io/kube-scheduler/framework"
)
const Name = "GPUPacking"
const gpu v1.ResourceName = "nvidia.com/gpu"
type Plugin struct{}
var _ fwk.ScorePlugin = &Plugin{}
func (*Plugin) Name() string { return Name }
func (*Plugin) Score(_ context.Context, _ fwk.CycleState, pod *v1.Pod, info fwk.NodeInfo) (int64, *fwk.Status) {
if pod == nil || info == nil || info.Node() == nil {
return 0, fwk.NewStatus(fwk.Error, "missing node")
}
requests := resourcehelper.PodRequests(pod, resourcehelper.PodResourcesOptions{})
request := requests[gpu]
needed, exact := request.AsInt64()
if !exact || needed < 0 {
return 0, fwk.NewStatus(fwk.Error, "GPU request must be a non-negative integer")
}
if needed == 0 {
return 0, nil
}
allocatable := info.GetAllocatable().GetScalarResources()[gpu]
requested := info.GetRequested().GetScalarResources()[gpu]
remaining := allocatable - requested - needed
if remaining < 0 {
// Score cannot exclude a node. NodeResourcesFit must remain enabled.
return 0, nil
}
if remaining >= 10 {
return 0, nil
}
return 100 - remaining*10, nil
}
func (*Plugin) ScoreExtensions() fwk.ScoreExtensions { return nil }
func New(_ context.Context, _ runtime.Object, _ fwk.Handle) (fwk.Plugin, error) {
return &Plugin{}, nil
}
```
`cmd/gpu-packing-scheduler/main.go`에서 등록하고 `CGO_ENABLED=0 go build -buildvcs=false -o custom-scheduler ./cmd/gpu-packing-scheduler`로 Part1 이미지 절차를 사용합니다.
```go
package main
import (
"os"
"example.com/custom-scheduler/packing"
"k8s.io/component-base/cli"
"k8s.io/kubernetes/cmd/kube-scheduler/app"
)
func main() {
command := app.NewSchedulerCommand(app.WithPlugin(packing.Name, packing.New))
os.Exit(cli.Run(command))
}
```
기존 70:30 배치·사용률 아이디어는 `(packingScore * 7 + utilizationScore * 3) / 10`, `utilizationScore = (1 - utilization) * 100`이라는 설명용 식으로 표현할 수 있습니다. **이 바이너리에 연동되어 있지는 않습니다.** 실제 구현에는 일관되고 최신인 장치별 스냅샷, 0–1 범위의 유한 값, 제한된 수집 지연, 명시적인 누락 데이터 정책이 필요합니다. 메트릭 누락을 사용률0으로 처리하면 안 되며 사용률은 미할당 GPU 용량과 다릅니다.
3. **스케줄러 구성**:
```yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: true
resourceLock: leases
resourceName: custom-scheduler
resourceNamespace: scheduler-lab
leaseDuration: 15s
renewDeadline: 10s
retryPeriod: 2s
profiles:
- schedulerName: custom-scheduler
plugins:
score:
enabled:
- name: GPUPacking
weight: 10
```
4. **Pod에 필수 배치 조건을 표현합니다.** 모델·메모리 예시 값은 실제 검증한 목록으로 바꾸세요. `Gt: "40959"`는 정수 레이블이 최소40960MiB라는 뜻입니다. 아래 BusyBox는 준비된 실습 환경에서 실행할 경우 GPU 리소스를 예약할 뿐 CUDA를 실행하거나 GPU를 검증하지 않습니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: gpu-reservation-demo
namespace: scheduler-lab
spec:
schedulerName: custom-scheduler
automountServiceAccountToken: false
restartPolicy: Never
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: training.example.com/gpu-model
operator: In
values:
- A100
- key: training.example.com/gpu-memory-mib
operator: Gt
values:
- '40959'
containers:
- name: reservation
image: busybox:1.37.0
command:
- sh
- -c
- sleep 60
resources:
requests:
cpu: 100m
memory: 64Mi
nvidia.com/gpu: 2
limits:
memory: 128Mi
nvidia.com/gpu: 2
```
### 사례 2: 네트워크 지역성 최적화 스케줄러
EKS 클러스터에서 네트워크 비용을 최적화하기 위해 네트워크 지역성을 고려하는 커스텀 스케줄러를 구현할 수 있습니다.
#### 네트워크 지역성 최적화 스케줄러 아키텍처
다음 다이어그램은 네트워크 지역성 최적화 스케줄러의 아키텍처를 보여줍니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-03-custom-scheduler-part3-12.html)
#### 네트워크 지역성 최적화 워크플로우
다음 다이어그램은 네트워크 지역성 최적화 스케줄러의 워크플로우를 보여줍니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-03-custom-scheduler-part3-13.html)
새 스케줄러를 만들기 전에 required/preferred Pod affinity, topology spread, 스토리지 토폴로지를 확인합니다. 실제 서비스 의존성과 적격 AZ를 모델링하세요. 같은 위치 배치로 일부 교차 AZ 트래픽을 줄일 수 있지만 장애 집중이나 경합이 커질 수 있습니다. 레이블·점수만으로 지연 시간·비용 절감을 입증할 수 없습니다.
그림은 가능한 연동 구조이며 완성된 NetworkPolicy·서비스 메시·CloudWatch 구현이 아닙니다. 후보 노드별 점수 계산 밖에서 메트릭을 수집하고 신선도·타임아웃을 제한하며 실제 트래픽으로 비용·가용성의 균형을 검증하세요. 이 장에 배포된 네트워크 지역성 스케줄러는 없습니다.
## Pod Deletion Cost를 이용한 스케일 다운 최적화
Pod Deletion Cost는 Deployment의 ReplicaSet이 소유한 Pod를 포함해 **ReplicaSet 축소 시 적용하는 best-effort 삭제 선호도**입니다. 1.21에서 alpha로 시작해1.22에서 기본 활성화된 beta가 되었으며, 참조 문서에서도 beta입니다. StatefulSet의 ordinal 삭제, 독립 Pod 삭제, 축출, 노드 장애를 제어하지 않습니다.
### Pod Deletion Cost 개념
각 Pod의 `controller.kubernetes.io/pod-deletion-cost` 어노테이션에 값을 지정합니다. 비용은 **같은 ReplicaSet 내부**에서 비교하며 Deployment·노드 전체의 전역 우선순위가 아닙니다. 높은 값도 더 우선하는 조건이 허용할 때만 유지 선호도를 높입니다.
**주요 특성:**
* 어노테이션이 없으면0이며 음수를 포함한 signed int32의 십진수 값이 유효합니다. 기능이 활성화된 상태에서 잘못된 값은 거부됩니다.
* 고정한 컨트롤러에서는 할당 여부·Pod phase·준비 상태가 비용보다 우선하고, 복제본 배치 등 다른 기준이 뒤따릅니다.
* 삭제 순서를 보장하지 않습니다. 모든 템플릿 비용이 같으면 복제본을 구분하지 못합니다.
* 잦은 메트릭 기반 갱신을 피하고 큰 애플리케이션 상태 전환이나 애플리케이션이 제어하는 축소 직전의 갱신을 사용하세요.
### Pod Deletion Cost 아키텍처
그림은 **할당·phase·준비 상태 등 관련 조건이 비교 가능한 경우**의 비용 선호도입니다. 확정적인 삭제 순서가 아닙니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-03-custom-scheduler-part3-0.html)
### 사용 사례
#### 1. 캐시가 워밍업된 Pod 보호
각 Pod의 비용을0으로 시작하는 Deployment입니다. **사용자 애플리케이션의 계약 예제**이므로 이미지를 바꾸고8080포트의 `/readyz`가 실제 워밍업 완료를 반영하도록 구현해야 합니다. 이번 감사에서는 해당 앱을 빌드·실행하지 않았습니다. 일정 시간이 지났다는 이유만으로 높은 비용을 주면 안 됩니다.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: deletion-cost-lab
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cache-app
namespace: deletion-cost-lab
spec:
replicas: 5
selector:
matchLabels:
app: cache-app
template:
metadata:
labels:
app: cache-app
annotations:
controller.kubernetes.io/pod-deletion-cost: '0'
spec:
automountServiceAccountToken: false
containers:
- name: app
image: registry.example.com/training/cache-app:validated
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /readyz
port: http
periodSeconds: 5
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_UID
valueFrom:
fieldRef:
fieldPath: metadata.uid
```
특정 복제본의 실제 워밍업을 확인한 뒤 권한이 있는 운영자·컨트롤러가 그 Pod의 어노테이션을 갱신할 수 있습니다. Deployment 템플릿을 바꾸면 롤아웃과 새 ReplicaSet 생성이 발생합니다.
```bash
: "${POD_NAME:?Select a verified warm replica of cache-app}"
kubectl -n deletion-cost-lab annotate pod "$POD_NAME" \
controller.kubernetes.io/pod-deletion-cost=100 --overwrite
```
예제 Deployment는 API 토큰을 마운트하지 않습니다. 아래 선택적 동적 갱신 함수에는 별도로 설정·인가한 Kubernetes 클라이언트가 필요합니다. 네임스페이스 RBAC의 `patch pods`는 호출한 Pod 자신으로 자동 제한되지 않습니다. 신뢰할 수 있는 컨트롤러나 적절한 자격 증명·어드미션 제약을 사용하세요. 이 예제는 그러한 운영 정책을 설치하지 않습니다.
#### 2. 활성 연결이 있는 Pod 보호
활성 연결 수는 유지 선호도의 한 입력이 될 수 있습니다. 비용이 삭제를 막지 않으므로 graceful shutdown과 연결 드레이닝은 여전히 필요합니다. 이 라이브러리는 갱신을 직렬화하고 큰 구간별 비용·Pod UID 검사를 사용하며 어노테이션만 패치하고 같은 값은 다시 쓰지 않습니다. 어노테이션의 단일 작성자와 Pod 템플릿에 이미 존재하는 annotations 객체를 전제로 합니다.
설정된 `client-go` 클라이언트와 어드미션된 Pod 이름·네임스페이스·UID를 전달합니다. Pod 내부 연동이면 식별자는 Downward API에서 얻을 수 있지만 식별자를 얻었다고 API 권한이 생기지는 않습니다. 연결 이벤트마다 호출하지 말고 제어하는 상태 전환이나 직접 제어하는 축소 직전에 `UpdateDeletionCost(ctx)`를 호출합니다.
```go
package deletioncost
import (
"context"
"encoding/json"
"fmt"
"strconv"
"sync"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
)
const Annotation = "controller.kubernetes.io/pod-deletion-cost"
type ConnectionTracker struct {
client kubernetes.Interface
namespace, podName string
uid types.UID
connectionsMu, updateMu sync.Mutex
activeConnections int64
lastCost int32
lastCostSet bool
}
func NewConnectionTracker(client kubernetes.Interface, namespace, podName string, uid types.UID) (*ConnectionTracker, error) {
if client == nil || namespace == "" || podName == "" || uid == "" {
return nil, fmt.Errorf("client and admitted Pod namespace/name/UID are required")
}
return &ConnectionTracker{client: client, namespace: namespace, podName: podName, uid: uid}, nil
}
func (t *ConnectionTracker) OnConnectionOpen() {
t.connectionsMu.Lock()
defer t.connectionsMu.Unlock()
t.activeConnections++
}
func (t *ConnectionTracker) OnConnectionClose() {
t.connectionsMu.Lock()
defer t.connectionsMu.Unlock()
if t.activeConnections > 0 {
t.activeConnections--
}
}
// Illustrative coarse policy, not a benchmark or an availability guarantee.
func CostForConnections(count int64) int32 {
switch {
case count <= 0:
return 0
case count < 10:
return 100
case count < 100:
return 500
default:
return 1000
}
}
// Call at an application-controlled transition or before a controlled scale-down,
// not for every request. Assumes a single owner of this Pod's cost annotation.
func (t *ConnectionTracker) UpdateDeletionCost(parent context.Context) (bool, error) {
t.updateMu.Lock()
defer t.updateMu.Unlock()
if err := parent.Err(); err != nil {
return false, err
}
t.connectionsMu.Lock()
cost := CostForConnections(t.activeConnections)
t.connectionsMu.Unlock()
if t.lastCostSet && t.lastCost == cost {
return false, nil
}
// The Deployment template must already contain an annotations object.
// JSON Pointer escapes the slash in the annotation key as ~1.
patch, err := json.Marshal([]map[string]any{
{"op": "test", "path": "/metadata/uid", "value": string(t.uid)},
{"op": "add", "path": "/metadata/annotations/controller.kubernetes.io~1pod-deletion-cost", "value": strconv.FormatInt(int64(cost), 10)},
})
if err != nil {
return false, err
}
ctx, cancel := context.WithTimeout(parent, 3*time.Second)
defer cancel()
_, err = t.client.CoreV1().Pods(t.namespace).Patch(ctx, t.podName, types.JSONPatchType, patch, metav1.PatchOptions{})
if err != nil {
return false, err
}
t.lastCost, t.lastCostSet = cost, true
return true, nil
}
```
#### 3. 데이터 지역성이 있는 Pod 보호
유용한 캐시를 가진 복제본에 지역성 힌트를 줄 수 있습니다. 아래는 모두50으로 시작하므로 신뢰할 수 있는 컨트롤러가 개별 Pod를 바꾸기 전까지 비용 차이가 없습니다. 비용이 데이터를 마운트·보존·복원하지 않으며 스토리지 제약·앱 복구는 별도입니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: data-processor
spec:
replicas: 5
selector:
matchLabels:
app: data-processor
template:
metadata:
labels:
app: data-processor
annotations:
# 데이터 지역성이 높은 Pod에 높은 비용 설정
controller.kubernetes.io/pod-deletion-cost: "50"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- data-processor
topologyKey: kubernetes.io/hostname
containers:
- name: processor
image: registry.example.com/training/data-processor:validated
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
```
#### 4. 새로 시작된 Pod 우선 삭제
초기 음수 비용은 다른 컨트롤러 조건이 같을 때 신규 복제본의 삭제를 선호하게 할 수 있습니다. **최초 배포 전** 템플릿에서 선택하고 실제 준비 상태 전환 뒤 개별 Pod를 갱신합니다. 고정된 postStart sleep은 캐시 준비 증거가 아니며 기존 Deployment 템플릿을 바꾸면 롤아웃이 발생합니다.
```yaml
# Deployment Pod-template fragment, chosen before initial deployment.
spec:
template:
metadata:
annotations:
controller.kubernetes.io/pod-deletion-cost: "-50"
```
### Horizontal Pod Autoscaler와의 통합
HPA는 원하는 복제본 수를 조정하고 Deployment·ReplicaSet 컨트롤러가 삭제할 Pod를 선택합니다. 비용은 그 선택의 힌트이며 HPA 신호나 보호 보장이 아닙니다. 아래 HPA는 앞의 `cache-app` Deployment를 대상으로 하며 정상 CPU 메트릭과 CPU requests가 필요합니다. `selectPolicy: Min`은 더 제한적인 축소 정책을 선택합니다.
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: cache-app
namespace: deletion-cost-lab
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: cache-app
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
- type: Pods
value: 2
periodSeconds: 60
selectPolicy: Min
```
### 동적 Pod Deletion Cost 업데이트 패턴
대안 정책으로 최신 Pod별 요청·캐시·지연 시간 값을100점 구간의 힌트로 묶습니다. 가중치는 설명용이며 실측 성능 결과가 아닙니다. 가짜 수집기나 백그라운드 폴링 루프는 없습니다. Pod UID와 시간대가 있는 `observed_at`을 포함한 실제 샘플을 전달하세요. 누락·오류·오래된 샘플·다른 Pod의 데이터는 예외를 발생시키고 기존 어노테이션을 유지합니다.
미리 설정한 Kubernetes Python `ApiClient`를 전달합니다. 호출 시그니처는 클라이언트35.0.0으로 확인했습니다. JSON Patch 형식과 UID 검사, 제한된 연결·읽기 타임아웃을 명시합니다. 같은 Pod에 두 예제를 함께 실행하지 말고 어노테이션의 작성자·정책 하나를 선택하세요.
```python
from datetime import datetime, timezone
import math
import threading
ANNOTATION_PATH = (
"/metadata/annotations/controller.kubernetes.io~1pod-deletion-cost"
)
def calculate_cost(metrics, now):
"""Illustrative coarse hint from a real, fresh per-Pod sample."""
observed = datetime.fromisoformat(metrics["observed_at"].replace("Z", "+00:00"))
if observed.tzinfo is None or now.tzinfo is None:
raise ValueError("timestamps must include a timezone")
age = (now - observed).total_seconds()
if age < -5 or age > 60:
raise ValueError("metrics timestamp is in the future or stale")
active = metrics["active_requests"]
if isinstance(active, bool) or not isinstance(active, int) or active < 0:
raise ValueError("active_requests must be a non-negative integer")
hit_rate = metrics["cache_hit_rate"]
latency = metrics["avg_response_time_ms"]
for value in (hit_rate, latency):
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
raise ValueError("metrics must be finite numbers")
if not 0 <= hit_rate <= 1 or latency < 0:
raise ValueError("invalid hit rate or latency")
raw_cost = active * 5 + int(hit_rate * 100)
raw_cost += 50 if latency < 100 else 20 if latency < 500 else 0
# Coarse buckets reduce annotation churn. Weights are an example policy.
return min(1000, (raw_cost // 100) * 100)
class DeletionCostManager:
"""Uses a configured Kubernetes Python ApiClient; starts no background loop."""
def __init__(self, api_client, namespace, pod_name, pod_uid):
if api_client is None or not all((namespace, pod_name, pod_uid)):
raise ValueError("API client and admitted Pod namespace/name/UID required")
self.api_client = api_client
self.namespace = namespace
self.pod_name = pod_name
self.pod_uid = pod_uid
self._last_cost = None
self._lock = threading.Lock()
def update_from_metrics(self, metrics, now=None):
# A single writer should own this annotation. The Pod template must
# already create the annotations object with an initial deletion cost.
if metrics["pod_uid"] != self.pod_uid:
raise ValueError("metrics belong to a different Pod UID")
now = now or datetime.now(timezone.utc)
cost = calculate_cost(metrics, now)
with self._lock:
if cost == self._last_cost:
return False
patch = [
{"op": "test", "path": "/metadata/uid", "value": self.pod_uid},
{"op": "add", "path": ANNOTATION_PATH, "value": str(cost)},
]
# Explicit JSON Patch media type; preserve unrelated Pod fields.
self.api_client.call_api(
"/api/v1/namespaces/{namespace}/pods/{name}",
"PATCH",
path_params={"namespace": self.namespace, "name": self.pod_name},
header_params={
"Accept": "application/json",
"Content-Type": "application/json-patch+json",
},
body=patch,
response_type="V1Pod",
auth_settings=["BearerToken"],
_return_http_data_only=True,
_request_timeout=(3, 5),
)
self._last_cost = cost
return True
```
### 모니터링 및 디버깅
다음은 어노테이션을 조회하고 복제본 수를 바꾸지 않은 채 축소 요청을 검증합니다. 서버 dry run은 **ReplicaSet의 삭제 대상 선택을 시뮬레이션하지 않습니다.** 실제 축소 실험은 제어 가능한 격리 워크로드에서 진행하고 복제본 수를 다시 덮어쓸 HPA를 고려하며 전후 Pod UID·소유 ReplicaSet을 비교하세요. kubelet의 `Killing` 이벤트 하나로 비용 순서를 입증할 수 없습니다.
```bash
kubectl -n deletion-cost-lab get pods -l app=cache-app \
-o custom-columns='NAME:.metadata.name,UID:.metadata.uid,COST:.metadata.annotations.controller\.kubernetes\.io/pod-deletion-cost'
kubectl -n deletion-cost-lab get pods -l app=cache-app -o json | \
jq -r '.items[] | [.metadata.name, .metadata.uid, (.metadata.annotations["controller.kubernetes.io/pod-deletion-cost"] // "0")] | @tsv'
# Server-side dry run changes no replicas and does not predict victim selection.
kubectl -n deletion-cost-lab scale deployment/cache-app --replicas=3 --dry-run=server
kubectl -n deletion-cost-lab get replicasets,pods -l app=cache-app
```
### Prometheus 메트릭 수집
`kube_pod_annotations`는 애플리케이션이 아닌 **kube-state-metrics**가 제공합니다. 기존 exporter에 필요한 어노테이션만 허용하고 다른 플래그·허용 목록은 보존합니다. 기존 Prometheus 수집 대상이 정상이어야 하며 아래는 새 Deployment가 아닌 인자 조각입니다.
```yaml
# Fragment to merge into the existing kube-state-metrics container arguments.
# Preserve its other arguments and allowlisted keys.
args:
- --metric-annotations-allowlist=pods=[controller.kubernetes.io/pod-deletion-cost]
```
메트릭 값은1인 gauge이고 비용은 `annotation_controller_kubernetes_io_pod_deletion_cost` **레이블**입니다. 어노테이션을 relabel한다고 메트릭 값이 숫자 비용으로 바뀌지 않습니다. 시계열 누락은 수집 누락일 수 있으므로 비용0으로 단정하지 마세요.
### Grafana 대시보드
가져오기용 대시보드 JSON 객체이며 HTTP API의 `{"dashboard": ...}` 요청 wrapper가 아닙니다. `PROMETHEUS_UID`를 실제 데이터 소스 UID로 바꾸세요. 패널은 음수를 포함한 명시적 어노테이션 레이블별 Pod 수를 세며 gauge 값1을 비용으로 그리지 않습니다. 결과를 해석하기 전에 kube-state-metrics 허용 목록을 확인하세요.
```json
{
"id": null,
"uid": "pod-deletion-cost-hints",
"title": "Pod Deletion Cost Hints",
"schemaVersion": 39,
"version": 1,
"refresh": "30s",
"time": {
"from": "now-1h",
"to": "now"
},
"panels": [
{
"id": 1,
"title": "Pods by explicit deletion cost",
"type": "piechart",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "PROMETHEUS_UID"
},
"targets": [
{
"refId": "A",
"expr": "count by (annotation_controller_kubernetes_io_pod_deletion_cost) (kube_pod_annotations{namespace=\"deletion-cost-lab\",annotation_controller_kubernetes_io_pod_deletion_cost=~\"-?[0-9]+\"})",
"legendFormat": "{{annotation_controller_kubernetes_io_pod_deletion_cost}}",
"instant": true
}
],
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"options": {}
},
{
"id": 2,
"title": "Pods with an explicit cost",
"type": "stat",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "PROMETHEUS_UID"
},
"targets": [
{
"refId": "A",
"expr": "count(kube_pod_annotations{namespace=\"deletion-cost-lab\",annotation_controller_kubernetes_io_pod_deletion_cost!=\"\"})",
"legendFormat": "",
"instant": true
}
],
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"options": {}
}
]
}
```
### 모범 사례
1. **일관된 비용 범위 사용**: 팀 내에서 일관된 비용 범위를 정의하여 사용합니다.
* `-100 ~ -1`: 우선 삭제 (새로운 Pod, 워밍업 중인 Pod)
* `0`: 기본값 (일반 Pod)
* `1 ~ 100`: 보통 중요도 (활성 연결이 있는 Pod)
* `100 ~ 1000`: 높은 중요도 (캐시가 워밍업된 Pod, 많은 연결이 있는 Pod)
2. **갱신 제한**: 큰 상태 전환이나 제어하는 축소 전에 갱신하고 요청·메트릭 샘플마다 쓰지 않습니다.
3. **상한선 설정**: deletion cost에 상한선을 설정하여 너무 큰 값으로 인한 문제를 방지합니다.
4. **모니터링**: deletion cost의 분포를 모니터링하여 예상대로 작동하는지 확인합니다.
5. **테스트**: 프로덕션에 적용하기 전에 스테이징 환경에서 스케일 다운 동작을 테스트합니다.
6. **문서화**: 각 비용 범위가 의미하는 바를 문서화합니다.
### 제한사항
* **PDB 범위**: 일반적인 ReplicaSet·Deployment 축소는 Pod를 직접 삭제하며 PDB가 차단하지 않습니다. PDB는 eviction API 요청을 제어하며 어느 방식도 장애 중 생존을 보장하지 않습니다.
* **버전·기능**: 1.21 alpha, 1.22부터 beta·기본 활성화입니다. 참조한1.35.8 기준에서도 활성화됩니다. 지원이 끝난 과거 마이너 버전을 배포하라는 의미는 아닙니다.
* **워크로드·소유권**: 한 ReplicaSet 내부의 선호도입니다. 독립 Pod·StatefulSet ordinal·전체 워크로드 삭제는 다른 경로입니다.
* **비동기 동작**: 동시 갱신·준비 상태·다른 선택 기준이 기대한 순서에 영향을 줄 수 있습니다. 새 Pod에는 자체 어노테이션이 필요하며 이 힌트는 영속 애플리케이션 상태가 아닙니다.
## 커스텀 스케줄러 모니터링 및 디버깅
커스텀 스케줄러를 구현한 후에는 모니터링 및 디버깅이 중요합니다. 이 섹션에서는 커스텀 스케줄러를 모니터링하고 디버깅하는 방법을 알아보겠습니다.
### 모니터링 아키텍처
관측 연동을 선택하는 개념도입니다. 수집 대상·인증·remote write·로그 경로는 별도 구성해야 하며 아래 예제는 스케줄러 자체 HTTPS 엔드포인트를 사용합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-03-custom-scheduler-part3-14.html)
### 주요 모니터링 메트릭
다음 다이어그램은 커스텀 스케줄러의 주요 모니터링 메트릭과 그 관계를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-scheduling-03-custom-scheduler-part3-15.html)
### 로깅
커스텀 스케줄러의 로그를 확인하여 스케줄링 결정을 이해할 수 있습니다:
```bash
kubectl logs -n scheduler-lab -l app=custom-scheduler --prefix --tail=100
```
### 이벤트 확인
포드 스케줄링과 관련된 이벤트를 확인할 수 있습니다:
```bash
kubectl -n scheduler-lab get events --field-selector involvedObject.name=
```
### 메트릭 수집
아래는10259에서 HTTPS 메트릭을 직접 제공하는 **보조 스케줄러**를 모니터링합니다. 메트릭 사이드카가 필수는 아닙니다. 그림의 AMP·CloudWatch·로그 수집기·알림 경로는 별도 설정이 필요합니다.
전제 조건은 탐색 권한이 있는 기존 Prometheus Operator 스택과 `custom-scheduler.scheduler-lab.svc`용 서버 인증서·개인 키가 들어 있는 `scheduler-lab/custom-scheduler-serving-tls` Secret입니다. 공개 CA는 `monitoring/custom-scheduler-ca`의 `ca.crt`에 둡니다. `monitoring/scheduler-scrape-token` Secret에는 Prometheus ServiceAccount의 유효하고 **교체되는 단기 토큰**이 필요합니다. 발급·교체는 이 예제에 구현하지 않았습니다.
PKI 준비 후 아래 **strategic merge patch**를 Part1 전체 Deployment에 적용합니다. 기존 이미지·설정·ServiceAccount를 유지하고 서버 키를 마운트합니다. 통제된 롤아웃 절차를 사용하세요. 이번 감사에서는 TLS 롤아웃을 실행하지 않았습니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: custom-scheduler
namespace: scheduler-lab
spec:
template:
spec:
containers:
- name: custom-scheduler
args:
- --config=/etc/scheduler/config.yaml
- --tls-cert-file=/etc/scheduler-serving/tls.crt
- --tls-private-key-file=/etc/scheduler-serving/tls.key
volumeMounts:
- name: serving-tls
mountPath: /etc/scheduler-serving
readOnly: true
volumes:
- name: serving-tls
secret:
secretName: custom-scheduler-serving-tls
defaultMode: 288
```
Service가 명명된 HTTPS 포트를 노출합니다. 예제의 `monitoring/prometheus` ServiceAccount는 실제 수집기 자격 증명으로 바꾸세요. 추가 ClusterRole은 non-resource `/metrics` GET만 허용하며 탐색 권한을 제공하지 않습니다. Prometheus 리소스가 ServiceMonitor의 레이블·네임스페이스를 선택하도록 설정해야 합니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: custom-scheduler
namespace: scheduler-lab
labels:
app: custom-scheduler
spec:
selector:
app: custom-scheduler
ports:
- name: https
port: 10259
targetPort: https
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: custom-scheduler-metrics
rules:
- nonResourceURLs:
- /metrics
verbs:
- get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: custom-scheduler-metrics
subjects:
- kind: ServiceAccount
name: prometheus
namespace: monitoring
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: custom-scheduler-metrics
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: custom-scheduler
namespace: monitoring
labels:
app: custom-scheduler
spec:
namespaceSelector:
matchNames:
- scheduler-lab
selector:
matchLabels:
app: custom-scheduler
endpoints:
- port: https
path: /metrics
scheme: https
interval: 15s
tlsConfig:
serverName: custom-scheduler.scheduler-lab.svc
ca:
configMap:
name: custom-scheduler-ca
key: ca.crt
authorization:
type: Bearer
credentials:
name: scheduler-scrape-token
key: token
```
### 대시보드 구성
안정 메트릭 `scheduler_scheduling_attempt_duration_seconds`는 `result`·`profile` 레이블이 있으며 histogram으로 시도 지연 시간을 추정합니다. `scheduler_schedule_attempts_total`은 counter이므로 처리량에는 rate를 사용합니다. `_count` 원시 값은 지연 시간이 아닙니다. `kubectl get --raw /metrics`는 보조 스케줄러가 아닌 API 서버의 메트릭을 반환합니다.
`PROMETHEUS_UID`를 바꾸고 내장 JSON을 가져오거나 Grafana 대시보드 provider·sidecar가 이 ConfigMap을 읽도록 구성합니다. ConfigMap 생성만으로 Grafana에 로드되지 않습니다. `grafana_dashboard: "1"`은 흔히 쓰는 provider 규칙이며 실제 설정과 일치해야 합니다. 실제 가져오기·쿼리 실행은 검증하지 않았습니다.
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: custom-scheduler-dashboard
namespace: monitoring
labels:
grafana_dashboard: '1'
data:
custom-scheduler-dashboard.json: |
{
"id": null,
"uid": "custom-scheduler",
"title": "Custom Scheduler",
"schemaVersion": 39,
"version": 1,
"refresh": "30s",
"time": {
"from": "now-1h",
"to": "now"
},
"panels": [
{
"id": 1,
"title": "Successful scheduling attempt p95",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "PROMETHEUS_UID"
},
"targets": [
{
"refId": "A",
"expr": "histogram_quantile(0.95, sum by (le, profile) (rate(scheduler_scheduling_attempt_duration_seconds_bucket{profile=\"custom-scheduler\",result=\"scheduled\"}[5m])))",
"legendFormat": "{{profile}}",
"instant": false
}
],
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"options": {}
},
{
"id": 2,
"title": "Scheduling attempts per second",
"type": "timeseries",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"datasource": {
"type": "prometheus",
"uid": "PROMETHEUS_UID"
},
"targets": [
{
"refId": "A",
"expr": "sum by (result) (rate(scheduler_schedule_attempts_total{profile=\"custom-scheduler\"}[5m]))",
"legendFormat": "{{result}}",
"instant": false
}
],
"fieldConfig": {
"defaults": {
"unit": "ops"
},
"overrides": []
},
"options": {}
}
]
}
```
## 결론
커스텀 스케줄러는 특정 요구 사항에 맞게 Kubernetes 스케줄링 동작을 조정할 수 있는 강력한 방법입니다. EKS에서는 다중 스케줄러 접근 방식, 스케줄러 확장 접근 방식, 스케줄러 프레임워크 플러그인 접근 방식 등 다양한 방법으로 커스텀 스케줄러를 구현할 수 있습니다.
GPU 워크로드 최적화, 네트워크 지역성 최적화 등 다양한 사례에서 커스텀 스케줄러를 활용할 수 있습니다. 커스텀 스케줄러를 구현할 때는 모니터링 및 디버깅을 위한 도구를 함께 구성하는 것이 중요합니다.
## 참고 자료와 검증 범위
* [ReplicaSet 삭제 비용과 제한](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/#pod-deletion-cost)
* [Pod disruption budget](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/)
* [고정 버전 ReplicaSet 삭제 경로](https://github.com/kubernetes/kubernetes/blob/v1.35.8/pkg/controller/replicaset/replica_set.go)
* [고정 버전 삭제 순서](https://github.com/kubernetes/kubernetes/blob/v1.35.8/pkg/controller/controller_utils.go)
* [스케줄러 메트릭](https://github.com/kubernetes/kubernetes/blob/v1.35.8/pkg/scheduler/metrics/metrics.go)
* [kube-state-metrics Pod 메트릭](https://github.com/kubernetes/kube-state-metrics/blob/main/docs/metrics/workload/pod-metrics.md)
* [Kubernetes Python 클라이언트 API](https://github.com/kubernetes-client/python/blob/v35.0.0/kubernetes/client/api_client.py)
로컬 테스트는 합성 Pod·가짜 API 클라이언트·산술 fixture를 사용합니다. 벤치마크 재실행, 실제 GPU 사용률·Pod 삭제·앱 워밍업·인증서/토큰 교체·운영 가용성을 검증한 결과가 아닙니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/scheduling/02-custom-scheduler-part3-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/autoscaling/01-keda
----------------------------------------
# KEDA (Kubernetes Event-driven Autoscaling)
> **예제 버전**: KEDA/Helm 차트 2.20.2; Kubernetes 테스트 호환 범위는 아래 참고.
> **마지막 업데이트**: 2026년 9월 11일
## 목차
- [소개](#소개)
- [아키텍처](#아키텍처)
- [설치 및 구성](#설치-및-구성)
- [스케일러](#스케일러)
- [커스텀 메트릭 스케일링](#커스텀-메트릭-스케일링)
- [Twitter 메트릭 스케일링](#twitter-메트릭-스케일링)
- [Google Calendar 스케일링](#google-calendar-스케일링)
- [Istio 메트릭 스케일링](#istio-메트릭-스케일링)
- [Cron 기반 스케일링](#cron-기반-스케일링)
- [Amazon EKS와의 통합](#amazon-eks와의-통합)
- [모범 사례](#모범-사례)
- [문제 해결](#문제-해결)
- [결론](#결론)
## 소개
KEDA(Kubernetes Event-driven Autoscaling)는 Kubernetes 애플리케이션을 이벤트 기반으로 자동 확장할 수 있게 해주는 오픈 소스 프로젝트입니다. KEDA는 Kubernetes의 기본 Horizontal Pod Autoscaler(HPA)를 확장하여 CPU 및 메모리 사용량 외에도 다양한 이벤트 소스와 메트릭을 기반으로 워크로드를 확장할 수 있게 해줍니다.
### KEDA의 주요 이점
1. **이벤트 기반 스케일링**: 다양한 이벤트 소스(메시지 큐, 데이터베이스, 스트림 등)에 기반한 스케일링
2. **제로 스케일링**: 지원하는 이벤트 트리거로 유휴 워크로드를 활성화하며 최소 복제본·활성화 임계값·쿨다운을 구성합니다.
3. **다양한 스케일러 지원**: 50개 이상의 내장 스케일러와 커스텀 스케일러 지원
4. **Kubernetes 네이티브**: 기존 Kubernetes HPA와 통합
5. **클라우드 중립적**: 필요한 API·네트워크·인증 구성을 갖춘 호환 Kubernetes 배포판에서 실행합니다.
6. **배포 모델**: 기본 설치는 오퍼레이터·메트릭 API 서버·어드미션 웹훅으로 구성됩니다.
### 기존 스케일링 방식과의 비교
| 기능 | KEDA | Kubernetes HPA | Cloud Provider Autoscaler |
|------|------|----------------|---------------------------|
| 메트릭 소스 | 내장 이벤트 스케일러·외부 스케일러 | 적절한 어댑터를 통한 리소스·커스텀·외부 메트릭 | 제품별 상이 |
| 제로 스케일링 | 지원 트리거와 설정 필요 | 버전·기능에 따라 다름; Kubernetes 1.37 beta는 object/external 메트릭으로 지원 | 제품별 상이 |
| 이벤트 기반 | 이벤트 소스 통합과 활성화 | 커스텀·외부 메트릭 어댑터로 가능 | 제품별 상이 |
| 클라우드 중립적 | ✅ | ✅ | ❌ |
| 배포 복잡성 | 오퍼레이터·메트릭 서버·웹훅·인증 구성 | 내장 컨트롤러, 필요 시 메트릭 어댑터 추가 | 제품별 상이 |
| 커스텀 메트릭 | 스케일러 통합 또는 HTTP/gRPC 생산자 | 적절한 메트릭 어댑터 필요 | 제품별 상이 |
## 아키텍처
KEDA는 Kubernetes 오퍼레이터 패턴을 기반으로 하며, 외부 메트릭 소스를 모니터링하고 Kubernetes HPA를 자동으로 관리합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-autoscaling-01-keda-0.html)
### 주요 구성 요소
1. **KEDA 오퍼레이터**: ScaledObject와 HPA를 조정하고 제로 활성화·비활성화를 처리하며 ScaledJob의 Job을 생성합니다.
2. **KEDA 메트릭 서버**: 오퍼레이터의 메트릭 서비스에서 스케일러 결과를 받아 Kubernetes 집계 API로 외부 메트릭을 제공합니다.
3. **ScaledObject**: 배포(Deployment), 상태 저장 세트(StatefulSet) 등의 스케일링 구성을 정의
4. **ScaledJob**: Kubernetes Job의 스케일링 구성을 정의
5. **트리거/스케일러**: 이벤트 소스의 메트릭과 활성화를 평가합니다. 어드미션 웹훅은 지원하는 리소스 구성을 검증합니다.
### 작동 방식
1. ScaledObject가 같은 네임스페이스의 호환 스케일 대상을 참조하면 KEDA가 HPA 하나를 관리합니다. 대상별 스케일링 소유자는 하나여야 합니다.
2. 오퍼레이터는 워크로드가 0개일 때도 `pollingInterval`에 따라 트리거 활성화를 조회합니다.
3. 복제본이 0보다 크면 HPA가 메트릭 API 서버와 오퍼레이터를 통해 외부 메트릭을 조회합니다. HPA 동기화와 메트릭 캐시 설정도 조회 빈도에 영향을 줍니다.
4. HPA는 0보다 큰 복제본 수를 조절하고 KEDA는 활성화와 설정된 제로 축소 쿨다운을 담당합니다. `cooldownPeriod`는 N→1 축소의 HPA 안정화 설정을 대체하지 않습니다.
5. ScaledJob은 별도 경로로 이벤트와 스케일링 전략에 따라 batch Job을 생성하며 해당 Job의 HPA를 만들지 않습니다.
## 설치 및 구성
예제는 대안이며 한꺼번에 적용할 매니페스트 묶음이 아닙니다. 참조하는 워크로드·Service·Secret·이미지를 준비하고 계정·큐·URL·이미지 자리표시자를 교체하세요. 같은 대상에 여러 ScaledObject/HPA를 붙이지 마세요. 자동 확장이 복제본을 관리하면 GitOps/apply의 `spec.replicas` 소유권도 조정해야 합니다. 이 레시피는 운영 환경에 배포하거나 실측하지 않았습니다.
Kubernetes 1.37은 object/external 메트릭의 HPA 제로 스케일링을 beta로 도입했고 `HPAScaleToZero`가 기본 활성화됩니다. CPU·메모리만으로는 0에서 활성화할 수 없습니다. 이것이 KEDA 2.20의 오퍼레이터/HPA 역할을 바꾸거나 Kubernetes 1.37 호환성을 입증하지는 않습니다.
### 사전 요구 사항
- 공급자와 선택한 KEDA 릴리스가 지원하는 Kubernetes를 선택하세요. KEDA 2.20 설치 문서의 최소 버전은 1.30이지만 공개된 **테스트 호환 범위는 1.33–1.35**입니다. 이 표가 1.36·1.37 호환성을 입증하지는 않으므로 별도 검증이 필요합니다.
- kubectl 설정
- Helm (선택 사항)
### 설치 방법
#### 1. Helm을 사용한 설치
```bash
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --version 2.20.2 --namespace keda --create-namespace
```
#### 2. YAML 매니페스트를 사용한 설치
```bash
kubectl apply --server-side -f https://github.com/kedacore/keda/releases/download/v2.20.2/keda-2.20.2.yaml
```
#### 3. 설치 확인
```bash
kubectl get deployments,pods -n keda
kubectl wait --for=condition=Available deployment --all -n keda --timeout=180s
kubectl get apiservice v1beta1.external.metrics.k8s.io
```
출력 형식 예시(차트 설정에 따라 이름·개수가 달라지며 실제 실행 결과가 아닙니다):
```
NAME READY STATUS RESTARTS AGE
keda-operator-- 1/1 Running 0 1m
keda-operator-metrics-apiserver-- 1/1 Running 0 1m
keda-admission-webhooks-- 1/1 Running 0 1m
```
### 기본 구성
뒤의 IRSA 값은 같은 고정 버전 Helm 값에 병합한 뒤 업그레이드에 적용해야 합니다. 결과 ServiceAccount 주석을 확인하고 인증을 변경하면 정상 롤아웃 절차로 오퍼레이터 Pod를 재생성하세요.
다음 값은 Helm 차트 2.20.2에 맞습니다. 오퍼레이터 2개는 리더 선출 대기 복제본을 제공하며 동시에 두 조정자가 활성화되는 것은 아닙니다. 메트릭 서버 이중화도 API 집계 라우팅에 영향을 받으며 전체 경로의 완전한 고가용성을 보장하지 않습니다. 리소스 값은 출발점이며 실측 사이징 결과가 아닙니다.
#### Helm 값 파일을 사용한 사용자 정의 구성
```yaml
operator:
replicaCount: 2
metricsServer:
replicaCount: 1
resources:
operator:
limits:
cpu: '1'
memory: 1000Mi
requests:
cpu: 100m
memory: 100Mi
metricServer:
limits:
cpu: '1'
memory: 1000Mi
requests:
cpu: 100m
memory: 100Mi
webhooks:
limits:
cpu: '1'
memory: 1000Mi
requests:
cpu: 100m
memory: 100Mi
logging:
operator:
level: info
metricServer:
level: 0
```
```bash
helm upgrade --install keda kedacore/keda --version 2.20.2 --namespace keda --create-namespace -f values.yaml
```
## 스케일러
KEDA는 다양한 이벤트 소스에 대한 스케일러를 제공합니다. 각 스케일러는 특정 이벤트 소스에서 메트릭을 수집하고 이를 기반으로 워크로드를 스케일링합니다.
### 주요 스케일러
KEDA는 50개 이상의 스케일러를 지원하며, 주요 스케일러는 다음과 같습니다:
1. **메시지 큐**:
- Apache Kafka
- RabbitMQ
- AWS SQS
- Azure Service Bus
- Google Cloud Pub/Sub
2. **데이터베이스**:
- MySQL
- PostgreSQL
- MongoDB
- Redis
3. **스트리밍 플랫폼**:
- Apache Kafka
- AWS Kinesis
- Azure Event Hubs
4. **클라우드 서비스**:
- AWS CloudWatch
- Azure Monitor
- Google Cloud Monitoring
5. **기타**:
- Prometheus
- Influxdb
- Cron
- CPU/Memory
### 기본 ScaledObject 예시
대상 네임스페이스의 `rabbitmq-credentials` Secret에 완전하고 권한이 있는 AMQP/AMQPS 연결 URI를 `host` 키로 준비하세요. 참조한 `rabbitmq-consumer` Deployment는 해당 큐를 소비하도록 미리 설정되어 있어야 합니다. 격리되지 않은 망에서는 신뢰를 검증하는 TLS를 사용하세요. 예제가 브로커나 자격 증명을 생성하지는 않습니다.
```yaml
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: rabbitmq-auth
namespace: default
spec:
secretTargetRef:
- parameter: host
name: rabbitmq-credentials
key: host
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: rabbitmq-scaledobject
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: rabbitmq-consumer
pollingInterval: 15
cooldownPeriod: 30
minReplicaCount: 0
maxReplicaCount: 30
triggers:
- type: rabbitmq
metadata:
protocol: amqp
queueName: hello
mode: QueueLength
value: '5'
authenticationRef:
name: rabbitmq-auth
```
### 기본 ScaledJob 예시
위의 `rabbitmq-auth`·`rabbitmq-credentials`를 재사용합니다. 한정된 작업을 소비·승인하고 종료하는 실제 worker 이미지를 준비해야 Job이 완료됩니다. 재시도는 처리를 반복할 수 있으므로 애플리케이션 멱등성과 적절한 승인·가시성 시간 제한을 설계하세요. `jobTargetRef`는 JobSpec이며 Job이나 PodTemplate을 한 번 더 중첩하지 않습니다.
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
name: rabbitmq-scaledjob
namespace: default
spec:
jobTargetRef:
template:
spec:
containers:
- name: rabbitmq-worker
image: rabbitmq-worker:latest
imagePullPolicy: Always
env:
- name: RABBITMQ_HOST
valueFrom:
secretKeyRef:
name: rabbitmq-credentials
key: host
restartPolicy: Never
backoffLimit: 4
pollingInterval: 15
maxReplicaCount: 30
successfulJobsHistoryLimit: 5
failedJobsHistoryLimit: 5
triggers:
- type: rabbitmq
metadata:
protocol: amqp
queueName: hello
mode: QueueLength
value: '5'
authenticationRef:
name: rabbitmq-auth
```
## 커스텀 메트릭 스케일링
KEDA는 다양한 내장 스케일러 외에도 커스텀 메트릭을 기반으로 스케일링할 수 있는 유연성을 제공합니다. 이를 통해 비즈니스 요구사항에 맞는 고유한 스케일링 로직을 구현할 수 있습니다.
### 외부 메트릭 API 사용
카운터 예제는 `rate(...[2m])`를 사용하므로 목표 단위는 누적 건수가 아닌 복제본당 초당 이벤트 수입니다. Prometheus 질의는 숫자 결과 하나를 반환해야 합니다. `ignoreNullValues: false`는 누락된 시계열을 오류로 드러냅니다. 빈 결과가 0인지 수집 장애인지 의도적으로 정하세요.
Prometheus와 같은 외부 메트릭 소스를 사용하여 커스텀 메트릭 기반 스케일링을 구현할 수 있습니다:
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: custom-metrics-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicaCount: 1
maxReplicaCount: 10
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-server.monitoring.svc.cluster.local
threshold: '100'
query: sum(rate(custom_metric_total{namespace="default",pod=~"my-app-.*"}[2m]))
ignoreNullValues: 'false'
```
### HTTP 스케일러 사용
숫자 엔드포인트 데이터를 조회하는 `metrics-api` 스케일러입니다. 활성화를 위해 요청을 가로채고 버퍼링하는 별도 KEDA HTTP add-on과 다릅니다. 특히 대상이 0개일 때도 메트릭 엔드포인트는 대상과 독립적으로 사용 가능해야 합니다.
HTTP 엔드포인트에서 메트릭을 가져와 스케일링할 수 있습니다:
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: http-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicaCount: 1
maxReplicaCount: 10
triggers:
- type: metrics-api
metadata:
targetValue: '100'
url: https://metrics.example.com/metrics
valueLocation: value
```
### 커스텀 스케일러 개발
다음 Go 코드는 내장 `metrics-api` 스케일러용 **HTTP JSON 메트릭 생산자**입니다. Kubernetes external.metrics.k8s.io나 KEDA 외부 스케일러 프로토콜 구현이 아닙니다. KEDA의 `external`·`external-push` 서비스는 공식 gRPC 메서드 `IsActive`·`GetMetricSpec`·`GetMetrics`와 푸시 활성화용 `StreamIsActive`를 구현해야 합니다.
1. 메트릭 서버 구현:
완전한 Go 서버는 숫자 `value`와 RFC3339 `observed_at`을 가진 JSON 스냅샷 `METRICS_FILE`을 읽습니다. 별도 업무 메트릭 생산자가 파일을 원자적으로 교체해야 합니다. 누락·형식 오류·음수·2분 이상 오래된 값은 HTTP503을 반환합니다. 다음 ScaledObject 사용 전에 서버를 빌드·배포하고 Service를 구성하세요. Kubernetes 집계 API 서버 구현은 아닙니다.
```go
package main
import (
"encoding/json"
"errors"
"io"
"log"
"net/http"
"os"
"time"
)
type snapshot struct {
Value *float64 `json:"value"`
ObservedAt time.Time `json:"observed_at"`
}
func metricsHandler(path string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", "GET")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
f, err := os.Open(path)
if err != nil {
http.Error(w, "metric unavailable", http.StatusServiceUnavailable)
return
}
defer f.Close()
var v snapshot
decoder := json.NewDecoder(io.LimitReader(f, 1<<20))
if err = decoder.Decode(&v); err == nil {
var extra any
if err = decoder.Decode(&extra); !errors.Is(err, io.EOF) {
http.Error(w, "invalid snapshot", http.StatusServiceUnavailable)
return
}
} else {
http.Error(w, "invalid snapshot", http.StatusServiceUnavailable)
return
}
age := time.Since(v.ObservedAt)
if v.Value == nil || *v.Value < 0 || v.ObservedAt.IsZero() || age < -5*time.Second || age > 2*time.Minute {
http.Error(w, "stale or invalid metric", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(v)
}
}
func main() {
path := os.Getenv("METRICS_FILE")
if path == "" { log.Fatal("METRICS_FILE is required") }
mux := http.NewServeMux()
mux.HandleFunc("/metrics", metricsHandler(path))
server := &http.Server{
Addr: ":8080", Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Fatal(server.ListenAndServe())
}
```
2. KEDA와 통합:
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: custom-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicaCount: 1
maxReplicaCount: 10
triggers:
- type: metrics-api
metadata:
targetValue: '100'
url: http://custom-metrics-server:8080/metrics
valueLocation: value
```
## Twitter 메트릭 스케일링
X API(이전 Twitter)의 v2 recent-counts 엔드포인트를 사용합니다. 수집 시점 30초 전까지의 5분 구간 일치 건수이며 누적값이나 순간 게시 속도가 아닙니다. 계정 접근권·질의 의미·과금·호출 한도는 실제 사용 조건을 확인해야 합니다.
### 사전 요구 사항
- recent Post counts 접근권이 있는 X 개발자 앱과 앱 bearer token. 예제만으로 API 접근권이나 비용 조건이 보장되지 않습니다.
- 메트릭을 수집하고 노출하는 서비스
### 구현 단계
1. Twitter 메트릭 수집기 서비스 구현:
독립 예제를 `app.py`로 저장하고 Flask·requests·Gunicorn을 collector 이미지에 포함하세요. 배포는 Gunicorn worker 하나와 `app:create_app()`을 사용해 WSGI에서도 수집 스레드를 시작합니다. worker·복제본을 늘리면 API 폴링도 늘어납니다. 부분 결과·호출 제한·인증 오류는 실패 후 HTTP503으로 응답하며 잘린 검색 페이지 길이를 속도로 사용하지 않습니다. 실제 API 접근 조건에 맞춰 폴링을 조정하세요.
```python
import datetime as dt
import os
import threading
import time
import requests
from flask import Flask, jsonify
TOKEN = os.environ["X_BEARER_TOKEN"]
QUERY = os.environ.get("X_QUERY", "#kubernetes")
POLL_SECONDS = 60
MAX_AGE_SECONDS = 120
METRIC_NAME = "tweet_count"
def fetch_value():
# Five-minute window ending 30 seconds ago; not a lifetime count or live rate.
end = dt.datetime.now(dt.timezone.utc) - dt.timedelta(seconds=30)
start = end - dt.timedelta(minutes=5)
response = requests.get(
"https://api.x.com/2/tweets/counts/recent",
headers={"Authorization": f"Bearer {TOKEN}"},
params={"query": QUERY, "granularity": "minute",
"start_time": start.isoformat(), "end_time": end.isoformat()},
timeout=(3, 10),
)
response.raise_for_status()
body = response.json()
meta = body["meta"]
# Never silently scale from a partial result or an API error payload.
if body.get("errors") or meta.get("next_token"):
raise ValueError("incomplete counts response")
value = meta["total_tweet_count"]
if type(value) is not int or value < 0:
raise ValueError("invalid count")
return value
def create_app():
app = Flask(__name__)
lock = threading.Lock()
state = {"value": None, "updated": 0.0, "healthy": False}
def collect():
while True:
try:
value = fetch_value()
if type(value) is not int or value < 0:
raise ValueError("invalid metric")
with lock:
state.update(value=value, updated=time.monotonic(), healthy=True)
except Exception as exc:
with lock:
state["healthy"] = False
app.logger.warning("Metric refresh failed: %s", type(exc).__name__)
time.sleep(POLL_SECONDS)
@app.get("/metrics")
def get_metrics():
with lock:
current = state.copy()
if not current["healthy"] or time.monotonic() - current["updated"] > MAX_AGE_SECONDS:
return jsonify(error="metric unavailable or stale"), 503
response = jsonify({METRIC_NAME: current["value"]})
response.headers["Cache-Control"] = "no-store"
return response
threading.Thread(target=collect, daemon=True).start()
return app
if __name__ == "__main__":
# Local development only; use a WSGI server for the deployment example.
create_app().run(host="127.0.0.1", port=8080)
```
2. 메트릭 수집기 서비스 배포:
collector 이미지를 빌드하고 고정한 뒤 배포하세요. 토큰 값을 셸 인수에 넣지 않고 기존 파일로 Secret을 생성합니다.
```bash
kubectl create secret generic twitter-api-secrets --namespace default --from-file=bearer-token=./x-bearer-token
```
실행하면 Kubernetes Secret을 생성하는 명령이며 이번 감사에서는 실행하지 않았습니다. 원본 파일은 버전 관리에서 제외하세요.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: twitter-metrics-collector
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: twitter-metrics-collector
template:
metadata:
labels:
app: twitter-metrics-collector
spec:
containers:
- name: collector
image: twitter-metrics-collector:latest
ports:
- containerPort: 8080
env:
- name: X_BEARER_TOKEN
valueFrom:
secretKeyRef:
name: twitter-api-secrets
key: bearer-token
command:
- gunicorn
args:
- --bind
- 0.0.0.0:8080
- --workers
- '1'
- --threads
- '4'
- app:create_app()
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
readinessProbe:
httpGet:
path: /metrics
port: 8080
periodSeconds: 10
failureThreshold: 3
automountServiceAccountToken: false
---
apiVersion: v1
kind: Service
metadata:
name: twitter-metrics-collector
namespace: default
spec:
selector:
app: twitter-metrics-collector
ports:
- port: 80
targetPort: 8080
```
3. KEDA ScaledObject 구성:
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: twitter-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: twitter-processor
minReplicaCount: 1
maxReplicaCount: 20
pollingInterval: 15
cooldownPeriod: 30
triggers:
- type: metrics-api
metadata:
targetValue: "10"
url: "http://twitter-metrics-collector/metrics"
valueLocation: "tweet_count"
```
기본 AverageValue에서는 건수/목표 비율이 설정 범위 내 복제본 수를 제안하며 처리량을 직접 모델링하지는 않습니다. collector는 독립적으로 1개를 유지하고 사용 불가 시 503을 반환합니다. 상위 API 장애만으로 재시작하는 liveness probe는 두지 않았습니다. 운영 사용에는 인증·한도·수명주기·관측성을 별도로 검증해야 합니다.
## Google Calendar 스케일링
Google Calendar에서 **다음 1시간과 겹치는** 이벤트 인스턴스를 계산합니다. 이미 진행 중인 이벤트도 포함합니다. `timeMin`은 이벤트 종료 시각, `timeMax`는 시작 시각을 필터링합니다. 모든 페이지를 합산하며 부분·실패·오래된 수집값은 거짓 0 대신 메트릭 사용 불가로 응답합니다.
### 사전 요구 사항
- Calendar API를 활성화하고 특정 공유 캘린더에 읽기 권한이 있는 서비스 계정을 사용하세요. 실제 캘린더 ID가 필요하며 `primary`는 사용자 캘린더를 서비스 계정에 공유하는 작업을 대체하지 않습니다.
- 메트릭을 수집하고 노출하는 서비스
### 구현 단계
1. Google Calendar 메트릭 수집기 서비스 구현:
별도 이미지의 `app.py`로 저장하고 Flask·requests·google-auth·Gunicorn을 포함하세요. 수집 스레드 하나가 모든 페이지를 순회하고 반복 토큰이나 안전 페이지 한도에 도달하면 실패로 처리합니다. 값은 참가자 수나 필요한 복제본 수가 아닌 겹치는 이벤트 인스턴스 수입니다. 서비스 계정에 캘린더 접근권을 부여해야 하며 OAuth scope만으로 그 권한이 생기지는 않습니다.
```python
import datetime as dt
import os
import threading
import time
from urllib.parse import quote
from flask import Flask, jsonify
from google.auth.transport.requests import AuthorizedSession
from google.oauth2 import service_account
CALENDAR_ID = os.environ["CALENDAR_ID"]
SERVICE_ACCOUNT_FILE = "/etc/secrets/service-account.json"
POLL_SECONDS = 300
MAX_AGE_SECONDS = 360
METRIC_NAME = "upcoming_events"
def fetch_value():
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE,
scopes=["https://www.googleapis.com/auth/calendar.readonly"],
)
now = dt.datetime.now(dt.timezone.utc)
params = {"timeMin": now.isoformat(),
"timeMax": (now + dt.timedelta(hours=1)).isoformat(),
"singleEvents": "true", "showDeleted": "false",
"orderBy": "startTime", "maxResults": 2500}
url = f"https://www.googleapis.com/calendar/v3/calendars/{quote(CALENDAR_ID, safe='')}/events"
total = 0
seen_tokens = set()
with AuthorizedSession(credentials) as session:
for _ in range(100):
response = session.get(url, params=params, timeout=(3, 10))
response.raise_for_status()
body = response.json()
if body.get("error") or body.get("kind") != "calendar#events" or not isinstance(body.get("items", []), list):
raise ValueError("invalid events response")
total += len(body.get("items", []))
token = body.get("nextPageToken")
if not token:
return total
if token in seen_tokens:
raise ValueError("repeated page token")
seen_tokens.add(token)
params["pageToken"] = token
raise ValueError("pagination limit exceeded; result is incomplete")
def create_app():
app = Flask(__name__)
lock = threading.Lock()
state = {"value": None, "updated": 0.0, "healthy": False}
def collect():
while True:
try:
value = fetch_value()
if type(value) is not int or value < 0:
raise ValueError("invalid metric")
with lock:
state.update(value=value, updated=time.monotonic(), healthy=True)
except Exception as exc:
with lock:
state["healthy"] = False
app.logger.warning("Metric refresh failed: %s", type(exc).__name__)
time.sleep(POLL_SECONDS)
@app.get("/metrics")
def get_metrics():
with lock:
current = state.copy()
if not current["healthy"] or time.monotonic() - current["updated"] > MAX_AGE_SECONDS:
return jsonify(error="metric unavailable or stale"), 503
response = jsonify({METRIC_NAME: current["value"]})
response.headers["Cache-Control"] = "no-store"
return response
threading.Thread(target=collect, daemon=True).start()
return app
if __name__ == "__main__":
# Local development only; use a WSGI server for the deployment example.
create_app().run(host="127.0.0.1", port=8080)
```
2. 메트릭 수집기 서비스 배포:
실제 공유 캘린더 ID와 기존 서비스 계정 JSON 파일을 사용하세요. 다음은 실행 시 Secret을 생성하는 명령이며 검토 중 실행하지 않았습니다.
```bash
kubectl create secret generic google-calendar-secrets --namespace default --from-file=service-account.json=./service-account.json
```
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: calendar-metrics-collector
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: calendar-metrics-collector
template:
metadata:
labels:
app: calendar-metrics-collector
spec:
containers:
- name: collector
image: calendar-metrics-collector:latest
ports:
- containerPort: 8080
env:
- name: CALENDAR_ID
value: REPLACE_WITH_SHARED_CALENDAR_ID
volumeMounts:
- name: google-calendar-credentials
mountPath: /etc/secrets
readOnly: true
command:
- gunicorn
args:
- --bind
- 0.0.0.0:8080
- --workers
- '1'
- --threads
- '4'
- app:create_app()
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
readinessProbe:
httpGet:
path: /metrics
port: 8080
periodSeconds: 10
failureThreshold: 3
volumes:
- name: google-calendar-credentials
secret:
secretName: google-calendar-secrets
automountServiceAccountToken: false
---
apiVersion: v1
kind: Service
metadata:
name: calendar-metrics-collector
namespace: default
spec:
selector:
app: calendar-metrics-collector
ports:
- port: 80
targetPort: 8080
```
3. KEDA ScaledObject 구성:
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: calendar-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: calendar-processor
minReplicaCount: 1
maxReplicaCount: 10
pollingInterval: 15
cooldownPeriod: 30
triggers:
- type: metrics-api
metadata:
targetValue: "1"
url: "http://calendar-metrics-collector/metrics"
valueLocation: "upcoming_events"
```
기본 AverageValue에서는 건수/목표 비율이 설정 범위 내 복제본 수를 제안하며 처리량을 직접 모델링하지는 않습니다. collector는 독립적으로 1개를 유지하고 사용 불가 시 503을 반환합니다. 상위 API 장애만으로 재시작하는 liveness probe는 두지 않았습니다. 운영 사용에는 인증·한도·수명주기·관측성을 별도로 검증해야 합니다.
## Istio 메트릭 스케일링
Istio 서비스 메시에서 수집된 메트릭을 기반으로 애플리케이션을 스케일링하는 예제입니다. 특히 초당 요청 수(requests per second, RPS)를 기반으로 스케일링하는 방법을 살펴보겠습니다.
### 사전 요구 사항
- Istio 서비스 메시 설치
- Prometheus 설치 및 Istio와 통합
### 구현 단계
1. 기존 Istio 사이드카 설치와 주입 정책을 확인합니다. 예제는 메시 내부 라우팅이므로 존재하지 않는 ingress Gateway 리소스를 가정하지 않습니다.
```bash
istioctl proxy-status
kubectl get namespace default --show-labels
kubectl get pods -n default
```
2. 샘플 애플리케이션 배포:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-app
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: sample-app
template:
metadata:
labels:
app: sample-app
spec:
containers:
- name: sample-app
image: nginx:1.30.4
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: sample-app
namespace: default
spec:
selector:
app: sample-app
ports:
- port: 80
targetPort: 80
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: sample-app
namespace: default
spec:
hosts:
- sample-app.default.svc.cluster.local
gateways:
- mesh
http:
- route:
- destination:
host: sample-app
port:
number: 80
```
3. KEDA ScaledObject 구성:
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: istio-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: sample-app
minReplicaCount: 1
maxReplicaCount: 10
pollingInterval: 15
cooldownPeriod: 30
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.istio-system:9090
threshold: '10'
query: sum(rate(istio_requests_total{reporter="destination",destination_service="sample-app.default.svc.cluster.local"}[2m]))
ignoreNullValues: 'false'
```
기본 `AverageValue` 메트릭에서 전체 100 RPS, 복제본당 목표 10 RPS는 HPA 허용 오차·안정화·한도 적용 전 약 10개 복제본을 제안합니다. 질의는 destination 보고만 선택해 송신·수신 프록시의 중복 집계를 피합니다. Prometheus가 실제 해당 트래픽을 수집해야 합니다.
### 고급 구성
다음 예제의 **제한된 `request_path` 사용자 정의 텔레메트리 라벨**은 Istio 기본 메트릭 차원이 아닙니다. 라벨을 먼저 설정·검증하고 임의 URL로 카디널리티를 늘리지 마세요. 준비되지 않았다면 앞의 기본 질의를 사용합니다. 같은 대상의 대안 ScaledObject이며 추가 소유자로 함께 적용하지 않습니다.
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: istio-path-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: sample-app
minReplicaCount: 1
maxReplicaCount: 10
pollingInterval: 15
cooldownPeriod: 30
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.istio-system:9090
threshold: '5'
query: sum(rate(istio_requests_total{reporter="destination",destination_service="sample-app.default.svc.cluster.local",request_path="/api/v1/products"}[2m]))
ignoreNullValues: 'false'
```
오류 비율·지연은 스케일링보다 알림에 적합한 경우가 많습니다. 다음 선택적 예시는 서비스 전체 비율에 `metricType: Value`를 사용하고 0 분모를 방어하며 확장 속도를 제한합니다. 복제본 추가가 진단된 과부하를 줄인다는 가정이 필요합니다. 하위 시스템 오류나 적은 표본의 잡음은 오히려 해로운 확장을 유발할 수 있으며 운영 효과를 실측하지 않았습니다.
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: istio-error-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: sample-app
minReplicaCount: 1
maxReplicaCount: 10
pollingInterval: 15
cooldownPeriod: 30
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.istio-system:9090
threshold: '0.05'
query: (sum(rate(istio_requests_total{reporter="destination",destination_service="sample-app.default.svc.cluster.local",response_code=~"5.*"}[2m]))
or vector(0)) / clamp_min(sum(rate(istio_requests_total{reporter="destination",destination_service="sample-app.default.svc.cluster.local"}[2m])),
0.001)
ignoreNullValues: 'false'
metricType: Value
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleUp:
policies:
- type: Pods
value: 1
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
```
## Cron 기반 스케일링
KEDA는 Cron 표현식을 사용하여 시간 기반 스케일링을 지원합니다. 이를 통해 예측 가능한 트래픽 패턴이나 일정에 따라 애플리케이션을 사전에 스케일링할 수 있습니다.
### 기본 Cron 스케일러
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: cron-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: sample-app
minReplicaCount: 0
maxReplicaCount: 10
pollingInterval: 15
cooldownPeriod: 30
triggers:
- type: cron
metadata:
timezone: Asia/Seoul
start: 30 * * * *
end: 45 * * * *
desiredReplicas: "5"
```
매시간 30–45분 사이에는 Cron 트리거가 최소 목표 5개를 제안합니다. 구간 밖에서는 비활성화되고 0으로 줄어들기까지 폴링·설정된 쿨다운(여기서는 30초)·컨트롤러 실행 지연이 필요합니다. 정확히 45분에 축소가 완료된다고 보장하지 않습니다.
### 업무 시간과 비업무 시간
`minReplicaCount: 2`를 비업무 시간 기준으로 두고 평일 업무 시간에 5개를 요청하는 Cron 하나를 사용합니다. 야간·주말 구간 중첩을 피할 수 있습니다. 업무 종료 후 감소는 HPA 안정화 설정으로 지연될 수 있습니다.
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: multi-cron-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: sample-app
minReplicaCount: 2
maxReplicaCount: 10
pollingInterval: 15
cooldownPeriod: 30
triggers:
- type: cron
metadata:
timezone: Asia/Seoul
start: 0 9 * * 1-5
end: 0 18 * * 1-5
desiredReplicas: '5'
```
### Cron과 다른 스케일러 결합
Cron 스케일러를 다른 스케일러와 결합하여 기본 스케일링 동작을 설정하고 실제 부하에 따라 추가로 스케일링할 수 있습니다:
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: combined-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: sample-app
minReplicaCount: 1
maxReplicaCount: 20
pollingInterval: 15
cooldownPeriod: 30
triggers:
- type: cron
metadata:
timezone: Asia/Seoul
start: 0 9 * * 1-5
end: 0 18 * * 1-5
desiredReplicas: '5'
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc.cluster.local:9090
threshold: '10'
query: sum(rate(http_requests_total{app="sample-app"}[1m]))
ignoreNullValues: 'false'
```
## Amazon EKS와의 통합
KEDA는 Kubernetes 호환성·오퍼레이터 인증·권한·통신 경로를 구성하면 AWS 메트릭으로 EKS 워크로드를 확장할 수 있습니다. KEDA 자체가 컴퓨팅 용량을 추가하지는 않으므로 적절한 노드·Fargate 용량 설계와 함께 사용하세요.
### EKS에 KEDA 설치
```bash
helm status keda -n keda
kubectl get deployment -n keda
```
### AWS 서비스 기반 스케일링
#### SQS 대기열 기반 스케일링
```yaml
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: aws-credentials
namespace: default
spec:
podIdentity:
provider: aws
identityOwner: keda
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: aws-sqs-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: sqs-consumer
minReplicaCount: 0
maxReplicaCount: 10
pollingInterval: 15
cooldownPeriod: 30
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.us-west-2.amazonaws.com/123456789012/my-queue
queueLength: '5'
awsRegion: us-west-2
authenticationRef:
name: aws-credentials
```
#### CloudWatch 메트릭 기반 스케일링
```yaml
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: aws-credentials
namespace: default
spec:
podIdentity:
provider: aws
identityOwner: keda
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: aws-cloudwatch-scaler
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: cloudwatch-app
minReplicaCount: 1
maxReplicaCount: 10
pollingInterval: 15
cooldownPeriod: 30
triggers:
- type: aws-cloudwatch
metadata:
namespace: AWS/SQS
dimensionName: QueueName
dimensionValue: my-queue
metricName: ApproximateNumberOfMessagesVisible
targetMetricValue: '5'
minMetricValue: '0'
awsRegion: us-west-2
metricStat: Average
metricStatPeriod: '60'
metricCollectionTime: '300'
authenticationRef:
name: aws-credentials
```
### IRSA(IAM Roles for Service Accounts) 통합
이 예제는 `system:serviceaccount:keda:keda-operator`용 **기존 IRSA 역할**을 가정합니다. 해당 클러스터 OIDC 공급자와 `aud: sts.amazonaws.com` 조건으로 신뢰를 제한하세요. 지정한 큐의 `sqs:GetQueueAttributes`와 필요한 CloudWatch 메트릭 조회 작업(예: `cloudwatch:GetMetricData`)을 허용합니다. 리소스 단위 제한을 지원하지 않는 CloudWatch 작업은 Resource 와일드카드가 필요하며 요청 리전으로 제한할 수 있습니다. 소비자 권한은 별도입니다. 아래 명령은 구성을 조회하며 역할 생성은 검토된 인프라 설정에서 처리합니다.
```bash
: "${KEDA_IAM_ROLE_NAME:?Set the existing IRSA role name}"
aws iam get-role --role-name "$KEDA_IAM_ROLE_NAME" --query Role.AssumeRolePolicyDocument
kubectl get serviceaccount keda-operator -n keda -o yaml
```
```yaml
serviceAccount:
operator:
create: true
name: keda-operator
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/keda-operator-role
eks.amazonaws.com/sts-regional-endpoints: 'true'
```
## 모범 사례
### 성능 최적화
1. **적절한 폴링 간격 설정**: 워크로드 특성에 맞는 폴링 간격 설정
2. **쿨다운과 HPA 동작 분리**: 쿨다운은 제로 축소를, HPA 안정화·정책은 0보다 큰 복제본 수 변경을 제어합니다.
3. **리소스 요청 및 제한 설정**: KEDA 구성 요소에 적절한 리소스 할당
4. **효율적인 쿼리 작성**: 메트릭 쿼리 최적화
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: optimized-scaler
namespace: default
spec:
pollingInterval: 30
cooldownPeriod: 300
scaleTargetRef:
name: my-app
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc.cluster.local:9090
threshold: '100'
query: sum(rate(http_requests_total{namespace="default",app="my-app"}[2m]))
ignoreNullValues: 'false'
```
### 안정성 향상
1. **다중 트리거 이해**: HPA는 보통 가장 큰 복제본 제안을 선택하며 값을 합산하지 않습니다. 메트릭 오류가 축소를 막을 수도 있습니다.
2. **적절한 최소 및 최대 복제본 설정**: 워크로드 요구사항에 맞는 범위 설정
3. **장애 처리 전략**: KEDA 2.20 fallback은 CPU·메모리를 제외한 Value·AverageValue 트리거를 지원하며 ScaledObject용이고 ScaledJob용은 아닙니다. 통신·인증 장애 동작을 시험하세요.
4. **모니터링 및 알림 설정**: KEDA 작동 상태 모니터링
```yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: reliable-scaler
namespace: default
spec:
minReplicaCount: 2
maxReplicaCount: 20
fallback:
failureThreshold: 3
replicas: 5
scaleTargetRef:
name: my-app
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc.cluster.local:9090
threshold: '100'
query: sum(rate(http_requests_total{namespace="default",app="my-app"}[2m]))
ignoreNullValues: 'false'
```
### 보안 강화
아래 NetworkPolicy는 Twitter collector의 TCP8080 인바운드를 KEDA 오퍼레이터로 제한하며 KEDA 제어 경로 전체의 정책이 아닙니다. 집행에는 NetworkPolicy 지원이 필요합니다. KEDA 자체를 제한하기 전에는 API 서버→메트릭 API·어드미션 웹훅, 오퍼레이터↔메트릭 서버, DNS, Kubernetes API, 스케일러별 엔드포인트를 고려하세요. EKS 제어 플레인은 단순히 `kube-system`의 Pod가 아닙니다.
1. **최소 권한 원칙 적용**: 필요한 권한만 부여
2. **시크릿 관리**: 민감한 정보 안전하게 관리
3. **네트워크 정책 적용**: KEDA 구성 요소에 대한 액세스 제한
4. **RBAC 설정**: 적절한 역할 기반 액세스 제어 구성
```yaml
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: secure-auth
namespace: default
spec:
secretTargetRef:
- parameter: host
name: rabbitmq-credentials
key: host
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: twitter-metrics-from-keda
namespace: default
spec:
podSelector:
matchLabels:
app: twitter-metrics-collector
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: keda
podSelector:
matchLabels:
app: keda-operator
ports:
- protocol: TCP
port: 8080
```
## 문제 해결
### 일반적인 문제
#### 1. 스케일링이 작동하지 않음
**증상**: 메트릭이 임계값을 초과해도 파드가 스케일링되지 않음
**해결 방법**:
- KEDA 로그 확인
- 메트릭 소스 연결 확인
- 인증 구성 확인
```bash
# KEDA 오퍼레이터 로그 확인
kubectl logs -n keda -l app=keda-operator
# KEDA 메트릭 서버 로그 확인
kubectl logs -n keda -l app=keda-operator-metrics-apiserver
# ScaledObject 상태 확인
kubectl get scaledobject -n -o yaml
```
#### 2. 제로 스케일링 문제
**증상**: 활동이 없을 때 0으로 스케일 다운되지 않음
**해결 방법**:
- minReplicaCount 설정 확인
- 메트릭 값 확인
- HPA 상태 확인
```bash
# HPA 상태 확인
kubectl get hpa -n
# 메트릭 값 직접 확인
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces//?labelSelector=scaledobject.keda.sh%2Fname%3D" | jq
```
#### 3. 인증 문제
**증상**: 메트릭 소스에 연결할 수 없음
**해결 방법**:
- TriggerAuthentication 구성 확인
- 시크릿 또는 환경 변수 확인
- 권한 확인
```bash
# TriggerAuthentication 확인
kubectl get triggerauthentication -n -o yaml
# 시크릿 확인
kubectl get secret -n -o json | jq '{name: .metadata.name, type: .type, keys: ((.data // {}) | keys)}'
```
### 디버깅 도구
```bash
# KEDA 버전 확인
kubectl get deployment -n keda keda-operator -o jsonpath="{.spec.template.spec.containers[0].image}"
# ScaledObject 상태 확인
kubectl describe scaledobject -n
# HPA 상태 확인
kubectl describe hpa -n
# 메트릭 값 확인
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces//?labelSelector=scaledobject.keda.sh%2Fname%3D"
# KEDA 로그 확인
kubectl logs -n keda -l app=keda-operator --tail=100
```
## 결론
KEDA(Kubernetes Event-driven Autoscaling)는 Kubernetes 환경에서 이벤트 기반 자동 확장을 제공하는 강력한 도구입니다. 기본 Kubernetes HPA를 확장하여 다양한 이벤트 소스와 메트릭을 기반으로 워크로드를 스케일링할 수 있게 해줍니다.
이 문서에서는 KEDA의 기본 개념, 설치 방법, 다양한 스케일러 사용법, 커스텀 메트릭 스케일링, Twitter 및 Google Calendar와 같은 외부 서비스 통합, Istio 메트릭 기반 스케일링, Cron 기반 스케일링, Amazon EKS와의 통합, 모범 사례 및 문제 해결에 대해 살펴보았습니다.
KEDA를 사용하면 애플리케이션을 더 효율적으로 스케일링하고, 리소스 사용을 최적화하며, 비용을 절감할 수 있습니다. 특히 이벤트 기반 아키텍처와 서버리스 패턴을 구현하는 데 매우 유용합니다.
### 다음 단계
- KEDA를 사용한 서버리스 아키텍처 구현
- 다양한 이벤트 소스와의 통합 탐색
- 커스텀 스케일러 개발
- 멀티 클러스터 환경에서의 KEDA 활용
- KEDA와 다른 클라우드 네이티브 도구와의 통합
## 참고 자료
- [KEDA 공식 문서](https://keda.sh/docs/)
- [KEDA GitHub 저장소](https://github.com/kedacore/keda)
- [KEDA 스케일러 목록](https://keda.sh/docs/latest/scalers/)
- [KEDA Operator Hub](https://operatorhub.io/operator/keda)
- [AWS IRSA 설정](https://docs.aws.amazon.com/eks/latest/userguide/associate-service-account-role.html)
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/autoscaling/05-keda-quiz)를 풀어보세요.
이번 수정에서 확인한 공식 자료: [KEDA compatibility](https://keda.sh/docs/2.20/operate/cluster/), [ScaledObject](https://keda.sh/docs/2.20/reference/scaledobject-spec/), [ScaledJob](https://keda.sh/docs/2.20/reference/scaledjob-spec/), [AWS authentication](https://keda.sh/docs/2.20/authentication-providers/aws/), [External scaler gRPC](https://keda.sh/docs/2.20/concepts/external-scalers/), [X counts](https://docs.x.com/x-api/posts/counts/quickstart), [Calendar events](https://developers.google.com/workspace/calendar/api/v3/reference/events/list), [HPA](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/).
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/autoscaling/02-karpenter
----------------------------------------
# Karpenter
> **지원 버전**: Karpenter 1.14 LTS(예제: 1.14.1); Kubernetes/EKS는 호환성 표와 공급자 지원 범위로 선택하세요.
> **마지막 업데이트**: 2026년 9월 11일
## 목차
- [소개](#소개)
- [아키텍처](#아키텍처)
- [설치 및 구성](#설치-및-구성)
- [NodePool](#nodepool)
- [노드 클래스](#노드-클래스)
- [인터럽션 처리](#인터럽션-처리)
- [통합](#통합)
- [Amazon EKS와의 통합](#amazon-eks와의-통합)
- [모범 사례](#모범-사례)
- [문제 해결](#문제-해결)
- [결론](#결론)
## 소개
Karpenter는 오픈 소스 노드 오토스케일러입니다. 이 장은 호환 Kubernetes 워크로드에 EC2 용량을 제공하는 AWS 공급자 구현을 다룹니다. 가용성·효율은 제약 조건, 클라우드 용량, 노드 초기화와 애플리케이션 설계에 따라 달라집니다.
### Karpenter의 주요 이점
1. **수요에 따른 스케일링**: 스케줄링되지 못하는 워크로드 수요에 반응해 프로비저닝을 시작하며 노드·애플리케이션 준비 시간은 고정적으로 보장되지 않습니다.
2. **비용 최적화**: 워크로드에 가장 적합한 인스턴스 유형 선택
3. **단순한 구성**: 선언적 API를 통한 간단한 구성
4. **워크로드 중심 설계**: 파드 요구 사항에 기반한 노드 프로비저닝
5. **클라우드 통합**: 클라우드 제공업체의 기능 활용
6. **효율적인 빈 패킹**: 리소스 활용도 최적화
7. **유연한 노드 관리**: 노드 수명 주기 관리 및 통합 인터럽션 처리
### 기존 오토스케일러와의 비교
| 기능 | Karpenter | Cluster Autoscaler | Cloud Provider 관리형 노드 그룹 |
|------|-----------|-------------------|---------------------------|
| 스케일링 속도 | 스케줄링·EC2 용량·초기화에 따라 다름 | 노드 그룹 확장·초기화에 따라 다름 | 확장 정책·용량·초기화에 따라 다름 |
| 인스턴스 유형 선택 | 동적 | 노드 그룹 기반 | 노드 그룹 기반 |
| 빈 패킹 효율성 | 워크로드·제약 조건에 따라 다름 | 워크로드·노드 그룹에 따라 다름 | 스케줄러·확장 컨트롤러에 따라 다름 |
| 구성 복잡성 | 낮음 | 중간 | 낮음 |
| 클라우드 통합 | 공급자별 구현 | 여러 클라우드 공급자 통합 | 공급자 기본 기능 |
| 노드 그룹 관리 | 불필요 | 필요 | 필요 |
| 인터럽션 처리 | 설정된 이벤트 처리·노드 수명주기 | 플랫폼·통합 구성에 따라 다름 | 플랫폼별 처리 |
> **참고**: EKS는 2026년4월8일 Managed Node Group warm pool 지원을 추가했습니다. 사전 초기화된 인스턴스는 반복 초기화 작업을 줄이며 Stopped·Running 상태는 전환 시간과 비용이 다르고 scale-in 재사용은 선택 사항입니다. AWS 발표에 따르면 Cluster Autoscaler 추가 설정은 필요하지 않습니다. 재개·노드 준비·애플리케이션 시작 시간은 남습니다. EKS Managed Node Group/Auto Scaling 기능이며 Karpenter가 관리하는 풀은 아닙니다.
## 아키텍처
Karpenter는 Kubernetes 컨트롤러로 작동하며, 스케줄링할 수 없는 파드를 감지하고 적절한 노드를 프로비저닝합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-autoscaling-02-karpenter-0.html)
### Karpenter 워크플로우
다음 다이어그램은 Karpenter가 EKS 클러스터에서 작동하는 방식을 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-autoscaling-02-karpenter-1.html)
### 주요 구성 요소
1. **Karpenter 컨트롤러**: 스케줄링 수요를 시뮬레이션하고 NodeClaim과 노드 수명주기를 관리합니다. 실제 Pod 바인딩은 Kubernetes 스케줄러가 수행합니다.
2. **CRD CEL 검증**: NodePool·EC2NodeClass를 CRD의 CEL 검증 규칙으로 유효성 검사 (어드미션·변환 웹훅은 Karpenter 1.1에서 제거)
3. **NodePool·NodeClaim CRD**: NodePool은 정책을 정의하고 NodeClaim은 개별 프로비저닝 노드의 요구사항·수명주기를 기록합니다.
4. **EC2NodeClass CRD**: 프로비저닝할 노드의 구성을 정의
5. **클라우드 제공업체 통합**: 클라우드 제공업체의 API와 통합하여 컴퓨팅 리소스 관리
### 작동 방식
1. Karpenter 컨트롤러가 스케줄링할 수 없는 파드를 감지
2. 파드 요구 사항(리소스, 노드 선택기, 허용 오차 등)을 분석
3. NodePool 및 EC2NodeClass 구성에 따라 적절한 노드 유형 결정
4. 클라우드 제공업체 API를 호출하여 노드 프로비저닝
5. 노드가 등록·준비되면 Kubernetes 스케줄러가 적합한 Pod를 바인딩할 수 있습니다. Karpenter가 kube-scheduler를 대체하지는 않습니다.
6. 통합·드리프트·만료·수동 삭제·클라우드 인터럽션은 서로 다른 계기와 보호 장치를 사용하며 모두 SQS 인터럽션 이벤트인 것은 아닙니다.
## 설치 및 구성
apiVersion/kind가 없는 YAML은 설명 중인 NodePool·EC2NodeClass spec의 설정 조각이며 단독 kubectl apply 문서가 아닙니다.
기존 클러스터용 독립 학습 예제이며 운영 준비가 검증된 배포 묶음이 아닙니다. 반복되는 객체 이름은 대안입니다. 설치 전에 IAM/OIDC·노드 접근·승인된 서브넷/보안 그룹·부트스트랩 용량·인터럽션 큐를 준비·검증하세요. YAML의 `my-cluster` 등은 문자 그대로의 예시 이름이며 kubectl은 저장된 YAML 안의 `${CLUSTER_NAME}`를 확장하지 않습니다. 이번 감사에서는 AWS 생성·Karpenter 설치·스케일링 실측을 수행하지 않았습니다. 신규 설치 명령은 같은 릴리스가 있으면 실패하며 기존 설치는 버전별 업그레이드·CRD 마이그레이션 지침을 따라야 합니다.
### 사전 요구 사항
- 플랫폼 지원 버전과 Karpenter 호환성 표를 함께 확인하세요. 공개 표의 최소 Karpenter 버전은 Kubernetes1.34에1.6, 1.35에1.9, 1.36에1.13입니다. 최소 버전이라고 모든 구형 마이너가 계속 유지 보수된다는 뜻은 아닙니다. 현재 표는1.37 호환성을 입증하지 않습니다. EKS 제공·지원 버전은 별도로 확인해야 합니다.
- kubectl 설정
- 클라우드 제공업체 자격 증명 및 권한
- Helm (선택 사항)
### AWS EKS에 설치
#### 1. IAM 역할 및 정책 설정
조회 명령은 기존 컨트롤러·노드 역할과 API 또는 API_AND_CONFIG_MAP EKS 인증을 가정합니다. CONFIG_MAP 전용 클러스터는 list-access-entries 대신 기존 aws-auth 노드 매핑을 확인해야 하며 설정 도중 인증 모드를 부수적으로 변경하지 마세요. 노드 역할은 EC2를 신뢰하고 AmazonEKSWorkerNodePolicy·AmazonEC2ContainerRegistryPullOnly 등의 워커·ECR 다운로드 권한이 필요합니다. 지원되는 경우 VPC CNI에 별도 인증을 부여하세요. SSM 권한은 선택 사항이며 설정된 에이전트가 필요합니다. AmazonEKSClusterPolicy는 Karpenter 컨트롤러 정책의 대체물이 아닙니다.
```bash
# Set the existing cluster and Region; these commands only inspect AWS/Kubernetes.
export CLUSTER_NAME="my-cluster"
export AWS_REGION="us-west-2"
export KARPENTER_VERSION="1.14.1"
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
CLUSTER_ENDPOINT=$(aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" --query cluster.endpoint --output text)
export ACCOUNT_ID CLUSTER_ENDPOINT
kubectl config current-context
aws iam get-role --role-name "KarpenterControllerRole-${CLUSTER_NAME}" --query Role.AssumeRolePolicyDocument
aws iam get-role --role-name "KarpenterNodeRole-${CLUSTER_NAME}" --query Role.AssumeRolePolicyDocument
aws eks list-access-entries --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME"
```
#### 2. Helm을 사용한 설치
참조 큐와 부트스트랩 용량을 준비한 뒤 신규 설치 예제를 한 번만 사용하세요. 업그레이드는 버전별 마이그레이션 지침과 해당 CRD 갱신(예: 별도로 관리하는 karpenter-crd 차트)이 필요합니다. 컨트롤러 차트 업그레이드만으로 CRD가 일반적으로 갱신되지는 않습니다. 기존 CRD 관리 방식을 옮기기 전에 소유권을 확인하세요.
```bash
# Run only after the prerequisites and the current kubectl context are verified.
: "${CLUSTER_NAME:?Set the existing cluster name}"
: "${ACCOUNT_ID:?Set the AWS account ID}"
: "${CLUSTER_ENDPOINT:?Set the matching EKS endpoint}"
KARPENTER_VERSION="1.14.1"
helm install karpenter oci://public.ecr.aws/karpenter/karpenter \
--version "$KARPENTER_VERSION" \
--namespace karpenter --create-namespace \
--set-string 'serviceAccount.annotations.eks\.amazonaws\.com/role-arn'="arn:aws:iam::${ACCOUNT_ID}:role/KarpenterControllerRole-${CLUSTER_NAME}" \
--set-string settings.clusterName="$CLUSTER_NAME" \
--set-string settings.clusterEndpoint="$CLUSTER_ENDPOINT" \
--set-string settings.interruptionQueue="$CLUSTER_NAME" \
--wait --timeout 5m
```
#### 3. 설치 확인
```bash
kubectl get deployments,pods -n karpenter
kubectl rollout status deployment/karpenter -n karpenter --timeout=180s
kubectl get nodepools,ec2nodeclasses,nodeclaims
```
기본 컨트롤러 복제본2개의 출력 형식 예시이며 이번 감사의 실행 결과가 아닙니다:
```
NAME READY STATUS RESTARTS AGE
karpenter-- 1/1 Running 0 1m
karpenter-- 1/1 Running 0 1m
```
### 기본 NodePool 및 EC2NodeClass 구성
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
limits:
cpu: '1000'
memory: 1000Gi
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
- key: kubernetes.io/arch
operator: In
values:
- amd64
- key: node.kubernetes.io/instance-type
operator: In
values:
- m5.large
- m5.xlarge
- m5.2xlarge
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
role: KarpenterNodeRole-my-cluster
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
tags:
karpenter.sh/discovery: my-cluster
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3
deleteOnTermination: true
encrypted: true
```
## NodePool
NodePool은 Karpenter가 노드를 프로비저닝하는 방법을 정의하는 Kubernetes 사용자 정의 리소스입니다. 이전의 Provisioner를 대체합니다.
### 기본 NodePool 구성
아래 특수 taint는 의도적으로 워크로드의 일치하는 toleration을 요구합니다. 설정한 taint를 제거하는 초기화 담당자를 검증하기 전까지 startupTaints는 비워둡니다. 담당자 없는 taint를 임의로 추가하면 노드를 사용할 수 없게 될 수 있습니다. 리소스·초기화·중단 예제는 대안이며 운영 NodePool에 모두 더하는 변경이 아닙니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
metadata:
labels:
environment: training
app: web
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
- key: kubernetes.io/arch
operator: In
values:
- amd64
- key: node.kubernetes.io/instance-type
operator: In
values:
- m5.large
- m5.xlarge
- m5.2xlarge
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
taints:
- key: example.com/special-taint
value: 'true'
effect: NoSchedule
startupTaints: []
expireAfter: 720h
limits:
cpu: '1000'
memory: 1000Gi
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
```
### 요구 사항 구성
요구사항은 서로, EC2NodeClass, Pod 제약과 교집합으로 적용됩니다. amd64·arm64를 모두 허용해도 적합한 인스턴스·AMI·컨테이너 이미지가 두 아키텍처를 지원해야 의미가 있습니다. AZ·용량 유형 목록만으로 균등 분산이 보장되지는 않으므로 워크로드 토폴로지 제약을 사용하세요.
요구 사항은 Karpenter가 프로비저닝할 노드의 특성을 정의합니다:
```yaml
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
- spot
- key: kubernetes.io/arch
operator: In
values:
- amd64
- arm64
- key: node.kubernetes.io/instance-type
operator: In
values:
- m5.large
- m5.xlarge
- c5.large
- m6g.large
- c6g.large
- key: topology.kubernetes.io/zone
operator: In
values:
- us-west-2a
- us-west-2b
- us-west-2c
- key: kubernetes.io/os
operator: In
values:
- linux
```
### 제한 구성
`spec.limits`는 총 프로비저닝 리소스를 제한하지만 병렬 프로비저닝의 검사는 최종 일관성이므로 일시적으로 초과할 수 있습니다. 엄격한 과금 상한이 아닙니다. 문자열 수량으로 API·GitOps 형식 차이를 줄일 수 있습니다.
```yaml
limits:
cpu: '1000'
memory: 1000Gi
nvidia.com/gpu: '10'
```
### 실험적인 DRA 할당 추적 (v1.13)
코어 Karpenter v1.13은 DRA 디바이스 할당 추적을 추가했습니다. 일반적인 운영 DRA 프로비저닝을 보장하는 것은 아닙니다. AWS1.14.1 차트는 `settings.ignoreDRARequests: true`가 기본이고 업스트림도 정식 DRA 지원은 아직 GA가 아니라고 명시합니다. Kubernetes1.29 이상이라는 조건만으로 호환성을 판단할 수 없습니다. 정확한 Kubernetes 리소스 API·DRA 드라이버·ResourceClaim/ResourceSlice·Karpenter 설정을 검증해야 하며 아래 디바이스 플러그인 확장 리소스 예제와 동일하게 취급하지 마세요.
### 노드 만료 구성
`expireAfter`는 만료에 따른 드레이닝 시작 시점이며 교체 완료 시간을 보장하지 않습니다. 아래 선택적 `terminationGracePeriod`는 드레이닝을 제한하지만 PDB에 막힌 Pod도 강제 삭제할 수 있습니다. 애플리케이션 종료·복구 요구를 검증한 뒤 기한을 선택하세요.
```yaml
spec:
template:
spec:
expireAfter: 720h
terminationGracePeriod: 30m
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
```
### NodeReadinessController Taint 인식 (v1.13)
코어 Karpenter v1.13은 별도 NodeReadinessController의 `readiness.k8s.io/` taint를 초기화 전 관리 노드의 스케줄링 시뮬레이션에서 임시 taint로 인식합니다. 초기화 완료는 해당 taint가 제거될 때까지 기다립니다. Karpenter가 taint를 삭제하거나 Kubernetes 스케줄러가 우회하게 하는 기능이 아닙니다. 다른 초기화 taint에는 정확한 `startupTaints` 설정과 이를 제거하는 컨트롤러가 여전히 필요합니다.
### 2026년 7월 업데이트: v1.14 릴리스
2026년 7월 11일 릴리스된 Karpenter v1.14의 주요 기능:
- **CapacityBuffers API 지원**: alpha 용량 버퍼 통합이며 `CapacityBuffer`는 기본 비활성화입니다. 해당 CRD·컨트롤러 구성이 필요하고 예비 용량이 무료이거나 지연을 보장하는 것은 아닙니다.
- **프리뷰 인스턴스 타입 지원**: 적합한 프리뷰 제공 용량을 인식하지만 실제 프로비저닝에는 계정·리전 접근권과 용량이 필요합니다.
- **Nitro Enclaves 지원**: NodeClaim 리소스 요청에 `eks.amazonaws.com/nip-slots`가 있으면 공급자 구현이 생성한 시작 템플릿의 `EnclaveOptions.Enabled`를 설정합니다. 호환 인스턴스·AMI·디바이스 플러그인이 필요하며1.14.1에 `EC2NodeClass.spec.enclaveOptions` 필드는 없습니다.
- 버그 수정: 보조 ENI의 기본 IP 계산 반영, Zonal Shift 초기 캐시 동기화 수정, AWS SDK 클라이언트 타임아웃 설정 등
자세한 내용은 [v1.14.0 릴리스 노트](https://github.com/aws/karpenter-provider-aws/releases/tag/v1.14.0)를 참고하세요.
2026년7월17일 구형 브랜치에1.3.8·1.11.3 등의 패치가 배포되었습니다. 이 역사적 백포트가 모든 중간 마이너의 계속된 지원을 뜻하지는 않습니다. 현재 지원 정책은 LTS1.9를2027년2월까지, LTS1.14를2027년7월까지 지원하며 일반 마이너는 다음 마이너가 나올 때까지만 지원합니다. 구형 라인이 계속 유지된다고 가정하지 말고 지원되는 라인과 마이그레이션 지침을 선택하세요.
AWS의2026년7월22일 발표는 Karpenter/EKS Auto Mode의 EFA 인터페이스·배치 그룹 구성을 추가했습니다. AWS Karpenter1.14.1의 실제 필드는 NodePool이 아닌 `EC2NodeClass.spec.networkInterfaces`와 `spec.placementGroupSelector`입니다. EFA-only 인터페이스는 VPC IP를 소비하지 않지만 device/card index0의 기본 `interface`는 여전히 필요합니다. 기존 배치 그룹을 이름 또는 ID로 선택하며 해당 cluster/spread/partition 전략과 지원 인스턴스가 배치를 제한합니다. 이 예제에서 HPC 워크로드를 구성·검증하지는 않았습니다.
### 2026년 8월 업데이트: v1.14.1 패치 릴리스
2026년 8월 21일 v1.14 라인의 첫 패치인 [v1.14.1](https://github.com/aws/karpenter-provider-aws/releases/tag/v1.14.1)이 공개되었습니다. 업스트림 `sigs.k8s.io/karpenter` 버전 갱신과 v1.14.0 이후 수정 사항의 체리픽을 담은 유지 보수 릴리스입니다.
## 노드 클래스
노드 클래스는 Karpenter가 프로비저닝하는 노드의 구성을 정의합니다. AWS에서는 EC2NodeClass CRD를 사용합니다.
### AWS EC2NodeClass 구성
```yaml
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
tags:
karpenter.sh/discovery: my-cluster
environment: training
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3
deleteOnTermination: true
encrypted: true
role: KarpenterNodeRole-my-cluster
amiSelectorTerms:
- alias: al2023@latest
userData: '#!/bin/bash
echo "Hello from Karpenter node!"
'
metadataOptions:
httpEndpoint: enabled
httpProtocolIPv6: disabled
httpPutResponseHopLimit: 1
httpTokens: required
```
### 서브넷 및 보안 그룹 선택
서브넷과 보안 그룹은 태그 기반 선택 조건(selector terms)을 사용하여 선택할 수 있습니다. 여러 term은 OR로, 하나의 term 안의 태그는 AND로 평가됩니다:
```yaml
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
Name: private-*
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
```
### AMI 구성
Karpenter는 `amiSelectorTerms`로 AMI를 선택합니다. alias는 패밀리·버전을 선택하지만 **`@latest`는 버전 고정이 아닙니다.** 해석된 AMI가 바뀌면 드리프트가 발생할 수 있으며 아래 `@latest`는 학습 예제입니다. 운영에서는 실제 릴리스 날짜의 `al2023@vYYYYMMDD` 또는 승인한 AMI ID를 조회·시험한 뒤 배포하세요. EKS AL2 AMI는 Kubernetes1.33 이상에 게시되지 않습니다. 아래 변형들은 대안이며 Custom AMI에는 올바른 부트스트랩·등록 taint·kubelet·CNI/런타임·인증 구성이 필요합니다.
```yaml
# Amazon Linux 2023
amiSelectorTerms:
- alias: al2023@latest
---
# Bottlerocket
amiSelectorTerms:
- alias: bottlerocket@latest
---
# 사용자 정의 AMI (ID 지정) — alias 항목이 없으면 amiFamily가 필수
amiFamily: Custom
amiSelectorTerms:
- id: "ami-0123456789abcdef0"
# Ubuntu: v1 alias 없음 — amiFamily: Custom과 id/tags/name 항목을 사용
```
### 블록 디바이스 구성
디바이스 매핑은 AMI 패밀리별로 다릅니다. AL2023 예제는 루트 /dev/xvda를 사용하며 다른 AMI는 실제 레이아웃을 확인하세요. 추가 볼륨은 파일시스템·마운트 또는 애플리케이션 저장 계획이 필요합니다. 고객 관리 KMS 키에는 해당 키·IAM 권한도 필요합니다.
노드의 스토리지 구성을 정의할 수 있습니다:
```yaml
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3
iops: 3000
throughput: 125
deleteOnTermination: true
encrypted: true
kmsKeyID: arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab
- deviceName: /dev/xvdb
ebs:
volumeSize: 500Gi
volumeType: gp3
deleteOnTermination: true
encrypted: true
```
### 사용자 데이터 구성
이 셸 사용자 데이터는 AL2023을 가정하며 Karpenter가 생성하는 bootstrap/nodeadm 설정과 병합됩니다. Bottlerocket·Windows에 그대로 사용하는 일반 부트스트랩이 아닙니다. 시작할 때마다 무제한 패키지 갱신을 하지 말고 AMI에 패키지를 넣어 검증하세요. CloudWatch Agent 설치·시작만으로 설정·IAM·통신 경로가 없는 상태에서 수집이 이루어지지는 않습니다.
```yaml
userData: |
#!/bin/bash
set -euo pipefail
# Only for a workload that requires this setting; keep node packages in a tested AMI.
cat > /etc/sysctl.d/99-workload-map-count.conf <<'EOF'
vm.max_map_count=262144
EOF
sysctl -p /etc/sysctl.d/99-workload-map-count.conf
```
### 노드 통합 프로세스
다음 다이어그램은 Karpenter의 노드 통합(consolidation) 프로세스를 보여줍니다. 이 기능은 클러스터 효율성을 최적화하고 비용을 절감하는 데 중요합니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-autoscaling-02-karpenter-2.html)
## 인터럽션 처리
설정된 인터럽션 처리는 용량 손실 전에 대응을 시도합니다. 알림·대체 용량·종료 기한·애플리케이션 복구에 따라 결과가 달라지며 무중단을 보장하지 않습니다.
### 통합 인터럽션 처리
클라우드 인터럽션 처리는 통합·만료와 별개이며 다음과 같은 신호를 다룹니다.
1. **Spot 중단 경고**: 가능한 경우 드레이닝과 대체 용량 요청을 시작하지만 알림 시간은 복구 SLA가 아닙니다.
2. **예정된 상태·유지 보수 이벤트**: 영향을 받는 인스턴스에 대응합니다.
3. **인스턴스 중지·종료 이벤트**: 서비스를 떠나는 용량을 조정합니다.
4. **EC2 인스턴스 상태 검사 실패**: 필요한 EC2 권한으로 상태를 확인합니다. Rebalance recommendation만으로는 자동 taint·drain·terminate를 수행하지 않고 이벤트를 게시합니다.
### 인터럽션 처리 구성
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
# 노드 만료 설정
expireAfter: 720h # 30일
# 통합(consolidation) 설정
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
```
### 드레이닝 구성
Karpenter는 정상 드레이닝에서 eviction을 사용합니다. Helm은 컨트롤러를 설정하고 NodePool disruption budget은 자발적인 통합·드리프트 시작을 제한하며 모든 동시 노드 손실을 제한하지는 않습니다.30% 예산은 올림 후 삭제 중·준비되지 않은 노드 수를 차감하므로 엄격한30% 상한이 아닙니다. 만료·인터럽션·복구는 강제 종료 동작이 다를 수 있습니다. `interruptionQueue` 활성화 전에 SQS 큐·EventBridge 규칙/대상·큐 정책·컨트롤러 권한을 구성해야 하며 Helm 필드만으로 생성되지 않습니다.
```yaml
settings:
clusterName: my-cluster
interruptionQueue: my-cluster
batchMaxDuration: 10s
batchIdleDuration: 1s
featureGates:
spotToSpotConsolidation: false
controller:
resources:
requests:
cpu: 1
memory: 1Gi
limits:
cpu: '1'
memory: 1Gi
logLevel: info
```
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
expireAfter: 720h
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
budgets:
- nodes: "30%" # 자발적 교체 예산: 올림 후 삭제 중·준비되지 않은 노드 수 차감
```
### PDB(PodDisruptionBudget) 통합
PDB는 정상 복제본 수에 따라 자발적인 eviction을 제한하지만 복제본을 생성하거나 애플리케이션 가용성을 보장하지 않습니다. 아래 `minAvailable: 2`는 축출 전에 충분한 정상 Pod가 필요합니다. 인스턴스 손실이나 강제 종료 기한은 여전히 영향을 줄 수 있습니다.
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb
namespace: default
spec:
minAvailable: 2
selector:
matchLabels:
app: my-app
```
## 통합
Karpenter는 다양한 Kubernetes 및 클라우드 서비스와 통합됩니다.
### Kubernetes 통합
#### 1. Pod Topology Spread Constraints
Karpenter는 Pod Topology Spread Constraints를 고려하여 노드를 프로비저닝합니다:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
namespace: default
spec:
replicas: 10
template:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web-server
containers:
- name: web-server
image: nginx:1.30.4
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
metadata:
labels:
app: web-server
selector:
matchLabels:
app: web-server
```
#### 2. Pod Affinity/Anti-Affinity
Karpenter는 Pod Affinity 및 Anti-Affinity 규칙을 고려합니다:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
namespace: default
spec:
replicas: 10
template:
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-server
topologyKey: kubernetes.io/hostname
containers:
- name: web-server
image: nginx:1.30.4
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
metadata:
labels:
app: web-server
selector:
matchLabels:
app: web-server
```
#### 3. 테인트 및 허용 오차
GPU 예제에는 호환 가속 AMI·NVIDIA 드라이버와 GPU taint를 허용해 nvidia.com/gpu를 광고하는 디바이스 플러그인 DaemonSet이 필요합니다. BusyBox Pod는 GPU 예약만 보여주며 CUDA를 실행·벤치마크하지 않습니다. 실제 GPU 애플리케이션 이미지는 별도 검증하세요.
Karpenter는 테인트 및 허용 오차를 고려하여 노드를 프로비저닝합니다:
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: gpu
spec:
template:
spec:
requirements:
- key: node.kubernetes.io/instance-type
operator: In
values:
- g4dn.xlarge
- g4dn.2xlarge
taints:
- key: nvidia.com/gpu
value: 'true'
effect: NoSchedule
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpu-app
namespace: default
spec:
replicas: 3
template:
spec:
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
nodeSelector:
karpenter.sh/nodepool: gpu
containers:
- name: gpu-allocation-demo
image: busybox:1.37.0
command:
- sh
- -c
- sleep 3600
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
memory: 128Mi
nvidia.com/gpu: 1
metadata:
labels:
app: gpu-app
selector:
matchLabels:
app: gpu-app
```
### AWS 통합
#### 1. EC2 Spot 인스턴스
Karpenter는 EC2 Spot 인스턴스를 지원하여 비용을 최적화합니다:
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: spot
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: spot
spec:
role: KarpenterNodeRole-my-cluster
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
```
#### 2. EC2 인스턴스 프로필
role과 instanceProfile은 상호 배타적입니다. role을 사용하면 Karpenter가 인스턴스 프로필을 관리하므로 해당 IAM API 권한·통신 경로가 필요합니다. IAM 엔드포인트로 갈 경로가 없다면 미리 만든 instanceProfile을 사용하세요. IAM에는 PrivateLink 엔드포인트가 없습니다. 컨트롤러는 노드 역할에 대한 PassRole이 여전히 필요하며 EC2 자격 증명 보유와 EKS 노드 접근은 별개입니다.
Karpenter는 EC2 인스턴스 프로필을 사용하여 노드에 IAM 권한을 부여합니다:
```yaml
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
instanceProfile: KarpenterNodeInstanceProfile-my-cluster
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
```
#### 3. Launch Template 대체 (EC2NodeClass)
Karpenter v1에서는 사용자가 만든 EC2 시작 템플릿을 직접 참조하는 `launchTemplate` 필드가 제거되었습니다. 대신 Karpenter가 `EC2NodeClass`의 `amiSelectorTerms`, `blockDeviceMappings`, `userData`, `metadataOptions`, `tags` 등을 바탕으로 시작 템플릿을 자동 생성·관리합니다. 기존 시작 템플릿에 담겨 있던 설정은 해당 `EC2NodeClass` 필드로 옮겨 표현하세요:
```yaml
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: node-config
spec:
role: KarpenterNodeRole-my-cluster
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
amiSelectorTerms:
- alias: al2023@latest
userData: '#!/bin/bash
echo "Hello from Karpenter node!"
'
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 100Gi
volumeType: gp3
deleteOnTermination: true
encrypted: true
metadataOptions:
httpEndpoint: enabled
httpProtocolIPv6: disabled
httpPutResponseHopLimit: 1
httpTokens: required
```
## Amazon EKS와의 통합
Karpenter AWS 공급자는 인증·네트워크·노드 접근·부트스트랩을 구성하면 EKS 관리형 컴퓨팅과 함께 EC2 용량을 제공할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-autoscaling-02-karpenter-3.html)
### EKS 클러스터 준비
모든 예시 이름을 일관되게 교체하세요. 워커에 적합한 서브넷·보안 그룹만 선택해야 하며 제어 플레인의 모든 서브넷에 태그를 붙이는 것은 안전한 검색 전략이 아닙니다. 노드 시작을 허용하기 전에 경로·보안 규칙·CNI IP 용량·필요한 사설 엔드포인트를 확인하세요.
#### 1. 클러스터 태그 설정
인프라 설정에서 승인된 워커 서브넷·보안 그룹에만 태그를 부여하세요. VPC 태그가 이들 리소스를 선택하는 것은 아닙니다. discovery 값은 예제의 `my-cluster`와 맞아야 하며 태그만으로 사설 라우팅·보안 규칙·여유 주소가 증명되지 않습니다. NodePool 적용 전에 실제 리소스와 경로를 확인하세요.
```bash
# Inspect resources that your infrastructure configuration has tagged for this cluster.
: "${CLUSTER_NAME:?Set the existing cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
aws ec2 describe-subnets --region "$AWS_REGION" \
--filters "Name=tag:karpenter.sh/discovery,Values=${CLUSTER_NAME}" \
--query 'Subnets[].{ID:SubnetId,AZ:AvailabilityZone,FreeIPs:AvailableIpAddressCount,PublicIPOnLaunch:MapPublicIpOnLaunch,VPC:VpcId}'
aws ec2 describe-security-groups --region "$AWS_REGION" \
--filters "Name=tag:karpenter.sh/discovery,Values=${CLUSTER_NAME}" \
--query 'SecurityGroups[].{ID:GroupId,VPC:VpcId,Name:GroupName}'
# Inspect explicit and main route-table associations for the intended subnets.
aws ec2 describe-route-tables --region "$AWS_REGION" \
--filters "Name=vpc-id,Values=$(aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" --query cluster.resourcesVpcConfig.vpcId --output text)"
```
#### 2. IAM 역할 설정
불완전한 수동 전체 허용 정책 대신 아래 버전별 컨트롤러 정책·인프라 참조를 사용하세요. 컨트롤러 인증(여기서는 IRSA), 노드 인증, VPC CNI·워크로드 인증은 별도입니다. 컨트롤러 신뢰는 클러스터 OIDC 공급자, `system:serviceaccount:karpenter:karpenter`, `aud: sts.amazonaws.com`에 맞추고 이 버전과 기능에 필요한 작업·리소스·조건만 허용해야 합니다. 노드 역할은 EKS 조인 권한이 필요하며 API 인증을 켠 경우 보통 `EC2_LINUX` access entry를 사용합니다. 아래 조회 명령은 IAM 역할이나 access entry를 생성하지 않습니다.
```bash
# Download a versioned reference for review; this does not create a CloudFormation stack.
KARPENTER_VERSION="1.14.1"
curl --fail --show-error --location \
"https://raw.githubusercontent.com/aws/karpenter-provider-aws/v${KARPENTER_VERSION}/website/content/en/preview/getting-started/getting-started-with-karpenter/cloudformation.yaml" \
--output karpenter-cloudformation-reference.yaml
# Inspect the existing role's attached and inline policies.
: "${CLUSTER_NAME:?Set the existing cluster name}"
aws iam list-attached-role-policies --role-name "KarpenterControllerRole-${CLUSTER_NAME}"
aws iam list-role-policies --role-name "KarpenterControllerRole-${CLUSTER_NAME}"
```
### EKS 클러스터에 Karpenter 설치
```bash
# Run only after the prerequisites and the current kubectl context are verified.
: "${CLUSTER_NAME:?Set the existing cluster name}"
: "${ACCOUNT_ID:?Set the AWS account ID}"
: "${CLUSTER_ENDPOINT:?Set the matching EKS endpoint}"
KARPENTER_VERSION="1.14.1"
helm install karpenter oci://public.ecr.aws/karpenter/karpenter \
--version "$KARPENTER_VERSION" \
--namespace karpenter --create-namespace \
--set-string 'serviceAccount.annotations.eks\.amazonaws\.com/role-arn'="arn:aws:iam::${ACCOUNT_ID}:role/KarpenterControllerRole-${CLUSTER_NAME}" \
--set-string settings.clusterName="$CLUSTER_NAME" \
--set-string settings.clusterEndpoint="$CLUSTER_ENDPOINT" \
--set-string settings.interruptionQueue="$CLUSTER_NAME" \
--wait --timeout 5m
```
### EKS 관리형 노드 그룹과 함께 사용
Karpenter는 EKS Managed Node Group과 공존할 수 있습니다. 아래 NodePool은 별도 EC2 노드를 프로비저닝하며 Managed Node Group을 관리하지 않습니다. 컨트롤러는 안정적인 부트스트랩 용량에 두세요. 기본 차트는 Karpenter 노드를 제외하고 서로 다른 호스트의 복제본2개를 요청합니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: managed-ng
spec:
template:
metadata:
labels:
managed-by: karpenter
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
- key: node.kubernetes.io/instance-type
operator: In
values:
- m5.large
- m5.xlarge
taints:
- key: managed-by
value: karpenter
effect: NoSchedule
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: managed-ng
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: managed-ng
spec:
role: KarpenterNodeRole-my-cluster
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
tags:
karpenter.sh/discovery: my-cluster
```
### EKS Fargate와 함께 사용
Fargate는 Kubernetes topologySpreadConstraints를 지원하지 않으며 affinity/anti-affinity 규칙도 적용하지 않습니다. 기본 차트의 EC2 배치 제약이 Fargate AZ 분리를 보장한다고 가정하지 말고 프로파일·서브넷 배치를 별도로 설계·확인하세요.
좁게 선택한 Fargate 프로파일에 컨트롤러 Pod를 두고 Karpenter로 EC2 워커를 제공할 수 있습니다. Karpenter가 Fargate 용량·프로파일을 만들거나 관리하지는 않습니다. `default`·`kube-system` 전체 대신 `karpenter` 네임스페이스와 컨트롤러 라벨을 선택하세요. JSON 선택기 형식은 `[{"namespace":"karpenter","labels":{"app.kubernetes.io/name":"karpenter"}}]`입니다. 프로파일에는 별도 Pod 실행 역할·사설 서브넷이 필요하고 컨트롤러 AWS 권한에는 IRSA가 필요합니다(EKS Pod Identity는 Fargate 미지원). 아래 명령은 기존 프로파일을 조회합니다.
```bash
# Inspect an existing, narrowly selected controller Fargate profile.
: "${CLUSTER_NAME:?Set the existing cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
aws eks describe-fargate-profile --region "$AWS_REGION" \
--cluster-name "$CLUSTER_NAME" --fargate-profile-name karpenter-controller
```
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: ec2
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: ec2
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: ec2
spec:
role: KarpenterNodeRole-my-cluster
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
```
### AZ 장애 대응: Amazon ARC Zonal Shift 통합 (2026년 5월)
Karpenter는 활성화된 EKS ARC zonal-shift 리소스와 연동할 수 있습니다. 수동 zonal shift나 설정된 autoshift가 활성화되면 해당 AZ에 새 용량을 만들지 않습니다. 모든 AZ 장애를 독립적으로 감지하는 기능은 아니며 특정 AZ에 고정된 Pod·PV 요구사항을 무시하지도 않습니다.
shift가 활성화되면 해당 AZ의 자발적 교체를 중단하고, Pod를 해당 AZ로 보내야 하는 정상 AZ의 교체도 막습니다. 모든 정상 AZ의 자발적 교체를 무조건 일괄 중단하는 것은 아닙니다. EKS/ARC 사전 조건, `eks:DescribeCluster`·ARC 권한, `settings.enableZonalShift: true`(환경 옵션 `ENABLE_ZONAL_SHIFT`)를 구성하세요. Autoshift는 별도 선택·연습 설정이 필요합니다. 별도 ARC CRD는 필요하지 않으며 shift 종료 후 일반 동작을 재개합니다.
### EKS 비용 최적화
Karpenter를 사용하여 EKS 클러스터의 비용을 최적화할 수 있습니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-autoscaling-02-karpenter-4.html)
#### 1. 스팟 인스턴스 사용
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
- key: kubernetes.io/arch
operator: In
values:
- amd64
- arm64
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: spot
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: spot
spec:
role: KarpenterNodeRole-my-cluster
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
```
#### 2. 다양한 인스턴스 유형 사용
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: flexible
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
- spot
- key: kubernetes.io/arch
operator: In
values:
- amd64
- arm64
- key: node.kubernetes.io/instance-type
operator: In
values:
- m5.large
- m5.xlarge
- m5.2xlarge
- m6g.large
- m6g.xlarge
- m6g.2xlarge
- c5.large
- c5.xlarge
- c5.2xlarge
- c6g.large
- c6g.xlarge
- c6g.2xlarge
- r5.large
- r5.xlarge
- r5.2xlarge
- r6g.large
- r6g.xlarge
- r6g.2xlarge
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
```
#### 3. 노드 통합 활성화
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
```
## 모범 사례

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-autoscaling-02-karpenter-5.html)
### 성능 최적화
1. **적절한 인스턴스 유형 선택**: 워크로드에 적합한 인스턴스 유형 선택
2. **다양한 인스턴스 유형 허용**: 가용성 및 비용 최적화를 위해 다양한 인스턴스 유형 허용
3. **적절한 TTL 설정**: 워크로드 패턴에 맞는 TTL 설정
4. **노드 통합 활성화**: 리소스 활용도 최적화를 위한 노드 통합 활성화
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: optimized
spec:
template:
spec:
requirements:
- key: node.kubernetes.io/instance-type
operator: In
values:
- m5.large
- m5.xlarge
- m5.2xlarge
- c5.large
- c5.xlarge
- c5.2xlarge
- r5.large
- r5.xlarge
- r5.2xlarge
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
expireAfter: 720h
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
```
### 비용 최적화
1. **스팟 인스턴스 활용**: 비용 절감을 위한 스팟 인스턴스 사용
2. **적절한 인스턴스 크기 선택**: 워크로드에 적합한 인스턴스 크기 선택
3. **빈 노드 제거 평가**: 적합한 워커 NodePool은0개가 될 수 있지만 컨트롤러·부트스트랩 용량과 다른 클러스터 비용은 남습니다.
4. **노드 갱신 계획**: 만료는 현재 제약에 따라 용량을 교체하며 새 인스턴스 유형을 자동 선택하거나 고정 AMI를 패치하지는 않습니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: cost-optimized
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
expireAfter: 168h
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
```
### 가용성 향상
1. **다중 가용 영역 사용**: 여러 가용 영역에 걸쳐 노드 배포
2. **온디맨드 및 스팟 인스턴스 혼합**: 가용성과 비용 균형 유지
3. **적절한 PDB 설정**: 실제 정상 복제본 범위에서 자발적 eviction을 제한하고 이중화·복구 설계와 함께 사용합니다.
4. **인터럽션 처리 설정·검증**: 무중단을 가정하지 말고 알림 경로·종료 기한·대체 용량을 확인합니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: high-availability
spec:
template:
spec:
requirements:
- key: topology.kubernetes.io/zone
operator: In
values:
- us-west-2a
- us-west-2b
- us-west-2c
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
- spot
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
expireAfter: 720h
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 60s
```
## 문제 해결
### 일반적인 문제
#### 1. 노드 프로비저닝 실패
**증상**: 파드가 Pending 상태로 유지되고 노드가 프로비저닝되지 않음
**해결 방법**:
- Karpenter 로그 확인
- IAM 권한 확인
- NodePool 구성 확인
```bash
# Karpenter 로그 확인
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter -c controller
# NodePool 상태 확인
kubectl describe nodepool
# 파드 이벤트 확인
kubectl describe pod
```
#### 2. 노드 제거 문제
**증상**: 노드가 예상대로 제거되지 않음
**해결 방법**:
- TTL 설정 확인
- 노드 통합 설정 확인
- 파드 드레이닝 상태 확인
```bash
# 노드 상태 확인
kubectl describe node
# 노드 레이블 확인
kubectl get node --show-labels
# Karpenter 로그 확인
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter -c controller --since=30m
```
#### 3. 인스턴스 유형 선택 문제
**증상**: 예상하지 않은 인스턴스 유형이 프로비저닝됨
**해결 방법**:
- NodePool 요구 사항 확인
- 파드 리소스 요청 확인
- 가용 영역 제약 조건 확인
```bash
# NodePool 요구 사항 확인
kubectl get nodepool -o yaml
# 파드 리소스 요청 확인
kubectl describe pod
# 노드 정보 확인
kubectl describe node
```
### 디버깅 도구
```bash
# Karpenter 버전 확인
kubectl get deployment -n karpenter karpenter -o jsonpath="{.spec.template.spec.containers[0].image}"
# Karpenter 로그 확인
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter -c controller
# NodePool 목록 확인
kubectl get nodepools
# EC2NodeClass 목록 확인
kubectl get ec2nodeclasses
# 이벤트 확인
kubectl get events --sort-by='.lastTimestamp'
# Inspect the installed chart version and values before any optional log-level change.
helm list --namespace karpenter --filter '^karpenter$'
helm get values karpenter --namespace karpenter
kubectl get nodeclaims -o wide
```
## 결론
Karpenter는 워크로드·인프라 제약에 따라 노드 프로비저닝과 수명주기를 자동화합니다. 용량 관리를 개선할 수 있지만 가용성·성능·비용 결과는 워크로드별 검증이 필요합니다.
이 문서에서는 Karpenter의 기본 개념, 설치 방법, NodePool 및 EC2NodeClass 구성, 인터럽션 처리, 다양한 통합, Amazon EKS와의 통합, 모범 사례 및 문제 해결에 대해 살펴보았습니다.
Karpenter를 사용하면 클러스터 관리를 간소화하고, 리소스 활용도를 최적화하며, 비용을 절감할 수 있습니다. 특히 Amazon EKS와 같은 클라우드 관리형 Kubernetes 환경에서 Karpenter의 이점을 최대한 활용할 수 있습니다.
### 다음 단계
- Karpenter를 사용한 비용 최적화 전략 구현
- 다양한 워크로드 유형에 맞는 NodePool 구성
- 하이브리드 클러스터 아키텍처 설계
- Karpenter와 다른 Kubernetes 도구와의 통합
- 고급 노드 수명 주기 관리 전략 개발
## 참고 자료
- [Karpenter 공식 문서](https://karpenter.sh/)
- [Karpenter AWS 공급자 저장소](https://github.com/aws/karpenter-provider-aws)
- [Amazon EKS 워크숍 - Karpenter](https://www.eksworkshop.com/docs/autoscaling/compute/karpenter/)
- [AWS 블로그 - Karpenter](https://aws.amazon.com/blogs/aws/introducing-karpenter-an-open-source-high-performance-kubernetes-cluster-autoscaler/)
- [Karpenter 모범 사례](https://aws.github.io/aws-eks-best-practices/karpenter/)
- [Karpenter GitHub Releases](https://github.com/aws/karpenter-provider-aws/releases)
- [AWS What's New - Karpenter ARC Zonal Shift 지원](https://aws.amazon.com/about-aws/whats-new/2026/05/karpenter-arc-zonal-shift/)
- [AWS What's New - Amazon EKS Managed Node Group Warm Pool 지원](https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-eks-managed-node-groups-ec2-warm-pools/)
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/autoscaling/06-karpenter-quiz)를 풀어보세요.
이번 수정에서 확인한 공식 자료: [Compatibility](https://karpenter.sh/v1.14/upgrading/compatibility/), [NodePool](https://karpenter.sh/v1.14/concepts/nodepools/), [EC2NodeClass](https://karpenter.sh/v1.14/concepts/nodeclasses/), [Disruption](https://karpenter.sh/v1.14/concepts/disruption/), [Support policy](https://github.com/aws/karpenter-provider-aws/blob/main/SUPPORT.md), [Pinned Helm values](https://github.com/aws/karpenter-provider-aws/blob/v1.14.1/charts/karpenter/values.yaml), [Readiness taints](https://github.com/kubernetes-sigs/karpenter/commit/05431485c90c76a3a662b678a46c1a8da330038d), [Pinned DRA option](https://github.com/kubernetes-sigs/karpenter/blob/6e7eab7a0f48/pkg/operator/options/options.go), [EKS node IAM](https://docs.aws.amazon.com/eks/latest/userguide/create-node-role.html), [Fargate profiles](https://docs.aws.amazon.com/eks/latest/userguide/fargate-profile.html).
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/autoscaling/03-knative
----------------------------------------
# Knative
> **예제 버전**: Serving/Eventing/Kourier 1.23.0, Operator 1.23.1. 설치 전 Kubernetes/EKS 호환성을 확인하세요.
> **마지막 업데이트**: 2026년 9월 11일
< [이전: Karpenter](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/02-karpenter.md) | 다음: 없음 >
## 목차
* [개요 및 학습 목표](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/03-knative.md#개요-및-학습-목표)
* [Knative 아키텍처](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/03-knative.md#knative-아키텍처)
* [EKS 설치 및 구성](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/03-knative.md#eks-설치-및-구성)
* [Knative Serving 심화](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/03-knative.md#knative-serving-심화)
* [Knative Eventing 심화](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/03-knative.md#knative-eventing-심화)
* [KEDA와 Knative 비교](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/03-knative.md#keda와-knative-비교)
* [프로덕션 운영](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/03-knative.md#프로덕션-운영)
* [모범 사례](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/03-knative.md#모범-사례)
* [참고 문서](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/03-knative.md#참고-문서)
***
## 개요 및 학습 목표
### Knative란?
Knative는 Kubernetes 위에서 서버리스(Serverless) 워크로드를 배포, 실행, 관리하기 위한 오픈 소스 플랫폼입니다. 2022년 CNCF Incubating 프로젝트로 승인되었으며, 2025년 9월 11일 **CNCF Graduated** 프로젝트로 졸업했습니다. 이 프로젝트 성숙도 분류가 개별 배포의 운영 준비 상태를 보장하지는 않습니다.
Knative는 개발자가 컨테이너 기반 애플리케이션을 서버리스 방식으로 운영할 수 있도록 두 가지 핵심 컴포넌트를 제공합니다:
* **Knative Serving**: HTTP 요청 기반의 서버리스 워크로드 배포 및 오토스케일링 (Scale-to-Zero 포함)
* **Knative Eventing**: 이벤트 드리븐 아키텍처를 위한 이벤트 소싱, 라우팅, 필터링 프레임워크
### Serverless on Kubernetes
전통적인 서버리스 플랫폼(AWS Lambda, Google Cloud Functions)은 특정 클라우드 벤더에 종속되는 반면, Knative는 Kubernetes가 실행되는 어디에서든 서버리스 경험을 제공합니다. 이를 통해 다음을 달성할 수 있습니다:
1. **이식 가능한 API**: 지원 Kubernetes 환경에서 공통 API를 사용하며 클라우드 인증·스토리지·네트워크 의존성은 별도 검증
2. **컨테이너 기반 실행**: PORT·HTTP·시작·준비 상태 등 Knative Serving 런타임 계약을 충족하는 이미지 사용
3. **Kubernetes 생태계 활용**: 기존 Kubernetes 도구, 모니터링, 보안 정책을 그대로 사용
4. **Scale-to-Zero**: 트래픽이 없을 때 파드를 0으로 축소하여 리소스 비용 절감
### Knative Serving vs Eventing
| 구분 | Knative Serving | Knative Eventing |
| ----------------- | --------------------------------------- | ---------------------------------------------- |
| **목적** | HTTP 요청 기반 서버리스 워크로드 | 이벤트 드리븐 아키텍처 |
| **트리거** | HTTP 요청 | CloudEvents (Kafka, SQS, API Server 등) |
| **스케일링** | 동시 요청 수 기반 자동 스케일링 | 이벤트 소스에 따라 다름 |
| **Scale-to-Zero** | 네이티브 지원 | 소비자 워크로드에 따라 다름 |
| **주요 사용처** | API 서버, 웹 앱, ML 추론 | 비동기 처리, 데이터 파이프라인, 워크플로 |
| **리소스 모델** | Service, Configuration, Revision, Route | Source, Broker, Trigger, Channel, Subscription |
### Knative vs AWS Lambda/Fargate 비교
| 기능 | EKS의 Knative Serving | AWS Lambda(표준 컴퓨팅) | AWS Fargate |
|---|---|---|---|
| 런타임 | Serving 런타임 계약을 충족하는 컨테이너 | 지원 Lambda 런타임 또는 런타임 API를 구현한 이미지 | 지원 ECS task/EKS Pod; 플랫폼 제약 적용 |
| 요청·실행 수명 | 요청 타임아웃 설정 가능; 기본300초, 최대 허용값은 별도 설정 전600초 | 일반 함수 타임아웃 최대900초 | Lambda 호출 제한이 아닌 task/Pod 수명주기에 따름 |
| Scale-to-Zero | KPA가 유휴 Revision의0개 축소 지원 | 온디맨드 실행 | ECS desired task·Kubernetes replica를0으로 설정 가능; 활성화는 적절한 컨트롤러·메트릭 필요 |
| 콜드 스타트 | 기본 복제본·이미지·시작·준비 상태 조정 | 런타임별 최적화·Provisioned Concurrency | task/Pod 시작·이미지·네트워크 설정에 따라 다르며 고정 시간 비교 불가 |
| 메모리 | 노드 allocatable·컨테이너 요청·사이드카에 따라 다름 |128–10,240MB 설정 | EKS Fargate 슬롯 최대120GB; 플랫폼 오버헤드·CPU/메모리 조합 제약 적용 |
| GPU | 적합한 노드·디바이스 플러그인·리소스 요청·PodSpec 기능 활성화 필요 | 표준 Lambda 컴퓨팅에 GPU 없음 | Fargate는 GPU 미지원 |
| 네트워킹 | Kubernetes/CNI·게이트웨이 구성 | 고객 VPC 연결은 선택 사항 | VPC 네트워킹; EKS Fargate 플랫폼 제약 적용 |
| 이식성 | Kubernetes/Knative API 사용; 클라우드 인증·스토리지 통합은 별도 | AWS 런타임·서비스 API | ECS/EKS 통합과 지원 플랫폼 API |
| 이벤트 입력 | HTTP/CloudEvents·설정한 어댑터 | 지원 AWS 이벤트 통합 | 애플리케이션·컨트롤러 통합; 범용 이벤트 라우터는 아님 |
| 로컬 검증 | 로컬 Kubernetes가 도움이 되지만 클라우드 동작은 별도 검증 | 로컬 도구·에뮬레이터는 선택이며 재현 범위에 한계 | 컨테이너 로직은 로컬 시험 가능, 플랫폼 동작은 다름 |
| 관측성 | 메트릭·로그·추적 내보내기 구성 | CloudWatch·지원 추적 통합 | 지원 AWS/OpenTelemetry 수집 구성 |
| 비용 기준 | 할당된 클러스터·스토리지·로드밸런서·보조 리소스 | 요청·실행시간·선택한 기능 | 할당 task/Pod CPU·메모리와 관련 리소스 |
Lambda 열은 표준 컴퓨팅 기준입니다. Lambda Managed Instances의 지원되는 비동기·이벤트 소스 호출은 서비스별 예외를 두고 최대90분까지 허용할 수 있으며 운영 제약이 다릅니다. Pod가0개가 되어도 EC2 노드·영구 스토리지·로드밸런서 비용이 자동 제거되지는 않습니다.
### 학습 목표
이 문서를 통해 다음을 학습합니다:
1. Knative Serving과 Eventing의 아키텍처 및 핵심 컴포넌트 이해
2. Amazon EKS 클러스터에 Knative를 설치하고 구성하는 방법
3. Knative Service를 사용한 서버리스 워크로드 배포 및 트래픽 관리
4. Knative Eventing을 사용한 이벤트 드리븐 아키텍처 구현
5. KEDA와 Knative의 차이점과 적절한 사용 시나리오
6. 프로덕션 환경에서의 운영, 모니터링, 문제 해결 방법
***
## Knative 아키텍처
### Serving 아키텍처
Knative Serving은 서버리스 워크로드의 배포, 스케일링, 네트워킹을 관리하는 핵심 컴포넌트입니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-autoscaling-03-knative-0.html)
#### 핵심 컴포넌트 설명
**1. Activator**
* Scale-to-Zero 상태에서 들어오는 첫 번째 요청을 버퍼링
* Autoscaler에 스케일업을 요청하고, 파드가 준비되면 요청을 전달
* 버스트 트래픽 시 요청 큐잉을 통한 과부하 방지
**2. Autoscaler**
* **KPA (Knative Pod Autoscaler)**: Knative 기본 오토스케일러. 동시 요청 수(concurrency) 또는 RPS(requests per second) 기반 스케일링. Scale-to-Zero 지원
* **HPA (Horizontal Pod Autoscaler)**: Kubernetes 기본 HPA 사용. CPU/메모리 기반 스케일링 가능하나 Scale-to-Zero 미지원
**3. Queue Proxy**
* 모든 Knative 파드에 사이드카로 주입되는 프록시 컨테이너
* 요청 큐잉, 동시성 제한(concurrency enforcement), 메트릭 수집 수행
* Autoscaler에 실시간 동시성 메트릭 보고
* 헬스체크 프로브 처리
**4. Controller**
* Knative Service, Configuration, Revision, Route 리소스의 생명주기 관리
* Kubernetes Deployment, Service, Ingress 등 하위 리소스 생성 및 동기화
**5. Webhook**
* Knative 리소스의 생성/수정 시 유효성 검사(Validation) 및 기본값 설정(Defaulting)
### Eventing 아키텍처
Knative Eventing은 느슨하게 결합된 이벤트 드리븐 아키텍처를 제공합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-autoscaling-03-knative-1.html)
#### Broker/Trigger 패턴
* **Broker**: 이벤트를 수신하고 등록된 Trigger에 따라 적절한 소비자에게 라우팅하는 이벤트 허브
* **Trigger**: Broker에 등록되는 이벤트 필터. CloudEvents 속성(type, source 등)으로 필터링하여 특정 서비스로 전달
#### Channel/Subscription 패턴
참조한 구독 서비스·reply Channel을 먼저 생성해야 합니다. reply는 구독자가 반환한 유효한 CloudEvent를 전달하며 빈204 승인 응답이 reply 이벤트를 만들지는 않습니다. Kafka 영속성도 KafkaChannel 종류만이 아니라 복제·보존·승인·장애 처리에 달려 있습니다.
* **Channel**: 이벤트를 임시 저장하고 전달하는 메시징 채널 (InMemoryChannel, KafkaChannel 등)
* **Subscription**: Channel의 이벤트를 특정 서비스로 구독하여 전달
#### Event Source
* **ApiServerSource**: Kubernetes API Server의 이벤트(리소스 생성/수정/삭제)를 CloudEvents로 변환
* **SinkBinding**: 기존 Kubernetes 워크로드에 이벤트 전송 기능을 주입
* **KafkaSource**: Apache Kafka 토픽의 메시지를 CloudEvents로 변환
* **SQSSource**: Amazon SQS 큐의 메시지를 CloudEvents로 변환
***
## EKS 설치 및 구성
설치 예제는 격리된 테스트 환경용이며 자리표시자를 포함합니다. LoadBalancer를 포함한 Serving을 적용하면 설치된 컨트롤러가 AWS 리소스를 만들 수 있으며 이번 감사에서는 실행하지 않았습니다. Operator 소유 ConfigMap·Service·워크로드는 KnativeServing/KnativeEventing으로 설정합니다. 병합 패치를 만들 때 기존 설정, 특히 배열 필드를 보존하세요. 뒤의 예제에 필요한 네임스페이스·ServiceAccount·Secret·이미지·Kafka/SQS 리소스·IAM 역할을 먼저 준비해야 하며 스키마 검사가 운영 준비 상태를 입증하지는 않습니다.
### 사전 요구 사항
```bash
kubectl config current-context
kubectl version -o yaml
kubectl get nodes
```
### Knative Operator를 사용한 설치
이 예제는 기존 테스트 클러스터에 Operator1.23.1과 Serving/Eventing/Kourier1.23.0을 새로 설치합니다. 기존1.16 설치를 한 번에 업그레이드하는 절차가 아닙니다. 지원되는 EKS 버전과 Knative 업그레이드 지침을 확인하세요.
```bash
# Fresh installation on the intended test cluster; review versioned upgrade guidance for existing installs.
kubectl config current-context
kubectl apply --server-side -f https://github.com/knative/operator/releases/download/knative-v1.23.1/operator.yaml
kubectl wait --for=condition=Established crd/knativeservings.operator.knative.dev crd/knativeeventings.operator.knative.dev --timeout=120s
kubectl wait --for=condition=Available deployment/knative-operator deployment/operator-webhook -n knative-operator --timeout=300s
```
```yaml
apiVersion: operator.knative.dev/v1beta1
kind: KnativeServing
metadata:
name: knative-serving
namespace: knative-serving
spec:
version: 1.23.0
ingress:
kourier:
enabled: true
config:
network:
ingress-class: kourier.ingress.networking.knative.dev
autoscaler:
pod-autoscaler-class: kpa.autoscaling.knative.dev
container-concurrency-target-percentage: '70'
enable-scale-to-zero: 'true'
defaults:
revision-timeout-seconds: '300'
max-revision-timeout-seconds: '600'
deployment:
queue-sidecar-cpu-request: 25m
queue-sidecar-memory-request: 400Mi
queue-sidecar-memory-limit: 800Mi
services:
- name: kourier
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: external
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
```
```bash
kubectl create namespace knative-serving --dry-run=client -o yaml | kubectl apply -f -
kubectl create namespace knative-demo --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f knative-serving.yaml
kubectl wait --for=condition=Ready knativeserving/knative-serving -n knative-serving --timeout=600s
kubectl get deployments,pods,services -n knative-serving
```
```yaml
apiVersion: operator.knative.dev/v1beta1
kind: KnativeEventing
metadata:
name: knative-eventing
namespace: knative-eventing
spec:
version: 1.23.0
defaultBrokerClass: MTChannelBasedBroker
config:
default-ch-webhook:
default-ch-config: "clusterDefault:\n apiVersion: messaging.knative.dev/v1\n\
\ kind: InMemoryChannel\n"
sinkBindingSelectionMode: inclusion
```
```bash
kubectl create namespace knative-eventing --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f knative-eventing.yaml
kubectl wait --for=condition=Ready knativeeventing/knative-eventing -n knative-eventing --timeout=600s
kubectl get deployments,pods -n knative-eventing
kubectl get crd inmemorychannels.messaging.knative.dev integrationsources.sources.knative.dev
```
### Kourier (경량 Ingress) 설치
Kourier는 Knative용 Envoy 기반 네트워킹 구현입니다. 지원 기능이 워크로드에 적합한지 확인하고 리소스·지연 이점은 측정해야 합니다. Operator 경로에서는 gateway Service가 `knative-serving`에 생성됩니다. 독립 수동 설치의 `kourier-system` 경로를 이 구성에 중복 적용하지 마세요.
```bash
# Operator-managed Kourier uses the KnativeServing namespace.
kubectl get deployment net-kourier-controller 3scale-kourier-gateway -n knative-serving
kubectl get service kourier -n knative-serving -o yaml
```
```bash
kubectl get service kourier -n knative-serving -o jsonpath='{.spec.loadBalancerClass}{"\n"}{.status.loadBalancer.ingress}{"\n"}'
```
### DNS 구성
Knative Service에는 실제 Route/DomainMapping 호스트와 일치하는 DNS가 필요합니다. 아래 예제는 AWS Load Balancer Controller가 관리하는 NLB를 가정하며 해당 컨트롤러·IAM·서브넷을 먼저 준비해야 합니다. Service 주석은 ALB를 만드는 설정이 아니고 EKS Auto Mode도 별도 관리 경로입니다.
#### Magic DNS (sslip.io) - 개발/테스트 환경용
sslip.io는 지원하는 IP 포함 이름을 해석합니다. AWS 로드밸런서 호스트명과 바뀔 수 있는 IP를 운영용 고정 DNS로 취급하지 마세요. 이 Operator 예제는 명시적 도메인을 사용하며 standalone default-domain 도우미로 Operator 소유 설정을 중복 변경하지 않습니다.
```bash
# Inspect the address type; this Operator workflow uses an explicitly configured domain.
kubectl get service kourier -n knative-serving -o jsonpath='{.status.loadBalancer.ingress}'
kubectl get ksvc -n knative-demo
```
#### Real DNS (Route53) - 프로덕션 환경용
제어하는 도메인·호스팅 영역과 생성된 변경 내용을 확인한 뒤 적용하세요. 아래 AWS 명령은 실행 시 해당 DNS 영역을 변경하며 이번 감사에서 실행하지 않았습니다. Terraform 코드는 별도의 구성 조각이므로 필요한 provider·조회 리소스를 정의하고 DNS 소유자를 하나로 정해야 합니다.
```bash
# Review the intended zone/domain and wait for the NLB hostname before preparing a DNS change.
set -euo pipefail
export KOURIER_HOST
KOURIER_HOST=$(kubectl get service kourier -n knative-serving -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
: "${KOURIER_HOST:?Wait for the load-balancer hostname}"
export KNATIVE_DOMAIN="knative.example.com"
export HOSTED_ZONE_ID="REPLACE_WITH_HOSTED_ZONE_ID"
python3 - <<'PYDNS'
import json, os
from pathlib import Path
host = os.environ["KOURIER_HOST"].strip()
if not host or any(c.isspace() for c in host):
raise SystemExit("Invalid load-balancer hostname")
change = {"Changes": [{"Action": "UPSERT", "ResourceRecordSet": {
"Name": "*." + os.environ["KNATIVE_DOMAIN"], "Type": "CNAME", "TTL": 300,
"ResourceRecords": [{"Value": host}]
}}]}
Path("knative-dns-change.json").write_text(json.dumps(change, indent=2))
patch = {"spec": {"config": {"domain": {os.environ["KNATIVE_DOMAIN"]: ""}}}}
Path("knative-domain.patch.json").write_text(json.dumps(patch))
PYDNS
# These commands change the selected DNS zone and Operator configuration when run.
aws route53 change-resource-record-sets --hosted-zone-id "$HOSTED_ZONE_ID" --change-batch file://knative-dns-change.json
kubectl patch knativeserving knative-serving -n knative-serving --type merge --patch-file knative-domain.patch.json
```
```hcl
# Provider 인증과 기존 hosted zone은 별도로 구성합니다.
data "kubernetes_service_v1" "kourier" {
metadata {
name = "kourier"
namespace = "knative-serving"
}
}
variable "hosted_zone_id" {
type = string
}
resource "aws_route53_record" "knative_wildcard" {
zone_id = var.hosted_zone_id
name = "*.knative.example.com"
type = "CNAME"
ttl = 300
records = [data.kubernetes_service_v1.kourier.status[0].load_balancer[0].ingress[0].hostname]
}
```
#### ExternalDNS 연동
ExternalDNS의 `knative-serving` 소스·DNS 권한·도메인 필터가 필요합니다. 주석만으로 새로운 Knative Route가 생기지는 않으므로 실제 Service URL 또는 DomainMapping 호스트와 일치시켜야 합니다. 같은 레코드를 Terraform·수동 명령과 동시에 관리하지 마세요.
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: my-app
annotations:
external-dns.alpha.kubernetes.io/hostname: my-app.knative-demo.knative.example.com
namespace: knative-demo
spec:
template:
spec:
containers:
- image: my-app:latest
```
### Cert-manager TLS 연동
Serving1.23 컨트롤러에 cert-manager 통합이 포함되어 있으므로 보관된 net-certmanager 저장소의 별도 릴리스를 설치하지 않습니다. 호환 cert-manager와 Route53 DNS01용 별도 IAM 인증·권한을 먼저 준비하세요. 처음에는 staging 발급자로 검증하며 staging 인증서는 공개적으로 신뢰되지 않습니다. 발급·갱신을 검증한 뒤 운영 발급자로 전환하세요.
```yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: REPLACE_WITH_CERTIFICATE_CONTACT_EMAIL
privateKeySecretRef:
name: letsencrypt-staging-key
solvers:
- dns01:
route53:
region: us-west-2
hostedZoneID: REPLACE_WITH_HOSTED_ZONE_ID
```
위 발급자를 `cluster-issuer.yaml`, 다음 Operator 병합 패치를 `serving-tls.patch.yaml`로 저장하세요.
```yaml
spec:
config:
network:
certificate-class: cert-manager.certificate.networking.knative.dev
external-domain-tls: Enabled
http-protocol: Redirected
certmanager:
issuerRef: |
group: cert-manager.io
kind: ClusterIssuer
name: letsencrypt-staging
```
```bash
# cert-manager and its Route 53 identity must already be configured.
kubectl apply -f cluster-issuer.yaml
kubectl wait --for=condition=Ready clusterissuer/letsencrypt-staging --timeout=180s
kubectl patch knativeserving knative-serving -n knative-serving --type merge --patch-file serving-tls.patch.yaml
# The integration starts in the Serving controller after the setting is effective.
kubectl get configmap config-network -n knative-serving -o yaml
kubectl rollout restart deployment/controller -n knative-serving
kubectl rollout status deployment/controller -n knative-serving --timeout=300s
```
### HPA vs KPA 오토스케일러 선택
| 기준 | KPA (Knative Pod Autoscaler) | HPA (Horizontal Pod Autoscaler) |
| ----------------- | ---------------------------- | ------------------------------- |
| **Scale-to-Zero** | 지원 | 미지원 (최소 1 파드) |
| **메트릭** | 동시 요청 수, RPS | CPU, 메모리, 커스텀 메트릭 |
| **반응 속도** | 빠름 (초 단위) | 보통 (15-30초) |
| **안정 구간** | 60초 (설정 가능) | 5분 (기본) |
| **사용 사례** | HTTP 워크로드, Scale-to-Zero 필요 | CPU/메모리 바운드 워크로드 |
```yaml
spec:
additionalManifests:
- URL: https://github.com/knative/serving/releases/download/knative-v1.23.0/serving-hpa.yaml
config:
autoscaler:
pod-autoscaler-class: kpa.autoscaling.knative.dev
stable-window: 60s
panic-window-percentage: '10'
panic-threshold-percentage: '200'
scale-to-zero-grace-period: 30s
scale-to-zero-pod-retention-period: 0s
target-burst-capacity: '211'
requests-per-second-target-default: '200'
container-concurrency-target-default: '100'
```
`serving-autoscaler.patch.yaml`로 저장해 기존 Operator 리소스에 병합합니다. 기존 `additionalManifests`가 있다면 함께 보존하세요.
```bash
kubectl patch knativeserving knative-serving -n knative-serving --type merge --patch-file serving-autoscaler.patch.yaml
kubectl get deployment autoscaler-hpa -n knative-serving
```
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: kpa-service
namespace: knative-demo
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/class: kpa.autoscaling.knative.dev
autoscaling.knative.dev/metric: concurrency
autoscaling.knative.dev/target: '100'
spec:
containers:
- image: my-app:latest
---
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: hpa-service
namespace: knative-demo
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/class: hpa.autoscaling.knative.dev
autoscaling.knative.dev/metric: cpu
autoscaling.knative.dev/target: '70'
spec:
containers:
- image: my-app:latest
resources:
requests:
cpu: 500m
limits:
cpu: '1'
```
***
## Knative Serving 심화
### 리소스 모델
Knative Serving의 네 가지 핵심 리소스는 다음과 같이 연결됩니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-autoscaling-03-knative-2.html)
* **Service**: 전체 서버리스 워크로드를 정의하는 최상위 리소스. Configuration과 Route를 자동으로 관리
* **Configuration**: 워크로드 템플릿을 정의하며 해당 템플릿 변경이 새 Revision을 생성합니다. 메타데이터·트래픽 변경마다 생성되는 것은 아닙니다.
* **Revision**: 불변 워크로드 spec이며 외부 Secret·ConfigMap·스토리지 상태까지 고정하지 않습니다. 보존·GC 정책이 롤백 가능한 Revision을 결정합니다.
* **Route**: 트래픽을 하나 이상의 Revision으로 라우팅. 비율 기반 트래픽 분할 지원
### 완전한 Knative Service YAML
참조한 이미지·ServiceAccount·Secret을 먼저 준비하세요. Serving 컨트롤러의 레지스트리 태그/다이제스트 해석과 노드의 이미지 다운로드는 별도로 가능해야 하며 EKS 노드 권한만으로 컨트롤러 접근이 보장되지는 않습니다. 프로브 경로는 실제 앱에 맞추고 하위 시스템 장애가 재시작 폭주를 만들지 않게 설계하세요.
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: order-api
namespace: knative-demo
labels:
app: order-api
team: backend
annotations: {}
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/metric: concurrency
autoscaling.knative.dev/target: '100'
autoscaling.knative.dev/min-scale: '2'
autoscaling.knative.dev/max-scale: '50'
autoscaling.knative.dev/initial-scale: '3'
autoscaling.knative.dev/scale-down-delay: 15m
autoscaling.knative.dev/window: 60s
autoscaling.knative.dev/class: kpa.autoscaling.knative.dev
labels:
app: order-api
version: v1
name: order-api-v1
spec:
containerConcurrency: 0
timeoutSeconds: 300
serviceAccountName: order-api-sa
containers:
- image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/order-api:v1.2.3
ports:
- containerPort: 8080
protocol: TCP
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: db-credentials
key: host
- name: LOG_LEVEL
value: info
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: '2'
memory: 2Gi
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
```
### 트래픽 분할
#### Canary 배포
위의 완전한 기본 Service와 준비된 `order-api-v1`에서 시작하는 대안 실습입니다. 이미지·이름을 검증한 값으로 교체하고 템플릿을 변경할 때마다 사용하지 않은 Revision 이름을 지정하세요. JSON 패치는 containers 목록 전체를 바꾸지 않고 환경 변수·ServiceAccount·프로브·리소스를 보존하며 기본 예제의 수신 컨테이너가 index0이라고 가정합니다.0% 태그는 테스트 경로를 제공하지만 전체 용량의 준비를 보장하지 않습니다.
`canary-template.patch.json`로 저장합니다:
```json
[
{
"op": "add",
"path": "/spec/traffic",
"value": [
{
"revisionName": "order-api-v1",
"percent": 100
},
{
"revisionName": "order-api-v2",
"percent": 0,
"tag": "canary"
}
]
},
{
"op": "add",
"path": "/spec/template/metadata/name",
"value": "order-api-v2"
},
{
"op": "replace",
"path": "/spec/template/spec/containers/0/image",
"value": "123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/order-api:v2.0.0"
}
]
```
```bash
kubectl wait --for=condition=Ready revision/order-api-v1 -n knative-demo --timeout=300s
kubectl patch ksvc order-api -n knative-demo --type json --patch-file canary-template.patch.json
kubectl wait --for=jsonpath='{.status.latestCreatedRevisionName}'=order-api-v2 ksvc/order-api -n knative-demo --timeout=180s
kubectl wait --for=condition=Ready revision/order-api-v2 -n knative-demo --timeout=300s
kubectl get ksvc order-api -n knative-demo -o jsonpath='{.status.traffic}'
# Route10%, then50%, then100% only after validating each stage.
kubectl patch ksvc order-api -n knative-demo --type merge --patch '{"spec":{"traffic":[{"revisionName":"order-api-v1","percent":90},{"revisionName":"order-api-v2","percent":10,"tag":"canary"}]}}'
kubectl patch ksvc order-api -n knative-demo --type merge --patch '{"spec":{"traffic":[{"revisionName":"order-api-v1","percent":50},{"revisionName":"order-api-v2","percent":50,"tag":"canary"}]}}'
kubectl patch ksvc order-api -n knative-demo --type merge --patch '{"spec":{"traffic":[{"revisionName":"order-api-v1","percent":0},{"revisionName":"order-api-v2","percent":100,"tag":"canary"}]}}'
```
태그 URL은 도메인·태그 템플릿과 TLS 설정에 따라 달라지므로 `status.traffic`에서 확인하세요. 변경마다 Route 준비 상태·반영된 설정·애플리케이션 지표를 확인합니다. 비율은 라우팅 정책이며 작은 요청 표본의 정확한 건수를 보장하지 않습니다.
#### Blue-Green 배포
기본 `order-api-v1`에서 별도로 실습하거나 현재 검증된 기준 트래픽을 처리하는 Revision 이름으로 stable 대상을 수정하세요. 다음을 `green-template.patch.json`로 저장합니다:
```json
[
{
"op": "add",
"path": "/spec/traffic",
"value": [
{
"revisionName": "order-api-v1",
"percent": 100
},
{
"revisionName": "order-api-green",
"percent": 0,
"tag": "green"
}
]
},
{
"op": "add",
"path": "/spec/template/metadata/name",
"value": "order-api-green"
},
{
"op": "replace",
"path": "/spec/template/spec/containers/0/image",
"value": "123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/order-api:v2.0.0"
}
]
```
```bash
kubectl patch ksvc order-api -n knative-demo --type json --patch-file green-template.patch.json
kubectl wait --for=jsonpath='{.status.latestCreatedRevisionName}'=order-api-green ksvc/order-api -n knative-demo --timeout=180s
kubectl wait --for=condition=Ready revision/order-api-green -n knative-demo --timeout=300s
kubectl get ksvc order-api -n knative-demo -o jsonpath='{.status.traffic}'
# Validate the green tag URL and capacity before requesting this switch.
kubectl patch ksvc order-api -n knative-demo --type merge --patch '{"spec":{"traffic":[{"revisionName":"order-api-v1","percent":0},{"revisionName":"order-api-green","percent":100,"tag":"green"}]}}'
```
트래픽 변경은 비동기로 반영되며 연결·진행 중 요청은 이전 Revision을 계속 사용할 수 있습니다. 롤백을 위해 이전 Revision과 외부 의존성을 보존하고 실제 전파·준비 시간을 측정하세요. 이 명령을 실제 클러스터에서 실행하지 않았습니다.
### Scale-to-Zero 동작 원리
Scale-to-Zero는 Knative의 핵심 기능으로, 트래픽이 없을 때 파드를 0으로 축소하여 리소스를 절약합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-autoscaling-03-knative-3.html)
**동작 단계:**
1. **유휴 감지**: Autoscaler가 `stable-window` (기본 60초) 동안 동시 요청 수가 0인 것을 감지
2. **네트워크 준비**: `scale-to-zero-grace-period`(기본30초)는 마지막 Pod 제거 전 제로 활성화 경로 준비의 상한이며 마지막 요청 후30초 보존을 보장하지 않습니다.
3. **Activator 경로 확인**: 마지막 복제본 제거 전 내부 라우팅을 준비합니다. 별도 retention 설정은 제로 축소 결정 후 마지막 Pod의 최소 유지 시간을 제어합니다.
4. **콜드 스타트**: 새 요청이 오면 Activator가 버퍼링하고 Autoscaler에 스케일업 요청
5. **요청 전달**: 파드가 Ready 상태가 되면 버퍼링된 요청을 전달
### Concurrency 기반 스케일링
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: concurrency-demo
namespace: knative-demo
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/target: '10'
autoscaling.knative.dev/metric: concurrency
autoscaling.knative.dev/target-utilization-percentage: '70'
spec:
containerConcurrency: 50
containers:
- image: my-app:latest
```
Readiness는 실제 준비 상태를 검증해야 하며 프로브 간격을 줄이는 것만으로 앱·모델 초기화가 빨라지지는 않습니다. `containerConcurrency: 1`도 모든 복제본의 전역 직렬 처리나 컨테이너 내부 스레드 안전성을 보장하지 않습니다.
**소프트 타겟 vs 하드 리밋:**
* `autoscaling.knative.dev/target` (소프트): Autoscaler의 스케일링 목표. 이 값을 기준으로 파드 수 계산
* `containerConcurrency` (하드): Queue Proxy가 강제하는 절대 최대 동시성. 초과 요청은 큐잉되거나 503 반환
**스케일링 계산 예시:**
* 현재 동시 요청: 70
* Target: 10, Utilization: 70%
* 실제 타겟: 10 \* 0.7 = 7
* 필요 파드 수: 70 / 7 = 10개
### 콜드 스타트 최적화
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: low-latency-api
namespace: knative-demo
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/min-scale: '2'
autoscaling.knative.dev/initial-scale: '3'
autoscaling.knative.dev/scale-down-delay: 5m
autoscaling.knative.dev/window: 120s
spec:
containers:
- image: my-app:latest
resources:
requests:
cpu: '1'
memory: 1Gi
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 1
periodSeconds: 2
timeoutSeconds: 1
failureThreshold: 3
```
사전 풀 DaemonSet 예시는 셸을 포함하는 NGINX1.30.4를 캐시합니다. 실제 앱에는 그 이미지에 유효한 명령·동일 다이제스트·아키텍처가 필요합니다. Distroless에 셸이 있다고 가정하지 마세요. Fargate는 DaemonSet 미지원이며 노드 교체·이미지 GC로 캐시가 사라질 수 있습니다.
**콜드 스타트 최적화 전략:**
| 전략 | 설정 | 효과 |
| ----------------- | ------------------------ | ------------------------ |
| 최소 인스턴스 유지 | `min-scale: 1+` | 일반적인 유휴 제로 축소 방지; 새 Revision·재시작·추가 확장에는 초기화가 남음 |
| 초기 스케일 설정 | `initial-scale: N` | 첫 배포 시 빠른 응답 |
| 스케일 다운 지연 | `scale-down-delay: 5m` | 간헐적 트래픽에서 불필요한 스케일 다운 방지 |
| 컨테이너 이미지 최적화 | 경량 베이스 이미지 사용 | 이미지 풀 시간 단축 |
| Readiness 프로브 최적화 | 짧은 `initialDelaySeconds` | 트래픽 수신 시작 시간 단축 |
| 이미지 사전 풀 | 호환 EC2 노드의 사전 캐시 | 같은 다이제스트·아키텍처·남아 있는 캐시에 한해 다운로드를 줄임; Fargate는 DaemonSet 미지원 |
### Private/Public 서비스
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: public-api
labels: {}
namespace: knative-demo
spec:
template:
spec:
containers:
- image: order-api:latest
---
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: internal-processor
labels:
networking.knative.dev/visibility: cluster-local
namespace: knative-demo
spec:
template:
spec:
containers:
- image: my-processor:latest
```
```bash
# Private 서비스 접근 방식 (클러스터 내부에서)
# http://internal-processor.knative-demo.svc.cluster.local
curl http://internal-processor.knative-demo.svc.cluster.local
```
***
## Knative Eventing 심화
### Event Source
#### ApiServerSource
Kubernetes API Server의 이벤트를 CloudEvents로 변환하여 전달합니다.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: k8s-events-sa
namespace: knative-demo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: k8s-events-reader
namespace: knative-demo
rules:
- apiGroups:
- ''
resources:
- pods
verbs:
- get
- list
- watch
- apiGroups:
- apps
resources:
- deployments
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: k8s-events-reader
namespace: knative-demo
subjects:
- kind: ServiceAccount
name: k8s-events-sa
namespace: knative-demo
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: k8s-events-reader
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: knative-demo-namespace-discovery
rules:
- apiGroups:
- ''
resources:
- namespaces
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: knative-demo-namespace-discovery
subjects:
- kind: ServiceAccount
name: k8s-events-sa
namespace: knative-demo
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: knative-demo-namespace-discovery
---
apiVersion: sources.knative.dev/v1
kind: ApiServerSource
metadata:
name: k8s-events
namespace: knative-demo
spec:
resources:
- apiVersion: v1
kind: Pod
- apiVersion: apps/v1
kind: Deployment
mode: Reference
sink:
ref:
apiVersion: eventing.knative.dev/v1
kind: Broker
name: default
serviceAccountName: k8s-events-sa
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: knative-demo
```
#### SinkBinding
Eventing 설정은 inclusion 모드를 사용합니다. 데모 네임스페이스만 주입 대상으로 표시하고 subject Deployment의 네임스페이스·메타데이터 이름/라벨이 맞는지 확인하세요. 이미 실행 중인 워크로드는 주입된 환경을 사용하기 위해 정상 롤아웃이 필요할 수 있습니다:
```bash
kubectl label namespace knative-demo bindings.knative.dev/include=true --overwrite
```
SinkBinding은 목적지 `K_SINK`와 `K_CE_OVERRIDES`를 주입합니다. 애플리케이션이 이를 읽어 전송해야 하며 임의의 HTTP 요청을 자동으로 바꾸지 않습니다. 아래 생산자는 올바른 구조화 CloudEvent, 타임아웃·응답 확인을 사용합니다. 동일한 논리 이벤트를 재시도할 때는 `event_id`를 유지하세요.
```yaml
apiVersion: sources.knative.dev/v1
kind: SinkBinding
metadata:
name: order-events-binding
namespace: knative-demo
spec:
subject:
apiVersion: apps/v1
kind: Deployment
selector:
matchLabels:
app: order-service
sink:
ref:
apiVersion: eventing.knative.dev/v1
kind: Broker
name: default
ceOverrides:
extensions:
team: commerce
producer: /orders/api
```
```python
import json
import os
import re
import requests
from cloudevents.http import CloudEvent
from cloudevents.conversion import to_structured
CORE_ATTRIBUTES = {"specversion", "id", "source", "type", "time", "subject", "datacontenttype", "dataschema"}
def emit_order_event(order_id, event_type, data, event_id):
"""Use one stable event_id for retries of the same logical event."""
attributes = {
"specversion": "1.0", "id": event_id,
"type": f"com.example.order.{event_type}",
"source": "/orders/api", "subject": f"order/{order_id}",
"datacontenttype": "application/json",
}
overrides = json.loads(os.environ.get("K_CE_OVERRIDES", "{}"))
for key, value in overrides.get("extensions", {}).items():
if key in CORE_ATTRIBUTES or not re.fullmatch(r"[a-z0-9]+", key):
raise ValueError("Only valid extension attributes may be overridden")
attributes[key] = value
event = CloudEvent(attributes, data)
headers, body = to_structured(event)
response = requests.post(
os.environ["K_SINK"], data=body, headers=headers, timeout=(3, 10)
)
response.raise_for_status()
return response.status_code
```
#### KafkaSource
KafkaSource·KafkaChannel에는 해당 Kafka 확장과 설정된 기존 Kafka 클러스터가 필요합니다(bootstrap/TLS/SASL과 복제 계수에 충분한 브로커 포함). 다음1.23.1 URL을 기존 additionalManifests와 합쳐 KnativeEventing으로 관리하고 같은 확장을 다른 경로에서 동시에 관리하지 마세요. eventing-kafka.patch.yaml로 저장합니다:
```yaml
spec:
additionalManifests:
- URL: https://github.com/knative-extensions/eventing-kafka-broker/releases/download/knative-v1.23.1/eventing-kafka-controller.yaml
- URL: https://github.com/knative-extensions/eventing-kafka-broker/releases/download/knative-v1.23.1/eventing-kafka-source.yaml
- URL: https://github.com/knative-extensions/eventing-kafka-broker/releases/download/knative-v1.23.1/eventing-kafka-channel.yaml
```
```bash
kubectl patch knativeeventing knative-eventing -n knative-eventing --type merge --patch-file eventing-kafka.patch.yaml
GENERATION=$(kubectl get knativeeventing knative-eventing -n knative-eventing -o jsonpath='{.metadata.generation}')
kubectl wait --for=jsonpath='{.status.observedGeneration}'="$GENERATION" knativeeventing/knative-eventing -n knative-eventing --timeout=600s
kubectl wait --for=condition=Ready knativeeventing/knative-eventing -n knative-eventing --timeout=600s
kubectl get crd kafkasources.sources.knative.dev kafkachannels.messaging.knative.dev
```
```yaml
apiVersion: sources.knative.dev/v1
kind: KafkaSource
metadata:
name: kafka-order-events
namespace: knative-demo
spec:
consumerGroup: knative-order-consumer
bootstrapServers:
- kafka-bootstrap.kafka:9092
topics:
- orders
- order-updates
net:
sasl:
enable: true
type:
secretKeyRef:
name: kafka-credentials
key: sasl-type
user:
secretKeyRef:
name: kafka-credentials
key: user
password:
secretKeyRef:
name: kafka-credentials
key: password
tls:
enable: true
sink:
ref:
apiVersion: eventing.knative.dev/v1
kind: Broker
name: default
```
#### SQSSource (AWS 연동)
보관된 TriggerMesh 예제를1.23 core의 alpha IntegrationSource로 대체했습니다. 기존 전용 테스트 큐와 IRSA 역할을 준비하고 ReceiveMessage/DeleteMessage/GetQueueAttributes/GetQueueUrl 권한을 해당 큐로 제한하세요. 실행하면 메시지를 소비·삭제하며 이번 감사에서는 배포하지 않았습니다. autoCreateQueue는 false입니다. 실제 adapter의 CloudEvent type/source와 승인·가시성 시간 제한·실패 동작을 확인해야 하며 수동 주문 이벤트용 Trigger 필터를 그대로 적용할 수 있다고 가정하지 마세요. EKS Pod Identity는 선택한 EKS 컴퓨팅·에이전트·SDK가 지원할 때 사용할 수 있는 대안이며 Fargate에서는 지원되지 않습니다.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: sqs-event-source
namespace: knative-demo
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/KnativeSqsSourceRole
---
apiVersion: sources.knative.dev/v1alpha1
kind: IntegrationSource
metadata:
name: sqs-order-events
namespace: knative-demo
spec:
aws:
sqs:
arn: arn:aws:sqs:us-west-2:123456789012:knative-demo-orders
region: us-west-2
autoCreateQueue: false
deleteAfterRead: true
visibilityTimeout: 120
auth:
serviceAccountName: sqs-event-source
sink:
ref:
apiVersion: eventing.knative.dev/v1
kind: Broker
name: default
```
### Broker/Trigger 패턴
#### 완전한 Broker/Trigger YAML
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-broker-channel
namespace: knative-demo
data:
channel-template-spec: |
apiVersion: messaging.knative.dev/v1
kind: InMemoryChannel
---
apiVersion: eventing.knative.dev/v1
kind: Broker
metadata:
name: default
namespace: knative-demo
annotations:
eventing.knative.dev/broker.class: MTChannelBasedBroker
spec:
config:
apiVersion: v1
kind: ConfigMap
name: demo-broker-channel
namespace: knative-demo
delivery:
deadLetterSink:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: dead-letter-handler
retry: 3
backoffPolicy: exponential
backoffDelay: PT2S
---
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
name: order-created-trigger
namespace: knative-demo
spec:
broker: default
filter:
attributes:
type: com.example.order.created
source: /orders/api
subscriber:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: order-processor
uri: /process
delivery:
deadLetterSink:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: order-dlq-handler
retry: 5
backoffPolicy: exponential
backoffDelay: PT1S
---
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
name: payment-processed-trigger
namespace: knative-demo
spec:
broker: default
filter:
attributes:
type: com.example.payment.processed
subscriber:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: shipping-service
---
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
name: audit-all-events
namespace: knative-demo
spec:
broker: default
subscriber:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: audit-logger
```
### CloudEvents 표준
아래 수신기는 형식 확인과 수신 승인 데모입니다. 주문·결제 트랜잭션을 구현하지 않으며 실제 소비자는 처리·멱등성 기록을 완료한 뒤 승인해야 합니다. 각 Python 예제는 별도 이미지의 `app.py`로 저장하고 WSGI 서버로 실행하세요.
Knative Eventing은 **CloudEvents v1.0** 사양을 표준 이벤트 형식으로 사용합니다.
```json
{
"specversion": "1.0",
"type": "com.example.order.created",
"source": "/orders/api",
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"time": "2025-06-15T10:30:00Z",
"datacontenttype": "application/json",
"subject": "order/12345",
"data": {
"orderId": "12345",
"customerId": "C001",
"items": [
{"productId": "P100", "quantity": 2, "price": 29900}
],
"totalAmount": 59800
}
}
```
```python
import json
from flask import Flask, request
from cloudevents.http import from_http
def create_app():
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024
@app.post("/")
def receive_event():
try:
event = from_http(request.headers, request.get_data())
metadata = {key: event[key] for key in ("source", "id", "type")}
except Exception:
# This boundary converts malformed input into a client error.
return "invalid CloudEvent", 400
app.logger.info("Received CloudEvent metadata: %s", json.dumps(metadata))
# Receipt-only demo. Real consumers must commit processing before acknowledging.
return "", 204
return app
```
### Channel/Subscription 패턴
```yaml
apiVersion: messaging.knative.dev/v1
kind: KafkaChannel
metadata:
name: order-events-channel
namespace: knative-demo
spec:
numPartitions: 6
replicationFactor: 3
retentionDuration: PT168H
---
apiVersion: messaging.knative.dev/v1
kind: Subscription
metadata:
name: analytics-subscription
namespace: knative-demo
spec:
channel:
apiVersion: messaging.knative.dev/v1
kind: KafkaChannel
name: order-events-channel
subscriber:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: analytics-service
uri: /events/orders
reply:
ref:
apiVersion: messaging.knative.dev/v1
kind: KafkaChannel
name: analytics-results-channel
delivery:
deadLetterSink:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: dlq-handler
retry: 3
backoffPolicy: linear
backoffDelay: PT5S
---
apiVersion: messaging.knative.dev/v1
kind: Subscription
metadata:
name: notification-subscription
namespace: knative-demo
spec:
channel:
apiVersion: messaging.knative.dev/v1
kind: KafkaChannel
name: order-events-channel
subscriber:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: notification-service
```
### Dead Letter Sink
처리기 이미지는 Flask·CloudEvents·boto3·Gunicorn과 아래 `app.py`로 직접 빌드해야 합니다. 기존의 보호된 버킷, 전용 ServiceAccount의 제한된 `s3:PutObject`·필요한 KMS 권한, 통신 경로가 필요합니다. `S3_BUCKET`·`AWS_REGION`을 설정하고 정적 AWS 자격 증명을 넣지 마세요. 실행 예시는 `gunicorn --bind 0.0.0.0:8080 --workers 1 --threads 4 --timeout 90 --graceful-timeout 60 app:create_app()`이며 Knative·프록시·Pod 종료 기한과 맞춰 검증해야 합니다.
DLS는 구독자 전달 실패 시 사용하는 설정된 대체 목적지입니다. DLS 자체도 실패할 수 있으며 영속 저장은 처리기와 저장소가 구현해야 합니다. 저장 성공 후 승인하고 CloudEvent의 source+id로 중복을 다루어야 합니다.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: dead-letter-writer
namespace: knative-demo
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/KnativeDeadLetterWriterRole
---
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: dead-letter-handler
namespace: knative-demo
labels:
networking.knative.dev/visibility: cluster-local
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/min-scale: '1'
spec:
containers:
- image: dead-letter-handler:latest
env:
- name: S3_BUCKET
value: REPLACE_WITH_EXISTING_BUCKET
- name: AWS_REGION
value: us-west-2
ports:
- containerPort: 8080
command:
- gunicorn
args:
- --bind
- 0.0.0.0:8080
- --workers
- '1'
- --threads
- '4'
- --timeout
- '90'
- --graceful-timeout
- '60'
- app:create_app()
serviceAccountName: dead-letter-writer
timeoutSeconds: 60
```
```python
import base64
import hashlib
import json
import os
import boto3
from botocore.config import Config
from botocore.exceptions import BotoCoreError, ClientError
from cloudevents.http import from_http
from flask import Flask, request
def create_app(s3_client=None):
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024
bucket = os.environ["S3_BUCKET"]
if s3_client is None:
# Create once per application worker; IRSA/Pod Identity uses the credential chain.
s3_client = boto3.client(
"s3", region_name=os.environ["AWS_REGION"],
config=Config(connect_timeout=3, read_timeout=10,
retries={"mode": "standard", "total_max_attempts": 2}),
)
@app.post("/")
def store_dead_letter():
raw_body = request.get_data()
try:
event = from_http(request.headers, raw_body)
source, event_id = str(event["source"]), str(event["id"])
except Exception:
return "invalid CloudEvent", 400
identity = json.dumps([source, event_id], ensure_ascii=False,
separators=(",", ":")).encode("utf-8")
key = "dead-letters/" + hashlib.sha256(identity).hexdigest() + ".json"
record = {
"source": source, "id": event_id,
"content_type": request.headers.get("Content-Type", "application/octet-stream"),
# Preserve CloudEvents transport attributes, never Authorization/Cookie headers.
"ce_headers": {k.lower(): v for k, v in request.headers.items()
if k.lower().startswith("ce-")},
"body_base64": base64.b64encode(raw_body).decode("ascii"),
}
try:
s3_client.put_object(
Bucket=bucket, Key=key,
Body=json.dumps(record, ensure_ascii=False).encode("utf-8"),
ContentType="application/json", IfNoneMatch="*",
)
except ClientError as exc:
# HTTP boundary: acknowledge a stored duplicate; retry other storage failures.
status = exc.response.get("ResponseMetadata", {}).get("HTTPStatusCode")
if status == 412:
return "", 204
app.logger.error("DLS storage failed: %s", exc.response.get("Error", {}).get("Code"))
return "storage unavailable", 503
except BotoCoreError as exc:
app.logger.error("DLS storage unavailable: %s", type(exc).__name__)
return "storage unavailable", 503
return "", 204
return app
```
본문을 Base64로 보존하고 CloudEvents 전송 헤더만 저장하므로 바이너리 이벤트를 유지하고 Authorization/Cookie 헤더를 제외합니다. `(source, id)` 키와 조건부 저장을 사용하며 기존 키의412는 승인,409와 다른 저장 실패는 전달 정책이 재시도하도록503을 반환합니다. 보존·삭제 정책과 생산자의 ID 재사용이 중복 처리에 영향을 주므로 업무 처리의 정확히1회 보장은 아닙니다.1MiB 초과 요청은 거부합니다. 로컬 모의 테스트만 수행했고 실제 버킷·Eventing 배포를 시험하지 않았습니다.
### Event 필터링
#### Attributes 기반 필터링
```yaml
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
name: exact-filter
namespace: knative-demo
spec:
broker: default
filter:
attributes:
type: com.example.order.created
source: /orders/api
subscriber:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: order-handler
```
#### 고급 필터 API (1.23 예제)
선택한1.23 API는 `spec.filters`의 any/all/not·exact/prefix/suffix 등 표현식을 제공합니다. 기존 `spec.filter.attributes`는 AND이며 두 필터 형식을 섞지 마세요. 사용하는 Broker 구현의 지원도 확인해야 합니다.
```yaml
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
name: advanced-filter
namespace: knative-demo
spec:
broker: default
filters:
- all:
- prefix:
type: com.example.order.
- exact:
source: order-service
- not:
exact:
priority: low
subscriber:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: high-priority-order-handler
---
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
name: multi-event-filter
namespace: knative-demo
spec:
broker: default
filters:
- any:
- exact:
type: com.example.order.created
- exact:
type: com.example.order.updated
- exact:
type: com.example.order.cancelled
subscriber:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: order-lifecycle-handler
```
***
## KEDA와 Knative 비교
### 스케일링 모델 차이

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-autoscaling-03-knative-4.html)
| 비교 항목 | Knative | KEDA |
| ------------ | ------------------------------------ | ------------------------------- |
| **스케일링 트리거** | HTTP 동시성/RPS (Queue Proxy 기반) | 50+ 외부 메트릭 소스 |
| **스케일링 주체** | KPA 또는 선택적 HPA 확장 | ScaledObject는 오퍼레이터 활성화+HPA, ScaledJob은 직접 Job 생성 |
| **메트릭 수집** | Queue Proxy 사이드카 | KEDA Metrics Server |
| **최소 스케일** | 0 (Scale-to-Zero 네이티브) | 0 (ScaledObject로 구현) |
| **스케일링 대상** | Knative Revision의 관리 Deployment | 호환 scale 대상은 ScaledObject, Job 생성은 ScaledJob |
| **네트워킹** | Ingress 포함 (Kourier/Istio) | 네트워킹 불포함 |
| **서비스 모델** | Knative Service (Revision, Route 포함) | 기존 Kubernetes 워크로드 그대로 사용 |
| **프로토콜** | HTTP/gRPC | 프로토콜 무관 |
### Scale-to-Zero 동작 차이
| 측면 | Knative Scale-to-Zero | KEDA Scale-to-Zero |
| ------------- | -------------------------------- | -------------------------------- |
| **구현 방식** | Activator가 트래픽을 버퍼링하고 파드 기동 후 전달 | 외부 메트릭이 임계값 이하일 때 replicas=0 |
| **콜드 스타트 처리** | Activator가 용량·타임아웃 범위에서 대기 | 큐·소비자가 보존/승인을 구현; KEDA 자체는 메시지를 저장하지 않음 |
| **트리거 방식** | HTTP 요청이 직접 스케일업 트리거 | 메트릭 폴링으로 감지 (pollingInterval 지연) |
| **스케일업 지연** | 컨테이너 시작 시간 | pollingInterval + 컨테이너 시작 시간 |
| **적합한 워크로드** | 동기 HTTP API, 웹 서비스 | 비동기 큐 처리, 배치 작업 |
### 이벤트 드리븐 아키텍처에서의 역할
**Knative Eventing**: 이벤트 라우팅 및 전달 프레임워크
* CloudEvents 표준 기반 이벤트 소싱
* Broker/Trigger 패턴으로 이벤트 필터링 및 라우팅
* 이벤트 소스에서 소비자까지의 전체 파이프라인 관리
**KEDA**: 이벤트 기반 스케일링 엔진
* 이벤트 큐 깊이에 따른 워커 스케일링
* 다양한 메시지 브로커(SQS, Kafka, RabbitMQ 등) 직접 연동
* 메트릭 기반으로 워크로드 수를 동적으로 조절
### 사용 시나리오 가이드
| 시나리오 | 권장 도구 | 이유 |
| ---------------------- | -------------------------------- | ------------------------------------------- |
| HTTP API 서버리스 배포 | **Knative Serving** | Scale-to-Zero + HTTP 라우팅 + 트래픽 분할 |
| SQS 큐 메시지 처리 워커 | **KEDA** | SQS 큐 깊이 기반 스케일링에 최적화 |
| Kafka 이벤트 스트림 처리 | **KEDA** 또는 **Knative Eventing** | 단순 스케일링: KEDA, 이벤트 라우팅 필요: Knative |
| ML 추론 서비스 | **Knative Serving** | HTTP 기반 + Scale-to-Zero로 GPU 비용 절감 |
| 일정 기반 Job / 이벤트 배치 | **CronJob / KEDA** | 일정마다 실행할 Job은 Kubernetes CronJob, 이벤트 수요의 Job 생성은 ScaledJob. KEDA Cron은 시간 구간의 복제본 목표이며 CronJob 확장이 아님 |
| 마이크로서비스 이벤트 파이프라인 | **Knative Eventing** | CloudEvents + Broker/Trigger로 복잡한 이벤트 흐름 관리 |
| Prometheus 메트릭 기반 스케일링 | **KEDA** | Prometheus 스케일러로 커스텀 메트릭 연동 |
### 함께 사용하는 시나리오
Knative와 KEDA는 상호 배타적이지 않으며, 같은 클러스터에서 함께 사용할 수 있습니다.
```yaml
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: keda-aws-credentials
namespace: knative-demo
spec:
podIdentity:
provider: aws
identityOwner: keda
---
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: order-api
namespace: knative-demo
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/target: '50'
autoscaling.knative.dev/min-scale: '1'
spec:
containers:
- image: order-api:latest
env:
- name: SQS_QUEUE_URL
value: https://sqs.ap-northeast-2.amazonaws.com/123456789012/order-queue
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-worker-scaler
namespace: knative-demo
spec:
scaleTargetRef:
name: order-worker
minReplicaCount: 0
maxReplicaCount: 100
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.ap-northeast-2.amazonaws.com/123456789012/order-queue
queueLength: '5'
awsRegion: ap-northeast-2
authenticationRef:
name: keda-aws-credentials
```
***
## 프로덕션 운영
### 리소스 제한 및 QoS
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: production-api
namespace: knative-demo
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/min-scale: '2'
autoscaling.knative.dev/max-scale: '100'
autoscaling.knative.dev/target: '80'
spec:
containerConcurrency: 200
timeoutSeconds: 60
containers:
- image: production-api:latest
resources:
requests:
cpu: '1'
memory: 1Gi
ephemeral-storage: 512Mi
limits:
cpu: '2'
memory: 2Gi
ephemeral-storage: 1Gi
```
**QoS 클래스와 실제 Pod 확인:**
CPU·메모리 QoS는 Queue Proxy 등 모든 관련 컨테이너를 기준으로 정해집니다. 위 예제는 requests와 limits가 달라 Burstable이며 Guaranteed가 아닙니다. ephemeral-storage는 QoS 클래스 결정 기준이 아닙니다. 리소스 제한은 OOM·축출 방지를 보장하지 않으며 앱에서 값을 생략해도 Knative 기본값·사이드카 요청이 적용될 수 있습니다.
* **프로덕션 API**: `Guaranteed` (requests = limits) 또는 `Burstable` (limits > requests)
* **배치 처리**: `Burstable` (유연한 리소스 사용)
* **개발/테스트**: `BestEffort` 가능 (리소스 제한 없음)
### Revision GC (가비지 컬렉션) 정책
오래된 Revision을 자동으로 정리하여 클러스터 리소스를 확보합니다.
```bash
cat > serving-gc.patch.yaml <<'YAML'
spec:
config:
gc:
min-non-active-revisions: "2"
max-non-active-revisions: "10"
retain-since-create-time: "48h"
retain-since-last-active-time: "24h"
YAML
kubectl patch knativeserving knative-serving -n knative-serving --type merge --patch-file serving-gc.patch.yaml
```
| 설정 | 기본값 | 설명 |
| ------------------------------- | --- | --------------------- |
| `max-non-active-revisions` |1000| 비활성 Revision 최대 보관 수 |
| `retain-since-create-time` | 48h | 생성 후 최소 보존 시간 |
| `retain-since-last-active-time` | 15h | 마지막 활성 후 최소 보존 시간 |
| `min-non-active-revisions` |20| 최소 보관할 비활성 Revision 수 |
### 고가용성 구성
```yaml
spec:
version: 1.23.0
high-availability:
replicas: 3
ingress:
kourier:
enabled: true
config:
network:
ingress-class: kourier.ingress.networking.knative.dev
autoscaler:
enable-scale-to-zero: 'true'
stable-window: 120s
panic-window-percentage: '10.0'
panic-threshold-percentage: '200.0'
features:
kubernetes.podspec-topologyspreadconstraints: enabled
```
Operator 병합 패치를 serving-ha.patch.yaml로 저장하고 기존 workloads·배열 항목을 보존하세요. 복제본 수만으로 HA를 보장하지 말고 배치·리소스·장애 동작을 검증해야 합니다.
```bash
kubectl patch knativeserving knative-serving -n knative-serving --type merge --patch-file serving-ha.patch.yaml
```
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: ha-api
namespace: knative-demo
spec:
template:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
serving.knative.dev/service: ha-api
containers:
- image: ha-api:latest
```
### 모니터링 (Prometheus 메트릭)
Knative1.23은 OpenTelemetry를 사용하며 메트릭 내보내기는 기본 비활성화입니다. 기존 Prometheus/Prometheus Operator 배포를 가정합니다. OTLP 수신기(`--web.enable-otlp-receiver` 또는 해당 Operator 설정)를 활성화하고 접근 범위·리소스 속성 승격을 구성하세요. 다음은 Helm 값이 아닌 **Prometheus 기본 설정 조각**이며 설치 버전의 지원을 확인해야 합니다:
```yaml
otlp:
translation_strategy: UnderscoreEscapingWithSuffixes
convert_histograms_to_nhcb: false
promote_resource_attributes:
- k8s.namespace.name
- k8s.pod.name
- kn.service.name
- kn.configuration.name
- kn.revision.name
```
다음을 `serving-metrics.patch.yaml`로 저장해 Operator 관리 Serving에 병합합니다. 엔드포인트는 실제 OTLP 수신기로 교체해야 하며 이 Service 이름이 자동 존재한다고 가정하지 않습니다. 컨트롤 플레인은 스크레이프하고 요청 메트릭은 전송하므로 Queue Proxy 관리 포트를 스크레이프하지 않습니다:
```yaml
spec:
config:
observability:
metrics-protocol: prometheus
request-metrics-protocol: http/protobuf
request-metrics-endpoint: http://prometheus-operated.monitoring.svc.cluster.local:9090/api/v1/otlp/v1/metrics
request-metrics-export-interval: 10s
```
```bash
kubectl patch knativeserving knative-serving -n knative-serving --type merge --patch-file serving-metrics.patch.yaml
kubectl patch knativeeventing knative-eventing -n knative-eventing --type merge --patch '{"spec":{"config":{"observability":{"metrics-protocol":"prometheus"}}}}'
kubectl get service prometheus-operated -n monitoring
```
다음 ServiceMonitor는 릴리스의 컨트롤 플레인 Service 라벨과 `http-metrics` Service 포트에 맞습니다. 모니터 네임스페이스·라벨도 Prometheus 리소스의 선택 조건과 맞아야 합니다. 설정 또는 필요한 롤아웃 후 실제 엔드포인트를 확인하세요:
```yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: knative-serving-control-plane
namespace: monitoring
labels:
release: prometheus
spec:
namespaceSelector:
matchNames:
- knative-serving
selector:
matchExpressions:
- key: app
operator: In
values:
- controller
- webhook
- autoscaler
- activator
endpoints:
- port: http-metrics
path: /metrics
interval: 30s
honorLabels: true
```
**현재 메트릭 이름**(OTel 계측 이름이며 선택한 Prometheus 변환은 점을 바꾸고 실제 단위 접미사를 추가합니다):
| 계측 이름 | 의미 |
|---|---|
| `kn.serving.invocation.duration` | 초 단위 요청 완료 지연 히스토그램; count로 완료 요청 속도를 계산할 수 있음 |
| `kn.serving.queue.depth` | Queue Proxy 큐·진행 중 요청 표본이며 항상 최신인 전체 동시성 수치는 아님 |
| `kn.revision.pods.desired` / `kn.revision.pods.requested` / `kn.revision.pods.count` | 원하는·요청한·현재 할당된 Pod 게이지 |
| `kn.revision.pods.not_ready.count` / `kn.revision.pods.pending.count` | 준비되지 않은·대기 중 Pod 게이지 |
| `kn.revision.concurrency.stable` / `kn.revision.concurrency.panic` | 각 윈도우의 관찰 Pod당 평균 동시성; Pod 수로 다시 나누지 않음 |
| `kn.revision.request.concurrency` | Activator를 지나는 요청 동시성이며 콜드 스타트 횟수가 아님 |
| `kn.workqueue.depth` / `kn.workqueue.process.duration` | 컨트롤러 대기열 깊이·처리 시간 |
Eventing Broker·Source·백엔드 메트릭은 구현에 따라 다릅니다. 공식 페이지도 예전 OpenCensus 표 일부의 마이그레이션이 끝나지 않았다고 명시하므로 `broker_event_count`·`trigger_filter_event_count`가 있다고 가정하지 마세요. 선택한 구현의 내보낸 메트릭과 전달 상태를 확인해야 합니다. 이번 감사에서는 실제 스크레이프·추적 전송을 실행하지 않았습니다.
### Grafana 대시보드
HTTP API의 `dashboard` 래퍼가 아닌 대시보드 JSON 정의입니다. 가져오기 전에 데이터 소스 UID를 교체하세요. 쿼리는 위 변환 방식·일반 히스토그램 버킷·승격된 속성을 가정하며 실제 배포의 라벨·집계를 확인해야 합니다. panic 윈도우 값은 패닉 모드의 Boolean 표시가 아니며 복제본이0보다 커도 Activator를 사용할 수 있습니다:
```json
{
"id": null,
"uid": "knative-demo-overview",
"title": "Knative Demo Overview",
"schemaVersion": 39,
"version": 1,
"refresh": "30s",
"time": {
"from": "now-1h",
"to": "now"
},
"panels": [
{
"id": 1,
"title": "Completed Request Rate",
"type": "timeseries",
"gridPos": {
"x": 0,
"y": 0,
"w": 12,
"h": 8
},
"datasource": {
"type": "prometheus",
"uid": "REPLACE_WITH_PROMETHEUS_DATASOURCE_UID"
},
"fieldConfig": {
"defaults": {
"unit": "reqps"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"expr": "sum by (k8s_namespace_name, kn_revision_name) (rate(kn_serving_invocation_duration_seconds_count{k8s_namespace_name=\"knative-demo\"}[5m]))",
"legendFormat": "{{kn_revision_name}}"
}
]
},
{
"id": 2,
"title": "Request Duration P99",
"type": "timeseries",
"gridPos": {
"x": 12,
"y": 0,
"w": 12,
"h": 8
},
"datasource": {
"type": "prometheus",
"uid": "REPLACE_WITH_PROMETHEUS_DATASOURCE_UID"
},
"fieldConfig": {
"defaults": {
"unit": "s"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"expr": "histogram_quantile(0.99, sum by (le, k8s_namespace_name, kn_revision_name) (rate(kn_serving_invocation_duration_seconds_bucket{k8s_namespace_name=\"knative-demo\"}[5m])))",
"legendFormat": "{{kn_revision_name}}"
}
]
},
{
"id": 3,
"title": "Queue Depth Sample",
"type": "timeseries",
"gridPos": {
"x": 0,
"y": 8,
"w": 12,
"h": 8
},
"datasource": {
"type": "prometheus",
"uid": "REPLACE_WITH_PROMETHEUS_DATASOURCE_UID"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"expr": "max by (k8s_namespace_name, kn_revision_name) (kn_serving_queue_depth{k8s_namespace_name=\"knative-demo\"})",
"legendFormat": "{{kn_revision_name}}"
}
]
},
{
"id": 4,
"title": "Desired and Actual Pods",
"type": "timeseries",
"gridPos": {
"x": 12,
"y": 8,
"w": 12,
"h": 8
},
"datasource": {
"type": "prometheus",
"uid": "REPLACE_WITH_PROMETHEUS_DATASOURCE_UID"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"expr": "max by (k8s_namespace_name, kn_revision_name) (kn_revision_pods_desired{k8s_namespace_name=\"knative-demo\"})",
"legendFormat": "{{kn_revision_name}}"
},
{
"refId": "B",
"expr": "max by (k8s_namespace_name, kn_revision_name) (kn_revision_pods_count{k8s_namespace_name=\"knative-demo\"})",
"legendFormat": "actual {{kn_revision_name}}"
}
]
},
{
"id": 5,
"title": "Stable and Panic Window Concurrency",
"type": "timeseries",
"gridPos": {
"x": 0,
"y": 16,
"w": 12,
"h": 8
},
"datasource": {
"type": "prometheus",
"uid": "REPLACE_WITH_PROMETHEUS_DATASOURCE_UID"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"expr": "max by (k8s_namespace_name, kn_revision_name) (kn_revision_concurrency_stable{k8s_namespace_name=\"knative-demo\"})",
"legendFormat": "{{kn_revision_name}}"
},
{
"refId": "B",
"expr": "max by (k8s_namespace_name, kn_revision_name) (kn_revision_concurrency_panic{k8s_namespace_name=\"knative-demo\"})",
"legendFormat": "panic window {{kn_revision_name}}"
}
]
},
{
"id": 6,
"title": "Requests Through Activator",
"type": "timeseries",
"gridPos": {
"x": 12,
"y": 16,
"w": 12,
"h": 8
},
"datasource": {
"type": "prometheus",
"uid": "REPLACE_WITH_PROMETHEUS_DATASOURCE_UID"
},
"fieldConfig": {
"defaults": {
"unit": "short"
},
"overrides": []
},
"targets": [
{
"refId": "A",
"expr": "max by (k8s_namespace_name, kn_revision_name) (kn_revision_request_concurrency{k8s_namespace_name=\"knative-demo\"})",
"legendFormat": "{{kn_revision_name}}"
}
]
}
]
}
```
`knative-overview.json`로 저장하세요. Grafana 사이드카로 ConfigMap을 읽으려면 라벨·네임스페이스 선택을 구성해야 하며 추가 `dashboard` 래퍼를 넣지 않습니다. 실제 Grafana 렌더링·메트릭 수집은 검증하지 않았습니다.
```bash
kubectl create configmap knative-serving-dashboard -n monitoring --from-file=knative-serving.json=knative-overview.json --dry-run=client -o yaml | kubectl apply -f -
kubectl label configmap knative-serving-dashboard -n monitoring grafana_dashboard=1 --overwrite
```
### 문제 해결
아래의 Revision 설정 패치는 실습 Service 전체에 새 설정을 적용하는 예제입니다. 기존 `spec.template.metadata.name`을 `null`로 제거해 새 Revision 이름을 자동 생성하고, 트래픽 100%를 준비된 최신 Revision으로 전환합니다. 기존 canary 분할이나 고정 Revision 라우팅을 유지해야 한다면 이 트래픽 설정을 그대로 적용하지 말고 별도 테스트 경로에서 검증한 뒤 전환하세요.
#### 콜드 스타트 지연
```bash
# 증상: Scale-to-Zero 후 첫 요청 응답이 느림
# 1. 콜드 스타트 시간 측정
kubectl logs -n knative-serving -l app=activator -c activator | grep "request buffered"
# 2. 이미지 풀 시간 확인
kubectl describe pod | grep -A5 "Events:"
# 3. 해결: 최소 인스턴스 설정
kubectl patch ksvc order-api -n knative-demo --type merge -p '
{
"spec": {
"template": {
"metadata": {
"name": null,
"annotations": {
"autoscaling.knative.dev/min-scale": "1"
}
}
},
"traffic": [
{ "latestRevision": true, "percent": 100 }
]
}
}'
# 4. 해결: 이미지 사전 캐싱 (DaemonSet)
kubectl apply -f - < -n knative-demo
# 3. 해결: 패닉 모드 임계값 조정
kubectl patch knativeserving knative-serving -n knative-serving --type merge --patch '{"spec":{"config":{"autoscaler":{"panic-window-percentage":"10","panic-threshold-percentage":"150"}}}}'
# 4. 해결: initialScale로 시작 파드 수 확보
kubectl patch ksvc order-api -n knative-demo --type merge -p '
{
"spec": {
"template": {
"metadata": {
"name": null,
"annotations": {
"autoscaling.knative.dev/initial-scale": "5"
}
}
},
"traffic": [
{ "latestRevision": true, "percent": 100 }
]
}
}'
```
#### Ingress/네트워킹 문제
```bash
# Kourier 상태 확인
kubectl get pods -n knative-serving
kubectl logs -n knative-serving -l app=3scale-kourier-gateway --tail=50
# Knative 서비스 URL 확인
kubectl get ksvc -n knative-demo
kubectl get king -n knative-demo # Knative Ingress 확인
# DNS 확인
nslookup order-api.knative-demo.knative.example.com
# Gateway diagnostics: keep this terminal open.
kubectl port-forward -n knative-serving svc/kourier 8080:80
```
다른 터미널에서 실제 Route 호스트로 요청합니다. TLS 리다이렉트 설정에 따라 리다이렉트 응답이 정상일 수 있으며 완전한 TLS 검증은 실제 Service URL로 수행하세요.
```bash
SERVICE_URL=$(kubectl get ksvc order-api -n knative-demo -o jsonpath='{.status.url}')
SERVICE_HOST=${SERVICE_URL#*://}
SERVICE_HOST=${SERVICE_HOST%%/*}
curl --fail --show-error -H "Host: ${SERVICE_HOST}" http://127.0.0.1:8080/
# Stop port-forward with Ctrl+C after diagnostics.
```
#### Eventing 이벤트 전달 실패
```bash
# Broker 상태 확인
kubectl get broker -n knative-demo
kubectl describe broker default -n knative-demo
# Trigger 상태 확인
kubectl get trigger -n knative-demo
kubectl describe trigger -n knative-demo
# 이벤트 소스 상태 확인
kubectl get sources -A
# Dead Letter Sink에 쌓인 이벤트 확인
kubectl logs -n knative-demo -l serving.knative.dev/service=dead-letter-handler --tail=50
# MTChannelBasedBroker example: first terminal.
kubectl port-forward -n knative-eventing svc/broker-ingress 8081:80
```
다른 터미널에서 전용 데모 Broker에만 테스트 이벤트를 보냅니다. 실제 실행 기록이 아닙니다. 인증을 활성화한 배포에서는 해당 인증도 필요합니다.
```bash
python3 - <<'PYCE'
import json, uuid
from pathlib import Path
Path("manual-event.json").write_text(json.dumps({
"specversion": "1.0", "type": "com.example.test",
"source": "urn:example:knative-demo:manual", "id": str(uuid.uuid4()),
"datacontenttype": "application/json", "data": {"message": "hello"}
}))
PYCE
curl --fail --show-error --request POST http://127.0.0.1:8081/knative-demo/default \
--header 'Content-Type: application/cloudevents+json' --data-binary @manual-event.json
# Stop port-forward with Ctrl+C after diagnostics.
```
***
## 모범 사례
### 서비스 설계 패턴
**1. 빠른 시작을 위한 경량 컨테이너 설계**
```dockerfile
# 권장: 멀티 스테이지 빌드로 이미지 크기 최소화
FROM golang:1.27.1 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
CMD ["/server"]
# 원문의 ~15MB는 검증되지 않은 예시 추정이며 실제 빌드 크기가 아닙니다.
```
**2. 상태 비저장(Stateless) 설계 원칙**
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: stateless-api
namespace: knative-demo
spec:
template:
spec:
containers:
- image: stateless-api:latest
env:
- name: REDIS_URL
value: redis://redis.cache:6379
- name: S3_BUCKET
value: my-app-data
- name: CACHE_ENDPOINT
value: cache.abc123.apne2.cache.amazonaws.com:6379
```
**3. Graceful Shutdown 구현**
```bash
exec gunicorn --bind 0.0.0.0:8080 --workers 1 --threads 4 \
--timeout 90 --graceful-timeout 60 'app:create_app()'
```
신호 처리기에서 즉시 종료하지 말고 WSGI 서버의 정상 종료 기능을 사용하세요. 아래 예시 값은 프록시 드레이닝과 서버 유예 시간보다 긴 Pod 종료 예산이 필요합니다. 요청 기한과 맞추고 부하 중 종료 동작을 검증해야 합니다.
### 이벤트 드리븐 마이크로서비스 패턴
CQRS·Event Sourcing에서는 명령을 승인하기 전에 원본 이벤트 또는 트랜잭션 outbox를 확정해야 합니다. Broker는 전달 계층이며 원본 이벤트 저장소가 아닙니다. 아래 비동기 보관 소비자만으로 트랜잭션이 성립하지 않습니다. 순서·멱등성·재생·실패 구간을 애플리케이션에서 검증해야 합니다.
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: command-api
namespace: knative-demo
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/target: '50'
spec:
containers:
- image: command-api:latest
---
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: event-archive-writer
labels:
networking.knative.dev/visibility: cluster-local
namespace: knative-demo
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/min-scale: '1'
spec:
containers:
- image: event-archive-writer:latest
---
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
name: event-store-trigger
namespace: knative-demo
spec:
broker: default
filter:
attributes:
source: command-api
subscriber:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: event-archive-writer
```
### 비용 최적화 (Scale-to-Zero 활용)
**1. 개발/스테이징 환경에서의 활용**
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: dev-api
namespace: knative-demo
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/min-scale: '0'
autoscaling.knative.dev/max-scale: '3'
autoscaling.knative.dev/scale-to-zero-pod-retention-period: 0s
spec:
containers:
- image: dev-api:latest
resources:
requests:
cpu: 100m
memory: 128Mi
```
**2. 비용 절감 효과 추정**
다음은 원문의 수치를 보존한 **출처 미확인 추정**이며 실측 결과나 현재 요금 견적이 아닙니다. Pod 시간과 청구되는 노드 시간은 다르고 컨트롤 플레인·스토리지·LB·약정 비용을 포함하지 않습니다.
| 환경 | 서비스 수 | 기존 방식 (Always-On) | Knative (Scale-to-Zero) | 절감률 |
| --------- | ----- | ----------------- | ----------------------- | ----- |
| 개발 | 30 | 30 파드 x 24시간 | 평균 5 파드 x 8시간 | \~83% |
| 스테이징 | 20 | 20 파드 x 24시간 | 평균 3 파드 x 12시간 | \~92% |
| 프로덕션 (야간) | 10 | 10 파드 x 24시간 | 야간 2 파드 x 8시간 | \~33% |
원문 표의 산술도 확정값으로 사용하면 안 됩니다. 나머지 시간의 Pod가0개라는 가정이면 개발은720→40 Pod시간으로 약94.4%, 스테이징은480→36으로92.5%입니다. 운영이 낮16시간10개·밤8시간2개라면240→176으로 약26.7%이며 원문의33%와 다릅니다. 이는 재측정이 아닌 명시적 가정에 따른 계산입니다.
### GPU 워크로드에서의 Knative
모델 PVC는 먼저 채우고 동시 Pod·노드·AZ를 지원하는 CSI/접근 모드(적합한 ReadOnlyMany/ReadWriteMany 등) 또는 Pod별 복사본을 사용하세요. RWO가 Pod1개를 뜻하지는 않지만 다른 노드 연결을 막을 수 있습니다. 읽기 전용 마운트이므로 persistent-volume-write는 켜지 않습니다. min-scale0은 콜드 활성화를 허용하며 양수 기본 용량은 지연·비용 정책에 따라 선택하세요. 평균 윈도우와 요청 타임아웃은 별개입니다.
노드 선택자·toleration·선택적 읽기 전용 PVC 예제에는 아래 PodSpec 확장이 필요합니다. serving-gpu-features.patch.yaml로 저장해 기존 Operator에 병합하세요. 참조한 gpu NodePool은 시스템·Queue Proxy 오버헤드를 뺀 CPU/메모리/GPU 용량과 드라이버·디바이스 플러그인을 제공해야 합니다. 모델 서빙·GPU 사이징 실측 결과가 아닌 미검증 예제입니다.
```yaml
spec:
config:
features:
kubernetes.podspec-nodeselector: enabled
kubernetes.podspec-tolerations: enabled
kubernetes.podspec-persistent-volume-claim: enabled
```
```bash
kubectl patch knativeserving knative-serving -n knative-serving --type merge --patch-file serving-gpu-features.patch.yaml
```
ML 추론 서비스에 Knative를 사용하면 GPU 비용을 크게 절감할 수 있습니다.
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: ml-inference
namespace: knative-demo
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/min-scale: '0'
autoscaling.knative.dev/max-scale: '10'
autoscaling.knative.dev/target: '1'
autoscaling.knative.dev/metric: concurrency
autoscaling.knative.dev/scale-down-delay: 30m
autoscaling.knative.dev/window: 300s
spec:
containerConcurrency: 1
timeoutSeconds: 600
containers:
- image: ml-inference:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: '4'
memory: 16Gi
nvidia.com/gpu: '1'
limits:
cpu: '6'
memory: 24Gi
nvidia.com/gpu: '1'
env:
- name: MODEL_PATH
value: /models/llama-7b
volumeMounts:
- name: model-cache
mountPath: /models
readOnly: true
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: model-cache-pvc
readOnly: true
nodeSelector:
karpenter.sh/nodepool: gpu
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
```
**GPU Scale-to-Zero 비용 절감 예시:**
아래 `$3.06/시간`과 일별 금액은 원문의 가정 단가를 보존한 계산이며 현재 리전별 가격 또는 실측 청구액이 아닙니다. Pod 종료만으로 GPU 노드가 즉시 종료되지 않습니다. min-scale·지연·캐시·스토리지·약정·기본 용량을 포함해 실제 비용을 확인해야 합니다.
* GPU 인스턴스 (p3.2xlarge): 약 $3.06/시간
* 하루 추론 요청: 8시간 x 불규칙적 (실제 GPU 사용 약 4시간)
* Always-On: $3.06 x 24 = $73.44/일
* Scale-to-Zero: $3.06 x 4 = $12.24/일 (약 83% 절감)
***
## 참고 문서
### 공식 문서
* [Knative 공식 문서](https://knative.dev/docs/)
* [Knative GitHub 저장소](https://github.com/knative)
* [Knative Serving API 명세](https://knative.dev/docs/reference/api/serving-api/)
* [Knative Eventing API 명세](https://knative.dev/docs/reference/api/eventing-api/)
* [Kourier GitHub 저장소](https://github.com/knative-extensions/net-kourier)
* [CloudEvents 사양](https://cloudevents.io/)
* [CNCF Knative 프로젝트 페이지](https://www.cncf.io/projects/knative/)
### AWS 관련 문서
* [Amazon EKS에서 Knative 실행](https://aws.amazon.com/blogs/containers/)
* [AWS Controllers for Kubernetes (ACK)](https://aws-controllers-k8s.github.io/community/)
* [Knative IntegrationSource SQS](https://knative.dev/docs/eventing/sources/integration-source/aws_sqs/)
### 관련 내부 문서
* [KEDA (Kubernetes Event-driven Autoscaling)](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/01-keda.md) - 이벤트 기반 스케일링 도구
* [Karpenter](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/02-karpenter.md) - 노드 레벨 오토스케일링
* [EKS 비용 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/eks/07-eks-cost-optimization.md) - EKS 환경에서의 비용 최적화 전략
* [Istio Traffic Management](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/istio/traffic-management/README.md) - 서비스 메시 기반 트래픽 관리
* [Prometheus](https://www.atomai.click/kubernetes-docs/llms/ko/observability/metrics/01-prometheus.md) - Knative 메트릭 수집 및 모니터링
* [cert-manager](https://www.atomai.click/kubernetes-docs/llms/ko/security/10-cert-manager.md) - TLS 인증서 자동 관리
***
## 결론
Knative는 Kubernetes 위에 요청 기반 확장·트래픽 관리·CloudEvents 전달 기능을 제공합니다. CNCF Graduated 상태가 이 예제의 운영 안정성·비용 효과를 입증하지는 않습니다. 실제 워크로드·의존성·장애 복구를 별도로 검증해야 합니다.
이 문서에서는 Knative의 아키텍처, EKS에서의 설치 및 구성, Serving과 Eventing의 심화 사용법, KEDA와의 비교, 프로덕션 운영 전략에 대해 살펴보았습니다.
### 다음 단계
* Knative Serving을 사용한 서버리스 API 배포 실습
* Knative Eventing을 사용한 이벤트 드리븐 마이크로서비스 파이프라인 구축
* KEDA와 Knative를 함께 사용하는 하이브리드 아키텍처 설계
* GPU 워크로드에서의 Scale-to-Zero를 통한 비용 최적화
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/autoscaling/03-knative-quiz)를 풀어보세요.
< [이전: Karpenter](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/02-karpenter.md) | 다음: 없음 >
이번 수정에서 확인한 공식 자료: [Serving1.23](https://github.com/knative/serving/releases/tag/knative-v1.23.0), [Operator1.23.1](https://github.com/knative/operator/releases/tag/knative-v1.23.1), [CNCF milestone](https://www.cncf.io/projects/knative/), [Operator configuration](https://knative.dev/docs/install/operator/configuring-serving-cr/), [Scale-to-zero semantics](https://knative.dev/docs/serving/autoscaling/scale-to-zero/), [HPA implementation](https://github.com/knative/serving/blob/knative-v1.23.0/pkg/reconciler/autoscaling/hpa/resources/hpa.go), [SQS IntegrationSource](https://knative.dev/docs/eventing/sources/integration-source/aws_sqs/), [CloudEvents HTTP binding](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/bindings/http-protocol-binding.md), [S3 conditional put](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/put_object.html), [Serving metrics](https://knative.dev/docs/serving/observability/metrics/serving-metrics/), [Prometheus OTLP configuration](https://github.com/prometheus/prometheus/blob/main/docs/configuration/configuration.md).
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/01-eks-introduction
----------------------------------------
# EKS 소개
> **지원 버전**: Amazon EKS 표준 지원 1.34–1.36, 연장 지원 1.31–1.33
> **마지막 업데이트**: 2026년 9월 11일
Amazon Elastic Kubernetes Service(EKS)는 AWS에서 Kubernetes를 실행하기 위한 관리형 서비스입니다. 이 장에서는 EKS의 기본 개념, 아키텍처, 그리고 일반 Kubernetes와의 차이점을 살펴보겠습니다.
## EKS와 Kubernetes
EKS는 표준 Kubernetes API를 제공하는 관리형 서비스입니다. Kubernetes의 기본 개념과 작동 방식에 대한 자세한 내용은 [Kubernetes 소개](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md) 문서를 참조하세요.
### EKS의 주요 이점
1. **관리형 컨트롤 플레인**: AWS가 Kubernetes 컨트롤 플레인의 가용성과 확장성을 관리
2. **보안 강화**: AWS IAM과의 통합을 통한 인증 및 권한 부여
3. **AWS 서비스 통합**: 다른 AWS 서비스(ELB, ECR, IAM 등)와의 원활한 통합
4. **다양한 컴퓨팅 옵션**: EC2 기반 노드, EKS Auto Mode, Fargate, Hybrid Nodes 지원. Bottlerocket은 별도 컴퓨팅 서비스가 아니라 노드 운영체제
5. **자동 확장**: 클러스터 오토스케일러, Karpenter 등을 통한 자동 확장 지원
6. **관리형 노드 그룹**: 노드 수명 주기 관리 자동화
## EKS 아키텍처 및 구성 요소
Amazon EKS의 전체 아키텍처는 다음과 같습니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-01-eks-introduction-10.html)
### 컨트롤 플레인
EKS는 고가용성 컨트롤 플레인을 제공합니다. 컨트롤 플레인은 여러 가용 영역에 걸쳐 실행되며, 다음과 같은 구성 요소로 이루어져 있습니다:
* **API 서버**: Kubernetes API를 노출하고 클러스터와의 상호 작용을 처리합니다.
* **etcd**: 클러스터의 상태를 저장하는 분산 키-값 저장소입니다.
* **컨트롤러 매니저**: 클러스터의 상태를 관리하는 컨트롤러를 실행합니다.
* **스케줄러**: 포드를 노드에 할당합니다.
EKS에서는 이러한 컨트롤 플레인 구성 요소가 AWS에 의해 관리되므로, 사용자는 이를 직접 관리할 필요가 없습니다.
### 데이터 플레인
EKS 데이터 플레인은 다음과 같은 옵션으로 구성할 수 있습니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-01-eks-introduction-11.html)
1. **관리형 노드 그룹**: AWS가 EC2 노드의 프로비저닝·교체 과정을 관리합니다. 운영자가 노드 버전/AMI 업데이트를 선택하고 시작하며, 컨트롤 플레인 업그레이드만으로 이 노드들이 자동 갱신되지는 않습니다.
2. **자체 관리형 노드**: 사용자가 직접 관리하는 EC2 인스턴스입니다.
3. **AWS Fargate**: Fargate 프로필로 선택하는 Pod 단위 컴퓨팅입니다. 워크로드 구성과 리소스 요청은 운영자가 관리합니다.
4. **EKS Auto Mode**: AWS가 EC2 노드 프로비저닝·확장·업데이트와 지원되는 네트워킹·로드 밸런싱·블록 스토리지 기능을 관리합니다.
5. **Hybrid Nodes**: 고객이 관리하는 온프레미스/엣지 머신을 AWS의 EKS 컨트롤 플레인에 연결합니다. 안정적인 네트워크 연결이 필요합니다.
### 네트워킹
일반 EC2 기반 EKS 노드에서는 Amazon VPC CNI가 기본이며 일반 Pod에 VPC 주소를 할당합니다. `hostNetwork` Pod는 노드 네트워크를 공유합니다. Auto Mode는 자체 네트워킹 기능을 관리하고, Hybrid Nodes는 Amazon VPC CNI 대신 호환되는 온프레미스 CNI를 사용합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-01-eks-introduction-12.html)
## 일반 Kubernetes와 EKS의 차이점
### 관리 책임
* **자체 관리형 Kubernetes**: 운영자가 컨트롤 플레인과 데이터 플레인을 관리합니다. 다른 관리형 배포판은 책임 분담이 다를 수 있습니다.
* **EKS**: AWS가 컨트롤 플레인을 관리합니다. 데이터 플레인 책임은 컴퓨팅 옵션에 따라 다르며, 워크로드 보안·신원·구성·가용성·데이터 보호는 고객 책임으로 남습니다.
### 네트워킹
* **일반 Kubernetes**: 다양한 CNI 플러그인 중에서 선택할 수 있습니다.
* **EKS**: 일반 EC2 기반 클러스터의 기본은 Amazon VPC CNI입니다. 대체 CNI와 Auto Mode/Hybrid Nodes에는 서로 다른 기능·지원 제약이 있습니다.
### 로드 밸런싱
* **일반 Kubernetes**: `LoadBalancer` 타입의 서비스를 사용하려면 별도의 컨트롤러를 설치해야 합니다.
* **EKS**: 일반 클러스터에서는 NLB Service와 ALB Ingress를 위해 AWS Load Balancer Controller를 설치하고 권한을 부여합니다. 레거시 컨트롤러는 Classic Load Balancer를 생성할 수 있습니다. Auto Mode는 별도 클래스와 지원 설정을 사용하는 관리형 NLB/ALB 통합을 제공합니다. Service 타입만으로 담당 컨트롤러가 정해지는 것은 아닙니다. Fargate는 ALB/NLB의 IP 타깃을 지원합니다.
### 스토리지
* **일반 Kubernetes**: 다양한 스토리지 드라이버를 수동으로 설치하고 구성해야 합니다.
* **EKS**: 일반 클러스터에서는 EBS CSI 드라이버/애드온을 설치하고 IAM 권한을 부여합니다. Auto Mode의 관리형 EBS 프로비저너는 `ebs.csi.eks.amazonaws.com`으로, 일반 드라이버의 `ebs.csi.aws.com`과 다릅니다. EFS·FSx 통합에는 별도 선행 조건이 있으며, Fargate와 Hybrid Nodes에서는 EBS 볼륨을 마운트할 수 없습니다.
## EKS 비용 구조
EKS 클러스터를 운영할 때 발생하는 비용은 다음과 같습니다:
1. **EKS 컨트롤 플레인 비용**: 클러스터당 시간 요금은 표준/연장 지원에 따라 다르며, 선택한 프로비저닝 컨트롤 플레인 등급에는 추가 요금이 있습니다.
2. **컴퓨팅 비용**:
* EC2 인스턴스(관리형 또는 자체 관리형 노드)
* Fargate(포드 실행 시간 및 리소스 사용량에 따라 요금 부과)
3. **스토리지 비용**: EBS, EFS, FSx 등의 스토리지 서비스 사용 비용
4. **네트워크 비용**: 데이터 전송, NAT 게이트웨이, 퍼블릭 IPv4 주소, 로드 밸런서 사용 비용
5. **관리형 기능 비용**: Auto Mode, EKS Capabilities, Hybrid Nodes에는 해당 클러스터/인프라 비용 외에 별도 요금이 있습니다.
### 비용 최적화 전략
1. **Spot 인스턴스 사용**: 현재 Spot 가격과 중단 허용 범위를 비교합니다. 광고된 절감률이 개별 워크로드의 절감률을 보장하지는 않습니다.
2. **Fargate 평가**: 프로비저닝된 Pod 크기, 실행 시간, 기능 제약, 운영 부담을 EC2와 비교합니다. 앱 사용률이 낮다는 이유만으로 저렴해지지는 않습니다.
3. **오토스케일링 구성**: 필요에 따라 노드를 자동으로 확장하고 축소합니다.
4. **Locality Routing**: 지원되는 경우 같은 AZ의 트래픽을 우선하되 충분한 용량과 장애 전환을 유지합니다. 실제 전송 비용 절감은 라우팅·로드 밸런서 설정에 따라 달라집니다.
5. **EKS Auto Mode**: 자동 확장·통합의 절감 효과와 Auto Mode 관리 요금을 함께 평가합니다.
6. **Hybrid Nodes**: 기존 온프레미스 용량을 vCPU당 Hybrid Nodes 요금 및 연결·운영 비용과 함께 평가합니다. EC2 인스턴스 유형 혼합을 뜻하는 기능이 아닙니다.
## AWS 서비스와의 통합
EKS는 다음과 같은 AWS 서비스와 통합됩니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-01-eks-introduction-0.html)
1. **IAM**: IAM 신원이 클러스터에 인증하고 EKS access entry의 접근 정책 및/또는 Kubernetes RBAC 그룹으로 권한을 부여합니다. 워크로드의 AWS 권한은 Pod Identity·IRSA로 별도 구성합니다.
2. **VPC**: 네트워킹 인프라를 제공합니다.
3. **CloudWatch**: 모니터링 및 로깅을 제공합니다.
4. **ALB/NLB**: 로드 밸런싱을 제공합니다.
5. **ECR**: 컨테이너 이미지 저장소를 제공합니다.
6. **EBS/EFS/FSx**: 영구 스토리지를 제공합니다.
7. **AWS App Mesh**: 기존 통합은 마이그레이션을 계획해야 합니다. AWS 지원 종료일은 2026년 9월 30일이며 신규 배포 대상으로 선택하지 않습니다.
8. **AWS Certificate Manager**: SSL/TLS 인증서를 관리합니다.
9. **AWS Secrets Manager**: 민감한 정보를 안전하게 저장하고 관리합니다.
10. **AWS SageMaker**: 머신 러닝 워크로드를 실행합니다.
11. **AWS Bedrock**: 생성형 AI 모델을 활용합니다.
## EKS 모범 사례
1. **클러스터 설계**:
* 다중 가용 영역에 노드 배포
* 적절한 인스턴스 유형 선택
* 노드 그룹 전략 수립
2. **보안**:
* 최소 권한 원칙 적용
* 네트워크 정책 구현
* Pod Security Admission 및/또는 admission 정책으로 Pod Security Standards 적용. PodSecurityPolicy API는 Kubernetes 1.25에서 제거됨
* 이미지 스캐닝 및 취약점 관리
3. **네트워킹**:
* 적절한 서브넷 설계
* 보안 그룹 구성
* Locality Routing 활용
4. **모니터링 및 로깅**:
* CloudWatch 컨테이너 인사이트 활성화
* 컨트롤 플레인 로깅 구성
* 프로메테우스 및 그라파나 활용
5. **업그레이드 전략**:
* 정기적인 업그레이드 계획
* 블루/그린 배포 전략 고려
* 업그레이드 전 테스트 수행
## 공식 참고 자료
- [EKS version lifecycle](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html)
- [Compute and shared responsibilities](https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html)
- [AWS Load Balancer Controller](https://docs.aws.amazon.com/eks/latest/userguide/aws-load-balancer-controller.html)
- [EBS CSI and Auto Mode](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html)
- [Hybrid Nodes](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-overview.html)
- [Fargate considerations](https://docs.aws.amazon.com/eks/latest/userguide/fargate.html)
- [App Mesh support notice](https://docs.aws.amazon.com/app-mesh/latest/userguide/what-is-app-mesh.html)
- [EKS pricing](https://aws.amazon.com/eks/pricing/)
- [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/)
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [Amazon EKS 소개 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/01-eks-introduction-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/02-eks-cluster-creation
----------------------------------------
# EKS 클러스터 생성
> **마지막 업데이트**: 2026년 9월 11일
Amazon EKS 클러스터를 생성하는 방법은 여러 가지가 있습니다. 이 장에서는 다양한 도구와 방법을 사용하여 EKS 클러스터를 생성하는 방법을 자세히 알아보겠습니다.
이 장의 생성 방법은 대안입니다. 한 방법을 선택해 새 전용 클러스터에 적용하고 같은 리소스를 여러 도구로 동시에 관리하지 않습니다. 예시 이름·계정·역할·VPC·서브넷·CIDR은 승인된 실제 값으로 바꿉니다. 별도 표시가 없는 셸 예제는 Bash 기준이며 선행 명령 실패 시 중단합니다. 실제 AWS 프로비저닝이나 워크로드 실측은 이 감사에서 수행하지 않았습니다.
## 목차
1. [사전 요구 사항](#사전-요구-사항)
2. [eksctl을 사용한 클러스터 생성](#eksctl을-사용한-클러스터-생성)
3. [AWS Management Console을 사용한 클러스터 생성](#aws-management-console을-사용한-클러스터-생성)
4. [AWS CLI를 사용한 클러스터 생성](#aws-cli를-사용한-클러스터-생성)
5. [Terraform을 사용한 클러스터 생성](#terraform을-사용한-클러스터-생성)
6. [AWS CDK를 사용한 클러스터 생성](#aws-cdk를-사용한-클러스터-생성)
7. [클러스터 액세스 구성](#클러스터-액세스-구성)
8. [클러스터 검증](#클러스터-검증)
9. [클러스터 업그레이드](#클러스터-업그레이드)
10. [클러스터 삭제](#클러스터-삭제)
## 사전 요구 사항
EKS 클러스터를 생성하기 전에 다음과 같은 사전 요구 사항이 필요합니다:
### 1. AWS 계정
유효한 AWS 계정이 필요합니다. AWS 계정이 없는 경우 [AWS 웹사이트](https://aws.amazon.com/)에서 가입할 수 있습니다.
### 2. IAM 권한
필요한 권한은 생성 도구와 직접 관리할 리소스에 따라 달라집니다. `eks:*`, `ec2:*`, `iam:*`, `cloudformation:*`를 모든 리소스에 부여하는 정책을 필수 최소 권한으로 취급하지 않습니다.
| 작업 | 검토할 권한 범위 |
| --- | --- |
| EKS 클러스터·노드 그룹 관리 | 필요한 EKS 작업과 대상 리소스 |
| 기존 IAM 역할 전달 | 승인된 역할 ARN의 `iam:PassRole` 및 서비스 조건 |
| IAM 역할·정책·OIDC 제공자 생성 | 도구가 관리하는 IAM 리소스와 이름·태그 범위 |
| 네트워크 생성 | 새 VPC·서브넷·보안 그룹에 필요한 EC2 작업 |
| eksctl/CDK 사용 | 해당 CloudFormation 스택, 실행 역할, 부트스트랩 리소스 |
프로비저닝 사용자, 클러스터 서비스 역할, 노드 역할은 별개입니다. SCP·권한 경계·세션 정책도 적용됩니다. 합성된 템플릿/계획을 기준으로 조직의 프로비저닝 권한을 검토합니다. Auto Mode의 역할 요구 사항은 일반 노드 그룹과 다릅니다.
참고: [EKS IAM 작업과 리소스](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonelastickubernetesservice.html), [Auto Mode 역할](https://docs.aws.amazon.com/eks/latest/userguide/auto-cluster-iam-role.html).
### 3. 도구 설치
#### AWS CLI
[AWS CLI v2 공식 설치 지침](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)에서 OS/CPU에 맞는 패키지를 선택하고 서명 검증 절차를 따릅니다. 이전 v1/v2 설치가 있다면 업데이트·마이그레이션 절차를 먼저 확인합니다.
| 환경 | 공식 패키지/설치 방식 |
| --- | --- |
| macOS | 서명된 `AWSCLIV2.pkg` |
| Linux x86_64 | `awscli-exe-linux-x86_64.zip` 및 PGP 서명 검증 |
| Linux ARM64 | `awscli-exe-linux-aarch64.zip` 및 PGP 서명 검증 |
| Windows | 지원되는 Windows용 MSI 설치 프로그램 |
Linux x86_64 패키지를 ARM 시스템에 그대로 사용하지 않습니다. 설치 후 `aws --version`으로 실제 실행되는 CLI를 확인합니다. 조직에서 IAM Identity Center를 사용하는 경우 다음과 같이 승인된 프로필을 구성합니다. 다른 페더레이션 방식을 사용하는 조직은 그 절차를 따르며 장기 액세스 키를 전제로 하지 않습니다.
```bash
aws configure sso --profile eks-docs
aws sso login --profile eks-docs
aws sts get-caller-identity --profile eks-docs
export AWS_PROFILE=eks-docs
```
참고: [IAM Identity Center 인증](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html). 이후 명령에서도 승인된 계정·역할·리전을 유지합니다.
#### kubectl과 eksctl — Linux/macOS
이 장의 EKS 1.36 예제에는 kubectl **1.36.4**, eksctl **0.230.0**을 기준으로 합니다. kubectl은 서버와 같은 마이너 버전을 권장하며 허용되는 차이는 ±1 마이너입니다. 업스트림 `stable.txt`의 최신 마이너를 이전 EKS 클러스터에 무조건 설치하지 않습니다.
아래 Bash 예제는 AMD64/ARM64를 구분하고 공식 체크섬을 확인한 뒤 설치합니다. `curl`, `tar`, `awk`와 `sha256sum` 또는 `shasum`이 필요하며 `/usr/local/bin` 설치는 관리자 권한이 필요합니다. 다운로드/검증 실패 시 설치를 중단합니다.
```bash
(
set -e
case "$(uname -s)" in
Linux) EKS_TOOL_OS=linux; EKS_ARCHIVE_OS=Linux ;;
Darwin) EKS_TOOL_OS=darwin; EKS_ARCHIVE_OS=Darwin ;;
*) printf 'Use the official installer for this operating system\n' >&2; exit 1 ;;
esac
case "$(uname -m)" in
x86_64) EKS_TOOL_ARCH=amd64 ;;
aarch64|arm64) EKS_TOOL_ARCH=arm64 ;;
*) printf 'Select a supported CPU architecture\n' >&2; exit 1 ;;
esac
EKS_TOOL_ARCHIVE="eksctl_${EKS_ARCHIVE_OS}_${EKS_TOOL_ARCH}.tar.gz"
EKS_TOOL_DIR=$(mktemp -d)
: "${EKS_TOOL_DIR:?}"
trap 'rm -f -- "$EKS_TOOL_DIR/kubectl" "$EKS_TOOL_DIR/kubectl.sha256" "$EKS_TOOL_DIR/eksctl" "$EKS_TOOL_DIR/eksctl_checksums.txt" "$EKS_TOOL_DIR/$EKS_TOOL_ARCHIVE" "$EKS_TOOL_DIR/selected.sha256"; rmdir -- "$EKS_TOOL_DIR"' EXIT
cd "$EKS_TOOL_DIR" || exit 1
verify_sha() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum --check "$1"
else
shasum -a 256 --check "$1"
fi
}
EKS_KUBECTL_VERSION=v1.36.4
curl -fL "https://dl.k8s.io/release/$EKS_KUBECTL_VERSION/bin/$EKS_TOOL_OS/$EKS_TOOL_ARCH/kubectl" -o kubectl || exit 1
curl -fL "https://dl.k8s.io/release/$EKS_KUBECTL_VERSION/bin/$EKS_TOOL_OS/$EKS_TOOL_ARCH/kubectl.sha256" -o kubectl.sha256 || exit 1
printf '%s kubectl\n' "$(tr -d '[:space:]' < kubectl.sha256)" > selected.sha256
verify_sha selected.sha256 || exit 1
EKSCTL_VERSION=0.230.0
curl -fL "https://github.com/eksctl-io/eksctl/releases/download/v$EKSCTL_VERSION/$EKS_TOOL_ARCHIVE" -o "$EKS_TOOL_ARCHIVE" || exit 1
curl -fL "https://github.com/eksctl-io/eksctl/releases/download/v$EKSCTL_VERSION/eksctl_checksums.txt" -o eksctl_checksums.txt || exit 1
awk -v name="$EKS_TOOL_ARCHIVE" '$2 == name {print; count++} END {if (count != 1) exit 1}' \
eksctl_checksums.txt > selected.sha256 || exit 1
verify_sha selected.sha256 || exit 1
tar -xzf "$EKS_TOOL_ARCHIVE" eksctl || exit 1
sudo install -m 0755 kubectl /usr/local/bin/kubectl || exit 1
sudo install -m 0755 eksctl /usr/local/bin/eksctl || exit 1
kubectl version --client
eksctl version
)
```
공식 절차: [Linux kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/), [macOS kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl-macos/), [eksctl 설치](https://eksctl.io/installation/).
#### Windows
PowerShell에서 [공식 kubectl 설치 절차](https://kubernetes.io/docs/tasks/tools/install-kubectl-windows/)를 따릅니다. 이 장의 AMD64 예제에는 [kubectl 1.36.4](https://dl.k8s.io/release/v1.36.4/bin/windows/amd64/kubectl.exe)와 같은 경로의 `.sha256` 파일을 사용합니다. [eksctl 0.230.0 릴리스](https://github.com/eksctl-io/eksctl/releases/tag/v0.230.0)에서 CPU에 맞는 Windows ZIP과 `eksctl_checksums.txt`를 선택합니다.
`Get-FileHash -Algorithm SHA256` 결과를 공식 해시와 비교하고 불일치하면 중단합니다. 검증 후 ZIP을 풀고 실행 파일 디렉터리를 PATH에 추가한 뒤 `kubectl version --client`, `eksctl version`을 확인합니다. PowerShell 구문을 Bash로 실행하지 않습니다.
AWS CLI 예제의 JSON 생성을 위해서는 `jq`도 준비합니다. 실제 다운로드·설치·로그인은 감사에서 실행하지 않았습니다.
### 4. VPC 및 서브넷
리전 EKS 클러스터에는 같은 VPC의 서로 다른 AZ에 있는 서브넷이 최소 2개 필요합니다. 각 클러스터 서브넷은 EKS용 IP가 최소 6개 남아 있어야 하며 AWS는 16개 이상을 권장합니다. 노드·Pod·로드 밸런서·업그레이드에 필요한 IP는 별도로 계획합니다. VPC DNS 호스트명과 DNS 해석도 활성화해야 합니다.
인터넷 경로가 모든 EKS 클러스터의 필수 조건은 아닙니다. 노드와 워크로드가 API·이미지·필요한 AWS 서비스에 접근할 수 있어야 하며, NAT/인터넷 경로나 필요한 VPC 엔드포인트 및 미러 이미지를 준비합니다. 프라이빗 Kubernetes 엔드포인트는 VPC 또는 연결된 네트워크에서 올바른 DNS·라우팅으로 접근합니다.
#### EKS 클러스터를 위한 VPC 태그
`kubernetes.io/cluster/` VPC 태그는 오래된 클러스터의 레거시 방식이며 현재 EKS 생성의 보편적 필수 조건이 아닙니다. 로드 밸런서의 서브넷 자동 검색에는 선택한 컨트롤러의 규칙을 따릅니다:
- 퍼블릭 로드 밸런서용 서브넷: `kubernetes.io/role/elb=1`
- 내부 로드 밸런서용 서브넷: `kubernetes.io/role/internal-elb=1`
태그만으로 라우팅·보안 그룹·가용 IP가 구성되지는 않습니다. [VPC/서브넷 요구 사항](https://docs.aws.amazon.com/eks/latest/userguide/network-reqs.html)과 [인터넷 없이 운영하는 클러스터](https://docs.aws.amazon.com/eks/latest/userguide/private-clusters.html)를 확인합니다.
## eksctl을 사용한 클러스터 생성
eksctl은 EKS 클러스터를 생성하고 관리하기 위한 가장 간단한 방법입니다. eksctl은 CloudFormation을 사용하여 EKS 클러스터와 관련 리소스를 생성합니다.
eksctl의 kubeconfig는 이 셸의 전용 경로에 저장합니다.
```bash
EKS_CLIENT_DIR=$(mktemp -d /tmp/eks-client.XXXXXX)
: "${EKS_CLIENT_DIR:?}"
EKS_KUBECONFIG="$EKS_CLIENT_DIR/kubeconfig"
export KUBECONFIG="$EKS_KUBECONFIG"
```
### 기본 클러스터 생성
검토한 구성 파일로 기본 클러스터를 생성합니다:
```bash
eksctl create cluster --config-file cluster.yaml --kubeconfig "${EKS_KUBECONFIG:?}"
```
명령 실행 전에 아래 `cluster.yaml`을 읽고 수정합니다. EKS 1.36, AL2023, 기존 VPC 서브넷과 노드 그룹 용량을 명시하는 예제입니다. 예시 식별자와 문서용 CIDR을 승인된 실제 값으로 바꿉니다. 모든 eksctl 버전의 기본값을 설명하는 목록이 아닙니다.
### 구성 파일을 사용한 클러스터 생성
더 복잡한 구성의 경우 YAML 파일을 사용하여 클러스터를 정의할 수 있습니다:
```yaml
# cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-cluster
region: us-west-2
version: '1.36'
vpc:
id: vpc-12345678
subnets:
private:
us-west-2a:
id: subnet-12345678
us-west-2b:
id: subnet-87654321
public:
us-west-2a:
id: subnet-23456789
us-west-2b:
id: subnet-98765432
clusterEndpoints:
privateAccess: true
publicAccess: true
publicAccessCIDRs:
- 203.0.113.10/32
managedNodeGroups:
- name: ng-1
instanceType: m5.large
desiredCapacity: 2
minSize: 1
maxSize: 3
privateNetworking: true
volumeSize: 80
volumeType: gp3
amiFamily: AmazonLinux2023
disableIMDSv1: true
- name: ng-2
instanceType: c5.xlarge
desiredCapacity: 2
privateNetworking: true
spot: true
amiFamily: AmazonLinux2023
disableIMDSv1: true
cloudWatch:
clusterLogging:
enableTypes:
- api
- audit
- authenticator
- controllerManager
- scheduler
fargateProfiles:
- name: fp-default
selectors:
- namespace: default
labels:
env: fargate
iam:
withOIDC: true
accessConfig:
authenticationMode: API
```
이 구성 파일을 사용하여 클러스터를 생성하려면 다음 명령을 실행합니다:
```bash
eksctl create cluster -f cluster.yaml --kubeconfig "${EKS_KUBECONFIG:?}"
```
위 구성은 EC2 노드와 선택적 앱 Fargate 프로필을 보여 줍니다. CoreDNS는 EC2에 두며, Fargate로 옮기려면 프로필 외에 CoreDNS의 컴퓨팅 설정도 검토해야 합니다.
### 관리형 노드 그룹 생성
기존 클러스터에 관리형 노드 그룹을 추가하려면 다음 명령을 실행합니다:
```bash
eksctl create nodegroup \
--cluster my-cluster \
--region us-west-2 \
--name my-nodegroup \
--node-type m5.large \
--nodes 3 \
--nodes-min 1 \
--nodes-max 5 \
--managed --node-ami-family AmazonLinux2023 --node-private-networking
```
또는 구성 파일을 사용할 수 있습니다:
```yaml
# nodegroup.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-cluster
region: us-west-2
managedNodeGroups:
- name: my-nodegroup
instanceType: m5.large
desiredCapacity: 3
minSize: 1
maxSize: 5
volumeSize: 80
volumeType: gp3
amiFamily: AmazonLinux2023
privateNetworking: true
disableIMDSv1: true
```
```bash
eksctl create nodegroup -f nodegroup.yaml
```
### EKS Auto Mode 클러스터 생성
EKS Auto Mode는 2024년에 출시된 새로운 기능으로, Kubernetes 클러스터 인프라를 자동화하여 운영 오버헤드를 크게 줄입니다. Auto Mode는 컴퓨팅, 네트워킹, 스토리지 등의 인프라 관리를 AWS가 자동으로 처리합니다.
#### EKS Auto Mode의 주요 특징
- **자동화된 노드 관리**: 워크로드 요구사항에 따라 자동으로 노드를 추가/제거
- **보안 강화**: 불변 AMI, SELinux 강제 모드, 읽기 전용 루트 파일 시스템
- **노드 유지 보수**: 노드 만료·드리프트에 따라 교체합니다. 21일은 클러스터 마이너 버전 업그레이드 주기가 아닙니다
- **통합 구성 요소**: Pod 네트워킹, DNS, 스토리지, GPU 지원 등이 기본 제공
- **비용 최적화**: 사용하지 않는 인스턴스 자동 종료 및 워크로드 통합
#### 기본 Auto Mode 클러스터 생성
아래 `auto-cluster.yaml`의 CIDR과 네트워크 설정을 먼저 검토합니다. Auto Mode의 네트워킹·DNS·블록 스토리지를 자체 관리형 애드온으로 중복 설치하지 않습니다. 혼합 클러스터는 컴퓨팅별 배치와 구성 요소 적용 범위를 별도로 설계합니다.
```bash
eksctl create cluster --config-file auto-cluster.yaml --kubeconfig "${EKS_KUBECONFIG:?}"
```
#### 구성 파일을 사용한 Auto Mode 클러스터 생성
```yaml
# auto-cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-auto-cluster
region: us-west-2
version: '1.36'
autoModeConfig:
enabled: true
nodePools:
- system
- general-purpose
vpc:
cidr: 10.0.0.0/16
nat:
gateway: Single
clusterEndpoints:
privateAccess: true
publicAccess: true
publicAccessCIDRs:
- 203.0.113.10/32
cloudWatch:
clusterLogging:
enableTypes:
- api
- audit
- authenticator
- controllerManager
- scheduler
accessConfig:
authenticationMode: API
```
클러스터 생성:
```bash
eksctl create cluster -f auto-cluster.yaml --kubeconfig "${EKS_KUBECONFIG:?}"
```
#### Auto Mode vs 기존 방식 비교
| 기능 | 기존 EKS | EKS Auto Mode |
|------|----------|---------------|
| 노드 관리 | 관리형 노드 그룹 또는 고객 관리 노드 | AWS 관리형 노드 수명 주기 |
| 스케일링 | Cluster Autoscaler·자체 관리 Karpenter 등 구성 | 관리형 노드 자동 확장 |
| 업그레이드 | 제어면·노드·애드온을 계획하여 갱신 | 노드/관리 구성 요소를 AWS가 갱신; 제어면 마이너 버전은 지원 정책에 따라 계획 |
| 보안 | 사용자 구성 | 강화된 보안 기본 제공 |
| 네트워킹 | CNI 플러그인 설정 | 자동 네트워킹 구성 |
| 스토리지 | CSI 드라이버 설치·권한 필요 | 관리형 EBS 프로비저너 `ebs.csi.eks.amazonaws.com` |
| GPU 지원 | 호환되는 가속 AMI와 필요한 디바이스 플러그인 구성 | 지원 인스턴스의 드라이버·플러그인 관리 |
#### Auto Mode 클러스터 검증
클러스터가 생성된 후 다음 명령으로 상태를 확인할 수 있습니다:
```bash
# 클러스터 상태 확인
kubectl get nodes
# Auto Mode 노드 풀 확인
kubectl get nodepools
# Auto Mode 노드 클래스 확인
kubectl get nodeclasses
# 시스템 파드 상태 확인
kubectl get pods -n kube-system
```
#### 커스텀 노드 풀 생성
Auto Mode에서는 기본 노드 풀 외에 커스텀 노드 풀을 생성할 수 있습니다:
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: gpu-nodepool
spec:
template:
metadata:
labels:
workload-type: gpu
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
- key: node.kubernetes.io/instance-type
operator: In
values:
- p3.2xlarge
- p3.8xlarge
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
expireAfter: 336h
taints:
- key: nvidia.com/gpu
value: present
effect: NoSchedule
limits:
cpu: '1000'
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
```
> **참고**: Auto Mode에서는 `EC2NodeClass`, `amiFamily`, 커스텀 `userData`(`/etc/eks/bootstrap.sh`)를 사용할 수 없습니다. 노드 AMI와 부트스트랩은 AWS가 관리하며, 서브넷/보안 그룹/임시 스토리지 등은 `eks.amazonaws.com/v1` `NodeClass`로 정의합니다.
GPU 예제는 현재 공식 지원 목록에 있는 p3를 유지합니다. 실제 AZ 용량·할당량·GPU 메모리/모델 요구를 확인해야 합니다. GPU Pod는 `nvidia.com/gpu`를 요청하고 위 taint를 허용하도록 구성합니다. 이 YAML을 검토했으며 GPU 노드를 생성하거나 성능을 측정하지 않았습니다.
#### Auto Mode 제한사항
- SSH 또는 SSM을 통한 노드 직접 액세스 불가
- 기본 만료는 336시간(14일), `expireAfter` 설정 상한은 21일입니다. 드레인·PDB·NodePool 설정에 따른 차단과 기본 24시간 종료 유예를 함께 검토합니다
- 기본 노드 풀 및 노드 클래스 수정 불가
- 특정 인스턴스 유형 제한 가능
#### Auto Mode 모니터링
Auto Mode의 인프라 관리가 워크로드의 모든 CloudWatch 메트릭 수집을 자동 구성하는 것은 아닙니다. 아래 `cluster_node_count`는 `AWS/EKS`가 아니라 **ContainerInsights** 네임스페이스의 메트릭이며, Container Insights 수집 구성이 있어야 합니다. 환경에 맞는 수집 방식을 구성한 뒤 실제 데이터가 존재하는 기간을 조회합니다.
```bash
# Requires a configured Container Insights collection pipeline and metric data.
aws cloudwatch list-metrics --namespace ContainerInsights --metric-name cluster_node_count \
--dimensions Name=ClusterName,Value="${EKS_CLUSTER_NAME:?}" --region "${EKS_REGION:?}"
aws cloudwatch get-metric-statistics \
--namespace ContainerInsights --metric-name cluster_node_count \
--dimensions Name=ClusterName,Value="$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--start-time "${METRICS_START_TIME:?Set a reviewed ISO8601 start time}" \
--end-time "${METRICS_END_TIME:?Set a later ISO8601 end time}" \
--period 3600 --statistics Average
```
빈 결과를 노드 수 0이나 Auto Mode 실패로 해석하지 않습니다. 수집·차원·시간 범위를 먼저 확인합니다. 이 감사에서는 CloudWatch 조회나 측정을 실행하지 않았습니다. [공식 Container Insights 메트릭 목록](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Container-Insights-metrics-EKS.html)을 참조합니다.
### Fargate 프로필 생성
Fargate 프로필을 생성하려면 다음 명령을 실행합니다:
```bash
eksctl create fargateprofile \
--cluster my-cluster \
--region us-west-2 \
--name my-fargate-profile \
--namespace default \
--labels env=fargate
```
또는 구성 파일을 사용할 수 있습니다:
```yaml
# fargate.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-cluster
region: us-west-2
fargateProfiles:
- name: my-fargate-profile
selectors:
- namespace: default
labels:
env: fargate
```
```bash
eksctl create fargateprofile -f fargate.yaml
```
### 클러스터 업데이트
[클러스터 업그레이드](#클러스터-업그레이드)에서 호환성과 단계별 절차를 확인합니다.
### 클러스터 삭제
[클러스터 삭제](#클러스터-삭제)에서 앱·데이터·소유 도구와 정리 순서를 확인합니다.
## AWS Management Console을 사용한 클러스터 생성
AWS Management Console을 사용하여 EKS 클러스터를 생성하는 단계는 다음과 같습니다:
1. [AWS Management Console](https://console.aws.amazon.com/)에 로그인합니다.
2. "EKS"를 검색하거나 서비스 목록에서 "Elastic Kubernetes Service"를 선택합니다.
3. "클러스터" 페이지에서 "클러스터 생성" 버튼을 클릭합니다.
### EKS Auto Mode 클러스터 생성 (빠른 구성)
EKS Auto Mode는 인프라 설정을 줄여 줍니다. 워크로드 신원·네트워크·용량·가용성·복구는 별도로 구성하고 검증해야 합니다.
#### 1. 빠른 구성 선택
4. "빠른 구성" 옵션이 선택되어 있는지 확인합니다.
5. 다음 정보를 입력합니다:
- **클러스터 이름**: 클러스터의 고유한 이름을 입력합니다.
- **Kubernetes 버전**: 사용할 Kubernetes 버전을 선택합니다 (최신 버전 권장).
#### 2. IAM 역할 구성
6. **클러스터 IAM 역할** 선택:
- 첫 번째 Auto Mode 클러스터인 경우 "권장 역할 생성" 옵션을 사용합니다.
- 기존 역할이 있는 경우 재사용할 수 있습니다.
- 권장 역할 이름: `AmazonEKSAutoClusterRole`
7. **노드 IAM 역할** 선택:
- 첫 번째 Auto Mode 클러스터인 경우 "권장 역할 생성" 옵션을 사용합니다.
- 권장 역할 이름: `AmazonEKSAutoNodeRole`
#### 3. 네트워킹 구성
8. **VPC 선택**:
- 새 VPC 생성: "VPC 생성" 옵션을 선택하여 EKS용 새 VPC를 생성합니다.
- 기존 VPC 사용: 이전에 생성한 EKS용 VPC를 선택합니다.
9. **서브넷 구성** (선택사항):
- EKS Auto Mode는 자동으로 VPC의 프라이빗 서브넷을 선택합니다.
- 필요에 따라 서브넷을 추가하거나 제거할 수 있습니다.
#### 4. 구성 검토 및 생성
10. **빠른 구성 기본값 보기**를 선택하여 모든 구성 값을 검토합니다.
11. **클러스터 생성**을 클릭합니다. (클러스터 생성에는 약 15분이 소요됩니다)
### 사용자 지정 구성을 사용한 클러스터 생성
더 세밀한 제어가 필요한 경우 사용자 지정 구성을 사용할 수 있습니다.
### 클러스터 구성
4. "클러스터 구성" 페이지에서 다음 정보를 입력합니다:
- **클러스터 이름**: 클러스터의 고유한 이름을 입력합니다.
- **Kubernetes 버전**: 사용할 Kubernetes 버전을 선택합니다.
- **클러스터 서비스 역할**: 새 역할을 생성하거나 기존 역할을 선택합니다.
- **EKS Auto Mode**: Auto Mode를 활성화하려면 체크박스를 선택합니다.
- **태그**: 필요한 경우 태그를 추가합니다.
- "다음" 버튼을 클릭합니다.
### 네트워킹 지정
5. "네트워킹 지정" 페이지에서 다음 정보를 입력합니다:
- **VPC**: 새 VPC를 생성하거나 기존 VPC를 선택합니다.
- **서브넷**: 클러스터에 사용할 서브넷을 선택합니다. 최소 2개의 서브넷이 서로 다른 가용 영역에 있어야 합니다.
- **보안 그룹**: 클러스터에 사용할 보안 그룹을 선택합니다.
- **클러스터 엔드포인트 액세스**: 클러스터 API 서버 엔드포인트에 대한 액세스를 구성합니다.
- **퍼블릭**: 인터넷에서 API 서버에 액세스할 수 있습니다.
- **프라이빗**: VPC 또는 연결된 네트워크에서 올바른 DNS·라우팅으로 접근합니다.
- **퍼블릭 및 프라이빗**: 인터넷과 VPC 내에서 모두 API 서버에 액세스할 수 있습니다.
- "다음" 버튼을 클릭합니다.
### 로깅 구성
6. "로깅 구성" 페이지에서 다음 정보를 입력합니다:
- **컨트롤 플레인 로깅**: 활성화할 로그 유형을 선택합니다.
- API 서버 로그
- 감사 로그
- 인증자 로그
- 컨트롤러 관리자 로그
- 스케줄러 로그
- "다음" 버튼을 클릭합니다.
### 애드온 선택
아래 애드온은 일반 컴퓨팅 기준입니다. Auto Mode는 겹치는 네트워킹·DNS·블록 스토리지 기능을 관리하므로 클러스터의 컴퓨팅 유형에 맞게 구성 요소를 선택합니다.
7. "애드온 선택" 페이지에서 다음 정보를 입력합니다:
- **Amazon VPC CNI**: 포드 네트워킹을 위한 CNI 플러그인입니다.
- **CoreDNS**: 클러스터 내 DNS 서비스입니다.
- **kube-proxy**: 네트워크 프록시 및 로드 밸런싱을 제공합니다.
- **스토리지/네트워킹 애드온**: 일반 컴퓨팅에는 필요한 구성 요소와 IAM 권한을 설치합니다. Auto Mode의 관리형 EBS·네트워킹·DNS와 중복되는 구성 요소를 Auto Mode 노드에 설치하지 않습니다.
- "다음" 버튼을 클릭합니다.
### 검토 및 생성
8. "검토 및 생성" 페이지에서 구성을 검토하고 "생성" 버튼을 클릭합니다.
### Auto Mode가 아닌 클러스터의 노드 그룹 추가
일반 EC2 컴퓨팅에는 클러스터 생성 후 노드 그룹을 추가합니다. Fargate 프로필 등 다른 지원 방식에는 별도 설정이 있으며, Auto Mode가 아닌 모든 클러스터에 관리형 노드 그룹이 필수인 것은 아닙니다.
### 노드 그룹 추가
1. "노드 그룹 구성" 페이지에서 다음 정보를 입력합니다:
- **노드 그룹 이름**: 노드 그룹의 고유한 이름을 입력합니다.
- **노드 IAM 역할**: 새 역할을 생성하거나 기존 역할을 선택합니다.
- "다음" 버튼을 클릭합니다.
2. "컴퓨팅 및 크기 조정 구성 설정" 페이지에서 다음 정보를 입력합니다:
- **AMI 유형**: 노드에 사용할 AMI 유형을 선택합니다.
- **인스턴스 유형**: 노드에 사용할 EC2 인스턴스 유형을 선택합니다.
- **디스크 크기**: 노드의 디스크 크기를 지정합니다.
- **노드 수**: 최소, 최대 및 원하는 노드 수를 지정합니다.
- "다음" 버튼을 클릭합니다.
3. "네트워킹 지정" 페이지에서 다음 정보를 입력합니다:
- **서브넷**: 노드 그룹에 사용할 서브넷을 선택합니다.
- **원격 액세스 구성**: 승인된 관리 경로에 필요한 경우에만 SSH를 설정하고 소스 보안 그룹을 검토합니다.
- "다음" 버튼을 클릭합니다.
4. "검토 및 생성" 페이지에서 구성을 검토하고 "생성" 버튼을 클릭합니다.
## AWS CLI를 사용한 클러스터 생성
아래는 **새 클러스터** 생성 예제입니다. Auto Mode와 일반 클러스터 중 하나를 선택하고 두 생성 명령을 같은 이름으로 연속 실행하지 않습니다. Bash·`jq`·현재 AWS CLI v2와 승인된 AWS 역할이 필요합니다. 코드의 EKS 1.36은 확인한 예제 버전이며 다른 버전은 해당 리전의 지원·호환성을 먼저 확인합니다.
서브넷 두 개는 같은 VPC의 서로 다른 AZ에 있어야 합니다. DNS, 여유 IP, 보안 그룹, 노드의 이미지/서비스 접근 경로를 별도로 준비합니다. `endpointPrivateAccess`와 `endpointPublicAccess`가 실제 API 필드 이름입니다. 초기 생성자 관리자 권한은 이 전용 예제의 부트스트랩을 위한 것이며 후속 접근은 access entry로 관리합니다.
### 공통 입력 준비
선택한 모드의 클러스터 역할을 미리 생성하고 필요한 `iam:PassRole` 및 서비스 연결 역할 생성 권한을 확인합니다. 계정·리전·서브넷·승인 CIDR을 검토한 다음에만 생성 명령을 실행합니다.
```bash
: "${EKS_CLUSTER_NAME:?Choose a unique new cluster name}"
: "${EKS_REGION:?Choose the intended AWS region}"
: "${EKS_CLUSTER_ROLE_ARN:?Pre-created cluster role for the chosen mode}"
: "${EKS_SUBNET_A:?Existing subnet in the intended VPC}"
: "${EKS_SUBNET_B:?Existing subnet in a different AZ of the same VPC}"
: "${EKS_PUBLIC_API_CIDR:?Approved client CIDR, normally /32}"
EKS_CREATION_DIR=$(mktemp -d /tmp/eks-create.XXXXXX)
: "${EKS_CREATION_DIR:?}"
EKS_KUBECONFIG="$EKS_CREATION_DIR/kubeconfig"
unset EKS_CREATED_CLUSTER_ARN
aws sts get-caller-identity
```
### EKS Auto Mode 클러스터 생성
클러스터 역할은 `eks.amazonaws.com`에 대한 `sts:AssumeRole`·`sts:TagSession` 신뢰와 다음 정책이 필요합니다: `AmazonEKSClusterPolicy`, `AmazonEKSComputePolicy`, `AmazonEKSBlockStoragePolicyV2`, `AmazonEKSLoadBalancingPolicy`, `AmazonEKSNetworkingPolicy` 또는 동등한 사용자 정책입니다.
노드 역할은 `ec2.amazonaws.com`을 신뢰하고 `AmazonEKSWorkerNodeMinimalPolicy`·`AmazonEC2ContainerRegistryPullOnly`를 사용합니다. 워크로드의 AWS 권한은 별도의 Pod Identity/IRSA 경로로 구성합니다. 컴퓨팅·로드 밸런싱·블록 스토리지를 함께 켜고 자체 관리형 기본 애드온 부트스트랩을 끕니다.
```bash
: "${EKS_AUTO_NODE_ROLE_ARN:?Pre-created Auto Mode node role}"
jq -n \
--arg name "${EKS_CLUSTER_NAME:?}" \
--arg role "${EKS_CLUSTER_ROLE_ARN:?}" \
--arg subnetA "${EKS_SUBNET_A:?}" --arg subnetB "${EKS_SUBNET_B:?}" \
--arg cidr "${EKS_PUBLIC_API_CIDR:?}" \
--arg nodeRole "$EKS_AUTO_NODE_ROLE_ARN" \
'{
name: $name,
version: "1.36",
roleArn: $role,
resourcesVpcConfig: {
subnetIds: [$subnetA, $subnetB],
endpointPrivateAccess: true,
endpointPublicAccess: true,
publicAccessCidrs: [$cidr]
},
accessConfig: {authenticationMode: "API", bootstrapClusterCreatorAdminPermissions: true},
logging: {clusterLogging: [{
types: ["api", "audit", "authenticator", "controllerManager", "scheduler"],
enabled: true
}]},
tags: {"docs-lab": $name}
} + {
bootstrapSelfManagedAddons: false,
computeConfig: {
enabled: true, nodePools: ["system", "general-purpose"], nodeRoleArn: $nodeRole
},
kubernetesNetworkConfig: {ipFamily: "ipv4", elasticLoadBalancing: {enabled: true}},
storageConfig: {blockStorage: {enabled: true}}
}' > "${EKS_CREATION_DIR:?}/create-auto.json"
cat "$EKS_CREATION_DIR/create-auto.json"
EKS_CREATED_CLUSTER_ARN=$(aws eks create-cluster --region "${EKS_REGION:?}" \
--cli-input-json "file://$EKS_CREATION_DIR/create-auto.json" \
--query cluster.arn --output text)
: "${EKS_CREATED_CLUSTER_ARN:?Creation failed; inspect the error before continuing}"
```
#### 생성 완료와 접근 확인
```bash
: "${EKS_CREATED_CLUSTER_ARN:?Create and verify the new cluster first}"
aws eks wait cluster-active --name "${EKS_CLUSTER_NAME:?}" --region "${EKS_REGION:?}"
aws eks describe-cluster --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--query 'cluster.{arn:arn,status:status,version:version}' --output table
aws eks update-kubeconfig --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--kubeconfig "${EKS_KUBECONFIG:?}"
kubectl --kubeconfig "$EKS_KUBECONFIG" get nodes
```
Auto Mode는 필요한 워크로드가 아직 없다면 노드가 없을 수 있습니다. NodePool/NodeClass 상태와 이후 Pod 스케줄링을 확인합니다. 클러스터 `ACTIVE`는 애플리케이션 정상 동작이나 정해진 생성 시간을 보장하지 않습니다.
```bash
kubectl --kubeconfig "${EKS_KUBECONFIG:?}" get nodepools.karpenter.sh
kubectl --kubeconfig "$EKS_KUBECONFIG" get nodeclasses.eks.amazonaws.com
```
### 일반 클러스터 생성
Auto Mode를 선택하지 않은 경우에만 이 대안을 사용합니다. 클러스터 역할에는 일반 EKS 클러스터 권한이 필요하며, EC2 노드 역할과 CNI/애드온 권한은 별도입니다. 기본 네트워킹 애드온을 부트스트랩하더라도 EBS CSI나 AWS LBC까지 설치되는 것은 아닙니다.
```bash
jq -n \
--arg name "${EKS_CLUSTER_NAME:?}" \
--arg role "${EKS_CLUSTER_ROLE_ARN:?}" \
--arg subnetA "${EKS_SUBNET_A:?}" --arg subnetB "${EKS_SUBNET_B:?}" \
--arg cidr "${EKS_PUBLIC_API_CIDR:?}" \
'{
name: $name,
version: "1.36",
roleArn: $role,
resourcesVpcConfig: {
subnetIds: [$subnetA, $subnetB],
endpointPrivateAccess: true,
endpointPublicAccess: true,
publicAccessCidrs: [$cidr]
},
accessConfig: {authenticationMode: "API", bootstrapClusterCreatorAdminPermissions: true},
logging: {clusterLogging: [{
types: ["api", "audit", "authenticator", "controllerManager", "scheduler"],
enabled: true
}]},
tags: {"docs-lab": $name}
} + {
bootstrapSelfManagedAddons: true,
kubernetesNetworkConfig: {ipFamily: "ipv4"}
}' > "${EKS_CREATION_DIR:?}/create-standard.json"
cat "$EKS_CREATION_DIR/create-standard.json"
EKS_CREATED_CLUSTER_ARN=$(aws eks create-cluster --region "${EKS_REGION:?}" \
--cli-input-json "file://$EKS_CREATION_DIR/create-standard.json" \
--query cluster.arn --output text)
: "${EKS_CREATED_CLUSTER_ARN:?Creation failed; inspect the error before continuing}"
```
#### 생성 완료와 kubeconfig
```bash
: "${EKS_CREATED_CLUSTER_ARN:?Create and verify the new cluster first}"
aws eks wait cluster-active --name "${EKS_CLUSTER_NAME:?}" --region "${EKS_REGION:?}"
aws eks describe-cluster --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--query 'cluster.{arn:arn,status:status,version:version}' --output table
aws eks update-kubeconfig --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--kubeconfig "${EKS_KUBECONFIG:?}"
kubectl --kubeconfig "$EKS_KUBECONFIG" get nodes
```
#### 관리형 노드 그룹 생성
EC2 노드 역할에는 워커 노드·ECR 가져오기 권한을 준비하고, CNI에는 별도 역할이나 검토된 노드 역할 권한을 제공합니다. Auto Mode의 최소 노드 역할을 그대로 재사용하지 않습니다. 프라이빗 서브넷의 NAT 또는 필요한 VPC 엔드포인트도 준비되어 있어야 합니다. 아래는 AL2023 예제이며 SSH 공개를 기본 활성화하지 않습니다.
```bash
: "${EKS_MANAGED_NODE_ROLE_ARN:?Pre-created conventional EC2 node role}"
: "${EKS_NODEGROUP_NAME:?Unique managed node group name}"
aws eks create-nodegroup \
--cluster-name "${EKS_CLUSTER_NAME:?}" \
--nodegroup-name "$EKS_NODEGROUP_NAME" \
--subnets "${EKS_SUBNET_A:?}" "${EKS_SUBNET_B:?}" \
--instance-types m5.large --ami-type AL2023_x86_64_STANDARD \
--node-role "$EKS_MANAGED_NODE_ROLE_ARN" \
--scaling-config minSize=1,maxSize=3,desiredSize=2 \
--disk-size 20 --region "${EKS_REGION:?}"
aws eks wait nodegroup-active --cluster-name "$EKS_CLUSTER_NAME" \
--nodegroup-name "$EKS_NODEGROUP_NAME" --region "$EKS_REGION"
aws eks describe-nodegroup --cluster-name "$EKS_CLUSTER_NAME" \
--nodegroup-name "$EKS_NODEGROUP_NAME" --region "$EKS_REGION" \
--query 'nodegroup.{status:status,health:health,version:version}' --output json
kubectl --kubeconfig "${EKS_KUBECONFIG:?}" get nodes
```
`minSize`/`maxSize`만으로 Pod 수요 기반 노드 자동 확장이 활성화되지는 않습니다. 상태와 헬스 오류를 확인한 뒤 Cluster Autoscaler 등 별도 구성 여부를 결정합니다. 생성 실패 시 기록한 이름·ARN과 관련 리소스를 확인하며, 감사에서는 이러한 생성·대기 명령을 실행하지 않았습니다.
참고: [CreateCluster API](https://docs.aws.amazon.com/eks/latest/APIReference/API_CreateCluster.html), [CreateNodegroup API](https://docs.aws.amazon.com/eks/latest/APIReference/API_CreateNodegroup.html), [Auto Mode IAM and creation](https://docs.aws.amazon.com/eks/latest/userguide/automode-get-started-cli.html).
## Terraform을 사용한 클러스터 생성
아래 완결된 예제는 최초 apply 전에 Auto Mode 또는 일반 관리형 노드 그룹을 선택해 **새 클러스터 하나**를 만듭니다. 대안을 두 번째 클러스터 리소스로 덧붙이거나 기존 클러스터에서 마이그레이션 계획 없이 전환하지 않습니다. Terraform state와 provider lock 파일은 해당 배포에 속합니다.
### EKS Auto Mode 클러스터 Terraform 구성
`main.tf`로 저장합니다. AWS provider **6.64.0**, EKS **1.36**을 사용합니다. 기존 프라이빗 서브넷 ID를 명시하며 `Type=Private` 태그만으로 라우팅·연결성을 판단하지 않습니다. precondition은 plan에서 VPC/AZ 관계를 확인하지만 프라이빗 라우팅·서비스 엔드포인트·DNS·IP 여유·로드 밸런서 서브넷 태그까지 검증하지 않습니다.
Auto Mode는 컴퓨팅·로드 밸런싱·블록 스토리지를 함께 켜고 `bootstrap_self_managed_addons = false`를 지정해야 합니다. 클러스터 역할에는 `sts:TagSession`과 정책 5개를 포함합니다. IAM 연결 의존성은 EKS가 관리형 인프라를 삭제하는 동안 권한을 유지합니다.
```hcl
terraform {
required_version = ">= 1.5.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.64.0"
}
}
}
provider "aws" {
region = var.region
}
data "aws_partition" "current" {}
data "aws_subnet" "selected" {
for_each = toset(var.private_subnet_ids)
id = each.value
}
locals {
cluster_policies = var.enable_auto_mode ? toset([
"AmazonEKSClusterPolicy",
"AmazonEKSComputePolicy",
"AmazonEKSBlockStoragePolicyV2",
"AmazonEKSLoadBalancingPolicy",
"AmazonEKSNetworkingPolicy",
]) : toset(["AmazonEKSClusterPolicy"])
node_policies = var.enable_auto_mode ? toset([
"AmazonEKSWorkerNodeMinimalPolicy",
"AmazonEC2ContainerRegistryPullOnly",
]) : toset([
"AmazonEKSWorkerNodePolicy",
"AmazonEC2ContainerRegistryPullOnly",
# Conventional bootstrap baseline; see the CNI role caveat in the text.
"AmazonEKS_CNI_Policy",
])
}
resource "aws_iam_role" "cluster" {
name = "${var.cluster_name}-cluster"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = var.enable_auto_mode ? ["sts:AssumeRole", "sts:TagSession"] : ["sts:AssumeRole"]
Principal = {
Service = "eks.amazonaws.com"
}
}]
})
}
resource "aws_iam_role_policy_attachment" "cluster" {
for_each = local.cluster_policies
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/${each.value}"
role = aws_iam_role.cluster.name
}
resource "aws_iam_role" "node" {
name = "${var.cluster_name}-node"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["sts:AssumeRole"]
Principal = {
Service = "ec2.amazonaws.com"
}
}]
})
}
resource "aws_iam_role_policy_attachment" "node" {
for_each = local.node_policies
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/${each.value}"
role = aws_iam_role.node.name
}
resource "aws_eks_cluster" "main" {
name = var.cluster_name
role_arn = aws_iam_role.cluster.arn
version = var.kubernetes_version
access_config {
authentication_mode = "API"
bootstrap_cluster_creator_admin_permissions = false
}
bootstrap_self_managed_addons = !var.enable_auto_mode
compute_config {
enabled = var.enable_auto_mode
node_pools = var.enable_auto_mode ? ["system", "general-purpose"] : null
node_role_arn = var.enable_auto_mode ? aws_iam_role.node.arn : null
}
kubernetes_network_config {
ip_family = "ipv4"
elastic_load_balancing {
enabled = var.enable_auto_mode
}
}
storage_config {
block_storage {
enabled = var.enable_auto_mode
}
}
vpc_config {
subnet_ids = var.private_subnet_ids
endpoint_private_access = true
endpoint_public_access = true
public_access_cidrs = var.public_api_cidrs
}
enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
# Keep policies attached until EKS finishes deleting managed infrastructure.
depends_on = [
aws_iam_role_policy_attachment.cluster,
aws_iam_role_policy_attachment.node,
]
lifecycle {
precondition {
condition = (
alltrue([for subnet in data.aws_subnet.selected : subnet.vpc_id == var.vpc_id]) &&
length(toset([for subnet in data.aws_subnet.selected : subnet.availability_zone])) >= 2
)
error_message = "Supply subnets in at least two AZs of the selected VPC."
}
}
tags = var.tags
}
resource "aws_eks_node_group" "standard" {
count = var.enable_auto_mode ? 0 : 1
cluster_name = aws_eks_cluster.main.name
node_group_name = "main-nodegroup"
node_role_arn = aws_iam_role.node.arn
subnet_ids = var.private_subnet_ids
version = aws_eks_cluster.main.version
ami_type = "AL2023_x86_64_STANDARD"
capacity_type = "ON_DEMAND"
instance_types = ["m5.large"]
scaling_config {
desired_size = 2
max_size = 3
min_size = 1
}
update_config {
max_unavailable = 1
}
depends_on = [aws_iam_role_policy_attachment.node]
tags = var.tags
}
resource "aws_eks_access_entry" "operator" {
cluster_name = aws_eks_cluster.main.name
principal_arn = var.operator_role_arn
type = "STANDARD"
}
resource "aws_eks_access_policy_association" "operator" {
cluster_name = aws_eks_cluster.main.name
principal_arn = aws_eks_access_entry.operator.principal_arn
policy_arn = "arn:${data.aws_partition.current.partition}:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
access_scope {
type = "cluster"
}
}
variable "enable_auto_mode" {
description = "Creation-time choice. Changing an existing cluster requires a separate migration plan."
type = bool
default = true
}
variable "cluster_name" {
description = "Unique name for this new cluster."
type = string
}
variable "kubernetes_version" {
description = "EKS-supported minor version; 1.36 is the reviewed example."
type = string
default = "1.36"
}
variable "region" {
type = string
}
variable "vpc_id" {
type = string
}
variable "private_subnet_ids" {
type = list(string)
validation {
condition = length(distinct(var.private_subnet_ids)) >= 2
error_message = "At least two distinct subnet IDs are required."
}
}
variable "public_api_cidrs" {
type = list(string)
validation {
condition = length(var.public_api_cidrs) > 0 && alltrue([
for cidr in var.public_api_cidrs :
can(cidrhost(cidr, 0)) && cidr != "0.0.0.0/0" && cidr != "::/0"
])
error_message = "Supply reviewed client CIDRs instead of unrestricted public API access."
}
}
variable "operator_role_arn" {
description = "Existing approved IAM role allowed to administer this cluster."
type = string
}
variable "tags" {
type = map(string)
default = {
Environment = "dev"
Project = "eks-creation-example"
}
}
output "cluster_name" {
value = aws_eks_cluster.main.name
}
output "cluster_endpoint" {
value = aws_eks_cluster.main.endpoint
}
output "cluster_security_group_id" {
value = aws_eks_cluster.main.vpc_config[0].cluster_security_group_id
}
output "cluster_arn" {
value = aws_eks_cluster.main.arn
}
```
운영자 access entry는 승인된 IAM 역할에 클러스터 관리 권한을 부여하며 생성자에게 Kubernetes 관리자 권한을 자동 부여하지 않습니다. 운영자 역할은 미리 존재해야 하고 kubectl 사용자가 AssumeRole할 수 있어야 합니다.
일반 노드 대안은 초기 부트스트랩용 CNI 권한을 노드 역할에 포함합니다. 이는 공유 권한 경계이며 강화된 프로덕션 워크로드 신원 설계가 아닙니다. 프로덕션 사용 전에 별도 CNI 역할과 Pod의 IMDS 접근 제한을 검토합니다. 앱의 AWS 권한은 워크로드별 신원에 부여하고 관련 없는 애드온 권한을 모든 노드에 추가하지 않습니다.
### Terraform 실행
새 작업 디렉터리와 인증된 프로비저닝 역할을 사용합니다. 예시 ID·문서용 CIDR을 실제 리소스·승인된 클라이언트 CIDR로 바꿉니다. 기본 선택은 Auto Mode입니다:
```hcl
# terraform.tfvars — replace every example identifier before planning.
cluster_name = "eks-docs-unique-name"
region = "us-west-2"
vpc_id = "vpc-0123456789abcdef0"
private_subnet_ids = ["subnet-0123456789abcdef0", "subnet-1123456789abcdef0"]
public_api_cidrs = ["203.0.113.10/32"]
operator_role_arn = "arn:aws:iam::111122223333:role/ApprovedOperator"
enable_auto_mode = true
```
```bash
# Run in a new directory containing main.tf and the reviewed terraform.tfvars.
terraform init
terraform fmt -check
terraform validate
terraform plan -out=reviewed.tfplan
# Apply only the plan you reviewed for the intended account/region/resources.
terraform apply reviewed.tfplan
: "${EKS_REGION:?Use the same region as terraform.tfvars}"
: "${EKS_OPERATOR_ROLE_ARN:?Use the same operator role as terraform.tfvars}"
aws eks update-kubeconfig --name "$(terraform output -raw cluster_name)" \
--region "$EKS_REGION" --role-arn "$EKS_OPERATOR_ROLE_ARN" --kubeconfig ./kubeconfig
kubectl --kubeconfig ./kubeconfig get nodes
```
apply 전에 state/backend, IAM·네트워크 변경과 과금 리소스를 검토합니다. `terraform validate`는 AWS 계정 권한·할당량·라우팅·노드의 실제 워크로드 실행 가능성을 검증하지 않습니다. 노드 그룹의 최소·최대 크기만으로 Pod 수요 기반 오토스케일러가 설치되지 않습니다.
### 기존 방식 Terraform 구성
**새 일반 클러스터**에는 동일한 전체 `main.tf`를 사용하고 최초 plan 전에 `terraform.tfvars`를 다음과 같이 설정합니다:
```hcl
enable_auto_mode = false
```
Auto Mode 기능 3개를 모두 끄고 일반 네트워킹 애드온을 부트스트랩하며 일반 EC2 노드 권한과 AL2023 관리형 노드 그룹을 구성합니다. EBS CSI·AWS LBC·Metrics Server·노드 오토스케일러는 설치하지 않으므로 필요한 기능을 별도로 구성합니다. 배포 후 값을 바꾸면 인프라와 IAM이 변경되므로 별도 마이그레이션 검토가 필요합니다.
검증: Terraform **1.15.7**, 서명된 AWS provider **6.64.0**으로 로컬 `terraform validate`를 수행하여 오류·경고 0건을 확인했습니다. **plan·apply·AWS API 호출은 수행하지 않았습니다.** 두 모드 모두 배포 검증이 필요하며 프로덕션 준비 상태를 입증하는 예제가 아닙니다.
참고: [AWS provider EKS cluster](https://github.com/hashicorp/terraform-provider-aws/blob/v6.64.0/website/docs/r/eks_cluster.html.markdown) · [managed node group](https://github.com/hashicorp/terraform-provider-aws/blob/v6.64.0/website/docs/r/eks_node_group.html.markdown) · [Auto Mode role requirements](https://docs.aws.amazon.com/eks/latest/userguide/auto-cluster-iam-role.html)
## AWS CDK를 사용한 클러스터 생성
이 **새 스택** 예제는 `aws-cdk-lib/aws-eks-v2`로 네이티브 `AWS::EKS::Cluster`와 access entry를 생성합니다. Auto Mode 활성화용 Lambda 커스텀 리소스가 필요하지 않습니다. 이미 배포된 construct를 이 예제로 교체하는 일을 제자리 마이그레이션으로 간주하지 않습니다.
CDK 라이브러리 **2.269.0**, CLI **2.1141.0**, EKS **1.36**을 사용합니다. 합성 중 VPC 조회 없이 기존 프라이빗 서브넷 속성을 가져옵니다. 배포 전 서브넷/AZ 대응 관계, VPC DNS, 라우팅, 서비스 접근과 로드 밸런서 서브넷 태그를 확인해야 합니다. VPC를 가져오는 코드 자체가 이를 검증하지는 않습니다.
### TypeScript를 사용한 EKS Auto Mode 클러스터
`lib/eks-auto-mode-stack.ts`로 저장합니다. 승인된 기존 운영자 역할에만 이 클러스터의 관리자 접근을 부여합니다. 워크로드 권한은 별도로 구성합니다.
```typescript
import * as cdk from 'aws-cdk-lib';
import * as eks from 'aws-cdk-lib/aws-eks-v2';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export interface EksAutoModeProps extends cdk.StackProps {
readonly clusterName: string;
readonly vpcId: string;
readonly privateSubnetIds: string[];
readonly availabilityZones: string[];
readonly publicApiCidrs: string[];
readonly operatorRoleArn: string;
}
export class EksAutoModeStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: EksAutoModeProps) {
super(scope, id, props);
if (props.privateSubnetIds.length < 2 ||
props.privateSubnetIds.length !== props.availabilityZones.length ||
new Set(props.availabilityZones).size < 2 ||
props.publicApiCidrs.length === 0) {
throw new Error('Supply corresponding private subnets/AZs in at least two AZs and approved API CIDRs');
}
const vpc = ec2.Vpc.fromVpcAttributes(this, 'Vpc', {
vpcId: props.vpcId,
availabilityZones: props.availabilityZones,
privateSubnetIds: props.privateSubnetIds,
});
const clusterRole = new iam.Role(this, 'ClusterRole', {
assumedBy: new iam.ServicePrincipal('eks.amazonaws.com'),
managedPolicies: [
'AmazonEKSClusterPolicy',
'AmazonEKSComputePolicy',
'AmazonEKSBlockStoragePolicyV2',
'AmazonEKSLoadBalancingPolicy',
'AmazonEKSNetworkingPolicy',
].map(name => iam.ManagedPolicy.fromAwsManagedPolicyName(name)),
});
clusterRole.assumeRolePolicy!.addStatements(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal('eks.amazonaws.com')],
actions: ['sts:TagSession'],
}));
const nodeRole = new iam.Role(this, 'NodeRole', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
'AmazonEKSWorkerNodeMinimalPolicy',
'AmazonEC2ContainerRegistryPullOnly',
].map(name => iam.ManagedPolicy.fromAwsManagedPolicyName(name)),
});
const cluster = new eks.Cluster(this, 'Cluster', {
clusterName: props.clusterName,
version: eks.KubernetesVersion.V1_36,
vpc,
vpcSubnets: [{ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }],
endpointAccess: eks.EndpointAccess.PUBLIC_AND_PRIVATE.onlyFrom(...props.publicApiCidrs),
defaultCapacityType: eks.DefaultCapacityType.AUTOMODE,
bootstrapSelfManagedAddons: false,
bootstrapClusterCreatorAdminPermissions: false,
// All required policies/trust are defined above. Avoid adding the older
// BlockStoragePolicy that CDK 2.269.0 otherwise attaches automatically.
role: clusterRole.withoutPolicyUpdates(),
compute: {
nodePools: ['system', 'general-purpose'],
nodeRole: nodeRole.withoutPolicyUpdates(),
},
clusterLogging: [
eks.ClusterLoggingTypes.API,
eks.ClusterLoggingTypes.AUDIT,
eks.ClusterLoggingTypes.AUTHENTICATOR,
eks.ClusterLoggingTypes.CONTROLLER_MANAGER,
eks.ClusterLoggingTypes.SCHEDULER,
],
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
cluster.node.addDependency(clusterRole, nodeRole);
cluster.grantClusterAdmin('OperatorAccess', props.operatorRoleArn);
new cdk.CfnOutput(this, 'ClusterName', { value: cluster.clusterName });
new cdk.CfnOutput(this, 'ClusterEndpoint', { value: cluster.clusterEndpoint });
}
}
```
클러스터 역할은 `sts:TagSession`과 `AmazonEKSBlockStoragePolicyV2`를 포함한 Auto Mode 정책 5개를 명시합니다. `withoutPolicyUpdates()`는 CDK 2.269.0이 이전 블록 스토리지 정책을 추가하는 것을 막으므로 필요한 권한 전체를 이 예제에서 제공할 책임이 있습니다. 의존성은 클러스터 삭제 완료까지 역할을 유지합니다. 노드 역할에는 Auto Mode 최소 워커 정책과 ECR 가져오기 정책만 부여합니다.
`bootstrapSelfManagedAddons: false`로 자체 관리형 네트워킹 애드온과의 중복을 피합니다. Auto Mode는 컴퓨팅·로드 밸런싱·블록 스토리지를 함께 관리합니다. 운영자 access entry는 클러스터 실행 역할과 별도로 생성됩니다.
### CDK 앱 진입점
`bin/eks-auto-mode.ts`로 저장하고 shebang을 첫 줄에 둡니다. 예시 계정·VPC·역할을 실수로 사용하지 않도록 환경변수를 필수 입력으로 받습니다.
```typescript
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { EksAutoModeStack } from '../lib/eks-auto-mode-stack';
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Set ${name} before synthesis/deployment`);
return value;
}
function list(name: string): string[] {
return required(name).split(',').map(value => value.trim()).filter(Boolean);
}
const app = new cdk.App();
new EksAutoModeStack(app, 'EksAutoModeStack', {
env: {
account: required('CDK_DEFAULT_ACCOUNT'),
region: required('CDK_DEFAULT_REGION'),
},
clusterName: required('EKS_CLUSTER_NAME'),
vpcId: required('EKS_VPC_ID'),
privateSubnetIds: list('EKS_PRIVATE_SUBNET_IDS'),
availabilityZones: list('EKS_AVAILABILITY_ZONES'),
publicApiCidrs: list('EKS_PUBLIC_API_CIDRS'),
operatorRoleArn: required('EKS_OPERATOR_ROLE_ARN'),
});
```
### CDK 배포
새 프로젝트를 초기화한 뒤 위 두 파일을 저장하고 대상 계정/리전 값을 설정합니다. 인증된 승인 역할로 실행하며 배포 전에 합성된 IAM·네트워크 설정과 diff를 검토합니다. CDK 라이브러리와 CLI는 별도 패키지로 버전 번호가 같지 않습니다.
```bash
mkdir eks-auto-mode
cd eks-auto-mode
npx --yes --package aws-cdk@2.1141.0 cdk init app --language typescript
npm install --save-exact aws-cdk-lib@2.269.0 constructs@10.5.0
npm install --save-dev --save-exact aws-cdk@2.1141.0 typescript@5.9.3
# Save the source files above, then set the required environment values.
: "${CDK_DEFAULT_ACCOUNT:?Set the intended AWS account ID}"
: "${CDK_DEFAULT_REGION:?Set the intended AWS region}"
: "${EKS_CLUSTER_NAME:?Use a unique name for this new cluster}"
: "${EKS_VPC_ID:?}"
: "${EKS_PRIVATE_SUBNET_IDS:?Comma-separated existing subnet IDs}"
: "${EKS_AVAILABILITY_ZONES:?Corresponding comma-separated AZs}"
: "${EKS_PUBLIC_API_CIDRS:?Approved client CIDRs, normally /32}"
: "${EKS_OPERATOR_ROLE_ARN:?Existing operator role you may assume}"
# Export the variables so the CDK application receives them.
export CDK_DEFAULT_ACCOUNT CDK_DEFAULT_REGION EKS_CLUSTER_NAME EKS_VPC_ID
export EKS_PRIVATE_SUBNET_IDS EKS_AVAILABILITY_ZONES EKS_PUBLIC_API_CIDRS EKS_OPERATOR_ROLE_ARN
npx tsc --noEmit
npx cdk synth
# Bootstrap creates AWS resources; review the account/region and execution policy.
npx cdk bootstrap "aws://$CDK_DEFAULT_ACCOUNT/$CDK_DEFAULT_REGION"
npx cdk diff
npx cdk deploy
aws eks update-kubeconfig --name "$EKS_CLUSTER_NAME" --region "$CDK_DEFAULT_REGION" \
--role-arn "$EKS_OPERATOR_ROLE_ARN" --kubeconfig ./kubeconfig
kubectl --kubeconfig ./kubeconfig get nodes
```
감사에서는 더미 식별자로 두 TypeScript 파일을 컴파일하고 로컬 합성을 수행했습니다. Auto Mode 기능 3개, 제한된 API CIDR, 필요한 역할과 운영자 access entry 1개를 확인했고 Lambda/커스텀 리소스는 생성되지 않았습니다. 필수 입력 누락도 거부되었습니다. **부트스트랩·조회·배포·클러스터 생성·AWS API 호출은 수행하지 않았으므로** 계정 할당량·네트워크·워크로드 가용성은 실제 배포 시 검증해야 합니다.
참고: [EKS V2 construct library](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_eks_v2-readme.html) · [Auto Mode cluster role](https://docs.aws.amazon.com/eks/latest/userguide/auto-cluster-iam-role.html) · [Auto Mode creation](https://docs.aws.amazon.com/eks/latest/userguide/automode-get-started-cli.html)
## 클러스터 액세스 구성
선택한 도구가 만든 클러스터 이름·리전·별도 kubeconfig 경로를 설정합니다. Terraform/CDK 예제에서는 승인된 운영자 역할을 `EKS_OPERATOR_ROLE_ARN`에 지정합니다. CLI 생성자 접근을 사용하면 생략할 수 있습니다. kubeconfig 생성 자체가 Kubernetes 권한을 부여하지는 않습니다.
```bash
: "${EKS_CLUSTER_NAME:?Use the selected cluster name}"
: "${EKS_REGION:?Use the selected region}"
: "${EKS_KUBECONFIG:?Set the dedicated kubeconfig path for this method}"
aws sts get-caller-identity
if [[ -n ${EKS_OPERATOR_ROLE_ARN:-} ]]; then
aws eks update-kubeconfig --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--role-arn "$EKS_OPERATOR_ROLE_ARN" --kubeconfig "$EKS_KUBECONFIG"
else
aws eks update-kubeconfig --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--kubeconfig "$EKS_KUBECONFIG"
fi
eks_kubectl() {
kubectl --kubeconfig "${EKS_KUBECONFIG:?}" "$@"
}
eks_kubectl config current-context
aws eks describe-cluster --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--query cluster.accessConfig.authenticationMode
```
### RBAC 구성
신규 IAM 접근은 access entry를 사용합니다. 아래는 기존 `dev` 네임스페이스의 조회 전용 새 역할 예제이며, 생성 도구가 관리하는 운영자 entry와 중복 생성하지 않습니다. 인증 모드는 `API` 또는 `API_AND_CONFIG_MAP`이어야 합니다. 기존 `aws-auth`는 deprecated이며 전체 ConfigMap을 덮어쓰지 않습니다. 전환이 필요하면 공식 이전 절차를 따릅니다.
```bash
# A new reader identity, distinct from the operator already granted by IaC.
: "${EKS_READER_ROLE_ARN:?Existing approved IAM role without an access entry yet}"
if eks_kubectl get namespace dev &&
aws eks create-access-entry --cluster-name "${EKS_CLUSTER_NAME:?}" \
--region "${EKS_REGION:?}" --principal-arn "$EKS_READER_ROLE_ARN" \
--type STANDARD --kubernetes-groups eks-docs-readers; then
EKS_ACCESS_DIR=$(mktemp -d /tmp/eks-access.XXXXXX)
: "${EKS_ACCESS_DIR:?}"
cat > "$EKS_ACCESS_DIR/rbac.yaml" << 'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: eks-docs-reader
namespace: dev
rules:
- apiGroups: [""]
resources: ["pods", "services"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: eks-docs-readers
namespace: dev
subjects:
- kind: Group
name: eks-docs-readers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: eks-docs-reader
apiGroup: rbac.authorization.k8s.io
EOF
eks_kubectl create -f "$EKS_ACCESS_DIR/rbac.yaml"
fi
```
조회 역할에는 Secret API나 변경 권한을 부여하지 않습니다. 실제 역할로 인증해 `kubectl auth can-i`를 확인하며 다른 RBAC/EKS 접근 정책의 허용이 합산될 수 있습니다.
참고: [EKS access entries](https://docs.aws.amazon.com/eks/latest/userguide/access-entries.html)
## 클러스터 검증
위 액세스 절차의 `eks_kubectl`과 같은 전용 kubeconfig를 사용합니다. 제어면 접근만으로 노드·워크로드 정상 상태를 판단하지 않습니다.
### 기본 검증
```bash
eks_kubectl cluster-info
eks_kubectl get nodes
eks_kubectl get pods -n kube-system
eks_kubectl get events --sort-by='.lastTimestamp'
```
### Auto Mode 특정 검증
Auto Mode 프로비저닝 컨트롤러는 AWS가 관리하므로 `karpenter` 네임스페이스의 자체 관리형 컨트롤러 Pod를 기대하지 않습니다. NodePool/NodeClass 상태·이벤트와 용량이 필요한 워크로드의 NodeClaim을 확인합니다.
```bash
eks_kubectl get nodepools.karpenter.sh
eks_kubectl get nodeclasses.eks.amazonaws.com
eks_kubectl get nodeclaims.karpenter.sh
```
### 샘플 애플리케이션 배포
새 네임스페이스에서 이미지 접근·스케줄링·HTTP readiness·Service를 확인합니다. 순수 Fargate 구성에는 먼저 이 네임스페이스에 맞는 프로필이 필요합니다.
```bash
EKS_SAMPLE_DIR=$(mktemp -d /tmp/eks-validate.XXXXXX)
: "${EKS_SAMPLE_DIR:?}"
unset EKS_SAMPLE_NAMESPACE EKS_SAMPLE_UID
EKS_SAMPLE_CANDIDATE=$(basename "$EKS_SAMPLE_DIR" | tr '[:upper:].' '[:lower:]-')
if EKS_SAMPLE_UID=$(eks_kubectl create namespace "$EKS_SAMPLE_CANDIDATE" -o jsonpath='{.metadata.uid}'); then
EKS_SAMPLE_NAMESPACE=$EKS_SAMPLE_CANDIDATE
fi
: "${EKS_SAMPLE_NAMESPACE:?Namespace creation failed}"
: "${EKS_SAMPLE_UID:?Namespace UID missing}"
cat > "$EKS_SAMPLE_DIR/sample-app.yaml" << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-app
namespace: ${EKS_SAMPLE_NAMESPACE}
spec:
replicas: 2
selector:
matchLabels:
app: sample-app
template:
metadata:
labels:
app: sample-app
spec:
automountServiceAccountToken: false
containers:
- name: app
image: nginx:1.30.4-alpine
ports:
- name: http
containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
readinessProbe:
httpGet:
path: /
port: http
---
apiVersion: v1
kind: Service
metadata:
name: sample-app-service
namespace: ${EKS_SAMPLE_NAMESPACE}
spec:
type: ClusterIP
selector:
app: sample-app
ports:
- port: 80
targetPort: http
EOF
eks_kubectl apply -f "$EKS_SAMPLE_DIR/sample-app.yaml"
eks_kubectl -n "$EKS_SAMPLE_NAMESPACE" rollout status deployment/sample-app --timeout=180s
eks_kubectl -n "$EKS_SAMPLE_NAMESPACE" get pods,services
eks_kubectl -n "$EKS_SAMPLE_NAMESPACE" port-forward --address 127.0.0.1 service/sample-app-service 8080:80
```
포트 포워딩 중 `http://127.0.0.1:8080`을 열고 Ctrl-C로 종료합니다. 이는 ClusterIP 접근 검증이며 외부 로드 밸런서 검증은 아닙니다. NLB에는 Auto Mode의 `eks.amazonaws.com/nlb` 또는 설치·권한 설정된 AWS LBC의 `service.k8s.aws/nlb`를 선택하고 네트워크·비용 조건을 별도로 확인합니다. 실제 배포나 포트 포워딩은 감사에서 실행하지 않았습니다.
## 클러스터 업그레이드
업그레이드 인사이트·제거 API·웹훅·애드온 호환성, 노드 버전, IP 여유와 복구 계획을 먼저 확인합니다. 오래된 노드를 현재 제어면 버전에 맞춘 뒤 다음 지원 마이너로 한 단계씩 진행합니다. 일부 구성 요소는 제어면보다 먼저 호환 버전으로 갱신해야 합니다.
Auto Mode가 제어면 마이너 버전을 21일마다 올리는 것은 아닙니다. 운영자가 버전 업그레이드를 계획하고 지원 정책에 따른 자동 업그레이드도 고려합니다.
```bash
aws eks describe-cluster-versions --region "${EKS_REGION:?}" --output table
aws eks describe-cluster --name "${EKS_CLUSTER_NAME:?}" --region "$EKS_REGION" \
--query 'cluster.{version:version,status:status}' --output table
eks_kubectl get nodes
# After readiness/compatibility review, choose the next supported minor.
: "${NEXT_MINOR_VERSION:?Select one supported minor step, for example 1.35 to 1.36}"
EKS_UPDATE_ID=$(aws eks update-cluster-version --name "$EKS_CLUSTER_NAME" \
--region "$EKS_REGION" --kubernetes-version "$NEXT_MINOR_VERSION" \
--query update.id --output text)
: "${EKS_UPDATE_ID:?Update request failed}"
aws eks describe-update --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--update-id "$EKS_UPDATE_ID" --query 'update.{status:status,errors:errors}'
# Alternative interface; do not execute both:
# eksctl upgrade cluster --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" --version "$NEXT_MINOR_VERSION" --approve
```
업데이트가 `Successful`인지 확인한 후 데이터 플레인으로 진행합니다. Auto Mode 노드는 AWS가 점진적으로 갱신하며 일반 관리형 노드 그룹은 별도 업데이트가 필요합니다.
```bash
# Conventional managed node group only, after the control plane update is Successful.
aws eks update-nodegroup-version --cluster-name "${EKS_CLUSTER_NAME:?}" \
--region "${EKS_REGION:?}" --nodegroup-name "${EKS_NODEGROUP_NAME:?}" \
--kubernetes-version "${NEXT_MINOR_VERSION:?}"
```
자체 관리형/Hybrid Nodes, Fargate Pod 재생성, 애드온·kubectl 갱신을 각 방식에 맞게 계획합니다. PDB와 여유 용량을 확인하고 실패를 강제 옵션으로 숨기지 않습니다. 조건을 만족하는 제어면 업그레이드는 완료 후 7일 이내 이전 마이너로 롤백할 수 있지만, 노드·애드온·API 호환성과 지원 정책 제약이 있으며 앱 데이터 복구를 대신하지 않습니다.
참고: [Upgrade procedure](https://docs.aws.amazon.com/eks/latest/userguide/update-cluster.html) · [Rollback conditions](https://docs.aws.amazon.com/eks/latest/userguide/rollback-cluster.html)
## 클러스터 삭제
삭제는 선택한 생성 도구의 원래 상태·계정·리전에서 수행합니다. 먼저 앱과 Ingress/LoadBalancer Service를 담당 컨트롤러가 실행 중일 때 제거하고 완료를 확인합니다. PVC/PV·스냅샷·로그의 보존·복구 결정을 먼저 내립니다. 타임아웃이나 finalizer 문제를 강제로 우회하지 않습니다.
### 실습 네임스페이스 정리
```bash
EKS_SAMPLE_CLEANUP_OK=true
if [[ -n ${EKS_SAMPLE_NAMESPACE:-} && -n ${EKS_SAMPLE_UID:-} ]]; then
current_uid=$(eks_kubectl get namespace "$EKS_SAMPLE_NAMESPACE" -o jsonpath='{.metadata.uid}') || current_uid=""
if [[ "$current_uid" = "$EKS_SAMPLE_UID" ]]; then
eks_kubectl delete namespace "$EKS_SAMPLE_NAMESPACE" --wait=true --timeout=180s || EKS_SAMPLE_CLEANUP_OK=false
else
EKS_SAMPLE_CLEANUP_OK=false
fi
elif [[ -n ${EKS_SAMPLE_NAMESPACE:-} || -n ${EKS_SAMPLE_UID:-} ]]; then
EKS_SAMPLE_CLEANUP_OK=false
fi
# Remove local files only after successful sample cleanup.
if [[ "$EKS_SAMPLE_CLEANUP_OK" = true ]]; then
if [[ -n ${EKS_SAMPLE_DIR:-} ]]; then
rm -f -- "$EKS_SAMPLE_DIR/sample-app.yaml"
rmdir -- "$EKS_SAMPLE_DIR"
fi
if [[ -n ${EKS_ACCESS_DIR:-} ]]; then
rm -f -- "$EKS_ACCESS_DIR/rbac.yaml"
rmdir -- "$EKS_ACCESS_DIR"
fi
else
printf 'Sample cleanup could not be verified; stop before deleting the cluster\n' >&2
fi
```
정리가 실패하면 여기서 중단하고 원인을 해결합니다. 다음 ARN은 현재 조회 결과를 무조건 복사하지 말고 생성 결과나 소유한 IaC state의 값과 대조합니다.
```bash
: "${EKS_EXPECTED_CLUSTER_ARN:?Copy the ARN from the creation result or owning IaC state}"
: "${EKS_CLUSTER_NAME:?}"
: "${EKS_REGION:?}"
EKS_DELETE_TARGET_VERIFIED=false
if [[ ${EKS_SAMPLE_CLEANUP_OK:-false} != true ]]; then
printf 'Complete the sample cleanup check above first\n' >&2
else
current_arn=$(aws eks describe-cluster --name "$EKS_CLUSTER_NAME" \
--region "$EKS_REGION" --query cluster.arn --output text) || current_arn=""
if [[ "$current_arn" = "$EKS_EXPECTED_CLUSTER_ARN" ]]; then
EKS_DELETE_TARGET_VERIFIED=true
aws eks list-nodegroups --cluster-name "$EKS_CLUSTER_NAME" --region "$EKS_REGION"
aws eks list-fargate-profiles --cluster-name "$EKS_CLUSTER_NAME" --region "$EKS_REGION"
else
printf 'Target mismatch; stop and check the account, region and owning tool\n' >&2
fi
fi
```
### eksctl을 사용한 삭제
eksctl이 만든 클러스터에만 이 방법을 사용합니다.
```bash
if [[ ${EKS_DELETE_TARGET_VERIFIED:-false} = true ]]; then
eksctl delete cluster --name "${EKS_CLUSTER_NAME:?}" --region "${EKS_REGION:?}" --wait
fi
```
### AWS CLI를 사용한 삭제
CLI로 직접 생성한 클러스터용입니다. 앞에서 조회한 소유 노드 그룹/프로필 이름을 설정하고 각 리소스에 대해 반복합니다. 삭제 완료를 기다리고 남은 리소스가 없는지 확인한 뒤 클러스터를 삭제합니다.
```bash
# For a cluster created directly with the AWS CLI, after workload/data cleanup.
if [[ ${EKS_DELETE_TARGET_VERIFIED:-false} = true ]]; then
if [[ -n ${EKS_NODEGROUP_NAME:-} ]]; then
aws eks delete-nodegroup --cluster-name "$EKS_CLUSTER_NAME" \
--nodegroup-name "$EKS_NODEGROUP_NAME" --region "$EKS_REGION" &&
aws eks wait nodegroup-deleted --cluster-name "$EKS_CLUSTER_NAME" \
--nodegroup-name "$EKS_NODEGROUP_NAME" --region "$EKS_REGION"
fi
if [[ -n ${EKS_FARGATE_PROFILE_NAME:-} ]]; then
aws eks delete-fargate-profile --cluster-name "$EKS_CLUSTER_NAME" \
--fargate-profile-name "$EKS_FARGATE_PROFILE_NAME" --region "$EKS_REGION" &&
aws eks wait fargate-profile-deleted --cluster-name "$EKS_CLUSTER_NAME" \
--fargate-profile-name "$EKS_FARGATE_PROFILE_NAME" --region "$EKS_REGION"
fi
remaining_nodes=$(aws eks list-nodegroups --cluster-name "$EKS_CLUSTER_NAME" \
--region "$EKS_REGION" --query 'length(nodegroups)' --output text) || remaining_nodes=unknown
remaining_profiles=$(aws eks list-fargate-profiles --cluster-name "$EKS_CLUSTER_NAME" \
--region "$EKS_REGION" --query 'length(fargateProfileNames)' --output text) || remaining_profiles=unknown
if [[ "$remaining_nodes" = 0 && "$remaining_profiles" = 0 ]]; then
aws eks delete-cluster --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" &&
aws eks wait cluster-deleted --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION"
else
printf 'Owned node groups/Fargate profiles remain, or their status could not be verified\n' >&2
fi
fi
```
### Terraform을 사용한 삭제
원래 구성·backend·workspace에서 리소스와 계정을 검토한 삭제 계획만 적용합니다.
```bash
if [[ ${EKS_DELETE_TARGET_VERIFIED:-false} = true ]]; then
terraform plan -destroy -out=reviewed-destroy.tfplan &&
terraform apply reviewed-destroy.tfplan
fi
```
### CDK를 사용한 삭제
원래 앱·환경변수·계정·리전에서 대상 스택을 확인합니다.
```bash
if [[ ${EKS_DELETE_TARGET_VERIFIED:-false} = true ]]; then
npx cdk list
npx cdk destroy EksAutoModeStack
fi
```
완료 후 실제 EKS/CloudFormation 상태와 남은 ELB·디스크·NAT·로그·IAM 리소스를 확인합니다. Auto Mode 역할과 정책은 관리형 인프라 삭제가 끝날 때까지 유지합니다. VPC·IAM 역할·로그·보존 데이터가 다른 도구/소유자에게 속한다면 클러스터 삭제만으로 제거된다고 가정하지 않습니다. 감사에서는 삭제 명령을 실행하지 않았습니다.
## 결론
EKS 클러스터 생성에는 여러 가지 방법이 있으며, 각각의 장단점이 있습니다:
- **EKS Auto Mode**: 인프라 운영을 자동화하지만 앱의 운영 준비 상태는 별도 검증 필요
- **eksctl**: 간단하고 빠른 클러스터 생성
- **AWS Management Console**: GUI를 통한 직관적인 생성
- **AWS CLI**: 스크립트 자동화에 적합
- **Terraform**: 인프라를 코드로 관리
- **AWS CDK**: 프로그래밍 언어를 사용한 인프라 정의
프로덕션에서는 요구 사항에 맞는 컴퓨팅 모델과 인프라 도구를 선택하고 IAM·연결성·용량·중단·복구 동작을 검증합니다. 템플릿이나 클러스터 생성 성공만으로 운영 준비 상태가 입증되지는 않습니다.
### 감사 검증 범위
두 언어의 전체 문서를 읽고 공식 문서·스키마와 대조했습니다. 셸/JSON/YAML, CDK 컴파일·합성, Terraform validate 및 모의 실패 경로를 검증했습니다. AWS 프로비저닝·업그레이드·삭제, 실제 앱 실행·부하 시험·비용 측정은 수행하지 않았습니다.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/02-eks-cluster-creation-part1
----------------------------------------
# Part 1: 사전 요구 사항
> **마지막 업데이트**: 2026년 9월 11일
Amazon EKS 클러스터를 생성하는 방법은 여러 가지가 있습니다. 이 장에서는 다양한 도구와 방법을 사용하여 EKS 클러스터를 생성하는 방법을 알아보겠습니다.
## 목차
1. [사전 요구 사항](#사전-요구-사항)
2. [eksctl](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part2.md)
3. [AWS Management Console 및 CLI](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part3.md)
4. [Terraform](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part4.md)
5. [액세스·검증·업그레이드·삭제](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part5.md)
6. [종합 가이드와 CDK](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation.md)
## 사전 요구 사항
EKS 클러스터를 생성하기 전에 다음과 같은 사전 요구 사항이 필요합니다:
### 1. AWS 계정
유효한 AWS 계정이 필요합니다. AWS 계정이 없는 경우 [AWS 웹사이트](https://aws.amazon.com/)에서 가입할 수 있습니다.
### 2. IAM 권한
필요한 권한은 생성 도구와 직접 관리할 리소스에 따라 달라집니다. `eks:*`, `ec2:*`, `iam:*`, `cloudformation:*`를 모든 리소스에 부여하는 정책을 필수 최소 권한으로 취급하지 않습니다.
| 작업 | 검토할 권한 범위 |
| --- | --- |
| EKS 클러스터·노드 그룹 관리 | 필요한 EKS 작업과 대상 리소스 |
| 기존 IAM 역할 전달 | 승인된 역할 ARN의 `iam:PassRole` 및 서비스 조건 |
| IAM 역할·정책·OIDC 제공자 생성 | 도구가 관리하는 IAM 리소스와 이름·태그 범위 |
| 네트워크 생성 | 새 VPC·서브넷·보안 그룹에 필요한 EC2 작업 |
| eksctl/CDK 사용 | 해당 CloudFormation 스택, 실행 역할, 부트스트랩 리소스 |
프로비저닝 사용자, 클러스터 서비스 역할, 노드 역할은 별개입니다. SCP·권한 경계·세션 정책도 적용됩니다. 합성된 템플릿/계획을 기준으로 조직의 프로비저닝 권한을 검토합니다. Auto Mode의 역할 요구 사항은 일반 노드 그룹과 다릅니다.
참고: [EKS IAM 작업과 리소스](https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonelastickubernetesservice.html), [Auto Mode 역할](https://docs.aws.amazon.com/eks/latest/userguide/auto-cluster-iam-role.html).
### 3. 도구 설치
#### AWS CLI
[AWS CLI v2 공식 설치 지침](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)에서 OS/CPU에 맞는 패키지를 선택하고 서명 검증 절차를 따릅니다. 이전 v1/v2 설치가 있다면 업데이트·마이그레이션 절차를 먼저 확인합니다.
| 환경 | 공식 패키지/설치 방식 |
| --- | --- |
| macOS | 서명된 `AWSCLIV2.pkg` |
| Linux x86_64 | `awscli-exe-linux-x86_64.zip` 및 PGP 서명 검증 |
| Linux ARM64 | `awscli-exe-linux-aarch64.zip` 및 PGP 서명 검증 |
| Windows | 지원되는 Windows용 MSI 설치 프로그램 |
Linux x86_64 패키지를 ARM 시스템에 그대로 사용하지 않습니다. 설치 후 `aws --version`으로 실제 실행되는 CLI를 확인합니다. 조직에서 IAM Identity Center를 사용하는 경우 다음과 같이 승인된 프로필을 구성합니다. 다른 페더레이션 방식을 사용하는 조직은 그 절차를 따르며 장기 액세스 키를 전제로 하지 않습니다.
```bash
aws configure sso --profile eks-docs
aws sso login --profile eks-docs
aws sts get-caller-identity --profile eks-docs
export AWS_PROFILE=eks-docs
```
참고: [IAM Identity Center 인증](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html). 이후 명령에서도 승인된 계정·역할·리전을 유지합니다.
#### kubectl과 eksctl — Linux/macOS
이 장의 EKS 1.36 예제에는 kubectl **1.36.4**, eksctl **0.230.0**을 기준으로 합니다. kubectl은 서버와 같은 마이너 버전을 권장하며 허용되는 차이는 ±1 마이너입니다. 업스트림 `stable.txt`의 최신 마이너를 이전 EKS 클러스터에 무조건 설치하지 않습니다.
아래 Bash 예제는 AMD64/ARM64를 구분하고 공식 체크섬을 확인한 뒤 설치합니다. `curl`, `tar`, `awk`와 `sha256sum` 또는 `shasum`이 필요하며 `/usr/local/bin` 설치는 관리자 권한이 필요합니다. 다운로드/검증 실패 시 설치를 중단합니다.
```bash
(
set -e
case "$(uname -s)" in
Linux) EKS_TOOL_OS=linux; EKS_ARCHIVE_OS=Linux ;;
Darwin) EKS_TOOL_OS=darwin; EKS_ARCHIVE_OS=Darwin ;;
*) printf 'Use the official installer for this operating system\n' >&2; exit 1 ;;
esac
case "$(uname -m)" in
x86_64) EKS_TOOL_ARCH=amd64 ;;
aarch64|arm64) EKS_TOOL_ARCH=arm64 ;;
*) printf 'Select a supported CPU architecture\n' >&2; exit 1 ;;
esac
EKS_TOOL_ARCHIVE="eksctl_${EKS_ARCHIVE_OS}_${EKS_TOOL_ARCH}.tar.gz"
EKS_TOOL_DIR=$(mktemp -d)
: "${EKS_TOOL_DIR:?}"
trap 'rm -f -- "$EKS_TOOL_DIR/kubectl" "$EKS_TOOL_DIR/kubectl.sha256" "$EKS_TOOL_DIR/eksctl" "$EKS_TOOL_DIR/eksctl_checksums.txt" "$EKS_TOOL_DIR/$EKS_TOOL_ARCHIVE" "$EKS_TOOL_DIR/selected.sha256"; rmdir -- "$EKS_TOOL_DIR"' EXIT
cd "$EKS_TOOL_DIR" || exit 1
verify_sha() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum --check "$1"
else
shasum -a 256 --check "$1"
fi
}
EKS_KUBECTL_VERSION=v1.36.4
curl -fL "https://dl.k8s.io/release/$EKS_KUBECTL_VERSION/bin/$EKS_TOOL_OS/$EKS_TOOL_ARCH/kubectl" -o kubectl || exit 1
curl -fL "https://dl.k8s.io/release/$EKS_KUBECTL_VERSION/bin/$EKS_TOOL_OS/$EKS_TOOL_ARCH/kubectl.sha256" -o kubectl.sha256 || exit 1
printf '%s kubectl\n' "$(tr -d '[:space:]' < kubectl.sha256)" > selected.sha256
verify_sha selected.sha256 || exit 1
EKSCTL_VERSION=0.230.0
curl -fL "https://github.com/eksctl-io/eksctl/releases/download/v$EKSCTL_VERSION/$EKS_TOOL_ARCHIVE" -o "$EKS_TOOL_ARCHIVE" || exit 1
curl -fL "https://github.com/eksctl-io/eksctl/releases/download/v$EKSCTL_VERSION/eksctl_checksums.txt" -o eksctl_checksums.txt || exit 1
awk -v name="$EKS_TOOL_ARCHIVE" '$2 == name {print; count++} END {if (count != 1) exit 1}' \
eksctl_checksums.txt > selected.sha256 || exit 1
verify_sha selected.sha256 || exit 1
tar -xzf "$EKS_TOOL_ARCHIVE" eksctl || exit 1
sudo install -m 0755 kubectl /usr/local/bin/kubectl || exit 1
sudo install -m 0755 eksctl /usr/local/bin/eksctl || exit 1
kubectl version --client
eksctl version
)
```
공식 절차: [Linux kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/), [macOS kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl-macos/), [eksctl 설치](https://eksctl.io/installation/).
#### Windows
PowerShell에서 [공식 kubectl 설치 절차](https://kubernetes.io/docs/tasks/tools/install-kubectl-windows/)를 따릅니다. 이 장의 AMD64 예제에는 [kubectl 1.36.4](https://dl.k8s.io/release/v1.36.4/bin/windows/amd64/kubectl.exe)와 같은 경로의 `.sha256` 파일을 사용합니다. [eksctl 0.230.0 릴리스](https://github.com/eksctl-io/eksctl/releases/tag/v0.230.0)에서 CPU에 맞는 Windows ZIP과 `eksctl_checksums.txt`를 선택합니다.
`Get-FileHash -Algorithm SHA256` 결과를 공식 해시와 비교하고 불일치하면 중단합니다. 검증 후 ZIP을 풀고 실행 파일 디렉터리를 PATH에 추가한 뒤 `kubectl version --client`, `eksctl version`을 확인합니다. PowerShell 구문을 Bash로 실행하지 않습니다.
AWS CLI 예제의 JSON 생성을 위해서는 `jq`도 준비합니다. 실제 다운로드·설치·로그인은 감사에서 실행하지 않았습니다.
### 4. VPC 및 서브넷

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-02-eks-cluster-creation-part1-0.html)
이 그림은 NAT를 사용하는 한 가지 배치 예시이며 인터넷 경로가 필수라는 뜻은 아닙니다.
리전 EKS 클러스터에는 같은 VPC의 서로 다른 AZ에 있는 서브넷이 최소 2개 필요합니다. 각 클러스터 서브넷은 EKS용 IP가 최소 6개 남아 있어야 하며 AWS는 16개 이상을 권장합니다. 노드·Pod·로드 밸런서·업그레이드에 필요한 IP는 별도로 계획합니다. VPC DNS 호스트명과 DNS 해석도 활성화해야 합니다.
인터넷 경로가 모든 EKS 클러스터의 필수 조건은 아닙니다. 노드와 워크로드가 API·이미지·필요한 AWS 서비스에 접근할 수 있어야 하며, NAT/인터넷 경로나 필요한 VPC 엔드포인트 및 미러 이미지를 준비합니다. 프라이빗 Kubernetes 엔드포인트는 VPC 또는 연결된 네트워크에서 올바른 DNS·라우팅으로 접근합니다.
#### EKS 클러스터를 위한 VPC 태그
`kubernetes.io/cluster/` VPC 태그는 오래된 클러스터의 레거시 방식이며 현재 EKS 생성의 보편적 필수 조건이 아닙니다. 로드 밸런서의 서브넷 자동 검색에는 선택한 컨트롤러의 규칙을 따릅니다:
- 퍼블릭 로드 밸런서용 서브넷: `kubernetes.io/role/elb=1`
- 내부 로드 밸런서용 서브넷: `kubernetes.io/role/internal-elb=1`
태그만으로 라우팅·보안 그룹·가용 IP가 구성되지는 않습니다. [VPC/서브넷 요구 사항](https://docs.aws.amazon.com/eks/latest/userguide/network-reqs.html)과 [인터넷 없이 운영하는 클러스터](https://docs.aws.amazon.com/eks/latest/userguide/private-clusters.html)를 확인합니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [EKS 클러스터 생성 - 1부 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/02-eks-cluster-creation-part1-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/02-eks-cluster-creation-part2
----------------------------------------
# Part 2: eksctl을 사용한 클러스터 생성
> **마지막 업데이트**: 2026년 9월 11일
[Part 1 사전 준비](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part1.md)를 완료하고 전용 교육용 계정·클러스터 및 임시 kubeconfig(`EKS_KUBECONFIG`)를 사용합니다. 예제는 대안이며 하나의 연속 스크립트가 아닙니다. `EKS_CLUSTER_NAME`·`EKS_REGION`을 검토한 대상으로 설정하고 승인된 기존 자격 증명을 사용합니다. AWS 리소스 생성 명령은 과금되며 이 감사에서는 프로비저닝하지 않았습니다. 예제는 eksctl 0.230.0과 EKS 1.36을 기준으로 검토했습니다.
## eksctl을 사용한 클러스터 생성
eksctl은 EKS의 명령줄·선언적 구성 인터페이스를 제공합니다. eksctl은 CloudFormation을 사용하여 EKS 클러스터와 관련 리소스를 생성합니다.
다음 다이어그램은 eksctl을 사용한 EKS 클러스터 생성 프로세스를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-02-eks-cluster-creation-part2-0.html)
새 VPC를 만드는 흐름의 예시입니다. 기존 VPC를 재사용할 수 있고 소요 시간은 달라지며 kubeconfig 작성만으로 접근 권한이나 준비 상태가 확보되지는 않습니다.
### 기본 클러스터 생성
검토한 구성 파일로 기본 클러스터를 생성합니다:
```bash
eksctl create cluster --config-file cluster.yaml --kubeconfig "${EKS_KUBECONFIG:?}"
```
명령 실행 전에 아래 `cluster.yaml`을 읽고 수정합니다. EKS 1.36, AL2023, 기존 VPC 서브넷과 노드 그룹 용량을 명시하는 예제입니다. 예시 식별자와 문서용 CIDR을 승인된 실제 값으로 바꿉니다. 모든 eksctl 버전의 기본값을 설명하는 목록이 아닙니다.
### 구성 파일을 사용한 클러스터 생성
더 복잡한 구성의 경우 YAML 파일을 사용하여 클러스터를 정의할 수 있습니다:
```yaml
# cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-cluster
region: us-west-2
version: '1.36'
vpc:
id: vpc-12345678
subnets:
private:
us-west-2a:
id: subnet-12345678
us-west-2b:
id: subnet-87654321
public:
us-west-2a:
id: subnet-23456789
us-west-2b:
id: subnet-98765432
clusterEndpoints:
privateAccess: true
publicAccess: true
publicAccessCIDRs:
- 203.0.113.10/32
managedNodeGroups:
- name: ng-1
instanceType: m5.large
desiredCapacity: 2
minSize: 1
maxSize: 3
privateNetworking: true
volumeSize: 80
volumeType: gp3
amiFamily: AmazonLinux2023
disableIMDSv1: true
- name: ng-2
instanceType: c5.xlarge
desiredCapacity: 2
privateNetworking: true
spot: true
amiFamily: AmazonLinux2023
disableIMDSv1: true
cloudWatch:
clusterLogging:
enableTypes:
- api
- audit
- authenticator
- controllerManager
- scheduler
fargateProfiles:
- name: fp-default
selectors:
- namespace: default
labels:
env: fargate
iam:
withOIDC: true
accessConfig:
authenticationMode: API
```
이 구성 파일을 사용하여 클러스터를 생성하려면 다음 명령을 실행합니다:
```bash
eksctl create cluster -f cluster.yaml --kubeconfig "${EKS_KUBECONFIG:?}"
```
위 구성은 EC2 노드와 선택적 앱 Fargate 프로필을 보여 줍니다. CoreDNS는 EC2에 두며, Fargate로 옮기려면 프로필 외에 CoreDNS의 컴퓨팅 설정도 검토해야 합니다.
이 구성은 노드 역할의 포괄적인 애드온 권한을 제거했습니다. `iam.withOIDC`를 활성화했을 때 eksctl이 생성하는 CNI IRSA와 정책을 확인하고 다른 AWS 연동 컨트롤러에는 별도 역할을 구성합니다. 실제 애드온 구성과 일반 노드 역할의 CNI 권한 대안도 검토합니다. API 인증에도 운영자의 적절한 EKS 액세스 항목·정책이 필요합니다. 최소·최대 노드 수는 Cluster Autoscaler를 설치하지 않습니다.
### 관리형 노드 그룹 생성
다음 다이어그램은 EKS 클러스터의 관리형 노드 그룹 아키텍처를 보여줍니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-02-eks-cluster-creation-part2-1.html)
인프라를 조정하는 주체는 AWS EKS 관리형 노드 그룹 서비스와 Auto Scaling 그룹입니다. 그림은 EKS가 AMI를 선택하는 기본 경로를 보여주며 사용자 지정 AMI에는 별도로 검토한 부트스트랩 구성이 필요합니다. Kubernetes는 생성된 노드에 포드를 스케줄링합니다.
기존 클러스터에 관리형 노드 그룹을 추가하려면 다음 명령을 실행합니다:
```bash
eksctl create nodegroup \
--cluster my-cluster \
--region us-west-2 \
--name my-nodegroup \
--node-type m5.large \
--nodes 3 \
--nodes-min 1 \
--nodes-max 5 \
--managed --node-ami-family AmazonLinux2023 --node-private-networking
```
또는 구성 파일을 사용할 수 있습니다:
```yaml
# nodegroup.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-cluster
region: us-west-2
managedNodeGroups:
- name: my-nodegroup
instanceType: m5.large
desiredCapacity: 3
minSize: 1
maxSize: 5
volumeSize: 80
volumeType: gp3
amiFamily: AmazonLinux2023
privateNetworking: true
disableIMDSv1: true
ssh:
allow: false
```
```bash
eksctl create nodegroup -f nodegroup.yaml
```
### Fargate 프로필 생성
프로필은 일치하는 포드를 선택하며 포드 생성이나 애플리케이션 복제본 확장을 수행하지 않습니다. 프라이빗 서브넷, Fargate 포드 실행 역할, 지원되는 워크로드 기능을 확인합니다. 애플리케이션 프로필만으로 CoreDNS가 Fargate로 이동하지는 않습니다. 아래 CLI와 파일 예제는 대안입니다.
Fargate 프로필은 네임스페이스와 레이블로 포드를 선택합니다. 프로필이 겹치면 `eks.amazonaws.com/fargate-profile` 레이블로 일치하는 프로필을 명시할 수 있으며, AWS는 여러 프로필이 일치할 때 프로필 이름의 영숫자 정렬 기준 선택을 설명합니다. 시작 지연은 이미지·용량·환경에 따라 달라집니다.
Fargate 프로필을 생성하려면 다음 명령을 실행합니다:
```bash
eksctl create fargateprofile \
--cluster my-cluster \
--region us-west-2 \
--name my-fargate-profile \
--namespace default \
--labels env=fargate
```
또는 구성 파일을 사용할 수 있습니다:
```yaml
# fargate.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-cluster
region: us-west-2
fargateProfiles:
- name: my-fargate-profile
selectors:
- namespace: default
labels:
env: fargate
```
```bash
eksctl create fargateprofile -f fargate.yaml
```
### 클러스터 업데이트
지원되는 마이너 버전을 한 단계씩 업그레이드합니다. 먼저 EKS 업그레이드 인사이트, 제거된 API, kubelet 버전 차이, 애드온, 용량, 워크로드 중단을 검토합니다. EKS 지원 버전은 업스트림 릴리스와 별개입니다. 첫 eksctl 명령은 변경을 미리 확인하고 `--approve`가 실제 업데이트를 시작합니다.
```bash
aws eks describe-cluster-versions --region "${EKS_REGION:?}" --output table
aws eks describe-cluster --name "${EKS_CLUSTER_NAME:?}" --region "$EKS_REGION" \
--query 'cluster.{version:version,status:status}' --output table
# Select the next supported minor after compatibility/readiness review.
eksctl upgrade cluster --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--version "${NEXT_MINOR_VERSION:?}"
# This separate command actually starts the reviewed control-plane upgrade.
eksctl upgrade cluster --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--version "$NEXT_MINOR_VERSION" --approve
```
컨트롤 플레인 업데이트 성공을 기다린 후 검토한 순서에 따라 호환 애드온과 관리형 노드 그룹을 갱신합니다. 다음 노드 그룹 명령은 EKS가 선택하는 AMI의 업데이트이며, 사용자 지정 AMI에는 같은 시작 템플릿의 검토한 버전이 필요합니다. 관리형 노드 업데이트는 인스턴스를 교체하며 컨트롤 플레인 갱신과 별개입니다. PDB와 여유 용량은 중단 제어에 도움이 되지만 가용성을 보장하지는 않습니다.
```bash
# After control-plane completion and add-on/workload compatibility checks.
eksctl upgrade nodegroup --cluster "${EKS_CLUSTER_NAME:?}" \
--region "${EKS_REGION:?}" --name "${EKS_NODEGROUP_NAME:?}" --wait
```
### 클러스터 삭제
생성 후 대상 실습 클러스터 ARN을 `EKS_EXPECTED_CLUSTER_ARN`으로 기록합니다. 삭제 전에 컨트롤러가 실행 중일 때 실습 LoadBalancer Service·Ingress를 제거하고 AWS 리소스 정리를 기다리며 PVC 회수 정책과 보존 데이터를 확인합니다. 제거할 리소스만 있는 클러스터인지 확인합니다. 아래 ARN 검사는 기록한 계정·리전·이름을 확인할 뿐 백업이나 불변 생성 식별자가 아닙니다.
```bash
if CURRENT_CLUSTER_ARN=$(aws eks describe-cluster \
--name "${EKS_CLUSTER_NAME:?}" --region "${EKS_REGION:?}" \
--query cluster.arn --output text) &&
[ "$CURRENT_CLUSTER_ARN" = "${EKS_EXPECTED_CLUSTER_ARN:?Recorded lab cluster ARN required}" ]; then
eksctl delete cluster --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" --wait
else
printf '%s\n' 'Cluster lookup/identity mismatch; no deletion attempted.' >&2
fi
```
이후 CloudFormation 삭제 이벤트와 보존·별도 생성 리소스를 확인합니다. 기존 공유 VPC는 이 예제가 소유한 리소스가 아닙니다. 수명 주기 의존성은 [전체 정리 안내](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part5.md)를 함께 확인합니다.
## EKS 클러스터 라이프사이클 관리
수명 주기는 생성·구성·운영·검토한 업그레이드와 최종 정리를 포함합니다. 컨트롤러가 실행 중일 때 애플리케이션 로드 밸런서 리소스를 제거하고, 이후 노드 그룹·프로필과 클러스터를 제거합니다. 전용 VPC는 의존 리소스가 사라진 뒤에만 제거합니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [EKS 클러스터 생성 - 2부 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/02-eks-cluster-creation-part2-quiz)를 풀어보세요.
## 참고 자료
- [eksctl schema](https://schema.eksctl.io/)
- [EKS versions](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html)
- [AL2023](https://docs.aws.amazon.com/eks/latest/userguide/al2023.html)
- [Fargate profiles](https://docs.aws.amazon.com/eks/latest/userguide/fargate-profile.html)
- [Upgrade EKS](https://docs.aws.amazon.com/eks/latest/userguide/update-cluster.html)
- [Managed node updates](https://docs.aws.amazon.com/eks/latest/userguide/managed-node-update-behavior.html)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/02-eks-cluster-creation-part3
----------------------------------------
# Part 3: AWS Management Console 및 CLI를 사용한 클러스터 생성
> **마지막 업데이트**: 2026년 9월 11일
[Part 1 사전 준비](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part1.md)를 완료합니다. 예제는 과금되는 AWS 리소스를 만드는 교육용 절차이며 이번 감사에서 프로비저닝하거나 프로덕션 구성을 인증하지 않았습니다.
## AWS Management Console을 사용한 클러스터 생성
일반 EC2 관리형 노드 그룹 클러스터를 만드는 절차입니다. **Custom configuration**을 선택하고 **Use EKS Auto Mode**를 끕니다. 빠른 Auto Mode 경로는 역할과 인프라 관리 방식이 다릅니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-02-eks-cluster-creation-part3-0.html)
그림은 예시 흐름이며 생성 시간은 달라집니다. 기본값이 요구사항을 충족한다고 가정하지 말고 생성자 접근과 노드 준비 상태를 명시적으로 검토합니다.
### 클러스터 구성
대상 계정·리전의 EKS 콘솔에서 **Add cluster → Create**를 선택하고 위의 사용자 지정 구성을 사용합니다.
- 고유한 클러스터 이름과 현재 EKS 지원 버전을 선택합니다. 아래 CLI 예제는 **1.36**이며 업스트림 Kubernetes 릴리스와 EKS 지원 목록은 별개입니다.
- `eks.amazonaws.com` 신뢰와 `AmazonEKSClusterPolicy`를 가진 검토한 클러스터 IAM 역할을 선택합니다. 프로비저닝 자격 증명에는 범위를 제한한 `iam:PassRole`, 필요한 서비스 연결 역할 생성 등 적절한 EKS·IAM 권한도 필요합니다.
- 표준·확장 지원 정책, 태그, 선택 기능을 검토합니다. EKS 1.28 이상은 AWS 소유 키로 Kubernetes API 데이터를 이미 봉투 암호화하며 고객 관리 KMS 키는 선택 사항입니다.
- API 액세스 항목 인증을 선택합니다. 이 실습은 생성자의 자동 관리자 접근을 허용하지 않고 기존 운영자 역할의 명시적 액세스 항목을 구성합니다. 이후 kubeconfig를 만들어도 접근 권한이 생기지는 않습니다.
### 네트워킹 지정
EKS 요구사항에 맞는 기존 VPC를 선택하거나 완전한 네트워크 설계로 먼저 준비합니다. 서로 다른 AZ의 적합한 서브넷을 최소 두 개 선택합니다. 클러스터 서브넷마다 여유 IP가 최소 여섯 개 필요하고 AWS는 열여섯 개 이상을 권장합니다. 노드·포드·업데이트 용량도 별도로 계획합니다.
VPC DNS, 라우팅, IP 계열·서비스 CIDR 중복, 필요한 AWS 서비스·레지스트리 연결을 검토합니다. 적절한 NAT·VPC 엔드포인트가 있는 프라이빗 노드 서브넷을 사용합니다. API 엔드포인트 선택은 다음과 같습니다:
- **퍼블릭:** 퍼블릭 경로를 사용하며 `publicAccessCidrs`로 클라이언트 송신 주소 범위를 제한합니다.
- **프라이빗:** 필요한 경로·DNS·보안 그룹·IAM/Kubernetes 권한을 갖춘 VPC 내부 또는 연결된 네트워크에서 접근합니다.
- **퍼블릭 및 프라이빗:** 두 경로를 사용하되 퍼블릭 CIDR을 제한하고 프라이빗 경로도 확인합니다.
EKS가 클러스터 보안 그룹을 생성합니다. 추가 그룹은 선택 사항이며 클러스터 인터페이스에 연결되고 모든 노드 그룹에 자동 적용되지는 않습니다. 이 그룹에 TCP 443을 전면 개방하는 규칙으로 퍼블릭 API를 제어하지 않습니다.
### 로깅 구성
**Configure observability**에서 필요한 컨트롤 플레인 로그(`api`, `audit`, `authenticator`, `controllerManager`, `scheduler`)를 선택합니다. 선택적 지표 기능은 별도로 검토합니다. CloudWatch 수집·저장·쿼리 비용이 발생하며 로그는 최선 노력 방식으로 전달됩니다.
### 애드온 선택
일반 EC2 클러스터에서는 검토한 대체 구현이 없다면 호환 VPC CNI·CoreDNS·kube-proxy를 유지합니다. **Configure selected add-ons settings**에서 호환 버전과 필요한 애드온 IAM 자격 증명을 구성합니다. 선택적 컨트롤러·스토리지 드라이버는 별도 설치와 권한이 필요합니다.
### 검토 및 생성
역할, 접근 모드, 네트워크, 애드온 설정을 검토한 뒤 클러스터를 생성하고 **ACTIVE**를 기다립니다. kubectl 접근 전에 운영자 액세스 항목을 구성합니다.
### 노드 그룹 추가
클러스터의 **Compute**에서 관리형 노드 그룹을 추가합니다:
1. 사용하지 않는 그룹 이름과 검토한 EC2 노드 IAM 역할을 선택합니다.
2. **AL2023 x86_64**, 호환 인스턴스 유형, 워크로드에 맞는 디스크·크기 범위를 선택합니다. CLI의 `m5.large`, 80 GiB, 1–3 범위는 예시이며 측정된 크기 권장값이 아닙니다.
3. 실제 프라이빗 서브넷을 선택합니다. 별도로 검토한 접근 경로가 필요하지 않으면 SSH는 비활성화합니다.
4. 그룹을 생성하고 **ACTIVE**, 노드의 정상 **Ready** 상태를 기다립니다. 최소·최대 범위만으로 워크로드 기반 노드 오토스케일러가 설치되지는 않습니다.
노드 역할에는 워커·이미지 풀 권한이 필요합니다. CNI 전용 워크로드 역할을 우선 검토하고, 아래 CLI의 단순한 IPv4 노드 역할 대안은 한계를 명시하여 사용합니다.
## AWS CLI를 사용한 클러스터 생성
일반 IPv4 예제로 EKS 1.36과 AL2023 관리형 노드를 사용합니다. 이 경로와 콘솔 경로 중 하나를 선택합니다. CLI 단계는 같은 Bash 세션에서 사용하지 않는 실습 이름으로 실행하고, 소유권·정리 확인을 위해 전용 응답 파일을 보관합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-02-eks-cluster-creation-part3-1.html)
그림은 일반적인 단계를 보여줍니다. EKS가 클러스터 보안 그룹을 자동 생성하므로 아래의 현재 역할 정책과 명시적인 접근 설정을 사용합니다. 이번 감사에서는 AWS 작업을 실행하지 않았습니다.
```bash
: "${EKS_CLUSTER_NAME:?Choose an unused training cluster name}"
: "${EKS_REGION:?For example us-west-2}"
: "${OPERATOR_ROLE_ARN:?Existing operator IAM role that this login may assume}"
: "${APPROVED_API_CIDR:?Actual approved administration egress CIDR}"
EKS_CREATE_DIR=$(mktemp -d /tmp/eks-console-cli.XXXXXX)
: "${EKS_CREATE_DIR:?}"
EKS_KUBECONFIG="$EKS_CREATE_DIR/kubeconfig"
aws sts get-caller-identity
```
### 1. 클러스터 IAM 역할 생성
다음 명령은 **새** 역할을 만들고 생성 실패 시 중단합니다. 이미 검토한 역할을 재사용한다면 `EKS_CLUSTER_ROLE_ARN`을 설정하고 생성·연결 블록을 건너뜁니다. 이름이 우연히 같은 기존 역할을 변경하지 않습니다.
```bash
cat > "${EKS_CREATE_DIR:?}/cluster-trust.json" << 'EOF'
{
"Version":"2012-10-17",
"Statement":[{
"Effect":"Allow",
"Principal":{"Service":"eks.amazonaws.com"},
"Action":"sts:AssumeRole"
}]
}
EOF
aws iam create-role --role-name "${NEW_CLUSTER_ROLE_NAME:?Unused role name}" \
--assume-role-policy-document "file://$EKS_CREATE_DIR/cluster-trust.json" \
--query Role --output json > "$EKS_CREATE_DIR/created-cluster-role.json" || exit 1
EKS_CLUSTER_ROLE_ARN=$(jq -er '.Arn' "$EKS_CREATE_DIR/created-cluster-role.json") || exit 1
aws iam attach-role-policy --role-name "$NEW_CLUSTER_ROLE_NAME" \
--policy-arn arn:aws:iam::aws:policy/AmazonEKSClusterPolicy || exit 1
```
### 2. VPC 및 서브넷 생성
검토한 기존 VPC를 사용하거나 아래 공식 템플릿으로 완전한 예제 네트워크를 선택적으로 생성합니다. 템플릿은 AZ 두 개의 퍼블릭·프라이빗 서브넷 각각 두 개, 인터넷 게이트웨이, NAT 게이트웨이·EIP 두 개를 생성합니다. CIDR 중복, 경로, 비용을 먼저 검토합니다. URL의 날짜는 Kubernetes 버전이 아닙니다.
```bash
# Optional new-network path; review the entire template and its CIDR/NAT costs first.
aws cloudformation create-stack --region "${EKS_REGION:?}" \
--stack-name "${NEW_VPC_STACK_NAME:?Unused stack name}" \
--template-url https://s3.us-west-2.amazonaws.com/amazon-eks/cloudformation/2020-10-29/amazon-eks-vpc-private-subnets.yaml \
> "${EKS_CREATE_DIR:?}/vpc-stack.json" || exit 1
aws cloudformation wait stack-create-complete --region "$EKS_REGION" \
--stack-name "$NEW_VPC_STACK_NAME" || exit 1
aws cloudformation describe-stacks --region "$EKS_REGION" \
--stack-name "$NEW_VPC_STACK_NAME" --query 'Stacks[0].Outputs' --output table
```
스택의 `SubnetIds` 출력에는 **네 서브넷 모두** 포함됩니다. 라우팅 테이블·태그로 프라이빗 두 개를 확인한 뒤 `EKS_PRIVATE_SUBNET_A/B`를 설정합니다. 명시적으로 라우팅 테이블에 연결하지 않은 서브넷도 VPC의 기본 라우팅 테이블을 사용합니다.
다음은 AZ·VPC·IP·DNS 기본 조건 검사입니다. 실제 경로, 보안 통제, 레지스트리·S3 접근, 필요한 VPC 엔드포인트도 별도로 확인합니다. VPC와 서브넷 두 개만 생성하면 노드 송신 경로까지 구성되는 것은 아닙니다.
```bash
# Set these IDs after identifying the actual private subnets and their routes.
aws ec2 describe-subnets --region "${EKS_REGION:?}" \
--subnet-ids "${EKS_PRIVATE_SUBNET_A:?}" "${EKS_PRIVATE_SUBNET_B:?}" \
--query Subnets --output json > "${EKS_CREATE_DIR:?}/subnets.json" || exit 1
jq -e 'length == 2 and
(map(.VpcId) | unique | length) == 1 and
(map(.AvailabilityZone) | unique | length) == 2 and
all(.[]; .AvailableIpAddressCount >= 6)' \
"$EKS_CREATE_DIR/subnets.json" >/dev/null || exit 1
EKS_VPC_ID=$(jq -er '.[0].VpcId' "$EKS_CREATE_DIR/subnets.json") || exit 1
aws ec2 describe-vpc-attribute --region "$EKS_REGION" --vpc-id "$EKS_VPC_ID" \
--attribute enableDnsSupport --output json > "$EKS_CREATE_DIR/dns-support.json" || exit 1
aws ec2 describe-vpc-attribute --region "$EKS_REGION" --vpc-id "$EKS_VPC_ID" \
--attribute enableDnsHostnames --output json > "$EKS_CREATE_DIR/dns-hostnames.json" || exit 1
jq -e '.EnableDnsSupport.Value == true' "$EKS_CREATE_DIR/dns-support.json" >/dev/null || exit 1
jq -e '.EnableDnsHostnames.Value == true' "$EKS_CREATE_DIR/dns-hostnames.json" >/dev/null || exit 1
```
### 3. 클러스터 보안 그룹 생성
EKS가 클러스터 생성 중 보안 그룹을 자동 생성합니다. 이 예제에는 `0.0.0.0/0`으로 개방한 별도 그룹이 필요하지 않습니다. 퍼블릭 API는 `publicAccessCidrs`로 제한하며 클러스터 보안 그룹은 프라이빗 경로·노드 통신에 적용됩니다.
추가 그룹이 필요하면 규칙을 검토하고 해당 ID를 명시적으로 구성에 추가합니다. 클러스터 네트워크 요청을 JSON으로 작성합니다:
```bash
# EKS creates the cluster security group; public API restrictions use this CIDR list.
jq -n --arg a "${EKS_PRIVATE_SUBNET_A:?}" --arg b "${EKS_PRIVATE_SUBNET_B:?}" \
--arg cidr "${APPROVED_API_CIDR:?}" '{
subnetIds:[$a,$b],
endpointPublicAccess:true,
endpointPrivateAccess:true,
publicAccessCidrs:[$cidr]
}' > "${EKS_CREATE_DIR:?}/vpc-config.json" || exit 1
```
### 4. EKS 클러스터 생성
`aws eks create-cluster`에는 `--kubernetes-version`을 사용합니다. `--version`은 AWS CLI 버전을 출력하며 API JSON 필드는 `version`입니다. 예제는 생성자의 자동 관리자 접근을 끄고 컨트롤 플레인 로그 다섯 유형과 일반 코어 애드온 부트스트랩을 활성화합니다. CLI 부트스트랩 애드온은 자체 관리형이므로 EKS 관리형 애드온으로 전환하려면 별도의 호환 버전·구성 절차가 필요합니다.
```bash
aws eks describe-cluster-versions --region "${EKS_REGION:?}" --output table
aws eks create-cluster --name "${EKS_CLUSTER_NAME:?}" --region "$EKS_REGION" \
--kubernetes-version 1.36 --role-arn "${EKS_CLUSTER_ROLE_ARN:?}" \
--resources-vpc-config "file://${EKS_CREATE_DIR:?}/vpc-config.json" \
--access-config authenticationMode=API,bootstrapClusterCreatorAdminPermissions=false \
--bootstrap-self-managed-addons \
--logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}' \
--query cluster --output json > "$EKS_CREATE_DIR/created-cluster.json" || exit 1
aws eks wait cluster-active --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" || exit 1
aws eks describe-cluster --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--query 'cluster.{arn:arn,status:status,version:version,vpc:resourcesVpcConfig}'
```
Waiter 실패가 모든 리소스의 롤백을 의미하지는 않습니다. 다음 단계 전에 클러스터 상태와 기록한 응답을 확인합니다. 로깅에는 CloudWatch 비용이 발생합니다.
### 5. 노드 IAM 역할 생성
EC2를 신뢰하는 새 역할을 만듭니다. `AmazonEC2ContainerRegistryPullOnly`는 이미지 풀 권한을 제공합니다. 단순한 **IPv4 실습**에서는 부트스트랩된 VPC CNI가 동작하도록 노드 역할에 CNI 권한을 둡니다. 지원되는 전용 CNI IRSA·Pod Identity 역할을 우선 검토하고 해당 구성이 동작한 뒤에만 대안 권한을 제거합니다. 다른 애플리케이션·컨트롤러의 AWS 권한을 모든 노드에 부여하지 않습니다.
```bash
cat > "${EKS_CREATE_DIR:?}/node-trust.json" << 'EOF'
{
"Version":"2012-10-17",
"Statement":[{
"Effect":"Allow",
"Principal":{"Service":"ec2.amazonaws.com"},
"Action":"sts:AssumeRole"
}]
}
EOF
aws iam create-role --role-name "${NEW_NODE_ROLE_NAME:?Unused role name}" \
--assume-role-policy-document "file://$EKS_CREATE_DIR/node-trust.json" \
--query Role --output json > "$EKS_CREATE_DIR/created-node-role.json" || exit 1
EKS_NODE_ROLE_ARN=$(jq -er '.Arn' "$EKS_CREATE_DIR/created-node-role.json") || exit 1
aws iam attach-role-policy --role-name "$NEW_NODE_ROLE_NAME" \
--policy-arn arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy || exit 1
aws iam attach-role-policy --role-name "$NEW_NODE_ROLE_NAME" \
--policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPullOnly || exit 1
# Simple IPv4 lab fallback only. Prefer a dedicated CNI workload role in production.
aws iam attach-role-policy --role-name "$NEW_NODE_ROLE_NAME" \
--policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy || exit 1
```
### 6. 노드 그룹 생성
검토한 프라이빗 서브넷에 관리형 그룹을 만듭니다. EKS가 선택한 AL2023 AMI의 노드 초기화와 관리형 노드 액세스 항목을 제공합니다. 예제는 SSH 접근과 사용자 지정 시작 템플릿 덮어쓰기를 사용하지 않습니다.
```bash
aws eks create-nodegroup --cluster-name "${EKS_CLUSTER_NAME:?}" --region "${EKS_REGION:?}" \
--nodegroup-name "${NEW_NODEGROUP_NAME:?}" --node-role "${EKS_NODE_ROLE_ARN:?}" \
--subnets "${EKS_PRIVATE_SUBNET_A:?}" "${EKS_PRIVATE_SUBNET_B:?}" \
--ami-type AL2023_x86_64_STANDARD --instance-types m5.large --capacity-type ON_DEMAND \
--disk-size 80 --scaling-config minSize=1,maxSize=3,desiredSize=2 \
--query nodegroup --output json > "${EKS_CREATE_DIR:?}/created-nodegroup.json" || exit 1
aws eks wait nodegroup-active --cluster-name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--nodegroup-name "$NEW_NODEGROUP_NAME" || exit 1
aws eks describe-nodegroup --cluster-name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--nodegroup-name "$NEW_NODEGROUP_NAME" \
--query 'nodegroup.{status:status,health:health,ami:amiType,release:releaseVersion}'
```
### 7. kubeconfig 구성
현재 로그인에서 수임할 수 있는 기존 운영자 IAM 역할을 사용합니다. 프로비저닝 자격 증명에는 EKS 액세스 항목 생성과 실습 접근 정책 연결 권한이 필요합니다. 다음은 이 클러스터의 Kubernetes 관리 권한이며 AWS 계정 전체의 관리자 권한이 아닙니다.
```bash
# The named operator gets cluster-admin only on this training cluster.
aws eks create-access-entry --cluster-name "${EKS_CLUSTER_NAME:?}" --region "${EKS_REGION:?}" \
--principal-arn "${OPERATOR_ROLE_ARN:?}" --type STANDARD || exit 1
aws eks associate-access-policy --cluster-name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--principal-arn "$OPERATOR_ROLE_ARN" \
--policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy \
--access-scope type=cluster || exit 1
aws eks update-kubeconfig --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--role-arn "$OPERATOR_ROLE_ARN" --kubeconfig "${EKS_KUBECONFIG:?}" \
--alias "$EKS_CLUSTER_NAME" || exit 1
```
### 8. 클러스터 확인
대상 컨텍스트, 노드 준비 상태, 시스템 포드를 확인합니다:
```bash
kubectl --kubeconfig "${EKS_KUBECONFIG:?}" config current-context
kubectl --kubeconfig "$EKS_KUBECONFIG" auth can-i get nodes
kubectl --kubeconfig "$EKS_KUBECONFIG" get nodes -o wide
kubectl --kubeconfig "$EKS_KUBECONFIG" get pods -n kube-system
```
인프라의 `ACTIVE` 상태만으로 애플리케이션 준비를 확인할 수 없으며 `get nodes` 결과에서 실제 정상 Ready 상태를 확인해야 합니다. 운영 사용 전에 스케줄링, DNS, 이미지 풀, 애플리케이션 의존성을 검증합니다. 이번 감사에서는 성능·가용성을 측정하지 않았습니다.
정리는 [검토한 수명 주기 절차](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation.md#클러스터-삭제)를 따릅니다. 애플리케이션의 클라우드 의존성을 먼저 제거하고 적절한 노드 그룹·프로필과 클러스터를 정리합니다. 실습 전용 VPC는 의존 리소스가 사라진 뒤 제거하고 역할도 실습 소유만 정리합니다. 실패하면 응답·소유권 파일을 유지합니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [EKS 클러스터 생성 - 3부 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/02-eks-cluster-creation-part3-quiz)를 풀어보세요.
## 참고 자료
- [Create an EKS cluster](https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html)
- [EKS network requirements](https://docs.aws.amazon.com/eks/latest/userguide/network-reqs.html)
- [API endpoint access](https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html)
- [Node IAM role](https://docs.aws.amazon.com/eks/latest/userguide/create-node-role.html)
- [Default envelope encryption](https://docs.aws.amazon.com/eks/latest/userguide/envelope-encryption.html)
- [EKS access entries](https://docs.aws.amazon.com/eks/latest/userguide/access-entries.html)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/02-eks-cluster-creation-part4
----------------------------------------
# EKS 클러스터 생성 - Part 4: Terraform을 사용한 클러스터 생성
> **예제 버전**: Amazon EKS 1.36; Terraform 1.15.7; AWS 프로바이더 6.64.0; EKS 모듈 21.25.0
> **마지막 업데이트**: 2026년 9월 11일
## 3개 레이어 Terraform 예제
Terraform은 인프라를 코드로 관리합니다. 이 예제는 AWS 프로바이더 6.x와 고정된 EKS 모듈 **21.25.0**, VPC 모듈 **5.21.0**을 사용합니다. HCL은 Terraform 1.15.7·AWS 프로바이더 6.64.0으로 검사했으며 AWS 배포나 프로덕션 동작은 실행하지 않았습니다. 기존 v20 방식의 `cluster_*` 입력은 모듈 v21과 호환되지 않으므로 아래의 `name`, `kubernetes_version`, `addons` 등 v21 이름을 사용하세요.
상태를 분리하면 팀별 소유권과 수명주기에 따라 변경을 검토하기 쉽습니다. 의존성이나 운영 영향을 없애지는 않습니다. 네트워크 변경은 클러스터를 중단시킬 수 있고 애드온·접근 정책 변경은 모든 워크로드에 영향을 줄 수 있습니다. 검증된 프로덕션 아키텍처가 아닌 구조 예제입니다.
### 3-레이어 아키텍처
```
eks-terraform/
├── 01-network/ # 레이어 1: VPC 및 네트워킹
│ ├── providers.tf
│ ├── backend.tf # S3 키: eks/dev/network/terraform.tfstate
│ ├── variables.tf
│ ├── main.tf # VPC 모듈
│ └── outputs.tf # vpc_id, subnet_ids → 원격 상태
├── 02-cluster/ # 레이어 2: EKS 클러스터 및 노드 그룹
│ ├── providers.tf
│ ├── backend.tf # S3 키: eks/dev/cluster/terraform.tfstate
│ ├── data.tf # terraform_remote_state → 01-network
│ ├── variables.tf
│ ├── main.tf # EKS 모듈, 노드 그룹, 코어 애드온
│ └── outputs.tf # cluster_name, endpoint → 원격 상태
└── 03-platform/ # 레이어 3: 애드온, RBAC, Pod Identity
├── providers.tf
├── backend.tf # S3 키: eks/dev/platform/terraform.tfstate
├── data.tf # terraform_remote_state → 01-network, 02-cluster
├── variables.tf
├── addons.tf # EBS CSI 드라이버, 추가 애드온
├── pod-identity.tf # Pod Identity 연결
└── access-entries.tf # 개발자/뷰어 액세스 엔트리
```
### 레이어를 분리하는 이유
| 레이어 | 변경 빈도 | 소유 팀 | 영향 범위 |
|--------|----------|---------|----------|
| 01-network | 예시: 낮은 빈도 | 인프라 팀 | VPC·서브넷과 이에 의존하는 연결 |
| 02-cluster | 월간 | 플랫폼 팀 | EKS 클러스터, 노드 |
| 03-platform | 주간 | 플랫폼 / 앱 팀 | 애드온, RBAC, Pod Identity |
각 레이어는 별도 상태 키와 계획을 갖습니다. IAM 권한·CI 소유권도 분리하고 레이어 간 변경을 조율해야 합니다. 상태 분리만으로 애드온 변경이 클러스터 동작에 영향을 주지 않는다고 보장할 수는 없습니다.
### 공유 S3 백엔드
예제는 S3 기본 잠금인 `use_lockfile = true`(Terraform 1.10 이상)와 환경·레이어별 상태 키를 사용합니다. 백엔드 버킷을 별도로 생성하고 버전 관리·암호화·퍼블릭 액세스 차단을 설정한 뒤 초기화 전에 모든 `REPLACE_WITH_YOUR_STATE_BUCKET`을 바꾸세요. 필요한 상태 키 권한과 해당 `.tflock` 객체의 Get·Put·Delete 권한만 부여합니다. DynamoDB 잠금은 폐기 예정이므로 기존 백엔드 전환 시 잠금 테이블을 바로 삭제하지 말고 모든 클라이언트를 조율하세요.
```hcl
terraform {
backend "s3" {
bucket = "REPLACE_WITH_YOUR_STATE_BUCKET"
key = "eks/dev/network/terraform.tfstate"
region = "ap-northeast-2"
use_lockfile = true
encrypt = true
}
}
```
`terraform_remote_state`는 HCL에 루트 출력을 제공하지만 읽기 권한을 가진 주체는 민감한 값이 포함된 **전체 상태 스냅샷**을 가져올 수 있습니다. 별도 프로젝트 간 적용 순서를 자동으로 만들지도 않습니다. 소비 팀에 전체 상태를 공개하면 안 되는 경우 선택한 값만 별도 통제된 경로로 게시하세요.
---
## 레이어 1: 네트워크 (01-network)
이 예제는 실습 규모를 줄이기 위해 3개 AZ VPC에 NAT Gateway 하나를 만듭니다. 단일 NAT는 특정 AZ 의존성과 AZ 간 전송 요금을 만들 수 있습니다. 프로덕션에는 AZ 장애를 고려한 egress, 서브넷 용량, DNS와 프라이빗 엔드포인트를 검토하세요.
### 01-network/providers.tf
```hcl
terraform {
required_version = ">= 1.10, < 2.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.region
}
```
### 01-network/backend.tf
```hcl
terraform {
backend "s3" {
bucket = "REPLACE_WITH_YOUR_STATE_BUCKET"
key = "eks/dev/network/terraform.tfstate"
region = "ap-northeast-2"
use_lockfile = true
encrypt = true
}
}
```
### 01-network/variables.tf
```hcl
variable "cluster_name" {
description = "Name of the EKS cluster"
type = string
default = "my-eks-cluster"
}
variable "region" {
description = "AWS region"
type = string
default = "ap-northeast-2"
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
default = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
}
variable "private_subnets" {
description = "Private subnet CIDR blocks"
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
variable "public_subnets" {
description = "Public subnet CIDR blocks"
type = list(string)
default = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
}
variable "tags" {
description = "Common tags for all resources"
type = map(string)
default = {
Environment = "dev"
Terraform = "true"
}
}
```
### 01-network/main.tf
```hcl
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.21.0"
name = "${var.cluster_name}-vpc"
cidr = var.vpc_cidr
azs = var.availability_zones
private_subnets = var.private_subnets
public_subnets = var.public_subnets
enable_nat_gateway = true
single_nat_gateway = true
enable_dns_hostnames = true
public_subnet_tags = {
"kubernetes.io/role/elb" = "1"
}
private_subnet_tags = {
"kubernetes.io/role/internal-elb" = "1"
}
tags = var.tags
}
```
> **서브넷 검색**은 Terraform EKS 모듈 버전이 아닌 Load Balancer Controller 버전·기능 게이트·서브넷 적격성 규칙을 따릅니다. 예제의 역할 태그 외에도 AZ 범위, 가용 주소, 라우팅과 클러스터 태그 필터를 확인하세요.
### 01-network/outputs.tf
```hcl
output "vpc_id" {
description = "VPC ID"
value = module.vpc.vpc_id
}
output "private_subnet_ids" {
description = "Private subnet IDs"
value = module.vpc.private_subnets
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = module.vpc.public_subnets
}
```
---
## 레이어 2: EKS 클러스터 (02-cluster)
이 레이어는 **새 일반 EC2 클러스터**, 관리형 노드 그룹과 코어 애드온을 생성합니다. 기존 `cluster_admin_role_arn`을 명시적으로 설정하며 Terraform 호출자에게 Kubernetes 관리자 권한을 자동 부여하지 않습니다. 운영자는 해당 역할을 맡을 수 있어야 합니다. kubectl·플랫폼 설치 전에 프라이빗 엔드포인트에 연결·라우팅되는 관리 환경이 필요합니다.
### 02-cluster/providers.tf
```hcl
terraform {
required_version = ">= 1.10, < 2.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.region
}
```
### 02-cluster/backend.tf
```hcl
terraform {
backend "s3" {
bucket = "REPLACE_WITH_YOUR_STATE_BUCKET"
key = "eks/dev/cluster/terraform.tfstate"
region = "ap-northeast-2"
use_lockfile = true
encrypt = true
}
}
```
### 02-cluster/data.tf
```hcl
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "REPLACE_WITH_YOUR_STATE_BUCKET"
key = "eks/dev/network/terraform.tfstate"
region = "ap-northeast-2"
}
}
data "aws_caller_identity" "current" {}
locals {
cluster_arn = "arn:aws:eks:${var.region}:${data.aws_caller_identity.current.account_id}:cluster/${var.cluster_name}"
}
```
### 02-cluster/variables.tf
```hcl
variable "cluster_name" {
description = "Name of the EKS cluster"
type = string
default = "my-eks-cluster"
}
variable "cluster_version" {
description = "Kubernetes version for the EKS cluster"
type = string
default = "1.36"
}
variable "region" {
description = "AWS region"
type = string
default = "ap-northeast-2"
}
variable "tags" {
description = "Common tags for all resources"
type = map(string)
default = {
Environment = "dev"
Terraform = "true"
}
}
variable "cluster_admin_role_arn" {
description = "Existing approved IAM role for initial Kubernetes administration; not an STS session ARN"
type = string
}
```
### 02-cluster/main.tf
```hcl
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "21.25.0"
name = var.cluster_name
kubernetes_version = var.cluster_version
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
encryption_config = null
create_kms_key = false
# Cluster endpoint access
endpoint_private_access = true
endpoint_public_access = false
# Use API-based authentication (replaces aws-auth ConfigMap)
authentication_mode = "API"
# Use an explicitly selected existing administrator role
enable_cluster_creator_admin_permissions = false
access_entries = {
bootstrap_admin = {
principal_arn = var.cluster_admin_role_arn
policy_associations = {
admin = {
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
access_scope = {
type = "cluster"
}
}
}
}
}
# EKS Add-ons (core only — additional add-ons go in 03-platform)
addons = {
coredns = {
most_recent = false
resolve_conflicts_on_update = "PRESERVE"
}
vpc-cni = {
most_recent = false
resolve_conflicts_on_update = "PRESERVE"
before_compute = true
configuration_values = jsonencode({
env = {
ENABLE_PREFIX_DELEGATION = "true"
}
})
}
kube-proxy = {
most_recent = false
resolve_conflicts_on_update = "PRESERVE"
}
eks-pod-identity-agent = {
most_recent = false
resolve_conflicts_on_update = "PRESERVE"
before_compute = true
}
}
# Managed Node Groups
eks_managed_node_groups = {
default = {
ami_type = "AL2023_x86_64_STANDARD"
instance_types = ["m5.large"]
min_size = 2
max_size = 5
desired_size = 2
block_device_mappings = {
root = {
device_name = "/dev/xvda"
ebs = {
volume_size = 50
volume_type = "gp3"
encrypted = true
delete_on_termination = true
}
}
}
}
spot = {
ami_type = "AL2023_x86_64_STANDARD"
instance_types = ["m5.large", "m5a.large", "m5d.large"]
capacity_type = "SPOT"
min_size = 0
max_size = 5
desired_size = 1
block_device_mappings = {
root = {
device_name = "/dev/xvda"
ebs = {
volume_size = 50
volume_type = "gp3"
encrypted = true
delete_on_termination = true
}
}
}
}
}
# CloudWatch Logging
enabled_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
tags = var.tags
}
```
노드 그룹 루트 디스크는 Launch Template의 block-device mapping으로 설정합니다. 모듈 기본 커스텀 Launch Template을 사용할 때 `disk_size`는 무시됩니다. 이 예제는 기본 AWS 소유 키 기반 Kubernetes API envelope encryption을 사용하며 고객 KMS 키는 별도 설계 선택입니다.
배포 전에 모듈 기본값을 검토하세요. 고정 모듈의 관리형 노드 IAM 역할에는 ECR ReadOnly와 IPv4 CNI 권한이 포함됩니다. 예제 동작을 위한 기본값이며 프로덕션 최소 권한이라는 주장이 아닙니다. 전용 CNI 신원과 명시적으로 검토한 노드 역할을 우선하고 충분한 경우 ECR PullOnly를 사용하세요. 노드 그룹 min/max는 오토스케일러를 설치하지 않습니다.
Terraform이 데이터 소스를 다시 평가하면 기본 호환 애드온 빌드가 달라질 수 있습니다. 대상 클러스터의 호환 빌드를 확인·기록하고 재현성을 위해 고정해야 한다면 `addon_version`을 사용하세요. 업데이트 시 검토한 커스텀 구성을 보존합니다.
### 02-cluster/outputs.tf
```hcl
output "cluster_name" {
description = "EKS cluster name"
value = module.eks.cluster_name
}
output "cluster_endpoint" {
description = "EKS cluster API endpoint"
value = module.eks.cluster_endpoint
}
output "cluster_certificate_authority_data" {
description = "Base64 encoded certificate data for the cluster"
value = module.eks.cluster_certificate_authority_data
}
output "cluster_security_group_id" {
description = "Security group ID attached to the EKS cluster"
value = module.eks.cluster_security_group_id
}
output "oidc_provider_arn" {
description = "OIDC provider ARN for the EKS cluster"
value = module.eks.oidc_provider_arn
}
output "region" {
description = "AWS region"
value = var.region
}
output "cluster_arn" {
description = "EKS cluster ARN used to scope platform role trust"
value = module.eks.cluster_arn
}
```
---
## 레이어 3: 플랫폼 구성 (03-platform)
일반 EC2 플랫폼 레이어는 추가 애드온, Pod Identity 연결과 개발자·뷰어 접근을 관리합니다. 상태는 분리되지만 변경은 클러스터 보안과 워크로드 가용성에 영향을 줄 수 있습니다. 아래 Auto Mode·Hybrid 대안은 플랫폼 파일 구성이 다릅니다.
### 03-platform/providers.tf
```hcl
terraform {
required_version = ">= 1.10, < 2.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.region
}
```
### 03-platform/backend.tf
```hcl
terraform {
backend "s3" {
bucket = "REPLACE_WITH_YOUR_STATE_BUCKET"
key = "eks/dev/platform/terraform.tfstate"
region = "ap-northeast-2"
use_lockfile = true
encrypt = true
}
}
```
### 03-platform/data.tf
```hcl
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "REPLACE_WITH_YOUR_STATE_BUCKET"
key = "eks/dev/network/terraform.tfstate"
region = "ap-northeast-2"
}
}
data "terraform_remote_state" "cluster" {
backend = "s3"
config = {
bucket = "REPLACE_WITH_YOUR_STATE_BUCKET"
key = "eks/dev/cluster/terraform.tfstate"
region = "ap-northeast-2"
}
}
```
### 03-platform/variables.tf
```hcl
variable "cluster_name" {
description = "Name of the EKS cluster"
type = string
default = "my-eks-cluster"
}
variable "region" {
description = "AWS region"
type = string
default = "ap-northeast-2"
}
variable "tags" {
description = "Common tags for all resources"
type = map(string)
default = {
Environment = "dev"
Terraform = "true"
}
}
variable "developer_role_arn" {
description = "Existing approved IAM role for app-dev/app-staging Kubernetes access"
type = string
}
variable "viewer_role_arn" {
description = "Existing approved IAM role for Kubernetes read access"
type = string
}
variable "app_bucket_name" {
description = "Existing approved S3 bucket for the application's app/ prefix"
type = string
}
```
### 03-platform/addons.tf
```hcl
# EBS CSI Driver with Pod Identity
resource "aws_iam_role" "ebs_csi" {
name = "${var.cluster_name}-ebs-csi"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "pods.eks.amazonaws.com"
}
Action = [
"sts:AssumeRole",
"sts:TagSession"
]
Condition = {
StringEquals = {
"aws:RequestTag/eks-cluster-arn" = data.terraform_remote_state.cluster.outputs.cluster_arn
"aws:RequestTag/kubernetes-namespace" = "kube-system"
"aws:RequestTag/kubernetes-service-account" = "ebs-csi-controller-sa"
}
}
}]
})
tags = var.tags
}
resource "aws_iam_role_policy_attachment" "ebs_csi" {
role = aws_iam_role.ebs_csi.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"
}
resource "aws_eks_addon" "ebs_csi" {
cluster_name = data.terraform_remote_state.cluster.outputs.cluster_name
addon_name = "aws-ebs-csi-driver"
resolve_conflicts_on_create = "NONE"
resolve_conflicts_on_update = "PRESERVE"
depends_on = [aws_iam_role_policy_attachment.ebs_csi]
pod_identity_association {
role_arn = aws_iam_role.ebs_csi.arn
service_account = "ebs-csi-controller-sa"
}
tags = var.tags
}
```
### 03-platform/pod-identity.tf
```hcl
# The Kubernetes namespace and ServiceAccount are managed separately.
resource "aws_iam_role" "app_s3_access" {
name = "${var.cluster_name}-app-s3-access"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "pods.eks.amazonaws.com"
}
Action = ["sts:AssumeRole", "sts:TagSession"]
Condition = {
StringEquals = {
"aws:RequestTag/eks-cluster-arn" = data.terraform_remote_state.cluster.outputs.cluster_arn
"aws:RequestTag/kubernetes-namespace" = "app-dev"
"aws:RequestTag/kubernetes-service-account" = "app-sa"
}
}
}]
})
tags = var.tags
}
resource "aws_iam_role_policy" "app_s3_access" {
name = "ReadApprovedAppPrefix"
role = aws_iam_role.app_s3_access.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "s3:ListBucket"
Resource = "arn:aws:s3:::${var.app_bucket_name}"
Condition = {
StringLike = { "s3:prefix" = ["app/", "app/*"] }
}
},
{
Effect = "Allow"
Action = "s3:GetObject"
Resource = "arn:aws:s3:::${var.app_bucket_name}/app/*"
}
]
})
}
resource "aws_eks_pod_identity_association" "app_s3_access" {
cluster_name = data.terraform_remote_state.cluster.outputs.cluster_name
namespace = "app-dev"
service_account = "app-sa"
role_arn = aws_iam_role.app_s3_access.arn
depends_on = [aws_iam_role_policy.app_s3_access]
}
```
애플리케이션 테스트 전에 Kubernetes·GitOps 소유자를 통해 새 `app-dev` 네임스페이스와 `app-sa` ServiceAccount를 만드세요. EKS 연결은 두 객체를 생성하지 않습니다. 예제는 승인한 버킷의 `app/` prefix 읽기만 허용하며 버킷 정책·KMS 암호화·크로스 어카운트 접근에는 추가로 검토한 권한이 필요할 수 있습니다. 실제 assumed role을 확인하고 IAM·연결 전파를 고려한 후 사용하세요.
### 03-platform/access-entries.tf
```hcl
# Developer with namespace-scoped access
resource "aws_eks_access_entry" "developer" {
cluster_name = data.terraform_remote_state.cluster.outputs.cluster_name
principal_arn = var.developer_role_arn
}
resource "aws_eks_access_policy_association" "developer" {
cluster_name = data.terraform_remote_state.cluster.outputs.cluster_name
principal_arn = aws_eks_access_entry.developer.principal_arn
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSEditPolicy"
access_scope {
type = "namespace"
namespaces = ["app-dev", "app-staging"]
}
}
# Read-only access
resource "aws_eks_access_entry" "viewer" {
cluster_name = data.terraform_remote_state.cluster.outputs.cluster_name
principal_arn = var.viewer_role_arn
}
resource "aws_eks_access_policy_association" "viewer" {
cluster_name = data.terraform_remote_state.cluster.outputs.cluster_name
principal_arn = aws_eks_access_entry.viewer.principal_arn
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy"
access_scope {
type = "cluster"
}
}
```
---
## EKS Pod Identity 구성
EKS Pod Identity는 지원되는 워크로드의 선택지이며 IRSA를 보편적으로 대체하지는 않습니다. IAM OIDC provider가 필요하지 않습니다. 일반 Linux EC2·Auto Mode·적절히 구성한 Hybrid Nodes에는 지원 경로가 있으며 Fargate·Windows에는 다른 지원 신원 방식을 사용해야 합니다. IRSA도 계속 지원됩니다.
### Pod Identity 동작 방식
1. 일반 지원 노드는 Pod Identity Agent DaemonSet을 사용합니다. Auto Mode는 기능을 제공하며 Hybrid Nodes는 문서화된 자격 증명 파일·전용 DaemonSet 구성이 필요합니다.
2. Pod Identity 트러스트 정책이 포함된 IAM 역할이 생성됩니다 (레이어 3에서 생성).
3. `aws_eks_pod_identity_association`을 통해 IAM 역할이 Kubernetes 서비스 어카운트와 연결됩니다.
4. 기본 자격 증명 체인을 사용하는 호환 SDK가 임시 자격 증명을 가져옵니다. 체인 앞쪽의 기존 정적 자격 증명이 우선할 수 있으므로 실제 신원을 확인하세요.
위의 `03-platform/pod-identity.tf`에 표시된 Pod Identity 리소스가 이 패턴을 따릅니다. IAM 역할의 트러스트 정책은 `pods.eks.amazonaws.com`을 보안 주체로 사용하며, `sts:TagSession`은 클러스터, 네임스페이스, 서비스 어카운트 메타데이터를 사용한 자동 세션 태깅을 활성화합니다.
### Pod Identity vs IRSA 비교
| 기능 | Pod Identity | IRSA |
|------|-------------|------|
| OIDC 프로바이더 필요 | 아니오 | 예 |
| 크로스 어카운트 | 지원되는 `targetRoleArn` 등 명시적 역할 위임; 신뢰·권한 필요 | 대상 계정의 OIDC 신뢰 또는 명시적 역할 체이닝 |
| 설정 복잡도 | 낮음 — 단일 연결 | 보통 — OIDC, 역할, 어노테이션 |
| 세션 태그 | 활성화 시 자동 EKS 컨텍스트 태그 | EKS Pod Identity 컨텍스트 태그는 자동 제공되지 않음 |
| 재사용 | 검토한 여러 연결에서 역할 사용 가능 | 명시적으로 제한한 여러 OIDC 발급자·subject를 한 역할에서 신뢰 가능 |
> 컴퓨팅 유형·SDK가 지원하는 신원 방식을 선택하세요. `sts:TagSession`은 태그를 추가하며 그 자체로 크로스 어카운트 신뢰를 설정하지 않습니다. 위 신뢰 정책은 세션 태그 활성화를 요구합니다. 모듈은 Pod Identity와 별개로 선택적 IRSA 사용을 위한 OIDC provider를 생성할 수 있습니다.
---
## EKS Auto Mode 클러스터
최초 배포 전에 선택하는 **새 클러스터 대안**입니다. 이미 적용한 클러스터 구성을 덮어쓰는 것은 마이그레이션 절차가 아닙니다. Auto Mode는 컴퓨팅·인프라 기능을 관리합니다. 아래 순수 Auto Mode 예제는 관리형 노드 그룹을 정의하지 않으며 모듈 v21의 `compute_config`를 사용합니다:
```hcl
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "21.25.0"
name = var.cluster_name
kubernetes_version = var.cluster_version
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
endpoint_private_access = true
endpoint_public_access = false
authentication_mode = "API"
encryption_config = null
create_kms_key = false
enable_cluster_creator_admin_permissions = false
access_entries = {
bootstrap_admin = {
principal_arn = var.cluster_admin_role_arn
policy_associations = {
admin = {
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
access_scope = {
type = "cluster"
}
}
}
}
}
compute_config = {
enabled = true
node_pools = ["general-purpose", "system"]
}
enabled_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
tags = var.tags
}
```
### 핵심 사항
- **`compute_config.enabled = true`**를 통해 이 모듈이 컴퓨팅·로드 밸런싱·블록 스토리지를 함께 활성화합니다. 고정 모듈이 생성하는 IAM 정책을 검토하세요.
- **`node_pools`**는 활성화할 내장 노드 풀을 지정합니다 (`general-purpose`, `system`).
- 모듈 v21은 기반 리소스의 `bootstrap_self_managed_addons`를 `false`로 고정하며 모듈 입력으로 받지 않습니다. 현재 Auto Mode는 클러스터 DNS·네트워킹·스토리지·Pod Identity 기능을 포함하므로 Auto Mode 컴퓨팅에 대응하는 기존 애드온을 중복 설치할 필요가 없습니다.
- 이 대안은 관리형 노드 그룹을 사용하지 않습니다. 혼합 컴퓨팅도 지원하지만 Auto Mode 이외 노드는 해당 애드온과 배치 구성이 필요합니다.
- Auto Mode는 노드 풀에서 EC2 인스턴스를 프로비저닝하고 OS 패치, 스케일링, 라이프사이클을 처리합니다.
---
순수 Auto Mode에서는 일반 `03-platform/addons.tf`를 제외합니다. 그 파일의 기존 EBS CSI 드라이버는 Auto Mode 스토리지 컨트롤러가 아닙니다. 검토한 StorageClass에 Auto Mode provisioner인 `ebs.csi.eks.amazonaws.com`을 사용하세요. 애플리케이션 Pod Identity·액세스 엔트리 파일은 선행조건 충족 후 사용할 수 있습니다.
## EKS Hybrid Nodes 구성
이 구성은 **새 Hybrid 전용 제어 플레인 대안**이며 전체 호스트 프로비저닝 절차나 기존 클러스터 변환 절차가 아닙니다. VPN·Direct Connect 등 지원되는 라우팅 경로, DNS·방화벽·지원 OS·자격 증명 공급자를 별도로 준비해야 합니다. 클러스터 레이어 완료 후 nodeadm·CNI 설정으로 Hybrid 컴퓨팅을 연결한 다음 CoreDNS 애드온 준비를 기다리세요.
```hcl
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "21.25.0"
name = var.cluster_name
kubernetes_version = var.cluster_version
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
endpoint_private_access = true
endpoint_public_access = false
authentication_mode = "API"
encryption_config = null
create_kms_key = false
enable_cluster_creator_admin_permissions = false
access_entries = {
hybrid_nodes = {
principal_arn = aws_iam_role.hybrid_node_role.arn
type = "HYBRID_LINUX"
}
bootstrap_admin = {
principal_arn = var.cluster_admin_role_arn
policy_associations = {
admin = {
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
access_scope = {
type = "cluster"
}
}
}
}
}
remote_network_config = {
remote_node_networks = { cidrs = ["172.16.0.0/16"] }
remote_pod_networks = { cidrs = ["192.168.0.0/16"] }
}
security_group_additional_rules = {
hybrid_api = {
description = "Hybrid nodes to private Kubernetes API"
protocol = "tcp"
from_port = 443
to_port = 443
type = "ingress"
cidr_blocks = ["172.16.0.0/16"]
}
hybrid_kubelet = {
description = "Control plane to hybrid kubelet"
protocol = "tcp"
from_port = 10250
to_port = 10250
type = "egress"
cidr_blocks = ["172.16.0.0/16"]
}
}
enabled_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
tags = var.tags
}
resource "aws_iam_role" "hybrid_node_role" {
name = "${var.cluster_name}-hybrid-node-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ssm.amazonaws.com" }
Action = "sts:AssumeRole"
Condition = {
StringEquals = { "aws:SourceAccount" = data.aws_caller_identity.current.account_id }
ArnLike = {
"aws:SourceArn" = "arn:aws:ssm:${var.region}:${data.aws_caller_identity.current.account_id}:*"
}
}
}]
})
tags = var.tags
}
resource "aws_iam_role_policy_attachment" "hybrid_baseline" {
for_each = toset([
"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPullOnly",
"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
])
role = aws_iam_role.hybrid_node_role.name
policy_arn = each.value
}
resource "aws_iam_role_policy" "hybrid_lifecycle" {
name = "ScopedHybridNodeLifecycle"
role = aws_iam_role.hybrid_node_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["eks:DescribeCluster", "eks:ListAccessEntries"]
Resource = local.cluster_arn
},
{
Effect = "Allow"
Action = "ssm:DescribeInstanceInformation"
Resource = "*"
Condition = { StringEquals = { "aws:RequestedRegion" = var.region } }
},
{
Effect = "Allow"
Action = "ssm:DeregisterManagedInstance"
Resource = "arn:aws:ssm:${var.region}:${data.aws_caller_identity.current.account_id}:managed-instance/*"
Condition = {
StringEquals = { "ssm:resourceTag/EKSClusterARN" = local.cluster_arn }
}
}
]
})
}
```
### 핵심 사항
- **`remote_network_config`**는 중복되지 않는 원격 노드·Pod CIDR을 선언하며 VPN·라우팅·방화벽·CNI를 만들지 않습니다. 모듈 v21은 `cidrs`를 담은 객체를 받으며 해당 객체의 목록이 아닙니다.
- Hybrid 노드는 `HYBRID_LINUX` 유형의 액세스 엔트리가 있는 IAM 역할을 통해 인증합니다.
- Hybrid 노드는 TCP 443으로 프라이빗 API에 연결하고 제어 플레인은 TCP 10250으로 **Hybrid kubelet에 아웃바운드 연결**합니다. 온프레미스 방화벽과 필요한 Pod·웹훅 경로도 구성해야 하며 SG 규칙 두 개가 전체 네트워크 설계는 아닙니다.
- Hybrid 노드에서는 VPC CNI가 사용되지 않으므로 온프레미스 측에서 대체 CNI(예: Cilium)를 구성해야 합니다.
---
위 SSM 역할은 범위를 제한한 nodeadm 등록 해제도 지원합니다. 별도의 SSM activation·관리형 인스턴스에 `EKSClusterARN = local.cluster_arn` 태그를 지정해 정책과 맞추세요. 서명 키 변경 때문에 신규 SSM 설치·업그레이드에는 nodeadm 1.0.19 이상이 필요합니다. activation 비밀을 Terraform 소스에 넣거나 포함된 상태를 공개하지 마세요.
일반 EBS CSI 플랫폼 파일은 온프레미스 디스크에 적용되지 않습니다. Hybrid 노드·CNI 설정 후 별도로 검토한 플랫폼 구성을 사용합니다. 아래는 Hybrid 전용 대안의 코어 애드온 소유권 구성이며 같은 애드온을 이미 관리하는 구성에 덧붙이면 안 됩니다:
```hcl
# Hybrid-only platform alternative, after nodeadm/CNI and node readiness checks.
resource "aws_eks_addon" "hybrid_core" {
for_each = toset(["coredns", "kube-proxy"])
cluster_name = data.terraform_remote_state.cluster.outputs.cluster_name
addon_name = each.value
resolve_conflicts_on_create = "NONE"
resolve_conflicts_on_update = "PRESERVE"
}
```
Hybrid 노드에서 애플리케이션 Pod Identity를 사용하기 전에 지원 에이전트의 Hybrid DaemonSet, 노드 자격 증명 파일과 노드의 `eks-auth:AssumeRoleForPodIdentity` 권한을 구성하세요. 위 SSM 기본 역할에는 이 선택적 권한이 없습니다. Hybrid Nodes 전용 애드온 안내를 따라야 하며 연결 생성만으로 충분하지 않습니다.
## Add-on 관리
일반 EC2 대안은 클러스터 레이어에서 CoreDNS·VPC CNI·kube-proxy·Pod Identity Agent를 관리하고 플랫폼 레이어에서 EBS CSI·애플리케이션 신원을 관리합니다. Auto Mode·Hybrid는 구성 요소와 설치 순서가 다르므로 일반 플랫폼 파일을 그대로 적용하면 안 됩니다.
### 주요 옵션
| 옵션 | 설명 |
|------|------|
| `most_recent` | true이면 Terraform 평가 시 최신 호환 빌드, false이면 EKS 기본값을 선택합니다. 자율적인 업그레이드 서비스가 아닙니다. 재현성이 필요하면 검토한 `addon_version`을 지정하세요. |
| `before_compute` | 모듈 관리 애드온을 노드 그룹보다 먼저 생성하도록 순서를 정합니다. VPC CNI 초기화 등에 유용하지만 모든 애드온의 보편적 필수조건은 아닙니다. CoreDNS 정상 동작에는 사용 가능한 컴퓨팅이 필요합니다. |
| `configuration_values` | 애드온별 설정의 JSON 문자열입니다 (예: VPC CNI 프리픽스 위임). |
| `service_account_role_arn` | IRSA 역할 ARN입니다. Pod Identity는 `pod_identity_association`을 사용합니다. |
| `resolve_conflicts_on_create` | `NONE`은 충돌을 검토할 수 있게 합니다. 기존 설치의 검토된 마이그레이션에서만 `OVERWRITE`를 사용하세요. |
| `resolve_conflicts_on_update` | `PRESERVE`는 지원 범위에서 충돌하는 커스텀 설정을 보존합니다. 애드온 소유 필드는 스키마·configurationValues로 관리하세요. `OVERWRITE`는 변경을 버릴 수 있습니다. |
### Add-on의 Pod Identity
일부 애드온은 Pod Identity 연결을 직접 지원합니다. `03-platform/addons.tf`의 EBS CSI 드라이버 구성이 `pod_identity_association`을 사용하는 패턴을 보여줍니다:
위의 완전한 `03-platform/addons.tf` 리소스를 사용하세요. 중첩된 `pod_identity_association` 블록은 애드온이 소유하므로 같은 ServiceAccount의 연결을 별도 리소스로 중복 생성하지 않습니다.
---
## Access Entry 기반 접근 제어
초기 Kubernetes 관리 권한은 `02-cluster`의 명시적 액세스 엔트리로, 개발자·뷰어 권한은 `03-platform`으로 관리합니다. 정책 연결이 액세스 엔트리 리소스를 참조하여 Terraform 실행 순서를 만듭니다. API 접근 모드 변경은 마이그레이션 결정이며 API 활성화를 단순히 ConfigMap 전용으로 되돌릴 수는 없습니다.
### 인증 모드
| 모드 | 설명 |
|------|------|
| `API` | Access Entry만 사용 (신규 클러스터에 권장). |
| `API_AND_CONFIG_MAP` | Access Entry와 `aws-auth` ConfigMap 모두 사용 (마이그레이션 기간). |
| `CONFIG_MAP` | 레거시 `aws-auth`만 사용 (권장하지 않음). |
### 사용 가능한 Access Policy ARN
| 정책 | ARN | 설명 |
|------|-----|------|
| Cluster Admin | `arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy` | 전체 클러스터 접근 |
| Admin | `arn:aws:eks::aws:cluster-access-policy/AmazonEKSAdminPolicy` | 선택한 접근 범위의 Kubernetes 관리; AWS IAM 권한은 제공하지 않음 |
| Edit | `arn:aws:eks::aws:cluster-access-policy/AmazonEKSEditPolicy` | 대부분의 리소스 읽기/쓰기 |
| View | `arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy` | 정책 대상 Kubernetes 리소스 조회; 일반적인 Secret 조회 권한은 아님 |
---
## 배포 워크플로
아래 명령은 백엔드·역할 권한·프라이빗 네트워크 선행조건을 갖춘 **일반 EC2 3개 레이어 예제**입니다. 필수 Terraform 변수(레이어 2의 `cluster_admin_role_arn`, 레이어 3의 `developer_role_arn`·`viewer_role_arn`·`app_bucket_name`)는 검토한 변수 파일이나 `TF_VAR_*` 값으로 설정합니다. 의도한 AWS 계정·리전을 사용하세요. Auto Mode·Hybrid 대안은 앞서 설명한 파일·부트스트랩 변경이 필요합니다.
### 레이어를 하나씩 계획하고 적용
셸 디렉터리 변경으로 다른 레이어를 선택하지 않도록 절대 프로젝트 경로를 사용합니다. 먼저 네트워크 계획을 초기화·저장합니다:
```bash
set -euo pipefail
umask 077
: "${TF_PROJECT_DIR:?Set the absolute path to eks-terraform}"
case "$TF_PROJECT_DIR" in /*) ;; *) printf '%s\n' 'An absolute project path is required.' >&2; exit 1 ;; esac
# Repeat for 02-cluster and then 03-platform only after the previous layer succeeds.
TF_LAYER=01-network
terraform -chdir="$TF_PROJECT_DIR/$TF_LAYER" init
terraform -chdir="$TF_PROJECT_DIR/$TF_LAYER" plan -out=reviewed.tfplan
terraform -chdir="$TF_PROJECT_DIR/$TF_LAYER" show -no-color reviewed.tfplan
```
저장한 계획에서 생성·교체·삭제 리소스, IAM, 네트워크 노출과 비용을 검토합니다. 계획 파일에는 민감한 데이터가 포함될 수 있으므로 보호하세요. 검토한 파일만 적용합니다:
```bash
# Run only after reviewing this saved plan; a saved-plan apply does not prompt again.
: "${TF_PROJECT_DIR:?}" "${TF_LAYER:?}"
terraform -chdir="$TF_PROJECT_DIR/$TF_LAYER" apply reviewed.tfplan
```
성공하면 `TF_LAYER=02-cluster`, 이어서 `TF_LAYER=03-platform`으로 계획·검토·적용을 반복합니다. 실패하면 진행을 중단하고 누락되거나 오래된 출력을 사용하는 하위 상태를 적용하지 마세요. 검토한 프로바이더 잠금 파일은 버전 관리하되 상태·kubeconfig·자격 증명·계획 파일은 소스 저장소에 넣지 않습니다.
기존 문서는 클러스터 생성 시간을 10–15분으로 추정했습니다. 이번 감사에서 측정·재현한 값이 아니며 용량·애드온·IAM·네트워크에 따라 시간이 달라집니다.
### kubeconfig 구성
클러스터 레이어 완료 후 명시적으로 선택한 관리자 역할을 사용합니다. 현재 AWS 신원에 해당 역할을 맡을 권한이 있어야 합니다. 이미 허용된 주체로 직접 인증한다면 그에 맞는 검토한 자격 증명 경로를 사용하세요.
```bash
set -euo pipefail
: "${TF_PROJECT_DIR:?}"
: "${EXAMPLE_KUBECONFIG:?Choose a private kubeconfig file}"
: "${TF_VAR_cluster_admin_role_arn:?Set the approved role the operator can assume}"
EKS_CLUSTER_NAME=$(terraform -chdir="$TF_PROJECT_DIR/02-cluster" output -raw cluster_name)
EKS_REGION=$(terraform -chdir="$TF_PROJECT_DIR/02-cluster" output -raw region)
aws eks update-kubeconfig --name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--role-arn "$TF_VAR_cluster_admin_role_arn" --kubeconfig "$EXAMPLE_KUBECONFIG"
```
### 결과 검증
일반 EC2 예제는 실제 노드·시스템 Pod·관리형 애드온 상태를 확인합니다:
```bash
: "${EXAMPLE_KUBECONFIG:?}" "${EKS_CLUSTER_NAME:?}" "${EKS_REGION:?}"
kubectl --kubeconfig "$EXAMPLE_KUBECONFIG" get nodes
kubectl --kubeconfig "$EXAMPLE_KUBECONFIG" wait --for=condition=Ready nodes --all --timeout=5m
kubectl --kubeconfig "$EXAMPLE_KUBECONFIG" -n kube-system get pods
aws eks list-addons --cluster-name "$EKS_CLUSTER_NAME" --region "$EKS_REGION"
aws eks describe-addon --cluster-name "$EKS_CLUSTER_NAME" --region "$EKS_REGION" \
--addon-name coredns --query 'addon.{status:status,version:addonVersion,health:health}'
```
예상 애드온별로 `describe-addon`을 반복합니다. DaemonSet 목록만으로 EKS 애드온 정상 상태를 입증할 수는 없습니다. API 인증·DNS·네트워크·스토리지·애플리케이션의 실제 AWS 신원을 확인하세요. 이번 감사에서는 이러한 실환경 검사를 실행하지 않았습니다. 순수 Auto Mode·Hybrid 전용 클러스터는 해당 검증 경로가 필요하며 같은 시스템 Pod가 없을 수 있습니다.
### 역순으로 삭제
데이터를 백업하고 PVC·PV 회수 동작을 검토한 뒤, Kubernetes가 생성한 로드 밸런서·볼륨 리소스를 담당 컨트롤러를 통해 먼저 정리합니다. 정리가 끝날 때까지 컨트롤러와 IAM 권한을 유지하세요. 이후 플랫폼 삭제 계획을 저장·검토합니다:
```bash
set -euo pipefail
: "${TF_PROJECT_DIR:?Set the absolute project path}"
case "$TF_PROJECT_DIR" in /*) ;; *) printf '%s\n' 'An absolute project path is required.' >&2; exit 1 ;; esac
# After workload/data cleanup, handle one layer at a time in this order:
# 03-platform, then 02-cluster, then 01-network.
TF_LAYER=03-platform
terraform -chdir="$TF_PROJECT_DIR/$TF_LAYER" plan -destroy -out=reviewed-destroy.tfplan
terraform -chdir="$TF_PROJECT_DIR/$TF_LAYER" show -no-color reviewed-destroy.tfplan
```
선택한 상태와 제안된 모든 삭제 대상이 이 환경의 리소스임을 확인한 뒤에만 적용합니다:
```bash
# Run only after reviewing this exact destruction plan.
: "${TF_PROJECT_DIR:?}" "${TF_LAYER:?}"
terraform -chdir="$TF_PROJECT_DIR/$TF_LAYER" apply reviewed-destroy.tfplan
```
성공하면 `02-cluster`, 마지막으로 `01-network`에 반복합니다. 오류가 나면 중단하고 남은 ENI·로드 밸런서·볼륨·finalizer를 조사하세요. 삭제 계획은 해당 상태의 KMS 키 삭제도 예약할 수 있으므로 보존된 암호화 데이터에 필요한 키를 유지합니다. 독립적으로 관리하는 백엔드·상태 버전·복구 자료를 보존하세요.
---
## 모범 사례
### 상태 관리
서로 다른 S3 상태 키와 기본 잠금을 사용합니다. 잠금은 한 상태의 동시 쓰기를 제어하며 3개 상태 전체의 배포를 조율하지는 않습니다.
- S3 버킷에 **버전 관리를 활성화**하여 실수로 인한 상태 손상을 복구할 수 있도록 합니다.
- IAM 정책으로 **버킷 접근을 제한**합니다 — CI/CD 파이프라인과 권한 있는 운영자만 상태를 읽고 쓸 수 있어야 합니다.
- **상태 파일을 수동으로 편집하지 마세요** — 상태 조작이 필요한 경우 `terraform state` 명령을 사용합니다.
### 모듈 버전 관리
- `~> 21.0`은 22.0 미만의 마이너·패치 릴리스를 모두 허용하며 `~> 21.0.0`은 21.0.x로 제한합니다. 호환성을 보장하지는 않습니다. 이 예제는 모듈 버전을 명시적으로 고정하며 `.terraform.lock.hcl`은 원격 모듈이 아닌 프로바이더를 잠급니다.
- 메이저 버전 업그레이드 전에 모듈 CHANGELOG를 검토합니다.
- 비프로덕션 환경에서 먼저 업그레이드를 테스트합니다.
### 환경 분리
다음 방법 중 하나를 사용하여 환경을 분리합니다:
| 방식 | 장점 | 단점 |
|------|------|------|
| **별도 디렉토리** | 명확한 격리, 독립적인 상태 | 코드 중복 |
| **Terraform Workspace** | 단일 코드베이스, 쉬운 전환 | 공유 백엔드, 제한적 격리 |
| **Terragrunt** | 구성 재사용과 실행 조율 | 추가 도구 필요; 상태·권한 분리가 여전히 필요 |
멀티 레이어 아키텍처에서 가장 일반적인 접근 방식은 **환경별 별도 디렉토리**로, 각 환경이 서로 다른 변수 값과 상태 키를 가진 자체 `01-network/`, `02-cluster/`, `03-platform/` 트리를 갖는 것입니다.
### 태깅 전략
비용 할당, 컴플라이언스, 리소스 관리를 위해 일관된 태그를 적용합니다:
```hcl
variable "tags" {
default = {
Environment = "dev"
Team = "platform"
ManagedBy = "terraform"
Project = "eks-cluster"
}
}
```
---
## 다음 단계
- [EKS 클러스터 생성 - 1부: 사전 요구 사항](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part1.md) — EKS 클러스터 생성을 위한 사전 준비 사항
- [EKS 클러스터 생성 - 2부: eksctl을 사용한 클러스터 생성](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part2.md) — eksctl을 사용한 EKS 클러스터 생성
- [EKS 클러스터 생성 - 3부: AWS Console 및 CLI를 사용한 클러스터 생성](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part3.md) — Console과 CLI를 사용한 EKS 클러스터 생성
- [EKS 클러스터 생성 - 5부: 클러스터 액세스, 검증, 업그레이드 및 삭제](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part5.md) — EKS 클러스터 관리
- [EKS 네트워킹 - 1부: 기본 개념 및 VPC 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks/03-eks-networking-part1.md) — EKS 네트워킹 기본 개념
- [EKS 보안](https://www.atomai.click/kubernetes-docs/llms/ko/eks/05-eks-security.md) — EKS 클러스터 보안 구성
### 관련 주제
- [ArgoCD](https://www.atomai.click/kubernetes-docs/llms/ko/gitops/argocd/README.md) — GitOps 연속 배포
- [AWS Controllers for Kubernetes (ACK)](https://www.atomai.click/kubernetes-docs/llms/ko/platform-engineering/02-ack.md) — Kubernetes에서 AWS 리소스 관리
- [Karpenter](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/02-karpenter.md) — 노드 프로비저닝 자동화
- [Kubernetes 확장](https://www.atomai.click/kubernetes-docs/llms/ko/core/11-extending-kubernetes.md) — Operator와 CRD를 사용한 Kubernetes API 확장
## 용어집
| 용어 | 설명 |
|------|------|
| **EKS** | Amazon Elastic Kubernetes Service — AWS에서 제공하는 관리형 Kubernetes 서비스입니다. |
| **Terraform** | HashiCorp에서 개발한 인프라를 코드로 프로비저닝하고 관리하는 도구입니다. |
| **Access Entry** | `aws-auth` ConfigMap을 대체하는 EKS API 기반의 클러스터 접근 권한 부여 메커니즘입니다. |
| **Pod Identity** | OIDC 프로바이더 없이 파드에 AWS 자격 증명을 제공하는 EKS 기능입니다. |
| **Auto Mode** | AWS가 노드 프로비저닝, 스케일링, OS 업데이트를 완전히 관리하는 EKS 모드입니다. |
| **Hybrid Nodes** | 온프레미스 또는 엣지 서버를 EKS 클러스터의 워커 노드로 참여시킬 수 있는 EKS 기능입니다. |
| **IAM** | Identity and Access Management — AWS 리소스에 대한 접근을 제어하는 서비스입니다. |
| **VPC** | Virtual Private Cloud — AWS 클라우드 내의 논리적으로 격리된 가상 네트워크입니다. |
| **IRSA** | IAM Roles for Service Accounts — 지원되는 OIDC 기반 워크로드 신원 방식입니다. |
| **원격 상태** | 하나의 Terraform 구성이 다른 구성의 상태 파일에서 출력을 읽을 수 있게 하는 Terraform 기능입니다. |
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [EKS 클러스터 생성 - Part 4 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/02-eks-cluster-creation-part4-quiz)를 풀어보세요.
## 검증 참고 자료
- [EKS module v21 migration](https://github.com/terraform-aws-modules/terraform-aws-eks/blob/v21.25.0/docs/UPGRADE-21.0.md)
- [S3 backend locking](https://developer.hashicorp.com/terraform/language/backend/s3)
- [Remote state access](https://developer.hashicorp.com/terraform/language/state/remote-state-data)
- [Pod Identity role trust](https://docs.aws.amazon.com/eks/latest/userguide/pod-id-role.html)
- [EKS add-ons and Auto Mode](https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html)
- [Hybrid credentials](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-creds.html)
- [Hybrid networking](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-networking.html)
- [Hybrid add-ons](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-add-ons.html)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/02-eks-cluster-creation-part5
----------------------------------------
# Part 5: 클러스터 액세스, 검증, 업그레이드 및 삭제
> **마지막 업데이트**: 2026년 9월 11일
기존 EKS 클러스터를 대상으로 하는 가이드입니다. 승인된 계정·리전과 전용 kubeconfig를 사용하세요. 현재 AWS 문서와 로컬 파서로 예제를 검토했으며 이번 감사에서 AWS 변경·클러스터 워크로드·프로덕션 복구 테스트를 실행하지 않았습니다.
## 클러스터 액세스 구성
### 대상 컨텍스트 준비
의도한 클러스터의 `EXAMPLE_CLUSTER`·`EXAMPLE_REGION`을 설정합니다. 현재 AWS CLI 신원에는 해당 AWS API 권한이 필요합니다. Kubernetes 관리에는 이미 허용된 신원을 사용하며 필요하면 운영자가 맡을 수 있는 승인된 역할을 `ADMIN_ROLE_ARN`으로 지정합니다. kubeconfig는 권한을 부여하지 않습니다. 초기 관리 권한은 클러스터의 부트스트랩·접근 설정에 따라 달라지며 항상 “생성자만” 접근하는 것은 아닙니다.
아래 변수·헬퍼 함수는 전용 Bash 세션에서 사용합니다. 필요한 작업 흐름만 실행하세요. 업그레이드와 삭제는 별도 작업입니다.
```bash
set -euo pipefail
umask 077
: "${EXAMPLE_CLUSTER:?Set the existing cluster name}"
: "${EXAMPLE_REGION:?Set the Region}"
EKS_REVIEW_DIR=$(mktemp -d /tmp/eks-lifecycle-review.XXXXXX)
ADMIN_KUBECONFIG="$EKS_REVIEW_DIR/admin.kubeconfig"
aws eks describe-cluster --name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--query cluster --output json > "$EKS_REVIEW_DIR/cluster-before.json"
jq -e '{arn,createdAt} | (.arn | type == "string") and (.createdAt != null)' \
"$EKS_REVIEW_DIR/cluster-before.json" >/dev/null
EXPECTED_CLUSTER_ARN=$(jq -er '.arn' "$EKS_REVIEW_DIR/cluster-before.json")
EXPECTED_CLUSTER_CREATED=$(jq -er '.createdAt | tostring' "$EKS_REVIEW_DIR/cluster-before.json")
CLUSTER_KUBERNETES_VERSION=$(jq -er '.version' "$EKS_REVIEW_DIR/cluster-before.json")
KUBECONFIG_ARGS=(--name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION"
--kubeconfig "$ADMIN_KUBECONFIG" --alias "$EXAMPLE_CLUSTER-review")
if [ -n "${ADMIN_ROLE_ARN:-}" ]; then
KUBECONFIG_ARGS+=(--role-arn "$ADMIN_ROLE_ARN")
fi
aws eks update-kubeconfig "${KUBECONFIG_ARGS[@]}"
```
기록된 검토 디렉터리에 전용 kubeconfig를 작성하므로 기본 컨텍스트를 바꾸지 않습니다. `--kubeconfig`를 생략하면 AWS CLI가 `KUBECONFIG` 또는 기본 `~/.kube/config`를 사용하므로 모든 환경에서 경로가 고정되지는 않습니다.
액세스 엔트리는 EKS 접근 정책, Kubernetes 그룹·RBAC 또는 둘 다 사용할 수 있습니다. 권한은 **합산**되므로 네임스페이스 RoleBinding이 EKS 클러스터 관리자 접근 정책을 제한할 수는 없습니다. 이 예제는 광범위한 접근 정책을 추가하지 않고 사용자 정의 그룹과 네임스페이스 RBAC를 사용합니다.
### EKS 업데이트 완료 확인
설정·버전 변경은 비동기입니다. `ACTIVE`·`InProgress` 상태를 한 번 확인한 것으로 완료를 판단하지 말고 반환된 업데이트 ID를 추적하며 오류가 나면 중단합니다:
```bash
# Additional describe-update arguments can identify a node group or add-on.
wait_eks_update() {
local update_id="$1"
shift
local update_json update_status attempt
for attempt in $(seq 1 120); do
update_json=$(aws eks describe-update --name "$EXAMPLE_CLUSTER" \
--region "$EXAMPLE_REGION" --update-id "$update_id" "$@" --output json) || return 1
update_status=$(printf '%s' "$update_json" | jq -er '.update.status') || return 1
case "$update_status" in
Successful) return 0 ;;
Failed|Cancelled)
printf '%s' "$update_json" | jq '.update.errors' >&2
return 1 ;;
InProgress) sleep 10 ;;
*) printf 'Unexpected update status: %s\n' "$update_status" >&2; return 1 ;;
esac
done
printf 'Update %s did not finish within this wait window; inspect it before retrying.\n' "$update_id" >&2
return 1
}
```
### 방법 1: 액세스 엔트리와 범위를 제한한 RBAC
인증 모드를 먼저 확인합니다. 기존 `CONFIG_MAP` 클러스터는 마이그레이션 검토 후 `API_AND_CONFIG_MAP`을 활성화할 수 있습니다. `API` 클러스터는 이미 액세스 엔트리를 지원하므로 ConfigMap 접근을 다시 켜려 하지 마세요. 되돌릴 수 없는 전환 동안 관리자·노드 접근을 보존합니다.
```bash
AUTH_MODE=$(aws eks describe-cluster --name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--query cluster.accessConfig.authenticationMode --output text)
case "$AUTH_MODE" in
CONFIG_MAP)
AUTH_UPDATE_ID=$(aws eks update-cluster-config --name "$EXAMPLE_CLUSTER" \
--region "$EXAMPLE_REGION" --access-config authenticationMode=API_AND_CONFIG_MAP \
--query update.id --output text)
wait_eks_update "$AUTH_UPDATE_ID"
;;
API_AND_CONFIG_MAP|API)
printf '%s\n' 'Access entries are already enabled.'
;;
*)
printf 'Unexpected authentication mode: %s\n' "$AUTH_MODE" >&2
exit 1
;;
esac
```
**별도의 기존 개발자 IAM 역할**을 사용하며 유일한 관리자 역할을 대상으로 삼지 않습니다. 생성한 그룹·네임스페이스로 다른 바인딩과의 충돌을 피합니다. 역할 세션을 식별할 수 있도록 사용자 이름은 EKS가 생성하게 둡니다. 네임스페이스 범위를 제한하는 이 예제에 `system:masters`를 사용하면 안 됩니다.
```bash
# Use a separate, existing developer IAM role; retain the administrator's access.
: "${DEVELOPER_ROLE_ARN:?Existing role that the test operator can assume}"
ACCESS_NAMESPACE="eks-access-$(date +%s)-$$"
DEVELOPER_GROUP="$ACCESS_NAMESPACE-developers"
kubectl --kubeconfig "$ADMIN_KUBECONFIG" create namespace "$ACCESS_NAMESPACE"
ACCESS_NAMESPACE_UID=$(kubectl --kubeconfig "$ADMIN_KUBECONFIG" \
get namespace "$ACCESS_NAMESPACE" -o jsonpath='{.metadata.uid}')
: "${ACCESS_NAMESPACE_UID:?}"
jq -n --arg name "$ACCESS_NAMESPACE" --arg uid "$ACCESS_NAMESPACE_UID" \
'{namespace:$name,namespaceUID:$uid}' > "$EKS_REVIEW_DIR/access-namespace.json"
kubectl --kubeconfig "$ADMIN_KUBECONFIG" label namespace "$ACCESS_NAMESPACE" \
pod-security.kubernetes.io/enforce=restricted \
"pod-security.kubernetes.io/enforce-version=v$CLUSTER_KUBERNETES_VERSION"
# Creation fails rather than rewriting an existing principal's entry.
aws eks create-access-entry --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--principal-arn "$DEVELOPER_ROLE_ARN" --type STANDARD \
--kubernetes-groups "$DEVELOPER_GROUP" \
--query accessEntry --output json > "$EKS_REVIEW_DIR/created-access-entry.json"
```
일치하는 Role·Group 바인딩을 만들고 개발자 역할 자체로 테스트합니다. 이 Role은 Secret 직접 조회나 RBAC·네임스페이스 관리 권한을 주지 않습니다. 다만 워크로드 생성 권한으로 같은 네임스페이스의 신원·Secret을 사용할 수 있으므로 강한 격리가 필요하면 admission 제어로 허용 ServiceAccount·마운트를 제한하세요.
```bash
kubectl --kubeconfig "$ADMIN_KUBECONFIG" -n "$ACCESS_NAMESPACE" create -f - <"$EKS_REVIEW_DIR/can-i-errors.txt") || {
# kubectl returns nonzero for an ordinary "no"; distinguish other failures.
[ "$NODE_DELETE_ALLOWED" = no ] || exit 1
}
[ "$NODE_DELETE_ALLOWED" = no ] || {
printf '%s\n' 'Unexpected cluster-wide permission; review all access policies and RBAC bindings.' >&2
exit 1
}
```
액세스 엔트리는 전파 시간이 필요하므로 오류를 확인하고 전파를 고려해 재시도합니다. `kubectl --as`·`--as-group`은 IAM 주체의 EKS 접근 정책 권한이 아닌 Kubernetes RBAC를 테스트합니다. `auth can-i --list`도 EKS 접근 정책 권한 전체 목록은 아닙니다. IAM 사용자도 지원되지만 임시 자격 증명의 역할을 우선하며 기존 사용자는 별도의 올바른 자격 증명 경로가 필요합니다.
### 방법 2: 기존 aws-auth 마이그레이션

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-02-eks-cluster-creation-part5-1.html)
클러스터 인증 모드에 ConfigMap 접근이 포함될 때만 사용합니다. 실제 전체 ConfigMap을 보존하고 노드 역할 하나만 있는 예제로 대체하지 마세요. 관리형 노드·Fargate 매핑은 대응하는 액세스 엔트리를 검증할 때까지 유지합니다. 두 방식 모두 IaC로 관리하고 해당 AWS·Kubernetes 로그로 감사할 수 있습니다.
```bash
kubectl --kubeconfig "$ADMIN_KUBECONFIG" -n kube-system get configmap aws-auth -o yaml > "$EKS_REVIEW_DIR/aws-auth-before.yaml"
kubectl --kubeconfig "$ADMIN_KUBECONFIG" -n kube-system edit configmap aws-auth
```
기존 `mapRoles`·`mapUsers` YAML에 검토한 역할·사용자 항목만 병합하고 RoleBinding과 같은 사용자 정의 Group을 사용합니다. 노드 사용자 이름·그룹과 다른 매핑은 그대로 유지하세요. 기존 aws-auth의 역할 ARN 경로 제약은 액세스 엔트리와 다르므로 임의 ARN을 직접 고치지 말고 문서화된 전환 경로를 따릅니다. 같은 주체가 양쪽에 있으면 액세스 엔트리 매핑이 우선합니다. 이전한 신원을 검증하는 동안 별도의 관리자 세션을 유지하세요.
## 클러스터 검증
### 대상 컴퓨팅과 시스템 구성 요소 확인
```bash
kubectl --kubeconfig "$ADMIN_KUBECONFIG" get nodes -o wide
kubectl --kubeconfig "$ADMIN_KUBECONFIG" get pods -n kube-system -o wide
aws eks list-addons --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION"
```
예상 노드 수·준비 상태, Deployment·DaemonSet 롤아웃과 애드온 상태를 확인합니다. `Running`은 Pod 단계이며 컨테이너 준비를 입증하지 않고 성공한 Job은 정상적으로 `Succeeded`일 수 있습니다. 빈 노드 목록을 일반 EC2 클러스터의 정상 상태로 판단하면 안 됩니다. Auto Mode·Fargate·Hybrid Nodes는 시스템 구성 요소 배치가 다르며 기본 StorageClass는 그것을 사용하는 워크로드에 필요합니다.
### 소유권을 기록한 HTTP 테스트 배포
일반 Linux 예제로 새 네임스페이스에 작은 HTTP 응답 서버를 만듭니다. 퍼블릭 로드 밸런서를 자동 생성하지 않고 스케줄링·이미지 pull·DNS·ClusterIP Service를 테스트합니다. 조직 전체 정책이 있으면 DNS·HTTP에 대한 승인된 허용 규칙이 필요할 수 있으며 이 예제는 기존 정책을 끄지 않습니다.
```bash
VALIDATION_NAMESPACE="eks-validation-$(date +%s)-$$"
kubectl --kubeconfig "$ADMIN_KUBECONFIG" create namespace "$VALIDATION_NAMESPACE"
VALIDATION_NAMESPACE_UID=$(kubectl --kubeconfig "$ADMIN_KUBECONFIG" \
get namespace "$VALIDATION_NAMESPACE" -o jsonpath='{.metadata.uid}')
: "${VALIDATION_NAMESPACE_UID:?}"
jq -n --arg name "$VALIDATION_NAMESPACE" --arg uid "$VALIDATION_NAMESPACE_UID" \
'{namespace:$name,namespaceUID:$uid}' > "$EKS_REVIEW_DIR/validation-namespace.json"
kubectl --kubeconfig "$ADMIN_KUBECONFIG" label namespace "$VALIDATION_NAMESPACE" \
pod-security.kubernetes.io/enforce=restricted \
"pod-security.kubernetes.io/enforce-version=v$CLUSTER_KUBERNETES_VERSION"
kubectl --kubeconfig "$ADMIN_KUBECONFIG" -n "$VALIDATION_NAMESPACE" create -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-validation
spec:
replicas: 3
selector:
matchLabels: {app: http-validation}
template:
metadata:
labels: {app: http-validation}
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile: {type: RuntimeDefault}
containers:
- name: server
image: public.ecr.aws/docker/library/busybox:1.37.0
command: [sh, -c]
args:
- 'printf "%s\n" eks-validation-ok > /work/index.html && exec httpd -f -p 8080 -h /work'
ports:
- containerPort: 8080
readinessProbe:
httpGet: {path: /, port: 8080}
resources:
requests: {cpu: 50m, memory: 32Mi}
limits: {cpu: 200m, memory: 64Mi}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: [ALL]}
volumeMounts:
- {name: work, mountPath: /work}
volumes:
- name: work
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: http-validation
spec:
type: ClusterIP
selector: {app: http-validation}
ports:
- {port: 8080, targetPort: 8080, protocol: TCP}
EOF
kubectl --kubeconfig "$ADMIN_KUBECONFIG" -n "$VALIDATION_NAMESPACE" \
rollout status deployment/http-validation --timeout=180s
```
```bash
kubectl --kubeconfig "$ADMIN_KUBECONFIG" -n "$VALIDATION_NAMESPACE" create -f - <<'EOF'
apiVersion: batch/v1
kind: Job
metadata:
name: dns-http-check
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile: {type: RuntimeDefault}
containers:
- name: client
image: public.ecr.aws/docker/library/busybox:1.37.0
command: [sh, -c]
args:
- |
set -eu
nslookup http-validation
response=$(wget -qO- -T 5 http://http-validation:8080/) || exit 1
[ "$response" = eks-validation-ok ]
printf '%s\n' 'DNS and Service HTTP check passed'
resources:
requests: {cpu: 10m, memory: 16Mi}
limits: {cpu: 100m, memory: 32Mi}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: [ALL]}
EOF
kubectl --kubeconfig "$ADMIN_KUBECONFIG" -n "$VALIDATION_NAMESPACE" \
wait --for=condition=complete job/dns-http-check --timeout=120s
kubectl --kubeconfig "$ADMIN_KUBECONFIG" -n "$VALIDATION_NAMESPACE" logs job/dns-http-check
```
성공 결과는 이 경로만 검증하며 모든 네트워크·스토리지·애플리케이션 요구를 검증하지는 않습니다. 선택적인 로드 밸런서 검증에는 설치·권한이 구성된 컨트롤러, 검토한 scheme·서브넷·보안 규칙과 일반 또는 Auto Mode 컴퓨팅에 맞는 `loadBalancerClass`가 필요합니다. AWS는 `status.loadBalancer.ingress`에 보통 외부 IP 대신 호스트 이름을 반환합니다. 이 테스트는 유료 리소스를 생성하므로 정리까지 포함해야 합니다. Port-forward는 디버깅에 유용하지만 일반 Service·로드 밸런서 데이터 경로를 우회합니다.
```bash
CURRENT_VALIDATION_UID=$(kubectl --kubeconfig "${ADMIN_KUBECONFIG:?}" \
get namespace "${VALIDATION_NAMESPACE:?}" --ignore-not-found \
-o jsonpath='{.metadata.uid}') || exit 1
if [ -z "$CURRENT_VALIDATION_UID" ]; then
printf '%s\n' 'Validation namespace is already absent.'
elif [ "$CURRENT_VALIDATION_UID" = "${VALIDATION_NAMESPACE_UID:?Recorded UID required}" ]; then
kubectl --kubeconfig "$ADMIN_KUBECONFIG" delete namespace "$VALIDATION_NAMESPACE" --wait=true
else
printf '%s\n' 'Namespace identity changed; no deletion attempted.' >&2
exit 1
fi
```
### 실제 로그 전달 확인
새 이벤트를 기대하려면 제어 플레인 로깅이 활성화되어 있어야 합니다. 올바른 리전의 활성 로그 유형·스트림 시각을 확인하며 로그 그룹 존재만으로 전달 성공을 판단하지 않습니다. 전달은 최선형이며 스트림이 회전합니다. 로그를 공개 보고서에 덤프하지 말고 적절한 권한으로 관련 실제 이벤트를 검토하세요.
```bash
aws eks describe-cluster --name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--query cluster.logging
aws logs describe-log-streams --region "$EXAMPLE_REGION" \
--log-group-name "/aws/eks/$EXAMPLE_CLUSTER/cluster" \
--order-by LastEventTime --descending --max-items 5 \
--query 'logStreams[].{stream:logStreamName,lastEvent:lastEventTimestamp}'
```
워커 kubelet·컨테이너 로그에는 별도 수집 경로가 필요하며 EKS 제어 플레인 로깅만 켠다고 활성화되지는 않습니다.
## 클러스터 업그레이드
EKS 버전 일정과 업그레이드 인사이트를 사용합니다. 제거된 API·워크로드 및 데이터 백업·용량·애드온 호환성을 검토하세요. 제어 플레인 업그레이드 전에 EKS 절차에 따라 관리형·Fargate 노드를 현재 제어 플레인 마이너에 맞추고 자체 관리·Hybrid 노드도 권장에 따라 갱신합니다. kubelet은 API 서버보다 최신일 수 없습니다. 아래 API 작업 흐름은 준비 상태 검토를 대체하지 않으며 Terraform·eksctl 소유 리소스는 해당 소유자의 절차를 사용합니다.
```bash
# Read the EKS release catalog; add-on versions are not the cluster release catalog.
aws eks describe-cluster-versions --region "$EXAMPLE_REGION" \
--query 'clusterVersions[].{version:clusterVersion,status:versionStatus,standardEnd:endOfStandardSupportDate,extendedEnd:endOfExtendedSupportDate}' \
--output table
: "${NEXT_KUBERNETES_VERSION:?Select the next supported minor after readiness review}"
CURRENT_KUBERNETES_VERSION=$(aws eks describe-cluster --name "$EXAMPLE_CLUSTER" \
--region "$EXAMPLE_REGION" --query cluster.version --output text)
if [[ "$CURRENT_KUBERNETES_VERSION" =~ ^1\.([0-9]+)$ ]]; then
EXPECTED_NEXT_VERSION="1.$((BASH_REMATCH[1] + 1))"
else
printf '%s\n' 'Unexpected version; stop and inspect.' >&2
exit 1
fi
[ "$NEXT_KUBERNETES_VERSION" = "$EXPECTED_NEXT_VERSION" ] || {
printf '%s\n' 'This upgrade workflow only permits the next minor version.' >&2
exit 1
}
CLUSTER_UPDATE_ID=$(aws eks update-cluster-version --name "$EXAMPLE_CLUSTER" \
--region "$EXAMPLE_REGION" --kubernetes-version "$NEXT_KUBERNETES_VERSION" \
--query update.id --output text)
wait_eks_update "$CLUSTER_UPDATE_ID"
```
### 노드와 애드온 업데이트
관리형 노드 그룹별로 업데이트 전략·PDB·여유 용량·영속 볼륨 제약을 먼저 검증합니다. 강제 축출을 일상적인 우회책으로 사용하지 마세요:
```bash
# Configure/review node update strategy and PDB/capacity prerequisites separately first.
: "${NODEGROUP_TO_UPDATE:?Select an owned managed node group}"
NODE_UPDATE_ID=$(aws eks update-nodegroup-version --cluster-name "$EXAMPLE_CLUSTER" \
--region "$EXAMPLE_REGION" --nodegroup-name "$NODEGROUP_TO_UPDATE" \
--query update.id --output text)
wait_eks_update "$NODE_UPDATE_ID" --nodegroup-name "$NODEGROUP_TO_UPDATE"
```
자체 관리·Hybrid 노드는 이미지·패키지·드레인 수명주기를 별도로 관리합니다. Auto Mode는 노드 수명주기를 관리하며 기존 Fargate Pod는 현재 버전을 사용하도록 제어된 교체가 필요할 수 있습니다. Cluster Autoscaler 같은 컨트롤러도 대상 마이너에 맞춥니다. 제어 플레인보다 먼저 필요한 업데이트는 각 애드온의 호환성 절차에 따라 결정하세요.
```bash
: "${ADDON_NAME:?Select an existing managed add-on}"
TARGET_CLUSTER_VERSION=$(aws eks describe-cluster --name "$EXAMPLE_CLUSTER" \
--region "$EXAMPLE_REGION" --query cluster.version --output text)
aws eks describe-addon --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--addon-name "$ADDON_NAME" > "$EKS_REVIEW_DIR/addon-before.json"
aws eks describe-addon-versions --region "$EXAMPLE_REGION" --addon-name "$ADDON_NAME" \
--kubernetes-version "$TARGET_CLUSTER_VERSION"
: "${REVIEWED_ADDON_VERSION:?Choose a compatible build before inspecting its schema}"
aws eks describe-addon-configuration --region "$EXAMPLE_REGION" --addon-name "$ADDON_NAME" \
--addon-version "$REVIEWED_ADDON_VERSION" --query configurationSchema --output text \
> "$EKS_REVIEW_DIR/addon-target-schema.json"
# Preserve the configuration string, whether the service returned JSON or YAML.
jq -er '(.addon.configurationValues // "{}") |
if type != "string" then error("Unexpected configurationValues type")
elif . == "" then "{}" else . end' \
"$EKS_REVIEW_DIR/addon-before.json" > "$EKS_REVIEW_DIR/addon-values-reviewed.txt"
# Stop here to review this file against the target schema, plus IAM and direct customizations.
```
저장한 구성을 대상 스키마와 대조한 뒤 적용합니다. `configurationValues`에 없는 직접 Kubernetes 수정도 확인하세요. 관리형 CoreDNS의 커스텀 Corefile은 지원되는 `corefile` 설정 키로 관리합니다. `PRESERVE`가 설정 소유권·스키마 검토를 대체하지는 않습니다.
```bash
: "${REVIEWED_ADDON_VERSION:?Use the reviewed compatible build}"
ADDON_UPDATE_ID=$(aws eks update-addon --cluster-name "$EXAMPLE_CLUSTER" \
--region "$EXAMPLE_REGION" --addon-name "$ADDON_NAME" \
--addon-version "$REVIEWED_ADDON_VERSION" --resolve-conflicts PRESERVE \
--configuration-values "file://$EKS_REVIEW_DIR/addon-values-reviewed.txt" \
--query update.id --output text)
wait_eks_update "$ADDON_UPDATE_ID" --addon-name "$ADDON_NAME"
aws eks describe-addon --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--addon-name "$ADDON_NAME" --query 'addon.{status:status,version:addonVersion,health:health}'
```
현재 EKS는 in-place 업그레이드 후 7일 이내에 조건부로 직전 마이너로 롤백할 수 있습니다. etcd·워크로드 구성·영속 데이터를 이전 시점으로 되돌리지는 않습니다. 호환 노드·애드온과 모든 롤백 자격 조건을 확인해야 하며 Auto Mode 노드 롤백은 EKS가 처리합니다. “다운그레이드 불가”라는 일괄 설명이나 무조건적인 실행 취소 보장에 의존하면 안 됩니다.
## 클러스터 삭제
폐기는 별도로 검토하는 작업입니다. 대상 계정·리전·클러스터 신원, 백업·복원 요구와 관련 리소스 소유권을 확인하세요. 먼저 목록을 조사하며 전체 PVC·모든 네임스페이스의 Service를 일괄 삭제하지 않습니다.
```bash
# Read-only inventory: review ownership and data retention before selecting any deletion.
kubectl --kubeconfig "$ADMIN_KUBECONFIG" get services,ingresses -A
kubectl --kubeconfig "$ADMIN_KUBECONFIG" get pvc -A
kubectl --kubeconfig "$ADMIN_KUBECONFIG" get pv
aws eks list-nodegroups --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION"
aws eks list-fargate-profiles --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION"
aws eks list-capabilities --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION"
```
명시적으로 검토한 Service·Ingress·데이터 리소스만 해당 소유자를 통해 삭제합니다. PVC 삭제는 `Delete` 회수 정책에서 스토리지도 삭제할 수 있으며 `Retain`·스냅샷·백업·finalizer는 별도 처리가 필요합니다. 컨트롤러와 IAM 권한을 유지한 상태에서 애플리케이션을 중지하고 의도한 스토리지·로드 밸런서 정리를 기다리세요. EKS 노드 그룹 목록은 관리형 그룹만 포함하므로 자체 관리 ASG·인스턴스·Hybrid Nodes도 별도 조사합니다.
### 원래 리소스 소유자 사용
레이어형 Terraform 프로젝트는 Part 4의 저장·검토한 역순 삭제 계획을 사용합니다. eksctl 생성 클러스터는 리소스·데이터 선행 정리를 마친 뒤 해당 삭제 절차를 사용하고 완료를 기다립니다:
```bash
eksctl delete cluster --name "${EXAMPLE_CLUSTER:?}" --region "${EXAMPLE_REGION:?}" --wait
```
Terraform·CloudFormation 소유 리소스를 API로 삭제한 뒤 상태·스택이 일치한다고 가정하면 안 됩니다. API 소유 클러스터는 최종 클러스터 삭제 전에 모든 소유 관리형 그룹·Fargate 프로필·EKS Capabilities를 처리합니다. ACK·Argo CD·kro 같은 Capability에는 별도 정리 정책이 있습니다. 삭제 보호는 검토한 소유자 절차를 통해서만 해제하며 아래 검사는 여전히 활성화되어 있으면 중단합니다.
```bash
check_retirement_cluster() {
[ "${RETIREMENT_REVIEWED:?Set yes only after this cluster retirement is reviewed}" = yes ] || return 1
local current_cluster
current_cluster=$(aws eks describe-cluster --name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--query cluster --output json) || return 1
printf '%s' "$current_cluster" |
jq -e --arg arn "${EXPECTED_CLUSTER_ARN:?}" --arg created "${EXPECTED_CLUSTER_CREATED:?}" \
'.arn == $arn and (.createdAt | tostring) == $created and .deletionProtection != true' \
>/dev/null || {
printf '%s\n' 'Cluster identity changed or deletion protection is enabled; stop.' >&2
return 1
}
}
```
```bash
# For an API-owned group, after workload/data cleanup and ownership review.
: "${NODEGROUP_TO_DELETE:?Select a reviewed managed node group}"
aws eks describe-nodegroup --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--nodegroup-name "$NODEGROUP_TO_DELETE" --query 'nodegroup.{arn:nodegroupArn,status:status}'
if [ "${RETIREMENT_REVIEWED:?Set yes only for this reviewed cluster retirement}" = yes ]; then
check_retirement_cluster
aws eks delete-nodegroup --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--nodegroup-name "$NODEGROUP_TO_DELETE"
aws eks wait nodegroup-deleted --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--nodegroup-name "$NODEGROUP_TO_DELETE"
fi
```
```bash
# Delete profiles serially; another profile cannot be deleted while one is DELETING.
: "${FARGATE_PROFILE_TO_DELETE:?Select a reviewed Fargate profile}"
aws eks describe-fargate-profile --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--fargate-profile-name "$FARGATE_PROFILE_TO_DELETE"
if [ "${RETIREMENT_REVIEWED:?}" = yes ]; then
check_retirement_cluster
aws eks delete-fargate-profile --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--fargate-profile-name "$FARGATE_PROFILE_TO_DELETE"
aws eks wait fargate-profile-deleted --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--fargate-profile-name "$FARGATE_PROFILE_TO_DELETE"
fi
```
검토한 소유 항목별로 반복하며 Fargate 프로필은 순차적으로 삭제합니다. Capability·자체 관리 인프라는 문서화된 소유자를 통해 정리합니다. 이후 기록된 클러스터 신원과 관리 리소스 목록이 비었는지 검증하고 마지막 API를 호출하세요:
```bash
# Final API-owned-cluster step. It does not disable deletion protection or delete capabilities.
[ "${RETIREMENT_REVIEWED:?}" = yes ] || exit 1
check_retirement_cluster
aws eks describe-cluster --name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
--query cluster --output json > "$EKS_REVIEW_DIR/cluster-before-delete.json"
jq -e --arg arn "${EXPECTED_CLUSTER_ARN:?}" --arg created "${EXPECTED_CLUSTER_CREATED:?}" \
'.arn == $arn and (.createdAt | tostring) == $created and .deletionProtection != true' \
"$EKS_REVIEW_DIR/cluster-before-delete.json" >/dev/null || {
printf '%s\n' 'Cluster identity changed or deletion protection is enabled; stop.' >&2
exit 1
}
aws eks list-nodegroups --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
> "$EKS_REVIEW_DIR/remaining-nodegroups.json"
aws eks list-fargate-profiles --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
> "$EKS_REVIEW_DIR/remaining-fargate.json"
aws eks list-capabilities --cluster-name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION" \
> "$EKS_REVIEW_DIR/remaining-capabilities.json"
jq -e '.nodegroups | type == "array" and length == 0' \
"$EKS_REVIEW_DIR/remaining-nodegroups.json" >/dev/null
jq -e '.fargateProfileNames | type == "array" and length == 0' \
"$EKS_REVIEW_DIR/remaining-fargate.json" >/dev/null
jq -e '.capabilities | type == "array" and length == 0' \
"$EKS_REVIEW_DIR/remaining-capabilities.json" >/dev/null
aws eks delete-cluster --name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION"
aws eks wait cluster-deleted --name "$EXAMPLE_CLUSTER" --region "$EXAMPLE_REGION"
```
### 잔여 리소스와 보존 정책 확인
Auto Mode 클러스터 삭제는 문서화된 관리 노드·EC2 인스턴스·로드 밸런서도 제거합니다. 그렇다고 임의의 공유 VPC 리소스를 삭제해도 되는 것은 아닙니다. 기록한 소유권·보존 계획에 따라 볼륨·스냅샷·NAT Gateway·EIP·ENI·보안 그룹·IAM 역할·OIDC provider·로그 그룹을 확인하세요.
가능하면 원래 VPC·IAM Terraform 상태나 CloudFormation 스택을 사용합니다. `delete-vpc` 하나는 의존성을 고려한 정리 절차가 아닙니다. 일반적인 `EKSClusterRole`·`EKSNodeRole` 이름만 보고 정책을 분리하거나 공유 역할을 삭제하지 마세요. 감사·복구에 필요한 로그 그룹과 암호화 키를 보존합니다. EKS 제어 플레인 삭제가 이들의 삭제를 뜻하지는 않습니다.
## 퀴즈
[EKS 클러스터 생성 - Part 5 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/02-eks-cluster-creation-part5-quiz)
## 참고 자료
- [Access entry groups](https://docs.aws.amazon.com/eks/latest/userguide/create-k8s-group-access-entry.html)
- [Access-policy authorization](https://docs.aws.amazon.com/eks/latest/userguide/access-policies.html)
- [Access migration](https://docs.aws.amazon.com/eks/latest/userguide/migrating-access-entries.html)
- [EKS version lifecycle](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html)
- [Cluster upgrade](https://docs.aws.amazon.com/eks/latest/userguide/update-cluster.html)
- [Cluster rollback](https://docs.aws.amazon.com/eks/latest/userguide/rollback-cluster.html)
- [Control-plane logging](https://docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html)
- [Cluster deletion](https://docs.aws.amazon.com/eks/latest/userguide/delete-cluster.html)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/02-eks-cluster-creation-conclusion
----------------------------------------
# EKS 클러스터 생성 - 결론 및 모범 사례
> **마지막 업데이트**: 2026년 9월 11일
## EKS 클러스터 생성 방법 비교
지금까지 다양한 방법으로 EKS 클러스터를 생성하는 방법을 살펴보았습니다. 각 방법의 장단점을 비교해 보겠습니다.
도구는 재현성·검토 가능성·팀 역량·수명 주기 소유권으로 선택합니다. 어떤 도구로 만든 클러스터도 워크로드·네트워크·보안 검증이 필요합니다. 리소스별 의도한 소유자를 하나로 정하고 plan/change set을 검토하며 수동 변경과 IaC 상태를 조정합니다.
### eksctl
**장점:**
- EKS 중심의 간결한 워크플로우; 소요 시간은 생성 리소스에 따라 다름
- 단일 명령어로 클러스터 생성 가능
- YAML 파일을 통한 선언적 구성 지원
- 노드 그룹, Fargate 프로필 등 다양한 기능 지원
**단점:**
- 복잡한 인프라 요구 사항에는 제한적일 수 있음
- 기존 인프라와의 통합이 어려울 수 있음
**적합한 사용 사례:**
- 빠른 프로토타이핑
- 개발 및 테스트 환경
- 구성·소유권·수명 주기를 검토한 프로덕션 환경
### AWS Management Console
**장점:**
- 시각적 인터페이스로 쉽게 이해 가능
- 단계별 가이드를 통한 클러스터 생성
- 다양한 옵션을 시각적으로 확인 가능
**단점:**
- 수동 프로세스로 자동화가 어려움
- 반복적인 작업에 시간이 많이 소요됨
- 구성 관리 및 버전 관리가 어려움
**적합한 사용 사례:**
- 학습 및 탐색
- 일회성 클러스터 생성
- 소규모 팀 또는 프로젝트
### AWS CLI
**장점:**
- 스크립트를 통한 자동화 가능
- 세밀한 제어 가능
- AWS 서비스와의 통합이 용이
**단점:**
- 복잡한 명령어 구조
- 여러 단계의 명령어 실행 필요
- 오류 처리가 어려울 수 있음
**적합한 사용 사례:**
- 자동화 스크립트의 일부
- CI/CD 파이프라인 통합
- 세밀한 제어가 필요한 환경
### Terraform
**장점:**
- 인프라를 코드로 관리(IaC)
- 상태 관리 및 변경 추적
- 다양한 AWS 서비스와의 통합
- 모듈화 및 재사용성
**단점:**
- 학습 곡선이 있음
- 초기 설정에 시간이 소요됨
- 상태의 보호·잠금·복구 설계 필요; 로컬 backend 자체에 추가 인프라가 필수는 아님
**적합한 사용 사례:**
- 대규모 프로덕션 환경
- 다중 환경 관리(개발, 스테이징, 프로덕션)
- 복잡한 인프라 요구 사항
### AWS CDK
**장점:**
- 익숙한 프로그래밍 언어 사용(TypeScript, Python 등)
- 높은 수준의 추상화
- 코드 재사용 및 모듈화
- AWS 서비스와의 긴밀한 통합
**단점:**
- 학습 곡선이 있음
- 디버깅이 복잡할 수 있음
- Construct·버전별 지원 범위가 다름; 합성된 CloudFormation과 custom resource 동작 검토 필요
**적합한 사용 사례:**
- 개발자 중심 환경
- 복잡한 애플리케이션 인프라
- 기존 애플리케이션 코드와의 통합
## EKS 클러스터 생성 모범 사례
### 네트워킹
1. **VPC 설계**
- 최소 2개 이상의 가용 영역에 서브넷 배포
- 실제 ingress/egress 요구에 따라 public/private 배치 선택; private-only 설계는 서비스 엔드포인트와 프라이빗 연결 사용 가능
- 사용 가능 주소, CNI warm/prefix pool과 업그레이드 여유 계획; 서브넷 공간과 노드 ENI/maxPods 제한 구분
- 선택한 컨트롤러의 서브넷 디스커버리 태그·구성 사용; 태그가 경로나 보안 경계를 만들지는 않음
2. **보안 그룹 구성**
- 최소 권한 원칙 적용
- 실제 API·kubelet·DNS·webhook·애플리케이션 경로 허용; kubelet은 임의의 광범위 임시 포트가 아닌 TCP 10250 사용
- 소스 IP 제한
- 보안 그룹 간 참조 활용
3. **네트워크 정책**
- 지원 정책 엔진 선택: VPC CNI 네이티브 정책 또는 적절한 Calico/Cilium 설계; 충돌하는 엔진을 함께 활성화하지 않음
- 포드 간 통신 제한
- 네임스페이스 ingress/egress·DNS를 허용/거부 테스트로 검증; 일치하는 Kubernetes NetworkPolicy 허용은 합집합
### 보안
1. **IAM 역할 및 정책**
- 최소 권한 원칙 적용
- 컴퓨팅·agent/SDK·신뢰 요건에 맞춰 EKS Pod Identity 또는 IRSA 사용; 애플리케이션 권한 제한
- 세분화된 권한 정책 구성
2. **암호화**
- EBS 볼륨 암호화 활성화
- EKS 기본 API 데이터 envelope encryption(1.28+ KMSv2)과 customer-managed KMS key 필요 여부 확인; base64는 암호화가 아님
- 전송 중 데이터 암호화(TLS)
3. **인증 및 권한 부여**
- EKS IAM 인증과 적절한 access entry/access policy 사용; AWS CLI 토큰 사용 시 클라이언트에 별도 aws-iam-authenticator 바이너리가 필수는 아님
- Kubernetes RBAC와 EKS access-policy 권한을 함께 검토; 허용 권한은 합집합
- 신원·네임스페이스를 분리하고 RBAC·Pod Security·네트워크 제어 적용; 네임스페이스만으로 완전한 테넌트 격리가 되지는 않음
### 확장성 및 가용성
1. **노드 그룹 구성**
- 여러 가용 영역에 노드 배포
- managed/self-managed node group, Karpenter, Auto Mode, Fargate 등 컴퓨팅 소유자 확인; 모든 모드를 운영자 관리 ASG로 가정하지 않음
- 다양한 인스턴스 유형 활용(Spot 인스턴스 포함)
2. **클러스터 오토스케일러**
- 필요한 경우 호환 Cluster Autoscaler/Karpenter 릴리스 사용; Auto Mode는 자체 용량 관리. 같은 pool의 소유자 충돌 방지
- 워크로드 복제본 확장과 노드 프로비저닝 구분; requests·배치 불가 Pod·용량 제약 검증
- 선택한 컨트롤러에서 disruption/consolidation 예산·시간 조정; 애플리케이션 드레인·복구 테스트
3. **고가용성 구성**
- 다중 가용 영역 활용
- 해당되는 자발적 eviction에 PodDisruptionBudget 사용; 노드 장애나 모든 강제 중단을 막지는 않음
- 목표 장애 시나리오에 맞춰 복제본 수·topology spread·readiness·용량 설정
### 모니터링 및 로깅
1. **컨트롤 플레인 로깅**
- 컨트롤 플레인 로그5종(api, audit, authenticator, controllerManager, scheduler)을 보존 기간·접근·비용 제어와 함께 검토
- CloudWatch Logs와 통합
2. **노드 및 포드 모니터링**
- 필요한 CloudWatch Container Insights/add-on 신호와 지원 컴퓨팅 구성 선택
- Prometheus/Grafana 또는 기존 모니터링 플랫폼을 의도적으로 선택; 중복 수집·검토하지 않은 자동 계측 방지
- 사용자 정의 메트릭 구성
3. **알림 및 경고**
- CloudWatch 경보 구성
- 승인된 알림 목적지와 전달·소유권 확인; 구독 확인 완료를 가정하지 않음
- 중요 이벤트에 대한 알림 구성
### 비용 최적화
1. **인스턴스 유형 선택**
- 워크로드에 적합한 인스턴스 유형 선택
- 중단을 허용할 수 있는 워크로드에 Spot 사용; 용량·장애 처리 검증
- 애플리케이션·이미지·agent·add-on의 아키텍처 호환성 검증 후 Graviton 고려
2. **오토스케일링**
- 수요에 따른 자동 스케일링 구성
- 스케일 다운 정책 최적화
- 예약 스케일링 고려
3. **리소스 요청 및 제한**
- 적절한 CPU 및 메모리 요청 설정
- 워크로드 동작에 맞춰 제한 설정; 메모리 OOM·CPU throttling 영향을 고려
- 리소스 쿼터 및 제한 범위 설정
4. **Fargate 활용**
- 스케줄링·네트워킹·스토리지·권한 제약이 맞는 워크로드에 Fargate 사용
- Fargate 프로필 최적화
- 비용 대비 성능 평가
## 다음 단계
EKS 클러스터를 성공적으로 생성한 후에는 다음과 같은 단계를 고려해 볼 수 있습니다:
1. **클러스터 업그레이드 전략 수립**
- Upstream Kubernetes 릴리스뿐 아니라 EKS 지원 목록과 정확한 add-on·client·node 호환성에 따라 업그레이드 계획
- In-place와 클러스터 교체 전략 비교; 현재 EKS control-plane rollback은 조건부이며 애플리케이션 데이터를 복원하지 않음
- 업그레이드 테스트 자동화
2. **재해 복구 계획**
- RPO/RTO 정의 후 애플리케이션 데이터·구성·필요 키 백업; 스냅샷 생성뿐 아니라 일관성·복원 검증
- 복제·DNS/failover·IAM/KMS·비용 전제를 명시하여 다중 리전 복구 선택
- 장애 시나리오 테스트
3. **CI/CD 파이프라인 통합**
- GitOps 워크플로우 구현
- 자동화된 배포 파이프라인 구축
- 테스트 및 검증 자동화
4. **추가 서비스 통합**
- 선택한 컴퓨팅·ingress 경로에 필요한 AWS Load Balancer Controller; Auto Mode 관리 컨트롤러 중복 설치 방지
- DNS 소유권·IAM 권한을 제한한 ExternalDNS
- Kubernetes 인증서 발급이 필요한 경우 cert-manager; ALB ACM 인증서 관리는 별도 경로
- 컴퓨팅 지원·프로비저닝 소유자에 맞는 EBS/EFS 스토리지; Auto Mode EBS·Fargate 경로는 일반 EC2 add-on과 다름
5. **보안 강화**
- 취약점 스캐닝 구현
- 컴플라이언스 모니터링
- 보안 정책 자동화
EKS 클러스터 생성은 Kubernetes 여정의 시작일 뿐입니다. 지속적인 관리, 모니터링, 최적화를 통해 안정적이고 효율적인 Kubernetes 환경을 유지하는 것이 중요합니다.
## 검증 참고 자료
- [EKS networking requirements](https://docs.aws.amazon.com/eks/latest/userguide/network-reqs.html)
- [Private EKS clusters](https://docs.aws.amazon.com/eks/latest/userguide/private-clusters.html)
- [Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html)
- [API-data envelope encryption](https://docs.aws.amazon.com/eks/latest/userguide/envelope-encryption.html)
- [Access policy permissions](https://docs.aws.amazon.com/eks/latest/userguide/access-policies.html)
- [EKS Auto Mode](https://docs.aws.amazon.com/eks/latest/userguide/automode.html)
- [Conditional cluster rollback](https://docs.aws.amazon.com/eks/latest/userguide/rollback-cluster.html)
구현과 로컬 검증 예제는 [생성 Part 4](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part4.md), [Part 5](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation-part5.md), [네트워킹 Part 2](https://www.atomai.click/kubernetes-docs/llms/ko/eks/03-eks-networking-part2.md)에 있습니다. 프로덕션 준비 여부는 해당 환경의 장애·복원 테스트로 확인해야 합니다.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/03-eks-networking-part1
----------------------------------------
# EKS 네트워킹
> **마지막 업데이트**: 2026년 9월 11일
## 개요
일반 EKS 클러스터의 VPC·서브넷 계획과 보안 그룹 경로를 다룹니다. 제어 플레인은 AWS 관리 인프라에 있고 고객 VPC에는 클러스터 연결 ENI, 노드·Pod 인터페이스, 로드 밸런서·엔드포인트 리소스가 있습니다. 고객 VPC 경계 안에 관리형 제어 플레인이나 리전 S3·ECR·STS 서비스가 물리적으로 위치한다고 해석하면 안 됩니다.
## EKS 네트워킹 아키텍처
| 구성 요소 | 역할 |
| --- | --- |
| VPC·서브넷 | 주소·라우팅 경계; 서브넷 하나는 AZ 하나에 속함 |
| 라우팅 테이블 | 목적지 범위에 따른 다음 경로 선택 |
| Internet Gateway | VPC에 연결되며 퍼블릭 서브넷이 이 경로를 사용 |
| 퍼블릭 NAT Gateway | 퍼블릭 서브넷의 EIP·IGW 경로로 프라이빗 IPv4 인터넷 egress 제공 |
| 보안 그룹 | 지원 네트워크 인터페이스·리소스에 연결되는 상태 저장 규칙 |
| 네트워크 ACL | 필요한 응답 트래픽도 포함하는 비상태 저장 서브넷 경계 규칙 |
| CNI | 선택한 구현·모드에 따라 Pod 네트워크 구성 |
### 트래픽 경로
Pod 간 트래픽은 한 노드 안에서 처리되거나 노드 인터페이스·VPC 라우트를 지날 수 있습니다. 같은 노드의 트래픽은 VPC 경로를 지나지 않을 수 있으므로 VPC Flow Logs가 모든 Pod 통신 기록은 아닙니다. Service 트래픽은 구성한 프록시 경로와 선택한 백엔드를 사용하며 Service 자체가 전용 전달 장비는 아닙니다. 외부 ingress·egress는 scheme·라우팅·대상 유형·보안 제어에 따라 달라지고 제어 플레인 통신은 애플리케이션 트래픽과 별개입니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-03-eks-networking-part1-2.html)
그림은 퍼블릭 ingress와 AZ 단위 NAT의 IPv4 egress 예제입니다. 모든 EKS 클러스터에 퍼블릭 서브넷·NAT를 만들라는 요구가 아닙니다. 완전한 프라이빗 엔드포인트, 중앙 egress, 네이티브 IPv6 경로는 조건과 비용이 다릅니다.
## VPC와 서브넷 요구 사항
* 리전 EKS 클러스터는 최소 두 AZ의 적격 서브넷을 선택합니다. 클러스터 서브넷마다 EKS용 가용 IP가 최소 6개 필요하며 AWS는 16개 이상을 권장합니다. 업그레이드의 제어 플레인 ENI 교체와 다른 리소스의 여유도 확보하세요.
* VPC DNS 지원·호스트 이름을 활성화합니다. 클러스터 서브넷과 노드·Pod 서브넷이 같을 필요는 없지만 필요한 경로는 라우팅되어야 합니다.
* 노드에는 Kubernetes API·이미지 레지스트리·사용하는 AWS 서비스 접근이 필요합니다. 반드시 퍼블릭 인터넷이 필요한 것은 아니며 프라이빗 엔드포인트·미러로 경로를 제공할 수 있습니다. EKS AWS 서비스 엔드포인트와 클러스터 Kubernetes API 엔드포인트는 별개입니다.
* `controlPlaneEgressMode=CUSTOMER_ROUTED`를 사용하면 제어 플레인 egress 경로도 검토합니다. 클러스터 서브넷의 라우팅·보안 규칙이 필요한 웹훅·OIDC 등 엔드포인트에 도달해야 합니다.
* 클러스터 서브넷 변경에는 원래 VPC·AZ 집합 제약이 유지됩니다. VPC CIDR 추가가 모든 제어 플레인 작업에 즉시 반영되는 것은 아니며 AWS는 최대 한 시간의 조정 시간이 걸릴 수 있다고 설명합니다.
### CIDR 계획
예상 노드, 일반·branch ENI Pod, warm pool·prefix 블록, 제어 플레인 인터페이스, 로드 밸런서·엔드포인트, 증가량·업데이트 용량으로 주소를 계획합니다. Service CIDR·연결 VPC·온프레미스 범위와 중복을 확인하세요. 노드 수만으로 안전한 VPC CIDR을 정할 수 없으며 고정된 20–30% 여유가 보편적 용량 규칙도 아닙니다.
다음은 클러스터 규모 권장값이나 사용 가능한 Pod 용량이 아닌 **전체 IPv4 주소 개수**입니다:
| CIDR | Total addresses |
| --- | ---: |
| /24 | 256 |
| /22 | 1,024 |
| /20 | 4,096 |
| /16 | 65,536 |
일반 AWS IPv4 서브넷 할당에서는 각 서브넷의 처음 4개·마지막 1개 주소를 예약합니다. 따라서 워크로드·인프라 사용 전 /24는 251개, /22는 1,019개를 할당할 수 있습니다. VPC 전체에서 5개가 아닌 서브넷별 예약이며 BYOIP에는 문서화된 예외가 있습니다. 기존 서브넷 CIDR은 제자리에서 간단히 늘릴 수 없습니다. Prefix delegation은 연속 블록이 필요하며 주소 공간 자체를 만들지는 않습니다.
### 서브넷 설계 예제
VPC가 `10.0.0.0/16`일 때 아래 정렬된 범위는 중복되지 않습니다. 퍼블릭 로드 밸런서·AZ별 NAT 설계 예제이며 실측 사이징은 아닙니다. 퍼블릭 서브넷은 IGW로 향하는 라우트가 연결되어 있어야 하며 태그·이름만으로 퍼블릭이 되지 않습니다. 프라이빗 노드는 NAT 또는 적절한 프라이빗 서비스 엔드포인트를 사용할 수 있습니다.
| 유형 | AZ | CIDR | 예시 용도 |
| --- | --- | --- | --- |
| 퍼블릭 | us-west-2a | 10.0.0.0/24 | 퍼블릭 로드 밸런서·AZ별 NAT |
| 퍼블릭 | us-west-2b | 10.0.1.0/24 | 퍼블릭 로드 밸런서·AZ별 NAT |
| 프라이빗 | us-west-2a | 10.0.4.0/22 | 워커 노드·Pod |
| 프라이빗 | us-west-2b | 10.0.8.0/22 | 워커 노드·Pod |
기존 `10.0.2.0/22`·`10.0.6.0/22`는 정렬된 네트워크 주소가 아니었습니다. AWS가 CIDR을 정규화하면 첫 범위가 `10.0.0.0/22`가 되어 퍼블릭 서브넷과 중복됩니다. 프로비저닝 전에 경계·중복을 검증하세요. AZ 내부 NAT 경로는 다른 AZ의 NAT 의존성을 줄이지만 여유 용량·라우팅·워크로드 배치도 검토해야 합니다.
### 서브넷 검색 태그
검색 동작은 선택한 컨트롤러·버전에 따라 다릅니다. AWS Load Balancer Controller 3.5는 역할 태그로 퍼블릭·내부 서브넷을 선택하며 값은 `1` 또는 빈 값입니다. 최신 LBC에서 기존 클러스터 소유권 태그가 항상 필수는 아닙니다. 역할 태그 후보가 없으면 LBC 2.12.1 이상은 `SubnetDiscoveryByReachability` 활성화 시 라우팅 기반 검색을 사용할 수 있습니다. 클러스터 태그 필터·가용 IP·AZ별 선택도 중요합니다. EKS Auto Mode는 별도의 태그 요구 사항을 따릅니다.
* 인터넷 연결 배치: `kubernetes.io/role/elb`.
* 내부 배치: `kubernetes.io/role/internal-elb`.
* `kubernetes.io/cluster/`는 필터·우선순위에 영향을 줄 수 있으며 `owned`·`shared`는 보안 경계나 자동 라우팅 규칙이 아닙니다.
```bash
# After reviewing subnet ownership and its associated routes, tag the intended public subnet.
aws ec2 describe-subnets --region "${EXAMPLE_REGION:?}" --subnet-ids "${PUBLIC_SUBNET_ID:?}" \
--query 'Subnets[].{id:SubnetId,vpc:VpcId,cidr:CidrBlock,free:AvailableIpAddressCount,tags:Tags}'
aws ec2 create-tags --region "$EXAMPLE_REGION" --resources "$PUBLIC_SUBNET_ID" \
--tags Key=kubernetes.io/role/elb,Value=1
```
검토한 프라이빗·내부 배치에는 internal-elb 역할 태그를 사용합니다. 검색 실패를 검토 없이 우회하려고 임의의 기존 서브넷을 태깅하거나 클러스터 태그 검사를 끄지 마세요. 서브넷을 명시적으로 선택해도 로드 밸런서 적격성 요구를 충족해야 합니다.
### 보안 그룹 경로
EKS는 기본 클러스터 보안 그룹을 만들고 클러스터 ENI 및 일반적인 관리형 노드 인터페이스에 연결합니다. 항상 두 그룹으로 분리되는 구조는 아닙니다. 추가 클러스터 SG가 자동으로 노드 SG가 되지는 않으며 커스텀 Launch Template·Pod 보안 그룹에 따라 경로가 달라집니다. 규칙 수정 전에 실제 연결을 확인하세요:
```bash
aws eks describe-cluster --name "${EXAMPLE_CLUSTER:?}" --region "${EXAMPLE_REGION:?}" \
--query 'cluster.resourcesVpcConfig.{clusterSG:clusterSecurityGroupId,additionalSGs:securityGroupIds,subnets:subnetIds,public:endpointPublicAccess,private:endpointPrivateAccess,publicCIDRs:publicAccessCidrs}'
```
| 경로 | 일반적인 목적지 포트 | 검토 범위 |
| --- | --- | --- |
| 노드·허용된 연결 클라이언트 → 프라이빗 Kubernetes API | TCP 443 | 엔드포인트 SG와 승인된 소스 |
| 제어 플레인 → kubelet | TCP 10250 | 대상 노드 SG·라우팅 |
| 노드·Pod → DNS 백엔드 | UDP·TCP 53 | 실제 CoreDNS·NodeLocal DNS 경로 |
| 제어 플레인 → admission webhook | 구성된 백엔드 포트 | 웹훅 Service·엔드포인트·SG 경로 |
| 애플리케이션·로드 밸런서 → 워크로드 | 구성된 앱·상태 확인 포트 | 대상 유형·SG·상태 확인 |
AWS는 기본 클러스터 SG를 제한할 때 클러스터 SG 대상 TCP 443·TCP 10250·TCP/UDP 53을 최소 아웃바운드로 안내하며 실제 앱·노드 간 통신·서비스 접근 요구도 추가해야 합니다. 기존 `1025–65535` kubelet 범위가 최소 요구는 아닙니다. SG는 상태 저장 방식이므로 허용된 연결의 응답을 위해 임시 포트 전체를 무조건 열지 않습니다. NACL은 비상태 저장 방식이므로 해당 응답 경로도 허용해야 합니다.
기본 self ingress·self egress/EFA 규칙은 클러스터 업데이트에서 다시 생성될 수 있습니다. 좁은 SG를 추가해도 다른 연결 SG의 넓은 허용을 취소하지 못합니다. 퍼블릭 API는 `publicAccessCidrs`를 사용하며 클러스터 SG는 프라이빗 엔드포인트 경로를 제어합니다. 레지스트리·AWS API egress를 검토하고 적절한 엔드포인트·라우팅을 사용하세요. 모든 클러스터에 `ALL → 0.0.0.0/0`이 필요하다고 가정하면 안 됩니다.
## 다음 단계와 퀴즈
[EKS 네트워킹 Part 2](https://www.atomai.click/kubernetes-docs/llms/ko/eks/03-eks-networking-part2.md)에서 서비스·로드 밸런싱·정책을 이어서 다룹니다. [Part 1 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/03-eks-networking-part1-quiz)로 이해를 확인하세요.
## 참고 자료
- [EKS VPC/subnets](https://docs.aws.amazon.com/eks/latest/userguide/network-reqs.html)
- [Private clusters](https://docs.aws.amazon.com/eks/latest/userguide/private-clusters.html)
- [Subnet sizing](https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html)
- [Security groups](https://docs.aws.amazon.com/eks/latest/userguide/sec-group-reqs.html)
- [LBC subnet discovery](https://github.com/kubernetes-sigs/aws-load-balancer-controller/blob/v3.5.0/docs/deploy/subnet_discovery.md)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/03-eks-networking-part2
----------------------------------------
# EKS 네트워킹 - Part 2: 서비스 및 로드 밸런싱, 네트워크 정책
> **예제 검증 버전**: EKS Kubernetes 1.36, AWS Load Balancer Controller 3.5.0, Gateway API 1.6.0
> **마지막 업데이트**: 2026년 9월 11일
## 개요
이 문서에서는 Amazon EKS에서의 서비스 및 로드 밸런싱, 네트워크 정책에 대해 알아보겠습니다. Kubernetes 서비스를 통해 애플리케이션을 노출하는 방법, AWS 로드 밸런서와의 통합, 그리고 네트워크 정책을 사용하여 포드 간 통신을 제어하는 방법을 다룹니다.
## Kubernetes 서비스 유형
Kubernetes에서는 다음과 같은 서비스 유형을 제공합니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-03-eks-networking-part2-0.html)
1. **ClusterIP**: 클러스터 라우팅용 가상 IP; 접근 제어 경계는 아님
2. **NodePort**: 라우팅·방화벽 규칙이 허용하는 노드 주소와 할당 포트로 노출되는 서비스
3. **LoadBalancer**: 설치된 로드 밸런서 구현이 처리하는 서비스; 내부 로드 밸런서도 가능
4. **ExternalName**: 외부 서비스에 대한 CNAME 레코드 제공
### ClusterIP 서비스
ClusterIP는 기본 유형이며 가상 IP를 통한 클러스터 통신을 제공합니다. 네임스페이스 격리나 인증을 강제하지는 않습니다. Headless Service(`clusterIP: None`)는 DNS로 엔드포인트 주소를 제공합니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
type: ClusterIP
```
### NodePort 서비스
NodePort는 구성된 노드 주소와 할당 포트(기본 범위 30000–32767)를 사용합니다. 노드 상태, 서비스 프록시 설정, `externalTrafficPolicy`, 경로와 보안 규칙에 따라 실제 연결 가능성이 달라집니다. 서비스 하나를 위해 전체 포트 범위를 열 필요는 없습니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
nodePort: 30080
type: NodePort
```
### LoadBalancer 서비스
설치된 컨트롤러가 Service를 처리합니다. 예제는 LBC를 명시적으로 선택하여 Pod IP를 대상으로 내부 NLB를 만듭니다. ALB는 Ingress 또는 ALB Gateway로 구성합니다. EKS Auto Mode는 `eks.amazonaws.com/nlb`와 별도 지원 설정을 사용합니다. 같은 `my-service` 예제를 중복 적용하지 마세요.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internal
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
spec:
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
allocateLoadBalancerNodePorts: false
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
```
### ExternalName 서비스
ExternalName 서비스는 외부 서비스에 대한 CNAME 레코드를 제공합니다. 트래픽을 프록시하거나 TLS·포트·방화벽 접근을 구성하지 않습니다. HTTP Host 헤더와 인증서 이름이 실제 대상과 일치해야 합니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: ExternalName
externalName: my-service.example.com
```
## AWS 로드 밸런서 통합
EKS는 Kubernetes 서비스를 AWS 로드 밸런서와 통합하여 외부에서 애플리케이션에 액세스할 수 있게 합니다.
### Classic Load Balancer(CLB)
이전 AWS 서비스 컨트롤러 경로에서는 CLB를 만들 수 있었습니다. 이는 현재 LBC의 기본 동작이 아닙니다. LBC 2.5+는 일반적으로 새 LoadBalancer Service에 NLB 클래스를 지정합니다. 기존 Service를 이전하기 전에 실제 컨트롤러·클래스·소유권을 확인하세요. 소유권 어노테이션을 직접 바꾸면 리소스 누수나 노출 변경이 생길 수 있습니다.
### Network Load Balancer (NLB)
위의 명시적 NLB 예제를 사용합니다. 다음은 독립 매니페스트가 아니라 **해당 Service에 병합할 어노테이션 조각**입니다:
```yaml
metadata:
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internal
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-attributes: load_balancing.cross_zone.enabled=true
service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: preserve_client_ip.enabled=true
```
`aws-load-balancer-nlb-target-type`이 대상 유형을 선택합니다. `preserve_client_ip.enabled`는 대상 유형이 아니라 소스 IP 동작을 바꿉니다. 교차 영역 로드 밸런싱은 용량·가용성·비용을 함께 판단하며 영역별 대상과 장애 동작을 확인해야 합니다. Proxy Protocol v2는 이를 해석하는 백엔드가 필요하므로 일반 HTTP 서버에 무조건 켜면 요청이 실패할 수 있습니다.
### Application Load Balancer(ALB)
ALB를 사용하려면 AWS Load Balancer Controller를 설치하고 Ingress 리소스를 사용해야 합니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-03-eks-networking-part2-2.html)
1. 플랫폼 관리자와 컨트롤러를 준비합니다. 아래 명령은 고정된 IAM 정책 다운로드와 차트 렌더링만 수행하며 IAM 신뢰 관계를 만들지 않습니다. 릴리스 정책 및 검토한 Pod Identity 연결 또는 IRSA 역할/OIDC 신뢰를 가진 `kube-system/aws-load-balancer-controller` ServiceAccount를 먼저 준비하세요. 기존 설치는 원래 Helm/IaC 소유자로 관리합니다. 서브넷 디스커버리, API/webhook 연결과 kubeconfig도 확인합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the existing cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
: "${VPC_ID:?Set the cluster VPC ID}"
curl --fail --show-error --location \
https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v3.5.0/docs/install/iam_policy.json \
--output lbc-iam-policy-v3.5.0.json
helm repo add eks https://aws.github.io/eks-charts
helm repo update eks
helm template aws-load-balancer-controller eks/aws-load-balancer-controller \
--version 3.5.0 --namespace kube-system \
--set-string clusterName="$CLUSTER_NAME" \
--set-string region="$AWS_REGION" --set-string vpcId="$VPC_ID" \
--set serviceAccount.create=false \
--set-string serviceAccount.name=aws-load-balancer-controller \
> lbc-rendered.yaml
```
렌더링 결과 검토와 IAM 준비를 마친 뒤 다음 명령을 실행하면 클러스터가 변경됩니다. CRD는 릴리스 절차대로 갱신하며 Helm upgrade가 모든 CRD를 자동 갱신하지는 않습니다.
```bash
helm upgrade --install aws-load-balancer-controller eks/aws-load-balancer-controller \
--version 3.5.0 --namespace kube-system \
--set-string clusterName="$CLUSTER_NAME" \
--set-string region="$AWS_REGION" --set-string vpcId="$VPC_ID" \
--set serviceAccount.create=false \
--set-string serviceAccount.name=aws-load-balancer-controller
kubectl -n kube-system rollout status deployment/aws-load-balancer-controller --timeout=180s
```
2. `spec.ingressClassName: alb`인 Ingress를 만듭니다. 별도 로드 밸런서를 중복 생성하지 않도록 첫 예제의 **ClusterIP** `my-service`를 백엔드로 사용합니다. 선택된 Pod는 실제로 8080 포트를 리스닝해야 합니다. 위 그림은 논리적 구성 관계이며 트래픽이 Ingress API 객체나 컨트롤러 Pod를 통과한다는 뜻은 아닙니다.
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
annotations:
alb.ingress.kubernetes.io/scheme: internal
alb.ingress.kubernetes.io/target-type: ip
spec:
ingressClassName: alb
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-service
port:
number: 80
```
HTTPS 사용 시 아래 조각을 Ingress에 병합하고 ACM ARN과 보안 그룹을 같은 리전/VPC의 실제 리소스로 바꿉니다. 사용자 지정 프론트엔드 SG 규칙은 직접 구성하며 백엔드 규칙 관리는 별도 선택입니다. 참조되지 않은 action 어노테이션만으로 리디렉션이 생기지는 않습니다. 아래 `ssl-redirect`는 공식 단축 설정입니다.
```yaml
metadata:
annotations:
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80},{"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:123456789012:certificate/00000000-0000-4000-8000-000000000000
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/security-groups: sg-0123456789abcdef0
alb.ingress.kubernetes.io/manage-backend-security-group-rules: "true"
```
### 서비스 및 로드 밸런서 모범 사례
1. **내부 서비스에는 ClusterIP 사용**: 클러스터 내부에서만 액세스하는 서비스에는 ClusterIP 유형을 사용합니다.
2. **외부 서비스에는 LoadBalancer 또는 Ingress 사용**: 외부에서 액세스해야 하는 서비스에는 LoadBalancer 유형 또는 Ingress 리소스를 사용합니다.
3. **ALB 사용**: 경로 기반 라우팅, SSL 종료, 인증 등의 기능이 필요한 경우 ALB를 사용합니다.
4. **NLB 사용**: TCP/UDP 트래픽, 고성능, 정적 IP가 필요한 경우 NLB를 사용합니다.
5. **내부 로드 밸런서 사용**: 허용된 연결 VPC·온프레미스 등 프라이빗 경로의 클라이언트에는 내부 로드 밸런서를 사용합니다. 클러스터 내부 통신에는 보통 ClusterIP로 충분합니다.
6. **교차 영역 로드 밸런싱 활성화**: 대상 용량·영역 장애 테스트·전송 비용에 따라 교차 영역 동작을 선택합니다. 활성화만으로 고가용성이 보장되지 않습니다.
7. **적절한 대상 유형 선택**: 포드 IP를 직접 대상으로 사용하려면 `ip` 대상 유형을, 노드 IP를 대상으로 사용하려면 `instance` 대상 유형을 선택합니다.
## 네트워크 정책
NetworkPolicy는 적용 엔진이 활성화된 경우 선택한 Pod와 방향의 L3/L4 통신을 제어합니다. 지원되는 EC2/Linux 환경의 Amazon VPC CNI는 네이티브 네트워크 정책을 지원하므로 다른 CNI 설치가 필수는 아닙니다. 정확한 add-on 버전, 커널·컴퓨팅 제약, standard/strict 모드와 관리되는 Pod 요건은 [AWS 안내](https://docs.aws.amazon.com/eks/latest/userguide/cni-network-policy.html)를 확인하세요.
### 정책 구현 선택
EKS 관리형 VPC CNI add-on은 `describe-addon-configuration` 확인 후 아래 조각을 기존 구성에 병합하여 소유 관리 도구로 적용합니다. 다른 설정을 보존하세요. 정책 객체 존재 여부만 보지 말고 허용·거부 TCP 테스트로 실제 적용을 확인합니다.
```json
{"enableNetworkPolicy":"true"}
```
Calico는 대안 정책 엔진입니다. VPC CNI와 함께 사용할 때는 `cni.type: AmazonVPC`, Pod IP 주석 권한과 버전 호환성을 포함한 [공식 EKS 정책 전용 절차](https://docs.tigera.io/calico/latest/getting-started/kubernetes/managed-public-cloud/eks)를 따릅니다. VPC CNI 네이티브 정책 엔진을 동시에 활성화하지 마세요. 기존 VPC CNI 클러스터에 VXLAN 매니페스트를 적용하는 것은 정책 전용 설치가 아닙니다. 네트워크 교체에는 별도 이전 설계가 필요합니다.
### 기본 네트워크 정책
해당 방향을 선택하는 NetworkPolicy가 없으면 Pod는 그 방향에 대해 비격리 상태이지만 경로·보안 그룹 등 다른 제어는 적용됩니다. Ingress와 Egress 격리는 별개이며 일치하는 정책의 허용 규칙은 합집합입니다. 양쪽이 격리된 연결은 소스 egress와 대상 ingress 모두 허용해야 하며 응답 트래픽은 암묵적으로 허용됩니다. 아래 정책은 대안 예제이며 순서대로 누적 적용하여 더 제한하는 구성이 아닙니다. 네임스페이스 전체 ingress 허용을 함께 적용하면 뒤의 frontend 전용 정책이 의도한 제한이 완화됩니다.
### 네임스페이스 격리 정책
`my-namespace`의 모든 Pod를 선택하여 **ingress만** 격리하고 같은 네임스페이스에서 모든 포트의 연결을 허용합니다. Egress는 제한하지 않습니다. 네임스페이스와 워크로드 레이블을 먼저 준비하세요.
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: namespace-isolation
namespace: my-namespace
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- podSelector: {}
```
### 특정 포드 간 통신 허용 정책
특정 레이블을 가진 포드 간 통신만 허용하는 정책:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: my-namespace
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 80
```
### 외부 트래픽 제한 정책
소스 CIDR에 대한 ingress 허용 예제로 다른 정책의 허용과 합쳐집니다. 정책 적용 지점에서 보이는 소스 IP를 판단하므로 NAT·NodePort·로드 밸런서가 주소를 바꿀 수 있습니다. 프론트엔드 로드 밸런서 SG나 WAF 규칙을 대체하지 않습니다.
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-external-traffic
namespace: my-namespace
spec:
podSelector:
matchLabels:
app: web
policyTypes:
- Ingress
ingress:
- from:
- ipBlock:
cidr: 192.168.1.0/24
except:
- 192.168.1.10/32
ports:
- protocol: TCP
port: 80
```
### 이그레스(Egress) 트래픽 제한 정책
특정 대상으로만 이그레스 트래픽을 허용하는 정책: `203.0.113.0/24`는 문서용 주소이므로 승인된 실제 외부 목적지로 바꿉니다. DNS 허용은 일반 CoreDNS Pod를 전제로 하며 NodeLocal DNS 등에서는 실제 경로에 맞춰야 합니다. `0.0.0.0/0`에서 RFC1918만 제외해도 특정 외부 서비스만 허용하거나 인스턴스 메타데이터를 확실히 차단하는 것은 아닙니다.
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: limit-egress-traffic
namespace: my-namespace
spec:
podSelector:
matchLabels:
app: web
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: db
ports:
- protocol: TCP
port: 5432
- to:
- ipBlock:
cidr: 203.0.113.0/24
ports:
- protocol: TCP
port: 443
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
```
### 네트워크 정책 모범 사례

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-03-eks-networking-part2-5.html)
1. **기본 거부 정책 적용**: 모든 트래픽을 기본적으로 거부하고 필요한 트래픽만 명시적으로 허용합니다.
2. **네임스페이스 격리**: 네임스페이스 간 통신을 제한하여 보안을 강화합니다.
3. **최소 권한 원칙 적용**: 필요한 최소한의 통신만 허용합니다.
4. **이그레스 트래픽 제한**: 포드에서 나가는 트래픽도 제한하여 보안을 강화합니다.
5. **정책 테스트**: 네트워크 정책을 적용하기 전에 테스트하여 의도하지 않은 통신 차단을 방지합니다.
---
## Gateway API
### 개요
Gateway API는 인프라 소유권(GatewayClass/Gateway)과 애플리케이션 경로를 분리합니다. LBC는 **ALB와 NLB Gateway를 각각** 사용하며 하나의 Gateway에 L4와 L7 경로를 혼합하지 않습니다. ALB는 HTTPRoute/GRPCRoute, NLB는 TCPRoute/UDPRoute/TLSRoute를 컨트롤러가 지원하는 기능 범위에서 처리합니다.
### 사전 요구 사항
예제는 LBC **3.5.0**이 명시한 Gateway API **1.6.0**을 기준으로 합니다. 이전 L4 지원은 2.13.3, L7 지원은 2.14.0부터이므로 “2.13+에서 전부 지원”은 틀립니다. 3.5.0은 CRD를 감지하고 `NLBGatewayAPI`/`ALBGatewayAPI`를 기본 활성화합니다. `EnableGatewayAPI`라는 gate는 없습니다. TCPRoute와 UDPRoute는 이제 standard 채널의 v1 리소스이므로 이전 experimental CRD를 무조건 설치하지 마세요.
```bash
set -euo pipefail
curl --fail --show-error --location \
https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.0/standard-install.yaml \
--output gateway-standard-v1.6.0.yaml
curl --fail --show-error --location \
https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v3.5.0/config/crd/gateway/gateway-crds.yaml \
--output lbc-gateway-crds-v3.5.0.yaml
```
클러스터 범위 갱신을 적용하기 전에 다운로드 내용과 기존 CRD 소유권·저장 버전을 검토합니다. 기존 설치는 릴리스 이전 절차를 따르세요. CRD 없이 시작한 컨트롤러는 CRD 준비 후 재시작 또는 재조정하여 활성 상태를 확인합니다.
```bash
kubectl apply --server-side -f gateway-standard-v1.6.0.yaml
kubectl apply --server-side -f lbc-gateway-crds-v3.5.0.yaml
kubectl get crd gateways.gateway.networking.k8s.io \
tcproutes.gateway.networking.k8s.io udproutes.gateway.networking.k8s.io \
loadbalancerconfigurations.gateway.k8s.aws
```
### GatewayClass 및 Gateway 설정
`gateway-demo` 전용 네임스페이스, 아래에서 참조하는 Service와 준비된 워크로드를 먼저 만듭니다. ACM ARN과 소스 CIDR을 교체하고 ALB와 같은 리전에서 사용 가능한 인증서를 준비하세요. 구성 예제이며 프로덕션 실행 검증 결과는 아닙니다. 기본 TargetGroupConfiguration 참조로 ClusterIP 백엔드에 IP 대상을 사용합니다. 그렇지 않으면 컨트롤러 기본값인 instance 대상으로 NodePort가 필요할 수 있습니다. 이 LBC 전용 HTTPS 패턴은 LoadBalancerConfiguration으로 ACM을 지정하며 지원하지 않는 `tls.certificateRefs`를 생략합니다.
```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: amazon-alb
spec:
controllerName: gateway.k8s.aws/alb
---
apiVersion: gateway.k8s.aws/v1
kind: TargetGroupConfiguration
metadata:
name: ip-targets
namespace: gateway-demo
spec:
defaultConfiguration:
targetType: ip
---
apiVersion: gateway.k8s.aws/v1
kind: LoadBalancerConfiguration
metadata:
name: alb-config
namespace: gateway-demo
spec:
scheme: internal
sourceRanges:
- 10.0.0.0/16
defaultTargetGroupConfiguration:
name: ip-targets
listenerConfigurations:
- protocolPort: HTTPS:443
defaultCertificate: arn:aws:acm:us-west-2:123456789012:certificate/00000000-0000-4000-8000-000000000000
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-hotel-gateway
namespace: gateway-demo
spec:
gatewayClassName: amazon-alb
infrastructure:
parametersRef:
group: gateway.k8s.aws
kind: LoadBalancerConfiguration
name: alb-config
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: app.example.com
allowedRoutes:
namespaces:
from: Same
```
### HTTPRoute 예제 (L7 → ALB)
90/10 가중치는 `/api`에 일치하는 요청에 적용되며 정확한 요청 수 비율이나 상태 기반 장애 전환을 보장하지 않습니다. 호스트명이 일치하는 HTTPS 리스너에 연결합니다. 백엔드 포트는 Service 포트이며 다른 네임스페이스의 경로·백엔드는 allowedRoutes/ReferenceGrant 제어가 필요합니다.
```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-route
namespace: gateway-demo
spec:
parentRefs:
- name: my-hotel-gateway
sectionName: https
hostnames:
- app.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: api-service
port: 80
weight: 90
- name: api-service-v2
port: 80
weight: 10
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: frontend-service
port: 80
```
### TCPRoute 예제 (L4 → NLB)
별도의 내부 NLB Gateway가 `ip-targets`를 재사용합니다. `postgres-service`는 `gateway-demo`에 존재하고 실제 targetPort에 준비된 라우팅 가능 대상이 있어야 합니다. TCP 리스너는 바이트를 전달하며 데이터베이스 인증이나 TLS를 대신 설정하지 않습니다.
```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: amazon-nlb
spec:
controllerName: gateway.k8s.aws/nlb
---
apiVersion: gateway.k8s.aws/v1
kind: LoadBalancerConfiguration
metadata:
name: nlb-config
namespace: gateway-demo
spec:
scheme: internal
sourceRanges:
- 10.0.0.0/16
defaultTargetGroupConfiguration:
name: ip-targets
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-nlb-gateway
namespace: gateway-demo
spec:
gatewayClassName: amazon-nlb
infrastructure:
parametersRef:
group: gateway.k8s.aws
kind: LoadBalancerConfiguration
name: nlb-config
listeners:
- name: tcp
protocol: TCP
port: 5432
allowedRoutes:
namespaces:
from: Same
---
apiVersion: gateway.networking.k8s.io/v1
kind: TCPRoute
metadata:
name: db-route
namespace: gateway-demo
spec:
parentRefs:
- name: my-nlb-gateway
sectionName: tcp
rules:
- backendRefs:
- name: postgres-service
port: 5432
```
### QUIC/HTTP3 지원
ALB HTTPS 리스너가 자동으로 HTTP/3가 되지는 않습니다. LBC 3.5.0의 QUIC 지원은 `listenerConfigurations[].quicEnabled`를 사용하는 **NLB UDP/TCP_UDP 리스너**용입니다. IP 대상과 보안 그룹이 연결되지 않은 NLB가 필요하며 백엔드가 직접 QUIC/HTTP3를 종료해야 합니다. 다음은 UDP:443 리스너·UDPRoute가 있는 별도 NLB Gateway에 연결할 구성 요소이지 ALB 설정이나 완성된 배포가 아닙니다. SG 없는 설계 선택 전 대상 측 보안과 헬스체크를 계획하세요.
```yaml
apiVersion: gateway.k8s.aws/v1
kind: LoadBalancerConfiguration
metadata:
name: quic-config
namespace: gateway-demo
spec:
scheme: internal
disableSecurityGroup: true
defaultTargetGroupConfiguration:
name: ip-targets
listenerConfigurations:
- protocolPort: UDP:443
quicEnabled: true
```
### 인증서 디스커버리
정적 인증서는 `LoadBalancerConfiguration.spec.listenerConfigurations[].defaultCertificate`에 지정하며 추가 ARN은 `certificates`를 사용합니다. 또는 보안 리스너가 있을 때 리스너와 연결된 경로의 호스트명으로 일치하는 ACM 인증서를 찾습니다. HTTPRoute만으로 HTTPS 리스너가 추가되거나 인증서가 발급되지는 않습니다. 이 LBC 릴리스는 Kubernetes Secret을 가리키는 Gateway `certificateRefs`를 지원하지 않으므로 Secret 생성만으로 ACM에 가져오지 않습니다.
### 보안 그룹
기본적으로 LBC는 프론트엔드/백엔드 SG 경로를 관리합니다. 사용자 지정 프론트엔드 SG는 `gateway.k8s.aws/security-group-ids`가 아니라 LoadBalancerConfiguration으로 지정합니다. 아래 필드를 기존 `alb-config`의 인증서·scheme·대상 설정을 보존하면서 병합하고 프론트엔드 규칙은 별도로 구성합니다. 명시한 프론트엔드 SG 위에 `sourceRanges`가 추가 필터로 적용되는 것은 아닙니다. 백엔드 규칙 소유권을 확인하고 필요한 대상·헬스체크 포트만 허용하세요.
```yaml
apiVersion: gateway.k8s.aws/v1
kind: LoadBalancerConfiguration
metadata:
name: alb-config
namespace: gateway-demo
spec:
securityGroups:
- sg-0123456789abcdef0
manageBackendSecurityGroupRules: true
```
### Out-of-Band 대상 그룹
LBC 확장은 Kubernetes TargetGroupBinding이 아니라 `group: ""`, `kind: TargetGroupName`과 **기존 AWS 대상 그룹 이름**을 사용합니다. 등록·수명 주기·프로토콜·VPC·로드 밸런서 연결 호환성은 외부 소유자가 관리합니다. 아래는 앞의 루트 경로에 대한 대안이므로 같은 리스너에 충돌하는 루트 경로를 중복 생성하지 마세요.
```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: oob-route
namespace: gateway-demo
spec:
parentRefs:
- name: my-hotel-gateway
sectionName: https
hostnames:
- app.example.com
rules:
- backendRefs:
- group: ""
kind: TargetGroupName
name: existing-target-group
weight: 1
```
### Gateway API vs Ingress 비교
| 기능 | LBC Ingress | LBC 3.5.0 Gateway API |
|---|---|---|
| 라우팅 | 호스트/경로와 컨트롤러 어노테이션 | HTTPRoute/GRPCRoute 조건과 지원 확장 |
| L4 | 별도 NLB Service 사용 | TCPRoute/UDPRoute/TLSRoute를 쓰는 별도 NLB Gateway |
| 트래픽 분할 | 참조된 weighted-forward action | Route 백엔드 가중치 |
| 소유권 | IngressClass와 Ingress | GatewayClass, Gateway, Route 역할 |
| TLS 인증서 | ACM 어노테이션/디스커버리 | ACM LoadBalancerConfiguration/디스커버리; Secret certificateRefs 미지원 |
| 이식성 | 컨트롤러별 어노테이션 | 컨트롤러 적합성 확인; 모든 표준 필터를 구현하지 않음 |
공식 참고: [LBC Gateway API](https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/guide/gateway/gateway/), [LoadBalancerConfiguration](https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/guide/gateway/loadbalancerconfig/), [Kubernetes Service](https://kubernetes.io/docs/concepts/services-networking/service/), [NetworkPolicy](https://kubernetes.io/docs/concepts/services-networking/network-policies/).
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/03-eks-networking-part2-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/03-eks-networking-part3
----------------------------------------
# Part 3: 문제 해결
> **예제 검증 버전**: EKS Kubernetes 1.36, Amazon VPC CNI 1.23.0
> **마지막 업데이트**: 2026년 9월 11일
## 개요
이 문서에서는 Amazon EKS 네트워킹의 성능 최적화, 문제 해결 방법, 그리고 고급 사용 사례에 대해 알아보겠습니다. 네트워크 성능을 최적화하는 방법, 일반적인 네트워킹 문제를 해결하는 방법, 그리고 고급 네트워킹 기능을 활용하는 방법을 다룹니다.
## 네트워크 성능 최적화
EKS 클러스터의 네트워크 성능을 최적화하기 위한 여러 전략이 있습니다.
### 인스턴스 유형 선택
C5/M5/R5는 ENA 지원 제품군의 예시이지 이전 세대를 권장하는 목록은 아닙니다. 실제 인스턴스의 기본·버스트 대역폭, 초당 패킷 수, 연결 추적, ENA 큐, 단일 흐름 제한과 CPU 부하를 비교합니다. 크기 증가가 보편적인 지연 개선은 아니며 100 Gbps가 모든 ENA의 상한도 아닙니다.
[공식 M5 사양](https://docs.aws.amazon.com/ec2/latest/instancetypes/gp.html)은 **m5.large: 기본 0.75 Gbps / 최대 10 Gbps 버스트**, **m5.24xlarge: 25 Gbps**입니다. 애플리케이션 실측 처리량이 아니라 인스턴스 제한이며 지속 부하·목적지·단일 흐름 제한이 병목일 수 있습니다. 제품군 이름으로 추정하지 말고 대상 리전에서 확인합니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the instance Region}"
aws ec2 describe-instance-types --region "$AWS_REGION" \
--instance-types m5.large m5.24xlarge \
--query 'InstanceTypes[].{Type:InstanceType,Network:NetworkInfo.NetworkPerformance,Cards:NetworkInfo.NetworkCards,ENIs:NetworkInfo.MaximumNetworkInterfaces,IPsPerENI:NetworkInfo.Ipv4AddressesPerInterface}'
```
### 클러스터 네트워킹 모드
EKS는 여러 네트워킹 모드를 지원하며, 각 모드는 성능 특성이 다릅니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-03-eks-networking-part3-1.html)
1. **Amazon VPC CNI(일반 EC2 노드)**:
* 포드에 VPC IP 주소를 직접 할당합니다.
* VPC 주소를 사용하며 실제 처리량·지연은 인스턴스와 경로 제한에 달려 있습니다. Prefix delegation은 주로 IP 할당·Pod 밀도에 영향을 주며 패킷 경로 지연을 자동 개선하지 않습니다.
* 각 노드는 할당할 수 있는 IP 주소 수에 제한이 있습니다.
2. **사용자 정의 네트워킹**:
* 포드에 특정 서브넷의 IP 주소를 할당할 수 있습니다.
* 같은 VPC/AZ의 ENIConfig 서브넷을 통해 적절히 라우팅된 보조 VPC CIDR을 사용할 수 있습니다. 기존 서브넷 자체의 크기를 늘리는 것은 아닙니다.
* 네트워크 토폴로지를 더 세밀하게 제어할 수 있습니다.
3. **대체 CNI 플러그인**:
* Calico, Cilium 등의 대체 CNI 플러그인을 사용할 수 있습니다.
* 기능·성능 차이는 정책 전용·체이닝·오버레이 모드, 암호화와 부하에 따라 달라집니다. 지원 컴퓨팅의 VPC CNI도 NetworkPolicy를 제공합니다. Auto Mode와 Hybrid Nodes는 별도 네트워킹·운영 모델이므로 일반 EC2 절차로 CNI를 교체하지 않습니다.
### MTU 최적화
실제 VPC CNI 환경 변수는 `ENI_MTU`가 아닌 **`AWS_VPC_ENI_MTU`**입니다. 1.23.0 기본값은 9001이며 `POD_MTU`는 Pod 가상 인터페이스를 제어하고 미설정 시 ENI MTU에서 값을 가져옵니다. 구성 변경 전 init/main 컨테이너를 모두 확인합니다:
```bash
kubectl -n kube-system get daemonset aws-node -o json > aws-node-current.json
python3 - <<'PY'
import json
with open("aws-node-current.json") as stream:
spec = json.load(stream)["spec"]["template"]["spec"]
for field in ("initContainers", "containers"):
for container in spec.get(field, []):
settings = {e["name"]: e.get("value", "")
for e in container.get("env", [])
if e["name"] in {"AWS_VPC_ENI_MTU", "POD_MTU",
"DISABLE_TCP_EARLY_DEMUX", "POD_SECURITY_GROUP_ENFORCING_MODE"}}
print(field, container["name"], settings)
PY
```
경로 테스트로 1500이 적합함을 확인했다면 아래 **Helm values 조각**을 기존 소유자의 구성에 병합합니다. EKS add-on은 정확한 구성 스키마를 확인하고 기존 값을 보존하며 Helm 소유 DaemonSet을 임의 패치하지 않습니다. ENI·Pod 인터페이스 변경에는 계획된 노드·Pod 교체가 필요할 수 있으므로 새 인터페이스와 기존 워크로드를 각각 확인합니다.
```yaml
env:
AWS_VPC_ENI_MTU: "1500"
POD_MTU: "1500"
```
점보 프레임은 패킷 오버헤드를 줄일 수 있지만 **실제 전체 경로**가 크기를 수용해야 합니다. SG와 서브넷 자체는 MTU를 설정하는 장치가 아닙니다. 인터넷 게이트웨이·VPN 경로는 흔히 1500으로 제한되며 게이트웨이·피어링·터널·로드 밸런서별 제한도 확인합니다. 경로 MTU 탐색을 위한 ICMP fragmentation needed/IPv6 Packet Too Big을 허용하세요. 작은 ping 성공이 큰 애플리케이션 패킷의 성공을 증명하지는 않습니다. CNI 허용 범위는 IPv4 576–9001, IPv6 1280–9001이며 유효한 값이 종단 경로 적합성을 보장하지는 않습니다.
### TCP 최적화
**TCP early demux:** 일반적인 처리량 향상 스위치가 아닙니다. 공식 안내의 Pod SG **strict** 모드에서는 비활성화하여 kubelet TCP 프로브가 branch ENI Pod에 접근하게 합니다. 설정 대상은 `aws-node` main 컨테이너가 아니라 `aws-vpc-cni-init`입니다. Standard 모드에는 이 우회가 필요하지 않습니다. 모드와 실패 경로를 확인한 뒤 적용할 Helm 조각은 다음과 같습니다:
```yaml
init:
env:
DISABLE_TCP_EARLY_DEMUX: "true"
```
**Keepalive:** TCP keepalive는 애플리케이션이 소켓에서 활성화한 경우 유휴·단절된 장기 연결을 탐지합니다. HTTP 연결 풀과 별개이며 짧은 연결을 빠르게 만들지 않습니다. 먼저 영향받은 호스트·네트워크 네임스페이스의 값을 읽습니다. 관리자 노트북에서 실행한 `sysctl`은 EKS 노드가 아니라 노트북 값을 보여줍니다:
```bash
sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl \
net.ipv4.tcp_keepalive_probes net.ipv4.tcp_rmem net.ipv4.tcp_wmem \
net.core.rmem_max net.core.wmem_max
```
기존 60/15/6은 보편적인 프로덕션 기본값이 아닌 **측정하지 않은 튜닝 예시**입니다. 호환 Linux 커널과 hostNetwork가 아닌 워크로드에서 이 sysctl은 Kubernetes 1.29부터 safe 집합에 포함됩니다. 기존 Pod securityContext의 다른 설정을 보존하며 sysctl만 병합합니다:
```yaml
spec:
template:
spec:
securityContext:
sysctls:
- name: net.ipv4.tcp_keepalive_time
value: "60"
- name: net.ipv4.tcp_keepalive_intvl
value: "15"
- name: net.ipv4.tcp_keepalive_probes
value: "6"
```
**버퍼:** 바이트 단위 대역폭·지연 곱 `초당 비트 대역폭 × RTT 초 / 8`을 기준으로 실험합니다. TCP 자동 튜닝, 병렬 흐름, 소켓별 재정의와 총 메모리 압력도 중요합니다. 이전 16,777,216바이트(16 MiB) 상한 및 `4096 87380 16777216` / `4096 65536 16777216`은 실측 최적값이 아닌 예시입니다. `tcp_rmem`/`tcp_wmem`은 커널 4.15+에서 Kubernetes 1.32부터 safe Pod sysctl이지만 `net.core.*`도 같은 승인·격리 지원을 가진다고 가정하지 않습니다. 노드 수준 변경은 소유자의 관리 구성으로 적용하고 전후 오류·지연·처리량·메모리를 비교합니다.
### 노드 배치 및 지역성
아래는 같은 Deployment의 대안 배치 예제입니다. 같은 네임스페이스에 `app=cache` Pod를 먼저 준비합니다. 선호 조건은 배치를 강제하거나 기존 Pod를 옮기지 않습니다. Python 서버는 교육용이며 재현성이 필요하면 승인된 image digest를 고정하세요. 지역성과 복제본 분산·노드/AZ 장애 내성을 함께 판단하며 같은 노드 배치는 장애 영역을 공유합니다.
노드 배치 및 지역성을 최적화하여 네트워크 성능을 향상시킬 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-03-eks-networking-part3-2.html)
1. **가용 영역 지역성**:
* 통신이 빈번한 포드를 같은 가용 영역에 배치하여 지연 시간을 줄입니다.
* 포드 어피니티 및 안티-어피니티를 사용하여 포드 배치를 제어합니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: http
image: python:3.13-alpine
command:
- python
- -u
- -c
args:
- |
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write((os.environ["APP_NAME"] + "\n").encode())
HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
env:
- name: APP_NAME
value: web
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /health
port: http
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cache
topologyKey: topology.kubernetes.io/zone
```
2. **노드 지역성**:
* 통신이 빈번한 포드를 같은 노드에 배치하여 네트워크 홉을 줄입니다.
* 이는 지연 시간에 민감한 애플리케이션에 특히 유용합니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: http
image: python:3.13-alpine
command:
- python
- -u
- -c
args:
- |
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write((os.environ["APP_NAME"] + "\n").encode())
HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
env:
- name: APP_NAME
value: web
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /health
port: http
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cache
topologyKey: kubernetes.io/hostname
```
3. **Service 트래픽 선호**:
현재 예제는 호환 서비스 프록시에서 `trafficDistribution: PreferSameZone`을 사용합니다. 같은 영역의 준비된 엔드포인트를 선호하고 없으면 다른 영역으로 대체하며 격리·영역 간 비용을 보장하지 않습니다. 지역 용량도 확보하세요. 방식을 바꿀 때는 더 높은 우선순위의 기존 `service.kubernetes.io/topology-mode: Auto` 어노테이션을 검토하여 제거합니다. `internalTrafficPolicy: Local`/`externalTrafficPolicy: Local`은 각각의 트래픽에 더 엄격한 노드 지역성을 적용하며 우선합니다. 로컬 엔드포인트가 없으면 트래픽이 중단될 수 있습니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
type: ClusterIP
trafficDistribution: PreferSameZone
```
### 네트워크 정책 최적화
Kubernetes NetworkPolicy 허용은 합집합이며 규칙·정책 이름 순서로 “먼저 일치한 규칙 우선”을 정하지 않습니다. 자주 사용하는 규칙을 앞에 놓는 것은 이식 가능한 최적화가 아닙니다. Calico tier/order 등은 별도 API입니다. 필요한 ingress/egress 격리를 보존하면서 확인된 중복·폐기 규칙만 소유 관리 도구로 정리합니다. 대표 부하에서 정책 반영 시간, 규칙·맵 사용량, CPU와 패킷 손실을 측정해야 하며 정책 개수만으로 병목을 확정할 수 없습니다.
## 네트워킹 문제 해결
EKS 클러스터에서 발생할 수 있는 일반적인 네트워킹 문제와 해결 방법을 알아보겠습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-03-eks-networking-part3-3.html)
### 포드 네트워킹 문제
실제 Pod 이벤트부터 확인합니다. `ContainerCreating`은 CNI/IPAM뿐 아니라 이미지·볼륨·런타임 문제일 수 있으므로 IP 고갈의 증거가 아닙니다. 영향 노드, 서브넷 여유 주소, ENI/IP 제한, prefix 단편화, API 오류·제한과 CNI 로그를 함께 확인합니다:
```bash
set -euo pipefail
: "${APP_NAMESPACE:?Set the affected namespace}"
: "${APP_POD:?Set the affected Pod}"
: "${APP_SERVICE:?Set the affected Service}"
kubectl -n "$APP_NAMESPACE" describe pod "$APP_POD"
kubectl -n "$APP_NAMESPACE" get events --field-selector "involvedObject.name=$APP_POD" --sort-by=.metadata.creationTimestamp
kubectl -n "$APP_NAMESPACE" get service "$APP_SERVICE" -o yaml
kubectl -n "$APP_NAMESPACE" get endpointslices \
-l "kubernetes.io/service-name=$APP_SERVICE" -o yaml
kubectl -n "$APP_NAMESPACE" get networkpolicy
kubectl -n kube-system logs -l k8s-app=aws-node -c aws-node --tail=200 --prefix=true
```
**IP 할당:** `WARM_IP_TARGET`을 늘리면 여유 주소를 더 예약하므로 서브넷 고갈을 악화시킬 수 있습니다. 관찰한 할당·시작 지연 요구와 여유 용량에 맞춰 조정하세요. 여유 IP 총합이 많아도 연속된 /28이 없으면 prefix를 할당할 수 없습니다. 노드 크기 변경도 서브넷을 확장하지 않습니다. 병목을 찾은 뒤 서브넷·prefix 예약, 지원 Pod 밀도 또는 단계적 네트워크 이전을 계획합니다.
**연결:** 실제 TCP/UDP 프로토콜로 같은 노드·다른 노드·다른 영역·Pod IP·Service 경로를 비교합니다. DNS 실패·도구 부재·ICMP 차단이 NetworkPolicy 거부의 증거는 아닙니다. 아래는 표시된 도구를 가진 기존 승인 진단 Pod를 전제로 하며 클러스터 DNS 접미사와 대상 URL을 조정합니다. 이를 위해 프로덕션 Pod에 특권 도구를 설치하지 마세요:
```bash
set -euo pipefail
: "${APP_NAMESPACE:?Set the affected namespace}"
: "${DIAGNOSTIC_POD:?Set a running diagnostic Pod with curl and DNS tools}"
: "${TARGET_URL:?Set the real application URL and port}"
kubectl -n "$APP_NAMESPACE" exec "$DIAGNOSTIC_POD" -- cat /etc/resolv.conf
kubectl -n "$APP_NAMESPACE" exec "$DIAGNOSTIC_POD" -- nslookup kubernetes.default.svc.cluster.local
kubectl -n "$APP_NAMESPACE" exec "$DIAGNOSTIC_POD" -- curl \
--fail --show-error --max-time 10 "$TARGET_URL"
```
**DNS:** dnsPolicy/dnsConfig, resolv.conf, DNS Service/EndpointSlice, CoreDNS 이벤트·로그와 업스트림 연결을 확인합니다. `nslookup`과 `dig`는 대안 도구이지 애플리케이션 이미지에 반드시 포함되지는 않습니다. Auto Mode 네이티브 DNS는 관리 경로가 다르므로 기존 CoreDNS Deployment가 없다고 바로 장애는 아닙니다. 재시작을 고려하기 전에 관리형 add-on 구성과 증거를 보존하세요.
### 서비스 및 로드 밸런싱 문제

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-03-eks-networking-part3-5.html)
**Service 경로:** 네임스페이스, 선택자 레이블, 실제 리스닝 포트, Service port/targetPort, readiness와 EndpointSlice 주소·조건을 확인합니다. 대규모 백엔드가 잘릴 수 있는 기존 Endpoints API 대신 EndpointSlice를 사용합니다. 이후 실제 서비스 프록시·CNI 모드와 트래픽 정책·지역성 선호를 확인합니다.
**로드 밸런서/Ingress:** 먼저 소유 컨트롤러와 클래스를 식별합니다. 일반 LBC는 Ingress 이벤트, 컨트롤러 로그와 AWS 대상 상태 사유를 확인하고 Auto Mode는 관리형 컨트롤러의 별도 진단 경로를 사용합니다. scheme·클라이언트 경로, 서브넷 선택·구성, 프론트엔드·대상 SG, 실제 대상 유형, 헬스체크 포트·경로, 인증서·SNI·DNS를 확인하세요. 서브넷 태그가 경로를 만들지 않으며 컨트롤러 Running이 대상의 정상 상태를 증명하지는 않습니다.
```bash
set -euo pipefail
: "${APP_NAMESPACE:?Set the affected namespace}"
: "${INGRESS_NAME:?Set the affected Ingress}"
: "${AWS_REGION:?Set the load balancer Region}"
: "${TARGET_GROUP_ARN:?Set the target group identified from this Ingress}"
kubectl -n "$APP_NAMESPACE" describe ingress "$INGRESS_NAME"
kubectl -n kube-system logs -l app.kubernetes.io/name=aws-load-balancer-controller --tail=200 --prefix=true
aws elbv2 describe-target-health --region "$AWS_REGION" --target-group-arn "$TARGET_GROUP_ARN"
```
확인된 원인을 하나씩 변경하고 되돌릴 경로를 보존하며 동일한 허용·거부 애플리케이션 테스트를 반복합니다. 위 명령은 진단 예제이며 이 감사에서는 EKS 클러스터를 대상으로 실행하지 않았습니다.
공식 참고: [EC2 대역폭](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-network-bandwidth.html), [MTU](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/network_mtu.html), [VPC CNI 1.23.0 설정](https://github.com/aws/amazon-vpc-cni-k8s/blob/v1.23.0/README.md), [Kubernetes sysctl](https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/), [Service 트래픽 분산](https://kubernetes.io/docs/reference/networking/virtual-ips/).
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/03-eks-networking-part3-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/04-eks-storage-part1
----------------------------------------
# EKS 스토리지
> **마지막 업데이트**: 2026년 9월 12일
Amazon EKS에서 애플리케이션을 실행할 때 데이터를 저장하고 관리하기 위한 다양한 스토리지 옵션이 있습니다. 이 문서에서는 EKS 스토리지의 기본 개념과 Amazon EBS(Elastic Block Store) 및 Amazon EFS(Elastic File System)를 사용하는 방법에 대해 알아보겠습니다.
## 목차
1. [Kubernetes 스토리지 기본 개념](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part1.md#kubernetes-스토리지-기본-개념)
2. [Amazon EKS 스토리지 옵션 개요](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part1.md#amazon-eks-스토리지-옵션-개요)
3. [Amazon EBS를 사용한 스토리지](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part1.md#amazon-ebs를-사용한-스토리지)
4. [Amazon EFS를 사용한 스토리지](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part1.md#amazon-efs를-사용한-스토리지)
5. [스토리지 클래스 및 동적 프로비저닝](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part1.md#스토리지-클래스-및-동적-프로비저닝)
## Kubernetes 스토리지 기본 개념
Kubernetes에서 스토리지를 관리하기 위한 핵심 개념들을 먼저 이해해 보겠습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-04-eks-storage-part1-0.html)
### 볼륨(Volume)
볼륨은 컨테이너에 파일 시스템 마운트 또는 지원되는 raw block 장치로 스토리지를 제공합니다. 수명은 유형에 따라 다릅니다. `emptyDir`는 컨테이너 재시작에는 유지되지만 Pod와 함께 제거되고, PVC 기반 영구 볼륨은 별도로 관리됩니다. Pod 삭제가 모든 백엔드 데이터 삭제를 의미하지는 않습니다.
### 영구 볼륨(PersistentVolume, PV)
영구 볼륨은 관리자가 프로비저닝하거나 스토리지 클래스를 통해 동적으로 프로비저닝된 클러스터의 스토리지 조각입니다. PV 객체는 Pod와 별개이지만 PVC 소유권과 reclaim policy에 따라 보존 여부가 달라집니다. 예를 들어 generic ephemeral volume의 PVC는 소유 Pod와 함께 가비지 컬렉션될 수 있습니다.
### 영구 볼륨 클레임(PersistentVolumeClaim, PVC)
영구 볼륨 클레임은 사용자의 스토리지 요청입니다. PVC는 특정 크기와 액세스 모드를 가진 스토리지를 요청하며, 이 요청은 적절한 PV에 바인딩됩니다.
PVC는 네임스페이스 리소스이며 보통 PV 하나와 바인딩됩니다. 접근 모드·백엔드가 허용하면 같은 네임스페이스의 여러 Pod가 클레임을 사용할 수 있습니다. 바인딩 자체가 파일 권한이나 용량·성능을 보장하지는 않습니다.
### 스토리지 클래스(StorageClass)
스토리지 클래스는 관리자가 제공하는 스토리지의 "클래스"를 설명합니다. 스토리지 클래스를 사용하면 PVC가 생성될 때 동적으로 PV를 프로비저닝할 수 있습니다.
### 액세스 모드
Kubernetes는 다음과 같은 액세스 모드를 지원합니다:
* **ReadWriteOnce(RWO)**: 단일 노드에서 읽기/쓰기로 마운트 가능
* **ReadOnlyMany(ROX)**: 여러 노드에서 읽기 전용으로 마운트 가능
* **ReadWriteMany(RWX)**: 여러 노드에서 읽기/쓰기로 마운트 가능
* **ReadWriteOncePod(RWOP)**: 호환 CSI 구성에서 클러스터 전체의 Pod 하나로 읽기/쓰기 사용 제한; 1.22 도입, 1.29부터 stable
**RWO는 Pod 하나가 아니라 노드 하나**를 뜻하므로 그 노드의 여러 Pod가 PVC를 공유할 수 있습니다. RWOP는 별도 제약이며 호환 CSI sidecar가 필요합니다. 다른 접근 모드는 주로 매칭·마운트 기능에 관여하므로 필요한 read-only 플래그, 파일 권한과 서비스 권한도 설정해야 합니다. RWX가 동시 쓰기의 애플리케이션 일관성을 보장하지는 않습니다.
## Amazon EKS 스토리지 옵션 개요
Amazon EKS에서는 다양한 AWS 스토리지 서비스를 활용하여 컨테이너화된 애플리케이션에 스토리지를 제공할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-04-eks-storage-part1-1.html)
### 주요 스토리지 옵션
1. **Amazon EBS(Elastic Block Store)**
* AZ 범위의 네트워크 블록 스토리지; 일반 gp3 파일 시스템 볼륨은 노드 하나에 연결(RWO 또는 호환 RWOP)
* 고성능, 내구성 있는 블록 스토리지
* 데이터베이스, 상태 유지 애플리케이션에 적합
2. **Amazon EFS(Elastic File System)**
* 완전 관리형 NFS 파일 시스템
* 여러 노드에서 동시에 마운트 가능(RWX)
* 공유 파일 시스템이 필요한 워크로드에 적합
3. **Amazon FSx for Lustre**
* 고성능 파일 시스템
* 기계 학습, HPC, 빅 데이터 분석에 적합
* 여러 노드에서 동시에 마운트 가능(RWX)
4. **Amazon S3(Simple Storage Service)**
* 객체 스토리지
* S3 API 또는 공식 Mountpoint for Amazon S3 CSI 드라이버로 접근(기존 버킷, 제한된 POSIX 인터페이스); 별도 공유 파일 시스템인 S3 Files는 EFS CSI 3.0+ 사용
* 대용량 데이터 저장에 적합
5. **EC2 Instance Store(로컬 NVMe)**
* EC2 인스턴스에 물리적으로 직접 연결된 임시(ephemeral) 로컬 NVMe 스토리지, 매우 낮은 지연시간
* EC2 Instance Store CSI 드라이버는 2026년 5월 5일 EKS add-on으로 출시됨. 로컬 NVMe를 Kubernetes PV로 관리하지만 PV 객체가 노드 소실·종료 시 데이터 내구성을 보장하지는 않음. 설치 전 인스턴스·OS·add-on 호환성 확인 필요
* AI/ML 임시 데이터 처리, Spark/Hadoop 로컬 캐시, 고속 로그 처리, DB 캐시 계층에 적합
* 비용: 호환 EC2 인스턴스와 관련 AWS 리소스 비용을 계획하며 로컬 스토리지는 선택한 인스턴스에 종속됨 ([출처](https://aws.amazon.com/about-aws/whats-new/2026/05/ec2-csi-eks/))
### 스토리지 옵션 비교
성능은 용량·처리량/IOPS 모드·클라이언트/네트워크 제한·부하에 따라 달라집니다. 아래는 실측 순위가 아닌 기능 비교입니다.
| 옵션 | 인터페이스 | 대표 용도 | 핵심 제약 |
|---|---|---|---|
| EBS | 블록/파일 시스템 | DB, 복제본별 상태 | 일반 볼륨은 한 AZ에 위치; 연결·일관성 규칙 적용 |
| EFS | 공유 NFS 파일 시스템 | 공유 파일 | Regional/One Zone, 처리량, POSIX 신원과 mount target 경로 구분 |
| FSx for Lustre | 병렬 파일 시스템 | HPC/ML 데이터셋 | 클라이언트·커널, 배포 유형, 용량과 프로비저닝 처리량 |
| S3 + Mountpoint CSI | 객체/파일 인터페이스 | 대규모 객체 데이터 | 기존 버킷 static provisioning; 모든 POSIX 연산을 지원하지 않음 |
| S3 Files + EFS CSI | S3 기반 공유 파일 시스템 | S3 데이터의 파일 접근 | 별도 서비스/IAM 구성; EFS CSI3.0+와 컴퓨팅 제약 |
| EC2 Instance Store CSI | 로컬 블록/파일 시스템 | 재생성 가능한 캐시·scratch | 노드·로컬 매체 수명에 데이터 종속 |
서로 다른 의미와 컨트롤러·노드 IAM 요건은 [Mountpoint CSI](https://docs.aws.amazon.com/eks/latest/userguide/s3-csi.html), [S3 Files](https://docs.aws.amazon.com/eks/latest/userguide/s3files-csi.html)를 참고하세요. 어느 쪽도 트랜잭션 DB 파일 시스템의 자동 대체재는 아닙니다.
## Amazon EBS를 사용한 스토리지
Amazon EBS는 EC2 인스턴스에 연결할 수 있는 블록 수준 스토리지 볼륨을 제공합니다. EKS에서는 EBS CSI(Container Storage Interface) 드라이버를 통해 EBS 볼륨을 Kubernetes 파드에 마운트할 수 있습니다.
### EBS CSI 드라이버 설치
일반 Linux EC2 노드는 인프라 소유 관리 도구로 호환 EBS CSI add-on을 설치합니다. Auto Mode는 `ebs.csi.eks.amazonaws.com`으로 블록 스토리지를 관리하며 기존 `ebs.csi.aws.com` 볼륨은 다른 provisioner를 사용합니다. 이전은 바인딩된 PVC나 driver의 in-place 수정이 아닙니다. 검증한 backup/snapshot 복원 계획 또는 현재 [AWS 이전 가이드의 workload 중지·Retain·static PV/PVC 재생성 절차](https://docs.aws.amazon.com/eks/latest/userguide/migrate-auto.html)로 기존 EBS volume을 재사용할 수 있습니다. 쓰기 재개 전에 backup 복구, volume/AZ/KMS 소유권, IAM/tag 권한, reclaim policy, finalizer와 새 binding을 검증하세요. Fargate Pod와 Hybrid Node에는 EBS를 마운트할 수 없습니다. 컨트롤러는 Fargate에 실행할 수 있지만 node plugin은 실행할 수 없으며 별도 배포·신원 설계가 필요합니다.
아래 공통 절차는 EBS 또는 EFS용입니다. 이 절에서는 `CSI_ADDON_NAME=aws-ebs-csi-driver`로 설정하고 목록에서 정확한 호환 add-on 버전을 선택합니다. AWS API는 버전에 문자열 `latest`를 사용하지 않습니다. `eksctl --version latest`는 AWS API 값이 아닌 별도 도구 편의 기능입니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the existing cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
: "${CSI_ADDON_NAME:?Use aws-ebs-csi-driver or aws-efs-csi-driver}"
case "$CSI_ADDON_NAME" in
aws-ebs-csi-driver|aws-efs-csi-driver) ;;
*) echo "Unexpected add-on"; exit 1 ;;
esac
KUBERNETES_VERSION=$(aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--query cluster.version --output text)
CLUSTER_ENDPOINT=$(aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--query cluster.endpoint --output text)
CURRENT_ENDPOINT=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
test "$CURRENT_ENDPOINT" = "$CLUSTER_ENDPOINT" || { echo "kubeconfig points to another cluster"; exit 1; }
aws eks describe-addon-versions --region "$AWS_REGION" --addon-name "$CSI_ADDON_NAME" \
--kubernetes-version "$KUBERNETES_VERSION" --output json > csi-addon-versions.json
aws eks list-addons --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" --output json
```
설치 전에 정확한 `kube-system/ebs-csi-controller-sa`의 Pod Identity 역할·신뢰와 지원 컴퓨팅의 Pod Identity agent를 준비합니다. `AmazonEBSCSIDriverPolicyV2` 또는 제한한 정책, 필요한 고객 키 KMS 권한을 검토합니다. IRSA는 범위가 맞는 OIDC 신뢰와 `--service-account-role-arn`을 사용하며 신원 옵션을 무조건 혼합하지 않습니다. 기존 설치가 있으면 아래 코드는 중단합니다. 덮어쓰지 말고 해당 소유자의 갱신·인계 절차를 사용하세요.
```bash
set -euo pipefail
: "${CSI_ADDON_VERSION:?Choose a reviewed compatible version from csi-addon-versions.json}"
: "${CSI_ROLE_ARN:?Set the prepared Pod Identity role ARN}"
: "${CLUSTER_NAME:?Run the inspection step first}"
: "${AWS_REGION:?Run the inspection step first}"
: "${CSI_ADDON_NAME:?Run the inspection step first}"
case "$CSI_ADDON_NAME" in
aws-ebs-csi-driver) CSI_SA=ebs-csi-controller-sa; CSI_PREFIX=ebs-csi ;;
aws-efs-csi-driver) CSI_SA=efs-csi-controller-sa; CSI_PREFIX=efs-csi ;;
*) echo "Unexpected add-on"; exit 1 ;;
esac
python3 - "$CSI_ADDON_NAME" "$CSI_ADDON_VERSION" <<'PY'
import json, sys
with open("csi-addon-versions.json") as stream:
catalog = json.load(stream)
versions = [v["addonVersion"] for a in catalog["addons"]
if a["addonName"] == sys.argv[1] for v in a["addonVersions"]]
if sys.argv[2] not in versions:
raise SystemExit("Version not present in the inspected compatible catalog")
PY
aws eks list-addons --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--output json > csi-existing-addons.json
python3 - "$CSI_ADDON_NAME" <<'PY'
import json, sys
with open("csi-existing-addons.json") as stream:
names = json.load(stream)["addons"]
if sys.argv[1] in names:
raise SystemExit("Existing add-on: use its owner's update procedure")
PY
EXISTING_CSI=$(kubectl -n kube-system get "deployment/$CSI_PREFIX-controller" \
"daemonset/$CSI_PREFIX-node" --ignore-not-found -o name)
test -z "$EXISTING_CSI" || { echo "Existing CSI installation: review its owner"; exit 1; }
aws eks describe-addon-configuration --region "$AWS_REGION" \
--addon-name "$CSI_ADDON_NAME" --addon-version "$CSI_ADDON_VERSION"
aws eks create-addon --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--addon-name "$CSI_ADDON_NAME" --addon-version "$CSI_ADDON_VERSION" \
--pod-identity-associations "serviceAccount=$CSI_SA,roleArn=$CSI_ROLE_ARN" \
--resolve-conflicts NONE
aws eks wait addon-active --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--addon-name "$CSI_ADDON_NAME"
```
Add-on Active가 애플리케이션의 연결·마운트·쓰기·복원 성공을 증명하지는 않습니다. 전용 환경에서 각각 검증하세요. 이 장의 AWS 명령은 실행 시 리소스를 생성하며 감사에서는 로컬 검증만 수행했습니다.
### EBS 스토리지 클래스 생성
EBS 볼륨을 동적으로 프로비저닝하기 위한 스토리지 클래스를 생성합니다. 여기서는 gp3 볼륨 타입을 사용합니다.
이 장의 네임스페이스 리소스는 같은 `storage-demo`에서 사용합니다. 실습 전용 새 네임스페이스를 생성하고 이미 존재하면 소유권 확인 전 중단하세요. StorageClass와 snapshot class는 클러스터 범위이므로 별도 소유권 검토도 필요합니다. 아래 구성은 프로덕션 용량 산정값이 아닌 예제입니다.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: storage-demo
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36
```
위 네임스페이스 매니페스트를 **먼저** `storage-demo-namespace.yaml`로 저장한 뒤 create 명령을 실행합니다. 데이터 보존을 의도적으로 선택하세요. 예제는 `Retain`을 사용하므로 PVC 삭제 후 과금 AWS 리소스가 남을 수 있습니다.
```bash
kubectl create -f storage-demo-namespace.yaml
```
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-gp3
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
parameters:
type: gp3
encrypted: 'true'
csi.storage.k8s.io/fstype: ext4
```
### 영구 볼륨 클레임(PVC) 생성
애플리케이션에서 사용할 PVC를 생성합니다.
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-claim
namespace: storage-demo
spec:
accessModes:
- ReadWriteOnce
storageClassName: ebs-gp3
resources:
requests:
storage: 10Gi
```
### 파드에서 PVC 사용
생성한 PVC를 파드에 마운트하여 사용합니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-ebs
namespace: storage-demo
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.37.0
command:
- sh
- -c
args:
- test -w /data && touch /data/demo-marker && sync && sleep 3600
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: ebs-claim
```
### EBS 볼륨 스냅샷
이 리소스를 사용하기 전에 snapshot CRD, 호환 snapshot controller와 드라이버 snapshotter 구성 요소를 준비합니다. 관리 소유자 또는 검토한 고정 릴리스를 사용하고 floating `master` 매니페스트를 적용하지 마세요. Class의 driver는 볼륨 provisioner와 일치해야 합니다. 스냅샷은 블록 시점 복사이므로 일관성에 따라 애플리케이션 quiesce/flush 또는 지원 백업 절차가 필요합니다.
```yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: ebs-snapshot-retain
driver: ebs.csi.aws.com
deletionPolicy: Retain
```
```yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: ebs-snapshot
namespace: storage-demo
labels:
storage-demo: ebs
spec:
volumeSnapshotClassName: ebs-snapshot-retain
source:
persistentVolumeClaimName: ebs-claim
```
```bash
set -euo pipefail
kubectl -n storage-demo wait --for=jsonpath='{.status.readyToUse}'=true \
volumesnapshot/ebs-snapshot --timeout=300s
kubectl -n storage-demo get volumesnapshot ebs-snapshot -o yaml
```
복원은 스냅샷 네임스페이스에서 `status.restoreSize` 이상 크기의 새 PVC를 만들고 소비 Pod로 데이터를 검증합니다. 아래20Gi는 스냅샷이 그 이하라는 전제입니다. `WaitForFirstConsumer`이면 스케줄링 전 복원 PVC가 Pending인 것은 정상일 수 있습니다. Snapshot `deletionPolicy`는 PV `reclaimPolicy`와 별개이며 Retain은 백엔드 스냅샷을 남겨 별도 정리가 필요합니다.
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-restored
namespace: storage-demo
spec:
accessModes:
- ReadWriteOnce
storageClassName: ebs-gp3
resources:
requests:
storage: 20Gi
dataSource:
name: ebs-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
```
### EBS 볼륨 확장
StorageClass가 확장을 허용하고 드라이버·파일 시스템이 지원해야 합니다. 이10Gi 예제에서는 소유 관리 도구로 PVC 요청만20Gi로 늘립니다. 축소하거나 PV capacity를 수동 변경하여 확장을 흉내 내지 마세요. PVC 조건·용량과 마운트된 파일 시스템을 확인하며 `FileSystemResizePending`이면 문서화된 재마운트·재시작 경로가 필요할 수 있습니다.
```bash
set -euo pipefail
kubectl -n storage-demo get pvc ebs-claim -o yaml
kubectl -n storage-demo patch pvc ebs-claim --type merge \
-p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
kubectl -n storage-demo describe pvc ebs-claim
```
### EBS 볼륨 유형 및 성능
Amazon EBS는 다양한 볼륨 유형을 제공합니다:
| 볼륨 유형 | 설명 | 사용 사례 |
| ----- | --------------- | --------------------- |
| gp3 | 범용 SSD | 대부분의 워크로드에 적합, 비용 효율적 |
| io2 | 프로비저닝된 IOPS SSD | 고성능 데이터베이스 |
| st1 | 처리량 최적화 HDD | 빅 데이터, 로그 처리 |
| sc1 | 콜드 HDD | 자주 액세스하지 않는 데이터 |
gp3는 일반적인 시작점이지만 워크로드와 인스턴스 EBS 제한에 맞춰 용량·IOPS·처리량을 선택합니다. 일반 gp3 파일 시스템 볼륨은 다중 노드 공유 파일 시스템이 아닙니다. EBS CSI1.66.0은 `ReadWriteMany`용 io2 **raw block** Multi-Attach 경로를 지원하지만 호환 노드와 애플리케이션의 동시 접근 조정·fencing이 필요합니다. ext4/XFS를 여러 노드에서 독립적으로 동시 마운트해도 안전해지는 것은 아닙니다. RWOP와 RWO를 구분하고 CSI sidecar 호환성을 확인하세요.
## Amazon EFS를 사용한 스토리지
Amazon EFS는 완전 관리형 NFS 파일 시스템으로, 여러 EC2 인스턴스에서 동시에 액세스할 수 있습니다. EKS에서는 EFS CSI 드라이버를 통해 EFS 파일 시스템을 여러 파드에 동시에 마운트할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-04-eks-storage-part1-3.html)
### EFS CSI 드라이버 설치
지원 Linux EC2 컴퓨팅에서는 앞의 공통 add-on 절차에 `CSI_ADDON_NAME=aws-efs-csi-driver`, 호환 EFS 버전과 `kube-system/efs-csi-controller-sa`용 역할을 사용합니다. `AmazonEFSCSIDriverPolicy` 또는 제한한 동등 정책을 검토하세요. Fargate는 관리형 통합으로 EFS를 마운트하며 동적이 아닌 static provisioning을 지원합니다. 관리형 EFS CSI 지원 범위에서는 Windows/Hybrid Nodes가 제외됩니다. S3 Files는 EFS CSI3.0+를 사용하지만 컨트롤러 **및 노드** IAM 요건이 별도이고 Fargate를 지원하지 않습니다.
### EFS 파일 시스템 생성
`efs-ap` 동적 프로비저닝은 **기존** 파일 시스템에 access point를 만듭니다. 파일 시스템과 mount target은 네트워크·스토리지 소유자가 생성합니다. 아래 선택적 CLI 예제는 새 암호화 Regional 파일 시스템과 **서로 다른 AZ 두 개**에 mount target을 하나씩 생성합니다. 실제 클라이언트 AZ의 서브넷을 선택하고 추가 AZ는 모든 서브넷이 아닌 AZ당 target 하나로 확장합니다. 이미 IaC가 관리하는 파일 시스템은 기존 소유자의 절차를 사용하세요.
운영자 권한, 정확한 리전·계정, 클라이언트 SG와 DNS/NFS 경로를 먼저 준비합니다. 모호한 Name 태그 검색 대신 반환된 ID를 저장하고 쓰기 전에 VPC/AZ를 검증하며 부분 실패 기록을 남깁니다. 고유하고 안정적인 creation token은 해당 요청 소유입니다. 기존 token 응답이 다른 파일 시스템을 인계·변경할 권한은 아닙니다. 생성을 무조건 반복하지 말고 부분 생성 리소스를 검토·조정하세요.
```bash
set -euo pipefail
umask 077
: "${CLUSTER_NAME:?Set the cluster name}"
: "${AWS_REGION:?Set the Region}"
: "${EFS_CREATION_TOKEN:?Set a unique, stable token for this new filesystem}"
: "${EFS_SUBNET_A:?Set the first approved subnet}"
: "${EFS_SUBNET_B:?Set a subnet in a different AZ}"
: "${NFS_CLIENT_SG_ID:?Set the SG of the actual NFS clients}"
EFS_SETUP_DIR=$(mktemp -d -t eks-efs-setup.XXXXXX)
printf 'Creation records: %s\n' "$EFS_SETUP_DIR"
VPC_ID=$(aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--query cluster.resourcesVpcConfig.vpcId --output text)
aws ec2 describe-subnets --region "$AWS_REGION" \
--subnet-ids "$EFS_SUBNET_A" "$EFS_SUBNET_B" --output json > "$EFS_SETUP_DIR/subnets.json"
aws ec2 describe-security-groups --region "$AWS_REGION" \
--group-ids "$NFS_CLIENT_SG_ID" --output json > "$EFS_SETUP_DIR/client-sg.json"
python3 - "$EFS_SETUP_DIR" "$VPC_ID" "$EFS_CREATION_TOKEN" <<'PY'
import json, pathlib, sys
root, vpc, token = pathlib.Path(sys.argv[1]), sys.argv[2], sys.argv[3]
subnets = json.loads((root / "subnets.json").read_text())["Subnets"]
groups = json.loads((root / "client-sg.json").read_text())["SecurityGroups"]
if not 1 <= len(token) <= 64 or not token.isascii():
raise SystemExit("Creation token must contain 1–64 ASCII characters")
if len(subnets) != 2 or len({s["SubnetId"] for s in subnets}) != 2:
raise SystemExit("Exactly two distinct subnets are required")
if any(s["VpcId"] != vpc for s in subnets) or len({s["AvailabilityZoneId"] for s in subnets}) != 2:
raise SystemExit("Subnets must be in the cluster VPC and different AZs")
if len(groups) != 1 or groups[0]["VpcId"] != vpc:
raise SystemExit("The NFS client SG must belong to the cluster VPC")
PY
aws efs create-file-system --region "$AWS_REGION" --creation-token "$EFS_CREATION_TOKEN" \
--performance-mode generalPurpose --throughput-mode elastic --encrypted \
--tags Key=Name,Value=eks-storage-demo --output json > "$EFS_SETUP_DIR/filesystem-created.json"
EFS_FS_ID=$(python3 - "$EFS_SETUP_DIR/filesystem-created.json" <<'PY'
import json, sys
with open(sys.argv[1]) as stream:
print(json.load(stream)["FileSystemId"])
PY
)
test -n "$EFS_FS_ID"
FS_READY=false
for ((attempt=0; attempt<60; attempt++)); do
STATE=$(aws efs describe-file-systems --region "$AWS_REGION" --file-system-id "$EFS_FS_ID" \
--query 'FileSystems[0].LifeCycleState' --output text)
case "$STATE" in
available) FS_READY=true; break ;;
creating) sleep 5 ;;
*) echo "Unexpected filesystem state: $STATE"; exit 1 ;;
esac
done
test "$FS_READY" = true || { echo "Filesystem readiness timed out"; exit 1; }
aws ec2 create-security-group --region "$AWS_REGION" --group-name "efs-nfs-$EFS_FS_ID" \
--description "NFS clients for $EFS_FS_ID" --vpc-id "$VPC_ID" \
--output json > "$EFS_SETUP_DIR/sg-created.json"
EFS_SG_ID=$(python3 - "$EFS_SETUP_DIR/sg-created.json" <<'PY'
import json, sys
with open(sys.argv[1]) as stream:
print(json.load(stream)["GroupId"])
PY
)
test -n "$EFS_SG_ID"
aws ec2 authorize-security-group-ingress --region "$AWS_REGION" --group-id "$EFS_SG_ID" \
--protocol tcp --port 2049 --source-group "$NFS_CLIENT_SG_ID"
for SUBNET_ID in "$EFS_SUBNET_A" "$EFS_SUBNET_B"; do
aws efs create-mount-target --region "$AWS_REGION" --file-system-id "$EFS_FS_ID" \
--subnet-id "$SUBNET_ID" --security-groups "$EFS_SG_ID" --output json \
> "$EFS_SETUP_DIR/mount-target-$SUBNET_ID.json"
done
TARGETS_READY=false
for ((attempt=0; attempt<60; attempt++)); do
aws efs describe-mount-targets --region "$AWS_REGION" --file-system-id "$EFS_FS_ID" \
--output json > "$EFS_SETUP_DIR/mount-targets.json"
STATE=$(python3 - "$EFS_SETUP_DIR/mount-targets.json" <<'PY'
import json, sys
with open(sys.argv[1]) as stream:
targets = json.load(stream)["MountTargets"]
states = [t["LifeCycleState"] for t in targets]
if any(s not in ("creating", "available") for s in states):
raise SystemExit("Unexpected mount-target state")
print("available" if len(states) == 2 and all(s == "available" for s in states) else "creating")
PY
)
if test "$STATE" = available; then TARGETS_READY=true; break; fi
sleep 5
done
test "$TARGETS_READY" = true || { echo "Mount-target readiness timed out"; exit 1; }
printf 'Filesystem: %s\nMount-target SG: %s\nCreation records: %s\n' "$EFS_FS_ID" "$EFS_SG_ID" "$EFS_SETUP_DIR"
```
예제는 준비 상태 확인에 횟수를 제한한 Describe polling을 사용합니다. CSI 컨트롤러 역할은 일반적인 파일 시스템 생성 역할이 아닙니다. NFS ingress에는 실제 클라이언트 SG를 사용하며 해당 설계에서 노드·Pod 중 어느 ENI가 마운트 트래픽을 보내는지 확인하세요.
### EFS 스토리지 클래스 생성
EFS를 사용하기 위한 스토리지 클래스를 생성합니다. 파일 시스템 ID를 저장한 값으로 바꿉니다. 예제는 access point에서 UID/GID1000을 강제하고 고유 디렉토리를 유지하므로 신뢰 경계에 맞는 신원을 선택하세요. TLS를 사용합니다. `iam` 마운트 옵션은 애플리케이션 ServiceAccount가 아닌 **CSI node Pod의 신원**을 사용합니다. 컨트롤러 프로비저닝 권한과 클라이언트 마운트 권한은 별개입니다.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: efs-sc
provisioner: efs.csi.aws.com
reclaimPolicy: Retain
mountOptions:
- tls
parameters:
provisioningMode: efs-ap
fileSystemId: fs-0123456789abcdef0
directoryPerms: '750'
uid: '1000'
gid: '1000'
basePath: /storage-demo
ensureUniqueDirectory: 'true'
```
### 영구 볼륨 클레임(PVC) 생성
EFS를 사용하기 위한 PVC를 생성합니다. `5Gi` 요청은 Kubernetes 바인딩 메타데이터이지 EFS 디렉토리 quota나 할당 용량이 아닙니다. 증가하는 사용량에 대한 처리량·IOPS, access point quota와 비용 계획은 여전히 필요합니다.
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: efs-claim
namespace: storage-demo
spec:
accessModes:
- ReadWriteMany
storageClassName: efs-sc
resources:
requests:
storage: 5Gi
```
### 파드에서 EFS PVC 사용
생성한 PVC를 파드에 마운트하여 사용합니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-efs
namespace: storage-demo
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.37.0
command:
- sh
- -c
args:
- test -w /shared-data && touch /shared-data/demo-marker && sync && sleep 3600
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: data
mountPath: /shared-data
volumes:
- name: data
persistentVolumeClaim:
claimName: efs-claim
```
### EFS 액세스 포인트
Access point는 제시하는 루트 디렉토리와 강제 POSIX 신원을 설정합니다. 이것만으로 네임스페이스 격리가 완성되지는 않으므로 파일 시스템 IAM 정책과 네트워크 제어로 의도한 access point·TLS·클라이언트 권한을 강제합니다. 상호 신뢰하지 않는 테넌트가 공유하는 class에 `reuseAccessPoint`를 켜지 마세요. 검토한 드라이버의 재사용 token은 네임스페이스가 아닌 PVC 이름으로 정해져 서로 다른 클레임이 같은 데이터에 접근할 수 있습니다.
다음 static 예제는 기본·동적 프로비저닝을 사용하지 않도록 `storageClassName: ""`를 지정하고 기존 access point에 PVC를 명시적으로 바인딩합니다. 앞의 동적 클레임에 대한 대안입니다. Access point의 루트 디렉토리·POSIX 권한이 이미 적합해야 하며 소비자는 `storage-demo`의 `efs-static-claim`을 참조해야 합니다:
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: efs-static-pv
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ''
mountOptions:
- tls
csi:
driver: efs.csi.aws.com
volumeHandle: fs-0123456789abcdef0::fsap-0123456789abcdef0
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: efs-static-claim
namespace: storage-demo
spec:
accessModes:
- ReadWriteMany
storageClassName: ''
volumeName: efs-static-pv
resources:
requests:
storage: 5Gi
```
### EFS 성능 모드 및 처리량 모드
- **General Purpose**는 AWS가 모든 파일 시스템에 권장하는 성능 모드입니다. **Max I/O**는 연산별 지연이 높은 이전 세대 모드이며 Elastic 처리량 또는 One Zone과 함께 사용할 수 없습니다.
- **Elastic**은 수요에 맞춰 처리량을 조정하고, **Provisioned**는 선택한 처리량을 프로비저닝하며, **Bursting**은 저장 데이터·크레딧에 영향을 받습니다. 명시적으로 선택하며 콘솔/API의 기본값을 하나로 일반화하지 마세요.
- Regional과 One Zone의 장애·가용성 특성은 다릅니다. 현재 One Zone도 Elastic 처리량을 지원하지만 단일 AZ의 내구성 고려 사항은 남습니다.
- 실제 접근 패턴과 클라이언트 제한을 측정하세요. 이 설명은 예제의 지연·처리량 벤치마크가 아닙니다.
## 스토리지 클래스 및 동적 프로비저닝
Kubernetes의 스토리지 클래스를 사용하면 영구 볼륨을 동적으로 프로비저닝할 수 있습니다. EKS에서는 다양한 AWS 스토리지 서비스에 대한 스토리지 클래스를 구성할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-04-eks-storage-part1-4.html)
### 볼륨 바인딩 모드
스토리지 클래스의 `volumeBindingMode` 필드는 PVC가 생성될 때 PV가 바인딩되는 방식을 결정합니다:
* **Immediate**: PVC가 생성되는 즉시 PV를 프로비저닝하고 바인딩합니다.
* **WaitForFirstConsumer**: 파드가 PVC를 사용하려고 할 때까지 PV 프로비저닝을 지연합니다.
일반적인 AZ 범위 EBS 볼륨에는 `WaitForFirstConsumer`를 사용하여 스케줄러 제약이 프로비저닝·바인딩에 반영되게 합니다. EBS는 물리적인 instance-store 매체가 아닌 네트워크 연결 블록 스토리지이며, 드라이버의 별도 사전 연결 node-local 캐시 모드는 다른 기능입니다. 지연 바인딩 PVC가 Pending일 때 `spec.nodeName`으로 스케줄러를 우회하지 말고 nodeSelector 같은 스케줄링 제약을 사용하세요.
### 기본 스토리지 클래스 설정
PVC가 storageClassName을 생략하면 기본 class를 사용하며 명시적인 `storageClassName: ""`는 제외됩니다. 기존 기본값을 소유 관리 도구로 검토하세요. 전환 중 기본 class가 여러 개이면 가장 최근 생성된 기본값을 선택하지만 이후 의도한 기본값 하나를 유지합니다. 기존 바인딩 볼륨이 이전되지는 않습니다. 아래 같은 이름의 class 예제는 순차적인 파라미터 변경이 아닌 대안입니다.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-gp3
annotations:
storageclass.kubernetes.io/is-default-class: 'true'
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
parameters:
type: gp3
encrypted: 'true'
csi.storage.k8s.io/fstype: ext4
```
### 스토리지 클래스 예제
**1. EBS gp3 스토리지 클래스**
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-gp3
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
parameters:
type: gp3
encrypted: 'true'
csi.storage.k8s.io/fstype: ext4
iops: '3000'
throughput: '125'
```
**2. EFS 스토리지 클래스**
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: efs-sc
provisioner: efs.csi.aws.com
reclaimPolicy: Retain
mountOptions:
- tls
parameters:
provisioningMode: efs-ap
fileSystemId: fs-0123456789abcdef0
directoryPerms: '750'
uid: '1000'
gid: '1000'
basePath: /storage-demo
ensureUniqueDirectory: 'true'
```
**3. FSx for Lustre 스토리지 클래스**
지원 FSx CSI·컨트롤러 IAM 역할, Lustre 클라이언트·커널과 네트워크 경로를 먼저 준비합니다. 아래 최소 SCRATCH_2 class는 persistent 전용 처리량·백업 설정을 생략합니다. 재생성·복구 가능한 데이터에 사용하며 Retain이 scratch 스토리지를 영구 백업으로 바꾸지는 않습니다. CSI1.10.0의 `s3ImportPath`는 선택한 FSx 배포가 해당 통합을 지원할 때 사용할 수 있는 유효한 선택 파라미터입니다.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fsx-lustre
provisioner: fsx.csi.aws.com
reclaimPolicy: Retain
parameters:
subnetId: subnet-0123456789abcdef0
securityGroupIds: sg-0123456789abcdef0
deploymentType: SCRATCH_2
dataCompressionType: NONE
```
### 리클레임 정책
영구 볼륨의 리클레임 정책은 PVC가 삭제될 때 PV와 해당 데이터를 어떻게 처리할지 결정합니다:
* **Delete**: 클레임 해제·보호 처리 후 provisioner가 드라이버에 따라 백엔드 정리를 시도합니다. EBS는 볼륨을 삭제하지만 EFS 동적 프로비저닝은 기본적으로 파일 시스템·파일이 아닌 access point를 삭제합니다. EFS 컨트롤러의 `deleteAccessPointRootDir` 설정에 따라 동작이 달라집니다.
* **Retain**: PVC가 삭제되어도 PV와 데이터는 유지됩니다. 관리자가 수동으로 정리해야 합니다.
* **Recycle**: 사용되지 않는 정책으로, 대신 동적 프로비저닝과 스토리지 클래스를 사용하는 것이 좋습니다.
StorageClass 필드는 **`reclaimPolicy`**이며 **`persistentVolumeReclaimPolicy`**는 PV spec의 필드입니다. Class는 새 PV의 초기 정책을 정하며 이를 바꿔도 기존 PV 정책이 자동 변경되지는 않습니다. 클레임 삭제 전에 실제 PV와 백업을 확인하세요:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-gp3-retain
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
parameters:
type: gp3
encrypted: 'true'
csi.storage.k8s.io/fstype: ext4
```
공식 참고: [EBS CSI](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html), [EFS CSI](https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html), [snapshot controller](https://docs.aws.amazon.com/eks/latest/userguide/csi-snapshot-controller.html), [EFS 성능](https://docs.aws.amazon.com/efs/latest/ug/performance.html), [Kubernetes PV](https://kubernetes.io/docs/concepts/storage/persistent-volumes/).
## 결론
Amazon EKS에서는 다양한 스토리지 옵션을 활용하여 애플리케이션의 요구 사항에 맞는 스토리지 솔루션을 구성할 수 있습니다. 이 문서에서는 EBS와 EFS를 중심으로 기본 개념과 구성 방법을 살펴보았습니다. 다음 문서에서는 FSx for Lustre와 S3를 활용한 고급 스토리지 구성에 대해 알아보겠습니다.
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/04-eks-storage-part1-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/04-eks-storage-part2
----------------------------------------
# Part 2: 스토리지 클래스
> **마지막 업데이트**: 2026년 9월 11일
이 문서는 Amazon EKS 스토리지 시리즈의 두 번째 부분으로, FSx for Lustre, Amazon S3, 스냅샷, 볼륨 확장 및 성능 최적화에 대해 다룹니다.
## 목차
1. [Amazon FSx for Lustre](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part2.md#amazon-fsx-for-lustre)
2. [Amazon S3 스토리지 통합](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part2.md#amazon-s3-스토리지-통합)
3. [스냅샷 및 백업](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part2.md#스냅샷-및-백업)
4. [볼륨 확장 및 크기 조정](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part2.md#볼륨-확장-및-크기-조정)
5. [볼륨 클로닝](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part2.md#볼륨-클로닝)
6. [다중 연결 EBS (Multi-Attach)](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part2.md#다중-연결-ebs-multi-attach)
7. [Mountpoint for S3 CSI 심화](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part2.md#mountpoint-for-s3-csi-심화)
8. [스토리지 성능 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part2.md#스토리지-성능-최적화)
## Amazon FSx for Lustre
FSx for Lustre는 지원되는 HPC/ML/분석 워크로드용 병렬 파일 시스템입니다. 성능은 배포·스토리지 유형, 용량, 프로비저닝 처리량, 클라이언트와 네트워크에 따라 달라지며 작은 예제가 제품의 모든 집계 최댓값을 제공하지는 않습니다.
그림은 선택적인 S3 데이터 저장소 통합을 나타냅니다. Import/export 정책·작업·권한이 필요하며 CSI 볼륨 생성만으로 자동 양방향 동기화가 구성되지는 않습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-04-eks-storage-part2-0.html)
### FSx for Lustre CSI 드라이버 설치
인프라 소유자가 관리하는 지원 EKS add-on 또는 배포된 Helm 릴리스를 사용합니다. 아래 관리형 예제는 `CSI_ADDON_NAME=aws-fsx-csi-driver`로 설정합니다. `kube-system/fsx-csi-controller-sa`용 검토한 FSx 드라이버 권한과 Pod Identity 신뢰·agent를 준비합니다. IRSA도 OIDC 신뢰와 대응 add-on 역할 옵션으로 지원됩니다. Pod Identity agent는 해당 신원 방식에 필요하며 IRSA의 필수 조건은 아닙니다. Fargate는 지원 FSx CSI 노드 환경이 아니므로 Linux 커널·Lustre 클라이언트·실제 컴퓨팅 지원을 확인하세요.
`eksctl --role-only`는 Kubernetes ServiceAccount를 생성하지 않습니다. 관리형 add-on은 계정을 만들지만 Helm은 실제 계정을 생성·참조하고 신원을 연결해야 합니다. 계정을 준비하지 않은 상태에서 role-only와 `serviceAccount.create=false`를 조합하지 마세요.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the existing cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
: "${CSI_ADDON_NAME:?Use aws-fsx-csi-driver}"
case "$CSI_ADDON_NAME" in
aws-fsx-csi-driver) ;;
*) echo "Unexpected add-on"; exit 1 ;;
esac
KUBERNETES_VERSION=$(aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--query cluster.version --output text)
CLUSTER_ENDPOINT=$(aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--query cluster.endpoint --output text)
CURRENT_ENDPOINT=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
test "$CURRENT_ENDPOINT" = "$CLUSTER_ENDPOINT" || { echo "kubeconfig points to another cluster"; exit 1; }
aws eks describe-addon-versions --region "$AWS_REGION" --addon-name "$CSI_ADDON_NAME" \
--kubernetes-version "$KUBERNETES_VERSION" --output json > csi-addon-versions.json
aws eks list-addons --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" --output json
```
```bash
set -euo pipefail
: "${CSI_ADDON_VERSION:?Choose a reviewed compatible version from csi-addon-versions.json}"
: "${CSI_ROLE_ARN:?Set the prepared Pod Identity role ARN}"
: "${CLUSTER_NAME:?Run the inspection step first}"
: "${AWS_REGION:?Run the inspection step first}"
: "${CSI_ADDON_NAME:?Run the inspection step first}"
case "$CSI_ADDON_NAME" in
aws-fsx-csi-driver) CSI_SA=fsx-csi-controller-sa; CSI_PREFIX=fsx-csi ;;
*) echo "Unexpected add-on"; exit 1 ;;
esac
python3 - "$CSI_ADDON_NAME" "$CSI_ADDON_VERSION" <<'PY'
import json, sys
with open("csi-addon-versions.json") as stream:
catalog = json.load(stream)
versions = [v["addonVersion"] for a in catalog["addons"]
if a["addonName"] == sys.argv[1] for v in a["addonVersions"]]
if sys.argv[2] not in versions:
raise SystemExit("Version not present in the inspected compatible catalog")
PY
aws eks list-addons --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--output json > csi-existing-addons.json
python3 - "$CSI_ADDON_NAME" <<'PY'
import json, sys
with open("csi-existing-addons.json") as stream:
names = json.load(stream)["addons"]
if sys.argv[1] in names:
raise SystemExit("Existing add-on: use its owner's update procedure")
PY
EXISTING_CSI=$(kubectl -n kube-system get "deployment/$CSI_PREFIX-controller" \
"daemonset/$CSI_PREFIX-node" --ignore-not-found -o name)
test -z "$EXISTING_CSI" || { echo "Existing CSI installation: review its owner"; exit 1; }
aws eks describe-addon-configuration --region "$AWS_REGION" \
--addon-name "$CSI_ADDON_NAME" --addon-version "$CSI_ADDON_VERSION"
aws eks create-addon --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--addon-name "$CSI_ADDON_NAME" --addon-version "$CSI_ADDON_VERSION" \
--pod-identity-associations "serviceAccount=$CSI_SA,roleArn=$CSI_ROLE_ARN" \
--resolve-conflicts NONE
aws eks wait addon-active --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--addon-name "$CSI_ADDON_NAME"
```
기존 설치가 있으면 절차가 중단되므로 해당 소유자의 이전·갱신 경로를 사용합니다. Add-on Active가 파일 시스템 마운트를 증명하지는 않습니다. 아래 예제는 Part1의 전용 `storage-demo` 네임스페이스를 재사용하며 클러스터 범위 리소스 이름도 검토해야 합니다.
### FSx for Lustre 파일 시스템 생성
**동적 또는 정적 프로비저닝 중 선택합니다.** 동적 방식은 PVC로 파일 시스템을 만들므로 먼저 수동 생성한 시스템을 자동 인계한다고 가정하지 마세요. 아래 선택적 수동 절차는 정적 경로용입니다. 지원 AZ의 승인된 서브넷과 클라이언트·파일 시스템 통신 규칙을 준비한 Lustre SG를 선택합니다. `Subnets[0]`을 임의 선택하거나 TCP988만으로 모든 구성이 완료된다고 가정하지 않습니다. 예제는 클러스터 VPC로 제한하며 연결 VPC는 별도 경로·보안 검토가 필요합니다. 실행하면 과금 리소스가 생성되며 실패 시 반환 ID를 보존합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the existing cluster}"
: "${AWS_REGION:?Set the filesystem Region}"
: "${FSX_SUBNET_ID:?Select an approved subnet in a supported AZ}"
: "${FSX_SECURITY_GROUP_ID:?Set the reviewed Lustre filesystem SG}"
: "${FSX_CREATION_TOKEN:?Set a unique stable token for this new filesystem}"
FSX_SETUP_DIR=$(mktemp -d -t eks-fsx-setup.XXXXXX)
printf 'Creation records: %s\n' "$FSX_SETUP_DIR"
VPC_ID=$(aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--query cluster.resourcesVpcConfig.vpcId --output text)
aws ec2 describe-subnets --region "$AWS_REGION" --subnet-ids "$FSX_SUBNET_ID" \
--output json > "$FSX_SETUP_DIR/subnet.json"
aws ec2 describe-security-groups --region "$AWS_REGION" --group-ids "$FSX_SECURITY_GROUP_ID" \
--output json > "$FSX_SETUP_DIR/sg.json"
python3 - "$FSX_SETUP_DIR" "$VPC_ID" <<'PY'
import pathlib, json, sys
root, vpc = pathlib.Path(sys.argv[1]), sys.argv[2]
subnets = json.loads((root / "subnet.json").read_text())["Subnets"]
groups = json.loads((root / "sg.json").read_text())["SecurityGroups"]
if len(subnets) != 1 or len(groups) != 1 or subnets[0]["VpcId"] != vpc or groups[0]["VpcId"] != vpc:
raise SystemExit("This example requires one subnet and SG in the cluster VPC")
PY
aws fsx create-file-system --region "$AWS_REGION" --file-system-type LUSTRE \
--client-request-token "$FSX_CREATION_TOKEN" --storage-capacity 1200 --storage-type SSD \
--subnet-ids "$FSX_SUBNET_ID" --security-group-ids "$FSX_SECURITY_GROUP_ID" \
--lustre-configuration DeploymentType=SCRATCH_2,DataCompressionType=NONE \
--tags Key=Name,Value=eks-lustre-demo --output json > "$FSX_SETUP_DIR/created.json"
FSX_FILE_SYSTEM_ID=$(python3 - "$FSX_SETUP_DIR/created.json" <<'PY'
import json, sys
with open(sys.argv[1]) as stream:
print(json.load(stream)["FileSystem"]["FileSystemId"])
PY
)
FSX_READY=false
for ((attempt=0; attempt<60; attempt++)); do
STATE=$(aws fsx describe-file-systems --region "$AWS_REGION" --file-system-ids "$FSX_FILE_SYSTEM_ID" \
--query 'FileSystems[0].Lifecycle' --output text)
case "$STATE" in
AVAILABLE) FSX_READY=true; break ;;
CREATING) sleep 10 ;;
*) echo "Unexpected filesystem state: $STATE"; exit 1 ;;
esac
done
test "$FSX_READY" = true || { echo "Filesystem creation still pending; inspect recorded ID"; exit 1; }
aws fsx describe-file-systems --region "$AWS_REGION" --file-system-ids "$FSX_FILE_SYSTEM_ID" \
--query 'FileSystems[0].{Id:FileSystemId,State:Lifecycle,DNS:DNSName,MountName:LustreConfiguration.MountName,CapacityGiB:StorageCapacity}' \
--output json > "$FSX_SETUP_DIR/available.json"
cat "$FSX_SETUP_DIR/available.json"
```
### FSx for Lustre 스토리지 클래스 생성
**동적** 경로는 class에 실제 서브넷·보안 그룹을 지정합니다. 용량은 무시되는 `storageCapacity` class 파라미터가 아닌 PVC 요청으로 정합니다. SCRATCH_2에는 persistent 전용 per-unit 처리량·백업 설정을 혼합하지 않습니다. 반환된 mount name은 class의 `mountName`이 아니라 static PV의 volumeAttributes에 들어갑니다.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fsx-lustre-sc
provisioner: fsx.csi.aws.com
reclaimPolicy: Retain
parameters:
subnetId: subnet-0123456789abcdef0
securityGroupIds: sg-0123456789abcdef0
deploymentType: SCRATCH_2
dataCompressionType: NONE
```
### PVC 생성 및 파드에 마운트
읽기 전용 소비자는 마운트 접근만 확인하며 GPU 작업·처리량 벤치마크가 아닙니다. 이 점검에 CUDA 이미지는 필요하지 않습니다. UID/GID1000의 파일 권한과 실제 클라이언트 네트워크 경로를 준비합니다.
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: fsx-claim
namespace: storage-demo
spec:
accessModes:
- ReadWriteMany
storageClassName: fsx-lustre-sc
resources:
requests:
storage: 1200Gi
```
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-fsx
namespace: storage-demo
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.37.0
command:
- sh
- -c
args:
- test -r /data && sleep 3600
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: data
mountPath: /data
readOnly: true
volumes:
- name: data
persistentVolumeClaim:
claimName: fsx-claim
```
### 정적 프로비저닝을 사용한 FSx for Lustre 마운트
기존 파일 시스템의 실제 ID·DNSName·MountName·용량을 사용하며 동적 PVC의 대안입니다. 조회한 값으로 아래 자리표시자를 바꾸고 예약된 static PV/PVC를 바인딩합니다. `storageClassName: ""`는 기본 동적 프로비저닝을 막습니다. 소비자는 `fsx-static-claim`을 사용해야 합니다.
```bash
set -euo pipefail
: "${AWS_REGION:?Set the filesystem Region}"
: "${FSX_FILE_SYSTEM_ID:?Set the owned existing filesystem ID}"
aws fsx describe-file-systems --region "$AWS_REGION" --file-system-ids "$FSX_FILE_SYSTEM_ID" \
--query 'FileSystems[0].{Id:FileSystemId,State:Lifecycle,DNS:DNSName,MountName:LustreConfiguration.MountName,CapacityGiB:StorageCapacity}' \
--output json
```
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: fsx-lustre-static
spec:
capacity:
storage: 1200Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ''
claimRef:
namespace: storage-demo
name: fsx-static-claim
csi:
driver: fsx.csi.aws.com
volumeHandle: fs-0123456789abcdef0
volumeAttributes:
dnsname: replace-with-filesystem-dns.example.internal
mountname: replace-with-mountname
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: fsx-static-claim
namespace: storage-demo
spec:
accessModes:
- ReadWriteMany
storageClassName: ''
volumeName: fsx-lustre-static
resources:
requests:
storage: 1200Gi
```
### FSx for Lustre 배포 유형
- **SCRATCH_1/SCRATCH_2**는 임시·재생성 가능한 데이터용입니다. PV로 마운트했다고 persistent 배포의 복제·복구를 제공하지는 않으며 SCRATCH_2는 버스트·성능·암호화 특성이 다릅니다.
- **PERSISTENT_1/PERSISTENT_2**는 스토리지·처리량·지연 기능이 다른 영구 배포 선택지입니다. 모든 리전/AZ 구성이 PERSISTENT_2를 지원하지는 않습니다.
- 처리량 필드를 배포·스토리지 유형과 맞춥니다. PERSISTENT_2 SSD는125/250/500/1000MB/s/TiB 선택지를 지원하며 다른 조합은 다릅니다. Retain은 Kubernetes 수명 정책이지 scratch 내구성 향상 기능이 아닙니다.
### vLLM을 위한 FSx for Lustre 구성
vLLM은 LLM 추론·서빙 프로젝트이며 “Vector Language Model”이 아닙니다. 아래는 모델 파일 저장소의 예시 할당이지 최적화·실측한 vLLM 배포가 아닙니다. 로딩에는 파일 형식·CPU 역직렬화·캐시·GPU 초기화도 관여합니다. 이미 압축된 데이터에는 압축이 오버헤드를 더할 수 있으므로 측정합니다.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fsx-lustre-vllm
provisioner: fsx.csi.aws.com
reclaimPolicy: Retain
parameters:
subnetId: subnet-0123456789abcdef0
securityGroupIds: sg-0123456789abcdef0
deploymentType: PERSISTENT_2
dataCompressionType: LZ4
perUnitStorageThroughput: '1000'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vllm-models
namespace: storage-demo
spec:
accessModes:
- ReadWriteMany
storageClassName: fsx-lustre-vllm
resources:
requests:
storage: 4800Gi
```
## Amazon S3 스토리지 통합
S3는 객체 스토리지입니다. 애플리케이션은 API, Hadoop은 S3A를 사용할 수 있고 Mountpoint CSI는 기존 버킷을 제한이 있는 파일 시스템 인터페이스로 노출합니다. 이 경로들은 서로 대체 가능한 POSIX 파일 시스템이 아닙니다. Part1의 S3 Files는 별도 EFS CSI 요구사항이 있는 또 다른 통합입니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-04-eks-storage-part2-1.html)
### S3 액세스를 위한 Pod Identity 또는 IRSA
Part1의 전용 `storage-demo` 네임스페이스를 재사용합니다. 기존 버킷·리전과 대상 버킷/접두사 범위의 IAM 역할을 준비합니다. 예시 정책은 버킷 하나의 목록 조회와 `training/` 읽기만 허용하며 쓰기는 허용하지 않습니다. SSE-KMS 객체는 적절한 key policy와 범위를 제한한 KMS decrypt 권한도 필요합니다. 버킷 정책·endpoint·교차 계정 신뢰에는 추가 제약이 있을 수 있습니다.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListOneBucket",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::replace-with-owned-data-bucket"
},
{
"Sid": "ReadTrainingPrefix",
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::replace-with-owned-data-bucket/training/*"
}
]
}
```
아래 ServiceAccount는 **IRSA** 예제입니다. 예시 ARN을 해당 클러스터·`system:serviceaccount:storage-demo:s3-access-sa`·STS audience만 허용하는 OIDC 신뢰가 준비된 역할로 교체하세요. Annotation만으로 역할·신뢰가 생성되지는 않습니다. **EKS Pod Identity**를 선택하면 IRSA annotation을 생략하고 지원 agent와 이 애플리케이션 계정의 검토한 association을 준비합니다. 두 방식을 우발적인 fallback으로 함께 구성하지 마세요. 매니페스트에 정적 AWS 키를 넣지 않습니다.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: s3-access-sa
namespace: storage-demo
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/storage-demo-s3-reader
```
### S3 액세스를 위한 파드 구성
아래 제한 시간·읽기 전용 목록 조회 Job은 공식 AWS CLI 이미지와 준비한 계정을 사용합니다. 실행 전에 버킷·리전을 교체하세요. 전용 쓰기 가능 디렉터리는 비특권 CLI 프로세스를 지원합니다. 종료 상태만으로 애플리케이션이 모든 대상 객체를 읽을 수 있다고 입증되지는 않으므로 Job·로그와 대표적인 허용 객체를 별도로 확인합니다.
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: s3-read-check
namespace: storage-demo
spec:
backoffLimit: 0
activeDeadlineSeconds: 120
template:
spec:
serviceAccountName: s3-access-sa
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: reader
image: public.ecr.aws/aws-cli/aws-cli:2.36.43
command:
- aws
args:
- s3api
- list-objects-v2
- --bucket
- replace-with-owned-data-bucket
- --prefix
- training/
- --max-items
- '5'
env:
- name: AWS_REGION
value: us-west-2
- name: AWS_EC2_METADATA_DISABLED
value: 'true'
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: private-home
mountPath: /root
- name: tmp
mountPath: /tmp
volumes:
- name: private-home
emptyDir: {}
- name: tmp
emptyDir: {}
```
### Hadoop S3A 액세스
S3A는 Hadoop의 `s3a://` 파일 시스템 구현이며 Kubernetes 볼륨 마운트가 아닙니다. Hadoop3.5.0은 AWS SDK for Java v2를 사용합니다. 같은 버전의 `hadoop-common`·`hadoop-aws`와 호환 shaded SDK bundle을 포함한 이미지를 빌드·검토하세요. 아래 placeholder는 바로 실행할 수 있는 배포 이미지가 아닙니다. 이미지 계약은 PATH의 `hadoop`, `/opt/hadoop/etc/hadoop`와 UID1000 지원을 포함합니다. 다른 optional tool이 필요하면 `hadoop-aws`와 합칩니다. 번들 SDK가 지원하는 경우 v2 default credentials provider로 선택한 워크로드 신원을 사용할 수 있으며 v1 `com.amazonaws` provider를 재사용하지 않습니다.
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: hadoop-s3a-config
namespace: storage-demo
data:
core-site.xml: |
fs.s3a.aws.credentials.providersoftware.amazon.awssdk.auth.credentials.DefaultCredentialsProviderfs.s3a.endpoint.regionus-west-2
---
apiVersion: batch/v1
kind: Job
metadata:
name: hadoop-s3a-read-check
namespace: storage-demo
spec:
backoffLimit: 0
activeDeadlineSeconds: 120
template:
spec:
serviceAccountName: s3-access-sa
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: hadoop
image: registry.example.com/reviewed-hadoop-s3a:3.5.0
command:
- hadoop
args:
- fs
- -ls
- s3a://replace-with-owned-data-bucket/training/
env:
- name: AWS_REGION
value: us-west-2
- name: AWS_EC2_METADATA_DISABLED
value: 'true'
- name: HADOOP_CONF_DIR
value: /opt/hadoop/etc/hadoop
- name: HADOOP_OPTIONAL_TOOLS
value: hadoop-aws
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: tmp
mountPath: /tmp
- name: hadoop-config
mountPath: /opt/hadoop/etc/hadoop/core-site.xml
subPath: core-site.xml
readOnly: true
volumes:
- name: tmp
emptyDir: {}
- name: hadoop-config
configMap:
name: hadoop-s3a-config
```
ConfigMap 키 하나를 subPath로 마운트하면 이미지의 다른 Hadoop 설정 파일을 가리지 않습니다. SubPath는 ConfigMap 변경을 실시간 반영하지 않으므로 설정 변경 후 Job을 다시 생성합니다.
### Mountpoint CSI로 기존 버킷 마운트
검토한 upstream 조합은 CSI2.8.0과 번들 Mountpoint1.23.0입니다. Standalone Mountpoint1.24.0이 CSI 이미지에 자동 반영되지는 않습니다. 실제 EKS add-on 버전과 컴퓨팅 호환성을 확인하세요. 이 upstream CSI 릴리스는 Kubernetes1.31+가 필요하며 AL2·Ubuntu22.04 지원이 제거되었습니다. 기존 인프라 소유자를 따르며 관리형 add-on·기존 드라이버 위에 Helm을 설치하지 않습니다.
별도로 관리하는 Helm 설치는 **배포된 chart**를 먼저 렌더링하여 검토하고 `--kube-version`에 실제 클러스터 버전을 사용합니다. Git checkout의 chart는 지원 배포본이 아니므로 배포 검사를 우회하지 마세요. 선택한 소유자 경로로 배포하기 전에 특권 노드 구성 요소·CRD 소유권·`mount-s3` 네임스페이스를 검토합니다:
```bash
set -euo pipefail
helm repo add aws-mountpoint-s3-csi-driver https://awslabs.github.io/mountpoint-s3-csi-driver
helm repo update aws-mountpoint-s3-csi-driver
helm template aws-mountpoint-s3-csi-driver \
aws-mountpoint-s3-csi-driver/aws-mountpoint-s3-csi-driver \
--version 2.8.0 --namespace kube-system --kube-version 1.36.0 --include-crds \
> s3-driver-review.yaml
```
배포 chart는 기본적으로 `s3-csi-driver-sa`와 `s3-csi-driver-controller-sa`를 생성합니다. 아래 PV는 `s3-access-sa`의 **pod-level credentials**를 명시적으로 선택하며 이 볼륨에서는 driver-level 신원을 무시합니다. IRSA·EKS Pod Identity 모두 지원됩니다. Helm 렌더링 성공만으로 준비된 것은 아니며 실제 드라이버·신원 설치가 필요합니다.
Mountpoint CSI는 기존 버킷을 **정적 프로비저닝**하며 이전 예제의 동적 StorageClass를 사용하지 않습니다. 양쪽 storageClassName을 빈 값으로 유지하고 PV/PVC 사전 바인딩과 클러스터에서 고유한 volumeHandle을 사용합니다. Capacity는 Kubernetes 바인딩 메타데이터이며 S3 용량 생성·제한 기능이 아닙니다. 버킷·접두사·리전을 함께 교체하세요. 읽기 전용 마운트와 IAM 권한은 각각 별도 제어입니다:
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: s3-training-pv
spec:
capacity:
storage: 1Ti
volumeMode: Filesystem
accessModes:
- ReadOnlyMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ''
claimRef:
namespace: storage-demo
name: training-data
mountOptions:
- read-only
- region us-west-2
- prefix training/
- allow-other
- uid 1000
- gid 1000
- dir-mode 0750
- file-mode 0440
csi:
driver: s3.csi.aws.com
volumeHandle: storage-demo-s3-training-v1
volumeAttributes:
bucketName: replace-with-owned-data-bucket
authenticationSource: pod
stsRegion: us-west-2
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: training-data
namespace: storage-demo
spec:
accessModes:
- ReadOnlyMany
storageClassName: ''
volumeName: s3-training-pv
resources:
requests:
storage: 1Ti
```
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-s3
namespace: storage-demo
spec:
serviceAccountName: s3-access-sa
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: reader
image: busybox:1.37.0
command:
- sh
- -c
args:
- ls -la /data && sleep 3600
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: data
mountPath: /data
readOnly: true
volumes:
- name: data
persistentVolumeClaim:
claimName: training-data
readOnly: true
```
Mount option은 파일 시스템 소유권을 UID/GID1000으로 표시하며 S3 객체 소유권을 바꾸지 않습니다. `allow-other`는 애플리케이션 UID가 드라이버가 생성한 마운트에 접근하도록 합니다. 이 마운트 읽기 경로에서 애플리케이션 컨테이너 자체에 AWS CLI·FUSE 특권은 필요하지 않습니다.
### S3 사용 사례
데이터 레이크·모델 저장소·아카이브·정적 자산·감사 객체에 S3를 사용할 수 있습니다. 객체 버전·조건부 요청·Mountpoint 파일 시스템 계약 밖의 기능이 필요하면 API 경로를 선택합니다. 랜덤 갱신·POSIX 잠금이 필요한 쓰기 가능 DB 디렉터리는 다른 스토리지 설계가 필요합니다.
## 스냅샷 및 백업
CSI 스냅샷은 해당 드라이버·백엔드 지원이 필요하며 PVC라는 이유만으로 지원되지는 않습니다. 아래 EBS 예제는 블록 볼륨의 한 시점을 캡처합니다. 필요에 따라 애플리케이션 quiesce/flush 또는 DB 전용 백업·WAL 보관을 사용합니다. 스냅샷 성공은 애플리케이션 일관성·DB 시점 복구를 입증하지 않습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-04-eks-storage-part2-2.html)
### 스냅샷 컨트롤러 준비
기존 snapshot CRD·컨트롤러 소유자·CSI snapshotter를 먼저 조회합니다. 호환 EKS add-on 또는 검토한 고정 external-snapshotter 릴리스를 사용하며 기존 설치에 floating master CRD를 덮어쓰지 않습니다. External-snapshotter8.6.0은 검토한 upstream 참고 버전이지 모든 add-on의 자동 업그레이드 목표가 아닙니다. Part1의 드라이버·네임스페이스 전제도 적용됩니다.
```bash
set -euo pipefail
kubectl get crd volumesnapshots.snapshot.storage.k8s.io \
volumesnapshotcontents.snapshot.storage.k8s.io volumesnapshotclasses.snapshot.storage.k8s.io
kubectl -n storage-demo get pvc ebs-claim -o yaml
```
### 클래스와 스냅샷 생성
Class driver와 PV driver가 일치해야 합니다. 수동 관리 예제의 Retain은 Kubernetes 스냅샷 삭제 시 보존한 EBS snapshot을 자동 삭제하지 않습니다. 소유권·비용을 추적하고 아래 Velero 수명과 혼동하지 마세요. 이 EBS 예제에는 빈 snapshotter-secret 파라미터가 필요하지 않습니다.
```yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: ebs-snapshot-class
driver: ebs.csi.aws.com
deletionPolicy: Retain
```
```yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: ebs-part2-snapshot
namespace: storage-demo
labels:
storage-demo: ebs
spec:
volumeSnapshotClassName: ebs-snapshot-class
source:
persistentVolumeClaimName: ebs-claim
```
```bash
set -euo pipefail
kubectl -n storage-demo wait --for=jsonpath='{.status.readyToUse}'=true \
volumesnapshot/ebs-part2-snapshot --timeout=300s
kubectl -n storage-demo get volumesnapshot ebs-part2-snapshot -o yaml
```
### 새 PVC로 복원
`readyToUse=true` 이후 restoreSize와 바인딩된 content·driver를 확인합니다. 같은 네임스페이스의 별도 이름과 restoreSize 이상의 용량을 사용하세요. 아래20Gi는 스냅샷이 그보다 크지 않다는 가정입니다. 운영 클레임은 보존하며 WaitForFirstConsumer라면 호환 소비자가 스케줄될 때까지 후보가 Pending일 수 있습니다. 통제된 전환 전에 격리한 호환 애플리케이션으로 복구 데이터를 검증합니다.
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-part2-restored
namespace: storage-demo
spec:
accessModes:
- ReadWriteOnce
storageClassName: ebs-gp3
resources:
requests:
storage: 20Gi
dataSource:
name: ebs-part2-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
```
### Velero로 백업 예약
검토한 조합은 Velero1.18.2와 AWS plugin1.14.2입니다. 설치 전 CLI 릴리스 checksum·plugin 호환성을 확인합니다. CSI 지원은 Velero에 통합되었지만 이 경로는 여전히 `EnableCSI`가 필요하며 이전 별도 CSI plugin을 추가하지 않습니다. 비공개 백업 버킷·접두사, 네트워크 경로, 제한된 IAM/KMS 권한과 `velero/velero` IRSA 역할을 준비합니다. 다음은 리소스를 생성하지 않고 신규 설치 매니페스트를 검토용으로 렌더링합니다:
```bash
set -euo pipefail
: "${BACKUP_BUCKET:?Set the existing private backup bucket}"
: "${VELERO_ROLE_ARN:?Set the reviewed IRSA role for system:serviceaccount:velero:velero}"
: "${AWS_REGION:?Set the bucket/snapshot Region for this example}"
velero install --provider aws \
--plugins velero/velero-plugin-for-aws:v1.14.2 \
--bucket "$BACKUP_BUCKET" --prefix eks-storage-demo \
--backup-location-config "region=$AWS_REGION" \
--snapshot-location-config "region=$AWS_REGION" \
--features EnableCSI --no-secret \
--sa-annotations "eks.amazonaws.com/role-arn=$VELERO_ROLE_ARN" \
--dry-run -o yaml > velero-review.yaml
```
이 CLI dry-run에도 유효한 kubeconfig 형식이 필요하지만 렌더링은 권한·백업 테스트가 아닙니다. 기존 Velero·CRD 설치를 덮어쓰지 마세요. 기본 ServiceAccount는 IRSA annotation과 함께 생성됩니다. 대신 `--service-account-name`을 사용하면 이미 존재하는 계정을 선택하며 `--sa-annotations`를 무시합니다. Pod Identity는 별도 association·agent 설정이 필요한 대안입니다. 정적 자격 증명 파일은 필요하지 않습니다.
CSI class 선택은 드라이버당 기본 class 하나만 레이블로 지정하거나 지원 backup/schedule annotation을 사용합니다. 다음 class를 기존 class들과 함께 검토하세요:
```yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: ebs-velero-snapshots
labels:
velero.io/csi-volumesnapshot-class: 'true'
driver: ebs.csi.aws.com
deletionPolicy: Retain
```
**Velero가 자신의 CSI 백업 스냅샷 수명을 관리합니다.** 백업 만료·삭제 시 원래 class가 Retain이어도 VolumeSnapshotContent 정책을 Delete로 바꾸어 스냅샷을 삭제합니다. 백업 TTL과 독립 보존·아카이브 절차를 명시적으로 정하세요. S3의 Velero 백업이 모든 볼륨 바이트를 포함한다고 가정하면 안 됩니다. CSI snapshot·filesystem backup·data mover는 다른 경로입니다. 예를 들어 FSx CSI1.10.0은 CSI snapshot을 구현하지 않으므로 지원 FSx·애플리케이션 백업 전략이 필요합니다.
```bash
set -euo pipefail
velero schedule create storage-demo-daily --schedule="0 1 * * *" \
--include-namespaces=storage-demo --ttl=168h0m0s -o yaml > velero-schedule-review.yaml
```
Schedule 명령도 YAML만 출력합니다. 소유자 경로로 검토한 schedule을 배포한 후 실제 Backup phase·오류·snapshot/data mover 완료와 복구 테스트를 확인합니다. 예약 스냅샷만으로 임의 시점 DB PITR을 제공하지는 않습니다.
복구는 과거 시각을 하드코딩하지 말고 실제 검증한 백업을 선택합니다. `-o yaml`이어도 API 검색과 선택한 Backup 조회를 수행하므로 대상 클러스터의 읽기 권한이 필요합니다. 아래 미리보기는 요청 리소스 유형을 제한하고 네임스페이스를 매핑합니다. 제출 전에 의존성과 plugin restore action을 확인하세요. 대상 네임스페이스 격리·이름 충돌·StorageClass/CSI driver/KMS/AZ 매핑을 검토해야 하며 완성된 프로덕션 복구 절차는 아닙니다:
```bash
set -euo pipefail
: "${VERIFIED_BACKUP:?Choose an actual completed backup after checking its contents/errors}"
velero restore create storage-demo-recovery-review --from-backup "$VERIFIED_BACKUP" \
--namespace-mappings storage-demo:storage-recovery \
--include-namespaces storage-demo \
--include-resources persistentvolumes,persistentvolumeclaims,volumesnapshots.snapshot.storage.k8s.io,volumesnapshotcontents.snapshot.storage.k8s.io \
--restore-volumes=true -o yaml > velero-restore-review.yaml
```
후보 데이터의 애플리케이션 검증 후 복구 워크로드를 시작하고 되돌릴 경로를 유지합니다. 교차 클러스터 CSI 복원은 같은 driver name과 접근 가능한 스냅샷·키가 필요하며 네임스페이스 매핑만으로 클라우드 리소스가 이식되지는 않습니다.
## 볼륨 확장 및 크기 조정

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-04-eks-storage-part2-3.html)
`allowVolumeExpansion: true` class와 확장을 지원하는 CSI·파일 시스템이 필요합니다. 바인딩된 클레임의 storageClassName은 유지하며 크기 조정 스위치처럼 바꾸지 않습니다. 예제는 Part1의 `ebs-gp3`를 재사용하고 클레임 요청만 늘립니다. 이미 더 큰 클레임을 줄이지 않도록 현재 요청·실제 용량을 먼저 확인하세요.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-gp3
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
parameters:
type: gp3
encrypted: 'true'
csi.storage.k8s.io/fstype: ext4
```
```bash
set -euo pipefail
kubectl -n storage-demo get pvc ebs-claim -o yaml
kubectl -n storage-demo patch pvc ebs-claim --type merge \
-p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
kubectl -n storage-demo describe pvc ebs-claim
```
지원되는 경우 CSI가 백엔드·파일 시스템 확장을 처리합니다. PVC 조건, 컨트롤러·노드 로그와 마운트 용량을 확인합니다. 필요한 경우 문서화된 재마운트·재시작 절차를 따르며 애플리케이션 컨테이너에서 추측한 `/dev/xvdf`에 `resize2fs`를 실행하지 않습니다. PV capacity를 수정해 확장을 우회하지 마세요. Quota·비용 상한과 이전 확장이 진행 중일 때의 중복 증가 방지를 계획합니다.
## 볼륨 클로닝
현재 EBS CSI는 `dataSource`와 CSI clone capability로 PVC 복제를 지원하며 EBS CSI1.66.0은 숨겨진 스냅샷을 가정하는 방식이 아닌 native EBS volume copy(`CopyVolumes`)를 사용합니다. 사용 전 배포한 드라이버/add-on 지원을 확인하세요. 대상은 독립 볼륨이지만 사용 가능 상태와 초기화 완료 성능은 다릅니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-04-eks-storage-part2-10.html)
일반 PVC dataSource 경로는 같은 네임스페이스의 Bound 소스, 호환 volume mode·드라이버와 소스 이상의 요청 크기를 사용합니다. StorageClassName을 명시하며 생략 시 원본을 자동 상속하는 대신 기본 class 규칙이 적용됩니다. Native EBS 복사본은 소스 AZ에 생성됩니다. 애플리케이션 quiesce/flush가 없으면 crash-consistent이며 소스당 초기화 중 복사본 하나와 계정·리전 quota가 적용됩니다.
전용 학습 소스를 사용하여 검증하지 않은 DB 클론을 프로덕션 네임스페이스에 시작하지 않습니다. Writer와 reader **양쪽** 예시 AZ를 실제 지원 AZ로 바꾸세요. Seed Job은 소스 PVC를 소비하고 기존 marker를 덮어쓰지 않으면서 파일을 만든 뒤 종료합니다.
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-clone-source
namespace: storage-demo
spec:
accessModes:
- ReadWriteOnce
storageClassName: ebs-gp3
resources:
requests:
storage: 10Gi
---
apiVersion: batch/v1
kind: Job
metadata:
name: ebs-clone-seed
namespace: storage-demo
spec:
backoffLimit: 0
template:
metadata:
labels:
app: ebs-clone-seed
spec:
restartPolicy: Never
automountServiceAccountToken: false
nodeSelector:
topology.kubernetes.io/zone: us-west-2a
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: writer
image: busybox:1.37.0
command:
- sh
- -c
args:
- |
set -eu
(set -C; printf 'clone-demo\n' > /data/seed.txt)
sync
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: ebs-clone-source
```
```bash
kubectl -n storage-demo wait --for=condition=complete job/ebs-clone-seed --timeout=300s
kubectl -n storage-demo get pvc ebs-clone-source -o yaml
```
Seed Job 완료 후 별도 클론과 읽기 전용 소비자를 만듭니다. Marker 확인은 검증할 동작을 보여주며 이 검토에서 클라우드 복사를 실행하지는 않았습니다. DB 복사는 기존 암호를 재설정하지 않으며 DB로 시작할 때는 데이터와 호환되는 DB 버전이 필요합니다.
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-clone
namespace: storage-demo
spec:
accessModes:
- ReadWriteOnce
storageClassName: ebs-gp3
resources:
requests:
storage: 10Gi
dataSource:
kind: PersistentVolumeClaim
name: ebs-clone-source
---
apiVersion: v1
kind: Pod
metadata:
name: app-with-clone
namespace: storage-demo
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.37.0
command:
- sh
- -c
args:
- test "$(cat /data/seed.txt)" = clone-demo && sleep 3600
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: data
mountPath: /data
readOnly: true
volumes:
- name: data
persistentVolumeClaim:
claimName: ebs-clone
nodeSelector:
topology.kubernetes.io/zone: us-west-2a
```
| 특성 | PVC/native volume copy | 스냅샷 복원 |
|---|---|---|
| 소스 | 기존 볼륨/PVC | 보존된 시점 스냅샷 |
| 준비 상태 | 백그라운드 초기화 완료 전 사용 가능 | 스냅샷·복원·초기화 상태에 따라 다름 |
| 배치 | Native EBS 복사본은 소스 AZ 유지 | 새 복원 볼륨을 허용된 다른 AZ에 생성 가능 |
| 일관성 | 애플리케이션 quiescing 필요 가능 | 애플리케이션-aware 백업 필요 가능 |
| 네임스페이스 | 일반 PVC dataSource는 같은 네임스페이스 | 스냅샷 객체는 네임스페이스 범위; 교차 import는 명시적인 지원 절차 필요 |
| 비용·보존 | 복사 작업·새 볼륨 요금, 독립 수명 | 스냅샷·복원 볼륨 요금, 별도 삭제 정책 |
“한 단계/두 단계”만으로 속도·스토리지 오버헤드·RPO를 추정하지 않습니다. Native volume copy에는 fast snapshot restore나 provisioned initialization rate를 사용할 수 없으므로 실제 복사 초기화 안내를 따릅니다.
## 다중 연결 EBS (Multi-Attach)
EC2 서비스는 볼륨 유형·리전 조건에 따라 적격 io1/io2를 같은 AZ의 호환 Nitro 인스턴스 최대16개에 Multi-Attach할 수 있습니다. **EBS CSI1.66.0 동적 경로는 io2 + ReadWriteMany + volumeMode: Block**을 지원하며 capability로 Multi-Attach를 활성화합니다. 이 경로에서 `multiAttachEnabled`는 지원 StorageClass 스위치가 아닙니다. RWOP는 Pod 하나 제약이지 Multi-Attach 모드가 아닙니다.
공유 블록 장치는 애플리케이션 쓰기를 조정하지 않습니다. 일반 ext4/XFS를 여러 노드가 독립적으로 읽기/쓰기 마운트하면 안 되며 조정된 애플리케이션·클러스터 파일 시스템과 fencing 설계가 필요합니다. io2는 NVMe reservation fencing을 지원하지만 애플리케이션이 올바르게 사용해야 합니다. 아래 연결 데모는 **장치 I/O·포맷을 수행하지 않습니다**. 일치하는 레이블·anti-affinity로 볼륨 AZ의 적합한 노드 두 개가 필요합니다. 실제 부하는 sleep 컨테이너가 아닌 검토한 장치 접근·동시성 조정이 필요합니다:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-io2-multi-attach
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
parameters:
type: io2
iops: '10000'
encrypted: 'true'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: shared-block-pvc
namespace: storage-demo
spec:
accessModes:
- ReadWriteMany
volumeMode: Block
storageClassName: ebs-io2-multi-attach
resources:
requests:
storage: 100Gi
---
apiVersion: v1
kind: Pod
metadata:
name: shared-block-a
namespace: storage-demo
labels:
app: shared-block-demo
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: shared-block-demo
topologyKey: kubernetes.io/hostname
containers:
- name: attachment-only
image: busybox:1.37.0
command:
- sleep
- '3600'
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
volumeDevices:
- name: shared
devicePath: /dev/ebs-shared
volumes:
- name: shared
persistentVolumeClaim:
claimName: shared-block-pvc
---
apiVersion: v1
kind: Pod
metadata:
name: shared-block-b
namespace: storage-demo
labels:
app: shared-block-demo
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: shared-block-demo
topologyKey: kubernetes.io/hostname
containers:
- name: attachment-only
image: busybox:1.37.0
command:
- sleep
- '3600'
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
volumeDevices:
- name: shared
devicePath: /dev/ebs-shared
volumes:
- name: shared
persistentVolumeClaim:
claimName: shared-block-pvc
```
io2 Multi-Attach는 서비스 조건에 따라 크기·IOPS 변경을 지원하므로 “온라인 확장 불가”로 일반화하면 틀립니다. io1의 변경 지원은 다릅니다. Multi-Attach 활성/비활성 전환은 분리된 볼륨이 필요하며 안전한 확장에는 CSI·파일 시스템·애플리케이션 지원도 확인해야 합니다. 공유 볼륨 장애는 연결 인스턴스 전체에 영향을 줄 수 있고 Retain·연결 설정은 백업 전략이 아닙니다.
## Mountpoint for S3 CSI 심화
아래 동작은 CSI2.8.0에 포함된 Mountpoint1.23.0을 기준으로 확인했습니다. 더 새로운 standalone 옵션을 적용하기 전에 실제 설치 버전을 확인하세요.
### 성능 특성
순차 대용량 객체 읽기·새 파일 순차 쓰기는 일반적인 적용 대상이며 Mountpoint는 자동 prefetch를 수행합니다. Random range 읽기도 지원하지만 객체 크기·요청률·네트워크·캐시·애플리케이션 동시성이 결과를 결정합니다. 임의 위치 랜덤 쓰기는 단지 느린 선택지가 아니라 미지원 동작입니다.
**검증되지 않은 과거 수치:** 기존 영어 문서는 재현 구성·추적 가능한 출처 없이 다음 집계 수치를 제시했습니다. 과거 주장으로 보존하며 현재 서비스 한도·이번 검토의 실측·용량 설계 보장으로 사용하지 않습니다.
| 과거 작업 | 원래 주장 |
|---|---|
| 대용량 순차 읽기 | 집계 최대100Gbps |
| 새 파일 순차 쓰기 | 집계 최대50Gbps |
| 소용량 랜덤 읽기 | 높은 지연·낮은 처리량, 워크로드에 따라 다름 |
기존 “우수/양호/보통” 등급도 통제된 벤치마크가 아니었습니다.
### 파일 시스템·일관성 제한
- S3는 강한 읽기·목록 일관성을 제공합니다. Mountpoint 캐시는 TTL 동안 오래된 메타데이터·내용·negative entry를 의도적으로 유지할 수 있으며 이를 S3 자체의 eventual consistency로 설명하면 안 됩니다.
- 새 파일은 순차 쓰기합니다. 기존 객체 교체는 `allow-overwrite`와 truncating open이 필요하며 임의 랜덤 갱신은 여전히 미지원입니다.
- Availability Zone의 S3 Express One Zone directory bucket은 `incremental-upload` append와 개별 파일의 atomic rename을 지원합니다. 이 Mountpoint 버전에서 general purpose bucket의 파일 rename과 모든 directory rename은 미지원입니다. Local Zone directory bucket은 기능 지원이 다릅니다. 교체 rename에도 overwrite 권한·옵션이 필요합니다.
- 삭제는 `allow-delete`와 IAM 권한으로 허용하는 선택 기능이며 읽기 전용 예제는 둘 다 허용하지 않습니다. Append가 항상 “새 객체 버전 생성”인 것은 아닙니다.
- Hard/symbolic link·chmod/chown·extended attribute·POSIX 잠금·장치 파일·일반 sparse-file 의미론을 지원하지 않습니다. 누락된 기능에 DB 정확성을 의존시키면 안 됩니다.
### 캐시 설정
Mountpoint CLI 옵션은 StorageClass parameters가 아닌 **PV.spec.mountOptions**에 두며 `metadata-ttl 300` 같은 옵션 문자열을 사용합니다. Mountpoint1.23의 metadata TTL 단위는 초, standalone `max-cache-size`는 **MiB**, read/write-part-size는 **byte**이며8MiB는8,388,608byte입니다. 이전 prefetch-bytes·read-ahead·max-read-parallelism·max-cache-size-mb·cache-block-size 예제는 유효한1.23 CLI flag가 아닙니다.
CSI v2는 보통 `mount-s3`의 **Mountpoint Pod**에 캐시 스토리지를 만듭니다. 애플리케이션 Pod의 emptyDir가 그 캐시로 연결되지는 않습니다. 아래 대안 정적 PV는 드라이버가 크기 한도를 적용하는10Gi 디스크 기반 emptyDir 캐시를 사용합니다. Memory는 NVMe가 아니라 tmpfs RAM입니다. 실제 버킷·접두사·리전을 교체하고 별도로 사전 바인딩한 PVC를 사용합니다:
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: s3-training-cached-pv
spec:
capacity:
storage: 1Ti
volumeMode: Filesystem
accessModes:
- ReadOnlyMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ''
claimRef:
namespace: storage-demo
name: training-data-cached
mountOptions:
- read-only
- region us-west-2
- prefix training/
- allow-other
- uid 1000
- gid 1000
- dir-mode 0750
- file-mode 0440
- metadata-ttl 300
- read-part-size 8388608
csi:
driver: s3.csi.aws.com
volumeHandle: storage-demo-s3-training-cached-v1
volumeAttributes:
bucketName: replace-with-owned-data-bucket
authenticationSource: pod
stsRegion: us-west-2
cache: emptyDir
cacheEmptyDirSizeLimit: 10Gi
cacheEmptyDirMedium: ''
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: training-data-cached
namespace: storage-demo
spec:
accessModes:
- ReadOnlyMany
storageClassName: ''
volumeName: s3-training-cached-pv
resources:
requests:
storage: 1Ti
```
300초 TTL은 그 기간 외부 변경을 숨길 수 있으므로 필요하면 불변·버전별 데이터셋 접두사를 사용합니다. 로컬 캐시의 내용은 평문이므로 노드·스토리지 접근도 위협 모델에 포함합니다. 학습 컨테이너와 별도로 캐시 용량·eviction·Mountpoint Pod 메모리를 계획하세요. `cache: ephemeral`과 해당 StorageClass·요청 필드로 ephemeral PVC 캐시도 지원되며 수명·비용을 평가해야 합니다. 이전 host cache path를 전달한 뒤 CSI v2가 그대로 사용한다고 가정하지 않습니다.
### 대규모 데이터 학습 예제
다음은 최적화된 p4d 벤치마크가 아닌 **미실행 통합 템플릿**입니다. `/opt/training/train.py`가 포함된 검토한 GPU 호환 이미지, 스케줄 가능한 GPU 네 개·device plugin, 앞의 계정·캐시 데이터 PVC와 UID/GID1000이 쓸 수 있는 RWX `reviewed-model-output-rwx` PVC를 준비합니다. 스크립트는 표시한 인수와 독립 출력 디렉터리 생성을 구현해야 합니다. 배포 전에 모든 placeholder를 해결하세요.
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: s3-sharded-training
namespace: storage-demo
spec:
completions: 4
parallelism: 4
completionMode: Indexed
backoffLimit: 0
activeDeadlineSeconds: 7200
template:
spec:
serviceAccountName: s3-access-sa
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: trainer
image: registry.example.com/reviewed-trainer:replace-me
command:
- sh
- -c
args:
- exec python /opt/training/train.py --data-dir=/data --shard-index="$JOB_COMPLETION_INDEX"
--shard-count=4 --output-dir="/models/$JOB_COMPLETION_INDEX"
env:
- name: JOB_COMPLETION_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
resources:
requests:
cpu: '4'
memory: 16Gi
nvidia.com/gpu: 1
limits:
cpu: '8'
memory: 32Gi
nvidia.com/gpu: 1
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: data
mountPath: /data
readOnly: true
- name: output
mountPath: /models
- name: tmp
mountPath: /tmp
volumes:
- name: data
persistentVolumeClaim:
claimName: training-data-cached
readOnly: true
- name: output
persistentVolumeClaim:
claimName: reviewed-model-output-rwx
- name: tmp
emptyDir: {}
```
Indexed completion 네 개는 각각 GPU 하나를 사용하는 독립 데이터 shard입니다. 분산 학습 rendezvous·gradient 동기화·exactly-once 부수 효과를 구성하지는 않습니다. 재시도에도 출력 처리가 안전하도록 구현하세요. 이전4-GPU/8-GPU 예제는 서로 다른 설명용 구성이었으며 비교 실측이 아닙니다. 실제 하드웨어·리소스는 측정한 요구로 선택합니다.
### S3·EFS·FSx 선택
| 고려 사항 | Mountpoint/S3 | EFS | FSx for Lustre |
|---|---|---|---|
| 인터페이스 | 객체 기반 파일 시스템 부분집합 | 관리형 NFS 파일 시스템 | 병렬 Lustre 파일 시스템 |
| 일반 용도 | 대규모 불변 입력 데이터셋 | 공유 애플리케이션 파일 | 지원 HPC/ML 병렬 I/O |
| 쓰기 | 문서화된 순차·덮어쓰기와 버킷별 append 제한 | 애플리케이션 조정이 필요한 파일 쓰기 | 애플리케이션 조정이 필요한 파일 쓰기 |
| 크기 산정 근거 | 요청 패턴·캐시·네트워크·객체 배치 | 성능·처리량 모드·클라이언트·접근 패턴 | 배포·용량·처리량·클라이언트·stripe |
| 비용·동시성 | 요청·전송·캐시·클라이언트 한도 측정 | 처리량·스토리지·클라이언트 한도 측정 | 할당·처리량·클라이언트 한도 측정 |
서비스 이름만으로 보편적인 낮음/중간/높음 비용이나 무제한 클라이언트 등급을 정할 수는 없습니다.
## 스토리지 성능 최적화
EKS에서 스토리지 성능을 최적화하기 위한 다양한 전략을 살펴보겠습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-04-eks-storage-part2-4.html)
### EBS 성능 최적화
측정한 워크로드 요구와 인스턴스 EBS 제한에 따라 볼륨 유형·IOPS·처리량을 선택합니다. 이전16,000IOPS·1,000MiB/s는 유효한 구성 예제이지만 현재 gp3의 보편적 최댓값은 아닙니다. Regional gp3는 용량·IOPS 제약에 따라 최대80,000IOPS·2,000MiB/s를 지원하며 Outposts 한도는 더 낮습니다. 최댓값 프로비저닝이 필요·충분하다고 가정하지 않습니다.
빈 볼륨은 초기화가 필요하지 않습니다. 스냅샷 복원·native copy는 초기화 지연이 있을 수 있으며 절차가 다릅니다. **0 쓰기는 기존 데이터를 파괴하며 안전한 초기화가 아닙니다.** 정확한 볼륨을 먼저 식별합니다. 스냅샷 복원은 지원 provisioned initialization rate, fast snapshot restore 또는 공식 읽기 기반 절차를 검토하며 이 가속 기능은 native copy에 적용되지 않습니다. 다음은 승인된 노드 환경의 메타데이터 조회만 수행합니다:
```bash
lsblk -o NAME,SERIAL,SIZE,TYPE,MOUNTPOINT
```
### EFS 성능 최적화
AWS는 높은 동시성 부하에도 General Purpose를 권장하며 Max I/O는 연산 지연이 더 높은 이전 세대 선택지입니다. 실제 수요에 맞는 처리량 모드를 선택합니다. Mount option은 `Pod.spec.volumes`가 아닌 StorageClass 또는 PV에 설정합니다.
아래는 새 클레임용 대안 class입니다. EFS helper의1MiB RPC 크기, hard mount, timeout과 noresvport는 시작점이지 벤치마크가 아닙니다. `retrans=2`는 재시도 후 추가 복구 동작을 정하며 hard mount가 두 번 뒤 요청을 포기하는 것은 아닙니다. Part1처럼 파일 시스템 ID·access point 신원을 준비합니다:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: efs-tuned
provisioner: efs.csi.aws.com
reclaimPolicy: Retain
mountOptions:
- tls
- rsize=1048576
- wsize=1048576
- hard
- timeo=600
- retrans=2
- noresvport
parameters:
provisioningMode: efs-ap
fileSystemId: fs-0123456789abcdef0
directoryPerms: '750'
uid: '1000'
gid: '1000'
basePath: /storage-demo
ensureUniqueDirectory: 'true'
```
### FSx for Lustre 성능 최적화
배포·스토리지·처리량과 클라이언트 용량을 함께 선택합니다. 파일 크기·동시성·측정한 병목에 따라 Lustre stripe를 정하며 많다고 항상 빠르지는 않습니다. PV/StorageClass의 지원 mount option을 사용하고 `noatime`·`relatime`을 충돌시키지 말고 하나의 정책을 선택합니다. 압축 효과는 데이터에 따라 다릅니다. `s3ImportPath`는 지원 CSI 파라미터이지만 import/export 지원·자동화는 선택한 FSx 통합에 달려 있습니다.
### vLLM 워크로드를 위한 스토리지 최적화
Class의 `storageCapacity` 필드 대신 앞에서 명시한 class/PVC 할당을 사용합니다. Cold/warm 모델 로딩, 메타데이터, CPU/GPU 초기화와 동시 소비자를 측정합니다. 양자화·sharding은 파일 배치뿐 아니라 모델 메모리·연산도 바꾸며 자동 스토리지 최적화가 아닙니다. EFA는 지원 파일 시스템·클라이언트 또는 통신 스택에서만 도움이 되며 EFA GPU 인스턴스 선택만으로 모든 스토리지 경로가 빨라지지는 않습니다.
## 결론
이 문서에서는 Amazon EKS에서 FSx for Lustre, S3, 스냅샷, 볼륨 확장 및 성능 최적화에 대해 알아보았습니다. 각 스토리지 옵션은 서로 다른 특성과 사용 사례를 가지고 있으므로, 애플리케이션의 요구사항에 맞는 적절한 스토리지 솔루션을 선택하고 최적화하는 것이 중요합니다.
다음 파트에서는 EKS 스토리지의 모니터링, 문제 해결, 비용 최적화 및 보안에 대해 알아보겠습니다.
## 참고 자료
* [Amazon FSx for Lustre CSI 드라이버](https://github.com/kubernetes-sigs/aws-fsx-csi-driver)
* [Amazon S3 CSI 드라이버](https://github.com/awslabs/mountpoint-s3-csi-driver)
* [Kubernetes 볼륨 스냅샷](https://kubernetes.io/docs/concepts/storage/volume-snapshots/)
* [Velero 백업 및 복원](https://velero.io/docs/)
* [Amazon EKS 스토리지 모범 사례](https://docs.aws.amazon.com/eks/latest/best-practices/storage.html)
* [Mountpoint CSI2.8 configuration](https://github.com/awslabs/mountpoint-s3-csi-driver/blob/v2.8.0/docs/CONFIGURATION.md)
* [Mountpoint CSI2.8 cache](https://github.com/awslabs/mountpoint-s3-csi-driver/blob/v2.8.0/docs/CACHING.md)
* [Mountpoint1.23 filesystem semantics](https://github.com/awslabs/mountpoint-s3/blob/mountpoint-s3-1.23.0/doc/SEMANTICS.md)
* [Hadoop3.5 S3A authentication](https://hadoop.apache.org/docs/r3.5.0/hadoop-aws/tools/hadoop-aws/index.html)
* [Velero1.18 CSI snapshot lifecycle](https://velero.io/docs/v1.18/csi/)
* [Velero AWS plugin compatibility](https://github.com/velero-io/velero-plugin-for-aws)
* [EBS native copy](https://docs.aws.amazon.com/ebs/latest/userguide/ebs-copying-volume.html)
* [EBS Multi-Attach](https://docs.aws.amazon.com/ebs/latest/userguide/ebs-volumes-multi.html)
* [FSx CSI add-on identities](https://docs.aws.amazon.com/eks/latest/userguide/fsx-csi-create.html)
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/04-eks-storage-part2-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/04-eks-storage-part3
----------------------------------------
# Amazon EKS 스토리지 - Part 3: 모니터링, 문제 해결, 비용 최적화, 보안
> **마지막 업데이트**: 2026년 9월 11일
이 문서는 Amazon EKS 스토리지 시리즈의 세 번째이자 마지막 부분으로, 스토리지 모니터링, 문제 해결, 비용 최적화 및 보안에 대해 다룹니다.
## 목차
1. [스토리지 모니터링](#스토리지-모니터링)
2. [스토리지 문제 해결](#스토리지-문제-해결)
3. [스토리지 비용 최적화](#스토리지-비용-최적화)
4. [스토리지 보안](#스토리지-보안)
5. [스토리지 관리 모범 사례](#스토리지-관리-모범-사례)
## 스토리지 모니터링
백엔드·Kubernetes·애플리케이션 관측을 함께 사용합니다. 백엔드 I/O 카운터는 파일 시스템 여유 공간을 측정하지 않으며 Kubernetes readiness가 DB 일관성을 입증하지도 않습니다. 경보를 만들기 전에 단위·차원·집계 기간·데이터 누락 동작을 기록하세요.
### CloudWatch를 사용한 모니터링
EBS/EFS/FSx는 Kubernetes exporter 없이 서비스 지표를 게시합니다. CloudWatch 조회·시각화에는 적절한 IAM 권한이 필요합니다. get-dashboard는 이미 존재하는 대시보드 정의를 가져오며 대시보드 생성·지표 활성화 명령이 아닙니다.
#### EBS 볼륨 지표
| 지표 | 올바른 해석 |
|---|---|
| VolumeReadBytes / VolumeWriteBytes | Sum은 선택 기간 전송 byte이며 기간 초로 나누면 byte/s |
| VolumeReadOps / VolumeWriteOps | Sum은 완료 작업 수이며 기간 초로 나누면 IOPS |
| VolumeTotalReadTime / VolumeTotalWriteTime | Sum은 누적 작업 시간(초); 대응 작업 수 Sum으로 나누면 평균 초/op, 작업0건은 별도 처리 |
| VolumeQueueLength | 대기 I/O gauge; Average/Maximum으로 지속 queue·peak 구분 |
| BurstBalance | gp2·st1·sc1의 남은 credit이며 gp3 credit 지표가 아님 |
지원 Nitro 연결에는 현재 VolumeAvgIOPS(Ops/s), VolumeAvgThroughput(KiB/s), VolumeAvgReadLatency/VolumeAvgWriteLatency(ms)와 exceeded/stalled-I/O 지표도 있습니다. 각각 Multi-Attach·컴퓨팅·zone 제약을 확인하세요. 일반 볼륨 지표는 연결된 볼륨에 게시되며 누락이 자동으로 사용량0을 의미하지 않습니다. 작업이 겹치면 기존 누적 시간 카운터가 실제 경과 기간보다 클 수 있습니다.
#### EFS 파일 시스템 지표
TotalIOBytes·DataReadIOBytes·DataWriteIOBytes·MetadataIOBytes는 이미 정규화된 전송률이 아니라 byte 지표입니다. 적절한 Sum을 기간으로 나누어 byte/s를 구합니다. MeteredIOBytes는 읽기 할인 등을 반영한 EFS 처리량 계량값이며 원시 전송 byte와 같지 않습니다. PermittedThroughput은 속도 지표입니다. 기간·단위를 맞추고 필요에 따라 ClientConnections·모드에 맞는 PercentIOLimit·스토리지 class를 모니터링합니다. BurstCreditBalance는 Bursting 처리량에 적용되며 Elastic에는 적용되지 않습니다.
#### FSx for Lustre 지표
DataReadBytes/DataWriteBytes·DataReadOperations/DataWriteOperations는 FileSystemId를 사용하며 Sum/기간으로 처리량·작업/s를 구합니다. **NetworkThroughputUtilization은 유효한 지표**이며 OSS별 FileSystemId·FileServer 차원의 사용률(%)입니다. FreeDataStorageCapacity는 FileSystemId·StorageTargetId의 OST별 지표입니다. Target 불균형과 같은 시점의 용량 gauge를 확인하며 gauge를 시간축으로 합산해 현재 여유 용량처럼 해석하지 않습니다.
LogicalDiskUsage·PhysicalDiskUsage도 유효하며 압축 전 논리 byte와 압축 후 물리 byte를 설명합니다. 파일 시스템 집계로 압축 효과를 평가할 수 있지만 프로비저닝 용량 요금이 사용량만의 요금으로 바뀌지는 않습니다.
### Prometheus 및 Grafana를 사용한 모니터링
모니터링 소유자의 기존 stack을 재사용합니다. 신규 설치를 검토한다면 배포된 kube-prometheus-stack90.1.1/operator0.93.1은 검증한 참고 조합이지 기존 클러스터의 자동 업그레이드 지시가 아닙니다. 다음은 매니페스트만 렌더링합니다. admin-user/admin-password 키를 가진 `monitoring/grafana-admin`과 본문의 확장 가능한 ebs-gp3 class를 준비합니다. 실제 클러스터 버전·스토리지 크기·보존 기간으로 조정하세요. 알림 전달·Grafana 영속성·가용성·kubelet TLS/인증 기본값은 배포별 검토가 필요합니다:
```yaml
grafana:
admin:
existingSecret: grafana-admin
userKey: admin-user
passwordKey: admin-password
prometheus:
prometheusSpec:
retention: 14d
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: ebs-gp3
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
```
```bash
set -euo pipefail
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update prometheus-community
helm template prometheus prometheus-community/kube-prometheus-stack \
--version 90.1.1 --namespace monitoring --kube-version 1.36.0 \
--include-crds -f monitoring-values.yaml > monitoring-review.yaml
```
ServiceMonitor는 **Service**와 그 endpoint를 선택하며 namespaceSelector와 Prometheus 인스턴스의 ServiceMonitor selector가 모두 맞아야 합니다. EBS CSI1.66 Helm chart의 controller.enableMetrics 기본값은 false입니다. 해당 chart로 관리하는 설치는 기존 소유자와 다음 값을 검토합니다. 드라이버3301 endpoint·sidecar metrics Service·생성되는 ServiceMonitor를 활성화하며 release 레이블을 실제 Prometheus selector와 맞춥니다:
```yaml
controller:
enableMetrics: true
serviceMonitor:
labels:
release: prometheus
```
EKS 관리형 add-on은 다른 설정 옵션을 노출할 수 있습니다. Helm values를 그대로 적용하지 말고 버전·configuration·실제 리소스를 확인합니다. 아래 독립 ServiceMonitor는 소유자가 표시한 Service를 이미 노출하고 대응 monitor를 생성하지 않았을 때의 **대안**입니다. 같은 생성 monitor와 함께 배포하지 않습니다:
```yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: csi-metrics-reviewed
namespace: monitoring
labels:
release: prometheus
spec:
namespaceSelector:
matchNames:
- kube-system
selector:
matchLabels:
app: ebs-csi-controller
endpoints:
- port: metrics
path: /metrics
interval: 30s
```
```bash
set -euo pipefail
kubectl -n kube-system get svc ebs-csi-controller -o yaml
kubectl -n kube-system get endpointslice \
-l kubernetes.io/service-name=ebs-csi-controller -o wide
kubectl -n monitoring get prometheus -o yaml
```
선택한 target의 Up 상태와 실제 지표를 확인합니다. 드라이버·provisioner/attacher/resizer/snapshotter는 별도 endpoint를 가지므로 드라이버 Service 하나가 모든 sidecar를 수집하지는 않습니다. CSI API 작업 지연과 애플리케이션/EBS 데이터 I/O 지연도 구분합니다.
### 파일 시스템 사용량과 알림
CSI 드라이버가 필요한 volume 통계를 구현한 경우 인증된 kubelet의 kubelet_volume_stats_*를 사용합니다. kube-state-metrics는 객체 상태·요청 정보, node-exporter는 host 파일 시스템 지표를 제공합니다. node-exporter DaemonSet을 하나 더 설치해도 PVC별 사용량 지표가 생기지 않습니다. kube-prometheus-stack에는 node-exporter 옵션이 이미 있으므로 host mount·특권을 중복하지 마세요.
Raw block과 통계를 지원하지 않는 드라이버는 파일 시스템 용량을 게시하지 않을 수 있습니다. EFS access point/PVC 요청은 디렉터리별 quota가 아니며 보고된 용량이 공유 파일 시스템을 가리킬 수 있습니다. container_fs_usage_bytes도 보편적 PVC 측정값이 아닙니다. 수집기·마운트·네임스페이스/클레임 매핑을 확인하고 누락 지표를 별도로 처리합니다.
다음 규칙은 같은 클레임의 중복 scrape를 합산하지 않고 max로 제거합니다. Federation 데이터에는 신뢰할 수 있는 cluster 레이블이 필요합니다. 용량0 series는 제외합니다. 예측은 gauge·최근 추세를 사용하며 조치 전에 scrape 누락·클레임 재생성/확장·워크로드 변화를 검토합니다. 읽기 전용·정적 데이터셋처럼 호출 경보 대상이 아닌 클레임에는 selector를 조정하세요:
```yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: storage-alerts
namespace: monitoring
labels:
release: prometheus
spec:
groups:
- name: storage-reviewed
rules:
- record: pvc:storage_used_bytes:max
expr: max by (cluster, namespace, persistentvolumeclaim) (kubelet_volume_stats_used_bytes)
- record: pvc:storage_capacity_bytes:max
expr: max by (cluster, namespace, persistentvolumeclaim) (kubelet_volume_stats_capacity_bytes)
- alert: VolumeUsageHigh
expr: (pvc:storage_used_bytes:max / pvc:storage_capacity_bytes:max > 0.85) and
(pvc:storage_capacity_bytes:max > 0)
for: 10m
labels:
severity: warning
annotations:
summary: Volume usage high ({{ $value | humanizePercentage }})
description: PVC {{ $labels.namespace }}/{{ $labels.persistentvolumeclaim
}} requires capacity review.
- alert: VolumeMayFillIn24Hours
expr: (predict_linear(pvc:storage_used_bytes:max[6h], 86400) > pvc:storage_capacity_bytes:max)
and (pvc:storage_capacity_bytes:max > 0) and (delta(pvc:storage_used_bytes:max[1h])
> 0)
for: 10m
labels:
severity: warning
annotations:
summary: Recent trend projects capacity exhaustion
description: Review the trend and workload for PVC {{ $labels.namespace }}/{{
$labels.persistentvolumeclaim }}; this is not a guarantee.
```
네임스페이스·release 레이블이 Prometheus rule selector와 맞아야 합니다. 규칙 문법·합성 시나리오를 검증한 뒤 대상 배포의 실제 지표 범위·알림 전달을 확인합니다. 이번 검토에서 실제 지표 수집·알림 전달을 실행하지 않았습니다.
## 스토리지 문제 해결
객체 신원·이벤트부터 확인합니다. Pending·ContainerCreating·느린 I/O는 원인이 다를 수 있으며 이미지 pull·스케줄링·앱 readiness가 반드시 스토리지 장애인 것은 아닙니다. 그림은 초기 분류 안내이지 모든 증상을 특정 원인에 대응시키는 표가 아닙니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-04-eks-storage-part3-1.html)
### 프로비저닝과 WaitForFirstConsumer
실제 워크로드 네임스페이스·클레임·대상 소비자를 사용합니다. 추측으로 광범위한 권한을 부여하지 말고 참조 class·컨트롤러 이벤트·드라이버 신원/KMS 권한·quota·노드 연결 한도를 확인합니다:
```bash
set -euo pipefail
: "${NAMESPACE:?Set the workload namespace}"
: "${PVC_NAME:?Set the claim name}"
: "${POD_NAME:?Set its intended consumer Pod}"
kubectl -n "$NAMESPACE" get pvc "$PVC_NAME" -o yaml
kubectl -n "$NAMESPACE" describe pvc "$PVC_NAME"
kubectl -n "$NAMESPACE" describe pod "$POD_NAME"
kubectl get storageclass
kubectl get nodes -L topology.kubernetes.io/zone
```
```bash
set -euo pipefail
: "${CSI_CONTROLLER_POD:?Select the actual controller Pod}"
: "${CSI_CONTAINER:?Select the relevant driver/sidecar container}"
kubectl -n kube-system get pod "$CSI_CONTROLLER_POD" \
-o jsonpath='{.spec.containers[*].name}'
kubectl -n kube-system logs "$CSI_CONTROLLER_POD" -c "$CSI_CONTAINER" --since=15m --tail=200
```
WaitForFirstConsumer는 스케줄 가능한 소비자가 topology를 결정할 때까지 새 PVC를 의도적으로 Pending으로 둡니다. 아직 바인딩되지 않은 새 PVC에는 노드풀을 이동시켜야 할 AZ가 이미 정해진 것이 아닙니다. Pod selector·affinity·taint·리소스·스토리지 topology를 확인하세요. spec.nodeName으로 스케줄러를 우회하면 이 바인딩이 진행되지 않을 수 있으므로 지원 스케줄 제약을 사용합니다. 바인딩 후에는 EBS PV의 AZ·node affinity가 중요합니다. Pending을 지우려고 클레임을 삭제하거나 PV capacity를 편집하지 않습니다.
### 마운트 실패
Attach 오류·node publish/mount 오류·파일 시스템 client 누락·신원/권한 오류·애플리케이션 권한을 구분합니다. 로그는 관련 CSI 드라이버/sidecar 컨테이너를 선택하며 다중 컨테이너 Pod의 기본 로그가 모든 구성 요소를 포함하지는 않습니다. 필요하면 VolumeAttachment·바인딩 PV의 driver/handle·배정된 노드를 확인합니다.
노드 로그는 소유자가 지원하는 접근·진단 경로를 사용합니다. Bottlerocket·Auto Mode·다른 관리형 컴퓨팅에서 ec2-user SSH·journalctl이 보편적인 방법은 아닙니다. 특권 amazonlinux:2 helper가 올바른 CSI/client 설정을 대체하지 않으며 AL2의2026년 OS 지원은 종료되었습니다. 별도 승인된 노드 수동 마운트 전에 본문의 지원 CSI 소비자 테스트·이벤트를 사용합니다.
EFS는 실제 mount client에서 mount target·DNS·TCP2049를 확인합니다. Lustre는 TCP988·1018–1023과 서비스의 client/server 규칙이 필요하고 EFA에는 추가 SG 참조 조건이 있습니다. NACL 반환 트래픽·route도 확인하세요. ICMP ping 실패가 NFS 불가의 증거는 아니며 AWS CLI 컨테이너에 ping·telnet·mount helper가 있다고 보장되지 않습니다. Pod 네트워크 테스트는 노드 CSI 마운트와 출발지·SG가 다를 수 있습니다.
### 느린 I/O
측정한 작업 크기·queue·앱 동시성·프로비저닝 성능·인스턴스 EBS 한도·초기화 상태를 확인합니다. 다음 이식 가능한 Python 시간 계산은 완료된5분 구간을 사용합니다. VolumeReadOps의 Sum/300이 읽기 IOPS이며 Average를 그대로 속도로 해석하지 않습니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the volume Region}"
: "${EBS_VOLUME_ID:?Set the verified owned EBS volume ID}"
read -r START_TIME END_TIME < <(python3 - <<'PY'
import datetime, time
end = int(time.time()) // 300 * 300
fmt = lambda value: datetime.datetime.fromtimestamp(value, datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
print(fmt(end - 3600), fmt(end))
PY
)
aws cloudwatch get-metric-statistics --region "$AWS_REGION" \
--namespace AWS/EBS --metric-name VolumeReadOps \
--dimensions "Name=VolumeId,Value=$EBS_VOLUME_ID" \
--start-time "$START_TIME" --end-time "$END_TIME" \
--period 300 --statistics Sum --output json > ebs-read-ops.json
python3 - ebs-read-ops.json <<'PY'
import json, sys
with open(sys.argv[1]) as stream:
points = json.load(stream)["Datapoints"]
for point in sorted(points, key=lambda p: p["Timestamp"]):
print(point["Timestamp"], "read IOPS:", point["Sum"] / 300)
if not points:
print("No datapoints: check dimensions, attachment/activity, Region and publication delay")
PY
```
파일 시스템 쓰기 테스트는 검증한 마운트의 승인된 폐기 가능 디렉터리를 사용합니다. 의도한 테스트 환경에 다음 스크립트를 저장하세요. 고유64MiB 파일을 만들고 자신의 파일·디렉터리만 정리합니다. 읽기는 캐시에 적중할 수 있으므로 이 시간만으로 cold storage 성능을 입증하지 않습니다. 운영 데이터에 고정 `/data/test` 덮어쓰기·raw 장치0 쓰기를 실행하지 마세요:
```bash
set -euo pipefail
: "${STORAGE_TEST_DIR:?Set an approved disposable directory on the verified disposable filesystem mount}"
test -d "$STORAGE_TEST_DIR" && test -w "$STORAGE_TEST_DIR"
STORAGE_TEST_PATH=$(mktemp -d "$STORAGE_TEST_DIR/storage-test.XXXXXX")
cleanup() { rm -f -- "$STORAGE_TEST_PATH/payload"; rmdir -- "$STORAGE_TEST_PATH"; }
trap cleanup EXIT
time dd if=/dev/zero of="$STORAGE_TEST_PATH/payload" bs=1M count=64 conv=fsync
time dd if="$STORAGE_TEST_PATH/payload" of=/dev/null bs=1M
```
이번 검토에서는 스토리지 벤치마크를 실행하지 않았습니다. 단편화는 근거가 필요한 가설로 다루며 파일 시스템 재생성·포맷은 일반적인 첫 해결책이 아니라 데이터 이전입니다. 추측한 nvme0n1의 I/O scheduler를 바꾸지 마세요. 다른 볼륨·루트 장치일 수 있고 현대 blk-mq scheduler의 이름·지원도 다릅니다.
EFS는 General Purpose·실제 처리량 모드·client 한도·메타데이터 요구·동시성이 중요합니다. Part2의 hard/TLS·적절한 timeout/retry를 포함한 지원 mount-helper 옵션을 사용합니다. 파일 묶음·순차 접근 증가가 측정한 부하에는 도움이 될 수 있지만 앱 데이터 배치 변경이 항상 이로운 것은 아닙니다.
## 스토리지 비용 최적화
지연·내구성·복구·소유권 요구를 유지하면서 전체 워크로드 비용을 최적화합니다. 컴퓨팅 할인·할당 스토리지·프로비저닝 성능·요청/전송 요금·보존 백업은 별도 비용입니다.
### 볼륨 유형·크기·이전
실제 gp2 비용·성능과 gp3를 비교하고 HDD는 적합한 접근 패턴에만 고려합니다. 임의 최대 크기가 아닌 측정한 여유·경보를 준비합니다. EBS/PVC 용량은 일반적으로 확장하며 축소하지 않으므로 할당 용량 감소에는 더 작은 새 볼륨으로의 지원 이전·데이터 검증이 필요합니다.
gp3 StorageClass 생성·기본 class 지정은 **기존 gp2 볼륨을 이전하지 않습니다**. 기본 class 변경은 관련 없는 신규 클레임에도 영향을 줍니다. Part1의 명시적 class를 재사용하세요. 기존 볼륨은 배포 드라이버가 지원하는 변경 절차나 새 클레임으로의 검증한 백업/복원을 선택하고 스토리지 변경 전에 소유권·앱 일관성·되돌리기를 확인합니다.
### 수명과 보존
VolumeSnapshotClass는 드라이버·보존 동작을 정의하며 스냅샷 예약·나이별 삭제를 수행하지 않습니다. 백업 소유자의 schedule·보존 정책을 사용하세요. Part2는 원래 class가 Retain이어도 삭제될 수 있는 Velero CSI snapshot 수명을 설명합니다.
PV가 Available·Released라고 자동으로 폐기 가능하지 않으며 Bound도 실제 사용의 증거는 아닙니다. 정리 전에 claimRef/UID·워크로드 소유자·snapshot·보존 의무·실제 백엔드를 조사합니다. Retain은 과금 리소스를 남길 수 있고 Delete 동작은 드라이버에 따라 다릅니다. EFS access point 삭제와 파일 시스템·데이터 삭제는 다릅니다. S3/Archive 계층화에도 앱과 호환되는 복구·접근 계획이 필요합니다.
### EFS 비용 최적화
AWS는 예측하기 어려운·급증하는 워크로드에 Elastic 처리량을 권장합니다. 실제 metered I/O·현재 요금으로 지속 수요의 Provisioned, 용량/credit 모델의 Bursting과 비교합니다. 권장 성능 모드는 General Purpose입니다. Access point는 별도 POSIX 신원으로 파일 시스템을 공유하지만 PVC별 용량 예약·자동 앱별 과금은 제공하지 않습니다.
변경 제안 전에 기존 lifecycle configuration 전체를 조회합니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the filesystem Region}"
: "${EFS_FILE_SYSTEM_ID:?Set the owned filesystem ID}"
aws efs describe-file-systems --region "$AWS_REGION" --file-system-id "$EFS_FILE_SYSTEM_ID" \
--output json > efs-filesystem-review.json
aws efs describe-lifecycle-configuration --region "$AWS_REGION" \
--file-system-id "$EFS_FILE_SYSTEM_ID" --output json > efs-lifecycle-before.json
```
put-lifecycle-configuration은 파일 시스템 구성을 변경하므로 의도한 기존 transition을 검토한 배열에 모두 보존합니다. 빈 배열은 lifecycle 관리를 비활성화하며 각 policy 객체는 transition 하나를 포함합니다. Archive는 지원 General Purpose/Elastic 구성과 IA보다 나중의 transition이 필요합니다. IA/Archive 접근·최소 보관 기간 요금도 분석하고 기존 정책을 단일30일 예제로 무조건 덮어쓰지 않습니다.
### FSx for Lustre와 비용 할당
Scratch는 재생성 가능한 데이터에만 선택하고 필요한 수명에는 persistent 배포·스토리지를 사용합니다. LZ4가 물리 데이터 크기를 줄일 수 있어도 이미 프로비저닝한 SSD 할당 요금이 자동으로 줄지는 않습니다. 선택한 요금 모델·압축률·처리량/CPU 영향을 평가하세요. S3 repository 통합은 버킷 이름만이 아닌 import/export/release 동작 구성이 필요합니다.
Cost Explorer·Kubernetes 비용 도구에는 과금 데이터 설정·필요한 비용 할당 tag 활성화·PVC/PV와 클라우드 ID의 검증한 매핑이 필요합니다. 네임스페이스 레이블만으로 모든 AWS 요금에 tag가 자동 부여되지는 않습니다. 보존 볼륨·snapshot·요청/전송·관측 데이터 보존 비용을 추적합니다. EC2 Reserved Instance/Compute Savings Plans는 적격 컴퓨팅 비용에 영향을 주며 별도 EBS/EFS/FSx 스토리지 요금을 자동으로 낮추지는 않습니다.
## 스토리지 보안
백엔드·노드/마운트 경로·Kubernetes 제어 영역을 각각 보호합니다. Class 이름·네임스페이스 정책·컨테이너 루트의 읽기 전용 설정만으로 쓰기 가능한 PVC의 내용이 보호되지는 않습니다.
### 데이터 암호화
EBS 암호화는 볼륨 프로비저닝 때 요청하며 StorageClass 변경으로 기존 볼륨이 소급 암호화되지는 않습니다. 아래 신규 class는 암호화된 gp3·지연 topology 바인딩·명시적 보존을 요청합니다. Customer managed key가 필요하면 실제 검토한 kmsKeyId와 key policy·드라이버 grant를 준비하세요. 예시 ARN은 동작하는 키가 아닙니다.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-encrypted
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
parameters:
type: gp3
encrypted: 'true'
csi.storage.k8s.io/fstype: ext4
```
EFS 암호화는 Part1의 검사를 포함한 인프라 절차로 파일 시스템 생성 때 설정합니다. 기존 비암호화 데이터는 mount option이 아닌 암호화 파일 시스템으로의 지원 이전 절차가 필요합니다. FSx for Lustre는 저장 데이터를 자동 암호화합니다. Scratch는 서비스 관리 키를 사용하며 선택 가능한 AWS managed/customer managed KMS 키는 **persistent 파일 시스템**의 선택지입니다. SCRATCH_2 예제에 customer kms-key-id를 전달하지 마세요.
EBS ID를 바인딩된 PV volumeHandle과 맞추고 계정·리전을 확인하여 실제 클라우드 리소스를 조회합니다. Scratch FSx 응답에 선택한 customer key가 없다고 비암호화된 것은 아닙니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the resources Region}"
: "${EBS_VOLUME_ID:?Identify the actual volume from its bound PV}"
: "${EFS_FILE_SYSTEM_ID:?Set the owned EFS filesystem ID}"
: "${FSX_FILE_SYSTEM_ID:?Set the owned FSx filesystem ID}"
aws ec2 describe-volumes --region "$AWS_REGION" --volume-ids "$EBS_VOLUME_ID" \
--query 'Volumes[0].{Id:VolumeId,Encrypted:Encrypted,KmsKeyId:KmsKeyId}' --output json
aws efs describe-file-systems --region "$AWS_REGION" --file-system-id "$EFS_FILE_SYSTEM_ID" \
--query 'FileSystems[0].{Id:FileSystemId,Encrypted:Encrypted,KmsKeyId:KmsKeyId}' --output json
aws fsx describe-file-systems --region "$AWS_REGION" --file-system-ids "$FSX_FILE_SYSTEM_ID" \
--query 'FileSystems[0].{Id:FileSystemId,Type:LustreConfiguration.DeploymentType,KmsKeyId:KmsKeyId}' --output json
```
전송 데이터는 지원 EFS CSI/mount helper의 `tls` 옵션과 실제 마운트 경로를 확인합니다. FSx 전송 암호화는 지원 파일 시스템·클라이언트·인스턴스 구성 조건에 따라 서비스 안내를 따릅니다. **S3 HTTPS/TLS는 전송 보호이고 `aws s3 cp --sse AES256`는 저장 데이터의 SSE-S3 암호화 선택입니다.** 이 flag가 TLS를 켜지는 않습니다. HTTPS endpoint·인증서 검증을 사용하고 secure transport를 요구하는 버킷 정책을 검토하세요. KMS key policy와 전송 인증서 관리는 별개입니다.
### 액세스 제어
Part1에서 준비한 CSI 신원, 즉 컨트롤러의 지원 Pod Identity/IRSA 역할·정확한 드라이버 권한과 필요한 KMS grant를 사용합니다. 관련 없는 eksctl 명령으로 기존 관리형 add-on ServiceAccount를 재생성하거나 모든 노드·앱에 컨트롤러 권한을 옮기지 않습니다. Mountpoint pod-level 신원·EFS IAM mount·POSIX/access-point 신원은 서로 다른 권한 경로입니다.
네트워크는 실제 클라이언트/노드 SG에서 EFS mount target의 TCP2049 접근을 허용합니다. Lustre는 필요한 self/client 통신을 포함하여 클라이언트·파일 서버 사이의 TCP988·1018–1023 규칙이 필요합니다. EFA Lustre에는 지정된 SG 참조 all-traffic 규칙이 필요하며 인터넷 전체 CIDR로 대체할 수 없습니다. SG뿐 아니라 route·DNS·NACL·실제 CSI mount 출발지도 확인하세요. Kubernetes Pod NetworkPolicy가 노드에서 출발한 모든 파일 시스템 연결을 자동 제어하지는 않습니다.
아래 네임스페이스 reader는 PVC 객체를 조회하며 생성·확장·삭제하지 못합니다. 클러스터 범위 PV 조회는 별도로 검토한 ClusterRole이 필요합니다. 어느 Role도 파일 바이트를 직접 제어하지 않습니다. 네임스페이스에서 Pod를 생성할 수 있는 주체는 그 안의 PVC를 마운트할 수 있으므로 Pod 생성·워크로드 신원·POSIX/access point 권한·테넌트 격리도 제어해야 합니다:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: storage-auditor
namespace: storage-demo
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pvc-reader
namespace: storage-demo
rules:
- apiGroups:
- ''
resources:
- persistentvolumeclaims
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pvc-reader
namespace: storage-demo
subjects:
- kind: ServiceAccount
name: storage-auditor
namespace: storage-demo
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: pvc-reader
```
### 파드 보안 컨텍스트
아래 완전한 예제는 Pod 수준 runAsUser/runAsGroup/fsGroup/seccomp와 컨테이너 수준 allowPrivilegeEscalation/capabilities/readOnlyRootFilesystem을 구분합니다. 선언되지 않은 data volume·쓰기 경로가 준비되지 않은 nginx 대신 준비한 암호화 class와 선언한 PVC를 사용합니다. 네임스페이스 정책 버전은 검토한 Kubernetes1.36 예제에 맞추었으므로 실제 클러스터에 적절한 버전을 사용하세요.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: secure-ns
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: secure-data
namespace: secure-ns
spec:
accessModes:
- ReadWriteOnce
storageClassName: ebs-encrypted
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
namespace: secure-ns
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.37.0
command:
- sh
- -c
args:
- test -w /data && sleep 3600
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: secure-data
```
읽기 전용 루트가 `/data`까지 읽기 전용으로 만들지는 않습니다. 이 워크로드는 의도적으로 쓰기 가능한 PVC를 확인합니다. fsGroup 동작은 CSI·파일 시스템에 따라 다르고 EFS access point가 다른 POSIX 신원을 강제할 수 있습니다. SELinux/AppArmor는 실제 노드·런타임 지원과 준비한 정책이 필요하므로 임의 MCS label·profile 이름을 복사하지 마세요. Pod Security Standards는 Pod 구성을 제한하며 클라우드 볼륨 암호화·IAM 권한을 제공하지 않습니다. Host 접근이 필요한 모니터링·CSI node agent는 별도로 검토한 네임스페이스·보안 설계가 필요합니다.
### 보안 정책 적용
PVC 이름 패턴·StorageClass allowlist만으로 EBS 암호화를 입증할 수는 없습니다. 다음 **Kyverno ValidatingPolicy**는 Kyverno1.19.1·CRD로 확인한 served `policies.kyverno.io/v1` API를 사용합니다. 설치된 컨트롤러·CRD와 적절한 admission 관리 주체가 필요합니다. 해당 릴리스에서 이전 ClusterPolicy 예제는 deprecated이므로 API 호환성을 가정하지 말고 계획하여 이전합니다.
```yaml
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: require-declared-ebs-encryption
spec:
validationActions:
- Deny
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups:
- storage.k8s.io
apiVersions:
- v1
resources:
- storageclasses
operations:
- CREATE
- UPDATE
scope: Cluster
validations:
- expression: '!(object.provisioner in [''ebs.csi.aws.com'', ''ebs.csi.eks.amazonaws.com''])
|| (has(object.parameters) && ''encrypted'' in object.parameters && object.parameters[''encrypted'']
== ''true'')'
message: EBS StorageClasses must explicitly request encryption.
```
이 정책은 Auto Mode provisioner를 포함한 **EBS StorageClass 생성·변경의 선언된 암호화 파라미터**를 검사합니다. 기존 AWS 볼륨·정적 PV·snapshot 내용·match 밖의 우회 경로를 검사하지 않습니다. Admission과 함께 StorageClass/PV 관리 권한 제한·백엔드 준수 검사를 사용하세요. 전체 강제 적용 전에 대표 리소스로 검증하고 정책 report·webhook 상태를 확인합니다. 기존 데이터 암호화에는 별도 이전 절차가 필요합니다.
## 스토리지 관리 모범 사례

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-04-eks-storage-part3-4.html)
### 계획과 용량
지연·IOPS/처리량·용량 증가·읽기/쓰기 패턴·동시성·가용성·내구성·RPO/RTO를 구분해 기록합니다. 그 요구로 블록·공유 NFS·병렬 Lustre·객체 접근을 선택합니다. 측정한 여유와 상한이 있는 확장 절차를 사용하며 autoscaler·PVC resize가 앱 복제·임의 용량 축소를 제공하지는 않습니다.
### 백업과 재해 복구
고유 백업 신원·보존 소유자가 있는 지원 schedule을 사용합니다. 고정 이름 snapshot을 반복 생성하는 shell cron은 첫 객체 생성 후 실패하며 daily-backup이라는 일회성 Velero 백업은 schedule이 아닙니다. Part2의 검토한 Velero/CSI 설치·DB 일관성 요구와 함께 다음 명령으로 일일 schedule을 검토용 출력합니다:
```bash
velero schedule create storage-daily --schedule="0 0 * * *" \
--include-namespaces=storage-demo --ttl=720h0m0s -o yaml > storage-schedule-review.yaml
```
검토 후 소유자 경로로 배포하고 누락·실패 백업 경보와 격리 복원을 테스트합니다. S3 백업은 모든 볼륨 바이트 대신 native snapshot을 참조할 수 있습니다. 교차 AZ/리전 복구에는 접근 가능한 데이터·키·driver/storage 매핑·앱 검증이 필요합니다. 통제된 전환 성공까지 원본을 유지하며 schedule만으로 RPO/RTO를 주장하지 말고 실제 복구 시간을 기록합니다.
### Infrastructure as Code와 GitOps
Part1/Part2의 검사를 포함한 파일 시스템 생성 예제 또는 provider/schema 버전·subnet/mount-target/SG·삭제 보호를 검토한 소유자의 Terraform/CloudFormation 모듈을 사용합니다. 파일 시스템만의 Terraform 리소스는 EKS 마운트 경로 전체가 아닙니다. 백업 보존과 IaC destroy/prune 동작을 구분하세요.
Helm values는 chart별 입력입니다. Custom `storage.encrypted: true`는 template이 실제 지원 리소스 필드로 연결하지 않으면 아무 효과가 없습니다. 배포 전에 생성되는 StorageClass/PVC/워크로드를 렌더링·검증합니다. GitOps prune·chart uninstall·클레임 보존 설정은 데이터에 서로 다른 영향을 줄 수 있으므로 리소스별 소유 시스템을 기록하고 폐기 가능한 데이터로 수명 변경을 테스트합니다.
예를 들어 다음 파일 시스템 리소스는 암호화·Elastic 처리량을 요청하고30일 IA 학습 정책을 유지하며 Terraform destroy 방지를 추가합니다. Creation token을 프로젝트별 안정된 값으로 교체하고 provider·계정·리전과 검토한 네트워크·mount target을 구성합니다. prevent_destroy는 Terraform 작업 보호이지 백업·모든 외부 삭제에 대한 보호는 아닙니다:
```hcl
resource "aws_efs_file_system" "example" {
creation_token = "example"
performance_mode = "generalPurpose"
throughput_mode = "elastic"
encrypted = true
lifecycle_policy {
transition_to_ia = "AFTER_30_DAYS"
}
lifecycle {
prevent_destroy = true
}
tags = {
Name = "ExampleFileSystem"
}
}
```
### 지속적인 검토
워크로드 변화에 따라 병목·프로비저닝 한도·보존 비용·보안 제어를 검토합니다. 소유권·백업/복구·삭제 영향을 확인하기 전까지 정리 보고서는 읽기 전용으로 유지합니다. 자동화 전에 경보·상한이 있는 변경 제안을 사용하세요. 본문 예제는 로컬 검사한 참고 구성이며 실제 백엔드 성능·admission 배포·복구는 대상 환경에서 검증해야 합니다.
## 결론
이 문서에서는 Amazon EKS 스토리지의 모니터링, 문제 해결, 비용 최적화 및 보안에 대해 알아보았습니다. 효과적인 스토리지 관리는 EKS 클러스터의 성능, 안정성 및 비용 효율성을 보장하는 데 중요합니다.
스토리지 요구사항은 애플리케이션마다 다르므로, 워크로드의 특성을 이해하고 적절한 스토리지 솔루션을 선택하는 것이 중요합니다. 또한, 정기적인 모니터링, 문제 해결, 비용 최적화 및 보안 검토를 통해 스토리지 리소스를 효과적으로 관리해야 합니다.
## 참고 자료
- [Amazon EKS 스토리지 모범 사례](https://docs.aws.amazon.com/eks/latest/best-practices/storage.html)
- [Kubernetes 스토리지 문제 해결](https://kubernetes.io/docs/tasks/debug-application-cluster/debug-application/#debugging-pods)
- [Kubernetes 스토리지 보안](https://kubernetes.io/docs/concepts/security/)
- [EBS CloudWatch metrics](https://docs.aws.amazon.com/ebs/latest/userguide/using_cloudwatch_ebs.html)
- [FSx Lustre metric dimensions](https://docs.aws.amazon.com/fsx/latest/LustreGuide/fs-metrics.html)
- [EFS performance modes](https://docs.aws.amazon.com/efs/latest/ug/performance.html)
- [EFS lifecycle API](https://docs.aws.amazon.com/efs/latest/APIReference/API_PutLifecycleConfiguration.html)
- [FSx encryption at rest](https://docs.aws.amazon.com/fsx/latest/LustreGuide/encryption-at-rest.html)
- [FSx network access](https://docs.aws.amazon.com/fsx/latest/LustreGuide/limit-access-security-groups.html)
- [S3 encryption at rest](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html)
- [Kyverno CEL migration](https://kyverno.io/docs/guides/migration-to-cel/)
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/04-eks-storage-part3-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/05-eks-security
----------------------------------------
# Amazon EKS 보안
> **지원 버전**: EKS 표준 지원 1.34–1.36; 연장 지원 1.31–1.33 (2026년 9월 11일 확인)
> **마지막 업데이트**: 2026년 9월 11일
Amazon EKS(Elastic Kubernetes Service)에서 워크로드를 안전하게 실행하기 위해서는 다양한 보안 계층과 모범 사례를 이해하고 구현해야 합니다. 이 문서에서는 EKS 클러스터의 보안을 강화하기 위한 주요 개념, 구성 요소 및 모범 사례를 다룹니다.
## 목차
1. [EKS 보안 개요](#eks-보안-개요)
2. [보안 실무](#보안-실무)
3. [IAM 및 인증](#iam-및-인증)
4. [OIDC Provider 심화](#oidc-provider-심화)
5. [EKS Pod Identity](#eks-pod-identity)
6. [Cluster Endpoint 접근 제어](#cluster-endpoint-접근-제어)
7. [네트워크 보안](#네트워크-보안)
8. [포드 보안](#포드-보안)
9. [Bottlerocket 및 읽기 전용 OS](#bottlerocket-및-읽기-전용-os)
10. [IAM 권한 경계](#iam-권한-경계)
11. [암호화 및 비밀 관리](#암호화-및-비밀-관리)
12. [컴플라이언스 및 감사](#컴플라이언스-및-감사)
13. [보안 모니터링 및 탐지](#보안-모니터링-및-탐지)
14. [EKS 보안 모범 사례](#eks-보안-모범-사례)
15. [금융 서비스를 위한 EKS 보안 고려사항](#금융-서비스를-위한-eks-보안-고려사항)
## EKS 보안 개요
인프라·클러스터 접근·워크로드 제어를 구분합니다. AWS는 컨트롤 플레인을 관리하며 노드/OS 책임은 EC2 자체/관리형 노드·Fargate·Auto Mode·Hybrid Nodes에 따라 달라집니다. 앱 이미지·신원·데이터 처리·워크로드 정책은 여전히 고객 책임입니다. 일반 EC2 노드 그림이 고객이 Auto Mode/Fargate 호스트 OS를 패치한다는 뜻은 아닙니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-05-eks-security-0.html)
## 보안 실무
범위가 명시된 여러 제어를 사용합니다. 도구 하나로 zero trust·워크로드 인증이 완성되지는 않습니다. 호출자·워크로드 신원을 확인하고 대상 작업을 허용하며 네트워크 경로와 위협 모델에 맞는 근거를 관리합니다.
### 신원과 네트워크 제어
IRSA·Pod Identity는 워크로드 AWS 자격 증명, NetworkPolicy는 지원 네트워크 필터링, admission 엔진은 설정한 Kubernetes 요청 검사를 제공합니다. 호환되는 유지보수 중인 서비스 메시로 mTLS·앱 트래픽 정책을 추가할 수 있습니다. AWS App Mesh는2026년9월30일 지원 종료이므로 신규 기본 권장 대상이 아니라 기존 배포의 이전 계획이 필요합니다.
### 공급망 보안
SLSA 같은 검토한 빌드·provenance 절차, SBOM 구성 요소 목록, 대상 취약점 검사와 의도한 서명자 신원에 대한 artifact 서명 검증을 사용합니다. Syft는 SBOM 도구, Grype는 취약점 scanner입니다. ECR/Inspector·다른 scanner의 범위·업데이트 요구를 확인하세요. 서명·검사된 이미지가 무해한 코드의 증거는 아닙니다. 빌드 신원·저장소·admission 구성도 보호합니다.
현재 ECR은 AWS Signer 관리형 이미지 서명과 Notation 수동 서명을 지원합니다. 서명과 admission 검증은 별도 단계이며 registry filter·서명 profile 권한·검증기 신뢰를 대상 pipeline과 맞춰야 합니다.
### 런타임 탐지와 Policy as Code
GuardDuty EKS Protection은 독립적인 EKS audit-log stream을 분석합니다. Runtime Monitoring은 별도 agent 기반 기능이며 현재 EC2·Auto Mode EKS를 지원하고 플랫폼/agent 조건이 있으며 EKS Fargate·Hybrid Nodes는 제외됩니다. CloudWatch 감사 로그 전달은 별도 설정입니다. Security Hub CSPM은 설정한 제어를 평가하고 Security Hub는 findings를 연계할 수 있습니다. 앱 권한·모든 규제 요구 평가를 대체하지는 않습니다.
필요한 kernel·controller·API·metadata 통합을 갖춘 Falco·Gatekeeper·Kyverno 같은 지원 런타임/정책 도구를 사용합니다. gVisor/Kata 같은 추가 sandbox runtime에는 호환 노드·런타임 설계가 필요하며 모든 EKS 컴퓨팅 경로에서 쓸 수는 없습니다. 정책·이미지·OS 강화는 특정 위험을 줄이며 모든 탈출·악성 작업을 불가능하게 보장하지는 않습니다.
Policy-as-code 도구의 단계도 다릅니다. Gatekeeper/Kyverno는 Kubernetes admission, CloudFormation Guard·Sentinel은 인프라 변경, AWS Config는 지원되는 배포 리소스 구성을 평가할 수 있습니다. Detective 같은 조사 도구는 설정한 데이터 소스에 의존합니다. 이를 런타임 방지 기능과 구분합니다.
## IAM 및 인증
| 신원·제어 | 목적 |
|---|---|
| 사람·자동화 IAM 주체 | 설정한 IAM 매핑/access-entry 경로로 클러스터 접근 인증 |
| Kubernetes RBAC·EKS access policy | Kubernetes 작업 허용; 허용 권한은 합산됨 |
| 외부 OIDC identity provider | Client·claim 설정이 별도인 Kubernetes API 사용자 로그인 |
| EKS cluster IAM role | EKS 서비스의 클러스터 관련 AWS API 호출 |
| EC2 node IAM role | Bootstrap·필요한 node agent AWS 작업 |
| IRSA·EKS Pod Identity 역할 | 앱에 임시 AWS 자격 증명 제공 |
| Kubernetes ServiceAccount token | RBAC 권한에 따른 Pod의 Kubernetes API 인증 |
IRSA용 IAM OIDC provider와 Kubernetes 사용자 로그인을 위해 연결한 외부 OIDC provider는 다릅니다. 앱 AWS 권한이 Kubernetes API 권한을 자동 부여하지는 않습니다.
### 클러스터 역할과 생성 호출자
일반 EKS cluster role은 EKS 서비스를 신뢰합니다. 다음은 그 신뢰 관계이며 개발자 사용자의 권한 정책이 아닙니다:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "eks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
```
이 역할에는 서비스 작업에 맞는 AmazonEKSClusterPolicy 또는 지원 custom policy가 필요하며 Auto Mode에는 추가 역할·정책 요구가 있습니다. 생성 호출자에게는 선택한 구성의 작업 권한과 대상 역할 전달 권한이 별도로 필요합니다. 현재 권한 참조에서 CreateCluster는 resource ARN 범위를 지원하지 않으므로 지원 request condition을 사용하고 ARN 범위를 지원하는 작업·PassRole은 범위를 제한합니다. 개발자 역할에 AmazonEKSClusterPolicy를 연결해도 Kubernetes 앱 접근 권한이 생기지는 않습니다.
### Access Entry와 네임스페이스 권한
IAM 클러스터 접근에는 지원 access-entry API를 우선 검토합니다. 현재 모드를 조회하고 이전 전에 관리자·노드 매핑을 보존합니다. CONFIG_MAP에서 API_AND_CONFIG_MAP/API로의 전환은 자유롭게 되돌리는 스위치가 아닙니다. 노드 역할이 빠진 짧은 예제로 aws-auth를 덮어쓰지 마세요. API-only 선택 전에 전체 신원·정책·노드·복구 경로를 검토합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the existing cluster name}"
: "${AWS_REGION:?Set its Region}"
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query 'cluster.{Name:name,Status:status,Endpoint:endpoint,Access:accessConfig}' --output json
```
다음은 승인된 기존 IAM 개발자 역할·access entry가 이미 활성화된 클러스터·플랫폼 운영자의 권한을 전제로 합니다. 소유자를 통해 전용 네임스페이스를 준비합니다. Pod Security Admission 버전은 검토한 EKS1.36 예제에 맞추었으므로 대상 클러스터에 적절한 버전을 선택하세요:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: security-demo
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36
```
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the verified cluster name}"
: "${AWS_REGION:?Set its Region}"
: "${DEVELOPER_ROLE_ARN:?Set a prepared IAM role ARN, not an STS session ARN}"
MODE=$(aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster.accessConfig.authenticationMode --output text)
case "$MODE" in
API|API_AND_CONFIG_MAP) ;;
*) echo "Access entries are not enabled; review the migration first"; exit 1 ;;
esac
aws eks list-access-entries --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--output json > security-access-entries.json
python3 - "$DEVELOPER_ROLE_ARN" <<'PY'
import json, sys
with open("security-access-entries.json") as stream:
existing = json.load(stream)["accessEntries"]
if sys.argv[1] in existing:
raise SystemExit("Entry already exists; inspect its groups/policies instead of overwriting")
PY
aws eks create-access-entry --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--principal-arn "$DEVELOPER_ROLE_ARN" --type STANDARD \
--kubernetes-groups security-demo-developers
```
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: developer
namespace: security-demo
rules:
- apiGroups:
- ''
resources:
- pods
verbs:
- get
- list
- watch
- apiGroups:
- apps
resources:
- deployments
verbs:
- get
- list
- watch
- create
- update
- patch
- apiGroups:
- batch
resources:
- jobs
verbs:
- get
- list
- watch
- create
- update
- patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer
namespace: security-demo
subjects:
- kind: Group
name: security-demo-developers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: developer
apiGroup: rbac.authorization.k8s.io
```
Entry는 IAM 주체를 그룹에 매핑하고 RoleBinding은 표시한 네임스페이스 권한을 부여합니다. 그룹 이름만으로 네임스페이스 경계가 생기지는 않습니다. 워크로드 컨트롤러 생성 권한으로 네임스페이스의 ServiceAccount·Secret·PVC를 사용하는 Pod가 생길 수 있습니다. 테넌트를 구분하고 적절한 admission·소유권 제어로 워크로드·ServiceAccount 사용을 제한합니다.
별도로 검토한 viewer entry에는 custom RBAC 대신 EKS access policy를 사용할 수 있습니다. 아래 namespace view를 추가하기 전에 현재 그룹·연결 정책을 조회합니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the verified cluster name}"
: "${AWS_REGION:?Set its Region}"
: "${VIEWER_ROLE_ARN:?Set the IAM principal of a prepared, reviewed access entry}"
aws eks list-associated-access-policies --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--principal-arn "$VIEWER_ROLE_ARN"
aws eks associate-access-policy --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--principal-arn "$VIEWER_ROLE_ARN" \
--policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy \
--access-scope type=namespace,namespaces=security-demo
```
View 추가가 더 넓은 RBAC/access-policy 권한을 취소하지는 않습니다. 변경 전파에 시간이 걸릴 수 있습니다. 의도한 실제 IAM 로그인으로 검증하며 kubectl --as는 Kubernetes impersonation/RBAC 검사이지 IAM access-policy 경로의 증거가 아닙니다. Kubeconfig는 클러스터·자격 증명 경로를 가리킬 뿐 자체적으로 권한을 부여하지 않습니다.
IAM eks:DescribeCluster/ListClusters는 AWS 관리·검색 작업용입니다. eks:AccessKubernetesApi는 콘솔 조회 권한입니다. eks:namespaces 조건은 access-policy association 요청을 필터링하며 kubectl API 호출의 범용 네임스페이스 필터가 아닙니다.
## OIDC Provider 심화
EKS는 클러스터 OIDC issuer와 공개 서명 키를 게시합니다. Endpoint만으로 IAM OIDC provider가 생성되거나 역할 전환이 허용되지는 않습니다. IRSA는 역할 계정의 대상 IAM OIDC provider, 올바른 issuer/subject/audience 신뢰 조건과 호환 SDK가 필요합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-05-eks-security-3.html)
### IRSA 토큰과 역할 전환
IRSA webhook은 대상 Pod에 projected token·role 설정을 추가합니다. SDK는 AssumeRoleWithWebIdentity로 web-identity token을 STS와 교환합니다. STS가 issuer·서명·토큰·역할 신뢰를 검증하면 임시 AWS 자격 증명을 반환하며 앱은 허용된 AWS 작업에 이를 사용합니다. Kubernetes의 projected token 갱신과 SDK의 AWS 자격 증명 갱신은 수명이 다릅니다.
다음은 오래되어 만료된 시각과 placeholder 신원을 사용한 **decoded payload 구조 예제**입니다. 서명된 토큰·인증 테스트가 아닙니다. STS audience는 IRSA 경로용이며 Pod Identity는 다른 audience를 사용합니다:
```json
{
"aud": [
"sts.amazonaws.com"
],
"exp": 1234567890,
"iat": 1234567800,
"iss": "https://oidc.eks.us-west-2.amazonaws.com/id/REPLACE_WITH_CLUSTER_ISSUER_ID",
"kubernetes.io": {
"namespace": "security-demo",
"pod": {
"name": "irsa-read-check-example",
"uid": "example-pod-uid"
},
"serviceaccount": {
"name": "irsa-reader",
"uid": "example-serviceaccount-uid"
}
},
"sub": "system:serviceaccount:security-demo:irsa-reader"
}
```
JSON decode 성공만이 아니라 issuer·audience·만료·예상 subject를 검증합니다. 실제 토큰·AWS secret access key·session token을 예제·로그에 출력하지 않습니다. 역할이 명시적으로 허용한 여러 ServiceAccount·issuer를 신뢰할 수 있으며 IRSA가 ServiceAccount당 역할 하나를 강제하지는 않습니다.
### Discovery와 JWKS 조회
실제 클러스터가 반환한 issuer를 사용합니다. 다음 진단은 공개 discovery/JWKS를 조회하고 discovery issuer·HTTPS scheme을 확인합니다. 워크로드 토큰·IAM 권한 검증은 아닙니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the verified cluster}"
: "${AWS_REGION:?Set its Region}"
OIDC_URL=$(aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster.identity.oidc.issuer --output text)
case "$OIDC_URL" in https://*) ;; *) echo "Unexpected issuer URL"; exit 1 ;; esac
curl --fail --silent --show-error --proto '=https' --connect-timeout 5 --max-time 20 \
"${OIDC_URL%/}/.well-known/openid-configuration" > oidc-discovery.json
JWKS_URI=$(python3 - "$OIDC_URL" <<'PY'
import json, sys, urllib.parse
with open("oidc-discovery.json") as stream:
doc = json.load(stream)
if doc["issuer"] != sys.argv[1]:
raise SystemExit("Discovery issuer does not match the cluster issuer")
uri = doc["jwks_uri"]
parsed = urllib.parse.urlparse(uri)
if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password:
raise SystemExit("JWKS must be an HTTPS URL without embedded credentials")
print(uri)
PY
)
curl --fail --silent --show-error --proto '=https' --connect-timeout 5 --max-time 20 \
"$JWKS_URI" > oidc-jwks.json
python3 - <<'PY'
import json
with open("oidc-jwks.json") as stream:
keys = json.load(stream).get("keys")
if not isinstance(keys, list) or not keys or not all(isinstance(k, dict) and "kty" in k for k in keys):
raise SystemExit("Unexpected JWKS response")
print("Fetched", len(keys), "public keys; no token signature was validated")
PY
```
자동 validator는 Cache-Control에 따라 키를 캐시하고 검토한 JWT 라이브러리로 서명 키 교체·알 수 없는 kid를 처리해야 합니다. EKS OIDC 서명 키는7일마다 교체됩니다. 현재 EKS는 인터넷 egress가 없는 validator용 클러스터 OIDC discovery/JWKS PrivateLink interface endpoint인 `com.amazonaws.region-code.oidc-eks`도 지원합니다. 해당 endpoint의 리전·DNS 조건을 확인하세요. EKS 관리 API endpoint와는 다른 서비스입니다.
### 범위를 제한한 IRSA 예제
소유자를 통해 IAM OIDC provider·역할·버킷을 준비합니다. 계정·전체 issuer hostname/path·role ARN을 일관되게 교체하세요. IPv6 클러스터는 dual-stack issuer hostname을 사용할 수 있습니다. 다음은 security-demo/irsa-reader와 STS audience만 연결한 신규 역할 신뢰 예제이며 기존 역할의 다른 신뢰 문장을 대체하는 정책이 아닙니다:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/REPLACE_WITH_CLUSTER_ISSUER_ID"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-west-2.amazonaws.com/id/REPLACE_WITH_CLUSTER_ISSUER_ID:sub": "system:serviceaccount:security-demo:irsa-reader",
"oidc.eks.us-west-2.amazonaws.com/id/REPLACE_WITH_CLUSTER_ISSUER_ID:aud": "sts.amazonaws.com"
}
}
}
]
}
```
읽기 정책은 특정 버킷 접두사만 허용합니다. 객체가 customer managed 암호화 키를 사용하면 필요한 KMS 권한만 추가하고 버킷·endpoint 정책도 확인합니다. 이 접두사 하나의 예제에 계정 전체 AmazonS3ReadOnlyAccess가 필요한 것은 아닙니다:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::replace-with-owned-security-bucket",
"Condition": {
"StringLike": {
"s3:prefix": [
"security-demo/",
"security-demo/*"
]
}
}
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::replace-with-owned-security-bucket/security-demo/*"
}
]
}
```
security-demo 네임스페이스·역할 준비 후 다음 ServiceAccount와 제한 시간의 목록 조회 Job을 생성합니다. 공식 AWS CLI 이미지에는 client가 있지만 임의 amazonlinux:2 이미지에 있다고 보장되지 않습니다. 버킷·리전·역할 값을 교체하세요. Job은 비밀 값 출력 없이 접두사 목록을 조회하며 성공이 모든 앱 GetObject/KMS 권한의 증거는 아닙니다.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: irsa-reader
namespace: security-demo
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/SecurityDemoIRSAReader
---
apiVersion: batch/v1
kind: Job
metadata:
name: irsa-read-check
namespace: security-demo
spec:
backoffLimit: 0
activeDeadlineSeconds: 120
template:
spec:
serviceAccountName: irsa-reader
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: reader
image: public.ecr.aws/aws-cli/aws-cli:2.36.43
command:
- aws
args:
- s3api
- list-objects-v2
- --bucket
- replace-with-owned-security-bucket
- --prefix
- security-demo/
- --max-items
- '5'
env:
- name: AWS_REGION
value: us-west-2
- name: AWS_EC2_METADATA_DISABLED
value: 'true'
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: private-home
mountPath: /root
- name: tmp
mountPath: /tmp
volumes:
- name: private-home
emptyDir: {}
- name: tmp
emptyDir: {}
nodeSelector:
kubernetes.io/os: linux
```
## EKS Pod Identity
EKS Pod Identity는 SDK의 container credential provider와 EKS Auth를 통해 임시 AWS 자격 증명을 제공합니다. Association은 클러스터/네임스페이스/ServiceAccount와 IAM 역할의 매핑이며 ServiceAccount 생성·Kubernetes RBAC 권한 부여 기능은 아닙니다.
### IRSA와 비교
| 특성 | IRSA | EKS Pod Identity |
|---|---|---|
| 신뢰 | IAM OIDC provider와 issuer/subject/audience 조건 | pods.eks.amazonaws.com 역할 신뢰와 association |
| 역할 재사용 | IAM 정책 한도 안에서 여러 subject/issuer 허용 가능 | 적절한 association·신뢰 조건으로 재사용 가능 |
| 내장 Kubernetes session tag | 표준 EKS IRSA 경로가 자동 제공하지 않음 | 기본 활성화되며 의도적으로 비활성화할 수 있음 |
| 자격 증명 provider | STS와 web identity 교환 | EKS Auth 기반 container credential endpoint |
| 교차 계정 | 역할 계정의 직접 OIDC 신뢰 또는 role chaining | 같은 계정의 association 역할과 선택적 target-role chaining |
| 갱신 | Kubernetes가 token, SDK가 AWS 자격 증명 갱신 | Kubernetes가 projected token, agent/service cache·SDK가 AWS 자격 증명 처리 |
Session tag는 assumed-role session의 속성이며 앱이 생성하는 모든 AWS 리소스를 자동으로 태깅하지 않습니다. 설정이 간단해져도 IAM·네트워크·SDK·앱 검증 요구가 없어지지는 않습니다.
### Agent와 자격 증명 흐름
Association이 있는 새 Pod에는 EKS가 audience가 pods.eks.amazonaws.com인 token·token-file 환경 변수·container credentials URI를 주입합니다. SDK가 agent endpoint(일반적으로169.254.170.23/v1/credentials)를 호출하고 agent는 **EKS Auth의 AssumeRoleForPodIdentity**를 호출하여 반환된 자격 증명을 SDK에 제공합니다. 모든 앱 요청을 투명하게 가로채는 방식이 아니며 앱은 그 자격 증명으로 AWS 서비스에 직접 요청합니다.
지원되는 일반 EC2 노드는 Pod Identity Agent add-on/DaemonSet을 사용합니다. Auto Mode는 관리형 노드의 일부로 기능을 제공합니다. Hybrid Nodes는 문서화된 OS·agent·노드 자격 증명 설정으로 지원하므로 일반 EC2 agent 구성을 그대로 적용하지 않습니다. EKS Fargate는 이 Pod Identity agent 경로를 지원하지 않습니다. 실제 컴퓨팅 지원·노드 EKS Auth 권한·네트워크를 확인하세요.
### 역할과 Association 준비
일반 관리형 agent 설치는 소유자를 통해 실제 클러스터 버전·호환 add-on catalog를 조회합니다. 이전v1.0.0 build를 하드코딩하거나 Auto Mode·다른 소유자의 설치에 agent를 중복 설치하지 마세요:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the existing cluster}"
: "${AWS_REGION:?Set its Region}"
EKS_VERSION=$(aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster.version --output text)
aws eks describe-addon-versions --addon-name eks-pod-identity-agent \
--kubernetes-version "$EKS_VERSION" --region "$AWS_REGION" --output json
```
앞의 범위가 제한된 S3 읽기 정책과 다음 신뢰 관계로 같은 계정의 IAM 역할을 준비합니다. 계정·클러스터 값을 함께 교체하세요. 이 예제는 기본 session tag에 의존하므로 tag를 비활성화하면 조건을 재검토해야 합니다. 호출자에는 해당 association 권한·대상 역할의 iam:PassRole이 필요합니다:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "pods.eks.amazonaws.com"
},
"Action": [
"sts:AssumeRole",
"sts:TagSession"
],
"Condition": {
"StringEquals": {
"aws:RequestTag/eks-cluster-arn": "arn:aws:eks:us-west-2:111122223333:cluster/my-cluster",
"aws:RequestTag/kubernetes-namespace": "security-demo",
"aws:RequestTag/kubernetes-service-account": "podid-reader"
}
}
}
]
}
```
다음 신규 association 예제는 기존 association을 덮어쓰지 않습니다. IAM 역할·agent·Kubernetes 계정을 생성하는 명령은 아닙니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the verified cluster}"
: "${AWS_REGION:?Set its Region}"
: "${POD_ID_ROLE_ARN:?Set the prepared same-account role matching the trust example}"
aws eks list-pod-identity-associations --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--namespace security-demo --service-account podid-reader --output json > podid-associations-before.json
python3 - <<'PY'
import json
with open("podid-associations-before.json") as stream:
existing = json.load(stream)["associations"]
if existing:
raise SystemExit("Association exists; review its owner/configuration before changing it")
PY
aws eks create-pod-identity-association --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--namespace security-demo --service-account podid-reader --role-arn "$POD_ID_ROLE_ARN"
```
Association 전파 후 ServiceAccount·새 제한 시간 테스트 Job을 생성합니다. IAM 역할 신뢰·네임스페이스·계정 이름·버킷 접두사를 맞추세요. Association보다 먼저 생성된 Pod는 주입 설정을 받기 위해 재생성이 필요할 수 있습니다. 실제 선택한 provider·대표 권한을 검증하고 자격 증명·token 파일은 출력하지 않습니다.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: podid-reader
namespace: security-demo
---
apiVersion: batch/v1
kind: Job
metadata:
name: podid-read-check
namespace: security-demo
spec:
backoffLimit: 0
activeDeadlineSeconds: 120
template:
spec:
serviceAccountName: podid-reader
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: reader
image: public.ecr.aws/aws-cli/aws-cli:2.36.43
command:
- aws
args:
- s3api
- list-objects-v2
- --bucket
- replace-with-owned-security-bucket
- --prefix
- security-demo/
- --max-items
- '5'
env:
- name: AWS_REGION
value: us-west-2
- name: AWS_EC2_METADATA_DISABLED
value: 'true'
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: private-home
mountPath: /root
- name: tmp
mountPath: /tmp
volumes:
- name: private-home
emptyDir: {}
- name: tmp
emptyDir: {}
nodeSelector:
kubernetes.io/os: linux
```
### 교차 계정과 캐시 고려 사항
Association 역할은 EKS 클러스터 계정에 있어야 합니다. 선택적 target IAM role은 다른 계정에 둘 수 있으며 양쪽 신뢰·assume-role 권한으로 같은 계정의 역할에서 target으로 chaining합니다. 임의의 외부 역할을 association 역할로 직접 연결할 수 있다는 뜻은 아닙니다.
현재 target-role 안내는 target이 없으면6시간, 있으면59분 자격 증명 캐시를 설명합니다. Association 변경이 캐시를 초기화하지 않으며 Pod 재생성으로 새 구성을 더 빨리 받을 수 있습니다. 실제 앱의 갱신·취소 동작을 확인하세요. 새 association session-policy 옵션은 session tag 비활성화가 필요하고 target role이 있으면 그 권한을 제한합니다. 해당 tag가 필요한 신뢰 조건과 검토 없이 조합하지 않습니다.
### IRSA에서 Pod Identity로 마이그레이션
이전 준비 전에 기존 IRSA 신뢰·ServiceAccount 구성을 보존합니다. 역할 신뢰를 Pod Identity 서비스 Principal만으로 덮어쓰면 기존 소비자의 자격 증명 갱신이 실패할 수 있습니다. 다음은 구성 기록만 수행합니다:
```bash
set -euo pipefail
: "${IRSA_ROLE_NAME:?Set the existing role whose trust must be preserved}"
: "${WORKLOAD_NAMESPACE:?Set the workload namespace}"
: "${SERVICE_ACCOUNT:?Set the existing application ServiceAccount}"
aws iam get-role --role-name "$IRSA_ROLE_NAME" \
--query Role.AssumeRolePolicyDocument --output json > irsa-trust-before.json
kubectl -n "$WORKLOAD_NAMESPACE" get serviceaccount "$SERVICE_ACCOUNT" \
-o yaml > irsa-serviceaccount-before.yaml
```
1. 실제 컴퓨팅 지원·호환 SDK/container credential provider·Pod Identity agent 또는 내장 기능·준비한 IAM 역할 권한을 확인합니다.
2. 소유자 경로로 검토한 Pod Identity 신뢰 문장을 기존 정책에 병합하고 모든 소비자가 이전될 때까지 IRSA issuer·subject·audience 조건과 다른 유효한 문장을 유지합니다.
3. 범위가 제한된 association과 의도한 Pod Identity 신원 경로만 사용하는 별도 ServiceAccount의 canary를 준비합니다. 검증 전에 운영 IRSA annotation을 지우거나 운영 Deployment를 재시작하지 않습니다.
4. 토큰·비밀 값을 출력하지 않고 실제 선택된 credential provider와 필요한 AWS 접근을 검증합니다. Association 생성 후에도 SDK 체인의 앞선 신원이 계속 사용될 수 있습니다. 같은 IAM 역할이 표시되는 get-caller-identity 성공만으로 IRSA·Pod Identity를 구별할 수는 없습니다.
5. 통제된 rollout으로 대상 워크로드를 이전하고 주입 설정에 필요한 Pod 재생성과 앱 동작·갱신을 확인합니다. ServiceAccount annotation 변경이 기존 Pod 환경을 다시 작성하지는 않습니다.
6. 워크로드 구성을 되돌릴 검증한 경로를 유지합니다. 모든 남은 소비자를 조사하고 이전 완료를 확인한 뒤에만 이전 IRSA 신뢰를 제거합니다.
대상 환경에서 검증할 이전 절차이며 이번 문서 검토에서 실제 이전을 실행했다는 뜻이 아닙니다.
## Cluster Endpoint 접근 제어
Endpoint 구성은 네트워크 도달성을 정하며 API 작업에는 별도로 인증·권한이 필요합니다. 운영 요구로 접근 경로를 선택하고 도구 기본값을 가정하지 말고 실제 설정을 조회합니다.
| Public | Private | 네트워크 동작 |
|---|---|---|
| 활성화 | 비활성화 | 클라이언트·노드가 public 주소에 도달해야 하며 CIDR 제한에는 실제 public egress 출발지가 필요 |
| 비활성화 | 활성화 | 승인된 연결 네트워크를 포함한 VPC/private 경로·DNS·SG 규칙 필요 |
| 활성화 | 활성화 | 클러스터 VPC 출발 요청은 private 경로, 허용된 외부 클라이언트는 public 경로 사용 |
Public 주소라는 이유만으로 EC2–EKS 트래픽이 AWS 네트워크 밖으로 나간다고 단정하지 않습니다. Private 접근이 호출자 권한·VPN/DNS·복구 경로를 자동 구성하지도 않습니다. EKS는 접근 모드 하나 이상이 필요합니다.
### 기존 구성 조회와 보존
관련 블록은 같은 shell에서 실행하고 새 검토 디렉터리를 보존합니다. Kubernetes 로그인과 별도로 endpoint 설정을 조회·복구할 AWS 관리 신원을 준비합니다. 다음은 복구용으로 변경 가능한 endpoint 접근 필드만 기록하며 다른 VPC 설정·아래의 일방향 egress 모드까지 복원하려는 절차가 아닙니다.
```bash
set -euo pipefail
umask 077
: "${CLUSTER_NAME:?Set the existing cluster name}"
: "${AWS_REGION:?Set its Region}"
ENDPOINT_REVIEW_DIR=$(mktemp -d -t eks-endpoint-review.XXXXXX)
printf 'Review records: %s\n' "$ENDPOINT_REVIEW_DIR"
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster --output json > "$ENDPOINT_REVIEW_DIR/cluster-before.json"
python3 - "$ENDPOINT_REVIEW_DIR" "$CLUSTER_NAME" <<'PY'
import json, pathlib, sys, urllib.parse
root = pathlib.Path(sys.argv[1])
cluster = json.loads((root / "cluster-before.json").read_text())
if cluster["name"] != sys.argv[2] or cluster["status"] != "ACTIVE":
raise SystemExit("Unexpected cluster or cluster is not ACTIVE")
endpoint = urllib.parse.urlparse(cluster["endpoint"])
if endpoint.scheme != "https" or not endpoint.hostname or endpoint.username or endpoint.password:
raise SystemExit("Unexpected Kubernetes endpoint")
(root / "endpoint-host.txt").write_text(endpoint.hostname + "\n")
config = cluster["resourcesVpcConfig"]
before = {k: config[k] for k in ["endpointPublicAccess", "endpointPrivateAccess", "publicAccessCidrs"] if k in config}
(root / "endpoint-before.json").write_text(json.dumps(before, indent=2) + "\n")
(root / "endpoint-private.json").write_text(json.dumps({"endpointPublicAccess": False, "endpointPrivateAccess": True}, indent=2) + "\n")
(root / "ssm-remote-host.json").write_text(json.dumps({"host": [endpoint.hostname], "portNumber": ["443"], "localPortNumber": ["6443"]}, indent=2) + "\n")
print(json.dumps(before, indent=2))
PY
```
### Public 주소 경로 제한
Public allowlist에는 승인된 실제 public NAT/VPN/사무실 egress 주소를 넣어야 합니다.10.0.0.0/8 같은 private 범위는 public 출발지 주소를 나타내지 않습니다.203.0.113.0/24 같은 문서용 범위도 동작하는 사무실 네트워크가 아닌 placeholder입니다.
PUBLIC_EGRESS_CIDRS를 네트워크 소유자가 제공한 JSON 배열로 설정합니다. 이 제한된 예제는 IPv4 CIDR과 명백한 private/default/문서용 범위를 검사하지만 소유권·현재 클라이언트 포함 여부를 입증하지 않습니다. 범위·quota·클러스터 IP family 지원을 확인하세요. Dual-stack/IPv6에는 별도로 검증한 allowlist가 필요합니다.
```bash
set -euo pipefail
: "${ENDPOINT_REVIEW_DIR:?Run the inspection step in this shell first}"
: "${PUBLIC_EGRESS_CIDRS:?Set a JSON array of approved actual public IPv4 egress CIDRs}"
python3 - "$ENDPOINT_REVIEW_DIR" "$PUBLIC_EGRESS_CIDRS" <<'PY'
import ipaddress, json, pathlib, sys
values = json.loads(sys.argv[2])
if not isinstance(values, list) or not values:
raise SystemExit("Provide a nonempty reviewed CIDR list")
cidrs = []
for value in values:
network = ipaddress.ip_network(value, strict=True)
if network.version != 4 or network.prefixlen == 0 or not network.is_global or network.is_multicast or network.is_reserved:
raise SystemExit("This IPv4 example requires actual public egress CIDRs, not private/default/documentation ranges")
cidrs.append(str(network))
if len(cidrs) != len(set(cidrs)):
raise SystemExit("Remove duplicate CIDRs")
desired = {"endpointPublicAccess": True, "endpointPrivateAccess": True, "publicAccessCidrs": cidrs}
path = pathlib.Path(sys.argv[1]) / "endpoint-public-restricted.json"
path.write_text(json.dumps(desired, indent=2) + "\n")
print(json.dumps(desired, indent=2))
PY
```
계획한 public-restricted 구성은 private 접근도 활성화합니다. Private 접근이 없으면 노드·Fargate의 public egress 주소도 허용해야 하며 누락 시 API 통신이 실패할 수 있습니다. ENDPOINT_CHANGE=public-restricted 선택 전에 생성 JSON을 검토합니다.
### Private 연결 경로

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-05-eks-security-5.html)

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-05-eks-security-6.html)

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-05-eks-security-7.html)
Route·DNS·SG·권한이 구성되면 VPN·Direct Connect/TGW·승인된 관리 호스트를 경로로 사용할 수 있습니다. Client VPN 생성 명령 하나로 subnet association·권한 규칙·route·인증서·연결 로그가 준비되지는 않습니다. AWS CLI 이미지 Deployment도 SSM 관리 bastion·kubectl·SSM Agent를 자동 생성하지 않습니다.
SSM remote-host forwarding은 SSM Agent3.1.1374.0 이상인 관리 호스트·Session Manager IAM/네트워크 접근·로컬 plugin·private EKS endpoint의 DNS/route가 필요합니다. Private endpoint가 활성화되어 있고 호스트가 실제 private 경로를 해석·접속하는지 확인합니다. 첫 터미널에서 전달 세션을 유지합니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the cluster/managed-instance Region for this example}"
: "${MANAGED_INSTANCE_ID:?Set the prepared SSM-managed instance with a private route to EKS}"
: "${ENDPOINT_REVIEW_DIR:?Use the endpoint inspection directory}"
aws ssm start-session --region "$AWS_REGION" --target "$MANAGED_INSTANCE_ID" \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "file://$ENDPOINT_REVIEW_DIR/ssm-remote-host.json"
```
두 번째 터미널은 같은 클러스터의 kubeconfig·CA·의도한 IAM exec 인증을 유지합니다. 로컬로 연결하되 실제 TLS 서버 이름을 지정하며 hostname 불일치를 숨기려고 인증서 검증을 끄지 않습니다:
```bash
set -euo pipefail
: "${ENDPOINT_REVIEW_DIR:?Set the same review directory in this second terminal}"
: "${CLUSTER_KUBECONFIG:?Set the kubeconfig for this same EKS cluster and intended IAM identity}"
EKS_ENDPOINT_HOST=$(cat "$ENDPOINT_REVIEW_DIR/endpoint-host.txt")
kubectl --kubeconfig "$CLUSTER_KUBECONFIG" \
--server https://127.0.0.1:6443 --tls-server-name "$EKS_ENDPOINT_HOST" \
--request-timeout=10s -n security-demo get pods
```
Remote-host 문서는 지정한 EKS host로 전달합니다. 기존 AWS-StartPortForwardingSession은 관리 인스턴스 자체의 port를 전달하므로 그443을 전달한다고 EKS API 서버가 되지는 않습니다. Tunnel·health check 성공만으로 권한·private route가 입증되지는 않습니다. Public 접근을 제거하기 전에 대상 API 작업·DNS/private 주소·실제 route를 검증합니다.
### 검토한 Endpoint 변경 적용
Private 접근을 활성화하고 대상 private client 경로를 검증한 뒤 ENDPOINT_CHANGE=private를 선택하기 전에 새 조회 기록을 만듭니다. 다음은 다른 클러스터·계정, 변경된 endpoint 설정, private 접근 활성화 전 private-only 전환을 거부합니다. 조회·검사는 원자적 transaction이 아니므로 소유자와 변경을 조율합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Use the inspected cluster}"
: "${AWS_REGION:?Use its Region}"
: "${ENDPOINT_REVIEW_DIR:?Use the review directory}"
: "${ENDPOINT_CHANGE:?Choose public-restricted or private after validating the intended route}"
case "$ENDPOINT_CHANGE" in
public-restricted|private) ;;
*) echo "Unexpected endpoint change"; exit 1 ;;
esac
test -f "$ENDPOINT_REVIEW_DIR/endpoint-$ENDPOINT_CHANGE.json"
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster --output json > "$ENDPOINT_REVIEW_DIR/cluster-current.json"
python3 - "$ENDPOINT_REVIEW_DIR" "$CLUSTER_NAME" "$ENDPOINT_CHANGE" <<'PY'
import json, pathlib, sys
root = pathlib.Path(sys.argv[1])
before = json.loads((root / "cluster-before.json").read_text())
current = json.loads((root / "cluster-current.json").read_text())
keys = ["endpointPublicAccess", "endpointPrivateAccess", "publicAccessCidrs"]
if current["name"] != sys.argv[2] or current["arn"] != before["arn"] or current["status"] != "ACTIVE":
raise SystemExit("Cluster identity/state differs from the reviewed target")
if any(current["resourcesVpcConfig"].get(k) != before["resourcesVpcConfig"].get(k) for k in keys):
raise SystemExit("Endpoint configuration changed; inspect and review again")
if sys.argv[3] == "private" and current["resourcesVpcConfig"].get("endpointPrivateAccess") is not True:
raise SystemExit("Enable and verify private access before disabling public access")
PY
aws eks update-cluster-config --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--resources-vpc-config "file://$ENDPOINT_REVIEW_DIR/endpoint-$ENDPOINT_CHANGE.json" \
--output json > "$ENDPOINT_REVIEW_DIR/update-response.json"
EKS_UPDATE_ID=$(python3 - "$ENDPOINT_REVIEW_DIR/update-response.json" <<'PY'
import json, sys
with open(sys.argv[1]) as stream:
print(json.load(stream)["update"]["id"])
PY
)
UPDATE_DONE=false
for ((attempt=0; attempt<60; attempt++)); do
STATE=$(aws eks describe-update --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--update-id "$EKS_UPDATE_ID" --query update.status --output text)
case "$STATE" in
Successful) UPDATE_DONE=true; break ;;
InProgress) sleep 10 ;;
*) echo "Update $EKS_UPDATE_ID status: $STATE; inspect before further changes"; exit 1 ;;
esac
done
test "$UPDATE_DONE" = true || { echo "Update still pending; inspect $EKS_UPDATE_ID"; exit 1; }
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster.resourcesVpcConfig --output json
```
Cluster ACTIVE만이 아니라 반환된 update ID와 Successful 상태로 완료를 확인합니다. Timeout이면 기존 작업을 조회하며 다른 변경을 무조건 제출하지 않습니다. 이후 실제 API 경로를 다시 확인합니다. 이전 endpoint 필드·AWS 관리 복구 접근을 보존하세요. 이번 검토에서 실제 endpoint 변경을 실행하지 않았습니다.
### Private AWS 서비스 의존성
| 경로 | 관련 Endpoint·의존성 |
|---|---|
| Kubernetes API | Cluster endpoint private 접근과 route/DNS/SG |
| AWS EKS 관리 API | Private 관리 접근에 필요한 eks interface endpoint |
| Pod Identity agent | 노드가 public egress를 쓸 수 없을 때 eks-auth interface endpoint |
| OIDC discovery/JWKS 도구 | oidc-eks interface endpoint; 익명 공개 키 데이터이며 기본 endpoint policy만 지원 |
| IRSA 자격 증명 교환 | OIDC 키 조회와 별도인 regional STS endpoint |
| ECR 이미지 pull | ecr.api/ecr.dkr interface와 동작하는 S3 layer 다운로드 경로 |
| 다른 컨트롤러·앱 | 실제 필요한 EC2·Logs·Secrets Manager·SSM 등 서비스 의존성만 |
EKS interface endpoint와 Kubernetes API endpoint는 다릅니다. S3 gateway endpoint는 route table, interface endpoint는 subnet·SG·private DNS를 사용합니다. Endpoint type을 생략한 loop 하나로 모든 서비스를 생성하지 마세요. OIDC PrivateLink는 STS token 검증·IRSA 역할 권한을 바꾸지 않습니다. 생성 장의 절차로 소유자가 관리하는 전체 네트워크·신원·컴퓨팅을 준비하며 여기의 endpoint 조각은 프로덕션 준비가 끝난 클러스터 배포가 아닙니다.
### Customer-Routed Control Plane Egress (2026년 6월)
2026년6월18일 발표된 CUSTOMER_ROUTED는 지원되는 EKS 컨트롤 플레인 egress 모드입니다. 별도 egress ENI를 생성하지 않고 사용자 subnet의 기존 cross-account cluster network interface를 사용합니다. Admission webhook·OIDC discovery·aggregated API server 같은 고객 대상 API-server 호출에 적용됩니다.
**전환은 일방향입니다. CUSTOMER_ROUTED 활성화 후 해당 클러스터를 AWS_MANAGED로 되돌릴 수 없습니다.** 저장한 구성·Kubernetes 버전 rollback으로 이 모드를 취소할 수 있는 것처럼 설명하면 안 됩니다. 새 경로가 실패하면 routing·연결 구성을 복구해야 합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the existing cluster}"
: "${AWS_REGION:?Set its Region}"
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query 'cluster.{Status:status,Vpc:resourcesVpcConfig,IPFamily:kubernetesNetworkConfig.ipFamily}' --output json
```
전환 전에 webhook·OIDC·aggregated API 목적지와 port를 조사합니다. Cluster ENI subnet의 route·outbound SG·NACL 반환 트래픽·DNS를 확인하세요. VPC DHCP option에는 AmazonProvidedDNS가 포함되어야 하며 실제 목적지의 Route53 private zone/Resolver forwarding·외부 DNS가 동작해야 합니다. 목적지에 따라 NAT·firewall·중앙 routing을 사용할 수 있습니다. IPv6 클러스터는 문서화된 IPv4·IPv6 경로가 필요하고 IPv4 전용 의존성에는 동작하는 IPv4 경로가 여전히 필요합니다.
위 검토 후 사용하는 실제 API 옵션은 다음과 같습니다. 이 감사에서는 오프라인 문법만 확인했으며 AWS 클러스터에 실행하지 않았습니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the reviewed cluster}"
: "${AWS_REGION:?Set its Region}"
# One-way change: complete subnet route/SG/NACL/DNS and dependency checks first.
aws eks update-cluster-config --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--resources-vpc-config controlPlaneEgressMode=CUSTOMER_ROUTED --output json
```
반환된 update ID를 기록하고 DescribeUpdate의 Successful 상태를 확인합니다. ACTIVE만으로는 부족합니다. 이후 실제 설정한 webhook·사용자 OIDC·aggregated API 경로를 검증하세요. 로컬 CLI parser 검사가 VPC 연결을 입증한다고 주장하지 않습니다.
| 트래픽 | 이 설정의 영향 |
|---|---|
| 고객 대상 API-server egress | 설정한 고객 VPC 경로 사용 |
|10250 kubelet API | Cluster ENI/노드 경로 사용; 외부 egress 장치 경유 아님 |
| etcd·CloudWatch Logs·내부 EKS 트래픽 | EKS 관리 경로 유지 |
| 관리형 ArgoCD/ACK/KRO 같은 EKS Capabilities controller | 별도 관리형 인프라에서 실행되며 이 기능으로 경로 변경되지 않음 |
| 앱의 STS/EKS Auth/S3 호출 | 워크로드·노드 네트워크를 따르며 API-server 모드가 제어하지 않음 |
기능 자체는 EKS 리전에서 추가 기능 요금 없이 제공되지만 NAT·firewall·PrivateLink·logging 리소스는 별도 요금이 있습니다. 네트워크 흐름 근거가 필요하면 Flow Logs를 구성해야 하며 암호화된 payload·앱 권한을 자동으로 보여주거나 입증하지 않습니다.
#### 요청 범위의 SCP 예제
eks:controlPlaneEgressMode는 CreateCluster/UpdateClusterConfig 요청에 지정한 모드를 평가합니다. 존재 조건 없는 StringNotEquals Deny는 키 생략에도 일치합니다. 아래 예제는 신규 클러스터에 해당 모드를 요구하고 update의 명시적 다른 모드를 거부하되, 모드를 생략한 관련 없는 update 요청은 허용합니다:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RequireCustomerRoutedForNewClusters",
"Effect": "Deny",
"Action": "eks:CreateCluster",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"eks:controlPlaneEgressMode": "CUSTOMER_ROUTED"
}
}
},
{
"Sid": "RejectExplicitOtherEgressMode",
"Effect": "Deny",
"Action": "eks:UpdateClusterConfig",
"Resource": "*",
"Condition": {
"Null": {
"eks:controlPlaneEgressMode": "false"
},
"StringNotEquals": {
"eks:controlPlaneEgressMode": "CUSTOMER_ROUTED"
}
}
}
]
}
```
의도한 요청 제어이며 기존 클러스터 자동 이전 기능이 아닙니다. 이 예제 아래에서 기존 AWS_MANAGED 클러스터는 관련 없는 update를 계속 받을 수 있습니다. 조직이 다른 rollout 정책을 요구하면 별도로 설계해야 합니다. SCP가 IAM 권한·routing/DNS를 구성하지는 않습니다. 배포 전에 조직 소유자와 정책 상속·대표 허용/거부 요청을 검증합니다.
## 네트워크 보안
SG·routing·NetworkPolicy·앱 인증을 각각의 계층에 사용합니다. 그림은 전통적인 네트워크 구성이며 public bastion·private AWS endpoint는 실제 구성해야 하는 선택 요소이지 모든 EKS 클러스터의 기본 속성이 아닙니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-05-eks-security-8.html)
### 보안 그룹과 필요한 경로
API·노드 경로에 필요한 node→API TCP443·control-plane→kubelet TCP10250을 허용합니다. DNS·webhook·워크로드는 실제 목적지 port가 추가로 필요할 수 있습니다. “노드 간 통신”이라며 TCP1025–65535 전체를 열어야 하는 보편적 요구는 없습니다. SG는 stateful이고 NACL·반환 경로 규칙은 별개입니다.
실제 ENI·노드에 연결된 SG를 조회합니다. Custom launch-template SG가 모든 EKS 기본 규칙을 자동 상속하지는 않습니다. Security Groups for Pods와 Auto Mode NodeClass pod SG selector는 지원·동작이 다른 별도 기능이며 SG를 추상적인 인스턴스 전용 계층으로만 설명하면 안 됩니다. 선택한 컴퓨팅·CNI·실제 출발 interface를 확인하세요.
### NetworkPolicy 의미
NetworkPolicy는 지원·구성된 시행 구현이 필요합니다. YAML 생성만으로 패킷이 필터링되지는 않습니다. 기존 네트워크 소유자의 지원 EKS VPC CNI 정책 기능 또는 의도적으로 선택한 호환 stack을 사용합니다. 기존 클러스터에 floating Calico manifest·관련 없는 Cilium 설정을 설치하지 마세요. Auto Mode는 네트워크를 자체 관리하며 임의 대체 CNI 설치 대상이 아닙니다.
정책은 합산됩니다. 격리된 연결은 출발지 egress·목적지 ingress 양쪽에서 허용되어야 합니다. Rule 순서는 deny/allow 우선순위가 아닙니다. PodSelector만 있으면 정책 네임스페이스의 peer를 선택하고 같은 peer의 namespaceSelector+podSelector는 AND입니다. 별도 peer 항목은 대안으로 합쳐집니다.
### 격리된 정책 실습
이 예제는 새 실습 네임스페이스·정책을 시행하는 Linux EC2 CNI·kube-system의 k8s-app=kube-dns 레이블을 가진 전통적인 CoreDNS Deployment를 전제로 합니다. app=frontend/api/database 레이블의 준비된 Pod, API8080·실습 metrics9090·DB5432 port를 가정합니다. 이 정책들이 앱을 배포하지는 않습니다.
Default deny는 양방향을 포함하며 DNS·의도한 출발지/목적지 흐름을 명시합니다. 모든 의존성 조사 없이 기존 프로덕션 네임스페이스에 가정을 적용하지 않습니다:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: security-network-demo
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36
---
apiVersion: v1
kind: Namespace
metadata:
name: security-monitoring-demo
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: security-network-demo
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: security-network-demo
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: frontend-to-api
namespace: security-network-demo
spec:
podSelector:
matchLabels:
app: frontend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-ingress
namespace: security-network-demo
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-to-database
namespace: security-network-demo
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: database-ingress
namespace: security-network-demo
spec:
podSelector:
matchLabels:
app: database
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: monitor-api
namespace: security-network-demo
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: security-monitoring-demo
podSelector:
matchLabels:
app: prometheus
ports:
- protocol: TCP
port: 9090
```
모니터링 Pod에도 egress 권한이 필요합니다. 다음 별도 실습 정책은 API metrics·전통적인 DNS 경로를 허용하며 프로덕션 Prometheus의 완전한 네트워크 정책은 아닙니다:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: prometheus-to-demo-api
namespace: security-monitoring-demo
spec:
podSelector:
matchLabels:
app: prometheus
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: security-network-demo
podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 9090
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
```
Pure Auto Mode는 전통적 Deployment 대신 node-local CoreDNS를 사용하며 NodeLocal DNS·custom resolver 경로도 다를 수 있습니다. DNS 규칙을 적용하기 전에 실제 resolver 경로를 확인하세요. 레이블은 selector이지 암호학적 워크로드 신원이 아니므로 생성·재레이블 권한과 필요한 앱 권한을 제어합니다.
### 외부 목적지와 검증 한계
다음 문서용 주소는 연결 실습 전에 승인된 실제 목적지로 교체해야 합니다. IP/port 하나의 권한을 설명하며 AWS 서비스·FQDN allowlist가 아닙니다:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: approved-https-example
namespace: security-network-demo
spec:
podSelector:
matchLabels:
app: external-client
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 203.0.113.10/32
ports:
- protocol: TCP
port: 443
```
0.0.0.0/0의443 허용은 넓은 public 목적지를 허용하며 private/link-local 제외만으로 신뢰 서비스가 식별되지 않습니다. Native NetworkPolicy는 도메인을 영속 allowlist로 해석하거나 IAM 권한을 제공하지 않습니다. Node/hostNetwork 예외·NAT 순서·구현별 동작 때문에 IMDS 격리의 유일한 수단으로 삼을 수도 없습니다.
실제 CNI에서 허용·거부되는 새 연결을 테스트합니다. 정책 전파는 비동기일 수 있고 기존 연결 처리도 구현에 따라 다릅니다. 본문은 합성 selector/port 사례로 검사했으며 이번 검토에서 패킷 필터링·DNS·실제 트래픽을 실행하지 않았습니다.
## 포드 보안
Pod Security Standards는 profile을 정의하고 Pod Security Admission(PSA)이 선택한 네임스페이스 정책을 시행합니다. PSA는 Kubernetes1.25부터 stable입니다. PodSecurityPolicy는1.21에서 deprecated, **1.25에서 제거**되었으므로 현재 EKS에 배포할 API가 아닙니다. 이름에 PSP가 있는 Gatekeeper constraint도 제거된 API가 아니라 별도 custom resource입니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-05-eks-security-9.html)
### 버전이 있는 Profile과 시행
| Profile | 의미 |
|---|---|
| Privileged | 이 PSS profile의 제한은 없지만 다른 권한·admission 제어는 적용됨 |
| Baseline | 알려진 권한 확대 구성에 대한 기본 제약 |
| Restricted | 지원 non-root·capability·seccomp 조건 등을 추가한 더 강한 제약 |
Namespace enforce는 위반 Pod 요청을 거부합니다. Audit·warn은 위반을 기록·보고하며 controller apply 성공이 Pod 실행 가능성의 증거가 되지는 않습니다. Deployment/Job template에는 audit/warn을 적용할 수 있고 enforce는 생성되는 Pod에 적용됩니다. kubectl apply뿐 아니라 rollout·이벤트를 확인하세요. 네임스페이스 정책 변경이 기존 Pod를 소급 eviction하지는 않습니다.
### 완전한 Linux Security Context 예제
다음은 실제 이미지·쓰기 가능한 앱 runtime 경로가 필요 없는 작은 Linux 신원/security-context 데모이며 nginx 앱 배포가 아닙니다. 실제 클러스터에 맞는 PSS 버전을 사용하세요. 예제는 검토한 EKS1.36 정책에 고정했습니다.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: security-demo
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.36
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.36
---
apiVersion: v1
kind: Pod
metadata:
name: security-context-demo
namespace: security-demo
spec:
automountServiceAccountToken: false
nodeSelector:
kubernetes.io/os: linux
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.37.0
command:
- sh
- -c
args:
- id && sleep 3600
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
```
Pod 수준 runAsUser/runAsGroup/runAsNonRoot/seccomp와 컨테이너 수준 allowPrivilegeEscalation·capability를 구분합니다. fsGroup은 드라이버·파일 시스템 동작에 영향을 받는 Pod 수준 볼륨 소유권 설정이며 container capability가 아닙니다. readOnlyRootFilesystem은 앱과 호환되어야 하는 추가 강화이며 PSS Restricted의 보편적 필수 조건이 아닙니다. 모든 PVC까지 읽기 전용으로 만들지도 않습니다.
실제 앱에는 이미지 UID/GID·쓰기 가능한 tmp/cache/socket volume·probe port를 준비합니다. 필요한 경로 없이 root 중심 nginx가 임의 UID·읽기 전용 루트로 시작한다고 가정하지 마세요. 표준 제어는 위험을 줄이며 모든 kernel/runtime 탈출이 불가능함을 입증하지는 않습니다.
### Admission 정책 예제
호환되는 소유자 관리 정책 엔진과 실제 CRD를 사용합니다. Kyverno1.19.1은 policies.kyverno.io/v1 ValidatingPolicy API를 제공하며 해당 릴리스에서 이전 ClusterPolicy 형식은 deprecated입니다. 아래 제한된 예제는 security-demo에만 적용되어 일반·init·ephemeral container를 검사합니다. Privileged 생략은 기본 false로 허용하고 privileged:true는 거부합니다.
```yaml
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: demo-disallow-privileged
spec:
validationActions:
- Deny
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups:
- ''
apiVersions:
- v1
resources:
- pods
- pods/ephemeralcontainers
operations:
- CREATE
- UPDATE
scope: Namespaced
matchConditions:
- name: demo-namespace
expression: has(object.metadata.namespace) && object.metadata.namespace == 'security-demo'
validations:
- expression: object.spec.containers.all(c, !has(c.securityContext) || !has(c.securityContext.privileged)
|| c.securityContext.privileged == false) && (!has(object.spec.initContainers)
|| object.spec.initContainers.all(c, !has(c.securityContext) || !has(c.securityContext.privileged)
|| c.securityContext.privileged == false)) && (!has(object.spec.ephemeralContainers)
|| object.spec.ephemeralContainers.all(c, !has(c.securityContext) || !has(c.securityContext.privileged)
|| c.securityContext.privileged == false))
message: Privileged containers, including init and ephemeral containers, are not
allowed in security-demo.
```
이 규칙 하나가 전체 PSS profile·이미지 서명 검증을 대체하지는 않습니다. 플랫폼 CSI·모니터링 agent와 의도한 예외는 별도 검토한 소유권으로 관리하며 kube-system에 데모 정책을 일괄 적용하지 않습니다. Gatekeeper 대안도 추측한 constraint kind만이 아니라 대응 ConstraintTemplate·schema·동작 검증이 필요합니다.
CEL 정책은 배포 CRD와 실제 Kyverno CLI로 생략/false·일반/init/ephemeral true·다른 네임스페이스 사례를 검사했습니다. 실제 admission webhook·Pod 배포·정책 시행을 실행한 것은 아닙니다. 프로덕션 시행 전에 기존 리소스·컨트롤러 rollout을 검토합니다.
## Bottlerocket 및 읽기 전용 OS
Bottlerocket은 호스트 소프트웨어 구성을 작게 유지하고 API로 설정을 관리하며 이미지 단위로 업데이트하는 Linux 컨테이너 호스트입니다. 읽기 전용 루트가 모든 저장 공간을 불변으로 만들거나 커널·runtime 취약점을 없애지는 않습니다.
### API 기반 구성
호스트는 일반적인 SSH 서버·패키지 관리자 대신 API로 관리합니다. Control/admin host container의 접근 경로와 권한은 다릅니다. SSM·SSH 진입 경로, 노드 IAM, 로컬 API socket 접근을 보호하세요. Socket 접근자는 호스트 구성을 바꿀 수 있습니다. Control container 활성화만으로 SSM 등록·권한·네트워크 연결이 준비되지는 않습니다.
```bash
# Run inside an authorized Bottlerocket control container.
apiclient get settings.host-containers.admin
apiclient get settings.updates
apiclient get settings.motd
```
승인된 설정 변경에는 `apiclient set motd="EKS Bottlerocket node"`를 사용할 수 있습니다. `set`은 **자동으로 commit·apply**하며 관련 서비스를 재시작할 수 있습니다. 독립된 `apiclient commit` 명령은 없습니다. 저수준 staged transaction은 API의 transaction commit-and-apply 동작을 사용합니다.
User data는 Bash가 아닌 TOML입니다. 아래는 실제 클러스터용 bootstrap 구성에 병합할 설정 조각이며 endpoint·CA·모든 bootstrap 요건을 제공하는 전체 구성이 아닙니다:
```toml
[settings]
motd = "EKS Bottlerocket node"
[settings.host-containers.admin]
enabled = false
```
### SELinux 및 파일 시스템 무결성
SELinux는 enforcing 모드에서 process·file label에 따라 접근을 제한합니다. 필요한 호스트 서비스에는 여전히 높은 권한이 필요하며 충분한 권한을 가진 사용자는 일부 label을 변경할 수 있습니다. 이 제어는 위험을 줄이지만 모든 컨테이너 탈출 방지를 보장하지는 않습니다.
루트 파일 시스템은 dm-verity를 사용해 보호 블록을 읽을 때 hash tree와 검증합니다. 부팅 시 모든 파일을 이미 검사했다는 뜻은 아닙니다. 로그·컨테이너 이미지·앱 volume·설정에는 변경 가능한 저장 공간이 있으며 `/etc`의 일부는 ephemeral이므로 지원하는 구성 경로를 사용해야 합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-05-eks-security-11.html)
### 업데이트: In-Place 또는 노드 교체
Bottlerocket은 **in-place 이미지 업데이트를 지원**합니다. `apiclient update check`로 적용 가능한 업데이트를 확인하고 `apiclient update apply`로 다른 partition에 기록하여 다음 부팅 대상으로 선택합니다. 명시적으로 결합하지 않았다면 reboot는 별도의 중단을 유발하는 단계입니다. 원시 apiclient·SSM 업데이트 명령은 Kubernetes 워크로드를 drain하지 않습니다.
Bottlerocket 문서는 Kubernetes in-place 업데이트의 조정 도구로 Brupop을 권장합니다. EKS 관리형 노드 그룹 업데이트는 인스턴스 교체 방식도 제공합니다. Fleet의 업데이트 주체를 하나로 조정하고 버전·variant 호환성, 여유 용량, PDB, 로컬 데이터와 앱 상태를 검토한 뒤 소규모 rollout을 확인하고 진행합니다. A/B partition은 복구 수단을 제공하지만 모든 앱·bootstrap 실패의 자동 복구를 보장하지 않습니다.
`settings.updates.version-lock`은 `1.64.0` 같은 전체 버전 또는 `latest`를 받으며 `1.15.%` 같은 wildcard lock은 지원하지 않습니다. Lock은 업데이트 선택을 제한할 뿐 자동 업데이트 일정·완료를 보장하지 않습니다. 검증한 custom repository를 의도적으로 운영하는 경우 외에는 variant의 update repository 설정을 유지하고 일반적인 updates URL로 덮어쓰지 마세요.
### EKS 관리형 노드 그룹 예제
소유한 기존 클러스터의 이름·Region·버전·용량으로 바꾼 후 아래를 `bottlerocket-nodegroup.yaml`로 저장합니다. eksctl 관리형 노드 그룹 구성과 생성된 bootstrap 설정을 사용합니다. 리소스를 생성하기 전에 Bottlerocket AMI variant의 클러스터 버전·인스턴스 아키텍처 지원, private subnet egress·endpoint, 노드 역할과 별도의 워크로드·CNI 신원을 확인합니다. 위 OS 버전은 문법 예시이며 모든 노드를 해당 릴리스로 올리라는 지시가 아닙니다.
```yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: secure-cluster
region: us-west-2
version: "1.36"
managedNodeGroups:
- name: bottlerocket-ng
amiFamily: Bottlerocket
instanceType: m5.large
privateNetworking: true
minSize: 2
desiredCapacity: 3
maxSize: 5
volumeSize: 100
volumeType: gp3
volumeEncrypted: true
updateConfig:
maxUnavailable: 1
bottlerocket:
enableAdminContainer: false
settings:
motd: "EKS Bottlerocket node"
host-containers:
control:
enabled: true
```
검토한 배포의 생성 명령은 `eksctl create nodegroup --config-file bottlerocket-nodegroup.yaml`입니다. 노드 경계 예제를 완성하려고 모든 노드에 앱의 Secrets Manager 권한이나 CSI controller의 volume 관리 권한을 주지 마세요. 해당 워크로드에 지원되는 별도 신원을 부여합니다. 활성화한 기능에 따라 노드의 EKS Auth·registry·SSM 필수 권한을 검토합니다.
Fleet 변경 전에 관리형 노드 그룹과 노드가 실제 보고하는 OS를 확인합니다. kubectl context가 같은 소유 클러스터인지 확인하세요:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the owned cluster name}"
: "${NODEGROUP_NAME:?Set the owned managed node group name}"
: "${AWS_REGION:?Set the cluster Region}"
aws eks describe-nodegroup --region "$AWS_REGION" \
--cluster-name "$CLUSTER_NAME" --nodegroup-name "$NODEGROUP_NAME" \
--query 'nodegroup.{Name:nodegroupName,Status:status,AMIType:amiType,Release:releaseVersion,Kubernetes:version,Role:nodeRole,Update:updateConfig,Health:health.issues}'
kubectl get nodes -l "eks.amazonaws.com/nodegroup=$NODEGROUP_NAME" \
-o custom-columns='NAME:.metadata.name,OS:.status.nodeInfo.osImage,KUBELET:.status.nodeInfo.kubeletVersion'
```
AMI release만으로 이후 API 설정 변경·in-place OS 업데이트의 전체 이력이 남지는 않습니다. 원하는 설정, 업데이트 결과와 실행 중인 노드 목록을 함께 보관합니다. 노드 교체에는 명시적인 상태 확인·복구 계획이 필요하며 검증하지 않은 drain·emptyDir 삭제·노드 그룹 삭제를 연달아 실행하지 않습니다.
이 문서의 검증 범위는 TOML 문법, 배포된 eksctl 구성 schema와 shell 문법입니다. Bottlerocket 노드·업데이트·SSM session·노드 그룹을 생성하거나 실행 검증하지 않았습니다.
공식 참고 자료: [Bottlerocket API client](https://github.com/bottlerocket-os/bottlerocket-core-kit/tree/v15.0.0/sources/api/apiclient), [in-place updates](https://bottlerocket.dev/en/os/1.64.x/update/methods/in-place/), [node replacement](https://bottlerocket.dev/en/os/1.64.x/update/methods/node-replacement/), [version locks](https://bottlerocket.dev/en/os/1.64.x/update/locking-to-a-specific-release/), [dm-verity](https://docs.kernel.org/admin-guide/device-mapper/verity.html).
## IAM 권한 경계
Permissions boundary는 identity-based policy가 IAM 사용자·역할에 부여할 수 있는 권한을 제한하며 자체적으로 권한을 부여하지 않습니다. Identity policy 경로의 허용 범위는 policy와 boundary의 교집합이며 적용되는 session·Organizations 정책과 명시적 Deny도 평가합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-05-eks-security-12.html)
그림은 identity policy 경로를 설명하며 모든 IAM 권한 평가의 공식은 아닙니다. 같은 계정의 resource policy가 사용자 ARN이나 role-session ARN에 직접 부여하는 권한은 implicit deny와의 관계가 다를 수 있습니다. 명시적 Deny는 여전히 중요합니다. 실제 principal·resource policy를 확인하며 boundary가 모든 resource-based grant의 무조건적인 상한이라고 가정하지 않습니다. Boundary가 있는 principal에 resource policy의 `NotPrincipal`과 `Deny`를 조합하지 말고 문서의 principal-ARN 조건 패턴을 사용하세요.
### 범위를 제한한 워크로드 Boundary
아래 예시 boundary는 버킷 하나의 목록 조회·객체 읽기만 허용 범위로 둡니다. 소유한 버킷으로 바꾸고 별도의 identity policy와 올바른 IRSA·Pod Identity trust를 준비하세요. SSE-KMS 객체에는 해당 KMS key 권한·key policy도 검토해야 하며 이 S3 전용 boundary는 KMS 동작을 부여하거나 허용하지 않습니다.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListOwnedBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::amzn-s3-demo-app-bucket"
},
{
"Sid": "ReadOwnedObjects",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-app-bucket/*"
}
]
}
```
역할의 기존 IaC·소유권 절차로 검토한 boundary를 적용하고 허용·거부 사례를 모두 확인합니다. Pod 역할에도 boundary를 사용할 수 있지만 association·trust·session 요건은 별도로 충족해야 합니다. 위임받은 관리자가 필수 boundary를 바꾸거나 제거하고 policy version을 변경하지 못하도록 관리하세요.
이 boundary를 EKS 노드 역할에 붙이지 마세요. 노드 요건은 활성화한 기능에 따라 EKS 노드 정보 조회, registry pull, `eks-auth:AssumeRoleForPodIdentity`, SSM 동작 등을 포함합니다. 불완전한 allowlist를 붙이면 노드·자격 증명 기능이 중단될 수 있습니다. 실제 노드 policy·기능 의존성을 조사한 뒤 canary에서 노드 boundary를 검증합니다. 앱 secret 권한과 CSI·CNI controller 권한은 지원하는 워크로드 신원에 두고 모든 노드에 추가하지 않습니다.
### 최소 권한 패턴
**Kubernetes namespace 접근:** 인증 절의 access entry와 namespace 범위 EKS access policy 또는 RBAC Role·RoleBinding을 사용합니다. `eks:AccessKubernetesApi`는 EKS console에서 Kubernetes 객체를 보는 권한이며 kubectl용 일반 namespace RBAC 권한이 아닙니다. `eks:namespaces`는 `AssociateAccessPolicy`·`DisassociateAccessPolicy` 요청의 ArrayOfString 조건이며 `AccessKubernetesApi`의 조건이 아닙니다. IAM `DescribeCluster`·`ListClusters`만으로 Kubernetes 작업이 허용되지는 않습니다. 여러 grant는 합산되므로 좁은 association이 기존 cluster 전체 grant를 제거하지 않습니다.
**Repository 범위 pull:** 이미지 읽기 동작은 승인한 repository ARN으로 제한할 수 있습니다. `GetAuthorizationToken`은 `Resource: "*"`가 필요하지만 인증 권한만으로 이미지 접근이 허용되지는 않습니다.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PullApprovedRepository",
"Effect": "Allow",
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
],
"Resource": "arn:aws:ecr:us-west-2:123456789012:repository/approved-*"
},
{
"Sid": "RegistryAuthentication",
"Effect": "Allow",
"Action": "ecr:GetAuthorizationToken",
"Resource": "*"
}
]
}
```
**S3 bucket ABAC:** 현재 S3 general purpose bucket은 ListBucket·GetObject 등의 동작에서 bucket tag 조건을 지원하지만 **해당 버킷의 ABAC를 먼저 활성화**해야 합니다. 기본값은 disabled입니다. 아래 IAM policy는 object tag가 아닌 bucket의 Environment tag를 사용합니다:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListMatchingEnvironment",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::amzn-s3-demo-app-bucket",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Environment": "${aws:PrincipalTag/Environment}"
}
}
},
{
"Sid": "ReadMatchingEnvironment",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::amzn-s3-demo-app-bucket/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Environment": "${aws:PrincipalTag/Environment}"
}
}
}
]
}
```
별도 통제하는 principal·session에 실제로 일치하는 Environment tag가 있어야 합니다. Kubernetes 객체나 Pod Identity association 리소스에 tag를 붙인다고 임의 principal tag가 자동으로 생기지는 않습니다. ABAC 활성화 전에 기존 bucket policy를 감사하고 tag·ABAC 상태 변경 권한을 제한합니다. 활성화 후 bucket tag 변경에는 S3 `TagResource`·`UntagResource`를 사용하며 `PutBucketTagging`·`DeleteBucketTagging`은 동작하지 않습니다. 상태는 `aws s3api get-bucket-abac --bucket YOUR_OWNED_BUCKET --region YOUR_REGION`으로 확인합니다. 이 예제는 ABAC를 활성화하거나 버킷을 변경하지 않습니다.
### Organizations SCP 통제
SCP는 member account의 적용 대상 principal 권한을 제한하며 권한을 부여하지 않습니다. Management account와 service-linked role에는 적용되지 않습니다. Kubernetes RBAC·네트워크 제어도 대체하지 않습니다. 제한된 OU·계정에서 검증하고 소유자가 관리하는 복구 경로를 준비한 뒤 범위를 확대합니다. Deny 전용 예제는 Organizations 계층에 필요한 Allow policy가 유지된다는 전제입니다.
아래 삭제 통제는 명시한 operator 역할 하나를 이 Deny의 예외로 둡니다. 검토한 복구 principal의 계정·역할로 바꾸세요. 예외 자체가 삭제 권한을 부여하지는 않습니다:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RestrictClusterDeletion",
"Effect": "Deny",
"Action": "eks:DeleteCluster",
"Resource": "*",
"Condition": {
"ArnNotEquals": {
"aws:PrincipalArn": "arn:aws:iam::123456789012:role/EKSDeletionOperator"
}
}
}
]
}
```
### 신규 EKS IAM 조건 키 7종 (2026년 4월)
2026년 4월 20일 발표된 기능은 유효합니다. 각 조건은 현재 service authorization reference에서 해당 조건을 제공하는 동작에만 적용합니다:
| 키 | 타입 | 지원 동작 범위 |
|---|---|---|
| `eks:endpointPublicAccess`, `eks:endpointPrivateAccess` | Bool | CreateCluster, UpdateClusterConfig |
| `eks:encryptionConfigProviderKeyArns` | ArrayOfARN | CreateCluster, AssociateEncryptionConfig |
| `eks:kubernetesVersion` | String | CreateCluster, UpdateClusterVersion |
| `eks:controlPlaneScalingTier` | String | CreateCluster, UpdateClusterConfig |
| `eks:deletionProtection` | Bool | CreateCluster, UpdateClusterConfig |
| `eks:zonalShiftEnabled` | Bool | CreateCluster, UpdateClusterConfig |
아래는 전체 조직 정책이 아닌 **요청 통제 예제**입니다. 생성 시 명시적인 private-only endpoint 설정과 customer-managed key를 요구하고 업데이트에서 명시적으로 endpoint 통제를 약화하는 요청을 거부합니다. 승인 버전 목록은 이번 검토 시점의 예시입니다. 조직 정책·지원 기간에 따라 목록을 관리하며 기존 클러스터를 업그레이드하라는 명령이 아닙니다.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RequireExplicitPrivateOnlyCreation",
"Effect": "Deny",
"Action": "eks:CreateCluster",
"Resource": "*",
"Condition": {
"BoolIfExists": {
"eks:endpointPublicAccess": "true"
}
}
},
{
"Sid": "RequireExplicitPrivateEndpointCreation",
"Effect": "Deny",
"Action": "eks:CreateCluster",
"Resource": "*",
"Condition": {
"BoolIfExists": {
"eks:endpointPrivateAccess": "false"
}
}
},
{
"Sid": "DenyEnablingPublicEndpoint",
"Effect": "Deny",
"Action": "eks:UpdateClusterConfig",
"Resource": "*",
"Condition": {
"Bool": {
"eks:endpointPublicAccess": "true"
}
}
},
{
"Sid": "DenyDisablingPrivateEndpoint",
"Effect": "Deny",
"Action": "eks:UpdateClusterConfig",
"Resource": "*",
"Condition": {
"Bool": {
"eks:endpointPrivateAccess": "false"
}
}
},
{
"Sid": "RequireCustomerKeyAtCreation",
"Effect": "Deny",
"Action": "eks:CreateCluster",
"Resource": "*",
"Condition": {
"Null": {
"eks:encryptionConfigProviderKeyArns": "true"
}
}
},
{
"Sid": "RequireReviewedVersion",
"Effect": "Deny",
"Action": [
"eks:CreateCluster",
"eks:UpdateClusterVersion"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"eks:kubernetesVersion": [
"1.34",
"1.35",
"1.36"
]
}
}
}
]
}
```
생성 문장은 API 기본값이 있더라도 endpoint 필드 생략을 의도적으로 거부합니다. 업데이트 문장은 IfExists 없는 Bool을 사용하므로 해당 필드를 생략한 무관한 업데이트는 이 문장에 의해 거부되지 않습니다. 조건은 요청을 검사하며 클러스터 전체 현재 상태를 검사하거나 기존 클러스터를 자동 수정하지 않습니다.
Customer key 조건은 존재 여부만 검사합니다. 승인 key allowlist에는 ArrayOfARN에 맞는 set·ARN 연산자와 누락값 처리가 필요하며 UpdateClusterConfig에는 이 조건을 사용할 수 없습니다. Customer key 요구는 소유·제어 정책입니다. EKS 1.28+는 이미 AWS-owned KMS v2 방식으로 모든 Kubernetes API 데이터를 기본 암호화합니다. CreateCluster는 cluster resource ARN 범위 제한을 지원하지 않으므로 요청 조건과 함께 `Resource: "*"`를 사용합니다.
JSON과 제한된 조건 truth-table을 로컬에서 확인했습니다. SCP 연결·IAM 역할 변경·AWS 권한 simulation은 실행하지 않았으며 rollout 전에 전체 조직·resource policy 맥락을 검증해야 합니다.
공식 참고 자료: [IAM boundaries](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html), [SCP effects](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html), [EKS authorization reference](https://docs.aws.amazon.com/service-authorization/latest/reference/list_eks.html), [EKS condition key announcement](https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-eks-iam-condition-keys/), [S3 ABAC enablement](https://docs.aws.amazon.com/AmazonS3/latest/userguide/buckets-tagging-enable-abac.html), [bucket tag conditions](https://docs.aws.amazon.com/AmazonS3/latest/userguide/buckets-tagging.html).
## 암호화 및 비밀 관리
### 기본 암호화와 Customer Key 소유권
Kubernetes 1.28+ EKS 클러스터는 Secret·ConfigMap을 포함한 **모든 Kubernetes API 데이터**를 AWS-owned key 기반 기본 KMS v2 envelope encryption으로 암호화합니다. 이는 기존 etcd disk 암호화와 별개입니다. Customer-managed KMS key를 선택하면 key 소유·제어 방식이 바뀌는 것이며 현재 EKS의 평문 저장을 처음 암호화하는 것이 아닙니다. 선택 전에 grant, key 가용성·교체·삭제 보호를 검토합니다. 이 제어가 노드·EBS·EFS의 앱 데이터나 네트워크 연결을 암호화하지는 않습니다.
Secret manifest의 Base64는 암호화가 아닙니다. API 권한, admission 권한, 백업·노드·워크로드 접근 제어도 필요합니다. API로 Secret을 읽을 수 있는 principal은 저장 시 암호화 여부와 관계없이 내용을 받습니다.
### Secret 전달 경로 선택
| 통합 | 결과와 운영 경계 |
|---|---|
| External Secrets Operator(ESO) | 설정한 backend를 읽어 Kubernetes Secret을 기록하며 앱은 일반 Secret volume·환경 변수 참조로 사용 |
| ASCP + Secrets Store CSI Driver | Backend 값을 파일로 mount하며 Kubernetes Secret 동기화는 별도로 활성화하는 driver 기능 |
| SOPS | 저장·검토할 파일을 암호화하며 통제된 배포 단계에서 복호화한 후 Kubernetes Secret 데이터를 적용 |
ESO와 CSI 동기화가 같은 target Secret을 동시에 소유하지 않도록 합니다. 선택한 provider·driver의 현재 노드 지원과 신원 방식을 확인하세요. CSI node plugin은 Fargate나 모든 hybrid 구성에서 보편적으로 제공되지 않습니다.
### Namespace 범위 IRSA 신원을 사용하는 ESO
**새로운 소유자 관리 설치** 예제는 chart·application 2.10.0과 `external-secrets.io/v1` API를 고정합니다. 먼저 기존 Helm release·CRD 소유권을 확인하며 기존 설치는 두 번째 controller를 실행하는 대신 migration 절차를 검토합니다. Chart는 Kubernetes 1.36용으로 로컬 render했으며 EKS에 배포하지 않았습니다.
```bash
helm repo add external-secrets https://charts.external-secrets.io
helm repo update external-secrets
helm install external-secrets external-secrets/external-secrets \
--version 2.10.0 --namespace external-secrets --create-namespace \
--set installCRDs=true --wait --timeout 5m
```
승인된 secret 입력 절차로 `username`·`password` 속성이 있는 Secrets Manager JSON secret을 준비합니다. 모든 예제에서 정확한 secret ARN·계정·Region·역할로 바꿉니다. 역할의 IRSA trust는 실제 IAM OIDC provider와 정확한 namespace·service-account subject를 사용해야 합니다. Dual-stack 형식 등을 포함한 **issuer hostname·path 전체**를 실제 값으로 바꾸세요:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/EXAMPLEOIDCID"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-west-2.amazonaws.com/id/EXAMPLEOIDCID:aud": "sts.amazonaws.com",
"oidc.eks.us-west-2.amazonaws.com/id/EXAMPLEOIDCID:sub": "system:serviceaccount:security-secrets-demo:eso-reader"
}
}
}
]
}
```
아래처럼 명시한 remote key의 읽기 policy는 해당 secret으로 제한할 수 있습니다. Customer-managed KMS key를 사용한다면 그 key에 맞는 `kms:Decrypt` 권한과 key policy도 필요하며 다음 예제에는 KMS grant가 없습니다:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": "arn:aws:secretsmanager:us-west-2:123456789012:secret:training/db-credentials-ABC123"
}
]
}
```
ServiceAccount·SecretStore·ExternalSecret은 같은 namespace에 둡니다. ESO는 참조한 ServiceAccount의 단기 token을 요청하므로 해당 account의 자동 token mount를 꺼도 TokenRequest 동작이 차단되지는 않습니다. Controller에는 chart에서 요구하는 Kubernetes RBAC와 Kubernetes API·regional STS·Secrets Manager 네트워크 접근이 필요합니다.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: security-secrets-demo
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: eso-reader
namespace: security-secrets-demo
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/EKSSecretReader
automountServiceAccountToken: false
---
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: aws-secretsmanager
namespace: security-secrets-demo
spec:
provider:
aws:
service: SecretsManager
region: us-west-2
auth:
jwt:
serviceAccountRef:
name: eso-reader
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: security-secrets-demo
spec:
refreshPolicy: Periodic
refreshInterval: 1h
secretStoreRef:
name: aws-secretsmanager
kind: SecretStore
target:
name: eso-db-credentials
creationPolicy: Owner
deletionPolicy: Retain
data:
- secretKey: username
remoteRef:
key: arn:aws:secretsmanager:us-west-2:123456789012:secret:training/db-credentials-ABC123
property: username
- secretKey: password
remoteRef:
key: arn:aws:secretsmanager:us-west-2:123456789012:secret:training/db-credentials-ABC123
property: password
```
여기서 `creationPolicy: Owner`는 ExternalSecret이 `eso-db-credentials`를 소유하게 하므로 ExternalSecret 삭제 시 해당 Secret도 garbage collection될 수 있습니다. `deletionPolicy: Retain`은 backend secret이 없어지는 경우에 대한 설정이며 owner garbage collection 방지가 아닙니다. Secret 데이터 출력 없이 SecretStore·ExternalSecret의 Ready condition과 이벤트를 확인하세요.
**Pod Identity 대안:** ESO controller 자체 ServiceAccount를 역할과 연결하고 store의 `auth` 블록을 생략하여 controller credential chain을 사용합니다. `auth.jwt.serviceAccountRef`를 유지한 채 ESO가 임의의 Pod Identity association account를 impersonate한다고 기대하지 마세요. Store별 IRSA와 controller Pod Identity는 다른 인증 경로이므로 controller 전체의 trust 범위를 검토합니다.
### Rotation과 앱 Reload
Backend rotation·동기화·앱 reload는 서로 다른 단계입니다. Secrets Manager는 지원 secret에 구성한 rotation을 제공하며 Parameter Store가 같은 내장 credential rotation 절차를 제공하는 것은 아닙니다. 예제의 ESO 1시간 주기는 즉시 갱신 보장이 아닙니다. 일반 Secret volume은 나중에 갱신되지만 subPath mount는 갱신을 받지 못하고 기존 환경 변수도 바뀌지 않습니다. 앱의 파일 재개방·reload 또는 통제된 rollout이 필요하며 credential에 맞는 중첩 유효 기간·복구를 설계합니다.
`aws secretsmanager rotate-secret`은 기본적으로 즉시 rotation합니다. `--no-rotate-immediately`도 Lambda rotation 구성을 시험하며 AWSPENDING version을 생성·제거할 수 있고, 이전 rate·day 기반 일정의 rotation이 실행될 수도 있습니다. 읽기 전용 검증 명령이 아닙니다. 실행 전 rotation 함수·권한·네트워크·일정을 검토하세요. 이번 검토에서 secret rotation은 수행하지 않았습니다.
### 파일 암호화를 위한 SOPS
SOPS는 getsops 프로젝트가 관리하며 “Mozilla SOPS”는 과거 출처입니다. 검증한 release·설치 도구를 사용하세요. 다음 SOPS 3.13.3 문법은 검토한 AWS KMS key로 Kubernetes Secret의 data·stringData 필드를 암호화합니다. 평문은 Git 밖에 보관하고 secret 값을 명령 인수로 전달하거나 shell tracing을 켜지 마세요:
```bash
set -euo pipefail
umask 077
: "${SOPS_KMS_ARN:?Set the reviewed KMS key ARN}"
: "${PLAINTEXT_FILE:?Set a protected YAML file outside the Git working tree}"
: "${ENCRYPTED_FILE:?Set a new output path for the encrypted YAML}"
test -f "$PLAINTEXT_FILE"
test ! -e "$ENCRYPTED_FILE"
sops encrypt --kms "$SOPS_KMS_ARN" \
--input-type yaml --output-type yaml \
--encrypted-regex '^(data|stringData)$' \
--output "$ENCRYPTED_FILE" "$PLAINTEXT_FILE"
```
이 regex는 metadata·다른 필드를 노출하며 임의 YAML이 아닌 Kubernetes Secret 파일용입니다. `.sops.yaml` creation rule의 `path_regex`는 shell redirection 출력 경로가 아니라 입력 경로 또는 `--filename-override`를 검사합니다. Staging 전에 암호화된 결과를 확인하세요. KMS 접근에는 실제 caller 권한·key policy가 필요합니다.
승인된 로컬 검증에서는 private 임시 파일로 복호화하고 종료 시 제거하며 CI log에 평문을 출력하지 않습니다:
```bash
set -euo pipefail
umask 077
: "${ENCRYPTED_FILE:?Set the reviewed encrypted YAML path}"
review_dir=$(mktemp -d "${TMPDIR:-/tmp}/eks-secret-review.XXXXXXXX")
trap 'rm -rf -- "$review_dir"' EXIT
sops decrypt --output "$review_dir/secret.yaml" "$ENCRYPTED_FILE"
# Use this private file only in an authorized local validation step.
# Do not print it, commit it, or enable shell tracing.
test -s "$review_dir/secret.yaml"
```
임시 파일 삭제가 모든 파일 시스템에서 안전한 소거를 보장하지는 않습니다. 실행 환경·백업도 보호하세요. Terraform `sensitive`는 일부 출력을 숨길 뿐 secret 값을 state에서 자동 제외하지 않습니다. 실제 secret 값을 예제 Terraform resource나 보호되지 않은 plan·state 산출물에 넣지 마세요.
검증은 배포된 ESO CRD·chart, Kubernetes 기본 schema와 합성 데이터의 로컬 age 기반 SOPS 암호화를 사용합니다. AWS KMS 암복호화·secret 조회·controller reconciliation·앱 rotation은 실제 환경에서 확인할 사항이며 실행하지 않았습니다.
공식 참고 자료: [EKS envelope encryption](https://docs.aws.amazon.com/eks/latest/userguide/envelope-encryption.html), [ESO 2.10 AWS authentication](https://github.com/external-secrets/external-secrets/blob/v2.10.0/docs/provider/aws-access.md), [ESO release](https://github.com/external-secrets/external-secrets/releases/tag/helm-chart-2.10.0), [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/), [Secrets Manager rotation CLI](https://docs.aws.amazon.com/cli/latest/reference/secretsmanager/rotate-secret.html), [SOPS](https://getsops.io/docs/).
## 컴플라이언스 및 감사
### EKS 컨트롤 플레인 감사 로그
Kubernetes audit log는 audit policy·level이 선택한 요청을 기록하며 모든 요청 본문이나 워크로드 내부 작업을 빠짐없이 기록하는 보장이 아닙니다. CloudTrail은 AWS API 활동을 기록하고 앱 데이터 접근에는 앱·서비스별 log가 필요할 수 있습니다. 각 자료는 상호 보완합니다.
먼저 실제 클러스터 logging 설정을 확인하세요. EKS의 CloudWatch control plane log 전달은 best effort이며 보통 수분 이내에 이루어지고 수집·저장 비용이 발생합니다. 보존 기간·접근 제어·후속 전달·log 누락 탐지를 구성합니다. Export를 켠다고 이전에 내보내지 않은 이벤트가 소급 생성되지는 않습니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the owned cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query 'cluster.{ARN:arn,Status:status,Logging:logging}'
```
승인한 logging 변경에서는 아래처럼 다섯 log type을 활성화하고 반환된 update를 확인할 수 있습니다. 먼저 subnet 여유 IP 요건, 계정·클러스터 신원과 진행 중인 update 상태를 검토합니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the reviewed cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
UPDATE_ID=$(aws eks update-cluster-config \
--name "$CLUSTER_NAME" --region "$AWS_REGION" \
--logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}' \
--query 'update.id' --output text)
if [[ -z "$UPDATE_ID" || "$UPDATE_ID" == None ]]; then
printf '%s\n' 'No update ID returned; inspect the request result.' >&2
exit 1
fi
aws eks describe-update --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--update-id "$UPDATE_ID" \
--query 'update.{ID:id,Status:status,Errors:errors}'
```
마지막 DescribeUpdate는 waiter가 아닌 상태 조회입니다. Successful 또는 최종 실패가 될 때까지 재조회한 뒤 설정한 유형과 `/aws/eks/CLUSTER_NAME/cluster`의 실제 log 도착을 확인합니다. UpdateClusterConfig가 ID를 반환했다는 이유만으로 완료를 보고하지 않습니다.
### AWS Config와 Security Hub CSPM
정확한 managed rule과 parameter·Region 지원을 확인합니다. 비슷한 이름의 logging 규칙 두 개는 모두 유효합니다:
| 규칙 | 검사 범위 |
|---|---|
| `eks-cluster-logging-enabled` | 모든 control plane log type 활성화 여부를 주기적으로 검사하며 parameter 없음 |
| `eks-cluster-log-enabled` | 구성 변경 기반 검사이며 선택적 `logTypes` CSV로 유형 지정 |
| `eks-cluster-oldest-supported-version` | 지정한 `oldestVersionSupported`와 비교하므로 parameter 관리 필요; 자동 갱신 지원 catalog가 아님 |
| `eks-endpoint-no-public-access` | Endpoint가 공개 접근 가능한지 검사 |
| `eks-secrets-encrypted` | 명시적 encryptionConfig·secrets와 선택적 `kmsKeyArns` 검사; finding이 현재 EKS API 데이터의 평문 저장을 의미하지는 않음 |
Security Hub CSPM은 활성화한 AWS Foundational Security Best Practices(FSBP), 지원 CIS AWS Foundations 등의 표준에 포함된 지원 control을 평가합니다. FSBP는 CIS Kubernetes Benchmark가 아닙니다. Control 통과·점수는 앱 인증, 전체 Kubernetes 강화 감사, PCI DSS·HIPAA·개인정보 규정 준수의 증명이 아닙니다. AWS Config recording, 지원 resource·Region 범위, control 상태와 중앙 구성의 영향을 확인합니다.
이미 활성화된 소유 CSPM 계정·Region에서는 `aws securityhub describe-hub --region YOUR_REGION`, `aws securityhub get-enabled-standards --region YOUR_REGION`으로 상태를 확인합니다. FSBP subscription ARN은 `standards/aws-foundational-security-best-practices/v/1.0.0`으로 끝나며 이를 CIS subscription이라고 표시하지 않습니다. 중앙 구성을 사용한다면 활성화·표준 변경은 delegated administrator와 조정합니다.
현재 Security Hub OCSF finding 경로와 CSPM ASFF 이벤트의 schema는 다릅니다. EventBridge에서 CSPM 이벤트는 `Security Hub Findings - Imported`, V2는 `Findings Imported V2`입니다. 실제 schema로 matching하고 대표 이벤트를 시험합니다. 어느 통합도 모든 CloudWatch 원시 log를 자동으로 finding으로 수집한다는 뜻은 아닙니다.
## 보안 모니터링 및 탐지
Audit 기반 위협 탐지, agent 기반 runtime telemetry, posture 평가와 인시던트 대응은 별도 제어입니다. 서비스 enabled 상태만으로 전체 클러스터·노드 coverage나 알림 전달 성공을 입증할 수는 없습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-05-eks-security-15.html)
### GuardDuty Audit·Runtime Coverage
**EKS Protection**은 GuardDuty의 독립된 stream으로 Kubernetes audit log를 분석합니다. 고객 CloudWatch audit log export는 조사에 유용하지만 GuardDuty EKS audit 분석의 선행 요건은 아닙니다.
**Runtime Monitoring**은 GuardDuty security agent와 data endpoint를 사용합니다. 현재 EKS runtime coverage는 EC2 노드·EKS Auto Mode를 지원하지만 EKS Fargate·Hybrid Nodes는 지원하지 않습니다. 공개 agent·OS·Kubernetes 호환표와 실제 resource coverage 상태를 확인하세요. Agent 호환표의 오래된 OS 항목이 OS 자체의 지원 수명을 연장하지는 않습니다.
```bash
set -euo pipefail
: "${AWS_REGION:?Set the reviewed Region}"
: "${DETECTOR_ID:?Select the owned regional detector; do not pick an arbitrary first result}"
aws guardduty get-detector --region "$AWS_REGION" --detector-id "$DETECTOR_ID" \
--query '{Status:Status,Features:Features}'
aws guardduty list-coverage --region "$AWS_REGION" --detector-id "$DETECTOR_ID"
```
검토한 feature 구성에서 현재 이름은 다음처럼 구분합니다:
```json
[
{
"Name": "EKS_AUDIT_LOGS",
"Status": "ENABLED"
},
{
"Name": "RUNTIME_MONITORING",
"Status": "ENABLED"
}
]
```
이는 feature payload 예제이며 계정 rollout 전체가 아닙니다. 먼저 기존 Region detector·조직 정책을 조사합니다. 이전 `EKS_RUNTIME_MONITORING`이 켜져 있다면 공식 `RUNTIME_MONITORING` migration 절차를 따르며 호환되지 않는 구·신 모드를 함께 켜지 않습니다.
수동·자동 agent 관리 방식을 의도적으로 선택합니다. 자동 관리는 agent 배포와 GuardDuty data endpoint·security group 생성을 수행할 수 있으며 inclusion·exclusion tag와 편집 권한이 coverage에 영향을 줍니다. 수동 관리는 지원 agent·접근 가능한 data endpoint가 필요합니다. Coverage와 통제된 finding·알림 경로를 시험하며 API 성공 응답만으로 보호가 동작한다고 판단하지 않습니다.
### Falco Runtime 규칙
Falco는 runtime event를 규칙으로 평가합니다. 아래 교육용 values는 stable chart 9.1.0·Falco 0.44.1, modern eBPF를 지원하는 Linux EC2 노드와 chart의 container metadata plugin을 전제로 합니다. Fargate·Hybrid Nodes·모든 Auto Mode 구성의 배포 검증 결과가 아닙니다. 설치 전에 render된 privileged·host 접근, runtime socket, kernel·BTF 요건, scheduling과 namespace admission 예외를 검토합니다.
`falco-demo-values.yaml`로 저장합니다. 배포된 Terminal shell in container 규칙을 재정의하는 대신 고유 이름의 shell audit 규칙을 추가합니다. 성공한 exec event·terminal 조건을 포함하며 정상적인 관리자 shell도 matching될 수 있습니다. 출력은 명령 인수를 제외하고 Kubernetes metadata enrichment가 활성화되었다고 가정하지 않습니다:
```yaml
driver:
kind: modern_ebpf
loader:
enabled: false
falcoctl:
artifact:
follow:
enabled: false
customRules:
eks-shell-demo.yaml: |
- rule: Interactive shell in container - EKS demo
desc: Audit successful shell execution with a terminal; tune expected administrative use.
condition: evt.type in (execve, execveat) and evt.rawres=0 and container.id != host and proc.name in (bash,
sh, dash, ash, zsh, ksh) and proc.tty != 0
output: Interactive container shell | container_id=%container.id user_uid=%user.uid process=%proc.name parent=%proc.pname
terminal=%proc.tty
priority: NOTICE
source: syscall
tags:
- container
- audit
```
검토를 마친 신규 소유 설치에서는 `falcosecurity` repository를 `https://falcosecurity.github.io/charts`로 구성한 뒤 `helm install falco falcosecurity/falco --version 9.1.0 --namespace falco --create-namespace -f falco-demo-values.yaml --wait --timeout 5m`을 사용합니다. 기존 release는 소유자의 upgrade 절차를 따릅니다.
Artifact follow를 꺼도 Pod 시작 시 설정된 rule·plugin artifact는 설치합니다. Chart 기본 `falco-rules:5`는 major version tag이므로 chart 고정만으로 불변 rule bundle이 되지 않습니다. 운영 rollout에서는 실제 OCI artifact를 검토·고정합니다. Pod·namespace 출력에는 호환 Kubernetes metadata collector·plugin과 RBAC가 필요하며 규칙에 `%k8s.pod.name`만 쓰면 제공되는 기능이 아닙니다.
실제 Falco 0.44.1·container plugin 0.7.1의 `--validate` 모드에서 모든 runtime collector를 끈 상태로 custom rule과 undefined-macro 거부 사례를 확인했습니다. Parser 경고에 따라 obsolete `evt.dir` 조건도 제거했습니다. Helm render는 확인했지만 syscall capture·kernel driver·BPF attachment·클러스터 설치·실제 알림은 실행하지 않았습니다.
공식 참고 자료: [EKS logs](https://docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html), [all-type Config rule](https://docs.aws.amazon.com/config/latest/developerguide/eks-cluster-logging-enabled.html), [selected-type Config rule](https://docs.aws.amazon.com/config/latest/developerguide/eks-cluster-log-enabled.html), [CSPM standards](https://docs.aws.amazon.com/securityhub/latest/userguide/standards-view-manage.html), [ASFF events](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cwe-event-formats.html), [V2 events](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-v2-cwe-event-formats.html), [GuardDuty EKS Protection](https://docs.aws.amazon.com/guardduty/latest/ug/kubernetes-protection.html), [GuardDuty runtime](https://docs.aws.amazon.com/guardduty/latest/ug/how-runtime-monitoring-works-eks.html), [Falco chart 9.1.0](https://github.com/falcosecurity/charts/releases/tag/falco-9.1.0).
## EKS 보안 모범 사례
### 클러스터 보안 강화
1. **지원되는 호환 버전 유지**: AWS 지원 기간 안에서 워크로드·노드·CNI·CSI·add-on 호환성을 확인하고 EKS upgrade를 계획하며 최신 upstream Kubernetes 버전을 자동 선택하지 않음
2. **프라이빗 API 엔드포인트 사용**: 퍼블릭 인터넷에서 API 서버에 대한 액세스 제한
3. **최소 권한 원칙 적용**: IAM 역할 및 RBAC에 최소 권한 원칙 적용
4. **보안 그룹 제한**: 필요한 포트만 허용하도록 보안 그룹 구성
5. **네트워크 정책 구현**: 파드 간 통신을 제한하는 네트워크 정책 적용
### 노드 및 컨테이너 보안
1. **패치된 노드 이미지 유지**: 클러스터·아키텍처에 맞는 지원 OS·AMI를 선택하고 canary에서 업데이트 검증
2. **이미지 스캔·검증**: 구성한 ECR·Inspector 또는 다른 scanner와 필요한 provenance·서명 검증을 적용하며 scan이 모든 backdoor·취약점 부재를 증명하지는 않음
3. **노드 업데이트 조정**: 통제된 인스턴스 교체 또는 지원 Bottlerocket in-place 조정 방식을 사용하고 기존 용량 제거 전에 여유 용량·PDB·로컬 데이터·앱 상태 확인
4. **비 루트 사용자로 컨테이너 실행**: 컨테이너를 비 루트 사용자로 실행하여 권한 제한
5. **읽기 전용 파일 시스템 사용**: 가능한 경우 컨테이너의 루트 파일 시스템을 읽기 전용으로 마운트
### 지속적인 보안 모니터링
1. **감사 로깅 활성화**: EKS 컨트롤 플레인 감사 로그 활성화
2. **GuardDuty Coverage 확인**: EKS Protection의 audit 분석과 agent 기반 Runtime Monitoring을 구분하고 실제 노드 coverage 검증
3. **Security Hub 통합**: CSPM control과 finding 수집·schema를 검토하고 인시던트 전달·대응 소유권 시험
4. **정기적인 보안 평가**: 해당 CIS·EKS benchmark 버전을 사용하고 관리형 서비스 예외·수동 검사 기록
5. **인시던트 대응 계획 수립**: EKS 클러스터에 대한 보안 인시던트 대응 계획 수립 및 테스트
## 금융 서비스를 위한 EKS 보안 고려사항
다음은 범위를 정한 금융 워크로드의 설계 고려사항이며 인증 checklist나 예제의 운영 검증 완료 주장이 아닙니다. 담당 보안·준법 소유자와 적용 관할권·데이터 유형·계약 요건·제어 증거를 정해야 합니다.
### 규제 준수
1. **PCI DSS**: 카드 결제 데이터를 처리하는 워크로드에 대한 PCI DSS 요구사항 준수
2. **GDPR/CCPA**: 개인 식별 정보(PII)에 대한 데이터 보호 규정 준수
3. **금융 규제**: 국내 금융 규제 기관의 요구사항 준수(예: 금융감독원 지침)
### 데이터 보안
1. **전송 중 암호화**: 현재 승인된 TLS protocol·cipher를 선택하고 각 hop을 확인하며 ALB listener TLS만으로 평문 ALB-to-Pod 연결까지 암호화되지는 않음
2. **저장 데이터 암호화**: 각 datastore·volume·backup·API 데이터 경로의 암호화와 key 소유권 구성·검증
3. **데이터 분류**: 민감도에 따른 데이터 분류 및 적절한 보안 제어 적용
4. **데이터 접근 로깅**: 필요한 감사 범위·보존 기간을 정의하고 전달 여부를 검증하며 log에 credential·민감 payload가 노출되지 않도록 보호
### 고가용성 및 재해 복구
1. **다중 가용 영역 배포**: 워크로드 replica를 분산하고 storage·database·AZ 장애 동작을 확인하며 관리형 multi-AZ control plane만으로 모든 앱의 고가용성이 보장되지는 않음
2. **재해 복구 계획**: 정기적인 백업 및 복구 테스트를 포함한 재해 복구 계획 수립
3. **비즈니스 연속성**: 금융 서비스에 적합한 RTO(Recovery Time Objective) 및 RPO(Recovery Point Objective) 정의
### 금융 서비스를 위한 EKS 보안 아키텍처 예시

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-05-eks-security-16.html)
이는 제어 배치의 논리도입니다. WAF는 지원 ingress resource에 연결하는 제어이며 별도로 routing하는 network hop이 아닙니다. 실제 TLS 종료·재암호화, 네트워크 경로, 데이터 서비스 권한, finding coverage와 대응 절차를 정의해야 하며 그림 자체가 규제 준수·운영 검증 완료를 입증하지 않습니다.
## 결론
EKS 보안은 IAM·Kubernetes 권한, workload admission, 네트워크 제어, 암호화, 노드 이미지 유지 관리와 관찰 가능한 인시던트 대응을 결합합니다. 각 제어의 범위·실패 방식은 다르므로 enabled 상태나 sample manifest를 보호의 증거로 삼지 말고 실제 동작·예외를 검증해야 합니다.
특히 금융 서비스와 같은 규제가 엄격한 산업에서는 추가적인 보안 제어 및 규정 준수 요구사항을 고려해야 합니다. 정기적인 보안 평가, 취약점 스캔, 그리고 지속적인 모니터링을 통해 EKS 환경의 보안 상태를 유지하는 것이 중요합니다.
## 참고 자료
- [Amazon EKS 보안 모범 사례](https://docs.aws.amazon.com/eks/latest/best-practices/security.html)
- [Kubernetes 보안 모범 사례](https://kubernetes.io/docs/concepts/security/overview/)
- [CIS Kubernetes Benchmark](https://www.cisecurity.org/benchmark/kubernetes)
- [AWS Security Hub](https://aws.amazon.com/security-hub/)
- [Amazon GuardDuty](https://aws.amazon.com/guardduty/)
- [Amazon EKS Customer-Routed Control Plane Egress (2026-06-18)](https://aws.amazon.com/about-aws/whats-new/2026/06/amazon-eks-customer-routed-control-plane-egress/)
- [Amazon EKS 신규 IAM Condition Key 7종 (2026-04-20)](https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-eks-iam-condition-keys/)
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/05-eks-security-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/06-eks-monitoring-logging
----------------------------------------
# Amazon EKS 모니터링 및 로깅
> **마지막 업데이트**: 2026년 9월 12일
효과적인 모니터링 및 로깅은 Amazon EKS 클러스터의 안정성, 가용성 및 성능을 유지하는 데 필수적입니다. 이 문서에서는 EKS 클러스터에서 모니터링 및 로깅을 구현하기 위한 다양한 도구, 기술 및 모범 사례를 다룹니다.
## 목차
1. [모니터링 및 로깅 개요](#모니터링-및-로깅-개요)
2. [EKS 컨트롤 플레인 로깅](#eks-컨트롤-플레인-로깅)
3. [컨테이너 로깅](#컨테이너-로깅)
4. [클러스터 모니터링](#클러스터-모니터링)
5. [알림 및 이벤트 관리](#알림-및-이벤트-관리)
6. [로그 분석 및 시각화](#로그-분석-및-시각화)
7. [모니터링 및 로깅 모범 사례](#모니터링-및-로깅-모범-사례)
8. [문제 해결 및 디버깅](#문제-해결-및-디버깅)
## 모니터링 및 로깅 개요
### 모니터링과 로깅의 중요성
Amazon EKS 클러스터에서 모니터링과 로깅은 다음과 같은 이유로 중요합니다:
1. **가시성 확보**: 클러스터의 상태, 성능 및 동작에 대한 가시성 제공
2. **문제 감지**: 문제가 심각해지기 전에 조기 감지
3. **트렌드 분석**: 시간에 따른 성능 및 리소스 사용량 추세 파악
4. **용량 계획**: 리소스 요구사항 예측 및 계획
5. **보안 및 감사**: 조사와 적용되는 제어에 필요한 증거 확보 지원
6. **문제 해결**: 문제 발생 시 신속한 진단 및 해결
### 모니터링 및 로깅 아키텍처
EKS 클러스터의 포괄적인 모니터링 및 로깅 아키텍처는 다음과 같은 구성 요소로 이루어집니다:
관리형 control plane log는 AWS가 CloudWatch Logs로 전달합니다. Container runtime은 CRI log 파일을 기록하고 kubelet은 rotation·log 접근을 관리하며 노드 collector는 파일을 읽습니다. Metric·trace는 별도로 구성한 수집·export 경로를 사용합니다.
### 모니터링 및 로깅 전략
효과적인 모니터링 및 로깅 전략을 개발하려면 다음 단계를 따르세요:
1. **목표 정의**: 모니터링 및 로깅의 목표와 요구사항 정의
2. **지표 및 로그 식별**: 수집할 핵심 지표 및 로그 식별
3. **도구 선택**: 요구사항에 맞는 모니터링 및 로깅 도구 선택
4. **기준선 설정**: 정상 동작에 대한 기준선 설정
5. **알림 구성**: 중요한 이벤트 및 임계값에 대한 알림 구성
6. **자동화**: 가능한 한 모니터링 및 로깅 프로세스 자동화
7. **정기적인 검토**: 모니터링 및 로깅 전략 정기적 검토 및 개선
## EKS 컨트롤 플레인 로깅
EKS는 선택한 관리형 control plane log type을 CloudWatch Logs로 export합니다. 노드 collector가 관리형 control plane 파일 시스템을 직접 scrape하는 방식이 아닙니다. 전달은 best effort이므로 접근·보존·log 누락 탐지를 구성하고 실제 도착을 확인합니다.
### 컨트롤 플레인 로그 유형
| 유형 | 목적 |
|---|---|
| `api` | API server 구성 요소 진단 |
| `audit` | Audit policy·level이 선택한 Kubernetes 요청 |
| `authenticator` | EKS IAM 인증 진단 |
| `controllerManager` | Core controller-manager 동작 |
| `scheduler` | Scheduler 결정·진단 |
Audit log는 모든 요청 본문·앱 작업을 빠짐없이 기록하는 보장이 아닙니다. Secret 관련 기록은 metadata만 남길 수 있고 일부 이벤트는 policy에서 제외합니다. CloudTrail의 AWS API 기록과 앱 데이터 접근 log는 별도 증거입니다.
### 로깅 조회·활성화
변경 전에 소유 클러스터의 Region·ARN·현재 logging 설정과 update 상태를 확인합니다. Logging 변경에는 문서화된 subnet 여유 IP가 필요하고 CloudWatch 수집·저장 비용이 발생합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the owned cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query 'cluster.{ARN:arn,Status:status,Logging:logging}'
```
승인한 변경에서는 아래처럼 다섯 log type을 활성화하고 반환된 update를 확인합니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the reviewed cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
UPDATE_ID=$(aws eks update-cluster-config \
--name "$CLUSTER_NAME" --region "$AWS_REGION" \
--logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}' \
--query 'update.id' --output text)
if [[ -z "$UPDATE_ID" || "$UPDATE_ID" == None ]]; then
printf '%s\n' 'No update ID returned; inspect the request result.' >&2
exit 1
fi
aws eks describe-update --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--update-id "$UPDATE_ID" \
--query 'update.{ID:id,Status:status,Errors:errors}'
```
마지막 명령은 waiter가 아닌 상태 조회입니다. Successful 또는 최종 실패가 될 때까지 DescribeUpdate를 반복한 뒤 실제 설정·log 도착을 확인합니다. Update ID만으로 완료가 증명되지는 않습니다. 선택한 유형 활성화를 위해 기존 다른 유형을 끌 필요는 없으며 명시적인 disable은 보존·가시성에 대한 별도 결정입니다.
검토한 eksctl CLI에서 --approve를 생략하면 변경을 preview합니다. CLUSTER_NAME·AWS_REGION을 확인한 같은 클러스터로 설정하고 preview를 확인합니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the verified cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
eksctl utils update-cluster-logging \
--region "$AWS_REGION" --cluster "$CLUSTER_NAME" \
--enable-types api,audit,authenticator,controllerManager,scheduler
```
해당 계획을 검토한 뒤 별도 apply 명령을 사용합니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the verified cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
eksctl utils update-cluster-logging \
--region "$AWS_REGION" --cluster "$CLUSTER_NAME" \
--enable-types api,audit,authenticator,controllerManager,scheduler --approve
```
### 컨트롤 플레인 로그 조회
실제 `/aws/eks/CLUSTER_NAME/cluster` group을 사용합니다. 앞의 두 예제는 선택한 구성 요소 stream의 문자열 검색이며 완전한 오류율 측정이 아닙니다.
**API 진단:**
```text
fields @timestamp, @message
| filter @logStream like /kube-apiserver-/ and @logStream not like /audit/
| filter @message like /[Ee]rror/
| sort @timestamp desc
| limit 20
```
**IAM 인증 진단:**
```text
fields @timestamp, @message
| filter @logStream like /authenticator/
| filter @message like /[Ff]ailed|[Dd]enied|[Uu]nauthorized/
| sort @timestamp desc
| limit 20
```
**Audit 거부:** 원시 JSON에 responseStatus.code 문자열이 그대로 있다고 가정하지 말고 발견된 JSON 필드를 사용합니다. 표본 이벤트의 필드 구조를 확인하세요.
```text
fields @timestamp, user.username, verb, objectRef.resource, objectRef.namespace, responseStatus.code
| filter @logStream like /kube-apiserver-audit/
| filter responseStatus.code in [401, 403]
| sort @timestamp desc
| limit 20
```
### 보존 기간과 비용 관리
Prefix 검색 결과에서 정확한 group 항목을 확인합니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the owned cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
aws logs describe-log-groups --region "$AWS_REGION" \
--log-group-name-prefix "/aws/eks/$CLUSTER_NAME/cluster" \
--query 'logGroups[].{Name:logGroupName,RetentionDays:retentionInDays,KmsKey:kmsKeyId}'
```
승인한 정책이 30일을 요구한다면 `aws logs put-retention-policy --region "$AWS_REGION" --log-group-name "/aws/eks/$CLUSTER_NAME/cluster" --retention-in-days 30`으로 변경합니다. 30일은 예시 정책이며 보편적인 준법 요건이 아닙니다. 보존 기간 축소는 오래된 증거를 만료시킬 수 있고 확대해도 삭제된 log가 복구되지는 않습니다. Retention만으로 archive 전달·무결성·준수가 입증되지는 않습니다.
### EKS Capabilities 로깅 (GitOps, ACK, kro)
2026년 6월 4일 발표된 기능은 지원됩니다. ACK·kro·Argo CD capability controller는 **고객 클러스터 밖의 AWS 관리 인프라**에서 실행되며 CloudWatch Vended Logs로 구조화된 controller log를 전달합니다. 이는 다섯 표준 control plane log type과 별도의 capability별 delivery 구성입니다.
| Capability | Log type |
|---|---|
| ACK | `EKS_CAPABILITY_ACK_LOGS` |
| kro | `EKS_CAPABILITY_KRO_LOGS` |
| Argo CD | `EKS_CAPABILITY_ARGOCD_APPLICATION_LOGS`, `EKS_CAPABILITY_ARGOCD_APPLICATIONSET_LOGS`, `EKS_CAPABILITY_ARGOCD_COMMITSERVER_LOGS`, `EKS_CAPABILITY_ARGOCD_REPOSERVER_LOGS`, `EKS_CAPABILITY_ARGOCD_SERVER_LOGS` |
Delivery source를 구성하기 전에 실제 capability ARN을 확인합니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the owned cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
: "${CAPABILITY_NAME:?Set the actual capability name}"
aws eks describe-capability --region "$AWS_REGION" \
--cluster-name "$CLUSTER_NAME" --capability-name "$CAPABILITY_NAME" \
--query 'capability.capabilityArn' --output text
```
소유자는 해당 ARN·log type으로 PutDeliverySource, 승인한 목적지로 PutDeliveryDestination, 둘을 연결하는 CreateDelivery를 구성합니다. 활성화 전에 목적지 policy·암호화·cross-account 권한·보존을 검토합니다.
CloudWatch Logs 목적지는 Logs Insights로 조회할 수 있습니다. S3 목적지는 선택한 S3·Athena 경로의 객체이며 Firehose는 설정된 target으로 전달합니다. 이들이 자동으로 CloudWatch Logs Insights의 조회 대상이 되지는 않습니다. ACK 기록의 controllerGroup으로 service controller를 구분할 수 있습니다. 실제 전달·query 필드를 확인하고 Vended Logs 비용을 고려합니다.
참고: [EKS control plane logging](https://docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html), [audit policy·query 예제](https://docs.aws.amazon.com/eks/latest/best-practices/auditing-and-logging.html), [capability controller log](https://docs.aws.amazon.com/eks/latest/userguide/capabilities-controller-logs.html).
## 컨테이너 로깅
Container runtime은 stdout·stderr를 노드의 CRI log 파일에 기록합니다. Kubelet은 rotation·Pod log 접근을 관리하고 collector는 파일을 읽어 선택한 backend로 전달합니다. JSON 앱 메시지도 CRI wrapper 안에 있으므로 전체 컨테이너 log line을 Docker JSON으로 가정해 파싱하면 안 됩니다.
### Collector 소유권 선택
CloudWatch Observability add-on에는 컨테이너 log collector가 포함되므로 이를 선택한 stack이라면 소유자가 해당 구성을 관리합니다. 아래 standalone Fluent Bit는 독립적으로 관리하는 log pipeline의 대안입니다. 중복 전송·비용을 설계하지 않은 채 같은 파일·목적지에 collector를 겹쳐 설치하지 않습니다.
### 실제 연결된 Fluent Bit 예제
예제는 공개 release·구성을 확인한 AWS chart 0.2.0과 image 3.4.14(Fluent Bit 5.0.9)를 사용합니다. 소유한 Linux·containerd EC2 노드를 대상으로 하며 affinity에서 이 예제의 Fargate·Auto Mode·Hybrid label을 제외합니다. 해당 mode에는 별도의 지원 수집·신원 설계가 필요합니다. 선택한 노드의 taint를 허용하므로 배치 범위와 platform agent admission 권한을 검토합니다.
logging namespace, logging/eks-log-collector ServiceAccount를 위한 최소 권한 IRSA 역할 EKSLogWriter, 소유 CloudWatch log group을 준비합니다. 계정·역할·Region·클러스터별 group을 일관되게 바꾸고 보안 장처럼 IAM OIDC provider·trust audience·subject를 구성합니다. Collector에는 CloudWatch stream·쓰기 권한과 regional STS·backend 연결이 필요합니다. IAM 선행 요건과 Kubernetes metadata RBAC는 별개입니다.
fluent-bit-values.yaml로 저장합니다:
```yaml
fullnameOverride: eks-log-collector
image:
tag: 3.4.14
serviceAccount:
create: true
name: eks-log-collector
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/EKSLogWriter
nodeSelector:
kubernetes.io/os: linux
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: eks.amazonaws.com/compute-type
operator: NotIn
values:
- fargate
- auto
- hybrid
tolerations:
- operator: Exists
service:
extraService: 'Flush 5
Log_Level info
Daemon Off
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_Port 2020
Health_Check On
storage.path /var/fluent-bit/state/storage
storage.sync normal
storage.checksum On
storage.backlog.mem_limit 20M
'
input:
path: /var/log/containers/*.log
db: /var/fluent-bit/state/tail.db
multilineParser: cri
skipLongLines: 'On'
extraInputs: 'storage.type filesystem
Read_from_Head On
'
filter:
kubeURL: https://kubernetes.default.svc:443
mergeLog: 'On'
mergeLogKey: data
keepLog: 'On'
k8sLoggingParser: 'Off'
k8sLoggingExclude: 'Off'
extraFilters: 'Use_Kubelet Off
'
cloudWatch:
enabled: false
cloudWatchLogs:
enabled: true
region: us-west-2
logGroupName: /aws/eks/my-cluster/application
logStreamPrefix: unmatched-
logStreamTemplate: $kubernetes['namespace_name'].$kubernetes['pod_name'].$kubernetes['container_name']
autoCreateGroup: false
extraOutputs: 'auto_create_group false
storage.total_limit_size 512M
'
volumes:
- name: varlog
hostPath:
path: /var/log
type: Directory
- name: state
hostPath:
path: /var/lib/eks-log-collector
type: DirectoryOrCreate
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: state
mountPath: /var/fluent-bit/state
```
Chart 0.2.0은 native cloudWatchLogs 출력을 기본 활성화하고 이전 cloudWatch 출력은 비활성화합니다. 따라서 cloudWatch.region만 지정하면 실제 native 출력에 적용되지 않습니다. 위 values는 실제 출력의 Region·group을 설정하고 전체 record를 유지합니다. log_key를 지정하면 선택한 값만 전송되어 Kubernetes context가 빠질 수 있습니다.
logStreamTemplate은 record accessor 문법과 fallback prefix를 사용합니다. logStreamPrefix는 record별 Kubernetes 표현식이 아닌 고정 prefix입니다. CRI multiline parser가 컨테이너 wrapper를 처리하고 Merge_Log는 JSON 앱 필드를 data 아래에 넣으며 log도 유지합니다. 이 예제에서는 workload annotation으로 parser를 선택하거나 수집에서 제외할 수 없습니다.
### Render 단계의 Metadata RBAC 제한
공개 chart의 넓은 ClusterRole에는 nodes/proxy와 이전 PodSecurityPolicy 규칙이 있습니다. 이 API server metadata 방식은 Use_Kubelet Off를 명시하며 선택한 Pod·Namespace 읽기 권한을 사용하고 kubelet proxy 접근은 사용하지 않습니다. Python 3·PyYAML post-renderer를 fluent-bit-rbac.py로 저장합니다. 예상과 다른 chart 신원이 나오면 다른 넓은 역할을 그대로 남기지 않고 중단합니다:
```python
#!/usr/bin/env python3
"""Helm post-renderer for this pinned, owned metadata-only Fluent Bit setup."""
import sys
import yaml
objects = [obj for obj in yaml.safe_load_all(sys.stdin) if obj is not None]
roles = [obj for obj in objects if obj.get("kind") == "ClusterRole"]
bindings = [obj for obj in objects if obj.get("kind") == "ClusterRoleBinding"]
if len(roles) != 1 or roles[0].get("metadata", {}).get("name") != "eks-log-collector":
raise SystemExit("Unexpected chart RBAC; review this renderer before proceeding")
if len(bindings) != 1 or bindings[0].get("roleRef", {}).get("name") != "eks-log-collector":
raise SystemExit("Unexpected chart role binding")
subjects = bindings[0].get("subjects", [])
if len(subjects) != 1 or any(
subjects[0].get(key) != value
for key, value in {
"kind": "ServiceAccount", "name": "eks-log-collector", "namespace": "logging"
}.items()
):
raise SystemExit("Unexpected collector identity")
if any(obj.get("kind") == "PodSecurityPolicy" for obj in objects):
raise SystemExit("Obsolete PodSecurityPolicy output is not supported by this example")
roles[0]["rules"] = [{
"apiGroups": [""],
"resources": ["namespaces", "pods"],
"verbs": ["get", "list", "watch"],
}]
yaml.safe_dump_all(objects, sys.stdout, sort_keys=False)
```
신규 소유 release는 values와 renderer를 함께 사용하여 설치합니다:
```bash
helm repo add eks https://aws.github.io/eks-charts
helm repo update eks
chmod +x fluent-bit-rbac.py
helm install eks-log-collector eks/aws-for-fluent-bit \
--version 0.2.0 --namespace logging --create-namespace \
-f fluent-bit-values.yaml --post-renderer ./fluent-bit-rbac.py \
--wait --timeout 5m
```
모든 upgrade에도 같은 검토한 post-renderer를 사용하고 chart version·release 신원·namespace·metadata 방식을 바꿀 때 다시 검증합니다. Render된 ConfigMap은 실제 DaemonSet에 mount되며 무관한 ConfigMap만 apply해도 구성이 바뀌지는 않습니다. 계정·IRSA 역할·log group은 선행 요건이며 이 chart 예제가 생성하는 리소스가 아닙니다.
### Buffer·Rotation·실패 동작
Host log는 읽기 전용이고 checkpoint·filesystem buffer 상태는 별도 node-local directory에 둡니다. Tail database는 offset 기록이며 그 자체가 내구성 있는 backend archive는 아닙니다. 노드 교체 시 상태가 사라질 수 있습니다. Checkpoint가 없을 때 Read_from_Head On은 남아 있는 파일 내용을 읽으므로 backfill·중복 처리를 설계합니다.
512M output queue 한도와 backlog memory는 예시 할당입니다. storage.total_limit_size에 도달하면 Fluent Bit가 해당 queue의 오래된 chunk를 버릴 수 있습니다. Skip_Long_Lines On도 큰 record를 의도적으로 건너뜁니다. 유한 buffer·retry·node-local 상태는 무손실·exactly-once 전달 보장이 아닙니다. Retry·drop·disk 용량·목적지 실패를 감시하고 실제 log 유입량·장애 기간에 맞게 한도를 검증합니다.
### 선택적 OpenSearch 동시 전송
소유 VPC domain과 collector IAM·FGAC mapping을 준비한 후 overlay를 fluent-bit-opensearch-values.yaml로 저장하고 endpoint hostname을 바꿉니다. 같은 검토된 Helm 작업에 `-f fluent-bit-opensearch-values.yaml`을 추가합니다. CloudWatch를 유지하면 두 목적지로 복제 전송하며 OpenSearch만 선택하는 경우에만 cloudWatchLogs.enabled=false를 설정합니다.
```yaml
opensearch:
enabled: true
host: vpc-eks-logs-EXAMPLE.us-west-2.es.amazonaws.com
port: '443'
tls: 'On'
awsAuth: 'On'
awsRegion: us-west-2
index: eks-logs
generateId: 'On'
suppressTypeName: 'On'
extraOutputs: 'tls.verify On
storage.total_limit_size 512M
'
```
TLS 인증서·hostname 검증과 통제한 고정 index·rollover 설계를 사용합니다. Pod별 index는 index·shard 수를 과도하게 늘릴 수 있습니다. Generate_ID는 retry 중 중복 indexing을 줄이며 독립 출력·실패 복구는 여전히 검증해야 합니다. 한 목적지의 쓰기 성공이 다른 목적지 전달의 증거는 아닙니다.
### 다른 Logging Stack
Fluentd도 소유한 배포에서 필요한 parser·output plugin, host mount, 신원·TLS를 구성하면 선택할 수 있습니다. 일반 JSON parser가 CRI wrapper를 해석하는 것은 아니며 ssl_verify 비활성화는 인증서 오류의 해결책이 아닙니다. 이전 Elastic Helm chart repository는 archive되었으므로 Elastic 배포에는 유지 관리되는 product·operator 설치 경로를 사용합니다.
loki-stack chart는 deprecated이며 Promtail agent는 2026년 3월 2일 EOL에 도달했습니다. Loki는 지원되는 현재 배포와 Alloy 같은 지원 client를 사용하고 storage·접근 제어·retention을 명시합니다. 전용 구성은 [Loki 문서](https://www.atomai.click/kubernetes-docs/llms/ko/observability/logging/01-loki.md), [collector 문서](https://www.atomai.click/kubernetes-docs/llms/ko/observability/logging/05-collectors.md)를 참고하세요. Promtail agent 종료에는 별도 lambda-promtail client가 포함되지 않습니다.
### 구조화된 애플리케이션 Log
다음은 유지한 합성 앱 데이터 예제이며 새 실측이나 이번 감사에서 수집한 기록이 아닙니다. Timestamp는 원래 예시 값을 유지했습니다:
```json
{
"timestamp": "2025-07-11T13:00:00Z",
"level": "INFO",
"message": "Request processed successfully",
"request_id": "12345",
"user_id": "user-789",
"duration_ms": 45,
"status_code": 200
}
```
이 pipeline에서는 CRI record가 전달 timestamp를 제공하고 앱 JSON은 data 아래에 놓입니다. 앱 필드로 timestamp를 덮어쓰기 전에 timezone·clock 동작을 확인합니다. User·session identifier도 민감하거나 연결 가능한 정보일 수 있으므로 최소화하고 credential·원시 session token은 기록하지 않습니다.
다음 query는 이 collector의 record 구조를 전제로 하며 다른 collector는 field path가 다를 수 있습니다:
```text
fields @timestamp, kubernetes.namespace_name, kubernetes.pod_name, data.level, log
| filter kubernetes.namespace_name = "production"
| filter data.level in ["ERROR", "error"]
| sort @timestamp desc
| limit 100
```
공개 chart·image metadata, 실제 Helm ConfigMap·DaemonSet 연결, Kubernetes 기본 schema와 post-renderer 실패 사례를 검증했습니다. Image 설치·collector process 실행·AWS log 전송·운영 파일 시스템 접근은 시험하지 않았습니다. Rollout 전에 실제 노드 권한·IRSA 자격 증명·전체 전달 경로를 검증해야 합니다.
참고: [AWS image 3.4.14](https://github.com/aws/aws-for-fluent-bit/releases/tag/v3.4.14), [CloudWatch output](https://docs.fluentbit.io/manual/data-pipeline/outputs/cloudwatch), [Fluent Bit buffering](https://docs.fluentbit.io/manual/data-pipeline/buffering), [Promtail lifecycle](https://grafana.com/docs/loki/latest/send-data/promtail/).
## 클러스터 모니터링
효과적인 클러스터 모니터링은 EKS 클러스터의 상태, 성능 및 리소스 사용량을 추적하는 데 필수적입니다. 이 섹션에서는 EKS 클러스터를 모니터링하기 위한 다양한 도구와 기술을 살펴봅니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-06-eks-monitoring-logging-2.html)
### CloudWatch Container Insights
CloudWatch Observability add-on은 Container Insights·컨테이너 log 수집·Application Signals 기능을 결합합니다. 소유자가 관리하는 설치 하나를 사용하고 같은 파일을 읽는 다른 Fluent Bit collector와 겹치지 않게 합니다. 데이터 수집을 기대하기 전에 신원·지원 노드 접근·backend 연결을 구성합니다.
#### 클러스터와 호환 Add-on Build 확인
일반 Linux EC2 예제에서는 적절한 EKS Pod Identity agent와 이 클러스터·amazon-cloudwatch/cloudwatch-agent로 trust 범위를 정한 전용 CloudWatch 역할을 준비합니다. 운영자에게는 필요한 add-on·association 제어와 승인 역할 전달 권한이 있어야 합니다. EKS add-on 목록뿐 아니라 기존 Helm release·ServiceAccount annotation·association도 확인합니다. 기존 collector는 소유자의 migration·upgrade 절차가 필요합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the owned cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
CLUSTER_VERSION=$(aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster.version --output text)
aws eks list-addons --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION"
aws eks describe-addon-versions --region "$AWS_REGION" \
--addon-name amazon-cloudwatch-observability --kubernetes-version "$CLUSTER_VERSION"
```
고정된 v5.0.0 예제로 downgrade하지 말고 catalog에서 호환되는 검토한 build를 선택합니다. Add-on configuration schema는 버전별로 다릅니다. 여기서 로컬 기준 render에는 Helm chart 6.6.0을 사용했고 Fluent Bit DaemonSet도 agent 신원 경로의 cloudwatch-agent ServiceAccount를 사용합니다.
#### 명시적인 Auto Monitor 구성
신규 설치 예제의 cloudwatch-config.json으로 저장합니다. 기본 컨테이너 log 구성은 유지하고 앱 rollout을 검토할 때까지 광범위한 자동 workload 선택·자동 restart를 비활성화합니다:
```json
{
"manager": {
"applicationSignals": {
"autoMonitor": {
"monitorAllServices": false,
"restartPods": false
}
}
}
}
```
이 설정은 Auto Monitor 선택·restart를 제어하며 모든 telemetry source를 끄는 설정은 아닙니다. 기존 annotation·custom selector·수동 계측 앱은 별도로 검토합니다. 로컬 Python 3·jsonschema를 준비한 뒤 선택한 build의 schema를 받아 구성을 검사합니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the cluster Region}"
: "${CLOUDWATCH_ADDON_VERSION:?Choose a reviewed compatible add-on build}"
aws eks describe-addon-configuration --region "$AWS_REGION" \
--addon-name amazon-cloudwatch-observability --addon-version "$CLOUDWATCH_ADDON_VERSION" \
--output json > cloudwatch-addon-review.json
python3 - "$CLOUDWATCH_ADDON_VERSION" <<'PY'
import json
import sys
import jsonschema
with open("cloudwatch-addon-review.json") as stream:
review = json.load(stream)
if review["addonName"] != "amazon-cloudwatch-observability" or review["addonVersion"] != sys.argv[1]:
raise SystemExit("Returned schema does not match the selected add-on build")
schema = json.loads(review["configurationSchema"])
with open("cloudwatch-config.json") as stream:
config = json.load(stream)
validator = jsonschema.validators.validator_for(schema)
validator.check_schema(schema)
validator(schema).validate(config)
PY
```
#### 신규 소유 설치 요청
CloudWatch 역할에는 선택한 기능에 필요한 CloudWatch·trace 권한이 이미 있어야 합니다. 다음은 검토한 schema·build를 재확인하고 기존 add-on·collector association이 있으면 중단하며 충돌 리소스를 인수하지 않습니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the reviewed cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
: "${CLOUDWATCH_ADDON_VERSION:?Set the reviewed, schema-checked compatible build}"
: "${CLOUDWATCH_ROLE_ARN:?Set the prepared CloudWatch Pod Identity role ARN}"
python3 - "$CLOUDWATCH_ADDON_VERSION" <<'PY'
import json
import sys
import jsonschema
with open("cloudwatch-addon-review.json") as stream:
review = json.load(stream)
if review["addonName"] != "amazon-cloudwatch-observability" or review["addonVersion"] != sys.argv[1]:
raise SystemExit("Selected add-on build changed; review its schema again")
schema = json.loads(review["configurationSchema"])
with open("cloudwatch-config.json") as stream:
config = json.load(stream)
jsonschema.validators.validator_for(schema)(schema).validate(config)
PY
EXISTING_ADDONS=$(aws eks list-addons --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" --output json)
python3 - "$EXISTING_ADDONS" <<'PY'
import json, sys
if "amazon-cloudwatch-observability" in json.loads(sys.argv[1])["addons"]:
raise SystemExit("Add-on already exists; use its owner's reviewed upgrade procedure")
PY
EXISTING_ASSOCIATIONS=$(aws eks list-pod-identity-associations \
--cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--namespace amazon-cloudwatch --service-account cloudwatch-agent --output json)
python3 - "$EXISTING_ASSOCIATIONS" <<'PY'
import json, sys
if json.loads(sys.argv[1])["associations"]:
raise SystemExit("Collector association already exists; inspect its owner before installation")
PY
ASSOCIATIONS=$(python3 - "$CLOUDWATCH_ROLE_ARN" <<'PY'
import json, sys
print(json.dumps([{"serviceAccount": "cloudwatch-agent", "roleArn": sys.argv[1]}]))
PY
)
aws eks create-addon --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--addon-name amazon-cloudwatch-observability --addon-version "$CLOUDWATCH_ADDON_VERSION" \
--pod-identity-associations "$ASSOCIATIONS" \
--configuration-values file://cloudwatch-config.json --resolve-conflicts NONE
aws eks describe-addon --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--addon-name amazon-cloudwatch-observability \
--query 'addon.{Status:status,Version:addonVersion,Health:health.issues}'
```
CreateAddon은 비동기입니다. ACTIVE가 될 때까지 status·health를 확인한 뒤 agent·collector Pod, 실제 ContainerInsights datapoint와 대상 log group을 검증합니다. 요청 성공·ACTIVE·dashboard만으로 전체 수집이 증명되지는 않습니다. 마지막 DescribeAddon은 waiter가 아닌 조회입니다. 재시도 전에 실패·권한을 확인하며 검토하지 않은 overwrite나 기존 구성 삭제로 설치를 강제하지 않습니다.
#### 버전 5.0.0의 변경점
2026년 2월 26일의 기본 APM 변경은 실제 기능입니다. 5.0.0+의 신규 설치·upgrade는 Application Signals Auto Monitor를 기본 활성화합니다. monitorAllServices 기본값은 true, restartPods는 false입니다. 지원되는 Service 연결 Deployment·DaemonSet·StatefulSet이 대상이며 kube-system·amazon-cloudwatch는 기본 제외됩니다. 대상의 신규·재시작 workload는 개별 annotation 없이 계측될 수 있지만 실행 중인 모든 Pod가 즉시 재계측되는 것은 아닙니다.
지원 언어·workload를 선택하고 기존 OpenTelemetry·APM 통합, sampling·비용과 restart rollout을 검토합니다. 명시적 제외가 우선합니다. Container Insights는 Linux·Windows 구성을 지원하지만 EKS Windows 노드의 Application Signals는 지원하지 않습니다. Fargate·Hybrid 수집·신원은 해당 플랫폼의 문서화된 경로가 필요하며 일반 DaemonSet 예제로 지원을 단정하지 않습니다.
#### Metric·Dashboard·경보
Container Insights console view에서 실제 클러스터를 선택하고 공개 metric 이름·차원·최근 데이터를 확인합니다. Node CPU·메모리·파일 시스템 metric은 노드 사용량을 나타냅니다. Pod CPU·메모리 사용률의 분모는 Pod request가 아닌 노드 한도입니다. 문서화된 metric에는 namespace·service·cluster 집계가 있지만 집계 백분율이 이기종 클러스터의 용량 가중 사용률이 되는 것은 아닙니다.
Dashboard는 데이터 조회를 돕지만 모든 경보를 만들거나 알림 전달을 보장하지 않습니다. 뒤의 CloudWatch 경보 예제는 명시적으로 선택한 차원과 node metric·Maximum을 사용합니다. Metric 존재를 검증하고 workload에 맞는 경보·전달 경로를 구성하세요.
기존 공개 chart의 기본 log 구성·Auto Monitor 인수를 render하고 schema·소유권·실패 흐름의 mocked 사례 9개를 검증했습니다. 합성 test schema는 helper 동작 검사이며 위의 실제 build별 schema를 대체하지 않습니다. 이번 감사에서 add-on·workload 계측·telemetry export·AWS resource를 실행하지 않았습니다.
참고: [CloudWatch add-on 설치·Auto Monitor](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/install-CloudWatch-Observability-EKS-addon.html), [기본 APM 발표](https://aws.amazon.com/about-aws/whats-new/2026/02/application-performance-monitoring-cloudwatch-eks/), [Container Insights metric](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Container-Insights-metrics-EKS.html).
### EKS Node Monitoring Agent
EKS Node Monitoring Agent는 노드의 시스템·스토리지·네트워크·가속기 상태를 condition으로 게시합니다. 2026년 2월 24일 open source로 공개되었고 EKS Auto Mode에 포함됩니다. 별도로 관리하는 add-on 설치에서는 release 선택 전에 실제 클러스터 버전·기존 add-on·호환 agent 버전을 확인합니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the owned cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
KUBERNETES_VERSION=$(aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster.version --output text)
aws eks list-addons --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION"
aws eks describe-addon-versions --addon-name eks-node-monitoring-agent \
--kubernetes-version "$KUBERNETES_VERSION" --region "$AWS_REGION"
```
기존 add-on 소유자가 검토한 버전·지원 노드 구성으로 설치하거나 update합니다. Add-on create 명령은 기존 설치의 upgrade 절차가 아닙니다.
Condition 이름의 존재만으로 노드가 비정상이라고 판단하지 않습니다. Status·reason과 각 condition의 의미를 읽어야 합니다. Ready=False와 MemoryPressure=False는 의미가 다릅니다.
```bash
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{range .status.conditions[*]}{" "}{.type}{"="}{.status}{" reason="}{.reason}{"\n"}{end}{end}'
```
모니터링과 repair 활성화는 별개입니다. Auto Mode는 automatic node repair가 활성화되어 있고 managed node group은 nodeRepairConfig, Karpenter는 NodeRepair feature gate를 사용합니다. Agent 설치만으로 복구 동작을 단정하지 말고 실제 managed node group 구성을 확인합니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the owned cluster name}"
: "${NODEGROUP_NAME:?Set an actual managed node group name}"
: "${AWS_REGION:?Set the cluster Region}"
aws eks describe-nodegroup --region "$AWS_REGION" \
--cluster-name "$CLUSTER_NAME" --nodegroup-name "$NODEGROUP_NAME" \
--query 'nodegroup.{Name:nodegroupName,Repair:nodeRepairConfig,Health:health.issues}'
```
Repair 대상 여부는 condition·reason·대기 시간과 적용되는 보호 조건에 따라 달라집니다. 문서화된 기본값에서 MemoryPressure·DiskPressure에는 automatic repair action이 없습니다. 모든 condition이나 agent 설치를 노드 교체의 증거로 간주하지 않습니다. Open source agent를 수정할 때도 게시하는 condition 의미를 선택한 repair 구성과 함께 시험해야 합니다.
참고: [open source 발표](https://aws.amazon.com/about-aws/whats-new/2026/02/amazon-eks-node-monitoring-agent-open-source/), [automatic node repair](https://docs.aws.amazon.com/eks/latest/userguide/node-repair.html).
### Prometheus 및 Grafana
Prometheus는 시계열 데이터베이스 및 모니터링 시스템이며, Grafana는 지표를 시각화하기 위한 대시보드 도구입니다. 이 두 도구를 함께 사용하여 EKS 클러스터를 포괄적으로 모니터링할 수 있습니다.
#### Amazon Managed Service for Prometheus 및 Grafana
AMP는 수집한 Prometheus metric을 저장·조회하고 AMG는 구성한 data source를 조회해 dashboard로 표시합니다. Workspace 생성만으로 scraper 배포·AWS 신원 권한·data source 연결이 완료되지는 않습니다. 이 예제는 기존 소유 workspace를 조회하고 아래 kube-prometheus-stack release를 확장하므로 Prometheus를 중복 설치하지 않습니다.
**AMP 수집 신원과 values**
ServiceAccount monitoring/amp-writer용 IRSA 역할을 준비합니다. Trust policy는 이 클러스터의 OIDC provider와 정확한 sub=system:serviceaccount:monitoring:amp-writer·aud=sts.amazonaws.com 조건을 사용해야 합니다. 대상 workspace ARN에 aps:RemoteWrite를 부여합니다. Prometheus process는 projected token을 받고 STS·AMP endpoint에 연결할 수 있어야 하며 Kubernetes RBAC는 별도 권한 경로입니다. 이 예제에 무관한 Pod Identity association이나 정적 AWS key를 함께 연결하지 않습니다.
다음은 workspace를 읽고 검토한 chart 90.1.1의 overlay를 작성합니다. /api/v1/이 있거나 없는 endpoint 형식을 처리해 remote_write 경로를 한 번만 붙입니다. Python 3이 필요하며 workspace를 생성하지 않습니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the AMP workspace Region}"
: "${AMP_WORKSPACE_ID:?Set the owned AMP workspace ID}"
: "${AMP_WRITE_ROLE_ARN:?Set the prepared IRSA writer role ARN}"
: "${CLUSTER_NAME:?Set the source cluster name}"
aws amp describe-workspace --region "$AWS_REGION" \
--workspace-id "$AMP_WORKSPACE_ID" --output json > amp-workspace.json
python3 - "$AWS_REGION" "$AMP_WORKSPACE_ID" "$AMP_WRITE_ROLE_ARN" "$CLUSTER_NAME" <<'PY'
import json
import sys
from urllib.parse import urlsplit, urlunsplit
region, workspace_id, role_arn, cluster = sys.argv[1:]
with open("amp-workspace.json") as stream:
workspace = json.load(stream)["workspace"]
if workspace["workspaceId"] != workspace_id or workspace["status"]["statusCode"] != "ACTIVE":
raise SystemExit("Review the workspace identity and ACTIVE status")
arn = workspace["arn"].split(":", 5)
if len(arn) != 6 or arn[2] != "aps" or arn[3] != region or arn[5] != "workspace/" + workspace_id:
raise SystemExit("Workspace ARN does not match the selected Region/ID")
endpoint = urlsplit(workspace["prometheusEndpoint"])
path = endpoint.path.rstrip("/")
if path.endswith("/api/v1"):
path = path[:-7]
if (endpoint.scheme != "https" or not endpoint.hostname or endpoint.username
or endpoint.password or endpoint.query or endpoint.fragment
or path != "/workspaces/" + workspace_id):
raise SystemExit("Inspect the workspace endpoint before configuring remote write")
remote_write = urlunsplit((endpoint.scheme, endpoint.netloc, path + "/api/v1/remote_write", "", ""))
values = {
"prometheus": {
"serviceAccount": {
"create": True, "name": "amp-writer", "createTokenSecret": True,
"annotations": {"eks.amazonaws.com/role-arn": role_arn},
},
"prometheusSpec": {
"externalLabels": {"cluster": cluster},
"remoteWrite": [{"url": remote_write, "sigv4": {"region": region}}],
},
},
}
with open("amp-values.json", "w") as stream:
json.dump(values, stream, indent=2)
stream.write("\n")
print("Wrote amp-values.json; review it with all existing release values")
PY
```
소유자의 검토한 설치·upgrade values에서 amp-values.json과 monitoring-values.yaml을 함께 사용합니다. Overlay는 remoteWrite 목록을 교체하므로 기존 목적지·external label을 의도에 맞게 보존합니다. 신규 release는 아래 설치 명령에 -f amp-values.json을 추가합니다. 기존 release는 현재 설정을 모두 유지하고 render diff·rollout을 검토한 뒤 소유자의 upgrade 절차를 따릅니다. ServiceAccount 변경은 Prometheus Pod 신원을 바꾸며 Chart 90.1.1의 기본 API-server·kubelet ServiceMonitor 인증은 명시적인 ServiceAccount token Secret을 사용합니다. 이 Secret을 보호하고 rotation·폐기 절차를 따릅니다. IRSA의 짧은 수명·audience 제한 token과는 다릅니다. 의존하는 모든 monitor 자격 증명을 교체하지 않고 createTokenSecret을 끄면 render·인증이 실패합니다.
여러 replica에서는 중복 scrape가 무료라고 가정하지 말고 AMP의 문서화된 HA deduplication label·replica 구성을 설정합니다. 실제 workload에 맞게 cardinality·retention·수집 비용을 계획합니다. Remote-write 실패·backlog와 대상 workspace의 최근 데이터를 확인하며 Helm rollout 성공만으로 수집을 증명하지 않습니다.
**AMG 인증과 data source 연결**
```bash
set -euo pipefail
: "${AMG_REGION:?Set the Grafana workspace Region}"
: "${AMG_WORKSPACE_ID:?Set the owned Grafana workspace ID}"
aws grafana describe-workspace --region "$AMG_REGION" \
--workspace-id "$AMG_WORKSPACE_ID" \
--query 'workspace.{ID:id,Status:status,Version:grafanaVersion,Endpoint:endpoint,Role:workspaceRoleArn,Authentication:authentication,PermissionType:permissionType}'
```
Workspace 사용자 인증(IAM Identity Center·SAML), Grafana 사용자 권한, AWS data source용 workspace IAM 역할은 서로 다른 제어입니다. Grafana service account는 Grafana HTTP API 신원이며 ADMIN service account를 만든다고 AMP data source가 생성되거나 aps:QueryMetrics가 부여되지는 않습니다. Metric 조회를 위해 불필요하게 광범위한 API 신원을 만들지 않습니다.
AMG 12+에서는 Amazon Managed Service for Prometheus data-source plugin을 선택합니다. 해당 AMG 버전의 Core Prometheus plugin에서 SigV4 지원이 제거되었고 기존 AMP data source는 AMP plugin으로 migration됩니다. 실제 workspace 버전에 맞는 문서를 사용합니다. 승인된 workspace 구성 경로에서 대상 account·Region·workspace와 조회 신원을 설정하고 알려진 series를 시험합니다. 문서의 AWS data-source configuration 절차는 service-managed 권한을 사용합니다. Customer-managed workspace는 자체 IAM 구성을 검토해야 하며 자동으로 소유권 방식을 바꾸지 않습니다.
조회 역할에는 일반적으로 workspace 범위의 aps:QueryMetrics·aps:GetSeries·aps:GetLabels·aps:GetMetricMetadata가 필요하며 discovery·다른 활성 기능에는 추가 action이 필요할 수 있습니다. 조회 endpoint와 remote_write 수집 URL은 다릅니다. 별도로 workspace를 생성할 때 CLI는 --workspace-name을 사용하고 --account-access-type·인증·권한 구성이 필요합니다. Service-managed IAM 자동화는 문서화된 console 절차와 연결됩니다. Workspace를 사용 가능하다고 판단하기 전에 신원·사용자 할당·네트워크 접근을 완료합니다.
참고: [AMP remote write](https://docs.aws.amazon.com/prometheus/latest/userguide/AMP-onboard-ingest-metrics-existing-Prometheus.html), [AMG AMP plugin](https://docs.aws.amazon.com/grafana/latest/userguide/amazon-prometheus-data-source.html), [AWS data-source configuration](https://docs.aws.amazon.com/grafana/latest/userguide/amazon-AMP-adding-AWS-config.html). 이번 감사에서 workspace·인증 흐름·telemetry 수집을 실행하지 않았습니다.
#### 자체 관리형 Prometheus 및 Grafana
이 예제는 ServiceMonitor·PrometheusRule 예제에 필요한 Prometheus Operator를 포함하는 kube-prometheus-stack release 하나를 소유자가 관리합니다. 독립 Prometheus chart만 설치하면 해당 CRD·controller가 자동 제공되는 것은 아닙니다. 설치 전에 기존 operator·release·CRD 소유권을 확인하며 기존 stack은 소유자의 upgrade 절차를 따릅니다.
검토한 기준은 chart 90.1.1·Operator 0.93.1이며 EKS 1.36용으로 render했습니다. 일반 Linux EC2 노드와 동작하는 EBS CSI 권한, 준비된 암호화 ebs-gp3 StorageClass를 전제로 합니다. Auto Mode·다른 storage 구현은 실제 지원 class와 노드 배치를 선택합니다. 아래 retention·PVC 크기는 예시 할당이며 실측 용량 보장이 아닙니다.
승인된 secret 관리 경로로 monitoring namespace와 admin-user·admin-password key가 있는 grafana-admin Secret을 준비합니다. Values는 공통 암호를 Helm values·명령 인수에 넣는 대신 Secret을 참조합니다. 다음을 monitoring-values.yaml로 저장합니다:
```yaml
grafana:
admin:
existingSecret: grafana-admin
userKey: admin-user
passwordKey: admin-password
service:
type: ClusterIP
rbac:
namespaced: true
sidecar:
dashboards:
searchNamespace: monitoring
datasources:
searchNamespace: monitoring
persistence:
enabled: true
storageClassName: ebs-gp3
size: 10Gi
accessModes:
- ReadWriteOnce
deploymentStrategy:
type: Recreate
prometheus:
prometheusSpec:
retention: 14d
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: ebs-gp3
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
kubeEtcd:
enabled: false
kubeControllerManager:
enabled: false
kubeScheduler:
enabled: false
kubeProxy:
enabled: false
kubelet:
serviceMonitor:
tlsConfig:
insecureSkipVerify: false
ca:
configMap:
name: kubelet-serving-ca
key: ca.crt
```
Grafana dashboard·data-source sidecar는 namespaced role로 monitoring만 감시합니다. ClusterIP 서비스는 로컬 port-forward로 접근합니다. Replica 하나와 PVC를 사용하는 예제는 rollout 중 동시 writer를 피하도록 Recreate를 사용하므로 교체 중 UI 중단을 계획해야 하며 고가용성 Grafana 설계는 아닙니다.
Chart 기본값은 kubelet server 인증서 검증을 생략합니다. 이 values는 대신 신뢰하는 kubelet serving CA chain을 ca.crt에 담은 monitoring/kubelet-serving-ca ConfigMap과 scrape endpoint의 SAN이 일치하는 인증서를 전제로 합니다. 노드 소유자의 인증서 관리 절차로 신뢰를 검증하며 EKS API-server CA가 자동으로 kubelet serving CA가 되는 것은 아닙니다. 이 전제가 충족되지 않으면 수집 활성화 전에 인증서 구성을 해결합니다. 실패한 target을 정상으로 보이게 하려고 insecureSkipVerify를 조용히 되돌리지 않습니다. 이번 감사에서 kubelet TLS handshake는 시험하지 않았습니다.
이 구성에서 직접 endpoint를 제공하지 않는 etcd·controller-manager·scheduler·kube-proxy scrape job은 비활성화했습니다. 실제 도달 가능하고 권한이 있는 endpoint를 구성한 뒤 해당 job을 활성화합니다. API server·CloudWatch metric은 별도 자료이며 없는 구성 요소 ServiceMonitor target을 동작시키지 않습니다.
신규 소유 release 설치:
```bash
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update prometheus-community
helm install monitoring prometheus-community/kube-prometheus-stack \
--version 90.1.1 --namespace monitoring --create-namespace \
-f monitoring-values.yaml --wait --timeout 10m
```
Helm wait가 모든 operator 생성 resource·target·알림 경로의 동작을 입증하지는 않습니다. 생성된 resource, PVC binding, Prometheus target 상태와 실제 metric 데이터를 확인합니다:
```bash
kubectl get pods,svc,pvc -n monitoring
kubectl get prometheus,alertmanager -n monitoring
```
다음 명령 실행 중 `http://127.0.0.1:3000`에서 Grafana에 접근하고 승인된 secret store의 자격 증명을 사용합니다:
```bash
kubectl port-forward --address 127.0.0.1 -n monitoring \
svc/monitoring-grafana 3000:80
```
Grafana database·PVC와 export한 dashboard 정의를 보호합니다. Grafana가 지원하는 자격 증명 rotation 절차를 사용하며 bootstrap Secret 변경만으로 기존 database 사용자 암호가 바뀌었다고 판단하지 않습니다. 공통 sample 암호로 UI를 공개하지 마세요.
Chart render에서 실제 Prometheus service monitoring-kube-prometheus-prometheus, Grafana service monitoring-grafana와 data-source UID prometheus를 확인했습니다. Custom ServiceMonitor·PrometheusRule은 이 release의 release=monitoring selector와 일치해야 합니다. Render·schema 검사는 로컬 증거이며 실제 클러스터의 stack·PVC·로그인을 시험하지 않았습니다.
참고: [kube-prometheus-stack chart and upgrade guidance](https://github.com/prometheus-community/helm-charts/tree/kube-prometheus-stack-90.1.1/charts/kube-prometheus-stack).
#### 주요 Prometheus 지표
Metric 존재 여부는 dashboard 이름이 아니라 실제 scrape target·권한에 따라 달라집니다:
- Node exporter는 지원 노드의 CPU·메모리·파일 시스템·네트워크 series를 제공합니다.
- Kubelet·cAdvisor는 컨테이너 resource metric을, kube-state-metrics는 재시작·readiness 같은 Kubernetes 객체 상태를 제공합니다.
- API server는 권한 있는 API metric을 제공합니다. 직접 etcd·controller-manager·scheduler metric에는 각각 도달 가능한 endpoint가 필요하며 위 구성은 해당 target을 활성화하지 않습니다.
Series가 없으면 exporter 부재·scrape 실패·미지원 플랫폼·metric 변경 등을 확인합니다. 사용량 0이나 정상 상태를 의미하지는 않습니다.
#### 유용한 Grafana 대시보드
검토한 kube-prometheus-stack release에 포함된 Kubernetes·node-exporter·API-server dashboard부터 사용하고 prometheus data-source UID를 선택합니다. Community dashboard ID만으로 metric·label 호환성이 보장되지는 않습니다. 가져온 dashboard의 query·단위·필요한 recording rule·data-source 참조를 확인합니다. 데이터가 없는 graph는 해당 구성 요소가 정상이라는 증거가 아닙니다.
기존 community ID는 모두 존재하지만 일부 제목·용도 설명이 부정확했습니다. Catalog에서 확인한 참조는 다음과 같으며 import·runtime 호환성은 시험하지 않았습니다:
| ID | 실제 catalog 제목 | 범위 참고 |
| --- | --- | --- |
| [15661](https://grafana.com/grafana/dashboards/15661-k8s-dashboard-en-20250125/) | K8S Dashboard | 전반적인 K8S resource 개요 |
| [1860](https://grafana.com/grafana/dashboards/1860-node-exporter-full/) | Node Exporter Full | 일치하는 node-exporter series 필요 |
| [6417](https://grafana.com/grafana/dashboards/6417-kubernetes-cluster-prometheus/) | Kubernetes Cluster (Prometheus) | 클러스터·컨테이너 개요; catalog 마지막 갱신 2018년 |
| [12006](https://grafana.com/grafana/dashboards/12006-kubernetes-apiserver/) | Kubernetes apiserver | API-server 지연·cache dashboard; catalog 마지막 갱신 2020년 |
| [13770](https://grafana.com/grafana/dashboards/13770-1-kubernetes-all-in-one-cluster-monitoring-kr/) | 1 Kubernetes All-in-one Cluster Monitoring KR | 해당 도서의 VM 환경에 최적화한 한국어 all-in-one dashboard |
#### PromQL 쿼리 예시
다음은 검토한 stack의 job·metrics_path label에 맞춘 예제입니다. 특히 공유 AMP workspace에서는 Pod의 namespace와 존재하는 cluster label을 보존합니다. Prometheus external label은 외부 전송 시 붙으며 로컬 저장 series에 자동으로 모두 추가되는 것은 아닙니다. Max 집계는 같은 식별 객체의 중복 관측을 합치는 용도이며 서로 다른 workload를 합치는 용도가 아닙니다. 적용 전에 label을 확인합니다.
CPU별 평균을 사용한 노드 CPU non-idle 비율:
```promql
100 * (1 - avg by (cluster, instance) (max by (cluster, instance, cpu) (rate(node_cpu_seconds_total{job="node-exporter",mode="idle"}[5m]))))
```
컨테이너 메모리 working-set byte 기준 상위 Pod 10개:
```promql
topk(10, sum by (cluster, namespace, pod) (max by (cluster, namespace, pod, container) (container_memory_working_set_bytes{job="kubelet",metrics_path="/metrics/cadvisor",container!="",container!="POD"})))
```
Pod UID별 현재 재시작 counter이며 CrashLoopBackOff 판별식은 아닙니다:
```promql
sum by (cluster, namespace, pod, uid) (max by (cluster, namespace, pod, uid, container) (kube_pod_container_status_restarts_total{job="kube-state-metrics"}))
```
크기가 0인 파일 시스템을 제외한 root 파일 시스템의 사용 불가 용량 비율:
```promql
(100 * (1 - max by (cluster, instance, device, mountpoint, fstype) (node_filesystem_avail_bytes{job="node-exporter",mountpoint="/"}) / max by (cluster, instance, device, mountpoint, fstype) (node_filesystem_size_bytes{job="node-exporter",mountpoint="/"}))) and on (cluster, instance, device, mountpoint, fstype) (max by (cluster, instance, device, mountpoint, fstype) (node_filesystem_size_bytes{job="node-exporter",mountpoint="/"}) > 0)
```
여기서 CPU 비율은 non-idle 시간, 메모리는 byte, 재시작은 현재 Pod·컨테이너 수명의 counter입니다. Counter 변화량이 필요하면 선택한 구간의 rate·increase를 사용합니다. 파일 시스템 식은 available 용량 기준이므로 일반 사용자에게 예약된 공간이 포함될 수 있습니다. 조사용 예제이며 보편적인 경보 threshold가 아닙니다.
### AWS X-Ray를 사용한 분산 추적
X-Ray는 계속 지원되는 trace backend입니다. SDK·daemon은 2026년 2월 25일부터 보안 수정만 제공하는 maintenance 단계이며 현재 AWS 일정은 해당 단계 종료일을 제시하지 않습니다. 신규 계측에는 지원되는 OpenTelemetry·ADOT 통합을 사용합니다. X-Ray daemon·collector의 trace 제출에 Kubernetes cluster-admin은 필요하지 않고 그 Kubernetes 역할이 AWS 쓰기 권한을 주지도 않습니다.
#### Collector 소유권과 사전 조건
계측·수집 소유 경로를 하나 선택합니다. 앞의 CloudWatch add-on Application Signals 경로도 대안이며 이미 계측된 process에 다른 SDK agent를 추가하기 전에 중복 span·충돌을 검토합니다. 아래 명시적 예제는 OpenTelemetry Operator 0.158.0·ADOT Collector 0.50.0과 trace 전용 pipeline을 사용합니다.
Collector 적용 전에 각 소유 경로에서 다음 의존성을 준비합니다:
- 일치하는 CRD·webhook이 있는 동작 중인 Operator. Upstream 공개 manifest는 cert-manager를 사용하므로 문서화된 설치·upgrade 경로를 따릅니다. EKS ADOT add-on도 별도 소유 경로이며 build별 schema를 확인해야 합니다. Collector release에는 Operator 설치 manifest가 없습니다.
- 전용 tracing-demo namespace와 이 클러스터의 IRSA trust, sub=system:serviceaccount:tracing-demo:adot-traces·aud=sts.amazonaws.com을 설정한 ServiceAccount adot-traces. 필요한 X-Ray 쓰기 action(이 pipeline은 PutTraceSegments)과 STS·X-Ray 연결을 준비합니다. 이 OTLP 전용 collector는 Kubernetes 객체를 discovery하지 않으므로 cluster-wide RBAC가 필요하지 않습니다.
- tls.crt·tls.key가 있는 Secret tracing-demo/otel-receiver-tls와 adot-traces-collector.tracing-demo.svc에 유효한 server 인증서. 신뢰하는 CA를 앱에 mount합니다. 인증서 발급·갱신과 collector reload·restart는 운영 책임입니다.
- NetworkPolicy를 강제하는 CNI와 검토한 앱 egress. 예제는 default namespace의 app=my-app Pod에서 들어오는 트래픽을 허용하며 전용 tracing-demo namespace의 모든 Pod에 적용됩니다. Label은 트래픽 선택 조건이지 workload 인증이나 Pod 생성 RBAC의 대체 수단은 아닙니다.
[Operator release](https://github.com/open-telemetry/opentelemetry-operator/releases/tag/v0.158.0)와 [ADOT release](https://github.com/aws-observability/aws-otel-collector/releases/tag/v0.50.0)를 검토하며 Collector URL을 Operator manifest로 적용하지 않습니다. V1beta1 CRD의 spec.config는 object입니다. 예시 Region을 바꿀 때 env·exporter 필드를 함께 변경합니다:
```yaml
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: adot-traces
namespace: tracing-demo
spec:
mode: deployment
replicas: 1
image: public.ecr.aws/aws-observability/aws-otel-collector:v0.50.0
serviceAccount: adot-traces
env:
- name: AWS_REGION
value: us-west-2
- name: AWS_EC2_METADATA_DISABLED
value: "true"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
volumes:
- name: receiver-tls
secret:
secretName: otel-receiver-tls
volumeMounts:
- name: receiver-tls
mountPath: /etc/otel/tls
readOnly: true
config:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
tls:
cert_file: /etc/otel/tls/tls.crt
key_file: /etc/otel/tls/tls.key
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 75
spike_limit_percentage: 15
batch: {}
exporters:
awsxray:
region: us-west-2
local_mode: true
no_verify_ssl: false
index_all_attributes: false
telemetry:
enabled: false
extensions:
health_check:
endpoint: 0.0.0.0:13133
service:
extensions: [health_check]
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [awsxray]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: tracing-ingress
namespace: tracing-demo
spec:
podSelector: {}
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: default
podSelector:
matchLabels:
app: my-app
ports:
- protocol: TCP
port: 4318
```
Operator는 OTLP/HTTP port에서 ClusterIP receiver Service를 구성합니다. 이름은 tracing-demo의 adot-traces-collector이고 TCP 4318입니다. Receiver가 사용자 신원·업무 권한을 검증하는 것은 아니며 이 예제는 server TLS와 선택한 네트워크 범위를 사용합니다. 더 강한 격리에는 mTLS·지원 receiver authenticator가 필요할 수 있습니다. 접근 범위를 넓히기 전에 양 끝을 일관되게 구성합니다.
Collector가 수신했다고 backend 저장이 확인되는 것은 아닙니다. 생성된 Deployment·Service, TLS handshake, AWS 자격 증명 선택, exporter 실패와 X-Ray의 알려진 trace를 확인합니다. Replica 하나·메모리 기반 예제는 무손실·고가용성 pipeline이 아닙니다. Workload에 맞게 buffer·backpressure·retry·실패 처리를 설계하고 시험해야 합니다. CRD schema 검사는 모든 component 구성이나 collector 시작을 검증하지 않습니다.
#### 앱 계측과 Context 전파
Python 3.10+ 앱의 의존성 lock에서 opentelemetry-sdk==1.44.0·opentelemetry-exporter-otlp-proto-http==1.44.0을 일치시킵니다. 서비스별로 설정하며 공유 collector가 모든 입력 service.name을 하나의 전역 값으로 덮어쓰면 안 됩니다. HTTP exporter endpoint에는 /v1/traces가 포함됩니다. 4317의 gRPC endpoint는 다른 protocol·구성입니다.
```bash
export OTEL_SERVICE_NAME=my-app
export CLUSTER_NAME=my-owned-cluster
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://adot-traces-collector.tracing-demo.svc:4318/v1/traces
export OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE=/etc/otel/ca.crt
```
CA file을 앱 컨테이너에 mount해야 합니다. 앱 시작 시 RequestTracing 하나를 만들고 실제 server handler에서 소문자로 정규화한 입력 header 이름과 제공된 출력 header를 사용할 operation을 handle_request에 전달하며 정상 종료 시 close를 호출합니다. Framework·client 자동 계측으로 이 경계를 처리할 수도 있으므로 같은 작업을 중복 계측하지 않습니다:
```python
import os
from urllib.parse import urlsplit
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased
from opentelemetry.trace import SpanKind
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
class RequestTracing:
def __init__(self):
endpoint = os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"]
parsed = urlsplit(endpoint)
if parsed.scheme != "https" or parsed.path != "/v1/traces":
raise ValueError("Set the HTTPS OTLP/HTTP traces endpoint including /v1/traces")
self.provider = TracerProvider(
resource=Resource.create({
"service.name": os.environ["OTEL_SERVICE_NAME"],
"k8s.cluster.name": os.environ["CLUSTER_NAME"],
}),
sampler=ParentBased(TraceIdRatioBased(0.1)),
)
exporter = OTLPSpanExporter(
endpoint=endpoint,
certificate_file=os.environ["OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE"],
timeout=10,
)
self.provider.add_span_processor(BatchSpanProcessor(exporter))
self.tracer = self.provider.get_tracer("example.request-handler")
self.propagator = TraceContextTextMapPropagator()
def handle_request(self, incoming_headers, operation):
parent = self.propagator.extract(incoming_headers)
with self.tracer.start_as_current_span("request", context=parent, kind=SpanKind.SERVER):
outgoing_headers = {}
self.propagator.inject(outgoing_headers)
return operation(outgoing_headers)
def close(self):
self.provider.shutdown()
```
Adapter는 W3C tracecontext 전파와 server span 하나를 보여주며 HTTP server·모든 client span을 구현하지는 않습니다. X-Amzn-Trace-Id를 사용하는 AWS edge 통합에는 지원 propagator·bridge가 필요하고 W3C 전용 extraction이 그 header를 읽는다고 가정하지 않습니다. 10% head sampling은 신규 root의 예시이며 parent 결정을 따릅니다. 나중에 발생하는 모든 오류·느린 요청의 보존을 보장할 수 없습니다. Tail sampling에는 같은 trace의 모든 span을 함께 전달하고 충분히 buffer하는 별도 설계가 필요합니다.
#### Trace Map과 조사
해당 account에서 사용할 수 있는 X-Ray·CloudWatch 추적 view에서 trace map·지연 분포·error·fault 세부 내용을 확인합니다. Map은 계측·sampling·전달에 성공한 span을 반영하므로 edge가 없다고 서비스 간 통신이 없었다고 단정하지 않습니다. 자격 증명·개인정보·무제한 request attribute를 제외하고 보존한 log·metric과 trace ID를 연계합니다.
이번 감사의 검증 범위는 공개 API·schema와 합성 로컬 동작입니다. Collector 배포·실제 계측·trace export·운영 용량 시험을 주장하지 않습니다. 참고: [X-Ray maintenance 일정](https://aws.amazon.com/blogs/mt/aws-x-ray-sdks-daemon-migration-to-opentelemetry/), [EKS ADOT 소유 경로](https://docs.aws.amazon.com/eks/latest/userguide/opentelemetry.html), [AWS X-Ray exporter](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/v0.158.0/exporter/awsxrayexporter).
### Kubernetes 대시보드
[Kubernetes Dashboard 프로젝트](https://github.com/kubernetes-retired/dashboard)는 archive되어 더 이상 유지 관리되지 않습니다. Maintainer는 현재 UI 용도로 Kubernetes SIG UI의 Headlamp를 안내합니다. 오래된 Dashboard v2.7 raw manifest·cluster-admin token 예제를 현재 설치 경로로 사용하지 않습니다.
유지 관리되는 UI를 선택하고 실제 인증·TLS·권한·upgrade 요건을 검토하세요. 보안 장의 access entry·RBAC 예제처럼 의도한 IAM·RBAC 신원과 namespace 범위를 사용합니다. UI의 기본 사용자로 무제한 cluster-admin ServiceAccount를 사용할 필요는 없으며 관리자 bearer token 노출이 접근 설계를 대체하지는 않습니다.
### 사용자 정의 지표 및 모니터링
애플리케이션별 지표를 수집하고 모니터링하기 위한 사용자 정의 솔루션을 구현할 수 있습니다:
#### Prometheus 클라이언트 라이브러리 통합
Prometheus Java client 1.8.0 API는 현재 io.prometheus.metrics package와 일치하는 의존성을 사용합니다. 기존 Gradle Java 프로젝트의 설정:
```groovy
dependencies {
implementation(platform("io.prometheus:prometheus-metrics-bom:1.8.0"))
implementation("io.prometheus:prometheus-metrics-core")
implementation("io.prometheus:prometheus-metrics-exporter-httpserver")
}
```
예제를 App.java로 저장합니다. Main은 9400 port에 metric을 노출하고 대기하며 실제 앱 request handler가 수행할 작업을 processRequest에 전달해야 합니다. Metric endpoint를 시작하는 것만으로 업무 요청이 계수되지는 않습니다. 합성 요청 수를 실측 트래픽으로 제시하지 않습니다:
```java
import io.prometheus.metrics.core.metrics.Counter;
import io.prometheus.metrics.core.metrics.Histogram;
import io.prometheus.metrics.exporter.httpserver.HTTPServer;
import java.io.IOException;
public class App {
private static final Counter requests = Counter.builder()
.name("app_requests_total").help("Requests processed by this application.")
.register();
private static final Histogram latency = Histogram.builder()
.name("app_request_latency_seconds").help("Request processing time in seconds.")
.register();
public static void processRequest(Runnable operation) {
requests.inc();
long started = System.nanoTime();
try {
operation.run();
} finally {
latency.observe((System.nanoTime() - started) / 1_000_000_000.0);
}
}
public static void main(String[] args) throws IOException, InterruptedException {
HTTPServer server = HTTPServer.builder().port(9400).buildAndStart();
Runtime.getRuntime().addShutdownHook(new Thread(server::close));
Thread.currentThread().join();
}
}
```
공개 client source에서 API·의존성 좌표를 확인했습니다. 감사 환경에 Java compiler·runtime이 없어 compile·실행하지 않았습니다. 배포 전 앱 build·lifecycle·request 경로에 통합해야 합니다. Label 종류를 제한하고 request ID·user ID·원문 URL을 기본 metric 차원으로 사용하지 않습니다.
#### 사용자 정의 지표 수집
소유 앱 Pod가 default namespace에 있고 app=my-app label로 TCP 9400의 /metrics를 실제 제공한다고 가정합니다. Service는 Pod를 선택하고 ServiceMonitor는 Service label·이름 있는 port를 선택합니다. Release=monitoring label과 namespaceSelector가 위 stack에 연결됩니다:
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-app-metrics
namespace: default
labels:
app: my-app
spec:
type: ClusterIP
selector:
app: my-app
ports:
- name: metrics
port: 9400
targetPort: 9400
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: app-monitor
namespace: monitoring
labels:
release: monitoring
spec:
namespaceSelector:
matchNames:
- default
selector:
matchLabels:
app: my-app
endpoints:
- port: metrics
interval: 30s
path: /metrics
```
Service EndpointSlice·Prometheus target 상태를 확인하고 적용되는 NetworkPolicy·보안 제어로 의도한 collector만 허용합니다. 이 클러스터 내부 HTTP metric 예제에는 검토한 네트워크 접근 범위가 필요하며 endpoint 요구에 따라 TLS·인증을 구성합니다. ServiceMonitor가 앱을 계측하거나 없는 metric server를 만드는 것은 아닙니다.
참고: [Java client quickstart](https://prometheus.github.io/client_java/getting-started/quickstart/), [Prometheus configuration](https://prometheus.io/docs/prometheus/latest/configuration/configuration/).
#### 사용자 정의 대시보드
Grafana에서 사용자 정의 대시보드를 생성하여 애플리케이션 지표를 시각화합니다:
1. Grafana에 로그인
2. "+" 아이콘을 클릭하고 "대시보드" 선택
3. "패널 추가" 클릭
4. 데이터 소스로 "Prometheus" 선택
5. PromQL 쿼리 작성(예: `rate(app_requests_total[5m])`)
6. 패널 제목, 설명 및 시각화 유형 구성
7. "저장" 클릭
## 알림 및 이벤트 관리
효과적인 알림 및 이벤트 관리는 EKS 클러스터에서 문제를 신속하게 감지하고 대응하는 데 필수적입니다. 이 섹션에서는 EKS 클러스터에서 알림 및 이벤트를 관리하기 위한 다양한 도구와 기술을 살펴봅니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-06-eks-monitoring-logging-3.html)
### CloudWatch 경보
경보 생성 전에 최근 datapoint와 정확한 metric namespace·이름·차원 집합을 확인합니다. 예제는 문서화된 ClusterName 단독 ContainerInsights node 집계와 Maximum을 사용합니다. 노드 hotspot을 찾는 데 도움이 되지만 전체 클러스터의 용량 가중 사용률은 아닙니다. 실제 노드 식별에는 node별 차원을 사용합니다.
80·80·85% threshold와 5분 평가 구간 두 개는 예시 정책입니다. 두 구간의 Maximum 초과가 10분 동안 매초 연속 포화되었다는 뜻은 아닙니다. 데이터 누락은 정상 사용률의 증거가 아닙니다. PutMetricAlarm은 기존 경보를 update하므로 이름을 재사용하기 전에 정의를 검토합니다. 알림 권한·subscription·전달 시험도 별도 선행 요건입니다.
#### 노드 CPU
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the verified cluster name}"
: "${AWS_REGION:?Set the metric Region}"
: "${ALARM_PREFIX:?Set a reviewed alarm-name prefix owned by this workflow}"
: "${SNS_TOPIC_ARN:?Set the approved notification topic ARN}"
aws cloudwatch put-metric-alarm --region "$AWS_REGION" \
--alarm-name "${ALARM_PREFIX}-node-cpu" \
--alarm-description "Example: maximum reported node cpu utilization exceeds 80 percent" \
--metric-name node_cpu_utilization --namespace ContainerInsights \
--statistic Maximum --period 300 --threshold 80 \
--comparison-operator GreaterThanThreshold \
--dimensions "Name=ClusterName,Value=$CLUSTER_NAME" \
--evaluation-periods 2 --datapoints-to-alarm 2 --treat-missing-data missing \
--alarm-actions "$SNS_TOPIC_ARN"
```
#### 노드 메모리
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the verified cluster name}"
: "${AWS_REGION:?Set the metric Region}"
: "${ALARM_PREFIX:?Set a reviewed alarm-name prefix owned by this workflow}"
: "${SNS_TOPIC_ARN:?Set the approved notification topic ARN}"
aws cloudwatch put-metric-alarm --region "$AWS_REGION" \
--alarm-name "${ALARM_PREFIX}-node-memory" \
--alarm-description "Example: maximum reported node memory utilization exceeds 80 percent" \
--metric-name node_memory_utilization --namespace ContainerInsights \
--statistic Maximum --period 300 --threshold 80 \
--comparison-operator GreaterThanThreshold \
--dimensions "Name=ClusterName,Value=$CLUSTER_NAME" \
--evaluation-periods 2 --datapoints-to-alarm 2 --treat-missing-data missing \
--alarm-actions "$SNS_TOPIC_ARN"
```
#### 노드 파일 시스템
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the verified cluster name}"
: "${AWS_REGION:?Set the metric Region}"
: "${ALARM_PREFIX:?Set a reviewed alarm-name prefix owned by this workflow}"
: "${SNS_TOPIC_ARN:?Set the approved notification topic ARN}"
aws cloudwatch put-metric-alarm --region "$AWS_REGION" \
--alarm-name "${ALARM_PREFIX}-node-disk" \
--alarm-description "Example: maximum reported node disk utilization exceeds 85 percent" \
--metric-name node_filesystem_utilization --namespace ContainerInsights \
--statistic Maximum --period 300 --threshold 85 \
--comparison-operator GreaterThanThreshold \
--dimensions "Name=ClusterName,Value=$CLUSTER_NAME" \
--evaluation-periods 2 --datapoints-to-alarm 2 --treat-missing-data missing \
--alarm-actions "$SNS_TOPIC_ARN"
```
이 경보 명령을 AWS에 실행하지 않았습니다. 참고: [Container Insights metric·차원](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Container-Insights-metrics-EKS.html).
### Prometheus Alertmanager
Prometheus가 경보 rule을 평가하고 Alertmanager가 결과 경보를 묶고 중복 처리해 전달합니다. 임의 이름의 ConfigMap을 만든다고 Operator 관리 Alertmanager가 자동으로 읽지는 않습니다. 검토한 stack은 Alertmanager 0.34.0을 사용하며 구성·자격 증명 file을 명시적으로 연결해야 합니다.
#### Alertmanager 구성
Slack 예제는 승인된 secret 관리 경로로 실제 webhook URL을 slack-url key에 담은 monitoring/notification-credentials를 준비합니다. 자격 증명을 ConfigMap·저장소 본문·Helm 명령 인수에 넣지 않습니다. 다음 비밀 값이 없는 routing 정의를 alertmanager-config.yaml로 저장합니다:
```yaml
global:
resolve_timeout: 5m
route:
group_by: [cluster, namespace, alertname]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: slack-notifications
routes:
- matchers:
- alertname="Watchdog"
receiver: discard
receivers:
- name: discard
- name: slack-notifications
slack_configs:
- api_url_file: /etc/alertmanager/secrets/notification-credentials/slack-url
channel: "#eks-alerts"
send_resolved: true
title: '[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}'
text: '{{ range .Alerts }}{{ .Annotations.summary }} — {{ .Annotations.description }}{{ "\n" }}{{ end }}'
```
Webhook은 대상 Slack 목적지의 권한이 있어야 하며 channel 필드가 Slack app 권한을 덮어쓰지는 않습니다. 예제는 Slack으로 주기적 메시지를 보내지 않도록 기본 항상 firing인 Watchdog 경보를 버립니다. 실제 dead-man·heartbeat 감시는 별도 receiver와 외부 수신 중단 감지가 필요하며 Watchdog 폐기가 알림 전달을 시험하는 것은 아닙니다.
일치하는 amtool로 file을 로컬 검증한 뒤 신규 소유 설치에만 구성 Secret을 만듭니다. 기존 Secret·release는 소유자의 검토한 update 절차를 따릅니다:
```bash
amtool --no-version-check check-config alertmanager-config.yaml
kubectl create secret generic alertmanager-routing -n monitoring \
--from-file=alertmanager.yaml=alertmanager-config.yaml
```
다음을 alertmanager-values.yaml로 저장하고 monitoring release의 검토한 전체 values와 함께 사용합니다. ConfigSecret이 Secret을 선택하며 Operator는 alertmanager.yaml key를 사용합니다. Secrets는 notification-credentials를 /etc/alertmanager/secrets/notification-credentials에 mount합니다. 배포 전에 monitoring에서 release=monitoring으로 선택되는 추가 AlertmanagerConfig 리소스도 검토합니다:
```yaml
alertmanager:
alertmanagerSpec:
useExistingSecret: true
configSecret: alertmanager-routing
secrets:
- notification-credentials
alertmanagerConfigSelector:
matchLabels:
release: monitoring
alertmanagerConfigNamespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
```
생성된 구성·reload 상태·notification 실패를 확인하고 paging에 의존하기 전에 승인된 시험 목적지로 구분 가능한 test alert를 전달합니다. 로컬 parser·route 검사는 Secret 존재·webhook 권한·SMTP 연결·실제 전달을 증명하지 않습니다. Replica 하나인 chart 예제에는 별도 가용성 설계도 필요합니다.
#### 알림 규칙 구성
Rule의 release label은 Prometheus selector와 일치합니다. 중복 경보를 피하도록 유사한 기본 rule이 있는지 확인합니다. CrashLoopBackOff는 Kubernetes waiting reason이며 재시작 rate threshold만으로 해당 상태를 입증할 수 없습니다. 예제는 지정한 kube-state-metrics series를 전제로 하고 namespace·Pod UID·container 또는 node 신원을 보존합니다:
```yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: kubernetes-alerts
namespace: monitoring
labels:
release: monitoring
spec:
groups:
- name: kubernetes-example
rules:
- alert: KubernetesPodCrashLooping
expr: max by (cluster, namespace, pod, uid, container) (max_over_time(kube_pod_container_status_waiting_reason{job="kube-state-metrics",reason="CrashLoopBackOff"}[5m])) >= 1
for: 5m
labels:
severity: critical
annotations:
summary: "CrashLoopBackOff observed for {{ $labels.namespace }}/{{ $labels.pod }}"
description: "Inspect container {{ $labels.container }} logs and events; the rule tracks recent waiting reasons, not a restart-count guarantee."
- alert: KubernetesNodeMemoryPressure
expr: max by (cluster, node) (kube_node_status_condition{job="kube-state-metrics",condition="MemoryPressure",status="true"}) == 1
for: 5m
labels:
severity: warning
annotations:
summary: "Node {{ $labels.node }} reports MemoryPressure"
description: "Inspect node capacity and workloads; the observed condition has matched for five minutes."
- alert: KubernetesNodeDiskPressure
expr: max by (cluster, node) (kube_node_status_condition{job="kube-state-metrics",condition="DiskPressure",status="true"}) == 1
for: 5m
labels:
severity: warning
annotations:
summary: "Node {{ $labels.node }} reports DiskPressure"
description: "Inspect disk space and inodes; the observed condition has matched for five minutes."
```
CrashLoopBackOff rule은 최근 5분 구간을 반복 관찰하며 재시작 횟수를 세는 식이 아닙니다. Node pressure는 관측한 kubelet condition으로 일반적인 사용률과 다릅니다. Series 부재·staleness로 경보가 나오지 않을 수 있으므로 scrape 상태·metric 존재를 별도로 감시합니다. Threshold·대기 시간은 예시이며 장애 대응 보장이 아닙니다.
### EventBridge 이벤트 규칙
이벤트를 발행하는 서비스가 공개한 이름·payload 필드를 사용합니다. EKS 직접 event catalog에는 add-on 생성·update·삭제 결과, add-on health degraded·restored와 Fargate 예정 종료가 있습니다. 기존 예제의 일반적인 EKS Cluster State Change·EKS Node Group State Change 이름은 공개 catalog에 없습니다. CloudTrail로 전달되는 EKS API 활동은 다른 detail-type·payload를 사용합니다.
#### 직접 Add-on Health Event와 CloudTrail API 활동
다음은 선택한 account·Region 범위의 대안 두 개를 작성합니다. 단일 클러스터가 아니라 해당 account·Region의 일치하는 활동을 포함합니다. 범위를 더 좁히려면 해당 event type의 실제 수신 event·문서화된 필드를 확인해 시험하며 모든 event에 detail.clusterName이 있다고 가정하지 않습니다.
```bash
set -euo pipefail
: "${AWS_REGION:?Set the owned Region}"
: "${ACCOUNT_ID:?Set the owned 12-digit AWS account ID}"
python3 - "$AWS_REGION" "$ACCOUNT_ID" <<'PY'
import json
import re
import sys
region, account = sys.argv[1:]
if not re.fullmatch(r"[0-9]{12}", account):
raise SystemExit("ACCOUNT_ID must contain 12 digits")
base = {"source": ["aws.eks"], "account": [account], "region": [region]}
patterns = {
"eks-addon-health-pattern.json": dict(base, **{
"detail-type": ["EKS Addon Health Degraded", "EKS Addon Health Restored"],
}),
"eks-update-api-pattern.json": dict(base, **{
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventSource": ["eks.amazonaws.com"],
"eventName": ["UpdateClusterVersion", "UpdateNodegroupVersion"],
},
}),
}
for filename, pattern in patterns.items():
with open(filename, "w") as stream:
json.dump(pattern, stream, indent=2)
stream.write("\n")
PY
```
CloudTrail pattern은 적용되는 upgrade·rollback 요청을 포함한 UpdateClusterVersion·UpdateNodegroupVersion API event를 선택합니다. API 호출 event는 시도·접수된 요청을 기록하며 비동기 update 완료를 뜻하지 않습니다. Error 필드를 확인하고 성공한 요청도 update ID·DescribeUpdate 상태와 연계합니다. 관련 CloudTrail management-event 전달 구성이 필요합니다. 직접·CloudTrail 기반 전달 모두 best effort이므로 서비스 상태 조회·실패 감시도 사용합니다.
#### 소유한 SNS Target 연결
같은 account·Region에 standard SNS topic·확인한 subscription과 EventBridge target execution role을 준비합니다. 역할은 EventBridge를 신뢰하고 대상 topic의 sns:Publish 및 암호화에 필요한 KMS 권한을 가져야 합니다. 운영자에게는 rule·target 제어와 승인 역할 전달 권한이 필요합니다. 현재 EventBridge는 SNS target의 execution role을 지원합니다. Resource-based policy도 대안이지만 target 추가만으로 자동 구성되지는 않습니다.
생성한 pattern file 하나와 신규 소유 rule 이름을 선택합니다. 사전 조회에서 존재하는 이름은 거부하지만 PutRule은 upsert이므로 소유권·동시 변경을 조율해야 합니다. Rule은 비활성 상태로 만들고 PutTargets의 부분 실패도 workflow를 중단합니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the reviewed Region}"
: "${ACCOUNT_ID:?Set the reviewed account ID}"
: "${RULE_NAME:?Set a new owned rule name}"
: "${PATTERN_FILE:?Select one reviewed pattern JSON file}"
: "${SNS_TOPIC_ARN:?Set the prepared standard SNS topic ARN}"
: "${EVENTBRIDGE_ROLE_ARN:?Set the prepared EventBridge target execution role ARN}"
python3 - "$AWS_REGION" "$ACCOUNT_ID" "$RULE_NAME" "$PATTERN_FILE" \
"$SNS_TOPIC_ARN" "$EVENTBRIDGE_ROLE_ARN" <<'PY'
import json
import re
import sys
region, account, name, path, topic, role = sys.argv[1:]
if not re.fullmatch(r"[0-9]{12}", account) or not re.fullmatch(r"[A-Za-z0-9._-]{1,64}", name):
raise SystemExit("Review the account ID and rule name")
with open(path) as stream:
pattern = json.load(stream)
if pattern.get("account") != [account] or pattern.get("region") != [region] or pattern.get("source") != ["aws.eks"]:
raise SystemExit("Pattern scope differs from the selected account/Region/service")
t, r = topic.split(":", 5), role.split(":", 5)
if (len(t) != 6 or len(r) != 6 or t[0] != "arn" or r[0] != "arn"
or t[1] != r[1] or t[2:5] != ["sns", region, account]
or r[2:5] != ["iam", "", account] or not r[5].startswith("role/")
or not t[5] or t[5].endswith(".fifo")):
raise SystemExit("Use the reviewed same-account standard topic and target role")
with open("eks-event-targets.json", "w") as stream:
json.dump([{"Id": "ops-sns", "Arn": topic, "RoleArn": role}], stream)
PY
EXISTING=$(aws events list-rules --region "$AWS_REGION" --event-bus-name default \
--name-prefix "$RULE_NAME" --query 'Rules[].Name' --output json)
python3 - "$RULE_NAME" "$EXISTING" <<'PY'
import json
import sys
if sys.argv[1] in json.loads(sys.argv[2]):
raise SystemExit("Rule already exists; use its owner's reviewed update procedure")
PY
aws events put-rule --region "$AWS_REGION" --event-bus-name default \
--name "$RULE_NAME" --state DISABLED --event-pattern "file://$PATTERN_FILE"
aws events put-targets --region "$AWS_REGION" --event-bus-name default \
--rule "$RULE_NAME" --targets file://eks-event-targets.json \
--output json > eks-event-targets-result.json
python3 - <<'PY'
import json
with open("eks-event-targets-result.json") as stream:
result = json.load(stream)
if result["FailedEntryCount"] != 0:
raise SystemExit("Target configuration failed; inspect FailedEntries before retrying")
print("Rule remains DISABLED; review the target and pattern before enabling")
PY
```
전체 target 응답·역할·subscription·전달 retry·dead-letter policy와 대표 수신 event를 검토합니다. TestEventPattern으로 pattern 일치를 확인할 수 있지만 target 권한·전달을 시험하지는 않습니다. 해당 검토 후 활성화합니다:
```bash
aws events enable-rule --region "$AWS_REGION" --event-bus-name default --name "$RULE_NAME"
```
활성화 후 matched·failed invocation을 감시하고 승인된 end-to-end event를 확인합니다. Rule·target·API 응답 성공만으로 경보 전달이 보장되지는 않습니다. 이번 감사에서는 EventBridge·SNS resource·event·notification을 생성하지 않았습니다.
참고: [EKS EventBridge event catalog](https://docs.aws.amazon.com/eventbridge/latest/ref/events-ref-eks.html), [target 권한](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-use-resource-based.html).
### Kubernetes 이벤트 모니터링
Kubernetes Event는 scheduling·image pull·restart 등 객체 활동 조사에 도움이 됩니다. 수명이 짧은 best-effort 관측이며 반복 발생이 집계될 수 있으므로 완전하고 영구적인 audit trail은 아닙니다. 먼저 대상 namespace를 확인합니다:
```bash
kubectl events -n default --types=Warning
kubectl events -n default --types=Warning --watch
```
#### Collector 버전과 소유권
원래 Opsgenie exporter는 유지 관리되지 않습니다. 활성 fork는 resmoio에서 mustafaakin/kubernetes-event-exporter로 이전했습니다. 여기서 확인한 최신 공개 release는 2024년 2월의 v1.7이며 이후 repository 개발도 있습니다. 활성 repository나 오래된 latest tag만으로 현재 patch가 적용된 운영 image가 확인되는 것은 아닙니다.
다음 참조는 v1.7 configuration·watcher source를 기준으로 확인했습니다. 해당 구성·CLI 계약을 유지하고 UID 65532·읽기 전용 root filesystem에서 mount한 구성을 읽을 수 있도록 자체 build 절차로 검토·patch한 소유 image가 필요합니다. Render 시 immutable digest를 지정하며 공개 latest image나 임의 digest를 제공하지 않습니다. 이번 감사에서는 image build·취약점 검토·runtime 호환성 시험을 실행하지 않았습니다.
#### Namespace 범위·RBAC·구성
예제는 기존 monitoring namespace에서 실행하지만 default의 core/v1 Event만 감시합니다. OmitLookup=true는 관련 객체 label·annotation 보강을 위한 별도 GET 요청을 끄므로 Role은 default의 Event 읽기만 허용하고 Secret·모든 API resource의 wildcard 읽기를 허용하지 않습니다. Replica 하나에서 leader election을 끄므로 lease 쓰기 권한도 부여하지 않습니다. 범위를 바꿀 때 namespace·Role·RoleBinding을 함께 맞춥니다.
다음을 event-exporter-template.yaml로 저장합니다. Image marker가 있으므로 사용 전에 반드시 render해야 합니다. Match rule은 이름 있는 receiver를 가리키며 receiver는 기존 소유 컨테이너 log pipeline에서 수집할 수 있는 JSON을 stdout으로 출력합니다. 이 구성은 Warning event만 내보냅니다:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: event-exporter
namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: event-exporter-read
namespace: default
rules:
- apiGroups: [""]
resources: [events]
verbs: [get, list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: event-exporter-read
namespace: default
subjects:
- kind: ServiceAccount
name: event-exporter
namespace: monitoring
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: event-exporter-read
---
apiVersion: v1
kind: ConfigMap
metadata:
name: event-exporter-config
namespace: monitoring
data:
config.yaml: |
logLevel: warn
logFormat: json
namespace: default
omitLookup: true
maxEventAgeSeconds: 60
metricsNamePrefix: event_exporter_
leaderElection:
enabled: false
route:
routes:
- match:
- type: Warning
receiver: event-log
receivers:
- name: event-log
stdout:
deDot: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: event-exporter
namespace: monitoring
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: event-exporter
template:
metadata:
labels:
app: event-exporter
spec:
serviceAccountName: event-exporter
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: event-exporter
image: REVIEWED_EVENT_EXPORTER_IMAGE
args:
- -conf=/etc/event-exporter/config.yaml
- -metrics-address=127.0.0.1:2112
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
volumeMounts:
- name: config
mountPath: /etc/event-exporter
readOnly: true
volumes:
- name: config
configMap:
name: event-exporter-config
```
Render에는 Python 3·PyYAML이 필요합니다. 먼저 기존 event-exporter 리소스·소유자를 확인하고 기존 설치는 소유자의 upgrade 절차를 사용합니다:
```bash
set -euo pipefail
: "${EVENT_EXPORTER_IMAGE:?Set the reviewed, patched image reference including @sha256 digest}"
python3 - "$EVENT_EXPORTER_IMAGE" <<'PY'
import re
import sys
import yaml
image = sys.argv[1]
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:/-]*@sha256:[a-f0-9]{64}", image):
raise SystemExit("Use a reviewed image pinned by SHA256 digest")
with open("event-exporter-template.yaml") as stream:
objects = list(yaml.safe_load_all(stream))
deployment = next(obj for obj in objects if obj["kind"] == "Deployment")
container = deployment["spec"]["template"]["spec"]["containers"][0]
if container["image"] != "REVIEWED_EVENT_EXPORTER_IMAGE":
raise SystemExit("Review the template before replacing its image")
container["image"] = image
with open("event-exporter-rendered.yaml", "w") as stream:
yaml.safe_dump_all(objects, stream, sort_keys=False)
PY
```
신규 소유 설치는 render한 manifest·image pull 신원·API 연결을 검토한 뒤 적용합니다. Recreate는 rollout 중 replica 중첩을 피하지만 중단이 생기므로 고가용성 collector가 아닙니다:
```bash
kubectl apply -f event-exporter-rendered.yaml
kubectl rollout status deployment/event-exporter -n monitoring --timeout=120s
kubectl logs -n monitoring deployment/event-exporter --tail=100
```
#### 누락·반복 Event·Alert Payload
확인한 v1.7 watcher는 add notification을 처리하고 update·delete callback을 무시합니다. 따라서 반복 Event의 count·series update를 신뢰할 수 있는 내보낸 발생 횟수로 볼 수 없습니다. MaxEventAgeSeconds=60은 예시 수신 age 기준이며 backend retention이 아닙니다. 시작·throttling·중단 시 오래된 event를 버릴 수 있고 재조회·restart로 관측이 중복될 수도 있습니다. Event 횟수로 운영 결정을 내리기 전에 필요한 update 처리·buffer·영구 목적지를 선택하고 시험합니다.
Log뿐 아니라 watch·discard counter도 확인합니다. 예제의 exporter metric listener는 loopback에 bind되므로 권한 있는 운영자가 로컬 port-forward로 확인할 수 있습니다. Prometheus에 endpoint를 자동으로 추가하지는 않습니다:
```bash
kubectl port-forward --address 127.0.0.1 -n monitoring \
deployment/event-exporter 2112:2112
```
Forward 실행 중 `http://127.0.0.1:2112/metrics`의 /metrics를 읽습니다. Rollout·stdout record만으로 CloudWatch·OpenSearch 저장이 확인되지는 않으므로 기존 log collector·대상 목적지를 검증합니다. Event message에는 민감한 운영 정보가 포함될 수 있어 접근·필터·retention을 검토해야 합니다.
Webhook URL만 바꿔 원문 Kubernetes Event 객체를 Alertmanager로 보내면 안 됩니다. 현재 /api/v2/alerts endpoint는 자체 alert-array schema를 요구하며 기존 /api/v1/alerts는 현재 API가 아닙니다. 신원·label·annotation·해제 의미를 변환하는 명시적 adapter가 필요합니다. 위 stdout·log 경로가 해당 adapter를 구현한다고 가정하지 않습니다.
참고: [유지 관리되는 exporter 저장소](https://github.com/mustafaakin/kubernetes-event-exporter), [v1.7 watcher](https://github.com/mustafaakin/kubernetes-event-exporter/blob/v1.7/pkg/kube/watcher.go), [Alertmanager v2 API](https://github.com/prometheus/alertmanager/blob/v0.34.0/api/v2/openapi.yaml).
### 알림 채널 통합
실제 Alertmanager 버전이 지원하는 receiver를 사용합니다. 아래 fragment는 Kubernetes 리소스가 아니라 alertmanager-config.yaml의 receivers 항목입니다. 사용할 receiver를 추가한 뒤 route.receiver 또는 일치하는 하위 route를 정확한 이름으로 연결하고 전체 구성을 검증해 선택한 Secret을 갱신합니다. 사용하지 않는 receiver 항목만 추가하면 경보가 전달되지 않습니다.
#### Slack 통합
위의 연결된 Slack 예제는 mount한 webhook file을 사용합니다. 허용 channel을 확인하고 민감정보 검토 없이 전체 alert label·annotation을 게시하지 않습니다. 저장소에 commit하는 Provider·ConfigMap 예제에 실제 Slack token·webhook을 넣지 않습니다.
#### PagerDuty 통합
Events API v2 integration은 notification-credentials의 pagerduty-routing-key key를 준비합니다. Routing_key_file은 일반 PagerDuty REST API token이 아닌 integration key를 가리킵니다. Critical만 paging하려면 검토한 하위 route로 critical 경보를 연결합니다:
```yaml
name: pagerduty-notifications
pagerduty_configs:
- routing_key_file: /etc/alertmanager/secrets/notification-credentials/pagerduty-routing-key
send_resolved: true
severity: '{{ if eq .CommonLabels.severity "critical" }}critical{{ else }}warning{{ end }}'
description: '{{ .CommonLabels.alertname }}'
```
#### 이메일 통합
예약된 예시 SMTP host·주소·사용자를 승인된 mail service 설정으로 바꾸고 같은 자격 증명 Secret에 smtp-password를 준비합니다. TLS 필수를 유지하고 server 신뢰·발신 권한·전달을 검증합니다. 이는 Flux Provider가 아닌 Alertmanager email receiver입니다:
```yaml
name: email-notifications
email_configs:
- to: oncall@example.com
from: alerts@example.com
smarthost: smtp.example.com:587
auth_username: alerting-user
auth_password_file: /etc/alertmanager/secrets/notification-credentials/smtp-password
require_tls: true
send_resolved: true
```
#### 기본 Amazon SNS 통합
Alertmanager 0.34.0은 기본 SNS receiver를 제공하므로 이 경로에 정의되지 않은 sns-forwarder webhook service를 둘 필요는 없습니다. 예시 Region·account·topic을 소유한 standard topic으로 바꿉니다. 실제 Alertmanager ServiceAccount에 해당 topic의 sns:Publish와 필요한 암호화 topic KMS 권한·네트워크 연결을 갖춘 지원 AWS 신원을 부여합니다. Prometheus·앱 역할이 자동으로 Alertmanager 역할이 되지는 않습니다. 정적 AWS access key를 포함하지 않습니다:
```yaml
name: sns-notifications
sns_configs:
- sigv4:
region: us-west-2
topic_arn: arn:aws:sns:us-west-2:123456789012:eks-alerts
send_resolved: true
subject: 'EKS {{ .CommonLabels.alertname }}'
```
Topic subscription·목적지 policy를 확인하고 전달을 별도로 시험합니다. FIFO topic에는 추가 deduplication·grouping 조건이 있으며 이 fragment는 standard topic 대상입니다. Receiver parser 검사는 IAM·SNS 게시를 실행하지 않습니다.
Flux notification Provider는 Flux Alert 리소스·선택한 event source와 함께 Flux reconciliation event를 전달합니다. 존재하는 것만으로 임의 Prometheus alert·Kubernetes Event를 수신하지는 않습니다. 해당 workflow와 여기의 Alertmanager receiver 구성을 구분합니다.
참고: [Alertmanager 구성·receiver](https://prometheus.io/docs/alerting/latest/configuration/).
### 알림 관리 및 에스컬레이션
알림을 효과적으로 관리하고 에스컬레이션하기 위한 전략을 구현할 수 있습니다:
#### 알림 심각도 수준
알림을 다음과 같은 심각도 수준으로 분류합니다:
- **Critical**: 즉각적인 조치가 필요한 심각한 문제
- **Warning**: 주의가 필요하지만 즉각적인 조치가 필요하지 않은 문제
- **Info**: 정보 제공 목적의 알림
#### 알림 에스컬레이션 정책
PagerDuty와 같은 도구를 사용하여 알림 에스컬레이션 정책을 구현합니다:
1. **1차 대응**: 온콜 엔지니어에게 알림
2. **에스컬레이션 1**: 15분 후 응답이 없으면 백업 엔지니어에게 알림
3. **에스컬레이션 2**: 30분 후 응답이 없으면 팀 리더에게 알림
4. **에스컬레이션 3**: 45분 후 응답이 없으면 관리자에게 알림
#### 알림 피로 감소
알림 피로를 줄이기 위한 전략을 구현합니다:
1. **알림 그룹화**: 관련 알림을 그룹화하여 중복 알림 감소
2. **알림 필터링**: 중요한 알림만 전달하도록 필터링
3. **알림 조절**: 반복되는 알림의 빈도 제한
4. **알림 시간대**: 비즈니스 크리티컬하지 않은 알림은 업무 시간에만 전달
## 로그 분석 및 시각화
로그 분석 및 시각화는 EKS 클러스터에서 발생하는 문제를 진단하고 해결하는 데 중요한 역할을 합니다. 이 섹션에서는 EKS 클러스터의 로그를 분석하고 시각화하기 위한 다양한 도구와 기술을 살펴봅니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-06-eks-monitoring-logging-4.html)
### CloudWatch Logs Insights
Query 전에 실제 log group과 제한된 시간 구간을 선택합니다. Container·API/audit·authenticator stream의 schema는 서로 다릅니다. 위의 독립 Fluent Bit values는 파싱한 앱 JSON을 data, Kubernetes metadata를 kubernetes에 두고 원문 log 필드를 유지합니다. 다른 collector·구성은 field path가 다를 수 있으므로 먼저 저장된 event를 확인합니다.
#### 컨테이너 로그 쿼리
문자열 level 필드가 있는 구조화 앱 record 예제:
```
fields @timestamp, @log, kubernetes.pod_name, data.level, data.message, log
| filter kubernetes.namespace_name = "default"
| filter kubernetes.container_name = "app"
| filter toupper(data.level) = "ERROR"
| sort @timestamp desc
| limit 20
```
평문·JSON 파싱 실패 record에는 data.level이 없습니다. 결과가 없다고 “오류 없음”으로 해석하지 말고 log 필드를 별도로 확인합니다.
#### Audit Log의 API 오류 응답
Audit logging이 켜져 있고 record에 responseStatus가 있으면 모든 API-server 본문에서 Error 문자열을 찾는 대신 숫자 응답 code를 사용합니다:
```
fields @timestamp, verb, objectRef.resource, responseStatus.code, user.username
| filter @logStream like /kube-apiserver-audit/
| filter responseStatus.code >= 400
| sort @timestamp desc
| limit 20
```
선택한 audit stream·시간 구간에 기록된 응답을 찾으며 모든 API 요청 시도를 의미하지 않습니다. 4xx는 호출자·권한 문제일 수 있고 자동으로 control-plane 장애를 뜻하지 않습니다. Logging policy·stage·수집·retention 등이 포함 범위에 영향을 줍니다.
#### Authenticator Event 확인
실패 filter를 추가하기 전에 현재 authenticator message 형식을 확인합니다:
```
fields @timestamp, @message
| filter @logStream like /authenticator/
| sort @timestamp desc
| limit 50
```
고정된 “authentication failed” 문자열은 실제 실패를 놓치거나 무관한 본문과 일치할 수 있습니다. 관측한 message·status 필드를 확인하고 audit의 401·403 응답과 연계합니다. 인증과 Kubernetes 권한 검사는 별도이며 하나의 stream이 양쪽의 완전한 증거가 되지는 않습니다.
#### Level별 Log 수
같은 구조화 앱 schema의 집계 예제:
```
fields toupper(data.level) as level, kubernetes.namespace_name
| filter ispresent(data.level)
| stats count(*) as log_records by @log, level, kubernetes.namespace_name
| sort log_records desc
```
이는 고유 요청 수·오류율이 아닌 log record 수입니다. Retry·반복 message·collector 중복이 count에 영향을 줍니다. 가정한 공백 구분 형식으로 임의 JSON·container·control-plane log를 신뢰성 있게 분류할 수는 없습니다.
참고: [JSON field discovery](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData-discoverable-fields.html), [query function](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_QuerySyntax-operations-functions.html).
### Amazon OpenSearch Service
Amazon OpenSearch Service(이전의 Amazon Elasticsearch Service)를 사용하여 EKS 클러스터의 로그를 저장, 분석 및 시각화할 수 있습니다:
#### 소유자가 관리하는 OpenSearch 도메인 준비
플랫폼 소유자가 검토한 provisioning 절차로 준비한 도메인을 사용합니다. 워크로드에 맞는 지원 engine·버전, 용량과 보존 정책을 선택하세요. 이 로깅 예제는 승인된 네트워크 경로, HTTPS, 저장 시 암호화, node-to-node 암호화와 fine-grained access control(FGAC)이 있는 VPC 도메인을 사용합니다. Collector 구성 전에 실제 도메인을 확인합니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the owned domain Region}"
: "${OPENSEARCH_DOMAIN:?Set the owned domain name}"
aws opensearch describe-domain --region "$AWS_REGION" \
--domain-name "$OPENSEARCH_DOMAIN" \
--query 'DomainStatus.{ARN:ARN,Engine:EngineVersion,Endpoint:Endpoint,EndpointV2:EndpointV2,Endpoints:Endpoints,VPC:VPCOptions,HTTPS:DomainEndpointOptions.EnforceHTTPS,AtRest:EncryptionAtRestOptions.Enabled,NodeToNode:NodeToNodeEncryptionOptions.Enabled,FGAC:AdvancedSecurityOptions.Enabled}'
```
네트워크 도달성, domain access policy와 FGAC는 별도 계층입니다. SigV4 collector 신원은 domain·IAM policy에서 허용되어야 하고 제한된 OpenSearch ingestion role에 매핑되어야 합니다. 관리 권한과 ingestion 권한을 분리합니다. Internal user database를 사용하는 구성도 IAM 신원을 매핑할 수 있지만 한 요청에 HTTP basic 자격 증명과 SigV4 자격 증명을 함께 사용하지 않습니다.
승인된 신원·secret 관리 경로로 자격 증명을 제공하세요. 공개 문서의 공통 관리자 암호와 wildcard public access policy는 적절한 로깅 구성이 아닙니다. 기존 public 도메인을 VPC로 옮기려면 새 도메인과 데이터 migration이 필요하며 endpoint 설정만 바꾸는 작업이 아닙니다. 기존 Terraform resource type·address 변경에도 소유권·state migration 계획이 필요합니다.
반환된 endpoint hostname을 collector 구성에 사용하고 CA·hostname과 승인된 ingestion을 검증합니다. 도메인 조회만으로 ingestion·운영 준비 완료를 검증한 것은 아닙니다.
참고: [OpenSearch FGAC](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/fgac.html), [VPC 도메인과 migration](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/vpc.html).
#### Fluent Bit를 사용하여 OpenSearch로 로그 전송
컨테이너 로깅 절의 연결된 Fluent Bit values·RBAC post-renderer·OpenSearch overlay를 사용합니다. 실제 소유 endpoint·SigV4 권한·FGAC ingestion mapping을 준비하세요. 다른 이름의 ConfigMap을 mount하는 Helm release에 무관한 fluent-bit-config를 apply해도 구성이 바뀌지는 않습니다.
#### OpenSearch Dashboards를 사용한 로그 시각화
OpenSearch Dashboards에서 다음과 같은 시각화를 생성할 수 있습니다:
1. **로그 탐색기**: 로그 검색 및 필터링
2. **대시보드**: 로그 데이터를 기반으로 한 대시보드 생성
3. **시각화**: 로그 데이터를 기반으로 한 차트 및 그래프 생성
4. **알림**: 로그 패턴에 기반한 알림 구성
### Grafana Loki
Grafana Loki는 로그 집계 시스템으로, Prometheus와 유사한 레이블 기반 접근 방식을 사용합니다:
#### Loki 설치
[현재 Loki 구성 문서](https://www.atomai.click/kubernetes-docs/llms/ko/observability/logging/01-loki.md)를 따라 소유한 배포와 Alloy 같은 지원 client를 구성합니다. Deprecated loki-stack·Promtail 조합은 현재 설치 경로가 아닙니다. 운영 log 전송 전에 storage·인증·label·retention 정책을 구성합니다.
#### LogQL 쿼리 예시
다음은 collector가 namespace·pod stream label을 만들고 최상위 level 필드가 있는 앱 JSON을 저장한다고 가정합니다. Label·JSON 추출 결과가 CloudWatch·Fluent Bit의 data wrapper와 자동으로 같아지는 것은 아닙니다:
```logql
{namespace="default"} |= "ERROR"
{namespace="default", pod=~"app-.*"} | json | __error__=""
sum by (level) (
count_over_time(
{namespace="default"} | json | __error__="" | level=~"INFO|WARN|ERROR" [5m]
)
)
```
JSON parser 오류 filter는 파싱 뒤에 둡니다. Metric query에서는 pipeline error를 제외해야 하며, 누락·형식 오류 record를 별도 조사해 filter가 수집 문제를 가리지 않도록 합니다. 예제 level은 대문자 구조화 log sample과 일치합니다. 실제 데이터의 field·대소문자에 맞춰 조정하세요. Stream label 종류를 제한하고 request ID·user ID를 기본 고카디널리티 stream label로 사용하지 않습니다.
#### Grafana 대시보드 생성
Grafana에서 Loki 데이터 소스를 사용하여 로그 대시보드를 생성할 수 있습니다:
1. Grafana에 로그인
2. "+" 아이콘을 클릭하고 "대시보드" 선택
3. "패널 추가" 클릭
4. 데이터 소스로 "Loki" 선택
5. LogQL 쿼리 작성
6. 패널 제목, 설명 및 시각화 유형 구성
7. "저장" 클릭
### AWS CloudTrail
CloudTrail은 EKS cluster·add-on·node-group 관리 같은 지원 AWS API 활동을 기록합니다. Kubernetes API audit log·앱 request log의 대체 수단은 아닙니다. 준비되지 않은 bucket으로 중복 trail을 만들기보다 기존 조직·계정 trail을 먼저 확인합니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the Region to inspect}"
aws cloudtrail describe-trails --region "$AWS_REGION" --include-shadow-trails \
--query 'trailList[].{Name:Name,ARN:TrailARN,HomeRegion:HomeRegion,Organization:IsOrganizationTrail,MultiRegion:IsMultiRegionTrail}'
```
소유 trail·home Region을 선택한 뒤 확인합니다:
```bash
set -euo pipefail
: "${TRAIL_ARN:?Choose the existing owned trail ARN}"
: "${TRAIL_HOME_REGION:?Use the home Region of the selected trail}"
aws cloudtrail get-trail-status --region "$TRAIL_HOME_REGION" --name "$TRAIL_ARN"
aws cloudtrail get-event-selectors --region "$TRAIL_HOME_REGION" --trail-name "$TRAIL_ARN"
```
Logging·delivery 오류와 event selector를 확인합니다. 신규 trail에는 검토한 bucket·delivery policy, 암호화·key 권한, retention·소유권 구성이 필요합니다. Trail resource만으로 저장소 전달이 확인되지는 않으며 trail 생성과 logging 시작도 별도 작업입니다.
#### 최근 Management Event
CloudTrail Event history는 trail을 만들지 않아도 사용할 수 있으며 선택한 Region의 최근 90일 management event를 포함합니다. 다음 읽기 전용 예제는 최근 1시간을 요청하고 출력 batch를 50개로 제한합니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the Region to query}"
python3 - <<'PY'
import datetime
import json
end = datetime.datetime.now(datetime.timezone.utc)
request = {
"LookupAttributes": [{"AttributeKey": "EventSource", "AttributeValue": "eks.amazonaws.com"}],
"StartTime": (end - datetime.timedelta(hours=1)).isoformat(),
"EndTime": end.isoformat(),
}
with open("cloudtrail-lookup.json", "w") as stream:
json.dump(request, stream, indent=2)
PY
aws cloudtrail lookup-events --region "$AWS_REGION" \
--cli-input-json file://cloudtrail-lookup.json --max-items 50 --output json
```
CLI가 NextToken을 반환하면 같은 요청·시간 구간에 --starting-token을 사용해 나머지를 확인합니다. 전체 신원·request·error 필드는 CloudTrailEvent JSON 문자열을 파싱하며 Username만으로 호출자를 완전히 식별하지 않습니다. Event history는 장기 보존 계획이 아니며 모든 data-event 범주를 포함하지 않습니다.
#### 사용 자격이 있는 기존 고객의 CloudTrail Lake
CloudTrail Lake는 2026년 5월 31일부터 신규 고객에게 닫혔고 현재는 중요 bug·security update를 제공합니다. 기존 고객은 문서화된 조건에 따라 계속 사용할 수 있습니다. 조직 event data store는 신규 member account를 포함할 수 있지만 기존 계정 단위 store가 새로 추가한 계정까지 Lake 수집을 자동 확장하지는 않습니다. CloudTrail Trails·Insights·Aggregated Events는 계속 지원됩니다. 신규 분석 설계는 새 Lake 가입을 요구하지 말고 AWS의 현재 CloudWatch migration·ingestion 가이드를 검토합니다.
사용 가능한 기존 store에서는 EVENT_DATA_STORE_ID를 Lake query editor에서 선택한 실제 ID로 바꿉니다. Eks_events 같은 임의 table alias가 아닙니다. 다음은 원래 예시의 2025년 7월 1–11일 구간을 유지합니다. 이번 감사에서 실행한 query가 아니며 해당 store가 실제로 그 기간을 보존하고 있어야 합니다:
```sql
SELECT eventTime, eventName, userIdentity.arn, requestParameters
FROM EVENT_DATA_STORE_ID
WHERE eventSource = 'eks.amazonaws.com'
AND eventTime >= '2025-07-01 00:00:00'
AND eventTime < '2025-07-12 00:00:00'
ORDER BY eventTime DESC
```
배타적인 상한으로 마지막 날짜 전체를 포함하며 timestamp가 초 단위로만 존재한다고 가정하지 않습니다. Query는 사용 가능한 EKS AWS 관리 활동을 반환하고 신원 필드는 caller type에 따라 다를 수 있습니다. Query 실행에는 서비스 비용이 발생할 수 있습니다.
참고: [CloudTrail Event history](https://aws.amazon.com/cloudtrail/features/), [Lake availability 변경](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-lake-service-availability-change.html), [event data store 선택](https://docs.aws.amazon.com/help-panel/awscloudtrail/latest/console/query-editor-eds.html).
### 로그 분석 모범 사례
EKS 클러스터의 로그를 효과적으로 분석하기 위한 모범 사례:
#### 구조화된 로깅
애플리케이션에서 구조화된 로그 형식(예: JSON)을 사용합니다:
```json
{
"timestamp": "2025-07-11T13:00:00Z",
"level": "INFO",
"message": "Request processed successfully",
"request_id": "12345",
"user_id": "user-789",
"duration_ms": 45,
"status_code": 200
}
```
#### 상관 ID
위의 2025년 JSON·duration·가상 식별자는 설명용 예제이며 신규 실측이 아닙니다. 가명 처리한 user·session 식별자도 민감할 수 있으므로 필요한 필드만 적절한 접근·보존 범위로 포함합니다.
SLF4J·MDC 지원 logging backend를 사용하는 Java 앱에서는 길이·문자를 제한한 식별자를 쓰고 중첩 호출의 기존 context를 복원합니다. Framework에 의존하지 않는 다음 helper는 정의되지 않은 Request type을 제거하고 UUID import를 포함합니다:
```java
import java.util.UUID;
import java.util.regex.Pattern;
import org.slf4j.MDC;
public final class CorrelationContext {
private static final Pattern SAFE_ID = Pattern.compile("[A-Za-z0-9._-]{1,128}");
public static void run(String suppliedId, Runnable operation) {
String correlationId = suppliedId != null && SAFE_ID.matcher(suppliedId).matches()
? suppliedId : UUID.randomUUID().toString();
String previous = MDC.get("correlation_id");
MDC.put("correlation_id", correlationId);
try {
operation.run();
} finally {
if (previous == null) {
MDC.remove("correlation_id");
} else {
MDC.put("correlation_id", previous);
}
}
}
}
```
추출한 request header·실제 작업으로 CorrelationContext.run을 호출합니다. Encoder·pattern에서 correlation_id를 포함하도록 구성해야 하며 MDC에 값을 넣는 것만으로 log 형식이 바뀌지는 않습니다. 호출자가 제공한 correlation ID는 추적 metadata이지 인증 정보가 아닙니다. MDC context는 thread에 연결되므로 executor·reactive 경계에서는 backend·framework가 지원하는 방법으로 전파·복원해야 합니다. Helper는 source 검토를 했으며 이 환경에는 Java compiler·runtime이 없었습니다.
참고: [SLF4J MDC API](https://www.slf4j.org/apidocs/org/slf4j/MDC.html), [Logback MDC·thread pool](https://logback.qos.ch/manual/mdc.html).
#### 로그 수준 사용
적절한 로그 수준을 사용하여 로그의 중요도를 나타냅니다:
- **ERROR**: 애플리케이션 오류 및 예외
- **WARN**: 잠재적인 문제 또는 예상치 못한 상황
- **INFO**: 일반적인 애플리케이션 이벤트
- **DEBUG**: 디버깅에 유용한 상세 정보
- **TRACE**: 매우 상세한 디버깅 정보
#### 로그 보존 정책
승인된 운영·데이터 접근·보존 요구에 맞게 retention을 선택합니다. 보존 기간을 줄이면 기존 저장 데이터가 만료될 수 있으며 기간 값 자체가 규정 준수·불변 hold를 보장하지는 않습니다. 다음은 소유 CloudWatch log group의 단순 조회가 아니라 보존 기간을 변경합니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the log group Region}"
: "${LOG_GROUP:?Set the reviewed owned log group}"
: "${RETENTION_DAYS:?Choose a supported approved retention value such as 30}"
aws logs put-retention-policy --region "$AWS_REGION" \
--log-group-name "$LOG_GROUP" --retention-in-days "$RETENTION_DAYS"
```
S3 general purpose bucket의 PutBucketLifecycleConfiguration은 전체 lifecycle 구성을 교체합니다. 무관한 모든 rule, versioning·Object Lock 요구와 transition 최소 크기 설정을 확인하고 보존합니다. Expected account owner로 잘못된 bucket 선택을 줄입니다:
```bash
set -euo pipefail
: "${LOG_BUCKET:?Set the owned general purpose S3 bucket}"
: "${ACCOUNT_ID:?Set its expected AWS account ID}"
aws s3api get-bucket-versioning --bucket "$LOG_BUCKET" --expected-bucket-owner "$ACCOUNT_ID"
aws s3api get-bucket-lifecycle-configuration --bucket "$LOG_BUCKET" \
--expected-bucket-owner "$ACCOUNT_ID" --output json > current-lifecycle.json
```
서비스가 명확히 NoSuchLifecycleConfiguration을 반환한 경우 부재를 확인하고 신규 policy용 current-lifecycle.json을 {"Rules":[]}로 초기화합니다. AccessDenied·다른 조회 실패를 빈 policy로 취급하지 않습니다. 아래 예제를 추가하기 전에 기존 rule을 검토합니다.
설명용 90일 current-object policy를 log-lifecycle-example.json으로 저장합니다. 대상 log를 30일 후 Standard-IA로 전환하고 current object를 90일 후 만료시킵니다. 기존 60일째 Glacier 전환·90일째 만료 조합은 명목상 Glacier Flexible Retrieval 저장 기간이 30일뿐인데 최소 저장 요금 기간은 90일입니다. 따라서 이 90일 예제에서는 해당 전환을 제외합니다.
```json
{
"Rules": [
{
"ID": "example-logs-expiry-90d",
"Status": "Enabled",
"Filter": {
"Prefix": "logs/"
},
"Expiration": {
"Days": 90
}
},
{
"ID": "example-logs-standard-ia-30d",
"Status": "Enabled",
"Filter": {
"Prefix": "logs/"
},
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
}
]
}
]
}
```
이는 이미 존재하는 객체도 포함하는 object-age rule이며 동작은 비동기입니다. 실제 전환 시점·이른 수동 삭제·덮어쓰기로 최소 기간 요금이 발생할 수도 있습니다. Standard-IA의 최소 요금 기간은 30일, Glacier Flexible Retrieval은 90일, Deep Archive는 180일입니다. 장기 archive 일정은 이 조건과 실제 접근·retrieval 비용을 고려해 선택합니다. 해당 숫자는 서비스 규칙이지 실측 절감액이 아닙니다.
검토한 rule 추가 시 다음을 merge-log-lifecycle.py로 저장해 실행합니다. 기존 Rules를 보존하고 예제 ID 충돌을 거부하지만 겹치는 filter를 해결하거나 조합된 retention policy를 승인하는 것은 아닙니다:
```python
import json
with open("current-lifecycle.json") as stream:
current = json.load(stream)
with open("log-lifecycle-example.json") as stream:
example = json.load(stream)
if not isinstance(current.get("Rules"), list) or not isinstance(example.get("Rules"), list):
raise SystemExit("Both files must contain an explicitly reviewed Rules array")
existing_ids = {rule.get("ID") for rule in current["Rules"] if rule.get("ID")}
new_ids = [rule.get("ID") for rule in example["Rules"]]
if any(not name for name in new_ids) or len(new_ids) != len(set(new_ids)):
raise SystemExit("Example rules need distinct nonempty IDs")
if existing_ids.intersection(new_ids):
raise SystemExit("A rule ID already exists; review its owner and changes instead of replacing it")
merged = {"Rules": current["Rules"] + example["Rules"]}
if len(merged["Rules"]) > 1000:
raise SystemExit("The merged configuration exceeds the lifecycle rule limit")
with open("reviewed-full-lifecycle.json", "w") as stream:
json.dump(merged, stream, indent=2)
stream.write("\n")
print("Wrote a candidate preserving existing Rules; review overlaps, retention impact and minimum-size setting")
```
```bash
python3 merge-log-lifecycle.py
```
완전한 후보를 확인하고 최신 bucket 구성과 비교하며 이 API가 전체 교체 방식이므로 다른 writer와 조율합니다. 2024년 9월부터 새로 만들거나 수정한 구성은 기본적으로 128 KB 미만 객체의 전환을 막습니다. 수정하지 않은 이전 구성은 과거 동작을 유지할 수 있습니다. Size filter가 기본값을 재정의할 수 있고 이 설정은 보존한 다른 rule에도 영향을 줄 수 있습니다. 검토한 all_storage_classes_128K·varies_by_storage_class 값을 명시적으로 선택합니다. GET 응답은 사용 가능할 때 TransitionDefaultMinimumObjectSize를 제공합니다.
전체 구성과 기존 데이터에 미치는 영향을 검토한 뒤 완전한 file을 제출합니다:
```bash
set -euo pipefail
: "${LOG_BUCKET:?Set the reviewed owned bucket}"
: "${ACCOUNT_ID:?Set its expected AWS account ID}"
: "${TRANSITION_MINIMUM_OBJECT_SIZE:?Choose the reviewed minimum-size behavior}"
aws s3api put-bucket-lifecycle-configuration --bucket "$LOG_BUCKET" \
--expected-bucket-owner "$ACCOUNT_ID" \
--transition-default-minimum-object-size "$TRANSITION_MINIMUM_OBJECT_SIZE" \
--lifecycle-configuration file://reviewed-full-lifecycle.json
```
Versioning-enabled bucket에서 current-version 만료는 일반적으로 delete marker를 만들고 noncurrent version을 남깁니다. Noncurrent-version 만료·delete marker 정리·Object Lock·replication 제한은 각각 검토해야 하며 이 예제가 모든 version 삭제·bucket 비움을 보장하지 않습니다. 전환하지 않는 작은 객체도 expiration rule에 일치할 수 있습니다.
이번 감사에서 retention policy·객체 lifecycle 동작을 적용하지 않았습니다. 참고: [lifecycle 전체 교체 API](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLifecycleConfiguration.html), [전환 제한](https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-transition-general-considerations.html), [만료·versioning·최소 기간](https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-expire-general-considerations.html).
## 모니터링 및 로깅 모범 사례
EKS 클러스터의 모니터링 및 로깅을 효과적으로 구현하기 위한 모범 사례를 살펴보겠습니다.
### 모니터링 모범 사례
#### 다중 계층 모니터링
EKS 클러스터의 모든 계층을 모니터링합니다:
1. **인프라 계층**: EC2 인스턴스, VPC, 서브넷, 보안 그룹
2. **클러스터 계층**: 컨트롤 플레인, 노드, 파드, 서비스
3. **애플리케이션 계층**: 애플리케이션 성능, 사용자 경험
#### 골든 시그널 모니터링
Google의 SRE 책에서 제안하는 "4개의 골든 시그널"에 초점을 맞춥니다:
1. **지연 시간**: 요청을 처리하는 데 걸리는 시간
2. **트래픽**: 시스템에 대한 요청 수
3. **오류**: 실패한 요청의 비율
4. **포화도**: 시스템이 얼마나 "가득 찼는지"(예: 메모리 사용량)
#### 프로액티브 모니터링
추세·이상을 통해 진행 중인 위험을 파악합니다. 예측은 추정치이며 장애를 미리 감지한다고 보장하지 않습니다:
1. **추세 분석**: 시간에 따른 리소스 사용량 추세 분석
2. **이상 탐지**: 비정상적인 패턴 감지
3. **예측 분석**: 미래 리소스 요구사항 예측
#### 자동화된 스케일링
다음 HPA는 소유한 default/my-app Deployment, 동작하는 resource-metrics API와 적절한 CPU·메모리 request를 전제로 한 예제입니다. Utilization은 node 용량·container limit이 아닌 request 대비 비율입니다. 여러 metric의 권장 replica 중 가장 큰 값을 선택하며 metric 오류가 downscale을 막을 수 있습니다. HPA condition을 확인하고 해당 workload가 실제로 수평 확장의 이점을 얻는지 시험합니다:
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: app-hpa
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
```
#### 비즈니스 지표 모니터링
기술적 지표뿐만 아니라 비즈니스 지표도 모니터링합니다:
1. **사용자 활동**: 활성 사용자 수, 세션 길이
2. **트랜잭션**: 트랜잭션 수, 트랜잭션 값
3. **전환율**: 사용자 전환율, 이탈률
4. **SLI·SLO 달성**: 지표와 정의한 목표를 측정합니다. SLA는 결과·보상 조건 등을 포함할 수 있는 합의이며 내부 SLO 달성만으로 계약 준수가 입증되지는 않습니다.
### 로깅 모범 사례
#### 중앙 집중식 로깅
필요한 log를 승인된 목적지에 수집하고 접근·retention·수집 실패 감시를 명시합니다:
1. **일관된 형식**: 모든 애플리케이션에서 일관된 로그 형식 사용
2. **중앙 저장소**: CloudWatch Logs, OpenSearch, Loki와 같은 중앙 로그 저장소 사용
3. **로그 전송**: Fluent Bit, Fluentd와 같은 로그 전송 에이전트 사용
#### 컨텍스트 정보 포함
로그에 충분한 컨텍스트 정보를 포함합니다:
1. **타임스탬프**: 정확한 타임스탬프(ISO 8601 형식 권장)
2. **요청 ID**: 분산 시스템에서 요청 추적을 위한 고유 ID
3. **승인된 식별자**: 사용자·session ID도 민감하거나 식별 가능할 수 있습니다. 필요한 식별자만 적절한 접근·retention 범위로 포함합니다.
4. **서비스 정보**: 서비스 이름, 버전, 인스턴스 ID
5. **오류 세부 정보**: 오류 코드, 오류 메시지, 스택 트레이스
#### 로그 수준 필터링
환경에 따라 적절한 로그 수준을 설정합니다:
1. **개발 환경**: 조사 목적에 맞게 DEBUG·TRACE를 사용하고 test에서도 secret 필터를 적용합니다.
2. **스테이징 환경**: INFO 수준
3. **프로덕션 환경**: 유용한 INFO·WARN 범위를 선택하고 일시적인 DEBUG도 양·기간을 제한하며 민감정보를 필터링합니다.
#### 민감 정보 보호
로그에서 민감한 정보를 보호합니다:
1. **PII 마스킹**: 개인 식별 정보(PII) 마스킹
2. **자격 증명 제외**: 암호, 토큰, API 키와 같은 자격 증명 제외
3. **암호화**: 저장 및 전송 중인 로그 암호화
### 알림 모범 사례
#### 알림 우선순위 지정
알림의 우선순위를 지정하여 알림 피로를 줄입니다:
1. **P1(Critical)**: 즉각적인 조치가 필요한 심각한 문제
2. **P2(High)**: 서비스의 합의된 응답 시간 안에 처리할 중요한 문제이며 긴급도가 높다고 업무 시간까지 기다려도 되는 것은 아닙니다.
3. **P3(Medium)**: 계획된 유지 관리 중에 조치가 필요한 문제
4. **P4(Low)**: 정보 제공 목적의 알림
#### 알림 그룹화
아래 route 필드를 앞의 완전한 Alertmanager 구성에 병합하되 receiver·하위 route를 유지합니다. 모든 instance를 별도 group으로 나누기보다 서비스 context에 따라 그룹화합니다:
```yaml
route:
group_by: ['cluster', 'namespace', 'alertname']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
```
#### 실행 가능한 알림
알림에 문제 해결을 위한 충분한 정보를 포함합니다:
1. **명확한 제목**: 문제를 명확하게 설명하는 제목
2. **증거와 영향**: 관측한 증상·영향을 설명하고 확인 전의 추정 원인은 가설로 표시합니다.
3. **문제 해결 단계**: 문제 해결을 위한 단계 또는 링크
4. **관련 지표 및 로그**: 문제 진단에 도움이 되는 지표 및 로그 링크
#### 알림 테스트
알림 시스템을 정기적으로 테스트합니다:
1. **알림 시뮬레이션**: 테스트 알림 생성
2. **에스컬레이션 테스트**: 에스컬레이션 경로 테스트
3. **장애 주입**: 제어된 환경에서 장애 주입
### 비용 최적화 모범 사례
#### 로그 볼륨 최적화
로그 볼륨을 최적화하여 비용을 절감합니다:
1. **샘플링**: 필요한 포함 범위가 허용하는 경우에만 sampling하고 누락 범위를 기록하며 필요한 audit·error 증거를 보존합니다.
2. **필터링**: 불필요한 로그 필터링
3. **압축**: 로그 압축
#### 지표 카디널리티 관리
지표 카디널리티를 관리하여 비용을 절감합니다:
1. **Label 값 제한**: label 이름 수뿐 아니라 서로 다른 값과 label 조합의 수를 제한합니다.
2. **집계**: 상세 지표를 더 높은 수준으로 집계
3. **수집 해상도**: 필요한 신호를 보존하도록 scrape 간격·집계를 선택하며 낮은 수집 해상도가 peak를 숨길 수 있음을 고려합니다.
#### 스토리지 계층화
비용 효율적인 스토리지 계층화를 구현합니다:
1. **핫 스토리지**: 최근 로그 및 자주 액세스하는 로그
2. **웜 스토리지**: 덜 자주 액세스하는 로그
3. **콜드 스토리지**: 아카이브된 로그
## 문제 해결 및 디버깅
EKS 클러스터에서 발생하는 문제를 해결하고 디버깅하기 위한 다양한 기술을 살펴보겠습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-06-eks-monitoring-logging-5.html)
### 클러스터 문제 해결
#### 클러스터 상태 확인
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the owned cluster name}"
: "${AWS_REGION:?Set the cluster Region}"
LOG_GROUP="/aws/eks/$CLUSTER_NAME/cluster"
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query 'cluster.{ARN:arn,Status:status,Endpoint:endpoint,Logging:logging}'
aws logs describe-log-streams --region "$AWS_REGION" \
--log-group-name "$LOG_GROUP" --order-by LastEventTime --descending \
--max-items 10 --query 'logStreams[].{Name:logStreamName,LastEvent:lastEventTimestamp}'
```
반환된 실제 stream을 선택해 제한된 GetLogEvents 조회를 수행하거나 위 query를 사용합니다. 이 조회는 logging을 켜거나 끄지 않습니다. Log가 없다면 별도로 검토한 구성 절차를 사용하고 update 완료를 확인합니다.
#### 노드 문제 해결
Node condition을 해석하기 전에 Kubernetes context·실제 노드를 확인합니다. Node Ready·스케줄링 가능 여부·앱 상태는 서로 다른 신호입니다:
```bash
kubectl config current-context
kubectl get nodes
: "${NODE_NAME:?Choose the node to inspect}"
kubectl describe node "$NODE_NAME"
kubectl get node "$NODE_NAME" -o jsonpath='{.spec.providerID}{"\n"}{.status.nodeInfo.kubeletVersion}{"\n"}{.status.nodeInfo.containerRuntimeVersion}{"\n"}'
```
Managed node group은 실제 이름을 선택해 status·health를 확인합니다. 모든 Auto Mode·Fargate·self-managed node에 해당하는 것은 아닙니다:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the owned cluster}"
: "${AWS_REGION:?Set its Region}"
: "${NODEGROUP_NAME:?Choose the actual managed node group}"
aws eks describe-nodegroup --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--nodegroup-name "$NODEGROUP_NAME" \
--query 'nodegroup.{Status:status,Version:version,Health:health.issues,Resources:resources}'
```
노드 소유자의 승인된 접근·진단 경로를 사용합니다. SSH 가능 여부·로그인 사용자·systemd 도구는 OS·구성에 따라 달라지며 EKS 공통 기능이 아닙니다. Auto Mode·Fargate·Bottlerocket에는 해당 managed·platform 절차를 사용합니다. Systemd가 있는 일반 Linux EC2 노드의 이미 승인된 session에서는 제한된 kubelet log·공간·inode를 확인합니다:
```bash
sudo journalctl -u kubelet --since "30 minutes ago" --no-pager
df -h
df -i
```
접근 가능한 소유 instance의 EC2 console output은 부팅 문제에 도움이 되지만 완전한 kubelet·앱 log stream은 아닙니다. 모든 노드가 Docker를 실행한다고 가정하지 말고 보고된 runtime과 해당 진단 절차를 사용합니다.
#### 파드 문제 해결
```bash
set -euo pipefail
: "${NAMESPACE:?Set the owned namespace}"
: "${POD_NAME:?Choose the actual Pod}"
: "${CONTAINER_NAME:?Choose its application container}"
kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o wide
kubectl describe pod "$POD_NAME" -n "$NAMESPACE"
kubectl events -n "$NAMESPACE" --for="pod/$POD_NAME"
kubectl logs "$POD_NAME" -n "$NAMESPACE" -c "$CONTAINER_NAME" --tail=100
```
해당 container의 이전 종료 instance가 남아 있으면 해당 log도 확인합니다:
```bash
kubectl logs "$POD_NAME" -n "$NAMESPACE" -c "$CONTAINER_NAME" --previous --tail=100
```
Previous log 명령은 이전에 종료된 container instance가 남아 있을 때만 해당합니다. 임의의 과거 Pod·노드와 함께 사라진 log를 복구하지는 않습니다. Exit code·최근 종료 reason·probe·resource event를 함께 읽으며 restart count만으로 OOM·특정 원인을 단정하지 않습니다. 모든 앱 image에 /bin/bash 같은 shell이 있는 것은 아닙니다.
### 네트워킹 문제 해결
#### 서비스 문제 해결
```bash
set -euo pipefail
: "${NAMESPACE:?Set the service namespace}"
: "${SERVICE_NAME:?Choose the actual Service}"
kubectl get service "$SERVICE_NAME" -n "$NAMESPACE" -o yaml
kubectl get endpointslices.discovery.k8s.io -n "$NAMESPACE" \
-l "kubernetes.io/service-name=$SERVICE_NAME" -o yaml
kubectl get pods -n "$NAMESPACE" --show-labels
kubectl get networkpolicies -n "$NAMESPACE"
```
현재 endpoint 확인에는 EndpointSlice를 사용합니다. 기존 Endpoints API는 deprecated이며 모든 버전에서 제거된 것은 아닙니다. Selector·ready endpoint 주소·Service port·targetPort·protocol을 확인합니다. DNS 해석·endpoint readiness·네트워크 도달성은 별도 검사입니다.
로컬 port-forward로 선택한 Service port를 확인할 수 있지만 진단 경로를 이용하므로 일반 Pod-to-Service 트래픽·load balancer의 동작을 증명하지는 않습니다. 실제 Service port를 선택합니다:
```bash
set -euo pipefail
: "${NAMESPACE:?Set the service namespace}"
: "${SERVICE_NAME:?Choose the Service}"
: "${SERVICE_PORT:?Choose its service port number}"
kubectl port-forward --address 127.0.0.1 -n "$NAMESPACE" \
"svc/$SERVICE_NAME" "8080:$SERVICE_PORT"
```
#### NetworkPolicy와 진단 Context
Source egress·destination ingress, namespace·Pod selector, DNS 접근과 강제하는 CNI를 모두 확인합니다. Security group·route·network ACL·service-mesh policy·TLS도 추가 계층일 수 있습니다. 임의의 임시 Pod는 실패한 workload와 label·ServiceAccount·security group·sidecar가 다를 수 있습니다.
승인된 조사에서는 restricted profile과 호환되는 검토한 non-root image로 선택한 Pod에 ephemeral 진단 container를 추가할 수 있습니다. 오래된 BusyBox tag·검토하지 않은 latest 대신 DEBUG_IMAGE에 승인된 digest를 지정합니다:
```bash
set -euo pipefail
: "${NAMESPACE:?Set the owned Pod namespace}"
: "${POD_NAME:?Choose the Pod}"
: "${CONTAINER_NAME:?Choose the target container}"
: "${DEBUG_IMAGE:?Set the reviewed non-root diagnostic image digest}"
kubectl debug "$POD_NAME" -n "$NAMESPACE" -it \
--image="$DEBUG_IMAGE" --profile=restricted --target="$CONTAINER_NAME" -- sh
```
이는 ephemeral container를 추가하는 Pod 변경이며 해당 RBAC·admission 권한이 필요합니다. 기록은 Pod가 제거될 때까지 남으며 shell 종료가 ephemeral-container 항목을 삭제하지는 않습니다. Process namespace target은 runtime 지원에 따라 달라집니다. Image에 필요한 도구가 있어야 하고 profile이 금지하는 capability 없이 실행할 수 있어야 합니다. 거부된 진단을 우회하려고 privileged profile로 조용히 바꾸지 않습니다.
진단 shell 안에서도 대상 값을 지정합니다. Host shell 변수가 자동 전달되지는 않습니다. 선택한 image에 해당 도구가 있을 때 실행합니다:
```bash
: "${SERVICE_DNS:?Set the intended service DNS name inside this shell}"
: "${SERVICE_PORT:?Set its port inside this shell}"
nslookup "$SERVICE_DNS"
nc -zv "$SERVICE_DNS" "$SERVICE_PORT"
```
FQDN이 필요하면 실제 cluster DNS suffix를 사용합니다. TCP 연결 성공이 HTTP·앱 인증·TLS를 검증하지는 않습니다. Packet capture·node debug에는 별도로 검토한 privilege·context와 제한된 filter·기간·저장 범위가 필요하며 일반 unprivileged tcpdump 명령이 어디서나 동작하는 것은 아닙니다. 잘라낸 capture에도 자격 증명·사용자 데이터가 포함될 수 있습니다. 이번 감사에서는 진단 Pod·ephemeral container·packet capture·노드 session을 시작하지 않았습니다.
### 로깅 및 모니터링 문제 해결
#### Fluent Bit 문제 해결
이 장의 독립 collector render 결과는 logging namespace의 DaemonSet·ConfigMap 이름이 모두 eks-log-collector입니다:
```bash
kubectl get daemonset eks-log-collector -n logging
kubectl get pods -n logging -l app.kubernetes.io/instance=eks-log-collector
kubectl logs -n logging -l app.kubernetes.io/instance=eks-log-collector \
-c aws-for-fluent-bit --prefix --tail=100
kubectl get configmap eks-log-collector -n logging -o yaml
```
CloudWatch add-on이 수집을 소유하면 amazon-cloudwatch의 실제 리소스를 확인합니다. Mount한 구성·노드 file 접근·IRSA/Pod Identity 선택·RBAC·output 연결·buffer·backlog·drop 지표를 검증합니다. 무관한 kube-system/fluent-bit-config를 만들고 collector가 읽는다고 가정하지 않습니다. 구성·log를 공유하기 전에 민감정보를 확인합니다.
#### Prometheus 문제 해결
```bash
kubectl get prometheus,alertmanager -n monitoring
kubectl get pods,pvc -n monitoring
kubectl get servicemonitors,prometheusrules -n monitoring
kubectl port-forward --address 127.0.0.1 -n monitoring \
svc/monitoring-kube-prometheus-prometheus 9090:9090
```
Forward 실행 중 `http://127.0.0.1:9090/targets`를 확인합니다. 실제 Pod log·target error·인증서 신뢰·이름 있는 Service port·namespace·label selector·rule·최근 sample을 확인합니다. ServiceMonitor 객체만으로 Prometheus가 선택하거나 endpoint에 연결된다고 증명되지는 않습니다. AMP는 remote-write 실패·backlog와 대상 workspace를 별도로 확인합니다.
#### Grafana 문제 해결
```bash
kubectl get deployment monitoring-grafana -n monitoring
kubectl logs deployment/monitoring-grafana -n monitoring -c grafana --tail=100
kubectl port-forward --address 127.0.0.1 -n monitoring \
svc/monitoring-grafana 3000:80
```
`http://127.0.0.1:3000`에서 구성한 data source·자격 증명·시간 구간·query를 확인합니다. 로컬 stack의 Prometheus data-source UID는 prometheus이고 AMG·AMP는 별도의 plugin·IAM 조건을 가집니다. 빈 dashboard는 사용량 0이 아니라 잘못된 data source·label·시간 구간·수집 실패 때문일 수 있습니다.
### 일반적인 문제 및 해결 방법
#### ImagePullBackOff 오류
Pull error·image repository·digest·architecture·registry 권한·노드 네트워크 경로를 확인합니다. 적절한 private endpoint·route가 있으면 인터넷 접근이 항상 필요한 것은 아닙니다. ECR은 EC2 node 또는 Fargate Pod execution 신원과 repository policy를 검증합니다. 앱 IRSA·Pod Identity가 kubelet image pull 신원은 아닙니다.
Pull Secret을 사용하는 private registry는 승인된 자격 증명 절차로 보호된 독립 Docker auth JSON file을 준비합니다. Credential-helper 참조만 있는 file은 Kubernetes imagePullSecret으로 충분하지 않습니다. 신규 소유 Secret은 명령 인수에 password를 넣지 않고 file path를 전달합니다:
```bash
set -euo pipefail
: "${NAMESPACE:?Set the owned workload namespace}"
: "${DOCKER_CONFIG_JSON:?Set the protected self-contained registry auth JSON file}"
kubectl create secret generic regcred -n "$NAMESPACE" \
--type=kubernetes.io/dockerconfigjson \
--from-file=".dockerconfigjson=$DOCKER_CONFIG_JSON"
```
의도한 소유 workload에만 연결합니다. Deployment의 다음 strategic patch는 이름 있는 imagePullSecrets 항목을 기존 항목과 병합합니다. Pod template을 바꾸고 rollout을 유발하므로 소유자의 배포 절차를 먼저 확인합니다:
```bash
set -euo pipefail
: "${NAMESPACE:?Set the owned workload namespace}"
: "${DEPLOYMENT:?Set the owned Deployment name}"
kubectl patch deployment "$DEPLOYMENT" -n "$NAMESPACE" --type=strategic \
-p '{"spec":{"template":{"spec":{"imagePullSecrets":[{"name":"regcred"}]}}}}'
```
기존 Secret은 소유자의 rotation 절차를 사용하고 다른 registry 참조를 보존합니다. Namespace의 default ServiceAccount를 일괄 수정하는 해결책을 사용하지 않습니다. Secret 생성만으로 registry login·image 존재·pull 성공이 검증되지는 않습니다.
#### CrashLoopBackOff 오류
선택한 container의 현재·이전 log, exit code·종료 reason, startup·liveness probe, 구성·resource event를 연계합니다. OOMKilled·probe 실패·앱 종료는 다른 원인이며 시작 의존성 부재·잘못된 command도 restart를 유발할 수 있습니다. 일반 image에 도구가 없으면 위의 범위를 제한한 진단 절차를 사용하고 증거 수집 전에 restart·삭제하지 않습니다.
#### 노드 NotReady 상태
적절한 platform 접근 경로로 Ready status·reason, node·lease 최신 상태, pressure condition과 실제 runtime·kubelet 진단을 확인합니다. Disk byte·inode 고갈·네트워크·API 연결·runtime 실패는 각각 다른 조치가 필요합니다. NotReady 표시만으로 자동 복구 trigger나 안전한 drain을 추정하지 않습니다.
#### 서비스 연결 문제
위의 Service→EndpointSlice→Pod 경로를 따라가고 source·destination 네트워크 제어와 앱 listening port·protocol을 확인합니다. 대표 workload context에서 시험합니다. Port-forward·DNS 조회·TCP handshake 성공만으로 end-to-end 서비스 정상이 입증되지는 않습니다.
### 디버깅 도구
Kubernetes 상태·event는 kubectl, 해당 managed-service 상태는 AWS CLI, 연결은 승인된 context의 network 도구로 확인합니다. 증거를 보존하고 조회와 Pod 생성·template 변경·node drain·logging 변경을 구분합니다.
#### CloudWatch Logs Insights 결과 조회
StartQuery는 최종 결과가 아니라 비동기 query ID를 반환합니다. 다음 Bash·Python 예제는 macOS 전용 date -v 대신 UTC epoch second를 사용하고 제한된 횟수로 polling하며 Complete 이후에만 결과를 출력합니다:
```bash
set -euo pipefail
: "${AWS_REGION:?Set the log group Region}"
: "${LOG_GROUP:?Set the owned log group to query}"
python3 - "$LOG_GROUP" <<'PY'
import datetime
import json
import sys
end = datetime.datetime.now(datetime.timezone.utc)
request = {
"logGroupName": sys.argv[1],
"startTime": int((end - datetime.timedelta(hours=1)).timestamp()),
"endTime": int(end.timestamp()),
"queryString": "fields @timestamp, @message | sort @timestamp desc | limit 50",
}
with open("logs-query-request.json", "w") as stream:
json.dump(request, stream, indent=2)
PY
STARTED=$(aws logs start-query --region "$AWS_REGION" \
--cli-input-json file://logs-query-request.json --output json)
QUERY_ID=$(python3 - "$STARTED" <<'PY'
import json
import sys
value = json.loads(sys.argv[1]).get("queryId")
if not isinstance(value, str) or not value:
raise SystemExit("StartQuery did not return a query ID")
print(value)
PY
)
printf 'Started query %s\n' "$QUERY_ID" >&2
for ((query_attempt = 1; query_attempt <= 20; query_attempt++)); do
aws logs get-query-results --region "$AWS_REGION" --query-id "$QUERY_ID" \
--output json > logs-query-result.json
QUERY_STATE=$(python3 - <<'PY'
import json
with open("logs-query-result.json") as stream:
print(json.load(stream)["status"])
PY
)
case "$QUERY_STATE" in
Complete)
cat logs-query-result.json
exit 0
;;
Scheduled|Running)
if (( query_attempt < 20 )); then sleep 3; fi
;;
*)
printf 'Query %s ended with status %s; inspect logs-query-result.json\n' "$QUERY_ID" "$QUERY_STATE" >&2
exit 1
;;
esac
done
printf 'Query %s is still %s; inspect it again or stop it explicitly if no longer needed\n' "$QUERY_ID" "$QUERY_STATE" >&2
exit 2
```
Polling 한도는 서비스 timeout이 아닙니다. 계속 실행 중인 query는 active 상태로 남으므로 다시 확인하거나 사용을 중단하면 명시적으로 StopQuery를 사용합니다. Log group·시간 구간·필요한 IAM 권한을 제한하고 query 비용을 고려합니다. 이번 감사에서는 CloudWatch query·클러스터 debug 작업을 실행하지 않았습니다.
## 결론
이 문서에서는 Amazon EKS 클러스터의 모니터링 및 로깅을 위한 다양한 도구, 기술 및 모범 사례를 살펴보았습니다. 효과적인 모니터링 및 로깅 전략을 구현하면 클러스터의 상태를 지속적으로 파악하고, 문제를 조기에 감지하며, 문제가 발생했을 때 신속하게 대응할 수 있습니다.
주요 내용:
1. **모니터링 및 로깅 개요**: 모니터링과 로깅의 중요성 및 아키텍처
2. **EKS 컨트롤 플레인 로깅**: 컨트롤 플레인 로그 유형 및 활성화 방법
3. **컨테이너 로깅**: Fluent Bit, CloudWatch Container Insights를 사용한 컨테이너 로그 수집
4. **클러스터 모니터링**: CloudWatch, Prometheus, Grafana를 사용한 클러스터 모니터링
5. **알림 및 이벤트 관리**: CloudWatch 경보, Prometheus Alertmanager를 사용한 알림 구성
6. **로그 분석 및 시각화**: CloudWatch Logs Insights, OpenSearch, Grafana Loki를 사용한 로그 분석
7. **모니터링 및 로깅 모범 사례**: 효과적인 모니터링 및 로깅을 위한 모범 사례
8. **문제 해결 및 디버깅**: 일반적인 문제 및 해결 방법
EKS 클러스터의 모니터링 및 로깅은 지속적인 프로세스로, 클러스터 및 애플리케이션의 요구사항에 맞게 지속적으로 개선해야 합니다.
## 참고 자료
- [Amazon EKS 모니터링 모범 사례](https://docs.aws.amazon.com/eks/latest/userguide/eks-observe.html)
- [Amazon EKS 로깅 모범 사례](https://docs.aws.amazon.com/prescriptive-guidance/latest/amazon-eks-observability-best-practices/logging-best-practices.html)
- [Kubernetes 모니터링 아키텍처](https://kubernetes.io/docs/tasks/debug-application-cluster/resource-usage-monitoring/)
- [Prometheus 문서](https://prometheus.io/docs/introduction/overview/)
- [Grafana 문서](https://grafana.com/docs/grafana/latest/)
- [Fluent Bit 문서](https://docs.fluentbit.io/manual/)
- [Amazon CloudWatch 문서](https://docs.aws.amazon.com/cloudwatch/)
- [Amazon OpenSearch Service 문서](https://docs.aws.amazon.com/opensearch-service/)
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/06-eks-monitoring-logging-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/07-eks-cost-optimization
----------------------------------------
# Amazon EKS 비용 최적화
> **검증 범위**: 현재 AWS 요금·지원 문서 및 Kubernetes 1.36 매니페스트 스키마. 실제 EKS 버전과 애드온은 AWS 지원 카탈로그에서 선택합니다.
> **마지막 업데이트**: 2026년 9월 12일
Amazon EKS(Elastic Kubernetes Service)를 사용하면 컨테이너화된 애플리케이션을 쉽게 배포, 관리 및 확장할 수 있지만, 비용을 효과적으로 관리하는 것이 중요합니다. 이 문서에서는 EKS 클러스터의 비용을 최적화하기 위한 다양한 전략과 모범 사례를 다룹니다.
명령은 구성 예시이며 실제 배포 기록이나 실측 절감 결과가 아닙니다. 적용 전에 계정·리전·기존 리소스 소유자·워크로드 요구·지원 도구 버전을 확인합니다. 아래의 과거 가격 가정은 현재 서비스 동작과 구분합니다.
## 목차
1. [EKS 비용 구성 요소](#eks-비용-구성-요소)
2. [FinOps 원칙과 EKS](#finops-원칙과-eks)
3. [컴퓨팅 비용 최적화](#컴퓨팅-비용-최적화)
4. [스토리지 비용 최적화](#스토리지-비용-최적화)
5. [네트워킹 비용 최적화](#네트워킹-비용-최적화)
6. [리소스 관리 및 거버넌스](#리소스-관리-및-거버넌스)
7. [비용 모니터링 및 분석](#비용-모니터링-및-분석)
8. [비용 최적화 모범 사례](#비용-최적화-모범-사례)
## EKS 비용 구성 요소
Amazon EKS를 사용할 때 발생하는 비용은 다음과 같은 구성 요소로 이루어집니다:

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-07-eks-cost-optimization-0.html)
## FinOps 원칙과 EKS
FinOps는 기술의 비즈니스 가치를 높이기 위한 운영 프레임워크와 문화적 실천입니다. 엔지니어링·재무·비즈니스 팀이 적시 데이터 기반 의사결정과 재무 책임을 공유하며, 이 장은 그 접근을 EKS에 적용합니다.
### FinOps 프레임워크의 핵심 원칙
FinOps Foundation은 팀 간 협업, 비즈니스 가치 기반 의사결정, 사용량에 대한 책임, 적시에 접근 가능한 정확한 데이터, 중앙 조직의 지원, 클라우드 변동 비용 모델의 활용을 강조합니다. 아래 EKS 실천 방법은 이 원칙을 적용한 것이며 별도의 공식 6대 원칙 체계가 아닙니다.
### EKS에 FinOps 적용하기
1. **비용 가시성 확보**
- Kubernetes 네임스페이스, 레이블, 어노테이션을 사용한 비용 할당
- AWS Cost Explorer와 Kubecost 같은 도구를 통합하여 세부적인 비용 분석
- 팀별, 애플리케이션별, 환경별 비용 분석
2. **책임 공유 모델 구현**
- 팀별 비용 할당 및 보고
- 비용 최적화 목표 설정 및 추적
- 비용 절감 인센티브 제공
3. **지속적인 최적화 자동화**
- 자동 스케일링 정책 구현
- 스팟 인스턴스 활용 자동화
- 낭비 후보 감지 및 소유자·보존 요구 검토 후 리소스 제거
4. **비용 예측 및 계획**
- 워크로드 패턴 분석을 통한 비용 예측
- 예약 인스턴스 및 Savings Plans 활용
- 비용 이상 탐지 및 알림
### 최신 FinOps 도구 및 기술
1. **Kubecost**: Kubernetes 비용 모니터링 및 최적화 도구
2. **AWS Cost Anomaly Detection**: 비정상적인 비용 증가 감지
3. **Karpenter**: 효율적인 노드 프로비저닝 및 비용 최적화
4. **Goldilocks**: 리소스 요청 및 제한 최적화
5. **Vertical Pod Autoscaler**: 파드 리소스 요청 자동 조정
### EKS 클러스터 비용
공식 버전 지원 요금은 다음과 같습니다.
- **표준 지원**: 클러스터당 시간당 $0.10.
- **확장 지원**: 클러스터당 **시간당 총 $0.60**($0.10 기본 요금 + $0.50 확장 지원 요금). 별도의 “확장 클러스터”가 $0.10인 것이 아닙니다.
표준 지원은 EKS 버전 출시 후 14개월이며, 이후 확장 지원 12개월이 이어집니다. 정확한 버전별 날짜와 지원 정책은 [EKS 출시 일정](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html)에서 확인합니다. Provisioned Control Plane 등급, Auto Mode, Hybrid Nodes, EKS Capabilities에는 별도 요금이 추가될 수 있습니다. EC2/Fargate·스토리지·네트워크·관측 비용도 별개입니다. 개요 그림의 $0.10은 표준 버전 지원만 나타냅니다. [현재 EKS 요금](https://aws.amazon.com/eks/pricing/)을 확인하세요.
### 컴퓨팅 비용
EKS 클러스터에서 실행되는 워커 노드에 대한 비용:
- **EC2 인스턴스**: 노드 그룹에 사용되는 EC2 인스턴스 비용
- **Fargate**: 순간 사용률이 아닌 프로비저닝된 Pod vCPU/메모리 구성과 시간에 대한 요금 및 해당 스토리지 요금
### 스토리지 비용
EKS 클러스터에서 사용하는 스토리지에 대한 비용:
- **EBS 볼륨**: 영구 볼륨에 사용되는 EBS 볼륨 비용
- **EFS**: 공유 파일 시스템에 사용되는 EFS 비용
- **S3**: 객체 스토리지에 사용되는 S3 비용
### 네트워킹 비용
EKS 클러스터의 네트워킹과 관련된 비용:
- **데이터 전송**: 가용 영역 간·리전 간·인터넷 전송 중 과금 대상 경로의 비용. 방향과 서비스 경로에 따라 다릅니다.
- **로드 밸런서**: 서비스에 사용되는 로드 밸런서 비용
- **NAT 게이트웨이**: 프라이빗 서브넷의 아웃바운드 트래픽을 위한 NAT 게이트웨이 비용
### 기타 비용
- **CloudWatch**: 모니터링 및 로깅에 사용되는 CloudWatch 비용
- **ECR**: 컨테이너 이미지 저장에 사용되는 ECR 비용
- **기타 AWS 서비스**: EKS 클러스터와 함께 사용되는 기타 AWS 서비스 비용
## 컴퓨팅 비용 최적화
컴퓨팅 비용은 일반적으로 EKS 클러스터의 가장 큰 비용 구성 요소입니다. 다음과 같은 전략을 사용하여 컴퓨팅 비용을 최적화할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-07-eks-cost-optimization-2.html)
### 적절한 인스턴스 유형 선택
워크로드에 적합한 인스턴스 유형을 선택하는 것이 중요합니다:
#### 인스턴스 패밀리 선택
워크로드 특성에 따라 선택합니다. 아래 패밀리는 기존 세대의 예시이며 최신 세대 권장이 아닙니다. 리전 가용성, CPU 아키텍처, AMI, 가격을 확인하세요.
- **범용(T3, M5, M6)**: 균형 잡힌 컴퓨팅, 메모리 및 네트워킹 리소스가 필요한 워크로드
- **컴퓨팅 최적화(C5, C6)**: 고성능 프로세서가 필요한 컴퓨팅 집약적 워크로드
- **메모리 최적화(R5, R6, X1)**: 대규모 인메모리 데이터베이스, 캐시 등 메모리 집약적 워크로드
- **스토리지 최적화(I3, D2)**: 높은 디스크 I/O가 필요한 워크로드
- **가속 컴퓨팅(P3, G4, Inf1)**: GPU 또는 기계 학습 가속기가 필요한 워크로드
#### 인스턴스 크기 최적화
워크로드 요구사항에 맞는 적절한 인스턴스 크기를 선택합니다:
- 너무 큰 인스턴스는 리소스 낭비로 이어질 수 있습니다.
- 너무 작은 인스턴스는 성능 문제를 일으킬 수 있습니다.
- CloudWatch Container Insights 또는 Kubernetes 지표를 사용하여 실제 리소스 사용량을 모니터링하고 적절한 크기를 선택합니다.
#### 인스턴스 세대 고려
자체 워크로드 측정과 리전 가격으로 새 세대를 비교합니다. 다음은 과거 세대 전환 예시이며, x86(`i`)에서 Graviton(`g`)으로 옮길 때는 arm64 이미지와 의존성 호환성도 필요합니다:
- M5 대신 M6i 또는 M6g 사용
- C5 대신 C6i 또는 C6g 사용
- R5 대신 R6i 또는 R6g 사용
### 스팟 인스턴스 활용
AWS는 Spot의 온디맨드 대비 최대 90% 할인을 안내하지만 실제 가격·가용 용량·중단 가능성은 달라집니다. 상태 비저장이라는 이유만으로 중단을 허용할 수 있다고 판단하지 않습니다:
#### 스팟 인스턴스에 적합한 워크로드
- **스테이트리스 애플리케이션**: 상태를 저장하지 않는 애플리케이션
- **내결함성 애플리케이션**: 인스턴스 중단을 처리할 수 있는 애플리케이션
- **배치 처리 작업**: 중단되어도 다시 시작할 수 있는 작업
- **CI/CD 파이프라인**: 빌드 및 테스트 작업
#### 관리형 노드 그룹에서 스팟 인스턴스 사용
기존 클러스터에 관리형 노드 그룹을 생성하며 Cluster Autoscaler를 설치하는 명령은 아닙니다. 소유 구성에서 프라이빗 서브넷·IAM 역할·AMI·인스턴스 다양성을 검토합니다. 광범위한 노드 역할 애드온 권한 대신 컨트롤러 소유자를 통해 스케일링/IAM을 구성합니다.
```bash
eksctl create nodegroup \
--cluster my-cluster \
--name my-spot-ng \
--managed \
--node-type m5.large \
--nodes-min 2 \
--nodes-max 5 \
--spot
```
#### Karpenter를 사용한 스팟 인스턴스 프로비저닝
Karpenter/CRD 설치, 범위가 제한된 컨트롤러 IAM, 승인된 노드 역할, 검색 태그가 있는 서브넷/보안 그룹, 중단 처리 큐가 필요합니다. 현재 호환성 표에서 EKS 1.36은 Karpenter >=1.13이 필요합니다. 다음 리소스가 컨트롤러를 설치하지는 않습니다. 인스턴스 목록과 limits는 예시이며 금액 상한이 아닙니다. `al2023@latest`는 변하는 선택자로 drift/교체를 유발할 수 있으므로 실제 해석된 AMI를 확인하고 운영 변경에서는 검증한 별칭 버전 또는 AMI ID를 고정합니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: node.kubernetes.io/instance-type
operator: In
values: ["m5.large", "m5.xlarge", "m5.2xlarge"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: spot-class
limits:
cpu: 1000
memory: 1000Gi
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: spot-class
spec:
role: KarpenterNodeRole-my-cluster
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
```
#### 스팟 인스턴스 중단 처리
스팟 인스턴스 중단 처리를 위한 모범 사례:
1. **여러 인스턴스 유형 사용**: 다양한 인스턴스 유형을 사용하여 중단 위험 분산
2. **여러 가용 영역 사용**: 여러 가용 영역에 걸쳐 인스턴스 배포
3. **노드 집합별 중단 처리 소유자 하나 선택**: 관리형 노드 그룹은 Spot 중단/리밸런싱을 이미 처리합니다. Karpenter 노드는 기본 중단 처리 큐를 구성하고 같은 노드에 Node Termination Handler를 중복 설치하지 않습니다. 자체 관리 ASG는 [AWS Node Termination Handler](https://github.com/aws/aws-node-termination-handler)의 IMDS 또는 큐 모드를 명시적으로 선택하고 해당 권한/이벤트 연결을 구성할 수 있습니다.
4. **애플리케이션 복구 설계**: 대체 용량은 보장되지 않습니다. 정상 종료, 재시도/체크포인트, 장애 영역별 복제본을 중단 허용 시간에 맞춰 설계합니다. PDB로 EC2의 Spot 회수를 막을 수는 없습니다.
### Savings Plans 및 예약 인스턴스
예측 가능한 워크로드의 경우 Savings Plans 또는 예약 인스턴스를 사용하여 비용을 절감할 수 있습니다:
#### Compute Savings Plans
Compute Savings Plans는 1년 또는 3년 약정으로 온디맨드 요금보다 최대 66% 할인된 가격을 제공합니다:
- **유연성**: 인스턴스 패밀리, 크기, OS, 테넌시 및 리전에 관계없이 적용
- **EC2, Fargate 및 Lambda 포함**: 여러 컴퓨팅 서비스에 걸쳐 적용
#### EC2 Instance Savings Plans
EC2 Instance Savings Plans는 특정 리전의 인스턴스 패밀리에 대해 최대 72% 할인을 제공합니다:
- **중간 수준의 유연성**: 특정 리전 내에서 인스턴스 패밀리 내의 크기 및 OS에 걸쳐 적용
- **더 높은 할인율**: Compute Savings Plans보다 더 높은 할인율 제공
#### 예약 인스턴스
AWS가 현재 안내하는 RI 할인은 최대 **72%**입니다. Standard/Convertible RI의 변경·교환 규칙과 Regional/Zonal 범위는 다릅니다. Zonal RI는 해당 AZ의 용량 예약을 포함하지만 Regional RI는 포함하지 않습니다. RI는 일치하는 사용량에 적용되는 청구 혜택이며 Kubernetes 스케줄러나 최고 할인 보장이 아닙니다.
Savings Plans는 1년 또는 3년 동안 시간당 적격 지출을 약정하고, RI는 적격 인스턴스 사용을 약정합니다. 적정 크기 조정 후 안정적인 기준 사용량에 맞춰 약정하고 미사용 약정 위험을 검토합니다. Spot 사용에 추가 할인을 중첩하지 않으며, 광고된 최대 할인율은 이 클러스터의 실측 절감률이 아닙니다.
### Fargate vs EC2 비용 비교
Fargate와 EC2 중에서 선택할 때 비용을 고려해야 합니다:
#### Fargate 장점
- **운영 오버헤드 감소**: 노드 관리 불필요
- **정확한 리소스 프로비저닝**: 파드 수준에서 리소스 할당
- **별도로 관리하는 유휴 워커 노드 없음**: 다만 Pod에 프로비저닝된 용량·시간과 반올림·최소 과금 기준에 따라 비용이 발생
#### EC2 장점
- **대규모 워크로드에 더 비용 효율적**: 높은 리소스 사용률의 경우
- **더 많은 인스턴스 유형 옵션**: 다양한 워크로드에 맞는 인스턴스 유형 선택 가능
- **스팟 인스턴스 지원**: 스팟 인스턴스를 사용하여 추가 비용 절감 가능
#### 비용 비교 예시
**과거 설명용 가정(가격 출처/리전 기록 없음, 현재 견적·벤치마크 아님)**: 2 vCPU와 4 GB 메모리를 요청하는 애플리케이션. 원 단가와 산술 계산은 아래에 보존하지만 EKS 예약량과 노드 오버헤드가 빠져 있습니다.
**Fargate 비용**:
- vCPU: $0.04048 per vCPU-hour × 2 = $0.08096 per hour
- 메모리: $0.004445 per GB-hour × 4 = $0.01778 per hour
- 총 비용: $0.09874 per hour
**EC2 비용(t3.medium)**:
- 온디맨드: $0.0416 per hour
- 스팟: ~$0.0125 per hour (70% 할인 가정)
**용량 산정 수정:** EKS Fargate는 Kubernetes 구성 요소용 256 MB를 추가하고 지원 구성으로 올림합니다. 위 가정의 2 vCPU/4 GB 요청에는 **2 vCPU/5 GB** 프로비저닝이 필요합니다. 같은 과거 단가를 쓰면 `2 × 0.04048 + 5 × 0.004445 = 0.103185 USD/hour`이며 새 견적이 아닌 산술 계산입니다. Linux Fargate 과금은 이미지 다운로드부터 시작하며 최소 1분입니다. EC2 `t3.medium`의 명목 용량은 2 vCPU/4 GiB이지만 OS/Kubernetes/DaemonSet 예약 후 allocatable은 더 작아 해당 Pod가 들어간다고 가정할 수 없습니다. 지속 CPU 사용에는 T3 크레딧 요금도 발생할 수 있습니다. 실제 스케줄링 가능한 용량 계획에서 EBS·네트워크·클러스터 요금·활용도·운영 비용을 함께 비교해야 하며, 이 표로 동등 서비스의 우열을 결정할 수 없습니다.
### 자동 스케일링 최적화
효과적인 자동 스케일링 전략을 구현하여 비용을 최적화할 수 있습니다:
#### Cluster Autoscaler
Cluster Autoscaler는 스케줄되지 못한 Pod를 위해 ASG를 확장하고, 단순 CPU 실측 저하가 아닌 requests 및 재배치/disruption 조건을 검토해 노드를 줄입니다. Kubernetes 마이너 버전을 클러스터와 맞추고 실제 ASG에 검색 태그를 구성하며 전용 워크로드 IAM 역할을 사용합니다. EKS/노드 그룹 태그가 모든 하위 리소스에 자동 전파되지는 않습니다. [업스트림 AWS 설정](https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler/cloudprovider/aws)과 [EKS 권고](https://docs.aws.amazon.com/eks/latest/best-practices/cas.html)에 따라 소유 릴리스를 준비합니다.
다음 chart values는 CLI 인수가 됩니다. 임의의 `CLUSTER_AUTOSCALER_*` 환경 변수로는 설정되지 않으며, 수정하지 않은 `master` 예제 적용만으로 클러스터별 IAM/검색 설정이 완성되지 않습니다. 아래 시간은 조정 예시이며 짧게 줄이면 반복 확장/축소가 늘 수 있습니다.
```yaml
# Values fragment for the upstream cluster-autoscaler Helm chart.
# Merge into the existing release's reviewed values, including workload IAM.
autoDiscovery:
clusterName: my-cluster
awsRegion: us-west-2
extraArgs:
expander: least-waste
scale-down-delay-after-add: 10m
scale-down-unneeded-time: 10m
max-node-provision-time: 15m
```
#### Karpenter
Karpenter는 고정 ASG 크기를 바꾸는 대신 NodePool/EC2NodeClass 제약에 따라 프로비저닝합니다. 지연과 비용은 워크로드·가용 용량에 따라 달라지며 앞의 컨트롤러/IAM/AMI 전제 조건이 여기에도 적용됩니다. Cluster Autoscaler가 관리하는 ASG와 노드 소유 범위를 분리합니다:
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: node.kubernetes.io/instance-type
operator: In
values: ["m5.large", "m5.xlarge", "m5.2xlarge"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default-class
limits:
cpu: 1000
memory: 1000Gi
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default-class
spec:
role: KarpenterNodeRole-my-cluster
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: my-cluster
```
Karpenter 비용 최적화 설정:
- **disruption.consolidateAfter**: Pod 추가/제거 후 통합 검토까지의 지연으로, 실제 동작은 정책과 disruption 검사에 따름 (예: `30s`, 기존 `ttlSecondsAfterEmpty`를 대체)
- **disruption.consolidationPolicy**: 노드 통합 정책 — `WhenEmpty`(빈 노드만 정리) 또는 `WhenEmptyOrUnderutilized`(저사용 노드까지 통합, 기존 `consolidation.enabled: true`에 해당)
- **template.spec.requirements** (`node.kubernetes.io/instance-type`): 비용 효율적인 인스턴스 유형 지정
#### Horizontal Pod Autoscaler (HPA)
HPA는 CPU 사용률 또는 사용자 정의 지표를 기반으로 파드 수를 자동으로 조정합니다:
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
```
CPU/메모리 utilization 목표는 requests 대비 백분율이며 resource metrics API와 대상 컨테이너의 requests가 필요합니다. 여러 메트릭 중 가장 큰 희망 복제본 수를 선택하고, 메트릭 누락은 축소를 막을 수 있습니다. 복제본을 늘려도 메모리가 줄지 않을 수 있어 애플리케이션 동작을 검증해야 합니다. EKS의 controller-manager 플래그는 AWS가 관리하므로 전역 플래그 수정 대신 HPA별 `spec.behavior.scaleDown.stabilizationWindowSeconds`를 설정합니다. HPA가 CPU/메모리 requests를 분모로 사용할 때 VPA로 같은 requests를 동시에 자동 변경하지 않습니다.
#### Vertical Pod Autoscaler (VPA)
VPA는 파드의 CPU 및 메모리 요청을 자동으로 조정하여 리소스 사용률을 최적화합니다:
```yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: app-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: app
updatePolicy:
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: '*'
minAllowed:
cpu: 50m
memory: 100Mi
maxAllowed:
cpu: 1
memory: 1Gi
```
여기서는 위 HPA와 충돌하지 않도록 자동 변경 없는 **Off** 모드로 권고를 수집합니다. 먼저 VPA 구성 요소/CRD와 메트릭 의존성을 설치해야 합니다.
- **Off**: 권고만 제공.
- **Initial**: 새 Pod 생성 시 admission에서 requests 설정.
- **Recreate**: updater가 eviction 정책/PDB에 따라 Pod를 축출하고 컨트롤러가 권고 리소스로 재생성할 수 있음.
- **Auto**: Recreate의 deprecated 별칭이므로 새 구성은 명시적 모드를 선택.
- In-place 모드는 호환 VPA/Kubernetes 릴리스와 문서화된 feature gate가 필요합니다. 선택한 모드의 동작을 확인하세요. `InPlaceOrRecreate`는 eviction으로 fallback할 수 있지만 `InPlace`는 그런 재생성 fallback을 사용하지 않습니다. 자동 변경 전 상·하한, disruption, 피크 수요를 검토합니다.
## 스토리지 비용 최적화
스토리지는 EKS 클러스터의 중요한 비용 구성 요소입니다. 다음과 같은 전략을 사용하여 스토리지 비용을 최적화할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-07-eks-cost-optimization-3.html)
### EBS 볼륨 최적화
EBS 볼륨은 EKS 클러스터의 영구 스토리지에 주로 사용됩니다:
#### 적절한 볼륨 유형 선택
워크로드에 적합한 EBS 볼륨 유형을 선택합니다:
- **gp3**: 대부분의 워크로드에 권장되는 범용 SSD
- **gp2**: 이전 세대 범용 SSD, gp3로 마이그레이션 권장
- **io1/io2**: 고성능 워크로드를 위한 프로비저닝된 IOPS SSD
- **st1**: 처리량 집약적 워크로드를 위한 처리량 최적화 HDD
- **sc1**: 자주 액세스하지 않는 데이터를 위한 콜드 HDD
gp3는 용량·IOPS·처리량 요금을 분리하며, 기본 3,000 IOPS가 모든 gp2 볼륨보다 높은 것은 아닙니다. 아래 한도는 일반 AWS 리전 볼륨 기준으로 용량/IOPS 비율과 인스턴스 한도에 따릅니다. Outposts 한도는 다릅니다. $0.08/$0.10 스토리지 단가는 리전/기준일 기록이 없는 설명용 가정으로 보존하며, 추가 IOPS/처리량을 포함한 현재 견적을 확인해야 합니다.
| 볼륨 유형 | 기본 IOPS | 최대 IOPS | 기본 처리량 | 최대 처리량 | GB당 가격 |
|----------|----------|----------|------------|------------|---------|
| gp3 | 3,000 | 80,000 | 125 MiB/s | 2,000 MiB/s | $0.08/GB-월 (설명용) |
| gp2 | 3 IOPS/GiB, 최소 100; 해당 소형 볼륨은 버스트 가능 | 16,000 | 용량/I/O에 따라 다름 | 250 MiB/s | $0.10/GB-월 (설명용) |
#### gp3로 마이그레이션
이 클래스는 표준 EBS CSI 드라이버(`ebs.csi.aws.com`)로 새 gp3 볼륨을 생성하며 기존 볼륨을 마이그레이션하거나 클러스터 기본 클래스를 바꾸지 않습니다. Auto Mode는 다른 provisioner(`ebs.csi.eks.amazonaws.com`)와 그에 맞는 노드/스토리지 전환 절차를 사용합니다. 드라이버·IAM/KMS·기존 클래스 소유권·토폴로지를 확인하세요. `Retain`은 해제된 스토리지를 소유자 검토용으로 남겨 요금이 계속 발생할 수 있습니다:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: "true"
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
```
기존 PVC를 gp3로 마이그레이션:
1. 설치된 CSI snapshot 구성 요소로 애플리케이션 일관성 백업/스냅샷을 만들고 준비 상태와 복원 권한을 확인합니다.
2. gp3 클래스로 **새** PVC를 복원합니다. 바인딩된 PVC의 StorageClass를 제자리에서 바꾸는 방식이 아닙니다.
3. 복원 검증 후 계획된 전환에서 워크로드를 옮기고, 복구가 확인될 때까지 원본을 보존합니다.
지원 구성에서는 EBS Elastic Volumes의 볼륨 유형 변경도 대안입니다. CSI/IaC 소유자와 조율하여 구성 drift를 피합니다. [스토리지 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/04-eks-storage-part1.md)를 참고하세요.
#### 볼륨 크기 최적화
필요한 크기의 볼륨만 프로비저닝합니다:
- 과도하게 프로비저닝된 볼륨은 불필요한 비용을 발생시킵니다.
- 파일 시스템 사용량을 관측하고 지원 볼륨을 확장합니다. EBS 볼륨과 Kubernetes PVC는 제자리 축소가 불가능하며, 용량을 줄이려면 더 작은 새 볼륨과 애플리케이션을 고려한 데이터 이전이 필요합니다.
- `allowVolumeExpansion`은 지원되는 확장 요청을 허용할 뿐 사용량 감시나 자동 크기 조정을 수행하지 않습니다. 자동 확장에는 별도 컨트롤러·상한·실패 처리가 필요합니다.
#### 볼륨 수명 주기 관리
불필요한 볼륨을 식별하고 제거합니다:
- 사용되지 않는 PVC 및 PV 정기적으로 검토
- Pod 종료만으로 PVC/PV를 삭제 대상으로 보지 않습니다. claim/볼륨 삭제 전 StatefulSet 보존 정책, 대기 소비자, 백업, 소유권을 확인합니다.
- 적절한 PV 재확보 정책 설정(Delete 또는 Retain)
### EFS 비용 최적화
EFS는 여러 노드에서 공유 액세스가 필요한 워크로드에 유용합니다:
#### 적절한 처리량 모드 선택
워크로드에 적합한 EFS 처리량 모드를 선택합니다:
- **버스팅 처리량**: 간헐적인 액세스 패턴에 적합
- **프로비저닝된 처리량**: 예측 가능한 성능이 필요한 워크로드에 적합
- **탄력적 처리량**: 변동이 심한 워크로드에 적합
#### 수명 주기 관리
EFS lifecycle policy는 대상 파일을 IA/Archive로 옮기고 선택적으로 접근 시 기본 스토리지로 되돌릴 수 있습니다. 접근 요금, 최소 과금 크기/기간, 처리량 모드, 접근 패턴에 따라 절감 효과가 달라집니다. 다음은 검토할 기존 정책 배열을 내보내는 예시입니다. 그 배열에서 IA 규칙을 수정하되 필요한 Archive/기본 스토리지 복귀 항목을 보존한 뒤 원하는 전체 구성을 제출합니다:
```bash
aws efs describe-lifecycle-configuration \
--file-system-id fs-1234567890abcdef0 \
--query LifecyclePolicies --output json > efs-lifecycle-policies.json
# Edit the exported array; an IA rule is {"TransitionToIA":"AFTER_30_DAYS"}.
# Preserve required Archive/return-to-primary rules and review the complete array.
aws efs put-lifecycle-configuration \
--file-system-id fs-1234567890abcdef0 \
--lifecycle-policies file://efs-lifecycle-policies.json
```
#### 액세스 패턴 최적화
EFS 액세스 패턴을 최적화하여 비용을 절감합니다:
- 작은 파일보다 큰 파일 사용
- 메타데이터 작업 최소화
- 순차적 액세스 패턴 사용
### S3 비용 최적화
S3는 로그, 백업, 정적 콘텐츠 등을 저장하는 데 비용 효율적인 옵션입니다:
#### 스토리지 클래스 최적화
워크로드에 적합한 S3 스토리지 클래스를 선택합니다:
- **S3 Standard**: 자주 액세스하는 데이터
- **S3 Intelligent-Tiering**: 액세스 패턴이 변하는 데이터
- **S3 Standard-IA**: 자주 액세스하지 않는 데이터
- **S3 One Zone-IA**: 자주 액세스하지 않고 중요하지 않은 데이터
- **S3 Glacier**: 아카이브 데이터
#### 수명 주기 정책
이 설명용 규칙은 현재 객체를 30/90일에 전환하고 365일에 만료시킵니다. 복구 지연, 보존/Object Lock 요구, 전환/요청 요금, 최소 보관 기간을 확인하세요. 신규/수정 구성은 기본적으로 128 KB 미만 객체를 전환에서 제외합니다. 버전 관리 버킷은 noncurrent version을 별도로 관리해야 하며, 현재 버전 만료가 delete marker만 만들고 과거 데이터는 계속 과금될 수 있습니다. put-bucket-lifecycle-configuration은 버킷 lifecycle 전체를 교체하므로 관련 없는 규칙을 병합·보존합니다:
```json
{
"Rules": [
{
"ID": "Move to IA after 30 days, Glacier after 90 days",
"Status": "Enabled",
"Filter": {"Prefix": "logs/"},
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
},
{
"Days": 90,
"StorageClass": "GLACIER"
}
],
"Expiration": {
"Days": 365
}
}
]
}
```
#### S3 요청 최적화
S3 요청 비용을 최적화합니다:
- 작은 객체를 더 큰 객체로 결합
- 불필요한 LIST 작업 최소화
- 멀티파트 업로드는 전송/재시도에 도움이 되지만 요청 및 미완료 파트 보관 요금이 발생하므로 중단된 업로드 정리를 구성합니다. Transfer Acceleration은 추가 요금이 발생할 수 있는 지연/처리량 옵션이며 요청 비용을 자동으로 줄이지 않습니다.
## 네트워킹 비용 최적화
네트워킹 비용은 특히 대규모 데이터 전송이 있는 경우 상당할 수 있습니다. 다음과 같은 전략을 사용하여 네트워킹 비용을 최적화할 수 있습니다.
### 데이터 전송 최적화
#### 리전 내 통신 활용
가능한 한 동일한 리전 내에서 통신하여 리전 간 데이터 전송 비용을 줄입니다:
- EKS 클러스터와 관련 AWS 서비스를 동일한 리전에 배치
- 여러 리전에 걸쳐 있는 경우 리전 간 데이터 전송 최소화
#### 가용 영역 인식 라우팅
가용 영역 간 데이터 전송 비용을 줄이기 위해 가용 영역 인식 라우팅을 구현합니다:
- 토폴로지 인식 서비스 라우팅 사용
- 다중 AZ 가용성을 유지하면서 지원되는 로컬 엔드포인트를 선호합니다. `trafficDistribution: PreferSameZone`은 Kubernetes 1.35+에서 stable이며 클러스터/프록시 구현을 확인해야 합니다. 로컬 엔드포인트가 없을 때 fallback하는 선호 설정으로 AZ 간 트래픽 0을 보장하지 않습니다. 배치 affinity만으로 Service 트래픽이 라우팅되지는 않습니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
trafficDistribution: PreferSameZone
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
type: ClusterIP
```
#### 압축 사용
데이터 전송 전에 압축을 사용하여 전송되는 데이터 양을 줄입니다:
- API 응답 압축
- 로그 및 지표 압축
- 이미지 및 정적 자산 최적화
### 로드 밸런서 최적화
#### 적절한 로드 밸런서 유형 선택
워크로드에 적합한 로드 밸런서 유형을 선택합니다:
- **Network Load Balancer(NLB)**: TCP/UDP 트래픽, 낮은 지연 시간이 필요한 경우
- **Application Load Balancer(ALB)**: HTTP/HTTPS 트래픽, 경로 기반 라우팅이 필요한 경우
- **Classic Load Balancer(CLB)**: 레거시 워크로드
#### 로드 밸런서 공유
여러 서비스에서 로드 밸런서를 공유하여 비용을 절감합니다:
- AWS Load Balancer Controller 사용
- 인그레스 리소스를 사용하여 여러 서비스 노출
표준 AWS Load Balancer Controller는 [네트워킹 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/03-eks-networking-part1.md)에 따라 chart·IAM/service account·서브넷·보안 그룹을 구성하고 기존 컨트롤러 소유자를 재사용합니다. Auto Mode의 기본 통합은 다르므로 이 설치/클래스 설정을 그대로 적용하지 않습니다. 아래 두 backend Service는 Ingress와 같은 namespace에 있고 80번 포트를 노출해야 합니다. IP target 모드는 도달 가능한 Pod IP가 필요합니다. HTTP 라우팅 예시이므로 인터넷 운영 환경에는 검토한 TLS·DNS·접근 제어도 필요합니다.
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shared-ingress
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
spec:
ingressClassName: alb
rules:
- host: service1.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service1
port:
number: 80
- host: service2.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service2
port:
number: 80
```
#### 유휴 로드 밸런서 제거
사용되지 않는 로드 밸런서를 식별하고 제거합니다:
- 트래픽이 없는 로드 밸런서 모니터링
- 테스트 또는 개발 환경의 불필요한 로드 밸런서 제거
### NAT 게이트웨이 최적화
NAT 게이트웨이는 시간당 요금과 데이터 처리 요금이 부과됩니다:
#### NAT 게이트웨이 공유
여러 서브넷에서 NAT 게이트웨이를 공유하여 비용을 절감합니다:
- Zonal NAT gateway는 같은 AZ의 프라이빗 서브넷이 공유할 수 있습니다. AZ별 가용성과 고정 시간 요금을 함께 평가합니다.
- 하나의 zonal gateway를 여러 AZ에서 공유하면 AZ 간 의존성과 전송 요금이 추가될 수 있어 항상 저렴한 고가용성 설계가 아닙니다. 현재 regional NAT 옵션은 해당 가용성/가격 모델로 별도 평가합니다.
#### VPC 엔드포인트 사용
실제 트래픽의 NAT 경로와 endpoint 비용을 비교합니다. S3/DynamoDB gateway endpoint는 추가 endpoint 시간/데이터 처리 요금이 없지만 ECR/Logs/STS 같은 interface endpoint는 별도 요금과 DNS/보안 그룹 구성이 필요합니다. 다음은 검토한 기존 라우팅 테이블에 gateway endpoint를 생성하는 예시이며 적절한 endpoint policy를 적용해야 합니다. ECR 이미지 pull은 일반적으로 `ecr.api`/`ecr.dkr` interface endpoint와 S3 접근이 필요하며 ECR API endpoint 하나로 완성되지 않습니다:
```bash
# S3 VPC 엔드포인트 생성
aws ec2 create-vpc-endpoint \
--vpc-id vpc-1234567890abcdef0 \
--service-name com.amazonaws.us-west-2.s3 \
--route-table-ids rtb-1234567890abcdef0
# DynamoDB VPC 엔드포인트 생성
aws ec2 create-vpc-endpoint \
--vpc-id vpc-1234567890abcdef0 \
--service-name com.amazonaws.us-west-2.dynamodb \
--route-table-ids rtb-1234567890abcdef0
```
일반적으로 사용되는 VPC 엔드포인트:
- S3
- DynamoDB
- ECR
- CloudWatch Logs
- STS
#### 아웃바운드 트래픽 최적화
NAT 게이트웨이를 통과하는 아웃바운드 트래픽을 최적화합니다:
- 불필요한 외부 API 호출 최소화
- 예약은 경합을 줄일 수 있지만 일반 NAT/데이터 전송 요금에 보편적인 오프 피크 할인이 있는 것은 아닙니다. 과금 바이트나 프로비저닝 시간을 줄여야 합니다.
- 데이터 압축 사용
## 리소스 관리 및 거버넌스
효과적인 리소스 관리 및 거버넌스는 EKS 클러스터의 비용을 제어하는 데 중요합니다. 다음과 같은 전략을 사용하여 리소스를 효과적으로 관리할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-07-eks-cost-optimization-5.html)
### 리소스 요청 및 제한 최적화
#### 적절한 리소스 요청 설정
애플리케이션의 실제 리소스 요구사항에 맞는 리소스 요청을 설정합니다:
- 너무 높은 요청은 리소스 낭비로 이어집니다.
- 너무 낮은 요청은 성능 문제를 일으킬 수 있습니다.
- VPA(Vertical Pod Autoscaler)를 사용하여 리소스 요청 최적화
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: app:latest
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
```
#### 리소스 제한 설정
리소스 제한을 설정하여 컨테이너가 과도한 리소스를 사용하지 않도록 합니다:
- CPU limit은 보통 throttling으로 적용되며 너무 낮으면 노드에 여유 CPU가 있어도 지연이 늘 수 있습니다.
- 메모리 limit은 사후적으로 강제되어 OOM 종료를 일으킬 수 있으며 사용량이 순간적으로 값을 넘지 않는다는 보장이 아닙니다. 보편적인 비율 대신 워크로드 측정으로 requests/limits를 정합니다.
#### QoS 클래스 이해
Kubernetes QoS(Quality of Service) 클래스를 이해하고 활용합니다:
여기서 사용하는 컨테이너 수준 리소스 구성 기준은 다음과 같습니다.
- **Guaranteed**: 모든 컨테이너에 0보다 큰 CPU·메모리 requests가 있고 각각 해당 limit과 같음.
- **Burstable**: 일부 CPU/메모리 request 또는 limit이 있지만 Guaranteed 조건은 충족하지 않음.
- **BestEffort**: 모든 컨테이너에 CPU/메모리 request와 limit이 없음.
QoS는 Pod Priority가 아니며 절대적인 축출 순서를 보장하지 않습니다. 노드 압력에서는 kubelet이 requests 초과 여부, Pod Priority, 상대적 초과량을 고려합니다. CPU/메모리 QoS로 ephemeral-storage requests를 분류하지 않으므로 디스크 압력 축출도 다릅니다. Pod 수준 리소스를 사용할 때는 [현재 QoS 규칙](https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/)을 확인합니다.
### 네임스페이스 및 리소스 쿼터
#### 네임스페이스 기반 분리
네임스페이스를 사용하여 리소스를 논리적으로 분리합니다:
- 팀, 환경 또는 애플리케이션별로 네임스페이스 생성
- 네임스페이스별 리소스 사용량 모니터링
#### 리소스 쿼터 설정
ResourceQuota는 기존 namespace에서 admission되는 requests/limits와 객체 수를 제한하며 지출 상한이나 런타임 CPU 계량기가 아닙니다. `team-a` namespace를 먼저 생성하세요. CPU/메모리 quota는 새 컨테이너의 requests/limits를 요구할 수 있어 LimitRange 기본값을 워크로드와 조율해야 합니다:
```yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: team-a
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
pods: "20"
services: "10"
persistentvolumeclaims: "5"
```
#### LimitRange 설정
LimitRange를 사용하여 네임스페이스 내의 컨테이너에 대한 기본 리소스 제한을 설정합니다:
```yaml
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: team-a
spec:
limits:
- default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 256Mi
type: Container
```
### 비용 할당 및 태깅
#### 리소스 태깅
AWS 리소스에 태그를 적용하고 청구 담당자가 적격 청구 키를 활성화합니다. EKS 클러스터 태그가 EC2·ASG·EBS·로드 밸런서에 자동 전파되지는 않습니다. 실제 태그 범위와 청구 처리 지연을 확인해야 하며, 다음 개별 태그 예제로 전체 클러스터 비용 귀속이 완성되지는 않습니다:
- 팀, 프로젝트, 환경, 비용 센터 등으로 태그 지정
- 일관된 태깅 전략 구현
```bash
# EKS 클러스터에 태그 지정
aws eks tag-resource \
--resource-arn arn:aws:eks:us-west-2:123456789012:cluster/my-cluster \
--tags Team=DevOps,Environment=Production,CostCenter=123456
# EC2 인스턴스에 태그 지정
aws ec2 create-tags \
--resources i-1234567890abcdef0 \
--tags Key=Team,Value=DevOps Key=Environment,Value=Production Key=CostCenter,Value=123456
```
#### Kubernetes 레이블 및 주석
Kubernetes 레이블/어노테이션은 별도 메타데이터 체계입니다. 비용 도구는 선택한 워크로드 레이블로 그룹화할 수 있으며, Pod별 그룹화에 필요한 레이블은 Deployment의 Pod template에도 있어야 합니다. Namespace 레이블은 Pod나 AWS 리소스에 자동 상속되지 않습니다. AWS split cost allocation과 EKS Pod 비용 생성 속성은 별도 청구 설정이 필요합니다:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
labels:
app: app
team: team-a
environment: production
cost-center: "123456"
spec:
replicas: 3
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
team: team-a
environment: production
cost-center: "123456"
spec:
containers:
- name: app
image: app:latest
```
#### Kubecost 사용
Kubecost를 사용하여 Kubernetes 리소스 비용을 추적하고 최적화합니다:
아래 설치 절차를 따르며 Kubecost/OpenCost 수집기를 중복 설치하지 않고 소유 배포 하나를 선택합니다. 리소스 기반 할당은 청구 데이터 및 합의한 공용 비용 정책과 대조하기 전까지 추정치입니다.
Kubecost는 다음과 같은 기능을 제공합니다:
- 네임스페이스, 배포, 서비스, 레이블별 비용 분석
- 비용 최적화 권장 사항
- 비용 할당 및 차지백 보고서
## 비용 모니터링 및 분석
비용을 효과적으로 최적화하려면 비용을 지속적으로 모니터링하고 분석해야 합니다. 다음과 같은 도구와 전략을 사용하여 EKS 클러스터의 비용을 모니터링하고 분석할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-07-eks-cost-optimization-6.html)
### AWS Cost Explorer
AWS Cost Explorer는 AWS 비용 및 사용량을 시각화, 이해 및 관리하는 데 도움이 되는 도구입니다:
#### 비용 분석
AWS Cost Explorer를 사용하여 EKS 클러스터 비용을 분석합니다:
- 서비스별 비용 분석
- 태그별 비용 분석
- 시간에 따른 비용 추세 분석
```bash
# AWS CLI를 사용하여 비용 데이터 가져오기
aws ce get-cost-and-usage \
--time-period Start=2025-06-01,End=2025-07-01 \
--granularity MONTHLY \
--metrics "UnblendedCost" "AmortizedCost" \
--group-by Type=DIMENSION,Key=SERVICE Type=TAG,Key=Environment
```
2025년 날짜는 과거 문법 예시이며 현재 비용 보고서가 아닙니다. 조회 가능한 UTC 청구 기간을 고르고 종료일은 미포함임을 고려하세요. `NextPageToken`이 있으면 첫 응답을 전체로 취급하지 말고 이어서 조회합니다. 비용 기준별로 비교하며 서로 다른 기준을 더하거나 단위가 다른 `UsageQuantity`를 합산하지 않습니다. 서비스/태그 그룹에는 태그 없는 값도 포함되며 자동으로 클러스터 범위가 되는 것은 아닙니다.
#### 비용 이상 탐지
AWS Cost Anomaly Detection을 사용하여 비정상적인 비용 증가를 감지합니다:
1. AWS Management Console에 로그인
2. AWS Cost Management 서비스로 이동
3. "Cost Anomaly Detection" 선택
4. "Create anomaly monitor" 클릭
5. 모니터 유형 및 알림 기본 설정 구성
#### 비용 예산 설정
이 1,000 USD/80% 예시는 실제 월별 예산 알림을 생성하므로 계정과 이메일을 승인된 값으로 바꿉니다. `user:Environment$Production` 같은 Budgets 태그 필터의 정확한 형식을 청구 설정과 대조합니다. 이 필터는 서비스 전반의 태그 비용을 선택하며, Service를 EKS로 한정하면 EC2·스토리지 등 클러스터 비용이 빠집니다. 태그 없는 비용/공용 비용은 별도로 할당해야 합니다. Budgets는 지연된 청구 데이터를 처리하며 강제 지출 상한이 아닙니다. 기존 예산은 소유자와 조정하고, 여기서는 이미 지난 고정 만료일을 설정하지 않습니다:
```bash
# AWS CLI를 사용하여 예산 생성
aws budgets create-budget \
--account-id 123456789012 \
--budget file://budget.json \
--notifications-with-subscribers file://notifications.json
```
budget.json:
```json
{
"BudgetName": "Tagged Production Workloads",
"BudgetLimit": {
"Amount": "1000",
"Unit": "USD"
},
"BudgetType": "COST",
"CostFilters": {
"TagKeyValue": [
"user:Environment$Production"
]
},
"TimeUnit": "MONTHLY"
}
```
notifications.json:
```json
[
{
"Notification": {
"ComparisonOperator": "GREATER_THAN",
"NotificationType": "ACTUAL",
"Threshold": 80,
"ThresholdType": "PERCENTAGE"
},
"Subscribers": [
{
"Address": "email@example.com",
"SubscriptionType": "EMAIL"
}
]
}
]
```
### Kubecost
Kubecost는 Kubernetes 클러스터의 비용을 모니터링하고 최적화하기 위한 전용 도구입니다:
#### Kubecost 설치
확인한 chart/애플리케이션은 아래 현재 저장소의 **3.2.4**입니다. Kubecost 3.x는 ClickHouse와 finops-agent 직접 수집을 사용하므로 2.x `cost-analyzer` 설치법이나 번들 Prometheus/node-exporter values를 재사용하지 않습니다. 적용 전 [chart 문서](https://github.com/kubecost/cost-analyzer-helm-chart)에서 라이선스·Kubernetes 호환성·StorageClass/PVC 용량·cluster ID·네트워크 수집·접근 제어를 검토합니다. 기존 2.x는 공식 마이그레이션 절차를 따르며 다음은 제자리 업그레이드 절차가 아닙니다. 실제 설치나 운영 준비 완료를 주장하지 않습니다. [FinOps 플랫폼 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/ops/13-finops-cost-platform.md)는 OpenCost 및 청구 대조도 다룹니다.
```bash
helm repo add kubecost https://kubecost.github.io/kubecost/
helm repo update kubecost
helm show values kubecost/kubecost --version 3.2.4 > kubecost-values.yaml
# Edit this file for the reviewed cluster ID, storage, license, and collection settings.
helm template kubecost kubecost/kubecost --version 3.2.4 \
--namespace kubecost -f kubecost-values.yaml > kubecost-rendered.yaml
# Install a NEW release only after reviewing the rendered resources and prerequisites.
helm install kubecost kubecost/kubecost --version 3.2.4 \
--namespace kubecost --create-namespace -f kubecost-values.yaml
```
#### Kubecost 대시보드
Kubecost 대시보드에서 다음과 같은 정보를 확인할 수 있습니다:
- 네임스페이스, 배포, 서비스, 노드별 비용
- 리소스 효율성 및 사용률
- 비용 최적화 권장 사항
- 비용 할당 및 차지백 보고서
#### Kubecost 알림
설치한 Kubecost 에디션/버전이 문서화한 알림 방식을 사용하고 수신자·자격 증명·예산 기간·집계·전달을 명시적으로 구성합니다. 임의의 `cost-analyzer-alerts` ConfigMap과 `alerts.json`이 자동 소비되지는 않으며 이전 예시는 유효한 스키마나 마운트를 제공하지 않았습니다. 위 AWS Budgets 예시는 별개의 구체적인 청구 알림이며 [FinOps 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/ops/13-finops-cost-platform.md)는 명시적인 할당 보고 절차를 제공합니다. 알림에 의존하기 전에 합성 테스트로 전달을 검증하고 임계값 초과뿐 아니라 누락/오래된 데이터도 감시합니다.
### CloudWatch Container Insights
CloudWatch Container Insights를 사용하여 EKS 클러스터의 리소스 사용량을 모니터링합니다:
#### Container Insights 활성화
Container Insights는 CloudWatch agent/observability 애드온이 수집하는 노드/워크로드 텔레메트리입니다. `containerinsights`는 `eksctl utils update-cluster-logging`에서 사용하는 EKS 컨트롤 플레인 로그 유형이 아닙니다. [모니터링 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/06-eks-monitoring-logging.md)에 따라 호환 애드온·IAM 연결·설정·플랫폼별 수집 경로를 선택합니다. 수집기 소유자를 하나로 유지하고 실제 메트릭/로그 전달을 확인하세요. 애드온/agent 텔레메트리 자체에도 요금이 발생할 수 있습니다.
#### 리소스 사용량 모니터링
CloudWatch 대시보드에서 다음과 같은 지표를 모니터링할 수 있습니다:
- CPU 및 메모리 사용량
- 디스크 및 네트워크 I/O
- 컨테이너 재시작 횟수
- 노드 상태
#### 비용 최적화 인사이트
CloudWatch Container Insights 데이터를 분석하여 비용 최적화 기회를 식별합니다:
- 과도하게 프로비저닝된 리소스 식별
- 리소스 사용률이 낮은 노드 식별
- 리소스 요청과 실제 사용량 간의 차이 분석
### 사용자 정의 비용 대시보드
사용자 정의 비용 대시보드를 생성하여 EKS 클러스터의 비용을 종합적으로 모니터링할 수 있습니다:
#### Grafana 대시보드
Prometheus 및 Grafana를 사용하여 사용자 정의 비용 대시보드를 생성합니다:
1. Prometheus에서 리소스 사용량 지표 수집
2. Grafana에서 비용 대시보드 생성
3. Cost Explorer/CUR 결과는 별도로 구현한 인증된 청구 데이터 파이프라인이나 지원 데이터 소스로 연동합니다. Prometheus 사용량에 패널을 추가한다고 실제 청구 데이터가 되지는 않습니다. 브라우저 대시보드 JSON에 청구 자격 증명을 노출하지 않습니다.
#### 비용 최적화 점수
계산식·수집 범위·기간을 정의하여 다음을 별도 지표로 추적합니다. 보편적인 비용 최적화 점수가 있는 것은 아니며 어느 비율 하나로 낭비나 금액 절감을 입증할 수 없습니다:
- 리소스 요청 대 사용량 비율
- 노드 사용률
- 스팟 인스턴스 사용 비율
- 유휴 리소스 비율
## 비용 최적화 모범 사례
EKS 클러스터의 비용을 최적화하기 위한 모범 사례를 살펴보겠습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-07-eks-cost-optimization-7.html)
### 일반적인 모범 사례
#### 지속적인 비용 최적화
비용 최적화는 일회성 작업이 아닌 지속적인 프로세스입니다:
1. **측정**: 현재 비용 및 리소스 사용량 측정
2. **분석**: 비용 동인 및 최적화 기회 분석
3. **최적화**: 비용 최적화 전략 구현
4. **모니터링**: 결과 모니터링 및 필요에 따라 조정
5. **반복**: 프로세스 반복
#### 비용 인식 문화 구축
조직 내에서 비용 인식 문화를 구축합니다:
- 팀에 비용 가시성 제공
- 비용 최적화 목표 설정
- 비용 최적화 성과 인정 및 보상
- 비용 최적화 모범 사례 공유
#### 자동화 활용
자동화를 활용하여 비용을 최적화합니다:
- 자동 스케일링 구현
- 사용량 기반 리소스 프로비저닝
- 비용 이상 탐지 및 알림 자동화
- 후보 식별은 자동화하되 소유권·보존·의존성·복구 확인 후 제거
### 워크로드별 최적화
#### 개발 및 테스트 환경
개발 및 테스트 환경의 비용을 최적화합니다:
- 사용하지 않을 때 환경 자동 종료
- 스팟 인스턴스 사용
- 리소스 제한 설정
- 공유 환경 사용 고려
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: dev-app-scaler
namespace: dev
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: dev-app-scaler
namespace: dev
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
resourceNames: ["dev-app"]
verbs: ["get"]
- apiGroups: ["apps"]
resources: ["deployments/scale"]
resourceNames: ["dev-app"]
verbs: ["get", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-app-scaler
namespace: dev
subjects:
- kind: ServiceAccount
name: dev-app-scaler
namespace: dev
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: dev-app-scaler
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: dev-app-shutdown
namespace: dev
spec:
suspend: true
schedule: "0 20 * * 1-5"
timeZone: Etc/UTC
concurrencyPolicy: Forbid
startingDeadlineSeconds: 1800
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 2
jobTemplate:
spec:
backoffLimit: 0
activeDeadlineSeconds: 120
template:
spec:
serviceAccountName: dev-app-scaler
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: kubectl
image: registry.k8s.io/kubectl:v1.36.2
command: ["kubectl"]
args: ["scale", "deployment/dev-app", "--namespace=dev", "--current-replicas=3", "--replicas=0"]
env:
- name: HOME
value: /tmp
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128Mi
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
```
CronJob은 **기본 suspend 상태**로 기존 `dev/dev-app` Deployment 하나만 대상으로 하며 현재 복제본 3개를 전제로 합니다. 서버와 지원되는 버전 차이의 kubectl을 사용하세요. 활성화 전 UTC 일정과 복구 절차를 합의하고 이름/전제 조건을 의도적으로 조정합니다. HPA나 GitOps가 복제본을 관리하면 경쟁하는 대신 해당 소유자의 일정 기능과 조율합니다. Deployment 직접 축소는 PDB eviction admission을 사용하지 않으므로 승인된 개발 환경 종료에 한정합니다. Pod를 중지해도 EKS 컨트롤 플레인 요금, 보존 스토리지, 축소할 수 없는 노드의 요금이 멈추지는 않습니다.
#### 배치 워크로드
배치 워크로드의 비용을 최적화합니다:
- 스팟 인스턴스 사용
- 작업 기한과 가용 용량에 맞춰 예약합니다. 온디맨드 컴퓨팅에 보편적인 시간대 할인이 있는 것은 아닙니다.
- 리소스 요청 최적화
- 작업 완료 후 리소스 해제
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: batch-job
spec:
template:
spec:
nodeSelector:
eks.amazonaws.com/capacityType: SPOT
containers:
- name: batch-processor
image: batch-processor:latest
resources:
requests:
cpu: 2
memory: 4Gi
limits:
cpu: 4
memory: 8Gi
restartPolicy: Never
backoffLimit: 4
```
이 Job은 **관리형 노드 그룹의 Spot 노드**를 선택합니다. Karpenter는 대신 `karpenter.sh/capacity-type: spot`을 사용하므로 대상 노드의 실제 레이블을 선택하세요. 애플리케이션 이미지는 자리표시자입니다. 멱등 재시도/체크포인트를 구현하고 노드 taint의 toleration도 확인합니다. Job 완료/TTL은 Kubernetes 객체를 정리하며 PVC·볼륨·과금 노드를 반드시 제거하지는 않습니다.
#### 웹 애플리케이션
웹 애플리케이션의 비용을 최적화합니다:
- 자동 스케일링 구현
- CDN 사용하여 트래픽 감소
- 캐싱 전략 구현
- 서버리스 아키텍처 고려
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
```
#### 데이터베이스 워크로드
데이터베이스 워크로드의 비용을 최적화합니다:
- 적절한 인스턴스 유형 선택
- 스토리지 자동 확장 구성
- 읽기 전용 복제본 사용 고려
- 캐싱 계층 추가 고려
### 금융 서비스를 위한 비용 최적화
금융 서비스 산업에서 EKS를 사용할 때 고려해야 할 추가 비용 최적화 전략:
#### 규제 준수 비용 관리
규제 준수 요구사항을 충족하면서 비용을 최적화합니다:
- 규제 요구사항에 맞는 최소한의 리소스 프로비저닝
- 규제 준수 자동화를 통한 운영 비용 절감
- 규제 준수 환경과 비규제 환경 분리
#### 고가용성과 비용 균형
고가용성 요구사항과 비용 사이의 균형을 유지합니다:
- 중요 워크로드에 대한 다중 가용 영역 배포
- 비중요 워크로드에 대한 단일 가용 영역 배포 고려
- 재해 복구 환경에 대한 비용 효율적인 접근 방식 구현
#### 보안 요구사항과 비용 균형
보안 요구사항과 비용 사이의 균형을 유지합니다:
- 위험 기반 접근 방식을 사용하여 보안 제어 구현
- 보안 자동화를 통한 운영 비용 절감
- 비용 효율적인 보안 도구 및 서비스 선택
## 결론
Amazon EKS 클러스터의 비용을 효과적으로 최적화하려면 컴퓨팅, 스토리지, 네트워킹 및 운영 비용을 포괄하는 종합적인 접근 방식이 필요합니다. 각 변경은 실제 청구와 워크로드 SLO에 대조하여 평가해야 하며, 비용 절감이나 성능·안정성 유지가 보장되지는 않습니다.
주요 내용:
1. **EKS 비용 구성 요소**: EKS 클러스터 비용, 컴퓨팅 비용, 스토리지 비용, 네트워킹 비용 및 기타 비용
2. **컴퓨팅 비용 최적화**: 적절한 인스턴스 유형 선택, 스팟 인스턴스 활용, Savings Plans 및 예약 인스턴스 사용, 자동 스케일링 최적화
3. **스토리지 비용 최적화**: EBS 볼륨 최적화, EFS 비용 최적화, S3 비용 최적화
4. **네트워킹 비용 최적화**: 데이터 전송 최적화, 로드 밸런서 최적화, NAT 게이트웨이 최적화
5. **리소스 관리 및 거버넌스**: 리소스 요청 및 제한 최적화, 네임스페이스 및 리소스 쿼터, 비용 할당 및 태깅
6. **비용 모니터링 및 분석**: AWS Cost Explorer, Kubecost, CloudWatch Container Insights, 사용자 정의 비용 대시보드
7. **비용 최적화 모범 사례**: 일반적인 모범 사례, 워크로드별 최적화, 금융 서비스를 위한 비용 최적화
비용 최적화는 지속적인 프로세스이며, 클러스터 및 워크로드가 발전함에 따라 비용 최적화 전략을 정기적으로 검토하고 조정해야 합니다.
## 참고 자료
- [Amazon EKS 요금](https://aws.amazon.com/eks/pricing/)
- [AWS 비용 최적화 리소스](https://aws.amazon.com/aws-cost-management/)
- [Kubernetes 리소스 관리](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
- [AWS Well-Architected Framework - 비용 최적화 원칙](https://docs.aws.amazon.com/wellarchitected/latest/cost-optimization-pillar/welcome.html)
- [Kubecost 문서](https://www.kubecost.com/kubernetes-cost-optimization/kubernetes-cost-optimization-best-practices/)
- [EKS 모범 사례 - 비용 최적화](https://docs.aws.amazon.com/eks/latest/best-practices/cost-opt.html)
- [FinOps principles](https://www.finops.org/framework/principles/)
- [Karpenter compatibility](https://karpenter.sh/docs/upgrading/compatibility/)
- [EKS Fargate allocation](https://docs.aws.amazon.com/eks/latest/userguide/fargate-pod-configuration.html)
- [EBS gp3 limits](https://docs.aws.amazon.com/ebs/latest/userguide/general-purpose.html)
- [Regional NAT gateways](https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateways-regional.html)
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/07-eks-cost-optimization-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/08-eks-upgrades
----------------------------------------
# Amazon EKS 업그레이드
> **마지막 업데이트**: 2026년 9월 12일
Amazon EKS 클러스터를 최신 상태로 유지하는 것은 보안, 안정성 및 새로운 기능을 활용하기 위해 중요합니다. 이 문서에서는 EKS 클러스터를 안전하게 업그레이드하기 위한 전략, 모범 사례 및 단계별 가이드를 제공합니다.
이 문서는 소유자가 검토하여 수행할 절차이며 이번 감사에서 실제 업그레이드를 실행한 기록이 아닙니다. 계정·리전·Kubernetes context를 명시하고 승인한 현재/대상 버전과 update ID를 기록하며 애플리케이션 동작을 검증합니다. 같은 리소스에 모든 대안 예시를 연속 실행하지 않습니다. 로컬 파서/모의 검증은 운영 준비 완료를 입증하지 않습니다.
## 목차
1. [EKS 업그레이드 개요](#eks-업그레이드-개요)
2. [업그레이드 계획 및 준비](#업그레이드-계획-및-준비)
3. [EKS 컨트롤 플레인 업그레이드](#eks-컨트롤-플레인-업그레이드)
4. [노드 그룹 업그레이드](#노드-그룹-업그레이드)
5. [애드온 업그레이드](#애드온-업그레이드)
6. [업그레이드 검증 및 문제 해결](#업그레이드-검증-및-문제-해결)
7. [업그레이드 자동화](#업그레이드-자동화)
8. [업그레이드 모범 사례](#업그레이드-모범-사례)
## EKS 업그레이드 개요

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-08-eks-upgrades-0.html)
### EKS 버전 관리
EKS는 Kubernetes 버전 번호를 사용하지만 자체 출시/지원 일정을 따릅니다. 마이너 버전별 **표준 지원 14개월** 이후 추가 요금이 있는 **확장 지원 12개월**이 이어집니다. AWS는 표준 지원 종료를 최소 60일 전에 안내합니다. [현재 EKS 카탈로그/일정](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html)을 확인하며 업스트림 출시를 EKS 제공 여부로 간주하지 않습니다. 고정된 “최소 4개 버전”이나 14개월이 전체 지원 기간이라는 가정을 사용하지 않습니다.
### 최근 EKS 업그레이드 관련 발표 (2026년)
- **Kubernetes 버전 롤백 지원 (2026-07-01)**: 업그레이드 후 7일 이내라면 컨트롤 플레인을 이전 마이너 버전으로 롤백할 수 있습니다. 롤백 전 API 호환성, version skew, 애드온 호환성, 클러스터 상태를 점검하는 Rollback Readiness 검사가 자동으로 수행되며, 사용자가 롤백을 요청하면 Auto Mode가 해당 워커 노드를 먼저 교체한 뒤 컨트롤 플레인을 되돌립니다. 애플리케이션 장애를 감지해 자동 시작하는 기능은 아니며 자격 조건과 disruption 제어가 적용됩니다. EKS 제공 리전에서 기능 자체의 추가 요금은 없지만 노드·스토리지 및 해당 버전 지원 요금은 계속 적용됩니다. 자세한 절차는 [롤백 절차](#롤백-절차)를 참고하세요. (출처: [Amazon EKS 버전 롤백 발표](https://aws.amazon.com/about-aws/whats-new/2026/07/amazon-eks-version-rollback))
- **99.99% SLA 및 8XL 컨트롤 플레인 티어 (2026-03-20)**: Provisioned Control Plane의 SLA가 99.95%에서 99.99%로 향상되었으며 1분 단위로 측정됩니다. 초대규모 클러스터, AI/ML, HPC 워크로드를 위한 8XL 스케일링 티어가 신규 도입되어 기존 4XL 대비 API 처리 용량이 2배로 늘었습니다. (출처: [Amazon EKS SLA 및 8XL 스케일링 티어 발표](https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-eks-announces-sla-8xl-scaling-tier/))
### 업그레이드 구성 요소
EKS 클러스터 업그레이드에는 다음과 같은 구성 요소가 포함됩니다:
1. **EKS 컨트롤 플레인**: Kubernetes API 서버, etcd, 컨트롤러 관리자 등
2. **노드 그룹**: 워커 노드 및 노드 AMI
3. **애드온**: AWS 관리형 애드온(예: CoreDNS, kube-proxy, VPC CNI)
4. **자체 관리형 구성 요소**: Helm 차트, 사용자 정의 리소스 등
### 업그레이드 경로
EKS 클러스터는 한 번에 한 마이너 버전씩 업그레이드해야 합니다:
- 과거 예시 1.24 → 1.25 → 1.26 → 1.27은 한 마이너씩 진행하는 형태를 설명하며 현재 배포 대상이 아닙니다.
- 1.24 → 1.26 직접 이동은 마이너 버전 건너뛰기의 예시입니다.
- 실제 변경은 해당 리전/지원 정책에서 EKS가 제공하는 다음 마이너를 선택합니다.
### 업그레이드 순서
1. 현황/테스트/백업과 진행 중인 업데이트를 확인하고, 보수적인 준비 절차로 노드를 **현재** 컨트롤 플레인 마이너에 맞춥니다.
2. 현재/대상 Kubernetes 모두와 호환되는 애드온/컨트롤러 중간 버전이 필요하면 먼저 적용하고 구성 요소별 전제 조건을 검증합니다.
3. 컨트롤 플레인을 지원되는 다음 마이너로 올리고 해당 update ID의 성공을 기다립니다.
4. 검증한 의존성 순서로 노드·클라이언트/컨트롤러·나머지 애드온을 갱신하며 매 단계 확인합니다. Auto Mode는 기본 기능을 관리하고 컨트롤 플레인 업그레이드 후 노드 갱신을 시작합니다.
5. 실제 노드 버전, 워크로드 readiness, 통신, 스토리지, SLO를 검증합니다.
업스트림 정책에서 kubelet 1.25+는 API server보다 최대 3개 마이너 이전을 허용하지만 더 새로울 수 없으며, API server 버전이 혼재하면 허용 범위가 좁아집니다. EKS 문서에는 준비 단계의 노드 버전 정렬 안내와 지원 skew 허용 범위가 함께 있습니다. 이 절차는 준비 정책으로 정렬을 선택하며 지원되는 모든 skew를 EKS API가 일률적으로 거절한다고 주장하지 않습니다. 애드온도 모두 CP 이전/이후라는 단일 순서로 정할 수 없습니다. 롤백은 아래의 다른 노드 우선 순서를 따릅니다.
## 업그레이드 계획 및 준비
### 업그레이드 평가
업그레이드를 시작하기 전에 다음 사항을 평가해야 합니다:
#### 버전 호환성 확인
대상 Kubernetes 버전과의 호환성을 확인합니다:
- **API 사용 중단**: 사용 중단된 API를 사용하는 워크로드 식별
- **기능 변경**: 새 버전의 기능 변경 사항 검토
- **애드온 호환성**: 애드온이 대상 버전과 호환되는지 확인
이미지 목록이나 `kubectl get … .apiVersion`은 클라이언트/API 사용 중단 감사가 아닙니다. API server가 협상한 현재 표현을 반환하며 beta API라는 이유만으로 deprecated는 아닙니다. 소스/Helm 매니페스트·클라이언트·CRD·웹훅과 EKS upgrade insight를 대상 릴리스 기준으로 검토합니다. 단일 API server 메트릭 수집은 관측 증거일 뿐이며 누락/오류가 미사용 증명은 아닙니다.
다음 읽기 전용 수집기를 `eks-upgrade-preflight.py`로 저장합니다. `CLUSTER_NAME`, `AWS_REGION`, `EXPECTED_ACCOUNT_ID`, `KUBE_CONTEXT`, `TARGET_VERSION`을 export하고 JSON을 제한된 로컬 권한으로 저장합니다. 종료 성공은 수집 성공이며 업그레이드 승인이나 모든 워크로드의 호환성 확인이 아닙니다.
```python
import json
import os
import re
import subprocess
def run_json(args):
result = subprocess.run(args, check=True, capture_output=True, text=True, timeout=60)
return json.loads(result.stdout)
def run_text(args):
return subprocess.run(args, check=True, capture_output=True, text=True, timeout=30).stdout.strip()
def inspect_upgrade(cluster_name, region, expected_account, context, target, query=run_json, text=run_text):
if not re.fullmatch(r"[0-9]{12}", expected_account):
raise ValueError("Set the reviewed 12-digit AWS account ID")
if not re.fullmatch(r"[0-9]+\.[0-9]+", target):
raise ValueError("Target must be an EKS major.minor version")
aws = [os.environ.get("AWS_CLI", "aws")]
suffix = ["--region", region, "--output", "json", "--no-cli-pager"]
identity = query(aws + ["sts", "get-caller-identity"] + suffix)
if identity["Account"] != expected_account:
raise RuntimeError("AWS account mismatch")
cluster = query(aws + ["eks", "describe-cluster", "--name", cluster_name] + suffix)["cluster"]
if cluster["status"] != "ACTIVE":
raise RuntimeError("Cluster must be ACTIVE; inspect any in-progress updates")
if cluster["arn"].split(":")[4] != expected_account:
raise RuntimeError("Cluster/account mismatch")
current = cluster["version"]
current_parts = tuple(map(int, current.split(".")))
target_parts = tuple(map(int, target.split(".")))
if target_parts != (current_parts[0], current_parts[1] + 1):
raise ValueError("This upgrade example requires exactly the next minor version")
server = text(["kubectl", "--context", context, "config", "view", "--minify",
"--output", "jsonpath={.clusters[0].cluster.server}"])
if server != cluster["endpoint"]:
raise RuntimeError("Kubernetes context does not match the EKS API endpoint")
catalog = query(aws + ["eks", "describe-cluster-versions", "--cluster-versions", target,
"--include-all", "--no-default-only"] + suffix)["clusterVersions"]
if not any(item["clusterVersion"] == target and item.get("versionStatus") in
["STANDARD_SUPPORT", "EXTENDED_SUPPORT"] for item in catalog):
raise ValueError("Target is not offered as a supported EKS version in this Region")
insights = query(aws + [
"eks", "list-insights", "--cluster-name", cluster_name,
"--filter", json.dumps({"categories": ["UPGRADE_READINESS"], "kubernetesVersions": [target]}),
] + suffix)["insights"]
nodes = query(["kubectl", "--context", context, "get", "nodes", "--output", "json"])["items"]
update_ids = query(aws + ["eks", "list-updates", "--name", cluster_name] + suffix)["updateIds"]
addon_names = query(aws + ["eks", "list-addons", "--cluster-name", cluster_name] + suffix)["addons"]
addons = []
for name in addon_names:
addon = query(aws + ["eks", "describe-addon", "--cluster-name", cluster_name,
"--addon-name", name] + suffix)["addon"]
addons.append({"name": name, "version": addon["addonVersion"], "status": addon["status"]})
return {
"cluster": cluster_name, "region": region, "account": expected_account,
"currentVersion": current, "targetVersion": target, "targetCatalog": catalog,
"insights": insights,
"clusterUpdateIds": update_ids,
"nodes": [{"name": node["metadata"]["name"],
"kubeletVersion": node["status"]["nodeInfo"]["kubeletVersion"],
"ready": next((condition.get("status") for condition in node["status"].get("conditions", [])
if condition.get("type") == "Ready"), None),
"computeType": node["metadata"].get("labels", {}).get("eks.amazonaws.com/compute-type"),
"nodegroup": node["metadata"].get("labels", {}).get("eks.amazonaws.com/nodegroup")}
for node in nodes],
"managedAddons": addons,
"decision": "Inventory collected only; active-update review, owner approval, node alignment, API/client scans, "
"backups/restore tests, add-on bridge versions, capacity, and workload tests remain required.",
}
if __name__ == "__main__":
report = inspect_upgrade(
os.environ["CLUSTER_NAME"], os.environ["AWS_REGION"],
os.environ["EXPECTED_ACCOUNT_ID"], os.environ["KUBE_CONTEXT"],
os.environ["TARGET_VERSION"],
)
print(json.dumps(report, indent=2))
```
#### 리소스 요구사항 평가
업그레이드에 필요한 리소스를 평가합니다:
- **클러스터 용량**: 업그레이드 중 추가 노드를 수용할 수 있는 충분한 용량
- **다운타임 허용**: 워크로드의 다운타임 허용 여부
- **롤백 계획**: 문제 발생 시 롤백 계획
#### 업그레이드 일정 계획
업그레이드 일정을 계획합니다:
- **유지 관리 기간**: 트래픽이 적은 시간에 업그레이드 예약
- **단계적 접근**: 비프로덕션 환경부터 시작하여 프로덕션 환경으로 진행
- **롤백 기간**: 문제 발생 시 롤백에 필요한 시간 계획
### 업그레이드 전 준비
#### 클러스터 상태 확인
업그레이드 전에 클러스터 상태를 확인합니다:
```bash
# 노드 상태 확인
kubectl --context "$KUBE_CONTEXT" get nodes
# 파드 상태 확인
kubectl --context "$KUBE_CONTEXT" get pods --all-namespaces
# 컴포넌트 상태 확인
kubectl --context "$KUBE_CONTEXT" get --raw /readyz
# 이벤트 확인
kubectl --context "$KUBE_CONTEXT" get events --all-namespaces
```
#### 백업 생성
업그레이드 전에 중요한 데이터를 백업합니다:
관리형 컨트롤 플레인 etcd는 EKS 소유이므로 사용자 namespace의 `etcd-pod`/`etcdctl snapshot` 명령으로 백업할 수 없습니다. `kubectl get all`도 중요한 리소스와 볼륨 데이터를 빠뜨리므로 전체 백업이 아닙니다.
[AWS Backup의 EKS 지원](https://docs.aws.amazon.com/eks/latest/userguide/integration-backup.html)은 문서화된 IAM, API/API_AND_CONFIG_MAP 접근 모드, 스토리지/복원 전제 조건 아래 cluster state와 PVC 기반 EBS/EFS/S3를 composite recovery point로 보호할 수 있습니다. 또는 볼륨 범위와 자격 증명을 검증한 소유 Velero/애플리케이션 백업을 사용합니다. namespace/클러스터 범위, DB 일관성, 보존, 복원 테스트를 검토하며 백업 요청 ID만으로 완료/복구 가능성을 판단하지 않습니다.
#### 업그레이드 테스트
비프로덕션 환경에서 업그레이드를 테스트합니다:
1. 프로덕션 환경과 유사한 테스트 클러스터 생성
2. 테스트 클러스터에서 업그레이드 수행
3. 워크로드 및 기능 테스트
4. 문제 식별 및 해결
#### 업그레이드 문서 작성
업그레이드 프로세스를 문서화합니다:
- 업그레이드 단계
- 담당자 및 연락처
- 롤백 절차
- 문제 해결 가이드
## EKS 컨트롤 플레인 업그레이드
### 컨트롤 플레인 업그레이드 준비
#### 현재 버전 확인
현재 EKS 클러스터 버전을 확인합니다:
```bash
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" --query "cluster.version"
```
#### 사용 가능한 버전 확인
사용 가능한 Kubernetes 버전을 확인합니다:
```bash
aws eks describe-cluster-versions --region "$AWS_REGION" \
--no-default-only --output json --no-cli-pager
```
#### 업그레이드 계획 수립
일반 업그레이드 요청 전 [현재 EKS 안내](https://docs.aws.amazon.com/eks/latest/userguide/update-cluster.html)를 확인합니다. Upgrade insight 문제에 `--force`를 요구하는 강제 적용 기능은 일시 철회된 상태이며, 버전 롤백의 `ROLLBACK_READINESS` 차단 규칙과 다릅니다. 문제를 해결하거나 명시적으로 평가하세요. 수정 후에도 최근 30일 API 사용 증거가 남을 수 있습니다. 클러스터 서브넷의 존재, 필요한 여유 주소(EKS가 최대 5개 필요로 할 수 있음), CP 통신 규칙을 확인합니다. 시작한 CP 업그레이드는 일시중지/중단할 수 없으며 클라이언트 재연결을 처리해야 합니다.
컨트롤 플레인 업그레이드 계획을 수립합니다:
- 업그레이드 시간: 트래픽이 적은 시간 선택
- 모니터링 설정: 업그레이드 중 클러스터 상태 모니터링
- 롤백 계획: 문제 발생 시 롤백 절차
### 컨트롤 플레인 업그레이드 수행
#### AWS Management Console을 사용한 업그레이드
1. AWS Management Console에 로그인
2. Amazon EKS 서비스로 이동
3. 클러스터 목록에서 업그레이드할 클러스터 선택
4. "클러스터 구성" 탭 선택
5. "Kubernetes 버전 업데이트" 클릭
6. 대상 버전 선택 및 "업데이트" 클릭
#### AWS CLI를 사용한 업그레이드
기록한 계획과 전제 조건의 승인이 끝난 후에만 실행합니다. 다음 절의 polling helper를 `eks-wait-update.py`로 저장하세요. 컨트롤 플레인 업데이트만 요청하며 노드/애드온 갱신이나 워크로드 readiness를 입증하지 않습니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the reviewed cluster}"
: "${AWS_REGION:?Set the reviewed Region}"
: "${TARGET_VERSION:?Set the next supported EKS minor version}"
umask 077
aws eks update-cluster-version \
--name "$CLUSTER_NAME" --region "$AWS_REGION" \
--kubernetes-version "$TARGET_VERSION" --output json --no-cli-pager \
> control-plane-update.json
UPDATE_ID=$(jq -er '.update.id | select(type == "string" and length > 0)' control-plane-update.json)
export CLUSTER_NAME AWS_REGION UPDATE_ID
unset NODEGROUP_NAME ADDON_NAME
python3 eks-wait-update.py
```
#### eksctl을 사용한 업그레이드
```bash
eksctl upgrade cluster \
--name "$CLUSTER_NAME" --region "$AWS_REGION" \
--version "$TARGET_VERSION" \
--approve
```
### 컨트롤 플레인 업그레이드 모니터링
#### 업그레이드 상태 확인
업그레이드 상태를 확인합니다:
정확한 update ID의 `DescribeUpdate`를 추적합니다. `cluster-active`/`nodegroup-active`는 해당 요청의 성공 증명이 아니며 AWS CLI 2.36.44에는 `eks wait update-successful` waiter가 없습니다. 다음을 `eks-wait-update.py`로 저장합니다. 노드 그룹/애드온 요청은 `NODEGROUP_NAME` 또는 `ADDON_NAME` 하나만 설정하고 CP 요청은 둘 다 해제합니다. API 오류, 실패/취소, 알 수 없는 상태, 클라이언트 기한 초과에서 실패합니다. 클라이언트 timeout은 AWS 작업을 취소하지 않습니다. 기본 2시간 클라이언트 대기는 승인한 작업에 맞춰 조정하며 긴 Auto Mode 롤백은 특히 주의합니다.
```python
import json
import os
import subprocess
import time
from pathlib import Path
def wait_update(lookup, timeout_seconds, interval_seconds=15, clock=time.monotonic, sleep=time.sleep):
if timeout_seconds <= 0 or interval_seconds < 0:
raise ValueError("Use a positive timeout and nonnegative polling interval")
deadline = clock() + timeout_seconds
while True:
update = lookup()["update"]
status = update["status"]
print(json.dumps({"id": update["id"], "status": status, "errors": update.get("errors", [])}), flush=True)
if status == "Successful":
return update
if status in ["Failed", "Cancelled"]:
raise RuntimeError(f"EKS update ended with {status}; inspect its error details")
if status not in ["InProgress", "Cancelling"]:
raise RuntimeError(f"Unexpected update status: {status}")
if clock() >= deadline:
raise TimeoutError("Client wait expired; the AWS operation may still be running. Preserve its update ID.")
sleep(interval_seconds)
if __name__ == "__main__":
cluster = os.environ["CLUSTER_NAME"]
region = os.environ["AWS_REGION"]
update_id = os.environ["UPDATE_ID"]
nodegroup = os.environ.get("NODEGROUP_NAME")
addon = os.environ.get("ADDON_NAME")
if nodegroup and addon:
raise ValueError("Set only NODEGROUP_NAME or ADDON_NAME for a scoped update")
command = [
os.environ.get("AWS_CLI", "aws"), "eks", "describe-update",
"--name", cluster, "--region", region, "--update-id", update_id,
"--output", "json", "--no-cli-pager",
]
if nodegroup:
command += ["--nodegroup-name", nodegroup]
if addon:
command += ["--addon-name", addon]
status_file = Path(os.environ.get("UPDATE_STATUS_FILE", "eks-update-status.json"))
def lookup():
completed = subprocess.run(command, check=True, capture_output=True, text=True, timeout=60)
response = json.loads(completed.stdout)
status_file.write_text(json.dumps(response, indent=2) + "\n")
return response
wait_update(lookup, int(os.environ.get("WAIT_TIMEOUT_SECONDS", "7200")))
```
#### 클러스터 상태 모니터링
업그레이드 중 클러스터 상태를 모니터링합니다:
```bash
# 노드 상태 확인
kubectl --context "$KUBE_CONTEXT" get nodes
# 파드 상태 확인
kubectl --context "$KUBE_CONTEXT" get pods --all-namespaces
# 이벤트 확인
kubectl --context "$KUBE_CONTEXT" get events --all-namespaces --sort-by='.lastTimestamp'
```
#### CloudWatch 지표 모니터링
EKS 버전/등급과 구성한 텔레메트리에서 실제 제공되는 메트릭을 모니터링합니다. API 요청률·지연·오류, API readiness와 클라이언트 재연결, 스케줄링, 노드/워크로드 상태, 애플리케이션 SLO를 확인하며 직접 etcd/controller-manager endpoint나 모든 구성 요소 메트릭의 노출을 가정하지 않습니다. `/readyz`는 API 준비 상태이며 모든 애플리케이션이나 과거 가용성 상태의 검증이 아닙니다.
### 컨트롤 플레인 업그레이드 문제 해결
#### 일반적인 문제
컨트롤 플레인 업그레이드 중 발생할 수 있는 일반적인 문제:
- **업그레이드 실패**: 업그레이드 프로세스가 실패하거나 중단됨
- **API 서버 가용성**: 업그레이드 중 API 서버 가용성 문제
- **호환성 문제**: 워크로드와 새 버전 간의 호환성 문제
#### 문제 해결 단계
1. 업그레이드 상태 확인
2. CloudTrail 로그 검토
3. EKS 컨트롤 플레인 로그 검토
4. AWS Support에 문의
## 노드 그룹 업그레이드
컨트롤 플레인을 업그레이드한 후에는 노드 그룹을 업그레이드해야 합니다. 노드 그룹 업그레이드에는 여러 전략이 있으며, 각 전략에는 장단점이 있습니다.
### 노드 그룹 업그레이드 전략
#### 관리형 노드 그룹 업그레이드
관리형 노드 그룹은 AWS에서 제공하는 노드 그룹 관리 기능으로, 노드 업그레이드를 자동화합니다:
- **교체 전략**: `maxUnavailable`/percentage에 따라 병렬 교체할 수 있습니다. API의 `updateStrategy=DEFAULT`는 새 용량 확보 후 기존 용량을 제거하고, `MINIMAL`은 기존 용량을 먼저 제거하므로 가용성/용량 조건이 다릅니다.
- **드레이닝**: 정상 업데이트는 Pod eviction/PDB를 확인하며 컨트롤러가 Pod를 재생성합니다. live migration이 아닙니다. 노드 그룹 force 옵션은 PDB 관련 drain 실패를 우회할 수 있으며 클러스터 롤백의 force와 다릅니다.
- **버전/AMI 선택**: 검토한 Kubernetes/AMI 릴리스 또는 custom AMI를 담은 기존 launch template의 새 버전을 선택합니다. 업데이트 실패가 전체 노드의 자동 롤백을 뜻하지 않으므로 혼재한 노드/AMI 상태와 오류를 확인합니다.
#### 자체 관리형 노드 그룹 업그레이드
자체 관리형 노드 그룹의 경우 수동으로 노드를 업그레이드해야 합니다:
- **블루/그린 배포**: 새 노드 그룹 생성 후 워크로드 마이그레이션
- **롤링 업그레이드**: 노드를 하나씩 드레이닝하고 종료한 후 새 노드로 교체
- **인플레이스 업그레이드**: 기존 노드에서 kubelet 및 컨테이너 런타임 업그레이드
#### Fargate 노드 업그레이드
Fargate 노드 인프라는 AWS가 관리하지만 워크로드 소유자가 Pod 교체를 조율해야 합니다. 새 Fargate Pod의 kubelet 버전은 CP와 일치하며 기존 Pod는 CP 업데이트만으로 갱신되지 않습니다. 별도 작업이 전혀 없다고 가정하지 말고 컨트롤러 rollout·가용성·검증을 계획합니다. Auto Mode는 별도로 CP 업그레이드 후 자체적인 점진적 노드 교체를 disruption 제어에 따라 시작합니다.
### 관리형 노드 그룹 업그레이드
#### 관리형 노드 그룹 버전 확인
현재 관리형 노드 그룹 버전을 확인합니다:
```bash
aws eks describe-nodegroup \
--cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--nodegroup-name "$NODEGROUP_NAME" \
--query "nodegroup.version"
```
#### AWS Management Console을 사용한 업그레이드
1. AWS Management Console에 로그인
2. Amazon EKS 서비스로 이동
3. 클러스터 목록에서 업그레이드할 클러스터 선택
4. "컴퓨팅" 탭 선택
5. 업그레이드할 노드 그룹 선택
6. "노드 그룹 업데이트" 클릭
7. 대상 버전 선택 및 "업데이트" 클릭
#### AWS CLI를 사용한 업그레이드
EKS 최적화 AMI는 검토한 릴리스와 대상 버전을 사용합니다. 별도의 update ID를 가지므로 polling에도 노드 그룹 범위를 유지합니다. 플랫폼/AMI 계열 지원을 검토하며 Kubernetes 버전만 바꿔 AL2 노드 그룹이 AL2023으로 전환되는 것은 아닙니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${NODEGROUP_NAME:?}"
: "${TARGET_VERSION:?Set the reviewed target, no newer than the control plane}"
: "${TARGET_AMI_RELEASE:?Set the reviewed EKS-optimized AMI release}"
umask 077
aws eks describe-nodegroup --cluster-name "$CLUSTER_NAME" --nodegroup-name "$NODEGROUP_NAME" \
--region "$AWS_REGION" --output json > nodegroup-before.json
if jq -e '.nodegroup.amiType == "CUSTOM"' nodegroup-before.json >/dev/null; then
echo "Use the custom launch-template path for this node group" >&2
exit 1
fi
aws eks update-nodegroup-version \
--cluster-name "$CLUSTER_NAME" --nodegroup-name "$NODEGROUP_NAME" --region "$AWS_REGION" \
--kubernetes-version "$TARGET_VERSION" --release-version "$TARGET_AMI_RELEASE" \
--output json > nodegroup-update.json
UPDATE_ID=$(jq -er '.update.id | select(type == "string" and length > 0)' nodegroup-update.json)
export CLUSTER_NAME AWS_REGION UPDATE_ID NODEGROUP_NAME
unset ADDON_NAME
python3 eks-wait-update.py
```
**Custom AMI**는 원래 launch template의 검토한 새 버전을 사용합니다. 이 요청에는 Kubernetes `version`이나 `releaseVersion`을 함께 전달하지 않으며 교체 노드의 실제 kubelet/runtime/AMI를 검증합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${NODEGROUP_NAME:?}"
: "${LAUNCH_TEMPLATE_ID:?Set the same launch template originally used by the group}"
: "${LAUNCH_TEMPLATE_VERSION:?Set the reviewed version containing the updated custom AMI}"
aws eks update-nodegroup-version \
--cluster-name "$CLUSTER_NAME" --nodegroup-name "$NODEGROUP_NAME" --region "$AWS_REGION" \
--launch-template "id=$LAUNCH_TEMPLATE_ID,version=$LAUNCH_TEMPLATE_VERSION" \
--output json > nodegroup-update.json
UPDATE_ID=$(jq -er '.update.id | select(type == "string" and length > 0)' nodegroup-update.json)
export CLUSTER_NAME AWS_REGION UPDATE_ID NODEGROUP_NAME
unset ADDON_NAME
python3 eks-wait-update.py
```
#### eksctl을 사용한 업그레이드
```bash
eksctl upgrade nodegroup \
--cluster "$CLUSTER_NAME" --region "$AWS_REGION" \
--name "$NODEGROUP_NAME" \
--kubernetes-version "$TARGET_VERSION"
```
#### 관리형 노드 그룹 업그레이드 구성
관리형 노드 그룹 업그레이드 동작을 구성할 수 있습니다:
- **최대 사용 불가능**: 업그레이드 중 사용할 수 없는 최대 노드 수
- **PDB**: 해당 자발적 축출을 제한하지만 모든 장애나 애플리케이션 결과를 보장하지 않습니다. 강제 노드 그룹 업데이트는 PDB 문제를 무시할 수 있습니다. 관리형 노드 그룹 desired/min/max 변경은 ASG scaling이며 업그레이드의 drain/PDB 보호를 제공하지 않으므로 0대 전환을 안전한 마이그레이션으로 취급하지 않습니다.
```bash
aws eks update-nodegroup-config \
--cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--nodegroup-name "$NODEGROUP_NAME" \
--update-config maxUnavailable=1
```
### 자체 관리형 노드 그룹 업그레이드
#### 블루/그린 배포
소유한 구성에서 다른 이름의 새 노드 그룹을 준비하고 검토한 subnet/AZ·IAM·AMI 아키텍처/bootstrap·레이블·taint·스토리지·네트워크 요구를 유지합니다. [클러스터 생성 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation.md)를 참고하세요. `eksctl create nodegroup`은 관리형 그룹이 기본이므로 자체 관리형은 `--managed=false` 또는 해당 `nodeGroups` 구성으로 명시합니다. 생성은 `--version`, `eksctl upgrade nodegroup`은 `--kubernetes-version`을 사용합니다.
1. 검토한 구성으로 green 용량을 만들고 노드 수·대상 kubelet/AMI·Ready·CNI/DNS·애플리케이션 배치를 확인합니다. 노드 목록 조회만으로 검증되지 않습니다.
2. 소유자가 단계적으로 워크로드를 옮깁니다. preferred affinity는 green 배치 보장이 아니므로 실제 Pod 노드와 스토리지 토폴로지를 확인합니다.
3. 식별한 기존 노드 하나씩 유한 timeout으로 drain하며 축출/상태 검사가 실패하면 중단합니다. 워크로드와 대체 용량을 확인한 뒤 다음 노드로 진행합니다.
4. 마이그레이션·상태·데이터 보존·복구 수용 기준을 기록한 뒤에만 기존 그룹을 폐기합니다. 확인하지 않은 반복문이나 고정 sleep 뒤에 자동 삭제하지 않습니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?Set the reviewed cluster context}"
: "${NODE_NAME:?Set one reviewed old node}"
kubectl --context "$KUBE_CONTEXT" get node "$NODE_NAME" -o wide
kubectl --context "$KUBE_CONTEXT" get node "$NODE_NAME" \
-o jsonpath='{.spec.providerID}{"\n"}'
kubectl --context "$KUBE_CONTEXT" get pods --all-namespaces \
--field-selector "spec.nodeName=$NODE_NAME" -o wide
# Stop on failure. Do not terminate the instance or delete the node group here.
kubectl --context "$KUBE_CONTEXT" drain "$NODE_NAME" --ignore-daemonsets --timeout=10m
```
기본 drain은 별도 판단이 필요한 unmanaged/local-data 사례에서 실패하도록 둡니다. `--delete-emptydir-data`는 emptyDir 데이터 손실을 명시적으로 허용하므로 무조건 추가하지 않습니다. `kubectl drain --force`는 unmanaged Pod 허용이며 eviction/PDB 우회와 다릅니다. DaemonSet·static Pod·컨트롤러·영구 데이터는 각 소유자와 조정합니다.
#### 롤링 업그레이드
자체 관리 ASG는 먼저 소유 launch configuration/template을 검증한 AMI로 갱신해야 하며 그렇지 않으면 교체 노드가 이전 이미지로 다시 시작할 수 있습니다. 위 단일 노드 drain 검사를 사용하고 종료 전에 provider ID·EC2 인스턴스·ASG 소유권을 확인합니다. 노드 이름을 private-DNS 검색의 첫 EC2 결과와 임의로 연결하지 않습니다. ASG 소유자가 drain한 인스턴스를 교체하고 **새** 노드의 구성/Ready와 워크로드 복구를 확인한 뒤 진행합니다. 60초 sleep은 교체 준비의 증명이 아닙니다. ASG scaling/instance refresh 자체가 PDB를 집행하지 않으므로 Kubernetes draining 연결이 필요합니다.
#### 인플레이스 업그레이드
이를 지원하는 자체 관리 호스트에서만 별도 검증한 OS/이미지별 절차를 사용합니다. EKS 최적화 관리형 fleet은 검증한 불변 AMI 교체 경로를 우선 검토하고 Auto Mode/Fargate 인프라는 서비스 소유입니다. 일반적인 `yum update kubelet kubectl`은 검토한 Kubernetes/runtime 버전 선택이나 EKS 노드 업그레이드 절차가 아닙니다. SSM 요청은 비동기이므로 invocation·필요한 서비스 재시작·실제 버전·노드 readiness·워크로드 테스트 성공 전 uncordon하지 않습니다. 오류 후 자동 uncordon/종료 대신 실패 증거를 보존합니다.
### 노드 업그레이드 모니터링 및 검증
#### 노드 버전 확인
노드 Kubernetes 버전을 확인합니다:
```bash
kubectl --context "$KUBE_CONTEXT" get nodes -o custom-columns=NAME:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion
```
#### 노드 상태 확인
노드 상태를 확인합니다:
```bash
kubectl --context "$KUBE_CONTEXT" get nodes
kubectl --context "$KUBE_CONTEXT" describe nodes
```
#### 파드 배포 확인
파드가 정상적으로 배포되었는지 확인합니다:
```bash
kubectl --context "$KUBE_CONTEXT" get pods --all-namespaces -o wide
kubectl --context "$KUBE_CONTEXT" get pods --all-namespaces -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,PHASE:.status.phase,READY:'.status.conditions[?(@.type=="Ready")].status'
```
## 애드온 업그레이드
실제 구성 요소의 소유자와 업그레이드 경로를 조사합니다. EKS 관리형 애드온 버전은 CP 업그레이드만으로 자동 갱신되지 않으며 소유자가 호환 버전을 선택해 요청합니다. 필요한 중간 릴리스는 CP 변경 전에 적용할 수 있습니다. Auto Mode 기본 기능은 AWS가 별도로 관리하므로 모든 클러스터가 같은 DaemonSet/Deployment를 사용한다고 가정해 네트워크·스토리지·DNS 구성 요소를 중복 설치/업데이트하지 않습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-08-eks-upgrades-4.html)
### AWS 관리형 애드온
#### 관리형 애드온 목록 확인
클러스터에 설치된 관리형 애드온을 확인합니다:
```bash
aws eks list-addons --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION"
```
#### 관리형 애드온 버전 확인
관리형 애드온의 현재 버전을 확인합니다:
```bash
aws eks describe-addon \
--cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--addon-name vpc-cni \
--query "addon.addonVersion"
```
#### 사용 가능한 애드온 버전 확인
사용 가능한 애드온 버전을 확인합니다:
배열 첫 원소가 “최신” 또는 올바른 버전이라는 보장은 없습니다. `compatibilities`, default-version 표시, 플랫폼/compute/architecture 지원, 릴리스 노트, IAM 변경, 중간 버전 요구를 검토합니다. 변경 전에 구성과 identity 연결을 기록하고 민감 값이 있을 수 있는 구성 파일의 접근을 제한합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${ADDON_NAME:?}"
: "${TARGET_VERSION:?Set the Kubernetes version for this stage}"
umask 077
aws eks describe-addon --cluster-name "$CLUSTER_NAME" --addon-name "$ADDON_NAME" \
--region "$AWS_REGION" --output json > addon-before.json
aws eks describe-addon-versions --addon-name "$ADDON_NAME" \
--kubernetes-version "$TARGET_VERSION" --region "$AWS_REGION" \
--output json > addon-candidates.json
# Select ADDON_VERSION after reviewing compatibility, architecture, compute type, and upgrade path.
: "${ADDON_VERSION:?Set the reviewed add-on version}"
aws eks describe-addon-configuration --addon-name "$ADDON_NAME" \
--addon-version "$ADDON_VERSION" --region "$AWS_REGION" \
--output json > addon-schema.json
jq -r '.addon.configurationValues // "{}"' addon-before.json > addon-config-candidate.json
```
#### 관리형 애드온 업그레이드
AWS Management Console, AWS CLI 또는 eksctl을 사용하여 관리형 애드온을 업그레이드할 수 있습니다:
대상 스키마로 후보 JSON을 검증하고 의도한 구성/identity 의미를 보존합니다. `PRESERVE`는 관리 필드 충돌을 다루며 모든 사용자 값 검증, 애플리케이션 동작 보장, 명시한 부분 JSON과 기존 JSON의 병합을 뜻하지 않습니다. `{}` 전달은 구성을 초기화할 수 있습니다. 소유 애드온 하나씩 요청하고 update ID·health·버전·워크로드 동작을 확인합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${ADDON_NAME:?}"; : "${ADDON_VERSION:?}"
: "${REVIEWED_ADDON_CONFIG_FILE:?Provide the complete reviewed target configuration JSON}"
aws eks update-addon --cluster-name "$CLUSTER_NAME" --addon-name "$ADDON_NAME" \
--addon-version "$ADDON_VERSION" --region "$AWS_REGION" \
--configuration-values "file://$REVIEWED_ADDON_CONFIG_FILE" \
--resolve-conflicts PRESERVE --output json > addon-update.json
UPDATE_ID=$(jq -er '.update.id | select(type == "string" and length > 0)' addon-update.json)
export CLUSTER_NAME AWS_REGION UPDATE_ID ADDON_NAME
unset NODEGROUP_NAME
python3 eks-wait-update.py
aws eks describe-addon --cluster-name "$CLUSTER_NAME" --addon-name "$ADDON_NAME" \
--region "$AWS_REGION" --output json
```
eksctl 0.229의 `update addon`에는 `--wait`와 `--config-file`이 있지만 `--preserve`는 없습니다. 의도한 충돌 정책을 검토한 eksctl 구성에 넣거나 위 AWS CLI 절차를 사용합니다. eksctl의 `--force`는 자체 관리 애드온의 소유권 이전이며 일반적인 충돌 보존 옵션이 아닙니다.
### 자체 관리형 애드온
#### 자체 관리형 애드온 업그레이드
Helm 또는 kubectl을 사용하여 자체 관리형 애드온을 업그레이드합니다:
기존 Helm/매니페스트 소유자를 사용합니다. 예시는 검토한 chart 저장소/OCI와 기존 릴리스를 전제로 하며, 릴리스가 없을 때 두 번째 관리자를 생성하지 않도록 `--install`을 사용하지 않습니다. 새 chart의 CRD/IAM 마이그레이션을 확인하며 `helm upgrade`만으로 chart `crds/` 디렉토리의 CRD가 갱신되지는 않습니다. Helm과 원시 매니페스트 배포는 대안이지 연속 업데이트 단계가 아닙니다. 이전 Metrics Server 0.6.1/3.8.2 예시는 대상 버전 호환성을 선택한 결과가 아닙니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${RELEASE:?}"; : "${ADDON_NAMESPACE:?}"
: "${CHART_REF:?Set the verified repository/chart or OCI reference}"
: "${CHART_VERSION:?Set a reviewed compatible chart version}"
: "${REVIEWED_VALUES_FILE:?Set the complete reviewed target values file}"
umask 077
helm get values "$RELEASE" --namespace "$ADDON_NAMESPACE" --kube-context "$KUBE_CONTEXT" \
--all > addon-values-before.yaml
helm show values "$CHART_REF" --version "$CHART_VERSION" > addon-values-defaults.yaml
# Merge/migrate values and handle CRDs/IAM through the owner before this step.
helm upgrade "$RELEASE" "$CHART_REF" --version "$CHART_VERSION" \
--namespace "$ADDON_NAMESPACE" --kube-context "$KUBE_CONTEXT" \
-f "$REVIEWED_VALUES_FILE" --wait --timeout 15m
```
### 주요 애드온 업그레이드 가이드
#### CoreDNS 업그레이드
표준 노드는 CoreDNS Deployment의 소유자가 EKS인지 다른 도구인지 확인하고 Corefile·PDB·사용자 설정을 보존하며 선택한 버전의 마이그레이션 안내를 따릅니다. Auto Mode 노드는 CoreDNS를 노드 system service로 실행하므로 순수 Auto Mode에는 Deployment 애드온이 필요하지 않지만 혼합 클러스터는 비-Auto 노드의 DNS를 유지해야 합니다. 순수 Auto Mode에서 Deployment가 없다고 업그레이드 실패로 판단하지 않습니다.
#### kube-proxy 업그레이드
API server와 해당 노드에 호환되는 kube-proxy를 사용하며 API server보다 새로울 수 없습니다. 대상 버전의 지원 애드온 build와 검증한 순서를 사용합니다. Auto Mode의 서비스 네트워킹은 AWS가 관리하므로 자체 kube-proxy DaemonSet이 있다고 가정하지 않습니다.
#### VPC CNI 업그레이드
표준 EC2 노드는 CNI 버전 경로, IP/prefix 모드, IAM, network-policy 설정, 워크로드 연결을 검증합니다. `aws-node` ConfigMap 하나가 모든 설정을 담는다고 가정하지 말고 EKS `configurationValues`, DaemonSet/컨테이너 설정, service-account identity, 해당 Helm/GitOps 소유 구성을 포함합니다. Auto Mode 기본 네트워킹은 별도 관리 경로를 따릅니다.
각 EKS 소유 애드온에 위 capture/select/update/wait 절차를 사용하며 표준 노드 agent는 실제 설치된 환경에서만 조회합니다.
```bash
kubectl --context "$KUBE_CONTEXT" -n kube-system get deployment coredns -o wide
kubectl --context "$KUBE_CONTEXT" -n kube-system get daemonset kube-proxy aws-node -o wide
```
### 애드온 업그레이드 문제 해결
#### 일반적인 문제
애드온 업그레이드 중 발생할 수 있는 일반적인 문제:
- **구성 충돌**: 사용자 정의 구성과 새 버전 간의 충돌
- **호환성 문제**: 애드온과 Kubernetes 버전 간의 호환성 문제
- **리소스 제약**: 업그레이드에 필요한 리소스 부족
#### 문제 해결 단계
1. 애드온 상태 확인:
```bash
aws eks describe-addon \
--cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--addon-name vpc-cni
```
2. 애드온 로그 확인:
```bash
kubectl --context "$KUBE_CONTEXT" logs -n kube-system -l k8s-app=kube-dns
kubectl --context "$KUBE_CONTEXT" logs -n kube-system -l k8s-app=kube-proxy
kubectl --context "$KUBE_CONTEXT" logs -n kube-system -l k8s-app=aws-node
```
3. 애드온 이벤트 확인:
```bash
kubectl --context "$KUBE_CONTEXT" get events -n kube-system --sort-by='.lastTimestamp'
```
## 업그레이드 검증 및 문제 해결
업그레이드가 완료된 후에는 클러스터가 정상적으로 작동하는지 검증하고 발생할 수 있는 문제를 해결해야 합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-08-eks-upgrades-5.html)
### 업그레이드 검증
#### 클러스터 버전 확인
클러스터 및 노드 버전을 확인합니다:
```bash
# 클러스터 버전 확인
kubectl --context "$KUBE_CONTEXT" version --output=yaml
# 노드 버전 확인
kubectl --context "$KUBE_CONTEXT" get nodes -o custom-columns=NAME:.metadata.name,VERSION:.status.nodeInfo.kubeletVersion
```
#### 클러스터 상태 확인
클러스터 구성 요소의 상태를 확인합니다:
```bash
# 노드 상태 확인
kubectl --context "$KUBE_CONTEXT" get nodes
# 파드 상태 확인
kubectl --context "$KUBE_CONTEXT" get pods --all-namespaces
# 네임스페이스 상태 확인
kubectl --context "$KUBE_CONTEXT" get namespaces
# 서비스 상태 확인
kubectl --context "$KUBE_CONTEXT" get services --all-namespaces
```
#### 워크로드 검증
애플리케이션 워크로드가 정상적으로 작동하는지 확인합니다:
```bash
# 배포 상태 확인
kubectl --context "$KUBE_CONTEXT" get deployments --all-namespaces
# 스테이트풀셋 상태 확인
kubectl --context "$KUBE_CONTEXT" get statefulsets --all-namespaces
# 데몬셋 상태 확인
kubectl --context "$KUBE_CONTEXT" get daemonsets --all-namespaces
# 서비스 엔드포인트 확인
kubectl --context "$KUBE_CONTEXT" get endpointslices.discovery.k8s.io --all-namespaces
```
#### 기능 테스트
격리한 소유 테스트 리소스와 명시적인 수용 기준을 사용합니다. 다음 **Linux EC2 노드 smoke test**는 Deployment rollout/확장, Service/DNS 요청, 별도 Job 사이의 PVC 데이터 지속성을 확인합니다. 부하 벤치마크나 모든 노드/AZ 검사, 애플리케이션/HA/보안 정확성의 증명은 아닙니다. 워크로드별 ingress/egress·컨트롤러/웹훅·복구 검사를 추가하고 Fargate/hybrid 등 다른 플랫폼은 호환되는 별도 계획을 사용합니다.
`eks-upgrade-smoke.py`로 저장합니다. 이번 감사에서 manifest를 확인한 공식 BusyBox digest를 사용하지만 이미지/클러스터 테스트를 실제 실행한 것은 아닙니다. 실행 전에 이미지 정책, admission/network 규칙, 용량, 대상 노드의 호환 filesystem StorageClass를 확인합니다. `KUBE_CONTEXT`, `TEST_STORAGE_CLASS`, `RUN_SMOKE_TEST=yes`와 특정 교체 노드 검사용 `SMOKE_NODE_SELECTOR` JSON map을 설정하세요. 고유 namespace를 생성하고 기존 것을 인수하지 않으며, 실패 시 중단하고 실제 배치/PV reclaim 정보를 기록합니다. `WaitForFirstConsumer` 볼륨에는 완료를 기다리기 전에 writer 소비자를 생성합니다.
리소스는 기본적으로 검토용으로 남깁니다. `CLEANUP_ON_SUCCESS=yes`는 성공한 후 해당 실행이 생성한 namespace UID만 삭제합니다. Retain 클래스는 namespace 삭제 뒤에도 과금 PV/스토리지를 남길 수 있으므로 기록한 PV를 확인해 소유 테스트 스토리지만 정리합니다. 실패는 진단용으로 보존하며 성공으로 표시하지 않습니다. 현재 메트릭과 순차 HTTP 20회로 성능 동등성을 입증하지 않습니다.
```python
import json
import os
import subprocess
import uuid
from pathlib import Path
IMAGE = "docker.io/library/busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662"
def manifests(namespace, storage_class, marker, node_selector):
if node_selector.get("kubernetes.io/os", "linux") != "linux":
raise ValueError("This BusyBox example requires Linux nodes")
node_selector = {"kubernetes.io/os": "linux", **node_selector}
security = {"runAsNonRoot": True, "runAsUser": 65532, "fsGroup": 65532,
"seccompProfile": {"type": "RuntimeDefault"}}
container_security = {"allowPrivilegeEscalation": False, "readOnlyRootFilesystem": True,
"capabilities": {"drop": ["ALL"]}}
resources = {"requests": {"cpu": "50m", "memory": "32Mi"},
"limits": {"cpu": "200m", "memory": "128Mi"}}
def job(name, command, with_volume=False):
container = {"name": "check", "image": IMAGE, "command": ["sh", "-ec", command],
"resources": resources, "securityContext": container_security}
pod = {"restartPolicy": "Never", "automountServiceAccountToken": False,
"securityContext": security, "nodeSelector": node_selector, "containers": [container]}
if with_volume:
container["volumeMounts"] = [{"name": "data", "mountPath": "/data"}]
pod["volumes"] = [{"name": "data", "persistentVolumeClaim": {"claimName": "smoke-data"}}]
return {"apiVersion": "batch/v1", "kind": "Job", "metadata": {"name": name, "namespace": namespace},
"spec": {"backoffLimit": 0, "activeDeadlineSeconds": 120,
"template": {"spec": pod}}}
config = {"apiVersion": "v1", "kind": "ConfigMap",
"metadata": {"name": "smoke-content", "namespace": namespace},
"data": {"index.html": marker + "\n"}}
deployment = {
"apiVersion": "apps/v1", "kind": "Deployment",
"metadata": {"name": "smoke-http", "namespace": namespace},
"spec": {"replicas": 2, "selector": {"matchLabels": {"app": "smoke-http"}},
"template": {"metadata": {"labels": {"app": "smoke-http"}}, "spec": {
"automountServiceAccountToken": False, "securityContext": security,
"nodeSelector": node_selector,
"containers": [{"name": "http", "image": IMAGE,
"command": ["httpd", "-f", "-p", "8080", "-h", "/www"],
"securityContext": container_security, "resources": resources,
"ports": [{"name": "http", "containerPort": 8080}],
"readinessProbe": {"httpGet": {"path": "/", "port": "http"}},
"volumeMounts": [{"name": "content", "mountPath": "/www", "readOnly": True}]}],
"volumes": [{"name": "content", "configMap": {"name": "smoke-content"}}],
}}},
}
service = {"apiVersion": "v1", "kind": "Service",
"metadata": {"name": "smoke-http", "namespace": namespace},
"spec": {"selector": {"app": "smoke-http"}, "ports": [{"port": 80, "targetPort": "http"}]}}
pvc = {"apiVersion": "v1", "kind": "PersistentVolumeClaim",
"metadata": {"name": "smoke-data", "namespace": namespace},
"spec": {"storageClassName": storage_class, "accessModes": ["ReadWriteOnce"],
"resources": {"requests": {"storage": "1Gi"}}}}
writer = job("smoke-write", f"printf '%s\\n' '{marker}' > /data/marker; sync", True)
reader = job("smoke-read", f"test \"$(cat /data/marker)\" = '{marker}'", True)
http = job("smoke-request", f"i=0; while [ \"$i\" -lt 20 ]; do "
f"test \"$(wget -T 5 -q -O - http://smoke-http)\" = '{marker}'; "
"i=$((i + 1)); sleep 1; done")
return [config, deployment, service, pvc, writer], reader, http
def run_smoke(context, storage_class, node_selector, call=subprocess.run):
if not context or not storage_class or not isinstance(node_selector, dict):
raise ValueError("Set the reviewed context, StorageClass, and node selector map")
namespace = "eks-upgrade-smoke-" + uuid.uuid4().hex[:12]
marker = uuid.uuid4().hex
evidence = Path(namespace)
evidence.mkdir()
prefix = ["kubectl", "--context", context]
def kubectl(*args, payload=None):
completed = call(prefix + list(args), input=json.dumps(payload) if payload else None,
check=True, capture_output=True, text=True, timeout=240)
return completed.stdout
# Create, never apply/adopt, the namespace. A failed creation must not lead to deletion.
created = json.loads(kubectl("create", "namespace", namespace, "--output", "json"))
namespace_uid = created["metadata"]["uid"]
(evidence / "namespace.json").write_text(json.dumps(created, indent=2) + "\n")
print(f"Owned smoke namespace: {namespace}; preserve evidence and inspect PV reclamation before cleanup.", flush=True)
try:
initial, reader, http = manifests(namespace, storage_class, marker, node_selector)
kubectl("create", "-f", "-", payload={"apiVersion": "v1", "kind": "List", "items": initial})
# The writer is a PVC consumer, so WaitForFirstConsumer provisioning can proceed.
kubectl("-n", namespace, "wait", "--for=condition=Complete", "job/smoke-write", "--timeout=180s")
kubectl("-n", namespace, "delete", "job", "smoke-write", "--cascade=foreground",
"--wait=true", "--timeout=60s")
kubectl("create", "-f", "-", payload=reader)
kubectl("-n", namespace, "wait", "--for=condition=Complete", "job/smoke-read", "--timeout=180s")
kubectl("-n", namespace, "rollout", "status", "deployment/smoke-http", "--timeout=180s")
kubectl("-n", namespace, "scale", "deployment/smoke-http", "--replicas=3")
kubectl("-n", namespace, "rollout", "status", "deployment/smoke-http", "--timeout=180s")
kubectl("create", "-f", "-", payload=http)
kubectl("-n", namespace, "wait", "--for=condition=Complete", "job/smoke-request", "--timeout=180s")
pods = json.loads(kubectl("-n", namespace, "get", "pods", "--output", "json"))
claim = json.loads(kubectl("-n", namespace, "get", "pvc", "smoke-data", "--output", "json"))
volume = json.loads(kubectl("get", "pv", claim["spec"]["volumeName"], "--output", "json"))
result = {"namespace": namespace, "namespaceUID": namespace_uid, "nodeSelector": node_selector,
"checks": ["PVC write/read across Jobs", "Deployment rollout/scale", "20 HTTP/DNS requests"],
"pods": pods, "pvc": claim, "pv": volume,
"limits": "A bounded smoke test, not a load benchmark, HA proof, or full application validation."}
(evidence / "result.json").write_text(json.dumps(result, indent=2) + "\n")
except Exception:
print(f"Smoke test failed; namespace {namespace} retained for diagnosis.", flush=True)
raise
print(f"Smoke checks completed. Evidence: {evidence}/result.json", flush=True)
if os.environ.get("CLEANUP_ON_SUCCESS") == "yes":
current = json.loads(kubectl("get", "namespace", namespace, "--output", "json"))
if current["metadata"]["uid"] != namespace_uid:
raise RuntimeError("Namespace identity changed; refusing cleanup")
kubectl("delete", "namespace", namespace, "--wait=true", "--timeout=180s")
print("Namespace deleted; inspect the recorded PV reclaim policy for retained billable storage.", flush=True)
return namespace
if __name__ == "__main__":
if os.environ.get("RUN_SMOKE_TEST") != "yes":
raise SystemExit("Set RUN_SMOKE_TEST=yes only for the approved test scope")
os.umask(0o077)
run_smoke(os.environ["KUBE_CONTEXT"], os.environ["TEST_STORAGE_CLASS"],
json.loads(os.environ.get("SMOKE_NODE_SELECTOR", "{}")))
```
### 업그레이드 문제 해결
#### 일반적인 업그레이드 문제
업그레이드 중 발생할 수 있는 일반적인 문제:
1. **컨트롤 플레인 업그레이드 실패**:
- API 서버 가용성 문제
- etcd 데이터베이스 문제
- IAM 권한 문제
2. **노드 업그레이드 문제**:
- 노드 드레이닝 실패
- 새 노드 시작 실패
- kubelet 버전 불일치
3. **애드온 업그레이드 문제**:
- 구성 충돌
- 호환성 문제
- 리소스 제약
4. **워크로드 문제**:
- API 사용 중단으로 인한 워크로드 실패
- 리소스 제약으로 인한 파드 스케줄링 실패
- 네트워킹 문제
#### 문제 해결 단계
1. **로그 확인**:
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${KUBE_CONTEXT:?}"
: "${START_TIME_MS:?Set the reviewed start timestamp in epoch milliseconds}"
: "${END_TIME_MS:?Set the reviewed analysis-window end timestamp}"
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query 'cluster.logging' --output json --no-cli-pager
# Read existing logs; enabling logging is a separate change and does not backfill history.
aws logs filter-log-events --region "$AWS_REGION" \
--log-group-name "/aws/eks/$CLUSTER_NAME/cluster" \
--start-time "$START_TIME_MS" --end-time "$END_TIME_MS" \
--max-items 200 --output json --no-cli-pager
# Standard EC2-node agents only, where installed; these are Pod logs, not all host journals.
kubectl --context "$KUBE_CONTEXT" -n kube-system logs -l k8s-app=kube-proxy \
--all-containers=true --prefix=true --since=15m --tail=100
kubectl --context "$KUBE_CONTEXT" -n kube-system logs -l k8s-app=aws-node \
--all-containers=true --prefix=true --since=15m --tail=100
```
2. **이벤트 확인**:
```bash
kubectl --context "$KUBE_CONTEXT" get events --all-namespaces --sort-by='.lastTimestamp'
```
3. **리소스 상태 확인**:
```bash
kubectl --context "$KUBE_CONTEXT" describe nodes
kubectl --context "$KUBE_CONTEXT" -n "$WORKLOAD_NAMESPACE" get deployments,statefulsets,daemonsets,pods -o wide
```
4. **API 버전 확인**:
```bash
kubectl --context "$KUBE_CONTEXT" api-versions
```
#### 롤백 절차
EKS 버전 롤백은 2026년 7월 도입된 실제 사용자 요청 작업입니다. 과거의 일괄적인 “다운그레이드 불가” 설명 대신 [현재 롤백 가이드](https://docs.aws.amazon.com/eks/latest/userguide/rollback-cluster.html)를 따릅니다. Kubernetes 마이너를 되돌리는 작업이며 이전 etcd/애플리케이션/PV 데이터 스냅샷을 복원하거나 애드온을 자동 되돌리지 않습니다. 플랫폼 버전은 이전 마이너의 최신 플랫폼 버전이 됩니다.
**자격과 준비**
- **in-place 업그레이드 완료 후 7일 이내**에 시작해야 합니다. 현재 버전으로 처음 생성한 클러스터는 대상이 아니며 바로 이전 마이너만 허용합니다. 연쇄 롤백으로 더 오래된 마이너까지 내려갈 수 없습니다.
- 대상 버전은 지원 중이어야 합니다. 확장 지원 대상은 먼저 `EXTENDED` 정책으로 바꾸고 해당 요금을 고려합니다. 확장 지원 종료 시 자동 업그레이드는 되돌릴 수 없으며 표준 지원 종료 자동 업그레이드에는 문서화된 확장 정책 조건이 적용됩니다.
- 클러스터는 충돌하는 업데이트 없이 ACTIVE여야 합니다. 새 버전에서 활성화한 하위 비호환 EKS 기능은 롤백을 막을 수 있으며 `--force`로 이 전제 조건을 우회할 수 없습니다.
- 롤백 insight, 애플리케이션/클라이언트/CRD/웹훅 호환성, 노드 skew, 애드온을 검토합니다. 검사는 특정 시점의 best effort이므로 롤백 도중 비호환 변경을 추가하지 않습니다.
`ROLLBACK_READINESS`의 ERROR/UNKNOWN은 롤백을 막고 WARNING은 권고입니다. 일반 upgrade insight의 일시 철회된 강제 적용과 다릅니다. 롤백 `--force`는 ERROR/WARNING/UNKNOWN insight 검사를 우회하지만 위험한 계획을 안전하게 만들거나 자격/Auto Mode disruption 제어를 우회하지 않습니다. 가능하면 원인을 해결하고 override는 명시적인 위험 판단으로 취급하며 정상 예시에서는 사용하지 않습니다.
```bash
aws eks list-insights --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--filter '{"categories":["ROLLBACK_READINESS"]}' --output json --no-cli-pager
# Set INSIGHT_ID from the response to inspect one finding.
aws eks describe-insight --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--id "$INSIGHT_ID" --output json --no-cli-pager
kubectl --context "$KUBE_CONTEXT" get nodes -o wide
```
**컨트롤 플레인 전에 노드와 애드온 준비**
현재 [관리형 노드 그룹 가이드](https://docs.aws.amazon.com/eks/latest/userguide/update-managed-node-group.html)는 `UpdateNodegroupVersion`을 통한 롤백을 명시합니다. CP를 낮추기 전에 수행하고 실제 노드/AMI 버전을 검증합니다. 일부 API 개요에는 과거의 롤백 불가 문구가 남아 있지만 현재 롤백 가이드가 이 버전 복구 절차를 설명합니다. 임의의 과거 AMI로 항상 내릴 수 있다는 뜻은 아니며 custom AMI 그룹은 검토한 원래 launch-template 경로가 필요합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${NODEGROUP_NAME:?}"
: "${ROLLBACK_VERSION:?Set the reviewed previous minor for the eligible rollback}"
aws eks update-nodegroup-version \
--cluster-name "$CLUSTER_NAME" --nodegroup-name "$NODEGROUP_NAME" --region "$AWS_REGION" \
--kubernetes-version "$ROLLBACK_VERSION" --output json --no-cli-pager > node-rollback-update.json
UPDATE_ID=$(jq -er '.update.id | select(type == "string" and length > 0)' node-rollback-update.json)
export CLUSTER_NAME AWS_REGION UPDATE_ID NODEGROUP_NAME
unset ADDON_NAME
python3 eks-wait-update.py
```
자체 관리/hybrid 노드는 소유자가 처리하고 Auto Mode는 자신의 노드를 CP보다 먼저 자동 처리합니다. Fargate kubelet은 제자리 롤백할 수 없습니다. HPA/GitOps/Job 등을 포함한 컨트롤러와 가용성을 조율하여 비호환 Fargate Pod가 새 버전으로 즉시 재생성되지 않게 해야 합니다. 검증한 마이그레이션/유지관리 계획을 사용하고 CP 복구 후 재생성합니다. 단순 Pod 삭제 반복문으로 해결되지 않으며 skew insight를 강제로 넘겨도 새 kubelet이 이전 API server에서 지원되는 것은 아닙니다.
EKS는 애드온 버전을 되돌리지 않습니다. 호환되는 중간/이전 버전과 검토한 구성을 선택하고 각 update ID 및 기능을 확인합니다. 자체 관리 컨트롤러/CRD도 별도 평가해야 합니다. 일괄 OVERWRITE나 추측한 오래된 AL2 노드 그룹을 롤백 계획으로 사용하지 않습니다.
**컨트롤 플레인 롤백 요청과 관찰**
승인한 이전 마이너로 `update-cluster-version`을 사용하며 별도의 `rollback-cluster` 명령은 없습니다. 업그레이드 요청의 대안이지 모든 업데이트 뒤에 자동 실행할 단계가 아닙니다. 적절한 클라이언트 대기 시간을 명시하고 반환된 update ID를 보존합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${ROLLBACK_VERSION:?}"
: "${WAIT_TIMEOUT_SECONDS:?Set an explicit client wait for the approved rollback}"
aws eks update-cluster-version --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--kubernetes-version "$ROLLBACK_VERSION" --output json --no-cli-pager > rollback-update.json
UPDATE_ID=$(jq -er '.update.id | select(type == "string" and length > 0)' rollback-update.json)
export CLUSTER_NAME AWS_REGION UPDATE_ID WAIT_TIMEOUT_SECONDS
unset NODEGROUP_NAME ADDON_NAME
python3 eks-wait-update.py
```
**Auto Mode의 시간과 취소**
Auto Mode 노드 롤백 중에는 클러스터가 **ACTIVE**로 남고 CP 단계에서 UPDATING이 됩니다. 전 과정에서 update ID를 추적합니다. NodePool drift budget과 노드의 do-not-disrupt는 교체를 막을 수 있고 PDB/Pod의 do-not-disrupt는 `terminationGracePeriod` 조건 아래 지연시킵니다. 절대적인 애플리케이션 가용성 보장이 아니며 `--force`로 이런 제어를 우회하지 않습니다.
Auto Mode의 `rollbackConfig.timeoutMinutes`는 기본 **720**, 허용 범위 **120–10080**입니다. 정확한 종료 시각이 아닌 최소 시간 경계입니다. timeout이면 업데이트가 실패하고 CP는 현재 버전에 남으며 노드는 그 버전으로 다시 drift합니다. 재시도에도 원래의 7일 시작 자격 기간이 적용됩니다. 다음 옵션은 CLI 2.36.44에서 로컬 파서로 확인했습니다.
```bash
# Alternative Auto Mode request: do not run this as a second request after the preceding one.
# Select a reviewed timeout from 120 to 10080 minutes; default is 720 when omitted.
aws eks update-cluster-version --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--kubernetes-version "$ROLLBACK_VERSION" --rollback-config timeoutMinutes=720 \
--output json --no-cli-pager > rollback-update.json
```
Auto Mode 요청도 반환 ID와 같은 polling helper를 사용합니다. helper 기본 2시간은 서비스 기본 12시간보다 짧으므로 클라이언트 기한을 의도적으로 선택해야 합니다. CI/IaC/자격 증명 timeout은 AWS 작업을 취소하지 않습니다. CancelUpdate는 CP 롤백 전 Auto Mode 노드 단계에서만 가능하며 best effort이고 이미 중단 중인 노드는 작업을 마칩니다. Cancelling → Cancelled와 이후 노드 수렴을 확인합니다. 일반 CP 업그레이드나 시작한 CP 롤백을 이 방법으로 취소할 수는 없습니다.
```bash
# Only during the cancellable Auto Mode node phase, before control-plane rollback starts.
aws eks cancel-update --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--update-id "$UPDATE_ID" --output json --no-cli-pager
```
CloudFormation stack rollback이나 Git revert가 EKS 버전 롤백을 자동 시작하지 않습니다. 명시적인 복구 후 관측한 버전과 IaC state/plan을 조정합니다. 롤백 자격이 없으면 전진 수정 또는 지원 버전의 새 클러스터와 검증한 이전/복원을 평가합니다. 어느 방식도 DB 마이그레이션이나 애플리케이션 데이터 변경을 자동 취소하지 않습니다.
## 업그레이드 자동화
대규모 환경에서는 업그레이드 프로세스를 자동화하는 것이 중요합니다. 다음과 같은 도구와 방법을 사용하여 EKS 업그레이드를 자동화할 수 있습니다.
### eksctl을 사용한 자동화
검토한 eksctl 구성과 작업별 올바른 플래그를 사용합니다. `upgrade cluster --approve`는 CP 변경을 요청하며 `--approve`가 없으면 미리보기입니다. 노드 그룹 생성은 `--version`, `upgrade nodegroup`은 `--kubernetes-version`을 사용합니다. 작업별 소유자/도구 하나를 선택하고 실제 EKS 업데이트 및 워크로드 상태를 검증하며 AWS CLI와 eksctl 업데이트를 중복 연속 요청하지 않습니다.
### AWS CLI 및 스크립트를 사용한 자동화
위의 읽기 전용 preflight, 정확한 update polling, 구성 요소별 요청 예시를 조합할 수 있습니다. 오케스트레이터는 결과를 보존하고 실패·timeout·검증 미완료에서 멈춰야 합니다. 배열 순서가 아닌 승인한 애드온/AMI/구성 버전을 사용하고 custom AMI 및 Auto/Fargate 차이를 처리하며 클라이언트 실패 후 update ID로 추적을 재개합니다. 리소스가 ACTIVE가 되었다는 이유로 전체 업그레이드 성공을 선언하지 않습니다. 다음 진입점은 조사만 수행하고 업그레이드를 시작하지 않습니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${EXPECTED_ACCOUNT_ID:?}"
: "${KUBE_CONTEXT:?}"; : "${TARGET_VERSION:?}"
export CLUSTER_NAME AWS_REGION EXPECTED_ACCOUNT_ID KUBE_CONTEXT TARGET_VERSION
umask 077
python3 eks-upgrade-preflight.py > preflight.json
# Review this evidence and the workload/backup/capacity plan before a separate change step.
```
### GitOps를 사용한 자동화
Git에 원하는 버전, 호환성 증거, 검토한 runbook을 저장할 수 있습니다. Argo CD/Flux가 eksctl `ClusterConfig` 파일을 기본적으로 EKS CP 변경으로 변환하지는 않으며 적절한 권한의 AWS-aware controller/runner가 필요합니다. Git/CloudFormation rollback도 EKS 버전 업그레이드를 자동 역전하지 않습니다.
다음 workflow는 **읽기 전용 준비 자료 수집**입니다. 사용 전 위의 전체 `eks-upgrade-preflight.py`를 `runbooks/eks-upgrade-preflight.py`에 저장하여 관리합니다. 신뢰하는 private-network runner와 검토한 AWS CLI/kubectl/Python toolchain은 별도로 준비합니다. 고정한 Actions는 Node.js 24를 사용하므로 self-hosted runner가 지원해야 합니다. 저장소 변수와 보호된 `eks-upgrade-review` environment를 구성하고 OIDC trust를 해당 저장소/environment 및 `sts.amazonaws.com`에 제한합니다. 이름을 지정하는 것만으로 reviewer 보호가 생기지 않습니다. 필요한 EKS/STS 조회와 Kubernetes 노드 목록 권한만 가진 조사 역할을 사용하며 클러스터 버전 변경 권한은 필요하지 않습니다.
artifact를 애플리케이션/API/백업/용량 테스트와 함께 검토한 후 별도로 승인한 변경 단계에서 위 구성 요소별 절차를 사용합니다. 수집 성공이 준비 완료나 검증된 운영 업그레이드를 뜻하지 않습니다. 인프라/insight 메타데이터에 맞게 artifact 접근을 제한하며 격리한 kubeconfig는 업로드하지 않습니다.
```yaml
name: Inspect EKS upgrade readiness
'on':
workflow_dispatch:
inputs:
target_version:
description: Reviewed next EKS minor version; inspection only
required: true
type: string
permissions:
contents: read
id-token: write
concurrency:
group: eks-readiness-${{ vars.AWS_REGION }}-${{ vars.EKS_CLUSTER_NAME }}
cancel-in-progress: false
jobs:
inspect:
runs-on:
- self-hosted
- linux
- eks-upgrade
environment: eks-upgrade-review
timeout-minutes: 20
env:
CLUSTER_NAME: ${{ vars.EKS_CLUSTER_NAME }}
AWS_REGION: ${{ vars.AWS_REGION }}
EXPECTED_ACCOUNT_ID: ${{ vars.AWS_ACCOUNT_ID }}
KUBE_CONTEXT: eks-upgrade-review
TARGET_VERSION: ${{ inputs.target_version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- name: Assume the scoped review role
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c
with:
role-to-assume: ${{ vars.EKS_REVIEW_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
allowed-account-ids: ${{ env.EXPECTED_ACCOUNT_ID }}
unset-current-credentials: true
- name: Prepare isolated context
shell: bash
run: "set -euo pipefail\numask 077\nEVIDENCE_DIR=\"$RUNNER_TEMP/eks-readiness-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT\"\
\nKUBECONFIG=\"$EVIDENCE_DIR/kubeconfig\"\nexport EVIDENCE_DIR KUBECONFIG\n\
mkdir -m 700 -p \"$EVIDENCE_DIR\"\nprintf 'EVIDENCE_DIR=%s\\nKUBECONFIG=%s\\\
n' \"$EVIDENCE_DIR\" \"$KUBECONFIG\" >> \"$GITHUB_ENV\"\naws eks update-kubeconfig\
\ --name \"$CLUSTER_NAME\" --region \"$AWS_REGION\" \\\n --kubeconfig \"\
$KUBECONFIG\" --alias \"$KUBE_CONTEXT\"\naws --version\nkubectl version --client\
\ --output=json\npython3 --version"
id: prepare
- name: Collect review evidence
shell: bash
run: 'set -euo pipefail
umask 077
python3 runbooks/eks-upgrade-preflight.py > "$EVIDENCE_DIR/preflight.json"'
- name: Preserve the review artifact
if: ${{ always() && steps.prepare.outcome == 'success' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: eks-readiness-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ env.EVIDENCE_DIR }}/preflight.json
if-no-files-found: warn
retention-days: 14
```
### 자동화 모범 사례
EKS 업그레이드 자동화를 위한 모범 사례:
1. **점진적 접근**: 비프로덕션 환경부터 시작하여 프로덕션 환경으로 진행
2. **복구 계획**: 자격 조건을 확인한 검증된 절차를 사용하며 버전/데이터 변경을 무조건 역순 실행하지 않음
3. **검증 단계**: 업그레이드 후 자동 검증 단계 포함
4. **알림**: 업그레이드 성공 또는 실패 시 알림 구성
5. **문서화**: 자동화 프로세스 및 단계 문서화
## 업그레이드 모범 사례
EKS 클러스터 업그레이드를 위한 모범 사례를 살펴보겠습니다.
### 일반적인 모범 사례
#### 업그레이드 계획
1. **버전 선택**: 안정적인 버전 선택 및 릴리스 노트 검토
2. **업그레이드 일정**: 트래픽이 적은 시간에 업그레이드 예약
3. **단계적 접근**: 비프로덕션 환경부터 시작하여 프로덕션 환경으로 진행
4. **롤백 계획**: 문제 발생 시 롤백 계획 수립
#### 업그레이드 준비
1. **백업**: 중요한 데이터 백업
2. **리소스 확보**: 업그레이드에 필요한 충분한 리소스 확보
3. **호환성 확인**: 워크로드 및 애드온 호환성 확인
4. **사용 중단된 API 식별**: 사용 중단된 API를 사용하는 워크로드 식별 및 업데이트
#### 업그레이드 수행
1. **준비**: 대상 변경 전 현재 노드 버전과 필요한 중간 애드온/컨트롤러를 정렬
2. **컨트롤 플레인 대상**: 지원되는 다음 마이너를 요청하고 update ID를 검증
3. **의존 구성 요소**: 검토한 호환 순서대로 노드·나머지 애드온/컨트롤러·클라이언트를 갱신
4. **점진적 노드 업그레이드**: 노드를 점진적으로 업그레이드하여 워크로드 중단 최소화
#### 업그레이드 후
1. **검증**: 클러스터 및 워크로드 상태 검증
2. **모니터링**: 업그레이드 후 클러스터 모니터링
3. **문서화**: 업그레이드 프로세스 및 결과 문서화
4. **학습**: 업그레이드 중 발생한 문제 및 해결 방법 학습
### 대규모 클러스터를 위한 모범 사례
대규모 EKS 클러스터 업그레이드를 위한 추가 모범 사례:
1. **카나리 배포**: 일부 노드 또는 워크로드로 시작하여 점진적으로 확장
2. **자동화**: 업그레이드 프로세스 자동화
3. **모니터링 강화**: 업그레이드 중 클러스터 상태 지속적 모니터링
4. **통신 계획**: 이해관계자에게 업그레이드 상태 정기적으로 통신
5. **통제된 복구**: 증거/자격 검사를 자동화하고 검토한 복구 경로를 적용하며 롤백은 조건부로 수행
### 금융 서비스를 위한 모범 사례
금융 서비스 산업에서 EKS 클러스터 업그레이드를 위한 추가 모범 사례:
1. **규제 준수**: 업그레이드가 규제 요구사항을 충족하는지 확인
2. **위험 평가**: 업그레이드 전 위험 평가 수행
3. **변경 관리**: 엄격한 변경 관리 프로세스 따르기
4. **테스트 강화**: 업그레이드 전 철저한 테스트 수행
5. **문서화 강화**: 업그레이드 프로세스 및 결과 상세 문서화
## 결론
Amazon EKS 클러스터를 성공적으로 업그레이드하려면 철저한 계획, 준비 및 검증이 필요합니다. 이 문서에서는 EKS 클러스터의 컨트롤 플레인, 노드 그룹 및 애드온을 안전하게 업그레이드하기 위한 전략, 단계 및 모범 사례를 다루었습니다.
주요 내용:
1. **EKS 업그레이드 개요**: EKS 버전 관리, 업그레이드 구성 요소 및 경로
2. **업그레이드 계획 및 준비**: 업그레이드 평가, 준비 및 테스트
3. **EKS 컨트롤 플레인 업그레이드**: 컨트롤 플레인 업그레이드 방법 및 모니터링
4. **노드 그룹 업그레이드**: 관리형 및 자체 관리형 노드 그룹 업그레이드 전략
5. **애드온 업그레이드**: AWS 관리형 및 자체 관리형 애드온 업그레이드
6. **업그레이드 검증 및 문제 해결**: 업그레이드 검증 및 일반적인 문제 해결
7. **업그레이드 자동화**: eksctl, AWS CLI 및 GitOps를 사용한 업그레이드 자동화
8. **업그레이드 모범 사례**: 일반적인 모범 사례 및 특정 산업을 위한 모범 사례
EKS 클러스터를 최신 상태로 유지하면 보안 패치, 버그 수정 및 새로운 기능을 활용할 수 있으며, 이는 클러스터의 전반적인 보안, 안정성 및 성능을 향상시킵니다.
## 참고 자료
- [Amazon EKS 업그레이드 문서](https://docs.aws.amazon.com/eks/latest/userguide/update-cluster.html)
- [Kubernetes 버전 및 버전 차이](https://kubernetes.io/docs/setup/release/version-skew-policy/)
- [EKS 관리형 노드 그룹 업그레이드](https://docs.aws.amazon.com/eks/latest/userguide/update-managed-node-group.html)
- [EKS 애드온 업그레이드](https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html)
- [eksctl 문서](https://eksctl.io/usage/cluster-upgrade/)
- [Kubernetes 업그레이드 모범 사례](https://kubernetes.io/docs/tasks/administer-cluster/cluster-upgrade/)
## 퀴즈
이 장에서 배운 내용을 테스트하려면 [주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/08-eks-upgrades-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/09-eks-troubleshooting
----------------------------------------
# Amazon EKS 문제 해결
> **마지막 업데이트**: 2026년 9월 12일
Amazon EKS 클러스터를 운영하다 보면 다양한 문제가 발생할 수 있습니다. 이 문서에서는 EKS 클러스터에서 발생할 수 있는 일반적인 문제와 그 해결 방법을 제공합니다.
## 목차
1. [문제 해결 기본 사항](#문제-해결-기본-사항)
2. [클러스터 생성 및 관리 문제](#클러스터-생성-및-관리-문제)
3. [네트워킹 문제](#네트워킹-문제)
4. [노드 및 파드 문제](#노드-및-파드-문제)
5. [IAM 및 인증 문제](#iam-및-인증-문제)
6. [스토리지 문제](#스토리지-문제)
7. [로깅 및 모니터링 문제](#로깅-및-모니터링-문제)
8. [성능 문제](#성능-문제)
9. [업그레이드 문제](#업그레이드-문제)
10. [일반적인 오류 메시지 및 해결 방법](#일반적인-오류-메시지-및-해결-방법)
## 문제 해결 기본 사항

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-09-eks-troubleshooting-0.html)
### 문제 해결 접근 방식
1. 증상, 영향받는 사용자·워크로드와 장애 시간 범위를 식별합니다.
2. 리소스를 변경하기 전에 관련 상태·로그·이벤트·메트릭을 수집합니다.
3. 근거로 여러 가설을 비교합니다. 일반적인 오류 메시지를 확정 원인으로 취급하지 않습니다.
4. 데이터·가용성 영향을 파악한 표적 수정을 소유자를 통해 적용합니다.
5. 명령 종료 코드뿐 아니라 앱 동작·메트릭으로 복구를 확인합니다.
6. 원인, 변경, 결과, 남은 불확실성과 예방책을 기록합니다.
이 장의 명령은 검토한 환경을 위한 예시이며 위에서 아래로 모두 실행하는 스크립트가 아닙니다. 조회, debug 워크로드 생성, 구성 요소 재시작과 인프라 삭제의 효과는 다릅니다. 이번 검토에서는 실제 AWS·Kubernetes 작업을 실행하지 않았습니다.
### 필수 도구 및 명령어
**기존** 클러스터에서는 계정·리전·클러스터·kubectl 컨텍스트를 지정하고 일치를 확인합니다. 임의의 context 별칭에서 클러스터 이름을 추출하지 않습니다. 사용할 클러스터가 생성되기 전에 실패했다면 생성 절차의 계정·리전·원래 요청·stack 근거를 사용합니다.
```bash
set -euo pipefail
: "${AWS_REGION:?Set the intended Region}"
: "${CLUSTER_NAME:?Set the existing cluster name}"
: "${EXPECTED_ACCOUNT_ID:?Set the intended 12-digit account ID}"
: "${KUBE_CONTEXT:?Set the explicit kubectl context}"
ACTUAL_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
if [ "$ACTUAL_ACCOUNT_ID" != "$EXPECTED_ACCOUNT_ID" ]; then
echo "Account mismatch" >&2; exit 1
fi
CLUSTER_ENDPOINT=$(aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster.endpoint --output text)
KUBE_ENDPOINT=$(kubectl config view --context "$KUBE_CONTEXT" --minify \
-o jsonpath='{.clusters[0].cluster.server}')
if [ "$CLUSTER_ENDPOINT" != "$KUBE_ENDPOINT" ]; then
echo "kubectl context does not match the selected EKS cluster" >&2; exit 1
fi
export AWS_REGION CLUSTER_NAME EXPECTED_ACCOUNT_ID KUBE_CONTEXT
```
#### AWS CLI 및 eksctl
```bash
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query 'cluster.{arn:arn,status:status,version:version,health:health,access:accessConfig}'
aws eks list-nodegroups --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION"
aws eks list-addons --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION"
eksctl get nodegroup --cluster "$CLUSTER_NAME" --region "$AWS_REGION"
```
관리형 노드 그룹과 설치된 EKS 애드온 목록이며 모든 자체 관리 컨트롤러·컴퓨팅 리소스를 포함하지는 않습니다. 절차 선택 전에 실제 소유자와 컴퓨팅 유형을 기록합니다.
#### kubectl
```bash
kubectl --context "$KUBE_CONTEXT" get nodes -o wide
kubectl --context "$KUBE_CONTEXT" get pods -A -o wide
kubectl --context "$KUBE_CONTEXT" get services -A
kubectl --context "$KUBE_CONTEXT" get events -A --sort-by='.metadata.creationTimestamp'
: "${NAMESPACE:?}"; : "${POD_NAME:?}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" describe pod "$POD_NAME"
```
영향받는 namespace·Pod·container·node를 명시적으로 선택합니다. `describe`와 앱 로그에는 민감한 운영 정보가 포함될 수 있습니다. `kubectl auth can-i`는 특정 동작의 인가를 확인하고 `aws sts get-caller-identity`는 AWS 자격 증명 주체를 확인합니다. 어느 하나만으로 네트워크 접근이나 전체 Kubernetes 권한을 증명하지 못합니다.
### 로그 수집 및 분석
#### EKS 컨트롤 플레인 로그
기존 로깅 설정과 제한된 장애 시간 범위를 확인합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"
: "${START_TIME_MS:?Set the incident-window start in epoch milliseconds}"
: "${END_TIME_MS:?Set the incident-window end in epoch milliseconds}"
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster.logging --output json
aws logs filter-log-events --region "$AWS_REGION" \
--log-group-name "/aws/eks/$CLUSTER_NAME/cluster" \
--start-time "$START_TIME_MS" --end-time "$END_TIME_MS" \
--max-items 200 --output json --no-cli-pager
```
컨트롤 플레인 로깅 활성화는 자체 Update ID·권한·CloudWatch 비용이 있는 별도 `UpdateClusterConfig` 변경입니다. 과거 로그를 복구하거나 앱·호스트 로그 수집을 활성화하지는 않습니다. 그룹 부재·조회 거부·빈 시간 범위는 관찰 한계입니다. 앱·호스트 로그는 `/aws/containerinsights//...` 또는 실제 수집기 목적지를 확인합니다.
#### 노드 로그
운영자가 접근할 수 있는 표준 Linux EC2 노드는 `spec.providerID`, 계정·리전과 SSM 전제 조건을 확인한 뒤 세션을 엽니다.
```bash
: "${INSTANCE_ID:?Verify the node EC2 ProviderID and account/Region first}"
aws ssm start-session --target "$INSTANCE_ID" --region "$AWS_REGION"
```
다음은 로컬 터미널이 아니라 **해당 노드 세션 안에서** 실행합니다. systemd·containerd 도구가 있는 이미지를 가정합니다. Bottlerocket, Fargate와 Auto Mode는 지원되는 진단 경로를 사용합니다.
```bash
sudo journalctl -u kubelet --since "15 minutes ago" --no-pager
sudo journalctl -u containerd --since "15 minutes ago" --no-pager
df -h
df -i
free -m
```
현재 EKS 최적화 Linux 노드는 containerd를 사용합니다. 해당 노드의 Docker daemon 로그는 kubelet 런타임 로그가 아닙니다. 이미지 정리·journal 삭제·재시작 전에 로그와 디스크·inode 근거를 보존합니다. 비정상 노드의 `kubectl top` 메트릭 부재만으로 CPU·메모리 고갈을 확정하지 않습니다.
#### 파드 로그
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${POD_NAME:?}"; : "${CONTAINER_NAME:?}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" logs "$POD_NAME" \
-c "$CONTAINER_NAME" --since=15m --tail=200 --timestamps=true
# Run only when a previous container instance exists in this same Pod.
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" logs "$POD_NAME" \
-c "$CONTAINER_NAME" --previous --tail=200 --timestamps=true
```
`--previous`는 **동일 Pod**의 지정 컨테이너에서 이전에 종료된 인스턴스입니다. 삭제된 이전 Pod 로그를 조회하지 못하므로 보존 이력은 로그 백엔드를 사용합니다. 일반·init container 상태, readiness와 종료 이유를 함께 확인합니다.
### 진단 정보 수집
```bash
set -euo pipefail
: "${EVIDENCE_PARENT:?Set an existing private directory}"
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"
umask 077
EVIDENCE_DIR=$(mktemp -d "$EVIDENCE_PARENT/eks-diagnosis.XXXXXXXX")
kubectl --context "$KUBE_CONTEXT" get nodes -o wide > "$EVIDENCE_DIR/nodes.txt"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pods -o wide > "$EVIDENCE_DIR/pods.txt"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get services -o wide > "$EVIDENCE_DIR/services.txt"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get events \
--sort-by='.metadata.creationTimestamp' > "$EVIDENCE_DIR/events.txt"
printf 'Evidence saved to %s; assess the findings before remediation.\n' "$EVIDENCE_DIR"
```
조회 실패 시 예제가 중단되며 기존 파일은 부분 근거입니다. 범위를 제한한 정보로 충분하면 무차별 `cluster-info dump`나 전체 Pod 상세 수집을 피하고 공유 전에 근거를 검토·마스킹합니다.
리소스 압박은 requests·limits, 노드 allocatable과 Metrics Server가 있을 때의 `kubectl top`을 비교합니다. 접근 가능한 노드에서는 `df -h`와 `df -i`를 모두 확인합니다. 여유 바이트가 있어도 inode는 고갈될 수 있습니다. node debug Pod의 루트와 `/host`에 마운트된 호스트 루트는 다르므로 대상 경로 없는 `df -h`는 다른 파일시스템을 볼 수 있습니다. debug Pod 생성에는 namespace·이미지·profile·권한·정리 절차 검토가 필요합니다.
네트워크 진단은 출발 Pod·namespace·node, 목적지·프로토콜·포트를 식별하고 적용 정책과 출발지 resolver부터 확인합니다. 새 debug Pod는 실제 워크로드와 레이블·identity·DNS·경로가 다를 수 있습니다. ICMP ping은 TCP·UDP 앱 접근을 증명하지 못합니다. 공통 이름·미고정 이미지의 `dnsutils`·`netshoot` Pod를 반복 생성하기보다 소유자를 통해 도구·정리를 준비하고 실제 허용 경로에서 제한된 검사를 수행합니다.
출처: [EKS 문제 해결](https://docs.aws.amazon.com/eks/latest/userguide/troubleshooting.html), [Kubernetes 로그](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_logs/), [노드 디버깅](https://kubernetes.io/docs/tasks/debug/debug-cluster/kubectl-node-debug/).
## 클러스터 생성 및 관리 문제

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-09-eks-troubleshooting-1.html)
### 클러스터 생성 실패
#### 일반적인 원인
실패한 요청·CloudFormation 이벤트에서 호출자 권한, 클러스터 서비스 역할 trust·policy, 서비스 할당량, 지원 서브넷·AZ 선택, IP 여유, 이름 충돌과 서비스 가용성을 확인합니다. 이는 가설이며 실제 오류에 따라 가능성이 다릅니다.
#### 문제 해결 단계
```bash
set -euo pipefail
: "${AWS_REGION:?}"; : "${CLUSTER_ROLE_NAME:?}"; : "${VPC_ID:?}"
aws sts get-caller-identity
aws iam get-role --role-name "$CLUSTER_ROLE_NAME" \
--query 'Role.{Arn:Arn,Trust:AssumeRolePolicyDocument}'
aws iam list-attached-role-policies --role-name "$CLUSTER_ROLE_NAME"
aws iam list-role-policies --role-name "$CLUSTER_ROLE_NAME"
aws ec2 describe-subnets --region "$AWS_REGION" --filters "Name=vpc-id,Values=$VPC_ID" \
--query 'Subnets[].{Id:SubnetId,AZ:AvailabilityZone,AZId:AvailabilityZoneId,AvailableIPs:AvailableIpAddressCount,CIDR:CidrBlock}'
aws service-quotas list-service-quotas --service-code eks --region "$AWS_REGION"
aws cloudtrail lookup-events --region "$AWS_REGION" \
--lookup-attributes AttributeKey=EventName,AttributeValue=CreateCluster \
--max-items 20 --output json
```
연결된 정책만으로 호출자의 유효 권한을 알 수 없습니다. inline policy, permissions boundary, session policy와 Organizations 제어도 포함합니다. `AmazonEKSClusterPolicy`는 EKS 클러스터 서비스 역할용입니다. 사용자에게 연결해도 필요한 `eks:CreateCluster`·`iam:PassRole`이나 Kubernetes 접근 권한을 부여하지 않습니다. service-linked role 생성에도 별도 권한·생명주기가 있습니다.
네트워크는 선택한 클러스터 서브넷과 정확한 라우팅·보안 그룹·NACL을 확인합니다. 클러스터 서브넷 요구와 노드·Pod·LB 주소 용량은 별도 계획입니다. 사설 클러스터는 NAT gateway·일반 인터넷 없이 필요한 서비스 endpoint를 사용할 수 있습니다. `kubernetes.io/cluster/...` 서브넷 태그는 컨트롤 플레인 생성의 보편적 해결책이 아닙니다.
할당량 오류는 해당 서비스·quota를 식별하고 적용된 현재 값을 조회한 뒤 증가를 요청합니다. EKS 클러스터 수, EC2 vCPU·인스턴스 계열, VPC 한도는 다릅니다. “limit is 5” 같은 과거 메시지는 예시이며 현재 계정 한도가 아닙니다.
#### 일반적인 해결 방법
인프라 소유자를 통해 실제 권한·서비스 역할 trust·서브넷·IP·quota 문제를 수정하고 재시도를 검토합니다. `UnsupportedAvailabilityZoneException`은 지정한 클러스터 서브넷의 AZ가 해당 계정에서 EKS를 지원하지 않는다는 뜻입니다. 오류에 표시된 지원 AZ를 선택하며 EC2 인스턴스 유형 제공 목록만으로 진단하지 않습니다.
AWS Health의 서비스 이벤트를 확인합니다. 리전 변경은 별도 배치·데이터·네트워크 설계와 추가 클러스터를 만들 수 있으므로 기본 재시도 방식이 아닙니다. `eksctl create cluster --verbose ...`나 AWS CLI debug 플래그도 실제 생성을 실행합니다. 새 프로비저닝 전에 기존 stack event·요청 ID를 확인합니다.
### 클러스터 엔드포인트 접근 문제
#### DNS·전송·TLS·인증·인가를 구분하여 진단
endpoint mode, 허용 public CIDR과 cluster security group을 확인하고 클러스터 CA·타임아웃으로 테스트합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${EVIDENCE_PARENT:?}"
umask 077
ENDPOINT_DIR=$(mktemp -d "$EVIDENCE_PARENT/eks-endpoint.XXXXXXXX")
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--output json > "$ENDPOINT_DIR/cluster.json"
jq -er '.cluster.certificateAuthority.data' "$ENDPOINT_DIR/cluster.json" \
| base64 --decode > "$ENDPOINT_DIR/cluster-ca.crt"
ENDPOINT=$(jq -er '.cluster.endpoint' "$ENDPOINT_DIR/cluster.json")
jq '.cluster.resourcesVpcConfig | {endpointPublicAccess,endpointPrivateAccess,publicAccessCidrs,clusterSecurityGroupId,vpcId}' \
"$ENDPOINT_DIR/cluster.json"
curl --silent --show-error --connect-timeout 5 --max-time 10 \
--cacert "$ENDPOINT_DIR/cluster-ca.crt" --output /dev/null \
--write-out 'HTTP status: %{http_code}\n' "$ENDPOINT"
```
Kubernetes bearer token을 보내지 않는 요청입니다. 401·403은 접근이 거부되더라도 DNS·TCP·TLS 연결 성공을 보여줄 수 있습니다. HTTP 성공도 앱 상태 증명은 아닙니다. `curl -k`는 인증서 검증 오류를 숨깁니다. DNS 조회는 `https://` URL 전체가 아닌 endpoint URL의 hostname을 사용합니다.
public endpoint는 클라이언트의 실제 egress·NAT 주소와 허용 CIDR을 비교합니다. private endpoint는 VPC·연결 네트워크 경로, DNS와 보안 그룹 접근을 확인합니다. `com.amazonaws..eks` interface endpoint는 Kubernetes API가 아닌 **EKS 관리 API**용입니다. 이를 생성하는 것만으로 kubectl 접근을 해결하지 못하며 Kubernetes private endpoint는 별개입니다.
#### kubeconfig 및 권한
원시 자격 증명을 출력하지 않고 context 이름·server를 확인합니다. 별도 진단 kubeconfig에는 명시적 경로·alias를 사용하고 `NAMESPACE`를 의도한 인가 범위로 설정합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"
: "${NAMESPACE:?Set the namespace for the authorization check}"
: "${DIAGNOSTIC_KUBECONFIG:?Set a separate writable kubeconfig path}"
: "${KUBE_CONTEXT:?Choose an explicit alias for this cluster}"
umask 077
aws eks update-kubeconfig --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--kubeconfig "$DIAGNOSTIC_KUBECONFIG" --alias "$KUBE_CONTEXT"
export KUBECONFIG="$DIAGNOSTIC_KUBECONFIG"
kubectl --context "$KUBE_CONTEXT" auth can-i get pods --namespace "$NAMESPACE"
```
kubeconfig 생성에는 `eks:DescribeCluster`가 필요하고 Kubernetes 인증·인가는 별도 요구입니다. role assume이 필요하면 검토한 `--role-arn`과 trust·STS 권한을 사용합니다. 세션 토큰을 출력하지 말고 설정한 SSO 세션 등 실제 자격 증명 제공자를 통해 갱신합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query 'cluster.accessConfig'
# Use the next command when API or API_AND_CONFIG_MAP authentication is enabled.
aws eks list-access-entries --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION"
```
access-entry 인증이 켜져 있으면 해당 entry·연결 policy scope를 확인합니다. 기존 `CONFIG_MAP`·혼합 클러스터는 `aws-auth`도 사용할 수 있습니다. 기존 노드 매핑을 보존하고 계획한 마이그레이션을 수행합니다. 일반 접근 오류를 고치려고 `system:masters`를 부여하거나 ConfigMap 전체를 덮어쓰지 않습니다. IRSA의 IAM OIDC provider는 워크로드 AWS 자격 증명용이며 사람 IAM 주체의 Kubernetes 권한 매핑이 아닙니다.
#### 접근 경로 수정
endpoint mode에 맞는 사설 관리 경로나 검토한 public CIDR allow-list를 사용합니다. 진단 명령을 성공시키려고 `0.0.0.0/0`을 열지 않습니다. public access를 제거하기 전에 사설 경로를 테스트하고 무관한 기존 VPC 설정을 유지하며 configuration Update ID를 추적합니다. 계획된 변경은 [보안 장의 endpoint 절차](https://www.atomai.click/kubernetes-docs/llms/ko/eks/05-eks-security.md)를 따릅니다.
#### CloudShell 원클릭 접근
2026년 4월 30일 원클릭 기능은 실제 지원됩니다. 클러스터 상세 화면의 **Connect**를 선택하면 kubectl이 구성된 CloudShell이 열립니다. public·private API endpoint를 모두 지원합니다. private endpoint는 CloudShell VPC environment를 자동으로 시작하며 이름을 입력하도록 안내합니다. EKS 리전에서 기능 자체의 추가 요금 없이 사용할 수 있습니다.
콘솔 접근에도 IAM·CloudShell·VPC environment 권한, Kubernetes 접근과 정상 네트워크 구성이 필요합니다. 로컬 설정을 줄여줄 뿐 인가를 우회하지 않습니다. 적용 가능한 리소스·데이터 전송 비용과 세션 주체를 확인한 뒤 명령을 실행합니다.
출처: [kubeconfig·CloudShell](https://docs.aws.amazon.com/eks/latest/userguide/create-kubeconfig.html), [원클릭 발표](https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-eks-one-click-cluster-access/), [EKS PrivateLink 구분](https://docs.aws.amazon.com/eks/latest/userguide/vpc-interface-endpoints.html), [클러스터 문제 해결](https://docs.aws.amazon.com/eks/latest/userguide/troubleshooting.html).
### 클러스터 삭제 문제
#### 차단하는 의존성 확인
클러스터 삭제는 의도적인 폐기 작업이며 일반적인 문제 해결 수단이 아닙니다. 정확한 EKS·CloudFormation 오류, 클러스터 ARN·계정·리전, 업데이트 상태와 삭제 보호 설정을 확인합니다. 관리형 노드 그룹·Fargate 프로필 외에 삭제 보호와 설치한 EKS Capabilities도 삭제를 막을 수 있습니다. 삭제가 끝날 때까지 클러스터 IAM·서비스 역할을 유지합니다.
선택한 클러스터를 조사하고 엔드포인트를 명시적 kubectl 컨텍스트와 비교합니다. 아래 명령은 읽기 전용이며 삭제 대상을 자동으로 선택하지 않습니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?Set the cluster being deliberately retired}"
: "${AWS_REGION:?}"; : "${KUBE_CONTEXT:?}"; : "${EXPECTED_ACCOUNT_ID:?}"
ACTUAL_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
if [ "$ACTUAL_ACCOUNT_ID" != "$EXPECTED_ACCOUNT_ID" ]; then
echo "Account mismatch; stop" >&2
exit 1
fi
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query 'cluster.{arn:arn,status:status,deletionProtection:deletionProtection,endpoint:endpoint}'
kubectl config view --context "$KUBE_CONTEXT" --minify \
-o jsonpath='{.clusters[0].cluster.server}{"\n"}'
# Compare the endpoints before inspecting Kubernetes resources.
kubectl --context "$KUBE_CONTEXT" get services -A \
-o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,TYPE:.spec.type,CLASS:.spec.loadBalancerClass,ADDRESS:.status.loadBalancer.ingress'
kubectl --context "$KUBE_CONTEXT" get ingress -A
kubectl --context "$KUBE_CONTEXT" get pvc -A
kubectl --context "$KUBE_CONTEXT" get pv \
-o custom-columns='NAME:.metadata.name,CLAIM_NS:.spec.claimRef.namespace,CLAIM:.spec.claimRef.name,RECLAIM:.spec.persistentVolumeReclaimPolicy,DRIVER:.spec.csi.driver,HANDLE:.spec.csi.volumeHandle'
aws eks list-nodegroups --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION"
aws eks list-fargate-profiles --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION"
aws eks list-capabilities --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION"
aws eks list-addons --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION"
```
Service의 `EXTERNAL-IP` 출력만으로 소유권을 판단할 수 없습니다. 컨트롤러 관리 `LoadBalancer` Service, 수동 external IP와 다른 Service 유형을 구분합니다. namespace·name·UID, 컨트롤러 소유권, AWS ARN과 관련 태그를 기록합니다. 컨트롤러가 설치된 Ingress·Gateway·TargetGroupBinding도 포함하고 대상 그룹·로드 밸런서 공유 여부를 확인합니다.
#### 소유자의 순서에 따라 리소스 폐기
1. 트래픽·워크로드를 이전하고 애플리케이션 일관성 백업·복원과 데이터 보존 요구를 확인합니다. PVC YAML 저장은 데이터 백업이 아닙니다. `Delete` reclaim policy는 claim 삭제 시 실제 스토리지를 삭제할 수 있으며 `Retain`은 별도 데이터·스토리지 처리 결정이 필요합니다.
2. 필요한 로드 밸런서 컨트롤러가 실행 중일 때 로드 밸런서를 소유하는 검토된 Kubernetes 리소스만 제거합니다. finalizer 처리를 기다리고 해당 AWS 리소스가 해제됐는지 확인합니다. 컨트롤러 노드를 삭제하기 전에 IAM·의존성 오류를 해결합니다. 정리 실패를 숨기려고 finalizer를 제거하지 않습니다.
3. 문서화된 소유권·리소스 삭제 의미에 따라 EKS Capabilities를 제거합니다. 관리형 노드 그룹·Fargate 프로필을 검토한 순서로 삭제하고 완료를 기다립니다. 자체 관리 노드·스택은 별도 폐기가 필요합니다. 애드온 삭제도 Kubernetes 구성 요소를 제거할 수 있으므로 네트워크·스토리지 컨트롤러는 의존 리소스 정리가 끝날 때까지 유지합니다.
4. 명시적인 폐기 결정으로만 삭제 보호를 해제하고 원래 인프라 소유자를 통해 클러스터를 삭제합니다. Auto Mode 클러스터 삭제는 관리 노드와 로드 밸런서도 삭제합니다. 이 범위와 내장 TargetGroupBinding의 대상 그룹 생명주기도 고려합니다.
5. 전용 스택·VPC, 볼륨·스냅샷, 로드 밸런서, IAM, 로그와 Prometheus scraper 등 남은 리소스의 정확한 소유권을 확인합니다. 공유 리소스와 보존 데이터는 독립적인 생명주기를 가지며 비용이 계속 발생할 수 있습니다.
다음은 앞의 트래픽·데이터 검토 후 명시적으로 선택한 Service **하나**를 삭제하는 예시이며, 조회 결과 전체를 삭제하는 루프가 아닙니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${SERVICE_NAMESPACE:?}"; : "${SERVICE_NAME:?}"
# Separate approved teardown step after traffic/data migration and owner review.
kubectl --context "$KUBE_CONTEXT" -n "$SERVICE_NAMESPACE" get service "$SERVICE_NAME" -o yaml
kubectl --context "$KUBE_CONTEXT" -n "$SERVICE_NAMESPACE" delete service "$SERVICE_NAME" \
--wait=true --timeout=10m
```
타임아웃은 AWS 로드 밸런서의 보존·삭제를 증명하지 않습니다. finalizer, 컨트롤러 이벤트와 정확한 AWS ARN을 확인합니다. Kubernetes와 AWS 리소스 정리 완료 시점은 다를 수 있습니다.
#### 삭제 오류와 force 동작
EKS 업데이트 상세와 CloudFormation stack event로 의존성·권한·진행 중 작업 오류를 확인합니다. eksctl 0.229의 `delete cluster --force`는 실제 지원되며 오류가 발생해도 삭제를 계속하게 합니다. 완전한 정리나 orphan 소유권을 보장하지 않습니다. 별도 `--disable-nodegroup-eviction`은 eviction 대신 delete를 사용하여 PDB 검사를 우회합니다. 둘 다 기본 장애 대응 절차가 아닙니다.
계정·리전 전체 ELB·ELBv2 목록을 삭제 명령에 연결하거나, 의존성 오류를 없애려고 모든 Service·PVC·namespace를 삭제하지 않습니다. orphan을 수동 제거해야 한다면 정확한 클러스터·스택 소유권과 데이터·트래픽 영향을 확인한 뒤 해당 서비스의 검토된 폐기 절차를 따릅니다.
출처: [EKS 클러스터 삭제](https://docs.aws.amazon.com/eks/latest/userguide/delete-cluster.html), [삭제 문제 해결](https://repost.aws/knowledge-center/eks-delete-cluster-issues), [영구 볼륨 생명주기](https://kubernetes.io/docs/concepts/storage/persistent-volumes/).
## 네트워킹 문제

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-09-eks-troubleshooting-2.html)
먼저 실제 컴퓨팅·데이터 플레인을 식별합니다. 오픈소스 VPC CNI를 사용하는 표준 EC2, Fargate, Auto Mode의 에이전트·설정은 다릅니다. Auto Mode는 `NodeClass` 네트워크 제어를 사용하며 `aws-node` DaemonSet·`ENIConfig` 변경으로 Auto Mode 노드를 구성하지 못합니다.
### 파드 간 통신 문제
#### 실패 경로 추적
출발·목적 Pod, namespace, node·AZ, IP 계열, 프로토콜과 목적 포트를 기록합니다. 허용된 테스트로 차이를 분리할 수 있으면 같은 노드·다른 노드 경로를 비교합니다. 해당 경로의 정책, 보안 그룹, 라우팅·NACL, CNI 상태, IP 할당과 path MTU를 확인합니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${POD_NAME:?}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pod "$POD_NAME" -o wide
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pod "$POD_NAME" \
-o jsonpath='{.metadata.labels}{"\n"}{.spec.nodeName}{"\n"}{.spec.hostNetwork}{"\n"}'
kubectl --context "$KUBE_CONTEXT" get namespace "$NAMESPACE" --show-labels
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get networkpolicies
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get events \
--sort-by='.metadata.creationTimestamp'
```
표준 `networking.k8s.io/v1` NetworkPolicy는 허용 트래픽을 합집합으로 적용하며 규칙 우선순위나 “마지막 정책 우선”이 없습니다. 양쪽이 격리되어 있으면 출발 egress와 목적 ingress가 모두 허용해야 합니다. admin·cluster-wide API와 벤더 정책 엔진의 의미는 별도입니다. namespace 레이블과 `namespaceSelector`·`podSelector`가 같은 peer의 AND인지, 별도 peer의 OR인지 확인합니다.
VPC CNI는 네이티브 정책 집행을 지원하므로 별도 정책 Pod가 없다는 이유만으로 Calico·Cilium을 설치하지 않습니다. 지원 CNI·platform·kernel과 policy-agent 활성화 설정을 확인합니다. standard startup mode는 정책 구성이 끝날 때까지 새 Pod를 허용하고 strict mode는 거부에서 시작하여 DNS·의존성 정책이 필요합니다. host-network 동작과 기타 적용 범위는 해당 구현 문서를 확인합니다.
표준 VPC CNI에서는 해당 컨테이너가 존재할 때 실제 `aws-node` Pod의 `aws-network-policy-agent` 로그를 확인합니다. `kubectl get pods -l ...`이 빈 목록으로 성공해도 플러그인 설치를 증명하지 않습니다. Auto Mode는 내장 정책 제어를 사용합니다.
#### 의도한 흐름만 수정
namespace 전체 allow-all을 추가하거나 정책을 삭제하기보다 전체 허용 흐름을 검토합니다. 아래는 backend API Pod를 선택하여 frontend web Pod의 TCP 8080만 허용하는 예시입니다.
```yaml
# Example ingress policy only: review both peers and the complete allowed-flow matrix.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-from-web
namespace: backend
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: frontend
podSelector:
matchLabels:
app: web
ports:
- protocol: TCP
port: 8080
```
적용하면 다른 정책이 허용하지 않는 backend ingress는 격리될 수 있습니다. client egress, DNS와 모든 의존성을 설정하지 않으므로 허용·거부 테스트로 따로 확인합니다. 예시 이름 대신 실제 namespace·레이블을 사용합니다.
보안 그룹은 Pod 그룹·사용자 지정 Pod 서브넷을 포함한 실제 출발·목적 ENI의 그룹을 확인합니다. 소유자를 통해 필요한 source·port를 허용하며 모든 프로토콜이나 넓은 CIDR을 일반 해결책으로 추가하지 않습니다. 1500·9001 같은 MTU는 경로에 따라 다릅니다. CNI 변경·Pod 교체 전에 실패를 측정하고 캡슐화를 고려합니다.
### 서비스 접근 문제
Service selector, 실제 Pod readiness, endpoint 조건과 포트 매핑을 함께 확인합니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${SERVICE_NAME:?}"
SERVICE_JSON=$(kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get service "$SERVICE_NAME" -o json)
printf '%s\n' "$SERVICE_JSON" | jq '{metadata: {name: .metadata.name, namespace: .metadata.namespace}, spec: .spec, status: .status}'
SELECTOR=$(printf '%s\n' "$SERVICE_JSON" | jq -r '(.spec.selector // {}) | to_entries | map("\(.key)=\(.value)") | join(",")')
if [ -n "$SELECTOR" ]; then
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pods -l "$SELECTOR" -o wide
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pods -l "$SELECTOR" -o json \
| jq '.items[] | {name:.metadata.name,phase:.status.phase,ready:[.status.conditions[]? | select(.type=="Ready")],containers:.status.containerStatuses}'
else
printf 'No selector: inspect ExternalName or explicitly managed EndpointSlices as applicable.\n'
fi
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get endpointslices \
-l "kubernetes.io/service-name=$SERVICE_NAME" -o yaml
```
폐기 예정 Endpoints API에 의존하지 말고 EndpointSlice를 사용합니다. `ready`·`serving`·`terminating`과 `publishNotReadyAddresses`, `externalTrafficPolicy`, `internalTrafficPolicy`를 확인합니다. Running Pod가 Ready라는 보장은 없습니다.
`ExternalName`은 선택된 Pod 대신 DNS alias를 진단합니다. headless Service의 `clusterIP: None`은 의도된 값입니다. selector가 없는 Service는 소유자가 관리하는 EndpointSlice를 사용할 수 있습니다. Service `port`와 `targetPort`는 달라도 되며 container port 선언만으로 앱이 해당 포트를 listen하지는 않습니다.
레이블·포트가 잘못되면 릴리스 설정에서 소유 Service·Pod template을 수정합니다. 컨트롤러 소유 Pod 하나의 레이블 변경은 지속적인 해결책이 아닙니다. 패치 전에 포트 목록 전체와 immutable 필드를 확인합니다. 원인 진단 없이 Service 삭제·재생성, ClusterIP 변경, kube-proxy 전체 재시작을 하지 않습니다.
정책이 허용하는 같은 출발지·프로토콜로 직접 Pod와 Service 접근을 비교합니다. iptables·nftables·eBPF를 조사하기 전에 kube-proxy, 대안 구현, Auto Mode 중 실제 데이터 플레인을 확인합니다. kube-proxy Pod 부재가 항상 장애는 아닙니다.
### 로드 밸런서 문제
Service·Ingress class, 어노테이션과 소유권에서 컨트롤러를 식별합니다. 표준 AWS Load Balancer Controller와 Auto Mode는 class·API·생명주기가 다릅니다. 해당 소유자의 이벤트, 서브넷 선택, IAM, 보안 그룹, target 등록과 health check를 확인합니다.
워크로드에 연결된 정확한 LB·target group ARN을 사용합니다.
```bash
set -euo pipefail
: "${AWS_REGION:?}"; : "${LOAD_BALANCER_ARN:?}"; : "${TARGET_GROUP_ARN:?}"
aws elbv2 describe-load-balancers --region "$AWS_REGION" \
--load-balancer-arns "$LOAD_BALANCER_ARN"
aws elbv2 describe-tags --region "$AWS_REGION" \
--resource-arns "$LOAD_BALANCER_ARN" "$TARGET_GROUP_ARN"
aws elbv2 describe-load-balancer-attributes --region "$AWS_REGION" \
--load-balancer-arn "$LOAD_BALANCER_ARN"
aws elbv2 describe-target-groups --region "$AWS_REGION" \
--target-group-arns "$TARGET_GROUP_ARN"
aws elbv2 describe-target-health --region "$AWS_REGION" \
--target-group-arn "$TARGET_GROUP_ARN"
```
`describe-load-balancer-attributes`는 속성이며 운영 상태는 `describe-load-balancers`에서 확인합니다. target health reason, health-check protocol·port·path, listener·rule 경로와 앱 응답을 확인합니다. instance target은 보통 node·NodePort, IP target은 Pod target port로 접근합니다. 보안 그룹은 실제 경로에 맞아야 합니다.
자동 discovery에 사용하는 subnet role tag와 public·internal scheme, route table, IP 여유와 AZ를 확인합니다. 같은 서브넷에 public·private role tag를 모두 추가하는 것은 해결책이 아닙니다. 명시적 subnet 지정·controller 버전에 따라 tag 요구가 달라질 수 있습니다. health check 실패를 우회하려고 frontend·backend SG에 `0.0.0.0/0`을 열지 않습니다.
소유자의 현재 scheme·type 설정을 사용합니다. 기존 `aws-load-balancer-internal`·`aws-load-balancer-type: nlb` 예시를 현재 class와 혼용하지 않습니다. 소유자·scheme 변경에는 계획한 교체·트래픽 이전이 필요할 수 있으며 어노테이션 수정이 인플레이스 전환을 보장하지 않습니다. Auto Mode는 자체 관리 컨트롤러의 기존 LB를 인수하지 않습니다.
Service 삭제는 LB 삭제로 이어질 수 있고, export한 Service YAML은 완전한 트래픽·데이터 롤백 계획이 아닙니다. ALB를 수동 생성해도 Kubernetes Service에 자동으로 연결되지 않습니다. 변경 전에 선택한 소유자의 [네트워킹 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/03-eks-networking-part2.md)를 확인합니다.
### DNS 문제
#### 실제 Pod가 사용하는 resolver 확인
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${POD_NAME:?}"; : "${CONTAINER_NAME:?}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pod "$POD_NAME" \
-o jsonpath='{.spec.dnsPolicy}{"\n"}{.spec.dnsConfig}{"\n"}{.spec.hostNetwork}{"\n"}'
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" exec "$POD_NAME" \
-c "$CONTAINER_NAME" -- cat /etc/resolv.conf
# Where this container actually includes nslookup, test the intended name.
: "${DNS_TEST_NAME:?Set the intended Service FQDN or reviewed external hostname}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" exec "$POD_NAME" \
-c "$CONTAINER_NAME" -- nslookup "$DNS_TEST_NAME"
```
이미지에 shell·DNS 도구가 없다면 도구 제한이지 DNS 질의 실패가 아닙니다. 관련 network·identity context를 유지하는 검토된 debug 방식을 준비합니다. 새 debug Pod의 DNS·정책은 실제 Pod와 다를 수 있습니다.
표준 비 Auto 노드는 설치된 CoreDNS Deployment·Service, 설정과 EndpointSlice를 확인합니다.
```bash
kubectl --context "$KUBE_CONTEXT" -n kube-system get deployment coredns
kubectl --context "$KUBE_CONTEXT" -n kube-system get pods -l k8s-app=kube-dns -o wide
kubectl --context "$KUBE_CONTEXT" -n kube-system get service kube-dns
kubectl --context "$KUBE_CONTEXT" -n kube-system get endpointslices \
-l kubernetes.io/service-name=kube-dns
kubectl --context "$KUBE_CONTEXT" -n kube-system get configmap coredns -o yaml
kubectl --context "$KUBE_CONTEXT" -n kube-system logs -l k8s-app=kube-dns \
--all-containers=true --prefix=true --since=15m --tail=100
```
**Auto Mode 노드**에서는 CoreDNS가 node system service로 실행됩니다. 순수 Auto Mode는 기존 Deployment 없이도 동작할 수 있습니다. 혼합 Auto·비 Auto 클러스터는 비 Auto 노드용 Deployment를 유지해야 합니다. Auto Mode 문제를 고치려고 NodeLocal DNSCache를 설치하거나 없는 Deployment를 재시작하지 않습니다.
Pod nameserver, CoreDNS·NodeLocal upstream과 VPC resolver를 구분합니다. `169.254.20.10`은 흔히 선택하는 NodeLocal DNSCache 주소이지 보편적인 VPC DNS 주소가 아닙니다. `8.8.8.8` 같은 public resolver는 Kubernetes Service zone·AWS private DNS의 fallback이 아닙니다.
```bash
set -euo pipefail
: "${AWS_REGION:?}"; : "${VPC_ID:?}"
aws ec2 describe-vpc-attribute --region "$AWS_REGION" --vpc-id "$VPC_ID" \
--attribute enableDnsSupport
aws ec2 describe-vpc-attribute --region "$AWS_REGION" --vpc-id "$VPC_ID" \
--attribute enableDnsHostnames
aws ec2 describe-vpcs --region "$AWS_REGION" --vpc-ids "$VPC_ID" \
--query 'Vpcs[].{VpcId:VpcId,DhcpOptionsId:DhcpOptionsId}'
```
DNS 속성은 `describe-vpcs`의 필드가 아니라 `describe-vpc-attribute`로 조회합니다. 반환된 ID로 DHCP option을 확인합니다. 다른 워크로드를 검토하지 않고 공유 VPC의 DHCP 설정을 바꾸지 않습니다.
#### 원인에 맞는 DNS 수정
필요한 UDP·TCP 53, 실제 CoreDNS readiness·설정, upstream 접근, DNS policy·search 설정을 확인합니다. hostNetwork Pod가 cluster DNS를 쓰려면 일반적으로 `ClusterFirstWithHostNet`이 필요하고 `dnsPolicy: None`은 완전하고 의도적인 resolver 설정이 필요합니다. DNS만 허용하는 egress policy도 다른 정책이 허용하지 않으면 선택한 Pod의 나머지 egress를 격리합니다.
소유 Corefile의 사용자 설정을 보존하고 애드온 지원 스키마를 사용합니다. scale·restart 전에 replica·resource·PDB·scheduling과 autoscaling 소유자를 확인합니다. Update ID와 실제 DNS 테스트를 유지하며 처음부터 모든 DNS Pod를 삭제하거나 추정한 최신 이미지를 설치하지 않습니다.
### VPC CNI 문제
다음은 **오픈소스 VPC CNI를 사용하는 표준 EC2 노드**용입니다. 문제 노드의 Pod를 명시적으로 선택합니다. `kubectl exec`는 label selector를 받지 않습니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NODE_NAME:?}"
kubectl --context "$KUBE_CONTEXT" get node "$NODE_NAME" \
-o jsonpath='{.spec.providerID}{"\n"}{.status.nodeInfo}{"\n"}{.status.allocatable.pods}{"\n"}'
kubectl --context "$KUBE_CONTEXT" -n kube-system get daemonset aws-node -o json \
| jq '.spec.template.spec | {containers:[.containers[] | {name,image,args,env}],initContainers:[.initContainers[]? | {name,image,args,env}]}'
kubectl --context "$KUBE_CONTEXT" -n kube-system get pods \
-l k8s-app=aws-node --field-selector "spec.nodeName=$NODE_NAME" -o wide
kubectl --context "$KUBE_CONTEXT" get pods -A \
--field-selector "spec.nodeName=$NODE_NAME" -o wide
: "${AWS_NODE_POD:?Select the aws-node Pod on that exact node}"
kubectl --context "$KUBE_CONTEXT" -n kube-system logs "$AWS_NODE_POD" \
-c aws-node --since=15m --tail=200
```
애드온 `configurationValues`, DaemonSet env와 관련 custom resource를 확인합니다. 모든 설정이 `aws-node` ConfigMap에 있다고 가정하지 않습니다. 노드 `.spec.podCIDR`은 VPC CNI Pod 주소 인벤토리로 신뢰할 수 없으므로 실제 Pod IP, EC2 ENI·prefix·subnet을 확인합니다.
```bash
set -euo pipefail
: "${AWS_REGION:?}"; : "${INSTANCE_ID:?Verify it from the selected node ProviderID}"
aws ec2 describe-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" \
--query 'Reservations[].Instances[].{Id:InstanceId,Type:InstanceType,Subnet:SubnetId,SGs:SecurityGroups,ENIs:NetworkInterfaces}'
: "${SUBNET_ID:?Set the actual node or custom Pod subnet being investigated}"
aws ec2 describe-subnets --region "$AWS_REGION" --subnet-ids "$SUBNET_ID" \
--query 'Subnets[].{Id:SubnetId,CIDR:CidrBlock,AvailableIPs:AvailableIpAddressCount}'
: "${INSTANCE_TYPE:?Set the selected instance type}"
aws ec2 describe-instance-types --region "$AWS_REGION" --instance-types "$INSTANCE_TYPE" \
--query 'InstanceTypes[].{Type:InstanceType,Network:NetworkInfo}'
```
활성화된 IPAMD introspection은 노드에 설정된 endpoint(보통 loopback 61679)에서 제공합니다. 필요한 도구가 있는 허용된 node·agent 진단 방법을 사용합니다. CNI 이미지에 curl이 없다는 것은 IPAM 장애가 아닙니다.
#### 할당 제약 구분
- **서브넷 고갈·단편화:** free IP와 prefix를 비교합니다. prefix delegation에는 지원 인스턴스·설정과 연속된 prefix 블록이 필요합니다. 여유 IP 개수만으로 /28 할당 가능성을 증명하지 못합니다.
- **인스턴스 ENI·IP 한도:** `NetworkInfo`와 기존 ENI를 확인합니다. node-group desired·min·max는 노드 수이며 인스턴스 유형·노드별 한도를 바꾸지 않습니다. 검토한 새 그룹이나 지원되는 원래 launch-template 업데이트 경로로 인스턴스 설정을 변경합니다.
- **Custom networking:** 활성화 전에 맞는 `ENIConfig`, Pod subnet·SG와 node 선택을 준비합니다. secondary ENI·Pod IP의 출처를 바꾸며 무제한 용량을 만들지 않습니다.
- **Warm target:** `WARM_IP_TARGET`은 여유 IP, `MINIMUM_IP_TARGET`은 전체 할당 IP의 하한입니다. 문서에 따라 warm-ENI 동작을 우선하고 prefix mode의 warm-prefix에도 영향을 줍니다. 양의 warm 여유 없이 minimum만 설정하면 추가 할당을 막을 수 있습니다. 임의의 1·2·5 값 대신 워크로드·IP 예산에서 도출합니다.
- **Identity·소유권:** 실제 CNI IRSA·Pod Identity 또는 적용 node role과 IPv4·IPv6 policy를 확인합니다. 모든 node role에 IPv4 CNI policy를 붙이는 것은 보편적 해결책이 아닙니다.
Auto Mode는 `NodeClass`의 subnet·SG·policy를 사용하고 이 warm-IP·ENI 또는 `ENIConfig` 설정을 받지 않습니다. 관리형 네트워크 모델을 유지합니다.
지원 중간 버전·스키마에 따라 소유자를 통해 검토한 CNI 버전·설정을 적용합니다. [업그레이드 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/08-eks-upgrades.md)로 정확한 애드온 요청을 추적하고 네트워크를 검증합니다. 컨테이너 이미지 하나만 바꾸거나 설정을 무조건 덮어쓰거나 모든 워크로드를 재시작하여 할당 실패를 숨기지 않습니다.
출처: [VPC CNI 정책 설정](https://docs.aws.amazon.com/eks/latest/userguide/cni-network-policy-configure.html), [Auto Mode 네트워크](https://docs.aws.amazon.com/eks/latest/userguide/auto-networking.html), [custom networking](https://docs.aws.amazon.com/eks/latest/best-practices/custom-networking.html), [CNI 설정](https://github.com/aws/amazon-vpc-cni-k8s), [Kubernetes NetworkPolicy](https://kubernetes.io/docs/concepts/services-networking/network-policies/), [EndpointSlice](https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/).
## 노드 및 파드 문제

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-09-eks-troubleshooting-3.html)
### 노드 상태 문제
#### 조건과 실제 노드 확인
`Ready=False`와 `Ready=Unknown`은 다른 근거가 필요합니다. 비정상 kubelet·런타임이 실패를 보고할 수 있고 heartbeat 부재는 연결 단절·노드 정지일 수 있습니다. Memory·Disk·PID pressure는 별도 조건이며 항상 NotReady를 뜻하지 않습니다. 한 레이블로 원인을 단정하지 말고 condition reason·시각과 lease·event를 확인합니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NODE_NAME:?}"
NODE_JSON=$(kubectl --context "$KUBE_CONTEXT" get node "$NODE_NAME" -o json)
printf '%s\n' "$NODE_JSON" | jq '{name:.metadata.name,uid:.metadata.uid,labels:.metadata.labels,providerID:.spec.providerID,taints:.spec.taints,unschedulable:.spec.unschedulable,nodeInfo:.status.nodeInfo,conditions:.status.conditions,capacity:.status.capacity,allocatable:.status.allocatable}'
NODE_UID=$(printf '%s\n' "$NODE_JSON" | jq -er '.metadata.uid')
kubectl --context "$KUBE_CONTEXT" get events -A --field-selector "involvedObject.uid=$NODE_UID" \
--sort-by='.metadata.creationTimestamp'
kubectl --context "$KUBE_CONTEXT" get pods -A --field-selector "spec.nodeName=$NODE_NAME" -o wide
```
EC2 확인에는 정확한 ProviderID를 사용합니다. node IP를 `grep`으로 매칭하면 다른 리소스를 선택할 수 있습니다. 관리형 그룹은 health·이미지·release·repair 설정을 확인합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${NODEGROUP_NAME:?}"
aws eks describe-nodegroup --cluster-name "$CLUSTER_NAME" --nodegroup-name "$NODEGROUP_NAME" \
--region "$AWS_REGION" \
--query 'nodegroup.{status:status,health:health,version:version,releaseVersion:releaseVersion,amiType:amiType,nodeRepairConfig:nodeRepairConfig,updateConfig:updateConfig,scalingConfig:scalingConfig}'
```
노드 접근을 지원하는 환경에서는 기본 사항의 원격 세션 절차로 kubelet·containerd journal, 네트워크, disk byte·inode와 메모리를 확인합니다. private key를 출력하지 않고 실제 certificate·kubeconfig 경로를 검사합니다. `kubeadm certs renew`는 EKS 관리형 컨트롤 플레인 복구가 아니며 `eksctl replace nodegroup` 명령도 지원되지 않습니다. AL2023는 nodeadm 설정을 사용하므로 모든 이미지에 AL2 `/etc/eks/bootstrap.sh`를 재실행하지 않습니다.
#### 복구와 자동 repair
근거를 보존한 뒤 소유자와 복구 동작을 선택합니다. kubelet·containerd 재시작, reboot·교체는 워크로드에 영향을 주며 지속적인 IAM·네트워크·bootstrap 문제를 해결하지 못할 수 있습니다. reboot API 응답이 instance·kubelet Ready를 뜻하지는 않습니다. 동일 node·instance와 워크로드를 확인한 뒤 uncordon합니다.
계획한 교체는 용량, 영구 데이터, PDB와 교체 소유권을 확인하고 노드 하나를 timeout과 함께 drain합니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?Set the reviewed cluster context}"
: "${NODE_NAME:?Set one reviewed old node}"
kubectl --context "$KUBE_CONTEXT" get node "$NODE_NAME" -o wide
kubectl --context "$KUBE_CONTEXT" get node "$NODE_NAME" \
-o jsonpath='{.spec.providerID}{"\n"}'
kubectl --context "$KUBE_CONTEXT" get pods --all-namespaces \
--field-selector "spec.nodeName=$NODE_NAME" -o wide
# Stop on failure. Do not terminate the instance or delete the node group here.
kubectl --context "$KUBE_CONTEXT" drain "$NODE_NAME" --ignore-daemonsets --timeout=10m
```
drain 실패 후 EC2 종료로 진행하거나 기본적으로 `emptyDir` 데이터를 버리지 않습니다. 일부만 비워진 노드는 cordon 상태일 수 있습니다. PDB는 eviction 경로를 보호하며 모든 인프라 장애·종료·컨트롤러 scale-down을 막지는 않습니다. 패키지를 직접 바꾸기보다 [노드 업그레이드 절차](https://www.atomai.click/kubernetes-docs/llms/ko/eks/08-eks-upgrades.md)의 관리형 교체를 따릅니다.
EKS 자동 node repair는 실제 지원되는 별도 기능입니다. Auto Mode는 기본 활성화이고 관리형 그룹은 `nodeRepairConfig`, Karpenter는 자체 feature·설정 요구를 사용합니다. monitoring이 조건을 보고해도 repair가 자동 활성화되지는 않습니다. 현재 기본 표에는 지속적인 Ready·runtime·kernel·networking·storage 실패 교체가 있으며 **MemoryPressure·DiskPressure의 기본 repair 동작은 없습니다**. repair threshold·parallelism과 unhealthy fleet·ARC 제어가 새 동작을 멈출 수 있지만 진행 중 동작은 계속될 수 있습니다. Lambda 하나, ASG tag 또는 `maxUnavailable`만으로 이 동작을 제공한다고 설명하지 않습니다.
### 파드 문제
#### 상태·이벤트·소유 컨트롤러 확인
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${POD_NAME:?}"
POD_JSON=$(kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pod "$POD_NAME" -o json)
printf '%s\n' "$POD_JSON" | jq '{
name:.metadata.name,uid:.metadata.uid,owners:.metadata.ownerReferences,
node:.spec.nodeName,serviceAccount:.spec.serviceAccountName,
imagePullSecrets:.spec.imagePullSecrets,
containers:[.spec.containers[] | {name,image,imagePullPolicy,resources}],
initContainers:[.spec.initContainers[]? | {name,image,resources}],
phase:.status.phase,reason:.status.reason,message:.status.message,
conditions:.status.conditions,containerStatuses:.status.containerStatuses,
initContainerStatuses:.status.initContainerStatuses
}'
POD_UID=$(printf '%s\n' "$POD_JSON" | jq -er '.metadata.uid')
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get events \
--field-selector "involvedObject.uid=$POD_UID" --sort-by='.metadata.creationTimestamp'
```
기본 사항의 container별 로그 절차를 사용합니다. Pending, ContainerCreating, image-pull 대기, init-container 실패, readiness 실패, OOMKilled와 restart backoff는 서로 다른 문제입니다. CrashLoopBackOff는 반복 실패 후 backoff이며 근본 원인이 아닙니다.
| 근거 | 다음 확인 |
| --- | --- |
| Image pull 오류 | Registry·name·tag·digest·architecture, node 측 DNS·TLS·route, rate limit과 실제 pull identity |
| FailedScheduling | requests·allocatable, Pod 수, taint·affinity·topology, quota와 PVC 소비자 제약 |
| FailedMount / attach | PVC·PV·StorageClass, CSI·identity, AZ·현재 attachment. 스토리지 절 참조 |
| OOMKilled / Evicted | container 종료 상태, limit, node pressure와 사용 이력. 누수로 단정하지 않음 |
| Forbidden / admission 실패 | 정확한 API 주체와 RBAC·admission policy. Pod list 권한 추가가 registry·filesystem 접근을 고치지 않음 |
`imagePullPolicy: Always`는 없는 이미지·잘못된 자격 증명을 고치지 않습니다. 노트북 Docker pull은 node 경로·identity 검증이 아니고 Docker load도 containerd 런타임에 이미지를 자동으로 넣지 않습니다.
#### Registry 자격 증명과 워크로드 변경
private ECR은 실제 node·Fargate execution identity와 repository policy를 확인합니다. 앱 IRSA·Pod Identity는 앱 시작 전에 이미지를 pull하는 주체가 아닙니다. 사설 ECR 경로에는 API·DKR·S3 접근이 필요할 수 있으며 ECR endpoint가 임의 registry의 사설 접근을 제공하지는 않습니다.
image-pull Secret이 필요한 registry는 올바른 자격 증명이 포함된 보호된 Docker auth JSON을 사용합니다. `.dockerconfigjson`이나 비밀번호를 로그·명령 인자에 출력하지 않습니다. 데스크톱 credential-helper 참조만으로 kubelet이 사용할 자격 증명이 제공되지는 않습니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${DEPLOYMENT_NAME:?}"
: "${PULL_SECRET_NAME:?Choose an application-specific secret name}"
: "${DOCKER_CONFIG_JSON:?Provide a protected registry auth JSON file}"
# Separate reviewed change; an existing Secret causes create to fail rather than replacing it.
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" create secret generic "$PULL_SECRET_NAME" \
--type=kubernetes.io/dockerconfigjson \
--from-file=".dockerconfigjson=$DOCKER_CONFIG_JSON"
PATCH=$(jq -n --arg name "$PULL_SECRET_NAME" \
'{spec:{template:{spec:{imagePullSecrets:[{name:$name}]}}}}')
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" patch deployment "$DEPLOYMENT_NAME" \
--type=strategic --patch "$PATCH"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" rollout status \
"deployment/$DEPLOYMENT_NAME" --timeout=5m
```
Secret은 Pod와 같은 namespace여야 합니다. strategic Pod-template patch는 이름 기준으로 pull-secret 항목을 병합하고 통제된 Deployment rollout을 시작합니다. 릴리스 소유권과 다른 설정을 보존합니다. 기존 Pod의 `imagePullSecrets`는 일반적으로 수정 가능한 필드가 아닙니다. ServiceAccount 기본값은 새로 승인되는 Pod에 적용되며 namespace의 default SA 변경은 무관한 워크로드에도 영향을 줄 수 있습니다. timer로 공유 Secret을 삭제·재생성하지 말고 자격 증명 소유자를 통해 만료를 관리합니다.
다른 설정 오류도 컨트롤러의 선언 template에서 고치고 rollout·readiness를 관찰합니다. 적절한 컨트롤러가 있을 때만 Pod 삭제 후 재생성되며 삭제로 근거가 사라질 수 있습니다. debug `--copy-to`는 앱 부작용을 복제할 수 있으므로 라이브 앱에 패키지를 설치하기보다 권한·정리가 명시된 검토된 진단 방식·이미지를 사용합니다.
### 리소스 제약 문제
```bash
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get resourcequotas,limitranges
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pods -o wide
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" top pods --containers
kubectl --context "$KUBE_CONTEXT" top nodes
```
메트릭 명령은 Metrics Server와 정상 kubelet 접근이 필요합니다. 스케줄링은 현재 top 사용률이 아니라 requests·node allocatable을 사용합니다. 해당하는 init container, Pod overhead, ephemeral storage, extended resource와 Pod 수 한도를 포함합니다. namespace quota·LimitRange는 별도 제약입니다.
측정한 필요량이 뒷받침할 때만 requests를 줄입니다. memory request 감소는 Insufficient pods를 해결하거나 limit 내 동작을 보장하지 않습니다. limit 증가는 pressure를 node로 옮길 수 있습니다. 로그·cache 삭제 전에 disk·inode·image·filesystem 사용을 확인하고 장애 근거를 보존합니다.
node 수 증가는 instance별 용량을 바꾸지 않습니다. 관리형 그룹 desired·min·max는 autoscaler와 조율하며 scaling config 축소는 PDB를 따르지 않습니다. instance type 변경은 적절한 새 그룹이나 지원되는 원래 launch-template/version 경로를 사용합니다. Pod를 스케줄하기 위해 taint·affinity·topology 제약을 무조건 제거하지 않습니다.
### 자동 스케일링 문제
replica 확장(HPA), resource 추천·갱신(VPA), node provisioning(CA·Karpenter·Auto Mode)과 앱 병목을 구분합니다. 다른 provisioner가 용량을 소유하면 CA Pod가 없는 것이 정상일 수 있습니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get hpa -o json \
| jq '.items[] | {name:.metadata.name,target:.spec.scaleTargetRef,min:.spec.minReplicas,max:.spec.maxReplicas,current:.status.currentReplicas,desired:.status.desiredReplicas,metrics:.status.currentMetrics,conditions:.status.conditions}'
kubectl --context "$KUBE_CONTEXT" get apiservice v1beta1.metrics.k8s.io
kubectl --context "$KUBE_CONTEXT" get --raw "/apis/metrics.k8s.io/v1beta1/namespaces/$NAMESPACE/pods"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get events \
--sort-by='.metadata.creationTimestamp'
```
AbleToScale·ScalingActive·ScalingLimited 등 HPA 조건, 현재 metrics와 behavior를 확인합니다. desired/current replica의 일시적 차이가 곧 장애는 아닙니다. CPU·memory utilization target에는 requests가 필요하며 custom·external metrics는 별도 adapter·KEDA 연동을 사용합니다. metric 오류는 scale-down을 막을 수 있습니다.
CA는 설치 릴리스, 지원 Kubernetes minor, identity, discovery tag, unschedulable Pod 제약과 group max·quota를 확인합니다. 평균 node CPU가 높다는 이유만으로 node를 추가하지 않습니다. Karpenter·Auto Mode는 해당 NodePool·NodeClaim·provider limit과 event를 확인하며 CA를 보편적 해결책으로 설치하지 않습니다.
VPA는 추천만 하는 Off, 생성 시점 Initial과 의도적인 update mode를 구분합니다. Auto는 Recreate로 대체되어 deprecated 상태이며 mode 변경은 워크로드 중단·동일 CPU/memory 신호를 쓰는 HPA 충돌을 일으킬 수 있습니다. API 조회 실패가 VPA CRD 미설치를 증명하지는 않습니다.
아래는 custom metrics 구성이 아닌 **resource metrics** HPA 예시입니다.
```yaml
# Resource metrics example, not a custom/external-metrics adapter configuration.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: app-hpa
namespace: applications
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
```
70%·80%, replica 범위와 stabilization window는 예시이며 측정 기반 권고가 아닙니다. target Deployment, 적절한 requests와 용량이 필요합니다. HPA는 여러 metrics 중 가장 큰 replica 추천을 선택하며 memory 동작·adapter 오류는 워크로드별 테스트가 필요합니다. replica 소유자를 하나로 유지하고 Terraform·GitOps가 spec.replicas를 계속 되돌리지 않게 합니다.
[자동 스케일링 개념](https://www.atomai.click/kubernetes-docs/llms/ko/core/09-cluster-administration.md)과 설치한 컨트롤러의 설정 문서를 사용합니다. 미검토 master manifest를 적용하거나 node role에 AutoScalingFullAccess를 주기보다 실제 Helm values·identity·검증하고 고정한 릴리스를 확인합니다.
출처: [EKS node repair](https://docs.aws.amazon.com/eks/latest/userguide/node-repair.html), [Pod 생명주기](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/), [private image pull](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/), [HPA](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/), [VPA](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler).
## IAM 및 인증 문제
### 클러스터 접근 거부
AWS 호출자, EKS API 권한, kubeconfig·STS 인증, 클러스터 identity mapping과 Kubernetes 인가를 구분합니다. EKS 컨트롤 플레인의 IAM 역할과 운영자 역할은 다릅니다. AWS DescribeCluster 성공만으로 Kubernetes 접근이 부여되지 않습니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"
aws sts get-caller-identity
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query 'cluster.{arn:arn,endpoint:endpoint,access:accessConfig}'
kubectl config view --context "$KUBE_CONTEXT" --minify \
-o jsonpath='{.contexts[0].name}{"\n"}{.clusters[0].cluster.server}{"\n"}'
kubectl --context "$KUBE_CONTEXT" auth can-i get pods -n "$NAMESPACE"
```
전송 오류는 접근 절의 endpoint·CA 검사를 사용합니다. 만료된 자격 증명은 설정한 SSO·federation·assumed-role 세션을 갱신하고 선택한 profile·role을 확인합니다. `sts get-session-token`은 보편적 갱신 명령이 아니며 출력에 자격 증명이 노출될 수 있습니다.
identity mapping을 판단하기 전에 클러스터 인증 모드를 확인합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${PRINCIPAL_ARN:?Set the exact IAM principal}"
# These APIs apply to clusters with API or API_AND_CONFIG_MAP authentication.
aws eks list-access-entries --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION"
aws eks describe-access-entry --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--principal-arn "$PRINCIPAL_ARN"
aws eks list-associated-access-policies --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--principal-arn "$PRINCIPAL_ARN"
```
API 접근은 정확한 principal, entry type, Kubernetes group과 연결 policy의 namespace·cluster scope를 확인합니다. `aws-auth`는 CONFIG_MAP·혼합 모드의 legacy 경로에 관련되며 부재가 전체 접근 장애를 뜻하지 않습니다. node mapping을 유지하고 인증 모드 변경 전에 문서화된 마이그레이션 방향을 확인합니다.
짧은 예시로 `aws-auth` 전체를 덮어쓰거나 일반 해결책으로 `system:masters`를 부여하지 않습니다. namespace 제한 권한을 추가해도 다른 binding·access policy의 더 넓은 권한이 취소되지는 않습니다. 접근 소유자를 통해 의도한 경로를 수정합니다.
### RBAC 문제
인증 실패, Kubernetes Forbidden, impersonation 요청 실패는 다른 근거입니다. 정확한 verb, resource·subresource, namespace와 subject kind를 확인합니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get roles,rolebindings
kubectl --context "$KUBE_CONTEXT" get clusterroles,clusterrolebindings
: "${SUBJECT_NAME:?Set the exact subject name}"
kubectl --context "$KUBE_CONTEXT" get rolebindings -A -o json \
| jq --arg name "$SUBJECT_NAME" '.items[] | select(any(.subjects[]?; .name == $name)) | {namespace:.metadata.namespace,name:.metadata.name,roleRef,subjects}'
kubectl --context "$KUBE_CONTEXT" get clusterrolebindings -o json \
| jq --arg name "$SUBJECT_NAME" '.items[] | select(any(.subjects[]?; .name == $name)) | {name:.metadata.name,roleRef,subjects}'
```
쿼리는 이름이 일치하는 후보를 찾습니다. 같은 문자열이 다른 주체일 수 있으므로 kind, ServiceAccount namespace와 roleRef를 확인합니다. `kubectl auth can-i --as=...`는 impersonation 권한이 필요합니다. impersonated RBAC 검사·`--list`는 EKS access policy 권한 전체를 재현하지 못하므로 실제 의도한 주체로도 확인합니다.
예를 들어 표준 IAM access entry를 `eks-troubleshoot-readers` 그룹에 의도적으로 매핑한 뒤 다음 Role로 namespace의 Pod·Service·event·EndpointSlice 읽기와 Pod log만 부여할 수 있습니다.
```yaml
# Example for a deliberately mapped Kubernetes group in an existing namespace.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: troubleshooting-reader
namespace: applications
rules:
- apiGroups: [""]
resources: [pods, services, events]
verbs: [get, list, watch]
- apiGroups: [""]
resources: [pods/log]
verbs: [get]
- apiGroups: [discovery.k8s.io]
resources: [endpointslices]
verbs: [get, list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: troubleshooting-readers
namespace: applications
subjects:
- kind: Group
name: eks-troubleshoot-readers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: troubleshooting-reader
apiGroup: rbac.authorization.k8s.io
```
namespace는 이미 존재해야 하고 group mapping은 별도 검토합니다. 앱 ServiceAccount라면 kind ServiceAccount와 정확한 이름·namespace를 지정한 별도 binding을 사용합니다. node·namespace 읽기는 cluster scope이므로 검토한 ClusterRole이 필요합니다. 모든 진단 사용자에게 cluster-admin을 부여하지 않습니다. Secret 읽기가 없어도 log 권한으로 앱 데이터가 노출될 수 있습니다.
### IRSA 및 Pod Identity 문제
IRSA는 **워크로드 AWS 자격 증명**을 제공하며 cluster IAM OIDC provider는 사람 IAM 주체의 Kubernetes RBAC 접근 방식이 아닙니다. 실제 Pod ServiceAccount, trust·permission policy, SDK credential chain과 서비스 endpoint 접근을 확인합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${KUBE_CONTEXT:?}"
: "${NAMESPACE:?}"; : "${SERVICE_ACCOUNT:?}"; : "${POD_NAME:?}"; : "${ROLE_NAME:?}"
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster.identity.oidc.issuer --output text
aws iam get-role --role-name "$ROLE_NAME" --query 'Role.{Arn:Arn,Trust:AssumeRolePolicyDocument}'
aws iam list-attached-role-policies --role-name "$ROLE_NAME"
aws iam list-role-policies --role-name "$ROLE_NAME"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get serviceaccount "$SERVICE_ACCOUNT" -o json \
| jq '{name:.metadata.name,namespace:.metadata.namespace,annotations:.metadata.annotations}'
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pod "$POD_NAME" -o json \
| jq '{serviceAccount:.spec.serviceAccountName,containers:[.spec.containers[] | {name,awsEnvironmentNames:[.env[]? | select(.name | startswith("AWS_")) | .name]}]}'
```
여기서는 AWS 환경 변수 이름만 출력합니다. 전체 env 값이나 projected token을 출력하지 않습니다. IRSA는 지원 SDK가 projected web-identity token과 지정 역할을 사용하는지 확인합니다. 더 앞선 static·default credential source가 우선할 수 있습니다.
아래 trust 예시는 ServiceAccount subject 하나와 STS audience에 연결합니다. 계정·partition·리전·issuer ID·subject를 검증한 값으로 바꾸며 공유 역할에 그대로 덮어쓸 정책이 아닙니다.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/EXAMPLE"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-west-2.amazonaws.com/id/EXAMPLE:aud": "sts.amazonaws.com",
"oidc.eks.us-west-2.amazonaws.com/id/EXAMPLE:sub": "system:serviceaccount:applications:app"
}
}
}
]
}
```
역할 변경 시 기존의 정당한 trust statement를 유지합니다. assume 성공 후에도 IAM permission·resource policy·KMS grant·조직 제어가 AWS 동작을 거부할 수 있습니다. ServiceAccount annotation 변경은 기존 Pod에 env·volume을 소급 주입하지 않으므로 소유자를 통해 rollout을 조율합니다.
현재 EKS는 OIDC discovery·JWKS용 별도 `com.amazonaws..oidc-eks` PrivateLink endpoint를 지원합니다. EKS 관리 endpoint, Pod Identity용 eks-auth, STS와 구분합니다. 사설 OIDC 접근만으로 IAM provider·role trust·STS 접근이 제공되지는 않습니다.
EKS Pod Identity는 정확한 namespace·ServiceAccount의 association을 확인합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${NAMESPACE:?}"; : "${SERVICE_ACCOUNT:?}"
aws eks list-pod-identity-associations --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--namespace "$NAMESPACE" --service-account "$SERVICE_ACCOUNT"
```
반환된 association ID, role trust·permission과 agent·SDK·compute 지원 조건을 확인합니다. IRSA annotation 부재가 Pod Identity 장애는 아닙니다. association 변경, credential cache와 앞선 SDK provider를 고려합니다. 모든 소비자를 검증하기 전에 identity 방식을 바꾸거나 기존 IRSA trust를 제거하지 않습니다. 현재 설정·마이그레이션은 [보안 장](https://www.atomai.click/kubernetes-docs/llms/ko/eks/05-eks-security.md)을 확인합니다.
### 노드 조인 실패
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${NODEGROUP_NAME:?}"
aws eks describe-nodegroup --cluster-name "$CLUSTER_NAME" --nodegroup-name "$NODEGROUP_NAME" \
--region "$AWS_REGION" \
--query 'nodegroup.{name:nodegroupName,status:status,health:health,nodeRole:nodeRole,subnets:subnets,amiType:amiType,release:releaseVersion,launchTemplate:launchTemplate}'
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query 'cluster.{endpoint:endpoint,access:accessConfig,vpc:resourcesVpcConfig}'
```
관리형 그룹은 health.issues, launch-template·AMI·bootstrap과 실제 EC2 상태를 사용합니다. 자체 관리·hybrid는 관리형 API가 설명한다고 가정하지 말고 해당 bootstrap·등록 방식을 확인합니다.
instance-profile ARN이 아닌 IAM **role ARN**과 적절한 node 인증 mapping·access entry를 확인합니다. 관리형 그룹·Fargate에는 서비스가 관리하는 identity 동작이 있으므로 매핑을 덮어쓰지 않습니다. node entry의 type·의미는 사람용 standard entry와 다릅니다. role path·legacy aws-auth 제약도 인증 모드별 문서를 따릅니다.
필요 node policy·ECR pull 권한을 확인하되 지원되는 CNI·CSI·앱 권한은 실제 identity에 유지합니다. node role에 AmazonEKSClusterPolicy나 모든 CNI·storage 권한을 붙이는 것은 보편적인 등록 해결책이 아닙니다.
node→API server HTTPS, API server→kubelet 10250과 실제 webhook·의존성 경로의 DNS·route·NACL·SG 방향을 확인합니다. backend webhook 규칙 때문에 모든 node에 임의 출발지의 inbound 443이 필요한 것은 아닙니다.
AMI의 bootstrap 모델을 따릅니다. AL2023·nodeadm, Bottlerocket 설정과 custom AMI의 전제 조건은 다릅니다. AL2 스크립트 재실행·kubelet만 교체하는 것으로 모든 이미지를 복구하지 못합니다. 검증한 원인을 해결한 뒤 근거를 보존하고 소유자의 node 교체 절차를 따릅니다.
출처: [EKS access entry](https://docs.aws.amazon.com/eks/latest/userguide/access-entries.html), [RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/), [IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html), [OIDC PrivateLink](https://docs.aws.amazon.com/eks/latest/userguide/irsa-fetch-keys.html), [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html).
## 스토리지 문제

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-09-eks-troubleshooting-5.html)
### PVC·PV·소비자 진단
정확한 namespace·claim·UID, provisioner와 소비자부터 확인합니다. WaitForFirstConsumer의 Pending은 적합한 소비 Pod가 스케줄될 때까지 정상일 수 있습니다. storage controller뿐 아니라 해당 Pod의 scheduling·zone·capacity 제약도 확인합니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${PVC_NAME:?}"
PVC_JSON=$(kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pvc "$PVC_NAME" -o json)
printf '%s\n' "$PVC_JSON" | jq '{name:.metadata.name,uid:.metadata.uid,status:.status,spec:.spec,storageClassFieldPresent:(.spec | has("storageClassName"))}'
PVC_UID=$(printf '%s\n' "$PVC_JSON" | jq -er '.metadata.uid')
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get events \
--field-selector "involvedObject.uid=$PVC_UID" --sort-by='.metadata.creationTimestamp'
SC_NAME=$(printf '%s\n' "$PVC_JSON" | jq -r '.spec.storageClassName // empty')
if [ -n "$SC_NAME" ]; then
kubectl --context "$KUBE_CONTEXT" get storageclass "$SC_NAME" -o yaml
else
printf 'Inspect absent versus explicitly empty storageClassName and default/static binding intent.\n'
fi
PV_NAME=$(printf '%s\n' "$PVC_JSON" | jq -r '.spec.volumeName // empty')
if [ -n "$PV_NAME" ]; then
kubectl --context "$KUBE_CONTEXT" get pv "$PV_NAME" -o yaml
kubectl --context "$KUBE_CONTEXT" get volumeattachments -o json \
| jq --arg pv "$PV_NAME" '.items[] | select(.spec.source.persistentVolumeName == $pv) | {name:.metadata.name,spec,status}'
fi
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pods -o json \
| jq --arg pvc "$PVC_NAME" '.items[] | select(any(.spec.volumes[]?; .persistentVolumeClaim.claimName == $pvc)) | {name:.metadata.name,node:.spec.nodeName,phase:.status.phase,conditions:.status.conditions}'
```
Pod API에는 `spec.volumes.persistentVolumeClaim.claimName` field selector가 없으므로 위 JSON 쿼리로 소비자를 찾습니다. storageClassName 부재와 명시적 빈 문자열은 default·static binding 의도가 다릅니다. ``라는 class를 그대로 조회하지 않습니다.
PVC YAML은 객체 설정이며 데이터 백업이 아닙니다. claim 삭제·재생성은 Delete 정책에서 실제 storage를 삭제하거나 Retain PV를 별도 재바인딩 대상으로 남길 수 있습니다. Pending·FailedMount의 일반 해결책으로 사용하지 않습니다. 생명주기 변경 전에 reclaim policy, snapshot·backup과 workload·data 소유권을 확인합니다. Bound만으로 앱의 mount·read를 증명하지 못합니다.
### EBS 볼륨 문제
#### Driver와 실제 volume 확인
표준 `ebs.csi.aws.com`, Auto Mode `ebs.csi.eks.amazonaws.com`, legacy·migrated volume과 소유자를 구분합니다. 표준 EBS CSI controller는 구성된 IAM identity를 사용하므로 node role만 조사해서는 충분하지 않습니다. 실제 KMS key 권한과 controller·node 구성 요소 상태도 확인합니다.
Auto Mode node의 root·data volume 암호화가 모든 workload PVC의 암호화를 보장하지는 않습니다. 현재 [Auto Mode StorageClass reference](https://docs.aws.amazon.com/eks/latest/userguide/create-storage-class.html)는 encrypted 기본값을 false로 명시합니다. 두 EBS provisioner 모두 `encrypted: "true"`를 명시하고 실제 EBS volume·key를 확인합니다. 계정의 기본 암호화와 snapshot 속성도 결과에 영향을 줄 수 있습니다.
Auto Mode 자체 volume에는 표준 EBS CSI controller를 별도 설치할 필요가 없습니다. EBS는 Fargate Pod에 mount할 수 없고 EKS Hybrid Nodes도 EBS CSI driver·volume 지원 대상이 아닙니다. Fargate에서 controller를 실행할 수 있어도 Fargate workload의 EBS mount를 지원한다는 뜻은 아닙니다.
```bash
set -euo pipefail
: "${AWS_REGION:?}"; : "${VOLUME_ID:?Verify it from the selected PV CSI volumeHandle}"
aws ec2 describe-volumes --region "$AWS_REGION" --volume-ids "$VOLUME_ID" \
--query 'Volumes[].{Id:VolumeId,State:State,AZ:AvailabilityZone,Type:VolumeType,Size:Size,Encrypted:Encrypted,KmsKey:KmsKeyId,Attachments:Attachments}'
aws ec2 describe-volume-status --region "$AWS_REGION" --volume-ids "$VOLUME_ID"
```
선택한 Pod·node와 EBS가 같은 AZ에서 사용할 수 있는지, attachment limit, CSI 오류와 VolumeAttachment·EC2 상태를 확인합니다. PVC 이름은 namespace·과거 volume 사이에서 고유하지 않으므로 PV에서 정확한 volume을 식별합니다.
#### Attach·mount 복구
Multi-Attach는 기존 소비자가 아직 실행·쓰기 중인지, node가 연결·격리되었는지, rollout이 다른 node에 두 번째 소비자를 배치했는지 확인합니다. ReadWriteOnce는 단일 node access mode이지 단일 Pod lock이 아닙니다. workload shutdown과 CSI unmount·detach를 조율하며 Pod 객체를 삭제했다고 실제 프로세스가 멈췄다고 가정하지 않습니다.
수동 attach·detach는 CSI 조정을 대체하지 못합니다. 수동 복구가 필요하면 writer 정지를 확인하고 데이터를 보호한 뒤 EBS 복구 절차를 따릅니다. mount된 volume을 분리하면 데이터가 손상될 수 있습니다. 반복 reboot·force detach로 attachment 오류를 우회하지 않습니다.
#### StorageClass와 프로비저닝
**새** 표준 driver class에 암호화·지연 binding·보존을 명시한 예시입니다.
```yaml
# New, explicitly selected StorageClass for standard EBS CSI, not an in-place edit.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: diagnostic-ebs-gp3
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: "true"
csi.storage.k8s.io/fstype: ext4
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
reclaimPolicy: Retain
```
기존 PVC class를 변경하지 않습니다. 기존 StorageClass의 provisioner·parameters·binding mode는 자유롭게 수정할 수 없으며 default class 변경은 다른 claim에도 영향을 줍니다. Retain은 별도 정리 결정을 위해 storage를 남기므로 비용이 계속 발생할 수 있습니다. 확장은 driver·class·filesystem 지원이 필요하며 요청 용량을 줄여 PVC를 축소할 수 없습니다.
기존 소유자를 통해 호환 CSI add-on과 실제 IRSA·Pod Identity·전체 설정을 구성합니다. `eksctl create iamserviceaccount --role-only`는 역할을 만들 뿐 add-on에 연결하지 않습니다. 강제 재설치 대신 조사한 add-on identity와 업데이트 절차를 사용합니다.
Auto Mode 이전은 [EBS 가이드](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html)의 snapshot 경로와 [Auto Mode 이전 가이드](https://docs.aws.amazon.com/eks/latest/userguide/migrate-auto.html)의 workload 정지·Retain·static PV 경로가 문서화되어 있습니다. 선택 전에 적용 driver, tag·IAM, claim·finalizer 생명주기와 복구 계획을 검증합니다. bound claim의 provisioner 문자열 변경은 마이그레이션이 아닙니다. snapshot에는 CSI snapshot controller·CRD, 적절한 class·권한도 필요합니다.
### EFS 문제
#### 프로비저닝과 mount 접근을 구분
PV의 filesystem·access point, 소비 node·AZ, mount target 가용성, DNS·NFS 경로를 확인합니다. access point 생성용 controller API 권한과 mount·파일 접근용 client 권한은 다릅니다. filesystem policy, TLS·IAM 요구, access-point POSIX identity·root-directory 소유권과 앱 UID·GID를 확인합니다.
```bash
set -euo pipefail
: "${AWS_REGION:?}"; : "${FILE_SYSTEM_ID:?Verify the filesystem from the PV}"
aws efs describe-file-systems --region "$AWS_REGION" --file-system-id "$FILE_SYSTEM_ID"
aws efs describe-mount-targets --region "$AWS_REGION" --file-system-id "$FILE_SYSTEM_ID"
aws efs describe-access-points --region "$AWS_REGION" --file-system-id "$FILE_SYSTEM_ID"
: "${MOUNT_TARGET_ID:?Choose the mount target on the affected path}"
aws efs describe-mount-target-security-groups --region "$AWS_REGION" \
--mount-target-id "$MOUNT_TARGET_ID"
```
실제 client network identity와 mount target 사이 TCP 2049, route·NACL·SG를 확인합니다. control-plane subnet에서 node subnet을 추정하지 않습니다. workload AZ·topology에 맞는 mount target을 사용하며 추가 생성은 별도 인프라 변경입니다.
지원 EC2 환경은 설치된 EFS CSI controller·node plugin과 현재 호환 버전을 확인합니다. Fargate는 내장 EFS mount와 문서화된 static provisioning을 사용하며 현재 EKS 가이드는 Fargate node의 dynamic provisioning을 지원하지 않습니다. EFS CSI driver는 Windows container·EKS Hybrid Nodes를 지원하지 않습니다. 오래된 release-1.5 manifest를 무조건 설치하거나 관리형 add-on을 중복 설치하지 않습니다.
#### Dynamic과 static provisioning은 대안 경로
Dynamic provisioning은 **기존** EFS filesystem에 access point를 생성하며 filesystem·mount target을 만들지 않습니다. 예시 ID를 바꾸고 access-point 소유권·권한, quota·보존을 검토합니다.
```yaml
# Dynamic EFS access-point provisioning example for a supported EC2-node setup.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: diagnostic-efs-ap
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap
fileSystemId: fs-0123456789abcdef0
directoryPerms: "700"
mountOptions:
- tls
reclaimPolicy: Retain
```
대신 준비한 기존 access point를 static PV로 참조할 수도 있습니다.
```yaml
# Alternative static provisioning: replace the filesystem/access-point IDs.
apiVersion: v1
kind: PersistentVolume
metadata:
name: diagnostic-efs-static
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
mountOptions:
- tls
csi:
driver: efs.csi.aws.com
volumeHandle: fs-0123456789abcdef0::fsap-0123456789abcdef0
```
static PV에는 검토한 claim의 `storageClassName: ""`와 의도한 volumeName을 지정하고 binding·claimRef 의미를 유지합니다. dynamic class와 혼용하여 filesystem root에 의도치 않게 binding하지 않습니다. 5Gi capacity는 Kubernetes binding metadata이며 EFS directory·filesystem의 강제 용량 한도가 아닙니다.
mount 진단은 기존 Pod·CSI event·log부터 사용합니다. 진단 Pod는 claim과 같은 namespace, 호환 node·identity가 필요합니다. df 확인을 위해 앱 PVC를 read-write mount하면 writer를 추가할 수 있습니다. probe가 필요하면 검토한 read-only mount, 준비한 이미지, 제한된 수명과 소유권에 맞는 정리를 사용합니다. 무관한 EBS volume 생성·수동 device 연결을 storage “테스트”로 실행하지 않습니다.
출처: [EBS CSI](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html), [EFS CSI](https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html), [영구 볼륨](https://kubernetes.io/docs/concepts/storage/persistent-volumes/), [StorageClass](https://kubernetes.io/docs/concepts/storage/storage-classes/).
## 로깅 및 모니터링 문제

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-09-eks-troubleshooting-6.html)
### CloudWatch 로그와 Container Insights
EKS control-plane log 전송, Fluent Bit 등 앱·host collector, CloudWatch agent metrics와 앱 instrumentation을 구분합니다. cluster logging 활성화가 앱 collector를 설치하지 않으며 collector 실행만으로 의도한 account·region·group 도착을 증명하지 못합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${KUBE_CONTEXT:?}"
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster.logging
aws logs describe-log-groups --region "$AWS_REGION" \
--log-group-name-prefix "/aws/eks/$CLUSTER_NAME/"
aws logs describe-log-groups --region "$AWS_REGION" \
--log-group-name-prefix "/aws/containerinsights/$CLUSTER_NAME/"
: "${COLLECTOR_NAMESPACE:?}"; : "${COLLECTOR_POD:?}"; : "${COLLECTOR_CONTAINER:?}"
kubectl --context "$KUBE_CONTEXT" -n "$COLLECTOR_NAMESPACE" get pod "$COLLECTOR_POD" -o wide
kubectl --context "$KUBE_CONTEXT" -n "$COLLECTOR_NAMESPACE" logs "$COLLECTOR_POD" \
-c "$COLLECTOR_CONTAINER" --since=15m --tail=200 --timestamps=true
```
이름·레이블을 가정하지 말고 실제 collector의 namespace·Pod·container·설정·목적지를 사용합니다. input path·parser·filter, buffering·backpressure, filesystem 용량, timestamp, output error, DNS·TLS·endpoint·quota를 확인합니다. log group 부재는 미전송·다른 목적지·조회 권한 부족일 수 있으며 group 생성만으로 producer가 고쳐지지 않습니다.
실제 IRSA·Pod Identity 등 지원 identity를 확인합니다. node policy·ServiceAccount annotation만으로 실제 사용하는 credentials를 증명하지 못합니다. CloudWatch Observability EKS add-on으로 설치한 경우 해당 identity·health를 확인합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"
# Only for an installation actually owned by this EKS add-on.
aws eks describe-addon --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--addon-name amazon-cloudwatch-observability \
--query 'addon.{status:status,version:addonVersion,health:health,role:serviceAccountRoleArn,podIdentity:podIdentityAssociations}'
```
API 오류가 add-on 부재를 증명하지는 않습니다. 오류와 실제 Helm·add-on 소유자를 확인하고 업데이트 시 소유권·사용자 설정을 보존합니다. 오래된 미렌더링 Fluentd·Fluent Bit quickstart를 적용하거나 기존 ServiceAccount를 덮어쓰는 것을 일반 복구로 사용하지 않습니다. Windows·Fargate·Auto Mode·EC2 수집 경로는 다릅니다.
Container Insights는 실제 metric namespace·dimension·time window를 조회합니다. metric 목록은 metadata이며 현재 datapoint·alarm·notification 동작 증명이 아닙니다. 제한된 data query와 collector log를 비교합니다. 설정·로그 근거를 보호하고 retention·KMS 변경은 group 소유자를 통해 수행합니다.
### Metrics Server와 Resource Metrics
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"
kubectl --context "$KUBE_CONTEXT" get apiservice v1beta1.metrics.k8s.io -o yaml
kubectl --context "$KUBE_CONTEXT" get --raw "/apis/metrics.k8s.io/v1beta1/namespaces/$NAMESPACE/pods"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" top pods --containers
kubectl --context "$KUBE_CONTEXT" top nodes
```
resource Metrics API, kube-state-metrics의 객체 metrics, Prometheus·cAdvisor sample은 서로 다른 데이터 소스입니다. APIService condition, aggregator·RBAC 접근, Metrics Server log와 kubelet 연결을 확인합니다.
Unauthorized는 어떤 호출자·endpoint가 credentials를 거부했는지, Forbidden은 정확한 RBAC verb·resource를 확인합니다. scrape 실패는 route, kubelet address·port, certificate, authentication 또는 kubelet 장애 때문일 수 있습니다. 과거 unauthenticated 10255를 열거나 kubelet-insecure-tls를 보편적인 해결책으로 사용하지 않습니다.
v1.Pod resource-not-found만으로 EKS API-server 설정 결함을 확정하지 않습니다. kubeconfig·URL, discovery, client 호환성과 proxy 응답을 확인합니다. 근거 수집 전 최신 Metrics Server 재설치·전체 Pod 재시작은 문제를 숨길 수 있습니다.
### Prometheus 및 Grafana
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${MONITORING_NAMESPACE:?}"; : "${MONITORING_RELEASE:?}"
helm status "$MONITORING_RELEASE" --namespace "$MONITORING_NAMESPACE" --kube-context "$KUBE_CONTEXT"
helm history "$MONITORING_RELEASE" --namespace "$MONITORING_NAMESPACE" --kube-context "$KUBE_CONTEXT"
kubectl --context "$KUBE_CONTEXT" -n "$MONITORING_NAMESPACE" get pods,services,pvc
kubectl --context "$KUBE_CONTEXT" -n "$MONITORING_NAMESPACE" get events \
--sort-by='.metadata.creationTimestamp'
```
실제 chart·operator, release namespace, service port, storage와 selector를 식별합니다. prometheus-community/prometheus는 standalone chart이며 설치만으로 ServiceMonitor용 Operator 조정이 제공되지 않습니다. Operator stack에는 CRD, Prometheus custom resource와 selector·RBAC 요구가 있습니다.
기존 Prometheus를 검사하려면 한 터미널에서 loopback 전용 port-forward를 유지합니다.
```bash
: "${KUBE_CONTEXT:?}"; : "${MONITORING_NAMESPACE:?}"
: "${PROMETHEUS_SERVICE:?Select the actual Prometheus Service}"
: "${PROMETHEUS_SERVICE_PORT:?Select its Service port}"
kubectl --context "$KUBE_CONTEXT" -n "$MONITORING_NAMESPACE" port-forward \
--address 127.0.0.1 "service/$PROMETHEUS_SERVICE" "9090:$PROMETHEUS_SERVICE_PORT"
```
이 접근을 허용하는 endpoint에 대해 두 번째 로컬 터미널에서 조회합니다.
```bash
set -euo pipefail
curl --fail --silent --show-error --max-time 10 \
http://127.0.0.1:9090/api/v1/targets \
| jq '.data.activeTargets[] | {scrapePool,health,lastError,lastScrape}'
```
실제 endpoint의 scheme·authentication에 맞추고 접근 제어를 우회하지 않습니다. scrape error, relabeling, discovery, query window와 retention을 확인합니다. 빈 쿼리는 label·data 부재일 수 있으며 정상 사용량 0이 아닙니다.
#### ServiceMonitor 선택
아래는 기존 앱의 이름 있는 metrics port와 Operator ServiceMonitor를 연결하는 예시입니다.
```yaml
# Requires an existing application exporting metrics on a named container port "metrics".
apiVersion: v1
kind: Service
metadata:
name: app-metrics
namespace: applications
labels:
app: metrics-demo
spec:
selector:
app: metrics-demo
ports:
- name: metrics
port: 9090
targetPort: metrics
---
# Requires Prometheus Operator and a Prometheus CR selecting this namespace/label.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: app-metrics
namespace: monitoring
labels:
release: observability
spec:
namespaceSelector:
matchNames:
- applications
selector:
matchLabels:
app: metrics-demo
endpoints:
- port: metrics
path: /metrics
interval: 30s
```
namespace·label·release selector를 실제 설치에 맞춥니다. Prometheus의 ServiceMonitor namespace 선택, serviceMonitorSelector의 monitor label 선택, monitor namespaceSelector·selector의 Service 선택을 구분합니다. endpoints.port는 임의 container port가 아닌 **Service port 이름**입니다. 앱이 실제로 listen하고 기대한 metrics path를 제공해야 합니다. Prometheus에는 discovery 권한과 network·TLS·auth 접근도 필요합니다.
#### 설정 보존과 변경 검증
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${MONITORING_NAMESPACE:?}"; : "${MONITORING_RELEASE:?}"
: "${EVIDENCE_PARENT:?Set an existing private directory}"
umask 077
MONITORING_EVIDENCE=$(mktemp -d "$EVIDENCE_PARENT/monitoring-config.XXXXXXXX")
helm get values "$MONITORING_RELEASE" --namespace "$MONITORING_NAMESPACE" \
--kube-context "$KUBE_CONTEXT" --all > "$MONITORING_EVIDENCE/values.yaml"
printf 'Protected configuration snapshot: %s\n' "$MONITORING_EVIDENCE"
```
보호된 파일의 values에도 민감 데이터가 있을 수 있으므로 공유 전에 가립니다. 대상 chart·version 기본값, CRD migration, custom values, workload resources와 PVC 용량을 검토합니다. 기존 소유자를 통해 업데이트하고 scrape·alert 동작을 검증합니다. stack 중복 설치·무조건 resource limit patch는 진단이 아닙니다.
Grafana는 datasource UID·URL·authentication, network, query label·time range와 dashboard provisioning·sidecar 선택을 확인합니다. 기대한 label·namespace 없는 ConfigMap이 자동으로 dashboard가 되지는 않습니다. 기존 login·SSO를 사용하고 관리자 비밀번호를 진단 로그에 출력하지 않습니다.
검토한 설치·query·alert 절차는 [EKS 모니터링과 로깅](https://www.atomai.click/kubernetes-docs/llms/ko/eks/06-eks-monitoring-logging.md)을 참고합니다. 위 예시는 진단·설정 template이며 실제 log 전송·monitoring coverage·운영 준비 검증을 주장하지 않습니다.
출처: [CloudWatch EKS add-on](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Container-Insights-setup-EKS-addon.html), [Metrics Server](https://github.com/kubernetes-sigs/metrics-server), [Prometheus Operator 문제 해결](https://prometheus-operator.dev/docs/platform/troubleshooting/).
## 성능 문제
### 비교 가능한 기준선 수립
워크로드, 요청률, 지연·오류 분포, requests·limits, node·AMI·runtime, placement와 기간을 기록합니다. 현재 사용량과 예약 용량·포화는 다르며 메모리 증가만으로 누수를 증명하지 못합니다. node·Pod·storage·network·application 병목을 구분합니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${POD_NAME:?}"; : "${NODE_NAME:?}"
kubectl --context "$KUBE_CONTEXT" top nodes
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" top pods --containers
kubectl --context "$KUBE_CONTEXT" get node "$NODE_NAME" -o json \
| jq '{nodeInfo:.status.nodeInfo,allocatable:.status.allocatable,conditions:.status.conditions}'
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pod "$POD_NAME" -o json \
| jq '{containers:[.spec.containers[] | {name,resources}],status:.status}'
```
메트릭 수집 경로가 정상이어야 합니다. CPU throttling, working set·RSS·heap, OOM 종료, disk latency·queue와 network drop을 필요에 따라 포함합니다. 앱 부하와 연결하며 무관한 워크로드에 같은 100m·128Mi를 처방하지 않습니다.
### 노드 성능 문제
확인한 원격 Linux node 세션에서 필요한 도구가 있을 때 제한된 관찰을 수행합니다.
```bash
top -b -n 1
free -m
df -h
df -i
iostat -x 1 5
ip -s link
ss -s
```
이를 실행하려고 운영 앱 container에 도구를 설치하지 않습니다. 준비된 진단 이미지·지원 node 접근을 사용하고 host 변경 전에 근거를 보존합니다.
node 추가는 schedulable capacity에 도움이 되지만 instance별 network·ENI·EBS 한도를 바꾸지 않습니다. instance type은 지원 교체·launch-template 경로를 사용하며 `update-nodegroup-config --launch-template`는 유효한 명령이 아닙니다. 소유자 몰래 resize하지 말고 autoscaling·placement와 조율합니다.
#### Kernel 설정
튜닝 전에 의도한 host 또는 Pod namespace의 관련 설정을 확인합니다.
```bash
sysctl net.ipv4.ip_local_port_range net.ipv4.tcp_fin_timeout
sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog fs.file-max
```
여러 network sysctl은 namespaced입니다. hostPID만으로 host network namespace에 들어가지 않으므로 privileged DaemonSet이 다른 network namespace와 node-global 설정을 섞어 변경할 수 있습니다. 허용된 namespaced 설정은 Pod securityContext.sysctls, node-level 설정은 소유 node 구성으로 관리합니다. kernel·Kubernetes policy 지원, 격리와 효과를 확인한 뒤 변경합니다. 임의 cluster-wide privileged sysctl 튜닝은 성능 진단이 아닙니다.
#### EBS 성능 변경
변경 전에 실제 volume type·IOPS·throughput과 instance EBS 대역폭을 확인합니다. EC2 modify-instance-attribute block-device mapping은 volume type·IOPS·throughput 튜닝 인터페이스가 아니며 EBS 작업은 ModifyVolume입니다. CSI storage는 지원하는 storage-owner 경로로 구성 일치를 유지합니다.
다음 선택적 **변경**은 owner가 지원 limit·ratio, instance 능력, 비용, 기존 modification 상태와 app·data 영향을 검토했다고 가정합니다.
```bash
set -euo pipefail
: "${AWS_REGION:?}"; : "${VOLUME_ID:?Verify the owned EBS volume}"
: "${TARGET_IOPS:?Set a reviewed supported gp3 IOPS value}"
: "${TARGET_THROUGHPUT:?Set a reviewed supported gp3 MiB/s value}"
: "${EVIDENCE_PARENT:?Set an existing private directory}"
umask 077
EBS_CHANGE_DIR=$(mktemp -d "$EVIDENCE_PARENT/ebs-performance.XXXXXXXX")
aws ec2 describe-volumes --region "$AWS_REGION" --volume-ids "$VOLUME_ID" \
--output json > "$EBS_CHANGE_DIR/before.json"
# Separate approved volume change; this is not a diagnostic read.
aws ec2 modify-volume --region "$AWS_REGION" --volume-id "$VOLUME_ID" \
--volume-type gp3 --iops "$TARGET_IOPS" --throughput "$TARGET_THROUGHPUT" \
--output json > "$EBS_CHANGE_DIR/request.json"
aws ec2 describe-volumes-modifications --region "$AWS_REGION" --volume-ids "$VOLUME_ID" \
--output json
```
비동기 요청이므로 modifying·optimizing·completed·failed를 추적합니다. 첫 응답은 완료가 아닙니다. 변경 빈도 제한을 확인하고 이전 modification이 끝난 뒤 다음 요청을 합니다. 용량 증가에는 별도 filesystem 확장도 고려합니다. 기존 16000 IOPS·1000 MiB/s는 설정 예시이며 측정한 보편적 최적값이 아닙니다. 이번 검토에서 EBS 변경을 실행하지 않았습니다.
### 파드 성능과 메모리 문제
container별 사용량·limit, restart·termination reason을 요청 부하와 연결합니다. OOMKilled는 container status에서 확인하며 Event reason 문자열에 없을 수 있습니다. node에 여유 메모리가 있어도 cgroup limit으로 OOM이 날 수 있습니다. cache 증가, allocator, burst, reachable retained object는 다른 조사이며 주기적 GC·node reboot는 일반 누수 해결책이 아닙니다.
실제 runtime·version에 맞는 profiler를 검토한 절차로 대상 process에 연결합니다. node --inspect는 새 프로세스를 시작하며 기존 앱에 자동 연결하지 않습니다. JVM·Python·Go profiler에는 도구·symbol·code·endpoint 전제 조건이 있습니다. heap dump는 pause·disk 고갈·secret 노출을 일으킬 수 있으므로 수집을 제한하고 보호합니다. 이번 장 검토에서는 profile·benchmark를 실행하지 않았습니다.
측정한 필요량·runtime overhead로 requests·limits를 조정하고 quota·rollout·HPA·VPA 소유권을 확인합니다. preferred anti-affinity·ScheduleAnyway topology spread는 선호이며 보장이 아닙니다. strict 규칙은 적격 node가 부족하면 Pending을 만들 수 있습니다. replica 확장으로 단일 thread·storage·downstream 병목이 반드시 해결되지는 않습니다.
### 네트워크 성능 문제
실제 CNI·policy·SG 경로, instance bandwidth·PPS·connection limit, MTU·DNS·placement를 확인합니다. MTU 9001 변경, ENA 활성화, launch-template 교체는 보편적인 라이브 수정이 아닙니다. platform·state 전제 조건과 소유 rollout을 따릅니다. Auto Mode는 이미 node DNS와 자체 network 설정을 제공합니다.
계획한 테스트는 호환 client·server image, resource, node·AZ placement와 TCP 5201 허용을 분리된 범위에 준비합니다. image·version·방향·topology를 기록합니다. 아래는 원래 30초 시간을 유지하면서 단일 stream의 target bitrate를 제한한 예시입니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?Set the approved test context}"
: "${TEST_NAMESPACE:?}"; : "${CLIENT_POD:?}"; : "${CLIENT_CONTAINER:?}"
: "${SERVER_IP:?Set the prepared test server IP}"
# Existing prepared test client/server only: one stream, 30 seconds, 10 Mbit/s target.
kubectl --context "$KUBE_CONTEXT" -n "$TEST_NAMESPACE" exec "$CLIENT_POD" \
-c "$CLIENT_CONTAINER" -- iperf3 -c "$SERVER_IP" -P 1 -t 30 -b 10M -J
```
10 Mbit/s는 test pacing이며 예상 성능·network ceiling 증명이 아닙니다. 여러 stream은 각각 해당 제한을 적용받습니다. 먼저 client·server Ready를 확인하고 준비한 테스트 리소스만 정리합니다. DNS timing은 cache hit, upstream lookup과 command·exec overhead를 구분합니다. 실제 근거 없이 운영 throughput·latency나 성공한 benchmark를 주장하지 않습니다.
출처: [Kubernetes sysctl](https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/), [EBS ModifyVolume](https://docs.aws.amazon.com/botocore/latest/reference/services/ec2/client/modify_volume.html), [iperf 매뉴얼](https://software.es.net/iperf/invoking.html).
## 업그레이드 문제

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-09-eks-troubleshooting-8.html)
### 정확한 작업 식별
cluster·node group의 ACTIVE는 특정 요청 결과를 대체하지 못합니다. Update ID, 작업 scope, 대상 version·config, 마지막 성공 단계와 오류를 기록합니다.
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${UPDATE_ID:?}"
: "${UPDATE_KIND:?Set control-plane, nodegroup, or addon}"
args=(--name "$CLUSTER_NAME" --region "$AWS_REGION" --update-id "$UPDATE_ID")
case "$UPDATE_KIND" in
control-plane) ;;
nodegroup) : "${NODEGROUP_NAME:?}"; args+=(--nodegroup-name "$NODEGROUP_NAME") ;;
addon) : "${ADDON_NAME:?}"; args+=(--addon-name "$ADDON_NAME") ;;
*) echo "Invalid UPDATE_KIND" >&2; exit 2 ;;
esac
aws eks describe-update "${args[@]}" --output json --no-cli-pager
```
진행 중 작업은 [EKS 업그레이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/08-eks-upgrades.md)의 제한된 exact-ID 폴러를 사용합니다. Successful만 성공이며 실패·취소·unknown·조회 오류는 조사해야 합니다. 로컬 timeout은 AWS 작업 취소가 아닙니다.
### 컨트롤 플레인과 API 호환성
```bash
set -euo pipefail
: "${CLUSTER_NAME:?}"; : "${AWS_REGION:?}"; : "${TARGET_VERSION:?}"
FILTER=$(jq -n --arg target "$TARGET_VERSION" \
'{categories:["UPGRADE_READINESS"],kubernetesVersions:[$target]}')
aws eks list-insights --cluster-name "$CLUSTER_NAME" --region "$AWS_REGION" \
--filter "$FILTER" --output json
aws eks describe-addon-versions --kubernetes-version "$TARGET_VERSION" --region "$AWS_REGION" \
--output json
```
애드온 카탈로그와 클러스터 버전 카탈로그는 다릅니다. 실제 현재 버전·EKS 카탈로그에서 다음 지원 minor를 선택하며 upstream release로 EKS 지원을 추정하지 않습니다. 보수적 준비로 node를 현재 control-plane version에 맞추되 지원 skew 범위와 구분합니다. 일부 애드온은 control-plane 변경 전에 중간 버전이 필요합니다.
원래 manifest·Helm metadata, runtime API caller, insight와 admission·conversion webhook을 확인합니다. container image 목록이나 현재 반환된 객체의 apiVersion으로 제거된 API 호출 부재를 증명하지 못합니다. migration guide와 본문 upgrade 장의 검증된 Pluto 명령을 사용합니다. kubectl convert는 별도 plugin이지 보편적인 내장 migration·test가 아닙니다.
현재 EKS 가이드에서 일반 upgrade insight의 force 강제는 일시 철회되었으며 ROLLBACK_READINESS 차단과 별개입니다. 이전 명령 timeout만으로 업그레이드를 재제출하지 않고, 시작한 일반 control-plane upgrade를 취소할 수 있다고 가정하지 않습니다.
### 노드 그룹·애드온 복구
PDB allowed disruptions, replica·readiness, 교체 EC2·IP 용량, AMI·bootstrap과 실제 update 오류를 확인합니다. kubectl drain --force는 unmanaged Pod 제거를 허용하며 PDB eviction 검사를 우회하지 않습니다. disable-eviction과 managed-node force update는 별도 중단 의미를 가집니다. 오류를 없애려고 minAvailable을 0으로 만들거나 emptyDir 데이터를 버리지 않습니다.
EKS 최적화 AMI는 Kubernetes version과 AMI release를 모두 검토합니다. custom AMI group은 정확한 API·option으로 원래 launch template의 검토한 새 버전을 사용합니다. 실패한 update가 fleet 자동 롤백을 보장하지 않습니다. 즉시 group을 만들고 삭제하기보다 앞 node·Pod 절과 단계적 upgrade 절차를 따릅니다.
애드온은 version·config·schema, IAM·Pod Identity와 owner를 유지합니다. PRESERVE는 전체 config 병합·기능 보장이 아니며 OVERWRITE는 customization을 버릴 수 있습니다. network·storage add-on 삭제·재생성은 의존 cleanup·workload 접근을 중단할 수 있습니다. 임의 key:value payload나 CoreDNS라고 잘못 표시한 VPC CNI manifest는 유효한 복구가 아닙니다.
### 롤백은 별도 판단
현재 EKS는 완료된 인플레이스 upgrade 후 7일 내 바로 이전 minor로 조건부 rollback을 지원합니다. 자격, support policy, feature 전제 조건, compute별 순서와 ROLLBACK_READINESS를 충족해야 합니다. managed node는 control plane보다 먼저, Auto Mode는 서비스가 node부터 조정합니다. timeout·cancel·disruption은 일반 upgrade와 다릅니다.
force는 rollback insight를 우회할 뿐 전제 조건·Auto Mode disruption 제어를 우회하지 않습니다. version rollback은 workload·data 상태를 보존하며 backup restore가 아니고 add-on도 자동 복원하지 않습니다. Fargate·hybrid·custom node, support policy·복구 조건은 [전체 rollback 절차](https://www.atomai.click/kubernetes-docs/llms/ko/eks/08-eks-upgrades.md)를 따릅니다. Git·CloudFormation rollback이 이 서비스 작업은 아닙니다.
출처: [EKS update](https://docs.aws.amazon.com/eks/latest/userguide/update-cluster.html), [관리형 node update](https://docs.aws.amazon.com/eks/latest/userguide/update-managed-node-group.html), [EKS rollback](https://docs.aws.amazon.com/eks/latest/userguide/rollback-cluster.html), [Auto Mode rollback](https://docs.aws.amazon.com/eks/latest/userguide/rollback-automode.html).
## 일반적인 오류 메시지 및 해결 방법

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-09-eks-troubleshooting-9.html)
아래는 예시 메시지·패턴이며 모든 현재 controller가 같은 문자열을 낸다는 뜻은 아닙니다. 실제 error code, resource UID, request ID와 condition을 사용합니다. 근거가 뒷받침되기 전 원인은 가설입니다.
| 메시지 / 패턴 | 근거와 다음 조치 |
| --- | --- |
| UnsupportedAvailabilityZoneException | 선택한 cluster subnet·AZ가 해당 계정의 EKS를 지원하지 않음. EC2 instance 제공 목록만 보지 말고 오류의 지원 AZ 확인 |
| ResourceLimitExceeded / quota 오류 | 실제 service·quota·적용 limit 확인. 과거 limit 5는 현재 한도가 아님 |
| InvalidParameterException: Error in role params | role 존재·trust, caller iam:PassRole, 필요 권한과 요청 확인. 공유 role을 무조건 새로 만들지 않음 |
| ClusterUnreachable | endpoint DNS·route·SG·NACL·TLS 확인. kubeconfig 재생성만으로 전송을 고치지 못함 |
| You must be logged in ... (Unauthorized) / the server has asked for the client to provide credentials | credential provider·role·profile·exec auth와 cluster auth mode·access entry·legacy mapping 확인 |
| Forbidden | 실제 subject·verb·resource·subresource·namespace·grant 확인. impersonation 실패와 구분 |
| error loading ... .kube/config ... permission denied | 파일·owner·부모 directory 권한 확인. chmod 600만으로 잘못된 owner·path가 고쳐지지 않음 |
| dial tcp: lookup ... no such host | endpoint hostname과 실제 resolver·private path 확인. HTTPS URL 전체를 DNS 조회에 넣지 않음 |
| FailedScheduling ... Insufficient memory | requests·overhead, 적격 node allocatable, placement·quota 비교. 현재 free-memory·top만으로 scheduler 계산을 알 수 없음 |
| Insufficient pods | node allocatable Pod slot·현재 할당 확인. memory request 감소로 slot이 생기지 않음 |
| CrashLoopBackOff | 일반·init container의 현재·이전 종료 상태, previous-instance log, config·probe 확인 |
| ImagePullBackOff | image·digest·platform, node registry route·TLS·rate limit·pull identity를 실제 오류로 확인. credentials를 노출하지 않음 |
| Evicted | Pod reason·message와 node pressure·시각 확인. 적절한 데이터·가용성 복구 적용 |
| FailedCreateServiceEndpoints / EndpointSlice update 오류 | Service selector·type, Pod Ready·endpoint condition, named port와 controller event 확인 |
| EniLimitExceeded / IPAM allocation 오류 | 실제 ENI·IP·subnet·prefix·quota·오류 context 확인. prefix·custom networking은 모든 한도를 제거하지 않음 |
| FailedLoadBalancerCreation / controller provisioning 오류 | 정확한 owner·subnet·IAM·target health·SG 경로 확인. 무차별 tag·전체 허용 규칙을 추가하지 않음 |
| FailedAttachVolume: Multi-Attach ... | 실제 consumer·attachment·fencing 확인. force detach 대신 CSI unmount·detach와 데이터 안전 조율 |
| FailedMount ... timeout ... | CSI controller·node plugin, identity·KMS·topology·filesystem·attachment event 확인. node restart는 자동 해결책이 아님 |
| PersistentVolumeClaim is not bound | 정상 지연 binding과 class·provisioner·identity·consumer scheduling 문제 구분. claim을 삭제하지 않음 |
| Failed to list *v1.Pod: Unauthorized | 거부 endpoint·caller·token·identity 확인. Metrics Server restart가 누락 credentials를 복원하지 않음 |
| Failed to scrape node | 인증된 kubelet scrape 경로, certificate·address·port·network·node health 확인 |
| Failed to list *v1.Pod: the server could not find the requested resource | EKS control-plane 설정 탓으로 돌리기 전 API URL·context·discovery·client·proxy 확인 |
위 관련 절에서 범위를 제한한 근거를 수집하고 소유자와 수정을 선택합니다. 결과·미확인 가정을 보존하며 예시 메시지만으로 진단·복구 검증을 주장하지 않습니다.
## 퀴즈
[주제 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/09-eks-troubleshooting-quiz)로 이해를 확인하세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/10-eks-resiliency
----------------------------------------
# EKS 고가용성과 복원력 아키텍처
> **예제 API 기준**: Kubernetes 1.36; 현재 지원 EKS와 호환 controller release 선택
> **마지막 업데이트**: 2026년 9월 12일
Amazon EKS 클러스터의 복원력(Resilience)은 장애 발생 시 서비스 영향을 최소화하고 신속하게 복구하는 능력을 의미합니다. 이 문서에서는 EKS 환경에서 고가용성과 복원력을 구현하기 위한 전략, 아키텍처 패턴 및 모범 사례를 제공합니다.
예시는 설정 패턴이며 검증된 운영 플랫폼이 아닙니다. 소유 test 범위에서 image·health contract·IAM·인프라 참조를 검토한 값으로 바꿉니다. 감사 중 cluster 생성·failover·chaos 실험은 하지 않았습니다. Multi-AZ·복제·control-plane SLA만으로 무손실이나 workload 복구 시간이 보장되지는 않습니다.
## 목차
1. [복원력 개요와 성숙도 모델](#복원력-개요와-성숙도-모델)
2. [Multi-AZ 전략 (Level 2)](#multi-az-전략-level-2)
3. [Cell-Based Architecture (Level 3)](#cell-based-architecture-level-3)
4. [Multi-Cluster/Multi-Region (Level 4)](#multi-clustermulti-region-level-4)
5. [애플리케이션 복원력 패턴](#애플리케이션-복원력-패턴)
6. [카오스 엔지니어링](#카오스-엔지니어링)
7. [구현 체크리스트](#구현-체크리스트)
8. [다음 단계](#다음-단계)
---
## 복원력 개요와 성숙도 모델
### 복원력의 정의
복원력(Resilience)은 두 가지 핵심 요소로 구성됩니다:
**1. 장애 영향 최소화 (Failure Impact Minimization)**
- 장애 발생 시 영향 범위(Blast Radius)를 제한
- 전체 시스템이 아닌 일부 구성 요소만 영향을 받도록 설계
- 격리(Isolation)와 중복성(Redundancy)을 통한 장애 격리
**2. 복구 능력 (Recovery Ability)**
- 감지·복원을 포함한 허용 복구 시간(RTO) 목표 수립
- 허용 recovery point·데이터 손실 기간(RPO) 정의 및 검증
- 자가 치유(Self-healing) 메커니즘 구현
### 4단계 성숙도 모델
| 단계 | 범위 | 예시 제어 | 기존 복구 시간 예시, 실측 아님 |
| --- | --- | --- | --- |
| 1. 기본 | Pod·workload | Probe·resource·eviction budget·shutdown | 초–분 |
| 2. Multi-AZ | AZ 장애 | Placement·잔여 용량·traffic·data 복구 | 초–분 |
| 3. Cell | 서비스 partition | Routing·제한한 의존성·용량 | 부분 영향의 초–분 |
| 4. Multi-Region | 리전 장애 | 지역별 traffic·data failover·운영 | 설계에 따라 near-zero 목표부터 분·시간 |
이전 양언어판의 시간 범위는 서로 다른 예시였습니다. 이 모델은 설계 선택을 정리하며 높은 단계가 항상 빠르다는 인증·보장이 아닙니다. 사용자 흐름·의존성별 실제 목표를 정하고 측정합니다.
Regional EKS control plane은 3개 AZ에 분산됩니다. 현재 endpoint SLA는 Standard 월 99.95%·5분 측정 간격, Provisioned 월 99.99%·1분 간격입니다. 조건이 있는 service-credit 약정이며 앱 SLO·RTO·RPO 보장이 아닙니다. [AWS EKS SLA](https://aws.amazon.com/eks/sla/)
> 모든 서비스가 Level 4를 필요로 하지는 않습니다. SLA 요구사항, 규정 준수 요건, 예산에 따라 적절한 수준을 선택하세요.
### Level 1: 기본 복원력 (Pod-level)
가장 기본적인 복원력 수준으로, 단일 Pod 장애에 대응합니다.
#### Liveness/Readiness/Startup Probes
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: resilience-demo
spec:
replicas: 3
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: registry.example.com/team/web-app:replace-with-reviewed-digest
ports:
- name: http
containerPort: 8080
startupProbe:
httpGet:
path: /healthz
port: http
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: http
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
sleep:
seconds: 5
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
```
#### Resource와 Probe 전제
위 image는 공개 artifact가 아닌 앱 placeholder입니다. 배포 전 검증한 digest·실제 health path로 바꿉니다. startup은 초기 liveness·readiness를 억제하며, 외부 의존성 지연만으로 liveness가 실패하게 만들지 않습니다. resource·probe 값은 예시이며 모든 Job·init container에 같은 probe가 필요한 것은 아닙니다.
#### 기본 PodDisruptionBudget
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-app-pdb
namespace: resilience-demo
spec:
maxUnavailable: 1
selector:
matchLabels:
app: web-app
```
---
PDB는 지원되는 자발적 eviction을 제한합니다. 3개 Ready·진행 중 차감 없음이면 maxUnavailable=1로 한 번 허용하지만, AZ·hardware 장애, 직접 Pod 삭제, Deployment 자체 rollout·scale-down의 Pod 수를 보장하지 않습니다. minAvailable 2는 고정 3 replica의 대안이며 한 필드만 선택합니다.
퍼센트는 올림합니다. 8×25%=2, 3×25%는 ceil(0.75)=1이며, 3 replica의 minAvailable 75%는 ceil(2.25)=3으로 정상 Pod eviction 여유가 없습니다. 실제 health·진행 중 disruption을 확인합니다.
## Multi-AZ 전략 (Level 2)
Multi-AZ 전략은 가용 영역(AZ) 장애에 대비하여 워크로드를 여러 AZ에 분산 배치합니다.
### Pod Topology Spread Constraints
배치에 영향을 주지만 건강한 용량을 생성하거나 장애 후 기존 Pod를 자동 이동시키지는 않습니다.
#### Hard Constraint (강제 분산)
조건을 만족하지 못하면 Pod가 스케줄링되지 않습니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: zone-spread-app
spec:
replicas: 6
selector:
matchLabels:
app: zone-spread-app
template:
metadata:
labels:
app: zone-spread-app
spec:
topologySpreadConstraints:
# 가용 영역 간 분산 (Hard)
- maxSkew: 1 # 최대 불균형 허용치
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # Hard constraint
labelSelector:
matchLabels:
app: zone-spread-app
minDomains: 2 # N-1 예시의 eligible-domain 하한; 용량·배치 별도 확인
containers:
- name: app
image: registry.example.com/team/web-app:replace-with-reviewed-digest
```
#### Soft Constraint (선호 분산)
이 기준은 선호도이며 resource·taint·affinity·storage 등 다른 조건을 만족해야 스케줄링됩니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: soft-spread-app
spec:
replicas: 4
selector:
matchLabels:
app: soft-spread-app
template:
metadata:
labels:
app: soft-spread-app
spec:
topologySpreadConstraints:
- maxSkew: 2 # 더 느슨한 불균형 허용
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway # Soft constraint
labelSelector:
matchLabels:
app: soft-spread-app
containers:
- name: app
image: registry.example.com/team/web-app:replace-with-reviewed-digest
```
#### Hard와 Soft 결합
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hybrid-spread-app
spec:
replicas: 9
selector:
matchLabels:
app: hybrid-spread-app
template:
metadata:
labels:
app: hybrid-spread-app
spec:
topologySpreadConstraints:
# AZ 분산: Hard (반드시 여러 AZ에 배치)
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: hybrid-spread-app
minDomains: 2
# 노드 분산: Soft (가능하면 여러 노드에 배치)
- maxSkew: 2
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: hybrid-spread-app
containers:
- name: app
image: registry.example.com/team/web-app:replace-with-reviewed-digest
```
| 파라미터 | 설명 |
|---------|------|
| `maxSkew` | DoNotSchedule에서 후보 domain count와 global minimum의 허용 차이 |
| `topologyKey` | 분산 기준 노드 레이블 (zone, hostname 등) |
| `whenUnsatisfiable` | `DoNotSchedule` (Hard) 또는 `ScheduleAnyway` (Soft) |
| `minDomains` | eligible domain이 더 적으면 global minimum을 0으로 계산 |
eligible zone이 두 개이고 각각 Pod 2개라면 minDomains=3은 global minimum=0으로 maxSkew=1의 replacement를 막을 수 있습니다. minDomains=2는 다른 조건·용량이 충족될 때 배치 계산을 허용합니다. 정확히 2·3개 AZ 점유를 요구하는 값이 아닙니다. 초기 3-AZ 배치와 잔여 AZ headroom을 따로 검증합니다.
### Karpenter Multi-AZ Node Provisioning
Karpenter를 사용하여 여러 AZ에 노드를 자동으로 프로비저닝합니다.
#### NodePool 설정
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: multi-az-nodepool
spec:
template:
spec:
requirements:
# 인스턴스 타입
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-size
operator: In
values: ["medium", "large", "xlarge"]
# 가용 영역 분산
- key: topology.kubernetes.io/zone
operator: In
values: ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
# 용량 타입
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
# Disruption 설정: 동시 20% 제한
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
budgets:
- nodes: "20%" # 해당 자발적 budget: 올림 후 deleting·NotReady 차감
- nodes: "0" # UTC 00–09 = 한국·일본 09–18; 모든 장애를 막지는 않음
schedule: "0 0 * * MON-FRI"
duration: 9h
limits:
cpu: 1000
memory: 1000Gi
weight: 100
```
#### Spot과 On-Demand 혼합 전략
```yaml
# Spot 우선 NodePool
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-preferred
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: topology.kubernetes.io/zone
operator: In
values: ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 30s
budgets:
- nodes: "20%"
weight: 100 # 높은 우선순위
---
# On-Demand 폴백 NodePool
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: on-demand-fallback
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
- key: topology.kubernetes.io/zone
operator: In
values: ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
weight: 50 # 낮은 provisioning 선호도; Spot 불가 때만이라는 보장은 없음
```
zone requirement는 허용 위치이며 균등 node 생성·장애 headroom 예약이 아닙니다. EKS와 호환되는 Karpenter를 선택합니다. schema 기준은 유지 관리되는 1.14.1이며 과거 1.0+가 모든 현재 cluster에 충분하다는 뜻은 아닙니다. EC2NodeClass·AMI·role·subnet·capacity를 준비해야 합니다.
퍼센트 budget은 ceil(total×percentage)에서 deleting·NotReady를 차감하고 적용 budget 중 가장 엄격한 값을 사용합니다. 모든 interruption·expiration·repair·수동 삭제를 제한하지 않습니다. schedule은 UTC이며 예시는 한국·일본 평일 09–18시 블록입니다. 과거 매시간 9–18 시작·9시간 duration은 중첩되어 의도한 한 업무 시간대가 아니었습니다.
Spot/On-Demand weight 100/50은 provisioning 선호도이지 낮은 weight가 Spot 불가 때만 쓰인다는 보장이 아닙니다. 기존 용량·조건·batching도 영향을 주며 NodePool limit는 pre-scaling이 아닌 상한입니다.
### 같은 Zone Service 선호도
```yaml
apiVersion: v1
kind: Service
metadata:
name: web-app
namespace: resilience-demo
spec:
selector:
app: web-app
ports:
- name: http
port: 80
targetPort: http
trafficDistribution: PreferSameZone
```
PreferSameZone·PreferSameNode는 1.35부터 GA이며 1.36 기준에서 유효합니다. PreferClose는 이전 alias입니다. 엄격한 locality·latency·cost 보장이 아닌 선호이며 실제 proxy·EndpointSlice와 Local traffic policy를 확인합니다. topology-mode=Auto는 다른 hint 할당 방식이고 topology-aware-hints는 legacy 설명입니다.
출처: [PDB](https://kubernetes.io/docs/tasks/run-application/configure-pdb/), [Pod 종료](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/), [topology spread](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/), [Service traffic distribution](https://kubernetes.io/docs/concepts/services-networking/service/#traffic-distribution), [Karpenter disruption](https://karpenter.sh/docs/concepts/disruption/).
### ARC Zonal Shift
수동 zonal shift와 자동 zonal autoshift는 별도 동작입니다. EKS 연동은 해당 AZ node를 cordon하고 EndpointSlice에서 endpoint를 제외하며 Pod eviction·node termination을 하지는 않습니다. Auto Mode는 해당 AZ 신규 node와 관련 자발적 disruption을 멈추고, managed node group은 AZ rebalancing을 중지하며 신규 node를 건강한 AZ에 배치합니다. 현재 Karpenter 연동에는 문서의 version·설정·IAM 준비가 필요합니다. Load balancer shift는 별도 resource 작업입니다.
먼저 계정·정확한 managed resource·연동 활성화·기존 practice 설정·alarm 동작을 확인합니다. 잔여 AZ의 앱·DNS·data·node 용량을 검증합니다. 모든 service endpoint가 장애 AZ에 있을 때 EKS fail-safe가 있으므로 절대적인 network 차단이 아니며 기존 zonal EBS를 이동시키지 않습니다. 순수 Auto Mode DNS는 node system service이고 혼합·non-Auto node에는 CoreDNS Deployment 용량이 필요합니다.
```bash
set -euo pipefail
: "${AWS_REGION:?Set the resource region}"
: "${ARC_RESOURCE_ARN:?Set the exact owned EKS cluster or eligible load-balancer ARN}"
aws sts get-caller-identity
aws arc-zonal-shift get-managed-resource \
--region "$AWS_REGION" --resource-identifier "$ARC_RESOURCE_ARN"
```
다음 블록은 하나의 일괄 설치 스크립트가 아닌 별도 운영 단계입니다. Practice 설정은 정기적인 실제 traffic 변경을 시작합니다. 기존 설정이 있으면 재생성 대신 검토합니다. Outcome alarm identifier는 alarmName/region 객체가 아닌 ARN 문자열이며 준비 후 autoshift를 별도 활성화합니다.
```bash
# MUTATION: authorizes recurring weekly traffic-shifting practice runs.
: "${OUTCOME_ALARM_ARN:?Set the reviewed CloudWatch alarm ARN in the resource region}"
aws arc-zonal-shift create-practice-run-configuration \
--region "$AWS_REGION" --resource-identifier "$ARC_RESOURCE_ARN" \
--outcome-alarms "alarmIdentifier=$OUTCOME_ALARM_ARN,type=CLOUDWATCH"
```
```bash
# MUTATION: enable automatic shifts only after the readiness review.
aws arc-zonal-shift update-zonal-autoshift-configuration \
--region "$AWS_REGION" --resource-identifier "$ARC_RESOURCE_ARN" \
--zonal-autoshift-status ENABLED
```
```bash
# MUTATION: a separate, manually initiated one-hour shift.
set -euo pipefail
: "${AWAY_FROM_AZ:?Set an AZ of this resource}"
SHIFT_ID=$(aws arc-zonal-shift start-zonal-shift \
--region "$AWS_REGION" --resource-identifier "$ARC_RESOURCE_ARN" \
--away-from "$AWAY_FROM_AZ" --expires-in 1h \
--comment "Owned resilience exercise" --query zonalShiftId --output text)
test -n "$SHIFT_ID" && test "$SHIFT_ID" != None
printf '%s\n' "$SHIFT_ID" > owned-zonal-shift-id.txt
```
```bash
# MUTATION: cancel only the recorded manual shift after checking its ownership.
: "${SHIFT_ID:?Use the exact ID recorded for this exercise}"
aws arc-zonal-shift cancel-zonal-shift \
--region "$AWS_REGION" --zonal-shift-id "$SHIFT_ID"
```
[EKS ARC behavior and prerequisites](https://docs.aws.amazon.com/eks/latest/userguide/zone-shift.html) · [Auto Mode/Karpenter integration](https://aws.amazon.com/blogs/containers/arc-zonal-shift-support-for-eks-auto-mode-and-karpenter/)
### 스토리지 고려사항
WaitForFirstConsumer는 scheduler 배치를 고려할 수 있을 때까지 초기 provisioning·binding을 늦춥니다. **EBS는 계속 AZ에 묶입니다.** AZ 장애 후 다른 AZ의 replacement Pod가 같은 volume을 attach할 수 없습니다. Backup·복제 기반 data 복구·이전과 RPO·RTO를 검증합니다.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: resilience-ebs
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
type: gp3
encrypted: 'true'
allowVolumeExpansion: true
reclaimPolicy: Retain
```
위 예제는 표준 EBS CSI driver입니다. Auto Mode는 ebs.csi.eks.amazonaws.com과 별도 node·IAM·migration 전제를 사용합니다. 어느 설계든 encrypted: "true"를 명시하고 생성 EBS·KMS key를 확인합니다. Auto Mode node root/data disk 암호화가 모든 workload PVC의 암호화를 뜻하지 않습니다. 현재 Auto Mode StorageClass parameter 기본값은 false입니다. Retain은 통제된 복구·정리를 위해 released volume을 보존하지만 backup이 아니며 비용이 남을 수 있습니다.
Cross-AZ 공유 filesystem은 기존 **Regional** EFS, 접근 가능한 mount target, TCP 2049 보안 규칙, access-point 권한·CSI IAM 전제가 필요합니다. EFS One Zone은 같은 복원력 설계가 아닙니다. ID는 placeholder이며 여기서 filesystem을 생성하지 않습니다.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: resilience-efs
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap
fileSystemId: fs-0123456789abcdef0
directoryPerms: '700'
basePath: /resilience-demo
reclaimPolicy: Retain
mountOptions:
- tls
```
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: efs-claim
namespace: resilience-demo
spec:
accessModes:
- ReadWriteMany
storageClassName: resilience-efs
resources:
requests:
storage: 5Gi
```
PVC의 5Gi 요청은 EFS가 강제하는 저장 quota가 아닙니다. TLS mount 암호화와 filesystem at-rest 암호화는 별도이며 Retain이면 access-point·data 정리 절차도 필요합니다.
[EKS EBS CSI](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html) · [Auto Mode StorageClass parameters](https://docs.aws.amazon.com/eks/latest/userguide/create-storage-class.html) · [EFS CSI](https://github.com/kubernetes-sigs/aws-efs-csi-driver)
### Istio Locality-Aware Routing
이 Istio sidecar-mode 예시는 문서화된 locality override가 없으면 Pod가 실행되는 node에서 locality를 읽습니다. 일반 Pod zone label이 자동으로 locality를 정의하지 않습니다. 실제 proxy endpoint·locality·건강한 잔여 용량을 확인합니다. 문서의 locality failover에는 outlier detection이 필요합니다.
```yaml
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: web-app-locality
namespace: resilience-demo
spec:
host: web-app.resilience-demo.svc.cluster.local
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
loadBalancer:
simple: ROUND_ROBIN
localityLbSetting:
enabled: true
outlierDetection:
consecutive5xxErrors: 5
interval: 10s
baseEjectionTime: 30s
maxEjectionPercent: 50
```
**가중 분산 대안 설정**은 loadBalancer의 localityLbSetting을 다음 fragment로 교체합니다. 세 source zone을 모두 포함하며 80/10/10은 설정 weight이지 실측 locality·failover 용량 보장이 아닙니다. 호환되지 않는 failover policy와 distribute를 동시에 조합하지 않습니다.
```yaml
localityLbSetting:
enabled: true
distribute:
- from: ap-northeast-2/ap-northeast-2a/*
to:
ap-northeast-2/ap-northeast-2a/*: 80
ap-northeast-2/ap-northeast-2b/*: 10
ap-northeast-2/ap-northeast-2c/*: 10
- from: ap-northeast-2/ap-northeast-2b/*
to:
ap-northeast-2/ap-northeast-2a/*: 10
ap-northeast-2/ap-northeast-2b/*: 80
ap-northeast-2/ap-northeast-2c/*: 10
- from: ap-northeast-2/ap-northeast-2c/*
to:
ap-northeast-2/ap-northeast-2a/*: 10
ap-northeast-2/ap-northeast-2b/*: 10
ap-northeast-2/ap-northeast-2c/*: 80
```
기존 80%+ local traffic, 60–80% 비용 절감, 동일 AZ <1ms 수치는 출처 확인이 안 된 예시로만 보존합니다. Health·connection reuse·endpoint 구성·전송량·가격에 따라 실제 결과가 달라집니다.
[Locality failover](https://istio.io/latest/docs/tasks/traffic-management/locality-load-balancing/failover/) · [Weighted distribution](https://istio.io/latest/docs/tasks/traffic-management/locality-load-balancing/distribute/)
---
## Cell-Based Architecture (Level 3)
Cell-Based Architecture는 시스템을 독립적인 셀로 분리하여 장애 영향 범위를 제한합니다.
### Cell의 정의
셀(Cell)은 다음 요소를 포함하는 자체 완결형 서비스 단위입니다:
- **애플리케이션 인스턴스**: 독립적으로 운영되는 서비스 Pod
- **데이터 저장소**: 셀 전용 데이터베이스 또는 파티션
- **캐시**: 셀 전용 Redis/ElastiCache 인스턴스
- **메시지 큐**: 셀 전용 SQS 큐 또는 Kafka 토픽
### Cell 파티셔닝 전략
| 전략 | 설명 | 장점 | 단점 |
|-----|------|------|------|
| **고객 기반** | 고객 ID 범위별 분리 | 데이터 지역성 우수 | 고객 규모 불균형 가능 |
| **지역 기반** | 지리적 위치별 분리 | 규정 준수 용이 | 글로벌 고객 처리 복잡 |
| **용량 기반** | 부하 수준별 분리 | 리소스 효율성 | 동적 재할당 필요 |
| **티어 기반** | 서비스 티어별 분리 | SLA 차별화 용이 | 관리 복잡성 증가 |
### Namespace 기반 Cell 구현
Namespace·quota·NetworkPolicy는 논리적 경계이며 독립 장애 영역이 아닙니다. Node·control plane·CNI·DNS·router·data service 공유가 남습니다. Network plugin의 policy enforcement와 모든 적용 policy의 허용 합집합을 확인합니다. 아래 router namespace·workload는 정확한 label로 존재해야 합니다.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: cell-1
labels:
cell: '1'
customer-range: a-f
```
```yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: cell-1-quota
namespace: cell-1
spec:
hard:
requests.cpu: '20'
requests.memory: 40Gi
limits.cpu: '40'
limits.memory: 80Gi
pods: '100'
services: '20'
persistentvolumeclaims: '50'
```
```yaml
apiVersion: v1
kind: LimitRange
metadata:
name: cell-1-limits
namespace: cell-1
spec:
limits:
- default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
type: Container
```
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: cell-1-isolation
namespace: cell-1
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {}
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: cell-router
podSelector:
matchLabels:
app: cell-router
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector: {}
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
```
DNS는 kube-system의 일치하는 CoreDNS Pod로 TCP·UDP를 허용합니다. Node-local·Auto Mode system DNS는 경로가 달라 mode별 검증이 필요합니다. 임의 외부 traffic은 허용하지 않으므로 필요한 endpoint·egress gateway 규칙을 별도 검토합니다. 0.0.0.0/0에서 10.0.0.0/8만 제외해도 모든 private network·cell이 격리되지는 않습니다.
[NetworkPolicy semantics](https://kubernetes.io/docs/concepts/services-networking/network-policies/)
### Cluster 기반 Cell 구현
별도 cluster는 control-plane·node 경계를 강화하지만 IAM·account quota·region·공유 data·router 의존성은 남을 수 있습니다. Cell별 context·계정·region·용량·배포·data owner inventory를 먼저 정의하고 [cluster 생성 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation.md)의 검토된 절차를 적용합니다. 여러 운영 cluster를 과거 버전으로 즉시 생성하는 loop는 이 설계의 전제가 아닙니다.
### Shuffle Sharding
Shuffle Sharding은 각 고객을 여러 셀 중 일부에만 할당하여 장애 영향을 제한합니다.
**Shuffle Sharding의 장점 (8개 셀에서 2개 선택):**
- 가능한 조합 수: C(8,2) = 28개
- 독립 균등 할당에서 고정한 단일 셀을 포함하는 assignment의 기대 비율: 25% (2/8), 최대 고객·부하 비율이 아님
- 독립 균등 할당한 두 고객의 완전 동일 조합 확률: 1/28 (약 3.6%)
```
8개 Cell 풀에서 2개 Cell 조합:
- 고객 A -> Cell 1, Cell 5
- 고객 B -> Cell 2, Cell 7
- 고객 C -> Cell 1, Cell 3
Cell 1 장애 시:
- 고객 A -> routing·data·capacity 준비 시 Cell 5 사용 가능
- 고객 B -> 영향 없음
- 고객 C -> routing·data·capacity 준비 시 Cell 3 사용 가능
```
```yaml
# 라우터가 소비하도록 구현해야 하는 예시 data; ConfigMap 자체가 failover를 구현하지 않음
apiVersion: v1
kind: ConfigMap
metadata:
name: shuffle-sharding-config
data:
sharding.yaml: |
# 8개 셀 풀에서 각 고객에게 2개 셀 할당
cells:
- name: cell-1
weight: 1
- name: cell-2
weight: 1
- name: cell-3
weight: 1
- name: cell-4
weight: 1
- name: cell-5
weight: 1
- name: cell-6
weight: 1
- name: cell-7
weight: 1
- name: cell-8
weight: 1
# 고객별 셀 할당 (해시 기반 자동 할당 또는 명시적 지정)
customer_assignments:
customer-001:
primary: cell-1
secondary: cell-4
customer-002:
primary: cell-2
secondary: cell-5
customer-003:
primary: cell-3
secondary: cell-6
```
---
## Multi-Cluster/Multi-Region (Level 4)
사용자 흐름·data consistency 요구별 패턴을 선택합니다. 두 번째 cluster·region만으로 near-zero RTO·RPO가 보장되지는 않습니다. 아래 기존 시간·비용 수치는 검증하지 못한 설계 예시이며 실측·AWS 약정이 아닙니다.
| Pattern | Earlier RTO illustration | Earlier RPO illustration | Earlier cost illustration | Design condition |
| --- | --- | --- | --- | --- |
| Active-Active | ~0 target | ~0 target | 2x+ | Routing, consistency, conflict handling and capacity |
| Active-Passive | Minutes–hours | Minutes | 1.5x | Standby readiness, replication lag and promotion |
| Regional Isolation | Not specified | Not specified | 1x per region | Independent regional service; not automatic regional failover |
| Hub-Spoke | Minutes | Minutes | 1.3x | Hub is a shared dependency unless separately protected |
### Argo CD ApplicationSet
세 대안은 기존 Argo CD·ApplicationSet controller, label을 갖춘 명시적으로 등록된 접근 가능한 cluster, repository credential, repo·destination·resource kind를 제한한 사전 AppProject가 필요합니다. 예제 repo·revision은 소유 값으로 바꿉니다. 생성 Application은 수동 sync이며 자동 sync·prune 전에 대상·manifest를 검토합니다. Generator가 EKS cluster를 생성하지는 않습니다.
#### Cluster generator
```yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: resilience-clusters
namespace: argocd
spec:
goTemplate: true
goTemplateOptions:
- missingkey=error
generators:
- clusters:
selector:
matchLabels:
resilience-example: 'true'
template:
metadata:
name: web-app-{{.nameNormalized}}
spec:
project: resilience-reviewed
source:
repoURL: https://github.com/example/owned-gitops.git
targetRevision: REPLACE_WITH_REVIEWED_COMMIT
path: apps/web-app/overlays/{{.metadata.labels.region}}
destination:
server: '{{.server}}'
namespace: resilience-demo
```
#### Git directories × registered clusters
```yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: resilience-region-directories
namespace: argocd
spec:
goTemplate: true
goTemplateOptions:
- missingkey=error
generators:
- matrix:
generators:
- git:
repoURL: https://github.com/example/owned-gitops.git
revision: REPLACE_WITH_REVIEWED_COMMIT
directories:
- path: regions/*
- clusters:
selector:
matchLabels:
resilience-example: 'true'
region: '{{.path.basename}}'
template:
metadata:
name: '{{.nameNormalized}}-{{.path.basename}}'
spec:
project: resilience-reviewed
source:
repoURL: https://github.com/example/owned-gitops.git
targetRevision: REPLACE_WITH_REVIEWED_COMMIT
path: '{{.path.path}}'
destination:
server: '{{.server}}'
namespace: resilience-demo
```
두 번째 matrix child는 directory basename과 등록 cluster label을 일치시키고 실제 server 값을 사용합니다. Region명으로 EKS API URL을 만들 수 없습니다. Label이 없거나 불일치하면 Application이 없을 수 있으므로 schema뿐 아니라 생성 결과를 확인합니다.
#### Cluster × application list
```yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: resilience-cluster-app-matrix
namespace: argocd
spec:
goTemplate: true
goTemplateOptions:
- missingkey=error
generators:
- matrix:
generators:
- clusters:
selector:
matchLabels:
resilience-example: 'true'
- list:
elements:
- app: frontend
port: '80'
- app: backend
port: '8080'
- app: worker
port: '9090'
template:
metadata:
name: '{{.nameNormalized}}-{{.app}}'
spec:
project: resilience-reviewed
source:
repoURL: https://github.com/example/owned-gitops.git
targetRevision: REPLACE_WITH_REVIEWED_COMMIT
path: apps/{{.app}}
helm:
parameters:
- name: cluster.name
value: '{{.name}}'
- name: service.port
value: '{{.port}}'
destination:
server: '{{.server}}'
namespace: resilience-demo
```
[ApplicationSet matrix parameters](https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Generators-Matrix/)
### Global Accelerator
TLS·health check·리전별 용량을 검증한 기존 ALB/NLB endpoint를 사용합니다. 아래는 실행하지 않은 선택적 provisioning 예시입니다. 반환 ID·소유 자원 정리 계획을 저장하며 중간 실패가 이미 생성한 자원을 삭제하지는 않습니다. Accelerator 비활성화만으로 비용이 없어지지 않습니다.
```bash
# MUTATIONS: creates a disabled, billable accelerator and its configuration.
set -euo pipefail
: "${GA_API_REGION:?Set the documented Global Accelerator API region}"
: "${ACCELERATOR_NAME:?Set a unique owned name}"
: "${REGION_ONE:?Set the first endpoint region}"
: "${REGION_TWO:?Set the second endpoint region}"
: "${REGION_ONE_LB_ARN:?Set the reviewed eligible ALB/NLB ARN}"
: "${REGION_TWO_LB_ARN:?Set the reviewed eligible ALB/NLB ARN}"
test "$REGION_ONE" != "$REGION_TWO"
ACCELERATOR_ARN=$(aws globalaccelerator create-accelerator \
--region "$GA_API_REGION" --name "$ACCELERATOR_NAME" \
--ip-address-type IPV4 --no-enabled --query Accelerator.AcceleratorArn --output text)
test -n "$ACCELERATOR_ARN" && test "$ACCELERATOR_ARN" != None
LISTENER_ARN=$(aws globalaccelerator create-listener \
--region "$GA_API_REGION" --accelerator-arn "$ACCELERATOR_ARN" \
--protocol TCP --port-ranges FromPort=443,ToPort=443 \
--query Listener.ListenerArn --output text)
test -n "$LISTENER_ARN" && test "$LISTENER_ARN" != None
aws globalaccelerator create-endpoint-group \
--region "$GA_API_REGION" --listener-arn "$LISTENER_ARN" \
--endpoint-group-region "$REGION_ONE" --traffic-dial-percentage 100 \
--endpoint-configurations "EndpointId=$REGION_ONE_LB_ARN,Weight=100"
aws globalaccelerator create-endpoint-group \
--region "$GA_API_REGION" --listener-arn "$LISTENER_ARN" \
--endpoint-group-region "$REGION_TWO" --traffic-dial-percentage 100 \
--endpoint-configurations "EndpointId=$REGION_TWO_LB_ARN,Weight=100"
```
```bash
# MUTATION: run separately after endpoint health, routing, data and rollback checks.
: "${ACCELERATOR_ARN:?Use the accelerator just reviewed}"
aws globalaccelerator update-accelerator \
--region "$GA_API_REGION" --accelerator-arn "$ACCELERATOR_ARN" --enabled
```
Traffic dial은 해당 regional endpoint group으로 이미 배정된 traffic 중 신규 connection의 비율입니다. 두 region을 50%로 설정해도 전 세계 50/50 분산이 되지 않으며 예시는 둘 다 100%입니다. Dial 변경이 기존 연결을 강제 이동시키지 않고 failover 규칙은 0 dial을 무시할 수 있습니다. Endpoint weight와도 다릅니다. ALB·NLB endpoint health는 ELB health check를 따르므로 Global Accelerator의 /healthz 값으로 target-group check를 설정할 수 없습니다.
[Traffic dial semantics](https://docs.aws.amazon.com/global-accelerator/latest/dg/about-endpoint-groups-traffic-dial.html) · [Endpoint health](https://repost.aws/knowledge-center/global-accelerator-unhealthy-endpoints) · [Failover rules](https://repost.aws/knowledge-center/global-accelerator-failover-different-region)
### Istio Multi-Primary Federation
Sidecar multi-primary·multiple-network 설계에는 신뢰하는 identity 체계, 고유 cluster·network명, remote Kubernetes API·east-west gateway 접근성, 일치하는 service·namespace와 data 동작이 필요합니다. ServiceEntry만으로 federation이 되지 않습니다. Gateway는 의도한 network만 접근하게 하며 Layer-7 TLS 종료 LB는 AUTO_PASSTHROUGH와 호환되지 않습니다.
다음 IstioOperator는 **istioctl 설치 입력**이며 kubectl apply할 in-cluster operator가 아닙니다. Tokyo 대응 설정과 공식 gateway·discovery 절차 전체를 준비합니다. DNS capture·auto-allocation flag는 이를 대체하지 않습니다.
```yaml
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
values:
global:
meshID: resilience-mesh
multiCluster:
clusterName: cluster-seoul
network: network-seoul
```
```bash
# MUTATIONS: source-cluster credentials/RBAC and destination Secret may be created.
set -euo pipefail
umask 077
: "${SEOUL_CONTEXT:?Verify the owned Seoul context}"
: "${TOKYO_CONTEXT:?Verify the owned Tokyo context}"
test "$SEOUL_CONTEXT" != "$TOKYO_CONTEXT"
test ! -e tokyo-remote-secret.yaml && test ! -e seoul-remote-secret.yaml
istioctl create-remote-secret --context "$TOKYO_CONTEXT" --name cluster-tokyo > tokyo-remote-secret.yaml
istioctl create-remote-secret --context "$SEOUL_CONTEXT" --name cluster-seoul > seoul-remote-secret.yaml
# Inspect Secret metadata without printing token data; verify destination contexts first.
kubectl --context "$SEOUL_CONTEXT" -n istio-system apply -f tokyo-remote-secret.yaml
kubectl --context "$TOKYO_CONTEXT" -n istio-system apply -f seoul-remote-secret.yaml
```
Remote-secret 파일은 credential을 포함하므로 비공개로 보관하고 source control에서 제외하며 설치 후 정책에 따라 로컬 사본을 정리합니다. 위 명령은 신뢰·network 설정 이후의 변경 작업이며 read-only 진단이 아닙니다.
명시적 routing subset은 각 cluster의 **Pod template에 직접 설정한 workload-region label**을 사용합니다. Node topology label이 Pod에 자동 복사되지 않습니다. x-region은 routing 입력이지 authorization이 아니며 80/20 weight도 data failover protocol을 구현하지 않습니다.
```yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: cross-cluster-routing
namespace: resilience-demo
spec:
hosts:
- web-app.resilience-demo.svc.cluster.local
http:
- match:
- headers:
x-region:
exact: tokyo
route:
- destination:
host: web-app.resilience-demo.svc.cluster.local
subset: tokyo
- route:
- destination:
host: web-app.resilience-demo.svc.cluster.local
subset: seoul
weight: 80
- destination:
host: web-app.resilience-demo.svc.cluster.local
subset: tokyo
weight: 20
```
```yaml
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: cross-cluster-subsets
namespace: resilience-demo
spec:
host: web-app.resilience-demo.svc.cluster.local
subsets:
- name: seoul
labels:
workload-region: ap-northeast-2
- name: tokyo
labels:
workload-region: ap-northeast-1
```
별도 외부 DNS service는 아래 ServiceEntry로 registry entry를 표현할 수 있습니다. 자체 DNS·TLS·앱 전제를 갖는 외부 service 대안이며 다른 cluster의 Kubernetes service discovery를 구현하지 않습니다.
```yaml
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
name: reviewed-remote-service
namespace: resilience-demo
spec:
hosts:
- remote-service.example.com
location: MESH_EXTERNAL
ports:
- number: 443
name: https
protocol: TLS
resolution: DNS
```
[Full Istio multi-primary prerequisites and steps](https://istio.io/latest/docs/setup/install/multicluster/multi-primary_multi-network/)
---
## 애플리케이션 복원력 패턴
### PodDisruptionBudgets
PDB는 지원 eviction 경로를 제한하며 모든 자발적·비자발적 중단이나 controller rollout을 보장하지 않습니다. 다음은 대안 예시이므로 같은 workload에 모두 적용하지 않습니다.
#### minAvailable 방식
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb-min
namespace: resilience-demo
spec:
minAvailable: 2 # 지원 eviction 뒤 필요한 Ready Pod 수
selector:
matchLabels:
app: my-app
```
#### maxUnavailable 방식
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb-max
namespace: resilience-demo
spec:
maxUnavailable: 1 # 이미 비정상·진행 중인 disruption을 포함한 eviction budget
selector:
matchLabels:
app: my-app
```
#### 비율 기반 PDB
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb-percentage
namespace: resilience-demo
spec:
minAvailable: "75%" # 75% 이상 Pod 유지
selector:
matchLabels:
app: my-app
```
```bash
# PDB 목록 및 상태 확인
kubectl --context "$KUBE_CONTEXT" -n resilience-demo get pdb
# 상세 정보 확인
kubectl --context "$KUBE_CONTEXT" -n resilience-demo describe pdb app-pdb-min
# 출력 예시:
# Name: app-pdb-min
# Min available: 2
# Selector: app=my-app
# Status:
# Allowed disruptions: 1
# Current: 3
# Desired: 2 # minAvailable=2; illustrative, not observed output
# Total: 3
```
### Graceful Shutdown
Pod 종료 시 진행 중인 요청을 완료하고 안전하게 종료하는 패턴입니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: graceful-app
namespace: resilience-demo
spec:
replicas: 3
selector:
matchLabels:
app: graceful-app
template:
metadata:
labels:
app: graceful-app
spec:
terminationGracePeriodSeconds: 60 # 최대 60초 대기
containers:
- name: app
image: registry.example.com/team/web-app:replace-with-reviewed-digest
ports:
- containerPort: 8080
lifecycle:
preStop:
sleep:
seconds: 5 # 지원 API 기준의 지연이며 전파 보장이 아님
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
```
**Graceful Shutdown 흐름:**
grace period에는 preStop 시간이 포함됩니다. native sleep은 1.34부터 GA이며 5초 대기는 endpoint·LB 전파 보장이 아닙니다. EndpointSlice 종료 갱신과 node shutdown은 병렬이며 endpoint가 ready=false·serving 상태로 남아 있을 수 있습니다.
hook 뒤 runtime이 설정된 stop signal(보통 SIGTERM)을 보내며 image·runtime 설정에 따라 달라질 수 있습니다. 앱은 signal을 처리하고 남은 시간 안에 작업을 마치거나 거부해야 합니다. preStop에서 PID 1을 수동 종료해 엄격한 endpoint 제거 순서를 가정하지 않습니다. 실제 연결·deregistration·data flush를 테스트합니다.
### Circuit Breaker via Istio
```yaml
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: backend-circuit-breaker
namespace: resilience-demo
spec:
host: backend-service.resilience-demo.svc.cluster.local
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
connectTimeout: 3s
http:
http1MaxPendingRequests: 100
http2MaxRequests: 1000
maxRequestsPerConnection: 10
maxRetries: 3
outlierDetection:
consecutive5xxErrors: 5
consecutiveGatewayErrors: 5
interval: 10s
baseEjectionTime: 30s
maxEjectionPercent: 50
minHealthPercent: 30
splitExternalLocalOriginErrors: true
```
이 제한은 설정한 proxy·destination pool 기준이며 cluster 전체 동시성 상한이 아닙니다. http2MaxRequests는 활성 HTTP 요청을 제한하고 maxRetries는 요청별 횟수가 아닌 **동시에 진행 중인 retry 수**입니다. minHealthPercent는 outlier detection 활성 조건이지 그 비율의 건강한 용량 보장이 아닙니다. Error ejection·연결 제한·retry는 측정 기반 조정이 필요합니다.
### Retry/Timeout
```yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: backend-retry-timeout
namespace: resilience-demo
spec:
hosts:
- backend-service.resilience-demo.svc.cluster.local
http:
- match:
- method:
exact: GET
route:
- destination:
host: backend-service.resilience-demo.svc.cluster.local
timeout: 10s
retries:
attempts: 3
perTryTimeout: 3s
retryOn: 5xx,reset,connect-failure
retryRemoteLocalities: true
```
GET 전용 route도 앱이 해당 요청을 안전하게 재시도할 수 있다는 전제입니다. attempts 3은 원 요청 이후 최대 3 retry지만 전체 10초·시도별 3초·backoff·동시성 제한 때문에 모든 시도가 실행되지는 않을 수 있습니다. Retry는 과부하·중복 부작용을 키울 수 있고 write는 명시적 idempotency contract가 필요합니다. retryRemoteLocalities는 대안 허용이지 건강한 원격 용량 보장이 아닙니다. Envoy retriable-4xx는 현재 **409만** 의미하며 408은 포함하지 않습니다. Optimistic-lock 충돌은 같은 요청 반복 대신 state 재조회가 필요할 수 있습니다.
[Istio DestinationRule](https://istio.io/latest/docs/reference/config/networking/destination-rule/) · [Envoy retry conditions](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter)
---
## 카오스 엔지니어링
카오스 엔지니어링은 통제된 fault로 반증 가능한 정상 상태 가설을 검증합니다. 소유한 대표 test 환경에서 시작하고 production은 영향 범위·권한·telemetry·중단 조건·복구를 별도 검토합니다. CR 적용이 fault를 실행할 수 있습니다. 여기서는 설정을 offline 검증했으며 실행·운영 준비 완료를 주장하지 않습니다.
```bash
# Read-only: verify the exact cluster/namespace and opt-in test workload.
: "${KUBE_CONTEXT:?Set the owned test context}"
kubectl --context "$KUBE_CONTEXT" -n resilience-demo get pods \
-l 'app=web-app,experiment-approved=true' -o wide
kubectl --context "$KUBE_CONTEXT" -n resilience-demo get pdb
```
### AWS Fault Injection Service (FIS)
공식 guide의 IAM experiment role·trust policy, EKS access entry(또는 문서화된 legacy mapping), namespace 내 Kubernetes ServiceAccount·Role·RoleBinding, action별 EC2·network 권한을 준비합니다. 아래 role·alarm ARN은 한 region·account의 placeholder이며 모두 일관되게 교체합니다. Alarm은 실제 존재하고 의미 있는 data를 받아 동작이 검증되어야 합니다. Stop condition은 data restore나 무영향 보장이 아닙니다.
Pod target은 clusterIdentifier·namespace·selector로 식별합니다. **aws:eks:pod의 resourceArns에 cluster ARN을 넣을 수 없습니다.** Action에는 kubernetesServiceAccount가 필요합니다. 직접 Pod 삭제이므로 PDB가 삭제를 막지 않습니다. COUNT(1)은 opt-in 집합에서 하나를 선택하며 AZ 장애를 재현하지 않습니다.
#### One Pod deletion
```json
{
"description": "Delete one selected test Pod; not an AZ outage",
"targets": {
"test-pod": {
"resourceType": "aws:eks:pod",
"selectionMode": "COUNT(1)",
"parameters": {
"clusterIdentifier": "REPLACE_WITH_OWNED_TEST_CLUSTER",
"namespace": "resilience-demo",
"selectorType": "labelSelector",
"selectorValue": "app=web-app,experiment-approved=true"
}
}
},
"actions": {
"delete-one": {
"actionId": "aws:eks:pod-delete",
"parameters": {
"kubernetesServiceAccount": "fis-test",
"maxErrorsPercent": "0"
},
"targets": {
"Pods": "test-pod"
}
}
},
"stopConditions": [
{
"source": "aws:cloudwatch:alarm",
"value": "arn:aws:cloudwatch:ap-northeast-2:123456789012:alarm:owned-resilience-stop"
}
],
"roleArn": "arn:aws:iam::123456789012:role/owned-fis-test"
}
```
#### One Pod network latency
```json
{
"description": "Add bounded IPv4 latency to one selected test Pod",
"targets": {
"test-pod": {
"resourceType": "aws:eks:pod",
"selectionMode": "COUNT(1)",
"parameters": {
"clusterIdentifier": "REPLACE_WITH_OWNED_TEST_CLUSTER",
"namespace": "resilience-demo",
"selectorType": "labelSelector",
"selectorValue": "app=web-app,experiment-approved=true"
}
}
},
"actions": {
"latency": {
"actionId": "aws:eks:pod-network-latency",
"parameters": {
"kubernetesServiceAccount": "fis-test",
"duration": "PT1M",
"delayMilliseconds": "200",
"jitterMilliseconds": "50",
"sources": "10.20.0.0/24",
"maxErrorsPercent": "0"
},
"targets": {
"Pods": "test-pod"
}
}
},
"stopConditions": [
{
"source": "aws:cloudwatch:alarm",
"value": "arn:aws:cloudwatch:ap-northeast-2:123456789012:alarm:owned-resilience-stop"
}
],
"roleArn": "arn:aws:iam::123456789012:role/owned-fis-test"
}
```
예시 destination CIDR은 검토한 test 의존성으로 바꿉니다. Network action에는 privileged·root fault injection이 필요하며 Fargate·bridge network는 지원하지 않습니다. IPv4 대상이므로 ALL·IPv4 CIDR도 IPv6를 장애 처리하지 않습니다. 현재 readonly-root-filesystem·container 보안 제약을 확인하고 이 예제를 위해 production 보안을 낮추지 않습니다. FIS는 injector Pod를 사용하고 pod-delete 외 action은 ephemeral container를 사용합니다. Process 종료가 Pod spec의 변경 불가능한 ephemeral-container 기록을 지우지는 않습니다.
#### One subnet network disruption
```json
{
"description": "One owned test subnet network disruption, not a complete AZ outage",
"targets": {
"test-subnet": {
"resourceType": "aws:ec2:subnet",
"selectionMode": "COUNT(1)",
"resourceArns": [
"arn:aws:ec2:ap-northeast-2:123456789012:subnet/subnet-0123456789abcdef0"
]
}
},
"actions": {
"network": {
"actionId": "aws:network:disrupt-connectivity",
"parameters": {
"duration": "PT1M",
"scope": "all"
},
"targets": {
"Subnets": "test-subnet"
}
}
},
"stopConditions": [
{
"source": "aws:cloudwatch:alarm",
"value": "arn:aws:cloudwatch:ap-northeast-2:123456789012:alarm:owned-resilience-stop"
}
],
"roleArn": "arn:aws:iam::123456789012:role/owned-fis-test"
}
```
무관한 workload가 없는 소유 test subnet만 사용합니다. 이 action은 NACL을 복제해 deny 규칙을 적용하고 완료 시 원래 association을 복원합니다. scope=all에서도 subnet 내부 traffic은 남으며 AZ 전체 전원 장애가 아닙니다. 시작 전 NACL quota·IAM·정확한 subnet·관리 및 telemetry 접근성을 검토합니다. Stop·복구는 비동기이므로 최종 experiment 상태와 실제 network·workload 복구를 확인합니다.
```bash
# MUTATION: stop only the recorded experiment ID, not all account experiments.
: "${AWS_REGION:?Set the experiment region}"
: "${EXPERIMENT_ID:?Set the exact running FIS experiment ID}"
aws fis stop-experiment --region "$AWS_REGION" --id "$EXPERIMENT_ID"
```
[FIS EKS Pod prerequisites/RBAC](https://docs.aws.amazon.com/fis/latest/userguide/eks-pod-actions.html) · [Action parameters and subnet behavior](https://docs.aws.amazon.com/fis/latest/userguide/fis-actions-reference.html)
### Litmus Chaos (CNCF Incubating)
검토한 3.31.0 operator CRD는 ChaosEngine·ChaosExperiment·ChaosResult이며 ChaosHub·ChaosSchedule은 정의하지 않습니다. 검토한 release, 각 ChaosExperiment, 제한한 RBAC, 검증한 runner·helper image를 먼저 준비합니다. 현재 catalog fault에는 CI·latest image 기본값이 있으므로 그대로 적용하지 않습니다. Schema 통과는 사용 cluster·runtime 호환성 증명이 아닙니다.
예시는 engineState: stop으로 시작합니다. 정확한 target 준비 후 설치 release의 workflow로 하나씩 검토합니다. Active engine은 duration 동안 반복 삭제할 수 있어 30초가 정확히 한 번 삭제를 뜻하지 않습니다. TARGET_PODS는 현재 name·UID로 Pod를 선택할 때까지 미완성 값으로 두며 공백이나 전체 production selector로 바꾸지 않습니다.
#### Pod deletion
```yaml
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
name: pod-delete-review
namespace: resilience-demo
spec:
engineState: stop
appinfo:
appns: resilience-demo
applabel: app=web-app,experiment-approved=true
appkind: deployment
chaosServiceAccount: pod-delete-sa
experiments:
- name: pod-delete
spec:
components:
env:
- name: TOTAL_CHAOS_DURATION
value: '30'
- name: CHAOS_INTERVAL
value: '10'
- name: FORCE
value: 'false'
- name: TARGET_PODS
value: REPLACE_WITH_ONE_REVIEWED_POD_NAME
- name: PODS_AFFECTED_PERC
value: '100'
```
#### Node drain, not instance termination
```yaml
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
name: node-drain-review
namespace: resilience-demo
spec:
engineState: stop
appinfo:
appns: resilience-demo
applabel: app=web-app,experiment-approved=true
appkind: deployment
chaosServiceAccount: node-drain-sa
experiments:
- name: node-drain
spec:
components:
env:
- name: TOTAL_CHAOS_DURATION
value: '60'
- name: TARGET_NODE
value: REPLACE_WITH_ONE_OWNED_TEST_NODE
```
Drain은 앱 selector 밖에서도 해당 node의 workload에 영향을 줍니다. 전용 test node·비어 있지 않은 정확한 이름·PDB를 고려한 eviction·복구 및 uncordon 계획이 필요합니다. 소유권 대신 kubernetes.io/os=linux 같은 범용 selector를 쓰지 않습니다. EC2 node termination 실험과 다릅니다.
#### DNS error
```yaml
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
name: pod-dns-error-review
namespace: resilience-demo
spec:
engineState: stop
appinfo:
appns: resilience-demo
applabel: app=web-app,experiment-approved=true
appkind: deployment
chaosServiceAccount: pod-dns-error-sa
experiments:
- name: pod-dns-error
spec:
components:
env:
- name: TOTAL_CHAOS_DURATION
value: '60'
- name: TARGET_HOSTNAMES
value: '["backend-service.resilience-demo.svc.cluster.local"]'
- name: MATCH_SCHEME
value: exact
- name: CONTAINER_RUNTIME
value: containerd
- name: SOCKET_PATH
value: /run/containerd/containerd.sock
- name: PODS_AFFECTED_PERC
value: '100'
```
TARGET_HOSTNAMES는 JSON array 문자열입니다. Runtime·socket·privilege 전제가 선택한 Linux test node와 일치해야 하며 모든 EKS node 유형에 이식 가능한 예제가 아닙니다. 퍼센트가 무관한 Pod를 선택하지 않도록 workload 범위를 제한합니다. Resource 존재만으로 성공을 판단하지 않고 ChaosResult와 앱 health를 확인합니다.
```bash
kubectl --context "$KUBE_CONTEXT" -n resilience-demo get chaosengine,chaosresult
```
[Litmus operator 3.31.0](https://github.com/litmuschaos/chaos-operator/releases/tag/3.31.0) · [Official fault catalog](https://github.com/litmuschaos/chaos-charts/tree/master/faults/kubernetes) · [CNCF project status](https://www.cncf.io/projects/litmus/)
### Chaos Mesh
예시는 release 2.8.4 CRD shape를 사용합니다. 설치 전에 Helm chart의 runtime·socket·node 선택·daemon privilege·dashboard 접근·cluster 호환성을 검토합니다. Host 권한 fault injection은 명시적으로 허용한 test node 유형에서 수행하며 schema만으로 Auto Mode·Fargate·Hybrid Node 지원을 추정하지 않습니다.
아래 NetworkChaos·IOChaos·TimeChaos는 release의 experiment.chaos-mesh.org/pause annotation으로 중지 상태입니다. Paused manifest 적용도 cluster 변경이며 target·status 검토 후 pause를 별도로 해제합니다. 중단 시 recovery 상태를 기다립니다. Pause가 삭제 data를 복원하지 않으며 one-shot fault는 pause 의미가 다릅니다.
#### Network latency
```yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: review-network-delay
namespace: resilience-demo
annotations:
experiment.chaos-mesh.org/pause: 'true'
spec:
action: delay
mode: fixed
value: '1'
selector:
namespaces:
- resilience-demo
labelSelectors:
app: web-app
experiment-approved: 'true'
delay:
latency: 100ms
jitter: 50ms
correlation: '25'
duration: 1m
```
#### Network partition
```yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: review-network-partition
namespace: resilience-demo
annotations:
experiment.chaos-mesh.org/pause: 'true'
spec:
action: partition
mode: fixed
value: '1'
selector:
namespaces:
- resilience-demo
labelSelectors:
app: web-app
experiment-approved: 'true'
direction: both
target:
mode: fixed
value: '1'
selector:
namespaces:
- resilience-demo
labelSelectors:
app: backend
experiment-approved: 'true'
duration: 1m
```
#### I/O latency
```yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: IOChaos
metadata:
name: review-io-delay
namespace: resilience-demo
annotations:
experiment.chaos-mesh.org/pause: 'true'
spec:
action: latency
mode: fixed
value: '1'
selector:
namespaces:
- resilience-demo
labelSelectors:
app: web-app
experiment-approved: 'true'
volumePath: /audit-data
delay: 100ms
percent: 50
duration: 1m
```
/audit-data에는 폐기 가능한 test volume을 mount합니다. 예시는 선택적 path filter를 생략했으므로 활성화 전 대상 file을 확인합니다. percent는 주입 operation 비율이지 용량 상한이 아닙니다. 기존 production PostgreSQL data directory에 연결하지 않습니다. 알려진 baseline으로 복구·data 무결성을 확인합니다.
#### Time offset
```yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: TimeChaos
metadata:
name: review-time-offset
namespace: resilience-demo
annotations:
experiment.chaos-mesh.org/pause: 'true'
spec:
mode: fixed
value: '1'
selector:
namespaces:
- resilience-demo
labelSelectors:
app: web-app
experiment-approved: 'true'
timeOffset: -2h
clockIds:
- CLOCK_REALTIME
duration: 1m
```
-2h는 선택한 주입 program의 CLOCK_REALTIME 동작 대상이며 node·모든 clock이 두 시간 바뀐다는 뜻이 아닙니다. Token·scheduler·lease 영향을 추정하기 전에 주입 방식과 앱 clock 사용을 확인합니다. Duration은 의도한 fault 기간이지 앱 복구 시간 보장이 아닙니다.
```bash
kubectl --context "$KUBE_CONTEXT" -n resilience-demo \
get networkchaos,iochaos,timechaos -o yaml
```
[Chaos Mesh 2.8.4 chart](https://github.com/chaos-mesh/chaos-mesh/tree/v2.8.4/helm/chaos-mesh) · [Pause controller](https://github.com/chaos-mesh/chaos-mesh/blob/v2.8.4/controllers/common/desiredphase/controller.go)
### Game Day Framework
주입 전 abort threshold·독립 observer·정확한 recovery owner를 정합니다. 실패·no-data도 포함해 감지와 복원을 분리 기록합니다. 다음 실험 전 현재 실험을 중단하고 duration 경과만으로 성공이라 하지 말고 실제 복구를 검증합니다.
Game Day는 체계적인 카오스 엔지니어링 실습입니다.
**5단계 프레임워크:**
| 단계 | 활동 | 산출물 |
|------|------|--------|
| 1. 정상 상태 기록 | 메트릭 베이스라인 수집 | 대시보드 스냅샷 |
| 2. 장애 주입 | FIS/Litmus/Chaos Mesh 실험 실행 | 실험 로그 |
| 3. 복구 관찰 | 자동 복구 과정 모니터링 | 복구 시간 측정 |
| 4. 영향 분석 | 에러율, 지연시간 변화 분석 | 영향 보고서 |
| 5. 사후 리뷰 | 개선 항목 도출, Action Item | 개선 계획 |
---
## 구현 체크리스트
### Level 1: 기본 복원력 체크리스트
- [ ] 장기 실행 앱별 적절한 liveness 동작 검토
- [ ] Service 제공 앱별 실제 readiness contract 검토
- [ ] 시작 시간이 긴 앱에 Startup Probe 설정
- [ ] Resource requests/limits 설정
- [ ] 중요 Deployment에 PDB 설정
- [ ] replicas >= 2 설정
### Level 2: Multi-AZ 체크리스트
- [ ] Topology Spread Constraints 적용
- [ ] eligible domain·maxSkew·minDomains와 AZ 장애 시 배치 검증
- [ ] Karpenter NodePool에 Multi-AZ 설정
- [ ] workload 용량에 맞는 disruption budget·올림·예외 경로 검토
- [ ] StorageClass volumeBindingMode: WaitForFirstConsumer
- [ ] 공유 스토리지에 EFS 사용
- [ ] Istio locality-aware routing 설정
- [ ] ARC 지원·N-1 용량·alarm 확인 후 별도 autoshift 활성화 결정
### Level 3: Cell-Based 체크리스트
- [ ] Cell 파티셔닝 전략 정의
- [ ] Namespace 또는 Cluster 기반 Cell 구현
- [ ] Cell별 ResourceQuota 설정
- [ ] Cell간 NetworkPolicy 적용
- [ ] Shuffle Sharding 구현 (선택적)
- [ ] Cell별 데이터스토어 분리
- [ ] Cell별 캐시 분리
### Level 4: Multi-Region 체크리스트
- [ ] 아키텍처 패턴 선택 (Active-Active/Passive)
- [ ] Global Accelerator 설정
- [ ] 리전별 EKS 클러스터 생성
- [ ] ArgoCD ApplicationSet 설정
- [ ] 데이터 복제 전략 구현 (Aurora Global DB 등)
- [ ] Istio Multi-Primary 구성 (선택적)
- [ ] Cross-region 장애 조치 테스트
- [ ] 리전별 모니터링 통합
### 비용 고려사항
아래는 기존 출처 미확인 비용 예시이며 현재 견적·실측 절감률이 아닙니다. Cross-AZ 요금은 service·경로·방향·region에 따라 달라 $0.01/GB를 모든 EKS traffic의 총 요금으로 쓸 수 없습니다. 현재 service 가격으로 실제 자원·장애 headroom을 계산하며 비용·chaos benchmark를 재실행하지 않았습니다. 이전 EN의 Active-Passive 50–70% 감소 예시도 검증된 절감률이 아닙니다.
| 항목 | 비용 영향 | 절감 전략 |
|------|----------|----------|
| **Multi-Region** | 2x+ 증가 | Active-Passive로 대기 리전 비용 절감 |
| **Spot Instances** | 60-90% 절감 | 상태 없는 워크로드에 Spot 사용 |
| **Locality Routing** | 60-80% 절감 | Cross-AZ 트래픽 최소화 |
| **Cell Architecture** | 10-20% 증가 | 장애 영향 감소로 운영 비용 절감 |
| **Chaos Engineering** | 기존 월 $100–500 예시 | 실제 FIS·자원 사용량으로 계산 |
| **Cross-AZ** | 기존 $0.01/GB 예시 | 경로·방향·service 가격 별도 확인 |
---
## 다음 단계
이 문서에서는 EKS 클러스터의 고가용성과 복원력 아키텍처에 대해 다루었습니다. 복원력 전략을 구현한 후에는 문제 발생 시 효과적인 디버깅이 중요합니다.
### 관련 문서
- **다음 문서**: [EKS 고급 디버깅](https://www.atomai.click/kubernetes-docs/llms/ko/eks/11-eks-advanced-debugging.md) - 복잡한 문제 상황에서의 디버깅 기법
- **퀴즈**: [EKS 복원력 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/10-eks-resiliency-quiz) - 학습 내용 확인
### 추가 학습 리소스
- [AWS Well-Architected Framework - Reliability Pillar](https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/welcome.html)
- [Amazon EKS Best Practices Guide - Reliability](https://aws.github.io/aws-eks-best-practices/reliability/docs/)
- [Kubernetes Documentation - Pod Topology Spread Constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/)
- [Istio Documentation - Locality Load Balancing](https://istio.io/latest/docs/tasks/traffic-management/locality-load-balancing/)
### 핵심 요약
1. **Level 1 (기본)**: Probes, Resource Limits, PDB로 Pod 수준 복원력 확보
2. **Level 2 (Multi-AZ)**: Topology Spread, ARC Zonal Shift로 AZ 장애 대응
3. **Level 3 (Cell-Based)**: Shuffle Sharding으로 장애 영향 범위 제한
4. **Level 4 (Multi-Region)**: Active-Active/Passive로 리전 장애 대응
5. **카오스 엔지니어링**: FIS, Litmus, Chaos Mesh로 복원력 검증
복원력은 한 번 구현하고 끝나는 것이 아니라, 지속적인 테스트와 개선이 필요한 여정입니다. 정기적인 Game Day를 통해 시스템의 약점을 발견하고 개선해 나가시기 바랍니다.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/11-eks-advanced-debugging
----------------------------------------
# EKS 고급 디버깅과 장애 대응
> **검토 기준**: Kubernetes 1.36 schema·kubectl 1.36.2; 현재 지원 EKS와 호환 component release 선택
> **마지막 업데이트**: 2026년 9월 12일
Amazon EKS 클러스터의 안정적인 운영을 위해서는 체계적인 장애 대응 프레임워크와 고급 디버깅 기술이 필수입니다. 이 문서에서는 프로덕션 환경에서 발생하는 복잡한 문제들을 신속하게 진단하고 해결하기 위한 실전 가이드를 제공합니다.
## 목차
1. [장애 대응 프레임워크](#1-장애-대응-프레임워크)
2. [컨트롤 플레인 디버깅](#2-컨트롤-플레인-디버깅)
3. [노드 레벨 문제 해결](#3-노드-레벨-문제-해결)
4. [워크로드 디버깅](#4-워크로드-디버깅)
5. [네트워킹 진단](#5-네트워킹-진단)
6. [스토리지 문제 해결](#6-스토리지-문제-해결)
7. [관측성 아키텍처](#7-관측성-아키텍처)
8. [장애 감지 아키텍처](#8-장애-감지-아키텍처)
9. [빠른 참조](#9-빠른-참조)
10. [다음 단계](#10-다음-단계)
---
## 1. 장애 대응 프레임워크
### 첫 5분 체크리스트 (Initial Triage)
기존 단계별 30초·영향 범위 확인 2분·전체 5분은 측정된 완료 시간이 아닌 계획 목표입니다. 고객 영향과 정확한 계정·cluster/context·namespace·최근 변경부터 확인합니다. API client 실패는 credential·authorization·DNS/network·control plane 문제일 수 있으며 실행 중인 모든 앱 중단을 뜻하지 않습니다.
Node condition·Pod/container state·controller rollout·최근 event·resource sample을 함께 봅니다. `phase!=Running`은 Running이면서 NotReady·CrashLooping인 Pod를 놓치고 정상 완료 Job은 포함합니다. Running phase가 readiness를 보장하지 않습니다. Deployment는 `1/1` 같은 화면 문자열 grep 대신 desired·updated·ready/available replica와 observed generation을 비교합니다.
표준 VPC CNI `aws-node` DaemonSet은 보통 `kube-system`에서 실행되며 `amazon-vpc-cni-system` namespace가 EKS 필수 전제는 아닙니다. 순수 Auto Mode는 networking·node system DNS를 달리 관리하므로 표준 add-on Pod 부재는 node·controller mode와 함께 해석합니다. Metrics Server는 수집된 resource sample이며 고객 가용성 신호가 아닙니다.
### 초기 진단 스크립트
선택한 workload namespace와 cluster node·system Pod 상태를 시간 제한 API 요청으로 수집해 비공개로 저장합니다. Secret data나 모든 Pod의 env·설정을 dump하지 않습니다. Log·event·오류 문구에도 앱의 민감 정보가 포함될 수 있으므로 공유 전 검사·삭제합니다. 실행 전 계정·context 입력과 기존 비공개 evidence directory를 확인합니다.
```bash
set -euo pipefail
: "${AWS_REGION:?Set the intended Region}"
: "${CLUSTER_NAME:?Set the existing cluster name}"
: "${EXPECTED_ACCOUNT_ID:?Set the intended account ID}"
: "${KUBE_CONTEXT:?Set the explicit kubectl context}"
: "${NAMESPACE:?Set the owned workload namespace}"
: "${EVIDENCE_PARENT:?Set an existing private evidence directory}"
test -d "$EVIDENCE_PARENT"
ACTUAL_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
test "$ACTUAL_ACCOUNT_ID" = "$EXPECTED_ACCOUNT_ID" || { echo "Account mismatch" >&2; exit 1; }
CLUSTER_ENDPOINT=$(aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster.endpoint --output text)
KUBE_ENDPOINT=$(kubectl config view --context "$KUBE_CONTEXT" --minify \
-o jsonpath='{.clusters[0].cluster.server}')
test "$CLUSTER_ENDPOINT" = "$KUBE_ENDPOINT" || { echo "Context/cluster mismatch" >&2; exit 1; }
umask 077
TRIAGE_DIR=$(mktemp -d "$EVIDENCE_PARENT/eks-triage.XXXXXXXX")
TRIAGE_FAILED=0
k() { kubectl --context "$KUBE_CONTEXT" --request-timeout=15s "$@"; }
collect() {
local name=$1
shift
if "$@" > "$TRIAGE_DIR/$name.txt" 2> "$TRIAGE_DIR/$name.stderr"; then
printf '%s\tok\n' "$name" >> "$TRIAGE_DIR/status.tsv"
else
local rc=$?
TRIAGE_FAILED=$((TRIAGE_FAILED + 1))
printf '%s\tfailed:%s\n' "$name" "$rc" >> "$TRIAGE_DIR/status.tsv"
fi
}
node_health() {
k get nodes -o json | jq '[.items[] | {
name:.metadata.name,uid:.metadata.uid,providerID:.spec.providerID,
unschedulable:.spec.unschedulable,taints:.spec.taints,conditions:.status.conditions
}]'
}
pod_health() {
k -n "$NAMESPACE" get pods -o json | jq '[.items[] | {
name:.metadata.name,uid:.metadata.uid,node:.spec.nodeName,owners:.metadata.ownerReferences,
deleting:.metadata.deletionTimestamp,phase:.status.phase,conditions:.status.conditions,
containers:[.status.containerStatuses[]? | {name,ready,restartCount,state,lastState}],
initContainers:[.status.initContainerStatuses[]? | {name,ready,restartCount,state,lastState}]
}]'
}
deployment_health() {
k -n "$NAMESPACE" get deployments -o json | jq '[.items[] | {
name:.metadata.name,generation:.metadata.generation,observed:.status.observedGeneration,
desired:(.spec.replicas // 1),updated:(.status.updatedReplicas // 0),
ready:(.status.readyReplicas // 0),available:(.status.availableReplicas // 0),
conditions:.status.conditions
}]'
}
load_balancers() {
k -n "$NAMESPACE" get services -o json | jq '[.items[] | select(.spec.type=="LoadBalancer") | {
name:.metadata.name,class:.spec.loadBalancerClass,selector:.spec.selector,
ports:.spec.ports,status:.status.loadBalancer
}]'
}
collect nodes node_health
collect pods pod_health
collect deployments deployment_health
collect load-balancers load_balancers
collect events k -n "$NAMESPACE" get events --sort-by='.metadata.creationTimestamp'
collect system-pods k -n kube-system get pods -o wide
collect node-resources k top nodes
collect pod-resources k -n "$NAMESPACE" top pods --sort-by=memory
printf 'Private evidence: %s; failed collections: %s\n' "$TRIAGE_DIR" "$TRIAGE_FAILED"
test "$TRIAGE_FAILED" -eq 0
```
`status.tsv`와 각 실패 출력을 확인합니다. 권한 부족·metric 부재·API timeout을 수집 실패로 남기며 하나라도 실패하면 nonzero로 종료합니다. Incident가 해결되었다고 주장하지 않습니다. 완료·Pending·Running 상태를 그대로 보여 주므로 함께 해석합니다. 후속 log는 정확한 namespace·Pod UID·container와 제한한 기간·행 수를 사용합니다.
LoadBalancer 목록은 Service JSON을 로컬 filter합니다. `spec.type`은 기본 Service의 지원 field selector가 아닙니다. 초기 수집에 전체 `cluster-info dump`·자동 archive 업로드·자원 restart·delete를 포함하지 않습니다.
### 장애 심각도 매트릭스 (Severity Matrix)
| 심각도 | 분류 | 영향 범위 | 대응 시간 | 예시 |
|--------|------|-----------|-----------|------|
| **P1** | Critical | 전체 서비스 중단 | 15분 이내 | 컨트롤 플레인 장애, 전체 노드 NotReady |
| **P2** | High | 주요 기능 장애 | 1시간 이내 | 특정 워크로드 전체 실패, 네트워크 연결 문제 |
| **P3** | Medium | 부분적 영향 | 4시간 이내 | 일부 파드 재시작, 성능 저하 |
| **P4** | Low | 경미한 문제 | 24시간 이내 | 로그 수집 지연, 비핵심 모니터링 알림 |
위 심각도별 대응 시간은 조직 목표의 예시입니다. 실제 고객 영향으로 분류하며 control-plane만의 장애에서도 기존 workload traffic은 실행될 수 있습니다.
### 신속한 문제 식별을 위한 의사결정 트리
---
## 2. 컨트롤 플레인 디버깅
### EKS 컨트롤 플레인 로그 유형
EKS는 다섯 control-plane log 유형을 제공하며 활성화한 유형만 해당 region의 CloudWatch log group으로 전송합니다. 활성화가 누락된 과거 log를 복구하지는 않습니다. 접근·retention을 제한하고 ingestion·보관·query 비용을 고려합니다. Group·stream 부재는 logging 비활성화·미전송·잘못된 region·권한 거부일 수 있으며 control-plane 장애로 단정하지 않습니다.
| 유형 | 확인할 근거 |
| --- | --- |
| api | API server 동작·오류 |
| audit | API 요청 identity·verb·resource·응답 상태 |
| authenticator | IAM과 Kubernetes 사이 인증 |
| controllerManager | Controller 조정 |
| scheduler | Scheduling 결정·오류 |
```bash
set -euo pipefail
: "${AWS_REGION:?}"; : "${CLUSTER_NAME:?}"
LOG_GROUP="/aws/eks/$CLUSTER_NAME/cluster"
aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--query 'cluster.{ARN:arn,Status:status,Logging:logging}'
aws logs describe-log-streams --region "$AWS_REGION" --log-group-name "$LOG_GROUP" \
--order-by LastEventTime --descending --max-items 10 \
--query 'logStreams[].{Name:logStreamName,LastEvent:lastEventTimestamp}'
```
```bash
# MUTATION: review cost, retention, access and available subnet IPs first.
set -euo pipefail
UPDATE_ID=$(aws eks update-cluster-config --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}' \
--query update.id --output text)
test -n "$UPDATE_ID" && test "$UPDATE_ID" != None
aws eks describe-update --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--update-id "$UPDATE_ID" --query update
```
변경은 비동기입니다. 반환 update ID가 Successful인지 추적하며 Failed·Cancelled·client timeout을 성공으로 처리하지 않습니다. 이후 describe-cluster와 새 log 전송을 확인합니다. Cluster ACTIVE만으로 해당 update 완료를 알 수 없습니다. 현재 EKS logging 전제에 따라 설정한 각 cluster subnet에 최대 다섯 개의 가용 IP가 필요할 수 있습니다.
### CloudWatch Logs Insights 쿼리
각 블록을 Bash·SQL이 아닌 **별도의 Logs Insights QL query**로 실행합니다. Console·StartQuery 요청에서 정확한 log group·기간을 선택합니다. CloudWatch가 발견한 EKS JSON audit field를 사용하므로 pipeline이 형식을 바꾸면 실제 record·중첩 log parsing을 확인합니다. 검색 결과가 없다고 서비스 정상·log 전송을 증명하지는 않습니다.
#### API 오류 메시지
```text
fields @timestamp, @message
| filter @logStream like /kube-apiserver/ and @logStream not like /audit/
| filter @message like /error|Error|ERROR/
| sort @timestamp desc
| limit 100
```
#### 선택한 시간 구간의 오류 수
```text
fields @timestamp, @message
| filter @logStream like /kube-apiserver/ and @logStream not like /audit/
| filter @message like /error|Error|ERROR/
| stats count(*) as error_count by bin(5m)
```
#### 검토가 필요한 Authenticator 메시지
```text
fields @timestamp, @message
| filter @logStream like /authenticator/
| filter @message like /access denied|Unauthorized|unauthorized/
| sort @timestamp desc
| limit 50
```
#### 구조화된 audit 로그의 인증·인가 거부
```text
fields @timestamp, user.username, verb, objectRef.resource, objectRef.namespace, responseStatus.code
| filter @logStream like /kube-apiserver-audit/
| filter responseStatus.code in [401, 403]
| sort @timestamp desc
| limit 100
```
#### 검토 대상 identity의 구조화된 audit 활동
```text
fields @timestamp, user.username, verb, objectRef.resource, objectRef.namespace, responseStatus.code
| filter @logStream like /kube-apiserver-audit/
| filter user.username = "REPLACE_WITH_OBSERVED_KUBERNETES_USERNAME"
| sort @timestamp desc
| limit 50
```
#### identity와 resource별 audit 429 이벤트
```text
fields user.username, verb, objectRef.resource, responseStatus.code
| filter @logStream like /kube-apiserver-audit/
| filter responseStatus.code = 429
| stats count(*) as request_count by user.username, verb, objectRef.resource
| sort request_count desc
| limit 50
```
#### API 요청량 — throttling 발생 여부와 구분
```text
fields user.username, verb, objectRef.resource
| filter @logStream like /kube-apiserver-audit/
| stats count(*) as request_count by user.username, verb, objectRef.resource
| sort request_count desc
| limit 50
```
Audit 401·403은 요청 단위 거부이며 authenticator 문구 검색과 다릅니다. 전체 API 호출 수는 throttled 호출 수가 아니고 time-bin 집계 후에는 각 event의 @timestamp로 정렬할 수 없습니다. StartQuery의 query ID로 GetQueryResults가 Complete인지 확인하고 Failed·Cancelled·Timeout·missing-data를 보존합니다. [모니터링 장](https://www.atomai.click/kubernetes-docs/llms/ko/eks/06-eks-monitoring-logging.md)의 제한한 query·polling 절차를 참고합니다. 감사에서 live CloudWatch query는 실행하지 않았습니다.
[EKS control-plane logging](https://docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html) · [AWS audit-field examples](https://docs.aws.amazon.com/eks/latest/best-practices/auditing-and-logging.html)
### IAM 인증 문제 해결
먼저 초기 triage의 계정·context guard를 실행합니다. Kubectl을 사용하는 사람·자동화 IAM identity, node bootstrap identity, 앱 Pod 내부 AWS identity를 구분합니다. Token 생성 성공이 Kubernetes 인증·인가 성공을 증명하지는 않습니다.
k8s-aws-v1으로 시작하는 EKS IAM token은 base64url로 인코딩한 presigned STS 요청이며 **세 부분 JWT가 아닙니다.** JSON처럼 decode·출력하거나 log에 복사하지 않습니다. Kubernetes projected ServiceAccount token은 별도의 JWT credential입니다.
```bash
set -euo pipefail
: "${AWS_REGION:?}"; : "${CLUSTER_NAME:?}"; : "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"
aws sts get-caller-identity
aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--query 'cluster.{ARN:arn,AuthenticationMode:accessConfig.authenticationMode}'
# Print only the credential expiry, not the bearer token.
aws eks get-token --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--query status.expirationTimestamp --output text
kubectl --context "$KUBE_CONTEXT" auth whoami
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" auth can-i get pods
```
AuthenticationMode에 맞춰 access를 확인합니다. API/API_AND_CONFIG_MAP은 principal의 access entry·연결 policy 범위·RBAC binding을, CONFIG_MAP은 기존 legacy mapping을 봅니다. 원인을 확인하지 않은 401·403 때문에 mode를 전환하거나 aws-auth를 교체하지 않습니다. Mode migration에는 별도 전제와 되돌릴 수 없는 전환이 있으며 IAM role path·STS session ARN을 문자열 치환으로 추정하지 않습니다.
```bash
# Run for API or API_AND_CONFIG_MAP authentication mode.
aws eks list-access-entries --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME"
: "${PRINCIPAL_ARN:?Use a reviewed IAM role/user ARN, not an STS assumed-role session ARN}"
aws eks describe-access-entry --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--principal-arn "$PRINCIPAL_ARN"
aws eks list-associated-access-policies --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--principal-arn "$PRINCIPAL_ARN"
```
```bash
# Read-only legacy mapping inspection for CONFIG_MAP/API_AND_CONFIG_MAP clusters.
kubectl --context "$KUBE_CONTEXT" -n kube-system get configmap aws-auth -o yaml
```
기존 node bootstrap mapping을 보존합니다. Group명만으로 권한이 생기지 않으며 대응 binding이 필요합니다. 진단 단계에서 system:masters를 추가하지 말고 검토한 최소 권한을 사용합니다. 403은 인가 거부, 401은 유효하지 않거나 만료한 credential일 수 있으며 network·TLS 실패와 구분합니다.
### IRSA 문제 해결
IRSA에는 올바른 OIDC issuer/provider, namespace·ServiceAccount subject와 sts.amazonaws.com audience에 맞는 trust policy, web-identity credential을 사용·갱신하는 SDK가 필요합니다. 아래 annotation은 일부 설정이며 namespace·role은 placeholder입니다. 이 YAML만으로 role·provider·bucket 권한이 생성되지 않습니다.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: s3-access-sa
namespace: diagnostics-example
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/owned-s3-access-role
```
```bash
set -euo pipefail
: "${SERVICE_ACCOUNT:?Set the actual ServiceAccount on the Pod}"
: "${POD_NAME:?Set an owned Pod}"; : "${CONTAINER_NAME:?Set its application container}"
: "${IRSA_ROLE_NAME:?Set the reviewed IAM role name}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get serviceaccount "$SERVICE_ACCOUNT" \
-o jsonpath='{.metadata.annotations.eks\.amazonaws\.com/role-arn}{"\n"}'
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--query cluster.identity.oidc.issuer --output text
aws iam get-role --role-name "$IRSA_ROLE_NAME" --query Role.AssumeRolePolicyDocument
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pod "$POD_NAME" -o json | jq '{
uid:.metadata.uid,serviceAccount:.spec.serviceAccountName,
envNames:[.spec.containers[] | {name,envNames:[.env[]?.name]}],
projectedVolumes:[.spec.volumes[]? | select(.projected) | {name,projected}]
}'
```
Secret 값·token bytes 대신 env **이름**, token 경로·mount metadata·SDK credential chain을 확인합니다. Static credential이나 앞선 provider가 의도한 identity를 덮을 수 있습니다. 새 debug Pod·container는 identity·설정이 다를 수 있으므로 실제 앱 container를 확인합니다.
```bash
# Optional read-only identity request, only if AWS CLI is already in this container.
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" exec "$POD_NAME" -c "$CONTAINER_NAME" \
-- aws sts get-caller-identity
```
STS 응답은 사용 identity를 나타내며 모든 bucket 목록·특정 object 접근 권한을 증명하지는 않습니다. Identity 검사만을 위해 계정 전체 aws s3 ls를 실행하지 않습니다.
### Pod Identity 문제 해결
```bash
aws eks list-pod-identity-associations --region "$AWS_REGION" \
--cluster-name "$CLUSTER_NAME" --namespace "$NAMESPACE" --service-account "$SERVICE_ACCOUNT"
: "${ASSOCIATION_ID:?Use the exact matching association ID}"
aws eks describe-pod-identity-association --region "$AWS_REGION" \
--cluster-name "$CLUSTER_NAME" --association-id "$ASSOCIATION_ID"
# Standard EC2-node setup only; Auto Mode provides the integration itself.
kubectl --context "$KUBE_CONTEXT" -n kube-system get pods \
-l app.kubernetes.io/name=eks-pod-identity-agent
```
Association·role trust/권한·지원 SDK provider·agent/node 접근성을 확인합니다. Auto Mode에는 기능이 내장되어 agent를 중복 설치하지 않으며 Fargate는 EKS Pod Identity를 지원하지 않습니다. Association 생성·변경은 진단과 분리합니다. IRSA·Pod Identity는 token audience·credential 전달 경로가 다르며 운영자 AWS CLI identity와 기본적으로 같지 않습니다.
### ServiceAccount Token 만료와 갱신
Projected token에 보편적인 “최대 24시간” 규칙은 없습니다. 요청 기간과 API server의 설정 상한은 다릅니다. Kubelet은 TTL의 80%보다 오래되었거나 24시간이 지난 token의 갱신을 요청하며 앱은 교체된 file을 다시 읽어야 합니다. EKS는 Kubernetes API ServiceAccount token migration의 90일 호환성 연장·stale-token audit annotation을 문서화하지만 이를 안전한 cache 기간이나 IRSA·Pod Identity·임의 외부 verifier의 수명 보장으로 쓰지 않습니다.
다음 예시는 STS audience의 custom token을 한 시간으로 요청합니다. 기본 API token을 연장하거나 IRSA를 자동 구성하지 않습니다. 기존 namespace·ServiceAccount, 검토한 image, role trust, 앱 SDK·token-file 설정은 별도 전제입니다. STS audience token이 Kubernetes API에도 유효하다고 가정하지 않습니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: audience-token-example
namespace: diagnostics-example
spec:
serviceAccountName: owned-app
automountServiceAccountToken: false
containers:
- name: app
image: registry.example.com/owned/app:replace-with-reviewed-digest
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: sts.amazonaws.com
```
[EKS access entries](https://docs.aws.amazon.com/eks/latest/userguide/access-entries.html) · [EKS token migration/rotation](https://docs.aws.amazon.com/eks/latest/userguide/service-accounts.html) · [Kubernetes projected tokens](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) · [IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) · [Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html)
### EKS Add-on 오류 패턴
변경 전 설치 version·owner·configuration/identity 설정과 health.issues를 읽습니다. ACTIVE는 add-on 상태이지 모든 고객 traffic 정상의 증명이 아니며 DEGRADED는 단순 속도 저하가 아닌 health issue를 뜻합니다. CREATE_FAILED·UPDATE_FAILED·DELETE_FAILED는 실제 issue 상세를 확인합니다. Auto Mode 관리 기능에는 표준 add-on 부재가 정상일 수 있습니다.
```bash
set -euo pipefail
: "${AWS_REGION:?}"; : "${CLUSTER_NAME:?}"; : "${ADDON_NAME:?Set the existing owned add-on}"
aws eks describe-addon --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--addon-name "$ADDON_NAME" \
--query 'addon.{Version:addonVersion,Status:status,Issues:health.issues,Configuration:configurationValues,Role:serviceAccountRoleArn,PodIdentity:podIdentityAssociations}'
CLUSTER_VERSION=$(aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--query cluster.version --output text)
aws eks describe-addon-versions --region "$AWS_REGION" --addon-name "$ADDON_NAME" \
--kubernetes-version "$CLUSTER_VERSION" \
--query 'addons[].addonVersions[].{Version:addonVersion,Architectures:architecture,ComputeTypes:computeTypes,Compatibility:compatibilities}'
```
응답 첫 version을 “최신” 또는 모든 node 유형에 자동 호환되는 값으로 선택하지 않습니다. Architecture·compute type·default 표시·configuration schema·IAM/Pod Identity·component migration 순서를 검토합니다. Configuration 출력은 민감할 수 있으므로 비공개로 취급합니다. Version update는 초기 진단이 아닌 의도한 변경입니다.
```bash
# MUTATION: use a reviewed compatible version and configuration/identity plan.
set -euo pipefail
: "${REVIEWED_ADDON_VERSION:?Choose from the compatible versions after review}"
: "${REVIEWED_ADDON_CONFIG:?Set the path to the reviewed JSON configuration file}"
test -f "$REVIEWED_ADDON_CONFIG"
aws eks describe-addon-configuration --region "$AWS_REGION" --addon-name "$ADDON_NAME" \
--addon-version "$REVIEWED_ADDON_VERSION" --query configurationSchema --output text
# The configuration file must be checked against this version's schema before this request.
UPDATE_ID=$(aws eks update-addon --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--addon-name "$ADDON_NAME" --addon-version "$REVIEWED_ADDON_VERSION" \
--configuration-values "file://$REVIEWED_ADDON_CONFIG" \
--resolve-conflicts PRESERVE --query update.id --output text)
test -n "$UPDATE_ID" && test "$UPDATE_ID" != None
aws eks describe-update --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--addon-name "$ADDON_NAME" --update-id "$UPDATE_ID" --query update
```
PRESERVE는 conflict 처리에서 기존 custom 설정 보존을 요청하지만 backup이나 임의의 이전 설정이 새 release에서도 동작한다는 보장은 아닙니다. OVERWRITE는 충돌한 custom 설정을 초기화할 수 있어 별도 검토합니다. 정확한 update ID의 완료·오류와 변경 후 add-on health를 확인합니다. configurationValues·identity 변경을 조용히 생략·덮어쓰지 말고 명시적으로 검토합니다.
[Update an EKS add-on](https://docs.aws.amazon.com/eks/latest/userguide/updating-an-add-on.html)
---
## 3. 노드 레벨 문제 해결
### 노드 조인 실패 진단
다음은 instance·NodeClaim·bootstrap log·endpoint 접근·authentication mode로 검증할 가설이며 확정 원인 목록이 아닙니다.
| 영역 | 확인할 항목 |
| --- | --- |
| Bootstrap·AMI | 정확한 cluster명·endpoint·CA, OS별 bootstrap, architecture·호환 kubelet/AMI; 모든 경우 version이 정확히 같아야 한다는 규칙은 아님 |
| Network·보안 | Node→API TCP 443, API→kubelet TCP 10250, DNS·workload별 경로; 방향·SG membership·routing 확인 |
| VPC DNS | DNS support/hostname·DHCP resolver/domain·실제로 사용하는 endpoint |
| Identity | Node IAM role과 Kubernetes node access entry/legacy mapping; instance-profile ARN과 role ARN 구분 |
| 소유·탐색 tag | Provisioner별 node ownership tag; LB 탐색용 subnet tag와 구분 |
| Private 접근 | 검토한 endpoint·egress를 통한 EKS/ECR/S3/STS 등 필수 경로; 모든 private cluster가 NAT를 요구하지 않음 |
| Launch 설정 | 해당 provisioner의 role/profile 처리, launch-template version, capacity·subnet IP |
| 초기화 | 선택 AMI에 맞는 nodeadm·cloud-init·bootstrap 근거; AL2023·Bottlerocket·Windows·Auto Mode의 경로는 같지 않음 |
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NODE_NAME:?Set the exact owned node name}"
NODE_JSON=$(kubectl --context "$KUBE_CONTEXT" get node "$NODE_NAME" -o json)
printf '%s\n' "$NODE_JSON" | jq '{
name:.metadata.name,uid:.metadata.uid,providerID:.spec.providerID,
os:.status.nodeInfo.osImage,kernel:.status.nodeInfo.kernelVersion,
kubelet:.status.nodeInfo.kubeletVersion,runtime:.status.nodeInfo.containerRuntimeVersion,
labels:.metadata.labels,taints:.spec.taints,conditions:.status.conditions
}'
NODE_UID=$(printf '%s\n' "$NODE_JSON" | jq -er '.metadata.uid')
kubectl --context "$KUBE_CONTEXT" get events -A \
--field-selector "involvedObject.uid=$NODE_UID" --sort-by='.metadata.creationTimestamp'
```
```bash
# EC2-backed nodes only: map the Node providerID to an inspected instance ID/Region.
: "${AWS_REGION:?}"; : "${INSTANCE_ID:?Use the verified EC2 ID, not a guessed node-name conversion}"
aws ec2 describe-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" \
--query 'Reservations[].Instances[].{ID:InstanceId,State:State.Name,AZ:Placement.AvailabilityZone,Subnet:SubnetId,Profile:IamInstanceProfile,Groups:SecurityGroups,Image:ImageId}'
aws ec2 describe-instance-status --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" \
--include-all-instances
```
아직 등록되지 않은 instance에는 Node 객체가 없으므로 소유 managed-node-group·NodeClaim·instance 근거를 사용합니다. Ready=False와 heartbeat 부재로 인한 Ready=Unknown을 구분합니다. Resource pressure는 Ready와 함께 나타날 수 있으므로 화면 문자열 하나로 원인을 추정하지 않습니다. 신규 Auto Mode EC2 managed instance는 일반 목록에 기본 숨김일 수 있습니다. 직접 instance ID 조회·managed resource 포함과 계정 전체 visibility 변경을 구분합니다.
### NotReady 노드 의사결정 트리
### Host·관리 node 진단
고객이 접근 가능한 Linux node의 SSM에는 node agent·role/network 전제와 정확한 instance 접근 권한이 필요하며 session을 엽니다. EKS Auto Mode managed instance는 직접 SSH를 지원하지 않습니다. 문서화된 NodeDiagnostic·console-output 또는 지원 kubectl debug node 경로를 사용합니다. 현재 Auto Mode 가이드는 live log용 **명시적 sysadmin debug profile**을 지원합니다. 이는 privileged Pod 생성이며 SSH나 기본 debug 권한이 아닙니다. NodeDiagnostic은 민감한 log·capture를 S3에 업로드할 수 있어 별도 범위·저장 권한 검토가 필요합니다.
```bash
# Interactive host access: an operational session, not an automatic triage step.
: "${AWS_REGION:?}"; : "${INSTANCE_ID:?Use the reviewed self-managed or managed-node-group instance}"
aws ssm start-session --region "$AWS_REGION" --target "$INSTANCE_ID"
```
```bash
# Read-only Linux/systemd host checks after authorized access.
sudo systemctl show kubelet containerd --no-pager \
-p Id -p LoadState -p ActiveState -p SubState -p ExecMainStatus
sudo journalctl -u kubelet --since "15 minutes ago" -n 200 --no-pager
sudo journalctl -u containerd --since "15 minutes ago" -n 100 --no-pager
sudo crictl info
sudo crictl ps
sudo crictl ps -a
sudo crictl images
df -h
df -i
sudo journalctl --disk-usage
```
```bash
# Exact container ID only; log content may be sensitive.
: "${CONTAINER_ID:?Use an inspected CRI container ID}"
sudo crictl logs --tail=100 "$CONTAINER_ID"
```
Systemd·CRI 명령은 선택 host에 해당 component·도구가 있다는 전제입니다. Crictl의 올바른 CRI endpoint를 확인합니다. Journal은 기간·행 수를 제한하며 journalctl -f를 tail에 연결하면 끝나지 않을 수 있습니다. Kubeconfig·client key를 출력하거나 log·종료 container·image cache를 일괄 삭제하지 않습니다. 증거이거나 kubelet GC가 관리하는 자원일 수 있습니다. Restart·drain·replacement·retention 변경은 원인 확인 뒤 별도 검토한 복구 단계입니다.
### Resource Pressure
DiskPressure는 df 용량뿐 아니라 가용 byte/inode·설정한 eviction threshold와 관련됩니다. df -h·df -i·mount·kubelet event를 함께 봅니다. Retention 정리를 허용한 경우에도 기존 journalctl --vacuum-size=500M은 정책 예시이며 필요한 증거를 먼저 확보하고 /var/log glob을 지우지 않습니다. MemoryPressure와 container OOM은 다릅니다. Limit·node 가용 memory·working set·log·pressure metric을 대조합니다. Limit·node 증가만으로 원인 해결을 증명하지 못합니다.
```bash
# Read-only host evidence, not remediation.
free -h
awk '/MemTotal|MemFree|MemAvailable|Buffers|Cached/ {print}' /proc/meminfo
cat /proc/sys/kernel/pid_max
cat /proc/sys/kernel/threads-max
ps -eLf --no-headers | wc -l
ps -eo pid,comm,nlwp --sort=-nlwp | head -20
if [ -r /proc/pressure/memory ]; then cat /proc/pressure/memory; fi
if [ -r /proc/pressure/cpu ]; then cat /proc/pressure/cpu; fi
```
/proc process directory 수는 전체 thread/task 수가 아닙니다. Ps NLWP·kernel limit는 단서이며 kubelet PID-pressure 계산·cgroup PID limit와 구분합니다. 일반 memory 사용률을 Kubernetes MemoryPressure condition이라고 표시하지 않습니다. Metric 부재는 근거 없음으로 처리합니다.
### Karpenter 프로비저닝 문제
```bash
# Self-managed Karpenter; use the actual release namespace and selected objects.
: "${KARPENTER_NAMESPACE:?Set the existing controller namespace}"
: "${NODEPOOL_NAME:?}"; : "${NODECLAIM_NAME:?}"
kubectl --context "$KUBE_CONTEXT" -n "$KARPENTER_NAMESPACE" logs \
-l app.kubernetes.io/name=karpenter -c controller --since=15m --tail=200 --prefix
kubectl --context "$KUBE_CONTEXT" get nodepool "$NODEPOOL_NAME" -o yaml
kubectl --context "$KUBE_CONTEXT" get nodeclaim "$NODECLAIM_NAME" -o yaml
kubectl --context "$KUBE_CONTEXT" get events -A \
--field-selector "involvedObject.name=$NODECLAIM_NAME" --sort-by='.metadata.creationTimestamp'
```
NodePool·NodeClass readiness, NodeClaim condition/event, constraints·limit·subnet IP·IAM·EC2 capacity를 확인합니다. Auto Mode는 내장 controller의 NodeClaim·NodeClass·event·audit log를 확인하며 self-managed karpenter Deployment·namespace를 기대하지 않습니다. 아래 self-managed v1 schema는 호환 release·검토한 기존 EC2NodeClass가 필요한 예시이며 기존 default pool 교체 명령이 아닙니다. CPU/memory limit는 예약 용량이 아닌 상한이고 capacity-type 목록이 Spot 전용·AZ 균형을 증명하지 않습니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: reviewed-capacity-example
spec:
template:
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values:
- amd64
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
- on-demand
- key: karpenter.k8s.aws/instance-category
operator: In
values:
- c
- m
- r
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: reviewed-existing-class
limits:
cpu: 1000
memory: 1000Gi
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 30s
```
### Managed Node Group 오류 코드
| Issue | 의미·확인할 근거 |
| --- | --- |
| AccessDenied | Kubernetes API 인증·인가 실패; IAM뿐 아니라 node access·EKS node-manager RBAC 확인 |
| AsgInstanceLaunchFailures | ASG launch 실패; 실제 activity message·template·capacity·권한 확인 |
| ClusterUnreachable | Kubernetes API 연결·요청 처리 timeout; VPC endpoint 누락으로 단정하지 않음 |
| InsufficientFreeAddresses | 선택한 node subnet의 가용 IP 부족; 기존 subnet IPv4 CIDR은 제자리 확장 불가 |
| NodeCreationFailure | 시작한 instance 등록 실패; bootstrap·access·필수 network 경로 확인 |
```bash
set -euo pipefail
: "${AWS_REGION:?}"; : "${CLUSTER_NAME:?}"; : "${NODEGROUP_NAME:?Set the exact managed node group}"
aws eks describe-nodegroup --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--nodegroup-name "$NODEGROUP_NAME" \
--query 'nodegroup.{Status:status,Issues:health.issues,Version:version,Release:releaseVersion,Subnets:subnets,Role:nodeRole,LaunchTemplate:launchTemplate,Repair:nodeRepairConfig}'
# For a Kubernetes authorization issue, inspect rather than blindly replace EKS-managed RBAC.
kubectl --context "$KUBE_CONTEXT" get clusterrole eks:node-manager -o yaml
kubectl --context "$KUBE_CONTEXT" get clusterrolebinding eks:node-manager -o yaml
```
각 issue의 message·resourceIds와 현재 AWS 복구 절차를 사용합니다. Troubleshooting 가이드는 managed node가 15분 안에 join하지 못하면 NodeCreationFailure가 나타날 수 있다고 설명하며 모든 boot가 그 안에 끝난다는 보장은 아닙니다. 공간이 부족하면 기존 CIDR 편집 대신 신규 주소 공간·subnet·provisioner별 migration을 계획합니다. EKS 관리 RBAC shape는 바뀔 수 있으므로 AccessDenied만 보고 이전 ClusterRole을 덮어쓰지 않습니다. Node repair·eviction은 진단과 별개이며 workload·budget·data·node 관리 mode를 고려합니다.
[EKS troubleshooting](https://docs.aws.amazon.com/eks/latest/userguide/troubleshooting.html) · [Auto Mode diagnostic paths](https://docs.aws.amazon.com/eks/latest/userguide/auto-troubleshoot.html) · [Security group paths](https://docs.aws.amazon.com/eks/latest/userguide/sec-group-reqs.html) · [Private clusters](https://docs.aws.amazon.com/eks/latest/userguide/private-clusters.html) · [Karpenter compatibility](https://karpenter.sh/docs/upgrading/compatibility/)
### Node Readiness Controller (단계별 부팅 검증)
Kubernetes SIGs Node Readiness Controller는 실제 별도 controller입니다. 검토한 v0.5.0은 cluster 범위의 `readiness.node.x-k8s.io/v1alpha1` `NodeReadinessRule`을 사용하며 EKS 내장 ConfigMap 처리기나 Node API의 GA field가 아닙니다.
Controller는 Node condition을 읽고 taint를 관리합니다. ConfigMap의 임의 `checks[].probe.exec`를 실행하지 않습니다. 기존 file 존재·containerd check에는 대응 condition을 게시하는 별도 구현·권한을 갖춘 reporter 또는 NPD custom monitor가 필요합니다. CNI 설정 file 존재만으로 CNI 준비 완료를 증명하지 못합니다. 프로젝트의 기본 reporter는 `CHECK_ENDPOINT`·`CONDITION_TYPE`·`NODE_NAME`으로 HTTP endpoint를 poll하며 이전 exec-probe 형식을 사용하지 않습니다.
아래는 명시적인 test 범위의 **taint 미리보기**입니다. 적용하면 cluster resource가 생성되고 controller가 status를 갱신하지만 `dryRun: true`는 node taint를 추가·제거하지 않습니다. 예제 condition명·node label은 EKS가 자동 공급하는 값이 아닌 custom 전제입니다.
```yaml
apiVersion: readiness.node.x-k8s.io/v1alpha1
kind: NodeReadinessRule
metadata:
name: reviewed-bootstrap-readiness
spec:
dryRun: true
enforcementMode: bootstrap-only
nodeSelector:
matchLabels:
audit.example.com/readiness-demo: 'true'
conditions:
- type: audit.example.com/CNIReady
requiredStatus: 'True'
- type: audit.example.com/ContainerRuntimeReady
requiredStatus: 'True'
taint:
key: readiness.k8s.io/bootstrap-not-ready
value: pending
effect: NoSchedule
```
실제 enforcement 전에 `status.dryRunResults`·`status.nodeEvaluations`·failed node와 선택한 Node condition을 확인합니다.
```bash
# Read-only: the controller and released CRD must already be installed.
: "${KUBE_CONTEXT:?Set the verified context}"
kubectl --context "$KUBE_CONTEXT" get nodereadinessrule reviewed-bootstrap-readiness -o yaml
kubectl --context "$KUBE_CONTEXT" get nodes \
-l audit.example.com/readiness-demo=true -o json
```
Bootstrap gate는 controller와 scheduling의 경합 전에 새 node가 일치하는 startup taint로 등록되어야 합니다. Reporter·필수 system DaemonSet은 해당 taint를 tolerate하고 API에 접근할 수 있어야 합니다. 모든 조건 충족 후 bootstrap-only는 taint를 제거하고 완료를 기록하며 이후 condition 장애에 gate를 다시 적용하지 않습니다. Continuous는 별도 정책 선택입니다.
`NoSchedule`은 taint를 tolerate하지 않는 새 Pod를 제한하며 기존 Pod를 eviction하지 않습니다. Bootstrap-only에 `defaultStatus`를 설정하는 조합은 release에서 거부합니다. 여기의 CRD 검증이 reporter health·admission webhook·모든 EKS node 유형의 운영 동작을 증명하지는 않습니다. 감사 중 node label·taint·controller·condition을 변경하지 않았습니다.
[Release v0.5.0](https://github.com/kubernetes-sigs/node-readiness-controller/releases/tag/v0.5.0) · [Enforcement and dry-run semantics](https://github.com/kubernetes-sigs/node-readiness-controller/blob/v0.5.0/docs/book/src/user-guide/concepts.md) · [Reporter configuration](https://github.com/kubernetes-sigs/node-readiness-controller/blob/v0.5.0/docs/book/src/reference/reporter-configuration.md)
---
## 4. 워크로드 디버깅
### Pod와 Container 상태
Pod phase는 Pending·Running·Succeeded·Failed·Unknown입니다. Container state는 Waiting·Running·Terminated이며 ContainerCreating·CrashLoopBackOff는 추가 Pod phase가 아닌 reason·표시 정보입니다. Running과 Ready는 다릅니다. Restart policy는 Pod 내 container를 재시작할 수 있지만 terminal Failed Pod를 Pending으로 되돌리지 않습니다. Controller가 교체한 Pod는 새로운 UID의 객체입니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${POD_NAME:?}"; : "${CONTAINER_NAME:?}"
POD_JSON=$(kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pod "$POD_NAME" -o json)
printf '%s\n' "$POD_JSON" | jq '{
name:.metadata.name,uid:.metadata.uid,owners:.metadata.ownerReferences,node:.spec.nodeName,
phase:.status.phase,reason:.status.reason,conditions:.status.conditions,
containers:.status.containerStatuses,initContainers:.status.initContainerStatuses,
ephemeralContainers:.status.ephemeralContainerStatuses
}'
POD_UID=$(printf '%s\n' "$POD_JSON" | jq -er '.metadata.uid')
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get events \
--field-selector "involvedObject.uid=$POD_UID" --sort-by='.metadata.creationTimestamp'
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" logs "$POD_NAME" -c "$CONTAINER_NAME" \
--since=15m --tail=200
```
```bash
# Separate read: this can fail when no previous container log exists.
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" logs "$POD_NAME" -c "$CONTAINER_NAME" \
--previous --tail=200
```
Previous log는 선택 container의 가장 최근 종료 instance에 대한 것이며 전체 restart 이력이 아닙니다. Rotation·Pod 삭제로 없어질 수 있습니다. 수집 시 UID·시간·교체 여부를 기록하고 init·sidecar·ephemeral container별 실패를 구분합니다. Log·state message에도 민감 정보가 있을 수 있으므로 비공개로 보관합니다. 진단 편의를 위해 전체 env·앱 config를 출력하지 않습니다.
### kubectl debug: 세 가지 다른 작업
Ephemeral container는 기존 Pod를 변경하고 --copy-to는 다른 Pod를, node/는 node 진단 Pod를 생성합니다. 모두 변경 작업이며 대응 RBAC·admission 권한이 필요합니다. 확인한 kubectl 1.36.2의 기본 profile은 general이며 자동 privileged를 뜻하지 않습니다. Host namespace·filesystem 접근과 privileged=true도 구분합니다.
#### Ephemeral container
```bash
# MUTATION: adds a permanent-to-this-Pod-spec ephemeral-container entry.
: "${DEBUG_IMAGE:?Use a reviewed non-root diagnostic image with a compatible shell}"
: "${DEBUG_CONTAINER_NAME:?Choose an unused container name}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" debug "$POD_NAME" -it \
--container="$DEBUG_CONTAINER_NAME" --target="$CONTAINER_NAME" \
--image="$DEBUG_IMAGE" --profile=restricted -- sh
```
Restricted profile은 capability를 제거하고 privilege escalation을 막으며 non-root·RuntimeDefault seccomp를 요구합니다. Image·user·shell이 이를 지원해야 하며 임의 root 전용 BusyBox가 시작된다고 보장하지 않습니다. --target은 runtime이 지원할 때 대상 process namespace를 요청할 뿐 앱 filesystem·env 복제나 권한 우회가 아닙니다. 종료해도 기존 Pod spec의 ephemeral-container entry를 삭제할 수는 없습니다.
#### Pod 복사
```bash
# MUTATION: copy only a reviewed reproduction Pod; inspect all side effects first.
: "${DEBUG_POD_NAME:?Choose a new owned Pod name in the same namespace}"
: "${DEBUG_IMAGE:?Use a reviewed diagnostic image that provides sleep}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" debug "$POD_NAME" \
--copy-to="$DEBUG_POD_NAME" --container="$CONTAINER_NAME" --image="$DEBUG_IMAGE" \
--keep-init-containers=false --keep-labels=false --keep-annotations=false \
--share-processes=true --profile=general -- sleep 3600
```
Native CLI 검증에서 선택 container image·command 교체, init container 제거, ServiceAccount 유지·process namespace 공유를 확인했습니다. 다른 일반 container·env/Secret 참조·volume은 남아 실행되거나 같은 data를 사용할 수 있습니다. 복사본은 같은 namespace에 있고 다른 node에 배치될 수 있으며 격리된 data clone이 아닙니다. Admission 변경·부작용·identity·persistent volume·정리를 먼저 검토합니다. General capability가 namespace policy에 거부될 수 있으며 policy를 조용히 낮추지 않습니다.
#### Node 진단
```bash
# MUTATION: privileged host diagnostic Pod, only where this access is authorized.
: "${NODE_NAME:?Use the exact reviewed Node}"
: "${NODE_DEBUG_IMAGE:?Use a reviewed image with nsenter}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" debug "node/$NODE_NAME" -it \
--image="$NODE_DEBUG_IMAGE" --profile=sysadmin \
-- nsenter -t 1 -m -- journalctl -u kubelet --since "15 minutes ago" -n 200 --no-pager
```
Node debug는 /host에 host root를 mount하고 host namespace를 사용하며 명시적 sysadmin은 privileged를 추가합니다. 명령이 log 조회여도 광범위한 host 접근 권한입니다. Image에 필요한 도구가 준비되어야 합니다. Auto Mode guide는 이 경로를 지원하지만 일반 SSH 접근은 여전히 불가합니다. 다른 OS/node 유형은 지원 경로를 사용하며 kubelet/runtime/network가 고장 나면 새 debug Pod가 시작된다고 보장하지 않습니다.
실제 생성된 debug Pod명·UID를 기록하고 증거 검토 후 해당 별도 Pod만 제거합니다. Ephemeral container 정리를 이유로 application Pod를 삭제하지 않습니다.
```bash
# MUTATION: remove only the separately created debug Pod after checking its identity.
: "${DEBUG_POD_NAME:?}"; : "${EXPECTED_DEBUG_UID:?Use the UID recorded at creation}"
ACTUAL_DEBUG_UID=$(kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pod "$DEBUG_POD_NAME" \
-o jsonpath='{.metadata.uid}')
test "$ACTUAL_DEBUG_UID" = "$EXPECTED_DEBUG_UID" || { echo "Debug Pod changed; stop" >&2; exit 1; }
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" delete pod "$DEBUG_POD_NAME" --timeout=2m
```
UID 확인은 안전 확인이며 원자적인 delete precondition은 아닙니다. 확인과 삭제 사이 이름 재사용이 없도록 작업을 조정합니다. 감사에서 debug container·privileged workload·node 명령을 실행하지 않았으며 native CLI 검증에는 로컬 모의 API만 사용했습니다.
### Deployment 롤아웃 관리
```bash
# Read-only rollout evidence.
: "${DEPLOYMENT_NAME:?Set the owned Deployment}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" rollout status \
"deployment/$DEPLOYMENT_NAME" --timeout=2m
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" rollout history "deployment/$DEPLOYMENT_NAME"
```
```bash
# MUTATION: workload revision rollback, not database/PVC/control-plane rollback.
set -euo pipefail
: "${REVIEWED_REVISION:?Set an inspected compatible revision}"
[[ "$REVIEWED_REVISION" =~ ^[1-9][0-9]*$ ]]
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" rollout undo \
"deployment/$DEPLOYMENT_NAME" --to-revision="$REVIEWED_REVISION"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" rollout status \
"deployment/$DEPLOYMENT_NAME" --timeout=5m
```
Timeout·실패는 조사할 근거이지 성공이 아닙니다. GitOps·다른 reconciler와 조정하며 rollout rollback이 database·schema 변경을 복원하지는 않습니다. Deployment pause/resume은 rollout 진행을 제어하며 HPA·모든 Pod 생성을 중지하지 않습니다. Rollout restart는 같은 image 참조여도 template 변경·replacement를 일으키며 미고정 image는 다른 content로 해석될 수 있습니다. 정확한 namespace·Deployment의 검토한 계획으로 실행하고 restart·undo·scale을 triage에 묶지 않습니다.
### HPA/VPA 스케일링 문제
```bash
# Read-only: use the actual scaler names and workload namespace.
: "${HPA_NAME:?}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get hpa "$HPA_NAME" -o yaml
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" describe hpa "$HPA_NAME"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" top pods --containers
```
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: app-hpa
namespace: diagnostics-example
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
```
```bash
# VPA is a separately installed controller/CRD, not built into EKS.
: "${VPA_NAME:?}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get vpa "$VPA_NAME" -o json | jq '{
target:.spec.targetRef,updatePolicy:.spec.updatePolicy,resourcePolicy:.spec.resourcePolicy,
recommendation:.status.recommendation,conditions:.status.conditions
}'
```
HPA 예시는 기존 Deployment·resource-metrics provider와 대상 container의 CPU/memory request가 필요합니다. Utilization은 limit가 아닌 request 대비 비율입니다. 여러 metric은 가장 큰 권장 replica를 선택하며 missing·error metric도 결정에 영향을 줍니다. Memory utilization이 leak·OOM의 보편적 해결은 아니며 scale-down 안정화·변경률 정책도 일시정지 스위치가 아닙니다.
VPA는 별도 설치합니다. 실제 recommendation·condition·update mode·지원 release를 확인합니다. 현재 안내에서 legacy Auto mode명은 deprecated이며 recommendation 전용 Off 또는 검토한 update mode를 선택합니다. HPA의 동일한 request 분모를 VPA가 변경하도록 할 때는 조정이 필요합니다.
### Probe 설정
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app-probe-example
namespace: diagnostics-example
spec:
containers:
- name: app
image: registry.example.com/owned/app:replace-with-reviewed-digest
ports:
- name: http
containerPort: 8080
startupProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: http
periodSeconds: 5
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
```
Placeholder image·health endpoint를 실제 앱 계약으로 교체합니다. Standalone Pod는 schema 예시이며 복제된 production workload가 아닙니다. Startup 성공 전 liveness·readiness가 억제됩니다. 5초 주기 30회와 initial delay는 대략적인 startup budget이며 정확한 150초 deadline이 아닙니다. Readiness 실패는 service routing의 readiness를 내리며 container를 재시작하지 않습니다. 외부 의존성이 느리다는 이유만으로 정상 process를 liveness가 재시작하게 하지 않습니다. Shutdown·resource pressure·실제 응답 시간을 별도 검증합니다.
[Pod lifecycle](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/) · [Debug running Pods](https://kubernetes.io/docs/tasks/debug/debug-application/debug-running-pod/) · [HPA behavior](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) · [VPA modes](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler)
---
## 5. 네트워킹 진단
### VPC CNI·IP 할당
먼저 node·network 구현을 구분합니다. 아래 aws-node 설정은 표준 Amazon VPC CNI 경로입니다. Auto Mode는 자체 관리 networking·NodeClass 설정을 사용하므로 aws-node DaemonSet 변경이 Auto Mode node 설정은 아닙니다. Windows·Fargate·Hybrid Node의 적용 범위·진단 경로도 다릅니다. 초기 계정·context guard와 정확한 node·namespace 범위를 유지합니다.
```bash
# Standard Amazon VPC CNI on applicable nodes, not an Auto Mode control interface.
: "${KUBE_CONTEXT:?}"; : "${AWS_REGION:?}"; : "${INSTANCE_ID:?Use the inspected EC2 node ID}"
kubectl --context "$KUBE_CONTEXT" -n kube-system get daemonset aws-node -o json | jq '{
containers:[.spec.template.spec.containers[] | {name,image,settings:[
.env[]? | select(.name | IN("ENABLE_PREFIX_DELEGATION","WARM_PREFIX_TARGET","WARM_IP_TARGET",
"MINIMUM_IP_TARGET","AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG","ENI_CONFIG_LABEL_DEF"))
]}]
}'
kubectl --context "$KUBE_CONTEXT" -n kube-system get pods -l k8s-app=aws-node -o wide
kubectl --context "$KUBE_CONTEXT" -n kube-system logs -l k8s-app=aws-node \
-c aws-node --since=15m --tail=100 --prefix
aws ec2 describe-network-interfaces --region "$AWS_REGION" \
--filters "Name=attachment.instance-id,Values=$INSTANCE_ID" \
--query 'NetworkInterfaces[].{ID:NetworkInterfaceId,Subnet:SubnetId,Description:Description,IPv4:PrivateIpAddresses[].PrivateIpAddress,IPv4Prefixes:Ipv4Prefixes,IPv6Prefixes:Ipv6Prefixes,Groups:Groups}'
```
```bash
# Read-only: use subnets actually selected by the node/provisioner, not all account subnets.
: "${SUBNET_ID:?Set an inspected subnet ID}"
aws ec2 describe-subnets --region "$AWS_REGION" --subnet-ids "$SUBNET_ID" \
--query 'Subnets[].{ID:SubnetId,VPC:VpcId,AZ:AvailabilityZone,CIDR:CidrBlock,AvailableIPv4:AvailableIpAddressCount}'
```
ENI description은 소유권 경계가 아닙니다. 확인한 instance·subnet ID와 관련 custom-networking·Pod ENI를 사용합니다. Subnet의 free-address 수가 연속된 prefix 가용성을 증명하지 않고 VPC CIDR 추가만으로 Pod network가 설정되지 않습니다.
### Prefix Delegation
지원하는 Linux·Nitro·CNI 구성에서는 prefix delegation으로 IP 밀도·할당 동작을 개선할 수 있습니다. 연속 prefix 공간·예약, ENI/prefix limit, node maxPods·allocatable Pod와 migration 준비를 확인합니다. 실행 node에서 무조건 활성화하거나 가용 IPv4 수만으로 실제 용량을 추정하지 않습니다.
다음은 **검토용 configuration fragment**이며 실제 지원 add-on·chart 설정에 병합할 값입니다. 기존 설정 전체를 교체하는 파일이 아닙니다.
```json
{
"env": {
"ENABLE_PREFIX_DELEGATION": "true",
"WARM_PREFIX_TARGET": "1"
}
}
```
설정한 WARM_IP_TARGET·MINIMUM_IP_TARGET은 WARM_PREFIX_TARGET보다 우선합니다. Warm target은 여분 주소·prefix를 유지할 뿐 node 용량 예약이나 고갈·단편화 subnet 복구가 아닙니다. 저장 configuration·owner를 검토한 뒤 통제된 rollout을 수행하고 새 Pod·실제 IPAM 상태를 확인합니다.
### Custom Networking
CNI mode 변경 전에 겹치지 않는 VPC 주소 공간, 실제 AZ별 Pod subnet, routing·egress·SG와 migration 용량을 준비합니다. 기존 CIDR·subnet 생성 몇 줄과 env toggle은 완전한 운영 절차가 아니었습니다. 기존 subnet IPv4 CIDR은 제자리 확장할 수 없습니다. 아래 표준 ENIConfig 방식은 Auto Mode networking 제어와 구분합니다.
```json
{
"env": {
"AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG": "true",
"ENI_CONFIG_LABEL_DEF": "topology.kubernetes.io/zone"
}
}
```
```yaml
apiVersion: crd.k8s.amazonaws.com/v1alpha1
kind: ENIConfig
metadata:
name: ap-northeast-2a
spec:
securityGroups:
- sg-0123456789abcdef0
subnet: subnet-0123456789abcdef0
```
ENIConfig는 placeholder ID를 사용하는 한 AZ 예시입니다. Zone 기반 선택이면 모든 eligible zone에 정확한 설정과 대응 node label이 필요합니다. AZ당 Pod subnet이 여러 개면 별도 선택 체계를 설계합니다. Pod용 SG 설정에 따라 적용 SG가 달라질 수 있으므로 설치 CNI의 우선순위를 확인합니다. Controller 설정·새 node rollout·Pod 배치를 검증한 뒤 이전 용량을 정리합니다. 전체 설정은 [네트워킹 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/03-eks-networking-part1.md)를 참고합니다.
### CoreDNS·Resolver 문맥
순수 Auto Mode node는 CoreDNS를 node system service로 실행합니다. 혼합 cluster는 non-Auto node용 Deployment를 유지해야 하며 순수 Auto에서 Deployment 부재만으로 DNS 장애라 할 수 없습니다. Deployment 기반 DNS는 다음을 확인합니다.
```bash
# CoreDNS Deployment on standard/mixed clusters; Auto Mode node-system DNS differs.
kubectl --context "$KUBE_CONTEXT" -n kube-system get pods -l k8s-app=kube-dns -o wide
kubectl --context "$KUBE_CONTEXT" -n kube-system logs -l k8s-app=kube-dns \
--since=15m --tail=100 --prefix
kubectl --context "$KUBE_CONTEXT" -n kube-system get configmap coredns -o yaml
# Inspect the actual resolver context in an owned application container with these tools.
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" exec "$POD_NAME" -c "$CONTAINER_NAME" \
-- cat /etc/resolv.conf
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" exec "$POD_NAME" -c "$CONTAINER_NAME" \
-- nslookup kubernetes.default.svc.cluster.local.
```
관련 logging 설정이 없으면 CoreDNS가 모든 DNS query를 기록하지는 않습니다. 새 test Pod는 장애 workload와 namespace·node·DNS·identity·policy 경로가 다를 수 있습니다. 실제 resolver 문맥에서 절대 해석 검증에는 끝에 점이 있는 FQDN을 사용합니다.
아래 ndots=2는 지연의 보편적 해결책이 아닌 실험 값입니다. Search 동작·부분 수식 이름에 영향을 줄 수 있습니다. Libc·언어 resolver·앱 cache가 다르므로 glibc의 single-request-reopen 같은 옵션을 이식 가능한 전제로 쓰지 않습니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: dns-options-example
namespace: diagnostics-example
spec:
dnsPolicy: ClusterFirst
dnsConfig:
options:
- name: ndots
value: '2'
- name: timeout
value: '2'
- name: attempts
value: '3'
containers:
- name: app
image: registry.example.com/owned/app:replace-with-reviewed-digest
```
다음 Corefile은 예시입니다. 설치 version·필수 plugin·custom zone/forwarder·managed add-on 설정과 비교하고 live ConfigMap을 통째로 덮어쓰지 않습니다. Cache·max_concurrent·lameduck 값은 traffic·health 검증이 필요합니다. Pods insecure는 Kubernetes plugin의 Pod-record mode이며 API TLS·인증을 끄는 옵션이 아닙니다.
```text
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
```
### Service·EndpointSlice 검증
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${SERVICE_NAME:?}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get service "$SERVICE_NAME" -o json | jq '{
name:.metadata.name,type:.spec.type,clusterIP:.spec.clusterIP,ipFamilies:.spec.ipFamilies,
externalName:.spec.externalName,selector:.spec.selector,ports:.spec.ports,
trafficDistribution:.spec.trafficDistribution,externalTrafficPolicy:.spec.externalTrafficPolicy
}'
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get endpointslices \
-l "kubernetes.io/service-name=$SERVICE_NAME" -o json | jq '[.items[] | {
name:.metadata.name,addressType,ports,
endpoints:[.endpoints[]? | {addresses,conditions,nodeName,zone,targetRef}]
}]'
```
현재 endpoint 검증은 EndpointSlice를 사용하며 이전 Endpoints API는 deprecated입니다. Service selector·port/targetPort·address family·endpoint ready/serving/terminating을 확인합니다. Headless·ExternalName·selectorless Service는 동작이 다릅니다. Endpoint 주소 존재가 의도한 traffic 수신을 증명하지 않으며 Service port와 container port가 항상 같지는 않습니다.
### NetworkPolicy AND/OR 로직
한 peer의 namespaceSelector·podSelector는 AND이며 별도 peer·rule은 대안입니다. PodSelector만 있는 peer는 policy namespace 안의 Pod를 선택합니다. 모든 적용 NetworkPolicy의 허용은 합집합이며 제한적인 policy가 다른 broad allow를 덮어쓰지 않습니다. Manifest를 firewall로 믿기 전에 실제 CNI·node 유형의 enforcement 지원·mode를 확인합니다.
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: reviewed-api-policy
namespace: diagnostics-example
spec:
podSelector:
matchLabels:
app: api-server
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 9090
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
```
```bash
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get networkpolicies -o yaml
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pods --show-labels
kubectl --context "$KUBE_CONTEXT" get namespaces --show-labels
```
예시는 일치하는 CoreDNS Pod로 TCP·UDP DNS를 허용합니다. Database-only egress만 있으면 DNS가 빠집니다. Node-local·Auto Mode DNS는 경로가 다르므로 mode별 확인이 필요합니다. 실제 monitoring·database label·port와 검토한 외부 의존성만 적용합니다. 예시 policy이며 입증된 production allowlist가 아닙니다.
### 범위를 제한한 Network Test
```bash
# An intentional, bounded request from the actual workload context with curl installed.
: "${HEALTH_URL:?Set the owned safe health-check URL}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" exec "$POD_NAME" -c "$CONTAINER_NAME" \
-- curl --silent --show-error --connect-timeout 5 --max-time 10 \
--output /dev/null --write-out 'HTTP status: %{http_code}\n' "$HEALTH_URL"
```
```bash
# In an approved diagnostic context with the named tools.
: "${SERVICE_FQDN:?Set the exact owned service DNS name}"
dig +time=2 +tries=1 "$SERVICE_FQDN"
# Packet capture needs the appropriate capabilities/privileges and an owned target.
: "${TARGET_IP:?Set one reviewed peer IP}"
umask 077
timeout 30 tcpdump -i any -nn -c 100 -s 96 "host $TARGET_IP and port 443" -w owned-capture.pcap
# Separate deliberate load test: only against an agreed iperf3 server.
: "${IPERF_SERVER:?Set the owned test server}"
iperf3 -c "$IPERF_SERVER" -p 5201 -t 10 -P 1 -b 10M
```
앞의 debug 절차로 검토한 image·도구 구현을 선택합니다. Netshoot Pod 생성은 수동 관찰이 아닌 변경입니다. Packet capture는 root 사용자명만이 아니라 capability·privilege가 필요하고 범위를 제한해도 민감 header·data가 포함될 수 있어 비공개 보관·공유 전 검토가 필요합니다. Dig +trace는 workload resolver만이 아닌 직접 iterative DNS 경로를 검사합니다. Iperf3는 의도적인 traffic 생성이며 throughput은 network latency도, 이 감사의 실측 결과도 아닙니다.
[Prefix mode](https://docs.aws.amazon.com/eks/latest/best-practices/prefix-mode-linux.html) · [Custom networking](https://docs.aws.amazon.com/eks/latest/best-practices/custom-networking.html) · [NetworkPolicy semantics](https://kubernetes.io/docs/concepts/services-networking/network-policies/) · [EndpointSlices](https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/)
---
## 6. 스토리지 문제 해결
### Driver·권한 식별
연결된 PV의 spec.csi.driver·volumeHandle과 StorageClass provisioner를 읽습니다. 표준 EBS는 ebs.csi.aws.com, Auto Mode는 ebs.csi.eks.amazonaws.com과 관리 controller를 사용하므로 표준 controller Deployment가 없을 수 있습니다. 아래 표준 driver log 명령은 해당 driver 설치를 전제합니다. Fargate Pod·EKS Hybrid Node에는 EBS를 mount할 수 없으며 표준 controller를 Fargate에 배치해도 data-plane 제약은 달라지지 않습니다.
```bash
# Read-only: identify the actual installed driver and workload owner first.
kubectl --context "$KUBE_CONTEXT" get csidrivers
: "${CSI_NAMESPACE:?Set the namespace of the installed standard CSI controller}"
kubectl --context "$KUBE_CONTEXT" -n "$CSI_NAMESPACE" get deployments,daemonsets,pods -o wide
: "${CSI_CONTROLLER_NAME:?Use an observed controller Deployment name}"
: "${CSI_CONTAINER_NAME:?Use the CSI plugin container name}"
kubectl --context "$KUBE_CONTEXT" -n "$CSI_NAMESPACE" logs "deployment/$CSI_CONTROLLER_NAME" \
-c "$CSI_CONTAINER_NAME" --since=15m --tail=100
```
```bash
# Inspect only the role actually used by the standard EBS CSI controller.
: "${CSI_ROLE_NAME:?Set the reviewed role name}"
aws iam get-role --role-name "$CSI_ROLE_NAME" --query Role.AssumeRolePolicyDocument
aws iam list-attached-role-policies --role-name "$CSI_ROLE_NAME"
aws iam list-role-policies --role-name "$CSI_ROLE_NAME"
```
현재 EKS guide는 표준 driver 권한으로 AmazonEBSCSIDriverPolicyV2 검토를 권장합니다. Driver ownership tag로 volume·snapshot 관리 범위를 제한하고 CSI-migrated volume tag도 지원합니다. 이전 policy를 교체하기 전에 migration·기존 자원 tag를 확인합니다. 모든 변경 action을 Resource:*로 허용한 policy를 범용 해결책으로 붙이지 않습니다. 일부 AWS 조회 action의 wildcard 요구와 광범위한 변경 권한은 다릅니다.
실제 Pod Identity·IRSA role/trust를 확인하고 node identity로 추정하지 않습니다. 고객 KMS key에는 key policy·grant·encrypt/decrypt 권한이 필요하며 문서의 CreateGrant 조건은 kms:GrantIsForAWSResource를 포함합니다. Volume 생성 권한만으로 선택 KMS key 사용·대상 node attach를 증명하지 못합니다. 위 진단 명령은 IAM을 변경하지 않습니다.
### EFS Mount Target·Access Point
```bash
set -euo pipefail
: "${AWS_REGION:?}"; : "${FILE_SYSTEM_ID:?Use the owned EFS filesystem}"
aws efs describe-file-systems --region "$AWS_REGION" --file-system-id "$FILE_SYSTEM_ID"
aws efs describe-mount-targets --region "$AWS_REGION" --file-system-id "$FILE_SYSTEM_ID"
: "${MOUNT_TARGET_ID:?Use the relevant mount target}"
aws efs describe-mount-target-security-groups --region "$AWS_REGION" --mount-target-id "$MOUNT_TARGET_ID"
: "${EFS_SECURITY_GROUP_ID:?Use an observed mount-target security group}"
aws ec2 describe-security-groups --region "$AWS_REGION" --group-ids "$EFS_SECURITY_GROUP_ID"
```
Filesystem 유형·region·접근 가능한 mount target·DNS·TCP 2049·양방향 network를 확인합니다. Regional EFS와 One Zone은 장애 영역 동작이 다릅니다. IAM authorization·access-point POSIX identity/directory 권한·Pod security context도 별도 계층입니다. 아래는 placeholder ID를 사용하는 기존 filesystem 설정 예시이며 filesystem·role·network 전체 생성 절차가 아닙니다.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: reviewed-efs
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap
fileSystemId: fs-0123456789abcdef0
directoryPerms: '700'
gidRangeStart: '1000'
gidRangeEnd: '2000'
basePath: /diagnostics-example
mountOptions:
- tls
reclaimPolicy: Retain
```
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: efs-claim
namespace: diagnostics-example
spec:
accessModes:
- ReadWriteMany
storageClassName: reviewed-efs
resources:
requests:
storage: 5Gi
```
5Gi PVC 요청은 EFS가 강제하는 용량 quota가 아닙니다. Access point는 server-side POSIX identity를 강제할 수 있으므로 client Pod UID만으로 접근을 판단하거나 일괄 chmod로 해결하지 않습니다. TLS mount 암호화와 filesystem at-rest 암호화는 별도입니다. Retain이면 access-point·data 정리 계획과 잔여 비용을 고려합니다. Fargate EFS는 별도 static provisioning 경로이며 모든 node 유형에 이 dynamic 예제가 적용되지는 않습니다.
### PVC/PV 상태·삭제 보호
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${PVC_NAME:?Set the owned claim name}"
PVC_JSON=$(kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pvc "$PVC_NAME" -o json)
printf '%s\n' "$PVC_JSON" | jq '{
name:.metadata.name,namespace:.metadata.namespace,uid:.metadata.uid,
deleting:.metadata.deletionTimestamp,finalizers:.metadata.finalizers,
phase:.status.phase,conditions:.status.conditions,volumeName:.spec.volumeName,
hasStorageClassName:(.spec | has("storageClassName")),
storageClassName:.spec.storageClassName,accessModes:.spec.accessModes,resources:.spec.resources
}'
PVC_UID=$(printf '%s\n' "$PVC_JSON" | jq -er '.metadata.uid')
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get events \
--field-selector "involvedObject.uid=$PVC_UID" --sort-by='.metadata.creationTimestamp'
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pods -o json | jq --arg claim "$PVC_NAME" '[
.items[] | select(any(.spec.volumes[]?; .persistentVolumeClaim.claimName? == $claim)) |
{name:.metadata.name,uid:.metadata.uid,owners:.metadata.ownerReferences,
node:.spec.nodeName,phase:.status.phase,deleting:.metadata.deletionTimestamp}
]'
PV_NAME=$(printf '%s\n' "$PVC_JSON" | jq -r '.spec.volumeName // empty')
if [ -z "$PV_NAME" ]; then
echo "No bound PV: inspect StorageClass, consumer scheduling and provisioning events."
else
kubectl --context "$KUBE_CONTEXT" get pv "$PV_NAME" -o json | jq '{
name:.metadata.name,uid:.metadata.uid,claimRef:.spec.claimRef,
deleting:.metadata.deletionTimestamp,finalizers:.metadata.finalizers,
reclaimPolicy:.spec.persistentVolumeReclaimPolicy,csi:.spec.csi,nodeAffinity:.spec.nodeAffinity
}'
kubectl --context "$KUBE_CONTEXT" get volumeattachments -o json | jq --arg pv "$PV_NAME" '[
.items[] | select(.spec.source.persistentVolumeName == $pv) |
{name:.metadata.name,driver:.spec.attacher,node:.spec.nodeName,status:.status}
]'
fi
```
PVC 이름은 namespace 안에서 고유하므로 consumer도 해당 namespace에서 검색해야 합니다. Claim/PV UID·controller owner·VolumeAttachment·finalizer를 확인합니다. Deletion timestamp에 따른 Terminating 표시는 별도 PVC status.phase가 아닙니다. WaitForFirstConsumer에서는 스케줄 가능한 consumer가 생기기 전 Pending이 정상일 수 있습니다. StorageClassName 생략과 class 없음을 명시한 빈 문자열은 다릅니다.
삭제를 끝내려고 PVC/PV finalizer를 모두 null로 만들지 않습니다. PVC protection·CSI detach/delete·reclaim policy는 다른 생명주기를 보호합니다. 재생성할 수 있는 controller를 포함한 consumer·attachment·controller 오류·backup·data owner를 먼저 확인합니다. 최후의 orphan 복구는 driver별 절차와 검증한 data/attachment 상태가 필요하며 metadata 제거가 안전한 detach·data 복원이 아닙니다. Delete는 backing storage를 지울 수 있고 Retain도 backup은 아닙니다.
### WaitForFirstConsumer·Topology·암호화
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: reviewed-ebs-wffc
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: 'true'
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
reclaimPolicy: Retain
```
위는 표준 EBS CSI provisioner입니다. WaitForFirstConsumer는 초기 provisioning·binding에 scheduler 제약을 반영하며 EBS를 cross-AZ로 만들거나 장애 AZ의 volume을 복구하지 않습니다. Ap-northeast-2a/2c 같은 zone으로 제한하려면 실제 CSI topology·가용 용량과 workload 배치를 맞춥니다. PV affinity 첫 expression이 항상 zone이거나 Pod가 요청한 affinity가 실제 위치라고 가정하지 않습니다.
```bash
# Use the actual consumer node, not the Pod's requested node-affinity text.
: "${NODE_NAME:?Set an observed consumer node}"
kubectl --context "$KUBE_CONTEXT" get node "$NODE_NAME" -o json | jq '{
name:.metadata.name,providerID:.spec.providerID,
topologyLabels:(.metadata.labels | with_entries(select(.key | contains("topology"))))
}'
kubectl --context "$KUBE_CONTEXT" get csinode "$NODE_NAME" -o json | jq '.spec.drivers'
# For an actual EBS-backed PV, inspect the volume handle and Region before this lookup.
: "${AWS_REGION:?}"; : "${EBS_VOLUME_ID:?Set the inspected EBS volume ID}"
aws ec2 describe-volumes --region "$AWS_REGION" --volume-ids "$EBS_VOLUME_ID" \
--query 'Volumes[].{ID:VolumeId,AZ:AvailabilityZone,State:State,Encrypted:Encrypted,KMS:KmsKeyId,Attachments:Attachments}'
```
Auto Mode는 별도 provisioner·node 호환 요구를 사용합니다. **encrypted: "true"를 명시하고 실제 volume·KMS key를 확인합니다.** 현재 Auto Mode StorageClass parameter 표의 encrypted 기본값은 false입니다. Auto Mode node root/data disk 암호화가 모든 workload PVC의 암호화를 증명하지 않습니다. StorageClass 변경이 기존 volume을 소급 변경하지도 않습니다.
WaitForFirstConsumer만으로 기존 EBS를 다른 AZ에 attach할 수 없습니다. Migration·복구는 문서의 snapshot 또는 통제된 static-volume 절차, 정확한 ownership tag·IAM·앱 일관성을 고려한 data 처리가 필요하며 driver명 편집으로 끝나지 않습니다. Snapshot controller·CRD도 별도 전제이고 snapshot 생성이 restore 성공의 증명은 아닙니다. ReadWriteOnce는 한 node 기준이며 보편적으로 “Pod 하나”를 뜻하지 않습니다. Auto Mode SELinux가 추가 cross-Pod 제약을 줄 수 있으므로 data를 보존하고 원하는 접근·일관성 모델을 검토합니다.
[EBS CSI/IAM](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html) · [Managed-policy scopes](https://docs.aws.amazon.com/eks/latest/userguide/security-iam-awsmanpol.html) · [Auto Mode parameters](https://docs.aws.amazon.com/eks/latest/userguide/create-storage-class.html) · [PV lifecycle](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) · [EFS CSI](https://github.com/kubernetes-sigs/aws-efs-csi-driver)
---
## 7. 관측성 아키텍처
### 기존 수집 상태 확인
장애 대응 중 이전 v1.0.0 add-on을 바로 설치하지 않습니다. [모니터링 설정 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/06-eks-monitoring-logging.md)로 현재 호환 add-on/chart와 관리 주체 하나를 선택합니다. 변경 전 IAM/Pod Identity·log/metric 설정·node 적용 범위·자동 instrumentation/restart 옵션을 확인합니다. 최근 operator는 앱 instrumentation·rollout에 영향을 줄 수 있고 두 관리 주체가 충돌할 수 있습니다.
```bash
# Read-only: inspect the installed owner/version rather than installing during triage.
: "${AWS_REGION:?}"; : "${CLUSTER_NAME:?}"; : "${KUBE_CONTEXT:?}"
aws eks describe-addon --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--addon-name amazon-cloudwatch-observability \
--query 'addon.{Version:addonVersion,Status:status,Issues:health.issues,Configuration:configurationValues,Role:serviceAccountRoleArn,PodIdentity:podIdentityAssociations}'
kubectl --context "$KUBE_CONTEXT" -n amazon-cloudwatch get pods,deployments,daemonsets -o wide
# If Helm owns the installation, inspect that existing release instead.
helm list -n amazon-cloudwatch --kube-context "$KUBE_CONTEXT"
```
Add-on 부재는 Helm 관리 또는 미설치일 수 있습니다. Pod Running·add-on ACTIVE만으로 전송·범위·사용자 관점 정상 상태를 증명하지 못합니다. Scrape target·IAM/network/TLS·ingestion 오류·retention·비용을 확인합니다. 아래는 이를 전제로 한 예시이며 검증된 production 플랫폼이 아닙니다.
### PromQL: 지표가 측정하는 대상 정의
Query는 **단일 cluster의 올바른 label을 가진 dataset**과 diagnostics-example namespace를 전제합니다. 공유 backend에서는 실제 cluster/job selector를 추가합니다. cAdvisor·kube-state-metrics 수집이 필요하며 query를 작성한다고 series가 생기지 않습니다. 집계는 명시한 범위 안의 중복 exporter-instance label만 제거합니다. Timestamp·Pod/container identity·last-termination 지표 등 version별 제공 여부를 확인합니다.
#### 컨테이너별 throttling이 발생한 CFS period 비율
```promql
sum by (namespace,pod,container) (rate(container_cpu_cfs_throttled_periods_total{namespace="diagnostics-example",container!="",container!="POD"}[5m]))
/ on (namespace,pod,container) (sum by (namespace,pod,container) (rate(container_cpu_cfs_periods_total{namespace="diagnostics-example",container!="",container!="POD"}[5m])) > 0)
```
#### throttling period 비율이 높은 컨테이너 10개
```promql
topk(10, sum by (namespace,pod,container) (rate(container_cpu_cfs_throttled_periods_total{namespace="diagnostics-example",container!="",container!="POD"}[5m]))
/ on (namespace,pod,container) (sum by (namespace,pod,container) (rate(container_cpu_cfs_periods_total{namespace="diagnostics-example",container!="",container!="POD"}[5m])) > 0))
```
#### 마지막 종료 원인이 OOM인 상태 — 신규 이벤트 수를 의미하지 않음
```promql
max by (namespace,pod,container) (kube_pod_container_status_last_terminated_reason{namespace="diagnostics-example",reason="OOMKilled"} == 1)
```
#### 최근 재시작이 증가하고 마지막 종료 원인이 OOM인 컨테이너 — 정확한 OOM 횟수와 구분
```promql
(max by (namespace,pod,container) (increase(kube_pod_container_status_restarts_total{namespace="diagnostics-example"}[15m])) > 0)
and on (namespace,pod,container) (max by (namespace,pod,container) (kube_pod_container_status_last_terminated_reason{namespace="diagnostics-example",reason="OOMKilled"} == 1))
```
#### 컨테이너별 working set과 0보다 큰 설정 memory limit의 비율
```promql
max by (namespace,pod,container) (container_memory_working_set_bytes{namespace="diagnostics-example",container!="",container!="POD"})
/ on (namespace,pod,container)
max by (namespace,pod,container) (kube_pod_container_resource_limits{namespace="diagnostics-example",resource="memory",unit="byte"} > 0)
```
#### Pod별 일반 컨테이너의 최근 15분 재시작 증가 추정치
```promql
sum by (namespace,pod) (max by (namespace,pod,container) (increase(kube_pod_container_status_restarts_total{namespace="diagnostics-example"}[15m])))
```
#### 재시작 증가 추정치가 높은 Pod 10개
```promql
topk(10, sum by (namespace,pod) (max by (namespace,pod,container) (increase(kube_pod_container_status_restarts_total{namespace="diagnostics-example"}[15m]))))
```
#### 현재 대기 사유가 CrashLoopBackOff로 보고된 컨테이너
```promql
max by (namespace,pod,container) (kube_pod_container_status_waiting_reason{namespace="diagnostics-example",reason="CrashLoopBackOff"} == 1)
```
#### 삭제 중이 아닌 활성 Pod의 Ready=false 상태 — Running Pod 포함
```promql
((1 - max by (namespace,pod) (kube_pod_status_ready{namespace="diagnostics-example",condition="true"})) > 0)
and on (namespace,pod) (max by (namespace,pod) (kube_pod_status_phase{namespace="diagnostics-example",phase=~"Pending|Running|Unknown"} == 1))
unless on (namespace,pod) kube_pod_deletion_timestamp{namespace="diagnostics-example"}
```
Throttled CFS period는 CPU 사용률·경과 CPU 시간 비율과 다릅니다. Memory 비율은 양수 limit가 있는 container만 포함하며 limit·data 부재가 사용률 0은 아닙니다. Increase()는 reset을 고려한 외삽 추정으로 소수일 수 있고 changes(restarts_total)는 관측 값 변경 수이지 OOM 횟수가 아닙니다. 마지막 종료 원인과 restart 증가는 상관관계이지 정확한 OOM 횟수·memory leak 증명이 아닙니다. 모든 restart를 CrashLoop로 추정하지 않고 waiting reason을 확인합니다.
Readiness query는 Running-but-NotReady를 포함하고 종료·삭제 중 Pod를 제외합니다. 별도의 scrape·absent-target 감시가 필요하며 series가 없다고 workload 정상으로 판단하지 않습니다.
### CloudWatch Logs Insights
각 블록을 적절한 log group·기간에 별도로 실행합니다. Kubernetes.* field는 collector schema에 의존하므로 실제 record를 확인합니다. Error 문구·OOM keyword는 진단 단서이며 요청 오류율·전체 장애 이력이 아닙니다.
#### 오류 메시지 표본 — 요청 오류율과 구분
```text
fields @timestamp, @message, kubernetes.pod_name, kubernetes.namespace_name
| filter kubernetes.namespace_name = "diagnostics-example"
| filter @message like /error|Error|ERROR|exception|Exception|EXCEPTION/
| sort @timestamp desc
| limit 100
```
#### 선택한 namespace의 특정 Pod
```text
fields @timestamp, @message
| filter kubernetes.namespace_name = "diagnostics-example" and kubernetes.pod_name = "REPLACE_WITH_OBSERVED_POD"
| sort @timestamp desc
| limit 100
```
#### 로그 형식에 정의된 경우에만 사용하는 애플리케이션 응답 시간 필드
```text
fields @timestamp, @message
| filter kubernetes.namespace_name = "diagnostics-example"
| parse @message /response_time=(?\d+)ms/
| filter ispresent(response_time)
| stats avg(response_time) as avg_response_ms, max(response_time) as max_response_ms by bin(5m)
```
#### 다른 지표와 함께 확인해야 하는 OOM 관련 로그 메시지
```text
fields @timestamp, @message
| filter @message like /OOMKilled|Out of memory|oom-kill/
| sort @timestamp desc
| limit 50
```
### PrometheusRule 선택·알림
Release label을 대상 Prometheus ruleSelector에 맞는 값으로 바꾸고 ruleNamespaceSelector를 확인합니다. CRD 접수만으로 rule loading·알림 전송을 증명하지 못합니다. Threshold·기간은 workload SLO에 맞출 예시이며 alert가 자동 삭제·restart를 승인하지 않습니다.
```yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: reviewed-eks-diagnostics
namespace: monitoring
labels:
release: REPLACE_WITH_SELECTED_PROMETHEUS_RELEASE
spec:
groups:
- name: reviewed-eks-diagnostics
rules:
- alert: NodeNotReady
expr: kube_node_status_condition{condition="Ready",status="true"} == 0
for: 5m
labels:
severity: critical
annotations:
summary: Node {{ $labels.node }} reports Ready=false/unknown; inspect the
node condition and heartbeat.
- alert: NodeMemoryPressure
expr: kube_node_status_condition{condition="MemoryPressure",status="true"} ==
1
for: 5m
labels:
severity: warning
annotations:
summary: Node {{ $labels.node }} reports MemoryPressure.
- alert: NodeDiskPressure
expr: kube_node_status_condition{condition="DiskPressure",status="true"} ==
1
for: 5m
labels:
severity: warning
annotations:
summary: Node {{ $labels.node }} reports DiskPressure.
- alert: PodCrashLooping
expr: max by (namespace,pod,container) (kube_pod_container_status_waiting_reason{namespace="diagnostics-example",reason="CrashLoopBackOff"}
== 1)
for: 5m
labels:
severity: warning
annotations:
summary: '{{ $labels.namespace }}/{{ $labels.pod }}/{{ $labels.container }}
reports CrashLoopBackOff.'
- alert: ActivePodNotReady
expr: '((1 - max by (namespace,pod) (kube_pod_status_ready{namespace="diagnostics-example",condition="true"}))
> 0)
and on (namespace,pod) (max by (namespace,pod) (kube_pod_status_phase{namespace="diagnostics-example",phase=~"Pending|Running|Unknown"}
== 1))
unless on (namespace,pod) kube_pod_deletion_timestamp{namespace="diagnostics-example"}'
for: 15m
labels:
severity: warning
annotations:
summary: Active Pod {{ $labels.namespace }}/{{ $labels.pod }} is not Ready.
- alert: ContainerRecentOOM
expr: '(max by (namespace,pod,container) (increase(kube_pod_container_status_restarts_total{namespace="diagnostics-example"}[15m]))
> 0)
and on (namespace,pod,container) (max by (namespace,pod,container) (kube_pod_container_status_last_terminated_reason{namespace="diagnostics-example",reason="OOMKilled"}
== 1))'
for: 0m
labels:
severity: warning
annotations:
summary: Recent restart and last reported OOM for {{ $labels.namespace }}/{{
$labels.pod }}/{{ $labels.container }}; verify events.
- alert: HighCPUThrottling
expr: '(sum by (namespace,pod,container) (rate(container_cpu_cfs_throttled_periods_total{namespace="diagnostics-example",container!="",container!="POD"}[5m]))
/ on (namespace,pod,container) (sum by (namespace,pod,container) (rate(container_cpu_cfs_periods_total{namespace="diagnostics-example",container!="",container!="POD"}[5m]))
> 0)) > 0.5'
for: 10m
labels:
severity: warning
annotations:
summary: More than 50% of CFS periods were throttled for {{ $labels.namespace
}}/{{ $labels.pod }}/{{ $labels.container }}.
```
```bash
# Read-only: this rule must be selected by the intended Prometheus instance.
kubectl --context "$KUBE_CONTEXT" -n monitoring get prometheus -o json | jq '[
.items[] | {name:.metadata.name,ruleSelector:.spec.ruleSelector,ruleNamespaceSelector:.spec.ruleNamespaceSelector}
]'
kubectl --context "$KUBE_CONTEXT" -n monitoring get prometheusrule reviewed-eks-diagnostics -o yaml
```
### ADOT Collector: 명시적 Pipeline·전제
예시는 검토한 Operator 0.158.0의 v1beta1 object config와 ADOT 0.50.0 component를 사용합니다. Namespace·Operator/CRD·receiver TLS Secret·client CA 신뢰·적절한 ServiceAccount AWS identity를 준비합니다. 아래 Role은 Kubernetes Pod discovery 권한이며 X-Ray·CloudWatch Logs·AMP 권한이 아닙니다. 배포 전 exporter IAM·실제 region/log-group/workspace 입력을 검토합니다. 실행하거나 production 준비 완료라고 주장하지 않습니다.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: adot-diagnostics
namespace: diagnostics-example
```
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: adot-pod-discovery
namespace: diagnostics-example
rules:
- apiGroups:
- ''
resources:
- pods
verbs:
- get
- list
- watch
```
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: adot-pod-discovery
namespace: diagnostics-example
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: adot-pod-discovery
subjects:
- kind: ServiceAccount
name: adot-diagnostics
namespace: diagnostics-example
```
```yaml
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: adot-diagnostics
namespace: diagnostics-example
spec:
mode: deployment
replicas: 1
image: public.ecr.aws/aws-observability/aws-otel-collector:v0.50.0
serviceAccount: adot-diagnostics
env:
- name: AWS_REGION
value: us-west-2
- name: AWS_EC2_METADATA_DISABLED
value: 'true'
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: '1'
memory: 512Mi
volumes:
- name: receiver-tls
secret:
secretName: otel-receiver-tls
volumeMounts:
- name: receiver-tls
mountPath: /etc/otel/tls
readOnly: true
config:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
tls:
cert_file: /etc/otel/tls/tls.crt
key_file: /etc/otel/tls/tls.key
http:
endpoint: 0.0.0.0:4318
tls:
cert_file: /etc/otel/tls/tls.crt
key_file: /etc/otel/tls/tls.key
prometheus:
config:
scrape_configs:
- job_name: owned-pod-metrics
scrape_interval: 30s
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- diagnostics-example
relabel_configs:
- source_labels:
- __meta_kubernetes_pod_annotation_prometheus_io_scrape
action: keep
regex: 'true'
- source_labels:
- __meta_kubernetes_pod_phase
action: keep
regex: Running
- source_labels:
- __meta_kubernetes_pod_container_port_name
action: keep
regex: metrics
- source_labels:
- __meta_kubernetes_pod_container_port_protocol
action: keep
regex: TCP
- source_labels:
- __meta_kubernetes_pod_annotation_prometheus_io_path
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels:
- __meta_kubernetes_namespace
target_label: namespace
- source_labels:
- __meta_kubernetes_pod_name
target_label: pod
- source_labels:
- __meta_kubernetes_pod_container_name
target_label: container
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 75
spike_limit_percentage: 15
batch:
timeout: 30s
send_batch_size: 8192
exporters:
awsxray:
region: us-west-2
local_mode: true
no_verify_ssl: false
index_all_attributes: false
telemetry:
enabled: false
awsemf:
region: us-west-2
namespace: EKS/DiagnosticsExample
log_group_name: /aws/eks/REPLACE_WITH_CLUSTER/otel-metrics
log_stream_name: adot-diagnostics
dimension_rollup_option: NoDimensionRollup
resource_to_telemetry_conversion:
enabled: false
prometheusremotewrite:
endpoint: https://aps-workspaces.us-west-2.amazonaws.com/workspaces/REPLACE_WITH_WORKSPACE_ID/api/v1/remote_write
auth:
authenticator: sigv4auth
resource_to_telemetry_conversion:
enabled: false
extensions:
sigv4auth:
region: us-west-2
service: aps
health_check:
endpoint: 0.0.0.0:13133
service:
extensions:
- sigv4auth
- health_check
pipelines:
traces:
receivers:
- otlp
processors:
- memory_limiter
- batch
exporters:
- awsxray
metrics:
receivers:
- otlp
- prometheus
processors:
- memory_limiter
- batch
exporters:
- awsemf
- prometheusremotewrite
```
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: adot-otlp-ingress
namespace: diagnostics-example
spec:
podSelector:
matchLabels:
app.kubernetes.io/managed-by: opentelemetry-operator
app.kubernetes.io/instance: diagnostics-example.adot-diagnostics
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
telemetry-client: 'true'
ports:
- protocol: TCP
port: 4317
- protocol: TCP
port: 4318
```
OTLP client는 certificate를 신뢰하고 생성 Service·protocol·4317/4318 port를 일치시켜야 합니다. NetworkPolicy enforcement가 필요하며 policy는 Collector Pod만 선택하고 같은 namespace의 label을 가진 client를 허용합니다. 필요한 metrics-target TLS/auth·workload ingress도 설정합니다. Prometheus discovery는 한 namespace에서 annotation으로 선택한 Running Pod의 metrics라는 TCP port만 사용합니다. 실제 endpoint port를 사용해 잘못된 annotation-port rewrite를 피합니다.
예시 replica 하나는 모든 scrape 중복을 피하기 위한 값이며 확장에는 target sharding/allocator 설계가 필요합니다. Memory_limiter를 batch 앞에 두어도 memory/batch 값이 무손실을 보장하지 않습니다. X-Ray는 trace, awsemf는 CloudWatch Logs 경유 metric, AMP는 SigV4 remote write를 받습니다. Custom EKS/DiagnosticsExample 지표가 자동으로 Container Insights schema/dashboard가 되지는 않습니다. 고정 log명은 {ClusterName}이 undefined로 치환되는 문제를 피하지만 resource attribute가 routing에 영향을 줄 수 있어 producer data·IAM을 제한합니다. Cardinality 검토 없이 모든 resource attribute를 metric label로 변환하지 않습니다.
앱 service identity·propagation을 유지합니다. Collector만으로 모든 요청을 instrument하거나 sampling 누락을 해결하지 못합니다. 사용하지 않는 exporter는 pipeline 참조와 함께 제거합니다. 감사에서 AWS telemetry 전송·앱 restart·Collector/Operator 설치는 하지 않았습니다.
[CloudWatch setup](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/install-CloudWatch-Observability-EKS-addon.html) · [Pod metrics](https://github.com/kubernetes/kube-state-metrics/blob/main/docs/metrics/workload/pod-metrics.md) · [Operator API](https://github.com/open-telemetry/opentelemetry-operator/releases/tag/v0.158.0) · [ADOT component versions](https://github.com/aws-observability/aws-otel-collector/blob/v0.50.0/go.mod) · [Prometheus receiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/v0.158.0/receiver/prometheusreceiver) · [EMF exporter](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/v0.158.0/exporter/awsemfexporter)
---
## 8. 장애 감지 아키텍처
### 4계층 감지 Pipeline

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-11-eks-advanced-debugging-3.html)
그림은 개념적 대안이며 완성된 연결 배포가 아닙니다. 수집·저장/조회·alarm 평가·알림마다 설정·identity·network와 전송 근거가 필요합니다. Trace 분석에는 설정한 trace backend도 필요하며 Logs Insights가 모든 trace를 자동 alarm으로 바꾸지 않습니다.
### AWS 기반 Log 수집: 설정 전제
아래는 검토한 Linux node용 Fluent Bit 설정 예시이지 설치된 DaemonSet이 아닙니다. 기존 관리 주체 또는 [완전한 모니터링 설정](https://www.atomai.click/kubernetes-docs/llms/ko/eks/06-eks-monitoring-logging.md)을 사용하고 중복 collector를 설치하지 않습니다. Read-only host log mount, Kubernetes metadata RBAC, AWS identity, 쓰기 가능한 **별도** checkpoint/buffer directory가 필요합니다. AWS_REGION·CLUSTER_NAME·NODE_NAME을 배포/Downward API로 설정합니다. Auto_create_group이 false이므로 대상 log group을 사전 생성·인가합니다.
```text
[SERVICE]
Flush 5
Grace 30
Log_Level info
Daemon off
storage.path /var/fluent-bit/buffer
storage.sync normal
storage.checksum on
storage.max_chunks_up 32
[INPUT]
Name tail
Tag kube.*
Path /var/log/containers/*.log
Exclude_Path /var/log/containers/*_amazon-cloudwatch_*.log
multiline.parser cri
DB /var/fluent-bit/state/containers.db
Mem_Buf_Limit 50MB
Skip_Long_Lines On
Refresh_Interval 10
storage.type filesystem
[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
Kube_Tag_Prefix kube.var.log.containers.
Merge_Log On
Merge_Log_Key log_processed
K8S-Logging.Exclude Off
[OUTPUT]
Name cloudwatch_logs
Match kube.*
region ${AWS_REGION}
log_group_name /aws/eks/${CLUSTER_NAME}/containers
log_stream_name ${NODE_NAME}
auto_create_group false
storage.total_limit_size 100M
```
내장 cri multiline parser는 containerd의 CRI stream/partial-record 형식을 처리하며 Docker JSON parser와 다릅니다. 실제 agent namespace에 맞춰 자기 log 제외를 조정합니다. Log_processed에는 병합한 앱 JSON이 들어가며 아래 metric filter의 전제입니다. Filesystem buffer와 DB checkpoint는 서로 다른 문제를 해결하고 무손실·exactly-once를 보장하지 않습니다. Output queue가 가득 차면 오래된 chunk를 버리고 긴 line 생략·container/node rotation도 손실을 만들 수 있습니다. 크기·retention·disk·IAM/KMS 실패를 감시합니다. Fargate·Auto Mode·Windows는 지원 경로가 다르므로 host mount 예제를 보편적으로 적용하지 않습니다.
### Alertmanager: 실제로 읽히는 설정·Secret File
Prometheus Operator에서는 alertmanager.yaml key가 있는 Secret을 기존 Alertmanager의 spec.configSecret으로 지정합니다. Alertmanager-config라는 ConfigMap만 만들어도 자동으로 읽히지 않습니다. 다음은 검토한 Helm/operator 관리 설정에 통합할 **spec fragment**이며 새로운 완전한 배포가 아닙니다. 별도 Secret은 url/key entry를 제공하고 `/etc/alertmanager/secrets//` 아래 mount되어야 합니다.
```yaml
spec:
configSecret: alertmanager-reviewed
secrets:
- alertmanager-slack
- alertmanager-pagerduty
```
```yaml
global:
resolve_timeout: 5m
slack_api_url_file: /etc/alertmanager/secrets/alertmanager-slack/url
route:
receiver: default
group_by:
- alertname
- cluster
- namespace
- pod
- node
- severity
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers:
- severity="critical"
receiver: critical
- matchers:
- severity="warning"
receiver: warnings
receivers:
- name: default
slack_configs:
- channel: '#alerts-default'
send_resolved: true
- name: critical
slack_configs:
- channel: '#incidents'
send_resolved: true
pagerduty_configs:
- routing_key_file: /etc/alertmanager/secrets/alertmanager-pagerduty/key
severity: critical
- name: warnings
slack_configs:
- channel: '#alerts-warnings'
send_resolved: true
title: '{{ .Status | toUpper }}: {{ .CommonAnnotations.summary }}'
text: '{{ .CommonAnnotations.description }}'
inhibit_rules:
- source_matchers:
- severity="critical"
target_matchers:
- severity="warning"
equal:
- alertname
- cluster
- namespace
- pod
- container
- node
```
이 설정은 critical을 한 receiver에서 Slack·PagerDuty 둘 다로, warning을 Slack으로, 나머지를 default로 보냅니다. Child가 일치한 뒤 continue:true가 parent/default receiver도 호출하는 것은 아닙니다. 현재 matchers·source_matchers·target_matchers를 사용합니다. Inhibition의 equal에는 자원 identity를 포함하며 없는 label은 빈 값으로 비교되므로 다른 Pod/node를 억제하지 않도록 실제 label 계약을 확인합니다. Loaded config·route·transport를 따로 검증합니다. Parser·합성 route 통과는 Slack/PagerDuty 수신 증명이 아니며 email/SMS는 추가 연결이 필요합니다.
### CloudWatch Threshold·Anomaly·Composite Alarm
실제 metric dimension·단위·statistic을 사용합니다. List-metrics filter보다 많은 dimension의 series가 반환될 수 있으므로 게시된 **완전한** dimension 집합 하나를 선택합니다. Container Insights 설정이 필요하며 앞 절의 custom ADOT namespace가 대체하지 않습니다. Node_cpu_utilization은 Pod CPU/request/limit 비율과 다릅니다. 아래 alarm명·topic ARN·cluster 값은 예시이며 기존 이름의 설정을 교체할 수 있는 명령 전에 확인합니다. Topic·접근/KMS policy·recipient는 별도 준비합니다.
```bash
# Read-only: select an actual published metric and its complete dimension set.
: "${AWS_REGION:?}"; : "${CLUSTER_NAME:?}"
aws cloudwatch list-metrics --region "$AWS_REGION" --namespace ContainerInsights \
--metric-name node_cpu_utilization --dimensions "Name=ClusterName,Value=$CLUSTER_NAME" \
--query 'Metrics[].{Name:MetricName,Namespace:Namespace,Dimensions:Dimensions}'
```
```bash
# MUTATION: creates/replaces this named alarm and can trigger configured notifications.
: "${AWS_REGION:?}"; : "${SNS_TOPIC_ARN:?Use the owned topic ARN}"
: "${METRIC_DIMENSIONS_FILE:?JSON array containing one reviewed complete dimension set}"
aws cloudwatch put-metric-alarm --region "$AWS_REGION" \
--alarm-name owned-eks-high-cpu --alarm-description "Example node CPU utilization threshold" \
--namespace ContainerInsights --metric-name node_cpu_utilization \
--dimensions "file://$METRIC_DIMENSIONS_FILE" --statistic Average \
--period 300 --evaluation-periods 3 --datapoints-to-alarm 3 \
--threshold 80 --comparison-operator GreaterThanThreshold \
--treat-missing-data missing --alarm-actions "$SNS_TOPIC_ARN"
```
예시는 300초 period 세 개와 breach datapoint 세 개, 즉 15분 평가 window이며 2분 감지 보장이 아닙니다. TreatMissingData=missing은 data 부재 상태를 보존합니다. 다른 정책은 지표 의미에 맞을 때만 선택합니다. 생성 성공만 믿지 말고 초기 INSUFFICIENT_DATA·상태 전환을 확인합니다.
Anomaly 예시는 API의 metric/band 구조를 따릅니다. M1은 관측 series, ad1은 ThresholdMetricId가 선택하는 band이며 model·period·statistic·dimension이 일치해야 합니다. 적절한 data·학습이 필요하고 장애 예측 보장이 아닙니다. JSON의 예시 dimension을 검토한 dimensions file과 일치하도록 교체한 뒤 사용합니다.
```json
{
"AlarmName": "owned-eks-anomaly-cpu",
"AlarmDescription": "Example anomaly model for an observed Container Insights metric",
"Metrics": [
{
"Id": "m1",
"ReturnData": true,
"MetricStat": {
"Metric": {
"Namespace": "ContainerInsights",
"MetricName": "node_cpu_utilization",
"Dimensions": [
{
"Name": "ClusterName",
"Value": "REPLACE_WITH_CLUSTER"
}
]
},
"Period": 300,
"Stat": "Average"
}
},
{
"Id": "ad1",
"Expression": "ANOMALY_DETECTION_BAND(m1, 2)"
}
],
"EvaluationPeriods": 3,
"ThresholdMetricId": "ad1",
"ComparisonOperator": "LessThanLowerOrGreaterThanUpperThreshold",
"TreatMissingData": "missing",
"AlarmActions": [
"arn:aws:sns:us-west-2:123456789012:owned-eks-alerts"
]
}
```
```bash
# MUTATIONS: same observed metric/statistic/dimensions as the reviewed model.
aws cloudwatch put-anomaly-detector --region "$AWS_REGION" \
--namespace ContainerInsights --metric-name node_cpu_utilization --stat Average \
--dimensions "file://$METRIC_DIMENSIONS_FILE"
# Replace the example cluster/topic/metric dimensions in the JSON before this request.
aws cloudwatch put-metric-alarm --region "$AWS_REGION" --cli-input-json file://anomaly-alarm-reviewed.json
```
```bash
# Read-only prerequisites: both named alarms must exist and have understood state.
aws cloudwatch describe-alarms --region "$AWS_REGION" \
--alarm-names owned-eks-high-cpu owned-eks-high-memory
# MUTATION: the AND policy requires both alarms to be ALARM.
aws cloudwatch put-composite-alarm --region "$AWS_REGION" \
--alarm-name owned-eks-combined-resource \
--alarm-rule 'ALARM("owned-eks-high-cpu") AND ALARM("owned-eks-high-memory")' \
--alarm-actions "$SNS_TOPIC_ARN"
```
Composite 예시는 참조한 두 alarm이 해당 계정·region에 존재하고 둘 다 ALARM일 때만 동작하는 정책입니다. AND·OR는 다른 장애 정책이며 같은 복원력 보장이 아닙니다. PutMetricAlarm 문서에 따라 anomaly-model alarm에는 Auto Scaling action을 둘 수 없습니다.
### Log 기반 Metric
다음 pattern은 Fluent Bit이 병합한 JSON의 log_processed.level을 전제합니다. 실제 record와 metric filter 지원 log-group class에 맞추며 여러 ellipsis가 있는 space-delimited pattern을 복사하지 않습니다. Filter는 생성 이후 일치한 log event를 세며 과거 요청·고유 오류 수가 아닙니다. Event가 들어오지 않을 때 defaultValue=0만으로 전송을 증명하지 못합니다.
```bash
# MUTATION: structured JSON must actually contain log_processed.level.
aws logs put-metric-filter --region "$AWS_REGION" \
--log-group-name "/aws/eks/$CLUSTER_NAME/containers" \
--filter-name OwnedApplicationErrors \
--filter-pattern '{ $.log_processed.level = "ERROR" }' \
--metric-transformations "metricName=ApplicationErrors,metricNamespace=EKS/$CLUSTER_NAME/Application,metricValue=1,defaultValue=0,unit=Count"
```
### 성숙도 목표·자동화 경계
기존 MTTD 30/15/5/2분은 검증하지 않은 계획 목표로 보존합니다. 여기 설정이 그 결과를 증명하지는 않습니다. Incident마다 발생·감지·복원 timestamp를 같은 기준으로 측정합니다. ML/anomaly detection만으로 예측 정확도·복구 권한이 생기지 않습니다.
| 단계 | 기존 MTTD 목표 예시 | 확인할 역량 |
| --- | --- | --- |
| 기본 | 30분 | 기본 metrics와 수동 log 조사 |
| 반응형 | 15분 | 조정된 임계값, log 기반 metrics와 대시보드 |
| 선제형 | 5분 | 연관 분석한 alarm과 검토된 runbook |
| 예측형 설계 목표 | 2분 | 검증된 예측, 범위를 제한한 자동화와 통제된 훈련 |
### EventBridge → Lambda 진단 접수
정확한 계정·region·alarm rule과 명시적 target 호출 권한을 사용합니다. 예시는 event를 분류하고 작은 진단 요청을 log로 남기며 Kubernetes/AWS 변경 client가 없습니다. 집계 alarm에서 Pod명·namespace·UID를 신뢰성 있게 얻을 수 없으므로 추정 Pod 삭제를 기본 CrashLoopBackOff 해결로 삼지 않습니다.
```json
{
"source": [
"aws.cloudwatch"
],
"detail-type": [
"CloudWatch Alarm State Change"
],
"account": [
"123456789012"
],
"region": [
"us-west-2"
],
"resources": [
"arn:aws:cloudwatch:us-west-2:123456789012:alarm:owned-eks-pod-crashlooping"
],
"detail": {
"alarmName": [
"owned-eks-pod-crashlooping"
],
"state": {
"value": [
"ALARM"
]
}
}
}
```
```python
"""EventBridge alarm intake example: classification/logging only, no AWS or Kubernetes client."""
import datetime
import json
import os
def classify_alarm(event, expected_alarm_arn, now):
parts=expected_alarm_arn.split(':',5)
if len(parts)!=6 or parts[2]!='cloudwatch' or not parts[5].startswith('alarm:'):
raise ValueError('Configure one exact CloudWatch alarm ARN')
if not isinstance(event,dict):
return {'status':'ignored','reason':'invalid event'}
detail=event.get('detail')
state=detail.get('state') if isinstance(detail,dict) else None
resources=event.get('resources')
if (event.get('source')!='aws.cloudwatch'
or event.get('detail-type')!='CloudWatch Alarm State Change'
or event.get('account')!=parts[4] or event.get('region')!=parts[3]
or not isinstance(resources,list) or expected_alarm_arn not in resources
or not isinstance(state,dict) or state.get('value')!='ALARM'
or detail.get('alarmName')!=parts[5][len('alarm:'):]):
return {'status':'ignored','reason':'outside configured alarm/state'}
event_id=event.get('id')
if not isinstance(event_id,str) or not 1<=len(event_id)<=128:
return {'status':'ignored','reason':'missing or invalid event ID'}
try:
changed=datetime.datetime.fromisoformat(state['timestamp'].replace('Z','+00:00'))
if changed.tzinfo is None or now.tzinfo is None:
raise ValueError('Timezone required')
age=(now-changed).total_seconds()
except (KeyError,TypeError,ValueError,AttributeError):
return {'status':'ignored','reason':'invalid timestamp'}
if age < -300 or age > 3600:
return {'status':'ignored','reason':'outside example event-age window'}
return {'status':'diagnostic_request','event_id':event_id,
'alarm_arn':expected_alarm_arn,'state_changed_at':changed.isoformat(),
'action':'inspect evidence and select a reviewed runbook'}
def lambda_handler(event,context):
result=classify_alarm(event,os.environ['EXPECTED_ALARM_ARN'],
datetime.datetime.now(datetime.timezone.utc))
print(json.dumps(result))
return result
```
EXPECTED_ALARM_ARN에는 정확한 소유 alarm을 지정합니다. 한 시간 age window·미래 5분 허용은 정책 예시입니다. 실제 Lambda invoker를 제한하며 event field 확인이 암호학적 발신자 검증은 아닙니다. EventBridge 비동기 호출은 반환 dictionary를 다음 action으로 전달하지 않습니다. 실제 진단 queue/workflow는 별도 연결해야 하며 선택 내용을 log로 남기는 것이 “자동 복구 실행”은 아닙니다.
변경 runbook을 활성화하기 전에 identity/UID 재확인, 영구 event-id 중복 제거, rate limit, 최소 권한, 동시성 제어, workload/data/PDB 확인, rollback·사후 검증을 구현합니다. Retry·중복 event가 반복 삭제를 일으키면 안 됩니다. 이 classifier가 해당 production 변경 제어를 구현했다고 주장하지 않습니다.
| 심각도 예시 | Slack | PagerDuty | 기타 채널 | 변경 실행 정책 |
| --- | --- | --- | --- | --- |
| P1 치명 | 사고 알림 | 즉시 호출 정책 | 연동된 경우 팀장·온콜 이메일 또는 SMS | 검토되고 범위가 제한된 runbook만 실행 |
| P2 높음 | 높은 우선순위 알림 | 15분 후 에스컬레이션 예시 | 연동된 경우 팀 이메일 | 조건부 검토 |
| P3 중간 | 알림 | 선택 사항 | 연동된 경우 팀 이메일 | 기본적으로 자동 변경 없음 |
| P4 낮음 | 낮은 우선순위 알림 | 없음 | 일일 요약 예시 | 자동 변경 없음 |
이는 routing 정책 예시이며 모든 channel의 배포·정시 전송 증명이 아닙니다. 감사에서 alarm·topic·policy·Lambda·cloud 자원을 생성하거나 알림·복구를 실행하지 않았습니다.
[Fluent Bit CRI parsing](https://docs.fluentbit.io/manual/administration/configuring-fluent-bit/multiline-parsing) · [Buffering limits](https://docs.fluentbit.io/manual/administration/buffering-and-storage) · [CloudWatch output](https://docs.fluentbit.io/manual/pipeline/outputs/cloudwatch) · [Alertmanager0.34 configuration](https://github.com/prometheus/alertmanager/blob/v0.34.0/docs/configuration.md) · [PutMetricAlarm examples](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricAlarm.html) · [Metric dimensions](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Container-Insights-metrics-EKS.html)
---
## 9. 빠른 참조
### 증상 해석 후 변경 선택
| 증상 | 근거·해석 |
| --- | --- |
| CrashLoopBackOff | Container 종료·재시작 반복의 backoff; exit code·last state·probe·현재/이전 log 확인. RestartAlways에서는 exit0도 반복 가능 |
| ImagePullBackOff | Pull 실패 뒤 retry 지연; 선행 오류·image/tag/digest·platform·pull identity·registry/network/CA/rate limit 확인 |
| ErrImagePull | Pull 시도 실패이며 network 문제나 앱 IRSA 문제로 단정하지 않음 |
| OOMKilled | Runtime reason·memory limit/working set·node 근거 대조; exit137 또는 높은 사용량만으로 leak을 증명하지 못함 |
| CreateContainerConfigError | ConfigMap/Secret/volume·namespace/key 참조 확인; credential 값 dump 금지 |
| Pending: resource | Requests·init/Pod overhead·allocatable·Pod limit·실제 event를 비교한 뒤 provisioning 판단 |
| Pending: placement | Selector/affinity/taint/topology/storage 확인; 의도된 Pending일 수 있음 |
| ContainerCreating | Pod phase가 아닌 container reason/표시; runtime·image·CNI·volume 설정 확인 |
| RunContainerError | 실제 runtime/security/command 오류 확인; 보편적인 restart 해결은 없음 |
| postStart hook 실패 | Handler·앱 초기화 확인. ENTRYPOINT보다 먼저 끝나는 순서 보장이 없고 일반 timeoutSeconds field도 없음 |
| preStop hook 실패 | Hook 시간을 포함한 grace period·handler 오류 확인; 강제 종료는 정상 draining이 아님 |
| FailedScheduling | PVC/affinity/resource 등 전체 message 확인; CPU 부족으로 단정하지 않음 |
| FailedMount | CSI owner·claim·attachment/topology·권한·backend/network 확인; finalizer 일괄 제거 금지 |
| NetworkNotReady | 실제 node networking 구현·readiness 확인; 표준 aws-node 명령은 mode별 적용 |
| NodeNotReady | Ready=False와 heartbeat 부재 Unknown을 구분하고 node/EC2/runtime/network 근거 확인 |
| Evicted | Node pressure·ephemeral storage·Pod reason 확인; limit 증가·log 삭제가 자동 해결은 아님 |
| BackOff | Retry 동작이며 조사 대상은 선행 실패 원인 |
| InvalidImageName | Image reference 문법 확인; 정상 registry 해석 이전 실패 |
### 범위를 지정한 조회 명령
```bash
# Use the account/context guard and the actual namespace/Pod/container from triage.
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${POD_NAME:?}"; : "${CONTAINER_NAME:?}"
kubectl --context "$KUBE_CONTEXT" get nodes -o wide
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pods -o wide
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get events --field-selector type=Warning
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" top pods --containers
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" logs "$POD_NAME" -c "$CONTAINER_NAME" --since=15m --tail=100
# Run separately: previous-container logs may not exist.
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" logs "$POD_NAME" -c "$CONTAINER_NAME" --previous --tail=100
```
```bash
# Authorized reads; print names/keys only, not configuration or Secret values.
: "${CONFIGMAP_NAME:?Set one relevant ConfigMap}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get configmap "$CONFIGMAP_NAME" -o json | jq '{
name:.metadata.name,textKeys:(.data // {} | keys),binaryKeys:(.binaryData // {} | keys)
}'
: "${SECRET_NAME:?Set one Secret whose read permission is explicitly authorized}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get secret "$SECRET_NAME" -o json | jq '{
name:.metadata.name,type:.type,keyNames:(.data // {} | keys)
}'
```
Secret 명령은 표시만 줄이며 API에서 Secret을 요청하므로 Secret-read 권한이 필요합니다. 인가 경계가 아닙니다. Event timestamp는 집계된 발생을 나타낼 수 있고 top은 과거 전체·순간의 진실이 아닌 최근 sample입니다. Endpoint는 앞의 EndpointSlice 절차로 확인합니다.
Pod 생성/debug/exec·rollout undo/restart·scale·drain/repair·delete는 별도로 검토한 운영 단계에 둡니다. Force delete는 API 객체를 지우지만 이전 process 종료를 증명하지 않아 중복 writer를 만들 수 있습니다. 이를 기본 triage에 넣거나 data/eviction 보호를 우회하지 않습니다.
### 도구 선택
| Tool | Use and boundary |
| --- | --- |
| netshoot | Reviewed diagnostic image/toolbox; creating a Pod and sending traffic require the scoped workflow above |
| eks-node-viewer | Scheduled Pod requests versus node allocatable capacity, not actual Pod CPU/memory usage; review release and cluster/AWS access |
| crictl | CRI state/log inspection on an authorized compatible host and configured runtime endpoint |
| kubeconform | Pin the Kubernetes schema version and provide CRD schemas; missing/skipped schemas are not successful validation |
| stern | Multi-Pod log inspection with explicit context/namespace/selectors and bounded output |
| k9s | Interactive TUI; use documented readonly mode and appropriate RBAC when inspection is intended |
| kubectx/kubens | Change local default context/namespace; explicit context flags are safer for shared diagnostic procedures |
장애 전에 검토한 release를 준비하고 unpinned go install @latest를 복구 절차로 실행하지 않습니다. Kubeconform은 로컬·오프라인 schema를 지원하지만 admission webhook·runtime을 실행해 검증하지는 않습니다. Server-side dry-run은 대상 API에 접속하며 offline check와 다릅니다. K9s는 --readonly를 지원하지만 기본이 read-only는 아닙니다. 감사에서 도구 benchmark·interactive session을 실행하지 않았습니다.
### 소유 Support Case용 EKS Log Collector
공식 EKS log-collector 경로는 이번 검토에서 접근 가능했습니다. Repository가 옮겨졌다고 추정해 바꾸지 않습니다. Host/system/runtime/network 정보를 수집하고 archive를 쓰므로 호환·인가된 node에서 현재 공식 절차와 검토한 revision을 사용합니다. Auto Mode는 NodeDiagnostic·문서화된 debug-container 경로를 사용하며 일반 직접 SSH가 아닙니다.
```bash
# Download only; do not automatically execute a newly downloaded host script.
set -euo pipefail
: "${EVIDENCE_PARENT:?Set an existing private evidence directory}"
: "${COLLECTOR_REF:?Set a reviewed full 40-character commit SHA from the official repository}"
[[ "$COLLECTOR_REF" =~ ^[0-9a-fA-F]{40}$ ]]
test -d "$EVIDENCE_PARENT"
umask 077
COLLECTOR_DIR=$(mktemp -d "$EVIDENCE_PARENT/eks-support.XXXXXXXX")
curl --fail --location --silent --show-error --connect-timeout 5 --max-time 30 \
"https://raw.githubusercontent.com/awslabs/amazon-eks-ami/$COLLECTOR_REF/log-collector-script/linux/eks-log-collector.sh" \
--output "$COLLECTOR_DIR/eks-log-collector.sh"
sha256sum "$COLLECTOR_DIR/eks-log-collector.sh"
```
Digest는 별도로 검토한 artifact 기록과 비교합니다. Sha256sum 출력만으로 인증된 것은 아닙니다. Collector 실행은 disk/CPU·민감 정보 영향을 고려한 별도 인가 host 작업입니다. Archive를 검사·삭제 처리한 뒤 소유 AWS Support case에 수동 첨부합니다. Kubeconfig/key·앱 log·endpoint·env 정보가 공개 공유에 안전하다고 가정하지 않습니다. 감사에서 support archive를 생성·업로드하지 않았습니다.
[Kubeconform](https://github.com/yannh/kubeconform) · [K9s](https://github.com/derailed/k9s) · [eks-node-viewer](https://github.com/awslabs/eks-node-viewer) · [EKS troubleshooting](https://docs.aws.amazon.com/eks/latest/userguide/troubleshooting.html)
---
## 10. 다음 단계
### 퀴즈
이 문서에서 다룬 내용을 테스트하려면 [EKS 고급 디버깅 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/11-eks-advanced-debugging-quiz)를 풀어보세요.
### 다음 문서
다음 version 계획 주제는 [Kubernetes version roadmap](https://www.atomai.click/kubernetes-docs/llms/ko/eks/12-kubernetes-version-roadmap.md)에서 확인합니다.
EKS 클러스터를 온프레미스 환경과 통합하는 방법을 알아보려면 [EKS Hybrid Nodes](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md)를 참조하세요.
### 추가 학습 자료
- [AWS EKS 공식 문서 - 문제 해결](https://docs.aws.amazon.com/eks/latest/userguide/troubleshooting.html)
- [Kubernetes 공식 문서 - 디버깅](https://kubernetes.io/docs/tasks/debug/)
- [Amazon EKS Best Practices Guide](https://docs.aws.amazon.com/eks/latest/best-practices/introduction.html)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks/12-kubernetes-version-roadmap
----------------------------------------
# Kubernetes 버전별 신규 기능과 로드맵
> **기능 이력 범위**: Kubernetes 1.29~1.36; 현재 EKS 지원 범위는 별도 표 참고
> **마지막 업데이트**: 2026년 9월 12일
Kubernetes는 연 3회 릴리스 주기를 통해 빠르게 진화하고 있으며, 각 버전마다 중요한 기능이 추가되거나 졸업(GA)합니다. 기업 환경에서 EKS 클러스터를 운영하는 팀에게 버전별 변경 사항을 체계적으로 파악하는 것은 안정적인 업그레이드 계획 수립과 새로운 기능의 적시 채택을 위해 필수적입니다.
이 문서에서는 Kubernetes 1.29부터 1.36까지의 주요 기능, 졸업 타임라인, Deprecation 정책, Amazon EKS의 버전 지원 체계, 그리고 향후 로드맵을 종합적으로 다룹니다.
## 목차
1. [개요 및 학습 목표](#1-개요-및-학습-목표)
2. [Kubernetes 릴리스 사이클](#2-kubernetes-릴리스-사이클)
3. [EKS 버전 지원 매트릭스](#3-eks-버전-지원-매트릭스)
4. [버전별 주요 기능 가이드](#4-버전별-주요-기능-가이드)
5. [주요 기능 졸업 타임라인](#5-주요-기능-졸업-타임라인)
6. [Deprecation 및 제거 사항](#6-deprecation-및-제거-사항)
7. [EKS 특화 고려사항](#7-eks-특화-고려사항)
8. [버전 업그레이드 계획](#8-버전-업그레이드-계획)
9. [향후 전망](#9-향후-전망)
10. [참고 자료](#10-참고-자료)
---
## 1. 개요 및 학습 목표
### 이 문서의 목적
Kubernetes 생태계는 빠르게 변화하고 있으며, 매 릴리스마다 수십 개의 Enhancement가 포함됩니다. 기업 운영 환경에서는 다음과 같은 질문에 대한 명확한 답이 필요합니다.
- 현재 사용 중인 버전에서 어떤 기능이 GA(Generally Available)인가?
- 다음 업그레이드 시 활용할 수 있는 새로운 기능은 무엇인가?
- 어떤 API나 기능이 Deprecated/Removed 되었는가?
- EKS에서 해당 버전과 기능을 언제부터 사용할 수 있는가?
- 장기적으로 어떤 방향으로 발전하고 있는가?
### 학습 목표
이 문서를 통해 다음을 이해할 수 있습니다.
| 목표 | 설명 |
|------|------|
| 릴리스 사이클 이해 | Kubernetes의 연 3회 릴리스 주기와 alpha/beta/GA 성숙도 모델 |
| 버전별 핵심 기능 파악 | 1.29~1.36 각 버전의 주요 Enhancement와 그 실무 영향 |
| 졸업 타임라인 추적 | 핵심 기능의 alpha → beta → GA 진행 경로 |
| EKS 지원 매트릭스 | Standard/Extended Support 체계와 비용 구조 |
| Deprecation 대응 | 제거 예정 기능에 대한 선제적 마이그레이션 계획 |
| 업그레이드 전략 수립 | Feature Gate 테스트, 호환성 검증, 롤백 계획 |
### 대상 독자
- EKS 클러스터를 운영하는 플랫폼 엔지니어링 팀
- Kubernetes 업그레이드 계획을 수립하는 인프라 아키텍트
- 새로운 기능 채택 시점을 결정하는 DevOps 리드
- 버전 지원 정책을 관리하는 운영팀

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-12-kubernetes-version-roadmap-0.html)
---
## 2. Kubernetes 릴리스 사이클
### 릴리스 주기와 단계
Kubernetes는 보통 약 4개월 간격으로 연 3회 **마이너** 버전을 릴리스합니다. Patch release는 별도로 보통 월 단위로 진행됩니다. Upstream patch branch는 약 14개월 동안 지원되며, 일반 유지보수 약 12개월과 CVE·중대한 오류를 위한 maintenance 약 2개월로 나뉩니다. EKS 출시일부터 계산하는 EKS의 14개월 standard support와는 별개의 기간입니다.
Release team은 enhancement 포함, code freeze, 안정화, release candidate의 일정을 공지합니다. 기존 그림의 “Week 15”는 개략적인 주기이며 고정 일정이나 모든 릴리스에 공통인 주차표가 아닙니다. 대상 릴리스의 일정과 예외 승인 절차를 따릅니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-12-kubernetes-version-roadmap-1.html)
### 기능 성숙도·API 안정성·Feature Gate
| 단계 | 해석 |
|---|---|
| Alpha | 보통 기본 비활성화이며 동작·API가 바뀌거나 제거될 수 있습니다. 실제 gate와 전제 조건을 확인합니다. |
| Beta | 검증 범위가 넓어지지만 기본값·호환성은 기능과 버전에 따릅니다. 비활성화 상태로 남는 beta gate도 있습니다. |
| Stable / GA | API 안정성 정책을 적용합니다. 특정 workload·driver·OS·배포의 안전성을 인증하는 것은 아닙니다. |
1.24부터 **새 beta API**는 기본 비활성화되지만, 기존에 활성화된 beta API와 그 새 버전은 다르게 취급합니다. API serving 설정과 feature gate는 관련되어 있지만 같은 개념은 아닙니다. 모든 beta 기능에 opt-in이 필요하거나 stable 기능은 workload 설정 없이 사용할 수 있다고 가정하지 않습니다.
GA API version은 같은 Kubernetes major version 안에서 제거할 수 없습니다. Feature gate 제거 규칙은 별개이며 beta→GA gate의 최소 유예 기간은 6개월 또는 2회 릴리스 중 더 긴 쪽입니다. 실제 제거 버전은 따로 확인해야 합니다. 잠기거나 제거된 gate를 지원되는 비활성화 수단으로 보지 않습니다. 예를 들어 출시된 1.36.2 소스에도 잠긴 `SidecarContainers` gate가 남아 있으므로 1.33 GA가 1.35 제거를 증명하지는 않습니다.
다음은 **과거 버전의 설정 조각**으로, 완전한 KubeletConfiguration이나 EKS 컨트롤 플레인 변경이 아닙니다. 현재 노드에는 해당 버전이 지원하는 설정을 사용하며 제거된 gate를 새 bootstrap 파일에 복사하지 않습니다.
```yaml
# Historical fragment for a self-managed Kubernetes 1.33 test node.
# Merge through the supported node bootstrap/configuration mechanism.
featureGates:
InPlacePodVerticalScaling: true
UserNamespacesSupport: true
```
EKS 컨트롤 플레인 설정은 AWS가 관리하므로 고객이 kube-apiserver static Pod를 편집하거나 임의 server flag를 넣을 수 없습니다. EKS version FAQ는 alpha 기능을 지원하지 않는다고 명시합니다. Self-managed node의 gate를 바꿔도 제공되지 않는 컨트롤 플레인 API가 활성화되지는 않습니다. AWS의 기능별 안내와 node runtime·OS 조건을 확인합니다.
Node `configz`는 선택한 kubelet의 설정을 보여 주며 생략된 기본값이나 컨트롤 플레인의 동작을 입증하지 않습니다. `/metrics`에는 적절한 non-resource URL 인가가 필요하고 feature metric이 제공되지 않거나 추가 label을 가질 수 있습니다. 출력 부재·접근 오류를 “비활성화”로 해석하지 않습니다.
```bash
# Authorized, read-only diagnostics; these endpoints may be restricted.
: "${KUBE_CONTEXT:?}"; : "${NODE_NAME:?Choose the actual node}"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s \
get --raw="/api/v1/nodes/$NODE_NAME/proxy/configz" | jq '.kubeletconfig.featureGates'
```
```bash
# Run separately; absence of a metric is not proof that a feature is disabled.
: "${KUBE_CONTEXT:?}"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get --raw='/metrics' \
| awk '/^kubernetes_feature_enabled/ { print }'
```
### SIG와 Enhancement Proposal
SIG는 관련 영역을 담당합니다. Node는 runtime·lifecycle, Auth는 인증·인가, Network는 Service routing, Storage는 CSI·volume을 맡으며 Scheduling·Apps·API Machinery·Instrumentation·Autoscaling 등의 SIG가 있습니다. 주요 enhancement의 KEP에는 동기·설계·졸업 기준·테스트·production-readiness 검토가 포함됩니다. 계획한 milestone은 출시 확약이 아니므로 실제 릴리스 API와 gate 이력을 확인합니다.
[Upstream patch policy](https://kubernetes.io/releases/patch-releases/) · [Feature gates](https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/) · [Deprecation policy](https://kubernetes.io/docs/reference/deprecation-policy/) · [Kubernetes 1.36.2 gate implementation](https://github.com/kubernetes/kubernetes/blob/v1.36.2/pkg/features/kube_features.go)
---
## 3. EKS 버전 지원 매트릭스
### 지원 기간과 비용 기준
| 구분 | EKS 출시일부터의 기간 | 버전 지원 요금 |
|---|---|---|
| Standard | 첫 14개월 | 클러스터 시간당 $0.10 |
| Extended | 이후 12개월 | 클러스터 시간당 총 $0.60 ($0.10 + $0.50) |
이는 공시된 버전 지원 요금이며 전체 클러스터 운영 비용이 아닙니다. Provisioned Control Plane tier·compute·Auto Mode/Hybrid Nodes·다른 capability·storage·network 비용이 추가될 수 있습니다. 동일 요율로 365일 운영하면 클러스터당 $876과 $5,256이며 차액은 $4,380입니다. 월 730시간 예시에서는 $73과 $438입니다. 실제 청구 측정값이 아닌 산술 예시입니다.
### 확인한 지원 일정 — 2026년 9월 12일 기준 (UTC)
| 버전 | Upstream 출시 | EKS 출시 | Standard 종료 | Extended 종료 | 검토일 상태 |
|---|---|---|---|---|---|
| 1.31 | 2024-08-13 | 2024-09-26 | 2025-11-26 | 2026-11-26 | Extended |
| 1.32 | 2024-12-11 | 2025-01-23 | 2026-03-23 | 2027-03-23 | Extended |
| 1.33 | 2025-04-23 | 2025-05-29 | 2026-07-29 | 2027-07-29 | Extended |
| 1.34 | 2025-08-27 | 2025-10-02 | 2026-12-02 | 2027-12-02 | Standard |
| 1.35 | 2025-12-17 | 2026-01-27 | 2027-03-27 | 2028-03-27 | Standard |
| 1.36 | 2026-04-22 | 2026-06-02 | 2027-08-02 | 2028-08-02 | Standard |
현재 AWS 일정은 1.31~1.36을 제공합니다. 본문의 1.29·1.30은 과거 기능 이력이며 지원되는 배포 대상이 아닙니다. Upstream 1.37 출시만으로 EKS 지원을 추론하지 않습니다. Extended 요금은 표의 standard 종료일 UTC 0시부터 적용됩니다. 변경 전에 실제 일정·API를 다시 확인하며 AWS가 월 단위로만 공지한 향후 날짜는 추정입니다.
일정상 EKS 1.35 출시는 **2026년 1월 27일**, 1.36은 **2026년 6월 2일**입니다. EKS Distro 발표일은 별개의 출시 이벤트이므로 기존의 1월 28일 통합 표기로 EKS 일정을 대신하지 않습니다. 기능별 runtime·admission 조건은 아래 해당 버전 섹션을 참고합니다. EKS 버전 롤백과 컨트롤 플레인 scaling·SLA는 [EKS 업그레이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/08-eks-upgrades.md)에서 다룹니다.
```bash
# Read-only when executed with your normal authorized AWS identity.
: "${AWS_REGION:?Choose the intended Region}"
aws eks describe-cluster-versions --region "$AWS_REGION" --no-cli-pager \
--query clusterVersions --output json
```
첫 배열 원소를 최신 버전으로 가정하거나 과거 예시의 status를 재사용하지 않고 서비스가 반환하는 버전 기록을 조회합니다. 감사에서는 AWS query를 실행하지 않았습니다.
### Upgrade policy와 자동 업그레이드
기본 cluster upgrade policy는 `EXTENDED`입니다. `STANDARD`를 선택하면 standard support 종료 후 자동 업그레이드될 수 있으므로 extended까지 유지할지는 비용·수명주기 관점에서 결정합니다. Extended 종료 후 EKS는 남은 컨트롤 플레인을 지원 버전으로 점진적으로 업그레이드합니다. AWS는 정확한 실행 시점을 약속하지 않으며 해당 자동 업데이트 직전 알림도 제공하지 않는다고 명시합니다. 최소 60일 전 고지는 **standard support 종료일**에 대한 고지이지 extended 종료 후 새 60일 유예나 60/30/7일 순차 알림 보장이 아닙니다.
Managed node group·self-managed node·Fargate Pod·Hybrid Node는 각각의 업데이트·교체 절차가 필요합니다. Auto Mode node는 자동 갱신될 수 있으며 일반적으로 설치한 add-on은 호환성과 소유권을 별도로 검토합니다. 지원 가능한 최대 skew를 목표로 삼지 말고 가능한 한 node와 컨트롤 플레인 버전을 맞춥니다. 컨트롤 플레인 버전 문자열뿐 아니라 실제 update 상태와 workload readiness를 확인합니다. Extended 종료로 자동 업그레이드된 클러스터에는 EKS의 7일 native rollback을 사용할 수 없습니다. 자격 조건과 node-first rollback 순서는 업그레이드 문서를 따릅니다.
[EKS support calendar and FAQ](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html) · [EKS pricing](https://aws.amazon.com/eks/pricing/)
---
## 4. 버전별 주요 기능 가이드
이 섹션은 Kubernetes 1.29부터 1.36까지 각 버전의 핵심 Enhancement를 상세히 다룹니다. 각 기능에 대해 실무적 관점에서의 영향과 활용법을 함께 설명합니다.
### 4.1 Kubernetes 1.29 "Mandala" (2023년 12월)
2023년 12월 13일 릴리스 발표의 수치는 **49개 enhancement: stable 11개, beta 19개, alpha 19개**입니다. 과거 릴리스 통계이며 EKS 1.29가 현재 지원된다는 뜻이 아닙니다. 그림의 기본값·production 표기는 일반화된 설명이므로 위의 기능별 gate 이력과 runtime 조건을 함께 확인합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-12-kubernetes-version-roadmap-6.html)
#### KMS v2 저장 시 암호화 — GA
KMS v2는 secret seed에서 일회용 data encryption key를 파생하고 seed 보호·교체 시 KMS plugin을 사용하여 매 object 쓰기마다 원격 암호화를 요구하지 않도록 envelope encryption 성능을 개선합니다. Envelope encryption의 data-encryption·key-encryption 계층은 v1에도 있으므로 v1을 “단일 계층”으로 설명하면 안 됩니다. 개선이 일정한 latency를 보장하지는 않습니다.
KMS v1은 1.28에서 deprecated, 1.29에서 기본 비활성화되었습니다. 현재 upstream KMS 문서에도 legacy 구현이 설명되어 있으므로 기존의 “1.31에서 제거”는 잘못된 설명입니다. 지원되는 v2 마이그레이션 경로를 우선합니다.
아래는 관리자가 운영하는 upstream API server에 검토한 v2 plugin을 지정한 socket으로 설치한 경우의 설정이며 **EKS 컨트롤 플레인 매니페스트가 아닙니다**. V2는 `cachesize`를 받지 않습니다. 마지막 `identity` provider는 마이그레이션 중 기존 평문을 읽기 위한 것으로, 첫 provider의 쓰기 암호화 실패 시 평문 fallback이 아닙니다. 암호화 마이그레이션을 검토하고 완료를 검증한 뒤 평문 읽기 허용을 제거합니다.
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- kms:
apiVersion: v2
name: reviewed-kms-provider
endpoint: unix:///var/run/kmsplugin/socket.sock
timeout: 3s
- identity: {}
```
**EKS 구분:** 현재 AWS 안내는 EKS 1.28 이상에서 모든 Kubernetes API data에 KMS v2 envelope encryption을 기본 제공하며 customer-managed key를 설정하지 않으면 AWS-owned key를 사용합니다. Secrets·ConfigMaps 같은 API data에 적용되고 node나 EBS volume의 임의 data까지 암호화하는 것은 아닙니다. EKS 1.29부터 시작한다고 추론하거나 위 upstream 설정을 EKS에 적용하지 않습니다.
#### ReadWriteOncePod — GA
`ReadWriteOncePod`는 클러스터 전체에서 PVC를 한 Pod로 제한합니다. `ReadWriteOnce`는 한 node의 여러 Pod가 접근할 수 있습니다. RWOP에는 호환되는 CSI volume·driver가 필요하며 upstream 최소 sidecar는 csi-provisioner 3.0.0, csi-attacher 3.3.0, csi-resizer 1.3.0입니다. 이는 기능 최소 조건이지 현재 권장 release가 아닙니다. 실제 클러스터와 provisioner가 지원하는 버전을 선택합니다.
예시는 기존 `version-lab` namespace와 적합한 `reviewed-csi-class`를 필요로 합니다. Access mode 조정은 privileged host 접근을 막는 kernel 보안 경계나 DB leader election이 아니며 앱 fencing·backup을 대신하지 않습니다.
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: database-pvc
namespace: version-lab
spec:
accessModes:
- ReadWriteOncePod
storageClassName: reviewed-csi-class
resources:
requests:
storage: 100Gi
```
#### 주요 beta·alpha 기능
| 기능 | 1.29 상태 | 의미 |
|---|---|---|
| SidecarContainers | Beta, 기본 활성화 | 재시작 가능한 init container. Alpha는 1.28, GA는 1.33 |
| NFTablesProxyMode | Alpha, 기본 비활성화 | Linux Service proxy backend. Kernel·CNI·NodePort 동작 확인 필요 |
| LoadBalancerIPMode | Alpha | Controller가 보고하는 LoadBalancer ingress status mode이며 임의 Pod 필드가 아님 |
| PodSchedulingReadiness | Beta | Scheduling gate로 scheduler의 검토를 보류 |
| NodeLogQuery | Alpha | 해당 kubelet 설정·접근 권한 필요 |
| KubeletTracing | Beta | 1.29 GA가 아니며 GA는 1.34 |
| MinDomainsInPodTopologySpread | Beta | 1.30에서 GA |
Native sidecar는 아래 **Pod spec 조각**처럼 정의합니다. 예시 image를 검토한 구현으로 바꾸고 실제 log pipeline을 설정해야 합니다. 설치된 Fluent Bit 배포가 아닙니다. Sidecar 시작과, 있을 경우 startup probe 성공 후 다음 시작 단계로 진행합니다. Readiness·정상 종료에는 적절한 probe·앱 동작·충분한 termination budget이 필요합니다.
```yaml
initContainers:
- name: log-helper
image: example.invalid/version-lab/log-helper:reviewed
restartPolicy: Always
```
이 릴리스에서 CSI `NodeExpandSecret`도 GA가 되어 driver의 node-side 확장 요청에 적절한 credential을 전달할 수 있습니다. Deprecated `flowcontrol.apiserver.k8s.io/v1beta2` endpoint는 1.29에서 serving이 중단되었으므로 stable `v1` API와 필드 변경을 검토합니다. `SecurityContextDeny`는 그 전에 deprecated되었고 1.30에서 제거되었으며 1.29에서 새로 deprecated된 것이 아닙니다. 여기서 모든 환경에 공통인 “Service 5,000개” 성능 경계나 proxy benchmark를 측정하지 않았습니다.
[Kubernetes 1.29 release](https://kubernetes.io/blog/2023/12/13/kubernetes-v1-29-release/) · [KMS provider](https://kubernetes.io/docs/tasks/administer-cluster/kms-provider/) · [EKS envelope encryption](https://docs.aws.amazon.com/eks/latest/userguide/envelope-encryption.html) · [Persistent volumes and RWOP](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) · [API migration guide](https://kubernetes.io/docs/reference/using-api/deprecation-guide/)
---
### 4.2 Kubernetes 1.30 "Uwubernetes" (2024년 4월)
4월 17일 릴리스의 수치는 **45개 enhancement: stable 17개, beta 18개, alpha 10개**입니다. 성숙도 표기가 기능별 설정과 runtime 검증을 대신하지는 않습니다.
#### ValidatingAdmissionPolicy — GA
ValidatingAdmissionPolicy는 API server 안에서 CEL을 평가합니다. 여러 validation webhook과 network·인증서·server 의존성을 줄일 수 있지만 잘못된 정책·평가 오류·fail-closed 설정은 여전히 요청을 거부할 수 있습니다. Policy·binding·선택적 parameter object는 역할이 다르며 parameter는 built-in resource나 custom resource가 될 수 있습니다. 필수인 세 번째 CRD 타입이 아닙니다.
아래는 현재 stable `v1` API 예시입니다. Binding은 **Audit-only**이고 `version-lab-policy=enabled` label이 있는 namespace만 선택합니다. 위반 시 audit annotation을 추가하고 거부하지는 않으며, 관찰하려면 audit-log 수집을 구성해야 합니다. 해당 namespace label 설정 권한을 통제합니다. 정상·오류 입력을 먼저 검증하고 강제 적용이 목적이면 의도적으로 `Deny`를 선택합니다. 예시가 production admission 동작의 검증 결과는 아닙니다.
Resource policy는 일반·init container에 CPU/memory limit key가 선언되었는지 확인합니다. 적절한 양수 크기까지 검증하지 않으며 값 0의 존재가 유용한 hard limit는 아닙니다. 용량 조건에는 적합한 LimitRange·resource policy를 사용합니다. Ephemeral container에는 이 limit를 지정할 수 없어 제외합니다. `pods/resize`는 현재 클러스터를 위한 항목으로, 원래 1.30의 VAP GA 이후에 도입된 subresource입니다.
```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: version-lab-resource-limits
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups:
- ''
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- pods
- pods/resize
validations:
- expression: "object.spec.containers.all(c,\n has(c.resources) && has(c.resources.limits)\
\ &&\n has(c.resources.limits.cpu) && has(c.resources.limits.memory)\n) &&\n\
(!has(object.spec.initContainers) || object.spec.initContainers.all(c,\n has(c.resources)\
\ && has(c.resources.limits) &&\n has(c.resources.limits.cpu) && has(c.resources.limits.memory)\n\
))"
message: Regular and init containers must declare CPU and memory limits.
reason: Invalid
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: version-lab-resource-limits
spec:
policyName: version-lab-resource-limits
validationActions:
- Audit
matchResources:
namespaceSelector:
matchLabels:
version-lab-policy: enabled
```
Image policy는 `/` 경계까지 포함한 전체 registry/repository prefix를 사용합니다. 기존 `123456789012.dkr.ecr.` prefix는 유사 도메인도 허용했습니다. 예시 계정·Region·public alias를 승인한 소스로 바꿉니다. Image 참조 검사이지 서명·취약점·digest 불변성 검사는 아닙니다. 선택적인 init/ephemeral 목록에는 존재 확인을 넣고 ephemeral-container subresource도 명시적으로 매칭합니다.
```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: version-lab-image-registries
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups:
- ''
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- pods
- pods/ephemeralcontainers
validations:
- expression: object.spec.containers.all(c, c.image.startsWith('123456789012.dkr.ecr.us-west-2.amazonaws.com/')
|| c.image.startsWith('public.ecr.aws/approved-alias/'))
message: Regular container images must use an approved registry/repository prefix.
- expression: '!has(object.spec.initContainers) || object.spec.initContainers.all(c,
c.image.startsWith(''123456789012.dkr.ecr.us-west-2.amazonaws.com/'') || c.image.startsWith(''public.ecr.aws/approved-alias/''))'
message: Init container images must use an approved registry/repository prefix.
- expression: '!has(object.spec.ephemeralContainers) || object.spec.ephemeralContainers.all(c,
c.image.startsWith(''123456789012.dkr.ecr.us-west-2.amazonaws.com/'') || c.image.startsWith(''public.ecr.aws/approved-alias/''))'
message: Ephemeral container images must use an approved registry/repository prefix.
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: version-lab-image-registries
spec:
policyName: version-lab-image-registries
validationActions:
- Audit
matchResources:
namespaceSelector:
matchLabels:
version-lab-policy: enabled
```
추가 **validation 목록 조각**은 일반 container의 유효 `runAsNonRoot` 상속과 비어 있지 않은 앱 label을 검사합니다. Container 설정이 Pod 설정을 재정의합니다. 별도 policy·binding이 필요하며 모든 Pod Security Standard·init/ephemeral container·image user를 검증하지는 않습니다. `/`가 들어간 map key는 membership으로 확인하며 `has(map["key"])`는 올바른 CEL macro 구문이 아닙니다.
```yaml
- expression: "object.spec.containers.all(c,\n has(c.securityContext) && has(c.securityContext.runAsNonRoot)\n\
\ ? c.securityContext.runAsNonRoot\n : (has(object.spec.securityContext)\
\ &&\n has(object.spec.securityContext.runAsNonRoot) &&\n object.spec.securityContext.runAsNonRoot)\n\
)"
message: Regular containers must effectively set runAsNonRoot.
- expression: 'has(object.metadata.labels) &&
''app.kubernetes.io/name'' in object.metadata.labels &&
''app.kubernetes.io/version'' in object.metadata.labels &&
object.metadata.labels[''app.kubernetes.io/name''] != '''' &&
object.metadata.labels[''app.kubernetes.io/version''] != '''' '
message: Nonempty application name and version labels are required.
```
#### Pod Scheduling Readiness — GA
Scheduling gate는 Pod를 scheduling 검토에서 제외합니다. 생성·admission 시 설정하고 이후 제거할 수 있지만 생성 뒤 새 gate를 추가할 수는 없습니다. Gated Pod만으로 일반적인 unschedulable-Pod 기반 node provisioning이 시작되지는 않으므로 외부 승인·provisioning 절차에서 조건을 충족해야 합니다. Gate만으로 atomic gang scheduling이 구현되지 않습니다.
아래에는 소유 namespace, 예시 image를 대체할 검토된 image, 적절한 GPU 용량·driver가 필요합니다. Gate 이름은 외부 quota 승인·보안 scan을 나타낼 뿐 이름을 붙였다고 Kubernetes가 해당 작업을 실행하지는 않습니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: gated-training
namespace: version-lab
spec:
schedulingGates:
- name: example.com/gpu-quota-approved
- name: example.com/security-scan-passed
containers:
- name: trainer
image: example.invalid/version-lab/training:reviewed
resources:
limits:
nvidia.com/gpu: 4
```
이름으로 지정한 조건을 별도로 검증한 뒤 아래 변경으로 해당 gate만 제거합니다. JSON Patch test가 UID·resourceVersion·선택한 gate 이름을 확인하므로 동시 변경이나 Pod 교체 시 실패합니다. 실패하면 다시 읽고 판단하며 추측한 index를 제거하지 않습니다. 모든 gate가 제거된 뒤에야 scheduling 대상이 되고 일반 placement·용량 제약은 계속 적용됩니다.
```bash
# MUTATION: remove only the named gate after independently verifying its condition.
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${POD_NAME:?}"; : "${GATE_NAME:?}"
gate_patch=$(kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n "$NAMESPACE" \
get pod "$POD_NAME" -o json | jq -ce --arg gate "$GATE_NAME" '
.metadata as $m |
[(.spec.schedulingGates // []) | to_entries[] | select(.value.name == $gate)] as $matches |
if ($matches | length) != 1 then error("Expected exactly one matching gate")
else ($matches[0].key | tostring) as $i | [
{op:"test", path:"/metadata/uid", value:$m.uid},
{op:"test", path:"/metadata/resourceVersion", value:$m.resourceVersion},
{op:"test", path:("/spec/schedulingGates/" + $i + "/name"), value:$gate},
{op:"remove", path:("/spec/schedulingGates/" + $i)}
] end')
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n "$NAMESPACE" \
patch pod "$POD_NAME" --type=json --patch "$gate_patch"
```
#### HPA ContainerResource metric — GA (KEP-2702)
ContainerResource는 지정한 container를 대상으로 하므로 log/proxy sidecar 사용량이 앱 utilization 신호를 왜곡하는 것을 줄일 수 있습니다. `version-lab`에 대상 Deployment가 존재하고 적절한 request를 설정한 `app` container가 있어야 합니다. 정상 resource-metrics provider도 필요합니다. Utilization의 분모는 limit가 아닌 request입니다. Metric이 여러 개면 HPA는 가장 큰 replica 권고를 선택하며 metric 누락·readiness·stabilization이 동작에 영향을 줍니다. Replica 2~50은 측정된 최적값이 아닌 용량 예시입니다.
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app-hpa
namespace: version-lab
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 2
maxReplicas: 50
metrics:
- type: ContainerResource
containerResource:
name: cpu
container: app
target:
type: Utilization
averageUtilization: 70
- type: ContainerResource
containerResource:
name: memory
container: app
target:
type: Utilization
averageUtilization: 80
```
#### 기타 주요 변경
| 기능 | 1.30 상태 |
|---|---|
| MinDomainsInPodTopologySpread | GA |
| StableLoadBalancerNodeSet | GA |
| PodDisruptionConditions | Beta; GA는 1.31 |
| NodeLogQuery | Beta, 기본 비활성화; GA는 1.36 |
| UserNamespacesSupport | Beta, 기본 비활성화 |
| ContextualLogging | Beta; 코드가 contextual logger를 사용해야 하며 모든 메시지에 Pod/node 필드가 자동 추가되지는 않음 |
| RecursiveReadOnlyMounts | Alpha; 적합한 kernel/runtime 지원 필요 |
| RelaxedEnvironmentVariableValidation | Alpha; 값이 아닌 환경변수 **이름**의 허용 범위 변경 |
| ServiceAccountTokenJTI | Beta; 추적용 식별자 제공 |
`SecurityContextDeny`는 1.30에서 제거되었습니다. Pod Security Admission과 환경에 필요한 정책을 검토하며 기능 성숙도를 마이그레이션 검증으로 대신하지 않습니다.
[Kubernetes 1.30 release](https://kubernetes.io/blog/2024/04/17/kubernetes-v1-30-release/) · [ValidatingAdmissionPolicy](https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/) · [Scheduling readiness](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-scheduling-readiness/) · [HPA container metrics](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#container-resource-metrics)
---
### 4.3 Kubernetes 1.31 "Elli" (2024년 8월)
8월 13일 릴리스의 수치는 **45개 enhancement: stable 11개, beta 22개, alpha 12개**입니다.
#### AppArmor native 필드 — GA
AppArmor native 필드는 1.30에 도입되고 1.31에서 GA가 되었으며 기존 container별 beta annotation 방식을 대체합니다. Host에서 AppArmor가 실제 활성화되어 있고 runtime이 지원해야 하며 `Localhost` profile은 배치 가능한 각 node에 로드되어 있어야 합니다. Custom node label은 운영자가 검증한 조건의 표시일 뿐 profile 설치·강제 적용 수단이 아닙니다.
첫 예시는 사전 설치한 profile을 사용하고 두 번째는 기존 Deployment 예시에 빠진 selector·Pod label을 갖춥니다. 예시 image를 교체하고 namespace를 준비합니다. `RuntimeDefault`는 runtime의 profile이고 `Unconfined`는 AppArmor 제약을 해제합니다. 모든 EKS OS·compute 유형이 지정한 profile을 지원하는 것은 아닙니다. AppArmor·seccomp·SELinux는 서로 다른 제어 방식이며 같은 보호의 다른 이름이 아닙니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: apparmor-local-profile
namespace: version-lab
spec:
nodeSelector:
version-lab.example.com/apparmor-profile: reviewed
containers:
- name: app
image: example.invalid/version-lab/app:reviewed
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: reviewed-app-profile
```
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: apparmor-runtime-default
namespace: version-lab
spec:
replicas: 1
selector:
matchLabels:
app: apparmor-runtime-default
template:
metadata:
labels:
app: apparmor-runtime-default
spec:
containers:
- name: app
image: example.invalid/version-lab/app:reviewed
securityContext:
appArmorProfile:
type: RuntimeDefault
```
#### PersistentVolume 마지막 phase 전환 시각 — GA
PV status의 `.status.lastPhaseTransitionTime`은 최근 phase 전환을 기록합니다. Event·backend 근거와 함께 수명주기를 진단하며 완전한 전환 이력이나 누락된 과거 이벤트의 복원으로 보지 않습니다. 아래 명령은 volume을 변경·삭제하지 않습니다.
```bash
# Read-only, for one owned cluster-scoped PV.
: "${KUBE_CONTEXT:?}"; : "${PV_NAME:?}"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get pv "$PV_NAME" -o json | jq '{
name:.metadata.name,phase:.status.phase,lastPhaseTransitionTime:.status.lastPhaseTransitionTime
}'
```
#### DRA structured parameters — 1.31에서는 아직 alpha
DRA 재설계는 structured API·ResourceSlice로 device 정보와 요청을 Kubernetes가 볼 수 있게 하여 scheduler 측 할당을 가능하게 했습니다. **1.31에도 classic DRA가 남아 있었으며**, 별도의 기본 비활성 `DRAControlPlaneController` gate로 제어했습니다. 출시된 1.31 소스에는 이 gate가 있고 1.32 소스에서는 제거됩니다. 따라서 기존 퀴즈의 “1.31에서 classic DRA 제거”는 잘못된 설명입니다.
DRA는 1.31에서 alpha, 1.32에서 beta, core API는 1.34에서 stable이 되었습니다. 기존 `resource.k8s.io/v1beta1` 예시는 1.31 당시 API 세대를 올바르게 표현하지 못했습니다. 현재 구문은 1.34 절의 stable DRA 예시를 사용하고 설치한 driver의 DeviceClass·ResourceSlice·attribute·기능을 확인합니다. Kubernetes API 자체가 GPU driver를 설치하거나 time-slicing/MIG를 구현하지는 않습니다.
#### Service traffic distribution — beta
Core `v1` Service의 `trafficDistribution: PreferClose`는 같은 zone endpoint를 선호하도록 요청합니다. 엄격한 locality 규칙·지리적 거리 계산·cross-AZ 요금 제거 보장이 아닌 routing 선호입니다. Endpoint 가용성·구현 proxy·traffic policy 우선순위가 영향을 줍니다. Selector가 실제 workload Pod와 일치해야 하며 예시가 endpoint를 생성하지는 않습니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: zone-preference
namespace: version-lab
spec:
trafficDistribution: PreferClose
selector:
app: web-app
ports:
- port: 80
targetPort: 8080
```
#### 기타 주요 변경
| 기능 | 1.31 상태 |
|---|---|
| NFTablesProxyMode | Beta, 기본 활성화. Proxy mode 선택과 Linux·kernel·CNI 호환성 확인은 별도 단계 |
| MultiCIDRServiceAllocator | Beta, 기본 비활성화 |
| VolumeAttributesClass | Beta, 기본 비활성화. Driver·controller·API 지원 필요 |
| ImageVolume | Alpha, 기본 비활성화 |
| PodDisruptionConditions | GA |
| JobPodReplacementPolicy | Beta; GA는 1.34 |
| SidecarContainers | 1.29부터 이미 beta이며 1.31에서 새로 beta가 된 것이 아님 |
Nftables backend 지원은 자동 network 마이그레이션이 아닙니다. NodePort·firewall 동작이 iptables와 다를 수 있으므로 production proxy mode 변경 전에 실제 구현을 평가합니다.
[Kubernetes 1.31 release](https://kubernetes.io/blog/2024/08/13/kubernetes-v1-31-release/) · [AppArmor prerequisites](https://kubernetes.io/docs/tutorials/security/apparmor/) · [1.31 feature source](https://github.com/kubernetes/kubernetes/blob/v1.31.0/pkg/features/kube_features.go) · [1.32 feature source](https://github.com/kubernetes/kubernetes/blob/v1.32.0/pkg/features/kube_features.go)
---
### 4.4 Kubernetes 1.32 "Penelope" (2024년 12월)
12월 11일 릴리스의 수치는 **44개 enhancement: stable 13개, beta 12개, alpha 19개**입니다. 그림의 기본 활성화 표기는 단순화한 성숙도 범례이며 아래 beta 기능 중에는 비활성화 상태로 남는 기능도 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-12-kubernetes-version-roadmap-9.html)
#### Structured authorization configuration — GA
Stable 설정은 `apiserver.config.k8s.io/v1`의 `AuthorizationConfiguration`을 사용합니다. 기존 authorization mode flag의 대안이지 `--authorization-mode`가 제거되었다는 뜻이 아닙니다. Flag 방식과 설정 파일 방식을 혼용하지 않습니다. EKS는 이 컨트롤 플레인 설정을 관리하므로 아래 파일은 관리자가 운영하는 API server용이며 kubectl로 적용하는 resource나 EKS 설정 인터페이스가 아닙니다.
Authorizer는 순서대로 평가하고 명시적인 allow/deny가 나오면 chain이 끝납니다. 아래 webhook은 Node·RBAC가 이미 결정하지 않은 `version-lab` resource 요청만 처리하므로 **RBAC가 이미 허용한 요청에 추가 deny filter를 적용하지 않습니다**. CEL match condition은 webhook 호출을 선택하며 CEL 자체가 별도 authorizer 타입은 아닙니다. Request는 SubjectAccessReview spec이므로 namespace는 `request.resourceAttributes` 아래에 있고 non-resource 요청에는 존재 확인이 필요합니다.
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AuthorizationConfiguration
authorizers:
- type: Node
name: node
- type: RBAC
name: rbac
- type: Webhook
name: reviewed-webhook
webhook:
authorizedTTL: 5m
unauthorizedTTL: 30s
timeout: 3s
subjectAccessReviewVersion: v1
matchConditionSubjectAccessReviewVersion: v1
failurePolicy: Deny
connectionInfo:
type: KubeConfigFile
kubeConfigFile: /etc/kubernetes/reviewed-authz-webhook.kubeconfig
matchConditions:
- expression: has(request.resourceAttributes) && request.resourceAttributes.namespace
== 'version-lab'
```
사용 전에 실제 webhook·TLS trust·보호된 kubeconfig를 준비합니다. `failurePolicy: Deny`는 해당 webhook·조건 평가 실패에 적용되며 캐시된 결정은 backend policy 변경 효과를 지연시킬 수 있습니다. 모든 API server에 일관된 설정을 사용합니다. 설정 reload를 지원하지만 Node/RBAC authorizer를 추가·제거할 수는 없으므로 비프로덕션에서 전체 정책과 복구 절차를 검증합니다.
#### StatefulSet PVC retention policy — GA
1.32에서 GA가 된 관련 기능은 **StatefulSet volume claim template으로 생성한 PVC의 자동 삭제·보존 정책**입니다. 모든 미사용 PVC의 보호 finalizer가 즉시 제거된다는 새 보장이 아닙니다. `whenDeleted`는 StatefulSet 삭제, `whenScaled`는 scale-down 동작을 제어하며 각각 `Retain` 또는 `Delete`를 지원합니다. 기본값은 data 보존입니다. 아래는 기존 StatefulSet에서 검토할 설정 조각입니다.
```yaml
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
```
`Delete` 선택은 data 수명주기 변경이며 PV reclaim policy에 따라 PVC 삭제가 backend storage 삭제로 이어질 수 있습니다. Pod ownership·garbage collection·CSI 작업·finalizer가 완료 시점에 영향을 줍니다. PVC 사용 중 보호는 1.32 전부터 있었으므로 멈춘 claim은 consumer·UID·attachment·controller를 조사해야 합니다. Finalizer 일괄 제거 또는 업그레이드만으로 해결된다고 가정하지 않습니다.
#### VolumeAttributesClass — 1.32에서는 아직 beta
VAC는 1.31에서 beta, 1.34에서 GA가 되었습니다. Beta API는 `storage.k8s.io/v1beta1`이며 현재 예시는 cluster·CSI driver가 지원할 때 1.34 절의 stable API를 사용합니다. Class parameter는 불변이고 PVC의 class 참조를 바꿔 EBS IOPS·throughput 같은 driver 지원 속성 변경을 요청합니다.
비동기 storage 변경이며 보편적인 무중단 보장이 아닙니다. Driver·controller 버전, API 제공 여부, IAM/KMS 권한, volume type 제한, 변경 cooldown과 상태를 확인합니다. 불완전한 PVC object를 완전한 생성 manifest처럼 적용하지 않습니다. 아래 조회로 원하는 class와 보고된 진행 상태를 비교합니다.
```bash
# Read-only: inspect one existing owned PVC and the CSI modification state.
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${PVC_NAME:?}"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n "$NAMESPACE" \
get pvc "$PVC_NAME" -o json | jq '{
requestedClass:.spec.volumeAttributesClassName,
currentClass:.status.currentVolumeAttributesClassName,
modification:.status.modifyVolumeStatus,
conditions:.status.conditions
}'
```
AWS의 1.34 안내도 stable VAC API와 이전 beta sidecar 지원을 구분합니다. EKS 컨트롤 플레인 버전만으로 임의 EBS CSI release의 VAC API 호환성이 입증되지는 않습니다.
#### User namespace — 1.32에서는 beta, 기본 비활성화
User namespace는 1.30에서 beta, 1.33에서 기본 활성화되었으며 GA는 1.36입니다. Pod는 `hostUsers: false`로 opt-in합니다. Container의 UID 0은 구현이 선택한 non-root host UID로 매핑되며 모든 환경에 공통인 `65534 + offset` 공식이 아닙니다. 호환 kernel·filesystem·CRI/runtime이 필요하고 모든 workload·host 접근 방식이 호환되는 것은 아닙니다.
아래는 매핑을 설명하기 위해 container UID 0을 의도적으로 사용합니다. 예시 image를 바꾸고 지원되는 테스트 환경에서 사용합니다. 심층 방어이지 모든 kernel·container escape 취약점 방지 보장이나 다른 보안 제어의 대체재가 아닙니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: userns-example
namespace: version-lab
spec:
hostUsers: false
containers:
- name: app
image: example.invalid/version-lab/app:reviewed
securityContext:
runAsUser: 0
```
#### 기타 주요 변경
| 기능 | 1.32 상태 |
|---|---|
| CustomResourceFieldSelectors | GA; CRD 작성자가 지원 selectable field를 선언해야 함 |
| RetryGenerateName | GA; 이름 충돌을 재시도하지만 생성 성공을 보장하지는 않음 |
| SizeMemoryBackedVolumes | GA; memory-backed emptyDir 제한은 Pod·node memory와 함께 고려 |
| ServiceAccountTokenJTI | GA; token 식별자이며 새 인가 권한이 아님 |
| JobManagedBy | Beta; GA는 1.35 |
| DynamicResourceAllocation | Beta, 기본 비활성화; core API stable은 1.34 |
| MultiCIDRServiceAllocator | 아직 beta, 기본 비활성화 |
| NFTablesProxyMode | 아직 beta; GA는 1.33 |
| MutatingAdmissionPolicy | Alpha; beta는 1.34, GA는 1.36 |
`StableLoadBalancerNodeSet`은 1.30에서 이미 GA가 되었습니다. 이 과거 단계와 특정 EKS 클러스터에서 현재 활성화된 기능을 구분합니다.
[Kubernetes 1.32 release](https://kubernetes.io/blog/2024/12/11/kubernetes-v1-32-release/) · [Authorization configuration](https://kubernetes.io/docs/reference/access-authn-authz/authorization/) · [StatefulSet PVC retention](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#persistentvolumeclaim-retention) · [EKS version notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions-standard.html)
---
### 4.5 Kubernetes 1.33 "Octarine" (2025년 4월)
4월 23일 릴리스의 수치는 **64개 enhancement: stable 18개, beta 20개, alpha 24개, deprecated 또는 withdrawn 2개**입니다. 그림은 세 성숙도 그룹의 62개를 전체 64개 대비 비율로 표시하며 나머지 2개는 그려져 있지 않습니다. 2025년의 대형 릴리스이지만 모든 workload의 성능·준비 상태가 더 좋다는 증거는 아닙니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-12-kubernetes-version-roadmap-10.html)
#### Native sidecar — GA
Alpha 1.28 → beta 1.29 → GA 1.33 순서로 진행했습니다. 재시작 가능한 init container에 `restartPolicy: Always`를 지정합니다. Sidecar의 `started`가 true가 되면 kubelet이 다음 init container로 진행합니다. Startup probe가 없으면 프로세스 실행, 있으면 해당 probe 성공을 의미하며 readiness는 별도 신호입니다. 아래 일반 init container는 **두 sidecar가 시작한 뒤** 실행되고, 완료 후 앱이 시작합니다.
이는 구조 예시입니다. 모든 `example.invalid` image를 검토한 구현으로 바꾸고 proxy는 선언한 readiness endpoint를 제공하며 log agent는 실제 pipeline을 설정해야 합니다. Envoy/Istio/Fluent Bit image만 지정한다고 service mesh나 log destination이 구성되지는 않습니다. 실제 DB migration에는 조정·멱등성이 필요하며 replica마다 실행해도 안전하다고 가정하지 않습니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: sidecar-lifecycle
namespace: version-lab
spec:
replicas: 2
selector:
matchLabels:
app: sidecar-lifecycle
template:
metadata:
labels:
app: sidecar-lifecycle
spec:
terminationGracePeriodSeconds: 60
initContainers:
- name: proxy-helper
image: example.invalid/version-lab/reviewed-proxy:reviewed
restartPolicy: Always
startupProbe:
httpGet:
path: /ready
port: 15021
periodSeconds: 2
failureThreshold: 30
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
- name: log-helper
image: example.invalid/version-lab/reviewed-log-agent:reviewed
restartPolicy: Always
volumeMounts:
- name: app-logs
mountPath: /var/log/app
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
- name: initialize-app
image: example.invalid/version-lab/reviewed-init:reviewed
volumeMounts:
- name: app-logs
mountPath: /var/log/app
containers:
- name: app
image: example.invalid/version-lab/reviewed-app:reviewed
volumeMounts:
- name: app-logs
mountPath: /var/log/app
volumes:
- name: app-logs
emptyDir: {}
```
일반적인 graceful termination에서는 main container 이후 sidecar를 역순으로 종료합니다. Pod의 공통 grace-period budget이 적용되므로 main 종료가 오래 걸리면 sidecar의 정상 종료 시간이 거의 남지 않을 수 있습니다. Native sidecar는 main container 완료 후 Job 완료를 막지 않으며 GA에서 처음 생긴 동작도 아닙니다. 용량 산정에는 동시에 실행하는 init·sidecar·app resource와 Pod overhead를 고려합니다. 여기서 수명주기 시간이나 앱 가용성을 측정하지 않았습니다.
#### 컨테이너 리소스 in-place resize — 1.33에서 beta
In-place resize는 Pod를 재생성하지 않고 원하는 CPU/memory 할당을 변경하지만 `resizePolicy`에 따라 container 재시작이 필요할 수 있습니다. 1.35에서 stable이 되었습니다. 아래 현재 schema 예시는 `Burstable` QoS를 유지하고 CPU는 `NotRequired`, memory는 `RestartContainer`로 설정합니다. 따라서 memory 변경은 정책상 container 재시작을 요청하며 모든 memory 변경의 본질적 제약이라는 뜻은 아닙니다.
호환 Linux runtime·node policy, 지원되는 kubectl skew, 소유 namespace와 검토한 image를 사용합니다. 1.36 문서의 일반 지원 범위에는 Windows와 기본 static CPU/Memory-manager 사례가 제외되며 별도 gate 기능은 버전별로 평가합니다. 이 API가 Deployment/StatefulSet template을 자동 변경하거나 HPA/VPA/GitOps의 resource 소유권을 조정하지는 않습니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: resizable-app
namespace: version-lab
spec:
containers:
- name: app
image: example.invalid/version-lab/app:reviewed
resources:
requests:
cpu: 500m
memory: 256Mi
limits:
cpu: '1'
memory: 512Mi
resizePolicy:
- resourceName: cpu
restartPolicy: NotRequired
- resourceName: memory
restartPolicy: RestartContainer
```
용량·소유권을 검토한 뒤 아래 CPU-only 예시로 request 1 core, limit 2 core를 요청합니다. 이름으로 container를 선택하고 기존 Burstable class를 유지하며 CPU 재시작 정책이면 거부하고 Pod UID·resourceVersion을 확인한 뒤 patch합니다. Memory는 변경하지 않습니다. 충돌 시 강제 적용하지 말고 대상을 다시 읽어 판단합니다.
```bash
# MUTATION: reviewed CPU-only resize; desired request=1 core and limit=2 cores.
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${POD_NAME:?}"; : "${CONTAINER_NAME:?}"
resize_patch=$(kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n "$NAMESPACE" \
get pod "$POD_NAME" -o json | jq -ce --arg container "$CONTAINER_NAME" '
. as $pod |
[(.spec.containers | to_entries[]) | select(.value.name == $container)] as $matches |
if .status.phase != "Running" or .metadata.deletionTimestamp != null
or ($matches | length) != 1
then error("Expected one target container in a non-deleting Running Pod")
elif .status.qosClass != "Burstable"
then error("This example preserves an existing Burstable QoS class")
elif $matches[0].value.resources.requests.cpu == null
or $matches[0].value.resources.limits.cpu == null
then error("This example requires existing CPU request and limit keys")
elif any($matches[0].value.resizePolicy[]?; .resourceName == "cpu" and .restartPolicy == "RestartContainer")
then error("This example requires CPU resize policy NotRequired")
elif ([.status.containerStatuses[]? | select(.name == $container and .state.running != null)] | length) != 1
then error("Target container is not reported running")
else ($matches[0].key | tostring) as $i | [
{op:"test",path:"/metadata/uid",value:$pod.metadata.uid},
{op:"test",path:"/metadata/resourceVersion",value:$pod.metadata.resourceVersion},
{op:"test",path:("/spec/containers/" + $i + "/name"),value:$container},
{op:"replace",path:("/spec/containers/" + $i + "/resources/requests/cpu"),value:"1"},
{op:"replace",path:("/spec/containers/" + $i + "/resources/limits/cpu"),value:"2"}
] end')
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n "$NAMESPACE" \
patch pod "$POD_NAME" --subresource=resize --type=json --patch "$resize_patch"
```
이전 `.status.resize` 문자열 대신 현재 status 필드를 사용합니다. `PodResizePending=True`는 `Deferred`·`Infeasible` 등을 보고하고 `PodResizeInProgress=True`는 적용 진행 중을 나타냅니다. 원하는 spec·확인된 generation·대상 container의 `status.containerStatuses[].resources`를 비교합니다. `allocatedResources`는 내부 할당 확인용 필드이며 runtime limit 적용의 단독 증거가 아닙니다. Condition 부재나 patch 수락만으로 workload 정상 여부를 판단하지 않습니다.
```bash
# Read-only observation; an accepted patch is not proof of completed actuation.
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${POD_NAME:?}"; : "${CONTAINER_NAME:?}"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n "$NAMESPACE" \
get pod "$POD_NAME" -o json | jq --arg container "$CONTAINER_NAME" '{
uid:.metadata.uid,generation:.metadata.generation,
observedGeneration:.status.observedGeneration,qosClass:.status.qosClass,
resizeConditions:[.status.conditions[]? | select(.type == "PodResizePending" or .type == "PodResizeInProgress")],
desired:[.spec.containers[] | select(.name == $container) | .resources],
reported:[.status.containerStatuses[]? | select(.name == $container) |
{name,resources,allocatedResources,containerID,restartCount,ready}]
}'
```
Resize로 Pod QoS class를 바꿀 수 없습니다. Guaranteed Pod는 CPU·memory request/limit 동등성을 유지해야 하며 위 예시는 의도적으로 Burstable입니다. `NotRequired` memory 축소는 best effort이고 사용량이 새 limit보다 크면 진행 상태에 머물 수 있으며 race로 OOM kill이 발생할 수도 있습니다. 재시작 불가능한 init·ephemeral container는 resize할 수 없습니다. 성숙도만으로 무중단·latency·resize 성공을 보장하지 않습니다.
#### 현재 VPA 연동은 별도의 버전 결정
다음은 **2026년 companion component 예시**이며 Kubernetes 1.33 출시 당시 VPA 1.7이 있었다는 뜻이 아닙니다. 출시된 VPA **1.7.1** API는 1.7.0에서 alpha로 도입한 `InPlace`를 지원합니다. Admission-controller·updater 양쪽의 VPA `InPlace` gate와 Kubernetes 1.33+ in-place-resize 지원이 필요합니다. VPA의 Pod eviction fallback을 피하지만 resize 완료나 모든 container policy의 무재시작을 보장하지 않습니다. 권고만 관찰하려면 먼저 `Off`를 사용합니다.
아래 CPU-only policy는 기존 Deployment/container의 예시 범위입니다. 변경 활성화 전에 controller 배포 flag와 용량을 검토합니다. `InPlaceOrRecreate`는 재생성으로 fallback할 수 있는 별도 모드이며 VPA 1.6에서 GA, 기존 gate는 1.7에서 제거되었습니다. 제거된 gate를 설정하거나 Kubernetes GA만으로 VPA 동작을 추론하지 않습니다.
```yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: current-in-place-example
namespace: version-lab
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
updatePolicy:
updateMode: InPlace
resourcePolicy:
containerPolicies:
- containerName: app
minAllowed:
cpu: 100m
maxAllowed:
cpu: '4'
controlledResources:
- cpu
controlledValues: RequestsAndLimits
```
#### ServiceCIDR·IPAddress — GA
Upstream Kubernetes는 allocator·API가 활성화되어 있을 때 추가 `networking.k8s.io/v1` ServiceCIDR object로 사용 가능한 Service 주소를 확장할 수 있습니다. 기본 `kubernetes` object는 API server의 초기 범위를 나타냅니다. 추가 전에 IPAM·address family·routing 중복을 검토하며 할당된 Service IP가 고아가 되는 삭제는 finalizer로 보호됩니다. ServiceCIDR는 VPC subnet이나 Pod 주소 CIDR가 아닙니다.
아래 IPv4 manifest는 upstream 예시이며 실행·검증된 EKS 범위 확장이 아닙니다. EKS 생성 parameter `serviceIpv4Cidr`는 생성 후 불변입니다. 추가 Kubernetes ServiceCIDR 생성은 별도 작업이며 이번에 확인한 AWS 자료만으로 검증된 EKS 절차가 확립되지는 않습니다. EKS에 사용하기 전에 provider 지원·admission policy·대상 network를 확인합니다. API discovery만으로 검증을 대신하지 않습니다.
```yaml
apiVersion: networking.k8s.io/v1
kind: ServiceCIDR
metadata:
name: reviewed-extra-service-range
spec:
cidrs:
- 10.200.0.0/16
```
```bash
# Read-only discovery; do not interpret availability alone as an approved EKS change.
set -euo pipefail
: "${KUBE_CONTEXT:?}"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s api-resources --api-group=networking.k8s.io
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get servicecidrs
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get ipaddresses
```
#### Topology-aware routing·traffic distribution — GA
Topology-aware endpoint hint와 Service `trafficDistribution` 선호는 관련되지만 다른 메커니즘입니다. 아래 `PreferClose`는 같은 zone 선호이며 엄격한 same-zone 보장·region 간 거리 계산·기존 annotation의 일괄 deprecated 선언이 아닙니다. Ready endpoint 분포·proxy 구현·`internalTrafficPolicy`/`externalTrafficPolicy`가 경로에 영향을 줍니다.
Cross-AZ traffic에는 요금이 생길 수 있지만 기존의 고정 `$0.01/GB` 설명만으로 전체 비용을 계산할 수는 없습니다. Service·경로·계량되는 inbound/outbound 측에 따라 요금이 달라지고 일부 in-Region traffic에는 예외가 있습니다. 고정 절감 효과를 약속하지 말고 실제 traffic과 청구 data를 비교합니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: same-zone-preference
namespace: version-lab
spec:
trafficDistribution: PreferClose
selector:
app: web-app
ports:
- port: 80
targetPort: 8080
```
#### Job success policy — GA
Success policy는 Indexed Job에 적용합니다. 아래는 index 0 성공을 요구하며 앱이 해당 프로토콜을 구현해야만 이를 leader로 해석할 수 있습니다. Kubernetes가 분산 작업 결과의 완결성·내구성을 추론하지는 않습니다. Failure policy와 나머지 Pod 종료도 고려해야 합니다. 기존 예시에 빠진 Job template의 `restartPolicy: Never`를 명시했습니다.
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: indexed-success-example
namespace: version-lab
spec:
completionMode: Indexed
completions: 8
parallelism: 8
backoffLimit: 2
successPolicy:
rules:
- succeededIndexes: '0'
succeededCount: 1
template:
spec:
restartPolicy: Never
containers:
- name: trainer
image: example.invalid/version-lab/training:reviewed
env:
- name: JOB_COMPLETION_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
```
#### OCI image volume — 1.33에서 beta, 기본 비활성화
Image volume은 모델 data 같은 OCI image 내용을 앱 image에 포함하지 않고 Pod에 제공합니다. 지원 runtime·기능 설정·registry pull identity가 필요합니다. 읽기 전용 mount이며 쓰기 가능한 PVC가 아닙니다. 앱·data image를 모두 검토하고 고정합니다. ImageVolume은 1.35에서 기본 활성화되고 1.36에서 stable이 되었으며 1.34 GA가 아닙니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: image-volume-example
namespace: version-lab
spec:
containers:
- name: inference
image: example.invalid/version-lab/inference:reviewed
volumeMounts:
- name: model
mountPath: /models
readOnly: true
volumes:
- name: model
image:
reference: example.invalid/version-lab/model:reviewed
pullPolicy: IfNotPresent
```
#### 1.33의 기타 주요 단계
| 기능 | 상태 |
|---|---|
| NFTablesProxyMode·RecursiveReadOnlyMounts | GA |
| CRDValidationRatcheting | GA; 변경한 잘못된 필드의 검증을 우회하는 권한은 아님 |
| MatchLabelKeysInPodAffinity·NodeInclusionPolicyInPodTopologySpread | GA |
| PV reclaim-policy 삭제 보호 | GA; PVC 사용 중 보호와는 별개 |
| UserNamespacesSupport | Beta, 이제 기본 활성화 |
| PodLevelResources | 아직 alpha; beta는 1.34 |
| StructuredAuthenticationConfiguration | Beta; GA는 1.34 |
| MutatingAdmissionPolicy | 아직 alpha; beta는 1.34 |
| PodLifecycleSleepAction | Beta; GA는 1.34 |
| JobManagedBy | Beta; GA는 1.35 |
LoadBalancerIPMode·RetryGenerateName은 1.32에서 이미 GA였습니다. KYAML 도입은 1.33이 아닌 1.34입니다.
[Kubernetes 1.33 release](https://kubernetes.io/blog/2025/04/23/kubernetes-v1-33-release/) · [Versioned 1.36 resize guide](https://github.com/kubernetes/website/blob/release-1.36/content/en/docs/tasks/configure-pod-container/resize-container-resources.md) · [VPA 1.7.1 features](https://github.com/kubernetes/autoscaler/blob/vertical-pod-autoscaler-1.7.1/vertical-pod-autoscaler/docs/features.md) · [Service range extension](https://kubernetes.io/docs/tasks/network/extend-service-ip-ranges/) · [EKS network configuration API](https://docs.aws.amazon.com/eks/latest/APIReference/API_KubernetesNetworkConfigRequest.html) · [Data-transfer charge interpretation](https://docs.aws.amazon.com/cur/latest/userguide/cur-data-transfers-charges.html)
---
### 4.6 Kubernetes 1.34 "Of Wind & Will" (2025년 8월)
8월 27일 릴리스의 수치는 **58개 enhancement: stable 23개, beta 22개, alpha 13개**입니다. Beta의 기본값은 기능마다 다르며 그림의 일반적인 기본 활성화 표기를 실제 enablement matrix로 보지 않습니다.
#### DRA core API — GA
DeviceClass·ResourceClaim·ResourceClaimTemplate·ResourceSlice는 built-in `resource.k8s.io/v1` API이며 설치해야 하는 DRA core CRD가 아닙니다. Driver가 ResourceSlice로 device inventory를 제공하고 scheduler가 자원을 할당하며 kubelet이 driver와 device 준비를 조정합니다. 아키텍처 그림은 논리 흐름이며 API server·ResourceSlice 전달 경로를 생략합니다.
DRA가 기존 device plugin 모델을 제거하거나 모든 vendor의 time-slicing·MPS·MIG·NUMA·network 기능을 자동 제공하지는 않습니다. 고급 DRA 기능은 별도 gate와 단계가 있으며 지원되는 조정 모델 없이 동일 device를 독립 allocator 두 개에 맡기지 않습니다.
아래는 **명시적인 가상 driver 계약**을 사용합니다. `gpu.example.com`이 문자열 `model`과 `numa` attribute를 제공한다고 가정하며 실제 NVIDIA driver의 attribute 이름·설정을 주장하지 않습니다. 설치한 driver의 ResourceSlice를 확인한 뒤 driver·attribute를 바꿉니다. Stable request의 `deviceClassName`·`allocationMode`·`count`는 `exactly` 아래에 있어야 하며 기존 root-level 형태는 잘못되었습니다. `matchAttribute`는 요청한 device 간 값 일치를 요구하는 강한 제약이지 NUMA 선호가 아닙니다.
```yaml
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
name: example-a100
spec:
selectors:
- cel:
expression: 'device.driver == "gpu.example.com" &&
"gpu.example.com" in device.attributes &&
"model" in device.attributes["gpu.example.com"] &&
device.attributes["gpu.example.com"].model == "A100"'
---
apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
name: training-gpus
namespace: version-lab
spec:
devices:
requests:
- name: gpu
exactly:
deviceClassName: example-a100
allocationMode: ExactCount
count: 4
constraints:
- requests:
- gpu
matchAttribute: gpu.example.com/numa
---
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
name: four-gpu-template
namespace: version-lab
spec:
spec:
devices:
requests:
- name: gpu
exactly:
deviceClassName: example-a100
allocationMode: ExactCount
count: 4
constraints:
- requests:
- gpu
matchAttribute: gpu.example.com/numa
```
의도한 수명주기에 따라 명시적으로 관리하는 claim 또는 template의 Pod별 claim을 선택합니다. 아래는 두 대안을 보여 줍니다. Template도 device 4개를 요청하여 `--tensor-parallel-size 4`와 맞추며, 기존 1개 요청은 맞지 않았습니다. Inference image는 해당 인자를 구현해야 하고 모든 image는 placeholder입니다. Replica 1개에 적합한 device 4개가 필요하며 replica 3개면 12개가 필요합니다. GPU 할당이나 모델 서빙 benchmark를 실행하지 않았습니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: direct-gpu-claim
namespace: version-lab
spec:
resourceClaims:
- name: accelerators
resourceClaimName: training-gpus
containers:
- name: trainer
image: example.invalid/version-lab/trainer:reviewed
resources:
claims:
- name: accelerators
request: gpu
```
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: four-gpu-serving
namespace: version-lab
spec:
replicas: 1
selector:
matchLabels:
app: four-gpu-serving
template:
metadata:
labels:
app: four-gpu-serving
spec:
resourceClaims:
- name: accelerators
resourceClaimTemplateName: four-gpu-template
containers:
- name: inference
image: example.invalid/version-lab/inference:reviewed
args:
- --tensor-parallel-size
- '4'
resources:
claims:
- name: accelerators
request: gpu
```
#### VolumeAttributesClass — GA
VAC는 1.34부터 `storage.k8s.io/v1`을 사용합니다. 아래 class는 표준 `ebs.csi.aws.com` driver와 기존의 호환 regional gp3 volume용이며 자동으로 Auto Mode storage 절차가 되는 것은 아닙니다. PVC class 변경 전에 driver·sidecar·API 버전·권한·volume 크기/종류·변경 cooldown·instance EBS 제한을 확인합니다.
현재 regional gp3 상한은 **80,000 IOPS·2,000 MiB/s**이며 기본 3,000 IOPS 초과분에는 GiB당 500 IOPS, throughput에는 provisioned IOPS당 0.25 MiB/s 비율이 적용됩니다. 따라서 기존 64,000 IOPS는 최소 128 GiB에서 유효할 수 있지만 4,000 MiB/s는 gp3의 유효 값이 아닙니다. 예시는 이를 2,000으로 고치고 검증한 500-GiB volume을 전제로 합니다. Outposts 상한은 더 낮은 16,000 IOPS·1,000 MiB/s입니다. Volume 설정 상한이 앱·instance의 지속 성능을 보장하지는 않습니다.
```yaml
apiVersion: storage.k8s.io/v1
kind: VolumeAttributesClass
metadata:
name: high-iops
driverName: ebs.csi.aws.com
parameters:
iops: '16000'
throughput: '1000'
---
apiVersion: storage.k8s.io/v1
kind: VolumeAttributesClass
metadata:
name: standard
driverName: ebs.csi.aws.com
parameters:
iops: '3000'
throughput: '125'
---
apiVersion: storage.k8s.io/v1
kind: VolumeAttributesClass
metadata:
name: io-intensive
driverName: ebs.csi.aws.com
parameters:
iops: '64000'
throughput: '2000'
---
apiVersion: storage.k8s.io/v1
kind: VolumeAttributesClass
metadata:
name: throughput-optimized
driverName: ebs.csi.aws.com
parameters:
iops: '3000'
throughput: '750'
```
Class parameter는 불변이므로 class를 직접 수정하거나 불완전한 PVC를 생성하지 말고 기존 PVC에서 다른 class를 선택합니다. `.status.currentVolumeAttributesClassName`·`.status.modifyVolumeStatus`·event·실제 EBS 상태를 확인하며 요청 수락을 성능 변경 완료로 보지 않습니다.
기존 business-hours CronJob에는 identity/RBAC와 timezone·중복 처리 조건이 빠져 있었습니다. 아래의 완전한 **suspended 구성 예시**도 실제 실행된 운영 절차는 아닙니다. `version-lab`, 소유한 `database-pvc`, 호환 class, kubectl/jq와 신뢰할 수 있는 client 설정을 갖춘 image를 준비합니다. ServiceAccount는 namespace의 해당 이름 PVC만 get/patch할 수 있지만 RBAC가 patch할 PVC 필드까지 제한하지는 않습니다.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: vac-scheduler
namespace: version-lab
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: vac-scheduler
namespace: version-lab
rules:
- apiGroups:
- ''
resources:
- persistentvolumeclaims
resourceNames:
- database-pvc
verbs:
- get
- patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: vac-scheduler
namespace: version-lab
subjects:
- kind: ServiceAccount
name: vac-scheduler
namespace: version-lab
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: vac-scheduler
```
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: vac-business-hours
namespace: version-lab
spec:
schedule: 0 8 * * 1-5
timeZone: Asia/Seoul
suspend: true
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 0
activeDeadlineSeconds: 120
template:
spec:
serviceAccountName: vac-scheduler
restartPolicy: Never
containers:
- name: request-class
image: example.invalid/version-lab/kubectl-jq:reviewed
command:
- /bin/sh
- -c
- "set -eu\n: \"${POD_NAMESPACE:?}\"; : \"${TARGET_CLASS:?}\"\ncase \"\
$TARGET_CLASS\" in high-iops|standard|io-intensive|throughput-optimized)\
\ ;; *) exit 2 ;; esac\nstate=$(kubectl --request-timeout=15s -n \"\
$POD_NAMESPACE\" get pvc database-pvc -o json)\npatch=$(printf '%s\\\
n' \"$state\" | jq -ce --arg class \"$TARGET_CLASS\" '\n if .metadata.deletionTimestamp\
\ != null or .status.phase != \"Bound\"\n then error(\"Expected an\
\ existing non-deleting Bound PVC\")\n elif .status.modifyVolumeStatus\
\ != null\n then error(\"Existing modification needs review before\
\ another request\")\n elif .spec.volumeAttributesClassName == $class\n\
\ then []\n else [\n {op:\"test\",path:\"/metadata/uid\",value:.metadata.uid},\n\
\ {op:\"test\",path:\"/metadata/resourceVersion\",value:.metadata.resourceVersion},\n\
\ {op:\"add\",path:\"/spec/volumeAttributesClassName\",value:$class}\n\
\ ] end')\nif [ \"$patch\" = '[]' ]; then\n printf '%s\\n' 'Class\
\ already requested; verify actual modification status separately.'\n\
else\n kubectl --request-timeout=15s -n \"$POD_NAMESPACE\" patch pvc\
\ database-pvc --type=json --patch \"$patch\"\n printf '%s\\n' 'Class\
\ change requested; this is not proof of completed EBS modification.'\n\
fi\n"
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: TARGET_CLASS
value: io-intensive
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: vac-off-hours
namespace: version-lab
spec:
schedule: 0 22 * * 1-5
timeZone: Asia/Seoul
suspend: true
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 0
activeDeadlineSeconds: 120
template:
spec:
serviceAccountName: vac-scheduler
restartPolicy: Never
containers:
- name: request-class
image: example.invalid/version-lab/kubectl-jq:reviewed
command:
- /bin/sh
- -c
- "set -eu\n: \"${POD_NAMESPACE:?}\"; : \"${TARGET_CLASS:?}\"\ncase \"\
$TARGET_CLASS\" in high-iops|standard|io-intensive|throughput-optimized)\
\ ;; *) exit 2 ;; esac\nstate=$(kubectl --request-timeout=15s -n \"\
$POD_NAMESPACE\" get pvc database-pvc -o json)\npatch=$(printf '%s\\\
n' \"$state\" | jq -ce --arg class \"$TARGET_CLASS\" '\n if .metadata.deletionTimestamp\
\ != null or .status.phase != \"Bound\"\n then error(\"Expected an\
\ existing non-deleting Bound PVC\")\n elif .status.modifyVolumeStatus\
\ != null\n then error(\"Existing modification needs review before\
\ another request\")\n elif .spec.volumeAttributesClassName == $class\n\
\ then []\n else [\n {op:\"test\",path:\"/metadata/uid\",value:.metadata.uid},\n\
\ {op:\"test\",path:\"/metadata/resourceVersion\",value:.metadata.resourceVersion},\n\
\ {op:\"add\",path:\"/spec/volumeAttributesClassName\",value:$class}\n\
\ ] end')\nif [ \"$patch\" = '[]' ]; then\n printf '%s\\n' 'Class\
\ already requested; verify actual modification status separately.'\n\
else\n kubectl --request-timeout=15s -n \"$POD_NAMESPACE\" patch pvc\
\ database-pvc --type=json --patch \"$patch\"\n printf '%s\\n' 'Class\
\ change requested; this is not proof of completed EBS modification.'\n\
fi\n"
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: TARGET_CLASS
value: throughput-optimized
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
```
시간대는 Asia/Seoul로 명시했습니다. `Forbid`는 CronJob별 제어이지 두 schedule·다른 운영자 사이의 공유 lock이 아닙니다. UID·resourceVersion test는 API patch를 보호할 뿐 비동기 EBS 작업 전체를 직렬화하지 않습니다. 명령은 기존 변경 상태가 있으면 거부하고 class 요청 사실만 출력합니다. Workload 영향·backend 상태 확인·조정·복구를 확립할 때까지 schedule을 suspended로 유지하며 production 준비 완료를 주장하지 않습니다.
#### Ordered namespace deletion — GA
Pod를 다른 namespaced resource보다 먼저 삭제하여 Pod가 살아 있는데 NetworkPolicy 같은 보안 제어가 먼저 사라지는 문제를 줄입니다. 임의 dependency graph를 계산하거나 모든 namespace 삭제 완료를 보장하지는 않습니다. 사용할 수 없는 API·controller·finalizer 때문에 여전히 멈출 수 있으므로 실제 condition을 조사합니다. Finalizer 강제 제거 또는 예시 transcript를 실측 해결 결과로 취급하지 않습니다.
#### KYAML — client 출력 형식, 1.34에서 alpha
KYAML은 **KEP-5295**이며 KEP-4222가 아닙니다. 명시적 구분자와 인용된 문자열 값을 사용하는 YAML-compatible 출력 형식입니다. API server admission validator·전체 YAML 1.2 마이그레이션이 아니며 모든 manifest에서 anchor를 제거해야 하는 이유도 아닙니다. Kubectl 1.35에서 beta/기본 활성화, 1.36에서도 beta였고 1.37에서 stable이 되었습니다.
아래 일반 YAML을 `format-example.yaml`로 저장합니다. Kubectl 1.36.2로 실제 확인한 로컬 예시는 anchor를 받아들이고 두 `"no"` 문자열을 유지하며 클러스터에 접속하지 않습니다.
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: kyaml-local-example
data:
first: &string_value "no"
norway: *string_value
```
```bash
# Local formatting example, checked with kubectl 1.36.2; no cluster request.
kubectl --kubeconfig=/dev/null --server=https://127.0.0.1:1 --request-timeout=1s \
label --local --dry-run=client -f format-example.yaml \
audit.example.com/checked=true -o kyaml
```
Kubectl 1.36.2의 `KUBECTL_KYAML=false`는 `-o kyaml` printer를 비활성화하지만 KYAML input은 다른 출력 형식에서도 YAML로 읽힙니다. EKS 컨트롤 플레인 설정을 바꾸지 않습니다. Schema/admission 검증과 formatting은 별개이며 기존 KYAML 경고·거부 transcript는 서버 기능의 유효한 시연이 아니었습니다.
#### MutatingAdmissionPolicy — 1.34에서 beta
MAP는 1.32 alpha, 1.34 beta(기본 비활성화), 1.36 GA 순서입니다. 아래는 **현재 1.36+ stable 형식**이며 1.34에 그대로 적용하는 manifest가 아닙니다. 과거 beta API는 `v1beta1`이고 적절한 serving·gate 설정이 필요했습니다. EKS 컨트롤 플레인 gate는 AWS가 관리합니다.
이 정책은 명시된 기존 값을 유지하면서 Deployment의 기본 label을 추가합니다. Namespace 기반 cost label은 예시이며 검증된 재무 배분 규칙이 아닙니다. Binding은 opt-in namespace만 선택하고 `failurePolicy: Fail`은 평가 오류 시 여전히 해당 요청을 막을 수 있습니다. Kubernetes 1.36.2의 실제 mutation compiler/patcher와 가상 Deployment로 표현식을 검사했지만 전체 admission chain·production 환경을 실행하지는 않았습니다. CEL의 결정성이 모든 조합 정책의 멱등성이나 reinvocation·순서 문제 해소를 보장하지 않습니다.
```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
name: version-lab-default-labels
spec:
failurePolicy: Fail
reinvocationPolicy: IfNeeded
matchConstraints:
resourceRules:
- apiGroups:
- apps
apiVersions:
- v1
operations:
- CREATE
resources:
- deployments
mutations:
- patchType: ApplyConfiguration
applyConfiguration:
expression: "Object{\n metadata: Object.metadata{\n labels: {\n \"\
app.kubernetes.io/managed-by\":\n has(object.metadata.labels) && \"\
app.kubernetes.io/managed-by\" in object.metadata.labels\n ? object.metadata.labels[\"\
app.kubernetes.io/managed-by\"] : \"platform-team\",\n \"cost-center\"\
:\n has(object.metadata.labels) && \"cost-center\" in object.metadata.labels\n\
\ ? object.metadata.labels[\"cost-center\"] : request.namespace\n \
\ }\n }\n}"
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
name: version-lab-default-labels
spec:
policyName: version-lab-default-labels
matchResources:
namespaceSelector:
matchLabels:
version-lab-policy: enabled
```
#### 1.34의 기타 주요 단계
| 기능 | 상태 |
|---|---|
| PodLevelResources | Beta, 기본 활성화; GA 아님 |
| ImageVolume | Beta, 1.35 전까지 기본 비활성화 |
| UserNamespacesSupport | Beta, 기본 활성화; GA는 1.36 |
| NFTablesProxyMode·MatchLabelKeysInPodAffinity·CRDValidationRatcheting | 1.33에서 이미 GA |
| KubeletTracing·PodLifecycleSleepAction | GA; 후자는 PreStop sleep action |
| JobPodReplacementPolicy·RecoverVolumeExpansionFailure | GA |
| StructuredAuthenticationConfiguration·AnonymousAuthConfigurableEndpoints | GA |
| NodeLogQuery | 아직 beta; GA는 1.36 |
이 예시의 표준 image pull policy에 `IfNotPresentOrNewer`는 없습니다. 지원되는 `Always`·`IfNotPresent`·`Never` 의미를 사용하고 image 불변성은 별도 검토합니다.
[Kubernetes 1.34 release](https://kubernetes.io/blog/2025/08/27/kubernetes-v1-34-release/) · [DRA](https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/) · [EBS gp3 limits](https://docs.aws.amazon.com/ebs/latest/userguide/general-purpose.html) · [KYAML KEP-5295](https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/5295-kyaml) · [Kubernetes 1.37 changelog](https://github.com/kubernetes/kubernetes/blob/v1.37.0/CHANGELOG/CHANGELOG-1.37.md) · [MutatingAdmissionPolicy](https://kubernetes.io/docs/reference/access-authn-authz/mutating-admission-policy/)
---
### 4.7 Kubernetes 1.35 "Timbernetes" (2025년 12월)
12월 17일 발표는 **enhancement 60개**, 주요 단계별 **stable 17개·beta 19개·alpha 22개**를 보고합니다. 세 수의 합은 58이며 해당 분포가 나머지 2개를 별도로 설명하지는 않습니다. 다른 분류를 지어내거나 성능 측정치로 해석하지 않고 발표된 원 수치를 보존합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-12-kubernetes-version-roadmap-12.html)
#### 컨테이너 리소스 in-place resize — GA
Alpha 1.27 → beta 1.33 → stable 1.35 순서입니다. 초기 alpha가 단순히 “1.33 전에는 CPU-only”였다는 설명은 맞지 않습니다. GA는 API 안정화이며 모든 memory resize·runtime·node policy·앱이 중단을 피한다는 보장이 아닙니다. 앞 절의 UID·resourceVersion 확인과 현재 status 필드를 사용하고 버전별 제약을 검토합니다.
일반적인 Deployment template 변경은 여전히 rollout을 일으킵니다. Pod API가 GA라고 자동 “Deployment rolling in-place resize”가 생기는 것은 아닙니다. 관리되는 Pod의 resize와 controller template 변경은 별개이며 교체 Pod는 template·admission 경로를 따릅니다. HPA·VPA·GitOps·custom resizer의 resource 소유권을 조정합니다.
아래 Deployment는 앞 VPA 예시의 `web-app`/`app` 대상을 제공합니다. Image·resource 값은 검토할 입력이며 앱은 port 8080 같은 Service endpoint를 실제로 구현해야 합니다. EKS에서 rollout·resize·서비스 가용성 검사를 실행하지 않았습니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: version-lab
spec:
replicas: 2
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: app
image: example.invalid/version-lab/app:reviewed
resources:
requests:
cpu: 500m
memory: 256Mi
limits:
cpu: '1'
memory: 512Mi
resizePolicy:
- resourceName: cpu
restartPolicy: NotRequired
- resourceName: memory
restartPolicy: RestartContainer
```
VPA는 별도 버전 체계를 따릅니다. `InPlaceOrRecreate`는 VPA 1.6에서 GA가 되었으며 in-place 실패 시 Pod를 재생성할 수 있습니다. `InPlace`는 별도 gate가 필요한 VPA 1.7 alpha 모드입니다. Eviction을 하지 않는다는 것이 모든 container resize policy의 무재시작이나 모든 권고의 적용 가능성을 보장하지는 않습니다. 앞의 현재 VPA 예시 조건을 따르며 Kubernetes 1.35만으로 해당 VPA 모드가 활성화되지는 않습니다.
#### PreferSameNode traffic distribution — GA
`PreferSameTrafficDistribution`은 1.35에서 stable이 되었습니다. `PreferSameNode`는 가능한 경우 같은 node endpoint를 선호하고 fallback을 허용하므로 엄격한 `internalTrafficPolicy: Local`과 다릅니다. 실제 Service 구현·ready endpoint·traffic policy 우선순위를 확인합니다. 모든 ALB/NLB routing을 제어하거나 cross-zone traffic을 없애는 보장이 아닙니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: prefer-same-node
namespace: version-lab
spec:
trafficDistribution: PreferSameNode
selector:
app: web-app
ports:
- port: 80
targetPort: 8080
```
#### KYAML — beta, kubectl에서 기본 활성화
1.35의 beta·기본 활성화는 `-o kyaml` 출력 형식에 대한 것입니다. 모든 API server 입력을 새 strict parser로 바꾸거나 모든 YAML anchor에 경고하거나 서버 gate 변경을 위해 EKS 지원 티켓을 요구하지 않습니다. 앞의 로컬 예시와 실제 1.36.2 검사가 동작을 보여 줍니다. KYAML stable은 1.36이 아닌 1.37입니다.
#### Native gang scheduling — alpha, KEP-4671
Kubernetes 1.35에 native workload-aware/gang scheduling 개념이 도입되었습니다. 1.36에도 alpha이며 `GenericWorkload`·`GangScheduling`과 적절한 API·scheduler 활성화가 필요합니다. EKS version FAQ는 alpha 기능을 지원하지 않으며 self-managed node gate로 없는 EKS 컨트롤 플레인 API를 켤 수는 없습니다.
아래는 **upstream 실험 환경용 1.36 `v1alpha2` schema 예시**이지 이전 1.35 schema나 GA EKS 절차가 아닙니다. `spec.schedulingPolicy.gang.minCount`와 Pod의 `spec.schedulingGroup.podGroupName`을 사용합니다. 기존 `minMember`·`scheduleTimeoutSeconds`, Pod label·schedulingGate만으로 이 native API를 구성할 수 없습니다. 외부 PodGroup CRD는 별도 계약을 따릅니다.
```yaml
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
name: experimental-training
namespace: version-lab
spec:
schedulingPolicy:
gang:
minCount: 8
```
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: experimental-training
namespace: version-lab
spec:
completionMode: Indexed
completions: 8
parallelism: 8
backoffLimit: 0
template:
spec:
restartPolicy: Never
schedulingGroup:
podGroupName: experimental-training
containers:
- name: worker
image: example.invalid/version-lab/worker:reviewed
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: '1'
memory: 256Mi
```
최소 group 크기와 Job parallelism·completions를 모두 8로 맞췄습니다. Standalone group 예시이므로 소유자·controller가 group 수명주기를 관리하고 Pod scheduling 중 연결 관계를 안정적으로 유지해야 합니다. Scheduling 결정은 프로세스 동시 시작·readiness·분산 계산 성공·모든 deadlock 방지를 보장하지 않습니다. 앱에는 barrier·timeout/복구 로직·호환 용량이 필요합니다. Object schema만 확인했으며 group placement 실험은 실행하지 않았습니다.
#### 주요 버전·업그레이드 고려사항
| 항목 | 올바른 해석 |
|---|---|
| JobManagedBy | 1.35에서 GA |
| ImageVolume | Beta, 이제 기본 활성화; GA는 1.36 |
| PodLevelResources | 1.34 beta 이후 여전히 beta |
| UserNamespacesSupport | 아직 beta; GA는 1.36 |
| ContextualLogging | 여전히 beta이며 1.35 GA 아님 |
| CRDValidationRatcheting | 1.33에서 이미 GA |
| NodeInclusionPolicyInPodTopologySpread | 1.33에서 이미 GA |
| RecoverVolumeExpansionFailure·익명 인증 endpoint 설정 | 1.34에서 이미 GA |
Node 업그레이드에서는 “GA이므로 production 안전”을 가정하지 말고 cgroup·runtime 조건을 확인합니다. EKS 1.35 안내는 kubelet의 기본 cgroup v1 거부와 Fargate 같은 provider별 사례를 구분하므로 관리되는 Fargate host 설정을 직접 편집하지 않습니다. 해당 안내에서 Kubernetes 1.35는 containerd 1.x를 지원하는 마지막 릴리스이며 kubelet의 `--pod-infra-container-image` flag도 제거되었습니다. 현재 EKS node·AMI 절차와 업그레이드 문서를 따르고 bootstrap flag를 일괄 덮어쓰지 않습니다.
[Kubernetes 1.35 release](https://kubernetes.io/blog/2025/12/17/kubernetes-v1-35-release/) · [Versioned 1.36 feature gates](https://github.com/kubernetes/kubernetes/blob/v1.36.2/pkg/features/kube_features.go) · [Kubernetes 1.36.2 API schema](https://github.com/kubernetes/kubernetes/blob/v1.36.2/api/openapi-spec/swagger.json) · [EKS version notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions-standard.html)
---
### 4.8 Kubernetes 1.36 "Haru" (2026년 4월)
4월 22일 발표의 전체 수치는 **enhancement 70개**이며 단계별 분포에 **stable 18개·beta 25개·alpha 25개**를 제시합니다. 세 그룹의 합은 68이며 기존 문서는 이 부분합을 전체 수치로 잘못 사용했습니다. EKS 출시일은 별도 지원 일정 표에 기록합니다.
#### MutatingAdmissionPolicy — GA
Stable resource는 `admissionregistration.k8s.io/v1`의 `MutatingAdmissionPolicy`·`MutatingAdmissionPolicyBinding`입니다. 지원되는 mutation에 별도 webhook이 필요 없어지지만 정책 실패·비용 제한·순서·재호출 고려사항이 사라지지는 않습니다. 결정성이 모든 정책의 멱등성을 보장하지 않습니다.
아래 resize-policy 예시는 명시적 opt-in이며 값이 있는 `resizePolicy`가 없는 container에만 기본값을 추가해 기존 명시적 정책을 보존합니다. Kubernetes CEL은 `indexOf()`를 지원합니다. 기존 표현식도 유효했지만 기존 정책을 덮어썼으며, 실제 Kubernetes 1.36.2 compiler/patcher 검사로 이전 동작과 수정 동작을 확인했습니다. `resizePolicy`는 atomic list이므로 ApplyConfiguration patcher로 수정하면 거부되고 여기에는 JSONPatch가 적절합니다. 이 구현에서 MAP 내부 JSONPatch의 `test` 실패는 자동 admission 거부가 아닌 no-op으로 처리됩니다.
```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
name: inject-resizepolicy
spec:
failurePolicy: Fail
reinvocationPolicy: Never
matchConstraints:
resourceRules:
- apiGroups:
- ''
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
matchConditions:
- name: only-resize-enabled
expression: has(object.metadata.annotations) && ("resize.example.com/enabled"
in object.metadata.annotations) && object.metadata.annotations["resize.example.com/enabled"]
== "true"
mutations:
- patchType: JSONPatch
jsonPatch:
expression: "object.spec.containers.filter(c, !has(c.resizePolicy)).map(c, JSONPatch{\n\
\ op: \"add\",\n path: \"/spec/containers/\" + string(object.spec.containers.indexOf(c))\
\ + \"/resizePolicy\",\n value: [\n {\"resourceName\": \"cpu\", \"\
restartPolicy\": \"NotRequired\"},\n {\"resourceName\": \"memory\", \"\
restartPolicy\": \"RestartContainer\"}\n ]\n})"
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
name: inject-resizepolicy-binding
spec:
policyName: inject-resizepolicy
matchResources:
namespaceSelector:
matchLabels:
map-demo: 'true'
```
이 binding에는 소유한 테스트 namespace만 label로 연결합니다. `failurePolicy: Fail`은 평가 실패 시 해당 Pod 생성을 여전히 막을 수 있습니다. Workload에 적용하기 전에 정책 준비 상태·실패 사례·전체 admission chain을 확인하며 정책 생성 후 고정 시간 sleep을 readiness 보장으로 보지 않습니다. 주입된 필드 관찰만으로 어느 admission component가 만들었는지 확정할 수는 없습니다.
#### In-place resize와 Pod-level budget
Container별 resize는 1.35에서 이미 GA였습니다. 별도 `InPlacePodLevelResourcesVerticalScaling`은 1.36에서 beta/기본 활성화되며 PodLevelResources 자체는 여전히 beta입니다. Pod-level budget과 container limit는 별도 계량·정책 검토가 필요합니다. 아래 예시는 Pod-level budget을 의도적으로 거부하는 뒤의 CPU-downscale prototype 대상이 아닙니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: pod-budget-example
namespace: version-lab
spec:
os:
name: linux
nodeSelector:
kubernetes.io/os: linux
resources:
requests:
cpu: '2'
memory: 4Gi
limits:
cpu: '4'
memory: 8Gi
containers:
- name: app
image: example.invalid/version-lab/app:reviewed
resources:
requests:
cpu: '1'
memory: 2Gi
- name: helper
image: example.invalid/version-lab/helper:reviewed
resources:
requests:
cpu: 500m
memory: 512Mi
```
CPUManager checkpoint 개선이 모든 static CPU/Memory-manager workload의 resize나 특정 NUMA 배치 보존을 입증하지는 않습니다. 해당 경로에는 별도 기능·지원 조건이 있습니다. `NotRequired`는 정책상 재시작을 요구하지 않는다는 뜻이지 모든 중단 방지가 아닙니다. `RestartContainer`는 해당 resource 변경 시 재시작을 요청하며 `NotRequired` memory 축소도 best effort라 지연되거나 OOM race가 생길 수 있습니다. 실제 container resource와 앱 동작을 확인합니다.
#### User namespace·kubelet 인가·device health
UserNamespacesSupport의 GA는 **1.36**이며 출시된 1.36.2 소스에도 잠긴 gate가 남아 있습니다. Pod는 `hostUsers: false`로 opt-in하고 호환 kernel·filesystem·runtime 조건을 만족해야 합니다. UID 매핑은 심층 방어이지 모든 escape가 무해하거나 모든 앱을 수정 없이 실행한다는 증명이 아닙니다.
KubeletFineGrainedAuthz도 GA가 됩니다. `/pods`·`/runningPods`·`/configz`·`/healthz`에 더 세밀한 검사를 수행한 뒤 넓은 `nodes/proxy` 권한으로 fallback합니다. `/metrics`·`/stats`·`/logs`에는 이미 별도 subresource 구분이 있었습니다. Kubelet의 API server 접근을 제어하는 Node authorizer와 혼동하지 말고 호출자의 실제 권한을 검토하며 더 좁은 권한으로 충분하면 넓은 proxy 권한을 피합니다.
ResourceHealthStatus는 1.36에서 beta가 되어 device plugin·DRA의 device별 health를 보고할 수 있습니다. `status.containerStatuses[].allocatedResourcesStatus`를 확인하며 `status.resourceClaimStatuses`는 claim 참조·생성된 이름의 매핑입니다. 누락·Unknown·Unhealthy 상태는 driver·node·앱과 대조해야 하며 단독으로 원인을 확정하거나 device reset을 허가하지 않습니다.
```bash
# Read-only per-container resource health; no device reset or Pod deletion.
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${POD_NAME:?}"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n "$NAMESPACE" get pod "$POD_NAME" -o json |
jq '{uid:.metadata.uid,containers:[.status.containerStatuses[]? |
{name,allocatedResourcesStatus}]}'
```
LegacyServiceAccountTokenCleanUp은 **1.30**에서 이미 GA였으며 1.36의 새 GA가 아닙니다. Cleanup은 ServiceAccount 참조와 사용·mount 조건 등으로 자동 생성된 legacy token Secret을 구분합니다. 기본 미사용 기간은 무효화 전 1년이며 이후에도 미사용이면 삭제됩니다. 모든 과거 token이나 수동 생성 token이 제거된다는 뜻은 아닙니다. 유효기간이 제한된 TokenRequest token을 우선하고 감사 편의를 위해 token 값을 출력하지 않습니다.
#### SELinux·networking·기타 호환성 변경
출시된 1.36.2 gate 정의는 **SELinuxMountReadWriteOncePod**·**SELinuxChangePolicy**(GA)와 **SELinuxMount**(여전히 beta/기본 false)를 구분합니다. 일부 요약 문서는 이를 더 넓게 표현합니다. 모든 volume의 mount-label 동작이 같아졌다고 단정하지 말고 실제 node·provider 설정, CSI 지원, volume 공유 방식을 확인합니다. 서로 다른 SELinux label로 volume을 공유하면 명시적인 검토가 필요할 수 있습니다.
`StrictIPCIDRValidation`은 1.36에서 beta/기본 활성화입니다. 검사 대상 built-in 필드를 생성·변경할 때 canonical IP/CIDR을 사용합니다. 기존 저장 값에는 validation ratcheting 호환성이 적용될 수 있으며 모든 CRD를 자동 정규화하는 기능은 아닙니다. `gitRepo` volume driver는 1.36에서 영구 비활성화됩니다. API schema에 필드가 남아 있어도 kubelet이 해당 volume 실행을 거부하므로 업그레이드 전에 workload 패턴을 변경합니다.
Service `externalIPs`는 1.36에서 deprecated되며 발표된 제거 목표는 향후 계획이지 이 릴리스의 제거가 아닙니다. Upstream 1.36.2에는 IPVS proxier 코드와 생성 경로가 남아 있습니다. AWS version 요약의 제거 표현은 이 upstream 코드와 다르므로 보편적인 upstream 제거 사실로 바꾸거나 특정 EKS add-on image의 지원을 추정하지 않습니다. 선택한 EKS add-on과 마이그레이션 경로를 별도 확인합니다. 여기서는 EKS IPVS runtime을 검사하지 않았습니다.
ImageVolume·NodeLogQuery는 1.36 GA입니다. DRA partitionable device·consumable capacity·device binding condition은 각각의 beta gate를 따릅니다. KYAML은 1.36에서도 kubectl beta(1.37 stable)이고 GenericWorkload/GangScheduling은 1.36에서도 alpha입니다. 이전 버전의 GA를 새 1.36 GA로 다시 분류하지 않습니다.
#### 단계별 CPU downscale prototype
시작 부하가 큰 앱은 steady-state CPU 할당을 달리할 수 있지만 적절한 하한은 해당 앱에서 측정해야 합니다. Kubernetes의 `Running`은 warmup 완료 신호가 아닙니다. 아래는 실제 startupProbe 신호를 기본으로 사용하는 좁은 CPU-only 계약의 **실험용 컨트롤러이며 클러스터에서 실행하지 않았습니다**. Production-ready 컨트롤러나 가용성 보장이 아닙니다.
필수 입력은 하나의 `WATCH_NAMESPACE`와 검토한 양수 `MIN_STEADY_CPU`입니다. 해당 namespace에서 `resize.example.com/managed=true` label의 Pod만 watch하고 opt-in annotation도 요구합니다. Label·annotation 선택은 인가 경계가 아니므로 namespace의 workload 작성자를 신뢰해야 합니다. 명시적으로 선택한 Linux·container-level Guaranteed Pod만 허용하며 app/init의 CPU·memory request와 limit가 같아야 합니다. Memory 변경·upscale·잘못된 target·CPU 재시작 정책·진행 중 resize·확인되지 않거나 서로 다른 관측 resource는 거부합니다.
StartupProbePassed는 모든 target의 실제 startupProbe와 `started=true`를 요구합니다. Ready·Delay는 명시적인 대안이며 Ready에는 의미 있는 readiness 신호가 필요하고 Delay는 타이머일 뿐 warmup 완료의 증거가 아닙니다. 30초 resync와 API·reconciliation 지연 때문에 정확한 시점 보장도 아닙니다.
호환 dependency를 사용합니다. 감사에서는 Go 1.27.1·Kubernetes library v0.36.2를 사용했습니다.
```text
module example.com/pod-resizer
go 1.26.0
require (
k8s.io/api v0.36.2
k8s.io/apimachinery v0.36.2
k8s.io/client-go v0.36.2
)
```
```go
// Experimental CPU-downscale controller for Kubernetes 1.36.
// Not a production-readiness or zero-downtime guarantee.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
)
const (
managedLabel = "resize.example.com/managed"
annEnabled = "resize.example.com/enabled"
annTrigger = "resize.example.com/trigger"
annDelay = "resize.example.com/delay-seconds"
annSteady = "resize.example.com/steady-resources"
)
type config struct {
namespace string
minCPU resource.Quantity
}
type resourceValues struct {
Requests map[string]string `json:"requests"`
Limits map[string]string `json:"limits"`
}
type patchOperation struct {
Op string `json:"op"`
Path string `json:"path"`
Value any `json:"value"`
}
func main() {
namespace := os.Getenv("WATCH_NAMESPACE")
minCPU, err := resource.ParseQuantity(os.Getenv("MIN_STEADY_CPU"))
if len(validation.IsDNS1123Label(namespace)) != 0 || err != nil || minCPU.Sign() <= 0 {
log.Fatal("Set one valid WATCH_NAMESPACE and a reviewed positive MIN_STEADY_CPU")
}
cfg := config{namespace: namespace, minCPU: minCPU}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
clientConfig, err := rest.InClusterConfig()
if err != nil {
log.Fatal("In-cluster client configuration unavailable")
}
clientConfig.QPS, clientConfig.Burst = 5, 10
client, err := kubernetes.NewForConfig(clientConfig)
if err != nil {
log.Fatal("Client initialization failed")
}
factory := informers.NewSharedInformerFactoryWithOptions(client, 30*time.Second,
informers.WithNamespace(namespace),
informers.WithTweakListOptions(func(options *metav1.ListOptions) {
options.LabelSelector = managedLabel + "=true"
}))
informer := factory.Core().V1().Pods().Informer()
queue := workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[string]())
enqueue := func(obj any) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err == nil {
queue.Add(key)
}
}
_, err = informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: enqueue, UpdateFunc: func(_, current any) { enqueue(current) },
})
if err != nil {
log.Fatal("Informer handler registration failed")
}
factory.Start(ctx.Done())
if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) {
queue.ShutDown()
return
}
log.Printf("Cache synchronized; watching one namespace: %s", namespace)
var workers sync.WaitGroup
workers.Add(1)
go func() {
defer workers.Done()
for {
key, shutdown := queue.Get()
if shutdown {
return
}
obj, exists, err := informer.GetIndexer().GetByKey(key)
if err == nil && exists {
pod, ok := obj.(*corev1.Pod)
if ok {
err = requestResize(ctx, client, pod, cfg, time.Now())
}
}
if err != nil && ctx.Err() == nil && queue.NumRequeues(key) < 5 {
queue.AddRateLimited(key)
} else {
queue.Forget(key)
if err != nil {
log.Printf("Request failed for %s (%s); later events/resync may retry", key, apierrors.ReasonForError(err))
}
}
queue.Done(key)
}
}()
<-ctx.Done()
queue.ShutDown()
workers.Wait()
}
func requestResize(ctx context.Context, client kubernetes.Interface, pod *corev1.Pod, cfg config, now time.Time) error {
patch, err := buildResizePatch(pod, cfg, now)
if err != nil {
// Do not log annotation values, credentials or entire Pod objects.
log.Printf("Configuration needs review for %s/%s: %v", pod.Namespace, pod.Name, err)
return nil // Retry only on a later event/resync, not a tight error loop.
}
if len(patch) == 0 {
return nil
}
requestCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
_, err = client.CoreV1().Pods(pod.Namespace).Patch(requestCtx, pod.Name,
types.JSONPatchType, patch, metav1.PatchOptions{}, "resize")
if err == nil {
log.Printf("RESIZE_REQUESTED %s/%s uid=%s; verify kubelet status separately",
pod.Namespace, pod.Name, pod.UID)
}
return err
}
func buildResizePatch(pod *corev1.Pod, cfg config, now time.Time) ([]byte, error) {
if pod == nil || pod.Namespace != cfg.namespace || pod.Labels[managedLabel] != "true" ||
pod.Annotations[annEnabled] != "true" || pod.DeletionTimestamp != nil ||
pod.Status.Phase != corev1.PodRunning {
return nil, nil
}
if pod.UID == "" || pod.ResourceVersion == "" {
return nil, errors.New("missing Pod identity/version")
}
// This prototype deliberately handles only container-level Guaranteed Linux Pods.
if pod.Spec.OS == nil || pod.Spec.OS.Name != corev1.Linux ||
pod.Spec.NodeSelector[corev1.LabelOSStable] != "linux" ||
pod.Spec.Resources != nil || pod.Status.QOSClass != corev1.PodQOSGuaranteed {
return nil, errors.New("prototype requires declared Linux, container-level Guaranteed resources")
}
for _, c := range append(append([]corev1.Container{}, pod.Spec.Containers...), pod.Spec.InitContainers...) {
for _, name := range []corev1.ResourceName{corev1.ResourceCPU, corev1.ResourceMemory} {
request, hasRequest := c.Resources.Requests[name]
limit, hasLimit := c.Resources.Limits[name]
if !hasRequest || !hasLimit || request.Sign() <= 0 || request.Cmp(limit) != 0 {
return nil, errors.New("all app/init resources must satisfy the Guaranteed contract")
}
}
}
if pod.Status.ObservedGeneration < pod.Generation {
return nil, nil
}
for _, condition := range pod.Status.Conditions {
if condition.Status == corev1.ConditionTrue &&
(condition.Type == corev1.PodResizePending || condition.Type == corev1.PodResizeInProgress) {
return nil, nil
}
}
raw := pod.Annotations[annSteady]
if len(raw) == 0 || len(raw) > 4096 {
return nil, errors.New("missing or oversized steady-resources annotation")
}
var desired map[string]resourceValues
decoder := json.NewDecoder(strings.NewReader(raw))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&desired); err != nil {
return nil, errors.New("invalid steady-resources JSON shape")
}
if err := decoder.Decode(new(any)); err != io.EOF || len(desired) == 0 {
return nil, errors.New("expected one nonempty steady-resources object")
}
trigger := pod.Annotations[annTrigger]
if trigger == "" {
trigger = "StartupProbePassed"
}
delay := 0
switch trigger {
case "StartupProbePassed", "Ready":
case "Delay":
var err error
delay, err = strconv.Atoi(pod.Annotations[annDelay])
if err != nil || delay < 1 || delay > 3600 {
return nil, errors.New("Delay requires an integer from1 to3600 seconds")
}
default:
return nil, errors.New("unknown trigger")
}
podReady := false
for _, condition := range pod.Status.Conditions {
if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue {
podReady = true
}
}
statuses := make(map[string]corev1.ContainerStatus, len(pod.Status.ContainerStatuses))
for _, status := range pod.Status.ContainerStatuses {
statuses[status.Name] = status
}
ops := []patchOperation{
{Op: "test", Path: "/metadata/uid", Value: string(pod.UID)},
{Op: "test", Path: "/metadata/resourceVersion", Value: pod.ResourceVersion},
}
matched := 0
for i, container := range pod.Spec.Containers {
values, selected := desired[container.Name]
if !selected {
continue
}
matched++
if len(values.Requests) != 1 || len(values.Limits) != 1 ||
values.Requests["cpu"] == "" || values.Limits["cpu"] == "" {
return nil, errors.New("only explicit CPU request and limit are supported")
}
request, errRequest := resource.ParseQuantity(values.Requests["cpu"])
limit, errLimit := resource.ParseQuantity(values.Limits["cpu"])
current := container.Resources.Requests[corev1.ResourceCPU]
if errRequest != nil || errLimit != nil || request.Sign() <= 0 ||
request.Cmp(limit) != 0 || request.Cmp(cfg.minCPU) < 0 || request.Cmp(current) > 0 {
return nil, errors.New("CPU target must be equal, positive, above the floor and no larger than current")
}
for _, policy := range container.ResizePolicy {
if policy.ResourceName == corev1.ResourceCPU && policy.RestartPolicy == corev1.RestartContainer {
return nil, errors.New("CPU restart policy is incompatible with this prototype")
}
}
status, exists := statuses[container.Name]
if !exists || status.State.Running == nil || status.Resources == nil {
return nil, nil
}
observedRequest, rqOK := status.Resources.Requests[corev1.ResourceCPU]
observedLimit, lmOK := status.Resources.Limits[corev1.ResourceCPU]
if !rqOK || !lmOK || observedRequest.Cmp(current) != 0 || observedLimit.Cmp(current) != 0 {
return nil, nil
}
switch trigger {
case "StartupProbePassed":
if container.StartupProbe == nil {
return nil, errors.New("StartupProbePassed requires a real startupProbe on every target")
}
if status.Started == nil || !*status.Started {
return nil, nil
}
case "Ready":
if !podReady {
return nil, nil
}
case "Delay":
if status.State.Running.StartedAt.IsZero() ||
now.Sub(status.State.Running.StartedAt.Time) < time.Duration(delay)*time.Second {
return nil, nil
}
}
if request.Cmp(current) == 0 {
continue
}
base := fmt.Sprintf("/spec/containers/%d", i)
ops = append(ops,
patchOperation{Op: "test", Path: base + "/name", Value: container.Name},
patchOperation{Op: "replace", Path: base + "/resources/requests/cpu", Value: request.String()},
patchOperation{Op: "replace", Path: base + "/resources/limits/cpu", Value: request.String()})
}
if matched != len(desired) {
return nil, errors.New("steady-resources contains an unknown regular container")
}
if len(ops) == 2 {
return nil, nil
}
return json.Marshal(ops)
}
```
PATCH 성공은 `RESIZE_REQUESTED`로 기록하며 완료 처리하지 않습니다. UID·resourceVersion test가 오래된 이름·변경된 object를 거부하고 work queue로 재시도를 제한하며 취소를 처리합니다. 기존의 계속 증가하는 processed-UID map도 사용하지 않습니다. Prototype은 같은 Pod 안의 container 재시작에 startup CPU를 복원하거나 다른 HPA/VPA/GitOps writer와 조정하거나 배포 packaging·readiness·HA 정책·앱 SLO를 입증하지 않습니다. 여러 workload controller가 대상 Pod를 만들 수 있지만 rollout·교체·storage 동작은 별도 통합 검증이 필요합니다.
ServiceAccount는 namespace 범위의 Pod 읽기와 resize subresource 쓰기만 가지며 일반 Pod patch·Secret-read 권한은 없습니다. 기존 namespace·label을 준비하고 controller image를 빌드·검토해 해당 ServiceAccount로 실행하며 필수 환경변수 두 개를 지정합니다. Demo 하한 `50m`는 예시이지 일반 production 권장값이 아닙니다.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: version-lab
labels:
map-demo: 'true'
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-resizer
namespace: version-lab
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-resizer
namespace: version-lab
rules:
- apiGroups:
- ''
resources:
- pods
verbs:
- get
- list
- watch
- apiGroups:
- ''
resources:
- pods/resize
verbs:
- patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pod-resizer
namespace: version-lab
subjects:
- kind: ServiceAccount
name: pod-resizer
namespace: version-lab
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: pod-resizer
```
아래 workload는 controller 계약에 맞춥니다. 실제 CPU 작업이 아닌 sleep으로 warmup을 모사하며 기존 200m→50m·64Mi demo 입력을 보존합니다. 사용 전에 image를 검토·고정합니다. 시작 프로세스가 readiness 파일을 만들고 probe는 확인만 합니다. 기본 timeout 1초인데 probe가 8초 sleep하고 main이 파일을 만들지 않던 기존 문제를 고쳤습니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: phase-aware-demo
namespace: version-lab
spec:
replicas: 2
selector:
matchLabels:
app: phase-aware-demo
template:
metadata:
labels:
app: phase-aware-demo
resize.example.com/managed: 'true'
annotations:
resize.example.com/enabled: 'true'
resize.example.com/trigger: StartupProbePassed
resize.example.com/steady-resources: '{"app":{"requests":{"cpu":"50m"},"limits":{"cpu":"50m"}}}'
spec:
os:
name: linux
nodeSelector:
kubernetes.io/os: linux
automountServiceAccountToken: false
containers:
- name: app
image: busybox:1.36
command:
- sh
- -ec
- 'echo ''starting illustrative warmup''
sleep 10
touch "$READY_FILE"
echo ''readiness file created''
exec sleep 86400'
env:
- name: READY_FILE
value: /tmp/ready
resizePolicy:
- resourceName: cpu
restartPolicy: NotRequired
- resourceName: memory
restartPolicy: RestartContainer
resources:
requests:
cpu: 200m
memory: 64Mi
limits:
cpu: 200m
memory: 64Mi
startupProbe:
exec:
command:
- sh
- -ec
- test -f "$READY_FILE"
initialDelaySeconds: 1
periodSeconds: 2
timeoutSeconds: 1
failureThreshold: 30
```
```bash
# Read-only observation for the owned example.
set -euo pipefail
: "${KUBE_CONTEXT:?}"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n version-lab \
get pods -l app=phase-aware-demo -o json | jq '.items[] | {
name:.metadata.name,uid:.metadata.uid,generation:.metadata.generation,
observedGeneration:.status.observedGeneration,qosClass:.status.qosClass,
desired:[.spec.containers[] | {name,resources}],
reported:[.status.containerStatuses[]? | {name,started,ready,resources,restartCount,containerID}],
conditions:.status.conditions
}'
```
로컬 감사에서는 fake Kubernetes/RFC6902 기반 leaf 단위 검사 50개와 schema·host-shell probe fixture를 실행했습니다. EKS에서 informer loop·BusyBox를 실행하거나 warmup 시간·cgroup·앱 성능을 측정하지 않았습니다. VPA 1.7에도 alpha CPUStartupBoost가 있지만 trigger는 Pod Ready와 선택적 지속 시간이며 이 StartupProbePassed 계약과 다르고 별도 flag·운영 조건을 가집니다.
#### 기존 EKS 결과 기록 — 출처 검증되지 않음
기존 문서는 EKS 1.36.1·containerd 2.2.3·AL2023·cgroup v2·arm64/Graviton 환경을 주장했습니다. 원본 실행 artifact·출처가 제공되지 않았습니다. 아래 원래 표·log는 **검증되지 않은 기존 보고 결과**로 보존하며 재실행·현재 controller 출력·무중단의 독립적 증거가 아닙니다. MAP 표 두 개는 같은 기존 주장을 반복하며 독립 측정 두 건이 아닙니다.
| 케이스 | annotation | 주입된 resizePolicy | 판정 |
|--------|-----------|-------------------|------|
| with-annotation | 有 | `[{cpu:NotRequired},{memory:RestartContainer}]` | ✅ 주입됨 (webhook 없이) |
| without-annotation | 無 | `[]` (없음) | ✅ 주입 안 됨 (matchCondition 동작) |
```text
RESIZED resize-demo/demo-deploy-xxxxx-aaaaa [ReplicaSet] trigger=StartupProbePassed patch={"spec":{"containers":[{"name":"app","resources":{"limits":{"cpu":"50m"},"requests":{"cpu":"50m"}}}]}}
RESIZED resize-demo/demo-deploy-xxxxx-bbbbb [ReplicaSet] trigger=StartupProbePassed patch={"spec":{"containers":[{"name":"app","resources":{"limits":{"cpu":"50m"},"requests":{"cpu":"50m"}}}]}}
RESIZED resize-demo/demo-ds-yyyyy [DaemonSet] trigger=StartupProbePassed patch={"spec":{"containers":[{"name":"app","resources":{"limits":{"cpu":"50m"},"requests":{"cpu":"50m"}}}]}}
RESIZED resize-demo/demo-sts-0 [StatefulSet] trigger=StartupProbePassed patch={"spec":{"containers":[{"name":"app","resources":{"limits":{"cpu":"50m"},"requests":{"cpu":"50m"}}}]}}
```
| 워크로드 | QoS | CPU (req/lim) | restartCount | containerID |
|----------|-----|---------------|--------------|-------------|
| Deployment (x2) | Guaranteed → **Guaranteed** | 200m → **50m** | 0 → **0** | **동일(IDENTICAL)** |
| DaemonSet | Guaranteed → **Guaranteed** | 200m → **50m** | 0 → **0** | **동일(IDENTICAL)** |
| StatefulSet | Guaranteed → **Guaranteed** | 200m → **50m** | 0 → **0** | **동일(IDENTICAL)** |
| 케이스 | annotation | 주입된 resizePolicy | 판정 |
|--------|-----------|-------------------|------|
| with-annotation | 有 | `[{cpu:NotRequired},{memory:RestartContainer}]` | ✅ 주입됨 (webhook 없이) |
| without-annotation | 無 | `[]` (없음) | ✅ 주입 안 됨 (matchCondition 동작) |
이전 `RESIZED` log는 API PATCH 성공 뒤 출력했습니다. ContainerID·restartCount 유지와 원하는 spec 변경만으로 cgroup 적용·앱 latency·요청 손실 부재를 증명할 수는 없습니다. 같은 Pod UID·시간 범위, kubelet의 실제 resource·generation, 적절한 runtime·앱 관측을 함께 확인해야 합니다. 과거 수치를 새 Kubernetes 버전으로 바꾸거나 새 실측으로 제시하지 않았습니다.
[Kubernetes 1.36 release](https://kubernetes.io/blog/2026/04/22/kubernetes-v1-36-release/) · [1.36.2 feature definitions](https://github.com/kubernetes/kubernetes/blob/v1.36.2/pkg/features/kube_features.go) · [Kubelet authorization](https://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/) · [ServiceAccount administration](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) · [SELinux security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) · [Released IPVS selection path](https://github.com/kubernetes/kubernetes/blob/v1.36.2/cmd/kube-proxy/app/server_linux.go) · [VPA 1.7.1 features](https://github.com/kubernetes/autoscaler/blob/vertical-pod-autoscaler-1.7.1/vertical-pod-autoscaler/docs/features.md)
---
## 5. 주요 기능 졸업 타임라인
출시된 1.36.2 gate 정의와 공식 제거된 gate 이력을 중심으로 **Kubernetes 1.36까지**의 주요 upstream 이력을 정리합니다. Beta는 첫 beta 릴리스이며 기본 활성화를 의미하지 않습니다. 대시는 향후 일정을 약속하지 않으며 API 제공 여부·runtime/driver 조건·EKS 지원은 별도 확인합니다.
| 기능 | 첫 alpha | 첫 beta | 1.36까지 stable |
|---|---|---|---|
| Sidecar container | 1.28 | 1.29 | 1.33 |
| Container in-place resize | 1.27 | 1.33 | 1.35 |
| Pod scheduling readiness | 1.26 | 1.27 | 1.30 |
| Job success policy | 1.30 | 1.31 | 1.33 |
| Pod-level resource | 1.32 | 1.34 | — |
| ValidatingAdmissionPolicy | 1.26 | 1.28 | 1.30 |
| MutatingAdmissionPolicy | 1.32 | 1.34 | 1.36 |
| 구조화된 인가 | 1.29 | 1.30 | 1.32 |
| AppArmor native 필드 | — | 1.30 | 1.31 |
| User namespace | 1.25 | 1.30 | 1.36 |
| ServiceCIDR/IPAddress | 1.27 | 1.31 | 1.33 |
| Topology-aware hint | 1.21 | 1.23 | 1.33 |
| nftables proxy | 1.29 | 1.31 | 1.33 |
| Service traffic distribution | 1.30 | 1.31 | 1.33 |
| 같은 node/zone 선호 | 1.33 | 1.34 | 1.35 |
| ReadWriteOncePod | 1.22 | 1.27 | 1.29 |
| VolumeAttributesClass | 1.29 | 1.31 | 1.34 |
| PV 마지막 phase 전환 | 1.28 | 1.29 | 1.31 |
| Volume 확장 실패 복구 | 1.23 | 1.32 | 1.34 |
| Gang scheduling | 1.35 | — | — |
| 최소 topology domain | 1.24 | 1.25 | 1.30 |
| DRA core | 1.26 | 1.32 | 1.34 |
| HPA container metric | 1.20 | 1.27 | 1.30 |
| Image volume | 1.31 | 1.33 | 1.36 |
| Node log query | 1.27 | 1.30 | 1.36 |
| KMS v2 | 1.25 | 1.27 | 1.29 |
| Kubelet tracing | 1.25 | 1.27 | 1.34 |
| KYAML | 1.34 | 1.35 | — |
이력 해석 시 구분할 사항:
- AppArmor annotation은 1.4부터 beta였으며 표는 새 native 필드(beta 1.30, GA 1.31)를 다룹니다.
- User namespace는 이전의 제한적/stateless 지원부터 발전했습니다. 출시된 gate 이력은 alpha 1.25·beta 1.30·기본 활성화 1.33·GA 1.36을 기록하며 GA 날짜로 gate 제거를 추론하지 않습니다.
- DRA의 alpha 1.26은 원래 설계의 이력입니다. 이후 structured-parameter 재설계(KEP-4381)는 같은 API가 그대로 발전한 것이 아니며 classic DRA는 1.31에 별도 gate로 남았다가 1.32에서 제거되었습니다. 현재 stable request 구문은 `exactly`를 사용합니다.
- Native gang scheduling은 KEP-4671이며 1.36까지 alpha입니다. KYAML은 KEP-5295이며 표 범위 밖의 upstream 1.37에서 stable이 됩니다. 둘 다 1.36 GA가 아닙니다.
- Beta 기본값은 별도로 바뀝니다. UserNamespacesSupport는 1.33, ImageVolume은 1.35에서 기본 활성화되었고 PodLevelResources의 beta 시작은 1.34입니다.
Gateway API는 별도 버전의 API/CRD 프로젝트입니다. 해당 channel·kind version·conformance를 Kubernetes “alpha 1.18 / GA 1.26”에 대응시키지 않습니다. VPA update mode·Karpenter·CSI driver도 별도 release/support matrix를 따릅니다. Core API 졸업이 해당 component나 모든 예시의 production 준비 상태를 인증하지는 않습니다.
[Released Kubernetes 1.36.2 feature history](https://github.com/kubernetes/kubernetes/blob/v1.36.2/pkg/features/kube_features.go) · [Feature gates](https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/) · [Removed gates](https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates-removed/) · [KYAML history](https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/5295-kyaml)
---
## 6. Deprecation 및 제거 사항
### API version·필드·구현·gate를 구분합니다
GA **API version**은 같은 Kubernetes major version에서 제거할 수 없습니다. CLI flag·feature gate·개별 필드·volume 구현과는 다른 규칙입니다. Beta API 제거에는 deprecation 이후 9개월 또는 3회 minor release 중 더 긴 최소 기간이 적용되며 alpha API에는 같은 보장이 없습니다. 기존의 “GA API도 12개월/3회 릴리스 뒤 제거 가능”은 잘못된 설명입니다.
Gate deprecation·제거 규칙도 별개이며 실제 릴리스를 확인해야 합니다. GA 버전 번호에 2를 더해 gate 제거·비활성화를 결정하지 않습니다. 앞의 버전별 설명과 출시된 코드는 기본 활성화·잠금·실제 제거를 구분합니다.
### 주요 API 제거 시점
| API와 kind | Serving 중단 버전 | 현재 대체 API / 주의점 |
|---|---|---|
| `autoscaling/v2beta1` HPA | 1.25 | `autoscaling/v2`; metric schema 확인 |
| `autoscaling/v2beta2` HPA | 1.26 | `autoscaling/v2` |
| `batch/v1beta1` CronJob | 1.25 | `batch/v1` |
| `policy/v1beta1` PDB | 1.25 | `policy/v1`; 빈 selector 의미가 다름 |
| `flowcontrol.apiserver.k8s.io/v1beta2` FlowSchema/PriorityLevelConfiguration | 1.29 | `v1`; concurrency-share 필드·기본값 변경 확인 |
| `flowcontrol.apiserver.k8s.io/v1beta3` FlowSchema/PriorityLevelConfiguration | 1.32 | `v1` |
| `admissionregistration.k8s.io/v1beta1` ValidatingAdmissionPolicy/Binding | 1.34 | `v1`; 같은 group/version의 MutatingAdmissionPolicy와 구분 |
| `resource.k8s.io/v1alpha3` ResourceClaim/Template·DeviceClass·ResourceSlice | 1.34 | 현재는 stable `v1`; 이전 저장 표현은 릴리스별 마이그레이션 필요 |
| `storage.k8s.io/v1beta1` CSIDriver·CSINode·StorageClass·VolumeAttachment | 1.22 | `storage.k8s.io/v1` |
| `storage.k8s.io/v1beta1` CSIStorageCapacity | 1.27 | `storage.k8s.io/v1` |
| Beta Ingress·CRD·admission-webhook configuration API | 1.22 | Stable `v1`; schema·필드 변경도 포함 |
Resource group에는 다른 `v1alpha3` kind가 남아 있으므로 core DRA 네 kind 제거를 group/version 전체 제거로 일반화하지 않습니다. 1.34 changelog는 과거 DRA 저장 표현도 경고합니다. Backup·workload/claim 소유권·지정된 마이그레이션/재생성 절차를 조정하며 claim 전체 삭제나 `apiVersion` 문자열 변경만으로 해결하지 않습니다.
### 1.36 구현에 여전히 구분되어 있는 beta API
출시된 1.36.2 lifecycle metadata와 REST storage는 아래 버전을 구분합니다. 향후 제거 값은 기록된 목표이며 이후 릴리스에서 변경되지 않거나 managed service가 모든 API를 기본 활성화한다는 보장이 아닙니다.
| API와 kind | Metadata의 deprecation | 기록된 제거 목표 |
|---|---|---|
| DRA core `resource.k8s.io/v1beta1` | 1.35 | 1.38 |
| DRA core `resource.k8s.io/v1beta2` | 1.36 | 1.39 |
| VAC `storage.k8s.io/v1beta1` | 1.34 | 1.37 |
| MutatingAdmissionPolicy/Binding `admissionregistration.k8s.io/v1beta1` | 1.37 | 1.40 |
GA 졸업과 동시에 해당 beta API가 제거된 것이 아닙니다. 반대로 과거 alpha 제거 예상일도 이후 실제 changelog에서 구현이 바뀌면 최종 근거가 아닙니다.
추가 정정: KMS v1은 deprecated·기본 비활성화이며 1.31 제거가 아닙니다. `--authorization-mode`는 구조화 인가 설정의 대안으로 남아 있습니다. Iptables proxy mode는 1.34에서 제거되지 않았고 IPVS는 deprecated지만 upstream 1.36.2에 구현이 남아 있습니다. Legacy ServiceAccount Secret 자동 생성 변경은 1.33이 아닌 1.24입니다. 과거 `kubectl --export` 제거를 새 1.35 변화로 제시하지 않습니다. In-tree storage는 일괄 날짜표 대신 해당 plugin·release·CSI migration 상태·volume 식별자·driver 준비 상태를 확인합니다.
### 저장된 manifest와 실제 client 사용을 별도로 조사합니다
GET 응답은 요청한/선호 API 표현으로 변환되어 원래 client가 사용한 version을 숨길 수 있습니다. `kubectl get flowschemas -o json`, API discovery, CRD conversion webhook 목록만으로 deprecated API 사용을 입증하지 못하며 모든 `v1beta1`이 deprecated인 것도 아닙니다. Rendered Git/Helm manifest, 있을 경우 원래 적용 설정, API 사용 metric·audit log, EKS Insights, 대상 버전 검사를 함께 사용합니다. CRD의 served/storage version과 conversion 동작은 별도 확인합니다.
아래 metric은 응답한 API process가 관측한 deprecated 요청을 보여 줄 수 있지만 완전한 과거 요청 수나 모든 API server replica의 수집 범위는 아닙니다. Metric 부재·권한 오류를 정상 결과로 보지 않습니다.
```bash
# Read-only, explicitly selected cluster; metrics access may be restricted.
set -euo pipefail
: "${KUBE_CONTEXT:?}"; : "${EVIDENCE_PARENT:?Existing private directory}"
umask 077
evidence_dir=$(mktemp -d "$EVIDENCE_PARENT/api-usage.XXXXXXXX")
kubectl --context "$KUBE_CONTEXT" --request-timeout=20s get --raw='/metrics' > "$evidence_dir/metrics.prom"
awk '/^apiserver_requested_deprecated_apis/ {print}' "$evidence_dir/metrics.prom"
```
### 제한된 오프라인 lifecycle 검사기
아래 예시는 Python/PyYAML과 소유한 rendered YAML/JSON manifest 디렉터리를 필요로 합니다. 유한한 catalog를 1.36.2 검토 기준으로 고정하고 target 1.29~1.36만 받습니다. 같은 API version의 kind를 구분하고 parse 오류·빈 디렉터리를 거부하며 resource body를 출력하지 않습니다. **완전한 schema·client 사용·runtime 호환성 감사가 아닙니다.** 모든 deprecated API·의미 변경·YAML/schema 문제를 찾지는 못하므로 실제 CRD를 포함한 버전별 schema validator도 사용하고 `notInCatalog`를 검토합니다.
Exit1은 lifecycle 지적, exit2는 입력·parse 문제이며 exit0도 parse된 입력에 알려진 catalog 지적이 없다는 뜻만 가집니다. 실패를 숨기거나 일부 scan을 “호환됨”으로 표시하지 않습니다.
```python
"""Limited offline GVK lifecycle audit, snapshot: Kubernetes 1.36.2.
Requires PyYAML. This is not a schema, runtime or complete client-usage audit.
"""
import argparse
import json
import re
from pathlib import Path
import yaml
CATALOG = {}
def add(api, kinds, deprecated, removed, replacement):
for kind in kinds.split(","):
CATALOG[(api, kind)] = (deprecated, removed, replacement)
add("autoscaling/v2beta1", "HorizontalPodAutoscaler", 22, 25, "autoscaling/v2")
add("autoscaling/v2beta2", "HorizontalPodAutoscaler", 23, 26, "autoscaling/v2")
add("batch/v1beta1", "CronJob", 21, 25, "batch/v1")
add("policy/v1beta1", "PodDisruptionBudget", 21, 25, "policy/v1")
add("networking.k8s.io/v1beta1", "Ingress", 19, 22, "networking.k8s.io/v1")
add("extensions/v1beta1", "Ingress", 14, 22, "networking.k8s.io/v1")
add("apiextensions.k8s.io/v1beta1", "CustomResourceDefinition", 16, 22, "apiextensions.k8s.io/v1")
add("admissionregistration.k8s.io/v1beta1", "MutatingWebhookConfiguration,ValidatingWebhookConfiguration", 16, 22, "admissionregistration.k8s.io/v1")
add("admissionregistration.k8s.io/v1beta1", "ValidatingAdmissionPolicy,ValidatingAdmissionPolicyBinding", 31, 34, "admissionregistration.k8s.io/v1")
add("admissionregistration.k8s.io/v1beta1", "MutatingAdmissionPolicy,MutatingAdmissionPolicyBinding", 37, 40, "admissionregistration.k8s.io/v1")
add("flowcontrol.apiserver.k8s.io/v1beta1", "FlowSchema,PriorityLevelConfiguration", 23, 26, "flowcontrol.apiserver.k8s.io/v1")
add("flowcontrol.apiserver.k8s.io/v1beta2", "FlowSchema,PriorityLevelConfiguration", 26, 29, "flowcontrol.apiserver.k8s.io/v1")
add("flowcontrol.apiserver.k8s.io/v1beta3", "FlowSchema,PriorityLevelConfiguration", 29, 32, "flowcontrol.apiserver.k8s.io/v1")
add("storage.k8s.io/v1beta1", "CSIDriver", 19, 22, "storage.k8s.io/v1")
add("storage.k8s.io/v1beta1", "CSINode", 17, 22, "storage.k8s.io/v1")
add("storage.k8s.io/v1beta1", "StorageClass", 19, 22, "storage.k8s.io/v1")
add("storage.k8s.io/v1beta1", "VolumeAttachment", 19, 22, "storage.k8s.io/v1")
add("storage.k8s.io/v1beta1", "CSIStorageCapacity", 24, 27, "storage.k8s.io/v1")
add("storage.k8s.io/v1beta1", "VolumeAttributesClass", 34, 37, "storage.k8s.io/v1")
# Alpha core DRA kinds were actually removed in 1.34, overriding older plans.
add("resource.k8s.io/v1alpha3", "ResourceClaim,ResourceClaimTemplate,DeviceClass,ResourceSlice", 34, 34, "resource.k8s.io/v1")
add("resource.k8s.io/v1beta1", "ResourceClaim,ResourceClaimTemplate,DeviceClass,ResourceSlice", 35, 38, "resource.k8s.io/v1")
add("resource.k8s.io/v1beta2", "ResourceClaim,ResourceClaimTemplate,DeviceClass,ResourceSlice", 36, 39, "resource.k8s.io/v1")
def resources(obj, seen=None):
seen = set() if seen is None else seen
if obj is None:
return
if not isinstance(obj, dict):
raise ValueError("expected a resource mapping")
if id(obj) in seen:
raise ValueError("recursive resource List")
if len(seen) >= 32:
raise ValueError("resource List nesting exceeds32")
seen.add(id(obj))
try:
if obj.get("kind") == "List":
for item in obj.get("items", []):
yield from resources(item, seen)
else:
yield obj
finally:
seen.remove(id(obj))
def audit(directory, target_minor):
findings, errors, skipped = [], [], 0
files = sorted(p for p in directory.rglob("*") if p.is_file() and p.suffix.lower() in {".yaml", ".yml", ".json"})
if len(files) > 5000:
raise ValueError("limit exceeded: 5000 rendered files")
if not files:
errors.append({"path": str(directory), "errorType": "NoManifestFiles", "line": None})
for path in files:
try:
if path.is_symlink() or path.stat().st_size > 16 * 1024 * 1024:
raise ValueError("symlink or file exceeds16MiB")
for document in yaml.safe_load_all(path.read_text(encoding="utf-8")):
for obj in resources(document):
key = (obj.get("apiVersion"), obj.get("kind"))
entry = CATALOG.get(key)
if entry is None:
skipped += 1
continue
deprecated, removed, replacement = entry
if target_minor < deprecated:
continue
metadata = obj.get("metadata") or {}
if not isinstance(metadata, dict) or any(
metadata.get(k) is not None and not isinstance(metadata[k], str)
for k in ("name", "namespace")
):
raise ValueError("invalid metadata identity fields")
findings.append({
"path": str(path), "apiVersion": key[0], "kind": key[1],
"namespace": metadata.get("namespace"), "name": metadata.get("name"),
"state": "removed" if target_minor >= removed else "deprecated",
"replacement": replacement,
})
except (OSError, UnicodeError, ValueError, TypeError, yaml.YAMLError) as exc:
# Do not print parser snippets or resource/Secret bodies.
mark = getattr(exc, "problem_mark", None)
errors.append({"path": str(path), "errorType": type(exc).__name__,
"line": mark.line + 1 if mark is not None else None})
return {"snapshot": "Kubernetes1.36.2", "files": len(files), "findings": findings,
"errors": errors, "notInCatalog": skipped,
"limit": "Selected GVK lifecycle checks only; no matches do not certify compatibility."}
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--directory", type=Path, required=True)
parser.add_argument("--target-version", required=True)
args = parser.parse_args()
match = re.fullmatch(r"1\.(\d+)(?:\.\d+)?", args.target_version)
if not match or not 29 <= int(match[1]) <= 36 or not args.directory.is_dir():
parser.error("provide a rendered directory and a reviewed target from1.29 through1.36")
try:
result = audit(args.directory, int(match[1]))
except ValueError as exc:
parser.error(str(exc))
print(json.dumps(result, indent=2))
raise SystemExit(2 if result["errors"] else 1 if result["findings"] else 0)
```
```bash
# Save the Python example as api-version-audit.py; requires PyYAML.
: "${MANIFEST_DIR:?Directory containing owned rendered manifests}"
python3 api-version-audit.py --directory "$MANIFEST_DIR" --target-version 1.36.0
```
### Pluto·kubent·Helm의 범위
Pluto는 보조 탐지기로 유용하지만 최신 tool/rule이 정확성을 증명하지는 않습니다. 이번 native **Pluto5.24.3** fixture는 제거된 VAP beta API를 놓치고, 잘못된 YAML에도 exit0을 반환했으며, 위 1.36.2 lifecycle/storage 근거와 달리 DRA beta1을 1.36에서 제거된 것으로 표시했습니다. 결과는 공식 근거와 대조할 조사 단서로 취급합니다. 기본 exit2/3/4는 deprecation·removal·대체 API 미제공 지적이며 다른 실패도 조사해야 합니다. `--components k8s`로 무관한 번들 component version 기본값을 조용히 사용하는 일을 피합니다.
```bash
# Advisory only: record the reviewed Pluto version and its rule coverage.
: "${MANIFEST_DIR:?Directory containing owned rendered manifests}"
pluto detect-files --directory "$MANIFEST_DIR" \
--target-versions k8s=v1.36.0 --components k8s --output json
```
```bash
# Read-only cluster/Helm inspection can require access to release Secrets.
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?Owned namespace}"
pluto detect-all-in-cluster --kube-context "$KUBE_CONTEXT" --namespace "$NAMESPACE" \
--target-versions k8s=v1.36.0 --components k8s --output json
```
```bash
# Inspect names with their namespaces; a Helm release name is not globally unique.
: "${KUBE_CONTEXT:?}"; : "${NAMESPACE:?}"; : "${RELEASE_NAME:?}"
helm list --kube-context "$KUBE_CONTEXT" --namespace "$NAMESPACE" --output json
# If exporting manifests, use a private file: they can contain Secret values.
: "${PRIVATE_MANIFEST_FILE:?Choose a private destination}"
umask 077
helm get manifest --kube-context "$KUBE_CONTEXT" --namespace "$NAMESPACE" \
"$RELEASE_NAME" > "$PRIVATE_MANIFEST_FILE"
```
Kubent도 원래 manifest를 활용하는 탐지기이며 API server의 모든 사용 이력을 알려 주지는 않습니다. 여기서 확인한 최신 tag는 0.7.3(2024년 8월)이므로 새 target API의 rule 수집 범위를 확인합니다. 문서의 `--context`·`--target-version`·`--exit-error` flag를 확인하고 Helm 수집에는 release Secret/ConfigMap 읽기 권한이 필요합니다. `kubectl convert`는 지원하는 object 표현을 변환하며 deprecated client 사용을 조사하는 도구가 아닙니다. CRD conversion webhook이 있다는 이유만으로 deprecated라고 판단하지 않습니다.
[Kubernetes deprecation policy](https://kubernetes.io/docs/reference/deprecation-policy/) · [API migration guide](https://kubernetes.io/docs/reference/using-api/deprecation-guide/) · [1.34 changelog](https://github.com/kubernetes/kubernetes/blob/v1.34.0/CHANGELOG/CHANGELOG-1.34.md) · [1.36.2 DRA REST storage](https://github.com/kubernetes/kubernetes/blob/v1.36.2/pkg/registry/resource/rest/storage_resource.go) · [Pluto](https://github.com/FairwindsOps/pluto) · [Kubent](https://github.com/doitintl/kube-no-trouble)
---
## 7. EKS 특화 고려사항
### 릴리스와 기능 제공 여부
EKS는 자체 검증·지원 일정을 따릅니다. 3절의 날짜는 확인한 출시 기록이지 모든 향후 릴리스가 고정 지연 후 나온다는 보장이 아닙니다. Upstream API 성숙도·EKS API 제공 여부·node/runtime 기능은 별개의 질문입니다. EKS 컨트롤 플레인 flag는 AWS가 관리하며 kube-apiserver Pod 편집, kubeadm 설정 적용, node gate 하나 변경으로 조정할 수 없습니다.
EKS FAQ는 GA Kubernetes API 지원, 새 beta API의 기본 비활성화, alpha 기능 미지원을 명시합니다. 기존 beta API와 그 새 버전은 다르게 취급합니다. 모든 beta 필드가 제공되거나 GA 기능이 driver·설정·호환 node 없이 작동한다고 가정하지 말고 해당 EKS release note와 compute 구현을 확인합니다.
### 추정 최소 버전 표 대신 호환성 기록을 조회합니다
기존 `v1.x+` add-on matrix는 EKS build·platform·architecture·compute 호환성을 입증하지 못하고 collector·chart·add-on 버전 체계를 섞었습니다. 먼저 소유 계정·Region·cluster 버전·설치 component를 기록합니다. IRSA role 필드가 null이라고 AWS 권한이 없다는 뜻은 아니며 Pod Identity·provider-managed identity를 사용할 수 있습니다. Auto Mode 내장 component는 일반 설치 add-on 목록에 나타나지 않을 수 있습니다.
```bash
# Read-only inventory in the explicitly selected account/Region/cluster.
set -euo pipefail
: "${AWS_REGION:?}"; : "${CLUSTER_NAME:?}"; : "${EXPECTED_ACCOUNT_ID:?}"
actual_account=$(aws sts get-caller-identity --region "$AWS_REGION" --query Account --output text)
test "$actual_account" = "$EXPECTED_ACCOUNT_ID" || { printf '%s\n' 'Account mismatch' >&2; exit 1; }
aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" --no-cli-pager \
--query 'cluster.{version:version,platform:platformVersion,compute:computeConfig,upgradePolicy:upgradePolicy}' --output json
aws eks list-addons --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" --no-cli-pager --output json
```
```bash
# Inspect one addon, not a guessed first element or a bare component version.
: "${AWS_REGION:?}"; : "${CLUSTER_NAME:?}"; : "${ADDON_NAME:?}"
aws eks describe-addon --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--addon-name "$ADDON_NAME" --no-cli-pager \
--query 'addon.{name:addonName,version:addonVersion,status:status,issues:health.issues,role:serviceAccountRoleArn,podIdentityAssociations:podIdentityAssociations}' --output json
```
아래 후보 조회는 각 compatibility 기록 안의 **요청한 Kubernetes 버전**을 매칭합니다. Architecture·compute type·platform version·default 선택·configuration/IAM 조건을 유지합니다. 배열 첫 원소나 문자열 정렬의 최대 버전이 “최신 호환 버전”은 아닙니다. AWS default flag도 해당 compatibility 기록의 값이지 전역 순위가 아닙니다.
```bash
# Read-only candidates; no addon is installed or changed.
set -euo pipefail
: "${AWS_REGION:?}"; : "${TARGET_K8S_VERSION:?For example1.36}"; : "${ADDON_NAME:?}"
aws eks describe-addon-versions --region "$AWS_REGION" --no-cli-pager \
--kubernetes-version "$TARGET_K8S_VERSION" --addon-name "$ADDON_NAME" --output json |
jq -e --arg target "$TARGET_K8S_VERSION" --arg name "$ADDON_NAME" '
[.addons[]? | select(.addonName == $name) | . as $addon |
.addonVersions[]? as $release | $release.compatibilities[]? |
select(.clusterVersion == $target) |
{addon:$addon.addonName,version:$release.addonVersion,
architecture:$release.architecture,computeTypes:$release.computeTypes,
requiresConfiguration:$release.requiresConfiguration,requiresIamPermissions:$release.requiresIamPermissions,
platformVersions:.platformVersions,defaultForThisCompatibility:.defaultVersion}] |
if length == 0 then error("No matching compatibility record; do not infer support")
else . end'
```
결과는 배포 결정이 아닌 후보입니다. 대상 platform·node architecture/compute 구성·release note·필수 설정·AWS 권한을 확인합니다. 빈 결과·CLI 실패 시 선택을 중단합니다. 별도 검토한 update 전에 정확한 add-on configuration schema를 읽고 의도한 기존 값을 보존합니다. OVERWRITE 일괄 사용, 임의 과거 버전으로 downgrade, 컨트롤 플레인 update가 모든 add-on을 갱신한다는 가정을 피합니다. 감사에서는 live AWS catalog 대신 fake 응답·현재 CLI model로 명령을 검사했습니다.
### Auto Mode와 혼합 클러스터
Auto Mode는 내장 compute/network/storage component를 관리하지만 node 버전을 항상 `n−1`로 유지하거나 모든 외부 add-on을 자동 관리한다는 뜻은 아닙니다. Workload 제약·disruption 제어로 교체가 늦어질 수 있으므로 실제 update 상태·node 버전·custom NodePool 호환성을 확인합니다. Managed node group·self-managed/Hybrid node·Fargate Pod는 각각의 update·교체 절차를 따릅니다.
현재 Auto Mode node는 CoreDNS를 **node system service**로 실행합니다. 해당 workload를 모두 Auto Mode node로 옮긴 순수 Auto Mode 클러스터는 기존 CoreDNS Deployment를 제거할 수 있습니다. Auto/non-Auto 혼합 클러스터는 non-Auto node를 위해 Deployment를 유지해야 합니다. 일반 CoreDNS/VPC CNI/kube-proxy Pod 부재를 Auto Mode 장애로 단정하지 않으며, 존재 자체도 정상 동작의 증명은 아닙니다.
```bash
# Read-only node inventory; the label is evidence, not an availability check.
set -euo pipefail
: "${KUBE_CONTEXT:?}"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get nodes -o json | jq '[
.items[] | {name:.metadata.name,kubelet:.status.nodeInfo.kubeletVersion,
computeType:.metadata.labels["eks.amazonaws.com/compute-type"]}]'
```
새 `metadata.version`을 넣은 eksctl ClusterConfig만으로 upgrade가 실행되지는 않습니다. 검토한 update 절차와 반환된 update ID를 따릅니다. PDB는 가용성 보장이 아니므로 어떤 교체 작업이 이를 준수하고 어떤 scaling·삭제 경로가 다른지 이해합니다. 앱 readiness·storage·rollback 준비도 계획에 포함합니다.
### Extended support 비용의 맥락
확인한 버전 지원 요금 차이는 클러스터 시간당 $0.50입니다. 365일 예시의 추가 요금은 1개 $4,380·5개 $21,900·10개 $43,800·25개 $109,500·50개 $219,000이며 compute·provisioned control-plane tier·network 등은 제외합니다. 월 730시간도 모든 달의 실제 시간이 아닌 계획 가정입니다.
Fleet 수는 계획 기준 하나입니다. 중요한 클러스터 하나가 단순한 여러 클러스터보다 운영 위험이 클 수 있습니다. Standard 종료가 다가오면 계획 우선순위를 높여야지 staging 검증을 생략하거나 최소 검사로 production을 직접 업그레이드할 근거가 되지는 않습니다. 통제된 upgrade와 해당되는 경우 명시적으로 수용한 extended-support 비용을 비교합니다.
[EKS support policy](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html) · [DescribeAddonVersions](https://docs.aws.amazon.com/eks/latest/APIReference/API_DescribeAddonVersions.html) · [Auto Mode networking and DNS](https://docs.aws.amazon.com/eks/latest/userguide/auto-networking.html) · [EKS pricing](https://aws.amazon.com/eks/pricing/) · [Reviewed EKS upgrade guide](https://www.atomai.click/kubernetes-docs/llms/ko/eks/08-eks-upgrades.md)
---
## 8. 버전 업그레이드 계획
### 한 번의 minor-version 단계에 대한 실행 계획
EKS는 한 번에 한 minor version씩 업그레이드합니다. 확인한 release/support 일정·대상별 호환성 기록·현재 EKS upgrade 안내를 사용하며 upstream 최신 tag만으로 자격을 추론하지 않습니다. 기존의 준비 1~2주·실행 1~2일은 계획 예시이지 실측 소요 시간이나 기한이 아닙니다.
1. 소유 계정·Region·cluster, 컨트롤 플레인/node 버전, compute mode, add-on build, API 사용, operator·workload 소유자를 기록합니다. 다음 컨트롤 플레인 upgrade 전에 node를 안전한 현재 버전으로 맞춥니다. 지원 skew는 호환 범위이지 node를 3개 minor 뒤에 유지하라는 권고나 모든 API 강제 조건에 대한 보편적 주장이 아닙니다.
2. 대상 릴리스 변경, deprecated/removed API·저장 version 마이그레이션, runtime·OS·AMI, CRD·admission policy를 검토합니다. 도구 exit0·변환된 GET 응답·GA 표기가 전체 호환성 검사는 아닙니다.
3. 앱 상태·Kubernetes 설정을 적절히 backup하고 복원을 검증합니다. EKS의 etcd는 AWS가 관리하므로 고객이 backup 일정을 직접 조회하거나 소유한 컨트롤 플레인처럼 etcd snapshot 명령을 실행할 수 없습니다. Git은 DB·PVC backup을 대체하지 않습니다.
4. 대표성 있는 비프로덕션에서 실제 버전 단계와 component 순서를 연습합니다. 앱 readiness·network/DNS·storage·autoscaling·identity·observability를 확인하고 production 전에 rollback·data recovery 기준을 정합니다.
5. 승인된 컨트롤 플레인 update의 반환된 update ID를 성공한 terminal 상태까지 추적합니다. Node·해당 component는 문서화된 순서를 따릅니다. 일부 호환성·마이그레이션 작업은 컨트롤 플레인 이전에 필요하며 모든 버전·compute mode에 공통인 “kube-proxy → CoreDNS → VPC CNI → CSI” 순서는 없습니다.
6. 각 단계 후 고객이 보는 동작·replica readiness·API/update 상태·component health를 검증합니다. Pod의 Running만으로 readiness를 판단하지 말고 근거를 보존하며 runbook을 갱신합니다.
검토 시점 EKS update 문서는 특정 **upgrade** insight 문제에 `--force`를 요구하는 enforcement가 일시 rollback되었다고 명시합니다. Rollback-readiness 검사와는 별개이며 insight는 계속 계획에 필요합니다. 강제 조건 관련 안내가 호환성 문제를 무시할 근거는 아닙니다.
### 기능 검사와 compute 소유권
제거된 gate 이름이나 지원되지 않는 `managedNodeGroups[].kubelet.featureGates` 구조를 eksctl에 복사하지 않습니다. [클러스터 생성 안내](https://www.atomai.click/kubernetes-docs/llms/ko/eks/02-eks-cluster-creation.md)의 현재 schema와 node OS·provisioner가 지원하는 bootstrap 경로를 사용합니다. Managed EKS 컨트롤 플레인 flag는 AWS가 관리합니다. 호환 client를 사용하고 오프라인 schema·server-side dry-run·실제 runtime 검사를 구분합니다.
Auto Mode는 node 교체를 관리하지만 workload readiness·disruption 제약으로 지연될 수 있습니다. 혼합 클러스터는 non-Auto DNS/add-on 조건을 유지합니다. Managed node group rolling update와 desired/min/max scaling은 다른 작업이며 PDB가 scaling·직접 삭제·모든 복구 경로를 보편적으로 보호하지는 않습니다. Fargate·Hybrid Nodes는 각각의 수명주기 절차가 필요합니다.
### EKS native rollback과 복구 대안
버전 rollback은 실제 EKS 기능입니다. **In-place upgrade 완료 후** 7일 안에 시작하고 바로 이전 minor version만 대상으로 하며 지원 version·cluster 자격 조건을 만족해야 합니다. 현재 버전으로 생성된 cluster, 이후 추가 upgrade, 기간 만료, 호환되지 않는 EKS 기능 등은 rollback을 막을 수 있습니다. Extended 종료로 자동 upgrade된 경우도 대상이 아니며 extended-support version으로 돌아가면 upgrade policy·요금도 고려합니다.
운영자가 rollback을 시작하면 Auto Mode가 node를 먼저 되돌린 뒤 컨트롤 플레인을 처리합니다. Managed node group은 별도 UpdateNodegroupVersion rollback을 먼저 수행하고 self-managed/Hybrid node도 각각 준비합니다. Fargate worker version은 in-place rollback할 수 없어 공식 절차의 컨트롤 플레인 rollback 전후 계획된 제거·재배포 조정이 필요합니다. Force로 kubelet skew 검사를 우회한 상태를 지원 구성으로 보거나 live Fargate workload를 일괄 삭제하지 않습니다.
`--force`는 ERROR/WARNING/UNKNOWN rollback insight를 우회할 수 있지만 자격·사전 조건 검증이나 Auto Mode disruption 제어는 우회하지 않습니다. 안전한 기본값이 아닙니다. 정확한 update 상태 추적과 Auto Mode phase·timeout·취소 제약을 포함한 [EKS 업그레이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/08-eks-upgrades.md) 절차를 따릅니다. 컨트롤 플레인 rollback은 앱·DB rollback이 아니며 EKS는 모든 앱을 과거 상태로 복원하는 대신 etcd/customer data를 보존합니다.
| 계층 | 복구 계획 |
|---|---|
| 컨트롤 플레인 | 자격을 만족하는 native rollback 또는 불가능할 때 준비된 병렬 cluster 복구 |
| Node | 호환 version·통제된 교체/drain. Taint 추가만으로 기존 Pod·traffic이 이동하지 않음 |
| Workload | 검토한 GitOps/Helm revision rollback과 앱·data 호환성 확인 |
| Add-on | 정확한 호환 build·설정/IAM·지원 downgrade 검토. OVERWRITE 일괄 적용 금지 |
| 영속 data | Cluster version과 별개인 검증된 backup·앱 일관성 복구 |
### 기존 Terraform 프로젝트에서의 upgrade 수정
아래는 **기존 state 관리 resource의 attribute 조각**이며 독립적인 Terraform 배포가 아닙니다. 프로젝트의 IAM·network·access·encryption·launch template·scaling 설정을 유지합니다. 실제 inventory로 검토한 변수를 정의하고 plan을 확인합니다. 최소 resource 정의로 덮으면 설정을 초기화하거나 다른 cluster를 만들 수 있습니다.
기존 cluster resource에서 한 minor 단계의 target과 support policy를 의도적으로 선택합니다. STANDARD는 standard 종료 후 자동 upgrade될 수 있고 EXTENDED는 이후 유료 지원 기간을 수용합니다.
```hcl
# Edit these arguments inside the existing aws_eks_cluster.main resource.
version = var.reviewed_target_version
upgrade_policy {
support_type = var.reviewed_support_type
}
```
완전한 managed node-group resource에는 `node_role_arn`·`subnet_ids`·`scaling_config`가 필요하며 기존 예시는 앞 두 항목을 빠뜨렸습니다. 기존 값을 유지합니다. 이전의 desired3/min2/max10과 update budget 33%는 예시 입력이지 upgrade 기본값·가용성 보장이 아닙니다.
```hcl
# Relevant arguments inside the existing aws_eks_node_group.main resource.
# Retain the rest of the existing resource, including its scaling_config.
node_role_arn = var.existing_node_role_arn
subnet_ids = var.existing_node_subnet_ids
version = aws_eks_cluster.main.version
update_config {
max_unavailable_percentage = 33
}
```
기존 add-on마다 검토한 EKS build와 명시적인 update conflict policy를 사용합니다. PRESERVE는 update 옵션이며 CreateAddon conflict 옵션이 아닙니다. Configuration schema·identity·rollback 검토를 대신하지 않습니다.
```hcl
# Edit one already-managed aws_eks_addon resource after compatibility review.
addon_version = var.reviewed_addon_version
resolve_conflicts_on_update = "PRESERVE"
```
그림의 단계 기간은 추정이며 component 순서는 대상 release·compute mode의 절차를 따라야 합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-12-kubernetes-version-roadmap-17.html)
[EKS update procedure](https://docs.aws.amazon.com/eks/latest/userguide/update-cluster.html) · [EKS rollback prerequisites and sequencing](https://docs.aws.amazon.com/eks/latest/userguide/rollback-cluster.html) · [Terraform EKS node-group reference](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_node_group) · [Terraform EKS add-on reference](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon)
---
## 9. 향후 전망
### 출시된 upstream 변화와 EKS 제공 여부를 구분합니다
2026년 9월 12일 기준 upstream Kubernetes **1.37.0은 이미 8월 26일 출시**되었습니다. 향후 1.37 약속이 아니며 upstream 출시로 EKS 제공 여부를 추론하지 않습니다. EKS 계획에는 3절의 확인된 일정을 사용합니다. 본문 예시는 주로 1.36.2로 검사했으며 조용히 1.37로 일괄 변경하지 않았습니다.
| 확인한 upstream 1.37 항목 | 해석 |
|---|---|
| KYAML | Stable kubectl 출력 형식이며 새 API server YAML validator가 아님 |
| GenericWorkload | Beta, 기본 비활성화. 별도 GangScheduling gate 변경이 native group scheduling의 GA를 뜻하지 않음 |
| DRADeviceTaints·DRAResourceClaimDeviceStatus | Stable로 승격. Driver·reporting 조건은 계속 필요 |
| PodLevelResources·Pod-level in-place resize | 출시된 gate 이력에서도 beta이며 기존 예상 GA가 아님 |
| DRAPartitionableDevices | 여전히 beta. 모든 GPU 공유 구현을 보장하지 않음 |
검증되지 않은 “다음 버전 예정” 대신 출시 코드·changelog·해당 KEP를 확인합니다. Stable 기능도 gate 기본값/잠금·API/driver 제공 여부가 다를 수 있습니다.
### 생태계 방향은 Kubernetes 릴리스 확약이 아닙니다
DRA driver·device sharing·topology-aware 배치·batch 조정은 계속 발전합니다. GPU time-slicing/MIG/RDMA 동작은 core API version만이 아니라 실제 hardware·driver에 달려 있습니다. Supply-chain 서명/검증·confidential container·GitOps·platform engineering·OpenTelemetry·Wasm은 별도 프로젝트와 릴리스 정책을 가진 생태계 연동 주제이며 Kubernetes에 자동 내장되거나 특정 미래 연도에 모두 제공된다는 보장이 아닙니다.
지원 기한·호환성·사업 위험에 맞는 반복 upgrade/연습 주기를 유지합니다. 분기별 일정과 upstream의 약 4개월 주기는 서로 다릅니다. 적절한 standard-supported EKS 버전은 추가 버전 요금을 피할 수 있지만 `최신−1`이라는 보편 규칙이 앱 검증을 대신하거나 모든 GA 기능을 즉시 무위험하게 도입해야 한다는 뜻은 아닙니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-12-kubernetes-version-roadmap-18.html)
### 과거 계획 템플릿 — 현재 배포 권고가 아님
이전 한글 문서의 예시 inventory와 계획 월을 보존합니다. Version·수량·월 값은 과거 예시 입력이지 실제 발견한 fleet·수행한 upgrade·검증된 component 조합이 아닙니다. 특히 이전 Istio/Argo CD/chart 버전을 새 Kubernetes와 호환된다고 취급하지 않습니다. 새 계획에서는 실제 inventory와 현재 호환성 근거로 바꿉니다. 아래 검토 항목은 재실행을 지어내지 않고 기존의 nftables 자동 전환·DRA CRD·KYAML 가정을 바로잡았습니다.
```yaml
historical_planning_example:
provenance: Illustrative prior chapter inputs; no executed upgrade or verified component
compatibility.
starting_state:
cluster_version: '1.33'
node_count: 50
workload_count: 200
component_versions:
- name: istio
version: '1.22'
- name: argocd
version: '2.11'
- name: prometheus-stack
version: '60.0'
target_version: '1.36'
upgrade_path:
- '1.33'
- '1.34'
- '1.35'
- '1.36'
phases:
- target: '1.34'
historical_planned_month: 2025-11
review:
- DRA API/driver and VAC compatibility
- Proxy backend migration only if deliberately selected; not automatic
- target: '1.35'
historical_planned_month: 2026-03
review:
- In-place resize and the actual VPA release/mode
- KYAML is a client output format, not a server parsing migration
- target: '1.36'
historical_planned_month: 2026-07
review:
- Pod-level resource policies and supported compute/runtime
- Gang scheduling is still alpha in1.36; not a GA EKS prerequisite
```
---
## 10. 참고 자료
과거 요약 표나 도구의 번들 가정보다 릴리스별 소스·vendor API 문서를 우선합니다. 실제 변경 시에는 대상 version·provider·component release·기능 설정을 다시 확인합니다.
- [Kubernetes releases](https://kubernetes.io/releases/)
- [Patch support policy](https://kubernetes.io/releases/patch-releases/)
- [Feature gates](https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/)
- [Removed feature gates](https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates-removed/)
- [API deprecation policy](https://kubernetes.io/docs/reference/deprecation-policy/)
- [API migration guide](https://kubernetes.io/docs/reference/using-api/deprecation-guide/)
- [Kubernetes 1.36.2 source](https://github.com/kubernetes/kubernetes/tree/v1.36.2)
- [Kubernetes 1.37 changelog](https://github.com/kubernetes/kubernetes/blob/v1.37.0/CHANGELOG/CHANGELOG-1.37.md)
- [EKS support calendar](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html)
- [EKS version notes](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions-standard.html)
- [EKS upgrades](https://docs.aws.amazon.com/eks/latest/userguide/update-cluster.html)
- [EKS rollback](https://docs.aws.amazon.com/eks/latest/userguide/rollback-cluster.html)
- [EKS add-on compatibility API](https://docs.aws.amazon.com/eks/latest/APIReference/API_DescribeAddonVersions.html)
- [EKS Auto Mode networking](https://docs.aws.amazon.com/eks/latest/userguide/auto-networking.html)
- [EKS best practices](https://docs.aws.amazon.com/eks/latest/best-practices/introduction.html)
- [EKS pricing](https://aws.amazon.com/eks/pricing/)
- [VPA 1.7.1 features](https://github.com/kubernetes/autoscaler/blob/vertical-pod-autoscaler-1.7.1/vertical-pod-autoscaler/docs/features.md)
- [Pluto](https://github.com/FairwindsOps/pluto)
- [Kubent](https://github.com/doitintl/kube-no-trouble)
### 공식 릴리스 발표
- [Kubernetes 1.29](https://kubernetes.io/blog/2023/12/13/kubernetes-v1-29-release/)
- [Kubernetes 1.30](https://kubernetes.io/blog/2024/04/17/kubernetes-v1-30-release/)
- [Kubernetes 1.31](https://kubernetes.io/blog/2024/08/13/kubernetes-v1-31-release/)
- [Kubernetes 1.32](https://kubernetes.io/blog/2024/12/11/kubernetes-v1-32-release/)
- [Kubernetes 1.33](https://kubernetes.io/blog/2025/04/23/kubernetes-v1-33-release/)
- [Kubernetes 1.34](https://kubernetes.io/blog/2025/08/27/kubernetes-v1-34-release/)
- [Kubernetes 1.35](https://kubernetes.io/blog/2025/12/17/kubernetes-v1-35-release/)
- [Kubernetes 1.36](https://kubernetes.io/blog/2026/04/22/kubernetes-v1-36-release/)
## 퀴즈와 다음 단계
- [버전별 기능과 로드맵 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks/12-kubernetes-version-roadmap-quiz)
- [EKS 업그레이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/08-eks-upgrades.md)
- [EKS 고급 디버깅](https://www.atomai.click/kubernetes-docs/llms/ko/eks/11-eks-advanced-debugging.md)
- [EKS 클러스터 생성 실습](https://www.atomai.click/kubernetes-docs/ko/labs/eks/01-eks-cluster-creation-lab)
- [EKS Auto Mode](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md)
< [이전: EKS 고급 디버깅](https://www.atomai.click/kubernetes-docs/llms/ko/eks/11-eks-advanced-debugging.md) | [목차](https://www.atomai.click/kubernetes-docs/ko/) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-hybrid-nodes/
----------------------------------------
# EKS Hybrid Nodes
> **지원 버전**: 현재 EKS 지원 버전; 예제 검토 기준 EKS 1.36 / nodeadm 1.0.20
> **마지막 업데이트**: 2026년 9월 13일
Amazon EKS Hybrid Nodes는 고객이 운영하는 온프레미스·엣지 노드를 AWS 관리형 EKS control plane에 연결합니다. Host·OS·연결·workload 운영은 계속 사용자 책임입니다. 이 가이드는 지원 인터페이스와 예제 구성을 구분하며 특정 온프레미스 프로덕션 배포를 검증했다는 증거가 아닙니다.
## 목차
1. [사전 요구 사항 및 시스템 요구 사항](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/01-prerequisites.md)
2. [네트워크 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/02-network-configuration.md)
3. [에어갭 환경 구성 (S3 + VPC 엔드포인트)](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/03-airgap-setup.md)
4. [노드 부트스트랩](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md)
5. [GPU 서버 통합](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/05-gpu-integration.md)
6. [워크로드 배치 전략](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/06-workload-placement.md)
7. [노드 라이프사이클 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/07-node-lifecycle.md)
8. [운영 및 유지보수](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/08-operations.md)
9. [베어메탈 서버 OS 설치 및 마이그레이션 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/09-bare-metal-os-setup.md)
10. [Hybrid Nodes Gateway](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/10-hybrid-nodes-gateway.md)
## Hybrid Nodes 개요
Hybrid Nodes와 일반 AWS compute node는 같은 cluster에 있을 수 있습니다. 반면 cloud machine을 **hybrid** node로 등록하는 것은 별개입니다. AWS Region·Local Zone·Outposts·다른 cloud를 hybrid-node 인프라로 사용하는 것은 지원하지 않으며 EC2에서도 hybrid 요금이 발생합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-highlevel-0.html)
아래 다이어그램은 VPC, 서브넷, Transit Gateway/Virtual Private Gateway, 원격 노드/파드 CIDR 연결을 포함한 네트워크 사전 요구 사항을 보여줍니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-prereq-0.html)
그림은 private 연결·라우팅 구조이며 모든 on-prem route/firewall/AWS service endpoint의 자동 생성을 뜻하지 않습니다.
## 사용 사례와 데이터 경계
온프레미스 GPU, 대규모 로컬 데이터, edge 처리와 기존 하드웨어는 Hybrid Nodes를 선택하는 이유가 될 수 있습니다. Data locality에는 애플리케이션·스토리지·egress·logging 제어도 필요합니다. Kubernetes API 객체와 control-plane metadata는 AWS에서 관리되므로 node selector만으로 데이터 주권·규정 준수가 입증되지는 않습니다.
`on-premises`라는 AWS zone이 있다고 가정하지 말고 실제 hybrid compute label과 명시적으로 관리하는 조직 label로 배치합니다.
```yaml
# Pod spec fragment; set organization labels through the node owner.
nodeSelector:
eks.amazonaws.com/compute-type: hybrid
example.com/data-location: on-premises
```
이 조각은 label, 완전한 앱, 보안 경계나 데이터 보존 정책을 만들지 않습니다. Image/runtime 호환성과 실제 데이터 경로를 검증하세요.
## 아키텍처와 운영 책임
| 구성 요소 | 위치 | 책임 |
|-----------|------|------|
| EKS API server·etcd·controller·scheduler | AWS | AWS 관리형 control plane |
| nodeadm | 지원되는 온프레미스 Linux host | 설치/bootstrap/upgrade CLI; 상시 node agent가 아님 |
| kubelet/containerd | 온프레미스 | Node agent/CRI runtime; host owner가 운영 |
| Cilium 또는 Calico | 온프레미스·cluster | 호환 CNI 구성; VPC CNI는 hybrid node를 관리하지 않음 |
| SSM Agent 또는 Roles Anywhere signing helper | 온프레미스 | 해당 AWS 서비스에서 임시 자격 증명 획득 |
| SSM/IAM Roles Anywhere 서비스 | AWS | 자격 증명 서비스이며 로컬 offline CA 대체물이 아님 |
| VPN/Direct Connect·routing | 양쪽 환경 | 양방향 연결; Direct Connect만으로 암호화가 보장되지 않음 |
지원 Bottlerocket VMware 변형은 자체 bootstrap 경로를 사용하며 nodeadm을 사용하지 않습니다. 다른 지원 host에서는 `nodeadm install`이 의존성을 설치하고 `nodeadm init`이 구성·join합니다. SSM signing key 변경 때문에 SSM 신규 설치/upgrade에는 **nodeadm 1.0.19 이상**이 필요하며, 확인한 현재 릴리스는 **1.0.20**입니다.
## 계획에 반영할 제약
- **연결된 환경:** AWS와 안정적인 private 양방향 연결이 필요합니다. Disconnected/intermittent DDIL용이 아닙니다. 이 가이드의 “에어갭”은 필요한 AWS 연결을 유지하면서 인터넷 접근을 제한하는 환경이지 AWS로부터의 완전 격리가 아닙니다.
- **주소:** IPv4 RFC1918 또는 CGNAT이며 remote node/Pod·VPC·service CIDR이 겹치면 안 됩니다. Cluster당 **node CIDR 15개·Pod CIDR 15개**까지 지원합니다.
- **인증:** `API` 또는 `API_AND_CONFIG_MAP`과 Hybrid Nodes IAM role/access entry를 준비합니다.
- **API endpoint:** AWS는 public-only 또는 private-only를 권장합니다. 둘 다 켜면 VPC 밖 노드는 public endpoint 주소를 해석하므로 기대한 private 경로·접근 규칙에 따라 join이 **실패할 수 있습니다**. 보편적인 API 금지는 아닙니다. Public API endpoint를 써도 control-plane→node private 연결 요구는 없어지지 않습니다.
- **리전:** 현재 overview 기준 AWS GovCloud (US)·AWS China를 제외한 리전에서 지원합니다.
- **Host 지원:** OS·아키텍처·CNI·kernel을 함께 검토합니다. AL2023은 on-prem 가상화 환경용이며 일반적인 bare-metal 권장이 아닙니다.
- **요금:** Node가 연결된 동안 보고된 vCPU-hours로 과금합니다. Hyperthreading한 bare-metal core는 vCPU 2개로 보고될 수 있습니다. Workload가 idle이어도 node 요금이 자동 종료되지 않으며 cluster·다른 서비스 요금은 별도입니다.
## 자격 증명 프로바이더
두 방식 모두 갱신을 위해 AWS service endpoint 접근이 필요합니다. 로컬 CA가 IAM Roles Anywhere의 AWS 자격 증명 offline 발급을 가능하게 하지는 않습니다. 검토한 혼합 이유가 없다면 fleet에서 한 provider를 일관되게 사용하는 것을 권장합니다.
| 항목 | SSM hybrid activation | IAM Roles Anywhere |
|------|-----------------------|--------------------|
| Bootstrap | Activation ID/code와 준비한 SSM-trusting role | PKI·node별 cert/key·trust anchor·profile·role |
| 이름 | SSM 생성 `mi-...` 이름 | 인증서 identity에 연결된 custom node name |
| Session 수명 | 고정 1시간, SSM이 갱신 | 기본 1시간; request/profile은 15분–12시간 범위, effective duration·role maximum 적용 |
| 단절 | 갱신 불가; 복구 후 retry backoff로 재연결이 지연될 수 있음 | Offline에서 새 자격 증명 획득 불가; 연결 복구 후 credential-process가 필요 시 획득 |
| 규모/비용 | SSM node 등록·node 수 기준 관리 요금 없음; 기능별 사용 요금 조건은 별도 | IAM Roles Anywhere quota와 PKI 운영 요건 확인 |
| 일반적인 선택 | 기존 PKI가 없고 간단한 등록이 필요할 때 | 기존 PKI·인증서 수명 관리가 있을 때 |
**요금 확인일: 2026년 9월 13일.** SSM은 2026년 6월 30일부로 Advanced Instances Tier를 폐지했습니다. Session Manager·Run Command 사용 요금 조건은 [현재 SSM 요금표](https://aws.amazon.com/systems-manager/pricing/)를 확인하며, [EKS Hybrid Nodes vCPU 요금](https://aws.amazon.com/eks/pricing/)은 별도입니다.
Roles Anywhere profile은 custom role session name을 허용해야 하며 trust policy가 그 이름을 선택한 인증서 속성에 연결해야 합니다. Effective session duration은 IAM role maximum을 **초과하면 안 되며**, CreateSession API상 같은 값도 허용됩니다. [사전 요구 사항](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/01-prerequisites.md)에서 이 계약과 안전한 준비를 설명합니다.
## 워크로드 예시
1. 검증한 runtime·복구 계획을 사용하는 local GPU training/inference.
2. AWS metadata/telemetry/egress 경로를 별도로 검토한 local data processing.
3. 안정적인 연결·단절 동작 검증이 있는 factory/edge 앱.
4. 기존 대규모 데이터 가까이에서 수행하는 media processing.
## 다음 단계
EKS Hybrid Nodes에 대한 이해를 더욱 깊이 하고 실습을 진행하려면 다음 리소스를 참고하세요:
### 퀴즈
이 문서의 내용을 테스트하려면 다음 퀴즈를 풀어보세요:
* [EKS Hybrid Nodes 사전 요구사항 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks-hybrid-nodes/01-prerequisites-quiz)
* [EKS Hybrid Nodes 네트워크 구성 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks-hybrid-nodes/02-network-configuration-quiz)
* [EKS Hybrid Nodes 에어갭 환경 구성 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks-hybrid-nodes/03-airgap-setup-quiz)
* [EKS Hybrid Nodes 노드 부트스트래핑 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks-hybrid-nodes/04-node-bootstrap-quiz)
* [EKS Hybrid Nodes GPU 통합 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks-hybrid-nodes/05-gpu-integration-quiz)
* [EKS Hybrid Nodes 워크로드 배치 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks-hybrid-nodes/06-workload-placement-quiz)
* [노드 라이프사이클 관리 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks-hybrid-nodes/07-node-lifecycle-quiz)
* [EKS Hybrid Nodes 운영 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks-hybrid-nodes/08-operations-quiz)
* [베어메탈 서버 OS 설치 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks-hybrid-nodes/09-bare-metal-os-setup-quiz)
* [EKS Hybrid Nodes Gateway 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks-hybrid-nodes/10-hybrid-nodes-gateway-quiz)
### 관련 문서
* [EKS 복원력 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks/10-eks-resiliency.md) - 하이브리드 환경에서의 고가용성 구성
* [EKS 비용 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/eks/07-eks-cost-optimization.md) - 비용 관리 전략
* [EKS 모니터링 및 로깅](https://www.atomai.click/kubernetes-docs/llms/ko/eks/06-eks-monitoring-logging.md) - 통합 모니터링 구성
### 공식 문서
* [AWS EKS Hybrid Nodes 공식 문서](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-overview.html)
* [nodeadm 사용자 가이드](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-nodeadm.html)
* [Harbor 공식 문서](https://goharbor.io/docs/)
* [NVIDIA GPU Operator 문서](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/overview.html)
* [하이브리드 노드 네트워킹 가이드](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-networking.html)
* [하이브리드 노드 CNI 구성](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-cni.html)
* [하이브리드 노드 트러블슈팅](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-troubleshooting.html)
* [Hybrid operating-system compatibility](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-os.html)
* [Hybrid credentials and IAM role](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-creds.html)
* [Host credentials during network disconnection](https://docs.aws.amazon.com/eks/latest/best-practices/hybrid-nodes-host-creds.html)
* [IAM Roles Anywhere CreateSession semantics](https://docs.aws.amazon.com/rolesanywhere/latest/userguide/authentication-create-session.html)
* [EKS pricing](https://aws.amazon.com/eks/pricing/)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-hybrid-nodes/01-prerequisites
----------------------------------------
# 사전 요구 사항
> **지원 버전**: 예제 검토 기준 EKS 1.36 / hybrid nodeadm 1.0.20; OS별 조건은 아래 참조
> **마지막 업데이트**: 2026년 9월 13일
Hybrid node join 전에 host·network·credential·cluster access를 준비합니다. 로컬 schema/crypto/input 검사는 실제 물리 네트워크·GPU runtime·프로덕션 cluster 검증이 아닙니다. 이번 감사에서 cloud·host network·GPU driver·image build 변경은 실행하지 않았습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-prereq-0.html)
## OS와 Runtime
| AWS가 검증하는 hybrid 통합 | 버전/범위 |
|---------------------------|-----------|
| Ubuntu | 20.04·22.04·24.04; 배포판 security maintenance도 별도 검토 |
| RHEL | 8·9; kernel/CNI·Red Hat 구독/지원 검토 |
| AL2023 | On-prem **가상화** 환경; 일반 bare metal이나 EC2 밖 AWS OS 지원 권리가 아님 |
| Bottlerocket | VMware 변형 v1.37.0+, Kubernetes 1.28+용, **x86_64만** 제공; 별도 bootstrap 절차 |
위 기준이 오래된 Kubernetes 릴리스의 현재 EKS 지원을 뜻하지는 않습니다. 현재 지원되는 EKS·OS/runtime/CNI 조합을 고르세요. AWS는 Ubuntu/RHEL의 hybrid 통합을 지원하며 vendor OS 유지보수 자체를 대신하지 않습니다.
SSM 신규 설치/upgrade에는 **nodeadm 1.0.19+**가 필요합니다. 이전 binary에는 오래된 SSM signing key가 있습니다. 검토한 릴리스는 **v1.0.20**이며, EC2 EKS AMI의 동명 도구가 아닌 hybrid nodeadm을 사용합니다.
ARM의 EKS kube-proxy 1.31+에는 ARMv8.2+crypto가 필요합니다. Pi 5 이전/Cortex-A72는 충족하지 못하지만 Pi 5 CPU만으로 전체 stack이 검증되지는 않습니다. 이전 kube-proxy 1.30 workaround는 2026년 7월 EKS extended support가 종료됐습니다.
Containerd는 필요하지만 Docker Engine은 필수가 아닙니다. “containerd 1.6+”나 Docker 버전 확인만으로 현재 CRI 호환성이 입증되지는 않습니다.
| OS/선택 | nodeadm containerd source |
|---------|---------------------------|
| Ubuntu/AL2023 | 기본 지원 source는 `distro` |
| RHEL | `docker` 또는 호환 runtime 사전 설치 후 `none`; `distro`는 무효 |
| AL2023 | `docker`는 미지원 |
| 수동 runtime 설치 | `none`은 설치 생략이며 runtime 제공이 아님 |
Ubuntu 24.04의 과거 Pod 종료/AppArmor 수정에는 containerd 1.7.19+ 또는 적절한 AppArmor 갱신이 필요했습니다. 오래된 버전 설치 권장이 아닙니다. 선택한 CNI의 kernel 요구도 검토하며 “kernel 5.4+”나 무조건적 kernel 교체만으로 해결하지 마세요.
### 용량과 Host 확인
AWS는 **1 vCPU/1 GiB RAM 이상**을 권장하면서 엄격한 보편적 최소값은 없다고 설명합니다. OS/runtime/CNI/agent·image/log·실제 workload 용량을 추가해야 합니다. 이전 퀴즈의 2-core/2GB 최소값은 일치하지 않았습니다.
이전 disk 20/50/100GB, CPU 2/4-core, RAM 4/8GB는 계획 예시이며 검증된 workload 크기가 아닙니다.
```bash
# Run these read-only checks on the intended hybrid host.
cat /etc/os-release
uname -m
uname -r
free -h
df -h /
swapon --show
ip -j link show
# When already installed, inspect the actual CRI runtime:
containerd --version
```
Binary 버전이 kubelet의 실제 runtime socket을 입증하지는 않습니다. Swap·cgroup·forwarding·CNI module·firewall은 host owner 절차로 검토합니다. Blanket swapoff/fstab/sysctl/MTU 변경은 모든 지원 host에 이식 가능한 절차가 아닙니다.
### Install·검증·init 순서
준비된 Ubuntu/AL2023 host에서는 join 전에 의존성을 설치합니다. RHEL은 앞 표의 runtime source를 사용하며 Bottlerocket은 다른 경로를 사용합니다.
```bash
set -euo pipefail
# Installation/bootstrap sequence for prepared Ubuntu/AL2023 hosts.
# RHEL requires --containerd-source docker, or none with a preinstalled compatible runtime.
# These commands mutate the host; review OS/runtime/network/identity first.
sudo nodeadm install 1.36 --credential-provider ssm --containerd-source distro \
--region ap-northeast-2 --timeout 20m
sudo nodeadm config check -c file:///etc/eks/nodeConfig.yaml
sudo nodeadm init -c file:///etc/eks/nodeConfig.yaml
```
Node config에 대상 cluster/provider 입력이 있어야 합니다. Credential이 있는 config는 mode 0600으로 보호하고 activation code를 golden image·log에 넣지 마세요. 이 명령은 host를 변경합니다. 실패를 숨기려고 validation phase를 건너뛰지 마세요. `nodeadm upgrade`는 disruptive 작업이며 workload를 먼저 이동해야 합니다.
## Packer Image 준비
AWS 예제는 Ubuntu 22.04/24.04·RHEL 8/9와 vSphere OVA·QEMU qcow2/raw target을 제공합니다. 검토한 **v1.0.20 template은 EKS 1.36에서 바로 실행할 image pipeline이 아닙니다.**
- `K8S_VERSION` validation이 아직 1.26–1.31만 허용합니다. 검토한 current fork를 유지하고 template 때문에 버전을 낮추지 마세요.
- `NODEADM_ARCH`는 **`amd` 또는 `arm`** 뒤에 `64`를 붙입니다. 이전 `amd64`는 `amd6464`가 됩니다.
- Packer의 `CREDENTIAL_PROVIDER=iam`은 nodeadm의 **`iam-ra`**로 변환됩니다.
- 실제 파일은 `hybrid-nodes-template.pkr.hcl`이며 `general-build.qemu.al2023` source는 없습니다.
- Mutable `releases/latest` 다운로드, vSphere `insecure_connection=true`, 기본 build password와 전역 변수 요구를 검토합니다. RHEL provisioner의 x86 repository는 고정돼 있어 binary 아키텍처만 바꿔도 ARM build가 되지는 않습니다.
| 입력 그룹 | 검토 사항 |
|-----------|-----------|
| 도구 | Packer ≥1.11, vSphere plugin ≥1.4 또는 QEMU 1.x; 검증한 조합 고정 |
| 공통 | `ISO_URL`, 검증한 `ISO_CHECKSUM`, `PKR_SSH_PASSWORD`, `K8S_VERSION`, `NODEADM_ARCH`, `CREDENTIAL_PROVIDER` |
| RHEL | `RH_USERNAME`, `RH_PASSWORD`, `RHEL_VERSION`; 최종 image/log에서 secret 제외 |
| vSphere | Server/user/password, datacenter, cluster, datastore, network, **VSPHERE_OUTPUT_FOLDER** |
| QEMU | **PACKER_OUTPUT_FORMAT** (`qcow2`/`raw`), CPU/가상화 호환성 |
소스에는 유료 AWS AMI builder도 있으며 cloud machine을 hybrid node로 운영하는 지원과는 별개입니다. 유지보수한 template과 결과 image를 검증하세요. 이번 감사에서는 Packer/VM build를 실행하지 않았습니다.
## GPU 호환성
GPU variant·CPU 아키텍처·OS/kernel·지원 NVIDIA driver·container toolkit/runtime·CUDA/framework image·memory를 함께 확인합니다. 이전 driver 525/535/545/550, CUDA 11.8/12.x는 과거 최소값·권장값이 섞여 있으며 보편적인 현재 Hybrid Nodes 요구가 아닙니다.
H100 80GB·H200 141GB·A100 40/80GB·L40S 48GB는 일부 variant 예시이며 전체 인증 목록이 아닙니다. 모든 model에 4GB 최소값을 적용할 수도 없습니다. 올바른 GPU container 실행만을 위해 host CUDA compiler가 필수는 아니고 driver·compiler 버전의 의미도 다릅니다.
```bash
# On a host where the driver is already installed:
nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv,noheader
# Optional: nvcc reports the locally installed compiler, if present.
if command -v nvcc >/dev/null 2>&1; then nvcc --version; fi
```
선택한 조합의 현재 NVIDIA 호환성/platform 안내를 사용하세요. Drain한 test host에서 driver/kernel 변경을 검증합니다. 오래된 driver 설치나 live worker의 containerd 재시작은 무해한 사전 검사가 아닙니다.
## Network·CIDR·MTU
Control plane의 kubelet·hybrid webhook 접근을 포함해 안정적인 **private 양방향 연결**이 필요합니다. Public API는 node→API 경로를 바꾸며 필요한 private 역방향 경로를 없애지 않습니다.
AWS의 **100 Mbps/RTT ≤200ms**는 일반 권장이지 엄격한 보편적 최소값이 아닙니다. 이전 10Gbps/5ms, packet loss 0.1%/0.01%, MTU 1500/9000은 미검증 계획 예시입니다. 실제 수요·encapsulation/path MTU를 시험하며 모든 NIC를 9000으로 바꾸지 마세요.
Remote node/Pod CIDR은 IPv4 RFC1918/CGNAT이며 서로·VPC·service network와 겹치면 안 됩니다. 각 remote 종류의 CIDR 최대 15개를 **하나의** remote-node-network, **하나의** remote-pod-network wrapper에 넣습니다. Cluster 간 Pod routing 분리도 필요합니다.
검토한 `network-plan.json`을 저장합니다.
```json
{
"vpcCidrs": [
"10.0.0.0/16"
],
"serviceCidrs": [
"10.100.0.0/16"
],
"remoteNodeCidrs": [
"10.80.0.0/16"
],
"remotePodCidrs": [
"10.85.0.0/16"
]
}
```
```bash
: "${NETWORK_PLAN_JSON:?Set the reviewed local network plan JSON}"
export NETWORK_PLAN_JSON
python3 - <<'PY'
import ipaddress, itertools, json, os
from pathlib import Path
plan = json.loads(Path(os.environ["NETWORK_PLAN_JSON"]).read_text())
allowed = [ipaddress.ip_network(c) for c in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10")]
groups = {}
for key in ("vpcCidrs", "serviceCidrs", "remoteNodeCidrs", "remotePodCidrs"):
values = plan[key]
if not isinstance(values, list) or not values:
raise SystemExit(f"{key} must be a nonempty list for this example")
groups[key] = [ipaddress.ip_network(v, strict=True) for v in values]
if any(n.version != 4 for n in groups[key]):
raise SystemExit("This Hybrid Nodes plan requires IPv4")
for key in ("remoteNodeCidrs", "remotePodCidrs"):
if len(groups[key]) > 15 or any(not any(n.subnet_of(a) for a in allowed) for n in groups[key]):
raise SystemExit("Remote CIDRs must use RFC1918/CGNAT ranges, at most 15 per kind")
flat = [(key, n) for key, networks in groups.items() for n in networks]
for (ka, a), (kb, other) in itertools.combinations(flat, 2):
if a.overlaps(other):
raise SystemExit(f"Overlapping CIDRs: {ka} {a}, {kb} {other}")
print("CIDR syntax/range/non-overlap checks passed; routing and reachability remain unverified")
PY
```
문법·범위·중첩 검사이지 reachability 검증은 아닙니다. 실제 TGW/VGW의 VPC return route와 on-prem return/per-node Pod route를 구성하세요.
| Flow | 검토 사항 |
|------|-----------|
| Remote node/Pod → API TCP443 | 대상 CIDR의 추가 cluster SG ingress |
| Control-plane ENI → node TCP10250 | 제한된 cluster egress·private route·on-prem host/firewall ingress |
| Control plane → webhook | 실제 Pod IP/port·route·firewall |
| Host/Pod → credential/image/DNS/time | Provider endpoint·registry·DNS·시각 동기화 |
TCP10250은 kubelet에서 API-server SG로 들어오는 ingress가 아닙니다. EKS 단독 API가 모든 remote custom rule을 만들지는 않으며 eksctl은 자신이 소유한 VPC 측 리소스를 자동화할 수 있습니다. 60을 불변 상한으로 보지 말고 실제 quota를 확인하세요.
AWS는 public-only/private-only를 권장합니다. 둘 다 켜면 VPC 밖 node가 public 주소를 해석하므로 경로·접근 규칙에 따라 join이 **실패할 수 있습니다**. API의 무조건적인 거부는 아닙니다. Private-only에는 private DNS·관리자 접근도 필요합니다.
## Provider별 자격 증명
대상 관리자 identity로 AWS 입력을 준비합니다. Activation 정보를 private directory에 저장하세요.
```bash
set -euo pipefail
: "${EXPECTED_ACCOUNT_ID:?Set the intended account}"
: "${AWS_REGION:?Set the intended Region}"
: "${CLUSTER_NAME:?Set the intended cluster}"
check_account() {
local actual
actual=$(aws sts get-caller-identity --region "$AWS_REGION" --query Account --output text) || return
test "$actual" = "$EXPECTED_ACCOUNT_ID" || { printf 'Account mismatch.\n' >&2; return 1; }
}
check_account
umask 077
export WORK_DIR
WORK_DIR=$(mktemp -d "$PWD/hybrid-preflight.XXXXXXXX")
export EXPECTED_ACCOUNT_ID AWS_REGION CLUSTER_NAME
printf 'Private preparation directory: %s\n' "$WORK_DIR"
```
SSM·Roles Anywhere 모두 AWS 연결이 필요합니다. 로컬 CA나 더 긴 cached credential은 offline control plane을 만들지 않습니다.
### SSM Role과 Activation
Role에는 `eks:DescribeCluster`, ECR pull, SSM core/cleanup 권한이 필요합니다. **AmazonEKSWorkerNodeMinimalPolicy만으로 DescribeCluster를 얻지 못합니다.** Pod Identity의 `eks-auth:AssumeRoleForPodIdentity`는 별도 권한입니다.
예시 계정/리전/cluster 값을 일관되게 바꾸세요. 아래 list operation은 instance별 resource scope를 지원하지 않아 wildcard를 쓰며 대상 Region으로 제한합니다.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ssm.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "123456789012"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:ssm:ap-northeast-2:123456789012:*"
}
}
}
]
}
```
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "eks:DescribeCluster",
"Resource": "arn:aws:eks:ap-northeast-2:123456789012:cluster/my-hybrid-cluster"
},
{
"Effect": "Allow",
"Action": "ssm:DescribeInstanceInformation",
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "ap-northeast-2"
}
}
},
{
"Effect": "Allow",
"Action": "ssm:DeregisterManagedInstance",
"Resource": "arn:aws:ssm:ap-northeast-2:123456789012:managed-instance/*",
"Condition": {
"StringEquals": {
"ssm:resourceTag/EKSClusterARN": "arn:aws:eks:ap-northeast-2:123456789012:cluster/my-hybrid-cluster"
}
}
}
]
}
```
검토한 `AmazonEC2ContainerRegistryPullOnly`·`AmazonSSMManagedInstanceCore` 또는 동등 권한을 연결합니다. Activation의 `EKSClusterARN` tag는 deregistration policy와 일치해야 합니다.
의도한 등록 한도·24시간 **등록** 만료를 준비합니다.
```bash
: "${HYBRID_ROLE_NAME:?Use the reviewed SSM-trusting role name}"
: "${REGISTRATION_LIMIT:?Set a deliberate node registration limit}"
export HYBRID_ROLE_NAME REGISTRATION_LIMIT
python3 - <<'PY'
import json, os, re
from datetime import datetime, timedelta, timezone
from pathlib import Path
account, region, cluster = (os.environ[k] for k in ("EXPECTED_ACCOUNT_ID", "AWS_REGION", "CLUSTER_NAME"))
if not re.fullmatch(r"\d{12}", account) or not re.fullmatch(r"[a-z]{2}(?:-[a-z]+)+-\d", region):
raise SystemExit("Invalid account/Region")
if region.startswith(("cn-", "us-gov-")):
raise SystemExit("Hybrid Nodes is not available in this Region family")
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,99}", cluster):
raise SystemExit("Invalid cluster name")
role = os.environ["HYBRID_ROLE_NAME"]
if not re.fullmatch(r"[\w+=,.@-]{1,64}", role, re.ASCII):
raise SystemExit("Use the actual IAM role name, not a role ARN")
limit = int(os.environ["REGISTRATION_LIMIT"])
if not 1 <= limit <= 1000:
raise SystemExit("RegistrationLimit must be 1..1000 per activation; review service quotas separately")
body = {"DefaultInstanceName": "eks-hybrid-node", "IamRole": role, "RegistrationLimit": limit,
"Description": "Reviewed EKS hybrid activation",
"Tags": [{"Key": "EKSClusterARN", "Value": f"arn:aws:eks:{region}:{account}:cluster/{cluster}"}],
"ExpirationDate": (datetime.now(timezone.utc) + timedelta(hours=24)).isoformat()}
(Path(os.environ["WORK_DIR"]) / "activation-request.json").write_text(json.dumps(body, indent=2) + "\n")
PY
```
```bash
set -euo pipefail
# Run only after the role/trust/tag policy and registration scope are prepared.
check_account
aws ssm create-activation --region "$AWS_REGION" \
--cli-input-json "file://$WORK_DIR/activation-request.json" --output json \
> "$WORK_DIR/activation-response.json"
chmod 600 "$WORK_DIR/activation-response.json"
# The response contains the secret ActivationCode. Do not print or commit it.
```
ActivationCode는 한 번 반환됩니다. ActivationId와 함께 안전하게 전달하세요. Activation 만료/삭제는 기존 managed instance의 deregistration이 아닙니다. [CreateActivation RegistrationLimit](https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_CreateActivation.html)은 activation별 1–1,000 범위를 유지합니다. 전체 fleet의 무료 구간 한도가 아니며, 현재 요금 조건은 [자격 증명 provider 비교](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md)에서 설명합니다.
SSM node name은 `mi-...`, credential 수명은 고정 1시간입니다. Network 복구 후에도 refresh backoff로 재연결이 지연될 수 있습니다. Credential을 log에 출력하지 마세요.
### Roles Anywhere Trust와 수명
Provider에 맞는 role·trust anchor·profile을 사용합니다. Session name을 인증서 identity/nodeName과 연결하며 이전 PrincipalTag/RequestTag 비교만으로는 강제되지 않았습니다.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "rolesanywhere.amazonaws.com"
},
"Action": [
"sts:TagSession",
"sts:SetSourceIdentity"
],
"Condition": {
"ArnEquals": {
"aws:SourceArn": "arn:aws:rolesanywhere:ap-northeast-2:123456789012:trust-anchor/11111111-2222-3333-4444-555555555555"
}
}
},
{
"Effect": "Allow",
"Principal": {
"Service": "rolesanywhere.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"ArnEquals": {
"aws:SourceArn": "arn:aws:rolesanywhere:ap-northeast-2:123456789012:trust-anchor/11111111-2222-3333-4444-555555555555"
},
"StringEquals": {
"sts:RoleSessionName": "${aws:PrincipalTag/x509Subject/CN}"
}
}
}
]
}
```
```json
{
"name": "hybrid-node-profile",
"roleArns": [
"arn:aws:iam::123456789012:role/EKSHybridNodeRole"
],
"enabled": true,
"acceptRoleSessionName": true,
"durationSeconds": 3600
}
```
Anchor ARN을 바꾸고 scoped DescribeCluster/ECR 권한과 **acceptRoleSessionName=true**를 구성합니다.
```text
effectiveDuration = min(profileDuration, requestedDuration)
or profileDuration when the request omits duration
effectiveDuration <= role.MaxSessionDuration
```
Request/profile duration은 900–43,200초입니다. CreateSession은 **같은 값도 허용**합니다. Profile 3,600·override 없음·role maximum 3,600은 유효합니다. 이전 “더 커야 함”이라는 엄격한 표현은 부정확했습니다. 인증서 유효성/revocation·AWS 연결은 여전히 필요합니다.
### Node별 Key와 인증서
대상 새 node의 private directory에서 고유 key/CSR을 만듭니다. 예제는 단순 lowercase DNS label을 사용합니다. CSR을 승인된 issuer에 전달하고 CA signing key는 node에 두지 마세요.
```bash
set -euo pipefail
umask 077
: "${WORK_DIR:?Set a private preparation directory on this node}"
: "${NODE_NAME:?Use a unique certificate CN / node name}"
export NODE_NAME
python3 - <<'PY'
import os, re
name = os.environ["NODE_NAME"]
if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,61}[a-z0-9]", name):
raise SystemExit("This example requires a lowercase DNS label of 2..63 characters")
PY
test ! -e "$WORK_DIR/node.key"
test ! -e "$WORK_DIR/node.csr"
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out "$WORK_DIR/node.key"
chmod 600 "$WORK_DIR/node.key"
openssl req -new -key "$WORK_DIR/node.key" -out "$WORK_DIR/node.csr" -subj "/CN=$NODE_NAME"
# Send the CSR to the approved PKI issuer; do not copy its CA private key to nodes.
```
발급 chain·승인된 CA·유효성/revocation·key 일치·CN을 확인하고 시각을 동기화합니다. 새 node에만 설치하세요.
```bash
set -euo pipefail
: "${ISSUED_NODE_CERT:?Set the verified certificate/chain issued for this node}"
: "${WORK_DIR:?Set the private directory containing the node key}"
# New-node installation only; do not overwrite an existing identity.
sudo test ! -e /etc/iam/pki/server.key
sudo test ! -e /etc/iam/pki/server.pem
sudo install -d -m 0700 /etc/iam/pki
sudo install -m 0644 "$ISSUED_NODE_CERT" /etc/iam/pki/server.pem
sudo install -m 0600 "$WORK_DIR/node.key" /etc/iam/pki/server.key
```
EKS Kubernetes API CA 파일과 다릅니다. Node identity를 image에 복제하거나 기존 key를 조용히 덮어쓰면 안 됩니다.
## CloudFormation 준비
고정된 v1.0.20 template은 provisioning의 출발점입니다. SSM template은 role을 만들며 **activation은 만들지 않습니다**. 여러 cluster에 쓰기 전 넓은 DescribeCluster scope와 고정 export 이름을 검토하세요.
IRA의 `CertAttributeTrustPolicy` 값은 `"CN"`이 아닌 literal **`${aws:PrincipalTag/x509Subject/CN}`**입니다. Python `cryptography` helper는 PEM을 정확히 serialize하고 기본 CA 형태·현재 유효성을 확인하며 issuer 권한·revocation 검증은 아닙니다.
```bash
: "${CA_PEM_FILE:?Set the approved CA certificate file, not a private key}"
: "${ROLE_NAME:?Set the new role name selected by the IaC owner}"
export CA_PEM_FILE ROLE_NAME
python3 - <<'PY'
import json, os, re
from pathlib import Path
from cryptography import x509
from datetime import datetime, timezone
raw = Path(os.environ["CA_PEM_FILE"]).read_bytes()
if b"PRIVATE KEY" in raw or raw.count(b"-----BEGIN CERTIFICATE-----") != 1:
raise SystemExit("Provide one reviewed CA certificate")
cert = x509.load_pem_x509_certificate(raw)
if not cert.extensions.get_extension_for_class(x509.BasicConstraints).value.ca:
raise SystemExit("Certificate is not a CA")
now = datetime.now(timezone.utc)
if not cert.not_valid_before.replace(tzinfo=timezone.utc) <= now <= cert.not_valid_after.replace(tzinfo=timezone.utc):
raise SystemExit("CA certificate is not currently valid")
role = os.environ["ROLE_NAME"]
if not re.fullmatch(r"[\w+=,.@-]{1,64}", role, re.ASCII):
raise SystemExit("Invalid IAM role name")
body = [
{"ParameterKey": "RoleName", "ParameterValue": role},
{"ParameterKey": "CertAttributeTrustPolicy", "ParameterValue": "${aws:PrincipalTag/x509Subject/CN}"},
{"ParameterKey": "CABundleCert", "ParameterValue": raw.decode("ascii")}
]
(Path(os.environ["WORK_DIR"]) / "cfn-iamra-parameters.json").write_text(json.dumps(body, indent=2) + "\n")
PY
```
고정 template의 parameter key/AllowedValues를 대조하고 IaC owner가 policy scope·export 이름을 정한 뒤 change set을 검토합니다. 잘못된 PEM shorthand나 미검토 mutable-main template을 배포하지 마세요.
## Cluster Access와 생성 계획
Node IAM role에는 **HYBRID_LINUX access entry**를 권장합니다.
```json
{
"clusterName": "my-hybrid-cluster",
"principalArn": "arn:aws:iam::123456789012:role/EKSHybridNodeRole",
"type": "HYBRID_LINUX"
}
```
Mapping은 `system:node:{{SessionName}}`과 node-bootstrap group을 사용합니다. API/API_AND_CONFIG_MAP 인증·소유권을 확인합니다. 기존 mapping을 지우는 한-role `aws-auth` ConfigMap을 적용하지 마세요. Legacy mapping 변경은 통제된 이전이 필요합니다.
아래 모든 ID를 검토한 실제 리소스로 바꿉니다. Private-only endpoint·IPv4·1.36이 명시돼 있습니다. Bootstrap creator admin 권한은 통제된 lab용이며 production operator 접근은 의도적으로 선택하세요.
eksctl 0.229.0에서는 문서화된 **SSM/IRA**를 사용합니다. RoleARN을 제공한다면 provider 리소스도 별도로 준비합니다.
```yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-hybrid-cluster
region: ap-northeast-2
version: '1.36'
iam:
serviceRoleARN: arn:aws:iam::123456789012:role/EKSClusterRole
accessConfig:
authenticationMode: API_AND_CONFIG_MAP
bootstrapClusterCreatorAdminPermissions: true
kubernetesNetworkConfig:
ipFamily: IPv4
serviceIPv4CIDR: 10.100.0.0/16
vpc:
id: vpc-0123456789abcdef0
subnets:
private:
ap-northeast-2a:
id: subnet-0123456789abcdef0
ap-northeast-2c:
id: subnet-0123456789abcdef1
controlPlaneSecurityGroupIDs:
- sg-0123456789abcdef0
clusterEndpoints:
privateAccess: true
publicAccess: false
remoteNetworkConfig:
iam:
provider: SSM
roleARN: arn:aws:iam::123456789012:role/EKSHybridNodeRole
vpcGatewayID: tgw-0123456789abcdef0
remoteNodeNetworks:
- cidrs:
- 10.80.0.0/16
remotePodNetworks:
- cidrs:
- 10.85.0.0/16
```
eksctl provisioning 시 `--without-nodegroup`과 명시적인 private kubeconfig 경로를 사용하세요. Schema 유효성은 IAM·route·gateway reachability 증거가 아닙니다. 예전 eksctl의 launch-only 문서는 오래됐으며 현재 EKS API는 기존 cluster의 hybrid remote-network 구성도 지원합니다.
대응하는 EKS CreateCluster 요청입니다.
```json
{
"name": "my-hybrid-cluster",
"version": "1.36",
"roleArn": "arn:aws:iam::123456789012:role/EKSClusterRole",
"resourcesVpcConfig": {
"subnetIds": [
"subnet-0123456789abcdef0",
"subnet-0123456789abcdef1"
],
"securityGroupIds": [
"sg-0123456789abcdef0"
],
"endpointPrivateAccess": true,
"endpointPublicAccess": false
},
"kubernetesNetworkConfig": {
"ipFamily": "ipv4",
"serviceIpv4Cidr": "10.100.0.0/16"
},
"accessConfig": {
"authenticationMode": "API_AND_CONFIG_MAP",
"bootstrapClusterCreatorAdminPermissions": true
},
"remoteNetworkConfig": {
"remoteNodeNetworks": [
{
"cidrs": [
"10.80.0.0/16"
]
}
],
"remotePodNetworks": [
{
"cidrs": [
"10.85.0.0/16"
]
}
]
}
}
```
한 infrastructure owner를 선택하며 두 경로를 모두 실행하지 않습니다. 실제 cluster/endpoint identity·operator 권한·node access 확인 후 join합니다. 예시 AWS ID는 검증된 리소스가 아닙니다.
## Add-on
아래 공식 hybrid 호환 기준은 모든 Kubernetes 버전의 **설치 target이 아닙니다**.
| Add-on | Hybrid 호환 기준 |
|--------|-------------------|
| kube-proxy/CoreDNS | 1.25.14-eksbuild.2 / 1.9.3-eksbuild.7 |
| ADOT/CloudWatch Observability | 0.102.1-eksbuild.2 / 2.2.1-eksbuild.1 |
| Pod Identity Agent | 일반 1.3.3-eksbuild.1, **Bottlerocket 1.3.7-eksbuild.2**; **Bottlerocket OS 1.39.0 이상**도 필요 |
| Node monitoring/snapshot controller | 1.2.0-eksbuild.1 / 8.1.0-eksbuild.2 |
| Private CA Connector/FSx CSI/Secrets Store provider | 1.6.0-eksbuild.1 / 1.7.0-eksbuild.1 / 2.1.1-eksbuild.1 |
| Metrics Server/cert-manager | 0.7.2-eksbuild.1 / 1.17.2-eksbuild.1 |
| Node Exporter/kube-state-metrics/External DNS | 1.9.1-eksbuild.2 / 2.15.0-eksbuild.4 / 0.19.0-eksbuild.1 |
```bash
check_account
: "${ADDON_NAME:?Select one add-on to review}"
aws eks describe-addon-versions --region "$AWS_REGION" --addon-name "$ADDON_NAME" \
--kubernetes-version 1.36 --output json > "$WORK_DIR/addon-catalog.json"
jq '[.addons[].addonVersions[] |
{addonVersion,architecture,computeTypes,compatibilities}]' "$WORK_DIR/addon-catalog.json"
```
선택한 버전의 hybrid/OS/kernel 설정을 검토합니다. VPC CNI는 hybrid node를 관리하지 않으므로 호환 CNI·cloud-only agent placement를 구성합니다. Catalog 존재는 설치·Ready 증거가 아닙니다.
## 참고 자료
- [Hybrid prerequisites](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-prereqs.html)
- [Operating systems and nodeadm minimum](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-os.html)
- [nodeadm reference](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-nodeadm.html)
- [Hybrid networking](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-networking.html)
- [Credentials and Hybrid Nodes IAM role](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-creds.html)
- [Host credentials during disconnections](https://docs.aws.amazon.com/eks/latest/best-practices/hybrid-nodes-host-creds.html)
- [IAM Roles Anywhere CreateSession](https://docs.aws.amazon.com/rolesanywhere/latest/userguide/authentication-create-session.html)
- [Supported hybrid add-ons](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-add-ons.html)
- [eksctl hybrid configuration](https://docs.aws.amazon.com/eks/latest/eksctl/hybrid-nodes.html)
- [Released nodeadm v1.0.20](https://github.com/aws/eks-hybrid/releases/tag/v1.0.20)
- [Pinned Packer source](https://github.com/aws/eks-hybrid/tree/v1.0.20/example/packer)
- [Pinned SSM CloudFormation template](https://github.com/aws/eks-hybrid/blob/v1.0.20/example/hybrid-ssm-cfn.yaml)
- [Pinned Roles Anywhere CloudFormation template](https://github.com/aws/eks-hybrid/blob/v1.0.20/example/hybrid-ira-cfn.yaml)
- [NVIDIA CUDA compatibility](https://docs.nvidia.com/deploy/cuda-compatibility/)
- [NVIDIA Container Toolkit installation](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
< [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: 네트워크 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/02-network-configuration.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-hybrid-nodes/02-network-configuration
----------------------------------------
# 네트워크 구성
> **지원 버전**: EKS 1.36 예제; AWS-maintained Cilium 1.18.3-0 기준, 호환 host/kernel 필요
> **마지막 업데이트**: 2026년 9월 12일
Routing·DNS·TLS·credential·앱 트래픽을 별도로 검증합니다. 아래는 Terraform mock provider 등을 사용해 로컬 schema/fixture로 확인한 예제이며 AWS 리소스·router·firewall·실제 cluster를 변경하지 않았습니다. 그림은 AWS 개념을 바탕으로 이 저장소에서 제작했으며 AWS가 이 구성을 검증한 결과물이 아닙니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-prereq-0.html)
## 네트워크 아키텍처 개요
Control-plane→hybrid node와 private Kubernetes API 트래픽은 cluster VPC 경로를 사용합니다. Public API endpoint로 가는 kubelet 트래픽은 설정된 public 경로를 사용하므로 “모든 트래픽이 항상 VPC ENI 경유”라는 설명은 과도했습니다. Direct Connect public VIF·private 연결·public internet 경로를 구분하세요.
Control-plane ENI/IP는 바뀔 수 있습니다. 공유 VPC의 모든 `Amazon EKS*` ENI를 이 cluster 것으로 간주하지 말고 실제 소유권·승인된 control-plane subnet 범위를 확인합니다.
읽기 전용 진단 전에 계정·Kubernetes context를 확인합니다.
```bash
set -euo pipefail
: "${EXPECTED_ACCOUNT_ID:?Set the intended account}"
: "${AWS_REGION:?Set the cluster Region}"
: "${CLUSTER_NAME:?Set the reviewed cluster name}"
: "${KUBECONFIG:?Set the reviewed kubeconfig}"
export KUBECONFIG KUBE_CONTEXT="${KUBE_CONTEXT:-$CLUSTER_NAME}"
check_account() {
local account
account=$(aws sts get-caller-identity --region "$AWS_REGION" --query Account --output text) || return
test "$account" = "$EXPECTED_ACCOUNT_ID" || { printf 'Account mismatch.\n' >&2; return 1; }
}
check_account
umask 077
export WORK_DIR
WORK_DIR=$(mktemp -d "$PWD/hybrid-network.XXXXXXXX")
aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" --output json \
> "$WORK_DIR/cluster.json"
endpoint=$(kubectl --context "$KUBE_CONTEXT" config view --minify \
-o jsonpath='{.clusters[0].cluster.server}')
jq -e --arg endpoint "$endpoint" '
.cluster.status=="ACTIVE" and .cluster.endpoint==$endpoint and
(.cluster.remoteNetworkConfig.remoteNodeNetworks|length)>0
' "$WORK_DIR/cluster.json" >/dev/null
printf 'Private diagnostics: %s\n' "$WORK_DIR"
```
## CIDR 범위 요구 사항
Remote node/Pod는 겹치지 않는 IPv4 **RFC1918 또는 CGNAT**이며 VPC·Service CIDR과도 분리합니다. 각 remote 종류는 최대 15개 CIDR을 지원합니다. 현재 EKS API는 기존 cluster의 remote-network 구성도 지원하므로 생성 시점 전용이 아닙니다.
| Network 동작 | 의미 |
|--------------|------|
| Routable Pod IP | 승인된 route를 통해 cloud/control-plane client가 Pod IP로 연결 시작 가능 |
| Masqueraded egress | SNAT는 Pod가 시작한 연결의 return path를 제공할 수 있지만 신규 inbound 연결을 자동 허용하지 않음 |
| Unroutable Pod network | 직접 cloud→Pod 통신에는 다른 지원 경로가 필요; 일반적인 구성에서는 cloud-hosted webhook/API service 사용 |
Unroutable이 Pod의 모든 AWS API 호출 불가를 뜻하지는 않습니다. 직접 hybrid/cloud Pod 통신·hybrid webhook에는 실제 Pod route가 필요합니다. Gateway/proxy 대안은 별도 요구 사항을 확인하세요.
## 필수 방화벽 포트
| Flow | Protocol/port |
|------|---------------|
| Node/Pod → Kubernetes API | 실제 cluster endpoint TCP443 |
| Control-plane ENI → kubelet | 인증·권한 검사를 포함한 TCP10250 |
| Control plane → webhook/aggregated API Pod | 실제 설정 TCP port; 일반적인 “8443+” 범위 아님 |
| DNS client ↔ 실제 resolver | UDP/TCP53·stateful return traffic |
| 참여 node 간 Cilium VXLAN | UDP8472 |
| 선택하고 지원 범위를 검토한 Cilium Geneve | UDP6081 |
| Cilium health check | TCP4240과 필요한 ICMP/health endpoint 접근 |
| BGP node↔router | 설정한 active/passive peer의 TCP179 |
| VPN gateway transport | UDP500/4500 및 해당 IPsec transport 요구 |
| 앱/AWS credential·registry service | 실제 목적지·port만 |
Network owner가 connection tracking·기존 rule을 보존하며 변경합니다. 이전의 광범위한 `10.0.0.0/8` INPUT, 무제한 DNS/VXLAN, 전체 ruleset 저장은 재사용할 안전한 정책이 아니었습니다. 인증 없는 kubelet 10255를 현대적인 선택 요구로 열지 마세요.
## AWS 엔드포인트 접근
**EKS 관리 API PrivateLink endpoint와 Kubernetes API server endpoint는 다릅니다.**
| `com.amazonaws..*` 서비스 suffix | 용도/필요한 경우 |
|------------------------------------------|------------------|
| `eks` | DescribeCluster 등 AWS EKS 관리 API |
| `eks-auth` | EKS Pod Identity 사용 시 |
| `ecr.api`, `ecr.dkr` | Private ECR API/registry; image layer에는 S3 접근도 필요 |
| `s3` | Private S3; on-prem은 VPC gateway endpoint를 직접 사용할 수 없음 |
| `ssm`, 해당 SSM messaging service | SSM credential/management 기능 |
| `rolesanywhere` | 선택 provider가 IAM Roles Anywhere일 때 |
| `sts` | 실제 client STS/IRSA/AssumeRole 호출; 로컬 EKS token 서명 자체는 client의 STS network 요청이 아님 |
| `logs`, `monitoring` 등 선택 서비스 | 해당 agent/workload가 호출할 때 |
| `oidc-eks` | 지원 리전의 현재 EKS OIDC discovery/JWKS PrivateLink |
| `eks-proxy` | AWS console resource view용이며 공개 application SDK/API가 아님 |
대상 리전의 서비스 가용성을 확인합니다. Private ECR endpoint가 **public ECR**, CloudFront, 임의 package repository를 private으로 만들지는 않습니다. AWS Cilium OCI chart의 public ECR도 승인된 접근·mirror 배포 경로가 필요합니다.
OIDC discovery/JWKS는 익명 public-key 데이터입니다. `oidc-eks`는 default full-access endpoint policy만 허용합니다. Reachability는 SG/route, role authorization은 IAM trust의 `aud`/`sub`로 제어하세요. STS의 IRSA 검증은 이 endpoint와 독립적으로 AWS 내부에서 수행됩니다.
Roles Anywhere CreateSession endpoint policy의 principal은 인증서 인증 전 평가 때문에 `*`여야 합니다. 문서에 따라 승인된 trust-anchor resource·지원 certificate condition으로 제한합니다. 서로 다른 서비스에 일반 policy 하나를 복사하지 마세요. Endpoint policy는 통과 트래픽의 필터이며 IAM/role trust를 대체하거나 public service endpoint 전체를 끄지 않습니다.
```bash
check_account
vpc_id=$(jq -er '.cluster.resourcesVpcConfig.vpcId' "$WORK_DIR/cluster.json")
aws ec2 describe-vpc-endpoints --region "$AWS_REGION" \
--filters "Name=vpc-id,Values=$vpc_id" --output json |
jq '[.VpcEndpoints[]|{id:.VpcEndpointId,service:.ServiceName,type:.VpcEndpointType,
state:.State,privateDNS:.PrivateDnsEnabled,dnsOptions:.DnsOptions,
subnets:.SubnetIds,groups:.Groups,dnsEntries:.DnsEntries}]'
```
### S3 Private DNS와 Artifact 배포
S3 **interface endpoint는 private DNS를 지원합니다**. Inbound-Resolver-only 옵션은 on-prem query에 interface를, VPC 내부 트래픽에는 필요한 S3 gateway endpoint를 사용합니다. 옵션을 켠 동안 gateway를 유지하세요. 옵션을 해제하면 해당 S3 트래픽을 interface endpoint로 보낼 수 있습니다.
Private DNS는 TLS rewrite가 아닙니다. `hybrid-assets.eks.amazonaws.com`을 S3 endpoint로 PHZ/CNAME 매핑해도 S3가 CloudFront hostname의 인증서나 object/Host routing을 갖지는 않습니다. TLS 검증을 끄지 마세요. 지원 artifact 준비/client 설정 경로, 자체 hostname/certificate의 승인된 mirror, 검증된 image에 미리 설치한 의존성을 사용합니다.
## VPC 프라이빗 엔드포인트 (에어갭/프라이빗 환경)
여기서 “에어갭”은 필요한 AWS 연결을 유지하며 인터넷 접근을 제한한다는 뜻이지 disconnected cluster가 아닙니다.
다음 완전한 Terraform 예제는 기존 VPC·endpoint subnet·TGW를 사용합니다. VPN/DX 회선·TGW attachment·on-prem route·EKS private DNS 구성을 만들지는 않습니다. VPC DNS support/hostnames와 실제 AZ 분리를 확인하세요. 서로 다른 subnet ID 두 개만으로 AZ 다양성이 입증되지는 않습니다.
원 infrastructure owner를 사용하고 기존 리소스를 import/adopt한 뒤 변경을 계획합니다. 기본 endpoint 집합은 SSM 예시이므로 provider/workload에 맞게 바꿉니다. Endpoint/Resolver ENI에는 요금이 발생합니다. Provider account guard·제한된 ingress가 있으며 응답 트래픽은 stateful SG tracking을 사용합니다.
```hcl
terraform {
required_version = ">= 1.9, < 2.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.64.0"
}
}
}
provider "aws" {
region = var.region
allowed_account_ids = [var.expected_account_id]
}
variable "expected_account_id" {
type = string
validation {
condition = can(regex("^[0-9]{12}$", var.expected_account_id))
error_message = "Set the reviewed 12-digit account ID."
}
}
variable "region" {
type = string
}
variable "name_prefix" {
type = string
default = "hybrid-network"
}
variable "vpc_id" {
type = string
}
variable "endpoint_subnet_ids" {
type = set(string)
validation {
condition = length(var.endpoint_subnet_ids) >= 2
error_message = "Provide subnets in at least two verified Availability Zones."
}
}
variable "s3_gateway_route_table_ids" {
type = set(string)
validation {
condition = length(var.s3_gateway_route_table_ids) > 0
error_message = "Provide the reviewed VPC route tables for the S3 gateway endpoint."
}
}
variable "client_ipv4_cidrs" {
type = set(string)
validation {
condition = length(var.client_ipv4_cidrs) > 0 && alltrue([
for c in var.client_ipv4_cidrs : can(cidrnetmask(c)) && c != "0.0.0.0/0"
])
error_message = "Provide scoped IPv4 CIDRs for the actual VPC/on-premises clients."
}
}
variable "onprem_dns_client_cidrs" {
type = set(string)
validation {
condition = length(var.onprem_dns_client_cidrs) > 0 && alltrue([
for c in var.onprem_dns_client_cidrs : can(cidrnetmask(c)) && c != "0.0.0.0/0"
])
error_message = "Scope inbound DNS to the actual on-premises resolvers."
}
}
variable "onprem_dns_servers" {
type = set(string)
validation {
condition = length(var.onprem_dns_servers) > 0 && alltrue([
for ip in var.onprem_dns_servers : can(cidrnetmask("${ip}/32"))
])
error_message = "Provide actual IPv4 addresses of the on-premises DNS servers."
}
}
variable "onprem_domain" {
type = string
default = "corp.example.internal"
validation {
condition = length(var.onprem_domain) <= 253 && length(split(".", trimsuffix(var.onprem_domain, "."))) >= 2 && alltrue([
for label in split(".", trimsuffix(var.onprem_domain, ".")) :
length(label) <= 63 && can(regex("^[A-Za-z0-9](https://raw.githubusercontent.com/Atom-oh/kubernetes-docs/main/ko/eks-hybrid-nodes/[A-Za-z0-9-]*[A-Za-z0-9])?$", label))
]) && !can(regex("(^|\\.)(amazonaws\\.com|api\\.aws|cluster\\.local)\\.?$", lower(var.onprem_domain)))
error_message = "Use a specific owned DNS suffix; do not forward root, AWS or Kubernetes service zones back to on-premises."
}
}
variable "interface_services" {
type = set(string)
default = ["eks", "ecr.api", "ecr.dkr", "ssm", "ssmmessages"]
validation {
condition = !contains(var.interface_services, "s3")
error_message = "S3 has its own gateway/interface configuration below."
}
}
variable "endpoint_policy_json" {
type = map(string)
default = {}
validation {
condition = !contains(keys(var.endpoint_policy_json), "oidc-eks") && alltrue([
for policy in values(var.endpoint_policy_json) : can(jsondecode(policy))
])
error_message = "Use valid service-specific JSON policies; oidc-eks supports only its default full-access policy."
}
}
variable "controlplane_route_table_ids" {
type = set(string)
}
variable "remote_ipv4_cidrs" {
type = set(string)
validation {
condition = alltrue([
for c in var.remote_ipv4_cidrs : can(cidrnetmask(c)) && c != "0.0.0.0/0"
])
error_message = "Use reviewed remote node, Pod and required DNS/service IPv4 CIDRs."
}
}
variable "existing_transit_gateway_id" {
type = string
}
```
```hcl
# Import/adopt existing resources through their owner before using this example.
# The existing VPC must have DNS support/hostnames and working hybrid routes.
resource "aws_security_group" "endpoints" {
name_prefix = "${var.name_prefix}-vpce-"
description = "HTTPS clients for interface endpoints"
vpc_id = var.vpc_id
}
resource "aws_vpc_security_group_ingress_rule" "endpoint_https" {
for_each = var.client_ipv4_cidrs
security_group_id = aws_security_group.endpoints.id
cidr_ipv4 = each.value
ip_protocol = "tcp"
from_port = 443
to_port = 443
}
resource "aws_vpc_endpoint" "service" {
for_each = var.interface_services
vpc_id = var.vpc_id
service_name = "com.amazonaws.${var.region}.${each.value}"
vpc_endpoint_type = "Interface"
private_dns_enabled = true
subnet_ids = var.endpoint_subnet_ids
security_group_ids = [aws_security_group.endpoints.id]
policy = lookup(var.endpoint_policy_json, each.key, null)
tags = { Name = "${var.name_prefix}-${each.key}" }
}
# S3 inbound-Resolver-only private DNS requires this gateway endpoint.
resource "aws_vpc_endpoint" "s3_gateway" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.${var.region}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = var.s3_gateway_route_table_ids
tags = { Name = "${var.name_prefix}-s3-gateway" }
}
resource "aws_vpc_endpoint" "s3_interface" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.${var.region}.s3"
vpc_endpoint_type = "Interface"
private_dns_enabled = true
subnet_ids = var.endpoint_subnet_ids
security_group_ids = [aws_security_group.endpoints.id]
dns_options {
private_dns_only_for_inbound_resolver_endpoint = true
}
depends_on = [aws_vpc_endpoint.s3_gateway]
tags = { Name = "${var.name_prefix}-s3-interface" }
}
resource "aws_security_group" "dns_inbound" {
name_prefix = "${var.name_prefix}-dns-in-"
description = "DNS from on-premises resolvers"
vpc_id = var.vpc_id
}
resource "aws_security_group" "dns_outbound" {
name_prefix = "${var.name_prefix}-dns-out-"
description = "DNS to reviewed on-premises resolvers"
vpc_id = var.vpc_id
}
locals {
inbound_dns_rules = {
for pair in setproduct(var.onprem_dns_client_cidrs, toset(["tcp", "udp"])) :
"${pair[0]}-${pair[1]}" => { cidr = pair[0], protocol = pair[1] }
}
outbound_dns_rules = {
for pair in setproduct(var.onprem_dns_servers, toset(["tcp", "udp"])) :
"${pair[0]}-${pair[1]}" => { ip = pair[0], protocol = pair[1] }
}
}
resource "aws_vpc_security_group_ingress_rule" "dns" {
for_each = local.inbound_dns_rules
security_group_id = aws_security_group.dns_inbound.id
cidr_ipv4 = each.value.cidr
ip_protocol = each.value.protocol
from_port = 53
to_port = 53
}
resource "aws_vpc_security_group_egress_rule" "dns" {
for_each = local.outbound_dns_rules
security_group_id = aws_security_group.dns_outbound.id
cidr_ipv4 = "${each.value.ip}/32"
ip_protocol = each.value.protocol
from_port = 53
to_port = 53
}
resource "aws_route53_resolver_endpoint" "inbound" {
name = "${var.name_prefix}-inbound"
direction = "INBOUND"
resolver_endpoint_type = "IPV4"
security_group_ids = [aws_security_group.dns_inbound.id]
dynamic "ip_address" {
for_each = var.endpoint_subnet_ids
content {
subnet_id = ip_address.value
}
}
}
resource "aws_route53_resolver_endpoint" "outbound" {
name = "${var.name_prefix}-outbound"
direction = "OUTBOUND"
resolver_endpoint_type = "IPV4"
security_group_ids = [aws_security_group.dns_outbound.id]
dynamic "ip_address" {
for_each = var.endpoint_subnet_ids
content {
subnet_id = ip_address.value
}
}
}
resource "aws_route53_resolver_rule" "onprem" {
domain_name = var.onprem_domain
name = "${var.name_prefix}-onprem"
rule_type = "FORWARD"
resolver_endpoint_id = aws_route53_resolver_endpoint.outbound.id
dynamic "target_ip" {
for_each = var.onprem_dns_servers
content {
ip = target_ip.value
port = 53
}
}
}
resource "aws_route53_resolver_rule_association" "onprem" {
resolver_rule_id = aws_route53_resolver_rule.onprem.id
vpc_id = var.vpc_id
}
output "inbound_resolver_ips" {
value = [for address in aws_route53_resolver_endpoint.inbound.ip_address : address.ip]
}
# VPC return routes only. Existing TGW attachment routes/propagation and
# on-premises routing must be managed separately by their infrastructure owner.
locals {
remote_routes = {
for pair in setproduct(var.controlplane_route_table_ids, var.remote_ipv4_cidrs) :
"${pair[0]}-${pair[1]}" => { table = pair[0], cidr = pair[1] }
}
}
resource "aws_route" "hybrid" {
for_each = local.remote_routes
route_table_id = each.value.table
destination_cidr_block = each.value.cidr
transit_gateway_id = var.existing_transit_gateway_id
# A VGW topology uses gateway_id instead; do not set both target fields.
}
```
S3 gateway 의존성이 명시돼 있습니다. `remote_ipv4_cidrs`에는 node/Pod 외에 필요한 on-prem DNS/service 대역도 넣습니다. 예시 DNS `192.168.1.10/11`에는 승인된 `192.168.1.0/24` 또는 해당 host route가 필요합니다. VGW return route에는 `transit_gateway_id` 대신 `gateway_id`를 사용하며 두 target을 동시에 설정하거나 TGW ID를 VGW/gateway 필드에 넣지 마세요. VPC route만으로 TGW/VPN/on-prem 전체 routing이 완성되지는 않습니다.
## DNS 구성
On-prem resolver에서 선택한 AWS/service·실제 cluster endpoint 이름을 Route 53 Resolver inbound IP로 조건부 전달할 수 있습니다. 실제 endpoint가 반환한 IP를 사용하세요. `amazonaws.com` 전체 전달은 다른 서비스에도 영향을 주므로 zone을 의도적으로 고르고 loop를 피합니다.
```text
// Example service zones only. Replace these Resolver IPs with actual outputs.
zone "eks.ap-northeast-2.amazonaws.com" {
type forward;
forward only;
forwarders { 10.0.1.10; 10.0.2.10; };
};
zone "s3.ap-northeast-2.amazonaws.com" {
type forward;
forward only;
forwarders { 10.0.1.10; 10.0.2.10; };
};
```
이 BIND 조각은 표시한 service zone용이며 Kubernetes API hostname을 자동으로 포함하지 않습니다. 실제 cluster endpoint 이름/suffix와 필요한 서비스 이름을 추가하세요. Resolver outbound의 on-prem zone은 선택 DNS server에서 authoritative/reachable해야 하며 같은 loop로 다시 전달하면 안 됩니다.
### CoreDNS 커스텀 도메인 구성
CoreDNS 직접 forwarding을 선택했다면 기존 Corefile에 검토한 server block을 병합합니다. 관리형 ConfigMap 전체를 덮어쓰지 마세요.
```text
# Fragment to merge through the CoreDNS configuration owner.
corp.example.internal:53 {
errors
cache 30
forward . 192.168.1.10 192.168.1.11 {
max_concurrent 1000
}
}
```
기존 Kubernetes zone·health/readiness·reload 동작을 보존합니다. 실제 resolver file·systemd-resolved/stub 구성을 확인하세요. 자기 자신으로 forwarding하면 loop가 생깁니다. 한 zone에 모순된 경로를 겹치기보다 적절한 VPC forwarding 또는 직접 CoreDNS 경로를 선택합니다.
관리형 EKS add-on은 **설치된 버전**의 schema를 조회하고 관련 없는 기존 설정을 보존한 전체 candidate를 검증합니다.
```bash
check_account
aws eks describe-addon --region "$AWS_REGION" --cluster-name "$CLUSTER_NAME" \
--addon-name coredns --output json > "$WORK_DIR/coredns-addon.json"
addon_version=$(jq -er '.addon.addonVersion' "$WORK_DIR/coredns-addon.json")
aws eks describe-addon-configuration --region "$AWS_REGION" --addon-name coredns \
--addon-version "$addon_version" --output json > "$WORK_DIR/coredns-schema-response.json"
jq -r '.configurationSchema' "$WORK_DIR/coredns-schema-response.json" \
> "$WORK_DIR/coredns-schema.json"
# Prepare the full intended values, preserving unrelated existing configuration.
: "${COREDNS_CANDIDATE_JSON:?Set the reviewed full configurationValues JSON file}"
export COREDNS_CANDIDATE_JSON
python3 - <<'PY'
import json, os
from pathlib import Path
import jsonschema
folder = Path(os.environ["WORK_DIR"])
schema = json.loads((folder / "coredns-schema.json").read_text())
candidate = json.loads(Path(os.environ["COREDNS_CANDIDATE_JSON"]).read_text())
validator = jsonschema.validators.validator_for(schema)
validator.check_schema(schema)
validator(schema).validate(candidate)
print("Configuration matches the fetched schema; rollout and DNS behavior are not yet verified")
PY
```
이는 로컬 schema 검사이며 rollout이 아닙니다. Managed add-on/GitOps 소유권·autoscaling·복구를 조율한 뒤 적용하세요.
### CoreDNS 배치와 Locality
AWS는 혼합 cluster에서 cloud/hybrid에 각각 최소 한 replica를 권장합니다. 각 위치 두 개는 복원력 선택지이며 네 replica가 보편적인 최소값·보장은 아닙니다.
모든 DNS 대상 node의 실제 zone label을 확인합니다. Hybrid node에는 owner가 정한 `topology.kubernetes.io/zone: onprem-dc1` 같은 값이 필요합니다. Compute-type label이 zone이나 taint를 자동 생성하지는 않습니다. Pod template `spec` 조각을 병합할 때 기존 affinity/toleration을 지우지 마세요.
```json
{
"affinity": {
"podAntiAffinity": {
"preferredDuringSchedulingIgnoredDuringExecution": [
{
"weight": 100,
"podAffinityTerm": {
"labelSelector": {
"matchLabels": {
"k8s-app": "kube-dns"
}
},
"topologyKey": "kubernetes.io/hostname"
}
},
{
"weight": 50,
"podAffinityTerm": {
"labelSelector": {
"matchLabels": {
"k8s-app": "kube-dns"
}
},
"topologyKey": "topology.kubernetes.io/zone"
}
}
]
}
}
}
```
Soft affinity/spread는 선호이며 2+2 배치나 bootstrap 성공 보장이 아닙니다. 배치만으로 client가 가까운 DNS replica를 선택하지도 않습니다.
AWS의 Service Traffic Distribution 예제는 `PreferClose`를 사용합니다. Cilium에는 지원되는 `loadBalancer.serviceTopology` 설정과 owner를 통한 agent rollout이 필요합니다. 실제 dataplane/version·정상 local endpoint를 확인하세요.
```json
{
"spec": {
"trafficDistribution": "PreferClose"
}
}
```
```bash
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n kube-system \
get service kube-dns -o json |
jq '{name:.metadata.name,uid:.metadata.uid,clusterIP:.spec.clusterIP,
clusterIPs:.spec.clusterIPs,ports:.spec.ports,trafficDistribution:.spec.trafficDistribution}'
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n kube-system \
get endpointslices -l kubernetes.io/service-name=kube-dns -o json |
jq '[.items[]|{name:.metadata.name,addressType,ports,
endpoints:[.endpoints[]?|{addresses,nodeName,zone,conditions,hints}]}]'
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n kube-system \
get pods -l k8s-app=kube-dns -o json |
jq '[.items[]|{name:.metadata.name,node:.spec.nodeName,phase:.status.phase,
ready:([.status.conditions[]?|select(.type=="Ready")|.status]|first // "NotReported")}]'
```
실제 Service IP·EndpointSlice zone/hint·Pod readiness를 확인합니다. `10.100.0.10`은 특정 Service CIDR의 예시이지 보편적인 DNS 주소가 아닙니다. Local replica가 disconnected EKS를 독립적인 DNS/control plane으로 바꾸지는 않습니다.
## 트래픽 플로우 패턴
그림의 주소·처리 단계는 설명용입니다. 실제 Service dataplane이 kube-proxy iptables, nftables/IPVS, Cilium eBPF replacement 중 무엇인지 확인하세요.
### 패턴 1: Kubelet → EKS 컨트롤 플레인
Kubelet은 설정된 Kubernetes API endpoint를 해석해 연결합니다. Private/public 접근의 route가 다르며 EKS 관리 PrivateLink endpoint와 혼동하면 안 됩니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-02-network-configuration-10.html)
### 패턴 2: EKS 컨트롤 플레인 → Kubelet
Control plane은 보고된 routable node 주소의 TCP10250에 연결합니다. Logs/exec/port-forward에 사용되며 return route·firewall·kubelet 인증이 필요합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-02-network-configuration-11.html)
### 패턴 3: Pod → EKS 컨트롤 플레인
Kubernetes Service IP를 사용하는 Pod에는 선택한 API endpoint로의 Service 변환이 필요합니다. Egress SNAT가 적용되면 응답은 node 주소로 오고 connection tracking이 역변환합니다. SNAT가 없으면 Pod 주소의 return route가 필요합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-02-network-configuration-12.html)
그림의 SNAT→DNAT 번호는 보편적인 hook 순서가 아닙니다. Iptables 경로에서는 일반적으로 Service DNAT 다음 routing·해당 POSTROUTING SNAT가 적용됩니다. eBPF 경로는 다르므로 실제 packet/connection 상태를 확인하세요.
### 패턴 4: EKS 컨트롤 플레인 → Pod (웹훅)
API server가 선택한 webhook Pod IP/port에 접근할 수 있어야 합니다. 실제 설정 port를 사용하며 그림의 이전 “8443+”는 유효한 port-range 요구가 아닙니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-02-network-configuration-13.html)
### 패턴 5: 하이브리드 노드 간 Pod ↔ Pod
지원 VXLAN overlay는 **outer node IP**로 대상 node에 접근합니다. 캡슐화 패킷 운반만을 위해 underlay에 inner destination Pod CIDR route가 필요한 것은 아닙니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-02-network-configuration-14.html)
그림에서 캡슐화 후 `10.85.x.0/24`를 조회하는 전달 label은 오래된 단순화입니다. 실제 outer packet은 `10.80.0.x`로 routing됩니다. 같은 L2 node끼리는 router hop 없이 직접 통신할 수도 있습니다.
VXLAN은 inner Ethernet frame을 UDP로 캡슐화합니다. IPv4·추가 encapsulation 없는 예제의 overhead 50바이트는 MTU 1500→1450을 설명하며 추가 tunnel에서는 달라집니다. Cilium VXLAN은 UDP8472, 일반 VXLAN은 흔히 4789, Geneve는 UDP6081입니다. 현재 Cilium tunnel 설정은 VXLAN/Geneve를 구분하며 IP-in-IP를 예전 `--tunnel` 옵션의 동등한 기본 overlay로 취급하면 안 됩니다.
VNI는 24비트이며 Cilium은 encapsulation metadata로 security identity를 전달할 수 있습니다. 암호학적 tenant 격리가 아니므로 policy·실제 identity 전파를 별도로 검토합니다.
### 패턴 6: 클라우드 Pod ↔ 하이브리드 Pod
직접 Pod-IP 트래픽은 VPC·WAN·on-prem Pod route가 필요합니다. 실제 요청이 Service VIP를 대상으로 할 때만 Service 변환이 필요합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-02-network-configuration-15.html)
그림의 kube-proxy/iptables block은 dataplane에 따라 다릅니다. 직접 Pod-IP packet에 kube-proxy DNAT가 본질적으로 필요한 것은 아닙니다.
### kube-proxy와 kubelet 상세
kube-proxy **iptables 모드**의 대표 chain 경로입니다.
```text
KUBE-SERVICES → KUBE-SVC-* → KUBE-SEP-* → endpoint DNAT
```
적격 equal-weight endpoint 3개와 별도 affinity/locality 정책이 없다면 조건부 확률 1/3, 남은 packet의 1/2, 나머지 선택이 대략 균등한 분배를 만듭니다. 아래는 설명용이며 실측 출력이나 모든 dataplane의 rule 구조가 아닙니다.
```text
# KUBE-SERVICES chain (nat table)
-A KUBE-SERVICES -d 172.20.0.10/32 -p tcp -m tcp --dport 80 -j KUBE-SVC-XXXXXX
# KUBE-SVC chain (load balancing)
-A KUBE-SVC-XXXXXX -m statistic --mode random --probability 0.33333 -j KUBE-SEP-AAAAAA
-A KUBE-SVC-XXXXXX -m statistic --mode random --probability 0.50000 -j KUBE-SEP-BBBBBB
-A KUBE-SVC-XXXXXX -j KUBE-SEP-CCCCCC
# KUBE-SEP chain (DNAT)
-A KUBE-SEP-AAAAAA -p tcp -j DNAT --to-destination 10.85.0.15:8080
-A KUBE-SEP-BBBBBB -p tcp -j DNAT --to-destination 10.85.0.16:8080
-A KUBE-SEP-CCCCCC -p tcp -j DNAT --to-destination 10.85.1.20:8080
```
| Secure kubelet endpoint | 용도 |
|------------------------|------|
| `/pods` | Pod 정보 |
| `/exec/{namespace}/{pod}/{container}` | Container exec stream |
| `/containerLogs/{namespace}/{pod}/{container}` | Container log; 이전 `/logs/...`가 아님 |
| `/metrics`, `/healthz` | 권한이 있는 metrics/health 접근 |
지원되는 API-server 경유 진단·권한을 사용합니다. 실제 Node `status.addresses`가 중요하며 다른 객체의 첫 주소나 hostname을 임의로 대신 쓰지 마세요.
## 라우팅 가능한 Pod CIDR 구성

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-02-network-configuration-0.html)
### 옵션 1: BGP (권장)

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-02-network-configuration-1.html)
AWS 전용 CNI 지원 페이지는 AWS-maintained Cilium 1.17/1.18을 안내합니다. 여기서는 호환 kernel/OS의 1.18.3-0을 참고하며 upstream 1.19로 무조건 바꾸지 않습니다. 다른 AWS 페이지는 Calico BGP·보존된 예제도 언급합니다. Calico 프로젝트가 deprecated됐다는 증거는 아니므로 기존 배포의 지원 범위를 확인하세요.
**기존 Cilium release의 정확한 버전**을 소유한 관리 도구에서 values를 병합하고 operator/agent rollout을 검토하며 BGP를 활성화합니다.
```yaml
bgpControlPlane:
enabled: true
operator:
rollOutPods: true
```
AWS 예제의 `v2alpha1`은 검토한 1.18.3 CRD에서 계속 served 상태이며 `v2`도 지원됩니다. 이 예제 때문에 CRD를 교체할 필요는 없습니다.
아래는 hybrid node를 선택하고 peer advertisement selector와 advertisement label을 연결하며 Pod CIDR만 광고합니다.
```yaml
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPClusterConfig
metadata:
name: hybrid-bgp-config
spec:
nodeSelector:
matchLabels:
eks.amazonaws.com/compute-type: hybrid
bgpInstances:
- name: hybrid-instance
localASN: 65001
peers:
- name: on-prem-router
peerASN: 65000
peerAddress: 10.80.1.1
peerConfigRef:
name: on-prem-peer
```
```yaml
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPPeerConfig
metadata:
name: on-prem-peer
spec:
timers:
holdTimeSeconds: 90
keepAliveTimeSeconds: 30
gracefulRestart:
enabled: true
restartTimeSeconds: 120
families:
- afi: ipv4
safi: unicast
advertisements:
matchLabels:
advertise: hybrid-pods
```
```yaml
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPAdvertisement
metadata:
name: hybrid-pod-cidrs
labels:
advertise: hybrid-pods
spec:
advertisements:
- advertisementType: PodCIDR
```
선택한 node가 의도한 peering topology로 router `10.80.1.1`에 접근한다는 전제입니다. Rack/loopback이 다르면 겹치지 않는 selector·검토한 multihop 설정이 필요할 수 있습니다. TCP179·ASN·인증·prefix filter·협상 timer·graceful-restart의 stale-route 동작을 network owner와 검토하세요.
BGP Established만으로 의도한 prefix 광고·수락·router forwarding-table 설치가 입증되지는 않습니다. Cilium BGP control plane은 reachability를 광고하며 모든 kernel/underlay routing을 대체하지 않습니다.
```bash
cilium --context "$KUBE_CONTEXT" bgp peers
cilium --context "$KUBE_CONTEXT" bgp routes
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s \
get ciliumbgpclusterconfigs,ciliumbgppeerconfigs,ciliumbgpadvertisements -o json |
jq '[.items[]|{kind,name:.metadata.name,status:.status}]'
```
#### ASN과 Router 구성
RFC6996 private 범위는 **64512–65534**, **4200000000–4294967294**입니다. 이전의 “16비트 범위만 사용” 규칙은 부정확했습니다. 1–64511 전체를 자유로운 public ASN으로 설명해서도 안 됩니다. Public/reserved/documentation 할당에는 별도 규칙이 있습니다.
조율된 기존 ASN을 사용합니다. 여기서 `localASN=65001`은 Cilium node, `peerASN=65000`은 on-prem router입니다. TGW ASN은 별도 upstream 관계이며 TGW가 존재한다고 Cilium이 자동으로 peer가 되지는 않습니다. Site-to-Site VPN BGP는 설정한 VPN/customer gateway 경로에서 종료되며 TGW Connect도 별도 transport/design입니다.
아래 vendor 조각은 설명용 출발점이며 장비 실행 검증을 하지 않았습니다. Router owner가 platform/version별 import/export prefix filter·limit·복구를 포함해 병합해야 합니다. Live router의 전역 ASN을 무작정 바꾸지 마세요.
**Cisco IOS / IOS-XE**
```text
router bgp 65000
neighbor 10.80.1.10 remote-as 65001
neighbor 10.80.1.10 description "EKS Hybrid Node - Cilium BGP"
!
address-family ipv4 unicast
neighbor 10.80.1.10 activate
neighbor 10.80.1.10 soft-reconfiguration inbound
exit-address-family
```
**Cisco NX-OS (Nexus)**
```text
router bgp 65000
address-family ipv4 unicast
neighbor 10.80.1.10
remote-as 65001
description EKS-Hybrid-Cilium
address-family ipv4 unicast
soft-reconfiguration inbound
```
**Juniper Junos (MX / QFX / SRX)**
```text
set protocols bgp group eks-hybrid type external
set protocols bgp group eks-hybrid peer-as 65001
set protocols bgp group eks-hybrid neighbor 10.80.1.10 description "EKS Hybrid Node"
set protocols bgp group eks-hybrid family inet unicast
set routing-options autonomous-system 65000
```
**Arista EOS**
```text
router bgp 65000
neighbor 10.80.1.10 remote-as 65001
neighbor 10.80.1.10 description EKS-Hybrid-Cilium
!
address-family ipv4
neighbor 10.80.1.10 activate
```
**MikroTik RouterOS 7.20+**
```text
/routing/bgp/instance
add name=hybrid as=65000
/routing/bgp/connection
add name=hybrid-node-001 instance=hybrid remote.address=10.80.1.10 remote.as=65001 local.role=ebgp address-families=ip disabled=yes
# Review input/output filters and routing before enabling the connection.
```
**FRRouting (FRR), reference 10.7.1**
```text
ip prefix-list HYBRID_PODS seq 10 permit 10.85.0.0/16 ge 25 le 25
route-map FROM_HYBRID permit 10
match ip address prefix-list HYBRID_PODS
route-map TO_HYBRID deny 10
router bgp 65000
bgp router-id 10.80.1.1
bgp ebgp-requires-policy
neighbor 10.80.1.10 remote-as 65001
address-family ipv4 unicast
neighbor 10.80.1.10 activate
neighbor 10.80.1.10 route-map FROM_HYBRID in
neighbor 10.80.1.10 route-map TO_HYBRID out
exit-address-family
```
RouterOS 7.20+는 BGP instance를 명시합니다. FRR traditional 기본값은 eBGP policy를 요구하므로 filter가 없으면 Established여도 `(Policy)` 상태로 route를 교환하지 않을 수 있습니다. FRR 예제는 검토한 `/25` Pod block만 수신하고 Cilium으로 route를 보내지 않습니다. 실제 IPAM·upstream routing에 맞게 filter를 조정하세요.
### 옵션 2: 정적 라우트

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-02-network-configuration-2.html)
Cilium **cluster-pool IPAM**에서는 할당된 `CiliumNode.spec.ipam.podCIDRs`를 모두 읽습니다. 등록 순서대로 할당된다는 보장은 없습니다. `/16`은 산술적으로 `/25` block 512개, 각 block은 주소 128개지만 지원 node 512개·node당 application Pod IP 128개를 보장하지 않습니다. 예약 주소·node/CNI 사용·kubelet/resource 제한도 고려해야 합니다.
다음은 검토한 새 pool의 Cilium Helm-values 조각이며 kubelet 전체의 `podCIDR` 설정이 아닙니다. 이전을 간단히 하려고 기존 할당 CIDR·block size를 바꾸지 마세요.
```yaml
ipam:
mode: cluster-pool
operator:
clusterPoolIPv4PodCIDRList:
- 10.85.0.0/16
clusterPoolIPv4MaskSize: 25
```
`.addresses[0]`은 Cilium-internal 주소일 수 있어 next hop으로 쓰면 안 됩니다. 아래는 Kubernetes Node와 IPv4 InternalIP를 대조하고 모든 Pod prefix를 승인된 remote 범위와 검증합니다. 중첩·누락을 거부하며 실행 shell이 아닌 **JSON candidate**를 생성합니다.
```bash
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get nodes \
-l eks.amazonaws.com/compute-type=hybrid -o json |
jq '{items:[.items[]|{metadata:{name:.metadata.name,uid:.metadata.uid},
status:{addresses:.status.addresses}}]}' > "$WORK_DIR/hybrid-nodes.json"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get ciliumnodes.cilium.io -o json |
jq '{items:[.items[]|{metadata:{name:.metadata.name,uid:.metadata.uid},
spec:{addresses:.spec.addresses,ipam:{podCIDRs:.spec.ipam.podCIDRs}}}]}' \
> "$WORK_DIR/cilium-nodes.json"
```
```bash
python3 - <<'PY'
import ipaddress, json, os
from pathlib import Path
folder = Path(os.environ["WORK_DIR"])
network = json.loads((folder / "cluster.json").read_text())["cluster"]["remoteNetworkConfig"]
node_ranges = [ipaddress.ip_network(c, strict=True) for n in network["remoteNodeNetworks"] for c in n["cidrs"]]
pod_ranges = [ipaddress.ip_network(c, strict=True) for n in network.get("remotePodNetworks", []) for c in n["cidrs"]]
if not pod_ranges:
raise SystemExit("A reviewed routable remote Pod range is required for this route plan")
nodes = {n["metadata"]["name"]: n for n in json.loads((folder / "hybrid-nodes.json").read_text())["items"]}
claims = json.loads((folder / "cilium-nodes.json").read_text())["items"]
rows, seen = [], []
for item in claims:
name = item["metadata"]["name"]
if name not in nodes:
continue
node = nodes[name]
ips = [ipaddress.ip_address(a["address"]) for a in node["status"]["addresses"]
if a["type"] == "InternalIP" and ":" not in a["address"]]
if len(ips) != 1 or not any(ips[0] in n for n in node_ranges):
raise SystemExit(f"Review the unique IPv4 InternalIP and remote-node range for {name}")
cilium_ips = [ipaddress.ip_address(a["ip"]) for a in item["spec"].get("addresses", [])
if a["type"] == "InternalIP" and ":" not in a["ip"]]
if cilium_ips != ips:
raise SystemExit(f"Kubernetes/Cilium InternalIP mismatch for {name}")
cidrs = item["spec"]["ipam"].get("podCIDRs") or []
if not cidrs:
raise SystemExit(f"No allocated cluster-pool Pod CIDRs for {name}; do not invent a route")
for raw in cidrs:
cidr = ipaddress.ip_network(raw, strict=True)
if cidr.version != 4 or not any(cidr.subnet_of(p) for p in pod_ranges):
raise SystemExit(f"Unapproved Pod CIDR for {name}: {cidr}")
if any(cidr.overlaps(previous) for previous in seen):
raise SystemExit("Overlapping or duplicate Pod routes require investigation")
seen.append(cidr)
rows.append({"node": name, "nodeUID": node["metadata"]["uid"],
"ciliumNodeUID": item["metadata"]["uid"], "destination": str(cidr), "nextHop": str(ips[0])})
if set(nodes) != {row["node"] for row in rows}:
raise SystemExit("Some hybrid nodes have no matching Cilium allocation")
(folder / "reviewed-route-candidates.json").write_text(json.dumps(rows, indent=2) + "\n")
print(json.dumps(rows, indent=2))
PY
```
Cluster-pool용 시점 snapshot이며 node identity의 원자적 lease·routing controller가 아닙니다. 변경 전 재확인하세요. Calico BlockAffinity는 다른 모델이므로 state·borrowing/pool·실제 route를 조사하고 이 generator를 그대로 재사용하지 마세요.
Owner 검토 후 수동 router syntax는 다음과 같을 수 있습니다.
```text
# Illustrative syntax after validating the route plan on the intended router:
# Linux
ip route add 10.85.0.0/25 via 10.80.1.10
# Cisco IOS / IOS-XE
ip route 10.85.0.0 255.255.255.128 10.80.1.10 name hybrid-node-001-pods
# FRR
ip route 10.85.0.0/25 10.80.1.10
```
실제 network manager/device 구성으로 영구 저장합니다. `up ip route ...`는 ifupdown stanza용이며 standalone Bash나 모든 현대 Linux network manager용이 아닙니다. Static route에는 drift/failure 추적이 필요하며 이전 “node 1–5개” 기준은 계획 예시이지 기술적 제한이 아닙니다.
### 옵션 3: ARP 프록시
AWS는 proxy ARP를 가능한 L2 접근으로 설명합니다. 적절한 on-link neighbor discovery·구체적인 CNI/host 설정이 필요하며 일반 Cilium 활성화만으로 이 경로가 준비되지는 않습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-02-network-configuration-3.html)
ARP broadcast는 TGW/VPN/DX L3 routing을 통과하지 않습니다. 이 선택이 VPC/WAN return route를 없애지 않으며 BGP/static 설계를 대체하기 전에 실제 L2·failover를 검증해야 합니다.
## 네트워크 정책
선택 대상·방향·실제 enforcing dataplane을 구분합니다. Kubernetes NetworkPolicy allow는 합집합이므로 다른 일치 policy가 트래픽을 허용할 수 있습니다. Cilium explicit deny·L7은 별도 평가가 필요합니다. 아래는 통제된 namespace에서 시험할 대안이며 모든 allow를 겹치면 더 엄격해진다는 뜻이 아닙니다.
### Kubernetes NetworkPolicy
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: bookinfo
spec:
podSelector:
matchLabels:
app: reviews
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: productpage
ports:
- protocol: TCP
port: 9080
```
`reviews` ingress를 선택해 같은 namespace의 일치하는 `productpage` Pod에 TCP9080을 허용합니다. 모든 Pod/방향을 격리하거나 다른 모든 policy를 무효화하지는 않습니다.
### CiliumNetworkPolicy와 L7
```yaml
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: bookinfo
spec:
endpointSelector:
matchLabels:
app: reviews
ingress:
- fromEndpoints:
- matchLabels:
app: productpage
k8s:io.kubernetes.pod.namespace: bookinfo
toPorts:
- ports:
- port: '9080'
protocol: TCP
```
```yaml
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: frontend-http-contract
namespace: bookinfo
spec:
endpointSelector:
matchLabels:
app: reviews
ingress:
- fromEndpoints:
- matchLabels:
app: productpage
k8s:io.kubernetes.pod.namespace: bookinfo
toPorts:
- ports:
- port: '9080'
protocol: TCP
rules:
http:
- method: GET
path: /api/v1/.*
```
HTTP rule은 무제한 L4 allow의 대안이며 그 위에 자동으로 더하는 제한이 아닙니다. 실제 앱 path를 사용하세요. HTTP inspection에는 적절한 가시성이 필요하며 암호화 mesh/TLS가 자동으로 검사되지는 않습니다.
### DNS 기반 Egress
별도 `external-api-client` 예제는 **Cilium이 식별하는 CoreDNS Pod**의 DNS와 관측한 API 주소의 HTTPS를 허용합니다.
```yaml
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-external-api
namespace: bookinfo
spec:
endpointSelector:
matchLabels:
app: external-api-client
egress:
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
k8s:k8s-app: kube-dns
toPorts:
- ports:
- port: '53'
protocol: ANY
rules:
dns:
- matchPattern: '*'
- toFQDNs:
- matchName: api.example.com
toPorts:
- ports:
- port: '443'
protocol: TCP
```
실제 resolver/identity 구성을 확인하세요. NodeLocal DNS·host DNS·비 Cilium endpoint에는 다른 지원 rule이 필요할 수 있습니다. FQDN IP 관측은 원격 API 인증이 아니며 DNS cache·공유 주소를 고려합니다. Cilium L7/FQDN 기능은 AWS가 명시한 기본 Kubernetes NetworkPolicy 지원 범위도 넘어섭니다.
## 웹훅 구성
일반 direct-routing 설계에서는 control plane이 webhook Pod IP에 접근해야 합니다. 적절한 Pod return path가 없으면 cloud-hosted component를 배치하고 gateway/proxy 대안은 별도로 검증합니다.
```yaml
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: eks.amazonaws.com/compute-type
operator: NotIn
values:
- hybrid
```
완전한 Deployment가 아닌 Pod-template affinity 조각입니다. `NotIn hybrid`만으로 정상 cloud capacity나 다른 배치 조건이 입증되지는 않습니다.
AWS Load Balancer Controller·CloudWatch/ADOT operator·cert-manager의 webhook 배치를 검토합니다. Operator와 node collector를 구분하세요. **Metrics Server는 admission webhook이 아닌 aggregated API service**지만 control-plane→Pod 접근은 필요합니다. Pod phase만 보지 말고 실제 API/webhook 호출을 확인하세요.
## 읽기 전용 연결 진단
### Kubernetes API TLS와 시간
```bash
set -euo pipefail
endpoint=$(jq -er '.cluster.endpoint' "$WORK_DIR/cluster.json")
case "$endpoint" in https://*) ;; *) printf 'HTTPS endpoint required.\n' >&2; exit 1;; esac
jq -er '.cluster.certificateAuthority.data' "$WORK_DIR/cluster.json" |
base64 --decode > "$WORK_DIR/cluster-ca.pem"
openssl x509 -in "$WORK_DIR/cluster-ca.pem" -noout >/dev/null
curl --silent --show-error --connect-timeout 5 --max-time 15 \
--cacert "$WORK_DIR/cluster-ca.pem" --output "$WORK_DIR/api-response.txt" \
--write-out '{"httpCode":%{http_code},"remoteIP":"%{remote_ip}","dnsTotalSeconds":%{time_namelookup},"connectTotalSeconds":%{time_connect},"tlsTotalSeconds":%{time_appconnect},"totalSeconds":%{time_total}}\n' \
"$endpoint/readyz" > "$WORK_DIR/api-timing.json"
cat "$WORK_DIR/api-timing.json"
```
Cluster CA·hostname 검증이 성공해야 합니다. HTTP401/403은 TLS endpoint 도달과 미충족 권한을 보여줄 수 있으나 앱 health 성공은 아닙니다. Curl 시간은 누적 단계이며 순수 RTT가 아닙니다. ICMP ping 무응답만으로 EKS API 장애를 판단하지 마세요.
### VPN 상태와 Metrics
```bash
: "${VPN_ID:?Select the reviewed VPN connection}"
: "${TUNNEL_IP:?Select its actual AWS tunnel outside IP}"
check_account
# Select telemetry only: do not dump customer gateway configuration or pre-shared keys.
aws ec2 describe-vpn-connections --region "$AWS_REGION" --vpn-connection-ids "$VPN_ID" \
--query 'VpnConnections[].{id:VpnConnectionId,state:State,telemetry:VgwTelemetry}' \
--output json > "$WORK_DIR/vpn-state.json"
jq -e --arg ip "$TUNNEL_IP" 'length==1 and any(.[0].telemetry[]?; .OutsideIpAddress==$ip)' \
"$WORK_DIR/vpn-state.json" >/dev/null
export VPN_ID TUNNEL_IP
python3 - <<'PY'
import ipaddress, json, os
from datetime import datetime, timedelta, timezone
from pathlib import Path
ipaddress.ip_address(os.environ["TUNNEL_IP"])
now = datetime.now(timezone.utc)
end = now.replace(minute=now.minute - now.minute % 5, second=0, microsecond=0)
body = {"Namespace": "AWS/VPN", "MetricName": "TunnelState",
"Dimensions": [{"Name": "VpnId", "Value": os.environ["VPN_ID"]},
{"Name": "TunnelIpAddress", "Value": os.environ["TUNNEL_IP"]}],
"StartTime": (end - timedelta(minutes=15)).isoformat(), "EndTime": end.isoformat(),
"Period": 300, "Statistics": ["Minimum", "Maximum"]}
(Path(os.environ["WORK_DIR"]) / "vpn-metric-request.json").write_text(json.dumps(body, indent=2) + "\n")
PY
aws cloudwatch get-metric-statistics --region "$AWS_REGION" \
--cli-input-json "file://$WORK_DIR/vpn-metric-request.json" --output json \
> "$WORK_DIR/vpn-metric-result.json"
jq '{label:.Label,datapoints:(.Datapoints|sort_by(.Timestamp))}' "$WORK_DIR/vpn-metric-result.json"
```
`available`은 VPN 리소스 상태이지 tunnel health가 아닙니다. TunnelState 1은 static의 UP/BGP의 ESTABLISHED, 0은 나머지이며 집계값은 소수일 수 있습니다. 데이터 부재와 DOWN을 구분하고 양쪽 tunnel·route·실제 workload를 검증합니다. 이 query는 customer gateway 구성·pre-shared key를 dump하지 않습니다.
AWS의 RTT ≤200ms/100Mbps는 일반 권장입니다. 이전 50/100ms 구간·“Direct Connect는 항상 10ms 미만”은 미검증 기준이며 보장값이 아닙니다.
## 참고 자료
- [Hybrid networking](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-networking.html)
- [EKS PrivateLink: management, OIDC and console endpoints](https://docs.aws.amazon.com/eks/latest/userguide/vpc-interface-endpoints.html)
- [S3 interface endpoints and private DNS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/privatelink-interface-endpoints.html)
- [Roles Anywhere endpoint policies](https://docs.aws.amazon.com/rolesanywhere/latest/userguide/vpc-interface-endpoints.html)
- [Current hybrid CNI support](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-cni.html)
- [AWS hybrid BGP procedure](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-cilium-bgp.html)
- [Mixed-mode DNS and webhooks](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-webhooks.html)
- [Hybrid routing concepts](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-concepts-kubernetes.html)
- [Hybrid traffic-flow reference](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-concepts-traffic-flows.html)
- [Cilium 1.18.3 routing source](https://github.com/cilium/cilium/blob/v1.18.3/Documentation/network/concepts/routing.rst)
- [Cilium 1.18.3 DNS-policy source](https://github.com/cilium/cilium/blob/v1.18.3/Documentation/security/dns.rst)
- [RFC6996 private ASNs](https://www.rfc-editor.org/rfc/rfc6996.html)
- [RouterOS BGP reference](https://help.mikrotik.com/docs/spaces/ROS/pages/328220/BGP)
- [FRR 10.7.1 BGP reference source](https://github.com/FRRouting/frr/blob/frr-10.7.1/doc/user/bgp.rst)
- [VPN metrics](https://docs.aws.amazon.com/vpn/latest/s2svpn/monitoring-cloudwatch-vpn.html)
- [Kubernetes 1.36.2 kubelet server source](https://github.com/kubernetes/kubernetes/blob/v1.36.2/pkg/kubelet/server/server.go)
- [Kubernetes NetworkPolicy semantics](https://kubernetes.io/docs/concepts/services-networking/network-policies/)
< [이전: 사전 요구 사항](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/01-prerequisites.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: 제한된 인터넷 환경](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/03-airgap-setup.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-hybrid-nodes/03-airgap-setup
----------------------------------------
# 인터넷 제한 환경 구성 (S3, 프라이빗 엔드포인트, 프록시)
< [이전: 네트워크 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/02-network-configuration.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: 노드 부트스트랩](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md) >
> **지원 버전**: EKS Hybrid Nodes; nodeadm v1.0.20 소스 확인. Kubernetes·OS·런타임·애드온 조합은 대상 클러스터에 맞게 선택합니다.
> **마지막 업데이트**: 2026년 9월 12일
이 문서는 퍼블릭 인터넷 접근을 제한한 Hybrid Nodes의 설치 준비를 다룹니다. **Hybrid Nodes에는 AWS에서 실행되는 EKS 컨트롤 플레인과 자격 증명에 사용하는 AWS 서비스 연결이 계속 필요합니다.** 소프트웨어를 물리적 매체로 전달하더라도 Hybrid Nodes가 완전히 단절된 Kubernetes 배포판으로 바뀌지는 않습니다.
예제는 준비·검토 절차이며 운영 배포를 검증한 레시피가 아닙니다. 이번 감사에서는 소스, 구성과 로컬 실패 사례를 확인했으며 OS 이미지 빌드, 아티팩트 게시, 노드 등록, 실제 프라이빗 네트워크 검증은 수행하지 않았습니다. 감사 환경에서 퍼블릭 아티팩트 manifest 요청은 TLS 호스트명 검증에 실패했습니다. 인증서 검사를 우회하지 않았으며, 실패한 요청을 근거로 최신 아티팩트 패치나 다이제스트를 추정하지 않습니다.
## 연결과 격리의 경계
| 방식 | 제공하는 기능 | Hybrid Nodes 고려 사항 |
|---|---|---|
| 물리적으로 단절된 네트워크 | AWS와 실시간 연결 없음 | 필수 EKS 컨트롤 플레인·자격 증명 서비스 연결을 제공할 수 없음 |
| 통제된 외부 통신 프록시 | 승인한 외부 HTTPS 목적지와 접근 기록 | 설치 프로그램, 패키지 관리자, 호스트 데몬과 해당 Pod를 각각 구성 |
| VPN/Direct Connect와 프라이빗 엔드포인트 | 클러스터와 지원 AWS API로 향하는 프라이빗 경로 | 양방향 라우팅, DNS, 보안 그룹, 권한 필요; 모든 퍼블릭 다운로드 호스트를 포함하지 않음 |
| 오프라인 소프트웨어 전달 | 검토한 아티팩트를 통제된 경로로 반입 | AWS 프라이빗 연결과 함께 사용 가능하며 그 연결을 대체하지 않음 |
네트워크 제한은 노출을 줄일 수 있지만 규정 준수, 데이터 유출의 완전 차단, 모든 공급망 공격 방지를 보장하지 않습니다. 인증서 신뢰, 승인된 게시자, 서명, 패치, 운영자 접근과 애플리케이션 데이터 흐름은 별도 통제 대상입니다. 프라이빗 연결도 AWS 서비스와 온프레미스 네트워크에 의존합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-03-airgap-setup-0.html)
> **다이어그램 설명 보완:** 물리적 격리 방식은 비교 대상이며 Hybrid Nodes의 지원 운영 방식이 아닙니다.
## 아키텍처와 아티팩트별 책임

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-03-airgap-setup-1.html)
> **다이어그램 정정:** `hybrid-assets.eks.amazonaws.com → PHZ → S3`만으로 투명 미러가 동작하지 않습니다. 아래 설치 경로를 사용합니다. DNS 변경은 원래 호스트명의 TLS 인증서, S3 객체 라우팅이나 요청 권한을 제공하지 않습니다.
| 아티팩트 | 준비와 전달 |
|---|---|
| Hybrid `nodeadm` | `aws/eks-hybrid` 릴리스를 승인하고 출처·체크섬을 확인한 뒤 root로 실행. EC2용 `amazon-eks-ami` nodeadm과 다름 |
| kubelet, kubectl, CNI 플러그인, ECR credential provider, IAM authenticator | 승인한 manifest에서 정확한 릴리스·빌드·OS·아키텍처 선택 |
| IAM Roles Anywhere signing helper | 별도 릴리스를 선택·검증. 배열의 첫 항목을 임의로 선택하지 않음 |
| SSM 설치 프로그램·에이전트 | 별도 리전별 다운로드, 서명과 등록 경로 사용. EKS 아티팩트 manifest만으로 모두 재지정되지 않음 |
| containerd, runc, iptables와 OS 의존성 | 전이 의존성과 서명된 저장소 메타데이터를 포함한 승인 OS·런타임 조합 |
| CNI, CoreDNS, kube-proxy, 샌드박스와 워크로드 이미지 | 실제 manifest, init container, 이미지 다이제스트와 플랫폼 목록 확인. 바이너리 manifest가 이미지 태그를 제공하지 않음 |
Amazon VPC CNI(`aws-node` / `vpc-cni-init`)는 Hybrid Nodes용 CNI가 아닙니다. [네트워크 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/02-network-configuration.md)의 지원 Hybrid CNI 절차를 따릅니다. 선택한 CNI 데이터 경로가 kube-proxy를 사용할 때만 이를 포함합니다. CNI 플러그인 바이너리 묶음을 설치하는 것과 CNI 컨트롤러 배포는 다릅니다.
## 설치 경로 선택
### 경로 A: 의존성을 미리 설치한 OS 이미지
통제된 빌더에서 승인한 Hybrid nodeadm을 설치하고, 대상 클러스터의 Kubernetes 버전과 자격 증명 공급자로 `nodeadm install`을 실행합니다. AWS는 이 이미지 빌드 사용 방식을 문서화합니다. 설치한 아티팩트와 nodeadm tracker를 이미지에 보존합니다.
```bash
# Controlled image builder only; installs software on this host.
set -euo pipefail
: "${KUBERNETES_VERSION:?Approved cluster-compatible version}"
: "${REGION:?}" "${CREDENTIAL_PROVIDER:?ssm or iam-ra}"
case "$CREDENTIAL_PROVIDER" in ssm|iam-ra) ;; *) exit 1 ;; esac
sudo nodeadm install "$KUBERNETES_VERSION" \
--credential-provider "$CREDENTIAL_PROVIDER" --region "$REGION"
```
기본 런타임 소스는 OS 배포판이며 RHEL에서는 지원되지 않습니다. RHEL은 문서화된 Docker 패키지 소스를 선택하거나 호환 런타임을 미리 설치하고 `--containerd-source none`을 사용합니다. AL2023에서는 Docker 소스를 지원하지 않습니다. `none`은 containerd를 대신 설치하지 않습니다.
빌더를 초기화·등록한 뒤 그 신원을 복제하면 **안 됩니다**. 노드별 SSM 활성화 또는 IAM Roles Anywhere 인증서·개인 키는 승인된 개별 전달 절차를 사용합니다. 활성화 코드, 개인 키, SSM 등록 상태, kubelet 인증서, 운영자 자격 증명을 재사용 이미지에 넣지 않습니다. Bottlerocket은 별도 준비·부트스트랩 방식을 사용하며 이 nodeadm 절차를 사용하지 않습니다.
SSM 신규 설치·업그레이드는 오래된 SSM 서명 키 문제 때문에 nodeadm **1.0.19 이상**이 필요합니다. 이 문서는 무제한으로 바뀌는 `latest`가 아닌 **v1.0.20**을 확인했습니다.
### 경로 B: 사용자 지정 아티팩트 manifest
릴리스된 **v1.0.20 소스**는 다음 옵션을 지원합니다. 사용자 가이드의 옵션 표에는 이들 중 일부가 나열되어 있지 않습니다.
| 명령·설정 | 확인한 릴리스의 실제 동작 |
|---|---|
| `install --manifest-override file:///path/manifest.json` | 로컬 manifest를 읽음. YAML 디코더는 JSON도 수용 |
| `install --manifest-override https://mirror.example.com/manifest.json` | 일반 HTTP 클라이언트로 manifest 다운로드 |
| `install --private-mode` | `--manifest-override` 필수. OS 패키지 설치를 건너뛰지만 자격 증명·EKS 아티팩트 설치는 수행 |
| `init --manifest-override ... --private-mode` | manifest 인자를 요구하고 그 파일에서 리전 메타데이터를 읽음. AWS 인증과 EKS 연결 요구 사항은 유지 |
| 개별 아티팩트 `uri` / `checksum_uri` | S3 SigV4 서명 없는 HTTP(S) 요청으로 가져옴. `file://` **manifest** 지원이 `file://` **아티팩트** 지원을 뜻하지 않음 |
| `gzip_uri` | 있으면 `uri`보다 우선 사용하며 압축 해제 후 체크섬 검증 |
이 옵션을 사용하기 전에 배포할 정확한 바이너리의 `install --help`와 `init --help`를 확인합니다. Private mode는 완전한 오프라인 패키지 설치 프로그램이 아닙니다. systemd unit을 포함한 containerd, runc, iptables, CA 인증서와 필수 OS 의존성을 미리 설치합니다.
`--credential-provider ssm`을 사용하면 v1.0.20은 리전별 `ssm-setup-cli`와 서명 URL을 별도로 구성합니다. manifest의 `ssm_releases`는 이 설치 경로를 재지정하지 않습니다. 해당 S3 객체와 이후 에이전트 설치·등록 의존성에 대한 접근을 준비하거나 검증한 사전 설치 이미지 방식을 사용합니다.
### Manifest 검토와 단일 조합 선택
원본 manifest에는 `supported_eks_releases`, `iam_roles_anywhere_releases`, `region_config`가 있습니다. Kubernetes 항목에는 `major_minor_version`, `latest_patch_version`, `patch_releases[].version`, **`patch_version`**, **`release_date`**와 아티팩트별 URL이 포함됩니다. 같은 패치에 여러 빌드가 있을 수 있습니다. 이전 `1.33.3` 예제는 역사적 스키마 설명이며 현재 승인된 패치라는 증거가 아닙니다.
받은 원본 manifest, 수집 시점·해시와 승인 기록을 보존하고 HTTPS 출처를 확인합니다. 다음 로컬 선택기는 정확한 Kubernetes 패치, 빌드 날짜, signing helper 릴리스, 아키텍처를 요구합니다. 모호한 선택, 누락 아티팩트, 중복 YAML 키와 알 수 없는 리전을 거부합니다. ECR 계정을 추정하지 않고 실제 리전 메타데이터를 유지합니다.
준비 호스트에 `select-mirror.py`로 저장합니다. Python 3과 PyYAML이 필요합니다.
```python
#!/usr/bin/env python3
"""Build a local review plan, not an installer. Requires PyYAML."""
import copy
import datetime
import json
import re
import sys
from pathlib import Path
from urllib.parse import urlsplit
import yaml
class UniqueLoader(yaml.SafeLoader):
pass
def mapping(loader, node, deep=False):
result = {}
for key_node, value_node in node.value:
key = loader.construct_object(key_node, deep=deep)
if key in result:
raise ValueError("duplicate YAML key")
result[key] = loader.construct_object(value_node, deep=deep)
return result
UniqueLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, mapping
)
def https_url(value):
if not isinstance(value, str) or any(c.isspace() for c in value):
raise ValueError("URL must be a nonempty HTTPS URL")
parsed = urlsplit(value)
if (parsed.scheme != "https" or not parsed.hostname or parsed.username
or parsed.password or parsed.query or parsed.fragment):
raise ValueError("HTTPS URL must not contain credentials, query or fragment")
return value
def select(manifest, version, build_date, iam_version, arch, region, mirror):
if not re.fullmatch(r"1\.\d+\.\d+", version):
raise ValueError("an exact approved Kubernetes patch is required")
datetime.date.fromisoformat(build_date)
if arch not in ("amd64", "arm64"):
raise ValueError("unsupported architecture")
mirror = https_url(mirror).rstrip("/")
region_info = manifest["region_config"][region] # No account fallback.
if (region_info.get("partition") != "aws"
or region_info.get("dns_suffix") != "amazonaws.com"
or not region_info.get("cred_providers", {}).get("iam-ra")
or not re.fullmatch(r"\d{12}", str(region_info.get("ecr_account_id", "")))):
raise ValueError("review a supported commercial Region with IAM Roles Anywhere")
minor, patch = version.rsplit(".", 1)
releases = [
release
for family in manifest["supported_eks_releases"]
if family["major_minor_version"] == minor
for release in family["patch_releases"]
if release["version"] == version and release["patch_version"] == patch
and release["release_date"] == build_date
]
iam = [
release for release in manifest["iam_roles_anywhere_releases"]
if release["version"] == iam_version
]
if len(releases) != 1 or len(iam) != 1:
raise ValueError("release selection must be unique")
eks_release, iam_release = copy.deepcopy(releases[0]), copy.deepcopy(iam[0])
plan = []
for release, names in [
(eks_release, ["kubelet", "kubectl", "cni-plugins",
"ecr-credential-provider", "aws-iam-authenticator"]),
(iam_release, ["aws_signing_helper"]),
]:
chosen = []
for name in names:
matches = [a for a in release["artifacts"]
if a["name"] == name and a["arch"] == arch and a["os"] == "linux"]
if len(matches) != 1:
raise ValueError("missing or duplicate artifact: " + name)
artifact = matches[0]
item_id = "a%02d" % len(plan)
plan.append({"id": item_id, "name": name,
"uri": https_url(artifact["uri"]),
"checksum_uri": https_url(artifact["checksum_uri"])})
# Use the original, uncompressed URI; its checksum is not a gzip-file hash.
artifact.pop("gzip_uri", None)
artifact["uri"] = mirror + "/" + item_id + "/data"
artifact["checksum_uri"] = mirror + "/" + item_id + "/data.sha256"
chosen.append(artifact)
release["artifacts"] = chosen
selected = {
"supported_eks_releases": [{
"major_minor_version": minor, "latest_patch_version": patch,
"patch_releases": [eks_release],
}],
"iam_roles_anywhere_releases": [iam_release],
"region_config": {region: copy.deepcopy(region_info)},
}
return selected, {"artifacts": plan}
def main():
if len(sys.argv) != 9:
raise ValueError(
"usage: select-mirror.py UPSTREAM VERSION BUILD_DATE IAM_VERSION "
"ARCH REGION HTTPS_MIRROR_PREFIX NEW_OUTPUT_DIR"
)
source, version, date, iam, arch, region, mirror, output = sys.argv[1:]
manifest = yaml.load(Path(source).read_text(), Loader=UniqueLoader)
selected, plan = select(manifest, version, date, iam, arch, region, mirror)
out = Path(output)
out.mkdir(mode=0o700, parents=False, exist_ok=False)
(out / "upstream.yaml").write_bytes(Path(source).read_bytes())
(out / "manifest.json").write_text(json.dumps(selected, indent=2) + "\n")
(out / "plan.json").write_text(json.dumps(plan, indent=2) + "\n")
if __name__ == "__main__":
main()
```
출력 디렉터리는 새 경로여야 합니다. HTTPS 미러 접두사는 게시에 사용하는 동일한 불변 객체 접두사로 연결되어야 합니다. 이 스크립트는 계획만 만들며 다운로드나 미러 인증은 수행하지 않습니다.
```bash
set -euo pipefail
: "${APPROVED_PATCH:?}" "${APPROVED_BUILD_DATE:?}" "${APPROVED_IAM_VERSION:?}"
: "${ARCH:?amd64 or arm64}" "${REGION:?}"
: "${MIRROR_PREFIX:?HTTPS URL for this reviewed candidate}"
: "${NEW_PLAN_DIR:?A new local directory}"
python3 select-mirror.py upstream.yaml "$APPROVED_PATCH" "$APPROVED_BUILD_DATE" \
"$APPROVED_IAM_VERSION" "$ARCH" "$REGION" "$MIRROR_PREFIX" "$NEW_PLAN_DIR"
```
다운로드 전에 모든 소스 호스트와 선택한 6개 아티팩트를 검토합니다. 이는 **IAM Roles Anywhere 아티팩트 예제**이며 SSM 설치 프로그램 미러가 아닙니다. nodeadm 자체, OS 패키지, 이미지, 서명 키와 인증서는 포함하지 않습니다.
`download-plan.sh`로 저장합니다.
```bash
#!/usr/bin/env bash
# Download into a new plan directory. No AWS writes or host installation.
set -euo pipefail
umask 077
cd -- "${1:?Use the directory produced by select-mirror.py}"
test ! -e checksums.sha256
test ! -e queue.tsv
jq -er '.artifacts[] | [.id, .uri, .checksum_uri] | @tsv' plan.json > queue.tsv
test "$(wc -l < queue.tsv)" -eq 6
while IFS=$'\t' read -r item_id uri checksum_uri; do
[[ "$item_id" =~ ^a[0-9]{2}$ ]]
mkdir -- "$item_id" # Refuse a partial run or existing directory.
curl --fail --show-error --silent --location \
--proto '=https' --proto-redir '=https' --connect-timeout 10 \
--max-time 300 --max-filesize 268435456 \
"$uri" -o "$item_id/data"
curl --fail --show-error --silent --location \
--proto '=https' --proto-redir '=https' --connect-timeout 10 \
--max-time 30 --max-filesize 4096 \
"$checksum_uri" -o "$item_id/upstream.sha256"
expected=$(python3 - "$item_id/upstream.sha256" <<'CHECKSUM_PY'
import pathlib, re, sys
text = pathlib.Path(sys.argv[1]).read_text().strip()
match = re.fullmatch(r"([0-9a-fA-F]{64})(?:[ \t]+[^\r\n]+)?", text)
if not match:
raise SystemExit("missing, malformed or multi-record upstream checksum")
print(match.group(1).lower())
CHECKSUM_PY
)
actual=$(sha256sum "$item_id/data")
[[ "${actual%% *}" == "$expected" ]]
# nodeadm v1.0.20 requires GNU format: digest, space, filename.
printf '%s data\n' "$expected" > "$item_id/data.sha256"
done < queue.tsv
sha256sum manifest.json plan.json upstream.yaml a*/data a*/data.sha256 \
> checksums.sha256
sha256sum --strict --check checksums.sha256
printf '%s\n' 'Six artifacts verified locally; publishing and node installation remain separate.'
```
아티팩트당 256 MiB 제한은 의도된 제한입니다. 승인한 파일이 더 크면 검토 후 조정합니다. 다운로드 실패, 잘못된 체크섬 또는 해시 불일치는 스크립트를 중단합니다. 부분 디렉터리는 조사용으로 보존하고 원인 해결 후 새 후보를 만듭니다. 공유 `/tmp` 디렉터리를 삭제하거나 누락 체크섬을 건너뛰지 않습니다.
이 해시는 선택한 바이트와 가져온 체크섬의 일치를 확인합니다. 독립적인 서명이나 침해된 게시자가 신뢰할 수 있다는 증거는 아닙니다. 검토한 manifest·체크섬 기록을 보호하고 게시자의 검증 수단을 사용할 수 있으면 함께 적용합니다.
## 프라이빗 S3 게시와 권한
Block Public Access, 승인한 암호화, 버전 관리·보존 정책과 분리된 게시자·읽기 권한을 갖춘 **사전 생성된 소유 버킷**을 사용합니다. 아래 예제는 버킷을 만들거나 정책을 교체하지 않습니다. AccessDenied, 자격 증명 만료, 시간 초과는 실패이며 버킷·객체가 없다는 증거가 아닙니다.
기존 버킷에 적용할 수 있는 *읽기 정책 statement* 예제:
```json
{
"Sid": "ReadApprovedHybridArtifacts",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111122223333:role/HybridArtifactReader"},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-hybrid-artifacts/hybrid-candidates/*",
"Condition": {"StringEquals": {"aws:SourceVpce": "vpce-0123456789abcdef0"}}
}
```
계정, 역할, 버킷, 접두사와 엔드포인트를 검토한 값으로 바꿉니다. 이 statement는 한 접근 경로를 허용하며 다른 기존 허용을 취소하지 않습니다. 한 엔드포인트 이외의 모든 요청에 버킷 전체 `Deny s3:*`를 적용하면 연결된 게시자와 관리자 복구 경로도 차단할 수 있습니다. 이런 경계 정책을 적용하기 전에 각 경로를 설계합니다.
지정한 Principal 정책은 서명된 요청을 요구합니다. **노드에 IAM 역할이 있다고 nodeadm의 일반 HTTPS 다운로더가 IAM 인증 S3 클라이언트로 바뀌지 않습니다.** 다음 방식 중 선택하여 검증합니다.
1. 인증된 준비 에이전트·CLI로 승인한 파일을 가져오고, 자체 호스트명 인증서와 적절한 네트워크 접근 통제가 있는 조직 내부 HTTPS 아티팩트 서비스를 통해 제공합니다.
2. 실행 시 바이너리 미러에 의존하지 않는 사전 설치 이미지 방식을 사용합니다.
프라이빗 S3 객체 URL의 `403`은 DNS 오버라이드로 해결되지 않습니다. Bearer presigned URL이나 자격 증명을 manifest, 프로세스 인자, 공개 로그에 넣지 않습니다. 조직이 비밀이 아닌 바이너리에 한해 프라이빗 네트워크로 제한한 비인증 읽기를 선택한다면 별도 검토할 정책이며 위의 Principal 지정 정책과 다릅니다.
`publish-plan.sh`로 저장합니다. 버킷 소유자가 후보와 권한을 승인한 뒤에만 실행하며 **S3 객체를 씁니다**.
```bash
#!/usr/bin/env bash
# Owner-approved publication only; creates billable S3 objects, never a bucket.
set -euo pipefail
umask 077
cd -- "${1:?Use a verified plan directory}"
: "${REGION:?}" "${BUCKET:?}" "${EXPECTED_ACCOUNT_ID:?}" "${PREFIX:?}"
[[ "$EXPECTED_ACCOUNT_ID" =~ ^[0-9]{12}$ ]]
[[ "$PREFIX" =~ ^hybrid-candidates/[A-Za-z0-9-]+$ ]]
sha256sum --strict --check checksums.sha256
aws s3api head-bucket --region "$REGION" --bucket "$BUCKET" \
--expected-bucket-owner "$EXPECTED_ACCOUNT_ID"
# Manifest is last. Any failed write stops; retain the partial prefix for review.
for file in a{00..05}/data a{00..05}/data.sha256 checksums.sha256 \
upstream.yaml plan.json manifest.json; do
test -f "$file"
aws s3api put-object --region "$REGION" --bucket "$BUCKET" \
--expected-bucket-owner "$EXPECTED_ACCOUNT_ID" \
--key "$PREFIX/$file" --body "$file" --if-none-match '*' \
--server-side-encryption AES256 --checksum-algorithm SHA256 \
--output json > "${file//\//_}.upload.json"
done
printf '%s\n' 'Candidate uploaded. Verify readback, mirror URL mapping and hashes before promotion.'
```
SSE-KMS를 요구하는 버킷에서는 AES256 예제 대신 승인한 키와 KMS 권한을 사용합니다. 조건부 쓰기는 기존 키 덮어쓰기를 막지만 다중 객체 업로드를 원자적으로 만들지는 않습니다. Manifest는 마지막에 올리며, 실패한 후보는 재읽기와 실제 HTTPS 미러 매핑 확인 전까지 배포 대상으로 승격하지 않습니다. 반환된 객체 VersionId·체크섬과 보존 정책을 기록합니다. S3 ETag는 모든 경우에 SHA-256 다이제스트인 값이 아닙니다.
## DNS와 프라이빗 엔드포인트 요구 사항
`hybrid-assets.eks.amazonaws.com`은 AWS CloudFront 다운로드 호스트입니다. 이 이름의 PHZ를 만들고 S3에 별칭으로 연결해도 다음이 보존되지 않습니다.
- TLS 인증서·SNI 호스트명
- HTTP Host 헤더와 S3 버킷·객체 키 매핑
- 원래 경로, 특히 업로더가 모든 파일을 basename으로 평탄화한 경우
- 요청 권한
TLS 검사를 꺼서 해결하지 않습니다. 승인한 원본에 통제된 프록시로 접근하거나, 사전 설치 이미지를 사용하거나, 실제 미러 URL을 지정한 manifest override를 사용합니다.
S3 **Interface** 엔드포인트는 VPN/Direct Connect로 온프레미스 클라이언트에 서비스를 제공할 수 있으며 S3 프라이빗 DNS도 지원됩니다. **Inbound Resolver에만 프라이빗 DNS 적용** 옵션은 VPC 측 S3 gateway 엔드포인트를 유지해야 합니다. 또는 VPC와 온프레미스 요청을 모두 interface 엔드포인트로 보낼 수 있습니다. Gateway 엔드포인트만으로는 온프레미스에서 직접 접근할 수 없습니다.
리전의 첫 S3 엔드포인트가 아니라 검토한 VPC·엔드포인트 ID를 선택합니다. [네트워크 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/02-network-configuration.md)의 DNS·라우팅 절차를 사용합니다. EKS 관리 API 엔드포인트는 Kubernetes API 엔드포인트가 아니며, 프라이빗 ECR 엔드포인트는 public ECR이나 CloudFront 일반 접근을 제공하지 않습니다.
## 준비한 노드의 설치와 초기화
사용자 지정 IAM Roles Anywhere 경로에서는 런타임, OS 의존성, 승인한 nodeadm과 미러 서비스가 이미 준비되어 있어야 합니다. 다음 명령은 대상 노드에 소프트웨어를 설치합니다.
```bash
set -euo pipefail
: "${APPROVED_PATCH:?}" "${REGION:?}" "${LOCAL_MANIFEST:?Absolute local path}"
[[ "$LOCAL_MANIFEST" = /* ]]
test -s "$LOCAL_MANIFEST"
sudo nodeadm install "$APPROVED_PATCH" --region "$REGION" \
--credential-provider iam-ra --containerd-source none \
--manifest-override "file://$LOCAL_MANIFEST" --private-mode
```
[사전 요구 사항](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/01-prerequisites.md)과 [노드 부트스트랩](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md)에 따라 노드별 설정을 준비합니다. 예를 들어 SSM 설정의 **형태**는 다음과 같습니다.
```yaml
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
name: my-hybrid-cluster
region: ap-northeast-2
hybrid:
ssm:
activationCode: REPLACE_WITH_NODE_ACTIVATION_CODE
activationId: REPLACE_WITH_NODE_ACTIVATION_ID
```
노드에 설치한 공급자와 동일한 공급자를 사용합니다. 이 SSM 형태는 위 IAM Roles Anywhere 명령용 설정이 아닙니다. 값을 채운 파일은 root 소유, `0600`으로 보호하고 커밋하지 않으며 비밀을 셸 기록에 넣지 않습니다. API 엔드포인트·CA 필드를 수동으로 채워도 문서화된 클러스터 조회·인증 경로가 없어지지 않습니다.
```bash
# Local config validation; this is not a join or an end-to-end network test.
sudo nodeadm config check --config-source file:///etc/eks/nodeconfig.yaml
```
네트워크, 신원과 CNI 요구 사항을 확인한 뒤 소유자가 `nodeadm init`을 실행합니다. 프라이빗 manifest 경로에서는 승인한 manifest를 다시 전달합니다.
```bash
# Mutates the target node and registers it with EKS.
sudo nodeadm init --config-source file:///etc/eks/nodeconfig.yaml \
--manifest-override file:///etc/eks/manifest.json --private-mode
```
확인한 버전에는 `nodeadm init --dry-run`이 없습니다. 준비가 불완전한 상태를 통과시키려고 초기화 검증을 건너뛰지 않습니다.
## 컨테이너 이미지 전달
프라이빗 ECR 풀에는 ECR API·DKR 경로, S3 레이어 다운로드 경로, DNS와 이미지 풀 권한이 필요합니다. `describe-repositories` 성공만으로 이미지 레이어 다운로드를 증명할 수 없습니다. Pull-through cache를 미리 채우고 시험합니다. ECR 엔드포인트 문서는 처음 캐시되지 않은 이미지를 가져올 때 추가 인터넷 요구 사항을 설명합니다.
배포한 애드온 manifest의 실제 레지스트리 계정·리전·이미지 참조를 사용합니다. Kubernetes 패치에 `-eksbuild.1`을 붙여 이미지 태그를 만들거나 다른 클러스터의 오래된 pause·CoreDNS 버전을 복사하지 않습니다.
nodeadm은 ECR helper를 `/etc/eks/image-credential-provider/ecr-credential-provider`에 설치하고 설정을 `/etc/eks/image-credential-provider/config.json`에 초기화합니다. 다른 디렉터리에 사용되지 않는 파일을 쓰기보다 생성된 kubelet 구성을 확인합니다. `ctr images pull`은 별도 클라이언트이며 kubelet의 exec credential provider를 자동 사용하지 않습니다.
### 오프라인 이미지 전달
승인한 다이제스트와 필요한 플랫폼을 선택합니다. 다중 플랫폼 아카이브는 소스·대상 형식이 지원하는 경우 인덱스와 다이제스트를 보존합니다.
```bash
# Preparation host: downloads images; requires reviewed registry authentication.
set -euo pipefail
: "${SOURCE_DIGEST_REF:?registry/repository@sha256:approved-digest}"
: "${NEW_IMAGE_DIR:?New directory}"
[[ "$SOURCE_DIGEST_REF" =~ @sha256:[0-9a-f]{64}$ ]]
mkdir -m 700 -- "$NEW_IMAGE_DIR"
skopeo copy --all --preserve-digests "docker://$SOURCE_DIGEST_REF" \
"oci-archive:$NEW_IMAGE_DIR/image.tar:approved"
(cd "$NEW_IMAGE_DIR" && sha256sum image.tar > image.tar.sha256)
```
아카이브와 독립적으로 보호한 승인·해시 기록을 전달합니다. 가져오기나 푸시 전에 해당 디렉터리에서 `sha256sum --strict --check image.tar.sha256`을 실행하고 실패하면 중단합니다. 내부 레지스트리로 보내는 경우:
```bash
# Internal staging host: writes an image to the reviewed destination registry.
set -euo pipefail
: "${DEST_DIGEST_REF:?approved-registry/repository@sha256:approved-digest}"
[[ "$DEST_DIGEST_REF" =~ @sha256:[0-9a-f]{64}$ ]]
sha256sum --strict --check image.tar.sha256
skopeo copy --all --preserve-digests oci-archive:image.tar:approved \
"docker://$DEST_DIGEST_REF"
```
레지스트리 TLS 검증을 끄지 않습니다. 압축·manifest 형식을 바꾸면 다이제스트를 보존하지 못할 수 있으므로 중단하고 결과 식별자를 검토합니다. 변경되지 않았다고 조용히 가정하지 않습니다.
containerd에 직접 미리 넣는 방식도 가능하지만 배포 런타임에서 검증해야 합니다. Kubernetes는 `k8s.io` 네임스페이스를 사용하며 가져온 이미지 참조가 Pod·샌드박스 참조와 일치하고 필요한 플랫폼 blob이 모두 있어야 합니다. 아카이브의 `approved` annotation이 Pod가 요청하는 레지스트리 이름으로 자동 변환되지는 않습니다. 이미지 가비지 컬렉션과 `imagePullPolicy` 때문에 이후 풀을 다시 시도할 수도 있습니다. Tar 가져오기 성공만으로 오프라인 Pod 시작을 증명하지 못합니다.
## 서명된 로컬 패키지 저장소
OS 릴리스, 아키텍처, 전이 의존성과 메타데이터를 검토한 조합으로 미러링합니다. 공급자 서명을 보존하거나 별도로 신뢰한 키로 조직 관리 저장소에 서명합니다.
**준비와 서명이 완료된** 로컬 flat 저장소용 Ubuntu 클라이언트 설정 예제:
```text
deb [signed-by=/etc/apt/keyrings/hybrid-mirror.gpg] file:///srv/apt-repo ./
```
저장소에는 `Packages.gz`만 아니라 유효한 `Release`와 `InRelease` 또는 `Release.gpg`가 필요합니다. 키 지문은 독립적인 신뢰 경로로 배포·검증합니다. 저장소 인증을 생략하려고 `trusted=yes`를 사용하지 않습니다.
DNF/YUM 저장소 설정 예제:
```ini
[hybrid-local]
name=Reviewed hybrid packages
baseurl=file:///srv/yum-repo
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-hybrid-mirror
```
유효한 패키지 서명과 서명된 저장소 메타데이터가 필요합니다. 메타데이터와 패키지 서명 키가 다를 수 있으므로 승인한 키 집합을 구성합니다. 검증 실패 시 `gpgcheck=0`으로 바꾸지 않습니다. 패키지 설치·서비스 재시작은 이미지 빌드나 drain한 유지보수 작업에서 수행하며 임의의 운영 노드 검증 스크립트에 넣지 않습니다.
## 프록시 구성
클라이언트별 목적지 표를 먼저 작성합니다. Loopback, 실제 프라이빗 API·레지스트리 이름과 프록시를 우회해야 하는 노드·Pod·Service 대역을 포함합니다. CIDR·접미사 매칭은 클라이언트마다 다릅니다. **`.eks.amazonaws.com`**을 무조건 `NO_PROXY`에 넣지 않습니다. 퍼블릭 다운로드 호스트 `hybrid-assets.eks.amazonaws.com`도 이 접미사와 일치합니다.
검토한 비밀이 아닌 프록시 설정의 셸 예제:
```bash
export HTTP_PROXY=http://proxy.internal.example.com:3128
export HTTPS_PROXY=http://proxy.internal.example.com:3128
export NO_PROXY=localhost,127.0.0.1,::1,.svc,.cluster.local,registry.internal.example.com
export http_proxy="$HTTP_PROXY" https_proxy="$HTTPS_PROXY" no_proxy="$NO_PROXY"
```
실제 프라이빗 API 호스트명·IP와 다른 우회 목적지를 추가합니다. 이 예제는 사이트 전체 완성 설정이 아닙니다. 로그인 셸 환경은 기존 systemd 서비스를 설정하지 않습니다. `/etc/environment`를 source하거나 반복해서 내용을 덧붙이지 않습니다.
`containerd.service`와 `kubelet.service`에는 `/etc/systemd/system/UNIT.service.d/http-proxy.conf`의 소유자 관리 drop-in을 사용합니다.
```ini
[Service]
Environment="HTTP_PROXY=http://proxy.internal.example.com:3128"
Environment="HTTPS_PROXY=http://proxy.internal.example.com:3128"
Environment="NO_PROXY=localhost,127.0.0.1,::1,.svc,.cluster.local,registry.internal.example.com"
```
기존 drop-in을 확인하고 승인한 빌드·유지보수 단계에서만 해당 unit을 재시작합니다. containerd TOML의 `[proxy.http]`는 HTTP 프록시 설정이 아닙니다.
| 구성 요소 | 설정과 조건 |
|---|---|
| nodeadm 프로세스 | `sudo`에 검토한 프록시 환경만 전달. `sudo -E`로 운영자 환경 전체를 전달하지 않음 |
| containerd / kubelet | 각각의 systemd 환경. 생성된 kubelet 구성과 호스트 환경은 다른 계층 |
| 문서화된 snap 설치를 사용한 Ubuntu SSM | `snap.amazon-ssm-agent.amazon-ssm-agent.service.d/http-proxy.conf` |
| AL2023/RHEL SSM | `amazon-ssm-agent.service.d/http-proxy.conf`; 실제 설치 unit 확인 |
| IAM Roles Anywhere credential process | nodeadm이 `--with-proxy`를 생성할 때 프록시 환경 감지. 호출하는 데몬에도 올바른 환경 필요 |
| `spec.hybrid.enableCredentialsFile: true`인 IAM Roles Anywhere | 이 모드에는 `aws_signing_helper_update.service`가 **실제로 존재**. 초기화 전에 drop-in 준비. 모든 IAM Roles Anywhere 설치에 있다고 가정하지 않음 |
| apt | `/etc/apt/apt.conf.d/`의 소유자 관리 파일에 `Acquire::http::Proxy`, `Acquire::https::Proxy` |
| snap | 실제로 snap을 사용하는 경우 `snap set system proxy.http=... proxy.https=...` |
| dnf / yum | 기존 구성의 `proxy` 설정을 검토·수정. 다른 설정 교체나 중복 추가 금지 |
| kube-proxy / 다른 Pod | 해당 트래픽에 프록시가 필요한 경우 Pod 환경 설정 |
문서화된 프록시 토폴로지에서는 클러스터 생성 후 Hybrid Node를 조인하기 전에 kube-proxy를 구성합니다. 기존 `NODE_NAME` 환경과 모든 명령 인자를 유지합니다. 다음은 독립 DaemonSet이 아닌 **strategic merge patch 조각**입니다.
```yaml
spec:
template:
spec:
containers:
- name: kube-proxy
env:
- name: HTTP_PROXY
value: http://proxy.internal.example.com:3128
- name: HTTPS_PROXY
value: http://proxy.internal.example.com:3128
- name: NO_PROXY
value: localhost,127.0.0.1,::1,.svc,.cluster.local
```
우회 목적지를 검토·추가하고 애드온 소유자를 통해 적용합니다. 내장 DaemonSet의 strategic merge는 컨테이너·환경 변수 이름을 사용합니다. JSON Patch의 `add /containers/0/env`는 기존 환경 전체를 교체할 수 있고 컨테이너 순서를 가정합니다. 선택한 CNI가 kube-proxy를 대체한다면 이 예제를 위해 kube-proxy를 배포하지 않습니다.
## 검증과 통제된 업데이트
| 검사 | 필요한 증거 | 충분하지 않은 검사 |
|---|---|---|
| 아티팩트 무결성 | 선택한 모든 파일·체크섬·승인 manifest·버전 조합 일치 | 누락 파일 건너뛰기, 검증 파일 0개 수용 |
| DNS/TLS | 의도한 경로의 정확한 목적지와 호스트명·CA 검증 | `10.*` 주소, 임의의 `172.*` 주소, `curl -k` |
| S3 | 정확한 버킷·키·버전의 권한 있는 재읽기, 예상 소유자와 해시 | 접두사 목록 조회, API 오류를 부재로 취급 |
| ECR | 워크로드 자격 증명 경로로 필요한 다이제스트·플랫폼·레이어 풀 | `describe-repositories`, 독립된 비인증 `ctr` 호출 |
| nodeadm 구성 | 보호되고 실제 값이 채워진 파일의 `nodeadm config check` 성공 | 설정 누락을 성공 처리, 존재하지 않는 `init --dry-run` |
| 노드 운영 | 자격 증명 갱신, Kubernetes API 신뢰·인증, CNI/DNS와 제한된 워크로드 시험 | 로컬 파서 성공 또는 `/healthz` 응답 한 번 |
AWS 읽기가 승인된 환경에서 문서화된 연결·신원 진단인 `nodeadm debug --config-source file:///etc/eks/nodeconfig.yaml`을 사용합니다. 서비스에 접속하고 민감한 진단 정보를 출력할 수 있으므로 비공개로 보관하고 공유 전에 정제합니다. 알 수 없거나 실패한 검사를 “운영 준비 완료”로 바꾸지 않습니다.
업데이트 자동화는 **후보 탐색**을 수행한 뒤 출처 검증, 호환성 검토, OS·이미지 검사, 로컬 검증, 대표 노드 canary와 승인을 거쳐 승격하도록 구성합니다. 새 불변 버전·빌드 접두사에 게시하고 이전 승인 조합을 보존하며 롤백 제약을 기록합니다. 운영 `latest` 키를 조용히 덮어쓰는 cron을 사용하지 않습니다. `nodeadm upgrade`는 워크로드 이동이 필요한 중단 작업입니다.
이전 퀴즈의 대역폭 추정치인 레이어 캐싱 **50–80%**, 압축 **30–50%**, 플랫폼 필터링 **50%**에는 확인 가능한 측정 출처가 없었습니다. 예측 절감률이 아닌 미검증 역사적 예시로만 보존합니다. 실제 레이어 재사용, 플랫폼과 압축 형식으로 전송량을 측정하며, 승인한 콘텐츠를 재압축하면서 다이제스트가 그대로라고 주장하지 않습니다.
## 공식 참고 자료
- [AWS Hybrid nodeadm 레퍼런스](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-nodeadm.html)
- [Hybrid OS 준비](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-os.html)
- [Hybrid 프록시 구성](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-proxy.html)
- [nodeadm v1.0.20 install 옵션](https://github.com/aws/eks-hybrid/blob/v1.0.20/cmd/nodeadm/install/install.go), [init 옵션](https://github.com/aws/eks-hybrid/blob/v1.0.20/cmd/nodeadm/init/init.go)
- [아티팩트 선택·다운로드 구현](https://github.com/aws/eks-hybrid/blob/v1.0.20/internal/aws/source.go), [SSM 소스](https://github.com/aws/eks-hybrid/blob/v1.0.20/internal/ssm/source.go)
- [S3 interface 엔드포인트·프라이빗 DNS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/privatelink-interface-endpoints.html)
- [ECR VPC 엔드포인트](https://docs.aws.amazon.com/AmazonECR/latest/userguide/vpc-endpoints.html)
- [Skopeo copy](https://github.com/containers/skopeo/blob/main/docs/skopeo-copy.1.md)
- [APT 저장소 인증](https://manpages.ubuntu.com/manpages/noble/man8/apt-secure.8.html)
- [DNF 저장소 서명 설정](https://github.com/rpm-software-management/dnf/blob/master/doc/conf_ref.rst)
- [S3 PutObject 조건·암호화·체크섬](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html)
- [Kubernetes 이미지 이름·다이제스트·풀 정책](https://kubernetes.io/docs/concepts/containers/images/)
< [이전: 네트워크 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/02-network-configuration.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: 노드 부트스트랩](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-hybrid-nodes/04-node-bootstrap
----------------------------------------
# 노드 부트스트랩
< [이전: 인터넷 제한 환경 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/03-airgap-setup.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: GPU 서버 통합](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/05-gpu-integration.md) >
> **지원 버전**: EKS Hybrid Nodes; nodeadm v1.0.20 소스 확인. 대상 클러스터에 지원되는 OS·Kubernetes·CNI 조합을 선택합니다.
> **마지막 업데이트**: 2026년 9월 12일
이 문서는 준비한 온프레미스 호스트를 EKS에 연결합니다. 소프트웨어 설치, AWS 신원, 초기화와 실제 워크로드 준비 확인을 구분합니다. 예제의 변경 작업에는 대상 클러스터·호스트 식별과 운영자 승인이 필요합니다. 로컬 검증은 운영 네트워크, OS 이미지, PKI 또는 워크로드를 시험했다는 증거가 아닙니다.
## 부트스트랩 흐름
1. [사전 요구 사항](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/01-prerequisites.md)과 [네트워크 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/02-network-configuration.md)에 따라 지원 OS·런타임, 고유 호스트 신원, 시각 동기화, 라우팅, DNS와 방화벽을 준비합니다.
2. **Hybrid Nodes IAM 역할**, SSM 또는 IAM Roles Anywhere 공급자와 `HYBRID_LINUX` 타입 클러스터 액세스 항목을 준비합니다. 일반 SSM 관리 역할만으로 충분하지 않습니다.
3. Hybrid nodeadm 릴리스를 검증한 뒤 호스트 또는 통제된 OS 이미지 빌드에서 의존성을 설치합니다.
4. 정확히 한 공급자를 사용하는 보호된 노드별 NodeConfig를 전달합니다. 필요하면 프록시·레지스트리 신뢰를 구성합니다.
5. 식별한 호스트에서 `nodeadm config check` 후 `nodeadm init`을 실행합니다.
6. 지원 Hybrid CNI와 필요한 애드온을 구성합니다. 등록된 Node도 `NotReady`일 수 있습니다.
7. 정확한 Node 신원, Ready 조건, CNI/DNS, 자격 증명 갱신과 제한된 워크로드를 확인한 뒤 서비스에 투입합니다.
## nodeadm과 의존성 설치
Hybrid nodeadm은 EC2용 `amazon-eks-ami`가 아닌 **`aws/eks-hybrid`**에서 제공합니다. 오래된 SSM 서명 키 문제 때문에 SSM 신규 설치·업그레이드에는 **nodeadm 1.0.19 이상**이 필요합니다. 이 검토는 릴리스된 **v1.0.20 소스**를 사용했습니다.
승인한 릴리스를 HTTPS 검증과 함께 받고 독립적으로 승인한 기록의 다이제스트와 비교한 뒤 root로 실행합니다. 변경 가능한 `latest` 다운로드로 `/usr/local/bin/nodeadm`을 무조건 교체하지 않습니다. [인터넷 제한 환경 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/03-airgap-setup.md)은 이미지 준비와 확인한 릴리스의 private manifest 옵션을 다룹니다. 감사 환경에서 공식 아티팩트 호스트의 TLS 호스트명 검증이 실패한 뒤 nodeadm 바이너리를 받거나 실행하지 않았습니다.
```bash
# Target host/image builder: installs software. Choose exactly one provider.
set -euo pipefail
: "${KUBERNETES_VERSION:?Approved cluster-compatible version}"
: "${CREDENTIAL_PROVIDER:?ssm or iam-ra}" "${REGION:?}"
: "${CONTAINERD_SOURCE:?distro, docker or none for this OS}"
case "$CREDENTIAL_PROVIDER" in ssm|iam-ra) ;; *) exit 1 ;; esac
case "$CONTAINERD_SOURCE" in distro|docker|none) ;; *) exit 1 ;; esac
sudo nodeadm install "$KUBERNETES_VERSION" \
--credential-provider "$CREDENTIAL_PROVIDER" \
--containerd-source "$CONTAINERD_SOURCE" --region "$REGION" --timeout 20m
```
새 노드는 컨트롤 플레인의 현재 마이너 버전을 우선합니다. kubelet 지원 스큐는 호환성 허용 범위이며 오래되어 지원되지 않는 EKS 버전을 설치할 이유가 아닙니다. kubelet은 API 서버보다 새 버전일 수 없으며 업스트림 스큐 정책 외에 EKS·CNI 지원도 확인합니다.
RHEL은 `distro`를 지원하지 않습니다. 문서화된 Docker 패키지 소스나 호환 런타임 사전 설치 후 `none`을 사용합니다. AL2023은 `docker`를 지원하지 않습니다. `none`은 containerd 설치를 건너뜁니다. Private mode도 OS 패키지를 건너뛰므로 누락된 런타임 의존성을 제공하지 않습니다.
| 구성 요소 | 문서화된 설치 위치 |
|---|---|
| kubelet | `/usr/bin/kubelet` |
| kubectl | `/usr/local/bin/kubectl` |
| ECR credential provider | `/etc/eks/image-credential-provider/ecr-credential-provider` |
| AWS IAM authenticator / signing helper | `/usr/local/bin/aws-iam-authenticator` / `/usr/local/bin/aws_signing_helper` |
| SSM setup CLI | `/opt/ssm/ssm-setup-cli` |
| SSM Agent | Ubuntu snap: `/snap/amazon-ssm-agent/current/amazon-ssm-agent`; AL2023/RHEL: `/usr/bin/amazon-ssm-agent` |
| containerd | Ubuntu/AL2023: `/usr/bin/containerd`; RHEL 문서는 `/bin/containerd` 사용 |
| nodeadm tracker | `/opt/nodeadm/tracker` |
`nodeadm install`은 서명된 setup CLI로 SSM 에이전트를 설치·설정하며 **SSM 등록은 `init`에서 수행**합니다. 수동 `dpkg`와 다른 nodeadm 설치 프로그램을 조합하는 절차가 아닙니다.
## NodeConfig와 자격 증명
### SSM
클러스터 이름·리전과 준비한 Hybrid 역할에 대한 유효한 활성화 코드·ID를 제공합니다.
```yaml
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
name: my-hybrid-cluster
region: ap-northeast-2
hybrid:
ssm:
activationCode: REPLACE_WITH_ACTIVATION_CODE
activationId: REPLACE_WITH_ACTIVATION_ID
kubelet:
config:
maxPods: 110
shutdownGracePeriod: 30s
shutdownGracePeriodCriticalPods: 10s
flags:
- --node-labels=workload.example.com/location=onprem
```
이는 입력 템플릿이며 실제 비밀을 커밋하는 예제가 아닙니다. root 소유·`0600`으로 보호하고 로그나 셸 기록에 값을 노출하지 않고 전달합니다. 클러스터 조회는 Hybrid 역할의 EKS 권한으로 API 엔드포인트와 CA를 가져옵니다. Base64 CA 필드에 넣은 원본 PEM 블록은 API 표현과 바꿔 쓸 수 없습니다.
`maxPods`와 종료 시간은 계획 예시이며 용량 보장이 아닙니다. CNI 할당·예약 IP, 리소스와 워크로드 종료 요구 사항을 확인합니다. 선택적인 `NoSchedule` taint에는 워크로드·CNI의 대응 toleration이 필요합니다. 필수 에이전트나 애플리케이션을 조용히 막는 기본 taint를 추가하지 않습니다.
활성화는 자격 증명 소유자를 통해 준비한 Hybrid 역할, 올바른 리전, 명시적인 만료 시각과 제한된 등록 수로 생성합니다. 코드를 출력하기보다 응답을 비공개 파일에 저장합니다.
```bash
# AWS write: owner-approved activation for one new host.
set -euo pipefail
umask 077
: "${REGION:?}" "${HYBRID_ROLE_NAME:?Prepared Hybrid Nodes IAM role name}"
: "${ACTIVATION_EXPIRY:?Future UTC timestamp within SSM service limits}"
test ! -e activation.json
aws ssm create-activation --region "$REGION" \
--iam-role "$HYBRID_ROLE_NAME" --registration-limit 1 \
--expiration-date "$ACTIVATION_EXPIRY" \
--default-instance-name eks-hybrid-node > activation.json
```
실패했거나 결과를 알 수 없는 요청을 무조건 다시 생성하지 말고 자격 증명 소유자와 확인합니다. SSM은 등록된 `mi-*` 관리형 인스턴스 ID를 Node 이름으로 사용합니다. 일반 재부팅에서는 SSM 등록과 갱신되는 임시 자격 증명을 재사용합니다. 재등록에는 만료되지 않았고 등록 여유가 남은 활성화가 필요하며, 한도뿐 아니라 만료 시각도 중요합니다.
### IAM Roles Anywhere
[사전 요구 사항](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/01-prerequisites.md)에서 준비한 trust anchor, 활성 프로필, Hybrid 역할과 노드별 인증서·키를 사용합니다. 이 문서를 실행할 때마다 trust anchor·프로필을 추가 생성하지 않습니다.
```yaml
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
name: my-hybrid-cluster
region: ap-northeast-2
hybrid:
iamRolesAnywhere:
nodeName: hybrid-node-001
trustAnchorArn: arn:aws:rolesanywhere:ap-northeast-2:111122223333:trust-anchor/REPLACE_ID
profileArn: arn:aws:rolesanywhere:ap-northeast-2:111122223333:profile/REPLACE_ID
roleArn: arn:aws:iam::111122223333:role/EKSHybridNodeRole
certificatePath: /etc/iam/pki/server.pem
privateKeyPath: /etc/iam/pki/server.key
```
경로는 예시이며 여러 머신에서 인증서 하나를 재사용하라는 의미가 아닙니다. `nodeName`을 역할 신뢰 정책의 인증서 속성과 결합합니다. 일반적인 CN 조건에서는 두 값이 일치해야 합니다. 이름은 노드 명명 규칙을 따르고 최대 64자여야 합니다. 프로필의 **`acceptRoleSessionName: true`**를 활성화합니다.
역할 최대값을 12시간으로 설정하는 것만으로 세션이 늘어나지 않습니다. 요청·프로필의 유효 세션 시간은 역할 `MaxSessionDuration` 이하여야 하며 같은 값도 허용됩니다. 보안·가용성 요구 사항에 맞게 선택하고 갱신을 확인합니다. 정적 IAM 사용자 액세스 키는 세 번째 Hybrid nodeadm 지원 공급자가 아닙니다.
## 프라이빗 레지스트리 신뢰
내부 레지스트리 소유자를 통해 CA 지문과 인증서 호스트명을 확인합니다. 적절한 경우 레지스트리 범위 신뢰를 우선합니다. 서버가 제시한 임의 인증서를 전역 신뢰에 추가하거나 호스트명 검증을 끄지 않습니다.
승인한 레지스트리의 `hosts.toml` 예제:
```toml
# /etc/containerd/certs.d/registry.internal.example.com/hosts.toml
server = "https://registry.internal.example.com"
[host."https://registry.internal.example.com"]
capabilities = ["pull", "resolve"]
ca = "/etc/containerd/certs.d/registry.internal.example.com/ca.crt"
```
대응하는 검토된 CA 파일이 각 노드에 있어야 합니다. containerd 1.x에서는 `config_path`가 `plugins."io.containerd.grpc.v1.cri".registry` 아래에, containerd 2.x/config version 3에서는 `plugins."io.containerd.cri.v1.images".registry` 아래에 위치합니다. nodeadm이 생성한 `/etc/containerd/config.toml`을 확인하고 설치한 런타임에 맞는 절을 사용합니다. 폐기 예정 inline registry `configs/auth`와 `config_path` 방식을 섞거나 NodeConfig에 레지스트리 비밀번호를 넣지 않습니다.
조직에 시스템 전체 CA 신뢰가 필요하면 Ubuntu는 `/usr/local/share/ca-certificates/`와 `update-ca-certificates`, RHEL/AL2023은 `/etc/pki/ca-trust/source/anchors/`와 `update-ca-trust extract`를 사용합니다. RHEL 예제에서 Ubuntu 경로를 참조하지 않습니다. 일반 ECR 인증서는 공인 신뢰를 사용하지만 OS에 정상 CA bundle은 필요합니다. 레지스트리 신뢰와 인증은 별개입니다.
## 초기화와 확인
```bash
# Identified target host; init changes local configuration and joins EKS.
set -euo pipefail
sudo nodeadm config check --config-source file:///etc/eks/nodeconfig.yaml
sudo nodeadm init --config-source file:///etc/eks/nodeconfig.yaml
```
프라이빗 manifest 설치에서는 [인터넷 제한 환경 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/03-airgap-setup.md)에 따라 `init`에 승인한 `--manifest-override`와 `--private-mode`를 전달합니다. 실제 nodeadm·데몬 프로세스에 프록시 설정이 전달되어야 합니다. `config check`는 로컬 설정 검사이며 전체 조인 시험이 아닙니다.
**클러스터 관리자** 워크스테이션에서 의도한 kubeconfig context와 정확한 예상 Node 이름(SSM `mi-*` 또는 승인한 IAM Roles Anywhere 이름)을 확인합니다.
```bash
set -euo pipefail
umask 077
: "${KUBECONFIG:?}" "${CONTEXT:?}" "${EXPECTED_NODE_NAME:?}"
kubectl --kubeconfig "$KUBECONFIG" --context "$CONTEXT" \
get node "$EXPECTED_NODE_NAME" -o json > node-registration.json
jq -e '.metadata.labels["eks.amazonaws.com/compute-type"] == "hybrid"
and (.metadata.uid | type == "string" and length > 0)' node-registration.json
expected_uid=$(jq -er '.metadata.uid' node-registration.json)
# After CNI and required add-ons are ready:
kubectl --kubeconfig "$KUBECONFIG" --context "$CONTEXT" wait \
--for=condition=Ready "node/$EXPECTED_NODE_NAME" --timeout=5m
observed_uid=$(kubectl --kubeconfig "$KUBECONFIG" --context "$CONTEXT" \
get node "$EXPECTED_NODE_NAME" -o jsonpath='{.metadata.uid}')
test "$observed_uid" = "$expected_uid"
```
Node UID를 기록하고 provider·주소를 식별한 물리·가상 호스트와 대조합니다. Ready는 필요 조건이며 DNS, 이미지 풀, 연결, 자격 증명 갱신과 실제 요구 워크로드도 검증합니다. 이전 `v1.31.0` 출력은 역사적 예시이며 새 등록 실측이 아닙니다.
## 사전 설치 호스트용 systemd 자동화
통제된 빌드·설치 단계에서 `nodeadm install`로 소프트웨어를 준비합니다. 아래 자동화는 **초기화만** 실행하고 machine ID, 승인 nodeadm 바이너리, NodeConfig와 선택적 프라이빗 manifest에 결합된 기록을 남깁니다. 변경되었거나 부분 초기화된 상태를 거부하며 등록을 조용히 반복하지 않습니다. 등록 상태, machine ID, 키나 이 상태 디렉터리를 다른 노드로 복제하지 않습니다.
root 소유 `/etc/eks/bootstrap.env` 예제(`0600`):
```text
NODECONFIG_PATH=/etc/eks/nodeconfig.yaml
APPROVED_NODEADM_SHA256=REPLACE_WITH_APPROVED_64_HEX_DIGEST
# Only for the reviewed private-manifest path:
# LOCAL_MANIFEST=/etc/eks/manifest.json
```
아래 스크립트를 root 소유 `/usr/local/bin/eks-hybrid-bootstrap.sh`로 저장합니다. 보호된 상위 디렉터리·파일은 호스트 소유자가 관리해야 하며 악의적인 root에 대한 방어를 제공하는 예제가 아닙니다.
```bash
#!/usr/bin/env bash
# Initialize one preinstalled, uniquely identified host. Run as root.
set -euo pipefail
umask 077
[[ "$EUID" -eq 0 ]]
: "${NODECONFIG_PATH:?Absolute per-node config path}"
: "${APPROVED_NODEADM_SHA256:?Hash from the approved binary record}"
[[ "$NODECONFIG_PATH" = /* ]]
[[ "$APPROVED_NODEADM_SHA256" =~ ^[0-9a-f]{64}$ ]]
NODEADM=/usr/local/bin/nodeadm
STATE=/var/lib/eks-hybrid-bootstrap
private_file() {
local path=$1 owner mode
[[ -f "$path" && ! -L "$path" ]]
owner=$(stat -c '%u' "$path")
mode=$(stat -c '%a' "$path")
[[ "$owner" == 0 && "$mode" == 600 ]]
}
private_file "$NODECONFIG_PATH"
test -s /etc/machine-id
test -s /opt/nodeadm/tracker
test -x "$NODEADM"
actual=$(sha256sum "$NODEADM")
[[ "${actual%% *}" == "$APPROVED_NODEADM_SHA256" ]]
if [[ -e /var/lib/eks/.nodeadm-installed || -e /var/lib/eks/.nodeadm-initialized ]]; then
echo 'Legacy markers found: review existing installation and migrate state manually.' >&2
exit 1
fi
[[ ! -L "$STATE" ]]
mkdir -p -m 700 "$STATE"
[[ "$(stat -c '%u:%a' "$STATE")" == 0:700 ]]
exec 9>"$STATE/lock"
flock -n 9
args=(--config-source "file://$NODECONFIG_PATH")
fingerprint_inputs=(/etc/machine-id "$NODEADM" "$NODECONFIG_PATH")
if [[ -n "${LOCAL_MANIFEST:-}" ]]; then
[[ "$LOCAL_MANIFEST" = /* ]]
private_file "$LOCAL_MANIFEST"
fingerprint_inputs+=("$LOCAL_MANIFEST")
args+=(--manifest-override "file://$LOCAL_MANIFEST" --private-mode)
fi
fingerprint=$(sha256sum "${fingerprint_inputs[@]}" | sha256sum)
fingerprint=${fingerprint%% *}
if [[ -e "$STATE/state" ]]; then
private_file "$STATE/state"
if [[ "$(cat "$STATE/state")" == "$fingerprint init-command-completed" ]]; then
echo 'Matching initialization record; verify current Node readiness separately.'
exit 0
fi
echo 'Changed identity/config or incomplete initialization: manual recovery required.' >&2
exit 1
fi
"$NODEADM" config check --config-source "file://$NODECONFIG_PATH"
printf '%s started\n' "$fingerprint" > "$STATE/state.new"
mv "$STATE/state.new" "$STATE/state"
# Failure/interruption retains started state and prevents an automatic retry.
"$NODEADM" init "${args[@]}"
systemctl is-active --quiet containerd
systemctl is-active --quiet kubelet
printf '%s init-command-completed\n' "$fingerprint" > "$STATE/state.new"
mv "$STATE/state.new" "$STATE/state"
echo 'Init command completed; cluster registration/CNI/readiness still require verification.'
```
상태 기록은 init 명령이 완료되고 그 시점에 두 로컬 서비스가 active였음을 뜻합니다. **Kubernetes Node가 Ready라는 뜻은 아닙니다.** 실패하면 `started` 기록이 남습니다. 운영자가 기록을 정리하기 전에 실제 호스트·SSM·클러스터 상태를 조사합니다. 서비스 시간 초과나 명령 중단이 등록되지 않았다는 증거는 아닙니다.
```ini
# /etc/systemd/system/eks-hybrid-bootstrap.service
[Unit]
Description=Initialize one prepared EKS Hybrid Node
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
EnvironmentFile=/etc/eks/bootstrap.env
ExecStart=/usr/local/bin/eks-hybrid-bootstrap.sh
TimeoutStartSec=10min
RemainAfterExit=true
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
```
호스트 구성 소유자를 통해 설치·활성화합니다. `network-online.target`은 네트워크 관리자의 wait 서비스 기준 부팅 순서이며 DNS, VPN, EKS나 자격 증명 서비스 접근을 보장하지 않습니다. `RemainAfterExit`는 서비스 상태 기록이며 노드 상태가 아닙니다. `EnvironmentFile`은 systemd가 파싱하며 스크립트가 셸 `source`로 실행하지 않습니다.
기존 마커 전용 설치는 명시적으로 마이그레이션합니다. `.nodeadm-installed` / `.nodeadm-initialized` 삭제 후 재부팅만으로 초기화하지 않습니다. Credentials file 활성화와 같은 승인된 설정 변경에는 문서화된 유지보수·init 절차를 사용하고 이후 자동화 기록을 맞춥니다.
일반 재부팅에서는 구성된 kubelet·자격 증명 서비스가 재개됩니다. Kubernetes Node 객체 삭제는 SSM 등록 취소나 kubelet 중지가 아닙니다. 실행 중이고 권한이 유효한 kubelet은 Node를 다시 생성할 수 있습니다. `delete node`를 호스트 폐기로 사용하거나 모든 장애 호스트가 반드시 다시 조인한다고 가정하지 않습니다.
## Cilium CNI
AWS는 현재 자체 유지 관리 **Cilium 1.17.x와 1.18.x** 빌드를 Hybrid Nodes에서 지원합니다. `oci://public.ecr.aws/eks/cilium/cilium`의 게시 예제에는 `1.17.9-0`, `1.18.3-0`이 있습니다. 임의의 더 새로운 업스트림 차트를 선택하라는 의미가 아닙니다.
현재 AWS CNI 페이지는 커널 요구 사항 때문에 **Cilium v1.18.3에서 Ubuntu 20.04와 RHEL 8을 명시적으로 제외**합니다. 커널만 바꾸면 AWS 지원이 성립한다고 주장하지 말고 문서화된 행렬 안의 OS·CNI 조합을 선택합니다.
Calico 예제는 `aws-samples/eks-hybrid-examples`로 이동했습니다. Calico 프로젝트 폐기나 기존 모든 배포의 계속 동작을 보장하는 의미가 아닙니다. 현재 전용 AWS CNI 지원 페이지는 AWS 유지 관리 Cilium 빌드와 지원 기능을 명시합니다.
### 설치 값
이 IPv4 cluster-pool 예제는 검토한 원격 Pod 네트워크가 `10.85.0.0/16`이며 Node/VPC/Service 네트워크와 겹치지 않는다고 가정합니다. **최초 설치 전** 클러스터의 승인한 값으로 바꿉니다.
```yaml
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: eks.amazonaws.com/compute-type
operator: In
values: [hybrid]
ipam:
mode: cluster-pool
operator:
clusterPoolIPv4MaskSize: 25
clusterPoolIPv4PodCIDRList: [10.85.0.0/16]
loadBalancer:
serviceTopology: true
operator:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: eks.amazonaws.com/compute-type
operator: In
values: [hybrid]
unmanagedPodWatcher:
restart: false
envoy:
enabled: false
kubeProxyReplacement: "false"
preflight:
nodeSelector:
eks.amazonaws.com/compute-type: hybrid
```
Preflight selector는 agent affinity와 별개입니다. `/25`에는 주소 128개가 있지만 Cilium cluster-pool은 2개를 예약하므로 사용 가능한 Pod 주소 128개를 보장하지 않습니다. kubelet `maxPods`, hostNetwork Pod와 다른 제약도 함께 검토합니다.
기존 pool 항목이나 `clusterPoolIPv4MaskSize`를 수정하지 않습니다. Cilium은 새 pool 항목 **추가**로 확장하는 방식을 문서화합니다. 먼저 EKS remote network, 라우팅/BGP, 겹침과 용량을 검토합니다. “목록 전체를 절대 확장할 수 없다”는 표현은 지나치게 강합니다.
```bash
# Cluster write; requires approved context, values and supported chart.
set -euo pipefail
: "${KUBECONFIG:?}" "${CONTEXT:?}" "${CILIUM_VERSION:?Approved AWS chart version}"
helm install cilium oci://public.ecr.aws/eks/cilium/cilium \
--version "$CILIUM_VERSION" --namespace kube-system \
--kubeconfig "$KUBECONFIG" --kube-context "$CONTEXT" \
--values cilium-values.yaml --wait --timeout 10m
```
대상 Hybrid Node마다 Cilium agent가 있는지와 operator, Node readiness, 노드 간 트래픽을 확인합니다. Affinity는 이 설치를 Hybrid Node로 한정하며 클라우드 노드 네트워킹은 기존 컨트롤러가 관리합니다. kube-proxy replacement 설계에는 별도 API 접근·마이그레이션 계획이 필요하며 같은 노드에 경쟁하는 kube-proxy 동작을 남기지 않습니다.
### 업그레이드와 제거 경계
Cilium 업그레이드 전에 현재 values·manifest와 명시적인 Helm revision을 저장하고, 버전별 업그레이드 문서를 읽고, 승인한 값으로 대상 차트를 렌더링합니다. Hybrid Node로 한정한 **별도 preflight 릴리스**를 `preflight.enabled=true`, `agent=false`, `operator.enabled=false`로 실행합니다. DaemonSet 대상 커버리지와 검증 Deployment readiness를 **모두** 기다립니다. Preflight 릴리스 생성은 검사 결과가 아닙니다.
통과한 뒤 그 preflight 릴리스만 제거합니다. 검토한 기존 값과 적절한 `upgradeCompatibility`로 업그레이드합니다. `--reuse-values`로 오래된 설정을 마이너 버전 사이에 무조건 넘기지 않습니다. 롤백은 명시적으로 확인한 revision과 CNI 상태·CRD 호환성 검토가 필요하며 데이터 경로 복구를 자동 보장하지 않습니다.
CNI 제거는 중단이 발생하는 폐기·마이그레이션 작업입니다. 워크로드를 이동하고 Cilium에 의존하는 노드·정책·CR을 확인합니다. 공유 클러스터에서 `kubectl get crds | grep cilium | xargs kubectl delete`를 실행하지 않습니다. Helm uninstall은 호스트 라우트·인터페이스·BPF 상태 제거를 보장하지 않습니다. 식별하고 비운 호스트에서 버전별 CNI 정리 절차를 사용하며 경로 삭제 전 활성 mount와 보존 데이터를 확인합니다.
## Bottlerocket은 별도 부트스트랩 계약 사용
AWS는 **1.37.0**부터 지원 x86_64 Kubernetes variant와 사전 요구 사항을 갖춘 VMware Bottlerocket을 지원합니다. nodeadm을 **사용하지 않습니다**. AWS 가이드는 Kubernetes/AWS 설정과 **`eks-hybrid-setup` bootstrap container**를 구성합니다. 이전 `[settings.hybrid.ssm]`, `[settings.hybrid.iam-roles-anywhere]` 예제는 문서화된 설정 계약이 아닙니다.
[Bottlerocket Hybrid Node 연결](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-bottlerocket.html)의 선택 공급자용 전체 설정을 사용합니다. 공통 필드 예제:
```toml
# Common fragment only: combine with the provider-specific settings from AWS.
[settings.kubernetes]
cluster-name = "my-hybrid-cluster"
api-server = "https://REPLACE_WITH_CLUSTER_ENDPOINT"
cluster-certificate = "REPLACE_WITH_BASE64_CLUSTER_CA"
hostname-override = "hybrid-node-001"
provider-id = "eks-hybrid:///ap-northeast-2/my-hybrid-cluster/hybrid-node-001"
authentication-mode = "aws"
cloud-provider = ""
server-tls-bootstrap = true
[settings.network]
hostname = "hybrid-node-001"
[settings.aws]
region = "ap-northeast-2"
[settings.kubernetes.node-labels]
"eks.amazonaws.com/compute-type" = "hybrid"
[settings.bootstrap-containers.eks-hybrid-setup]
mode = "always"
user-data = "REPLACE_WITH_BASE64_PROVIDER_BOOTSTRAP_INPUT"
```
이 조각은 완성된 부팅 설정이 아닙니다. SSM 공급자는 `eks-hybrid-ssm-setup` 활성화·리전 입력을 제공하고 등록 후 Node 이름은 `mi-*`로 바뀝니다. IAM Roles Anywhere는 `eks-hybrid-iam-ra-setup` 인증서·키 입력과 인증서 정책에 role-session-name을 결합한 AWS credential-process 설정을 제공합니다. 문서화된 ECR provider와 공급자별 노드 label·설정도 포함합니다.
Base64는 암호화가 아닌 인코딩입니다. 공급자 bootstrap data에는 활성화 비밀이나 개인 키가 들어갈 수 있으므로 VM 구성, guestinfo, 관리 권한과 진단 내보내기를 보호하고 공개 저장소·로그에 넣지 않습니다. Admin-container SSH는 선택 사항이며 별도 승인이 필요합니다. 이미 등록한 VM을 깨끗한 템플릿으로 복제하지 않습니다.
첫 전원 켜기 전에 승인한 **새로 준비한 전원 꺼진 VM**을 구성합니다. AWS 가이드는 `settings.toml`을 base64 인코딩하고 `guestinfo.userdata.encoding=base64`를 사용합니다. 평문 base64를 `gzip+base64`로 표시하지 않습니다. 배포할 govc/VMware 버전, datastore, 네트워크, 템플릿 소유권과 비밀 전달 경로를 확인한 뒤 프로비저닝합니다. 이번 감사에서 VMware 배포는 실행하지 않았습니다.
## 애드온 배치와 Pod Identity
API 서버 트래픽이 실제 webhook·aggregated API listener에 도달하도록 구성합니다. Hybrid Pod CIDR에 접근할 수 없으면 **웹훅 서버·operator**를 접근 가능한 클라우드 노드에 두거나 지원되는 적절한 hostNetwork 구성을 검증합니다. 모든 CloudWatch/ADOT 에이전트나 애플리케이션 Pod를 클라우드에 둬야 한다는 뜻은 아닙니다.
클라우드 노드 배치는 명시적으로 검토한 node label·affinity를 사용하고 용량·taint를 확인합니다. `NotIn [hybrid]`는 label이 없는 노드도 선택할 수 있으므로 승인한 EC2 대상이라는 증거가 아닙니다.
AWS는 혼합 클라우드/Hybrid 클러스터에서 양쪽에 CoreDNS 최소 1개를 권고합니다. 레플리카 4개와 soft spread만으로 각 2개를 보장하지 않습니다. [네트워크 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/02-network-configuration.md)의 배치·Service Traffic Distribution 절차를 적용하고 확인합니다.
| 노드 OS | Pod Identity 준비 |
|---|---|
| Ubuntu/RHEL/AL2023 | `spec.hybrid.enableCredentialsFile: true` 후 문서화된 `nodeadm init`; 에이전트 구성에서 `daemonsets.hybrid.create` 활성화 |
| Bottlerocket | OS **1.39.0 이상**; 공급자 bootstrap 명령의 `--enable-credentials-file=true`; `daemonsets.hybrid-bottlerocket.create` 활성화 |
호환성 최소값은 일반 OS 에이전트 **v1.3.3-eksbuild.1**, Bottlerocket **v1.3.7-eksbuild.2**이며 현재 설치 대상 버전이 아닙니다. 클러스터와 현재 호환되는 애드온 버전·설정 스키마를 선택합니다. 임시 자격 증명 mount 위치는 일반 `/eks-hybrid/.aws/credentials`, Bottlerocket `/var/eks-hybrid/.aws/credentials`로 다릅니다.
프라이빗 배포에는 `eks-auth` 서비스 경로, 노드 역할의 `eks-auth:AssumeRoleForPodIdentity` 권한과 워크로드별 Pod Identity association·신뢰·권한이 필요합니다. 에이전트 설치만으로 충분하지 않습니다. 애드온 소유자를 통해 필요한 DaemonSet 설정을 병합하고 기존 애드온에 무조건 `create-addon`을 실행하지 않습니다.
## 업그레이드, 복구와 제거
기존 노드를 비우기 전에 대체 용량을 추가하고 **초기화·검증을 완료**하는 방식을 우선합니다. 새 호스트에 바이너리만 설치해도 클러스터 용량이 늘어나지는 않습니다. DNS 가용성을 유지하고 “복원력”을 이유로 기존 CoreDNS 4개를 무조건 2개로 줄이지 않습니다.
식별한 기존 노드 하나에 대해 context, Node UID, 호스트 매핑과 워크로드·데이터 소유권을 확인합니다. Cordon 후 시간 제한을 둔 drain을 수행하고 PDB·eviction 오류가 있으면 중단합니다. `--delete-emptydir-data`는 로컬 emptyDir 손실을 명시적으로 허용하며 기본 안전 옵션이 아닙니다. 관리되지 않는 Pod, 로컬 영구 데이터와 DaemonSet은 별도 처리합니다. 막힌 drain을 성공처럼 만들려고 `--force`나 eviction 비활성화를 사용하지 않습니다.
인플레이스 업데이트는 비운 호스트에서 문서화된 `nodeadm upgrade`를 실행하고, 신원·소프트웨어 버전·Ready/CNI/DNS·워크로드를 확인한 뒤 uncordon합니다. 중단 작업이며 여유 용량이 없다는 이유로 안전해지지 않습니다.
폐기 시 자동 bootstrap·재조인 경로를 중지하고 nodeadm 제거와 공급자 등록 취소를 완료한 뒤 정확한 Node 객체를 제거하며 CNI 잔여 리소스는 소유자를 통해 정리합니다. 필요한 복구 증거는 비공개로 보존합니다.
`nodeadm uninstall`은 `kubectl drain`이나 `kubectl delete node`가 아닙니다. 기본적으로 남은 워크로드 Pod가 있으면 거부하며 모든 CNI·애드온 아티팩트를 제거하지 않습니다. **v1.0.9부터 문서화된 force/skip 제거 경로에서도 `/var/lib/kubelet`을 삭제하지 않습니다.** Mount 경로를 통해 호스트 파일시스템이 노출될 수 있기 때문입니다. `--force`는 추가 기본 CNI/Kubernetes 경로를 제거하는 옵션이며 “확인 생략 후 모두 삭제”가 아닙니다. 수동 제거 전에 mount·데이터를 조사합니다.
제거 후 실제 재설치를 승인했다면 유효한 자격 증명으로 **install → config check → init**을 다시 실행하고 자동화 기록을 맞춥니다. 인증 오류, 기존 프로필이나 부분 init만으로 운영 노드를 제거하지 않습니다.
## 문제 해결
| 관찰 사항 | 상태 변경 전 조사 |
|---|---|
| 오래된 nodeadm의 SSM 설치 서명 실패 | 승인 nodeadm이 최소 1.0.19인지 확인. 서명 검사 우회 금지 |
| 패키지 관리자·다운로드 실패 | 실제 저장소 접근, 프록시, CA 신뢰, 패키지 잠금, 지원 OS·런타임과 오류 확인. `dnf update`는 시스템 업그레이드이며 진단이 아님 |
| 시간 초과 | 실패 단계와 연결 확인. 시간 제한 증가만으로 해결되지 않음 |
| remote network 밖의 Node IP | 실제 IP와 EKS remote-node CIDR |
| API 접근 불가 / Unauthorized | DNS·라우팅·443·반환 경로, 의도한 Hybrid 역할, 신뢰·자격 증명 유효성, `HYBRID_LINUX` 액세스 항목 |
| NotReady | CNI·에이전트 로그, 선택 데이터 경로 포트, 런타임, 디스크·리소스 조건. 항상 CNI 누락인 것은 아님 |
| 이미지 풀 / x509 오류 | 정확한 이미지·인증 경로, ECR API/DKR/S3, 레지스트리 CA·호스트명. TLS 검증 유지 |
| 활성화 / 토큰 만료 | 활성화 만료·용량·리전과 실행 에이전트의 갱신·시각 동기화·AWS 접근 구분. 재시작은 보장된 해결책이 아님 |
| 기존 Hybrid 프로필 / 부분 init | 실제 nodeadm·공급자 상태와 의도한 클러스터 조사. 증거 보존, 자동 uninstall 금지 |
```bash
# Private diagnostic output on the identified node; no nonexistent nodeadm status.
sudo systemctl status kubelet containerd --no-pager
sudo journalctl -u kubelet --since '-15 min' --lines 200 --no-pager
sudo nodeadm debug --config-source file:///etc/eks/nodeconfig.yaml
```
`nodeadm debug`는 AWS·클러스터 읽기와 자격 증명 검사를 수행합니다. 출력을 비공개로 보관하고 공유 전에 정제합니다. `sudo aws sts get-caller-identity`는 다른 root·관리자 자격 증명 경로를 사용할 수 있어 kubelet이 사용하는 신원을 증명하지 않습니다. TLS 검증으로 `curl -k`를 사용하지 않습니다. 서버 CA 신뢰와 kubelet 클라이언트 인증서 승인·발급도 별개입니다.
## 공식 참고 자료
- [Hybrid nodeadm 명령·파일 위치·제거 동작](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-nodeadm.html)
- [Hybrid 자격 증명](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-creds.html)
- [IAM Roles Anywhere CreateSession 시간](https://docs.aws.amazon.com/rolesanywhere/latest/userguide/authentication-create-session.html)
- [SSM CreateActivation](https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_CreateActivation.html)
- [AWS Hybrid CNI 지원·수명 주기](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-cni.html)
- [Cilium cluster-pool 확장](https://github.com/cilium/cilium/blob/v1.18.3/Documentation/network/concepts/ipam/cluster-pool.rst)
- [Bottlerocket Hybrid 부트스트랩](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-bottlerocket.html)
- [Hybrid 애드온·Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-add-ons.html)
- [containerd 레지스트리 호스트 구성](https://github.com/containerd/containerd/blob/v2.2.0/docs/hosts.md)
- [systemd network-online 의미](https://systemd.io/NETWORK_ONLINE/)
- [Kubernetes 버전 스큐](https://kubernetes.io/releases/version-skew-policy/)
< [이전: 인터넷 제한 환경 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/03-airgap-setup.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: GPU 서버 통합](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/05-gpu-integration.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-hybrid-nodes/05-gpu-integration
----------------------------------------
# GPU 서버 통합
< [이전: 노드 부트스트랩](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: 워크로드 배치 전략](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/06-workload-placement.md) >
> **지원 버전**: DRA GPU 예제는 지원되는 EKS 버전의 Kubernetes 1.34.2 이상 필요. 확인한 인터페이스: GPU Operator 26.7.0, DRA driver 0.5.0, device plugin 0.20.0.
> **마지막 업데이트**: 2026년 9월 12일
이 문서는 준비한 NVIDIA GPU 호스트를 EKS Hybrid Nodes에 통합하며 할당, GPU Operator 소유권, MIG와 time-slicing을 다룹니다. **이번 감사에서 GPU 실행, 드라이버 설치, 모델 다운로드, 추론 벤치마크를 수행하지 않았습니다.** 예제에는 승인한 OS·커널·드라이버·toolkit·런타임·이미지 조합과 실제 하드웨어 시험이 필요합니다.
## GPU마다 할당 관리자 하나 선택
| 경로 | 사전 요구 사항과 할당 |
|---|---|
| 독립 NVIDIA device plugin | 준비한 호스트 드라이버, NVIDIA Container Toolkit·런타임. `nvidia.com/gpu` 등의 확장 리소스 게시 |
| 독립 DRA driver | 준비한 드라이버·CDI 런타임. ResourceSlice 게시 후 DeviceClass/ResourceClaim으로 할당 |
| `ClusterPolicy`를 사용하는 GPU Operator | Operator가 관리하는 device-plugin 방식. 기존 드라이버·toolkit·MIG 구성과 소유권을 맞춤 |
| `GPUCluster`를 사용하는 GPU Operator 26.7 | 신규 설치용 Operator 관리 DRA 방식. 같은 클러스터의 `ClusterPolicy`와 상호 배타적 |
AWS는 지원되는 정적 용량 프로비저닝을 사용하는 신규 EKS 1.34 이상 배포에 DRA를 권고합니다. **EKS Auto Mode는 현재 DRA를 지원하지 않으며** GPU device plugin을 자체 관리합니다. 아래 독립 설치 예제는 식별한 Hybrid GPU 노드만 대상으로 하며 Auto Mode나 다른 GPU 노드에 중복 할당 관리자를 설치하지 않습니다.
`workload.example.com/gpu-allocation=device-plugin` 또는 `dra`처럼 소유자가 관리하는 배타적인 label을 사용합니다. 이는 스케줄링 입력이며 보안 경계나 GPU 정상 상태의 증거가 아닙니다. 어느 경로든 활성화 전에 같은 물리 장치를 게시하는 다른 plugin·Operator가 없는지 확인합니다.
## 호스트 구성 조합 준비와 확인
- Hybrid 신원, CNI와 시각 동기화를 포함한 [노드 부트스트랩](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md)을 완료합니다.
- GPU 모델·form factor, firmware, 드라이버 branch, OS·커널·런타임을 NVIDIA 행렬 및 AWS Hybrid OS 지원과 대조합니다. 새 NVIDIA 행렬에 나오는 모든 Kubernetes 버전을 EKS가 제공한다는 뜻은 아닙니다.
- 실제 NVIDIA 드라이버와 toolkit 설치를 확인합니다. 온프레미스라는 이유만으로 드라이버가 설치되어 있다고 가정하지 않습니다.
- 기존 device-plugin 예제에는 containerd의 NVIDIA runtime handler에 연결된 `nvidia` 이름의 `RuntimeClass`가 필요합니다. Class 이름만으로 handler를 구성하지 못합니다.
- DRA 0.5.0 GPU 할당의 릴리스 사전 요구 사항은 Kubernetes **1.34.2 이상**, 독립 설치 드라이버 **565 이상**, Toolkit **1.18 이상**, CDI 활성 런타임입니다. GPU Operator DRA 방식은 드라이버 **580 이상**을 요구합니다. 이는 호환성 하한이며 현재 패치·branch 권고값이 아닙니다.
- 승인한 NFD/GFD 설치나 호스트 인벤토리 절차로 discovery label을 준비합니다. 예제에 활성화 옵션이 있다고 두 번째 discovery 컨트롤러를 배포하지 않습니다.
다음은 운영자가 실행하는 호스트 GPU 메타데이터 읽기이며 CUDA·LLM 벤치마크가 아닙니다.
```bash
nvidia-smi --query-gpu=name,driver_version,uuid,memory.total --format=csv,noheader
```
이전 예제의 `cpu: 128`, `memory: 1024Gi`, `nvidia.com/gpu: 8`은 **미검증 예시 출력**이며 이번 감사 실측이 아닙니다. Kubernetes GPU capacity는 구성에 따라 물리 GPU, MIG 인스턴스 또는 time-slicing replica를 나타낼 수 있습니다.
## 선택한 Hybrid Node의 독립 device plugin
다음 값은 드라이버·런타임 사전 설치와 기존 호환 discovery 구성을 요구합니다. 두 node-selector label은 차트의 GPU-discovery affinity와 함께 적용됩니다. 선택되는 노드가 없다면 인벤토리·label을 고치며 Pod를 실행시키기 위해 affinity를 제거하지 않습니다.
```yaml
nodeSelector:
eks.amazonaws.com/compute-type: hybrid
workload.example.com/gpu-allocation: device-plugin
runtimeClassName: nvidia
migStrategy: none
failOnInitError: true
deviceListStrategy: envvar
gfd:
enabled: false
nfd:
enabled: false
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
```
`device-plugin-values.yaml`로 저장합니다. 확인한 차트는 **0.20.0**이며 배포 전 업그레이드 경로와 이미지 호환성을 확인합니다.
```bash
# Cluster write; installs a privileged infrastructure component.
set -euo pipefail
umask 077
: "${KUBECONFIG:?}" "${CONTEXT:?}"
kubectl --kubeconfig "$KUBECONFIG" --context "$CONTEXT" get nodes \
-l 'eks.amazonaws.com/compute-type=hybrid,workload.example.com/gpu-allocation=device-plugin' \
-o json > plugin-nodes.json
jq -e '.items | length > 0 and
all(.[]; .metadata.labels["nvidia.com/mps.capable"] != "true")' plugin-nodes.json
helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm repo update nvdp
helm install hybrid-gpu-plugin nvdp/nvidia-device-plugin \
--version 0.20.0 --namespace gpu-system --create-namespace \
--kubeconfig "$KUBECONFIG" --kube-context "$CONTEXT" \
--values device-plugin-values.yaml --wait --timeout 10m
```
GPU 인프라 운영자가 소유한 namespace·release를 사용합니다. 렌더링된 RBAC, host path, privilege, 레지스트리 접근과 toleration을 검토합니다. Helm `--wait` 성공은 CUDA 호환성이나 워크로드 성능의 증거가 아닙니다.
게시된 0.20.0 차트는 선택한 Hybrid 집합과 `nvidia.com/mps.capable=true`로 한정한 MPS control DaemonSet도 렌더링합니다. 이 전용 GPU 예제는 해당 label이 이미 있으면 중단합니다. Label을 무조건 지우지 말고 기존 MPS 소유권을 조사하며, 차트에 DaemonSet 하나만 있다고 가정하지 않습니다.
## 별도 Hybrid GPU 집합의 독립 DRA
현재 드라이버 프로젝트는 **`kubernetes-sigs/dra-driver-nvidia-gpu`**입니다. DRA API 안정성, EKS 지원과 개별 공급자 기능 성숙도는 별도 확인합니다. 현재 NVIDIA 26.7 문서는 전체 GPU·기존 MIG 할당을 GA로 명시하며 동적 MIG, MPS, 일부 공유·NVML 할당 상태 기능에는 별도 Alpha gate가 있습니다. 데모를 시작하려고 모든 feature gate를 활성화하지 않습니다.
태그된 저장소 README의 이전 설명에는 GPU 할당 미지원 문구가 있습니다. 릴리스된 사전 요구 사항·설치 문서와 현재 NVIDIA 기능 표는 더 새로운 기능별 안내를 제공합니다. 운영 배포에는 정확한 버전과 지원 계약을 기록합니다.
일반 H100/H200 할당에는 Multi-Node NVLink 오케스트레이션이 필요하지 않아 이 예제는 ComputeDomains를 끕니다. ComputeDomains에는 Grace Blackwell/MNNVL, IMEX와 discovery 추가 요구 사항이 있습니다.
```yaml
gpuResourcesEnabledOverride: true
nvidiaDriverRoot: /
resources:
gpus:
enabled: true
computeDomains:
enabled: false
featureGates: {}
kubeletPlugin:
nodeSelector:
eks.amazonaws.com/compute-type: hybrid
workload.example.com/gpu-allocation: dra
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
containers:
gpus:
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
webhook:
enabled: false
```
`dra-values.yaml`로 저장합니다. 호스트 드라이버 root가 `/`이고 CDI가 구성되었다고 가정합니다. Operator가 설치한 드라이버는 일반적으로 다른 root를 사용하므로 이 값을 그대로 복사하지 않습니다.
```bash
# Alternative cluster write, only for the separately selected DRA cohort.
set -euo pipefail
: "${KUBECONFIG:?}" "${CONTEXT:?}"
helm install hybrid-gpu-dra \
oci://registry.k8s.io/dra-driver-nvidia/charts/dra-driver-nvidia-gpu \
--version 0.5.0 --namespace gpu-dra-system --create-namespace \
--kubeconfig "$KUBECONFIG" --kube-context "$CONTEXT" \
--values dra-values.yaml --wait --timeout 10m
```
이 차트는 GPU 리소스에 여전히 `gpuResourcesEnabledOverride=true`를 요구합니다. 옵션 존재가 모든 GPU 기능이 Alpha라는 뜻은 아닙니다. 최소 예제에서는 선택적 admission webhook을 끕니다. 이를 켜려면 접근 가능한 webhook 엔드포인트와 검증된 TLS/cert-manager 또는 기존 Secret 구성이 필요합니다.
### GPU Operator 대안과 소유권
Operator의 구성 요소 관리 방식이 의도한 설치와 맞을 때 사용합니다. `driver.enabled=false`는 **호스트 드라이버를 검증한 경우에만** 적절합니다. Toolkit 관리도 필요한 런타임 구성을 준비한 뒤에만 비활성화합니다. Operator 업그레이드는 드라이버, validator, 리소스 게시와 노드 가용성을 바꿀 수 있습니다.
**신규 Operator 관리 DRA**의 문서화된 값 예제:
```yaml
clusterPolicy:
deployCR: false
gpuCluster:
deployCR: true
driver:
enabled: false
nfd:
enabled: false
draDriver:
computeDomains:
enabled: false
```
승인한 드라이버·CDI 런타임·discovery의 사전 설치를 가정합니다. GPUCluster가 ClusterPolicy의 toolkit·MIG 관리 기능을 자동으로 제공하는 것은 아닙니다. **26.7.0**의 전체 차트 값을 검토한 뒤 클러스터 GPU 소유자를 통해 배포합니다.
`GPUCluster`는 이름이 `gpu-cluster`인 cluster-scoped singleton이며 `ClusterPolicy`와 공존할 수 없습니다. 이 방식은 ClusterPolicy 또는 독립 DRA Helm release에서 GPUCluster로의 제자리 마이그레이션을 지원하지 않습니다. 문서화된 신규 설치 경로를 사용하고 Operator 관리 배포에 독립 DRA release를 추가하지 않습니다.
Operator 컨트롤러의 `nodeSelector`는 **모든 operand를 한정하지 않습니다**. Operand 배치는 GPU discovery와 `nvidia.com/gpu.deploy.*` label을 사용합니다. 제외해야 할 클라우드·Auto Mode GPU와 새로 생길 노드까지 검토합니다. 경계를 유지할 수 없다면 분리된 클러스터 또는 적절히 한정한 독립 경로를 사용합니다.
GPUCluster 조정 과정은 GPU validator를 실행하고 claim을 할당할 수 있습니다. Readiness와 DCGM telemetry는 DRA 장치 할당 상태와 같지 않습니다. 이 릴리스의 `NVMLDeviceHealthCheck`는 Alpha이며 기본 비활성입니다.
## 현재 DRA manifest
다음 예제는 **`resource.k8s.io/v1`**을 사용합니다. Kubernetes 1.31의 이전 alpha 형태를 현재 클러스터에 그대로 적용하지 않습니다. DRA에는 동작하는 공급자 드라이버, ResourceSlice, 호환 kubelet·런타임과 활성 API가 필요합니다. DeviceClass만으로 GPU가 발견되지 않습니다.
```yaml
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
name: hybrid-full-gpu
spec:
selectors:
- cel:
expression: >-
device.driver == "gpu.nvidia.com" &&
device.attributes["gpu.nvidia.com"].type == "gpu"
---
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
name: hybrid-large-gpu
spec:
selectors:
- cel:
expression: >-
device.driver == "gpu.nvidia.com" &&
device.attributes["gpu.nvidia.com"].type == "gpu" &&
device.capacity["gpu.nvidia.com"].memory.isGreaterThan(quantity("40Gi"))
---
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
name: single-large-gpu
namespace: ai-workloads
spec:
spec:
devices:
requests:
- name: gpu
exactly:
deviceClassName: hybrid-large-gpu
allocationMode: ExactCount
count: 1
```
`type == "gpu"`는 같은 `gpu.nvidia.com` 드라이버가 게시하는 MIG·VFIO 장치를 제외합니다. 두 번째 class는 게시된 메모리가 **40 GiB보다 큰** 장치를 선택하며 H100/H200 전용 선택자가 아닙니다. 실제 capacity와 NVML `productName`은 ResourceSlice에서 확인합니다. `NVIDIA-H200` 같은 GFD node label과 DRA 제품명 속성이 같다고 가정하지 않습니다.
이 API의 DeviceClass에는 `suitableNodes` 필드가 없습니다. 워크로드 node selector·affinity와 DRA 장치·노드 가용성 매칭을 사용합니다. 요청은 `requests[].exactly`를 사용하며 Pod claim은 이전 `source` wrapper 없이 `resourceClaimTemplateName`을 직접 참조합니다.
## 제한된 GPU smoke Job
승인한 namespace(예: `ai-workloads`)와 선택한 드라이버·런타임, non-root UID, 읽기 전용 root filesystem에서 검증한 **다이제스트 고정 이미지**를 사용합니다. 아래 placeholder 이미지는 의도적으로 사용할 수 없는 값입니다. 두 Job은 **기본 정지 상태**이므로 적용만으로 GPU 작업을 시작하지 않습니다.
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: hybrid-gpu-plugin-smoke
namespace: ai-workloads
spec:
suspend: true
completions: 1
parallelism: 1
backoffLimit: 0
activeDeadlineSeconds: 120
ttlSecondsAfterFinished: 600
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
runtimeClassName: nvidia
nodeSelector:
eks.amazonaws.com/compute-type: hybrid
workload.example.com/gpu-allocation: device-plugin
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
securityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: gpu-check
image: registry.example.invalid/approved/gpu-smoke:replace-with-approved-digest
command: ["nvidia-smi", "-L"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 100m
memory: 128Mi
nvidia.com/gpu: 1
limits:
cpu: 500m
memory: 256Mi
nvidia.com/gpu: 1
---
apiVersion: batch/v1
kind: Job
metadata:
name: hybrid-gpu-dra-smoke
namespace: ai-workloads
spec:
suspend: true
completions: 1
parallelism: 1
backoffLimit: 0
activeDeadlineSeconds: 120
ttlSecondsAfterFinished: 600
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
nodeSelector:
eks.amazonaws.com/compute-type: hybrid
workload.example.com/gpu-allocation: dra
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
securityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: gpu-check
image: registry.example.invalid/approved/gpu-smoke:replace-with-approved-digest
command: ["nvidia-smi", "-L"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
claims:
- name: gpu-resource
resourceClaims:
- name: gpu-resource
resourceClaimTemplateName: single-large-gpu
```
첫 Job은 전용 GPU를 쓰는 기존 device-plugin 예제이며 두 번째는 별도 DRA 집합용입니다. 같은 GPU를 두 방식으로 요청하지 않습니다. 여러 컨테이너가 같은 DRA claim을 참조해도 할당은 하나입니다.
소유자가 이미지를 교체하고 배치·런타임·보안을 확인한 뒤 GPU 시험을 승인하면 일반 배포 절차로 의도한 Job 하나만 재개합니다. TTL 정리 전 결과를 보존합니다. 120초 deadline은 시작된 Job을 제한하며 이미지 준비나 외부 유지보수 작업 시간을 제한하는 값이 아닙니다.
장치 하나를 요청한 claim에 `CUDA_VISIBLE_DEVICES=0,1,2,3`을 설정하지 않습니다. 장치 관리자·런타임이 할당 장치를 제공하며 CUDA의 프로세스 내부 index와 물리 GPU index는 다를 수 있습니다. 수동 override는 할당을 숨기거나 잘못 식별할 수 있으며 추가 GPU를 허용하지 않습니다.
`nvidia-smi -L`은 장치 가시성만 확인합니다. 승인한 하드웨어에서 제한된 CUDA 연산과 실제 애플리케이션을 별도로 검증합니다. 여기서는 그 실행이나 성능 결과를 주장하지 않습니다.
## MIG와 time-slicing
MIG는 지원 GPU를 하드웨어 기반 compute·memory 인스턴스로 나눕니다. Time-slicing보다 강한 리소스·메모리·장애 격리를 제공하지만 공유 호스트·드라이버를 완전한 보안 경계로 바꾸거나 애플리케이션 지연 SLO를 보장하지 않습니다. 지원 profile·instance 한도는 GPU SKU와 드라이버에 따라 다릅니다.
일반적인 **A100 40 GB** profile:
| Profile | 명목상 profile 메모리 | 해당 profile 최대 인스턴스 |
|---|---|---|
| `1g.5gb` | 5 GB | 7 |
| `2g.10gb` | 10 GB | 3 |
| `3g.20gb` | 20 GB | 2 |
| `4g.20gb` | 20 GB | 1 |
| `7g.40gb` | 40 GB | 1 |
`4g.40gb`는 **A100 80 GB** profile 집합에 해당하며 이전 퀴즈는 두 SKU 표를 섞었습니다. `1g`는 profile의 GPU compute slice 수이며 물리 GPU 수가 아닙니다. 명목 메모리 label은 정확한 사용 가능 메모리 보장이 아닙니다. 혼합 profile 배치에는 추가 geometry 제약이 있습니다.
MIG 활성화·재구성은 GPU 워크로드를 중단할 수 있는 소유자 통제 유지보수 작업입니다. 위 독립 plugin 기본값은 `migStrategy: none`입니다. Pod 요청만 `nvidia.com/mig-1g.5gb`로 바꿔도 MIG 인스턴스가 생성되지는 않습니다. 실제 MIG geometry와 적절한 plugin strategy를 먼저 준비합니다.
Time-slicing은 물리적으로 분리된 GPU나 메모리 partition이 아닌 여러 **논리적 접근 슬롯**을 게시합니다. 별도 검토한 device-plugin 설정 예제:
```yaml
# Fragment of device-plugin configuration, not a Kubernetes resource.
version: v1
sharing:
timeSlicing:
renameByDefault: true
failRequestsGreaterThanOne: true
resources:
- name: nvidia.com/gpu
replicas: 4
```
이 구성을 이름 있는 ConfigMap/config 선택으로 plugin과 대상 노드에 연결합니다. 연결되지 않은 ConfigMap만으로 동작은 바뀌지 않습니다. `renameByDefault: true`이면 게시되는 리소스는 `nvidia.com/gpu.shared`입니다. 전용 GPU smoke 요청을 그대로 공유 리소스에 사용하지 않습니다.
Replica 4개는 독립적인 메모리 영역 4개를 예약하지 않으며 더 많은 슬롯 요청이 비례하는 compute를 보장하지 않습니다. 경합, context switch와 워크로드 동작이 지연·처리량에 영향을 줄 수 있으므로 원인을 하나로 단정하지 않고 측정합니다. Time-slicing replica 사이에는 메모리·장애 격리가 없습니다. 추론이라고 초과 할당이 자동으로 안전하거나 학습이 항상 공유와 호환되지 않는 것은 아닙니다.
## H100/H200 사양의 범위
| NVIDIA 사양 | H100 **SXM** | H200 **SXM** |
|---|---|---|
| GPU 메모리 | 80 GB HBM3 | 141 GB HBM3e |
| 게시된 메모리 대역폭 | 3.35 TB/s | 4.8 TB/s |
| MIG | 최대 7개, 모델별 profile | 최대 7개, 모델별 profile |
공급자 하드웨어 사양이며 이 Kubernetes 환경의 실측값이 아닙니다. H100 NVL/PCIe variant의 용량·대역폭은 다르므로 SXM 행을 모든 H100에 일반화하지 않습니다. 메모리 증가가 워크로드 수용에 도움이 될 수 있지만 실제 추론·학습 성능은 모델, 정밀도, batch, 소프트웨어, interconnect와 경합에도 영향을 받습니다.
## 할당 조사와 안전한 정리
명시적인 관리자 kubeconfig·context를 사용하고 하드웨어·Pod 진단은 비공개로 보관합니다.
```bash
set -euo pipefail
umask 077
: "${KUBECONFIG:?}" "${CONTEXT:?}"
kubectl --kubeconfig "$KUBECONFIG" --context "$CONTEXT" \
get resourceslices -o json > gpu-slices.json
jq '[.items[] | select(.spec.driver == "gpu.nvidia.com") |
{name: .metadata.name, node: .spec.nodeName,
devices: [.spec.devices[] | {name, attributes, capacity}]}]' gpu-slices.json
kubectl --kubeconfig "$KUBECONFIG" --context "$CONTEXT" \
get resourceclaims -n ai-workloads -o json > gpu-claims.json
jq '[.items[] | {name: .metadata.name,
allocation: .status.allocation.devices.results,
reservedFor: .status.reservedFor}]' gpu-claims.json
```
현재 ResourceClaim에는 일반적인 저장 필드로 `Pending → Allocated → Bound` phase enum이 없습니다. 할당 결과, 예약, Pod 스케줄링·이벤트와 드라이버 준비 상태를 확인합니다. 예약만으로 컨테이너의 GPU 사용 성공을 증명하지 못합니다. 이전 `resourceHandles` 예제는 현재 structured allocation 형태가 아닙니다.
로그를 조사할 때 선택한 드라이버 릴리스가 실제 렌더링하는 워크로드 이름·label을 사용합니다. Operator 관리 DRA와 독립 DRA release가 이전 `app=nvidia-dra-driver` selector를 공통 사용한다고 가정하지 않습니다.
결과를 보존한 뒤 소유한 시험 Job·Pod만 삭제하고 생성된 claim이 해제되고 장치가 unprepare되었는지 확인합니다. 보존된 claim·finalizer는 중지된 컨테이너보다 오래 남을 수 있습니다. 워크로드가 claim unprepare에 사용해야 하는 DRA kubelet plugin을 먼저 제거하지 않습니다.
Operator 관리 DRA는 문서화된 Helm hook·finalizer의 순서 있는 제거 절차를 사용합니다. `helm uninstall --no-hooks`나 공유 claim의 무조건적인 finalizer 제거·강제 삭제를 사용하지 않습니다. 실패한 정리를 리소스 소유자와 확인한 뒤 하드웨어를 재사용합니다.
## 공식 참고 자료
- [AWS NVIDIA 장치 관리](https://docs.aws.amazon.com/eks/latest/userguide/device-management-nvidia.html)
- [AWS Hybrid 추론: 범위를 제한한 device plugin](https://aws.amazon.com/blogs/containers/run-genai-inference-across-environments-with-amazon-eks-hybrid-nodes/)
- [GPU Operator 플랫폼·구성 요소 행렬](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/platform-support.html)
- [GPU Operator DRA/GPUCluster 절차](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/dra-intro-install.html)
- [DRA driver 0.5.0 사전 요구 사항](https://github.com/kubernetes-sigs/dra-driver-nvidia-gpu/blob/v0.5.0/site/content/docs/prerequisites.md)
- [DRA ResourceSlice 속성](https://github.com/kubernetes-sigs/dra-driver-nvidia-gpu/blob/v0.5.0/site/content/docs/reference/resourceslice-attributes.md)
- [Kubernetes DRA](https://kubernetes.io/docs/concepts/resource-management/dynamic-resource-allocation/)
- [NVIDIA device plugin 0.20.0](https://github.com/NVIDIA/k8s-device-plugin/tree/v0.20.0)
- [MIG profile](https://docs.nvidia.com/datacenter/tesla/mig-user-guide/supported-mig-profiles.html), [time-slicing](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html)
- [H100 사양](https://www.nvidia.com/en-us/data-center/h100/), [H200 사양](https://www.nvidia.com/en-us/data-center/h200/)
< [이전: 노드 부트스트랩](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: 워크로드 배치 전략](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/06-workload-placement.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-hybrid-nodes/06-workload-placement
----------------------------------------
# 워크로드 배치 전략
< [이전: GPU 서버 통합](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/05-gpu-integration.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: 노드 라이프사이클 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/07-node-lifecycle.md) >
> **지원 버전**: 호환 Karpenter를 사용하는 지원 EKS 버전. Kubernetes 1.36.2와 Karpenter/provider 1.14.1 인터페이스 확인.
> **마지막 업데이트**: 2026년 9월 13일
이 문서는 Hybrid·클라우드 노드에 워크로드를 배치하고 cloud bursting과 Pod deletion cost의 한계를 설명합니다. 예제는 로컬로 검증한 구성·패치 방식입니다. 감사 중 클라우드 용량 생성, 노드 변경, 애플리케이션·GPU 워크로드 실행을 수행하지 않았습니다.
## 배치 제약과 허용
| 기법 | 동작 | 보장하지 않는 것 |
|---|---|---|
| `nodeSelector` / required node affinity | 스케줄링 시 대상 노드 필터링 | 데이터 존재, 의존성 정상 상태, label 변경 후 기존 Pod 이동 |
| Preferred node affinity | 스케줄링 선호도 추가 | 엄격한 온프레미스 우선, 고정 비율, 실행 Pod 자동 이동 |
| Taint / toleration | 해당 toleration 없는 Pod 배제 | Toleration은 Pod를 유인하거나 GPU 사용을 증명하지 않음 |
| Pod anti-affinity / topology spread | 적격 도메인 사이 분산 제약·점수 | 공유 물리 호스트·전원·스토리지·네트워크 장애에 대한 완전한 가용성 |
| PDB | 지원되는 자발적 eviction 요청 제약 | 일반적인 가용성, 배치 또는 모든 삭제·scale-down 경로 보호 |
`eks.amazonaws.com/compute-type=hybrid`와 소유자 관리 위치 label을 함께 사용합니다. Compute-type label의 `DoesNotExist`는 “클라우드”의 정의가 아닙니다. 정상 클라우드 노드도 compute-type label을 가질 수 있고, label이 없는 노드가 승인한 클라우드 위치라는 증거도 아닙니다.
예제는 [노드 부트스트랩](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md)의 Hybrid Node에 `workload.example.com/location=onprem`, 아래 특정 Karpenter pool에 `cloud`를 사용합니다. Label은 운영 입력이며 단독으로 데이터 상주·보안 통제를 제공하지 않습니다. 스토리지 토폴로지, egress, IAM과 신뢰할 수 있는 노드 관리도 필요합니다.
실제 노드 소유자의 구성으로 위치 label을 설정합니다. 예를 들어 nodeadm의 kubelet flag `--node-labels=workload.example.com/location=onprem` 또는 대응하는 Bottlerocket node-label 설정을 사용합니다. 자격 증명만 있는 최소 NodeConfig는 이 사용자 지정 label을 자동으로 추가하지 않습니다.
### 선택적 taint
Hybrid taint는 선택 사항입니다. 추가 전에 필수 CNI, DNS, GPU 인프라와 애플리케이션 Pod의 toleration을 확인합니다. 이전 GPU 예제가 새로 만든 모든 taint를 자동 허용하지는 않습니다.
```bash
# Identified node only; owner-approved scheduling-policy change.
set -euo pipefail
: "${KUBECONFIG:?}" "${CONTEXT:?}" "${NODE_NAME:?}"
kubectl --kubeconfig "$KUBECONFIG" --context "$CONTEXT" \
taint node "$NODE_NAME" eks.amazonaws.com/compute-type=hybrid:NoSchedule
```
GPU 전용 taint에는 `nvidia.com/gpu=present:NoSchedule` 같은 검토한 규칙을 사용하고 인프라·워크로드 toleration을 맞춥니다. CPU 전용 Pod도 이 taint를 허용할 수 있으므로 admission·리소스 요청 정책은 별도입니다.
`NoSchedule`은 새 스케줄링에 영향을 주고 `PreferNoSchedule`은 약한 회피 선호입니다. `NoExecute`는 toleration과 `tolerationSeconds`에 따라 기존 Pod도 퇴거시킬 수 있습니다. Label이나 `IgnoredDuringExecution`이 붙은 required affinity를 바꿔도 이미 실행 중인 Pod가 자동 이동하지 않습니다.
## Cloud burst pool 준비
Karpenter는 조건을 만족하는 Pending Pod를 위해 EC2 용량을 생성하며 온프레미스 서버를 추가하지 않습니다. HPA/KEDA나 애플리케이션 컨트롤러가 replica 수요를 조절하고, 노드 프로비저너는 별도 제어 루프입니다. AWS quota, 인스턴스 가용성, subnet IP, IAM, 노드 bootstrap과 워크로드 의존성이 확장을 막을 수 있습니다.
클러스터 Kubernetes minor와 호환되는 Karpenter를 사용합니다. 현재 호환성 행렬에서 **Kubernetes 1.36은 최소 1.13**이 필요합니다. 예제는 **1.14.1** CRD로 확인했습니다. Auto Mode `NodeClass`가 아닌 자체 관리 Karpenter `EC2NodeClass` 예제입니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: cloud-burst-pool
spec:
template:
metadata:
labels:
workload.example.com/location: cloud
spec:
taints:
- key: workload.example.com/cloud-burst
value: "true"
effect: NoSchedule
requirements:
- key: kubernetes.io/arch
operator: In
values: [amd64]
- key: kubernetes.io/os
operator: In
values: [linux]
- key: topology.kubernetes.io/zone
operator: In
values: [ap-northeast-2a, ap-northeast-2b]
- key: karpenter.sh/capacity-type
operator: In
values: [spot, on-demand]
- key: node.kubernetes.io/instance-type
operator: In
values: [m6i.xlarge, m6i.2xlarge, m6i.4xlarge]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: hybrid-cloud-burst
limits:
cpu: "1000"
memory: 4000Gi
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
budgets:
- nodes: "1"
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: hybrid-cloud-burst
spec:
amiFamily: AL2023
amiSelectorTerms:
- id: ami-0123456789abcdef0
subnetSelectorTerms:
- id: subnet-0123456789abcdef0
- id: subnet-0fedcba9876543210
securityGroupSelectorTerms:
- id: sg-0123456789abcdef0
instanceProfile: REPLACE_WITH_APPROVED_EC2_NODE_INSTANCE_PROFILE
```
**적용 전 모든 AWS 예시 리소스 ID와 instance profile을 교체합니다.** 리전·아키텍처·Kubernetes 버전에 맞는 승인한 불변 AL2023 AMI를 선택합니다. AMI ID 선택 시 `amiFamily: AL2023`이 bootstrap family를 제공합니다. Instance profile은 SSM/IAM Roles Anywhere Hybrid 역할이 아닌 준비한 **EC2 노드 profile**입니다. Subnet·security group은 의도한 프라이빗 네트워크·AZ와 실제로 일치해야 합니다.
AZ 제한은 `requirements`에 두며 template label로 AWS zone을 위조하지 않습니다. Pool의 opt-in taint는 대응 toleration을 가진 워크로드로 범위를 제한하며 데이터·레지스트리 접근 권한을 주지 않습니다.
보존한 CPU `1000` / 메모리 `4000Gi`는 큰 규모의 **계획 예시**이며 권장 할당량이나 지출 상한이 아닙니다. 사용 전 크기를 정합니다. Karpenter 한도 검사는 eventual consistency이므로 빠른 확장 중 초과할 수 있습니다. `budgets: [{nodes: "1"}]`는 해당 자발적 중단의 동시성을 제한하며 최소 용량 예약이 아닙니다.
완성된 NodePool 정의 하나를 사용합니다. 같은 이름의 두 번째 부분 객체를 적용하는 것은 “위 설정 상속”의 안전한 방법이 아닙니다. 만료, AMI 선택과 disruption policy 변경은 별도 rollout 검토가 필요합니다.
## 로컬·클라우드 전용·버스팅 허용 워크로드
`hybrid-placement-lab` 같은 승인한 lab namespace를 만들고 실행 전 실제 이미지 다이제스트, 애플리케이션 보안 설정, probe, 스토리지와 레지스트리 접근을 준비합니다. 예제는 **replica 0개**와 의도적으로 사용 불가능한 이미지로 시작합니다. 이전 3/5/10 replica 수는 가능한 규모 예시이며 용량 실측이나 애플리케이션 동작 보장이 아닙니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hybrid-local-processor
namespace: hybrid-placement-lab
spec:
replicas: 0
selector:
matchLabels:
app: hybrid-local-processor
template:
metadata:
labels:
app: hybrid-local-processor
spec:
nodeSelector:
eks.amazonaws.com/compute-type: hybrid
workload.example.com/location: onprem
tolerations:
- key: eks.amazonaws.com/compute-type
operator: Equal
value: hybrid
effect: NoSchedule
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: processor
image: registry.example.invalid/approved/data-processor:replace-with-approved-digest
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: "2"
memory: 4Gi
limits:
cpu: "4"
memory: 8Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: hybrid-cloud-api
namespace: hybrid-placement-lab
spec:
replicas: 0
selector:
matchLabels:
app: hybrid-cloud-api
template:
metadata:
labels:
app: hybrid-cloud-api
spec:
nodeSelector:
workload.example.com/location: cloud
karpenter.sh/nodepool: cloud-burst-pool
tolerations:
- key: workload.example.com/cloud-burst
operator: Equal
value: "true"
effect: NoSchedule
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: registry.example.invalid/approved/inference-api:replace-with-approved-digest
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: "2"
memory: 4Gi
limits:
cpu: "4"
memory: 8Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: hybrid-burst-app
namespace: hybrid-placement-lab
spec:
replicas: 0
selector:
matchLabels:
app: hybrid-burst-app
template:
metadata:
labels:
app: hybrid-burst-app
spec:
tolerations:
- key: eks.amazonaws.com/compute-type
operator: Equal
value: hybrid
effect: NoSchedule
- key: workload.example.com/cloud-burst
operator: Equal
value: "true"
effect: NoSchedule
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: eks.amazonaws.com/compute-type
operator: In
values: [hybrid]
- key: workload.example.com/location
operator: In
values: [onprem]
- matchExpressions:
- key: workload.example.com/location
operator: In
values: [cloud]
- key: karpenter.sh/nodepool
operator: In
values: [cloud-burst-pool]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: workload.example.com/location
operator: In
values: [onprem]
topologySpreadConstraints:
- maxSkew: 2
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: hybrid-burst-app
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.invalid/approved/latency-app:replace-with-approved-digest
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: "1"
memory: 2Gi
limits:
cpu: "2"
memory: 4Gi
```
로컬 processor는 Hybrid·온프레미스 label을 모두 요구합니다. 클라우드 API는 명시적으로 소유한 cloud-burst pool을 요구합니다. 다른 승인 EC2 group에는 별도의 명시적 선택 규칙이 필요하며, label 누락을 fallback 허가로 해석하지 않습니다.
Burst 애플리케이션의 두 node-affinity term은 승인 온프레미스 집합 또는 승인 cloud pool이라는 **OR** 조건입니다. 한 term 안의 expression은 **AND**입니다. 온프레미스를 선호하지만 scheduler 점수, 리소스 요청, taint와 토폴로지 선호도 함께 적용됩니다. Karpenter는 새 노드를 계획할 때 선호도를 완화할 수 있습니다. 이는 포화 감지기나 모든 온프레미스 슬롯 우선 채우기 보장이 아닙니다.
100·50 같은 weight는 스케줄링 점수이며 용량 2:1 할당 비율이 아닙니다. 노드 하나에 여러 Pod가 들어가거나 호환 Pod가 전혀 없을 수도 있습니다. 따라서 “8개 노드에 Pod1–8을 두고 Pod9부터 무제한 cloud로 넘긴다”는 모델은 잘못입니다.
온프레미스 용량이 돌아와도 실행 중인 cloud Pod는 자동 복귀하지 않습니다. 재배치에는 데이터·가용성을 고려한 별도 통제 rollout·eviction 정책이 필요합니다.
온프레미스 GPU 학습·클라우드 CPU API 패턴에서는 GPU 요구 사항과 데이터 이동을 명시합니다. [GPU 통합](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/05-gpu-integration.md)의 제한된 Job·할당 패턴을 사용합니다. 이전 **GPU4개 / CPU16 / 64Gi** 학습 요청은 크기 예시이며 실제 GPU 용량, 호환 장치와 지속 가능한 입력·checkpoint 접근이 필요합니다. 유한 학습 프로세스를 계속 재시작하는 Deployment는 Job이나 적절한 학습 컨트롤러를 대체하지 않습니다.
### 토폴로지와 데이터 locality
`topology.kubernetes.io/zone` 사용 전에 정확한 장애 도메인 label을 부여합니다. 클라우드는 실제 AWS AZ, 온프레미스는 의미 있는 자체 도메인을 사용합니다. Selector를 맞추려고 모든 Hybrid host를 AWS AZ로 표시하지 않습니다.
Burst 예제의 `ScheduleAnyway` spread는 점수 선호이며 `maxSkew`를 초과할 수 있습니다. `DoNotSchedule`에서는 설정한 `minDomains`를 고려하여 **적격 도메인 전체의 global minimum**을 기준으로 skew를 검사합니다. 항상 클러스터 모든 zone의 최대값−최소값이라는 의미는 아닙니다.
엄격한 spread·anti-affinity는 적격 노드·도메인이 부족하면 Pod를 Pending으로 남길 수 있습니다. 노드 적격성에는 affinity, taint와 topology policy도 적용됩니다. 강한 분산과 온프레미스 활용 극대화는 경쟁하는 목표일 수 있으므로 tradeoff를 선택합니다.
Required hostname anti-affinity는 충분한 용량이 있을 때 matching replica가 같은 Kubernetes Node에 배치되지 않게 할 수 있습니다. 나머지 replica가 정상이고 충분한 처리 용량을 가지며 공유 물리 장애를 피한다는 보장은 아닙니다.
데이터 위치 label은 데이터를 생성·복제하지 않습니다. 로컬 영구 데이터에는 PV node affinity와 적절한 `WaitForFirstConsumer` binding을 사용하는 관리된 local PV/PVC를 사용합니다. 원시 `hostPath: /mnt/data`는 dataset을 확인하거나 이동 가능한 영속성을 제공하지 않습니다. 로컬 스토리지에 bound된 Pod는 단순히 EC2 node로 fallback할 수 없으므로 복제, 접근 가능한 스토리지 또는 별도 애플리케이션 경로를 계획합니다.
## Pod deletion cost는 선호도
`controller.kubernetes.io/pod-deletion-cost`는 Pod의 정수 annotation입니다. 유효 범위는 **−2147483648~2147483647**, 없으면 기본값0이며 음수도 허용됩니다. ReplicaSet 축소 시 해당 Pod 집합 안에서 낮은 값을 **best-effort**로 먼저 제거합니다.
확인한 Kubernetes1.36.2 컨트롤러에서 deletion cost보다 먼저 비교하는 항목은 다음과 같습니다.
1. 할당된 Pod보다 미할당 Pod 우선.
2. Pending → Unknown → Running 순서.
3. Ready보다 NotReady 우선.
4. 그 뒤 낮은 deletion cost → 높은 cost 순서.
이후 replica 동시 배치, readiness 경과 시간, restart 수와 생성 시각 등을 비교합니다. 따라서 cost1000인 비정상 온프레미스 Pod가 cost0인 정상 cloud Pod보다 먼저 제거될 수 있습니다. Cost1000은 eviction, rollout, 노드 장애, 수동 삭제나 다른 컨트롤러로부터의 보호가 아닙니다.
이전 “replica10→4, cloud Pod6개 모두 삭제·온프레미스4개 모두 보존”은 **조건부 예시**입니다. 같은 ReplicaSet이고 상위 우선순위 기준과 상태가 안정적이어야 하며, 특히 Deployment revision이 다르면 보장된 결과가 아닙니다.
온프레미스에도 전력·유지보수·기회비용이 있습니다. 이를 보존하는 것은 평가할 운영 목표이며 보편적인 경제 법칙이 아닙니다. Deletion-cost 값 자체가 지출 예산이나 가용성 목표를 강제하지 않습니다.
## Binding 후 소유한 Pod 하나에 cost 부여
일반적인 Pod `CREATE` admission 요청에는 아직 할당된 `spec.nodeName`이 없습니다. CREATE 전용 mutating webhook은 최종 노드 위치를 신뢰성 있게 알 수 없습니다. 이전 webhook 예제는 Service/TLS/backend/RBAC 구성도 빠져 있어 failure policy에 따라 Pod 생성을 막을 수 있었습니다. 운영 솔루션으로 설치하지 않습니다.
작은 통제 작업에서는 **스케줄링 후** cost를 계산합니다. 다음 스크립트는 선택한 Pod 하나를 읽고 controller ReplicaSet UID·노드 분류를 검사한 뒤 검토할 비공개 JSON Patch를 만듭니다. 모든 namespace를 조사하거나 변경하지 않습니다.
공유 label만 보지 말고 애플리케이션 소유자의 실제 Deployment/ReplicaSet 소유 체인에서 `NAMESPACE`, `POD_NAME`, `EXPECTED_RS_UID`를 정합니다. 스크립트는 이 문서의 위치 규칙과 cloud pool만 인식합니다.
```bash
#!/usr/bin/env bash
# Read one owned, scheduled ReplicaSet Pod and write a reviewable JSON Patch.
# This script does not mutate the cluster.
set -euo pipefail
umask 077
: "${KUBECONFIG:?}" "${CONTEXT:?}" "${NAMESPACE:?}" "${POD_NAME:?}"
: "${EXPECTED_RS_UID:?UID of the ReplicaSet owned by the application operator}"
[[ "$NAMESPACE" =~ ^[a-z0-9](https://raw.githubusercontent.com/Atom-oh/kubernetes-docs/main/ko/eks-hybrid-nodes/[-a-z0-9]*[a-z0-9])?$ && ${#NAMESPACE} -le 63 ]]
[[ "$POD_NAME" =~ ^[a-z0-9](https://raw.githubusercontent.com/Atom-oh/kubernetes-docs/main/ko/eks-hybrid-nodes/[-a-z0-9.]*[a-z0-9])?$ && ${#POD_NAME} -le 253 ]]
PLAN_DIR=$(mktemp -d ./deletion-cost-review.XXXXXXXX)
kubectl --kubeconfig "$KUBECONFIG" --context "$CONTEXT" \
get pod "$POD_NAME" -n "$NAMESPACE" -o json |
jq '{metadata: {name: .metadata.name, namespace: .metadata.namespace,
uid: .metadata.uid, resourceVersion: .metadata.resourceVersion,
ownerReferences: .metadata.ownerReferences,
deletionTimestamp: .metadata.deletionTimestamp},
nodeName: .spec.nodeName,
hasAnnotations: (.metadata.annotations | type == "object"),
currentCost: .metadata.annotations["controller.kubernetes.io/pod-deletion-cost"]}' \
> "$PLAN_DIR/pod.json"
jq -e --arg ns "$NAMESPACE" --arg pod "$POD_NAME" --arg owner "$EXPECTED_RS_UID" '
.metadata.namespace == $ns and .metadata.name == $pod and
(.metadata.uid | type == "string" and length > 0) and
(.metadata.resourceVersion | type == "string" and length > 0) and
.metadata.deletionTimestamp == null and
([.metadata.ownerReferences[]? | select(.controller == true)] | length == 1) and
any(.metadata.ownerReferences[]?; .controller == true and
.apiVersion == "apps/v1" and .kind == "ReplicaSet" and .uid == $owner) and
(.nodeName | type == "string" and length > 0)' "$PLAN_DIR/pod.json" > /dev/null
NODE_NAME=$(jq -er '.nodeName' "$PLAN_DIR/pod.json")
[[ "$NODE_NAME" =~ ^[a-z0-9](https://raw.githubusercontent.com/Atom-oh/kubernetes-docs/main/ko/eks-hybrid-nodes/[-a-z0-9.]*[a-z0-9])?$ ]]
kubectl --kubeconfig "$KUBECONFIG" --context "$CONTEXT" \
get node "$NODE_NAME" -o json |
jq '{metadata: {name: .metadata.name, uid: .metadata.uid, labels: {
compute: .metadata.labels["eks.amazonaws.com/compute-type"],
location: .metadata.labels["workload.example.com/location"],
nodepool: .metadata.labels["karpenter.sh/nodepool"]}}}' > "$PLAN_DIR/node.json"
desired=$(jq -er --arg node "$NODE_NAME" '
if .metadata.name != $node or (.metadata.uid | type != "string" or length == 0) then
error("unexpected Node identity")
elif .metadata.labels.compute == "hybrid" and .metadata.labels.location == "onprem" then "1000"
elif .metadata.labels.compute != "hybrid" and .metadata.labels.location == "cloud"
and .metadata.labels.nodepool == "cloud-burst-pool" then "0"
else error("unknown or conflicting node classification") end' "$PLAN_DIR/node.json")
jq --arg cost "$desired" '
[{op:"test",path:"/metadata/uid",value:.metadata.uid},
{op:"test",path:"/metadata/resourceVersion",value:.metadata.resourceVersion},
{op:"test",path:"/spec/nodeName",value:.nodeName}]
+ (if .hasAnnotations then [] else
[{op:"add",path:"/metadata/annotations",value:{}}] end)
+ (if .currentCost == $cost then [] else
[{op:"add",path:"/metadata/annotations/controller.kubernetes.io~1pod-deletion-cost",
value:$cost}] end)' "$PLAN_DIR/pod.json" > "$PLAN_DIR/patch.json"
printf 'Review private snapshots and patch in %s; nothing was applied.\n' "$PLAN_DIR"
```
적용 전 snapshot과 원하는 annotation을 검토합니다. Patch는 Pod UID, resourceVersion과 bound node를 검사하므로 객체 교체·동시 변경에서 새 Pod를 덮어쓰지 않고 실패합니다. Deletion-cost 키만 추가하고 다른 annotation은 보존합니다.
스크립트가 출력한 디렉터리를 호출 셸의 `PLAN_DIR`로 지정합니다. 자식 프로세스는 호출자의 변수를 설정하지 않습니다.
```bash
# Cluster write, only after the named Pod/owner and generated patch are reviewed.
set -euo pipefail
: "${KUBECONFIG:?}" "${CONTEXT:?}" "${NAMESPACE:?}" "${POD_NAME:?}" "${PLAN_DIR:?}"
kubectl --kubeconfig "$KUBECONFIG" --context "$CONTEXT" \
patch pod "$POD_NAME" -n "$NAMESPACE" --type=json \
--patch-file "$PLAN_DIR/patch.json"
```
Test 실패나 노드 분류 변경 시 새 상태에서 다시 만들고 검토합니다. Test를 제거하거나 관련 없는 Pod에 `--overwrite`를 적용하지 않습니다. 기존 컨트롤러와 annotation 소유권을 조정합니다.
빠르게 변하는 metric으로 cost를 계속 갱신하지 않습니다. Kubernetes 문서는 API 업데이트 부담을 설명합니다. 운영 post-binding 컨트롤러에는 좁은 권한, 소유권 검사, 제한된 재시도와 충돌 처리가 필요합니다. 구현되지 않은 이전의 모든-namespace CronJob, 누락된 ServiceAccount/ConfigMap과 변경 가능한 `bitnami/kubectl:latest` 이미지는 그런 컨트롤러가 아닙니다.
Namespace RBAC으로 Pod patch 경계를 제한할 수 있지만 일반 RBAC은 임의의 Pod label selector를 권한으로 표현하지 않습니다. 노드 메타데이터 읽기와 애플리케이션 namespace 쓰기 권한을 적절히 분리합니다. 이 단일 Pod 관리 예제는 클러스터 전체 쓰기 권한을 부여하지 않습니다.
## Karpenter와의 관계
Karpenter1.14.1도 Pod priority와 함께 **정규화된 eviction-cost 계산**에 `pod-deletion-cost`를 사용합니다. ReplicaSet만 이 값을 읽는다는 설명도 잘못입니다. Karpenter는 Pod cost와 노드·중단 정보를 조합하며 값1000이 보존 가능성1000배를 보장하지 않습니다.
| 제어 | 적용 범위 |
|---|---|
| ReplicaSet deletion cost | 해당 ReplicaSet의 축소 후보 사이 선호 |
| Karpenter disruption cost | 후보 평가 heuristic이며 eviction 금지가 아님 |
| `WhenEmpty` | 관련 워크로드 Pod가 없는 적격 노드를 delay·검사 후 통합 가능 |
| `WhenEmptyOrUnderutilized` | 해당 제약에 따라 통합 과정에서 워크로드 이동 가능 |
| PDB / disruption budget / lifecycle 설정 | 서로 적용 범위가 다르며 cost annotation으로 대체하지 않음 |
빈 노드가 즉시 사라진다는 보장은 없습니다. 조정 주기, disruption budget, finalizer와 cloud 종료 과정이 남습니다. Affinity·spread 선호도 통합 기회를 줄일 수 있습니다. 모든 HPA 축소가 같은 수의 cloud node 삭제를 만든다고 가정하지 말고 함께 평가합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-06-workload-placement-0.html)
> **다이어그램 정정:** cloud Pod 우선 삭제와 온프레미스 완전 보존은 best-effort 목표이며 불변 조건이 아닙니다. “빈 노드”에도 DaemonSet Pod가 남을 수 있습니다. 이전 `WhenUnderutilized` label은 v1 예제의 `WhenEmptyOrUnderutilized`로 읽어야 합니다. 위의 정렬·수명 주기 조건을 함께 읽습니다.
## 공식 참고 자료
- [Pod 노드 배치](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/)
- [Taint와 toleration](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
- [Pod topology spread](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/)
- [ReplicaSet deletion cost](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/#pod-deletion-cost)
- [Kubernetes1.36.2 scale-down 비교](https://github.com/kubernetes/kubernetes/blob/v1.36.2/pkg/controller/controller_utils.go)
- [Pod disruption과 PDB 범위](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/)
- [Local volume과 PV node affinity](https://kubernetes.io/docs/concepts/storage/volumes/#local)
- [StorageClass volume binding](https://kubernetes.io/docs/concepts/storage/storage-classes/#volume-binding-mode)
- [Karpenter NodePool](https://karpenter.sh/docs/concepts/nodepools/), [호환성](https://karpenter.sh/docs/upgrading/compatibility/)
- [Karpenter disruption](https://karpenter.sh/docs/concepts/disruption/)
- [Karpenter1.14.1 eviction-cost 구현](https://github.com/kubernetes-sigs/karpenter/blob/v1.14.1/pkg/utils/disruption/disruption.go)
< [이전: GPU 서버 통합](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/05-gpu-integration.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: 노드 라이프사이클 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/07-node-lifecycle.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-hybrid-nodes/07-node-lifecycle
----------------------------------------
# 노드 라이프사이클 관리
< [이전: 워크로드 배치 전략](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/06-workload-placement.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: 운영 및 유지보수](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/08-operations.md) >
> **지원 버전**: AWS가 지원하는 Kubernetes 버전의 EKS Hybrid Nodes; nodeadm 1.0.20 기준 (SSM 신규 설치·업그레이드는 1.0.19 이상 필요)
> **마지막 업데이트**: 2026년 9월 13일
이 문서에서는 EKS Hybrid Nodes의 nodeadm 고급 설정, 대규모 노드 설치 자동화, 업그레이드 전략, 자격 증명 관리 및 헬스체크 자동화를 다룹니다.
## 1. nodeadm 고급 설정 (Advanced NodeConfig)
### kubelet 튜닝
프로덕션 환경에서는 kubelet의 리소스 예약, 축출 임계값, 이미지 가비지 컬렉션 등을 세밀하게 조정해야 합니다.
#### 리소스 예약 (system-reserved / kube-reserved)
이 설정은 Node Allocatable을 산정할 때 예약량을 차감합니다. 모든 호스트 프로세스에 보편적인 강제 상한을 거는 설정은 아닙니다. 기본 enforceNodeAllocatable은 Pod를 대상으로 하며 system/kube 예약의 강제 적용에는 별도 cgroup과 검토된 cgroup 계층이 필요합니다. OS·kubelet·런타임·CNI/드라이버 사용량을 측정해 값을 정합니다.
```yaml
kubelet:
config:
systemReserved:
cpu: "500m"
memory: "1Gi"
ephemeral-storage: "10Gi"
kubeReserved:
cpu: "500m"
memory: "1Gi"
ephemeral-storage: "5Gi"
```
| 파라미터 | 설명 | 초기 산정 예 — 실측 후 조정 |
|----------|------|--------|
| `systemReserved.cpu` | OS 및 시스템 데몬용 CPU | 500m ~ 1000m |
| `systemReserved.memory` | OS 및 시스템 데몬용 메모리 | 1Gi ~ 2Gi |
| `kubeReserved.cpu` | kubelet, containerd용 CPU | 500m ~ 1000m |
| `kubeReserved.memory` | kubelet, containerd용 메모리 | 1Gi ~ 2Gi |
#### 축출 임계값 (Eviction Thresholds)
kubelet은 리소스 압박 시 노드 자원 회수를 시도하고 필요하면 Pod를 축출합니다. 안정성을 보장하는 기능은 아니며 노드 압박 축출은 API eviction 요청처럼 PodDisruptionBudget을 따르지 않습니다.
```yaml
kubelet:
config:
evictionHard:
memory.available: 200Mi
nodefs.available: 10%
imagefs.available: 15%
nodefs.inodesFree: 5%
imagefs.inodesFree: 5%
evictionSoft:
memory.available: 500Mi
nodefs.available: 15%
evictionSoftGracePeriod:
memory.available: 1m30s
nodefs.available: 2m
evictionMaxPodGracePeriod: 60
```
> **참고**: Hard 임계값에는 soft 관찰 유예 기간이 없으며 종료 유예 없이 처리됩니다. Soft 임계값은 evictionSoftGracePeriod 동안 지속되어야 하고, Pod 종료 유예의 상한은 별도 evictionMaxPodGracePeriod로 정합니다. Soft 설정으로 이후 hard 축출이나 OOM이 방지되지는 않습니다. evictionHard를 변경하면 inode를 포함한 전체 의도한 임계값을 명시합니다. 지원되는 기본값 병합을 명시적으로 켜지 않으면 누락한 기본 임계값은 0이 됩니다.
#### maxPods 계산
CPU·메모리·데몬 사용량과 Cilium 노드별 pool을 함께 고려합니다. Cilium cluster-pool은 CIDR당 IPv4 주소 두 개를 예약하므로 /25, /24, /26에서 사용 가능한 주소는 각각 126, 254, 62개입니다. 아래 값은 산정 예이며 검증된 플릿 권장값이나 ENI 기반 한도가 아닙니다. 기존 clusterPoolIPv4MaskSize를 단순 변경해 할당한 블록을 늘릴 수는 없습니다.
```yaml
kubelet:
config:
maxPods: 110 # /25의 사용 가능 IPv4는 126개; 데몬·운영 여유를 남김
```
| 마스크 크기 | 전체 IPv4 주소 수 | maxPods 산정 예 |
|-------------|-------|-------------|
| /25 | 128 | 110 |
| /24 | 256 | 240 |
| /26 | 64 | 50 |
#### 이미지 가비지 컬렉션
디스크 공간 관리를 위해 미사용 이미지를 자동으로 정리합니다.
```yaml
kubelet:
config:
imageGCHighThresholdPercent: 85
imageGCLowThresholdPercent: 80
imageMinimumGCAge: "2m"
```
#### 셧다운 그레이스 기간
지원되는 Linux 호스트의 graceful node shutdown은 systemd inhibitor lock과 kubelet 설정에 의존합니다. 전원 상실이나 강제 종료 때도 정상 종료를 보장하지는 않습니다.
```yaml
kubelet:
config:
shutdownGracePeriod: 60s
shutdownGracePeriodCriticalPods: 20s
```
> **참고**: 이 두 그룹 설정에서 전체 60초에는 critical Pod용 20초가 포함되어 다른 Pod의 종료 창은 40초입니다. 모든 Pod가 항상 40초를 받는다는 뜻은 아니며 각 terminationGracePeriodSeconds와 실제 종료 상황도 영향을 줍니다.
### containerd 고급 설정
#### 프라이빗 레지스트리 미러 설정
아래는 containerd 1.x의 config version 2 문법입니다. containerd 2.x에는 config version 3과 io.containerd.cri.v1.images.registry 경로를 사용합니다. 런타임 재시작 전 설치 버전과 병합된 실제 설정을 확인합니다. 레지스트리 CA·mirror 경로 지원을 검증하고 TLS 검증을 끄지 않습니다.
신뢰하는 프라이빗 레지스트리를 미러로 사용합니다. 아래 server 항목은 upstream endpoint를 fallback으로 유지하므로 에어갭 구성이 아닙니다. 미러에 의존하기 전에 인증, proxy 프로젝트 경로와 CA 신뢰를 검증합니다.
```yaml
containerd:
config: |
version = 2
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
```
`hosts.toml` 파일을 통해 레지스트리별 미러를 구성합니다:
```bash
# /etc/containerd/certs.d/docker.io/hosts.toml
sudo mkdir -p /etc/containerd/certs.d/docker.io
cat <
### 전체 NodeConfig 검토용 템플릿
실행 검증하지 않은 설정 템플릿이며 운영 준비 완료를 뜻하지 않습니다. 앞의 kubelet/containerd fragment는 spec 아래에 배치합니다. 실제 cluster name과 Region을 지정하면 nodeadm이 권한 있는 discovery 경로로 메타데이터를 가져옵니다. 실제 activation 값은 승인된 비공개 NodeConfig 파일로 전달하며 containerd 문법과 자원 값을 호스트에 맞게 선택합니다.
```yaml
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
name: prod-hybrid-cluster
region: ap-northeast-2
hybrid:
ssm:
activationCode:
activationId:
kubelet:
config:
maxPods: 110
shutdownGracePeriod: 60s
shutdownGracePeriodCriticalPods: 20s
systemReserved:
cpu: 500m
memory: 1Gi
ephemeral-storage: 10Gi
kubeReserved:
cpu: 500m
memory: 1Gi
ephemeral-storage: 5Gi
evictionHard:
memory.available: 200Mi
nodefs.available: 10%
imagefs.available: 15%
nodefs.inodesFree: 5%
imagefs.inodesFree: 5%
evictionSoft:
memory.available: 500Mi
nodefs.available: 15%
evictionSoftGracePeriod:
memory.available: 1m30s
nodefs.available: 2m
imageGCHighThresholdPercent: 85
imageGCLowThresholdPercent: 80
evictionMaxPodGracePeriod: 60
flags:
- --node-labels=node.kubernetes.io/instance-type=on-prem-gpu,workload-type=ml-training
- --register-with-taints=eks.amazonaws.com/compute-type=hybrid:NoSchedule
containerd:
config: "version = 2\n[plugins.\"io.containerd.grpc.v1.cri\".registry]\n config_path\
\ = \"/etc/containerd/certs.d\"\n"
```
---
## 2. 대규모 노드 설치 자동화 (Fleet Installation)
### Ansible Playbook
승인한 인벤토리와 작은 배치로 [부트스트랩 절차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md)를 실행합니다. 호스트 아키텍처별 Hybrid Nodes nodeadm 바이너리를 고정하고 검증합니다. 모든 호스트에 amd64 바이너리를 배포하거나 배치 도중 검토하지 않은 `latest`를 다시 받지 않습니다. SSM 신규 설치·업그레이드에는 오래된 설치 프로그램 서명 키 문제 때문에 AWS가 nodeadm **1.0.19 이상**을 요구합니다. 이 장은 1.0.20을 기준으로 확인했으며 실행 전 OS·Kubernetes·CNI·CSI·런타임 조합을 검토해야 합니다.
#### 인벤토리 구성
아래 주소는 문서용 예시입니다. 실제 호스트 접속 대상과 Kubernetes Node 이름·UID를 별도로 기록합니다. 특히 SSM 기반 Node 이름이 SSH로 접속할 수 있는 호스트 이름이라고 가정하지 않습니다.
```ini
[hybrid_nodes:children]
gpu_nodes
cpu_nodes
[gpu_nodes]
gpu-host-a ansible_host=192.0.2.10 kubernetes_node_name=REPLACE_WITH_REGISTERED_NODE_NAME
[cpu_nodes]
cpu-host-a ansible_host=192.0.2.20 kubernetes_node_name=REPLACE_WITH_REGISTERED_NODE_NAME
[hybrid_nodes:vars]
ansible_user=REPLACE_WITH_APPROVED_OPERATOR
```
#### 자동화 플레이북
다음은 **사전 설치한 호스트의 점검용**이며 완전한 설치 플레이북이 아닙니다. root 소유의 비공개 NodeConfig를 승인한 시크릿 파일 전달 절차로 먼저 배포합니다. 활성화 코드나 개인 키를 일반 inventory/group 변수 또는 템플릿 로그에 넣지 않습니다. 두 digest 자리표시자는 선택한 릴리스에서 독립적으로 확인한 값으로 교체합니다. install/init을 실행하거나 중지된 서비스를 시작하면서 건강 상태를 검증했다고 처리하지 않습니다.
```yaml
- name: Review preinstalled Hybrid Nodes before an approved bootstrap
hosts: hybrid_nodes
gather_facts: true
become: true
serial: 1
any_errors_fatal: true
vars:
architecture_map:
x86_64: amd64
aarch64: arm64
approved_nodeadm_sha256:
amd64: REPLACE_WITH_REVIEWED_AMD64_SHA256
arm64: REPLACE_WITH_REVIEWED_ARM64_SHA256
nodeconfig_path: /etc/eks/nodeconfig.yaml
tasks:
- name: Require a reviewed architecture and binary digest
ansible.builtin.assert:
that:
- ansible_facts.architecture in architecture_map
- approved_nodeadm_sha256[architecture_map[ansible_facts.architecture]] is match('^[a-f0-9]{64}$')
- name: Inspect the existing nodeadm artifact
ansible.builtin.stat:
path: /usr/local/bin/nodeadm
checksum_algorithm: sha256
get_checksum: true
register: nodeadm_artifact
- name: Match the approved artifact
ansible.builtin.assert:
that:
- nodeadm_artifact.stat.exists
- nodeadm_artifact.stat.executable
- nodeadm_artifact.stat.checksum == approved_nodeadm_sha256[architecture_map[ansible_facts.architecture]]
- name: Validate the privately delivered NodeConfig
ansible.builtin.command:
argv:
- /usr/local/bin/nodeadm
- config
- check
- -c
- file://{{ nodeconfig_path }}
changed_when: false
no_log: true
```
`/usr/bin/kubelet` 파일 하나만으로 전체 설치 완료를 판정하지 않습니다. 사전 점검 후 부트스트랩 장의 전체 install/init 절차와 호스트별 작업 기록·식별자 검증을 사용합니다. 실패하면 배치를 멈추고 부분 완료·확인 불가 상태를 복구 대상으로 남기며 init을 무조건 재실행하지 않습니다. `changed_when: false`는 Ansible 표시를 바꿀 뿐 명령의 실제 동작을 제한하지 않습니다.
#### 롤별 변수 (GPU 노드 vs 일반 노드)
| 호스트 그룹 | 명시적으로 검토할 호스트별 설정 |
| --- | --- |
| CPU | OS·아키텍처, 런타임 설정 형식, 측정한 예약량, 워크로드 레이블과 테인트 |
| GPU | CPU 전제 조건과 실제 드라이버·툴킷, device-plugin/DRA 경로, 런타임 handler/RuntimeClass, GPU 검증 |
호스트별 템플릿을 명시적으로 선택합니다. `group_names[0]`은 신뢰할 수 있는 역할 선택 기준이 아닙니다. `nvidia.com/gpu.present=true` 레이블만 추가해도 드라이버가 설치되거나 GPU allocatable이 생기는 것은 아닙니다. [GPU 통합](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/05-gpu-integration.md)을 참고합니다.
### 설치 검증 스크립트
클러스터에서 기대하는 **전체 Hybrid Nodes**의 `expected-nodes.json`을 별도로 승인해 저장합니다. 검증 대상 API 응답 자체에서 기대 목록을 만들면 누락된 노드를 발견할 수 없습니다. 교체 시에는 새 식별자를 확인한 뒤 인벤토리를 갱신합니다.
```json
[
{"name": "REPLACE_WITH_REGISTERED_NODE_NAME", "uid": "REPLACE_WITH_APPROVED_NODE_UID"}
]
```
다음을 `check-fleet.sh`로 저장합니다. Bash·kubectl·jq가 필요합니다. 운영자 워크스테이션에서는 `KUBE_CONTEXT`를 지정하고, 아래 클러스터 내부 관측기는 ServiceAccount를 사용합니다. API 실패·권한 거부, 빈 목록, 다른 UID, 조건 누락·Unknown은 0이 아닌 종료 코드로 처리합니다.
```bash
#!/usr/bin/env bash
# Read-only Node inventory/condition snapshot; no workload or host mutations.
set -euo pipefail
EXPECTED_FILE="${1:?Usage: check-fleet.sh expected-nodes.json}"
umask 077
WORK_DIR=$(mktemp -d)
trap 'rm -rf -- "$WORK_DIR"' EXIT
KUBECTL=(kubectl --cache-dir "$WORK_DIR/kube-cache")
if [ -n "${KUBE_CONTEXT:-}" ]; then KUBECTL+=(--context "$KUBE_CONTEXT"); fi
"${KUBECTL[@]}" get nodes -l eks.amazonaws.com/compute-type=hybrid -o json > "$WORK_DIR/nodes.json"
jq -e --slurpfile expected "$EXPECTED_FILE" '
def required($kind; $status):
[.status.conditions[]? | select(.type == $kind)] as $c |
($c | length) == 1 and $c[0].status == $status;
$expected[0] as $want |
($expected | length) == 1 and ($want | type) == "array" and
($want | length) > 0 and
($want | length) == ($want | map(.name) | unique | length) and
all($want[]; (.name | type) == "string" and (.name | length) > 0 and
(.uid | type) == "string" and (.uid | length) > 0) and
(.items | type) == "array" and
(.items | map(.metadata.name) | sort) == ($want | map(.name) | sort) and
all(.items[];
. as $node |
any($want[]; .name == $node.metadata.name and .uid == $node.metadata.uid) and
.metadata.deletionTimestamp == null and
.metadata.labels["eks.amazonaws.com/compute-type"] == "hybrid" and
required("Ready"; "True") and
required("MemoryPressure"; "False") and
required("DiskPressure"; "False") and
required("PIDPressure"; "False"))
' "$WORK_DIR/nodes.json" > /dev/null
printf 'Expected Node identities and conditions match this snapshot.\n'
# CNI readiness, DNS, network paths, storage, applications and freshness need
# separate checks; Node Ready is not an end-to-end health guarantee.
```
이 검사는 Node 목록과 조건의 스냅샷입니다. `Ready=True`만으로 현재 CNI·DNS·스토리지·애플리케이션 상태를 입증하지 않습니다. 선택한 CNI의 실제 DaemonSet/Pod, 사이트 사이 연결, DNS 응답, 스토리지 작업과 워크로드 endpoint를 별도로 점검합니다. 부분 문자열 비교로 `NotReady`를 Ready에 포함하지 않습니다.
## 3. 노드 업그레이드 전략 (Upgrade Strategies)
### 버전 스큐 정책
아래 1.31 표는 skew 계산을 설명하는 과거 버전 예시이며 지금 해당 버전의 노드를 배포하라는 권장이 아닙니다. 현재 AWS 문서에서 지원되는 EKS 대상 버전과 OS·CNI·CSI·런타임 호환성을 선택합니다.
Kubernetes는 kubelet과 API 서버 간 엄격한 버전 호환성 정책을 유지합니다.
| kubelet 버전 | API 서버 버전 | 호환 여부 |
|-------------|-------------|----------|
| 1.31 | 1.31 | ✅ 동일 버전 |
| 1.30 | 1.31 | ✅ n-1 |
| 1.29 | 1.31 | ✅ n-2 |
| 1.28 | 1.31 | ✅ n-3 |
| 1.27 | 1.31 | ❌ n-4 (미지원) |
| 1.32 | 1.31 | ❌ kubelet > API 서버 (미지원) |
> **업그레이드 순서**: 뒤처진 노드를 현재 컨트롤 플레인의 minor 버전으로 먼저 맞춥니다. 노드를 다음 minor 버전으로 올리기 전에는 컨트롤 플레인을 업그레이드합니다. kubelet은 API 서버보다 최신일 수 없으며, 지원되는 skew가 오래된 노드를 계속 유지하라는 권장은 아닙니다.
### 업그레이드 사전 체크리스트
배치마다 대상 major.minor와 실제 artifact 버전·checksum을 승인하고 컨트롤 플레인 skew, OS·CNI·CSI·런타임 호환성을 확인합니다. 축출할 워크로드를 수용할 여유 용량, Pod requests·배치 제약, PDB 허용 중단 수, local PV/emptyDir 소유권, 백업·복구와 관측성을 검토합니다. `kubectl top`의 사용량은 보조 관측값이며 모든 Pod가 다른 노드에 배치될 수 있다는 증거는 아닙니다.
Node 이름·UID, 실제 호스트 접속 대상, 자격 증명 공급자와 기존 `.spec.unschedulable` 상태를 기록합니다. `nodeadm upgrade`는 Node 이름을 보존하며 자격 증명 공급자를 바꾸는 작업이 아닙니다. 기본적으로 지정한 minor의 최신 artifact를 선택하므로 minor 고정만으로 재현 가능한 artifact 계획이 되지는 않습니다. 재현성이 필요하면 승인한 manifest·비공개 artifact 절차를 사용합니다.
### 롤링 업그레이드
**명시적으로 선택한 노드를 한 번에 하나씩** 처리하고 첫 실패에서 멈춥니다. 아래는 실행하지 않은 운영자 절차이며 가용성 보장을 검증한 fleet controller가 아닙니다. 첫·마지막 블록은 승인한 클러스터 관리 워크스테이션에서 같은 셸로 실행하거나 기록한 디렉터리·입력을 복원해 사용합니다. 운영자에게 Node·Lease·Pod/PDB 조회와 cordon/drain 권한이 필요합니다.
비공개 기록 경로를 보관합니다. 다음 명령은 선택한 Node를 cordon하고 drain합니다. PDB 차단이나 local emptyDir은 워크로드·데이터 보존 판단이 필요합니다. 실패를 없애기 위해 force, disable-eviction, delete-emptydir-data를 일괄 추가하지 않습니다.
```bash
set -euo pipefail
umask 077
: "${KUBE_CONTEXT:?Set the approved host cluster context}"
: "${NODE:?Set the actual Kubernetes Node name}"
: "${EXPECTED_UID:?Set the approved Node UID}"
UPGRADE_RECORD_DIR=$(mktemp -d "$PWD/hybrid-upgrade.XXXXXX")
kubectl --context "$KUBE_CONTEXT" get node "$NODE" -o json \
> "$UPGRADE_RECORD_DIR/node-before.private.json"
jq -e --arg uid "$EXPECTED_UID" '
.metadata.uid == $uid and
.metadata.labels["eks.amazonaws.com/compute-type"] == "hybrid" and
.metadata.deletionTimestamp == null
' "$UPGRADE_RECORD_DIR/node-before.private.json" > /dev/null
kubectl --context "$KUBE_CONTEXT" -n kube-node-lease get lease "$NODE" -o json \
> "$UPGRADE_RECORD_DIR/lease-before.private.json"
jq -e --arg uid "$EXPECTED_UID" '
any(.metadata.ownerReferences[]?; .kind == "Node" and .uid == $uid) and
(.spec.renewTime | type) == "string"
' "$UPGRADE_RECORD_DIR/lease-before.private.json" > /dev/null
printf 'Private upgrade record: %s\n' "$UPGRADE_RECORD_DIR"
kubectl --context "$KUBE_CONTEXT" cordon "$NODE"
kubectl --context "$KUBE_CONTEXT" drain "$NODE" --ignore-daemonsets --timeout=10m
```
drain이 성공한 뒤에만 **매핑한 실제 호스트**에 접속하여 식별자와 승인한 nodeadm 바이너리를 확인하고 다음 업그레이드를 실행합니다. 노드 중단이 발생하는 명령입니다. NodeConfig는 기존 자격 증명 공급자를 유지하며 node/pod/init 검증을 통상적으로 생략하지 않습니다.
```bash
set -euo pipefail
: "${TARGET_MINOR:?Set the approved EKS-supported major.minor target}"
sudo /usr/local/bin/nodeadm upgrade "$TARGET_MINOR" \
-c file:///etc/eks/nodeconfig.yaml --timeout 20m
```
명령 오류나 세션 단절은 작업 실패·확인 불가로 취급합니다. Kubernetes에 이전 Ready 조건이 남아 있어도 성공으로 간주하지 않고 cordon을 유지합니다. 호스트 작업의 성공을 확인한 뒤 워크스테이션에서 관측합니다. artifact 계획의 build suffix까지 포함한 전체 kubelet 버전을 사용합니다.
```bash
set -euo pipefail
umask 077
: "${KUBE_CONTEXT:?Set the approved host cluster context}"
: "${NODE:?Set the recorded Node name}"
: "${EXPECTED_UID:?Set the recorded Node UID}"
: "${EXPECTED_KUBELET_VERSION:?Set the full version from the approved artifact plan}"
: "${UPGRADE_RECORD_DIR:?Use the private record directory from the pre-upgrade step}"
jq -e --arg uid "$EXPECTED_UID" --arg node "$NODE" \
'.metadata.uid == $uid and .metadata.name == $node' \
"$UPGRADE_RECORD_DIR/node-before.private.json" > /dev/null
POSTCHECK_DIR=$(mktemp -d "$UPGRADE_RECORD_DIR/check.XXXXXX")
kubectl --context "$KUBE_CONTEXT" get node "$NODE" -o json \
> "$POSTCHECK_DIR/node-after.private.json"
jq -e --arg uid "$EXPECTED_UID" --arg version "$EXPECTED_KUBELET_VERSION" '
[.status.conditions[]? | select(.type == "Ready")] as $ready |
.metadata.uid == $uid and
.metadata.labels["eks.amazonaws.com/compute-type"] == "hybrid" and
.metadata.deletionTimestamp == null and
.status.nodeInfo.kubeletVersion == $version and
($ready | length) == 1 and $ready[0].status == "True"
' "$POSTCHECK_DIR/node-after.private.json" > /dev/null
kubectl --context "$KUBE_CONTEXT" -n kube-node-lease get lease "$NODE" -o json \
> "$POSTCHECK_DIR/lease-after.private.json"
jq -e --arg uid "$EXPECTED_UID" \
--slurpfile before "$UPGRADE_RECORD_DIR/lease-before.private.json" '
def epoch: sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601;
any(.metadata.ownerReferences[]?; .kind == "Node" and .uid == $uid) and
(.spec.renewTime | epoch) > ($before[0].spec.renewTime | epoch)
' "$POSTCHECK_DIR/lease-after.private.json" > /dev/null
printf 'Node identity, target kubelet version, Ready and a newer Lease observed.\n'
# Workload/CNI/DNS/storage checks and the prior scheduling intent remain separate.
# This check never uncordons the node.
```
대상 버전·Node 식별자와 함께 저장한 스냅샷보다 새로운 Lease 갱신을 요구하지만, 이것도 애플리케이션 합격 검사는 아닙니다. CNI·DNS·볼륨·드라이버/런타임·워크로드 복구를 확인한 뒤 원래 스케줄 가능했던 Node만 명시적으로 uncordon합니다. EXIT trap에서 자동 uncordon하거나 원래 의도적으로 cordon했던 노드를 스케줄 가능하게 바꾸지 않습니다. 증거를 보관하고 다음 승인 노드를 처리합니다.
### 카나리 업그레이드
API 목록의 첫 번째 노드 대신 OS·아키텍처·런타임·자격 증명 공급자·워크로드를 대표하는 카나리를 선택합니다. 동일한 한 노드 절차를 적용하고 서비스에서 합의한 관측 기간 동안 오류·지연, 스토리지·네트워크와 실제 버전을 검증합니다. 고정된 sleep이나 Node Ready만으로 통과 처리하지 않습니다. 확인 후 작은 배치로 확대하고, cutover라면 합격 전까지 이전 호스트를 보존합니다.
### 복구와 롤백의 한계
AWS는 여유 용량이 있으면 새 호스트를 준비해 제어된 방식으로 전환하는 절차를 권장합니다. 애플리케이션·스토리지·네트워크와 대상 버전 검사가 끝날 때까지 이전 호스트를 보존합니다. 인플레이스 `nodeadm upgrade`는 노드를 중단하며 일반적인 트랜잭션식 다운그레이드 기능이 아닙니다.
실패하면 해당 노드의 cordon을 유지하고 나머지 작업을 중단한 뒤 진단 자료와 설치된 구성 요소·자격 증명을 확인합니다. 현재 컨트롤 플레인과 호환되는 승인된 이미지/버전으로 복구합니다. 이전 `Ready=True` 상태가 남아 있다는 이유만으로 자동 uncordon하지 말고 예상 노드 식별자, 실제 kubelet 버전과 워크로드 준비 상태도 확인합니다.
`/var/lib/kubelet`이나 `/etc/kubernetes`의 재귀 삭제를 일반적인 롤백 단계로 사용하지 않습니다. Pod volume·subpath mount를 통해 호스트나 애플리케이션 데이터가 노출될 수 있습니다. nodeadm 1.0.9부터 강제 uninstall도 `/var/lib/kubelet`을 의도적으로 보존하므로 예외적인 정리에는 mount와 데이터 보존 검토가 필요합니다. Uninstall은 Kubernetes Node의 drain·삭제나 CNI 전체 제거를 대신하지 않으며 SSM 관리형 인스턴스 등록도 해제합니다. 따라서 재구축에는 완전한 install/부트스트랩과 식별자 대조가 필요하고 무조건 uninstall/reinstall하는 반복문을 사용하지 않습니다.
---
## 4. 자격 증명 라이프사이클 (Credential Lifecycle)
### SSM Hybrid Activation 만료
활성화의 만료는 **새 등록**을 제한합니다. 이미 등록한 노드는 명시적으로 등록 해제할 때까지 Systems Manager 관리형 노드로 남습니다. 원래 활성화가 만료됐다는 이유만으로 정상 노드를 uninstall하거나 재등록하지 않습니다. 등록, 에이전트의 자격 증명 회전, IAM 권한과 연결 상태는 서로 다른 수명 주기입니다.
추가 호스트 등록이나 승인된 복구 과정에서 재등록이 필요한 경우에만 새 활성화를 만듭니다. SSM용으로 구성한 실제 Hybrid Nodes IAM 역할을 사용하고 일반 Run Command 역할로 대체하지 않습니다. 아래 AWS 리소스 생성 명령 전에는 역할의 trust/권한, 계정, Region과 새 노드 수를 확인합니다. 활성화 코드는 비밀번호에 해당하는 비밀이므로 응답을 비공개 파일에 저장하고 승인된 NodeConfig 비밀 파일 전달 절차를 사용합니다.
```bash
set -euo pipefail
umask 077
: "${HYBRID_NODE_ROLE_NAME:?Set the reviewed Hybrid Nodes IAM role name}"
: "${AWS_REGION:?Set the cluster Region}"
: "${NEW_NODE_COUNT:?Set the approved registration count}"
set -C # Refuse to overwrite an existing private response file.
aws ssm create-activation --iam-role "$HYBRID_NODE_ROLE_NAME" --registration-limit "$NEW_NODE_COUNT" --region "$AWS_REGION" --output json > activation.private.json
```
AWS CLI가 실패하면 응답 파일도 불완전할 수 있으므로 중단하고 비공개로 확인한 뒤 재시도합니다. 활성화 코드를 출력하거나 파일을 커밋하지 않습니다. API의 기본 등록 기간이 맞지 않으면 승인된 만료 시각을 명시하며 최대 기간은 30일입니다. 등록 개수 제한은 유출된 활성화를 사용할 수 있는 호스트의 권한 경계를 대신하지 않습니다.
`nodeadm uninstall`은 SSM 기반 호스트의 등록을 해제하고 설치된 구성 요소를 제거합니다. 이후 `init`만 실행해도 `install`이 대체되지는 않습니다. 의도적인 복구라면 drain·데이터 보존과 식별자 변경을 검토한 뒤 완전한 [부트스트랩 절차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md)를 사용합니다. 노드 이름·UID와 SSM 관리형 인스턴스 ID를 인벤토리와 다시 대조하며 이전 성공 기록을 그대로 재사용하지 않습니다.
### IAM Roles Anywhere 인증서 갱신
만료 전에 기존 PKI를 통해 노드의 **호스트 인증용 인증서**를 갱신합니다. kubelet client/server 인증서나 EKS 컨트롤 플레인 CA와는 별개입니다. 인증서 subject, 설정한 nodeName, role session 조건, profile·role·trust anchor가 계속 호환되어야 하며 CN 변경을 단순 파일 교체로 취급하지 않습니다.
#### 인증서 만료 모니터링
임의 경로 대신 NodeConfig의 실제 certificatePath를 검사합니다. 파일이 없거나 읽을 수 없거나 인증서가 잘못됐거나 30일 안에 만료되면 실패합니다. 30일은 운영 경고 임계값의 예시입니다.
```bash
#!/usr/bin/env bash
set -euo pipefail
CERT_PATH="${1:?Usage: check-cert-expiry.sh certificate.pem}"
test -r "$CERT_PATH" || { echo 'Certificate missing or unreadable' >&2; exit 1; }
openssl x509 -in "$CERT_PATH" -checkend 2592000 -noout
# This is an expiration check only, not chain/key/CN/trust/IAM validation.
```
#### 인증서 갱신 절차
조직이 승인한 CA 클라이언트와 인증된 발급 정책을 사용합니다. 범용 무인증 `/sign` endpoint는 없습니다. 기존 개인 키를 재사용하면 보호 상태를 유지하고, 키도 바꾸려면 승인한 키 교체 절차를 따릅니다. 의도한 식별자의 CSR을 제출하고 발급받은 후보 인증서를 활성 파일과 분리해 저장합니다.
다음은 **로컬 검사만** 수행합니다. 잔여 유효 기간, 별도로 승인한 루트에 대한 인증서 체인, 공개 키 일치를 확인하며 중간 CA가 필요하면 `INTERMEDIATE_CHAIN`을 제공합니다. 모든 PKI/IAM 정책, 폐기 여부, nodeName/CN이나 실제 인증 성공을 검증하는 코드는 아닙니다.
```bash
set -euo pipefail
umask 077
: "${CANDIDATE_CERT:?Path to the issued candidate leaf certificate}"
: "${PRIVATE_KEY:?Path to its existing protected private key}"
: "${APPROVED_CA_PEM:?Path to the separately approved trust roots}"
CERT_CHECK_DIR=$(mktemp -d)
trap 'rm -rf -- "$CERT_CHECK_DIR"' EXIT
openssl x509 -in "$CANDIDATE_CERT" -checkend 2592000 -noout
VERIFY=(openssl verify -CAfile "$APPROVED_CA_PEM")
if [ -n "${INTERMEDIATE_CHAIN:-}" ]; then VERIFY+=(-untrusted "$INTERMEDIATE_CHAIN"); fi
"${VERIFY[@]}" "$CANDIDATE_CERT"
openssl x509 -in "$CANDIDATE_CERT" -pubkey -noout > "$CERT_CHECK_DIR/cert.pub"
openssl pkey -in "$PRIVATE_KEY" -pubout > "$CERT_CHECK_DIR/key.pub"
cmp "$CERT_CHECK_DIR/cert.pub" "$CERT_CHECK_DIR/key.pub"
```
반영 전에 subject/SAN·용도·CA 정책·폐기 여부와 정확한 nodeName/role 조건을 검토합니다. 현재 인증서를 비공개로 백업한 후 승인한 인증서 관리자가 소유자·권한을 보존하며 같은 파일시스템에서 검증한 인증서를 원자적으로 교체하도록 합니다. 키와 인증서를 함께 교체하면 소비자가 서로 다른 쌍을 읽지 않도록 조정해야 합니다.
nodeadm이 실제 구성한 credential process 또는 자격 증명 파일 갱신 모드를 확인하고 승인한 비공개 진단 경로에서 다음 credential refresh를 검증합니다. kubelet 재시작만으로 X.509 인증서가 갱신되거나 helper의 새 인증서 수용이 입증되지는 않습니다. 발급자 연동·후보 거부·원자적 반영·refresh·복구를 해당 환경에서 시험한 후 자동 갱신을 구성합니다. 이 장에서는 그러한 운영 환경 갱신을 실행하지 않았습니다.
#### Trust Anchor 업데이트
동일하게 신뢰하는 CA에서 leaf 인증서를 갱신하면 일반적으로 trust anchor를 교체할 필요가 없습니다. CA rollover는 의존하는 전체 호스트·profile·role에 영향을 줍니다. PKI/IAM 담당자와 신뢰 중첩·이전 계획을 세우고, source 유형과 의존 노드를 확인하며 복구 경로를 보존한 후 신뢰 설정을 바꿉니다.
**CERTIFICATE_BUNDLE** source는 PEM 줄바꿈을 보존하도록 JSON 파일로 전달합니다. 아래 sourceData union에는 x509CertificateData만 넣습니다. AWS_ACM_PCA 유형은 acmPcaArn을 사용하며 별도로 검토합니다. 아래 예시는 비공개 요청 파일 생성 후 AWS 업데이트까지 수행하므로 일상적인 인증서 만료 점검으로 실행하지 않습니다.
```bash
set -euo pipefail
umask 077
: "${APPROVED_CA_PEM:?Path to the reviewed CA certificate bundle}"
: "${TRUST_ANCHOR_ID:?Set the reviewed existing trust anchor ID}"
: "${AWS_REGION:?Set the trust anchor Region}"
set -C
jq -n --rawfile bundle "$APPROVED_CA_PEM" \
'{sourceType:"CERTIFICATE_BUNDLE", sourceData:{x509CertificateData:$bundle}}' \
> trust-anchor-source.private.json
# AWS mutation: run only after the CA rollover and dependent-node review.
aws rolesanywhere update-trust-anchor --trust-anchor-id "$TRUST_ANCHOR_ID" \
--region "$AWS_REGION" --source file://trust-anchor-source.private.json \
--output json > trust-anchor-update.private.json
```
의도한 anchor를 다시 조회하고 카나리의 새 자격 증명 발급을 검증한 뒤 rollover를 마칩니다. API 오류는 실패·확인 불가이며 기존 신뢰가 유효하다는 증거가 아닙니다. 발급받은 자격 증명을 로그에 남기거나 anchor 하나를 교체하면 모든 기존 인증서의 접근이 자동 보존된다고 가정하지 않습니다.
## 5. 노드 헬스체크 자동화 (Health Monitoring)
### 자동화된 헬스체크 CronJob
이 관측기는 2절의 Node 스냅샷 검사를 실행합니다. 호스트에서 nodeadm을 실행하거나 AWS 자격 증명을 검증하는 Job이 **아닙니다**. Bash·kubectl·jq를 포함하고 클러스터와 호환되며 UID10001로 실행 가능한 검토한 이미지를 준비해야 합니다. 배포 전 이미지 자리표시자를 교체해야 하며 이 장에서 이미지·클러스터 실행은 검증하지 않았습니다. monitoring namespace도 먼저 있어야 합니다.
저장한 check-fleet.sh와 승인한 expected-nodes.json으로 ConfigMap manifest를 만든 뒤 아래 ServiceAccount/RBAC/CronJob과 함께 검토·적용합니다. ClusterRole은 모든 Node를 나열할 수 있습니다. label selector는 조회 필터이며 권한 경계가 아닙니다. ConfigMap 수정과 이 ServiceAccount로 Pod를 실행할 수 있는 namespace 사용자의 권한도 검토합니다.
```bash
: "${KUBE_CONTEXT:?Set the approved cluster context}"
kubectl --context "$KUBE_CONTEXT" -n monitoring create configmap hybrid-node-check \
--from-file=check-fleet.sh --from-file=expected-nodes.json \
--dry-run=client -o yaml > hybrid-node-check.yaml
# Review this manifest and the observer/RBAC manifest before applying either.
```
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: hybrid-node-observer
namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: hybrid-node-observer
rules:
- apiGroups:
- ''
resources:
- nodes
verbs:
- get
- list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: hybrid-node-observer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: hybrid-node-observer
subjects:
- kind: ServiceAccount
name: hybrid-node-observer
namespace: monitoring
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: hybrid-node-observer
namespace: monitoring
spec:
schedule: '*/30 * * * *'
concurrencyPolicy: Forbid
startingDeadlineSeconds: 120
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 2
jobTemplate:
spec:
backoffLimit: 0
activeDeadlineSeconds: 120
ttlSecondsAfterFinished: 1800
template:
spec:
serviceAccountName: hybrid-node-observer
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: observer
image: example.invalid/hybrid-observer:replace-with-reviewed-build
command:
- /bin/bash
- /config/check-fleet.sh
- /config/expected-nodes.json
env:
- name: TMPDIR
value: /work
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
volumeMounts:
- name: config
mountPath: /config
readOnly: true
- name: work
mountPath: /work
volumes:
- name: config
configMap:
name: hybrid-node-check
defaultMode: 292
- name: work
emptyDir:
sizeLimit: 64Mi
```
Job 실패뿐 아니라 **실행 누락·최근 성공 없음**에도 알립니다. 30분 주기로 즉각적인 장애 탐지를 보장할 수 없습니다. 알림은 관측 시스템의 보호된 연동을 사용합니다. Slack webhook bearer URL을 Pod 환경 변수에 넣거나 알림 전송 실패를 건강 상태 검증 성공으로 취급하지 않습니다.
### kubelet/containerd 상태 모니터링 (노드 레벨)
다음 root 소유 스크립트는 서비스를 재시작하지 않고 로컬 서비스 상태와 루트 파일시스템을 관찰합니다. kubelet/containerd/image 경로가 별도 마운트이면 추가 점검이 필요하며 루트 디스크 사용률만으로 충분하지 않습니다. 명령 실행 불가나 잘못된 측정값을 정상으로 처리하지 않습니다.
```bash
#!/usr/bin/env bash
# Observe local services/filesystem; do not restart anything automatically.
set -euo pipefail
failed=0
for service in kubelet containerd; do
if ! systemctl is-active --quiet "$service"; then
printf '%s is not active\n' "$service" >&2
failed=1
fi
done
usage=$(df --output=pcent / | tail -n 1 | tr -d ' %')
case "$usage" in ''|*[!0-9]*) echo 'Unknown filesystem usage' >&2; exit 1;; esac
if [ "$usage" -ge 90 ]; then
printf 'Root filesystem usage: %s%%\n' "$usage" >&2
failed=1
fi
exit "$failed"
```
검토한 호스트 관리 절차로 `/usr/local/bin/node-health-check.sh`에 실행 권한을 주어 설치하고 신뢰하지 않는 사용자가 수정하지 못하도록 합니다. timer/unit은 구성 예시이며 journal·failed unit 감시를 별도로 연결해야 합니다. `PrivateTmp`가 자격 증명을 로그에 남겨도 된다는 의미는 아닙니다.
```ini
[Unit]
Description=Periodic Hybrid Node local observation
[Timer]
OnCalendar=*:0/5
Persistent=true
[Install]
WantedBy=timers.target
```
```ini
[Unit]
Description=Observe Hybrid Node local services and root filesystem
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/node-health-check.sh
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
```
특정 네트워크·자격 증명 문제를 진단할 때 운영자가 root 권한으로 `nodeadm debug -c file:///etc/eks/nodeconfig.yaml`을 별도로 실행할 수 있습니다. AWS와 클러스터에 접속하고 진단 문맥을 출력하므로 결과는 비공개로 보관합니다. 매 타이머마다 조용히 실행해 오류를 버리거나 장애별 판단 없이 kubelet/containerd를 자동 재시작하지 않습니다.
## 검증 범위와 근거
로컬 검증 범위는 예제 구문, 가상 Node/API 실패 사례와 합성 인증서 검사입니다. 실제 fleet 설치·호스트 업그레이드·credential rollover·Kubernetes admission·CNI/DNS/스토리지 시험이나 운영 SLO 달성을 입증하지 않습니다.
- [AWS Hybrid Nodes nodeadm](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-nodeadm.html)
- [AWS Hybrid Nodes upgrades](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-upgrade.html)
- [SSM registration and activation lifetime](https://docs.aws.amazon.com/systems-manager/latest/userguide/hybrid-activation-managed-nodes.html)
- [IAM Roles Anywhere credential configuration](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-creds.html)
- [UpdateTrustAnchor input](https://docs.aws.amazon.com/botocore/latest/reference/services/rolesanywhere/client/update_trust_anchor.html)
- [Kubernetes version skew](https://kubernetes.io/releases/version-skew-policy/)
- [Node pressure eviction](https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/)
- [Node Allocatable](https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/)
- [Graceful node shutdown](https://kubernetes.io/docs/concepts/cluster-administration/node-shutdown/)
- [containerd configuration](https://github.com/containerd/containerd/blob/main/docs/cri/config.md)
- [Cilium 1.20.1 cluster-pool allocator](https://github.com/cilium/cilium/blob/v1.20.1/Documentation/network/concepts/ipam/cluster-pool.rst)
---
< [이전: 워크로드 배치 전략](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/06-workload-placement.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: 운영 및 유지보수](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/08-operations.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-hybrid-nodes/08-operations
----------------------------------------
# 운영 및 유지보수
< [이전: 노드 라이프사이클 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/07-node-lifecycle.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: 베어메탈 서버 OS 설치](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/09-bare-metal-os-setup.md) >
> **검증 기준**: nodeadm 1.0.20; Cilium 1.18.3 CRD; Prometheus Operator 0.93.1; kube-prometheus-stack 90.0.0; Harbor 2.15.2. 검토에 사용한 기준이며 모든 제품의 지원 버전 조합을 뜻하지 않습니다.
> **마지막 업데이트**: 2026년 9월 13일
이 문서에서는 EKS Hybrid Nodes 환경의 운영 및 유지보수 절차를 다룹니다.
## Harbor 취약점 스캔 자동화
관리자 비밀번호를 가진 CronJob이 latest 태그만 순회하는 대신 Harbor 내장 scan-all 스케줄러를 사용합니다. 시스템 관리자 UI에서 **Administration → Interrogation Services → Vulnerability → Schedule to scan all**을 엽니다. Hourly/Daily/Weekly/Custom을 지원하며 문서화된 UI의 Daily는 자정입니다. 오전 02:00 작업 창이 필요하면 배포한 버전에 맞는 Custom 문법·시간대·다음 실행 시각을 확인합니다.
Harbor 2.15.2에는 GET/POST/PUT `/api/v2.0/system/scanAll/schedule` API가 있습니다. UI 또는 신뢰할 CA 검증·보호된 자격 증명·필요한 시스템 권한을 갖춘 승인한 API 연동을 사용합니다. 프로젝트 robot 권한이 전체 스캔 설정 권한을 뜻하지 않습니다. 관리자 비밀번호를 Pod 환경 변수·명령 인자에 넣거나 TLS 검증을 끄지 않습니다.
스캐너 상태, 취약점 DB 최신성, 실제 완료와 실패를 기록합니다. 요청 제출이나 빈 API 응답은 취약점 평가 성공이 아닙니다. 지원하지 않는 artifact도 별도로 처리해야 하며 전체 스캔은 리소스를 소비합니다. 일부 artifact를 자동화할 때도 필요한 모든 페이지·digest를 열거하고 repository 경로를 올바르게 인코딩하며 모든 응답을 확인합니다. latest만 순회하는 루프는 전체 레지스트리를 검사하지 않습니다.
[Harbor 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/container-registry/03-harbor.md)와 [공식 스케줄 절차](https://github.com/goharbor/website/blob/main/docs/administration/vulnerability-scanning/schedule-scans.md)를 참고합니다.
## 데이터베이스 백업 절차
복구 목표와 인벤토리를 정한 뒤 데이터를 복사합니다. Harbor 복구에는 호환되는 DB 메타데이터, registry blob/object storage, 구성과 보호된 시크릿·암호화 키가 필요합니다. PostgreSQL dump만으로 전체 백업이 되지 않습니다. 최신 Harbor에는 Notary v1이 없으므로 notarysigner/notaryserver DB가 있다고 가정하지 않습니다.
[공식 Harbor Velero 절차](https://github.com/goharbor/website/blob/main/docs/administration/backup-restore/_index.md)는 repository read-only 모드와 지정한 Kubernetes 리소스/PV를 사용합니다. 결과는 **application-consistent가 아닌 crash-consistent**이며 Redis를 제외합니다. 동기화되지 않은 메타데이터·세션이 유실되거나 작업 정리가 필요할 수 있습니다. 내부 DB를 대상으로 하며 외부 관리형 DB는 포함하지 않습니다. 지원하는 snapshot/file-backup/data-movement 플러그인을 선택하고 복구 위치에서 모든 볼륨·object 데이터에 접근할 수 있는지 확인합니다. snapshot 참조만으로 별도 위치에 복원 가능한 복사본이 되지는 않습니다.
다음은 확인한 내부 PostgreSQL Pod의 **DB 구성 요소 dump 예시**입니다. pg_dump/pg_restore와 로컬 인증이 이미 구성되어 있어야 합니다. 외부 DB는 해당 시스템의 인증된 백업·복원 절차를 사용합니다. 자격 증명을 명령 인자에 전달하지 않습니다. 오류에서 중단하고 부분 파일을 비공개로 보관하며 완료된 dump로 처리하지 않습니다.
```bash
set -euo pipefail
umask 077
: "${KUBE_CONTEXT:?Set the approved cluster context}"
: "${HARBOR_NAMESPACE:?Set the Harbor namespace}"
: "${HARBOR_DB_POD:?Set the verified internal PostgreSQL Pod}"
: "${HARBOR_DB_USER:?Set the approved backup database user}"
: "${HARBOR_DB_NAME:?Set the actual Harbor database name}"
: "${PRIVATE_BACKUP_ROOT:?Set an existing protected durable directory}"
BACKUP_DIR=$(mktemp -d "$PRIVATE_BACKUP_ROOT/harbor-db.XXXXXX")
kubectl --context "$KUBE_CONTEXT" -n "$HARBOR_NAMESPACE" exec "$HARBOR_DB_POD" -- \
pg_dump --format=custom --username "$HARBOR_DB_USER" --dbname "$HARBOR_DB_NAME" \
> "$BACKUP_DIR/registry.dump.partial"
test -s "$BACKUP_DIR/registry.dump.partial"
kubectl --context "$KUBE_CONTEXT" -n "$HARBOR_NAMESPACE" exec -i "$HARBOR_DB_POD" -- \
pg_restore --list < "$BACKUP_DIR/registry.dump.partial" > "$BACKUP_DIR/archive-toc.private.txt"
mv "$BACKUP_DIR/registry.dump.partial" "$BACKUP_DIR/registry.dump"
printf 'Database archive created: %s; full Harbor recovery requires separate evidence.\n' "$BACKUP_DIR"
```
archive 목록을 읽었다고 복원이 입증되지는 않습니다. 호환되는 PostgreSQL/Harbor 버전으로 복원을 시험하고 artifact pull, 메타데이터, 권한과 연동을 검증합니다. 전체 백업 인벤토리를 보호·체크섬 검증하고 read-only 모드, 작업과 upload/GC 활동을 조정합니다. 실패한 작업의 상태를 확인하지 않은 채 read-only를 자동 해제하지 않습니다.
Redis BGSAVE는 비동기 작업이므로 바로 dump.rdb를 복사하면 이전 세대 파일일 수 있습니다. 별도 설계에서 Redis persistence를 포함한다면 담당자와 완료·상태·세대를 검증합니다. Redis를 제외하는 공식 튜토리얼과 해당 맞춤 설계를 무심코 섞지 않습니다. 이 장에서는 백업·복원을 실행하지 않았습니다.
## Prometheus 메트릭 수집
호스트, kubelet/container, GPU 지표를 구분합니다. `node_cpu_seconds_total`과 `node_memory_*`는 kubelet endpoint가 아니라 Node Exporter가 제공합니다. 검토한 Node Exporter/DCGM 구성에서 실제 host mount, 권한, 노드 배치와 지표 가용성을 확인합니다. Container Insights는 EC2 IMDS를 통해 Hybrid 호스트 수준 지표를 제공하지 않습니다.
아래 discovery 예시는 `monitoring` namespace의 `kube-prom` 릴리스에 대해 kube-prometheus-stack 90.0.0의 Node Exporter Service label·port를 사용합니다. DCGM 부분은 `gpu-operator` namespace에 `app: nvidia-dcgm-exporter` label과 이름이 `metrics`인 container port를 가진 Pod가 있다는 전제이므로 설치한 exporter와 맞춥니다. Monitor 리소스가 exporter를 설치하지는 않습니다. Prometheus 리소스의 monitor·namespace selector를 확인하고 기존 monitor와 중복 수집하지 않습니다.
`attachMetadata.node`는 Node discovery 메타데이터를 제공하지만 이를 지표 label로 자동 복사하지 않습니다. ServiceMonitor는 Prometheus >=2.37, PodMonitor는 >=2.35와 Prometheus ServiceAccount의 Node `list`/`watch` 권한이 필요합니다. relabeling은 실제 `eks.amazonaws.com/compute-type=hybrid` 노드를 선택하고 `node`·`compute_type` target label을 만듭니다. SSM Node 이름의 접두사로 Hybrid 배치를 추측하지 않습니다.
이 예시는 kubelet HTTPS가 아니라 보호된 exporter HTTP endpoint를 사용합니다. 수집기의 접근을 제한합니다. 신뢰하지 않는 경계를 지나면 exporter TLS·인증 또는 검토한 proxy와 CA·authorization 설정을 적용하며 `insecureSkipVerify`를 사용하지 않습니다. kubelet 수집은 별도의 인증·CA 검증 구성을 유지합니다.
```yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: hybrid-node-exporter
namespace: monitoring
labels:
release: kube-prom
spec:
attachMetadata:
node: true
selector:
matchLabels:
app.kubernetes.io/name: prometheus-node-exporter
app.kubernetes.io/instance: kube-prom
namespaceSelector:
matchNames: [monitoring]
endpoints:
- port: http-metrics
interval: 30s
relabelings:
- sourceLabels: [__meta_kubernetes_node_label_eks_amazonaws_com_compute_type]
regex: hybrid
action: keep
- sourceLabels: [__meta_kubernetes_pod_node_name]
targetLabel: node
- targetLabel: compute_type
replacement: hybrid
---
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: hybrid-gpu-metrics
namespace: monitoring
labels:
release: kube-prom
spec:
attachMetadata:
node: true
selector:
matchLabels:
app: nvidia-dcgm-exporter
namespaceSelector:
matchNames: [gpu-operator]
podMetricsEndpoints:
- port: metrics
interval: 30s
relabelings:
- sourceLabels: [__meta_kubernetes_node_label_eks_amazonaws_com_compute_type]
regex: hybrid
action: keep
- sourceLabels: [__meta_kubernetes_pod_node_name]
targetLabel: node
- targetLabel: compute_type
replacement: hybrid
```
### Grafana 대시보드 쿼리 예시
위 target label과 호환되는 exporter 지표가 있어야 합니다. 단위, GPU/MIG 식별자, 미지원·오류 sentinel, scrape 누락과 중복 series를 확인합니다. GPU framebuffer 사용률은 free가 아니라 전체 용량(`used + free`)으로 나누며, 용량이 0인 series는 제외합니다. 아래 식은 로컬에서 검증할 수 있는 쿼리이며 이 환경의 실측값이 아닙니다.
```promql
# Host CPU utilization percent
100 * (1 - avg by (node) (rate(node_cpu_seconds_total{mode="idle",compute_type="hybrid"}[5m])))
# Host memory utilization percent
100 * (1 - node_memory_MemAvailable_bytes{compute_type="hybrid"} / node_memory_MemTotal_bytes{compute_type="hybrid"})
# GPU utilization: this metric is already a percentage
DCGM_FI_DEV_GPU_UTIL{compute_type="hybrid"}
# GPU framebuffer usage: used / (used + free), excluding zero capacity
(100 * DCGM_FI_DEV_FB_USED{compute_type="hybrid"} /
(DCGM_FI_DEV_FB_USED{compute_type="hybrid"} + DCGM_FI_DEV_FB_FREE{compute_type="hybrid"}))
and
((DCGM_FI_DEV_FB_USED{compute_type="hybrid"} + DCGM_FI_DEV_FB_FREE{compute_type="hybrid"}) > 0)
```
## Direct Connect 성능 검증
시험 계획과 AWS 서비스 보장을 구분합니다. 기존 예제의 RTT <5ms, 변동 <2ms, 손실 <0.01%, 처리량 >1Gbps는 예시 목표이며 실측 결과나 모든 Direct Connect 연결의 보장이 아닙니다. 실제 위치, 회선, endpoint, workload와 계약 용량에 맞춰 목표를 정하고 측정 경로가 VPN 등 다른 경로가 아닌 Direct Connect인지 확인합니다.
`ping`은 ICMP RTT와 Linux iputils의 RTT mdev를 보고합니다. 이 분산 지표는 단방향 지연 변동이나 iperf3 UDP jitter와 같지 않습니다. ICMP 필터링·우선순위는 실제 애플리케이션 트래픽과 다를 수 있습니다. 1,000개 패킷의 손실 비율 단위는 0.1%이며, 손실 0개를 관측했다고 장기 손실률 <0.01%가 입증되지는 않습니다.
승인된 private test server에서 iperf3가 준비되어 있어야 합니다. 시험 시간과 전송률 상한을 조정하고 결과를 보호합니다. EKS API endpoint에 iperf3를 실행하지 않습니다. 다음 예시는 Bash, Python3, iputils ping, GNU timeout과 해당 옵션을 지원하는 iperf3가 필요합니다. 오류가 나면 중단하며 도구 누락, 잘못된 JSON, iperf3 오류는 성능 시험 통과가 아닙니다.
```bash
set -euo pipefail
umask 077
: "${PROBE_HOST:?Set the approved private test host}"
: "${TEST_BITRATE:?Set an approved traffic cap, for example 10M}"
: "${PRIVATE_RESULTS_ROOT:?Set an existing protected results directory}"
if [[ ! "$TEST_BITRATE" =~ ^[1-9][0-9]*[KMGT]?$ ]]; then
printf 'TEST_BITRATE must be a positive integer with an optional K/M/G/T suffix.\n' >&2
exit 2
fi
RUN_DIR=$(mktemp -d "$PRIVATE_RESULTS_ROOT/dx-check.XXXXXX")
date -u +%FT%TZ > "$RUN_DIR/started-at.txt"
LC_ALL=C ping -n -c 100 -W 2 "$PROBE_HOST" > "$RUN_DIR/ping.txt"
timeout 45s iperf3 --client "$PROBE_HOST" --connect-timeout 3000 \
--time 10 --bitrate "$TEST_BITRATE" --json > "$RUN_DIR/iperf-tcp.json"
python3 - "$RUN_DIR/iperf-tcp.json" <<'PY'
import json, math, sys
with open(sys.argv[1]) as stream:
result = json.load(stream)
if result.get("error") or not isinstance(result.get("end"), dict):
raise SystemExit("iperf3 result is incomplete or reports an error")
received = result["end"].get("sum_received", {})
for field in ("bits_per_second", "bytes", "seconds"):
value = received.get(field)
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0:
raise SystemExit("Missing or invalid TCP receiver statistics")
if received["seconds"] <= 0:
raise SystemExit("Invalid TCP test duration")
print("Saved a completed iperf3 result; compare receiver statistics with the approved test plan.")
PY
printf 'Private observations: %s; this does not establish an AWS latency/throughput guarantee.\n' "$RUN_DIR"
```
전송률을 제한한 TCP 시험으로 회선 최대 용량을 입증하지는 못합니다. 수신 측 처리량, 재전송, 방향, 기간과 혼잡을 확인합니다. 별도로 승인한 UDP 시험에는 `--udp`와 명시적인 bitrate를 사용하고 해당 버전 JSON의 수신 측 loss/jitter를 보존·해석합니다. 실패·생략한 시험은 그대로 기록합니다. 이 감사에서는 네트워크 부하 시험을 실행하지 않았습니다.
## 인증서 갱신 관리
Harbor TLS 서버 인증서, CA 체인, kubelet serving 인증서, EKS 컨트롤 플레인 CA, IAM Roles Anywhere 호스트 인증서 중 무엇을 점검하는지 먼저 구분합니다. CA 인증서가 유효해도 서버 leaf의 유효성을 입증하지 못하며, Node Ready heartbeat는 인증서 만료 정보가 아닙니다. EKS 컨트롤 플레인 인증서는 AWS가 관리하므로 kubeadm 갱신 명령을 EKS 복구 절차로 사용하지 않습니다.
다음 로컬 만료 검사는 파일 누락·읽기 실패·잘못된 인증서 또는 예시 경고 기간 30일 이내의 만료를 실패로 처리합니다. 체인·호스트 이름·폐기 여부나 실제 서비스가 제시하는 인증서를 확인하는 검사는 아닙니다. 실제 endpoint에는 아래 TLS 검사를 사용하고, 호스트 인증서 갱신은 [자격 증명 라이프사이클 절차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/07-node-lifecycle.md)를 따릅니다.
```bash
set -euo pipefail
: "${CERT_PATH:?Set the actual certificate file to inspect}"
test -r "$CERT_PATH"
openssl x509 -in "$CERT_PATH" -checkend 2592000 -noout
```
실제 발급자·담당자, 설정한 인증서 경로, 만료일과 경고 전달을 추적합니다. kubelet serving/client 자격 증명 경로는 설정에 따라 다릅니다. serverTLSBootstrap만 활성화해도 serving CSR이 승인되거나 IAM Roles Anywhere 인증서가 갱신되는 것은 아닙니다.
## Ingress 구성
### ALB Ingress (ip target mode)
자체 관리형 AWS Load Balancer Controller는 `alb.ingress.kubernetes.io/target-type: ip`로 도달 가능한 Hybrid Pod IP를 등록할 수 있습니다. 라우트, 반환 경로, security group/firewall과 EKS remote Pod network 구성이 맞아야 합니다.
AWS 혼합 모드 webhook 예제는 controller를 cloud node에 배치합니다. 이는 해당 설계의 권장 배치이며 Hybrid Node에서 webhook을 실행하는 것이 언제나 불가능하다는 뜻은 아닙니다. Add-on 지침은 control plane에서 설정한 remote Pod CIDR로 접근할 수 있을 때 Hybrid 배치를 허용합니다. label이 없는 노드까지 선택하는 `compute-type NotIn [hybrid]` 대신 관리자가 확인한 배치 label을 사용합니다. 아래 예시 label은 적격 cloud node에만 부여해야 합니다.
```yaml
# Fragment under the controller Deployment's spec.template.spec:
nodeSelector:
infrastructure.example.com/location: aws
```
### Cilium Ingress Controller
이 절의 Cilium Ingress와 Gateway API 예제는 L7 proxy를 활성화한 Cilium 구성을 전제로 합니다. [EKS Hybrid Nodes Gateway](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/10-hybrid-nodes-gateway.md)를 위해 구성한 동일 Cilium 설치에는 함께 적용할 수 없습니다. Gateway의 [AWS 필수 VTEP 설정](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-gateway-cni.html)은 `vtep.enabled=true`, `l7Proxy=false`입니다. 기능을 활성화하기 전에 네트워크 설계를 선택합니다. 이 제약은 Cilium의 L7 proxy 기능에 해당하며, HTTP 애플리케이션이 Gateway의 라우팅 경로를 이용하지 못한다는 뜻은 아닙니다.
Cilium 1.18.3 upstream Ingress에는 NodePort 지원 또는 kube-proxy replacement, L7 proxy와 노출할 load-balancer 경로가 필요합니다. 다음 조각을 적용한다고 혼합 클러스터의 cloud node CNI를 바꿔도 되는 것은 아닙니다. 검토한 Hybrid Cilium 구성을 보존하고 추가 기능의 AWS 지원 범위를 확인합니다. dedicated/shared 전환은 주소 변경과 기존 연결 중단을 일으킬 수 있습니다.
```yaml
# Merge into the reviewed Cilium release values, not a full installation:
nodePort:
enabled: true
l7Proxy: true
ingressController:
enabled: true
loadbalancerMode: dedicated
```
### Cilium Gateway API
선택한 controller 버전이 지원하는 Gateway API CRD·리소스 버전을 설치하고 NodePort/kube-proxy replacement와 L7 전제 조건, GatewayClass/Gateway/Route condition을 확인합니다. Helm flag 하나가 CRD 설치나 외부 도달성을 보장하지 않습니다.
```yaml
# Required Gateway API CRDs and controller prerequisites must already be met:
gatewayAPI:
enabled: true
```
### LoadBalancer IPAM (Cilium)
다음 pool은 1.18.3 CRD로 확인한 `cilium.io/v2` API를 사용하고 명시한 label이 있는 Service만 선택합니다. 예시 CIDR은 네트워크 인벤토리에서 예약한 비중복 주소 범위로 바꿉니다. IP 할당이 라우터 광고나 실제 통신 성공을 보장하지는 않습니다.
```yaml
apiVersion: cilium.io/v2
kind: CiliumLoadBalancerIPPool
metadata:
name: on-prem-pool
spec:
blocks:
- cidr: "10.80.100.0/24"
serviceSelector:
matchLabels:
exposure: onprem-bgp
```
## 로드 밸런싱
### NLB (ip target mode)
자체 관리형 AWS Load Balancer Controller에는 소유권을 지정한 `LoadBalancer` Service의 `spec.loadBalancerClass: service.k8s.aws/nlb`와 `service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip`, 또는 해당 controller의 문서화된 `aws-load-balancer-type: external` 방식을 사용합니다. target-type annotation만으로 controller가 선택되지는 않습니다. AWS에서 Hybrid Pod target과 반환 경로에 접근할 수 있어야 하며 internal/public 노출, subnet과 접근 통제를 명시적으로 정합니다.
이 예시는 EKS Auto Mode 소유권 구성이 아닙니다. 기존 Service의 class/controller를 바꿀 때는 리소스 교체와 트래픽 영향을 검토해야 합니다.
### Cilium LB + BGP
설치한 `cilium.io/v2` schema를 사용합니다. Service 주소 종류의 위치는 `advertisements[].service.addresses`입니다. 아래 예시는 `exposure: onprem-bgp` label이 있는 Service의 LoadBalancer IP만 광고하며, 사실상 모든 Service를 선택하는 `NotIn` 조건을 사용하지 않습니다.
```yaml
apiVersion: cilium.io/v2
kind: CiliumBGPAdvertisement
metadata:
name: hybrid-service-advertisement
labels:
advertise: hybrid-services
spec:
advertisements:
- advertisementType: Service
service:
addresses: [LoadBalancerIP]
selector:
matchLabels:
exposure: onprem-bgp
```
CiliumBGPClusterConfig의 node·peer 선택과 CiliumBGPPeerConfig의 family를 구성합니다. Peer의 advertisement selector는 `advertise: hybrid-services`와 일치해야 하며 Service selector도 실제 Service label과 맞아야 합니다. 검토한 BGP control plane을 활성화하고 router session, 수락된 라우트, next hop, 반환 경로와 traffic policy를 확인합니다. IPPool/Advertisement만으로는 완성된 구성이 아닙니다. 변경 전에 [네트워크 기초](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/02-network-configuration.md)와 설치 버전의 BGP 문서를 참고합니다.
## 애드온 상세 설정
### CloudWatch Observability Agent
지원되는 Pod Identity 구성을 사용하고 실제 workload IAM association·권한을 확인합니다. 현재 AWS 절차는 이름과 달리 Pod Identity 환경에서도 Hybrid 호환 변수 `RUN_WITH_IRSA`를 요구합니다. 기존 `AmazonCloudWatchAgent` 리소스의 **spec.env 목록**에 추가하고 `K8S_NODE_NAME` 등 기존 항목을 보존합니다. 임의의 최상위 `env`를 EKS add-on configurationValues에 넣는 형태가 아닙니다.
```yaml
# Add this item to the existing AmazonCloudWatchAgent.spec.env list:
- name: RUN_WITH_IRSA
value: "True"
```
편집 전에 `amazon-cloudwatch` namespace의 `amazoncloudwatchagents/cloudwatch-agent`를 확인하고 add-on/operator가 이 구성을 어떻게 조정하는지 검토합니다. 이후 agent rollout과 실제 수집을 확인합니다. Hybrid의 cluster/workload/Pod/container 지표는 지원하지만 EC2 IMDS 의존성이 없어 node-level Container Insights 지표는 제공하지 않습니다. Operator가 Hybrid Node에 있으면 control plane webhook 접근 조건도 충족해야 합니다.
### EKS Pod Identity Agent
| 호스트 OS | 문서화된 최소 조건 | Hybrid DaemonSet / credential 경로 |
| --- | --- | --- |
| Ubuntu, RHEL, AL2023 | Add-on 1.3.3-eksbuild.1 | `hybrid`; `/eks-hybrid/.aws/credentials` |
| Bottlerocket (지원되는 VMware variant) | Add-on 1.3.7-eksbuild.2 및 OS 1.39.0 | `hybrid-bottlerocket`; `/var/eks-hybrid/.aws/credentials` |
이는 기능 최소 버전이며 오래된 버전 설치 권장이 아닙니다. 현재 호환되는 add-on 버전과 configuration schema를 확인합니다. Ubuntu/RHEL/AL2023에서는 각 호스트의 기존 전체 NodeConfig에 다음 조각을 병합합니다.
```yaml
# Merge this fragment into each host's complete, protected NodeConfig:
spec:
hybrid:
enableCredentialsFile: true
```
AWS 절차는 이미 가입한 노드를 포함하여 대상 호스트마다 계획된 `nodeadm init -c file:///path/to/nodeconfig.yaml` 조정을 요구합니다. 운영 노드를 일괄 재초기화하지 말고 identity·구성을 보존하면서 수명주기 절차에 따라 한 호스트씩 검증합니다. Bottlerocket은 이 nodeadm 조각이 아니라 문서화된 OS 설정 경로를 사용합니다. 임시 credential 파일은 민감하므로 출력하지 않습니다.
Bottlerocket이 아닌 Hybrid DaemonSet을 위한 add-on 설정에는 다음이 포함됩니다.
```json
{
"daemonsets": {
"hybrid": {
"create": true
}
}
}
```
Bottlerocket은 선택한 버전의 `hybrid-bottlerocket` 설정을 사용합니다. 기존 설정을 조회·병합하고 add-on이 없을 때만 생성합니다. 이미 설치되어 있으면 무조건 create 또는 OVERWRITE하지 말고 충돌 처리를 검토해 업데이트합니다. Agent와 credential 파일이 애플리케이션별 Pod Identity association을 생성해 주는 것은 아닙니다. Namespace, ServiceAccount, IAM role trust·권한, SDK credential 해석과 실제 인가 성공을 확인합니다.
## 혼합 모드 웹훅 운영
AWS가 지원하는 혼합 모드 패턴은 cloud node의 VPC CNI와 Hybrid Node의 Cilium/Calico를 구분합니다. 이 패턴의 webhook은 cloud 배치를 권장합니다. Hybrid에 배치한 webhook에는 routable remote Pod CIDR와 control-plane 접근이 필요하므로 모든 webhook이 특정 위치에서만 또는 어디서나 동작한다고 가정하지 않습니다.
### CoreDNS 배치
AWS는 이 혼합 모드 설계에서 cloud node와 Hybrid Node에 각각 CoreDNS replica 1개 이상을 권장합니다. Desired replica 2개 이상, 적격 용량, selector, toleration과 실제 endpoint를 확인합니다. `maxSkew: 1`만으로 두 도메인이 생기거나 각각 Pod 1개가 보장되지 않으며 cloud node에는 `eks.amazonaws.com/compute-type` label이 없을 수 있습니다.
`minDomains`를 지원하는 클러스터라면 아래 Pod spec 조각처럼 관리자가 명시한 두 도메인을 사용할 수 있습니다. 의도한 DNS 노드만 표시하고 모든 적격 노드에 확인한 `location` 값 `aws` 또는 `onprem`을 부여합니다. 기존 affinity/toleration과 CoreDNS Pod label을 확인하고 add-on이 지원하는 구성 경로로 조정합니다.
```yaml
# Fragment under CoreDNS Deployment.spec.template.spec.
# Label eligible nodes with exactly aws or onprem in this administrative domain.
nodeSelector:
infrastructure.example.com/dns-eligible: "true"
topologySpreadConstraints:
- maxSkew: 1
minDomains: 2
topologyKey: infrastructure.example.com/location
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
k8s-app: kube-dns
```
적격 도메인 2개와 replica 2개 이상일 때 엄격한 분산이 양쪽 배치를 제한합니다. 한 도메인에 용량이 없으면 새 replica가 Pending일 수 있으므로 이는 가용성 절충이며 failover 보장이 아닙니다. DNS Service/EndpointSlice와 로컬·원격 해석을 실제로 시험합니다. Auto Mode를 포함하면 node-local DNS system service와 non-Auto node에 필요한 Deployment를 구분합니다.
### 애드온별 배치 가이드
| 애드온 | 이 설계의 배치 | 확인할 사항 |
| --- | --- | --- |
| AWS Load Balancer Controller | AWS 혼합 모드 예제의 cloud node | Webhook 접근, positive label, routable Hybrid IP target |
| CloudWatch agent/operator | 지원하는 노드의 agent; operator webhook은 cloud 권장 | IAM·agent 상태; Hybrid node-level 지표 제외 |
| cert-manager | Webhook cloud 권장; routable Hybrid 배치 가능 | Control-plane 접근과 remote Pod network |
| Metrics Server | Cloud 또는 도달 가능한 Hybrid Pod endpoint | Control-plane→Pod, Metrics-Server→kubelet 경로 |
| CoreDNS | Cloud와 Hybrid replica 확인 | 적격 도메인·용량·실제 DNS 통신 |
| Cilium/Calico | AWS가 지원하는 혼합 CNI 설계의 Hybrid Node | Cloud node의 VPC CNI 유지 |
## 일반적인 문제 해결
### ImagePullBackOff 진단
Pod 이벤트와 참조하는 Secret 이름·유형을 확인하되 레지스트리 자격 증명을 디코딩해 출력하지 않습니다. Secret은 Pod와 같은 namespace에 있어야 합니다. 레지스트리 호스트 이름, 필요한 repository 권한과 만료 여부를 자격 증명 담당자와 확인합니다. 메타데이터만으로 실제 인증 성공을 입증할 수는 없습니다.
```bash
set -euo pipefail
: "${KUBE_CONTEXT:?Set the approved cluster context}"
: "${NAMESPACE:?Set the workload namespace}" "${POD:?Set the affected Pod}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" describe pod "$POD"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pod "$POD" \
-o jsonpath='{.spec.imagePullSecrets[*].name}{"\n"}'
: "${PULL_SECRET:?Set a referenced imagePullSecret in that namespace}"
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get secret "$PULL_SECRET" \
-o jsonpath='{.type}{"\n"}'
```
실제로 문제가 발생한 호스트·네트워크 경로에서 신뢰할 CA와 호스트 이름 검증을 켜고 TLS를 확인합니다. curl -k를 사용하거나 인증하지 않은 registry 401 응답을 TLS 실패로 해석하지 않습니다. 이 예시는 Bash·OpenSSL·GNU timeout을 사용합니다.
```bash
set -euo pipefail
: "${HARBOR_HOST:?Set the registry DNS name, without scheme or port}"
: "${HARBOR_CA_FILE:?Set the approved CA bundle file}"
timeout 10s openssl s_client -connect "$HARBOR_HOST:443" \
-servername "$HARBOR_HOST" -verify_hostname "$HARBOR_HOST" \
-verify_return_error -CAfile "$HARBOR_CA_FILE"
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-hybrid-nodes/09-bare-metal-os-setup
----------------------------------------
# 베어메탈 서버 OS 설치 및 마이그레이션 가이드
< [이전: 운영 및 유지보수](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/08-operations.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: Hybrid Nodes Gateway](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/10-hybrid-nodes-gateway.md) >
> **검증 기준**: 2026년 9월 13일 AWS Hybrid OS/nodeadm 문서·공개 요금 확인; nodeadm 1.0.20은 검토한 CLI 기준입니다. 현재 지원되는 EKS/OS/CNI 조합을 선택합니다.
> **마지막 업데이트**: 2026년 9월 13일
데이터를 지우는 OS 설치, 이미지 준비, 클러스터 가입, 읽기 전용 검증을 구분합니다. 예제에는 placeholder가 있으므로 호스트별 설치 계획이 필요합니다. 이번 검토에서는 OS 설치, Packer build, VM 생성, AWS/클러스터 작업을 실행하지 않았습니다.
## 개요
### 베어메탈을 선택하는 이유
하이퍼바이저를 제거하면 해당 라이선스와 실행 계층을 없앨 수 있지만 총비용 절감이나 애플리케이션 성능 향상을 보장하지는 않습니다. 가상화 기능, 가용성, 백업, storage, networking, 지원 계약과 이전 작업을 먼저 조사합니다. VM 애플리케이션이 자동으로 컨테이너가 되거나 파일럿 클러스터를 시작했다고 기존 계약 의무가 사라지는 것은 아닙니다.
### OS 인프라 지원 매트릭스
| OS | 베어메탈 | 온프레미스 가상화 | 구성과 지원 범위 |
| --- | --- | --- | --- |
| Ubuntu 20.04/22.04/24.04 | Hybrid에 명시된 OS 계열 | OS/kernel/CNI 조건을 충족한 호스트 | nodeadm/YAML; Ubuntu 수명주기·지원은 별도 확인 |
| RHEL 8/9 | Hybrid에 명시된 OS 계열 | OS/kernel/CNI 조건을 충족한 호스트 | nodeadm/YAML; Red Hat 구독·OS 지원은 별도 |
| AL2023 | **지원되는 배포 경로 아님** | 온프레미스 가상화 guest | nodeadm/YAML; EC2 밖 AL2023 OS에는 AWS Support Plans 미적용 |
| Bottlerocket VMware variant >=1.37.0 | **Hybrid bare metal 미지원** | VMware vSphere, **x86_64** | Bottlerocket settings/bootstrap; govc는 VMware VM 관리 |
AWS는 명시된 Ubuntu/RHEL 계열과의 Hybrid 통합을 지원하며 OS vendor의 지원을 대신하지 않습니다. 구체적 버전, architecture, CNI/kernel 조건도 확인합니다. EC2 밖 AL2023은 VM guest 선택지이며 일반 raw-image 베어메탈 권장이 아닙니다. Bottlerocket VMware용 Kubernetes variant가 1.28부터 있다는 설명은 과거 variant 범위이며 오래된 EKS 버전 사용 권장이 아닙니다. SSM 설치·업그레이드는 오래된 SSM 서명 키 문제를 피하도록 nodeadm >=1.0.19가 필요합니다.
## 비용 비교 분석
### 라이선스/구독 비용 비교
#### VMware vSphere
실제 제품 bundle, licensed core, 최소 수량, 기간과 지원 조건에 맞는 견적을 받습니다. 기존의 소켓당 연간 $4,500–8,500에는 여기서 검증한 출처가 없으므로 현재 vSphere 견적으로 사용할 수 없습니다.
#### OpenShift
실제 Red Hat entitlement, physical/virtual core·socket 산정과 지원 tier를 비교합니다. 기존 노드당 연간 $2,500–5,000과 “premium 지원 포함” 주장은 미검증이며 현재 요금·권리 보장이 아닙니다.
#### EKS Hybrid Nodes
[공식 요금](https://aws.amazon.com/eks/pricing/)은 **노드가 보고한 vCPU-hours**의 월별 구간 요금입니다. 처음 576,000은 $0.020, 다음 576,000은 $0.014, 다음 4,608,000은 $0.010, 다음 5,760,000은 $0.008, 11,520,000 초과분은 $0.006입니다. 모든 사용량에 적용되는 고정 $0.01 요금이 아닙니다.
동일 계정·리전에서 합산하며 AWS Organizations 통합 결제는 동일 리전의 조직 계정 사용량을 합산합니다. Hyperthreading한 bare-metal core 하나가 vCPU 2개로 보고될 수 있습니다. 노드 가입 시 과금이 시작하고 제거 시 종료하므로 workload가 idle이어도 노드 요금이 사라지지 않습니다. AWS는 머신당 32 vCPU를 초과하면 account team과 요금을 상담하도록 안내합니다. 클러스터, provisioned control plane/capability, 네트워크, 로그, storage, OS와 기타 지원 요금은 별도입니다.
### 규모별 연간 비용 비교 (32 vCPU 서버 기준)
다음은 청구서·실측이 아닌 결정적 **계산 모델**입니다. 노드당 보고된 32 vCPU, 매월 730시간, 동일 리전·합산 범위, 다른 Hybrid 사용량 없음, 같은 월을 12번 반복한 연환산을 가정합니다. 마지막 열은 기본 control-plane tier의 표준 지원 클러스터 1개($0.10/시간)를 더합니다. 다른 서비스, 할인·세금, 하드웨어, 전력, OS entitlement, 인력·이전 비용은 제외하며 실제 월 길이와 구간 초기화가 청구액에 영향을 줍니다.
| 노드 | 월 vCPU-hours | 월 노드 요금 | 연환산 노드 요금 | 노드 + 기본 tier 표준 지원 클러스터 1개 연환산 |
| --- | --- | --- | --- | --- |
| 10 | 233,600 | $4,672.00 | $56,064.00 | $56,940.00 |
| 50 | 1,168,000 | $19,744.00 | $236,928.00 | $237,804.00 |
| 100 | 2,336,000 | $31,424.00 | $377,088.00 | $377,964.00 |
보존한 과거 추정치 — 미검증이며 현재 요금 아님
원래 표를 추적 목적으로 보존합니다. Vendor 견적 출처는 확인하지 못했습니다. EKS 열은 폐기한 고정 요금 가정 `32 × $0.01 × 8,760 = $2,803.20/노드/년`을 사용했고 클러스터 요금·구간별 요금을 포함하지 않았습니다. 현재 TCO 비교 근거로 사용하지 않습니다.
| 규모 | VMware vSphere (과거 연간 추정치) | OpenShift (과거 연간 추정치) | EKS Hybrid (폐기한 고정 요금 추정치) |
| --- | --- | --- | --- |
| 10 nodes | ~$45,000–85,000 | ~$25,000–50,000 | ~$28,032 |
| 50 nodes | ~$225,000–425,000 | ~$125,000–250,000 | ~$140,160 |
| 100 nodes | ~$450,000–850,000 | ~$250,000–500,000 | ~$280,320 |
### TCO(총 소유 비용) 고려 사항
여유 용량, 시설·전력, OS/보안 패치, PKI/SSM 운영, storage/백업·복원, 연결성, 모니터링, 인력, 계약 의무와 rollback 용량을 포함합니다. 동등한 가용성·지원 범위로 비교하고 미검증 과거 표에서 절감률을 도출하지 않습니다.
## OS별 베어메탈 설치
### 사전 준비
#### BIOS/UEFI 설정
선택한 installer, firmware, boot mode, NIC/storage driver와 서명된 boot chain을 확인합니다. 검증한 bootloader/kernel/module이 지원하면 Secure Boot를 유지하며 컨테이너 설치의 기본 조건으로 비활성화하지 않습니다. 일반 Linux containerd/runc 컨테이너에는 VT-x/AMD-V 하드웨어 가상화가 필수가 아닙니다. VM 기반 sandbox, QEMU/KVM 이미지 builder 등의 요구사항은 별도입니다.
#### 네트워크 인프라
Legacy PXE는 주로 DHCP/ProxyDHCP·TFTP를 사용하지만 UEFI HTTP/iPXE 설계는 다른 boot transport를 사용할 수 있습니다. `pxelinux.0`은 모든 UEFI의 bootloader가 아닙니다. Firmware에 맞는 loader와 ISO/kernel/initrd 무결성을 확인하고 provisioning network를 workload와 분리합니다. 호스트별 설정을 보호하며 인증 없는 공유 HTTP/TFTP root에 activation code, private key, password를 두지 않습니다.
#### AWS Packer 템플릿
[AWS 예제 디렉터리](https://github.com/aws/eks-hybrid/tree/main/example/packer)의 실제 파일은 `hybrid-nodes-template.pkr.hcl`입니다. 기존 `bare-metal-template.pkr.hcl`은 제공된 파일이 아닙니다. 템플릿의 `CREDENTIAL_PROVIDER`는 `ssm`·`iam`을 받고 HCL이 `iam`을 nodeadm의 `iam-ra`로 바꿉니다. QEMU 출력은 존재하지 않는 `output_format` 변수가 아니라 `PACKER_OUTPUT_FORMAT`을 사용합니다. 승인한 commit의 template·provisioner script에서 builder별 필수 입력과 password/user-data 처리를 검토합니다.
```bash
# From a reviewed checkout of aws/eks-hybrid/example/packer:
export NODEADM_ARCH=amd # This template uses amd/arm, not amd64/arm64.
export CREDENTIAL_PROVIDER=ssm # The template accepts ssm/iam; maps iam to nodeadm iam-ra.
export PACKER_OUTPUT_FORMAT=raw
: "${K8S_VERSION:?Set a currently supported cluster-compatible major.minor}"
: "${ISO_URL:?Set the approved Ubuntu or RHEL installer ISO}"
: "${ISO_CHECKSUM:?Set the verified vendor ISO checksum}"
export K8S_VERSION ISO_URL ISO_CHECKSUM
# Also prepare the selected builder's required inputs using the reviewed template.
packer validate -syntax-only hybrid-nodes-template.pkr.hcl
# A separate build action, not a validation step:
# packer build -only=general-build.qemu.ubuntu24 hybrid-nodes-template.pkr.hcl
```
템플릿에는 AMI/vSphere builder도 있으므로 별도 build 전에 의도한 builder만 선택합니다. Syntax-only 검증은 이미지 생성·실행 검증이 아닙니다. 오래된 Kubernetes 예제, 기본 builder password, 전체 읽기 가능한 credential 파일이나 등록된 machine/SSM identity를 재사용 이미지에 복사하지 않습니다.
수동으로 준비한 호스트에서는 승인된 nodeadm release·architecture와 검토한 checksum을 사용합니다. 다음은 상태 검사가 아니라 **설치 단계**입니다. 승인된 release 검증 경로로 checksum을 확보하고 값을 지어내거나 불일치를 우회하지 않습니다.
```bash
set -euo pipefail
: "${NODEADM_VERSION:?Set the approved release, at least 1.0.19 for SSM}"
: "${NODEADM_SHA256:?Set the approved SHA-256 for this release and architecture}"
: "${NODEADM_ARTIFACT:?Set the local file delivered through the approved artifact channel}"
case "$(uname -m)" in
x86_64) NODEADM_ARCH=amd64 ;;
aarch64|arm64) NODEADM_ARCH=arm64 ;;
*) printf 'Unsupported architecture\n' >&2; exit 2 ;;
esac
[[ "$NODEADM_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit 2
[[ "$NODEADM_SHA256" =~ ^[[:xdigit:]]{64}$ ]] || exit 2
[[ "$(printf '%s\n' 1.0.19 "$NODEADM_VERSION" | sort -V | head -n 1)" == 1.0.19 ]] || exit 2
umask 077
STAGING_DIR=$(mktemp -d)
trap 'rm -f -- "$STAGING_DIR/nodeadm"; rmdir -- "$STAGING_DIR"' EXIT
cp -- "$NODEADM_ARTIFACT" "$STAGING_DIR/nodeadm"
printf '%s %s\n' "$NODEADM_SHA256" "$STAGING_DIR/nodeadm" | sha256sum --check -
chmod 0700 "$STAGING_DIR/nodeadm"
# Check the verified staged binary before replacing the installed executable.
ACTUAL_VERSION=$("$STAGING_DIR/nodeadm" --version)
VERSION_RE="(^|[^0-9.])v?${NODEADM_VERSION//./\\.}([^0-9A-Za-z.+-]|$)"
[[ "$ACTUAL_VERSION" =~ $VERSION_RE ]] || { printf 'Artifact version mismatch\n' >&2; exit 2; }
printf '%s\n' "$ACTUAL_VERSION"
sudo install -m 0755 "$STAGING_DIR/nodeadm" /usr/local/bin/nodeadm
/usr/local/bin/nodeadm --version
```
### Ubuntu LTS (22.04/24.04)
Ubuntu Server의 Subiquity Autoinstall은 YAML을 사용하며 cloud-init으로 전달할 수 있습니다. 다음 템플릿은 storage를 대화형으로 유지하고 정확한 승인 disk serial·SSH key 입력을 요구합니다. 선택한 disk를 partition하면 기존 데이터가 지워집니다. 백업, disk 인벤토리, installer 전달, 접근·복구 경로와 firmware 호환성을 먼저 폐기 가능한 대상에서 확인합니다. 데이터 disk가 있는 호스트에 wildcard·빈 match·“가장 큰 disk” 가정을 사용하지 않습니다.
#### Autoinstall 설정 예시
```yaml
#cloud-config
autoinstall:
version: 1
interactive-sections: [storage]
locale: en_US.UTF-8
keyboard:
layout: us
storage:
layout:
name: lvm
match:
serial: REPLACE_WITH_APPROVED_DISK_SERIAL
identity:
hostname: replace-with-unique-hostname
username: hybrid-admin
password: "!"
ssh:
install-server: true
allow-pw: false
authorized-keys:
- "ssh-ed25519 REPLACE_WITH_APPROVED_PUBLIC_KEY"
packages: [curl, jq]
shutdown: poweroff
```
이 템플릿의 `password: "!"`는 password 인증을 잠급니다. 실제 승인된 SSH 접근과 복구 경로를 확인합니다. 호스트와 installer에 맞는 network/NIC/VLAN/bond 설정은 별도로 제공합니다. 로컬 schema 검증은 disk 선택·boot·login 시험이 아닙니다. nodeadm은 앞의 별도 단계에서 준비하며 installer가 무결성 확인 없는 `latest` binary를 내려받지 않습니다.
#### Ubuntu 24.04 특이사항
[Ubuntu 버그 2065423](https://bugs.launchpad.net/ubuntu/+source/containerd-app/+bug/2065423)은 컨테이너 종료 signal에 대한 AppArmor 거부 문제이며 패키지 수정이 배포되어 있습니다. AWS OS 문서는 여전히 1.7.19+ 수정 맥락을 설명합니다. 실제 배포판 패키지·backport, 로드된 profile, runtime과 denial log를 확인합니다. 모든 stuck Pod의 원인을 이 버그로 단정하거나 오래된 containerd에 머무르도록 권장하지 않습니다.
```bash
# Read-only investigation on the affected host:
containerd --version
dpkg-query -W 'containerd*' 'runc*' 'apparmor*'
sudo journalctl -k --since '30 minutes ago' --no-pager -n 200
if test -f /run/reboot-required.pkgs; then cat /run/reboot-required.pkgs; fi
```
문서화된 해당 패키지/profile 전환에는 수정된 profile을 로드하기 위한 재시작이 필요합니다. 수명주기 절차로 drain·reboot·workload 검증을 계획합니다. 모든 AppArmor 편집에 reboot가 필요하다고 일반화하지 않습니다. `aa-remove-unknown`은 `/etc/apparmor.d`에 없는 로드된 profile을 모두 제거하므로 특정 profile 편집 명령이나 기본 복구 명령이 아닙니다.
### RHEL 9
RHEL은 Kickstart를 사용합니다. 다음은 **Bash가 아닌 설치 템플릿**입니다. 승인한 단일 설치 disk 식별자, password hash, 전체 SSH public key를 대입합니다. `ignoredisk --only-use`와 `clearpart --drives`는 같은 확인된 disk를 지정해야 하며 제한 없는 `clearpart --all`을 사용하지 않습니다. 선택한 installer의 repository/stage2/bootloader/network 조건을 맞추고 해당 버전의 ksvalidator로 검증합니다.
#### Kickstart 설정 예시
```text
# Template: substitute and validate the approved target disk, key and password hash.
lang en_US.UTF-8
keyboard us
timezone UTC --utc
rootpw --lock
user --name=hybrid-admin --groups=wheel --iscrypted --password=REPLACE_WITH_CRYPT_HASH
sshkey --username=hybrid-admin "ssh-ed25519 REPLACE_WITH_APPROVED_PUBLIC_KEY"
network --bootproto=dhcp --device=link --activate
ignoredisk --only-use=REPLACE_WITH_APPROVED_INSTALL_DISK
clearpart --all --initlabel --drives=REPLACE_WITH_APPROVED_INSTALL_DISK
autopart --type=lvm
selinux --enforcing
services --enabled=sshd
%packages
@core
openssh-server
curl
jq
%end
poweroff
```
SELinux enforcing을 유지하고 구체적인 policy/context 요구사항을 진단합니다. nodeadm 문서는 containerd SELinux·Pod security context 설정을 제공하므로 `container_t` 전체를 permissive로 바꾸지 않습니다. OS 패키지와 nodeadm/containerd 설치는 별도 단계입니다.
#### RHEL containerd 설치 주의사항
RHEL에서 nodeadm의 `distro` containerd source는 지원되지 않습니다. 호환되는 Docker 배포 패키지를 nodeadm으로 설치하려면 `docker`, 별도로 설치·관리한다면 `none`을 선택합니다. “RHEL은 항상 docker가 필수”라는 표현은 너무 넓습니다.
```bash
set -euo pipefail
: "${K8S_VERSION:?Set a currently supported cluster-compatible major.minor}"
sudo nodeadm install "$K8S_VERSION" --credential-provider ssm \
--containerd-source docker
# If containerd is separately installed and maintained, choose --containerd-source none.
```
#### 대규모 환경: Satellite/Foreman 통합
Satellite/Foreman으로 검토한 Kickstart template, repository와 provisioning workflow를 관리할 수 있습니다. 호스트별 disk, hardware identity, 구독, artifact 무결성과 실패 복구를 확인합니다. 공유 template 문법이 맞다는 사실은 임의의 전체 fleet을 지워도 된다는 뜻이 아닙니다.
### Amazon Linux 2023
이 절은 베어메탈 설치가 아닌 **가상화 guest 참고 사항**입니다. AWS는 EC2 밖 AL2023에 KVM, VMware, Hyper-V VM 이미지를 제공합니다. 해당 OS 사용에는 AWS Support Plans이 적용되지 않으며 EKS Hybrid 통합 지원과 구분합니다. 적절한 이미지·cloud-init 전달 경로를 선택합니다. 최소 VM identity 조각은 다음과 같습니다.
```yaml
#cloud-config
# VM identity fragment only; it does not install or join a Hybrid Node.
hostname: replace-with-unique-hostname
manage_etc_hosts: true
ssh_pwauth: false
```
관리자 생성, key 삽입이나 node 초기화를 수행하는 예제가 아닙니다. 승인한 접근을 준비하고 별도로 검토한 bootstrap을 사용합니다. AL2023은 `--containerd-source docker`를 지원하지 않으므로 지원되는 distro source를 쓰거나 containerd를 별도로 관리합니다.
### Bottlerocket on VMware (참고)
EKS Hybrid Nodes에는 x86_64용 지원 VMware variant >=1.37.0을 사용합니다. OS/인증은 Bottlerocket settings와 bootstrap container가 구성하며 govc는 TOML parser가 아니라 VMware VM 관리 CLI입니다. 버전별 settings, private user-data·credential은 [전체 bootstrap 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md)를 따릅니다.
## 자격 증명 프로바이더 설정 비교
### nodeadm 기반 설정 (Ubuntu/RHEL/AL2023)
아래는 SSM과 IAM Roles Anywhere의 별도 선택지입니다. 모든 placeholder를 승인한 cluster/region/identity로 교체하고 NodeConfig를 root 소유·0600으로 보호하며 호스트별 private key를 비공개로 유지합니다. 등록된 SSM state나 공유 host key를 golden image에 포함하지 않습니다.
```yaml
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
name: my-cluster
region: ap-northeast-2
hybrid:
ssm:
activationCode: "REPLACE_WITH_PRIVATE_ACTIVATION_CODE"
activationId: "REPLACE_WITH_PRIVATE_ACTIVATION_ID"
```
```yaml
apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
cluster:
name: my-cluster
region: ap-northeast-2
hybrid:
iamRolesAnywhere:
trustAnchorArn: arn:aws:rolesanywhere:ap-northeast-2:111122223333:trust-anchor/REPLACE_WITH_ID
profileArn: arn:aws:rolesanywhere:ap-northeast-2:111122223333:profile/REPLACE_WITH_ID
roleArn: arn:aws:iam::111122223333:role/HybridNodeRole
certificatePath: /etc/eks/pki/node.crt
privateKeyPath: /etc/eks/pki/node.key
```
Nodeadm은 구성을 검증하고 설정된 AWS identity로 필요한 cluster metadata를 조회합니다. 가입 전에 실제 API 연결, EKS access entry, trust·권한과 certificate 경로를 확인합니다. YAML parser 통과는 인가 성공 증거가 아닙니다.
### Bottlerocket 기반 설정 (VMware)
이전의 `[settings.hybrid.ssm]`·`[settings.hybrid.iam-roles-anywhere]`는 유효한 Bottlerocket settings가 아니었습니다. [노드 bootstrap](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md)에 설명한 Kubernetes settings와 Hybrid bootstrap-container 입력을 사용합니다. Base64 user-data는 암호화가 아닌 인코딩이므로 VMware guestinfo·provisioning log를 보호합니다.
### 자격 증명 프로바이더 선택 가이드
| 상황 | 선택 시 검토할 사항 |
| --- | --- |
| 관리되는 PKI 없음 | SSM hybrid activation으로 인증서 관리 부담을 줄일 수 있음; identity·activation 수명주기 필요 |
| 관리되는 PKI 있음 | 호스트별 X.509를 IAM Roles Anywhere에 사용; 발급·신뢰·만료·폐기 운영 필요 |
| Public internet 없음 | 어느 방식이든 필요한 AWS API를 승인된 private/proxy 경로로 접근해야 함; endpoint 지원 확인 |
| 완전 단절/DDIL | EKS Hybrid Nodes의 지원 운영 모델이 아님; IAM Roles Anywhere도 AWS CreateSession 호출 필요 |
| 사용자 지정 node identity | Provider의 naming·수명주기 조건 검토; Node 이름이 호스트 DNS 이름이라는 가정 금지 |
[Private 연결](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/03-airgap-setup.md)을 참고합니다. “Public internet 없음”과 “AWS 연결 없음”은 서로 다른 조건입니다.
## 대규모 프로비저닝 자동화
### PXE 부트 인프라 구성
```text
승인한 호스트/firmware 인벤토리
→ 일치하는 DHCP/HTTP/PXE boot 설정
→ 검증한 signed loader와 installer kernel/initrd
→ 보호된 호스트별 설치 설정
→ 명시한 승인 disk / OS 설치
→ 고유 host identity / 별도의 Hybrid bootstrap
```
위 TFTP/PXELINUX 조합은 legacy BIOS를 설명합니다. UEFI PXE도 TFTP를 사용할 수 있지만 UEFI 호환 loader가 필요하므로 실제 환경에 맞는 안전한 전달 경로를 선택합니다. ISO checksum·서명 신뢰는 DHCP 도달성과 별도입니다.
### Ansible 자동화 플레이북
다음은 **이미 준비된 호스트의 읽기 전용 preflight**입니다. 구성 요소·서비스가 없으면 중단하며 `/usr/bin/kubelet` 존재를 원하는 버전이 설치되었다는 증거로 사용하지 않습니다. Binary 설치, secret template 배포, nodeadm init 반복 실행을 하지 않습니다.
```yaml
# Read-only preflight; does not provision, install, or initialize hosts.
- name: Inspect approved Hybrid hosts
hosts: hybrid_nodes
become: true
gather_facts: false
serial: 1
any_errors_fatal: true
tasks:
- name: Read OS release
ansible.builtin.command:
argv: [cat, /etc/os-release]
changed_when: false
- name: Read architecture
ansible.builtin.command:
argv: [uname, -m]
changed_when: false
- name: Read installed nodeadm version
ansible.builtin.command:
argv: [/usr/local/bin/nodeadm, --version]
changed_when: false
- name: Check each required service
ansible.builtin.command:
argv: [systemctl, is-active, --quiet, "{{ item }}"]
loop: [containerd, kubelet]
changed_when: false
```
Provisioning은 별도의 승인한 단계로 수행합니다: host/disk 인벤토리 확인 → 검증한 OS/artifact 준비 → private 호스트별 구성 공급 → [bootstrap state·identity 절차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md) → 실제 Node UID, 버전, 새 Lease·workload 확인. 기존 호스트는 [수명주기 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/07-node-lifecycle.md)를 따릅니다. 한 호스트씩 조정하고 실패·부분 상태를 보존하며 `creates: /usr/bin/kubelet`로 필요한 변경을 가리지 않습니다.
## 마이그레이션 전략
### VMware → 베어메탈 + EKS Hybrid Nodes
#### Phase 1: 병행 운영 인프라 구축
Workload, VM, 계약, network/security 기능, storage·의존성을 조사합니다. 지원되는 병행 target, private 연결과 rollback 용량을 준비합니다. 전환 중 Bottlerocket on VMware를 유지할 수 있지만 이를 bare-metal OS로 바꾸는 것은 아닙니다.
#### Phase 2: 워크로드 컨테이너화
적합한 workload를 명시적으로 컨테이너화하며 일부 VM은 계속 VM으로 남아야 할 수 있습니다. Writer 이전 전에 데이터, CSI 동작, 권한, backup·restore를 검증합니다. AWS 관리형 DB는 설계 선택지이며 자동 변환이 아닙니다.
#### Phase 3: 네트워크 전환
NSX-T의 routing, overlay, firewall, load balancing과 policy를 각각 조사합니다. Cilium BGP는 경로를 교환하며 모든 NSX-T 기능을 동등하게 대체하지 않습니다. 트래픽 전환 전에 addressing, 반환 경로, ingress/TLS, DNS, policy와 기존 연결을 검증합니다.
#### Phase 4: VMware 폐기
Workload/data 수락, 합의한 rollback 기간, backup 복구와 계약·보존 판단 뒤 폐기합니다. 제거하는 실제 인프라를 확인하고 라이선스 취소·disk wipe를 pipeline의 자동 마무리 작업으로 만들지 않습니다.
### OpenShift → EKS Hybrid Nodes
#### 개념 매핑
| OpenShift | Target 후보 | 해결할 이전 차이 |
| --- | --- | --- |
| Route | Ingress / Gateway API controller | TLS termination/reencrypt/passthrough, weight·annotation·policy는 controller별 확인 |
| SCC | PSS/Pod Security Admission 및 필요한 policy/defaulting | PSS는 SCC UID/SELinux/group 전략·mutation을 재현하지 않음 |
| OLM | 지원되는 OLM, Helm 또는 EKS add-on/operator 공급 | OLM은 Kubernetes에서도 실행 가능; CRD conversion·upgrade·의존성은 자동 이전되지 않음 |
| MachineSet | Host lifecycle/provisioning automation | nodeadm/Ansible은 교체·scale을 담당하는 Machine API controller가 아님 |
| ImageStream | Registry 및 image promotion/trigger automation | ECR은 image 저장소이며 ImageStream import/tag/change trigger를 대체하지 않음 |
| BuildConfig | 검토한 외부 CI/CD | Source, image, credential·build policy 동작 재구성 |
| DeploymentConfig | Deployment 및 필요한 rollout automation | Trigger, hook, strategy·rollback 동작 보존 |
#### 워크로드 마이그레이션 체크리스트
- Route, policy/default, CRD/operator, image trigger, build·rollout hook을 모두 조사합니다.
- 대표 workload로 target 동작과 ServiceAccount/RBAC 범위를 검증합니다.
- 데이터 일관성, snapshot/restore, DNS/TLS, network 통제·관찰성을 확인합니다.
- 원본 폐기 전에 cutover·rollback을 연습합니다.
#### 단계별 마이그레이션
의존성 평가 → 대표 비핵심 workload pilot → 통제된 wave 전환 → 데이터·운영 수락과 복구 증거 보존 → 합의한 rollback 기간 후 폐기 순서로 진행합니다.
## 설치 후 검증
검증 중 패키지를 재설치하거나 가입한 호스트를 초기화하지 않습니다. 아래 읽기는 관측일 뿐이며 프로세스 활성 상태나 과거 Ready만으로 workload를 수락하지 않습니다.
```bash
# Read-only, on the host mapped to the intended Kubernetes Node:
set -euo pipefail
cat /etc/os-release
uname -m -r
/usr/local/bin/nodeadm --version
containerd --version
for service in containerd kubelet; do
systemctl is-active --quiet "$service"
done
```
```bash
# From the approved administrative context; these are reads, not installation:
set -euo pipefail
: "${KUBE_CONTEXT:?Set the approved context}"
: "${NODE:?Set the actual registered Node name from inventory}"
kubectl --context "$KUBE_CONTEXT" get node "$NODE" -o wide
kubectl --context "$KUBE_CONTEXT" -n kube-node-lease get lease "$NODE" -o yaml
```
Node와 실제 호스트의 인벤토리 대응을 사용하고 새 Lease·버전, CNI/DNS/storage와 [bootstrap](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md)·[수명주기](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/07-node-lifecycle.md)의 승인한 workload 검증을 확인합니다. 실패·확인 불가를 통과로 처리하지 않습니다.
## 트러블슈팅
| 문제 | 확인과 제한된 다음 단계 |
| --- | --- |
| Boot 실패 | Firmware mode, signed loader, DHCP/HTTP/TFTP 설계, NIC driver·승인한 image checksum |
| Autoinstall/Kickstart 실패 | Installer log/schema, 대상 disk, repository/stage2·호스트별 설정; 파괴적인 partition 재시도 금지 |
| Ubuntu 종료 문제 | 실제 AppArmor signal-denial 증거, fixed package/backport·필요한 계획 reboot |
| RHEL containerd | 지원되는 docker/none source, 실제 runtime·SELinux policy |
| nodeadm/인증 | 현재 nodeadm, private config, AWS private 연결, identity·trust; 무조건 재등록 금지 |
## 검증 범위와 참고 자료
로컬에서 schema·구성·명령 대체 검사 50개, artifact/host/API 읽기 subprocess 사례 12개, pykickstart 3.78 RHEL9 parser와 Decimal 요금 검증 10개를 확인했습니다. Canonical Autoinstall schema는 추가 root key를 허용하므로 선택한 installer의 검증을 대신하지 않습니다. 실제 OS 설치, disk 삭제, boot/login, Packer image build, Ansible SSH 작업, nodeadm 실행, VM 생성, AWS/Kubernetes 호출은 수행하지 않았습니다. 시험 대상과 실행 파일은 합성 fixture였습니다.
- [AWS Hybrid OS requirements](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-os.html)
- [AWS Hybrid nodeadm](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-nodeadm.html)
- [AWS EKS pricing](https://aws.amazon.com/eks/pricing/)
- [AWS Packer example](https://github.com/aws/eks-hybrid/tree/main/example/packer)
- [Canonical Autoinstall reference](https://canonical-subiquity.readthedocs-hosted.com/en/latest/reference/autoinstall-reference.html)
- [RHEL 9 Kickstart reference](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/automatically_installing_rhel/kickstart-commands-and-options-reference_rhel-installer)
- [Ubuntu bug 2065423](https://bugs.launchpad.net/ubuntu/+source/containerd-app/+bug/2065423)
- [aa-remove-unknown manual](https://manpages.ubuntu.com/manpages/noble/man8/aa-remove-unknown.8.html)
- [AL2023 outside EC2](https://docs.aws.amazon.com/linux/al2023/ug/outside-ec2.html)
- [Operator Lifecycle Manager](https://olm.operatorframework.io/docs/)
- [OpenShift SCC API](https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html/security_apis/securitycontextconstraints-security-openshift-io-v1)
---
< [이전: 운영 및 유지보수](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/08-operations.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) | [다음: Hybrid Nodes Gateway](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/10-hybrid-nodes-gateway.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-hybrid-nodes/10-hybrid-nodes-gateway
----------------------------------------
# EKS Hybrid Nodes Gateway
< [이전: 베어메탈 서버 OS 설치](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/09-bare-metal-os-setup.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) >
> **검토 기준**: Gateway/chart 1.0.2. EKS가 지원하는 Kubernetes 버전과 Gateway 전제 조건을 충족하는 AWS 유지 관리 Cilium 릴리스를 사용합니다.
> **마지막 업데이트**: 2026년 9월 13일
이 문서에서는 EKS Hybrid Nodes Gateway의 아키텍처, 설치, 구성, 운영 방법을 다룹니다. Hybrid Nodes Gateway는 EKS 클러스터 VPC의 Pod와 온프레미스 Hybrid Node의 Pod 간 네트워크 연결을 VXLAN 터널로 자동화하는 오픈소스 솔루션입니다.
---
## 개요 및 학습 목표
### EKS Hybrid Nodes Gateway란?
EKS Hybrid Nodes Gateway는 **2026년 4월 21일 정식 출시(GA)** 된 오픈소스 프로젝트로, EKS 클러스터의 VPC 네트워크와 온프레미스 Hybrid Nodes의 Kubernetes Pod 네트워크 간 라우팅 가능한 연결을 자동으로 구성합니다.
핵심 원리는 간단합니다: EC2 인스턴스에서 실행되는 게이트웨이 Pod가 VXLAN 터널을 통해 온프레미스 Cilium 노드와 직접 연결되고, VPC 라우트 테이블을 자동으로 업데이트하여 양방향 Pod-to-Pod 통신을 가능하게 합니다.
**GitHub**: [github.com/aws/eks-hybrid-nodes-gateway](https://github.com/aws/eks-hybrid-nodes-gateway)
### 학습 목표
이 문서를 완료하면 다음을 이해하고 수행할 수 있습니다:
1. Hybrid Nodes Gateway의 아키텍처와 VXLAN 터널링 메커니즘 이해
2. Cilium VTEP(Virtual Tunnel Endpoint)와 CiliumVTEPConfig CRD 구성
3. Helm 차트를 사용한 게이트웨이 설치 및 구성
4. IAM 역할 및 보안 그룹 설정
5. VPC Pod에서 Hybrid Pod로, Hybrid Pod에서 VPC Pod로의 트래픽 흐름 이해
6. 고가용성 구성 및 페일오버 메커니즘 운영
7. 모니터링, 트러블슈팅, 업그레이드 수행
8. 기존 수동 라우팅 방식에서 게이트웨이 방식으로 마이그레이션
### 왜 Hybrid Nodes Gateway가 필요한가?
기존 EKS Hybrid Nodes 환경에서 VPC Pod와 온프레미스 Pod 간 직접 통신을 위해서는 다음과 같은 수동 작업이 필요했습니다:
| 과제 | 기존 수동 방식 | Gateway 방식 |
|------|---------------|-------------|
| Pod CIDR 라우팅 | VPN/Direct Connect + 수동 정적 라우트 관리 | VXLAN 터널로 자동화 |
| VPC 라우트 테이블 | 수동으로 라우트 추가/삭제 관리 | 게이트웨이가 자동 프로그래밍 |
| 노드 추가/삭제 시 | BGP 또는 수동 라우트 업데이트 필요 | 자동 감지 및 업데이트 |
| 웹훅 연결 | 실제 route·remote Pod·TLS·보안 설정 필요 | 같은 전제 조건을 검증한 Gateway 경로 |
| 비용 | Cluster·연결·인프라·운영 비용 | Gateway 소프트웨어 요금 없음. EC2·해당 Auto Mode·전송·storage 등은 별도 |
| 복잡도 | BGP 구성, 방화벽 규칙, NAT 등 | Helm 차트 하나로 설치 |
> **핵심 가치**: Hybrid Nodes Gateway는 추가 요금 없이 사용 가능한 오픈소스 프로젝트입니다. EC2·해당 Auto Mode 관리 요금·storage·데이터 전송·private 연결·관측성 비용은 별도입니다.
---
## 아키텍처 심층 분석
### 전체 아키텍처 개요

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-10-hybrid-nodes-gateway-0.html)
### 핵심 구성 요소 상세
#### 1. EC2 게이트웨이 노드
게이트웨이는 EC2 인스턴스에서 실행되는 Kubernetes Pod입니다. 이 인스턴스는 다음과 같은 특별한 요구 사항을 갖습니다:
- **소스/대상 확인 비활성화**: EC2 인스턴스의 소스/대상 확인(source/destination check)을 비활성화해야 합니다. 이는 인스턴스가 자신이 소스나 대상이 아닌 트래픽을 전달하는 라우터 역할을 하기 때문입니다.
- **VPC 네트워킹**: 관리형/자체 관리 노드의 AWS VPC CNI와 Auto Mode의 내장 네트워킹을 구분합니다. Gateway는 `hostNetwork: true`로 노드 네트워크를 사용합니다.
- **프라이빗 연결**: Direct Connect 또는 VPN을 통해 온프레미스 네트워크와 연결됩니다.
```yaml
# 기존 적격 EC2 노드의 metadata 조각; 독립 실행 Node manifest가 아닙니다.
metadata:
labels:
hybrid-gateway-node: "true"
```
#### 2. VXLAN 터널 인터페이스 (hybrid_vxlan0)
게이트웨이 Pod는 `hybrid_vxlan0`이라는 VXLAN 인터페이스를 생성합니다:
| 속성 | 값 | 설명 |
|------|-----|------|
| 인터페이스 이름 | `hybrid_vxlan0` | 게이트웨이 측 VXLAN 인터페이스 |
| VNI (VXLAN Network Identifier) | 2 | Gateway 기본값. CiliumVTEPConfig의 설정 필드는 아님 |
| UDP 포트 | 8472 | VXLAN 캡슐화를 위한 UDP 포트 |
| MTU | 실제 인터페이스와 전체 경로에서 확인 | IPv4 VXLAN 오버헤드와 DX/VPN 경로의 가장 작은 MTU를 함께 고려 |
```bash
# ip 도구가 있는 승인된 노드 진단 환경에서 읽기 전용으로 확인
ip link show hybrid_vxlan0
ip addr show hybrid_vxlan0
```
Gateway 1.0.2는 VXLAN 인터페이스에 IP 주소를 할당하지 않습니다. 외부 터널 endpoint는 노드의 private IP이며 인터페이스 MAC은 해당 IP에서 유도합니다. 기본 Gateway 이미지에 `ip`·`bridge` 도구가 있다고 가정하지 않습니다. 이전 문서의 MTU 8950·`inet 10.0.1.5/32` 출력은 실행 증거가 아니며, 특히 IP 할당 부분은 현재 구현과 다릅니다.
#### 3. FDB 엔트리, ARP 엔트리, 라우트 프로그래밍
각 replica의 node reconciler는 `eks.amazonaws.com/compute-type: hybrid` label이 있는 `CiliumNode`를 감시하며, node internal IP와 할당된 Pod CIDR을 사용해 로컬 엔트리를 관리합니다:
**FDB (Forwarding Database) 엔트리**: VXLAN 터널의 원격 엔드포인트를 정의합니다.
```bash
# FDB 엔트리 확인 - 노드 IPv4에서 유도한 MAC과 node internal IP 매핑
bridge fdb show dev hybrid_vxlan0
# 출력 예시:
# 02:00:c0:a8:0a:65 dst 192.168.10.101 self permanent (합성 예시)
# 02:00:c0:a8:0a:66 dst 192.168.10.102 self permanent (합성 예시)
```
**ARP 엔트리**: VXLAN 터널 내에서 IP-to-MAC 매핑을 제공합니다.
```bash
# ARP 엔트리 확인
ip neigh show dev hybrid_vxlan0
# 출력 예시:
# 192.168.10.101 lladdr 02:00:c0:a8:0a:65 PERMANENT (합성 예시)
# 192.168.10.102 lladdr 02:00:c0:a8:0a:66 PERMANENT (합성 예시)
```
**라우트 엔트리**: Hybrid Pod CIDR에 대한 라우팅 경로를 정의합니다.
```bash
# 라우트 엔트리 확인
ip route show dev hybrid_vxlan0
# 출력 예시:
# 10.85.1.0/24 via 192.168.10.101 dev hybrid_vxlan0 onlink (합성 예시)
# 10.85.2.0/24 via 192.168.10.102 dev hybrid_vxlan0 onlink (합성 예시)
```
이 세 가지 엔트리가 함께 작동하여 다음과 같은 패킷 처리 파이프라인을 구성합니다:
```text
VPC Pod → 패킷 도착 (dst: 10.85.1.5)
→ 라우트 조회: 10.85.1.0/24 via 192.168.10.101 dev hybrid_vxlan0 onlink
→ ARP 조회: 192.168.10.101 → MAC 02:00:c0:a8:0a:65
→ FDB 조회: 02:00:c0:a8:0a:65 → 192.168.10.101 (Hybrid Node 1 IP)
→ VXLAN 캡슐화 (VNI 2, UDP 8472)
→ 전송: 192.168.10.101:8472
```
#### 4. CiliumVTEPConfig: 게이트웨이를 원격 VTEP로 등록
Cilium 측에서는 `CiliumVTEPConfig` CRD를 사용하여 EC2 게이트웨이를 원격 VTEP(Virtual Tunnel Endpoint)로 등록합니다. 이를 통해 Hybrid Node의 Cilium 에이전트가 VPC Pod로 향하는 트래픽을 VXLAN 터널을 통해 게이트웨이로 전달합니다.
```yaml
apiVersion: cilium.io/v2
kind: CiliumVTEPConfig
metadata:
name: hybrid-gateway
spec:
endpoints:
- name: vpc-gateway
tunnelEndpoint: "10.0.1.5" # 실제 leader node IP
cidr: "10.0.0.0/16" # endpoint 하나당 VPC prefix 하나
mac: "82:36:6c:89:e6:ad" # 예시값. 실제 leader VXLAN MAC을 확인
```
> **자동 관리**: Hybrid Nodes Gateway가 이 CRD를 자동으로 생성하고 업데이트합니다. 수동으로 CiliumVTEPConfig를 만들 필요는 없지만, 구조를 이해하는 것은 트러블슈팅에 중요합니다.
#### 5. Lease 기반 리더 선출
게이트웨이는 **Deployment**로 배포되며, 기본적으로 **2개의 레플리카**로 구성됩니다. Kubernetes Lease 오브젝트를 사용한 리더 선출 메커니즘을 통해 한 번에 하나의 Pod만 활성(리더) 상태로 동작합니다.
```bash
# Lease 오브젝트 확인
kubectl get lease hybrid-gateway-leader -n eks-hybrid-nodes-gateway
# 출력 예시:
# NAME HOLDER AGE
# hybrid-gateway-leader gateway-node-hostname_example-uuid 5d
```
holderIdentity가 Pod 이름과 같다고 가정하지 않습니다. 실제 node/Pod 대응을 조회한 뒤 운영 명령을 결정합니다.
리더 Pod의 역할:
- VXLAN 터널 관리 (FDB, ARP, 라우트 프로그래밍)
- VPC 라우트 테이블 업데이트
- CiliumVTEPConfig CRD 관리
- CiliumNode 감시와 로컬 터널 엔트리 갱신
팔로워 Pod의 역할:
- 리더와 마찬가지로 VXLAN·FDB·neighbor·로컬 라우트를 계속 유지
- 인계 후 AWS 라우트와 CiliumVTEPConfig 갱신 필요. 무중단 인계 보장은 아님
- Lease 갱신 모니터링

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-10-hybrid-nodes-gateway-1.html)
Lease 관련 주요 파라미터:
| 파라미터 | 기본값 | 설명 |
|---------|--------|------|
| `--leader-election-lease-duration` | 3s | Lease 유효 기간 |
| `--leader-election-renew-deadline` | 2s | 리더 갱신 deadline |
| `--leader-election-retry-period` | 1s | Lease 획득 재시도 간격 |
표는 binary flag입니다. chart 1.0.2는 `leaderElection` values 객체를 지원하지 않으므로 해당 YAML을 추가하는 것만으로 값이 바뀌지 않습니다.
#### 6. VPC 라우트 테이블 자동 관리
게이트웨이의 리더 Pod는 VPC 라우트 테이블을 자동으로 관리합니다:
```
VPC 라우트 테이블 (rtb-0abc123456789def0):
┌────────────────────┬────────────────────────────────┐
│ Destination │ Target │
├────────────────────┼────────────────────────────────┤
│ 10.0.0.0/16 │ local │
│ 10.85.0.0/16 │ eni-0abc... (게이트웨이 ENI) │
│ 0.0.0.0/0 │ igw-0xyz... │
└────────────────────┴────────────────────────────────┘
```
게이트웨이는 `routeTableIDs` 값에 지정된 모든 라우트 테이블에 대해:
1. Hybrid Pod CIDR(`podCIDRs`)에 대한 라우트를 게이트웨이 EC2 인스턴스의 ENI로 설정
2. 리더 변경 시 새 리더의 ENI로 라우트 업데이트
3. 같은 CIDR의 기존 라우트가 다른 대상을 가리키면 `ReplaceRoute`로 변경. 설치 전 라우트 소유자와 전환·복구 절차 확인
리더 setup은 AWS 라우트를 먼저 변경한 뒤 `CiliumVTEPConfig`를 갱신합니다. AWS의 aggregate `podCIDRs` 라우트와 각 replica의 CiliumNode별 로컬 터널 엔트리는 별개입니다. Runtime은 `DescribeRouteTables`·`DescribeInstances`·`CreateRoute`·`ReplaceRoute`를 사용하며 `DeleteRoute`는 호출하지 않습니다. **Helm 제거 시 AWS 라우트는 자동 정리되지 않습니다.**
[1.0.2 구현](https://github.com/aws/eks-hybrid-nodes-gateway/tree/v1.0.2/internal)과 [AWS 운영 문서](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-gateway-operations.html)로 확인했습니다. API 조회 성공·readiness·leader metric만으로 실제 forwarding 성공을 판단하지 않습니다.
---
## 사전 요구 사항
### 클러스터 요구 사항
| 요구 사항 | 최소 버전 / 조건 | 비고 |
|-----------|-----------------|------|
| EKS 클러스터 | EKS와 선택한 add-on이 지원하는 버전 | IPv4, API/API_AND_CONFIG_MAP 인증, 겹치지 않는 주소 범위 |
| Hybrid Nodes | 1개 이상 등록됨 | Cilium CNI 실행 중 |
| Cilium CNI | [CNI 구성](#cni-구성)의 AWS branch 최소 버전 충족 | VTEP 활성화·L7 proxy 비활성화 |
| Cloud networking | 관리형/자체 관리 노드는 지원되는 AWS VPC CNI, Auto Mode는 내장 네트워킹 | aws-node ClusterIP 경로는 Hybrid CIDR SNAT 제외 |
| kubectl | 대상 API server와 지원되는 version skew | 임의의 최소 버전만으로 호환성을 판단하지 않음 |
| Helm | 3.12+ | 게이트웨이 설치에 사용 |
### 네트워크 요구 사항
#### 프라이빗 연결
온프레미스 node IP와 AWS VPC 사이의 승인된 private 연결이 필요합니다. 아래 이전 수치·비용 등급은 미검증 예시이며 현재 상품 최대치나 지연 보장이 아닙니다. Tunnel 유형·라우팅·packet mix·중복 연결 설계에 따라 달라집니다. [Hybrid cluster 요구 사항](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-cluster-create.html)에 따라 public-only 또는 private-only endpoint를 선택하며 IPv4와 API/API_AND_CONFIG_MAP 인증을 사용합니다:
| 연결 방식 | 대역폭 | 지연 시간 | 비용 | 적합한 환경 |
|-----------|--------|----------|------|------------|
| AWS Direct Connect | 1-100 Gbps | < 5ms | 높음 | 프로덕션 대규모 환경 |
| Site-to-Site VPN | ~1.25 Gbps/터널 | 가변적 | 낮음 | 개발/소규모 환경 |
| Direct Connect + VPN | 1-100 Gbps | < 5ms | 높음 | 최고 보안 요구 환경 |
#### EC2 인스턴스
다음 기존 instance/node-count 표는 실측 없는 sizing 예시입니다. 실제 network baseline/burst·PPS·CPU·memory·장애 여유를 검토합니다:
```
권장 인스턴스 타입:
┌──────────────┬──────────┬──────────┬──────────────────────────┐
│ 인스턴스 타입 │ vCPU │ 메모리 │ 적합한 환경 │
├──────────────┼──────────┼──────────┼──────────────────────────┤
│ c5.large │ 2 │ 4 GiB │ 개발/테스트 (< 10 노드) │
│ c5.xlarge │ 4 │ 8 GiB │ 소규모 프로덕션 (< 50) │
│ c5.2xlarge │ 8 │ 16 GiB │ 대규모 프로덕션 (< 200) │
│ c5n.xlarge │ 4 │ 10.5 GiB │ 고대역폭 필요 시 │
└──────────────┴──────────┴──────────┴──────────────────────────┘
```
> **중요**: EC2 인스턴스의 **소스/대상 확인(Source/Destination Check)**을 반드시 비활성화해야 합니다. 게이트웨이가 라우터 역할을 하므로 자신이 소스/대상이 아닌 패킷도 전달해야 하기 때문입니다.
Auto Mode는 NodeClass의 `advancedNetworking.sourceDestCheck: DisabledPrimaryENI`로 준비합니다. 관리형/자체 관리 노드는 provisioning 소유자가 정확한 primary ENI에 필요한 변경을 수행합니다. 다음 명령은 현재 속성만 읽습니다.
```bash
: "${AWS_REGION:?Set the reviewed Region}"
: "${GATEWAY_PRIMARY_ENI_ID:?Set the verified gateway primary ENI}"
aws ec2 describe-network-interface-attribute --region "$AWS_REGION" \
--network-interface-id "$GATEWAY_PRIMARY_ENI_ID" --attribute sourceDestCheck
```
#### 보안 그룹
| 경로 | 확인 범위 |
|---|---|
| Gateway private node IP ↔ Hybrid node IP | 외부 VXLAN UDP8472 양방향 |
| Cloud workload ↔ Hybrid Pod | 실제 내부 application protocol/port와 반환 트래픽 |
| Node → Kubernetes API·AWS API/registry·DNS | 실제 endpoint/resolver의 TCP443와 DNS 경로 |
| Control plane → kubelet | 대상 node TCP10250. Node의 API443 outbound와 구분 |
| Prometheus → Gateway | 승인된 scraper의 TCP10080만 허용 |
UDP8472 하나가 모든 application/control-plane 연결을 보장하지 않습니다. SG·stateless NACL·온프레미스 firewall을 각각 검토하고, 전체 VPC all-protocol ingress로 요구 사항 확인을 대신하지 않습니다. 기존 node/network IaC에서 승인된 flow matrix를 관리합니다. SG가 생성됐다는 사실은 rule·연결 성공의 증거가 아닙니다.
#### 온프레미스 방화벽 규칙
Private underlay로 도달 가능한 Gateway **private node IP**를 사용하며 Elastic IP를 사용하지 않습니다. 모든 leader/standby 후보와 replacement node를 반영합니다. API/kubelet·DNS·application 경로는 별도로 확인하고 node 교체 시 firewall inventory를 갱신합니다. Auto Mode와 관리형/자체 관리 node의 네트워킹·source/destination check 설정 소유권도 구분합니다.
---
## IAM 구성
### 필요 권한
Gateway workload role, EC2 node role, 운영자의 provisioning/cleanup 권한을 분리합니다. [AWS 시작 가이드](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-gateway-getting-started.html)는 EKS Pod Identity를 권장합니다. 적격 관리형/자체 관리 노드에는 agent가 필요하며 Auto Mode는 Pod Identity 지원을 제공합니다. 기존 설치 위에 add-on을 무조건 생성하지 않습니다.
Gateway runtime은 `ec2:DescribeRouteTables`·`ec2:DescribeInstances`·`ec2:CreateRoute`·`ec2:ReplaceRoute`를 사용합니다. Describe에는 `Resource: "*"`가 필요하므로 Region을 제한하고, route 쓰기는 정확한 route-table ARN과 VPC condition으로 제한합니다. 종료된 라우트 삭제는 운영자 작업이며 runtime 권한이 아닙니다.
아래 정책은 account·Region·VPC·route-table ID를 검토한 inventory로 바꾼 후 `gateway-permissions.json`으로 저장합니다. 식별자는 예시이며 이번 감사에서 생성한 리소스가 아닙니다. 이 정책은 지정 table 안에서 수정할 destination CIDR까지 제한하지 않습니다. Route table을 보안 경계로 취급하고 Gateway values와 ServiceAccount를 사용할 권한도 통제합니다.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadGatewayRoutingMetadata",
"Effect": "Allow",
"Action": [
"ec2:DescribeRouteTables",
"ec2:DescribeInstances"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "ap-northeast-2"
}
}
},
{
"Sid": "ManageOnlyOwnedRouteTables",
"Effect": "Allow",
"Action": [
"ec2:CreateRoute",
"ec2:ReplaceRoute"
],
"Resource": [
"arn:aws:ec2:ap-northeast-2:111122223333:route-table/rtb-0abc123456789def0",
"arn:aws:ec2:ap-northeast-2:111122223333:route-table/rtb-0def456789abc1230"
],
"Condition": {
"StringEquals": {
"ec2:Vpc": "arn:aws:ec2:ap-northeast-2:111122223333:vpc/vpc-0123456789abcdef0"
}
}
}
]
}
```
Route-table ARN과 `ec2:Vpc` 조건은 [EC2 공식 정책 예제](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ExamplePolicies_EC2.html)를 따릅니다. Describe 성공이 CreateRoute/ReplaceRoute 권한을 입증하지 않습니다. 전환 전 승인된 환경에서 권한과 route 소유권을 확인합니다.
### IAM 역할 생성 (AWS CLI)
Pod Identity trust policy를 `gateway-trust.json`으로 저장하며 정확한 cluster ARN으로 바꿉니다. Cluster·namespace·ServiceAccount 조건을 사용하므로 session tag를 활성화 상태로 유지합니다. 해당 Pod/ServiceAccount 생성과 Pod Identity association 관리 권한도 함께 제한합니다.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "pods.eks.amazonaws.com"
},
"Action": [
"sts:AssumeRole",
"sts:TagSession"
],
"Condition": {
"StringEquals": {
"aws:RequestTag/eks-cluster-arn": "arn:aws:eks:ap-northeast-2:111122223333:cluster/hybrid-production",
"aws:RequestTag/kubernetes-namespace": "eks-hybrid-nodes-gateway",
"aws:RequestTag/kubernetes-service-account": "eks-hybrid-nodes-gateway"
}
}
}
]
}
```
[Pod Identity trust policy](https://docs.aws.amazon.com/eks/latest/userguide/pod-id-role.html)와 [session tag](https://docs.aws.amazon.com/eks/latest/userguide/pod-id-abac.html)의 조건을 사용합니다. IAM 소유자가 검토한 trust/permission JSON으로 role을 준비한 뒤 연결합니다. ServiceAccount 이름은 렌더링한 chart와 일치해야 하며 release/name override에 따라 달라질 수 있습니다. Chart 1.0.2의 `serviceAccount.annotations` 값은 무시되므로 IAM 연결을 대신하지 못합니다.
```bash
: "${AWS_REGION:?Set the reviewed Region}"
: "${CLUSTER_NAME:?Set the reviewed cluster}"
: "${GATEWAY_ROLE_ARN:?Set the prepared, scoped Pod Identity role ARN}"
# Inspect existing associations; do not create a duplicate or replace another owner.
aws eks list-pod-identity-associations --region "$AWS_REGION" \
--cluster-name "$CLUSTER_NAME" --namespace eks-hybrid-nodes-gateway \
--service-account eks-hybrid-nodes-gateway
# Run only for the reviewed new association; keep session tags enabled:
aws eks create-pod-identity-association --region "$AWS_REGION" \
--cluster-name "$CLUSTER_NAME" --namespace eks-hybrid-nodes-gateway \
--service-account eks-hybrid-nodes-gateway --role-arn "$GATEWAY_ROLE_ARN" \
--no-disable-session-tags
```
### Terraform을 사용한 IAM 구성
다음은 기존 provider/cluster stack에 넣는 조각입니다. 변수와 JSON 파일을 실제 inventory로 준비하고, 리소스마다 Terraform 또는 CLI 중 한 소유자를 정합니다. 완전한 cluster 배포 예제가 아닙니다.
```hcl
# Fragment in the existing reviewed AWS provider/cluster stack.
# Declare and validate these variables; do not create duplicate CLI-managed resources.
resource "aws_iam_role" "gateway" {
name = var.gateway_role_name
assume_role_policy = file("${path.module}/gateway-trust.json")
}
resource "aws_iam_role_policy" "gateway_routes" {
name = "GatewayOwnedRoutes"
role = aws_iam_role.gateway.id
policy = file("${path.module}/gateway-permissions.json")
}
resource "aws_eks_pod_identity_association" "gateway" {
cluster_name = var.cluster_name
namespace = "eks-hybrid-nodes-gateway"
service_account = "eks-hybrid-nodes-gateway"
role_arn = aws_iam_role.gateway.arn
}
```
IRSA도 실제 OIDC trust policy와 렌더링된 ServiceAccount annotation을 관리하는 patch/overlay를 갖추면 사용할 수 있습니다. Helm이 무시하는 값만으로 IRSA가 설정되지는 않습니다. 모든 node workload에 route 쓰기 권한을 넓게 부여하지 않습니다. Binary의 node 식별용 EC2 metadata 조회와 SDK 자격 증명 체인은 별개이므로 필요한 node identity 입력을 제공·검증하지 않고 metadata를 무조건 차단하지 않습니다.
Auto Mode는 `NodeClass.spec.advancedNetworking.sourceDestCheck: DisabledPrimaryENI`로 forwarding을 준비합니다. 관리형/자체 관리 node bootstrap은 별도로 제한한 node/operator 권한으로 대상 primary ENI의 source/destination check를 비활성화해야 합니다. `ModifyNetworkInterfaceAttribute`와 cluster/role/add-on 생성 권한은 위 Gateway workload 정책에 포함하지 않습니다.
---
## 설치 및 구성
### Helm 차트 설치
#### 기본 설치
적격 cloud EC2 노드를 먼저 준비합니다. 관리형/자체 관리 노드는 `autoMode.enabled=false`를 사용합니다. Auto Mode는 NodeClass/NodePool을 준비하고 `autoMode.enabled=true`를 사용합니다. IAM·source/destination check·CNI·security group과 반환 경로를 확인한 뒤 설치합니다. Leader가 기존 CIDR 라우트를 즉시 바꿀 수 있으므로 소유자·전환·복구 계획을 먼저 정합니다. 이 장의 검증은 로컬 구성 검증이며 실제 설치 성공 기록이 아닙니다.
AWS account·cluster VPC·remote Pod CIDR과 해당 subnet/control-plane route table을 확인합니다. VPC의 모든 table을 조회했다고 모두 변경해도 되는 것은 아닙니다. `remoteNetworkConfig`는 `cluster` 바로 아래에 있으며 `kubernetesNetworkConfig`의 하위 필드가 아닙니다.
```bash
: "${AWS_REGION:?Set the reviewed AWS Region}"
: "${CLUSTER_NAME:?Set the reviewed EKS cluster}"
: "${VPC_ID:?Set the cluster VPC ID}"
aws sts get-caller-identity --query Account --output text
aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--query 'cluster.{VpcId:resourcesVpcConfig.vpcId,RemotePodCIDRs:remoteNetworkConfig.remotePodNetworks[].cidrs[]}' \
--output json
aws ec2 describe-vpcs --region "$AWS_REGION" --vpc-ids "$VPC_ID" \
--query 'Vpcs[].CidrBlockAssociationSet[].{CIDR:CidrBlock,State:CidrBlockState.State}' \
--output json
aws ec2 describe-route-tables --region "$AWS_REGION" \
--filters "Name=vpc-id,Values=$VPC_ID" \
--query 'RouteTables[].{ID:RouteTableId,Associations:Associations,Routes:Routes}' \
--output json
```
#### 전체 values.yaml 예제
배포된 1.0.2 chart의 `podCIDRs`·`routeTableIDs`는 YAML 배열이 아닌 **CSV 문자열**입니다. Helm `--set`의 comma/list 해석을 피하도록 values 파일을 사용합니다.
```yaml
# values.yaml: replace CIDRs/table IDs from the reviewed network inventory.
vpcCIDR: "10.0.0.0/16"
podCIDRs: "10.85.0.0/16"
routeTableIDs: "rtb-0abc123456789def0,rtb-0def456789abc1230"
replicas: 2
nodeLabel: hybrid-gateway-node
autoMode:
enabled: false # MNG/self-managed. Set true only for prepared Auto Mode nodes.
```
그 밖에 `image.repository`·`image.tag`·`image.pullPolicy`와 이름 helper를 지원합니다. `replicaCount`·`nodeSelector`·`resources`·`affinity`·`topologySpreadConstraints`·`serviceAccount.annotations`·`leaderElection`·`logLevel`·`metrics`·`extraEnv`·사용자 volume values는 1.0.2 템플릿에 연결되어 있지 않습니다. YAML을 받았다는 사실이 설정 적용을 의미하지 않습니다. Workload identity는 별도로 연결합니다. 다른 설정에 관리하는 post-renderer가 필요하면 최종 Deployment와 업그레이드 동작을 검토합니다.
두 모드 모두 hostNetwork·NET_ADMIN·필수 host anti-affinity·선호 AZ anti-affinity를 사용하며 Service·ServiceMonitor·PDB는 만들지 않습니다. Auto Mode 전략은 maxSurge=1/maxUnavailable=0, 다른 노드는 0/1입니다. 어느 전략도 리더를 인식한 교체나 무중단 forwarding을 보장하지 않습니다. Auto Mode surge에는 host anti-affinity를 만족할 추가 적격 노드가 필요합니다.
#### 설치 및 검증
```bash
: "${KUBE_CONTEXT:?Set the reviewed Kubernetes context}"
# Local rendering first; it does not prove API admission, IAM or network readiness.
helm template eks-hybrid-nodes-gateway \
oci://public.ecr.aws/eks/eks-hybrid-nodes-gateway \
--version 1.0.2 --namespace eks-hybrid-nodes-gateway \
--values values.yaml > gateway-rendered.yaml
# Creates/changes cluster resources and can redirect existing VPC routes:
helm upgrade --install eks-hybrid-nodes-gateway \
oci://public.ecr.aws/eks/eks-hybrid-nodes-gateway \
--version 1.0.2 --namespace eks-hybrid-nodes-gateway --create-namespace \
--kube-context "$KUBE_CONTEXT" --values values.yaml
```
실제 Lease holder·Gateway Pod의 node/IP·VTEP endpoint/MAC·route ENI가 같은 리더를 나타내는지 대조합니다. holderIdentity는 node hostname과 UUID일 수 있으므로 `kubectl logs` 또는 Pod 삭제 명령에 그대로 전달하지 않습니다.
```bash
: "${KUBE_CONTEXT:?Set the reviewed Kubernetes context}"
: "${AWS_REGION:?Set the reviewed AWS Region}"
: "${ROUTE_TABLE_ID:?Set one reviewed route table ID}"
kubectl --context "$KUBE_CONTEXT" -n eks-hybrid-nodes-gateway \
rollout status deployment/eks-hybrid-nodes-gateway --timeout=180s
kubectl --context "$KUBE_CONTEXT" -n eks-hybrid-nodes-gateway get pods -o wide
kubectl --context "$KUBE_CONTEXT" -n eks-hybrid-nodes-gateway get lease hybrid-gateway-leader -o yaml
kubectl --context "$KUBE_CONTEXT" get ciliumvtepconfig hybrid-gateway -o yaml
aws ec2 describe-route-tables --region "$AWS_REGION" \
--route-table-ids "$ROUTE_TABLE_ID" --query 'RouteTables[].Routes' --output json
kubectl --context "$KUBE_CONTEXT" -n eks-hybrid-nodes-gateway logs \
-l app.kubernetes.io/name=eks-hybrid-nodes-gateway --all-containers=true --tail=50
```
Running·readiness·leader gauge만으로 forwarding 성공을 판단하지 않습니다. 명시적으로 선택한 cloud/Hybrid workload에서 양방향 Pod IP, Hybrid endpoint를 가진 ClusterIP, 실제 webhook과 반환 트래픽을 시험합니다. 테스트 서버는 실제 해당 포트를 listen해야 합니다. 로그는 비공개로 보관하고 공유 전 workload 민감 자료를 제거합니다.
### 노드 레이블링
가능하면 서로 다른 AZ의 적격 노드 두 개를 선택합니다. Instance type/AZ처럼 provider가 관리하는 label을 임의로 바꾸지 않습니다. 아래 label은 forwarding·IAM·network 전제 조건을 확인한 노드에만 적용합니다. 실제 순서는 노드 준비·label 적용 후 Helm 설치입니다.
```bash
: "${KUBE_CONTEXT:?Set the reviewed Kubernetes context}"
: "${GW_NODE_A:?Set the first eligible gateway node}"
: "${GW_NODE_B:?Set the second eligible gateway node}"
kubectl --context "$KUBE_CONTEXT" get node "$GW_NODE_A" "$GW_NODE_B" \
-L topology.kubernetes.io/zone,eks.amazonaws.com/compute-type
# Apply only after source/destination check, IAM and network prerequisites are met:
kubectl --context "$KUBE_CONTEXT" label node "$GW_NODE_A" "$GW_NODE_B" hybrid-gateway-node=true
```
---
## CNI 구성
### Cilium VTEP 활성화
AWS가 유지 관리하는 Cilium build를 사용합니다. [Gateway CNI 문서](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-gateway-cni.html)의 branch별 최소 버전은 **1.17.13-1·1.18.8-1·1.19.2-1**입니다. 이는 기능 최소 버전이며 downgrade나 branch 지원 종료를 무시하라는 권장이 아닙니다. 기존 node selector·IPAM 범위·검토한 release 값을 유지합니다.
#### Cilium Helm 값에서 VTEP 활성화
필수 변경은 `vtep.enabled=true`, **`l7Proxy=false`**입니다. [운영 문서](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/08-operations.md)의 Cilium Ingress/Gateway API L7 profile을 같은 Cilium 설치에서 함께 켤 수 없습니다. 일반 HTTP 앱이 Gateway 라우팅을 사용하는 것까지 금지하는 제약은 아닙니다. VXLAN은 암호화를 제공하지 않으며, 별도 예제의 WireGuard flag만으로 이 datapath의 암호화 호환성이 확인되는 것도 아닙니다.
#### Cilium 설치/업데이트
```bash
: "${KUBE_CONTEXT:?Set the reviewed Kubernetes context}"
: "${CILIUM_VERSION:?Choose an AWS-maintained Cilium version meeting the gateway minimum}"
# Apply to the existing reviewed Hybrid Cilium release during a maintenance window.
helm upgrade cilium oci://public.ecr.aws/eks/cilium/cilium \
--version "$CILIUM_VERSION" --namespace kube-system \
--kube-context "$KUBE_CONTEXT" --reuse-values \
--set vtep.enabled=true --set l7Proxy=false
kubectl --context "$KUBE_CONTEXT" -n kube-system rollout restart daemonset/cilium
kubectl --context "$KUBE_CONTEXT" -n kube-system rollout status daemonset/cilium --timeout=300s
kubectl --context "$KUBE_CONTEXT" -n kube-system get configmap cilium-config \
-o jsonpath='{.data.enable-vtep}{"\n"}{.data.enable-l7-proxy}{"\n"}'
```
출력은 순서대로 `true`, `false`여야 합니다. 각 Hybrid Node의 Cilium 건강 상태와 controller 소유 VTEP 객체도 확인합니다. 선택한 image에 들어 있는 CLI와 help를 확인한 뒤 해당 버전의 `bpf vtep` 명령을 사용합니다. DaemonSet에서 임의로 선택한 Pod 하나가 모든 노드를 검증하지는 않습니다.
### VPC CNI 구성
AWS VPC CNI를 쓰는 cloud node에서 **Hybrid endpoint로 가는 ClusterIP 트래픽**을 위해 Hybrid Pod CIDR의 SNAT 제외 설정이 필요합니다. 이 값이 없어도 직접 Pod IP 통신은 성공할 수 있으므로 그 테스트만으로 완료 처리하지 않습니다. 기존 제외 범위를 유지하고 add-on 설정 소유자와 변경을 일치시킵니다.
```bash
: "${KUBE_CONTEXT:?Set the reviewed Kubernetes context}"
# MNG/self-managed cloud nodes using the aws-node DaemonSet:
kubectl --context "$KUBE_CONTEXT" -n kube-system get daemonset aws-node
# Preserve existing exclusions; use the complete reviewed CSV union, not just a new CIDR.
: "${SNAT_EXCLUDE_CIDRS:?Set existing exclusions plus all Hybrid Pod CIDRs}"
kubectl --context "$KUBE_CONTEXT" -n kube-system set env daemonset/aws-node \
AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS="$SNAT_EXCLUDE_CIDRS"
```
Auto Mode는 내장 네트워킹을 사용하므로 설정할 `aws-node` DaemonSet이 있는 것으로 가정하지 않습니다. 이 절차로 Auto Mode 네트워킹을 설치·교체하지 않습니다. 지원되는 Hybrid service 경로를 별도로 확인하며, 혼합 환경에서는 실제 aws-node가 관리하는 cloud node에만 DaemonSet 설정을 적용합니다. Prefix delegation/custom networking은 별도 설계 선택이며 Gateway 필수 설정이 아닙니다.
### CiliumVTEPConfig CRD 상세
[1.0.2 upsert 구현](https://github.com/aws/eks-hybrid-nodes-gateway/blob/v1.0.2/internal/cilium/vtep.go)의 API 구조입니다. Gateway는 `hybrid-gateway`라는 객체를 관리합니다. 클러스터에 CiliumVTEPConfig가 하나만 존재할 수 있다는 뜻은 아닙니다.
```yaml
# Illustrative controller-owned observation; do not apply over the running controller.
apiVersion: cilium.io/v2
kind: CiliumVTEPConfig
metadata:
name: hybrid-gateway
spec:
endpoints:
- name: vpc-gateway
tunnelEndpoint: "10.0.1.5"
cidr: "10.0.0.0/16"
mac: "82:36:6c:89:e6:ad" # Read the actual leader VXLAN MAC.
```
리더 변경 시 `tunnelEndpoint`와 `mac`을 갱신합니다. Endpoint 하나는 `cidr` 하나를 가지며 여러 VPC prefix는 여러 entry로 표현합니다. MAC은 실제 리더 VXLAN 인터페이스의 값이며 임의 dummy 값이 아닙니다. Cilium 반영과 애플리케이션 복구를 관찰해야 하며 보편적인 1–5초 완료를 가정하지 않습니다. 이전의 해당 시간 범위는 측정 증거가 없는 예시였습니다.
### CNI 구성 검증 체크리스트
1. AWS Cilium 버전과 VTEP=true/L7=false를 확인합니다.
2. CiliumNode의 Hybrid label·internal IP·Pod CIDR, 각 Gateway의 로컬 tunnel 상태를 대조합니다.
3. VTEP의 leader IP/MAC과 aggregate VPC route의 ENI를 대조합니다.
4. cloud node 유형에 맞는 SNAT·return routing과 security group/firewall을 확인합니다.
5. 직접 Pod IP와 ClusterIP·webhook 등 실제 애플리케이션 경로를 각각 검증합니다. API 조회 실패는 정상/없음으로 처리하지 않습니다.
---
## 트래픽 흐름 패턴
### 패턴 1: VPC Pod에서 Hybrid Pod로

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-10-hybrid-nodes-gateway-2.html)
**상세 흐름:**
1. VPC Pod(10.0.1.50)가 Hybrid Pod(10.85.1.50)로 패킷을 전송
2. VPC CNI가 패킷을 VPC 네트워크로 라우팅
3. VPC 라우트 테이블에서 `10.85.0.0/16 → eni-게이트웨이` 규칙에 의해 게이트웨이 EC2로 전달
4. 게이트웨이의 커널 라우트 테이블에서 `10.85.1.0/24 via dev hybrid_vxlan0 onlink` 매칭
5. hybrid_vxlan0 인터페이스에서 VXLAN 캡슐화 (VNI 2, UDP 8472)
6. 캡슐화된 패킷이 Direct Connect/VPN을 통해 온프레미스로 전달
7. Hybrid Node의 Cilium이 VXLAN 패킷을 수신하고 디캡슐화
8. 원본 패킷이 대상 Hybrid Pod로 전달
### 패턴 2: Hybrid Pod에서 VPC Pod로

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-10-hybrid-nodes-gateway-3.html)
**상세 흐름:**
1. Hybrid Pod(10.85.1.50)가 VPC Pod(10.0.1.50)로 패킷을 전송
2. Cilium eBPF 프로그램이 CiliumVTEPConfig를 참조하여 대상 IP(10.0.1.50)가 VPC CIDR(10.0.0.0/16)에 속하는 것을 확인
3. Cilium이 패킷을 VXLAN으로 캡슐화하여 게이트웨이 EC2(10.0.1.100)로 전송
4. 게이트웨이의 hybrid_vxlan0 인터페이스가 VXLAN 패킷을 수신하고 디캡슐화
5. 디캡슐화된 패킷이 VPC 네트워크를 통해 대상 VPC Pod로 라우팅
### 패턴 3: Control Plane에서 Webhook으로
이 패턴은 Hybrid Node에서 실행되는 Admission Webhook이나 Conversion Webhook으로의 통신에 중요합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-10-hybrid-nodes-gateway-4.html)
> Control plane에서 실제 remote Pod endpoint로 접근할 수 있어야 합니다. Remote Pod network·control-plane subnet route·webhook Service/endpoint/TLS·firewall·return path를 확인합니다. Gateway 설치만으로 이 조건이 완성되지는 않으며 routable native Pod 설계도 Hybrid webhook을 지원할 수 있습니다.
### 패턴 4: AWS 서비스에서 Hybrid Pod로
ALB/NLB IP target과 AMP managed collector의 scraper처럼 VPC에서 Hybrid endpoint를 호출하는 구성입니다. AMP workspace ingestion과 scraper는 서로 다른 역할입니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-10-hybrid-nodes-gateway-5.html)
**통합별 전제 조건:**
| 경로 | 확인 사항 |
|---|---|
| ALB/NLB IP target | 의도한 controller의 target 등록, LB subnet route·health check·application port·source identity·return path |
| AMP managed collector | Scraper에서 metric endpoint까지 VPC/remote Pod route와 firewall port. Private cluster endpoint 필요 |
| Prometheus remote_write | Prometheus/ADOT collector가 AMP workspace ingestion으로 push. Workspace 자체가 inbound scraper는 아님 |
| CloudWatch / trace export | Agent/collector가 service endpoint로 전송. IAM/API/DNS/egress 설정은 별도 |
| PrivateLink | Consumer endpoint → provider service/LB → target 설계를 검토. 일반 AWS service가 임의 Hybrid Pod로 연결을 시작하는 구조가 아님 |
[Hybrid add-on 문서](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-add-ons.html)는 해당 endpoint/네트워크 전제 조건을 충족한 AMP managed collection을 지원합니다. 이 scraper와 자체 관리 remote_write를 구분하며 Gateway 설치만으로 둘 중 어느 것도 자동 구성되지 않습니다.
---
## 고가용성 및 페일오버
### HA 아키텍처

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-hybrid-nodes-10-hybrid-nodes-gateway-6.html)
### Lease 기반 리더 선출 상세
리더 선출은 Kubernetes 내장 기능인 `coordination.k8s.io/v1` Lease 리소스를 사용합니다:
```yaml
apiVersion: coordination.k8s.io/v1
kind: Lease
metadata:
name: hybrid-gateway-leader
namespace: eks-hybrid-nodes-gateway
spec:
holderIdentity: gateway-node-hostname_example-uuid
leaseDurationSeconds: 3
acquireTime: "2026-06-28T10:00:00Z"
renewTime: "2026-06-28T10:05:15Z"
leaseTransitions: 2
```
### 페일오버 동작
**아래 표는 재현 가능한 측정 trace가 없는 이전 예시입니다.** 원 수치를 보존하지만 현재 SLO나 보장값으로 사용하지 않습니다. 현재 AWS 지침의 예상치는 약 3–5초이며 tagged README에는 다른 예상치도 있습니다. 실제 API·route·Cilium·애플리케이션 복구를 검증해야 합니다. Node/AZ 장애에서 kubelet 상태 전파를 기다려야만 Lease 인계가 시작된다는 의미도 아닙니다.
이전 문서의 페일오버 예시:
| 장애 유형 | 감지 시간 | 복구 시간 | 총 중단 시간 |
|-----------|----------|----------|------------|
| Gateway Pod 크래시 | 즉시 (Pod 종료) | ~3초 (Lease 만료) | ~5-10초 |
| EC2 인스턴스 장애 | ~30초 (kubelet 타임아웃) | ~3초 (Lease 만료) | ~35-40초 |
| AZ 전체 장애 | ~1분 (노드 상태 전파) | ~3초 (Lease 만료) | ~65-70초 |
| 네트워크 파티션 | ~2초 (renewDeadline 초과) | ~3초 (Lease 만료) | ~5-10초 |
**페일오버 프로세스:**
1. 리더 Pod가 Lease 갱신에 실패 (renewDeadline: 2초)
2. Lease가 만료됨 (leaseDuration: 3초)
3. 팔로워 Pod가 Lease를 획득 (retryPeriod: 1초)
4. 새 리더가 자신의 EC2 인스턴스 ENI로 VPC 라우트 테이블 업데이트
5. CiliumVTEPConfig CRD를 새 리더의 정보로 업데이트
6. 기존 standby의 VXLAN·로컬 엔트리는 이미 유지됨. Cilium 반영과 실제 데이터 경로 확인
7. 트래픽이 새 리더를 통해 흐름
장애 시험은 승인된 창에서 실제 Pod/node 대응과 traffic probe, 복구 담당자를 정한 뒤 수행합니다. holderIdentity는 hostname_UUID일 수 있으므로 이를 곧바로 Pod 삭제 인자로 사용하지 않습니다. 이전 스크립트의 40–55초 또는 고정 sleep 60초 역시 복구 보장이 아니며 실행 증거가 없습니다. 별도 측정 없이 sleep 종료를 성공으로 처리하지 않습니다.
```bash
: "${KUBE_CONTEXT:?Set the reviewed Kubernetes context}"
kubectl --context "$KUBE_CONTEXT" -n eks-hybrid-nodes-gateway get pods -o wide
kubectl --context "$KUBE_CONTEXT" get nodes -l hybrid-gateway-node=true -o wide
kubectl --context "$KUBE_CONTEXT" -n eks-hybrid-nodes-gateway get lease hybrid-gateway-leader -o yaml
kubectl --context "$KUBE_CONTEXT" get ciliumvtepconfig hybrid-gateway -o yaml
```
두 Gateway가 모두 실패하면 별도 underlay/API/Cilium 장애와 구분합니다. Gateway를 거치지 않는 node/control-plane 또는 intra-Hybrid 경로도 자체 네트워크·DNS·종속성이 정상일 때만 유지됩니다. 리더 복구 후 실제 VPC route ENI와 양방향 앱 probe를 다시 확인합니다.
### Multi-AZ 배포 전략
Chart 1.0.2는 서로 다른 host를 필수로 하고 다른 AZ를 선호합니다. 서로 다른 AZ에 적격 노드를 준비하고 실제 배치를 확인하며 topology label을 임의로 바꾸지 않습니다. 이전 `affinity`·`topologySpreadConstraints` values는 이 chart에 연결되지 않습니다. 엄격한 AZ 제약이 필요하면 관리하는 Deployment overlay와 AZ/capacity 부족 시 Pending되는 동작을 함께 검토합니다.
PDB를 별도로 구성하면 eviction API를 통한 자발적 퇴거를 제한할 수 있습니다. 직접 Pod 삭제·Deployment rollout·비자발적 장애는 막지 않으며 leader의 신원이나 교체 순서를 보호하지도 않습니다. Auto Mode surge에는 host anti-affinity를 만족할 추가 적격 노드가 필요합니다.
---
## 운영
### 모니터링
#### Prometheus 메트릭
1.0.2는 **10080**의 `/metrics`, 8088의 `/healthz`·`/readyz`를 노출합니다. [Metric 정의](https://github.com/aws/eks-hybrid-nodes-gateway/blob/v1.0.2/internal/metrics/metrics.go)와 [collector](https://github.com/aws/eks-hybrid-nodes-gateway/blob/v1.0.2/internal/metrics/collector.go)에서 이름·타입을 확인했습니다.
| Metric | 타입 | 의미 |
|---|---|---|
| `hybrid_gateway_leader_is_active` | Gauge | 해당 replica의 리더 상태. Route setup 성공 증거는 아님 |
| `hybrid_gateway_hybrid_nodes_configured` | Gauge | 해당 replica의 로컬 노드 구성 수 |
| `hybrid_gateway_vxlan_tx_bytes_total`, `hybrid_gateway_vxlan_rx_bytes_total` | Counter | Kernel byte counter. Rate와 reset 고려 |
| `hybrid_gateway_vxlan_tx_packets_total`, `hybrid_gateway_vxlan_rx_packets_total` | Counter | Packet 수. Application 성공 횟수는 아님 |
| `hybrid_gateway_vxlan_interface_up` | Gauge | 인터페이스 상태. 전체 경로 도달성은 아님 |
| `hybrid_gateway_vxlan_fdb_entries`, `hybrid_gateway_vxlan_route_count` | Gauge | 로컬 table entry 수 |
| `hybrid_gateway_aws_route_table_update_total`, `hybrid_gateway_aws_route_table_update_errors_total` | Counter | Route 작업 성공·오류 event |
| `hybrid_gateway_aws_route_table_update_duration_seconds`, `hybrid_gateway_leader_setup_duration_seconds` | Histogram | 작업 duration. Event가 없으면 유용한 지연 추정 불가 |
일부 설명 표가 gauge라고 부르더라도 이 코드의 network `_total` 값은 Counter입니다. `LeaderIsActive`는 route/VTEP setup 완료 전에 설정되며 readiness도 애플리케이션 probe가 아닙니다. Leader 부재/중복이 지속되면 scrape 상태·API 오류를 함께 조사하며 순간 sample 하나로 split-brain을 단정하지 않습니다.
#### ServiceMonitor 설정
Chart는 Service를 생성하지 않습니다. 다음 별도 예제는 Service와 그 **이름 있는 Service port**를 선택하는 ServiceMonitor를 함께 정의합니다. Prometheus Operator/CRD·namespace 선택·실제 `serviceMonitorSelector`가 미리 설정되어 있어야 합니다. 예시 `release` label은 해당 stack에 맞게 바꿉니다. Host-network metrics port 접근은 node/network 제어로 제한합니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: hybrid-gateway-metrics
namespace: eks-hybrid-nodes-gateway
labels:
app.kubernetes.io/name: eks-hybrid-nodes-gateway
app.kubernetes.io/instance: eks-hybrid-nodes-gateway
spec:
selector:
app.kubernetes.io/name: eks-hybrid-nodes-gateway
app.kubernetes.io/instance: eks-hybrid-nodes-gateway
ports:
- name: metrics
port: 10080
targetPort: metrics
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: hybrid-gateway-metrics
namespace: eks-hybrid-nodes-gateway
labels:
release: kube-prom # Must match the existing Prometheus serviceMonitorSelector.
spec:
namespaceSelector:
matchNames: [eks-hybrid-nodes-gateway]
selector:
matchLabels:
app.kubernetes.io/name: eks-hybrid-nodes-gateway
app.kubernetes.io/instance: eks-hybrid-nodes-gateway
endpoints:
- port: metrics
interval: 30s
scrapeTimeout: 10s
path: /metrics
```
#### Grafana 대시보드 쿼리 예시
```promql
# Expected steady-state leader count, scoped to this one Service scrape job.
sum(hybrid_gateway_leader_is_active{namespace="eks-hybrid-nodes-gateway",service="hybrid-gateway-metrics"})
# Per-target VXLAN TX/RX byte rates; do not sum duplicate scrape jobs.
rate(hybrid_gateway_vxlan_tx_bytes_total{namespace="eks-hybrid-nodes-gateway",service="hybrid-gateway-metrics"}[5m])
rate(hybrid_gateway_vxlan_rx_bytes_total{namespace="eks-hybrid-nodes-gateway",service="hybrid-gateway-metrics"}[5m])
# Failures in the observation window, not a cumulative nonzero counter alert.
increase(hybrid_gateway_aws_route_table_update_errors_total{namespace="eks-hybrid-nodes-gateway",service="hybrid-gateway-metrics"}[5m])
# Per-replica local configuration counts; summing leader and standby double-counts nodes.
hybrid_gateway_hybrid_nodes_configured{namespace="eks-hybrid-nodes-gateway",service="hybrid-gateway-metrics"}
# Target scrape health is separate from leader/traffic metrics.
up{namespace="eks-hybrid-nodes-gateway",service="hybrid-gateway-metrics"}
```
Leader와 standby의 node count를 합산하면 같은 노드를 중복 계산합니다. 여러 scrape job도 구분합니다. Series가 없을 때 sum이 자동으로 0이 되는 것은 아니므로 missing target/scrape 경고를 별도로 둡니다. 트래픽 감소는 idle 상태일 수도 있으며 과거 누적 오류가 nonzero라는 이유만으로 현재 장애를 단정하지 않습니다.
#### CloudWatch 알람 설정
EC2 `NetworkIn`/`NetworkOut`은 인스턴스 단위이며 VXLAN만의 ENI metric이 아닙니다. Gateway custom metric은 collector의 namespace·dimension·unit·누적 counter 처리를 먼저 구성하고 실제 게시 sample을 확인해야 합니다. put-metric-alarm이 metric을 만들거나 트래픽 장애를 입증하지 않습니다. Collector 단절·idle·counter reset을 알람 설계에 반영합니다.
### 트러블슈팅
#### 자주 발생하는 문제와 해결 방법
| 관찰 | 확인할 후보 원인·범위 |
|---|---|
| 트래픽 전달 실패 | 양방향 UDP8472·SG/NACL/firewall, primary ENI forwarding, 실제 listener와 반환 경로 |
| Aggregate VPC route 누락/오류 | 설정한 table/CIDR, Gateway workload IAM identity, leader setup 오류 |
| Leader/VTEP 이상 | Lease/API/RBAC, AWS Cilium 최소 버전과 VTEP=true/L7=false, 실제 endpoint/MAC |
| 작은 요청만 성공 | 전체 underlay 최소 MTU, packet loss, TLS/앱 설정. 증상 하나로 MTU를 확정하지 않음 |
| 간헐적 인계 | API 지연·node 건강·자원 압박·scrape 누락. Readiness는 로컬 boolean 확인 |
#### 진단 명령어
다음 수집은 조회나 log 요청 실패 시 중단하며 auth/transport 오류를 리소스 없음으로 숨기지 않습니다. 결과는 비공개 파일이며 공유 전 민감 정보를 제거합니다. Label selector는 리더만이 아니라 두 replica를 선택합니다.
```bash
#!/usr/bin/env bash
set -euo pipefail
umask 077
: "${KUBE_CONTEXT:?Set the reviewed Kubernetes context}"
: "${AWS_REGION:?Set the reviewed AWS Region}"
: "${ROUTE_TABLE_ID:?Set one reviewed route table ID}"
OUT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gateway-diagnose.XXXXXX")
kubectl --context "$KUBE_CONTEXT" -n eks-hybrid-nodes-gateway get pods -o json > "$OUT_DIR/pods.json"
kubectl --context "$KUBE_CONTEXT" -n eks-hybrid-nodes-gateway \
get lease hybrid-gateway-leader -o json > "$OUT_DIR/lease.json"
kubectl --context "$KUBE_CONTEXT" get ciliumvtepconfig hybrid-gateway -o json > "$OUT_DIR/vtep.json"
kubectl --context "$KUBE_CONTEXT" get ciliumnodes -o json > "$OUT_DIR/ciliumnodes.json"
aws ec2 describe-route-tables --region "$AWS_REGION" \
--route-table-ids "$ROUTE_TABLE_ID" --output json > "$OUT_DIR/routes.json"
kubectl --context "$KUBE_CONTEXT" -n eks-hybrid-nodes-gateway logs \
-l app.kubernetes.io/name=eks-hybrid-nodes-gateway \
--all-containers=true --prefix=true --tail=100 > "$OUT_DIR/gateway.log"
printf 'Private diagnostic files: %s\n' "$OUT_DIR"
```
holderIdentity를 Pod 이름으로 사용하지 않습니다. 실제 Pod node/IP·VTEP IP/MAC·route primary ENI·CiliumNode CIDR을 대조합니다. `Processing CiliumNode`·`Adding hybrid node to gateway`·`Remote VTEP added`·`Reconciling CiliumVTEPConfig` 등 실제 1.0.2 로그를 사용하며 runtime이 호출하지 않는 AWS DeleteRoute 로그를 기다리지 않습니다. chart의 `logLevel=debug`도 지원 값이 아니므로 logging flag와 관리하는 overlay를 먼저 확인합니다.
#### 연결 테스트
명시적으로 선택한 cloud/Hybrid workload에서 실제 listen 중인 서버를 대상으로 양방향 직접 Pod IP, ClusterIP, webhook/LB 경로를 각각 시험합니다. sleep만 실행하는 BusyBox Pod는 HTTP 서버가 아니며, `compute-type=ec2`라는 label을 모든 cloud node가 가진다고 가정하지 않습니다. 승인된 namespace·node selector·image를 준비하고 자신이 생성한 test resource만 정리합니다.
UDP는 연결 handshake가 없어 `nc -uz` 성공이 VXLAN 도착/decapsulation을 입증하지 않습니다. 외부 UDP8472는 physical interface에서, 내부 packet은 `hybrid_vxlan0`에서 관찰합니다. 내부 인터페이스에 외부 UDP8472 필터를 적용하면 조사할 트래픽을 놓칠 수 있습니다. 캡처 범위·시간·권한과 민감 payload 노출을 제한합니다.
MTU 검사는 iproute2/iputils가 실제 포함된 진단 환경에서 제한한 횟수·timeout으로 수행합니다. BusyBox ping이 iputils `-M do`를 지원한다고 가정하지 않습니다. IPv4 ICMP에는 payload 외 28바이트의 IP/ICMP header가 필요하며 VPN/DX 등 전체 경로의 최소 MTU를 고려합니다. ICMP 차단이나 API 서버의 TLS/auth 오류를 네트워크 단절로 단정하지 않습니다.
운영자의 STS identity 조회는 Gateway SDK의 identity를 증명하지 않습니다. DryRun은 대상 principal/parameter와 `DryRunOperation`·`UnauthorizedOperation`을 구분해야 하며 실제 라우트 생성 성공이 아닙니다. 누락된 map 때문에 Cilium을 무조건 재시작하지 말고 버전·설정·reconcile 오류부터 확인합니다.
### 스케일링 고려 사항
#### Hybrid Node 수에 따른 게이트웨이 스케일링
**이전의 미검증 sizing 예시를 보존한 표입니다.** 노드 수별 성능 한도나 실측 bandwidth가 아닙니다. CiliumNode별 로컬 tunnel과 aggregate AWS route를 구분하며, standby replica 증설은 처리량을 분산하지 않습니다. Peak byte rate·PPS·동시 연결/tunnel·CPU·memory·장애 여유를 실제 instance 사양/트래픽으로 평가합니다.
| Hybrid Node 수 | 게이트웨이 인스턴스 타입 | FDB 엔트리 수 | 예상 대역폭 |
|---------------|----------------------|-------------|-----------|
| 1-10 | c5.large | ~10 | ~5 Gbps |
| 10-50 | c5.xlarge | ~50 | ~10 Gbps |
| 50-200 | c5.2xlarge | ~200 | ~20 Gbps |
| 200+ | c5n.2xlarge | ~500+ | ~25 Gbps |
> VXLAN 처리는 CPU·memory도 사용합니다. EC2 burst/baseline bandwidth와 PPS 한계를 확인하며 표의 최대치만으로 지속 처리량을 보장하지 않습니다.
#### 대역폭 모니터링
```bash
# EC2 인스턴스의 네트워크 사용량 모니터링
aws cloudwatch get-metric-statistics \
--namespace "AWS/EC2" \
--metric-name "NetworkIn" \
--dimensions "Name=InstanceId,Value=i-0abc123456789def0" \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 300 \
--statistics Average Maximum
```
### 업그레이드
#### Helm 차트 업그레이드
실제로 게시된 release와 chart/application 계약을 확인합니다. 이 감사는 1.0.2를 검증했으며 이전 1.1.0 명령은 실행하지 않은 예시였습니다. 소유한 values·manifest·route inventory를 비공개로 보관하고 Helm history에서 정확한 rollback revision을 선택합니다. Revision 1을 무조건 복구 대상으로 삼지 않습니다.
```bash
: "${KUBE_CONTEXT:?Set the reviewed Kubernetes context}"
: "${GATEWAY_VERSION:?Set an existing, reviewed release version}"
helm history eks-hybrid-nodes-gateway -n eks-hybrid-nodes-gateway --kube-context "$KUBE_CONTEXT"
# OCI charts are inspected/pulled by an explicit version, not helm search repo.
helm show chart oci://public.ecr.aws/eks/eks-hybrid-nodes-gateway --version "$GATEWAY_VERSION"
helm template eks-hybrid-nodes-gateway \
oci://public.ecr.aws/eks/eks-hybrid-nodes-gateway \
--version "$GATEWAY_VERSION" --namespace eks-hybrid-nodes-gateway \
--values values.yaml > gateway-upgrade-rendered.yaml
# Run only after reviewing the rendered diff, IAM, routes and rollback plan:
helm upgrade eks-hybrid-nodes-gateway \
oci://public.ecr.aws/eks/eks-hybrid-nodes-gateway \
--version "$GATEWAY_VERSION" --namespace eks-hybrid-nodes-gateway \
--kube-context "$KUBE_CONTEXT" --values values.yaml
kubectl --context "$KUBE_CONTEXT" -n eks-hybrid-nodes-gateway \
rollout status deployment/eks-hybrid-nodes-gateway --timeout=300s
```
#### 업그레이드 시 주의 사항
Deployment rolling update는 리더를 인식하지 않으므로 standby부터 교체한다고 보장하지 않습니다. chart 1.0.2는 `strategy` value도 연결하지 않습니다. Host anti-affinity/capacity, Lease와 route/VTEP 반영으로 트래픽 중단이 생길 수 있습니다. 이전 5–10초·40–55초는 실측 또는 upgrade SLO가 아닙니다. Helm rollback도 CRD/config 호환성과 앱·라우트 검증이 필요하며 외부 AWS route 전체를 자동 복구하지 않습니다.
### 제거와 외부 라우트 정리
**Helm 제거는 AWS 라우트를 삭제하지 않습니다.** 설치 전과 제거 전에 table·CIDR·기존 target·현재 Gateway ENI를 기록합니다. Gateway 종속 트래픽을 먼저 이전/종료하고 controller를 중지한 뒤 소유한 VTEP를 정리해야 재생성을 피할 수 있습니다.
```bash
: "${KUBE_CONTEXT:?Set the reviewed Kubernetes context}"
: "${AWS_REGION:?Set the reviewed AWS Region}"
: "${ROUTE_TABLE_ID:?Set one route table whose ownership was confirmed}"
# Snapshot/read current targets before deciding which routes to restore or delete.
aws ec2 describe-route-tables --region "$AWS_REGION" --route-table-ids "$ROUTE_TABLE_ID" --output json
kubectl --context "$KUBE_CONTEXT" get ciliumvtepconfig hybrid-gateway -o yaml
# Disruptive: retire/migrate traffic and record the current leader/ENI before running.
helm uninstall eks-hybrid-nodes-gateway -n eks-hybrid-nodes-gateway --kube-context "$KUBE_CONTEXT" --wait --timeout=180s
```
Gateway Pod가 사라진 뒤 migration 계획에 따라 이전 route target을 복구하거나, table/CIDR/현재 target과 소유권이 확인된 라우트만 삭제합니다. 다른 소유자가 target을 바꿨을 수 있으므로 설정한 CIDR 전체를 무조건 지우지 않습니다. 승인된 변경 후 AWS를 다시 조회하고 access/transport 오류를 없음으로 처리하지 않습니다.
`ciliumvtepconfig/hybrid-gateway`도 다른 consumer/controller가 사용하지 않는지 확인한 후 제거합니다. Pod Identity association/role·metrics resource·전용 capacity는 기록한 IaC 소유자를 통해 정리합니다. Helm 삭제만으로 유료 인프라 종료가 입증되지 않습니다. Namespace·node label은 독점 소유와 다른 workload 부재를 확인한 뒤 정리하고 앱 경로·route·association·capacity를 재검증합니다.
---
## 비교: 게이트웨이 사용 vs 미사용
### 상세 비교표
| 항목 | Gateway 없는 routable Pod 설계 | Hybrid Nodes Gateway |
|---|---|---|
| 라우팅 | 소유자가 관리하는 BGP/static/다른 자동화 | Aggregate VPC route와 Cilium VTEP·로컬 tunnel reconciliation |
| 구성 | Underlay/router/CNI/address 계획 | 같은 underlay 계획과 적격 node·IAM·AWS Cilium VTEP/L7 profile·route 소유권 |
| Webhook·ALB/NLB IP target | Route·remote Pod network·반환 경로·보안 규칙이 맞으면 가능 | 동일한 실제 경로 검증 필요. Helm 설치만으로 완료되지 않음 |
| CNI | 지원되는 routable Pod 설계 선택 | AWS Cilium VTEP와 L7 proxy 비활성화 필요 |
| 처리량 | 실제 router/link/CNI/host 한계 | 활성 Gateway 한 대와 전체 link/path 한계 |
| HA | 소유한 네트워크의 수렴·장애 설계 | Lease 기반 active-standby와 route/VTEP 반영. 일시 중단 가능 |
| 비용 | Cluster/Hybrid Nodes·연결·인프라·운영 비용 | 위 비용과 Gateway EC2/storage·해당 Auto Mode 요금·추가 cross-AZ 트래픽 가능 |
| 제거 | 소유한 route/resource 정리 | Helm은 외부 AWS route·IAM association·전용 capacity를 삭제하지 않음 |
이전의 설정 시간 “~30분” 대 “수 시간~수 일”, failover 5–10초는 측정 증거 없는 비교 예시이며 일반화하지 않습니다. 양쪽 모두 network 팀의 underlay·CIDR·MTU·firewall·복구 계획이 필요합니다.
### 아키텍처 비교
#### 게이트웨이 미사용 (수동 방식)
승인된 private underlay 위에서 Pod CIDR을 BGP/static routing 또는 다른 관리 경로로 연결할 수 있습니다. Gateway가 없다는 이유만으로 Hybrid webhook이나 ALB/NLB IP target이 불가능한 것은 아닙니다. 실제 remote Pod configuration·route·return path·security rule이 결정합니다.
#### 게이트웨이 사용 (자동 방식)
VPC의 aggregate Pod route가 활성 Gateway primary ENI를 가리키고, Gateway의 로컬 VXLAN state와 Hybrid node의 Cilium VTEP가 forwarding을 연결합니다. AWS route 소유권과 각 CiliumNode의 로컬 tunnel state는 구분합니다. Replica 증설은 active-active 처리량 분산이 아니며, VTEP/L7 조합·추가 hop·장애 수렴을 수용할 수 있는지 평가합니다.
### 마이그레이션 가이드: 수동 방식에서 게이트웨이로
#### Phase 1: 준비
정확한 table/CIDR/current target과 기존 BGP/static/NAT/CNI state의 소유권을 비공개로 기록합니다. Rollback 경로와 underlay·firewall·주소 중복을 검증합니다. 일률적인 “무중단 준비”를 가정하지 않습니다.
#### Phase 2: Cilium VTEP 활성화
위에서 검토한 AWS chart/minimum branch와 VTEP=true/L7=false profile을 사용합니다. 기존 selector/IPAM·release 값을 보존하고 CNI 변경 자체의 중단 가능성을 계획합니다.
#### Phase 3: 게이트웨이 설치
설치를 route-changing cutover로 취급합니다. Leader는 같은 CIDR의 기존 route를 ReplaceRoute할 수 있으므로 CreateRoute 실패로 이전 target이 안전하게 남을 것이라 가정하지 않습니다.
#### Phase 4: 검증 및 전환
양방향 직접 Pod IP·ClusterIP·실제 webhook/LB·return path를 확인합니다. 다른 route controller와 소유권을 조율하여 서로 덮어쓰지 않도록 합니다. 이미 Gateway가 재사용한 table/CIDR을 “이전 수동 route”라며 지우지 않습니다.
#### Phase 5: 정리
소유권과 불필요성이 확인된 BGP/static/NAT/resource만 종료합니다. Rollback은 Gateway controller 정지, 기록한 이전 target과 호환되는 Cilium/network 구성 복구, 트래픽 재검증을 포함합니다. Helm만으로 외부 route 변경이 원복되지 않습니다. 더 긴 prefix는 longest-prefix routing 영향을 검토한 설계 선택이지 일반적인 충돌 회피법이 아닙니다.
---
## 모범 사례
### 보안 모범 사례
#### 1. 최소 권한 원칙
앞의 4개 runtime action과 정확한 route-table 정책을 사용하고 DeleteRoute는 승인된 정리 소유자의 권한으로 분리합니다. Gateway values·ServiceAccount·node label·Pod Identity association 수정 권한도 제한합니다.
#### 2. 네트워크 세그먼테이션
실제 Hybrid node private CIDR, application port, API/DNS/metrics 경로로 SG와 firewall을 제한합니다. NACL은 stateless이므로 inbound UDP8472 rule 하나로 양방향 경로가 완성되지 않습니다. Node 교체와 반환 트래픽을 함께 검토합니다.
#### 3. Pod 보안 표준
1.0.2 chart는 capability를 모두 drop한 뒤 NET_ADMIN만 추가하며 privilege escalation을 비활성화합니다. NET_RAW·readOnlyRootFilesystem 등의 이전 values 조각은 실제 chart 설정이 아닙니다. `privileged: true`가 아니더라도 hostNetwork와 NET_ADMIN은 node 네트워크를 제어하는 높은 권한입니다. Baseline/Restricted Pod Security 정책에서 자동 허용되는 일반 앱 Pod로 취급하지 않습니다.
#### 4. 네트워크 정책
hostNetwork 트래픽에 대한 일반 Pod NetworkPolicy 동작은 구현에 따라 달라지므로 node/ENI/firewall 제어를 대신하지 않습니다. 실제 workload endpoint에 지원되는 L3/L4 정책을 적용하고 NAT·캡슐화 뒤 source identity를 검증합니다. CIDR selector는 Kubernetes namespace selector가 아닙니다. Cilium L7 정책은 이 Gateway의 l7Proxy=false 요구 사항과 충돌합니다.
### 성능 모범 사례
#### 1. 인스턴스 타입 선택
Sustained/burst bandwidth·PPS·packet size·CPU·memory·장애 여유를 평가합니다. 이전 node-count별 instance 표는 미검증 추정입니다. AWS 현재 Gateway operations 표의 c6in.2xlarge는 up to 40 Gbps이며, 최대치가 지속 처리량을 보장하지 않습니다.
#### 2. MTU 최적화
IPv4 VXLAN의 일반적인 50바이트에는 내부 Ethernet header가 포함됩니다. 기존 계산 9001−50=8951은 산술 예시이며 DX/VPN을 포함한 실제 path MTU 측정이 아닙니다. 더 작은 underlay MTU와 다른 캡슐화가 있으면 달라집니다. 승인된 환경에서 interface MTU와 양방향 packet behavior를 확인합니다.
#### 3. 커널 파라미터 튜닝
Gateway는 ip_forward를 읽어 확인하며 이 프로그램이 전역 sysctl을 자동 튜닝하지 않습니다. 이전 제안값 nf_conntrack_max=1048576, rmem/wmem max=16777216·default=1048576, tcp_rmem/tcp_wmem=4096/1048576/16777216, neighbor gc_thresh=1024/4096/8192는 검증 근거 없는 예시로만 보존합니다. Memory·kernel·node 관리 방식의 영향을 측정하지 않고 적용하지 않습니다. Auto Mode immutable node에 임의 sysctl 스크립트를 배포하는 절차로 해석하지 않습니다.
#### 애플리케이션 경로 확인
curl이 이미 설치된 승인된 client Pod와 실제 listen 중인 endpoint를 사용합니다. 명시한 cloud/Hybrid 위치에서 직접 Pod IP와 ClusterIP·실제 앱 경로를 각각 확인합니다. `time_total`은 DNS·연결/TLS·서버 처리를 포함하며 순수 network RTT나 benchmark가 아닙니다.
```bash
#!/usr/bin/env bash
set -euo pipefail
: "${KUBE_CONTEXT:?Set the reviewed Kubernetes context}"
: "${PROBE_NAMESPACE:?Set the approved probe namespace}"
: "${PROBE_CLIENT_POD:?Set an existing client Pod with curl installed}"
: "${PROBE_URL:?Set the actual listening application URL}"
# Select and inspect the client node first; run separately from cloud and Hybrid clients.
kubectl --context "$KUBE_CONTEXT" -n "$PROBE_NAMESPACE" get pod "$PROBE_CLIENT_POD" -o wide
for attempt in 1 2 3; do
kubectl --context "$KUBE_CONTEXT" -n "$PROBE_NAMESPACE" exec "$PROBE_CLIENT_POD" -- \
curl --fail --show-error --silent --connect-timeout 3 --max-time 10 \
--output /dev/null --write-out 'http_code=%{http_code} total_seconds=%{time_total}\n' \
"$PROBE_URL"
done
```
### 비용 최적화
#### 1. 인스턴스 비용 추정
Gateway 소프트웨어 요금은 없지만 EC2/storage·해당 Auto Mode 관리 요금·cross-AZ 전송·private 연결·관측성과 cluster/Hybrid Nodes 요금은 별도입니다. 같은 VPC라도 cross-AZ 전송이 무료인 것은 아닙니다.
**아래는 출처를 재확인할 수 없는 이전 가격·할인·인건비 추정입니다.** 원 수치를 보존하지만 현재 견적이나 측정한 절감 효과가 아닙니다. 실제 Region·사용량·약정·데이터 경로를 [EKS 가격](https://aws.amazon.com/eks/pricing/)과 관련 요금에 대조하여 새로 산정합니다.
```
게이트웨이 비용 계산 (ap-northeast-2 기준):
c5.xlarge (4 vCPU, 8 GiB):
온디맨드: $0.192/시간 × 24시간 × 30일 = ~$138/월
1년 예약: ~$89/월 (약 35% 절감)
3년 예약: ~$59/월 (약 57% 절감)
HA 구성 (2 인스턴스):
온디맨드: ~$276/월
1년 예약: ~$178/월
3년 예약: ~$118/월
비교: 기존 수동 방식의 관리 비용
- 네트워크 엔지니어 인건비 (부분 시간): ~$2,000-5,000/월
- BGP 라우터 유지보수: ~$200-500/월
- 운영 오버헤드: 측정 불가
```
#### 2. 비용 절감 전략
실제 utilization과 standby/failure 요구를 바탕으로 크기를 조정합니다. 장기 약정 할인은 안정적인 사용량과 실제 조건을 평가한 뒤 선택하며 고정 절감률을 가정하지 않습니다. Spot 등 capacity 유형은 명시한 중단 예산으로 결정합니다. Replica가 둘이라는 이유만으로 Spot이 안전하거나 모든 환경의 중단 허용치가 같다고 판단하지 않습니다.
### 통합 모범 사례
#### 1. GitOps와의 통합
Argo CD에 Helm OCI repository(enableOCI=true)와 제한된 AppProject/destination 접근을 먼저 등록합니다. 이 예제는 valuesObject를 포함하므로 chart에 없는 values.yaml 파일을 요구하지 않습니다. 실제 CIDR·table ID·node 모드로 바꾸고 node/CNI/IAM 준비와 render/diff 검토 후 동기화합니다. 자동 prune/selfHeal가 외부 AWS route 정리나 안전한 cutover를 대신하지 않습니다.
```yaml
# Assumes an existing restricted AppProject and Helm OCI repository registration (enableOCI=true).
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: hybrid-nodes-gateway
namespace: argocd
spec:
project: infrastructure
source:
chart: eks-hybrid-nodes-gateway
repoURL: public.ecr.aws/eks
targetRevision: "1.0.2"
helm:
releaseName: eks-hybrid-nodes-gateway
valuesObject:
vpcCIDR: "10.0.0.0/16"
podCIDRs: "10.85.0.0/16"
routeTableIDs: "rtb-0abc123456789def0,rtb-0def456789abc1230"
replicas: 2
nodeLabel: hybrid-gateway-node
autoMode:
enabled: false
destination:
server: https://kubernetes.default.svc
namespace: eks-hybrid-nodes-gateway
syncPolicy:
syncOptions:
- CreateNamespace=true
```
#### 2. Terraform과의 통합
기존 node/network/IAM stack에 다음 fragment를 연결합니다. 변수·provider 버전·cluster 접근과 실제 node/CNI 준비를 별도로 검증합니다. Bare EC2의 tag는 Kubernetes node label이 아니며 AMI만 지정한다고 cluster에 join하지 않습니다. Instance profile, bootstrap과 primary ENI 설정을 제공하지 않는 예제를 전체 배포로 사용하지 않습니다. `aws_instance.user_data`에는 plain user data를, base64를 사용한다면 해당 `user_data_base64` 속성을 구분합니다.
```hcl
# Fragment after node/CNI/IAM preparation; provider configuration and variables are external.
resource "helm_release" "hybrid_gateway" {
name = "eks-hybrid-nodes-gateway"
repository = "oci://public.ecr.aws/eks"
chart = "eks-hybrid-nodes-gateway"
version = "1.0.2"
namespace = "eks-hybrid-nodes-gateway"
create_namespace = true
values = [yamlencode({
vpcCIDR = var.vpc_cidr
podCIDRs = join(",", var.hybrid_pod_cidrs)
routeTableIDs = join(",", var.route_table_ids)
replicas = 2
nodeLabel = "hybrid-gateway-node"
autoMode = { enabled = false }
})]
depends_on = [aws_eks_pod_identity_association.gateway]
}
```
#### 3. CI/CD 파이프라인 통합
CI는 action/tool 버전을 검토·고정하고 최소 권한 OIDC role, 제한된 EKS access, 독립 kubeconfig/context, 대상 환경 보호와 동시 실행 제어를 사용합니다. PR에서 values/render/schema를 검사하고 route-changing 배포와 rollback은 해당 환경의 절차로 연결합니다. rollout status 성공만으로 실제 forwarding·webhook·ClusterIP 검증을 통과 처리하지 않습니다. 이 감사에서는 CI provider 로그인이나 AWS/cluster 배포를 실행하지 않았습니다.
---
## 전체 배포 예제: 처음부터 끝까지
다음은 앞의 검토된 구성 요소를 연결하는 절차이며 실행 완료한 production recipe가 아닙니다. 기존 중복 예제의 1.31/1.31.2 출력과 가짜 subnet/table ID는 실측·실제 inventory가 아니었습니다. 확인된 현재 inventory와 지원 버전을 사용합니다.
### 환경 가정
예시 주소는 VPC 10.0.0.0/16, AZ별 private subnet 10.0.1.0/24·10.0.2.0/24, remote node 192.168.10.0/24, Pod 10.85.0.0/16입니다. 이는 가상 설계이며 Service CIDR·다른 연결망과 중복되지 않는지 실제로 확인해야 합니다. 하나의 Gateway deployment는 하나의 EKS cluster를 위한 것입니다.
### Step 1: 사전 요구 사항 확인
AWS account/Region·EKS endpoint/auth/IPv4·CIDR inventory·private routing과 CiliumNode 상태를 조회합니다. AWS-supported Cilium 최소 버전과 VTEP/L7 요구를 확인합니다.
### Step 2: EC2 게이트웨이 노드 프로비저닝
공식 node 준비 절차에 따라 Auto Mode NodeClass/NodePool 또는 관리형/자체 관리 node를 준비합니다. 올바른 primary ENI forwarding·IAM·SG·private subnet·실제 AZ 분산·chart label을 확인합니다. Account 전체에서 nodegroup 이름만 필터링하여 다른 cluster의 instance 속성을 변경하지 않습니다.
### Step 3: IAM 역할 설정
앞의 정확한 route-table 정책과 cluster/namespace/ServiceAccount trust를 연결합니다. Pod Identity runtime 또는 별도로 검토한 IRSA를 준비하고 실제 gateway principal을 검증합니다.
### Step 4: Cilium VTEP 활성화
앞의 AWS Helm OCI 절차로 VTEP=true/L7=false를 적용합니다. 기존 values와 IPAM을 보존하고 cloud node 유형에 맞는 SNAT/ClusterIP 경로를 확인합니다.
### Step 5: 게이트웨이 설치
CSV 타입·replicas·nodeLabel·autoMode.enabled를 확인한 values.yaml과 검증한 chart version을 렌더링한 뒤 소유권과 cutover 계획에 따라 설치합니다. 존재하지 않는 chart value로 IRSA·metrics·rollout 설정이 적용된다고 가정하지 않습니다.
### Step 6: 설치 검증
실제 node/IP·Lease·VTEP·primary ENI route를 대조하고 양방향 Pod IP·ClusterIP·webhook/LB·return path를 검증합니다. 두 위치의 client와 실제 listener가 있어야 합니다. 본인이 소유한 시험 resource만 정리하며 Gateway 제거는 별도 external-route/IAM/capacity 정리 절차를 따릅니다.
---
## 자주 묻는 질문 (FAQ)
### Q1: Calico를 CNI로 사용할 수 있나요?
이 Gateway 구현은 AWS Cilium VTEP와 L7 proxy 비활성화가 필요합니다. Calico를 사용하는 다른 routable Pod 설계와 구분합니다. Cloud node는 관리형/자체 관리 AWS VPC CNI와 Auto Mode 내장 네트워킹을 구분합니다.
### Q2: 게이트웨이 EC2 인스턴스가 다운되면 어떻게 되나요?
정상 standby가 Lease를 획득하고 AWS route를 바꾼 뒤 VTEP를 갱신합니다. 실제 복구는 API·Cilium·underlay·애플리케이션 상태에 달려 있습니다. 이전 40–55초 예시는 검증된 중단 시간이나 보장값이 아닙니다.
### Q3: 여러 VPC에서 하나의 게이트웨이를 공유할 수 있나요?
Gateway deployment는 하나의 EKS cluster를 위한 것입니다. 추가 VPC에서 접근하는 설계는 지원 범위·route·CIDR·security·반환 경로를 별도로 검토해야 하며, 하나의 Helm release가 multi-cluster/TGW 라우팅을 구성하지 않습니다. VPC prefix를 여러 개 설정할 수 있다는 사실만으로 임의 cross-VPC 설계를 보장하지 않습니다.
### Q4: VXLAN 오버헤드가 성능에 미치는 영향은?
IPv4 VXLAN은 일반적으로 50바이트 overhead를 추가하지만 PPS·packet size·CPU·offload와 전체 경로가 중요합니다. Jumbo frame이라는 이유만으로 overhead나 CPU 영향이 무시할 수준이라고 단정하지 않습니다.
### Q5: 온프레미스에서 인터넷으로 나가는 트래픽도 게이트웨이를 경유하나요?
이 구성은 지정한 VPC prefix에 대한 VTEP 경로를 제공합니다. Internet default route나 NAT를 자동 구성하지 않습니다. 실제 목적지 prefix와 기존 CNI/라우팅/NAT 설정에 따라 egress 경로를 확인해야 합니다.
### Q6: 게이트웨이 없이도 Hybrid Node를 사용할 수 있나요?
네. 선택 사항이며, 지원되는 native/BGP/static Pod routing 설계도 직접 통신·webhook·AWS service IP target 경로를 제공할 수 있습니다. 각각 실제 route·return path·보안·remote Pod 구성을 검증해야 합니다.
### Q7: 기존 Direct Connect/VPN 설정을 변경해야 하나요?
Gateway가 기반 연결을 만들지는 않지만 기존 설정이 항상 충분한 것은 아닙니다. Node private IP 라우팅·UDP8472·MTU·firewall·교체 node 범위를 확인하고 필요한 변경을 수행합니다.
---
## 참고 자료
### AWS 공식 문서
- [EKS Hybrid Nodes Gateway 공식 문서](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-gateway-overview.html)
- [EKS Hybrid Nodes 네트워킹 가이드](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-networking.html)
- [EKS Hybrid Nodes CNI 구성](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-cni.html)
- [EKS Hybrid Nodes 트러블슈팅](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-troubleshooting.html)
### GitHub
- [eks-hybrid-nodes-gateway (소스 코드)](https://github.com/aws/eks-hybrid-nodes-gateway)
- [Cilium VTEP 문서](https://docs.cilium.io/en/stable/network/vtep/)
### 내부 관련 문서
- [네트워크 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/02-network-configuration.md) - CIDR 요구 사항, 방화벽 포트, 보안 그룹
- [노드 부트스트랩](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/04-node-bootstrap.md) - nodeadm을 사용한 Hybrid Node 설정
- [운영 및 유지보수](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/08-operations.md) - 모니터링, 로깅, 트러블슈팅
- [베어메탈 서버 OS 설치](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/09-bare-metal-os-setup.md) - 베어메탈 환경 구축
- [사전 요구 사항](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/01-prerequisites.md) - 전체 사전 요구 사항
### 추가 학습 자료
- [AWS re:Invent - EKS Hybrid Nodes Deep Dive](https://www.youtube.com/results?search_query=eks+hybrid+nodes+reinvent)
- [Cilium VXLAN Tunnel Endpoint (VTEP) 통합](https://docs.cilium.io/en/stable/network/vtep/)
- [VXLAN RFC 7348](https://datatracker.ietf.org/doc/html/rfc7348)
---
< [이전: 베어메탈 서버 OS 설치](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/09-bare-metal-os-setup.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-hybrid-nodes/README.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-auto-mode/
----------------------------------------
# EKS Auto Mode 운영 가이드
> **지원 버전**: EKS Auto Mode GA; 예제 기준 EKS 1.36
> **마지막 업데이트**: 2026년 9월 12일
Amazon EKS Auto Mode는 Kubernetes 노드 관리를 완전히 자동화하는 기능으로, 워크로드 요구 사항에 따라 자동으로 노드를 프로비저닝하고 최적화합니다. 이 가이드는 Auto Mode의 개념, 설정과 운영 시 고려 사항을 다룹니다. AWS가 Auto Mode 인프라를 관리하더라도 애플리케이션의 가용성, resource requests, 보안, 모니터링과 클러스터/VPC 구성은 사용자 책임입니다. 예제는 프로덕션 적용 전에 해당 환경에서 검증해야 합니다.
### 2026년 7월 업데이트: EFA 및 배치 그룹(Placement Group) 지원
2026년 7월 22일, EKS Auto Mode(및 오픈소스 Karpenter)의 노드 풀에서 Elastic Fabric Adapter(EFA) 네트워크 디바이스 구성과 EC2 배치 그룹을 지원한다고 발표되었습니다. EFA 지원 인스턴스의 네트워크 인터페이스를 EFA 전용 또는 표준 ENI로 구성할 수 있으며 — EFA 전용 인터페이스는 VPC IP 주소를 소비하지 않으면서도 인터커넥트 대역폭을 최대로 활용할 수 있습니다 — cluster, spread, partition 배치 전략을 노드 풀 구성에서 직접 지정해 인스턴스를 시작할 수 있습니다. 최대 처리량이나 장애 격리가 필요한 분산 학습/추론 워크로드를 겨냥한 기능입니다. 자세한 내용은 [발표](https://aws.amazon.com/about-aws/whats-new/2026/07/amazon-eks-efa-placement-groups/)를 참고하세요.
### 2026년 7월 업데이트: ARC Zonal Shift 지원
2026년 7월 10일부터 EKS Auto Mode 클러스터에서도 Amazon Application Recovery Controller(ARC) zonal shift와 autoshift를 사용할 수 있습니다. Auto Mode가 컴퓨팅을 대신 관리하므로 별도 플래그 설정이나 Karpenter 버전 관리 없이 클러스터에서 ARC zonal shift만 활성화하면 됩니다. zonal shift가 발동되면 Auto Mode는 장애 AZ에서 신규 용량 프로비저닝을 중단하고, 해당 존 노드에 대한 consolidation·drift 같은 자발적 중단(voluntary disruption)도 함께 중단합니다. 정상 AZ의 자발적 중단도 대체 Pod의 배치가 장애 AZ에 의존하면 막습니다. Auto Mode에 별도의 Karpenter 플래그는 필요하지 않지만, 클러스터 등록 후 ARC zonal autoshift는 별도로 구성해야 합니다. Zonal shift가 단일 AZ 볼륨이나 엄격한 AZ 배치 제약을 자동으로 이동 가능한 상태로 만들지는 않습니다. ARC zonal shift 자체의 추가 요금은 없지만 대체 용량과 일반 인프라 요금은 계속 적용됩니다. 자세한 내용은 [발표](https://aws.amazon.com/about-aws/whats-new/2026/07/eks-auto-mode-arc-zonal-shift)와 [ARC zonal shift 문서](https://docs.aws.amazon.com/eks/latest/userguide/zone-shift-enable.html)를 참고하세요.
## 목차
1. [Auto Mode 시작하기](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/01-getting-started.md) - 클러스터 활성화 및 기본 설정
2. [NodePool 구성 및 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/02-nodepool-configuration.md) - 기본 및 커스텀 NodePool 설정
3. [스케일링 동작 이해](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/03-scaling-behavior.md) - 프로비저닝, Consolidation, Drift
4. [Spot 인스턴스 활용 전략](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/04-spot-strategies.md) - 비용 최적화를 위한 Spot 활용
5. [운영 및 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/05-operations.md) - 모니터링, 문제 해결, Day-2 운영
6. [비용 관리 및 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/06-cost-management.md) - 비용 분석 및 절감 전략
7. [노드 생명주기 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/07-node-lifecycle.md) - AMI 관리, 노드 갱신, 만료 정책
8. [워크로드별 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/08-workload-optimization.md) - 웹, 배치, GPU, AI/ML 워크로드
9. [관리형 노드 그룹에서 마이그레이션](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/09-migration-guide.md) - 마이그레이션 단계 및 주의사항
---
## EKS Auto Mode란 무엇인가?
EKS Auto Mode는 AWS가 관리하는 완전 자동화된 노드 관리 솔루션입니다. 내부적으로 Karpenter를 기반으로 하며, AWS가 관리형 인프라 컨트롤러를 운영합니다. 사용자는 Auto Mode용 Karpenter 컨트롤러를 별도로 설치하는 대신 워크로드 제약, 커스텀 NodePool/NodeClass와 disruption budget을 구성합니다.
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ EKS Auto Mode 아키텍처 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ EKS Control Plane (AWS 관리) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ API Server │ │ etcd │ │ Controller │ │ Karpenter │ │ │
│ │ │ │ │ │ │ Manager │ │ Controller │ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ NodePool 리소스 │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ general-purpose │ │ system │ │ custom-pool │ │ │
│ │ │ (기본 제공) │ │ (기본 제공) │ │ (사용자 정의) │ │ │
│ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ EC2 인스턴스 (자동 관리) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ m6i.2xl │ │ c7g.xl │ │ r6i.4xl │ ... │ │
│ │ │ (On-Demand) │ │ (Spot) │ │ (On-Demand) │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
## 기존 관리 방식과의 비교
| 특성 | 관리형 노드 그룹 | Fargate | Auto Mode |
|------|-----------------|---------|-----------|
| 노드 관리 | AWS가 노드 그룹을 관리하고 사용자가 용량·업데이트를 구성 | AWS가 Pod별 인프라 관리 | AWS가 생성한 노드와 인프라 컨트롤러 관리 |
| 스케일링 | 설치한 Cluster Autoscaler 또는 명시적인 그룹 용량 변경 | Fargate profile에 따른 Pod별 프로비저닝 | 스케줄 불가능한 Pod에 대한 Karpenter 기반 프로비저닝 |
| 프로비저닝 시간 | 용량·부트스트랩·워크로드에 따라 달라짐 | 용량·이미지·워크로드에 따라 달라지며 즉시 완료가 아님 | 용량·부트스트랩·이미지·제약 조건에 따라 달라지며 고정 시간 보장이 없음 |
| 인스턴스 선택 | 설정한 인스턴스 유형 | 관리형 컴퓨팅 크기 | NodePool·워크로드 제약 내에서 선택 |
| Spot | 관리형 노드 그룹에서 지원 | EKS Fargate에서 미지원 | NodePool에서 허용한 경우 지원 |
| GPU 워크로드 | 적절한 노드에서 지원 | 미지원 | 호환되는 가속 인스턴스에서 지원하며 워크로드·런타임 조건 확인 필요 |
| DaemonSet | 지원 | 미지원 | 관리형 노드 제한 범위에서 지원 |
| 비용 제어 | requests·인스턴스 선택·autoscaler 정책 | Pod 리소스 크기와 복제본 | requests·허용 용량·consolidation 정책, 절감액 보장 없음 |
| 호스트 사용자 설정 | AMI·launch template 선택지 | 호스트 설정 불가 | AWS 관리 Bottlerocket 변형과 지원되는 NodeClass 설정 |
## 내부 아키텍처와 동작 원리
Karpenter 기반 컨트롤러는 워커 노드 외부에서 AWS가 운영합니다. 그림은 개념도입니다. 스케줄러가 스케줄 불가능한 Pod를 식별하고 이후 노드에 바인딩하며 API 서버는 객체를 저장합니다. Pending 상태라는 사실만으로 노드 추가가 문제를 해결한다고 판단할 수는 없습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-auto-mode-readme-0.html)
## 지원 리전 및 제한 사항
### 리전과 지원 버전
[EKS FAQ](https://aws.amazon.com/eks/faqs/)는 중국 리전을 제외한 EKS 리전에서 Auto Mode를 제공하며 GovCloud(US)를 포함한다고 안내합니다. 인스턴스와 개별 기능의 리전별 가용성은 별도로 확인하세요. 이전의 짧은 리전 목록은 전체 목록이 아니었습니다.
FAQ의 초기 기능 지원 하한인 `1.29+`는 그 이후 모든 버전을 현재 생성할 수 있거나 표준 지원 중이라는 뜻이 아닙니다. 2026년 9월 12일 기준 EKS 1.34–1.36이 표준 지원 대상이며 예제는 1.36을 사용합니다. 수명 주기와 확장 지원 요금은 [현재 EKS 버전 일정](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html)에서 확인하세요.
### 확인할 제약
| 항목 | 확인 사항 |
|------|----------|
| 용량과 규모 | 적용된 EKS/EC2 할당량, 서브넷 IP 용량과 워크로드 제약을 확인하고 목표 규모를 해당 환경에서 검증합니다 |
| NodePool limits | 사용자가 지정한 리소스 제한과 AWS 계정·서비스 할당량은 다릅니다. 확장 규모와 노드 교체 여유를 검증하세요 |
| 운영 체제 | AWS가 관리하는 Bottlerocket 변형을 선택합니다. AL2023이나 임의의 커스텀 AMI는 Auto Mode의 AMI 선택지가 아닙니다 |
| Windows | Auto Mode는 Windows 노드를 제공하지 않습니다. 필요하면 호환되는 별도 노드 그룹을 사용하세요 |
| DNS와 스토리지 | Auto Mode 노드는 로컬 CoreDNS를 제공합니다. 혼합 클러스터는 다른 노드를 위해 CoreDNS Deployment를 유지합니다. 관리형 노드 디스크 암호화가 모든 동적 PVC 암호화를 뜻하지 않으므로 StorageClass에 명시하세요 |
[Service Quotas](https://docs.aws.amazon.com/eks/latest/userguide/service-quotas.html)와 [NodeClass 참조](https://docs.aws.amazon.com/eks/latest/userguide/create-node-class.html)에서 관련 제약을 확인하세요. 이번 감사에서 실제 계정 할당량이나 인스턴스 용량은 조회하지 않았습니다.
---
## 다음 단계
EKS Auto Mode를 성공적으로 구성한 후 다음 주제를 학습하는 것이 좋습니다:
1. **[EKS 비용 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/eks/07-eks-cost-optimization.md)**: Spot, Savings Plans, 리소스 최적화
2. **[EKS 모니터링 및 로깅](https://www.atomai.click/kubernetes-docs/llms/ko/eks/06-eks-monitoring-logging.md)**: CloudWatch, Prometheus, Grafana
3. **[EKS 보안](https://www.atomai.click/kubernetes-docs/llms/ko/eks/05-eks-security.md)**: IAM, 네트워크 정책, Pod 보안
4. **[Karpenter 심화](https://www.atomai.click/kubernetes-docs/llms/ko/autoscaling/02-karpenter.md)**: 직접 Karpenter 설치 및 고급 기능
## 관련 퀴즈
학습 내용을 테스트하려면 [EKS Auto Mode 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks-auto-mode/01-getting-started-quiz)를 풀어보세요.
---
## 참고 자료
- [AWS EKS Auto Mode 공식 문서](https://docs.aws.amazon.com/eks/latest/userguide/automode.html)
- [Karpenter 공식 문서](https://karpenter.sh/)
- [EKS Best Practices Guide](https://docs.aws.amazon.com/eks/latest/best-practices/)
- [AWS 비용 최적화 가이드](https://aws.amazon.com/ko/pricing/cost-optimization/)
- [New EKS Auto Mode features for enhanced security, network control, and performance (AWS Containers Blog, 2025-10-16)](https://aws.amazon.com/blogs/containers/new-amazon-eks-auto-mode-features-for-enhanced-security-network-control-and-performance/)
- [Migrate from self-managed Karpenter to EKS Auto Mode](https://docs.aws.amazon.com/eks/latest/userguide/auto-migrate-karpenter.html)
- [Auto Mode 아키텍처와 책임 범위](https://docs.aws.amazon.com/eks/latest/userguide/automode.html)
- [EKS Fargate 제한](https://docs.aws.amazon.com/eks/latest/userguide/fargate.html)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-auto-mode/01-getting-started
----------------------------------------
# Auto Mode 시작하기
> **지원 버전**: EKS Auto Mode GA; 예제 기준 EKS 1.36
> **마지막 업데이트**: 2026년 9월 12일
새 클러스터 생성과 기존 클러스터의 Auto Mode 활성화를 다룹니다. 생성 방법은 **하나만** 선택하세요. 세 방법을 모두 실행하면 각각 과금되는 인프라가 만들어집니다. 기존 클러스터 절차는 의도한 대상 클러스터에만 적용합니다.
예제는 상용 서울 리전과 사전에 생성·검토한 IAM 역할을 사용합니다. eksctl 0.229.0, Terraform 1.15.7/AWS provider 6.64.0, CDK 2.269.0/constructs 10.5.0으로 로컬 검증과 합성을 수행했습니다. **이번 감사에서 실제 클러스터 생성이나 마이그레이션은 실행하지 않았습니다.** IAM/SCP 권한, 할당량, 라우팅, 인스턴스 가용성과 워크로드 호환성은 환경별 검증이 필요합니다. 프로덕션 실행을 검증한 배포 예제가 아닙니다.
검토일에 EKS 1.36은 표준 지원 대상입니다. 초기 Auto Mode 기능 지원 하한인 1.29가 현재 EKS 지원 여부를 보장하지는 않습니다. 실행 전에 AWS 버전 일정을 확인하세요. `STANDARD` 업그레이드 정책은 표준 지원 종료 후 자동 업그레이드를 허용하며 과금을 중지하지 않습니다.
## 사전 요구 사항과 IAM 역할
임시 역할 자격 증명, AWS CLI v2, EKS 1.36과 호환되는 kubectl, Bash, Python 3, jq를 준비합니다. 관리자에게 아래의 서로 다른 역할과 호출자의 프로비저닝/PassRole 권한을 검토받으세요.
| 역할 | 신뢰 관계와 권한 |
|------|-----------------|
| 클러스터 역할 | `eks.amazonaws.com`에 `sts:AssumeRole`과 `sts:TagSession`을 허용합니다. 현재 AWS 권장 정책은 `AmazonEKSClusterPolicy`, `AmazonEKSComputePolicy`, `AmazonEKSBlockStoragePolicyV2`, `AmazonEKSLoadBalancingPolicy`, `AmazonEKSNetworkingPolicy`입니다 |
| Auto Mode 노드 역할 | `ec2.amazonaws.com`을 신뢰하며 `AmazonEKSWorkerNodeMinimalPolicy`와 `AmazonEC2ContainerRegistryPullOnly`를 연결합니다 |
| 워크로드 역할 | EKS Pod Identity 등으로 애플리케이션 권한을 별도로 부여합니다. 노드 역할에 애플리케이션 권한을 넣지 않습니다 |
기존 클러스터를 Auto Mode로 전환할 때 클러스터 역할 ARN 자체는 교체할 수 없습니다. 그 역할을 관리하는 IaC 절차로 승인된 정책·신뢰 관계를 갱신하세요. 공유 역할의 신뢰 정책을 무작정 덮어쓰지 마세요. 아래 코드는 검토된 역할을 참조하며 생성하지 않습니다. 기존 볼륨이 있다면 스토리지 정책을 바꾸기 전에 추가 마이그레이션 검토가 필요할 수 있습니다.
Auto Mode는 Pod Identity agent 기능을 제공합니다. Auto Mode 활성화만을 위해 IAM OIDC provider를 만들 필요는 없습니다. 애플리케이션이 IRSA를 사용한다면 OIDC를 별도로 구성하세요.
### 공통 계정과 로컬 컨텍스트
필수 환경 변수를 승인된 실제 값으로 설정합니다. 새 클러스터에는 고유한 이름을 사용하고, 기존 클러스터에는 식별 정보와 소유권을 확인합니다. 이후 명령도 이 전용 Bash 세션에서 실행하고 생성된 디렉터리를 비공개로 보관하세요.
```bash
set -euo pipefail
: "${EXPECTED_ACCOUNT_ID:?Set the approved account ID}"
: "${CLUSTER_NAME:?Set a unique new name, or the approved existing cluster name}"
: "${AUTO_CLUSTER_ROLE_ARN:?Set the reviewed EKS cluster role ARN}"
: "${AUTO_NODE_ROLE_ARN:?Set the reviewed Auto Mode node role ARN}"
: "${API_CLIENT_CIDR:?Set the approved public client IPv4 CIDR}"
export AWS_REGION="${AWS_REGION:-ap-northeast-2}"
test "$AWS_REGION" = ap-northeast-2 || { printf 'This concrete example uses Seoul subnets/AZs; adapt it before using another region.\n' >&2; exit 1; }
export AWS_DEFAULT_REGION="$AWS_REGION"
export EXPECTED_ACCOUNT_ID CLUSTER_NAME AUTO_CLUSTER_ROLE_ARN AUTO_NODE_ROLE_ARN API_CLIENT_CIDR
python3 - <<'PY'
import ipaddress, os, re
account = os.environ["EXPECTED_ACCOUNT_ID"]
if not re.fullmatch(r"[0-9]{12}", account):
raise SystemExit("Invalid account")
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,99}", os.environ["CLUSTER_NAME"]):
raise SystemExit("Invalid cluster name")
for key in ("AUTO_CLUSTER_ROLE_ARN", "AUTO_NODE_ROLE_ARN"):
if not re.fullmatch(r"arn:aws:iam::" + account + r":role/[A-Za-z0-9+=,.@_/-]+", os.environ[key]):
raise SystemExit("Role account/ARN mismatch: " + key)
network = ipaddress.ip_network(os.environ["API_CLIENT_CIDR"], strict=True)
if network.version != 4 or network.prefixlen < 24:
raise SystemExit("Use a reviewed /24 or narrower IPv4 CIDR")
PY
check_account() {
local account
account=$(aws sts get-caller-identity --region "$AWS_REGION" --query Account --output text) || return
test "$account" = "$EXPECTED_ACCOUNT_ID" || { printf 'Wrong AWS account; stop.\n' >&2; return 1; }
}
check_account
umask 077
export WORK_DIR
WORK_DIR=$(mktemp -d "$PWD/auto-mode.XXXXXXXX")
export KUBECONFIG="$WORK_DIR/kubeconfig"
printf 'Private evidence/config directory: %s\n' "$WORK_DIR"
```
## 새 클러스터 생성
### 승인된 프라이빗 서브넷을 사용하는 eksctl
이 예제는 검토된 VPC와 서로 다른 AZ의 프라이빗 서브넷 2개를 재사용합니다. 먼저 `VPC_ID`, `PRIVATE_SUBNET_A`, `PRIVATE_SUBNET_B`를 설정하세요. 서브넷 라우팅 테이블이 NAT 또는 적절한 엔드포인트를 통해 필요한 서비스·이미지 접근을 제공하는지 확인합니다. `MapPublicIpOnLaunch=false`만으로 프라이빗 라우팅이 입증되지는 않습니다.
선택한 프라이빗 서브넷을 클러스터 서브넷으로도 사용합니다. 기본 Auto Mode NodeClass가 클러스터 서브넷 선택을 상속하기 때문입니다. 퍼블릭 클러스터 서브넷을 가진 일반적인 eksctl 클러스터에서는 Auto Mode 노드도 해당 서브넷에 생성될 수 있습니다.
```bash
# Use approved PRIVATE subnets in the same VPC and at least two AZs.
: "${VPC_ID:?Set the approved VPC ID}"
: "${PRIVATE_SUBNET_A:?Set the first private subnet ID}"
: "${PRIVATE_SUBNET_B:?Set the second private subnet ID in another AZ}"
export VPC_ID
check_account
aws ec2 describe-subnets --region "$AWS_REGION" \
--subnet-ids "$PRIVATE_SUBNET_A" "$PRIVATE_SUBNET_B" > "$WORK_DIR/subnets.json"
python3 - <<'PY'
import json, os
from pathlib import Path
out = Path(os.environ["WORK_DIR"])
subnets = json.loads((out / "subnets.json").read_text())["Subnets"]
if len(subnets) != 2 or len({s["AvailabilityZone"] for s in subnets}) != 2:
raise SystemExit("Two subnets in distinct AZs are required")
if not all(s["VpcId"] == os.environ["VPC_ID"] and
s["OwnerId"] == os.environ["EXPECTED_ACCOUNT_ID"] and
s["State"] == "available" and not s["MapPublicIpOnLaunch"] for s in subnets):
raise SystemExit("Subnet account/VPC/state/public-IP settings do not match")
config = {
"apiVersion": "eksctl.io/v1alpha5", "kind": "ClusterConfig",
"metadata": {"name": os.environ["CLUSTER_NAME"], "region": os.environ["AWS_REGION"], "version": "1.36"},
"accessConfig": {"authenticationMode": "API"},
"upgradePolicy": {"supportType": "STANDARD"},
"iam": {"serviceRoleARN": os.environ["AUTO_CLUSTER_ROLE_ARN"], "withOIDC": False},
"autoModeConfig": {"enabled": True, "nodePools": ["general-purpose", "system"],
"nodeRoleARN": os.environ["AUTO_NODE_ROLE_ARN"]},
"vpc": {"id": os.environ["VPC_ID"],
"controlPlaneSubnetIDs": [s["SubnetId"] for s in subnets],
"subnets": {"private": {s["AvailabilityZone"]: {"id": s["SubnetId"]} for s in subnets}},
"clusterEndpoints": {"publicAccess": True, "privateAccess": True},
"publicAccessCIDRs": [os.environ["API_CLIENT_CIDR"]]}
}
(out / "cluster.json").write_text(json.dumps(config, indent=2) + "\n")
PY
# REVIEW cluster.json and routes/permissions first. Creates billed EKS resources.
check_account
eksctl create cluster --config-file "$WORK_DIR/cluster.json" \
--write-kubeconfig=false --timeout=45m
```
현재 eksctl 스키마에서 `autoModeConfig.nodePools`는 유효합니다. eksctl은 Auto Mode 활성화 시 compute, load balancing, block storage를 함께 구성하며, 기본 풀에는 제공한 노드 역할을 사용합니다. kubeconfig는 뒤의 검증 절차에서 생성합니다.
생성이 실패하면 구성을 보관하고 해당 이름의 CloudFormation 스택을 확인하세요. timeout이 리소스 미생성을 뜻하지 않습니다. 재시도나 삭제 전에 소유권과 부분 생성 리소스를 확인합니다.
### Terraform
다음 내용을 전용 디렉터리의 `main.tf`로 저장합니다. 이 방법은 VPC와 과금되는 NAT Gateway 하나를 만듭니다. 이는 실습의 가용성·비용 선택이며 AZ별 NAT 이중화가 아닙니다. CIDR과 AZ를 검토해 충돌을 방지하세요.
EKS 모듈 21.25.0과 VPC 모듈 5.21.0을 고정합니다. EKS 모듈에는 AWS provider **6.59 이상**이 필요하며 생성된 의존성 lock 파일을 보관해야 합니다. 21.25.0은 클러스터 역할을 직접 만들 때 이전 `AmazonEKSBlockStoragePolicy`를 연결하므로 이 예제는 사전 요구 사항의 검토된 역할을 제공합니다. 별도의 고객 관리 KMS 키를 생성하지 않고 EKS 기본 API 데이터 암호화를 사용합니다.
```hcl
terraform {
required_version = ">= 1.5.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 6.59, < 7.0"
}
}
}
variable "expected_account_id" {
type = string
validation {
condition = can(regex("^[0-9]{12}$", var.expected_account_id))
error_message = "Set the intended 12-digit AWS account."
}
}
variable "cluster_name" { type = string }
variable "auto_cluster_role_arn" { type = string }
variable "auto_node_role_arn" { type = string }
variable "api_client_cidr" {
type = string
validation {
condition = can(cidrnetmask(var.api_client_cidr)) && try(
tonumber(split("/", var.api_client_cidr)[1]) >= 24, false
)
error_message = "Use an approved narrow IPv4 CIDR (/24 through /32)."
}
}
provider "aws" {
region = "ap-northeast-2"
allowed_account_ids = [var.expected_account_id]
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.21.0"
name = "${var.cluster_name}-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
enable_dns_hostnames = true
enable_dns_support = true
public_subnet_tags = { "kubernetes.io/role/elb" = "1" }
private_subnet_tags = { "kubernetes.io/role/internal-elb" = "1" }
}
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "21.25.0"
name = var.cluster_name
kubernetes_version = "1.36"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
endpoint_public_access = true
endpoint_private_access = true
endpoint_public_access_cidrs = [var.api_client_cidr]
authentication_mode = "API"
enable_cluster_creator_admin_permissions = true # Dedicated lab creator only.
upgrade_policy = { support_type = "STANDARD" }
# Reviewed pre-created roles: this module release still attaches the older
# AmazonEKSBlockStoragePolicy when it creates the cluster role itself.
create_iam_role = false
iam_role_arn = var.auto_cluster_role_arn
create_node_iam_role = false
compute_config = {
enabled = true
node_pools = ["general-purpose", "system"]
node_role_arn = var.auto_node_role_arn
}
# EKS default API-data encryption; no separate customer-managed KMS key here.
create_kms_key = false
encryption_config = null
enable_irsa = false # Pod Identity does not require a cluster OIDC provider.
tags = { Environment = "lab", Terraform = "true" }
}
output "cluster_endpoint" { value = module.eks.cluster_endpoint }
output "cluster_name" { value = module.eks.cluster_name }
```
모듈의 `compute_config` 입력은 대응하는 EKS compute·storage·load-balancing 리소스 블록을 함께 구성합니다. 모듈 v20의 이전 `cluster_compute_config` 이름과 다릅니다. 전용 실습을 위해 생성자의 관리자 접근을 명시했습니다. 프로덕션에서는 필요한 범위로 검토한 access entry를 정의하세요.
```bash
python3 - <<'PY'
import json, os
from pathlib import Path
fields = {"expected_account_id": "EXPECTED_ACCOUNT_ID", "cluster_name": "CLUSTER_NAME",
"auto_cluster_role_arn": "AUTO_CLUSTER_ROLE_ARN", "auto_node_role_arn": "AUTO_NODE_ROLE_ARN",
"api_client_cidr": "API_CLIENT_CIDR"}
(Path(os.environ["WORK_DIR"]) / "terraform.tfvars.json").write_text(
json.dumps({key: os.environ[value] for key, value in fields.items()}, indent=2) + "\n")
PY
check_account
terraform init
terraform validate
terraform plan -var-file="$WORK_DIR/terraform.tfvars.json" -out="$WORK_DIR/tfplan"
# REVIEW the saved plan; this applies billed infrastructure changes.
terraform apply "$WORK_DIR/tfplan"
```
### AWS CDK
승인된 계정과 `ap-northeast-2`를 대상으로 하는 CDK 애플리케이션에서 사용합니다. CloudFormation 파라미터 `ClusterName`, `ClusterRoleArn`, `NodeRoleArn`, `OperatorRoleArn`, `ApiClientCidr`를 지정하세요. Operator 파라미터에는 kubectl에 사용할 자격 증명의 검토된 IAM 역할 ARN을 입력합니다. STS assumed-role 세션 ARN을 사용하지 않습니다.
실제 **L1 `eks.CfnCluster`**를 사용합니다. 기존 `eks.Cluster` L2의 `defaultChild`를 `CfnCluster`로 형 변환해도 해당 custom resource가 `AWS::EKS::Cluster`로 바뀌거나 Auto Mode가 올바르게 설정되는 것은 아닙니다. 여기서는 세 Auto Mode 기능, API 접근과 노드 역할을 명시합니다. Access entry로 검토된 실습 운영자에게 Kubernetes 관리자 접근을 부여하며, CloudFormation 실행 주체의 bootstrap 접근은 비활성화합니다. 프로덕션에서는 이 실습용 클러스터 전체 권한을 적절한 접근 범위로 대체하세요. 참조한 IAM 역할은 이 스택의 수명 주기에 포함되지 않습니다.
```typescript
import * as cdk from 'aws-cdk-lib/core';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as eks from 'aws-cdk-lib/aws-eks';
import { Construct } from 'constructs';
export class EksAutoModeStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Pre-created, reviewed roles; see the IAM prerequisites in this chapter.
const clusterRole = new cdk.CfnParameter(this, 'ClusterRoleArn', { type: 'String' });
const nodeRole = new cdk.CfnParameter(this, 'NodeRoleArn', { type: 'String' });
const operatorRole = new cdk.CfnParameter(this, 'OperatorRoleArn', {
type: 'String',
allowedPattern: '^arn:aws:iam::[0-9]{12}:role/[A-Za-z0-9+=,.@_/-]+$',
constraintDescription: 'The reviewed lab operator IAM role ARN, not an STS session ARN',
});
const clientCidr = new cdk.CfnParameter(this, 'ApiClientCidr', {
type: 'String',
allowedPattern: '^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}/(?:2[4-9]|3[0-2])$',
constraintDescription: 'An approved narrow IPv4 CIDR (/24 through /32)',
});
const clusterName = new cdk.CfnParameter(this, 'ClusterName', { type: 'String' });
const vpc = new ec2.Vpc(this, 'EksVpc', {
maxAzs: 3,
natGateways: 1, // Lab tradeoff: billed, and not per-AZ NAT redundancy.
subnetConfiguration: [
{ cidrMask: 24, name: 'Public', subnetType: ec2.SubnetType.PUBLIC },
{ cidrMask: 24, name: 'Private', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
],
});
for (const subnet of vpc.publicSubnets) {
cdk.Tags.of(subnet).add('kubernetes.io/role/elb', '1');
}
for (const subnet of vpc.privateSubnets) {
cdk.Tags.of(subnet).add('kubernetes.io/role/internal-elb', '1');
}
// An actual L1 AWS::EKS::Cluster, not an L2 defaultChild cast.
const cluster = new eks.CfnCluster(this, 'EksAutoModeCluster', {
name: clusterName.valueAsString,
version: '1.36',
roleArn: clusterRole.valueAsString,
accessConfig: {
authenticationMode: 'API',
bootstrapClusterCreatorAdminPermissions: false,
},
resourcesVpcConfig: {
subnetIds: vpc.privateSubnets.map(subnet => subnet.subnetId),
endpointPrivateAccess: true,
endpointPublicAccess: true,
publicAccessCidrs: [clientCidr.valueAsString],
},
computeConfig: {
enabled: true,
nodePools: ['general-purpose', 'system'],
nodeRoleArn: nodeRole.valueAsString,
},
kubernetesNetworkConfig: { elasticLoadBalancing: { enabled: true } },
storageConfig: { blockStorage: { enabled: true } },
upgradePolicy: { supportType: 'STANDARD' },
});
// CloudFormation's execution role may differ from the interactive operator.
new eks.CfnAccessEntry(this, 'LabOperatorAccess', {
clusterName: cluster.ref,
principalArn: operatorRole.valueAsString,
type: 'STANDARD',
accessPolicies: [{
policyArn: cdk.Fn.sub('arn:${AWS::Partition}:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy'),
accessScope: { type: 'cluster' }, // Explicit, reviewed admin access for this lab only.
}],
});
new cdk.CfnOutput(this, 'ClusterNameOutput', { value: cluster.ref });
new cdk.CfnOutput(this, 'ClusterEndpoint', { value: cluster.attrEndpoint });
}
}
```
CDK 앱에서 `EksAutoModeStack`을 생성하고 `StackProps.env`에 대상 계정·리전을 지정합니다. 승인된 배포 전에 `cdk synth`와 `cdk diff`를 검토하세요. 로컬 감사에서는 TypeScript 컴파일과 합성된 클러스터 속성을 확인했으며 `cdk deploy`는 실행하지 않았습니다.
## 기존 클러스터에서 Auto Mode 활성화
Auto Mode 활성화와 워크로드 이동은 별개의 작업입니다. 먼저 다음 사항을 확인하세요.
- Terraform/CDK/eksctl 등 IaC로 관리하는 클러스터라면 해당 구성을 갱신해 비관리 변경을 피합니다.
- 기존 클러스터 역할에 앞에서 설명한 Auto Mode 권한과 `sts:TagSession` 신뢰 관계가 있어야 합니다. 예제는 그 ARN이 검토한 역할과 같은지 확인합니다.
- 설치된 add-on의 현재 호환 버전과 [마이그레이션 최소 요구 버전](https://docs.aws.amazon.com/eks/latest/userguide/auto-enable-existing.html)을 확인합니다. 과거 최소 버전이 그 오래된 빌드를 새로 설치하라는 권고는 아닙니다.
- API 인증 활성화 전에 access entry 전환을 계획합니다. `CONFIG_MAP`에서 `API_AND_CONFIG_MAP`으로의 변경은 단방향이며, 기존 ConfigMap 경로를 유지하면서 access entry를 추가합니다.
- 기존 워커 그룹과 non-Auto 노드용 CoreDNS Deployment를 유지합니다. 지원되는 CNI·네트워크 구성을 검토하세요. Auto Mode 활성화만으로 기존 EBS 볼륨이나 로드 밸런서가 이전되지는 않습니다.
eksctl 관리 클러스터의 현재 명령은 `eksctl update auto-mode-config --config-file `입니다. `eksctl update cluster --enable-auto-mode`가 아닙니다. 기능 활성화만을 위해 `--drain-all-nodegroups`를 추가하지 마세요.
아래 AWS CLI 대안은 같은 요청에서 compute, load balancing과 block storage를 활성화합니다. 아직 Auto Mode를 활성화하지 않은 클러스터를 대상으로 합니다. 요청 수락을 완료로 간주하지 않고 update ID를 보관해 최종 결과를 기다립니다.
```bash
check_account
aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--output json > "$WORK_DIR/before.json"
jq -e --arg role "$AUTO_CLUSTER_ROLE_ARN" '
.cluster.status == "ACTIVE" and .cluster.roleArn == $role and
.cluster.computeConfig.enabled != true
' "$WORK_DIR/before.json" >/dev/null
wait_update() {
local id=$1 status
for attempt in $(seq 1 120); do
aws eks describe-update --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--update-id "$id" --output json > "$WORK_DIR/update-$id.json" || return
status=$(jq -er '.update.status' "$WORK_DIR/update-$id.json") || return
case "$status" in
Successful) return 0 ;;
Failed|Cancelled) printf 'Update %s: %s; inspect the private response.\n' "$id" "$status" >&2; return 1 ;;
InProgress) sleep 15 ;;
*) printf 'Unknown update state; stop.\n' >&2; return 1 ;;
esac
done
printf 'Update wait timed out; do not assume completion or resubmit blindly.\n' >&2
return 1
}
# One-way authentication migration; review existing access before running.
mode=$(jq -er '.cluster.accessConfig.authenticationMode' "$WORK_DIR/before.json")
case "$mode" in
CONFIG_MAP)
auth_id=$(aws eks update-cluster-config --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--access-config authenticationMode=API_AND_CONFIG_MAP --query update.id --output text)
wait_update "$auth_id"
;;
API|API_AND_CONFIG_MAP) ;;
*) printf 'Unknown authentication mode; stop.\n' >&2; exit 1 ;;
esac
compute=$(jq -nc --arg role "$AUTO_NODE_ROLE_ARN" \
'{enabled:true,nodePools:["general-purpose","system"],nodeRoleArn:$role}')
# MUTATION: compute, load balancing and block storage change together.
check_account
update_id=$(aws eks update-cluster-config --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--compute-config "$compute" \
--kubernetes-network-config '{"elasticLoadBalancing":{"enabled":true}}' \
--storage-config '{"blockStorage":{"enabled":true}}' \
--query update.id --output text)
wait_update "$update_id"
```
업데이트 실패·timeout·알 수 없는 상태가 발생하면 비공개 응답을 확인하고 작업 상태를 정리한 뒤 재시도하세요. 기본 풀의 노드 역할은 compute 활성화 후 임의로 바꿀 수 없습니다. 노드 ID 구성을 바꾸려면 문서화된 NodeClass/access-entry 절차를 따릅니다.
## AWS 콘솔
동일한 IAM, add-on과 접근 사전 요구 사항을 확인한 뒤 클러스터의 **EKS Auto Mode → Manage**에서 기능을 활성화하고 기본 풀과 검토된 노드 역할을 선택합니다. 업데이트 결과를 확인하세요. 새 클러스터 생성 화면에서도 대응하는 설정을 제공합니다. 콘솔 배치는 바뀔 수 있으며, 필수 API 기능과 IAM 역할 확인이 핵심입니다.
## 활성화 검증
선택한 생성 방법이 성공하거나 기존 클러스터 업데이트가 완료된 뒤 실행합니다.
```bash
check_account
aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--query 'cluster.{status:status,compute:computeConfig,network:kubernetesNetworkConfig,storage:storageConfig,access:accessConfig}' \
--output json > "$WORK_DIR/after.json"
jq -e '.status == "ACTIVE" and .compute.enabled == true and
.network.elasticLoadBalancing.enabled == true and .storage.blockStorage.enabled == true' \
"$WORK_DIR/after.json"
aws eks update-kubeconfig --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--kubeconfig "$KUBECONFIG" --alias "$CLUSTER_NAME"
kubectl --context "$CLUSTER_NAME" --request-timeout=15s get nodepools,nodeclasses
kubectl --context "$CLUSTER_NAME" wait nodepool/general-purpose nodepool/system \
--for=condition=Ready --timeout=300s
kubectl --context "$CLUSTER_NAME" --request-timeout=15s get nodes -L eks.amazonaws.com/compute-type
```
유휴 클러스터는 적합한 워크로드에 용량이 필요해질 때까지 Auto Mode 노드가 없을 수 있습니다. NodePool readiness가 애플리케이션 가용성 검증을 대신하지는 않습니다. 제약을 지정한 테스트 워크로드로 NodeClaim·노드·애플리케이션 readiness를 확인한 뒤 테스트 워크로드를 제거하세요.
Auto Mode 노드는 CoreDNS를 로컬 시스템 서비스로 실행합니다. 순수 Auto Mode로 워크로드를 옮긴 뒤에는 기존 Deployment를 제거할 수 있지만, **Auto/non-Auto 혼합 클러스터에서는 다른 노드를 위해 유지해야 합니다.**
## 정리와 다음 단계
필요한 데이터를 내보내고 보존 정책에 따라 애플리케이션 로드 밸런서/PVC를 정리한 뒤, 선택한 생성 방법이 소유한 리소스만 삭제합니다. 상황에 맞게 `--wait`를 포함한 eksctl 삭제, 검토한 Terraform destroy plan 또는 검토한 CDK 스택 삭제를 사용하세요. 예제는 기존 IAM 역할을 참조하며 eksctl 예제는 VPC도 재사용합니다. 예제가 생성하지 않은 공유 사전 요구 리소스를 함께 삭제하지 마세요.
기존 클러스터의 기능 활성화는 일회용 클러스터 실습이 아닙니다. 워크로드, 스토리지, DNS와 트래픽 이전을 검증하기 전에 기존 워커 그룹이나 컨트롤러를 제거하지 마세요. 작업이 중단돼도 노드, 볼륨, 로드 밸런서, NAT와 컨트롤 플레인 비용이 계속 발생할 수 있습니다. 고정 sleep에 의존하지 말고 잔여 리소스를 확인합니다.
- [NodePool 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/02-nodepool-configuration.md)
- [관리형 노드 그룹 마이그레이션](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/09-migration-guide.md)
- [시작하기 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/eks-auto-mode/01-getting-started-quiz)
## 참고 자료
- [Auto Mode CLI creation and IAM roles](https://docs.aws.amazon.com/eks/latest/userguide/automode-get-started-cli.html)
- [Enable Auto Mode on an existing cluster](https://docs.aws.amazon.com/eks/latest/userguide/auto-enable-existing.html)
- [eksctl Auto Mode configuration](https://docs.aws.amazon.com/eks/latest/eksctl/auto-mode.html)
- [EKS support calendar](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html)
- [Terraform EKS module v21.25.0](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/v21.25.0)
- [CDK CfnCluster API](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_eks.CfnCluster.html)
- [IAM principal access through EKS access entries](https://docs.aws.amazon.com/eks/latest/userguide/access-entries.html)
- [Auto Mode networking and DNS](https://docs.aws.amazon.com/eks/latest/userguide/auto-networking.html)
- [Migration boundaries](https://docs.aws.amazon.com/eks/latest/userguide/migrate-auto.html)
< [이전: 목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md) | [다음: NodePool 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/02-nodepool-configuration.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-auto-mode/02-nodepool-configuration
----------------------------------------
# NodePool 구성 및 최적화
> **지원 버전**: EKS Auto Mode GA; 예제 기준 EKS 1.36
> **마지막 업데이트**: 2026년 9월 12일
AWS 관리 기본 풀, 커스텀 NodePool 제약과 AWS 전용 NodeClass API를 구분합니다. 먼저 [시작하기](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/01-getting-started.md)를 완료하고 대상 계정·컨텍스트를 확인하세요. 적용 전에 모든 IAM/profile 이름과 네트워크 태그를 검토합니다. 예제의 용량 제한은 설명용이며 상당한 비용을 허용할 수도 있습니다.
NodePool 예제는 릴리스된 Karpenter 1.14.1 CRD의 구조 스키마로 검사하고 AWS 전용 NodeClass 필드는 AWS 문서와 대조했습니다. 이것이 Auto Mode 내부 컨트롤러 버전을 특정하거나 실제 클러스터의 admission/readiness를 입증하지는 않습니다. 이번 감사에서 노드를 프로비저닝하지 않았습니다.
## 기본 NodePool 이해
활성화하면 다음 풀을 제공합니다. AWS 관리 구성을 직접 수정하기보다 별도의 커스텀 풀을 만드세요.
| 풀 | 아키텍처 | 용량과 인스턴스 선택 | 용도 |
|----|----------|----------------------|------|
| `general-purpose` | `amd64` | On-Demand, C/M/R 계열, 5세대 이상 | 범용 워크로드 |
| `system` | `amd64`, `arm64` | On-Demand, C/M/R 계열, 5세대 이상 | `CriticalAddonsOnly`를 toleration으로 허용한 클러스터 중요 워크로드 |
기본 general-purpose 풀은 Spot을 활성화하지 않습니다. Spot이나 다른 아키텍처·인스턴스 제약이 필요하면 커스텀 풀을 사용하세요. Auto Mode는 노드 로컬 DNS와 서비스 네트워킹 기능을 제공하므로 순수 Auto Mode 클러스터에서 system 풀을 채우기 위해 일반 CoreDNS/kube-proxy Pod를 배치할 필요는 없습니다. 혼합 클러스터에서는 non-Auto 노드용 기존 CoreDNS Deployment가 필요합니다.
추측한 기본 YAML을 적용하지 말고 실제 관리 객체에서 disruption 설정과 taint 세부 값을 확인하세요.
```bash
kubectl --context "$CLUSTER_NAME" get nodepool general-purpose system -o yaml
kubectl --context "$CLUSTER_NAME" get nodeclass default -o yaml
```
AWS가 `default` NodeClass를 제공하려면 기본 풀 중 하나 이상이 활성화돼야 합니다. 둘 다 끄면 직접 NodeClass를 만들고 아래 참조를 바꿔야 합니다. `computeConfig.nodePools`에서 기본 풀 이름을 제거하면 해당 풀과 노드가 drain·종료됩니다. 새 워크로드에서만 숨기는 설정이 아닙니다.
## 커스텀 NodePool 생성
아래 예제는 배치 제약에 집중하도록 기존 `default` NodeClass를 참조합니다. 뒤에서 만드는 커스텀 NodeClass를 사용하려면 먼저 생성하고 대상 풀의 `nodeClassRef.name`을 바꾸세요. `template` 아래 label·taint가 새 노드에 적용되며, NodePool metadata에만 붙인 label은 노드 label이 아닙니다.
### 컴퓨팅 최적화 워크로드
`Gt ["6"]` 조건은 7세대 이상을 허용합니다. 최신 세대 하나만 선택하는 조건이 아닙니다. 예제는 x86 C 계열 On-Demand를 허용하고 메모리 예제보다 높은 프로비저닝 선호도를 지정합니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: compute-optimized
labels:
workload-type: compute-intensive
spec:
template:
metadata:
labels:
workload-type: compute-intensive
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- c
- key: eks.amazonaws.com/instance-generation
operator: Gt
values:
- '6'
- key: eks.amazonaws.com/instance-size
operator: In
values:
- xlarge
- 2xlarge
- 4xlarge
- key: kubernetes.io/arch
operator: In
values:
- amd64
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
limits:
cpu: '1000'
memory: 4000Gi
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m
weight: 10
```
### 메모리 최적화 워크로드
6세대 이상 R 계열에서 두 아키텍처를 허용합니다. 스케줄러가 선택할 수 있는 아키텍처를 컨테이너 이미지와 애플리케이션 의존성도 지원해야 합니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: memory-optimized
labels:
workload-type: memory-intensive
spec:
template:
metadata:
labels:
workload-type: memory-intensive
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- r
- key: eks.amazonaws.com/instance-generation
operator: Gt
values:
- '5'
- key: eks.amazonaws.com/instance-size
operator: In
values:
- 2xlarge
- 4xlarge
- 8xlarge
- 12xlarge
- key: kubernetes.io/arch
operator: In
values:
- amd64
- arm64
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
limits:
cpu: '500'
memory: 8000Gi
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 10m
weight: 5
```
## AWS NodeClass 구성
Auto Mode는 `eks.amazonaws.com/v1`의 `NodeClass`를 사용합니다. 자체 관리 AWS Karpenter의 `EC2NodeClass`와 필드가 다릅니다. `amiFamily`, `blockDeviceMappings`, 임의의 셸 `userData`, `metadataOptions`를 이 API에 복사하지 마세요. AWS가 관리형 Bottlerocket 변형을 선택합니다.
다음 내용을 `custom-nodeclass.yaml`로 저장하고 예시 profile·selector 값을 검토된 리소스로 바꿉니다. `instanceProfile`은 지원되며 이름은 `eks`로 시작해야 합니다. 대신 `role`에 노드 IAM 역할 이름을 지정할 수도 있습니다. **`role`과 `instanceProfile` 중 하나만 지정하세요.** Profile에 담긴 역할에는 적절한 Auto Mode EKS access entry와 권한이 필요합니다. 이미 구성된 역할을 재사용하면 entry를 중복 생성하지 않습니다. 새 역할은 AWS NodeClass 참조의 access-entry 절차를 따르세요.
서브넷 태그는 리소스를 선택할 뿐 라우팅이나 격리를 입증하지 않습니다. VPC, AZ 가용성, 라우팅 테이블과 보안 그룹 규칙을 확인하세요. `associatePublicIPAddress: false`는 공인 IP 할당을 막지만 노드에는 적절한 아웃바운드 연결이 필요합니다.
```yaml
apiVersion: eks.amazonaws.com/v1
kind: NodeClass
metadata:
name: custom-nodeclass
spec:
instanceProfile: eks-node-instance-profile
subnetSelectorTerms:
- tags:
kubernetes.io/role/internal-elb: '1'
Environment: production
securityGroupSelectorTerms:
- tags:
kubernetes.io/cluster/my-cluster: owned
Type: worker-node
ephemeralStorage:
size: 100Gi
iops: 3000
throughput: 125
advancedNetworking:
associatePublicIPAddress: false
advancedCompute:
kernel:
sysctl:
vm.max_map_count: 262144
tags:
Environment: production
ManagedBy: eks-auto-mode
```
클러스터 역할이 `spec.tags`의 커스텀 키로 리소스를 생성·태깅할 권한이 있는지 확인하세요. NodeClass 자체가 IAM 권한을 부여하지는 않습니다.
`ephemeralStorage`는 임의의 블록 디바이스 매핑이 아니라 노드 임시 스토리지를 구성합니다. `100Gi`, 3000 IOPS, 125 MiB/s는 예시입니다. 현재 Auto Mode 필드 제한, 인스턴스 스토리지 동작과 비용을 확인하세요. 고객 KMS 설정 위치는 `ephemeralStorage.kmsKeyID`이며 노드의 루트·데이터 **EBS** 볼륨에 적용됩니다. 로컬 NVMe instance store의 암호화 키를 선택하거나 애플리케이션 PVC 암호화를 보장하지 않습니다.
이전 bootstrap 예제는 `vm.max_map_count`를 바꿨습니다. 지원되는 대안은 노드 부팅 시 적용하는 `advancedCompute.kernel.sysctl`입니다. 지원되는 커널 설정을 바꾸면 기존 노드에 drift·교체가 발생하며, 실행 중인 노드에 즉시 셸 명령을 적용하는 방식이 아닙니다. 해당 설정이 필요한 워크로드에만 구성하고 중단 동작을 검토하세요.
### 고정된 IMDS 보안 설정
Auto Mode는 IMDSv2와 hop limit 1을 강제하며 NodeClass에서 변경할 수 없습니다. 일반 non-host-network Pod는 이 경로로 IMDS에 접근하지 못하지만 모든 Pod에 대한 격리 보장은 아닙니다. `hostNetwork` 사용 시 도달성이 달라집니다. 애플리케이션 AWS 접근에는 워크로드 ID를 사용하고, 노드 메타데이터에 의존하기보다 리전 등 필요한 설정을 명시하세요.
### 고객 KMS, CA bundle과 Pod 네트워크 분리
잘린 인증서 문자열을 manifest에 붙이지 마세요. 선택적인 다음 생성기는 검토한 기본 템플릿을 읽고 승인된 공개 PEM bundle을 검사한 뒤 `certificateBundles[].data`에 base64로 넣어 완전한 두 번째 NodeClass를 만듭니다. Python 3와 PyYAML이 필요합니다. `NODE_KMS_KEY_ARN`, `CA_BUNDLE_FILE`을 설정하고 이전 장에서 검토한 `AWS_REGION`·`EXPECTED_ACCOUNT_ID`를 유지하세요.
CA 발급자, fingerprint와 유효 기간은 별도로 승인받아야 합니다. 아래 형식 검사는 신뢰성, KMS 권한이나 클라우드 리소스의 존재를 입증하지 않습니다. 적용 전에 key policy/IAM grant와 Pod 네트워크 태그 선택을 검토하세요.
```bash
set -euo pipefail
: "${AWS_REGION:?Set the reviewed cluster region}"
: "${EXPECTED_ACCOUNT_ID:?Set the intended AWS account}"
: "${NODE_KMS_KEY_ARN:?Set the approved same-region customer-managed KMS key ARN}"
: "${CA_BUNDLE_FILE:?Set the path to an approved public CA PEM bundle}"
export AWS_REGION EXPECTED_ACCOUNT_ID NODE_KMS_KEY_ARN CA_BUNDLE_FILE
umask 077
python3 - <<'PY'
import base64, json, os, re, ssl
from pathlib import Path
import yaml
doc = yaml.safe_load(Path("custom-nodeclass.yaml").read_text())
if doc.get("kind") != "NodeClass" or doc.get("apiVersion") != "eks.amazonaws.com/v1":
raise SystemExit("Expected the reviewed Auto Mode NodeClass template")
spec = doc["spec"]
if ("role" in spec) == ("instanceProfile" in spec):
raise SystemExit("Set exactly one reviewed role or instanceProfile")
key = os.environ["NODE_KMS_KEY_ARN"]
prefix = "arn:aws:kms:" + os.environ["AWS_REGION"] + ":" + os.environ["EXPECTED_ACCOUNT_ID"] + ":key/"
if not key.startswith(prefix) or not re.fullmatch(r"(?:[a-f0-9-]{36}|mrk-[a-f0-9]{32})", key[len(prefix):]):
raise SystemExit("KMS key ARN must match the reviewed account and region")
pem = Path(os.environ["CA_BUNDLE_FILE"]).read_bytes()
if b"PRIVATE KEY" in pem:
raise SystemExit("Use public CA certificates only, never a private key")
ssl.create_default_context().load_verify_locations(cadata=pem.decode("ascii"))
doc["metadata"]["name"] = "secure-network-nodeclass"
spec["ephemeralStorage"]["kmsKeyID"] = key
spec["certificateBundles"] = [{"name": "corporate-ca",
"data": base64.b64encode(pem).decode("ascii")}]
spec["podSubnetSelectorTerms"] = [{"tags": {"Purpose": "pod-network"}}]
spec["podSecurityGroupSelectorTerms"] = [{"tags": {"Purpose": "pod-network"}}]
Path("secure-network-nodeclass.json").write_text(json.dumps(doc, indent=2) + "\n")
PY
```
| 필드 | 의미 |
|------|------|
| `ephemeralStorage.kmsKeyID` | 노드 루트·데이터 EBS 볼륨용 고객 관리 키. 클러스터와 같은 리전의 키 사용 |
| `certificateBundles[].data` | 노드 신뢰를 위한 base64 인코딩 인증서 bundle. 모든 애플리케이션 컨테이너의 trust store를 자동으로 변경하지 않음 |
| `podSubnetSelectorTerms` | 이 NodeClass용 별도 Pod 서브넷 선택 |
| `podSecurityGroupSelectorTerms` | Pod 네트워크 보안 그룹. Pod 서브넷 selector와 함께 구성 |
Pod 서브넷과 보안 그룹 selector는 함께 구성하고 VPC/AZ 범위를 맞춰야 합니다. 이 구분은 namespace나 ServiceAccount별 설정이 아니라 NodeClass 단위입니다. Host-network 트래픽은 노드 네트워크를 사용하며 egress SNAT도 출발지 주소와 적용 보안 그룹을 바꿀 수 있습니다. 모든 트래픽이 격리됐다고 판단하기 전에 라우팅, SNAT와 Pod 밀도 영향을 검토하세요.
## 워크로드와 환경 분리
### 프론트엔드·백엔드 풀
Taint는 맞는 toleration이 없는 Pod의 배치를 막습니다. Toleration은 배치를 허용할 뿐입니다. 지정한 노드를 사용해야 한다면 selector나 required affinity도 지정하세요.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: frontend
spec:
template:
metadata:
labels:
workload-tier: frontend
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- c
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
- on-demand
taints:
- key: workload-tier
value: frontend
effect: NoSchedule
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
weight: 10
limits:
cpu: '100'
memory: 400Gi
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: backend
spec:
template:
metadata:
labels:
workload-tier: backend
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- r
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
taints:
- key: workload-tier
value: backend
effect: NoSchedule
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
weight: 10
limits:
cpu: '100'
memory: 800Gi
```
예를 들어 소유한 `nodepool-lab` namespace를 준비한 뒤 프론트엔드 배치를 확인하는 Pod에 selector와 toleration을 함께 지정합니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: frontend-placement-check
namespace: nodepool-lab
spec:
automountServiceAccountToken: false
nodeSelector:
workload-tier: frontend
tolerations:
- key: workload-tier
operator: Equal
value: frontend
effect: NoSchedule
containers:
- name: nginx
image: nginx:1.30.4
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
readinessProbe:
httpGet:
path: /
port: 80
periodSeconds: 5
```
적용 전에 검토하세요. 이 Pod는 과금되는 노드 프로비저닝을 유발할 수 있습니다. 배치된 노드, readiness와 node label을 확인한 뒤 해당 namespace에서 테스트 Pod를 삭제합니다. `NoSchedule`은 이미 실행 중인 Pod를 축출하지 않으며 label·taint만으로 신뢰하지 않는 테넌트 사이의 보안 경계를 만들 수는 없습니다.
### 개발용 풀
현재 AWS 지원 목록에는 M 계열과 함께 T 계열 burstable 인스턴스도 있습니다. 과거 Auto Mode 가정만으로 T를 제거하지 마세요. Auto Mode는 CPU가 1개보다 많아야 하며 nano/micro/small 크기를 제외합니다. 그래도 리전별 타입 가용성, CPU credit 동작과 워크로드 적합성을 확인해야 합니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: dev-pool
spec:
template:
metadata:
labels:
environment: development
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- t
- m
- key: eks.amazonaws.com/instance-size
operator: In
values:
- medium
- large
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
taints:
- key: environment
value: development
effect: NoSchedule
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
limits:
cpu: '100'
memory: 400Gi
weight: 1
```
## 리소스 제한, 가중치와 검증
`limits.cpu`와 `limits.memory`는 풀의 합산 리소스 기준이며 최대 노드 수나 금액 예산이 아닙니다. 정수·문자열 GitOps 차이를 피하도록 CPU quantity를 따옴표로 감싸세요. 예를 들어 `memory: 4000Gi`는 4000 GiB(약 3.91 TiB)이지 정확한 4 TB가 아닙니다.
Upstream Karpenter는 빠른 프로비저닝 시 eventual consistency로 제한을 넘을 수 있다고 설명합니다. 이 필드로 즉시 적용되는 과금 상한을 약속하지 마세요. 노드 교체 여유를 두고 사용량을 관찰하며 관리형 환경의 실제 동작을 검증합니다.
`weight`는 적합한 풀 사이에서 프로비저닝 선호도에 영향을 줍니다. Kubernetes 스케줄러의 노드 우선순위를 부여하거나 기존 워크로드를 축출하거나 Pod를 특정 풀에 고정하지 않습니다. 워크로드 제약을 명시하고 의도하지 않은 풀 중첩을 피하세요.
```bash
# Validate against the target cluster's actual schemas; no object is persisted.
kubectl --context "$CLUSTER_NAME" apply --dry-run=server -f custom-nodeclass.yaml
kubectl --context "$CLUSTER_NAME" apply --dry-run=server -f secure-network-nodeclass.json
# After an approved apply, inspect conditions rather than assuming readiness.
kubectl --context "$CLUSTER_NAME" get nodeclasses,nodepools
kubectl --context "$CLUSTER_NAME" describe nodeclass secure-network-nodeclass
kubectl --context "$CLUSTER_NAME" get nodeclaims
```
Server dry-run은 admission을 검사할 수 있지만 IAM, 네트워크 연결, 프로비저닝, 스토리지나 애플리케이션 동작을 입증하지는 않습니다. False/unknown readiness를 해결하고 통제된 워크로드로 검증한 뒤 프로덕션에 적용하세요.
## 참고 자료
- [Built-in NodePools](https://docs.aws.amazon.com/eks/latest/userguide/set-builtin-node-pools.html)
- [Auto Mode NodePool fields and labels](https://docs.aws.amazon.com/eks/latest/userguide/create-node-pool.html)
- [Auto Mode NodeClass specification](https://docs.aws.amazon.com/eks/latest/userguide/create-node-class.html)
- [Managed instance types and IMDS restrictions](https://docs.aws.amazon.com/eks/latest/userguide/automode-learn-instances.html)
- [Auto Mode node security](https://docs.aws.amazon.com/whitepapers/latest/security-overview-amazon-eks-auto-mode/eks-auto-mode-data-plane.html)
- [Upstream Karpenter NodePool limits](https://karpenter.sh/docs/concepts/nodepools/)
- [Karpenter weighted provisioning](https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools)
- [Node CA, KMS and network features](https://aws.amazon.com/blogs/containers/new-amazon-eks-auto-mode-features-for-enhanced-security-network-control-and-performance/)
< [이전: 시작하기](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/01-getting-started.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md) | [다음: 스케일링 동작](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/03-scaling-behavior.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-auto-mode/03-scaling-behavior
----------------------------------------
# 스케일링 동작 이해
> **지원 버전**: EKS Auto Mode GA; 예제 기준 EKS 1.36
> **마지막 업데이트**: 2026년 9월 12일
프로비저닝, consolidation, drift와 expiration을 구분합니다. [시작하기](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/01-getting-started.md)에서 확인한 계정·컨텍스트를 사용하세요. Manifest는 구성된 `default` NodeClass를 가정하며 수명 주기 동작에 집중하도록 On-Demand 용량을 사용합니다. 예시 제한값도 상당한 비용을 허용할 수 있습니다.
이번 감사에서 클러스터, 워크로드나 지연 시간 벤치마크를 실행하지 않았습니다. 구성과 진단 변환은 로컬에서 검사했으며, 프로덕션 적용 전에 통제된 환경에서 admission, IAM, 배치와 중단 동작을 검증해야 합니다.
## 스케줄 불가능한 Pod에서 용량 확보까지
Auto Mode는 스케줄러가 기존 용량에 배치하지 못하는 Pod를 관찰합니다. 적합한 NodePool/NodeClass, 호환되는 제약, 할당량과 EC2 용량이 있어야 프로비저닝이 성공할 수 있습니다. Requests, affinity, taint, topology, 볼륨 위치와 아키텍처가 모두 영향을 줍니다. 복제본 증가는 HPA/KEDA 등 애플리케이션 컨트롤러의 역할이며 노드 자동화가 애플리케이션 CPU 사용률 autoscaler를 대신하지 않습니다.
`Pending`만으로 노드 부족을 입증할 수는 없습니다. 이미 노드에 배치된 Pod가 이미지나 초기화를 기다릴 수도 있습니다. 그림은 성공적인 용량 확보 경로를 나타내며 실패 경로를 생략합니다. 호환 인스턴스 선택은 전역 최적값이나 준비 시간 보장이 아닙니다. `Running`도 애플리케이션 readiness를 뜻하지 않습니다.

[인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-auto-mode-03-scaling-behavior-0.html)
### 과거 교육용 추정값
이전 가이드에는 원시 관측 자료나 재현 가능한 벤치마크 없이 다음 수치가 제시됐습니다. **검증되지 않은 과거 추정값**으로 보존하며 EKS 1.36에서 측정한 결과나 AWS SLO가 아닙니다. 단계 정의가 겹칠 수 있으므로 실측 trace처럼 합산하지 마세요.
| 이전 설명의 단계 | 제시됐던 시간 |
|------------------|---------------|
| Pending 감지 | 1–5초 |
| 인스턴스 선택 | 1–3초 |
| EC2 시작 | 10–30초 |
| AMI 부팅 | 20–40초 |
| kubelet 등록 | 5–10초 |
| Pod 스케줄링 | 1–5초 |
| 제시됐던 합계 | 40–90초 |
## Consolidation: 타이머 보장이 아닌 후보 자격
Consolidation은 배치 가능성을 유지하면서 비용을 줄일 수 있는 제거·교체를 찾습니다. 이를 실측 CPU·메모리 사용률의 고정 임계값으로 모델링하지 마세요. Resource requests와 배치 제약이 중요합니다. PDB, disruption budget, annotation, 대체 용량과 drain 진행 상태가 작업을 막거나 늦출 수 있습니다.
`consolidateAfter`는 안정화·후보 검토 지연입니다. Pod가 추가·제거되면 타이머가 재설정됩니다. 30초를 설정해도 정확히 30초 뒤 삭제되거나 그 안에 대체 노드가 준비된다는 보장이 아닙니다.
### WhenEmpty
이 정책은 조건을 충족한 빈 노드를 검토합니다. 여기서 “빈 노드”가 `kubectl get pods`에 Pod가 전혀 없는 노드만을 뜻하지는 않습니다. DaemonSet만 있는 노드도 대상이 될 수 있습니다. Consolidation 관점에서는 보수적이지만 drift, expiration, Spot interruption 같은 다른 중단 원인을 비활성화하지 않습니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: when-empty-example
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- c
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
budgets:
- nodes: 10%
limits:
cpu: '100'
memory: 400Gi
```
### WhenEmptyOrUnderutilized
관련 제약을 만족하면서 워크로드를 더 저렴하게 재배치할 수 있으면 비어 있지 않은 노드도 통합할 수 있습니다. 기존 여유 용량으로 노드를 제거하거나 더 저렴한 용량으로 교체할 수 있으며 항상 새 노드가 필요한 것은 아닙니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: when-underutilized-example
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- c
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
budgets:
- nodes: 10%
limits:
cpu: '100'
memory: 400Gi
```
### Balanced
현재 AWS NodePool 참조는 중단 비용과 절감 효과를 함께 평가하는 `Balanced`도 지원합니다. 더 적극적인 정책이 수행할 수 있는 작은 절감 작업을 건너뛸 수 있지만 무중단 보장은 아닙니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: balanced-example
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- c
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
disruption:
consolidationPolicy: Balanced
consolidateAfter: 1m
budgets:
- nodes: 10%
limits:
cpu: '100'
memory: 400Gi
```
### 패킹 그림 해석

[인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-auto-mode-03-scaling-behavior-1.html)
그림의 CPU 20/15/10%, 메모리 30/25/20%는 같은 용량의 노드에서 각각 45%, 75%로 합산됩니다. 보편적인 consolidation 임계값의 실측 입력이 아닌 설명용 패킹 수치입니다. 실제 배치는 requests, topology, 스토리지와 가용성 요구 사항도 만족해야 합니다. Pod는 축출 후 워크로드 컨트롤러가 다시 생성하며 live migration되는 것이 아닙니다.
## Drift 감지와 교체
Drift는 NodeClaim이 관련된 원하는 구성이나 해석된 리소스 선택과 달라졌음을 뜻합니다. `Drifted` condition을 확인하세요. 이전 `karpenter.sh/drift-hash` 노드 annotation 조회는 유효한 공개 drift 상태 확인이 아니었습니다. 아래에서는 condition 부재를 확정적인 false로 만들지 않고 `NotReported`로 표시합니다.
```bash
kubectl --context "$CLUSTER_NAME" --request-timeout=15s get nodeclaims -o json |
jq '[.items[] | {
claim: .metadata.name,
node: .status.nodeName,
pool: .metadata.labels["karpenter.sh/nodepool"],
drift: ((.status.conditions // [] | map(select(.type == "Drifted") |
{status, reason, lastTransitionTime, observedGeneration}) | first)
// {status: "NotReported"})
}]'
```
| 변경 | 해석 |
|------|------|
| Requirements가 현재 인스턴스를 제외 | Drift가 발생할 수 있음 |
| Requirements를 넓혀도 현재 인스턴스가 여전히 허용됨 | 반드시 drift가 발생하는 것은 아님 |
| 관련 NodeClass 설정이나 해석된 서브넷·보안 그룹 선택 변경 | Drift 가능. 실제 condition 확인 |
| AWS가 새 관리형 Auto Mode AMI를 선택 | 교체 가능. 사용자가 `amiFamily`를 선택하는 방식이 아님 |
| 이미 참조한 보안 그룹의 규칙 편집 | 다른 그룹을 선택하는 것과 다름. 항상 node drift가 발생한다고 가정하지 않음 |
| NodePool weight, limits, disruption 동작 | 그 자체는 노드 템플릿 drift가 아니지만 허용되는 작업을 바꿀 수 있음 |
| `expireAfter` 또는 `terminationGracePeriod` 변경 | 기존 NodeClaim 필드를 덮어쓰지 않으며 교체된 노드에 새 값 적용 |
일반적인 graceful 경로는 budget과 배치 가능성을 확인하고, 선택한 노드에 새 배치를 막고, **필요하면** 대체 용량을 준비한 뒤 기존 Pod를 축출·drain하고 노드를 종료합니다. 대체 용량과 기존 용량이 겹쳐 비용이 발생할 수 있습니다. 병렬성은 budget으로 제어되며 반드시 하나씩 교체하는 것은 아닙니다. 강제 interruption/expiration 경로도 항상 정상 대체 노드를 먼저 기다린다고 설명해서는 안 됩니다.
## Expiration과 종료 유예 시간
AWS는 Auto Mode 노드의 최대 수명을 21일로 설명합니다. 기본 expiry는 336시간이며 커스텀 NodePool에 `terminationGracePeriod`가 없으면 NodeClaim에 기본 24시간을 적용합니다. 일반 upstream의 720시간 기본값을 Auto Mode 권장값으로 가져오지 마세요.
다음은 168시간 후 expiration과 명시적인 유예 시간을 요청합니다. Expiration은 drain을 시작하는 조건이며 정확히 7일 뒤 정상 대체 노드가 준비됨을 보장하지 않습니다. 다른 원인으로 그보다 먼저 중단될 수도 있습니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: with-expiration
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- c
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
expireAfter: 168h
terminationGracePeriod: 24h
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
budgets:
- nodes: 10%
limits:
cpu: '100'
memory: 400Gi
```
| 이전 가이드의 정책 값 | 현재 해석 |
|-----------------------|-----------|
| 24–72시간 | 더 많은 교체를 수반하는 선택적 단기 정책. 매 주기 새 패치가 있다는 증거는 아님 |
| 168시간 | 위의 7일 예제. 가용성과 교체 오버헤드 평가 필요 |
| 336시간 | 문서화된 Auto Mode 기본 expiry |
| 720시간 / 30일 | Auto Mode의 문서화된 최대 21일을 초과. 노드 재사용 기간 보장으로 사용하지 않음 |
NodePool disruption budget은 graceful 방식의 속도를 제한하며 expiration, interruption, repair를 모두 막는 방패가 아닙니다. PDB와 `do-not-disrupt`는 drain·중단 판단에 영향을 주지만 무기한 보존을 보장하지 않습니다. 설정한 종료 유예 시간이 끝나면 남은 Pod가 강제로 제거될 수 있습니다. Stateful 워크로드, 볼륨 detach, 애플리케이션 종료 유예와 장애 시나리오를 반영해 정책을 선택하세요.
## 벤치마크를 만들어내지 않는 지연 진단
다음 명령은 전체 Pod spec 대신 필요한 metadata와 status만 수집합니다. 증거를 비공개로 보관하세요. 스냅샷의 마지막 condition 전환은 최초 시작이 아니라 이후 전환이나 readiness flap일 수도 있습니다.
```bash
umask 077
: "${WORK_DIR:?Use the private evidence directory from the getting-started guide}"
kubectl --context "$CLUSTER_NAME" --request-timeout=15s get nodeclaims -o json |
jq '[.items[] | {
claim: .metadata.name, uid: .metadata.uid,
node: .status.nodeName, createdAt: .metadata.creationTimestamp,
pool: .metadata.labels["karpenter.sh/nodepool"],
expireAfter: .spec.expireAfter,
terminationGracePeriod: .spec.terminationGracePeriod,
conditions: [.status.conditions[]? |
select(.type == "Launched" or .type == "Registered" or .type == "Initialized" or .type == "Ready") |
{type, status, reason, lastTransitionTime}]
}]' > "$WORK_DIR/nodeclaims-summary.json"
```
```bash
: "${WORKLOAD_NAMESPACE:?Set the controlled test namespace}"
: "${POD_NAME:?Set the controlled test Pod name}"
kubectl --context "$CLUSTER_NAME" --request-timeout=15s -n "$WORKLOAD_NAMESPACE" \
get pod "$POD_NAME" -o json |
jq '{name: .metadata.name, uid: .metadata.uid,
createdAt: .metadata.creationTimestamp, node: .spec.nodeName, phase: .status.phase,
conditions: [.status.conditions[]? |
select(.type == "PodScheduled" or .type == "Ready") |
{type, status, reason, lastTransitionTime}]}' > "$WORK_DIR/pod-summary.json"
```
실제 벤치마크에는 워크로드 생성부터 통제된 관찰을 시작하고 객체 UID와 NodeClaim·노드 배치를 연결해야 합니다. 첫 스케줄링, 노드 readiness와 애플리케이션 readiness를 구분하고 실패·timeout, 환경, 이미지와 워크로드 구성도 기록하세요. 스냅샷과 보관된 Kubernetes 이벤트만으로 전체 지연 분포를 입증할 수는 없습니다.
### 지원되는 스토리지 조정
Auto Mode가 Bottlerocket 변형을 선택합니다. 부팅 시간 단축을 기대하며 `amiFamily`나 `blockDeviceMappings`를 설정하지 마세요. 유효한 다음 스토리지 템플릿은 `ephemeralStorage`를 사용합니다. Profile·selector를 검토된 리소스로 바꾸고 대상 풀에서 이 NodeClass를 참조하세요.
```yaml
apiVersion: eks.amazonaws.com/v1
kind: NodeClass
metadata:
name: image-storage-example
spec:
instanceProfile: eks-node-instance-profile
subnetSelectorTerms:
- tags:
Name: private-subnet
securityGroupSelectorTerms:
- tags:
Name: eks-cluster-sg
advancedNetworking:
associatePublicIPAddress: false
ephemeralStorage:
size: 50Gi
iops: 3000
throughput: 125
```
설정을 바꾸기 전에 이미지 전송·압축 해제, CPU, 네트워크와 스토리지 병목을 측정하세요. 이전의 “Bottlerocket으로 10–20초 단축”, “EBS 축소로 5–10초 단축”, “IOPS 증가로 5–10초 단축”은 검증된 실측값이 없었습니다. 현재 튜닝 보장이 아니며 용량 축소가 오히려 이미지·임시 스토리지 부족을 일으킬 수 있습니다.
호환되는 인스턴스·AZ 선택지를 늘리면 용량 확보 선택지는 넓어질 수 있지만 워크로드와 볼륨 제약을 지켜야 합니다. Placeholder Pod는 유휴 비용을 지불하며 용량을 예약할 수 있을 뿐 이미지 다운로드나 애플리케이션 readiness가 거의 즉시 끝남을 보장하지 않습니다.
## 적절한 신호 관찰
모든 Pending Pod를 용량 요청으로 취급하지 말고 스케줄 불가능하다고 명시된 Pod를 확인합니다.
```bash
kubectl --context "$CLUSTER_NAME" --request-timeout=15s \
get pods -A --field-selector=status.phase=Pending -o json |
jq '[.items[] | select(any(.status.conditions[]?;
.type == "PodScheduled" and .status == "False" and .reason == "Unschedulable")) |
{namespace: .metadata.namespace, name: .metadata.name, uid: .metadata.uid,
createdAt: .metadata.creationTimestamp}]'
```
이벤트는 판단 이유를 설명하지만 집계·중복·만료될 수 있습니다. 아래는 명시적인 `events.k8s.io` 리소스에서 워크로드 데이터 대신 상태 식별자를 선택합니다.
```bash
kubectl --context "$CLUSTER_NAME" --request-timeout=15s \
get events.events.k8s.io -A -o json |
jq '[.items[] |
select(.regarding.kind == "NodeClaim" or .regarding.kind == "Node" or .regarding.kind == "Pod") |
{time: (.eventTime // .deprecatedLastTimestamp // .metadata.creationTimestamp),
reason, regarding: {kind: .regarding.kind, name: .regarding.name, uid: .regarding.uid},
count: (.series.count // .deprecatedCount // 1)}] | sort_by(.time)'
```
| 신호 | 증거와 알람 설계 고려 사항 |
|------|----------------------------|
| 지속되는 unschedulable Pod | Kubernetes condition과 구성한 collector. 이전의 10개 초과·5분 기준은 예시였음 |
| NodeClaim 생성·실패율 | 지속 보관된 이벤트·로그 또는 계측. 현재 객체 스냅샷은 삭제된 시도를 누락 |
| 프로비저닝·애플리케이션 지연 | 실패를 포함해 연결한 관측. 이전 p99 120초 초과 기준은 AWS SLO가 아님 |
| Pool 리소스가 limits에 근접 | NodePool status, 할당량과 용량 확인. requests·예약 리소스와 실측 사용률 구분 |
이전 `karpenter_*` 표의 이름이 기본 Auto Mode CloudWatch 메트릭이라고 가정하지 마세요. 자체 관리 Karpenter Prometheus 메트릭, EKS control-plane 메트릭과 직접 구성한 collector는 서로 다른 인터페이스입니다. 실제 publisher에서 제공하는 메트릭 이름과 차원을 선택해야 합니다.
### AWS 관리 컴포넌트 로그
Auto Mode는 CloudWatch Vended Logs delivery로 관리 컴포넌트 로그를 제공합니다. 일반 EKS control-plane logging과 별도로 구성합니다.
- 관리형 Karpenter 판단을 위한 `AUTO_MODE_COMPUTE_LOGS`
- `AUTO_MODE_BLOCK_STORAGE_LOGS`
- `AUTO_MODE_LOAD_BALANCING_LOGS`
- `AUTO_MODE_IPAM_LOGS`
문서화된 설정은 delivery source, delivery destination, delivery를 연결합니다. 활성화 전에 대상 권한과 전송·보관 요금을 검토하세요. 기존 control-plane audit 로그에서도 `DisruptionBlocked`, `Unconsolidatable`, `FailedScheduling`, `NodeClassNotReady`와 종료 실패 등의 Kubernetes 이벤트를 확인할 수 있습니다. AWS 문제 해결 참조와 제한된 시간 범위를 사용하세요. 이 진단 이벤트가 자동으로 지연 histogram 메트릭이 되는 것은 아닙니다.
## 참고 자료
- [EKS Auto Mode behavior and maximum node lifetime](https://docs.aws.amazon.com/eks/latest/userguide/automode.html)
- [Auto Mode NodePool policies and grace period](https://docs.aws.amazon.com/eks/latest/userguide/create-node-pool.html)
- [Karpenter v1.14 disruption and drift](https://karpenter.sh/v1.14/concepts/disruption/)
- [Auto Mode NodeClass fields](https://docs.aws.amazon.com/eks/latest/userguide/create-node-class.html)
- [Auto Mode troubleshooting](https://docs.aws.amazon.com/eks/latest/userguide/auto-troubleshoot.html)
- [AWS-managed component log delivery](https://docs.aws.amazon.com/eks/latest/userguide/auto-managed-component-logs.html)
< [이전: NodePool 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/02-nodepool-configuration.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md) | [다음: Spot 전략](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/04-spot-strategies.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-auto-mode/04-spot-strategies
----------------------------------------
# Spot 인스턴스 활용 전략
> **지원 버전**: EKS Auto Mode GA; 예제 기준 EKS 1.36
> **마지막 업데이트**: 2026년 9월 12일
Spot은 중단·용량 불확실성을 감수하는 대신 EC2 비용을 줄일 수 있는 선택지입니다. 혼합 용량, 다양화와 복제본은 복원력에 도움이 될 수 있지만 각각만으로 가용성이나 절감액을 보장하지는 않습니다. 앞 장에서 확인한 계정·컨텍스트와 NodeClass를 사용하세요.
Manifest는 로컬 스키마로 확인한 실습 예제이며 프로덕션 장애조치 시험 결과가 아닙니다. 관계없는 워크로드의 우발적 배치를 줄이도록 `spot-lab` taint/toleration과 명시적인 풀 선택을 사용합니다. Taint가 테넌트 보안 경계는 아닙니다. 적용 전에 복제본 수와 리소스 제한을 검토하세요. 예제도 과금되는 노드를 생성할 수 있습니다. 이번 감사에서는 Spot 인스턴스 생성이나 고객 청구·EC2 Spot Price History API 조회를 실행하지 않았습니다.
## 혼합 용량과 명시적인 기본 용량
다음 풀은 Spot과 On-Demand를 모두 허용합니다. 적합한 한 풀 안에서는 Auto Mode가 허용된 용량 유형을 우선순위로 선택하며, 둘 다 허용되고 사용 가능하면 On-Demand보다 Spot을 우선합니다. `reserved`도 허용하고 적합한 예약 용량이 있으면 그 우선순위가 더 높습니다. 배열 순서나 NodePool weight가 Spot 비율을 지정하지는 않습니다.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: spot-lab
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: mixed-capacity
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- c
- r
- key: eks.amazonaws.com/instance-generation
operator: Gt
values:
- '5'
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
taints:
- key: spot-lab
value: 'true'
effect: NoSchedule
metadata:
labels:
capacity-example: spot-lab
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
limits:
cpu: '100'
memory: 400Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: spot-friendly-app
namespace: spot-lab
spec:
replicas: 10
selector:
matchLabels:
app: spot-friendly
template:
metadata:
labels:
app: spot-friendly
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
containers:
- name: app
image: nginx:1.30.4
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
periodSeconds: 5
automountServiceAccountToken: false
nodeSelector:
karpenter.sh/nodepool: mixed-capacity
tolerations:
- key: spot-lab
operator: Equal
value: 'true'
effect: NoSchedule
```
Pod의 preferred affinity는 선호이지 필수 조건이나 용량 예약이 아닙니다. 즉시 On-Demand로 전환되거나 정해진 비율을 유지하거나 중단 시간 안에 대체 용량이 준비됨을 보장하지 않습니다. 기존 용량, 제약, 할당량과 실제 가용성이 중요합니다.
별도의 기본 용량 Deployment가 반드시 On-Demand를 사용해야 한다면 Pod template의 Spot 선호를 아래와 같은 필수 선택으로 바꾸고 실습 toleration을 유지하세요. 적절한 기본 복제본을 계속 실행해야 합니다. 풀에 On-Demand를 허용하는 것만으로 여유 용량이 미리 준비되지는 않습니다.
```yaml
nodeSelector:
karpenter.sh/nodepool: mixed-capacity
karpenter.sh/capacity-type: on-demand
tolerations:
- key: spot-lab
operator: Equal
value: 'true'
effect: NoSchedule
```
이 selector는 의도적으로 `on-demand`를 요구하며 `reserved` 용량은 포함하지 않습니다. 나중에 capacity reservation을 사용한다면 label과 예약 구성을 다시 검토하세요. On-Demand 자체도 데이터 내구성이나 용량 가용성 보장은 아닙니다.
## 호환되는 용량 다양화
Spot 용량 풀은 인스턴스 유형과 AZ에 연결됩니다. 호환되는 유형·AZ가 많으면 선택지가 늘어나지만 중단이 서로 독립적이 되거나 아키텍처 두 개만으로 용량이 정확히 두 배가 되지는 않습니다.
예제는 세대 상한을 7로 고정하지 않고 5세대 이상을 허용합니다. 제약을 넓히기 전에 이미지, 바이너리, 스토리지와 성능 호환성을 확인하세요.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: diversified-spot
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- c
- r
- i
- d
- key: eks.amazonaws.com/instance-generation
operator: Gt
values:
- '4'
- key: eks.amazonaws.com/instance-size
operator: In
values:
- large
- xlarge
- 2xlarge
- key: kubernetes.io/arch
operator: In
values:
- amd64
- arm64
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
taints:
- key: spot-lab
value: 'true'
effect: NoSchedule
metadata:
labels:
capacity-example: spot-lab
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
limits:
cpu: '100'
memory: 400Gi
```
`consolidationPolicy`와 `consolidateAfter`는 통합 정책이며 EC2 interruption에 반응하는 시간을 정하지 않습니다. 여러 아키텍처에는 호환되는 multi-architecture 이미지와 의존성이 필요합니다. 세대·크기를 늘려도 워크로드에 적합하고 선택한 AZ에서 가용해야 의미가 있습니다.
## 자발적 Budget과 EC2 Interruption 구분
Auto Mode는 Spot interruption을 기본 처리하며 이를 위해 추가 Node Termination Handler나 사용자 관리 SQS queue가 필요하지 않습니다. 이전의 잘못된 NTH DaemonSet을 Auto Mode 노드에 배포하지 마세요. 다른 노드 유형이 공존한다면 대상과 권한을 구분해 그 노드의 interruption 처리를 구성합니다.
NodePool disruption budget은 consolidation·drift 같은 자발적 중단을 제한합니다. EC2의 Spot 회수나 경고 시간을 바꾸거나 대체 용량 준비를 보장하지 않습니다.
적용되는 budget을 함께 평가해 가장 제한적인 허용량을 사용합니다. 아래 `10%`와 `3`은 대안 관계가 아닙니다. 백분율은 올림 계산하며 삭제 중·NotReady 노드도 허용량을 소모합니다. 그 외 모두 정상인 노드가 20개라면 zero-budget 구간 밖에서 두 항목은 새 자발적 중단을 최대 2개 허용합니다.
예제는 **서울(KST) 월–금 09:00–18:00**을 보호하도록 **UTC 00:00**에 9시간 창을 시작합니다. Karpenter budget schedule은 UTC입니다. 이전 `0 9-18 * * mon-fri`는 매시간 9시간 창을 추가해 18시에 끝나는 대신 다음 날까지 중첩됐습니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-with-disruption-budget
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- c
- r
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
taints:
- key: spot-lab
value: 'true'
effect: NoSchedule
metadata:
labels:
capacity-example: spot-lab
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
budgets:
- nodes: 10%
- nodes: '3'
- nodes: '0'
schedule: 0 0 * * mon-fri
duration: 9h
limits:
cpu: '100'
memory: 400Gi
```
운영 달력에 맞게 조정하세요. 다른 시간대와 일광절약시간은 의도적으로 변환해야 합니다. Zero voluntary budget은 필요한 유지보수를 늦출 수 있으며 비자발적 interruption을 막지는 못합니다.
## 실제 남은 시간 안의 Graceful Shutdown
EC2는 일반적으로 stop/terminate interruption 전에 2분 경고를 보내지만 best effort입니다. Hibernation은 즉시 시작하는 다른 동작을 가집니다. 모든 Pod에 2분이 보장된다고 해석하지 마세요. 감지, 축출, 애플리케이션 처리와 라우팅 변경이 시간을 사용하며 장애로 경고를 처리하지 못할 수도 있습니다.
`terminationGracePeriodSeconds`는 Kubernetes 종료 예산이며 EC2 회수 기한을 늘리지 않습니다. `preStop`은 그 예산 안에서 일반 종료 신호보다 먼저 실행됩니다. 무조건 `sleep 90`을 실행하면 120초 예산 대부분을 소비하며 애플리케이션 종료나 체크포인트를 구현하지도 않습니다.
nginx용 다음 예제는 즉시 graceful quit를 시작하고 실제 HTTP readiness probe와 정확한 node name을 사용합니다. 다른 애플리케이션에는 자체 종료 신호 처리, readiness 전환, 진행 중 작업 완료와 durable checkpoint를 구현·검증해야 합니다. 60초는 예시 상한 예산이지 실제로 모두 주어진다는 보장이 아닙니다.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spot-aware-app
namespace: spot-lab
spec:
replicas: 6
selector:
matchLabels:
app: spot-aware
template:
metadata:
labels:
app: spot-aware
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: nginx:1.30.4
lifecycle:
preStop:
exec:
command:
- nginx
- -s
- quit
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
periodSeconds: 5
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: spot-aware
minDomains: 2
nodeAffinityPolicy: Honor
nodeTaintsPolicy: Honor
automountServiceAccountToken: false
nodeSelector:
karpenter.sh/nodepool: spot-with-disruption-budget
tolerations:
- key: spot-lab
operator: Equal
value: 'true'
effect: NoSchedule
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: spot-aware-budget
namespace: spot-lab
spec:
maxUnavailable: 1
selector:
matchLabels:
app: spot-aware
```
`NODE_NAME`은 Downward API로 노드 이름을 받습니다. Spot 여부를 나타내는 boolean이 아니며 Pod용 Downward API가 node label을 자동으로 노출하지도 않습니다. 애플리케이션은 보통 구매 옵션과 관계없이 graceful termination을 처리해야 합니다. 용량 유형이 필요하면 노드 IMDS 자격 증명에 의존하기보다 검토된 방법으로 확인한 metadata를 전달하세요.
예제는 적합한 AZ domain 2개를 요구하고 `maxSkew: 1`로 복제본 6개를 분산합니다. 엄격한 `DoNotSchedule` 제약은 AZ·용량 부족 시 Pod를 Pending에 남길 수 있으며 용량을 만들어내지 않습니다. 세 domain을 요구하기 전에 이 tradeoff를 검토하세요. AZ 분산이 AZ 안의 인스턴스 유형까지 분산한다는 뜻도 아닙니다.
PDB는 자발적인 애플리케이션 축출을 제한할 뿐 EC2가 회수하는 VM을 보존하지 않습니다. 복제본 수를 늘리는 효과도 트래픽 처리, 적합한 용량, topology와 장애 복구를 함께 검증해야 얻을 수 있습니다.
## 워크로드 적합성
| 워크로드 | 시작점과 필수 검증 |
|----------|------------------|
| Stateless 웹·추론 | 중단을 허용하는 Spot 용량과 명시적인 중요 기본 용량, 지연·장애조치 검증 |
| Batch·CI | 재시도 가능하고 idempotent한 작업에 적합할 수 있으나 마감 시간이 엄격하면 다른 용량도 필요 |
| 긴 학습·상태 처리 작업 | 마지막 순간 checkpoint 보장 대신 주기적 durable checkpoint와 resume/replay 검증 |
| 단일 DB·중요 quorum | 명시적인 복구 구조 없이 중단에 노출하지 않음. On-Demand만으로 로컬 데이터를 보호하지는 않음 |
| 개발·테스트 | 비용 제한과 용량 부족 허용. Spot-only 작업이 항상 시작한다고 가정하지 않음 |
중요한 상태의 유일한 복사본을 일회성 노드 로컬 스토리지에 두지 마세요. 구매 옵션과 별도로 durable storage, backup과 restore를 검증합니다.
## 현재 견적이 아닌 과거 비용 예시
이전 가이드에는 리전, 인스턴스 시간 구성, 과금 범위나 재현 가능한 측정 없이 다음 월 비용이 제시됐습니다. **검증되지 않은 교육용 예시**로 보존하며 현재 EKS 버전에서 측정한 절감액이 아닙니다.
| 예시 | 이전 On-Demand 금액 | 이전 Spot 금액 | 산술상 감소율 |
|------|---------------------|----------------|---------------|
| Batch | $1,000/월 | $300/월 | 70% |
| 개발·테스트 | $2,000/월 | $500/월 | 75% |
| CI/CD | $500/월 | $150/월 | 70% |
| 비중요 API | $3,000/월 | $1,200/월 | 60% |
AWS는 EC2 Spot 가격을 최대 90% 할인으로 안내하지만 최소 할인율이나 전체 워크로드 절감 보장은 아닙니다. Spot Instance Advisor는 지난 한 달의 interruption·절감 데이터를 AZ 평균으로 요약하며 지연될 수 있습니다. 계산에는 현재 AZ별 Spot Price History나 실제 청구 데이터를 사용하세요. 과거 interruption 구간은 다음 작업의 예측값이 아닙니다.
같은 유효 작업량, 리전, 기간과 가용성 목표를 비교합니다.
```text
Baseline total =
sum(baseline On-Demand node-hours[type] * On-Demand rate[type])
+ other baseline costs
Actual total =
sum(billed Spot node-hours[type, AZ] * time-weighted Spot rate[type, AZ])
+ sum(billed On-Demand fallback node-hours[type] * On-Demand rate[type])
+ other actual costs
Net savings = Baseline total - Actual total
```
시간당 단가에는 시간을 같은 단위로 사용하고 변동 가격은 실제 청구 사용 구간으로 가중하세요. 청구된 Spot·fallback 시간에 실제 지불한 재시도, 용량 중첩과 복구 작업이 포함돼야 합니다. 이미 포함된 작업을 다시 일반적인 “interrupt overhead”로 빼서 이중 계산하지 마세요.
기타 비용에는 관련 Auto Mode 관리 요금, control-plane, EBS, 네트워크/NAT, 전송, 로그와 라이선스를 일관되게 포함합니다. Auto Mode 요금은 EC2 구매 옵션에 추가됩니다. 이전 `중단 횟수 × 복구 시간 × 인스턴스 수` 단축식은 단위와 중단 횟수가 이미 클러스터 전체 기준인지 모호했습니다.
## 운영 검증
Spot에 의존하기 전에 승인된 환경에서 중단·복구를 시험하세요. 경고 누락·지연, 대체 용량 부족, 진행 중 작업, 이미지 다운로드, 스토리지 복구와 엄격한 topology 제약을 포함합니다. 실제 손실 작업량과 청구된 복구 비용을 관찰해야 합니다. 이번 감사는 로컬 스키마·시간 창·구성 검사만 수행했으며 해당 장애 실험을 실행하지 않았습니다.
## 참고 자료
- [Spot interruption notices](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html)
- [Auto Mode native interruption handling](https://docs.aws.amazon.com/eks/latest/userguide/ml-node-pools.html)
- [Capacity type priority](https://docs.aws.amazon.com/eks/latest/userguide/create-node-pool.html)
- [Karpenter disruption budgets and UTC schedules](https://karpenter.sh/v1.14/concepts/disruption/)
- [Pod termination and preStop](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/)
- [Topology spread](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/)
- [Downward API fields](https://kubernetes.io/docs/concepts/workloads/pods/downward-api/)
- [nginx graceful shutdown](https://nginx.org/en/docs/control.html)
- [EC2 Spot published discount guidance](https://aws.amazon.com/ec2/spot/)
- [Spot Instance Advisor](https://aws.amazon.com/ec2/spot/instance-advisor/)
- [Spot price history API](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSpotPriceHistory.html)
- [EKS Auto Mode pricing](https://aws.amazon.com/eks/pricing/)
< [이전: 스케일링 동작](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/03-scaling-behavior.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md) | [다음: 운영 및 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/05-operations.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-auto-mode/05-operations
----------------------------------------
# 운영 및 관리
> **지원 버전**: EKS Auto Mode GA; 예제 기준 EKS 1.36
> **마지막 업데이트**: 2026년 9월 12일
Day-2 운영에서는 원하는 용량, 노드 수명 주기, 애플리케이션 가용성과 실제 수집되는 신호를 구분해야 합니다. 아래는 통제된 실습 구성이지 검증된 프로덕션 runbook이 아닙니다. 이번 감사에서 클라우드 리소스 변경이나 실제 노드·Pod 실행은 하지 않았습니다.
NodePool은 릴리스된 Karpenter 1.14.1 구조 스키마로 확인했습니다. 워크로드는 Kubernetes 1.36.2 구조 및 Restricted Pod Security 정책 검사를 통과했고 nginx 이미지 tag/index digest와 문서화된 non-root 구성을 확인했습니다. 실제 이미지 pull, IAM, 네트워크와 애플리케이션 동작은 해당 환경에서 검증해야 합니다.
## 운영 컨텍스트 확인
임시 자격 증명과 대상 kubeconfig를 사용합니다. 아래 읽기 전용 검사는 계정과 직접 API 엔드포인트를 비교합니다. 의도적으로 proxy를 사용하는 kubeconfig는 불일치를 우회하지 말고 별도로 검토하세요.
```bash
set -euo pipefail
: "${EXPECTED_ACCOUNT_ID:?Set the intended AWS account}"
: "${AWS_REGION:?Set the cluster region}"
: "${CLUSTER_NAME:?Set the intended cluster name}"
: "${KUBECONFIG:?Set the reviewed kubeconfig path}"
export KUBE_CONTEXT="${KUBE_CONTEXT:-$CLUSTER_NAME}"
check_account() {
local account
account=$(aws sts get-caller-identity --region "$AWS_REGION" --query Account --output text) || return
test "$account" = "$EXPECTED_ACCOUNT_ID" || { printf 'Account mismatch; stop.\n' >&2; return 1; }
}
check_account
umask 077
export WORK_DIR
WORK_DIR=$(mktemp -d "$PWD/auto-ops.XXXXXXXX")
aws eks describe-cluster --region "$AWS_REGION" --name "$CLUSTER_NAME" \
--query 'cluster.{arn:arn,endpoint:endpoint}' --output json > "$WORK_DIR/cluster.json"
endpoint=$(kubectl --context "$KUBE_CONTEXT" config view --minify \
-o jsonpath='{.clusters[0].cluster.server}')
jq -e --arg endpoint "$endpoint" '.endpoint == $endpoint' "$WORK_DIR/cluster.json" >/dev/null
printf 'Private diagnostic directory: %s\n' "$WORK_DIR"
```
Manifest는 검토된 `default` NodeClass를 가정합니다. 필요한 예제만 선택하고 리소스 이름·네트워크 ID와 용량·비용을 검토하세요. 실습 풀의 `ops-lab` taint와 워크로드의 toleration/pool selector는 배치 제어이며 테넌트 보안 경계가 아닙니다.
## Budget은 덮어쓰지 않고 함께 평가
적용되는 모든 NodePool budget 중 가장 작은 중단 허용량을 사용합니다. 항상 적용되는 `10%`가 있으면 예약된 `30%`를 더해도 더 많이 허용되지 않습니다. 백분율은 올림 계산하며 삭제 중·NotReady 노드가 남은 허용량을 줄입니다.
다음 달력은 30% 상한, 평일 10% 상한과 업무 시간 1개 상한을 명시합니다. Karpenter schedule은 UTC입니다.
| 항목 | UTC schedule | 의도한 구간 |
|------|--------------|-------------|
| 30% | 항상 | 바깥 상한 |
| 10% | 일–목 15:00, 24시간 | 서울 월–금 달력 날짜 |
| 1개 | 월–금 00:00, 9시간 | 서울 업무 시간 09:00–18:00 |
| 0개 | 매월 1일 00:00, 24시간 | 명시적인 **UTC** 월간 freeze |
그 외 모두 정상인 노드가 20개라면 새 자발적 중단을 주말에는 6개, 평일 업무 외 시간에는 2개, 업무 시간에는 1개, freeze에는 0개 허용합니다. 이는 예시 정책이며 보편적인 프로덕션 권장값이 아닙니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: ops-calendar
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- c
- r
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
taints:
- key: ops-lab
value: 'true'
effect: NoSchedule
metadata:
labels:
example: ops-lab
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m
budgets:
- nodes: 30%
- nodes: 10%
schedule: 0 15 * * sun-thu
duration: 24h
- nodes: '1'
schedule: 0 0 * * mon-fri
duration: 9h
- nodes: '0'
schedule: 0 0 1 * *
duration: 24h
limits:
cpu: '100'
memory: 400Gi
```
이전의 매시간 `9-18`/`9-21` cron과 반복된 48시간 주말 창은 설명된 구간을 넘어 중첩됐습니다. Schedule은 창의 시작점을 지정하며 긴 창을 매시간 다시 시작하라는 뜻이 아닙니다.
## 교체와 애플리케이션 가용성
1개 노드 budget은 해당 graceful 작업의 속도를 제한합니다. Expiration, interruption과 repair도 순차적으로 진행된다는 보장은 아닙니다. `expireAfter`는 최소 uptime 약속이 아니며 바꿔도 기존 NodeClaim 값을 덮어쓰지 않습니다. Auto Mode의 기본 expiry/grace와 최대 수명도 적용됩니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: ops-rolling
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
expireAfter: 168h
terminationGracePeriod: 24h
taints:
- key: ops-lab
value: 'true'
effect: NoSchedule
metadata:
labels:
example: ops-lab
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 2m
budgets:
- nodes: '1'
limits:
cpu: '100'
memory: 400Gi
```
빈 노드 consolidation 정책이 drift나 expiry를 비활성화하지는 않습니다. `do-not-disrupt`도 무기한 보존 보장이 아닙니다. Node와 Pod 제어 범위가 다르고 명시적·기본 termination grace period는 blocking Pod가 drift와 최종 종료에 미치는 영향을 바꿉니다. 유지보수에 사용하기 전에 [disruption 설명](https://karpenter.sh/v1.14/concepts/disruption/)을 확인하세요.
### PDB 예제
다음 namespace는 검토한 Kubernetes 버전의 Restricted 정책을 고정합니다. 이미지는 UID/GID 101로 실행하고 8080을 수신하며 PID·임시 파일에 `/tmp`를 사용합니다. Root filesystem을 읽기 전용으로 설정해도 이 writable emptyDir가 필요합니다. 이미지의 종료 신호는 SIGQUIT이며, port 80의 root nginx 이미지와 설정을 그대로 바꿔 쓸 수는 없습니다.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: ops-lab
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.36
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.36
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-app-pdb
namespace: ops-lab
spec:
minAvailable: 3
selector:
matchLabels:
app: web-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: ops-lab
spec:
replicas: 5
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web
image: nginxinc/nginx-unprivileged:1.30.4@sha256:cb92301e719d6639028de775fe8b28e15f58343aca5e5372001311958aafb300
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
readinessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 5
terminationGracePeriodSeconds: 60
automountServiceAccountToken: false
nodeSelector:
karpenter.sh/nodepool: ops-calendar
tolerations:
- key: ops-lab
operator: Equal
value: 'true'
effect: NoSchedule
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
seccompProfile:
type: RuntimeDefault
volumes:
- name: tmp
emptyDir:
sizeLimit: 128Mi
```
정상 desired replica가 5개일 때 `minAvailable: 3`은 다른 제약이 없다면 자발적 Pod 축출을 최대 2개 허용합니다. `maxUnavailable: 1`은 더 엄격한 별도 선택이며 같은 의미의 대체 표기가 아닙니다.
PDB는 healthy/Ready Pod를 기준으로 Eviction API를 제한하며 복제본을 만들거나 모든 장애·직접 삭제를 막지는 않습니다. 백분율은 올림합니다. 복제본 6개의 `minAvailable: "80%"`는 정상 5개를 요구하지만, 단일 복제본의 `maxUnavailable: "30%"`는 그 1개 축출을 허용할 수 있습니다.
상태 저장 시스템에는 실제 quorum과 readiness 의미를 적용하세요. 다수결 3-voter에는 정상 2개가 맞을 수 있으나 5-voter에는 3개가 필요합니다. `minAvailable: 2` 하나로 모든 stateful 워크로드를 다룰 수 없습니다. Singleton PDB는 의도적으로 자발적 drain을 막을 수 있어도 고가용성을 만들지는 않습니다.
## AZ 배치는 최소 용량이나 Failover 보장이 아님
동적 풀의 `limits.cpu`는 합산 상한이며 AZ별 최소값이 아닙니다. 예제는 가능한 AZ를 나열하지만 실제 적합성은 NodeClass 서브넷, 용량과 배치 제약이 결정합니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: ops-multi-az
spec:
template:
spec:
requirements:
- key: topology.kubernetes.io/zone
operator: In
values:
- ap-northeast-2a
- ap-northeast-2b
- ap-northeast-2c
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- c
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
taints:
- key: ops-lab
value: 'true'
effect: NoSchedule
metadata:
labels:
example: ops-lab
limits:
cpu: '100'
memory: 400Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: high-availability-app
namespace: ops-lab
spec:
replicas: 6
selector:
matchLabels:
app: ha-app
template:
metadata:
labels:
app: ha-app
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: ha-app
minDomains: 2
nodeAffinityPolicy: Honor
nodeTaintsPolicy: Honor
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: ha-app
containers:
- name: app
image: nginxinc/nginx-unprivileged:1.30.4@sha256:cb92301e719d6639028de775fe8b28e15f58343aca5e5372001311958aafb300
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
readinessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 5
automountServiceAccountToken: false
nodeSelector:
karpenter.sh/nodepool: ops-multi-az
tolerations:
- key: ops-lab
operator: Equal
value: 'true'
effect: NoSchedule
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
seccompProfile:
type: RuntimeDefault
terminationGracePeriodSeconds: 60
volumes:
- name: tmp
emptyDir:
sizeLimit: 128Mi
```
복제본 6개만으로 3 AZ × 2 배치가 입증되지 않습니다. 예제는 적합한 domain을 최소 2개 요구합니다. 실제 서브넷 범위와 장애 정책을 검토한 뒤 변경하세요. 엄격한 `DoNotSchedule`은 AZ 장애 중 Pod를 Pending에 남길 수 있습니다.
노드마다 복제본 하나를 요구한다면 검토한 Pod template에 다음 affinity 조각을 추가할 수 있습니다. 복제본 9개라면 적합한 노드가 최소 9개 필요합니다. 대응하는 topology와 용량 없이 3-AZ 가용성이 보장되는 것은 아닙니다.
```yaml
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: ha-app
topologyKey: kubernetes.io/hostname
```
Active-active/standby에는 애플리케이션 상태, health check와 트래픽·failover 제어도 필요합니다. Auto Mode는 ARC zonal shift로 장애 AZ의 신규 용량을 피할 수 있으며 autoshift는 별도 구성이 필요합니다. AZ에 묶인 볼륨이나 엄격한 배치 제약을 이동 가능한 상태로 바꿔주지는 않습니다.
### 기존 Capacity Reservation 사용
풀 이름을 `reserved-capacity`로 정하고 On-Demand와 CPU limit을 설정해도 예약이 생성되지는 않습니다. 승인된 기존 예약은 커스텀 NodeClass에서 선택하고 `reserved` 용량을 허용합니다. 예시 reservation ID를 바꾸고 계정, AZ, 유형, 상태, 권한과 남은 예약 용량을 확인하세요.
```yaml
apiVersion: eks.amazonaws.com/v1
kind: NodeClass
metadata:
name: reserved-nodeclass
spec:
instanceProfile: eks-node-instance-profile
subnetSelectorTerms:
- tags:
Name: private-subnet
securityGroupSelectorTerms:
- tags:
Name: worker-restricted
advancedNetworking:
associatePublicIPAddress: false
ephemeralStorage:
size: 100Gi
iops: 3000
throughput: 125
capacityReservationSelectorTerms:
- id: cr-0123456789abcdef0
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: reserved-capacity
spec:
template:
spec:
requirements:
- key: topology.kubernetes.io/zone
operator: In
values:
- ap-northeast-2a
- key: karpenter.sh/capacity-type
operator: In
values:
- reserved
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: reserved-nodeclass
taints:
- key: ops-lab
value: 'true'
effect: NoSchedule
metadata:
labels:
example: ops-lab
limits:
cpu: '100'
memory: 400Gi
```
이 manifest는 EC2 예약이나 고정된 노드 수를 만들지 않습니다. 예약은 사용하지 않아도 비용이 발생할 수 있습니다. Pod 수요와 무관한 desired node count에는 Auto Mode의 `spec.replicas` 정적 풀이 있으며 limits·weight·consolidation·scale 의미가 다릅니다. 정적 용량 참조를 확인하세요. Desired count도 실제 프로비저닝과 정상 용량 확보가 필요합니다.
## 실제 Publisher에 맞춘 모니터링
EKS control-plane 메트릭, 구성한 Container Insights, 자체 Prometheus exporter와 Auto Mode 컴포넌트 로그는 서로 다른 인터페이스입니다. 이전 `karpenter_*` 이름이 들어 있는 `Karpenter` CloudWatch namespace가 자동 생성된다고 가정하지 마세요.
관리형 compute/storage/load-balancer/IPAM 로그는 별도의 Vended Logs delivery로 구성합니다. 기본 control-plane logging이 모든 관리형 컴포넌트 로그를 켜는 스위치는 아닙니다. 시간 범위를 제한하고 IAM, 전송 대상과 요금을 확인합니다.
### 실제 메트릭 Metadata로 Dashboard 만들기
`AWS/EKS` control-plane namespace에서 대상 클러스터에 실제로 반환되는 메트릭을 먼저 확인합니다.
```bash
check_account
aws cloudwatch list-metrics --region "$AWS_REGION" --namespace AWS/EKS \
--dimensions "Name=ClusterName,Value=$CLUSTER_NAME" --output json \
> "$WORK_DIR/metric-catalog.json"
jq '.Metrics | to_entries | map({index:.key,metric:.value})' "$WORK_DIR/metric-catalog.json"
```
정확한 catalog index와 해당 메트릭에 적합한 통계를 선택하세요. 아래 로컬 생성기는 실제 namespace·dimension을 보존하고 widget region을 포함합니다. 없는 메트릭이나 다른 클러스터 항목을 거부해 허구의 패널을 만들지 않습니다.
```bash
: "${METRIC_INDEX:?Choose an exact entry from the captured catalog}"
: "${METRIC_STAT:?Choose the documented statistic for that metric, such as Maximum}"
export METRIC_INDEX METRIC_STAT AWS_REGION CLUSTER_NAME
python3 - <<'PY'
import json, os, re
from pathlib import Path
folder = Path(os.environ["WORK_DIR"])
metrics = json.loads((folder / "metric-catalog.json").read_text())["Metrics"]
index = int(os.environ["METRIC_INDEX"])
if index < 0 or index >= len(metrics):
raise SystemExit("Metric is absent; verify collection instead of creating an empty widget")
metric = metrics[index]
if metric["Namespace"] != "AWS/EKS" or not any(
d["Name"] == "ClusterName" and d["Value"] == os.environ["CLUSTER_NAME"]
for d in metric["Dimensions"]
):
raise SystemExit("Metric catalog entry does not match the reviewed cluster")
stat = os.environ["METRIC_STAT"]
if stat not in {"Average", "Sum", "Minimum", "Maximum", "SampleCount"}:
if not re.fullmatch(r"p[0-9]+(?:\.[0-9]+)?", stat) or not 0 <= float(stat[1:]) <= 100:
raise SystemExit("Unsupported statistic for this template")
series = [metric["Namespace"], metric["MetricName"]]
for dim in sorted(metric["Dimensions"], key=lambda d: d["Name"]):
series.extend([dim["Name"], dim["Value"]])
body = {"widgets": [{"type": "metric", "x": 0, "y": 0, "width": 12, "height": 6,
"properties": {"title": metric["MetricName"], "region": os.environ["AWS_REGION"],
"view": "timeSeries", "metrics": [series], "stat": stat, "period": 60}}]}
(folder / "dashboard.json").write_text(json.dumps(body, indent=2) + "\n")
PY
```
승인된 절차로 dashboard를 생성·갱신하기 전에 `dashboard.json`을 검토하세요. Catalog에 있다고 모든 시간 구간에 datapoint가 있다는 뜻은 아닙니다. NodePool 개수, 프로비저닝 지연과 애플리케이션 SLO에는 실제 publisher·계측이 필요하며 Container Insights 하나로 모든 신호를 얻는다고 가정해서는 안 됩니다.
## 구조화된 Kubernetes 진단
존재하지 않는 `.status.phase` 대신 NodeClaim condition을 사용합니다. 모든 Pending Pod를 노드 용량 요청으로 취급하지 마세요.
```bash
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get nodeclaims -o json |
jq '[.items[] | {name: .metadata.name, uid: .metadata.uid, node: .status.nodeName,
pool: .metadata.labels["karpenter.sh/nodepool"], createdAt: .metadata.creationTimestamp,
expireAfter: .spec.expireAfter, terminationGracePeriod: .spec.terminationGracePeriod,
imageID: .status.imageID,
conditions: [.status.conditions[]? | {type,status,reason,lastTransitionTime,observedGeneration}]}]'
```
```bash
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get nodepools -o json |
jq '[.items[] | {name:.metadata.name,limits:.spec.limits,resources:.status.resources,
requirements:.spec.template.spec.requirements,disruption:.spec.disruption,
conditions:[.status.conditions[]? | {type,status,reason,observedGeneration}]}]'
```
```bash
: "${WORKLOAD_NAMESPACE:?Select the workload namespace}"
: "${POD_NAME:?Select the Pod}"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n "$WORKLOAD_NAMESPACE" \
get pod "$POD_NAME" -o json |
jq '{name:.metadata.name,uid:.metadata.uid,node:.spec.nodeName,phase:.status.phase,
nodeSelector:.spec.nodeSelector,affinity:.spec.affinity,tolerations:.spec.tolerations,
conditions:[.status.conditions[]? | {type,status,reason,lastTransitionTime}],
containers:[.status.containerStatuses[]? |
{name,ready,restartCount,waitingReason:.state.waiting.reason}]}'
```
노드별 분포는 사람이 읽는 표의 열 번호 대신 구조화된 필드로 계산합니다. 아래는 배치됐지만 준비되지 않은 Pod를 포함한 active Pod 객체를 세고 미배치 객체를 구분합니다.
```bash
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get pods -A -o json |
jq '[.items[] | select(.status.phase != "Succeeded" and .status.phase != "Failed") |
{node: (.spec.nodeName // "(unscheduled)"), namespace: .metadata.namespace, pod: .metadata.name}] |
group_by(.node) | map({node: .[0].node, activePodObjects: length})'
```
`kubectl top`은 metrics API가 구성되고 정상일 때 사용합니다. 실패했다고 metrics-server가 없다고 단정하지 마세요. 실제 인스턴스 호환성, NodeClass readiness, IAM·네트워크 문제는 event reason과 관리형 compute 로그로 확인합니다. `consolidateAfter`를 줄여도 Spot interruption 복구가 빨라지는 것은 아닙니다.
### PDB 허용량은 준수 여부 판정이 아님
`disruptionsAllowed`가 0이어도 의도한 정상 상태일 수 있습니다. Generation과 현재·원하는 health를 먼저 확인하세요.
```bash
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get pdb -A -o json |
jq '[.items[] | {
namespace: .metadata.namespace, name: .metadata.name,
currentHealthy: .status.currentHealthy, desiredHealthy: .status.desiredHealthy,
disruptionsAllowed: .status.disruptionsAllowed,
assessment: (if .status.observedGeneration != .metadata.generation
or .status.currentHealthy == null or .status.desiredHealthy == null
or .status.disruptionsAllowed == null
then "UnknownOrStale"
elif .status.currentHealthy < .status.desiredHealthy then "BelowDesiredHealthy"
elif .status.disruptionsAllowed == 0 then "HealthyNoVoluntaryEvictions"
else "EvictionsPermitted" end)
}]'
```
워크로드 가용성 요구와 복구 계획을 이해하기 전에 blocking PDB를 삭제하거나 완화하지 마세요.
### Node 객체의 나이
Node 객체 생성 시각은 EC2 시작 시각이나 AMI patch age가 아닙니다. 정책에 맞는 진단 임계값을 선택하세요. 이전의 고정된 “7일 정수 초과” 스크립트는 시간을 버림 처리하고 부적절한 보편적 한도를 가정했습니다.
```bash
: "${MAX_NODE_OBJECT_AGE_HOURS:?Set a reviewed diagnostic threshold in hours}"
export MAX_NODE_OBJECT_AGE_HOURS
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get nodes \
-l eks.amazonaws.com/compute-type=auto -o json |
jq '{items:[.items[] | {name:.metadata.name,createdAt:.metadata.creationTimestamp}]}' \
> "$WORK_DIR/node-times.json"
python3 - <<'PY'
import json, math, os
from datetime import datetime, timezone
from pathlib import Path
limit = float(os.environ["MAX_NODE_OBJECT_AGE_HOURS"])
if not math.isfinite(limit) or limit <= 0:
raise SystemExit("Set a finite positive threshold")
now = datetime.now(timezone.utc)
rows = []
for item in json.loads((Path(os.environ["WORK_DIR"]) / "node-times.json").read_text())["items"]:
result = {"node": item["name"], "thresholdHours": limit}
try:
created = datetime.fromisoformat(item.get("createdAt").replace("Z", "+00:00"))
if created.tzinfo is None or created > now:
raise ValueError("timestamp is not usable")
hours = (now - created).total_seconds() / 3600
result.update(nodeObjectAgeHours=round(hours, 3), exceedsThreshold=hours > limit)
except (ValueError, TypeError, AttributeError):
result["assessment"] = "UnknownTimestamp"
rows.append(result)
print(json.dumps(rows, indent=2))
PY
```
알 수 없거나 미래의 timestamp를 정상으로 보고하지 않습니다. 교체 지연을 판단하기 전에 관측된 NodeClaim 정책, 이미지 정보와 AWS 유지보수·보안 정보를 대조하세요. 이 로컬 보고서는 CloudWatch alarm을 생성하지 않습니다.
## 보안 구성
Auto Mode는 관리형 Bottlerocket 이미지와 고정된 IMDSv2/hop-limit 설정을 사용합니다. 이전 `amiFamily`, `metadataOptions`, `blockDeviceMappings`와 잘못된 KMS ARN은 유효한 Auto Mode NodeClass 구성이 아니었습니다.
```yaml
apiVersion: eks.amazonaws.com/v1
kind: NodeClass
metadata:
name: ops-nodeclass
spec:
instanceProfile: eks-node-instance-profile
subnetSelectorTerms:
- tags:
Name: private-subnet
securityGroupSelectorTerms:
- tags:
Name: worker-restricted
advancedNetworking:
associatePublicIPAddress: false
ephemeralStorage:
size: 100Gi
iops: 3000
throughput: 125
```
Profile의 node role/access entry, 실제 프라이빗 라우팅과 보안 그룹 규칙을 검토하세요. 태그만으로 네트워크 제한이 입증되지는 않습니다. 노드 루트·데이터 EBS 암호화가 애플리케이션 PVC 암호화를 뜻하지도 않습니다. 고객 관리 키나 CA bundle에는 [NodePool 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/02-nodepool-configuration.md)의 유효한 `ephemeralStorage.kmsKeyID`·인증서 절차와 검토한 IAM/key policy를 사용하세요.
Restricted namespace에는 호환되는 워크로드 security context가 필요합니다. Host 접근이 필요한 노드 모니터링 agent를 억지로 이 namespace에 넣거나 agent 실행을 위해 클러스터 전체 보안을 해제하지 마세요. Collector의 별도 권한·namespace 정책을 검토합니다.
## 선택적인 Prometheus Query와 Alert
아래는 **단일 클러스터**, 설치된 kube-state-metrics와 Linux node-exporter 메트릭을 가정합니다.
- kube-state-metrics에서 `karpenter.sh/nodepool`, `eks.amazonaws.com/compute-type` node label을 allowlist에 포함합니다.
- CPU series에 정확히 매핑된 `node` label이 필요합니다. 모든 node-exporter scrape 구성에 자동으로 붙는 label은 아닙니다.
- Join/count 전에 kube-state-metrics replica를 중복 제거합니다. 한 노드에는 하나의 현재 NodePool label이 있어야 합니다.
- 여러 클러스터라면 모든 소스에 실제 cluster label을 보존하고 모든 group/join에 포함합니다.
Node query는 Auto Mode 노드만 선택합니다. Pod pending/unschedulable 신호는 클러스터 전체 기준이므로 Auto Mode 문제로 분류하기 전에 진단해야 합니다. CPU 식은 **노드별 non-idle 시간**이며 request 비율이나 용량 가중 pool 평균이 아닙니다. Series 부재를 자동으로 사용량 0으로 해석하지 마세요.
```promql
# nodes_by_pool
count by (label_karpenter_sh_nodepool) (max by (node, label_karpenter_sh_nodepool) (kube_node_labels{label_karpenter_sh_nodepool!="",label_eks_amazonaws_com_compute_type="auto"}))
# cpu
100 * (1 - avg by (node) (rate(node_cpu_seconds_total{mode="idle",node!=""}[5m])))
* on (node) group_left (label_karpenter_sh_nodepool)
max by (node, label_karpenter_sh_nodepool) (kube_node_labels{label_karpenter_sh_nodepool!="",label_eks_amazonaws_com_compute_type="auto"})
# pending
sum(max by (namespace, pod, uid) (kube_pod_status_phase{phase="Pending"}))
# unschedulable
sum(max by (namespace, pod, uid) (kube_pod_status_unschedulable))
# age
((time() - max by (node) (kube_node_created)) / 86400)
* on (node) group_left (label_karpenter_sh_nodepool)
max by (node, label_karpenter_sh_nodepool) (kube_node_labels{label_karpenter_sh_nodepool!="",label_eks_amazonaws_com_compute_type="auto"})
# not_ready
max by (node) (kube_node_status_condition{condition="Ready",status=~"false|unknown"})
* on (node) group_left (label_karpenter_sh_nodepool)
max by (node, label_karpenter_sh_nodepool) (kube_node_labels{label_karpenter_sh_nodepool!="",label_eks_amazonaws_com_compute_type="auto"})
```
Prometheus Operator가 설치돼 있다면 Prometheus가 선택하는 namespace와 `ruleSelector`에 맞춰 다음 rule을 배치합니다. 임계값·기간은 설명용이며 모든 프로비저닝 실패가 termination counter로 표현된다는 뜻이 아닙니다.
```yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: auto-ops-example
namespace: monitoring
spec:
groups:
- name: auto-ops-example
rules:
- alert: ReportedUnschedulablePods
expr: sum(max by (namespace, pod, uid) (kube_pod_status_unschedulable)) > 10
for: 5m
labels:
severity: warning
annotations:
summary: Reported unschedulable Pods exceed the example threshold
- alert: AutoNodeNotReady
expr: '(max by (node) (kube_node_status_condition{condition="Ready",status=~"false|unknown"})
* on (node) group_left (label_karpenter_sh_nodepool)
max by (node, label_karpenter_sh_nodepool) (kube_node_labels{label_karpenter_sh_nodepool!="",label_eks_amazonaws_com_compute_type="auto"}))
== 1'
for: 5m
labels:
severity: warning
annotations:
summary: A registered Auto Mode node is not Ready
```
## 점검 주기
| 주기 | 검토할 증거 |
|------|-------------|
| 일일 | 지속되는 scheduling/NodeClaim 오류, Node condition, 워크로드 health, 현재 PDB 허용량과 collector 최신성 |
| 주간 | Disruption/drift event, 실제 객체·노드 나이, 용량·Spot 분포, resource requests와 청구 추세 |
| 변경 전·월간 | IAM/네트워크/스토리지 정책, 호환 소프트웨어·이미지 갱신, 검증한 복구, 할당량과 향후 수요 |
이전의 “Pending 0–5”, “CPU/메모리 80% 미만”, “시작 90초 미만”, “가용성 99.9%”와 응답 시간 범위는 검증되지 않은 계획용 기준이며 Auto Mode 정상 범위나 기본 SLO가 아닙니다. 모든 전환 상태나 PDB 허용량 0을 위반으로 부르지 말고 애플리케이션 목표·실측 동작에 근거해 임계값을 정의하세요.
## 참고 자료
- [Auto Mode NodePool behavior](https://docs.aws.amazon.com/eks/latest/userguide/create-node-pool.html)
- [Disruption budgets, drift and termination](https://karpenter.sh/v1.14/concepts/disruption/)
- [Configure a PDB](https://kubernetes.io/docs/tasks/run-application/configure-pdb/)
- [Topology spread constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/)
- [Auto Mode static capacity](https://docs.aws.amazon.com/eks/latest/userguide/auto-static-capacity.html)
- [NodeClass and capacity reservation selectors](https://docs.aws.amazon.com/eks/latest/userguide/create-node-class.html)
- [EKS ARC zonal shift](https://docs.aws.amazon.com/eks/latest/userguide/zone-shift-enable.html)
- [Managed component log delivery](https://docs.aws.amazon.com/eks/latest/userguide/auto-managed-component-logs.html)
- [Control-plane metrics and CloudWatch](https://aws.amazon.com/blogs/containers/proactive-amazon-eks-monitoring-with-amazon-cloudwatch-operator-and-aws-control-plane-metrics/)
- [CloudWatch dashboard structure](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Dashboard-Body-Structure.html)
- [Kube-state-metrics node metrics](https://github.com/kubernetes/kube-state-metrics/blob/main/docs/metrics/cluster/node-metrics.md)
- [Kube-state-metrics Pod metrics](https://github.com/kubernetes/kube-state-metrics/blob/main/docs/metrics/workload/pod-metrics.md)
- [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/)
- [NGINX unprivileged image](https://github.com/nginx/docker-nginx-unprivileged)
< [이전: Spot 전략](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/04-spot-strategies.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md) | [다음: 비용 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/06-cost-management.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-auto-mode/06-cost-management
----------------------------------------
# 비용 관리 및 최적화
> **지원 버전**: EKS Auto Mode GA; 예제 검토 기준 EKS 1.36
> **마지막 업데이트**: 2026년 9월 12일
비용 최적화에는 같은 유효 작업량을 비교하는 청구 증거가 필요합니다. 노드 개수 snapshot, 낮은 CPU 사용률이나 광고 할인율은 실측 절감액이 아닙니다. 아래 예제는 소스·스키마·CLI 계약을 로컬에서 확인했으며 구매, 클라우드 배포나 실제 청구 조회는 수행하지 않았습니다.
## 청구 구성 요소
EKS 클러스터 요금, EC2 사용량, **별도 Auto Mode 요금**, EBS, 로드 밸런서, NAT/데이터 전송, 관측 및 기타 워크로드 서비스를 포함합니다. Auto Mode compute 요금은 최소 1분·초 단위이며 EC2 구매 옵션과 독립적입니다. EC2 Savings Plans/RI 할인이 별도 Auto Mode 요금에 적용되는 것은 아닙니다.
### 2026년 7월 GPU 요금 인하
AWS 7월 발표에 따르면 7월 1일부터 G 시리즈의 **Auto Mode 관리 요금**은 35%, P 시리즈/Trainium은 60% 인하되어 지원 리전에 자동 적용됩니다. 해당 요금 구성 요소의 인하이며 전체 GPU 청구액에 같은 비율이 적용되지는 않습니다. 발표에는 local NVMe GPU 인스턴스의 병렬 이미지 pull/unpack과 가속기 인식 복구도 포함되지만 이 예제의 실측 시작·애플리케이션 복구 시간을 입증하지는 않습니다.
## 비용을 고려한 배치
먼저 [운영 및 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/05-operations.md)의 계정/API 엔드포인트 검사와 private `WORK_DIR`를 사용하세요. 예제는 검토된 `default` NodeClass와 호환 용량을 가정합니다. 배포 전 비용을 검토합니다.
다음 완전한 실습 워크로드는 이전 영어 예제의 누락된 selector/image를 보완합니다. 운영 장에서 확인한 non-root nginx 이미지, Restricted namespace와 pool selector/toleration을 사용합니다. Requests는 **실측값이 아닌 설명용 값**입니다. ARM을 허용하기 전에 다중 아키텍처 이미지·라이브러리·애플리케이션 동작을 확인하세요.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: cost-lab
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.36
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.36
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: cost-optimized
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- c
- r
- i
- d
- key: kubernetes.io/arch
operator: In
values:
- amd64
- arm64
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
taints:
- key: cost-lab
value: 'true'
effect: NoSchedule
metadata:
labels:
cost-lab: 'true'
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
budgets:
- nodes: 10%
limits:
cpu: '100'
memory: 400Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cost-efficient-app
namespace: cost-lab
spec:
replicas: 5
selector:
matchLabels:
app: cost-efficient
template:
metadata:
labels:
app: cost-efficient
spec:
containers:
- name: web
image: nginxinc/nginx-unprivileged:1.30.4@sha256:cb92301e719d6639028de775fe8b28e15f58343aca5e5372001311958aafb300
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
readinessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 5
terminationGracePeriodSeconds: 60
automountServiceAccountToken: false
nodeSelector:
karpenter.sh/nodepool: cost-optimized
tolerations:
- key: cost-lab
operator: Equal
value: 'true'
effect: NoSchedule
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
seccompProfile:
type: RuntimeDefault
volumes:
- name: tmp
emptyDir:
sizeLimit: 128Mi
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
```
Spot과 On-Demand를 함께 허용하면 용량을 선택할 수 있지만 soft Pod preference가 비율이나 즉각적인 fallback을 보장하지는 않습니다. Category 다양성도 리전·NodeClass·워크로드·실제 재고가 허용해야 유효합니다. 별도 티어 풀은 interruption/GPU/아키텍처 제약을 표현할 수 있으나 불필요한 분할은 packing 효율을 낮출 수 있습니다.
`WhenEmptyOrUnderutilized`는 requests와 제약으로 Pod를 더 저렴하게 재배치할 수 있는지 판단하며 CPU 사용률 임계값이 아닙니다. `consolidateAfter`는 debounce이지 삭제 마감 시각이 아닙니다. PDB·affinity·budget이 consolidation을 막을 수 있습니다. Pool CPU/memory limits는 인프라 확장을 제한하지만 급격한 프로비저닝에서는 eventual consistency로 일시적으로 초과할 수 있습니다. 통화 예산이나 계정 전체의 강제 지출 상한은 아닙니다.
## 청구와 운영 메트릭 구분
현재 노드 gauge는 node-hours가 아니며, node-hours도 인스턴스·요율·시간 없이 달러가 되지 않습니다. Auto Mode가 이전 예제의 허구 `Karpenter` CloudWatch 비용 메트릭을 자동 발행하지는 않습니다. 금액에는 실제 청구 export/Cost Explorer를, 용량·성능에는 구성한 collector를 사용하세요.
### CloudWatch 청구 개요
Billing alerts/metrics를 활성화하면 전 세계 계정의 `AWS/Billing` 추정 요금이 **us-east-1**에 발행됩니다. 현재 월의 누적 추정 요금이며 일일 EC2 지출, 특정 EKS 클러스터 합계나 예측값이 아닙니다. Payer 계정의 linked-account 범위도 확인하세요. 다음 로컬 dashboard 정의에는 필요한 currency dimension과 region이 있습니다.
```json
{
"widgets": [
{
"type": "metric",
"x": 0,
"y": 0,
"width": 12,
"height": 6,
"properties": {
"title": "Account estimated charges, month to date (USD)",
"region": "us-east-1",
"view": "timeSeries",
"metrics": [
[
"AWS/Billing",
"EstimatedCharges",
"Currency",
"USD"
]
],
"period": 21600,
"stat": "Maximum"
}
}
]
}
```
서비스·클러스터 할당과 일별 변화에는 Cost Explorer/CUR 또는 Data Exports를 사용합니다. AWS Budgets/Cost Anomaly Detection은 검토한 금액 임계값을 알릴 수 있지만 강제 지출 상한은 아닙니다. 노드 개수 alarm은 별도 용량 제어이며 실제 publisher·dimension·알림 대상이 필요합니다. 이전 alarm은 허구의 메트릭과 정의되지 않은 SNS 리소스를 참조했습니다.
### Kubecost
검토한 stable chart/app은 **3.2.4**, chart 이름은 새 저장소의 `kubecost`입니다. 더 최신 release candidate를 무조건 업그레이드 대상으로 선택하지 않았습니다. 버전 3은 FinOps agent와 ClickHouse 기반 구조를 사용합니다. 이전 `cost-analyzer` 저장소, CLI의 `kubecostToken` 예제나 버전 2 Prometheus 배포 가정을 재사용하지 마세요.
버전에 맞춰 라이선스, cluster identity, 청구 연동, 범위가 제한된 워크로드 IAM, 인증, 보존과 스토리지 values를 준비합니다. Auto Mode EBS StorageClass는 `ebs.csi.eks.amazonaws.com`이며 필요한 곳에 명시적으로 선택하고 암호화를 검토하세요. Chart에는 영구 데이터 보존/keep annotation이 있으므로 uninstall만으로 모든 유료 스토리지 삭제가 입증되지 않습니다. 접근을 private으로 유지하고 collector/telemetry 동작을 검토합니다. 설치 전에 로컬로 렌더링하세요.
```bash
: "${KUBECOST_VALUES:?Set the reviewed Kubecost 3.2.4 values file}"
test -f "$KUBECOST_VALUES"
helm repo add kubecost https://kubecost.github.io/kubecost/
helm repo update kubecost
helm show chart kubecost/kubecost --version 3.2.4
helm template cost-review kubecost/kubecost --version 3.2.4 \
--namespace kubecost --values "$KUBECOST_VALUES" \
> "$WORK_DIR/kubecost-rendered.yaml"
```
이 명령은 설치하지 않습니다. Pod/namespace/idle cost 할당에는 agent와 일치하는 데이터 소스가, 실제 청구 대조에는 AWS 연동이 필요합니다. Namespace label만으로 청구 데이터가 생성되지는 않습니다. 호환되지 않는 추정치를 더하지 말고 idle/shared cost·할인·미할당 비용을 조정하세요.
## Spot 절감 측정
아래 구조화된 snapshot은 노드 0개, 누락된 label과 혼합 인스턴스를 처리합니다. **Auto Mode Node 객체**를 세며 청구 시간이나 지출을 계산하지 않습니다.
```bash
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get nodes \
-l eks.amazonaws.com/compute-type=auto -o json |
jq '[.items[] | {
capacity: (.metadata.labels["karpenter.sh/capacity-type"] // "unknown"),
instanceType: (.metadata.labels["node.kubernetes.io/instance-type"] // "unknown")
}] as $nodes |
{
totalNodeObjects: ($nodes | length),
byCapacityAndType: ($nodes | group_by([.capacity,.instanceType]) |
map({capacity: .[0].capacity, instanceType: .[0].instanceType, count:length})),
spotNodePercent: (if ($nodes|length) == 0 then null
else 100 * ([$nodes[]|select(.capacity=="spot")]|length) / ($nodes|length) end)
}'
```
비용 비교에는 같은 기간, 리전/AZ, 인스턴스/OS/tenancy, 통화와 유효 작업량 조건을 사용합니다. 과거 Spot 요율은 AZ·시각별로 다릅니다. 인스턴스 유형만으로 선택한 Pricing API 결과는 리전/OS/tenancy/상품이 다를 수 있고 이전 스크립트는 API 오류도 숨겼습니다. 현재 가격 표본으로 지난달 실제 청구를 재구성할 수는 없습니다.
```text
reference_total = cost of the reviewed On-Demand counterfactual for the same useful work
actual_total = actual compute + Auto Mode fees + other allocated costs
+ recovery costs not already included in those billed components
savings_amount = reference_total - actual_total
savings_percent = 100 * savings_amount / reference_total (reference_total > 0)
```
중단·재시도 작업과 idle/미사용 약정은 한 번씩 포함하고 복구 compute를 중복 계산하지 마세요. 기준 비용에도 대응하는 Auto Mode/기타 요금이 필요합니다. 가정한 70% Spot 할인이나 현재 Spot 노드 비율을 실제 월간 절감이라고 부를 수 없습니다. Interruption event의 증거도 따로 유지하세요. 추측한 termination counter reason이 모든 interruption을 측정하지는 않습니다.
### Cost Explorer 읽기 전용 조회
먼저 AWS 생성 태그 **`aws:eks:cluster-name`**을 활성화합니다. `eks:cluster-name`은 문서화된 청구 key가 아닙니다. 참여 EC2 인스턴스 비용을 할당하며 **control-plane 요금이나 모든 클러스터 관련 서비스를 포함하지는 않습니다**. 계정/payer 범위와 태그 coverage를 검토하세요. Cost Explorer API 조회 자체에도 요금이 발생할 수 있습니다.
시작 포함·종료 제외 기간을 명시합니다. 다음 요청은 금액인 `AmortizedCost`를 사용하며 다른 단위의 `UsageQuantity`를 더하거나 노드 비율을 달러로 바꾸지 않습니다.
```bash
: "${COST_START:?Set YYYY-MM-DD inclusive start}"
: "${COST_END:?Set YYYY-MM-DD exclusive end, no later than today UTC}"
export COST_START COST_END CLUSTER_NAME
python3 - <<'PY'
import json, os
from datetime import date, datetime, timezone
from pathlib import Path
start, end = (date.fromisoformat(os.environ[key]) for key in ("COST_START", "COST_END"))
if not start < end <= datetime.now(timezone.utc).date():
raise SystemExit("Require start < end <= today UTC")
request = {
"TimePeriod": {"Start": start.isoformat(), "End": end.isoformat()},
"Granularity": "DAILY",
"Metrics": ["AmortizedCost"],
"Filter": {"Tags": {"Key": "aws:eks:cluster-name", "Values": [os.environ["CLUSTER_NAME"]]}},
"GroupBy": [{"Type": "DIMENSION", "Key": "INSTANCE_TYPE"},
{"Type": "DIMENSION", "Key": "PURCHASE_TYPE"}]
}
(Path(os.environ["WORK_DIR"]) / "ce-request.json").write_text(json.dumps(request, indent=2) + "\n")
PY
```
```bash
check_account
aws ce get-cost-and-usage --region us-east-1 \
--cli-input-json "file://$WORK_DIR/ce-request.json" \
--output json > "$WORK_DIR/ce-result.json"
jq '[.ResultsByTime[] | {period:.TimePeriod,estimated:.Estimated,groups:.Groups}]' \
"$WORK_DIR/ce-result.json"
```
`Estimated` 표시를 보존하고 환불·credit·할인 할당·불완전한 데이터를 고려합니다. 태그 필터에는 미할당 요금이나 미사용 약정이 빠질 수 있으므로 합계·절감을 주장하기 전에 전체 청구 데이터와 대조하세요.
## 리소스 적정 크기
고정된 [VPA 1.7.1 설치 가이드](https://github.com/kubernetes/autoscaler/blob/vertical-pod-autoscaler-1.7.1/vertical-pod-autoscaler/docs/installation.md)를 사용합니다. 문서상 Kubernetes 1.28+를 지원하며 특정 in-place 기능은 더 높은 버전이 필요합니다. 이전 `releases/latest/download/...` URL은 유효한 VPA 설치 절차가 아니었습니다. 클러스터 범위 설치 스크립트 적용 전에 CRD·RBAC·metrics-server·컴포넌트/인증서 구성을 검토하세요. `Off`도 정상 recommender가 필요합니다.
아래 리소스는 같은 namespace의 실제 실습 Deployment를 대상으로 합니다.
```yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: cost-app-vpa
namespace: cost-lab
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: cost-efficient-app
updatePolicy:
updateMode: 'Off'
resourcePolicy:
containerPolicies:
- containerName: '*'
minAllowed:
cpu: 100m
memory: 128Mi
maxAllowed:
cpu: '4'
memory: 8Gi
```
```bash
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n cost-lab \
get vpa cost-app-vpa -o json |
jq '{conditions:.status.conditions,recommendations:.status.recommendation.containerRecommendations}'
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n cost-lab \
get deployment cost-efficient-app -o json |
jq '[.spec.template.spec.containers[] | {name,resources}]'
```
`Off`는 권장값만 만들고 적용하지 않습니다. 모든 컨테이너, recommendation condition/이력, 계절성, 시작 peak, latency, CPU throttling과 memory/OOM 동작을 확인하세요. `resourcePolicy` 범위가 권장값과 기존 limits의 호환성을 입증하지는 않습니다. VPA는 성능 보장이 아니며 검토한 버전에는 Pod-level resource stanza 관련 제한도 문서화돼 있습니다.
### Kubernetes quantity 단위 보존
CPU `1`은 1 core, `1m`은 1 millicore입니다. `1Gi`는 1024Mi이며 접미사를 지워서는 변환되지 않습니다. 이전 `sed`/`awk` 합계 대신 컨테이너별 원 requests/limits와 사용량을 보존하세요.
```bash
: "${WORKLOAD_NAMESPACE:?Select a namespace}"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n "$WORKLOAD_NAMESPACE" \
get pods -o json |
jq '[.items[] | {pod:.metadata.name,uid:.metadata.uid,
podResources:.spec.resources,overhead:.spec.overhead,
containers:[.spec.containers[]|{name,resources}],
initContainers:[.spec.initContainers[]?|{name,restartPolicy,resources}]}]' \
> "$WORK_DIR/pod-requests.json"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n "$WORKLOAD_NAMESPACE" \
top pods --containers > "$WORK_DIR/container-usage.txt"
```
Metrics 조회는 성공해야 하며 데이터 부재는 사용률 0이 아닙니다. 두 snapshot은 동시 관측도 아니고, 컨테이너 사용량이 scheduler의 전체 Pod request 계산도 아닙니다. Init/restartable-init 컨테이너, Pod-level resources와 overhead도 고려해야 합니다.
| 관측 | 검토할 조치 |
|------|-------------|
| Request가 대표 사용량의 2배 초과로 보임 | 축소 전 peak/SLO 여유 조사; 자동 20–50% 절감 아님 |
| Request가 사용량의 2배 이내 | 최적이라는 증거 아님 |
| 사용량이 request 초과 | 배치·여유 검토; OOM 제한에는 단순 request가 아닌 메모리 **limit**이 관련 |
| Limit이 request보다 매우 큼 | Burst/throttling/OOM 정책 검토; limit만 줄여도 일반적인 request 기반 packing이 좋아지지는 않음 |
### 선택적인 Prometheus 검토 후보
다음 info rule은 **단일 클러스터**, CPU core/memory byte로 정규화된 kube-state-metrics와 namespace/Pod/container label이 있는 컨테이너 수준 cAdvisor를 가정합니다. CPU query는 합계 `cpu="total"` series가 필요하므로 collector label을 확인하세요. Collector replica 중복·인프라 cgroup을 제외하고 양수 request 분모를 요구합니다. 여러 클러스터는 모든 group/join에 실제 cluster label을 포함하세요. Series 부재를 0으로 바꾸지 않습니다. 오래된 series·컨테이너 재생성 영향과 대표 이력도 검토합니다.
1시간 동안 30% 미만이라는 임계값은 검토 trigger 예시이며 자동 request 축소 권장이나 실측 절감이 아닙니다. Prometheus Operator 설치 후 namespace/rule selector를 맞춰 사용하세요.
```yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: cost-review-candidates
namespace: monitoring
spec:
groups:
- name: cost-review-candidates
rules:
- alert: LowCpuRequestUtilization
expr: "((\n sum by (namespace,pod) (max by (namespace,pod,container) (rate(container_cpu_usage_seconds_total{cpu=\"\
total\",container!=\"\",container!=\"POD\",image!=\"\"}[5m])) and on (namespace,pod,container)\
\ max by (namespace,pod,container) (kube_pod_container_resource_requests{resource=\"\
cpu\",unit=\"core\"}))\n / sum by (namespace,pod) (max by (namespace,pod,container)\
\ (kube_pod_container_resource_requests{resource=\"cpu\",unit=\"core\"}))\n\
) and on (namespace,pod) (sum by (namespace,pod) (max by (namespace,pod,container)\
\ (kube_pod_container_resource_requests{resource=\"cpu\",unit=\"core\"}))\
\ > 0)\nunless on (namespace,pod) count by (namespace,pod) (\n max by (namespace,pod,container)\
\ (kube_pod_container_resource_requests{resource=\"cpu\",unit=\"core\"}) unless\
\ on (namespace,pod,container) max by (namespace,pod,container) (rate(container_cpu_usage_seconds_total{cpu=\"\
total\",container!=\"\",container!=\"POD\",image!=\"\"}[5m]))\n)) < 0.3"
for: 1h
labels:
severity: info
annotations:
summary: Review request sizing; do not automatically reduce it
- alert: LowMemoryRequestUtilization
expr: "((\n sum by (namespace,pod) (max by (namespace,pod,container) (container_memory_working_set_bytes{container!=\"\
\",container!=\"POD\",image!=\"\"}) and on (namespace,pod,container) max by\
\ (namespace,pod,container) (kube_pod_container_resource_requests{resource=\"\
memory\",unit=\"byte\"}))\n / sum by (namespace,pod) (max by (namespace,pod,container)\
\ (kube_pod_container_resource_requests{resource=\"memory\",unit=\"byte\"\
}))\n) and on (namespace,pod) (sum by (namespace,pod) (max by (namespace,pod,container)\
\ (kube_pod_container_resource_requests{resource=\"memory\",unit=\"byte\"\
})) > 0)\nunless on (namespace,pod) count by (namespace,pod) (\n max by (namespace,pod,container)\
\ (kube_pod_container_resource_requests{resource=\"memory\",unit=\"byte\"\
}) unless on (namespace,pod,container) max by (namespace,pod,container) (container_memory_working_set_bytes{container!=\"\
\",container!=\"POD\",image!=\"\"})\n)) < 0.3"
for: 1h
labels:
severity: info
annotations:
summary: Review request sizing; do not automatically reduce it
```
## Savings Plans와 Reserved Instances
| 옵션 | 적격 사용량/범위 | 주요 제한 |
|------|------------------|-----------|
| Compute Savings Plans | 패밀리·크기·리전·OS·tenancy와 무관한 적격 EC2 사용량, Fargate/Lambda도 포함 | 최대 66%는 광고상 최대이며 예상 할인 아님 |
| EC2 Instance Savings Plans | 한 리전의 선택한 패밀리; 그 범위의 크기·OS·tenancy 유연성 | 최대 72%; 정확한 한 인스턴스 크기에 대한 약정 아님 |
| EC2 RI | 조건이 일치하는 사용량; offering별 regional size 유연성/교환 규칙 | 기존에 일치하는 약정도 Auto Mode에 유효할 수 있음 |
| Spot | 별도 Spot 가격 | Savings Plans 중복 할인 없음 |
적격 Graviton·GPU EC2 사용량에 별도 “ARM Savings Plan”이나 GPU 전체 제외 규칙이 필요한 것은 아닙니다. SageMaker AI Savings Plans는 SageMaker 사용량 대상이며 ML을 실행한다는 이유로 EC2 GPU 노드에 적용되지 않습니다. Savings Plans는 물리 용량을 예약하지 않으므로 capacity reservation은 별도로 검토하세요. 별도 Auto Mode 요금은 EC2 할인 범위 밖입니다.
지속되는 적격·미커버 시간별 사용량으로 **해당 Savings Plans 요율의 USD/hour** 약정을 계산합니다. Baseline에서 이미 Spot을 제외했다면 `(1 - Spot%)`를 다시 곱하지 마세요. 기존 RI/SP coverage, 향후 적정화·아키텍처 변경, 공유 설정, 계절성과 미사용 약정을 고려합니다. 낮은 EC2 요율이나 노드 수만으로 안전한 구매액이 결정되지는 않습니다.
```bash
check_account
aws ce get-savings-plans-purchase-recommendation --region us-east-1 \
--savings-plans-type COMPUTE_SP --term-in-years ONE_YEAR \
--payment-option NO_UPFRONT --lookback-period-in-days THIRTY_DAYS \
--output json > "$WORK_DIR/savings-plan-recommendation.json"
```
권장값 조회이며 구매 명령이 아닙니다. 30일 lookback은 API 선택지이므로 결정 전에 더 긴 대표 이력과 비교하세요.
이전 Compute coverage 60–70%/70%, EC2 Instance coverage 30–40%, On-Demand coverage 50%와 Spot 40–60% / covered 30–40% / uncovered 10–20% 그림은 **검증되지 않은 계획 예시**이며 서로 더하는 보편적 목표가 아닙니다. 워크로드는 Spot 또는 On-Demand 용량에서 실행되며 Savings Plans는 적격 사용량의 청구 coverage이지 세 번째 노드 유형이 아닙니다.
## 비용 귀속
유효한 NodeClass identity/network selector와 승인된 태깅 권한을 사용합니다. 커스텀 NodeClass에는 node-role access entry가 필요합니다. 태그는 리소스를 설명하며 네트워크 격리나 모든 종속 서비스 coverage를 자동 생성하지 않습니다.
```yaml
apiVersion: eks.amazonaws.com/v1
kind: NodeClass
metadata:
name: tagged-nodeclass
spec:
instanceProfile: eks-node-instance-profile
subnetSelectorTerms:
- tags:
Name: private-subnet
securityGroupSelectorTerms:
- tags:
Name: worker-restricted
advancedNetworking:
associatePublicIPAddress: false
ephemeralStorage:
size: 100Gi
iops: 3000
throughput: 125
tags:
Environment: lab
Team: platform
Project: web-services
CostCenter: CC-12345
Application: cost-lab
ManagedBy: eks-auto-mode
```
이 예제를 사용하려면 대상 pool에서 `tagged-nodeclass`를 의도적으로 참조하세요. `amiFamily: AL2023`은 Auto Mode NodeClass 필드가 아닙니다. 실제 리소스 태그를 확인하고 Billing에서 key를 활성화합니다. 사용자 정의 key는 활성화 목록에 보이는 데 최대 24시간, 그 후 활성화에도 최대 24시간이 걸릴 수 있으며 보고서 최신성은 별도입니다. 정확히 24시간 후 모든 데이터가 나온다고 보장하지 마세요.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: team-a
labels:
cost-center: team-a
environment: production
```
Namespace label은 Kubernetes 비용 할당 도구에 사용할 수 있지만 AWS 비용 할당 태그로 자동 전파되지는 않습니다. EC2 태그 귀속도 shared/control-plane/storage/network 비용 배분을 대체하지 않습니다.
## 최적화 체크리스트와 이전 수치
호환 인스턴스 다양성, 워크로드에 맞는 Spot, 측정한 request 적정화, 가능한 consolidation, 청구 통합과 약정 검토를 별도 작업으로 관리합니다. 변경 전후 전체 유효 작업 비용과 가용성을 측정하세요.
아래 이전 추정치에는 확인된 benchmark/청구 출처가 없습니다. 맥락 보존용이며 보장값이 아니고, 더하거나 곱해서 전체 절감액이라고 주장해서는 안 됩니다.
| 이전 주제 | 원래 예시 범위 |
|-----------|----------------|
| Spot | 60–70%, 60–90%, 70–90% |
| ARM/Graviton | 20% |
| Request 적정화/VPA | 10–30%, 15–30%, 20–40%, 20–50% |
| Consolidation | 10–20%, 10–30% |
| Savings Plans | 20–30%, 20–40% |
| Multi-AZ/스케줄링 | 5–10% / 10–20% |
AZ 복원력 축소는 일반적인 비용 최적화가 아닙니다. 배치·스케줄링 실험에는 데이터 전송, 장애 복구, 스토리지 보존과 워크로드 목표를 함께 반영하세요.
## 참고 자료
- [EKS pricing and Auto Mode charges](https://aws.amazon.com/eks/pricing/)
- [July 2026 GPU management-fee reduction](https://aws.amazon.com/about-aws/whats-new/2026/07/amazon-eks-auto-mode-gpu-price/)
- [Auto Mode cost controls](https://docs.aws.amazon.com/eks/latest/userguide/auto-cost-control.html)
- [NodePool resource limits and disruption](https://karpenter.sh/v1.14/concepts/nodepools/)
- [Savings Plans types](https://docs.aws.amazon.com/savingsplans/latest/userguide/plan-types.html)
- [Savings Plans versus RIs](https://docs.aws.amazon.com/savingsplans/latest/userguide/sp-ris.html)
- [EKS billing tags](https://docs.aws.amazon.com/eks/latest/userguide/eks-using-tags.html)
- [Activating cost allocation tags](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/activating-tags.html)
- [CloudWatch estimated billing charges](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html)
- [Kubecost 3.2.4 chart](https://kubecost.github.io/kubecost/kubecost-3.2.4.tgz)
- [VPA 1.7.1 installation](https://github.com/kubernetes/autoscaler/blob/vertical-pod-autoscaler-1.7.1/vertical-pod-autoscaler/docs/installation.md)
- [VPA known limitations](https://github.com/kubernetes/autoscaler/blob/vertical-pod-autoscaler-1.7.1/vertical-pod-autoscaler/docs/known-limitations.md)
- [Kubernetes resource units and scheduling](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
- [Auto Mode NodeClass and tags](https://docs.aws.amazon.com/eks/latest/userguide/create-node-class.html)
< [이전: 운영 및 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/05-operations.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md) | [다음: 노드 생명주기](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/07-node-lifecycle.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-auto-mode/07-node-lifecycle
----------------------------------------
# 노드 생명주기 관리
> **지원 버전**: EKS Auto Mode GA; 예제 검토 기준 EKS 1.36
> **마지막 업데이트**: 2026년 9월 12일
Expiration, 관리형 이미지 갱신과 애플리케이션 복구는 노드 수명 주기의 서로 다른 부분입니다. Kubernetes Node 객체가 새롭다고 모든 CVE 패치나 워크로드 규정 준수가 입증되지는 않습니다. 공식 소스·로컬 스키마/fixture로 검토했으며 실제 노드 교체·클라우드 변경·benchmark는 수행하지 않았습니다.
## Expiration과 Auto Mode 수명 제한
`spec.template.spec.expireAfter`는 해당 template에서 생성된 NodeClaim의 나이 기준 만료를 정합니다. 이전 Provisioner 시기의 `ttlSecondsUntilExpired`는 이 `karpenter.sh/v1` NodePool에서 사용하지 않습니다.
다음 세 가지를 구분하세요.
| 개념 | Auto Mode 동작 |
|------|----------------|
| 기본 expiration | AWS 문서상 **336h(14일)**이며 7일·21일이 아님 |
| Termination grace | NodePool에서 생략하면 Auto Mode가 **NodeClaim에 24h**를 기본 적용; pool에 없더라도 claim 확인 |
| 관리형 인스턴스 최대 수명 | AWS가 **21일(504h)**을 적용; 그때까지 노드 가용성을 보장한다는 뜻은 아님 |
Upstream Karpenter의 기본 720h를 Auto Mode 기본값으로 대신 사용하지 마세요. 큰 duration이나 upstream `Never` 문법이 Auto Mode 관리형 인스턴스의 무기한 보존을 제공하지는 않습니다. 504h에 긴 drain을 더해 AWS 최대 수명을 연장할 수 있다고 생각해서도 안 됩니다.
아래 읽기 전용 명령 전에 [운영 및 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/05-operations.md)의 계정/context 검사와 private `WORK_DIR`를 사용합니다. 예제 pool은 검토된 `default` NodeClass를 가정하는 제한·taint가 있는 실습 설정입니다. 일치하는 workload selector/toleration과 애플리케이션 복구 검증이 필요합니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: with-expiration
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
expireAfter: 168h
terminationGracePeriod: 24h
taints:
- key: lifecycle-lab
value: 'true'
effect: NoSchedule
metadata:
labels:
lifecycle-lab: 'true'
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m
budgets:
- nodes: 10%
limits:
cpu: '100'
memory: 400Gi
```
### Expiration의 실제 동작
Expiration은 **forceful disruption trigger**입니다. NodeClaim이 정책 나이에 도달하면 termination/drain을 시작하며 미리 준비한 대체 노드의 Ready를 기다리지 않습니다. NodePool disruption budget은 expiration 속도를 제한하지 않습니다. 워크로드 controller·프로비저닝이 필요에 따라 대체 용량을 만들 수 있지만 이전의 “새 노드 Ready 다음 drain” 순서가 보장되지는 않습니다.
Termination controller는 일반적인 신규 배치를 막고 Eviction API 기반 drain, volume detach와 인스턴스 종료를 처리합니다. PDB와 Pod `do-not-disrupt`가 drain에 영향을 줄 수 있지만 termination grace 마감·AWS 최대 수명이 있어 무기한 가용성 보호는 아닙니다. Pod shutdown grace와 노드 termination grace도 다른 설정입니다. Local/ephemeral storage 데이터에는 복구 계획이 필요합니다.
Pool의 `expireAfter`를 바꿔도 기존 NodeClaim 값은 바뀌지 않습니다. Drift를 유발할 수 있으며 새 claim에 변경 정책이 적용됩니다. 다른 중단 원인으로 노드가 더 일찍 종료될 수도 있습니다. 1개 노드 budget이 여러 동시 expiration을 순차 처리로 바꾸지는 않습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-auto-mode-07-node-lifecycle-0.html)
그림의 graceful 경로는 무기한 PDB 보호를 뜻하지 않습니다. 빈 노드도 controller/finalizer·리소스 정리 절차가 있으므로 “즉시 종료”를 완료 시간 보장으로 읽지 마세요.
### Duration 선택
이전의 환경별 24h·48h·72h·168h·336h는 정책 예시이며 AWS 프로덕션 기본값이나 PCI/HIPAA/SOC2의 필수 교체 간격이 아닙니다. 개발용 504h 예시는 서비스 상한이지 권장 `expireAfter`와 drain 시간 조합이 아닙니다.
패치 긴급도, checkpoint, cache warm-up, replica/quorum, 용량과 복구 검증으로 정책을 정합니다. 잦은 교체는 image pull·재배치·유료 복구 작업을 늘릴 수 있습니다. 특정 Spot 인스턴스의 EC2 interruption 확률 자체를 높이거나 새 패치 가용성을 보장하지는 않습니다.
## 관리형 AMI와 NodeClass 구성
Auto Mode는 AWS 관리형 **Bottlerocket 변형**을 사용합니다. AL2023/Bottlerocket `amiFamily` 전환, 커스텀 `amiSelectorTerms`, 임의 `userData`, SSH나 SSM Session Manager 접근을 제공하지 않습니다. 자체 Karpenter의 EC2NodeClass나 일반 managed node group 절차에서 이 인터페이스를 복사해서는 안 됩니다.
다음 지원 NodeClass는 스토리지·네트워크 identity를 구성하며 AMI를 선택·고정하지 않습니다.
```yaml
apiVersion: eks.amazonaws.com/v1
kind: NodeClass
metadata:
name: lifecycle-nodeclass
spec:
instanceProfile: eks-node-instance-profile
subnetSelectorTerms:
- tags:
Name: private-subnet
securityGroupSelectorTerms:
- tags:
Name: worker-restricted
advancedNetworking:
associatePublicIPAddress: false
ephemeralStorage:
size: 100Gi
iops: 3000
throughput: 125
```
Instance-profile role/access entry와 실제 subnet/security-group 선택을 검토한 뒤 대상 pool에서만 `lifecycle-nodeclass`를 참조하세요. Auto Mode IMDSv2/hop-limit은 관리형이며 `metadataOptions`로 덮어쓰는 필드가 아닙니다. `blockDeviceMappings` 대신 `ephemeralStorage`를 사용합니다. 노드 root/data EBS 암호화가 애플리케이션 PVC 암호화를 입증하지는 않습니다. 지원 key/인증서 설정은 [NodePool 구성](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/02-nodepool-configuration.md)을 참고하세요.
### 과거 OS 비교는 Auto Mode 선택 메뉴가 아님
AL2023은 선택한 Fedora upstream 구성 요소를 사용하는 범용 Amazon Linux 배포판이며 단순한 RHEL 기반 OS가 아닙니다. 일반 AL2023·독립 운영 Bottlerocket 호스트의 관리 기능과 잠긴 Auto Mode 노드의 기능은 다릅니다. Auto Mode는 관리형 이미지로 GPU도 지원하므로 “GPU에는 반드시 AL2023”이라는 설명도 맞지 않습니다.
원래 AL2023 40–60초, Bottlerocket 20–30초/20–40초와 퀴즈의 20–40초 대 15–25초에는 확인된 실측 출처가 없습니다. 과거 예시로 보존하며 OS 속도 순위나 이 manifest의 예측값이 아닙니다. Image pull, 아키텍처, 인스턴스 유형과 workload readiness는 별도 측정이 필요합니다.
## Drift와 관리형 이미지 갱신
AWS는 Auto Mode AMI를 대략 매주 릴리스하고 적격 노드의 drift 교체를 허용한다고 설명합니다. 개별 CVE 패치 SLA는 아닙니다. 다른 EKS-optimized AMI family 릴리스가 자동으로 Auto Mode 이미지 갱신이 되는 것도 아닙니다.
| 변경·관측 | 올바른 해석 |
|-----------|-------------|
| 관리형 Auto Mode AMI 갱신 | 기존 claim이 drift 상태가 될 수 있으므로 실제 condition·rollout 확인 |
| NodePool requirement 변경 | 모든 변경이 drift는 아님; 기존 노드와 호환되는 허용값 확장은 적합성을 유지할 수 있음 |
| `expireAfter` template 변경 | 기존 claim은 저장 값을 유지하고 새 claim이 변경 정책 사용 |
| Weight·limits·disruption | 동작 설정이며 일괄 drift trigger 아님 |
| NodeClass desired state 변경 | 지원 필드를 사용하고 관리형 controller 관측; 장식용 tag는 긴급 패치 trigger 보장 아님 |
| `amiFamily`·`blockDeviceMappings` 변경 | 유효한 Auto Mode NodeClass 필드가 아님 |
NodeClaim의 `Drifted` condition을 사용하세요. Hash annotation은 boolean drift 상태가 아니고 condition 미보고도 “drift 아님”의 증거가 아닙니다. Reason/transition/generation을 현재 리소스와 대조합니다.
```bash
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get nodeclaims -o json |
jq '[.items[] | {
name:.metadata.name,uid:.metadata.uid,node:.status.nodeName,
createdAt:.metadata.creationTimestamp,deletionTimestamp:.metadata.deletionTimestamp,
pool:.metadata.labels["karpenter.sh/nodepool"],
expireAfter:.spec.expireAfter,terminationGracePeriod:.spec.terminationGracePeriod,
imageID:.status.imageID,
drift:([.status.conditions[]?|select(.type=="Drifted")|
{status,reason,lastTransitionTime,observedGeneration}] |
if length == 0 then {status:"NotReported"} else .[0] end),
conditions:[.status.conditions[]?|{type,status,reason,lastTransitionTime,observedGeneration}]
}]'
```

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-auto-mode-07-node-lifecycle-1.html)
그림의 `AL2023 / Bottlerocket` 선택과 무조건적인 “순차적 교체”는 Auto Mode 동작으로 읽으면 안 됩니다. 이 절의 managed image·condition·budget 설명이 적용됩니다.
Drift는 해당 budget, 배치 가능성과 drain 제약을 따르는 graceful 작업입니다. 다음은 자발적 중단 1개를 허용하며 평일 UTC 00:00–08:00, 즉 서울 09:00–17:00에는 **drift만** 멈춥니다. 이전 `0 9-17`/`0 9-18` 매시간 schedule은 긴 창을 반복해 중첩했습니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: controlled-drift
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
expireAfter: 168h
terminationGracePeriod: 24h
taints:
- key: lifecycle-lab
value: 'true'
effect: NoSchedule
metadata:
labels:
lifecycle-lab: 'true'
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 10m
budgets:
- nodes: '1'
- nodes: '0'
schedule: 0 0 * * mon-fri
duration: 8h
reasons:
- Drifted
limits:
cpu: '100'
memory: 400Gi
```
Expiration, EC2 interruption이나 repair를 멈추지는 않습니다. `consolidateAfter`는 consolidation 설정이며 AMI patch 마감이 아닙니다. Pod·Node의 `do-not-disrupt` 범위도 다르고, Pod annotation이 drift를 막는지 판단할 때 Auto Mode의 기본 NodeClaim grace를 고려해야 합니다.
## 패치와 예외적 수동 복구
AWS는 노드 OS·Auto Mode 컴포넌트 패치를 관리하지만 애플리케이션·컨테이너 의존성과 workload 보안은 사용자 책임입니다. 실제 이미지, 관련 AWS 릴리스/권고, rollout 진행과 애플리케이션 검사를 기록합니다. 새로운 timestamp나 `SecurityPatch` tag가 패치 존재 증거는 아닙니다.
긴급 대응 절차:
1. 필요한 관리형 이미지·수정이 이 클러스터에 제공되는지 확인하고 영향 워크로드를 식별합니다.
2. 대상 context에서 **단일 NodeClaim UID**, node mapping, pool, condition, PDB, 데이터 내구성과 여유/확보 가능한 용량을 검토합니다.
3. 요구를 충족하면 관리형 drift rollout을 사용합니다. 수동 교체가 필요하면 검토된 단일 리소스 유지보수 절차를 사용하고 다음 리소스로 가기 전에 대체·애플리케이션 readiness를 확보합니다.
4. 변경마다 health·이미지를 확인하며 상태 불명, 용량 실패나 애플리케이션 회귀 시 중단합니다.
다음은 증거만 수집합니다.
```bash
: "${NODECLAIM_NAME:?Select one NodeClaim for review}"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get nodeclaim "$NODECLAIM_NAME" -o json |
jq '{name:.metadata.name,uid:.metadata.uid,node:.status.nodeName,
pool:.metadata.labels["karpenter.sh/nodepool"],imageID:.status.imageID,
expireAfter:.spec.expireAfter,terminationGracePeriod:.spec.terminationGracePeriod,
conditions:[.status.conditions[]?|{type,status,reason}]}'
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get pdb -A -o json |
jq '[.items[]|{namespace:.metadata.namespace,name:.metadata.name,
observedGeneration:.status.observedGeneration,generation:.metadata.generation,
currentHealthy:.status.currentHealthy,desiredHealthy:.status.desiredHealthy,
disruptionsAllowed:.status.disruptionsAllowed}]'
```
`kubectl delete nodes -l ...`은 일괄 삭제이며 순차 rolling update가 아닙니다. 수동 node/claim 삭제는 NodePool budget으로 제한되지 않습니다. `drain --delete-emptydir-data`는 local 데이터를 버릴 수 있고 drain만으로 대체 노드가 보장되지도 않습니다. 이전 퀴즈의 무제한 삭제 명령·tag 변경 “패치 trigger”는 긴급 운영 절차가 아닙니다.
## Consolidation·Drift·Expiration
| 방식 | Trigger와 제어 |
|------|----------------|
| Consolidation | Requests/제약에 따라 더 저렴하고 가능한 배치; graceful 제어 적용 |
| Drift | 기존 claim이 관리형 desired state와 불일치; graceful 제어 적용 |
| Expiration | Claim이 저장된 정책 나이에 도달; forceful trigger이며 NodePool budget 대상 아님 |
Graceful disruption controller는 consolidation보다 drift를 먼저 평가하지만 expiration은 별도 forceful 경로입니다. 보편적인 “drift > expiration > consolidation” 우선순위나 먼저 조건을 충족한 작업 승리 계약은 없습니다. 5일 된 노드는 7일 만료 전에 consolidate될 수 있고 8일 된 만료 노드는 저활용 상태가 아니어도 종료를 시작할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-auto-mode-07-node-lifecycle-2.html)
그림의 “사용률 < 임계치”는 실제 consolidation 조건을 단순화한 오래된 표현입니다. Expiration과의 고정된 우선순위도 의미하지 않습니다.
다음은 대안적인 실습 정책이며 비용·보안 결과를 보장하지 않습니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: cost-priority
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
expireAfter: 336h
terminationGracePeriod: 24h
taints:
- key: lifecycle-lab
value: 'true'
effect: NoSchedule
metadata:
labels:
lifecycle-lab: 'true'
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
budgets:
- nodes: 10%
limits:
cpu: '100'
memory: 400Gi
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: security-priority
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
expireAfter: 72h
terminationGracePeriod: 24h
taints:
- key: lifecycle-lab
value: 'true'
effect: NoSchedule
metadata:
labels:
lifecycle-lab: 'true'
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 10m
budgets:
- nodes: 10%
limits:
cpu: '100'
memory: 400Gi
```
`WhenEmpty`는 consolidation을 좁히지만 drift/expiration을 끄지는 않습니다. 선택한 expiry·grace에 애플리케이션을 맞추고 정책 이름으로 가용성을 추론하지 마세요.
## Node 객체 나이와 이미지 증거
`Node.metadata.creationTimestamp`, NodeClaim 생성 시각과 EC2 시작 시각은 서로 다른 관측입니다. `Node.status.nodeInfo.osImage`는 OS 설명이며 AMI ID가 아닙니다. 이전 CREATED/AGE 중복 timestamp 열이나 “보안 패치 상태” age script는 패치 상태를 입증하지 못했습니다.
필요한 Node 필드를 수집한 뒤 이식 가능한 소수 나이·명시적인 bucket을 계산합니다. 잘못된 값, 누락, timezone 부재와 미래 시각은 unknown으로 유지합니다.
```bash
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get nodes \
-l eks.amazonaws.com/compute-type=auto -o json |
jq '{items:[.items[] | {
name:.metadata.name,uid:.metadata.uid,createdAt:.metadata.creationTimestamp,
pool:.metadata.labels["karpenter.sh/nodepool"],
osImage:.status.nodeInfo.osImage,kernelVersion:.status.nodeInfo.kernelVersion
}]}' > "$WORK_DIR/node-lifecycle.json"
```
```bash
python3 - <<'PY'
import json, os
from datetime import datetime, timezone
from pathlib import Path
now = datetime.now(timezone.utc)
rows = []
for item in json.loads((Path(os.environ["WORK_DIR"]) / "node-lifecycle.json").read_text())["items"]:
result = {"node": item["name"], "pool": item.get("pool")}
try:
created = datetime.fromisoformat(item["createdAt"].replace("Z", "+00:00"))
if created.tzinfo is None or created > now:
raise ValueError("unusable timestamp")
hours = (now - created).total_seconds() / 3600
bucket = "<1d" if hours < 24 else "1d–<3d" if hours < 72 else "3d–<7d" if hours < 168 else ">=7d"
result.update(nodeObjectAgeHours=round(hours, 3), bucket=bucket)
except (KeyError, AttributeError, TypeError, ValueError):
result["bucket"] = "UnknownTimestamp"
rows.append(result)
summary = [{"bucket": bucket, "count": sum(row["bucket"] == bucket for row in rows)}
for bucket in ("<1d", "1d–<3d", "3d–<7d", ">=7d", "UnknownTimestamp")]
print(json.dumps({"observedAt": now.isoformat(), "nodes": rows, "distribution": summary}, indent=2))
PY
```
Bucket은 `[0,1)`, `[1,3)`, `[3,7)`, `>=7`일이며 정확히 7일은 마지막 bucket입니다. 성공한 빈 Node 목록과 API 오류를 구분합니다. Snapshot 진단이며 실시간 dashboard나 patch compliance 검사가 아닙니다.
### Prometheus와 Grafana
kube-state-metrics를 설치하고 `karpenter.sh/nodepool`, `eks.amazonaws.com/compute-type`을 allowlist에 포함합니다. 다음 **단일 클러스터** rule file은 scrape를 중복 제거하고 Node 생성 gauge에 Auto Mode label을 join합니다. 여러 클러스터라면 모든 group/join에 실제 cluster label을 포함해야 합니다. Metrics 부재나 미래 timestamp를 정상 0일 노드로 바꾸지 않습니다.
Age alert의 10일·표준편차 3일은 이전 값을 **검토 신호 예시**로 보존한 것입니다. Auto Mode 기본값이 아니며 pool 정책 혼합·정상 autoscaling도 분포를 넓힐 수 있습니다. Prometheus rule file로 로드하거나 operator가 선택하는 PrometheusRule의 `spec` 아래 groups를 넣어 사용합니다.
```yaml
groups:
- name: node-lifecycle
rules:
- record: eks_auto:node_object_age_days
expr: "(\n ((time() - max by (node) (kube_node_created)) >= 0) / 86400\n) * on\
\ (node) group_left (label_karpenter_sh_nodepool)\nmax by (node,label_karpenter_sh_nodepool)\
\ (\n kube_node_labels{label_karpenter_sh_nodepool!=\"\",label_eks_amazonaws_com_compute_type=\"\
auto\"}\n)"
- alert: ReviewNodeObjectAge
expr: eks_auto:node_object_age_days > 10
for: 1h
labels:
severity: warning
annotations:
summary: Review this node against its actual lifecycle policy
- alert: ReviewNodeAgeSpread
expr: stddev(eks_auto:node_object_age_days) > 3
for: 4h
labels:
severity: info
annotations:
summary: Age spread is a review signal, not proof of failed rotation
```
```promql
# median_object_age_days
quantile(0.5, eks_auto:node_object_age_days)
# mean_by_pool
avg by (label_karpenter_sh_nodepool) (eks_auto:node_object_age_days)
# oldest_five
topk(5, eks_auto:node_object_age_days)
# less_than_1d
sum((eks_auto:node_object_age_days >= bool 0) * (eks_auto:node_object_age_days < bool 1))
# 1d_to_under_3d
sum((eks_auto:node_object_age_days >= bool 1) * (eks_auto:node_object_age_days < bool 3))
# 3d_to_under_7d
sum((eks_auto:node_object_age_days >= bool 3) * (eks_auto:node_object_age_days < bool 7))
# 7d_or_more
sum(eks_auto:node_object_age_days >= bool 7)
```
`kube_node_created`는 timestamp gauge이며 histogram/counter가 아닙니다. 기본 `kube_node_created_bucket`은 없고 `rate()`와 `histogram_quantile()`로 node-age histogram을 만들 수 없습니다. 현재 나이 중앙값에는 `quantile()`, 막대그래프에는 명시적 boolean bucket을 사용합니다. 관측 노드는 있으나 해당 bucket이 비었을 때는 합계 0이며 입력 부재는 부재로 남습니다.
| Grafana panel | 증거 |
|---------------|------|
| 현재 나이 분포 | 명시적인 네 bucket query |
| Pool별 평균/오래된 노드 | Label join과 `avg`/`topk` |
| 만료 임박 claim | 실제 NodeClaim 생성 시각과 저장된 `expireAfter`; Node 나이만으로는 부족 |
| 교체율 | 구성한 event/log 이력 또는 확인된 counter publisher; 현재 객체 수로 추측하지 않음 |
| 패치 rollout | 관리형 이미지/릴리스, claim condition과 애플리케이션 health |
## 참고 자료
- [Auto Mode NodePool defaults and termination grace](https://docs.aws.amazon.com/eks/latest/userguide/create-node-pool.html)
- [Auto Mode security and maximum instance lifetime](https://docs.aws.amazon.com/eks/latest/userguide/auto-security.html)
- [Auto Mode managed OS and responsibilities](https://docs.aws.amazon.com/eks/latest/userguide/automode.html)
- [NodeClass supported configuration](https://docs.aws.amazon.com/eks/latest/userguide/create-node-class.html)
- [Karpenter disruption, expiration and drift](https://karpenter.sh/v1.14/concepts/disruption/)
- [Kube-state-metrics Node metrics](https://github.com/kubernetes/kube-state-metrics/blob/main/docs/metrics/cluster/node-metrics.md)
- [Prometheus aggregation operators](https://prometheus.io/docs/prometheus/latest/querying/operators/)
- [AL2023 relationship to Fedora](https://docs.aws.amazon.com/linux/al2023/ug/relationship-to-fedora.html)
< [이전: 비용 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/06-cost-management.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md) | [다음: 워크로드 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/08-workload-optimization.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-auto-mode/08-workload-optimization
----------------------------------------
# 워크로드별 최적화
> **지원 버전**: EKS Auto Mode GA; 예제 검토 기준 EKS 1.36
> **마지막 업데이트**: 2026년 9월 12일
워크로드에 배치·복구 정책을 맞춘 뒤 실제 용량·성능·비용을 검증합니다. Web/batch 예제는 제한된 실습 구성입니다. GPU 예제는 **비활성 구성 template**이며 검증된 학습/추론 애플리케이션이 아닙니다. 추론은 replicas 0, legacy training job은 suspend 상태입니다. GPU/model 실행·실제 클러스터 배포·benchmark는 수행하지 않았습니다.
[운영 및 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/05-operations.md)의 계정/context 검사를 사용하세요. 모든 pool은 동적이며 물리 용량을 예약하지 않습니다. 필요한 예제만 선택하고 `default`/커스텀 NodeClass identity·subnet 접근과 지속되는 노드/스토리지 비용을 검토합니다. Taint/selector는 배치 제어이지 테넌트 보안 경계가 아닙니다.
## 웹 서비스: 가용성과 복구
On-Demand는 Spot 회수 이벤트를 피하지만 용량·가용성을 보장하지는 않습니다. Replica, topology, readiness, disruption policy, 영구 상태와 장애 복구 검증이 필요합니다.
다음은 복제본 3개와 모두 정상일 때 자발적 축출 1개를 허용하는 PDB입니다. `N-1`을 PDB의 실제 값으로 가정하지 않습니다. 이전 복제본 10개는 크기 예시이지 보편적 요구가 아닙니다. Nginx 이미지/digest·non-root 구성은 운영 장에서 확인한 것을 사용하며 8080의 `/`를 probe합니다. 임의 애플리케이션 이미지에 `/health`가 있다고 가정할 수는 없습니다.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: workload-lab
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.36
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.36
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: web-tier
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
- key: kubernetes.io/arch
operator: In
values:
- amd64
- arm64
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
expireAfter: 168h
terminationGracePeriod: 24h
taints:
- key: workload-lab
value: web-tier
effect: NoSchedule
metadata:
labels:
workload-lab: web-tier
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m
budgets:
- nodes: 10%
limits:
cpu: '32'
memory: 128Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-frontend
namespace: workload-lab
spec:
replicas: 3
selector:
matchLabels:
app: web-frontend
template:
metadata:
labels:
app: web-frontend
spec:
containers:
- name: web
image: nginxinc/nginx-unprivileged:1.30.4@sha256:cb92301e719d6639028de775fe8b28e15f58343aca5e5372001311958aafb300
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
readinessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 5
startupProbe:
httpGet:
path: /
port: 8080
periodSeconds: 2
failureThreshold: 30
livenessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 20
failureThreshold: 3
terminationGracePeriodSeconds: 60
automountServiceAccountToken: false
nodeSelector:
karpenter.sh/nodepool: web-tier
tolerations:
- key: workload-lab
operator: Equal
value: web-tier
effect: NoSchedule
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
seccompProfile:
type: RuntimeDefault
volumes:
- name: tmp
emptyDir:
sizeLimit: 128Mi
topologySpreadConstraints:
- maxSkew: 1
minDomains: 2
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
nodeAffinityPolicy: Honor
nodeTaintsPolicy: Honor
labelSelector:
matchLabels:
app: web-frontend
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: web-frontend
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-frontend
namespace: workload-lab
spec:
minAvailable: 2
selector:
matchLabels:
app: web-frontend
```
Hard zone spread는 적합한 domain을 최소 2개 요구하므로 용량이 없으면 Pod가 Pending일 수 있습니다. Soft hostname spread는 선호이며 노드당 복제본 1개나 3-AZ 보장이 아닙니다. Label 일치·subnet 범위·장애 정책을 확인하세요.
Startup probe는 느린 초기화 중 조기 liveness 검사를 막습니다. Readiness는 endpoint 적격성, liveness는 컨테이너 재시작을 제어합니다. 애플리케이션에 맞게 임계값을 정하고 공통 의존 서비스 장애 때문에 모든 replica를 재시작하는 liveness 검사는 피하세요. 예시 probe 간격은 실측 startup SLA가 아닙니다.
## 배치: 재시도와 Checkpoint 복구는 다름
Interruption, deadline 미준수와 Spot 용량 부재를 감당하는 작업에만 Spot을 사용합니다. 이 pool에는 On-Demand fallback이 없습니다. `restartPolicy: OnFailure`는 살아 있는 Pod에서 실패한 컨테이너를 재시작할 수 있지만 종료된 노드의 process memory를 복구하지 못합니다. `SPOT_AWARE=true`도 애플리케이션 구현이 없으면 단순 환경 변수입니다.
다음 Indexed Job은 논리 shard 번호만 출력하는 **smoke 예제**입니다. 이전 parallelism 20/completions 100 대신 병렬 2개·완료 5개·deadline·제한된 재시도를 사용하며 데이터 처리 구현은 아닙니다. 확인한 BusyBox 이미지 index는 Linux amd64/arm64를 포함하지만 이번 감사에서 이미지를 실행하지 않았습니다.
```yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: batch-tier
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- c
- m
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
- key: kubernetes.io/arch
operator: In
values:
- amd64
- arm64
- key: eks.amazonaws.com/instance-generation
operator: Gt
values:
- '4'
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: default
expireAfter: 168h
terminationGracePeriod: 24h
taints:
- key: workload-lab
value: batch-tier
effect: NoSchedule
metadata:
labels:
workload-lab: batch-tier
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
budgets:
- nodes: 10%
limits:
cpu: '16'
memory: 64Gi
---
apiVersion: batch/v1
kind: Job
metadata:
name: indexed-smoke
namespace: workload-lab
spec:
completionMode: Indexed
parallelism: 2
completions: 5
backoffLimit: 2
activeDeadlineSeconds: 300
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app: indexed-smoke
spec:
restartPolicy: Never
automountServiceAccountToken: false
terminationGracePeriodSeconds: 30
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: processor
image: busybox:1.37.0@sha256:9db7b59979c38555a39def84a31fb98b5296952f9e3afd4f6f11f05b07adfab0
command:
- /bin/sh
- -ec
args:
- printf 'logical shard=%s; smoke only\n' "$JOB_COMPLETION_INDEX"
env:
- name: JOB_COMPLETION_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
resources:
requests:
cpu: 100m
memory: 32Mi
limits:
cpu: 500m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
nodeSelector:
karpenter.sh/nodepool: batch-tier
tolerations:
- key: workload-lab
operator: Equal
value: batch-tier
effect: NoSchedule
```
실제 작업에는 멱등 output commit, 영구 checkpoint와 resume 로직이 필요합니다. Indexed Job도 장애 상황에서 같은 index를 여러 번 실행할 수 있으므로 exactly-once 부수 효과를 가정하지 마세요. TTL 정리 전에 필요한 로그·artifact를 export합니다. Node-local cache·emptyDir는 영구 checkpoint가 아닙니다.
`WhenEmpty`는 해당 애플리케이션 작업이 사라진 뒤 노드를 정리할 수 있습니다. 30초 `consolidateAfter`는 debounce이며 실행 중 작업 완료나 30초 후 노드 삭제 보장이 아닙니다. Drift·expiration·interruption은 별도 수명 주기 경로로 남습니다.
## GPU 추론: 노드 전체 사양 확인
Auto Mode는 NVIDIA driver/device 지원과 Bottlerocket 이미지를 관리합니다. 이전 NodeClass의 `amiFamily: AL2023`·`blockDeviceMappings`는 지원하지 않습니다.
| 인스턴스 | GPU | GPU 메모리 | Host vCPU/RAM |
|----------|-----|------------|---------------|
| g5.xlarge | A10G 1개 | 24 GB | 4 / 16 GiB |
| g5.2xlarge | A10G 1개 | 24 GB | 8 / 32 GiB |
| g5.4xlarge | A10G 1개 | 24 GB | 16 / 64 GiB |
| g5.12xlarge | A10G 4개 | 합계 96 GB, 각각 24 GB | 48 / 192 GiB |
| p5.48xlarge | H100 8개 | 합계 640 GB, 각각 80 GB | 192 / 2 TiB |
최신·모든 지역에서 사용 가능한 GPU 목록이 아닌 예시입니다. GPU 메모리 합계는 단일 장치의 연속 메모리가 아닙니다. 특히 이전 Pod의 4 CPU/16Gi request는 노드/system 예약·DaemonSet overhead를 제외하면 g5.xlarge에 들어가지 않습니다. 추론 pool은 더 큰 G5를 허용하며 검토되지 않은 1-GPU 서비스 때문에 8-GPU 학습 노드를 선택하지 않도록 구성합니다.
```yaml
apiVersion: eks.amazonaws.com/v1
kind: NodeClass
metadata:
name: gpu-nodeclass
spec:
instanceProfile: eks-node-instance-profile
subnetSelectorTerms:
- tags:
Name: private-subnet
securityGroupSelectorTerms:
- tags:
Name: worker-restricted
advancedNetworking:
associatePublicIPAddress: false
ephemeralStorage:
size: 200Gi
iops: 6000
throughput: 250
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: gpu-tier
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- g
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
- key: kubernetes.io/arch
operator: In
values:
- amd64
- key: node.kubernetes.io/instance-type
operator: In
values:
- g5.2xlarge
- g5.4xlarge
- key: eks.amazonaws.com/instance-gpu-manufacturer
operator: In
values:
- nvidia
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: gpu-nodeclass
expireAfter: 168h
terminationGracePeriod: 24h
taints:
- key: workload-lab
value: gpu-tier
effect: NoSchedule
- key: nvidia.com/gpu
value: 'true'
effect: NoSchedule
metadata:
labels:
workload-lab: gpu-tier
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 10m
budgets:
- nodes: 10%
limits:
cpu: '64'
memory: 256Gi
nvidia.com/gpu: '4'
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ml-inference
namespace: workload-lab
spec:
replicas: 0
selector:
matchLabels:
app: ml-inference
template:
metadata:
labels:
app: ml-inference
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: inference
image: registry.example.invalid/reviewed-inference:replace-me
resources:
requests:
cpu: '4'
memory: 16Gi
nvidia.com/gpu: 1
limits:
cpu: '4'
memory: 16Gi
nvidia.com/gpu: 1
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
nodeSelector:
karpenter.sh/nodepool: gpu-tier
tolerations:
- key: workload-lab
operator: Equal
value: gpu-tier
effect: NoSchedule
- key: nvidia.com/gpu
operator: Equal
value: 'true'
effect: NoSchedule
```
의도적으로 무효인 예시 registry/image를 검토된 immutable image로 바꾸고 실제 entrypoint·model/input 경로·probe·스토리지·제한된 워크로드 identity를 제공하기 전까지 `replicas: 0`을 유지합니다. Driver/CUDA/library와 non-root UID 1000/security policy 호환성을 그 이미지로 검증하세요. 추론 runtime 실행 검증은 하지 않았습니다.
`ephemeralStorage`는 노드 스토리지 설정이며 영구 model/checkpoint 저장소나 throughput 보장이 아닙니다. 실제 allocatable storage와 backing device를 확인하세요. Node root/data EBS 암호화가 임의 PVC 암호화를 입증하지 않습니다. GPU 개수 limit은 pool 리소스 제어이며 전체 달러 상한이 아니고 급격한 프로비저닝에서 일시 초과할 수도 있습니다. 이전 16/20-GPU는 크기 예시이며 실습은 더 작은 4-GPU 상한을 사용합니다.
`WhenEmpty`·10분 debounce는 작업 종료 후 churn을 줄일 수 있지만 GPU 초기화까지 배치를 지연하거나 warm node를 보장하지는 않습니다. 미리 확보한 desired 용량이 필요하면 별도 limits/consolidation 의미와 지속 비용을 가진 static NodePool도 검토할 수 있습니다.
## 분산 학습과 EFA
Auto Mode의 EFA는 현재 지원 기능입니다. **디스크 설정 옆 주석만으로 활성화되지 않습니다.** 지원 NodeClass 필드는 `advancedNetworking.networkInterfaces`이며 device-plugin 경로는 `vpc.amazonaws.com/efa`를 발행합니다.
학습 활성화 전에 다음을 검토하세요.
- Auto Mode는 현재 EFA **DRA 경로를 지원하지 않습니다**. 다른 compute mode의 DRANET ResourceClaim 예제를 복사하지 말고 EFA device plugin을 사용합니다.
- Auto Mode가 NVIDIA/Neuron host driver를 관리하지만 EFA host 의존성이 있다고 별도 EFA device plugin의 설치·Ready가 입증되지는 않습니다.
- EFA-only interface는 RDMA용이며 Pod IP를 운반하지 않습니다. Primary interface는 card 0/device 0/type `interface`입니다. 예제는 `efa-only` 장치 4개와 Pod IP용 `/28` prefix 1개를 추가합니다.
- Static interface 구성은 IPv4만 지원하며 시작 후 IP/prefix/ENI를 추가하지 않습니다. Multi-interface와 `associatePublicIPAddress`를 조합하지 마세요. 워크로드·system Pod IP 수를 계획합니다.
- 호환되는 기존 placement group, 같은 private AZ/subnet과 필요한 self-reference 통신을 허용한 검토된 EFA security group을 사용합니다. 예시 AZ/group 이름은 placeholder이며 그 위치의 P5 용량 증거가 아닙니다.
- EFA/libfabric/NCCL, process launch/rendezvous, GPU/EFA locality와 필요한 hugepage/memory-lock 설정을 확인합니다. 장치 4개 예제는 P5 최대 네트워크 대역폭이나 Auto Mode Bottlerocket의 자동 GPU/EFA 정렬 보장이 아닙니다.
다음은 P4d/P5 worker를 서로 바꿔 쓸 수 있다고 가정하지 않는 동일 P5 pool입니다. P4d는 별도 호환 pool/runtime 검증이 필요한 대안으로 남습니다. 이전 500Gi/16,000 IOPS/1,000 throughput과 추가 2,000Gi data disk는 예시이며, 지원 NodeClass가 임의 추가 block device를 만드는 것은 아닙니다.
```yaml
apiVersion: eks.amazonaws.com/v1
kind: NodeClass
metadata:
name: ml-training-nodeclass
spec:
instanceProfile: eks-node-instance-profile
subnetSelectorTerms:
- tags:
Name: private-subnet
securityGroupSelectorTerms:
- tags:
Name: reviewed-efa-security-group
advancedNetworking:
networkInterfaces:
- networkCardIndex: 0
deviceIndex: 0
interfaceType: interface
secondaryIPv4PrefixCount: 1
- networkCardIndex: 0
deviceIndex: 1
interfaceType: efa-only
- networkCardIndex: 1
deviceIndex: 0
interfaceType: efa-only
- networkCardIndex: 2
deviceIndex: 0
interfaceType: efa-only
- networkCardIndex: 3
deviceIndex: 0
interfaceType: efa-only
ephemeralStorage:
size: 500Gi
iops: 16000
throughput: 1000
placementGroupSelector:
name: reviewed-ml-training-pg
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: ml-training
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- p
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
- key: kubernetes.io/arch
operator: In
values:
- amd64
- key: node.kubernetes.io/instance-type
operator: In
values:
- p5.48xlarge
- key: topology.kubernetes.io/zone
operator: In
values:
- ap-northeast-2a
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: ml-training-nodeclass
expireAfter: 168h
terminationGracePeriod: 24h
taints:
- key: workload-lab
value: ml-training
effect: NoSchedule
- key: nvidia.com/gpu
value: 'true'
effect: NoSchedule
metadata:
labels:
workload-lab: ml-training
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30m
budgets:
- nodes: 10%
limits:
cpu: '960'
memory: 10Ti
nvidia.com/gpu: '40'
```
### 유지한 Legacy PyTorchJob 구조
`kubeflow.org/v1 PyTorchJob`에는 **Kubeflow Training Operator V1**이 필요하며 구조 검증에는 릴리스된 v1.9.3 CRD를 사용했습니다. 현재 Kubeflow Trainer의 TrainJob/Runtime API는 다릅니다. Auto Mode에서 NodePool을 만든다고 어느 operator도 자동 설치되지 않습니다.
Template은 suspend 상태이며 의도적으로 무효인 placeholder 이미지를 사용합니다. Master 1개·worker 3개, 노드별 GPU process 8개·Pod별 EFA 장치 4개로 활성화 시 **GPU 32개/8-GPU 노드 4대**를 표현합니다. Pool의 40-GPU 상한은 교체 여유를 포함한 해당 사양 노드 최대 5대의 계획 제한이며 예약이나 강제 청구 상한은 아닙니다.
```yaml
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
name: distributed-training
namespace: workload-lab
spec:
nprocPerNode: '8'
runPolicy:
suspend: true
activeDeadlineSeconds: 3600
backoffLimit: 1
cleanPodPolicy: None
pytorchReplicaSpecs:
Master:
replicas: 1
restartPolicy: Never
template:
metadata:
labels:
app: distributed-training
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: pytorch
image: registry.example.invalid/reviewed-training:replace-me
resources:
requests:
cpu: '32'
memory: 128Gi
nvidia.com/gpu: 8
vpc.amazonaws.com/efa: 4
limits:
cpu: '32'
memory: 128Gi
nvidia.com/gpu: 8
vpc.amazonaws.com/efa: 4
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
nodeSelector:
karpenter.sh/nodepool: ml-training
tolerations:
- key: workload-lab
operator: Equal
value: ml-training
effect: NoSchedule
- key: nvidia.com/gpu
operator: Equal
value: 'true'
effect: NoSchedule
Worker:
replicas: 3
restartPolicy: Never
template:
metadata:
labels:
app: distributed-training
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: pytorch
image: registry.example.invalid/reviewed-training:replace-me
resources:
requests:
cpu: '32'
memory: 128Gi
nvidia.com/gpu: 8
vpc.amazonaws.com/efa: 4
limits:
cpu: '32'
memory: 128Gi
nvidia.com/gpu: 8
vpc.amazonaws.com/efa: 4
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
nodeSelector:
karpenter.sh/nodepool: ml-training
tolerations:
- key: workload-lab
operator: Equal
value: ml-training
effect: NoSchedule
- key: nvidia.com/gpu
operator: Equal
value: 'true'
effect: NoSchedule
```
Unsuspend 전에 operator의 분산 launch 계약을 구현한 검토된 image/entrypoint, 실제 dataset 경로와 영구 checkpoint/artifact export를 준비하세요. Operator, queue/gang scheduling, 4-node 용량, DNS/rendezvous, network rule, runtime 권한과 deadline을 검증합니다. 완전한 프로덕션 학습 절차가 아닙니다. 가속기 runtime이 충족할 수 없는 권한을 요구하면 Restricted namespace template과 별도로 검토된 workload policy가 필요할 수 있으며, 단순히 클러스터 전체 admission 보안을 끄면 안 됩니다.
이미 실행 중인 legacy PyTorchJob을 suspend하면 active Pod/PodGroup이 삭제됩니다. Checkpoint 작업이 아닙니다. `cleanPodPolicy: None`은 완료 Pod를 검사용으로 남기지만 artifact를 export하지 않습니다. 영구 export 후 PVC·노드·별도 소유 reservation까지 정리를 검토하세요.
### 실제 장치 용량 관측
다음 읽기 전용 snapshot은 실제 allocatable과 Ready condition을 보여줍니다. GPU/EFA 값 부재나 API 오류를 장치 설정 성공으로 해석하지 마세요.
```bash
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get nodes \
-l eks.amazonaws.com/compute-type=auto -o json |
jq '[.items[]|{name:.metadata.name,
instanceType:.metadata.labels["node.kubernetes.io/instance-type"],
pool:.metadata.labels["karpenter.sh/nodepool"],
allocatable:{cpu:.status.allocatable.cpu,memory:.status.allocatable.memory,
gpu:.status.allocatable["nvidia.com/gpu"],efa:.status.allocatable["vpc.amazonaws.com/efa"]},
conditions:[.status.conditions[]?|select(.type=="Ready")|{type,status,reason}]}]'
```
## 리소스와 아키텍처 결정
| 워크로드 | 정책 후보 | 여전히 필요한 검증 |
|----------|-----------|--------------------|
| Web/API | On-Demand 또는 검토한 혼합 용량·적당한 consolidation | 가용성 예산·replica·runtime 아키텍처·실측 latency |
| Batch/CI | 재시작·deadline을 허용할 때 Spot | 멱등성·영구 진행 상태·재시도·용량 부재 |
| Database/streaming | 상태를 고려한 배치·disruption | Quorum·volume topology·복구·partition 동작 |
| GPU 추론 | Model에 맞는 GPU/CPU/RAM, 필요하면 warm capacity | Image/driver·probe·cold start·비용 |
| 분산 학습 | 동일 가속기 pool과 검증한 network/runtime | Gang capacity·checkpoint/export·EFA 장치·placement |
이전 expiry 24h/72h/168h/336h, consolidation 30s/1m/5m/10m/15m/30m은 정책 예시이며 워크로드 유형별 기본값이 아닙니다. Auto Mode 최대 수명과 별도 termination grace도 적용됩니다.
CPU 중심 워크로드의 이전 2 CPU/2Gi request·4 CPU/4Gi limit은 가능한 burst 정책 예시이지 보편적 크기가 아닙니다. Memory 중심 워크로드도 8Gi request/limit만 같고 CPU가 다르면 Guaranteed QoS가 되지 않습니다. Guaranteed QoS는 모든 해당 컨테이너·리소스 조건도 충족해야 하며 memory limit에서도 OOM은 가능합니다. 이전 사용량 1.2–1.5배·request 2배 공식에는 확인된 일반 성능 근거가 없습니다.
Device-plugin API의 GPU는 정수 extended resource입니다. Limit을 지정하고 request도 지정한다면 일치시킵니다. Request만 쓰는 방식은 GPU 패턴이 아닙니다. CPU/memory request 외에 필수 system 작업의 node allocatable 여유도 필요합니다. Limit만 줄여도 일반적인 request 기반 bin-packing이 개선되는 것은 아닙니다.
### 다중 아키텍처 검증
`nginx:latest` grep 대신 고정된 image index를 확인합니다. 두 platform이 있는 index는 필요한 packaging 확인이지만 native 의존성·애플리케이션 동작까지 입증하지는 않습니다.
```bash
: "${IMAGE_REF:?Set a reviewed image reference pinned by digest}"
if ! [[ "$IMAGE_REF" =~ @sha256:[0-9a-f]{64}$ ]]; then
printf 'Use an immutable sha256 digest reference.\n' >&2
exit 1
fi
docker buildx imagetools inspect --raw "$IMAGE_REF" > "$WORK_DIR/image-index.json"
jq -e '[.manifests[]?.platform |
select(.os=="linux" and (.architecture=="amd64" or .architecture=="arm64")) |
.architecture] | unique | sort == ["amd64","arm64"]' \
"$WORK_DIR/image-index.json"
```
검토한 자체 Dockerfile에 대해 다음은 불명확한 image를 게시하지 않고 로컬 OCI archive를 생성합니다.
```bash
: "${BUILD_CONTEXT:?Set the reviewed Dockerfile directory}"
test -f "$BUILD_CONTEXT/Dockerfile"
docker buildx build --platform linux/amd64,linux/arm64 \
--output "type=oci,dest=$WORK_DIR/app-multiarch.tar" "$BUILD_CONTEXT"
```
적합한 Buildx builder, native worker 또는 올바른 emulation/cross-compilation이 필요합니다. 정상 registry 절차로 게시하기 전에 아키텍처별 애플리케이션 테스트를 실행하세요. 이전 Spot/Graviton 혼합 ~40%는 미검증 추정이며 [비용 관리](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/06-cost-management.md)처럼 실제 유효 작업 비용을 비교합니다.
## 참고 자료
- [Auto Mode AI/ML compute](https://docs.aws.amazon.com/eks/latest/userguide/ml-node-pools.html)
- [NodeClass storage and static network interfaces](https://docs.aws.amazon.com/eks/latest/userguide/create-node-class.html)
- [EFA device management, including Auto Mode](https://docs.aws.amazon.com/eks/latest/userguide/device-management-efa.html)
- [G5 instance specifications](https://aws.amazon.com/ec2/instance-types/g5/)
- [P5 instance specifications](https://aws.amazon.com/ec2/instance-types/p5/)
- [Kubernetes Jobs and Indexed Jobs](https://kubernetes.io/docs/concepts/workloads/controllers/job/)
- [Kubernetes GPU scheduling](https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/)
- [Resource requests, limits and quantities](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
- [Topology spread constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/)
- [Startup, readiness and liveness probes](https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/)
- [Training Operator v1.9.3 PyTorchJob CRD](https://github.com/kubeflow/training-operator/blob/v1.9.3/manifests/base/crds/kubeflow.org_pytorchjobs.yaml)
- [Kubeflow Trainer and legacy-v1 migration](https://github.com/kubeflow/trainer)
- [Docker multi-platform builds](https://docs.docker.com/build/building/multi-platform/)
< [이전: 노드 생명주기](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/07-node-lifecycle.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md) | [다음: 마이그레이션 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/09-migration-guide.md) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/eks-auto-mode/09-migration-guide
----------------------------------------
# 관리형 노드 그룹에서 Auto Mode로 마이그레이션
> **지원 버전**: EKS Auto Mode GA; 예제 검토 기준 EKS 1.36
> **마지막 업데이트**: 2026년 9월 12일
기존 용량 제거 **전에** 애플리케이션·스토리지·controller 소유권을 확인해야 합니다. 아래는 로컬 스키마/CLI fixture를 확인한 단계별 예제이며 무중단 프로덕션 검증 절차가 아닙니다. 이번 감사에서 클라우드·클러스터 변경은 수행하지 않았습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-eks-auto-mode-09-migration-guide-0.html)
그림의 마지막 검증 단계가 앞선 health check·rollback 계획 없이 drain, 0으로 축소, 삭제해도 된다는 의미는 아닙니다.
## 1. 인벤토리와 운영 Context 고정
현재 지원되는 EKS/add-on 조합을 사용하세요. 이전 1.29 최소값은 과거 기능 기준이며 현재 지원 버전 안내가 아닙니다. 설치된 VPC CNI·kube-proxy·EBS CSI·snapshot controller·Pod Identity Agent에 공식 migration 최소값과 **실제 Kubernetes 버전의** 호환 목록을 함께 적용합니다.
Controller 소유권, workload placement, system agent, volume/AZ, load-balancer class, IAM, 복구 절차와 비용을 기록합니다. 아래는 계정, cluster ARN/생성 시각/API endpoint와 node-group ARN/생성 시각을 확인합니다. 특정 시점의 identity 검사이며 동시 controller 변경을 막는 원자적 lock은 아닙니다. 의도적으로 proxy한 endpoint에는 검토된 대안이 필요하며 불일치를 조용히 우회하면 안 됩니다.
```bash
set -euo pipefail
: "${EXPECTED_ACCOUNT_ID:?Set the intended account}"
: "${AWS_REGION:?Set the cluster region}"
: "${CLUSTER_NAME:?Set the cluster name}"
: "${OLD_NODEGROUP:?Set the reviewed managed node group}"
: "${KUBECONFIG:?Set the reviewed kubeconfig}"
export KUBECONFIG
export KUBE_CONTEXT="${KUBE_CONTEXT:-$CLUSTER_NAME}"
check_account() {
local actual
actual=$(aws sts get-caller-identity --region "$AWS_REGION" --query Account --output text) || return
test "$actual" = "$EXPECTED_ACCOUNT_ID" || { printf 'Account mismatch.\n' >&2; return 1; }
}
check_account
umask 077
export WORK_DIR
WORK_DIR=$(mktemp -d "$PWD/auto-migration.XXXXXXXX")
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" --output json \
> "$WORK_DIR/cluster-before.json"
aws eks describe-nodegroup --cluster-name "$CLUSTER_NAME" --nodegroup-name "$OLD_NODEGROUP" \
--region "$AWS_REGION" --output json > "$WORK_DIR/nodegroup-before.json"
guard_context() {
check_account || return
local endpoint
aws eks describe-cluster --name "$CLUSTER_NAME" --region "$AWS_REGION" --output json \
> "$WORK_DIR/cluster-current.json" || return
endpoint=$(kubectl --context "$KUBE_CONTEXT" config view --minify \
-o jsonpath='{.clusters[0].cluster.server}') || return
jq -e --arg endpoint "$endpoint" --slurpfile before "$WORK_DIR/cluster-before.json" '
.cluster.arn == $before[0].cluster.arn and
.cluster.createdAt == $before[0].cluster.createdAt and
.cluster.endpoint == $endpoint and .cluster.status == "ACTIVE"
' "$WORK_DIR/cluster-current.json" >/dev/null
}
guard_nodegroup() {
guard_context || return
aws eks describe-nodegroup --cluster-name "$CLUSTER_NAME" --nodegroup-name "$OLD_NODEGROUP" \
--region "$AWS_REGION" --output json > "$WORK_DIR/nodegroup-current.json" || return
jq -e --slurpfile before "$WORK_DIR/nodegroup-before.json" '
.nodegroup.nodegroupArn == $before[0].nodegroup.nodegroupArn and
.nodegroup.createdAt == $before[0].nodegroup.createdAt and .nodegroup.status == "ACTIVE"
' "$WORK_DIR/nodegroup-current.json" >/dev/null
}
guard_nodegroup
printf 'Private migration evidence: %s\n' "$WORK_DIR"
```
```bash
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s \
get deployments,statefulsets,daemonsets,jobs,cronjobs -A -o json |
jq '[.items[] | (.spec.template // .spec.jobTemplate.spec.template) as $t |
{kind,namespace:.metadata.namespace,name:.metadata.name,uid:.metadata.uid,
nodeSelector:$t.spec.nodeSelector,affinity:$t.spec.affinity,tolerations:$t.spec.tolerations,
serviceAccountName:$t.spec.serviceAccountName,hostNetwork:$t.spec.hostNetwork,
pvcNames:[$t.spec.volumes[]?.persistentVolumeClaim.claimName // empty]}]' \
> "$WORK_DIR/workload-placement.json"
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get pods -A -o json |
jq '[.items[] | {namespace:.metadata.namespace,name:.metadata.name,node:.spec.nodeName,
phase:.status.phase,deletionTimestamp:.metadata.deletionTimestamp,
ready:([.status.conditions[]?|select(.type=="Ready")|.status]|first // "NotReported"),
owners:[.metadata.ownerReferences[]?|{kind,name,controller}]}]' \
> "$WORK_DIR/pod-state.json"
```
Private 파일은 운영 metadata를 포함하므로 복구용으로 보존합니다. 조회 실패는 실패로 남습니다. `kubectl top`에는 정상 metrics API가 필요하고 순간 CPU snapshot은 workload 이력을 대체하지 않습니다.
| 기존 구성 | 이전 시 고려 사항 |
|-----------|-------------------|
| 커스텀 AMI/bootstrap/user data | Auto Mode는 관리형 Bottlerocket이며 임의 AL2023/custom AMI/userData가 아님 |
| Node IAM 권한 | Node role/profile/access entry 준비; workload는 node-role/IMDS fallback 대신 검토한 IRSA/Pod Identity로 이전 |
| Cluster IAM role | 기존 role에 필요한 Auto Mode 권한/trust 추가; CLI 활성화가 모든 role을 자동 생성하지 않음 |
| Selector/affinity/taint | Deployment·StatefulSet·DaemonSet·Job·CronJob 포함; 충돌 placement를 의도적으로 제거 |
| 대체 CNI/network | 활성화 전 공식 호환성 확인; NodeClass가 미지원 networking을 고치지 않음 |
| Scaling/IaC 소유자 | Cluster Autoscaler·GitOps·원래 IaC owner가 이전 변경을 되돌리지 않도록 조율 |
### 데이터·Load Balancer·혼합 노드 DNS
Driver/controller 인터페이스는 다릅니다.
| 리소스 | 자체 관리 | Auto Mode |
|--------|-----------|-----------|
| EBS StorageClass provisioner | `ebs.csi.aws.com` | `ebs.csi.eks.amazonaws.com` |
| NLB Service loadBalancerClass | `service.k8s.aws/nlb` | `eks.amazonaws.com/nlb` |
| ALB IngressClass controller | `ingress.k8s.aws/alb` | `eks.amazonaws.com/alb` |
| TargetGroupBinding apiVersion | `elbv2.k8s.aws/v1beta1` | `eks.amazonaws.com/v1` |
| Compute class | `karpenter.k8s.aws/v1 EC2NodeClass` | `eks.amazonaws.com/v1 NodeClass` |
StorageClass 이름을 바꿔도 기존 PVC의 driver가 바뀌지 않으며 기존 load balancer가 관리형 controller에 자동 인수되지도 않습니다. EBS에는 검증한 backup/snapshot 복원 계획이나 현재 AWS가 문서화한 **workload 중지·Retain·static PV/PVC 재생성** 절차를 사용합니다. 후자는 EBS volume을 재사용하지만 Kubernetes binding 객체를 재생성하며 in-place driver 전환은 아닙니다. Backup 복구, volume/AZ/KMS 소유권, reclaim policy, finalizer, binding과 앱 일관성을 검증하세요. 이 문서는 파괴적인 volume 이전 스크립트를 제공하지 않습니다.
자체 AWS Load Balancer Controller가 리소스를 소유하는 동안 유지합니다. 새 관리형 load balancer를 생성·검증하고 blue/green DNS 계획으로 트래픽을 전환한 뒤 기존 것을 정리합니다. Class 불변성, annotation과 TargetGroupBinding 소유권은 별도로 검토하며 API group 변경만으로 안전한 인수가 되지는 않습니다.
Auto Mode 노드는 node-local DNS를 사용합니다. **비 Auto 노드가 필요로 하는 동안 CoreDNS Deployment를 유지하세요.** 남은 노드 유형의 CNI/proxy/storage/identity agent와 placement도 유지합니다. Pure Auto Mode 관리형 기능을 이유로 혼합 클러스터 의존성을 먼저 제거하면 안 됩니다.
## 2. 무제한 기본 Pool 없이 Auto Mode 활성화
먼저 [시작하기](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/01-getting-started.md)의 IAM/access 준비를 완료하세요. 기존 cluster role에는 공식 Compute, **BlockStoragePolicyV2**, LoadBalancing, Networking, Cluster 정책과 필요한 `sts:TagSession` trust가 필요합니다. 예제는 API 또는 API_AND_CONFIG_MAP 인증과 준비된 custom node role/profile·EC2 access entry를 가정합니다.
이 단계적 절차는 **built-in pool 없이** 시작하므로 다음 단계에서 자체 NodeClass를 만들며 `default`가 있다고 가정하지 않습니다. 무제한 general-purpose pool은 의도한 전환 전에 기존 Pending workload를 배치할 수 있습니다. 예제는 이미 Auto Mode가 활성화된 cluster의 pool 목록을 덮어쓰지 않고 거부합니다.
특정 update ID의 완료를 기다립니다. Cluster `ACTIVE`만으로 이번 설정 갱신 성공이 입증되지는 않습니다.
```bash
wait_eks_update() {
local id="$1" group="${2:-}" attempt status
local extra=()
test -n "$group" && extra=(--nodegroup-name "$group")
for ((attempt=0; attempt<120; attempt++)); do
aws eks describe-update --name "$CLUSTER_NAME" --region "$AWS_REGION" \
--update-id "$id" "${extra[@]}" --output json > "$WORK_DIR/update-current.json" || return
status=$(jq -er '.update.status' "$WORK_DIR/update-current.json") || return
case "$status" in
Successful) return 0 ;;
Failed|Cancelled)
jq '{id:.update.id,status:.update.status,errorCodes:[.update.errors[]?.errorCode]}' \
"$WORK_DIR/update-current.json" >&2
return 1 ;;
InProgress) sleep 10 ;;
*) printf 'Unknown update state; inspect saved evidence.\n' >&2; return 1 ;;
esac
done
printf 'Update still unconfirmed; stop and retain its ID.\n' >&2
return 1
}
```
```bash
guard_context
jq -e '.cluster.computeConfig.enabled != true and
(.cluster.accessConfig.authenticationMode == "API" or
.cluster.accessConfig.authenticationMode == "API_AND_CONFIG_MAP")' \
"$WORK_DIR/cluster-current.json" >/dev/null
jq -n --arg name "$CLUSTER_NAME" '{
name:$name,
computeConfig:{enabled:true,nodePools:[]},
storageConfig:{blockStorage:{enabled:true}},
kubernetesNetworkConfig:{elasticLoadBalancing:{enabled:true}}
}' > "$WORK_DIR/enable-request.json"
aws eks update-cluster-config --region "$AWS_REGION" \
--cli-input-json "file://$WORK_DIR/enable-request.json" --output json \
> "$WORK_DIR/enable-response.json"
update_id=$(jq -er '.update.id' "$WORK_DIR/enable-response.json")
wait_eks_update "$update_id"
guard_context
jq -e '.cluster.computeConfig.enabled == true and
.cluster.computeConfig.nodePools == [] and
.cluster.storageConfig.blockStorage.enabled == true and
.cluster.kubernetesNetworkConfig.elasticLoadBalancing.enabled == true' \
"$WORK_DIR/cluster-current.json" >/dev/null
```
Compute·block storage·managed load balancing을 같은 요청에서 함께 활성화/비활성화해야 합니다. Native request 검증이 IAM/service admission 성공을 입증하지는 않습니다. 오류 시 중단하고 update 증거를 보존하며 응답 불명 상태에서 맹목적으로 재실행하지 마세요. Authentication mode에는 별도 이전/rollback 제약이 있습니다.
## 3. 선택적인 Pool과 양쪽 Canary
NodeClass profile/subnet/security-group placeholder를 검토한 기존 리소스로 바꾸세요. Profile role에는 올바른 node access entry가 필요합니다. Manifest는 IAM·VPC·애플리케이션 자격 증명을 만들지 않습니다.
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: migration-lab
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.36
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.36
---
apiVersion: eks.amazonaws.com/v1
kind: NodeClass
metadata:
name: migration-nodeclass
spec:
instanceProfile: eks-migration-node-profile
subnetSelectorTerms:
- tags:
Name: private-subnet
securityGroupSelectorTerms:
- tags:
Name: worker-restricted
advancedNetworking:
associatePublicIPAddress: false
ephemeralStorage:
size: 100Gi
iops: 3000
throughput: 125
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: migration-pool
spec:
template:
spec:
requirements:
- key: eks.amazonaws.com/instance-category
operator: In
values:
- m
- key: karpenter.sh/capacity-type
operator: In
values:
- on-demand
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: migration-nodeclass
expireAfter: 168h
terminationGracePeriod: 24h
taints:
- key: migration
value: auto-mode
effect: NoSchedule
metadata:
labels:
migration: auto-mode
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 2m
budgets:
- nodes: '1'
limits:
cpu: '32'
memory: 128Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: legacy-canary
namespace: migration-lab
spec:
replicas: 2
selector:
matchLabels:
app: legacy-canary
template:
metadata:
labels:
app: legacy-canary
spec:
containers:
- name: web
image: nginxinc/nginx-unprivileged:1.30.4@sha256:cb92301e719d6639028de775fe8b28e15f58343aca5e5372001311958aafb300
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
readinessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 5
terminationGracePeriodSeconds: 60
automountServiceAccountToken: false
nodeSelector:
eks.amazonaws.com/nodegroup: REPLACE_WITH_OLD_NODEGROUP
tolerations: []
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
seccompProfile:
type: RuntimeDefault
volumes:
- name: tmp
emptyDir:
sizeLimit: 128Mi
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: auto-canary
namespace: migration-lab
spec:
replicas: 2
selector:
matchLabels:
app: auto-canary
template:
metadata:
labels:
app: auto-canary
spec:
containers:
- name: web
image: nginxinc/nginx-unprivileged:1.30.4@sha256:cb92301e719d6639028de775fe8b28e15f58343aca5e5372001311958aafb300
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
readinessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 5
terminationGracePeriodSeconds: 60
automountServiceAccountToken: false
nodeSelector:
karpenter.sh/nodepool: migration-pool
eks.amazonaws.com/compute-type: auto
tolerations:
- key: migration
operator: Equal
value: auto-mode
effect: NoSchedule
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
seccompProfile:
type: RuntimeDefault
volumes:
- name: tmp
emptyDir:
sizeLimit: 128Mi
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
```
Legacy canary의 `REPLACE_WITH_OLD_NODEGROUP`를 바꿔 사용합니다. 완전한 nginx 예제는 운영 장에서 검토한 non-root image/security context를 사용하며 기본 placement/readiness만 시험합니다. 실제 앱의 상태·identity·traffic 경로 검증은 아닙니다.
Auto canary는 정확한 pool과 `eks.amazonaws.com/compute-type=auto`를 함께 선택합니다. `karpenter.sh/nodepool` 존재만 확인하면 자체 Karpenter 노드도 선택됩니다. Toleration은 배치를 허용하지만 그 자체로 pool을 선택하지는 않습니다.
| MNG 설정 | Auto Mode 대응 |
|----------|----------------|
| Instance type/capacity type | 정확한 instance requirement 또는 의도적으로 확장한 category; capacity-type requirement |
| Label/taint | `spec.template.metadata.labels` / `spec.template.spec.taints` |
| Min/desired/max 노드 수 | CPU/memory `spec.limits`와 동등하지 않음; 고정 desired node 수에는 별도 static-pool 의미 검토 |
| Subnet/security group | Custom NodeClass selector와 검토한 rule |
| AMI/bootstrap | 임의 AMI-family/userData 대응 필드 없음 |
## 4. Wave별 이전과 실패 시 중단
대표성이 있는 저위험 workload부터 staging/비중요 production, 의존성 검증 후 중요 workload로 진행합니다. Live Pod만 바꾸지 말고 **소유 controller/GitOps desired configuration**을 바꿉니다.
전체 placement를 검토하세요. 기존 node-group selector를 유지하면서 Auto selector를 추가하면 두 조건의 교집합이 없어 배치되지 않을 수 있습니다. `affinity: null`로 관련 없는 Pod affinity·보안 placement·toleration을 지우지 마세요. StatefulSet·active Job·local-volume workload에는 데이터에 맞는 절차가 필요합니다.
복제본이 0이 아닌 Deployment의 rollout·관측 replica readiness 검사입니다.
```bash
: "${WORKLOAD_NAMESPACE:?Select the namespace}"
: "${DEPLOYMENT_NAME:?Select one migrated Deployment}"
kubectl --context "$KUBE_CONTEXT" -n "$WORKLOAD_NAMESPACE" rollout status \
"deployment/$DEPLOYMENT_NAME" --timeout=10m
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s -n "$WORKLOAD_NAMESPACE" \
get deployment "$DEPLOYMENT_NAME" -o json |
jq -e '(.spec.replicas // 1) as $desired |
$desired > 0 and .status.observedGeneration >= .metadata.generation and
.status.updatedReplicas == $desired and .status.readyReplicas == $desired and
.status.availableReplicas == $desired' >/dev/null
```
실제 요청, DNS, load-balancer target, volume read/write, identity, logs/metrics와 앱 SLO도 확인합니다. Deployment rolling-update 설정과 PDB Eviction API 보호는 다른 제어입니다. `rollout status` 성공이나 Pod phase `Running`만으로 종단 간 가용성이 입증되지 않으며 완료 Job은 `Succeeded`가 정상일 수 있습니다.
### 선택적인 단일 노드 Drain
대체 용량, placement, PDB와 데이터 내구성을 검토한 뒤 기존 managed node **하나**와 기록한 UID를 선택합니다.
```bash
: "${NODE_NAME:?Select exactly one old managed node}"
: "${EXPECTED_NODE_UID:?Set its previously reviewed UID}"
guard_nodegroup
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get node "$NODE_NAME" -o json \
> "$WORK_DIR/node-before-drain.json"
jq -e --arg uid "$EXPECTED_NODE_UID" --arg group "$OLD_NODEGROUP" '
.metadata.uid == $uid and .metadata.labels["eks.amazonaws.com/nodegroup"] == $group and
.metadata.labels["eks.amazonaws.com/compute-type"] != "auto" and
.metadata.deletionTimestamp == null and
any(.status.conditions[]?; .type=="Ready" and .status=="True")
' "$WORK_DIR/node-before-drain.json" >/dev/null
kubectl --context "$KUBE_CONTEXT" drain "$NODE_NAME" --ignore-daemonsets --timeout=10m
printf 'Drain returned successfully. Validate application health before selecting another node.\n'
```
일괄 cordon/drain loop, 고정 sleep health gate나 “실패 후 계속 진행”은 없습니다. 기본 drain은 unmanaged Pod/local emptyDir data를 의도적으로 처리하기 전에는 거부합니다. 오류를 없애려고 `--force`, `--disable-eviction`, `--delete-emptydir-data`를 붙이지 마세요. 실패·중단된 drain은 노드를 cordon 상태로 남길 수 있습니다. 상태를 확인하고 복구 계획상 필요할 때만 배치를 복원합니다. 다음 노드 전에 영향 앱을 검증하세요.
## 5. 기존 Workload 제거 확인 후 축소
**MNG desired size 변경은 PDB를 따르지 않습니다.** EKS는 ASG scale-down을 사용하며 일반 node-group version update의 drain과 다릅니다. Desired size를 절반으로 줄이고 5분 기다리는 것은 안전한 안정화가 아닙니다.
원래 scaling/IaC owner와 조율하고 이전 노드에 신규 앱 배치를 막습니다. 아래 API 예제는 모든 관측된 이전 노드가 cordon됐고 active non-DaemonSet Pod가 없는지 확인한 뒤 min/desired 0을 요청합니다. Cordon을 무시하거나 노드를 교체하는 controller와의 race까지 없애지는 못하므로 migration placement를 유지합니다. 먼저 남은 모든 DaemonSet/system 의존성과 완료 Job artifact export를 확인하세요.
```bash
guard_nodegroup
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get nodes \
-l "eks.amazonaws.com/nodegroup=$OLD_NODEGROUP" -o json > "$WORK_DIR/old-nodes.json"
jq -e 'all(.items[]; .spec.unschedulable == true)' "$WORK_DIR/old-nodes.json" >/dev/null
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get pods -A -o json \
| jq '{items:[.items[]|{metadata:{name:.metadata.name,namespace:.metadata.namespace,
ownerReferences:.metadata.ownerReferences},spec:{nodeName:.spec.nodeName},
status:{phase:.status.phase}}]}' > "$WORK_DIR/pods-before-scale.json"
jq --slurpfile nodes "$WORK_DIR/old-nodes.json" '
($nodes[0].items|map(.metadata.name)) as $names |
[.items[] | select(.spec.nodeName as $n | $names|index($n)) |
select(.status.phase!="Succeeded" and .status.phase!="Failed") |
select(any(.metadata.ownerReferences[]?; .kind=="DaemonSet" and .controller==true)|not) |
{namespace:.metadata.namespace,name:.metadata.name,phase:.status.phase}]
' "$WORK_DIR/pods-before-scale.json" > "$WORK_DIR/old-active-workloads.json"
jq -e 'length == 0' "$WORK_DIR/old-active-workloads.json" >/dev/null
guard_nodegroup
aws eks update-nodegroup-config --cluster-name "$CLUSTER_NAME" --nodegroup-name "$OLD_NODEGROUP" \
--region "$AWS_REGION" --scaling-config minSize=0,desiredSize=0 --output json \
> "$WORK_DIR/scale-zero-response.json"
update_id=$(jq -er '.update.id' "$WORK_DIR/scale-zero-response.json")
wait_eks_update "$update_id" "$OLD_NODEGROUP"
```
실제 이전 노드·인스턴스가 사라지는지와 앱 health를 확인합니다. 합의한 rollback 기간 동안 node-group 정의와 원 scaling 설정을 보존하세요. Desired 0이 즉시 복구 가능한 용량을 보장하지는 않습니다.
## 6. 원래 소유자를 통한 정리
Workload/data/traffic 검증과 정한 안정화 기간 후 **원래 owner**인 Terraform·CloudFormation·eksctl 등으로 group을 삭제합니다. IaC 소유 group을 EKS API로 직접 지우면 drift, stack, IAM role 등이 남을 수 있습니다.
다음은 검토한 **direct-API-managed** group 전용입니다. Desired 0, node/workload 상태를 다시 확인하고 보이는 CloudFormation ownership tag를 거부합니다. 모든 외부 IaC owner를 발견하는 검사는 아니므로 소유권 확인은 사전 조건입니다.
```bash
guard_nodegroup
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get nodes \
-l "eks.amazonaws.com/nodegroup=$OLD_NODEGROUP" -o json > "$WORK_DIR/old-nodes.json"
jq -e 'all(.items[]; .spec.unschedulable == true)' "$WORK_DIR/old-nodes.json" >/dev/null
kubectl --context "$KUBE_CONTEXT" --request-timeout=15s get pods -A -o json \
| jq '{items:[.items[]|{metadata:{name:.metadata.name,namespace:.metadata.namespace,
ownerReferences:.metadata.ownerReferences},spec:{nodeName:.spec.nodeName},
status:{phase:.status.phase}}]}' > "$WORK_DIR/pods-before-scale.json"
jq --slurpfile nodes "$WORK_DIR/old-nodes.json" '
($nodes[0].items|map(.metadata.name)) as $names |
[.items[] | select(.spec.nodeName as $n | $names|index($n)) |
select(.status.phase!="Succeeded" and .status.phase!="Failed") |
select(any(.metadata.ownerReferences[]?; .kind=="DaemonSet" and .controller==true)|not) |
{namespace:.metadata.namespace,name:.metadata.name,phase:.status.phase}]
' "$WORK_DIR/pods-before-scale.json" > "$WORK_DIR/old-active-workloads.json"
jq -e 'length == 0' "$WORK_DIR/old-active-workloads.json" >/dev/null
: "${NODEGROUP_MANAGEMENT:?Use the original IaC owner, or explicitly set direct-api for an API-managed group}"
test "$NODEGROUP_MANAGEMENT" = direct-api
guard_nodegroup
jq -e '.nodegroup.scalingConfig.desiredSize == 0 and
((.nodegroup.tags // {} | keys | map(select(startswith("aws:cloudformation:"))) | length) == 0)' \
"$WORK_DIR/nodegroup-current.json" >/dev/null
aws eks delete-nodegroup --cluster-name "$CLUSTER_NAME" --nodegroup-name "$OLD_NODEGROUP" \
--region "$AWS_REGION" --output json > "$WORK_DIR/delete-nodegroup-response.json"
aws eks wait nodegroup-deleted --cluster-name "$CLUSTER_NAME" --nodegroup-name "$OLD_NODEGROUP" \
--region "$AWS_REGION"
```
Deletion waiter가 성공해야 합니다. AccessDenied·만료 자격 증명·조회 실패는 부재가 아닙니다. 별도 소유 IAM/network/storage와 청구도 확인하세요. Node-group API 객체 삭제가 모든 관련 비용 종료의 증거는 아닙니다. 이전 1–2주 안정화는 계획 예시이지 보편적 필수 기간이 아닙니다.
## 7. 최종 검증과 최적화
Wave별 검증 증거를 보존하고 정리 후 controller/Pod readiness, placement, PVC, DNS, 앱 트래픽, IAM, logs/metrics와 전체 비용 할당을 다시 확인합니다. 양쪽 compute·load balancer가 함께 과금될 수 있으므로 비용은 **공존 중에도** 추적합니다.
이전 Pending 0–5/5분간 >10, 시작 <90초/>120초, 가용성 >99.9%/<99.5%, API 응답 <200ms/>500ms는 미검증 예시 임계값입니다. Auto Mode 기본 메트릭이라고 가정하지 말고 실제 publisher·앱 목표·측정 baseline을 사용하세요.
## Self-Managed Karpenter와 공존
AWS는 직접 공존 이전을 지원합니다. **v1.1** 조건은 migration 기능의 최소값이며 현재 Kubernetes 호환 matrix도 만족해야 합니다. 예를 들어 Kubernetes 1.36은 Karpenter 1.13 이상이 필요합니다. 기존 controller를 유지한 상태에서 별도 taint Auto Mode pool을 만들고 선택한 workload group을 이전합니다.
이전 중 공유 `nodepools.karpenter.sh`·`nodeclaims.karpenter.sh` CRD를 변경·삭제하지 마세요. 일반 Karpenter label 대신 class reference·정확한 pool로 소유권을 기록합니다. 기존 workload가 사라지면 **기존 controller가 finalization을 완료할 수 있는 동안** 해당 소유 NodePool/NodeClaim만 정리합니다. 인스턴스·의존성 정리를 확인한 뒤 자체 release와 그 소유 IAM/queue만 제거하세요. 먼저 uninstall하거나 namespace 삭제를 리소스 정리 대용으로 사용하면 안 됩니다.
## Auto 용량 제거 전 Capacity·Placement Rollback
이는 **workload/인프라 이전 rollback**이며 Kubernetes control-plane version rollback과 다릅니다.
1. Auto pool을 유지하고 호환되는 이전 용량을 먼저 복원·확보합니다. Old group을 이미 삭제했다면 scaling JSON만으로 재생성할 수 없습니다.
2. 기록된 scaling 값을 현재 수요와 대조합니다. 여전히 존재하며 identity가 같은 group에는 아래처럼 원 설정을 복원할 수 있습니다.
```bash
guard_nodegroup
jq --arg name "$CLUSTER_NAME" --arg group "$OLD_NODEGROUP" '{
clusterName:$name,nodegroupName:$group,scalingConfig:.nodegroup.scalingConfig
}' "$WORK_DIR/nodegroup-before.json" > "$WORK_DIR/restore-capacity-request.json"
aws eks update-nodegroup-config --region "$AWS_REGION" \
--cli-input-json "file://$WORK_DIR/restore-capacity-request.json" --output json \
> "$WORK_DIR/restore-capacity-response.json"
update_id=$(jq -er '.update.id' "$WORK_DIR/restore-capacity-response.json")
wait_eks_update "$update_id" "$OLD_NODEGROUP"
```
3. 이전 노드가 충분히 Ready가 되고 network/DNS/identity/storage agent가 정상인지 확인합니다. Update 성공만으로 Pod 용량 Ready가 입증되지 않습니다. 남아 있는 이전 노드를 uncordon하기 전 UID도 확인하세요.
4. 검토한 controller placement/traffic/data 계획을 복원합니다. 관련 없는 affinity를 보존하면서 충돌하는 Auto selector를 명시적으로 제거하고 이전 fleet에서 실제 readiness·앱 동작을 검증합니다.
5. 그 후에만 정확한 migration 소유 Auto pool/resource를 원 owner로 정리합니다. NodePool 삭제는 node까지 cascade될 수 있으므로 rollback 첫 단계에 삭제하거나 Karpenter label 전체를 선택하면 안 됩니다.
Auto Mode 비활성화는 선택적인 별도 작업입니다. 먼저 Auto 소유 compute/storage/load-balancer 의존성을 해결합니다. 필요하면 세 flag가 한 요청에 들어가야 하며, 아래는 검토할 요청 파일만 준비합니다.
```bash
jq -n --arg name "$CLUSTER_NAME" '{
name:$name,
computeConfig:{enabled:false},
storageConfig:{blockStorage:{enabled:false}},
kubernetesNetworkConfig:{elasticLoadBalancing:{enabled:false}}
}' > "$WORK_DIR/disable-request.json"
```
필요할 때 context 검사·update-ID 추적을 포함한 검토된 절차로 제출합니다. Auto Mode 비활성화가 앱 selector·data·traffic을 복원하거나 authentication mode 변경을 되돌리지는 않습니다.
## 참고 자료
- [Enable Auto Mode on an existing cluster](https://docs.aws.amazon.com/eks/latest/userguide/auto-enable-existing.html)
- [Migration reference, EBS and load balancers](https://docs.aws.amazon.com/eks/latest/userguide/migrate-auto.html)
- [Managed node-group migration](https://docs.aws.amazon.com/eks/latest/userguide/auto-migrate-mng.html)
- [Self-managed Karpenter migration](https://docs.aws.amazon.com/eks/latest/userguide/auto-migrate-karpenter.html)
- [Managed node-group scaling and PDB behavior](https://docs.aws.amazon.com/eks/latest/userguide/update-managed-node-group.html)
- [Auto Mode networking and mixed-node DNS](https://docs.aws.amazon.com/eks/latest/userguide/auto-networking.html)
- [NodeClass identity and access entry](https://docs.aws.amazon.com/eks/latest/userguide/create-node-class.html)
- [Karpenter/Kubernetes compatibility](https://karpenter.sh/docs/upgrading/compatibility/)
- [Safely drain a Kubernetes node](https://kubernetes.io/docs/tasks/administer-cluster/safely-drain-node/)
- [Kubernetes Pod disruptions](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/)
< [이전: 워크로드 최적화](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/08-workload-optimization.md) | [목차](https://www.atomai.click/kubernetes-docs/llms/ko/eks-auto-mode/README.md) | [EKS 주제로](https://www.atomai.click/kubernetes-docs/ko/) >
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/networking/
----------------------------------------
# Kubernetes 네트워킹
> **마지막 업데이트**: 2026년 9월 13일. 기능 근거는 Cilium 1.20.1, Calico Open Source 3.32, Flannel 0.28.9, AWS VPC CNI 1.23.0을 포함합니다. 설치 전 제품별 Kubernetes·플랫폼 지원 범위를 확인합니다. 이 버전들을 하나의 클러스터에서 함께 검증했다는 의미는 아닙니다.
## 개요
Kubernetes 네트워킹은 컨테이너화된 애플리케이션 간의 통신을 가능하게 하는 핵심 인프라 계층입니다. 이 섹션에서는 Kubernetes 네트워킹의 기본 개념부터 고급 CNI(Container Network Interface) 솔루션, 그리고 AWS EKS 환경에서의 네트워킹 패턴까지 다룹니다.
## Kubernetes 네트워킹 모델
현재 Kubernetes 모델의 Pod 네트워크는 **의도적인 네트워크 분리를 고려하면서**, 노드 경계를 넘어 주소 변환이나 프록시 없이 Pod끼리 직접 통신할 수 있는 기반을 제공합니다. kubelet 같은 노드 에이전트는 자기 노드의 Pod에 접근할 수 있어야 합니다. 개별 연결의 성공 여부는 정책, 라우팅, 애플리케이션 리스너에도 달려 있습니다.
일반 Pod는 자체 네트워크 네임스페이스와 클러스터 범위 주소를 가지며, 같은 Pod의 컨테이너는 그 네임스페이스와 localhost를 공유합니다. host-network Pod는 노드 네트워크를 공유하고 dual-stack·다중 네트워크 구성에서는 주소를 더 세밀하게 구분해야 합니다. Pod를 재생성하면 IP가 달라질 수 있지만 같은 Pod 내부 컨테이너를 재시작한다고 네트워크 sandbox까지 반드시 재생성되지는 않습니다.
| 구성 요소 | 역할 |
|---|---|
| Pod 네트워크 | 워크로드 네트워크 네임스페이스의 주소와 연결 제공 |
| Service·discovery | 바뀌는 엔드포인트에 안정적인 서비스 이름이나 가상 주소 제공 |
| Ingress/Gateway 구현 | 설정한 외부 진입점과 애플리케이션 라우팅 제공 |
| 네트워크 정책 엔진 | 선택한 구현이 지원하는 정책 강제 |
이 역할이 반드시 순서대로 지나는 패킷 경로를 뜻하지는 않습니다. Service 주소 변환, L7 프록시, 워크로드 정책에 따라 실제 요청 경로가 달라집니다.
### Pod 네트워킹
Pod 네트워킹은 통신에 필요한 주소와 라우팅을 제공합니다. 아래 그림은 일반 IPv4 Pod 예제이며 해당 정책과 네트워크 제어가 연결을 허용한다고 가정합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-networking-readme-1.html)
주소는 설명용 일반 Pod 주소입니다. 의도적인 격리나 host-network·다중 네트워크 구성에서는 별도의 해석이 필요합니다.
#### Pod 네트워킹 구현 방식
| 방식 | 설명 | 예시 CNI |
|------|------|----------|
| **Overlay 네트워크** | 기존 네트워크 위에서 트래픽 캡슐화 | Flannel VXLAN, Calico VXLAN/IPIP, Cilium VXLAN/Geneve |
| **Native Routing** | 해당 overlay 캡슐화 없이 하부 네트워크의 경로 사용 | AWS VPC CNI, Calico routing/BGP, Cilium native routing |
| **조건부 캡슐화** | 설정한 토폴로지에 따라 직접 경로나 캡슐화 선택 | 제품별 전제가 다른 Calico/Flannel/Cilium 지원 모드 |
### Service 네트워킹
Service는 보통 Pod로 이루어진 논리적 엔드포인트 집합과 접근 방법을 정의합니다. 기본 ClusterIP는 안정적인 가상 IP를 제공하고, headless Service는 가상 IP를 생략하며, ExternalName은 DNS CNAME을 사용합니다. Pod 선택자 없이 관리하는 엔드포인트도 참조할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-networking-readme-2.html)
일반적인 노출 방식이며 보안 보장이 아닙니다. NodePort 범위와 접근 가능한 노드 주소를 설정할 수 있고 LoadBalancer는 내부용일 수도 있습니다. ExternalName은 DNS 별칭을 반환하며 전달 프록시를 만들지 않습니다.
#### Service 유형별 특징
`default`에 표시된 target port를 수신하는 `app: my-app` Pod를 준비합니다. NodePort의 기본 할당 범위는 30000–32767이며 변경할 수 있습니다. 외부 접근은 주소, 라우팅, 접근 제어에도 달려 있습니다.
LoadBalancer 예제는 **AWS Load Balancer Controller**를 명시적으로 선택하고 EC2 instance 대상과 할당된 NodePort를 사용합니다. 먼저 해당 컨트롤러와 IAM·서브넷 전제를 구성합니다. EKS Auto Mode는 다른 컨트롤러·class를 사용합니다. 여기서 포트 443은 TCP 포트 선택일 뿐이며, 백엔드 8443에서 TLS를 제공하거나 로드 밸런서에 별도로 설정해야 합니다.
포트 변환은 일반 Kubernetes Service API를 설명합니다. 현재 AWS 문서의 EKS 네이티브 네트워크 정책에는 Service 포트와 컨테이너 포트 일치, 안정적인 강제를 위한 `metadata.ownerReferences`가 있는 컨트롤러 관리 Pod라는 추가 전제가 있습니다. 해당 정책 구현을 시험하기 전에 예제를 이 조건에 맞춥니다.
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: default
spec:
type: ClusterIP
selector:
app: my-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: my-nodeport-service
namespace: default
spec:
type: NodePort
selector:
app: my-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30080
---
apiVersion: v1
kind: Service
metadata:
name: my-loadbalancer-service
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: instance
namespace: default
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- protocol: TCP
port: 443
targetPort: 8443
loadBalancerClass: service.k8s.aws/nlb
allocateLoadBalancerNodePorts: true
```
### Ingress 네트워킹
Ingress 리소스에는 컨트롤러와 데이터 플레인이 필요합니다. 이 HTTP 예제는 AWS LBC, `spec.ingressClassName: alb`, IP 대상을 사용합니다. `api-v1`, `api-v2`, `web-frontend` Service가 `default`에 존재하고 포트 80 및 VPC에서 라우팅 가능한 준비된 Pod 엔드포인트를 제공해야 합니다. 필요한 HTTPS·인증서는 별도로 구성합니다. 설치·대상 전제는 [LBC 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/networking/03-aws-lb-controller.md)를 확인합니다.
Ingress는 HTTP/HTTPS 트래픽을 클러스터 내부 Service로 라우팅하는 규칙을 정의합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-networking-readme-3.html)
상자는 Ingress 데이터 플레인 기능을 나타냅니다. AWS LBC는 ALB를 설정하며 애플리케이션 트래픽이 컨트롤러 조정 프로세스를 통과하지 않습니다. 대상 모드에 따라 Service 가상 IP를 실제 추가 홉으로 거치지 않고 Pod IP나 NodePort에 도달할 수 있습니다.
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
namespace: default
spec:
rules:
- host: api.example.com
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: api-v1
port:
number: 80
- path: /v2
pathType: Prefix
backend:
service:
name: api-v2
port:
number: 80
- host: web.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-frontend
port:
number: 80
ingressClassName: alb
```
## CNI (Container Network Interface)
CNI는 런타임이 컨테이너 네트워크를 설정하는 인터페이스를 표준화합니다. 현재 Kubernetes에서는 kubelet이 CRI로 Pod sandbox 작업을 요청하고 **컨테이너 런타임이 CNI를 관리**합니다. 과거 kubelet의 직접 CNI 관리 플래그는 Kubernetes 1.24에서 제거되었습니다.
### 런타임과 플러그인의 역할
| 주체 | 역할 |
|---|---|
| kubelet | 컨테이너 런타임 인터페이스로 sandbox 생성·제거 요청 |
| 컨테이너 런타임 | 네트워크 설정 선택과 CNI 플러그인 체인 호출 |
| CNI 플러그인 | 설정을 받아 ADD/DEL 등 지원 작업을 수행하고 결과 반환 |
| IPAM 구현 | 주소 할당·반환, 위임한 플러그인이나 공급자별 에이전트로 구현 가능 |
| 선택적 노드 에이전트 | 공급자별 라우팅·정책·IP pool·데이터 플레인 상태 유지 |
런타임이 CNI 인터페이스로 플러그인에 설정을 전달합니다. 모든 플러그인에 별도 장기 실행 에이전트나 IPAM 바이너리가 필수인 것은 아닙니다. veth pair가 흔하지만 다른 인터페이스 구현도 있습니다.
## CNI 비교
| 프로젝트·범위 | 네트워킹과 정책 | 구분할 기능·제약 |
|---|---|---|
| **Cilium 1.20.1** | eBPF 네트워킹, 해당 L7 기능에 Envoy 사용, Cilium 네트워크 정책·Hubble | Linux 워커 데이터 플레인과 AMD64/Arm64 전제. Windows CLI 제공은 Windows CNI 지원이 아님. WireGuard/IPsec과 Beta ztunnel mTLS의 범위가 다름. |
| **Calico Open Source 3.32** | 라우팅·캡슐화 선택, iptables·nftables·eBPF 옵션, 순서 있는 정책 tier와 호스트·워크로드 정책 | Windows에는 Linux eBPF·WireGuard 데이터 플레인 미지원 등 별도 제약이 있음. Whisker/Goldmane 흐름 관측은 Tech Preview. 유료 기능은 제품 edition 표 확인. |
| **Flannel 0.28.9** | 호스트 subnet 할당과 노드 간 전달, VXLAN·host-gw 등 백엔드 | `flanneld` 자체는 NetworkPolicy를 강제하지 않지만 차트의 선택적 `netpol.enabled`가 SIGs 정책 컨트롤러를 배포함. WireGuard 백엔드가 문서화되어 있고 IPsec은 실험 기능. Windows VXLAN은 별도 설정·제약 적용. |
| **AWS VPC CNI 1.23.0 / EKS** | VPC 주소·EC2 ENI/prefix 할당, 지원되는 Linux EC2 노드에서 EKS 표준·Admin 정책 | EKS Auto Mode는 DNS 정책 기능도 가진 관리형 네트워킹 구현. Windows, Fargate, custom networking, prefix delegation, multi-NIC는 각각 조건이 다름. |
| **원래 Weave Net 프로젝트** | 과거 overlay 네트워킹 구현 | 원래 `weaveworks/weave` 저장소가 archived 상태이므로 신규 클러스터의 활성·지원 기본 선택으로 설명하지 않음. |
### 정책·암호화·관측성
- Cilium은 해당 L7 구성 요소를 통해 HTTP/DNS 인식 정책과 클러스터 범위·호스트 정책을 제공합니다. deny/allow 의미는 Calico의 순서 있는 Tier API와 구분합니다.
- Calico Open Source에는 계층적 정책 tier와 호스트 정책이 있습니다. 현재 제품 표의 application-layer 정책, DNS/FQDN 정책, Cluster Mesh는 Cloud/Enterprise 기능이므로 오픈소스 edition 기능으로 혼동하지 않습니다. 문서화된 전송 암호화는 WireGuard입니다.
- Amazon EKS는 Auto Mode와 지원되는 EC2/VPC-CNI 설치에 `ClusterNetworkPolicy` Admin/Baseline 제어를 제공합니다. AWS가 설명하는 DNS/FQDN `ApplicationNetworkPolicy`는 **Auto Mode** 기능입니다. 이름만으로 현재 HTTP 메서드·본문 검사까지 지원한다고 해석하지 않습니다.
- Flannel의 선택적 정책 컨트롤러에는 자체 전제가 있으며 네트워킹 백엔드 선택만으로 정책이 활성화되지는 않습니다.
- 노드 간 암호화, 인증된 워크로드 신원, 애플리케이션 mTLS는 서로 다른 제어입니다. 네트워크 흐름 관측도 애플리케이션 추적이나 프로세스·파일 강제와 다릅니다.
### 라우팅과 성능
Calico와 Cilium은 BGP로 경로를 광고할 수 있지만 이것만으로 멀티클러스터 서비스 검색, 정책 동기화, 암호화가 제공되지는 않습니다. Flannel host-gw는 직접 경로와 적절한 L2 연결이 필요합니다. overlay에는 캡슐화·MTU 고려가 추가되지만 CNI 이름만으로 보편적인 성능 순위를 정할 수 없습니다.
이전 100/98/95/85/80/75% 처리량 그림에는 재현할 워크로드, 버전, 측정 출처가 없었습니다. 비교 가능한 하드웨어·커널·패킷/요청 크기·동시성·암호화/정책 설정·처리량·손실·꼬리 지연을 사용합니다. 별도 [Pod 벤치마크](https://www.atomai.click/kubernetes-docs/llms/ko/networking/06-pod-network-benchmark.md)의 실제 과거 환경과 측정은 해당 문서에 유지합니다.
## CNI 선택 가이드
필요한 라우팅, 정책, 운영체제, 지원 모델을 먼저 선택하고 그 조합을 시험합니다.
| 요구 | 검토 경로 |
|---|---|
| 표준 EKS VPC 주소와 지원 네트워크 정책 | 두 번째 정책 엔진을 추가하기 전에 AWS VPC CNI/EKS 기능 검토 |
| 순서 있는 정책 tier, 호스트 정책, 인프라 BGP | 해당 Calico edition·데이터 플레인과 라우팅 전제 검토 |
| Cilium 정책, Hubble, 선택적 메시 기능 | Linux·커널·플랫폼 호환성과 [Cilium 메시 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/cilium-service-mesh/README.md) 확인. 해당 L7 경로에는 Envoy가 계속 포함됨. |
| 제한된 기능이 필요한 작은 네트워크 | 실제 요구에 맞는 Flannel 백엔드·선택적 정책 컨트롤러 검토 |
| 프로세스·시스템 호출·파일 제어 | 네트워크 정책과 별도로 Tetragon 같은 런타임 보안 구성 요소 검토 |
### EKS 관리형 Add-on 설정
다음은 **설정 payload** 예제이며 같은 워크로드에 Calico와 VPC CNI 정책 엔진을 함께 설치하라는 의미가 아닙니다.
```json
{
"enableNetworkPolicy": "true"
}
```
문자열 `"true"`는 공식 문서의 값 타입과 일치합니다. 기존 Kubernetes 버전에 호환되는 EKS add-on build를 선택하고 해당 build의 설정 스키마를 확인합니다.
```bash
EKS_REGION=ap-northeast-2
KUBERNETES_MINOR=1.35 # Replace with the existing cluster's minor version
aws eks describe-addon-versions --region "$EKS_REGION" --addon-name vpc-cni \
--kubernetes-version "$KUBERNETES_MINOR"
: "${VPC_CNI_ADDON_VERSION:?Set the compatible eksbuild version selected from metadata}"
aws eks describe-addon-configuration --region "$EKS_REGION" --addon-name vpc-cni \
--addon-version "$VPC_CNI_ADDON_VERSION"
```
업스트림 1.23.0 릴리스 번호와 EKS `eksbuild` 버전은 다른 식별자입니다. 의도한 기존 add-on 설정과 변경을 합치고, 무조건 `latest`를 선택하거나 다른 값을 덮어쓰지 않습니다. 기존 타사 정책 구현에서 전환한다면 남아 있는 정책 적용 상태 제거와 검증한 노드·워크로드 전환 계획도 필요합니다.
## EKS 네트워킹 기본 사항
### EKS 기본 네트워킹 아키텍처
| 위치·구성 요소 | 역할 |
|---|---|
| EKS 관리 VPC | AWS가 여러 가용 영역에서 관리형 Kubernetes 제어 평면 실행 |
| 고객 클러스터 VPC | 워커 네트워킹, 선택한 서브넷, EKS 관리 cross-account ENI가 설정한 제어 평면 연결 제공 |
| 선택한 고객 VPC 서브넷의 ALB/NLB | 선택한 공개·내부 애플리케이션 진입점 제공. Internet Gateway/NAT Gateway만으로 해당 라우팅이 대체되지는 않음. |
| NAT Gateway·프라이빗 서비스 엔드포인트 | 워크로드 설계에 필요한 특정 아웃바운드 경로 제공 |
이전 그림은 제어 평면을 고객 VPC 안에, 로드 밸런서를 밖에 표시하여 위 소유 경계로 대체했습니다.
### 컴퓨팅 모드별 DNS와 네트워킹
| 컴퓨팅 모드 | DNS·구성 요소 위치 |
|---|---|
| 표준 EC2 노드 | 일반적으로 설정한 CoreDNS Deployment와 설치한 네트워킹 구성 요소를 사용하며 대체 구현은 별도 지원 설정 필요 |
| 순수 EKS Auto Mode | CoreDNS, VPC CNI, kube-proxy 기능이 관리형 노드 systemd 서비스로 실행되므로 이 노드에는 CoreDNS Deployment/add-on 불필요 |
| Auto Mode와 비 Auto 노드 혼합 | 다른 노드의 Auto Mode DNS 서비스를 사용할 수 없는 비 Auto 노드를 위해 CoreDNS Deployment 유지 |
Auto Mode의 첫 DNS resolver는 노드 로컬입니다. 업스트림 전달과 제어 평면 통신에는 여전히 네트워크 접근이 필요할 수 있으므로 모든 DNS 관련 패킷이 노드 안에 머문다는 보장은 아닙니다. AWS는 Auto Mode의 Admin·DNS 정책을 문서화하고 있으며 표준 EC2 VPC-CNI Admin 정책에는 별도 버전·활성화 전제가 있습니다.
### VPC CNI 동작 방식
AWS VPC CNI는 선택한 IPAM 모드로 일반 Pod에 VPC에서 라우팅 가능한 주소를 제공합니다. 보조 IPv4 주소, 위임 prefix, branch ENI, multi-NIC 구성은 서로 다르며 host-network Pod는 노드 네트워크를 공유합니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-networking-readme-9.html)
secondary-IP 모드만 나타냅니다. warm ENI는 설정 가능한 할당 전략이며 모든 노드가 반드시 하나씩 예약한다는 의미가 아닙니다. prefix delegation, custom networking, branch ENI는 할당 규칙이 다릅니다.
#### ENI 및 IP 제한
| 인스턴스 유형 | 최대 ENI | ENI당 IPv4 슬롯 | 과거 secondary-IP bootstrap 값 |
|--------------|----------|------------|----------------|
| t3.medium | 3 | 6 | 17 |
| t3.large | 3 | 12 | 35 |
| m5.large | 3 | 10 | 29 |
| m5.xlarge | 4 | 15 | 58 |
| m5.2xlarge | 4 | 15 | 58 |
| c5.4xlarge | 8 | 30 | 234 |
VPC CNI 1.23.0의 인스턴스 한계와 과거 max-Pods 표로 확인한 값입니다. 과거 공식은 `ENI 수 × (ENI당 IPv4 슬롯 − 1) + 2`이며 현재 모든 환경의 권장값이 아닙니다. prefix delegation, custom networking, branch ENI, 다중 네트워크 카드는 주소 용량을 바꿉니다. Kubernetes 스케줄링은 kubelet `maxPods`와 리소스에도 제한됩니다. EKS 관리형 노드 그룹은 vCPU 30개 미만에서 `maxPods` 상한 110, 그 외에는 250을 적용하며 사용 가능한 IP 수만으로 상한이 바뀌지 않습니다.
### EKS 네트워킹 고려사항
#### IP 주소 관리
**Linux VPC CNI**에서는 선택한 add-on/Helm/DaemonSet 관리 방식으로 공식 환경 변수를 구성합니다. 아래는 EKS add-on 설정 조각입니다. 이전 `amazon-vpc-cni` ConfigMap의 `enable-prefix-delegation`은 Linux IPAMD를 이렇게 설정하지 않습니다. 변경 시 의도한 다른 add-on 값도 보존합니다.
```json
{
"env": {
"ENABLE_PREFIX_DELEGATION": "true",
"WARM_PREFIX_TARGET": "1"
}
}
```
대신 전체 할당 하한과 여유 IP 목표를 조정할 수 있습니다. `MINIMUM_IP_TARGET` 또는 `WARM_IP_TARGET`을 설정하면 `WARM_PREFIX_TARGET`보다 우선하므로 네 가지 독립적인 목표를 더하는 방식이 아닙니다. 실제 할당은 prefix 단위로 이루어집니다. Nitro 지원, IPv4의 연속된 `/28` 공간, 적절한 kubelet Pod 상한은 별도 전제입니다.
Windows prefix 할당은 별도 경로입니다. AWS는 `amazon-vpc-cni` ConfigMap의 `enable-windows-prefix-delegation`과 warm-target 키를 문서화합니다. Linux 환경 변수 절차를 Windows에 그대로 복사하지 않습니다.
```json
{
"env": {
"ENABLE_PREFIX_DELEGATION": "true",
"MINIMUM_IP_TARGET": "5",
"WARM_IP_TARGET": "2"
}
}
```
#### 사용자 정의 네트워킹
이 IPv4 예제에는 의도한 AZ·VPC의 실제 서브넷·보안 그룹 ID가 필요합니다. custom networking을 켜고 노드의 zone 레이블로 ENIConfig를 선택합니다. 명시적인 ENIConfig 노드 어노테이션이 있으면 레이블보다 우선합니다. 영문·한글 예제는 같은 리전 이름을 사용하며 실제 노드 zone으로 교체합니다. ENIConfig 객체만 설치한다고 custom networking이 활성화되지는 않습니다.
```json
{
"env": {
"AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG": "true",
"ENI_CONFIG_LABEL_DEF": "topology.kubernetes.io/zone"
}
}
```
```yaml
apiVersion: crd.k8s.amazonaws.com/v1alpha1
kind: ENIConfig
metadata:
name: ap-northeast-2a
spec:
securityGroups:
- sg-0123456789abcdef0
subnet: subnet-0123456789abcdef0
---
apiVersion: crd.k8s.amazonaws.com/v1alpha1
kind: ENIConfig
metadata:
name: ap-northeast-2b
spec:
securityGroups:
- sg-0123456789abcdef0
subnet: subnet-fedcba9876543210f
```
## 네트워크 심화 개념
아래 항목들은 이 개요 곳곳에서 이름만 스치듯 언급되는 요소입니다. 각 항목의 전체 설치·설정 절차나 실측값은 링크된 심화 문서에 있으며, 이 절은 그 요소들이 서로 어떻게 다르고 어디에 맞는지를 계층별로 정리합니다.
### L2~L7과 라우터·로드밸런서의 차이
"라우터"와 "로드밸런서"는 종종 같은 자리에서 언급되지만 판단 기준이 다릅니다. 라우터는 목적지 하나에 대해 (일반적으로) 경로 하나를 고르는 장비이고, 로드밸런서는 동등한 대상 여러 개 중 하나를 분산 알고리즘으로 고르는 장비입니다.
| 계층 | 장비·기능 | 판단 기준 | Kubernetes·AWS 매핑 |
|---|---|---|---|
| L2 (링크) | 스위치, 브리지 | 목적지 MAC 주소 | CNI가 만드는 veth pair·Linux 브리지, ENI가 노출하는 가상 NIC |
| L3 (네트워크) | 라우터 또는 투과형 어플라이언스 삽입 | 라우팅은 목적지 IP, 어플라이언스 선택은 흐름 식별자 | VPC의 암묵적 라우터·TGW, IP 패킷을 캡슐화하는 GWLB |
| L4 (전송) | L4 로드밸런서 | 연결·흐름 식별자, 흔히 5-tuple | NLB, kube-proxy(iptables·IPVS·nftables), 별도 eBPF Service 구현 |
| L7 (애플리케이션) | L7 로드밸런서·리버스 프록시 | 요청 단위 호스트·경로·헤더, 프로토콜 인식 | ALB, Ingress/Gateway API 구현체, 서비스 메시 사이드카(Envoy) |
핵심 차이는 **분산 단위**입니다. L4 로드밸런서는 보통 TCP 연결이나 추적하는 UDP 흐름의 대상을 선택합니다. L7 프록시는 같은 연결을 공유하더라도 지원하는 애플리케이션 요청별로 대상을 선택할 수 있습니다. GWLB는 애플리케이션 요청을 해석하는 대신 캡슐화한 IP 흐름을 보안 어플라이언스에 분배합니다. 흐름 고정성은 timeout·상태 검사·failover 설정에 영향을 받으며, 대상 재선택이나 연결 중단이 절대 없다는 보장은 아닙니다.
> 📎 L2/L3 개념의 프로토콜별 정의는 [네트워크 기초 Part 1](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part1.md), ALB/NLB 대상 유형과 실제 설정은 [AWS Load Balancer Controller](https://www.atomai.click/kubernetes-docs/llms/ko/networking/03-aws-lb-controller.md) 참고.
### 계정·VPC 간 연결: TGW·VPC Peering·GWLB·PrivateLink·Lattice
다섯 연결 방식은 계층과 트래픽 모델이 다릅니다. TGW RAM 공유, VPC Peering, PrivateLink, TGW Peering, VPC Lattice의 실측 지연 비교는 [Cross-Org VPC 연결](https://www.atomai.click/kubernetes-docs/llms/ko/networking/05-cross-org-vpc-connectivity.md)에 있습니다. 이 절은 그 표에 없는 GWLB를 포함해 계층 관점으로 다시 정리합니다.
| 연결 방식 | 계층·모델 | 특징 |
|---|---|---|
| VPC Peering | L3, 양방향 IP 라우팅 | 전이(transitive)되지 않음, CIDR 중복 시 구성 불가 |
| Transit Gateway (TGW) | L3, 허브-스포크 IP 라우팅 | 하나 이상의 TGW route table에서 attachment association·propagation을 구성, RAM으로 계정 간 공유 |
| Gateway Load Balancer (GWLB) | L3, 투과형 어플라이언스 삽입 | GENEVE(UDP 6081)로 원본 패킷 캡슐화, VPC 엔드포인트 서비스 모델로 소비자 트래픽을 공급자의 어플라이언스 fleet에 연결 |
| PrivateLink | 사설 엔드포인트 연결 | NLB 기반 endpoint service 외에 resource endpoint도 지원, 소비자·공급자 CIDR 중복 허용 |
| VPC Lattice | 애플리케이션·리소스 네트워킹 | HTTP/HTTPS service는 L7 라우팅과 선택적 IAM 인가 지원, TLS passthrough·resource configuration은 기능 범위가 다름 |
GWLB는 Gateway Load Balancer endpoint를 통해 방화벽·IDS/IPS 같은 검사 어플라이언스를 IP 경로에 삽입합니다. 기본 흐름 고정성은 5개 필드를 사용하며 지원되는 설정에서는 2개 또는 3개로 바꿀 수 있습니다. 왕복 라우트, 어플라이언스 상태, 캡슐화 MTU, NACL 및 실제 workload·어플라이언스의 security group을 확인합니다. GWLB 자체에는 ALB 형태의 security group이 없으며, 흐름 고정성이 장애 검증을 대신하지 않습니다.
> 📎 EKS와 VPC Lattice의 전체 연동(Gateway API Controller, IAM 인가, 라우팅)은 [VPC Lattice](https://www.atomai.click/kubernetes-docs/llms/ko/networking/02-vpc-lattice.md) 참고.
### DNS resolver와 Route Table의 실제 동작
**DNS resolver:** AmazonProvidedDNS는 **Route 53 Resolver 자체**입니다. 주소에는 기본 VPC IPv4 network 주소에 2를 더한 값(`10.0.0.0/16`이면 `10.0.0.2`)과 `169.254.169.253`이 있으며, 연결된 private zone과 public 이름을 Resolver 규칙에 따라 해석합니다. CoreDNS는 보통 `cluster.local` 같은 설정된 Kubernetes cluster domain을 담당합니다. `kube-dns`는 Service 이름이며 namespace나 DNS zone이 아닙니다. 외부 조회 전달은 Corefile과 DNS Pod에서 보이는 resolver 파일에 따르므로 node 설정을 그대로 쓴다고 가정하지 말고 실제 구성을 확인합니다. Resolver endpoint를 사용하는 설계에서는 inbound endpoint가 온프레미스 조회를 받고, outbound endpoint와 연결된 규칙이 선택한 VPC 조회를 온프레미스 DNS로 보냅니다. Auto Mode의 node-local resolver도 upstream 의존성을 없애지는 않습니다.
**Route Table:** VPC 라우팅은 일반적으로 최장 접두사 일치를 사용합니다. AWS는 `local` route의 target 교체와 어플라이언스 경로를 위한 지원 범위 내의 더 구체적인 subnet route를 허용하므로 `local`이 무조건 가장 구체적인 것은 아닙니다. 목적지가 같으면 정적 VPC route가 virtual private gateway에서 전파된 route보다 우선합니다. TGW를 target으로 하는 VPC route는 정적이며, TGW 내부 propagation은 별도의 TGW route table에 속합니다. 유효하지 않은 target이 `blackhole` 항목을 남기면 트래픽이 폐기되므로 목적지와 route 상태를 함께 확인합니다. 명시적으로 route table을 연결하지 않은 subnet은 VPC의 main route table을 사용합니다.
> 📎 TGW/Peering 라우트 우선순위와 정적 라우트 구성 예시는 [Cross-Org VPC 연결의 운영 시 확인할 사항](https://www.atomai.click/kubernetes-docs/llms/ko/networking/05-cross-org-vpc-connectivity.md#운영-시-확인할-사항) 참고.
### 커널 데이터 플레인: iptables·IPVS·eBPF·packet filter
Linux의 Service 전달과 network policy 적용은 서로 다른 메커니즘을 사용할 수 있습니다. Netfilter는 iptables·nftables에서 사용하는 패킷 경로 hook을 제공합니다. eBPF 구현은 XDP·tc·socket hook에서 Service 대상을 선택할 수 있습니다. 그렇다고 eBPF를 쓰는 cluster의 모든 패킷이 Netfilter나 connection tracking을 우회하는 것은 아닙니다. 실제 경로는 CNI·kernel·라우팅·기능 설정에 따라 달라집니다.
| 구현 | 위치 | 특징 |
|---|---|---|
| iptables | Netfilter 후크의 순차 규칙 체인 | 규칙 수에 비례해 평가 시간 증가(O(n)), kube-proxy의 오랜 기본 모드 |
| IPVS | 커널 네이티브 L4 로드밸런서, netfilter 확장 | 해시 기반 조회(O(1) 근사), Kubernetes 1.35부터 kube-proxy 모드로는 deprecated |
| nftables | iptables의 후속 netfilter 프레임워크 | 1.33부터 kube-proxy의 stable 모드, 커널·CNI 호환성 확인 필요 |
| eBPF (예: Cilium) | 구성한 XDP·tc·socket hook | kube-proxy의 Service 처리를 대체하는 별도 구현, Netfilter·conntrack 동작은 경로별로 다름 |
구현을 바꾸면 kernel rule과 활성 연결이 남을 수 있습니다. 배포판·CNI의 마이그레이션 절차에 따라 필요한 workload drain을 수행하고, 정리에 필요한 경우 node 재시작을 계획합니다. eBPF 기반 CNI로 kube-proxy를 대체할 때도 두 구현이 같은 Service 트래픽을 두고 충돌하지 않도록 지원되는 전환 순서를 지켜야 합니다.
> 📎 IPVS deprecation 일정과 nftables stable 전환은 [Kubernetes 소개](https://www.atomai.click/kubernetes-docs/llms/ko/basics/04-kubernetes-introduction.md), Cilium의 eBPF kube-proxy 대체 구현은 [Cilium eBPF](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/02-ebpf.md), Calico eBPF 데이터 플레인과 전환 절차는 [Calico eBPF](https://www.atomai.click/kubernetes-docs/llms/ko/networking/calico/06-ebpf-dataplane.md) 참고.
### 컴퓨팅 집약 네트워킹: ENI·EFA·NVLink·광 트랜시버
ENI·EFA·NVLink는 서로 다른 경로를 담당합니다. **ENI**는 하나의 AZ에 있는 EC2 instance에 연결되는 가상 network interface이지만, 일반 IP 트래픽은 라우팅·정책이 허용하면 다른 AZ와 연결된 VPC에 도달할 수 있습니다([VPC CNI](https://www.atomai.click/kubernetes-docs/llms/ko/networking/01-vpc-cni.md) 참고). **EFA**는 호환되는 MPI/NCCL 소프트웨어가 libfabric으로 사용하는 OS-bypass device를 제공합니다. **EFA device 트래픽은 라우팅할 수 없고 VPC/AZ 경계를 넘지 못합니다.** EFA-with-ENA interface의 ENA device를 지나는 일반 IP 트래픽은 여전히 라우팅할 수 있습니다. EFA-only interface에는 ENA device와 IP 주소가 없습니다. **NVLink**는 지원되는 시스템의 GPU를 연결하며, 지원되는 rack-scale NVLink domain도 포함합니다. EFA 대비 고정 배수의 성능 향상을 가정하지 말고 실제 hardware·collective 연산·배치를 측정합니다.
**광 트랜시버**는 일반적인 데이터센터 네트워킹 개념입니다. 구리 DAC(Direct Attach Copper)는 짧은 구간에 사용하고, 광 모듈과 광섬유는 다른 거리·대역폭 요구를 지원합니다. QSFP·OSFP는 모듈의 form factor이며 반드시 광 매체라는 의미는 아닙니다. 이 설명만으로 특정 AWS workload의 실제 물리 배선을 알 수는 없습니다.
> 📎 NVLink/IMEX 토폴로지 인식 스케줄링과 GPU 파드 배치 예시는 [AI/ML 인프라](https://www.atomai.click/kubernetes-docs/llms/ko/ai-ml/06-ai-infrastructure.md), EFA의 VPC/AZ 경계 제약과 실측은 [Cross-Org VPC 연결](https://www.atomai.click/kubernetes-docs/llms/ko/networking/05-cross-org-vpc-connectivity.md) 참고.
### 차세대 프로토콜의 Kubernetes 함의: HTTP/3·gRPC·QUIC
HTTP/3(RFC 9114)와 그 전송 기반인 QUIC(RFC 9000)의 프로토콜 동작 자체는 [네트워크 기초 Part 2](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part2.md)·[Part 3](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part3.md)에서 다룹니다. 여기서는 Kubernetes 트래픽 분산에 실제로 영향을 주는 지점만 짚습니다.
- **gRPC와 L4 로드밸런서:** gRPC는 HTTP/2 연결에서 요청을 다중화합니다. L4 로드밸런서는 보통 이미 맺어진 TCP 연결을 선택한 endpoint에 유지하며, 그 endpoint가 프록시라면 추가 라우팅을 수행할 수 있습니다. Pod 추가만으로 기존 연결을 재분배하지는 않습니다. RPC 단위 분산에는 호환되는 L7 프록시나 client-side 정책이 필요합니다. streaming RPC는 하나의 호출이므로 내부 메시지를 각각 분산하지 않습니다.
- **Gateway API의 GRPCRoute:** Ingress에는 gRPC 전용 리소스가 없지만 Gateway API는 `GRPCRoute`로 서비스·메서드 단위 라우팅을 표준화합니다. 구현체별 지원 범위(헤더 매칭 개수, 재시도 정책 등)는 컨트롤러 문서를 확인해야 합니다.
- **HTTP/3/QUIC의 클러스터 도달 범위:** 클라이언트와 엣지(예: CDN·로드밸런서) 사이의 HTTP/3 지원과, 클러스터 내부·Ingress 백엔드까지의 HTTP/3 지원은 별개입니다. 다수의 Ingress/Gateway 구현체는 여전히 백엔드 연결에 HTTP/1.1 또는 HTTP/2를 사용하며, 엔드투엔드 HTTP/3 지원 여부는 구현체와 버전마다 다르므로 일반화하지 말고 실제 사용 중인 컨트롤러의 문서를 확인해야 합니다.
## 네트워킹 하위 페이지
이 섹션에서는 다음 주제들을 상세히 다룹니다:
### [VPC CNI](https://www.atomai.click/kubernetes-docs/llms/ko/networking/01-vpc-cni.md)
일반 Pod의 VPC 주소와 모드별 IPAM·정책 전제를 다루는 EKS 네트워킹.
### [Cilium 딥다이브](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/README.md)
eBPF 기반의 고성능 CNI 솔루션. L7 Network Policy, Service Mesh, 관측성(Hubble) 등 고급 기능 제공.
### [Calico 딥다이브](https://www.atomai.click/kubernetes-docs/llms/ko/networking/calico/README.md)
가장 널리 사용되는 CNI 중 하나. 강력한 Network Policy, BGP 지원, 엔터프라이즈 기능. 소개, 아키텍처, 네트워킹 모드, BGP 심화, Network Policy, eBPF, 고급 주제, EKS 통합, 운영 가이드를 다룹니다.
### [VPC Lattice](https://www.atomai.click/kubernetes-docs/llms/ko/networking/02-vpc-lattice.md)
AWS의 관리형 애플리케이션 네트워킹 서비스. 크로스 VPC, 크로스 계정 서비스 간 통신.
### [AWS Load Balancer Controller](https://www.atomai.click/kubernetes-docs/llms/ko/networking/03-aws-lb-controller.md)
Kubernetes Service와 Ingress를 AWS ELB(ALB/NLB)와 통합.
### [Gateway API](https://www.atomai.click/kubernetes-docs/llms/ko/networking/04-gateway-api.md)
차세대 Kubernetes 인그레스 API. 표준화된 리소스 모델과 역할 기반 구성.
### [Pod 네트워크 실측 벤치마크](https://www.atomai.click/kubernetes-docs/llms/ko/networking/06-pod-network-benchmark.md)
같은 노드·같은 AZ·다른 AZ의 Pod 간 RTT·HTTP 레이턴시·처리량과 DNS `ndots:5` 쿼리 증폭을 EKS에서 직접 측정한 숫자.
## 네트워크 트러블슈팅
### 일반적인 문제와 해결 방법
#### Pod 간 통신 실패
```bash
NAMESPACE=default
POD_NAME=iperf-client # An existing diagnostic Pod with nslookup/curl
SERVICE_NAME=my-service
kubectl -n "$NAMESPACE" get pods -o wide
kubectl -n "$NAMESPACE" exec "$POD_NAME" -- nslookup "$SERVICE_NAME"
kubectl -n "$NAMESPACE" exec "$POD_NAME" -- \
curl --connect-timeout 3 --max-time 5 -v "http://$SERVICE_NAME:80/"
kubectl -n kube-system logs -l k8s-app=aws-node -c aws-node --tail=100
kubectl -n kube-system logs -l k8s-app=cilium -c cilium-agent --tail=100
```
표시한 도구가 있는 기존 Pod에서 진단합니다. 설치된 CNI의 로그만 조회하며 Auto Mode 시스템 서비스는 해당 DaemonSet이 아닙니다. DNS 성공, TCP 도달성, 애플리케이션 HTTP 응답은 서로 다른 검사입니다. ICMP가 차단되거나 추가 권한이 필요할 수 있으므로 ping 실패만으로 TCP Service에 접근할 수 없다고 단정하지 않습니다.
#### Service 접근 불가
```bash
NAMESPACE=default
SERVICE_NAME=my-service
kubectl -n "$NAMESPACE" get service "$SERVICE_NAME" -o yaml
kubectl -n "$NAMESPACE" get endpointslices \
-l "kubernetes.io/service-name=$SERVICE_NAME" -o yaml
kubectl -n kube-system logs -l k8s-app=kube-proxy --tail=100
```
현재 엔드포인트 진단에는 EndpointSlice를 사용합니다. Service 선택자, target port, 엔드포인트 준비 상태, 주소 계열, 적용 정책을 확인합니다. kube-proxy가 실제 Service 전달을 담당할 때만 해당 로그를 확인하고, eBPF 대체 구현이나 Auto Mode는 자체 진단을 사용합니다.
#### Network Policy 디버깅
```bash
kubectl get networkpolicies.networking.k8s.io -A
kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg policy get
kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg endpoint list
# For a Calico installation using its standard CRD datastore:
kubectl get networkpolicies.crd.projectcalico.org -A
kubectl get globalnetworkpolicies.crd.projectcalico.org
```
Cilium 명령은 DaemonSet 참조로 선택한 Agent 하나를 조사하므로 장애 시 해당 노드의 Agent를 지정합니다. Calico native API 설치는 다른 API group을 노출할 수 있으니 실제 제공되는 리소스를 확인합니다. Kubernetes, Calico, AWS 확장 정책은 별도 리소스이며 우선순위가 다를 수 있습니다.
### 네트워크 성능 테스트
이 제한된 TCP 실습은 게시자의 고정 Netshoot v0.16 이미지 index를 사용합니다. Linux AMD64·Arm64 이미지를 포함하고 Dockerfile에 `iperf3`가 명시되어 있습니다. TCP 5201이 허용된 테스트 환경에서 Pod를 생성합니다. 설명용 워크로드이며 측정된 CNI 비교 결과가 아닙니다.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: iperf-server
namespace: default
labels:
app: iperf-server
spec:
restartPolicy: Never
automountServiceAccountToken: false
nodeSelector:
kubernetes.io/os: linux
containers:
- name: netshoot
image: nicolaka/netshoot:v0.16@sha256:b09d9b21381f47a79b3cbcb30da25266dc17186ea00ae65e99fdc51396f48e70
command:
- iperf3
- -s
workingDir: /tmp
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
ports:
- containerPort: 5201
protocol: TCP
---
apiVersion: v1
kind: Pod
metadata:
name: iperf-client
namespace: default
labels:
app: iperf-client
spec:
restartPolicy: Never
automountServiceAccountToken: false
nodeSelector:
kubernetes.io/os: linux
containers:
- name: netshoot
image: nicolaka/netshoot:v0.16@sha256:b09d9b21381f47a79b3cbcb30da25266dc17186ea00ae65e99fdc51396f48e70
command:
- sleep
- '3600'
workingDir: /tmp
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
```
```bash
kubectl -n default wait --for=condition=Ready pod/iperf-server pod/iperf-client --timeout=120s
IPERF_SERVER_IP="$(kubectl -n default get pod iperf-server -o jsonpath='{.status.podIP}')"
test -n "$IPERF_SERVER_IP"
kubectl -n default exec iperf-client -- iperf3 -c "$IPERF_SERVER_IP" -t 10 -b 10M
```
클라이언트는 1시간 대기하고 명령은 10초 동안 송신 부하를 10 Mbit/s로 제한합니다. 최대 처리량이 아닌 선택한 경로를 검사합니다. 해석 전에 실제 Pod·노드·AZ 위치, 리소스 제한, 정책을 기록합니다. Windows 노드에는 해당 플랫폼의 도구를 선택합니다. 완료 후 직접 만든 테스트 리소스만 정리합니다.
이 독립 진단 Pod는 연결 검사 용도입니다. EKS 네이티브 네트워크 정책을 시험할 때는 Deployment/Job 관리 Pod와 문서화된 Service·컨테이너 포트 조건을 사용합니다.
## 모범 사례
### 1. IP 주소 계획
- CIDR 블록을 충분히 크게 설계
- Pod 네트워크와 Service 네트워크 분리
- 향후 확장을 고려한 서브넷 설계
### 2. Network Policy 적용
먼저 격리된 `networking-demo` 네임스페이스를 생성합니다. 예제는 표준 Kubernetes NetworkPolicy 의미에 따라 그 안의 모든 Pod의 ingress·egress를 격리하므로 필요한 DNS·애플리케이션 흐름에는 명시적 allow가 필요합니다. 지원하는 정책 엔진이 있어야 강제되며, 추가 cluster/admin 정책 API는 우선순위를 바꿀 수 있습니다. 이 매니페스트 하나가 전체 zero-trust 아키텍처는 아닙니다.
- 기본 거부 정책 적용 (Zero Trust)
- 필요한 트래픽만 명시적으로 허용
- 네임스페이스 간 격리
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: networking-demo
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
```
### 3. 성능 최적화
- 적절한 CNI 선택 (워크로드에 맞는)
- MTU 최적화
- 커널 파라미터 튜닝
### 4. 보안 강화
- 지원되는 전송 암호화를 선택하고 실제 보호 트래픽 범위를 검증합니다.
- 필요한 워크로드·애플리케이션 신원과 mTLS를 구성하고 DNS/IP allowlist와 구분합니다.
- 정책, 인증서, 접근 제어 변경을 정기적으로 검토합니다.
### 5. 관측성 확보
- 네트워크 메트릭 수집
- 플로우 로그 활성화
- 분산 추적 구현
## 다음 단계
1. [VPC CNI](https://www.atomai.click/kubernetes-docs/llms/ko/networking/01-vpc-cni.md) - EKS 기본 CNI
2. [Cilium 딥다이브](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/README.md) - eBPF 기반 네트워킹
3. [Calico 딥다이브](https://www.atomai.click/kubernetes-docs/llms/ko/networking/calico/README.md) - 라우팅·정책·데이터 플레인
4. [VPC Lattice](https://www.atomai.click/kubernetes-docs/llms/ko/networking/02-vpc-lattice.md) - AWS 관리형 네트워킹
5. [AWS Load Balancer Controller](https://www.atomai.click/kubernetes-docs/llms/ko/networking/03-aws-lb-controller.md) - ELB 통합
6. [Gateway API](https://www.atomai.click/kubernetes-docs/llms/ko/networking/04-gateway-api.md) - 차세대 인그레스
7. [Cross-Org VPC 연결](https://www.atomai.click/kubernetes-docs/llms/ko/networking/05-cross-org-vpc-connectivity.md) - 서로 다른 AWS Organization 간 VPC 연결 (실측 기반)
8. [Pod 네트워크 실측 벤치마크](https://www.atomai.click/kubernetes-docs/llms/ko/networking/06-pod-network-benchmark.md) - 노드·AZ 경계별 실측 레이턴시와 처리량
---
## 참고 자료
- [Kubernetes network model](https://kubernetes.io/docs/concepts/services-networking/)
- [Kubernetes Services](https://kubernetes.io/docs/concepts/services-networking/service/)
- [Container runtime and CNI](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/)
- [Kubernetes NetworkPolicy](https://kubernetes.io/docs/concepts/services-networking/network-policies/)
- [CNI specification](https://raw.githubusercontent.com/containernetworking/cni/main/SPEC.md)
- [Calico product editions](https://docs.tigera.io/calico/latest/about)
- [Calico policy tiers](https://docs.tigera.io/calico/latest/network-policy/policy-tiers/tiered-policy)
- [Calico Whisker flow logs](https://docs.tigera.io/calico/latest/observability/view-flow-logs)
- [Calico Windows limitations](https://docs.tigera.io/calico/latest/getting-started/kubernetes/windows-calico/limitations)
- [Flannel 0.28.9 networking and policy](https://raw.githubusercontent.com/flannel-io/flannel/v0.28.9/README.md)
- [Flannel backends](https://raw.githubusercontent.com/flannel-io/flannel/v0.28.9/Documentation/backends.md)
- [Original Weave repository status](https://api.github.com/repos/weaveworks/weave)
- [AWS VPC CNI 1.23.0](https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.23.0/README.md)
- [EKS network policy configuration](https://docs.aws.amazon.com/eks/latest/userguide/cni-network-policy-configure.html)
- [EKS standard and Admin network policies](https://docs.aws.amazon.com/eks/latest/userguide/cni-network-policy.html)
- [EKS prefix delegation and maxPods](https://docs.aws.amazon.com/eks/latest/userguide/cni-increase-ip-addresses-procedure.html)
- [EKS Admin and DNS policy deployment models](https://aws.amazon.com/blogs/containers/enhance-amazon-eks-network-security-posture-with-dns-and-admin-network-policies/)
- [EKS Auto Mode networking](https://docs.aws.amazon.com/eks/latest/userguide/auto-networking.html)
- [EKS add-on requirements](https://docs.aws.amazon.com/eks/latest/userguide/workloads-add-ons-available-eks.html)
- [EKS control plane architecture](https://docs.aws.amazon.com/eks/latest/best-practices/control-plane.html)
- [Netshoot v0.16 image metadata](https://hub.docker.com/v2/repositories/nicolaka/netshoot/tags/v0.16)
- [Netshoot v0.16 Dockerfile](https://raw.githubusercontent.com/nicolaka/netshoot/v0.16/Dockerfile)
- [Tetragon runtime security](https://tetragon.io/docs/overview/)
- [AWS LBC 3.5 NLB configuration](https://github.com/kubernetes-sigs/aws-load-balancer-controller/blob/v3.5.0/docs/guide/service/nlb.md)
- [AWS LBC 3.5 Ingress configuration](https://github.com/kubernetes-sigs/aws-load-balancer-controller/blob/v3.5.0/docs/guide/ingress/annotations.md)
- [Gateway Load Balancer concepts](https://docs.aws.amazon.com/vpc/latest/privatelink/gateway-load-balancers.html)
- [GENEVE encapsulation (RFC 8926)](https://www.rfc-editor.org/rfc/rfc8926)
- [VPC DNS resolver](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html)
- [Route 53 Resolver endpoints and rules](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver.html)
- [VPC route table evaluation order](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Route_Tables.html)
- [Local routes and more-specific subnet routes](https://docs.aws.amazon.com/vpc/latest/userguide/subnet-route-tables.html)
- [Static and propagated route priority](https://docs.aws.amazon.com/vpc/latest/userguide/route-tables-priority.html)
- [AmazonProvidedDNS addresses and behavior](https://docs.aws.amazon.com/vpc/latest/userguide/AmazonDNS-concepts.html)
- [GWLB flow stickiness and failover](https://docs.aws.amazon.com/elasticloadbalancing/latest/gateway/edit-target-group-attributes.html)
- [Kubernetes Service virtual IPs and kube-proxy modes](https://kubernetes.io/docs/reference/networking/virtual-ips/)
- [CoreDNS Service names and forwarding configuration](https://kubernetes.io/docs/tasks/administer-cluster/dns-custom-nameservers/)
- [PrivateLink resource endpoints](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-resources.html)
- [Netfilter/iptables project documentation](https://www.netfilter.org/documentation/index.html)
- [EC2 Elastic Fabric Adapter](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/efa.html)
- [QUIC transport protocol (RFC 9000)](https://www.rfc-editor.org/rfc/rfc9000)
- [HTTP/3 (RFC 9114)](https://www.rfc-editor.org/rfc/rfc9114)
- [gRPC over HTTP/2 and load balancing](https://grpc.io/blog/grpc-load-balancing/)
- [Gateway API GRPCRoute](https://gateway-api.sigs.k8s.io/guides/user-guides/grpc-routing/)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/basics/06-network-fundamentals-part1
----------------------------------------
# 네트워크 기초 Part 1 — 계층 모델과 링크·라우팅 계층
> **마지막 업데이트**: 2026년 9월 11일
::: tip 4부작 시리즈입니다
**Part 1: 계층 모델과 링크·라우팅** *(현재 문서)* ·
[Part 2: 전송 계층과 TLS](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part2.md) ·
[Part 3: 애플리케이션 프로토콜](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part3.md) ·
[Part 4: 요청의 여정과 클라우드](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part4.md)
:::
브라우저 요청에는 여러 프로토콜이 협력합니다. 실제 순서는 캐시, 연결 재사용, IP 및 HTTP 버전에 따라 달라지므로 장애 분석에서는 HTTP뿐 아니라 관련 계층도 확인해야 합니다.
이 시리즈에서는 인터넷을 실제로 굴러가게 만드는 네트워크 프로토콜과 메커니즘 25개를 **계층 순서대로 아래에서 위로** 정리합니다. 계층을 아래에서부터 쌓는 이유는 단순합니다. 상위 계층은 하위 계층이 이미 동작한다고 가정하고 설계되어 있기 때문에, 위에서부터 읽으면 "그건 어떻게 되는 건데?"가 계속 남습니다.
각 항목은 **한 줄 정의 → 동작 방식 → 실무에서 걸리는 지점** 순으로 씁니다.
---
## 0. 계층 지도 한 장
| 계층 | 하는 일 | 이 글에서 다루는 프로토콜 |
|---|---|---|
| 애플리케이션 | 실제 서비스 의미론 | HTTP/3, WebSocket, WebRTC, gRPC, DNS, DoH, DHCP, MQTT, SSH, SMTP |
| 보안 | 암호화·인증 (전송 계층에 얹힘) | TLS |
| 전송 | 종단 간(end-to-end) 데이터 전달 | TCP, UDP, QUIC |
| 인터넷 / 라우팅 | 네트워크 간 경로 결정 | IPv4, IPv6, ICMP, BGP, OSPF, NAT |
| 링크 | 같은 물리 구간 내 전달 | Ethernet, Wi-Fi, VLAN, PPP, ARP |
계층 경계가 깔끔하게 지켜지지 않는 항목이 몇 개 있습니다. TLS는 전송과 애플리케이션 사이에 끼어 있고, QUIC은 UDP 위에 얹혀서 전송 계층 역할을 하며, ARP는 IP와 링크 사이를 잇습니다. NAT는 아예 프로토콜이라기보다 기능입니다. 이런 "예외"들이 실무 트러블슈팅의 대부분을 차지합니다.
---

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-06-network-fundamentals-part1-0.html)
---
## 1. 링크 계층 — 같은 구간 안에서 옮기기
링크 계층의 관심사는 딱 하나입니다. **바로 옆에 붙어 있는 장비에게 어떻게 비트를 넘기는가.** 목적지가 지구 반대편이든 옆 서버든, 이 계층은 "다음 홉(next hop)"까지만 책임집니다.
### Ethernet
**정의:** 유선 로컬 네트워크에서 프레임을 실어 나르는 링크 계층 표준.
**동작:** 데이터를 프레임 단위로 감싸고, 앞에 목적지·출처 MAC 주소를 붙입니다. 알려진 유니캐스트는 MAC 테이블에 따라 포트로 전달합니다. 브로드캐스트와 미학습 유니캐스트는 보통 VLAN 안에서 flooding하며 멀티캐스트 처리는 설정에 따라 다릅니다. 초기 이더넷은 충돌 감지(CSMA/CD)에 의존했지만, 현대의 스위치 기반 풀듀플렉스 환경에서는 충돌 자체가 거의 사라졌습니다.
**실무 포인트:** Ethernet IP 트래픽에서 MTU 1500은 프레임 내부 IP 패킷 1500바이트를 뜻하며 Ethernet 헤더/FCS는 제외합니다. 점보 MTU는 장비/경로별로 다르고 9001은 EC2 지원 값이지 보편적인 Ethernet 크기가 아닙니다. 클라우드에서 VPN이나 오버레이 네트워크를 얹으면 캡슐화 헤더 때문에 실효 MTU가 줄어들고, MTU 탐색까지 실패하면 생길 수 있는 블랙홀은 "핑은 되는데 큰 응답만 멈춘다"는 형태로 나타납니다. 원인 파악이 유독 오래 걸리는 유형의 장애입니다.
**MTU와 MSS의 관계:** MSS는 프레임 전체가 아닌 TCP 데이터 바이트 수를 제한합니다. MTU 1500에서 기본 헤더만 고려하면 IPv4는 1460(1500−20−20), IPv6는 1440(1500−40−20)입니다. 송신자는 실제 패킷에 포함하는 IP/TCP 옵션만큼 데이터 길이를 추가로 줄입니다. TCP는 핸드셰이크에서 MSS를 교환하므로, 터널 구간에서 MTU 문제가 반복될 때는 라우터에서 MSS 클램핑(TCP MSS를 강제로 낮추는 설정)으로 우회하는 방법이 널리 쓰입니다.
### Wi-Fi
**정의:** 무선 구간에서 LAN 프레임을 실어 나르는 링크 계층 표준(IEEE 802.11).
**동작:** 공기라는 공유 매체를 쓰기 때문에 이더넷과 근본적으로 다릅니다. 송신 중 충돌 감지에 의존하지 않고 CSMA/CA로 매체 접근을 조정합니다. 프레임을 보내기 전에 채널이 비었는지 확인하고, 일반 유니캐스트는 ACK/재시도를 사용하며 브로드캐스트/멀티캐스트 처리는 다릅니다. 즉 링크 계층에 이미 재전송이 들어 있습니다.
**실무 포인트:** 링크 계층 재전송과 TCP 재전송이 겹치면서 지연 변동(jitter)이 커집니다. 실시간 통신 품질 문제가 "서버 탓"으로 보고되지만 실제로는 클라이언트의 무선 구간인 경우가 많습니다. 서버 RTT만으로 원인을 특정할 수 없으며 애플리케이션 처리 시간, 클라이언트/AP 재시도, 신호 및 큐 지표를 함께 확인합니다.
### VLAN
**정의:** 공유 스위치 인프라를 논리적 L2 네트워크로 나누는 기술(IEEE 802.1Q).
**동작:** 802.1Q 태그 프레임은 4바이트 VLAN 태그를 담습니다. 액세스 포트에서는 태그 없는 프레임을 스위치가 해당 VLAN에 연결할 수도 있습니다. 같은 VLAN끼리만 브로드캐스트가 도달하므로, 물리 배선을 바꾸지 않고 네트워크를 분리할 수 있습니다. VLAN 간 통신은 반드시 L3 장비(라우터 또는 L3 스위치)를 거쳐야 합니다.
**실무 포인트:** VLAN은 논리적 L2 분리이며 물리적 분리나 암호화를 제공하지 않습니다. 구간 간 허용 트래픽은 라우팅과 방화벽으로 제어합니다. VPC·서브넷·보안 그룹은 서로 다른 클라우드 네트워크 역할을 하며 VLAN의 일대일 대체물이 아닙니다.
> 📎 EKS의 VPC 구성은 [EKS 네트워킹 기초](https://www.atomai.click/kubernetes-docs/llms/ko/eks/03-eks-networking-part1.md) 참고.
### PPP
**정의:** 두 노드를 직접 연결하는 점대점 링크에서 패킷을 실어 나르는 프로토콜.
**동작:** 이더넷과 달리 주소 지정이 필요 없습니다. 링크 양 끝에 노드가 하나씩만 있으니까요. 대신 링크 수립·선택적 인증·상위 프로토콜 협상 절차(LCP/NCP)를 갖추고 있습니다.
**실무 포인트:** 다이얼업 시대의 유물처럼 보이지만, PPPoE 형태로 여전히 상당수의 가정용 인터넷 회선에서 살아 있습니다. 그리고 표준 1500바이트 Ethernet payload에서는 일반적인 PPPoE 헤더 6바이트와 PPP protocol 필드 2바이트를 빼 IP에 1492바이트가 남습니다. 더 큰 하위 링크와 협상 지원이 있으면 1500을 유지할 수 있습니다. 앞서 말한 MTU 문제의 대표적 원인입니다.
### ARP
**정의:** 같은 링크의 IPv4 다음 홉 주소를 MAC 주소로 확인하는 프로토콜.
**동작:** IP 계층은 "10.0.1.5로 보내라"고 지시하지만, 이더넷은 MAC 주소만 이해합니다. 그래서 호스트는 브로드캐스트로 "10.0.1.5의 MAC 주소를 가진 분?"이라고 묻고, 해당 호스트가 응답합니다. 결과는 OS별 상태/시간 제한에 따라 캐시됩니다. 다른 링크의 목적지로 보낼 때는 원격 호스트가 아닌 게이트웨이의 MAC을 확인합니다.
**실무 포인트:** ARP는 인증이 없습니다. 아무나 "그 IP는 제 겁니다"라고 응답할 수 있어서 ARP 스푸핑이 성립합니다. 반대로 이 성질을 이용하는 정상 기법도 있습니다. 페일오버 시 새 액티브 노드가 Gratuitous ARP로 이웃의 VIP-to-MAC 정보를 갱신하고 스위치도 프레임의 source MAC 위치를 학습하게 하는 방식이 대표적입니다. VIP 기반 HA 구성에서 전환이 느릴 때는 이 갱신이 지연되는 경우를 의심해볼 수 있습니다.
> 📎 Cilium이 L2/라우팅 동작을 eBPF와 어떻게 통합하는지는 [Cilium 네트워킹](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/03-networking.md) 참고.
---
## 2. 인터넷·라우팅 계층 — 네트워크를 건너가기
링크 계층이 "옆집까지"라면, 이 계층은 "지구 반대편까지"를 담당합니다. 핵심 질문은 **어디로 보낼 것인가**입니다.
### IPv4
**정의:** 32비트 주소 체계 기반의 인터넷 계층 프로토콜.
**동작:** 각 패킷에 출발지·목적지 IP를 붙이고, 라우터는 라우팅 테이블에서 가장 구체적인(longest prefix match) 경로를 찾아 다음 홉으로 넘깁니다. 배달을 보장하지 않는 최선 노력(best-effort) 방식이며, 순서 보장도 없습니다. 그 보장은 위층(TCP)의 일입니다.
**실무 포인트:** IPv4 주소 공간은 약 43억 개이며 가용 주소 부족으로 NAT가 널리 사용되고, 사설 주소 대역(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)이 내부망 표준이 됐습니다. 대규모 조직에서 클라우드 마이그레이션할 때 가장 먼저 부딪히는 문제가 이 사설 대역의 중복입니다. 온프레미스와 VPC CIDR이 겹치면 단순 라우팅으로는 구분할 수 없으므로 주소 재설계·변환·프록시 같은 별도 해결책을 설계해야 합니다. IP 주소 설계는 프로젝트 시작 시점에 확정해야 하는 항목입니다.
### IPv6
**정의:** 128비트 주소 체계의 차세대 인터넷 계층 프로토콜.
**동작:** 주소가 128비트로 늘어나 고갈 문제가 사라집니다. 기본 헤더는 고정 40바이트이며 헤더 체크섬이 없고 라우터가 IPv6 패킷을 단편화하지 않습니다. SLAAC를 통해 DHCP 없이도 주소를 자동 설정할 수 있고, ARP는 NDP(Neighbor Discovery Protocol)로 대체됩니다.
**실무 포인트:** IPv4와 하위 호환되지 않습니다. 그래서 현실에서는 듀얼 스택으로 운영하며, 이는 방화벽 규칙과 보안 정책을 두 벌 관리해야 한다는 뜻입니다. IPv6 경로에 대한 규칙 누락은 흔한 보안 공백입니다. 글로벌 IPv6 주소만으로 워크로드가 인터넷에서 접근 가능해지는 것은 아닙니다. AWS에서도 라우팅과 보안 그룹/NACL 허용이 필요하며 egress-only internet gateway로 외부의 새 연결을 막고 아웃바운드를 제공할 수 있습니다.
**전환 메커니즘:** IPv4와 공존하는 현실적 방법은 세 갈래입니다. **듀얼 스택**(두 프로토콜을 나란히 운영 — 가장 흔하지만 정책 이중화 비용), **터널링**(IPv6 패킷을 IPv4로 감싸 통과), 그리고 **NAT64/DNS64**(IPv6 전용 클라이언트가 IPv4 서버에 접근하도록 변환 — 모바일 통신망이 464XLAT 형태로 대규모로 사용). 쿠버네티스도 듀얼 스택 Service를 지원하므로, 클러스터 CIDR 설계 시 IPv6 대역을 함께 고려할 수 있습니다.
### ICMP
**정의:** 네트워크 오류와 상태를 보고하는 제어 프로토콜.
**동작:** 제어 정보를 전달하며 Echo payload나 원본 패킷의 일부 데이터도 포함할 수 있습니다. 목적지 도달 불가, TTL 초과, 단편화 필요 등을 알립니다. ping은 Echo Request/Reply를, traceroute는 TTL(IPv6는 Hop Limit)을 1씩 늘려가며 돌아오는 Time Exceeded를 이용합니다.
**실무 포인트:** "보안상" ICMP를 전면 차단하는 정책이 흔한데, 이게 앞서 언급한 MTU 블랙홀의 직접적 원인입니다. 전통적인 IPv4 PMTUD는 ICMP Type 3 Code 4, IPv6는 ICMPv6 Packet Too Big Type 2를 사용합니다. 필요한 메시지를 막으면 블랙홀이 생길 수 있지만 PLPMTUD는 ICMP에 의존하지 않고 패킷 크기를 탐색할 수도 있습니다. IP 버전과 정책에 맞는 오류/탐색 트래픽을 허용합니다.
> 📎 EKS에서 이 문제가 실제로 나타나는 형태는 [EKS 네트워킹 심화](https://www.atomai.click/kubernetes-docs/llms/ko/eks/03-eks-networking-part3.md) 참고.
### OSPF
**정의:** 하나의 자율 시스템 내부에서 최적 경로를 계산하는 링크 상태 라우팅 프로토콜.
**동작:** 각 라우터가 자신의 링크 상태를 영역 전체에 전파하고, 같은 영역의 라우터는 수렴 후 일관된 링크 상태 정보를 갖습니다. 그 위에서 다익스트라 알고리즘으로 최단 경로를 계산합니다. 인터페이스 코스트를 구성하며(보통 대역폭을 기준으로 산정), 규모 확장을 위해 영역(Area)으로 나눕니다.
**실무 포인트:** 내부망(IGP)용입니다. 수렴이 빠르고 자동으로 최적 경로를 찾지만, 라우터마다 자신이 속한 영역의 링크 상태 정보를 유지하므로 대규모에서는 영역 설계가 성능을 좌우합니다.
### BGP
**정의:** 자율 시스템(AS) 간에 경로 정보를 교환하는 경로 벡터 라우팅 프로토콜.
**동작:** OSPF와 목적이 다릅니다. "가장 빠른 길"이 아니라 "정책적으로 원하는 길"을 고릅니다. 각 AS는 자신이 도달 가능한 프리픽스와 AS 경로를 이웃에게 광고하고, 수신 측은 AS_PATH 길이, Local Preference, MED 등의 속성으로 우선순위를 결정합니다. 인터넷 전체의 라우팅이 이 위에서 성립합니다.
**실무 포인트:** BGP는 광고를 기본적으로 신뢰합니다. 그래서 잘못된 프리픽스 광고 하나가 대륙 단위 장애로 번지는 사고가 반복적으로 발생했습니다. RPKI origin validation은 프리픽스 origin AS 권한을 검증하지만 전체 AS 경로나 모든 경로 유출을 검증하는 것은 아닙니다. 클라우드 관점에서는 Direct Connect는 BGP를 사용하며 Site-to-Site VPN은 BGP 또는 지원되는 정적 라우팅을 사용하므로, AS 번호와 광고 프리픽스 설계, 그리고 이중화 시 경로 우선순위 조정(AS_PATH prepending 등)이 실제 설계 항목으로 들어옵니다.
> 📎 Calico가 BGP를 클러스터 내부에서 어떻게 쓰는지는 [Calico BGP 심화](https://www.atomai.click/kubernetes-docs/llms/ko/networking/calico/04-bgp-deep-dive.md) 참고.
### NAT
**정의:** IP 주소를 변환하고 NAPT/PAT에서는 전송 포트도 변환하는 네트워크 기능.
**동작:** 흔한 용도는 여러 사설 호스트가 PAT/NAPT로 공인 주소 하나를 공유하는 것입니다. 사설 주소 간 변환도 가능하므로 항상 공인 인터넷 주소 공유만을 뜻하지는 않습니다. 변환 테이블에 세션별 매핑을 유지해 응답 패킷을 올바른 내부 호스트로 되돌립니다.
**실무 포인트:** 계층 모델을 위반하는 대표적 존재입니다. L3 장비인데 L4 포트를 건드리고, 종단 간 연결성이라는 인터넷의 원래 전제를 깨뜨립니다. 그 결과 P2P 통신이 어려워지고, STUN/TURN 같은 우회 기법이 필요해집니다(뒤의 WebRTC 참고). 클라우드에서는 NAT Gateway의 포트 고갈과 데이터 처리 비용이 실무 이슈입니다. 아웃바운드 트래픽이 많은 워크로드라면 지원되는 AWS 서비스의 VPC 엔드포인트로 NAT 처리를 줄일 수 있지만 엔드포인트 시간/데이터 요금과 트래픽 경로를 비교해야 합니다.
---
**다음:** [Part 2: 전송 계층과 TLS](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part2.md)
## 검증 참고 자료
- https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/network_mtu.html
- https://www.rfc-editor.org/rfc/rfc894
- https://www.rfc-editor.org/rfc/rfc6691
- https://www.rfc-editor.org/rfc/rfc4638
- https://www.rfc-editor.org/rfc/rfc5227
- https://www.rfc-editor.org/rfc/rfc792
- https://www.rfc-editor.org/rfc/rfc8899
- https://www.rfc-editor.org/rfc/rfc2328
- https://www.rfc-editor.org/rfc/rfc6811
- https://docs.kernel.org/networking/bridge.html
- https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html
- https://docs.aws.amazon.com/vpc/latest/userguide/egress-only-internet-gateway.html
- https://docs.aws.amazon.com/vpn/latest/s2svpn/VPNRoutingTypes.html
- https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-scenarios.html
- https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-pricing.html
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/basics/06-network-fundamentals-part2
----------------------------------------
# 네트워크 기초 Part 2 — 전송 계층과 TLS
> **마지막 업데이트**: 2026년 9월 11일
::: tip 4부작 시리즈입니다
[Part 1: 계층 모델과 링크·라우팅](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part1.md) ·
**Part 2: 전송 계층과 TLS** *(현재 문서)* ·
[Part 3: 애플리케이션 프로토콜](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part3.md) ·
[Part 4: 요청의 여정과 클라우드](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part4.md)
:::
Part 1이 패킷을 목적지 호스트까지 보냈다면, 이 파트는 신뢰성 있는 스트림(TCP·QUIC)과 UDP 데이터그램을 비교하고 TLS의 통신 보호를 설명합니다. UDP 자체에는 신뢰성이나 TLS가 내장되어 있지 않습니다. 애플리케이션은 DTLS 같은 적절한 보안 프로토콜을 선택하거나 TLS 1.3을 통합한 QUIC 같은 전송 프로토콜을 사용합니다.
먼저 이 파트의 핵심을 그림 하나로 요약하면 이렇습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-06-network-fundamentals-part2-0.html)
---
## 3. 전송 계층 — 종단 간 전달
여기서부터는 "네트워크"가 아니라 "프로세스"가 대화 상대입니다. 포트 번호가 등장하는 이유입니다.
### TCP
**정의:** 신뢰성 있는 순차적 바이트 스트림을 제공하는 연결 지향 전송 프로토콜.
**동작:** 3-way 핸드셰이크(SYN → SYN+ACK → ACK)로 연결을 수립합니다. 시퀀스 번호로 순서를 보장하고, ACK와 재전송으로 손실을 복구하며, 슬라이딩 윈도우로 흐름을 제어하고, 혼잡 제어 알고리즘으로 네트워크 부하에 적응합니다. 애플리케이션에게는 "빈틈없이 이어지는 바이트 흐름"이라는 깔끔한 추상화를 제공합니다.
**실무 포인트:** 순서대로 전달하기 때문에 **HOL(Head-of-Line) 블로킹**이 발생합니다. TCP 세그먼트가 누락되면 빈 구간 뒤의 바이트를 전달할 수 없습니다. HTTP/2에서는 같은 연결의 여러 스트림 전달이 지연될 수 있지만, 이미 전달된 데이터와 독립적인 애플리케이션 작업까지 모두 멈추는 것은 아닙니다. QUIC은 이처럼 스트림 간에 공유하는 전송 계층의 순서 의존성을 제거합니다.
최적화를 사용하지 않는 일반적인 새 연결은 TCP 수립에 약 1 RTT, 이어서 TLS 1.3 전체 핸드셰이크에 1 RTT(TLS 1.2는 보통 2 RTT)가 필요합니다. 연결 재사용, 세션 재개, 조기 데이터, TCP Fast Open에 따라 달라지고 재시도는 지연을 추가할 수 있습니다. 실제 워크로드를 기준으로 커넥션 풀을 평가해야 합니다.
**혼잡 제어의 계보:** 혼잡 제어는 대역폭·지연·버퍼·애플리케이션 동작과 함께 처리량에 영향을 줍니다. 고전 **Reno**는 손실 시 혼잡 윈도를 줄입니다. 리눅스에서 흔히 기본값으로 사용하는 **CUBIC**은 3차 함수 기반 윈도 증가로 고대역폭 경로의 확장성을 개선합니다. **BBR**은 병목 대역폭과 전파 RTT를 모델링해 전송을 조절하며, 손실·ECN을 사용하는 방식은 구현과 버전에 따라 다릅니다. 장거리·모바일 경로에서 항상 더 높은 처리량을 보장하지는 않습니다. `sysctl net.ipv4.tcp_congestion_control`은 설정된 기본값을 **조회**하는 명령입니다. 기본값 변경은 새 연결에 적용되며 별도의 설정과 측정이 필요합니다.
**TIME_WAIT와 포트 고갈:** 정상적인 순차 종료에서는 일반적으로 능동 종료 측이 TIME_WAIT에 들어가고, 동시 종료에서는 양쪽 모두 들어갈 수 있습니다. 짧은 연결을 대량 생성하면 구현과 목적지 패턴에 따라 연결 튜플·임시 포트·NAT 매핑 한도가 부족해질 수 있습니다. TIME_WAIT 항목 하나가 모든 원격 목적지에 대해 해당 포트를 항상 독점하는 것은 아닙니다. 실제 고갈 지점을 확인하고 커널 설정 변경 전에 연결 재사용을 검토하세요. TIME_WAIT는 이전 연결의 지연 패킷으로부터 새 연결을 보호하는 역할도 합니다.
### UDP
**정의:** 연결 설정 없이 데이터그램을 보내는 최소 기능 전송 프로토콜.
**동작:** 헤더 8바이트에 출발지 포트, 목적지 포트, 길이, 체크섬만 있습니다. 핸드셰이크 없음, 재전송 없음, 순서 보장 없음, 혼잡 제어 없음. "IP에 포트 번호만 붙인 것"에 가깝습니다.
**실무 포인트:** 실시간 애플리케이션은 오래된 데이터를 재전송하기보다 제때 전달하는 것을 우선할 수 있고, DNS도 작은 교환에 UDP를 흔히 사용합니다. 필요한 신뢰성과 적절한 혼잡 제어는 애플리케이션이 구현하거나 QUIC처럼 이를 제공하는 프로토콜을 사용해야 합니다.
주의할 점은 상태가 없어서 스푸핑과 증폭 공격에 쓰이기 쉽다는 것입니다. UDP 기반 서비스를 외부에 노출할 때는 응답 크기 제한과 요청량 제어를 함께 고려해야 합니다.
### QUIC
**정의:** UDP 위에 구현된, 보안이 내장된 다중화 전송 프로토콜.
**동작:** TCP+TLS가 하던 일을 UDP 위에서 처음부터 다시 설계했습니다. 핵심 특징 네 가지입니다.
1. **스트림별 독립 전달** — 각 스트림에 별도의 바이트 순서가 있어 한 스트림의 손실이 다른 스트림의 도착한 데이터 전달을 반드시 막지는 않습니다. 패킷 복구와 혼잡 제어는 여전히 연결·경로에서 공유합니다. 각 스트림의 누락 바이트, 애플리케이션 의존성, HTTP/3 QPACK 의존성으로 인한 블로킹도 남습니다.
2. **암호화 내장** — QUIC은 TLS 1.3 핸드셰이크를 통합하고 TLS 레코드 대신 자체 패킷 보호를 사용합니다. 일반적인 전체 핸드셰이크는 약 1 RTT입니다. 조건을 충족하고 서버가 수락한 재개에서는 **0-RTT 조기 데이터**를 보낼 수 있지만 핸드셰이크 완료는 그 이후입니다. Retry나 추가 핸드셰이크 교환은 지연을 늘릴 수 있습니다.
3. **연결 ID** — 경로 검증과 양 끝점의 지원을 통해 주소 변경 후 연결을 이어갈 수 있습니다. 마이그레이션 제한이나 경로 단절이 있으면 Wi-Fi에서 셀룰러로 전환할 때도 연결이 끊길 수 있으므로 무중단을 보장하지 않습니다.
4. **구현의 유연성** — 사용자 공간 구현이 흔하므로 애플리케이션·라이브러리와 함께 전송 계층 개선을 배포할 수 있습니다. 사용자 공간에서 구현해야 한다는 프로토콜 요구사항은 아닙니다.
**실무 포인트:** HTTP/3은 보통 UDP 443을 사용합니다. 이 경로가 차단되면 HTTP 클라이언트가 서버 지원 여부에 따라 TCP 기반 HTTP/2 또는 HTTP/1.1을 시도할 수 있습니다. QUIC 자체가 TCP로 바뀌는 것은 아닙니다. 성능 차이를 QUIC 때문이라고 판단하기 전에 경로 도달성·협상된 프로토콜·구현과 오프로드 동작을 확인하세요. CPU 비용은 구현과 워크로드에 따라 달라집니다.
한 가지 보안 주의점: **0-RTT 데이터는 재생(replay)될 수 있습니다.** 조기 데이터 교환을 재생하면 애플리케이션이 요청을 여러 번 처리할 수 있으며, 전송 계층의 패킷 중복 제거만으로 애플리케이션 재생 방지가 보장되지는 않습니다. 애플리케이션이 재생되어도 안전하다고 명시적으로 평가한 작업만 허용하세요. GET이라는 이름이나 멱등성 주장만으로 충분하지 않습니다. 서버는 조기 데이터를 거부할 수 있고, HTTP 서버는 `425 Too Early`로 응답해 클라이언트가 핸드셰이크 후 재시도하게 할 수 있습니다. 클라이언트·CDN·원본 서버에 걸쳐 정책을 맞추어야 합니다.
---
## 4. 보안 — TLS
### TLS
**정의:** 전송 구간의 기밀성, 무결성, 인증을 제공하는 프로토콜.
**동작:** 핸드셰이크에서 암호 매개변수를 협상하고 키를 설정합니다. 인증서 기반 핸드셰이크는 인증서와 키 소유 증명을 통해 서버를 인증하지만, PSK 기반 핸드셰이크는 이전에 설정했거나 외부에서 제공한 키로 인증할 수도 있습니다. TLS 레코드는 애플리케이션 데이터를 보호합니다. TLS 1.3은 인증된 암호화(AEAD)로 기밀성과 무결성을 제공합니다.
일반적인 TLS 1.3 전체 핸드셰이크는 약 1 RTT이며, 재개 시에는 추가 조건하에서 조기 데이터를 선택적으로 사용할 수 있습니다. 정적 RSA 키 교환과 구형 암호 스위트는 제거됐지만 RSA 인증서 서명은 계속 지원합니다. 임시 (EC)DHE 키 교환은 PSK와 결합할 때도 전방 비밀성을 제공합니다. **PSK 단독 키 교환과 0-RTT 데이터에는 동일한 전방 비밀성 보장이 없습니다.**
**실무 포인트:** 세 가지가 반복적으로 문제가 됩니다.
- **인증서 만료** — 갱신을 자동화하고 만료 시점과 실제 인증서 배포 성공 여부를 별도로 모니터링하세요.
- **SNI 노출** — TLS 1.3만으로는 ClientHello의 SNI가 숨겨지지 않습니다. 양 끝점에서 지원·설정한 ECH(Encrypted Client Hello, RFC 9849)는 내부 ClientHello를 보호할 수 있습니다. QUIC Initial 패킷의 키는 공개 정보로 유도할 수 있어 Initial 암호화만으로 SNI를 숨기지 못합니다. ECH도 목적지 IP나 모든 트래픽 메타데이터까지 숨기지는 않습니다.
- **종료 지점 설계** — 클라이언트→로드밸런서, 로드밸런서→애플리케이션, 서비스 간 연결을 각각 명시하세요. TLS 종료가 다음 구간을 자동으로 암호화하지는 않습니다. 필요한 구간에는 TLS를 사용하고, 양쪽을 인증서로 인증해야 한다면 mTLS를 사용합니다. 서비스 메시로 자동화할 수 있지만 모든 설계에 필수인 것은 아닙니다.
**인증서 체인과 OCSP 스테이플링:** 일반적인 X.509 검증은 말단 인증서에서 필요한 중간 인증서를 거쳐 설정된 신뢰 앵커까지 경로를 구성합니다. 서버는 필요한 중간 인증서를 보내야 하고, 루트는 보통 클라이언트가 이미 신뢰합니다. 중간 인증서를 빠뜨리면 클라이언트에 따라 실패할 수 있지만 모든 유효한 체인에 중간 인증서가 있는 것은 아닙니다. 폐기 확인은 발급기관과 클라이언트에 따라 다릅니다. OCSP를 지원하면 서버가 서명된 상태 응답을 첨부하는 스테이플링으로 클라이언트의 직접 조회를 줄일 수 있습니다. 보편적인 필수 방식은 아닙니다. Let’s Encrypt는 2025년 8월 OCSP 서비스를 종료하고 CRL을 사용합니다. 실제 CA와 클라이언트에 맞춰 인증서·폐기 확인을 설정하세요.
> 📎 Istio가 이를 자동화하는 방식은 [Istio mTLS](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/istio/security/01-mtls.md) 참고.
**공식 참고 자료**: [TLS 1.3](https://www.rfc-editor.org/rfc/rfc8446.html), [QUIC transport](https://www.rfc-editor.org/rfc/rfc9000.html), [QUIC/TLS](https://www.rfc-editor.org/rfc/rfc9001.html), [HTTP early data](https://www.rfc-editor.org/rfc/rfc8470.html), [ECH](https://www.rfc-editor.org/rfc/rfc9849.html), [Linux TCP settings](https://docs.kernel.org/networking/ip-sysctl.html), [Let’s Encrypt OCSP retirement](https://letsencrypt.org/2025/08/06/ocsp-service-has-reached-end-of-life/).
---
**다음:** [Part 3: 애플리케이션 프로토콜](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part3.md)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/basics/06-network-fundamentals-part3
----------------------------------------
# 네트워크 기초 Part 3 — 애플리케이션 프로토콜 10종
> **마지막 업데이트**: 2026년 9월 11일
::: tip 4부작 시리즈입니다
[Part 1: 계층 모델과 링크·라우팅](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part1.md) ·
[Part 2: 전송 계층과 TLS](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part2.md) ·
**Part 3: 애플리케이션 프로토콜** *(현재 문서)* ·
[Part 4: 요청의 여정과 클라우드](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part4.md)
:::
전송 프로토콜이 제공하는 스트림·데이터그램 위에서 애플리케이션 프로토콜이 실제 서비스를 만듭니다. 이 파트는 이름 해석(DNS·DoH), 부트스트랩(DHCP), 운영 접속(SSH), 메일(SMTP), HTTP/3·WebSocket·WebRTC·gRPC·MQTT를 다룹니다.
---
## 5. 애플리케이션 계층 — 실제 서비스
### DNS
**정의:** 도메인 이름을 IP 주소 등의 레코드로 변환하는 분산 디렉터리 시스템.
**동작:** 계층적 위임 구조입니다. 캐시에 답이 없으면 재귀 리졸버가 루트→TLD→권한 네임서버의 위임을 따라가거나 다른 리졸버에 전달합니다. 캐시가 있으면 일부 또는 전체 조회를 생략할 수 있습니다. A/AAAA는 주소, CNAME은 별칭, MX는 메일 서버, TXT는 여러 프로토콜이 사용하는 문자열을 담습니다.
**실무 포인트:** DNS는 분산 시스템이지만 특정 리졸버·공급자·설정이 공통 의존점이 될 수 있습니다. DNS 페일오버는 장애 감지, 레코드 갱신, 이미 캐시된 응답의 TTL, 애플리케이션 캐시, 기존 연결을 함께 고려해야 합니다. 지금 TTL을 줄여도 이전 응답의 캐시 수명은 단축되지 않습니다. 일부 리졸버는 정의된 장애 조건에서 만료된 응답을 제공할 수도 있습니다(RFC 8767). 각 단계를 측정하세요. 짧은 TTL만으로 전환 시간을 보장하지 못합니다. 로드밸런서와 애니캐스트를 보완적으로 사용할 수 있지만 각각의 상태 감지·수렴 시간도 필요합니다.
**자주 쓰는 레코드 타입 한눈에:**
| 타입 | 용도 | 실무 메모 |
|---|---|---|
| A / AAAA | 도메인 → IPv4 / IPv6 | 가장 기본 |
| CNAME | 별칭 → 정식 이름 | apex의 SOA/NS와 공존 불가; 공급자별 ALIAS/ANAME 또는 Route 53 Alias는 지원 대상에 apex 매핑 제공 |
| MX | 메일 수신 서버 | 우선순위 숫자가 낮을수록 먼저 |
| TXT | 임의 문자열 | SPF/DKIM/DMARC, 도메인 소유 검증 |
| NS | 위임 네임서버 | 하위 존 위임 |
| SRV | 서비스 위치(호스트+포트) | 일부 프로토콜의 디스커버리 |
| CAA | 인증서 발급 허용 CA 제한 | CA의 정책 준수가 필요하며 모든 잘못된 발급을 자체 차단하지는 않음 |
**DNSSEC과 DoH는 다른 문제를 풉니다.** DNSSEC은 검증된 신뢰 체인으로 서명된 DNS 데이터의 출처와 무결성을 인증하며 질의를 암호화하지 않습니다. DoH는 HTTPS로 선택한 리졸버를 인증하고 클라이언트–리졸버 구간의 기밀성·무결성을 보호합니다. 악의적이거나 잘못 설정된 리졸버의 응답이 권한 데이터와 일치함을 자체 증명하지는 않습니다. 둘은 함께 사용할 수 있습니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-06-network-fundamentals-part3-0.html)
### DoH
**정의:** DNS 질의를 HTTPS로 감싸 전송하는 방식.
**동작:** 전통적인 DNS는 일반적으로 평문 UDP와 **TCP** 53을 사용합니다. DoH는 DNS 메시지를 HTTPS로 전송해 해당 구간의 수동 도청과 변조를 방지합니다. 리졸버 주소와 트래픽 메타데이터로 DoH 사용을 식별할 가능성은 남고, 선택한 리졸버는 질의 내용을 볼 수 있습니다.
**실무 포인트:** 별도로 선택한 공개 DoH 리졸버는 조직 리졸버의 필터링·로깅을 우회하거나 사설 이름 해석에 실패할 수 있습니다. DoH 자체가 정책을 무력화하지는 않습니다. 관리형 DoH 리졸버도 로깅·필터링을 적용할 수 있고 브라우저·OS 정책으로 승인된 리졸버를 선택할 수 있습니다. 암호화 해제를 항상 전제하지 말고 분할 DNS와 단말 정책을 검증하세요.
### DHCP
**정의:** 호스트에게 IP 주소와 네트워크 설정을 자동으로 할당하는 프로토콜.
**동작:** 일반적인 초기 **DHCPv4** 교환은 DORA, 즉 Discover→Offer→Request→Acknowledge입니다. 로컬 브로드캐스트나 DHCP 릴레이를 통해 서버에 도달합니다. 임대에는 IPv4 주소·서브넷 마스크·게이트웨이·DNS 설정이 포함될 수 있고 갱신은 더 짧은 교환으로 이루어질 수 있습니다. DHCPv6는 다른 메시지를 사용하며, IPv6 기본 라우터 정보는 보통 Router Advertisement로 받습니다. SLAAC도 IPv6 주소 설정 방법입니다.
**실무 포인트:** 클라우드에서는 대부분 추상화되어 보이지 않지만, VPC의 DHCP 옵션 세트로 DNS 서버와 도메인 네임을 지정하는 지점에서 다시 만납니다. 온프레미스 DNS를 쓰는 하이브리드 구성에서 이름 해석이 안 될 때 확인해야 하는 설정입니다.
### SSH
**정의:** 암호화된 원격 셸 접속과 터널링을 제공하는 프로토콜.
**동작:** 서버 호스트 키로 서버를 인증하고, 키 교환으로 세션 키를 만든 뒤 사용자를 인증합니다(공개키 또는 비밀번호). 이후 모든 트래픽이 암호화됩니다. 원격 셸 외에 포트 포워딩, SFTP, 에이전트 포워딩까지 지원합니다.
**실무 포인트:** 정책에 맞춰 포워딩을 제한하고 서버 호스트 키를 검증하세요. 침해된 호스트가 전달된 에이전트 소켓에 접근하면 에이전트에 서명·인증을 요청할 수 있지만, 보통 개인키 자체가 그 호스트로 복사되는 것은 아닙니다. 에이전트 포워딩이 필요 없다면 점프 호스트(`ProxyJump`)를 검토하세요. 원시 키에는 자체 만료가 없지만 OpenSSH 인증서 유효기간이나 `authorized_keys`의 만료 제한을 사용할 수 있습니다. 퇴사자의 접근을 제거하고 자격 증명을 교체·폐기해야 합니다.
AWS Systems Manager Session Manager는 관리형 노드·IAM 권한·서비스 연결을 설정하면 인바운드 SSH 포트나 SSH 키 배포 없이 셸 접속을 제공할 수 있습니다. CloudTrail은 API 활동을 기록하며 셸 내용을 CloudWatch Logs/S3에 기록하려면 별도 설정이 필요합니다. **Session Manager의 SSH·포트 포워딩 세션에는 세션 내용 로깅을 사용할 수 없습니다.** IAM 기반 접속이라는 이유만으로 모든 명령이 기록되지는 않습니다.
### SMTP
**정의:** 메일 서버 간에 메시지를 전달하는 프로토콜.
**동작:** 클라이언트는 제출 서버에 메일을 보내고, SMTP 서버는 보통 MX 조회로 경로를 정해 메시지를 릴레이하고 수신합니다. IMAP·POP3는 사용자가 이미 사서함에 저장된 메시지에 접근·조회하는 프로토콜이며 SMTP 서버의 수신 역할을 대체하지 않습니다.
**실무 포인트:** SMTP 인증과 TLS는 제출·전송을 보호하지만 표시되는 발신 도메인을 자체 증명하지는 않습니다. 다음 세 가지 도메인 인증 체계가 상호 보완적으로 사용됩니다.
- **SPF** — 봉투 MAIL FROM 또는 HELO 식별자의 발신 호스트를 허용합니다. 표시되는 From 헤더와 자동으로 일치하는 것은 아닙니다.
- **DKIM** — 서명 도메인의 DNS 키로 서명 대상 메시지 내용의 서명을 검증합니다. 서명 도메인은 표시되는 From 도메인과 다를 수 있습니다.
- **DMARC** — 표시되는 From 도메인과 통과한 SPF **또는** DKIM 식별자의 정렬(alignment)을 요구하고 처리·리포팅 정책을 게시합니다.
필요에 맞게 SPF·DKIM·DMARC를 함께 설정하고 보고서와 전달·메일링리스트 동작을 확인하세요. DMARC는 정렬된 인증 방식 하나가 통과해도 성공할 수 있습니다. 이 통제들이 전달 성공을 보장하거나 표시 이름·유사 도메인을 이용한 사칭까지 모두 막지는 않으며 수신자도 자체 정책을 적용합니다.
### HTTP/3
**정의:** QUIC 위에서 동작하는 HTTP의 세 번째 메이저 버전.
**동작:** HTTP 의미론은 버전 간에 공유하지만 HTTP/3은 QUIC 스트림과 자체 프레이밍·매핑을 사용합니다. TCP의 스트림 간 순서 의존성을 제거하지만 스트림 내부 손실, QPACK 의존성, 공유 혼잡 제어에 따른 지연은 남습니다. 일반적인 전체 핸드셰이크는 약 1 RTT이고 지원되는 마이그레이션으로 주소 변경 후 연결을 이어갈 수 있습니다. 독립적인 스트림 전달에 대응하는 QPACK이 HPACK을 대체합니다.
**실무 포인트:** 클라이언트는 `Alt-Svc`, 사전 정보, 지원 프로토콜을 알리는 HTTPS DNS 레코드로 HTTP/3을 발견할 수 있습니다. `Alt-Svc`는 앞선 TCP 연결에서 배울 수 있고, HTTPS 레코드를 지원하는 클라이언트는 그 교환 전에 HTTP/3을 발견할 수 있습니다. 어느 방식도 도달성이나 일정한 지연 절감을 보장하지 않습니다.
독립 전달과 통합 핸드셰이크는 손실이나 지연이 큰 경로에서 도움이 될 수 있습니다. 실제 지연·처리량·CPU 비용은 구현·오프로드·워크로드·네트워크 조건에 따라 다릅니다. 항상 유리하거나 불리하다고 가정하지 말고 대표적인 모바일·데이터센터 트래픽을 측정하세요.
**세 세대 비교로 보는 진화 방향:**
| | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 전송 | TCP | TCP | QUIC (UDP) |
| 연결당 요청 | 순차, 또는 응답 순서를 지키는 파이프라이닝 | 다중화 | 다중화 |
| HOL 블로킹 | 응답 순서와 TCP 전달 | 스트림 간 TCP 순서 의존성 | TCP 스트림 간 순서 의존성 제거; 다른 블로킹은 남음 |
| 헤더 압축 | 없음 | HPACK | QPACK |
| 암호화 | 선택(HTTPS) | HTTPS는 TLS 사용; 평문 HTTP/2도 존재 | QUIC에 TLS 1.3 통합 |
다중화는 순서 의존성이 발생하는 위치를 바꿉니다. HTTP/3은 블로킹 원인 하나를 줄이지만 스케줄링·흐름 제어·애플리케이션 의존성까지 모두 제거하지는 않습니다.
### WebSocket
**정의:** 하나의 연결에서 양방향 메시지를 주고받는 애플리케이션 프로토콜.
**동작:** HTTP/1.1 핸드셰이크는 `Upgrade`와 성공 응답 `101`을 사용합니다. HTTP/2·HTTP/3은 지원되는 경우 Extended CONNECT를 사용합니다(RFC 8441·9220). 연결 후 양쪽은 반복적인 HTTP 폴링 없이 WebSocket 메시지를 전송할 수 있습니다.
**실무 포인트:** 장기 연결을 고려해 해당 유휴 타임아웃보다 짧은 하트비트, 배포 시 점진적 연결 종료, 지터를 넣은 재접속 백오프를 설계하세요. 소켓은 연결을 소유한 인스턴스에 남습니다. 공유 애플리케이션 상태나 Redis Pub/Sub 같은 메시징은 인스턴스 간 이벤트를 전달할 수 있지만 살아 있는 소켓을 이전하거나 자체적으로 영속 전달을 제공하지는 않습니다. 협상된 HTTP 버전의 핸드셰이크와 프록시 지원을 확인하세요.
### WebRTC
**정의:** 브라우저·미디어 서버 등 호환되는 끝점 사이에서 실시간 미디어와 데이터를 교환하는 API와 프로토콜.
**동작:** NAT는 직접 도달을 어렵게 할 수 있지만 양쪽이 NAT 뒤에 있어도 직접 연결에 성공할 수 있습니다. ICE는 호스트, STUN으로 알아낸 서버 반사(server-reflexive), TURN 릴레이 후보를 교환하고 검사합니다. 애플리케이션 시그널링이 세션 설명과 후보를 전달하며 실제 경로는 연결 검사와 정책에 따라 선택됩니다. 미디어는 SRTP를 사용하며 보통 DTLS-SRTP로 키를 설정합니다. 데이터 채널은 SCTP over DTLS를 사용합니다.
**실무 포인트:** TURN 사용량은 대역폭·인프라 비용에 영향을 주며 직접 미디어 경로에서도 시그널링·STUN 등 서비스 비용은 남습니다. NAT 매핑·필터링과 방화벽 동작이 연결에 영향을 주므로 “대칭형 NAT”라는 분류만으로 중계가 항상 필수라고 단정할 수 없습니다. TURN 폴백 비용을 계획하고 실제 망을 시험하세요. 다자간 통화에서는 서버 대역폭·연산을 사용해 완전 P2P 메시보다 클라이언트 업로드를 줄이는 SFU가 흔한 선택입니다.
### gRPC
**정의:** 표준 네이티브 전송에 HTTP/2를 사용하며 보통 Protocol Buffers 서비스·메시지 스키마를 사용하는 RPC 프레임워크.
**동작:** Protocol Buffers 정의로 클라이언트·서버 코드를 생성할 수 있고 단일 요청·응답, 서버 스트리밍, 클라이언트 스트리밍, 양방향 스트리밍을 지원합니다. 바이너리 인코딩은 간결할 수 있지만 JSON 대비 크기·속도는 데이터·구현·압축에 따라 달라지며 프로토콜 보장이 아닙니다.
**실무 포인트:** 네이티브 gRPC는 여러 서비스 API에 적합합니다. 브라우저 API가 네이티브 gRPC의 모든 요구 기능을 노출하지 않으므로 브라우저는 보통 gRPC-Web과 호환 서버 또는 변환 프록시를 사용합니다. 스트리밍 지원 범위는 구현에 따라 다릅니다. 스키마를 이해하는 도구로 내용을 확인하고 디버깅하세요.
gRPC 채널은 **0개 이상의 HTTP/2 연결**을 사용할 수 있고 많은 RPC가 장기 연결을 공유할 수 있습니다. L4 분산은 연결별 백엔드를 선택하므로 연결 풀이 작으면 RPC 트래픽이 집중될 수 있으며 RPC별 분산을 보장하지 않습니다. 적절한 클라이언트 정책이나 gRPC를 이해하는 L7 프록시를 검토하세요. 서비스 메시가 그 프록시를 제공할 수도 있습니다. 이미 수립된 스트림은 선택된 백엔드에 남습니다. 스키마 변경 시 삭제한 Protocol Buffers 필드 번호·이름을 예약하고 번호를 재사용하지 마세요.
> 📎 Istio에서의 gRPC 처리는 [Istio gRPC 고급](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/istio/advanced/05-grpc.md) 참고.
### MQTT
**정의:** 발행-구독 모델의 경량 메시징 프로토콜.
**동작:** 클라이언트가 브로커에 연결해 토픽으로 발행·구독합니다. 고정 헤더는 최소 2바이트지만 실제 패킷에는 가변 헤더·속성·페이로드가 추가될 수 있습니다. QoS 0/1/2는 **해당 송신자–수신자 구간의 프로토콜 전달**에서 최대 1회·최소 1회·정확히 1회를 제공합니다. 발행자→브로커와 브로커→구독자 전달은 별도입니다. 설정된 Will은 특정 연결 종료 조건에서 발행될 수 있으며 MQTT 5의 Will Delay와 재접속 동작에 따라 시점이 달라집니다.
**실무 포인트:** 손실·중복 허용 범위와 비용에 따라 QoS를 선택하세요. 성공하는 일반적인 QoS 2 전달은 PUBLISH·PUBREC·PUBREL·PUBCOMP를 교환하지만 애플리케이션의 데이터베이스 부수 효과나 전체 업무 흐름을 정확히 한 번 실행하게 만들지는 않습니다. QoS 1과 애플리케이션 중복 제거도 선택지입니다. 제품에 맞춰 브로커 가용성, 세션·메시지 영속 상태와 복구를 설계하세요. TLS와 적절한 디바이스 인증·인가를 사용하며 클라이언트 인증서는 발급·교체 관리가 필요한 한 가지 방법입니다.
**공식 참고 자료**: [DoH](https://www.rfc-editor.org/rfc/rfc8484.html), [DNS serve-stale](https://www.rfc-editor.org/rfc/rfc8767.html), [OpenSSH](https://man.openbsd.org/ssh), [Session Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html), [DMARC](https://www.rfc-editor.org/rfc/rfc7489.html), [HTTP/3](https://www.rfc-editor.org/rfc/rfc9114.html), [ICE](https://www.rfc-editor.org/rfc/rfc8445.html), [gRPC performance](https://grpc.io/docs/guides/performance/), [MQTT 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html).
---
**다음:** [Part 4: 요청의 여정과 클라우드](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part4.md)
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/basics/06-network-fundamentals-part4
----------------------------------------
# 네트워크 기초 Part 4 — 요청의 여정과 클라우드 매핑
> **마지막 업데이트**: 2026년 9월 11일
::: tip 4부작 시리즈입니다
[Part 1: 계층 모델과 링크·라우팅](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part1.md) ·
[Part 2: 전송 계층과 TLS](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part2.md) ·
[Part 3: 애플리케이션 프로토콜](https://www.atomai.click/kubernetes-docs/llms/ko/basics/06-network-fundamentals-part3.md) ·
**Part 4: 요청의 여정과 클라우드** *(현재 문서)*
:::
이 시리즈의 프로토콜·메커니즘 25개를 예시 요청으로 연결하고 AWS·쿠버네티스의 관련 역할과 비교합니다. 기능상 대응 관계이며 일대일 대체 관계는 아닙니다.

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-basics-06-network-fundamentals-part4-0.html)
---
## 6. 하나의 요청을 끝까지 따라가기
`https://example.com`에 새로 연결할 때의 개념적 의존 관계입니다. 실제 패킷 추적 결과는 아닙니다. DNS 질의와 핸드셰이크 패킷 자체도 링크·라우팅 계층을 사용하고, 캐시나 기존 연결이 있으면 일부 작업을 생략합니다.
1. **주소 설정** — 호스트는 DHCP, 정적 설정, IPv6 SLAAC·Router Advertisement 또는 관리형 방식으로 주소·경로·리졸버 설정을 이미 갖추고 있습니다.
2. **DNS (또는 DoH)** — 필요할 때 목적지를 조회합니다. 재귀 리졸버는 캐시·위임 조회·전달을 사용할 수 있고 애플리케이션이 루트에 직접 물을 필요는 없습니다. HTTPS 레코드로 연결 매개변수를 알 수도 있습니다.
3. **이웃 해석** — Ethernet의 IPv4에서는 선택한 다음 홉의 MAC이 캐시에 없으면 ARP로 조회합니다. IPv6는 Neighbor Discovery를 사용합니다. 다음 홉은 로컬 목적지 또는 라우터일 수 있습니다.
4. **Ethernet / Wi-Fi** — 해당 다음 홉으로 프레임을 전송합니다. 선택한 경로가 요구할 때만 기본 게이트웨이를 사용합니다.
5. **IP 라우팅** — 라우터는 포워딩 테이블로 패킷을 전달합니다. 경로는 정적·직접 연결·BGP·OSPF 또는 다른 제어 방식으로 구성될 수 있습니다. 요청마다 라우팅 프로토콜을 새로 협상하지는 않습니다.
6. **선택적인 NAT** — IPv4 인터넷 송신 경로에서 사설 주소를 공인 주소로 변환할 수 있습니다. 여러 내부·IPv6 경로는 NAT를 사용하지 않고 사설 주소 간 NAT도 존재합니다.
7. **TCP 또는 QUIC** — 전송 연결을 수립하거나 재사용합니다. HTTP/1.1·HTTP/2는 일반적으로 TCP를, HTTP/3은 UDP 위의 QUIC을 사용합니다.
8. **TLS** — 핸드셰이크 모드에 따라 상대를 인증하고 트래픽 키를 설정합니다. QUIC에서는 TLS 1.3이 7번과 통합됩니다. 재개는 새 인증서 기반 핸드셰이크와 다릅니다.
9. **HTTP** — 협상한 버전으로 요청·응답을 교환합니다. HTTP/3은 QUIC 경로가 필요하며 TCP 경로에서 동작하지 않습니다.
10. **선택적인 애플리케이션 기능** — WebSocket, 브라우저 호환 gRPC, WebRTC는 구현에 따라 추가 연결을 만들거나 기존 전송을 재사용·다중화할 수 있습니다.
**ICMP**는 목적지 도달 불가나 경로에 비해 큰 패킷 등 일부 IP 계층 오류를 알릴 수 있습니다. 모든 오류를 보고하지는 않습니다. 패킷이나 ICMP 오류가 차단될 수 있고 TLS·애플리케이션 오류는 별도 방식으로 처리합니다. 필요한 ICMP 허용과 전송·애플리케이션 로그 및 측정을 함께 사용하세요. ICMP 오류가 없다고 성공이 증명되지는 않습니다.
---
## 7. 클라우드에서 이 개념들은 어디로 가는가
클라우드에도 주소·라우팅·필터링·전송 역할이 있지만 전통적인 장비와 책임 경계가 다릅니다. AWS에서는 다음과 같이 비교할 수 있습니다.
| 전통적 개념 | AWS에서의 대응 |
|---|---|
| 분리와 필터링 | VPC·서브넷은 논리적 망 경계, 보안 그룹·NACL은 필터링이며 VLAN과 동일하지 않음 |
| 라우팅 테이블 | VPC 라우트 테이블, Transit Gateway |
| BGP 피어링 | Direct Connect 가상 인터페이스, 동적 라우팅 Site-to-Site VPN; 정적 VPN 라우팅도 가능 |
| NAT / 사설 서비스 접근 | NAT Gateway는 주소 변환, VPC 엔드포인트는 지원 서비스의 사설 접근 경로 제공 |
| DNS 서버 | Route 53, Resolver 엔드포인트 |
| DHCP | VPC DHCP 옵션 세트 |
| TLS 종료 / 인증서 | ALB HTTPS 리스너·NLB TLS 리스너·CloudFront; ACM은 트래픽 전달이 아닌 지원 인증서 관리 |
| L7 로드밸런싱 | ALB, 별도로 관리하는 서비스 메시의 Istio·Envoy 같은 애플리케이션 프록시 |
| SSH 접속 | Systems Manager Session Manager |
| 내부 구간 암호화 | 애플리케이션·프록시의 TLS/mTLS; 네트워크 계층 암호화는 별도의 설계 선택 |
AWS App Mesh는 기존 설계의 예이며 신규 설계 기본값으로 제시하지 않습니다. AWS가 발표한 지원 종료일은 **2026년 9월 30일**입니다. 기존 배포는 마이그레이션을 계획해야 합니다.
**설계 시 먼저 결정해야 하는 항목 세 가지**를 꼽자면 이렇습니다.
1. **IP 주소 계획** — 온프레미스·Pod·Service 범위를 포함해 연결해야 하는 망의 CIDR을 계획하세요. 중복되면 주소 변환이나 재설계가 필요할 수 있습니다. 주소 변경에는 운영 비용이 있지만 언제나 가장 비싸다는 순위를 단정할 수는 없습니다.
2. **아웃바운드 경로** — 목적지별로 인터넷 또는 사설 서비스 경로를 정하세요. NAT의 시간·데이터 요금, 인터페이스 엔드포인트의 시간·데이터 요금, AZ 간 전송과 가용성 요구를 비교해야 합니다. S3·DynamoDB 게이트웨이 엔드포인트는 추가 엔드포인트 요금이 없지만 모든 인터넷 목적지를 대체하지는 않습니다.
3. **암호화 종료 지점** — 로드밸런서 이후 백엔드 연결을 포함해 구간별 암호화·인증을 명시하세요. 워크로드 요구와 적용 정책을 확인해야 하며 TLS 종료만으로 다음 구간이 보호되지는 않습니다.
---
## 8. 쿠버네티스에서는 누가 이 일을 하는가
클러스터 안에서도 같은 개념이 컴포넌트 이름만 바꿔 반복됩니다. 이 표가 이 시리즈와 이후 심화 문서들을 잇는 다리입니다.
| 전통적 개념 | 쿠버네티스에서의 대응 |
|---|---|
| Pod IP 할당 | CNI/IPAM 통합(VPC CNI, Cilium 등); 반드시 Pod마다 DHCP를 수행하는 것은 아님 |
| 로컬 전달 / 포워딩 | 호스트 인터페이스·이웃 처리·CNI 데이터패스; veth·경로·터널·eBPF 등 구현별 상이 |
| DNS | 보통 CoreDNS를 사용하는 클러스터 DNS; `서비스명.네임스페이스.svc.<클러스터 도메인>`에서 `cluster.local`은 흔한 설정값 |
| Service 가상 IP / L4 분산 | Linux kube-proxy의 iptables·nftables; IPVS는 1.35부터 deprecated. eBPF 구현은 kube-proxy를 대체할 수 있지만 kube-proxy 모드는 아님 |
| Pod 트래픽 정책 | NetworkPolicy를 지원하는 네트워킹 컨트롤러·플러그인이 집행 |
| L7 라우팅 / TLS 종료 | Ingress·Gateway API 리소스와 이를 구현하는 컨트롤러·데이터플레인 |
| 서비스 간 mTLS | 애플리케이션 TLS 또는 설정된 Istio·Linkerd 등 메시; 임의의 CNI 설치만으로 활성화되지 않음 |
| BGP 라우팅 / 광고 | 예: Calico BGP 경로, MetalLB의 Service 주소 BGP 광고; 역할은 서로 다름 |
Pod를 백엔드로 갖는 일반적인 ClusterIP Service는 클러스터 DNS가 Service IP를 반환하고 kube-proxy 또는 대체 구현이 Service·EndpointSlice 상태로 적합한 엔드포인트를 선택합니다. 데이터패스는 같은 노드나 다른 노드의 해당 엔드포인트로 전달합니다. Headless Service는 DNS로 엔드포인트 주소를 노출하고 ExternalName Service는 CNAME을 반환합니다. mTLS는 해당 피어와 정책을 설정했을 때 적용됩니다. 이런 차이 때문에 “모든 계층이 항상 실행된다”는 패킷 흐름 가정은 성립하지 않습니다.
---
## 마무리
프로토콜·메커니즘 25개를 살펴본 뒤에는 각 선택의 절충과 보장 범위를 확인해야 합니다.
TCP는 복구·순서 유지 비용으로 신뢰성 있는 순차 전달을 제공합니다. UDP는 이를 상위 계층에 맡깁니다. QUIC은 UDP 위에 신뢰성 있는 스트림과 통합 보안을 제공합니다. NAT는 공인 IPv4 주소를 절약하지만 외부에서 시작하는 연결을 복잡하게 만들며 ICE·STUN·TURN이 이를 다루는 데 도움을 줍니다. DoH는 리졸버까지의 구간을 보호하며 조직의 가시성은 리졸버·단말 정책에 따라 달라집니다.
장애 조사에서는 각 계층의 실제 보장과 관찰 증거로 원인 범위를 좁히세요. 그럴듯한 프로토콜 설명도 로그·추적·측정으로 다른 가능성과 구분하기 전까지는 가설입니다.
---
## 다음 문서
이 기초 위에서 클러스터 네트워킹으로 넘어갑니다.
- [eBPF 기초](https://www.atomai.click/kubernetes-docs/llms/ko/basics/05-ebpf-fundamentals.md) — 커널에서 패킷을 처리하는 방식
- [Cilium 네트워킹](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/03-networking.md) — eBPF 기반 CNI
- [Calico BGP 심화](https://www.atomai.click/kubernetes-docs/llms/ko/networking/calico/04-bgp-deep-dive.md) — 클러스터 내 BGP 라우팅
- [Amazon VPC CNI](https://www.atomai.click/kubernetes-docs/llms/ko/networking/01-vpc-cni.md) — VPC CNI와 IP 할당
## 참고
프로토콜 목록 구성은 ByteByteGo의 "What Keeps the Internet Running?" 인포그래픽을
출발점으로 삼았으며, 설명과 실무 관점은 별도로 작성했습니다.
공식 참고 자료: [Kubernetes Service proxy modes](https://kubernetes.io/docs/reference/networking/virtual-ips/), [Service DNS](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/), [NetworkPolicy](https://kubernetes.io/docs/concepts/services-networking/network-policies/), [NAT Gateway cost guidance](https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-pricing.html), [ECR VPC endpoints](https://docs.aws.amazon.com/AmazonECR/latest/userguide/vpc-endpoints.html), [App Mesh lifecycle](https://docs.aws.amazon.com/app-mesh/latest/userguide/what-is-app-mesh.html).
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/networking/01-vpc-cni
----------------------------------------
# Amazon VPC CNI
> **검토 기준**: VPC CNI / Helm 차트 1.23.0, network policy agent 1.4.1.
> **최종 검토**: 2026년 9월 11일. 실제 클러스터 버전·리전에 호환되는 EKS add-on build를 선택합니다. 업스트림, Helm, EKS `eksbuild` 버전은 서로 다른 식별자입니다.
## 목차
- [VPC CNI 개요](#vpc-cni-개요)
- [네트워킹 모델](#네트워킹-모델)
- [설치 및 구성](#설치-및-구성)
- [IP 주소 관리](#ip-주소-관리)
- [Network Policy 지원](#network-policy-지원)
- [고급 기능](#고급-기능)
- [트러블슈팅](#트러블슈팅)
- [모범 사례](#모범-사례)
## VPC CNI 개요
Amazon VPC CNI는 표준 EKS EC2 노드에 VPC 기반 Pod 네트워킹을 제공합니다. 이 문서의 `aws-node` DaemonSet 명령도 해당 설치를 대상으로 합니다. Auto Mode는 노드 서비스로 관리형 네트워킹을 실행하고 Fargate·Windows는 관리 경로가 다릅니다. EC2/Linux 절차를 다른 컴퓨팅 모드에 적용하기 전에 [개요](https://www.atomai.click/kubernetes-docs/llms/ko/networking/README.md)를 확인합니다.
Pod는 overlay 캡슐화 없이 VPC에서 라우팅 가능한 주소를 사용합니다. 실제 연결·성능은 라우팅, 보안 그룹, 네트워크 정책, ENI/IP 한계, 애플리케이션에도 달려 있습니다. EKS는 생성 시 선택한 **IPv4 또는 IPv6 Pod/Service 주소**를 지원하며 dual-stack Pod·Service는 지원하지 않습니다. dual-stack VPC나 IPv6 Pod의 IPv4 egress 보조 인터페이스는 다른 개념입니다.
### 아키텍처
| 구성 요소 | 역할 |
|---|---|
| 컨테이너 런타임 / CNI 바이너리 | 런타임이 Pod sandbox의 CNI를 호출하고 AWS 플러그인이 주소 요청·네트워크 네임스페이스 설정 수행 |
| IPAMD | Linux EC2 노드의 주소 pool과 필요한 일반 ENI·IP 관리 |
| EKS 네트워크 정책 컨트롤러 / 노드 에이전트 | 관리형 컨트롤러가 정책 엔드포인트를 해석하고, 활성화된 `aws-eks-nodeagent`가 지원 정책을 eBPF로 강제 |
| VPC resource controller | 별도 전제에 따라 branch/trunk 인터페이스, Windows 주소 할당 등 관리 |
이전 그림은 kubelet이 CNI 바이너리를 직접 호출한다고 표시했습니다. 현재 Kubernetes의 CNI 관리는 컨테이너 런타임이 담당합니다.
### IP 할당 방식
| 특성 | 보조 IPv4 주소 | Prefix delegation |
|---|---|---|
| 할당 단위 | ENI의 개별 보조 주소 | IPv4는 16개 주소의 `/28`, IPv6는 `/80` prefix |
| 용량 | 인터페이스·주소 슬롯 및 kubelet 설정에 제한 | 슬롯당 주소를 늘리지만 지원 하드웨어·여유 prefix·kubelet/리소스 한계 적용 |
| 할당 절충 | 주소 단위로 세밀하게 할당 | 블록을 한 번에 할당하므로 warm target에 따라 미사용 주소 예약 |
| 선택 기준 | 호환성과 측정한 수요 | Nitro 지원, 서브넷 단편화, 워크로드 변경, 기능 조합 확인 |
Prefix delegation이 모든 대규모 클러스터의 필수 조건은 아니며 고갈된 서브넷에 새 주소 공간을 만들어 주지도 않습니다.
## 네트워킹 모델
### ENI 아키텍처
일반 secondary-IPv4 모드에서는 각 ENI에 primary 주소와 CNI가 사용할 추가 주소가 있습니다. Primary ENI는 노드의 primary 주소도 전달합니다. 추가 ENI에서 더 많은 보조 Pod 주소를 공급할 수 있습니다. Custom networking은 Pod 주소를 공급하는 인터페이스·서브넷을 바꾸고, prefix·branch-ENI 모드는 할당 규칙이 다릅니다.
```text
Linux EC2 노드 — 일반 secondary-IPv4 예제
├── Primary ENI: 노드 primary IP + Pod용 secondary IP
├── 추가 ENI: 해당 ENI의 primary IP + Pod용 secondary IP
└── 추가 ENI: 해당 ENI의 primary IP + Pod용 secondary IP
```
### 인스턴스 유형별 ENI/IP 제한
| 인스턴스 유형 | 최대 ENI 수 | ENI당 IPv4 슬롯 | 과거 secondary-IP bootstrap maxPods |
|---|---|---|---|
| t3.medium | 3 | 6 | 17 |
| t3.large | 3 | 12 | 35 |
| m5.large | 3 | 10 | 29 |
| m5.xlarge | 4 | 15 | 58 |
| m5.2xlarge | 4 | 15 | 58 |
| c5.4xlarge | 8 | 30 | 234 |
| m5.8xlarge | 8 | 30 | 234 |
과거 공식은 **`ENI 수 × (ENI당 IPv4 슬롯 − 1) + 2`**입니다. `+2`는 해당 bootstrap 계산의 host-network 시스템 Pod 2개를 반영하므로 m5.large는 `3 × 9 + 2 = 29`입니다. 현재 모든 배포에 host-network Pod가 정확히 2개라는 뜻은 아닙니다.
이 값은 과거 bootstrap 값이며 현재 모든 환경의 Pod 밀도 권장값이 아닙니다. Prefix delegation, custom networking, branch 인터페이스, 다중 네트워크 카드, CPU·메모리, kubelet `maxPods`를 함께 고려합니다. EKS 관리형 노드 그룹은 vCPU 30개 미만에서 `maxPods` 110, 그 외에는 250을 상한으로 적용합니다. 실제 노드의 allocatable 용량을 확인합니다.
### Prefix Delegation
Linux IPv4 prefix 모드의 EKS 관리형 add-on 설정 조각입니다.
```json
{
"env": {
"ENABLE_PREFIX_DELEGATION": "true",
"WARM_PREFIX_TARGET": "1"
}
}
```
아래 관리 절차로 의도한 add-on 설정과 합칩니다. Helm 관리 설치에서는 같은 `env` 매핑을 Helm values에 둡니다. 직접 `kubectl set env`로 바꾼 값은 관리 주체가 다시 조정할 수 있습니다.
IPv4 할당에는 단순히 양수 `AvailableIpAddressCount`가 아니라 적절한 연속 `/28` 블록이 필요합니다. 서브넷 예약·단편화와 Nitro 지원을 확인합니다. Prefix를 켜도 기존 kubelet Pod 상한이나 branch-ENI Pod 상한이 모두 자동으로 증가하지는 않습니다.
## 설치 및 구성
### 관리 주체와 호환성 확인
대상 클러스터의 AWS CLI 자격 증명과 Kubernetes context를 준비하고 읽기부터 시작합니다.
```bash
EKS_REGION=ap-northeast-2
CLUSTER_NAME=my-cluster
KUBERNETES_MINOR="$(aws eks describe-cluster --region "$EKS_REGION" \
--name "$CLUSTER_NAME" --query cluster.version --output text)"
aws eks describe-addon-versions --region "$EKS_REGION" \
--addon-name vpc-cni --kubernetes-version "$KUBERNETES_MINOR"
aws eks describe-addon --region "$EKS_REGION" \
--cluster-name "$CLUSTER_NAME" --addon-name vpc-cni
kubectl -n kube-system get daemonset aws-node -o yaml
```
EKS `describe-addon`의 `ResourceNotFoundException`만으로 CNI가 없다고 판단하지 않습니다. 자체 관리 설치일 수 있습니다. 기존 DaemonSet, ServiceAccount, Helm release, 설정, IAM을 확인하고 관리 주체를 **하나** 선택합니다. Auto Mode 네트워킹에는 이 설치 절차를 적용하지 않습니다.
### 기존 EKS 관리형 Add-on
기존 설정을 내보내고 선택한 호환 build의 스키마를 확인합니다.
```bash
umask 077
aws eks describe-addon --region "$EKS_REGION" \
--cluster-name "$CLUSTER_NAME" --addon-name vpc-cni > vpc-cni-before.json
jq -r '.addon.configurationValues // "{}"' vpc-cni-before.json > vpc-cni-config.json
: "${VPC_CNI_ADDON_VERSION:?Select a compatible EKS add-on build from the metadata}"
aws eks describe-addon-configuration --region "$EKS_REGION" \
--addon-name vpc-cni --addon-version "$VPC_CNI_ADDON_VERSION"
```
필요한 중간 업그레이드 버전과 릴리스 변경을 검토합니다. `vpc-cni-config.json`에 의도한 기존 설정을 보존하면서 선택한 변경만 반영합니다. 일부 payload나 충돌 플래그가 모든 값을 자동 보존한다고 가정하지 않습니다. CNI IAM 권한과 설정한 IRSA/Pod Identity 역할을 확인하고 IPv6에 맞는 권한도 준비합니다.
이미 관리형인 add-on의 검토된 업데이트는 다음 형태로 실행할 수 있습니다.
```bash
set -eu
VPC_CNI_UPDATE_ID="$(aws eks update-addon --region "$EKS_REGION" \
--cluster-name "$CLUSTER_NAME" --addon-name vpc-cni \
--addon-version "$VPC_CNI_ADDON_VERSION" \
--configuration-values file://vpc-cni-config.json --resolve-conflicts PRESERVE \
--query update.id --output text)"
aws eks describe-update --region "$EKS_REGION" --name "$CLUSTER_NAME" \
--addon-name vpc-cni --update-id "$VPC_CNI_UPDATE_ID"
```
`PRESERVE`는 명시적인 충돌 처리 선택이며 결과 환경 변수·이미지·동작을 확인해야 합니다. 기록한 update ID의 상태가 `Successful`이 될 때까지 조회합니다. `Failed`·`Cancelled`이면 오류를 확인하고 진행을 중단합니다. 성공한 뒤 결과 add-on·DaemonSet을 확인합니다.
```bash
aws eks describe-addon --region "$EKS_REGION" \
--cluster-name "$CLUSTER_NAME" --addon-name vpc-cni
kubectl -n kube-system rollout status daemonset/aws-node --timeout=10m
```
API 요청 수락이나 이전 DaemonSet의 준비 상태만으로 이번 업데이트·네트워크 검증이 끝났다고 판단하지 않습니다.
관리형 add-on이 없고 설치·소유 관계 준비를 마친 경우의 생성은 별도 작업입니다.
```bash
aws eks create-addon --region "$EKS_REGION" \
--cluster-name "$CLUSTER_NAME" --addon-name vpc-cni \
--addon-version "$VPC_CNI_ADDON_VERSION" \
--configuration-values file://vpc-cni-config.json
```
개별 기능을 켤 때마다 `create-addon`을 반복하거나, 기존 사용자 설정에 마이그레이션 계획 없이 `OVERWRITE`를 강제하지 않습니다.
### Helm 관리 설치
Init·정책 에이전트 구성 요소도 함께 선택되도록 전체 차트를 고정합니다. 이미지 태그 두 개만 바꿔서는 차트의 나머지가 업데이트되지 않습니다.
```bash
helm repo add eks https://aws.github.io/eks-charts
helm repo update eks
helm show values eks/aws-vpc-cni --version 1.23.0 > chart-defaults.yaml
helm template aws-vpc-cni eks/aws-vpc-cni --namespace kube-system \
--version 1.23.0 -f helm-values.yaml > rendered-cni.yaml
```
실제 IP family, CNI ServiceAccount/IAM, 선택한 기능에 맞게 `helm-values.yaml`을 준비합니다. 이 문서의 기본 Linux 예제는 IPv4입니다. 렌더링한 리소스를 검토한 뒤 적용합니다.
```bash
helm upgrade --install aws-vpc-cni eks/aws-vpc-cni --namespace kube-system \
--version 1.23.0 -f helm-values.yaml --wait --timeout 10m
```
명령은 새 설치나 Helm 관리 설치를 전제로 합니다. 기존 EKS add-on·bootstrap 관리 객체는 소유 전환 계획이 필요합니다. EKS 파티션·레지스트리 접근과 이미지 pull 전제도 환경에 맞춰야 합니다.
### 주요 설정 값
| 설정 | 의미 | 기준·기본값 구분 |
|---|---|---|
| `WARM_IP_TARGET` | 새 일반 Pod 할당에 대비한 여유 주소 목표 | 기본 미설정, 최대 한계가 아님 |
| `MINIMUM_IP_TARGET` | 전체 할당 주소의 하한 | 기본 미설정, 사용 시 양수 warm-IP 목표와 조합 |
| `WARM_ENI_TARGET` | Warm ENI 용량 목표 | 릴리스 기본 1, IP target이 우선 |
| `WARM_PREFIX_TARGET` | 여유 IPv4 prefix 목표 | 릴리스 차트·매니페스트는 1, bare daemon 문서는 미설정 |
| `ENABLE_PREFIX_DELEGATION` | Prefix 할당 선택 | Linux 차트 기본 `"false"` |
| `AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG` | Custom networking 선택 | 기본 `"false"` |
| `ENI_CONFIG_LABEL_DEF` | ENIConfig를 선택하는 노드 레이블 키 | Daemon 기본 `k8s.amazonaws.com/eniConfig`, zone 예제는 재정의 |
| `ENABLE_POD_ENI` | EC2 Pod-ENI 연동 활성화 | 기본 `"false"`, 다른 SGPP 전제도 필요 |
| `POD_SECURITY_GROUP_ENFORCING_MODE` | SGPP 라우팅·SNAT·보안 그룹 동작 | 기본 `strict` |
| `NETWORK_POLICY_ENFORCING_MODE` | 새 Pod의 규칙을 설정하는 동안 네트워크 정책 동작 | 기본 `standard` |
두 enforcing-mode 설정은 다른 시스템을 제어합니다. EKS 설정 payload의 환경 변수 값은 문자열입니다.
### Custom Networking (ENIConfig)
의도한 VPC·AZ에 실제 Pod 서브넷·보안 그룹을 생성하고 ID를 참조합니다.
```yaml
apiVersion: crd.k8s.amazonaws.com/v1alpha1
kind: ENIConfig
metadata:
name: ap-northeast-2a
spec:
subnet: subnet-0123456789abcdef0
securityGroups:
- sg-0123456789abcdef0
---
apiVersion: crd.k8s.amazonaws.com/v1alpha1
kind: ENIConfig
metadata:
name: ap-northeast-2b
spec:
subnet: subnet-0abcdef0123456789
securityGroups:
- sg-0123456789abcdef0
```
Custom networking을 켜고 노드의 실제 zone 레이블을 사용합니다.
```json
{
"env": {
"AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG": "true",
"ENI_CONFIG_LABEL_DEF": "topology.kubernetes.io/zone"
}
}
```
명시적인 ENIConfig 노드 어노테이션이 레이블보다 우선합니다. ENIConfig 객체만으로 custom networking이 활성화되지는 않습니다. 라우팅, DNS·보안 규칙, 주소 용량, 워크로드·노드 전환을 계획합니다. 새 CIDR·ENIConfig만 생성해도 현재 Pod가 다른 서브넷으로 이동하지는 않습니다.
## IP 주소 관리
### Warm Pool 튜닝
측정한 Pod 수요·변경 빈도, 주소 공간, EC2 API 한계로 정합니다. 다음은 클러스터 크기만으로 정한 권장값이 아닌 서로 다른 목표 예제입니다.
```json
{
"env": {
"WARM_IP_TARGET": "2",
"MINIMUM_IP_TARGET": "4"
}
}
```
```json
{
"env": {
"WARM_IP_TARGET": "5",
"MINIMUM_IP_TARGET": "10"
}
}
```
`MINIMUM_IP_TARGET`은 전체 할당 하한, `WARM_IP_TARGET`은 여유 주소 목표이며 ENI/prefix warm 전략보다 우선합니다. Prefix delegation에서는 실제로 prefix 단위 할당이 이루어집니다. Warm 용량은 할당 대기를 줄일 수 있지만 주소를 사용하고, 잦은 변경은 API 호출을 늘릴 수 있습니다.
### Secondary CIDR 추가
먼저 기존 연결, 연결된 네트워크와의 중복, VPC CIDR 제한, 서브넷·라우팅 전제를 검토합니다.
```bash
VPC_ID=vpc-0123456789abcdef0
aws ec2 describe-vpcs --region "$EKS_REGION" --vpc-ids "$VPC_ID" \
--query 'Vpcs[0].CidrBlockAssociationSet'
```
다음 ID·주소 공간은 예제이며 검토한 VPC 계획의 일부로만 수행합니다.
```bash
aws ec2 associate-vpc-cidr-block --region "$EKS_REGION" \
--vpc-id "$VPC_ID" --cidr-block 100.64.0.0/16
aws ec2 describe-vpcs --region "$EKS_REGION" --vpc-ids "$VPC_ID" \
--query 'Vpcs[0].CidrBlockAssociationSet'
```
새 CIDR의 상태가 associating이 아니라 **associated**인지 확인한 뒤 그 범위에 서브넷을 생성합니다.
```bash
aws ec2 create-subnet --region "$EKS_REGION" --vpc-id "$VPC_ID" \
--cidr-block 100.64.0.0/19 --availability-zone ap-northeast-2a
```
서브넷의 라우팅 테이블·보안 규칙·CNI 선택도 필요합니다. 기존 Pod는 계획한 전환 전까지 현재 네트워킹을 유지합니다. RFC 6598 `100.64.0.0/10`은 shared address space이지 전 세계에서 고유한 사설 용량이 아니므로 연결된 모든 환경과 중복을 확인합니다.
### IPv6 클러스터 구성
IP family는 생성 시 선택하고 이후 변경할 수 없습니다. 공식 `eksctl` 인터페이스는 `--ip-family` 플래그가 아닌 **설정 파일**입니다. 스키마 예제는 다음과 같습니다.
```yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: ipv6-example
region: ap-northeast-2
version: '1.35'
kubernetesNetworkConfig:
ipFamily: IPv6
iam:
withOIDC: true
addons:
- name: vpc-cni
- name: coredns
- name: kube-proxy
managedNodeGroups:
- name: linux-nitro
amiFamily: AmazonLinux2023
instanceType: m5.large
desiredCapacity: 2
privateNetworking: true
```
예제 버전·리전을 바꾸고 지원되는 노드 이미지를 선택하며 생성 전 VPC endpoint·접근·IAM을 검토합니다. 여기서 생략한 add-on 버전은 EKS/eksctl의 지원 경로로 선택되므로, 통제된 배포에는 필요한 호환 build를 확인·고정합니다. `iam.withOIDC`와 관리형 add-on·node group은 공식 eksctl IPv6 전제를 반영합니다.
```bash
eksctl create cluster --config-file ipv6-cluster.yaml
```
이 감사에서 클러스터를 생성하지 않았습니다. 로컬 CLI help·schema 검사는 eksctl 0.229.0으로 했으며 네트워킹·IAM·리전별 build의 실제 배포 검증은 아닙니다.
IPv6에는 지원되는 Linux Nitro/Fargate 경로와 prefix 할당이 필요하고 Windows는 지원되지 않습니다. IPv6 Pod에 egress-only IPv4 보조 인터페이스가 있을 수 있습니다. 정책 에이전트 문서는 주 인터페이스의 IPv6 정책이 보조 인터페이스의 IPv4 트래픽을 보호하지 않는다고 명시합니다. 이 경로를 제거해야 한다면 `ENABLE_V4_EGRESS`, 의존성, Pod 롤아웃을 검토하고 IPv6 정책만으로 차단된다고 가정하지 않습니다.
## Network Policy 지원
### 네이티브 정책 강제
표준 네이티브 eBPF 정책은 VPC CNI 1.14에서 처음 도입되었습니다. 현재 EKS 문서의 표준·Admin 정책에는 더 새 전제가 있으며 검토한 1.23도 클러스터·플랫폼에 맞춰야 합니다.
```json
{
"enableNetworkPolicy": "true"
}
```
`"enableNetworkPolicy": "true"`는 공식 문자열 설정입니다. 지원되는 EC2 Linux 노드에서 사용할 수 있고 Fargate·Windows에는 이 강제를 적용하지 않습니다. Auto Mode는 자체 관리형 구현을 사용합니다. EKS `ClusterNetworkPolicy` Admin/Baseline 제어와 Auto Mode DNS `ApplicationNetworkPolicy`는 표준 `NetworkPolicy`의 다른 이름이 아닌 확장입니다.
Standard 모드의 새 Pod는 규칙이 해석되는 동안 처음에 트래픽을 허용합니다. 더 엄격한 시작 동작을 의도적으로 선택할 수 있습니다.
```json
{
"enableNetworkPolicy": "true",
"env": {
"NETWORK_POLICY_ENFORCING_MODE": "strict"
}
}
```
Strict 모드에서는 시작 전 DNS 등 필수 트래픽 정책을 올바르게 준비해야 합니다. SGPP의 별도 `POD_SECURITY_GROUP_ENFORCING_MODE`를 설정하는 기능은 아닙니다.
### NetworkPolicy 예제
전제는 `app` 네임스페이스의 컨트롤러 관리 frontend/backend 워크로드와 TCP 8080을 수신하는 backend Pod입니다. 현재 AWS는 안정적인 강제에 `metadata.ownerReferences`가 중요하며 Service·컨테이너 포트 번호(이름 있는 포트는 이름도)가 일치해야 한다고 문서화합니다.
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: app
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
```
출발 Pod 선택자는 같은 네임스페이스로 제한됩니다. 선택한 backend ingress를 격리하고 해당 frontend의 8080 접근을 허용하지만 backend egress를 거부하거나 애플리케이션 사용자를 인증하지는 않습니다. 다른 일치 정책과 Admin tier 동작도 고려합니다. 독립 진단 Pod 대신 Deployment/Job 관리 Pod로 허용·거부 흐름을 모두 확인합니다.
### 검증과 진단
```bash
kubectl -n kube-system logs -l k8s-app=aws-node -c aws-eks-nodeagent --tail=200
kubectl get networkpolicy -A
kubectl get policyendpoints.networking.k8s.aws -A
```
이는 **노드 에이전트** 로그이며 정책 컨트롤러는 EKS 관리 제어 평면에서 실행됩니다. PolicyEndpoint는 생성된 상태이므로 임의로 편집·삭제하지 않습니다.
접근 권한이 있고 정책 CLI가 설치된 Linux 노드에서는 다음을 사용합니다.
```bash
sudo /opt/cni/bin/aws-eks-na-cli ebpf progs
sudo /opt/cni/bin/aws-eks-na-cli ebpf maps
```
도구는 `ebpf-sdk list-maps`가 아닌 `aws-eks-na-cli`입니다. 영향받은 노드를 확인하고 프로세스 로그, 설정한 정책 이벤트 로그, CloudWatch 전달을 구분합니다. 외부 로그 전달에는 해당 IAM·설정도 필요합니다.
## 고급 기능
### Pod별 보안 그룹
해당 보안 그룹이 DNS·API·애플리케이션·반환 경로에 맞는 규칙을 가지고 이미 존재해야 합니다.
```yaml
apiVersion: vpcresources.k8s.aws/v1beta1
kind: SecurityGroupPolicy
metadata:
name: my-security-group-policy
namespace: app
spec:
podSelector:
matchLabels:
app: database
securityGroups:
groupIds:
- sg-0123456789abcdef0
- sg-0abcdef0123456789
```
Standard SGPP 동작을 선택한 **EC2** 설정 예제입니다.
```json
{
"env": {
"ENABLE_POD_ENI": "true",
"POD_SECURITY_GROUP_ENFORCING_MODE": "standard"
}
}
```
완전한 SGPP 설치가 아닙니다. Trunking 지원 인스턴스, 클러스터 역할의 VPC resource-controller 권한, CNI 권한, 서브넷 용량, EKS 전제를 확인합니다. T 계열이 Nitro라고 해서 trunking을 지원하지는 않습니다. 새로 생성·재생성한 선택 Pod에 의도한 설정이 적용됩니다.
관리형 resource controller는 **추가 trunk ENI**를 붙이고 branch ENI를 연결합니다. Trunk는 노드의 primary `eth0` ENI가 아닙니다. 선택 Pod는 보안 그룹이 지정된 branch 인터페이스를 사용하며 prefix delegation이 branch Pod 상한을 늘리지 않습니다. Fargate 보안 그룹은 별도 관리 경로를 따릅니다.
| 모드·기능 | 확인할 영향 |
|---|---|
| SGPP `strict` | Branch 보안 그룹 동작과 Pod SNAT 비활성화, NodeLocal DNSCache 및 instance 대상 LoadBalancer/NodePort의 `externalTrafficPolicy: Local` 제약 |
| SGPP `standard` | 문서화된 정책·DNS 조합 지원. 기본 external-SNAT 동작에서는 VPC 밖 트래픽이 노드 primary 주소·보안 그룹 사용 |
| Custom networking + SGPP | Pod 보안 그룹이 ENIConfig 보안 그룹보다 우선 |
| IPv6 | EKS 문서의 버전·플랫폼 조건에 따라 지원(EC2 CNI 1.16+ 포함). 오래된 README 기능 표의 “No”가 상세 안내를 무효화하지 않음 |
| Windows / Auto Mode | 이 SGPP 방식은 미지원, Auto Mode는 별도 NodeClass 네트워킹 제어 사용 |
모드 변경은 새 Pod에 적용되므로 재생성과 트래픽 경로를 검증합니다. SNAT 뒤에도 모든 패킷에 같은 보안 그룹이 적용된다고 가정하지 않습니다.
### 다중 인터페이스와 Multus
VPC CNI 1.20+에는 다중 네트워크 카드를 지원하는 인스턴스용 native multi-NIC 기능이 있습니다. `ENABLE_MULTI_NIC`와 Pod NIC 설정은 Multus와 다르며 추가 인터페이스의 이점을 얻으려면 애플리케이션이 실제로 사용해야 합니다.
Multus는 meta-plugin입니다. AWS 지원 구성은 VPC CNI를 **주 delegate**로 사용하며 VPC CNI를 추가 인터페이스의 플러그인으로 쓰는 것은 지원하지 않습니다. 추가 인터페이스에는 호환 플러그인, 주소 할당, 수명 주기 관리가 필요합니다.
| 추가 인터페이스 전제 | 이유 |
|---|---|
| 식별한 전용 인터페이스 | 고정 `eth1`이 IPAMD 관리 인터페이스일 수 있음 |
| 추가 ENI의 `node.k8s.amazonaws.com/no_manage=true` | VPC CNI가 Multus 인터페이스를 관리하지 않도록 함 |
| AWS에 할당·라우팅된 주소와 올바른 서브넷·SG·경로 | 임의의 `192.168.1.0/24` 할당이 EC2 ENI에서 자동으로 유효해지지 않음 |
| 조정된 IPAM | 공유 `host-local` 범위는 여러 노드에서 중복 주소를 할당할 수 있음 |
| 인터페이스별 정책 시험 | 추가 인터페이스·IPv4 보조 경로가 모든 주 인터페이스 정책에 자동 포함되지는 않음 |
NetworkAttachmentDefinition의 `spec.config`는 지원 버전, 플러그인, 실제 parent 인터페이스, IPAM을 포함한 CNI JSON입니다. 이전 범용 `ipvlan`/`eth1`/`host-local` 매니페스트는 위 전제를 누락하여 구현 요구사항으로 대체했습니다. 이 문서는 실제 배포한 Multus/IPAM 솔루션이라고 주장하지 않습니다.
### Windows
Windows는 VPC resource-controller IPAM을 사용합니다. 클러스터 역할 권한, Windows 노드 역할의 인증·access entry(해당 시 `EC2_WINDOWS`), CoreDNS용 Linux/Fargate 용량을 준비합니다. Windows Fargate, Auto Mode, Hybrid Nodes, IPv6, custom networking, SGPP, 네이티브 VPC-CNI 네트워크 정책에는 문서화된 제약이 있습니다.
컨트롤러의 결과 ConfigMap에는 다음 Windows IPAM 항목이 필요합니다. 필요한 데이터를 나타내며 관리 주체가 소유한 ConfigMap 전체를 덮어쓰라는 명령은 아닙니다.
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: amazon-vpc-cni
namespace: kube-system
data:
enable-windows-ipam: 'true'
```
**Helm 관리** 설치의 Windows prefix 목표는 Linux와 다른 키를 사용합니다.
```yaml
enableWindowsIpam: 'true'
enableWindowsPrefixDelegation: 'true'
warmWindowsPrefixTarget: 1
warmWindowsIPTarget: 0
minimumWindowsIPTarget: 0
```
해당 build 스키마, 필드 소유, 결과 ConfigMap을 확인합니다. Helm 값이 EKS add-on 설정 속성으로 그대로 수락된다고 가정하지 않습니다. Windows 차트 플래그는 `enable-windows-ipam`, `enable-windows-prefix-delegation`으로 변환되며 여기의 warm-target 필드도 Windows용입니다.
전제와 AMI·버전을 확인한 후 node-group 명령은 다음 형태로 사용할 수 있습니다.
```bash
eksctl create nodegroup --region "$EKS_REGION" --cluster "$CLUSTER_NAME" \
--name windows-example --managed --node-type m5.large --nodes 2 \
--node-ami-family WindowsServer2022FullContainer
```
Windows secondary-IP 모드는 일반적으로 ENI 1개와 그 주소 슬롯 상한을 사용하며 Linux의 다중 ENI 공식과 다릅니다. Prefix delegation과 실제 kubelet 상한도 별도로 계산합니다.
## 트러블슈팅
### IP 할당과 스케줄링
Pod 이벤트로 스케줄링 실패와 sandbox/CNI 주소 할당 실패를 구분합니다.
```bash
kubectl -n kube-system logs -l k8s-app=aws-node -c aws-node --tail=300
kubectl get nodes -o json | jq '.items[] | {name: .metadata.name, allocatablePods: .status.allocatable.pods}'
SUBNET_ID=subnet-0123456789abcdef0
aws ec2 describe-subnets --region "$EKS_REGION" --subnet-ids "$SUBNET_ID" \
--query 'Subnets[].{SubnetId:SubnetId,AvailableIPs:AvailableIpAddressCount}'
```
`allocatablePods`는 kubelet 스케줄링 용량이지 현재 IP 사용률이 아닙니다. 서브넷의 여유 주소 수만으로 연속 `/28` 존재를 알 수 없습니다. 대응을 정하기 전에 IPAMD 로그, 모드, warm target, ENI 한계, API 오류, 영향받은 노드를 확인합니다.
### ENI 수
```bash
INSTANCE_ID=i-0123456789abcdef0
aws ec2 describe-instances --region "$EKS_REGION" --instance-ids "$INSTANCE_ID" \
--query 'Reservations[].Instances[].{InstanceId:InstanceId,AttachedENIs:length(NetworkInterfaces)}'
aws ec2 describe-instance-types --region "$EKS_REGION" --instance-types m5.large \
--query 'InstanceTypes[].NetworkInfo.{MaxENI:MaximumNetworkInterfaces,IPv4PerENI:Ipv4AddressesPerInterface}'
```
이전 쿼리는 인터페이스 자체가 아닌 중첩된 목록 수를 세었습니다. 수정 쿼리는 인스턴스별 개수를 반환합니다. 다중 카드, unmanaged/trunk 인터페이스, 인스턴스별 한계는 추가 해석이 필요합니다.
### Introspection과 메트릭
해당 노드의 실제 `aws-node` Pod를 선택하고 forwarding 세션을 유지합니다.
```bash
kubectl -n kube-system get pods -l k8s-app=aws-node -o wide
AWS_NODE_POD=aws-node-example
kubectl -n kube-system port-forward "pod/$AWS_NODE_POD" 61678:61678 61679:61679
```
다른 로컬 터미널에서 조회합니다.
```bash
curl --fail http://127.0.0.1:61679/v1/enis
curl --fail http://127.0.0.1:61678/metrics
```
IPAMD introspection 기본값은 loopback **61679**, Prometheus 메트릭은 **61678**입니다. `/v1/enis`는 메트릭 엔드포인트가 아닙니다. Kubernetes forwarding 경로의 로컬 curl을 사용하므로 CNI 이미지 안의 curl에 의존하지 않습니다.
### 변경 전 오류 분류
| 관측 | 조치 전 확인 |
|---|---|
| `InsufficientFreeAddressesInSubnet` | 실제 여유 주소, warm 할당, 선택 서브넷, 계획한 용량 확장 |
| `InsufficientCidrBlocks` | 연속 prefix·단편화·서브넷 예약 |
| ENI/SG 한계 오류 | 해당 quota, 인스턴스·인터페이스 유형, 사용 중 객체. 무관한 SG를 제거하지 않음 |
| ENI 생성 실패 | 상세 AWS 오류, CNI 자격 증명 역할, 권한·조건, quota, API 연결 |
| Pod IP 대기 | IPAMD 상태, 컨트롤러·API 지연, 스로틀링, sandbox 이벤트, 주소 준비 |
IPAMD 재시작, 인스턴스 확대, 노드 역할 권한 추가는 범용 해결책이 아닙니다. 근거를 먼저 수집하고 실제 실패 작업의 관리 주체에 검토한 변경을 적용합니다.
## 모범 사례
예상 Pod, warm pool, 증가량, 장애·교체 중복을 고려해 서브넷을 계획합니다. `/19`나 RFC 6598 범위는 설계 예제이지 필수 조건이 아닙니다. 새 CIDR 연결, 필요한 서브넷·경로, CNI·워크로드 전환을 함께 계획합니다.
Warm 전략을 하나 선택합니다. 여유 IP와 전체 IP 목표를 사용하는 IPv4 prefix 예제입니다.
```json
{
"env": {
"ENABLE_PREFIX_DELEGATION": "true",
"WARM_IP_TARGET": "5",
"MINIMUM_IP_TARGET": "10"
}
}
```
IP target이 설정된 상태의 추가 `WARM_PREFIX_TARGET`을 독립적으로 유효한 목표라고 해석하지 않습니다. 클러스터 크기만으로 안전성을 추정하지 말고 할당 실패·실제 제약을 감시합니다.
### 메트릭과 알림
릴리스 IPAMD 코드는 `awscni_total_ip_addresses`, `awscni_assigned_ip_addresses`, counter `awscni_no_available_ip_addresses`를 제공합니다. Total/assigned gauge는 전체 VPC 서브넷이 아닌 **IPAMD 할당 pool**을 나타냅니다. 작은 warm target에서는 높은 assigned/total 비율이 정상일 수 있으며 cooldown, branch 인터페이스, IP family, kubelet 용량을 함께 봐야 합니다.
Prometheus Operator CRD와 선택자가 먼저 구성되어 있어야 합니다. Helm 관리 CNI에서는 다음 **IPv4 클러스터 예제**로 차트의 PodMonitor를 생성할 수 있습니다.
```yaml
podMonitor:
create: true
labels:
release: prometheus
interval: 30s
relabelings:
- sourceLabels:
- __meta_kubernetes_pod_node_name
targetLabel: node
- targetLabel: cluster
replacement: example-cluster
- targetLabel: ip_family
replacement: ipv4
- targetLabel: job
replacement: aws-vpc-cni
```
클러스터 레이블을 교체하고 `release` 선택자를 맞추며 IP family를 사실대로 지정합니다. 레이블이 실제 family를 탐지하지는 않습니다. 차트는 Agent의 이름 있는 `metrics` 포트와 활성화한 정책 에이전트의 `agentmetrics`를 수집합니다. EKS 관리형 add-on에는 두 번째 CNI Helm release를 설치하지 말고 독립된 scraper/PodMonitor를 구성합니다.
```yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: vpc-cni-signals
namespace: monitoring
labels:
release: prometheus
spec:
groups:
- name: vpc-cni
rules:
- record: vpc_cni:allocated_ipv4_pool_utilization:ratio
expr: (awscni_assigned_ip_addresses{job="aws-vpc-cni",ip_family="ipv4"} / awscni_total_ip_addresses{job="aws-vpc-cni",ip_family="ipv4"})
and (awscni_total_ip_addresses{job="aws-vpc-cni",ip_family="ipv4"} > 0)
- alert: CniHighAllocatedIPv4PoolUtilization
expr: vpc_cni:allocated_ipv4_pool_utilization:ratio > 0.9
for: 5m
labels:
severity: info
annotations:
summary: Most currently allocated IPAMD IPv4 addresses are assigned
description: This is allocated-pool utilization, not subnet exhaustion. Check
warm targets, assignment failures and available subnet space.
- alert: CniIPAssignmentFailures
expr: increase(awscni_no_available_ip_addresses{job="aws-vpc-cni"}[5m]) > 0
for: 1m
labels:
severity: warning
annotations:
summary: IPAMD could not assign an available IP address
- alert: CniMetricsScrapeFailed
expr: up{job="aws-vpc-cni"} == 0
for: 5m
labels:
severity: warning
annotations:
summary: A known CNI metrics endpoint cannot be scraped
```
Pool 비율은 IPv4와 관측된 양수 분모로 한정합니다. 서브넷 고갈 증거가 아닌 정보성 튜닝 신호입니다. 할당 실패 counter는 실제 실패를 나타냅니다. 수집 실패와 대상 삭제는 다르므로 예상 노드·구성 요소 목록도 비교합니다. 데이터 없음은 정상 0이 아닙니다. 실제 관측 환경에 맞게 임계값, 레이블, 알림 전달을 조정합니다.
## 참고 자료
- [VPC CNI 1.23.0 documentation](https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.23.0/README.md)
- [VPC CNI Helm values](https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.23.0/charts/aws-vpc-cni/values.yaml)
- [Chart version metadata](https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.23.0/charts/aws-vpc-cni/Chart.yaml)
- [Released CNI manifest](https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.23.0/config/master/aws-k8s-cni.yaml)
- [IPAMD implementation](https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.23.0/pkg/ipamd/ipamd.go)
- [IPAMD introspection server](https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.23.0/pkg/ipamd/introspect.go)
- [IPAM datastore](https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.23.0/pkg/ipamd/datastore/data_store.go)
- [IPAMD metric definitions](https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.23.0/utils/prometheusmetrics/prometheusmetrics.go)
- [Network policy agent 1.4.1](https://github.com/aws/aws-network-policy-agent/blob/v1.4.1/README.md)
- [AWS Helm chart index](https://aws.github.io/eks-charts/index.yaml)
- [EKS security groups for Pods](https://docs.aws.amazon.com/eks/latest/userguide/security-groups-for-pods.html)
- [SGPP operating considerations](https://docs.aws.amazon.com/eks/latest/best-practices/sgpp.html)
- [EKS Multus support boundaries](https://docs.aws.amazon.com/eks/latest/userguide/pod-multus.html)
- [EKS Windows networking](https://docs.aws.amazon.com/eks/latest/userguide/windows-support.html)
- [EKS IPv6 support](https://docs.aws.amazon.com/eks/latest/userguide/cni-ipv6.html)
- [eksctl IPv6 configuration](https://docs.aws.amazon.com/eks/latest/eksctl/vpc-ip-family.html)
- [CNI IAM configuration](https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html)
- [EKS VPC CNI management](https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html)
- [EKS network policy conditions](https://docs.aws.amazon.com/eks/latest/userguide/cni-network-policy.html)
- [Enable EKS network policy](https://docs.aws.amazon.com/eks/latest/userguide/cni-network-policy-configure.html)
- [Prefix allocation and Pod-capacity limits](https://docs.aws.amazon.com/eks/latest/userguide/cni-increase-ip-addresses-procedure.html)
## 퀴즈
[VPC CNI 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/networking/01-vpc-cni-quiz)로 이해를 확인합니다.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/networking/cilium/
----------------------------------------
# Cilium 딥다이브: 클라우드 네이티브 네트워킹의 미래
## 개요와 검토 기준
Cilium 네트워킹, 정책, 관측성을 다룹니다. 예제 검토 기준은 **Cilium/Helm 차트 1.20.1**, Cilium CLI **0.20.0**, Hubble CLI **1.19.4**입니다. Cilium 1.20 호환성 문서의 Kubernetes 테스트 범위는 **1.33–1.36**이며 업스트림 1.37 출시만으로 자동 확대되지 않습니다. 호스트는 AMD64/AArch64 Linux, 커널 **5.10 이상** 또는 문서화된 배포판 동등 조건(예: RHEL 8.10의 backport된 4.18)을 충족해야 합니다. 개별 기능에는 추가 조건이 있습니다.
> **마지막 업데이트**: 2026년 9월 12일
### 릴리스 이력
아래 날짜는 GitHub 공개 시각의 UTC 날짜로 해당 릴리스 기록이며 현재 설치 버전 지정이 아닙니다. 릴리스 라인마다 백포트 내용이 다릅니다.
| 날짜 | 릴리스 | 확인한 주요 내용 |
| --- | --- | --- |
| 2026-07-14 | [1.20.0-rc.0](https://github.com/cilium/cilium/releases/tag/v1.20.0-rc.0) | 1.20 첫 release candidate |
| 2026-07-16 | [1.19.6](https://github.com/cilium/cilium/releases/tag/v1.19.6), [1.18.12](https://github.com/cilium/cilium/releases/tag/v1.18.12), [1.17.18](https://github.com/cilium/cilium/releases/tag/v1.17.18) | Gateway access-log 설정은 1.19.6/1.18.12에, 여기서 다루는 restart-policy·ClusterMesh affinity 수정은 1.19.6에 명시되어 있으며 세 릴리스 모두의 변경이 아님 |
| 2026-07-21 | [1.20.0-rc.1](https://github.com/cilium/cilium/releases/tag/v1.20.0-rc.1) | 1.20 두 번째 release candidate |
| 2026-07-29 | [1.20.0](https://github.com/cilium/cilium/releases/tag/v1.20.0) | GA 릴리스, 주요 변경은 아래 설명 |
| 2026-08-03 | [1.21.0-pre.0](https://github.com/cilium/cilium/releases/tag/v1.21.0-pre.0) | 다음 사이클 prerelease, 이 가이드의 배포 기준이 아님 |
| 2026-08-18 | [1.20.1](https://github.com/cilium/cilium/releases/tag/v1.20.1) | ClusterMesh 문서와 restart/CIDR-policy 처리 등 버그 수정 |
| 2026-08-18 | [1.19.7](https://github.com/cilium/cilium/releases/tag/v1.19.7) | ENI 인터페이스 타이밍, Service/LB 등 수정 |
| 2026-08-18 | [1.18.13](https://github.com/cilium/cilium/releases/tag/v1.18.13) | VRRP/IGMP host-firewall 지원 및 관련 수정 |
1.20.0 발표는 **2,660개 이상의 새 커밋**과 **1,100명 이상 기여자의 커뮤니티**를 소개합니다. 후자는 해당 릴리스 작성자 수가 아닌 커뮤니티 규모입니다. 주요 변경은 다음과 같습니다.
- Gateway API **1.6.1**, TCPRoute/UDPRoute, BackendTLSPolicy, ListenerSets, ExternalAuth, CORS 지원이며 각각의 설정과 API 성숙도를 확인해야 합니다.
- Datapath plugin과 명시적으로 선택하는 `bpf.datapathMode=auto`, 발표된 기본값은 여전히 veth입니다. Dual-stack 클러스터에는 IPv6 egress gateway 주소를 지정할 수 있습니다.
- **Beta** IPv6 ENI IPAM 및 클러스터를 재구축하지 않는 cluster-pool→multi-pool 전환입니다. In-place가 중단 없음을 보장하지는 않습니다.
- 트래픽 분배 힌트, 가중치 Maglev backend, stable MCS 통합, Kubernetes ClusterNetworkPolicy, **beta** ztunnel 워크로드 identity 지원입니다.
- 발표된 `cilium-cni` 바이너리 크기 감소는 약 **77MB→16MB**입니다. ADS/Delta xDS 개선은 1.20 발표 내용이며 1.18.13 패치 내용으로 귀속하면 안 됩니다. 이 감사에서 다시 측정한 결과가 아닌 업스트림 릴리스의 설명입니다.
레거시 Mutual Authentication, Envoy Go 확장, Kafka 인지 정책, 이전 CiliumNodeConfig API, libnetwork, 사용자 정의 CNI 설정의 제거/변경은 [1.20 업그레이드 안내](https://docs.cilium.io/en/v1.20/operations/upgrade/#upgrade-notes)를 확인하세요.
### NetworkPolicy 보안 권고
[GHSA-fm8w-2m5w-9j7r / CVE-2026-56743](https://github.com/cilium/cilium/security/advisories/GHSA-fm8w-2m5w-9j7r)의 프로젝트 권고문 공개일은 **2026년 7월 6일**입니다. 프로젝트 API와 GitHub 통합 권고 API의 날짜는 다르며 통합 기록은 9월 3일입니다. 이 릴리스 연혁에는 프로젝트 공개일을 사용하며 통합 기록 날짜를 새로운 수정 릴리스 날짜로 취급하지 않습니다. 권고문이 설명하는 custom cluster-name 조건에서 **1.19.0–1.19.4**가 영향을 받으며, 표준 Kubernetes NetworkPolicy의 `ipBlock`만 있는 peer 규칙이 선택한 Pod와 같은 네임스페이스의 워크로드 ingress를 의도하지 않게 허용할 수 있습니다. 해당 문제는 **1.19.5**에서 수정되었고 실제 배포에는 적절한 최신 패치 버전을 선택해야 합니다. 권고문은 CiliumNetworkPolicy/ClusterwideNetworkPolicy와 1.19.0 이전 릴리스는 이 특정 문제의 영향을 받지 않는다고 설명합니다.
## 소개
Cilium은 지원되는 Linux Kubernetes 환경의 네트워킹, 보안, 관측성을 제공합니다. 라우팅, IPAM, 암호화, Service 처리는 별도 선택이며 eBPF 선택만으로 모든 기능이나 성능이 보장되지는 않습니다. 이전 Docker libnetwork 통합은 1.20에서 제거되었으므로 Docker/Mesos를 현재 동일한 설치 대상으로 나열하면 안 됩니다.
### eBPF와 주요 기능
커널은 eBPF 프로그램을 로드하기 전에 검증하고 지원 hook에서 실행하도록 JIT 컴파일할 수 있습니다. 별도 커널 모듈 없이 패킷 처리와 관측성을 구현할 수 있지만 verifier가 애플리케이션이나 정책의 정확성을 증명하지는 않습니다. 실제 처리량, 지연, 메모리는 프로그램·플랫폼·워크로드에 따라 달라집니다.
Cilium은 L3/L4 정책, Envoy/DNS proxy 통합의 L7 정책, 선택적 WireGuard/IPsec, Service 부하 분산, Hubble flow 가시성, ClusterMesh, BGP 광고를 제공합니다. XDP 가속은 장치·설정에 따른 선택 기능입니다. L7은 노드별 Envoy를 사용할 수 있고 ztunnel 워크로드 identity에는 별도 beta 설정이 필요합니다. 에이전트 설치만으로 모든 mesh·암호화·멀티클러스터 동작이 활성화되지는 않습니다.
### 다른 네트워킹 프로젝트와 비교
| 프로젝트 | 연결 / IPAM | 정책과 관련 기능 |
| --- | --- | --- |
| Cilium | Native/overlay 라우팅, ENI 등 IPAM 모드 | eBPF, Cilium/Kubernetes 정책, L7 통합, Hubble, 선택적 암호화 |
| Calico | Calico 또는 외부 IPAM과 native/IPIP/VXLAN 구성 | Linux Iptables/Nftables/BPF, 지원 Windows HNS, OSS WireGuard·staged policy·별도 L7 통합 |
| Flannel | VXLAN/host-gw/WireGuard 등 선택한 backend의 Pod 연결 | 라우팅 데몬 자체는 NetworkPolicy를 강제하지 않지만 차트의 netpol.enabled로 SIGs 정책 컨트롤러를 함께 배포하거나 다른 정책 구현과 결합 가능 |
| AWS VPC CNI | VPC ENI 주소 할당/네트워킹 | 지원 EC2 Linux의 네이티브 정책과 별도 SG-for-Pods, EKS Auto Mode는 다른 관리형 구현 |
라우팅 모드와 패킷 처리 구현은 같은 분류가 아닙니다. Calico는 iptables/IPVS로 제한되지 않고 Flannel에는 암호화 backend가 있으며 AWS 정책과 Security Group도 동일하지 않습니다. 서비스 메시는 선택 계층이고 클러스터 간 VPC 연결이 Transit Gateway로만 가능한 것도 아닙니다. 보편적인 성능 등급 대신 실제 워크로드와 지원 매트릭스를 비교하세요.
## 아키텍처
**Kubernetes API 서버**가 Kubernetes/Cilium 리소스를 저장합니다. Cilium Agent는 관련 상태를 감시해 노드 데이터플레인을 설정하고 Operator는 선택한 IPAM, identity/controller 작업 등 클러스터 수준 책임을 담당합니다. 기본 구조에 별도의 필수 클러스터 전체 “Cilium API Server” Deployment는 없습니다. Agent 로컬 API와 선택적 ClusterMesh API server는 서로 다른 목적입니다.
| 컴포넌트 | 역할 |
| --- | --- |
| Cilium Agent | 노드 endpoint, 정책, routing/Service 상태, eBPF 관리 |
| Cilium Operator | 클러스터 수준 조정과 모드별 할당/controller 작업 |
| Envoy | 활성화한 L7 정책, Ingress/Gateway 등 userspace 프록시 |
| Hubble server | Agent와 통합된 노드 로컬 flow API |
| Hubble Relay / UI | Flow stream 집계 / 서비스 맵과 flow 표시 |
| Prometheus metrics endpoint | 별도 통계 수집, Relay/UI는 metrics scrape 파이프라인이 아님 |
| cilium / cilium-dbg / hubble | 클러스터 관리 CLI / Agent 진단 / Flow 클라이언트 |
### 네트워킹과 패킷 경로
Native routing에는 도달 가능한 underlay가 필요하고 tunneling은 VXLAN 또는 Geneve를 사용합니다. AWS ENI와 Azure IPAM은 플랫폼 전제가 있는 할당/통합 선택입니다. Cilium BGP Control Plane은 라우터에 경로를 광고하며 **datapath를 설정하거나 클러스터 내부 라우팅을 제공하지 않습니다**.
모든 패킷이 XDP→TC→Pod로 이동하는 것은 아닙니다. Socket load balancing은 패킷 생성 전에 작동할 수 있고 TC/netkit hook은 datapath에 따라 다르며 선택적 XDP는 일부 트래픽을 가속하고 L7은 Envoy를 거칠 수 있습니다. 반환 경로도 NAT, conntrack, DSR 선택에 따라 달라집니다. 해당 구성의 네트워킹/eBPF 장을 참고하세요.
## Amazon EKS와의 통합
설치 전에 실제 네트워킹과 컴퓨팅 구성을 선택하세요. 기존 예제의 `cilium` / `v1.17.0-eksbuild.1` 애드온 이름·버전은 검증된 AWS 배포판이 아니므로 설치 명령에서 제거했습니다. Vendor 애드온을 고려한다면 리전의 실제 카탈로그, 게시자, 라이선스, 지원 컴퓨팅 유형을 확인해야 합니다.
| EKS 구성 | 확인할 사항 |
| --- | --- |
| 일반 EC2, Cilium ENI로 VPC CNI 교체 | 업스트림/파트너 관리 CNI이며 AWS가 지원하는 EC2 CNI는 VPC CNI, CNI 소유권·IAM·주소·경로·bootstrap·노드 전환 계획 필요 |
| 일반 EC2, AWS VPC CNI chaining | VPC CNI가 인터페이스/IPAM을 소유하고 이후 Cilium이 연결, 기존 Pod 재생성과 L7/IPsec 제약 확인 |
| Hybrid Nodes | AWS 전용 CNI 가이드와 AWS 유지 Cilium 빌드 매트릭스 사용, 업스트림 1.20.1이 자동으로 AWS 지원 빌드가 아님 |
| Auto Mode | 대체 CNI/정책 플러그인 미지원, 관리형 NodeClass/네트워킹 사용 |
| Fargate | 대체 CNI/DaemonSet 설치 미지원 |
| Windows | Cilium Agent 요구사항은 Linux, 이 레시피를 Windows 워커에 적용하지 않음 |
AWS 일반 대체 CNI 문서와 Hybrid 전용 가이드의 Calico 지원 표현은 다르며 예제 저장소 이동만으로 지원 종료가 입증되지는 않습니다. Hybrid는 정확한 배포판·기능·지원 주체를 확인하세요. Auto Mode의 node-local CoreDNS/시스템 네트워킹도 이 문서의 일반 EC2 구성과 다릅니다. Non-Auto 노드가 섞이면 기존 DNS Deployment가 여전히 필요합니다.
### 준비된 EC2 클러스터의 Cilium ENI
다음은 **준비된 IPv4 EC2 클러스터용 Cilium Helm values**이며 전체 클러스터 생성이나 in-place 전환 레시피가 아닙니다. 사용 전에 다음을 준비하세요.
1. 지원 EKS/Kubernetes 버전과 Linux AMI, 하나의 CNI 소유자를 선택합니다. 기존 워크로드 클러스터에서 `aws-node`를 삭제하는 단축 방법을 사용하지 않습니다.
2. Cilium이 노드를 관리하기 전까지 워크로드가 기다리도록 taint/스케줄링을 준비합니다. 업스트림 EKS 가이드는 `node.cilium.io/agent-not-ready=true:NoExecute`를 사용하므로 실제 노드 수명주기의 eviction/bootstrap 영향을 검토해야 합니다.
3. 서브넷 용량, ENI quota/Security Group, 노드 metadata 접근, operator의 EC2 권한을 준비합니다. 아래 ARN은 **cilium-operator ServiceAccount**를 올바르게 신뢰하는 역할의 자리표시자이며 values 파일이 역할을 생성하지는 않습니다.
4. 이 `kubeProxyReplacement: false` 예제는 작동하는 kube-proxy와 DNS를 유지합니다. 교체 모드는 별도 API 직접 접근/bootstrap DNS 조건을 따르세요. max-Pods는 보편적인 110이 아닌 실제 인스턴스/IPAM 용량으로 결정합니다.
`cilium-eni-values.yaml`로 저장하고 역할·인터페이스를 검토한 값으로 변경하세요.
```yaml
eni:
enabled: true
ipam:
mode: eni
routingMode: native
kubeProxyReplacement: false
ipv4:
enabled: true
ipv6:
enabled: false
egressMasqueradeInterfaces: eth0
serviceAccounts:
operator:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/CiliumOperatorENI
```
```bash
helm repo add cilium https://helm.cilium.io/
helm repo update cilium
helm template cilium cilium/cilium --version 1.20.1 \
--namespace kube-system -f cilium-eni-values.yaml > cilium-eni-rendered.yaml
# 클러스터 준비와 렌더링 결과 검토 후:
helm install cilium cilium/cilium --version 1.20.1 \
--namespace kube-system -f cilium-eni-values.yaml
```
설치 소유 경로에서 설정을 관리하세요. 몇 개 키만 있는 `cilium-config`로 교체하면 다른 필수 설정을 잃을 수 있습니다. 이전 `tunnel=disabled` 대신 `routingMode: native`를 사용합니다. ENI 할당/권한과 SNAT 동작은 실제 환경에서 검증해야 하며 렌더링 성공만으로 EC2 네트워킹이 입증되지는 않습니다.
**IPv6 조건:** 1.20.1 ENI IPAM 참조는 IPv6를 beta로 설명하지만 같은 버전 EKS 전제 페이지에는 여전히 IPv4-only ENI 제한이 있습니다. 이 가이드는 IPv4 예제를 유지하고 문서 불일치를 기록하며 어느 한 문장만으로 프로덕션 EKS IPv6 호환성이 증명되었다고 판단하지 않습니다. 별도 IPv6 설계에는 현재 ENI/dual-stack 서브넷 조건을 확인하세요.
### 대안: VPC CNI Chaining
업스트림 chaining 가이드는 VPC CNI 1.11.2 이상과 다음 구성을 문서화합니다.
```yaml
cni:
chainingMode: aws-cni
exclusive: false
enableIPv4Masquerade: false
routingMode: native
kubeProxyReplacement: false
```
ENI 교체 values 위에 추가하는 설정이 아닌 **별도 구성**으로 사용하세요. VPC CNI가 할당자로 남습니다. 과거 업스트림 DaemonSet을 적용하는 대신 실제 관리 애드온을 소유 경로에서 업데이트합니다. 같은 endpoint의 경쟁 정책 엔진을 피하세요. CNI chain이 바뀌어도 기존 Pod가 자동으로 Cilium에 연결되지는 않습니다. 계획한 rollout으로 재생성하고 endpoint 관리를 확인해야 합니다. Chaining에는 L7 정책/IPsec 제약이 있으므로 아래 모든 예제가 해당 구성에서도 작동한다고 가정하지 마세요.
### ClusterMesh
고유한 cluster identity, 호환 버전, 접근 가능하고 중복되지 않는 Pod 네트워크, 인증된 API 연결, 적절한 노출 방식이 필요합니다. LoadBalancer Service는 클라우드 리소스를 만들 수 있어 의도적인 네트워크/보안 설계가 필요합니다. 공개 endpoint 두 개만 만든다고 안전한 클러스터 연결이 완성되지는 않습니다. 선택한 토폴로지의 [ClusterMesh 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/cilium-service-mesh/01-architecture.md)와 고급 장을 따르세요.
## 설치 및 구성
### 클라이언트 도구
워크스테이션 OS/아키텍처에 맞는 공식 Cilium CLI 0.20.0, Hubble CLI 1.19.4 자산을 사용하고 압축 해제 전에 제공된 체크섬을 검증하세요. Linux ARM64/AMD64는 다르며 macOS는 해당 Darwin 자산을 사용합니다. CLI 버전은 Cilium Agent/chart 버전과 별개입니다. [검증된 CLI 설치 안내](https://www.atomai.click/kubernetes-docs/llms/ko/service-mesh/cilium-service-mesh/README.md)를 참고하세요.
```bash
cilium version --client
hubble version
```
### 일반 Cluster-Pool 예제
다음은 kube-proxy와 DNS가 작동하는 일반 Linux 클러스터의 **별도 대안**입니다. 예시 Pod 범위 `10.244.0.0/16`이 클러스터와 맞고 Service, 노드, VPC, 연결 네트워크와 중복되지 않아야 합니다. ENI 모드에 이 pool 설정을 사용하지 마세요.
```yaml
routingMode: tunnel
tunnelProtocol: vxlan
kubeProxyReplacement: false
ipv4:
enabled: true
ipv6:
enabled: false
ipam:
mode: cluster-pool
operator:
clusterPoolIPv4PodCIDRList:
- 10.244.0.0/16
clusterPoolIPv4MaskSize: 24
hubble:
enabled: true
relay:
enabled: true
ui:
enabled: true
metrics:
enabled:
- dns
- drop
- tcp
- flow
- icmp
- httpV2
```
`cilium-values.yaml`로 저장하고 고정 버전 차트를 렌더링한 후 준비된 클러스터에만 설치합니다.
```bash
helm template cilium cilium/cilium --version 1.20.1 \
--namespace kube-system -f cilium-values.yaml > cilium-rendered.yaml
helm install cilium cilium/cilium --version 1.20.1 \
--namespace kube-system -f cilium-values.yaml
cilium status --wait
```
기존 release는 업그레이드/GitOps 경로에서 소유 values를 보존하며 버전별 절차를 따릅니다. `cilium install`을 반복하는 것은 개별 설정 변경의 일반적인 방법이 아닙니다.
| 선택 | 현재 설정과 전제 |
| --- | --- |
| VXLAN/Geneve | `routingMode: tunnel`과 `tunnelProtocol`, 해당 캡슐화 허용과 경로 MTU 설정 |
| Native routing | `routingMode: native`, underlay의 Pod 주소 라우팅 필요, `autoDirectNodeRoutes`는 임의의 다중 서브넷이 아닌 적합한 직접 연결 전제 |
| kube-proxy 교체 | 이전 `strict` 대신 `kubeProxyReplacement: true`/`false`, 교체 시 도달 가능한 `k8sServiceHost`/`k8sServicePort`와 bootstrap 계획 필요 |
| WireGuard | 커널/플랫폼과 peer 경로 확인 후 지원 모드 활성화, 모든 트래픽 자동 암호화가 아님 |
| IPsec | 문서화된 key Secret, 키 배포/교체, 호환 모드 필요, Helm enable 플래그만으로 불충분 |
| XDP/DSR/BBR | 장치/커널/토폴로지별 별도 선택, 보편적인 설치 프리셋이 아님 |
## 네트워크 정책
Kubernetes `networking.k8s.io/v1` NetworkPolicy와 Cilium `cilium.io/v2` 정책은 별도 API입니다. 여러 allow 정책이 합쳐질 수 있습니다. 다음은 L4 허용이 L7 제한을 우회하지 않도록 **각기 다른 준비된 테스트 네임스페이스**를 사용합니다. 실제 endpoint를 선택하는 모든 정책을 검토하세요.
### L4 예제
`cilium-l4-demo`에서 backend Pod를 선택하고 같은 네임스페이스 frontend Pod의 TCP 8080 ingress를 허용합니다.
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: cilium-l4-demo
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- port: 8080
protocol: TCP
```
### HTTP 예제
별도 `cilium-l7-demo`에서 backend Pod를 선택하고 해당 네임스페이스 frontend의 TCP 8080 평문 HTTP를 지정한 method/path로 제한합니다. 선택한 CNI 모드에서 L7 proxy를 지원해야 하며 암호화된 HTTP가 지원 termination 설정 없이 자동 검사되지는 않습니다.
```yaml
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-product-read
namespace: cilium-l7-demo
spec:
endpointSelector:
matchLabels:
k8s:app: backend
ingress:
- fromEndpoints:
- matchLabels:
k8s:app: frontend
k8s:io.kubernetes.pod.namespace: cilium-l7-demo
toPorts:
- ports:
- port: '8080'
protocol: TCP
rules:
http:
- method: GET
path: ^/api/v1/products$
```
같은 peer/port에 제한 없는 L4 allow를 추가하지 마세요. Cilium 문서는 이 경우 더 좁은 L7 제한이 효력을 잃는다고 설명합니다. L7 거부는 패킷 drop 대신 HTTP 403을 반환할 수 있습니다. 실제 endpoint identity로 허용 GET과 거부 method/path를 테스트하세요.
### DNS/FQDN 예제
`cilium-dns-demo`에서 일반 CoreDNS Pod로의 DNS 질의와 `api.example.com`에서 학습한 주소의 TCP 443을 허용합니다. 도메인은 예시이므로 승인한 대상으로 바꾸세요. 광범위한 `*.amazonaws.com`은 계정/리소스 경계가 아니므로 제외했습니다.
```yaml
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-api-domain
namespace: cilium-dns-demo
spec:
endpointSelector:
matchLabels:
k8s:app: web
egress:
- toEndpoints:
- matchLabels:
k8s:k8s-app: kube-dns
k8s:io.kubernetes.pod.namespace: kube-system
toPorts:
- ports:
- port: '53'
protocol: ANY
rules:
dns:
- matchPattern: '*'
- toFQDNs:
- matchName: api.example.com
toPorts:
- ports:
- port: '443'
protocol: TCP
```
DNS wildcard는 선택한 resolver로 질의를 허용할 뿐 반환된 모든 주소로의 연결을 허용하지는 않습니다. 임의 DNS 이름을 전달할 수 있으므로 필요하면 search suffix까지 고려해 질의 이름을 제한하세요. NodeLocal DNS나 다른 resolver에는 알맞은 목적지 선택이 필요합니다. FQDN 정책은 DNS로 얻은 IP 인가이며 TLS hostname 검증이나 HTTP URL 인가가 아닙니다. 공유 IP와 애플리케이션 TLS/인증도 고려해야 합니다.
## Hubble 관측성
위 cluster-pool values는 Relay/UI와 메트릭을 활성화합니다. 기존 설치는 소유 경로에서 의도한 Hubble values를 적용하세요. `cilium hubble enable --ui`는 지원 편의 명령이지만 `cilium hubble enable --metrics=...`는 CLI 0.20.0의 지원 플래그가 아닙니다. Helm의 `hubble.metrics.enabled`를 설정하고 이전 `http`와 `httpV2` handler를 동시에 켜지 마세요.
한 터미널에서 Relay port-forward를 유지합니다.
```bash
cilium hubble port-forward --port-forward 4245
```
Hubble CLI가 있는 다른 터미널에서 실행합니다.
```bash
hubble observe --server 127.0.0.1:4245 --namespace cilium-l7-demo
hubble observe --server 127.0.0.1:4245 --protocol http
hubble observe --server 127.0.0.1:4245 --from-label k8s:app=frontend --to-label k8s:app=backend
hubble observe --server 127.0.0.1:4245 --verdict DROPPED
hubble observe --server 127.0.0.1:4245 --http-status 403
```
로컬 예제는 Relay 서버 기본 구성을 전제로 하며 TLS를 켠 Relay에는 맞는 client trust/인증이 필요합니다. HTTP event는 해당 트래픽이 설정한 L7 proxy를 거쳐야 나옵니다. `DROPPED`는 datapath verdict이며 모든 애플리케이션 실패를 뜻하지 않습니다. UI port-forward는 `cilium hubble ui`를 사용하고 Prometheus target discovery는 별도로 설정합니다. Hubble flow streaming 자체가 분산 애플리케이션 tracing은 아닙니다.
## 테스트와 운영
Connectivity/performance 명령은 테스트 워크로드를 만들며 정책을 바꾸거나 상당한 트래픽을 생성할 수 있습니다. 검토한 테스트 네임스페이스/환경과 권한을 사용해야 합니다. 이 감사에서는 실제 클러스터에 실행하지 않았습니다.
```bash
cilium connectivity test --help
cilium connectivity perf --help
```
성능 runner는 `cilium connectivity perf`입니다. `connectivity test --test=performance`는 테스트 이름 filter일 뿐 성능 runner가 아닙니다. 처리량/지연 비교 전에 소프트웨어 버전, 토폴로지, 트래픽, 원본 결과를 기록하세요.
조회 시 관리 CLI와 **에이전트 내부 `cilium-dbg`**를 구분합니다.
```bash
cilium status --verbose
kubectl get cnp,ccnp -A
kubectl get pods -n kube-system -l k8s-app=cilium -o wide
# 문제가 있는 노드의 Agent Pod를 선택합니다.
CILIUM_POD=replace-with-actual-cilium-pod
kubectl exec -n kube-system "$CILIUM_POD" -c cilium-agent -- cilium-dbg endpoint list
kubectl exec -n kube-system "$CILIUM_POD" -c cilium-agent -- cilium-dbg map list
kubectl exec -n kube-system "$CILIUM_POD" -c cilium-agent -- cilium-dbg metrics list
kubectl logs -n kube-system "$CILIUM_POD" -c cilium-agent --since=15m --tail=200 --timestamps
```
관리 CLI의 `cilium endpoint list`, `cilium bpf maps list`, `cilium metrics list`는 같은 명령이 아닙니다. `cilium sysdump`로 진단 자료를 모을 수 있지만 인프라/로그 자료를 보호해야 합니다. Agent Ready나 scrape 성공은 애플리케이션과 거부 정책 테스트를 대신하지 않습니다.
### 운영 우선순위
- Map preallocation, XDP, DSR, BBR, 고정 device 패턴을 켜기 전에 측정하세요. 자원을 사용하거나 패킷 경로를 바꾸므로 기능별 검증이 필요합니다.
- 선택한 범위에서 DNS, API, identity, 애플리케이션 의존성을 명시적으로 허용한 뒤 default-deny를 도입합니다. 기존 연결뿐 아니라 신규 Pod와 업그레이드 전환도 확인하세요.
- 암호화, 인증서/키 교체, 정책 적용, 관측성을 별도 인수 검사로 다룹니다. 사용할 수 있는 관리/복구 경로를 보존하세요.
- 현재 플랫폼 매트릭스와 지원 업그레이드 경로를 확인합니다. 과거 릴리스 발표로 현재 배포 호환성이 증명되지는 않습니다.
## 딥다이브 목차
**[Cilium 소개 및 기본 개념](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/01-introduction.md)**
- Cilium 개요 및 역사
- 컨테이너 네트워킹 기초
- CNI(Container Network Interface) 이해하기
- Cilium의 차별화 포인트
**[eBPF 기술 심층 분석](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/02-ebpf.md)**
- eBPF 기술 소개 및 역사
- 커널 내 eBPF 작동 방식
- eBPF 프로그램 유형 및 맵
- Cilium에서의 eBPF 활용
**[네트워킹 모델 및 VXLAN](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/03-networking.md)**
- 컨테이너 네트워킹 모델 비교
- VXLAN 기술 심층 분석
- Cilium의 오버레이 네트워킹
- 성능 최적화 기법
- 라우팅 메커니즘 (Encapsulation vs Native-Routing)
- 클라우드 제공업체별 네트워킹 (AWS ENI, Google Cloud)
**[IPAM 및 네트워크 정책](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/04-ipam-policy.md)**
- IP 주소 관리(IPAM) 전략
- Kubernetes와 Cilium IPAM 통합
- 네트워크 정책 설계 및 구현
- 멀티 클러스터 시나리오
- IPAM 모드 심층 분석 (Cluster Scope, Kubernetes Host Scope, Multi-Pool)
- 클라우드 제공업체별 IPAM (Azure IPAM, AWS ENI, GKE)
- CRD 기반 IPAM
**[L2-L7 네트워킹 및 로드 밸런싱](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/05-l2-l7-networking.md)**
- OSI 모델 계층 이해 (L2, L3, L4, L7)
- Cilium의 계층별 기능
- 서비스 메시 통합
- 로드 밸런싱 아키텍처
- 마스커레이딩 구성 및 구현 모드
- IPv4 프래그먼트 처리
**[보안 및 가시성](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/06-security-visibility.md)**
- Cilium의 보안 기능
- 네트워크 가시성 및 모니터링
- Hubble 아키텍처 및 활용
- 실시간 위협 탐지
**[고급 주제 및 실제 사례](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/07-advanced-topics.md)**
- 성능 튜닝 및 문제 해결
- 대규모 배포 전략
- 실제 사용 사례 연구
- 미래 로드맵 및 발전 방향
## 추가 자료
- [네트워킹 개념 심층 분석](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/networking-concepts.md)
- [용어 및 약어](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/glossary.md)
## 참고 자료
- [Cilium 1.20 Kubernetes 호환성](https://github.com/cilium/cilium/blob/v1.20.1/Documentation/network/kubernetes/compatibility.rst)
- [Cilium 시스템 요구사항](https://github.com/cilium/cilium/blob/v1.20.1/Documentation/operations/system_requirements.rst)
- [EKS 전제조건](https://github.com/cilium/cilium/blob/v1.20.1/Documentation/installation/requirements-eks.rst)
- [Cilium ENI IPAM](https://github.com/cilium/cilium/blob/v1.20.1/Documentation/network/concepts/ipam/eni.rst)
- [AWS 대체 CNI 지원](https://docs.aws.amazon.com/eks/latest/userguide/alternate-cni-plugins.html)
- [AWS Hybrid Nodes CNI](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-cni.html)
- [Cilium L7 정책 의미](https://github.com/cilium/cilium/blob/v1.20.1/Documentation/security/policy/layer7.rst)
- [Hubble 프로젝트](https://github.com/cilium/hubble)
- [Flannel 네트워킹/정책 통합](https://github.com/flannel-io/flannel)
- [Calico 비교 용어](https://www.atomai.click/kubernetes-docs/llms/ko/networking/calico/glossary.md)
## 퀴즈
이 섹션에서 배운 내용을 테스트하려면 [Cilium 딥다이브 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/networking/cilium/01-introduction-quiz)를 풀어보세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/networking/cilium/01-introduction
----------------------------------------
# Part 1: 소개
> **검토 기준**: Cilium 1.20.1 / Cilium CLI 0.20.0 / Hubble CLI 1.19.4. **마지막 업데이트**: 2026년 9월 12일
## 실습 환경 설정
격리된 준비 환경을 사용하세요. Cilium 1.20의 Kubernetes 테스트 범위는 **1.33–1.36**입니다. 노드는 AMD64/AArch64 Linux와 커널 **5.10 이상**, 또는 문서화된 동등 조건(예: RHEL 8.10의 backport된 4.18)을 충족해야 합니다. Kind/minikube는 노드/VM 커널을 사용하므로 워크스테이션 OS 이름만으로 호환성이 확인되지는 않습니다. 기능별 추가 조건도 확인하세요.
Kubectl은 API 서버와 한 마이너 버전 이내여야 합니다. [메인 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/README.md)에 따라 OS/아키텍처에 맞는 Cilium/Hubble CLI 자산과 체크섬을 검증하세요. Helm으로 설정을 렌더링·검토할 수 있으며 이 감사는 Helm 3.21.3을 사용했습니다. 검증하지 않은 `latest` AMD64 다운로드나 장마다 같은 release 재설치를 반복하지 마세요.
### 선택한 구성으로 한 번 설치
EKS ENI, VPC CNI chaining, 일반 cluster-pool은 전제조건이 다릅니다. [메인 가이드](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/README.md)를 참고하세요. 다음은 kube-proxy/DNS가 작동하고 하나의 CNI 소유자를 준비한 **일반 IPv4 cluster-pool 대안**입니다. EKS 마이그레이션 레시피가 아닙니다. Pod CIDR이 Service, 노드, 연결 네트워크와 겹치지 않는지 확인하세요.
`cilium-lab-values.yaml`로 저장합니다.
```yaml
routingMode: tunnel
tunnelProtocol: vxlan
kubeProxyReplacement: false
ipv4:
enabled: true
ipv6:
enabled: false
ipam:
mode: cluster-pool
operator:
clusterPoolIPv4PodCIDRList:
- 10.244.0.0/16
clusterPoolIPv4MaskSize: 24
hubble:
enabled: true
relay:
enabled: true
ui:
enabled: true
metrics:
enabled:
- dns
- drop
- tcp
- flow
- icmp
- httpV2
```
```bash
CILIUM_LAB_CONTEXT=replace-with-nonproduction-context
kubectl --context "$CILIUM_LAB_CONTEXT" get nodes -o wide
cilium version --client
# 클러스터/CNI 소유권과 values를 준비한 새 설치에만 적용합니다.
cilium install --context "$CILIUM_LAB_CONTEXT" --version 1.20.1 \
--values cilium-lab-values.yaml
cilium status --context "$CILIUM_LAB_CONTEXT" --wait
```
이미 Cilium이 있다면 버전/설정을 확인하고 소유자의 업그레이드 절차를 따르세요. 설치/status 명령이나 이후 connectivity test가 프로덕션 호환성을 증명하지는 않습니다. 이 감사에서 클러스터를 프로비저닝하지 않았습니다.
## Cilium이란?
Cilium은 Linux eBPF 데이터플레인과 Kubernetes 통합으로 네트워킹, 보안, 관측성을 제공합니다. Endpoint 정책, 모드별 IPAM/라우팅, Service 처리, Hubble flow 가시성이 포함됩니다. Docker libnetwork 통합은 1.20에서 제거되었으므로 Kubernetes, Docker, Mesos를 현재 동일한 설치 대상으로 제시하면 안 됩니다.
### 핵심 컴포넌트와 기능
| 컴포넌트 / 기능 | 역할과 조건 |
| --- | --- |
| Cilium Agent | 노드 endpoint/dataplane 관리, 모든 호스트 네트워킹 기능을 소유하는 것은 아님 |
| Cilium Operator | 클러스터 할당/identity/controller 작업, 여러 replica와 해당 작업의 leader election 지원, 검토한 차트 기본 replica는 2 |
| eBPF | 검증과 기능 요건을 따르는 커널 hook의 프로그램/map, 실제 성능은 측정 필요 |
| L3/L4와 L7 정책 | L7은 지원 Envoy/DNS proxy 경로 필요, Kafka 인지 L7은 1.20에서 제거되었지만 Kafka 연결의 L4 제어는 가능 |
| kube-proxy 교체 | 선택적인 Service 처리, DSR·Maglev·XDP는 별도 설정/토폴로지 조건 |
| 암호화 | `encryption.type`에서 `ipsec`, `wireguard`, beta `ztunnel`을 선택하며, ztunnel 워크로드 mTLS는 별도의 enrollment·bootstrap·트래픽·정책 전제가 필요 |
| Hubble | 네트워크/proxy flow와 서비스 맵 관측, 자동 end-to-end 애플리케이션 tracing이 아님 |
| ClusterMesh / BGP | ClusterMesh는 identity·신뢰·네트워크 도달성 필요, BGP는 광고 기능이며 내부 라우팅을 설정하지 않음 |
컴포넌트 관계는 kubelet이 **CRI**로 Pod sandbox 작업을 요청하고, 컨테이너 런타임이 **CNI 플러그인**을 호출하며, Cilium이 endpoint 설정을 조정하고 agent가 dataplane을 설정하는 구조입니다. CNI는 패킷마다 통과하는 전달 계층이 아닙니다. Envoy는 설정한 L7 proxy 트래픽을 처리하고 Hubble flow event와 Prometheus scrape는 별도 경로입니다.
### 보안 Identity
Security identity는 해당 할당 범위에서 endpoint의 **보안 관련 레이블 집합**에 할당한 숫자 식별자입니다. 레이블은 필터링/설정되며 namespace에서 파생한 레이블을 포함할 수 있습니다. 같은 `app` 값만으로 네임스페이스나 클러스터가 다른 Pod가 같은 identity를 공유한다고 보장할 수 없습니다. 숫자 ID는 영구적 전역 hash나 Pod IP가 아니며 다른 endpoint 유형도 identity를 사용합니다.
## 컨테이너 네트워킹 기초
Host networking은 호스트 network namespace를 공유합니다. Bridge는 호스트의 인터페이스를 연결하고 overlay는 underlay 위에 캡슐화합니다. Native routing은 underlay가 Pod 주소에 도달할 수 있어야 합니다. 이 개념은 함께 사용될 수 있으며 eBPF와 Netfilter 구현 선택과 혼동하면 안 됩니다.
운영에서는 주소 용량, 라우팅/MTU, Service 동작, tenant 정책, 관측성, 장애 복구를 검토합니다. 네트워크 모델 하나만으로 성능이나 보안이 결정되지는 않습니다.
## CNI 이해하기
CNI는 컨테이너 네트워크 설정을 위한 CNCF 규격/라이브러리/플러그인 체계입니다. JSON으로 설정과 결과를 교환하고 IPAM 플러그인에 주소 할당을 위임할 수 있습니다. CNI의 설정/제거 계약은 kubelet과 컨테이너 런타임 사이의 CRI와 다릅니다. Kubernetes 1.24부터 kubelet은 제거된 `--network-plugin`/`--cni-bin-dir` 설정 플래그를 소유하지 않습니다.
| 프로젝트 | 구분할 사항 |
| --- | --- |
| Cilium | Linux eBPF, 여러 routing/IPAM 모드, proxy를 사용하는 L7 정책과 Hubble |
| Calico | Linux Iptables/Nftables/BPF와 지원 Windows HNS, OSS WireGuard·staged policy·별도 L7 통합 |
| Flannel | VXLAN/host-gw/WireGuard 등 연결 backend, 선택적 차트 컨트롤러나 다른 구현으로 정책 추가 |
| AWS VPC CNI | VPC 주소 할당/네트워킹, 지원 EC2 Linux의 네이티브 정책과 별도 SG-for-Pods |
| Weave Net | 원래 weaveworks/weave 저장소는 archived 상태, 과거 선택지로 구분하고 사용할 유지 배포판은 별도 확인 |
지원 경계는 [현재 비교](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/README.md)를 참고하세요. 상한 없는 Kubernetes 호환성이나 “매우 높음/높음/중간” 성능 등급을 배포 근거로 사용하지 마세요. 서비스 메시는 일반 Pod 네트워킹의 필수 요건이 아닌 선택적 통합입니다.
## 실습: 제한된 L4 정책
선택한 context와 전용 네임스페이스를 사용합니다. 예제는 정책을 정의하며 애플리케이션 서버나 클라이언트 이미지를 **배포하지 않습니다**. 승인된 이미지/도구로 controller가 관리하는 테스트 워크로드를 준비하세요.
- `app=backend` 레이블과 TCP 8080 listener가 있는 backend Pod.
- 적절한 테스트 클라이언트가 있는 `app=frontend` Pod와 다른 레이블의 client Pod.
- 일반 관리 Pod 인터페이스, 기본 레이블 처리, 확인한 DNS/Service 구성, 의도한 격리를 무효화할 다른 matching allow 정책이 없는 환경.
네임스페이스 정의를 `cilium-intro-namespace.yaml`로 저장·적용한 뒤 테스트 워크로드를 준비하세요:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: cilium-intro-demo
```
```bash
kubectl --context "$CILIUM_LAB_CONTEXT" apply -f cilium-intro-namespace.yaml
```
다음을 `cilium-intro-policy.yaml`로 저장합니다.
```yaml
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-frontend-backend
namespace: cilium-intro-demo
spec:
endpointSelector:
matchLabels:
k8s:app: backend
ingress:
- fromEndpoints:
- matchLabels:
k8s:app: frontend
k8s:io.kubernetes.pod.namespace: cilium-intro-demo
toPorts:
- ports:
- port: '8080'
protocol: TCP
```
```bash
kubectl --context "$CILIUM_LAB_CONTEXT" get pods -n cilium-intro-demo --show-labels
kubectl --context "$CILIUM_LAB_CONTEXT" apply -f cilium-intro-policy.yaml
kubectl --context "$CILIUM_LAB_CONTEXT" get cnp -n cilium-intro-demo
```
위 전제와 추가 matching allow가 없을 때 **일반 Pod 간 ingress**의 기대 결과는 다음과 같습니다.
| 출발지 / 목적지 | 기대 결과 |
| --- | --- |
| 같은 namespace frontend → backend TCP 8080 | 허용 |
| 다른 client 레이블 → backend TCP 8080 | 거부 |
| Frontend → backend의 다른 port/protocol | 이 정책에서는 허용하지 않음 |
| 다른 namespace의 같은 app 레이블 | 이 정책에서는 허용하지 않음 |
정책 반영 후 성공/실패 연결을 모두 검증하세요. 다른 allow/deny 정책, host 트래픽과 probe는 결과를 바꿀 수 있습니다. 이 ingress 규칙은 frontend egress나 HTTP method/path를 제한하지 않습니다. API 접수 성공을 적용 성공으로 가정하지 말고 실제 트래픽과 endpoint 상태를 확인하세요.
`cilium connectivity test`는 워크로드와 정책을 만드는 추가 테스트 runner입니다. 필요한 권한을 갖춘 검토 환경에서만 실행하며 읽기 전용 status 명령이 아닙니다. 별도 성능 runner는 `cilium connectivity perf`이며 측정 시 실제 버전·토폴로지·원본 결과를 보존하세요.
## 참고 자료와 다음 단계
- [Cilium 요구사항과 설정](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/README.md)
- [CNI 프로젝트](https://github.com/containernetworking/cni)
- [Kubernetes network plugin과 runtime 소유권](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/)
- [Kubernetes client version skew](https://kubernetes.io/releases/version-skew-policy/)
- [Cilium identity와 용어](https://github.com/cilium/cilium/blob/v1.20.1/Documentation/gettingstarted/terminology.rst)
- [원래 Weave 저장소 metadata](https://api.github.com/repos/weaveworks/weave)
[eBPF](https://www.atomai.click/kubernetes-docs/llms/ko/networking/cilium/02-ebpf.md)로 진행하거나 [소개 퀴즈](https://www.atomai.click/kubernetes-docs/ko/quizzes/networking/cilium/01-introduction-quiz)로 이해를 확인하세요.
----------------------------------------
Source: https://www.atomai.click/kubernetes-docs/ko/networking/cilium/02-ebpf
----------------------------------------
# eBPF 기술 심층 분석
> **검토 기준**: Cilium 1.20.1, Linux 5.10+ 또는 문서화된 동등 백포트(예: RHEL 8.10의 4.18 커널), 테스트된 Kubernetes 1.33–1.36. 개별 BPF 기능에는 별도 조건이 있습니다.
> **최종 검토**: 2026년 9월 12일
## 실습 환경 설정
유지보수되는 배포판, 아래 tracepoint, 추적 프로그램을 로드할 권한이 있는 일회용 Linux 개발 VM을 사용합니다. Cilium 설치와 별개 실습이므로 실험용 프로그램을 클러스터 노드에 로드하지 않습니다. 검증기 설명의 소스 기준은 Linux 6.12이며 모든 6.12 배포판에서 모든 기능이 활성화되어 있다는 뜻은 아닙니다.
BPF 백엔드가 있는 Clang, 대상 아키텍처의 UAPI 헤더, libbpf 1.x 개발 헤더·라이브러리, libelf, zlib, C 컴파일러와 bpftool이 필요합니다. BCC와 bpftrace는 선택 가능한 다른 도구입니다. Debian/Ubuntu에서는 보통 `clang`, `libbpf-dev`, `libelf-dev`, `zlib1g-dev`, `build-essential`, `pkg-config`를 사용하지만 bpftool 패키징은 배포판·커널에 따라 다릅니다. 모든 Debian 시스템에 `linux-tools-generic`이 맞는다고 가정하지 않습니다. Cilium은 AMD64/AArch64 호스트를 문서화하며 컨테이너 이미지 밖에서 Cilium을 네이티브 실행할 때는 Clang/LLVM 18.1+가 추가로 필요합니다. 이는 작은 추적 실습의 요구사항과 별개입니다.
```bash
uname -r
clang --version
clang --print-targets
pkg-config --modversion libbpf
bpftool version
test -r /sys/kernel/tracing/events/syscalls/sys_enter_execve/format
test -r /sys/kernel/tracing/events/sched/sched_process_exec/format
# 준비된 실습 VM에서만 BPF 기능을 능동적으로 탐색합니다.
sudo bpftool feature probe kernel
```
Tracefs가 마운트되어 있고 접근 가능해야 합니다. 일부 시스템에서는 `/sys/kernel/debug/tracing`에 노출됩니다. 커널 설정, capability, lockdown/LSM 정책, 컨테이너 제약 때문에 컨테이너 root도 로드·연결하지 못할 수 있습니다. `CAP_BPF` 하나가 모든 추적 권한을 뜻하지 않으며 필요한 권한은 커널, 프로그램 타입, BPF token 위임에 따라 다릅니다.
**검증 범위:** 예제는 libbpf 1.7 헤더를 사용한 호스트 C 문법 검사, 사용자 공간 링크, 결정적인 헬퍼 시뮬레이션을 통과했습니다. Clang BPF 대상 컴파일, 실행 커널 검증기와 실제 tracepoint 연결은 준비된 VM에서 추가 검증해야 합니다. 운영 환경 검증이나 무손실 추적을 보장하는 예제가 아닙니다.
## eBPF 기술 소개 및 역사적 배경
eBPF는 허용된 프로그램을 Linux의 지원 훅에서 실행하여 커널 동작을 관찰하거나 제어합니다. 검증기는 메모리 접근과 실행을 제한하지만 커널, 검증기, JIT, 헬퍼의 버그 가능성은 남습니다. 검증 통과가 호스트 장애 불가능이나 의도한 정책의 정확성을 보장하지는 않습니다.
### BPF에서 eBPF로: 발전 역사
McCanne과 Jacobson의 *The BSD Packet Filter: A New Architecture for User-level Packet Capture*에는 1992년 12월 19일 사전 원고 날짜와 1993년 1월 25–29일 Winter USENIX 발표가 함께 명시되어 있습니다. 연도를 인용할 때 이 차이를 보존합니다. Classic BPF는 32비트 A/X 레지스터와 scratch 메모리로 필터링하여 불필요한 사용자 공간 패킷 복사를 줄였습니다. 제한된 명령어 집합이 현대 CPU에서 실행할 수 없다는 뜻은 아닙니다.
확장 BPF는 64비트 명령어 집합, R0–R10의 레지스터 11개(R10은 읽기 전용 프레임 포인터), 일반적으로 512바이트로 제한되는 스택, 맵과 다양한 프로그램 타입을 추가했습니다. 범용 레지스터가 역사적으로 10개에서 11개로 늘어났다는 뜻은 아닙니다. 함수·tail call 조합에는 추가 스택 제약이 있습니다.
### eBPF의 기술적 진화: 커널 버전별 주요 기능
버전이 고정된 업스트림 소스에서 확인한 주요 변화입니다. 배포판 지원표는 아니며 백포트, 빌드 옵션, 아키텍처와 헬퍼 지원은 다를 수 있습니다.
| Kernel | 주요 변화 |
|---|---|
| [3.15](https://github.com/torvalds/linux/blob/v3.15/include/linux/filter.h) | 확장 명령어 집합과 내부 classic BPF 변환 |
| [3.16](https://github.com/torvalds/linux/blob/v3.16/arch/x86/net/bpf_jit_comp.c) | x86 확장 BPF JIT |
| [3.18](https://github.com/torvalds/linux/blob/v3.18/include/uapi/linux/bpf.h) | BPF 시스템 호출·검증 기반; 아직 사용 가능한 HASH/ARRAY 타입 없음 |
| [3.19](https://github.com/torvalds/linux/blob/v3.19/include/uapi/linux/bpf.h) | HASH/ARRAY 맵과 socket-filter 프로그램 타입 |
| [4.1](https://github.com/torvalds/linux/blob/v4.1/include/uapi/linux/bpf.h) | KPROBE와 TC SCHED_CLS/SCHED_ACT |
| [4.2](https://github.com/torvalds/linux/blob/v4.2/include/uapi/linux/bpf.h) | PROG_ARRAY와 tail call |
| [4.8](https://github.com/torvalds/linux/blob/v4.8/include/uapi/linux/bpf.h) | XDP 프로그램 타입 |
| [4.10](https://github.com/torvalds/linux/blob/v4.10/include/uapi/linux/bpf.h) | LRU 해시 맵 |
| [4.16](https://github.com/torvalds/linux/blob/v4.16/include/uapi/linux/bpf.h) | BPF-to-BPF 함수 호출 |
| [4.17](https://github.com/torvalds/linux/blob/v4.17/include/uapi/linux/bpf.h) | Raw tracepoint |
| [4.18](https://github.com/torvalds/linux/blob/v4.18/include/uapi/linux/bpf.h) | BTF 로드 API |
| [5.2](https://github.com/torvalds/linux/blob/v5.2/include/uapi/linux/bpf.h) | 전역 데이터에 사용하는 맵 값 직접 접근 |
| [5.7](https://github.com/torvalds/linux/blob/v5.7/include/uapi/linux/bpf.h) | BPF link API와 BPF LSM |
| [5.8](https://github.com/torvalds/linux/blob/v5.8/include/uapi/linux/bpf.h) | BPF 링 버퍼 |
| [5.10](https://github.com/torvalds/linux/blob/v5.10/include/uapi/linux/bpf.h) | 지원되는 연결 타입의 sleepable 프로그램 |
| [5.15](https://github.com/torvalds/linux/blob/v5.15/include/uapi/linux/bpf.h) | BPF 타이머 헬퍼 |
| [5.19](https://github.com/torvalds/linux/blob/v5.19/include/uapi/linux/bpf.h) | 동적 포인터 헬퍼 |
| [6.2](https://github.com/torvalds/linux/blob/v6.2/kernel/bpf/helpers.c) | 타입이 있는 객체 할당 kfunc; 임의 malloc과는 다름 |
Bounded loop는 Linux 5.3에 도입되었습니다. [업스트림 검증기 변경](https://github.com/torvalds/linux/commit/2589726d12a1b12eaaa93c7f1ea64287e383c7a5)은 루프 분석과 상태 가지치기를 설명합니다. 설계 FAQ에 남아 있는 “루프 미구현” 문단을 현재 기능 안내로 사용하면 안 됩니다. 제한된 루프도 검증 복잡도 한도를 초과할 수 있습니다.
### 생태계 성장과 활용 분야
Cilium 공개 저장소는 2015년 12월 생성되었으므로 프로젝트가 2017년에 처음 시작되었다는 설명은 부정확합니다. 저장소 생성일이 정확한 제품 출시일이나 “최초의 주요 프로젝트”라는 순위의 근거는 아닙니다.
| 분야 | 예와 경계 |
|---|---|
| 네트워킹 | Cilium/Calico 데이터플레인, Katran 로드밸런싱, XDP 필터링 |
| 런타임 보안 | Falco, Tracee, Tetragon의 커널 이벤트 활용; 차단 기능은 제품·훅에 따라 다름 |
| 추적 | Python/Lua 프런트엔드를 포함한 BCC, bpftrace, 스토리지·블록 I/O 추적 |
| 네트워크 관측 | Hubble의 플로우·프록시 이벤트; 플로우 그래프가 분산 애플리케이션 span 추적은 아님 |
| 서비스 메시 | Cilium이 커널 전달과 사용자 공간 프록시를 조합하여 지원되는 L7 기능 제공 |
| 커뮤니티 | eBPF Foundation이 생태계를 지원하며 투자·성숙도가 호환성 기준은 아님 |
`seccomp-bpf`는 시스템 호출 판단에 classic BPF 필터 인터페이스를 사용합니다. Linux가 내부에서 classic 필터를 변환할 수 있지만 일반 eBPF 프로그램·맵·헬퍼 API와 같지는 않습니다.
### eBPF와 전통적 커널 모듈 비교
| 특성 | eBPF | 커널 모듈 |
|---|---|---|
| 안전성 | 검증기로 실행 제한; 구현 버그와 운영 위험은 남음 | 더 넓은 네이티브 커널 접근; 버그로 호스트 장애 가능 |
| 배포 | 지원 프로그램은 재부팅 없이 로드·연결 가능 | 의존성·사용 상태가 허용하면 많은 모듈도 재부팅 없이 로드·해제 가능 |
| 호환성 | 명령어·헬퍼 ABI와 기능 조건; CO-RE는 지원되는 타입 접근 재배치 | 커널·모듈 ABI, 설정, 배포판 지원에 의존 |
| 성능 | 흔히 JIT 사용; 훅·프로그램·워크로드에 따른 오버헤드 | 네이티브 실행도 워크로드에 따른 비용 발생 |
| 개발 | 제한된 context, 헬퍼·kfunc, 검증기 한도 | 커널 API와 일반적인 커널 개발 제약 |
| 권한 | 로드·연결을 위한 적절한 권한 또는 위임 필요 | 특권 로드; 서명·lockdown 제약 가능 |
두 방식 모두 운영 검증이 필요합니다. 모듈이 벤더 구현으로만 제한되지 않으며 eBPF라는 이유만으로 운영 배포가 안전해지지 않습니다.
## 커널 내부 eBPF 아키텍처 심층 분석
### eBPF 아키텍처 구성 요소 상세 설명
사용자 공간에서 Clang은 C를 BPF ELF로 컴파일하고 Rust는 자체 컴파일러·도구 생태계를 사용합니다. libbpf는 ELF section, 맵, 재배치, 로드와 지원되는 연결 API를 처리합니다. BCC는 상위 API, bpftrace는 추적 언어를 제공합니다.
CO-RE는 BTF와 재배치로 지원되는 타입·필드 접근을 조정합니다. 없는 헬퍼, 프로그램 타입, 커널 설정을 제공하거나 임의의 아키텍처·커널 간 호환성을 보장하지 않습니다. BTF를 쓴다고 커널 내부 구조, tracepoint 형식, kfunc가 안정적인 ABI가 되지는 않습니다.
커널 검증기는 프로그램 타입, context, 헬퍼와 권한을 검사합니다. JIT는 허용된 BPF를 네이티브 명령어로 변환할 수 있으며 인터프리터는 지원 환경의 다른 실행 방식입니다. JIT 출력이 반드시 별도 VM 단계를 다시 통과하는 구조는 아닙니다. 연결 단계가 로드된 프로그램과 훅을 연결하며 로드만으로 tracepoint를 구독하지 않습니다.
### eBPF 프로그램 생명주기 상세 분석
1. **개발:** 훅/context와 맵·라이선스 메타데이터를 정의합니다. 모든 프로그램에 GPL 호환성이 필요한 것은 아니지만 GPL 전용 헬퍼와 일부 타입·kfunc에는 제한이 있습니다. 이 추적 예제는 헬퍼에 맞춰 GPL 메타데이터를 사용합니다.
2. **컴파일:** 대상 도구·헤더로 BPF ELF와 필요한 debug/BTF 정보를 만듭니다.
3. **열기·로드:** ELF를 읽고 맵을 생성하거나 명시적으로 재사용하며 재배치와 BPF 로드 API를 수행합니다. 검증과 선택적 JIT는 로드 중 발생합니다.
4. **연결:** 적절한 API를 사용합니다. libbpf는 아래 `SEC("tracepoint/...")`에서 훅을 추론할 수 있으며 link/연결의 수명을 유지해야 합니다.
5. **실행·관찰:** 이벤트가 프로그램을 호출하고 사용자 공간이 맵·버퍼를 읽습니다. 샘플링·용량 제한 때문에 관측이 누락될 수 있습니다.
6. **갱신·해제:** 필요한 경우에만 호환 맵·link·pin을 의도적으로 유지합니다. 이 실습에서는 link와 object를 닫아 자원을 해제합니다.
Linux 6.12에서 BPF 권한이 있는 로드 경로의 프로그램 길이는 최대 1,000,000개 명령어, 비특권 경로는 4,096개로 제한됩니다. 검증기에는 별도로 1,000,000개 명령어의 **분석 복잡도** 한도가 있습니다. 더 작은 프로그램도 검증에 실패할 수 있습니다. 비특권 BPF는 비활성화된 경우가 많으며 token/capability와 프로그램 타입 검사도 적용됩니다.
### eBPF 프로그램 유형과 특성
| 훅 / 프로그램 타입 | 용도와 반환값의 경계 |
|---|---|
| XDP / `BPF_PROG_TYPE_XDP` | Native driver XDP는 skb 할당 전에 실행하며 generic/offload 모드는 다름. `XDP_DROP`, `PASS`, `TX`, `REDIRECT`는 동작이지 처리량 보장이 아님 |
| TC / `SCHED_CLS`, `SCHED_ACT` | Ingress/egress 패킷 분류·동작. Classifier의 `TC_ACT_*` 의미에는 적절한 direct-action 설정 필요 |
| Socket filter / `SOCKET_FILTER` | 소켓 패킷 전달: 0은 폐기, 양수 캡처 길이는 절단 가능. 생성·connect 정책은 다른 훅 사용 |
| kprobe/uprobe / `KPROBE` | 커널·사용자 공간 probe. 별도 `BPF_PROG_TYPE_UPROBE` 없음. 인라이닝·금지 목록·심볼 존재가 연결 제한 |
| Tracepoint / `TRACEPOINT` | 정적 이벤트 context. 대상 format 확인 필요; 안정적인 커널 ABI 보장 아님 |
| Perf event / `PERF_EVENT` | 성능 샘플링; 반환 동작은 perf-event 통합에 따름 |
| cgroup / `CGROUP_SKB`, `CGROUP_SOCK`, `CGROUP_SOCK_ADDR` 등 | 네트워크·소켓 제어; context와 허용·거부 규칙이 다름 |
| LSM / `LSM` | MAC 방식은 보통 이전 오류를 유지하고 0/오류 반환; cgroup-LSM 허용 의미는 다름 |
| Socket operations / `SOCK_OPS` | TCP 콜백; 동작·reply 필드·헬퍼 지원 확인 필요 |
| fentry/fexit / `TRACING` | 지원 대상의 BTF 기반 함수 추적; 대상·연결 제약은 남음 |
필요한 가시성·제어로 훅을 선택합니다. XDP에는 후단 스택 context 일부가 없고 TC는 skb 기반 트래픽을 처리합니다. Tracepoint/probe 관측이 자동으로 정책을 집행하는 것은 아닙니다. 타입 간 context 구조나 반환 코드를 그대로 복사하지 않습니다.
### eBPF 맵: 데이터 공유와 상태 저장의 핵심
맵은 FD, 로드된 프로그램, 명시적인 bpffs pin 등의 참조가 남아 있는 동안 존재합니다. Pin은 디스크 영속 저장이 아니며 재부팅 후 내용도 보존하지 않습니다. 재로드 시 이전 맵을 자동 재사용하지 않습니다.
| 타입 | 용도와 제약 |
|---|---|
| `HASH` | 용량 제한 키·값 테이블. 가득 차면 삽입 실패 가능. 평균 상수 시간 조회가 지연 보장은 아님 |
| `ARRAY` | 유효 인덱스의 값은 미리 할당되고 0으로 초기화됨. 0이 없는 해시 엔트리를 뜻하지 않음 |
| `LRU_HASH` | LRU 방식 축출 캐시; 무손실 누적 카운터가 아님 |
| `RINGBUF` | CPU 간 다중 생산자·단일 소비자; key/value 크기 0, 2의 거듭제곱 바이트 용량; 예약 실패 시 블로킹하지 않음 |
| `PERF_EVENT_ARRAY` | CPU별 perf 채널; 사용자 공간 설정·소비와 유실 레코드 집계 필요 |
| `PROG_ARRAY` | Tail call 프로그램 참조; 대상 호환성과 호출 한도 적용 |
| `PERCPU_HASH` / `PERCPU_ARRAY` | CPU 간 경합 감소; 모든 race 제거는 아님. 사용자 공간은 모든 possible CPU 슬롯·패딩 고려 |
| `SOCKMAP` / `SOCKHASH` | 지원되는 리다이렉션·프로그램의 소켓 참조; 임의 소켓 동작 훅이 아님 |
libbpf 1.x에서 `struct bpf_map_def SEC("maps")`가 제거되었습니다. 다음 BTF 선언은 실제 타입으로 여덟 맵 범주를 보여줍니다. 필요한 맵을 적절한 프로그램과 조합해야 하며 선언만으로 이벤트 파이프라인이 완성되지 않습니다.
**`map_types.bpf.c`**
```c
#include
#include
/* Definitions only; combine the needed maps with a suitable program. */
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, __u64);
} hash_counts SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u64);
} total SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, __u64);
} cache SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__type(key, __u32);
__type(value, __u32);
/* libbpf determines max_entries from the number of possible CPUs. */
} perf_events SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
__uint(max_entries, 10);
__type(key, __u32);
__type(value, __u32);
} jump_table SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u64);
} cpu_counts SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_SOCKMAP);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, __u32);
} sockets SEC(".maps");
```
공유 카운터는 원자적 증가가 필요합니다. 새 해시 키에는 `BPF_NOEXIST` 삽입 후 실제로 만들어진 엔트리를 조회·증가시켜야 합니다. `BPF_ANY` 초기화는 다른 CPU의 카운트를 덮어쓸 수 있습니다. 배열의 유효 인덱스에는 엔트리가 이미 존재합니다.
## Cilium에서의 eBPF 활용: 컨테이너 네트워킹의 혁신
### Cilium 아키텍처와 eBPF의 역할

[🔍 인터랙티브 다이어그램 보기](https://www.atomai.click/kubernetes-docs/archmaps/ko-networking-cilium-02-ebpf-1.html)
그림은 논리적 역할이며 필수 위치나 단일 컴파일 파이프라인이 아닙니다. CLI는 클러스터 밖에서 실행할 수 있고 에이전트는 적격 관리 노드에서 실행합니다. 프로그램 빌드·로드는 버전·기능에 따라 다릅니다. Hubble Relay는 플로우를 집계하며 Prometheus 메트릭은 별도 엔드포인트입니다.
에이전트는 endpoint, identity, 정책, 데이터플레인 상태를 조정합니다. Operator는 설정된 identity/IPAM 생명주기 같은 클러스터 작업을 담당하며 패킷 전달 경로가 아닙니다. Hubble은 BPF 플로우 정보와 사용자 공간 프록시 이벤트를 조합합니다.
### Cilium eBPF 데이터플레인 상세 분석
다음은 협력하는 기능이며 모든 패킷의 고정 처리 순서가 아닙니다.
1. **진입:** 소켓 훅은 패킷 생성 전 Service 백엔드를 결정할 수 있고 TC는 패킷 경로를 처리합니다. 선택적 XDP 가속은 지원되는 외부 트래픽에 적용합니다.
2. **Identity·정책:** IP/identity와 endpoint 정책으로 L3/L4 접근을 제어합니다. 지원되는 HTTP/gRPC 정책은 Envoy, DNS는 DNS 프록시를 사용하며 L7 파싱·집행 전체가 BPF는 아닙니다.
3. **상태·변환:** conntrack, service/backend, reverse-NAT, affinity 맵은 역할이 다르며 모든 패킷에서 백엔드를 다시 선택하지 않습니다.
4. **전달:** native routing 또는 설정된 overlay를 사용합니다. DSR dispatch·반환 경로에는 모드에 맞는 네트워크 조건이 필요합니다.
5. **관찰:** 데이터플레인 카운터·이벤트와 프록시 이벤트에는 설정·수집 유실의 한계가 있습니다.
백엔드 readiness는 제어플레인 상태와 해당 health 메커니즘에서 얻으며 모든 애플리케이션을 BPF가 probe한다는 뜻은 아닙니다. Maglev, affinity, DSR, 가속은 선택 기능이지 항상 적용되는 기본값이 아닙니다.
### Cilium 주요 eBPF 프로그램 상세 설명
| Cilium 1.20.1 소스 | 역할 |
|---|---|
| `bpf/bpf_lxc.c` | Endpoint 패킷 경로, 정책, conntrack과 전달 |
| `bpf/bpf_overlay.c` | Overlay 패킷 경로 |
| `bpf/bpf_host.c` | 호스트·장치 경로와 지원되는 host firewall 처리 |
| `bpf/bpf_xdp.c` | 설정된 로드밸런서 가속 등을 포함한 XDP 경로 |
| `bpf/bpf_sock.c` | connect/sendmsg/recvmsg 서비스 변환 등의 socket-address 훅 |
| `bpf/lib/lb.h` | 공통 로드밸런싱 헬퍼 |
| `bpf/lib/policy.h` | 공통 정책 헬퍼 |
이 릴리스에는 최상위 `bpf_lb.c`, `bpf_network.c` 파일이 없습니다. 함수명·기능 조건은 변하므로 개념적인 이름을 소스 파일로 가정하지 말고 정확한 버전을 확인합니다.
### Cilium의 eBPF 맵 활용
다음 예는 안정적인 맵 레이아웃 API가 아닙니다.
| 이름 / 계열 | 키와 역할 |
|---|---|
| `cilium_lxc` | 주소·주소 계열 → endpoint 전달 메타데이터. 단순 endpoint ID가 아님 |
| `cilium_ipcache_v2` | Prefix, 주소 계열, 클러스터 context → identity·터널 메타데이터 |
| `cilium_policy_v3_