systemdユニット再*n入門

# systemdユニット再*n入門
systemdは、Unixで長らく使われていた、SysVinit、Upstartの改善のために導入された、プログラムスイートです。
いろいろあって（ https://www.slideshare.net/enakai/linux-27872553 ）現代的なブート処理として様々なLinuxディストリビューションで取り入れられています。

また、従来(Fedora15/CentOS6以前)はシェルスクリプトを利用して、起動・停止スクリプトを作成していましたが、systemdの設定ファイルによる管理が行えるようになりました。

systemdがFedora17で導入されてからもう7年も経っており、今更*.service関連はどうでもいいと思うので無視されがちなsystemdスイートに関する内容を記載します。

### 検証環境
今回の検証環境は以下のとおりです。

|  | ArchLinux | CentOS Linux release <br> 7.5.1804 (Core) |
|---|---|---|
| uname -r | 4.19.4-arch1-1-ARCH | 3.10.0-862.11.6.el7.x86_64 |
| systemd | libsystemd 239.303-1 <br> netctl 1.19-1 (base) <br> python-systemd 234-2 <br> systemd 239.303-1 (base-devel) <br> systemd-sysvcompat 239.303-1 (base) | systemd-libs-219-57.el7_5.1.x86_64 <br> systemd-219-57.el7_5.1.x86_64 <br> systemd-sysv-219-57.el7_5.1.x86_64 <br> systemd-libs-219-57.el7_5.1.i686 |


## systemdのユニットファイルの配置
systemdのユニットは大きく分けて以下の二箇所に配置されます。
他の場所も参照ディレクトリとして固定のパスがありますが、通常は意識する必要はありません。

* /usr/lib/systemd/system/
    * パッケージにより配置されたユニット
* /etc/systemd/system/
    * 管理者によって作成されたユニット


## systemd-journal
systemdでは、旧来のsyslog系サービスに対応する機能のため、journal機能が用意されています。

### ログの場所と出力形式
ログはバイナリ形式で出力されるため、Windowsや古いLinuxサーバ等のsystemd-journalの存在しない環境でログを読む場合、`strings <ログファイル> | grep -i message` のように、バイナリを単純に文字列化することで、旧来のログと同様に読むことができます。

また、初期状態ではtmpfs上にログを出力していくため、システムの起動時にログは破棄されます。
デフォルトでは `/var/log/journal` ディレクトリが存在すれば永続化し、ディレクトリが存在しない場合はtmpfs上にログを出力するよう設定されています。

ログを永続化する場合、`/var/log/journal`ディレクトリを作成する必要があります。
systemd-journaldの設定ファイルは`/etc/systemd/journald.conf`です。

### ログローテーションとクォータ
ログローテーションについても、systemd-journalで機能が用意されており`MaxFileSec`を指定することで、自動的にログをローテーションします。

```ini
[Journal]
...
MaxFileSec=1day
```

`RuntimeKeepFree`, `SystemKeepFree`オプションも用意されており、`Runtime`はtmpfs上にログを保存する場合に、`System`は `/var/log/journal` にログを保存する場合に、ログによるディスク圧迫を避ける機能が用意されています。
デフォルトではディスクの空き容量が15%以下になった場合、ログによるディスク圧迫を避けるため古いデータが消されるようになります。

`RuntimeMaxFileSize`, `SystemMaxFileSize`ではログの最大容量が指定され、`KeepFree`とあわせてより厳しい方で判定されます。
この機能を利用することで、ついうっかりログがディスクを埋め尽くしてサーバに異常が発生という事態を避ける事ができます。

```ini
[Journal]
...
RuntimeMaxFileSize=1G
SystemMaxFileSize=1G
```

### ログのフィルタ方法
ログの記録方法は、syslogとの互換を優先しており、プライオリティ/ファシリティ形式での分類を行います。
用意されているプライオリティ/ファシリティは RFC 5424 に準拠しており、仕様に準拠しているsyslogであれば、journalに変更したことを特に意識せず運用が可能です。

ログを見る際は、特定のユニットによるログの場合 `-u <ユニット名>` を行うことで、旧来のログ出力と同等のログを得ることができます。
また、旧来の`tail -f`相当の操作を行いたい場合は、`-f`オプションを使うことで実現できます。

### 改ざん検知
systemd-journalでは、FSS(Forward Secure Sealing)の機能が用意されており、ログファイルに対する署名が行えるようになっています。
デフォルトで機能は有効となっており、実際に利用する署名鍵を生成することで、自動的に署名を行いログの改ざん防止ができるようになります。

以下のコマンドを実行することで、FSSの際に利用する署名鍵を生成できます。

```console
# journalctl --setup-keys
```

出力されたキーを利用して、ログに改ざんが無いことを確認することが出来ます。

```console
# journalctl --verify-key <上記で出力された検証鍵>
PASS: /run/log/journal/f2bd78fce040480d9f9fd3077b9c0ef2/system.journal
PASS: /run/log/journal/d63e19b3a0a3436cae86e7def89cc3a6/system.journal
PASS: /var/log/journal/f2bd78fce040480d9f9fd3077b9c0ef2/system.journal
```

### rsyslogとの連携
Fedora等の新機能に積極的なシステムでは既にsystemd-journalからのログのソケット転送は無効になっています。
`/etc/systemd/journald.conf`で`ForwardToSyslog=no`とするのが現代的な設定です。

rsyslogを利用してログ収集サーバにログを転送する必要がある場合等で、rsyslogからのログの取得が必要な場合は、rsyslog.confで`$ModLoad imjournal`として、rsyslogからsystemd-journalを参照するよう設定します。

### リモートのログ収集サーバ
systemd-journaldでは、rsyslogでのログ収集機能と同等の機能が用意されています。
利用のためには、systemd-journal-remoteが必要となります。

ログ収集サーバでは、`libmicrohttpd`, `systemd-journal-remote`サービスを起動します。
デフォルトでは、19532ポートでの待ち受けを行います。
サーバ証明書を用意することでサーバ間の通信をSSL化可能です。

```console
# systemctl enable --now systemd-journal-remote
```

また、設定ファイルが`/lib/systemd/system/systemd-journal-remote.service`に存在します。
このファイルを`/etc/systemd/system/`にコピーして設定を行います。
デフォルトの状態では、httpsでの接続を前提に設定されているため、SSL化を行う場合は公的な証明書を用意するか、自己認証局を作成しCA証明書、サーバ証明書を作成する必要があります。
SSL化を行わない場合は以下のように編集してください。

```ini
ExecStart=/etc/systemd/systemd-journal-remote --listen-https=-3 --output=/var/log/journal/remote/
(↓変更)
ExecStart=/etc/systemd/systemd-journal-remote --listen-http=-3 --output=/var/log/journal/remote/
```

また、ログフォルダの権限変更が自動で行われません。
`/var/log/journal/remote`フォルダの存在確認と権限付与が必要です。

```console
# mkdir -p /var/log/journal/remote
# chown systemd-journal-remote /var/log/journal/remote
```


ログの送信は、systemd-journal-uploadサービスによって行われます。
`/etc/systemd/journal-upload.conf`で、ログ収集サーバのURLを指定してください。

```ini
[Upload]
URL=http://10.0.117.2:19532
```

取得したログは、まとめて見る場合は`-m`または`--merge`オプションで、全てのリモートホストのログを結合したログを参照できます。
また、環境変数にパラメータを渡すことでフィルタをかけることが可能です。
指定可能な環境変数は、`strings`コマンドで文字列化したバイナリデータから探すことが可能です。

参考例は以下のとおりです。

* 特定ホストのログのみ
    * journalctl _HOSTNAME=<ホスト名>

* 特定のsyslog
    * journalctl SYSLOG_IDENTIFIER=kernel SYSLOG_FACILITY=0


## systemd-networkd / systemd-resolved
systemctlでは、ネットワーク接続のためのサービスも提供されています。
systemd-networkd / systemd-resolved を使うことで、NetworkManager等のパッケージを利用せずにネットワーク設定を行うことが出来ます。

新しい機能の取り入れが遅いディストリビューションの場合、デフォルトではインストールされていないため、追加でパッケージのインストールが必要です。

```console
(CentOS7の場合)
# yum install systemd-networkd systemd-resolved
```

### インターフェースの設定
systemd-networkdでは、ネットワークインターフェースは、`/etc/systemd/network/*.network`,`/etc/systemd/network/*.netdev`で管理します。

一般的に利用する設定ファイルの書式は以下のとおりです。
ブリッジアダプタの設定なども記載できますが、詳細についてはsystemd-network(5)を確認ください。

```ini
[Match]
MACAddress=xx:xx:xx:xx:xx:xx (MACアドレス)       ←どちらかを記載
Name=eth0(デバイス名)

[NETWORK]
DHCP= yes | no | ipv4 | ipv6                    ←DHCPによる取得の場合記載

ADDRESS=192.168.1.2/24(CIDR表記)                ←固定IPによる取得の場合記載
GATEWAY=192.168.1.1(ゲートウェイのIP)
DNS=192.168.1.1(DNSサーバのIP)                   ←systemd-resolvedから利用
```

### DNS設定

systemd-resolvedでは、`resolv.conf`を`/run/systemd/resolve/resolv.conf`,`/run/systemd/resolve/stub-resolv.conf`に作成します。
旧来のサービスとの互換性のために、`/etc/resolv.conf`にシンボリックリンクを作成することが推奨されています。
`resolv.conf`は、通常のDNS問い合わせを行う際に利用します。
`stub-resolv.conf`は、systemd-resolvedをDNSスタブリスナーとして利用する際に設定します。
この場合、systemd-resolvedは`127.0.0.53:53`での待ち受けを行います。
後述する、systemd-nspawnを利用する場合、各コンテナは`127.0.0.53`で名前解決ができるようになるため、DNS設定を各コンテナに行わなくてもよくなるため便利です。

systemd-resolvedの設定ファイルは`/etc/systemd/resolved.conf`です。

DNSSECはデフォルトでは相手サーバが対応している場合のみ有効になります。
DNSSECの利用を強制したい場合は、`/etc/systemd/resolved.conf`で以下のように設定します。

```ini
[Resolve]
DNSSEC=true
```

systemd-resolvedの動作状態は以下のコマンドで確認できます。

```console
# systemd-resolve --status
```

### 有線/無線の同時利用
接続をsystemd-netwokrd管理で行うことによって、有線/無線アダプタの自動切り替えを行うことが出来ます。

Wi-Fi等の無線接続を使う場合は、wpa_supplicantデーモンを利用する必要があります。
systemd-networkd経由で"[Match]"部の"Name"で適切なインターフェース名を指定することで、`/etc/wpa_supplicant/wpa_supplicant-wlpxxx.conf`を利用してwpa_supplicantを起動します。

有線/無線を同時に設定する場合は、優先度を設定する必要があります。
優先度を設定することによって有線/無線がどちらも使える場合に、どちらのインターフェースを有線してを使うか判断ができるようになります。

```ini
[DHCP]
RouteMetric=10
```

## systemd-timesyncd

RHEL/CentOS以外では、systemd-timesyncdが用意されており、この機能を使うことで共通の設定方法でNTP同期を設定できます。（RHEL/CentOS8ではChronyd）
システム時刻は UTC/協定世界時刻 での時刻同期を行い、timedatectlでオフセットを入れるよう設計されています。

NTPサーバは`/etc/systemd/timesyncd.conf`で設定します。

```ini
[Time]
NTP=<NTPサーバ>
```

以下のコマンドでタイムゾーンをセットします。

```console
# timedatectl set-timezone Asia/Tokyo
```

NTP同期を有効にする際は以下のコマンドを実行します。

```console
# timedatectl set-ntp true
```

時刻の同期状態は以下のコマンドで確認できます。

```console
# timedatectl timesync-status
Server: (null) (ntp.nict.jp)
Poll interval: 0 (min: 32s; max 34min 8s)
Packet count: 0
```

## systemd-user
ユーザーディレクトリで、そのユーザーのサービスを定義する場合は、`~/.config/systemd/user/`にサービスファイルを配置します。

一般ユーザー権限でのサービスが必要な場合、このフォルダにサービスを定義することで、旧来は管理者権限以外では行えなかった、一般ユーザ権限でのサービスの管理が行えます。
この機能を利用することで、後述のsystemd-timer等も利用することができます。

一般ユーザーからの実行とルートユーザーからの実行の場合、以下のようにCGroupが異なることが確認できます。

```console
$ systemctl status
   CGroup: /
           ├─user.slice
           │ └─user-1000.slice
```

```console
# systemctl status
   CGroup: /
           ├─user.slice
           │ └─user-0.slice
```

## systemd-timer
systemdにはcronに相当するタイマー実行機能が用意されています。
タイマーを登録する際は、*.service形式でサービスとしてジョブを登録し、*.timerファイルを作成して実行周期を指定する必要があります。

定義されているジョブは、以下のコマンドで確認することが出来ます。

```console
# systemctl list-timers
NEXT                         LEFT       LAST                         PASSED       UNIT                         ACTIVATES
Fri 2018-11-16 10:41:38 JST  19h left   Thu 2018-11-15 10:41:38 JST  4h 57min ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
```

CentOSの場合、tmpfsの削除がタイマーで指定されており、Fedora等ではパッケージの定期アップデートが定義されています。定義ファイルは、systemdの慣習どおり`/etc/systemd/system/`に配置します。
※`/usr/lib/systemd/system`はパッケージによるsystemdファイルの追加・削除を想定しています。

ジョブは以下のように定義します。

* test.service

```ini
[Unit]
Description=Test Job

[Service]
ExecStart = /opt/test.sh
Type=simple

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

* test.timer

```ini
[Unit]
Description=Test Timer

[Timer]
OnCalendar=Mon *-*-* 5:00:00      ←曜日 年-月-日 時:分:秒　で指定
Persistent=true
Unit=test.service

[Install]
WantedBy=timers.target
```

サービスとタイマーの作成後は、実際に実行するようユニットを登録します。

```console
# systemctl enable --now test.service
# systemctl enable --now test.timer
```

タイマーの登録状態は以下のコマンドで確認可能です。

```console
# systemctl list-timers
NEXT                         LEFT          LAST                         PASSED       UNIT                         ACTIVATES
Mon 2018-11-19 05:00:00 JST  3 days left   n/a                          n/a          test.timer                   test.service
```

## systemdによるACPIイベントの操作
systemdでは`/etc/systemd/logind.conf`でsystemd-logindに設定を行うことで、ACPIイベントに対応した操作を行うことが出来ます。
ただし、電源ボタン等のACPIイベントには対応していますが、ノートパソコンのバッテリー等一部の環境では、一般的なACPIイベントが発生しないため、acpidを利用してACPIイベントをフック出来るようにする必要があります。

ACPIイベントに対する対応は、デフォルトでは以下の通り設定されています。

* HandlePowerKey=poweroff
* HandleSuspendKey=suspend
* HandleHibernateKey=hibernate
* HandleLidSwitch=suspend

手動で直接ACPIイベントを発生させる場合は、以下のコマンドを実行することで行なえます。

```console
# systemctl suspend | hybrid-sleep | suspend
```

## systemd-nspawn
systemdではコンテナ型仮想化も可能なサービスである、systemd-nspawnが用意されています。
これは、systemdで対応したcgroups等の比較的新しい機能を取り込みつつ、枯れたシステムコールを使ってコンテナ型仮想化を実現するものです。

コンテナ型仮想化は大まかにはすごいchrootであるため、Linuxカーネルを使わないシステムを動かすことは出来ません。
コンテナ型仮想化に利用するゲストOSはLinuxカーネル上で動作するOSでなければならない制限があります。

似たようなソフトウェアとしてDockerがありますが、Dockerでは1コンテナ1プロセスという設計思想があるため、systemdが原則利用できない（動かせなくはないがDockerでやるべき使い方ではない）等、コンテナ環境の動作には制限がありました。

※Dockerがしょぼいという話ではなく、一長一短です。※

しかし、systemd-nspawnでは、LXC等に近いコンテナ型仮想化を行うため、コンテナ型仮想化そのものによる技術的な制限はありますが、systemd-nspawnを使っている事による、ソフトウェア側から行われる動作の制限は、特にありません。


### コンテナ環境構築の準備
コンテナの作成の際は、各ディストリビューションのパッケージマネージャを利用して、コンテナ用の領域にゲストOSのパッケージ群をインストールする必要があります。

コンテナを作成する際、他OSのパッケージマネージャをインストール可能なディストリビューションであれば、コンテナに使いたいOSを用意せず、パッケージマネージャだけを利用してのOSパッケージ群のインストールも可能ですが、パッケージマネージャのための依存パッケージがコンテナ作成を行うOSにインストールされてしまうことになります。
特にコンテナのホストOSで行うと、ホストの環境があまりきれいにならなくなってしまいます。

基本的にはコンテナの作成の際のOSは、コンテナ化したいOSでコンテナの作成を行ったほうが気持ちよく作成できます。

また、DockerHub等であれば、ある程度は信用してもよさそうな出自のコンテナを利用できますが、誰が作ったのか解らないコンテナを利用するよりも、自分でインストールした環境のほうが安心して利用できます。

### コンテナ環境の作成
パッケージマネージャを利用して、それぞれのOSのコンテナを作成します。

* ArchLinux

```console
# mkdir /var/lib/machines/arch_container
# pacstrap -i -c /var/lib/machines/arch_container base
```

* Debian

```console
# mkdir /var/lib/machines/debian_container
# debootstrap <バージョン名> /var/lib/machines/debian_container <リポジトリURL>

例)
# debootstrap stable /var/lib/machines/debian_container http://deb.debian.org/debian/
```

* Ubuntu

```console
# mkdir /var/lib/machines/ubuntu_container
# debootstrap <バージョン名> /var/lib/machines/debian_container <リポジトリURL>

例)
debootstrap xenial /var/lib/machines/ubuntu_container http://archive.ubuntu.com/ubuntu/
```

* CentOS

```console
# mkdir /var/lib/machines/centos_container
# yum -y --releasever=7 --installroot=/var/lib/machines/centos_container install systemd passwd yum
```

* Fedora

```console
# mkdir /var/lib/machines/fedora_container
# dnf -y --releasever=29 --installroot=/var/lib/machines/fedora_container install systemd passwd dnf fedora-release
```


### コンテナの起動
コンテナへの操作は、以下のコマンドで行うことができます。
このコマンドで操作する際は、ログアウトするまでの一時的なセッションとして起動します（chrootのような状態です）。

この起動方法の場合は、init処理が動作しないため、コンテナの動作に制限のある状態で起動されます。
コンテナをサービスとして起動した時と同等の状態で動かしたい場合は、`-b`オプションを追加してください。

コンテナから出る際は、ctrlキーを押しながら%キー(USキーボードの場合"]")を3回押すことでコンテナから抜けることができます。

```console
# systemd-nspawn -D <コンテナのルートパス>

# systemd-nspawn -b -D <コンテナのルートパス>
```

コンテナを一時的なセッションではなく、サービスとして起動する場合は以下のコマンドで起動します。
この方法、および自動起動する場合は、`/usr/lib/systemd/system/systemd-nspawn@.service`を参照して起動しますが、ExecStartにオプションパラメータが定義されており、`systemd-nspawn -b -D`で起動した際と挙動が異なります。
コンテナでサービスを起動したい場合であれば、「--network-veth」の定義を削除するのが最も簡単な対応です。

```console
# systemctl start systemd-nspawn@<コンテナ名>
```

コンテナをOSの起動時に、サービスとして自動起動する場合は以下のコマンドで設定します。

```console
# systemctl enable systemd-nspawn@<コンテナ名>
```

既に起動しているコンテナのコンソールを操作する際は、以下のコマンドで操作可能です。
コンテナから出る際は、ctrlキーを押しながら%キー(USキーボードの場合"]")を3回押すことでコンテナから抜けることができます。

```console
# machinectl login <コンテナ名>
```

起動しているコンテナ、およびそのコンテナ名は以下のコマンドで確認できます。
今回の場合、以下のような出力になります。

```console
# systemctl list-units | grep systemd-nspawn
systemd-nspawn@arch_container.service               loaded active running   Container arch_container
systemd-nspawn@centos_container.service             loaded active running   Container centos_container
systemd-nspawn@debian_container.service             loaded active running   Container debian_container
systemd-nspawn@fedora_container.service             loaded active running   Container fedora_container
systemd-nspawn@ubuntu_container.service             loaded active running   Container ubuntu_container
```

動作しているコンテナの状態は、以下のコマンドで取得できます。
ここではコンテナ内のsystemdが動かしているユニットの一覧が確認できます。

```console
# systemctl status systemd-nspawn@arch_container
● systemd-nspawn@arch_container.service - Container arch_container
   Loaded: loaded (/usr/lib/systemd/system/systemd-nspawn@.service; disabled; vendor preset: disabled)
   Active: active (running) since Mon 2018-11-26 13:35:56 JST; 1min 48s ago
     Docs: man:systemd-nspawn(1)
 Main PID: 1657 (systemd-nspawn)
   Status: "Container running."
    Tasks: 7
   Memory: 14.8M
   CGroup: /machine.slice/systemd-nspawn@arch_container.service
           ├─1657 /usr/bin/systemd-nspawn --quiet --keep-unit --boot --link-journal=try-guest --network-veth --machine=arch_container
           ├─system.slice
           │ ├─console-getty.service
           │ │ └─1720 /sbin/agetty -o -p -- \u --noclear --keep-baud console 115200,38400,9600 vt220
           │ ├─dbus.service
           │ │ └─1717 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
           │ ├─systemd-logind.service
           │ │ └─1716 /usr/lib/systemd/systemd-logind
           │ ├─lvm2-lvmetad.service
           │ │ └─1702 /usr/bin/lvmetad -f
           │ └─systemd-journald.service
           │   └─1699 /usr/lib/systemd/systemd-journald
           └─init.scope
             └─1658 /usr/lib/systemd/systemd
```

コンテナの停止は以下のコマンドで停止できます。

```console
# systemctl stop systemd-nspawn@<コンテナ名>

(強制停止の場合)
# machinectl terminate <コンテナ名>
```

### コンテナのrootパスワード
コンテナ型仮想化の場合は、OSの起動時に通常のインストールと違いひと手間操作が必要です。

* ArchLinux

ArchLinuxではrootが空のパスワードである状態が許容されるためそのまま起動できます。
起動後に passwd コマンドでパスワードを設定することで、ログインすることが可能です。

* その他のOS

CentOS/Fedora, Debian, Ubuntu 等メジャーどころのOSでは、rootが空のパスワードの状態を許容していません。
そのため、コンテナを起動する前にパスワードを設定する必要があります。

パスワードを設定する際は、コンテナのディレクトリの `/etc/shadow` ファイルを直接編集して、パスワードをセットでも可能ですが、一旦、`systemd-nspawn -D`でコンテナに入り、`passwd`コマンドを実行すると便利です。

```console
# systemd-nspawn -D <コンテナのルートパス>
# passwd
```

### コンテナの管理
systemd-nspawn のコンテナ型仮想化は、systemdの機能を利用して行っています。
そのため、ホストマシンのsystemdスイートからコンテナの管理が可能です。

systemd-cglsを実行すると、以下のようにコンテナ内部のプロセスも含めた情報が表示されます。

```console
# systemd-cgls
├─1 /usr/lib/systemd/systemd --system --deserialize 17
├─machine.slice
...
│ ├─systemd-nspawn@ubuntu_container.service
...
│ ├─systemd-nspawn@fedora_container.service
...
│ ├─systemd-nspawn@debian_container.service
...
│ ├─systemd-nspawn@centos_container.service
...
│ └─systemd-nspawn@arch_container.service
...
├─user.slice
...
└─system.slice
```

また、ジャーナルも当然 systemd-nspawn 対応が行われており、何も指定しないと全体、-M オプションでコンテナを指定してジャーナルを確認することができます。

```console
# journalctl -M arch_container
```

# まとめ
systemdでは、様々な機能を一貫したサービスで提供することが出来ます。
また、機能がsystemdに依存してはいるものの、systemdが提供する共通したインターフェースでの操作となり、OSに関わらず同じ方法で利用できるため、便利です。