CentOS 7起動時にプログラムを1回だけ自動実行する（RunOnceのように）

# WindowsのRunOnce
インフラ構築をコードで自動化していると、たまに以下のように、「システム起動時にプログラムを1回だけ自動実行」したくなることがあります。

* アンインストーラの削除（アンインストーラ自身で自分を削除できないので）
* 処理の途中で再起動が必要な場合、再起動後に残りの処理を自動実行する

Windowsなら「RunOnce」というレジストリキーにプログラムを登録するだけで実現できることですが、CentOS 7でどうやって実現するのかを検討しました。

# CentOS 7 起動時にプログラムを自動実行
まず「1回だけ」を抜きにして、システム起動時にプログラムを自動実行する方法ですが、CentOS 7ではプログラムをサービスユニットとして登録して自動起動を有効化します。

普通のサービスは起動したプロセスがシステムに残りますが、実行して終了するだけのプログラムもサービスにすることができます。

## サービスにするメリット
以下のような条件をユニットファイルに記述するだけで指定することができるようになります。

* プログラムの実行前に起動完了してほしいサービス
* 逆に、プログラムの実行が終了するまで起動してほしくないサービス
* プログラムで使用する環境変数
* プログラム終了後に実行してほしい処理
* 実行ユーザー

ユニットファイルには、ほかにも多くのオプションを指定することができます。
参考: [SYSTEMD のユニットファイルの作成および変更](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/7/html/system_administrators_guide/sect-managing_services_with_systemd-unit_files)

また`systemctl`コマンドを使って、他のサービスと同じ方法で操作することができるようになります。
参考: [システムサービスの管理](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/7/html/system_administrators_guide/sect-managing_services_with_systemd-services)

## ユニットファイル
ユニットファイルを以下の例のように作成し、`/etc/systemd/system/`にユニット名＋拡張子`.service`で保存します。ユニット名は任意なので、以下の例では`autorun`としています。

```Ini:/etc/systemd/system/autorun.service
[Unit]
After=network-online.target

[Service]
User=root
ExecStart=/root/myscript

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

* `Unit`セクション
  * プログラムを実行する前に起動完了してほしいサービスを`After=`で指定する
  * この例では`network-online.target`を指定しているので、プログラムの中でネットワークを使用することができる
  * 逆に実行が終了するまで起動してほしくないサービスがあれば、`Before=`で指定することもできる
* `Service`セクション
  * 実行するユーザーを`User=`で指定する
  * 自動実行するプログラムを`ExecStart=`で指定する
  * この例では`/root/myscript`というスクリプトを実行する
* `Install`セクション
  * 起動する必要のあるターゲットを`WantedBy=`で指定する
  * この例では`multi-user.target`を指定しているので、CUI/GUIのマルチユーザー（ランレベル3または5）で起動する

## サービス自動起動の有効化
以下のコマンドでサービスの自動起動を有効化します。
`systemctl enable autorun.service`

ファイルを確認すると、`WantedBy=multi-user.target`と指定したので`multi-user.target.wants/`ディレクトリにシンボリックリンクが作成されたことが分かります。

```Shell:
$ find /etc/systemd -name "autorun.*" -print

/etc/systemd/system/multi-user.target.wants/autorun.service
/etc/systemd/system/autorun.service
```

# 起動時に1回だけ実行 その1 - 実行後に自動起動を無効化
このままでは登録したプログラムが起動のたびに実行されてしまうので、設定後最初の起動時にだけ実行されるようにします。

2回目以降のシステム起動時に自動実行されなくするだけなら、`ExecStart=`を以下のコードに変更するだけでも実現できます。

```Ini:/etc/systemd/system/autorun.service
ExecStart=/bin/bash -c "/root/myscript \
        && /usr/bin/systemctl disable autorun.service"
```
`ExecStart=`には複数のコマンドを指定できないので、bashを`-c`オプションで実行することで、続く文字列を評価して実行する例です。

この例では、`/root/myscript`の実行に成功したら`systemctl disable`コマンドでサービスの自動起動を解除しています。

逆に`/root/myscript`の実行に失敗した場合は自動起動が解除されないので、成功するまで何度でもシステム起動時に実行されます。

再度ファイルを確認すると`systemctl enable`で生成されたシンボリックリンクが削除されているので、2回目からは自動実行されないようになりました。

```Shell:
$ find /etc/systemd -name "autorun.*" -print

/etc/systemd/system/autorun.service
```

# 起動時に1回だけ実行 その2 - サービスを削除
`autorun.service`を今後も使用する予定があるならこのままでいいのですが、二度と使わないなら削除しないと気持ち悪いです。

`ExecStart=`でbashに評価させる文字列に、ユニットファイルを削除する処理を追加しました。

```Ini:/etc/systemd/system/autorun.service
ExecStart=/bin/bash -c "/root/myscript \
        && /usr/bin/systemctl disable autorun.service \
        && rm -f /etc/systemd/system/autorun.service"
```
もう一度`systemctl enable autorun.service`で自動起動を有効化してから再起動すると、サービス自体が削除されたことが分かります。

```Shell:
$ find /etc/systemd -name "autorun.*" -print | wc -l
0

$ systemctl list-unit-files autorun.service
UNIT FILE STATE

0 unit files listed.
```

# 起動時に1回だけ実行 その3 - プログラムも削除
自動実行するプログラムも削除してしまえば、1回実行した後には何も残らなくなります。

```Ini:/etc/systemd/system/autorun.service
ExecStart=/bin/bash -c "/root/myscript \
        && /usr/bin/systemctl disable autorun.service \
        && rm -f /etc/systemd/system/autorun.service /root/myscript"
```