[Kubernetes] Cluster: Kubeadm을 이용해 클러스터 구성하기 - 1.4. kubeadm init 실행

· 22 분 소요

서종호(가시다)님의 On-Premise K8s Hands-on Study 3주차 학습 내용을 기반으로 합니다.


TL;DR

이번 글의 목표는 kubeadm init 실행 및 컨트롤 플레인 구성이다.

  • kubeadm init: 설정 파일을 사용하여 컨트롤 플레인 초기화
  • kubeconfig 설정: kubectl 사용을 위한 설정


들어가며

이전 글에서 containerd와 kubeadm/kubelet/kubectl 설치를 완료했다. 이제 kubeadm init을 실행하여 컨트롤 플레인을 구성한다.

컨트롤 플레인 구성이 목적이므로, 이 글에서는 컨트롤 플레인 노드(k8s-ctr)에서만 작업을 진행한다.


kubeadm 설정 파일 작성

kubeadm init은 명령줄 옵션과 설정 파일 두 가지 방식을 지원한다.

명령줄 옵션 방식

명령줄 옵션 방식은 간단한 테스트에 적합하다.

kubeadm init \
  --apiserver-advertise-address=192.168.10.100 \
  --pod-network-cidr=10.244.0.0/16 \
  --service-cidr=10.96.0.0/16 \
  --kubernetes-version=1.32.11 \
  --token=123456.1234567890123456 \
  --token-ttl=0

설정 파일 방식

명령줄 옵션 대신 YAML 설정 파일을 사용하면 버전 관리와 재현성 측면에서 더 좋다. 이번 실습에서도 설정 파일 방식을 사용한다.

## kubeadm Configuration 파일 작성
cat << EOF > kubeadm-init.yaml
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
bootstrapTokens:
- token: "123456.1234567890123456"    # 토큰 고정 (실습용)
  ttl: "0s"                           # 토큰 만료 시간 없음 (실습용)
  usages:
  - signing
  - authentication
nodeRegistration:
  kubeletExtraArgs:
    - name: node-ip
      value: "192.168.10.100"         # 미설정 시 10.0.2.15로 매핑될 수 있음 (실습 환경 특수성)
  criSocket: "unix:///run/containerd/containerd.sock"
localAPIEndpoint:
  advertiseAddress: "192.168.10.100"
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: "1.32.11"
networking:
  podSubnet: "10.244.0.0/16"          # Flannel 기본값
  serviceSubnet: "10.96.0.0/16"
EOF
cat kubeadm-init.yaml

설정 파일 주요 항목

항목 설명
bootstrapTokens 워커 노드 join 시 사용할 토큰
nodeRegistration.kubeletExtraArgs kubelet에 전달할 추가 인자. node-ip를 명시하여 올바른 IP 사용
localAPIEndpoint.advertiseAddress API Server가 광고할 IP 주소
networking.podSubnet Pod 네트워크 CIDR. Flannel 사용 시 10.244.0.0/16
networking.serviceSubnet Service 네트워크 CIDR

주요 설정 항목 값에서 주의해서 봐야 하는 것은 다음과 같다:

  • node-ip: Vagrant처럼 여러 네트워크 인터페이스가 있는 환경에서는 반드시 명시해야 한다. 미설정 시 NAT 인터페이스 IP(10.0.2.15)가 사용되어 노드 간 통신에 문제가 발생할 수 있다.
  • advertiseAddress vs node-ip: advertiseAddress는 API Server가 광고하는 주소, node-ip는 kubelet이 사용하는 주소다. 둘 다 클러스터 통신용 IP로 일치시키는 것이 좋다.
  • podSubnet: CNI 플러그인마다 기본값이 다르다. Flannel은 10.244.0.0/16, Calico는 192.168.0.0/16이 기본값이다. 사용할 CNI에 맞춰 설정한다.
  • token: 실습 재현성을 위해 토큰을 고정했다. 프로덕션에서는 위험하므로 아래 섹션에서 자세히 다룬다.
  • criSocket: containerd 외 CRI-O 등 다른 런타임 사용 시 해당 소켓 경로로 변경해야 한다.

token 고정의 위험성

설정 파일의 bootstrapTokens 영역을 보면, 토큰을 123456.1234567890123456으로 고정하고 ttl: "0s"(만료 없음)로 설정했다. 이는 실습 재현성과 워커 노드 join 편의를 위한 것이다. 기본적으로 kubeadm init[a-z0-9]{6}.[a-z0-9]{16} 형식의 랜덤 토큰을 생성하고, 기본 TTL은 24시간이다.

프로덕션에서는 절대 이렇게 하면 안 된다. Bootstrap 신뢰 모델에서 살펴본 것처럼, JWS 보안 모델의 핵심은 Token Secret을 중간자가 모른다는 전제다. 토큰을 고정하면 이 전제가 무너진다.

설정 문제
token: "123456.1234567890123456" Secret이 1234567890123456이면 사실상 공개된 값. “out-of-band로 안전하게 전달한다”는 전제가 무너지고, 중간자가 Secret을 알기 때문에 유효한 JWS 서명을 위조할 수 있음
ttl: "0s" 기본값 24시간 대신 영구 유효. 토큰이 유출되면 언제든 아무 노드가 클러스터에 join 가능

프로덕션에서의 권장 사항:

  • kubeadm init이 자동 생성하는 랜덤 토큰 사용: InitConfiguration에서 bootstrapTokens 블록을 아예 넣지 않으면 kubeadm이 랜덤 토큰 생성
  • TTL은 기본 24시간, 또는 더 짧게 설정
  • join이 끝나면 kubeadm token delete로 즉시 삭제
  • 필요 시 kubeadm token create --print-join-command로 일회성 토큰 재생성


컨테이너 이미지 사전 다운로드 (선택)

네트워크 환경에 따라 kubeadm init 시간을 단축하기 위해 이미지를 미리 다운로드할 수 있다.

# 필요한 이미지 목록 확인
kubeadm config images list
# registry.k8s.io/kube-apiserver:v1.32.11
# registry.k8s.io/kube-controller-manager:v1.32.11
# registry.k8s.io/kube-scheduler:v1.32.11
# registry.k8s.io/kube-proxy:v1.32.11
# registry.k8s.io/coredns/coredns:v1.11.3
# registry.k8s.io/pause:3.10
# registry.k8s.io/etcd:3.5.24-0

# 이미지 사전 다운로드
kubeadm config images pull
# [config/images] Pulled registry.k8s.io/kube-apiserver:v1.32.11
# [config/images] Pulled registry.k8s.io/kube-controller-manager:v1.32.11
# [config/images] Pulled registry.k8s.io/kube-scheduler:v1.32.11
# [config/images] Pulled registry.k8s.io/kube-proxy:v1.32.11
# [config/images] Pulled registry.k8s.io/coredns/coredns:v1.11.3
# [config/images] Pulled registry.k8s.io/pause:3.10
# [config/images] Pulled registry.k8s.io/etcd:3.5.24-0

kubeadm은 설치된 버전과 설정 파일을 기반으로 필요한 이미지 목록을 결정한다:

이미지 버전 결정 방식
kube-apiserver, kube-controller-manager, kube-scheduler, kube-proxy kubernetesVersion과 동일
etcd, coredns, pause kubeadm 소스 코드에 하드코딩된 호환 버전

설정 파일이나 특정 버전을 지정하면 해당 버전에 맞는 이미지 목록을 확인할 수 있다:

kubeadm config images list --config=kubeadm-init.yaml      # 설정 파일 기반
kubeadm config images list --kubernetes-version=1.33.0    # 특정 버전 지정

참고: 업그레이드 시 이미지 사전 다운로드

클러스터 업그레이드 시에는 이미지 사전 다운로드가 특히 유용하다. kubeadm upgrade apply 과정에서 새 버전 이미지를 pull하는 시간이 포함되면, 컨트롤 플레인 컴포넌트의 다운타임이 길어질 수 있다.

업그레이드 전에 kubeadm config images pull --kubernetes-version=<target-version>으로 미리 이미지를 받아두면 실제 업그레이드 시간을 크게 단축할 수 있다.


dry-run으로 사전 확인 (선택)

--dry-run 옵션을 사용하면 실제로 클러스터를 생성하지 않고 어떤 작업이 수행될지 미리 확인할 수 있다. 생성될 리소스들의 YAML 매니페스트도 출력된다.

kubeadm init --config="kubeadm-init.yaml" --dry-run

dry-run 출력은 [dryrun] Would perform action <ACTION> on resource 형식으로 실제로 수행될 API 호출을 보여준다:

[dryrun] Would perform action CREATE on resource "configmaps" in API group "core/v1"
[dryrun] Attached object:
apiVersion: v1
data:
  kubelet: |
    apiVersion: kubelet.config.k8s.io/v1beta1
    cgroupDriver: systemd
    clusterDNS:
    - 10.96.0.10
    staticPodPath: /etc/kubernetes/manifests
    rotateCertificates: true
    ...
kind: ConfigMap
metadata:
  name: kubelet-config
  namespace: kube-system

이처럼 각 리소스에 대해 어떤 action(CREATE, GET, PATCH 등)이 수행될지와 함께 생성될 오브젝트의 YAML이 출력된다.

dry-run 출력에서 주요하게 확인할 부분은 아래와 같다:

리소스 설명
kubelet-config ConfigMap 클러스터 내 모든 kubelet이 공유할 설정 (cgroupDriver: systemd, clusterDNS, staticPodPath 등)
bootstrap-token-* Secret 워커 노드 join에 사용할 부트스트랩 토큰
cluster-info ConfigMap 워커 노드가 클러스터에 join할 때 사용하는 CA 인증서와 API Server 주소
coredns Deployment/ConfigMap 클러스터 DNS 서비스
kube-proxy DaemonSet/ConfigMap 각 노드의 네트워크 프록시

dry-run 후 /etc/kubernetes 디렉토리 구조를 확인하면, 실제 파일은 생성되지 않고 임시 디렉토리에만 생성된 것을 알 수 있다:

tree /etc/kubernetes
# /etc/kubernetes
# ├── manifests                      <- 비어있음 (dry-run이므로)
# └── tmp
#     └── kubeadm-init-dryrun*       <- dry-run 결과가 여기에 저장
#         ├── admin.conf
#         ├── apiserver.crt
#         ├── ca.crt
#         ├── ca.key
#         ├── etcd/
#         │   ├── ca.crt, ca.key
#         │   ├── server.crt, server.key
#         │   └── ...
#         ├── etcd.yaml              <- etcd Static Pod 매니페스트
#         ├── kube-apiserver.yaml    <- API Server Static Pod 매니페스트
#         ├── kube-controller-manager.yaml
#         ├── kube-scheduler.yaml
#         └── ...

이 구조를 통해 실제 init 시 어떤 인증서와 매니페스트가 생성될지 미리 확인할 수 있다. 문제가 없으면 실제 init을 진행한다.


실행

이제 init을 실행한다. 이전 글에서 살펴본 kubeadm init의 14단계가 실제로 빠르게 진행된다. 사전에 이미지를 pull해두었고, 설정이 올바르다면 전체 과정이 수 초 내에 완료된다.

kubeadm init --config="kubeadm-init.yaml"
분류별 YAML 예시
[init] Using Kubernetes version: v1.32.11
[preflight] Running pre-flight checks
[preflight] Pulling images required for setting up a Kubernetes cluster
[preflight] This might take a minute or two, depending on the speed of your internet connection
[preflight] You can also perform this action beforehand using 'kubeadm config images pull'
[certs] Using certificateDir folder "/etc/kubernetes/pki"
[certs] Generating "ca" certificate and key
[certs] Generating "apiserver" certificate and key
[certs] apiserver serving cert is signed for DNS names [k8s-ctr kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 192.168.10.100]
[certs] Generating "apiserver-kubelet-client" certificate and key
[certs] Generating "front-proxy-ca" certificate and key
[certs] Generating "front-proxy-client" certificate and key
[certs] Generating "etcd/ca" certificate and key
[certs] Generating "etcd/server" certificate and key
[certs] etcd/server serving cert is signed for DNS names [k8s-ctr localhost] and IPs [192.168.10.100 127.0.0.1 ::1]
[certs] Generating "etcd/peer" certificate and key
[certs] etcd/peer serving cert is signed for DNS names [k8s-ctr localhost] and IPs [192.168.10.100 127.0.0.1 ::1]
[certs] Generating "etcd/healthcheck-client" certificate and key
[certs] Generating "apiserver-etcd-client" certificate and key
[certs] Generating "sa" key and public key
[kubeconfig] Using kubeconfig folder "/etc/kubernetes"
[kubeconfig] Writing "admin.conf" kubeconfig file
[kubeconfig] Writing "super-admin.conf" kubeconfig file
[kubeconfig] Writing "kubelet.conf" kubeconfig file
[kubeconfig] Writing "controller-manager.conf" kubeconfig file
[kubeconfig] Writing "scheduler.conf" kubeconfig file
[etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
[control-plane] Using manifest folder "/etc/kubernetes/manifests"
[control-plane] Creating static Pod manifest for "kube-apiserver"
[control-plane] Creating static Pod manifest for "kube-controller-manager"
[control-plane] Creating static Pod manifest for "kube-scheduler"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Starting the kubelet
[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory "/etc/kubernetes/manifests"
[kubelet-check] Waiting for a healthy kubelet at http://127.0.0.1:10248/healthz. This can take up to 4m0s
[kubelet-check] The kubelet is healthy after 1.001573204s
[api-check] Waiting for a healthy API server. This can take up to 4m0s
[api-check] The API server is healthy after 3.002710936s
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[kubelet] Creating a ConfigMap "kubelet-config" in namespace kube-system with the configuration for the kubelets in the cluster
[upload-certs] Skipping phase. Please see --upload-certs
[mark-control-plane] Marking the node k8s-ctr as control-plane by adding the labels: [node-role.kubernetes.io/control-plane node.kubernetes.io/exclude-from-external-load-balancers]
[mark-control-plane] Marking the node k8s-ctr as control-plane by adding the taints [node-role.kubernetes.io/control-plane:NoSchedule]
[bootstrap-token] Using token: 123456.1234567890123456
[bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles
[bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to get nodes
[bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials
[bootstrap-token] Configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
[bootstrap-token] Configured RBAC rules to allow certificate rotation for all node client certificates in the cluster
[bootstrap-token] Creating the "cluster-info" ConfigMap in the "kube-public" namespace
[kubelet-finalize] Updating "/etc/kubernetes/kubelet.conf" to point to a rotatable kubelet client certificate and key
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

  export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.10.100:6443 --token 123456.1234567890123456 \
        --discovery-token-ca-cert-hash sha256:998060ec728a1cab3c1de8ad8fb6c63c2782f5cc1e982984197f0f226bfba91f

클러스터 초기화 단계의 하이라이트인 만큼, 출력을 단계별로 살펴보자.


1단계: [preflight] 사전 검사

[init] Using Kubernetes version: v1.32.11
[preflight] Running pre-flight checks
        [WARNING Firewalld]: firewalld is active, please ensure ports [6443 10250] are open or your cluster may not function correctly
[preflight] Pulling images required for setting up a Kubernetes cluster
[preflight] This might take a minute or two, depending on the speed of your internet connection
[preflight] You can also perform this action beforehand using 'kubeadm config images pull'
  • Kubernetes 버전 확인 (v1.32.11)
  • 사전 검사 수행: 시스템 요구사항, 포트 충돌, 커널 파라미터 등
  • firewalld 경고: 6443(API Server), 10250(kubelet) 포트가 열려있어야 함. 실습에서는 firewalld를 비활성화했으므로 무시해도 됨
  • 이미지 pull 안내: 이미 kubeadm config images pull로 받아두었으므로 빠르게 진행


2단계: [certs] 인증서 생성

[certs] Using certificateDir folder "/etc/kubernetes/pki"
[certs] Generating "ca" certificate and key
[certs] Generating "apiserver" certificate and key
[certs] apiserver serving cert is signed for DNS names [k8s-ctr kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 192.168.10.100]
[certs] Generating "apiserver-kubelet-client" certificate and key
[certs] Generating "front-proxy-ca" certificate and key
[certs] Generating "front-proxy-client" certificate and key
[certs] Generating "etcd/ca" certificate and key
[certs] Generating "etcd/server" certificate and key
[certs] etcd/server serving cert is signed for DNS names [k8s-ctr localhost] and IPs [192.168.10.100 127.0.0.1 ::1]
[certs] Generating "etcd/peer" certificate and key
[certs] Generating "etcd/healthcheck-client" certificate and key
[certs] Generating "apiserver-etcd-client" certificate and key
[certs] Generating "sa" key and public key

이전 글에서 살펴본, Kubernetes The Hard Way에서 OpenSSL로 일일이 생성했던 인증서들이 자동으로 생성된다:

인증서 용도
ca 클러스터 루트 CA (모든 인증서의 신뢰 기반)
apiserver API Server의 TLS 서빙 인증서
apiserver-kubelet-client API Server가 kubelet에 접근할 때 사용
front-proxy-ca/client API Aggregation Layer용
etcd/ca, etcd/server, etcd/peer etcd 클러스터 내부 통신용
etcd/healthcheck-client etcd 헬스체크용
apiserver-etcd-client API Server가 etcd에 접근할 때 사용
sa (Service Account) ServiceAccount 토큰 서명용 키 쌍


apiserver 인증서의 SAN(Subject Alternative Name)에 다양한 DNS와 IP가 포함됨에 주목하자.

  • DNS: k8s-ctr, kubernetes, kubernetes.default, kubernetes.default.svc, kubernetes.default.svc.cluster.local
  • IP: 10.96.0.1 (Service CIDR의 첫 번째 IP), 192.168.10.100 (advertiseAddress)

다시 한 번 짚고 넘어 가지만, 클라이언트가 API Server에 접속할 때 접속 주소가 인증서의 SAN에 포함되어 있어야 TLS 검증이 통과된다. 예를 들어,

  • Pod 내부에서 https://kubernetes.default.svc:443으로 접속하거나
  • 외부에서 https://192.168.10.100:6443으로 접속할 때 모두 이 인증서로 검증된다.

만약 다음과 같은 경우에는 --apiserver-cert-extra-sans 옵션으로 SAN을 추가해야 한다:

  • HA 구성: 여러 컨트롤 플레인 앞에 Load Balancer를 두는 경우, LB의 IP/DNS가 SAN에 포함되어야 함
  • 커스텀 도메인: api.mycompany.com 같은 도메인으로 접근하려는 경우
  • 클라우드 환경: 외부 IP(Public IP)가 내부 IP와 다른 경우


3단계: [kubeconfig] kubeconfig 파일 생성

각 컴포넌트가 API Server에 인증할 때 사용하는 kubeconfig 파일이 생성된다.

[kubeconfig] Using kubeconfig folder "/etc/kubernetes"
[kubeconfig] Writing "admin.conf" kubeconfig file
[kubeconfig] Writing "super-admin.conf" kubeconfig file
[kubeconfig] Writing "kubelet.conf" kubeconfig file
[kubeconfig] Writing "controller-manager.conf" kubeconfig file
[kubeconfig] Writing "scheduler.conf" kubeconfig file
파일 용도
admin.conf 클러스터 관리자용 (kubectl 사용 시)
super-admin.conf 최고 권한 관리자용 (1.29+에서 추가)
kubelet.conf kubelet이 API Server에 연결할 때 사용
controller-manager.conf Controller Manager가 API Server에 연결할 때 사용
scheduler.conf Scheduler가 API Server에 연결할 때 사용


4단계: [etcd], [control-plane] Static Pod 매니페스트 생성

etcd와 컨트롤 플레인 컴포넌트들의 Static Pod 매니페스트가 /etc/kubernetes/manifests/에 생성된다. kubelet은 이 디렉토리를 감시하다가 매니페스트가 생성되면 자동으로 Pod를 실행한다.

[etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
[control-plane] Using manifest folder "/etc/kubernetes/manifests"
[control-plane] Creating static Pod manifest for "kube-apiserver"
[control-plane] Creating static Pod manifest for "kube-controller-manager"
[control-plane] Creating static Pod manifest for "kube-scheduler"


5단계: [kubelet-start] kubelet 시작

kubelet을 시작한다. kubelet이 시작되면 /etc/kubernetes/manifests/의 Static Pod들을 실행한다.

[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Starting the kubelet
  • /var/lib/kubelet/kubeadm-flags.env: kubelet 시작 시 전달할 플래그 (node-ip 등)
  • /var/lib/kubelet/config.yaml: kubelet 설정 파일 (이 파일이 없어서 이전에 crashloop이었음)


6단계: [wait-control-plane] 컨트롤 플레인 대기

이 단계에서 이미지 사전 다운로드의 효과가 나타난다. 최대 4분까지 기다릴 수 있다고 하지만, 실제로는 수 초 만에 완료된다.

[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory "/etc/kubernetes/manifests"
[kubelet-check] Waiting for a healthy kubelet at http://127.0.0.1:10248/healthz. This can take up to 4m0s
[kubelet-check] The kubelet is healthy after 1.002214971s
[api-check] Waiting for a healthy API server. This can take up to 4m0s
[api-check] The API server is healthy after 3.003031359s
  • kubelet 헬스체크 (:10248/healthz): 약 1초 만에 healthy
  • API Server 헬스체크: 약 3초 만에 healthy


7단계: [upload-config] 설정 업로드

이제 API Server가 동작하므로, 클러스터 자체에 설정을 저장할 수 있다. 이렇게 하면 나중에 업그레이드하거나 노드를 추가할 때 일관된 설정을 사용할 수 있다.

[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[kubelet] Creating a ConfigMap "kubelet-config" in namespace kube-system with the configuration for the kubelets in the cluster
  • kubeadm-config: kubeadm init에 사용된 설정을 ConfigMap으로 저장 (kubeadm upgrade 시 참조)
  • kubelet-config: 클러스터 내 모든 kubelet이 공유할 설정 (워커 노드 join 시 참조)


8단계: [mark-control-plane] 컨트롤 플레인 마킹

컨트롤 플레인 노드는 etcd, API Server 등 핵심 컴포넌트가 실행되므로, 일반 워크로드와 분리하여 안정성을 확보해야 한다. Label과 Taint로 이를 구현한다.

[mark-control-plane] Marking the node k8s-ctr as control-plane by adding the labels: [node-role.kubernetes.io/control-plane node.kubernetes.io/exclude-from-external-load-balancers]
[mark-control-plane] Marking the node k8s-ctr as control-plane by adding the taints [node-role.kubernetes.io/control-plane:NoSchedule]
  • Label 추가: node-role.kubernetes.io/control-planekubectl get nodes에서 역할 표시
  • Taint 추가: node-role.kubernetes.io/control-plane:NoSchedule → 일반 Pod가 스케줄링되지 않음


9단계: [bootstrap-token] 부트스트랩 토큰 설정

부트스트랩 토큰은 워커 노드가 클러스터에 join할 때 사용하는 임시 인증 수단이다. 이 토큰으로 인증한 후 kubelet은 자신의 인증서를 발급받아 장기 자격 증명으로 전환한다.

[bootstrap-token] Using token: 123456.1234567890123456
[bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles
[bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to get nodes
[bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials
[bootstrap-token] Configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
[bootstrap-token] Configured RBAC rules to allow certificate rotation for all node client certificates in the cluster
[bootstrap-token] Creating the "cluster-info" ConfigMap in the "kube-public" namespace
  • 설정 파일에서 지정한 고정 토큰(123456.1234567890123456)이 사용됨
  • RBAC 규칙 설정: 워커 노드가 토큰으로 join하고 인증서를 발급받을 수 있도록
  • cluster-info ConfigMap: kube-public 네임스페이스에 생성되어 워커 노드가 클러스터 정보를 가져갈 수 있음


10단계: [addons] 애드온 설치

클러스터가 정상 작동하려면 DNS와 네트워크 프록시가 필수다. kubeadm은 이 두 가지 핵심 애드온을 자동으로 설치한다.

[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy
  • CoreDNS: 클러스터 내부 DNS 서비스 (Deployment로 배포, CNI 설치 전까지 Pending 상태)
  • kube-proxy: 각 노드의 네트워크 프록시 (DaemonSet으로 배포)

완료

드디어 완료 메시지를 볼 수 있다.

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

  export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.10.100:6443 --token 123456.1234567890123456 \
        --discovery-token-ca-cert-hash sha256:bd763182471f1ed47780644230f234a89061a29041a922a74c849a48342c797d

완료 메시지에서 중요한 정보를 살펴 보자.

  1. kubeconfig 설정 방법: kubectl 사용을 위한 설정
  2. CNI 플러그인 설치 필요: 아직 Pod 네트워크가 없음
  3. 워커 노드 join 명령어: 토큰과 CA cert hash가 포함된 명령어 → 워커 노드에서 그대로 복사하여 실행하면 됨

참고: 토큰을 고정해두었기 때문에 join 명령어의 토큰 부분이 항상 동일하다. CA cert hash만 기억해두면 워커 노드 join 시 바로 사용할 수 있다.

완료 메시지에서 보이는 아래 커맨드를 워커 노드 join 시 바로 사용할 수 있다.

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.10.100:6443 --token 123456.1234567890123456 \
        --discovery-token-ca-cert-hash sha256:bd763182471f1ed47780644230f234a89061a29041a922a74c849a48342c797d


crictl로 컨트롤 플레인 컴포넌트 확인

kubeadm init이 완료된 직후 crictl로 실행 중인 컨테이너를 확인해 보자.

crictl images
# IMAGE                                     TAG                 IMAGE ID            SIZE
# registry.k8s.io/coredns/coredns           v1.11.3             2f6c962e7b831       16.9MB
# registry.k8s.io/etcd                      3.5.24-0            1211402d28f58       21.9MB
# registry.k8s.io/kube-apiserver            v1.32.11            58951ea1a0b5d       26.4MB
# registry.k8s.io/kube-controller-manager   v1.32.11            82766e5f2d560       24.2MB
# registry.k8s.io/kube-proxy                v1.32.11            dcdb790dc2bfe       27.6MB
# registry.k8s.io/kube-scheduler            v1.32.11            cfa17ff3d6634       19.2MB
# registry.k8s.io/pause                     3.10                afb61768ce381       268kB

crictl ps
# CONTAINER      IMAGE          CREATED          STATE     NAME                      POD
# dc8f81e24dff7  dcdb790dc2bfe  18 minutes ago   Running   kube-proxy                kube-proxy-5p6jx
# 28856e606823f  58951ea1a0b5d  18 minutes ago   Running   kube-apiserver            kube-apiserver-k8s-ctr
# e7593756117ad  1211402d28f58  18 minutes ago   Running   etcd                      etcd-k8s-ctr
# 61a09c44673c6  cfa17ff3d6634  18 minutes ago   Running   kube-scheduler            kube-scheduler-k8s-ctr
# 65d20308c4200  82766e5f2d560  18 minutes ago   Running   kube-controller-manager   kube-controller-manager-k8s-ctr

사전에 다운로드한 7개의 이미지가 모두 사용되고 있다. 실행 중인 컨테이너를 보면:

  • Static Pod: kube-apiserver, etcd, kube-scheduler, kube-controller-manager (이름에 -k8s-ctr 노드명 포함)
  • DaemonSet Pod: kube-proxy (이름이 랜덤 suffix)
  • coredns는 CNI 플러그인 설치 전이라 아직 Pending 상태 (컨테이너로 보이지 않음)


kubeconfig 설정

kubectl을 사용하기 위해 kubeconfig를 설정한다.

mkdir -p /root/.kube
cp -i /etc/kubernetes/admin.conf /root/.kube/config
chown $(id -u):$(id -g) /root/.kube/config

/root/.kube/config

kubectl은 기본적으로 $HOME/.kube/config 파일에서 클러스터 접속 정보를 읽는다. 현재 root 사용자이므로 /root/.kube/config에 설정한다. 일반 사용자라면 /home/<username>/.kube/config가 된다.

admin.conf

이전 글에서 살펴본 것처럼, /etc/kubernetes/admin.conf클러스터 관리자 권한이 포함된 kubeconfig 파일이다:

  • 인증서: kubernetes-admin 사용자의 클라이언트 인증서 (Base64 인코딩)
  • API Server 주소: https://192.168.10.100:6443
  • 클러스터 CA: API Server 인증서 검증용

이 파일을 복사하면 kubectl이 API Server에 인증하고 모든 리소스에 접근할 수 있다.


초기 상태 확인

이제 클러스터 컨트롤 플레인 구성 및 kubectl 설정을 완료했으니, 클러스터 초기 상태를 확인해 보자.

kubectl cluster-info
# Kubernetes control plane is running at https://192.168.10.100:6443
# CoreDNS is running at https://192.168.10.100:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

컨트롤 플레인이 192.168.10.100:6443에서 실행 중임을 확인할 수 있다.

노드 상태

클러스터에 등록된 노드 정보를 확인한다. 현재 컨트롤 플레인만 초기화했기 때문에, 컨트롤 플레인 노드만 확인할 수 있다.

kubectl get node -o wide
# NAME      STATUS     ROLES           AGE   VERSION    INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                        KERNEL-VERSION                  CONTAINER-RUNTIME
# k8s-ctr   NotReady   control-plane   20m   v1.32.11   192.168.10.100   <none>        Rocky Linux 10.0 (Red Quartz)   6.12.0-55.39.1.el10_0.aarch64   containerd://2.1.5
필드 설명
STATUS NotReady CNI 플러그인 미설치로 네트워크 준비 안됨
ROLES control-plane 8단계에서 추가한 Label
INTERNAL-IP 192.168.10.100 node-ip로 지정한 클러스터 통신용 IP
CONTAINER-RUNTIME containerd://2.1.5 설치한 containerd 버전

노드 리소스 정보

kubelet이 노드의 리소스를 API Server에 보고한다. 스케줄러는 이 정보를 바탕으로 Pod 배치를 결정한다.

kubectl get nodes -o json | jq ".items[] | {name:.metadata.name} + .status.capacity"
# {
#   "name": "k8s-ctr",
#   "cpu": "4",
#   "ephemeral-storage": "60970Mi",
#   "memory": "2893976Ki",
#   "pods": "110"
# }

kube-system 네임스페이스 리소스 확인

kube-system은 Kubernetes 시스템 컴포넌트가 배포되는 예약된 네임스페이스다.

Pod

컨트롤 플레인 컴포넌트(Static Pod)와 kube-proxy(DaemonSet)는 hostNetwork: true로 호스트 네트워크를 사용하므로 Running 상태지만, CoreDNS(Deployment)는 Pod 네트워크가 필요하여 CNI 플러그인 없이는 IP를 할당받지 못해 Pending 상태다.

CNI를 설치하면 노드가 Ready가 되고, CoreDNS도 Pod IP를 할당받아 Running 상태가 된다.

kubectl get pod -n kube-system -o wide
# NAME                              READY   STATUS    RESTARTS   AGE   IP               NODE      NOMINATED NODE   READINESS GATES
# coredns-668d6bf9bc-n8jxf          0/1     Pending   0          21m   <none>           <none>    <none>           <none>
# coredns-668d6bf9bc-z6h69          0/1     Pending   0          21m   <none>           <none>    <none>           <none>
# etcd-k8s-ctr                      1/1     Running   0          21m   192.168.10.100   k8s-ctr   <none>           <none>
# kube-apiserver-k8s-ctr            1/1     Running   0          21m   192.168.10.100   k8s-ctr   <none>           <none>
# kube-controller-manager-k8s-ctr   1/1     Running   0          21m   192.168.10.100   k8s-ctr   <none>           <none>
# kube-proxy-5p6jx                  1/1     Running   0          21m   192.168.10.100   k8s-ctr   <none>           <none>
# kube-scheduler-k8s-ctr            1/1     Running   0          21m   192.168.10.100   k8s-ctr   <none>           <none>
Pod 유형 상태 설명
etcd-k8s-ctr Static Pod Running 클러스터 데이터 저장소
kube-apiserver-k8s-ctr Static Pod Running API Server
kube-controller-manager-k8s-ctr Static Pod Running 컨트롤러 매니저
kube-scheduler-k8s-ctr Static Pod Running 스케줄러
kube-proxy-5p6jx DaemonSet Running 네트워크 프록시
coredns-* Deployment Pending CNI 없어서 스케줄링 불가

Service

kubeadm 초기화 직후에는 kube-dns 서비스만 존재한다. CoreDNS Pod이지만 서비스명은 kube-dns인데, 이는 기존 kube-dns와의 호환성을 위한 것이다. Pod 내부에서 DNS 조회 시 /etc/resolv.confnameserver 10.96.0.10이 설정된다.

kubectl get svc -n kube-system
# NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
# kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   22m

Lease

컨트롤 플레인 컴포넌트와 kubelet이 시작되면서 Lease 리소스가 자동으로 생성된다.

# Leader Election용 Lease (kube-system)
kubectl get lease -n kube-system
# NAME                      HOLDER                                         AGE
# kube-controller-manager   k8s-ctr_...                                    25m
# kube-scheduler            k8s-ctr_...                                    25m

# Node Heartbeat용 Lease (kube-node-lease)
kubectl get lease -n kube-node-lease
# NAME      HOLDER    AGE
# k8s-ctr   k8s-ctr   25m
Lease 네임스페이스 생성 주체 용도
kube-scheduler kube-system kube-scheduler 스케줄러 Leader Election
kube-controller-manager kube-system kube-controller-manager 컨트롤러 매니저 Leader Election
<노드명> kube-node-lease kubelet 노드 상태 보고 (Heartbeat)

참고: Leader Election은 HA 구성에서 여러 인스턴스 중 하나만 활성화하기 위한 메커니즘이다. 현재 단일 컨트롤 플레인이므로 Leader가 하나뿐이지만, --leader-elect=true가 기본 활성화되어 있어 HA 확장 시 자동으로 동작한다. Node Lease는 노드 상태를 경량화된 방식으로 보고하여 etcd 부하를 줄인다.


TLS Bootstrap을 위한 객체들

kubeadm init은 워커 노드가 클러스터에 join할 수 있도록 부트스트랩 인프라를 자동으로 구성한다. 해당 인프라는 아직 클러스터 인증서가 없는 노드(worker)가 API Server에 처음 접속해서 최소 정보(엔드포인트 + CA)를 얻기 위해 필요하다.

이 객체들이 왜 필요하고, Bootstrap Token과 JWS 서명을 통해 어떻게 MITM을 방어하는지는 init과 join의 신뢰 모델에서 다루었다. 여기서는 실제 생성된 객체들을 확인한다.

각 객체들은 아래와 같다.

객체 이름 용도
Namespace kube-public 공개 리소스 저장용 네임스페이스
ConfigMap cluster-info API Server 엔드포인트 + CA 인증서
Role kubeadm:bootstrap-signer-clusterinfo cluster-info 읽기 권한 정의
RoleBinding kubeadm:bootstrap-signer-clusterinfo User system:anonymous에 Role 부여 (= 인증 없는 요청 허용)

cluster-info ConfigMap

cluster-info ConfigMap은 워커 노드가 클러스터에 join할 때 필요한 부트스트랩 데이터(API Server 주소 + CA 인증서)를 담고 있다. Role/RoleBinding은 인증되지 않은 요청(API Server가 부여하는 User system:anonymous)도 이 ConfigMap을 읽을 수 있도록 권한을 부여한다.

워커 노드 (인증서 없음)
    │
    ▼ curl -k https://API_SERVER/api/v1/namespaces/kube-public/configmaps/cluster-info
    │
    ▼ cluster-info에서 CA 인증서 + API Server 주소 획득
    │
    ▼ 부트스트랩 토큰으로 인증 → CSR 제출 → 인증서 발급
    │
    ▼ 정식 kubelet 인증서로 클러스터 참여

자세한 TLS Bootstrap 과정은 워커 노드 join 글에서 다룬다.

ConfigMap 내용 확인

kubectl -n kube-public get configmap cluster-info
# NAME           DATA   AGE
# cluster-info   2      24m

ConfigMap에는 2개의 데이터가 있다:

  • kubeconfig: API Server 주소와 CA 인증서 (워커 노드가 필요한 정보)
  • jws-kubeconfig-123456: 부트스트랩 토큰으로 서명한 값 (중간자가 ConfigMap을 조작하지 않았음을 검증)

cluster-info에는 CA 인증서만 포함되어 있고, 개인키나 인증 토큰은 없다. CA 인증서는 공개되어도 안전하며, 오히려 클라이언트가 API Server를 검증하는 데 필요하다.

cluster-info Configmap 전문
kubectl -n kube-public get configmap cluster-info -o yaml
apiVersion: v1
data:
  jws-kubeconfig-123456: eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1NiJ9..MmO9sDL7qDG1eP_KcoXgZMoGzFsv2oonAqcC3JP0cVU # JWS 값
  kubeconfig: |
    apiVersion: v1
    clusters:
    - cluster:
        certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJWmgxTEFtbENIZWd3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBek1ERXhOREE1TlRKYUZ3MHpOakF5TWpjeE5ERTBOVEphTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUNyQWhCZ1ZwTHpwbnBSVU8vK3pPTnZYek9OQlNNaE16c1gySno3OWFyN0ZEcFFqSklWSHhJc2x0dkYKOUxtU3J3UGhZTm1KSkVNL3NlN2ZOdTI5MFZUYXpKOFNsM2NETVBsMWcwRXdWUW9ZTldJNDcxaDMxL054RHNQQQpGalgwNVVoWTZGOW52SjYybnpIbFZ0TGJCMDJmcG5WRHd5TEdST0s3UGxPVFVBVXJGeGlzejRtUUJXNkNtZlU1CkxFY2drdE9sYTJ6VUR2QzRTOGdxWW5tTWtJWThUd0dtVkp3eUs0bzQ1bXBGS2FyN3Y1dC9lVFlram5XenRtckUKdjBaOGhROEl4UFdoamZ1b0VEdTREVXkrcHZHbWxoMENpYUlCUGhPNVIySklzcFJNQnZCY2ZqYlltQmlQSVMxRQpPWkVsazYyU3I2S2hPYXJXOFZTcjRJYmcrN3cvQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJST2RCSUlSTVYrYTFqZTNWMis4THdZcGxuckRqQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQnlzTHppUXhzOQphWm5WSWcrdmdES3ZsYzd0cy9UY29HTmdJYzlMUzNmQkppWVVtaGhKcmdUVTFkRFhha0ZwV0FGK1dpOEplMTB5ClFaR3d6dGQxUk9nSHVCK3ZtRFdiS3NJMkxUMDRlRklSUnVQTVU0UThSblZ0Wnh5QlFwWVV2dVJDbFFwYWhkUmcKVjUxK2NSUU4wZG5uWDB2WkFWWjA2RU9mOWV5dEVXRkd1d1FacHJNM1VVaklldUZ6UHI4bDhmUUdoSnBDNHNjQQozalVEcUJtbEVkTW5FWnJxSmMzd3JjaUduQXFSTzNBYjBLbVdleE12WjVKd0JKd2IyZ1VVZXhURUs4MEdteC9SCnhhb0pPMGNZSGRlNkh0UXZNR3Y4d3F2SDJVWDRUL2I0dDNFQlFod0J3ZmFtSGRHL0M2UDJETkt6ZkExdlNPS2QKSkszdTRVM3lRVXBBCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
        server: https://192.168.10.100:6443
      name: ""
    contexts: null
    current-context: ""
    kind: Config
    preferences: {}
    users: null
kind: ConfigMap
metadata:
  creationTimestamp: "2026-01-23T10:41:10Z"
  name: cluster-info
  namespace: kube-public
  resourceVersion: "311"
  uid: a83f9ad3-d842-4575-b310-51be6d50be7b

Role/RoleBinding 확인

이 ConfigMap이 인증 없이 접근 가능한 이유는 RBAC 설정 때문이다.

kubectl -n kube-public get role,rolebinding
# NAME                                               CREATED AT
# role.rbac.../kubeadm:bootstrap-signer-clusterinfo  2026-01-23T10:41:10Z
# role.rbac.../system:controller:bootstrap-signer    2026-01-23T10:41:10Z
# NAME                                                      ROLE                                        AGE
# rolebinding.../kubeadm:bootstrap-signer-clusterinfo       Role/kubeadm:bootstrap-signer-clusterinfo   25m
# rolebinding.../system:controller:bootstrap-signer         Role/system:controller:bootstrap-signer     25m

RoleBinding kubeadm:bootstrap-signer-clusterinfosubjectsUser system:anonymous가 있다. API Server는 인증 정보가 없는 요청에 이 사용자 identity를 부여하므로, 이 RoleBinding은 “인증 없는 요청에게 해당 Role 부여”를 의미한다. Role kubeadm:bootstrap-signer-clusterinforesourceNames: [cluster-info], verbs: [get]이므로, 결과적으로 인증 없이 cluster-info ConfigMap 읽기만 허용된다.

# Role kubeadm:bootstrap-signer-clusterinfo: cluster-info에 대한 get만 허용
rules:
- apiGroups: [""]
  resourceNames: [cluster-info]
  resources: [configmaps]
  verbs: [get]

# RoleBinding kubeadm:bootstrap-signer-clusterinfo: system:anonymous(= 인증 없는 요청)에 Role 부여
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: system:anonymous
Role / RoleBinding 전문 (kube-public)
kubectl -n kube-public get role,rolebinding -o yaml
apiVersion: v1
items:
- apiVersion: rbac.authorization.k8s.io/v1
  kind: Role
  metadata:
    name: kubeadm:bootstrap-signer-clusterinfo
    namespace: kube-public
  rules:
  - apiGroups: [""]
    resourceNames: [cluster-info]
    resources: [configmaps]
    verbs: [get]
- apiVersion: rbac.authorization.k8s.io/v1
  kind: Role
  metadata:
    name: system:controller:bootstrap-signer
    namespace: kube-public
  rules:
  - apiGroups: [""]
    resources: [configmaps]
    verbs: [get, list, watch]
  - apiGroups: [""]
    resourceNames: [cluster-info]
    resources: [configmaps]
    verbs: [update]
  - apiGroups: ["", "events.k8s.io"]
    resources: [events]
    verbs: [create, patch, update]
- apiVersion: rbac.authorization.k8s.io/v1
  kind: RoleBinding
  metadata:
    name: kubeadm:bootstrap-signer-clusterinfo
    namespace: kube-public
  roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: Role
    name: kubeadm:bootstrap-signer-clusterinfo
  subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: system:anonymous
- apiVersion: rbac.authorization.k8s.io/v1
  kind: RoleBinding
  metadata:
    name: system:controller:bootstrap-signer
    namespace: kube-public
  roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: Role
    name: system:controller:bootstrap-signer
  subjects:
  - kind: ServiceAccount
    name: bootstrap-signer
    namespace: kube-system
kind: List
metadata:
  resourceVersion: ""


참고: API Server의 anonymous auth 동작

API Server는 --anonymous-auth=true(기본값)일 때, 인증 정보가 없는 요청에 자동으로 다음 두 identity를 부여한다:

  • Username: system:anonymous
  • Groups: ["system:unauthenticated"]

따라서 위 RoleBinding은 인증 없는 모든 요청에 적용된다. User system:anonymous에 바인딩하는 것과 Group system:unauthenticated에 바인딩하는 것은 기능적으로 동일하다(인증 안 된 요청은 항상 둘 다 부여받으므로). kubeadm은 User에 직접 바인딩하여 더 좁은 범위를 지정한다.

참고: --enable-bootstrap-token-auth=true 자동 설정

kubeadm은 kube-apiserver Static Pod 매니페스트(/etc/kubernetes/manifests/kube-apiserver.yaml)를 생성할 때 --enable-bootstrap-token-auth=true를 자동으로 포함시킨다. 이는 kubeadm의 핵심 워크플로우가 Bootstrap Token 기반 노드 join이기 때문이다:

  1. kubeadm init → Bootstrap Token 생성 (123456.1234567890123456)
  2. kubeadm join –token … → Worker가 이 토큰으로 API Server에 인증

이 흐름이 동작하려면 API Server의 Bootstrap Token Authenticator가 활성화되어야 한다. 이 authenticator가 켜지면 토큰이 Bearer Token으로 사용되어 system:bootstrap:<token-id> 사용자로 인증되고 system:bootstrappers 그룹에 소속된다.

또한, 다음 컨트롤러도 함께 활성화된다:

컨트롤러 역할
tokencleaner 만료된 Bootstrap Token Secret 자동 삭제
bootstrapsigner cluster-info ConfigMap에 JWS 서명 생성/갱신

전체 흐름을 정리하면:

  1. kubeadm join 시 Worker가 인증 없이 GET cluster-info 요청 → system:anonymous로 처리됨 → 위 RoleBinding에 의해 응답 받음
  2. JWS 검증으로 CA 인증서 신뢰 확보
  3. 그 다음 Bootstrap Token으로 인증된 요청을 보내 TLS Bootstrap 진행

실제 설정은 kube-apiserver Static Pod에서 확인할 수 있다.


cluster-info 인증 없이 접근 가능 확인

RoleBinding 덕분에 cluster-info는 Kubernetes API 리소스 중 유일하게 인증 없이 접근 가능하다. 아직 인증서가 없는 새 노드가 이 데이터를 가져와 JWS 서명으로 검증한 뒤 CA를 신뢰하는 것이 TLS Bootstrap의 출발점이다.

curl -s -k https://192.168.10.100:6443/api/v1/namespaces/kube-public/configmaps/cluster-info | jq '.data | keys'
# [
#   "jws-kubeconfig-123456",
#   "kubeconfig"
# ]
응답 전문 (인증 없이 `cluster-info` GET 시)
{
  "kind": "ConfigMap",
  "apiVersion": "v1",
  "metadata": {
    "name": "cluster-info",
    "namespace": "kube-public",
    "uid": "a83f9ad3-d842-4575-b310-51be6d50be7b",
    "resourceVersion": "311",
    "creationTimestamp": "2026-01-23T10:41:10Z",
    "managedFields": [
      {
        "manager": "kubeadm",
        "operation": "Update",
        "apiVersion": "v1",
        "time": "2026-01-23T10:41:10Z",
        "fieldsType": "FieldsV1",
        "fieldsV1": {
          "f:data": {
            ".": {},
            "f:kubeconfig": {}
          }
        }
      },
      {
        "manager": "kube-controller-manager",
        "operation": "Update",
        "apiVersion": "v1",
        "time": "2026-01-23T10:41:17Z",
        "fieldsType": "FieldsV1",
        "fieldsV1": {
          "f:data": {
            "f:jws-kubeconfig-123456": {}
          }
        }
      }
    ]
  },
  "data": {
    "jws-kubeconfig-123456": "eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1NiJ9..MmO9sDL7qDG1eP_KcoXgZMoGzFsv2oonAqcC3JP0cVU",
    "kubeconfig": "apiVersion: v1\nclusters:\n- cluster:\n    certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJWmgxTEFtbENIZWd3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBek1ERXhOREE1TlRKYUZ3MHpOakF5TWpjeE5ERTBOVEphTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUNyQWhCZ1ZwTHpwbnBSVU8vK3pPTnZYek9OQlNNaE16c1gySno3OWFyN0ZEcFFqSklWSHhJc2x0dkYKOUxtU3J3UGhZTm1KSkVNL3NlN2ZOdTI5MFZUYXpKOFNsM2NETVBsMWcwRXdWUW9ZTldJNDcxaDMxL054RHNQQQpGalgwNVVoWTZGOW52SjYybnpIbFZ0TGJCMDJmcG5WRHd5TEdST0s3UGxPVFVBVXJGeGlzejRtUUJXNkNtZlU1CkxFY2drdE9sYTJ6VUR2QzRTOGdxWW5tTWtJWThUd0dtVkp3eUs0bzQ1bXBGS2FyN3Y1dC9lVFlram5XenRtckUKdjBaOGhROEl4UFdoamZ1b0VEdTREVXkrcHZHbWxoMENpYUlCUGhPNVIySklzcFJNQnZCY2ZqYlltQmlQSVMxRQpPWkVsazYyU3I2S2hPYXJXOFZTcjRJYmcrN3cvQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJST2RCSUlSTVYrYTFqZTNWMis4THdZcGxuckRqQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQnlzTHppUXhzOQphWm5WSWcrdmdES3ZsYzd0cy9UY29HTmdJYzlMUzNmQkppWVVtaGhKcmdUVTFkRFhha0ZwV0FGK1dpOEplMTB5ClFaR3d6dGQxUk9nSHVCK3ZtRFdiS3NJMkxUMDRlRklSUnVQTVU0UThSblZ0Wnh5QlFwWVV2dVJDbFFwYWhkUmcKVjUxK2NSUU4wZG5uWDB2WkFWWjA2RU9mOWV5dEVXRkd1d1FacHJNM1VVaklldUZ6UHI4bDhmUUdoSnBDNHNjQQozalVEcUJtbEVkTW5FWnJxSmMzd3JjaUduQXFSTzNBYjBLbVdleE12WjVKd0JKd2IyZ1VVZXhURUs4MEdteC9SCnhhb0pPMGNZSGRlNkh0UXZNR3Y4d3F2SDJVWDRUL2I0dDNFQlFod0J3ZmFtSGRHL0M2UDJETkt6ZkExdlNPS2QKSkszdTRVM3lRVXBBCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K\n    server: https://192.168.10.100:6443\n  name: \"\"\ncontexts: null\ncurrent-context: \"\"\nkind: Config\npreferences: {}\nusers: null\n"
  }
}

반면 다른 리소스는 인증 없이 접근하면 403 Forbidden 에러가 발생한다.

curl -s -k https://192.168.10.100:6443/api/v1/namespaces/default/pods | jq '.message'
# "pods is forbidden: User \"system:anonymous\" cannot list resource \"pods\" in API group \"\" in the namespace \"default\""


CA 인증서 확인

kubectl -n kube-public get configmap cluster-info -o jsonpath='{.data.kubeconfig}' | \
  grep certificate-authority-data | cut -d ':' -f2 | tr -d ' ' | base64 -d | openssl x509 -text -noout
# Certificate:
#     Data:
#         Issuer: CN=kubernetes
#         Validity
#             Not Before: Jan 23 10:36:04 2026 GMT
#             Not After : Jan 21 10:41:04 2036 GMT      # 10년 유효
#         Subject: CN=kubernetes
#         X509v3 Basic Constraints: critical
#             CA:TRUE                                   # CA 인증서임
#         X509v3 Subject Alternative Name:
#             DNS:kubernetes

이 CA 인증서는 /etc/kubernetes/pki/ca.crt와 동일하며, 워커 노드가 API Server의 인증서를 검증할 때 사용한다.


결과

이 단계를 완료하면 다음과 같은 결과를 얻을 수 있다:

항목 결과
컨트롤 플레인 kube-apiserver, kube-controller-manager, kube-scheduler, etcd 실행 중
인증서 /etc/kubernetes/pki에 모든 인증서 생성됨
kubeconfig admin.conf, controller-manager.conf, scheduler.conf, kubelet.conf 생성됨
노드 상태 NotReady (CNI 플러그인 미설치)
CoreDNS 상태 Pending (CNI 플러그인 미설치)


노드가 NotReady이고 CoreDNS가 Pending인 상태다. 이는 CNI 플러그인이 없어서 Pod 네트워크를 구성할 수 없기 때문이다. 다음 글에서 kubectl 편의 도구를 설치한 뒤, 그 다음에서 Flannel CNI를 설치한다.





hit count

댓글남기기