콘텐츠로 이동

6. 설정과 리소스

ConfigMap · Secret · requests/limits

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

섹션 제목: “설정을 이미지에서 분리하는 이유”
  • 같은 이미지를 dev / staging / prod에 그대로 쓸 수 있다
  • 설정만 바꿔서 재배포할 수 있다 — 이미지를 다시 빌드하지 않는다
  • 비밀 값이 이미지 레이어에 박히지 않는다
flowchart LR
    IMG["이미지 myapp:1.0<br/>한 번만 빌드"]
    IMG --> D["dev Pod"] --> CD["ConfigMap dev"]
    IMG --> S["staging Pod"] --> CS["ConfigMap staging"]
    IMG --> P["prod Pod"] --> CP["ConfigMap prod<br/>+ Secret"]

    classDef img fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef cfg fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class IMG img
    class CD,CS,CP cfg
    class D,S,P mute

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

Terminal window
# 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
Terminal window
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의 제약이다. 큰 파일은 볼륨을 쓰자

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

섹션 제목: “주입 방식 세 가지 — 전체 지도”
flowchart LR
    CM["ConfigMap · Secret"]
    CM --> M1["① env + valueFrom<br/>키 하나를 원하는 이름으로"]
    CM --> M2["② envFrom<br/>모든 키를 환경변수로"]
    CM --> M3["③ volumes + volumeMounts<br/>키 하나 = 파일 하나"]
    M1 --> E["컨테이너 환경변수"]
    M2 --> E
    M3 --> F["컨테이너 파일시스템"]
    E -.->|"자동 갱신 안 됨 ❌"| R1["Pod 을 다시 만들어야"]
    F -.->|"자동 갱신 됨 ✅<br/>subPath 는 예외"| R2["kubelet 이 파일을 갱신"]

    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class CM key
    class R1 bad
    class R2 ok
    class M1,M2,M3,E,F mute
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의 모든 키가 그대로 환경변수 이름이 된다
  • 그래서 키 이름이 환경변수로 유효해야 한다 (하이픈이 들어가면 조용히 건너뛴다)
  • envenvFrom을 같이 쓰면 env가 이긴다
Terminal window
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를 쓰면 명시한 키만 마운트된다
volumeMounts:
- name: config
mountPath: /etc/nginx
flowchart LR
    B["/etc/nginx 원래 내용<br/>nginx.conf · mime.types · conf.d/"] -.->|"전부 가려진다 ❌"| A["/etc/nginx<br/>ConfigMap 의 키들만 보인다"]
    U["ConfigMap 수정"] -->|"자동 반영 ✅"| A

    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class B bad
    class U ok
    class A mute

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

자동 갱신 정리

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

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

Terminal window
kubectl rollout restart deploy/web

Secret — ConfigMap과 무엇이 다른가

섹션 제목: “Secret — ConfigMap과 무엇이 다른가”
apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
data: # base64로 인코딩된 값
password: czNjcjN0
stringData: # 평문으로 쓰면 API 서버가 인코딩해준다
username: admin
Terminal window
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/tls TLS 인증서 (Ingress에서 쓴다)
kubernetes.io/service-account-token SA 토큰 (14장)
Terminal window
kubectl get secret db-secret -o jsonpath='{.data.password}' | base64 -d
# s3cr3t

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

flowchart LR
    S["Secret"] --> B["base64 인코딩"]
    B -.->|"누구나 디코딩 가능<br/>보호가 아니다 ❌"| X["보호 아님"]
    S --> P1["① RBAC<br/>Secret 을 읽을 수 있는 주체 제한 · 14장"]
    S --> P2["② EncryptionConfiguration<br/>etcd 저장 시 암호화"]
    P1 --> OK["실제 보호 ✅"]
    P2 --> OK

    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class X bad
    class OK ok
    class P1,P2 key
    class S,B mute
  • 누구나 디코딩할 수 있다. 인코딩은 바이너리를 담기 위한 것이지 보호가 아니다
  • 기본 설정에서 etcd에 평문으로 저장된다
# /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 - 를 한 번 돌린다.

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

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

Terminal window
kubectl patch sa default -p '{"imagePullSecrets":[{"name":"regcred"}]}'
resources:
requests: # 스케줄링의 기준. "최소 이만큼은 보장해달라"
cpu: 100m
memory: 128Mi
limits: # 런타임의 상한. "이 이상은 못 쓴다"
cpu: 500m
memory: 256Mi

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

flowchart LR
    REQ["requests"] -->|"배치 시점"| SCH["kube-scheduler<br/>노드 남은 용량과 비교"]
    LIM["limits"] -->|"실행 시점"| KL["kubelet · 런타임<br/>cgroup 제한으로 강제"]
    SCH --> N["노드는 request 의 합만 본다<br/>실제 사용량이 아니다"]
    KL --> C["초과하면 throttling 또는 OOMKilled"]

    classDef req fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef lim fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef warn fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    class REQ,SCH req
    class LIM,KL lim
    class N,C warn
  • requests는 스케줄러가 본다. 노드의 남은 용량과 비교해 배치 가능 여부를 판단
  • limits는 kubelet/런타임이 강제한다. 실제 cgroup 제한이 된다
  • 노드는 request의 합만 본다. 실제 사용량이 아니다

이 비대칭이 실무 감각의 핵심이다.

flowchart LR
    CPU["CPU · 압축 가능<br/>1 = 코어 1개 · 500m = 0.5코어"] -->|"limit 초과"| T["throttling<br/>느려질 뿐 죽지 않는다 ⚠️"]
    MEM["메모리 · 압축 불가<br/>Mi = 1024² · M = 1000²"] -->|"limit 초과"| O["OOMKilled<br/>즉시 죽는다 ❌"]
    T --> A["CPU limit 은 느슨하게"]
    O --> B["메모리 limit 은 신중하게"]

    classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    class T,A warn
    class O,B bad
    class CPU,MEM key

128Mi128M 이라는 점도 기억할 것.

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

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

노드에 자원이 부족하면 kubelet이 Pod을 축출(evict)한다. 순서는 QoS가 정한다.

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

flowchart TD
    Q1{"requests · limits 를<br/>썼는가"}
    Q1 -->|"둘 다 없다"| BE["BestEffort<br/>가장 먼저 축출 ❌"]
    Q1 -->|"있다"| Q2{"모든 컨테이너에서<br/>requests == limits 인가<br/>CPU·메모리 둘 다"}
    Q2 -->|"그렇다"| GU["Guaranteed<br/>가장 나중에 축출 ✅"]
    Q2 -->|"아니다"| BU["Burstable<br/>중간 ⚠️"]

    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class BE bad
    class GU ok
    class BU warn
    class Q1,Q2 mute
클래스 조건 축출 순서
Guaranteed 모든 컨테이너에 requests == limits (CPU·메모리 둘 다) 가장 나중
Burstable requests가 있지만 limits와 다르다 중간
BestEffort requests도 limits도 없다 가장 먼저
Terminal window
kubectl get pod web -o jsonpath='{.status.qosClass}'

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

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에는 소급 적용되지 않는다
  • typeContainer / Pod / PersistentVolumeClaim

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"
persistentvolumeclaims: "5"
services.loadbalancers: "2"
count/deployments.apps: "10"
Terminal window
kubectl get resourcequota -n dev
kubectl describe resourcequota dev-quota -n dev # Used / Hard 를 보여준다

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

flowchart LR
    POD["requests 없는 Pod 생성 요청"] --> LR["LimitRange<br/>defaultRequest 를 채운다"]
    LR --> RQ["ResourceQuota<br/>requests.cpu 총량 검사"]
    RQ -->|"여유 있음"| OK["생성 ✅"]
    RQ -->|"초과"| NG["거부 ❌"]
    POD2["LimitRange 가 없다면"] -.->|"requests 가 비어 있어<br/>Quota 검사에서 거부 ❌"| NG2["생성 실패"]

    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    class OK ok
    class NG,NG2 bad
    class LR,RQ key
  • ConfigMap/Secret 주입은 셋: 개별 env / envFrom / 볼륨
  • 자동 갱신은 볼륨 마운트만. subPath는 안 된다. 실무 표준은 rollout restart
  • Secret의 base64는 보호가 아니다. 보호는 RBAC + etcd 암호화
  • CreateContainerConfigError = ConfigMap/Secret 이름·키 오타
  • requests는 스케줄링, limits는 런타임 강제. 노드는 request 합만 본다
  • CPU 초과 = throttling, 메모리 초과 = OOMKilled(137)
  • QoS는 지정하는 게 아니라 계산된다. BestEffort가 가장 먼저 쫓겨난다
  • ResourceQuota + LimitRange는 짝으로 — 그래야 requests 없는 Pod이 안 막힌다