AWSのEC2でchef-soloを使ってサーバを自動構築してみます。

ここでは、chef-solo用AMIを準備し、
インスタンスとして起動すれば自動的にサーバが構築できるようにしてみます。
また、chefのcookbookはS3上に配置することにします。
# cookbookをgitサーバを立てて配置してもよいのですが、
# 構築する環境によっては、gitサーバの冗長化なども考える必要があるので、
# S3に配置した方が構成がすっきりするのではないかと思います(個人的に)。

インスタンス起動すると、
自動的に次のプロセスが実行されるようなAMIを作ります。
・S3から最新のcookbookを取得する
・インスタンスのタグから適用するレシピを判断し、
 chef-soloを起動してレシピを適用する

インスタンス化時の実行プロセス:

ここでは、上記構成を実現するために、
・プロセス実行用モジュールを組み込んだAMIの作成
・cookbookの作成とS3上への配置
を行います。

プロセス実行用モジュールを組み込んだAMIの作成

ベースとするAMIの準備

ここではCentOSで環境を構築するので、
suz-lab製の次のAMIイメージをベースに環境を作成します。
 811118151095/suz-lab_ebs_centos-core-x86_64-6.3.1

「AWS Management Console」の「EC2 Dashbord」から「Launch Instance」を選択、
「Classic Wizard」を選択、
「CHOOSE AN AMI」で「Community AMIs」タブに切り替え、
「suz-lab」で検索すれば、見つかると思います。

インスタンスを選んだら、適当なオプションでインスタンスを起動します。
Nameを指定して、後はデフォルト値で問題ないと思います。

起動したら、SSHでログインできることを確認します。
指定した鍵を使って、rootユーザでログインができるはず。

rubyのインストール

次にRubyをインストールします。
CentOSのyumリポジトリでは1.8系までなので、ソースからビルドすることにします。

まずは、yumを最新化します。

$ yum install yum-fastestmirror
$ yum update

Rubyをビルドするために必要なモジュールをインストールします。

$ yum install gcc-c++ patch readline readline-devel zlib zlib-devel libffi-devel openssl-devel make bzip2 autoconf automake libtool bison sqlite-devel mysql-devel libxslt-devel httpd-devel curl-devel ncurses-devel gdbm-devel wget libyaml-devel

Rubyのソースをダウンロードして、ビルド&インストールします。

$ wget ftp://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.3-p125.tar.gz
$ tar zxf ruby-1.9.3-p125.tar.gz
$ cd ruby-1.9.3-p125
$ ./configure --prefix=/usr
$ make
$ make install
$ cd ../
$ rm -rf ruby-1.9.3-p125

Rubyがインストールされたことを確認します。

$ ruby -v
ruby 1.9.3p125 (2012-02-16 revision 34643) [x86_64-linux]

chefのインストール

次にchefをインストールします。

$ gem install chef

chef-soloがインストールされたことを確認します。

$ /usr/bin/chef-solo -v
Chef: 10.16.2

chefが利用するディレクトリを作成します。

$ mkdir /etc/chef
$ mkdir /var/chef
$ mkdir /var/chef/cookbooks
$ mkdir /var/log/chef
$ mkdir /tmp/chef-solo

chef-solo実行時のパラメータファイルを作成します。

/etc/chef/solo.rb

file_cache_path "/tmp/chef-solo"
cookbook_path ["/var/chef/cookbooks"]

s3syncのインストール

次にs3syncをインストールします。

$ wget https://github.com/aproxacs/s3sync/tarball/master
$ tar zxf master
$ mkdir /usr/local/s3sync
$ cd aproxacs-s3sync-*
$ mv ./* /usr/local/s3sync/
$ mkdir /etc/s3conf

S3への認証のためにaccess_key_id,secret_access_keyを指定します。
access_key_id,secret_access_keyは自分の環境のものを記載してください。

/etc/s3conf/s3config.yml

aws_access_key_id: <access_key_idをここに>
aws_secret_access_key: <secret_access_keyをここに>
aws_calling_format: SUBDOMAIN

Cookbookとレシピの作成

cookbookを保存するため、S3にbucketを作ります。

「AWS Management Console」でS3を選び、
「Buckets」の「Create Bucket」を選択して、
「Bucket Name」を指定し「Create」を押下すればBucketを作成できます。

初期データとして空のCookbookを作成し、S3にアップロードします。

$ /usr/bin/knife cookbook create mycookbook -o /var/chef/cookbooks
WARNING: No knife configuration file found
** Creating cookbook mycookbook
** Creating README for cookbook: mycookbook
** Creating CHANGELOG for cookbook: mycookbook
** Creating metadata for cookbook: mycookbook
$ /usr/local/s3sync/bin/s3sync -r --delete -v /var/chef/cookbooks/ <s3のbucket名をここに>:repo/cookbooks/
Create node mycookbook
Create node mycookbook/CHANGELOG.md
Create node mycookbook/README.md
Create node mycookbook/attributes
Create node mycookbook/definitions
Create node mycookbook/files
Create node mycookbook/files/default
Create node mycookbook/libraries
Create node mycookbook/metadata.rb
Create node mycookbook/providers
Create node mycookbook/recipes
Create node mycookbook/recipes/default.rb
Create node mycookbook/resources
Create node mycookbook/templates
Create node mycookbook/templates/default

プロセス実行用スクリプトの作成

プロセス実行用のスクリプトでは以下の処理を行います。
・S3からcookbookをダウンロードする
・instanceのタグから適用するレシピの名称を取得する
・chef-soloを実行する

instanceのタグの読み込みにaws-sdkを利用するので、インストールします。

$ gem install aws-sdk

awsにアクセスするための認証情報を記載します。
access_key_id,secret_access_keyは自分の環境のものを記載してください。
以下の例では、endpointをtokyo regionで指定しています。

/etc/aws.yml

access_key_id: <access_key_idをここに>
secret_access_key: <secret_access_keyをここに>
ec2_endpoint: ec2.ap-northeast-1.amazonaws.com

プロセス実行用スクリプトを作成します。

/usr/local/sbin/execute-chef

#!/usr/bin/ruby
# encoding: utf-8
require 'aws-sdk'

S3_BUCKET="<s3のbucket名をここに>"

# S3から最新のcookbookを取得
puts `/usr/local/s3sync/bin/s3sync -r --delete -v #{S3_BUCKET}:repo/cookbooks/ /var/chef/cookbooks/`

# タグから適用するレシピ名を特定
AWS.config(YAML.load(File.read("/etc/aws.yml")))
myinstanceid = `curl --silent http://169.254.169.254/latest/meta-data/instance-id`
instance = AWS::EC2.new.instances[myinstanceid]
chef_recipes = instance.tags['chef_recipes']
exit unless chef_recipes
runlist = []
chef_recipes.split.each_with_index{|v,i| runlist[i] = '"recipe[' + v + ']"' }
File.open('/etc/chef/node.json','w') do |fp|
fp.puts '{ "run_list": [ ' + runlist.join(", ") + ' ] }'
end

# chef-soloを実行
puts `/usr/bin/chef-solo -c /etc/chef/solo.rb -j /etc/chef/node.json`

スクリプトの実行権限を付与します。

$ chmod +x /usr/local/sbin/execute-chef

スクリプトの動作確認のため、
インスタンスに実行するレシピのタグをつけます。
 タグの名: chef_recipes
 タグの値: mycookbook::default

「AWS Management Console」の「EC2」の「Navigation」から「Instances」を選択、
現在操作しているinstanceのコンテキストメニューから「Add/Edit Tags」を選択し、
上記の値でタグを追加します。

以下のコマンドで、プロセス実行用スクリプトを実行します。

$ /usr/local/sbin/execute-chef
Remove node
[2012-11-16T17:01:09+09:00] INFO: *** Chef 10.16.2 ***
[2012-11-16T17:01:09+09:00] INFO: Setting the run_list to ["recipe[mycookbook::default]"] from JSON
[2012-11-16T17:01:09+09:00] INFO: Starting Chef Run for ip-10-146-39-20.ap-northeast-1.compute.internal
[2012-11-16T17:01:09+09:00] INFO: Running start handlers
[2012-11-16T17:01:09+09:00] INFO: Start handlers complete.
[2012-11-16T17:01:09+09:00] INFO: Chef Run complete in 0.002502381 seconds
[2012-11-16T17:01:09+09:00] INFO: Running report handlers
[2012-11-16T17:01:09+09:00] INFO: Report handlers complete

上記のようにchefが実行されエラーにならなければOKです。
rc.localに以下を追記して、サーバ起動後に自動的にレシピを適用するようにします。

/etc/rc.local

/usr/local/sbin/execute-chef > /var/log/execute-chef.log

構築したインスタンスのAMI化

ここまでで構築した環境をAMIとして保存します。

「AWS Management Console」の「EC2」の「Navigation」から「Instances」を選択、
現在操作しているinstanceのコンテキストメニューから「Stop」を選択して、
インスタンスを停止します。

インスタンスの停止を確認したら、
現在操作しているinstanceのコンテキストメニューから「Create Image (EBS AMI)」を選択します。
適当な「Image Name」をつけ、「Yes, Create」を選択すると、
現在のイメージをAMI化できます。

「EC2」の「Navigation」から「AMIs」を選択すると、
作成したAMIのイメージがあることが確認できると思います。

ここまでで、以下の準備ができたことになります。
・S3上にcookbookを保存する環境
・レシピを適用しながら起動する環境(AMI)

以降で、
・レシピの作成と登録
・作成したレシピの適用
を行ってみたいと思います。

レシピの作成と登録、レシピの適用

サンプルとしてレシピを作成・S3へ登録してみます。
レシピはhttpdをインストール、起動するだけのシンプルなもので試してみます。

レシピの作成とS3上への配置

先ほど作成したサーバのインスタンスを再び起動し、rootでログインします。

/var/chef/cookbooks/mycookbook/recipes/httpd-server.rb

%w{httpd}.each do |pkg|
package pkg do
action :install
end
end

service "httpd" do
supports :status => true, :restart => true, :reload => true
action [ :enable, :start ]
end

レシピを作成したら、実行するレシピを指定します。

/etc/chef/node.json

{ "run_list": [ "recipe[mycookbook::httpd-server]" ] }

以下のコマンドでレシピを実行します。

$ /usr/bin/chef-solo -c /etc/chef/solo.rb -j /etc/chef/node.json

レシピの動作が確認できたら、S3にアップロードします。

$ /usr/local/s3sync/bin/s3sync -r --delete -v /var/chef/cookbooks/ <s3のbucket名をここに>:repo/cookbooks/

インスタンス化と自動適用の確認

次にアップロードしたレシピを自動的に新規インスタンスに適用します。

「AWS Management Console」の「EC2」の「Navigation」から「AMIs」を選択、
先ほど作成したAMIのコンテキストメニューから「Launch Instance」を選択して、
インスタンスを起動します。
タグの指定で以下のタグを追加しておきます。
 タグの名: chef_recipes
 タグの値: mycookbook::httpd-server

これでインスタンスを起動後、自動的にレシピが適用されます。
新しく起動したインスタンスにログインし、

$ ps -ef  | grep httpd

とすると、httpdが起動しているのが確認できます。
# 上手くいかない場合は「/var/log/execute-chef.log」を見て原因を確認。

このような流れで、レシピを作成し・S3に登録していけば、
一つのAMIから様々な役割のサーバを自動的に構築することが出来ます。

サーバを自動構築できるようにしておくことで、
以下のような用途で活用することが出来るのではないかと考えられます。
・高負荷時にサーバを増やして負荷分散する
・アプリケーション開発用時に環境を増やす
・障害調査のために本番とまったく同じ環境を作る