Serverless Primer Part 3 - More than just functions

Serverless Primer Part 3 - More than just functions

Congratulations! You have made it to the end of my serverless primer. What do you mean you just got here? Fine, scroll down a bit to read the previous two entries, or don’t if it’s TL;DR. Regardless of your level of familiarity with my blog, I thank you for taking the time to read through. In this post, I will showcase a few of the other AWS technologies that one might use when setting up serverless applications.

AWS has a number of services that are presented in a serverless model. A short list of a number of them are: Lambda, S3, EFS, DynamoDB, Aurora Serverless, API Gateway, SNS, SQS, Step Functions, Kinesis, Athena, and others. In this post I will go over AWS DynamoDB and API gateway, to put together a (very) basic web app, with an API front end and a database backend.

DynamoDB

DynamoDB is a fully managed, serverless, NoSQL database that stores data as documents and key-value pairs. It can automatically scale and offers two primary billing models: on-demand and provisioned capacity.

On-demand capacity is best used for unpredictable or unknown workloads, or if you would rather pay only for what you use (The likely choice for entirely serverless deployments.)

Provisioned capacity is best for predictable, consistent traffic that can be easily forecasted out to keep excess costs to a minimum.

API Gateway

API Gateway is a fully managed API service from AWS that fits nicely within the serverless deployment strategy. It is billed on a per request model (at very economical rates with a free tier eligibility for new accounts) and is infinitely scalable with no AWS imposed throttling. (But you can setup your own throttles as needed, to not overwhelm the backend.)

All together now

Now let’s put all of these random serverless elements together to make an actual working “application”. In thisCloudFormation template I build out a Python Lambda Function, with inline app code, use a DynamoDB table as the data backend, and use API gateway to, yes you guessed it, add an API in the front to interact with the app. In a mere ~75 seconds, CloudFormation will deploy all of the above components, ready for consumption.

End of story, no further explanation required… Fine, for those that need a little more detail, keep reading.

Breaking down the elements of the template:

DynamoDB:

DynamoDB:
  Type: 'AWS::DynamoDB::Table'
    Properties:
      AttributeDefinitions:
        - AttributeName: TestId
          AttributeType: S
      KeySchema:
        - AttributeName: TestId
          KeyType: HASH
      TableName: !Sub '${AWS::StackName}-db'
      BillingMode: PAY_PER_REQUEST

The following properties are defined:

AttributeDefinitions and KeySchema are required to set the DynamoDB parition key.

TableName , like most item name properties in CloudFormation, is an optional field. I change the names in this template mostly for the easy identification of resources after deployment but in production it is less common as it can be more restrictive to future changes. Typically you will point to references, like I do to generate this name, to address your resources.

BillingMode Specifies the On-Demand capacity model. It is an optional property, as Provisioned, is the default value. If you decided to stick to provisioned, you would need to set the read/write capacity with a ProvisionedThroughput object, which specifies the Read and Write Capacity Units.

Lambda:

LambdaFunction:
  Type: 'AWS::Lambda::Function'
    Properties:
      Environment:
        Variables:
          tableName: !Sub '${AWS::StackName}-db'
      Code:
        ZipFile: |
          import sys, boto3, os
          client = boto3.client('dynamodb')
          def main(event, context):
              conCat = event['k1'] + event['k2']
              response = client.put_item(
                TableName=os.environ['tableName'],
                Item={
                  'TestId': {'S': '1'},
                  'ConcatString': {'S': conCat}
                }
              )
          if __name__ == '__main__':
              main(event, context)
      FunctionName: !Sub '${AWS::StackName}-function'
      Role: !GetAtt LambdaRole.Arn
      Handler: index.main
      Runtime: python3.7

The Lambda Function here is similar to the one I used in part one, with a couple of changes. I used the Environment property to set an environment variable for the DynamboDB table name and in code I entered the function code inline to keep everything contained to just this template. In a typical scenario, I would have the code stored in an S3 bucket, to keep the template cleaner and to avoid some of the limitations of inline code.

API Gateway:

 ApiGateway:
   Type: 'AWS::ApiGateway::RestApi'
   Properties:
     Name: !Sub '${AWS::StackName}-api'
 ApiGatewayRestPOSTMethod:
   Type: 'AWS::ApiGateway::Method'
   Properties:
     ResourceId: !GetAtt ApiGateway.RootResourceId
     RestApiId: !Ref ApiGateway
     AuthorizationType: NONE
     HttpMethod: POST
     MethodResponses:
       - StatusCode: '200'
     Integration:
       IntegrationHttpMethod: POST
       Type: AWS
       Uri: !Sub >-
         arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction.Arn}/invocations
       IntegrationResponses:
         - StatusCode: '200'
 ApiGatewayDeploy:
   DependsOn: ApiGatewayRestPOSTMethod
   Type: 'AWS::ApiGateway::Deployment'
   Properties:
     RestApiId: !Ref ApiGateway
     StageName: !Ref 'AWS::StackName'

As you can see, to create an API gateway with CloudFormation, a few resources need to be created.

Type AWS::ApiGateway::RestApi requires no properties for creation but I did set the Name for easy identification.

Next comes the meat of the API configuration with the method type AWS::ApiGateway::Method. The properties needed for the method are:

ResourceId and RestApiId to map to the API gateway. AuthorizationType which for this demo is set to None but in production it would be advised to for it be specified. HttpMethod specifies the HTTP method (POST, GET, PUT, etc) that clients will use to call the method. MethodResponses are a list of the status codes that map to the method’s integration response. Integration is a mapping to the AWS backend service that the API method is calling.

Finally there is the stage deployment which is used to make the API callable. Whenever there are changes made to the API the stage needs to be updated or a new stage needs to be deployed. Within CloudFormation we will use the type AWS::ApiGateway::Deployment for our stage deployment. The only required setting for deployment is the RestApiId of the API Gateway. You will also note that I used the DependsOn attribute. If you are creating AWS::ApiGateway::RestApi and AWS::ApiGateway::Method in the same template as the deploy, the DependsOn will insure that the RestApi and its methods are created first to prevent an error on stack deployment.

The final part of the template, starting with LambdaRole, is setting the permissions in the least privilege security model for each resource within the stack. API Gateway -> Lambda Function -> DynamoDB Table I wont do a line by line on the IAM policy creation portion of the template as IAM could easily be its own post in itself. Maybe in a future posting I will do a deeper dive into it.

Now that we have gone through the template, on to deployment.

But does it actually work?

Ok so we’ve built this super fancy template that sets up a whole bunch of stuff… but does it actually work? Indeed it does! If you goto API Gateway in the AWS Console and click on your API you will get a list of the methods (POST will be the only one listed since it is the sole method that was created with the template.) Click on the POST method and then on TEST. The test will be expecting a Request Body, in json format, with two key pairs named k1 and k2. If you used the Lambda Code, that is in the Cloudformation template, use the following json template:

{
  "k1":"First String",
  "k2":"Second String"
}

When you run the test you will get a Response Body message of null and the logs will show, at the end, : Method completed with status: 200 if everything was successful. Awesome, no more verification is required right?! What do you need more proof of something actually happening? I don’t blame you, I wouldn’t trust my code either. Browse to the DynamoDB console, go to tables, and click on the table that starts with the name of the stack you created. If you click on items you will see a single item with the partition key TestId value of 1 that has the values of k1 and k2 concatenated. Awesome complex web application for the win!

Obviously this is a very rudimentary application but the primary purpose was to show an entirely serverless stack being deployed from 124 lines of yaml in the CloudFormation template. Go out and experiment with CloudFormation and work towards building all of your AWS deployments using it. A good bit of extra time will be spent learning the nuances of CloudFormation but it is a very powerful free tool that can make future deployments much easier to document and manage.

END OF LINE

And there we have it, the end of this primer. It has certainly taken me a lot longer to finish than I had planned but life and certification studies always seem to get in the way. There are a number of serverless services that I have not yet touched on, (In good likelihood it’s because I haven’t used them) but I certainly plan to, going forward, and will share my adventures using them. In my next post I will have a little more fun with the topic but that will all depend in which direction my creativity decides to strike. (I know your excitement will be hard to contain in the meantime)

Thank you for taking the time to read my post and hope it was informative and maybe slightly entertaining. Feel free to reach out to me with any questions or feedback on LinkedIn or Twitter @GregMadro.