Automating Using PowerShell and Lambda Part 1: Getting Started

Daniel Norred
Daniel Norred
PowershellAWSLambda

For the uninformed, what is Lambda?

Lambda is a Server-less function compute service, letting you run arbitrary code without having to manage underlying infrastructure. The AWS Flavor of Azure Functions or GCloud Functions.
Server-less functions are uniquely useful for tasks where running and managing a dedicated server isn't cost or time effective for the task at hand. With per millisecond billing, you pay for the time your code is being executed, making that automation you've been planning suddenly cost effective with less administrative overhead.

There's a few gotchas when it comes to bringing PowerShell to a server-less framework. Writing a PowerShell function for Lambda isn't a 1 - 1 experience for someone who has been scripting on the desktop for the last year. Powershell is notoriously slow compared to other languages. When you're being billed by the ms, speed is a priority. Using PowerShell compared to a language that is compiled to machine code like Go, will always be more expensive.

How much does it cost?

Using Cloudwatch and Lambda insights we can monitor our functions execution time to estimate the average cost for using the Lambda function made later in the post.
AWS is currently charging $0.0000166667 per GB Second of compute time. Our Lambda runs at 512 MB of memory, with an average execution time of 500 ms. Meaning, we're billed for 0.25 GB Seconds of compute or a total of $0.00000416667 per invocation. In other words, would have to execute the function 10,000 times to incur $0.41 of compute charges.

Getting Started.

This blog post assumes you have the following installed and configured.

  • AWS Powershell Module
    • Interact with AWS Service via PowerShell cmdlets
    • If using PSCore install AWSPowerShell.NetCore. The module is incredibly large, and does not auto import. When working with it, expect to manually import it every session.
  • AWSLambdaPSCore
    • AWS Tooling for creating and deploying Lambda functions
  • dotnetSDK
    • Dependency for building the Lambda's.
  • PowerShell Core 7
    • PowerShell. Duh.
  • Proper IAM Keys & access.
    Follow the AWS Lambda Getting started guide for Powershell for instructions on configuring your environment to work with Lambda.
    For general configuration of the AWS Powershell module see the AWS user guide.

A more accurate name for running PowerShell on Lambda, would be dotnet on lambda. The "Lambda" is composed of a C# project, required DLL's, Powershell modules, and any other code necessary to execute.
Since we can't "install" a module to a Lambda function, you declare what modules are needed at the top of the script using a #Requires statement at the top of your Lambda function. PSLambdaCore will download the modules declared in the #requires statements and add them into the C# environment upon package build.

A Lambda handler is required to be specified which consists of the C# Namespace, Class, and Method of the project that executes the Lambda function. This handler will execute our code and pass in relevant information into our enviroment. See Bootstrap.cs for the exact code.

Data is can be passed into and returned from Lambdas when invoked in the form of a json string. By default two variables will be present as they are passed thru from the lambda handler.

  • $LambdaInput – Data sent to the Lambda function, either manually or automatically through an event or trigger.
  • $LambdaContext – Information about the current execution.

The AWSLambdaPSCore module abstracts this process away from us leaving us with a script folder to work with. Since this is dotnet core running on a linux container, your code will need to be PowerShell 7 Core compatible. This excludes us from windows features of 5.1 like the Active Directory Module, port checking with Test-NetConnection and more.

Writing your first Lambda function.

The point of a lambda function is to automate tasks, that may not be worth managing the underlying infrastructure.
For Example:
Send me a text every morning at 9 AM with a Kanye West quote.

I've written out the example function below using Twilio to send me a text message, and the Kanye.rest API to provide the quotes.

# Twilio API endpoint and POST params
$Params = @{
    uri = "https://api.twilio.com/2010-04-01/Accounts/$env:TWILIO_ACCOUNT_SID/Messages.json"
    method = 'Post'
    Credential = [pscredential]::new($env:TWILIO_ACCOUNT_SID, ($env:TWILIO_AUTH_TOKEN | ConvertTo-SecureString -asPlainText -Force))
    Body = @{ 
        To   = $env:CELLPHONE
        From =  $env:TWILIO_NUMBER
        #Request A random Kanye West Quote
        Body = (Invoke-RestMethod -Uri api.kanye.rest).quote
    }
}
#Make the API Request, outputting all message details to CloudWatch
Invoke-RestMethod @Params | ConvertTo-Json -Compress

API keys are ephemeral and should be referenced as environment variables. When developing, set these environment variables locally so you can ensure functionality on your computer before deploying to AWS.
We need to create a new Lambda project using New-AWSPowerShellLambda. Templates are provided for common use cases. We'll be using the basic template, copying our code into Basic.ps1.

New-AWSPowerShellLambda -Template Basic -Directory BasicAutomation  

Get-ChildItem ./BasicAutomation/
<#
Name
----
Basic.ps1   <- The Entry point to you your Lambda Function
readme.txt
#>

When we upload this function to AWS, we want to specify the values for the environment variables configured locally. Supply a hash table containing the environment variable names and values as a parameter of the Publish-AWSPowerShellLambda function. This will build our PowerShell script into a dotnet project, download any required modules, and upload and configure the function to AWS.

$vars = @{
    CELLPHONE = ''
    TWILIO_NUMBER = ''
    TWILIO_ACCOUNT_SID = ''
    TWILIO_AUTH_TOKEN = ''
}
Publish-AWSPowerShellLambda -Name BasicAutomation -ScriptPath  ./basic.ps1 -EnvironmentVariable $vars

Once published, use Get-LMFunctionList to verify it's been uploaded successfully

Get-LMFunctionList  | select FunctionName,Runtime         
<#
FunctionName    Runtime
------------    -------
BasicAutomation dotnetcore3.1
#>

To manually invoke the function to ensure functionality we can call Invoke-LMFunction to execute our function on AWS.

Invoke-LMFunction -FunctionName BasicAutomation

If invoked from the AWS Powershell module, the output of the function is returned as a memory stream. Since the output is a JSON string, we'll have to transform into a string and parse back into a PowerShell object to retrieve our output.

$fn = Invoke-LMFunction -FunctionName BasicAutomation
$res = [System.Text.Encoding]::UTF8.GetString($fn.Payload.ToArray()) | ConvertFrom-JSON
$res | select Body,Status 
<#
body   : People say it's enough and I got my point across ... the point isn't across until we cross the point
status : queued
#>

Or alternatively get the Log Details of our Lambda function from CloudWatch:

$logname = '/aws/lambda/BasicAutomation' 
#Get all logs from a Lambda function from oldest to newest
$logs = Get-CWLLogStreams $logname | Get-CWLLogEvent -LogGroupName $logname

$logs.events

#Get the most recent events 
$streams = (Get-CWLLogStreams $logname)[-1].logstreamname
$log = Get-CWLLogEvent -LogGroupName $logname -LogStreamName $streams      
$log.events

To call our function we need to have something that 'triggers' the execution. It's not reasonable to have running commands in the terminal trigger our functions.

Enter Amazon Event Bridge formerly CloudWatch Events. With this service we can trigger our lambda functions from either an action being taken elsewhere in AWS I.E an EC2 instance being deployed, Object being uploaded to S3, or on a schedule from a standard cron expression. To define our event, we need a rule and a target. In this context our rule would be 9 AM every day with a target of the lambda function. See AWS docs for writing cron expressions.

#Create the rule via cron expression
Write-CWERule -Name 'KanyeQuoteTimer' -ScheduleExpression 'cron(0 9 ? * 1-5 *)'   

# Associate the rule with our Lambda function. 

$function = Get-LMFunction 'BasicAutomation'
$target = [Amazon.CloudWatchEvents.Model.Target]@{
    arn = $function.configuration.functionarn
    id  = "{0}-{1}" -f $function.configuration.functionname, $function.configuration.RevisionId
}
Write-CWETarget -Rule 'KanyeQuoteTimer' -Target $target

#Get rule details
Get-CWERuleDetail -Name 'KanyeQuoteTimer'
#Get Target details
Get-CWETargetsByRule -Rule 'KanyeQuoteTimer' 

Next post we’ll be getting into the nitty gritty of setting up a continuous delivery pipeline for our function.