Efficient Serverless Deployments with CDK - Infrastructure as Actual Code ⛅
Lambdas made easy
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 😎
Open a terminal window and navigate to the directory where you want to create the CDK project.
Run the following command to create a new Python CDK project :
cdk init app --language python
Change into the newly created directory :
cd <project-name>
Install the
aws-cdk-lib
andaws-cdk.aws-lambda
packages:pip install aws-cdk-lib aws-cdk.aws-lambda
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, )
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 ofFunction
followed by one or more digits. If the function name is invalid, raise aValueError
.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.Create the Lambda function dynamically using the
function_name
,handler_function
, andenv_var_value
variables.In the
app.py
file, update theLambdaStack
instantiation to pass in a list of function names with the patternFunction#
(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.Save the
app.py
file.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()
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 thecdk.out
staging directory and write the output to that directory.Deploy the stack using the following command:
cdk deploy LambdaStack TestLambdaStack --context function_names=Function1,Function2,Function3
This will deploy both the
LambdaStack
andTestLambdaStack
to your AWS account with thefunction_names
context variable set toFunction1,Function2,Function3
.This will create three Lambda functions named
Function1
,Function2
, andFunction3
, 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:
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 aValueError
with an error message indicating that the function name is invalid.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".
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".
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.
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
andfunction_name
:self
is a reference to theLambdaStack
instance andfunction_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 toPYTHON_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. Thegrant_read
operation is called on theS3Bucket
object, passing in thefunction
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.
Created a test file named
test_lambda_stack.py
. Here, we import theunittest
module for creating test cases, as well as the necessary CDK modules. We then define a test case class namedTestLambdaStack
and set up the CDK app and stack in thesetUp
method.We then define two test methods,
test_number_of_functions
andtest_function_environment_variable
, which in turn tests the number of functions and environment variables of each function in the stack, respectively.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 theLambdaStack
implementation before deploying it to an AWS account.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 thecdk.out
directory.Finally, the
LambdaStack
instance is created and synthesized using thecore.App
class and theapp.synth()
method, and is deployed usingcdk 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! 😎