[Immutable Infrastructure を目指す][その 1] ruby と chef で作る AWS AMI

# 目的
* 必要なソフトウェアをインストールした AMI を ruby スクリプトを使用し、自動で作成する
    * これにより、いつでも必要なソフトウェアがインストールされたインスタンスが用意できる
* 今回は、ruby スクリプトと chef を用いて Nginx, Git をインストールした EC2 インスタンスの立ち上げ、およびその AMI を作成する

# 事前準備
## 作業マシン環境
* chef, knife-solo がインストールされていること
* aws-sdk の gem がインストール済みであること

## AWS 上での準備
* EC2 インスタンスを作成するための AMI を選定する
    * 今回は、Ubuntu Server 12.04 LTS (64-bit) とする
        * AMI ID (今回は ami-f381f5f2) を確認しておく

![AMI選定.png](https://qiita-image-store.s3.amazonaws.com/0/28573/d9b2fb15-2328-5528-4735-3821ce86ec32.png)

* Security Group を作成する
    * 今回は "forQiita" という名前で作成

![create_security_group.png](https://qiita-image-store.s3.amazonaws.com/0/28573/61d4f571-f045-b8a9-84c5-e98c6a6171f0.png)

* Key Pair を作成する
    * 今回は "forQiita" という名前で作成

# スクリプトの作成
## chef のリポジトリ作成
* 以下のコマンドでリポジトリを作成する

```lang:bash
$ knife cookbook init <CHEF_KITCHEN>
```

## ruby スクリプトの作成
* 今回は creatingAMI.rb という名前で作成する

```ruby:creatingAMI.rb
require 'aws-sdk'
require 'dotenv'
Dotenv.load

# 環境変数の設定
ACCESS_KEY_ID        = ENV['ACCESS_KEY_ID']
SECRET_ACCESS_KEY    = ENV['SECRET_ACCESS_KEY']
REGION               = ENV['REGION']
BASE_AMI_NAME        = ENV['BASE_AMI_NAME']
INSTANCE_TYPE        = ENV['INSTANCE_TYPE']
KEY_PAIR             = ENV['KEY_PAIR']
ELB_NAME             = ENV['ELB_NAME']
SECURITY_GROUP       = ENV['SECURITY_GROUP']
AVAILABILITY_ZONE_A  = ENV['AVAILABILITY_ZONE_A']
AVAILABILITY_ZONE_C  = ENV['AVAILABILITY_ZONE_C']
EC2_ENDPOINT         = ENV['EC2_ENDPOINT']
ELB_ENDPOINT         = ENV['ELB_ENDPOINT']
REMOTE_USER          = ENV['REMOTE_USER']
SSH_KEY_FILE_PATH    = ENV['SSH_KEY_FILE_PATH']

AWS.config({
  region:            REGION,
  access_key_id:     ACCESS_KEY_ID,
  secret_access_key: SECRET_ACCESS_KEY,
  ec2_endpoint:      EC2_ENDPOINT,
  elb_endpoint:      ELB_ENDPOINT
})

INSTANCE_NAME        = "appServer_newInstnce"
NAME_TAG             = "Name"

p "EC2 Task"
@ec2 = AWS::EC2.new(
  access_key_id: ACCESS_KEY_ID,
  secret_access_key: SECRET_ACCESS_KEY,
  region: REGION
)

############################## Create the EC2 Instance
@new_instance = @ec2.instances.create(
  image_id: BASE_AMI_NAME,
  instance_type: INSTANCE_TYPE,
  security_groups: SECURITY_GROUP,
  availability_zone: AVAILABILITY_ZONE_C,
  key_name: KEY_PAIR
)
@ec2.tags.create(@new_instance, NAME_TAG, value: INSTANCE_NAME)
p "New Instance is creating ..."

# Set IP Address
# IP アドレスが割り当てられるまで(ステータスが running になるまで)待機
p "Status check."
loop do
  p "current status is #{@new_instance.status}."
  break if @new_instance.status.equal?(:running)
  sleep(20)
end
p "Status is #{@new_instance.status}!"

@all_instance_info = @ec2.client.describe_instances[:instance_index]
@all_instance_info.each_value do |instance_info|
  tag_set = instance_info[:tag_set]
  tag_set.each do |tag|
    if (tag[:key] == NAME_TAG && tag[:value] == INSTANCE_NAME)
      @instance_pub_ip_addr = @new_instance.public_ip_address
      @instance_id          = @new_instance.id
    end
  end
end

############################## Waiting status check
# Status Checks が "2/2 checks passed" になるまで sleep
i = 210
while i > 0
  p "Please wait #{i} seconds."
  sleep(30)
  i -= 30
end
p "Finished status check."

############################## Running Chef
# Chef を使用し、nginx をインストール
`knife solo prepare -i #{SSH_KEY_FILE_PATH} #{REMOTE_USER}@#{@instance_pub_ip_addr}`
# 後述する、json ファイルを新規作成した EC2 用にコピー
`cat nodes/template.json > nodes/#{@instance_pub_ip_addr}.json`
`knife solo cook -i #{SSH_KEY_FILE_PATH} #{REMOTE_USER}@#{@instance_pub_ip_addr}`

p "Finished chef."

############################## Createing AMI

ami_name = "#{@new_instance.id}_#{DateTime.now.strftime("%Y%m%d%H%M%S")}"
ami_description = "Web Server Base AMI(#{@new_instance.tags['Name']}) at #{DateTime.now}"
new_created_ami = @new_instance.create_image(ami_name, {description: ami_description, no_reboot: true})
@ec2.tags.create(new_created_ami, NAME_TAG, value: "Base_AMI_#{DateTime.now.strftime("%Y%m%d")}")
p "AMI is Created!!!"
p "Done."
```

## chef cookbook の作成

```bash
$ knife cookbook create nginx -o site-cookbooks
```

* site-cookbooks/nginx/recipes/default.rb を編集する

```rb:default.rb
#
# Cookbook Name:: nginx
# Recipe:: default
#
# Copyright 2014, YOUR_COMPANY_NAME
#
# All rights reserved - Do Not Redistribute
#

############################## apt-get update

log "Run apt-get update..."
execute "apt-get update" do
  action :run
end
log "apt-get update Done!!!"

############################## git install

log "Git install ..."
%w{libcurl4-gnutls-dev libexpat1-dev gettext libz-dev libssl-dev}.each do |pkg|
  package pkg do
    action :install
  end
end

package "git-core" do
  action :install
end
log "Git install Done!!!"

############################## nginx install

log "Nginx install ..."
package "nginx" do
  action :install
end

service "nginx" do
  supports :status => true, :restart => true, :reload => true
  action [ :enable , :start ]
end
log "Nginx install Done!!!"
```

* 実行するレシピ一覧を json に記載
* 下記のような名前でファイルを用意する
    * nodes/template.json (このファイルを新規 EC2 用にソースコード内でコピーしている)

```json:template.json
{
  "run_list":[
    "recipe[nginx]"
    ]
}
```

## 環境変数の設定
* ruby スクリプトで記載している環境変数を用意
* "$HOME/.bash_profile" などに書いても良いが、今回は dotenv を採用
    * dotenv については [環境によって変わる設定値はdotenvを使うと便利](http://qiita.com/closer/items/f8d8ba00ae86d7051764) を参考にしました。

```.env
SSH_KEY_FILE_PATH    = "$HOME/.ssh/forQiita.cer"

# ACCESS_KEY_ID と SECRET_ACCESS_KEY は各自記載
ACCESS_KEY_ID        = "<YOUR ACCESS KEY ID HERE>"
SECRET_ACCESS_KEY    = "<YOUR SECRET ACCESS KEY HERE>"
REGION               = "ap-northeast-1"
BASE_AMI_NAME        = "ami-f381f5f2"
INSTANCE_TYPE        = "t1.micro"
KEY_PAIR             = "forQiita"
SECURITY_GROUP       = "forQiita"
AVAILABILITY_ZONE_A  = "ap-northeast-1a"
AVAILABILITY_ZONE_C  = "ap-northeast-1c"
EC2_ENDPOINT         = "ec2.ap-northeast-1.amazonaws.com"
REMOTE_USER          = "ubuntu"
```

# 実行

* ここまでくると下記のようなディレクトリ構成になっているはずである

```
.
├── cookbooks
├── creatingAMI.rb
├── data_bags
├── environments
├── nodes
│   └── template.json
├── roles
└── site-cookbooks
    └── nginx
        ├── CHANGELOG.md
        ├── README.md
        ├── attributes
        ├── definitions
        ├── files
        │   └── default
        ├── libraries
        ├── metadata.rb
        ├── providers
        ├── recipes
        │   └── default.rb
        ├── resources
        └── templates
            └── default
```

* シェルを実行

```bash
$ ruby creatingAMI.rb
```

* AMI が作られたことを確認

![created_ami.png](https://qiita-image-store.s3.amazonaws.com/0/28573/3a7e6ecb-99a3-70d0-40e1-a58ef1f21159.png)

* また、作成したインスタンスにブラウザからアクセスし、"Welcome to nginx!" が表示されることも確認
    * nginx がインストールされている

# まとめ
* AWS で提供している AMI をベースに EC2 インスタンスの起動、さらに自分が必要とするソフトをインストールした状態の AMI を作るスクリプトを作成

* これにより、いつでも必要なソフトがインストールされているインスタンスが用意できる
* 必要とするソフトウェアに変更があった場合は、chef のレシピを修正するだけで、簡単かつ自動にベースとなる AMI を用意できる

# 参考
* [AWS SDK for Ruby](http://docs.aws.amazon.com/AWSRubySDK/latest/)
* [環境によって変わる設定値はdotenvを使うと便利](http://qiita.com/closer/items/f8d8ba00ae86d7051764)