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