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;
 
   $class->SUPER::new(
     service       => 'events',
     api           => 'AWSEvents',
     api_methods   => \@API_METHODS,
     decode_always => 1
   );
 }

 1;

Then...

my $rules = Amazon::CloudWatchEvents->new->ListRules;

DESCRIPTION

Generic class 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 how to 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 of invoking 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, but differences in services make a single solution difficult. All services more or less adhere to this framework:

1. Set HTTP headers (or query string) 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. Optionally send a payload of parameters for the method being invoked

Specific details of the more recent AWS services are well documented, however early services often deviated from some of these patterns or included special parameters. 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 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 invest time reading the documentation on Amazon's website. However, the payoff is that you can probably use this class to call any AWS API and you won't need to include all of CPAN to do so!

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

Amazon::CloudWatchEvents

And invoking an API could be as easy as:

Amazon::API->new(
  service     => 'sqs',
  http_method => 'GET'
}
)->invoke_api('ListQueues');

ERRORS

If an error is encountered an exception class (Amazon::API::Error) will be raised if raise_error has been set. Additionally, a detailed error message will be displayed if print_error is set to true.

See Amazon::API::Error for more details.

METHODS

new

new( options )

Options describe below. Can be a list or hash reference.

action

The API method. Example: PutEvents

api (reqired)

The name of the AWS service. Example: AWSEvents

api_methods

A reference to an array of method names for the API. The new constructor will create methods for each of the method names listed in the array.

Keep in mind these methods are nothing more than stubs. Consult the API documentation for the service to determine what parameters each method requires.

aws_access_key_id

Your AWS access key. Both the access key and secret access key are required.

aws_secret_access_key

Your AWS secret access key.

content_type

Default content for parameters 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 is to pass an empty hash.

$cwe->ListRules();

would be equivalent to...

$cwe->ListRules({});

CAUTION! This may not be what the API expects! Always consult the AWS API for the service you are are calling.

credentials (optional)

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

1. Pass the credentials (aws_access_key_id, aws_secret_access_key, token) keys directly. A session token is typically required when you have assumed a role, your are using the EC2's instance role or a container's role.

Pass the values for the credential key when call the new method.

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 getters for all three credential keys.

Pass the reference as credentials in the constructor as shown here:

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

If you don't explicitly pass 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. See Amazon::Credentials for more details.

NOTE: The latter method of obtaining credentials is probably the easiest to use and provides the most succinct and secure way of obtaining credentials.

debug

Set debug to a true value to enable debug messages. Debug mode will dump the request and response from all API calls.

default: false

decode_always

Set decode_always to a true value to return Perl objects from API method calls. The default is to return the raw output from the call. Typically, API calls will return either XML or JSON encoded objects. Setting decode_always will attempt to decode the content based on the returned content type.

default: false

error

The most recent result of an API call. undef indicates no error was encountered the last time invoke_api was called.

http_method

Sets the HTTP method used to invoke the API. Consult the AWS documentation for each service to determine the method utilized. Most of the more recent services utilize the POST method, however older services like SQS or S3 utilize GET or a combination of methods depending on the specific method being invoked.

default: POST

last_action

The last method call invoked.

Setting this value to true enables a detailed error message containing the error code and any messages returned by the API when errors occur.

default: true

protocol

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

default: https

raise_error

Setting this value to true will raise an exception when errors occur. If you set this value to false you can inspect the error attribute to determine the success or failure of the last method call.

$api->invoke_api('ListQueues');

if ( $api->get_error ) {
  ...
}

default: true

region

The AWS region.

default: $ENV{AWS_REGION}, $ENV{AWS_DEFAULT_REGION}, 'us-east-1'

response

The HTTP response from the last API call.

service

The AWS service name. Example: sqs. This value is used as a prefix when constructing the the service URL (if not url attribute is set).

service_url_base

Deprecated, use service

signer

The class used to sign the request. The default is Amazon::API::Signature4 which is a subclass of Amazon::Signature4.

token

Session token for assumed roles.

url

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

Typically this will be constructed for you based on the region and the service being invoked. You may want to set this manually if, for example you using a local service like <LocalStack|https://localstack.cloud/> that mocks AWS API calls.

my $api = Amazon::API->new(service => 's3', url => 'localhost:4566/');
user_agent

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

version

Sets the API version. Some APIs enable you to set the version.

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 most recent response from an invoked API based on the Content-Type header returned. If there is no Content-Type header, then the method will try to decode it as JSON or XML. If those fail, the raw content is returned.

You can enable decoded responses globally by setting the decode_always attribute.

submit

submit( options )

This method is used internally by invoke_api and normally should not be called by your applications.

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. The default is 'application/x-amz-json-1.1'.

$class->SUPER::new(
  content_type => 'application/x-amz-json-1.1',
  api          => 'AWSEvents',
  service      => 'events'
);

SEE OTHER

Amazon::Credentials, Amazon::API::Error, AWS::Signature4

AUTHOR

Rob Lauer - <rlauer6@comcast.net>

1 POD Error

The following errors were encountered while parsing the POD:

Around line 784:

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