본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 23. · 3 Views
Kubernetes 프로덕션 완벽 가이드
실전에서 꼭 알아야 할 Kubernetes 프로덕션 고려사항을 단계별로 학습합니다. 리소스 관리부터 보안, 모니터링까지 현업에서 사용하는 필수 설정들을 다룹니다.
목차
1. 리소스 요청과 제한
입사 6개월 차 김개발 씨가 처음으로 Kubernetes 클러스터에 애플리케이션을 배포했습니다. 처음에는 잘 작동하는 것 같았는데, 갑자기 다른 팀의 서비스까지 느려지기 시작했습니다.
긴급 회의가 소집되었고, 인프라팀 박시니어 씨가 원인을 찾아냈습니다.
리소스 요청과 제한은 Kubernetes에서 Pod가 사용할 CPU와 메모리를 정의하는 핵심 설정입니다. 마치 식당에서 테이블을 예약하고 최대 인원을 정하는 것과 같습니다.
이것을 제대로 설정하면 클러스터 전체의 안정성과 성능을 보장할 수 있습니다.
다음 코드를 살펴봅시다.
apiVersion: v1
kind: Pod
metadata:
name: production-app
spec:
containers:
- name: app
image: myapp:1.0
resources:
# 최소 보장 리소스 (requests)
requests:
cpu: "500m" # 0.5 CPU 코어
memory: "512Mi" # 512MB 메모리
# 최대 사용 제한 (limits)
limits:
cpu: "1000m" # 1 CPU 코어까지
memory: "1Gi" # 1GB까지 사용 가능
김개발 씨는 당황했습니다. "제 애플리케이션이 다른 서비스에까지 영향을 준다고요?" 박시니어 씨가 모니터링 화면을 보여주며 설명했습니다.
"여기 보세요. 당신의 Pod가 노드의 메모리를 90%나 사용하고 있어요.
같은 노드에 있는 다른 Pod들이 메모리 부족으로 느려진 겁니다." 이 상황은 실제 현업에서 매우 흔하게 발생합니다. 특히 초보 개발자들이 Kubernetes에 처음 배포할 때 리소스 설정을 생략하는 경우가 많습니다.
리소스 요청과 제한이란 정확히 무엇일까요? 쉽게 비유하자면, 이것은 마치 호텔 예약과 같습니다.
requests는 "최소한 이 정도는 보장해주세요"라고 예약하는 것입니다. limits는 "아무리 많이 써도 이것 이상은 쓰지 않겠습니다"라고 약속하는 것입니다.
호텔이 예약을 받아야 방을 효율적으로 배치할 수 있듯이, Kubernetes도 이 정보를 바탕으로 Pod를 적절한 노드에 배치합니다. 리소스 설정이 없던 시절에는 어땠을까요?
초기 컨테이너 오케스트레이션 시스템에서는 각 컨테이너가 마음대로 리소스를 사용했습니다. 하나의 애플리케이션이 메모리 누수를 일으키면 같은 서버의 다른 모든 애플리케이션이 영향을 받았습니다.
더 큰 문제는 어떤 서버에 여유가 있는지 알 수 없어서 수동으로 배치해야 했다는 점입니다. 바로 이런 문제를 해결하기 위해 리소스 요청과 제한 개념이 등장했습니다.
requests를 설정하면 Kubernetes 스케줄러가 충분한 리소스가 있는 노드를 자동으로 찾아줍니다. 또한 limits를 설정하면 한 Pod가 노드 전체를 점유하는 것을 방지할 수 있습니다.
무엇보다 클러스터의 용량을 정확히 계산할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 requests 섹션을 보면 Pod가 최소한 필요로 하는 리소스를 선언합니다. CPU는 "500m"로 설정했는데, 여기서 m은 밀리코어를 의미합니다.
1000m이 1개의 CPU 코어이므로, 500m은 0.5코어입니다. 메모리는 "512Mi"로 설정했습니다.
이는 512메가바이트를 의미합니다. 다음으로 limits 섹션에서는 Pod가 최대로 사용할 수 있는 리소스를 제한합니다.
CPU는 1000m, 즉 1코어까지, 메모리는 1Gi, 즉 1기가바이트까지 사용할 수 있습니다. 만약 Pod가 이 제한을 초과하려고 하면 어떻게 될까요?
CPU는 쓰로틀링되고, 메모리는 OOMKilled, 즉 종료됩니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 전자상거래 서비스를 운영한다고 가정해봅시다. 주문 처리 서비스는 중요하므로 높은 requests를 설정하여 항상 충분한 리소스를 보장받습니다.
반면 이미지 리사이징 같은 배치 작업은 낮은 requests로 설정하되 limits는 높게 설정하여, 여유가 있을 때 많이 사용하되 다른 서비스를 방해하지 않도록 합니다. 네이버, 카카오 같은 대형 서비스들은 수천 개의 Pod를 운영하면서도 이런 세밀한 리소스 설정을 통해 클러스터 효율을 극대화하고 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 requests를 너무 낮게 설정하는 것입니다.
실제로는 1GB가 필요한데 512MB로 설정하면, Pod가 메모리 부족으로 자주 재시작됩니다. 반대로 limits를 너무 낮게 설정하면, 트래픽이 증가할 때 성능이 급격히 떨어집니다.
따라서 실제 사용량을 모니터링하여 적절한 값을 찾아야 합니다. 또 다른 실수는 requests와 limits를 동일하게 설정하는 것입니다.
이것을 QoS Guaranteed 클래스라고 하는데, 중요한 서비스에는 좋지만 모든 Pod에 이렇게 설정하면 클러스터 효율이 떨어집니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 즉시 리소스 설정을 추가했습니다. 며칠간 모니터링한 결과 실제 평균 사용량은 CPU 300m, 메모리 400Mi였습니다.
피크 시간에는 CPU 800m, 메모리 900Mi까지 올라갔습니다. 이 데이터를 바탕으로 김개발 씨는 requests를 CPU 500m, 메모리 512Mi로, limits를 CPU 1000m, 메모리 1Gi로 설정했습니다.
여유를 두되 낭비하지 않는 적절한 값이었습니다. 리소스 설정을 제대로 이해하면 더 안정적이고 효율적인 클러스터를 운영할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 처음에는 넉넉하게 설정하고 모니터링하며 점진적으로 최적화하세요
- requests는 평균 사용량의 1.2배, limits는 피크 사용량의 1.5배가 적절합니다
- CPU limits는 생략할 수 있지만 메모리 limits는 반드시 설정하세요
2. Liveness와 Readiness Probe
김개발 씨의 애플리케이션이 배포되었습니다. 하지만 이상한 일이 발생했습니다.
가끔 사용자들이 "서비스가 응답하지 않는다"고 신고하는데, Kubernetes 대시보드에서는 Pod가 정상이라고 표시되는 것입니다. 박시니어 씨가 로그를 확인하더니 한숨을 쉬었습니다.
"헬스체크를 설정하지 않았네요."
Probe는 Kubernetes가 애플리케이션의 상태를 주기적으로 확인하는 헬스체크 메커니즘입니다. 마치 병원에서 환자의 맥박과 혈압을 체크하는 것과 같습니다.
Liveness Probe는 살아있는지, Readiness Probe는 요청을 받을 준비가 되었는지를 확인합니다.
다음 코드를 살펴봅시다.
apiVersion: v1
kind: Pod
metadata:
name: production-app
spec:
containers:
- name: app
image: myapp:1.0
# 애플리케이션이 살아있는지 확인 (죽었으면 재시작)
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30 # 시작 후 30초 대기
periodSeconds: 10 # 10초마다 체크
timeoutSeconds: 3 # 3초 안에 응답 없으면 실패
failureThreshold: 3 # 3번 실패하면 재시작
# 트래픽을 받을 준비가 되었는지 확인
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 2
박시니어 씨가 상황을 설명했습니다. "보세요, 여기 로그를 보면 애플리케이션이 데드락에 걸려서 응답을 못하고 있어요.
하지만 프로세스 자체는 살아있으니까 Kubernetes는 정상이라고 생각하는 겁니다." 김개발 씨가 물었습니다. "그럼 Kubernetes는 어떻게 우리 애플리케이션이 제대로 작동하는지 알 수 있나요?" 바로 이때 필요한 것이 Probe입니다.
Probe란 정확히 무엇일까요? 쉽게 비유하자면, Probe는 마치 경비원이 순찰을 도는 것과 같습니다.
Liveness Probe는 "이 사람이 살아있나요?"를 확인하는 것입니다. 만약 응답이 없으면 심폐소생술, 즉 컨테이너를 재시작합니다.
Readiness Probe는 "이 직원이 업무를 받을 준비가 되었나요?"를 확인하는 것입니다. 준비가 안 되었으면 일을 주지 않습니다.
Probe가 없던 시절에는 어땠을까요? 초기에는 개발자들이 외부 모니터링 도구로 애플리케이션을 감시했습니다.
문제가 발견되면 수동으로 컨테이너를 재시작해야 했습니다. 더 큰 문제는 애플리케이션이 아직 준비되지 않았는데 트래픽이 들어와서 에러가 발생하는 것이었습니다.
특히 대규모 배포 시에는 이런 문제가 치명적이었습니다. 바로 이런 문제를 해결하기 위해 Probe 개념이 등장했습니다.
Liveness Probe를 설정하면 데드락, 무한 루프 같은 상황에서 Kubernetes가 자동으로 컨테이너를 재시작합니다. 또한 Readiness Probe를 설정하면 애플리케이션이 완전히 준비될 때까지 트래픽을 보내지 않습니다.
무엇보다 롤링 업데이트 시 안전하게 배포할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 livenessProbe 섹션을 보면 HTTP GET 요청으로 /healthz 엔드포인트를 체크합니다. initialDelaySeconds: 30은 컨테이너가 시작된 후 30초 동안은 체크하지 않는다는 의미입니다.
애플리케이션이 초기화되는 시간을 주는 것입니다. periodSeconds: 10은 10초마다 체크한다는 뜻입니다.
timeoutSeconds: 3은 3초 안에 응답이 없으면 실패로 간주합니다. failureThreshold: 3은 3번 연속 실패하면 컨테이너를 재시작한다는 의미입니다.
한두 번의 일시적인 실패로 바로 재시작하지 않도록 여유를 둔 것입니다. 다음으로 readinessProbe 섹션은 /ready 엔드포인트를 체크합니다.
이것은 liveness보다 더 자주 체크하는 것이 좋습니다. periodSeconds: 5로 설정하여 5초마다 확인합니다.
만약 이 체크가 실패하면 Service의 엔드포인트 목록에서 제외되어 트래픽을 받지 않습니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 Spring Boot 애플리케이션을 운영한다고 가정해봅시다. 애플리케이션이 시작될 때 데이터베이스 연결을 초기화하고 캐시를 워밍업하는 데 20초가 걸립니다.
이때 Readiness Probe를 설정하면 완전히 준비된 후에만 트래픽을 받기 시작합니다. 사용자는 에러를 경험하지 않습니다.
또 다른 예로, Node.js 애플리케이션이 메모리 누수로 점점 느려지는 상황을 생각해봅시다. Liveness Probe가 응답 시간을 체크하여 일정 시간 이상 걸리면 실패로 판단하고 컨테이너를 재시작합니다.
이렇게 자동으로 복구됩니다. 쿠팡, 배달의민족 같은 대규모 서비스는 하루에도 수십 번 배포하면서 Probe 설정으로 무중단 배포를 실현합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 initialDelaySeconds를 너무 짧게 설정하는 것입니다.
애플리케이션이 아직 시작도 안 됐는데 체크가 시작되어 계속 재시작을 반복하는 CrashLoopBackOff 상태에 빠집니다. 또 다른 실수는 Liveness Probe에서 외부 의존성을 체크하는 것입니다.
예를 들어 데이터베이스 연결을 체크하면, DB가 잠시 불안정할 때 모든 Pod가 동시에 재시작되어 더 큰 장애로 이어집니다. Liveness는 애플리케이션 자체만 체크하고, 외부 의존성은 Readiness에서 체크해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언에 따라 김개발 씨는 두 가지 엔드포인트를 구현했습니다.
/healthz 엔드포인트는 단순히 HTTP 200을 반환합니다. 애플리케이션 프로세스가 응답할 수 있는지만 확인합니다.
/ready 엔드포인트는 데이터베이스 연결과 필수 캐시가 준비되었는지 확인하고 결과를 반환합니다. 배포 후 모니터링한 결과, 더 이상 사용자들의 에러 신고가 들어오지 않았습니다.
또한 한 번 데드락이 발생했을 때 Kubernetes가 자동으로 재시작하여 빠르게 복구되었습니다. Probe를 제대로 이해하면 더 견고하고 신뢰할 수 있는 서비스를 만들 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - Liveness는 애플리케이션 자체만, Readiness는 외부 의존성까지 체크하세요
- initialDelaySeconds는 애플리케이션 시작 시간의 1.5배로 설정하세요
- 헬스체크 엔드포인트는 가볍게 만들어 부하를 최소화하세요
3. PodDisruptionBudget
어느 날 새벽, 김개발 씨는 긴급 전화를 받았습니다. 클러스터 노드 업그레이드 작업 중 서비스가 전체 중단되었다는 것입니다.
아침에 출근한 김개발 씨는 로그를 확인하며 당황했습니다. "모든 Pod가 동시에 종료되었네요?" 박시니어 씨가 진단을 내렸습니다.
"PodDisruptionBudget을 설정하지 않아서 그래요."
PodDisruptionBudget은 자발적 중단 시 최소한 유지해야 할 Pod 개수를 보장하는 정책입니다. 마치 은행에 항상 일정 수의 창구 직원이 있어야 하는 것과 같습니다.
노드 유지보수나 클러스터 업그레이드 시에도 서비스 가용성을 보장할 수 있습니다.
다음 코드를 살펴봅시다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app-pdb
spec:
# 최소 2개의 Pod는 항상 실행되어야 함
minAvailable: 2
# 또는 최대 1개까지만 동시에 중단 가능
# maxUnavailable: 1
selector:
matchLabels:
app: production-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: production-app
spec:
replicas: 4 # PDB 보호를 받는 4개의 Pod
selector:
matchLabels:
app: production-app
template:
metadata:
labels:
app: production-app
spec:
containers:
- name: app
image: myapp:1.0
박시니어 씨가 상황을 재구성했습니다. "새벽에 인프라팀이 노드 4개를 업그레이드하려고 드레인했어요.
당신의 Pod 4개가 전부 그 노드들에 있었고, 동시에 종료되었습니다. 새 노드에서 Pod가 시작되기까지 약 2분간 서비스가 완전히 중단된 거죠." 김개발 씨가 답답해했습니다.
"그럼 노드 업그레이드를 어떻게 해야 하죠?" 바로 이때 필요한 것이 PodDisruptionBudget입니다. PodDisruptionBudget, 줄여서 PDB란 정확히 무엇일까요?
쉽게 비유하자면, PDB는 마치 소방법과 같습니다. 건물에 화재 시 사용할 수 있는 비상구와 소화기가 최소 몇 개 있어야 한다고 규정하듯이, PDB는 "아무리 긴급한 상황이라도 최소한 이 개수의 Pod는 유지해야 한다"고 규정합니다.
Kubernetes는 이 규칙을 지키면서 노드 유지보수를 진행합니다. PDB가 없던 시절에는 어땠을까요?
클러스터 관리자들은 노드 업그레이드 시 수동으로 하나씩 드레인하고, 새 Pod가 완전히 준비될 때까지 기다렸다가 다음 노드로 넘어갔습니다. 자동화 스크립트를 만들어도 각 애플리케이션의 최소 요구사항을 하드코딩해야 했습니다.
더 큰 문제는 긴급 상황에서 실수로 모든 노드를 동시에 드레인하여 서비스가 중단되는 것이었습니다. 바로 이런 문제를 해결하기 위해 PodDisruptionBudget이 등장했습니다.
PDB를 설정하면 Kubernetes가 자동으로 안전한 속도로 노드를 드레인합니다. 또한 minAvailable이나 maxUnavailable을 지정하여 서비스별 요구사항을 명확히 할 수 있습니다.
무엇보다 인간의 실수를 시스템적으로 방지할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 PodDisruptionBudget 리소스를 정의합니다. minAvailable: 2는 항상 최소 2개의 Pod가 실행 중이어야 한다는 의미입니다.
만약 현재 4개의 Pod가 있다면, 최대 2개까지만 동시에 중단할 수 있습니다. 주석 처리된 maxUnavailable: 1은 다른 방식으로 표현한 것입니다.
이것은 "최대 1개까지만 동시에 중단 가능"하다는 뜻입니다. 두 가지 방식 중 하나를 선택하면 됩니다.
어떤 차이가 있을까요? minAvailable은 절대값이므로, replicas가 변경되어도 항상 2개가 유지됩니다.
반면 maxUnavailable은 비율로 지정할 수도 있습니다. 예를 들어 "25%"로 설정하면 replicas가 4개일 때는 1개, 8개일 때는 2개까지 중단 가능합니다.
확장성 있는 설정입니다. selector 섹션은 어떤 Pod들을 보호할지 지정합니다.
여기서는 app=production-app 레이블을 가진 Pod들을 대상으로 합니다. Deployment의 Pod template에 동일한 레이블이 있어야 합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 결제 서비스를 운영한다고 가정해봅시다.
이 서비스는 절대 중단되어서는 안 되므로 replicas 5개에 minAvailable 4로 설정합니다. 노드 업그레이드 시에도 항상 4개는 유지되므로 서비스 품질이 보장됩니다.
반면 배치 작업 같은 경우는 maxUnavailable: "50%"로 설정하여 빠르게 업그레이드할 수 있습니다. 일시적인 중단이 허용되기 때문입니다.
대규모 클러스터를 운영하는 기업들은 PDB를 필수로 설정합니다. AWS EKS, GKE 같은 관리형 Kubernetes도 노드 업그레이드 시 PDB를 존중합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 minAvailable을 replicas와 같게 설정하는 것입니다.
예를 들어 replicas 3에 minAvailable 3으로 설정하면, 노드 드레인이 영원히 진행되지 않습니다. 최소 1개는 중단 가능하도록 여유를 두어야 합니다.
또 다른 실수는 PDB를 설정하고 replicas는 1개만 운영하는 것입니다. replicas가 1개이고 minAvailable이 1이면 역시 노드 드레인이 불가능합니다.
고가용성이 필요하면 최소 2개 이상의 replicas를 운영해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 조언에 따라 김개발 씨는 PDB를 추가했습니다. replicas는 4개로 늘리고, minAvailable은 3으로 설정했습니다.
한 달 후, 또 다시 노드 업그레이드가 있었습니다. 이번에는 달랐습니다.
Kubernetes가 첫 번째 노드를 드레인하고 Pod 1개를 종료했습니다. 새 Pod가 시작되어 Ready 상태가 되자, 두 번째 노드를 드레인했습니다.
이런 식으로 안전하게 진행되었고, 서비스는 단 한 번도 중단되지 않았습니다. 김개발 씨는 모니터링 대시보드를 보며 미소 지었습니다.
"이제 새벽 전화를 받을 일이 없겠네요." PodDisruptionBudget을 제대로 이해하면 더 안정적이고 신뢰할 수 있는 운영이 가능합니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - minAvailable은 replicas보다 최소 1개 적게 설정하세요
- 고가용성이 필요하면 replicas는 최소 3개 이상으로 운영하세요
- maxUnavailable을 퍼센트로 설정하면 확장 시에도 자동 조정됩니다
4. NetworkPolicy
김개발 씨의 애플리케이션이 안정적으로 운영되던 어느 날, 보안팀에서 연락이 왔습니다. "당신의 Pod에서 외부 인터넷으로 이상한 트래픽이 나가고 있습니다." 조사 결과, 취약한 라이브러리를 통해 공격자가 침투한 것이었습니다.
박시니어 씨가 말했습니다. "NetworkPolicy를 설정했다면 이런 일은 없었을 텐데요."
NetworkPolicy는 Pod 간 네트워크 트래픽을 제어하는 방화벽 규칙입니다. 마치 건물의 출입 통제 시스템과 같습니다.
어떤 Pod가 어떤 Pod와 통신할 수 있는지, 외부와의 통신은 허용할지를 세밀하게 제어할 수 있습니다.
다음 코드를 살펴봅시다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: app-network-policy
namespace: production
spec:
# 이 정책이 적용될 Pod 선택
podSelector:
matchLabels:
app: production-app
policyTypes:
- Ingress # 들어오는 트래픽 제어
- Egress # 나가는 트래픽 제어
# 허용할 들어오는 트래픽
ingress:
- from:
- podSelector:
matchLabels:
role: frontend # frontend Pod만 접근 허용
ports:
- protocol: TCP
port: 8080
# 허용할 나가는 트래픽
egress:
- to:
- podSelector:
matchLabels:
role: database # database Pod에만 연결 허용
ports:
- protocol: TCP
port: 5432
- to: # DNS 조회 허용 (필수)
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: UDP
port: 53
보안팀의 설명에 김개발 씨는 식은땀을 흘렸습니다. "공격자가 제 Pod에서 다른 내부 서비스까지 스캔하고 있었습니다.
다행히 큰 피해는 없었지만, 만약 데이터베이스에 접근했다면 큰일 날 뻔했어요." 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다. "기본적으로 Kubernetes는 모든 Pod가 모든 Pod와 통신할 수 있어요.
이것은 편리하지만 보안상 위험합니다. 최소 권한 원칙을 적용해야 해요." NetworkPolicy란 정확히 무엇일까요?
쉽게 비유하자면, NetworkPolicy는 마치 회사의 출입증 시스템과 같습니다. 일반 직원은 자기 부서에만 출입할 수 있고, 서버실은 IT 담당자만 들어갈 수 있습니다.
마찬가지로 NetworkPolicy는 각 Pod가 필요한 통신만 할 수 있도록 제한합니다. 침입자가 하나의 Pod를 장악하더라도 다른 곳으로 이동할 수 없습니다.
NetworkPolicy가 없던 시절에는 어땠을까요? 초기 컨테이너 환경에서는 네트워크가 완전히 평평했습니다.
한 컨테이너가 해킹당하면 공격자는 내부 네트워크 전체를 탐색할 수 있었습니다. 특히 마이크로서비스 아키텍처에서는 수십 개의 서비스가 연결되어 있어 공격 표면이 매우 넓었습니다.
개발자들은 각 애플리케이션에 IP 테이블 규칙을 수동으로 설정해야 했습니다. 바로 이런 문제를 해결하기 위해 NetworkPolicy가 등장했습니다.
NetworkPolicy를 설정하면 기본 거부 원칙을 적용할 수 있습니다. 명시적으로 허용한 통신만 가능하고 나머지는 모두 차단됩니다.
또한 레이블 기반으로 동적으로 정책이 적용되어 Pod가 추가되거나 삭제되어도 자동으로 반영됩니다. 무엇보다 제로 트러스트 보안 모델을 구현할 수 있다는 큰 이점이 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 podSelector로 이 정책이 적용될 Pod를 선택합니다.
app=production-app 레이블을 가진 모든 Pod가 대상입니다. 만약 빈 값으로 설정하면 네임스페이스의 모든 Pod에 적용됩니다.
policyTypes에서 Ingress와 Egress 모두를 지정했습니다. Ingress는 들어오는 트래픽, Egress는 나가는 트래픽을 의미합니다.
둘 다 제어하는 것이 가장 안전합니다. ingress 섹션을 보면 role=frontend 레이블을 가진 Pod에서 오는 요청만 허용합니다.
포트는 TCP 8080번만 열려있습니다. 다른 모든 Pod나 외부에서의 접근은 차단됩니다.
egress 섹션에서는 나가는 트래픽을 제어합니다. 첫 번째 규칙은 role=database Pod에 TCP 5432 포트로만 연결할 수 있습니다.
데이터베이스 외의 다른 서비스나 외부 인터넷으로는 연결할 수 없습니다. 두 번째 egress 규칙은 DNS 조회를 허용합니다.
kube-system 네임스페이스의 DNS 서비스에 UDP 53 포트로 연결할 수 있습니다. 이것은 필수입니다.
DNS가 막히면 도메인 이름을 IP로 변환할 수 없어 아무것도 작동하지 않습니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 3-tier 아키텍처를 운영한다고 가정해봅시다. Frontend는 인터넷에서 접근 가능하지만 Backend로만 통신합니다.
Backend는 Frontend에서만 요청을 받고 Database로만 통신합니다. Database는 Backend에서만 요청을 받습니다.
이렇게 계층별로 NetworkPolicy를 설정하면, 한 계층이 뚫려도 다른 계층으로 침투할 수 없습니다. 금융권이나 의료 분야처럼 규제가 엄격한 산업에서는 NetworkPolicy가 필수입니다.
컴플라이언스 요구사항을 충족하는 데에도 도움이 됩니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 DNS egress를 빠뜨리는 것입니다. 정책을 적용하니 갑자기 모든 통신이 안 되고, 한참 디버깅한 끝에 DNS가 막혀있다는 것을 발견합니다.
또 다른 실수는 너무 복잡한 정책을 만드는 것입니다. 수십 개의 규칙을 하나의 NetworkPolicy에 넣으면 관리가 어렵습니다.
역할별로 정책을 분리하는 것이 좋습니다. 중요한 점은 CNI 플러그인이 NetworkPolicy를 지원해야 한다는 것입니다.
Calico, Cilium, Weave Net 같은 CNI는 지원하지만, 기본 Flannel은 지원하지 않습니다. 클러스터를 구성할 때 이것을 고려해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 보안팀과 협의하여 김개발 씨는 엄격한 NetworkPolicy를 작성했습니다.
처음에는 기존 통신이 막혀서 몇 번 수정했지만, 결국 완벽한 정책을 완성했습니다. 적용 후 모니터링 결과, 차단된 연결 시도가 로그에 기록되었습니다.
대부분 정상적인 것들이었지만, 어떤 Pod가 Redis에 무단 접근하려는 시도도 발견되었습니다. NetworkPolicy 덕분에 차단된 것입니다.
김개발 씨는 이제 더 안심하고 잠을 잘 수 있게 되었습니다. "보안이란 이렇게 계층을 쌓는 거군요." NetworkPolicy를 제대로 이해하면 더 안전하고 견고한 인프라를 구축할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 처음에는 로그만 기록하고 점진적으로 차단 정책을 강화하세요
- DNS egress는 항상 허용해야 합니다
- 네임스페이스별로 기본 거부 정책을 먼저 만들고 필요한 것만 허용하세요
5. RBAC 설정
김개발 씨의 팀에 인턴 이주니어 씨가 합류했습니다. 김개발 씨는 kubectl 사용법을 가르쳐주려고 자신의 kubeconfig 파일을 복사해주었습니다.
다음 날 아침, 프로덕션 네임스페이스의 모든 Pod가 삭제되어 있었습니다. 이주니어 씨가 실수로 kubectl delete pods --all 명령을 실행한 것입니다.
박시니어 씨가 한숨을 쉬었습니다. "RBAC를 제대로 설정했어야죠."
RBAC는 Role-Based Access Control의 약자로, 사용자나 서비스가 Kubernetes 리소스에 대해 수행할 수 있는 작업을 제어하는 권한 관리 시스템입니다. 마치 회사의 직급별 권한 체계와 같습니다.
최소 권한 원칙으로 보안을 강화할 수 있습니다.
다음 코드를 살펴봅시다.
# 읽기 전용 Role 정의
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: production
rules:
- apiGroups: [""] # 코어 API 그룹
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"] # 읽기만 가능
---
# 개발자에게 Role 바인딩
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: production
subjects:
- kind: User
name: junior-dev # 이주니어 계정
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader # 위에서 정의한 Role
apiGroup: rbac.authorization.k8s.io
---
# 배포 권한을 가진 Role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer
namespace: production
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "update", "patch"] # 배포는 가능, 삭제는 불가
사고 수습 후 회의가 열렸습니다. 김개발 씨는 자책했습니다.
"제가 admin 권한을 그대로 줘서 이런 일이..." 박시니어 씨가 말했습니다. "이것은 시스템의 문제예요.
인간은 실수하기 마련입니다. 그래서 권한 관리가 중요한 겁니다." 실제로 Kubernetes 보안 사고의 상당수가 과도한 권한에서 비롯됩니다.
공격자가 한 계정을 탈취하면 전체 클러스터를 장악할 수 있는 것입니다. RBAC란 정확히 무엇일까요?
쉽게 비유하자면, RBAC는 마치 병원의 권한 체계와 같습니다. 의사는 환자 차트를 읽고 쓸 수 있습니다.
간호사는 읽을 수 있지만 일부만 수정할 수 있습니다. 접수 직원은 기본 정보만 볼 수 있습니다.
청소 직원은 의료 기록에 전혀 접근할 수 없습니다. 각자의 역할에 필요한 최소한의 권한만 갖는 것입니다.
RBAC가 없던 시절에는 어땠을까요? 초기 Kubernetes는 Attribute-Based Access Control을 사용했습니다.
설정이 복잡하고 실수하기 쉬웠습니다. 많은 팀이 그냥 모든 사용자에게 cluster-admin 권한을 주었습니다.
편하지만 위험했습니다. 한 번의 실수로 전체 클러스터가 마비될 수 있었고, 누가 무엇을 했는지 추적하기도 어려웠습니다.
바로 이런 문제를 해결하기 위해 RBAC가 도입되었습니다. RBAC를 사용하면 역할 기반으로 권한을 관리할 수 있습니다.
개발자 역할, 운영자 역할, 모니터링 역할 등을 정의하고 사용자나 서비스 계정에 할당합니다. 또한 네임스페이스 단위로 권한을 분리할 수 있어 멀티 테넌시를 구현할 수 있습니다.
무엇보다 감사 로그와 결합하여 누가 언제 무엇을 했는지 추적할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 Role 리소스를 정의합니다. name: pod-reader는 이 역할의 이름입니다.
namespace: production은 이 역할이 production 네임스페이스에만 적용된다는 뜻입니다. 다른 네임스페이스에는 영향을 주지 않습니다.
rules 섹션에서 권한을 정의합니다. **apiGroups: [""]**는 빈 문자열로, 코어 API 그룹을 의미합니다.
Pod, Service 같은 기본 리소스들이 여기 속합니다. **resources: ["pods", "pods/log"]**는 이 역할이 관리하는 리소스입니다.
Pod와 Pod의 로그를 대상으로 합니다. verbs는 수행할 수 있는 동작입니다.
get은 개별 리소스 조회, list는 목록 조회, watch는 변경 사항 실시간 감시를 의미합니다. create, update, delete는 포함되지 않았으므로 읽기 전용입니다.
다음으로 RoleBinding을 정의합니다. 이것은 Role을 실제 사용자에게 연결하는 것입니다.
subjects에서 junior-dev 사용자를 지정하고, roleRef에서 pod-reader Role을 참조합니다. 이제 이주니어 씨는 production 네임스페이스의 Pod를 조회하고 로그를 볼 수는 있지만, 삭제하거나 수정할 수는 없습니다.
세 번째 예시는 deployer Role입니다. apiGroups: ["apps"]는 Deployment, StatefulSet 같은 애플리케이션 리소스를 의미합니다.
verbs에 update와 patch가 있어서 배포를 업데이트할 수 있지만, delete가 없어서 삭제는 할 수 없습니다. 실제 현업에서는 어떻게 활용할까요?
대부분의 기업은 여러 등급의 Role을 정의합니다. viewer는 읽기만, developer는 자기 팀 네임스페이스에서 배포 가능, sre는 프로덕션 네임스페이스 관리 가능, admin은 클러스터 전체 관리 가능 이런 식입니다.
또한 애플리케이션 Pod도 ServiceAccount를 통해 RBAC를 적용받습니다. 예를 들어 CI/CD 파이프라인의 ServiceAccount는 Deployment를 업데이트할 수 있지만, Secret은 읽을 수 없게 제한합니다.
이렇게 하면 파이프라인이 해킹당해도 민감한 정보는 보호됩니다. 금융, 의료, 공공 분야에서는 컴플라이언스 요구사항으로 RBAC가 필수입니다.
모든 접근이 로그에 남고 정기적으로 감사받습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 너무 세밀한 권한을 만드는 것입니다. 리소스와 동작의 모든 조합에 대해 Role을 만들면 관리가 불가능해집니다.
실용적인 수준의 역할 몇 개만 정의하는 것이 좋습니다. 또 다른 실수는 ClusterRole과 Role을 혼동하는 것입니다.
Role은 특정 네임스페이스에만 적용되고, ClusterRole은 클러스터 전체나 네임스페이스 외부 리소스에 적용됩니다. 잘못 사용하면 의도치 않은 권한을 부여하게 됩니다.
중요한 점은 기본적으로 모든 권한이 거부된다는 것입니다. 명시적으로 허용한 것만 가능합니다.
따라서 새 사용자에게 아무 Role도 주지 않으면 아무것도 할 수 없습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
사고 후 팀은 체계적인 RBAC를 구축했습니다. 이주니어 씨에게는 pod-reader Role을 부여했습니다.
이제 조회와 로그 확인은 할 수 있지만, 삭제나 수정은 불가능합니다. 김개발 씨에게는 deployer Role을 부여했습니다.
자기 팀의 네임스페이스에서 배포를 업데이트할 수 있지만, 삭제는 시니어의 승인이 필요합니다. 박시니어 씨는 sre Role로 프로덕션을 관리하고, 팀장만 cluster-admin 권한을 갖습니다.
몇 주 후, 이주니어 씨가 또 실수로 잘못된 명령을 입력했습니다. 하지만 이번에는 "Forbidden" 에러가 반환되었습니다.
아무 일도 일어나지 않았습니다. 시스템이 실수를 막아준 것입니다.
RBAC를 제대로 이해하면 더 안전하고 관리하기 쉬운 클러스터를 운영할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 기본은 최소 권한, 필요할 때만 추가하세요
- 네임스페이스별로 권한을 분리하여 멀티 테넌시를 구현하세요
- ServiceAccount에도 RBAC를 적용하여 애플리케이션 권한을 제한하세요
6. 로그 수집
어느 날 심야에 장애가 발생했습니다. 사용자들이 에러 페이지를 보고 있다는 제보가 쏟아졌습니다.
김개발 씨가 급히 접속하여 Pod 로그를 확인하려 했지만, 문제의 Pod는 이미 OOMKilled되어 재시작된 상태였습니다. 로그도 함께 사라져서 원인을 알 수 없었습니다.
박시니어 씨가 한마디 했습니다. "중앙 로그 수집 시스템을 구축했어야 했는데요."
로그 수집은 분산된 컨테이너의 로그를 중앙 저장소에 모아서 검색하고 분석할 수 있게 하는 시스템입니다. 마치 CCTV 영상을 녹화하여 나중에 확인하는 것과 같습니다.
Pod가 사라져도 로그는 남아있어 문제 원인을 추적할 수 있습니다.
다음 코드를 살펴봅시다.
apiVersion: v1
kind: Pod
metadata:
name: production-app
spec:
containers:
- name: app
image: myapp:1.0
# 구조화된 JSON 로그 출력
env:
- name: LOG_FORMAT
value: "json"
- name: LOG_LEVEL
value: "info"
# 사이드카 패턴으로 로그 수집
- name: fluent-bit
image: fluent/fluent-bit:2.0
volumeMounts:
- name: varlog
mountPath: /var/log
- name: config
mountPath: /fluent-bit/etc/
volumes:
- name: varlog
hostPath:
path: /var/log
- name: config
configMap:
name: fluent-bit-config
---
# Fluent Bit 설정
apiVersion: v1
kind: ConfigMap
metadata:
name: fluent-bit-config
data:
fluent-bit.conf: |
[INPUT]
Name tail
Path /var/log/containers/*.log
Parser docker
Tag kube.*
[OUTPUT]
Name es
Match *
Host elasticsearch.logging.svc
Port 9200
Index kubernetes-logs
김개발 씨는 좌절했습니다. "로그만 있었다면 5분 안에 원인을 찾았을 텐데..." 새벽 내내 코드를 뒤지고 테스트를 반복한 끝에 겨우 원인을 찾았습니다.
메모리 누수였습니다. 로그에 분명히 경고가 있었을 텐데 확인할 방법이 없었습니다.
박시니어 씨가 다음 날 장기적인 해결책을 제시했습니다. "프로덕션 환경에서는 로그가 사라지면 안 돼요.
중앙 집중식 로그 시스템을 구축해야 합니다." 로그 수집이란 정확히 무엇일까요? 쉽게 비유하자면, 로그 수집은 마치 도서관 시스템과 같습니다.
각 교실에서 쓴 일지를 모두 도서관에 보내서 보관하고 색인을 만드는 것입니다. 나중에 "3월 15일에 무슨 일이 있었지?"라고 물으면 검색하여 찾을 수 있습니다.
교실이 없어져도 일지는 도서관에 남아있습니다. Kubernetes에서는 각 Pod가 교실이고, 로그 수집 시스템이 도서관입니다.
로그 수집이 없던 시절에는 어땠을까요? 초기에는 개발자들이 각 서버에 SSH로 접속하여 로그 파일을 직접 확인했습니다.
서버가 많아지면 하나하나 확인하기 불가능했습니다. 더 큰 문제는 컨테이너가 재시작되거나 삭제되면 로그도 함께 사라진다는 것입니다.
장애 발생 후 원인을 찾을 방법이 없었습니다. 또한 마이크로서비스에서는 한 요청이 여러 서비스를 거치므로, 각 서비스의 로그를 따로 보면 전체 흐름을 파악하기 어려웠습니다.
바로 이런 문제를 해결하기 위해 중앙 로그 수집 시스템이 필수가 되었습니다. 로그 수집 시스템을 구축하면 모든 Pod의 로그가 한곳에 저장됩니다.
Pod가 삭제되어도 로그는 영구 보관됩니다. 또한 검색과 필터링이 가능하여 특정 에러나 패턴을 빠르게 찾을 수 있습니다.
무엇보다 분산 추적을 통해 여러 서비스에 걸친 요청의 전체 경로를 볼 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 애플리케이션 컨테이너를 보면 환경 변수로 로그 설정을 합니다. LOG_FORMAT: json은 로그를 구조화된 JSON 형식으로 출력하라는 뜻입니다.
이렇게 하면 나중에 파싱하기 쉽습니다. timestamp, level, message, user_id 같은 필드로 구조화됩니다.
다음으로 사이드카 컨테이너인 fluent-bit을 추가합니다. 사이드카 패턴은 메인 애플리케이션 옆에 보조 컨테이너를 붙이는 것입니다.
fluent-bit은 로그를 수집하여 Elasticsearch로 전송하는 역할을 합니다. volumeMounts로 /var/log를 마운트합니다.
Kubernetes는 컨테이너 로그를 이 경로에 저장하므로, fluent-bit이 여기서 로그를 읽을 수 있습니다. 또한 configMap을 마운트하여 fluent-bit의 설정을 주입합니다.
ConfigMap의 fluent-bit.conf를 보면, INPUT 섹션에서 /var/log/containers/*.log 파일들을 감시합니다. tail 입력 플러그인은 파일의 끝을 계속 따라가며 새 로그를 읽습니다.
Parser는 docker 형식으로 로그를 파싱합니다. OUTPUT 섹션에서는 Elasticsearch로 로그를 전송합니다.
elasticsearch.logging.svc는 같은 클러스터의 Elasticsearch 서비스를 가리킵니다. Index는 kubernetes-logs로 설정하여 날짜별로 인덱스가 생성됩니다.
실제 현업에서는 어떻게 활용할까요? 대부분의 기업은 ELK 스택이나 EFK 스택을 사용합니다.
ELK는 Elasticsearch, Logstash, Kibana의 조합이고, EFK는 Logstash 대신 Fluentd를 사용합니다. Kibana는 웹 UI로 로그를 검색하고 시각화합니다.
예를 들어 사용자가 "결제가 실패했어요"라고 신고하면, 개발자는 Kibana에서 해당 사용자의 user_id로 검색합니다. 결제 서비스, 포인트 서비스, 알림 서비스의 로그를 시간순으로 보며 어디서 문제가 생겼는지 추적합니다.
trace_id를 사용하면 한 요청의 전체 경로를 자동으로 연결할 수 있습니다. 또한 로그를 분석하여 알람을 설정할 수 있습니다.
에러 로그가 분당 100개를 넘으면 Slack으로 알림을 보내는 식입니다. 장애를 조기에 발견할 수 있습니다.
클라우드 환경에서는 AWS CloudWatch Logs, GCP Cloud Logging, Azure Monitor 같은 관리형 서비스를 사용할 수도 있습니다. 직접 구축하는 것보다 간편하지만 비용이 더 들 수 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 로그를 남기는 것입니다.
모든 요청을 debug 레벨로 로그하면 로그 저장소가 금방 가득 차고 비용이 폭증합니다. 프로덕션에서는 info 레벨 이상만 남기고, 필요시 일시적으로 debug를 활성화하세요.
또 다른 실수는 민감한 정보를 로그에 남기는 것입니다. 비밀번호, 신용카드 번호, 주민등록번호 같은 것이 로그에 찍히면 보안 사고입니다.
로그에 남기기 전에 마스킹하거나 제거해야 합니다. 중요한 점은 로그와 메트릭과 트레이싱을 함께 사용해야 완전한 가시성을 얻는다는 것입니다.
로그는 무슨 일이 일어났는지, 메트릭은 얼마나 빠르고 얼마나 많이, 트레이싱은 어디를 거쳤는지를 알려줍니다. 이 셋을 Observability의 3대 요소라고 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 도움으로 김개발 씨는 EFK 스택을 구축했습니다.
모든 Pod에 fluent-bit 사이드카를 추가하고, 애플리케이션 코드를 수정하여 구조화된 JSON 로그를 출력하도록 했습니다. 며칠 후, 또 다시 장애가 발생했습니다.
하지만 이번에는 달랐습니다. 김개발 씨는 Kibana를 열고 에러 로그를 검색했습니다.
몇 초 만에 원인을 찾았습니다. 외부 API가 타임아웃을 반환하고 있었습니다.
해당 서비스 제공자에게 연락하여 10분 만에 문제를 해결했습니다. 김개발 씨는 만족스럽게 미소 지었습니다.
"이제 로그가 사라질 걱정은 없겠네요. 언제든 원인을 찾을 수 있어요." 로그 수집을 제대로 이해하면 더 빠르게 문제를 해결하고 시스템을 이해할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 프로덕션에서는 info 레벨 이상만 로그하여 비용을 절감하세요
- trace_id를 사용하여 분산 서비스 간 로그를 연결하세요
- 민감한 정보는 로그에 남기기 전에 반드시 마스킹하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
이스티오 기반 마이크로서비스 플랫폼 완벽 가이드
Kubernetes와 Istio를 활용한 엔터프라이즈급 마이크로서비스 플랫폼 구축 방법을 실전 프로젝트로 배웁니다. Helm 차트 작성부터 트래픽 관리, 보안, 모니터링까지 전체 과정을 다룹니다.
오토스케일링 완벽 가이드
트래픽 변화에 자동으로 대응하는 오토스케일링의 모든 것을 배웁니다. HPA, VPA, Cluster Autoscaler까지 실전 예제와 함께 쉽게 설명합니다. 초급 개발자도 술술 읽히는 실무 중심 가이드입니다.
카나리 배포와 블루/그린 배포 완벽 가이드
실서비스에서 안전하게 배포하는 두 가지 핵심 전략을 배웁니다. 카나리 배포로 점진적으로 트래픽을 전환하고, 블루/그린 배포로 즉각적인 롤백을 구현하는 방법을 실무 예제와 함께 익혀봅니다.
Istio 관찰 가능성 완벽 가이드
Istio 서비스 메시의 관찰 가능성을 위한 핵심 도구들을 다룹니다. Kiali 대시보드부터 Jaeger 분산 추적, Prometheus 메트릭, Grafana 연동까지 실무에서 바로 활용할 수 있는 모니터링 전략을 배웁니다.
Istio 보안 완벽 가이드
마이크로서비스 환경에서 필수적인 Istio 보안 기능을 실무 중심으로 설명합니다. mTLS부터 인증, 인가까지 단계별로 학습하여 안전한 서비스 메시를 구축할 수 있습니다.