· kaynak dev.to (home feed)
Otuz satır, Docker'sız: namespace'ler, pivot_root ve bir cgroup ile inşa edilmiş bir container
dev.to'daki bir anlatım, Docker'a hiç ihtiyaç duymadan unshare, namespace'ler, pivot_root ve bir cgroup bellek limiti kullanarak yaklaşık otuz satırla çalışan bir container inşa ediyor.

Özel görünümlere sahip bir süreç
dev.to'da yazan bir geliştirici, Docker kullanmadan ve host üzerinde root yetkisi olmadan, yaklaşık otuz satırlık Python ve shell koduyla çalışan bir container inşa etti. Yazarın açıkladığına göre bu alıştırmanın amacı, container denen bir kernel nesnesinin aslında olmadığını göstermek: container, aksi halde makinenin geri kalanıyla paylaşacağı kaynakların özel kopyalarını verilmiş sıradan bir süreçtir. Biten süreç, kendisini container adlı bir host üzerinde PID 1 sanıyordu, makinenin diğer süreçlerini görmüyordu, boş bir ağ yığınına ve kendi root dosya sistemine sahipti ve 20 MB'lık bellek limitini aştığında kernel tarafından öldürüldü.
Her namespace için bir unshare çağrısı
Yalıtım kısmı tamamen Linux namespace'lerinden geliyor; her biri tek bir global kaynağın özel bir kopyası: PID, mount, ağ, hostname için UTS, IPC ve user. Yazı, bütün işi tek bir unshare çağrısına indiriyor:
unshare --user --map-root-user --mount --uts --ipc --pid --fork --net
python3 enter.py rootfs
Her flag tam olarak bir tür ayrım sağlıyor: --uts özel bir hostname, --pid --fork içinde fork edilen çocuğun PID 1 olduğu özel bir süreç tablosu, --net tek arayüzü kapalı bir loopback olan bir ağ yığını ve --mount özel bir mount kümesi. Görünürdeki en kritik olanı ise --user --map-root-user; bu, çağıran kullanıcıyı bir user namespace içinde root'a eşleyerek sürecin host üzerinde yetkisiz kalırken dosya sistemleri mount etmesini ve pivot_root çağırmasını sağlıyor. İçeride, hostta seksen yedi süreç varken iki süreç görünür, ayrıca kapalı bir arayüz ve seçilmiş bir hostname vardır.
chroot değil pivot_root
Gerçekten uğraştırıcı adım root dosya sistemini değiştirmek ve yazıya göre bir oyuncağın container'a dönüştüğü yer tam da burası. chroot işe yarar ama eski root'u altta mount edilmiş halde bırakır; gizlidir ama bilinen kaçış hileleriyle erişilebilir, bu yüzden gerçek runtime'lar root mount'unu takas eden ve eski olanın tamamen unmount edilmesini sağlayan pivot_root kullanır. pivot_root'un yazarın makinesinde dostane bir sarmalayıcısı olmadığından enter.py syscall'i libc üzerinden çağırıyor. Üç gereksinimin her biri bir başarısız çalışmaya mal oldu: yeni root bir mount noktası olmalı, bu yüzden rootfs kendi üzerine bind-mount ediliyor; parent mount özel yapılmalı yoksa çağrı EINVAL ile başarısız olur; ve pivot'tan sonra sarkıda kalan eski root umount2 ile ayrılmalı. Bundan sonra host dosya sistemi gizli değil, yok olmuştur: ls /oldroot böyle bir dizin olmadığını söyler.
ps'yi yalancı yapan hata
Yazıdaki en öğretici hata, ps'ten sıfır süreç üretti; mükemmel yalıtım gibi görünüyordu ama aslında bir ölçüm hatasıydı. İlk sürüm, chroot'tan önce /proc'u unshare --mount-proc ile mount etmişti, bu yüzden süreç rootfs içindeki boş bir /proc dizinini okuyordu. ps boş bir süreç tablosu görmüyordu; boş bir klasör görüyordu. /proc'u root takasından sonra, yeni root'un içinden mount etmek, ps'nin tam olarak iki süreç rapor etmesini sağladı — gerçek cevap ve PID namespace'inin çalıştığının kanıtı.
Namespace'ler yalıtır, cgroup'lar sınırlar
İnsanların container derken kastettiği şeyin diğer yarısı olan kaynak tavanı, ayrı bir kernel özelliğidir. Yazarın çerçevesinde namespace'ler bir sürecin ne görebileceğini, control group'lar ise ne tüketebileceğini belirler. Bir cgroup v2 bellek limiti, memory.max dosyasına yazılan bir bayt sayısıdır. Limitin işlediğini görmek için yazıda 20 MB'lık tavan altında her seferinde bir megabayt kaplayan bir çocuk süreç çalıştırıldı: 15 MB ayırdıktan sonra 137 çıkış koduyla öldü — 128 artı 9, kernel'in OOM killer'ından bir SIGKILL — bu sırada container'ın PID 1 shell'i sağlam kaldı.
İki ek tökezleme demoyu şekillendirdi. İlk bellek oburu tmpfs'e yazıyordu; tmpfs'in sayfaları yazan süreçten daha uzun yaşar, bu yüzden OOM killer PID 1'i öldürüp container'ın tamamını çökertmeye devam etti; ölümünde serbest kalan anonim bellek, bir limiti göstermenin dürüst yoludur. Bir systemd makinesinde yazarın shell'i ayrıca kullanıcıya delege edilen ağacın dışındaki bir /init.scope cgroup'unda oturuyordu, bu yüzden manuel attach girişimleri EIO döndürdü; çözüm systemd-run --user --scope oldu, ki bu süreci limitlerin ayarlanabileceği delege edilmiş ağacın içinde başlatır.
Neden önemli
Birçok geliştirici kernel'in ne yaptığına dair bir zihinsel modele sahip olmadan günlük olarak container başlatıyor ve bu boşluk pratikte kendini gösteriyor: bir container içinde garip ps çıktısını hata ayıklamak, bir chroot'un bir güvenlik sınırı olup olmadığını sormak (değildir), bir pod'un neden OOM-kill edildiğini çözmek ya da rootless container'ların neden mümkün olduğunu görmek. Bu otuz satırlık gösterim kavramı temizce bölüyor — görünürlük için namespace'ler, dosya sistemi için pivot_root, limitler için cgroup'lar — ve her parçanın belirli bir aracın bir artefaktı değil, kernel'e yapılan bir istek olduğunu gösteriyor. Üretimde bir şey bozulduğunda, bu ayrıştırma tahmin etmek ile nereye bakacağını bilmek arasındaki farktır.
- #linux
- #containers
- #namespaces
- #cgroups
- #kernel