AmazonLinux にApacheHTTPをインストールしてAMIを作成したいと思います。
今回は、以下の手順で、作業することにします。
- AmazonLinux のAMIからEC2インスタンスを起動
- EC2インスタンスにSSHログイン
- ApacheHTTPをインストール
- httpサービスの起動確認
- EC2インスタンスを停止
- AMIを作成
- EC2インスタンスをターミネート
手作業だと、AWSコンソールや Teraterm を使うことになりますが、
これを、Packer+Ansible+Serverspec で自動化し、Packerのコマンド1回で済むようにしてみます。
Packerで1~7までの作業を実施しますが、
3では、Ansible のプレイブックを実行し、4では、Serverspec を実行します。
1.作業用のサーバを準備する
Packer+Ansible+Serverspecを実行するサーバを用意します。
今回は、AmazonLinuxを使用します。
なお、全ての作業は、ec2-user ユーザで行い、~/workspace ディレクトリを作って、そこに、Packer、Ansible、Serverspec のファイルを置くことにします。
1.1.作業用のEC2インスタンスを作成
今回、作業用に使用するEC2インスタンスには、管理者権限を付与したIAMロールを割り当てます。
これにより、Packer でAMIを作成するときに、アクセスキーの設定が不要になります。
1.2.Packer のインストール
Packer のダウンロードサイトからバイナリのコマンドをダウンロードして解凍し、/usr/local/bin に置きます。
[ec2-user@ip-10-0-11-158 ~]$ curl -OL https://releases.hashicorp.com/packer/0.10.1/packer_0.10.1_linux_amd64.zip
[ec2-user@ip-10-0-11-158 ~]$ unzip ./packer_0.10.1_linux_amd64.zip
[ec2-user@ip-10-0-11-158 ~]$ ls
packer packer_0.10.1_linux_amd64.zip
[ec2-user@ip-10-0-11-158 ~]$ sudo mv ./packer /usr/local/bin/.
バージョンは以下のとおり。
[ec2-user@ip-10-0-11-158 ~]$ packer --version
0.10.1
1.3.Ansible のインストール
pipコマンドで Ansible をインストールします。
[ec2-user@ip-10-0-11-158 ~]$ sudo pip install ansible
バージョンは以下のとおり。
[ec2-user@ip-10-0-11-158 ~]$ ansible --version
ansible 2.1.0.0
config file =
configured module search path = Default w/o overrides
設定は以下のとおり。
[ec2-user@ip-10-0-11-158 ~]$ cat ~/.ansible.cfg
[defaults]
retry_files_enabled = False
host_key_checking = False
log_path=~/workspace/logs/ansible.log
[ssh_connection]
scp_if_ssh = True
1.4.Serverspec のインストール
gem コマンドで Serverspec をインストールします。
[ec2-user@ip-10-0-11-158 ~]$ sudo gem install serverspec
[ec2-user@ip-10-0-11-158 ~]$ sudo gem install rake
バージョンは以下のとおり。
[ec2-user@ip-10-0-11-158 ~]$ gem list --local | grep serverspec
serverspec (2.36.0)
2.Ansibleのプレイブックを作成する
Ansible のプレイブックを作成します。
yum で httpd をインストールし、自動起動をONにしてサービスを開始します。なお、httpがインストールされるサーバのIPアドレスを確認するために、ipコマンドで取得した eth0 のIPアドレスを表示します。
[ec2-user@ip-10-0-11-158 workspace]$ cat playbook_dev001.yml
---
- hosts: all
become: yes
tasks:
- name: IPアドレス
shell: ip a | awk '($0~/inet .* eth0/){split($2,i,"/");print i[1]}'
register: ip_ret
- debug: var=ip_ret.stdout
- name: httpd インストール
yum: name=httpd state=present
- name: httpd の自動起動ON
service: name=httpd enabled=yes state=started
自分自身を対象にして、プレイブックをテストします。
[ec2-user@ip-10-0-11-158 workspace]$ ansible-playbook -i ,127.0.0.1 playbook_dev001.yml -u ec2-user --private-key=./virginia_key.pem
PLAY [all] *********************************************************************
TASK [setup] *******************************************************************
ok: [127.0.0.1]
TASK [IPアドレス] ******************************************************************
changed: [127.0.0.1]
TASK [debug] *******************************************************************
ok: [127.0.0.1] => {
"ip_ret.stdout": "10.0.11.158"
}
TASK [httpd インストール] ************************************************************
changed: [127.0.0.1]
TASK [httpd の自動起動ON] ***********************************************************
changed: [127.0.0.1]
PLAY RECAP *********************************************************************
127.0.0.1 : ok=5 changed=3 unreachable=0 failed=0
3.Packer+AnsibleでAMIを作成する
Ansibleのプレイブックができたので、Packer から起動したEC2インスタンスに Ansible のプレイブックを適用してAMIを作成してみます。
Packer のテンプレートは以下のようにました。
環境変数で、サーバ名、AMI-ID、EBSボリュームのサイズ(GB)、プレイブックを渡します。
[ec2-user@ip-10-0-11-158 workspace]$ cat packer.json
{
"variables": {
"server_name": "{{env `BUILD_SERVER_NAME`}}",
"ami": "{{env `BUILD_AMI`}}",
"volume_size": "{{env `BUILD_EBS_SIZE`}}",
"ansible_playbook": "{{env `BUILD_PLAYBOOK`}}"
},
"builders" : [{
"type" : "amazon-ebs",
"region" : "us-east-1",
"availability_zone" : "us-east-1b",
"instance_type" : "t2.nano",
"source_ami" : "{{user `ami`}}",
"launch_block_device_mappings": [
{
"device_name": "/dev/xvda",
"volume_size": "{{user `volume_size`}}",
"volume_type": "gp2",
"delete_on_termination": "true"
}
],
"subnet_id" : "subnet-f528a2ad",
"associate_public_ip_address" : "true",
"security_group_id": "sg-bbf176df",
"ssh_keypair_name" : "virginia_key",
"ssh_username" : "ec2-user",
"ssh_private_key_file" : "/home/ec2-user/workspace/virginia_key.pem",
"ssh_private_ip" : "true",
"ami_name" : "TEST-AMI-{{user `server_name`}}",
"ami_description" : "{{user `server_name`}} created by packer",
"tags" : {
"Name" : "{{user `server_name`}}",
"Environment" : "develop"
}
}],
"provisioners" : [
{
"type" : "ansible",
"playbook_file" : "{{user `ansible_playbook`}}"
}
]
}
キーペアのvirginia_keyはAWSコンソールで事前に作成しておきます。
サブネットやセキュリティグループは、作業用サーバと同じにして、プライベートIPアドレスで接続可能な環境を前提にしています。
また、yum を使用するので、Packer が一時的に起動するEC2インスタンスには パブリックIPアドレスを自動割り当てします。
続いて、Packe 実行用のシェルを作成します。
[ec2-user@ip-10-0-11-158 workspace]$ cat packer.sh
#!/bin/bash
server_name="dev001"
base_dir="/home/ec2-user/workspace"
packer_json="${base_dir}/packer.json"
packer_log="${base_dir}/logs/packer-${server_name}.log"
# Packer
export BUILD_PLAYBOOK="${base_dir}/playbook_${server_name}.yml"
export BUILD_SERVER_NAME="${server_name}"
export BUILD_AMI="ami-6869aa05"
export BUILD_EBS_SIZE="10"
env PACKER_LOG=1 PACKER_LOG_PATH=${packer_log} packer build ${packer_json}
exit 0
環境変数にAmazonLinux のAMI-IDなどを設定して、Packerを実行するようにし、詳細なログファイルを出力するようにしています。
これを実行すると以下のようになります。
[ec2-user@ip-10-0-11-158 workspace]$ ./packer.sh
amazon-ebs output will be in this color.
==> amazon-ebs: Prevalidating AMI Name...
==> amazon-ebs: Inspecting the source AMI...
==> amazon-ebs: Launching a source AWS instance...
amazon-ebs: Instance ID: i-1fc8af8f
==> amazon-ebs: Waiting for instance (i-1fc8af8f) to become ready...
==> amazon-ebs: Waiting for SSH to become available...
==> amazon-ebs: Connected to SSH!
==> amazon-ebs: Provisioning with Ansible...
==> amazon-ebs: Executing Ansible: ansible-playbook /home/ec2-user/workspace/playbook_dev001.yml -i /tmp/packer-provisioner-ansible182536530 --private-key /tmp/ansible-key107272703
==> amazon-ebs: SSH proxy: serving on 127.0.0.1:45242
amazon-ebs:
amazon-ebs: PLAY [all] *********************************************************************
amazon-ebs:
amazon-ebs: TASK [setup] *******************************************************************
amazon-ebs: SSH proxy: accepted connection
==> amazon-ebs: authentication attempt from 127.0.0.1:45682 to 127.0.0.1:45242 as ec2-user using none
==> amazon-ebs: authentication attempt from 127.0.0.1:45682 to 127.0.0.1:45242 as ec2-user using publickey
amazon-ebs: ok: [default]
amazon-ebs:
amazon-ebs: TASK [IPアドレス] ******************************************************************
amazon-ebs: changed: [default]
amazon-ebs:
amazon-ebs: TASK [debug] *******************************************************************
amazon-ebs: ok: [default] => {
amazon-ebs: "ip_ret.stdout": "10.0.11.39"
amazon-ebs: }
amazon-ebs:
amazon-ebs: TASK [httpd インストール] ************************************************************
amazon-ebs: changed: [default]
amazon-ebs:
amazon-ebs: TASK [httpd の自動起動ON] ***********************************************************
amazon-ebs: changed: [default]
amazon-ebs:
amazon-ebs: PLAY RECAP *********************************************************************
amazon-ebs: default : ok=5 changed=3 unreachable=0 failed=0
amazon-ebs:
==> amazon-ebs: shutting down the SSH proxy
==> amazon-ebs: Stopping the source instance...
==> amazon-ebs: Waiting for the instance to stop...
==> amazon-ebs: Creating the AMI: TEST-AMI-dev001
amazon-ebs: AMI: ami-10be3307
==> amazon-ebs: Waiting for AMI to become ready...
==> amazon-ebs: Modifying attributes on AMI (ami-10be3307)...
amazon-ebs: Modifying: description
==> amazon-ebs: Adding tags to AMI (ami-10be3307)...
amazon-ebs: Adding tag: "Name": "dev001"
amazon-ebs: Adding tag: "Environment": "develop"
==> amazon-ebs: Tagging snapshot: snap-4179b4a5
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Cleaning up any extra volumes...
==> amazon-ebs: No volumes to clean up, skipping
Build 'amazon-ebs' finished.
==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
us-east-1: ami-10be3307
Packer実行中は、下図のように "Packer Builder" という名称のEC2インスタンスが起動します。
この "Packer Builder" に対して、プレイブックが適用されます。
なお、AMI作成後は、ターミネートされます。
AMIは下図のように作成されています。
4.Serverspecを作成する
Serverspecで http のテストケースを作成します。
まず、以下のようにして serverspec の雛形を作成します。
[ec2-user@ip-10-0-11-158 workspace]$ mkdir serverspec
[ec2-user@ip-10-0-11-158 workspace]$ cd serverspec
[ec2-user@ip-10-0-11-158 serverspec]$ serverspec-init
Select OS type:
1) UN*X
2) Windows
Select number: 1
Select a backend type:
1) SSH
2) Exec (local)
Select number: 1
Vagrant instance y/n: n
Input target host name: packer-builder
+ spec/
+ spec/packer-builder/
+ spec/packer-builder/sample_spec.rb
+ spec/spec_helper.rb
+ Rakefile
+ .rspec
次に環境変数でSSH秘密鍵を指定できるように ~/workspace/serverspec/spec/spec_helper.rb を修正します。
以下の赤字部分を spec_helper.rb に追加します。
options = Net::SSH::Config.for(host)
options[:user] ||= Etc.getlogin
options[:keys] = ENV['TARGET_SSH_KEY']
set :host, options[:host_name] || host
set :ssh_options, options
テストケースは以下のとおりです。
httpdパッケージのインストール、自動起動ON、サービス起動中かをチェックします。
[ec2-user@ip-10-0-11-158 workspace]$ cat serverspec/spec/packer-builder/http_spec.rb
require 'spec_helper'
describe package('httpd') do
it { should be_installed }
end
describe service('httpd') do
it { should be_enabled }
it { should be_running }
end
describe port(80) do
it { should be_listening }
end
あとは、Packer が起動したEC2インスタンスに対して、Serverspec を実行するようにします。
いろいろ、調べたのですが、簡単な方法が見つかりませんでした。
しかたないので、Packer が起動したEC2インスタンスのIPアドレスをaws-cliで取得して、Serverspecを実行することにしました。
以下のような、Packer から Serverspec を実行するためのシェルを作成しました
[ec2-user@ip-10-0-11-158 workspace]$ cat serverspec.sh
#!/bin/bash
BASE_DIR=/home/ec2-user/workspace
SERVERSPEC_DIR=${BASE_DIR}/serverspec
ip=` \
aws ec2 describe-instances \
--region us-east-1 \
--filters "Name=tag-key,Values=Name" \
"Name=tag-value,Values=Packer Builder" \
"Name=instance-state-name,Values=running" \
--output text \
--query "Reservations[].Instances[].[LaunchTime,PrivateIpAddress]" \
| sort | tail -1 | cut -f2 \
`
ln -s ./packer-builder ${SERVERSPEC_DIR}/spec/${ip}
(
cd ${SERVERSPEC_DIR}
env TARGET_SSH_KEY=${BASE_DIR}/virginia_key.pem rake spec:${ip} > /tmp/xxx
)
rm -f ${SERVERSPEC_DIR}/spec/${ip} > /dev/null 2>&1
n=`cat /tmp/xxx | tail -2 | grep -c '0 failures'`
cat /tmp/xxx
rm -f /tmp/xxx
ret=0
if [ $n -eq 0 ]; then
ret=1
fi
exit $ret
aws-cli で "Nameタグ"が"Packer Builder"のインスタンスのなかから、起動時間が最新のものを選択し、プライベートIPアドレスを取得します。
取得したIPアドレスで Serverspec の packer-build にシンボリックリンクを作成して、Serverspec を実行します。
テスト結果が全部OKであれば終了ステータスで 0 を返します。
このシェルをテストしてみます。
作業用のサーバの "Nameタグ" を "Packer Builder" に変更して、シェルを実行すると以下のようになります。
httpd をインストールしてないので、エラーになりますが、ちゃんと動いているようです。
[ec2-user@ip-10-0-11-158 workspace]$ ./serverspec.sh
/usr/bin/ruby2.0 -I/usr/local/share/ruby/gems/2.0/gems/rspec-support-3.5.0/lib:/usr/local/share/ruby/gems/2.0/gems/rspec-core-3.5.1/lib /usr/local/share/ruby/gems/2.0/gems/rspec-core-3.5.1/exe/rspec --pattern spec/10.0.11.158/\*_spec.rb failed
/usr/bin/ruby2.0 -I/usr/local/share/ruby/gems/2.0/gems/rspec-support-3.5.0/lib:/usr/local/share/ruby/gems/2.0/gems/rspec-core-3.5.1/lib /usr/local/share/ruby/gems/2.0/gems/rspec-core-3.5.1/exe/rspec --pattern spec/10.0.11.158/\*_spec.rb
Package "httpd"
should be installed (FAILED - 1)
Service "httpd"
should be enabled (FAILED - 2)
should be running (FAILED - 3)
Port "80"
should be listening (FAILED - 4)
Failures:
1) Package "httpd" should be installed
On host `10.0.11.158'
Failure/Error: it { should be_installed }
expected Package "httpd" to be installed
sudo -p 'Password: ' /bin/sh -c rpm\ -q\ httpd
package httpd is not installed
# ./spec/10.0.11.158/http_spec.rb:4:in `block (2 levels) in <top (required)>'
2) Service "httpd" should be enabled
On host `10.0.11.158'
Failure/Error: it { should be_enabled }
expected Service "httpd" to be enabled
sudo -p 'Password: ' /bin/sh -c chkconfig\ --list\ httpd\ \|\ grep\ 3:on
# ./spec/10.0.11.158/http_spec.rb:8:in `block (2 levels) in <top (required)>'
3) Service "httpd" should be running
On host `10.0.11.158'
Failure/Error: it { should be_running }
expected Service "httpd" to be running
sudo -p 'Password: ' /bin/sh -c ps\ aux\ \|\ grep\ -w\ --\ httpd\ \|\ grep\ -qv\ grep
# ./spec/10.0.11.158/http_spec.rb:9:in `block (2 levels) in <top (required)>'
4) Port "80" should be listening
On host `10.0.11.158'
Failure/Error: it { should be_listening }
expected Port "80" to be listening
sudo -p 'Password: ' /bin/sh -c netstat\ -tunl\ \|\ grep\ --\ :80\\\
# ./spec/10.0.11.158/http_spec.rb:13:in `block (2 levels) in <top (required)>'
Finished in 0.31946 seconds (files took 0.28991 seconds to load)
4 examples, 4 failures
Failed examples:
rspec ./spec/10.0.11.158/http_spec.rb:4 # Package "httpd" should be installed
rspec ./spec/10.0.11.158/http_spec.rb:8 # Service "httpd" should be enabled
rspec ./spec/10.0.11.158/http_spec.rb:9 # Service "httpd" should be running
rspec ./spec/10.0.11.158/http_spec.rb:13 # Port "80" should be listening
[ec2-user@ip-10-0-11-158 workspace]$ echo $?
1
5.Packer+Ansibe+ServerspecでAMIを作成する
前述のPacker のテンプレートを修正して、Ansibleの後に Serverspec を実行するようにします。
テンプレートは以下のとおり。
赤字部分を追加しました。
[ec2-user@ip-10-0-11-158 workspace]$ cat packer.json
{
"variables": {
"server_name": "{{env `BUILD_SERVER_NAME`}}",
"ami": "{{env `BUILD_AMI`}}",
"volume_size": "{{env `BUILD_EBS_SIZE`}}",
"ansible_playbook": "{{env `BUILD_PLAYBOOK`}}"
},
"builders" : [{
"type" : "amazon-ebs",
"region" : "us-east-1",
"availability_zone" : "us-east-1b",
"instance_type" : "t2.nano",
"source_ami" : "{{user `ami`}}",
"launch_block_device_mappings": [
{
"device_name": "/dev/xvda",
"volume_size": "{{user `volume_size`}}",
"volume_type": "gp2",
"delete_on_termination": "true"
}
],
"subnet_id" : "subnet-f528a2ad",
"associate_public_ip_address" : "true",
"security_group_id": "sg-bbf176df",
"ssh_keypair_name" : "virginia_key",
"ssh_username" : "ec2-user",
"ssh_private_key_file" : "/home/ec2-user/workspace/virginia_key.pem",
"ssh_private_ip" : "true",
"ami_name" : "TEST-AMI-{{user `server_name`}}",
"ami_description" : "{{user `server_name`}} created by packer",
"tags" : {
"Name" : "{{user `server_name`}}",
"Environment" : "develop"
}
}],
"provisioners" : [
{
"type" : "ansible",
"playbook_file" : "{{user `ansible_playbook`}}"
},
{
"type" : "shell-local",
"command" : "/home/ec2-user/workspace/serverspec.sh"
}
]
}
これを、前述のシェルで実行すると、以下のとおり。
[ec2-user@ip-10-0-11-158 workspace]$ ./packer.sh
amazon-ebs output will be in this color.
==> amazon-ebs: Prevalidating AMI Name...
==> amazon-ebs: Inspecting the source AMI...
==> amazon-ebs: Launching a source AWS instance...
amazon-ebs: Instance ID: i-b12e4821
==> amazon-ebs: Waiting for instance (i-b12e4821) to become ready...
==> amazon-ebs: Waiting for SSH to become available...
==> amazon-ebs: Connected to SSH!
==> amazon-ebs: Provisioning with Ansible...
==> amazon-ebs: Executing Ansible: ansible-playbook /home/ec2-user/workspace/playbook_dev001.yml -i /tmp/packer-provisioner-ansible519331836 --private-key /tmp/ansible-key013979345
==> amazon-ebs: SSH proxy: serving on 127.0.0.1:43408
amazon-ebs:
amazon-ebs: PLAY [all] *********************************************************************
amazon-ebs:
amazon-ebs: TASK [setup] *******************************************************************
amazon-ebs: SSH proxy: accepted connection
==> amazon-ebs: authentication attempt from 127.0.0.1:55066 to 127.0.0.1:43408 as ec2-user using none
==> amazon-ebs: authentication attempt from 127.0.0.1:55066 to 127.0.0.1:43408 as ec2-user using publickey
amazon-ebs: ok: [default]
amazon-ebs:
amazon-ebs: TASK [IPアドレス] ******************************************************************
amazon-ebs: changed: [default]
amazon-ebs:
amazon-ebs: TASK [debug] *******************************************************************
amazon-ebs: ok: [default] => {
amazon-ebs: "ip_ret.stdout": "10.0.11.66"
amazon-ebs: }
amazon-ebs:
amazon-ebs: TASK [httpd インストール] ************************************************************
amazon-ebs: changed: [default]
amazon-ebs:
amazon-ebs: TASK [httpd の自動起動ON] ***********************************************************
amazon-ebs: changed: [default]
amazon-ebs:
amazon-ebs: PLAY RECAP *********************************************************************
amazon-ebs: default : ok=5 changed=3 unreachable=0 failed=0
amazon-ebs:
==> amazon-ebs: shutting down the SSH proxy
==> amazon-ebs: Executing local command: /home/ec2-user/workspace/serverspec.sh
amazon-ebs: /usr/bin/ruby2.0 -I/usr/local/share/ruby/gems/2.0/gems/rspec-support-3.5.0/lib:/usr/local/share/ruby/gems/2.0/gems/rspec-core-3.5.1/lib /usr/local/share/ruby/gems/2.0/gems/rspec-core-3.5.1/exe/rspec --pattern spec/10.0.11.66/\*_spec.rb
amazon-ebs:
amazon-ebs: Package "httpd"
amazon-ebs: should be installed
amazon-ebs:
amazon-ebs: Service "httpd"
amazon-ebs: should be enabled
amazon-ebs: should be running
amazon-ebs:
amazon-ebs: Port "80"
amazon-ebs: should be listening
amazon-ebs:
amazon-ebs: Finished in 0.31962 seconds (files took 0.29783 seconds to load)
amazon-ebs: 4 examples, 0 failures
amazon-ebs:
==> amazon-ebs: Stopping the source instance...
==> amazon-ebs: Waiting for the instance to stop...
==> amazon-ebs: Creating the AMI: TEST-AMI-dev001
amazon-ebs: AMI: ami-274ec330
==> amazon-ebs: Waiting for AMI to become ready...
==> amazon-ebs: Modifying attributes on AMI (ami-274ec330)...
amazon-ebs: Modifying: description
==> amazon-ebs: Adding tags to AMI (ami-274ec330)...
amazon-ebs: Adding tag: "Environment": "develop"
amazon-ebs: Adding tag: "Name": "dev001"
==> amazon-ebs: Tagging snapshot: snap-fe5ac41c
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Cleaning up any extra volumes...
==> amazon-ebs: No volumes to clean up, skipping
Build 'amazon-ebs' finished.
==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
us-east-1: ami-274ec330
これで、Ansible で作ったEC2インスタンスを Serverspec でチェックし、OKならAMIを作成するようになりました。
なお、Packerは、Ansible や Serverspec でエラーになると、AMIは作成せずに、EC2インスタンスをターミネートして終了します。