以下の内容はhttps://nonylene.hatenablog.jpより取得しました。


Python から eBPF の Map を操作する

みんな大好き eBPF。 eBPF 自体は C で書くが、その Map を他の言語から操作することもよくあると思う。いわゆる Data plane / Control Plane 的な役割分担ってやつ。

Go だと https://github.com/cilium/ebpf を使えばいいが、 Python だとどうするか?

BCC は嫌

Python で eBPF を利用する、となると BCC の情報がたくさんでてくる。 BCC は素晴らしいプロジェクトで nfsdist などツールは使ったこともあるのだが、 eBPF を開発するには使いたくない。

というのも、 BCC は eBPF プログラムの管理を含めて行うことを前提としている。全部乗っかりたいならいいが、 bpftool の使い方知ってるし、コンパイルだって Makefile でやりたいし、他の言語からだって使うし、そこをフレームワークで管理したくない。 BCC の作法でプログラムを書くのも嫌。

github.com

上記 Issue にもあるように、program は別途 ebpf で管理して Map だけ触りたい、といったケースでは BCC の Map は利用できないらしい(簡素なプログラムを書けばいいらしい?が... https://github.com/iovisor/bcc/issues/3517#issuecomment-1146853533)。

仮にそれが解決されたとしても pip で入れられなくて apt で入れる必要があり、その結果 dist-packages に入るから brew とか別途入れた Python ではパスに追加しないと使えない *1 とか、 virutalenv では system-site-packages しないと使えなくて不便とかもある。 面倒すぎ。

github.com

System call という API

現時点ではあまり有力なライブラリもない *2 ので、ctypes を使って System call を直接打つようにする。 eBPF の System call は割といろんなことができて、 Map の操作も一通りできる。

docs.ebpf.io

System call 自体は普通に ctypes の作法で打てば動く。Map の軽い操作くらいであればコードもシンプルで済むが、一つ注意点がある。それは、操作に使う Map の fd は BPF_OBJ_GET を利用すること。

docs.ebpf.io

こうせずに普通に open した fd を渡してしまうと、 error 22 (EINVAL) が返ってくる。

ということで、今 Python から pinned な Map を触る楽な方法は、 BPF system call の binding を ctypes で書いて適宜 call する ことだと思う。

*1:環境壊したくないから bcc module だけ dist-packages にあるものをとってくる、となるとさらに面倒

*2:ごく最近 pylibbpf というのが出たりしてるので、改善されるかも

# type: ignore じゃなくて typing.cast を使おう

Python で型アノテーションを使うとき、なんだかんだ # type: ignore したいときがあると思う。フィールドの全部が optional だけど、実際にはこのコードパスなら絶対 optional じゃない、とか。 API まわり触ってると割とよくある。

if foo is None とか assert とか TypeGuard 的なものでチェックしてもいいんだけど、アクセス前にいちいち assert するのめんどくさいし、コードはごちゃつくし、なにより無駄な処理だなって気持ちになる。Python でやりたいのは雑プログラミングであって堅牢なのが書きたければ Optional 対応がちゃんとしてるものを使えばいいんだよな

# type: ignore はよくない

そういうとき、 # type: ignore をつけると無視してエディタの警告を消し去ることができる。

ただ、 # type: ignore はあまり考えずに書くと型チェック除外の影響範囲が想定以上に大きくなってしまうことがある(以下 Pylance で確認)。

@dataclass
class Foo:
    a: float | None

def call_function(a: float):
    pass

foo = Foo(1)
call_function(foo.a) # type: ignore

たとえば上記のように書くと、 call_function 関数の呼び出し全体のチェックが行われなくなる。すなわち、ここで引数の数が足りなかったり多すぎたりしてもチェックすることができない。

ちゃんとチェックするには下のように書く必要がある。

call_function(
    foo.a, # type: ignore
    None, # => error!
)

これだけなら、一行で書かないように気をつければいいじゃんって程度だが、落とし穴はまだ深い。

例えば、以下のようなケースでも普通にエラーなく通ってしまう。

call_function(
    1,
    foo.a, # type: ignore
)

以下は第一引数の形が明らかに異なるが、警告なく通ってしまう。なんで?

call_function(
    "string",
    foo.a, # type: ignore
)

とにかく #type: ignore はできるだけ避けたほうがいい。

typing.cast

ここで typing.cast を使う。

typing.cast は型と値を引数に持って、値はそのまま返しつつ型情報のみ引数のものに変化する。

from typing import cast

call_function(cast(float, foo.a))

このようにすると、 call_function に渡している型は float ということになるので、# type: ignore を避けつつエディタの警告を消すことができるし、型の安全性を無視しているという意図もコードに含めることができる。

便利!

devcontainer で ZSH のプロンプトが root 用になってしまう対策

自分は ZSH のプロンプトを以下のように設定して、 現在 root user かどうかを判別できるようにしている。

PROMPT='${prompt_header}
%~ %(!. !root! #.>) '

! は Shell が権限を持ってる時に true になり、その時は ! root ! # が、そうでない時は > を出すようになる。

zsh.sourceforge.io

ここで devcontainer で ZSH を立ち上げたとき、vscode user なのにプロンプトが ! root ! # になっていた。

理由としては ZSH のプロセスに SYS_PTRACE が設定されていたことだった。

ZSH の "privileged" の判定は UID が 0 だけではなく、何かしらの cap が設定されている場合も含まれるらしい。

The definition of ‘privileged’, for these purposes, is that either the effective user ID is zero, or, if POSIX.1e capabilities are supported, that at least one capability is raised in either the Effective or Inheritable capability vectors.

zsh: 13 Prompt Expansion

Devcontainer によっては SYS_PTRACE が追加されて立ち上がることもあるのだと思う。実際 podman run の引数には --cap-add SYS_PTRACE がされていた。

この prompt は root の時にミスオペをしないために設定しているものであって、cap があるかどうかを確かめたいものではないので、EUID が 0 かどうかのみで判定するように書き換えた。

Homebrew (Linux) を専用ユーザーで使う

Homebrew は便利だが、通常ログインしているユーザーでインストールするのでカジュアルに環境を変更できてしまうのがよくないと思っている。

(最近ではオプションが必要になったものの) pip install とかすると break system package してしまうし、セキュリティ的にも汚染される心配がないことはないので権限を分離しておきたい。

ということで自ユーザーとは別のユーザーを作成して、そのホームディレクトリ以下に homebrew をインストールする(root でインストールするのではなく、別ユーザーにインストールする)。

$ sudo useradd linuxbrew
$ sudo mkdir -m 755 /home/linuxbrew
$ sudo chown linuxbrew:linuxbrew /home/linuxbrew
$ sudo -iu linuxbrew bash
(homebrew を普通にインストールする)
$ /home/linuxbrew/.linuxbrew/bin/brew shellenv >> /home/linuxbrew/.profile

ここで brew が linuxbrew ユーザー配下に配置された。 次に元ユーザーの shell に必要な env variables を /home/linuxbrew/.profile から元ユーザーの shell の rc にコピーして PATH などを通しておく。

この時点でもともとのユーザーから linuxbrew を叩くには以下のようにする。

$ sudo -iu linuxbrew brew install ...

これだとちょっとめんどくさいので、 shell の rc で alias を貼る。

alias brew='sudo -iu linuxbrew brew'

これで brew を打つと自動的に linuxbrew ユーザーで実行されるようになり、利便性を損なわず安心になる。

Podman で devcontainer の起動が遅い対策

Devcontainer を rootless 系のコンテナツールで利用するには podman がおすすめ(Docker rootless だと userns=keep-id がないのでコンテナ内から見たファイルが root 所有になってしまう)。

以前は userns=keep-id などをわざわざ runArgs に追加しないといけなくて面倒だったが、最近の vscode だと自動でこのフラグを付与してくれるみたいで、より使いやすくなった。

一方、podman を使っていても困ることがあり、それは大きな devcontainer になるとリビルド時の起動が遅くなること。

image のビルドが遅いのではなく、 podman によるコンテナの作成が遅い。 コンテナ作成中は podman コマンドがハングするのでステータスが謎なのも体験が悪い。

ここで、ハングしている最中に htop で確認すると storage-chown-by-maps が永遠に走っている事が分かる。これは userns=keep-id に関わる処理なのだが、 native overlay storage driver を使っていると userns=keep-id の時はコピーしないといけないから遅くなるとのこと。

github.com

github.com

上記ドキュメントにある通り、 fuse-overlayfs を利用するようにすればこの起動が遅い問題はなくなって rebuild しても一瞬で立ち上がるようになる。

永続的に設定するには ~/.config/containers/storage.conf に以下のように設定する。

[storage]
driver = "overlay"

[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"

devcontainer.json の runArgs に以下のように設定しても良いが、後述の通り native と共存はできないので config として設定してしまったほうが良いと思う。

"runArgs": ["--storage-opt", "overlay.mount_program=/usr/bin/fuse-overlayfs"]

注意事項

fuse-overlayfs は native overlayfs よりも遅いので、 image build など明らかに遅くなる。

なので VSCode で devcontianer を動かすときだけ fuse-overlayfs を使うようにしたいのだが、native と fuse の共存はできないらしい。

一度 fuse-overlayfs を利用すると、その後は native overlayfs を使うように設定していても $ podman info -f '{{index .Store.GraphStatus "Native Overlay Diff"}}' の結果は false になり、fuse-overlayfs が利用され続ける。

そのため、devcontainer.json 変更や vscode 更新の際に起動が遅いのを我慢して native overlayfs を利用し続けるか、I/O が遅いのを許容して fuse-overlayfs に切り替えるかの二択になる。

普通の Docker daemon で実行すればいいって…? あはは… rootfull な Docker daemon を一般ユーザーに開放するの普通に嫌じゃないですか...?

Cloud Run で IAP が使いやすくなっていた

Cloud Run で内部ユーザーを認証する際は IAP を使うが、 今までは LB が必要だった。今月のアップデートで LB がなくても Cloud Run で IAP が使えるようになったので便利という話。

まえがき

Google Cloud は無料枠が多くコンソールも使いやすく個人クラウドカジュアルケチ勢にはありがたいサービスで、 GCE をはじめ GKE や Firestore など便利に使わせてもらっている。

とりわけ最近使ってるのは Cloud Run。コンテナを走らせるサービスなので一般的な Web app のコードを書くだけで使えるし、コストはアクセスした分だけなので激安。Service だけでなく Job を走らせるのにも割と安くすむし、 Lambda っぽいのがほしければ Cloud Run functions を使えば自動でビルドしてくれる。Cloud Scheduler や PubSub との連携も簡単。

デプロイすると自動的にドメインが生成されて外部からアクセスできるように簡単にできるし、内部用途でも IAM 認証をかけて特定の SA に絞れたりするのも嬉しい。Outbound についても、VPC / Subnet 内への通信とかできるようになって連携しやすくなった。

…と褒めまくっているが、案外ネックなのはユーザーの認証/認可だった。具体的には、適当な個人ツールを作って Google アカウントでアクセスを自分だけに絞りたいケース。

Cloud Run での内部ユーザー認証・認可

やりたいことは以下の「内部ユーザーを認証する」にあたり、IAP を利用してとのこと。

cloud.google.com

IAP 便利そうだしいいじゃんってなるのだけど、 IAP は(今まで)ロードバランサーが必須だった。なので、雑プロジェクトを運用にするには結構高い。ほとんど使わない LB に月3kくらいかかるのはちょっとね…

そうすると Sidecar としてコンテナごとに oauth2-proxy を立てるだとか、Firebase auth を利用して app に認証認可を組み込むとかがケチ的選択肢になるが、ちょっと気軽でない感じ。

ということで今までは同じプロジェクト内にある GKE に Cloud Run を向いた ExternalName Service を作って、その Service を向いた Ingress を作り、そこでクラスタ内ですでに使っている oauth2-proxy を利用しつつ認証・認可・Proxyするようにしていた。ただそれはそれで Cloud run 側で IAM 認証が使えなくなって境界ベースの防御になるのでイケてない前時代 SOHO セキュリティになっていた。

Cloud Run と IAP の組み合わせが LB 無しでできるようになった

という悩みを長年抱えていて新しい Service を作るたびにもにょっていたのだが、今月 Cloud Run で IAP が LB なしで使える機能が出ていたのに気付いた。

cloud.google.com

便利すぎる。 LB で立てるよりも保護の範囲も広がるらしい。ということで使ってみた。

Google Cloud Project を Organization 配下にする

今は Preview 版だからか Cloud IAP の制約なのか知らないが、Project が Organization 配下でないとこの機能は使えない。

なので、Organization 配下にないような個人アカウントのプロジェクトは、まず Cloud Identity または Google Workspace をセットアップして今使っている Project を Organization 配下に移動する必要がある。

ちょっと敷居が高く聞こえるが、Cloud Identity はドメインさえあれば無料ですぐ利用できるし、移動しても特にリソースや権限は今と変わらず使えるので作ってしまえば良い。

Project 移動の注意点としては、変更がすぐに反映されるとは限らないこと。IAM も Org 操作も、少し時間がかかることが多かった。

Cloud Run で IAP を有効にする

Project が Org 配下になって少し待つと、Cloud Run に IAP の設定項目が現れる。

あとは IAP を有効化し、Policy を編集して許可したいユーザーを設定し、保存する。そうすると IAP の保護下になるので、URL にアクセスすると Google 認証画面が出てきて、特定のアカウントでしかアクセスできなくなる。

注意点

いくつか引っかかりもあったので共有。

Org 配下にしてから IAP 有効化の操作をするべき

Org の配下にする前に IAP を API 経由で有効にしようとすると以下のエラーになる。

IAP is only available for projects that are part of an Organization.

Org 配下でないときは仕様なので正しいのだが、自分が行ったときは Org 配下にした後もこのエラーが出ていた。これはおそらく Org 化前に試した際のキャッシュによるもので、1,2時間待てばこのエラーの頻度は減っていった(最初はガチャ性があるので何回か試すと良い)。 Org 配下にしてから有効化するとこんなことにはならなかったかもしれない(もっと別の場所のキャッシュかもしれないが)。

組織外のアカウントは Principal に追加しても認可されない

ドキュメントにもあるが、Cloud Run の IAP で Principal として設定できるのは組織に属するユーザーのみである(正確に言うと、他のユーザーも設定はできるが動作しない)。そのため、普段の gmail アカウントを Principal に入れてアクセスしても IAP のエラー画面が返ってくる。

Cloud Identity を作るときに組織のユーザーも作っているはずなので、それを使うようにすれば良い。

Ingress は All にする

Internal にしていると IAP 経由だろうが外からのアクセスは Not found が返ってくるので、IAP を有効化するときに All にしておく必要がある。

IAP の設定を変えた際は、Cloud Run の URL に紐付く Cookie は一旦消したほうが良い。そうすると認証が必ず再度走るので確実な挙動確認ができる。

…ということで Cloud Run で微妙だった点が解決した話だった。今後も積極的に使っていこうと思う。

Power Automate for Desktop を起動するショートカットが Task Scheduler 経由だと動かない対策

Power Automate for Desktop ではショートカットキーを設定することができるので、そのショートカットキーを押すスクリプトを Task Scheduler を経由して実行することで定期実行ができる。

「Power Automate for Desktop Task scheduler」とかで検索すると方法が出てくるが、Task Scheduler でタスクを作ってテスト実行してみるとなぜかうまく動かない。

どうやら、少なくとも Task Scheduler がアクティブなアプリケーションとなっているときは Power Automate for Desktop のショートカットが発動しないらしい。スクリプトでキーを送信する前にフォーカスを外しておく必要がある。

自分は一旦デスクトップをアクティブにしてからショートカットを押すようにした。

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName Microsoft.VisualBasic

[Microsoft.VisualBasic.Interaction]::AppActivate("Program Manager") # program manager = desktop
[Windows.Forms.SendKeys]::SendWait('^+{F1}') # フローに設定したショートカットキー



以上の内容はhttps://nonylene.hatenablog.jpより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14