[EC2] プライベートIPを(APIレベルで)アサインするスクリプトを書いてみた…が

スクリプトは以下の通り。gemは不要。1.8/1.9共通

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

Net::HTTP.version_1_2

class EC2Client
  API_VERSION = '2012-06-15'
  SIGNATURE_VERSION = 2
  SIGNATURE_ALGORITHM = :SHA256

  def initialize(accessKeyId, secretAccessKey, endpoint)
    @accessKeyId = accessKeyId
    @secretAccessKey = secretAccessKey
    @endpoint = endpoint

    if /\A[^.]+\Z/ =~ @endpoint
      @endpoint = "ec2.#{@endpoint}.amazonaws.com"
    end
  end

  def assign_private_ipaddrs(ni_id, ipaddrs, reassign = false)
    params = {
      'NetworkInterfaceId' => ni_id,
      'AllowReassignment'  => reassign,
    }

    ipaddrs.each_with_index do |ipaddrs, i|
      params["PrivateIpAddress.#{i+1}"] = ipaddrs
    end

    source = query('AssignPrivateIpAddresses', params)

    errors = []

    REXML::Document.new(source).each_element('//Errors/Error') do |element|
      code = element.text('Code')
      message = element.text('Message')
      errors << "#{code}:#{message}"
    end

    raise errors.join(', ') unless errors.empty?
  end

  private

  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#{SIGNATURE_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

  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(SIGNATURE_ALGORITHM).new, @secretAccessKey, string_to_sign)
    Base64.encode64(digest).gsub("\n", '')
  end
end # EC2Client

# main
access_key = nil
secret_key = nil
endpoint   = nil
ni_id      = nil
ipaddrs    = nil
reassign   = false

ARGV.options do |opt|
  begin
    opt.on('-k', '--access-key ACCESS_KEY') {|v| access_key = v }
    opt.on('-s', '--secret-key SECRET_KEY') {|v| secret_key = v }
    opt.on('-r', '--region REGION') {|v| endpoint = v }
    opt.on('-n', '--network-interface-id INTERFACE_ID') {|v| ni_id = v }
    opt.on('-a', '--ipaddrs IPADDR_LIST', Array) {|v| ipaddrs = v }
    opt.on('-e', '--reassign') { reassign = true }
    opt.parse!

    access_key ||= (ENV['AMAZON_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY_ID'] || ENV['ACCESS_KEY_ID'])
    secret_key ||= (ENV['AMAZON_SECRET_ACCESS_KEY'] || ENV['AWS_SECRET_ACCESS_KEY'] || ENV['SECRET_ACCESS_KEY'])
    endpoint   ||= (ENV['EC2_URL'] || ENV['EC2_REGION'])

    unless access_key and secret_key and endpoint and ni_id and ipaddrs and not ipaddrs.empty?
      puts opt.help
      exit 1
    end
  rescue => e
    $stderr.puts e
    exit 1
  end
end

ec2cli = EC2Client.new(access_key, secret_key, endpoint)
ec2cli.assign_private_ipaddrs(ni_id, ipaddrs, reassign)

以下のようなコマンドを実行すれば、一応、仮想IPをNICにつけられる…

export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export EC2_REGION=ap-northeast-1
ec2-assign-private-ipaddrs -n eni-XXXXXXXX -a 10.0.0.100 -e
sudo ip addr add 10.0.0.100/24 dev eth0

が…

  • 別のさーばで、APIでReassignして「ip addr add...」を実行すると「RTNETLINK answers: Cannot assign requested address」と言われることがある
    • どういうタイミングかはエラーになるかはよくわからない
  • ミドルか何かでこけた側の「ip addr del...」をきちんとやらないと、困ったことになる
    • keepalivedを使っていたときは、無茶に問題にはならなかったけど
  • APIを叩かないといけないというのが引っかかる(Linuxのコマンドだけで完結して欲しい…)
  • arpテーブルクリアする必要があるんじゃないかなぁ
    • Heartbeatはgarp飛ばしてくれるんだろうか?
    • garpって通るんだろうか?

所感

「わーい仮想IPがつけられるぞー!」と単純に考えていたんだけど、どうも微妙に想定と異なるというか何というか。
EIPのNAT用に複数のPrivae IPがつけられるようになった気がしないでもない。
同じセグメントに複数のENIをつけたときの細かいトラブルがなさそうなのはメリットだけど、今のところ非クラウドの仮想IPそのままという訳ではなさそう。


…なので、今のところ冗長化でのIPの付け替えはENIを差し替える方がよいと思っている。