AWS SDK for Rubyに無理矢理メソッドを追加する

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」はそれぞれQueryRequestBuilderQueryResponseParserを生成しています。 各クライアントクラスでそれらを引数にして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
  • :name - APIのアクション名
  • :method - Rubyのメソッド名
  • :inputs - リクエストパラメータ
  • :outputs - レスポンスのデータ構造

: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の拡張は早めに削除する予定です。