はじめに
以前に作成したeBPFプログラムをKubernetesクラスタのDaemonSetとして起動します。
今回は、eBPFプログラムのコンパイルにebpf-go
を利用します。
ebpf-go
に付属するbpf2go
というコード生成ツールを利用すると、必要なeBPFバイトコードをすべて内包した単一のバイナリとして、eBPFプログラムをコンパイルすることができます。
詳細は公式ドキュメントを参照して下さい。
実行環境
実行環境はM3 Macです。colima
でk3sクラスタを起動します。
colima start --cpu 2 --memory 6 --disk 80 --arch x86_64 --kubernetes --runtime containerd
コンテナイメージを格納するためのイメージレジストリをk3sにデプロイします。DaemonSetの検証に利用します。
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: private-registry
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: private-registry
template:
metadata:
labels:
app: private-registry
spec:
containers:
- name: registry
image: registry:2
ports:
- containerPort: 5000
volumeMounts:
- name: registry-storage
mountPath: /var/lib/registry
volumes:
- name: registry-storage
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: private-registry-service
namespace: kube-system
spec:
type: NodePort
selector:
app: private-registry
ports:
- protocol: TCP
port: 5000
targetPort: 5000
nodePort: 30500
EOF
ディレクトリ構成
ユーザー空間プログラムをGo (cilium/ebpf)で再実装します。cmd/agent-checker/main.go
が該当します。
.
├── Containerfile # コンテナイメージ
├── daemonset.yaml # k8sマニフェスト
├── cmd # ユーザー空間プログラムをGo(cilium/ebpf)で再実装
│ └── agent-checker
│ └── main.go
├── ebpf # 以前の記事で作成したeBPFプログラム
│ ├── agent.c
│ └── bpf_common.h
├── go.mod
└── go.sum
コード解説
ebpf/agent.c
以前に作成した内容から変更はありません。所定のディレクトリに配置します。
ebpf/bpf_common.h
こちらも内容に変更はありません。
cmd/agent-checker/main.go
cilium/ebpf/cmd/bpf2go
を使ってコンパイルされたeBPFオブジェクトをロードし、カーネルにアタッチします。
//go:generate
ディレクティブを設定した状態でgo generate
コマンドを実行すると、eBPFプログラムのCコードがコンパイルされ、Goのコードとして埋め込まれます。
package main
import (
"fmt"
"log"
"net"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
// bpf2goのコンパイル指定
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang bpf ../../ebpf/agent.c -- -I../../ebpf/
const (
// eBPFマップの統計情報を表示する間隔(秒)を取得するための環境変数名
intervalEnvVar = "CHECKER_INTERVAL_SECONDS"
// 環境変数が設定されていない場合のデフォルトの間隔(秒)
defaultInterval = 30
)
func main() {
// OSシグナルハンドリングの設定 (Ctrl+Cなどで終了できるようにする)
stopper := make(chan os.Signal, 1)
signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)
// eBPFプログラムのためのメモリロック上限を緩和
// eBPFプログラムとマップをカーネルにロードするために必要なメモリを、プロセスが確保できるようにするため
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
// bpf2goで生成されたeBPFオブジェクトをロード
objs := bpfObjects{}
if err := loadBpfObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
// main関数終了時に、ロードしたeBPFオブジェクトをクリーンアップする
defer objs.Close()
// tcp_sendmsg 関数にkprobeをアタッチ
kpSend, err := link.Kprobe("tcp_sendmsg", objs.TraceTcpSendmsg, nil)
if err != nil {
log.Fatalf("attaching kprobe tcp_sendmsg: %s", err)
}
// main関数終了時にkprobeをデタッチ
defer kpSend.Close()
// tcp_cleanup_rbuf 関数もアタッチ
kpRecv, err := link.Kprobe("tcp_cleanup_rbuf", objs.TraceTcpCleanupRbuf, nil)
if err != nil {
log.Fatalf("attaching kprobe tcp_cleanup_rbuf: %s", err)
}
defer kpRecv.Close()
log.Println("Successfully loaded and attached BPF programs. Watching TCP traffic...")
log.Println("Press Ctrl+C to exit.")
// 環境変数からタイマーの間隔を読み込む
intervalStr := os.Getenv(intervalEnvVar)
interval, err := strconv.Atoi(intervalStr)
if err != nil || interval <= 0 {
log.Printf("Invalid or no interval set via %s. Using default: %d seconds", intervalEnvVar, defaultInterval)
interval = defaultInterval
} else {
log.Printf("Interval set to %d seconds via %s", interval, intervalEnvVar)
}
// 定期的にeBPFマップの内容を表示するためのタイマーを設定
ticker := time.NewTicker(time.Duration(interval) * time.Second)
defer ticker.Stop()
// メインループ:タイマーの通知か、OSの終了シグナルを待つ
for {
select {
case <-ticker.C:
printStats(&objs)
case <-stopper:
log.Println("Received signal, detaching and cleaning up...")
return
}
}
}
// eBPFマップから統計情報を読み出して標準出力する
func printStats(objs *bpfObjects) {
now := time.Now().Format("2006-01-02 15:04:05")
fmt.Printf("\n--- Start printStats (%s) ---", now)
fmt.Printf("\n--- TX Stats ---\n")
printMap(objs.TxStats)
fmt.Printf("--- RX Stats ---\n")
printMap(objs.RxStats)
}
// 指定されたeBPFマップの内容をイテレートして標準出力する
func printMap(statsMap *ebpf.Map) {
var key bpfIpKeyT
var val bpfValT
iter := statsMap.Iterate()
// iter.Next() を使ってマップの全エントリをループ処理
for iter.Next(&key, &val) {
// C言語版のaddr_to_str関数と同様に、[16]byteのIPアドレスを文字列に変換
saddr := net.IP(key.Saddr[:]).String()
daddr := net.IP(key.Daddr[:]).String()
fmt.Printf(" %s -> %s : %d bytes\n", saddr, daddr, val.Bytes)
}
if err := iter.Err(); err != nil {
log.Printf("Error iterating map: %v", err)
}
}
go.mod
Goプロジェクトの設定ファイルです。go mod tidy
コマンドを実行して、go.sum
を生成して下さい。
module github.com/t-matsu200/k8s-agent-checker
go 1.24.4
require github.com/cilium/ebpf v0.19.0
require golang.org/x/sys v0.31.0 // indirect
Containerfile
cilium/ebpf-builder
イメージには、eBPFプログラムの開発に必要なClang, LLVM, libbpf, Goなどのツールチェーン一式が含まれています。
cilium/ebpf-builder
イメージをeBPFプログラムのコンパイル環境として利用します。
FROM ghcr.io/cilium/ebpf-builder:1755266860 AS builder
# clangとllvm-stripコマンドのバージョンを17に指定
RUN update-alternatives --install /usr/bin/clang clang /usr/bin/clang-17 90
RUN update-alternatives --install /usr/bin/llvm-strip llvm-strip /usr/bin/llvm-strip-17 90
# bpftoolのインストール
RUN git clone --recurse-submodules https://github.com/libbpf/bpftool.git /tmp/bpftool \
&& cd /tmp/bpftool/src \
&& make install \
&& rm -r /tmp/bpftool
WORKDIR /app
# Goモジュールのダウンロード
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# BTF情報からvmlinux.hを生成
RUN bpftool btf dump file /sys/kernel/btf/vmlinux format c > ./ebpf/vmlinux.h
# go generate を実行してeBPFプログラムをコンパイル
RUN BPF2GO_CFLAGS="-O2 -g -Wall -Werror -D__TARGET_ARCH_x86 $(CFLAGS)" go generate ./cmd/agent-checker/...
# CGO_ENABLED=0 で静的にリンクされたバイナリをビルドします
# https://ebpf-go.dev/guides/portable-ebpf/
RUN CGO_ENABLED=0 go build -buildvcs=false -o /app/agent-checker ./cmd/agent-checker/...
FROM alpine:3.22
COPY --from=builder /app/agent-checker /usr/local/bin/agent-checker
ENTRYPOINT ["/usr/local/bin/agent-checker"]
daemonset.yaml
コンパイルしたeBPFプログラムを実行するDaemonSetのマニフェストです。設定内容についてはコメントを参照して下さい。
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: agent-checker
namespace: kube-system
labels:
app: agent-checker
spec:
selector:
matchLabels:
app: agent-checker
template:
metadata:
labels:
app: agent-checker
spec:
# コントロールプレーンノードを含む、すべてのノードでPodがスケジュールされるように設定
tolerations:
- operator: "Exists"
# ホストOS上で実行されている全てのプロセスを正しく監視できるようにするため、ホストのPID名前空間を使用します
hostPID: true
containers:
- name: agent-checker
image: localhost:30500/ebpf-agent-checker
imagePullPolicy: Always
env:
- name: CHECKER_INTERVAL_SECONDS
value: "60"
securityContext:
# eBPFプログラムのロードと実行に必要なケーパビリティを設定
# - SYS_ADMIN: BPFやperfイベントへのアクセスなど、多くの管理操作に必要です
# - BPF: eBPFマップの作成やプログラムのロードに直接必要です
# - PERFMON: kprobeなどのトレースポイントにアクセスするために必要です
capabilities:
drop:
- ALL
add:
- SYS_ADMIN
- BPF
- PERFMON
# eBPFプログラムのロードとデバッグに必要なホストのディレクトリをマウントします
volumeMounts:
- name: sys-fs-cgroup
mountPath: /sys/fs/cgroup
readOnly: true
- name: sys-kernel-debug
mountPath: /sys/kernel/debug
readOnly: true
- name: sys-kernel-tracing
mountPath: /sys/kernel/tracing
readOnly: true
- name: sys-fs-bpf
mountPath: /sys/fs/bpf
readOnly: true
# ホストからマウントするボリュームを定義します
volumes:
- name: sys-fs-cgroup
hostPath:
path: /sys/fs/cgroup
- name: sys-kernel-debug
hostPath:
path: /sys/kernel/debug
- name: sys-kernel-tracing
hostPath:
path: /sys/kernel/tracing
- name: sys-fs-bpf
hostPath:
path: /sys/fs/bpf
動作確認
コンテナイメージをビルドして、ローカルのイメージレジストリにプッシュします。
nerdctl build -f Containerfile -t localhost:30500/ebpf-agent-checker .
nerdctl push localhost:30500/ebpf-agent-checker --insecure-registry
DaemonSetのマニフェストをk3sに適用します。
kubectl apply -f daemonset.yaml
agent-checker
Podの実行ログを確認します。
kubectl logs -n kube-system -l app=agent-checker --tail=50 -f
以下のようなログが出力されました。
2025/09/14 15:40:19 Successfully loaded and attached BPF programs. Watching TCP traffic...
2025/09/14 15:40:19 Press Ctrl+C to exit.
2025/09/14 15:40:19 Interval set to 60 seconds via CHECKER_INTERVAL_SECONDS
--- Start printStats (2025-09-14 15:41:19) ---
--- TX Stats ---
::1 -> ::1 : 113183 bytes
10.42.0.182 -> 10.43.0.1 : 15990 bytes
10.42.0.1 -> 10.42.0.184 : 21430 bytes
...
--- RX Stats ---
10.42.0.187 -> 192.168.5.1 : 181 bytes
10.42.0.186 -> 10.42.0.1 : 4932 bytes
...
クラスタ内で curl
コマンドを実行するなどしてTCP通信を発生させると、60秒ごとに統計情報が出力されます。
まとめ
cilium/ebpf
ライブラリとビルダーイメージを活用することで、eBPFプログラムの開発からKubernetesへのデプロイまでをスムーズに行うことができました。
次回は、eBPFプログラムで収集したデータをメトリクスとして公開するためのk8sカスタムコントローラーを作成したいと思います。
参考情報
以下、参考情報です。
DaemonSetがマウントしているファイルシステム
sysfsと呼ばれる、Linuxカーネルによって提供される仮想ファイルシステムです。
パス | 説明 |
---|---|
/sys/fs/cgroup | リソース制御(cgroup)機能へのインターフェース |
/sys/kernel/debug | カーネルのデバッグ情報へのインターフェース |
/sys/kernel/tracing | トレーシング機能(ftrace)へのインターフェース |
/sys/fs/bpf | BPFオブジェクト管理機能へのインターフェース |
詳細はmanページを参照して下さい。
bpf2goのコンパイルの仕組み
公式ドキュメントに、コンパイルしたCのコードをGoに取り込む仕組みについて、簡単に記述されています。
今回はコンテナイメージのビルド時にコンパイルしていますが、ローカル環境でコンパイルすると自動生成された各種コードを手元で確認することができます。