AizuDemy

Tutorial Build Container Linux dari Nol Pakai Namespaces & Cgroups v2 di Go

Tutorial Build Container Linux dari Nol Pakai Namespaces & Cgroups v2 di Go
IKLAN
IDCloudHost
๐ŸŽง
Dengarkan Artikel Ini
Suara AI Otomatis โ€ข 7 mnt baca baca
โšก TL;DR

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.
๐Ÿ“‹ Daftar Isi Materi Tutup โ–ด

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-kece tanpa 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-rootfs

Selanjutnya, 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/sh

Saat 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 memanggil PivotRoot.
  • Error permission denied saat menulis Cgroup: Penulisan file di bawah Cgroup v2 membutuhkan akses superuser. Pastikan kamu menjalankan program menggunakan sudo dan distro Linux kamu sudah mengaktifkan Cgroups v2 secara default (Ubuntu 21.10+ dan Debian 11+ sudah aktif bawaan).
  • File /proc gagal di-mount: Pastikan kamu melepaskan (unmount) mount point /proc lama 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.

FiturMini Container KitaOCI Runtime (runc)
IsolasiPID, UTS, MountLengkap (PID, UTS, Mount, Network, User, IPC)
Resource ControlBasic Cgroups v2 (CPU & RAM)Full Cgroups v1 & v2 Spec + Systemd Driver
KeamananRoot Linux StandarAppArmor, 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!

A
Aizu Dev

Tim penulis AizuDemy yang menyajikan tutorial teknologi, cloud, dan pengembangan perangkat lunak dalam Bahasa Indonesia.

๐Ÿ“– Artikel Terkait