スクリプトは以下の通り。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を差し替える方がよいと思っている。