스토리지와 설정 — 데이터·설정·비밀
파드는 언제든 죽고 다른 노드에서 다시 뜬다. 그런데 데이터는 남아야 하고, 설정은 이미지와 분리돼야 한다.
1. 볼륨의 수명은 파드에 묶인다
spec:
containers:
- name: app
volumeMounts:
- name: cache
mountPath: /tmp/cache
volumes:
- name: cache
emptyDir: {}
- emptyDir — 파드와 함께 생기고 함께 사라진다
- 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 를 늘리면
-
증상 — "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%)