Drupal 8 as a Static Site: AWS API Gateway, Lambda and SES Form Processing

Submitted by nigel on Wednesday 2nd January 2019
Serverless Installation

The intention is to build our AWS ecosystem using the serverless product. This will enable us to configure our AWS provisioning in a YML file which is then translated into AWS CloudFormation orchestration. In the same codebase our PHP Lambda function can be built out. It isn't my intention to provide heavy documentation here since the Contact Form architecture was defined in an earlier blog, and I have previously written many articles on the serverless / lambda / API Gateway combination of technologies to deliver HTTP content and handle form POSTs. 

Notwithstanding this, I will of course document points of interest along the way so it all makes sense! 

The first step is to install serverless on your system. A dependency is node, so if you don't have that then please follow tutorials elsewhere on the Internet before executing the command below to install globally serverless.
$ npm install serverless -g
We will be using Andy Raines' excellent PHP for AWS Lambda via Serverless Framework repo as our stating point. Issuing the following command will install our PHP serverless project into a directory called d8-contact-form.
$ serverless install --url https://github.com/araines/serverless-php -n d8-contact-form
Serverless: Downloading and installing "serverless-php"...
Serverless: Successfully installed "d8-contact-form" 
We then cd into the directory and list.
$ cd d8-contact-form/
$ ls -las
total 48
0 drwxrwxr-x 16 501 dialout  512 Jan  2 11:14 .
0 drwxr-xr-x 14 501 dialout  448 Jan  2 11:14 ..
4 -rwxr-xr-x  1 501 dialout  660 Feb 14  2018 buildphp.sh
4 -rw-r--r--  1 501 dialout  569 Feb 14  2018 CHANGELOG.md
4 -rw-r--r--  1 501 dialout  430 Feb 14  2018 composer.json
0 drwxrwxr-x  3 501 dialout   96 Jan  2 11:14 config
4 -rw-r--r--  1 501 dialout 1011 Feb 14  2018 dockerfile.buildphp
4 -rw-r--r--  1 501 dialout   40 Feb 14  2018 .gitattributes
4 -rw-r--r--  1 501 dialout  120 Feb 14  2018 .gitignore
4 -rw-r--r--  1 501 dialout 1330 Feb 14  2018 handler.js
4 -rw-r--r--  1 501 dialout  829 Feb 14  2018 handler.php
4 -rw-r--r--  1 501 dialout 1103 Feb 14  2018 LICENSE
4 -rwxr-xr-x  1 501 dialout  133 Feb 14  2018 php
4 -rw-r--r--  1 501 dialout 3705 Feb 14  2018 README.md
4 -rw-r--r--  1 501 dialout 3008 Jan  2 11:14 serverless.yml
0 drwxrwxr-x  5 501 dialout  160 Jan  2 11:14 src
Since I don't have Git LFS installed, the PHP executable hasn't been downloaded from the repo correctly. I will download this manually, and add it to the directory. Once completed, we can see it there with:
$ ls -lash | grep " php"
 27M -rwxr-xr-x  1 501 dialout  27M Jan  2 11:22 php
Now we need to install the PHP dependencies using composer. This does have a dependency on PHP 7.1.
$ composer install -o --no-dev
You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug
Loading composer repositories with package information
Updating dependencies
Package operations: 9 installs, 0 updates, 0 removals
  - Installing psr/log (1.1.0): Downloading (100%)         
  - Installing monolog/monolog (1.24.0): Downloading (100%)         
  - Installing symfony/polyfill-ctype (v1.10.0): Loading from cache
  - Installing symfony/filesystem (v4.2.1): Downloading (100%)         
  - Installing symfony/config (v4.2.1): Downloading (100%)         
  - Installing symfony/contracts (v1.0.2): Downloading (100%)         
  - Installing psr/container (1.0.0): Loading from cache
  - Installing symfony/dependency-injection (v4.2.1): Downloading (100%)         
  - Installing symfony/yaml (v4.2.1): Downloading (100%)         
Writing lock file
Generating optimized autoload files
Any Raines' code comes with a very basic Lambda function called hello. To prove the environment is working correctly, we should invoke it locally (no need for deploying to AWS yet) and if everything is correct, it'll look like this:
$ serverless invoke local -f hello
Got event [] []
{
    "statusCode": 200,
    "body": "Go Serverless v1.0! Your function executed successfully!"
}
In an earlier blog I replaced the references of Hello with demo. I have changed it to D8ContactForm for the contact form. By deploying the code to AWS, and submitting the contact form, you will see the same screenshot as shown at the end of the previous blog.
SES Validation of email address
Email address validation
Verification completed

To use the AWS SES service, your email address will need to be verified. This is easy - navigate to Simple Email Service and click on email addresses, then "Verify a new email address". Add the email address and a verification email message will be sent for you to click back and confirm the verification process. Once verified, you'll see the second screenshot above. 

Serverless configuration
The next step is to add the SES configuration to the serverless.yml file. Add a custom area for your own variables - in this case the email address.
custom:
  SENDER_EMAIL: xxxx@xxxxxxxx
  RECIPIENT_EMAIL: xxxx@xxxxxxxx
And add the SES config to the provider area so it looks like this in its entirety:
provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: eu-west-1
  environment:
    SENDER: ${self:custom.SENDER_EMAIL}
    RECIPIENT: ${self:custom.RECIPIENT_EMAIL}
    DOMAIN: "*"
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "ses:SendEmail"
      Resource: "*"
Then deploy
$ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
Add AWS SDK to the codebase using composer
The SDK will make it much easier to build out the SES interface code and can be installed to the codebase in the usual composer way
$ composer require aws/aws-sdk-php
You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug
Using version ^3.90 for aws/aws-sdk-php
./composer.json has been updated
Add SDK PHP required libraries
The AWS PHP SDK requires the filter and simplexml libraries that aren't in the PHP executable we deploy to AWS by default. Thankfully there is a way of recompiling the PHP executable and I covered this in an earlier blog. So the tl;dr is
  • Add the filter and simplexml libraries to dockerfile.buildphp
  • Make a note of the php creation date and filesize.
  • Make sure buildphp.sh is executable on the command line and run it.
    Add the Serverless and SDK handler code
    Now the AWS SDK is in the code base we can build out the functionality to integrate the SDK. Our first work is to ensure that the recipient and sender email addresses are available to the code. To achieve this, they were defined in the serverless.yml in the environment section which can be picked up in handler.php and assigned to the $event array which is made available to the handler class. Add the following lines before the call to handle
    <?php
    $event
    ['sender_email'] = getenv('SENDER');
    $event['recipient_email'] = getenv('RECIPIENT');
    ?>
    Now the handler itself. A few things to note.
    • I have added the necessary use clauses to the AWS SDK client and the exception class
    • The protected $client_config holds the configuration we have - but note that further down I am getting the credentials from the CredentialProvider
    • The recipients are an array and so it is possible if required to add multiples here.

    D8ContactFormHandler.php

    <?php

    namespace Raines\Serverless;

    require 
    'vendor/autoload.php';

    use 
    Aws\Ses\SesClient;
    use 
    Aws\Exception\AwsException;

    class 
    D8ContactFormHandler implements Handler
    {
        protected 
    $client_config = [
            
    'region' => 'eu-west-1',
            
    'version' => '2010-12-01',
            
    'credentials.cache' => TRUE,
            
    'validation' => FALSE,
        ];

        
    /**
         * {@inheritdoc}
         */
        
    public function handle(array $eventContext $context)
        {
            
    $logger $context->getLogger();
            
    // Comment out for debugging
            //$logger->notice('Got event', $event);

            // Set up AWS SDK
            
    $this->client_config['credentials'] = \Aws\Credentials\CredentialProvider::env();
            
    $SesClient = new SesClient($this->client_config);

            
    $sender_email $event['sender_email'];
            
    $recipient_emails[] = $event['recipient_email'];

            return [
                
    'statusCode' => 200,
                
    'body' => 'Go Serverless v1.0! Your function executed successfully!',
            ];
        }
    }

    ?>
    Email generation
    Now we can insert the code to actually generate the email. All of this is self evident and really doesn't need further commentary from me. It should be inserted just before the return statement.
    D8ContactFormHandler.php
    <?php
            
    // Process the submitted form.
            // *TODO* This could do with more validation.
            
    $fields = [];
            
    parse_str($event['body'], $fields);

            if (!isset(
    $fields['name'])) $fields['name'] = '{unknown name}';
            if (!isset(
    $fields['mail'])) $fields['mail'] = '{unknown email address}';
            if (!isset(
    $fields['subject'][0]['value'])) $fields['subject'][0]['value'] = '{unknown subject}';
            if (!isset(
    $fields['message'][0]['value'])) $fields['message'][0]['value'] = '{unknown message}';

            
    $subject '[badzilla.co.uk website feedback] '.$fields['subject'][0]['value'];
            
    $plaintext_body 'From: '.$fields['name'].'  '.$fields['mail'].
                
    'Subject: '.$fields['subject'][0]['value'].
                
    ' Message: '$fields['message'][0]['value'];
            
    $html_body =  '<h1>'.$fields['subject'][0]['value'].'</h1>'.
                
    '<h2>'.'From: '.$fields['name'].'  <a href="mailto:"'.$fields['mail'].'">'.$fields['mail'].'</a>'.'</h2>'.
                
    '<p>'.$fields['message'][0]['value'].'</p>';
            
    $char_set 'UTF-8';

            try {
                
    $result $SesClient->sendEmail([
                    
    'Destination' => [
                        
    'ToAddresses' => $recipient_emails,
                    ],
                    
    'ReplyToAddresses' => [$sender_email],
                    
    'Source' => $sender_email,
                    
    'Message' => [
                        
    'Body' => [
                            
    'Html' => [
                                
    'Charset' => $char_set,
                                
    'Data' => $html_body,
                            ],
                            
    'Text' => [
                                
    'Charset' => $char_set,
                                
    'Data' => $plaintext_body,
                            ],
                        ],
                        
    'Subject' => [
                            
    'Charset' => $char_set,
                            
    'Data' => $subject,
                        ],
                    ],
                ]);
                
    $messageId $result['MessageId'];
            } catch (
    AwsException $e) {
                
    // output error message if fails
                
    $logger->notice('Message'$e->getMessage());
                
    $logger->notice('AWS Message'$e->getAwsErrorMessage());
            }
    ?>
    Redirect to thank you page
    Once we've sent the email we want to redirect back to our thank you page. This is done with a 307 code which preserves the original POSTed values so they can be used on the forwarded page. Our use will be to make the message personalised. I have decided that I would like my redirect back to my originating page, but of course you could customise it as you see fit. For my case, I use the Referer header. The code below replaces the existing return functionality.
    D8 ContactFormHandler.php
    <?php
            
    return [
                
    'headers' => ['Location' => $event['headers']['Referer']],
                
    'statusCode' => 307,
            ];
    </
    codE>?>
    Debugging
    CloudWatch

    During the development process you will have to do multiple deploys and doubtless you will get numerous error responses from AWS. The best way of working these issues is to use CloudWatch, and a sample screenshot is shown above. The codebase has an excellent logging facility to check variables and these are written out to CloudWatch. Have a look at:

    <?php
    $logger
    ->notice('Got event'$event);
    ?>
    This can be amended for whatever you need to interrogate, but remember the second parameter must be an array.
    Git Repository

    The entire codebase discussed here is at https://github.com/sanddevil/serverless-php-contact-form - if you wish to extend its functionality, please send me a pull request.