最近、macOS を使い始めて、課題となったのが Linux のコンテナ環境をどうするかという事でした。
あまり余計な事をせずに containerd が動作するようなものを探したところ Lima を見つけました。
Lima は Linux 仮想マシンで containerd がビルトインされています。
containerd を操作するための CLI ツール nerdctl (Docker との互換性あり)も導入してくれるので、これだけでコンテナの実行環境が整います。
そこで今回は、Lima の環境を構築するついでに、仮想マシン内の containerd へ Go 言語のコードで接続してみました。
はじめに
Lima をインストールし、仮想マシンを実行しておきます。
インストール例
% brew install lima
次のように引数無しで start
を実行すると、default
という名の仮想マシンのインスタンスが(デフォルト設定で)実行されます。
この場合、RootlessKit を用いた rootless モードで containerd が実行されるようです。
lima 起動例
% limactl start
lima コマンド1で仮想マシンへ入る事ができ、nerdctl コマンドでコンテナを実行できます。
例えば、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 2で操作できるようになっているので、これを使ってコンテナの情報を取得してみます。
基本的に、containerd.sock
というソケットファイルで接続する事になるので、このファイルが何処に配置されているかが重要です。
rootless モードで containerd を起動した場合は、/run/containerd/containerd.sock
3では無く、以下の場所にありました。
/proc/<PID of containerd>/root/run/containerd/containerd.sock
この PID の値は $XDG_RUNTIME_DIR/containerd-rootless/child_pid
ファイルに記載されています。4
child_pid ファイルから PID を取得し containerd.sock へのパスを組み立て、containerd へ接続する処理は次のようになりました。(エラー処理は適当なのでご注意を)
sample.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 の仮想マシン内で実行する事になるので5、GOOS
環境変数に linux
を設定して、Linux 向けにビルドします。
ビルド例
[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
-
lima <COMMAND>
はlimactl shell $LIMA_INSTANCE <COMMAND>
の短縮版のようです ↩ -
nerdctl もこの API を使って containerd を操作しています ↩
-
一般的な配置場所らしく、rootful モードではここに配置されるようです。Docker の場合は /var/run/docker/containerd/containerd.sock だったり、以前 snap でインストールした Docker は /var/snap/docker/current/run/docker/containerd/containerd.sock でした ↩
-
XDG_RUNTIME_DIR 環境変数の値は /run/user/501 でした ↩
-
rootless モードの場合、containerd.sock の場所が固定されていないので macOS 側から接続させるのは厳しそうな気がします ↩