Ubuntu Weekly Recipe

第718回needrestartで学ぶパッケージのフック処理

Ubuntu 21.04から、サーバー版に「needrestart」というパッケージが最初からインストールされるようになりました。これはパッケージの更新時に再起動が必要なデーモンを通知してくれる仕組みです。便利ではあるものの、パッケージの更新を開始して放置したら、最後のほうでメニューが出て止まっていたということも起こりえます。そこで今回はneedrestartの設定方法と、どういう仕組みで動いているのかを解説しましょう。

needrestartの基本

glibcなど特定のライブラリパッケージに脆弱性が見つかり、対応が行われたとき、そのパッケージの更新だけではまだ対応が完了したとは言えません。実際にはそのライブラリを利用しているプロセスが一通り再起動し、ライブラリを再読込することでようやく脆弱性が解消されるのです。

しかしながらどのパッケージが更新され、そのパッケージに属するライブラリファイルは何で、そのライブラリファイルを使っているサービスはどれかを知るには一筋縄では行きません。そこで登場したのがneedrestartです[1]⁠。

needrestart自体はPerl製のシンプルなスクリプトです。簡単に仕組みを説明すると、実行中のプロセスについて/proc/PID/mapsを精査し、⁠deleted」フラグが含まれているサービスを再起動するというものです。これによりパッケージの更新によってライブラリが差し替わったとしても、必要なサービスのみを再起動して影響範囲を少なくしながら安全な状態を維持することを目指します。

ちなみにneedrestart自体は単にライブラリだけでなく、次のようなデータの更新に対応しています。

  • カーネル本体の更新
  • Intel/AMDのCPUのマイクロコードの更新
  • Java/Perl/Python/Rubyで利用しているライブラリの更新
  • Dockerコンテナ内部のプロセスは無視
  • LXC/LXDコンテナ内のプロセスの場合はインスタンスの再起動を通知
  • QEMU更新時に再起動が必要なVMのリストを通知

その結果、Ubuntu 21.04のサーバーからパッケージの更新時に次のようなメッセージが表示されるようになりました。

図1 パッケージを更新すると最後のほうで表示されるサービスの再起動通知
図1

unattended-upgradeのようにバックグラウンドで自動更新するケースについては、サービスの再起動通知は行いません。手動で確認したい場合は、sudo needrestartを実行すると良いでしょう。また-pオプションを付けることで、Nagiosのプラグインモードとしても動かせます。

ちなみにneedrestartが最初からインストールされるようになったのは、サーバー版とCloud Image版だけです。デスクトップ版はこれまでどおり、パッケージの更新後も特に何もしない状態となります。

特にサーバー版/Cloud Image版の場合、LTSのみ運用している人も多いことでしょう。よって、先日の22.04を導入以降にはじめて、システムを更新するとこのようなメッセージに遭遇したかもしれません。apt upgradeを実施してあとは放置するような運用の場合は、気づいたら変なところで止まっていたとなるとやっかいです。というわけで、基本的な設定方法からまず説明していきましょう。

needrestartの設定は/etc/needrestart/needrestart.confに集約されています。このファイルを直接変更してもいいのですが、/etc/needrestart/conf.d/ディレクトリにfoo.confのようなファイルを置いておけば、設定を上書き可能になります。設定ファイルの管理や将来的なneedrestartパッケージの更新を考えるとconf.d/以下のみを変更するようにしたほうが良いでしょう。ファイル名でソートされた上で読み込まれますので、00-foo.confのようにファイル名の先頭に数字を付けて、読み込み順をコントロールできるようにしておくと便利です。

再起動モードの変更

まずはパッケージ更新時の再起動通知について設定してみましょう。これはneedrestart.confに次のように説明されています。

# Restart mode: (l)ist only, (i)nteractive or (a)utomatically.
#
# ATTENTION: If needrestart is configured to run in interactive mode but is run
# non-interactive (i.e. unattended-upgrades) it will fallback to list only mode.
#
#$nrconf{restart} = 'i';

選択肢は次の3種類です。

  • 再起動が必要なサービスを表示するだけ(l)
  • サービスごとに再起動が必要かどうかを通知する(i)
  • 必要なサービスはすべて自動的に再起動する(a)

未設定時は「i」が設定されています。キュリティ的に安全側に倒すなら「a」ですが、サービスのライフサイクルを手動でコントロールしたい(21.04より前に近い状態にしたい)のであれば「l」を指定してください。

たとえば次のように実行すれば、/etc/needrestart/conf.d/ディレクトリに「a」を設定するファイルを作成できます。

$ echo "\$nrconf{restart} = 'a';" | sudo tee /etc/needrestart/conf.d/50-autorestart.conf

なお、⁠a」を指定した場合は、LXC/LXDインスタンスも自動的に再起動される可能性がある点に注意が必要です。ただしUbuntu 22.04 LTSリリース時点でのneedrestartは、LXC/LXDインスタンスは常に再起動しないという不具合があります。なおDockerインスタンスについては、たとえライブラリの更新を検知しても再起動の必要性の通知は行いません。単純に無視します。

常に再起動を抑止する

needrestartは、⁠i」を指定したインタラクティブモードでパッケージが更新されると、⁠再起動が必要なサービスに最初からチェックが入った状態」でユーザーに問い合わせを行います。つまりその状態で思わずエンターキーを押してしまうと、必要なものがすべて再起動されてしまいます。それを抑止するのがdefnoオプションです。

# Change default answer to 'no' in (i)nteractive mode.
#$nrconf{defno} = 1;

上記をコメントアウトしたり、再起動モードのように別ファイルにおいて、$nrconf{defno}を1にすることで、再起動が必要なサービスも常にチェックが外れた状態で問い合わせを行いますので、間違ってエンターを押しても安心です。

ただしこのままだと「どれを再起動すべきか」がわからなくなる弊害があります。よって手動でneedrestartを実行するときは、UIをDebconfからテキストモードに変更してしまいましょう。

$ sudo needrestart -u NeedRestart::UI::stdio -n
(中略)
Restarting services...
Services to be restarted:
Restart «ModemManager.service»? [yNas?]
Restart «dbus.service»? [yNas?]
Restart «lxd-agent.service»? [yNas?]

このように必要なものだけ、ひとつひとつ問い合わせがされますので、適宜エンター(再起動しない)「y」⁠再起動する)かを切り替えて行けば良いでしょう。なおUIの変更は、needrestart.confでも変更できます。

# Use preferred UI package.
#$nrconf{ui} = 'NeedRestart::UI::stdio';

これをコメントアウトするだけです。指定できるUIはneedrestart -u ?で確認できます。

特定のサービスの抑制

特定のサービスだけneedrestartのチェック対象から外すことも可能です。たとえば、unattended-upgradeサービスは、その中からパッケージの更新を行い、さらにneedrestartを呼び出す都合で更新対象から除外しています。

# Blacklist services (list of regex) - USE WITH CARE.
# You should prefer to put services to $nrconf{override_rc} instead.
# Any service listed in $nrconf{blacklist_rc} will be ignored completely!
#$nrconf{blacklist_rc} = [
#];

# Override service default selection (hash of regex).
$nrconf{override_rc} = {
    (中略)

    # do not restart oneshot services, see also #862840
    qr(^apt-daily\.service$) => 0,
    qr(^apt-daily-upgrade\.service$) => 0,
    qr(^unattended-upgrades\.service$) => 0,
    (中略)
};

# Override container default selection (hash of regex).
$nrconf{override_cont} = {
};

$nrconf{blacklist_rc}で指定したサービスは、完全に再起動リストから削除されます。それに対して$nrconf{override_rc}で指定したサービスは、再起動リストの初期値を強制的に変更できます。後者の場合、UIから別の画面に変更できますので、絶対に再起動させたくないものだけ前者を使うと良いでしょう。$nrconf{override_cont}はLXC/LXDのインスタンス名でのマッチングです。こちらはハッシュのキーのみ利用し、値は使用されません。

ようはこれらのハッシュに、qr(サービス名にマッチする正規表現) = 0,を追加してあげれば、そのサービスは再起動させない形に変更できるというわけです。他にもログイン画面を司るディスプレイマネージャーやネットワーク関連も後者のブラックリストに入っています。

もし/etc/needrestart/conf.d/以下で変更する場合は、次のように指定すると良いでしょう。

$nrconf{override_rc}{ qr(正規表現) } = 0;

特定のライブラリを指定したブラックリスト

サービス名ではなくライブラリのファイル名でブラックリスト化することも可能です。こちらは「このファイルが更新されても、サービスの更新対象とはしない」という考え方になります。

# Blacklist binaries (list of regex).
$nrconf{blacklist} = [
    # ignore sudo (not a daemon)
    qr(^/usr/bin/sudo(\.dpkg-new)?$),

    # ignore DHCP clients
    qr(^/sbin/(dhclient|dhcpcd5|pump|udhcpc)(\.dpkg-new)?$),

    # ignore apt-get (Debian Bug#784237)
    qr(^/usr/bin/apt-get(\.dpkg-new)?$),
];

もし/etc/needrestart/conf.d/以下で変更する場合は、次のように指定すると良いでしょう。

$nrconf{blacklist}{ qr(正規表現) } = 0;

パッケージのフック処理

needrestartはパッケージ更新時のフック処理によって呼び出されています。フックのタイミングは次の2パターンです。

  • /etc/dpkg/dpkg.cfg.d/needrestart
  • /etc/apt/apt.conf.d/99needrestart

前者はapt内部で呼ばれるdpkgコマンドにおいて、パッケージのステータスが変更された時に呼び出されます。渡される引数は主に次の4パターンです。

status: パッケージ名: ステータス
status: パッケージ名 : error : エラーメッセージ
status: ファイル名 : conffile-prompt : 選択肢
processing: ステージ: パッケージ名

needrestartで必要なのは「ステータスがunpackedになったかどうか(何がしかのパッケージがインストールされたか⁠⁠」と「エラーが起きたかどうか」の2種類です。

前者の/etc/dpkg/dpkg.cfg.d/needrestartでは、aptコマンドの対象となるパッケージを処理するごとに、/usr/lib/needrestart/dpkg-statusを呼び出し、状態に応じて/run/needrestartunpackedファイルやerrorファイルを作成します。

後者の/etc/apt/apt.conf.d/99needrestartでは、DPkg::Post-Invokeから/usr/lib/needrestart/apt-pinvokeを呼び出しています。DPkg::Post-Invokeはdpkgの処理が完了したときに呼ばれるスクリプトを指定する設定です。apt-pinvokeでは、unkackedがあるけれどもerrorがない、つまり何らかのパッケージはインストールされておりエラーも発生しなかった場合に、needrestartコマンドを実行しています。

この仕組みはパッケージに絡む処理ならなんでも流用可能です。よって上記の仕組みを参考にすれば、パッケージの更新時の成功・失敗の通知や、追加・更新・削除されたパッケージのログが自由に取れます。また、特定のパッケージが処理されたときのみWebhookを叩く、といった対応も可能になるでしょう。

おすすめ記事

記事・ニュース一覧