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(민감한 값). 구조는 거의 같고, 취급만 다르다.
ConfigMap
섹션 제목: “ConfigMap”ConfigMap 만들기
섹션 제목: “ConfigMap 만들기”# 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.envkubectl get cm app-config -o yamlkubectl describe cm app-configConfigMap 매니페스트
섹션 제목: “ConfigMap 매니페스트”apiVersion: v1kind: ConfigMapmetadata: name: app-configdata: 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
주입 방식 1 — 개별 환경변수
섹션 제목: “주입 방식 1 — 개별 환경변수”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-configkubectl set env deploy/web --from=secret/db-secretkubectl 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를 쓰면 명시한 키만 마운트된다
subPath — 파일 하나만 얹기
섹션 제목: “subPath — 파일 하나만 얹기”volumeMounts: - name: config mountPath: /etc/nginxflowchart 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자동 갱신은 되지만 원래 있던 파일이 전부 가려진다.
volumeMounts: - name: config mountPath: /etc/nginx/nginx.conf subPath: nginx.confflowchart LR
B["/etc/nginx 원래 내용<br/>mime.types · conf.d/"] -->|"그대로 남는다 ✅"| A["/etc/nginx"]
A --> F["nginx.conf 만 교체됨"]
U["ConfigMap 수정"] -.->|"자동 반영 안 됨 ❌"| F
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class B ok
class U bad
class A,F mute디렉터리를 덮지 않고 파일 하나만 교체한다. 대신 자동 갱신이 안 된다 — 여기가 핵심 트레이드오프다.
자동 갱신 정리
| 방식 | ConfigMap을 바꾸면 |
|---|---|
환경변수 (env, envFrom) |
반영 안 됨. Pod을 다시 만들어야 한다 |
| 볼륨 마운트 | 반영됨 (kubelet 동기화 주기, 대략 1분 이내) |
볼륨 + subPath |
반영 안 됨 |
반영되더라도 앱이 파일을 다시 읽어야 의미가 있다. 그래서 실무 표준은 —
kubectl rollout restart deploy/webSecret
섹션 제목: “Secret”Secret — ConfigMap과 무엇이 다른가
섹션 제목: “Secret — ConfigMap과 무엇이 다른가”apiVersion: v1kind: Secretmetadata: name: db-secrettype: Opaquedata: # base64로 인코딩된 값 password: czNjcjN0stringData: # 평문으로 쓰면 API 서버가 인코딩해준다 username: adminkubectl 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장) |
base64는 암호화가 아니다
섹션 제목: “base64는 암호화가 아니다”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/v1kind: EncryptionConfigurationresources: - resources: ["secrets"] providers: - aescbc: keys: - name: key1 secret: <base64로 인코딩한 32바이트 키> - identity: {}설정 후 기존 Secret을 다시 암호화하려면
kubectl get secrets -A -o json | kubectl replace -f - 를 한 번 돌린다.
imagePullSecrets
섹션 제목: “imagePullSecrets”spec: imagePullSecrets: - name: regcred containers: - name: app image: registry.example.com/myapp:1.0ServiceAccount에 붙여두면 그 SA를 쓰는 모든 Pod에 자동 적용된다.
kubectl patch sa default -p '{"imagePullSecrets":[{"name":"regcred"}]}'리소스 관리
섹션 제목: “리소스 관리”requests와 limits
섹션 제목: “requests와 limits”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의 합만 본다. 실제 사용량이 아니다
CPU와 메모리의 단위
섹션 제목: “CPU와 메모리의 단위”이 비대칭이 실무 감각의 핵심이다.
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
128Mi ≠ 128M 이라는 점도 기억할 것.
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도 없다 | 가장 먼저 |
kubectl get pod web -o jsonpath='{.status.qosClass}'중요한 워크로드를 Guaranteed로 만들려면 requests와 limits를 똑같이 쓰면 된다.
네임스페이스 단위 제약
섹션 제목: “네임스페이스 단위 제약”LimitRange — 네임스페이스의 기본값과 상한
섹션 제목: “LimitRange — 네임스페이스의 기본값과 상한”apiVersion: v1kind: LimitRangemetadata: name: defaults namespace: devspec: 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/PersistentVolumeClaim
ResourceQuota — 네임스페이스 총량 제한
섹션 제목: “ResourceQuota — 네임스페이스 총량 제한”apiVersion: v1kind: ResourceQuotametadata: name: dev-quota namespace: devspec: hard: requests.cpu: "4" requests.memory: 8Gi limits.cpu: "8" limits.memory: 16Gi pods: "20" persistentvolumeclaims: "5" services.loadbalancers: "2" count/deployments.apps: "10"kubectl get resourcequota -n devkubectl 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
6장 요약
섹션 제목: “6장 요약”- 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이 안 막힌다