Hatena::ブログ(Diary)

lambda {|diary| lambda { diary.succ! } }.call(hatena) このページをアンテナに追加 RSSフィード

 

2012-02-03

[] ec2-api-tools.spec作成スクリプト

#!/bin/sh
SOURCE=ec2-api-tools.zip
SPEC=ec2-api-tools.spec
DESTDIR=/usr

wget -q http://s3.amazonaws.com/ec2-downloads/$SOURCE -O $SOURCE
VERSION=`unzip -l ec2-api-tools.zip | awk '/ec2-api-tools-/{print $4; exit}' | sed -r 's|ec2-api-tools-([^/]+).*|\1|'`

cat <<EOF > $SPEC
Summary: Amazon EC2 API Tools
Name: ec2-api-tools
Version: $VERSION
Release: 1
License: Amazon Software License
Group: Development/Tools
Source: ec2-api-tools.zip
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
BuildArch: noarch

%define destdir $DESTDIR
%define _unpackaged_files_terminate_build 0

%description
The API tools serve as the client interface to the Amazon EC2 web service. Use these tools to register and launch instances, manipulate security groups, and more.

%prep
%setup -q

%install
install -d \$RPM_BUILD_ROOT/%{destdir}/bin
install -d \$RPM_BUILD_ROOT/%{destdir}/lib
install -m 755 bin/* \$RPM_BUILD_ROOT/%{destdir}/bin/
install -m 644 lib/* \$RPM_BUILD_ROOT/%{destdir}/lib/

%files
EOF

unzip -l $SOURCE | awk '$4 ~ /ec2-api-tools-.+\/(bin|lib)\/.+/ && $4 !~ /\.cmd/{sub(/[^/]+/,"'$DESTDIR'",$4); print $4}' >> $SPEC

2012-01-28

[] EventMachineでHTTPd

重い処理はバックグラウンドに回すのが正解なのかな。

基本的にmutableなオブジェクトの共有はNGな気がするな。

#!/usr/bin/env ruby
require 'rubygems'
require 'eventmachine'

class HTTP < EM::Connection
  def receive_data(data)
    operation = lambda do
      # 重い処理、ごにょごにょ…

      (<<-EOS).gsub(/\r?\n/,"\r\n")
HTTP/1.1 200 OK

<html>
  <head>
    <title>test</title>
  </head>
  <body>
    test
  </body
</html>
      EOS
    end # operation

    callback = lambda do |res|
      send_data(res)
      close_connection_after_writing 
    end

    EM.defer(operation, callback)
  end # receive_data
end

EM.run do
  EM.start_server('0.0.0.0', 80, HTTP)
end

[] EventMachineで簡単なロードバランサーを書いてみた その2

あ、さっきの嘘だ。

普通にラウンドロビン実装できた。

#!/usr/bin/env ruby
require 'rubygems'
require 'eventmachine'

class Backend < EM::Connection
  def initialize(proxy)
    @proxy = proxy
  end

  def receive_data(data)
    @proxy.send_data(data)
  end
end

class Proxy < EM::Connection
  attr_accessor :backend

  def receive_data(data)
    @backend.send_data(data)
  end
end

backends = [
  ['www.yahoo.co.jp', 80],
  ['www.hatena.ne.jp', 80],
]

EM.run do
  EM.start_server('0.0.0.0', 80, Proxy) do |srv|
    host, port = head = backends.shift
    backends.push(head)
    srv.backend = EM.connect(host, port, Backend, srv)
  end
end

shell1> ruby lb.rb

shell2> for i in {1..5}; do wget -q -O- 127.0.0.1 | nkf | fgrep '<title>'; done
<title>はてな</title>
<title>Yahoo! JAPAN</title>
<title>はてな</title>
<title>Yahoo! JAPAN</title>
<title>はてな</title>
shell2> for i in {1..5}; do wget -q -O- 127.0.0.1 | nkf | fgrep '<title>'; done
<title>Yahoo! JAPAN</title>
<title>はてな</title>
<title>Yahoo! JAPAN</title>
<title>はてな</title>
<title>Yahoo! JAPAN</title>

[] EventMachineで簡単なロードバランサーを書いてみた

同期の問題は大変デスネー

Hostヘッダ渡さなくてもレスポンス返してくれたのがちょっと意外だった。

Hatena Blogはレスポンス返してくれるのかな?


#!/usr/bin/env ruby
require 'rubygems'a
require 'eventmachine'

class Backend < EM::Connection
  def initialize(proxy)
    @proxy = proxy
  end

  def receive_data(data)
    @proxy.send_data(data)
  end
end

class Proxy < EM::Connection
  def initialize(backends)
    # コネクション毎の生成なので、このタイミングでバックエンドを決める
    host, port = backends[rand(backends.length)]
    @backend = EM.connect(host, port, Backend, self)

    # プロセス内でグローバルなラウンドロビンをやろうと思うと
    # プールの同期が必要になって、パフォーマンスが低下する気がする
    #
    # "以前に言及したように、接続と接続の間で君のクラスのインスタンスはどんな情報も共有できない。
    #  幸運にも、いや、設計されたことだろうが、EventMachine はこれを処理するメカニズムを提供する。"
    # http://keijinsonyaban.blogspot.com/2010/12/eventmachine.html
    # 
    # 共有…まじめに同期しようと思うと大変な気がするなー
    # mutableなオブジェクトについてのソリューションもあるのかしら
    #
    # バックエンドのコネクションもプーリングできたらよいけど
    # EventMachineのパターンと相性悪いような…
    # まあProxyへの接続が維持されている間はコネクションも維持されるので
    # そんなに問題ではないのかも
  end

  def receive_data(data)
    @backend.send_data(data)
  end
end

EM.run do
  # 順序が大事!なのでHashは×(たぶん)
  EM.start_server('0.0.0.0', 80, Proxy, [
    ['www.yahoo.co.jp', 80],
    ['www.hatena.ne.jp', 80],
  ])
end

shell1> ruby lb.rb

shell2> for i in {1..5}
> do
> wget -q -O- 127.0.0.1 | nkf | fgrep '<title>'
> done
<title>はてな</title>
<title>はてな</title>
<title>Yahoo! JAPAN</title>
<title>Yahoo! JAPAN</title>
<title>はてな</title>

2012-01-22

MurakumoによるElastic Network Interfaceのフェイルオーバー

Murakumo 0.4.5をリリースしました

https://rubygems.org/gems/murakumo/versions/0.4.5

今回はKeepalived/Heartbeatのような冗長化機能を追加しました。


Elastic Network Interfaceを使った冗長化の例

概要

アクティブサーバMySQLが落ちたときサーバ自体が落ちたときに、バックアップ側にENIを付け替える例です。


■準備

まずVPCインスタンスを用意します。

f:id:winebarrel:20120122210342j:image

server-01にENIをアタッチして、ルーティングテーブルを設定します。

server-01

[root@server-01 ~]# route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
169.254.169.254 *               255.255.255.255 UH    0      0        0 eth0
10.0.0.0        *               255.255.255.0   U     0      0        0 eth0
10.0.0.0        *               255.255.255.0   U     0      0        0 eth1
default         10.0.0.232      0.0.0.0         UG    0      0        0 eth0

server-02

[root@server-02 ~]# route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
169.254.169.254 *               255.255.255.255 UH    0      0        0 eth0
10.0.0.0        *               255.255.255.0   U     0      0        0 eth0
default         10.0.0.232      0.0.0.0         UG    0      0        0 eth0

※169.254.169.254を設定しておかないとメタ情報がとれなくてはまります


MurakumoとMySQLインストールします。

Murakumo

yum install ruby-devel sqlite-devel make gcc-c++
gem install murakumo

MySQL

yum install mysql-devel mysql-server
gem install mysql


Murakumoの設定はこんな感じ。

/etc/murakumo.yml
---
address: 0.0.0.0
port: 53

auth-key: onion
log-level: debug
resolver: 10.0.0.2, 8.8.8.8
max-ip-num: 8

domain: ap-northeast-1.compute.internal

init-script: /etc/murakumo-init.rb

addr-includes: ^10\..*

notification:
  host: 127.0.0.1
  sender: sender@mail.from
  recipients:
    - recipient@mail.from

# alias hostname, ttl, master/secondary/backup, weight
alias:
  - mysql-server,60,master,100

health-check:
  mysql-server:
    interval: 5
    timeout: 5
    healthy: 2
    unhealthy: 2
    script: |
      mysql_check 'root'

# hook of activation of backup/secondary
activity-check:
  mysql-server:
    start-delay: 60
    interval: 10
    active: 2
    inactive: 2
    on-activate: /usr/local/sbin/attach_if

murakumo-init.rbでサーバ情報の取得するので、どちらのサーバも内容は同じです。

Aliasは「mysql-server」。


murakumo-init.rbはこんな感じ。

/etc/murakumo-init.rb

AWS_ACCESS_KEY_ID = '...'
AWS_SECRET_ACCESS_KEY = '...'
REGION = 'ap-northeast-1'

# get self ip address
ip_addr = Murakumo::Util.self_ip_address

# get hostname
tags = Murakumo::Util.ec2_tags(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, REGION)
hostname = tags['Name'] || `curl -s http://169.254.169.254/latest/meta-data/local-hostname`

# rewrite host option
@options['host'] = "#{ip_addr}, #{hostname}"

# get instances
ip_addrs = Murakumo::Util::ec2_private_ip_addresses(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, REGION)

# rewrite initial-nodes
nodes = ip_addrs.select {|inst_id, ip_addr, status|
  status == 'running'
}.map {|inst_id, ip_addr, status| ip_addr }

@options['initial-nodes'] = nodes.join(',') unless nodes.empty?

それからENIをフェイルオーバーするスクリプトがこんな感じ。

/usr/local/sbin/attach_if

#!/bin/sh
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
REGION=ap-northeast-1
IF_ID=eni-...

/usr/bin/murakumo-attach-ec2-attach-interface \
  -k "$AWS_ACCESS_KEY_ID" -s "$AWS_SECRET_ACCESS_KEY" \
  -r $REGION -n $IF_ID 2>&1 | logger

exit 0

最後にserver-01/02でMySQLとMurakumoを起動します。

[root@server-01 ~]# /etc/init.d/mysqld start
Starting mysqld:                                           [  OK  ]
[root@server-01 ~]# /etc/init.d/murakumo start
Starting daemon...
Waiting for daemon to start...
Daemon status: running pid=15143


実験

初期状態ではserver-01にENIが刺さっています。

server-01

[root@server-01 ~]# ifconfig | grep ^e
eth0      Link encap:Ethernet  HWaddr 02:40:B5:1B:3F:9A
eth1      Link encap:Ethernet  HWaddr 02:40:B5:12:49:35

server-02

[root@server-02 ~]# ifconfig | grep ^e
eth0      Link encap:Ethernet  HWaddr 02:40:B5:37:0B:C0

Murakumoのステータスはこんな感じです。

[root@server-01 ~]# mrkmctl -L
IP address       TTL     Priority   Weight  Activity  Hostname
---------------  ------  ---------  ------  --------  ----------
10.0.0.133           60  Master        100  Active    mysql-server
10.0.0.133           60  Origin          -  Active    server-01
10.0.0.55            60  Backup        100  Active    mysql-server
10.0.0.55            60  Origin          -  Active    server-02


まず、server-01のmysqlを落としてみると…

server-01

[root@server-01 ~]# tail /var/log/murakumo.log
I, [2012-01-22T12:27:25.606223 #15143]  INFO -- : health condition changed: mysql-server: unhealthy
I, [2012-01-22T12:27:25.722457 #15143]  INFO -- : sent the notice: unhealthy
I, [2012-01-22T12:27:40.563515 #15143]  INFO -- : activity condition changed: mysql-server: inactive
I, [2012-01-22T12:27:40.640112 #15143]  INFO -- : sent the notice: inactive
[root@server-01 ~]# ifconfig | grep ^e
eth0      Link encap:Ethernet  HWaddr 02:40:B5:1B:3F:9A

server-02

[root@server-02 ~]# tail /var/log/murakumo.log
I, [2012-01-22T12:27:42.704476 #13467]  INFO -- : activity condition changed: mysql-server: active
I, [2012-01-22T12:27:42.820635 #13467]  INFO -- : sent the notice: active
[root@server-02 ~]# ifconfig | grep ^e
eth0      Link encap:Ethernet  HWaddr 02:40:B5:37:0B:C0
eth1      Link encap:Ethernet  HWaddr 02:40:B5:12:49:35

[root@server-01 ~]# mrkmctl -L
IP address       TTL     Priority   Weight  Activity  Hostname
---------------  ------  ---------  ------  --------  ----------
10.0.0.133           60  Master        100  Inactive  mysql-server
10.0.0.133           60  Origin          -  Active    server-01
10.0.0.55            60  Backup        100  Active    mysql-server
10.0.0.55            60  Origin          -  Active    server-02

server-01がUnhealthyになり、ENIがserver-02にアタッチされます。


次に、server-01のmysql再起動します。

server-01

[root@server-01 ~]# tail /var/log/murakumo.log
I, [2012-01-22T12:31:15.765068 #15143]  INFO -- : health condition changed: mysql-server: healthy
I, [2012-01-22T12:31:15.842994 #15143]  INFO -- : sent the notice: healthy
I, [2012-01-22T12:31:30.658938 #15143]  INFO -- : activity condition changed: mysql-server: active
I, [2012-01-22T12:31:30.739008 #15143]  INFO -- : sent the notice: active
[root@server-01 ~]# ifconfig | grep ^e
eth0      Link encap:Ethernet  HWaddr 02:40:B5:1B:3F:9A
eth1      Link encap:Ethernet  HWaddr 02:40:B5:12:49:35

server-02

[root@server-01 ~]# tail /var/log/murakumo.log
I, [2012-01-22T12:31:28.776223 #13467]  INFO -- : activity condition changed: mysql-server: inactive
I, [2012-01-22T12:31:28.855345 #13467]  INFO -- : sent the notice: inactive
[root@server-02 ~]# ifconfig | grep ^e
eth0      Link encap:Ethernet  HWaddr 02:40:B5:37:0B:C0

[root@server-01 ~]# mrkmctl -L
IP address       TTL     Priority   Weight  Activity  Hostname
---------------  ------  ---------  ------  --------  ----------
10.0.0.133           60  Master        100  Active    mysql-server
10.0.0.133           60  Origin          -  Active    server-01
10.0.0.55            60  Backup        100  Active    mysql-server
10.0.0.55            60  Origin          -  Active    server-02

今度はserver-01がActiveになり、ENIがserver-01にアタッチされます。


最後サーバ自体が落ちたものとして、server-01のMurakumoを落としてみます。

server-02

[root@server-02 ~]# tail /var/log/murakumo.log
I, [2012-01-22T12:36:08.879923 #13467]  INFO -- : activity condition changed: mysql-server: active
I, [2012-01-22T12:36:08.995964 #13467]  INFO -- : sent the notice: active
[root@server-02 ~]# ifconfig | grep ^e
eth0      Link encap:Ethernet  HWaddr 02:40:B5:37:0B:C0
eth1      Link encap:Ethernet  HWaddr 02:40:B5:12:49:35

しばらくするとserver-01のダウンが検知されてserver-02がActiveになり、ENIがserver-02に移ります。



…とまあこんな感じで、元々内部DNS用途だったMurakumoですが、Gossipプロトコルがなかなか便利だったので冗長化機能も持たせてみました。非VPCだとENIは使えませんが、そこは本来のDNSによる冗長化を使うということでどうせオワコンだし

Keepalivedはそもそも使えないしL7のチェックはちょっとめんどくさいし、Heartbeatマルチキャスト使えないしヘルスチェックはやっぱりめんどいし、、、ということでミドルウェアヘルスチェック込みで手軽にVPCで冗長化したい案件にはちょうどよいと思います。

絶賛、人柱募集中 ぜひぜひご利用ください。

2012-01-10

[][] Scheduled Eventチェック用Nagiosプラグイン

#!/usr/bin/env ruby
require 'cgi'
require 'base64'
require 'net/https'
require 'openssl'
require 'rexml/document' 

Net::HTTP.version_1_2

class EC2Client
  API_VERSION = '2011-12-15'
  SIGNATURE_VERSION = 2

  def initialize(accessKeyId, secretAccessKey, endpoint, algorithm = :SHA256)
    @accessKeyId = accessKeyId
    @secretAccessKey = secretAccessKey
    @endpoint = endpoint
    @algorithm = algorithm
  end

  def query(action, params = {})
    params = {
      :Action           => action,
      :Version          => API_VERSION,
      :Timestamp        => Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ'),
      :SignatureVersion => SIGNATURE_VERSION,
      :SignatureMethod  => "Hmac#{@algorithm}",
      :AWSAccessKeyId   => @accessKeyId,
    }.merge(params)

    signature = aws_sign(params)
    params[:Signature] = signature

    https = Net::HTTP.new(@endpoint, 443)
    https.use_ssl = true
    https.verify_mode = OpenSSL::SSL::VERIFY_NONE

    https.start do |w|
      req = Net::HTTP::Post.new('/',
        'Host' => @endpoint,
        'Content-Type' => 'application/x-www-form-urlencoded'
      )

      req.set_form_data(params)
      res = w.request(req)

      res.body
    end
  end

  private
  def aws_sign(params)
    params = params.sort_by {|a, b| a.to_s }.map {|k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&')
    string_to_sign = "POST\n#{@endpoint}\n/\n#{params}"
    digest = OpenSSL::HMAC.digest(OpenSSL::Digest.const_get(@algorithm).new, @secretAccessKey, string_to_sign)
    Base64.encode64(digest).gsub("\n", '')
  end
end # EC2Client

ACCESS_KEY_ID = '<ACCESS_KEY_ID>'
SECRET_ACCESS_KEY = '<SECRET_ACCESS_KEY>'
ENDPOINT = 'ec2.ap-northeast-1.amazonaws.com'

ec2cli = EC2Client.new(ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT)
source = ec2cli.query('DescribeInstanceStatus')

doc = REXML::Document.new(source)
errors = []

doc.each_element('/DescribeInstanceStatusResponse/instanceStatusSet/item') do |item|
  eventsSet = item.elements['eventsSet']
  next unless eventsSet

  instance_id = item.text('instanceId')
  az = item.text('availabilityZone')

  eventsSet.each_element('item') do |i|
    code = i.text('code')
    description = i.text('description')
    not_after = i.text('notAfter')
    not_before = i.text('notBefore')

    errors << "#{instance_id}:#{code}"
    # 間違い
    #errors << "#{instance_id}:#{code}" unless description =~ /Completed/
  end
end

unless errors.empty?
  puts errors.join(', ')
  exit 2
end

puts 'OK'