콘텐츠로 이동
Study NoteCKA

설정과 리소스

ConfigMap · Secret · requests/limits

설정을 이미지에서 분리하는 이유

섹션 제목: “설정을 이미지에서 분리하는 이유”
  • 같은 이미지를 dev / staging / prod에 그대로 쓸 수 있다
  • 설정만 바꿔서 재배포할 수 있다 — 이미지를 다시 빌드하지 않는다
  • 비밀 값이 이미지 레이어에 박히지 않는다
이미지 하나를 dev·staging·prod Pod 이 각자의 ConfigMap 과 Secret 으로 다르게 구성하는 구조

Kubernetes가 주는 두 가지 그릇 — ConfigMap(평범한 설정)과 Secret(민감한 값). 구조는 거의 같고, 취급만 다르다.

터미널 창
# 1) 값을 직접
kubectl create cm app-config \
--from-literal=LOG_LEVEL=debug \
--from-literal=MAX_CONN=100
# 2) 파일 하나 — 파일 이름이 키가 된다
kubectl create cm nginx-conf --from-file=./nginx.conf
# 3) 키 이름을 지정
kubectl create cm nginx-conf --from-file=custom.conf=./nginx.conf
# 4) 디렉터리 전체 — 각 파일이 키가 된다
kubectl create cm configs --from-file=./conf/
# 5) env 형식 파일 — 각 줄이 키=값
kubectl create cm app-env --from-env-file=./app.env
터미널 창
kubectl get cm app-config -o yaml
kubectl describe cm app-config
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "debug" # 짧은 값
MAX_CONN: "100"
nginx.conf: | # 파일 전체를 담을 수도 있다
server {
listen 80;
root /usr/share/nginx/html;
}
immutable: false # true면 수정 불가 (성능 이점 + 실수 방지)
  • 값은 전부 문자열이다. 100이라고 쓰면 YAML 파서가 숫자로 읽어 거부한다 → "100"
  • binaryData: 로 바이너리도 담을 수 있다 (base64)
  • 크기 제한 1MiB — etcd의 제약이다. 큰 파일은 볼륨을 쓰자
  • immutable: true 로 잠그면 수정 요청을 API 서버가 거부한다 — 실수 방지에 더해, kubelet이 변경 감시(watch)를 끊어 대규모 클러스터의 부하를 줄인다. 바꾸려면 새 ConfigMap을 만들어 참조를 교체한다. 깊게 팔 필요는 없다 — 이름과 효과만 알면 된다

주입 방식 세 가지 — 전체 지도

섹션 제목: “주입 방식 세 가지 — 전체 지도”
ConfigMap·Secret 을 env valueFrom · envFrom · volume 세 방식으로 주입할 때 환경변수는 자동 갱신이 안 되고 파일은 되는 차이
containers:
- name: app
image: myapp:1.0
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
optional: true # 없어도 Pod이 뜬다 (기본 false)
  • 키 하나하나를 원하는 이름으로 매핑할 수 있다
  • optional: false(기본)인데 ConfigMap이나 키가 없으면 → Pod이 CreateContainerConfigError 로 멈춘다

주입 방식 2 — 전부 환경변수로

섹션 제목: “주입 방식 2 — 전부 환경변수로”
containers:
- name: app
image: myapp:1.0
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: db-secret
- configMapRef:
name: extra-config
prefix: EXTRA_ # 키 앞에 접두사를 붙인다
  • ConfigMap의 모든 키가 그대로 환경변수 이름이 된다
  • 그래서 키 이름이 환경변수로 유효해야 한다 (하이픈이 들어가면 조용히 건너뛴다)
  • env와 envFrom을 같이 쓰면 env가 이긴다
터미널 창
kubectl set env deploy/web --from=configmap/app-config
kubectl set env deploy/web --from=secret/db-secret
kubectl set env deploy/web LOG_LEVEL=info # 직접
kubectl set env deploy/web LOG_LEVEL- # 제거

주입 방식 3 — 볼륨으로 마운트

섹션 제목: “주입 방식 3 — 볼륨으로 마운트”
spec:
containers:
- name: app
image: myapp:1.0
volumeMounts:
- name: config
mountPath: /etc/app
readOnly: true
volumes:
- name: config
configMap:
name: app-config
defaultMode: 0644
items: # 일부 키만 고르고 이름도 바꾼다
- key: nginx.conf
path: nginx.conf
  • 키 하나 = 파일 하나. /etc/app/nginx.conf 로 보인다
  • items를 쓰면 명시한 키만 마운트된다

volumes·volumeMounts 짝은 손으로 치다 들여쓰기가 틀리기 쉽다 — 시험장에서는 공식 Configure a Pod to Use a ConfigMap에서 복사해 이름만 바꾸는 게 정석이다 (env·envFrom 예시도 같은 페이지에 있다).

volumeMounts:
- name: config
mountPath: /etc/nginx
volumeMounts 로 디렉터리 전체를 마운트하면 원래 내용이 전부 가려지지만 ConfigMap 수정은 자동 반영되는 관계

자동 갱신은 되지만 원래 있던 파일이 전부 가려진다.

자동 갱신 정리

방식ConfigMap을 바꾸면
환경변수 (env, envFrom)반영 안 됨. Pod을 다시 만들어야 한다
볼륨 마운트반영됨 (kubelet 동기화 주기, 대략 1분 이내)
볼륨 + subPath반영 안 됨

반영되더라도 앱이 파일을 다시 읽어야 의미가 있다. 그래서 실무 표준은 —

터미널 창
kubectl rollout restart deploy/web

Secret — ConfigMap과 무엇이 다른가

섹션 제목: “Secret — ConfigMap과 무엇이 다른가”

구조는 ConfigMap과 거의 같다 — 다른 것은 취급이다. 값이 base64로 담기고(data), 노드에 볼륨으로 풀릴 때 디스크가 아니라 메모리(tmpfs)에만 풀리며, 별도 리소스라서 RBAC으로 ConfigMap과 다른 권한을 걸 수 있다. 민감한 값을 ConfigMap에 넣으면 이 구분이 전부 사라진다 — 그래서 그릇을 나눈다.

apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
data: # base64로 인코딩된 값
password: czNjcjN0
stringData: # 평문으로 쓰면 API 서버가 인코딩해준다
username: admin
터미널 창
kubectl create secret generic db-secret \
--from-literal=username=admin \
--from-literal=password=s3cr3t
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com --docker-username=user --docker-password=pass
kubectl create secret tls web-tls --cert=./tls.crt --key=./tls.key
type쓰임
Opaque임의의 값 (기본)
kubernetes.io/dockerconfigjson프라이빗 레지스트리 인증
kubernetes.io/tlsTLS 인증서 (Ingress에서 쓴다)
kubernetes.io/service-account-tokenSA 토큰 (ServiceAccount)
터미널 창
kubectl get secret db-secret -o jsonpath='{.data.password}' | base64 -d
# s3cr3t

실제 보호는 base64가 아니라 다른 두 축에서 온다.

Secret 의 base64 는 보호가 아니고 RBAC 과 EncryptionConfiguration 이 실제 보호라는 갈래
  • 누구나 디코딩할 수 있다. 인코딩은 바이너리를 담기 위한 것이지 보호가 아니다
  • 읽을 수 있는 주체를 좁히는 것은 RBAC(Role-Based Access Control — 역할 기반 접근 제어, RBAC)의 몫이다
  • 기본 설정에서 etcd에 평문으로 저장된다
  • Secret type 목록과 YAML 형식이 헷갈리면 공식 Secrets 페이지를 연다 — data/stringData 예시가 다 있다

providers 목록은 순서에 의미가 있다 — 쓸 때는 첫 번째 provider로 암호화하고, 읽을 때는 위에서부터 차례로 복호화를 시도한다. identity는 “암호화 없음(평문)“이라는 뜻의 provider라, 목록 끝에 두면 아직 암호화되지 않은 기존 데이터도 읽을 수 있다.

# /etc/kubernetes/enc/enc.yaml → apiserver의 --encryption-provider-config 로 지정
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: ["secrets"]
providers:
- aescbc:
keys:
- name: key1
secret: <base64로 인코딩한 32바이트 키>
- identity: {}

설정 후 기존 Secret을 다시 암호화하려면 kubectl get secrets -A -o json | kubectl replace -f - 를 한 번 돌린다.

EncryptionConfiguration 파일을 직접 쓰는 문제까지 깊게 팔 필요는 없다 — 기본은 평문 저장이고, apiserver 플래그로 켠다는 사실 정도면 충분하다.

spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: registry.example.com/myapp:1.0

ServiceAccount에 붙여두면 그 SA를 쓰는 모든 Pod에 자동 적용된다.

터미널 창
kubectl patch sa default -p '{"imagePullSecrets":[{"name":"regcred"}]}'

컨테이너는 선언이 없으면 노드의 CPU·메모리를 얼마든지 쓸 수 있다. 그러면 두 가지가 무너진다 — 스케줄러는 Pod이 얼마나 쓸지 모르니 배치를 판단할 근거가 없고, 런타임은 한 컨테이너가 폭주해도 막을 수단이 없다. requests/limits는 그 두 판단을 위해 spec에 적어 두는 선언이다 — 아키텍처 아키텍처에서 스케줄러와 kubelet이 각자 읽어 간다.

resources:
requests: # 스케줄링의 기준. "최소 이만큼은 보장해달라"
cpu: 100m
memory: 128Mi
limits: # 런타임의 상한. "이 이상은 못 쓴다"
cpu: 500m
memory: 256Mi

둘은 서로 다른 컴포넌트가 서로 다른 시점에 본다.

requests 는 스케줄러가 배치 시점에 보고 limits 는 kubelet 이 실행 시점에 cgroup 으로 강제한다는 역할 구분
  • requests는 스케줄러가 본다. 노드의 남은 용량과 비교해 배치 가능 여부를 판단
  • limits는 kubelet/런타임이 강제한다. 실제 cgroup(control group — 리눅스 커널의 자원 제한 장치) 제한이 된다
  • 노드는 request의 합만 본다. 실제 사용량이 아니다

Kubernetes의 리소스 단위에서 CPU는 코어 수, 메모리는 바이트 수로 적는다.

표기뜻
1 CPU물리 코어 또는 가상 코어 1개
1000m1 CPU. m은 millicpu 또는 millicore다
500m0.5 CPU
1m0.001 CPU, 코어 하나의 0.1%에 해당하는 양
1Mi1 mebibyte = 1,048,576 bytes = 1024² bytes
1M1 megabyte = 1,000,000 bytes = 1000² bytes

CPU의 m은 millisecond가 아니다. requests/limits에서는 얼마나 많은 CPU 시간을 예약·제한할지를 나타내고, kubectl top에서는 최근 측정 구간의 평균 CPU 사용 속도를 같은 단위로 나타낸다.

평균 CPU 사용량 = 측정 구간에 소비한 CPU 시간 / 실제 경과 시간
15초 동안 CPU 시간 7.5초 소비
= 7.5 CPU초 / 15초
= 0.5 CPU
= 500m

측정 구간이 15초이든 30초이든 결과는 코어 수로 정규화된다. 따라서 500m은 누적해서 CPU를 500ms 사용했다는 뜻이 아니라, 그 구간에 평균적으로 코어 절반을 쓴 것과 같다는 뜻이다. 실제 구간은 Metrics API 응답의 window에 있으며, timestamp - window부터 timestamp까지를 뜻한다. 자세한 필드 정의는 Metrics API에서 확인한다.

요청이 없는 frontend Pod은 CPU가 1m처럼 작아질 수 있다. 그래도 probe, 이벤트 루프, 가비지 컬렉션 같은 백그라운드 작업 때문에 꼭 0m인 것은 아니다. 메모리는 시간당 소비량이 아니라 측정 시점의 working set(실제로 사용 중인 메모리 집합)이라, 접속이 없어도 프로세스가 떠 있는 만큼 유지될 수 있다.

CPU와 메모리 limit을 넘었을 때의 비대칭도 실무 감각의 핵심이다.

압축 가능한 CPU 는 limit 초과 시 throttling 으로 느려질 뿐이고 압축 불가한 메모리는 OOMKilled 로 죽는다는 대비

128Mi ≠ 128M 이라는 점과, CPU의 1m과 메모리의 1Mi는 전혀 다른 단위라는 점도 기억할 것.

requests를 얼마로 줄지는 노드가 내줄 수 있는 양에서 거꾸로 계산한다. 스케줄러가 비교하는 기준은 노드의 Allocatable이다. 노드 전체 용량(Capacity)에서 kubelet과 시스템 몫을 뺀 값이고, 여기서 이미 배치된 Pod의 requests 합을 뺀 만큼이 새 Pod에 줄 수 있는 양이다.

터미널 창
kubectl get node node01 -o jsonpath='{.status.allocatable}{"\n"}'
kubectl describe node node01 | grep -A8 'Allocated resources' # 이미 잡힌 requests 합
Pod 하나의 requests ≤ (Allocatable − 다른 Pod의 requests 합) × (1 − 여유 비율) ÷ Pod 수

여유를 남기는 이유는 Allocatable을 전부 나눠 주면 그 노드에 새 Pod이나 DaemonSet Pod이 뜰 자리가 없어지기 때문이다.

컨테이너가 여럿이면 Pod의 requests는 단순 합이 아니다. 공식 규칙은 앱 컨테이너의 합과 가장 큰 init 컨테이너 중 큰 쪽을 Pod의 requests로 본다. init 컨테이너는 앱보다 먼저 실행되고 끝나므로 둘이 동시에 자원을 쓰지 않기 때문이다.

컨테이너 구성 (CPU requests)Pod의 CPU requests
앱 300m + 앱 200m500m
init 400m, 앱 300m400m
init 300m, 앱 300m300m

네이티브 사이드카는 initContainers에 적지만 계속 실행되므로 앱 컨테이너 쪽 합에 들어간다. 실제 수치로 나눠 보는 연습은 실전 과제에 있다.

QoS 클래스 — 누가 먼저 쫓겨나는가

섹션 제목: “QoS 클래스 — 누가 먼저 쫓겨나는가”

노드에 자원이 부족하면 kubelet이 Pod을 축출(evict)한다. 순서는 QoS(Quality of Service — 서비스 품질) 클래스가 정한다.

QoS는 지정하는 것이 아니라 계산된다.

requests·limits 유무와 일치 여부로 BestEffort·Guaranteed·Burstable QoS 클래스가 갈리고 축출 순서가 정해지는 결정 트리
클래스조건축출 순서
Guaranteed모든 컨테이너에 requests == limits (CPU·메모리 둘 다)가장 나중
Burstablerequests가 있지만 limits와 다르다중간
BestEffortrequests도 limits도 없다가장 먼저
터미널 창
kubectl get pod web -o jsonpath='{.status.qosClass}'

중요한 워크로드를 Guaranteed로 만들려면 requests와 limits를 똑같이 쓰면 된다.

requests/limits는 Pod 하나 단위의 선언이라, 안 쓴 Pod은 그냥 통과한다. 네임스페이스를 팀·환경 단위로 나눠 쓰는 클러스터에서 이걸 방치하면 한 네임스페이스가 클러스터 자원을 다 먹을 수 있다. 그래서 네임스페이스 단위로 거는 장치가 둘 있다 — 기본값·개별 상한은 LimitRange, 총량은 ResourceQuota. 둘 다 admission에서 동작한다 — 아키텍처의 “인증 → 인가 → admission → etcd” 중 마지막 관문으로, 요청이 저장되기 전에 값을 채우거나 거부하는 자리다 (admission 자체는 Admission).

LimitRange — 네임스페이스의 기본값과 상한

섹션 제목: “LimitRange — 네임스페이스의 기본값과 상한”
apiVersion: v1
kind: LimitRange
metadata:
name: defaults
namespace: dev
spec:
limits:
- type: Container
default: # limits를 안 쓰면 이 값이 들어간다
cpu: 500m
memory: 256Mi
defaultRequest: # requests를 안 쓰면 이 값
cpu: 100m
memory: 128Mi
max: # 이보다 크면 생성 거부
cpu: "2"
memory: 1Gi
min:
cpu: 50m
memory: 64Mi
  • admission 단계에서 동작한다 — Pod 생성 시 값을 채우거나 거부한다
  • 이미 떠 있는 Pod에는 소급 적용되지 않는다
  • type은 무엇을 한 단위로 볼지다 — Container는 컨테이너 하나씩, Pod은 Pod 안 컨테이너들의 합, PersistentVolumeClaim은 요청 용량(PV와 PVC). 기본값(default / defaultRequest)을 채워주는 것은 Container뿐이다

ResourceQuota — 네임스페이스 총량 제한

섹션 제목: “ResourceQuota — 네임스페이스 총량 제한”
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "20"
requests.storage: 100Gi
persistentvolumeclaims: "5"
services.loadbalancers: "2"
count/deployments.apps: "10"
터미널 창
kubectl get resourcequota -n dev
kubectl describe resourcequota dev-quota -n dev # Used / Hard 를 보여준다

requests.storage는 네임스페이스 안 PVC의 요청 용량 합을 센다. 볼륨 안에 실제로 쓴 파일 크기나 NFS 공유의 남은 공간을 재는 값이 아니다. StorageClass별로 따로 제한하려면 fast.storageclass.storage.k8s.io/requests.storage와 fast.storageclass.storage.k8s.io/persistentvolumeclaims처럼 클래스 이름을 앞에 붙인다. 정확한 quota 키는 공식 Resource Quotas에서 확인한다.

둘은 짝으로 쓴다. 왜 그런지는 순서대로 보면 명확하다.

LimitRange 가 requests 를 채워야 ResourceQuota 검사를 통과하고 LimitRange 가 없으면 거부되는 흐름
  • ConfigMap/Secret 주입은 셋: 개별 env / envFrom / 볼륨
  • 자동 갱신은 볼륨 마운트만. subPath는 안 된다. 실무 표준은 rollout restart
  • Secret의 base64는 보호가 아니다. 보호는 RBAC + etcd 암호화
  • CreateContainerConfigError = ConfigMap/Secret 이름·키 오타
  • requests는 스케줄링, limits는 런타임 강제. 노드는 request 합만 본다
  • CPU 1000m = 1코어, 메모리 1Mi = 1024² bytes. top의 CPU는 측정 구간의 평균 사용 속도
  • CPU 초과 = throttling, 메모리 초과 = OOMKilled(137)
  • QoS는 지정하는 게 아니라 계산된다. BestEffort가 가장 먼저 쫓겨난다
  • ResourceQuota + LimitRange는 짝으로 — 그래야 requests 없는 Pod이 안 막힌다
  • 스토리지 quota는 PVC 요청 용량과 개수를 센다. 실제 파일 사용량 제한은 스토리지 백엔드의 몫이다