The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Amazon::API

SYNOPSIS

 package Amazon::CloudWatchEvents;

 use parent qw/Amazon::API/;

 @API_METHODS = qw/
                  DeleteRule
                  DescribeEventBus
                  DescribeRule
                  DisableRule
                  EnableRule
                  ListRuleNamesByTarget
                  ListRules
                  ListTargetsByRule
                  PutEvents
                  PutPermission
                  PutRule
                  PutTargets
                  RemovePermission
                  RemoveTargets
                  TestEventPattern/;

 sub new {
   my $class = shift;
   my $options = shift || {};
 
   $class->SUPER::new({
                      %$options,
                      service_url_base => 'events',
                      version          => undef,
                      api              => 'AWSEvents',
                      api_methods      => \@API_METHODS,
                      content_type     => 'application/x-amz-json-1.1'
                     });
 }

 1;

my $cwe = Amazon::CloudWatchEvents->new();

my $rules = $cwe->ListRules();

DESCRIPTION

Generic class to use for constructing AWS API interfaces. Typically used as the parent class, but can be used directly.

  • See "IMPLEMENTATION NOTES" for using Amazon::API directly to call AWS services.

  • See Amazon::CloudWatchEvents for an example of use this module as a parent class.

BACKGROUND AND MOTIVATION

A comprehensive Perl interface to AWS services similar to the boto library for Python has been a long time in coming. The PAWS project has attempted to create an always up-to-date interface with community support. Some however may find that project a little heavy in the dependency department. If you are looking for an extensible (albeit spartan) method using a subset of services without consuming all of CPAN you might want to consider Amazon::API.

THE APPROACH

Essentially, most AWS APIs are RESTful services that adhere to a common protocol.

1. Set HTTP headers to indicate the API and method to be invoked
2. Set credentials in the header
3. Set API specific headers
4. Sign the request and set the signature in the header
5. Send a payload of parameters for the method being invoked

Specific details of each API call are well documented and early services often deviated from some of these patterns or included special parameters. In any event, this module attempts to account for most if not all of those nuances and provide a fairly generic way of invoking these APIs in the most lightweight way as possible.

Of course, you get what you pay for, so you'll probably need to be very familiar with the APIs you are calling and willng to RTFM on Amazon's website. However, the payoff is that you can probably use this class to call any AWS API.

Think of this class as a DIY kit to invoke just the methods you need to invoke for your AWS project. A good example of creating a quick and dirty interface to CloudWatch Events can be found here:

Amazon::CloudWatchEvents

ERRORS

Errors encountered are returned as an Amazon::API::Error exception object. See Amazon::API::Error/

METHODS

new

 new( options )
credentials (optional)

Accessing AWS services requires passing credentials that with sufficient privileges to invoke those APIs that support the service. This module supports three ways that you can provide those credentials.

1. Pass the credentials keys directly.

Pass the values for the keys below when call the new method.

aws_access_key_id
aws_secret_access_key
token
2. Pass a class that will provide the credential keys.

Pass a reference to a class that has getters for the credential keys. The class should supply get_{credential-key} methods.

Pass the reference as credentials in the constructor.

 my $api = Amazon::API->new({ credentials => $credentials_class, ... });
3. Use the default Amazon::Credentials class.

If you don't pass the credentials or pass a class that will supply credentials, the module will use the Amazon::Credentials class that attempts to find credentials in the environment, your credentials file, or the container or instance role.

This method of obtaining credentials is probably the easiest to use.

user_agent

Your own user agent object or by default LWP::UserAgent. Using Furl, if you have it avaiable may result in faster response.

api (reqired)

The name of the AWS service. Example: AWSEvents

url

The service url. Example: https://events.us-east-1.amazonaws.com

debug

0/1 - will dump request/response if set to true.

action

The API method. Example: PutEvents

content_type

Default content for paraemters passed to the invoke_api() method. The default is application/x-amz-json-1.1. If you are calling an API that does not expect parameters (or all of them are optional and you do not pass a parameter) the default will be to pass an empty hash.

  $cwe->ListRules();

would be equivalent to...

  $cwe->ListRules({});
protocol

One of 'http' or 'https'. Some Amazon services do not support https (yet).

invoke_api

 invoke_api(action, [parameters, [content-type]]);
action
parameters

Parameters to send to the API. Can be a scalar, a hash reference or an array reference.

content-type

If you send the content-type, it is assumed that the parameters are the payload to be sent in the request. Otherwise, the parameters will be converted to a JSON string if the parameters value is a hash reference or a query string if the parameters value is an array reference.

Hence, to send a query string, you should send an array key/value pairs, or an array of scalars of the form Name=Value.

 [ { Action => 'DescribeInstances' } ]
 [ "Action=DescribeInstances" ]

...are both equivalent ways to force the method to send a query string.

decode_response

Attempts to decode the response from the API based on the Content-Type returned in the response header. If there is no Content-Type, then the raw content is returned.

submit

 submit( options )

options is hash of options:

content

Payload to send.

content_type

Content types we have seen used to send values to AWS APIs:

 application/json
 application/x-amz-json-1.0
 application/x-amz-json-1.1
 application/x-www-form-urlencoded

IMPLEMENTATION NOTES

X-Amz-Target

Most of the newer AWS APIs accept a header (X-Amz-Target) in lieu of the CGI parameter Action. Some APIs also want the version in the target, some don't. There is sparse documentation about the nuances of using the REST interface directly to call AWS APIs.

We use the api value as a trigger to indicate we need to set the Action in the X-Amz-Target header. We also check to see if the version needs to be attached to the Action value as required by some APIs.

  if ( $self->get_api ) {
    if ( $self->get_version) {
      $self->set_target(sprintf("%s_%s.%s", $self->get_api, $self->get_version, $self->get_action));
    }
    else {
      $self->set_target(sprintf("%s.%s", $self->get_api, $self->get_action));
    }

    $request->header('X-Amz-Target', $self->get_target());
  }

DynamoDB & KMS seems to be able to use this in lieu of query variables Action & Version, although again, there seems to be a lot of inconsisitency in the APIs. DynamoDB uses DynamoDB_YYYYMMDD.Action while KMS will not take the version that way and prefers TrentService.Action (with no version). There is no explanation in any of the documentations I have been able to find as to what "TrentService" might actually mean.

In general, the AWS API ecosystem is very organic. Each service seems to have its own rules and protocol regarding what the content of the headers should be.

This generic API interface tries to make it possible to use a central class (Amazon::API) as a sort of gateway to the APIs. The most generic interface is simply sending query variables and not much else in the header. APIs like EC2 conform to that protocol, so as indicated above, we use action to determine whether to send the API action in the header or to assume that it is being sent as one of the query variables.

Rolling a New API

The class will stub out methods for the API if you pass an array of API method names. The stub is equivalent to:

 sub some_api {
   my $self = shift;

   $self ->invoke_api('SomeApi', @_);
 }

Some will also be happy to know that the class will create an equivalent CamelCase version of the method. If you choose to override the method, you should override the snake case version of the method.

As an example, here is a possible implementation of Amazon::CloudWatchEvents that implements one of the API calls.

 package Amazon::CloudWatchEvents;

 use parent qw/Amazon::API/;
 
 sub new {
   my $class = shift;
   my $options = shift || {};

   $options->{api} 'AWSEvents';
   $options->{url} 'https://events.us-east-1.amazonaws.com';
   $options->{api_methods} => [ 'ListRules' ];

   return $class->SUPER::new($options);
 }

 1;

Then...

  my $cwe = new Amazon::CloudWatchEvents();
  $cwe->ListRules({});

Of course, creating a class for the service is optional. It may be desirable however to create higher level and more convenient methods that aid the developer in utilizing a particular API.

 my $api = new Amazon::API(
   { credentials => new Amazon::Credentials,
     api         => 'AWSEvents',
     url         => 'https://events.us-east-1.amazonaws.com'
   }
 );

$api->invoke_api( 'ListRules', {} );

Content-Type

Yet another piece of evidence that suggests the organic nature of the Amazon API ecosystem is their use of multiple forms of input to their methods indicated by the required Content-Type for different services. Some of the variations include:

 application/json
 application/x-amz-json-1.0
 application/x-amz-json-1.1
 application/x-www-form-urlencoded

Accordingly, the invoke_api() can be passed the Content-Type or will try to make "best guess" based on the input parameter you passed. It guesses using the following decision tree:

  • If the Content-Type parameter is passed as the third argument, that is used. Full stop.

  • If the parameters value to invoke_api() is a reference, then the Content-Type is either the value of get_content_type or application/x-amzn-json-1.1.

  • If the parameters value to invoke_api() is a scalar, then the Content-Type is application/x-www-form-urlencoded.

You can set the default Content-Type used for the calling service when a reference is passed to the invoke_api() method by passing the content_type option to the constructor.

 $class->SUPER::new({%@_, content_type => 'application/x-amz-json-1.1', api => 'AWSEvents', 
                     url => 'https://events.us-east-1.amazonaws.com'});

SEE OTHER

Amazon::Credentials, Amazon::API::Error

AUTHOR

Rob Lauer - <rlauer6@comcast.net>

1 POD Error

The following errors were encountered while parsing the POD:

Around line 351:

You forgot a '=back' before '=head2'