AWS-Lambda-Quick

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN

The hard part is configuring AWS to execute the code.  Traditionally
you have to complete the following steps.

- Create a zip file containing your code
- Create (or update) an AWS Lambda function with this zip file
- Create a REST API with AWS Gateway API
- Configure a resource for that REST API for this script
- Set up a method and put method response for that resource
- Manage an integration and integration response for that resource

And then debug all the above things, a lot, and google weird error
messages it generates when you inevitably make a mistake.

This module provides a way to do all of this completely transparently
just by executing your script, without having to either interact with
the AWS Management Console nor directly use the awscli utility.

Simply include this module at the top of your script containing the
handler function:

    use AWS::Lambda::Quick (

README.md  view on Meta::CPAN

If you've only changed the source code and want to deploy a new version
you can just do that by setting the `AWS_LAMBDA_QUICK_UPDATE_CODE_ONLY`
enviroment variable:

    shell$ AWS_LAMBDA_QUICK_UPDATE_CODE_ONLY=1 perl lambda-function.pl

In the interest of being as quick as possible, when this is environment
variable is enabled the URL for the upload is not computed and printed
out.

## Enabling debugging output

To gain a little more insight into what is going on you can set
the `AWS_LAMBDA_QUICK_DEBUG` environment variable to enabled
debugging to STDERR:

    shell$ AWS_LAMBDA_QUICK_DEBUG=1 perl lambda-function.pl
    updating function code
    function code updated
    updating function configuration
    searching for existing role
    found existing role
    ...

# AUTHOR

lib/AWS/Lambda/Quick.pm  view on Meta::CPAN

=item Create a REST API with AWS Gateway API

=item Configure a resource for that REST API for this script

=item Set up a method and put method response for that resource

=item Manage an integration and integration response for that resource

=back

And then debug all the above things, a lot, and google weird error
messages it generates when you inevitably make a mistake.

This module provides a way to do all of this completely transparently
just by executing your script, without having to either interact with
the AWS Management Console nor directly use the awscli utility.

Simply include this module at the top of your script containing the
handler function:

    use AWS::Lambda::Quick (

lib/AWS/Lambda/Quick.pm  view on Meta::CPAN

If you've only changed the source code and want to deploy a new version
you can just do that by setting the C<AWS_LAMBDA_QUICK_UPDATE_CODE_ONLY>
enviroment variable:

   shell$ AWS_LAMBDA_QUICK_UPDATE_CODE_ONLY=1 perl lambda-function.pl

In the interest of being as quick as possible, when this is environment
variable is enabled the URL for the upload is not computed and printed
out.

=head2 Enabling debugging output

To gain a little more insight into what is going on you can set
the C<AWS_LAMBDA_QUICK_DEBUG> environment variable to enabled
debugging to STDERR:

    shell$ AWS_LAMBDA_QUICK_DEBUG=1 perl lambda-function.pl
    updating function code
    function code updated
    updating function configuration
    searching for existing role
    found existing role
    ...

=head1 AUTHOR

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN


has role      => default => 'perl-aws-lambda-quick';
has _role_arn => sub {
    my $self = shift;

    # if whatever we were passed in role was an actual ARN then we
    # can just use that without any further lookups
    if ( $self->role
        =~ /^arn:(aws[a-zA-Z-]*)?:iam::\d{12}:role\/?[a-zA-Z_0-9+=,.@\-_\/]+$/
    ) {
        $self->debug('using passed role arn');
        return $self->role;
    }

    $self->debug('searching for existing role');
    my $aws    = $self->aws;
    my $result = $aws->iam(
        'get-role',
        {
            'role-name' => $self->role,
        }
    );
    if ($result) {
        $self->debug('found existing role');
        return $result->{Role}{Arn};
    }

    $self->debug('creating new role');
    $result = $self->aws_do(
        'iam',
        'create-role',
        {
            'role-name' => $self->role,
            'description' =>
                'Role for lambda functions created by AWS::Lambda::Quick. See https://metacpan.org/pod/AWS::Lambda::Quick for more info.',
            'assume-role-policy-document' => <<'JSON',
{
    "Version": "2012-10-17",

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

                    "lambda.amazonaws.com",
                    "apigateway.amazonaws.com"
                ]
            }
        }
    ]
}
JSON
        }
    );
    $self->debug('new role created');
    $self->debug('attaching permissions to role');
    $self->aws_do(
        'iam',
        'attach-role-policy',
        {
            'policy-arn' =>
                'arn:aws:iam::aws:policy/service-role/AWSLambdaRole',
            'role-name' => $self->role,
        }
    );
    $self->aws_do(
        'iam',
        'attach-role-policy',
        {
            'policy-arn' =>
                'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess',
            'role-name' => $self->role,
        }
    );
    $self->debug('permissions attached to role');
    return $result->{Role}{Arn};
};

### rest api attributes

has rest_api    => default => 'perl-aws-lambda-quick';
has rest_api_id => sub {
    my $self = shift;

    # search existing apis
    $self->debug('searching for existing rest api');
    my $result = $self->aws_do(
        'apigateway',
        'get-rest-apis',
    );
    for ( @{ $result->{items} } ) {
        next unless $_->{name} eq $self->rest_api;
        $self->debug('found existing existing rest api');
        return $_->{id};
    }

    # couldn't find it.  Create a new one
    $self->debug('creating new rest api');
    $result = $self->aws_do(
        'apigateway',
        'create-rest-api',
        {
            name => $self->rest_api,
            description =>
                'Created by AWS::Lambda::Quick. See https://metacpan.org/pod/AWS::Lambda::Quick for more info.',
        },
    );
    $self->debug('created new rest api');
    return $result->{id};
};

has resource_id => sub {
    my $self = shift;

    # TODO: We shold probably make this configurable, right?
    my $path = '/' . $self->name;

    # search existing resources
    $self->debug('searching of existing resource');
    my $result = $self->aws_do(
        'apigateway',
        'get-resources',
        {
            'rest-api-id' => $self->rest_api_id,
        }
    );
    for ( @{ $result->{items} } ) {
        next unless $_->{path} eq $path;
        $self->debug('found exiting resource');
        return $_->{id};
    }

    # couldn't find it.  Create a new one
    $self->debug('creating new resource');
    my $parent_id;
    for ( @{ $result->{items} } ) {
        if ( $_->{path} eq '/' ) {
            $parent_id = $_->{id};
            last;
        }
    }
    unless ($parent_id) {
        die q{Can't find '/' resource to create a new resource from!};
    }
    $result = $self->aws_do(
        'apigateway',
        'create-resource',
        {
            'rest-api-id' => $self->rest_api_id,
            'parent-id'   => $parent_id,
            'path-part'   => $self->name,
        },
    );
    $self->debug('created new resource');
    return $result->{id};
};

has greedy_resource_id => sub {
    my $self = shift;

    my $path = '/' . $self->name . '/{proxy+}';

    # search existing resources
    $self->debug('searching of existing greedy resource');
    my $result = $self->aws_do(
        'apigateway',
        'get-resources',
        {
            'rest-api-id' => $self->rest_api_id,
        }
    );
    for ( @{ $result->{items} } ) {
        next unless $_->{path} eq $path;
        $self->debug('found exiting resource');
        return $_->{id};
    }

    # couldn't find it.  Create a new one
    $self->debug('creating new greedy resource');
    $result = $self->aws_do(
        'apigateway',
        'create-resource',
        {
            'rest-api-id' => $self->rest_api_id,
            'parent-id'   => $self->resource_id,
            'path-part'   => '{proxy+}',
        },
    );
    $self->debug('created new greedy resource');
    return $result->{id};
};

### methods

sub upload {
    my $self = shift;

    my $function_arn = $self->_upload_function;

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

sub _create_method {
    my $self        = shift;
    my $resource_id = shift;

    my @identifiers = (
        'rest-api-id' => $self->rest_api_id,
        'resource-id' => $resource_id,
        'http-method' => 'ANY',
    );

    $self->debug('checking for existing method');

    # get the current method
    my $result = $self->aws->apigateway(
        'get-method', {@identifiers},
    );

    if ($result) {
        $self->debug('found existing method');
        return ();
    }

    $self->debug('putting new method');
    $self->aws_do(
        'apigateway',
        'put-method',
        {
            @identifiers,
            'authorization-type' => 'NONE',
        },
    );
    $self->debug('new method put');

    return ();
}

sub _create_method_response {
    my $self        = shift;
    my $resource_id = shift;

    my $identifiers = {
        'rest-api-id' => $self->rest_api_id,
        'resource-id' => $resource_id,
        'http-method' => 'ANY',
        'status-code' => 200,
    };

    $self->debug('checking for existing method response');

    # get the current method response
    my $result = $self->aws->apigateway(
        'get-method-response', $identifiers,
    );
    if ($result) {
        $self->debug('found existing method response');
        return ();
    }

    $self->debug('putting new method response');
    $self->aws_do(
        'apigateway',
        'put-method-response',
        $identifiers,
    );
    $self->debug('new method response put');

    return ();
}

sub _create_integration {
    my $self         = shift;
    my $function_arn = shift;
    my $resource_id  = shift;

    my $identifiers = {

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

        'resource-id' => $resource_id,
        'http-method' => 'ANY',
    };

    # according the the documentation at https://docs.aws.amazon.com/cli/latest/reference/apigateway/put-integration.html
    # the uri has the form arn:aws:apigateway:{region}:{subdomain.service|service}:path|action/{service_api}
    # "lambda:path/2015-03-31/functions" is the {subdomain.service|service}:path|action for lambda functions
    my $uri
        = "arn:aws:apigateway:@{[ $self->region ]}:lambda:path/2015-03-31/functions/$function_arn/invocations";

    $self->debug('checking for existing integration');

    # get the current method response
    my $result = $self->aws->apigateway(
        'get-integration', $identifiers,
    );
    if ($result) {
        $self->debug('found existing integration');
        return ();
    }

    $self->debug('putting new integration');
    $self->aws_do(
        'apigateway',
        'put-integration',
        {
            %{$identifiers},
            type                      => 'AWS_PROXY',
            'integration-http-method' => 'POST',
            'credential'              => $self->_role_arn,
            uri                       => $uri,
        }
    );
    $self->debug('new integration put');

    return ();
}

sub _create_integration_response {
    my $self        = shift;
    my $resource_id = shift;

    my $identifiers = {
        'rest-api-id' => $self->rest_api_id,
        'resource-id' => $resource_id,
        'http-method' => 'ANY',
        'status-code' => 200,
    };

    $self->debug('checking for existing integration response');

    # get the current method response
    my $result = $self->aws->apigateway(
        'get-integration-response', $identifiers,
    );
    if ($result) {
        $self->debug('found existing integration response');
        return ();
    }

    $self->debug('putting new integration');
    $self->aws_do(
        'apigateway',
        'put-integration-response',
        {
            %{$identifiers},
            'selection-pattern' => q{},
        }
    );
    $self->debug('new integration put');

    return ();
}

sub _upload_function {
    my $self = shift;

    my $update_type = $self->update_type;
    my $region      = $self->region;

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

            my $pv = $region eq 'me-south-1' ? 3 : 4;
            push @{$layers},
                "arn:aws:lambda:$region:445285296882:layer:perl-5-30-paws:$pv";
            next;
        }

        die "Layer '$layer' is neither a known named layer nor a layer arn";
    }

    if ( $update_type eq 'create-function' ) {
        $self->debug('creating new function');
        my $result = $self->aws_do(
            'lambda',
            'create-function',
            {
                'function-name' => $self->name,
                'role'          => $self->_role_arn,
                'region'        => $region,
                'runtime'       => 'provided',
                'zip-file'      => $self->zip_file_blob,
                'handler'       => 'handler.handler',
                'layers'        => $layers,
                'timeout'       => $self->timeout,
                'memory-size'   => $self->memory_size,
            }
        );
        $self->debug('new function created');
        return $result->{FunctionArn};
    }

    $self->debug('updating function code');
    my $result = $self->aws_do(
        'lambda',
        'update-function-code',
        {
            'function-name' => $self->name,
            'zip-file'      => $self->zip_file_blob,
        }
    );
    $self->debug('function code updated');
    $self->debug('updating function configuration');
    $self->aws_do(
        'lambda',
        'update-function-configuration',
        {
            'function-name' => $self->name,
            'role'          => $self->_role_arn,
            'region'        => $region,
            'runtime'       => 'provided',
            'handler'       => 'handler.handler',
            'layers'        => $layers,
            'timeout'       => $self->timeout,
            'memory-size'   => $self->memory_size,
        }
    );
    $self->debug('function congifuration updated');
    return $result->{FunctionArn};
}

# just like $self->aws->$method but throws exception on error
sub aws_do {
    my $self   = shift;
    my $method = shift;

    my $aws    = $self->aws;
    my $result = $aws->$method(@_);

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

    my $code    = $AWS::CLIWrapper::Error->{Code};
    my $message = $AWS::CLIWrapper::Error->{Message};

    die "AWS CLI failure when calling $method $_[0] '$code': $message";
}

sub encode_json($) {
    return JSON::PP->new->ascii->canonical(1)->allow_nonref(1)->encode(shift);
}

sub debug {
    my $self = shift;
    return unless $ENV{AWS_LAMBDA_QUICK_DEBUG};
    for (@_) {
        print STDERR "$_\n" or die "Can't write to fh: $!";
    }
    return ();
}

sub just_update_function_code {
    my $self = shift;



( run in 1.706 second using v1.01-cache-2.11-cpan-49f99fa48dc )