Efficient Serverless Deployments with CDK - Infrastructure as Actual Code ⛅

Lambdas made easy

Efficient Serverless Deployments with CDK - Infrastructure as Actual Code ⛅

Are you ready for a deep dive into the world of AWS Cloud Development Kit (CDK) and how to consolidate multiple AWS Lambda functions into a single AWS CloudFormation template using Python? 🤔

If so, grab a cup of coffee ☕ and let's get started!

In this blog post, we will guide you through the step-by-step process of creating a Proof of Concept (PoC) and explore the benefits of this approach. By the end of this post, you'll have a better understanding of how to simplify your infrastructure, reduce maintenance overhead, and best practices for working with AWS CDK. So sit back, relax, and let's dive in! 🚀


Lambdas made easy 😎

  1. Open a terminal window and navigate to the directory where you want to create the CDK project.

  2. Run the following command to create a new Python CDK project :

     cdk init app --language python
    
  3. Change into the newly created directory :

     cd <project-name>
    
  4. Install the aws-cdk-lib and aws-cdk.aws-lambda packages:

     pip install aws-cdk-lib aws-cdk.aws-lambda
    
  5. Open the app.py file in your preferred text editor, and add the following import statements at the top of the file:

     from typing import List
     import re
     from aws_cdk import (
         core,
         aws_lambda as _lambda,
         aws_s3 as s3,
     )
    
  6. Replace the contents of the AppStack class with the following code:

     class LambdaStack(core.Stack):
          def __init__(self, scope: core.Construct, id: str, function_names: List[str], s3_bucket_name: str, **kwargs) -> None:
              super().__init__(scope, id, **kwargs)
    
              # create an S3Bucket object to represent the existing S3 bucket
              s3_bucket = s3.Bucket.from_bucket_name(self, "ImportedBucket", bucket_name=s3_bucket_name)
    
              for function_name in function_names:
                  if not re.match(r'^Function\d+$', function_name):
                      raise ValueError(f"Invalid function name: {function_name}")
    
                  function_number = function_name[len('Function'):]
    
                  handler_function = f'handler{function_number}.lambda_handler'
                  env_var_value = f'VALUE{function_number}'
    
                  function = _lambda.Function(
                      self, function_name,
                      runtime=_lambda.Runtime.PYTHON_3_8,
                      handler=handler_function,
                      code=_lambda.Code.from_bucket(bucket=s3_bucket, key=f'lambda_functions/{function_name.lower()}'),
                      environment={
                          'ENV_VAR': env_var_value
                      }
                  )
                  # grant read permissions to the Lambda function to access the S3 object
                  s3_bucket.grant_read(function)
    
     app = core.App()
     LambdaStack(app, "lambda-stack", function_names=["Function1", "Function2"], s3_bucket_name='my-lambda-assets-bucket')
     app.synth()
    

    In the LambdaStack class, modify the __init__ method to use regular expressions to check that the function name matches a pattern of Function followed by one or more digits. If the function name is invalid, raise a ValueError.

  7. Use the len function to extract the number from the function name, and use that to construct the handler function name and environment variable value.

  8. Create the Lambda function dynamically using the function_name, handler_function, and env_var_value variables.

  9. In the app.py file, update the LambdaStack instantiation to pass in a list of function names with the pattern Function# (eg: Function1, Function2) separated by commas:

     LambdaStack(app, "lambda-stack", function_names=["Function1", "Function2"], s3_bucket_name='my-lambda-assets-bucket')
    

    Make sure to replace my-lambda-assets-bucket with the name of the S3 bucket where your Lambda function code is stored.

  10. Save the app.py file.

  11. We can then create a test file named test_lambda_stack.py and import the necessary modules for a CDK app:

    import unittest
    from aws_cdk import core
    from my_cdk_app.my_cdk_app_stack import LambdaStack
    
    class TestLambdaStack(unittest.TestCase):
    
        def setUp(self):
            self.app = core.App()
            self.stack = LambdaStack(app, "lambda-stack", function_names=["Function1", "Function2"], s3_bucket_name='my-lambda-assets-bucket')
    
        def test_number_of_functions(self):
            self.assertEqual(len(self.stack.functions), 2)
    
        def test_function_environment_variables(self):
            for function in self.stack.functions:
                self.assertIn('ENV_VAR', function.environment)
    
    if __name__ == '__main__':
        unittest.main()
    
  12. We can then synthesize the AWS CloudFormation template using the following command:

    cdk synth LambdaStack --no-staging > LambdaStack.template.json
    
    cdk synth TestLambdaStack --no-staging > TestLambdaStack.template.json
    

    The --no-staging option can be useful if you want to avoid creating a temporary directory or if you need to write the output directly to a specific directory for deployment purposes. However, if you don't specify the --no-staging option, cdk synth will automatically create the cdk.out staging directory and write the output to that directory.

  13. Deploy the stack using the following command:

    cdk deploy LambdaStack TestLambdaStack --context function_names=Function1,Function2,Function3
    

    This will deploy both the LambdaStack and TestLambdaStack to your AWS account with the function_names context variable set to Function1,Function2,Function3.

    This will create three Lambda functions named Function1, Function2, and Function3, with corresponding environment variables and handler functions.🎉


Under the Hood 🔮

The code defines a new AWS CloudFormation stack LambdaStack creates one or more Lambda functions using the AWS Cloud Development Kit (CDK) for Python.

The constructor for LambdaStack takes three parameters: scope, id, and function_names. scope and id are standard parameters for creating a new AWS CDK stack, while function_names are a list of strings representing the names of the Lambda functions that should be created.

The code iterates through the list of function names using a for loop, and each function name performs the following steps:

  1. Validates the function name: If the function name does not match the pattern ^Function\d+$ (where \d+ is one or more digits), the code raises a ValueError with an error message indicating that the function name is invalid.

  2. Extracts the function number from the function name: The code extracts the numeric portion of the function name by taking a substring of the function name starting from the end of the string after the string "Function".

  3. Constructs the name of the handler function: The code constructs the name of the Python function that will serve as the entry point for the Lambda function. The name of the function is constructed by concatenating the string "handler" with the function number extracted in step 2, followed by the string ".lambda_handler".

  4. Constructs the name of the environment variable: The code constructs the name of an environment variable that will be passed to the Lambda function. The name of the environment variable is constructed by concatenating the string "VALUE" with the function number extracted in step 2.

  5. Creates the Lambda function: The code creates a new instance of the _lambda.Function class, which represents a Lambda function. The constructor for the _lambda.Function class takes several parameters:

    • self and function_name: self is a reference to the LambdaStack instance and function_name is the name of the Lambda function being created.

    • runtime: Specifies the runtime environment that will be used to execute the Lambda function. In this case, the runtime environment is set to PYTHON_3_8.

    • handler: Specifies the name of the Python function that will serve as the entry point for the Lambda function. The name of the handler function is constructed in step 3.

    • code: Specifies the source code of the Lambda function. In this case, the source code is provided from an existing S3 object that contains the function code. The grant_read operation is called on the S3Bucket object, passing in the function object as the grantee. This operation adds an S3 bucket policy that grants read permissions to the Lambda function to access the S3 object that contains the function code.

    • environment: Specifies a dictionary of environment variables that will be passed to the Lambda function. In this case, the dictionary contains a single key-value pair with the key 'ENV_VAR' and the value constructed in step 4.

  6. Created a test file named test_lambda_stack.py. Here, we import the unittest module for creating test cases, as well as the necessary CDK modules. We then define a test case class named TestLambdaStack and set up the CDK app and stack in the setUp method.

    We then define two test methods, test_number_of_functions and test_function_environment_variable, which in turn tests the number of functions and environment variables of each function in the stack, respectively.

  7. Then, we use the unittest.main() method to run the tests when the test file is executed. Note that the test file should be located in the same directory as the CDK app code, and should import the necessary modules from the app stack file.

    These tests help ensure that the LambdaStack object is creating the expected number of Lambda functions, and each function has the required environment variables set. By running these tests, you can catch any issues with the LambdaStack implementation before deploying it to an AWS account.

  8. Synthesize the AWS CloudFormation template: Use the cdk synth command to generate an AWS CloudFormation template for your infrastructure. This command will convert your AWS CDK code into an AWS CloudFormation template, which can be viewed in the cdk.out directory.

  9. Finally, the LambdaStack instance is created and synthesized using the core.App class and the app.synth() method, and is deployed using cdk deploy as is standard for AWS CDK applications.

And that's it!✌️ With these modifications, the LambdaStack class will dynamically create Lambda functions based on a pattern in the function name.


CDK Magic: From Code to CloudFormation 🪄

Here's the complete YAML template for the LambdaStack in case of a single lambda function generated by the AWS CDK:

  • LambdaStack:

      ---
      AWSTemplateFormatVersion: '2010-09-09'
      Description: (python=3.8) Synthesized stack using CDK for constructs.LambdaStack
      Resources:
        LambdaStackImportedBucketE2BFB5B7:
          Type: 'AWS::S3::Bucket'
          DeletionPolicy: Retain
          UpdateReplacePolicy: Retain
          Properties:
            BucketName: my-lambda-assets-bucket
        LambdaStackFunction11D1F2C2:
          Type: 'AWS::Lambda::Function'
          Properties:
            Code:
              S3Bucket: !Ref LambdaStackImportedBucketE2BFB5B7
              S3Key: lambda_functions/function1
            Environment:
              Variables:
                ENV_VAR: VALUE1
            Handler: handler1.lambda_handler
            MemorySize: 128
            Role: !GetAtt LambdaStackFunction11D1F2C2ServiceRole39E6E726.Arn
            Runtime: python3.8
            Timeout: 3
          DependsOn:
            - LambdaStackImportedBucketE2BFB5B7
          Metadata:
            aws:cdk:path: LambdaStack/LambdaStackFunction1/Resource
        LambdaStackFunction11D1F2C2ServiceRole39E6E726:
          Type: 'AWS::IAM::Role'
          Properties:
            AssumeRolePolicyDocument:
              Statement:
                - Action:
                    - 'sts:AssumeRole'
                  Effect: Allow
                  Principal:
                    Service:
                      - lambda.amazonaws.com
              Version: '2012-10-17'
            ManagedPolicyArns:
              - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
            Policies:
              - PolicyDocument:
                  Statement:
                    - Action:
                        - 's3:GetObject'
                      Effect: Allow
                      Resource: !Join
                        - ''
                        - - 'arn:'
                          - !Ref 'AWS::Partition'
                          - ':s3:::'
                          - !Ref LambdaStackImportedBucketE2BFB5B7
                          - '/lambda_functions/function1'
                  Version: '2012-10-17'
                PolicyName: LambdaStackFunction11D1F2C2ServiceRole39E6E726DefaultPolicyAF6E7D6B
          Metadata:
            aws:cdk:path: LambdaStack/LambdaStackFunction1/ServiceRole/Resource
        LambdaStackFunction21DFA8F9ServiceRole0E5D4FA2:
          Type: 'AWS::IAM::Role'
          Properties:
            AssumeRolePolicyDocument:
              Statement:
                - Action:
                    - 'sts:Assume
    

Conclusion 💡

Congratulations!🍾 You've successfully consolidated multiple Lambda functions into a single CDK stack and synthesized the CloudFormation template for deployment. By following the steps outlined in this blog, you've gained valuable insights into how CDK can help you to manage your AWS resources in a more efficient and scalable way. With Infrastructure as Actual Code and CDK Magic, you're now well-equipped to take on even more complex AWS projects with confidence. Happy coding! 😎