LabHub

CKAD — 쿠버네티스 애플리케이션 개발자 · 애플리케이션 환경·설정·보안 · 이론

설정을 이미지 밖으로 빼면 무엇이 달라지나

LabHub 에서 이어서 보기

한 줄 요약

ConfigMap·Secret 은 "설정 저장소"가 아니라 같은 이미지를 여러 환경에 재사용하기 위한 주입 지점이고, securityContext·resources 는 "옵션"이 아니라 그 컨테이너가 노드에서 무엇을 할 수 있고 얼마를 쓸 수 있는지의 계약서다.

왜 이게 필요했나

설정을 이미지에 구워 넣으면 개발·스테이징·운영마다 다른 이미지를 만들어야 한다. 그러면 "스테이징에서 통과한 이미지"와 "운영에 올라간 이미지"가 서로 다른 물건이 되어 테스트의 의미가 사라진다. 그래서 불변 아티팩트(이미지) + 가변 설정(ConfigMap/Secret) 으로 쪼갠다.

주입하는 방법은 두 갈래다. 환경변수는 프로세스 시작 시 한 번 읽히고 그 뒤로는 절대 바뀌지 않는다. 볼륨 마운트는 파일로 들어오고, ConfigMap 을 수정하면 kubelet 이 파일 내용을 갱신해 준다(단 subPath 로 마운트한 항목은 갱신되지 않는다). 앱이 설정을 다시 읽을 수 있으면 볼륨이, 시작 시 한 번만 읽으면 환경변수가 편하다.

resources 는 두 얼굴을 가진다. requests스케줄러가 보는 값이다. 노드에 그만큼의 여유가 있어야 파드를 얹는다. limitskubelet/런타임이 강제하는 값이다. CPU 는 스로틀링되고, 메모리는 넘으면 OOMKilled 된다. 이 둘의 관계가 QoS 클래스를 정하고, 노드가 압박을 받을 때 누가 먼저 쫓겨나는지를 정한다.

어떻게 동작하나

설정 주입의 선택지를 표로 정리하면 이렇다.

| 방법 | 필드 | 특징 |
| --- | --- | --- |
| 키 하나만 환경변수로 | env[].valueFrom.configMapKeyRef | 이름을 바꿔 넣을 수 있다 |
| 전부 환경변수로 | envFrom[].configMapRef | 키 이름이 그대로 변수명이 된다 |
| 파일로 | volumes[].configMap + volumeMounts | 갱신이 반영된다, defaultMode 로 권한 지정 |
| 파일 하나만 기존 디렉터리에 | volumeMounts[].subPath | 디렉터리를 덮지 않는다, 갱신 안 됨 |

optional: true 를 주면 참조 대상이 없어도 파드가 뜬다. 없으면 기본값으로 도는 앱에 쓴다. immutable: true 를 준 ConfigMap/Secret 은 수정할 수 없는 대신 kubelet 이 변경 감시를 하지 않아 대규모 클러스터에서 API 서버 부하가 줄어든다.

Secret 은 암호화가 아니라 base64 인코딩일 뿐이다. etcd 에 평문에 가깝게 들어간다. 실제 보호는 세 가지로 한다. RBAC 로 읽을 수 있는 주체를 좁히고, apiserver 에 --encryption-provider-config 를 걸어 저장 시 암호화하고, 파드에 필요한 것만 마운트한다.

securityContext 는 파드 수준과 컨테이너 수준이 따로 있다. 파드 수준(spec.securityContext)의 runAsUser·runAsNonRoot·fsGroup 은 모든 컨테이너에 기본으로 적용되고, 컨테이너 수준(spec.containers[].securityContext)의 capabilities·readOnlyRootFilesystem·allowPrivilegeEscalation 은 그 컨테이너에만 적용되며 파드 값을 덮어쓴다. fsGroup 이 파드 수준에만 있는 이유는 볼륨이 파드 단위 자원이기 때문이다.

QoS 는 자동으로 결정되는 읽기 전용 값이다.

LimitRange 와 ResourceQuota 는 짝이다. LimitRange 는 개별 컨테이너의 기본값·최솟값·최댓값을 정하고, ResourceQuota 는 네임스페이스 전체 합계를 제한한다. 쿼터가 걸린 네임스페이스에서는 requests/limits 를 명시하지 않은 파드가 아예 거부되는데, LimitRange 가 기본값을 채워 주면 통과한다. 그래서 둘을 같이 건다.

현장에서 만나는 모습

홈랩 NAS 를 쿠버네티스 볼륨으로 붙일 때 겪은 일이다. NFS 마운트를 확인하려고 privileged: true 파드를 띄웠는데 이렇게 실패했다.

mount.nfs: Operation not permitted for 10.0.0.109:/volume1/k8s on /mnt/tmount: permission denied (are you root?)

권한을 최대로 줬는데도 막혔다. 원인은 hostNetwork: true 가 빠진 것이었다. NFS 마운트는 rpcbind 와 통신하며 1024 미만의 예약 포트를 소스 포트로 쓰는데, 파드 네트워크 네임스페이스 안에서는 그 동작이 제약을 받는다. privileged 는 capability 를 주지만 네트워크 네임스페이스 문제는 해결하지 못한다. securityContext 로 풀리는 문제와 파드 스펙의 다른 필드로 풀리는 문제가 다르다는 걸 값비싸게 배운 사례다.

같은 클러스터의 GPU 쪽에서는 반대 방향의 교훈이 있었다. 파드에 resources.limits: {nvidia.com/gpu: 1} 한 줄을 적은 것만으로 스케줄러가 GPU 있는 노드를 골랐고 런타임이 장치를 넣어 줬다. requests 가 스케줄링의 언어라는 게 그대로 보인다. 그런데 여기서 새 문제가 생겼다. 워커 넷의 GPU 가 각각 24GB(3090), 32GB(5090), 8GB(4070 Laptop) ×2 인데, 쿠버네티스 입장에서는 전부 "GPU 1개"다. 32GB 가 필요한 학습이 8GB 노트북 GPU 에 얹히는 일이 실제로 벌어졌다. 결국 gpu.homelab/tier=xlarge|large|small 같은 의미 기반 라벨을 직접 붙이고 nodeSelector 로 고르게 만들었다. 자원 요청은 양(quantity)만 표현하고 질(quality)은 표현하지 못한다는 한계를 라벨로 메운 것이다.

다음 실습에서 할 것

ckad-config 네임스페이스에서 ConfigMap 을 리터럴·파일로 만들고 envFrom·configMapKeyRef·볼륨 마운트·defaultMode·subPath·optional·immutable 을 전부 손으로 지정한다. 이어서 ckad-secure 네임스페이스에서 ServiceAccount 지정, 파드/컨테이너 수준 securityContext, resources, LimitRange, ResourceQuota 를 만들고 QoS 클래스가 실제로 무엇으로 잡히는지 확인한다.