Creating, updating, and invoking AWS Lambda Functions in Haskell
For this part of the code, we will first work with a simple Lambda Function created in NodeJS. In the next section, we will replace the NodeJS part with a Lambda Function written completely in Haskell.
Important note about credentials
A slightly confusing aspect about working with Lambda Functions is that there are two IAM roles involved in the process:
The first IAM role is involved in managing the Lambda Function (i.e. creating it, or updating its code).
- This role is related to the user who is making the API calls to create/update the Lambda Function. With respect to this tutorial, it is related to the
getAwsEnv
function given below - The role usually requires the
AWSLambdaFullAccess
“permission policy” (as it is called in AWS parlance). If you’re doing a lot of critical things within AWS Lambda, then it might not be a a better idea to restrict these permissions even further.
AWS_KEY
and AWS_SECRET
environment variable while running this code (or set-up your ~/.aws
file). Make sure the relevant user has AwsLambdaFullAccess
The second IAM role is involed when the Lambda Function is invoked/executed.
- AWS needs to know which AWS services are available to the Lambda Function at the time of execution of the Lambda Function.
- The required permissions completely depend upon what your Lambda Function is doing. If your Lambda Function is not accessing any S3 bucket, this role need not have any S3 related permission. Similarly, if your Lambda Function needs to trigger some deployment via Code Deploy, then it will need the relevant persmission.
- At the minimum, this role will need the
AWSLambdaBasicExecutionRole
“permission policy”
AWSLambdaBasicExecutionRole
permission), please modify fnRole
given below before attempting to run this code.Preliminaries
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NamedFieldPuns #-}
module ManagingLambdaFunctions where
import Network.AWS as AWS
import Network.AWS.Lambda.ListFunctions as AWS
import Network.AWS.Lambda.Types as AWS
import Network.AWS.Lambda.UpdateFunctionConfiguration as AWS
import Control.Lens
import Network.AWS.Lambda.CreateFunction
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString as BS
import Network.AWS.Lambda.Invoke as AWS
import Network.AWS.Lambda.UpdateFunctionCode as AWS
import Control.Monad.IO.Class
import System.Environment (getEnv)
import Data.String.Conv (toS)
import System.Posix.Syslog.TCP as Syslog
import Data.Aeson as Aeson
import Control.Monad (forM_)
import Text.Printf(printf)
import Data.Maybe (fromMaybe, maybe)
import qualified Data.Text as T
import Data.Text (Text)
import Network.AWS.Data.Text (toText)
import Codec.Archive.Zip as Zip
import qualified Data.List as DL
Creating an AWS.Env
All AWS API calls triggered via amazonka
libraries, need to be in the AWST
monad. You can even define your own monad that implements the HasEnv
type-class, if you need to do something really exotic, but we’ll keep things simple and use the AWST
monad in this tutorial.
An action wrapped in the AWST
monad can be run via the runAWS
function, which requires an env :: Env
argument. The env
argument holds a bunch of stuff required to make AWS API calls. For example:
- the authentication credentials
- the AWS region to use
- the HTTP Manager with which to make HTTP calls
- whether to raise HTTP exceptions for non-200 responses, or to return the underlyint response as-is
- and a few other things
The most important thing in Env
are the AWS Auth credentials. There are multiple ways of creating these credentials, but the easiest is to use the Discover
mechanism provided by the library.
Listing Lambda Functions
Listing Lambda Functions is straightforward and can be achieved with the following code snippet:
Let’s write a wrapper on top of listFunctions
, that prints a well-formatted listing of Lambda Functions on the console.
listLambdaFunctions :: IO ()
listLambdaFunctions = do
env <- getAwsEnv
resp <- runResourceT $ runAWS env $ do
send listFunctions
printf "%-20s %-20s %-10s %-40s %-10s\n"
("FUNCTION NAME" :: String)
("HANDLER" :: String)
("VER" :: String)
("REVISION" :: String)
("RUNTIME" :: String)
forM_ (resp ^. lfrsFunctions) $ \fn -> printf "%-20s %-20s %-10s %-40s %-10s\n"
(maybe "no-name" (T.take 20) $ fn ^. fcFunctionName)
(maybe "no-handler" (T.take 20) $ fn ^. fcHandler)
(maybe "no-version" (T.take 10) $ fn ^. fcVersion)
(maybe "no-revision-id" (T.take 40) $ fn ^. fcRevisionId)
(maybe "no-runtime" ((T.take 10) . toText) $ fn ^. fcRuntime)
Creating a Lambda Function
There are two steps to creating a Lambda Function:
- Create a ZIP file containing the Lambda Function’s code, along with all other dependencies (like custom libraries, executables, etc)
- Upload the ZIP file using the
createFunction
API call while providing a bunch of other meta-data required by AWS Lambda.
Creating a ZIP file containing the Lambda Function’s code
You may choose to create a ZIP file using shell commands or other utilities, but in the spirit of reducing the number of moving parts, we will be creating even the ZIP file using the zip Haskell library.
/tmp/upload.zip
filecreateZipFile :: IO FilePath
createZipFile = do
let fname = "/tmp/upload.zip"
fn <- BS.readFile "index.js"
s <- Zip.mkEntrySelector "index.js"
Zip.createArchive fname (Zip.addEntry Zip.Deflate fn s)
pure fname
Uploading the ZIP file
Next, we will create a Lambda Function from a simple JS file.
-- NOTE: You need to change this to the ARN that you see in YOUR IAM console.
-- This will throw an error if you run this without changing it.
fnRole :: Text
fnRole = Prelude.error $
"You forgot to provide your AWS role's ARN. " <>
"It is a string of the format: arn:aws:iam::{some-number}:role/{your-role-name}"
Another important aspect is that, while a Lambda Function refers to a single function, the ZIP file is permitted to have multiple function definitions. Which function to use, is determined by the handler
that you configure while creating the Lambda Function.
So, let’s call our function myNodeFunction
which will correspdond to the myHandler
function defined in the index.js
file.
Finally, here is the code to create the Lambda Function using the node10.x
runtime
fnRuntime :: Runtime
fnRuntime = NODEJS10_x
createLambdaFunction :: IO AWS.FunctionConfiguration
createLambdaFunction = do
env <- getAwsEnv
zipFileName <- createZipFile
zipFileContents <- BS.readFile zipFileName
runResourceT $ runAWS env $ do
send $ createFunction fnName fnRuntime fnRole fnHandler (functionCode & fcZipFile ?~ zipFileContents)
Invoking a Lambda Function “directly”
As I had mentioned earlier, it is not necessary to use API Gateway to invoke your Lambda Function. Use an API Gateway or CloudFormation or suchlike ONLY IF it is really required.
In the spirit of reducing moving parts, here’s how to invoke the Lambda Function “directly”:
invokeLambdaFunction :: IO AWS.InvokeResponse
invokeLambdaFunction = do
env <- getAwsEnv
runResourceT $ runAWS env $ do
-- NOTE: the arguments to a Lambda Function need to be a stringified JSON.
-- Right now, we are not generating this JSON from a Haskell data-structure,
-- but later in this tutorial that's exactly what we will be doing.
send $ invoke "myNodeFunction" "{\"int1\":100, \"int2\": 200}"
Updating a Lambda Function’s code
createFunction
throws an error if you call it with an existing function name. We have to use updateFunctionCode
and updateFunctionConfiguration
to update an existing function. Here’s how to update an existing Lambda Function…
updateLambdaFunction :: IO AWS.FunctionConfiguration
updateLambdaFunction = do
env <- getAwsEnv
zipFileName <- createZipFile
zipFileContents <- BS.readFile zipFileName
runResourceT $ runAWS env $ do
-- fnName is defined above
send $ (updateFunctionCode fnName) & uZipFile ?~ zipFileContents
“Upsert” a Lambda Function’s code
… and here’s a unified function to “upsert” (create OR insert) a Lambda Function:
upsertLambdaFunction :: IO AWS.FunctionConfiguration
upsertLambdaFunction = do
env <- getAwsEnv
zipFileName <- createZipFile
zipFileContents <- BS.readFile zipFileName
runResourceT $ runAWS env $ do
fns <- fmap (view lfrsFunctions) (send listFunctions)
case DL.find (\fn -> fn ^. fcFunctionName == Just fnName ) fns of
Nothing ->
-- fnName, fnRuntime, fnRole, and fnHandler have been defined earlier
send $ createFunction fnName fnRuntime fnRole fnHandler (functionCode & fcZipFile ?~ zipFileContents)
Just _ -> do
send $ (updateFunctionCode fnName) & uZipFile ?~ zipFileContents
send $ (updateFunctionConfiguration fnName)
& ufcRuntime ?~ fnRuntime
& ufcRole ?~ fnRole
& ufcHandler ?~ fnHandler