Building a Serverless Website on AWS - Part 2

Project Date February 8th, 2024
Code Available at lakshminkmeda.github
Tools AWS SAM, Rest API, Lambda, Python, DynamoDB

Welcome to the second part of building a serverless website on AWS. A link to Part 1 can be found here.

Pre-requisites for Part 2:

  1. Website files uploaded to S3 bucket.
  2. Cloudfront distribution to serve the website.
  3. AWS SAM CLI installed on the pc. (AWS SAM Installation Help)

    Note: A couple of great resources to getting started with SAM are AWS's own Hello World tutorial and, an even better version is written by Chris Nagy.

The second part of the project is the Visitor Count function that counts the number of visitors to the website. It requires storing the visitor count in a DynamoDB database and updating the value everytime the website is viewed. This has to be done automatically, but, the current website is static and based on S3 bucket which only stores files and can't do any computing. Hence classic methods of using a POST method etc won't work which nesseciates the addition of serverless components to the design.

The key features of the Visitor Count Function are:

  • Code in the website calling the API.
  • A Rest API to trigger a Lambda function.
  • Lambda function to update and retreive the visitor count from a DynamoDB database.
  • DynamoDB database to store the visitor count value.
  • The whole process of creating the API, DynamoDB table, Lambda function, permissions for Lambda to access DynamoDB etc are done using AWS Serverless Application Model (SAM). SAM uses Infrastructure as Code (IaC) concept where the configuration is written in a template file which gets deployed in AWS using SAM Command Line Interface (SAM CLI).


    Step 1: Lambda Function to Update/Pull VisitorCount

    The Python code for the Lambda function to update the visitor count and fetch the updated value from DynamoDB is shown below. The following code is saved as a .py file and placed in a folder named visitor_count. The directory structure can be seen in the github directory.

                    import json
                    import boto3
                    import os
                    
                    # Initialize dynamodb boto3 object
                    dynamodb = boto3.resource('dynamodb')
                    # Set dynamodb table name variable from env
                    #ddbTableName = os.environ['databaseName']
                    ddbTableName = 'VisitorCount'
                    table = dynamodb.Table(ddbTableName)
                    
                    def lambda_handler(event, context):
                        # Update item in table or add if doesn't exist
                        ddbResponse = table.update_item(
                            Key={
                                'id': 'count'
                            },
                            UpdateExpression='SET visitor_count = visitor_count + :value',
                            ExpressionAttributeValues={
                                ':value':1
                            },
                            ReturnValues="UPDATED_NEW"
                        )
                    
                        # Format dynamodb response into variable
                        responseBody = json.dumps({"count": int(ddbResponse["Attributes"]["visitor_count"])})
                    
                        # Create api response object
                        apiResponse = {
                            "isBase64Encoded": False,
                            "statusCode": 200,
                            'headers': {
                                'Access-Control-Allow-Headers': 'Content-Type',
                                'Access-Control-Allow-Origin': '*',
                                'Access-Control-Allow-Methods': 'OPTIONS,POST,GET'
                            },
                            "body": responseBody
                        }
                    
                        # Return api response object
                        return apiResponse
                

    The next step is to write the SAM template to deploy the resources.


    Step 2: Writing the SAM Template

    SAM uses YAML (YAML Ain't Markup Languageā„¢) language for the template and deploys it in AWS using SAM Command Line Interface (CLI). While it simply uses CloudFormation to deploy resources, whereas traditional CloudFormation requires the template to be manually uploaded to an S3 bucket and using console to deploy the template, SAM sets up an S3 bucket, keeps it in sync with the template file and other supporting files such as Lambda python code file etc and deploys the template, all in one go.

    The following image shows the template used to deploy the rescources required for visitor count feature on the website.

                    AWSTemplateFormatVersion: '2010-09-09'
                    Transform: AWS::Serverless-2016-10-31
                    Description: >
                      Visitor Count Feature SAM Template for oncloud9.net
                    
                    Globals:
                      Api:
                        Cors:
                          AllowMethods: "'GET,POST,OPTIONS'"
                          AllowHeaders: "'content-type'"
                          AllowOrigin: "'*'"
                          AllowCredentials: "'*'"
                      Function:
                        Timeout: 3
                    Resources:
                      VisitorCountFunction:
                        Type: AWS::Serverless::Function
                        Properties:
                          CodeUri: visitor_count/
                          Handler: app.lambda_handler
                          Runtime: python3.8
                          Policies: DynamoDBCrudPolicy
                          Events:
                            VisitorCount:
                              Type: Api
                              Properties:
                                Path: /visitor_count
                                Method: get
                    VisitorCount:
                        Type: AWS::Serverless::SimpleTable
                        Properties:
                            PrimaryKey:
                                Name: myKeyName
                                Type: String    
                    Outputs:
                      VisitorCountApi:
                        Description: "API Gateway endpoint URL for Prod stage for Visitor Count function"
                        Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/visitor_count/"
                      VisitorCountFunction:
                        Description: "Visitor Count Lambda Function ARN"
                        Value: !GetAtt VisitorCountFunction.Arn
                      VisitorCountFunctionIamRole:
                        Description: "Implicit IAM Role created for Visitor Count function"
                        Value: !GetAtt VisitorCountFunctionRole.Arn
                

    Once the template is run the following resources get deployed in AWS.
  • S3 bucket to hold the code (template and python files)
  • RestAPI
  • Lambda function with RestAPI as trigger
  • IAM permissions for Lambda to access DynamoDB
  • DynamoDB table


  • Step 3: Python Code to trigger the Rest API

    The Python code to call the REST API can be seen below. The following code is saved in a script file called api_visitorcount and placed along in the scripts folder of website files. Whenever the website is opened in the browser or refreshed, the function gets triggered and calls the Rest API

                    // GET API REQUEST
                    async function get_visitors() {
                      // call post api request function
                      //await post_visitor();
                    try {	
                      let response = await fetch('https://xxxxxxxxxx.execute-api.eu-north-1.amazonaws.com/Prod/visitor_count', {
                        method: 'GET',
                        headers: {}
                        });
                        let data = await response.json()
                        document.getElementById("visitors").innerHTML = "Total visitors: " + data['count'];
                        console.log(data);
                        return data;
                    } catch (err) {
                        console.error(err);
                    }
                  }
                  
                  get_visitors();
                

    Step 4: Finishing Up

    The last step is to find a location for the Visitor Count to show on the website. Just add the following line to the index.html file at the location where you want the visitor count to show.

                <p id="visitors"> &nbsp; </p>
              

    The visitor count function should be working now. The count updates everytime the page is visited. It updates the count even for a page refresh instead of updating the count for a new visitor. To keep track of the cookies while updating the visitor count may be possible but leaving it for a future project.

    The next step is to automate the whole process using a Continuous Integration / Continuous Deployment (CI/CD). Whenever the code for the website is changed, it has to be manually pushed into the S3 bucket and the current Cloudfront distribution has to be invalidated to show the updated website. A CI/CD deployment makes use of repositories such as Github to fastrack the process of deployment to automatically upload the code to the S3 bucket, invalidate the Cloudfront cache to show the updated website which is done in Part - 3.