1. はじめに
以前書いた記事「cloud-initを使ったLinux OSの初期設定」が、特定クラウドに依存しないこともあって**「LGTM(いいね)」**が好調だ♪ そこでデバッグ方法も書いてみることにした。
**「cloud-initをデバッグする」というタイトルを付けたが、cloud-init自身はデバッグの対象ではない。正確には「cloud-initのUser-Dataに指定したスクリプトのデバッグ方法」**である。
1-1. 対象環境
- cloud-initを利用できる環境(cloud-init 18.5で確認)
- 特定のクラウドサービスには依存しない。プライベートクラウドも同様
- Linux OSイメージ
1-2. 予定している内容
- User-Dataを書くときのチェック項目
- デバッグのためのcloud-initの理解
- インスタンスを作成しないでcloud-initを実行する方法
- その他高度な分析方法
2. cloud-initふりかえり
まずは基本をふりかえろう。以下のことは覚えておきたい。
- cloud-initのUser-Dataには、シェルスクリプトやcloud-configなど複数の書式を指定できる
- cloud-config書式を利用すると、シンプルで汎用性の高い記述ができる
- cloud-configで設定できることは利用可能なモジュールに限定される。そのようなときはruncmdモジュールを使うと、cloud-config内にOSコマンドを記述できる
- cloud-initはroot権限で実行される
- cloud-initは、Kickstartとは異なりインスタンス作成後に実行される。そのため時間のかかる処理を記述すると、すべての設定が反映されるまでタイムラグがある
3. デバッグの基本
「デバッグの基本」というタイトルを付けたが、User-Dataを作成するうえでチェックすべき項目を説明する。
3-1. チェック項目
書式がシェルスクリプトなのか、cloud-configなのかでチェック項目は異なる。とくに長いステップものや使い回すものには事前に確認したい。以下の表の中で青文字の項目を説明する。
| 実施項目 | シェルスクリプト | cloud-config |
|:--|:-:|:-:|:-:|
| 実際の動作確認 | ○ | ○ |
| sedで文字列置換するときは、対象がシンボリックリンクでないこと | ○ | ○ |
| syslogやcloud-initのログの確認 | ○ | ○ |
| シェルスクリプトの文法チェック※ | ○ | × |
| YAML形式になっていること | × | ○ |
| 使用しているモジュールが設定ファイルにあること | × | ○ |
※シェルスクリプトは便宜上bashを想定している。#!
で呼び出せれば何でもよい。bash以外は言語ごとの文法チェッカーを使用すること。
3-2. sedの文字列置換に要注意
ファイル内の文字列を書き換えるのにsed -i
をよく使用する。しかし、シンボリックリンクに対して実行すると、リンクが切れて別ファイルになるので注意が必要だ。次のいずれかの条件に当てはまるときは、必ずチェックしてほしい。
- シェルスクリプトでsedを実行している
- cloud-configのruncmdでsedを実行している
3-2-1. 失敗例:シンボリックリンクに対する実行
たとえばCentOS 7において、SELinuxの設定ファイルは/etc/selinux/config
だ。ところが/etc/sysconfig/selinux
にもシンボリックリンクが存在する(システム系の設定ファイルは、互換性維持のためシンボリックリンクを持つものが多い)。
$ ls -l /etc/selinux/config
-rw-r--r--. 1 root root 543 Feb 19 2019 /etc/selinux/config
$ ls -l /etc/sysconfig/selinux
lrwxrwxrwx. 1 root root 17 Feb 19 2019 /etc/sysconfig/selinux -> ../selinux/config
次のようにシンボリックリンクに対して実行すると、リンクが切れて別ファイルになり、SELinuxは無効にならない。
sed -i -e 's/^\SELINUX=enforcing/SELINUX=disabled/' /etc/sysconfig/selinux
3-2-2. シンボリックリンクに対するsed -iの挙動を確かめる
実際にどのような動作になるのか試してみよう。cloud-initはroot権限で実行されるので、今回は意図的にrootユーザーを使用している。
- 事前準備として、テスト用のファイル
testfile1
に対してシンボリックリンクtestfile2
を作成する。
# echo "foo" > testfile1
# ln -s testfile1 testfile2
# ls -l
total 72
-rw-rw-r--. 1 opc opc 5 Dec 13 22:33 testfile1
lrwxrwxrwx. 1 opc opc 9 Dec 13 22:34 testfile2 -> testfile1
2.この段階ではシンボリックリンクなので中身は同じ。
# grep foo testfile*
testfile1:foo
testfile2:foo
3.シンボリックリンクに対してsedで置換を実施する。
# sed -i -e 's/^foo/foo2/' testfile2
4.シンボリックリンクtestfile2
は通常ファイルになっている。
# ls -l
-rw-rw-r--. 1 opc opc 5 Dec 13 22:42 testfile1
-rw-rw-r--. 1 opc opc 6 Dec 13 22:44 testfile2 ★シンボリックリンクではない
5.ファイルの中身を確認すると、sedの置換対象のtestfile2
だけが変わっている。
# grep test testfile*
testfile1:foo
testfile2:foo2
このような特性があるので、sedで置換するときは必ずシンボリックリンクではなく実体を指定する。
もしくは次のように--follow-symlinks
オプションを指定してもリンク切れを回避できる。実体に対して、このオプションを指定しても問題は起きない。そのため必ず指定するようにしてもいいかもしれない。
# sed -i --follow-symlinks -e 's/^test/test2/' testfile2
# ls -l
total 72
-rw-rw-r--. 1 opc opc 5 Dec 13 22:33 testfile1
lrwxrwxrwx. 1 opc opc 9 Dec 13 22:34 testfile2 -> testfile1
それとは別に-i"サフィックス"
を指定すると、バックアップファイルを自動的に作成できる。次のように指定するとfoo.bak
が作成される。
# sed -i".bak" -e 's/^test/test2/' foo
3-3. シェルスクリプトの文法チェック
シェルスクリプトはインタプリタ言語なので、そのコードパスを通るまでエラーがあるかわからない。そこで活用したいのが文法チェッカーだ。よく知られているのはbash -n
と、WebサイトのShellCheckがある。
以下のサンプルコードで試してみよう。
#!/bin/bash
VAL=1
if [ $VAL -eq 1 ] ; then
echo "True"
else
echo "False"
if ★ここが間違っている
bash -nによる文法チェック
上記のコードをbash -n
で実行すると最終行にエラーがあることがわかる。
$ bash -n test.sh
test.sh: 行 13: 構文エラー: 予期しないファイル終了 (EOF) です
ShellCheckによる文法チェック
bash -n
よりもインテリジェントなのが**ShellCheck**だ。bash -n
は抜け漏れもあるのだが、こちらはもっと厳密なチェックや推奨まで表示できる。次は実際の実行例だ。
3-4. YAMLの書式チェック
cloud-config形式のときは**「YAML形式になっていること」「先頭が#cloud-configで始まっていること」**を確認する。YAMLは「キー:␣値」という形式で記述するため、コロン(:)+ 空白 が含まれているときはダブルクォーテーションで囲む必要がある。
短いなら目視チェックでもよいが「YAML Lint」などのWebサイトを活用しよう。
runcmd:
- echo a : b
runcmd:
- "echo a : b"
4. cloud-initの仕組みを理解する:設定ファイル編
cloud-initの仕組みを理解するために、はじめに設定ファイルを説明する。
4.1. cloud-initの設定ファイル
cloud-initの設定ファイルは以下の2カ所にある。
1. /etc/cloud/cloud.cfg
2. /etc/cloud/cloud.cfg.d/*.cfg
ここで重要なのは、上記の順序で評価し、/etc/cloud/cloud.cfg.d/
配下のファイルは辞書順(数字、アルファベット)で評価されることだ。
また設定ファイルの数や内容は、使用しているクラウドやオペレーティング・システムによって異なる。AWSとAzure、Oracle Cloud Infrastructureで調べた結果は以下のとおり。
Amazon EC2
$ find /etc/cloud/ -name "*cfg" | sort
/etc/cloud/cloud.cfg
/etc/cloud/cloud.cfg.d/05_logging.cfg
/etc/cloud/cloud.cfg.d/10_aws_yumvars.cfg
Oracle Cloud Infrastructure Compute
$ find /etc/cloud/ -name "*cfg" | sort
/etc/cloud/cloud.cfg
/etc/cloud/cloud.cfg.d/05_logging.cfg
/etc/cloud/cloud.cfg.d/99_oci_cloud.cfg
$ find /etc/cloud/ -name "*cfg" | sort
/etc/cloud/cloud.cfg
/etc/cloud/cloud.cfg.d/05_logging.cfg
/etc/cloud/cloud.cfg.d/99_oci.cfg
$ find /etc/cloud/ -name "*cfg" | sort
/etc/cloud/cloud.cfg
/etc/cloud/cloud.cfg.d/05_logging.cfg
/etc/cloud/cloud.cfg.d/90_dpkg.cfg
/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
/etc/cloud/cloud.cfg.d/99-oracle-compute-infra-datasource.cfg
/etc/cloud/cloud.cfg.d/99-oracle-compute-user-redirect.cfg
Azure VM
$ find /etc/cloud/ -name "*cfg" | sort
/etc/cloud/cloud.cfg
/etc/cloud/cloud.cfg.d/05_logging.cfg
$ find /etc/cloud/ -name "*cfg" | sort
/etc/cloud/cloud.cfg
/etc/cloud/cloud.cfg.d/05_logging.cfg
/etc/cloud/cloud.cfg.d/10-azure-kvp.cfg
/etc/cloud/cloud.cfg.d/90-azure.cfg
/etc/cloud/cloud.cfg.d/90_dpkg.cfg
おまけ
これまで説明したとおり、クラウドやオペレーティング・システムによって設定ファイルは異なる。しかし基本はcloud-initパッケージに含まれる2つのファイルである。
$ rpm -ql cloud-init | grep .cfg$
/etc/cloud/cloud.cfg
/etc/cloud/cloud.cfg.d/05_logging.cfg
上記2つ以外のファイルはどこから来ているのか調べると、クラウドによって異なるようだ。
$ rpm -qf /etc/cloud/cloud.cfg.d/10_aws_yumvars.cfg
system-release-2-11.amzn2.x86_64
Oracle Linux 7はパッケージに含まれていない
$ rpm -qf /etc/cloud/cloud.cfg.d/99_oci_cloud.cfg
file /etc/cloud/cloud.cfg.d/99_oci_cloud.cfg is not owned by any package
Oracle Linux 8ではパッケージに含まれている
$ rpm -qf /etc/cloud/cloud.cfg.d/99_oci.cfg
oci-linux-config-2.0-1.0.13.el8.noarch
4.2. cloud-initの設定ファイルを解読する(基礎編)
これまでcloud-initに、どのような設定ファイルがあるか、どのような順序で評価されるかを説明した。次に実際の設定ファイルを見ながら具体的に説明する。
以下の/etc/cloud/cloud.cfgを軽く眺めてもらいたい。
users:
- default
disable_root: 1
ssh_pwauth: 0
mount_default_fields: [~, ~, 'auto', 'defaults,nofail', '0', '2']
resize_rootfs_tmp: /dev
ssh_deletekeys: 0
ssh_genkeytypes: ~
syslog_fix_perms: ~
datasource_list: ['OpenStack']
datasource:
OpenStack:
metadata_urls: ['http://169.254.169.254']
timeout: 10
max_wait: 20
cloud_init_modules: ★ひとかたまりの実行単位で、これが1つのステージ。
この下に書かれているのが、このステージで実行されるモジュール群。
- migrator
- bootcmd
- write-files
# The growpart module is disabled by default. To enable automatic boot volume resizing, uncomment
# the below entry for '- growpart' and reboot. All the dependent packages for the growpart
# module to work such as cloud-utils-growpart and gdisk are already included in the image.
#- growpart
- resizefs
- rsyslog
- users-groups
- ssh
cloud_config_modules: ★cloud_configステージ
- mounts
- locale
- set-passwords
- yum-add-repo
- package-update-upgrade-install
- timezone
- puppet
- chef
- salt-minion
- mcollective
- disable-ec2-metadata
- runcmd
cloud_final_modules: ★cloud_finalステージ
- rightscale_userdata
- scripts-per-once
- scripts-per-boot
- scripts-per-instance
- scripts-user
- ssh-authkey-fingerprints
- keys-to-console
- phone-home
- final-message
system_info:
default_user:
name: opc
lock_passwd: true
gecos: Oracle Public Cloud User
groups: [wheel, adm, systemd-journal]
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
shell: /bin/bash
distro: rhel
paths:
cloud_dir: /var/lib/cloud
templates_dir: /etc/cloud/templates
ssh_svcname: sshd
# vim:syntax=yaml
コメントを書いたので、大まかに理解できたと思うが、設定ファイルからわかることは以下のとおり。
**1点目:**YAML形式で記述されている。
**2点目:**cloud-initの各モジュールは、いくつかの単位(ステージ)にまとめられ、以下の順序で実行される。
- cloud_init_modules
- cloud_config_modules
- cloud_final_modules
**3点目:**ステージごとに記述されているモジュールが実行される。つまり、設定ファイルに記述していないモジュールをUser-Dataに書いても実行されない。
前回power_stateが動かなかった根本原因はここにある。cloud-initパッケージにpower_stateモジュールは含まれているが、設定ファイルに書かれていないので実行されなかったのだ。
★cloud-initにpower_stateは含まれている
$ rpm -ql cloud-init | grep power_state
/usr/lib/python2.7/site-packages/cloudinit/config/cc_power_state_change.py
/usr/lib/python2.7/site-packages/cloudinit/config/cc_power_state_change.pyc
/usr/lib/python2.7/site-packages/cloudinit/config/cc_power_state_change.pyo
注意:
cloud-initの設定ファイルやcloud-initのバージョンは、使用しているクラウドやオペレーティングシステムによって異なる。そのためpower_stateが使える場合もあれば、上記以外のモジュールを使えることもある。
4.3. cloud-initの設定ファイルを解読する(応用編)
本当に有効な設定を理解するには、これまでの知識に加えて、優先順位のメカニズムを理解する必要がある。設定ファイルは以下の順序で評価され、同じキーがあるときには最後の値が評価される。
- /etc/cloud/cloud.cfg
- /etc/cloud/cloud.cfg.d/*.cfg(辞書順)
別の言い方をすると以下のとおり。
- /etc/cloud/cloud.cfgだけでは、本当に有効な設定は判断できない
- 同じキーが複数のファイルにあるときは、最後のファイルの「キー:値」の組み合わせが有効になる
以下の実行例は、Oracle Cloud InfrastructureでOracle Linux 7を使用しているときのものだ。**キー「cloud_config_modules」**は/etc/cloud/cloud.cfg.d/99_oci_cloud.cfgにもあるので、実際に有効になっているのは、こちらのファイルに書かれた値である。
$ find /etc/cloud/ -name "*cfg" | xargs grep ^cloud_config_modules
/etc/cloud/cloud.cfg:cloud_config_modules:
/etc/cloud/cloud.cfg.d/99_oci_cloud.cfg:cloud_config_modules:
diffを取ってみると、いくつか書き換えていることがわかる。この例ではcloud.cfgを99_oci_cloud.cfgにコピーしてカスタマイズしているので、cloud.cfgの設定はすべて上書きされている。
Oracle Cloud Infrastructureでは、/etc/cloud/cloud.cfg.d/99_oci_cloud.cfgが主要な設定ファイルとして使われている。
$ diff -uNr /etc/cloud/cloud.cfg /etc/cloud/cloud.cfg.d/99_oci_cloud.cfg
--- /etc/cloud/cloud.cfg 2019-08-07 12:50:39.000000000 +0900
+++ /etc/cloud/cloud.cfg.d/99_oci_cloud.cfg 2019-11-07 12:20:39.942013483 +0900
@@ -1,3 +1,5 @@
+# Cloud.cfg file with merged content from cloud-init 18.5.1.
+# Aspects not used in OCI have been commented out.
users:
- default
@@ -9,18 +11,28 @@
ssh_deletekeys: 0
ssh_genkeytypes: ~
syslog_fix_perms: ~
-disable_vmware_customization: false
+#disable_vmware_customization: false
+
+datasource_list: ['Oracle', 'OpenStack']
+datasource:
+ OpenStack:
+ metadata_urls: ['http://169.254.169.254']
+ timeout: 10
+ max_wait: 20
cloud_init_modules:
- - disk_setup
+#-disk-setup
- migrator
- bootcmd
- write-files
- - growpart
+# The growpart module is disabled by default. To enable automatic boot volume resizing, uncomment
+# the below entry for '- growpart' and reboot. All the dependent packages for the growpart
+# module to work such as cloud-utils-growpart and gdisk are already included in the image.
+#- growpart
★以下省略
またcloud-initのお作法として「/etc/cloud/cloud.cfgを直接書き換えてはいけない」ことがある。
Oracle Cloud Infrastructureでは変更点が多いのでファイルを全部コピーしていた。しかし自分でカスタマイズするときは、変更する部分だけを書いて、/etc/cloud/cloud.cfg.d/配下に配置すればよい。
cloud_final_modules:
- scripts-per-once
- scripts-per-boot
- scripts-per-instance
- scripts-user
- ssh-authkey-fingerprints
- keys-to-console
- phone-home
- final-message
- power-state-change
Amazon EC2の場合(Amazon Linux 2)
Amazon EC2では、設定ファイル/etc/cloud/cloud.cfg.d/10_aws_yumvars.cfgを追加している。しかし、キー「write_metadata」は/etc/cloud/cloud.cfgに無いので、キーの上書きを考慮する必要は無い。
$ find /etc/cloud/ -name "*cfg" | xargs grep ^[a-zA-Z]
/etc/cloud/cloud.cfg.d/10_aws_yumvars.cfg:write_metadata:★追加したキー
/etc/cloud/cloud.cfg.d/05_logging.cfg:log_cfgs:
/etc/cloud/cloud.cfg.d/05_logging.cfg:output: {all: '| tee -a /var/log/cloud-init-output.log'}
/etc/cloud/cloud.cfg:users:
/etc/cloud/cloud.cfg:disable_root: true
/etc/cloud/cloud.cfg:ssh_pwauth: false
/etc/cloud/cloud.cfg:mount_default_fields: [~, ~, 'auto', 'defaults,nofail', '0', '2']
/etc/cloud/cloud.cfg:resize_rootfs: noblock
/etc/cloud/cloud.cfg:resize_rootfs_tmp: /dev
/etc/cloud/cloud.cfg:ssh_deletekeys: false
/etc/cloud/cloud.cfg:ssh_genkeytypes: ~
/etc/cloud/cloud.cfg:syslog_fix_perms: ~
/etc/cloud/cloud.cfg:datasource_list: [ Ec2, None ]
/etc/cloud/cloud.cfg:repo_upgrade: security
/etc/cloud/cloud.cfg:repo_upgrade_exclude:
/etc/cloud/cloud.cfg:network:
/etc/cloud/cloud.cfg:cloud_init_modules:
/etc/cloud/cloud.cfg:cloud_config_modules:
/etc/cloud/cloud.cfg:cloud_final_modules:
/etc/cloud/cloud.cfg:system_info:
/etc/cloud/cloud.cfg:mounts:
5. cloud-initの仕組みを理解する:その他のファイル編
設定ファイル以外の主要なファイルやサービスを説明する。オフィシャルのcloud-init FAQには、情報がコンパクトにまとまっている。興味のある方は読んでみてはどうだろう。
- cloud-initのログファイル
- cloud-initに指定したUser-Data
- cloud-initを構成するサービス
5-1. cloud-initのログファイル
cloud-initには以下のログファイルがある。
- /var/log/cloud-init.log
- 詳細なデバッグ情報を含んだログ。とても長い
- /var/log/cloud-init-output.log
- cloud-initを実行したときの簡易ログ。環境によって無いこともある
- /var/run/cloud-init/配下のファイル
- cloud-init自身が実行するかどうかを判断したときの情報を含んだログ
上記の中でデバッグに使いやすいのはcloud-init-output.logだ。必要な情報だけがまとまっている。ただし使用環境によっては無いこともあるので、そのときはcloud-init.logを参照する。
5-1-1. cloud-init終了の確認方法
cloud-initは、SSHでログインできても実行途中のことがある。ログファイルをtail -F
することで、終了したことを確認できる。次のようにmodules-final
やfinish
と表示されれば終了している。
Jan 28 02:20:08 cloud-init[3468]: handlers.py[DEBUG]: finish: modules-final: SUCCESS: running modules for final
Cloud-init v. 18.5-2.amzn2 running 'modules:final' at Tue, 28 Jan 2020 02:20:08 +0000. Up 22.21 seconds.
Cloud-init v. 18.5-2.amzn2 finished at Tue, 28 Jan 2020 02:20:08 +0000. Datasource DataSourceEc2. Up 22.75 seconds
5-2. cloud-initに指定したUser-Data
インスタンス作成時に指定したUser-Dataは以下のファイルに格納されている。
/var/lib/cloud/instances/*/user-data.txt
途中にアスタリスクを使っているのは、アスタリスクの部分はインスタンスIDで、インスタンスごとに異なるためだ。以下の例はOCIで実施しているが、AWSでもほとんど同じ結果だ。
- /var/lib/cloud/instances/を確認するとインスタンスIDがある。
$ ls -l /var/lib/cloud/instances/
total 4
drwxr-xr-x. 5 root root 4096 Jan 28 21:41 ocid1.instance.oc1.ap-tokyo-1.anxhiljripjhrziczokbsclxvsmfff6tviwuwotdjqbky46iXXXXXXXXX
2.サブディレクトリには、さまざまなファイルがある。
$ ls -l /var/lib/cloud/instances/*/
total 36
-rw-r--r--. 1 root root 50 Jan 28 21:41 boot-finished
-rw-------. 1 root root 126 Jan 28 21:40 cloud-config.txt
-rw-r--r--. 1 root root 35 Jan 28 21:40 datasource
drwxr-xr-x. 2 root root 6 Jan 28 21:40 handlers
-r--------. 1 root root 8129 Jan 28 21:40 obj.pkl
drwxr-xr-x. 2 root root 20 Jan 28 21:40 scripts
drwxr-xr-x. 2 root root 4096 Jan 28 21:41 sem
-rw-------. 1 root root 92 Jan 28 21:40 user-data.txt
-rw-------. 1 root root 433 Jan 28 21:40 user-data.txt.i
-rw-------. 1 root root 0 Jan 28 21:40 vendor-data.txt
-rw-------. 1 root root 344 Jan 28 21:40 vendor-data.txt.i
3.これらのなかでuser-data.txtに、指定したUser-Dataが格納されている。
$ sudo cat /var/lib/cloud/instances/*/user-data.txt
#cloud-config
timezone: Asia/Tokyo
runcmd:
- systemctl restart rsyslog
5-3. cloud-initを構成するサービス
cloud-initは複数のサービスで構成されている。それぞれの役割や、実行順序の依存関係は覚えておきたい。
# systemctl list-units --type=service | grep ^cloud-
cloud-config.service loaded active exited Apply the settings specified in cloud-config
cloud-final.service loaded active exited Execute cloud user/final scripts
cloud-init-local.service loaded active exited Initial cloud-init job (pre-networking)
cloud-init.service loaded active exited Initial cloud-init job (metadata service cr
systemd-analyze plot
コマンドを使うと、systemdのブートシーケンスを解析できる。
# systemd-analyze plot >bootup.svg
出力したSVG(Scalable Vector Graphics)ファイルをブラウザで表示し、一部を切り取ったものが以下の図だ。拡大しないとわからないが、黄色枠で囲んでいるのがcloud-initを構成するサービスだ。
上記の解析結果をまとめると、以下の順序で実行している。
- cloud-init-local.service
- ネットワークを起動
- cloud-init.service
- cloud-config.service
- すべてのサービスが起動
- cloud-final.service
これまでの設定ファイルの解析と併せると、サービスの役割は以下のとおり。
サービス | 内容 |
---|---|
cloud-init-local.service | ネットワーク起動前のcloud-init初期化 |
cloud-init.service | ディスクパーティションの設定やOSユーザーの設定など基礎となる設定 |
cloud-config.service | ロケールやタイムゾーン、パッケージのインストール/アップデート、ChefやPuppet、runcmdなどの設定 |
cloud-final.service | 再起動やメッセージの表示など、すべての設定が終わったあとに実行する作業 |
おわりに
cloud-initの仕組みについて、だいぶ理解が深まったのではないだろうか。次回は、インスタンスを作成しないでcloud-initを実行する方法や、高度な解析方法を説明する予定だ(どこまで書けるか微妙だけど)。
つづく