最新 追記

雑記帳


2011-08-02 (Tue) [長年日記]

[AWS][Ruby] AWS SDK for Rubyでスナップショットの一覧を取得する

この記事の後半に、パブリックなスナップショットも取得できてしまう、filterなどの指定が使えない…と書いたけど、実はfilterメソッドは存在していて、それを使うことで自分自身のスナップショットのみを取得することができることがあとで分かった。

下記のようにすると、自分自身のスナップショットと、パブリックなスナップショットの両方が取得できてしまう。

# スナップショットの一覧を確認する(自分のものとパブリックのもの)
ec2.snapshots.each do |s|
  print "#{s.id}, #{s.description}\n"
end

自分のスナップショットだけを取得するには、次のようにする。

# スナップショットの一覧を確認する(自分のものだけ)
ec2.snapshots.filter('owner-id', 'YOUR_ACCOUNT_NUMBER').each do |s|
  print "#{s.id}, #{s.description}\n"
end

AWS SDK for Rubyのドキュメントには、特に記載がなかったので気がつかなかっのだけど、filterというメソッドがあり、そこにフィルタする条件を指定できるらしい。

上記のコードでは、条件としてowner-idに自分のアカウント番号である12桁の数字を指定することで、自分自身のスナップショットのみを取得できた。

なお、スナップショットの取得で使用できるfilterの条件は、EC2のAPIリファレンスの、DescribeSnapshotsが参考になる。


2011-08-04 (Thu) [長年日記]

[AWS][Ruby] AWS SDK for RubyでS3の期間限定のURLを生成する

先日参加したZmandaクラウドバックアップセミナーでの玉川憲さんの発表で、S3で期間限定のURLを生成することができるということを知った。これはAWS Manegement Consoleでは簡単に実現できないのでRubyでやってみた。

require 'aws-sdk'

AWS.config(YAML.load(File.read('config.yml')))
o = AWS::S3.new.buckets['your-bucket-name'].objects['object-name']
puts o.url_for(:read, :expires => 60*60*24*31)

これで、下記のようなURLが生成された。このURLは、一ヶ月間だけ有効のものになる。

https://rubysdktest-20110803.s3.amazonaws.com/logo.gif?AWSAccessKeyId=AKIAIFCOKTCTDSDMPX2A&Expires=1315125436&Signature=zEmS9f4%2Fm%2BFLgxEsVA9ppfQDhks%3D

期限が切れたあとにアクセスすると、次のようなXMLが返ってきて、期限が切れてエラーであることを示している。Expiresの値で、いつ期限が切れたのかがわかるようになっている。

<Error>
 <Code>AccessDenied</Code>
 <Message>Request has expired</Message>
 <RequestId>AC51F171E7F19E2C</RequestId>
 <Expires>2011-08-04T02:38:45Z</Expires>
 <HostId>13wxo/zprw3XJmIMeK2HEMAbJZx3C+Men58H1jwAGaSgrXm0v7TTdmoCR3ItaLB0</HostId>
 <ServerTime>2011-08-04T02:44:59Z</ServerTime>
</Error>

ところで、URLに"Expires=..."という文字列が含まれていたので、この値を変更すれば期限が切れた後でもアクセスできるのでは…、と思ってやってみたが、さすがにダメだった。今度は別のエラーになった。

<Error>
 <Code>SignatureDoesNotMatch</Code>
 <Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
 <StringToSignBytes>47 45 54 0a 0a 0a 31 33 31 32 34 32 39 38 32 30 0a 2f 72 75 62 79 73 64 6b 74 65 73 74 2d 32 30 31 31 30 38 30 33 2f 6c 6f 67 6f 2e 67 69 66</StringToSignBytes>
 <RequestId>8F681D480FCEC291</RequestId>
 <HostId>rm5EVi56+MRrDquW45CO3A98Fbnmxqmx7O39ssMbLNE2bBLe41g1fHTghXRmiPOa</HostId>
 <SignatureProvided>MAhvZtzwPBwPvNhsyVCVhNjYa74=</SignatureProvided>
 <StringToSign>GET 1312429820 /rubysdktest-20110803/logo.gif</StringToSign>
 <AWSAccessKeyId>AKIAIFCOKTCTDSDMPX2A</AWSAccessKeyId>
</Error>

署名が合わないエラーになったらしい。QueryStringが署名されたもので、改竄ができないようになっている様子。いやーすばらしい。

ちなみに、ACLの設定で誰でも参照できるように設定されていると、通常のURLでアクセスすれば問題なくアクセスできてしまうので要注意。


2011-08-09 (Tue) [長年日記]

[AWS] AWSで使われるセキュリティ証明書

AWSを触り始めて最初に混乱したのは、AWSにアクセスするために必要な情報が多岐にわたること。

アカウントを作ると、アカウント番号が発行されて、アクセスキーIDとシークレットアクセスキーを生成し、X.509証明書をダウンロードし、キーペアを取得し…と、セキュリティに関係する情報が沢山で、どれをいつ使うのかなどが、最初は分からなかった。

ということで、一旦整理するために調べてみた。AWSのドキュメントに、About AWS Security Credentialsというのがあったので、読み解いてみた。

自分の理解では、次のような感じらしい。

  • キーペア(Key Pair)は、起動するEC2インスタンスに自動的に設定される公開鍵のこと。起動したEC2インスタンスに、いきなり公開鍵認証でサーバにログインできるのには驚いたけど、キーペアはそのためのもの。
  • アクセスキーは、RESTまたはQuery APIを実行する際につかう。対象鍵暗号(共通鍵暗号)方式で、シークレットアクセスキーがその鍵となる。リクエストを共通鍵で暗号化して送る。URIを署名するのにも使われる。90日ごとに変更することが推奨されている。
  • X.509証明書は、一般的には主にSSLやSSHで使われているもので、AWSではSOAP APIの呼び出しに使う。公開鍵暗号方式。キーペアと同じ方式のものなので、なぜ二種類あるのかと思ったのだが、単に用途が違った。

要するに、アクセスするやりかたによって、使用する暗号化方式(セキュリティ証明書)が変わってくる、ということらしい。

あと、セキュリティ証明書とは違うが、APIの呼び出し時にアカウント番号(口座番号)が必要になるケースがある。これは各種リソース(EBS、AMIなど)の共有に使用される。Public AMIなどを検索するときに、そのAMIの所有者を示すアカウント番号を使うことができる。

まだ理解が完全ではないので、今後ももう少し調べてみたい。


2011-08-10 (Wed) [長年日記]

[AWS][Ruby] 各リージョンごとにEC2、AMI、ELB、Snapshot、Elastic IPの一覧を取得

色々と実験をやっていると、どのリージョンに何を作ったのかがわからなくなってくることがあったけど、AWS Management Consoleだとリージョン別でしか見られなくて残念な思いをしたので、スクリプトを作った。

require 'aws-sdk'

def get_status(o)
  "#{o.id}, #{o.tags['Name']}, #{o.status if o.respond_to? :status}"
end

yaml = YAML.load(File.read('config.yml'))
AWS.config(yaml)

ec2 = AWS::EC2.new
owner_id = yaml['owner-id']

ec2.regions.each do |r|
  puts r.name
  r.instances.each do |i|
    puts "\t#{get_status(i)}"
  end

  r.images.filter('owner-id', owner_id).each do |i|
    puts "\t#{get_status(i)}"
  end

  r.volumes.each do |v|
    puts "\t#{get_status(v)}"
  end

  r.snapshots.filter('owner-id', owner_id).each do |s|
    puts "\t#{get_status(s)}"
  end

  r.elastic_ips.each do |i|
   puts "\t#{i.public_ip}, #{i.instance_id or 'not allocated'}"
 end
end

config.ymlに、owner-idを追加する。

owner-id: "000011112222"

実行結果は、こんな感じに。

eu-west-1
us-east-1
        i-d0f88446, test, running
        vol-ad14cf3a, , in_use
ap-northeast-1
        i-bde0085f, sdktest, running
        ami-8c46080c, middleware installed,
        vol-5a627cf0, , in_use
        snap-89af9deb, backup at 2011-08-01, completed
        175.41.255.172, not allocated
us-west-1
ap-southeast-1

現時点ではこの程度でいいけど、今後はもっと対象を増やしたくなりそう。


2011-08-15 (Mon) [長年日記]

[AWS][Ruby] アタッチ済みのボリュームから作成したスナップショットのうち最新のみの一覧を取得する

AWSでは、スナップショットを使って簡単にバックアップが取得できるので、長期的にバックアップを残す場合には、同じボリュームに対してたくさんのスナップショットができることになる。これらのうち、最新のスナップショットの一覧を取得する必要ができたので、例によってRubyで書いてみた。

require 'aws-sdk'

yaml = YAML.load(File.read('config.yml'))
AWS.config(yaml)

ec2 = AWS::EC2.new(:ec2_endpoint => 'ec2.ap-northeast-1.amazonaws.com')

owner_id = yaml['owner-id']

as = []
ec2.volumes.each do |v|
  v.attachments.each do |a|
    as << a
  end
end
ss = {}
r.snapshots.filter('owner-id', owner_id).sort {|a,b|
  b.start_time <=> a.start_time
}.map do |s|
  ss[s.volume_id] = s unless ss.include? s.volume_id
end
as.each do |a|
  vid = a.volume.id
  puts "#{a.instance.id} => #{vid} => #{ss[vid].id} (#{ss[vid].start_time})" if ss.include? vid
end

これを実行した結果は、次のようになる。

i-27835d1d => vol-359a6aea => snap-3ca48ff0 (2011-08-15 07:33:23 UTC)
i-27835d1d => vol-62c8fd63 => snap-85b56c34 (2011-08-15 07:32:49 UTC)

EBSをアタッチしているインスタンスと、ボリューム、それに対応するスナップショットの各IDと、スナップショットの作成された時間(start_time)を出力している。同じボリュームのスナップショットのうち、最も新しいもののみを抽出しているのがポイント。

ただし、スナップショットの量が多いと、特にsortをしているときにタイムアウトしてしまうことも多いので、もう少し工夫が必要。


2011-08-16 (Tue) [長年日記]

[AWS][Ruby] セキュリティグループをコピーする

2011-09-13訂正・追記: もともとここで公開していたスクリプトには重大なバグがあったので、そのバグを訂正したスクリプトに差し替えた。バグの解説は後述。

2011-12-06追記: ICMPでType, Codeを指定している場合に動作に問題がでる可能性があるので、その場合は使用しないこと。詳しくは、2011-12-06の日記を参照のこと。

-----

AWSに新しく作成したアカウントに対して、他のアカウントで使用しているセキュリティグループをそのまま移行したいということがあったので、例によって、Rubyで作った。

今回は、複数のアカウントに対してアクセスするので、config.ymlを次のように修正した。

user1@example.com:
  access_key_id: AAAAAAAAAAAAAAAAAAAA
  secret_access_key: SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS
  owner-id: "000011112222"
user2@example.com:
  access_key_id: BBBBBBBBBBBBBBBBBBBB
  secret_access_key: TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
  owner-id: "333344445555"

ソースコードは以下のとおり。

require 'aws-sdk'
require 'optparse'

dry_run = false
src_account = ''
dst_account = ''

opt = OptionParser.new
opt.on('-n', '--dry-run') {|v| dry_run = true }
opt.on('-s VAL', '--src VAL') {|v| src_account = v}
opt.on('-d VAL', '--dst VAL') {|v| dst_account = v}

opt.parse!(ARGV)

yaml = YAML.load(File.read('config.yml'))
AWS.config(yaml[src_account])
src = AWS::EC2.new(:ec2_endpoint => 'ec2.ap-northeast-1.amazonaws.com')

AWS.config(yaml[dst_account])
dst = AWS::EC2.new(:ec2_endpoint => 'ec2.ap-northeast-1.amazonaws.com')

src.security_groups.each do |src_sg|
  puts "#{src_sg.id}, #{src_sg.name}"

  begin
    dst_sg = dst.security_groups.create(src_sg.name)
  rescue
    dst_sg = dst.security_groups.filter('group-name', src_sg.name).first
  end

  AWS.memoize do
    src_sg.ip_permissions.each do |p|
      puts "#{p.protocol}, #{p.port_range}, #{p.ip_ranges}"
      begin
        unless p.ip_ranges.empty?
          dst_sg.authorize_ingress(p.protocol, p.port_range, *p.ip_ranges) unless dry_run
        end
      rescue => e
        puts e.message
      end
    end
  end
end

次のように呼び出す。

./copy_security_group.rb --src user1@example.com --dst user2@example.com

また、--dry-runを渡すことで、実際にはコピーを行わないモードで実行できる。

これで、user1の方に設定されているセキュリティグループを、そのままuser2の方にまるごとコピーすることができる。

今回のポイント。

  • 複数のアカウントで作業するときに、それぞれの設定でオブジェクトを作って操作できて楽。
  • すでに存在するgroup-name(特に'default')でセキュリティグループ作成しようとすると例外が出てしまうが、事前に確認するのが面倒なので、例外を補足してフィルタで取り直している。
  • また、すでに存在するのと同じ設定をすると、やはり例外が出るので、それも補足したうえで無視している。
  • 既存のip_permission.ip_rangesは配列だが、authorize_ingressなどにそのまま渡すとエラーになるので、*演算子で配列を展開して渡す必要がある。ここはハマりどころだった。

以前のスクリプトのバグについて

まず前提として、authorize_ingressの最後の引数(sources)に何も渡さないと、すべてのアクセス元(0.0.0.0/0)からの許可、という設定になる仕様になっている(Class: AWS::EC2::SecurityGroup ― AWS SDK for Ruby)。

ip_permissionオブジェクトは、同一のprotocolとportsで、異なるsourcesをまとめて保持している。IPアドレスは、ip_ranges、セキュリティグループは、groupsとして、まとめて配列として参照できるようになっている。

以前のバグのあるスクリプトでは、ip_rangesしか考慮しておらず、結果としてgroupsは無視した形になっている。

dst_sg.authorize_ingress(p.protocol, p.port_range, *p.ip_ranges)

しかし上記のコードでは、たとえばTCPの22ポートのアクセス許可が、セキュリティグループでのみ行われている場合に問題になる。p.ip_rangesは空の配列を返す。結果としてauthorize_ingressにはsourcesを渡さないことと同じになってしまう。つまり、どこからでもアクセスできてしまう状態になる(0.0.0.0/0からの許可という設定)。

このバグの解決方法としては、1. groupsは無視して、ip_rangesが空の場合はauthorize_ingressを呼ばないか、2. groupsとip_rangesを足した空でない配列をauthorize_ingressに渡すということになる(ちなみに、明示的に"0.0.0.0/0"が設定されている場合は、ip_rangesは空にはならずに["0.0.0.0/0"]という配列が返ってくる)。今回訂正したスクリプトは、1のアプローチを取っている。

2のアプローチをとらない理由は、セキュリティグループからのアクセスを許可する設定をコピーする際の問題としては、defaultセキュリティグループに最初から入っている、同一セキュリティグループからのアクセスは許可する、といった設定までコピーされてしまうからである。

defaultセキュリティグループに所属するインスタンス同士の通信はすべて許可する、といった設定をコピーするのはもう少し工夫が必要で、コピーするセキュリティグループのアカウント番号を調べて、同一なら設定しようとしているセキュリティグループで許可する、という処理を追加する必要がある。


2011-08-18 (Thu) [長年日記]

[AWS][Ruby] AWS SDK for Ruby 1.1.0 Release

自分が毎日のように使っているAWS SDK for Rubyの1.1.0がリリースされた。

何が嬉しいって、リリースにも書いてあるけど、自分のスナップショットの一覧を取得するのが簡単になったこと。with_ownerメソッドが追加になった。

1.1.0よりも前では、次のように書いていた。

ec2.snapshots.filter('owner-id', 'YOUR_ACCOUNT_NUMBER').each {|s| dosomething }

1.1.0からは、次のように書ける。

ec2.snapshots.with_owner(:self).each {|s| dosomething }

いちいち、owner-idを指定する必要がないという点で嬉しい。また、他のアカウントのものを取得したい場合は、:selfではなく、同じようにアカウント番号を渡せば良い。

ちなみに、リリースにはsnapshotsにしか言及されていないが、同じ問題を抱えていたイメージ(AMI)の一覧も対応している。

ec2.images.with_owner(:self).each {|i| dosomething }

あと、S3のACLの書き換えられるようになっているのも地味だけど便利。

今回のリリースで新しく追加された機能は、主にフォーラムにて要望があったものなので、みんなもフォーラムに投稿するといいよ。


2011-08-24 (Wed) [長年日記]

[AWS] CloudFrontのキャッシュ有効期限を1時間にする(カスタムオリジン)

CloudFrontのキャッシュ有効期間を1時間にする(S3オリジン)では、S3を使ったCloudFrontで有効期限を変更している。これをカスタムオリジンでやってみた。

カスタムオリジンの場合、オリジンとなるサーバ側での設定となる。httpdを使っている場合は、mod_expiresモジュールを使うことで、制御することができる。

例えば、jpegのキャッシュ有効期間を一時間にする場合は、次のようになる。

ExpiresActive On
ExpiresByType image/jpeg "access plus 1 hours"

これを有効にして実際に画像にアクセスすると、次のようにExpiresとCache-Controlヘッダが付与される(CloudFrontはCache-Controlヘッダを見ているらしい)。

HTTP/1.1 200 OK
Date: Wed, 24 Aug 2011 08:18:14 GMT
Server: Apache/2.2.15 (CentOS)
Last-Modified: Fri, 21 Nov 2008 08:44:53 GMT
Etag: "ac4-45c2f0f138b40"
Accept-Ranges: bytes
Content-Length: 2756
Cache-Control: max-age=3600
Expires: Wed, 24 Aug 2011 09:18:14 GMT
Connection: close
Content-Type: image/jpeg

特定の領域にある画像だけ、キャッシュを短くしたい場合などに便利。


2011-08-25 (Thu) [長年日記]

[Linux] 新しいサーバにユーザアカウントを移行する

サーバを今動いているものから新しいものに移行するときに、アカウントも移行することになる。現在のパスワードが分からないけど、パスワードも移行したいという場合がある。

そういったときは、もとのサーバにあるシャドウファイル(/etc/shadow)の、パスワードのフィールドを、新しいサーバに持っていけばよい。

シャドウファイルは、次のような構造になっている。

root:$6$sDEjOgMH$xpRZNXlqALWc7NyjBLtnfqfFCBnWK64HhFrmuFaxdiUnjyZBipIRvXK/F3vJ3WTZ/gcAbmFYhkKAHhviHSQuG/:15203:0:99999:7:::

フィールドは、コロン(:)で区切られている。最初のフィールドがユーザ名で、その次のフィールドが、パスワードになる。ここに書いてあるのは、ハッシュ化されたパスワードなので、パスワードそのものではないが、このフィールドの値を、新しい方のシャドウファイルにペーストすれば、もとのパスワードと同じパスワードでログインできるようになる。


2011-08-29 (Mon) [長年日記]

[AWS][Ruby] セキュリティグループに登録されているIPアドレスを置換する

動的なIPアドレスを使っていたり、引っ越しなどがあったりで、セキュリティグループに登録されているIPアドレスが変更になることがある。セキュリティグループがたくさんあったり、登録している設定が多かったりすると、すべて手作業でやるのは面倒かつミスの元なので、例によってスクリプトを作ってみた。

#!/usr/bin/ruby1.9.1

require 'aws-sdk'

AWS.config(YAML.load(File.read('config.yml'))
src = AWS::EC2.new(:ec2_endpoint => 'ec2.ap-northeast-1.amazonaws.com')

src_ip = 'x.x.x.x/32'
dst_ip = 'y.y.y.y/32'

src.security_groups.each do |src_sg|
  puts "#{src_sg.id}, #{src_sg.name}"

  src_sg.ip_permissions.each do |p|
    if p.ip_ranges.include? src_ip
      begin
        src_sg.authorize_ingress(p.protocol, p.port_range, dst_ip)
        src_sg.revoke_ingress(p.protocol, p.port_range, src_ip)
      rescue => e
        puts e.message
      end
    end
  end
end

作りは単純で、既存のセキュリティグループすべてから、置換もととなるIPアドレス(src_ip)があるかどうかチェックして、もしあれば、新しいIPアドレス(dst_ip)で登録をして、もとのを削除するだけ。

こういう単純作業を、スクリプトを作って簡単にできるのが嬉しい。


2011-08-30 (Tue) [長年日記]

[AWS][Ruby] セキュリティグループを割り当てているインスタンスの一覧を取得する

またしてもセキュリティグループ関係。インスタンスがどのセキュリティグループに登録されているのかはすぐに分かるけど、その逆はAPIが無いので簡単には取れなかった。ということで、作ってみた。

sec_ids = {}
ec2.instances.each do |i|
  i.groups.each do |s|
    sn = "#{s.id}, #{s.name}"
    sec_ids[sn] ||= []
    sec_ids[sn].push "#{i.id}, #{i.tags['Name']}"
  end
end

sec_ids.each_key do |k|
  puts k
  sec_ids[k].each do |v|
    puts "\t#{v}"
  end
end

require 'aws-sdk'とか、ec2オブジェクトを作るコードは割愛。各インスタンスに設定されているセキュリティグループを取ってきて、セキュリティグループごとに整理して出力しているだけ。

ここで、Ruby的に少しハマってしまった。sec_idsというハッシュに配列を入れているのだが、最初は次のようにしてHash#defaultを設定していた。

sec_ids = Hash.new([])
ec2.instances.each do |i|
  i.groups.each do |s|
    sn = "#{s.id}, #{s.name}"
    sec_ids[sn].push "#{i.id}, #{i.tags['Name']}"
  end
end

しかしこれだと上手くいかなくてしばらく悩んでしまった。irbでテストしてみたら、予想とは違う結果になった。

irb(main):001:0> h = Hash.new([])
=> {}
irb(main):002:0> h[:foo]
=> []
irb(main):003:0> h[:foo] << "foo"
=> ["foo"]
irb(main):004:0> h.keys
=> []
irb(main):005:0> h[:bar]
=> ["foo"]

つまり、存在しないキーを参照して、その配列に値を入れると、defaultとして与えた配列そのものに値が追加されてしまう。defaultの配列がdupされている訳ではないらしい。なので、別の存在しないキーを参照したときに、さっき値を入れた配列が返ってきてしまう。

また、一度値を入れているキーも存在しないことになってしまっている。これは謎。

というわけで、初めて現れたキーの時には、新しく配列をセットするようにしたら、期待したどおりの動作になった。

[AWS][Ruby] AWS.memoizeで高速化

以前にアタッチ済みのボリュームから作成したスナップショットのうち最新のみの一覧を取得するという記事の中で、量が多いときはタイムアウトしてしまうと書いた。これは、sortを実行しているときにstart_timeを参照するたびにAPIがリクエストされていて、リクエストしすぎで少しAWS側でリクエストを止められているからのようだった。この対策として、タイムアウトの例外をrescueしてretryしたり、自前でキャッシュしていた。

しかし、AWS.memoizeというのがあることに気がついた。これを使えばAPIの呼び出し回数が減るらしい。

ということで、以前のコードをmemoizeに対応させてみた。

require 'aws-sdk'

yaml = YAML.load(File.read('config.yml'))
AWS.config(yaml)

ec2 = AWS::EC2.new(:ec2_endpoint => 'ec2.ap-northeast-1.amazonaws.com')

owner_id = yaml['owner-id']

as = []
ec2.volumes.each do |v|
  v.attachments.each do |a|
    as << a
  end
end
ss = {}
AWS.memoize do
  ec2.snapshots.filter('owner-id', owner_id).sort {|a,b|
    b.start_time <=> a.start_time
  }.map do |s|
    ss[s.volume_id] = s unless ss.include? s.volume_id
  end
end
as.each do |a|
  vid = a.volume.id
  puts "#{a.instance.id} => #{vid} => #{ss[vid].id} (#{ss[vid].start_time})" if ss.include? vid
end

17行目に、AWS.memoize do ...のブロックを追加しただけ。これで実行してみると、参照しているアトリビュートがメモ化されて、かなり高速化された。簡単ですばらしい。

今回のケースのように、APIのリクエストが大量に発生してしまう場合には、memoizeを使うのは必須。ただし、現時点で最新の1.1.2では、EC2のAPIしか対応していないとのこと。