Containers are moving the world. Each passing day more and more organizations are embracing containers as first-class citizens for distribution and deployment of software components. Containers represent the core of the cloud native paradigm. We are witnessing the birth of new container runtimes such as CRI-O - a lightweight alternative to Docker that allows spinning up containers on top of Kubernetes. A while back, I blogged about container internals and the Linux pillars that power some of the leading container engines. I have to admit the code snippets were rather unfunctional and ugly. Interfacing with the underlying Linux subsystem through libc
crate with unsafe
scattered throughout the code looked terrifying. That’s why I decided to wrap up a small project called rabbitc - a minimal container runtime meant for learning purposes (keep in mind that my knowledge on Rust is modest and I’m still in process of educating myself). Rust toolchain is required to build it. Once you have the binary, grab a minimal rootfs (such as Alpine’s rootfs) and spawn a new containerized process.
$ sudo rabbitc --rootfs=<rootfs>
/# ps
PID USER TIME COMMAND
1 root 0:00 sh
5 root 0:00 ps
By default, you’ll be presented with a shell process, but you can run a different process with --cmd
option. For example, let’s build a Go-based HTTP server and run it inside container. Save the following code snippet to main.go
.
package main
import (
"net/http"
"fmt"
"net"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from rabbitc container")
})
var addr string
addrs, err := net.InterfaceAddrs()
if err != nil {
addr = "127.0.0.1"
}
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
addr = ipnet.IP.String()
}
}
}
endpoint := fmt.Sprintf("%s:80", addr)
fmt.Println("running HTTP server on", endpoint)
err = http.ListenAndServe(endpoint, nil)
if err != nil {
panic(err)
}
}
Instruct the Go compiler to produce statically linked binary by disabling CGO
. We also copy the resulting binary to our rootfs path.
$ CGO_ENABLED=0 go build -o server main.go
$ cp server /<rootfs>/bin
Since rabbitc
doesn’t support port forwarding (but it’s easily doable with some iptables
magic), we have to provide container IP address to send the request to HTTP server.
$ sudo rabbitc --rootfs=<rootfs> --cmd=/bin/server
Running HTTP server on 172.19.0.2:80
$ curl 172.19.0.2:80
Hello from rabbitc container
We’re running a full isolated process with its own network stack, file system and process tree in 400~ lines of Rust code. That’s awesome!
In the next blog post I’ll focus on explaining the remaining kernel namespaces, how resources are controlled by cgroup
subsystem and other container-specific nuances. Stay tuned.