Tutorial Build Container Linux dari Nol Pakai Namespaces & Cgroups v2 di Go
Poin Kunci Artikel Ini:
- Bayangkan kamu sedang kejar deadline jam 2 malam.
- Jari-jarimu mengetik perintah di terminal, dan dalam hitungan detik sebuah shell Linux baru siap dipakai.
- Padahal, container lahir dari fitur bawaan kernel Linux yang sudah ada belasan tahun.
Bayangkan kamu sedang kejar deadline jam 2 malam. Jari-jarimu mengetik perintah docker run -it ubuntu bash di terminal, dan dalam hitungan detik sebuah shell Linux baru siap dipakai. Pernahkah kamu berhenti sejenak dan bertanya-tanya: apa sih yang sebenarnya terjadi di balik layar?
Apakah Docker mendadak membuat Virtual Machine baru yang berat di dalam laptopmu? Jawabannya: sama sekali tidak. Container sebenarnya hanyalah proses Linux biasa yang diberi kacamata kuda agar matanya tidak bisa melihat proses lain, serta dibatasi jatah makan CPU dan RAM-nya oleh kernel.
Banyak developer menganggap container sebagai kotak ajaib. Padahal, container lahir dari fitur bawaan kernel Linux yang sudah ada belasan tahun. Di artikel ini, kita bakal membongkar kotak ajaib tersebut dan membangun container runtime sederhana dari nol memakai bahasa pemrograman Go (Golang).
1. Konsep Dasar Isolasi Linux: Namespaces dan Cgroups v2
Untuk menyulap proses biasa menjadi container, kernel Linux menyediakan dua pilar utama: Namespaces untuk mengisolasi penglihatan, dan Control Groups (Cgroups) untuk mengendalikan konsumsi sumber daya.
Cara membayangkannya sangat sederhana:
- Namespaces menentukan apa saja yang bisa dilihat oleh proses.
- Cgroups menentukan berapa banyak resource yang boleh dipakai oleh proses.
Linux Namespaces: Kacamata Kuda untuk Proses
Ketika sebuah proses dijalankan di dalam namespace terisolasi, proses tersebut akan kecele. Dia merasa menjadi satu-satunya proses yang sedang berjalan di dalam seluruh sistem operasi.
- CLONE_NEWPID: Mengisolasi Process ID. Di dalam container, proses utama kita akan memiliki PID 1 (seolah-olah dialah bos utamanya), padahal di sistem host laptop asli proses ini mungkin punya PID 45210.
- CLONE_NEWNS: Mengisolasi mount points. Fitur ini membuat container memiliki struktur folder filesystem tersendiri tanpa mengacak-acak folder di komputer host.
- CLONE_NEWUTS: Mengisolasi hostname. Container bisa memiliki nama sistem sendiri seperti
container-kecetanpa mengganggu nama komputer host.
Cgroups v2: Satpam Pembatas Resource
Jika Namespace membatasi jarak pandang, Cgroups bertugas mengunci penggunaan resource. Pada versi Linux modern, Cgroups v2 menggunakan struktur direktori tunggal yang berada di folder /sys/fs/cgroup.
Lewat Cgroups v2, kita bisa bilang ke kernel: "Linux, tolong jatah proses ini maksimal RAM 50MB dan CPU 20% saja!". Jika proses di dalam container nekat menggunakan RAM melebihi batas, kernel Linux akan langsung menghentikan proses tersebut secara paksa lewat mekanisme Out Of Memory (OOM) Killer.
2. Praktek Koding: Isolasi Filesystem Rootfs & Process ID di Go
Yuk, langsung buka editor kodemu! Sebelum nulis kode Go, kita butuh isi sistem operasi mini yang disebut root filesystem (rootfs). Kita manfaatkan distro Alpine Linux yang super ringan. Eksekusi perintah berikut di terminal untuk menyiapkannya di direktori /tmp/alpine-rootfs:
mkdir -p /tmp/alpine-rootfs
wget https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/x86_64/alpine-minirootfs-3.19.1-x86_64.tar.gz
tar -xvf alpine-minirootfs-3.19.1-x86_64.tar.gz -C /tmp/alpine-rootfsSelanjutnya, buat file Go bernama main.go. Di sini kita menerapkan pola self-execution (re-exec). Aplikasi Go akan memanggil dirinya sendiri dengan argumen khusus (child) untuk mengeksekusi perintah di dalam namespace terisolasi.
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"syscall"
)
func main() {
if len(os.Args) < 2 {
panic("Gunakan argumen: run <command>")
}
switch os.Args[1] {
case "run":
parent()
case "child":
child()
default:
panic("Perintah tidak dikenal")
}
}
func parent() {
cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
if err := cmd.Run(); err != nil {
fmt.Printf("Error running parent: %v
", err)
os.Exit(1)
}
}
func child() {
fmt.Printf("Running child process with PID %d
", os.Getpid())
syscall.Sethostname([]byte("my-custom-container"))
// Setup rootfs isolasi via pivot_root
rootfs := "/tmp/alpine-rootfs"
must(syscall.Mount(rootfs, rootfs, "", syscall.MS_BIND|syscall.MS_REC, ""))
must(os.MkdirAll(filepath.Join(rootfs, ".oldroot"), 0700))
must(syscall.PivotRoot(rootfs, filepath.Join(rootfs, ".oldroot")))
must(os.Chdir("/"))
must(syscall.Unmount("/.oldroot", syscall.MNT_DETACH))
must(os.Remove("/.oldroot"))
// Mount /proc khusus container
must(syscall.Mount("proc", "/proc", "proc", 0, ""))
cmd := exec.Command(os.Args[2], os.Args[3:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Error running child: %v
", err)
os.Exit(1)
}
must(syscall.Unmount("/proc", 0))
}
func must(err error) {
if err != nil {
panic(err)
}
}Jalankan kode di atas menggunakan sudo karena pembuatan namespace dan mounting adalah hak istimewa kernel:
sudo go run main.go run /bin/shSaat shell baru terbuka, coba jalankan perintah ps aux dan hostname. Kamu akan melihat bahwa hanya ada dua proses (/bin/sh dan ps) dengan PID 1, serta hostname yang berubah menjadi my-custom-container. Filesystem dan PID berhasil terisolasi sempurna!
3. Konfigurasi Limit Resource CPU & RAM Via Cgroups v2
Isolasi pandangan sudah beres, sekarang waktunya memasang pembatas resource agar container tidak melahap habis resource laptop host. Di Cgroups v2, langkahnya sangat praktis: cukup buat folder baru di bawah /sys/fs/cgroup/ lalu isi file teks konfigurasi di dalamnya.
Menambahkan Cgroup Limit pada Kode Parent Go
Tambahkan fungsi setupCgroup berikut untuk membuat aturan pembatasan memori dan CPU:
func setupCgroup(pid int) {
cgroups := "/sys/fs/cgroup"
containerCgroup := filepath.Join(cgroups, "my-container")
// 1. Buat folder cgroup khusus
os.MkdirAll(containerCgroup, 0755)
// 2. Batasi RAM maksimal 50MB (50 * 1024 * 1024 bytes)
must(os.WriteFile(filepath.Join(containerCgroup, "memory.max"), []byte("52428800"), 0644))
// 3. Batasi CPU ke 20% (quota 20000 microsecond per period 100000)
must(os.WriteFile(filepath.Join(containerCgroup, "cpu.max"), []byte("20000 100000"), 0644))
// 4. Daftarkan PID proses child ke dalam cgroup
must(os.WriteFile(filepath.Join(containerCgroup, "cgroup.procs"), []byte(fmt.Sprintf("%d", pid)), 0644))
}Panggil setupCgroup(cmd.Process.Pid) di fungsi parent() tepat setelah cmd.Start() dipanggil (ubah cmd.Run() menjadi cmd.Start(), jalankan setupCgroup, lalu akhiri dengan cmd.Wait()).
Solusi Kendala Teknis di Lapangan
Saat membuat container runtime sendiri, ada beberapa masalah klasik yang sering muncul beserta jalan keluarnya:
- Error
pivot_root: invalid argument: Masalah ini timbul karena folder rootfs belum dianggap sebagai mount point independen. Solusinya: bind mount folder rootfs ke dirinya sendiri terlebih dahulu sebelum memanggilPivotRoot. - Error
permission deniedsaat menulis Cgroup: Penulisan file di bawah Cgroup v2 membutuhkan akses superuser. Pastikan kamu menjalankan program menggunakansudodan distro Linux kamu sudah mengaktifkan Cgroups v2 secara default (Ubuntu 21.10+ dan Debian 11+ sudah aktif bawaan). - File
/procgagal di-mount: Pastikan kamu melepaskan (unmount) mount point/proclama sebelum menimpanya dengan instance baru di dalam container.
4. Kesimpulan & Perbandingan dengan Standar OCI Runtime
Kita baru saja berhasil membangun sebuah mini container runtime fungsional menggunakan bahasa Go. Kode sederhana ini menerapkan prinsip mendasar yang persis sama dengan mesin raksasa seperti Docker dan Podman.
Bedanya, Docker tidak mengurusi eksekusi low-level ini secara langsung. Docker bertindak sebagai pengelola tingkat tinggi yang mematuhi spesifikasi OCI (Open Container Initiative) dan menyerahkan tugas eksekusi low-level ke OCI-compliant runtime seperti runc atau crun.
| Fitur | Mini Container Kita | OCI Runtime (runc) |
|---|---|---|
| Isolasi | PID, UTS, Mount | Lengkap (PID, UTS, Mount, Network, User, IPC) |
| Resource Control | Basic Cgroups v2 (CPU & RAM) | Full Cgroups v1 & v2 Spec + Systemd Driver |
| Keamanan | Root Linux Standar | AppArmor, SELinux, Seccomp profiles, Rootless Mode |
Eksperimen ini membuktikan bahwa container bukanlah virtualisasi perangkat keras yang berat, melainkan gabungan cerdik dari fitur-fitur isolasi kernel Linux. Tantangan berikutnya untukmu: coba kembangkan kode di atas dengan menambahkan isolasi Network Namespace menggunakan pasangan veth (virtual ethernet) agar container buatanmu bisa terhubung ke internet!



