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의 requests와 limits는 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는 다음 순서로 결정됩니다.
- Image의
USER가 기본값을 정합니다. 설정하지 않으면 일반적으로 UID 0인 Root입니다. - Docker의
--user나 Kubernetes의securityContext가 이를 Override할 수 있습니다. - Container Runtime이 최종 UID/GID로 Process를 실행합니다.
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 2000runAsUser: Process의 UIDrunAsGroup: Process의 기본 GIDfsGroup: 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 100000Container 안에서는 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를 추가로 제한합니다.
다음 설정은 호스트와의 격리 경계를 크게 약화합니다.
privilegedhostPathhostNetwork,hostPID- Docker 또는 containerd socket Mount
Kubernetes와 Container Runtime
Kubernetes API
→ kubelet
→ CRI
→ containerd 또는 CRI-O
→ runc 같은 OCI Runtime
→ Linux KernelKubernetes는 Pod의 배치와 복구를 관리하고, kubelet은 CRI를 통해 Container Runtime에 실행을 요청합니다. containerd와 CRI-O는 Image와 Container의 Lifecycle을 관리하며, runc 같은 OCI Runtime이 Namespace, cgroup과 Mount를 설정합니다. 실제 격리, Resource와 Permission은 Linux Kernel이 집행합니다.
References
- Docker: What is a container?
- Docker: What is an image?
- Docker: Resource constraints
- Cgroups: Deep Dive into Resource Management in Kubernetes
- Kubernetes: Container Runtime Interface
- Kubernetes: Resource Management for Pods and Containers
- Kubernetes: User Namespaces
- Linux Kernel: Control Group v2
- Linux Kernel: Idmappings