unattended-upgradeでUbuntuのパッケージを自動更新する

## サマリー

Debian系のディストリビューションに搭載されている [UnattendedUpgrades](https://wiki.debian.org/UnattendedUpgrades) を使用して、Ubuntuのパッケージを定期的に自動更新してみます。

## unattended-upgradesとは

[UnattendedUpgrades](https://wiki.debian.org/UnattendedUpgrades) は [unattended-upgrades](https://packages.debian.org/search?keywords=unattended-upgrades) というパッケージで提供される、自動更新用のプログラムです。  
大抵のDebianディストリビューションにはデフォルトでインストールされています。  
デスクトップ環境を利用している人であれば、ソフトウェアセンターで無意識に設定しているかもしれません。

初期設定のままであればごく一部のパッケージを自動更新する程度なのですが、追加したリポジトリを対象にしたり必要に応じて再起動したり出来るので、全自動運転にはもってこいです。  
また、この自動更新によって特定のバージョンでしか動かないアプリケーションやドライバーが急に動かなくなることもあるので、活用はしなくとも存在を認識しておく必要はあるでしょう。

定期的にパッケージ一覧を更新している `apt-daily.timer` やunattended-upgradeコマンドを呼び出す `apt-daily-upgrade.timer` といったスケジューラは `apt` パッケージのものでunattended-upgradesパッケージには含まれていませんが、関連するため合わせて見ていく必要があります。

* https://packages.debian.org/search?suite=bullseye&section=all&arch=any&searchon=contents&keywords=apt-daily.timer
* https://packages.debian.org/search?suite=bullseye&section=all&arch=any&searchon=contents&keywords=apt-daily-upgrade.timer

以降は、Ubuntu 20.04を対象に設定を確認していきます。

## unattended-upgradesの初期設定確認

注目すべきファイルは以下の3つです。

* `/etc/apt/apt.conf.d/10periodic`
* `/etc/apt/apt.conf.d/20auto-upgrades`
* `/etc/apt/apt.conf.d/50unattended-upgrades`

おそらく、特に手を加えていなければ以下のような設定になっていると思います。

```
$ apt-config dump | grep Periodic
APT::Periodic "";
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "0";
APT::Periodic::AutocleanInterval "0";
APT::Periodic::Unattended-Upgrade "1";
```

重複した設定が含まれていますが、以下の設定ファイルに書かれている通りです。

```
$ cat /etc/apt/apt.conf.d/10periodic 
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "0";
APT::Periodic::AutocleanInterval "0";
$ cat /etc/apt/apt.conf.d/20auto-upgrades 
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
```

更に、unattended-upgradesの初期設定を見てみます。  
最初から `APT::Periodic::Unattended-Upgrade "1";` が設定されており、unattended-upgradesが有効になっていることが確認できます。

```
$ apt-config dump | grep Unatte
APT::Periodic::Unattended-Upgrade "1";
Unattended-Upgrade "";
Unattended-Upgrade::Allowed-Origins "";
Unattended-Upgrade::Allowed-Origins:: "${distro_id}:${distro_codename}";
Unattended-Upgrade::Allowed-Origins:: "${distro_id}:${distro_codename}-security";
Unattended-Upgrade::Allowed-Origins:: "${distro_id}ESMApps:${distro_codename}-apps-security";
Unattended-Upgrade::Allowed-Origins:: "${distro_id}ESM:${distro_codename}-infra-security";
Unattended-Upgrade::DevRelease "auto";
```

上記の設定は、以下のファイルに書かれている通りです。

```
$ grep -v -e "//" -e "^$" /etc/apt/apt.conf.d/50unattended-upgrades 
Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}";
        "${distro_id}:${distro_codename}-security";
        "${distro_id}ESMApps:${distro_codename}-apps-security";
        "${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Package-Blacklist {
};
Unattended-Upgrade::DevRelease "auto";
```

また、定期的な更新がいつ行われているのかは `apt-daily` 関係のtimerを見ることで確認できます。

```
$ systemctl list-timers apt-daily*
NEXT                        LEFT     LAST                        PASSED    UNIT                    ACTIVATES                
Wed 2023-06-14 06:06:08 JST 11h left Tue 2023-06-13 18:08:51 JST 21min ago apt-daily-upgrade.timer apt-daily-upgrade.service
Wed 2023-06-14 06:44:10 JST 12h left Tue 2023-06-13 18:08:51 JST 21min ago apt-daily.timer         apt-daily.service        

2 timers listed.
Pass --all to see loaded but inactive timers, too.
```

`apt-daily.timer` はパッケージ一覧を更新する `apt-daily.service` の実行を、
`apt-daily-upgrade.timer` はunattended-upgradeコマンドを実行してパッケージを更新する `apt-daily-upgrade.service` の実行を、それぞれスケジュールしています。  
（呼び出しているスクリプトはどちらも `/usr/lib/apt/apt.systemd.daily` です）

`apt-daily-upgrade.timer` の設定値を見ると、大体1日1回（6時～7時）実行するように設定されていました。

```ini
$ cat /lib/systemd/system/apt-daily-upgrade.timer
[Unit]
Description=Daily apt upgrade and clean activities
After=apt-daily.timer

[Timer]
OnCalendar=*-*-* 6:00
RandomizedDelaySec=60m
Persistent=true

[Install]
WantedBy=timers.target
```

ここまでが、Ubuntu 20.04の初期設定のようです。

## unattended-upgradesの設定（標準パッケージ）

unattended-upgradesでディストリビューションに付属する標準パッケージを自動アップデートの対象とする場合は `50unattended-upgrades` のコメントアウトを外すだけで実現できます。  

```diff
--- /tmp/50unattended-upgrades  2023-06-12 17:13:22.422196783 +0900
+++ 50unattended-upgrades       2023-06-12 17:40:37.276850113 +0900
@@ -12,9 +12,9 @@
        // should also install from here by default.
        "${distro_id}ESMApps:${distro_codename}-apps-security";
        "${distro_id}ESM:${distro_codename}-infra-security";
-//     "${distro_id}:${distro_codename}-updates";
-//     "${distro_id}:${distro_codename}-proposed";
-//     "${distro_id}:${distro_codename}-backports";
+       "${distro_id}:${distro_codename}-updates";
+       "${distro_id}:${distro_codename}-proposed";
+       "${distro_id}:${distro_codename}-backports";
 };
 
 // Python regular expressions, matching packages to exclude from upgrading
```

とはいえ、OSにデフォルトでついてくる設定ファイルを直接変更すると、アップデート時に何かと上書きされる可能性もありますので、別のファイルに出力しておきます。

```
$ cat <<'_EOL_' | sudo tee /etc/apt/apt.conf.d/51unattended-upgrades 
Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}-updates";
        "${distro_id}:${distro_codename}-proposed";
        "${distro_id}:${distro_codename}-backports";
};
_EOL_
```

Ubuntu 20.04の場合は `${distro_id}` には `Ubuntu` 、 `${distro_codename}` には `focal` が入ります。  
`sudo unattended-upgrade --dry-run` コマンドを実行することで、アップデート対象となるパッケージが確認できます。（以下出力例）

```
$ sudo unattended-upgrade --dry-run -v
Starting unattended upgrades script
Allowed origins are: o=Ubuntu,a=focal, o=Ubuntu,a=focal-security, o=UbuntuESMApps,a=focal-apps-security, o=UbuntuESM,a=focal-infra-security, o=Ubuntu,a=focal-updates, o=Ubuntu,a=focal-proposed, o=Ubuntu,a=focal-backports
Initial blacklist: 
Initial whitelist (not strict): 
Option --dry-run given, *not* performing real actions
Packages that will be upgraded: apport grub-common grub-pc grub-pc-bin grub2-common iptables libip4tc2 libip6tc2 libxtables12 mokutil python3-apport python3-problem-report tzdata
Writing dpkg log to /var/log/unattended-upgrades/unattended-upgrades-dpkg.log
/usr/bin/dpkg --status-fd 10 --no-triggers --unpack --auto-deconfigure /var/cache/apt/archives/iptables_1.8.4-3ubuntu2.1_amd64.deb /var/cache/apt/archives/libxtables12_1.8.4-3ubuntu2.1_amd64.deb /var/cache/apt/archives/libip6tc2_1.8.4-3ubuntu2.1_amd64.deb /var/cache/apt/archives/libip4tc2_1.8.4-3ubuntu2.1_amd64.deb 
/usr/bin/dpkg --status-fd 10 --configure --pending 
Preconfiguring packages ...
Preconfiguring packages ...
/usr/bin/dpkg --status-fd 10 --no-triggers --unpack --auto-deconfigure /var/cache/apt/archives/tzdata_2023c-0ubuntu0.20.04.2_all.deb 
/usr/bin/dpkg --status-fd 10 --configure --pending 
/usr/bin/dpkg --status-fd 10 --no-triggers --unpack --auto-deconfigure /var/cache/apt/archives/python3-problem-report_2.20.11-0ubuntu27.27_all.deb 
/usr/bin/dpkg --status-fd 10 --configure --pending 
/usr/bin/dpkg --status-fd 10 --no-triggers --unpack --auto-deconfigure /var/cache/apt/archives/python3-apport_2.20.11-0ubuntu27.27_all.deb /var/cache/apt/archives/apport_2.20.11-0ubuntu27.27_all.deb 
/usr/bin/dpkg --status-fd 10 --configure --pending 
/usr/bin/dpkg --status-fd 10 --no-triggers --unpack --auto-deconfigure /var/cache/apt/archives/mokutil_0.6.0-2~20.04.1_amd64.deb 
/usr/bin/dpkg --status-fd 10 --configure --pending 
Preconfiguring packages ...
Preconfiguring packages ...
/usr/bin/dpkg --status-fd 10 --no-triggers --unpack --auto-deconfigure /var/cache/apt/archives/grub-pc_2.04-1ubuntu26.17_amd64.deb /var/cache/apt/archives/grub2-common_2.04-1ubuntu26.17_amd64.deb /var/cache/apt/archives/grub-pc-bin_2.04-1ubuntu26.17_amd64.deb /var/cache/apt/archives/grub-common_2.04-1ubuntu26.17_amd64.deb 
/usr/bin/dpkg --status-fd 10 --configure --pending 
All upgrades installed
The list of kept packages can't be calculated in dry-run mode.
```

何も表示されない場合は `Unattended-Upgrade::Allowed-Origins` の設定が間違っているか、更新対象のパッケージが無いかのどちらかでしょう。  
更新対象パッケージが無いと動きが見えにくい機能でもあるので、設定の動作確認の際はパッケージ更新が遅れている環境を使うと分かりやすいと思います。

これで、寝て起きたら自動的にUbuntuの標準パッケージが更新されている準備が整いました。  
タイマーが待てない人は `sudo unattended-upgrade` を手動で実行してアップデートの動きを確認してみても良いでしょう。

## unattended-upgradesの設定（追加リポジトリ）

追加したリポジトリも含めて自動更新したい場合は、先ほどと同様に `Unattended-Upgrade::Allowed-Origins` にリポジトリに対応した設定を追加します。  
対象となるリポジトリの名称を知る必要があるので `/var/lib/apt/lists` から対象のリポジトリ登録を確認します。（以下はgitlab-eeの場合の例）

```
$ grep -e Origin -e Suite /var/lib/apt/lists/packages.gitlab.com_gitlab_gitlab-ee_ubuntu_dists_focal_InRelease 
Origin: packages.gitlab.com/gitlab/gitlab-ee
Suite: focal
```

`Unattended-Upgrade::Allowed-Origins` の記述形式は `${Origin}:${Suite}` なので、
`"packages.gitlab.com/gitlab/gitlab-ee:${distro_codename}";` を `Unattended-Upgrade::Allowed-Origins` に追加することで自動アップデートの対象となります。

例えば以下のように、gitlab-ee用のファイルを作成します。

```
$ cat <<'_EOL_' | sudo tee /etc/apt/apt.conf.d/51unattended-upgrades-gitlab-ee
Unattended-Upgrade::Allowed-Origins {
        "packages.gitlab.com/gitlab/gitlab-ee:${distro_codename}";
};
_EOL_
```

`sudo unattended-upgrade --dry-run` で確認してみると `16.0.4-ee.0` がアップデート対象として選出できていることが分かります。

```
$ sudo unattended-upgrade --dry-run -v
Starting unattended upgrades script
Allowed origins are: o=Ubuntu,a=focal, o=Ubuntu,a=focal-security, o=UbuntuESMApps,a=focal-apps-security, o=UbuntuESM,a=focal-infra-security, o=packages.gitlab.com/gitlab/gitlab-ee,a=focal
Initial blacklist: 
Initial whitelist (not strict): 
Option --dry-run given, *not* performing real actions
Packages that will be upgraded: gitlab-ee
Writing dpkg log to /var/log/unattended-upgrades/unattended-upgrades-dpkg.log
/usr/bin/dpkg --status-fd 10 --no-triggers --unpack --auto-deconfigure /var/cache/apt/archives/gitlab-ee_16.0.4-ee.0_amd64.deb 
/usr/bin/dpkg --status-fd 10 --configure --pending 
All upgrades installed
The list of kept packages can't be calculated in dry-run mode.
```

`apt list --upgradable` でも同じパッケージが確認できます。

```
$ apt list --upgradable
Listing... Done
gitlab-ee/focal 16.0.4-ee.0 amd64 [upgradable from: 16.0.3-ee.0]
N: There are 279 additional versions. Please use the '-a' switch to see them.
```

:::note info
ログの見やすさのため、先ほど作った `51unattended-upgrades` は `/tmp` に移動してあります（手順外）
:::

## (Option) OSの自動再起動

通常のパッケージ更新の場合はサービスの再起動で済みますが、`linux-image-.*-generic` をはじめとするカーネル関係のパッケージが更新された場合は再起動を要求されるケースもあります。  
任意の時間に自動的にOSの再起動がしたい場合は `Unattended-Upgrade::Automatic-Reboot` と `Unattended-Upgrade::Automatic-Reboot-Time` を設定しましょう。

例えば、自動的に再起動するスケジューラを "02:00" にセットする場合は以下のようになります。

```
$ cat <<'_EOL_' | sudo tee /etc/apt/apt.conf.d/51unattended-upgrades-reboot-policy
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
_EOL_
```

`Unattended-Upgrade::Automatic-Reboot-Time` はデフォルトが `"now"` なので、unattended-upgradeコマンド実行後に必要であれば即座に再起動されます。  

## (Option) 更新完了に伴う処理を追加したい場合

unattended-upgradesによって自動でパッケージが更新されるのは良いのですが、更新が終わった後に通知をしたり、何らかの処理を入れたい場合があります。  
メールを送るだけで良ければ `Unattended-Upgrade::Mail` の設定で済みそうですが、例えばSlackにメッセージを送ったり、カーネルの更新に合わせて独自のカーネルモジュールをコンパイルしたり、もう少し自由な処理を追加する方法があるとありがたいところです。  
そこで、力技で実現する方法を考えてみます。

unattended-upgradesによって更新されるファイルは以下の2つがあります。

| file                                                        | description                                        |
| ----------------------------------------------------------- | -------------------------------------------------- |
| `/var/lib/apt/periodic/unattended-upgrades-stamp`           | unattended-upgradeが完了したら更新される           |
| `/var/log/unattended-upgrades/unattended-upgrades-dpkg.log` | unattended-upgradeでパッケージをする時のログ出力先 |

`/var/lib/apt/periodic/unattended-upgrades-stamp` はパッケージ更新の有無によらず、unattended-upgradesの完了後にタイムスタンプが更新されるので、このファイルを監視しておくことで追加の処理を作ることができます。  
このファイルは `--dry-run` 実行時も更新されるので、動作を試すのも容易です。

今回はsystemdを利用してサンプルを実装してみます。  
まず `/etc/systemd/system/unattended-upgrades-notify.path` を作成し、`/var/lib/apt/periodic/unattended-upgrades-stamp` の変更を監視します。

```ini
[Unit]
Description="Check unattended upgrade finished"

[Path]
PathChanged=/var/lib/apt/periodic/unattended-upgrades-stamp

[Install]
WantedBy=multi-user.target
```

次に `/etc/systemd/system/unattended-upgrades-notify.service` を作成し、実行権限を付与した任意のスクリプトを呼び出せるようにします。

```ini
[Unit]
Description="Run unattended upgrade additional script"

[Service]
Type=oneshot
ExecStart=/opt/unattended-upgrades-script.sh

[Install]
WantedBy=multi-user.target
```

:::note info
もちろん作成するファイル名は何でも良いのですが `*.path` はデフォルトで同名の `*.service` に対応するので、名前は揃えておくと良いでしょう。

> By default, a service by the same name as the path (except for the suffix) is activated. Example: a path file foo.path activates a matching service foo.service.
> https://man.archlinux.org/man/systemd.path.5.en
:::

`/opt/unattended-upgrades-script.sh` の中身は任意の処理を書きましょう。今回はechoするだけです。

```bash
#!/bin/bash

set -eu

echo "Post processing after unattended-upgrade."
```

systemdのサービスを有効にします。

```
sudo systemctl daemon-reload 
sudo systemctl start unattended-upgrades-notify.path
sudo systemctl enable unattended-upgrades-notify.path
sudo systemctl enable unattended-upgrades-notify.service
```

あとは `sudo unattended-upgrade --dry-run` を実行し、任意の処理のスクリプトが呼び出されるか確認してみましょう。

```
$ systemctl status unattended-upgrades-notify.service
● unattended-upgrades-notify.service - "Run unattended upgrade additional script"
     Loaded: loaded (/etc/systemd/system/unattended-upgrades-notify.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Tue 2023-06-13 23:55:37 JST; 552ms ago
TriggeredBy: ● unattended-upgrades-notify.path
    Process: 6833 ExecStart=/opt/unattended-upgrades-script.sh (code=exited, status=0/SUCCESS)
   Main PID: 6833 (code=exited, status=0/SUCCESS)

Jun 13 23:55:37 qiita-gitlab systemd[1]: Starting "Run unattended upgrade additional script"...
Jun 13 23:55:37 qiita-gitlab unattended-upgrades-script.sh[6833]: Post processing after unattended-upgrade.
Jun 13 23:55:37 qiita-gitlab systemd[1]: unattended-upgrades-notify.service: Succeeded.
Jun 13 23:55:37 qiita-gitlab systemd[1]: Finished "Run unattended upgrade additional script".
```

`Post processing after unattended-upgrade.` がログに表示されており、スクリプトが呼び出されることが確認できました。  
あとは、必要に応じてスクリプトを拡張するだけです。

## おしまい

unattended-upgradesを使って、パッケージの自動更新ができるようになりました。

パッケージの自動更新が機能として可能でも、システムには様々な条件がありますので適切に機能させるのは難しいものです。  
状況や用途が許すのであれば、寝ている間に自動更新やOSの再起動ができるので運用負担が軽減できます。

仮に定期実行によってパッケージ更新をしなかったとしても、設定だけしておいてunattended-upgradeコマンドを手動で実行することで、パッケージ更新から再起動までを1コマンドで実行できるようになるので、それだけでも利用価値があると思います。