Lambda MicroVMsでeBPFプログラムを実行してみた
リテールアプリ共創部@大阪の岩田です。
Lambda MicroVMsの特徴の1つとしてLinuxケーパビリティが指定できるというものがあります。
通常のLambda FunctionはLinuxケーパビリティが制限された環境下でしか実行できませんが、Lambda MicroVMsであればイメージビルド時に全Linuxケーパビリティを利用できるよう指定できます。この特徴によってeBPFプログラムの実行も可能になると公式ドキュメントで紹介されています。
By default, Lambda MicroVMs run with a standard set of Linux capabilities. You can grant elevated Linux capabilities using the additionalOsCapabilities field when creating or updating a MicroVM image. The only supported value is ["ALL"]. Elevated capabilities enable operations such as mounting filesystems, creating network namespaces, or running eBPF programs. Capabilities are applied within the VM isolation boundary and do not affect the host or other MicroVMs.
このブログでは実際にMicroVM上でeBPFプログラムが動作することを確認してみました。
やってみる
それでは実際にやっていきましょう。
今回はテスト用にlibbpf-bootstrapのリポジトリで公開されているプログラムを利用します。
MicroVMイメージのビルド
まずeBPFプログラムを内包したMicroVMのイメージをビルドします。Dockerfileは以下のように記述しました。
FROM public.ecr.aws/amazonlinux/amazonlinux:2023 AS builder
RUN dnf install -y git clang elfutils-libelf elfutils-libelf-devel zlib-devel \
&& git clone https://github.com/libbpf/libbpf-bootstrap.git --recurse-submodules \
&& cd libbpf-bootstrap/examples/c \
&& make
FROM public.ecr.aws/lambda/microvms:al2023-minimal
RUN dnf install -y elfutils-libelf
COPY --from=builder /libbpf-bootstrap/examples/c/sockfilter /usr/local/sbin/sockfilter
CMD ["tail", "-f", "/dev/null"]
マルチステージビルドを利用してビルド用のコンテナイメージでeBPFプログラムをビルドし、生成されたeBPFプログラムの中からsockfilterという実際に稼働させるバイナリをコンテナイメージにコピーしています。後ほどMicroVMのシェルに接続して動作確認したいので、CMDにはダミーのプロセスとしてtail -f /dev/nullを指定しています。
このDockerfileをzip圧縮して適当なS3バケットにアップロードし、MicroVMのイメージをビルドします。ビルド時の設定で「すべてのオペレーティングシステム機能を有効にする」にチェックを入れるのがポイントです。その他の設定は全てデフォルト値としました。

しばらく待つとMicroVMイメージのビルドが完了します。
eBPFプログラムを実行してみる
イメージがビルドできたのでMicroVMを起動します。この際、動作確認しやすいようにイングレスネットワークインターフェイスにシェルを指定しておきます。

起動できたらマネコンからMicroVMのシェルに接続し、とりあえずuname -aでも実行してみます。出力結果は以下のようになりました。
Linux localhost 6.1.166-24.303.amzn2023.aarch64 #1 SMP Wed Mar 25 10:39:29 UTC 2026 aarch64 aarch64 aarch64 GNU/Linux
このへんはイメージビルド時に指定するベースMicroVMイメージや、ベースイメージバージョンによって変わってくるはずです。今回の検証時点におけるバージョンは上記の通りでした。
それでは実際にeBPFプログラムを実行してみましょう。以下のコマンドでsockfilterのバイナリを起動します。
/usr/local/sbin/sockfilter -i eth0 &
すると、以下のように出力された後にバックグラウンドでsockfilterが稼働を続けます。ちなみにこのsockfilterはパケットを監視するプログラムで、プロトコルと送信元/宛先のIPアドレスとポート番号を標準出力に出力するものです。
libbpf: loading object 'sockfilter_bpf' from buffer
libbpf: elf: section(2) .symtab, size 192, link 1, flags 0, type=2
libbpf: elf: section(3) socket, size 584, link 0, flags 6, type=1
libbpf: sec 'socket': found program 'socket_handler' at insn offset 0 (0 bytes), code size 73 insns (584 bytes)
libbpf: elf: section(4) license, size 13, link 0, flags 3, type=1
libbpf: license of sockfilter_bpf is Dual BSD/GPL
libbpf: elf: section(5) .maps, size 16, link 0, flags 3, type=1
libbpf: elf: section(6) .relsocket, size 16, link 2, flags 40, type=9
libbpf: elf: section(7) .BTF, size 2421, link 0, flags 0, type=1
libbpf: elf: section(8) .BTF.ext, size 608, link 0, flags 0, type=1
libbpf: looking for externs among 8 symbols...
libbpf: collected 0 externs total
libbpf: map 'rb': at sec_idx 5, offset 0.
libbpf: map 'rb': found type = 27.
libbpf: map 'rb': found max_entries = 262144.
libbpf: sec '.relsocket': collecting relocation for section(3) 'socket'
libbpf: sec '.relsocket': relo #0: insn #22 against 'rb'
libbpf: prog 'socket_handler': found map 0 (rb, sec 5, off 0) for insn #22
libbpf: object 'sockfilter_bpf': failed (-22) to create BPF token from '/sys/fs/bpf', skipping optional step...
libbpf: map 'rb': created successfully, fd=3
この状態でcurlコマンドを実行してみます。
curl https://dev.classmethod.jp -s -o /dev/null
するとsockfilterの出力が標準出力に表示されました。
interface: eth0 protocol: UDP 169.254.169.253:53(src) -> 169.254.0.2:53531(dst)
interface: eth0 protocol: UDP 169.254.169.253:53(src) -> 169.254.0.2:53530(dst)
interface: eth0 protocol: TCP 18.65.214.47:443(src) -> 169.254.0.2:37304(dst)
interface: eth0 protocol: TCP 18.65.214.47:443(src) -> 169.254.0.2:37304(dst)
interface: eth0 protocol: TCP 18.65.214.47:443(src) -> 169.254.0.2:37304(dst)
interface: eth0 protocol: TCP 18.65.214.47:443(src) -> 169.254.0.2:37304(dst)
interface: eth0 protocol: TCP 18.65.214.47:443(src) -> 169.254.0.2:37304(dst)
interface: eth0 protocol: TCP 18.65.214.47:443(src) -> 169.254.0.2:37304(dst)
...略
dev.classmethod.jpを名前解決した後にHTTPSで通信していることが分かります。無事にeBPFプログラムが実行できることが確認できました!
「すべてのオペレーティングシステム機能を有効にする」をOFFにするとどうなる?
無事eBPFプログラムの実行が確認できましたが、今度は確認のために「すべてのオペレーティングシステム機能を有効にする」をOFFにしてビルドしたMicroVMイメージだとどうなるか試してみましょう。
先ほどと同じ用にsockfilterのバイナリを実行すると以下のエラーになりました。
libbpf: loading object 'sockfilter_bpf' from buffer
libbpf: elf: section(2) .symtab, size 192, link 1, flags 0, type=2
libbpf: elf: section(3) socket, size 584, link 0, flags 6, type=1
libbpf: sec 'socket': found program 'socket_handler' at insn offset 0 (0 bytes), code size 73 insns (584 bytes)
libbpf: elf: section(4) license, size 13, link 0, flags 3, type=1
libbpf: license of sockfilter_bpf is Dual BSD/GPL
libbpf: elf: section(5) .maps, size 16, link 0, flags 3, type=1
libbpf: elf: section(6) .relsocket, size 16, link 2, flags 40, type=9
libbpf: elf: section(7) .BTF, size 2421, link 0, flags 0, type=1
libbpf: elf: section(8) .BTF.ext, size 608, link 0, flags 0, type=1
libbpf: looking for externs among 8 symbols...
libbpf: collected 0 externs total
libbpf: map 'rb': at sec_idx 5, offset 0.
libbpf: map 'rb': found type = 27.
libbpf: map 'rb': found max_entries = 262144.
libbpf: sec '.relsocket': collecting relocation for section(3) 'socket'
libbpf: sec '.relsocket': relo #0: insn #22 against 'rb'
libbpf: prog 'socket_handler': found map 0 (rb, sec 5, off 0) for insn #22
libbpf: object 'sockfilter_bpf': failed (-22) to create BPF token from '/sys/fs/bpf', skipping optional step...
libbpf: Failed to bump RLIMIT_MEMLOCK (err = -EPERM), you might need to do it explicitly!
libbpf: Error in bpf_object__probe_loading(): -EPERM. Couldn't load trivial BPF program. Make sure your kernel supports BPF (CONFIG_BPF_SYSCALL=y) and/or that RLIMIT_MEMLOCK is set to big enough value.
libbpf: failed to load BPF skeleton 'sockfilter_bpf': -EPERM
Failed to open and load BPF skeleton
Linuxケーパビリティが影響してそうですね。ということで「すべてのオペレーティングシステム機能を有効にする」のチェック有無によってcapsh --printの結果がどう変わるか比較してみようと思います。
まずはチェックONの場合
Current: =ep
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,cap_perfmon,cap_bpf,cap_checkpoint_restore
Ambient set =
Current IAB:
Securebits: 00/0x0/1'b0 (no-new-privs=1)
secure-noroot: no (unlocked)
secure-no-suid-fixup: no (unlocked)
secure-keep-caps: no (unlocked)
secure-no-ambient-raise: no (unlocked)
uid=0(root) euid=0(root)
gid=0(root)
groups=
Guessed mode: HYBRID (4)
続いてチェックOFFの場合
Current: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap=ep
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
Ambient set =
Current IAB: !cap_dac_read_search,!cap_linux_immutable,!cap_net_broadcast,!cap_net_admin,!cap_ipc_lock,!cap_ipc_owner,!cap_sys_module,!cap_sys_rawio,!cap_sys_ptrace,!cap_sys_pacct,!cap_sys_admin,!cap_sys_boot,!cap_sys_nice,!cap_sys_resource,!cap_sys_time,!cap_sys_tty_config,!cap_lease,!cap_audit_control,!cap_mac_override,!cap_mac_admin,!cap_syslog,!cap_wake_alarm,!cap_block_suspend,!cap_audit_read,!cap_perfmon,!cap_bpf,!cap_checkpoint_restore
Securebits: 00/0x0/1'b0 (no-new-privs=1)
secure-noroot: no (unlocked)
secure-no-suid-fixup: no (unlocked)
secure-keep-caps: no (unlocked)
secure-no-ambient-raise: no (unlocked)
uid=0(root) euid=0(root)
gid=0(root)
groups=
Guessed mode: HYBRID (4)
チェックONの場合はCurrent: =epとなっており、全ケーパビリティが付与されていることが分かります。Bounding set にcap_bpfも出力されていますね。
とはいえ実行不可能なeBPFプログラムも多い
さて、ここまでeBPFプログラムが動くことを確認してきたのですが、実は実際には多くの制約があります。sockfilter以外にもlibbpf-bootstrapのリポジトリで紹介されているサンプルプログラムを色々と試してみたのですが、sockfilter以外のプログラムは全て動作しませんでした。実際どんなエラーが出たのか紹介します。
まずbootstrapは以下のエラーになり実行できませんでした。※-vオプションを付与してログの詳細まで出力しています。
libbpf: loading object 'bootstrap_bpf' from buffer
libbpf: elf: section(2) .symtab, size 360, link 1, flags 0, type=2
libbpf: elf: section(3) tp/sched/sched_process_exec, size 504, link 0, flags 6, type=1
libbpf: sec 'tp/sched/sched_process_exec': found program 'handle_exec' at insn offset 0 (0 bytes), code size 63 insns (504 bytes)
libbpf: elf: section(4) tp/sched/sched_process_exit, size 680, link 0, flags 6, type=1
libbpf: sec 'tp/sched/sched_process_exit': found program 'handle_exit' at insn offset 0 (0 bytes), code size 85 insns (680 bytes)
libbpf: elf: section(5) license, size 13, link 0, flags 3, type=1
libbpf: license of bootstrap_bpf is Dual BSD/GPL
libbpf: elf: section(6) .rodata, size 8, link 0, flags 2, type=1
libbpf: elf: section(7) .maps, size 48, link 0, flags 3, type=1
libbpf: elf: section(8) .reltp/sched/sched_process_exec, size 48, link 2, flags 40, type=9
libbpf: elf: section(9) .reltp/sched/sched_process_exit, size 80, link 2, flags 40, type=9
libbpf: elf: section(10) .BTF, size 27584, link 0, flags 0, type=1
libbpf: elf: section(11) .BTF.ext, size 1356, link 0, flags 0, type=1
libbpf: looking for externs among 15 symbols...
libbpf: collected 0 externs total
libbpf: map 'exec_start': at sec_idx 7, offset 0.
libbpf: map 'exec_start': found type = 1.
libbpf: map 'exec_start': found key [8], sz = 4.
libbpf: map 'exec_start': found value [11], sz = 8.
libbpf: map 'exec_start': found max_entries = 8192.
libbpf: map 'rb': at sec_idx 7, offset 32.
libbpf: map 'rb': found type = 27.
libbpf: map 'rb': found max_entries = 262144.
libbpf: map 'bootstra.rodata' (global data): at sec_idx 6, offset 0, flags 480.
libbpf: map 2 is "bootstra.rodata"
libbpf: sec '.reltp/sched/sched_process_exec': collecting relocation for section(3) 'tp/sched/sched_process_exec'
libbpf: sec '.reltp/sched/sched_process_exec': relo #0: insn #10 against 'exec_start'
libbpf: prog 'handle_exec': found map 0 (exec_start, sec 7, off 0) for insn #10
libbpf: sec '.reltp/sched/sched_process_exec': relo #1: insn #14 against 'min_duration_ns'
libbpf: prog 'handle_exec': found data map 2 (bootstra.rodata, sec 6, off 0) for insn 14
libbpf: sec '.reltp/sched/sched_process_exec': relo #2: insn #19 against 'rb'
libbpf: prog 'handle_exec': found map 1 (rb, sec 7, off 32) for insn #19
libbpf: sec '.reltp/sched/sched_process_exit': collecting relocation for section(4) 'tp/sched/sched_process_exit'
libbpf: sec '.reltp/sched/sched_process_exit': relo #0: insn #9 against 'exec_start'
libbpf: prog 'handle_exit': found map 0 (exec_start, sec 7, off 0) for insn #9
libbpf: sec '.reltp/sched/sched_process_exit': relo #1: insn #20 against 'min_duration_ns'
libbpf: prog 'handle_exit': found data map 2 (bootstra.rodata, sec 6, off 0) for insn 20
libbpf: sec '.reltp/sched/sched_process_exit': relo #2: insn #26 against 'exec_start'
libbpf: prog 'handle_exit': found map 0 (exec_start, sec 7, off 0) for insn #26
libbpf: sec '.reltp/sched/sched_process_exit': relo #3: insn #29 against 'min_duration_ns'
libbpf: prog 'handle_exit': found data map 2 (bootstra.rodata, sec 6, off 0) for insn 29
libbpf: sec '.reltp/sched/sched_process_exit': relo #4: insn #35 against 'rb'
libbpf: prog 'handle_exit': found map 1 (rb, sec 7, off 32) for insn #35
libbpf: object 'bootstrap_bpf': failed (-22) to create BPF token from '/sys/fs/bpf', skipping optional step...
libbpf: kernel BTF is missing at '/sys/kernel/btf/vmlinux', was CONFIG_DEBUG_INFO_BTF enabled?
libbpf: failed to find valid kernel BTF
libbpf: Error loading vmlinux BTF: -ESRCH
libbpf: failed to load BPF skeleton 'bootstrap_bpf': -ESRCH
Failed to load and verify BPF skeleton
原因としてはkernel BTF is missing at '/sys/kernel/btf/vmlinux', was CONFIG_DEBUG_INFO_BTF enabled?のメッセージ通り、カーネルコンパイル時のオプションCONFIG_DEBUG_INFO_BTFオプションが無効になっているのが原因です。このbootstrapというサンプルプログラムは BPF CO-RE(Compile Once Run Anywhere)を利用したサンプルになっており、eBPFプログラムをビルドしたカーネルと別バージョンのカーネルであってもeBPFプログラムを実行できるようになっています。このCompile Once Run Anywhereという特性をサポートするにはプログラムを実行する環境のカーネルでCONFIG_DEBUG_INFO_BTFオプションが有効になっている必要があるのですが、Lambda MicroVMsのカーネルではこのオプションが無効化されているようです。よってこのサンプルプログラムは実行できません。他のサンプルプログラムについても様々な要因でうまく起動できず、うまく動作確認できたのはsockfilterのみという結果でした。
まとめ
Lambda MicroVMs上でeBPFプログラムを動かしてみました。イメージビルド時のオプションで全てのLinuxケーパビリティを付与できるため、ケーパビリティが不足するということは無いのですが、カーネルのコンパイルオプションなどの要因により、eBPFプログラムを実行するのは一筋縄ではいかないようです。
eBPFプログラムのテスト環境としてLambda MicroVMsを利用するというよりも、Lambda MicroVMs上で実行したい何かしらのワークロードがあった場合に、補助的にeBPFプログラムも実行できる場合がある。ぐらいの関係性だと捉えておくのが良いのかもしれませんね。




