#きょうのsystemd : ポータブルサービス

超久しぶり。 systemdが話題(?)ということで再開の機運が高まり、レナートのブログ見ていたら、また変なのが出ていたので、翻訳。

原文はこれ。

Walkthrough for Portable Services

systemd v239でポータブルサービス

systemd v239 には数多くの新機能がある。 そのうちの1つに、ポータブルサービスのファーストクラスのサポートがある。 このブログストーリーでは、ポータブルサービスとは何か、それがみんなのアプリケーションにとってなぜ興味深いものなのか、というところに光を当てたいと思う。

"ポータブルサービス"とは?

"ポータブルサービス" というコンセプトは、コンテナ管理に加え、古典的な chroot() 環境からインスピレーションを得たもので、より一般的なシステムサービスの管理にこれらの特徴のいくつかを持ち込むものだ。

"コンテナ" が何なのかという定義は非常にホットな話題だが、みんな、一般的に"コンテナ"のコンセプトには主要な2つの特徴があるということには同意するだろうということは言える。

  1. リソースバンドリング: コンテナは一般に自分自身のファイルシステムツリーを持っていて、共有ライブラリやその他のリソースといった、メインのサービスを実行するのに必要なものを束ねている。
  2. アイソレーションサンドボックス: コンテナはホストから比較的隔離された名前空間の環境で動いている。自身のファイルシステム名前空間で動いているのに加え、コンテナは自身のデータベースやプロセスツリーその他も持っている。コンテナからホストへのアクセスは様々なセキュリティ技術により制限されているものである。

この2つのコンセプトのうち、1つ目は伝統的な UNIX chroot() 環境についても言えることだ。

リソースバンドリングならびにアイソレーションサンドボックスの両方共、systemdが様々な度合いで、長い時間をかけて、実装してきたものである。 特に、RootDirectory=RootImage= は早い段階からあるし、systemdが提供する、様々な サンドボックス機能もある。 ポータブルサービスというコンセプトはこの上に成り立つものであり、これらの機能をまとめて、よりアクセスしやすくかつ使いやすくする、新しく、統合された方法にするものなのだ。

わかった、じゃあ、"ポータブルサービス"とは正確には何なんだ?

コンテナイメージとかなり似たような感じで、ディスク上のポータブルサービスはただのディレクトリツリーだ。 その中には、サービスの実行ファイルとその依存するものすべてが入っており、それらは通常のLinuxディレクトリ階層と似たような階層の中にある。 ポータブルサービスはrawディスクイメージとすることもできる。 そこには、ツリーを含む1つのファイルシステム(ループバックブロックデバイスを通じてマウント可能なもの)が入っていたり、複数のファイルシステム(この場合は、これらのファイルシステムDiscoverable Partitions Specificationに従い、GPTパーティションテーブル内に位置している必要がある)が入っていたりする。 ディスク上のポータブルサービスが単純なディレクトリであるか、rawディスクイメージであるかに関係なく、このコンセプトをポータブルサービスイメージと呼ぶことにしよう。

そのようなイメージはOSを何らかのディレクトリにインストールするために使われる古典的なツール、例えば、dnf --installroot=debootstrap で作ることができる。 これらのツリー上で必要となるものはほとんどないが、次の2つは必要になる。

  1. ツリーは関連したサービスの[systemd ユニットファイル]をサービスの中に含んでいる。
  2. ツリーは /usr/lib/os-release (ないしは /etc/os-release) のOSリリース情報を含んでいる。

もちろん、お気づきの通り、今日の主要なディストリビューションのいずれからも生成されるOSツリーは一般にこの2つの必要条件を特に変更しなくても含んでいる。 というのも、それらの多くは /usr/lib/os-release を受け入れているし、主要なサービスはsystemdユニットファイルとともに搭載されている。

このようなポータブルサービスイメージはホストに対し「アタッチ」や「デタッチ」することができる。

  1. イメージのホストへの「アタッチ」は新しい portablectl attach コマンドにより実行される。 このコマンドはイメージを分析する。つまり、os-release情報を読み込みその中にあるユニットファイルを探すのだ。 そうしたら、このコマンドは関連するユニットファイルイメージからコピーし、/etc/systemd/systemにコピーする。 その後、このコマンドはコピーしたサービスのユニットファイルを2つの方法で強化する。RootDirectory= ないしは RootImage= 行を追加するdrop-inが追加され、開始時にユニットファイルがホスト上でも利用できるようになる一方、ユニットファイルはイメージ内の参照されているバイナリーを実行するようになるのだ。 また、このコマンドでは、2つ目のdrop-inにシンボリックリンクを張る。これは「プロファイル」と呼ばれるもので、追加のセキュリティ設定をアタッチされたサービスに強制するようなmのので、つまり、ちょうどサンドボックスにあたるものを確保するものだ。

  2. ホストからのイメージの「デタッチ」は portable detach で実現される。 これは上のステップを逆さにしたものだ。 つまり、コピーされたユニットファイルは削除され、そのために生成された2つのdrop-inファイルも削除される。

ポータブルサービスがアタッチされると、関連するユニットファイルはホスト上で他のユニットファイルと同じように利用できるようになる。 つまり、それらのユニットファイルはsystemctl list-unit-filesで表示されるようになるし、有効にも無効にもでき、起動も停止もできる。 systemctl editで拡張もできる。 中も見れる。 他のサービスと同じようにリソース管理を適用できるし、他のサービスのようにログを処理すること、その他もできる。 それは、なぜなら、ポータブルサービスはネイティブなsystemdサービスである からである。ただし、望むように「ねじれている」という点を除いては。 つまり、ポータブルサービスはデフォルトでセキュリティが確保されており、ルートディレクトリないしはイメージの中に自身のリソースを持っている。

これはすでにポータブルサービスが何たるもののかのエッセンスである。

興味深いポイントは次のとおりだ:

  1. ポータブルサービスイメージの中にserviceユニットファイルを入れることが主眼ではあるが、timerユニットやsocketユニット、targetユニット、pathユニットもポータブルサービスに入れることができる。 これにより、非常に自然な形で、時間・ソケット・パスベースのアクティベーションが可能に鳴る。 より複雑なアプリケーションの場合は、複数のserviceユニットを同じイメージに入れることもできてしまう。

  2. このコンセプトは新しいメタデータを導入しない。 ユニットファイルは既存のコンセプトだし、os-releaseファイルもそうだ。--rawディスクイメージがお好きなら--GPTパーティションテーブルもすでに確立されたものだ。 これの意味するところは、イメージを作るための既存のツールがポータブルサービスを作るのに再利用でき、かなりの程度、完全に新しいタイプのアーティファクトが生み出されることないということだ。

  3. ポータブルサービスのコンセプトが新しいメタデータを導入せず、ただ既存のsystemdのセキュリティないしはリソースバンドリング機能の上に成立するため、これは個別のツールのセットで実装されており、比較的systemdのその他の部分と切り離されている点も挙げられる。 特に、主たるユーザーフェイシングなコマンドは portablectl であり、実際の操作は systemd-portabled.service で実装されている。 望むなら、ポータブルサービスはsystemdの真のアドオンであり、systemdその他が提供する基本的な操作よりも、使って便利な特定のワークフローとすることができる。 また、 systemd-portabled はバスAPIを提供し、それと接続したいいかなるプログラムからアクセス可能であり、portablectlはsystemdと一緒にたまたまついてくる1つのツールに過ぎないということにも注意してほしい。

  4. ポータブルサービスは最近追加したばかりの機能なので、まだまだ変化を加える余地を残したいと考えた。 そのため、 portablectl コマンドを /usr/lib/systemd に当面はインストールすることに決めた。 したがって、$PATH のデフォルトには現れてこない。 これの意味するところは、当面の間、フルパスで実行する必要があるということだ: /usr/lib/systemd/portablectl 早いうちに /usr/bin に移したいとは思っており、そのためにも、systemdの完全にサポートされたインターフェイスとしたいと考えている。

  5. ポータブルサービスイメージに含まれるユニットファイルのうち、どれが「関連する」とみなされ、 portablectl attach 操作で実際にコピーされるのだろうと思うかもしれない。 現時点では、これはイメージの名前に由来するようになっている。 仮に、 /var/lib/portables/foobar_4711/ というディレクトリ(ないしは代わりにrawイメージ /var/lib/portables/foobar_4711.raw) にイメージが保管されているとしよう。 その場合、コピーされるユニットファイルは次のパターンにマッチするものだ: foobar*.service, foobar*.socket, foobar*.target, foobar*.path, foobar*.timer

  6. ポータブルサービスの概念ではイメージをどのようにデプロイメントマシンに載せるかの方法は定義しておらず、それは完全に管理者の考えることだ。 scp で置くもよし、wgetするもよし。 RPMとしてパッケージするのもいいし、行けると思うなら dnf でデプロイするのもよしだ。

  7. ポータブルサービスは好きなディレクトリに置くことができる。しかし、 /var/lib/portables に置くと、 portablectl はそれらを簡単に見つけ、アタッチその他ができるイメージのリストを見せることができる。

  8. ポータブルサービスのアタッチは永続化できる。これにより、以後のブートでもアタッチしたままにすることができる(これはデフォルトだ)し、あるいは、--runtimeportablectl に渡すことで、次回のブートまでアタッチすることもできる。

  9. ポータブルサービスは完全にただの通常のOSイメージのため、自然で簡単に異なる3つの方法で利用できる1つのイメージを作ることができる。

    1. ポータブルサービスとしてどのホストにもアタッチできる
    2. OSコンテナとしてブートすることができる。例えば、[systemd-nspawn]https://www.freedesktop.org/software/systemd/man/systemd-nspawn.html) のようなコンテナマネージャーで。
    3. ホストシステムとしてブートすることも可能である。例えば、ベアメタルや仮想マシンマネージャーで。 もちろん、後者2つを実現するためには、イメージにはサービスのバイナリ、os-releaseファイルならびにユニットファイル以上のものを含んでいる必要がある。 systemd-nspawn のようなOSコンテナマネージャーでブート可能にするためには、イメージには例えばsystemdといった何らかの形のinitシステムが入っている必要がある。 ベアメタル上や仮想マシンとしてブート可能にするためには、例えば、systemd-bootといった、何らかのブートローダーも必要である。

プロファイル

前のセクションで「プロファイル」というコンセプトについては簡単に触れた。 プロファイルはポータブルサービスのコンセプトの主要な機能であるから、ある程度はフォーカスを当てる必要がある。 「プロファイル」は究極的にはただの pre-definedのdrop-inファイルで、ホストにアタッチされたユニットファイルのためのものである。 プロファイルはほとんど、サンドボックスとセキュリティの設定を含んでいると考えてよいが、実際にはその他の設定も含ませることができる。 ポータビルサービスがアタッチされると適切なプロファイルが選択される。 明示的にどのプロファイルも選択されない時、default と呼ばれるデフォルトのプロファイルが使われる。 systemdは4つの異なるプロファイルをデフォルトで提供する:

  1. default プロファイルは中間レベルのセキュリティを提供する。 これには、互換性のドロップ、システムコールフィルターの強制、制限された多くのカーネルインターフェイス、および、様々なファイルシステムのリードオンリーのマウントの設定が含まれる。

  2. strict プロファイルは default プロファイルと似ているが、全般的により制限的なサンドボックス設定を使用する。 例えば、ネットワーキングはオフで、 AF_NETLINK ソケットへのアクセスは禁止されている。

  3. trusted プロファイルはすべての中でもっとも制限がゆるい。 事実、ほとんどまったく制限をしない。 このプロファイルで起動しているサービスは基本的にホストシステムへのフルアクセスができる。

  4. nonetwork プロファイルはほとんど default と同じだが、加えてネットワークアクセスもオフになっている。

プロファイルはポータブルサービスイメージがアタッチされた時点で選択され、同じイメージに複数のサービスファイルが入っている場合、アタッチされるすべてのサービスファイルにプロファイルが適用される点に注意してほしい。 ゆえに、矯正されるサンドボックスの制限はイメージをアタッチする管理者により選択されるものであり、イメージの提供者が選択するものではない。

追加のプロファイルは管理者が簡単に定義することができる。 必要であれば、我々がsystemdと一緒に追加のプロファイルを早かれ遅かれ提供するかもしれない。

ユースケースは何?コンテナを持っていたら、なぜ困るの?

ポータブルサービスは主に、コードがホストシステムに対する「拡張機能」のようであるかのように感じられるユースケースをカバーすることを意図している。 接続がない、隔離された世界に住むということというよりかはそちらだ。 プロファイルというコンセプトはアプリケーションに執って必要とされる、統合と隔離をちょうどいいようにするためにチューニングできるようにと考えられたものだ。

コンテナの世界では、"super-privileged containers" というコンセプトが盛大に褒めちぎられている。つまり、完全な特権をもって起動するコンテナである。 ポータブルサービスが意図しているユースケースは正確にはこれである。 つまり、ホストOSの拡張機能であり、デフォルトでは隔離されているが、オプションで必要なだけのホストへのアクセスを得ることができ、本質的にはホストの完全な機能の恩恵を受けることができる。 したがって、このコンセプトは、OS自身とは一緒には提供されないが、様々な程度で、OSとの統合が必要とされるすべての種類の低レベルのシステムソフトウェアのユースケース向けである。 サーバーとアプライアンスに加え、これはIoTや組み込み系デバイスにとっても特に面白いことになるはずだ。

ポータブルサービスは、システムサービスがその他の方法でマネージされる方法への比較的小さな拡張機能に過ぎないので、ほぼすべてのユースケースで通常のサービスのように取り扱うことができる。 つまり、すべてのツールという観点では、systemdユニットデータを見ることができ、ロギング、リソース管理、ランタイムライフサイクルその他については同じ方法で管理することができ、通常のサービスの延長線上にある。

ポータブルサービスは非常に汎用的なコンセプトだ。 オリジナルのユースケースとしてはOSの拡張機能だが、もちろん、どう使うかはユーザ次第だ。

ウォークスルー

じゃあ、これがどう動くかを見ることに仕様。 ポータブルサービスイメージをホストにアタッチして、有効にして、起動する前に、スクラッチで作るところから始める。

ポータブルサービスイメージをビルドする

前述の通り、ポータブルサービスをビルドするためにOSツリーやrawイメージを作るのに、好きなツールを使うことができる。 例えば、 debootstrapdnf --installroot= などだ。 このウォークスルーでは、 mkosi を使うことにする。 これは、結局は dnfdebootstrap のファンシーなラッパーだが、ソースツリーから繰り返しイメージをビルドするときにはかなりのことを簡単にできる代物だ。

私はこのウォークスルーをローカルで再現するために必要なものすべてを GitHub レポジトリ にプッシュしておいた。 チェックアウトしよう。

$ git clone https://github.com/systemd/portable-walkthrough.git

レポジトリの中を見ておこう。

  1. まずはwalkthroughd.c は我々の小さなサービスのメインのソースファイルだ。 事を簡単にするために、Cで書いたが、言語は好きなものを使えばいい。 実装されたデーモンは多くのことをしない。 ただ起動して、シャットダウンのポイントである SIGTERMを待つ。 デーモンは結局は用途がないのだが、これがすべてどうフィットするかを希望をもって描く。 Cコードはlibc以外の依存関係を持たない。

  2. walkthrough.service はsystemdユニットファイルで、小さなデーモンをスタートさせるものだ。 シンプルなサービスのため、ユニットファイルは些細なものだ。

  3. Makefile はデーモンバイナリーをビルドするための、短い make のビルドスクリプトだ。 これも些細なもので、ただCファイルを取り上げ、そこからバイナリーをビルドするだけだ。 Makefileはデーモンのインストールもできる。 Makefileはバイナリーを /usr/local/lib/walkthroughd/walkthroughd に配置(なぜ、 /usr/local/bin じゃないかって?それは、このバイナリーはユーザーフェイシングのバイナリーではなく、システムサービスのバイナリーだからだ)し、ユニットファイルを/usr/local/lib/systemd/walkthroughd.service に配置する。 デーモンをホストでテストしたければ、単純に make を実行して、./walkthroughd をするだけで、ぜんぶちゃんと動いているかを確かめることができる。

  4. mkosi.defaultmkosi にどのようにイメージをビルドするかを伝える。 ここでは、我々は Fedora ベースのイメージを選ぶことにする(が、Debianを使うかもしれないし、その他かのサポートされたディストロを使うかもしれない)。 実行時に特定のパッケージは必要としない(なんといっても、libcにだけ依存している)が、ビルドフェ―ズには gcc と make が必要なので、これらばかりは BuildPackages= でリストしているパッケージだ。

  5. mkosi.buildシェルスクリプトで mkosi のビルドロジック中に実行される。 これがやることは、 makemake install を我々の小さなデーモンをビルドしてインストールするために実行し、その後、ディストリビューション提供の /etc/os-release ファイルを我々のサービスについてちょっと記述する追加のフィールドを着けて拡張する。

では、これを使ってポータブルサービスイメージをビルドしよう。 そのために、 mkosi ツールを使おう。 最初のイメージをビルドするにはパラメーター梨で実行するだけで十分だ。 mkosi は自動的に mkosi.defaultmkosi.build を見つけてこれらが伝える通りに処理をおこなう。 (このようなプロジェクトを長期に渡って実行する場合、 mkosi -if がおそらく使う上でベターなコマンドだろう。というのも、これは、インクリメンタルなビルドモードで、ビルドを実質的にスピードアップさせるものだからだ。) mkosi は必要なRPMをダウンロードして、全部一緒にする。 mkosi は我々の小さなデーモンをイメージの中でビルドして、すべて終わると、成果物のイメージを出力する。 walkthroughd_1.raw だ。

我々はGPT rawディスクイメージを mkosi.default で選択肢ているため、このファイルは実際にGPTパーティションテーブルを含む、rawディスクイメージだ。 fdisk -l walkthrough_1.raw を使えば、パーティションテーブルを列挙することができる。 また、必要なら systemd-nspawn -i walkthroughd_1.raw を使えば、イメージを素早く探検することができる。

ポータブルサービスイメージを使う

ポータブルサービスイメージは手にした。では、アタッチして、中には言っているサービスを有効にして起動してみよう。

まずはアタッチする。

# /usr/lib/systemd/portablectl attach ./walkthroughd_1.raw
(Matching unit files with prefix 'walkthroughd'.)
Created directory /etc/systemd/system/walkthroughd.service.d.
Written /etc/systemd/system/walkthroughd.service.d/20-portable.conf.
Created symlink /etc/systemd/system/walkthroughd.service.d/10-profile.conf → /usr/lib/systemd/portable/profile/default/service.conf.
Copied /etc/systemd/system/walkthroughd.service.
Created symlink /etc/portables/walkthroughd_1.raw → /home/lennart/projects/portable-walkthrough/walkthroughd_1.raw.

このコマンドは何をやったかを正確に表示してくれるだろう。 期待したとおり、このコマンドは、メインのサービスファイルを取り出してコピーし、2つのdrop-inを追加している。

では、約束したとおり、通常のユニットと同じように、ユニットがホストで利用可能になっているかを確かめよう。

# systemctl status walkthroughd.service
● walkthroughd.service - A simple example service
   Loaded: loaded (/etc/systemd/system/walkthroughd.service; disabled; vendor preset: disabled)
  Drop-In: /etc/systemd/system/walkthroughd.service.d
           └─10-profile.conf, 20-portable.conf
   Active: inactive (dead)

いいね、動いている。 ユニットファイルが利用可能で、systemdが正しく2つのdrop-inを見つけていることがわかる。 しかし、ユニットは有効なっていなければ、起動もしていない。 そう、ポータブルサービスイメージをアタッチするだけでは黙示的に有効にならなければ起動もしないのだ。 アタッチが意味するのは、イメージに入っているユニットファイルがホストで利用可能になったということだけなのだ。 それらを有効にする(つまり、例えば起動時など、必要なタイミングで自動的に起動する)ようにするのも、起動する(この場合では今すぐ起動する)のも、管理者次第だ。

では、サービスの有効化と起動をワンステップでやってしまおう。

# systemctl enable --now walkthroughd.service
Created symlink /etc/systemd/system/multi-user.target.wants/walkthroughd.service → /etc/systemd/system/walkthroughd.service.

動いていることを確認しよう。

# systemctl status walkthroughd.service
● walkthroughd.service - A simple example service
   Loaded: loaded (/etc/systemd/system/walkthroughd.service; enabled; vendor preset: disabled)
  Drop-In: /etc/systemd/system/walkthroughd.service.d
           └─10-profile.conf, 20-portable.conf
   Active: active (running) since Wed 2018-06-27 17:55:30 CEST; 4s ago
 Main PID: 45003 (walkthroughd)
    Tasks: 1 (limit: 4915)
   Memory: 4.3M
   CGroup: /system.slice/walkthroughd.service
           └─45003 /usr/local/lib/walkthroughd/walkthroughd

Jun 27 17:55:30 sigma walkthroughd[45003]: Initializing.

パーフェクト! サービスが有効化され、起動している。デーモンは PID 45003で動いている。

全部ちゃんと確かめたので、今度はサービスを停止し、無効化、デタッチをやってみよう。

# systemctl disable --now walkthroughd.service
Removed /etc/systemd/system/multi-user.target.wants/walkthroughd.service.
# /usr/lib/systemd/portablectl detach ./walkthroughd_1.raw
Removed /etc/systemd/system/walkthroughd.service.
Removed /etc/systemd/system/walkthroughd.service.d/10-profile.conf.
Removed /etc/systemd/system/walkthroughd.service.d/20-portable.conf.
Removed /etc/systemd/system/walkthroughd.service.d.
Removed /etc/portables/walkthroughd_1.raw.

最後に、ほんとに消えたかを見てみよう。

# systemctl status walkthroughd
Unit walkthroughd.service could not be found.

パーフェクト!うまく動いた!

上の通りやって、ポータブルサービスのスタートが切れたことを願うよ。 もっと疑問があれば、メーリングリスト にコンタクトしてほしい。

もっと知りたい人のために

詳細について説明している、より低レベルなドキュメントは systemdに同梱されている

関連するmanual ページもある: portablectl(1)systemd-portabled(8) だ。

mkosi についての詳細はそのホームページ をご覧いただきたい。