yikegaya’s blog

yikegayaのブログ

「Goならわかるシステムプログラミング」の17章を読んだ(Go言語とコンテナ)

コンテナ

仮想化は、使いたいサービスだけでなくOSも含めてまるごと動かすことが前提の仕組みです。そのため、たとえばゲストOSとホストOSが同じLinuxであればカーネルやシステムのデーモンを重複してロードすることになり、無駄にメモリを消費してしまいます。そこで、「OSのカーネルはホストのものをそのまま使うが、アプリケーションから見て自由に使えるOS環境が手に入る」の実現に特化したのがコンテナと呼ばれる技術です。「アプリケーションが好き勝手にしても全体が壊れないような、他のアプリケーションに干渉しない・されない箱を作る」という機能だけ見ると、仮想化もコンテナも同じであるため、コンテナのことを「OSレベル仮想化」と呼ぶこともあります。 仮想化ではストレージをまるごとファイル化したような仮想イメージを使ってアプリケーションを導入しますが、コンテナでもイメージと呼ばれるものを使います。

コンテナを実現するためのOSカーネルの機能

一口にコンテナ技術といっても、内部では複数の機能を組み合わせて実現されています。たとえばLinuxでは、コンテナを実現するためのOSカーネルの機能として、コントロールグループ(cgroups)および名前空間(Namespaces)があります。これらの機能を組み合わせることで、さまざまなOSのリソースを、仮想メモリを用意するように気軽に分割できます。 コントロールグループ(cgroups)は、次の項目の使用量とアクセスを制限できる ようにするカーネルの機能です。

  • CPU
  • メモリ
  • ブロックデバイスmmap可能なストレージとほぼ同義)
  • ネットワーク
  • /dev以下のデバイスファイル

名前空間

また、カーネルでは、次のような項目について名前空間(Namespaces)を分離できるようになっています。

-プロセスID - ネットワーク(インタフェース、ルーティングテーブル、ソケットなど) - マウント(ファイルシステム) - UTS(ホスト名) - IPC(セマフォ、MQ、共有メモリなどのプロセス間通信) - ユーザー(UID、GID

libcontainerでコンテナを自作する

Linux環境でのみ動作する

コンテナのブートに必要な下準備

docker pull alpine

docker run --name alpine alpine

docker export alpine > alpine.tar

docker rm alpine

mkdir rootfs

tar -C rootfs -xvf alpine.tar

go get github.com/opencontainers/runc/libcontainer ⏎

go get golang.org/x/sys/unix 
package main

import (
    "github.com/opencontainers/runc/libcontainer"
    "github.com/opencontainers/runc/libcontainer/configs"
    _ "github.com/opencontainers/runc/libcontainer/nsenter"
    "log"
    "os"
    "runtime"
    "path/filepath"
    "golang.org/x/sys/unix"
)

func init() {
    if len(os.Args) > 1 && os.Args[1] == "init" {
        runtime.GOMAXPROCS(1)
        runtime.LockOSThread()
        factory, _ := libcontainer.New("")
        if err != factory.StartInitialization(); err != nil {
            log.Fatal(err)
        }
        panic("--this line should have never been executed, congratulations--")
    }
}

func main() {
    abs, _ := filepath.Abs("./")
    factory, err := libcontainer.New(abs, libcontainer.Cgroupfs, libcontainer.InitArgs(os.Args[0], "init"))

    if err != nil {
        log.Fatal(err)
        return
    }

    capabilities := []string{
        "CAP_CHOWN",
        "CAP_DAC_OVERRIDE",
        "CAP_FSETID",
        "CAP_FOWNER",
        "CAP_MKNOD",
        "CAP_NET_RAW",
        "CAP_SETGID",
        "CAP_SETUID",
        "CAP_SETFCAP",
        "CAP_SETPCAP",
        "CAP_NET_BIND_SERVICE",
        "CAP_SYS_CHROOT",
        "CAP_KILL",
        "CAP_AUDIT_WRITE",
    }
    defaultMountFlags := unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV
    config := &configs.Config{
        Rootfs: abs+"/rootfs",
        Capabilities: &configs.Capabilities{
            Bounding: capabilities,
            Effective: capabilities,
            Inheritable: capabilities,
            Permitted: capabilities,
            Ambient: capabilities,
        },
        Namespaces: configs.Namespaces([]configs.Namespace{
            {Type: configs.NEWNS},
            {Type: configs.NEWUTS},
            {Type: configs.NEWIPC},
            {Type: configs.NEWPID},
            {Type: configs.NEWNET},
        }),
        Cgroups: &configs.Cgroup{
            Name: "test-container",
            Parent: "system",
            Resources: &configs.Resources{
            MemorySwappiness: nil,
            AllowAllDevices: nil,
            AllowedDevices: configs.DefaultAllowedDevices,
        },
    },
    MaskPaths: []string{
        "/proc/kcore", "/sys/firmware",
    },
    ReadonlyPaths: []string{
        "/proc/sys", "/proc/sysrq-trigger", "/proc/irq", "/proc/bus",
    },
    Devices: configs.DefaultAutoCreatedDevices,
    Hostname: "testing",
    Mounts: []*configs.Mount{
        {
            Source: "proc",
            Destination: "/proc",
            Device: "proc",
            Flags: defaultMountFlags,
        },
        {
            Source: "tmpfs",
            Destination: "/dev",
            Device: "tmpfs",
            Flags: unix.MS_NOSUID | unix.MS_STRICTATIME,
            Data: "mode=755",
        },
        {
            Source: "devpts",
            Destination: "/dev/pts",
            Device: "devpts",
            Flags: unix.MS_NOSUID | unix.MS_NOEXEC,
            Data: "newinstance,ptmxmode=0666,mode=0620,gid=5",
        },
        {
            Device: "tmpfs",
            Source: "shm",
            Destination: "/dev/shm",
            Data: "mode=1777,size=65536k",
            Flags: defaultMountFlags,
        },
        {
            Source: "mqueue",
            Destination: "/dev/mqueue",
            Device: "mqueue",
            Flags: defaultMountFlags,
        },
        {
            Source: "sysfs",
            Destination: "/sys",
            Device: "sysfs",
            Flags: defaultMountFlags | unix.MS_RDONLY,
        },
    },
    Networks: []*configs.Network{
        {
            Type: "loopback",
            Address: "127.0.0.1/0",
            Gateway: "localhost",
        },
    },
    Rlimits: []configs.Rlimit{
        {
            Type: unix.RLIMIT_NOFILE,
            Hard: uint64(1025),
            Soft: uint64(1025),
        },
    },
    container, err := factory.Create("container-id", config)
        if err != nil {
        log.Fatal(err)
        return
    }
    process := &libcontainer.Process{
        Args: []string{"/bin/sh"},
        Env: []string{"PATH=/bin"},
        User: "root",
        Stdin: os.Stdin,
        Stdout: os.Stdout,
        Stderr: os.Stderr,
    }
    err = container.Run(process)
    if err != nil {
        container.Destroy()
        log.Fatal(err)
        return
    }
    _, err = process.Wait()
    if err != nil {
        log.Fatal(err)
    }
    container.Destroy()
}