Lima の containerd へ接続してみる

最近、macOS を使い始めて、課題となったのが Linux のコンテナ環境をどうするかという事でした。

あまり余計な事をせずに [containerd](https://github.com/containerd/containerd) が動作するようなものを探したところ Lima を見つけました。

* [Lima](https://github.com/lima-vm/lima)

Lima は Linux 仮想マシンで containerd がビルトインされています。
containerd を操作するための CLI ツール [nerdctl](https://github.com/containerd/nerdctl) （Docker との互換性あり）も導入してくれるので、これだけでコンテナの実行環境が整います。

そこで今回は、Lima の環境を構築するついでに、仮想マシン内の containerd へ Go 言語のコードで接続してみました。

# はじめに

Lima をインストールし、仮想マシンを実行しておきます。

##### インストール例
```
% brew install lima
```

次のように引数無しで ```start``` を実行すると、```default``` という名の仮想マシンのインスタンスが（デフォルト設定で）実行されます。

この場合、[RootlessKit](https://github.com/rootless-containers/rootlesskit) を用いた rootless モードで containerd が実行されるようです。

##### lima 起動例
```
% limactl start
```

lima コマンド[^0]で仮想マシンへ入る事ができ、nerdctl コマンドでコンテナを実行できます。

[^0]: ```lima <COMMAND>``` は ```limactl shell $LIMA_INSTANCE <COMMAND>``` の短縮版のようです

例えば、nginx のコンテナを実行する場合は次のようになります。

##### nginx コンテナ実行例1
```
[macOS] % lima
[lima]  $ nerdctl run -d --name nginx1 -p 8080:80 nginx 
```

もしくは

##### nginx コンテナ実行例2
```
[macOS] % lima nerdctl run -d --name nginx1 -p 8080:80 nginx 
```

このように nerdctl は docker コマンドと同じように使えます。

# containerd へ接続

containerd は gRPC の API [^1]で操作できるようになっているので、これを使ってコンテナの情報を取得してみます。

[^1]: nerdctl もこの API を使って containerd を操作しています

基本的に、```containerd.sock``` というソケットファイルで接続する事になるので、このファイルが何処に配置されているかが重要です。

rootless モードで containerd を起動した場合は、```/run/containerd/containerd.sock``` [^2]では無く、以下の場所にありました。

* ```/proc/<PID of containerd>/root/run/containerd/containerd.sock```

[^2]: 一般的な配置場所らしく、rootful モードではここに配置されるようです。Docker の場合は /var/run/docker/containerd/containerd.sock だったり、以前 snap でインストールした Docker は /var/snap/docker/current/run/docker/containerd/containerd.sock でした

この PID の値は ```$XDG_RUNTIME_DIR/containerd-rootless/child_pid``` ファイルに記載されています。[^3]

[^3]: XDG_RUNTIME_DIR 環境変数の値は /run/user/501 でした

child_pid ファイルから PID を取得し containerd.sock へのパスを組み立て、containerd へ接続する処理は次のようになりました。（エラー処理は適当なのでご注意を）

##### sample.go
```go
package main

import (
	"context"
	"fmt"
	"path/filepath"
	"log"
	"os"
	"strings"

	"github.com/containerd/containerd"
	"github.com/containerd/containerd/namespaces"
)

func main() {
	pid := containerdPid()
	address := filepath.Join("/proc", pid, "root/run/containerd/containerd.sock")

    // containerd へ接続
	client, err := containerd.New(address)

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

	defer client.Close()

	ctx := context.Background()
    // ネームスペースの取得
	ns, err := client.NamespaceService().List(ctx)

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

	fmt.Printf("namespaces: %v\n", ns)

	ctx = namespaces.WithNamespace(ctx, ns[0])
    // コンテナの取得
	cs, err := client.Containers(ctx)

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

	for _, c := range cs {
		img, _ := c.Image(ctx)
		fmt.Printf("container: id=%s, image=%s\n", c.ID(), img.Name())
	}
}
// PID の取得
func containerdPid() string {
	xrDir := os.Getenv("XDG_RUNTIME_DIR")

	file := filepath.Join(xrDir, "containerd-rootless/child_pid")

	pid, _ := os.ReadFile(file)

	return strings.TrimSpace(string(pid))
}
```

## 動作確認

今回は macOS 上でビルドしますが、Linux の仮想マシン内で実行する事になるので[^4]、```GOOS``` 環境変数に ```linux``` を設定して、Linux 向けにビルドします。

[^4]: rootless モードの場合、containerd.sock の場所が固定されていないので macOS 側から接続させるのは厳しそうな気がします

##### ビルド例
```
[macOS] % GOOS=linux go build -o sample_linux sample.go
```

実行すると次のようになりました。

##### 実行例1
```
[macOS] % lima ./sample_linux
namespaces: [default]
container: id=75b4181874ffde1aaa956ea3762a62cffb478086b3288d3f0ae171f1fe228519, image=docker.io/library/nginx:latest
```

##### 実行例2
```
[macOS] % lima
[lima]  $ ./sample_linux
namespaces: [default]
container: id=75b4181874ffde1aaa956ea3762a62cffb478086b3288d3f0ae171f1fe228519, image=docker.io/library/nginx:latest
```