AWS Advent Calendar 2013の2日目のエントリです。
RubyからAWSのAPIをたたくときはAWS SDK for Rubyを使うことが多いと思いますが、公式のSDKでも新しい機能のAPIに未対応な場合があります。(例: aws-sdk 1.24.0 での describe_load_balancer_attributes など) アップストリームに対応してもらうのがベストですが、とにかくいますぐ直したい!という場合はライブラリを使う側でメソッドの追加を行うことになります。
ELB管理ツールを作成していたとき、Cross-Zone Load Balancingをつかるようにしようと思ったのですが、そのときはまだdescribe_load_balancer_attributesがありませんでした。 しょうがないから自分でクライアントに追加するか…と思ってソースコードを除いたところ空っぽ。
module AWS class ELB class Client < Core::QueryClient API_VERSION = '2012-06-01' CACHEABLE_REQUESTS = Set[] end class Client::V20120601 < Client define_client_methods('2012-06-01') end end end
いったいメソッドどこで定義されているのかいろいろ調べてみたところ、yamlファイルで定義されていました。
... - :name: DescribeLoadBalancers :method: :describe_load_balancers :inputs: LoadBalancerNames: - :membered_list: - :string ...
なので、yamlファイルから読み出したデータにメソッドと追加すれば、メソッドの追加はできそうです。
実際にメソッドを定義しているのはclient.rbの681行目。
def define_client_methods api_version const_set(:API_VERSION, api_version) api_config = load_api_config(api_version) api_config[:operations].each do |operation| builder = request_builder_for(api_config, operation) parser = response_parser_for(api_config, operation) define_client_method(operation[:method], builder, parser) end end
「request_builder_for」「response_parser_for」はそれぞれQueryRequestBuilder、QueryResponseParserを生成しています。 各クライアントクラスでそれらを引数にしてdefine_client_methodを呼び出せば、未対応のAPIをAWS SDK for Rubyに無理矢理追加できます。
実例
実際に追加してみた例がこちら。
require 'aws-sdk' proc { define_client_method = proc do |operation| operation[:outputs] ||= {} builder = AWS::Core::QueryRequestBuilder.new('2012-06-01', operation) parser = AWS::Core::QueryResponseParser.new(operation[:outputs]) AWS::ELB::Client::V20120601.send(:define_client_method, operation[:method], builder, parser) end define_client_method.call({ :name => 'DescribeLoadBalancerAttributes', :method => :describe_load_balancer_attributes, :inputs => {'LoadBalancerName' => [:string, :required]}, :outputs => { :children => { 'ResponseMetadata' => { :ignore => true, :children => {'RequestId' => {:ignore => true}}}, 'DescribeLoadBalancerAttributesResult' => { :ignore => true, :children => { 'LoadBalancerAttributes' => { :ignore => true, :children => { 'CrossZoneLoadBalancing' => { :children => { 'Enabled' => {:type => :boolean}}}}}}}}} }) define_client_method.call({ :name => 'ModifyLoadBalancerAttributes', :method => :modify_load_balancer_attributes, :inputs => { 'LoadBalancerName' => [:string, :required], 'LoadBalancerAttributes' => [{ :structure => { 'CrossZoneLoadBalancing' => [{ :structure => {'Enabled' => [:boolean]}}]}}, :required] }, }) }.call
:inputs、:outputsはキャメルスタイルをアンダースコアに変換したり、レスポンスをHash・Arrayに変換したりと、いい感じに適当に処理してくれます。
試しに:outputsを渡さなくても
require "aws-sdk" # <= 1.24.0 def define_client_method(operation) operation[:outputs] ||= {} builder = AWS::Core::QueryRequestBuilder.new('2012-06-01', operation) parser = AWS::Core::QueryResponseParser.new(operation[:outputs]) AWS::ELB::Client::V20120601.send(:define_client_method, operation[:method], builder, parser) end define_client_method({ :name => 'DescribeLoadBalancerAttributes', :method => :describe_load_balancer_attributes, :inputs => {'LoadBalancerName' => [:string, :required]}, }) client = AWS::ELB.new.client p client.describe_load_balancer_attributes(:load_balancer_name => 'test') #=> {:describe_load_balancer_attributes_result=>{:load_balancer_attributes=>{:cross_zone_load_balancing=>{:enabled=>"false"}}}, :response_metadata=>{:request_id=>"aec41886-5a7f-11e3-a7e6-fb50c249cc25"}}
と、すっきりした感じのデータが得られます。 データ構造を細かく指定してやれば必要なデータだけ得られます。
define_client_method({ :name => 'DescribeLoadBalancerAttributes', :method => :describe_load_balancer_attributes, :inputs => {'LoadBalancerName' => [:string, :required]}, :outputs => { :children => { 'ResponseMetadata' => { :ignore => true, :children => {'RequestId' => {:ignore => true}}}, 'DescribeLoadBalancerAttributesResult' => { :ignore => true, :children => { 'LoadBalancerAttributes' => { :ignore => true, :children => { 'CrossZoneLoadBalancing' => { :children => { 'Enabled' => {:type => :boolean}}}}}}}}} }) client = AWS::ELB.new.client p client.describe_load_balancer_attributes(:load_balancer_name => 'test') #=> {:cross_zone_load_balancing=>{:enabled=>false}}
まとめ
「新しいAPIが追加されたけどSDKはまだ対応していない!でもSDKのバージョンアップは待てない!」という方は、とりあえずの対応として新API用のメソッドを追加することはできますよ、ということで。
ちなみにdescribe_load_balancer_attributesはすでにSDK本体に追加されているので、Kelbimの拡張は早めに削除する予定です。