containerにおけるSELinuxの役割について
背景
本家のruncで実装されているSELinux機能が、Youki (Rustでruncを再実装するOSS)に実装されていないことがわかった。 そこで、SELinux機能をYoukiに導入することになったのだが、SELinux crateが無かったのでRustで再実装することになり、そのプロジェクトにアサインしてもらった。 しかし、SELinuxについて何も知らなかったので、SELiuxについて色々と調べたことをまとめた。
SELinuxとは何か?
security-enhanced Linuxの略称。MAC制御を行うことができる。通常のセキュリティに加えてSELinuxを設定することで、システムセキュリティを更に強化できる。
Labelとpolicyを組み合わせたセキュリティ制御が特徴である。process・file・networkなどのobject、process・userなどのsubjectのそれぞれに、labelが付与される。label同士のルールをsecurity policyに定義することで、subject・object間のアクセスをコントロールする。
メリット
- SELinuxのルールはlabelで決定するので、ルート権限などの概念は考慮する必要が無い。
- 権限昇格攻撃による被害を軽減することができる。process・fileなどはdomainで互いに分離されているので、あるprocessが乗っ取られても、被害を最小限に抑えることができる。
- 管理者がアクセス制御を行うため、一般ユーザーに変更される心配がない。
- ウイルス対策ソフトウェアなどと独立した機能として、OSの安全性を高めることができる。
DAC, MAC
Subject, Object
- Subject: process、userなどを指す。
- Object: file、directoryなどを指す。
SELinuxの動作モード
OSは、SELinuxの動作モードを選択する。config file(etc/selinux/config)を書き換えると、動作モードを変更できる。/sys/fs/selinux/enforceにも動作モードの設定がある。前者のファイルを書き換える場合は再起動が必要だが、後者の場合は、再起動せずに変更することができる。
Mount
SELinuxがenableされると、SELinuxファイルシステムが特定のディレクトリ以下でマウントされる。マウントされたSELinuxディレクトリ内で、ポリシー、状態管理が行われる。
label (SELinux context)
SELinuxが有効の場合、subject・objectは、通常のfile permission (例: 777)のほかに、label(SELinux context)を持つ。labelは4つのpartから構成される。subject, objectのどちらにも、以下の形式で付与される。
User:Role:Type:level
- User: Userごとに付与されるラベル。例:
system_u
- Role: Userに対して紐づいたRole名。RBACとして機能する。例:
object_r
- Type: subjectのdomain, およびobjectのtypeを定義する。末尾に
_t
が付与される。例:etc_t
。etc内のファイルにetc_t
が付与される、のようにルールが適用される。 - Level: MLS, MCSに利用される属性。例:
s0
例:system_u:object_r:bin_t:s0:c1,c2
Type enforcement (domain)
あるdomainを持つobjectには、policy ruleが許可されたsubjectしかアクセス出来ない。例えば、httpd_t
というドメインで実行されているプロセスは、httpd_sys_content_t
を持つファイルにアクセスできるが、mysql_db_t
というドメインを持つファイルにはアクセスできない。
あるsubject・objectをdomainで制約したくない場合、unconfined
というdomainを設定することができる。このdomainを使えば、特定のdomainに制約されることなく、多くの操作を行うことができる。
xattr
標準のファイル属性に加えて、追加のメタデータをファイルに格納する仕組み。SELinuxは、xattrを利用してファイルにlabelを設定する。security.selinux
という拡張属性(キー)を利用している。xattrは、ファイルのindex node (inode)に書き加えられる。inodeにはファイルのパーミッション・所有者・更新日時などの情報が保存されている。
procfs
Process filesystemのことで、processに関する情報が蓄えられている。processのlabelは、xattrではなくprocfsのファイルに書き込む。
MLS enforcement
Multi level securityの略称。同じTypeに属するが別の挙動をさせたいsystemに、levelを設定することができる。例えば、A, Bは同じType httpd_t
に属するが、Aだけにアクセスさせたいファイルがある場合、Aには強いlevelを設定することで、A・Bのアクセス権を区別できる。s0
からs15
までの16段階のlevelがあり、s15
が最も機密レベルが高い。
security Policy
SELinuxに関するルールを定義する。あるdomainは、あるtypeにアクセスできる、などを定める。以下の形式でルールを設定する。classは、どのリソースに対するルールなのかを表す。file, directory, simbolic link, device, ports, cursorなどのリソースがclassの種類として存在する。 以下の形式で書かれる。
allow <domain> <type>:<class> { <permissions> };
以下はルール例。
ftp_home_dir (off , off) Allow ftp to home dir smartmon_3ware (off , off) Allow smartmon to 3ware mpd_enable_homedirs (off , off) Allow mpd to enable homedirs xdm_sysadm_login (off , off) Allow xdm to sysadm login xen_use_nfs (off , off) Allow xen to use nfs mozilla_read_content (off , off) Allow mozilla to read content ssh_chroot_rw_homedirs (off , off) Allow ssh to chroot rw homedirs mount_anyfile (on , on) Allow mount to anyfile
Domain transition
domain transitionは、あるprocessのlabelを変えられる。例えば、あるprocessがあるコードを実行して新しくprocessが生成される時に、別のlabelとして実行させることができる。dyntransitionとも記載される。
Dockerなどのcontainerで、SELinuxがどのように使われるのか?
Host, containerを互いに保護するために利用される。
DockerはSELinuxの機能のうち、主に、type enforcement, MCS(Multi-category security)分離、という2つの機能を利用している。type enforcementは、Host・container間の保護に利用される。MCSはcontainer同士の保護に利用される。 Dockerでは、SELinuxがdefaultで有効化されているわけでは無いので、有効化したい場合は設定を変更する必要がある。
Type enforcement
subject, object間でのアクセス制御に使われる。label同士のやり取りをpolicyで設定する。
コンテナのdefault typeは、svirt_lxc_net_t
である。このtypeは、usr, etc以下のほぼ全てのファイルをread, execできるが、var, home, rootなど、他ファイルへのアクセスは制限される。ネットワーク使用は許可されている。コンテナ内の全てのファイルはsvirt_sandbox_file_t
ラベルが付与される。svirt_lxc_net_t
はsvirt_sandbox_file_t
およびdocker_var_lib_t
とラベルされたファイルにはアクセスできる。docker_var_lib_t
はdocker volumeの使用に関連する。
MCS(multi category security)分離
この機能は、Docker, OpenShiftなどのコンテナ権限管理に利用されることが多い。
MCS分離はsvirt
とも呼ばれる。コンテナごとにuniqueな値(Docker起動時のPID)が、SELinux labelのlevel fieldに付与される。つまり、コンテナは、そのprocessがコンテナであることを表すlabelと、「どのコンテナなのか」を表すlabelが付く。
processのMCS labelは、開きたいファイルのMCS labelに対応している必要がある。例えば、s0:c1,c2
のlabelを持つprocessはs0, s0:c1, s0:c2, s0:c1,c2
にアクセスできる。s0:c1,c3
にはアクセスできない。
labelは複数のカテゴリを推奨される。そのため、s0:c1
, s0:c2
は好ましく無い。
go-selinuxでカバーしている機能
containerd organization内にあるレポジトリ。
subject・objectのlabel操作を行う関数・動作モードを変更する関数などをサポートしている。 あくまで、containerd関連のプロジェクトがSELinux機能を使うサポート用のOSSである。そのため、SELinuxが持つ、label・policyを元にアクセス制御するシステム自体を実装しているわけでは無い。
このレポジトリを絶賛Rustに移植している。
参考にしたリンク
https://projectatomic.io/docs/introduction/
https://opensource.com/business/13/11/selinux-policy-guide
Web ComponentとReact
以下の記事は、自分の知識整理のための記事です。間違っていたらご指摘ください。
Web Componentとは
ウェブの再利用可能なウィジェット・コンポーネントを作成するための標準技術の集合。
これにより、HTML, CSS, JSをカプセル化した上で再利用することが可能になる。
1. カスタム要素
カスタム要素と、その動作に関するJavaScript API。以下のようなコードをJavaScriptに書くことで、HTMLからpopup-infoタグを使用できるようになる。
customElements.define("popup-info", PopupInfo); class PopupInfo extends HTMLElement { constructor() { // コンストラクターでは常に super を最初に呼び出してください super(); // ここに要素の機能を記述します } }
<popup-info></popup-info>
2. Shadow DOM
Shadow DOMツリーを要素に紐づけて関連する機能を制御するための、一連のJava Script API。カプセル化している。つまり、シャドウDOM外のJavaScript, CSSが、シャドウDOM内の要素に影響を与えることは無い。
1のカスタム要素単体だと、カプセル化されていないので、Shadow DOMと組み合わせることで、外部と隔離された形でタグが使用できるようになる。Shadow DOMは、HTML templateと違って、attachされたらすぐにレンダリングされる。
3. HTML template
template要素と、slot要素によって、レンダリングされたページに表示されないマークアップのtemplateを書くことができる。JavaScriptを介してインスタンス化される。
HTML templateは、templateの内容をカスタム要素としてカプセル化した上でShadow DOMに追加することで真価を発揮する。
customElements.define( "my-paragraph", class extends HTMLElement { constructor() { super(); let template = document.getElementById("my-paragraph"); let templateContent = template.content; const shadowRoot = this.attachShadow({ mode: "open" }); shadowRoot.appendChild(templateContent.cloneNode(true)); } }, );
<template id="my-paragraph"> <style> p { color: white; background-color: #666; padding: 5px; } </style> <p>My paragraph</p> </template>
こうすると、以下のような形で、my-paragraphタグを利用できる。my-paragraphは、HTML templateなので、ページ読み込みと同時にレンダリングされるわけではないので、パフォーマンス向上に貢献できる。
<my-paragraph></my-paragraph>
Web ComponentとReact
メルカリのWeb Componentsに関する記事によると、メルカリ Core teamはWeb ComoponentsからReactに移行中であるとのこと(2022年末)。 そもそもWeb Componentsを導入した背景としては、Web Componentsを使えば、React, Vueなど、様々なフレームワークで利用できるというメリットがあったためらしい。
しかし、導入後、以下のような課題が出てきた。
- Web Componentsを使ってくれる・contributeしてくれる人が少ない
- Web Componentsでサーバーサイドレンダリング(SSR)を実行するのは難しい
- Web Componentsのその他の技術的な制約
そこで、Web ComponentsからReactへの移行を実施しているらしい。
所感
Web Components自体は面白い技術だが、導入した場合、メルカリの記事にあるような技術課題に直面しそう。つまり、Web Componentsは「世間的な認知がそこまで高くない・SSRなどの技術的制約が存在する」。Web Componentsを導入するのであれば、組織全体でWeb Components体制をサポートする必要がある。ただ、そこまでの企業努力をしてまで導入するメリットは、一般的な会社には無さそう。エンジニア採用しやすくて、使いやすい技術スタックという観点で考えると、Reactに軍配が上がる。
参考
https://developer.mozilla.org/ja/docs/Web/API/Web_components
https://engineering.mercari.com/blog/entry/20210823-8128e0d987/
Youkiの基礎知識とcontribution環境構築
Youkiとは
runc同様、OCI runtime specである。Rustで書かれている。日本語の容器が名前の由来。
なお、OCI runtime specは、1. 隔離環境の作成, 2. コンテナ実行, 3. プロセスのKill, 4. コンテナの削除、に関する明文化された仕様を指す。
メリット
Rustで実装するメリットは、Goよりsystem callを扱いやすい(Cを呼び出すオーバーヘッドが少ない、ゼロコスト抽象化などの仕様のおかげ?)、Cと比べてメモリ安全、が挙げられていた。そのおかげか、Youkiは、runcより高速に動作するらしい。
仕様
現在はLinux環境しかサポートしていない。Linux以外の環境で動かしたい場合、VagrantでVMを利用する必要がある。
Youkiは低レベル container runtimeなので、Docker, Podmanなどの高レベル container runtimeと組み合わせて使うことになる。
Youkiには、ルート権限が必要なrootful modeと、不要なrootless modeがある。ルート権限とは、コンテナ内のプロセス権限がルートユーザーと同等かどうか。ルート権限がある場合、セキュリティ面での問題が懸念される。このあたりはコンテナセキュリティ本を読むと、雰囲気がわかるはず。
Dockerで動かす方法
etc/docker/daemon.json
を書き換えることで、Dockerでもyoukiもruntimeとして使える。
{ "default-runtime": "runc", "runtimes": { "youki": { "path": "/path/to/youki/youki", "runtimeArgs": [ "--debug", "--systemd-log" ] } } }
レポジトリの説明
libcgroups
Linux cgroupとやり取りしたりするための機能を担っている。common, stats, systemd, test_manager, v1, v2といったmoduleを持つ。例えば、cgroup fileの読み書きを簡単にしてくれるuser interfaceなどが提供されている。
libcontainer
containerの作成・管理を担う。apparmor, container, hook, namespacesなどの様々なmoduleがある。
liboci-cli
OCI container runtimeのためのCLIを提供している。
libseccomp
libseccompへのRust FFI bindingを提供する。FFIとは、あるプログラミング言語から他プログラミング言語を利用するための機構。この文脈でいうと、RustからC言語で書かれたlibseccompを扱うということだと思う。
Webasssembly
runwasiのように、WebAssembly moduleをYoukiで実行することが可能。
control flow
以下のような流れでYoukiは動作するらしい。
Contribution環境を作るときに試行錯誤したメモ
Rustに関するメモ
コードリーディング・contributionをする際に、分からなかったところのメモ。
Builder
以下のstruct Linuxの場合、ビルダーパターンを利用して初期値を設定することができる。ビルダーパターンを使うと、オブジェクトの構築をstep by stepで行えるようになり、オブジェクトの初期化を扱いやすくしてくれる。コード内でLinuxBuilderが出てきたが、LinuxBuilderというオブジェクトを定義している箇所はなく、LinuxオブジェクトにBuilderがderiveされているだけ。
#[derive(Builder, Clone, Debug, Deserialize, Eq, Getters, Setters, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] #[builder( default, pattern = "owned", setter(into, strip_option), build_fn(error = "OciSpecError") )] #[getset(get = "pub", set = "pub")] /// Linux contains platform-specific configuration for Linux based /// containers. pub struct Linux { #[serde(default, skip_serializing_if = "Option::is_none")] /// UIDMappings specifies user mappings for supporting user namespaces. uid_mappings: Option<Vec<LinuxIdMapping>>,
Linux Builderの初期化例
// Linux構造体のビルダーを使用して、uid_mappingsに値を設定 let linux = Linux::builder() .uid_mappings(vec![uid_mapping]) // ここでuid_mappingsにベクターを設定 .build() // ビルダーパターンを完成させ、Linuxインスタンスを生成 .expect("Failed to build Linux"); // build()メソッドがエラーを返す可能性があるため、unwrap()やexpect()で処理
enum型
以下のようなenum型があるとする。
#[derive(Debug, thiserror::Error)] pub enum InitProcessError { #[error("failed to set sysctl")] Sysctl(#[source] std::io::Error), #[error("failed to mount path as readonly")] MountPathReadonly(#[source] SyscallError),
enum型は、以下のように、InitProcessError::Sysctl (各enumの値)、のように指定して使う。
fn set_sysctl() -> Result<(), InitProcessError> { Err(InitProcessError::Sysctl(io::Error::new(io::ErrorKind::Other, "oh no!"))) }
contribution環境の構築
私はLinux環境 PCでは無いので、CodeSpaceを使用した。EC2(amazon Linux)も試したが、CodeSpaceの方がラクだった。
just
justとは、makeに影響を受けたtask runner。Makefileと違って、.PHONYが不要なので簡潔に書ける。
install方法はdocumentに書いてある(makeで良いのでは?と思った)
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/bin export PATH="$PATH:$HOME/bin"
golang
runtime-toolsを動かすために、go 1.19が必要。apt-getだとversion指定できないのでwgetでversion指定してインストールした。
wget https://go.dev/dl/go1.19.13.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.19.13.linux-amd64.tar.gz export PATH="$PATH:/usr/local/go/bin"
Rustc
CodeSpaceではすでにインストールされていて不要。EC2 amazon Linuxで動かすときはインストールが必要。
curl https://sh.rustup.rs -sSf | sh
GitHubとの接続
CodeSpaceでは不要。EC2 amazon Linuxで動かすとき必要だった。
sudo yum install -y git ssh-keygen -t rsa -b 4096 cat ~/.ssh/id_rsa.pub # 結果をgithub settingsに貼り付ける。 ssh -T git@github.com git clone git@github.com:hogehoge/youki.git
その他のライブラリ
CodeSpaceでは不要。EC2 amazon Linuxで動かすとき必要だった。このあたりはYoukiのREADMEに書いてある。
sudo dnf install \ pkg-config \ systemd-devel \ elfutils-libelf-devel \ libseccomp-devel \ clang-devel \ openssl-devel # 以下のコマンドを実行してリンカーを入れる。 sudo dnf install glibc-static libstdc++-static
参考
GraphQLの基礎知識
GraphQLを使っているエンジニアの人と話していて、自分がGraphQLをよくわかってないことに気づいたので、基本的な情報を調べてみた。
GraphQL
Metaが開発したOSS。APIを効率よく呼び出すために作られたクエリ言語。
SQLがRDBを使うように、GraphQLはGraphQL用の特殊なDBを使うのかと思いがちだが、そういうわけでは無い。GraphQLもRDBなどを使う。なので、DB内の計算などを効率化するわけでは無い。backend側のコードを見通し良く効率的に書ける、というモチベーションが強い印象。
GraphQLの特徴
必要なデータを過不足なく、一度で取得できる(アンダーフェッチ、オーバーフェッチが無い)。そのため、データフェッチの効率性が高い。
基本的に、1つのデータを取得する際1つのエンドポイントを呼ぶだけで完結するので、APIの設計をシンプルにすることができる。クエリ言語を柔軟に記載することができる。
色々なサポート機能がある。例えば、Subscriptionsという機能を使って、リアルタイムデータを効率的に扱うことができる。
向いているサービス
ソシャゲ、eコマースなどの複雑なデータ構造を持つサービス、リアルタイムデータを扱うアプリケーションなどで真価を発揮する。
一方で、データ構造がシンプルなサービス、キャッシュが重要なサービスなどには向いていない。
データベース
特定のデータベースに依存していない。RDBでも、NoSqlでもOK。必ずしも、GraphQLを使うとREST APIよりもデータベースからfetchする回数が減るとは限らない。実装次第。
Resolver
スキーマとデータソースを結びつける役割を持つ。スキーマはあくまで定義のみで、Resolverが実際の操作を行なってくれる。
Subscriptions
GraphQLサーバーにデータ変化などの特定のイベントが生じるたびに、クライアントにデータ送信するPubSubのような仕組み。クライアントがサーバーにGraphQLのクエリ、クエリ変数を送信する。サーバーは、イベントがトリガーされたらクエリを実行する。
DataLoader
データ取得に使用される汎用的なツール。データ取得をbatch処理する機能と、結果をcacheする2つの機能がある。
batchは、データ取得のリクエストを一定時間待って、その間に行われたリクエストを統合してbatch処理してくれる。
cacheは、batchに登録されるkeyと、それに対応するvalueの組をメモ化する。
これらの機能を使うと、データアクセスの回数が減少する。
DataLoader導入前
SELECT item_id, amount FROM orders WHERE customer_id = "foobar"; SELECT name, description, price FROM items WHERE id = "hoge"; SELECT name, description, price FROM items WHERE id = "fuga"; SELECT name, description, price FROM items WHERE id = "piyo";
DataLoader導入後
SELECT name, description, price FROM items WHERE id IN ("hoge", "fuga", "piyo");
N+1問題
N件のデータレコードを取得した時、関連レコードの取得にN回のfetchを行なって、N+1回のfetchをしてしまうこと。DataLoaderで解決できる。
Relay
Metaが開発しているGraphQLクライアント。
- 全てのObjectに対して、一意のGlobal IDを要求するので、データの再利用性が高い
- リスト、ページネーションなどの際、Connectionという仕様を使うことで効果的に処理できる
- Mutationを行いやすい
- Fragment colocationによって、データフェッチが最適化される
Fragment colocation
コンポーネントとGraphQLフラグメントを密接に配置するデザインパターン。
以下のような利点がある。
- 明確な依存関係を示すことができる
- 再利用性の向上
- 効率的なデータフェッチ
- 保守性の向上
GraphQLのメリット
クライアントはスキーマをもとに、欲しいフィールドだけをリクエストできる。
リレーションがある値は、一度のリクエストで取得できる。例えば、twitterのフォロー数、フォロワー数を取得する際、REST APIだと2回のリクエストが必要だが、GraphQLであれば一度で取得できる。
スキーマをもとに開発するので型の不一致が起きない。強い型付けが採用されている。
GraphQLのデメリット
クエリが複雑になる可能性・学習の難しさが挙げられる。
また、GraphQLのメリットを享受するのが難しい。GraphQLはMetaがReact・Relayと組み合わせて使い始めた。「RelayスタイルでないGraphQLに価値はない」と言うGraphQL有識者もいる。Relayを使わないにせよ、Fragment Colocationがわかってないとメリットを享受しづらい。
RESTでやっていたことが直感的にやり辛くなる側面もある。
gqlgen
GraphQLサーバーを構築するGoライブラリ。GraphQLスキーマ定義言語を使用してAPIを自動生成してくれる。
参考文献
https://sizu.me/adwd/posts/34mkeimfb06x
https://lyohe.github.io/blog/2021/12/16/reading-dataloader/#:~:text=graphql%2Fdataloader
runwasi関連の基礎知識
runwasiとは
ContainerdでWasmを動かすための共通platformを提供する。WASIをターゲットにcompileされたプログラムであれば、WASIに準拠したランタイム(wasmtimeなど)で実行できる。Rustで記述されている。
runcの代わりに、WebAssemblyランタイムを可能にするshim(異なる環境での互換性を保つソフトウェア層)であるrunwasiをcontainerdで動かすことが出来る。
Container毎に1つのshim processが存在するnormal modeと、すべてのshimを実行する単一のprocessで実行されるshared modeがある。
Wasm containerはコンパイルされたWasmバイトコードのみなので、Linuxコンテナよりも非常に軽量で起動が速く可搬性も高い。
Component
containerd-shim-[ wasmedge | wasmtime | wasmer ]-v1
wasmedge, wasmtime, wasmerでwasmを実行するためのcontainer shimである。このshimはPod毎に1つ実行される。
containerd-shim-[ wasmedge | wasmtime | wasmer ]d-v1
containerdをcontainerd-[wasmedge | wasmtime | wasmer]d sandbox daemonに接続するために使われるCLI。containerdがcontainer作成を要求すると、このshimバイナリが起動され、ホスト上で実行されているcontainerd-[wasmedge | wasmtime | wasmer]dサービスに接続する。このバイナリ自体がリクエストを処理することはなく、サンドボックスの作成・破壊のリクエストをcontainerd-[wasmedge | wasmtime | wasmer]d daemonに送信する。
containerd-[ wasmedge | wasmtime | wasmer ]d
Pod毎、Container毎ではなく、Node全体で1つのwasmホストを実行できるようにするsandbox manager。Containerが作成されると、このサービスはsandboxを作成するリクエストを受け取る。
Wasmedge | wasmtime | wasmer engineは、全てのsandbox間で共有化される。
Runtime
あるソフトウェアを実行するために必要な環境のこと。Javaプログラムを実行するにはJava Runtime環境(JRE)が必要、といった意味。Containerを実行するには、Container実行環境が必要である、ということ。Container runtimeが担っている役割は、以下のcontainerdの項目で記載している。
containerd
コンテナのライフサイクルを管理するコンテナランタイムで、Linux・windowsのデーモンとして動作する。コンテナイメージの管理、実行中のコンテナの管理・監視、ストレージからのコンテナ実行、低レベルのストレージ、ネットワークアタッチメントなど、ホストシステムのコンテナライフサイクルを管理する(厳密にはruncがサポートする部分もある)。
元々containerdはDockerの一部だったが、DockerがOCIの仕様に基づいてDockerが分割された。その一部がcontainerdとしてCNCFに寄贈された。そのため、containerdはシステムの一部として動くことを想定してデザインされている。
コンテナ実行レイヤのデフォルトのランタイムはruncを利用している。containerdが高レベルランタイム、runcが低レベルランタイムである。他の高レイヤランタイムにはcri-o, rktなどがあり、低レイヤランタイムには、gVisor, Nabla containersなどがある。
ctrコマンドで操作する。
runc
runcがカーネル機能を使ってコンテナ作成を行う。cgroups, namespaces, pivot_rootなどを操って、プロセスの隔離環境を作成して、コンテナを実行する。OCI runtime specificationに準拠している。
OCI Runtime specification
以下のライフサイクルに従う。runcは準拠している。
1 隔離環境の作成
2 コンテナ実行
3 プロセスのKill
4 コンテナの削除
CRI
criはkubernetesでcontainerdを動かすためのpluginである。cri pluginはcontainerdを介してコンテナライフサイクルなどを管理する。一方で、cri pluginがCNIを介してpod networkも管理する。
WebAssembly
Webブラウザ上でネイティブコード(CPUが直接実行可能な命令セット)に近い実行速度で高速に実行できるバイナリフォーマット。従来のJavaScriptがソースコード形式で提供されていたのと違って、低レベルのバイナリで提供されるので、高速に実行できる。特定のプロセッサに依存しない。
厳密には、stack virtual machineのためのバイナリ形式の命令セット。
ちなみに、WebAssembly単体ではメモリ確保ができないため、外部プログラムが確保したメモリを渡す必要がある。このメモリは線形メモリと呼ばれる。
WASI (WebAssembly System interface)
OSのAPIを抽象化するための業界標準仕様のこと。Webブラウザだけでなく、Windows・LinuxなどのOS上にWebAssembly runtimeを配置して実行できるようになった。WebAssemblyがファイルやネットワーク、メモリなどのシステムリソースへ安全にアクセスするためのAPI標準仕様。
WASIは、特定のOSに依存した設計にしないために、System call (OS操作)に依存せず、User Land(CPU・メモリーの直接計算)上での処理だけが可能になっている。その欠点を補うために、他のプログラム(runtime)と連携して動くようになっている。WebAssemblyをwasmtimeでファイル操作をしようとすると、ファイル操作のみwasmtimeが実行する、といった仕組み。
WASIは、外部プログラム(runtime)がWebAssemblyを実行する標準仕様を定義している。WebAssemblyをどう実行するか、WebAssemblyにExportする関数の仕様などである。
WASI preview1でファイル操作規格が決められ、WASI preview2でネットワークに関する規格が追加される予定。
参考にした記事
https://qiita.com/wbcchsyn/items/153327b0946358694061
OpenTelemetry 101
OpenTelemetryとは
テレメトリデータを収集してバックエンドプラットフォームに転送するための方法を標準化してくれるフレームワーク。OpenCensus・OpenTracingという2つのソフトウェアがマージされて誕生した。OpenTelemetryは、SDK・API・ツールの総称であり、バックエンドを担うものでは無い。
分散システム上のアプリケーション・ホスト環境のトラブルシューティング・デバッグに必要なデータを収集することが可能。複雑なシステムのトラブルシューティングを容易にしてくれる。
CNCFでk8sに次いで、2番目に活発なProjectだとか。
OpenTelemetryのメリット
- 一貫性
- 互換性
- OpenTracing, OpenCensusのどちらとも互換性がある
- 拡張性
- 用途に応じた拡張性能が高い
OpenTracing
OpenTracingは、トレーシングを標準化するAPIを提供することを目指していた。トレーシングに特化しており、用途が限られているという課題があった。
OpenCensus
OpenCensusはGoogleが社内のトレーシングプラットフォームをベースに開発した。収集したメトリクスを任意のバックエンドに転送することをサポートしていた。しかし、OpenCensusをコードに組み込むAPIが提供されていなかった。
可観測性(オブザーバビリティ)とは
「監視」と比較されることが多い。監視には積極的な意味合いが強く、システムに設定された閾値を超えたらアラートを鳴らす。一方で可観測性は、システム内の状態を常に包括的に把握することを指す。システムの出力からシステム状態を測定するために利用する。従来の監視は、既存の問題にしか対応できないが、分散システムにおいては、どのような問題が発生するか、事前に予測するのがとても難しい。可観測性は、予測不可能性にアプローチする最適解となり得る。
可観測性が誕生した背景
前述したように、分散システムを適切に監視できる手法が求められていた。従来の監視だと以下のような問題に対処できなかった。 - マイクロサービスに分散したログをリクエスト単位で追いづらい - エラー原因箇所の同定の難しさ - レスポンス遅延の原因把握
テレメトリーデータとは
テレメトリーデータは、Log, Metrics, Tracesの3つに分類される。それに加えて、OpenTelemetry特有のBaggageというテレメトリーが存在する。
Log
- 発生したイベントを各時点で記録するテキスト。プレーンテキスト・構造化・非構造化データがある。
- エラーログ、アクセスログなど
I, [2021-02-23T13:26:23.505892 #22473] INFO -- : [6459ffe1-ea53-4044-aaa3-bf902868f730] Started GET "/" for ::1 at 2021-02-23 13:26:23 -0800
Metrics
- 一定の期間にわたって測定された値を指す。タイムスタンプ・イベント名・イベント値などの属性が記録される。構造化データであることが普通
Traces
{ "name": "hello", "context": { "trace_id": "0x5b8aa5a2d2c872e8321cf37308d69df2", "span_id": "0x051581bf3cb55c13" }, "parent_id": null, "start_time": "2022-04-29T18:52:58.114201Z", "end_time": "2022-04-29T18:52:58.114687Z", "attributes": { "http.route": "some_route1" }, "events": [ { "name": "Guten Tag!", "timestamp": "2022-04-29T18:52:58.114561Z", "attributes": { "event_attributes": 1 } } ] }
Baggages
- spanを介して伝達されるcontext情報のこと。key-value storeとして機能する
- context, propagationという仕組みで、分散トレーシングを効率的に実現する
OpenTelemetryの機能
システム構成
言語別のOpenTelemetry API
- 各プログラミング言語に対応した、データ収集のためのコードがある
- Automatic instrumentationといった機能もある(後述)
- C++, Go, Java, JavaScript, Python, Rust etc
- コード例
言語別のOpenTelemetry SDK
- APIとエクスポーターの間の橋渡しとなる
OpenTelemetry Protocol (OTLP)
OpenTelemetry Collector (OTLC)
- ベンダーに依存することなくテレメトリーデータをアプリケーション、インフラから受信して、監視バックエンドにデータ連携する。Receiver, Processor, Exporterの3要素から構成される
- Receiver
- OpenTelemetryを受け取る。いろいろなデータを受け取ることができる
- kubernetesにおいては、kubeletstats Receiver, Filelog Receiver, Kubernetes Cluster Receiver, Kubernetes Objects Receiverなどがある
- OpenTelemetryを受け取る。いろいろなデータを受け取ることができる
- Processor
- 属性に応じて送信先を変えたり、指標を集積してカウンタ値を作成することが可能
- production・staging, host, serviceなど
- 属性に応じて送信先を変えたり、指標を集積してカウンタ値を作成することが可能
- Exporter
- HTTP, gRPCなどで送信。あくまでOpenTelemetryはバックエンドに送信するだけで、データ保存はPrometheusなどの下流が担う
- Receiver
Auto instrumentation (kubernetes operator)
- 従来のトレーシングでは、観測データを出力する部分をコーディングする必要があったが、OpenTelemetryでは、コードを記載しなくてもinstrumentationを行うことができる
- OpenTelemetry Operatorをk8s上にdeployした上でPodにannotationを追加すると、そのPod sidecarからトレーシングデータを自動取得できるようになる
- Auto instrumentationでカバーできないデータは、自分で実装する必要がある
- どのデータをauto instrumentationするのかは、yaml fileで指定可能
FaaS
AWS LambdaのようなFunctions as a Service (FaaS)でも、instrumentationは機能する。
まとめ
OpenTelemetryの各種仕様がfixしてきているので、今後さらに盛り上がることが期待される。
promptfooをkubernetesで動かすツールを作った
LLMの勢いがすごいです。そろそろエンジニアも廃業かと思いきや、なかなか僕の仕事は無くなりません。
昨今LLM自体の開発が進む一方で、LLMOpsのツールはそんなに発展しないなぁと思っていましたが、Gunosyさんのブログで、promptfooというすごいツールを知りました。
promptfooの出来栄えに感動したので、promptfooをkubernetesで動かせるツールを作りました。
要約
kubernetes上でpromptfooを定期実行(cronjob)するツールを作りました。PromptFooConfigというCRDに、prompt, openai_key, scheduleを設定すれば、cronjobが実行されます。
この記事で、promptfoo自体の紹介はしないので、気になる方はGunosyさんのblogを読んでください。
モチベーション
LLMをproductionで使う時の課題として、時間が経つことで、当初想定していたoutputを得られなくなってしまうことが挙げられます。例えば、以下のようなケースがあると思います。
- コード、promptの変更によって、想定されるoutputを入手できなくなる
- OpenAI APIのsilent updateによって、想定されるoutputがいつの間にか手に入らなくなっていた
前者の課題については、promptfooをCI/CDに組み込むことで解決できます。
しかし、後者の課題は、promptfoo単体では解決できません。定期実行するための機能を備えていないからです。
promptfooはCLIツールなので、定期実行できる環境さえあればどこでも実行できますが、kubernetes上でcronjobとして実行することができれば、1つの解決策になるだろうと考えました。
使い方
このsampleを参考にしていただければ、問題なく使えるはずです。
spec fieldに、実行したいyamlの内容をprompt, cronjobの設定をschedule, open_ai_api_keyをopenaiapikeyを書きます。
spec:
prompt: |.....
schedule: "*/10 * * * *"
openaiapikey: "sk-*****"
このCRDを作成すると、対応するconfigmap, cronjobが作成されます。openAI APIによってpromptのoutputが変化していないか、定時観測することができます。
課題
あくまでただの趣味で作成したツールなので、課題はたくさんあります。例えば、以下のような課題があります。
- secretでkeyを渡せない
- promptfooのファイルをstring型で渡す必要がある
- pod containerの内容が洗練されていない
- cronjobの結果を管理する方法がない
また、そもそも論として「自分でcronjobを書けばいいのでは?CRD要らなくない?」「サービス全体の整合性をチェックしたいが、promptfooだと複雑なことができなくない?」という点も、課題として挙げられます。その通りだと思います。趣味開発なのでいいか、と開き直っていますが、次はもっと洗練されたOSSを作りたいですね。
終わり