cloud-initを使ったLinux OSの初期設定

# 1. はじめに

仮想マシンなどCompute作成時にユーザーデータ（User Data）を指定したことがあるだろうか。ユーザーデータを使うと、簡単にLinux OSを設定できる。便利なわりに使われていない気がするので、ユーザーデータ／cloud-initについて説明する。

### 1-1. TL;DR

* cloud-initを使うと、簡単にLinux OSの初期設定ができる
* cloud-initの仕組みは各所で解説されているが、利用者にとってコンパクトで実践的なものは少ない
* 実践的なサンプルや、自力で書くときの参考情報を提供することで、活用のきっかけになりたい

### 1-2. 前提条件

* cloud-initが利用可能な環境（パブリック・クラウドに限らず、一部のプライベート・クラウドでも利用可能）
* Linux OSイメージ

# 2. なぜ、cloud-init ?

IaaSにおいて、本格的なOS設定の自動化やInfrastructure as Codeを目指すならば、AnsibleやChef、Puppet等の構成管理ツールが最適だ。またWebサーバなど、同一構成のサーバーをたくさん複製するにはカスタム・イメージ（カスタムAMI）が適している。

ただし実際の現場では、Ansibleなどの構成管理ツールを導入するほどではないけれど、ちょっとしたOS設定を変更したいことがある。

そのようなときに便利なのが**cloud-init**だ。もともとはAmazon EC2用に開発されたツールだが、現在はクラウドでLinux OSを設定する一般的なツールになっている。VMware vSphereやOpenStackでも使用できる。

またAnsibleなどの構成管理ツールを使うときでも、利用準備を整える前処理としてcloud-initを組み合わせて使うこともできる。

次の図はユーザーデータ（User Data）とcloud-initの関係を表した図だ。インスタンス作成時にユーザーデータを指定すると、OSイメージに組み込まれたcloud-initがユーザーデータの内容を実行することで、OSの初期設定ができる。

**ユーザーデータ（User Data）とcloud-initの関係**
![cloud-init01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266049/b13eed7a-3383-b9fc-3523-daed26664df2.png)

<font color="blue">**ヒント**</font>
<font color="blue">もともとcloud-initは、Amazon EC2インスタンスを迅速かつ簡単、確実に設定するツールとしてBrandon Fuller氏らが開発したものだ。そのことがブログ「[The beauty of cloud-init](https://brandon.fuller.name/archives/2011/05/02/06.40.57/)」に書かれている。</font>

<font color="blue">またcloud-initのメインメンテナーは、Ubuntuを開発するCanonical Server Teamなので、Ubuntu用のモジュールが充実している。</font>

# 3. cloud-initができること

### 3-1. cloud-initで使用できる書式

cloud-initでは複数の書式を使用できる。その中でも、よく使うのはシェルスクリプトとcloud-configだ。シェルスクリプトは当たり前なので省略するとして、今回はcloud-configについて説明する。

**cloud-initでよく使う書式**

| 書式 | 特徴 |
|:--|:--|
| シェルスクリプト | 既存のノウハウや資産を生かせる。`#!`で始まるスクリプトならば使用可能 |
| cloud-config | YAML形式で簡潔に書け、OSの差異を吸収できる |
| MIMEマルチパート形式 | シェルスクリプトとcloud-initを同時に使うなど、複数の書式を併用できる |

### 3-2. cloud-configとは

cloud-configとは、YAMLで簡潔にOS設定を記述できる機能だ。次の例ではタイムゾーンを東京に設定している。

```yml
#cloud-config
timezone: Asia/Tokyo
```

ここで注目することが二つある。一つ目は`#cloud-config`で始まっていること。これはcloud-configの書式で記述することを宣言している。

二つ目はタイムゾーンの指定方法。たとえばCentOS 6とCentOS 7ではタイムゾーンの設定方法は異なる。ところがcloud-configのtimezoneモジュールがOSの違いを吸収するため、まったく同じ記述を利用できる。

またtimezoneモジュールは複数のLinuxディストリビューションに対応しているので、RHEL系だけでなくUbuntuでも利用できる。

### 3-3. cloud-initの実行タイミング

基本的には **「インスタンス作成後の初回起動時に1回だけ実行する」** と覚えていいだろう。例外もあるのだが、今回説明する範囲は、この理解で問題ない。

* [cloud-initのデフォルト挙動を徹底的に調べてまとめてみた -結果ソースコードを読んだ-](https://dev.classmethod.jp/cloud/aws/cloud-init-cfg/)

もう一つ覚えておくべきことは **「OSのインストールが終わったあとに実行される」** ことだ。1回だけ実行される`/etc/rc.local`のようなものと考えてもいいだろう。

Linuxのサイレント・インストール・ツールKickstartとは異なるタイミングで実行されることは覚えておきたい。詳細は以下のエントリを参照のこと。

* [インスタンス作成時にUser Dataで指定した設定が遅れる理由](https://qiita.com/yamada-hakase/items/657fead978491e8db92f)


# 4. cloud-configチュートリアル基本編

それではcloud-configの記述例を紹介する。今回の環境ではCentOS 7と、以下のバージョンのcloud-initを使用している。

```bash
# cloud-init --version
/usr/bin/cloud-init 18.5
# python -V
Python 2.7.5
```

<font color="red">2021/06/10追記
CentOS 8とPython 3.6.8の組み合わせでも同じ結果になることは検証済み。</font>

### 4-1. cloud-config作成の基本

cloud-configを作成する手順は以下のとおり。

1. cloud-initマニュアルの[設定サンプルページ](https://cloudinit.readthedocs.io/en/latest/topics/examples.html)や[モジュール一覧](https://cloudinit.readthedocs.io/en/latest/topics/modules.html)から、やりたいことを実現できそうなサンプルやモジュールを探す。
<!--
![cloud-init02.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266049/31b8edf1-b55f-90ff-dbbb-f1e034c32fda.png) 
-->
2.[モジュールのページ](https://cloudinit.readthedocs.io/en/latest/topics/modules.html)で、自分が使うディストリビューションに対応しているか確認する。次のようにallになっていれば大丈夫だ。

> Supported distros: all

また以下のようにonce-per-instanceと書かれていれば、初回起動時に1回だけ実行される。

> Module frequency: once-per-instance

基本を理解したところで、次からは実際のサンプルを紹介しよう。

### 4-2. タイムゾーンとロケールを設定する

この設定ではタイムゾーンを東京、ロケールを日本語に設定している。timezoneもlocaleも複数のディストリビューションに対応しているので汎用的に利用できる。

```yml
#cloud-config
timezone: Asia/Tokyo
locale: ja_JP.utf8
```

<font color="red">**注意**</font>
タイムゾーンを変更したときはリブートすることを推奨する。なぜならば起動中のサービスには反映されないからだ。

たとえばsyslogのタイムスタンプは **「変更したタイムスタンプ」** では無く **「UTC」** のままになっている。そのため、次のようにサービスを再起動したほうがよいだろう。

```bash
systemctl restart rsyslog
```

### 4-3. パッケージを追加する

パッケージを追加インストールするときには、次のように`packages`のあとにパッケージ名を記述する。

```yml
#cloud-config
timezone: Asia/Tokyo
locale: ja_JP.utf8
packages:
  - man-pages-ja
  - multitail
  - nmap
  - nc
  - tmux
  - tree
```

ただしパッケージの追加やアップデートは、メタデータの同期やインストールに時間がかかることがある。そのためインスタンスの起動直後は実行されていない可能性がある。

実行状況は`/var/log/cloud-init.log`で確認できる。次のように`finish: modules-final`が出力されれば終了している。

```bash
# tail -f /var/log/cloud-init.log
★ここから下は画面の出力★
2019-05-16 23:04:25,343 - util.py[DEBUG]: Read 14 bytes from /proc/uptime
2019-05-16 23:04:25,343 - util.py[DEBUG]: cloud-init mode 'modules' took 0.208 seconds (0.21)
2019-05-16 23:04:25,344 - handlers.py[DEBUG]: finish: modules-final: SUCCESS: running modules for final
```

### 4-4. パッケージをアップデートする

`yum update`を実行するときは、以下のように`package_upgrade`の行を追加する。

```yml
#cloud-config
timezone: Asia/Tokyo
locale: ja_JP.utf8
package_upgrade: true
packages:
  - man-pages-ja
  - multitail
  - nmap
  - nc
  - tmux
  - tree
```

<font color="red">**注意**</font>
パッケージをアップデートしたときは再起動を推奨する。なぜならば、kernelやglibcなどは、再起動しないと有効にならないからだ。

### 4-5. ファイルを追記／作成する

ほかに使うものとしてはwrite_filesモジュールがある。詳しくは<a href="https://cloudinit.readthedocs.io/en/latest/topics/modules.html">ドキュメント</a>を読んで欲しいが、次の例は新規作成と追記である。

```yml
write_files:
    - content: |
        # My new /etc/sysconfig/samba file

        SMDBOPTIONS="-D"
      path: /etc/sysconfig/samba
    - content: |
        15 * * * * root ship_logs
      path: /etc/crontab
      append: true
```

### 4-6. 実行後に自動で再起動する

<font color="red">**注意：**
現時点では希望通りに動作しないことがある。必ず再起動したいときは後述のruncmdを使用すること。このあたりのことは今後別エントリで説明予定だ。</font>

パッケージをアップデートすると、kernelやglibcが更新される可能性がある。そのときに便利なのが`power_state`だ。cloud-init実行後に自動で再起動できる。

以下のように`delay:"+10", timeout:30`と指定した場合、cloud-init終了後30秒間のタイムアウト時間ののち、10分後に再起動される。

Oracle Linuxの場合、Kspliceのためにkernelアップデートに時間がかかるので、delayは5分未満に設定しないことを推奨する。

```yml
#cloud-config
timezone: Asia/Tokyo
locale: ja_JP.utf8
package_upgrade: true
packages:
  - man-pages-ja
  - multitail
  - nmap
  - nc
  - tmux
  - tree
power_state:
  delay: "+10"
  mode: reboot
  message: Bye Bye
  timeout: 30
```

# 5. cloud-configチュートリアル応用編

cloud-initのマニュアルを見ると、現時点ではcloud-configのモジュールの数が少なく設定できない項目も多い。そのためシェルスクリプトを書こうとしたとき、三つの選択肢がある。

1. cloud-configをやめて、全部シェルスクリプトで書く
2. MIMEマルチパート形式で書いて、cloud-configとシェルスクリプトを併用する
3. cloud-configのruncmdを使用する

このなかで便利なのがruncmdだ。制限事項はあるがスクリプトを記述できる。つまりcloud-configの利便性と、シェルスクリプトの自由度の高さを活用できるのだ。そこでruncmdの使い方を説明する。

なお今回は取り上げないが、MIMEマルチパート形式はcloud-utilsパッケージのwrite-mime-multipartコマンドを使えば作成できる。

### 5-1. runcmdの使い方

マニュアルに載っているサンプルは以下のとおり。ハイフン以降の各行が、それぞれのコマンドである。

```yml
runcmd:
  - [ ls, -l, / ]
  - [ sh, -xc, "echo $(date) ': hello world!'" ]
  - [ sh, -c, echo "=========hello world'=========" ]
  - ls -l /root
  - [ wget, "http://example.org", -O, /tmp/index.html ]
```

この例を見ても分かるように、カギ括弧以降に配列で書く方法と、ハイフン以降に直接書く方法がある。いずれもroot権限で実行され、後者の場合にはshで実行したとみなされる。

分かりやすさの点から、今回は直接記述する方法を採用する。そして注意事項を２点ほど。

* <font color="red">YAML形式なのでコロン（:）が含まれるときは、ダブルクオーテーションなどでクオートする必要がある。</font>
* <font color="red">既存のファイルをsedで書き換えるときは、シンボリックリンクではなく、実体のフルパスを指定すること。sedで書き換えると実ファイルになり、リンクが切れてしまう。</font>システム系のファイルは互換性のためシンボリックリンクが多いので要注意。なおsedに`--follow-symlinks`オプションを付けることで、シンボリックリンクのリンク切れを防止できる。

runcmdは約100行のPythonスクリプトなので、興味のある人は読んでみては。
`/usr/lib/python2.7/site-packages/cloudinit/config/cc_runcmd.py`

### 5-2. SELinuxを無効にする

OCI ComputeやAzure Virtual MachinesではデフォルトでSELinuxが有効になっている。runcmdでSELinuxを無効化する。

```yml
#cloud-config
timezone: Asia/Tokyo
locale: ja_JP.utf8
runcmd:
  - setenforce 0
  - sed -i -e 's/^\SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
  - systemctl restart rsyslog
```

### 5-3. ファイアウォールを無効にする

OCI Computeではデフォルトでfirewalld/iptablesが有効になっている。SELinuxに加えてファイアウォールを無効化する。ただし6系と7系では設定が異なる。

**CentOS 7/Oracle Linux 7の場合**

```yml
#cloud-config
timezone: Asia/Tokyo
locale: ja_JP.utf8
runcmd:
  - setenforce 0
  - sed -i -e 's/^\SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
  - systemctl stop firewalld
  - systemctl disable firewalld
  - systemctl restart rsyslog
```

**CentOS 6/Oracle Linux 6の場合**

```yml
#cloud-config
timezone: Asia/Tokyo
locale: ja_JP.utf8
runcmd:
  - setenforce 0
  - sed -i -e 's/^\SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
  - service iptables stop
  - chkconfig iptables off
  - service rsyslog restart
```

# 6. クラウドサービスでの指定方法

参考までに、AWSとAzure、Oracle Cloud Infrastructureでの指定方法を紹介する。クラウドの画面はよく変わるので、参考として捉えてほしい。

### 6-1. Amazon EC2でユーザーデータ（User data）の指定方法

1. EC2作成画面で**[インスタンスの詳細の編集]**をクリックする。
![aws-cloudinit01.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266049/e2bea2e4-ba9c-2153-e63b-8318486cc1db.png)

2. 次に表示された「インスタンスの詳細の設定」ページの一番下にある**[ユーザーデータ]**に入力する。
![aws-cloudinit02.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266049/4e1a1e65-84de-d4d4-20c4-925861784bc1.png)

### 6-2. OCI Computeでユーザーデータ（User data）の指定方法

1. Compute作成ページの一番下で**[Show Advanced Options（拡張オプションの表示）]**をクリックする。

![user-data01.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266049/c86ce9fc-74ca-37d0-ac55-f24eded6410f.png)

2.ページが拡張され、User dataを入力できるようになる。ファイルを指定するか、直接スクリプトを貼り付ける方法がある。
![user-data02.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266049/c364dd7a-14fa-5241-3b9a-55f495bac0b4.png)


### 6-3. Azure Virtual Machinesでユーザーデータ（User data）の指定方法

1. VMの作成画面で「詳細」をクリックする。そこで表示される**[クラウドの初期化]**に入力する。
![azure-cloudinit01.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266049/9b936066-eaae-e7cb-d136-e201379545b0.png)

Azureでのcloud-initサポートは下記リンクを参照のこと。Linux OSでもバージョンによってはcloud-initをサポートしていないことがある。またWebUIでは指定できないが、CLIだけで指定できることもある。

* [Azure での仮想マシンに対する cloud-init のサポート](https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/using-cloud-init)

# 7. おわりに

ここまで読んで

**「おれはインスタンス作成後に、コマンドのコピペでOK」**

と思う人もいるかも知れない。筆者も過去のエントリで、コマンドのコピペ用バージョンを紹介している。

* [OCI Compute払い出し直後にやること](https://qiita.com/yamada-hakase/items/e898ce08feb9497c81e3)

cloud-initの魅力は、インスタンス作成と同時に設定できること（fire and forget）と、汎用性が高く簡潔に書けることだろう。社内やグループ内のポータルなどに典型的なパターンを紹介しておけば再利用されることも多いのではないだろうか。

# 8. まとめ

* ユーザーデータ／cloud-initを使用すると、インスタンス作成時にLinux OSを設定できる
* シェルスクリプトやcloud-configなど複数の書式を利用できる
* cloud-config書式ではシンプルで汎用性の高い記述ができる
* rumcmdを使うと、cloud-conifg内にOSコマンドを記述できる
* LinuxのKickstartはOSインストール時に実行されるが、cloud-initはOS起動開始後に実行される。そのため時間のかかる処理を記述すると、すべての設定が反映されるまでタイムラグがある

# 9. 次は？

デバッグ編はこちら。

* [cloud-initをデバッグする Part 1](https://qiita.com/yamada-hakase/items/b42aed68e5623ed74024)

# 10. 参考

* [cloud-init公式サイト](https://launchpad.net/cloud-init)
* [cloud-init公式マニュアル](https://cloudinit.readthedocs.io/en/latest/)
* [AWSドキュメント：Linux インスタンスでの起動時のコマンドの実行](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/user-data.html)
* [Azureドキュメント：チュートリアル - Azure での Linux 仮想マシンの初回の起動時に cloud-init を使用してカスタマイズする方法](https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/tutorial-automate-vm-deployment)
* [An Introduction to Cloud-Config Scripting](https://www.digitalocean.com/community/tutorials/an-introduction-to-cloud-config-scripting)
* [How To Use Cloud-Config For Your Initial Server Setup](https://www.digitalocean.com/community/tutorials/how-to-use-cloud-config-for-your-initial-server-setup)
* [Automating Openstack with cloud init run a script on VM's first boot](https://raymii.org/s/tutorials/Automating_Openstack_with_Cloud_init_run_a_script_on_VMs_first_boot.html)
* [cloud-init の設定を上書き、拡張する方法](https://hiroki.jp/config-cloud-init)