목록으로 가기

Helm 차트 관리와 Ingress 설정, K8s 트러블슈팅 방법

Helm으로 패키지 관리하는 법, Ingress Controller 설정, Pod가 안 뜰 때 확인하는 디버깅 패턴까지 정리했습니다.

Helm 차트 관리와 Ingress 설정, K8s 트러블슈팅 방법 ko posts kubernetes Helm으로 패키지 관리하는 법, Ingress Controller 설정, Pod가 안 뜰 때 확인하는 디버깅 패턴까지 정리했습니다.

개요

심화 1편이 “Pod를 안정적으로 운영하기 위한 설정"이었다면, 이 글은 “여러 서비스를 효율적으로 관리하고, 문제가 생겼을 때 빠르게 찾는 방법"에 초점을 둡니다.

Helm으로 반복 배포를 줄이고, Ingress Controller로 외부 트래픽을 세밀하게 제어하고, 장애 상황에서 원인을 빠르게 좁히는 패턴을 정리합니다.

1. Helm — 패키지 관리

정리 보기

Helm = K8s의 패키지 매니저

환경별로 값이 다른 YAML을 각각 관리하면 파일 수가 환경 수만큼 늘어납니다. Helm은 이걸 하나의 패키지(Chart)로 묶어서 변수만 바꿔 배포할 수 있게 합니다.

핵심 용어

용어
Chart 패키지 (YAML 템플릿 묶음)
Release Chart를 클러스터에 설치한 인스턴스
Repository Chart를 저장/공유하는 저장소
Values Chart에 주입하는 설정값

Chart 디렉터리 구조

my-app/
├── Chart.yaml          # 이름, 버전, 설명
├── values.yaml         # 기본 설정값
├── templates/          # K8s YAML 템플릿
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   └── _helpers.tpl    # 공통 헬퍼 함수
└── charts/             # 의존 Chart

values.yaml 예시

replicaCount: 3
image:
  repository: myapp
  tag: "1.0.0"
  pullPolicy: IfNotPresent
service:
  type: ClusterIP
  port: 80
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi
ingress:
  enabled: true
  host: api.example.com

templates/deployment.yaml (템플릿)

값은 {{ .Values.xxx }}로 values.yaml에서 가져옵니다. helm template 명령어로 실제 배포 전에 렌더링 결과를 미리 확인할 수 있습니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-app.fullname" . }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ include "my-app.name" . }}
  template:
    metadata:
      labels:
        app: {{ include "my-app.name" . }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: {{ .Values.service.port }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
# 렌더링 결과 미리보기 (배포 안 함)
helm template my-release ./my-app -f values-prod.yaml

기본 명령어

# Chart 설치
helm install my-release ./my-app

# 환경별 values로 설치
helm install my-release ./my-app -f values-prod.yaml

# 업그레이드 (값 변경 or 이미지 태그 변경)
helm upgrade my-release ./my-app --set image.tag=2.0.0

# 롤백
helm rollback my-release 1

# 릴리즈 목록
helm list

# 릴리즈 상태
helm status my-release

# 삭제
helm uninstall my-release

# 템플릿 렌더링 미리보기 (실제 배포 안 함)
helm template my-release ./my-app -f values-prod.yaml

공개 Chart 사용 (Prometheus, nginx 등)

# 레포지토리 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# 설치
helm install prometheus prometheus-community/kube-prometheus-stack -f custom-values.yaml

# 어떤 values가 있는지 확인
helm show values prometheus-community/kube-prometheus-stack > default-values.yaml

환경별 관리 패턴

my-app/
├── values.yaml           # 공통 기본값
├── values-dev.yaml       # dev 환경 오버라이드
├── values-stg.yaml       # staging
└── values-prod.yaml      # production
# dev 배포
helm upgrade --install my-app ./my-app -f values-dev.yaml -n dev

# prod 배포
helm upgrade --install my-app ./my-app -f values-prod.yaml -n prod

-i, --install은 같은 이름의 릴리즈가 없으면 install을 수행하고 있으면 upgrade를 수행합니다. 설치와 업그레이드를 한 명령으로 처리합니다.

2. Ingress Controller 심화

정리 보기

기본편 복습: Ingress는 규칙이고, Ingress Controller가 실제로 동작한다

여기서는 nginx Ingress Controller를 예로 듭니다. 설치하면 nginx가 Pod로 뜨면서 Ingress 리소스의 규칙대로 트래픽을 라우팅합니다.

설치 (Helm으로)

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespace

TLS (HTTPS) 설정

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-ingress
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
      secretName: api-tls-secret    # TLS 인증서가 담긴 Secret
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 80

cert-manager로 인증서 자동 발급 (Let’s Encrypt)

helm repo add jetstack https://charts.jetstack.io --force-update
helm install cert-manager jetstack/cert-manager --set crds.enabled=true -n cert-manager --create-namespace

CRD 설치 옵션은 crds.enabled입니다. 예전 자료에 나오는 installCRDs도 동작하지만, 최신 차트(v1.16+)에서는 crds.enabled를 사용합니다.

# ClusterIssuer 생성
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

HTTP-01 solver에서 Ingress Controller를 지정하는 방법은 세 가지(ingressClassName, class, name)인데, 공식 문서는 ingressClassName을 권장합니다(cert-manager 1.12에 추가된 필드). classingressClassName을 지원하지 않는 ingress-gce에 권장되는 방식입니다. 세 필드를 모두 비워두면 cert-manager가 ingress class 없이 Ingress를 만들기 때문에 클러스터의 모든 Ingress Controller가 챌린지 트래픽을 처리하게 됩니다.

# Ingress에 어노테이션 추가하면 자동 발급
metadata:
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"

어노테이션

어노테이션 용도
nginx.ingress.kubernetes.io/proxy-body-size 요청 본문 최대 크기 (ConfigMap proxy-body-size 기본값 1m)
nginx.ingress.kubernetes.io/proxy-read-timeout 백엔드 응답 대기 시간 (단위 없는 초)
nginx.ingress.kubernetes.io/limit-rps IP당 초당 요청 수 제한 (컨트롤러 레플리카 단위로 적용)
nginx.ingress.kubernetes.io/rewrite-target URL 경로 재작성
nginx.ingress.kubernetes.io/cors-allow-origin 허용할 Origin 지정 (enable-cors: "true"와 함께 사용, 기본값 *)
nginx.ingress.kubernetes.io/affinity 세션 어피니티 (nginx에서 사용 가능한 값은 cookie)

limit-rps는 컨트롤러 레플리카별로 적용되기 때문에, 레플리카가 여러 개면 전체 허용량은 레플리카 수만큼 곱해집니다. HPA를 붙여 놓으면 레플리카 수가 고정되지 않으므로 전체 허용량도 함께 변합니다.

# 파일 업로드 제한 늘리기 + 타임아웃 늘리기
metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "120"

경로 기반 라우팅

spec:
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-service
                port:
                  number: 3000

하나의 도메인에서 /api는 백엔드로, /는 프론트로 보내는 구조입니다.

디버깅

# Ingress Controller 로그
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx -f

# Ingress 설정이 실제로 어떻게 반영됐는지
kubectl exec -n ingress-nginx <nginx-pod> -- cat /etc/nginx/nginx.conf | grep -A 20 "server_name"

3. Troubleshooting

정리 보기

기본 디버깅 흐름

문제 발생
  → kubectl get pods (상태 확인)
  → kubectl describe pod (이벤트 확인)
  → kubectl logs (앱 로그 확인)
  → 원인별 대응

Pod 상태와 확인 명령어

상태 원인 확인 방법
Pending 노드 자원 부족 or 스케줄링 조건 불일치 kubectl describe pod → Events
CrashLoopBackOff 컨테이너가 종료된 뒤 재시작을 반복하는 상태 kubectl logs --previous
ImagePullBackOff 이미지 못 가져옴 (이름 틀림, 권한 없음) kubectl describe pod → Events
OOMKilled 메모리 limit 초과 kubectl describe pod → Last State
Evicted 노드 디스크/메모리 부족으로 축출됨 kubectl describe pod → Events
Terminating (멈춤) graceful shutdown 실패, finalizer 걸림 kubectl get pod -o yaml → finalizers, deletionTimestamp

1. Pending — Pod가 안 뜸

kubectl describe pod <이름>
# Events 섹션 확인

Events 메시지별 원인:

Events 메시지 원인
Insufficient cpu/memory 노드에 자원 없음
no nodes available 스케줄 가능한 노드 없음
didn't match Pod's node selector nodeSelector 불일치
persistentvolumeclaim not found PVC 없거나 안 붙음

2. CrashLoopBackOff — 계속 죽음

# 현재 로그
kubectl logs <pod>

# 이전 크래시 로그 (죽기 직전)
kubectl logs <pod> --previous

# 컨테이너 안에서 직접 확인
kubectl run debug --image=<같은이미지> -it --rm -- /bin/sh

kubectl logs --previous는 재시작 이전 컨테이너 인스턴스의 로그를 출력하므로, 종료 시점에 남은 로그를 확인할 수 있습니다.


3. ImagePullBackOff — 이미지 못 가져옴

kubectl describe pod <이름>
# Events: Failed to pull image "myapp:1.0" ... unauthorized

Events에 기록되는 원인:

  • 이미지 이름/태그 오타
  • private registry 인증 없음
  • 이미지가 존재하지 않음
# imagePullSecrets 설정
spec:
  imagePullSecrets:
    - name: registry-secret
# Secret 생성
kubectl create secret docker-registry registry-secret \
  --docker-server=<registry-url> \
  --docker-username=<user> \
  --docker-password=<pass>

4. Service 연결 안 됨

# 1. Pod가 실제로 떠있는지
kubectl get pods -l app=web

# 2. Service의 Endpoints에 Pod IP가 있는지
kubectl get endpoints web-service
# → Endpoints가 비어있으면 selector가 Pod label과 불일치

# 3. Service selector와 Pod label 비교
kubectl get svc web-service -o yaml | grep -A 3 selector
kubectl get pods --show-labels

# 4. Pod 안에서 직접 테스트
kubectl exec -it <다른pod> -- curl http://web-service:80

5. Ingress 접근 안 됨

# 1. Ingress 설정 확인
kubectl describe ingress <이름>

# 2. Ingress Controller 실행 중인지
kubectl get pods -n ingress-nginx

# 3. Ingress Controller 로그
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx

# 4. Service가 정상인지 (위 4번 참고)

# 5. 인증서 문제인지
kubectl get certificate -A
kubectl describe certificate <이름>

6. 노드 문제

# 노드 상태
kubectl get nodes
# STATUS 열에서 Ready / NotReady 확인

# 노드 상세 (Conditions 섹션)
kubectl describe node <이름>
# MemoryPressure, DiskPressure, PIDPressure 확인

# 노드의 Pod 목록
kubectl get pods --field-selector spec.nodeName=<노드이름> -A

디버깅용 임시 Pod 띄우기

# 네트워크 테스트용
kubectl run debug-net --image=nicolaka/netshoot -it --rm -- /bin/bash
# 안에서: curl, dig, ping, traceroute 등 가능

# 특정 namespace에서
kubectl run debug -n production --image=busybox -it --rm -- /bin/sh

4. 리소스 쿼터와 LimitRange

정리 보기

ResourceQuota = 네임스페이스 전체의 자원 상한선

팀별로 네임스페이스를 나눴을 때, 한 팀이 클러스터 자원을 다 쓰는 걸 방지합니다.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-quota
  namespace: team-a
spec:
  hard:
    requests.cpu: "4"          # 전체 requests CPU 합계 4코어까지
    requests.memory: "8Gi"     # 전체 requests 메모리 8Gi까지
    limits.cpu: "8"
    limits.memory: "16Gi"
    pods: "20"                 # Pod 최대 20개
    services: "10"

LimitRange = 개별 Pod/컨테이너의 기본값/상한

requests/limits를 안 넣은 Pod에 기본값을 강제합니다.

apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: team-a
spec:
  limits:
    - type: Container
      default:              # limits 기본값
        cpu: "500m"
        memory: "256Mi"
      defaultRequest:       # requests 기본값
        cpu: "100m"
        memory: "128Mi"
      max:                  # 최대 허용
        cpu: "2"
        memory: "2Gi"
      min:                  # 최소 허용
        cpu: "50m"
        memory: "64Mi"

적용 범위 차이

  • ResourceQuota: 네임스페이스 전체의 requests/limits 합계와 오브젝트 개수에 상한을 둠
  • LimitRange: requests/limits가 없는 컨테이너에 기본값을 채우고, 컨테이너별 최소/최대 값을 검증함
# 네임스페이스 쿼터 확인
kubectl get resourcequota -n team-a
kubectl describe resourcequota team-quota -n team-a

# LimitRange 확인
kubectl get limitrange -n team-a

5. Pod Disruption Budget (PDB)

정리 보기

PDB = “최소 몇 개는 항상 떠있어야 한다"는 보호 규칙

노드 유지보수(kubectl drain), 클러스터 업그레이드 시 K8s가 Pod를 내리는데, PDB를 설정하면 동시에 너무 많이 내리지 못하도록 제한합니다.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-pdb
spec:
  minAvailable: 2          # 최소 2개는 항상 살아있어야 함
  # 또는
  # maxUnavailable: 1      # 동시에 최대 1개까지만 내릴 수 있음
  selector:
    matchLabels:
      app: web

언제 동작하냐

상황 PDB 적용
kubectl drain node (노드 유지보수) 적용됨
클러스터 오토스케일러가 노드 축소 적용됨
Pod가 스스로 죽음 (OOM, 앱 에러) 적용 안 됨
kubectl delete pod 적용 안 됨

PDB는 **자발적 축출(voluntary disruption)**에만 적용됩니다. 앱 자체 에러로 죽는 건 PDB와 관계없습니다.

# PDB 상태 확인
kubectl get pdb

# 상세 (현재 몇 개 available인지)
kubectl describe pdb web-pdb

6. 멀티 클러스터와 환경 분리

정리 보기

클러스터를 어떻게 나누냐

전략 구조 격리 범위
단일 클러스터 + 네임스페이스 분리 1 클러스터, dev/stg/prod 네임스페이스 네임스페이스 단위
환경별 클러스터 dev 클러스터, prod 클러스터 컨트롤 플레인과 노드까지 분리
팀별 + 환경별 팀A-prod, 팀B-prod 등 팀 단위로 클러스터 분리

kubectl context로 클러스터 전환

# 현재 context 확인
kubectl config current-context

# context 목록
kubectl config get-contexts

# context 전환
kubectl config use-context prod-cluster

# 특정 context로 명령어 실행 (전환 없이)
kubectl --context=dev-cluster get pods

kubeconfig 구조 (~/.kube/config)

apiVersion: v1
kind: Config
clusters:
  - name: dev-cluster
    cluster:
      server: https://dev-k8s.example.com
      certificate-authority-data: ...
  - name: prod-cluster
    cluster:
      server: https://prod-k8s.example.com
      certificate-authority-data: ...
contexts:
  - name: dev
    context:
      cluster: dev-cluster
      user: dev-user
      namespace: default
  - name: prod
    context:
      cluster: prod-cluster
      user: prod-user
      namespace: default
users:
  - name: dev-user
    user:
      token: ...
current-context: dev

kubectx + kubens (편의 도구)

# 클러스터 전환 (kubectx)
kubectx prod-cluster

# 네임스페이스 전환 (kubens)
kubens production

kubectx는 context 전환, kubens는 네임스페이스 전환을 짧은 명령으로 수행하는 별도 도구입니다.

7. 보안 강화 — Pod Security

정리 보기

securityContext = Pod/컨테이너 레벨 보안 설정

root로 실행하지 않기, 파일시스템 읽기 전용 등 컨테이너 보안을 강제합니다.

spec:
  securityContext:
    runAsNonRoot: true           # root 실행 금지
    runAsUser: 1000              # UID 1000으로 실행
    fsGroup: 2000                # 볼륨 그룹 ID
  containers:
    - name: app
      image: myapp:1.0
      securityContext:
        allowPrivilegeEscalation: false  # 권한 상승 금지
        readOnlyRootFilesystem: true     # 루트 파일시스템 읽기 전용
        capabilities:
          drop: ["ALL"]                  # 모든 Linux capability 제거

각 필드의 동작

  • runAsNonRoot: true: 컨테이너가 UID 0으로 실행되면 kubelet이 시작을 거부함
  • readOnlyRootFilesystem: true: 컨테이너 루트 파일시스템을 읽기 전용으로 마운트
  • capabilities.drop: ["ALL"]: 컨테이너에 부여되는 Linux capability를 모두 제거

Pod Security Standards (PSS)

PSS는 정책 3단계를 정의한 문서이고, 이걸 실제로 강제하는 건 Pod Security Admission(PSA) 컨트롤러입니다. PSA는 1.22 알파, 1.23부터 기본 제공(베타), 1.25부터 GA입니다. 정책은 네임스페이스 라벨로 적용됩니다.

레벨 제한
Privileged 제한 없는 정책. 모드 라벨이 없을 때 적용되는 기본값
Baseline 알려진 권한 상승을 막는 최소 제한 (hostNetwork, hostPID, privileged, hostPath 등 차단)
Restricted 가장 엄격 (non-root, allowPrivilegeEscalation: false, capabilities drop ALL, seccomp 지정 강제)

모드는 enforce(위반 시 Pod 거부), audit(감사 로그에만 기록), warn(경고만) 세 가지이고, 라벨이 없을 때 쓰이는 기본 레벨은 세 모드 모두 privileged입니다.

# 네임스페이스에 보안 레벨 적용
kubectl label namespace production pod-security.kubernetes.io/enforce=restricted

관련 포스트

Kubernetes Probe, HPA, RBAC — 상태 점검, 오토스케일링, 접근 제어 헬스체크(Probe), 리소스 요청/제한, 오토스케일링(HPA), 무중단 배포, 접근 제어(RBAC)까지. … Kubernetes 클러스터 구조와 핵심 오브젝트 이해하기 Pod, Service, Deployment가 뭔지, 클러스터는 어떻게 구성되는지, kubectl 명령어까지 …