[Kubernetes] Cluster: 내 손으로 클러스터 구성하기 - 8.2. Bootstrapping the Kubernetes Control Plane
서종호(가시다)님의 On-Premise K8s Hands-on Study 1주차 학습 내용을 기반으로 합니다.
TL;DR
이번 글의 목표는 Kubernetes Control Plane 배포 및 검증이다. Kubernetes the Hard Way 튜토리얼의 Bootstrapping the Kubernetes Control Plane 단계를 수행한다.
이전 글에서 분석한 설정 파일들을 실제로 배포하고, Control Plane이 정상적으로 동작하는지 확인한다.
- 파일 배포: jumpbox에서 server 노드로 바이너리, unit 파일, 설정 파일 전송
- Control Plane 실행: systemd로 kube-apiserver, kube-scheduler, kube-controller-manager 시작
- 동작 검증: 서비스 상태, 포트 리스닝, 클러스터 정보 확인
- RBAC 설정: kube-apiserver가 kubelet에 접근할 수 있도록 권한 부여
파일 배포
jumpbox에서 server 가상 머신으로 필요한 바이너리, systemd unit 파일, 설정 파일을 전송한다.
scp \
downloads/controller/kube-apiserver \
downloads/controller/kube-controller-manager \
downloads/controller/kube-scheduler \
downloads/client/kubectl \
units/kube-apiserver.service \
units/kube-controller-manager.service \
units/kube-scheduler.service \
configs/kube-scheduler.yaml \
configs/kube-apiserver-to-kubelet.yaml \
root@server:~/
# 출력
kube-apiserver 100% 86MB 107.8MB/s 00:00
kube-controller-manager 100% 80MB 119.1MB/s 00:00
kube-scheduler 100% 61MB 103.0MB/s 00:00
kubectl 100% 53MB 99.4MB/s 00:00
...
전송된 파일을 확인한다.
ssh server ls -l /root
# 출력 (축약)
-rw------- 1 root root 9953 Jan 8 22:41 admin.kubeconfig
-rw-r--r-- 1 root root 1899 Jan 8 00:21 ca.crt
-rw------- 1 root root 3272 Jan 8 00:21 ca.key
-rw-r--r-- 1 root root 271 Jan 8 22:55 encryption-config.yaml
-rwxr-xr-x 1 root root 90243224 Jan 9 20:09 kube-apiserver # 바이너리
-rw-r--r-- 1 root root 1442 Jan 9 20:10 kube-apiserver.service # unit 파일
-rw-r--r-- 1 root root 727 Jan 9 20:10 kube-apiserver-to-kubelet.yaml # RBAC 설정
...
Control Plane 실행
server 가상 머신에 접속하여 작업을 진행한다.
ssh root@server
디렉토리 생성
설정 파일을 저장할 디렉토리를 생성한다.
mkdir -p /etc/kubernetes/config # Kubernetes 설정 파일 저장 경로
바이너리 설치
Control Plane 바이너리를 /usr/local/bin에 설치한다.
mv kube-apiserver \
kube-controller-manager \
kube-scheduler kubectl \
/usr/local/bin/
# 확인
ls -l /usr/local/bin/kube-*
-rwxr-xr-x 1 root root 90243224 Jan 9 20:09 /usr/local/bin/kube-apiserver
-rwxr-xr-x 1 root root 83427480 Jan 9 20:09 /usr/local/bin/kube-controller-manager
-rwxr-xr-x 1 root root 64225432 Jan 9 20:09 /usr/local/bin/kube-scheduler
kube-apiserver 설정
kube-apiserver가 사용할 인증서, 키, 설정 파일을 /var/lib/kubernetes/에 배치한다.
mkdir -p /var/lib/kubernetes/
mv ca.crt ca.key \
kube-api-server.key kube-api-server.crt \
service-accounts.key service-accounts.crt \
encryption-config.yaml \
/var/lib/kubernetes/
# 확인
ls -l /var/lib/kubernetes/
total 28
-rw-r--r-- 1 root root 1899 Jan 8 00:21 ca.crt
-rw------- 1 root root 3272 Jan 8 00:21 ca.key
-rw-r--r-- 1 root root 271 Jan 8 22:55 encryption-config.yaml
-rw-r--r-- 1 root root 2354 Jan 8 00:21 kube-api-server.crt
-rw------- 1 root root 3272 Jan 8 00:21 kube-api-server.key
-rw-r--r-- 1 root root 2004 Jan 8 00:21 service-accounts.crt
-rw------- 1 root root 3272 Jan 8 00:21 service-accounts.key
경로 선택 이유:
/var/lib/kubernetes/는 Kubernetes 런타임 데이터를 저장하는 관례적인 경로다. 인증서, 키 등 민감한 데이터는 여기에 저장하고, 설정 파일은/etc/kubernetes/config/에 저장한다.
systemd unit 파일을 설치한다.
mv kube-apiserver.service /etc/systemd/system/
kube-controller-manager 설정
kube-controller-manager의 kubeconfig를 배치하고 unit 파일을 설치한다.
mv kube-controller-manager.kubeconfig /var/lib/kubernetes/
mv kube-controller-manager.service /etc/systemd/system/
kube-scheduler 설정
kube-scheduler의 kubeconfig, 설정 파일, unit 파일을 각각 배치한다.
mv kube-scheduler.kubeconfig /var/lib/kubernetes/
mv kube-scheduler.yaml /etc/kubernetes/config/
mv kube-scheduler.service /etc/systemd/system/
서비스 시작
systemd 설정을 리로드하고 서비스를 시작한다.
systemctl daemon-reload
systemctl enable kube-apiserver kube-controller-manager kube-scheduler
systemctl start kube-apiserver kube-controller-manager kube-scheduler
# enable 실행 시 출력
Created symlink /etc/systemd/system/multi-user.target.wants/kube-apiserver.service → /etc/systemd/system/kube-apiserver.service.
Created symlink /etc/systemd/system/multi-user.target.wants/kube-controller-manager.service → /etc/systemd/system/kube-controller-manager.service.
Created symlink /etc/systemd/system/multi-user.target.wants/kube-scheduler.service → /etc/systemd/system/kube-scheduler.service.
검증
포트 리스닝 확인
각 컴포넌트가 지정된 포트에서 리스닝하는지 확인한다.
ss -tlp | grep kube
LISTEN 0 4096 *:6443 *:* users:(("kube-apiserver",pid=3016,fd=3))
LISTEN 0 4096 *:10259 *:* users:(("kube-scheduler",pid=3018,fd=3))
LISTEN 0 4096 *:10257 *:* users:(("kube-controller",pid=3017,fd=3))
| 포트 | 컴포넌트 | 용도 |
|---|---|---|
| 6443 | kube-apiserver | HTTPS API 엔드포인트. kubectl, kubelet 등이 연결 |
| 10257 | kube-controller-manager | 헬스체크 및 메트릭 엔드포인트 |
| 10259 | kube-scheduler | 헬스체크 및 메트릭 엔드포인트 |
서비스 상태 확인
각 컴포넌트의 systemd 서비스 상태를 확인한다.
systemctl is-active kube-apiserver kube-controller-manager kube-scheduler
active
active
active
상세 상태를 확인한다.
systemctl status kube-apiserver --no-pager
● kube-apiserver.service - Kubernetes API Server
Loaded: loaded (/etc/systemd/system/kube-apiserver.service; enabled; preset: enabled)
Active: active (running) since Fri 2026-01-09 20:23:48 KST; 1min 36s ago
Docs: https://github.com/kubernetes/kubernetes
Main PID: 3016 (kube-apiserver)
Tasks: 8 (limit: 2096)
Memory: 206.2M
CPU: 2.860s
CGroup: /system.slice/kube-apiserver.service
└─3016 /usr/local/bin/kube-apiserver --allow-privileged=true ...
Jan 09 20:23:51 server kube-apiserver[3016]: I0109 20:23:51.485317 allocated clusterIPs service="default/kubernetes" ...
kube-scheduler와 kube-controller-manager도 동일한 방식으로 확인한다.
systemctl status kube-scheduler kube-controller-manager --no-pager
● kube-scheduler.service - Kubernetes Scheduler
Loaded: loaded (/etc/systemd/system/kube-scheduler.service; enabled; preset: enabled)
Active: active (running) since Fri 2026-01-09 20:23:48 KST; 2min 49s ago
...
└─3018 /usr/local/bin/kube-scheduler --config=/etc/kubernetes/config/kube-scheduler.yaml --v=2
Jan 09 20:23:54 server kube-scheduler[3018]: successfully acquired lease kube-system/kube-scheduler
...
● kube-controller-manager.service - Kubernetes Controller Manager
Loaded: loaded (/etc/systemd/system/kube-controller-manager.service; enabled; preset: enabled)
Active: active (running) since Fri 2026-01-09 20:23:48 KST; 2min 49s ago
...
└─3017 /usr/local/bin/kube-controller-manager --bind-address=0.0.0.0 --cluster-cidr=10.200.0.0/16 ...
로그에서 successfully acquired lease를 확인할 수 있다. 이는 리더 선출이 완료되었음을 의미한다.
클러스터 정보 확인
API Server가 정상 동작하는지 kubectl로 확인한다. admin kubeconfig를 사용한다.
5. Generating Kubernetes Configuration Files for Authentication 단계에서 생성한 admin kubeconfig에는 system:masters 그룹에 속한 인증서가 포함되어 있다. 이 그룹은 클러스터 슈퍼유저 권한을 가지므로 모든 작업이 허용된다.
kubectl cluster-info --kubeconfig admin.kubeconfig
Kubernetes control plane is running at https://127.0.0.1:6443
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
리소스 확인
현재 클러스터에 노드와 Pod가 없는지 확인한다. Worker 노드를 아직 구성하지 않았으므로 비어 있어야 정상이다.
kubectl get node --kubeconfig admin.kubeconfig
No resources found
kubectl get pod -A --kubeconfig admin.kubeconfig
No resources found
하지만 kubernetes Service는 자동으로 생성된다. API Server가 시작되면 자동으로 default namespace에 kubernetes Service를 생성한다.
kubectl get service,endpoints --kubeconfig admin.kubeconfig
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.32.0.1 <none> 443/TCP 7m30s
NAME ENDPOINTS AGE
endpoints/kubernetes 10.0.2.15:6443 7m30s
| 리소스 | 설명 |
|---|---|
service/kubernetes |
클러스터 내부에서 API Server에 접근하기 위한 Service. ClusterIP는 10.32.0.1로, --service-cluster-ip-range의 첫 번째 IP가 자동 할당됨 |
endpoints/kubernetes |
Service의 실제 백엔드 주소. 10.0.2.15:6443은 server 노드의 IP와 API Server 포트 |
RBAC 설정
kube-apiserver가 kubelet API에 접근할 수 있도록 RBAC을 설정한다.
kubelet의 인가 필요성
Kubernetes 클러스터에서 인가(Authorization)는 대부분 API Server가 담당한다. API Server가 kubelet에 직접 요청할 때는 그 반대다.
API Server는 다음과 같은 상황에서 kubelet API에 직접 요청을 보낸다:
kubectl logs: 컨테이너 로그 조회kubectl exec: 컨테이너 내 명령 실행kubectl top: 노드/Pod 메트릭 조회- Metrics Server: 리소스 사용량 수집
kubelet은 자체적인 HTTPS 서버를 실행하며, API Server의 요청을 받아 처리한다. 이 때 kubelet은 요청자가 적절한 권한을 가지고 있는지 확인해야 한다. 다음과 같은 선택지가 있다:
- AlwaysAllow: 모든 요청 허용 → 보안 위험
- 자체 구현: kubelet이 독자적 인가 로직 → 복잡성, 이원화
- Webhook: API Server에게 인가 결정 위임 → 실습에서 사용
Webhook 모드의 장점:
- 중앙 집중식: 모든 인가 정책이 API Server의 RBAC에 집중
- 일관성: 클러스터 전체가 동일한 인가 방식 사용
- 동적 변경: RBAC 규칙만 수정하면 즉시 적용
Webhook 인가 모드와 RBAC의 필요성
다음 단계(9. Bootstrapping the Kubernetes Worker Nodes)에서 kubelet의 authorization.mode를 Webhook으로 설정할 예정이다.
참고: 원본 가이드의 표현
실습 원본 가이드에 에서 “This tutorial sets the Kubelet
--authorization-modeflag toWebhook“이라고 나오는데, 현재 단계는 Control Plane(kube-apiserver) 구성이고 kubelet 설정이 아니다. 여기서 미리 RBAC을 설정하는 이유는, 다음 단계에서 kubelet을 Webhook 모드로 구성할 때 필요하기 때문이다. kubelet 설정 전에 권한을 미리 준비해두는 것이다.
Webhook 모드에서 kubelet은 SubjectAccessReview API를 사용하여 API Server에 인가 결정을 위임한다.
역설적인 동작 방식
처음 들으면 이상하게 들릴 수 있다: “API Server가 kubelet에 요청을 보내는데, kubelet이 다시 API Server에게 권한을 확인한다고?” 맞다. 이것이 Webhook 모드의 핵심 동작이다.
구체적인 동작 흐름
kubectl logs 명령을 예로 들어 전체 흐름을 살펴보자:
- 사용자 → API Server:
kubectl logs pod-1 --kubeconfig admin.kubeconfig- kubectl이 API Server(6443)에 로그 조회 요청
- API Server → kubelet 요청: kube-apiserver가 kubelet API에 요청
- API Server가 Pod가 실행 중인 노드의 kubelet(10250)에 연결
- 클라이언트 인증서(CN=
kubernetes)를 제시하며/logs엔드포인트 호출
- kubelet → API Server 인가 확인 (SubjectAccessReview 요청):
kubelet: "CN=kubernetes라는 사용자가 내 /logs에 접근하려고 하네?" kubelet: "내가 이 사용자에게 권한이 있는지 어떻게 알지?" kubelet → API Server: SubjectAccessReview 요청 "kubernetes라는 사용자가 nodes/log 리소스에 접근할 수 있나요?" - API Server RBAC 평가:
API Server: "ClusterRole system:kube-apiserver-to-kubelet을 확인해보니..." API Server: "ClusterRoleBinding system:kube-apiserver가 사용자 kubernetes와 연결되어 있고..." API Server: "nodes/log 리소스에 대한 권한이 있네!" API Server → kubelet: {"allowed": true} - kubelet 처리: 인가 결과에 따라 요청 처리
kubelet: "API Server가 허가했으니 로그를 반환하자" kubelet → API Server → kubectl: 로그 데이터 전송
Webhook 모드의 장점
- 중앙 집중식 권한 관리: 모든 인가 정책이 API Server의 RBAC에 집중됨
- 동적 정책 변경: RBAC 규칙만 변경하면 즉시 적용 (kubelet 재시작 불필요)
- 일관된 보안 모델: 클러스터 전체가 동일한 인가 방식 사용
- kubelet 단순화: kubelet이 복잡한 권한 로직을 구현할 필요 없음
만약 authorization.mode: AlwaysAllow로 설정하면 kubelet은 모든 요청을 무조건 허용하여 보안 위험이 발생한다.
RBAC 설정의 필요성
따라서 kubelet을 구성하기 전에 미리 RBAC 권한을 설정해둔다. 이 설정이 없으면 위의 4번 단계에서 API Server가 {"allowed": false}를 반환하고, kubectl logs, kubectl exec, kubectl top 등의 명령이 모두 실패한다.
RBAC 설정 파일
kube-apiserver가 kubelet API에 접근하기 위한 RBAC 설정 파일이다.
# configs/kube-apiserver-to-kubelet.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
annotations:
rbac.authorization.kubernetes.io/autoupdate: "true"
labels:
kubernetes.io/bootstrapping: rbac-defaults
name: system:kube-apiserver-to-kubelet
rules:
- apiGroups:
- ""
resources:
- nodes/proxy
- nodes/stats
- nodes/log
- nodes/spec
- nodes/metrics
verbs:
- "*"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system:kube-apiserver
namespace: ""
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:kube-apiserver-to-kubelet
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: kubernetes
ClusterRole 분석
ClusterRole의 주요 필드를 살펴보자.
name: system:kube-apiserver-to-kubelet: 역할 이름apiGroups: [""]: Core API 그룹(v1). Node 관련 리소스는 Core 그룹에 속함resources: 접근 가능한 Node 하위 리소스verbs: ["*"]: 모든 동작 허용
접근 가능한 리소스 목록은 아래와 같다.
nodes/proxy: API Server → kubelet 프록시 통신nodes/stats: 노드/Pod 리소스 통계nodes/log: 컨테이너 로그 조회nodes/spec: 노드 스펙 정보nodes/metrics: Metrics Server /kubectl top
ClusterRoleBinding 분석
ClusterRoleBinding은 ClusterRole을 특정 사용자에게 연결한다.
subjects.name: kubernetes: 이 권한을 부여받을 사용자roleRef.name: system:kube-apiserver-to-kubelet: 부여할 ClusterRole
subjects.name이 kubernetes인 이유는 kube-apiserver의 클라이언트 인증서 CN(Common Name)이 kubernetes이기 때문이다. 인증서의 CN이 Kubernetes에서 사용자 이름으로 매핑된다.
# kube-api-server.crt 인증서 확인
openssl x509 -in kube-api-server.crt -text -noout | grep Subject:
Subject: CN = kubernetes, C = US, ST = Washington, L = Seattle
설정 적용
RBAC 설정 파일을 적용한다.
kubectl apply -f kube-apiserver-to-kubelet.yaml --kubeconfig admin.kubeconfig
clusterrole.rbac.authorization.k8s.io/system:kube-apiserver-to-kubelet created
clusterrolebinding.rbac.authorization.k8s.io/system:kube-apiserver created
설정 확인
생성된 ClusterRole과 ClusterRoleBinding을 확인한다.
kubectl get clusterroles system:kube-apiserver-to-kubelet --kubeconfig admin.kubeconfig
NAME CREATED AT
system:kube-apiserver-to-kubelet 2026-01-09T11:38:34Z
kubectl get clusterrolebindings system:kube-apiserver --kubeconfig admin.kubeconfig
NAME ROLE AGE
system:kube-apiserver ClusterRole/system:kube-apiserver-to-kubelet 45s
기본 ClusterRole 확인
Kubernetes는 부트스트랩 시 다양한 기본 ClusterRole을 자동 생성한다. 일부를 확인해 보자.
kubectl get clusterroles --kubeconfig admin.kubeconfig | head -20
NAME CREATED AT
admin 2026-01-09T11:23:51Z
cluster-admin 2026-01-09T11:23:51Z
edit 2026-01-09T11:23:51Z
system:aggregate-to-admin 2026-01-09T11:23:51Z
system:aggregate-to-edit 2026-01-09T11:23:51Z
system:aggregate-to-view 2026-01-09T11:23:51Z
system:kube-controller-manager 2026-01-09T11:23:51Z
system:kube-scheduler 2026-01-09T11:23:51Z
system:node 2026-01-09T11:23:51Z
...
| ClusterRole | 설명 |
|---|---|
cluster-admin |
모든 리소스에 대한 전체 권한 |
admin |
Namespace 내 대부분의 리소스 관리 권한 |
edit |
읽기/쓰기 권한 (RBAC 수정 제외) |
view |
읽기 전용 권한 |
system:kube-scheduler |
kube-scheduler 전용 권한 |
system:kube-controller-manager |
kube-controller-manager 전용 권한 |
system:node |
kubelet 전용 권한 |
이러한 역할들은 kubernetes.io/bootstrapping: rbac-defaults 레이블을 가지며, rbac.authorization.kubernetes.io/autoupdate: "true" 어노테이션이 설정되어 있어 Kubernetes 업그레이드 시 자동으로 업데이트된다.
API Server 검증
HTTPS 요청으로 API Server가 정상 동작하는지 확인한다. /version 엔드포인트는 인증 없이 접근 가능하며, Kubernetes 버전 정보를 반환한다.
curl -s -k --cacert /var/lib/kubernetes/ca.crt \
https://server.kubernetes.local:6443/version | jq
{
"major": "1",
"minor": "32",
"gitVersion": "v1.32.3",
"gitCommit": "32cc146f75aad04beaaa245a7157eb35063a9f99",
"gitTreeState": "clean",
"buildDate": "2025-03-11T19:52:21Z",
"goVersion": "go1.23.6",
"compiler": "gc",
"platform": "linux/arm64"
}
server 가상머신 종료
작업이 완료되었으면 server 가상머신을 종료한다.
exit
결과
이 단계를 완료하면 다음과 같은 결과를 얻을 수 있다:
- Control Plane 바이너리 설치: kube-apiserver, kube-controller-manager, kube-scheduler, kubectl을
/usr/local/bin/에 설치 - 설정 파일 배치: 인증서, kubeconfig를
/var/lib/kubernetes/에, 설정 파일을/etc/kubernetes/config/에 배치 - systemd 서비스 시작: 모든 Control Plane 컴포넌트가 정상 실행 중
- 포트 리스닝 확인:
- API Server: 6443
- Controller Manager: 10257
- Scheduler: 10259
- RBAC 설정 완료: kube-apiserver가 kubelet API에 접근할 수 있는 권한 부여
- kubernetes Service 자동 생성: ClusterIP
10.32.0.1
이번 실습을 통해 Kubernetes Control Plane을 직접 구성해 보았다. API Server, Scheduler, Controller Manager가 각각의 역할을 수행하며, systemd로 관리되어 자동 재시작 및 부팅 시 자동 시작이 가능하다. 아직 Worker 노드가 없으므로 Pod를 실행할 수는 없지만, 클러스터의 핵심 컴포넌트들은 정상적으로 동작하고 있다.
다음 단계에서는 Worker 노드(node-0, node-1)에 kubelet, kube-proxy, containerd를 구성하여 실제로 Pod를 실행할 수 있는 환경을 만든다.
댓글남기기