« MongoDB+Rails3.1のベンチマーク | トップページ | 第14回 yokohama.rbに参加しました。 »

2011年11月 6日 (日)

rubyによるAmazon EC2のインスタンスのセットアップ

Amazon EC2を使う利点

各種APIが公開されているため、プログラマブルにインスタンスのセットアップができるところにつきると思います。

他のクラウド(もちろんAmazon EC2も)は、Webブラウザ上でインスタンスの増減やロードバランサ等の設定ができますが、手作業が必要なため自動でスケーリングができません。

また、もしデータセンター(amazonではゾーンと呼ばれる)全体が障害に陥いったとしても、セットアップをプログラムで自動化しておけば、ゾーンのURLを書き換えて実行するだけで、同じ環境を違うゾーンに簡単に復元できます。

Amazon EC2 セットアップスクリプト

ubuntuの素のOSから、ユーザを作成し、memcachedをインストール設定、起動するまでのスクリプトを書いてみました。
使用しているライブラリは公式のruby sdkではなく、https://github.com/appoxy/aws を使っています。
理由は公式よりも機能が豊富だったから。
スクリプトは、インスタンスに、debian系のイメージを使用することが前提になっています。
なおこのスクリプト作成には、SoftwareDesign 2010年10月号 クラウド活用プログラミング入門 第5章 自動スケールアウトに挑戦という山崎 泰宏さんの記事をかなり参考にしました。

AWSのインストール

gem install aws

config.yml

admin_user: "ubuntu"
application_user: "app"
aws_access_key: "AWS ACCESS KEY"
aws_secret_key: "AWS SECRET KEY"
key_name: "ubuntu"
image_id: "ami-e49723e5"
instance_type: "m1.small"
pem_path: "ubuntu.pem"
key_pub: "ssh-rsa AAAAB3N?????????????????"

ec2_setup.rb

#!/usr/bin/env ruby
require 'rubygems'
require 'aws'
require 'yaml'
require 'json'
require 'open3'

def create_ec2
  #EC2のインスタンスをTokyo regionで作成するように設定
  ENV["EC2_URL"] = "https://ec2.ap-northeast-1.amazonaws.com/"
  Aws::Ec2.new($config["aws_access_key"],$config["aws_secret_key"])
end

#EC2起動
def launch_instances(ec2)
    response = ec2.launch_instances($config["image_id"],
    {
      :min_count => $config["min_count"]||1,
      :max_count => $config["max_count"]||1,
      :user_data => $config["user_data"],
      :group_ids => [$config["security_group"]||"default"],
      :key_name => $config["key_name"],
      :instance_type => $config["instance_type"],
      :addressing_type       => nil,
      :kernel_id             => nil,
      :ramdisk_id            => nil,
      :availability_zone     => "ap-northeast-1a",
      :block_device_mappings => nil,
      :monitoring_enabled   => true
    })

    #作成したインスタンスのIDの配列を取得
    response.map{|instance| instance[:aws_instance_id]}
end

#インスタンスの起動完了を待つ
def wait_running(ec2,instance_ids)
  counter = 0
  unstart=true
  instance_dns_map = {}
  while(unstart)
    sleep 3
    counter += 1
    puts "*Status check...(#{counter})"
    instances = ec2.describe_instances(instance_ids)
    unstart = false
    instances.each do |instance|
      puts " Instance: #{instance[:aws_instance_id]} is #{instance[:aws_state]}"
      if instance[:aws_state] == "running"
        instance_dns_map[instance[:aws_instance_id]] = instance[:dns_name]
      else
        unstart = true
      end
    end
  end
  puts "All instances are running."
  instance_dns_map
end

#know_hostsから削除
def remove_known_hosts(hosts)
  f = "#{ENV['HOME']}/.ssh/known_hosts"
  ssh_hosts = File.read(f)
  new_rec = ""
  ssh_hosts.each_line do|el|
    rec = el
    hosts.each{|host| rec = "" if el =~ /#{host}/}
    new_rec += rec
  end
  File.open(f,"w"){|f| f.write(new_rec)}
end

#コマンドを実行する(system関数使わないのは、標準出力や標準エラー出力を受けとりたいため。)
def act_command(command,env={})
  puts command
  out = err = status = nil
  Open3.popen3(env,command) do|stdin, stdout, stderr, wait_thr|
    stdin.close
    out = stdout.read
    puts out if out && out != ''
    err = stderr.read
    if !err || err != ''
      STDERR.puts(err)
    end
    status = wait_thr.value
  end
  [status.to_i,out,err]
end

#sshコマンドを発行する StrictHostKeyCheckingのオプションは初回アクセスで未登録のホストに対してもエラーを出さない
def ssh_command(host,command)
  ssh = "ssh -o StrictHostKeyChecking=no -i #{$config['pem_path']} #{$config['admin_user']}@#{host} '#{command}'"
  act_command(ssh)
end

#scpコマンドを実行 directionはtoはリモートにcopy.それ以外はリモートからコピー
def scp(host,orig,dest,direction="to")
  if direction == "to"
    dest = "#{$config['admin_user']}@#{host}:#{dest}"
  else
    orig = "#{$config['admin_user']}@#{host}:#{orig}"
  end
  scp = "scp -o StrictHostKeyChecking=no -i #{$config['pem_path']} #{orig} #{dest}"
  act_command(scp)
end

#SSH接続ができるようになるまで待つ
def wait_ssh_up(instance_dns_map)
  counter = 0
  unstart=true
  while(unstart)
    sleep 3
    counter += 1
    puts "*SSH Connection check...(#{counter})"
    unstart = false
    instance_dns_map.each do |k,v|
      begin
        code = out = err = nil
        timeout(3) do
          code,out,err = ssh_command(v,'echo "success"')
        end
        if err && err =~ /REMOTE HOST IDENTIFICATION HAS CHANGED/
          act_command("ssh-keygen -R #{v}")
          unstart = true
        elsif code == 0
          puts " Try to ssh to an instance #{k}:#{v} Success."
          unstart = false
        else
          puts " Try to ssh to an instance #{k}:#{v} Failure."
          unstart = true
        end
      rescue Timeout::Error => e
        puts " Try to ssh to an instance #{k}:#{v} Timeout."
        unstart = true
      end
    end
  end
  puts "All instances ssh ready."
end

#アプリケーション用ユーザの作成
def user_setup(host)
  ssh_command(host,"sudo useradd -d /home/#{$config['application_user']} #{$config['application_user']}")
  ssh_command(host,"sudo mkdir -p /home/#{$config['application_user']}/.ssh")
  ssh_command(host,"sudo echo \"#{$config['key_pub']}\" > /tmp/authorized_keys")
  ssh_command(host,"sudo mv /tmp/authorized_keys /home/#{$config['application_user']}/.ssh/")
  ssh_command(host,"sudo chown -R #{$config['application_user']}:#{$config['application_user']} /home/#{$config['application_user']}")
end

#memcachedのインストール
def memcached_setup(host)
  ssh_command(host,"sudo mkdir -p /mnt/memcached/log")
  ssh_command(host,"sudo chown -R #{$config['application_user']}:#{$config['application_user']} /mnt/memcached")
  #質問に対して全部yesで返答
  ssh_command(host,"DEBIAN_FRONTEND=noninteractive sudo apt-get install --assume-yes memcached")
  scp(host,File.expand_path("../memcached.conf",__FILE__),"/tmp/memcached.conf")
  ssh_command(host,"sudo mv /tmp/memcached.conf /etc/memcached.conf")
  ssh_command(host,"sudo /etc/init.d/memcached restart")
end
#==========#
# MAIN処理 #
#==========#
#設定ファイルの読み込み
$config = YAML.load_file(File.expand_path("../config.yml", __FILE__))
keys = %w(admin_user application_user aws_access_key aws_secret_key
        key_name image_id instance_type pem_path key_pub)
if (remain_keys = keys - $config.keys) != []
  raise "config error: lack of #{remain_keys.join(',')} settings"
end
#EC2マネージャインスタンス作成
ec2 = create_ec2

#EC2インスタンス作成
instance_ids = launch_instances(ec2)

#インスタンスの起動完了を待つ
instance_dns_map = wait_running(ec2,instance_ids)

#以前に同じアドレスの違うインスタンスにログインしている可能性があるため、know_hostsから削除
remove_known_hosts(instance_dns_map.values)

#SSH接続ができるようになるまで待つ
wait_ssh_up(instance_dns_map)

instance_dns_map.each{|k,v|
  #アプリケーション用ユーザの作成
  user_setup(v)
  #memcachedのインストール
  memcached_setup(v)
}

memcached.conf

-d
logfile /mnt/memcached/log/memcached.log
# memory
-m 1440
# Run the daemon as root. The start-memcached will default to running as root if no
# -u command is present in this config file
-u app
-l 0.0.0.0

config.yml

admin_user
用意されている初期ユーザ 。使用するamiによって異ります。ubuntuの公式のイメージだとubuntuですが、イメージによってrootだったり、bitnamiだったりと異るため注意が必要です。

application_user

インスタンスのサーバのアプリを実行するユーザ。

aws_access_key

AWSのアクセスキー(AWSのアカウントのセキュリティ証明書のページに記載されています

aws_secret_key

AWSのシークレットアクセスキー(AWSのアカウントのセキュリティ証明書のページの表示リンククリックで表示されます)

key_name

EC2の初期ログインに必要なキーペアの名前 Management ConsoleでEC2を選択して、Key pairsを選択して作成。awsのライブラリを使って作成することもできます。

image_id

作成するインスタンスのイメージID(ami)

instance_type

インスタンスの種類(m1.largeとかm1.small)

pem_path

keynameで指定したキーファイルへのパス

key_pub

自分の公開鍵の文字列 インスタンスのセットアップが終了すると、立ち上がったインスタンスにapplication_userの名前でログインできます。

min_count

立ち上げたい最小インスタンス数(任意)

max_count

立ち上げたい最大インスタンス数(任意)

ec2_setup.rb

create_ec2
デフォルトだとvirginiaのリージョンになるので、tokyoリージョンになるように設定。

launch_instances

availability_zoneの設定は、もしzone全体に障害が起きた時は、create_ec2のEC2_URLとこの値を変更します。
monitoring_enabledはセットすると、clowdwathを使って、詳細メトリックスが取れます。 モニタリングの値もawsのライブラリで簡単に取れるので、cron等でチェックして負荷が高い時は、 スケールアウトさせることも容易です。
min_countやmax_countで同じイメージのインスタンスを同時に複数立ち上げることができます。

wait_running

インスタンスをlaunchしても1〜5分ぐらいstatusがpendingのまま。pending時間はその時によってまちまち。

remove_known_hosts

インスタンスの上げ下げをしていると同じアドレスの別インスタンスにぶつかり、前回とfinger_printが 違うのでエラーになって、ssh接続ができなくなるのを避けるため。

wait_ssh_up

sleepをしながら、SSH接続が成功するまで待ち続ける。 StrictHostKeyCheckingのオプションは、初回のsshアクセスの際にknow_hostsに追加していいかのプロンプトを出させなくするため。

user_setup

初期ユーザはno passwordでsudoができて危険なため、ユーザを作成し、公開鍵も登録してセットアップ完了時にログインができるようにする

memcached_setup

sudoが使えるのでapt-getでいれればいいだけだが、依存パッケージがある場合はインストールしていいかのプロンプトが出力されてしまうので、DEBIAN_FRONTEND=noninteractiveの設定と--assume-yes のオプションをつけています。

memcached.conf

logfile-mの設定は必須。EC2のディスク容量のほとんどは、/mnt配下に割り当てられている。 例えばsmallは、160G中147Gが/mnt以下に割り当てられています。
memoryはデフォルト64Mなので、メモリが許す範囲に割り当てなおします。

まとめ

実際やってみると、簡単ということがわかると思います。 基本的には、アップデートの容易さや他のアプリでの使い回しも考えると、素のOSのamiを使って、サーバのインストール、設定までを行うスクリプトを作成するのが望ましいですが、Webサーバのような負荷に応じて頻繁にインスタンスの上げ下げをする場合は、必要なパッケージや設定がすでにインストールされた状態の独自にamiを作成することで、インスタンスの立ち上がりからサービス開始までの時間を短縮させる必要があると思います。

|

« MongoDB+Rails3.1のベンチマーク | トップページ | 第14回 yokohama.rbに参加しました。 »

ruby」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




トラックバック

この記事のトラックバックURL:
http://app.cocolog-nifty.com/t/trackback/68673/53177756

この記事へのトラックバック一覧です: rubyによるAmazon EC2のインスタンスのセットアップ:

« MongoDB+Rails3.1のベンチマーク | トップページ | 第14回 yokohama.rbに参加しました。 »