IMJW
Blog

DevOps / Kubernetes

컨테이너 & 오케스트레이션 기초

2026년 9월 7일

쿠버네티스를 공부하기에 앞서 컨테이너와 오케스트레이션이 무엇이고, 왜 필요한지부터 알아보려고 한다. 이 부분을 먼저 정리해두면 이후에 나오는 쿠버네티스의 구성 요소와 동작을 이해하는 데 도움이 될 것 같았다.

Container / Virtual Machine / Kubernetes / Namespace / cgroups

컨테이너가 없을 때

기존에는 물리 서버에 OS를 설치하고, 그 위에 애플리케이션을 직접 실행했다. JDK, Nginx, Python 같은 런타임과 의존성도 서버에 직접 설치해야 했다.

애플리케이션 하나만 실행할 때는 큰 문제가 없지만, 한 서버에서 여러 애플리케이션을 실행하면 의존성 충돌이 발생할 수 있다. 한 애플리케이션은 Python 2를 요구하고 다른 애플리케이션은 Python 3을 요구하는 경우가 대표적이다.

이 문제를 해결하기 위해 VM(Virtual Machine)이 사용됐다. 물리 서버 한 대를 여러 가상 머신으로 나누고, 각 VM에 별도의 OS와 애플리케이션을 설치하면 서로 다른 실행 환경을 구성할 수 있다.

하지만 VM에도 몇 가지 한계가 있었다.

  • VM마다 Guest OS를 포함해야 하므로 필요한 저장 공간과 메모리가 커진다.
  • Guest OS를 부팅해야 하므로 컨테이너보다 시작 시간이 길다.
  • 서버마다 런타임과 의존성을 직접 구성하면 실행 환경이 달라질 수 있다.
  • 배포할 때마다 서버의 의존성을 수동으로 맞추면 서버별 설정이 조금씩 달라지는 스노우플레이크 서버 문제가 생길 수 있다.

예를 들어 애플리케이션의 크기는 200MB인데, 이를 실행하기 위해 수 GB 크기의 Guest OS까지 함께 관리해야 하는 상황이 생긴다.

컨테이너를 사용하는 이유

컨테이너는 VM과 달리 애플리케이션마다 Guest OS를 실행하지 않는다. 각 컨테이너는 Host OS의 커널을 공유하고, 애플리케이션 실행에 필요한 바이너리와 라이브러리만 별도로 구성한다.

classes: {
  app-orange: {
    width: 180
    height: 62
    style: {
      fill: "#FF5108"
      stroke: "#FF5108"
      font-color: "#FFFFFF"
      font-size: 29
      bold: true
      border-radius: 9
    }
  }
  app-amber: {
    width: 180
    height: 62
    style: {
      fill: "#FF8A00"
      stroke: "#FF8A00"
      font-color: "#FFFFFF"
      font-size: 29
      bold: true
      border-radius: 9
    }
  }
  app-teal: {
    width: 180
    height: 62
    style: {
      fill: "#08AAA0"
      stroke: "#08AAA0"
      font-color: "#FFFFFF"
      font-size: 29
      bold: true
      border-radius: 9
    }
  }
  shared-blue: {
    height: 66
    style: {
      fill: "#1076BD"
      stroke: "#1076BD"
      font-color: "#FFFFFF"
      font-size: 29
      bold: true
      border-radius: 9
    }
  }
  shared-green: {
    height: 66
    style: {
      fill: "#00A000"
      stroke: "#00A000"
      font-color: "#FFFFFF"
      font-size: 29
      bold: true
      border-radius: 9
    }
  }
  title: {
    height: 58
    style: {
      fill: "#FFFFFF"
      stroke: "#FFFFFF"
      stroke-width: 0
      font-color: "#334155"
      font-size: 38
      bold: true
    }
  }
  note: {
    height: 48
    style: {
      fill: "#FFFFFF"
      stroke: "#FFFFFF"
      stroke-width: 0
      font-color: "#082968"
      font-size: 26
      bold: true
    }
  }
  icon: {
    shape: image
    width: 48
    height: 48
  }
  left-spacer: {
    label: ""
    width: 318
    height: 48
    style.opacity: 0
  }
  right-spacer: {
    label: ""
    width: 62
    height: 48
    style.opacity: 0
  }
  divider: {
    label: ""
    width: 2
    height: 680
    style: {
      fill: "#E5E7EB"
      stroke: "#E5E7EB"
      stroke-width: 0
    }
  }
}

grid-columns: 3
horizontal-gap: 34

virtual_machines: {
  label: ""
  grid-columns: 1
  vertical-gap: 12

  title: "Virtual Machines" {class: title}

  apps: {
    label: ""
    grid-columns: 3
    horizontal-gap: 12

    first: {
      label: ""
      grid-columns: 1
      vertical-gap: 8
      app: "App 1" {class: app-orange}
      libs: "Bins / Libs" {class: app-orange}
      guest: "Guest OS" {class: app-orange}
      kernel: "Kernel 1" {class: app-orange}
    }
    second: {
      label: ""
      grid-columns: 1
      vertical-gap: 8
      app: "App 2" {class: app-amber}
      libs: "Bins / Libs" {class: app-amber}
      guest: "Guest OS" {class: app-amber}
      kernel: "Kernel 2" {class: app-amber}
    }
    third: {
      label: ""
      grid-columns: 1
      vertical-gap: 8
      app: "App 3" {class: app-teal}
      libs: "Bins / Libs" {class: app-teal}
      guest: "Guest OS" {class: app-teal}
      kernel: "Kernel 3" {class: app-teal}
    }
  }

  note: "Each VM has its own kernel" {class: note}
  hypervisor: "Hypervisor" {class: shared-blue}
  host: "Host OS + Host Kernel" {class: shared-blue}

  infrastructure: {
    label: "Infrastructure"
    grid-columns: 5
    horizontal-gap: 18
    left_space.class: left-spacer
    monitor: "" {
      class: icon
      icon: ./icons/monitor.svg
    }
    database: "" {
      class: icon
      icon: ./icons/database.svg
    }
    cloud: "" {
      class: icon
      icon: ./icons/cloud.svg
    }
    right_space.class: right-spacer
  }
}

divider.class: divider

containers: {
  label: ""
  grid-columns: 1
  vertical-gap: 12

  title: "Containers" {class: title}

  apps: {
    label: ""
    grid-columns: 3
    horizontal-gap: 12

    first: {
      label: ""
      grid-columns: 1
      vertical-gap: 8
      app: "App 1" {class: app-orange}
      libs: "Bins / Libs" {class: app-orange}
    }
    second: {
      label: ""
      grid-columns: 1
      vertical-gap: 8
      app: "App 2" {class: app-amber}
      libs: "Bins / Libs" {class: app-amber}
    }
    third: {
      label: ""
      grid-columns: 1
      vertical-gap: 8
      app: "App 3" {class: app-teal}
      libs: "Bins / Libs" {class: app-teal}
    }
  }

  note: "All 3 containers share the Host OS kernel" {class: note}
  runtime: "Container Runtime" {class: shared-green}
  host: "Host OS · ONE SHARED LINUX KERNEL" {class: shared-blue}

  infrastructure: {
    label: "Infrastructure"
    grid-columns: 5
    horizontal-gap: 18
    left_space.class: left-spacer
    monitor: "" {
      class: icon
      icon: ./icons/monitor.svg
    }
    database: "" {
      class: icon
      icon: ./icons/database.svg
    }
    cloud: "" {
      class: icon
      icon: ./icons/cloud.svg
    }
    right_space.class: right-spacer
  }
}

virtual_machines.style: {
  fill: "#FFFFFF"
  stroke: "#FFFFFF"
  stroke-width: 0
}
containers.style: {
  fill: "#FFFFFF"
  stroke: "#FFFFFF"
  stroke-width: 0
}
virtual_machines.apps.style: {
  fill: "#FFFFFF"
  stroke: "#FFFFFF"
  stroke-width: 0
}
containers.apps.style: {
  fill: "#FFFFFF"
  stroke: "#FFFFFF"
  stroke-width: 0
}
virtual_machines.apps.first.style.stroke-width: 0
virtual_machines.apps.first.style.fill: "#FFFFFF"
virtual_machines.apps.second.style.stroke-width: 0
virtual_machines.apps.second.style.fill: "#FFFFFF"
virtual_machines.apps.third.style.stroke-width: 0
virtual_machines.apps.third.style.fill: "#FFFFFF"
containers.apps.first.style.stroke-width: 0
containers.apps.first.style.fill: "#FFFFFF"
containers.apps.second.style.stroke-width: 0
containers.apps.second.style.fill: "#FFFFFF"
containers.apps.third.style.stroke-width: 0
containers.apps.third.style.fill: "#FFFFFF"

virtual_machines.infrastructure.style: {
  fill: "#082968"
  stroke: "#082968"
  font-color: "#FFFFFF"
  font-size: 28
  border-radius: 11
}
containers.infrastructure.style: {
  fill: "#082968"
  stroke: "#082968"
  font-color: "#FFFFFF"
  font-size: 28
  border-radius: 11
}

VM은 애플리케이션마다 Guest OS를 포함하지만, 컨테이너는 Host OS의 커널을 공유하고 애플리케이션과 의존성을 분리한다.

하나의 커널을 공유하면서도 컨테이너별 실행 환경을 분리할 수 있는 이유는 Linux 커널의 namespace와 cgroups 기능 덕분이다.

  • namespace: 프로세스, 네트워크, 마운트 포인트와 같은 시스템 자원을 컨테이너별로 격리한다.
  • cgroups: 컨테이너가 사용할 수 있는 CPU와 메모리 등의 자원을 제한하고 사용량을 관리한다.

또한 애플리케이션과 실행에 필요한 의존성을 하나의 컨테이너 이미지로 패키징할 수 있다. 동일한 이미지와 호환되는 컨테이너 실행 환경을 사용하면 개발 환경과 운영 환경의 차이를 줄일 수 있고, 배포 환경을 일관되게 관리할 수 있다.

오케스트레이션

컨테이너 한두 개를 직접 실행하고 관리하는 것은 어렵지 않다. 하지만 실제 운영 환경에서 여러 컨테이너와 여러 서버를 관리하기 시작하면 다음과 같은 요구사항이 생긴다.

  • 종료된 컨테이너를 감지하고 자동으로 다시 실행해야 한다.
  • 트래픽 변화에 따라 컨테이너 수를 조절해야 한다.
  • 여러 노드 중에서 컨테이너를 실행할 노드를 결정해야 한다.
  • 컨테이너 IP가 변경돼도 안정적으로 접근할 방법이 필요하다.
  • 서비스 중단을 줄이면서 새로운 버전을 배포해야 한다.

이러한 작업을 사람이 계속 수동으로 처리하기는 어렵다. 여러 컨테이너의 배치, 복구, 확장, 네트워크 연결, 배포를 자동으로 관리하는 것이 컨테이너 오케스트레이션이다.

쿠버네티스는 이러한 컨테이너 오케스트레이션 기능을 제공한다. 따라서 쿠버네티스를 이해하려면 컨테이너 자체뿐만 아니라 여러 컨테이너를 운영할 때 어떤 문제가 생기는지도 함께 이해해야 한다.

헷갈렸던 개념

Host OS / Guest OS

  • Host OS: 물리 하드웨어에서 실행되며, 컨테이너가 공유하는 커널을 제공하는 OS다.
  • Guest OS: VM 내부에 별도로 설치되는 OS다. 각 Guest OS는 자체 커널을 가진다.
  • 컨테이너: Guest OS를 별도로 실행하지 않고 Host OS의 커널을 공유한다.

VM 환경에서는 Host OS의 커널만 존재하는 것이 아니다. Host OS에는 Host 커널이 있고, 각 VM의 Guest OS에도 별도의 커널이 존재한다.

namespace

처음에는 namespace가 왜 필요한지 이해하기 어려웠다. namespace가 없는 경우를 생각해보니 역할이 조금 더 명확해졌다.

프로세스를 격리하는 PID namespace가 없다면 컨테이너의 프로세스가 동일한 프로세스 공간을 공유하게 되고, 다른 컨테이너나 호스트의 프로세스를 볼 수 있다.

PID namespace를 적용하면 각 컨테이너에는 독립된 프로세스 ID 공간이 제공된다. 이 때문에 컨테이너 내부에서는 자신의 프로세스만 보이는 것처럼 동작하며, 각 컨테이너에서 실행되는 첫 번째 프로세스가 각각 PID 1을 가질 수도 있다.

classes: {
  title: {
    height: 56
    style: {
      fill: "#FFFFFF"
      stroke: "#FFFFFF"
      stroke-width: 0
      font-color: "#334155"
      font-size: 34
      bold: true
    }
  }
  orange: {
    width: 166
    height: 58
    style: {
      fill: "#FF5108"
      stroke: "#FF5108"
      font-color: "#FFFFFF"
      font-size: 24
      bold: true
      border-radius: 9
    }
  }
  amber: {
    width: 166
    height: 58
    style: {
      fill: "#FF8A00"
      stroke: "#FF8A00"
      font-color: "#FFFFFF"
      font-size: 24
      bold: true
      border-radius: 9
    }
  }
  teal: {
    width: 166
    height: 58
    style: {
      fill: "#08AAA0"
      stroke: "#08AAA0"
      font-color: "#FFFFFF"
      font-size: 24
      bold: true
      border-radius: 9
    }
  }
  process-white: {
    shape: image
    icon: ./imjw-icons/process-white.svg
    width: 44
    height: 44
  }
  container-white: {
    shape: image
    icon: ./imjw-icons/container-white.svg
    width: 38
    height: 38
  }
  isolation-white: {
    shape: image
    icon: ./imjw-icons/isolation-white.svg
    width: 42
    height: 42
  }
  pid: {
    width: 164
    height: 48
    style: {
      fill: "#FFFFFF"
      stroke: "#FFFFFF"
      font-color: "#082968"
      font-size: 20
      bold: true
      border-radius: 7
    }
  }
  namespace: {
    width: 166
    height: 52
    style: {
      fill: "#1076BD"
      stroke: "#1076BD"
      font-color: "#FFFFFF"
      font-size: 21
      bold: true
      border-radius: 8
    }
  }
  spacer: {
    label: ""
    width: 90
    height: 42
    style.opacity: 0
  }
  divider: {
    label: ""
    width: 2
    height: 500
    style: {
      fill: "#E5E7EB"
      stroke: "#E5E7EB"
      stroke-width: 0
    }
  }
}

grid-columns: 3
horizontal-gap: 28

without: {
  label: ""
  grid-columns: 1
  vertical-gap: 18

  title: "Without PID Namespace" {class: title}

  containers: {
    label: ""
    grid-columns: 3
    horizontal-gap: 10
    a: "Container A" {class: orange}
    b: "Container B" {class: amber}
    c: "Container C" {class: teal}
  }

  shared: {
    label: "Shared Process View"
    grid-columns: 1
    vertical-gap: 8

    icon_row: {
      label: ""
      grid-columns: 3
      left_space.class: spacer
      symbol: "" {class: process-white}
      right_space.class: spacer
    }

    p1: "A  ·  PID 101, 102" {class: pid}
    p2: "B  ·  PID 103, 104" {class: pid}
    p3: "C  ·  PID 105, 106" {class: pid}
  }
}

divider.class: divider

with: {
  label: ""
  grid-columns: 1
  vertical-gap: 18

  title: "With PID Namespace" {class: title}

  containers: {
    label: ""
    grid-columns: 3
    horizontal-gap: 10

    a: {
      label: ""
      grid-columns: 1
      vertical-gap: 8
      symbol: "" {class: container-white}
      header: "Container A" {class: orange}
      namespace: "PID Namespace" {class: namespace}
      pids: "PID 1, 2" {class: pid}
    }
    b: {
      label: ""
      grid-columns: 1
      vertical-gap: 8
      symbol: "" {class: container-white}
      header: "Container B" {class: amber}
      namespace: "PID Namespace" {class: namespace}
      pids: "PID 1, 2" {class: pid}
    }
    c: {
      label: ""
      grid-columns: 1
      vertical-gap: 8
      symbol: "" {class: container-white}
      header: "Container C" {class: teal}
      namespace: "PID Namespace" {class: namespace}
      pids: "PID 1, 2" {class: pid}
    }
  }

  kernel: {
    label: ""
    grid-columns: 4
    horizontal-gap: 10
    left_space.class: spacer
    symbol: "" {class: isolation-white}
    message: "Kernel presents isolated process views"
    right_space.class: spacer
  }
}

without.style: {
  fill: "#FFFFFF"
  stroke: "#FFFFFF"
  stroke-width: 0
}
with.style: {
  fill: "#FFFFFF"
  stroke: "#FFFFFF"
  stroke-width: 0
}
without.containers.style: {
  fill: "#FFFFFF"
  stroke: "#FFFFFF"
  stroke-width: 0
}
without.shared.style: {
  fill: "#082968"
  stroke: "#082968"
  font-color: "#FFFFFF"
  font-size: 27
  border-radius: 12
}
without.shared.icon_row.style: {
  fill: "#082968"
  stroke: "#082968"
  stroke-width: 0
}
with.containers.style: {
  fill: "#FFFFFF"
  stroke: "#FFFFFF"
  stroke-width: 0
}
with.containers.a.style: {
  fill: "#FF5108"
  stroke: "#FF5108"
  border-radius: 11
}
with.containers.b.style: {
  fill: "#FF8A00"
  stroke: "#FF8A00"
  border-radius: 11
}
with.containers.c.style: {
  fill: "#08AAA0"
  stroke: "#08AAA0"
  border-radius: 11
}
with.kernel.style: {
  fill: "#082968"
  stroke: "#082968"
  font-color: "#FFFFFF"
  border-radius: 10
}
with.kernel.message.style: {
  fill: "#082968"
  stroke: "#082968"
  stroke-width: 0
  font-color: "#FFFFFF"
  font-size: 22
  bold: true
}

PID namespace가 적용되면 커널은 프로세스가 속한 namespace의 관점에 맞춰 보이는 프로세스와 PID를 결정한다.