CloudFormation、便利ですよね。インフラのコード化はインフラの状態の変化を可視化できるというたいへん素晴らしいメリットがあります。くわえてCloudFormationはスタック単位で作成・削除ができるため、WEBコンソールから手動で作成・削除するときに起こりうる「不要なリソースが残ったまま」や「必要なリソースを削除してしまう」というリスクが減ることで心理的・金銭的なコストが軽減される点がわたしは気に入っています。

さて当エントリでは「CloudFormationを使ってEC2インスタンスを立ち上げる」という基本的な作業において気になったUbuntuの起動システムについて調査した経過を記します。

前提

本番環境で稼働しているUbuntuベースのインスタンス(以後「稼働インスタンス」と呼びます)が存在します。この稼働インスタンスと同等のインスタンス(以後「テストインスタンス」と呼びます)を作成し、あるテストをおこないたいと思います。 そこで、稼働インスタンスのAMIをWEBコンソールから作成し、このAMIをもとにテストインスタンスをCloudFormationから作成することにしました。この際、稼働インスタンスでは各種デーモンが動いているのですがテストインスタンスにおいてはこれらを動かしたくありません。

どうする?

CloudFormationには各種ヘルパースクリプトが用意されています。

ヘルパースクリプトは Amazon Linux AMI の最新バージョンにプレインストールされています。他の UNIX/Linux AMI で使用するために、Amazon Linux yum リポジトリから入手することもできます。

http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-helper-scripts-reference.html

これらはAmazon Linux AMIですとプレインストールされていますがUbuntuベースのAMIですと意図的にインストールしてやる必要があります。 このヘルパースクリプトのうちcfn-initというスクリプトを使えばテストインスタンス起動時のデーモンの起動を無効にすることができそうです。

cfn-init ヘルパースクリプトは、AWS::CloudFormation::Init キーからテンプレートメタデータを読み取り、それに応じて次のような操作を行います。

  • CloudFormation のメタデータの取得と解析
  • パッケージのインストール
  • ディスクへのファイルの書き込み
  • サービスの有効化/無効化と開始/停止

http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-init.html

具体的にはAWS::CloudFormation::Initキーにおいてservicesキーを定義することでサービスの起動設定を行えるようです。 このとき、

Linux システムでは、このキーは sysvinit を使用してサポートされています。

http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-init.html#aws-resource-init-services

とありますが、ここで sysvinit とはなにを意味するのか?よくわかりませんので調べてみました。

sysvinit とは?

sysvinitはSystemV initの略で,UNIX SystemV(システムファイブ)と呼ばれるAT&T社謹製の古典的なUNIXが採用した起動メカニズムと同じ動作をするように設計されたソフトウェアです。

http://gihyo.jp/dev/serial/01/sc-literacy/0013

とありました。さらに調べるとこの仕組みはUbuntuにおいてはUbuntu 6.10 (Edgy Eft)においてUpstart に、 Ubuntu 15.04 (Vivid Vervet)においてsystemd に取って代わられたとのことです。 ということは、Ubuntuにおける直近のLTSリリースである14.0416.04sysvinitをサポートしていないのでしょうか?これではcfn-initは使えません。 とりあえずトライしてみることにしました。

cronデーモンを例にトライ

以下のようなテンプレートを用意しました。少々長いですが引用します。

 1 ---
 2 Description: sysvinit test
 3 Parameters:
 4   KeyName:
 5     Type: String
 6   VpcId:
 7     Type: String
 8   SubnetId:
 9     Type: String
10   Image:
11     Description: AMI
12     Type: String
13     AllowedValues:
14     - ami-8c4055eb # Ubuntu Server 14.04
15     - ami-785c491f # Ubuntu Server 16.04
16     Default: ami-785c491f # Ubuntu Server 16.04
17 Resources:
18   WebAccessGroup:
19     Type: AWS::EC2::SecurityGroup
20     Properties:
21       GroupDescription: Enable outgoing HTTP(S) access
22       SecurityGroupEgress:
23       - IpProtocol: tcp
24         FromPort: '80'
25         ToPort: '80'
26         CidrIp: 0.0.0.0/0
27       - IpProtocol: tcp
28         FromPort: '443'
29         ToPort: '443'
30         CidrIp: 0.0.0.0/0
31       VpcId: !Ref VpcId
32   SSHGroup:
33     Type: AWS::EC2::SecurityGroup
34     Properties:
35       GroupDescription: Enable global SSH access
36       SecurityGroupIngress:
37       - IpProtocol: tcp
38         CidrIp: 0.0.0.0/0
39         FromPort: 22
40         ToPort: 22
41       SecurityGroupEgress:
42       - IpProtocol: tcp
43         CidrIp: 0.0.0.0/0
44         FromPort: 22
45         ToPort: 22
46       VpcId: !Ref VpcId
47   TestServer:
48     Type: AWS::EC2::Instance
49     Properties:
50       ImageId: !Ref Image
51       InstanceType: t2.micro
52       KeyName: !Ref KeyName
53       AvailabilityZone: ap-northeast-1a
54       SubnetId: !Ref SubnetId
55       SecurityGroupIds:
56         - !Ref SSHGroup
57         - !Ref WebAccessGroup
58       UserData:
59         Fn::Base64:
60           Fn::Sub:
61           - |
62             #!/bin/bash -xe
63             apt-get update
64             apt-get -y install python-pip
65             pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz
66             cp -a /usr/local/init/ubuntu/cfn-hup /etc/init.d/cfn-hup
67
68             chmod u+x /etc/init.d/cfn-hup
69             /usr/local/bin/cfn-init -v \
70                      --stack ${StackName} \
71                      --region ${Region} \
72                      --resource TestServer
73           - StackName: {Ref: "AWS::StackName"}
74             Region: {Ref: "AWS::Region"}
75     Metadata:
76       AWS::CloudFormation::Init:
77         config:
78           services:
79             sysvinit:
80               cron:
81                 enabled: 'false'
82                 ensureRunning: 'false'

このテンプレートにおいて今回の肝は58~74行目のUserDataキーと75行目以降のMetadataキーになります。 UserDataにてcfn-initヘルパースクリプトをインストールし、停止したいサービスを列挙したMetadataキー配下をcfn-init使用時に指定することで、テストインスタンス起動時にcronデーモンを停止することを目的としています。

たとえばcfn-initを作動させた状態でUbuntu 14.04テストインスタンスを立ち上げたいときはaws-cliを用いて以下のように実行することで試すことができます。ImageパラメータにUbuntu 14.04のAMIを指定していることに注意してください。

$ aws cloudformation create-stack \
    --stack-name cfn-init-test-ubuntu-14-cron-off \
    --template-body file:///path/to/cfn-init-test.template \
    --parameter ParameterKey=Image,ParameterValue=ami-8c4055eb \
                ParameterKey=KeyName,ParameterValue=key-name \
                ParameterKey=VpcId,ParameterValue=vpc-id \
                ParameterKey=SubnetId,ParameterValue=subnet-id

同様にcfn-initを作動させない状態でUbuntu 16.04テストインスタンスを立ち上げたいときは58行目以降を削除したのちに、

- 58       UserData:
- 59         Fn::Base64:
- 60           Fn::Sub:
- 61           - |
- 62             #!/bin/bash -xe
- 63             apt-get update
- 64             apt-get -y install python-pip
- 65             pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz
- 66             cp -a /usr/local/init/ubuntu/cfn-hup /etc/init.d/cfn-hup
- 67
- 68             chmod u+x /etc/init.d/cfn-hup
- 69             /usr/local/bin/cfn-init -v \
- 70                      --stack ${StackName} \
- 71                      --region ${Region} \
- 72                      --resource TestServer
- 73           - StackName: {Ref: "AWS::StackName"}
- 74             Region: {Ref: "AWS::Region"}
- 75     Metadata:
- 76       AWS::CloudFormation::Init:
- 77         config:
- 78           services:
- 79             sysvinit:
- 80               cron:
- 81                 enabled: 'false'
- 82                 ensureRunning: 'false'

先述のようにaws-cliを用いて以下のように実行します。ImageパラメータにUbuntu 16.04のAMIを指定していることに注意してください。

$ aws cloudformation create-stack \
    --stack-name cfn-init-test-ubuntu-16-cron-off \
    --template-body file:///path/to/cfn-init-test.template \
    --parameter ParameterKey=Image,ParameterValue=ami-785c491f \
                ParameterKey=KeyName,ParameterValue=key-name \
                ParameterKey=VpcId,ParameterValue=vpc-id \
                ParameterKey=SubnetId,ParameterValue=subnet-id

ちなみにスタックが作成されるとWEBコンソールから以下のような表示が見られると思います。 cloudformation_

このように、各バージョンのUbuntuにおいてcfn-initの有効・無効を切り替えて試してみました。 結果、cfn-initを作動させた場合はcronデーモンは停止状態で、cfn-initを作動させない場合はcronデーモンは稼働状態でした。 これはUbuntu 14.04Ubuntu 16.04の双方で同様の現象でした。これらはsysvinitを採用していないはずなのになぜでしょう? まずは各々の実際に稼働した起動システムを調べてみます。

Ubuntu 14.04 の起動システム

ubuntu14$ sudo stat /proc/1/exe
  File: '/proc/1/exe' -> '/sbin/init'
  Size: 0               Blocks: 0          IO Block: 1024   symbolic link
Device: 3h/3d   Inode: 9323        Links: 1
Access: (0777/lrwxrwxrwx)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2017-06-28 07:43:10.904461000 +0000
Modify: 2017-06-28 06:03:36.184461000 +0000
Change: 2017-06-28 06:03:36.184461000 +0000
 Birth: -
ubuntu14$ /sbin/init --version
init (upstart 1.12.1)   # <= upstart
Copyright (C) 2006-2014 Canonical Ltd., 2011 Scott James Remnant

This is free software; see the source for copying conditions.  There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
ubuntu@ip-172-31-25-228:~$

Ubuntu 16.04 の起動システム

ubuntu16$ sudo stat /proc/1/exe
  File: '/proc/1/exe' -> '/lib/systemd/systemd'   # <= systemd
  Size: 0               Blocks: 0          IO Block: 1024   symbolic link
Device: 4h/4d   Inode: 9340        Links: 1
Access: (0777/lrwxrwxrwx)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2017-06-28 06:13:46.476000000 +0000
Modify: 2017-06-28 06:13:46.472000000 +0000
Change: 2017-06-28 06:13:46.472000000 +0000
 Birth: -

調べたところ予想どおりUbuntu 14.04ではUpstartが、Ubuntu 16.04ではsystemdが採用されていることがわかります。 どうしてsysvinitでないのにcfn-initは所望の動作をしてくれたのでしょうか?

cfn-initのソースを追っていくとサービスの有効・無効化にはupdate-rc.dを使用している ことがわかりました。 ログからもその動作が垣間見えます。

# /var/log/cfn-init.log@ubuntu14
2017-06-28 06:14:28,533 [DEBUG] Using service modifier: /usr/sbin/update-rc.d
2017-06-28 06:14:28,533 [DEBUG] Setting service cron to disabled
2017-06-28 06:14:28,639 [INFO] disabled service cron
2017-06-28 06:14:28,639 [DEBUG] Using service runner: /usr/sbin/service
2017-06-28 06:14:28,654 [DEBUG] Stopping service cron as it is running
2017-06-28 06:14:28,778 [INFO] Stopped cron successfully
2017-06-28 06:14:28,779 [INFO] ConfigSets completed
# /var/log/cfn-init.log@ubuntu16
2017-06-28 06:04:00,068 [DEBUG] Using service modifier: /usr/sbin/update-rc.d
2017-06-28 06:04:00,069 [DEBUG] Setting service cron to disabled
2017-06-28 06:04:00,080 [INFO] disabled service cron
2017-06-28 06:04:00,081 [DEBUG] Using service runner: /usr/sbin/service
2017-06-28 06:04:00,087 [DEBUG] Stopping service cron as it is running
2017-06-28 06:04:00,098 [INFO] Stopped cron successfully
2017-06-28 06:04:00,099 [INFO] ConfigSets completed

update-rc.dは下記のとおり、System V スタイルの起動設定をサポートしているとのこと。その意味ではcfn-initの説明と合致します。

update-rc.d updates the System V style init script links /etc/rcrunlevel.d/NNname whose target is the script /etc/init.d/name. These links are run by init when it changes runlevels; they are generally used to start and stop system services such as daemons. runlevel is one of the runlevels supported by init, namely, 0123456789S, and NN is the two-digit sequence number that determines where in the sequence init will run the scripts.

http://manpages.ubuntu.com/manpages/precise/man8/update-rc.d.8.html

残る疑問

さきほど調べたとおり今回のテストサーバの起動システムはUpstartおよびsystemdでした。sysvinitとは合致しません。どうしてcronデーモンの停止は実現したのでしょうか?またcronデーモン以外のサービスでも同様の動きになるでしょうか?

まとめ

今回はここまでです。次回は先述の疑問を解明すべくさらにいろいろ試したいと思います。