Package and deploy a PowerShell Lambda function with custom modules

Recently I had the need to create a Lambda function with PowerShell 7. The function is to synchronize data between two REST APIs. It's fairly simple, but does need to use a custom made module. I spent quite bit time to find out how to deploy PowerShell Lambdas with custom modules. Thought might write a guide to help people want to do the same.   

My script is fairly simple, it gets a list of users from one API and then convert it to a XML format object and export into the target API. To reuse some of the code, The script requires AWS.Tools module as well as a custom made module, let's call it CAT. The CAT is a module contains some functions that are invoked by the main script. The inital lines of my main script look like below:
#Requires -Modules @{ModuleName='AWS.Tools.Common';ModuleVersion=''}
#Requires -Modules @{ModuleName='AWS.Tools.SecretsManager';ModuleVersion=''}
#Requires -Modules CAT 
In addition to install the AWS.Tools modules, I manually copied my CAT module folder to the $Env:PSModulePath.

The nex thing is to package the code into a zip file along with all required modules. To do so, install AWSLambdaPSCore module and run this PowerShell command:
New-AWSPowerShellLambdaPackage -ScriptPath "../functions/FunctionName.ps1" -outputPackage ""
You should see the script and modules are all packaged as shown below.
Staging deployment at C:\Users\tom\AppData\Local\Temp\myfunction
Configuring PowerShell to version 7.0.0
Generating C# project C:\Users\tom\AppData\Local\Temp\myfunction\myfunction.csproj used to create Lambda function bundle.
Generating C:\Users\tom\AppData\Local\Temp\myfunction\Bootstrap.cs to load PowerShell script and required modules in Lambda environment.
Generating aws-lambda-tools-defaults.json config file with default values used when publishing project.
Copying PowerShell script to staging directory
Copying local module AWS.Tools.Common( from C:\Users\tom\OneDrive\Documents\PowerShell\Modules\AWS.Tools.Common\
Copying local module AWS.Tools.SecretsManager( from C:\Users\tom\OneDrive\Documents\PowerShell\Modules\AWS.Tools.SecretsManager\
Copying local module CAT(0.0.2) from C:\program files\powershell\7\Modules\CAT
Resolved full output package path as C:\Users\tom\Dropbox\github\myproject\deploy\
Creating deployment package at C:\Users\tom\Dropbox\github\myproject\deploy\
Restoring .NET Lambda deployment tool
Initiate packaging
When deploying this package to AWS Lambda you will need to specify the function handler. The handler for this package is: myfunction::myfunction.Bootstrap::ExecuteFunction. To request Lambda to invoke a specific PowerShell function in your script specify the name of the PowerShell function in the environment variable AWS_POWERSHELL_FUNCTION_HANDLER when publishing the package.
LambdaHandler                                         PathToPackage                                                         PowerShellFunctionHandlerEnvVar
-------------                                         -------------                                                         -------------------------------
myfunction::myfunction.Bootstrap::ExecuteFunction C:\Users\tom\Dropbox\github\myproject\deploy\ AWS_POWERSHELL_FUNCTION_HANDLER
The steps below are not required if you have a CICD pipeline built up like me. As soon as I push my updated code and package to GitHub, the pipeline will automatically upload the package and deploy it. But if don't, read on! 

The next step is to create a S3 bucket to host your package. You can skip this step if you already have a designated bucket for such purpose.

After created the bucket, we will then create a SAM template. SAM template is basically a transformed CloudFormation template. It allows us to add all the neccessary bits like Events and DeadLetterQueue under AWS::Serverless::Function resource. Here's a simple example. Pay close attention to CodeUri. Notice it's pointing to the local zip file instead of a S3 URL? This is by purpose. I will explain in next step.
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
    Type: 'AWS::Serverless::Function'
      Handler: myfunction::myfunction.Bootstrap::ExecuteFunction
      Runtime: dotnetcore3.1
      CodeUri: './'
        - LambdaRole
        - Arn
          ENV: dev
          CATAPPID: Abc12345
          CATSECRETID: CAT-api-key
          VENDORSECRETID: myproject
          Type: Schedule
            Schedule: 'rate(1 day)'
      Timeout: 300
      MemorySize: 256
        owner: tom
    Type: 'AWS::IAM::Role'
      RoleName: myproject-lambdarole
          Key: 'owner'
          Value: 'myteam'
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
        Version: 2012-10-17
          - Effect: Allow
              - 'sts:AssumeRole'
        - PolicyName: MyProjectPolicy
            Version: 2012-10-17
              - Effect: Allow
                Action: secretsmanager:GetSecretValue
                  - Fn::Sub: 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:CAT-api-key'
              - Effect: Allow
                Action: kms:Decrypt
                  - Fn::Sub: 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:CAT-api-key'
After created the template, run the AWS CLI command below. What it does is simply upload the zip file to the specified S3 bucket (myBucket in my example) and update CodeUri in your SAM template with the actual S3 URI path to the zip file. Then output a new template for you to use in the next step. You can open up the SAM template updated.yml to confirm. 
aws cloudformation package --template-file myfunction.yml --s3-bucket mybucket --output-template-file updated.yml
The last step is to deploy the Lambda function from updated.yml
aws cloudformation deploy --stack-name myfunctionstack --template-file updated.yml --capabilities CAPABILITY_AUTO_EXPAND CAPABILITY_NAMED_IAM
That's it folks! I know this isn't as straight forward in comparison to Python and Node.Js runtimes. But as I mentioned before, with a CICD system in place, you should be able to automate the whole deploy process fairly easily. Hope you find it useful!



  1. Very helpful guide! I'll note that a little more background on the creation of the module itself would be handy. What I've found after following this guide:
    - AWS requires you to create a .psd1 in the root directory of your module. Make sure the RootModule parameter in this .psd1 points to your module .psm1 file, like so: "RootModule = 'mymodule.psm1'"
    - If you have multiple functions in this module, make sure you are either listing them as an array or using a wildcard to export them all (though wildcards are discouraged for performance reasons)
    - FunctionsToExport = '*'
    Same goes with any variables you might want to export from your modules script:
    - VariablesToExport = '*'

    Good luck! Setting this up is admittedly muchh more difficult than python/nodejs environments in lambda


Post a Comment

Popular posts from this blog

Install AWS CLI on WSL Ubuntu

On Premise Mailbox user missing in Exchange Online GAL

Receive error: Target mailbox doesn't have an SMTP proxy matching '' when move mailbox to Office 365