Docker와 Kubernetes 아래에서 Linux가 담당하는 부분을 정리합니다. Kernel 구현이나 저수준 명령어는 다루지 않습니다.

Container

Container는 작은 VM이 아니라 격리된 Process입니다.

  • VM은 별도 Kernel을 가집니다.
  • Container는 호스트의 Linux Kernel을 공유합니다.
  • Image에는 애플리케이션, Library, 설정 등 User space만 들어가며 Kernel은 포함하지 않습니다.

macOS의 Docker Desktop과 Colima는 중간에 Linux VM을 실행합니다. Container는 macOS가 아니라 해당 VM의 Kernel을 공유합니다.

Image, Container, Volume

  • Image: 애플리케이션과 실행 환경을 묶은 읽기 전용 Package
  • Container: Image를 기반으로 실행한 Process와 임시 Writable Layer
  • Volume: Container와 별도 Lifecycle을 가지는 Storage

Container의 Writable Layer는 Container가 교체되면 사라질 수 있습니다. 영속 데이터는 Volume에 저장합니다.

Image가 같아도 Host Kernel, CPU Architecture, 환경변수, Volume과 Network는 달라질 수 있습니다. Container는 환경 차이를 줄이지만 전체 OS를 고정하지는 않습니다.

Namespace

Namespace는 Process가 볼 수 있는 범위를 격리합니다.

  • PID Namespace: Process와 PID
  • Network Namespace: Network Interface, IP, Port, Routing
  • Mount Namespace: Filesystem Mount
  • User Namespace: UID/GID

Container 내부의 localhost는 기본적으로 해당 Container입니다. 같은 Pod의 Container는 Network Namespace를 공유하므로 localhost로 통신할 수 있습니다. hostNetwork, hostPID, hostPath는 일부 경계를 의도적으로 공유합니다.

cgroup

cgroup은 Process를 그룹으로 묶어 CPU와 Memory 등의 사용량을 추적하고 제한합니다.

Kubernetes의 requestslimits는 kubelet과 Container Runtime을 거쳐 cgroup으로 설정되며, 실제 집행은 Kernel이 담당합니다.

CPU

  • CPU request는 Scheduling 기준이며, CPU가 부족할 때 상대적인 배분에도 영향을 줍니다.
  • CPU limit은 일정 시간 동안 사용할 수 있는 상한입니다.
  • 상한을 넘으면 Process가 종료되지 않고 CPU Throttling으로 실행이 지연됩니다.

상대적 배분에는 Weight, 상한에는 Quota를 사용합니다.

Memory

Memory Limit을 넘고 회수할 Memory도 부족하면 Kernel이 Process를 종료할 수 있습니다. Kubernetes에서는 일반적으로 OOMKilled로 표시됩니다. CPU Limit은 주로 지연으로, Memory Limit은 Process 종료로 나타납니다.

UID와 GID

Linux File Permission은 사용자 이름이 아니라 숫자 UID/GID를 기준으로 처리됩니다.

Process에는 실행 사용자의 UID, 기본 GID와 추가 Group이 붙습니다. File에는 소유자 UID, 소유 Group GID와 읽기·쓰기·실행 권한이 저장됩니다. Kernel은 이 숫자를 비교하여 접근을 허용합니다.

Process: UID 1000 / GID 1000
File:    UID 1000 / GID 2000 / rw-r-----
→ 소유자 권한으로 읽기·쓰기 가능

app, ubuntu 같은 사용자 이름은 각 환경의 /etc/passwd가 숫자를 표시하는 이름일 뿐입니다. User Namespace가 없다면 Container와 Host에서 이름이 달라도 UID 1000은 같은 숫자입니다.

Container Process의 UID/GID는 다음 순서로 결정됩니다.

  1. Image의 USER가 기본값을 정합니다. 설정하지 않으면 일반적으로 UID 0인 Root입니다.
  2. Docker의 --user나 Kubernetes의 securityContext가 이를 Override할 수 있습니다.
  3. Container Runtime이 최종 UID/GID로 Process를 실행합니다.
securityContext:
  runAsUser: 1000
  runAsGroup: 1000
  fsGroup: 2000
  • runAsUser: Process의 UID
  • runAsGroup: Process의 기본 GID
  • fsGroup: Process가 추가로 속하는 Group이며, 지원되는 Volume을 해당 Group 권한으로 접근할 수 있게 조정

이 설정은 호스트에 사용자를 생성하지 않습니다.

User Namespace가 없는 경우

Container의 UID/GID가 호스트에서도 같은 숫자로 사용됩니다. Host Mount나 Volume에 만든 파일도 해당 숫자 UID/GID를 소유자로 가집니다.

Volume의 기존 UID/GID와 Process의 UID/GID 또는 추가 Group이 맞지 않으면 Permission denied가 발생할 수 있습니다.

User Namespace가 있는 경우

Container UID와 Host UID를 다르게 Mapping할 수 있습니다.

Container UID 0 → Host UID 100000

Container 안에서는 UID 0인 Root로 동작하지만, 그 권한은 User Namespace 내부에 한정됩니다. 호스트에서는 Mapping된 비특권 UID로 취급되며, Host Mount와 Volume 접근은 UID/GID Mapping과 File Permission을 따릅니다.

권한

Container Root도 Namespace, Capability, seccomp 등의 제한을 받습니다. 하지만 User Namespace가 없다면 Kernel 관점의 UID 0은 호스트와 같으므로 안전하다고 볼 수는 없습니다.

Capability는 Root 권한을 기능별로 나누고, seccomp는 호출할 수 있는 System Call을 제한합니다. AppArmor와 SELinux는 접근 가능한 Resource를 추가로 제한합니다.

다음 설정은 호스트와의 격리 경계를 크게 약화합니다.

  • privileged
  • hostPath
  • hostNetwork, hostPID
  • Docker 또는 containerd socket Mount

Kubernetes와 Container Runtime

Kubernetes API
→ kubelet
→ CRI
→ containerd 또는 CRI-O
→ runc 같은 OCI Runtime
→ Linux Kernel

Kubernetes는 Pod의 배치와 복구를 관리하고, kubelet은 CRI를 통해 Container Runtime에 실행을 요청합니다. containerd와 CRI-O는 Image와 Container의 Lifecycle을 관리하며, runc 같은 OCI Runtime이 Namespace, cgroup과 Mount를 설정합니다. 실제 격리, Resource와 Permission은 Linux Kernel이 집행합니다.

References