Kubernetes from scratch — 직접 구축하면서 배우기
- CKA 스터디 주제1 : 직접 구축하기
- AWS 전용 함정이 생각보다 많습니다.
체크박스는 그대로 뒀습니다. 아직 다 채운 상태가 아니고, 따라 해보실 분은 복사해서 쓰시면 될 것 같아서요.
먼저 알고 갈 것 세 줄.
- Case 1부터
--control-plane-endpoint를 DNS 이름으로 잡아야 함. 노드 IP로 init하면 인증서 SAN이 고정돼 CP 추가가 막히고, 클러스터를 다시 만들어야 함 (Phase 0.3) - 홈랩 가이드는 AWS에서 그대로 안 먹힘. kube-vip·MetalLB L2는 gratuitous ARP 의존이라 AWS SDN이 차단하고, ALB는 TLS를 종료해서 클라이언트 인증서가 apiserver까지 못 감 (Phase 2)
- 목표는 "한 번 성공"이 아니라 "15분 재구축". 재구축이 부담스러워지는 순간 실습을 아끼게 되고, 그때부터 학습 사이클이 깨짐 (Phase 16.4)
개요 — kubeadm + Ubuntu 24.04 + AWS EC2 + OpenTofu
목적: 매니지드 서비스(EKS) 없이 컨트롤 플레인을 직접 구성하며 쿠버네티스 내부 동작을 이해하는 것. 방법: 같은 코드베이스로 규모만 바꿔 3가지 케이스를 반복 구축함.
| Case | 구성 | 핵심 학습 대상 |
|---|---|---|
| Case 1 | 1대 (CP 겸 워커) | 컨트롤 플레인 구조, static pod, 테인트 |
| Case 2 | 3대 (CP 겸 워커 ×3) | etcd 쿼럼, HA 조인, 다중 노드 네트워킹 |
| Case 3 | 8대 (CP 3 + 워커 5) | 역할 분리, 스케줄링 제어, 롤링 업그레이드 |
이번 편은 kubeadm 부트스트랩까지 다룹니다. Kubernetes The Hard Way는 다음 편으로 미뤄뒀습니다.
Phase 0. 학습 설계
- [ ] 0.1 케이스별 학습 목표 정의 — 왜 3단계로 나누는가
- [ ] 0.2 케이스 대조표 작성 (노드 수 / etcd 멤버 / 쿼럼 / 장애 허용 / 테인트 / 엔드포인트)
- [ ] 0.3 핵심 원칙: Case 1부터
--control-plane-endpoint를 DNS 이름으로 지정- 노드 IP로 init하면 인증서 SAN이 고정 → CP 추가 불가 → 클러스터 재생성
- 세 케이스를 같은 코드로 굴리기 위한 전제조건
- [ ] 0.4 반복 가능성 목표 설정 — "한 번 성공"이 아니라 "15분 안에 재구축"
- [ ] 0.5 학습 순서 확정: Case 1 반복 → Case 2 → Case 3
케이스 대조표
| 항목 | Case 1 | Case 2 | Case 3 |
|---|---|---|---|
| VM 수 | 1 | 3 | 8 |
| etcd 멤버 | 1 | 3 | 3 |
| 쿼럼 / 장애 허용 | 1 / 0대 | 2 / 1대 | 2 / 1대 |
| CP 테인트 | 제거 | 제거 | 유지 |
| API 엔드포인트 | Route53 → 노드 IP | NLB | NLB |
| 스케줄 가능 노드 | 1 | 3 | 5 |
| 스토리지 | local-path | local-path | Longhorn 후보 |
Phase 1. AWS 인프라 설계
- [ ] 1.1 리전 / AZ 선택 (학습은 단일 AZ로 단순화, 멀티 AZ는 나중에)
- [ ] 1.2 비용 통제 전략
- [ ] 세션 종료 시
tofu destroy습관화 - [ ] 스팟 인스턴스 검토 (워커만 — CP는 위험, 이 판단 자체가 학습 소재)
- [ ] 정지해도 EBS 요금은 계속 발생함을 인지
- [ ] AWS Budgets 예산 알림 설정
- [ ] 세션 종료 시
- [ ] 1.3 ⚠️ 인스턴스 타입 — CPU 크레딧 함정
- t3/t4g 버스터블은 크레딧 소진 시 스로틀링 → etcd/apiserver 원인불명 타임아웃
- CP는 최소 2vCPU, 가급적 고정 성능 계열
- [ ] 1.4 ⚠️ EBS — etcd는 디스크 지연에 극도로 민감
- gp3 사용.
apply request took too long경고의 주범이 IOPS
- gp3 사용.
- [ ] 1.5 3중 CIDR 충돌 회피 설계
VPC : 10.0.0.0/16 Pod CIDR : 10.244.0.0/16 Service : 10.96.0.0/12 - [ ] 1.6 서브넷 / IGW / 라우팅 테이블 (퍼블릭 단일 서브넷으로 시작)
- [ ] 1.7 Security Group 설계
- [ ] 클러스터 내부: self-reference 전면 허용 (학습용 단순화)
- [ ] 내 IP → 22 (SSH)
- [ ] 내 IP → 6443 (로컬 kubectl)
- [ ] NLB → 6443
- [ ] 1.8 접속 방식 결정: SSH 키페어 vs SSM Session Manager (후자는 22 포트 불필요)
- [ ] 1.9 AMI 선택 — Ubuntu 24.04 LTS, cloud-init 활용 범위 결정
- [ ] 1.10 워크스테이션 결정 — 로컬 노트북 vs 배스천 EC2
Phase 2. AWS에서만 생기는 함정
홈랩 가이드를 그대로 따라 하면 여기서 전부 막힘. 구축 전에 반드시 읽을 것.
- [ ] 2.1 ARP 기반 VIP 불가 — kube-vip(L2), keepalived는 gratuitous ARP 의존 → AWS SDN이 차단. API 엔드포인트를 이걸로 만들면 안 됨
- [ ] 2.2 MetalLB L2 모드도 같은 이유로 불가 → NodePort + NLB 또는 CCM으로 대체
- [ ] 2.3 ⚠️ ALB가 아니라 NLB — ALB는 TLS를 종료하므로 클라이언트 인증서가 apiserver에 도달하지 않아 인증 실패. 반드시 TCP passthrough(NLB)
- [ ] 2.4 NLB 헤어핀(loopback) 문제 — CP 노드가 자기 자신을 NLB 경유로 접근할 때 타임아웃. target type을
ip로 하거나 client IP preservation 비활성화 - [ ] 2.5 CCM(Cloud Controller Manager)이 없다
- LoadBalancer 자동 생성 ✗ / 노드 zone·region 라벨 ✗ / EBS 동적 프로비저닝 ✗
- 학습 목적이면 없이 시작하고 나중에 추가하는 편이 이해에 좋음
- [ ] 2.6 MTU 불일치 — VXLAN 50바이트 오버헤드. 작은 패킷은 되고 큰 패킷만 죽는 최악의 증상. CNI MTU 명시 설정
- [ ] 2.7 source/destination check — CNI 네이티브 라우팅 사용 시 비활성화 필요 (오버레이면 무관)
- [ ] 2.8
--node-ip는 반드시 사설 IP — 잘못 잡히면 노드 간 통신이 인터넷을 우회 - [ ] 2.9 certSANs에 외부 접근 주소 포함 — 로컬 kubectl용 공인 IP / NLB DNS. 나중에 추가는 번거로우니 처음부터
- [ ] 2.10 메타데이터 엔드포인트(169.254.169.254) — 파드에서 접근 가능하면 IAM 자격증명 유출 경로. NetworkPolicy 실습 소재
Phase 3. OpenTofu 코드 구성
- [ ] 3.1 OpenTofu 설치 및 Terraform과의 차이 확인
- CLI는
tofu, 프로바이더는registry.opentofu.org, HCL·state 호환 - state 암호화 내장 (선택적으로 활용)
- CLI는
- [ ] 3.2 디렉토리 구조 확정
infra/ ├── versions.tf # required_version, aws provider ├── variables.tf # cp_count, worker_count, create_nlb ... ├── network.tf # VPC / Subnet / IGW / RT ├── security.tf # SG ├── nodes.tf # aws_instance × count ├── dns.tf # Route53 private zone ├── lb.tf # NLB (조건부 생성) ├── outputs.tf # Ansible inventory 자동 생성 └── env/{case1,case2,case3}.tfvars - [ ] 3.3 케이스 분기는 변수 3개로만 —
cp_count,worker_count,create_nlb - [ ] 3.4 NLB 조건부 생성 (
count = var.create_nlb ? 1 : 0) - [ ] 3.5 Route53 Private Hosted Zone — 세 케이스 모두 동일한 DNS 이름 사용
- Case 1 → CP 사설 IP A 레코드
- Case 2/3 → NLB alias 레코드
- [ ] 3.6
templatefile()+local_file로 Ansible inventory 자동 생성 - [ ] 3.7 workspace로 케이스별 state 분리 (
tofu workspace new case2) - [ ] 3.8 backend 설정 (로컬 시작 → 필요 시 S3)
- [ ] 3.9 실행 사이클 정립:
init → plan → apply → (ansible) → destroy
Phase 4. OS 준비 (전 노드 공통)
- [ ] 4.1 hostname 규칙 (
k8s-cp-01,k8s-wk-01) 및 DNS 확인 - [ ] 4.2 swap 비활성 확인 (AWS Ubuntu AMI는 기본 없음)
- [ ] 4.3 커널 모듈
overlay,br_netfilter영구 로드 - [ ] 4.4 sysctl —
bridge-nf-call-iptables,ip_forward - [ ] 4.5 시간 동기화 확인 (AWS는 Amazon Time Sync 기본 적용)
- [ ] 4.6 SSH 키 배포 / sudo NOPASSWD
- [ ] 4.7 Ansible 도입 — 3대 이상이면 사실상 필수
- 롤 구조:
os→runtime→k8s-pkg→init→join→addons
- 롤 구조:
Phase 5. 컨테이너 런타임
- [ ] 5.1 containerd 설치
- [ ] 5.2
SystemdCgroup = true← 최다 실패 지점. 누락 시 kubelet 재시작 루프 - [ ] 5.3
sandbox_image버전을 kubeadm 기대값과 일치 - [ ] 5.4 runc / CNI 플러그인 바이너리 배치
- [ ] 5.5 crictl 설정 (
/etc/crictl.yaml) 및crictl info검증
Phase 6. 쿠버네티스 패키지
- [ ] 6.1
pkgs.k8s.io저장소 등록 — 마이너 버전별 URL이 다름 (업그레이드 시 교체 필요) - [ ] 6.2 kubeadm / kubelet / kubectl 설치 →
apt-mark hold - [ ] 6.3 kubelet
--node-ip에 사설 IP 지정 - [ ] 6.4
kubeadm config images pull로 사전 캐싱
Phase 7. API 엔드포인트 구성 (케이스 분기점)
- [ ] 7.1 Case 1 — Route53 A 레코드 → CP 사설 IP
- [ ] 7.2 Case 2/3 — NLB (TCP 6443 passthrough) → CP 3대
- [ ] 7.3 Target Group 헬스체크 TCP 6443 설정
- [ ] 7.4 init 전에는 6443이 닫혀 있어 LB가 unhealthy로 뜸 — 정상임
- [ ] 7.5 헤어핀 문제 대응 (Phase 2.4)
- [ ] 7.6 대안 검토: HAProxy EC2 1대 (SPOF지만 HAProxy 자체를 배울 수 있음)
- [ ] 7.7 ❌ 배제: kube-vip / keepalived (Phase 2.1 사유)
Phase 8. kubeadm 설정 템플릿화
케이스 전환의 실체. CLI 플래그가 아니라 YAML로 관리함.
- [ ] 8.1
ClusterConfiguration— controlPlaneEndpoint, Pod/Service CIDR, certSANs, etcd - [ ] 8.2
InitConfiguration/JoinConfiguration - [ ] 8.3
KubeletConfiguration—cgroupDriver: systemd - [ ] 8.4
KubeProxyConfiguration— iptables vs ipvs (Cilium 대체 시 제거 여부) - [ ] 8.5 Jinja2 /
envsubst로 케이스별 렌더링 - [ ] 8.6
kubeadm init --dry-run으로 사전 검증
Phase 9. 첫 CP 초기화
- [ ] 9.1
kubeadm init --config=... --upload-certs - [ ] 9.2 kubeconfig 배치 — 노드용 / 워크스테이션용(엔드포인트를 외부 주소로)
- [ ] 9.3 조인 명령 2종 저장 — certificate-key 2시간, 토큰 24시간 만료
- [ ] 9.4 static pod 4종 + etcd 기동 확인
- [ ] 9.5 이 시점
NotReady가 정상인 이유 이해 (CNI 부재)
Phase 10. 노드 조인
- [ ] 10.1 Case 1 — 조인 없음, 테인트 제거만
- [ ] 10.2 Case 2 — CP 2대 추가 조인(
--control-plane) 후 3대 전부 테인트 제거 - [ ] 10.3 Case 3 — CP 2대 조인 + 워커 5대 조인, 테인트 유지
- [ ] 10.4 노드 role 라벨 부여
- [ ] 10.5 토큰 / certificate-key 재발급 절차 숙지
- [ ] 10.6
etcdctl member list로 멤버 수 확인
Phase 11. CNI
- [ ] 11.1 선택 — Cilium(eBPF·관측성) / Calico(정책·라우팅) / Flannel(단순)
- [ ] 11.2 MTU 명시 설정 (Phase 2.6)
- [ ] 11.3 오버레이 vs 네이티브 라우팅 — AWS에선 오버레이가 무난
- [ ] 11.4 CoreDNS Running 전환 확인
- [ ] 11.5 연결성 테스트 (
cilium connectivity test등) - [ ] 11.6 NetworkPolicy 실습 — 메타데이터 엔드포인트 차단
Phase 12. 애드온
- [ ] 12.1 LoadBalancer 전략 결정 — NodePort + NLB 수동 / CCM 설치 / Ingress만 노출
- [ ] 12.2 ingress-nginx (NodePort 모드 → NLB 타깃)
- [ ] 12.3 StorageClass
- Case 1/2 — local-path-provisioner
- Case 3 — Longhorn(복제 3) 또는 NFS
- ⚠️ EBS는 단일 AZ 종속 — 파드가 다른 AZ 노드로 옮겨가면 마운트 실패. CSI/CCM이 필요한 이유를 체감하는 지점
- [ ] 12.4 metrics-server (
--kubelet-insecure-tls필요할 수 있음) - [ ] 12.5 kube-prometheus-stack (Case 3, 리소스 여유 시)
Phase 13. 검증
- [ ] 13.1 Deployment 다중 replica → 노드 분산 확인
- [ ] 13.2 DNS — 파드 내부에서
nslookup kubernetes.default - [ ] 13.3 노드 간 파드 통신 + 대용량 전송 테스트 (MTU 검증)
- [ ] 13.4 NodePort / Ingress 외부 접근
- [ ] 13.5 PVC 마운트 → 파드 재시작 후 데이터 유지
- [ ] 13.6 Case 3 — nodeSelector / affinity / taint-toleration 실습
- [ ] 13.7 HPA 동작 확인
Phase 14. 장애 주입 (Case 2/3의 핵심 학습)
- [ ] 14.1 CP 1대 강제 종료 → API 유지 확인, NLB 페일오버 시간 측정
- [ ] 14.2 CP 2대 종료 → 클러스터 완전 정지 관찰 (쿼럼 상실 체험)
- [ ] 14.3 Case 1 — CP가 죽어도 기존 파드는 계속 도는 것 확인 (kubelet 독립성)
- [ ] 14.4 워커 종료 → 파드 재스케줄 타이밍 관찰 (기본 약 5분)
- [ ] 14.5 etcd 멤버 제거 후 재추가 실습
- [ ] 14.6 AMI 스냅샷으로 실습 전 상태 저장 → 마음껏 부수기
Phase 15. 운영
- [ ] 15.1 etcd 스냅샷 백업 + 실제 복원 리허설 (반드시 한 번은)
- [ ] 15.2 인증서 만료(1년) —
kubeadm certs check-expiration/renew all - [ ] 15.3 업그레이드 — 마이너 1단계씩, CP → 워커 순, drain 포함
- [ ] 15.4 노드 추가·제거 — Case 2 → Case 3 확장 시나리오
- [ ] 15.5 트러블슈팅 루틴 —
journalctl -u kubelet,crictl ps -a, static pod 로그 - [ ] 15.6 백업 대상 목록화 — etcd,
/etc/kubernetes/pki, 매니페스트 저장소
Phase 16. 리셋 / 재구축
- [ ] 16.1
kubeadm reset+ iptables/ipvs 정리 +/etc/cni/net.d제거 - [ ] 16.2
tofu destroy→apply전체 사이클 - [ ] 16.3 Ansible 플레이북 멱등성 확보
- [ ] 16.4 목표: 명령 3줄로 Case 2 재구축 15분 — 도달하면 학습 효율이 급상승함
부록
A. 케이스별 변수 대조표
cp_count / worker_count / create_nlb / schedule_on_cp / k8s_version / pod_cidr / svc_cidr / cp_endpoint
B. 포트 / Security Group 매트릭스
| 대상 | 포트 |
|---|---|
| CP | 6443, 2379-2380, 10250, 10257, 10259 |
| 워커 | 10250, 30000-32767 |
| CNI | VXLAN 8472 등 |
C. 에러 → 원인 매핑
| 증상 | 유력 원인 |
|---|---|
| kubelet 재시작 루프 | cgroup driver 불일치 |
| 파드 간 통신 일부만 실패 | MTU 불일치 |
| 노드 영원히 NotReady | CNI 미설치 |
| join 실패 | 토큰/certificate-key 만료 |
| API 인증 실패 | ALB 사용 (TLS 종료) |
| etcd 경고 다발 | EBS IOPS 부족 |
| LoadBalancer pending | CCM 부재 |
D. 리포지토리 구조
infra/{versions,variables,network,security,nodes,dns,lb,outputs}.tf
infra/env/{case1,case2,case3}.tfvars
ansible/inventory/hosts.ini # OpenTofu가 자동 생성
ansible/roles/{os,runtime,k8s-pkg,init,join,addons}/
templates/{kubeadm-init.yaml.j2,kubeadm-join.yaml.j2}
addons/{ingress,storage,metrics,monitoring}/
docs/{runbook.md,troubleshooting.md}E. CKA 시험 도메인 ↔ Phase 매핑
| CKA 도메인 | 비중 | 해당 Phase |
|---|---|---|
| Cluster Architecture, Installation & Configuration | 25% | 8, 9, 10, 15 |
| Workloads & Scheduling | 15% | 13 |
| Services & Networking | 20% | 11, 12 |
| Storage | 10% | 12 |
| Troubleshooting | 30% | 14, 15 |
비중이 가장 큰 Troubleshooting이 Phase 14~15에 몰려 있음. 구축만 끝내고 덮으면 정작 배점이 큰 구간을 건너뛰는 셈임.
진행 원칙
- Case 1을 3번 짓고 부순 뒤 Case 2로 넘어간다
- Case 3은 Case 2가 손에 익은 다음 — 처음부터 8대는 원인 노드 특정이 어렵다
- 실습 세션이 끝나면 항상
tofu destroy - 막혔을 때 재구축이 부담스러우면 이미 학습 사이클이 깨진 것 → Phase 16으로 돌아간다