애플리케이션보다 먼저 정한 클러스터의 경계

이번 기록의 범위는 운영체제가 준비된 서버를 Kubernetes 노드로 만들고, 노드 간 네트워크가 연결되어 기본 워크로드를 받을 수 있는 상태까지입니다. 그 위에 올라가는 배포 도구, 데이터베이스, 관측 시스템, 공개 서비스는 의도적으로 제외합니다.

처음 정한 원칙은 세 가지입니다. 같은 작업을 다시 실행해도 안전하게 만들고, 실제 주소와 역할을 코드의 기본값에 넣지 않으며, Kubernetes가 책임질 영역과 그 위 플랫폼이 책임질 영역을 분리합니다. 이 원칙에 맞춰 호스트 준비와 kubeadm 작업은 Ansible 역할로 만들었으며, 환경마다 달라지는 값은 별도 인벤토리에서 주입했습니다.

자동화 진입점 예시

YAML
---
- name: Prepare Kubernetes nodes
  hosts: kubernetes_nodes
  become: true
  gather_facts: true
  roles:
    - node_baseline
    - kubernetes_bootstrap

실제 인벤토리 그룹과 역할 이름은 공개하지 않았으며 구조만 단순화했습니다. 설치 절차를 한 번의 셸 스크립트로 묶기보다 호스트 기준 설정과 Kubernetes 부트스트랩을 역할 단위로 분리한 점이 핵심입니다.

1. 설치보다 먼저 자동화한 노드 준비

노드는 제어 영역과 작업 노드 그룹으로만 나눴습니다. 공개 글에서는 실제 장비 이름, 수량, 주소, 내부 레이블을 다루지 않습니다. 자동화는 각 노드가 사용할 안정적인 내부 주소를 선택하고, 관리 트래픽과 노드 간 통신만 허용하도록 호스트 방화벽을 구성합니다.

방화벽의 기본 정책은 들어오는 연결을 거부하고 나가는 연결을 허용하는 방식입니다. 관리 접속은 신뢰할 수 있는 원본에서만 받고, 클러스터 내부 통신은 미리 정한 사설 네트워크 범위 안에서만 허용합니다. 중요한 점은 포트 목록을 공개하는 것이 아니라 허용 대상을 명시하고 나머지를 닫는 순서를 자동화에 포함하는 것입니다.

2. Kubernetes 기준에 맞춘 커널과 swap

kubelet을 설치하기 전에 모든 노드에서 같은 전제 조건을 만들었습니다.

  • 실행 중인 swap을 끄고 재부팅 뒤에도 다시 켜지지 않도록 설정합니다
  • overlay와 br_netfilter 커널 모듈을 로드하고 영구 설정으로 남깁니다
  • 브리지 트래픽이 필터링 경로를 지나도록 설정합니다
  • IPv4 forwarding을 활성화합니다

이 단계가 작은 설정처럼 보여도 노드마다 값이 다르면 네트워크 문제와 스케줄링 문제가 간헐적으로 나타날 수 있습니다. 따라서 설치 명령과 분리하지 않고 하나의 반복 가능한 역할에 넣었습니다.

3. 하나로 맞춘 containerd와 cgroup 기준

컨테이너 런타임은 containerd를 사용했습니다. 기본 설정을 생성한 뒤 cgroup 드라이버를 systemd로 바꿨으며, kubelet도 같은 드라이버를 사용하도록 설정했습니다. 운영체제가 systemd로 자원을 관리하는데 런타임과 kubelet이 다른 cgroup 체계를 사용하면 부하가 커질 때 자원 회수와 제한 동작이 어긋날 수 있기 때문입니다.

설정 변경 뒤에는 containerd를 즉시 재시작하고 정상 기동을 확인합니다. 이후 단계에서 런타임 오류를 Kubernetes 초기화 실패로 잘못 판단하지 않도록 만드는 순서입니다.

4. 공식 저장소에서 고정한 Kubernetes 패키지

kubelet, kubeadm, kubectl은 선택한 마이너 버전의 공식 패키지 저장소에서 설치했습니다. 저장소 키는 전용 keyring으로 관리하며, 설치가 끝난 패키지는 자동으로 다음 마이너 버전으로 넘어가지 않도록 hold 상태로 둡니다.

버전을 고정한 이유는 오래된 버전을 유지하기 위해서가 아닙니다. 업그레이드를 운영체제의 일반 패키지 갱신에 섞지 않고 별도의 검토와 검증을 거치는 변경으로 만들기 위해서입니다. 패치 업데이트도 한 번에 전체 노드에 적용하지 않고 작은 단위로 진행할 수 있어야 합니다.

5. kubeadm 설정 파일과 초기화

긴 명령행 옵션 대신 kubeadm 설정 API로 초기화 문서를 만들었습니다. 실제 값은 공개하지 않고 다음과 같은 항목만 환경 설정으로 관리합니다.

  • 제어 영역의 안정적인 접근 지점을 관리합니다
  • 서로 겹치지 않는 Pod 및 Service 사설 주소 범위를 관리합니다
  • 클러스터 DNS 도메인을 관리합니다
  • API 인증서에 필요한 최소 SAN 목록을 관리합니다
  • 노드가 사용할 내부 주소와 containerd CRI 소켓을 관리합니다

kubeadm 초기화 문서 예시

YAML
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
controlPlaneEndpoint: "<CONTROL_PLANE_ENDPOINT>"
networking:
  dnsDomain: "cluster.local"
  podSubnet: "<POD_CIDR>"
  serviceSubnet: "<SERVICE_CIDR>"
proxy:
  disabled: true
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd

예제의 대문자 placeholder는 실제 환경 값을 뜻합니다. 공개 문서나 저장소 기본값에는 운영 주소를 넣지 않고 배포 시점에 별도 설정으로 주입합니다.

서비스 네트워킹은 Cilium이 담당하도록 kube-proxy 생성을 끈 상태로 초기화했습니다. 따라서 제어 영역을 만든 직후 노드가 잠시 NotReady인 것은 실패가 아니라 CNI 설치 전의 예상 상태입니다. 이 구간을 장애로 오해하지 않도록 자동화와 검증 순서에 명시했습니다.

초기화는 관리자 kubeconfig가 없는 경우에만 실행합니다. 완료 뒤 kubeconfig는 제한된 권한으로 배치하고, API 서버의 readyz 응답이 성공할 때까지 기다립니다. 파일 존재 여부와 준비 상태를 모두 확인해야 재실행 가능한 자동화가 됩니다.

6. 짧게 살아 있는 자격으로 합류한 작업 노드

작업 노드를 추가할 때마다 제어 영역에서 수명이 짧은 bootstrap token을 발급하고, CA 공개키 해시와 함께 join 설정을 생성했습니다. 토큰과 설정 내용은 자동화 로그에 남기지 않았으며 파일 권한도 소유자만 읽을 수 있도록 제한했습니다.

이미 kubelet 설정이 존재하는 노드는 다시 join하지 않습니다. 처음 합류하는 노드만 등록을 수행하고, 제어 영역에서 해당 노드가 실제로 보일 때까지 확인합니다. 역할 레이블도 이 검증 뒤에 적용해 설치 성공과 배치 정책을 한 단계로 묶지 않았습니다.

7. Cilium으로 완성한 클러스터 네트워크

제어 영역이 준비된 다음 Cilium을 설치했습니다. kube-proxy replacement를 사용해 Service 전달을 eBPF 데이터 경로로 처리하고, 노드 사이에는 native routing을 사용했습니다. 노드 간 Pod 트래픽은 WireGuard로 암호화하되, 실제 인터페이스와 주소 범위는 환경 설정에만 남겼습니다.

Cilium 핵심 값 예시

YAML
kubeProxyReplacement: true
routingMode: native
encryption:
  enabled: true
  type: wireguard
ipam:
  mode: kubernetes

이 예제도 선택한 기능만 보여 줍니다. 실제 장치, 주소 범위, 노드 선택 조건과 외부 연결 설정은 공개 대상에서 제외했습니다.

kubeadm의 Pod 주소 범위와 Cilium의 native routing 범위는 반드시 같아야 합니다. 이 값이 어긋나면 노드는 Ready로 보여도 노드 간 Pod 통신이 불안정해질 수 있습니다. CNI를 설치한 뒤에는 에이전트와 오퍼레이터 상태, 노드 간 연결, DNS 조회, Service 접근을 차례로 확인했습니다.

여기까지가 이 글에서 말하는 클러스터 완성선입니다. Gateway 경로, 배포 시스템, 스토리지 플랫폼, 데이터 서비스와 실제 애플리케이션은 별도의 계층이며 이 글에는 포함하지 않습니다.

설치 뒤 확인한 체크리스트

  • 모든 노드가 Ready이며 kubelet과 containerd가 기대한 상태입니까?
  • API 서버의 readyz가 안정적으로 성공합니까?
  • 시스템 Pod가 반복 재시작 없이 준비됩니까?
  • 서로 다른 노드의 Pod 사이 통신과 DNS 조회가 가능합니까?
  • ClusterIP Service를 통한 통신이 가능합니까?
  • CNI 상태 검사에서 연결 오류나 암호화 누락이 없습니까?
  • 자동화를 다시 실행해도 불필요한 초기화나 재합류가 일어나지 않습니까?

가장 크게 배운 점

클러스터 설치의 핵심은 kubeadm init 한 줄이 아니었습니다. 호스트 방화벽, cgroup, 주소 범위, CNI의 책임을 먼저 맞추고 각 단계의 완료 조건을 자동화에 넣는 일이 더 중요했습니다.

또 하나는 공개 문서와 실행 문서를 분리하는 것입니다. 공개 글에는 선택의 이유와 검증 순서를 남기고, 실제 호스트명, 주소, 인증서 SAN, 인벤토리, 접근 경로는 내부 실행 문서에만 둡니다. 구체적인 자원 이름을 공개하지 않아도 재사용 가능한 설치 원칙과 실패를 줄인 판단은 충분히 공유할 수 있습니다.