쿠버네티스 학습 노트 목차

스토리지와 설정 — 데이터·설정·비밀

파드는 언제든 죽고 다른 노드에서 다시 뜬다. 그런데 데이터는 남아야 하고, 설정은 이미지와 분리돼야 한다.


1. 볼륨의 수명은 파드에 묶인다

spec:
  containers:
    - name: app
      volumeMounts:
        - name: cache
          mountPath: /tmp/cache
  volumes:
    - name: cache
      emptyDir: {}
  • emptyDir파드와 함께 생기고 함께 사라진다
    • 같은 파드 안 컨테이너끼리 파일을 주고받을 때
    • emptyDir: { medium: Memory } 로 tmpfs 도 가능하다
  • hostPath — 노드의 경로를 그대로 붙인다
    • 노드에 묶이고 보안 위험이 크다 → 운영에서는 피한다
  • configMap — 설정을 파일로 마운트
  • secret — 비밀을 파일로 마운트
  • projected — 여러 소스를 한 디렉터리에 합친다

컨테이너가 재시작돼도 emptyDir 는 남는다. 사라지는 것은 파드가 지워질 때다. 이 차이가 로그 사이드카 패턴이 동작하는 근거다.

2. PV·PVC — 저장소를 요청하는 구조

다이어그램 로딩 중…
  • PV (PersistentVolume) — 실제 저장 공간. 클러스터 수준 자원
  • PVC (PersistentVolumeClaim) — "이만큼, 이런 조건으로 달라" 는 요청. 네임스페이스 자원
  • StorageClass — PVC 가 오면 PV 를 자동으로 만들어 주는 규칙

애플리케이션은 PVC 만 안다 — 뒤에 어떤 스토리지가 있는지 몰라도 된다 이 분리가 이식성을 만든다

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: gp3
  resources:
    requests:
      storage: 20Gi

정적 프로비저닝과 동적 프로비저닝

  • 정적 — 관리자가 PV 를 미리 만들어 둔다

    • PVC 가 오면 조건이 맞는 PV 에 바인딩된다
    • 조건 — 용량 · 접근 모드 · StorageClass · 셀렉터
  • 동적 — PVC 가 오면 StorageClass 의 프로비저너가 PV 를 즉석에서 만든다

    • 클라우드에서는 이쪽이 기본이다
  • 기본 StorageClass 가 지정돼 있으면 storageClassName 을 생략해도 된다

    • kubectl get sc # (default) 표시가 있는지 확인

3. 접근 모드 — 가장 많이 오해하는 부분

  • ReadWriteOnce (RWO) — '하나의 노드' 에서 읽기·쓰기
    • 같은 노드의 여러 파드는 함께 쓸 수 있다
  • ReadOnlyMany (ROX) — 여러 노드에서 읽기만
  • ReadWriteMany (RWX) — 여러 노드에서 읽기·쓰기
  • ReadWriteOncePod (RWOP) '하나의 파드' 만 (더 엄격하다)

가장 흔한 오해 — "RWO 는 파드 하나만 쓸 수 있다" 아니다. '노드' 단위다. 같은 노드에 있으면 여러 파드가 쓴다 진짜 파드 하나로 제한하려면 RWOP 를 쓴다

두 번째 오해 — "RWX 를 쓰면 되지" 블록 스토리지(EBS · GCE PD)는 RWX 를 지원하지 않는다 파일 스토리지(NFS · EFS · CephFS)가 필요하다

  • 그래서 Deployment 의 replicas 를 늘리면

    • RWO 볼륨을 쓰는 파드들이 다른 노드에 배치돼 Pending 이 된다
  • 증상 — "replicas 를 2로 올렸더니 하나가 Pending"

    • Events: Multi-Attach error for volume ...
  • 원인 — RWO 볼륨을 다른 노드의 파드가 붙이려 한다

  • 대응 — StatefulSet + volumeClaimTemplates (파드마다 전용 볼륨)

    • 또는 RWX 지원 스토리지

4. 반환 정책과 바인딩 모드

persistentVolumeReclaimPolicy
  Retain   PVC 를 지워도 PV 와 데이터가 남는다 (수동 정리)
  Delete   PVC 를 지우면 PV 와 실제 저장소까지 지운다  ← 동적 기본값
  Recycle  폐기됨

운영 데이터에 Delete 를 쓰면 PVC 실수 삭제가 곧 데이터 손실이다 중요한 것은 Retain 으로 두고, 정리 절차를 별도로 만든다

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: gp3
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
volumeBindingMode
  Immediate               PVC 생성 즉시 PV 를 만든다
                          → 볼륨이 A영역에 생겼는데 파드가 B영역으로 스케줄되면
                            영원히 못 붙는다
  WaitForFirstConsumer    파드가 스케줄될 때까지 기다렸다가 만든다
                          → 파드가 갈 영역에 맞춰 볼륨이 생긴다
                          다중 영역 클러스터에서는 사실상 필수다

allowVolumeExpansion: true 면 PVC 의 용량을 늘릴 수 있다 (줄이는 것은 불가)

5. ConfigMap — 설정을 이미지에서 분리한다

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"
  application.yml: |
    server:
      port: 8080
# ① 환경변수로
envFrom:
  - configMapRef: { name: app-config }

# ② 파일로 마운트
volumeMounts:
  - name: config
    mountPath: /etc/app
volumes:
  - name: config
    configMap: { name: app-config }

갱신 반영 방식이 다르다

  • 환경변수로 넣은 것파드를 다시 만들어야 반영된다 (절대 안 바뀐다)

  • 파일로 마운트한 것kubelet 이 주기적으로 갱신한다 (수십 초~1분 정도)

    • 단 애플리케이션이 파일을 다시 읽어야 의미가 있다
  • subPath 로 마운트한 것 — 갱신되지 않는다 ← 자주 놓치는 예외

  • 실무 기본형 — 설정을 바꾸면 파드를 새로 띄운다

    • kubectl rollout restart deploy/web
  • 또는 ConfigMap 이름에 해시를 붙여 바뀔 때마다 새 이름을 쓴다

    • (Kustomize 의 configMapGenerator 가 이것을 해 준다)
    • 롤아웃이 자동으로 일어나고 롤백도 함께 된다

6. Secret — 이름만 비밀이다

apiVersion: v1
kind: Secret
metadata:
  name: db-cred
type: Opaque
stringData:
  username: admin
  password: s3cr3t

Secret 은 기본적으로 base64 인코딩일 뿐 암호화가 아니다

  • kubectl get secret db-cred -o jsonpath='{.data.password}' | base64 -d

etcd 에도 평문으로 저장된다 — 별도로 켜야 암호화된다 EncryptionConfiguration 으로 저장 시 암호화(at rest) KMS 프로바이더를 쓰면 키를 외부에서 관리한다

즉 "Secret 에 넣었으니 안전하다" 는 틀렸다 안전하게 만들려면

  • ① etcd 저장 시 암호화를 켠다
  • RBAC 으로 secret 읽기 권한을 좁힌다
  • ③ 외부 시크릿 매니저와 연동한다 (External Secrets · CSI Secret Store)
환경변수 vs 파일 마운트
  환경변수   프로세스 목록·크래시 덤프·로그에 새기 쉽다
             자식 프로세스에 그대로 상속된다
  파일       권한을 줄 수 있고 갱신이 반영된다

비밀은 가능하면 파일로 마운트한다

7. RBAC — 누가 무엇을 할 수 있나

  • Role네임스페이스 안의 권한
  • ClusterRole — 클러스터 전체 권한 (노드·PV 같은 비네임스페이스 자원 포함)
  • RoleBinding — Role 또는 ClusterRole 을 특정 네임스페이스에 묶는다
  • ClusterRoleBinding 클러스터 전체에 묶는다
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: prod
  name: pod-reader
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: prod
  name: read-pods
subjects:
  - kind: ServiceAccount
    name: monitoring
    namespace: prod
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

알아 둘 것

  • 허용만 있다. 거부 규칙이 없다 (기본이 거부이므로)
  • ClusterRole 을 RoleBinding 으로 묶으면 그 네임스페이스에만 적용된다
    • 공통 Role 을 한 번 정의해 여러 네임스페이스에 재사용하는 관용구다
  • 권한을 확인하는 가장 빠른 방법
    • kubectl auth can-i delete pods --as=system:serviceaccount:prod:monitoring

서비스 어카운트

파드서비스 어카운트로 API 서버에 인증한다 지정하지 않으면 그 네임스페이스의 default 를 쓴다

주의 — default 에 권한을 주면 그 네임스페이스의 모든 파드가 갖게 된다

  • 워크로드마다 전용 SA 를 만드는 것이 원칙이다

API 를 안 쓰는 파드는 토큰 마운트를 꺼 둔다

  • automountServiceAccountToken: false
  • 침투 시 API 서버로 향하는 경로 하나가 사라진다

8. 파드 보안

spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    fsGroup: 10001
    seccompProfile: { type: RuntimeDefault }
  containers:
    - name: app
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]

runAsNonRoot: true 는 이미지가 숫자 UID 를 지정했는지 확인한다

  • USER appuser 처럼 이름만 있으면 판정할 수 없어 기동이 거부된다

  • 이미지 쪽에서 USER 10001:10001 로 쓰는 이유다

  • fsGroup — 마운트된 볼륨의 그룹 소유권을 바꿔 준다

    • 비루트 파드가 볼륨에 쓸 수 있게 하는 표준 방법이다

Pod Security Admission

네임스페이스 라벨로 정책 수준을 정한다

privileged제한 없음
baseline알려진 권한 상승을 막는다 (최소 방어)
restricted강하게 제한 (비루트 · capability 제거 · seccomp 등)
  • kubectl label ns prod \

    • pod-security.kubernetes.io/enforce=restricted \
    • pod-security.kubernetes.io/warn=restricted
  • enforce(차단) · audit(감사 로그) · warn(경고) 세 모드가 있다

  • warn 부터 켜서 무엇이 걸리는지 본 뒤 enforce 로 올리는 순서가 안전하다

1.36 에서 User Namespaces 가 GA 됐다 hostUsers: false 를 주면 컨테이너root 가 호스트의 비특권 UID 로 매핑된다

  • 컨테이너 root 문제의 근본 해결에 가까워졌다 호스트 네트워크·PID·IPC 를 쓰지 않는 워크로드부터 적용할 수 있다

한눈에 정리

  • 볼륨 — 수명이 파드에 묶인다. emptyDir 는 컨테이너 재시작에는 살아남는다
  • PV/PVC/SC — 앱은 PVC 만 안다. StorageClass 가 PV 를 자동 생성
  • 접근 모드 — RWO 는 '노드' 단위다 (파드 단위는 RWOP)
    • 블록 스토리지는 RWX 를 지원하지 않는다 → Multi-Attach 오류
  • 반환 정책 — 동적 기본이 Delete. 운영 데이터는 Retain
  • 바인딩 모드 — WaitForFirstConsumer — 다중 영역에서는 사실상 필수
  • ConfigMap — 환경변수는 재생성해야 반영. 파일은 갱신되나 subPath 는 예외
    • rollout restart 또는 이름 해시가 실무 기본형
  • Secret — base64 일 뿐이다. etcd 암호화·RBAC·외부 매니저가 있어야 안전
  • RBAC — 허용만 있다. auth can-i 로 확인. default SA 에 권한 주지 않기
  • 보안 — runAsNonRoot 는 숫자 UID 를 요구한다 · PSA 는 warn → enforce 순

출처 — Kubernetes Docs: "Volumes" · "Persistent Volumes" · "Storage Classes" · "ConfigMaps" · "Secrets" · "RBAC Authorization" · "Pod Security Standards" · "Security Context" · CKA Curriculum v1.35 (Storage 10%)

네트워킹 — 서비스부터 Gateway API까지트러블슈팅 — 증상에서 원인으로