Skip to Content
Bloglxcfs-admission-webhook 프로젝트 개요

lxcfs-admission-webhook 프로젝트 개요

예전에 LXCFS와 Admission Controller를 같이 써보는 글을 짧게 남긴 적이 있습니다. 그때는 “LXCFS를 Pod에 자동으로 붙이면 컨테이너 안에서 보이는 CPU/메모리 값이 더 현실적이 된다” 정도의 실험에 가까웠습니다.

이번에는 그 아이디어를 조금 더 운영 가능한 형태로 정리한 프로젝트가 lxcfs-admission-webhook 입니다.

간단히 말하면, namespace에 라벨 하나를 붙이면 새로 생성되는 Pod에 LXCFS 기반 /proc, /sys 파일들을 자동으로 마운트해주는 Kubernetes Mutating Admission Webhook입니다.


문제는 컨테이너 안의 /proc 값입니다

컨테이너에 CPU 1개, 메모리 1Gi를 줬다고 해서 모든 런타임이 그 값을 정확히 보는 것은 아닙니다.

많은 런타임과 도구는 아직도 다음 파일들을 읽어서 현재 환경의 자원을 추정합니다.

/proc/cpuinfo /proc/meminfo /proc/stat /proc/loadavg /sys/devices/system/cpu/online

그런데 이 파일들이 Pod의 cgroup limit이 아니라 노드의 host 값을 보여주면 문제가 생깁니다.

예를 들어 노드가 32 core이고 Pod limit은 2 core인데, 애플리케이션이 32 core라고 믿으면 thread pool이나 worker 수가 과하게 잡힐 수 있습니다. 메모리도 마찬가지입니다. 런타임이 실제 limit보다 큰 메모리를 보고 heap을 잡으면 OOMKill이 더 쉽게 발생합니다.

JVM, Node.js, Go, Python, Ruby 같은 런타임뿐 아니라 nproc, free, top 같은 운영 도구도 이런 값에 영향을 받습니다.

LXCFS는 이 문제를 줄이기 위해 /proc 일부 파일을 컨테이너 관점으로 가상화해줍니다. 다만 모든 Pod에 그 mount를 직접 넣는 것은 귀찮고 실수하기 쉽습니다.

그래서 admission webhook으로 자동화하는 방식이 필요합니다.


프로젝트가 하는 일

lxcfs-admission-webhook은 크게 두 가지 컴포넌트로 구성됩니다.

  1. LXCFS DaemonSet

    • 각 Linux 노드에서 LXCFS를 실행합니다.
    • host의 /var/lib/lxc/lxcfs 아래에 가상화된 파일 view를 준비합니다.
  2. Mutating Admission Webhook

    • Pod CREATE 요청을 받습니다.
    • 조건에 맞는 Pod spec에 LXCFS volume과 volumeMount를 JSON Patch로 추가합니다.

흐름은 대략 이렇습니다.

Pod 생성 요청 └─ kube-apiserver └─ MutatingWebhookConfiguration └─ lxcfs-admission-webhook /mutate └─ Pod spec에 LXCFS volumeMount 추가

실제로 주입되는 대표 mount는 다음과 같습니다.

/proc/cpuinfo /proc/meminfo /proc/stat /proc/uptime /proc/loadavg /proc/diskstats /proc/swaps /sys/devices/system/cpu/online /var/lib/lxc/

즉 애플리케이션 Pod 입장에서는 원래 보던 /proc/cpuinfo, /proc/meminfo 경로를 그대로 읽는데, 그 뒤의 내용은 LXCFS가 제공하는 container-aware view가 됩니다.


적용 범위는 namespace 라벨로 제어합니다

이런 종류의 admission webhook은 무작정 전체 클러스터에 적용하면 부담스럽습니다. 그래서 이 프로젝트는 기본적으로 namespace opt-in 방식으로 동작합니다.

적용하고 싶은 namespace에 라벨을 붙입니다.

kubectl label namespace your-namespace lxcfs-admission-webhook=enabled

그러면 그 namespace에 새로 생성되는 Pod부터 mutation 대상이 됩니다. 이미 떠 있는 Pod는 admission 단계를 지나갔기 때문에 자동으로 바뀌지 않습니다. 적용하려면 Deployment rollout 같은 방식으로 다시 생성해야 합니다.

특정 Pod만 제외하고 싶을 때는 annotation을 넣으면 됩니다.

metadata: annotations: mutating.lxcfs-admission-webhook.io/enable: "false"

그리고 처리 결과는 Pod annotation으로 남습니다.

mutating.lxcfs-admission-webhook.io/status: mutated mutating.lxcfs-admission-webhook.io/status: skip mutating.lxcfs-admission-webhook.io/status: conflict

이 부분이 운영할 때 꽤 중요합니다. 나중에 “이 Pod가 정말 webhook을 탔나?”를 확인할 수 있어야 하거든요.


배포 방식은 Helm, Argo CD, install.sh 세 가지입니다

처음에는 script 기반 배포만 있어도 충분하다고 생각했는데, 실제로 운영하려면 GitOps나 chart 형태가 훨씬 관리하기 쉽습니다.

그래서 현재는 Helm chart와 Argo CD 예제를 같이 제공합니다.

Helm으로는 이렇게 설치할 수 있습니다.

helm install lxcfs-admission-webhook \ oci://ghcr.io/idoyo7/charts/lxcfs-admission-webhook \ --version 0.2.1 \ --namespace lxcfs \ --create-namespace

Argo CD를 쓰는 환경에서는 OCI chart를 직접 바라보는 Application 예제와, Git repository의 chart path를 바라보는 Application 예제를 둘 다 둘 수 있습니다.

가볍게 테스트할 때는 repository의 deploy/install.sh를 사용할 수도 있습니다.

git clone https://github.com/idoyo7/lxcfs-admission-webhook.git cd lxcfs-admission-webhook/deploy ./install.sh

다만 어떤 방식이든 cert-manager는 먼저 설치되어 있어야 합니다. webhook serving certificate와 CA bundle 주입을 cert-manager에 맡기기 때문입니다.


운영 전에 봐야 할 부분

구조 자체는 단순하지만, 클러스터에 넣을 때는 몇 가지를 확인해야 합니다.

1. privileged DaemonSet과 hostPath 허용

LXCFS DaemonSet은 노드에서 FUSE와 hostPath를 다룹니다. 그래서 Pod Security Admission, Kyverno, Gatekeeper 같은 정책에서 막히지 않는지 봐야 합니다.

특히 managed Kubernetes 환경에서는 보안 정책이 생각보다 강하게 걸려 있을 수 있습니다.

2. namespace 단위로 천천히 적용

처음부터 전체 namespace에 적용하기보다, JVM이나 Node.js처럼 효과가 명확한 workload가 있는 namespace부터 붙이는 편이 낫습니다.

kubectl label namespace app-prod lxcfs-admission-webhook=enabled

그리고 rollout 이후 Pod annotation과 컨테이너 안의 nproc, /proc/meminfo 값을 같이 확인합니다.

kubectl get pod <pod> \ -o jsonpath='{.metadata.annotations.mutating\.lxcfs-admission-webhook\.io/status}'

3. objectSelector로 더 좁힐 수 있음

Helm chart에는 mutatingWebhook.objectSelector 값도 열어두었습니다. namespace 전체가 아니라 특정 label을 가진 Pod만 mutation하고 싶다면 object selector를 추가할 수 있습니다.

예를 들어 운영 정책상 명시적으로 opt-in한 Pod만 바꾸고 싶다면 이런 방향도 가능합니다.

mutatingWebhook: objectSelector: matchLabels: lxcfs-admission-webhook: enabled

namespace selector는 큰 범위를 정하고, object selector는 그 안에서 더 좁히는 용도로 보면 됩니다.

4. initContainer까지 필요한지는 별도 판단

현재 webhook은 일반 container의 volumeMounts를 패치하는 구조입니다. 대부분의 애플리케이션에는 이 정도면 충분하지만, initContainer에서도 같은 /proc view가 필요하다면 추가 구현이 필요합니다.


실제로 확인하는 방법

적용 후에는 limit이 있는 Pod를 하나 띄워서 직접 확인하는 게 제일 빠릅니다.

kubectl label namespace default lxcfs-admission-webhook=enabled --overwrite kubectl run lxcfs-check --rm -it --restart=Never \ --image=alpine:3.21 \ --limits=cpu=1,memory=1Gi \ -- sh -c 'echo "nproc: $(nproc)"; head -3 /proc/meminfo; grep -c ^processor /proc/cpuinfo'

CPU 1개, 메모리 1Gi로 제한한 Pod라면 nproc1에 가깝게 나오고, MemTotal도 약 1048576 kB에 가까운 값으로 보이는지 확인하면 됩니다.

물론 LXCFS가 모든 kernel counter를 완벽히 가상화하는 것은 아닙니다. 그래서 CPU 사용률처럼 일부 지표는 여전히 host 기반 값이 섞일 수 있습니다. 이 프로젝트의 핵심은 runtime sizing에 자주 쓰이는 CPU/메모리 인식값을 더 현실적으로 맞추는 데 있습니다.


왜 fork를 정리했나

원래 프로젝트도 admission webhook으로 LXCFS mount를 자동화한다는 방향은 좋았습니다. 다만 시간이 지나면서 운영 관점에서 아쉬운 부분이 생겼습니다.

  • 오래된 Go / Kubernetes dependency
  • 오래된 LXCFS image
  • 직접 인증서를 만들고 caBundle을 넣는 설치 방식
  • 수동 이미지 빌드와 하드코딩된 image reference
  • Helm / GitOps 배포 흐름 부재

이번 정리에서는 이런 부분을 운영 가능한 형태로 맞추는 데 초점을 뒀습니다.

  • LXCFS 6.x 기반 이미지
  • Go 1.24 / Kubernetes 최신 dependency 기반 빌드
  • cert-manager 기반 serving certificate 관리
  • GHCR 이미지와 OCI Helm chart 배포
  • Helm values / Argo CD 예제 / install script 제공
  • conflict 상황에서 무리하게 덮어쓰지 않는 안전장치

커밋 몇 개를 추가했다는 식의 설명보다는, 실제로 운영자가 배포하고 관리할 때 필요한 면을 채우는 쪽에 가깝습니다.


마무리

LXCFS 자체는 새로운 기술은 아니지만, Kubernetes에서 모든 Pod에 일관되게 붙이는 일은 생각보다 번거롭습니다.

Admission webhook을 쓰면 이 반복 작업을 cluster policy처럼 만들 수 있습니다. namespace 라벨로 적용 범위를 정하고, 필요하면 Pod annotation이나 object selector로 더 좁히면 됩니다.

결국 이 프로젝트의 목적은 단순합니다.

컨테이너 안의 런타임이 host가 아니라 Pod limit에 가까운 값을 보고, 그 결과 heap, worker, thread pool 같은 기본 sizing이 조금 더 예측 가능해지도록 만드는 것입니다.