2019 twenty-four merry days of Perl Feed

Taking the Sleigh to a CloudFront

AWS::Lambda::PSGI - 2019-12-17

The Wise Old Elf was very impressed how Sugarplum Snoozysnaps had saved Christmas this year by moving some code to AWS Lambda at the last moment. So impressed that he wanted her to investigate how it could be used to do more.

Right now the elves were running dedicated servers for a few internal Mojolicious apps. Would it be possible to port these to run entirely on AWS Lambda?

Bundling Mojolicious

First, let's download Mojolicious to a local directory with cpanm

    $shell cpanm -L extra Mojolicious
    --> Working on Mojolicious
    Fetching http://www.cpan.org/authors/id/S/SR/SRI/Mojolicious-8.27.tar.gz ... OK
    Configuring Mojolicious-8.27 ... OK
    Building and testing Mojolicious-8.27 ... OK
    Successfully installed Mojolicious-8.27
    1 distribution installed

This creates a local copy of Mojolicious in our file system like so:

    shell$ find extra/lib/perl5
    extra/lib/perl5
    extra/lib/perl5/Mojo.pm
    extra/lib/perl5/Test
    extra/lib/perl5/Test/Mojo.pm
    extra/lib/perl5/ojo.pm
    extra/lib/perl5/Mojolicious.pm

Sugarplum can now ask AWS::Lambda::Quick to upload this for her with the rest of her code by specifying extra as an argument to extra_files.

#!/usr/bin/perl5
use strict;
use warnings;

use AWS::Lambda::Quick (
    name => 'mojo',
    extra_files => [ 'extra' ],
    timeout => 10,
);

Once that's done she can modify her script to load modules from the lib dir within the extra dir. Note the use of the LAMBDA_TASK_ROOT environment variable to help us locate those files:

# Where we put the local Mojo
use lib "$ENV{'LAMBDA_TASK_ROOT'}/extra/lib/perl5";

Since Mojolicious is a pure Perl distribution just uploading the installed files works regardless of different system architecture or perl versions between Sugarplum's local machine and the virtual machine AWS Lambda is running under.

Connecting Lambda to Mojolicious

Sugarplum needs to plumb the various bits together now, to get Mojolicious to somehow understand the hashref passed into the handler function and get it to spit out the hashref that the handler is expected to return.

The first stage is to get AWS::Lambda to talk to any standard web server that can communicate via the PSGI function

use AWS::Lambda::PSGI;

my $psgi_app = sub {
    return [
        200,
        [ 'Content-Type' => 'text/plain' ],
        [ '<html><body>Hello, World @ '.time.'</body></html>' ],
    ];
}

sub handler {
    return AWS::Lambda::PSGI->wrap( $psgi_app )->( @_ );
}

We already know how to get Mojolicious to produce a PSGI application. Sugarplum can use that instead of the hard coded application above:

use Mojolicious::Lite;

my $psgi_app = app->start('psgi');

sub handler {
    return AWS::Lambda::PSGI->wrap( $psgi_app )->( @_ );
}

get '/' => 'index';

1;

__DATA__

@@ index.html.ep

<html>
<body>
Hello, World @ <%= scalar time %>
</body>
</html>

And works! Well, at least Mojolicious executes and produces output of some sort...

mojo error

Uh, but what happened to her page? Why isn't it producing her hello world application?

Well, we remember we declared a route on / but look at that debug output that's not what the on the server is: It's running on /mojo for the base of /quick.

We'll fix that, but before we do, let's consider how Sugarplum is going to host this as a top level domain.

Configuring Cloudfront

Cloudfront is AWS's CDN solution that can sit infront of origin servers, S3 buckets, and - most usefully to Ms Snoozysnaps - the API Gateway running her Lambda function.

For today's exercise Sugarplum is just going to use one of Amazon's own subdomains. When Sugarplum releases the live version she's going to setup a CNAME for domain name she owns - but for development testing she doesn't want to go to the trouble of verifying all that with Amazon.

Sugarplum needs to load up the web interface to setup a new Cloudfront distribution and click on "Web" to see

cloudfront web ui

  • Origin Domain Name should be set to the Lambda gateway domain name (in this case '52p3rf890b.execute-api.us-east-1.amazonaws.com')
  • Origin Path should be set to /quick/mojo
  • Origin Protocol Policy (which will show after you set Origin Domain Name) should be set to HTTPS Only (as our API gateway doesn't support HTTP)
  • Object Caching should be set to Customize, and all the TTLs should be set to 0 to disable caching. Sugarplum Snoozysnaps will want to eventually to have her Mojo app produce custom caching headers to avoid each and every request running through Lambda, but this is good for development.

Finally we Sugarplum can click on the Create Distribution at the very bottom of the page. She's probably got time to go get a cup of fresh eggnog while AWS churns away getting all that set up and allowing domain names to propagate.

When the interface finally tells us it's ready, we can load it up...and still see errors. At least this time we can tell exactly what is broken.

Making the Paths Work

Sugarplum now knows what she needs to do to modify the paths.

  • The base URL needs to have /quick stripped off of it
  • The url itelf needs to have /mojo stripped off

It's important to do both of these so that URLs that Mojolicious creates linking to named routes (i.e. with the url_for or link_to helpers) end up linking to the right place.

Modification of the URLs can be managed by adding a hook that does the modifications before dispatch

app->hook(before_dispatch => sub {
    my $c = shift;

# see https://mojolicious.org/perldoc/Mojolicious/Guides/Cookbook#Rewriting
    # for details on what exactly we're doing here

# remove the '/quick' from the base url
shift @{$c->req->url->base->path->trailing_slash(1)};

# remove the '/mojo' from the main url
shift @{$c->req->url->path->leading_slash(0)};
});

A live test

Just to make sure this is working let's add a few routes that will (a) link to each other via route names (b) change content randomly each page load

#!/usr/bin/perl

use strict;
use warnings;

use AWS::Lambda::Quick (
    name => 'mojo',
    extra_files => [ 'extra' ],
    timeout => 10,
);

use lib "$ENV{'LAMBDA_TASK_ROOT'}/extra/lib/perl5";

use AWS::Lambda::PSGI;
use Mojolicious::Lite;

my $psgi_app = app->start('psgi');
sub handler { return AWS::Lambda::PSGI->wrap( $psgi_app )->( @_ ); }

app->hook(before_dispatch => sub {
    my $c = shift;
    shift @{$c->req->url->base->path->trailing_slash(1)};
    shift @{$c->req->url->path->leading_slash(0)};
});

# path template route name
get '/' => 'index' => 'main-page';
get '/random' => 'random' => 'merry-christmas-page';

1;

__DATA__

@@ index.html.ep

<html>
<body>
<p>Hello, World</p>
<p><%= link_to 'Say Merry Christmas' => 'merry-christmas-page' %></p>
</body>
</html>

@@ random.html.ep

<html>
<body>
% my @options = (
% "Merry Christmas",
% "Happy Holidays",
% "Bon Noel",
% "Feliz Navidad",
% );
%= $options[ rand @options ]; # render a random @option
</body>
</html>

Once we've uploaded that we can finally test it in the browser

Gravatar Image This article contributed by: Mark Fowler <mark@twoshortplanks.com>