Skip to content
Published on

Docker & Kubernetes 입문 완전 가이드 — 컨테이너부터 오케스트레이션까지

Authors

1. 컨테이너란 무엇인가

가상 머신과 컨테이너의 차이

전통적인 가상 머신(VM)은 하이퍼바이저 위에서 게스트 OS 전체를 실행합니다. 각 VM마다 커널, 라이브러리, 바이너리를 모두 포함하므로 수 GB의 디스크와 수십 초의 부팅 시간이 필요합니다.

컨테이너는 호스트 OS의 커널을 공유하면서 프로세스 수준으로 격리합니다. 이미지 크기는 수십 MB에 불과하고, 기동 시간은 밀리초 단위입니다.

항목가상 머신컨테이너
격리 수준OS 전체프로세스
이미지 크기수 GB수십~수백 MB
기동 시간수십 초밀리초
리소스 오버헤드높음낮음
이식성하이퍼바이저 의존커널만 일치하면 어디서든

리눅스 네임스페이스와 cgroups

컨테이너의 격리는 두 가지 리눅스 커널 기능으로 구현됩니다.

네임스페이스(Namespace) 는 프로세스가 보는 시스템 리소스의 범위를 제한합니다.

  • PID 네임스페이스: 컨테이너 안에서는 PID 1부터 시작하는 독립된 프로세스 트리를 봅니다
  • NET 네임스페이스: 독립된 네트워크 인터페이스, IP 주소, 라우팅 테이블을 가집니다
  • MNT 네임스페이스: 독립된 파일시스템 마운트 포인트를 가집니다
  • UTS 네임스페이스: 독립된 호스트네임을 가집니다
  • IPC 네임스페이스: 독립된 IPC 리소스를 가집니다
  • USER 네임스페이스: 독립된 UID/GID 매핑을 가집니다

cgroups(Control Groups) 는 CPU, 메모리, 디스크 I/O 등 하드웨어 리소스 사용량을 제한합니다.

# cgroup으로 메모리 제한 확인하기
cat /sys/fs/cgroup/memory/docker/CONTAINER_ID/memory.limit_in_bytes

이 두 기능 덕분에 컨테이너는 마치 독립된 머신처럼 동작하면서도 커널을 공유하여 가벼운 상태를 유지합니다.


2. Docker 기초

핵심 개념 3가지

이미지(Image) 는 읽기 전용 템플릿입니다. 애플리케이션 코드, 런타임, 시스템 라이브러리, 설정 파일 등이 레이어 구조로 저장되어 있습니다.

컨테이너(Container) 는 이미지의 실행 인스턴스입니다. 이미지 위에 쓰기 가능한 레이어가 추가됩니다.

레지스트리(Registry) 는 이미지를 저장하고 배포하는 서비스입니다. Docker Hub가 대표적이며, AWS ECR, GitHub Container Registry 등의 프라이빗 레지스트리도 있습니다.

필수 명령어

# 이미지 pull
docker pull nginx:1.25

# 컨테이너 실행
docker run -d --name my-nginx -p 8080:80 nginx:1.25

# 실행 중인 컨테이너 확인
docker ps

# 컨테이너 로그 확인
docker logs my-nginx

# 컨테이너 내부 접속
docker exec -it my-nginx /bin/bash

# 컨테이너 정지 및 삭제
docker stop my-nginx
docker rm my-nginx

# 이미지 빌드
docker build -t my-app:1.0 .

# 이미지를 레지스트리에 푸시
docker tag my-app:1.0 registry.example.com/my-app:1.0
docker push registry.example.com/my-app:1.0

이미지 레이어 구조 이해하기

Docker 이미지는 여러 개의 읽기 전용 레이어로 구성됩니다. Dockerfile의 각 명령어가 하나의 레이어를 만듭니다.

# 이미지 레이어 확인
docker history my-app:1.0

레이어는 캐싱됩니다. 변경되지 않은 레이어는 다시 빌드하지 않으므로, Dockerfile에서 변경 빈도가 낮은 명령어를 위에 배치하는 것이 빌드 속도 최적화의 핵심입니다.


3. Dockerfile 베스트 프랙티스

기본 Dockerfile 구조

# 베이스 이미지 지정
FROM node:20-alpine

# 작업 디렉터리 설정
WORKDIR /app

# 의존성 파일 먼저 복사 (캐시 최적화)
COPY package.json package-lock.json ./
RUN npm ci --only=production

# 소스 코드 복사
COPY . .

# 포트 노출
EXPOSE 3000

# 실행 명령
CMD ["node", "server.js"]

멀티스테이지 빌드

빌드 도구와 런타임 환경을 분리하여 최종 이미지 크기를 대폭 줄일 수 있습니다.

# 1단계: 빌드
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# 2단계: 프로덕션
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]

빌드 단계에서만 필요한 devDependencies, 소스 코드, 빌드 도구가 최종 이미지에 포함되지 않습니다.

레이어 캐시 최적화

# 나쁜 예: 소스가 변경될 때마다 npm install이 다시 실행됨
COPY . .
RUN npm ci

# 좋은 예: package.json이 변경되지 않으면 캐시 활용
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

.dockerignore 파일

불필요한 파일이 빌드 컨텍스트에 포함되지 않도록 합니다.

node_modules
.git
.env
*.md
dist
.DS_Store
coverage

보안 베스트 프랙티스

# 1. 루트가 아닌 사용자로 실행
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# 2. 특정 버전의 베이스 이미지 사용 (latest 태그 금지)
FROM node:20.11.1-alpine3.19

# 3. COPY를 사용하고 ADD는 피할 것
COPY ./config /app/config

# 4. 민감 정보는 이미지에 포함하지 않기
# 빌드 시 ARG를 사용하되, 최종 이미지에 남지 않도록 주의

4. Docker Compose

다중 컨테이너 오케스트레이션

Docker Compose는 여러 컨테이너를 하나의 YAML 파일로 정의하고 관리합니다.

version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    volumes:
      - ./src:/app/src
    networks:
      - backend

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    networks:
      - backend

volumes:
  postgres_data:

networks:
  backend:
    driver: bridge

Compose 핵심 명령어

# 전체 서비스 실행
docker compose up -d

# 로그 확인
docker compose logs -f app

# 특정 서비스 재빌드 후 실행
docker compose up -d --build app

# 서비스 상태 확인
docker compose ps

# 전체 서비스 종료 및 리소스 정리
docker compose down -v

네트워크와 볼륨

네트워크: 같은 Compose 파일 안의 서비스들은 서비스 이름으로 서로 통신할 수 있습니다. 위 예제에서 app 서비스는 db:5432로 데이터베이스에 접근합니다.

볼륨: 컨테이너가 삭제되어도 데이터가 유지됩니다. postgres_data라는 named volume이 데이터베이스 파일을 보관합니다.

개발 환경 vs 프로덕션 환경

# docker-compose.override.yml (개발 환경 자동 적용)
services:
  app:
    build:
      target: development
    volumes:
      - ./src:/app/src
    environment:
      - NODE_ENV=development
    command: npm run dev
# 프로덕션 배포 시 override 제외
docker compose -f docker-compose.yml up -d

5. Kubernetes 아키텍처

왜 Kubernetes인가

Docker Compose는 단일 호스트에서 여러 컨테이너를 관리하기에 좋지만, 실제 프로덕션 환경에서는 다음이 필요합니다.

  • 여러 서버에 걸친 컨테이너 배포
  • 자동 스케일링
  • 서비스 디스커버리와 로드 밸런싱
  • 롤링 업데이트와 롤백
  • 자가 치유(self-healing)

Kubernetes(K8s)는 이 모든 것을 제공하는 컨테이너 오케스트레이션 플랫폼입니다.

Control Plane 컴포넌트

API Server(kube-apiserver): 클러스터의 모든 요청을 처리하는 중앙 관문입니다. kubectl 명령, 내부 컴포넌트, 외부 클라이언트 모두 API Server를 거칩니다.

etcd: 클러스터의 모든 상태를 저장하는 분산 키-값 저장소입니다. 어떤 Pod가 어디에서 실행되는지, 어떤 Service가 존재하는지 등의 정보가 여기에 있습니다.

Scheduler(kube-scheduler): 새로 생성된 Pod를 어떤 노드에 배치할지 결정합니다. 리소스 요구사항, 노드 상태, 어피니티 규칙 등을 고려합니다.

Controller Manager(kube-controller-manager): 클러스터의 현재 상태를 원하는 상태(desired state)에 맞추는 역할을 합니다. ReplicaSet Controller, Node Controller, Job Controller 등이 포함됩니다.

Worker Node 컴포넌트

kubelet: 각 노드에서 실행되며, API Server의 지시에 따라 컨테이너를 실행하고 상태를 보고합니다.

kube-proxy: 각 노드에서 네트워크 규칙을 관리하여 Service의 로드 밸런싱을 처리합니다.

Container Runtime: 실제 컨테이너를 실행하는 소프트웨어입니다. containerd가 가장 널리 사용됩니다.

Control Plane
  +------------------+
  | API Server       |<--- kubectl, 클라이언트
  | etcd             |
  | Scheduler        |
  | Controller Mgr   |
  +------------------+
        |
  Worker Node 1          Worker Node 2
  +-----------------+   +-----------------+
  | kubelet         |   | kubelet         |
  | kube-proxy      |   | kube-proxy      |
  | containerd      |   | containerd      |
  | [Pod] [Pod]     |   | [Pod] [Pod]     |
  +-----------------+   +-----------------+

6. Kubernetes 핵심 오브젝트

Pod

Pod는 K8s에서 배포 가능한 가장 작은 단위입니다. 하나 이상의 컨테이너를 포함하며, 같은 네트워크와 스토리지를 공유합니다.

apiVersion: v1
kind: Pod
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  containers:
    - name: app
      image: my-app:1.0
      ports:
        - containerPort: 3000
      resources:
        requests:
          memory: "128Mi"
          cpu: "250m"
        limits:
          memory: "256Mi"
          cpu: "500m"
      livenessProbe:
        httpGet:
          path: /healthz
          port: 3000
        initialDelaySeconds: 10
        periodSeconds: 5
      readinessProbe:
        httpGet:
          path: /ready
          port: 3000
        initialDelaySeconds: 5
        periodSeconds: 3

ReplicaSet

지정된 수의 Pod 복제본이 항상 실행되도록 보장합니다. 보통 직접 사용하지 않고 Deployment를 통해 관리합니다.

Deployment

선언적으로 Pod와 ReplicaSet을 관리합니다. 롤링 업데이트, 롤백, 스케일링을 지원합니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: app
          image: my-app:1.0
          ports:
            - containerPort: 3000
          resources:
            requests:
              memory: "128Mi"
              cpu: "250m"
            limits:
              memory: "256Mi"
              cpu: "500m"
# 배포 상태 확인
kubectl rollout status deployment/my-app

# 이미지 업데이트 (롤링 업데이트 트리거)
kubectl set image deployment/my-app app=my-app:2.0

# 롤백
kubectl rollout undo deployment/my-app

# 스케일링
kubectl scale deployment/my-app --replicas=5

Service

Pod에 안정적인 네트워크 엔드포인트를 제공합니다. Pod는 임시적이지만 Service의 IP와 DNS는 고정입니다.

ClusterIP (기본값): 클러스터 내부에서만 접근 가능합니다.

apiVersion: v1
kind: Service
metadata:
  name: my-app-svc
spec:
  type: ClusterIP
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000

NodePort: 각 노드의 특정 포트를 통해 외부에서 접근합니다.

apiVersion: v1
kind: Service
metadata:
  name: my-app-nodeport
spec:
  type: NodePort
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000
      nodePort: 30080

LoadBalancer: 클라우드 프로바이더의 로드 밸런서를 자동으로 프로비저닝합니다.

apiVersion: v1
kind: Service
metadata:
  name: my-app-lb
spec:
  type: LoadBalancer
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000

Ingress

HTTP/HTTPS 트래픽을 클러스터 내부 Service로 라우팅합니다. 호스트명과 경로 기반의 라우팅 규칙을 정의할 수 있습니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app-svc
                port:
                  number: 80
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-svc
                port:
                  number: 80
  tls:
    - hosts:
        - app.example.com
      secretName: tls-secret

7. Kubernetes 설정 관리

ConfigMap

환경 설정을 컨테이너 이미지와 분리하여 관리합니다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DATABASE_HOST: "db-service"
  DATABASE_PORT: "5432"
  LOG_LEVEL: "info"
  app.properties: |
    server.port=3000
    cache.ttl=300
# Pod에서 ConfigMap 사용
spec:
  containers:
    - name: app
      image: my-app:1.0
      envFrom:
        - configMapRef:
            name: app-config
      volumeMounts:
        - name: config-volume
          mountPath: /app/config
  volumes:
    - name: config-volume
      configMap:
        name: app-config
        items:
          - key: app.properties
            path: app.properties

Secret

패스워드, API 키 등 민감한 데이터를 관리합니다. base64로 인코딩되어 저장됩니다.

# Secret 생성
kubectl create secret generic db-secret \
  --from-literal=username=admin \
  --from-literal=password=s3cret
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  username: YWRtaW4=
  password: czNjcmV0
# Pod에서 Secret 사용
spec:
  containers:
    - name: app
      env:
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: password

PersistentVolume (PV) 와 PersistentVolumeClaim (PVC)

데이터를 Pod 라이프사이클과 독립적으로 유지합니다.

# PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: standard
# Deployment에서 PVC 사용
spec:
  containers:
    - name: postgres
      image: postgres:16-alpine
      volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
  volumes:
    - name: postgres-storage
      persistentVolumeClaim:
        claimName: postgres-pvc

8. Helm - 쿠버네티스 패키지 매니저

Helm이란

Helm은 K8s 매니페스트를 패키지(차트)로 묶어서 관리하는 도구입니다. 여러 YAML 파일을 하나의 단위로 설치, 업그레이드, 롤백할 수 있습니다.

차트 구조

my-app-chart/
  Chart.yaml          # 차트 메타데이터
  values.yaml         # 기본 설정 값
  templates/          # K8s 매니페스트 템플릿
    deployment.yaml
    service.yaml
    ingress.yaml
    configmap.yaml
    _helpers.tpl      # 템플릿 헬퍼 함수
  charts/             # 의존성 차트

values.yaml

# values.yaml
replicaCount: 3

image:
  repository: my-app
  tag: "1.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: true
  hostname: app.example.com

resources:
  requests:
    cpu: 250m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilization: 70

Helm 핵심 명령어

# 차트 설치
helm install my-release ./my-app-chart

# 커스텀 values로 설치
helm install my-release ./my-app-chart -f production-values.yaml

# 릴리즈 업그레이드
helm upgrade my-release ./my-app-chart --set image.tag=2.0

# 릴리즈 목록 확인
helm list

# 릴리즈 상태 확인
helm status my-release

# 릴리즈 히스토리
helm history my-release

# 롤백
helm rollback my-release 1

# 릴리즈 삭제
helm uninstall my-release

환경별 values 파일 관리

# 환경별 파일 구조
values.yaml              # 공통 기본값
values-dev.yaml          # 개발 환경
values-staging.yaml      # 스테이징 환경
values-prod.yaml         # 프로덕션 환경
# 스테이징 배포
helm upgrade --install my-app ./my-app-chart \
  -f values.yaml \
  -f values-staging.yaml \
  --namespace staging

# 프로덕션 배포
helm upgrade --install my-app ./my-app-chart \
  -f values.yaml \
  -f values-prod.yaml \
  --namespace production

9. 실전 배포 예제 - 웹앱 + DB + Redis

전체 구성

웹 애플리케이션, PostgreSQL 데이터베이스, Redis 캐시를 Kubernetes에 배포하는 전체 예제입니다.

네임스페이스 생성

kubectl create namespace my-app

PostgreSQL 배포

# postgres-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16-alpine
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_DB
              value: mydb
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: username
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: password
          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
      volumes:
        - name: postgres-data
          persistentVolumeClaim:
            claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: my-app
spec:
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432

Redis 배포

# redis-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  namespace: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:7-alpine
          ports:
            - containerPort: 6379
          resources:
            requests:
              memory: "64Mi"
              cpu: "100m"
            limits:
              memory: "128Mi"
              cpu: "250m"
---
apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: my-app
spec:
  selector:
    app: redis
  ports:
    - port: 6379
      targetPort: 6379

웹 애플리케이션 배포

# app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: web-app
          image: my-web-app:1.0
          ports:
            - containerPort: 3000
          env:
            - name: DATABASE_URL
              value: "postgres://$(DB_USER):$(DB_PASS)@postgres:5432/mydb"
            - name: REDIS_URL
              value: "redis://redis:6379"
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: username
            - name: DB_PASS
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: password
          livenessProbe:
            httpGet:
              path: /healthz
              port: 3000
            initialDelaySeconds: 15
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
          resources:
            requests:
              memory: "128Mi"
              cpu: "250m"
            limits:
              memory: "256Mi"
              cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: web-app
  namespace: my-app
spec:
  type: ClusterIP
  selector:
    app: web-app
  ports:
    - port: 80
      targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-app-ingress
  namespace: my-app
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-app
                port:
                  number: 80

배포 순서

# 1. Secret 생성
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password=secure-password-here \
  -n my-app

# 2. PVC 생성
kubectl apply -f postgres-pvc.yaml

# 3. 데이터베이스 배포
kubectl apply -f postgres-deployment.yaml

# 4. Redis 배포
kubectl apply -f redis-deployment.yaml

# 5. 웹 앱 배포
kubectl apply -f app-deployment.yaml

# 6. 상태 확인
kubectl get all -n my-app

10. 트러블슈팅

CrashLoopBackOff

Pod가 반복적으로 시작과 크래시를 반복하는 상태입니다.

# 원인 확인
kubectl describe pod POD_NAME -n my-app
kubectl logs POD_NAME -n my-app --previous

# 흔한 원인들:
# - 애플리케이션 시작 시 에러 발생
# - 잘못된 환경 변수
# - 의존하는 서비스에 연결 실패
# - livenessProbe 실패

해결 방법: 로그를 확인하여 에러를 파악합니다. livenessProbe의 initialDelaySeconds를 늘리거나, 환경 변수가 올바른지 확인합니다.

ImagePullBackOff

컨테이너 이미지를 가져올 수 없는 상태입니다.

# 원인 확인
kubectl describe pod POD_NAME -n my-app

# 흔한 원인들:
# - 이미지 이름이나 태그 오타
# - 프라이빗 레지스트리 인증 미설정
# - 이미지가 존재하지 않음

# 프라이빗 레지스트리 인증 설정
kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=user \
  --docker-password=pass \
  -n my-app

OOMKilled

컨테이너가 메모리 제한을 초과하여 강제 종료된 상태입니다.

# 원인 확인
kubectl describe pod POD_NAME -n my-app

# 실시간 리소스 사용량 확인
kubectl top pod -n my-app

# 해결: 메모리 limits 값을 늘림
resources:
  limits:
    memory: "512Mi"  # 기존 256Mi에서 증가

일반적인 디버깅 명령어

# Pod 목록과 상태 확인
kubectl get pods -n my-app -o wide

# Pod 상세 정보 (이벤트 포함)
kubectl describe pod POD_NAME -n my-app

# 실시간 로그 확인
kubectl logs -f POD_NAME -n my-app

# Pod 내부 접속
kubectl exec -it POD_NAME -n my-app -- /bin/sh

# Service 엔드포인트 확인
kubectl get endpoints -n my-app

# 클러스터 내부에서 DNS 확인
kubectl run debug --rm -it --image=busybox -- nslookup web-app.my-app.svc.cluster.local

# 노드 리소스 현황
kubectl top nodes

마무리

이 글에서 다룬 내용을 정리하겠습니다.

  1. 컨테이너 기초: 네임스페이스와 cgroups로 경량 격리를 구현
  2. Docker: 이미지 빌드, 멀티스테이지 빌드, 보안 관행
  3. Docker Compose: 개발 환경에서의 멀티 컨테이너 관리
  4. K8s 아키텍처: Control Plane과 Worker Node의 역할
  5. K8s 오브젝트: Pod, Deployment, Service, Ingress의 활용
  6. 설정 관리: ConfigMap, Secret, PV/PVC
  7. Helm: 차트를 이용한 릴리즈 관리
  8. 실전 배포: 웹앱 + DB + Redis 3-tier 아키텍처
  9. 트러블슈팅: CrashLoopBackOff, ImagePullBackOff, OOMKilled 대응

Docker와 Kubernetes는 현대 인프라의 기초입니다. 이 글의 예제를 로컬 환경(minikube 또는 kind)에서 직접 실습해 보시기 바랍니다. 실제로 Pod를 배포하고 Service를 구성하고 트러블슈팅해 보는 경험이 가장 빠른 학습 방법입니다.