Skip to content
Published on

Kubespray 딥다이브 — Ansible로 프로덕션 온프렘 쿠버네티스 구축하기

Authors

들어가며

퍼블릭 클라우드의 매니지드 쿠버네티스(EKS, GKE, AKS)가 사실상 표준이 된 시대에도, 온프레미스 베어메탈에 쿠버네티스를 직접 구축해야 하는 조직은 여전히 많습니다. 오히려 최근에는 GPU 팜 구축 붐과 클라우드 비용 회귀(cloud repatriation) 흐름 때문에 온프렘 쿠버네티스 수요가 다시 늘고 있다는 것이 현장의 체감입니다.

문제는 "쿠버네티스를 직접 설치한다"는 일이 생각보다 훨씬 넓은 작업이라는 점입니다. kubeadm은 컨트롤 플레인 부트스트랩만 해결할 뿐, 그 앞뒤에 있는 OS 준비, 컨테이너 런타임 설치, etcd 토폴로지, CNI 배포, 로드밸런서, 인증서 갱신, 업그레이드 오케스트레이션은 전부 운영자의 몫으로 남습니다. 이 간극을 메우는 도구가 바로 Kubespray입니다.

이 글에서는 Kubespray가 정확히 무엇이고 내부적으로 어떻게 동작하는지, 프로덕션 클러스터를 구축할 때 인벤토리와 변수를 어떻게 설계해야 하는지, 그리고 구축 이후의 운영(노드 증설, 업그레이드, 백업, 하드닝)을 어떻게 플레이북으로 풀어내는지를 코드 중심으로 깊게 다룹니다. 마지막에는 Cluster API와의 관계를 정리해 "언제 Kubespray를 쓰고 언제 넘어가야 하는가"에 대한 판단 기준도 제시합니다. 기준 버전은 2026년 상반기 시점의 Kubespray v2.28 계열, Kubernetes v1.32 계열입니다.

온프렘 쿠버네티스가 여전히 필요한 이유

클라우드 우선 전략이 보편화됐음에도 온프렘/베어메탈 쿠버네티스가 사라지지 않는 이유는 명확합니다.

첫째, 데이터 주권과 규제입니다. 한국 금융권의 전자금융감독규정상 망분리 요건, 공공기관의 국가정보원 보안 가이드, 유럽의 GDPR 데이터 거주 요건 등은 워크로드가 어느 물리적 위치에서 실행되는지를 직접 규율합니다. 계정계와 연동되는 워크로드를 내부망에서 운영해야 하는 은행, 대외망과 내부망이 물리적으로 분리된 공공 환경에서는 퍼블릭 클라우드 자체가 선택지에서 빠지는 경우가 많습니다.

둘째, GPU 팜입니다. LLM 학습과 추론 인프라를 자체 구축하는 조직이 늘면서, 수십에서 수백 대의 GPU 서버를 쿠버네티스로 묶는 사례가 급증했습니다. 고가의 GPU를 클라우드에서 시간 단위로 빌리는 것보다 직접 보유해 상시 가동률을 높이는 편이 총소유비용(TCO) 측면에서 유리한 임계점이 분명히 존재합니다.

셋째, 비용입니다. 트래픽과 자원 사용량이 예측 가능하고 규모가 일정 수준을 넘는 워크로드는, 감가상각을 고려해도 자체 데이터센터나 코로케이션이 더 저렴해지는 구간이 옵니다. 특히 이그레스 트래픽 비용과 블록 스토리지 비용은 온프렘 회귀를 결정하게 만드는 단골 요인입니다.

넷째, 레이턴시와 특수 하드웨어입니다. 공장 라인의 엣지 컴퓨팅, 거래소와의 코로케이션이 필요한 트레이딩 시스템, SR-IOV나 DPDK 같은 특수 네트워크 장비를 쓰는 통신사 NFV 환경은 물리 인프라에 대한 직접 통제가 전제 조건입니다.

이런 환경에서 "어떻게 쿠버네티스를 설치하고 유지보수할 것인가"가 곧바로 다음 질문이 되고, 그 답의 후보들을 먼저 비교해 보겠습니다.

구축 도구 지형 — 무엇으로 깔 것인가

온프렘 쿠버네티스 구축 도구는 크게 다섯 갈래로 나뉩니다.

도구접근 방식대상 OSHA 컨트롤 플레인에어갭 지원적합한 환경
kubeadm 수동CLI로 단계별 부트스트랩범용 리눅스직접 구성직접 구성학습, 소규모, 완전 커스텀
KubesprayAnsible 플레이북으로 kubeadm 오케스트레이션Ubuntu, RHEL, Rocky, Debian 등 범용내장(다중 CP + LB 옵션)공식 지원(오프라인 미러)범용 베어메탈/VM, 이기종 OS, 세밀한 커스텀
kOps클러스터 수명주기 CLI클라우드 중심(AWS, GCE)내장제한적클라우드 셀프매니지드, 온프렘에는 부적합
RKE2 / k3s자체 배포판(단일 바이너리 지향)범용 리눅스내장양호엣지, 보안 중시(FIPS), Rancher 생태계
Talos Linux불변 OS 자체가 쿠버네티스 전용Talos 전용 OS내장양호OS까지 통제 가능한 신규 구축, 보안 극대화

선택 기준을 한 줄씩 요약하면 이렇습니다.

  • kubeadm 수동 구성은 쿠버네티스 내부를 이해하는 데 최고의 교재이지만, 수십 대 규모의 반복 가능한 구축에는 자동화 계층이 필요합니다.
  • kOps는 AWS 셀프매니지드 클러스터에 강하지만 베어메탈 스토리가 사실상 없습니다.
  • RKE2와 k3s는 설치가 가장 간단하고 보안 기본값이 좋지만, 업스트림 kubeadm 경로가 아닌 자체 배포판이라는 점, Rancher 생태계 의존이 생긴다는 점을 고려해야 합니다.
  • Talos는 SSH조차 없는 불변 OS로 가장 급진적이고 매력적인 접근이지만, 기존 OS 표준(보안 에이전트, 자산 관리 등)을 따라야 하는 조직에서는 도입 장벽이 높습니다.
  • Kubespray는 "이미 조직 표준 리눅스가 있고, 그 위에 업스트림 쿠버네티스를 세밀하게 통제하며 깔아야 한다"는 가장 흔한 엔터프라이즈 요구에 정확히 맞습니다. 기존에 Ansible을 쓰는 조직이라면 학습 곡선도 완만합니다.

이 글의 주인공은 마지막 선택지, Kubespray입니다.

Kubespray의 정체 — kubeadm을 감싼 Ansible 플레이북

Kubespray는 kubernetes-sigs 산하의 CNCF 프로젝트로, 본질은 "프로덕션급 쿠버네티스 클러스터를 구성하는 Ansible 플레이북과 롤(role)의 모음"입니다. 마법 같은 자체 엔진이 있는 것이 아니라, 검증된 구성 요소들을 Ansible로 오케스트레이션합니다.

  • 컨트롤 플레인 부트스트랩은 내부적으로 kubeadm을 호출합니다. 즉 Kubespray로 만든 클러스터는 kubeadm 클러스터이며, kubeadm의 인증서 체계와 업그레이드 경로를 그대로 따릅니다.
  • OS 준비(스왑 비활성화, 커널 모듈, sysctl), containerd 설치, etcd 클러스터 구성, CNI(Calico, Cilium, flannel 등) 배포, 애드온(CoreDNS, MetalLB, ingress-nginx 등) 설치까지 전 구간을 롤로 커버합니다.
  • 지원 범위가 넓습니다. 다양한 리눅스 배포판, 다중 CNI, 에어갭 환경, GPU 노드, 다양한 토폴로지(etcd 분리/통합)를 변수로 선택할 수 있습니다.

전체 흐름을 ASCII로 그리면 다음과 같습니다.

+--------------------------------------------------------------------+
| Ansible 컨트롤 노드 (운영자 PC 또는 배스천)                         |
|                                                                    |
|  inventory/prod/                                                   |
|   ├─ hosts.yaml            ← 노드 목록과 그룹(토폴로지) 정의        |
|   └─ group_vars/                                                   |
|       ├─ all/all.yml       ← 전역 변수(LB, 프록시, 레지스트리)      |
|       └─ k8s_cluster/                                              |
|           ├─ k8s-cluster.yml  ← 버전, CIDR, CNI, 프록시 모드        |
|           └─ addons.yml       ← ingress, metallb, cert-manager     |
|                                                                    |
|  ansible-playbook cluster.yml                                      |
|        │                                                           |
|        ▼                                                           |
|  [롤 실행 순서]                                                     |
|   1. kubernetes/preinstall  → OS 검증, sysctl, 커널 모듈, 스왑      |
|   2. container-engine       → containerd / crio 설치               |
|   3. download               → 바이너리·이미지 다운로드(또는 미러)    |
|   4. etcd                   → etcd 클러스터 구성·인증서             |
|   5. kubernetes/control-plane → kubeadm init / join (CP 노드)      |
|   6. kubernetes/node        → kubelet 구성, kubeadm join (워커)     |
|   7. network_plugin         → Calico / Cilium 매니페스트 적용       |
|   8. kubernetes-apps        → CoreDNS, 애드온, MetalLB 등           |
+--------------------------------------------------------------------+
        │ SSH (become: root)
+-------------------+  +-------------------+  +-------------------+
|  cp1 (CP+etcd)    |  |  cp2 (CP+etcd)    |  |  cp3 (CP+etcd)    |
+-------------------+  +-------------------+  +-------------------+
+-------------------+  +-------------------+  +-------------------+
|  worker1          |  |  worker2          |  |  workerN ...      |
+-------------------+  +-------------------+  +-------------------+

핵심 통찰은 두 가지입니다. 첫째, Kubespray는 선언적 컨트롤러가 아니라 절차적 실행 도구입니다. 플레이북을 실행하는 순간에만 상태를 수렴시키고, 실행하지 않으면 아무것도 하지 않습니다. 둘째, 인벤토리와 group_vars가 곧 클러스터의 형상 정의이므로, 이 디렉터리를 Git으로 관리하는 것이 운영의 출발점입니다.

사전 준비 — 노드, 네트워크, 접근

노드 요구사항

프로덕션 기준으로 다음을 권장합니다.

  • 컨트롤 플레인: 3대(쿼럼 유지를 위해 홀수), 최소 2 vCPU / 4GB RAM, 권장 4 vCPU / 8GB 이상. etcd를 같은 노드에 둘 경우 디스크는 반드시 로컬 SSD/NVMe로 구성합니다. etcd는 fsync 레이턴시에 극도로 민감해서, 느린 디스크는 클러스터 전체의 불안정으로 직결됩니다.
  • 워커: 워크로드에 따라 산정하되, kubelet과 시스템 데몬을 위한 예약(systemReserved, kubeReserved)을 감안합니다.
  • OS: Ubuntu 22.04/24.04 LTS, RHEL 9, Rocky 9 등 Kubespray 지원 매트릭스에 있는 배포판. 모든 노드의 OS와 커널 버전을 통일하는 것이 트러블슈팅 비용을 크게 줄입니다.
  • 공통 조건: 스왑 비활성화(Kubespray가 처리하지만 fstab 영구 설정 확인), 고유한 hostname과 MAC, product_uuid, 시간 동기화(chrony), 그리고 br_netfilter와 overlay 커널 모듈.

컨테이너 런타임은 containerd가 기본값이며 특별한 이유가 없다면 그대로 둡니다. CRI-O도 선택할 수 있지만 지원 폭은 containerd가 가장 넓습니다.

네트워크 계획 — CIDR 설계

구축 후 변경이 사실상 불가능한 값들이므로 가장 신중해야 하는 부분입니다. 세 개의 대역이 서로, 그리고 사내망과 절대 겹치지 않아야 합니다.

노드 네트워크   : 10.10.0.0/24    (물리 서버 IP — 사내 IPAM에서 할당)
파드 CIDR       : 10.233.64.0/18  (kube_pods_subnet — 기본값)
서비스 CIDR     : 10.233.0.0/18   (kube_service_addresses — 기본값)

노드당 파드 대역: /24 (kube_network_node_prefix)
→ 노드당 최대 약 110 파드, /18 기준 최대 64개 노드

규모 산정 공식은 단순합니다. 파드 CIDR 크기에서 노드당 prefix를 빼면 수용 가능한 노드 수가 나옵니다. 예를 들어 /18 파드 대역에 노드당 /24를 주면 2의 6승, 즉 64개 노드가 상한입니다. 300노드 규모를 계획한다면 파드 대역을 /16으로 넓히거나 노드당 prefix를 /25로 줄이는 결정을 구축 전에 해야 합니다. 또한 사내망과 겹치면 해당 대역의 사내 시스템과 통신이 단절되므로, 네트워크 팀과 함께 IPAM 문서에 이 대역들을 정식 등록해 두기를 권합니다.

컨트롤 플레인 HA — API 서버 앞의 VIP

컨트롤 플레인이 3대라도 클라이언트(kubelet, kubectl, CI)가 바라보는 엔드포인트가 하나의 노드 IP라면 그 노드가 단일 장애점입니다. 해법은 세 가지입니다.

  1. kube-vip: 컨트롤 플레인 노드 위에 스태틱 파드로 떠서 ARP(또는 BGP)로 VIP를 광고합니다. 외부 장비가 필요 없어 베어메탈에서 가장 간편합니다. Kubespray가 kube_vip_enabled 변수로 직접 지원합니다.
  2. HAProxy + keepalived: 별도 LB 노드 2대에 HAProxy로 6443 포트를 백엔드 CP 3대에 분산하고, keepalived의 VRRP로 VIP를 페일오버합니다. 전통적이고 검증된 방식이며, LB 계층을 네트워크 팀이 관리하는 조직에 적합합니다.
  3. 외부 하드웨어 LB: 이미 L4 장비(F5 등)가 있다면 그대로 활용합니다.

Kubespray에는 또 하나의 내장 장치가 있습니다. loadbalancer_apiserver_localhost 옵션(기본 활성)은 각 노드에 nginx-proxy 스태틱 파드를 띄워 localhost에서 CP 3대로 분산합니다. 즉 클러스터 내부 컴포넌트의 HA는 외부 LB 없이도 어느 정도 확보되지만, 클러스터 외부에서 접근하는 kubectl과 CI를 위해서는 여전히 VIP가 필요합니다.

SSH와 sudo

Ansible이 모든 노드에 SSH로 접속해 root 권한으로 작업하므로 다음을 준비합니다.

# 컨트롤 노드에서 키 생성 후 전 노드에 배포
ssh-keygen -t ed25519 -f ~/.ssh/kubespray_ed25519
for h in 10.10.0.11 10.10.0.12 10.10.0.13 10.10.0.21 10.10.0.22; do
  ssh-copy-id -i ~/.ssh/kubespray_ed25519.pub deploy@"$h"
done

# deploy 계정에 패스워드 없는 sudo 부여 (각 노드)
echo 'deploy ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/deploy

에어갭(폐쇄망) 환경 준비 개요

망분리 환경이라면 외부 인터넷에서 받아야 하는 세 종류의 산출물을 내부 미러로 옮겨야 합니다.

  1. 컨테이너 이미지 → 내부 레지스트리(Harbor 등)
  2. 바이너리 파일(kubeadm, kubelet, etcd, CNI 플러그인, crictl 등) → 내부 HTTP 서버
  3. OS 패키지(containerd 의존성 등) → 내부 yum/apt 저장소

Kubespray는 contrib/offline 디렉터리에 필요한 이미지·파일 목록을 뽑아 주는 스크립트를 제공하므로, 인터넷이 되는 준비 구역에서 목록을 생성해 미러를 채우고, 폐쇄망 인벤토리에서는 다운로드 URL 변수를 내부 미러로 바꾸는 방식으로 진행합니다. 구체적인 변수는 뒤의 커스터마이징 절에서 다룹니다.

실습 — 클러스터 구축 전체 흐름

1단계: Kubespray 준비와 인벤토리 생성

# 반드시 릴리스 태그를 체크아웃 — master 브랜치로 프로덕션 구축 금지
git clone --branch v2.28.0 https://github.com/kubernetes-sigs/kubespray.git
cd kubespray

# Ansible 버전 충돌을 피하기 위해 전용 가상환경 사용
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -r requirements.txt

# 샘플 인벤토리를 복사해 프로덕션 인벤토리 생성
cp -rfp inventory/sample inventory/prod

릴리스 태그 고정은 단순한 권고가 아닙니다. Kubespray 릴리스마다 지원하는 쿠버네티스 마이너 버전과 컴포넌트 버전 매트릭스가 고정되어 있고, 이 매트릭스를 벗어난 조합은 테스트되지 않은 영역입니다.

2단계: hosts.yaml — 토폴로지 정의

컨트롤 플레인 3대(etcd 동거)와 워커 3대의 표준 구성입니다.

# inventory/prod/hosts.yaml
all:
  hosts:
    cp1:
      ansible_host: 10.10.0.11
      ip: 10.10.0.11          # kubelet/etcd가 바인딩할 내부 IP
      access_ip: 10.10.0.11   # 다른 노드가 이 노드에 접근할 IP
    cp2:
      ansible_host: 10.10.0.12
      ip: 10.10.0.12
      access_ip: 10.10.0.12
    cp3:
      ansible_host: 10.10.0.13
      ip: 10.10.0.13
      access_ip: 10.10.0.13
    worker1:
      ansible_host: 10.10.0.21
      ip: 10.10.0.21
    worker2:
      ansible_host: 10.10.0.22
      ip: 10.10.0.22
    worker3:
      ansible_host: 10.10.0.23
      ip: 10.10.0.23
  children:
    kube_control_plane:
      hosts:
        cp1:
        cp2:
        cp3:
    kube_node:
      hosts:
        worker1:
        worker2:
        worker3:
    etcd:
      hosts:
        cp1:
        cp2:
        cp3:
    k8s_cluster:
      children:
        kube_control_plane:
        kube_node:
    calico_rr:
      hosts: {}

토폴로지 설계 포인트는 다음과 같습니다.

  • etcd 그룹을 컨트롤 플레인과 분리해 별도 노드 3대에 둘 수도 있습니다(스택드 vs 외부 etcd). 대규모 클러스터(노드 수백 대)나 API 서버 부하가 큰 환경에서는 외부 etcd가 안정적이지만, 관리 노드가 6대로 늘어납니다. 50노드 이하라면 스택드 구성으로 충분한 경우가 대부분입니다.
  • ip와 access_ip를 명시하는 습관을 들이세요. NIC가 여러 개인 서버에서 Ansible이 잘못된 인터페이스 IP를 자동 감지해 etcd가 엉뚱한 대역에 바인딩되는 사고가 흔합니다.
  • 그룹 이름(kube_control_plane, kube_node, etcd, k8s_cluster)은 Kubespray 롤이 참조하는 예약어이므로 바꾸면 안 됩니다.

3단계: group_vars 해부 — k8s-cluster.yml

클러스터의 정체성을 결정하는 파일입니다. 핵심 변수만 추리면 다음과 같습니다.

# inventory/prod/group_vars/k8s_cluster/k8s-cluster.yml

# 쿠버네티스 버전 — 해당 Kubespray 릴리스가 지원하는 범위 내에서만
kube_version: "1.32.5"

# 네트워크 플러그인: calico, cilium, flannel, kube-ovn 등
kube_network_plugin: calico

# CIDR — 구축 후 변경 불가, 사내망과 중복 금지
kube_service_addresses: 10.233.0.0/18
kube_pods_subnet: 10.233.64.0/18
kube_network_node_prefix: 24

# kube-proxy 모드: ipvs 권장 (서비스 수천 개 규모에서 iptables보다 유리)
kube_proxy_mode: ipvs

# 컨테이너 런타임
container_manager: containerd

# DNS
dns_mode: coredns
enable_nodelocaldns: true        # 노드 로컬 DNS 캐시 — 대규모에서 필수급

# 클러스터 이름 (내부 도메인)
cluster_name: cluster.local

# 구축 완료 후 컨트롤 노드에 admin kubeconfig 복사
kubeconfig_localhost: true

# 인증서 자동 갱신 (systemd timer로 매월 갱신 시도)
auto_renew_certificates: true

변수별 판단 기준을 짚어 보겠습니다.

  • kube_network_plugin: 기본값 Calico는 BGP 기반 라우팅과 네트워크 폴리시로 검증된 선택입니다. eBPF 데이터플레인, Hubble 관측성, L7 폴리시가 필요하면 Cilium을 선택합니다. 중요한 것은 이 선택이 사실상 영구적이라는 점입니다. 운영 중인 클러스터에서 CNI를 교체하는 것은 Kubespray가 지원하는 시나리오가 아닙니다.
  • kube_proxy_mode: ipvs는 서비스 수가 많을 때 iptables 모드의 선형 탐색 비용을 피할 수 있습니다. 참고로 Cilium을 kube-proxy replacement 모드로 쓰면 kube-proxy 자체를 제거하는 구성도 가능합니다.
  • enable_nodelocaldns: 파드의 DNS 질의를 노드 로컬 캐시가 받아 CoreDNS 부하와 conntrack 경합을 줄입니다. DNS 타임아웃으로 고생해 본 운영자라면 기본 활성화를 권합니다.

API 서버 엔드포인트(VIP)는 all 그룹 변수에 정의합니다.

# inventory/prod/group_vars/all/all.yml

# 외부 LB(HAProxy+keepalived 또는 하드웨어 LB)를 쓰는 경우
apiserver_loadbalancer_domain_name: "k8s-api.prod.internal"
loadbalancer_apiserver:
  address: 10.10.0.100   # VIP
  port: 6443

# 각 노드의 nginx-proxy 로컬 LB (기본 활성 — 내부 컴포넌트 HA)
loadbalancer_apiserver_localhost: true

kube-vip를 쓰는 경우에는 별도 LB 노드 없이 다음과 같이 설정합니다.

# kube-vip로 컨트롤 플레인 VIP 구성 (ARP 모드)
kube_vip_enabled: true
kube_vip_controlplane_enabled: true
kube_vip_arp_enabled: true
kube_vip_address: 10.10.0.100
loadbalancer_apiserver:
  address: 10.10.0.100
  port: 6443

4단계: group_vars 해부 — addons.yml

클러스터 위에 올라갈 기본 애드온을 선택합니다.

# inventory/prod/group_vars/k8s_cluster/addons.yml

helm_enabled: true
metrics_server_enabled: true

# Ingress 컨트롤러
ingress_nginx_enabled: true
ingress_nginx_host_network: false

# 베어메탈 LoadBalancer 서비스 구현 — MetalLB
metallb_enabled: true
metallb_speaker_enabled: true
metallb_config:
  address_pools:
    primary:
      ip_range:
        - 10.10.0.150-10.10.0.180
      auto_assign: true
  layer2:
    - primary

# 인증서 자동화
cert_manager_enabled: true

# 간단한 로컬 PV가 필요하면
local_path_provisioner_enabled: true

베어메탈에서는 클라우드처럼 LoadBalancer 타입 서비스를 만들어 주는 주체가 없으므로 MetalLB가 그 역할을 합니다. L2 모드는 설정이 단순한 대신 단일 노드로 트래픽이 수렴하는 한계가 있고, BGP 모드는 ToR 스위치와의 피어링이 필요하지만 진정한 분산이 가능합니다. 네트워크 팀과의 협의가 가능하다면 BGP 모드를 검토할 가치가 있습니다. 참고로 애드온은 "초기 부트스트랩만 Kubespray로 깔고, 이후 수명주기는 Helm/GitOps로 직접 관리"하는 전략도 널리 쓰입니다. Kubespray 애드온 변수는 편리하지만 차트 버전 선택의 자유도가 낮기 때문입니다.

5단계: 실행 — cluster.yml

# 사전 연결성 확인
ansible -i inventory/prod/hosts.yaml all -m ping \
  --private-key ~/.ssh/kubespray_ed25519 -u deploy -b

# 본 실행 — 6노드 기준 약 20~40분 소요
ansible-playbook -i inventory/prod/hosts.yaml \
  --private-key ~/.ssh/kubespray_ed25519 -u deploy \
  --become --become-user=root \
  cluster.yml

실행 중 화면에 흐르는 단계는 앞의 아키텍처 다이어그램의 롤 순서와 일치합니다. 눈여겨볼 구간은 세 곳입니다. download 롤에서 실패하면 네트워크/프록시/미러 문제이고, etcd 롤에서 멈추면 노드 간 2379/2380 포트 통신이나 ip 변수 설정 문제이며, control-plane 롤의 kubeadm init 이후 health check에서 실패하면 VIP나 인증서 SAN 구성 문제일 가능성이 높습니다.

6단계: 검증

# kubeconfig_localhost: true 였다면 인벤토리 아래에 admin.conf가 복사됨
export KUBECONFIG=$PWD/inventory/prod/artifacts/admin.conf

kubectl get nodes -o wide
# NAME      STATUS   ROLES           VERSION   INTERNAL-IP
# cp1       Ready    control-plane   v1.32.5   10.10.0.11
# cp2       Ready    control-plane   v1.32.5   10.10.0.12
# cp3       Ready    control-plane   v1.32.5   10.10.0.13
# worker1   Ready    <none>          v1.32.5   10.10.0.21
# ...

# 컨트롤 플레인 파드와 CNI 상태
kubectl get pods -n kube-system

# etcd 멤버와 헬스 (cp1에서)
sudo ETCDCTL_API=3 etcdctl \
  --endpoints=https://10.10.0.11:2379 \
  --cacert=/etc/ssl/etcd/ssl/ca.pem \
  --cert=/etc/ssl/etcd/ssl/node-cp1.pem \
  --key=/etc/ssl/etcd/ssl/node-cp1-key.pem \
  endpoint health --cluster

# 스모크 테스트: 배포 → 노출 → DNS → 삭제
kubectl create deployment nginx --image=nginx --replicas=3
kubectl expose deployment nginx --port=80
kubectl run dns-test --rm -it --image=busybox:1.36 --restart=Never \
  -- nslookup nginx.default.svc.cluster.local
kubectl delete deployment nginx svc/nginx

여기에 더해 VIP 페일오버 테스트(CP 노드 1대 강제 종료 후 kubectl 동작 확인)와 워커 1대 드레인 테스트까지 통과해야 프로덕션 인수 기준을 충족한다고 보는 것이 안전합니다.

운영 작업별 플레이북

Kubespray의 진짜 가치는 Day-2 운영이 전부 플레이북으로 정형화되어 있다는 점입니다.

노드 추가 — scale.yml

# 1) hosts.yaml에 worker4를 추가한 뒤
# 2) 신규 노드만 대상으로 scale 실행
ansible-playbook -i inventory/prod/hosts.yaml \
  --become --limit=worker4 \
  scale.yml

scale.yml은 cluster.yml에서 기존 노드를 재구성하는 단계를 건너뛰고 신규 노드의 준비와 kubeadm join만 수행하므로 빠르고 안전합니다. 주의할 점은 limit 옵션을 쓰더라도 Ansible이 etcd 그룹 등 다른 호스트의 팩트(facts)를 필요로 한다는 것입니다. 팩트 수집이 실패하면 플레이북이 깨지므로, 먼저 전체 노드 팩트를 갱신하는 습관을 권합니다.

# limit 실행 전 전체 팩트 갱신
ansible-playbook -i inventory/prod/hosts.yaml --become playbooks/facts.yml

컨트롤 플레인 노드 추가는 scale.yml이 아니라 cluster.yml을 사용해야 하며, 추가 후 인증서 SAN과 LB 백엔드 목록 갱신을 잊지 말아야 합니다.

노드 제거 — remove-node.yml

# 드레인 → 클러스터에서 제거 → 노드 정리까지 한 번에
ansible-playbook -i inventory/prod/hosts.yaml \
  --become \
  -e node=worker3 \
  remove-node.yml

remove-node.yml은 대상 노드를 cordon/drain하고, kubectl delete node와 노드 측 kubelet/런타임 정리를 수행합니다. 노드가 이미 죽어서 접속 불가능한 경우에는 reset_nodes=false 변수를 함께 줘서 노드 측 정리를 건너뛰고 클러스터 메타데이터만 제거할 수 있습니다. 제거가 끝난 뒤 hosts.yaml에서도 해당 노드를 지워 인벤토리와 실제 상태의 일치를 유지하세요. 이 동기화를 빼먹는 것이 인벤토리 드리프트의 시작입니다.

업그레이드 — upgrade-cluster.yml

가장 신중해야 하는 작업입니다. 원칙부터 정리합니다.

  1. 마이너 버전 스킵 금지. 1.30에서 1.32로 바로 갈 수 없습니다. kubeadm과 쿠버네티스의 버전 스큐 정책에 따라 1.30 → 1.31 → 1.32 순서로 단계를 밟아야 하며, Kubespray 릴리스도 각 단계에 맞는 태그로 갈아타야 합니다.
  2. Kubespray 버전과 클러스터 버전의 짝을 기록·유지하세요. 인벤토리 저장소에 "이 클러스터는 v2.27.1 태그로 1.31.4를 운영 중"이라는 사실이 남아 있어야 합니다.
  3. 업그레이드 전에 etcd 백업은 무조건입니다.
# 예: 1.31.x → 1.32.x
cd kubespray && git checkout v2.28.0
pip install -r requirements.txt   # 요구 Ansible 버전도 함께 바뀜

ansible-playbook -i inventory/prod/hosts.yaml \
  --become \
  -e kube_version=1.32.5 \
  upgrade-cluster.yml

upgrade-cluster.yml의 동작 방식이 무중단의 핵심입니다.

  • 컨트롤 플레인과 etcd를 먼저, 한 대씩 순차로 업그레이드합니다.
  • 워커는 드레인 → kubelet/런타임 업그레이드 → uncordon 순서로 진행하며, 동시에 처리할 노드 수는 serial 변수(기본값 20%)로 제어합니다. 보수적으로 가려면 serial=1을 줍니다.
  • 드레인 동작은 drain_grace_period, drain_timeout, drain_retries 변수로 조정합니다. PodDisruptionBudget이 너무 빡빡하면 드레인이 무한 대기하므로, 업그레이드 전 PDB 점검은 필수입니다.
# 한 대씩, 드레인 타임아웃을 넉넉히
ansible-playbook -i inventory/prod/hosts.yaml --become \
  -e kube_version=1.32.5 \
  -e serial=1 \
  -e drain_timeout=600s \
  -e drain_grace_period=120 \
  upgrade-cluster.yml

무중단을 위한 애플리케이션 측 요건도 같이 챙겨야 합니다. 레플리카 2 이상, 적절한 PDB, preStop 훅과 graceful shutdown, 그리고 단일 레플리카 StatefulSet 같은 드레인 블로커의 사전 식별이 그것입니다.

reset.yml — 마지막 수단

# 클러스터를 노드에서 완전히 철거 — 디스크의 etcd 데이터까지 삭제됨
ansible-playbook -i inventory/prod/hosts.yaml --become reset.yml

reset.yml은 쿠버네티스와 etcd, CNI 설정, 컨테이너 데이터를 노드에서 제거합니다. 운영 클러스터에서 "일부 노드만 초기화하려고" limit 없이 실행하는 사고가 실제로 일어납니다. 프로덕션 인벤토리에서는 실행 전 확인 프롬프트가 있긴 하지만, CI에서 자동 승인으로 돌리는 구성은 절대 금물입니다.

etcd 백업과 복구

Kubespray가 백업을 대신해 주지 않으므로 직접 체계를 갖춰야 합니다.

# 백업 (etcd 노드에서 — cron/systemd timer로 주기 실행)
sudo ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-snap-20260613.db \
  --endpoints=https://10.10.0.11:2379 \
  --cacert=/etc/ssl/etcd/ssl/ca.pem \
  --cert=/etc/ssl/etcd/ssl/node-cp1.pem \
  --key=/etc/ssl/etcd/ssl/node-cp1-key.pem

# 무결성 확인
ETCDCTL_API=3 etcdctl --write-out=table snapshot status /backup/etcd-snap-20260613.db

스냅샷 파일은 반드시 클러스터 외부 스토리지로 반출하고, 분기마다 복구 리허설(스냅샷 → etcdctl snapshot restore → 신규 데이터 디렉터리로 etcd 기동)을 수행해 백업이 실제로 복구 가능한지 검증해야 합니다. 백업은 있는데 복구 절차를 한 번도 연습하지 않은 조직이 의외로 많습니다.

커스터마이징 — 프로덕션에서 반드시 만나는 것들

인증서 관리

kubeadm 클러스터의 컨트롤 플레인 인증서는 기본 1년 유효입니다. Kubespray에서는 다음 두 변수로 관리합니다.

# 매월 1회 systemd timer(k8s-certs-renew.timer)가 만료 임박 인증서를 갱신
auto_renew_certificates: true

# 필요 시 플레이북 실행에서 강제 재발급
# -e force_certificate_regeneration=true

auto_renew_certificates를 켜 두면 갱신 누락으로 인한 "1년 뒤 클러스터 전체 인증 불능" 사고를 예방할 수 있습니다. 단, CA 자체의 유효기간(기본 10년)과 외부 LB에 박아 둔 인증서는 별도 관리 대상입니다. 만료 모니터링(예: x509 exporter)을 함께 두는 것을 권합니다.

프라이빗 레지스트리와 에어갭 상세 설정

폐쇄망 구축의 핵심은 모든 다운로드 경로를 내부 미러로 바꾸는 변수 세트입니다.

# inventory/prod/group_vars/all/offline.yml
registry_host: "harbor.internal:443/k8s-mirror"
files_repo: "https://mirror.internal/kubespray-files"

# 컨테이너 이미지 저장소를 전부 내부 레지스트리로
kube_image_repo: "{{ registry_host }}"
gcr_image_repo: "{{ registry_host }}"
github_image_repo: "{{ registry_host }}"
docker_image_repo: "{{ registry_host }}"
quay_image_repo: "{{ registry_host }}"

# 바이너리 다운로드 URL을 내부 파일 서버로
kubeadm_download_url: "{{ files_repo }}/kubeadm/{{ kube_version }}/kubeadm"
kubelet_download_url: "{{ files_repo }}/kubelet/{{ kube_version }}/kubelet"
kubectl_download_url: "{{ files_repo }}/kubectl/{{ kube_version }}/kubectl"
etcd_download_url: "{{ files_repo }}/etcd/etcd-{{ etcd_version }}-linux-{{ host_architecture }}.tar.gz"
cni_download_url: "{{ files_repo }}/cni/cni-plugins-linux-{{ host_architecture }}-{{ cni_version }}.tgz"
crictl_download_url: "{{ files_repo }}/crictl/crictl-{{ crictl_version }}-linux-{{ host_architecture }}.tar.gz"
runc_download_url: "{{ files_repo }}/runc/{{ runc_version }}/runc.{{ host_architecture }}"
containerd_download_url: "{{ files_repo }}/containerd/containerd-{{ containerd_version }}-linux-{{ host_architecture }}.tar.gz"

# 사설 CA를 쓰는 레지스트리라면 containerd에 신뢰 등록
containerd_registries_mirrors:
  - prefix: "harbor.internal"
    mirrors:
      - host: "https://harbor.internal"
        capabilities: ["pull", "resolve"]
        skip_verify: false

준비 구역에서는 contrib/offline의 generate_list.sh로 해당 릴리스가 요구하는 파일·이미지 전체 목록을 추출하고, manage-offline-container-images.sh로 이미지를 한꺼번에 받아 내부 레지스트리에 푸시합니다. 운영 팁 하나를 덧붙이면, 미러 채우기 작업 자체도 CI 파이프라인으로 만들어 두세요. 업그레이드 때마다 수작업으로 목록을 갱신하다 누락이 생기는 것이 폐쇄망 업그레이드 실패의 최다 원인입니다.

sysctl과 커널 튜닝 주입

노드 OS 튜닝을 별도 Ansible 롤로 관리해도 되지만, Kubespray 변수로 함께 주입하면 형상이 한곳에 모입니다.

# inventory/prod/group_vars/k8s_cluster/k8s-cluster.yml
additional_sysctl:
  - name: net.core.somaxconn
    value: 65535
  - name: net.ipv4.tcp_max_syn_backlog
    value: 65535
  - name: fs.inotify.max_user_instances
    value: 8192
  - name: fs.inotify.max_user_watches
    value: 1048576

# kubelet이 노드 자원을 예약하도록
kube_reserved: true
kube_memory_reserved: 512Mi
kube_cpu_reserved: 200m
system_reserved: true
system_memory_reserved: 1Gi
system_cpu_reserved: 500m

inotify 한도는 로그 수집기와 많은 파드를 돌릴 때 반드시 만나는 병목이므로 초기에 올려 두는 것을 권합니다.

추가 매니페스트와 GitOps 연계

Kubespray에는 임의의 매니페스트를 주입하는 범용 훅이 마땅치 않습니다. 실무 패턴은 명확합니다. Kubespray의 책임을 "노드 OS부터 CNI까지"로 한정하고, 그 위의 모든 것(모니터링, 로깅, 인그레스 상세 설정, 애플리케이션)은 Argo CD나 Flux 같은 GitOps 도구로 관리하는 것입니다. 구축 파이프라인 마지막 단계에서 Argo CD 부트스트랩 매니페스트 하나만 kubectl apply 하도록 구성하면, 이후 클러스터 내부 상태는 전부 Git이 진실의 원천이 됩니다.

프로덕션 하드닝

CIS 벤치마크와 kube-bench

CIS Kubernetes Benchmark 정합성은 금융권·공공 보안성 심의에서 단골 요구사항입니다. Kubespray 기본값은 CIS를 완전히 만족하지 않으므로, 구축 후 kube-bench를 돌려 격차를 확인하고 변수로 보정하는 사이클을 권합니다. 자주 보정하는 항목은 다음과 같습니다.

# API 서버 하드닝
kube_apiserver_request_timeout: 120s
kube_apiserver_enable_admission_plugins:
  - NodeRestriction
  - AlwaysPullImages
  - EventRateLimit
kube_apiserver_admission_event_rate_limits:
  limit_1:
    type: Namespace
    qps: 50
    burst: 100
    cache_size: 2000
kube_profiling: false

# kubelet 하드닝
kubelet_protect_kernel_defaults: true
kubelet_event_record_qps: 1
kubelet_streaming_connection_idle_timeout: 5m
kubelet_make_iptables_util_chains: true

# 익명 인증 차단은 kubeadm 기본값으로 처리되지만 명시적으로
kube_api_anonymous_auth: false

kube-bench 결과를 그대로 다 끄려고 하기보다, 각 항목이 자신의 워크로드에 주는 영향(예: AlwaysPullImages는 레지스트리 부하 증가)을 따져 예외 사유를 문서화하는 접근이 현실적입니다.

감사 로그 (audit log)

kubernetes_audit: true
audit_log_path: /var/log/kubernetes/audit/audit.log
audit_log_maxage: 30        # 보존 일수
audit_log_maxbackups: 10
audit_log_maxsize: 100      # MB
# 기본 정책 대신 커스텀 정책 파일을 쓰려면
# audit_policy_custom_rules에 규칙을 정의

감사 로그는 보안 사고 조사에서 사실상 유일한 1차 증거이므로, 노드 로컬에만 두지 말고 중앙 로그 시스템으로 수집하는 파이프라인까지가 한 세트입니다.

Secrets 저장 시 암호화 (encryption at rest)

etcd에 평문으로 저장되는 Secret을 암호화합니다.

kube_encrypt_secret_data: true
# 기본 프로바이더는 secretbox — aescbc 등으로 변경 가능
kube_encryption_algorithm: "secretbox"
kube_encryption_resources: [secrets]

이 설정은 etcd 스냅샷이 유출되어도 Secret 원문이 노출되지 않게 하는 최소 방어선입니다. 키 자체가 컨트롤 플레인 노드 디스크에 있다는 한계는 남으므로, 더 높은 요구 수준에서는 외부 KMS 연동을 검토합니다.

Pod Security Admission 기본값

PodSecurityPolicy 폐지 이후 표준은 PSA입니다. Kubespray 변수로 클러스터 전역 기본값을 지정할 수 있습니다.

kube_pod_security_use_default: true
kube_pod_security_default_enforce: baseline   # 신규 네임스페이스 기본 적용
kube_pod_security_default_audit: restricted
kube_pod_security_default_warn: restricted
kube_pod_security_exempt_namespaces:
  - kube-system

enforce를 처음부터 restricted로 올리면 기존 워크로드가 대거 거부될 수 있으므로, audit/warn으로 위반을 가시화한 뒤 단계적으로 enforce를 올리는 순서가 안전합니다.

CI/CD 통합과 대규모 운영

인벤토리를 Git으로, 실행을 파이프라인으로

권장 저장소 구조는 다음과 같습니다.

k8s-clusters/                  ← 사내 Git 저장소
├─ README.md                   ← 클러스터·Kubespray 버전 대응표
├─ clusters/
│   ├─ prod-seoul/
│   │   ├─ hosts.yaml
│   │   └─ group_vars/ ...
│   └─ stage-seoul/
│       ├─ hosts.yaml
│       └─ group_vars/ ...
└─ pipelines/
    └─ run-kubespray.yaml      ← CI 정의

Kubespray 본체는 서브모듈이나 파이프라인 내 git clone(태그 고정)으로 가져오고, 인벤토리 변경은 전부 PR 리뷰를 거치게 합니다. 실행 파이프라인의 골격 예시입니다.

# GitLab CI 예시 — 개념 골격
stages: [lint, diff, apply]

lint:
  stage: lint
  script:
    - ansible-lint clusters/prod-seoul || true
    - python3 scripts/validate_inventory.py clusters/prod-seoul

apply-prod:
  stage: apply
  when: manual            # 프로덕션은 반드시 수동 승인
  script:
    - git clone --branch v2.28.0 --depth 1
        https://github.com/kubernetes-sigs/kubespray.git
    - pip install -r kubespray/requirements.txt
    - ansible-playbook -i clusters/prod-seoul/hosts.yaml
        --become kubespray/cluster.yml
  environment: prod-seoul

멱등성의 활용과 한계

Ansible의 멱등성 덕분에 cluster.yml 재실행은 "변경된 부분만 수렴"이 기본 동작이고, 실패 지점부터의 재개도 같은 명령 재실행으로 충분한 경우가 많습니다. 그러나 한계를 정확히 알아야 합니다.

  • 변수에서 항목을 "제거"해도 노드의 기존 설정이 "철회"되지는 않습니다. 예를 들어 애드온을 enabled false로 바꿔도 이미 배포된 리소스가 자동 삭제되지 않는 경우가 있습니다. 절차적 도구의 본질적 한계입니다.
  • check 모드(드라이런)는 Kubespray에서 신뢰성이 낮습니다. 많은 태스크가 이전 태스크의 실제 결과에 의존하기 때문입니다. "diff를 보고 승인"하는 Terraform식 워크플로는 기대할 수 없으므로, 스테이징 클러스터에 먼저 적용하는 것으로 대체합니다.
  • 플레이북 실행 도중의 다른 변경(수동 kubectl 작업 등)과 충돌할 수 있으므로, 실행 시간대를 변경 동결 윈도와 맞추는 운영 규율이 필요합니다.

대규모 운영 팁 — 병렬도와 부분 실행

# ansible.cfg
[defaults]
forks = 50                 # 기본 5 — 수십 노드면 반드시 상향
strategy = linear
[ssh_connection]
pipelining = True          # SSH 왕복 감소 — 체감 효과 큼
ssh_args = -o ControlMaster=auto -o ControlPersist=30m

실행 시간 감각으로는, 6노드 신규 구축이 20~40분, 50노드 구축이 1시간 이상, 업그레이드는 드레인 시간 때문에 노드 수에 거의 선형으로 비례합니다. 부분 실행 도구는 두 가지입니다.

# 특정 노드만 — 반드시 facts 갱신 후
ansible-playbook -i inventory/prod/hosts.yaml --become playbooks/facts.yml
ansible-playbook -i inventory/prod/hosts.yaml --become \
  --limit=worker7 cluster.yml

# 특정 컴포넌트 태그만 — 예: CoreDNS 설정 변경 반영
ansible-playbook -i inventory/prod/hosts.yaml --become \
  --tags=coredns cluster.yml

# 다운로드 단계를 건너뛰어 반복 실행 단축
ansible-playbook -i inventory/prod/hosts.yaml --become \
  --skip-tags=download cluster.yml

태그와 limit은 강력하지만 태스크 간 의존성을 건너뛸 위험이 있으므로, "스테이징에서 같은 태그 조합을 먼저 실행"하는 원칙을 함께 두는 것이 좋습니다.

Cluster API와의 비교 — 그리고 공존

Cluster API(CAPI)는 쿠버네티스 클러스터 자체를 쿠버네티스 리소스(Cluster, MachineDeployment 등)로 선언하고, 매니지먼트 클러스터의 컨트롤러가 지속적으로 상태를 수렴시키는 SIG 프로젝트입니다. Kubespray와는 철학이 정반대입니다.

관점KubesprayCluster API
패러다임절차적 — 실행 시점에만 수렴선언적 — 컨트롤러가 상시 수렴
필요 인프라Ansible 컨트롤 노드만매니지먼트 클러스터 + 인프라 프로바이더
베어메탈 지원SSH만 되면 어디든Metal3(IPMI/Redfish) 또는 BYOH 프로바이더 필요
노드 복구수동(플레이북 재실행)MachineHealthCheck로 자동 재생성 가능
클러스터 다수 운영인벤토리 수만큼 반복 실행플릿 관리에 강함
OS 커스텀매우 유연(기존 OS 위에 적용)이미지 빌드 파이프라인 필요
학습 곡선Ansible 경험자에게 완만CRD·컨트롤러 모델 이해 필요

판단 기준을 정리하면 이렇습니다.

  • 클러스터가 한 자릿수이고, 서버가 수동 프로비저닝되며, 조직에 Ansible 역량이 있다면 Kubespray가 단순하고 충분합니다.
  • 클러스터를 수십 개 찍어내야 하고, IPMI/Redfish로 베어메탈 수명주기까지 자동화할 수 있다면 CAPI + Metal3가 장기적으로 우월합니다.
  • 현실적인 하이브리드도 있습니다. CAPI의 매니지먼트 클러스터 자체는 닭과 달걀 문제 때문에 어딘가에서 부트스트랩되어야 하는데, 이 첫 클러스터를 Kubespray로 구축하고 나머지 플릿을 CAPI로 관리하는 패턴입니다. 또는 기존 Kubespray 클러스터를 유지하면서 신규 클러스터부터 CAPI로 전환하는 점진 이행도 흔합니다.

흔한 함정과 트러블슈팅

함정 목록

  1. 인벤토리 드리프트: 누군가 노드에서 수동으로 설정을 바꾸거나, remove-node 후 hosts.yaml을 안 고치면, 다음 플레이북 실행이 예상 밖의 동작을 합니다. 인벤토리 변경은 PR로만, 노드 수동 변경은 금지라는 규율이 답입니다.
  2. OS 패치와의 충돌: unattended-upgrades가 containerd를 멋대로 올리거나 커널 업그레이드 후 재부팅으로 커널 모듈 설정이 빠지는 사고가 잦습니다. 쿠버네티스 관련 패키지는 hold로 고정하고, OS 패치는 드레인과 함께 통제된 절차로 수행해야 합니다.
  3. CNI 변경 불가: kube_network_plugin을 바꿔 cluster.yml을 다시 돌리면 클러스터가 망가집니다. CNI 교체가 정말 필요하면 신규 클러스터 구축 후 워크로드 이전이 정석입니다.
  4. Kubespray와 Ansible 버전 불일치: 각 릴리스는 특정 Ansible 버전 범위를 요구합니다. 가상환경을 릴리스마다 새로 만들고 requirements.txt를 다시 설치하는 것을 루틴으로 만드세요.
  5. 다른 버전 태그로 기존 클러스터에 cluster.yml 실행: 의도치 않은 컴포넌트 업그레이드가 일어납니다. 클러스터별 버전 대응표가 중요한 이유입니다.
  6. NTP 불일치: 노드 간 시간이 어긋나면 인증서 검증과 etcd가 불안정해집니다. chrony 구성을 preinstall 단계 점검 목록에 포함하세요.

실패 시 재개와 로그

# 상세 로그로 재실행 — 멱등성 덕분에 완료된 태스크는 changed 없이 통과
ansible-playbook -i inventory/prod/hosts.yaml --become cluster.yml -vvv \
  2>&1 | tee /tmp/kubespray-run.log

# 노드 측 1차 확인 지점
journalctl -u kubelet -f          # kubelet 기동 실패 원인
journalctl -u containerd -f      # 런타임/레지스트리 문제
crictl ps -a                      # 컨트롤 플레인 스태틱 파드 상태
ls /etc/kubernetes/manifests/     # kubeadm 스태틱 파드 매니페스트

경험적으로 실패의 8할은 "특정 노드의 환경 차이"입니다. 실패한 태스크 이름과 노드를 확인하고, 그 노드에서 동일 작업을 수동 재현해 보면 원인이 빨리 드러납니다. 해결 후에는 전체 플레이북을 처음부터 다시 돌려 수렴 상태를 확인하는 것이 안전합니다.

프로덕션 체크리스트

[ ] 인벤토리·group_vars가 Git에서 PR 리뷰로 관리되는가
[ ] Kubespray 릴리스 태그와 kube_version 대응이 문서화되어 있는가
[ ] CP 3대 + etcd 쿼럼, etcd는 SSD/NVMe인가
[ ] API 서버 VIP(kube-vip 또는 HAProxy+keepalived) 페일오버 테스트 통과
[ ] 파드/서비스 CIDR이 사내 IPAM에 등록되고 중복이 없는가
[ ] etcd 스냅샷 주기 백업 + 외부 반출 + 분기별 복구 리허설
[ ] auto_renew_certificates 활성 + 인증서 만료 모니터링
[ ] kubernetes_audit 활성 + 중앙 로그 수집
[ ] kube_encrypt_secret_data 활성
[ ] PSA 기본값(enforce baseline 이상) 적용
[ ] kube-bench 실행 결과와 예외 사유 문서화
[ ] 업그레이드 절차(스킵 금지, serial, PDB 점검)가 런북으로 존재
[ ] 스테이징 클러스터에서 동일 변경을 먼저 검증하는 프로세스
[ ] OS 패치 정책과 쿠버네티스 패키지 hold 설정

마치며

Kubespray는 화려한 도구가 아닙니다. 새로운 추상화를 발명하는 대신, kubeadm이라는 업스트림 표준 위에 Ansible이라는 검증된 자동화를 얹어 "범용 리눅스 서버 무더기를 프로덕션 쿠버네티스로 바꾸는" 가장 현실적인 경로를 제공합니다. 그 대가는 절차적 도구의 숙명인 운영 규율입니다. 인벤토리를 Git으로 통제하고, 버전 대응표를 유지하고, 스테이징에서 먼저 검증하고, 백업과 리허설을 빼먹지 않는 조직에게 Kubespray는 수년간 안정적으로 봉사하는 도구입니다.

반대로 클러스터 수가 늘어나 플릿 관리가 본질적 과제가 되는 시점이 오면, Cluster API로의 이행을 검토할 때입니다. 그때조차 첫 매니지먼트 클러스터를 부트스트랩하는 자리에는 여전히 Kubespray가 있을 가능성이 높습니다. 온프렘 쿠버네티스를 시작하는 팀이라면, kubeadm을 한 번 수동으로 끝까지 해 본 뒤 Kubespray로 자동화하는 학습 경로를 권합니다. 도구가 무엇을 대신해 주는지 알아야 도구가 실패했을 때 직접 고칠 수 있기 때문입니다.

참고 자료