This workshop consists of multiple levels of increasing difficulty. The basic track of this course uses the AWS Web Console. However, if you prefer working with the AWS CLI or with Terraform you may switch to one of these technologies at any stage in the course. If you stay on the basic track, there is nothing to install on your machine. If you want to switch to the CLI or to Terraform, you need to follow the optional installation instructions below.
At the start of the course, please take care of the following tasks:
You have received an AWS Account ID, an IAM user name and a password from the trainers. Please navigate to https://console.aws.amazon.com/, choose "IAM user", and enter the Account ID and then your credentials. This logs you into the console. From there you should be able to reach the service Lambda.
If you want to also work on some of the optional extra topics of this course, you need to install the AWS CLI on your machine. Please follow the AWS CLI installation instructions and choose the installation method best suited for your operating system.
In order to authenticate your CLI you need to first create an access key by performing the following steps:
- Log in to the AWS Console with your credentials
- Click on your name in the top right of the screen
- Click
My Security Credentialsin the dropdown - Click
Create Access KeyunderAccess keys for CLI, SDK, & API access
Then you need to configure your CLI with access key ID and the secret access key
- Type
aws configurein your terminal - Copy your access key ID and the secret access key from the web console and paste them at the prompts
- Choose
eu-central-1as the default region name - Test the connection by typing
aws sts get-caller-identityin your terminal. You should see some basic information about your user.
Some optional extra topics also require Terraform, which you need to install on your machine. Please follow the Terraform installation instructions and choose the installation method best suited for your operating system.
In this level you will learn how to create a first simple function in AWS Lambda. You will also learn how to pass configuration parameters into a function using environment variables. Furthermore, you will see that AWS has a very strict, deny-by-default permission scheme.
Please work through the following steps:
- Go to the AWS Lambda GUI
- Click on
Create function - Choose
my-function-AWSUSERas the function name, replacingAWSUSERwith you user name - Choose
Node.js 14.xas the runtime - Open the section
Change default execution roleand note that the UI automatically creates an execution role behind the scenes, granting the function certain privileges - Click
Create function - Copy the code from ./level-0/function/index.js and paste it into the code editor field
- Press the
Deploybutton - Set environment variable
NAMEin theConfigurationtab underEnvironment variables - Press the
Testbutton and create a test event calledtest - Press the
Testbutton again to run the test - Observe the test output
Try it with the AWS CLI!
- Set the AWSUSER environment variable.
export AWSUSER=<your AWS username>
- Create an execution role which will allow Lambda functions to access AWS resources:
aws iam create-role --role-name lambda-exec-cli-"$AWSUSER" --assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'
- Grant certain permissions to your newly created role. The managed policy
AWSLambdaBasicExecutionRolehas the permissions needed to write logs to CloudWatch:
aws iam attach-role-policy --role-name lambda-exec-cli-"$AWSUSER" --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- Create a deployment package for your function:
zip -j function.zip level-0/function/index.js
- Create the function:
Find out your Account ID by clicking your username in the top right corner.
export ACCOUNT_ID=<your account ID>
aws lambda create-function --function-name my-function-cli-"$AWSUSER" --zip-file fileb://function.zip --handler index.handler --runtime nodejs14.x --role arn:aws:iam::"$ACCOUNT_ID":role/lambda-exec-cli-"$AWSUSER"
- Set the
NAMEenvironment variable to your user name:
aws lambda update-function-configuration --function-name my-function-cli-"$AWSUSER" --environment "Variables={NAME='$AWSUSER'}"
- Invoke the function:
aws lambda invoke --function-name my-function-cli-"$AWSUSER" out --log-type Tail
- Invoke the function and decode the logs:
aws lambda invoke --function-name my-function-cli-"$AWSUSER" out --log-type Tail --query 'LogResult' --output text | base64 -d
Still bored? Then try it with Terraform!
- Copy the Terraform module to a directory of your choice
export WORKDIR=<your work directory>
mkdir $WORKDIR
cp level-0/advanced/terraform/* $WORKDIR
cp -r level-0/function $WORKDIR
- Navigate to the Terraform module in your work directory
pushd $WORKDIR
- Initialize the Terraform module
terraform init
- Set your AWS user name as a environment variable for Terraform
export TF_VAR_aws_user=<your AWS user name>
- Apply the Terraform module
terraform apply
- Invoke the function
aws lambda invoke --function-name=$(terraform output -raw function_name) response.json
- Navigate back to the workshop repo
popd
To reach level 1, you'll need to learn about the following topics:
- Logging
- Event parameters
- Permissions
- Go to the AWS Lambda UI
- Click on
Functionsin the left navigation - Choose the function
my-function-AWSUSER, which you created in level 0 - Copy the code from ./level-1/function/index.js and paste it over the existing code in the editor field
- Press the
Deploybutton - Press the
Testbutton and create a test event calledbob - Paste the following JSON object to the editor field
{
"name": "Bob"
}
- Press the
Testbutton again to run the test - Navigate to the tab
Monitor - Click
View logs in CloudWatch - Look for a recent log stream and open it
- Check for lines looking like this
2021-09-10T12:26:33.779Z c70ee5e7-4295-4408-a713-9f3ceaaa53e3 INFO Bob invoked me
2021-09-10T12:26:33.779Z c70ee5e7-4295-4408-a713-9f3ceaaa53e3 ERROR Oh noes!
- Navigate to the tab
Configurationand click on the categoryPermissions. - Observe the logging permissions which were assigned to your function automatically.
Try it with the AWS CLI!
- Make sure the AWSUSER and ACCOUNT_ID environment variables are still set.
export AWSUSER=<your AWS username>
export ACCOUNT_ID=<your account ID>
- Create a deployment package for your new function:
zip -j function.zip level-1/function/index.js
- Update the function with the new code:
aws lambda update-function-code --function-name my-function-cli-"$AWSUSER" --zip-file fileb://function.zip
- Invoke the function with a test event:
aws lambda invoke --function-name my-function-cli-"$AWSUSER" --cli-binary-format raw-in-base64-out --payload '{ "name": "Bob" }' out --log-type Tail
- Find the latest log stream for your function in CloudWatch:
aws logs describe-log-streams --log-group-name=/aws/lambda/my-function-cli-"$AWSUSER"
- Inspect the log events of the log stream. You might have to escape some characters in the value passed in
--log-stream-name
aws logs get-log-events --log-group-name=/aws/lambda/my-function-cli-"$AWSUSER" --log-stream-name=<name of latest log stream>
Still bored? Then try it with Terraform!
- Make sure your work directory and user variables are still set
export WORKDIR=<your work directory>
export TF_VAR_aws_user=<your AWS user name>
- Navigate to the Terraform module
cp -r level-1/function $WORKDIR
- Navigate to your work directory
pushd
- Apply the Terraform module again
terraform apply
- Invoke the function with a test event:
aws lambda invoke --function-name my-function-cli-"$AWSUSER" --cli-binary-format raw-in-base64-out --payload '{ "name": "Bob" }' out --log-type Tail
- Find the latest log stream for your function in CloudWatch:
aws logs describe-log-streams --log-group-name=/aws/lambda/my-function-cli-"$AWSUSER"
- Inspect the log events of the log stream:
aws logs get-log-events --log-group-name=/aws/lambda/my-function-cli-"$AWSUSER" --log-stream-name=<name of latest log stream>
- Navigate back to the workshop repo
popd
To reach level 2, you will learn about tracing in your function using AWS XRay. Additionally, we will use a DynamoDB table, which will allow us to trace calls from the function to the table.
We modify the the function to read a joke from a joke table and change the function parameters to receive a jokeID parameter by which it reads from the database.
- Go to the AWS Lambda UI
- Click on
Functionsin the left navigation - Choose the function
my-function-AWSUSER, which you updated in level 1 - Run
npm installin the folder ./level-2/function - Create a zip file from the folder ./level-2/function and upload it to the function
- Press the
Deploybutton - Grant DynamoDB permission to function
- Grant XRay permission to function
- In the
Configurationtab of the lambda function, select theMonitoring and operations tools, click edit and enableActive tracingin theAWS X-Raysection. - On the functions
Testtab create a test event with the following payload{ "jokeID": "1" }and click theTestbutton. You should see the joke loaded from the database in the response. - On the
Monitortab, select theTracesmenu option and inspect the service map as well as the individual traces. Click on one of the traces to get familiar of what info you have available, such as how long the request to query the DynamoDB took.
Try it with the AWS CLI!
- Make sure the AWSUSER and ACCOUNT_ID environment variables are still set.
export AWSUSER=<your AWS username>
export ACCOUNT_ID=<your account ID>
- Attach a policy for XRay access to your role
aws iam attach-role-policy --role-name lambda-exec-cli-"$AWSUSER" --policy-arn arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess
- Create a policy for access to the Jokes table in DynamoDB
aws iam create-policy --policy-name read-jokes-db-table-cli-"$AWSUSER" --policy-document '{ "Version": "2012-10-17", "Statement": [{ "Sid": "ReadWriteTable", "Effect": "Allow", "Action": [ "dynamodb:BatchGetItem", "dynamodb:GetItem", "dynamodb:Query", "dynamodb:Scan" ], "Resource": "arn:aws:dynamodb:eu-central-1:'$ACCOUNT_ID':table/Jokes" }]}'
- Attach the policy to your role
aws iam attach-role-policy --role-name lambda-exec-cli-"$AWSUSER" --policy-arn arn:aws:iam::"$ACCOUNT_ID":policy/read-jokes-db-table-cli-"$AWSUSER"
- Create a deployment package for your new function:
pushd ./level-2/function
npm install
zip -r function.zip ./*
popd
- Update the function with the new code:
aws lambda update-function-code --function-name my-function-cli-"$AWSUSER" --zip-file fileb://level-2/function/function.zip
- Switch on XRay tracing for your function
aws lambda update-function-configuration --function-name my-function-cli-"$AWSUSER" --tracing-config "Mode=Active"
- Invoke the function with a test event:
aws lambda invoke --function-name my-function-cli-"$AWSUSER" out --cli-binary-format raw-in-base64-out --payload '{ "jokeID": "1" }' --log-type Tail --query 'LogResult' --output text | base64 -d
- Inspect the traces that have been created during the last 20 minutes:
aws xray get-service-graph --start-time $(($(date +"%s") -1200)) --end-time $(date +"%s")
Still bored? Then try it with Terraform!
- Make sure your work directory and user variables are still set
export WORKDIR=<your work directory>
export TF_VAR_aws_user=<your AWS user name>
- Copy the new function code and the updated terraform resources to your work directory
cp level-2/advanced/terraform/* $WORKDIR
cp -r level-2/function $WORKDIR
- Install the functions dependencies
pushd $WORKDIR/function
npm install
popd
- Navigate to your Terraform module
pushd $WORKDIR
- Apply the Terraform module again
terraform apply
- Switch on XRay tracing for your function
aws lambda update-function-configuration --function-name my-function-tf-"$AWSUSER" --tracing-config "Mode=Active"
- Invoke the function with a test event:
aws lambda invoke --function-name my-function-tf-"$AWSUSER" out --cli-binary-format raw-in-base64-out --payload '{ "jokeID": "1" }' --log-type Tail --query 'LogResult' --output text | base64 -d
- Inspect the traces that have been created during the last 20 minutes:
aws xray get-service-graph --start-time $(($(date +"%s") -1200)) --end-time $(date +"%s")
- Navigate back to the workshop repo
popd
To reach this level, we'll make sure our function terminates in an orderly fashion within the timeout limit it is configured with.
This means, aborting IO operations or long running calculations when the function time runs out, or at least return to your program flow to handle the return values.
To do this, .
The AWS Lambda frameworks allow us to poll how much time is still left in a function call, which simplifies this.
Before you deploy the improved function to lambda, inspect the provided code. You will notice the following points:
- The usage of
context.getRemainingTimeInMillis()to receive from Lambda how much time is left in our function - An additional grace period, we retain for our function in
TIMEOUT_GRACE_PERIOD_IN_MILLIS - That we trigger a promise, that rejects when the remaining time is below the grace period.
Note!
Some AWS resources support the usage of an AbortController to terminate those actions. When doing IO operations using resources that do not support it or doing long running computations, make sure you set timeouts or implement a timeout check yourself.
- Inspect the provided code as described above
- Click on
Functionsin the left navigation - Choose the function
my-function-AWSUSER, which you created in level 0 - In the
Configurationtab of the function, select theGeneral configurationmenu and set the timeout to 1 second - Note that now only <20ms will be available for the DynamoDB query, because our grace period is set to 980ms
- Create a zip file from the
functionfolder and upload it to the function - Press the
Deploybutton - On the functions
Testtab create a test event with the following payload{ "jokeID": "1" }and click theTestbutton. You should see the function returning successfully, but with a message, that it reached the timeout. - Set the timeout to 2 seconds and try again. This time, the function should be able to read the joke from the database and return it.
Try it with the AWS CLI!
- Make sure the AWSUSER and ACCOUNT_ID environment variables are still set.
export AWSUSER=<your AWS username>
export ACCOUNT_ID=<your account ID>
- Create a deployment package for your new function:
pushd ./level-3/function
npm install
zip -r function.zip ./*
popd
- Update the function with the new code:
aws lambda update-function-code --function-name my-function-cli-"$AWSUSER" --zip-file fileb://level-3/function/function.zip
- Update the function's timeout setting to 1 second:
aws lambda update-function-configuration --function-name my-function-cli-"$AWSUSER" --timeout 1
- Invoke the function with a test event:
aws lambda invoke --function-name my-function-cli-"$AWSUSER" out --cli-binary-format raw-in-base64-out --payload '{ "jokeID": "1" }' --log-type Tail --query 'LogResult' --output text | base64 -d
-
Note that the function succeeds, but in the output tells you, that it ran into a timeout.
-
Update the function's timeout setting to 2 seconds:
aws lambda update-function-configuration --function-name my-function-cli-"$AWSUSER" --timeout 2
- Invoke the function again with a test event:
aws lambda invoke --function-name my-function-cli-"$AWSUSER" out --cli-binary-format raw-in-base64-out --payload '{ "jokeID": "1" }' --log-type Tail --query 'LogResult' --output text | base64 -d
- Note that the function succeeds and returns the joke.
Still bored? Then try it with Terraform!
- Make sure your work directory and user variables are still set
export WORKDIR=<your work directory>
export TF_VAR_aws_user=<your AWS user name>
- Copy the terraform module and the function code from level 3
cp -r level-3/advanced/terraform $WORKDIR
cp -r level-3/function $WORKDIR
- Apply the Terraform module again
terraform apply
- Invoke the function with a test event:
aws lambda invoke --function-name my-function-terraform-"$TF_VAR_aws_user" out --payload '{ "jokeID": "1" }' --log-type Tail --query 'LogResult' --output text | base64 -d
Note that the function returns but the response contains an error message because it ran into a timeout.
-
Change the timeout setting of the function in the terraform module in your working directory
-
Apply the Terraform module again
terraform apply
- Invoke the function with another test event:
aws lambda invoke --function-name my-function-terraform-"$TF_VAR_aws_user" out --payload '{ "jokeID": "1" }' --log-type Tail --query 'LogResult' --output text | base64 -d
Note that the function returns successfully and the response contains the joke loaded from the database.
To reach level 4, you will need to reduce the cold start time of your function. Warmers are usually not recommended but you can try moving initialization code outside of your handler.
- Go to the AWS Lambda UI
- Click on
Functionsin the left navigation - Choose the function
my-function-AWSUSER, which you updated in level 3 - Run
npm installin the folder ./level-4/function - Create a zip file from the folder ./level-4/function and upload it to the function
- Press the
Deploybutton - Press the
Testbutton and create a test event calledjoke - Paste the following JSON object to the editor field
{
"jokeID": "1"
}
- Press the
Testbutton again to run the test - Observe the test output
Try it with the AWS CLI!
- Make sure the AWSUSER and ACCOUNT_ID environment variables are still set.
export AWSUSER=<your AWS username>
export ACCOUNT_ID=<your account ID>
- Create a deployment package for your new function:
pushd ./level-4/function
npm install
zip -r function.zip ./*
popd
- Update the function with the new code:
aws lambda update-function-code --function-name my-function-cli-"$AWSUSER" --zip-file fileb://level-4/function/function.zip
- Invoke the function with a test event:
aws lambda invoke --function-name my-function-cli-"$AWSUSER" --cli-binary-format raw-in-base64-out --payload '{ "jokeID": "1" }' out --log-type Tail
Still bored? Then try it with Terraform!
- Make sure your work directory and user variables are still set
export WORKDIR=<your work directory>
export TF_VAR_aws_user=<your AWS user name>
- Copy the updated function code to your working directory
cp -r level-4/function $WORKDIR
- Install the functions dependencies
pushd $WORKDIR/function
npm install
popd
- Navigate to your Terraform module
pushd $WORKDIR
- Apply the Terraform module again
terraform apply
- Invoke the function with a test event:
aws lambda invoke --function-name my-function-cli-"$AWSUSER" --cli-binary-format raw-in-base64-out --payload '{ "jokeID": "1" }' out --log-type Tail
- Navigate back to the workshop repo
popd