Injecting Python Dependencies Using Lambda Layer and CodePipeline

- By Elena Kosobrodova
Blog post image

Benefits and Limitations of Lambda Layers

Managing dependencies can be difficult regardless of the type of application you are developing: desktop, mobile or cloud. AWS offers three ways of adding dependencies for Lambda functions:

  • build a container image with all the dependencies installed
  • pack them into the zip file together with function’s handler code
  • move them into one or more lambda layers (zip file archives) and share it between multiple lambda functions.

The first two methods have one thing in common - they pull all dependencies each time the function handler code is built. It is especially tedious when you have multiple lambda functions sharing the same dependencies.

Luckily, the deployment time can be significantly reduced if the dependencies are moved into a separate lambda layer which is deployed only when the list of the dependencies is updated. Additional benefits of this solution are reduced costs of artifact storage and faster function invocation. However, even good things have limitations. Lambda layers are not an exception. The limits are:

  • maximum 5 layers per lambda function
  • maximum size of 250 MB for unzipped deployment package including all layers
  • manual update of lambda layer version - when a new version of a lambda layer is created, configuration of the related lambda function must be updated manually.

The Path Forward

In this blog post, I am going to describe how to automate lambda layer versions using CodePipeline and CloudFormation. One of our projects requires several Python lambda functions sharing the same set of map processing packages. The size of the dependencies satisfied the lambda layer limitations. However, our developers were in a discovery mode, and the list of the required packages could change quite frequently, so we decided to automate lambda layer versions. At that time, we already had a large infrastructure built using CloudFormation and CodePipeline which determined our choice of tools.

Several manual steps required automation:

  • creation of a zip file with the dependencies listed in requirements.txt
  • uploading the zip file into S3 bucket
  • publishing a new version of the lambda layer
  • updating ARN of the lambda layer version stored in Parameter Store
  • updating configuration of the related lambda function.

All these operations were performed by a CodeBuild project. Usually, we have several deployment environments including build, dev, uat and production. CodeBuild projects, CodePipeline pipelines, their service IAM roles and artifact buckets are deployed in a build environment. The CodeBuild projects run in the build environment as well. However, in this particular case, the CodeBuild project for lambda layer version automation was deployed in each environment (except for build) to simplify interaction with resources from these environments. The execution of CodeBuild project was controlled by a pipeline, which was triggered by the changes in requirements.txt file only.

Scheme of interaction of CodeBuild project with other AWS resources

With the second version of CodePipeline (PipelineType: V2) a trigger can be configured for a specific file, in our case requirements.txt.

     Triggers:
       - GitConfiguration:
           SourceActionName: Source
           Push:
             - Branches:
                 Includes:
                   - !Ref SourceBranch
               FilePaths:
                 Includes: requirements.txt
         ProviderType: CodeStarSourceConnection

We have around 30 lambda functions in this project and chose to use one YAML file for each lambda function. The Parameter Store was used to pass the ARN of the current lambda layer version into lambda function templates:

Layers:
    - !Sub '{{resolve:ssm:/${ParameterName}}}'

This parameter helps with synchronization. We changed the configurations of lambda functions both through their CloudFormation templates and using CodeBuild project, so one source of truth was required to decide what version of the lambda layer should be used for the current configuration.

CodeBuild Permissions

The service role of the CodeBuild project requires the following permissions:

  1. S3 permissions for multipart upload for the bucket storing the zip file
  - Effect: Allow
    Action:
        - s3:PutObject
        - s3:AbortMultipartUpload
    Resource: !Sub arn:aws:s3:::${BucketName}/*
  - Effect: Allow
    Action:
        - s3:GetBucketLocation
        - s3:ListBucketMultipartUploads
    Resource: !Sub arn:aws:s3:::${BucketName}
  1. Permissions to get and update/create lambda layer version
  - Effect: Allow
    Action: lambda:PublishLayerVersion
    Resource: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:${LayerName}
  - Effect: Allow
    Action: lambda:GetLayerVersion
    Resource: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:${LayerName}:*
  1. Permission to update lambda function configuration
  - Effect: Allow
    Action: lambda:UpdateFunctionConfiguration
    Resource: 
      - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${FunctionName_1}
      - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${FunctionName_2}
  1. Permission to update Parameter Store parameter storing ARN of lambda layer version
  - Effect: Allow
    Action: ssm:PutParameter
    Resource: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${ParameterName}
  1. Permissions for the KMS key used for the bucket encryption
  - Effect: Allow
    Action:
        - kms:Decrypt
        - kms:GenerateDataKey*
    Resource: !Ref S3EncryptionKey

Python Version and CodeBuild Image

Python packages are installed using a virtual environment. It is important to know which Python version can be used for a specific CodeBuild image. For example, we use an aws/codebuild/standard:7.0 image. According to the AWS documentation, the image should support Python versions from 3.9 to 3.13. In fact, at the moment of writing, the image had built-in Python 3.10 and venv had to be installed for this specific version.

Our installation phase included the following commands:

- apt update && apt upgrade -y && apt install -y python3.10-dev python3.10-venv jq libpq-dev
- mkdir -p lambda-layer/python/lib/python3.10/site-packages
- python3.10 -m venv venv3_10
- . venv3_10/bin/activate && pip install -r requirements.txt -t lambda-layer/python/lib/python3.10/site-packages

Make sure you are using a correct path to the content of your lambda layer (python/lib/python3.10/site-packages in our case). The path depends on the runtime. The same Python version must be used for compatible runtimes when a new version of the lambda layer is published.

Updates and Configurations

Lambda layer version pulls its content from the S3 bucket. Obviously, the bucket must exist before the first CodeBuild run. There are two options here. You can create one bucket in the build environment and store zip files for all environments there. In this case, the bucket policy must allow cross-account access. Alternatively, each environment can have its own bucket for lambda dependencies.

To make sure that the upload of dependencies archive is finished before publication of a new lambda layer version, we placed the S3 upload into the pre_build phase of the buildspec.

- cd ""$CODEBUILD_SRC_DIR"/lambda-layer"
- zip -r python.zip *
- aws s3 cp python.zip s3://$BUCKET_NAME/python.zip

All other steps were performed in the build phase.

The following command publishes the lambda layer version, parses the output and assigns the .LayerVersionArn value to the corresponding environment variable:

- export LAYER_VERSION_ARN=$(aws lambda publish-layer-version --layer-name $LAYER_NAME --content S3Bucket=$BUCKET_NAME,S3Key=python.zip --compatible-runtimes python3.10 | jq -r '.LayerVersionArn')

The lambda layer version ARN is then used to update the SSM parameter:

- aws ssm put-parameter --name $SSM_PARAMETER_NAME --value $LAYER_VERSION_ARN --overwrite

and the configurations of all lambda functions included in the LambdaNamesList parameter:

- for lambda_name in ${LambdaNamesList}; do aws lambda update-function-configuration --function-name "$lambda_name" \ --layers $LAYER_VERSION_ARN; done

Afterwords

Although automation of lambda layer deployment required additional effort, we found it worthwhile as it freed our developers and DevOps teams from the burden of manually updating the dependencies, publishing new layer versions and updating lambda configurations. As a bonus, the build time was significantly reduced further improving the development workflow.

About Elena Kosobrodova

DevOps Engineer at Kablamo