A Real World PHP Lambda App Part 1: Extend to CloudFront + S3

Submitted by nigel on Saturday 23rd December 2017

We are going to create a voting app to collect information from our users. This will require a HTML form to be initially rendered to the user using the HTTP GET method and then POSTed to the PHP backend for processing and the data saved into AWS DynamoDB. The page should then be redirected to a thank you page. 

We shall call this app vote. So our first activity will be to refer back to my initial blog but instead of create a project demo, call it vote. Once you are ready check your function has been created correctly with the command below. 

$ serverless invoke local -f vote
    "statusCode": 200,
    "body": "Go Serverless v1.0! Your function executed successfully!"
Note how I am changing my regime. I now want to do all my development locally and only rarely deploy to S3. This will cut down S3 costs (although they won't be exorbitant) and speed up development .As we go through the blogs I will point out techniques to aid the local development process.

Ok - so we've reached base camp and we will now build on our sample function.

We will need the aws cli console to be able to upload our app's static assets (css, js, images) to S3 for them to be served by CloudFront. The console has a requirement for Python so that will need to be installed first if you don't already have it. If you already have Python installed, execute the command below. 

# pip install awscli --ignore-installed six
Next we will need to install our first Serverless plugin - this will provide us with the facility to sync our local assets with the S3 bucket we are going to create for our app. The plugin we need to achieve this is serverless-single-page-app-plugin. Let's install it. Navigate to the root directory of our app and issue the following.
$ npm install --save serverless-single-page-app-plugin
This will place the serverless-single-page-app-plugin in the parent directory node_modules. We need to let serverless know about our plugin by adding the following to our serverless.yml file just after the service: section.
  - serverless-single-page-app-plugin
We now need to include into the serverless.yml file the CloudFormation configuration for the S3 assets bucket and for it to serve pages through CloudFront. Thankfully this is provided in the serverless-single-page-app-plugin README.md file so simply copy and paste the following.
    ## Specifying the S3 Bucket
      Type: AWS::S3::Bucket
        AccessControl: PublicRead
          IndexDocument: index.html
          ErrorDocument: index.html
    ## Specifying the policies to make sure all files inside the Bucket are avaialble to CloudFront
      Type: AWS::S3::BucketPolicy
          Ref: WebAppS3Bucket
            - Sid: PublicReadGetObject
              Effect: Allow
              Principal: "*"
              - s3:GetObject
                Fn::Join: [
                  "", [
                    { "Ref": "WebAppS3Bucket" },
    ## Specifying the CloudFront Distribution to server your Web Application
      Type: AWS::CloudFront::Distribution
            - DomainName:
                Fn::Join: [
                  "", [
                    { "Ref": "WebAppS3Bucket" },
              ## An identifier for the origin which must be unique within the distribution
              Id: WebApp
                HTTPPort: 80
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
              ## In case you want to restrict the bucket access use S3OriginConfig and remove CustomOriginConfig
              # S3OriginConfig:
              #   OriginAccessIdentity: origin-access-identity/cloudfront/E127EXAMPLE51Z
          Enabled: 'true'
          ## Uncomment the following section in case you are using a custom domain
          # Aliases:
          # - mysite.example.com
          DefaultRootObject: index.html
          ## Since the Single Page App is taking care of the routing we need to make sure ever path is served with index.html
          ## The only exception are files that actually exist e.h. app.js, reset.css
            - ErrorCode: 404
              ResponseCode: 200
              ResponsePagePath: /index.html
              - DELETE
              - GET
              - HEAD
              - OPTIONS
              - PATCH
              - POST
              - PUT
            ## The origin id defined above
            TargetOriginId: WebApp
            ## Defining if and how the QueryString and Cookies are forwarded to the origin which in this case is S3
              QueryString: 'false'
                Forward: none
            ## The protocol that users can use to access the files in the origin. To allow HTTP use `allow-all`
            ViewerProtocolPolicy: redirect-to-https
          ## The certificate to use when viewers use HTTPS to request objects.
            CloudFrontDefaultCertificate: 'true'
          ## Uncomment the following section in case you want to enable logging for CloudFront requests
          # Logging:
          #   IncludeCookies: 'false'
          #   Bucket: mylogs.s3.amazonaws.com
          #   Prefix: myprefix
  ## In order to print out the hosted domain via `serverless info` we need to define the DomainName output for CloudFormation
        'Ref': WebAppS3Bucket
        'Fn::GetAtt': [ WebAppCloudFrontDistribution, DomainName ]
It's time to deploy to AWS but before you do, check your credentials are loaded, and if not then fix. In my case I added them to the bottom of my ~/.bashrc file then logged out and in for them to take effect.
$ env | grep AWS
Now deploy with
$ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (9.96 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
Serverless: Stack update finished...
Service Information
service: vote
stage: dev
region: eu-west-1
stack: vote-dev
api keys:
  GET - https://n4kofna4l1.execute-api.eu-west-1.amazonaws.com/dev/vote
  vote: vote-dev-vote
This took an eternity first time through! I went and made a cup of tea!
AWS Console - S3

If we now navigate to S3 in the AWS console we should be able to see our two buckets for this project; one for the assets and one for our deployed serverless code. 

AWS Console - CloudFront
AWS CloudFront

Now if we navigate to CloudFront we can see the service we have just created which will serve our S3 assets.

The plugin provides the serverless syncToS3 command which is run to perform the sync, but it requires configuration before we can use it. We need to set a custom variable s3LocalPath in the serverless.yml file.

$ cat serverless.yml | grep -A1 custom:
  s3LocalPath: dist/
I have decided to put my assets in a subdirectory under my root called dist/. I now need to create that local directory and whilst I am at it I'll create an image subdirectory with a sample Badzilla logo image I can test the sync with.
$ mkdir -p dist/images
$ cp ../meedjum/docroot/themes/custom/beezee8/badzilla-logo32x32.png dist/images/.
Now let's be adventurous and see if the sync works.
$ sls syncToS3
Serverless: s3,sync,dist/,s3://vote-dev-webapps3bucket-1htbi30lx4nii/
upload: dist/images/badzilla-logo32x32.png to s3://vote-dev-webapps3bucket-1htbi30lx4nii/images/badzilla-logo32x32.png
Serverless: stderr undefined
Serverless: Successfully synced to the S3 bucket
Yey! Looking good but let's check it properly!
S3 Sync
S3 Sync

Navigate on the console to the S3 bucket, then through the filesystem and the images directory then click on badzilla-logo32x32.png. Looks Good. 

CloudFront Browser Access
CloudFront Browser Access

Now if we point a web browser at your Bazilla logo asset we see that it is rendered correctly (look closely!). Yippee! We now have a working S3 / CloudFront configuration for our industrial strength Lambda function.