콘텐츠로 이동

Bootstrap Kubernetes The Hard Way

새로운 스터디 시작: Kubernetes 클러스터를 처음부터 수동으로 구축하며 내부 동작 원리를 깊이 이해하는 K8s-Deploy 스터디의 첫 번째 주차이다.


K8s-Deploy 스터디 소개

1. 스터디 목표

K8s-Deploy 스터디는 Kubernetes 클러스터 배포와 운영에 대한 깊은 이해를 목표로 한다.

2. Week 1 학습 목표

Kubernetes The Hard Way는 Kubernetes 클러스터를 kubeadm과 같은 자동화 도구 없이 처음부터 수동으로 구축하는 실습이다.

이번 주 핵심 학습 포인트: - Kubernetes 각 컴포넌트의 역할과 상호작용 이해 - mTLS(상호 TLS) 통신을 위한 CA 및 인증서 생성 - 인증서 Subject(CN, O)와 Kubernetes RBAC의 연관성 - Systemd를 사용한 각 컴포넌트 서비스 관리 - etcd에 저장되는 Secret 데이터 암호화 - CNI 없이 수동 라우팅으로 Pod 네트워크 구성

왜 "The Hard Way"인가? - 자동화 도구가 숨기고 있는 내부 동작을 직접 경험 - 트러블슈팅 능력 향상 - 프로덕션 환경 운영에 필요한 깊은 이해

3. 실습 환경 구성

가상머신 구성 (VirtualBox + Vagrant, Debian 12):

호스트명 IP 주소 역할 vCPU Memory
jumpbox 192.168.10.10 관리 호스트 2 1.5GB
server 192.168.10.100 Control Plane 2 2GB
node-0 192.168.10.101 Worker Node 2 2GB
node-1 192.168.10.102 Worker Node 2 2GB

네트워크 대역:

  • Cluster CIDR: 10.200.0.0/16
  • node-0 Pod CIDR: 10.200.0.0/24
  • node-1 Pod CIDR: 10.200.1.0/24
  • Service CIDR: 10.32.0.0/24
  • kubernetes Service ClusterIP: 10.32.0.1

컴포넌트 버전: - Kubernetes: v1.32.2 - etcd: v3.6.0 - containerd: v2.1.0 - runc: v1.3.0

실습 아키텍처:

graph TB
    subgraph Cluster["Kubernetes Cluster (192.168.10.0/24)"]
        subgraph ControlPlane["Control Plane (192.168.10.100)"]
            API["kube-apiserver<br/>:6443"]
            ETCD["etcd<br/>:2379"]
            CM["kube-controller-manager<br/>:10257"]
            SCHED["kube-scheduler<br/>:10259"]
        end

        subgraph Node0["Worker Node-0 (192.168.10.101)"]
            KUBELET0["kubelet<br/>:10250"]
            PROXY0["kube-proxy"]
            CONTAINERD0["containerd"]
            POD0["Pod CIDR<br/>10.200.0.0/24"]
        end

        subgraph Node1["Worker Node-1 (192.168.10.102)"]
            KUBELET1["kubelet<br/>:10250"]
            PROXY1["kube-proxy"]
            CONTAINERD1["containerd"]
            POD1["Pod CIDR<br/>10.200.1.0/24"]
        end
    end

    API ---|mTLS| ETCD
    API ---|mTLS| KUBELET0
    API ---|mTLS| KUBELET1
    CM ---|kubeconfig| API
    SCHED ---|kubeconfig| API
    KUBELET0 ---|kubeconfig| API
    KUBELET1 ---|kubeconfig| API
    PROXY0 ---|kubeconfig| API
    PROXY1 ---|kubeconfig| API

CA 및 TLS 인증서 구성

1. Root CA 생성

Kubernetes 클러스터의 모든 인증서를 서명하는 Root CA 생성:

# CA 개인키 생성 (RSA 4096 bits)
openssl genrsa -out ca.key 4096

# CA 인증서 생성 (Self-Signed, 10년 유효기간)
openssl req -x509 -new -sha512 -noenc \
  -key ca.key -days 3653 \
  -config ca.conf \
  -out ca.crt
CA 인증서 특징: - basicConstraints = CA:TRUE - CA 역할 가능 - keyUsage = cRLSign, keyCertSign - 다른 인증서를 서명 가능 - Kubernetes 모든 인증의 신뢰 루트(Root of Trust)

ca.conf 주요 섹션:

[ca_x509_extensions]
basicConstraints = CA:TRUE
keyUsage = cRLSign, keyCertSign

[req_distinguished_name]
C = US
ST = Washington
L = Seattle
CN = CA
인증서 확인:

openssl x509 -in ca.crt -text -noout
인증서 체인 구조:

graph TD
    CA["Root CA<br/>(ca.crt, ca.key)<br/>CN=CA"]

    CA -->|서명| ADMIN["Admin 인증서<br/>(admin.crt)<br/>CN=admin, O=system:masters"]
    CA -->|서명| NODE0["node-0 인증서<br/>(node-0.crt)<br/>CN=system:node:node-0<br/>O=system:nodes"]
    CA -->|서명| NODE1["node-1 인증서<br/>(node-1.crt)<br/>CN=system:node:node-1<br/>O=system:nodes"]
    CA -->|서명| PROXY["kube-proxy 인증서<br/>(kube-proxy.crt)<br/>CN=system:kube-proxy"]
    CA -->|서명| SCHED["kube-scheduler 인증서<br/>(kube-scheduler.crt)<br/>CN=system:kube-scheduler"]
    CA -->|서명| CM["kube-controller-manager 인증서<br/>(kube-controller-manager.crt)"]
    CA -->|서명| API["kube-apiserver 인증서<br/>(kube-api-server.crt)<br/>CN=kubernetes<br/>SAN: 10.32.0.1, server.kubernetes.local"]
    CA -->|서명| SA["service-accounts 인증서<br/>(service-accounts.crt)<br/>SA 토큰 서명용"]

    ADMIN -.->|kubectl| APISERVER["kube-apiserver"]
    NODE0 -.->|kubelet| APISERVER
    NODE1 -.->|kubelet| APISERVER
    PROXY -.->|kube-proxy| APISERVER
    SCHED -.->|scheduler| APISERVER
    CM -.->|controller-manager| APISERVER

2. Admin 클라이언트 인증서

kubectl을 사용할 관리자용 클라이언트 인증서:

# 개인키 생성
openssl genrsa -out admin.key 4096

# CSR 생성 (CN=admin, O=system:masters)
openssl req -new -key admin.key -sha256 \
  -config ca.conf -section admin \
  -out admin.csr

# CA가 서명한 인증서 생성
openssl x509 -req -days 3653 -in admin.csr \
  -copy_extensions copyall \
  -sha256 -CA ca.crt -CAkey ca.key \
  -CAcreateserial \
  -out admin.crt
Admin 인증서의 중요 필드:

Subject: CN=admin, O=system:masters
X509v3 Extended Key Usage: TLS Web Client Authentication
핵심 포인트: - CN = admin: Kubernetes에서 user로 매핑 - O = system:masters: Kubernetes에서 group으로 매핑 - system:masters 그룹은 인가를 우회하는 슈퍼유저 권한 보유

RBAC 확인:

kubectl describe clusterrolebindings cluster-admin

# Subjects:
# Kind Name Namespace
# ---- ---- ---------
# Group system:masters
system:masters 그룹의 특별함: - ClusterRole cluster-admin에 바인딩 - 모든 리소스(*.*)에 대한 모든 동작(*) 허용 - 인증 후 인가를 우회 - 프로덕션에서는 사용 제한 권장

3. Node(Kubelet) 인증서

Worker Node의 kubelet이 사용하는 인증서:

# node-0 인증서 생성
openssl genrsa -out node-0.key 4096

openssl req -new -key node-0.key -sha256 \
  -config ca.conf -section node-0 \
  -out node-0.csr

openssl x509 -req -days 3653 -in node-0.csr \
  -copy_extensions copyall \
  -sha256 -CA ca.crt -CAkey ca.key \
  -CAcreateserial \
  -out node-0.crt
Node 인증서의 중요 필드:

Subject: CN=system:node:node-0, O=system:nodes, C=US, ST=Washington, L=Seattle
X509v3 Extended Key Usage:
    TLS Web Client Authentication, TLS Web Server Authentication
X509v3 Subject Alternative Name:
    DNS:node-0, IP Address:127.0.0.1
핵심 포인트: - CN = system:node:node-0: Node Authorizer가 요구하는 명명 규칙 (system:node:<nodeName>) - O = system:nodes: Node Authorizer가 요구하는 그룹 - EKU: - clientAuth: kube-apiserver → kubelet 호출 시 - serverAuth: kubelet HTTPS 서버 역할 시 (포트 10250) - SAN: kubelet API 서버 접근을 위한 DNS/IP

Node Authorizer: - Kubelet이 수행하는 API 요청에 대한 권한을 특별히 부여하는 특수 목적 권한 부여 모드 - --authorization-mode=Node,RBAC에서 Node가 먼저 평가됨 - system:nodes 그룹과 system:node:<nodeName> 사용자에게만 적용

4. 컴포넌트별 인증서 생성

생성해야 할 인증서 목록:

컴포넌트 CN O 용도
kube-proxy system:kube-proxy system:node-proxier 서비스 네트워크 제어
kube-scheduler system:kube-scheduler system:kube-scheduler Pod 스케줄링 전용 권한
kube-controller-manager system:kube-controller-manager system:kube-controller-manager 클러스터 상태 관리 (Node, ReplicaSet, SA 토큰 등)
kube-apiserver kubernetes - API 서버
service-accounts service-accounts - SA 토큰 서명용

일괄 생성 스크립트:

certs=("node-0" "node-1" "kube-proxy" "kube-scheduler" \
       "kube-controller-manager" "kube-api-server" "service-accounts")

for i in ${certs[*]}; do
  # 개인키 생성
  openssl genrsa -out "${i}.key" 4096

  # CSR 생성
  openssl req -new -key "${i}.key" -sha256 \
    -config ca.conf -section ${i} \
    -out "${i}.csr"

  # CA 서명 인증서 생성
  openssl x509 -req -days 3653 -in "${i}.csr" \
    -copy_extensions copyall \
    -sha256 -CA ca.crt -CAkey ca.key \
    -CAcreateserial \
    -out "${i}.crt"
done
인증서 검증:

# kube-proxy 인증서 확인
openssl x509 -in kube-proxy.crt -text -noout
# Subject: CN=system:kube-proxy, O=system:node-proxier
# SAN: DNS:kube-proxy, IP:127.0.0.1

# kube-scheduler 인증서 확인
openssl x509 -in kube-scheduler.crt -text -noout
# Subject: CN=system:kube-scheduler, O=system:kube-scheduler

5. API Server 인증서 특징

kube-apiserver 인증서는 특별한다:

Subject: CN=kubernetes, C=US, ST=Washington, L=Seattle
X509v3 Extended Key Usage:
    TLS Web Client Authentication, TLS Web Server Authentication
Netscape Cert Type:
    SSL Client, SSL Server
X509v3 Subject Alternative Name:
    IP Address:127.0.0.1,
    IP Address:10.32.0.1,           # kubernetes Service ClusterIP
    DNS:kubernetes,
    DNS:kubernetes.default,
    DNS:kubernetes.default.svc,
    DNS:kubernetes.default.svc.cluster,
    DNS:kubernetes.svc.cluster.local,
    DNS:server.kubernetes.local,
    DNS:api-server.kubernetes.local
SAN(Subject Alternative Name)에 포함된 항목: - 127.0.0.1: 로컬 접근 - 10.32.0.1: Service CIDR의 첫 번째 IP (kubernetes Service의 ClusterIP) - 모든 내부/외부 접근 주소를 포함하여 TLS 검증 통과

Chrome 정책 변화 (2026년 6월 15일): - TLS 인증서에 서버 인증 EKU만 포함 가능 - 클라이언트 인증 EKU 제거 예정 - Kind K8s는 이미 분리됨: - apiserver.crt: 서버 인증서 (EKU: serverAuth) - apiserver-kubelet-client.crt: 클라이언트 인증서 (EKU: clientAuth) - apiserver-etcd-client.crt: etcd 클라이언트 인증서

인증서 배포:

# Worker Node에 배포
for host in node-0 node-1; do
  ssh root@${host} mkdir /var/lib/kubelet/
  scp ca.crt root@${host}:/var/lib/kubelet/
  scp ${host}.crt root@${host}:/var/lib/kubelet/kubelet.crt
  scp ${host}.key root@${host}:/var/lib/kubelet/kubelet.key
done

# Control Plane에 배포
scp ca.key ca.crt \
    kube-api-server.key kube-api-server.crt \
    service-accounts.key service-accounts.crt \
    root@server:~/

Kubeconfig 파일 생성

1. Kubeconfig 구조 이해

Kubeconfig는 Kubernetes API Server와 통신하기 위한 클라이언트 인증 설정 파일이다.

구성 요소:

clusters:
  - name: kubernetes-the-hard-way
    cluster:
      certificate-authority-data: <base64-encoded-ca.crt>
      server: https://server.kubernetes.local:6443

users:
  - name: admin
    user:
      client-certificate-data: <base64-encoded-admin.crt>
      client-key-data: <base64-encoded-admin.key>

contexts:
  - name: default
    context:
      cluster: kubernetes-the-hard-way
      user: admin

current-context: default
4가지 주요 섹션: 1. clusters: API Server 주소 및 CA 인증서 2. users: 클라이언트 인증 정보 (인증서 또는 토큰) 3. contexts: cluster + user 조합 4. current-context: 현재 사용 중인 context

2. Kubelet용 Kubeconfig

node-0.kubeconfig 생성:

# 1. Cluster 설정
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=node-0.kubeconfig

# 2. Credentials 설정 (중요: user 이름이 인증서 CN과 일치)
kubectl config set-credentials system:node:node-0 \
  --client-certificate=node-0.crt \
  --client-key=node-0.key \
  --embed-certs=true \
  --kubeconfig=node-0.kubeconfig

# 3. Context 설정
kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:node:node-0 \
  --kubeconfig=node-0.kubeconfig

# 4. Current Context 설정
kubectl config use-context default \
  --kubeconfig=node-0.kubeconfig
생성된 node-0.kubeconfig:

apiVersion: v1
kind: Config
clusters:
- cluster:
    certificate-authority-data: LS0tLS...  # base64 encoded ca.crt
    server: https://server.kubernetes.local:6443
  name: kubernetes-the-hard-way
users:
- name: system:node:node-0
  user:
    client-certificate-data: LS0tLS...  # base64 encoded node-0.crt
    client-key-data: LS0tLS...          # base64 encoded node-0.key
contexts:
- context:
    cluster: kubernetes-the-hard-way
    user: system:node:node-0
  name: default
current-context: default
Node Authorizer와의 관계: - Kubelet의 클라이언트 인증서 CN은 kubelet의 node 이름과 일치해야 함 - 이를 통해 Node Authorizer가 Kubelet을 적절히 인가 - system:node:node-0 사용자는 node-0 관련 리소스만 접근 가능

3. 컴포넌트별 Kubeconfig

생성해야 할 Kubeconfig 목록:

파일명 사용 컴포넌트 User Server 배포 위치
node-0.kubeconfig kubelet (node-0) system:node:node-0 https://server.kubernetes.local:6443 /var/lib/kubelet/
node-1.kubeconfig kubelet (node-1) system:node:node-1 https://server.kubernetes.local:6443 /var/lib/kubelet/
kube-proxy.kubeconfig kube-proxy system:kube-proxy https://server.kubernetes.local:6443 /var/lib/kube-proxy/
kube-controller-manager.kubeconfig kube-controller-manager system:kube-controller-manager https://server.kubernetes.local:6443 /var/lib/kubernetes/
kube-scheduler.kubeconfig kube-scheduler system:kube-scheduler https://server.kubernetes.local:6443 /var/lib/kubernetes/
admin.kubeconfig kubectl admin https://127.0.0.1:6443 ~/.kube/config

주의사항: - admin.kubeconfig는 server 주소가 127.0.0.1이다 (로컬 접근) - 나머지는 server.kubernetes.local로 원격 접근

일괄 생성 예시 (kube-proxy):

kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=kube-proxy.kubeconfig

kubectl config set-credentials system:kube-proxy \
  --client-certificate=kube-proxy.crt \
  --client-key=kube-proxy.key \
  --embed-certs=true \
  --kubeconfig=kube-proxy.kubeconfig

kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:kube-proxy \
  --kubeconfig=kube-proxy.kubeconfig

kubectl config use-context default \
  --kubeconfig=kube-proxy.kubeconfig
배포:

# Worker Node에 배포
for host in node-0 node-1; do
  ssh root@${host} "mkdir -p /var/lib/{kube-proxy,kubelet}"
  scp kube-proxy.kubeconfig root@${host}:/var/lib/kube-proxy/kubeconfig
  scp ${host}.kubeconfig root@${host}:/var/lib/kubelet/kubeconfig
done

# Control Plane에 배포
scp admin.kubeconfig \
    kube-controller-manager.kubeconfig \
    kube-scheduler.kubeconfig \
    root@server:~/

Data Encryption at Rest

1. ETCD 암호화 설정

문제점: 기본적으로 Kubernetes Secret은 etcd에 평문으로 저장된다.

스터디 멤버 투표 결과: - API-Server → ETCD Secret 암호화 적용 중: 1명 - 평문 그대로 사용 중: 4명 - 암호화 검토 및 적용 예정: 5명

대부분의 클러스터가 Secret을 평문으로 저장 중! 🚨

보안 위험: - etcd 백업 파일이 유출되면 모든 Secret 노출 - etcd 노드에 접근 권한을 가진 사람은 모든 Secret 조회 가능 - 규제 준수 이슈 (PCI-DSS, GDPR 등)

2. 암호화 Provider 이해

encryption-config.yaml:

kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
  - resources:
      - secrets  # 암호화 대상 리소스 (Secret만 암호화)
    providers:
      - aescbc:  # 첫 번째: 새 데이터 암호화 방식
          keys:
            - name: key1  # 키 식별자 (etcd 데이터에 기록됨)
              secret: ${ENCRYPTION_KEY}
      - identity: {}  # 두 번째: 평문 (하위 호환성)
암호화 키 생성:

export ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
echo $ENCRYPTION_KEY
# 예: JMnUP1PUUORZE9iadPdzYifnvPVIniSzOW6NUoMofVc=

# 환경 변수 치환하여 최종 파일 생성
envsubst < configs/encryption-config.yaml > encryption-config.yaml
Provider 우선순위 전략: 1. aescbc: 새로운 Secret 저장 시 AES-CBC 방식으로 암호화 2. identity: 기존에 평문으로 저장된 데이터도 읽을 수 있도록 하위 호환성 제공

하위 호환성이 중요한 이유: - 암호화 설정 전에 생성된 Secret은 평문으로 저장됨 - identity provider가 없으면 기존 Secret을 읽을 수 없음 - "새로운 데이터는 무조건 암호화, 이전 평문 데이터도 읽기 가능" 전략

etcd에 저장되는 형식:

k8s:enc:aescbc:v1:key1:<ciphertext>
- k8s:enc: Kubernetes 암호화 마커 - aescbc: 암호화 알고리즘 - v1: Encryption Provider 버전 - key1: 사용된 암호화 키 이름 - 이후: 암호화된 데이터

지원 암호화 Provider: | Provider | 설명 | 보안 수준 | |----------|------|----------| | aescbc | AES-CBC (권장) | 높음 | | aesgcm | AES-GCM (더 빠름) | 높음 | | kms v2 | 외부 KMS 연동 (AWS KMS, Vault 등) | 매우 높음 | | secretbox | NaCl Secretbox | 높음 | | identity | 평문 (암호화 없음) | 낮음 |

3. 암호화 검증

Secret 생성:

kubectl create secret generic kubernetes-the-hard-way \
  --from-literal="mykey=mydata"
Kubernetes API를 통한 확인 (여전히 복호화되어 보임):

kubectl get secret kubernetes-the-hard-way -o yaml
# data:
# mykey: bXlkYXRh (base64 인코딩)

kubectl get secret kubernetes-the-hard-way -o jsonpath='{.data.mykey}' | base64 -d
# mydata
etcd에서 직접 확인 (암호화 확인):

ssh root@server \
  'etcdctl get /registry/secrets/default/kubernetes-the-hard-way | hexdump -C'
출력 예시 (암호화 적용됨):

00000000  2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74  |/registry/secret|
00000010  73 2f 64 65 66 61 75 6c 74 2f 6b 75 62 65 72 6e  |s/default/kubern|
00000020  65 74 65 73 2d 74 68 65 2d 68 61 72 64 2d 77 61  |etes-the-hard-wa|
00000030  79 0a 6b 38 73 3a 65 6e 63 3a 61 65 73 63 62 63  |y.k8s:enc:aescbc|
00000040  3a 76 31 3a 6b 65 79 31 3a 44 61 dc 08 37 97 eb  |:v1:key1:Da..7..|
00000050  ...암호화된 데이터...
검증 포인트: - k8s:enc:aescbc:v1:key1: 헤더 확인 - 이후 데이터가 암호화되어 있음 확인 (읽을 수 없는 바이너리)

Kind K8s와 비교 (기본 평문):

# Kind K8s의 etcd 확인
docker exec -i myk8s-control-plane etcdctl get \
  /registry/secrets/default/kubernetes-the-hard-way | hexdump -C

# 출력에 k8s\x00 표시 (평문)
00000030  79 0a 6b 38 73 00 0a 0c 0a 02 76 31 12 06 53 65  |y.k8s.....v1..Se|
...
00000110  65 79 12 06 6d 79 64 61 74 61 1a 06 4f 70 61 71  |ey..mydata..Opaq|
프로덕션 권장사항: - 암호화 설정 필수 적용 - 키 로테이션 주기적 실행 - KMS v2 사용 (AWS KMS, HashiCorp Vault 등) - 기존 Secret 재암호화: kubectl get secrets --all-namespaces -o json | kubectl replace -f -


Control Plane 구성

1. ETCD 클러스터 구축

ETCD 설치 및 구성 (server 노드):

# Binary 설치
mv etcd etcdctl /usr/local/bin/

# 디렉토리 생성
mkdir -p /etc/etcd /var/lib/etcd
chmod 700 /var/lib/etcd

# 인증서 복사 (현재는 미사용, 향후 TLS 적용 시 사용)
cp ca.crt kube-api-server.key kube-api-server.crt /etc/etcd/
Systemd 서비스 생성 (/etc/systemd/system/etcd.service):

[Unit]
Description=etcd
Documentation=https://github.com/etcd-io/etcd

[Service]
Type=notify
ExecStart=/usr/local/bin/etcd \
  --name server \
  --initial-advertise-peer-urls http://127.0.0.1:2380 \
  --listen-peer-urls http://127.0.0.1:2380 \
  --listen-client-urls http://127.0.0.1:2379 \
  --advertise-client-urls http://127.0.0.1:2379 \
  --initial-cluster-token etcd-cluster-0 \
  --initial-cluster server=http://127.0.0.1:2380 \
  --initial-cluster-state new \
  --data-dir=/var/lib/etcd
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
플래그 설명
--name etcd 멤버 이름 (클러스터 내 고유)
--listen-peer-urls 다른 etcd 멤버와 통신할 주소 (2380)
--listen-client-urls 클라이언트 요청을 받을 주소 (2379)
--initial-cluster 초기 클러스터 구성원 목록
--data-dir 데이터 저장 경로

주의: 이 실습에서는 HTTP 통신 사용 (프로덕션에서는 HTTPS 필수!)

서비스 시작 및 확인:

systemctl daemon-reload
systemctl enable etcd
systemctl start etcd

# 상태 확인
systemctl status etcd

# 포트 확인
ss -tnlp | grep etcd
# LISTEN 127.0.0.1:2380 (peer)
# LISTEN 127.0.0.1:2379 (client)

# 멤버 확인
etcdctl member list -w table
# +------------------+---------+--------+------------------------+------------------------+------------+
# | ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |
# +------------------+---------+--------+------------------------+------------------------+------------+
# | 702b0a34e2cfd39 | started | server | http://127.0.0.1:2380 | http://127.0.0.1:2379 | false |
# +------------------+---------+--------+------------------------+------------------------+------------+

2. Kube-APIServer 설정

파일 준비:

# Binary 및 설정 파일 배치
mkdir -p /var/lib/kubernetes/
mv kube-apiserver /usr/local/bin/

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/
Systemd 서비스 (/etc/systemd/system/kube-apiserver.service):

ExecStart=/usr/local/bin/kube-apiserver \
  --allow-privileged=true \
  --apiserver-count=1 \
  --authorization-mode=Node,RBAC \
  --bind-address=0.0.0.0 \
  --client-ca-file=/var/lib/kubernetes/ca.crt \
  --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \
  --etcd-servers=http://127.0.0.1:2379 \
  --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.crt \
  --kubelet-client-certificate=/var/lib/kubernetes/kube-api-server.crt \
  --kubelet-client-key=/var/lib/kubernetes/kube-api-server.key \
  --service-account-key-file=/var/lib/kubernetes/service-accounts.crt \
  --service-account-signing-key-file=/var/lib/kubernetes/service-accounts.key \
  --service-account-issuer=https://server.kubernetes.local:6443 \
  --service-cluster-ip-range=10.32.0.0/24 \
  --service-node-port-range=30000-32767 \
  --tls-cert-file=/var/lib/kubernetes/kube-api-server.crt \
  --tls-private-key-file=/var/lib/kubernetes/kube-api-server.key \
  --v=2
핵심 플래그 설명:

플래그 설명
--allow-privileged=true Privileged 컨테이너 허용 (CNI, kube-proxy 등 시스템 파드 필수)
--authorization-mode=Node,RBAC 인가 모드: Node Authorizer (kubelet 전용) → RBAC
--bind-address=0.0.0.0 모든 인터페이스에서 6443 포트 수신
--client-ca-file X.509 클라이언트 인증서 검증용 CA
--enable-admission-plugins Admission Controller 목록
NodeRestriction kubelet이 자기 노드/파드만 조작 가능하게 제한
--etcd-servers etcd 엔드포인트 (apiserver는 etcd의 클라이언트)
--encryption-provider-config Secret 암호화 설정 파일
--kubelet-client-certificate apiserver → kubelet 호출 시 사용할 클라이언트 인증서
--service-account-key-file SA 토큰 검증용 공개키
--service-account-signing-key-file SA 토큰 서명용 개인키
--service-account-issuer SA 토큰 issuer (OIDC 호환)
--service-cluster-ip-range Service ClusterIP 대역
--tls-cert-file kube-apiserver HTTPS 서버 인증서

서비스 시작:

systemctl daemon-reload
systemctl enable kube-apiserver
systemctl start kube-apiserver

# 확인
systemctl status kube-apiserver
ss -tlp | grep kube
# LISTEN *:6443 *:* users:(("kube-apiserver",pid=XXX,fd=3))

# API 버전 확인
curl -s -k --cacert /var/lib/kubernetes/ca.crt \
  https://server.kubernetes.local:6443/version | jq

3. Kube-Controller-Manager 설정

Kube-Controller-Manager 역할: - 클러스터 상태 관리 (Desired State → Current State) - Node, ReplicaSet, ServiceAccount 토큰 등 관리

파일 준비:

mv kube-controller-manager /usr/local/bin/
mv kube-controller-manager.kubeconfig /var/lib/kubernetes/
Systemd 서비스:

ExecStart=/usr/local/bin/kube-controller-manager \
  --bind-address=0.0.0.0 \
  --cluster-cidr=10.200.0.0/16 \
  --cluster-name=kubernetes \
  --cluster-signing-cert-file=/var/lib/kubernetes/ca.crt \
  --cluster-signing-key-file=/var/lib/kubernetes/ca.key \
  --kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \
  --root-ca-file=/var/lib/kubernetes/ca.crt \
  --service-account-private-key-file=/var/lib/kubernetes/service-accounts.key \
  --service-cluster-ip-range=10.32.0.0/24 \
  --use-service-account-credentials=true \
  --v=2
핵심 플래그: - --cluster-cidr: Pod CIDR 전체 대역 (각 노드에 쪼개서 할당) - --service-account-private-key-file: SA 토큰 서명용 개인키 - --allocate-node-cidrs=true: (필요시 추가) 각 노드에 Pod IP 대역 자동 할당

트러블슈팅 팁: - Smoke Test에서 Pod IP가 정상적으로 할당되지 않으면 --allocate-node-cidrs=true 추가

서비스 시작:

systemctl enable kube-controller-manager
systemctl start kube-controller-manager
systemctl status kube-controller-manager

4. Kube-Scheduler 설정

파일 준비:

mv kube-scheduler /usr/local/bin/
mv kube-scheduler.kubeconfig /var/lib/kubernetes/
mkdir -p /etc/kubernetes/config/
mv kube-scheduler.yaml /etc/kubernetes/config/
kube-scheduler.yaml:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: "/var/lib/kubernetes/kube-scheduler.kubeconfig"
leaderElection:
  leaderElect: false  # HA가 아니므로 비활성화
Systemd 서비스:

ExecStart=/usr/local/bin/kube-scheduler \
  --config=/etc/kubernetes/config/kube-scheduler.yaml \
  --v=2
서비스 시작:

systemctl enable kube-scheduler
systemctl start kube-scheduler
systemctl status kube-scheduler

5. RBAC for Kubelet Authorization

system:kube-apiserver-to-kubelet ClusterRole 생성:

# kube-apiserver-to-kubelet.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: system:kube-apiserver-to-kubelet
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
rules:
  - apiGroups:
      - ""
    resources:
      - nodes/proxy    # apiserver → kubelet 프록시 통신
      - nodes/stats    # 노드/파드 리소스 통계 (cAdvisor)
      - nodes/log      # kubectl logs
      - nodes/spec
      - nodes/metrics  # metrics-server / top 명령
    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  # kube-api-server.crt의 CN
적용:

kubectl apply -f kube-apiserver-to-kubelet.yaml \
  --kubeconfig admin.kubeconfig
API Server → Kubelet 호출 흐름:

sequenceDiagram
    participant User as kubectl/User
    participant API as kube-apiserver
    participant Kubelet as kubelet<br/>(Port 10250)
    participant WebhookAuthz as Webhook Authz

    User->>API: kubectl logs pod-name
    Note over API: 클라이언트 인증서<br/>CN=kubernetes

    API->>Kubelet: GET /logs (TLS 요청)
    Note over API,Kubelet: TLS 클라이언트 인증서<br/>CN=kubernetes

    Kubelet->>Kubelet: TLS 서버 인증서로 응답<br/>(kubelet.crt)

    Kubelet->>WebhookAuthz: Webhook 인가 확인
    Note over WebhookAuthz: authorization.mode=Webhook

    WebhookAuthz->>API: SubjectAccessReview API
    Note over WebhookAuthz,API: User=kubernetes<br/>Resource=nodes/logs

    API->>API: RBAC 평가
    Note over API: ClusterRoleBinding:<br/>system:kube-apiserver<br/>↓<br/>ClusterRole:<br/>system:kube-apiserver-to-kubelet<br/>↓<br/>nodes/* 리소스 허용

    API-->>WebhookAuthz: Allowed
    WebhookAuthz-->>Kubelet: 인가 성공
    Kubelet-->>API: 200 OK (로그 데이터)
    API-->>User: 로그 출력
검증:

# ClusterRole 확인
kubectl describe clusterroles system:kube-apiserver-to-kubelet \
  --kubeconfig admin.kubeconfig

# ClusterRoleBinding 확인
kubectl describe clusterrolebindings system:kube-apiserver \
  --kubeconfig admin.kubeconfig

Worker Node 구성

1. Container Runtime 설치

설치 컴포넌트 (node-0, node-1):

컴포넌트 역할
runc OCI 런타임 (실제 컨테이너 프로세스 생성)
CNI 플러그인 bridge, loopback
containerd CRI (Container Runtime Interface)
crictl CRI 디버깅 도구
kubelet Node Agent
kube-proxy Service 네트워크 구현

디렉토리 구조:

mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes \
  /var/run/kubernetes
Binary 설치:

# Worker 관련 바이너리
mv crictl kubelet kube-proxy runc /usr/local/bin/
chmod +x /usr/local/bin/{crictl,kubelet,kube-proxy,runc}

# containerd 압축 해제
tar -xvf containerd-2.1.0-linux-arm64.tar.gz -C /

# CNI 플러그인
tar -xvf cni-plugins-linux-arm64-v1.6.2.tgz -C /opt/cni/bin/
containerd 설정 (/etc/containerd/config.toml):

version = 2

[plugins]
  [plugins."io.containerd.grpc.v1.cri"]
    [plugins."io.containerd.grpc.v1.cri".containerd]
      snapshotter = "overlayfs"
      [plugins."io.containerd.grpc.v1.cri".containerd.default_runtime]
        runtime_type = "io.containerd.runc.v2"
      [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
        runtime_type = "io.containerd.runc.v2"
        [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
          SystemdCgroup = true  # kubelet cgroupDriver와 일치 필수
Systemd 서비스 (/etc/systemd/system/containerd.service):

[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target

[Service]
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/bin/containerd
Restart=always
RestartSec=5
Delegate=yes
KillMode=process

[Install]
WantedBy=multi-user.target
서비스 시작:

systemctl daemon-reload
systemctl enable containerd
systemctl start containerd

# 확인
systemctl status containerd
crictl --runtime-endpoint unix:///var/run/containerd/containerd.sock version

2. CNI 플러그인 구성

Bridge 플러그인 설정 (/etc/cni/net.d/10-bridge.conf):

{
  "cniVersion": "1.0.0",
  "name": "bridge",
  "type": "bridge",
  "bridge": "cni0",
  "isGateway": true,  # cni0 게이트웨이 역할
  "ipMasq": true,     # Pod  외부 NAT
  "ipam": {
    "type": "host-local",
    "ranges": [
      [{"subnet": "10.200.0.0/24"}]  # node-0
      // node-1은 "10.200.1.0/24"
    ],
    "routes": [
      {"dst": "0.0.0.0/0"}  # 기본 라우트
    ]
  }
}
Loopback 플러그인 설정 (/etc/cni/net.d/99-loopback.conf):

{
  "cniVersion": "1.0.0",
  "name": "lo",
  "type": "loopback"
}
CNI 동작 흐름:

1. kubelet이 Pod 생성 요청 받음
2. containerd에 컨테이너 생성 요청
3. containerd가 CNI 플러그인 호출
   - /etc/cni/net.d/10-bridge.conf 실행
   - cni0 브리지 생성 (없으면)
   - veth pair 생성 (vethXXX ↔ Pod의 eth0)
   - Pod에 IP 할당 (10.200.0.2, 10.200.0.3, ...)
   - 라우팅 설정
4. /etc/cni/net.d/99-loopback.conf 실행
   - Pod 내부 loopback 인터페이스 설정
확인:

# Pod 생성 후
brctl show
# bridge name bridge id STP enabled interfaces
# cni0 8000.xxx no vethXXX

ip addr show cni0
ip route

3. Kubelet 설정

kubelet-config.yaml 주요 설정 (/var/lib/kubelet/kubelet-config.yaml):

kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
address: "0.0.0.0"  # kubelet HTTPS 서버 바인딩 주소
authentication:
  anonymous:
    enabled: false  # 익명 인증 비활성화
  webhook:
    enabled: true  # 인증 요청을 kube-apiserver에 위임 (SA 토큰, bootstrap 토큰 처리)
  x509:
    clientCAFile: "/var/lib/kubelet/ca.crt"  # kubelet에 접근하는 클라이언트 인증서 검증용 CA
authorization:
  mode: Webhook  # 인가 요청을 kube-apiserver에 위임 (Node Authorizer + RBAC)
cgroupDriver: systemd  # containerd와 일치 필수
containerRuntimeEndpoint: "unix:///var/run/containerd/containerd.sock"
enableServer: true  # kubelet API 서버 활성화 (false면 apiserver가 접근 불가)
failSwapOn: false
maxPods: 16
port: 10250  # kubelet HTTPS API 포트 (logs, exec, stats, metrics 접근)
resolvConf: "/etc/resolv.conf"  # 파드에 전달할 DNS 설정 파일
registerNode: true  # kubelet이 API 서버에 Node 객체 자동 등록
runtimeRequestTimeout: "15m"  # CRI 요청 최대 대기 시간 (이미지 pull, container start 등)
tlsCertFile: "/var/lib/kubelet/kubelet.crt"  # kubelet HTTPS 서버의 서버 인증서
tlsPrivateKeyFile: "/var/lib/kubelet/kubelet.key"
Systemd 서비스 (/etc/systemd/system/kubelet.service):

ExecStart=/usr/local/bin/kubelet \
  --config=/var/lib/kubelet/kubelet-config.yaml \
  --container-runtime-endpoint=unix:///var/run/containerd/containerd.sock \
  --kubeconfig=/var/lib/kubelet/kubeconfig \
  --v=2
- Webhook 인증/인가: kube-apiserver에 위임하여 SA 토큰, bootstrap 토큰 처리 - TLS 서버: apiserver가 kubelet API에 접근하기 위해 필요 (포트 10250) - cgroupDriver=systemd: containerd와 일치시켜야 함 (불일치 시 Pod 생성 실패)

서비스 시작:

# Swap 비활성화 (Kubernetes 요구사항)
swapoff -a
sed -i '/swap/s/^/#/' /etc/fstab

systemctl daemon-reload
systemctl enable kubelet
systemctl start kubelet

# 확인
systemctl status kubelet
journalctl -u kubelet -f

4. Kube-Proxy 설정

kube-proxy-config.yaml (/var/lib/kube-proxy/kube-proxy-config.yaml):

kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
clientConnection:
  kubeconfig: "/var/lib/kube-proxy/kubeconfig"
mode: "iptables"  # iptables 모드 (다른 옵션: ipvs, userspace)
clusterCIDR: "10.200.0.0/16"
Systemd 서비스:

ExecStart=/usr/local/bin/kube-proxy \
  --config=/var/lib/kube-proxy/kube-proxy-config.yaml
서비스 시작:

systemctl enable kube-proxy
systemctl start kube-proxy
systemctl status kube-proxy
확인:

# Kubelet API 포트 확인
ss -tnlp | grep kubelet
# LISTEN *:10250 *:* users:(("kubelet",pid=XXX,fd=18))

# iptables 규칙 확인 (Service 생성 후)
iptables -t nat -L KUBE-SERVICES

Pod Network Routes

1. Pod CIDR 구성

machines.txt 파일:

192.168.10.100 server.kubernetes.local  server
192.168.10.101 node-0.kubernetes.local  node-0  10.200.0.0/24
192.168.10.102 node-1.kubernetes.local  node-1  10.200.1.0/24
네트워크 구조:

Cluster CIDR: 10.200.0.0/16
├── node-0 Pod CIDR: 10.200.0.0/24
│   ├── cni0: 10.200.0.1 (브리지 게이트웨이)
│   └── Pods: 10.200.0.2, 10.200.0.3, ...
└── node-1 Pod CIDR: 10.200.1.0/24
    ├── cni0: 10.200.1.1 (브리지 게이트웨이)
    └── Pods: 10.200.1.2, 10.200.1.3, ...

Service CIDR: 10.32.0.0/24
└── kubernetes: 10.32.0.1 (첫 번째 Service)

2. 수동 라우팅 설정

문제: CNI가 없으므로 노드 간 Pod 통신을 위한 라우팅이 없음

해결: OS 커널에 직접 정적 라우팅 규칙 추가

# jumpbox에서 실행
SERVER_IP=$(grep server machines.txt | cut -d " " -f 1)
NODE_0_IP=$(grep node-0 machines.txt | cut -d " " -f 1)
NODE_0_SUBNET=$(grep node-0 machines.txt | cut -d " " -f 4)
NODE_1_IP=$(grep node-1 machines.txt | cut -d " " -f 1)
NODE_1_SUBNET=$(grep node-1 machines.txt | cut -d " " -f 4)

echo "SERVER_IP: $SERVER_IP"
echo "NODE_0_IP: $NODE_0_IP, SUBNET: $NODE_0_SUBNET"
echo "NODE_1_IP: $NODE_1_IP, SUBNET: $NODE_1_SUBNET"

# server 노드에 라우팅 추가
ssh root@server <<EOF
ip route add ${NODE_0_SUBNET} via ${NODE_0_IP}
ip route add ${NODE_1_SUBNET} via ${NODE_1_IP}
EOF

# node-0에 라우팅 추가 (node-1 Pod에 접근)
ssh root@node-0 <<EOF
ip route add ${NODE_1_SUBNET} via ${NODE_1_IP}
EOF

# node-1에 라우팅 추가 (node-0 Pod에 접근)
ssh root@node-1 <<EOF
ip route add ${NODE_0_SUBNET} via ${NODE_0_IP}
EOF
라우팅 테이블 확인:

ssh server ip route
# default via 10.0.2.2 dev eth0
# 10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15
# 10.200.0.0/24 via 192.168.10.101 dev eth1
# 10.200.1.0/24 via 192.168.10.102 dev eth1
# 192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.100

ssh node-0 ip route
# 10.200.1.0/24 via 192.168.10.102 dev eth1

ssh node-1 ip route
# 10.200.0.0/24 via 192.168.10.101 dev eth1

3. 네트워크 검증

Pod 생성 후 통신 테스트:

kubectl get pod -owide
# NAME READY STATUS IP NODE
# nginx-xxx-aaa 1/1 Running 10.200.0.2 node-0
# nginx-xxx-bbb 1/1 Running 10.200.1.2 node-1

# server 노드에서 Pod IP 접근 테스트
ssh server curl -s 10.200.0.2 | grep title
# <title>Welcome to nginx!</title>

ssh server curl -s 10.200.1.2 | grep title
# <title>Welcome to nginx!</title>

# node-0에서 node-1 Pod 접근
ssh node-0 curl -s 10.200.1.2 | grep title
# <title>Welcome to nginx!</title>

# node-1에서 node-0 Pod 접근
ssh node-1 curl -s 10.200.0.2 | grep title
# <title>Welcome to nginx!</title>

주의사항: - 영구 설정이 아니므로 재부팅 시 라우팅 규칙 사라짐 - 프로덕션에서는 CNI 플러그인 필수 (Calico, Cilium, Flannel 등) - BGP 기반 라우팅 또는 Overlay 네트워크 사용 권장


Smoke Test

1. Data Encryption 테스트

Secret 생성:

kubectl create secret generic kubernetes-the-hard-way \
  --from-literal="mykey=mydata"

kubectl get secret kubernetes-the-hard-way
# NAME TYPE DATA AGE
# kubernetes-the-hard-way Opaque 1 10s
Kubernetes API를 통한 확인 (복호화되어 보임):

kubectl get secret kubernetes-the-hard-way -o yaml
# apiVersion: v1
# data:
# mykey: bXlkYXRh # base64("mydata")
# kind: Secret
# ...

kubectl get secret kubernetes-the-hard-way -o jsonpath='{.data.mykey}' | base64 -d
# mydata
etcd에서 암호화 확인:

ssh root@server \
  'etcdctl get /registry/secrets/default/kubernetes-the-hard-way | hexdump -C'
검증 포인트: - k8s:enc:aescbc:v1:key1: 헤더 확인 - 데이터가 암호화되어 있음 확인

결론: Secret이 etcd에 암호화되어 저장됨!

2. Deployment 테스트

nginx Deployment 생성:

kubectl create deployment nginx --image=nginx:latest
kubectl scale deployment nginx --replicas=2

kubectl get deployments
# NAME READY UP-TO-DATE AVAILABLE AGE
# nginx 2/2 2 2 30s

kubectl get pods -owide
# NAME READY STATUS IP NODE
# nginx-54c98b4f84-pxp6c 1/1 Running 10.200.1.2 node-1
# nginx-54c98b4f84-qxpbn 1/1 Running 10.200.0.2 node-0
Pod 생성 흐름:

sequenceDiagram
    participant kubectl
    participant API as kube-apiserver
    participant etcd
    participant Sched as kube-scheduler
    participant CM as kube-controller-manager
    participant Kubelet as kubelet (node-0)
    participant CRI as containerd
    participant CNI as CNI Plugin

    kubectl->>API: kubectl create deployment nginx
    API->>etcd: Deployment 저장
    API-->>kubectl: Deployment created

    CM->>API: Watch Deployments
    CM->>CM: ReplicaSet 생성 필요
    CM->>API: Create ReplicaSet
    API->>etcd: ReplicaSet 저장

    CM->>API: Watch ReplicaSets
    CM->>CM: Pod 생성 필요
    CM->>API: Create Pod (unscheduled)
    API->>etcd: Pod 저장 (nodeName: "")

    Sched->>API: Watch Pods (nodeName="")
    Sched->>Sched: 스케줄링 알고리즘 실행
    Sched->>API: Bind Pod to node-0
    API->>etcd: Pod 업데이트 (nodeName: "node-0")

    Kubelet->>API: Watch Pods (nodeName="node-0")
    Kubelet->>Kubelet: Pod Spec 확인

    Kubelet->>CRI: CreateContainer
    CRI->>CRI: Pull image (nginx:latest)
    CRI->>CRI: Create Pause container
    CRI->>CNI: SetupPod (10-bridge.conf)
    CNI->>CNI: cni0 브리지 생성
    CNI->>CNI: veth pair 생성
    CNI->>CNI: IP 할당 (10.200.0.2)
    CNI-->>CRI: Success

    CRI->>CRI: Create nginx container
    CRI-->>Kubelet: Container created

    Kubelet->>API: Update Pod Status (Running)
    API->>etcd: Pod Status 저장
Container Runtime 확인:

# node-0에서 crictl로 컨테이너 확인
ssh node-0 crictl ps
# CONTAINER ID IMAGE CREATED STATE NAME POD ID
# xxx docker.io/nginx... 1 minute ago Running nginx yyy

# 프로세스 트리 확인
ssh node-0 pstree -ap
# systemd(1)
# ├─containerd(xxx)
# │ ├─containerd-shim-runc-v2(yyy)
# │ │ ├─pause(zzz) # Pause 컨테이너
# │ │ └─nginx(aaa) # 실제 nginx 프로세스
# │ │ └─nginx(bbb) # nginx worker

# 브리지 인터페이스 확인
ssh node-0 brctl show
# bridge name bridge id STP enabled interfaces
# cni0 8000.xxx no vethXXX

# veth 인터페이스 확인
ssh node-0 ip addr | grep veth
# vethXXX@if3: <BROADCAST,MULTICAST,UP,LOWER_UP>
포트 포워딩 테스트:

POD_NAME=$(kubectl get pods -l app=nginx -o jsonpath="{.items[0].metadata.name}")
echo $POD_NAME

kubectl port-forward $POD_NAME 8080:80 &
# Forwarding from 127.0.0.1:8080 -> 80

curl --head http://127.0.0.1:8080
# HTTP/1.1 200 OK
# Server: nginx/1.25.3
로그 확인:

kubectl logs $POD_NAME
# /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
# ...
# 127.0.0.1 - - [17/Jan/2026:...] "HEAD / HTTP/1.1" 200 0 ...
Exec 테스트:

kubectl exec -ti $POD_NAME -- nginx -v
# nginx version: nginx/1.25.3

kubectl exec -ti $POD_NAME -- cat /etc/os-release
# PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
포트 포워딩 종료:

kill -9 $(pgrep kubectl)

3. Service 테스트

NodePort Service 생성:

kubectl expose deployment nginx --type=NodePort --port=80

kubectl get service,ep nginx
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# service/nginx NodePort 10.32.0.149 <none> 80:31410/TCP
#
# NAME ENDPOINTS
# endpoints/nginx 10.200.0.2:80,10.200.1.2:80
Service 상세 정보:

kubectl describe service nginx
# Name: nginx
# Namespace: default
# Selector: app=nginx
# Type: NodePort
# IP Family Policy: SingleStack
# IP Families: IPv4
# IP: 10.32.0.149
# Port: <unset> 80/TCP
# TargetPort: 80/TCP
# NodePort: <unset> 31410/TCP
# Endpoints: 10.200.0.2:80,10.200.1.2:80
NodePort 접근 테스트:

NODE_PORT=$(kubectl get svc nginx --output=jsonpath='{range .spec.ports[0]}{.nodePort}')
echo $NODE_PORT
# 31410

# jumpbox에서 node-0로 접근
curl -s -I http://node-0:${NODE_PORT}
# HTTP/1.1 200 OK
# Server: nginx/1.25.3

# jumpbox에서 node-1로 접근
curl -s -I http://node-1:${NODE_PORT}
# HTTP/1.1 200 OK

# ClusterIP로 접근 (server 노드에서)
ssh server curl -s -I http://10.32.0.149
# HTTP/1.1 200 OK
kube-proxy iptables 규칙 확인:

ssh node-0 iptables -t nat -L KUBE-SERVICES | grep nginx
# KUBE-SVC-XXX tcp -- anywhere 10.32.0.149 tcp dpt:80
검증 완료


핵심 개념 정리

1. 인증(Authentication) vs 인가(Authorization)

인증 (Authentication): "너는 누구냐?" - 사용자/서비스의 신원 확인 - Kubernetes 인증 방식: - X.509 클라이언트 인증서 (CN, O) ← 이번 실습에서 사용 - ServiceAccount 토큰 (JWT) - Bootstrap 토큰 (node join 시) - OIDC 토큰 (외부 IdP 연동) - Webhook 토큰

인가 (Authorization): "너는 무엇을 할 수 있냐?" - 인증된 사용자가 특정 리소스에 접근할 권한이 있는지 확인 - Kubernetes 인가 방식: - Node Authorizer (Kubelet 전용) - RBAC (Role-Based Access Control) - ABAC, Webhook

kube-apiserver 플래그:

--authorization-mode=Node,RBAC
- 왼쪽부터 순서대로 평가 - Node Authorizer가 먼저 평가되어 Kubelet 요청 처리 - 거부되면 다음 RBAC로 전달

인증/인가 흐름:

sequenceDiagram
    participant Kubelet as kubelet<br/>(node-0)
    participant API as kube-apiserver
    participant Auth as Authentication
    participant NodeAuthz as Node Authorizer
    participant RBAC as RBAC Authorizer

    Kubelet->>API: API 요청 (TLS 클라이언트 인증서)
    Note over Kubelet,API: CN=system:node:node-0<br/>O=system:nodes

    API->>Auth: 1. 인증 (Authentication)
    Auth->>Auth: 인증서 검증 (ca.crt)
    Auth->>Auth: Subject 추출<br/>User=system:node:node-0<br/>Group=system:nodes
    Auth-->>API: 인증 성공

    API->>NodeAuthz: 2. 인가 - Node Authorizer 평가
    NodeAuthz->>NodeAuthz: User 패턴 확인<br/>"system:node:*"
    NodeAuthz->>NodeAuthz: Group 확인<br/>"system:nodes"
    NodeAuthz->>NodeAuthz: 자기 노드 리소스?
    alt Node Authorizer 허용
        NodeAuthz-->>API: 허용
        API-->>Kubelet: 200 OK
    else Node Authorizer 거부
        NodeAuthz-->>API: 거부
        API->>RBAC: 3. RBAC 평가
        RBAC->>RBAC: ClusterRole/Role 매칭
        RBAC->>RBAC: 권한 확인
        alt RBAC 허용
            RBAC-->>API: 허용
            API-->>Kubelet: 200 OK
        else RBAC 거부
            RBAC-->>API: 거부
            API-->>Kubelet: 403 Forbidden
        end
    end

2. Node Authorizer 이해

Node Authorizer는 특수 목적 권한 부여 모드: - Kubelet이 수행하는 API 요청에 대한 권한을 특별히 부여 - 다른 사용자/서비스는 Node Authorizer에 해당하지 않음

적용 조건 (모두 만족해야 함): 1. User: system:node:<nodeName> 패턴 2. Group: system:nodes 3. 인증서의 CN, O가 위 조건 충족

Node Authorizer가 허용하는 작업:

1. Read Operations: | 리소스 | 범위 | |--------|------| | Services, Endpoints | 모든 네임스페이스 | | Nodes | 모든 노드 | | Pods | 자기 노드에서 실행되는 Pod만 | | PVC, PV | 자기 노드의 Pod가 사용하는 것만 | | Secrets, ConfigMaps | 자기 노드의 Pod가 사용하는 것만 |

2. Write Operations: | 리소스 | 범위 | |--------|------| | Nodes, Node Status | 자기 자신만 | | Pods, Pod Status | 자기 노드에서 실행되는 것만 | | Events | 자기 노드 관련만 |

3. Auth-related Operations: | 리소스 | 용도 | |--------|------| | CertificateSigningRequests | kubelet 인증서 갱신 | | TokenReviews | 토큰 검증 | | SubjectAccessReviews | 권한 검증 |

NodeRestriction Admission Controller: - Node Authorizer와 함께 작동 - kubelet이 자기 노드/파드만 조작 가능하게 제한 - 다른 노드의 Pod label 수정 방지 - node-restriction.kubernetes.io/ 접두사 label 보호

kube-apiserver 플래그:

--enable-admission-plugins=...,NodeRestriction,...
실습 예시:

# node-0의 kubelet이 자기 노드 정보 수정 (허용)
# User: system:node:node-0, Group: system:nodes

# node-0의 kubelet이 node-1 정보 수정 시도 (거부)
# → NodeRestriction Admission Controller가 차단

3. Certificate Subject와 RBAC 관계

인증서 Subject 필드가 Kubernetes에 매핑되는 방식:

인증서 필드 Kubernetes 예시
CN (Common Name) User CN=admin → User: admin
O (Organization) Group O=system:masters → Group: system:masters

Certificate → Kubernetes → RBAC 매핑:

graph LR
    subgraph "Admin 인증서"
        CERT1["인증서<br/>CN=admin<br/>O=system:masters"]
    end

    subgraph "Kubernetes 매핑"
        USER1["User: admin<br/>Group: system:masters"]
    end

    subgraph "RBAC"
        CRB1["ClusterRoleBinding<br/>cluster-admin"]
        CR1["ClusterRole<br/>cluster-admin<br/>Resources: *.*<br/>Verbs: *"]
    end

    CERT1 -->|X.509 Subject 추출| USER1
    USER1 -->|Group 매칭| CRB1
    CRB1 -->|RoleRef| CR1

    subgraph "Kubelet 인증서"
        CERT2["인증서<br/>CN=system:node:node-0<br/>O=system:nodes"]
    end

    subgraph "Kubernetes 매핑2"
        USER2["User: system:node:node-0<br/>Group: system:nodes"]
    end

    subgraph "인가"
        NA["Node Authorizer<br/>자기 노드만"]
        CR2["(선택) ClusterRole<br/>system:node"]
    end

    CERT2 -->|X.509 Subject 추출| USER2
    USER2 -->|User/Group 패턴| NA
    USER2 -.->|필요시| CR2

    subgraph "Scheduler 인증서"
        CERT3["인증서<br/>CN=system:kube-scheduler"]
    end

    subgraph "Kubernetes 매핑3"
        USER3["User: system:kube-scheduler"]
    end

    subgraph "RBAC3"
        CRB3["ClusterRoleBinding<br/>system:kube-scheduler"]
        CR3["ClusterRole<br/>system:kube-scheduler<br/>Pod 스케줄링 권한"]
    end

    CERT3 -->|X.509 Subject 추출| USER3
    USER3 -->|User 매칭| CRB3
    CRB3 -->|RoleRef| CR3
예시 1: Admin:

인증서:
  Subject: CN=admin, O=system:masters

Kubernetes 매핑:
  User: admin
  Group: system:masters

RBAC:
  ClusterRoleBinding: cluster-admin
  ├─ Subject: Group=system:masters
  └─ RoleRef: ClusterRole=cluster-admin

ClusterRole: cluster-admin
  Resources: *.*
  Verbs: *

결과: 인가 우회, 슈퍼유저 권한
예시 2: Kubelet:

인증서:
  Subject: CN=system:node:node-0, O=system:nodes

Kubernetes 매핑:
  User: system:node:node-0
  Group: system:nodes

인가:
  1. Node Authorizer 평가 (허용)
  2. (필요시) ClusterRole: system:node

결과: 자기 노드 관련 리소스만 접근 가능
예시 3: Kube-Scheduler:

인증서:
  Subject: CN=system:kube-scheduler, O=system:kube-scheduler

Kubernetes 매핑:
  User: system:kube-scheduler
  Group: system:kube-scheduler (사실 그룹은 안 쓰임)

RBAC:
  ClusterRoleBinding: system:kube-scheduler
  ├─ Subject: User=system:kube-scheduler
  └─ RoleRef: ClusterRole=system:kube-scheduler

ClusterRole: system:kube-scheduler
  - Pod 스케줄링 전용 권한 (Nodes, PVC binding 등)

결과: Pod 스케줄링에 필요한 권한만
확인 방법:

# 인증서 Subject 확인
openssl x509 -in admin.crt -text -noout | grep Subject
# Subject: CN=admin, O=system:masters

# RBAC 확인
kubectl describe clusterrolebindings cluster-admin
# Subjects:
# Kind Name Namespace
# ---- ---- ---------
# Group system:masters

# 사용자 권한 확인 (krew 플러그인)
kubectl rolesum -k User admin
kubectl rbac-tool lookup system:masters
system:masters 그룹의 특별함: - Kubernetes 내장 슈퍼유저 그룹 - ClusterRole cluster-admin에 바인딩 - 모든 리소스(*.*)에 대한 모든 동작(*) 허용 - 인증 후 인가를 우회 (Built-in Authorizer) - 프로덕션에서는 사용 제한 권장 - 개인 사용자에게 admin 인증서 발급 금지 - Break-glass 시나리오에만 사용 - 감사 로그 필수


Week 1 학습을 마치며: Kubernetes를 "처음부터 끝까지 손으로 만들어보는" 이 실습을 통해 Kubernetes의 내부 동작 원리, 보안 메커니즘, 컴포넌트 간 상호작용을 깊이 이해할 수 있었다. 자동화 도구(kubeadm, kops, EKS 등)에 가려져 있던 세부 사항들을 직접 다루면서 진짜 Kubernetes를 배웠다!

다음 주에는 프로덕션 환경에 더 가까운 고급 네트워킹을 다루며 계속 발전해나가겠다. K8s-Deploy 스터디,