설정과 리소스
ConfigMap · Secret · requests/limits
설정을 이미지에서 분리하는 이유
섹션 제목: “설정을 이미지에서 분리하는 이유”- 같은 이미지를 dev / staging / prod에 그대로 쓸 수 있다
- 설정만 바꿔서 재배포할 수 있다 — 이미지를 다시 빌드하지 않는다
- 비밀 값이 이미지 레이어에 박히지 않는다
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의 제약이다. 큰 파일은 볼륨을 쓰자
immutable: true로 잠그면 수정 요청을 API 서버가 거부한다 — 실수 방지에 더해, kubelet이 변경 감시(watch)를 끊어 대규모 클러스터의 부하를 줄인다. 바꾸려면 새 ConfigMap을 만들어 참조를 교체한다. 깊게 팔 필요는 없다 — 이름과 효과만 알면 된다
주입 방식 세 가지 — 전체 지도
섹션 제목: “주입 방식 세 가지 — 전체 지도”주입 방식 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를 쓰면 명시한 키만 마운트된다
volumes·volumeMounts 짝은 손으로 치다 들여쓰기가 틀리기 쉽다 — 시험장에서는 공식
Configure a Pod to Use a ConfigMap에서
복사해 이름만 바꾸는 게 정석이다 (env·envFrom 예시도 같은 페이지에 있다).
subPath — 파일 하나만 얹기
섹션 제목: “subPath — 파일 하나만 얹기”volumeMounts: - name: config mountPath: /etc/nginx자동 갱신은 되지만 원래 있던 파일이 전부 가려진다.
volumeMounts: - name: config mountPath: /etc/nginx/nginx.conf subPath: nginx.conf디렉터리를 덮지 않고 파일 하나만 교체한다. 대신 자동 갱신이 안 된다 — 여기가 핵심 트레이드오프다.
자동 갱신 정리
| 방식 | ConfigMap을 바꾸면 |
|---|---|
환경변수 (env, envFrom) | 반영 안 됨. Pod을 다시 만들어야 한다 |
| 볼륨 마운트 | 반영됨 (kubelet 동기화 주기, 대략 1분 이내) |
볼륨 + subPath | 반영 안 됨 |
반영되더라도 앱이 파일을 다시 읽어야 의미가 있다. 그래서 실무 표준은 —
kubectl rollout restart deploy/webSecret
섹션 제목: “Secret”Secret — ConfigMap과 무엇이 다른가
섹션 제목: “Secret — ConfigMap과 무엇이 다른가”구조는 ConfigMap과 거의 같다 — 다른 것은 취급이다. 값이 base64로 담기고(data),
노드에 볼륨으로 풀릴 때 디스크가 아니라 메모리(tmpfs)에만 풀리며, 별도 리소스라서
RBAC으로 ConfigMap과 다른 권한을 걸 수 있다. 민감한 값을 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 토큰 (ServiceAccount) |
base64는 암호화가 아니다
섹션 제목: “base64는 암호화가 아니다”kubectl get secret db-secret -o jsonpath='{.data.password}' | base64 -d# s3cr3t실제 보호는 base64가 아니라 다른 두 축에서 온다.
- 누구나 디코딩할 수 있다. 인코딩은 바이너리를 담기 위한 것이지 보호가 아니다
- 읽을 수 있는 주체를 좁히는 것은 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/v1kind: EncryptionConfigurationresources: - resources: ["secrets"] providers: - aescbc: keys: - name: key1 secret: <base64로 인코딩한 32바이트 키> - identity: {}설정 후 기존 Secret을 다시 암호화하려면
kubectl get secrets -A -o json | kubectl replace -f - 를 한 번 돌린다.
EncryptionConfiguration 파일을 직접 쓰는 문제까지 깊게 팔 필요는 없다 — 기본은 평문 저장이고, apiserver 플래그로 켠다는 사실 정도면 충분하다.
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"}]}'리소스 관리
섹션 제목: “리소스 관리”컨테이너는 선언이 없으면 노드의 CPU·메모리를 얼마든지 쓸 수 있다. 그러면 두 가지가 무너진다 — 스케줄러는 Pod이 얼마나 쓸지 모르니 배치를 판단할 근거가 없고, 런타임은 한 컨테이너가 폭주해도 막을 수단이 없다. requests/limits는 그 두 판단을 위해 spec에 적어 두는 선언이다 — 아키텍처 아키텍처에서 스케줄러와 kubelet이 각자 읽어 간다.
requests와 limits
섹션 제목: “requests와 limits”resources: requests: # 스케줄링의 기준. "최소 이만큼은 보장해달라" cpu: 100m memory: 128Mi limits: # 런타임의 상한. "이 이상은 못 쓴다" cpu: 500m memory: 256Mi둘은 서로 다른 컴포넌트가 서로 다른 시점에 본다.
requests는 스케줄러가 본다. 노드의 남은 용량과 비교해 배치 가능 여부를 판단limits는 kubelet/런타임이 강제한다. 실제 cgroup(control group — 리눅스 커널의 자원 제한 장치) 제한이 된다- 노드는 request의 합만 본다. 실제 사용량이 아니다
CPU와 메모리의 단위
섹션 제목: “CPU와 메모리의 단위”Kubernetes의 리소스 단위에서 CPU는 코어 수, 메모리는 바이트 수로 적는다.
| 표기 | 뜻 |
|---|---|
1 CPU | 물리 코어 또는 가상 코어 1개 |
1000m | 1 CPU. m은 millicpu 또는 millicore다 |
500m | 0.5 CPU |
1m | 0.001 CPU, 코어 하나의 0.1%에 해당하는 양 |
1Mi | 1 mebibyte = 1,048,576 bytes = 1024² bytes |
1M | 1 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을 넘었을 때의 비대칭도 실무 감각의 핵심이다.
128Mi ≠ 128M 이라는 점과, CPU의 1m과 메모리의 1Mi는 전혀 다른 단위라는 점도
기억할 것.
노드 용량에서 requests 정하기
섹션 제목: “노드 용량에서 requests 정하기”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 + 앱 200m | 500m |
| init 400m, 앱 300m | 400m |
| init 300m, 앱 300m | 300m |
네이티브 사이드카는 initContainers에 적지만 계속 실행되므로 앱 컨테이너 쪽 합에 들어간다.
실제 수치로 나눠 보는 연습은 실전 과제에 있다.
QoS 클래스 — 누가 먼저 쫓겨나는가
섹션 제목: “QoS 클래스 — 누가 먼저 쫓겨나는가”노드에 자원이 부족하면 kubelet이 Pod을 축출(evict)한다. 순서는 QoS(Quality of Service — 서비스 품질) 클래스가 정한다.
QoS는 지정하는 것이 아니라 계산된다.
| 클래스 | 조건 | 축출 순서 |
|---|---|---|
| Guaranteed | 모든 컨테이너에 requests == limits (CPU·메모리 둘 다) | 가장 나중 |
| Burstable | requests가 있지만 limits와 다르다 | 중간 |
| BestEffort | requests도 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: 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은 Pod 안 컨테이너들의 합,PersistentVolumeClaim은 요청 용량(PV와 PVC). 기본값(default/defaultRequest)을 채워주는 것은Container뿐이다
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" requests.storage: 100Gi persistentvolumeclaims: "5" services.loadbalancers: "2" count/deployments.apps: "10"kubectl get resourcequota -n devkubectl 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에서 확인한다.
둘은 짝으로 쓴다. 왜 그런지는 순서대로 보면 명확하다.
- 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 요청 용량과 개수를 센다. 실제 파일 사용량 제한은 스토리지 백엔드의 몫이다