근묵자흑
Kubernetes를 배우기 전에 Linux를 먼저 배워야 하는 이유 본문
원문: Learn Linux before Kubernetes by Anish Narayan
들어가며: Google이 이미 수백만 개의 컨테이너를 운영하고 있었다
오래 전, Google은 이미 대규모로 수백만 개의 컨테이너를 운영하고 있었습니다.
Linux cgroups는 거의 아무도 몰랐던 숨겨진 초능력과 같았습니다.
Google은 "컨테이너화(containerization)"라는 용어가 유행어가 되기 훨씬 전부터, 수년간 자사의 방대한 인프라를 관리하기 위해 cgroups를 광범위하게 사용해왔습니다.
2007년에 등장한 고급 Linux 커널 기능인 cgroups는 프로세스를 격리하고 리소스를 제어할 수 있었습니다. 하지만 거의 아무도 그 존재를 알지 못했습니다. cgroups는 극도로 복잡했고, 사용하려면 깊은 Linux 전문 지식이 필요했기 때문입니다. 기술 업계 내에서도 대부분의 사람들은 cgroups가 무엇인지, 어떻게 효과적으로 사용하는지 알지 못했습니다.
그러다 2013년, Docker가 등장하면서 모든 것이 바뀌었습니다.
Docker는 컨테이너나 cgroups를 발명하지 않았습니다. 이 기술들은 이미 Linux 커널 안에 숨어 있었습니다.
Docker가 했던 영리한 일은 이러한 기존 Linux 기술들을 누구나 사용할 수 있는 단순한 인터페이스로 감싸고 단순화한 것입니다. Docker는 cgroups의 복잡성을 추상화했습니다.
수 시간의 설정 대신, 개발자들은 이제 단일 docker run 명령어로 컨테이너를 배포할 수 있게 되었고, 이 기술은 시스템 수준 전문가뿐만 아니라 모든 사람이 접근할 수 있게 되었습니다.
Docker는 컨테이너 기술을 민주화했습니다. 이전에는 Google과 같은 기업에게만 예약되어 있던 도구의 힘을 일반 개발자들의 손에 쥐어준 것입니다.
timeline
title 컨테이너 기술의 진화
2000 : FreeBSD Jails
: 최초의 OS 수준 가상화
2006 : Process Containers (Google)
: 나중에 cgroups로 명명
2007 : cgroups Linux 커널 병합
: Linux 2.6.24
2008 : LXC (Linux Containers)
: Namespace + cgroups 결합
2013 : Docker 등장
: 컨테이너 기술의 민주화
2014 : Kubernetes 발표
: Google Borg의 오픈소스화
2015 : CNCF 설립
: Kubernetes 기부
2020 : Kubernetes가 Docker 런타임 지원 중단 발표
: containerd/CRI-O로 전환
2024 : eBPF 기반 네트워킹 주류화
: Cilium CNCF Graduated
핵심 메시지: Linux를 먼저 배워라
Namespaces, cgroups (Control Groups), iptables/nftables, seccomp/AppArmor, OverlayFS, eBPF는 단순한 Linux 커널 기능이 아닙니다.
이것들은 Kubernetes와 Docker의 강력한 기능들—컨테이너 격리, 리소스 사용 제한, 네트워크 정책, 런타임 보안, 이미지 관리, 네트워킹 및 관측성 구현—의 기반이 됩니다.
containerd와 kubelet부터 Pod 보안과 볼륨 마운트까지, 각 컴포넌트는 핵심 Linux 기능에 의존합니다.
"Kubernetes는 강력하지만, 진짜 일은 Linux 엔진실에서 일어납니다."
Linux namespaces, cgroups, 네트워크 필터링 및 기타 기능들이 어떻게 작동하는지 이해하면, Kubernetes를 더 빨리 파악할 수 있을 뿐만 아니라 훨씬 더 효과적으로 트러블슈팅하고, 보안을 강화하고, 최적화할 수 있습니다.
Docker를 깊이 이해하려면, Linux 컨테이너가 커널 기능을 사용하여 시스템의 격리된 뷰를 가진 프로세스에 불과하다는 것을 탐구해야 합니다. 이러한 도구들을 직접 실습함으로써, Docker가 강력한 Linux 기본 요소들 위의 편리한 래퍼처럼 보이게 하는 기초 지식을 얻게 됩니다.
Linux를 먼저 배우세요. Kubernetes와 Docker가 '클릭'하게 될 것입니다.
전체 아키텍처: Linux 기반 위의 Kubernetes
아래 다이어그램은 Kubernetes의 모든 핵심 기능이 어떻게 Linux 커널 기술 위에 구축되는지 보여줍니다.
flowchart TB
subgraph K8S["☸️ Kubernetes 계층"]
direction TB
POD["Pod"]
SVC["Service"]
NP["NetworkPolicy"]
PSS["Pod Security Standards"]
PV["PersistentVolume"]
OBS["Observability"]
end
subgraph RUNTIME["🐳 컨테이너 런타임 계층"]
direction TB
CONTAINERD["containerd"]
CRIO["CRI-O"]
RUNC["runc"]
end
subgraph LINUX["🐧 Linux 커널 계층"]
direction TB
NS["Namespaces<br/>(격리)"]
CG["cgroups<br/>(리소스 제한)"]
NETFILTER["iptables/nftables<br/>(네트워크 필터링)"]
LSM["seccomp/AppArmor/SELinux<br/>(보안)"]
OVERLAY["OverlayFS<br/>(파일시스템)"]
EBPF["eBPF<br/>(관측성/네트워킹)"]
end
POD --> NS
POD --> CG
SVC --> NETFILTER
NP --> NETFILTER
NP --> EBPF
PSS --> LSM
PV --> OVERLAY
OBS --> EBPF
CONTAINERD --> RUNC
CRIO --> RUNC
RUNC --> NS
RUNC --> CG
RUNC --> OVERLAY
1. Linux Namespaces: 컨테이너 격리의 근간
Namespace(네임스페이스)를 이해하는 가장 쉬운 방법은 "가상의 방"으로 생각하는 것입니다.
여러분이 큰 사무실 건물에서 일한다고 상상해보세요. 모든 직원이 같은 공간에서 일하면 소음, 프라이버시, 리소스 충돌 문제가 생깁니다. 그래서 우리는 파티션으로 나눈 개별 사무실을 만듭니다.
Linux Namespace도 마찬가지입니다. 하나의 Linux 커널 위에서 여러 프로세스가 실행되지만, 각 프로세스는 마치 자신만의 독립된 시스템에서 실행되는 것처럼 느끼게 해줍니다.
flowchart LR
subgraph HOST["호스트 시스템"]
KERNEL["Linux 커널"]
subgraph NS1["컨테이너 A의 뷰"]
PID1["PID 1: nginx"]
NET1["eth0: 172.17.0.2"]
MNT1["/app/data"]
end
subgraph NS2["컨테이너 B의 뷰"]
PID2["PID 1: redis"]
NET2["eth0: 172.17.0.3"]
MNT2["/var/lib/redis"]
end
subgraph REAL["실제 호스트 뷰"]
RPID1["PID 12345: nginx"]
RPID2["PID 12400: redis"]
RNET["eth0: 192.168.1.100"]
end
end
NS1 -.->|"격리된 뷰"| KERNEL
NS2 -.->|"격리된 뷰"| KERNEL
REAL -->|"실제 상태"| KERNEL
8가지 Namespace 유형 상세 설명
Linux 커널은 현재 8종류의 네임스페이스를 지원합니다. 각각이 어떤 리소스를 격리하는지 살펴보겠습니다.
| Namespace | Clone Flag | 격리 대상 | 실생활 비유 | Kubernetes 활용 |
|---|---|---|---|---|
| PID | CLONE_NEWPID | 프로세스 ID 공간 | 각 방에서 직원 번호가 1번부터 시작 | Pod 내 컨테이너가 PID 1을 가짐 |
| Network | CLONE_NEWNET | 네트워크 스택, 인터페이스, 포트 | 각 방에 독립된 전화번호 | Pod마다 고유 IP 주소 할당 |
| Mount | CLONE_NEWNS | 마운트 포인트 | 각 방에 독립된 서류함 | 컨테이너 rootfs, 볼륨 마운트 |
| User | CLONE_NEWUSER | UID/GID 매핑 | 각 방에서 "사장"이지만 건물에선 일반 직원 | Rootless 컨테이너 |
| IPC | CLONE_NEWIPC | 공유 메모리, 세마포어 | 각 방의 독립된 화이트보드 | Pod 간 IPC 격리 |
| UTS | CLONE_NEWUTS | 호스트명, 도메인명 | 각 방의 독립된 명패 | 컨테이너별 hostname |
| Cgroup | CLONE_NEWCGROUP | cgroup 계층 뷰 | 각 방에서 자신의 예산만 보임 | 호스트 cgroup 정보 숨김 |
| Time | CLONE_NEWTIME | monotonic/boot 시계 | 각 방의 시계가 다른 시간대 | 컨테이너 마이그레이션 (커널 5.6+) |
PID Namespace 깊이 들여다보기
컨테이너 내부에서 ps aux를 실행하면 프로세스가 PID 1부터 시작하는 것을 볼 수 있습니다. 이것이 PID Namespace의 마법입니다.
# 호스트에서 실행
$ ps aux | grep nginx
root 12345 0.0 0.1 nginx: master process
# 컨테이너 내부에서 실행
$ docker exec -it my-nginx ps aux
PID USER COMMAND
1 root nginx: master process
왜 PID 1이 중요할까요?
Unix/Linux에서 PID 1 프로세스는 특별한 의미를 가집니다. 첫째로 Init 프로세스 역할을 하여 고아(orphan) 프로세스의 부모가 됩니다. 둘째로 신호 처리 책임이 있어 SIGTERM, SIGKILL 등의 신호를 적절히 처리해야 합니다. 셋째로 좀비 프로세스 회수 의무가 있어 자식 프로세스가 종료되면 wait()를 호출해 정리해야 합니다.
컨테이너에서 애플리케이션이 PID 1로 실행되면, 이러한 책임을 져야 합니다. 이것이 많은 컨테이너 이미지가 tini나 dumb-init 같은 경량 init 시스템을 사용하는 이유입니다.
Network Namespace와 Kubernetes Pod
Kubernetes Pod의 네트워킹을 이해하려면 먼저 Network Namespace를 알아야 합니다.
flowchart TB
subgraph NODE["Kubernetes 노드"]
subgraph HOST_NS["호스트 Network Namespace"]
ETH0["eth0<br/>192.168.1.100"]
BRIDGE["cni0 브릿지<br/>10.244.0.1"]
end
subgraph POD1_NS["Pod A Network Namespace"]
VETH1A["eth0<br/>10.244.0.10"]
end
subgraph POD2_NS["Pod B Network Namespace"]
VETH2A["eth0<br/>10.244.0.11"]
end
VETH1B["veth123"]
VETH2B["veth456"]
end
VETH1A <-->|"veth pair"| VETH1B
VETH2A <-->|"veth pair"| VETH2B
VETH1B --> BRIDGE
VETH2B --> BRIDGE
BRIDGE --> ETH0
모든 Kubernetes Pod에는 숨겨진 pause 컨테이너(sandbox/infra 컨테이너)가 있습니다. 이 pause 컨테이너가 Pod의 Network, IPC, UTS 네임스페이스를 생성하고 유지하며, 다른 애플리케이션 컨테이너들이 이를 공유합니다.
# Pod 네임스페이스 공유 설정 예시
apiVersion: v1
kind: Pod
metadata:
name: shared-namespace-pod
spec:
shareProcessNamespace: true # PID 네임스페이스 공유
hostNetwork: false # 호스트 네트워크 사용 안 함 (기본값)
hostPID: false # 호스트 PID 사용 안 함 (기본값)
hostIPC: false # 호스트 IPC 사용 안 함 (기본값)
containers:
- name: app
image: nginx
- name: sidecar
image: busybox
# 두 컨테이너는 같은 네트워크와 PID 네임스페이스를 공유
2024-2025 최신 동향: User Namespace 지원
Kubernetes 1.30부터 User Namespace가 Beta로 승격되어 기본 활성화되었습니다. 이는 컨테이너 보안의 큰 진전입니다.
문제 상황: 컨테이너 내부에서 root(UID 0)로 실행되는 프로세스가 컨테이너 탈출(escape)에 성공하면, 호스트에서도 root 권한을 가지게 됩니다.
해결책: User Namespace를 사용하면 컨테이너 내부의 root가 호스트에서는 높은 번호의 비특권 사용자(예: UID 100000)로 매핑됩니다.
apiVersion: v1
kind: Pod
spec:
hostUsers: false # User Namespace 활성화 (Kubernetes 1.30+)
containers:
- name: app
image: nginx
securityContext:
runAsUser: 0 # 컨테이너 내부에서는 root
# 하지만 호스트에서는 UID 100000+ 으로 매핑됨
요구사항으로는 Linux 커널 6.3+, containerd 1.7+ 또는 CRI-O 1.25+, 그리고 idmap 마운트를 지원하는 파일시스템 (ext4, XFS, btrfs 등)이 필요합니다.
2. cgroups: 리소스 제한과 QoS의 핵심
cgroups (Control Groups)를 이해하는 가장 쉬운 방법은 "예산 관리"로 생각하는 것입니다.
회사에 여러 부서가 있다고 상상해보세요. 각 부서에는 정해진 예산이 있고, 그 예산 내에서만 지출할 수 있습니다. 한 부서가 예산을 초과하면 더 이상 지출할 수 없고, 다른 부서의 예산에 영향을 주지 않습니다.
cgroups도 마찬가지입니다. 각 프로세스 그룹(컨테이너)에 CPU, 메모리, I/O 등의 리소스 사용량을 제한합니다.
flowchart TB
subgraph CGROUP["cgroups 계층 구조 (v2)"]
ROOT["/ (root cgroup)"]
subgraph KUBEPODS["kubepods.slice"]
BESTEFFORT["besteffort/"]
BURSTABLE["burstable/"]
GUARANTEED["guaranteed/"]
subgraph POD1["pod-abc123/"]
C1["container1/<br/>cpu.max=50000 100000<br/>memory.max=128Mi"]
C2["container2/<br/>cpu.max=100000 100000<br/>memory.max=256Mi"]
end
end
SYSTEM["system.slice"]
USER["user.slice"]
end
ROOT --> KUBEPODS
ROOT --> SYSTEM
ROOT --> USER
KUBEPODS --> BESTEFFORT
KUBEPODS --> BURSTABLE
KUBEPODS --> GUARANTEED
BURSTABLE --> POD1
POD1 --> C1
POD1 --> C2
cgroups v1 vs v2: 핵심 차이점
Kubernetes는 현재 cgroups v1에서 v2로의 전환기에 있습니다. 이 차이를 이해하는 것이 중요합니다.
| 특성 | cgroups v1 | cgroups v2 |
|---|---|---|
| 아키텍처 | 컨트롤러별 개별 계층 구조 | 단일 통합 계층 구조 |
| 파일 구조 | /sys/fs/cgroup/cpu/, /memory/ 등 분리 |
/sys/fs/cgroup/ 통합 |
| 메모리 제어 | memory.limit_in_bytes |
memory.max, memory.high, memory.low |
| CPU 제어 | cpu.shares, cpu.cfs_quota_us |
cpu.weight, cpu.max |
| PSI 지원 | 미지원 | 지원 (리소스 압력 모니터링) |
| OOM 관리 | 프로세스 단위 | cgroup 전체 단위 가능 |
flowchart LR
subgraph V1["cgroups v1 구조"]
direction TB
CPU1["/sys/fs/cgroup/cpu/"]
MEM1["/sys/fs/cgroup/memory/"]
IO1["/sys/fs/cgroup/blkio/"]
CPU1 --> PROC1["container-abc"]
MEM1 --> PROC2["container-abc"]
IO1 --> PROC3["container-abc"]
end
subgraph V2["cgroups v2 구조"]
direction TB
ROOT2["/sys/fs/cgroup/"]
ROOT2 --> UNIFIED["container-abc/<br/>cpu.max<br/>memory.max<br/>io.max"]
end
v2의 핵심 장점: memory.high
cgroups v2의 memory.high는 OOM kill 전에 쓰로틀링(throttling)을 적용합니다. 이는 갑작스러운 프로세스 종료 대신 성능 저하를 선택할 수 있게 해줍니다.
# cgroups v2 메모리 설정 예시
# memory.max: 하드 제한 - 초과 시 OOM kill
# memory.high: 소프트 제한 - 초과 시 쓰로틀링
# memory.low: 보호 수준 - 이 이하로는 회수 안 함
# memory.min: 절대 보호 - 절대 회수 안 함
echo 134217728 > /sys/fs/cgroup/mycontainer/memory.max # 128Mi
echo 67108864 > /sys/fs/cgroup/mycontainer/memory.high # 64Mi (쓰로틀링 시작)
echo 33554432 > /sys/fs/cgroup/mycontainer/memory.low # 32Mi (보호)
Kubernetes 리소스와 cgroups 매핑
Kubernetes에서 Pod spec에 정의하는 resources.requests와 resources.limits가 어떻게 cgroups로 변환되는지 이해하는 것이 중요합니다.
apiVersion: v1
kind: Pod
metadata:
name: resource-demo
spec:
containers:
- name: app
image: nginx
resources:
requests:
cpu: "250m" # 0.25 CPU -> cgroups cpu.weight에 영향
memory: "64Mi" # -> cgroups memory.min (Memory QoS 활성화 시)
limits:
cpu: "500m" # 0.5 CPU -> cgroups cpu.max = "50000 100000"
memory: "128Mi" # -> cgroups memory.max = 134217728
| Kubernetes 설정 | cgroups v2 파일 | 의미 |
|---|---|---|
limits.cpu: "500m" |
cpu.max = "50000 100000" |
100ms 주기 중 50ms CPU 사용 가능 (50%) |
requests.cpu: "250m" |
cpu.weight |
상대적 CPU 가중치 (스케줄링용) |
limits.memory: "128Mi" |
memory.max = 134217728 |
하드 제한, 초과 시 OOM kill |
requests.memory: "64Mi" |
memory.min |
커널이 절대 회수하지 않는 메모리 (Memory QoS) |
2024-2025 최신 동향: cgroups v1 지원 중단 일정
| Kubernetes 버전 | 상태 | 주요 내용 |
|---|---|---|
| 1.25 (2022.08) | cgroups v2 GA | 공식 정식 지원 시작 |
| 1.31 (2024.08) | cgroups v1 유지보수 모드 | 신규 기능 동결, 버그 수정만 |
| 1.35 (예정) | cgroups v1 Deprecated | kubelet 기본 시작 불가, 명시적 플래그 필요 |
현재 배포판별 기본값: Ubuntu 22.04+, Debian 11+는 cgroups v2 기본이며, RHEL 9, Fedora 31+도 cgroups v2 기본입니다. Amazon Linux 2023도 cgroups v2 기본이고, GKE, EKS, AKS 등 주요 관리형 Kubernetes 서비스는 대부분 cgroups v2로 전환 완료되었습니다.
# 현재 시스템의 cgroup 버전 확인
stat -fc %T /sys/fs/cgroup/
# 출력이 "cgroup2fs" -> v2
# 출력이 "tmpfs" -> v1
kubelet cgroup 드라이버 설정: systemd를 사용해야 cgroups v2와 호환됩니다.
# kubelet 설정 (/var/lib/kubelet/config.yaml)
cgroupDriver: systemd # cgroupfs 대신 systemd 사용 권장
3. iptables와 nftables: 네트워크 정책의 뒷받침
iptables/nftables를 이해하는 가장 쉬운 방법은 "공항 보안 검색대"로 생각하는 것입니다.
공항에서 모든 승객은 보안 검색대를 통과해야 합니다. 검색대는 다양한 규칙(금속 탐지, 액체 용량 제한 등)을 적용하여 승객을 통과시키거나 차단합니다.
Linux의 iptables/nftables도 마찬가지입니다. 모든 네트워크 패킷은 규칙 체인을 통과하며, 각 규칙에 따라 ACCEPT(허용), DROP(차단), REJECT(거부 응답과 함께 차단) 등의 결정이 내려집니다.
flowchart LR
subgraph NETFILTER["Linux Netfilter 프레임워크"]
direction TB
subgraph CHAINS["체인 (Chains)"]
PREROUTE["PREROUTING"]
INPUT["INPUT"]
FORWARD["FORWARD"]
OUTPUT["OUTPUT"]
POSTROUTE["POSTROUTING"]
end
subgraph TABLES["테이블 (Tables)"]
RAW["raw"]
MANGLE["mangle"]
NAT["nat"]
FILTER["filter"]
end
end
PKT_IN["📥 들어오는 패킷"] --> PREROUTE
PREROUTE -->|"로컬 목적지"| INPUT
PREROUTE -->|"다른 곳으로 전달"| FORWARD
INPUT --> LOCAL["💻 로컬 프로세스"]
LOCAL --> OUTPUT
OUTPUT --> POSTROUTE
FORWARD --> POSTROUTE
POSTROUTE --> PKT_OUT["📤 나가는 패킷"]
Kubernetes Service와 iptables
Kubernetes Service는 ClusterIP를 통해 Pod들을 로드밸런싱합니다. 이 마법은 iptables(또는 IPVS, nftables)로 구현됩니다.
# Kubernetes Service의 iptables 규칙 예시
# Service: my-service (ClusterIP: 10.96.0.100:80)
# Backend Pods: 10.244.0.10:8080, 10.244.0.11:8080
# 1. KUBE-SERVICES 체인에서 Service IP 매칭
-A KUBE-SERVICES -d 10.96.0.100/32 -p tcp --dport 80 \
-j KUBE-SVC-XXXXX
# 2. KUBE-SVC-XXXXX에서 랜덤 로드밸런싱
-A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.5 \
-j KUBE-SEP-POD1
-A KUBE-SVC-XXXXX \
-j KUBE-SEP-POD2
# 3. KUBE-SEP-POD1에서 DNAT (목적지 IP 변경)
-A KUBE-SEP-POD1 -p tcp -j DNAT --to-destination 10.244.0.10:8080
# 4. KUBE-SEP-POD2에서 DNAT
-A KUBE-SEP-POD2 -p tcp -j DNAT --to-destination 10.244.0.11:8080
sequenceDiagram
participant Client as 클라이언트 Pod
participant IPT as iptables/kube-proxy
participant SVC as Service IP<br/>10.96.0.100
participant Pod1 as Pod 1<br/>10.244.0.10
participant Pod2 as Pod 2<br/>10.244.0.11
Client->>SVC: 요청 (dst: 10.96.0.100:80)
SVC->>IPT: PREROUTING 체인
Note over IPT: DNAT 규칙 적용<br/>(50% 확률 선택)
alt Pod 1 선택
IPT->>Pod1: dst 변경: 10.244.0.10:8080
Pod1-->>Client: 응답
else Pod 2 선택
IPT->>Pod2: dst 변경: 10.244.0.11:8080
Pod2-->>Client: 응답
end
kube-proxy 모드 비교
kube-proxy는 세 가지 모드를 지원합니다. 각 모드의 특성을 이해하면 클러스터 규모에 맞는 선택을 할 수 있습니다.
| 모드 | 성능 | 특징 | 권장 환경 |
|---|---|---|---|
| iptables | O(n) 선형 | 안정적, 호환성 우수, 규칙이 많아지면 느려짐 | 소규모 클러스터 (<500 서비스) |
| IPVS | O(1) 해시 | 다양한 LB 알고리즘, 성숙한 기술 | 대규모 클러스터 (>1,000 서비스) |
| nftables | O(1) 해시 | 증분 업데이트, 일정한 지연, 최신 기술 | 최신 커널 (5.13+), 미래 기본값 |
2024-2025 최신 동향: nftables 모드 GA
Kubernetes 1.29에서 Alpha로 도입된 nftables 모드가 1.31에서 GA(General Availability)가 되었습니다.
nftables의 핵심 장점은 다음과 같습니다. 첫째, Verdict Maps를 통해 해시 테이블 기반 O(1) 조회가 가능합니다. 둘째, 증분 업데이트로 전역 락 없이 규칙 변경이 가능합니다. 셋째, 서비스 수에 관계없이 일정한 지연 시간을 유지합니다.
# nftables 버딕트 맵 예시
table ip kube-proxy {
map service-ips {
type ipv4_addr . inet_service : verdict
elements = {
172.30.0.41 . 80 : goto svc-chain1,
172.30.0.42 . 443 : goto svc-chain2
}
}
}
벤치마크 결과: 10,000개 서비스 기준, iptables 대비 20배 이상 낮은 지연 시간.
4. seccomp, AppArmor, SELinux: 런타임 보안
컨테이너 런타임 보안을 이해하는 가장 쉬운 방법은 "권한 등급"으로 생각하는 것입니다.
회사에서 직원마다 접근 권한이 다릅니다. 인턴은 사무실 출입만 가능하고, 일반 직원은 사무실 + 회의실 + 복사실에 접근할 수 있으며, 관리자는 위 모든 곳 + 서버실까지 접근 가능합니다.
Linux 보안 모듈도 마찬가지입니다. seccomp은 프로세스가 호출할 수 있는 시스템 콜을 제한합니다. AppArmor는 경로 기반으로 파일/네트워크 접근을 제어합니다. SELinux는 레이블 기반으로 모든 객체 접근을 제어합니다.
flowchart TB
subgraph SECURITY["컨테이너 보안 계층"]
direction TB
subgraph SECCOMP["seccomp (시스템 콜 필터링)"]
SC_ALLOW["허용된 syscall<br/>read, write, open..."]
SC_BLOCK["차단된 syscall<br/>mount, reboot, kexec_load..."]
end
subgraph APPARMOR["AppArmor (경로 기반 MAC)"]
AA_PATH["경로 규칙<br/>/etc/passwd r,<br/>/tmp/** rw,"]
AA_NET["네트워크 규칙<br/>network tcp,"]
end
subgraph SELINUX["SELinux (레이블 기반 MAC)"]
SE_TYPE["타입 강제<br/>container_t"]
SE_MCS["MCS 레이블<br/>s0:c123,c456"]
end
end
PROCESS["컨테이너 프로세스"] --> SECCOMP
SECCOMP --> APPARMOR
APPARMOR --> SELINUX
SELINUX --> KERNEL["Linux 커널"]
seccomp: 시스템 콜 필터링
seccomp (Secure Computing Mode)은 프로세스가 커널에 요청할 수 있는 시스템 콜을 제한합니다.
컨테이너 런타임의 기본 seccomp 프로필은 약 44-64개의 위험한 syscall을 차단합니다. mount, umount는 파일시스템 조작을, reboot, kexec_load는 시스템 제어를, ptrace는 다른 프로세스 디버깅을, personality는 실행 도메인 변경을 담당하는데 이들이 모두 차단됩니다.
# Pod에서 seccomp 설정 (Kubernetes 1.19+)
apiVersion: v1
kind: Pod
metadata:
name: seccomp-demo
spec:
securityContext:
seccompProfile:
type: RuntimeDefault # 런타임 기본 프로필 사용
containers:
- name: app
image: nginx
Kubernetes 1.27부터 SeccompDefault 기능이 GA로, --seccomp-default kubelet 플래그로 모든 Pod에 기본 프로필을 자동 적용할 수 있습니다.
Pod Security Standards (PSS)와 보안
Kubernetes 1.25에서 GA된 Pod Security Admission (PSA)은 세 가지 보안 정책 레벨을 정의합니다.
| 레벨 | 설명 | seccomp 요구사항 | 사용 사례 |
|---|---|---|---|
| Privileged | 제한 없음 | 없음 | 시스템 컴포넌트 |
| Baseline | 알려진 권한 상승 방지 | Unconfined 명시 금지 | 일반 워크로드 |
| Restricted | 현재 하드닝 모범 사례 | RuntimeDefault 필수 | 보안 민감 워크로드 |
# 네임스페이스에 보안 정책 적용
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
2024-2025 최신 동향: AppArmor GA
Kubernetes 1.30에서 AppArmor가 정식 securityContext 필드로 승격되었습니다 (기존에는 annotation 사용).
# 이전 방식 (annotation)
metadata:
annotations:
container.apparmor.security.beta.kubernetes.io/app: localhost/custom-profile
# 새로운 방식 (Kubernetes 1.30+)
spec:
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: custom-profile
containers:
- name: app
image: nginx
5. OverlayFS: 컨테이너 이미지의 비밀
OverlayFS를 이해하는 가장 쉬운 방법은 "투명 필름 레이어"로 생각하는 것입니다.
애니메이션 제작을 상상해보세요. 배경 그림 위에 투명한 셀룰로이드 필름을 올려 캐릭터를 그립니다. 배경은 그대로 두고 캐릭터만 바꾸면 새로운 장면이 됩니다.
OverlayFS도 마찬가지입니다. lowerdir (읽기 전용)은 배경 그림, 즉 컨테이너 이미지 레이어입니다. upperdir (읽기/쓰기)는 투명 필름, 즉 컨테이너 변경사항입니다. merged (통합 뷰)는 완성된 장면, 즉 컨테이너가 보는 파일시스템입니다.
flowchart TB
subgraph OVERLAY["OverlayFS 구조"]
direction TB
MERGED["📁 /merged (통합 뷰)<br/>컨테이너가 보는 파일시스템"]
subgraph UPPER["upperdir (읽기/쓰기)"]
U_NEW["새 파일: app.log"]
U_MOD["수정된 파일: nginx.conf"]
U_DEL["삭제 마커: .wh.old.txt"]
end
subgraph LOWER["lowerdir (읽기 전용)"]
subgraph L3["Layer 3: 애플리케이션"]
L3_APP["app/main.py"]
end
subgraph L2["Layer 2: 의존성"]
L2_PKG["usr/lib/python3"]
end
subgraph L1["Layer 1: 베이스 이미지"]
L1_BIN["bin/bash"]
L1_OLD["old.txt ❌"]
end
end
end
MERGED --> UPPER
MERGED --> LOWER
UPPER -.->|"Copy-on-Write"| L3
Copy-on-Write (CoW) 메커니즘
OverlayFS의 핵심 최적화는 Copy-on-Write입니다.
읽기 작업은 lowerdir에서 직접 읽으며 복사가 없습니다. 쓰기 작업은 lowerdir의 파일을 수정할 때만 upperdir로 복사 후 수정합니다. 삭제 작업은 "whiteout" 특수 파일로 삭제를 표시하며 실제 삭제는 없습니다.
# Copy-on-Write 동작 확인 예시
# 1. 컨테이너에서 /etc/nginx/nginx.conf 수정 시
# 2. 해당 파일이 upperdir로 복사됨
# 3. upperdir의 복사본이 수정됨
# 4. lowerdir의 원본은 그대로 유지
# 컨테이너 레이어 확인
docker inspect nginx:latest --format='{{json .RootFS.Layers}}' | jq
# [
# "sha256:abc123...", # Layer 1
# "sha256:def456...", # Layer 2
# "sha256:ghi789..." # Layer 3
# ]
컨테이너 런타임 스토리지 구현
containerd는 Snapshotter 아키텍처를 사용합니다:
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd]
snapshotter = "overlayfs" # 기본값
# 대안: native, btrfs, zfs, devmapper
주의사항: XFS 파일시스템 사용 시 ftype=1로 포맷해야 overlay2가 동작합니다.
# XFS ftype 확인
xfs_info /dev/sdXn | grep ftype
# ftype=1 이어야 함
2024-2025 최신 동향: Lazy Pulling과 eStargz
연구에 따르면 컨테이너 시작 시간의 76%가 이미지 pull에 소요되지만, 실제로 읽는 데이터는 6.4%에 불과합니다.
eStargz (Extended Seekable tar.gz)는 이 문제를 해결합니다. 이미지 전체를 다운로드하지 않고 필요한 파일만 on-demand로 가져옵니다. 기존 레지스트리와 호환되며, containerd의 stargz-snapshotter로 활성화할 수 있습니다.
6. eBPF: Kubernetes 네트워킹과 관측성의 미래
eBPF (extended Berkeley Packet Filter)를 이해하는 가장 쉬운 방법은 "커널 내 프로그래밍"으로 생각하는 것입니다.
기존에는 커널 기능을 수정하려면 커널 모듈을 작성하거나 커널 자체를 수정해야 했습니다. 이는 위험하고, 복잡하고, 커널 버전에 종속적이었습니다.
eBPF는 안전한 샌드박스 환경에서 커널 내 프로그램을 실행할 수 있게 해줍니다. 네트워크 패킷 처리, 시스템 콜 추적, 성능 모니터링, 보안 정책 적용을 모두 커널 수정 없이, 런타임에, 안전하게 할 수 있습니다.
flowchart TB
subgraph EBPF["eBPF 아키텍처"]
direction TB
subgraph USER["사용자 공간"]
APP["애플리케이션<br/>(Cilium, Falco, Pixie...)"]
LOADER["eBPF 로더"]
MAP_USER["Maps 접근"]
end
subgraph KERNEL["커널 공간"]
VERIFIER["Verifier<br/>(안전성 검증)"]
JIT["JIT 컴파일러"]
subgraph HOOKS["Hook Points"]
XDP["XDP<br/>(NIC 드라이버)"]
TC["TC<br/>(Traffic Control)"]
SOCKET["Socket"]
SYSCALL["Syscall"]
LSM["LSM"]
end
MAPS["eBPF Maps<br/>(커널-유저 데이터 공유)"]
end
end
APP --> LOADER
LOADER --> VERIFIER
VERIFIER -->|"검증 통과"| JIT
JIT --> HOOKS
HOOKS --> MAPS
MAPS --> MAP_USER
Cilium: eBPF 기반 Kubernetes 네트워킹
Cilium은 CNCF Graduated 프로젝트로, iptables 없이 순수 eBPF로 Kubernetes 네트워킹을 구현합니다.
핵심 기능으로는 kube-proxy 대체(소켓 레벨 로드밸런싱), L7 네트워크 정책(HTTP/gRPC/DNS 기반 필터링), Identity 기반 보안(IP 대신 워크로드 레이블 사용), Hubble(실시간 네트워크 관측성), 서비스 메시(사이드카 없이 mTLS, L7 라우팅)가 있습니다.
# Cilium L7 네트워크 정책 예시
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: api-policy
spec:
endpointSelector:
matchLabels:
app: api-server
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: GET
path: "/api/v1/users.*"
- method: POST
path: "/api/v1/orders"
flowchart LR
subgraph CILIUM["Cilium eBPF 데이터플레인"]
direction TB
subgraph POD1["Frontend Pod"]
APP1["Frontend App"]
BPF1["eBPF (socket)"]
end
subgraph POD2["API Pod"]
APP2["API Server"]
BPF2["eBPF (socket)"]
end
subgraph POD3["DB Pod"]
APP3["Database"]
BPF3["eBPF (socket)"]
end
HUBBLE["Hubble<br/>(관측성)"]
end
APP1 -->|"L7 정책 검사"| BPF1
BPF1 -->|"GET /api/v1/users ✅"| BPF2
BPF2 --> APP2
APP2 --> BPF2
BPF2 -->|"TCP:5432 ✅"| BPF3
BPF3 --> APP3
BPF1 -.-> HUBBLE
BPF2 -.-> HUBBLE
BPF3 -.-> HUBBLE
주요 클라우드의 eBPF 채택
GKE Dataplane V2는 Cilium 기반으로 기본 옵션입니다. Azure CNI Powered by Cilium은 AKS에서 선택 가능합니다. AWS VPC CNI는 eBPF 지원 추가가 진행 중입니다.
2024-2025 최신 동향
Linux 6.9 - BPF Arena는 커널-유저스페이스 간 최대 4GB 공유 메모리를 지원합니다. Linux 6.12 - sched_ext는 eBPF로 CPU 스케줄러 정책 커스터마이징을 가능하게 합니다. eBPF for Windows는 Microsoft가 개발 중이며 Windows 11/Server 2022+ 지원 예정입니다.
정리: Linux 기술과 Kubernetes 매핑 요약
mindmap
root((Kubernetes))
Pod 격리
Linux Namespaces
PID
Network
Mount
User 1.30 Beta
IPC
UTS
Cgroup
리소스 관리
cgroups v2
cpu.max
memory.max
memory.high
PSI 모니터링
네트워킹
iptables/nftables
Service ClusterIP
NodePort
LoadBalancer
eBPF/Cilium
L7 정책
관측성
보안
seccomp
RuntimeDefault
AppArmor
경로 기반 MAC
SELinux
레이블 기반 MAC
PSS/PSA
Baseline
Restricted
스토리지
OverlayFS
이미지 레이어
Copy-on-Write
Snapshotter
실무 체크리스트
환경 점검 명령어 모음:
# 1. cgroup 버전 확인
stat -fc %T /sys/fs/cgroup/
# cgroup2fs = v2, tmpfs = v1
# 2. 커널 버전 확인 (5.10+ 권장)
uname -r
# 3. XFS ftype 확인 (overlay2용)
xfs_info /dev/sdXn | grep ftype
# 4. kubelet cgroup 드라이버 확인
cat /var/lib/kubelet/config.yaml | grep cgroupDriver
# 5. 현재 네임스페이스 확인
ls -la /proc/self/ns/
# 6. seccomp 지원 확인
grep SECCOMP /boot/config-$(uname -r)
# 7. eBPF 지원 확인
ls /sys/fs/bpf/
권장 설정:
| 구성요소 | 권장 설정 | 비고 |
|---|---|---|
| Linux 커널 | 5.10+ (6.x 권장) | BTF 활성화, eBPF CO-RE |
| cgroups | v2 + systemd 드라이버 | 1.35부터 v1 deprecated |
| 컨테이너 런타임 | containerd 2.0+ / CRI-O 1.25+ | User Namespace 지원 |
| 스토리지 드라이버 | overlay2 | XFS는 ftype=1 필수 |
| 네트워킹 | Cilium (eBPF) 또는 nftables | 대규모 환경 권장 |
| 보안 | PSS Restricted + seccomp RuntimeDefault | 프로덕션 필수 |
참고 자료
공식 문서:
Kubernetes Enhancement Proposals (KEPs):
'k8s > K8s Annotated' 카테고리의 다른 글
| 왜 etcd는 대규모 Kubernetes 클러스터에서 한계에 부딪히는가 (0) | 2026.02.25 |
|---|---|
| Ingress-NGINX 지원 종료 완벽 가이드 — Gateway API 마이그레이션, 그 전에 알아야 할 모든 것 (0) | 2026.02.05 |
| Kubernetes Churn (0) | 2025.12.22 |
| Kubernetes cgroups (2025) (0) | 2025.12.21 |