Deployment를 배포한 뒤 한동안 잘 동작하던 Pod가 갑자기 재시작됩니다.
kubectl describe pod를 확인하면Reason: OOMKilled,Exit Code: 137이 보입니다. 메모리 limit을 올리면 잠시 괜찮다가 며칠 뒤 또 같은 증상이 반복됩니다. OOMKilled는 단순히 limit을 올린다고 해결되는 문제가 아닙니다. 왜 메모리가 초과했는지, 어느 수준에서 kill이 발생했는지, 어떤 기준으로 requests와 limits를 설계해야 하는지를 이해해야 재발을 방지할 수 있습니다.
핵심 요약
| 구분 | Container OOMKilled | Node 수준 Eviction |
|---|---|---|
| 트리거 | 컨테이너가 memory limit 초과 | Node 가용 메모리가 eviction threshold 이하 |
| Exit Code | 137 (SIGKILL) | 137 (SIGKILL) |
| Reason | OOMKilled |
Evicted |
| 대상 | 해당 컨테이너만 종료 | QoS 등급 낮은 Pod부터 축출 |
| 확인 방법 | kubectl describe pod → Last State |
kubectl describe node → Conditions |
| 해결 방향 | limit 조정 또는 앱 메모리 최적화 | requests 설정, Node 용량 확보 |
1. OOMKilled 동작 원리
OOMKilled는 두 가지 경로로 발생합니다. 하나는 컨테이너의 cgroup memory limit 초과, 다른 하나는 Node 전체 메모리 부족으로 인한 kubelet eviction입니다. 둘 다 Exit Code 137로 표시되지만, 원인과 대응 방법이 다릅니다.
1-1. Container OOMKilled (cgroup 기반)
Kubernetes는 컨테이너의 resources.limits.memory를 Linux cgroup의 memory.max(cgroup v2) 또는 memory.limit_in_bytes(cgroup v1)로 설정합니다. 컨테이너 내 프로세스의 메모리 사용량(RSS + page cache 일부)이 이 값을 초과하면, 커널의 OOM Killer가 해당 cgroup 내 프로세스를 SIGKILL(signal 9)로 종료합니다.
핵심 포인트:
- Exit Code 137 = 128 + 9 (SIGKILL)
- 컨테이너 단위로 격리되어, 같은 Pod의 다른 컨테이너에는 영향 없음
- limit이 설정되지 않은 컨테이너는 Node의 전체 메모리를 사용할 수 있어서, 다른 Pod에 영향을 줄 수 있음
1-2. Node 수준 Eviction
kubelet은 Node의 가용 메모리를 주기적으로 확인합니다. memory.available이 eviction threshold(기본: 100Mi) 아래로 내려가면, QoS 등급이 낮은 Pod부터 축출(evict)합니다.
QoS 등급별 축출 우선순위:
| QoS Class | 조건 | 축출 우선순위 |
|---|---|---|
| BestEffort | requests/limits 모두 미설정 | 가장 먼저 축출 |
| Burstable | requests < limits 또는 일부만 설정 | 두 번째 |
| Guaranteed | requests = limits (모든 컨테이너) | 가장 마지막 |
운영 환경에서는 모든 컨테이너에 최소 requests를 설정하여 BestEffort 등급을 피해야 합니다. BestEffort Pod는 Node에 메모리 압박이 오면 가장 먼저 종료됩니다.
2. OOMKilled 진단 절차
2-1. Pod 상태에서 OOMKilled 확인
# 1. Pod 상태 확인
kubectl describe pod <pod-name> -n <namespace>
출력에서 확인할 부분:
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Mon, 08 Jun 2026 09:15:00 +0900
Finished: Mon, 08 Jun 2026 11:42:33 +0900
Reason: OOMKilled가 명시되면 container limit 초과입니다. Reason: Evicted면 Node 수준 메모리 부족입니다.
2-2. Container OOMKilled vs Node Eviction 구분
# Container OOMKilled인 경우 — Pod는 같은 Node에서 재시작
kubectl get pod <pod-name> -n <namespace> -o wide
# RESTARTS 증가, NODE 동일
# Node Eviction인 경우 — Pod가 다른 Node로 이동
kubectl get events -n <namespace> --sort-by='.lastTimestamp' | grep -i evict
# "The node was low on resource: memory" 메시지 확인
2-3. 현재 메모리 사용량과 limit 비교
# 실시간 메모리 사용량 (metrics-server 필요)
kubectl top pod <pod-name> -n <namespace> --containers
# 설정된 limit 확인
kubectl get pod <pod-name> -n <namespace> \
-o jsonpath='{range .spec.containers[*]}{.name}{"\t"}{.resources.limits.memory}{"\n"}{end}'
출력 예시:
CONTAINER MEMORY(bytes)
my-app 480Mi
my-app 512Mi (← limit)
사용량이 limit에 근접하면 OOMKilled 위험이 높습니다. kubectl top은 특정 시점의 스냅샷이므로, 피크 시점을 포착하려면 모니터링 도구(Prometheus 등)에서 container_memory_working_set_bytes 메트릭을 확인하는 것이 정확합니다.
2-4. Node 수준 메모리 상태 확인
# Node 리소스 상태
kubectl describe node <node-name> | grep -A 5 "Conditions"
# MemoryPressure가 True이면 Node 수준 메모리 부족
# Node에서 실행 중인 Pod의 메모리 requests 합계
kubectl describe node <node-name> | grep -A 20 "Allocated resources"
출력 예시:
Conditions:
Type Status
MemoryPressure False
DiskPressure False
PIDPressure False
Ready True
Allocated resources:
Resource Requests Limits
memory 3200Mi (82%) 5120Mi (131%)
Limits 합계가 Node 용량을 초과(overcommit)하는 것은 허용되지만, 동시에 모든 Pod가 limit까지 사용하면 OOM이 발생합니다.
3. 원인별 분석
3-1. Memory Limit이 실제 사용량보다 낮게 설정된 경우
가장 단순한 원인입니다. 애플리케이션의 정상 동작에 필요한 메모리보다 limit이 낮게 설정되어 있습니다.
실무 시나리오:
Java Spring Boot 애플리케이션을 배포합니다. 개발 환경에서 kubectl top으로 확인한 메모리가 300Mi 정도여서 limit을 512Mi로 설정했습니다. 하지만 운영 환경에서는 동시 요청이 많아지면 Thread Pool, Connection Pool, HTTP 요청 버퍼 등이 추가로 메모리를 소비하여 512Mi를 초과합니다.
진단:
# Prometheus 쿼리로 메모리 피크 확인
# container_memory_working_set_bytes{pod="my-app-xxx", container="my-app"}
# 또는 kubectl top으로 부하 시점에 확인
kubectl top pod -n production --containers | grep my-app
해결:
부하 테스트를 수행한 뒤, P99 메모리 사용량을 기준으로 limit을 설정합니다.
spec:
containers:
- name: my-app
resources:
requests:
memory: "512Mi" # P50 사용량 기준
limits:
memory: "1Gi" # P99 사용량 + 20% 여유
3-2. 메모리 누수 (Memory Leak)
시간이 지날수록 메모리 사용량이 계속 증가하여 결국 limit에 도달하는 패턴입니다. limit을 올려도 시간이 더 걸릴 뿐 결국 OOMKilled가 발생합니다.
실무 시나리오:
Node.js Express 서버에서 요청마다 캐시 객체에 데이터를 저장하는데, 만료 정책 없이 무한히 쌓입니다. 배포 직후에는 메모리가 200Mi 수준이지만, 며칠 운영 후 limit(1Gi)에 도달하여 OOMKilled가 됩니다.
메모리 누수를 의심할 수 있는 패턴:
- 배포 직후에는 정상이지만, 일정 시간(수 시간~수 일) 후 OOMKilled 발생
- limit을 올려도 더 오래 버틸 뿐 결국 같은 증상 반복
- Prometheus 그래프에서 메모리가 계속 우상향 (톱니 패턴 없이)
진단:
# 시간별 메모리 추이 확인 (Prometheus/Grafana)
# rate(container_memory_working_set_bytes{pod=~"my-app.*"}[1h])
# Pod가 시작된 지 얼마나 됐는지 확인
kubectl get pod <pod-name> -n <namespace> \
-o jsonpath='{.status.startTime}'
해결 방향:
- 앱 코드에서 메모리 누수 원인을 찾아 수정 (근본 해결)
- 임시 방편: 정기적으로 Pod를 재시작 (롤링 리스타트 CronJob)
# 임시 방편 — 매일 새벽 3시에 롤링 리스타트
# CronJob으로 kubectl rollout restart를 수행
kubectl rollout restart deployment/my-app -n production
정기 재시작은 메모리 누수의 근본 해결이 아닙니다. 누수 원인을 찾는 동안 서비스 안정성을 유지하기 위한 임시 방편입니다. 장기적으로는 프로파일링 도구(Java: JFR/VisualVM, Node.js:
--inspect, Go: pprof)로 누수 지점을 식별하고 코드를 수정해야 합니다.3-3. JVM 메모리 구조와 컨테이너 limit 불일치
Java 애플리케이션에서 가장 흔한 OOMKilled 원인입니다. JVM의 전체 메모리 사용량은 Heap만이 아닙니다.
JVM 메모리 구성:
| 영역 | 설명 | 크기 예시 |
|---|---|---|
| Heap (-Xmx) | 객체 저장 | 512Mi |
| Metaspace | 클래스 메타데이터 | 100~200Mi |
| Thread Stack | 스레드당 1Mi (기본) | 200 threads × 1Mi = 200Mi |
| Native Memory | JNI, NIO DirectBuffer | 50~200Mi |
| Code Cache | JIT 컴파일 코드 | 50~240Mi |
| GC 오버헤드 | GC 알고리즘 내부 구조 | 가변 |
실무 시나리오:
Container limit을 1Gi로 설정하고, -Xmx512m을 지정했습니다. Heap은 512Mi지만 Metaspace(150Mi) + Thread Stack(200개 스레드 × 1Mi) + Native(100Mi)을 합치면 약 960Mi~1Gi에 달합니다. 트래픽이 몰려 스레드가 더 생성되면 limit을 초과합니다.
권장 설정:
spec:
containers:
- name: java-app
resources:
requests:
memory: "768Mi"
limits:
memory: "1Gi"
env:
- name: JAVA_OPTS
value: "-Xms256m -Xmx512m -XX:MaxMetaspaceSize=150m -XX:ReservedCodeCacheSize=64m"
JVM과 Container limit 관계 설계 공식:
Container limit ≥ Xmx + MaxMetaspaceSize + (Thread수 × ThreadStackSize) + Native + 여유(100~200Mi)
일반적으로 -Xmx를 container limit의 50~70%로 설정하면 Non-Heap 영역을 위한 여유가 확보됩니다.
Java 17+ 에서는
-XX:MaxRAMPercentage=70.0 옵션으로 컨테이너 메모리 limit의 70%를 자동으로 Heap에 할당할 수 있습니다. -Xmx를 직접 계산할 필요가 줄어듭니다. 다만 이 옵션은 cgroup limit을 기준으로 동작하므로, limit이 설정되어 있어야 합니다.3-4. Node.js / Go 런타임 메모리 설정
Node.js:
V8 엔진의 기본 Heap 크기는 약 1.5~2GB(64비트 시스템)입니다. Container limit이 512Mi인데 V8이 기본 설정으로 동작하면 limit을 초과할 수 있습니다.
spec:
containers:
- name: node-app
resources:
limits:
memory: "512Mi"
env:
- name: NODE_OPTIONS
value: "--max-old-space-size=384" # limit의 75% (MiB 단위)
Go:
Go 런타임은 기본적으로 가용 메모리의 일정 비율을 GC 트리거 기준으로 사용합니다. Go 1.19+에서는 GOMEMLIMIT 환경변수로 메모리 사용 상한을 설정할 수 있습니다.
spec:
containers:
- name: go-app
resources:
limits:
memory: "256Mi"
env:
- name: GOMEMLIMIT
value: "200MiB" # limit의 약 80%
- name: GOGC
value: "100" # 기본값 유지, 필요시 50으로 낮춰 GC 빈도 증가
3-5. 사이드카 컨테이너 메모리 미포함
Pod에 사이드카(Istio Envoy, Fluentd, CloudSQL Proxy 등)가 있을 때, 메인 컨테이너만 고려하고 사이드카의 메모리를 빠뜨리는 경우입니다.
실무 시나리오:
Istio Service Mesh를 사용하는 클러스터에서, 메인 앱 컨테이너 limit을 512Mi로 설정합니다. Istio가 자동 주입하는 Envoy 사이드카(istio-proxy)는 기본적으로 requests 128Mi, limit 미설정입니다. 트래픽이 많아지면 Envoy가 200~300Mi를 사용하면서 Node 메모리를 압박합니다. Node Eviction으로 다른 Pod가 축출되는 원인이 됩니다.
확인 방법:
# Pod의 모든 컨테이너 리소스 확인
kubectl get pod <pod-name> -n <namespace> -o json | \
jq '.spec.containers[] | {name, resources}'
# 모든 컨테이너의 실제 사용량 확인
kubectl top pod <pod-name> -n <namespace> --containers
해결:
# Istio 사이드카 리소스 설정 (Pod annotation)
metadata:
annotations:
sidecar.istio.io/proxyMemoryLimit: "256Mi"
sidecar.istio.io/proxyMemory: "128Mi"
4. requests와 limits 설계 전략
4-1. 설계 원칙
| 항목 | requests | limits |
|---|---|---|
| 목적 | 스케줄링 기준 (Node에 공간 예약) | 런타임 상한 (초과 시 OOMKill) |
| 기준값 | P50~P75 메모리 사용량 | P99 + 20~30% 여유 |
| 미설정 시 | 스케줄러가 0으로 간주 (어디든 배치) | Node 전체 메모리 사용 가능 |
| QoS 영향 | Guaranteed: requests = limits | BestEffort: 둘 다 미설정 |
4-2. 측정 기반 설정 절차
- 부하 테스트 실행: 운영과 유사한 트래픽 패턴으로 부하를 생성합니다.
- 메트릭 수집:
container_memory_working_set_bytes를 15~30분간 관찰합니다. - 백분위 계산: P50(requests 후보), P99(limits 후보)를 도출합니다.
- 여유분 추가: limits = P99 × 1.2~1.3 (버스트 대응)
- 검증: 설정 후 1~2주 운영하면서 OOMKilled 재발 여부 확인
# Prometheus 쿼리 — P99 메모리 사용량 (최근 7일)
quantile_over_time(0.99,
container_memory_working_set_bytes{
namespace="production",
pod=~"my-app.*",
container="my-app"
}[7d]
)
4-3. requests = limits로 설정할지 여부
Guaranteed QoS (requests = limits):
- 장점: OOM 시 가장 마지막에 축출. 성능 예측 가능.
- 단점: 메모리 효율이 낮음 (사용하지 않는 메모리도 예약됨).
- 적합한 경우: 미션 크리티컬 서비스, DB, 메시지 큐 등
Burstable QoS (requests < limits):
- 장점: 평상시 메모리 효율 높음. Node에 더 많은 Pod 배치 가능.
- 단점: 메모리 경합 시 축출 대상이 될 수 있음.
- 적합한 경우: 일반 웹 서비스, 배치 작업, 트래픽 변동이 큰 서비스
실무 권장:
# 미션 크리티컬 서비스 — Guaranteed
resources:
requests:
memory: "1Gi"
limits:
memory: "1Gi"
# 일반 웹 서비스 — Burstable
resources:
requests:
memory: "512Mi" # P50 사용량
limits:
memory: "1Gi" # P99 + 여유
5. Memory QoS와 cgroup v2 (Kubernetes v1.36)
Kubernetes v1.36에서는 Memory QoS 기능이 알파 단계로 제공됩니다(KEP-2570). 이 기능은 cgroup v2의 memory.min, memory.low, memory.high 컨트롤러를 활용하여, 단순한 "limit 초과 → 즉시 OOM Kill" 이외의 단계적 메모리 보호를 제공합니다.
기존 방식 (cgroup v1 또는 Memory QoS 미사용):
memory.max= limits.memory → 초과 즉시 OOM Kill- 단계적 경고나 throttling 없음
Memory QoS 활성화 시 (cgroup v2 + MemoryQoS feature gate):
| cgroup v2 파라미터 | Kubernetes 매핑 | 동작 |
|---|---|---|
memory.min |
requests.memory | 커널이 이 크기까지는 회수하지 않음 (hard reservation) |
memory.low |
정책에 따라 자동 설정 | 이 이하로는 가급적 회수하지 않음 (soft protection) |
memory.high |
limits.memory 근처 | 초과 시 throttling (프로세스 느려짐, kill은 아님) |
memory.max |
limits.memory | 초과 시 OOM Kill |
의미:
- requests로 설정한 메모리는 Node에 메모리 압박이 와도 커널이 보호합니다.
- limits에 도달하기 전에 throttling 단계가 있어, 즉시 kill 대신 "느려짐" 상태를 먼저 경험합니다.
- 운영자 입장에서 OOMKilled 빈도를 줄일 수 있는 가능성이 있습니다.
Memory QoS는 Kubernetes v1.36 기준 알파 기능입니다. 프로덕션에서 사용하려면 feature gate를 명시적으로 활성화해야 하며, Node OS가 cgroup v2를 지원해야 합니다. EKS(Amazon Linux 2023), AKS, GKE Autopilot은 기본적으로 cgroup v2를 사용하지만, 클러스터별로 확인이 필요합니다.
6. 실무 시나리오: Java 서비스 OOMKilled 디버깅
상황: 운영 환경의 Java Spring Boot 서비스가 배포 후 2~3시간이 지나면 OOMKilled로 재시작됩니다. Container limit은 1Gi, -Xmx는 설정하지 않은 상태입니다.
진단 과정:
# 1. OOMKilled 확인
kubectl describe pod order-service-7f8b9c-x4k2p -n production | grep -A 5 "Last State"
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
# 2. 현재 메모리 사용량 확인
kubectl top pod order-service-7f8b9c-x4k2p -n production --containers
# CONTAINER CPU MEMORY
# order-service 120m 945Mi ← limit(1Gi)에 근접
# 3. JVM 메모리 상세 확인 (Pod 접속 가능할 때)
kubectl exec order-service-7f8b9c-x4k2p -n production -- \
java -XX:+PrintFlagsFinal -version 2>&1 | grep -i "maxheapsize\|maxram"
# MaxHeapSize = 268435456 (256Mi) ← 기본값: container limit의 25%
# 4. Metaspace 확인 (JFR 또는 JMX로)
# Metaspace: 180Mi, Threads: 250개 × 1Mi = 250Mi
# 합계: Heap(256Mi) + Metaspace(180Mi) + Threads(250Mi) + Native(150Mi) ≈ 836Mi~1Gi+
원인 분석:
-Xmx를 지정하지 않으면 JVM은 기본적으로 container limit의 25%(또는 물리 메모리의 25%)를 Heap으로 사용합니다. 1Gi limit에서 Heap은 약 256Mi입니다. 다만 Non-Heap 영역(Metaspace, Thread Stack, Native Memory)이 예상보다 커서 전체 합이 1Gi를 초과합니다.
해결:
spec:
containers:
- name: order-service
image: order-service:v2.1.0
resources:
requests:
memory: "1Gi"
limits:
memory: "1536Mi" # 1.5Gi로 상향
env:
- name: JAVA_OPTS
value: >-
-XX:MaxRAMPercentage=65.0
-XX:InitialRAMPercentage=50.0
-XX:MaxMetaspaceSize=200m
-XX:ReservedCodeCacheSize=64m
-Xss512k
설정 근거:
MaxRAMPercentage=65.0: 1536Mi의 65% ≈ 998Mi를 Heap으로 사용MaxMetaspaceSize=200m: Metaspace 상한 제한Xss512k: 기본 1MB인 Thread Stack을 512KB로 줄임 (250 threads × 512KB = 125Mi 절약)- 총 예상: Heap(998Mi) + Metaspace(200Mi) + Threads(125Mi) + Native(100Mi) = 1423Mi < 1536Mi
7. limits 없이 운영하는 전략
일부 팀에서는 memory limits를 설정하지 않고 requests만 설정하는 전략을 사용합니다. 이 접근의 trade-off를 이해해야 합니다.
limits 미설정 시:
- 장점: OOMKilled가 발생하지 않음 (container 수준). 앱이 일시적으로 많은 메모리를 사용해도 kill되지 않음.
- 단점: Node 전체 메모리를 소진할 수 있음. 다른 Pod에 영향. Node Eviction 발생 가능.
이 전략이 적합한 경우:
- Node당 하나의 서비스만 운영하는 경우
- 모니터링이 잘 되어 있고, 메모리 이상을 빠르게 감지할 수 있는 경우
- 서비스의 메모리 사용 패턴이 예측 가능한 경우
이 전략이 위험한 경우:
- 멀티 테넌트 클러스터 (여러 팀이 같은 Node를 공유)
- 메모리 누수 가능성이 있는 서비스
- BestEffort Pod가 혼재하는 환경
운영 환경에서는 일반적으로 limits를 설정하는 것이 안전합니다. 하나의 Pod가 Node 전체를 다운시키는 상황을 방지할 수 있기 때문입니다.
8. 재발 방지 체크리스트
| 항목 | 확인 내용 | 권장 방법 |
|---|---|---|
| 메모리 측정 | 부하 테스트로 P99 사용량 확인했는가? | 최소 30분 부하 테스트 수행 |
| requests 설정 | P50~P75 기준으로 설정했는가? | BestEffort 등급 방지 |
| limits 설정 | P99 + 20~30% 여유가 있는가? | 버스트 대응 |
| 런타임 설정 | JVM/Node.js/Go의 메모리 옵션이 limit과 정합하는가? | Heap ≤ limit의 70% |
| 사이드카 | Envoy, 로그 수집기 등의 메모리를 포함했는가? | 모든 컨테이너 합계 확인 |
| 모니터링 | container_memory_working_set_bytes 알림 설정했는가? |
limit의 80% 도달 시 경고 |
| 메모리 누수 | 시간 경과에 따른 메모리 증가 패턴이 있는가? | 프로파일링 도구 사용 |
| Node 용량 | requests 합계가 Node allocatable의 80% 이하인가? | 시스템 예약분 확보 |
9. 모니터링 알림 설정 예시
OOMKilled가 발생하기 전에 경고를 받으려면, 메모리 사용률이 limit의 80%에 도달했을 때 알림을 보내는 것이 효과적입니다.
Prometheus Alerting Rule:
groups:
- name: memory-alerts
rules:
- alert: ContainerMemoryNearLimit
expr: |
container_memory_working_set_bytes{container!=""}
/
container_spec_memory_limit_bytes{container!=""}
> 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "Container {{ $labels.container }} in pod {{ $labels.pod }} is using >80% of memory limit"
description: "Current usage: {{ $value | humanizePercentage }} of limit. OOMKilled risk is high."
- alert: ContainerOOMKilled
expr: |
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
for: 0m
labels:
severity: critical
annotations:
summary: "Container {{ $labels.container }} in pod {{ $labels.pod }} was OOMKilled"
10. 정리
- OOMKilled는 container limit 초과(cgroup OOM)와 Node 메모리 부족(kubelet eviction) 두 경로로 발생합니다.
kubectl describe pod에서 Reason을 확인하여 구분합니다. - limit을 올리는 것은 임시 방편입니다. 메모리 누수가 원인이면 결국 재발합니다. 시간에 따른 메모리 추이를 먼저 확인하세요.
- JVM 애플리케이션은 Heap 외에 Metaspace, Thread Stack, Native Memory를 합산해야 합니다.
-Xmx를 container limit의 50~70%로 설정하는 것이 안전합니다. - requests는 스케줄링과 QoS 등급에 영향합니다. BestEffort 등급을 피하려면 최소한 requests를 설정해야 합니다.
- 부하 테스트 → P99 측정 → limits 설계 순서로 접근하면, 추측이 아닌 데이터 기반으로 안정적인 메모리 설정이 가능합니다.
관련 글
- Kubernetes CrashLoopBackOff 원인과 해결 방법
- Kubernetes HPA가 동작하지 않는 이유
- Kubernetes 아키텍처 기본 구조
- Prometheus + Grafana로 Kubernetes 모니터링 구성하기
참고 문서
'Troubleshooting' 카테고리의 다른 글
| EKS Pod가 외부 인터넷에 접근하지 못하는 경우 (0) | 2026.06.06 |
|---|---|
| Terraform Error acquiring the state lock 해결 방법 (0) | 2026.06.05 |
| Kubernetes CrashLoopBackOff 원인과 해결 방법 (0) | 2026.06.05 |
| S3 AccessDenied 원인과 해결 방법: Bucket Policy, IAM, KMS, VPC Endpoint까지 (0) | 2026.06.01 |
| Kubernetes ImagePullBackOff 원인과 해결 방법 (0) | 2026.06.01 |