본문으로 건너뛰기
인프라 · infra/kubernetes/02 전반 ② — 보안 · 스토리지 · 네트워크

← 스터디 홈

Kubernetes Study · 02 전반 ②

쿠버네티스 전반 ② — 보안 · 스토리지 · 네트워크

전반 세 노트 중 두 번째예요. 7~11부에서 누가 무엇을 할 수 있는지(인증 · 인가 · 보안), 데이터를 어디에 남기는지(스토리지), 패킷이 어떻게 오가는지(네트워크 기초 · 쿠버네티스 네트워킹)를 봐요. 앞 내용은 ① 클러스터 운영, 다음은 ③ 설치 · 도구 · 트러블슈팅이에요.

전반 ② · 7~11부 보안 · 스토리지 · 네트워크 · 2026.10.30 시작 · 2026.11.02 업데이트

그림의 색 = 역할 (입문과 같아요)
컨테이너 · 앱 이미지 · 레이어 레지스트리 가상 머신 OS · 커널 · 런타임 하드웨어 · 서버 · 사용자 원하는 상태 · 컨트롤 플레인 노드 새 버전 · 변경 정상 장애
입문 노트와 같이 보기 입문에서 처음 설명한 개념은 입문 2부 01처럼 링크를 걸어 뒀어요. 기억이 흐릿하면 링크로 가서 그림을 한 번 돌려 보고 돌아오면 돼요.
7부보안 ① 인증과 인가큰 그림인증TLS · 인증서kubeconfigAPI 그룹RBACServiceAccountAdmission

01. 보안의 큰 그림

1부 04에서 API 서버는 요청마다 인증 → 인가 → 어드미션 관문을 통과시킨다고 했어요. 7부는 그 관문 하나하나와, 관문이 믿고 쓰는 인증서(TLS)를 다뤄요. 8부에서는 그 밖의 보안(이미지 · Pod 권한 · 네트워크)을 봐요.

누가 들어오나(인증) · 무엇을 하나(인가) · 규칙에 맞나(어드미션) · 오가는 길은 안전한가(TLS)
질문쿠버네티스의 답절
누가 API 서버에 들어오나? (인증)인증서 · 토큰 · OIDC 같은 외부 로그인02 · 04 · 05
들어온 사람이 무엇을 할 수 있나? (인가)RBAC (Role · RoleBinding)06 · 07
Pod는 어떻게 API를 부르나?ServiceAccount 토큰08
요청 내용이 규칙에 맞나? (어드미션)Admission Controller · 웹훅 · 정책09
부품 사이 통신은 안전한가?TLS 인증서 (모든 부품이 서로 확인)03 · 04
Pod끼리, 컨테이너 자체는?Network Policy · SecurityContext · 이미지 보안8부
가장 먼저 지킬 것은 노드와 API 서버 노드에 root로 들어갈 수 있으면 그 위의 모든 것이 뚫려요. SSH는 비밀번호 대신 키로만, API 서버는 인터넷에 함부로 열지 않는 게 기본이에요. 쿠버네티스 보안은 그 위에 쌓는 층이에요.

02. 인증

1. 사람 사용자와 ServiceAccount

API 서버에 접근하는 쪽은 크게 둘이에요. 관리자 · 개발자 같은 사람, 그리고 Pod 안의 앱이나 CI 같은 기계. 놀랍게도 쿠버네티스에는 “User” 객체가 없어요. 사람은 클러스터 밖에서 관리하고(인증서 · 회사 로그인), 기계용 계정만 ServiceAccount라는 객체로 클러스터 안에서 만들어요.

사람은 밖에서 증명하고, 기계는 ServiceAccount로
$ kubectl create serviceaccount ci-bot      # 기계 계정은 객체로 만든다
$ kubectl create user jane                  # 이런 명령은 없다 — 사람은 객체가 아님
error: unknown command "user" for "kubectl create"

2. 인증 방법들

방법어떻게지금은
X.509 클라이언트 인증서인증서의 CN = 사용자 이름, O = 그룹kubeadm 관리자 계정 · 컴포넌트끼리 (04)
ServiceAccount 토큰API 서버가 서명한 시간 제한 토큰Pod · CI (08)
OIDC회사 로그인(Google · Okta · Keycloak 등)이 발급한 토큰운영 환경 사람 계정의 표준
웹훅 · 인증 프록시외부 서비스에 “이 토큰 누구야?”를 물어봄클라우드(GKE · EKS)의 IAM 연동 등
정적 토큰 파일 (--token-auth-file)CSV에 토큰 · 이름 나열바꾸려면 API 서버 재시작 — 실습용
정적 비밀번호 파일 (basic auth)CSV에 비밀번호1.19에서 제거 — 학습 자료에 나오면 옛 방식으로 이해

03. TLS 기초

쿠버네티스의 거의 모든 통신은 TLS로 암호화되고, 서로를 인증서로 확인해요. 인증서가 무엇인지부터 잡고 가요. 학습 자료도 이 부분을 꽤 길게 다뤄요.

1. 대칭키와 비대칭키

대칭키는 열쇠를 건네는 게 문제, 비대칭키는 자물쇠만 건넨다, 인증서는 그 자물쇠의 신분증
대칭키비대칭키 (공개키 · 개인키)
키잠그는 키 = 여는 키공개키(자물쇠)로 잠그면 개인키로만 열림
장점빠름공개키는 누구에게 줘도 됨
약점키를 상대에게 안전하게 건네기 어려움느림
TLS에서실제 데이터 암호화처음에 대칭키를 안전하게 주고받을 때

2. 인증서와 CA

인증서는 “이 공개키의 주인은 api.example.com이다”라는 문서에 CA(인증 기관)가 서명한 거예요. 받는 쪽은 미리 가지고 있는 CA의 공개키로 서명을 확인해서 가짜 서버를 걸러요. 브라우저는 세계적인 CA 목록을 내장하고 있고, 쿠버네티스는 클러스터 전용 CA를 하나 만들어 모든 부품이 그 CA를 믿게 해요.

파일무엇보통 이름
개인키절대 밖으로 안 나감*.key · *-key.pem
인증서 (공개키 + 서명)상대에게 보여 주는 신분증*.crt · *.pem
CSR (서명 요청)“이 공개키에 서명해 주세요”*.csr
서버 인증서와 클라이언트 인증서 보통 웹은 서버만 인증서를 보여 줘요. 쿠버네티스는 양쪽 다 보여 줘요(mTLS). API 서버는 서버 인증서로 “나 진짜 API 서버야”를, kubectl은 클라이언트 인증서로 “나 admin이야”를 증명해요. 02의 X.509 인증이 바로 이 클라이언트 인증서예요.

04. 쿠버네티스의 TLS

1. 컴포넌트별 인증서 지도

클러스터 CA 하나가 서명한 서버 인증서와 클라이언트 인증서들
인증서 (/etc/kubernetes/pki/)종류쓰는 곳
ca.crt · ca.keyCA클러스터의 모든 인증서에 서명 (기본 10년)
apiserver.crt서버API 서버가 자신을 증명
apiserver-etcd-client.crt클라이언트API 서버 → etcd
apiserver-kubelet-client.crt클라이언트API 서버 → kubelet (logs · exec)
etcd/ca.crt · etcd/server.crt · etcd/peer.crtetcd 전용 CA · 서버 · 복제etcd (1부 03의 etcdctl 인증서)
sa.key · sa.pub서명 키 쌍ServiceAccount 토큰 서명 (08)
/etc/kubernetes/admin.conf 등 *.conf클라이언트 인증서를 품은 kubeconfig관리자 · 스케줄러 · 컨트롤러 → API 서버

2. 인증서 내용 보기 (openssl)

인증서 문제는 대부분 이름이 안 맞거나, 만료됐거나, 다른 CA가 서명했거나 셋 중 하나예요. openssl로 직접 열어서 확인해요.

$ openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text -noout
        Issuer: CN = kubernetes                              ← 서명한 CA
        Validity
            Not After : Nov  2 09:12:41 2027 GMT             ← 만료일
        Subject: CN = kube-apiserver                         ← 주인
            X509v3 Subject Alternative Name:                 ← 이 이름들로 접속해도 인정
                DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc,
                DNS:kubernetes.default.svc.cluster.local, DNS:controlplane,
                IP Address:10.96.0.1, IP Address:192.168.0.10
$ sudo kubeadm certs check-expiration                       # 전체 만료일 한 번에
$ sudo kubeadm certs renew all                              # 갱신 (이후 컨트롤 플레인 재시작)
인증서는 1년이면 만료된다 kubeadm이 만든 컴포넌트 인증서는 기본 1년이에요. 만료되면 어느 날 갑자기 kubectl이 x509: certificate has expired로 안 돼요. 다행히 6부의 kubeadm upgrade가 갱신도 같이 해 줘서, 1년에 한 번 이상 업그레이드하면 자연스럽게 해결돼요. 안 되면 kubeadm certs renew.
학습 자료와 달라진 admin.conf 예전 kubeadm의 admin.conf는 system:masters 그룹(어떤 권한 설정으로도 막을 수 없는 그룹)에 속했어요. 1.29부터는 일반 그룹 kubeadm:cluster-admins로 바뀌었고, system:masters용 비상 열쇠는 super-admin.conf로 따로 분리됐어요.

3. Certificates API로 인증서 발급

새 팀원 jane에게 클러스터 접근 권한을 줄 때, CA 개인키로 직접 서명하는 대신 CertificateSigningRequest 객체를 만들어 쿠버네티스에 서명을 맡겨요. 서명은 컨트롤러 매니저가 CA 키로 해요.

개인키 → CSR → CertificateSigningRequest → 승인 → 서명된 인증서
$ openssl genrsa -out jane.key 2048
$ openssl req -new -key jane.key -subj "/CN=jane/O=dev-team" -out jane.csr    # CN=이름, O=그룹
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
  name: jane
spec:
  request: <cat jane.csr | base64 | tr -d "\n" 의 결과>
  signerName: kubernetes.io/kube-apiserver-client
  usages: ["client auth"]
  expirationSeconds: 86400        # 하루짜리
$ kubectl get csr
NAME   SIGNERNAME                            REQUESTOR          CONDITION
jane   kubernetes.io/kube-apiserver-client   kubernetes-admin   Pending
$ kubectl certificate approve jane                 # (거절은 deny)
$ kubectl get csr jane -o jsonpath='{.status.certificate}' | base64 -d > jane.crt

05. kubeconfig

매번 kubectl --server … --client-certificate … --client-key … --certificate-authority …를 칠 수는 없죠. 이걸 파일에 모아 둔 게 kubeconfig(기본 ~/.kube/config)예요. 입문 2부 04에서 GKE 접속 정보가 저장되던 그 파일이에요.

clusters · users · contexts — context가 “누가 + 어디에”를 묶는다
apiVersion: v1
kind: Config
current-context: admin@prod
clusters:
- name: prod
  cluster:
    server: https://192.168.0.10:6443
    certificate-authority-data: LS0tLS1CRUdJTi...   # ca.crt를 base64로 (또는 certificate-authority: 파일 경로)
users:
- name: admin
  user:
    client-certificate-data: LS0tLS1...           # 내 인증서
    client-key-data: LS0tLS1...                   # 내 개인키
- name: jane
  user:
    client-certificate: /home/jane/jane.crt
    client-key: /home/jane/jane.key
contexts:
- name: admin@prod
  context: { cluster: prod, user: admin }
- name: jane@prod
  context: { cluster: prod, user: jane, namespace: dev }
$ kubectl config view                               # 비밀값은 가려서 보여 줌
$ kubectl config get-contexts
$ kubectl config use-context jane@prod
$ kubectl config set-context --current --namespace=dev
$ kubectl get pods --kubeconfig=/root/my-kube-config  # 다른 파일 쓰기 (또는 KUBECONFIG 환경 변수)
kubeconfig는 열쇠 꾸러미 client-key-data에는 개인키가 들어 있어서, 파일만 있으면 누구든 그 사용자로 접속할 수 있어요. 메신저로 보내거나 Git에 올리지 않아요.

06. API Groups

권한(07)을 정확히 주려면 API가 어떻게 생겼는지 알아야 해요. 2부 01에서 본 apiVersion의 그룹이 실제 주소에 그대로 들어가요.

1. core 그룹과 named 그룹

/api/v1(core)과 /apis/그룹/버전(named) — 그리고 리소스마다 동사
$ kubectl proxy &                                   # 내 kubeconfig 인증으로 localhost:8001에 길을 열어 줌
Starting to serve on 127.0.0.1:8001
$ curl http://localhost:8001/apis/apps/v1 | head
$ curl http://localhost:8001/api/v1/namespaces/default/pods
kube-proxy와 kubectl proxy는 다르다 1부 07의 kube-proxy는 노드에서 Service 규칙을 만드는 부품이고, kubectl proxy는 내 PC에서 API 서버로 가는 임시 통로예요. 이름만 비슷해요.

2. 리소스와 동사(verbs)

동사kubectl로는
get · list · watchget pod web · get pods · get pods -w
createcreate · apply(없을 때) · run
update · patchedit · apply(있을 때) · scale · set image
delete · deletecollectiondelete pod web · delete pods --all
$ kubectl api-resources -o wide | grep -E "^NAME|^pods |^deployments "
NAME          SHORTNAMES   APIVERSION   NAMESPACED   KIND         VERBS
pods          po           v1           true         Pod          create,delete,deletecollection,get,list,patch,update,watch
deployments   deploy       apps/v1      true         Deployment   create,delete,deletecollection,get,list,patch,update,watch

pods/log · pods/exec처럼 하위 리소스도 있어요. “로그는 보되 exec는 못 하게” 같은 권한을 따로 줄 수 있어요.

07. RBAC

인가 방식은 여럿(Node · RBAC · ABAC · Webhook)이지만 표준은 RBAC(역할 기반 접근 제어)예요. “무엇을 할 수 있는가”를 Role로 만들고, “누구에게”를 RoleBinding으로 연결해요. kube-apiserver의 --authorization-mode=Node,RBAC(1부 04)가 이 설정이에요.

1. Role과 RoleBinding

Role(무엇을) + RoleBinding(누구에게) — 범위는 Namespace, 클러스터 전체는 Cluster로
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: dev
rules:
- apiGroups: [""]                   # "" = core 그룹 (06)
  resources: ["pods", "pods/log"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "create", "update"]
  resourceNames: []                 # 특정 이름만 허용할 때
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: jane-pod-reader
  namespace: dev
subjects:
- kind: User                        # User · Group · ServiceAccount
  name: jane                        # 인증서의 CN (04)
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
$ kubectl create role pod-reader -n dev --verb=get,list,watch --resource=pods,pods/log
$ kubectl create rolebinding jane-pod-reader -n dev --role=pod-reader --user=jane

2. ClusterRole과 ClusterRoleBinding

조합효과예
Role + RoleBinding그 Namespace 안에서만jane은 dev의 Pod만
ClusterRole + ClusterRoleBinding클러스터 전체 (모든 Namespace + 노드 · PV 같은 비Namespace 리소스)노드 관리자 · 모니터링 도구
ClusterRole + RoleBindingClusterRole의 규칙을 한 Namespace에서만기본 view · edit을 팀 Namespace에만 — 같은 Role을 Namespace마다 만들 필요가 없음

쿠버네티스에는 기본 ClusterRole이 있어요. cluster-admin(전부), admin(Namespace 관리), edit(읽기 · 쓰기, 권한 설정 제외), view(읽기 전용, Secret 제외). 새로 만들기 전에 이걸 먼저 써 보세요.

$ kubectl create clusterrole node-reader --verb=get,list,watch --resource=nodes
$ kubectl create clusterrolebinding jane-node-reader --clusterrole=node-reader --user=jane
$ kubectl create rolebinding jane-view -n prod --clusterrole=view --user=jane    # prod는 읽기만

3. kubectl auth can-i로 권한 확인

$ kubectl auth can-i create deployments -n dev --as jane
yes
$ kubectl auth can-i delete pods -n prod --as jane
no
$ kubectl auth can-i list pods -n dev --as system:serviceaccount:dev:ci-bot   # ServiceAccount로
$ kubectl auth can-i --list -n dev --as jane                                  # 할 수 있는 것 전부
$ kubectl get rolebindings,clusterrolebindings -A -o wide | grep jane           # 누가 jane에게 줬나
RBAC은 허용만 있고 금지는 없다 규칙은 더하기만 돼요. “전부 되는데 Secret만 빼고” 같은 금지 규칙은 쓸 수 없어서, 필요한 것만 하나씩 허용해야 해요. 그리고 *(모든 리소스 · 동사)는 정말 필요할 때만 써요.

08. ServiceAccount

1. Pod가 API를 부르는 방법

모니터링 도구 · CI · 오퍼레이터(15부)처럼 Pod 안의 앱이 API를 불러야 할 때가 있어요. Pod는 ServiceAccount의 이름으로 인증하고, 그 ServiceAccount에 RoleBinding으로 권한을 줘요. Namespace마다 default ServiceAccount가 자동으로 생기고, 따로 지정하지 않은 Pod는 이걸 써요(1부 05의 ServiceAccount 컨트롤러).

ServiceAccount → 시간 제한 토큰 → Pod에 자동 마운트 → API 호출 → RBAC
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ci-bot
  namespace: dev
automountServiceAccountToken: true     # API를 안 부르는 Pod라면 false 권장
---
spec:                                   # Pod
  serviceAccountName: ci-bot            # 생략하면 default
  containers:
  - name: app
    image: ci:1.0
$ kubectl exec -it web -- ls /var/run/secrets/kubernetes.io/serviceaccount
ca.crt  namespace  token
$ kubectl create rolebinding ci-bot-edit -n dev --clusterrole=edit --serviceaccount=dev:ci-bot

2. 바뀐 토큰 방식 (시간 제한 토큰)

학습 자료에 나오는 예전 방식은 ServiceAccount를 만들면 만료 없는 토큰이 담긴 Secret이 자동으로 생겼어요. 이 토큰은 유출되면 영원히 쓸 수 있는 위험이 있어서, 1.24부터 자동 생성이 사라졌어요. 지금은 kubelet이 TokenRequest API로 시간 제한 · 대상(audience) 제한 · Pod에 묶인 토큰을 받아 넣고, 만료 전에 계속 새로 바꿔요.

예전 (Secret 토큰)지금 (projected 토큰)
만료없음있음 (Pod용은 kubelet이 자동 갱신)
Pod가 지워지면토큰은 그대로 유효그 Pod에 묶인 토큰은 무효
생성ServiceAccount마다 Secret 자동 생성필요할 때 발급 · Secret 없음
밖에서 쓸 토큰Secret에서 꺼냄kubectl create token ci-bot --duration=1h
그래도 만료 없는 토큰이 꼭 필요하면 type: kubernetes.io/service-account-token인 Secret을 직접 만들고 어노테이션으로 ServiceAccount를 지정하면 예전 방식의 토큰이 생겨요. 다만 쓰지 않는 게 권장이고, 쓰면 주기적으로 교체해요.

09. Admission Controller

인증 · 인가는 “누가, 무엇을 해도 되는가”만 봐요. 하지만 “Pod를 만들 권한은 있는데, latest 태그 이미지는 안 돼” 같은 요청 내용에 대한 규칙은 인가로는 못 걸러요. 그걸 하는 게 어드미션 컨트롤러예요. 학습 자료에서는 스케줄링 주제에 있지만, API 서버 관문의 마지막 단계라 이 부에서 다뤄요.

1. 인가 다음 관문

Mutating(고치기) → 스키마 검사 → Validating(검사) → etcd
$ kube-apiserver -h | grep enable-admission-plugins           # 기본으로 켜진 목록
$ grep admission /etc/kubernetes/manifests/kube-apiserver.yaml
    - --enable-admission-plugins=NodeRestriction
$ kubectl run nginx --image=nginx -n blue
Error from server (NotFound): namespaces "blue" not found      ← NamespaceLifecycle이 거절
기본으로 켜진 플러그인 (일부)종류하는 일
NamespaceLifecycle검사없는 · 지워지는 Namespace에 생성 거절
ServiceAccount고치기Pod에 ServiceAccount · 토큰 볼륨 자동 추가 (08)
DefaultTolerationSeconds고치기not-ready · unreachable 300초 toleration 추가 (1부 05)
LimitRanger · ResourceQuota고치기 · 검사기본 requests 채우기 · 쿼터 초과 거절 (3부 06 · 2부 04)
DefaultStorageClass고치기StorageClass 없는 PVC에 기본값 (9부)
PodSecurity검사Pod Security Standards 위반 거절 (8부 02)
MutatingAdmissionWebhook · ValidatingAdmissionWebhook웹훅외부 서버에 판단을 맡김 (아래 2)

학습 자료의 NamespaceAutoProvision(없는 Namespace를 자동으로 만들기)처럼 기본으로 꺼진 플러그인은 --enable-admission-plugins에 추가해서 켜고, 끄려면 --disable-admission-plugins를 써요.

2. Validating · Mutating 웹훅

내장 플러그인으로 부족하면 웹훅으로 직접 만든 서버에 판단을 맡겨요. API 서버가 요청 내용을 AdmissionReview로 보내면, 서버가 “허용 / 거절(이유)”, Mutating이라면 “이렇게 고쳐라(JSON 패치)”를 돌려줘요. 서비스 메시의 사이드카 자동 주입, OPA Gatekeeper · Kyverno 같은 정책 도구가 이 방식이에요.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: image-policy
webhooks:
- name: image-policy.example.com
  clientConfig:
    service: { namespace: policy, name: image-checker, path: /validate }
    caBundle: LS0tLS1...          # 웹훅 서버 인증서를 믿기 위한 CA
  rules:
  - apiGroups: [""]
    apiVersions: ["v1"]
    operations: ["CREATE"]
    resources: ["pods"]
  admissionReviewVersions: ["v1"]
  sideEffects: None
  failurePolicy: Fail              # 웹훅 서버가 죽으면 거절 (Ignore면 통과)

3. ValidatingAdmissionPolicy

웹훅은 서버를 따로 띄우고 인증서까지 관리해야 해서 번거롭고, 그 서버가 죽으면 클러스터 생성 요청이 막혀요. ValidatingAdmissionPolicy(1.30 GA)는 규칙을 CEL이라는 짧은 식으로 써서 API 서버 안에서 바로 검사해요. 간단한 검사는 이제 이쪽이 권장이에요.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: max-replicas
spec:
  matchConstraints:
    resourceRules:
    - apiGroups: ["apps"]
      apiVersions: ["v1"]
      operations: ["CREATE", "UPDATE"]
      resources: ["deployments"]
  validations:
  - expression: "object.spec.replicas <= 5"
    message: "레플리카는 5개까지만 허용"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: max-replicas-dev
spec:
  policyName: max-replicas
  validationActions: ["Deny"]
  matchResources:
    namespaceSelector:
      matchLabels: { env: dev }

4. 7부 셀프 체크

  1. 쿠버네티스에서 사람 사용자는 어떻게 만드나요?
    객체로 만들지 않아요. 클러스터 밖에서 인증서 · OIDC 등으로 신원을 증명하면 API 서버가 이름과 그룹을 알아내요. 기계 계정만 ServiceAccount 객체로 만들어요.
  2. TLS에서 비대칭키는 무엇에 쓰고, 실제 데이터는 무엇으로 암호화하나요?
    비대칭키는 처음에 대칭키를 안전하게 주고받는 데 쓰고, 실제 데이터는 빠른 대칭키로 암호화해요.
  3. 인증서가 필요한 이유는? 공개키만 있으면 안 되나요?
    가짜 서버가 자기 공개키를 보낼 수 있어서예요. CA가 “이 공개키의 주인은 누구”라고 서명한 인증서로 진짜인지 확인해요.
  4. X.509 클라이언트 인증서에서 사용자 이름과 그룹은 어디에 들어가나요?
    CN = 사용자 이름, O = 그룹. 예: /CN=jane/O=dev-team.
  5. API 서버 인증서의 만료일과 허용된 이름(SAN)을 확인하는 명령은?
    openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text -noout. 전체 만료일은 kubeadm certs check-expiration.
  6. 새 사용자 인증서를 쿠버네티스로 서명받는 순서는?
    개인키 · CSR 생성 → CertificateSigningRequest 객체 생성 → kubectl certificate approve → .status.certificate를 base64 디코딩해 인증서로 저장.
  7. kubeconfig의 세 부분과 context의 역할은?
    clusters · users · contexts. context는 “어떤 user로 어떤 cluster에(기본 Namespace까지)”를 묶은 이름이에요.
  8. Deployment 권한을 줄 때 Role의 apiGroups에 무엇을 쓰나요?
    "apps". Pod · Service 같은 core 그룹은 "".
  9. ClusterRole을 RoleBinding으로 묶으면?
    ClusterRole의 규칙이 그 RoleBinding의 Namespace 안에서만 적용돼요.
  10. jane이 prod에서 Pod를 지울 수 있는지 확인하는 명령은?
    kubectl auth can-i delete pods -n prod --as jane.
  11. 1.24 이후 ServiceAccount 토큰은 무엇이 달라졌나요?
    Secret이 자동으로 생기지 않고, kubelet이 시간 제한 · Pod에 묶인 토큰을 받아 넣고 자동 갱신해요. 밖에서 쓸 토큰은 kubectl create token.
  12. Mutating과 Validating 어드미션의 순서와 차이는?
    Mutating(요청을 고침)이 먼저, 스키마 검사 뒤 Validating(허용 / 거절만)이 나중이에요.
  13. 웹훅 서버 없이 “replicas는 5 이하” 규칙을 거는 방법은?
    ValidatingAdmissionPolicy에 CEL 식 object.spec.replicas <= 5를 쓰고 Binding으로 적용 범위를 정해요.
8부보안 ② 워크로드와 네트워크이미지 보안SecurityContextPod Security AdmissionNetwork Policy

01. 이미지 보안

7부가 “누가 API에 들어오나”였다면, 8부는 이미 실행된 것을 지키는 방법이에요. 무엇을 실행하나(이미지), 어떤 권한으로 실행하나(SecurityContext), 누구와 통신하나(Network Policy). 먼저 이미지예요.

이미지 이름은 레지스트리/저장소/이름:태그예요(입문 1부 04). 레지스트리를 생략하면 Docker Hub(docker.io), 저장소를 생략하면 library예요. 회사 이미지는 보통 비공개 레지스트리에 두고, 받으려면 로그인 정보가 필요해요.

비공개 레지스트리 — 로그인 정보 없이는 ImagePullBackOff
$ kubectl create secret docker-registry regcred \
    --docker-server=myregistry.example.com \
    --docker-username=ci-bot --docker-password='…' --docker-email=ci@example.com
spec:
  containers:
  - name: web
    image: myregistry.example.com/team/web:1.4.2
  imagePullSecrets:
  - name: regcred
$ kubectl patch serviceaccount default -n dev \
    -p '{"imagePullSecrets": [{"name": "regcred"}]}'     # 이 SA를 쓰는 Pod 전부에 자동으로
지킬 것방법
정확히 그 이미지인가태그 대신 다이제스트로 고정 web@sha256:9b1e… — 태그는 덮어쓸 수 있어요
매번 새로 받을까imagePullPolicy: IfNotPresent(기본) · Always(태그가 latest면 기본) · Never
취약점이 없나빌드할 때 스캐너(Trivy 등)로 검사
우리가 만든 이미지인가서명(cosign)하고 어드미션(7부 09)에서 서명 없는 이미지 거절
허용한 레지스트리만ValidatingAdmissionPolicy로 image.startsWith('myregistry.example.com/') 같은 규칙

02. Pod 보안 설정

컨테이너는 기본적으로 root(UID 0)로 실행되는 경우가 많아요. 컨테이너도 결국 노드의 프로세스라서(입문 1부 03), 격리를 뚫고 나오면 노드의 root가 될 수 있어요. 그래서 “필요한 최소한의 권한으로만” 실행하게 설정해요.

1. SecurityContext

root로 · 모든 권한으로 · 쓰기 가능하게 — 하나씩 줄여 간다
spec:
  securityContext:                # Pod 전체 (모든 컨테이너 기본값)
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000                 # 볼륨 파일의 그룹
    runAsNonRoot: true            # root면 시작 거부
    seccompProfile:
      type: RuntimeDefault        # 위험한 시스템 호출 차단
  containers:
  - name: web
    image: web:1.0
    securityContext:              # 컨테이너별 (Pod 설정보다 우선)
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:               # ← 컨테이너 수준에만 있음
        drop: ["ALL"]
        add: ["NET_BIND_SERVICE"] # 1024 미만 포트 열기만 허용
    volumeMounts:
    - { name: tmp, mountPath: /tmp }   # 쓰기가 필요한 곳만 볼륨으로
  volumes:
  - { name: tmp, emptyDir: {} }
$ kubectl exec web -- id
uid=1000 gid=3000 groups=2000
설정막는 것
runAsUser · runAsNonRootroot로 실행
capabilities.drop: ["ALL"]root가 가진 특수 권한(네트워크 설정 · 시간 변경 · 소유자 무시 등) 전부
allowPrivilegeEscalation: false실행 중에 setuid 등으로 권한이 높아지는 것
readOnlyRootFilesystem컨테이너 안에 악성 파일을 쓰는 것
privileged: true (반대 방향)아무것도 안 막음 — 노드와 거의 같은 권한. CNI · 스토리지 드라이버 같은 시스템 Pod 외엔 쓰지 않아요
user namespace — 컨테이너의 root를 노드에서는 일반 사용자로 Pod에 hostUsers: false를 쓰면 컨테이너 안의 UID 0이 노드에서는 권한 없는 다른 UID로 매핑돼요. 격리가 뚫려도 노드의 root가 아니에요. 최근 버전(1.36)에서 정식이 됐어요. 노드의 커널 · 런타임이 지원해야 해요.

2. Pod Security Admission

모든 개발자가 SecurityContext를 잘 쓰리라 믿을 순 없죠. Pod Security Admission은 Namespace에 라벨 하나만 붙이면 그 Namespace의 Pod가 정해진 보안 수준(Pod Security Standards)을 지키는지 어드미션(7부 09)에서 검사해요. 학습 자료가 나온 뒤 쿠버네티스에 기본 내장된 기능이에요.

Namespace 라벨 하나로 — privileged · baseline · restricted
$ kubectl label namespace prod \
    pod-security.kubernetes.io/enforce=restricted \
    pod-security.kubernetes.io/warn=restricted
$ kubectl run test --image=nginx -n prod
Error from server (Forbidden): pods "test" is forbidden: violates PodSecurity "restricted:latest":
allowPrivilegeEscalation != false (container "test" must set securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "test" must set securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "test" must set securityContext.runAsNonRoot=true),
seccompProfile (pod or container "test" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
수준허용 범위쓰는 곳
privileged제한 없음kube-system · CNI · 스토리지 같은 시스템
baseline알려진 위험(호스트 네트워크 · privileged · hostPath 등)만 막음대부분의 일반 앱
restrictedroot 금지 · capabilities 전부 제거 · seccomp 필수 등 모범 사례 강제보안이 중요한 Namespace
모드 (라벨 이름)위반하면
enforcePod 생성 거절
warn만들어지지만 kubectl에 경고 표시
audit만들어지지만 감사 로그에 기록
학습 자료의 PodSecurityPolicy는 없어졌다 예전 학습 자료에 나오는 PodSecurityPolicy(PSP)는 1.25에서 제거됐어요. 대신 이 Pod Security Admission이 기본으로 들어갔어요. enforce는 이미 있는 Pod를 지우지 않으니, 처음엔 warn · audit로 어떤 Pod가 걸리는지 보고 나서 enforce를 켜는 게 안전해요.

03. Network Policy

클러스터 안의 Pod는 기본적으로 누구와든 통신할 수 있어요. 웹 Pod 하나가 털리면 그 Pod에서 DB로 바로 접속할 수 있다는 뜻이죠. NetworkPolicy는 Pod 단위의 방화벽이에요. 라벨로 대상 Pod를 고르고, 들어오는(ingress) · 나가는(egress) 트래픽을 허용 목록으로 정해요.

1. 기본은 모두 허용

정책이 고른 Pod만, 허용한 것만 — 나머지는 막힌다

2. ingress · egress 규칙

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-policy
  namespace: prod
spec:
  podSelector:
    matchLabels: { app: db }          # 이 정책이 지키는 Pod
  policyTypes: ["Ingress"]           # 들어오는 것만 제한 (Egress도 쓰면 나가는 것도)
  ingress:
  - from:
    - podSelector:
        matchLabels: { app: api }    # 같은 Namespace의 api Pod
    - namespaceSelector:
        matchLabels: { kubernetes.io/metadata.name: monitoring }   # 또는 monitoring Namespace 전체
    ports:
    - { protocol: TCP, port: 3306 }
from 아래 “- 하나”냐 “둘”이냐 (AND와 OR) 위처럼 podSelector와 namespaceSelector가 각자 -로 시작하면 “api Pod 또는 monitoring Namespace”(OR)예요. - 하나 아래에 둘을 같이 쓰면 “monitoring Namespace에 있는 api Pod”(AND)가 돼요. 들여쓰기 한 칸으로 의미가 완전히 바뀌니 시험 · 실무 모두 자주 틀리는 부분이에요.
# 모든 Pod의 모든 트래픽을 일단 막기 (default deny) — 여기에 필요한 것만 허용을 더해 간다
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: prod
spec:
  podSelector: {}                    # Namespace의 모든 Pod
  policyTypes: ["Ingress", "Egress"]
---
# egress를 막았다면 DNS는 꼭 열어 준다
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: prod
spec:
  podSelector: {}
  policyTypes: ["Egress"]
  egress:
  - to:
    - namespaceSelector:
        matchLabels: { kubernetes.io/metadata.name: kube-system }
      podSelector:
        matchLabels: { k8s-app: kube-dns }
    ports:
    - { protocol: UDP, port: 53 }
    - { protocol: TCP, port: 53 }
알아 둘 규칙내용
정책은 더하기만여러 정책이 같은 Pod를 고르면 허용 목록이 합쳐져요. “금지” 규칙은 없어요
고른 순간 기본 거부어떤 정책이든 Pod를 Ingress로 고르면, 그 Pod로 들어오는 건 허용한 것 빼고 전부 막혀요
응답은 자동 허용허용된 연결의 응답 패킷은 따로 열 필요 없어요
대상 고르기podSelector · namespaceSelector · ipBlock(클러스터 밖 IP 대역)

3. CNI가 지원해야 동작한다

NetworkPolicy 객체는 API 서버에 저장될 뿐이고, 실제로 패킷을 막는 건 CNI 플러그인(11부 03)이에요. Calico · Cilium은 지원하지만 Flannel만 쓰면 정책을 만들어도 아무 효과가 없고 오류도 안 나요. 학습 자료의 Weave는 지원했지만 개발이 중단됐어요. 정책이 안 먹히면 CNI부터 확인해요.

$ kubectl get networkpolicy -n prod
$ kubectl describe networkpolicy db-policy -n prod
$ kubectl exec -n prod web -- nc -zv -w 2 db 3306      # 실제로 막히는지 시험
nc: db (10.96.33.21:3306): Connection timed out

4. 8부 셀프 체크

  1. 비공개 레지스트리 이미지를 받으려면 무엇이 필요하고 어디에 넣나요?
    docker-registry 타입 Secret을 만들고 Pod의 imagePullSecrets에 넣어요. ServiceAccount에 붙이면 그 SA의 모든 Pod가 써요.
  2. 로그인 정보가 없어서 이미지를 못 받으면 Pod 상태는?
    ErrImagePull → ImagePullBackOff. Events에 401 · unauthorized가 보여요.
  3. Pod 수준과 컨테이너 수준 SecurityContext가 둘 다 있으면?
    컨테이너 수준이 우선이에요. 그리고 capabilities는 컨테이너 수준에만 있어요.
  4. capabilities를 안전하게 주는 방법은?
    drop: ["ALL"]로 전부 빼고, 필요한 것만 add해요(예: NET_BIND_SERVICE).
  5. readOnlyRootFilesystem: true인데 앱이 /tmp에 써야 하면?
    /tmp에 emptyDir 볼륨을 마운트해서 그곳만 쓰게 해요.
  6. Namespace의 모든 Pod에 restricted 수준을 강제하는 방법은?
    Namespace에 pod-security.kubernetes.io/enforce=restricted 라벨을 붙여요(Pod Security Admission).
  7. PodSecurityPolicy는 지금 쓸 수 있나요?
    아니요. 1.25에서 제거됐어요. Pod Security Admission이나 Kyverno · Gatekeeper · ValidatingAdmissionPolicy를 써요.
  8. NetworkPolicy가 하나도 없을 때 Pod끼리의 통신은?
    모두 허용이에요.
  9. db Pod에 “api Pod에서 3306만 허용” ingress 정책을 걸면 web → db는?
    막혀요. 정책이 db를 고른 순간 허용 목록에 없는 들어오는 연결은 전부 거부돼요.
  10. from 아래 podSelector와 namespaceSelector를 같은 -에 쓰는 것과 따로 쓰는 것의 차이는?
    같은 -면 AND(그 Namespace의 그 Pod), 따로면 OR(그 Pod 또는 그 Namespace)예요.
  11. egress를 막았더니 서비스 이름으로 접속이 안 돼요. 왜일까요?
    DNS(kube-dns, UDP · TCP 53)로 가는 egress를 열지 않아서 이름 풀이가 실패한 거예요.
  12. NetworkPolicy를 만들었는데 아무 효과가 없어요. 먼저 의심할 곳은?
    CNI 플러그인이 NetworkPolicy를 지원하는지예요(Flannel만으로는 안 돼요).
9부스토리지컨테이너 저장 구조CSIVolumePV · PVCStorageClass상태 있는 앱

01. 컨테이너 스토리지 기초

컨테이너는 지워지면 안의 데이터도 같이 사라져요. 왜 그런지, 그럼 데이터를 어디에 둬야 하는지를 컨테이너 수준부터 쿠버네티스 수준까지 차례로 올라가며 봐요. 입문 6부 03 · 04의 복습이자 확장이에요.

1. Docker의 레이어와 스토리지 드라이버

읽기 전용 이미지 레이어 + 얇은 쓰기 레이어 — 그리고 볼륨
용어뜻
이미지 레이어Dockerfile 한 줄마다 생기는 읽기 전용 층 (입문 1부 04). 같은 레이어는 컨테이너 여러 개가 공유
컨테이너 레이어컨테이너마다 생기는 얇은 쓰기 층. 컨테이너를 지우면 사라짐
copy-on-write이미지의 파일을 고치면 쓰기 층으로 복사한 뒤 고침 — 원본 이미지는 그대로
스토리지 드라이버이 층들을 하나로 겹쳐 보여 주는 장치. 지금은 거의 overlay2(overlayfs). containerd에서는 snapshotter라고 불러요
볼륨층 바깥의 저장 공간을 컨테이너 안 경로에 연결. 컨테이너가 지워져도 남음
$ docker volume create data_volume                         # /var/lib/docker/volumes/data_volume
$ docker run -v data_volume:/var/lib/mysql mysql            # 볼륨 마운트
$ docker run --mount type=bind,source=/data/mysql,target=/var/lib/mysql mysql   # 호스트 폴더 그대로 (bind mount)

2. Volume Driver Plugins

볼륨을 실제로 어디에 만들지는 볼륨 드라이버가 정해요. 기본(local)은 그 서버의 디스크이고, 플러그인을 쓰면 클라우드 디스크 · NAS 같은 외부 저장소에 만들 수 있어요. 컨테이너가 다른 서버로 옮겨 가도 데이터가 따라가게 하려면 이런 외부 저장소가 필요해요. 쿠버네티스에서는 이 역할을 다음 절의 CSI 드라이버가 맡아요.

02. CSI

1. Container Storage Interface

1부 02의 CRI가 “런타임을 꽂는 규격”이었던 것처럼, CSI는 “저장소를 꽂는 규격”이에요(네트워크는 CNI, 11부). 예전에는 AWS · GCP 디스크 지원 코드가 쿠버네티스 본체 안에 들어 있었지만(in-tree), 지금은 저장소 회사가 만든 CSI 드라이버를 설치해서 써요.

CSI 드라이버 — 디스크를 만들고 · 노드에 붙이고 · Pod에 마운트한다
CSI 호출누가하는 일
CreateVolume · DeleteVolume컨트롤러 플러그인 (+ external-provisioner)저장소에 디스크 만들기 · 지우기
ControllerPublishVolume컨트롤러 플러그인 (+ external-attacher)디스크를 노드에 붙이기 (attach)
NodeStageVolume · NodePublishVolume노드 플러그인 (DaemonSet)노드에서 포맷 · 마운트하고 Pod 경로에 연결
ControllerExpandVolume컨트롤러 플러그인 (+ external-resizer)디스크 크기 늘리기 (아래 2)
$ kubectl get csidrivers
NAME                    ATTACHREQUIRED   PODINFOONMOUNT   MODES
pd.csi.storage.gke.io   true             false            Persistent
$ kubectl get pods -n kube-system | grep csi       # 컨트롤러(Deployment) + 노드(DaemonSet)

2. CSI Volume Expansion

디스크가 꽉 찼을 때 데이터를 옮기지 않고 크기만 늘릴 수 있어요. StorageClass가 allowVolumeExpansion: true이고 CSI 드라이버가 지원하면, PVC의 요청 크기만 늘리면 돼요.

PVC의 storage를 늘리면 — 디스크 확장 → 파일 시스템 확장
$ kubectl patch pvc data -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
$ kubectl get pvc data
NAME   STATUS   VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS
data   Bound    pvc-7d…    20Gi       RWO            standard-rwo
늘리기만 되고 줄이기는 안 된다 PVC 크기를 줄이면 API 서버가 거절해요. 그리고 PV를 직접 만든 정적 프로비저닝(04)에서는 저장소 쪽 디스크를 따로 늘려야 해요.

03. Volume 복습

쿠버네티스 Volume은 Pod 단위예요. 같은 Pod의 컨테이너끼리 공유하고, 종류에 따라 Pod보다 오래 살기도 해요. 어떤 볼륨이 언제까지 사는지가 핵심이에요.

emptyDir는 Pod와 함께, hostPath는 노드에, PVC는 어디로 가든 따라온다
종류데이터가 사는 곳언제까지주로
emptyDir노드 디스크(또는 메모리)Pod가 지워질 때까지컨테이너끼리 공유 · 임시 파일 (5부 05 사이드카)
hostPath노드의 특정 폴더노드가 있는 한 — 다른 노드에선 안 보임노드 로그 · 실습. 보안 위험이 커서 일반 앱엔 X (8부 02)
configMap · secret · projectedAPI 객체객체가 있는 한설정 · 토큰 (5부 03 · 7부 08)
persistentVolumeClaim외부 저장소 디스크PVC를 지울 때까지 (Pod와 무관)DB · 업로드 파일

04. PV와 PVC

PV(PersistentVolume)는 클러스터에 준비된 실제 저장 공간, PVC(PersistentVolumeClaim)는 “이런 저장 공간이 필요해요”라는 요청서예요(입문 6부 04). 관리자는 PV를, 개발자는 PVC를 다루도록 역할이 나뉘어요. PV는 Namespace에 속하지 않고, PVC는 Namespace에 속해요.

1. 접근 모드와 반납 정책

PVC는 조건에 맞는 PV와 일대일로 묶이고, 반납 정책이 PVC 삭제 후를 정한다
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-20g
spec:
  capacity: { storage: 20Gi }
  accessModes: ["ReadWriteOnce"]
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  hostPath: { path: /mnt/data }        # 실습용. 운영은 csi: 드라이버
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data
spec:
  accessModes: ["ReadWriteOnce"]
  storageClassName: manual
  resources:
    requests: { storage: 8Gi }
접근 모드뜻
ReadWriteOnce (RWO)한 노드에서 읽기 · 쓰기 (그 노드의 Pod 여럿은 가능). 클라우드 블록 디스크 대부분
ReadOnlyMany (ROX)여러 노드에서 읽기만
ReadWriteMany (RWX)여러 노드에서 읽기 · 쓰기 — NFS · 파일 스토리지
ReadWriteOncePod (RWOP)Pod 하나만 읽기 · 쓰기 (1.29 GA)
반납 정책PVC를 지우면
RetainPV는 Released로 남고 데이터도 남아요. 다른 PVC가 바로 쓸 수는 없고, 관리자가 정리해야 해요
DeletePV와 실제 디스크까지 지워요. 동적 프로비저닝(05)의 기본값
Recycle안의 파일만 지우고 재사용 — 지원 중단, 학습 자료에 나오면 옛 방식

2. Pod에서 PVC 쓰기

spec:
  containers:
  - name: mysql
    image: mysql:8.4
    volumeMounts:
    - name: data
      mountPath: /var/lib/mysql
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: data
$ kubectl get pv,pvc
NAME                      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM          STORAGECLASS
persistentvolume/pv-20g   20Gi       RWO            Retain           Bound    default/data   manual
NAME                         STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS
persistentvolumeclaim/data   Bound    pv-20g   20Gi       RWO            manual
PVC가 Pending이라면 / 지웠는데 Terminating이라면 PVC가 Pending이면 크기 · 접근 모드 · storageClassName이 맞는 PV가 없는 거예요(kubectl describe pvc의 Events). 반대로 Pod가 쓰고 있는 PVC를 지우면 데이터를 지키려고 Terminating에서 멈춰요(PVC 보호). Pod를 먼저 지우면 그때 지워져요.

05. StorageClass

1. 동적 프로비저닝

PV를 관리자가 미리 만들어 두는 방식(정적)은 사람이 계속 일해야 해요. StorageClass는 “이 종류의 저장소는 이 드라이버로 이렇게 만들어라”라는 틀이에요. PVC가 StorageClass를 지정하면 CSI 드라이버가 그때 디스크와 PV를 자동으로 만들어요. 입문 6부에서 GKE의 standard-rwo로 했던 게 이거예요.

PVC → StorageClass → 디스크와 PV 자동 생성 — 그리고 언제 만들지
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"   # storageClassName 없는 PVC의 기본값
provisioner: pd.csi.storage.gke.io      # 어떤 CSI 드라이버로
parameters:
  type: pd-ssd                          # 드라이버별 옵션
reclaimPolicy: Delete                   # 기본 Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

2. volumeBindingMode

모드디스크를 만드는 때문제 · 장점
ImmediatePVC를 만드는 즉시Pod가 다른 영역(zone)에 배치되면 디스크를 붙일 수 없어 Pending
WaitForFirstConsumer그 PVC를 쓰는 Pod가 스케줄될 때Pod가 갈 노드의 영역에 만들어서 안전. 그 전까진 PVC가 Pending인 게 정상
$ kubectl get storageclass
NAME                     PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION
standard-rwo (default)   pd.csi.storage.gke.io   Delete          WaitForFirstConsumer   true
premium-rwo              pd.csi.storage.gke.io   Delete          WaitForFirstConsumer   true

06. 상태 있는 앱의 스토리지

DB처럼 Pod마다 자기 데이터가 있어야 하는 앱은 Deployment + PVC 하나로는 안 돼요(레플리카 전부가 같은 PVC를 나눠 써야 하니까요). 학습 목차엔 없지만 스토리지와 떼어 놓을 수 없어 넣었어요.

1. StatefulSet과 volumeClaimTemplates

Pod마다 자기 PVC — 다시 만들어져도 같은 이름 · 같은 디스크
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  serviceName: db              # headless Service (11부 05)
  replicas: 3
  selector: { matchLabels: { app: db } }
  template:
    metadata: { labels: { app: db } }
    spec:
      containers:
      - name: mysql
        image: mysql:8.4
        volumeMounts:
        - { name: data, mountPath: /var/lib/mysql }
  volumeClaimTemplates:        # Pod마다 이 모양으로 PVC를 하나씩
  - metadata: { name: data }   # → data-db-0, data-db-1, data-db-2
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast
      resources: { requests: { storage: 10Gi } }
StatefulSet을 줄이거나 지워도 PVC는 남는다 데이터를 실수로 잃지 않도록 기본적으로 PVC를 지우지 않아요. 자동으로 지우고 싶으면 persistentVolumeClaimRetentionPolicy(whenDeleted · whenScaled를 Delete로)를 써요.

2. VolumeSnapshot

6부 03에서 etcd 스냅샷에는 볼륨 데이터가 없다고 했죠. 볼륨 자체는 VolumeSnapshot으로 저장소 수준의 스냅샷을 떠요. CSI 드라이버가 지원해야 하고, 스냅샷 컨트롤러가 설치돼 있어야 해요.

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: data-db-0-snap
spec:
  volumeSnapshotClassName: csi-snapclass
  source:
    persistentVolumeClaimName: data-db-0
---
apiVersion: v1                  # 스냅샷에서 새 PVC로 복원
kind: PersistentVolumeClaim
metadata:
  name: data-restore
spec:
  storageClassName: fast
  dataSource:
    name: data-db-0-snap
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io
  accessModes: ["ReadWriteOnce"]
  resources: { requests: { storage: 10Gi } }

3. 9부 셀프 체크

  1. 컨테이너를 지우면 그 안에서 쓴 파일이 사라지는 이유는?
    컨테이너마다 생기는 얇은 쓰기 레이어에 저장되고, 컨테이너를 지우면 그 레이어도 함께 지워지기 때문이에요.
  2. copy-on-write란?
    이미지(읽기 전용) 레이어의 파일을 고칠 때, 쓰기 레이어로 복사한 뒤 고치는 방식이에요. 원본 이미지는 바뀌지 않아요.
  3. CRI · CNI · CSI는 각각 무엇을 꽂는 규격인가요?
    CRI = 컨테이너 런타임, CNI = 네트워크, CSI = 저장소.
  4. PVC 크기를 늘리려면 무엇이 필요한가요? 줄일 수는 있나요?
    StorageClass의 allowVolumeExpansion: true와 확장을 지원하는 CSI 드라이버가 필요하고, PVC의 storage만 늘리면 돼요. 줄이기는 안 돼요.
  5. hostPath 볼륨의 데이터가 Pod 재생성 후 안 보일 수 있는 이유는?
    hostPath는 그 노드의 폴더라서, Pod가 다른 노드에 다시 만들어지면 그 노드의 빈 폴더를 보게 돼요.
  6. 8Gi RWO를 요청한 PVC가 5Gi RWO · 10Gi RWX · 20Gi RWO PV 중 어느 것과 묶이나요?
    20Gi RWO. 5Gi는 작고, 10Gi는 RWO 모드가 없어요. PV는 PVC 하나와만 묶이니 남는 12Gi는 다른 PVC가 못 써요.
  7. reclaimPolicy Retain인 PV에서 PVC를 지우면?
    PV는 Released 상태로 데이터와 함께 남아요. 다른 PVC가 바로 쓸 수는 없고 관리자가 정리해야 해요.
  8. Pod가 쓰고 있는 PVC를 지우면?
    PVC 보호 때문에 Terminating에서 멈추고, Pod가 사라지면 그때 지워져요.
  9. WaitForFirstConsumer를 쓰는 이유는?
    Pod가 배치될 노드의 영역(zone)에 디스크를 만들어서, 디스크와 Pod가 다른 영역에 있어 붙지 못하는 문제를 막기 위해서예요.
  10. StatefulSet의 volumeClaimTemplates로 만든 PVC 이름 규칙은?
    템플릿이름-StatefulSet이름-순번. 예: data-db-0. Pod가 다시 만들어져도 같은 PVC에 붙어요.
10부네트워크 기초스위치 · 라우터 · 게이트웨이DNS네트워크 네임스페이스Docker 네트워킹CNI

01. 스위칭 · 라우팅 · 게이트웨이

쿠버네티스 네트워킹(11부)은 리눅스 네트워크 기능을 조합해서 만들어져 있어요. 그래서 학습 자료도 쿠버네티스 전에 리눅스 네트워크 사전 지식을 먼저 다뤄요. 인프라를 처음 접한다면 이 부가 11부를 이해하는 열쇠예요. 명령은 실습용 리눅스(VM)에서 해 보면 가장 빨리 익어요.

같은 네트워크는 스위치로, 다른 네트워크는 라우터를 거쳐
$ ip link                                          # 네트워크 인터페이스 목록 (eth0 …)
$ ip addr add 192.168.1.10/24 dev eth0             # 인터페이스에 IP 주기
$ ip route                                         # 라우팅 표 보기
$ ip route add 192.168.2.0/24 via 192.168.1.1      # 2번 네트워크는 1.1(라우터)로 보내라
$ ip route add default via 192.168.1.1             # 나머지 전부(인터넷 포함)는 여기로 = 기본 게이트웨이
$ cat /proc/sys/net/ipv4/ip_forward                # 1이면 이 서버가 다른 인터페이스로 패킷을 넘겨줌
0
$ sysctl -w net.ipv4.ip_forward=1                  # 켜기 (영구: /etc/sysctl.conf)
용어하는 일비유
스위치같은 네트워크(예: 192.168.1.0/24) 안의 기계끼리 연결한 층의 복도
라우터서로 다른 네트워크를 연결. 네트워크마다 IP를 하나씩 가짐층과 층을 잇는 계단
게이트웨이“그 네트워크로 가려면 여기로 보내라”의 그 문 (보통 라우터 IP)계단 입구
/24앞 24비트가 네트워크 주소 → 192.168.1.0 ~ 192.168.1.255가 한 네트워크같은 층 번호
ip_forward가 왜 중요하냐면 리눅스는 기본적으로 받은 패킷을 다른 인터페이스로 넘겨주지 않아요(보안). 그런데 쿠버네티스 노드는 Pod에서 나온 패킷을 밖으로 넘겨줘야 해서 ip_forward=1이 꼭 필요해요. kubeadm 설치 준비(12부 04)에 이 설정이 들어 있는 이유예요.

02. DNS

IP 대신 db · www.google.com 같은 이름으로 접속하려면 이름을 IP로 바꿔 주는 장치가 필요해요. 처음엔 각 서버의 파일로, 서버가 많아지면 DNS 서버로 해결해요.

/etc/hosts → DNS 서버 → search 도메인
$ cat /etc/hosts                     # 이 서버만의 이름표
192.168.1.11   db
$ cat /etc/resolv.conf               # 물어볼 DNS 서버 · 붙여 볼 도메인
nameserver 192.168.1.100
search mycompany.com
$ grep hosts /etc/nsswitch.conf      # 먼저 볼 곳: files(/etc/hosts) → dns
hosts:          files dns
$ nslookup web                       # DNS에 물어보기 (/etc/hosts는 안 봄)
$ dig web.mycompany.com              # 더 자세히
레코드뜻예
A이름 → IPv4web.mycompany.com → 192.168.1.10
AAAA이름 → IPv6web → 2001:db8::10
CNAME이름 → 다른 이름food.web-server → eat.web-server
쿠버네티스에서 다시 만나는 search Pod의 /etc/resolv.conf에는 search default.svc.cluster.local svc.cluster.local cluster.local이 들어 있어요. 그래서 2부 04에서 db라고만 써도 같은 Namespace의 Service가 찾아졌던 거예요(11부 05).

03. 네트워크 네임스페이스

입문 1부 03에서 컨테이너는 네임스페이스로 “보이는 범위”를 나눈다고 했어요. 네트워크 네임스페이스는 인터페이스 · IP · 라우팅 표 · ARP 표를 따로 가진 작은 방이에요. 컨테이너(Pod)마다 이 방이 하나씩 있어요. 직접 만들어 보면 Pod 네트워킹이 그대로 보여요.

방(namespace) 만들기 → 랜선(veth)으로 잇기 → 공유기(bridge)에 꽂기 → 밖으로(NAT)
# 1) 방 만들기
$ ip netns add red ; ip netns add blue
$ ip netns exec red ip link            # (= ip -n red link) 방 안에는 lo만 보임

# 2) 방이 많으면 브리지(가상 스위치) 하나에 모으기
$ ip link add v-net-0 type bridge ; ip link set v-net-0 up
$ ip addr add 192.168.15.5/24 dev v-net-0          # 호스트도 브리지에 IP 하나

# 3) 방마다 veth(가상 랜선) 쌍 — 한쪽은 방 안, 한쪽은 브리지에
$ ip link add veth-red type veth peer name veth-red-br
$ ip link set veth-red netns red
$ ip link set veth-red-br master v-net-0 ; ip link set veth-red-br up
$ ip -n red addr add 192.168.15.1/24 dev veth-red ; ip -n red link set veth-red up

# 4) 밖으로: 방의 기본 게이트웨이 = 브리지, 호스트에서 주소 바꿔 내보내기(NAT)
$ ip -n red route add default via 192.168.15.5
$ iptables -t nat -A POSTROUTING -s 192.168.15.0/24 -j MASQUERADE
리눅스 부품정체쿠버네티스에서는
network namespace네트워크가 분리된 방Pod 하나 (Pod 안 컨테이너들은 같은 방을 써서 localhost 공유)
veth pair양 끝이 이어진 가상 랜선Pod 방 ↔ 노드 연결
bridge가상 스위치노드 안 Pod들을 한 네트워크로 (예: cni0)
iptables NAT주소 바꾸기Pod → 밖으로 나갈 때 · Service 규칙(1부 07)

04. Docker 네트워킹

03에서 손으로 한 일을 Docker는 docker run 한 번에 자동으로 해요. 컨테이너마다 네트워크 네임스페이스를 만들고, veth로 docker0 브리지에 꽂고, IP를 주고, NAT를 걸어요.

none · host · bridge — 그리고 -p 8080:80
$ docker run --network none nginx      # 네트워크 없음
$ docker run --network host nginx      # 호스트 네트워크 그대로 (포트 충돌 주의)
$ docker run -p 8080:80 nginx          # 기본 bridge + 호스트 8080 → 컨테이너 80
$ ip addr show docker0
docker0: ... inet 172.17.0.1/16
$ iptables -t nat -L DOCKER -n | grep 8080
DNAT  tcp  --  0.0.0.0/0  0.0.0.0/0  tcp dpt:8080 to:172.17.0.2:80

05. CNI란 무엇인가

03 · 04에서 본 “네임스페이스 만들고 → veth 연결하고 → 브리지에 꽂고 → IP 주고 → NAT”는 Docker · rkt · containerd 누가 하든 같아요. 그래서 이 과정을 플러그인 하나로 떼어 내고, “런타임은 이렇게 부르고, 플러그인은 이렇게 답한다”는 약속을 정한 게 CNI(Container Network Interface)예요. 1부 02의 CRI, 9부 02의 CSI와 같은 생각이에요.

런타임이 네임스페이스를 만들고 → CNI 플러그인 ADD → IP를 돌려받는다
$ ls /opt/cni/bin                     # 플러그인 실행 파일
bridge  host-local  loopback  portmap  calico  …
$ ls /etc/cni/net.d                   # 어떤 플러그인을 어떤 설정으로 쓸지 (이름순으로 첫 파일)
10-calico.conflist
/etc/cni/net.d/10-bridge.conf — 학습 자료의 bridge 플러그인 예 (# 설명은 이해용, 실제 JSON에는 주석을 쓸 수 없어요)
{
  "cniVersion": "1.0.0",
  "name": "mynet",
  "type": "bridge",              # /opt/cni/bin/bridge 를 부른다
  "bridge": "cni0",
  "isGateway": true,
  "ipMasq": true,                # 밖으로 나갈 때 NAT
  "ipam": {
    "type": "host-local",        # IP 나눠 주는 플러그인 (IPAM, 11부 03)
    "subnet": "10.244.1.0/24",
    "routes": [{ "dst": "0.0.0.0/0" }]
  }
}
CNI가 정한 것내용
런타임의 일네트워크 네임스페이스 만들기 · 설정 파일 읽기 · 플러그인 실행
플러그인 명령ADD(연결) · DEL(정리) · CHECK · VERSION
플러그인의 일인터페이스 만들고 연결 · IP 할당 · 라우트 설정 · 결과(JSON)로 IP 돌려주기
학습 자료의 “Docker는 CNI를 안 쓴다” Docker는 CNM이라는 자기 방식을 써서, 예전 쿠버네티스는 Docker 컨테이너를 --network=none으로 띄운 뒤 CNI를 따로 불렀어요. 지금은 노드에 Docker 대신 containerd가 있고(1부 02), containerd가 CNI를 직접 불러요. 노드에 CNI 플러그인을 설치하는 게 클러스터 설치의 필수 단계인 이유예요(12부 04).

1. 10부 셀프 체크

  1. 192.168.1.10이 192.168.2.10과 통신하려면 라우팅 표에 무엇이 필요하나요?
    192.168.2.0/24 via <라우터의 1번 네트워크 쪽 IP> 같은 경로, 또는 기본 게이트웨이(default via …).
  2. 리눅스 서버가 한 인터페이스로 받은 패킷을 다른 인터페이스로 넘겨주게 하려면?
    /proc/sys/net/ipv4/ip_forward를 1로 (sysctl -w net.ipv4.ip_forward=1).
  3. /etc/hosts와 DNS 중 무엇을 먼저 보는지는 어디서 정하나요?
    /etc/nsswitch.conf의 hosts: 줄 (보통 files dns).
  4. resolv.conf의 search는 무엇을 하나요?
    짧은 이름 뒤에 도메인을 붙여서 찾아 줘요. search mycompany.com이면 web을 web.mycompany.com으로도 찾아요.
  5. 네트워크 네임스페이스 두 개를 직접 연결하는 가상 장치는?
    veth pair(양 끝이 이어진 가상 랜선). 여럿이면 bridge(가상 스위치)에 모아요.
  6. 네임스페이스 안에서 밖의 네트워크로 나가려면 필요한 두 가지는?
    방의 기본 게이트웨이를 브리지 주소로 두는 경로, 그리고 호스트에서 출발지 주소를 바꾸는 NAT(MASQUERADE).
  7. docker run -p 8080:80은 내부적으로 무엇을 만드나요?
    호스트 8080으로 온 패킷을 컨테이너 IP의 80으로 보내는 iptables DNAT 규칙.
  8. CNI 플러그인 실행 파일과 설정 파일은 각각 어디에 있나요?
    실행 파일 /opt/cni/bin, 설정 /etc/cni/net.d.
  9. CNI에서 런타임과 플러그인의 역할 분담은?
    런타임은 네임스페이스를 만들고 플러그인을 ADD · DEL로 불러요. 플러그인은 인터페이스 연결 · IP 할당 · 라우트 설정을 하고 결과를 돌려줘요.
11부쿠버네티스 네트워킹포트Pod 네트워킹CNI · IPAMService 네트워킹CoreDNSIngressGateway API

01. 클러스터 네트워킹

10부의 리눅스 부품들이 클러스터 안에서 어떻게 조합되는지 봐요. 쿠버네티스 네트워크는 층이 세 개예요. 노드끼리(서버 네트워크), Pod끼리(CNI가 만드는 네트워크), Service(kube-proxy가 만드는 가상 주소). 먼저 맨 아래, 노드와 부품들이 쓰는 포트부터예요.

1. 노드와 컴포넌트가 쓰는 포트

누가 어느 포트로 — 방화벽에 열어 둘 문 목록
포트부품누가 접속하나
6443kube-apiserverkubectl · 모든 부품 · kubelet
2379 · 2380etcd (클라이언트 · 피어)API 서버 · 다른 etcd
10250kubelet (모든 노드)API 서버 (logs · exec · metrics)
10259kube-scheduler자기 노드 (상태 확인)
10257kube-controller-manager자기 노드 (상태 확인)
30000-32767NodePort Service (2부 03)클러스터 밖 사용자
학습 자료의 10251 · 10252 예전 스케줄러 · 컨트롤러 매니저는 암호화 없는 10251 · 10252 포트도 썼어요. 지금은 HTTPS인 10259 · 10257만 써요. CNI에 따라 추가 포트도 필요해요(예: Calico BGP 179, VXLAN 4789 UDP).

2. 실습 환경 살펴보기

학습 자료의 “Explore Environment” 실습에서 자주 묻는 것들이에요. 노드의 IP · 인터페이스 · 기본 경로, 부품이 듣는 포트를 직접 찾아봐요.

$ kubectl get nodes -o wide                       # INTERNAL-IP = 노드 IP
NAME           STATUS   ROLES           INTERNAL-IP
controlplane   Ready    control-plane   192.168.0.10
node01         Ready    <none>          192.168.0.11
$ ip a | grep -B2 192.168.0.10                    # 그 IP가 붙은 인터페이스 (예: eth0)
$ ip link show eth0                               # MAC 주소 · 상태
$ ip route show default                           # 기본 게이트웨이
default via 192.168.0.1 dev eth0
$ ss -tlnp | grep -E "kube|etcd"                  # 부품이 듣는 포트 (netstat -nplt도 같음)
LISTEN  0  4096  *:6443   *:*  users:(("kube-apiserver",…))
LISTEN  0  4096  127.0.0.1:2379 …  users:(("etcd",…))

02. Pod 네트워킹

1. 쿠버네티스가 요구하는 네트워크 모델

쿠버네티스는 Pod 네트워크를 직접 만들지 않고 규칙만 정해요. 그 규칙을 지키는 네트워크를 만드는 건 CNI 플러그인(03)의 몫이에요.

규칙뜻
모든 Pod는 자기 IP를 가진다Pod 안의 컨테이너끼리는 그 IP를 공유하고 localhost로 통신 (10부 03의 방 하나)
모든 Pod는 다른 모든 Pod와 NAT 없이 통신한다노드가 달라도 Pod IP 그대로 닿아야 함
노드의 에이전트(kubelet 등)는 그 노드의 Pod와 통신한다probe · logs

2. 노드 사이의 Pod 통신

노드마다 Pod 대역 하나 — 같은 노드는 브리지로, 다른 노드는 경로 또는 터널로
$ kubectl get node node01 -o jsonpath='{.spec.podCIDR}'
10.244.1.0/24
$ kubectl get pods -o wide                        # IP 열이 노드별 대역 안에 있음
web-a   Running   10.244.1.5   node01
web-b   Running   10.244.2.7   node02
$ ip route | grep 10.244                          # node01에서: 다른 노드 대역으로 가는 길 (CNI가 넣음)
10.244.2.0/24 via 192.168.0.12 dev eth0

03. CNI 플러그인과 IPAM

1. kubelet이 CNI를 부르는 순서

10부 05에서 본 흐름이 그대로예요. 스케줄러가 Pod를 노드에 배정 → kubelet이 CRI로 containerd에 Pod 생성을 요청 → containerd가 네트워크 네임스페이스를 만들고 /etc/cni/net.d 설정대로 CNI 플러그인을 ADD → Pod IP가 정해져 kubelet이 API 서버에 보고.

학습 자료의 kubelet 옵션 --network-plugin=cni 예전에는 kubelet 실행 옵션에 --network-plugin=cni · --cni-conf-dir · --cni-bin-dir가 있었어요. 1.24에서 dockershim과 함께 빠졌고, 지금은 containerd 설정(/etc/containerd/config.toml)에서 CNI 폴더를 정해요. 기본값은 그대로 /etc/cni/net.d · /opt/cni/bin이에요.

2. IPAM: Pod IP 나눠 주기

클러스터 대역 → 노드 대역 → Pod IP — 겹치지 않게 나눠 주기
대역정하는 곳예 (kubeadm)
노드 IP서버 네트워크192.168.0.0/24
Pod 대역kubeadm init --pod-network-cidr + CNI 설정10.244.0.0/16 (노드마다 /24)
Service 대역API 서버 --service-cluster-ip-range10.96.0.0/12
세 대역은 절대 겹치면 안 된다 Pod 대역이 회사 네트워크나 노드 대역과 겹치면, 노드는 그 주소가 Pod인지 다른 서버인지 구분하지 못해요. 설치 전에 정하고, 설치 후에는 바꾸기 매우 어려워요.

3. Weave(개발 중단)와 요즘 쓰는 CNI

학습 자료는 실습에 Weave Net을 써요. Weave를 만든 회사(Weaveworks)가 2024년에 문을 닫아 더 이상 개발 · 보안 패치가 없어요. 원리를 배우는 데는 그대로 유효하지만, 새 클러스터에는 아래 중에서 골라요.

CNIPod 간 전달NetworkPolicy특징
FlannelVXLAN 터널없음가장 단순. 실습 · 작은 클러스터
CalicoBGP 경로 또는 VXLAN있음널리 쓰임. 터널 없이 경로로 빠르게
CiliumeBPF (+ VXLAN · 직접 경로)있음 (L7까지)kube-proxy 대체, Hubble로 트래픽 관찰. 요즘 가장 활발
클라우드 CNI클라우드 네트워크 직접 (VPC IP)클라우드별GKE · EKS · AKS 기본
$ ls /etc/cni/net.d                                # 어떤 CNI가 깔렸나
10-calico.conflist
$ kubectl get pods -n kube-system | grep -E "calico|cilium|flannel|weave"
$ kubectl logs -n kube-system -l k8s-app=calico-node --tail=20

04. Service 네트워킹

1. Service IP는 실제로 어디에도 없다

Pod IP는 실제 네트워크 인터페이스에 붙어 있지만, Service의 ClusterIP는 어떤 인터페이스에도 붙어 있지 않아요. ping도 안 돼요. 그 주소로 가는 패킷을 모든 노드의 kube-proxy가 만든 규칙이 가로채서 Pod IP로 바꿔 줄 뿐이에요(1부 07).

Service 생성 → ClusterIP 할당 → 모든 노드에 규칙 → 패킷 DNAT

2. kube-proxy 모드와 규칙 읽기

$ kubectl get svc web-svc
NAME      TYPE        CLUSTER-IP     PORT(S)
web-svc   ClusterIP   10.96.120.15   80/TCP
$ grep service-cluster-ip-range /etc/kubernetes/manifests/kube-apiserver.yaml
    - --service-cluster-ip-range=10.96.0.0/12
$ iptables -t nat -L KUBE-SERVICES -n | grep web-svc
KUBE-SVC-W5RQ3PZ2LJ6YT4AB  tcp -- 0.0.0.0/0  10.96.120.15  /* default/web-svc */ tcp dpt:80
$ iptables -t nat -L KUBE-SVC-W5RQ3PZ2LJ6YT4AB -n
KUBE-SEP-AAA…  all -- 0.0.0.0/0  0.0.0.0/0  statistic mode random probability 0.50000000000
KUBE-SEP-BBB…  all -- 0.0.0.0/0  0.0.0.0/0
$ iptables -t nat -L KUBE-SEP-AAA… -n
DNAT  tcp -- 0.0.0.0/0  0.0.0.0/0  tcp to:10.244.1.5:8080
$ kubectl logs -n kube-system -l k8s-app=kube-proxy | grep -i "proxier"   # 어떤 모드인지
체인역할
KUBE-SERVICES“이 ClusterIP:포트면 → 그 Service 체인으로”
KUBE-SVC-…Pod가 N개면 확률로 하나를 고름 (1/N, 1/(N-1) …)
KUBE-SEP-…골라진 Pod IP:포트로 목적지 바꾸기(DNAT)
KUBE-NODEPORTSNodePort로 온 패킷 → 같은 Service 체인으로

05. 클러스터 DNS와 CoreDNS

1. Service와 Pod의 DNS 이름

입문 5부 03에서 Service 이름으로 접속했어요. 그 이름을 IP로 바꿔 주는 게 kube-system의 CoreDNS예요. 10부 02의 DNS 서버 역할을 클러스터 안에서 하는 거죠.

Pod → kube-dns(10.96.0.10) → CoreDNS → API를 지켜본 기록 · 바깥은 위로
대상이름답
Servicedb.prod.svc.cluster.local (같은 Namespace면 db)ClusterIP
headless Service (clusterIP: None)db.prod.svc.cluster.localPod IP들
StatefulSet Poddb-0.db.prod.svc.cluster.local그 Pod IP (9부 06)
Pod (설정 시)10-244-1-5.prod.pod.cluster.local그 Pod IP
$ kubectl exec -it web -- cat /etc/resolv.conf
nameserver 10.96.0.10                                 ← kube-dns Service
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
$ kubectl exec -it web -- nslookup db.prod
Name:    db.prod.svc.cluster.local
Address: 10.96.33.21

2. CoreDNS 설정 (Corefile)

$ kubectl get deploy,svc -n kube-system -l k8s-app=kube-dns
deployment.apps/coredns   2/2
service/kube-dns          ClusterIP   10.96.0.10   53/UDP,53/TCP,9153/TCP
$ kubectl get configmap coredns -n kube-system -o yaml
  Corefile: |
    .:53 {
        errors
        health
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {   # 클러스터 이름은 API를 보고 답함
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
        prometheus :9153
        forward . /etc/resolv.conf                       # 나머지는 노드의 DNS로
        cache 30
        loop
        reload
        loadbalance
    }
$ grep -A1 clusterDNS /var/lib/kubelet/config.yaml        # kubelet이 Pod에 넣어 주는 DNS 주소
clusterDNS:
- 10.96.0.10
ndots:5의 의미 이름에 점이 5개보다 적으면 search 도메인을 먼저 붙여 봐요. 그래서 Pod에서 www.google.com을 찾으면 www.google.com.default.svc.cluster.local 같은 실패 질문을 몇 번 거친 뒤에 진짜 이름을 물어요. 바깥 도메인을 많이 부르는 앱은 이름 끝에 점(www.google.com.)을 붙이면 빨라져요.

06. Ingress

LoadBalancer Service는 Service마다 외부 IP(그리고 비용)가 하나씩 생겨요. Ingress는 입구 하나에서 호스트 · 경로를 보고 여러 Service로 나눠 줘요(입문 5부 04). 입문에서는 GKE가 컨트롤러를 대신 만들어 줬지만, 직접 설치한 클러스터에서는 컨트롤러부터 설치해야 해요.

1. Ingress와 Ingress Controller

Ingress 규칙 + Ingress Controller — 입구 하나에서 호스트 · 경로로 나누기

2. 경로 · 호스트 규칙

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop
  namespace: shop
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /     # /wear → 뒤 Service에는 / 로 (컨트롤러 전용 옵션)
spec:
  ingressClassName: nginx            # 어느 컨트롤러가 처리할지
  tls:
  - hosts: [shop.example.com]
    secretName: shop-tls             # type: kubernetes.io/tls Secret
  defaultBackend:
    service: { name: not-found, port: { number: 80 } }
  rules:
  - host: shop.example.com
    http:
      paths:
      - path: /wear
        pathType: Prefix             # Prefix · Exact · ImplementationSpecific
        backend:
          service: { name: wear-svc, port: { number: 80 } }
      - path: /watch
        pathType: Prefix
        backend:
          service: { name: watch-svc, port: { number: 80 } }
$ kubectl create ingress shop -n shop --class=nginx \
    --rule="shop.example.com/wear*=wear-svc:80" --rule="shop.example.com/watch*=watch-svc:80"
$ kubectl get ingressclass
$ kubectl describe ingress shop -n shop          # Rules · Backends 확인

3. Ingress NGINX 은퇴

학습 자료의 Ingress NGINX 컨트롤러는 2026년 3월로 유지보수 종료 대부분의 학습 자료가 쓰는 커뮤니티 Ingress NGINX(kubernetes/ingress-nginx)는 2026년 3월로 유지보수가 끝났어요. 이미 돌고 있는 것은 계속 동작하지만 더 이상 보안 패치가 나오지 않아요. 할 일은 둘 중 하나예요. ① 다른 Ingress 컨트롤러(Traefik · HAProxy · F5 NGINX · 클라우드 컨트롤러 등)로 교체 — Ingress API 자체는 그대로 쓸 수 있어요. ② 다음 절의 Gateway API로 이전 — 쿠버네티스가 권장하는 방향이에요. 시험 · 실습에서 Ingress 문법은 여전히 나오니 알아 둬야 해요.

07. Gateway API

Ingress는 기능이 적어서(헤더 조건 · 트래픽 비율 나누기 등) 결국 컨트롤러마다 다른 어노테이션으로 기능을 붙였어요. 같은 YAML이 컨트롤러를 바꾸면 안 돌았죠. Gateway API는 이런 기능을 표준 필드로 넣고, 리소스를 역할별로 나눈 후속 표준이에요.

1. GatewayClass · Gateway · HTTPRoute

인프라 담당 · 클러스터 운영자 · 앱 개발자가 각자 자기 리소스를
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway                         # 운영자: 입구
metadata:
  name: shop-gw
  namespace: infra
spec:
  gatewayClassName: envoy             # 인프라 담당이 설치한 구현 (Envoy Gateway · Istio · Cilium · NGINX Gateway Fabric · GKE …)
  listeners:
  - name: https
    protocol: HTTPS
    port: 443
    hostname: shop.example.com
    tls:
      certificateRefs: [{ name: shop-tls }]
    allowedRoutes:
      namespaces: { from: All }       # 다른 Namespace의 Route도 붙을 수 있게
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute                       # 개발자: 경로
metadata:
  name: wear
  namespace: shop
spec:
  parentRefs:
  - { name: shop-gw, namespace: infra }
  rules:
  - matches:
    - path: { type: PathPrefix, value: /wear }
    backendRefs:
    - { name: wear-v1, port: 80, weight: 90 }   # 카나리: 90%
    - { name: wear-v2, port: 80, weight: 10 }   #          10%
  - matches:
    - headers: [{ name: x-beta, value: "true" }]
    backendRefs:
    - { name: wear-v2, port: 80 }

2. Ingress와 무엇이 다른가

IngressGateway API
리소스Ingress 하나에 입구 + 규칙GatewayClass · Gateway(입구) · HTTPRoute(규칙) 분리
역할한 사람이 다 씀인프라 · 운영 · 개발이 각자 자기 것
고급 기능컨트롤러별 어노테이션가중치 · 헤더 매칭 · 리다이렉트 등 표준 필드
프로토콜HTTP(S)HTTP · gRPC(GRPCRoute) · TLS · TCP · UDP
상태기능 추가 중단(유지만)v1 GA, 계속 발전 — 새로 만든다면 이쪽
$ kubectl get gatewayclass,gateway,httproute -A
$ kubectl describe httproute wear -n shop          # Parents: Accepted · ResolvedRefs 조건 확인
설치가 따로 필요하다 Gateway API의 CRD(15부)와 구현(컨트롤러)은 쿠버네티스에 기본으로 들어 있지 않아 따로 설치해요. 관리형 서비스(GKE 등)는 켜기만 하면 돼요. 기존 Ingress를 옮길 때는 ingress2gateway 변환 도구를 쓸 수 있어요.

3. 11부 셀프 체크

  1. API 서버 · etcd · kubelet이 쓰는 포트는?
    API 서버 6443, etcd 2379(클라이언트) · 2380(피어), kubelet 10250.
  2. 쿠버네티스 네트워크 모델의 핵심 규칙 두 가지는?
    모든 Pod는 자기 IP를 가진다, 모든 Pod는 다른 노드의 Pod와도 NAT 없이 통신한다.
  3. node01의 Pod가 node02의 Pod로 가는 방법 두 가지는?
    노드마다 상대 노드의 Pod 대역으로 가는 경로를 두거나(Calico BGP 등), 패킷을 노드 IP로 감싸는 터널(VXLAN)을 써요(Flannel 등).
  4. Pod 대역 · Service 대역 · 노드 대역은 각각 어디서 정하나요?
    Pod: --pod-network-cidr + CNI 설정, Service: API 서버 --service-cluster-ip-range, 노드: 서버 네트워크. 셋은 겹치면 안 돼요.
  5. 학습 자료의 Weave Net을 새 클러스터에 쓰지 않는 이유는?
    만든 회사가 문을 닫아 개발 · 보안 패치가 중단됐기 때문이에요. Calico · Cilium · Flannel 등을 써요.
  6. ClusterIP로 ping이 안 되는 이유는?
    ClusterIP는 어떤 인터페이스에도 붙어 있지 않은 가상 주소이고, kube-proxy 규칙이 특정 포트의 TCP · UDP 패킷만 Pod로 바꿔 주기 때문이에요.
  7. iptables 모드에서 Service가 Pod를 고르는 방법은?
    KUBE-SVC 체인이 확률(statistic random)로 KUBE-SEP 체인 하나를 고르고, 거기서 Pod IP로 DNAT해요.
  8. Pod의 /etc/resolv.conf에 있는 nameserver는 누구인가요?
    kube-dns Service(보통 10.96.0.10)이고, 뒤에 CoreDNS Pod들이 있어요. kubelet 설정의 clusterDNS 값이 들어가요.
  9. CoreDNS 설정은 어디에 있고, 클러스터 밖 이름은 어떻게 처리하나요?
    kube-system의 ConfigMap coredns의 Corefile. 클러스터 밖 이름은 forward . /etc/resolv.conf로 노드의 DNS에 넘겨요.
  10. Ingress를 만들었는데 아무 일도 안 일어나요. 무엇부터 보나요?
    Ingress Controller가 설치돼 있는지, ingressClassName이 그 컨트롤러를 가리키는지 확인해요.
  11. Ingress NGINX 유지보수 종료 후의 선택지 두 가지는?
    다른 Ingress 컨트롤러로 교체(Ingress API는 그대로), 또는 Gateway API로 이전.
  12. Gateway API에서 Gateway와 HTTPRoute는 각각 누가 관리하나요?
    Gateway(입구 · 포트 · 인증서)는 클러스터 운영자, HTTPRoute(경로 · 백엔드)는 앱 개발자. GatewayClass는 인프라 담당이 정해요.
스터디를 하며 부마다 이어서 정리합니다. 그림은 개념 이해를 위해 단순화했어요.