This workshop consists of multiple levels of increasing difficulty. The basic track of this course uses the AWS Web Console. However, as soon as you're done with that track, you can alternatively use the AWS CLI version 2 or Terraform. You can switch to one of these technologies at any stage in the course. All serverless functions in this course are written in Node.js. If you stay on the basic track, you will only need to have the Node.js runtime and npm
installed on your machine. If you want to switch to the CLI or to Terraform, you need to follow the optional installation instructions below.
The commands are geared towards Unix systems. If you're using Windows, you might need to adapt some of them. A more convenient solution is to use WSL or Git BASH.
At the start of the course, take care of the following tasks:
Ensure that you have Node.js runtime version 18.x or newer installed on your machine. If you need to install it, follow the instructions on the Node.js site. Furthermore, you will also need the npm
CLI. After the Node.js installation, type npm
in a shell, to check that it is available.
In case you cannot or do not want to install Node.js and npm, you can also run the commands via a container. For example, using Docker:
docker run --rm -t -v "${PWD}:/src" -w '/src' docker.io/node:alpine npm install
Next, you will need this repo on your own machine. Run git clone https://github.com/bespinian/serverless-workshop.git
to clone this repo with all its steps.
Finally, you will of course also require access to AWS. You have received an AWS Account ID, an IAM user name and a password from the trainers. 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
.
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.
Work through the following steps:
- Go to the AWS Lambda GUI
- Choose Europe (Frankfurt) eu-central-1 as the region in the top-right corner. All resources you create should be created in that region.
- Click on
Create function
- Choose
my-function-AWSUSER
as the function name, replacingAWSUSER
with your user name - Choose
Node.js 18.x
as the runtime - Open the section
Change default execution role
and 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.mjs and paste it into the code editor field
- In the
Configuration
tab underEnvironment variables
, set a variable calledNAME
to the name of a person you like - In the
Test
tab, press theTest
button and create a test event calledtest
- Press the
Test
button again to run the test - Observe the test output
Lambda functions support various Triggers. Some of the most commonly used ones are:
- HTTP trigger using the API Gateway
- Message queue triggers from Amazon MQ or SQS
- S3 events, such as object creation or deletion events in a bucket
- Apache Kafka events
Work through the following steps to expose your function on the Internet via the API Gateway.
- In the Functional overview at the top, click
Add trigger
- Select
API Gateway
- For API, select
Create a new API
- Select
HTTP API
as the API Type - Select
Open
, in Security - Click
Add
- Click on the created API and visit the
Integrations
tab - Click the
ANY
integration and hit theManage integration
button - Click
Edit
and expand theAdvanced settings
. In there, choose the "Payload format version"2.0
and hit theSave
button - You can now close this tab to go back to your function. Changing the integration format was necessary because our function returns the newer and simpler version
2.0
format. - You should now see your newly created Trigger and the URL to access it. Click the link to see the response in your browser.
Try it with the AWS CLI!
For the optional CLI steps, you need to install the AWS CLI on your machine. Follow the AWS CLI installation instructions and choose the installation method best suited for your operating system.
To authenticate your CLI, you first need to 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 Credentials
in the dropdown -
Click
Create Access Key
underAccess keys for CLI, SDK, & API access
Then, you need to configure your CLI with access key ID and the secret access key.
-
Type
aws configure
in 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-1
as the default region name -
Test the connection by typing
aws sts get-caller-identity
in your terminal. You should see some basic information about your user.
-
Set the
AWSUSER
environment variable.export AWSUSER=<your AWS user name>
-
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
AWSLambdaBasicExecutionRole
has 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.mjs
-
Find out your Account ID by clicking your user name in the top-right corner. Then set it as a variable in your shell.
export ACCOUNT_ID=<your account ID>
-
Create the function:
aws lambda create-function --function-name "my-function-cli-${AWSUSER}" --zip-file fileb://function.zip --handler index.handler --runtime nodejs18.x --role "arn:aws:iam::${ACCOUNT_ID}:role/lambda-exec-cli-${AWSUSER}"
-
Set the
NAME
environment 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}" output.json --log-type Tail
-
Invoke the function and decode the logs:
aws lambda invoke --function-name "my-function-cli-${AWSUSER}" output.json --log-type Tail --query 'LogResult' --output text | base64 -d
-
Create an API on the API gateway
aws apigatewayv2 create-api --name "my-api-gw-cli-${AWSUSER}" --protocol-type HTTP
-
Set the variable
API_ID
to the ID that was returned by the command above:export API_ID=<the API ID>
-
Create an integration on the API gateway pointing to your Lambda function:
aws apigatewayv2 create-integration --api-id "$API_ID" --integration-type AWS_PROXY --integration-uri "arn:aws:lambda:eu-central-1:${ACCOUNT_ID}:function:my-function-cli-${AWSUSER}" --payload-format-version 2.0
-
Set the INTEGRATION_ID variable to the value of
IntegrationId
from the response:export INTEGRATION_ID=<the integration id>
-
Create a route pointing to your integration:
aws apigatewayv2 create-route --api-id "$API_ID" --route-key "ANY /my-function" --target "integrations/${INTEGRATION_ID}" --authorization-type NONE --no-api-key-required
-
Create a stage that deploys the configuration:
aws apigatewayv2 create-stage --api-id "$API_ID" --auto-deploy --stage-name default
-
Allow the API gateway to access the lambda function:
aws lambda add-permission --function-name "my-function-cli-${AWSUSER}" --statement-id apigateway-get --action lambda:InvokeFunction --principal apigateway.amazonaws.com --source-arn "arn:aws:execute-api:eu-central-1:${ACCOUNT_ID}:${API_ID}/*/*/my-function"
Still bored? Then try it with Terraform!
For the optional Terraform steps, you need to install Terraform on your machine. Follow the Terraform installation instructions and choose the installation method best suited for your operating system.
To authenticate Terraform, you first need to 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 Credentials
in the dropdown -
Click
Create Access Key
underAccess keys for CLI, SDK, & API access
Then, you need to configure your machine with access key ID and the secret access key.
-
Type
aws configure
in 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-1
as the default region name -
Test the connection by typing
aws sts get-caller-identity
in your terminal. You should see some basic information about your user.
-
Copy the Terraform module and the function code to a separate working directory
mkdir my-tf-module cp level-0/advanced/terraform/* my-tf-module cp -r level-0/function my-tf-module
-
Navigate to the Terraform module in your work directory
pushd my-tf-module
-
Initialize the Terraform module
terraform init
-
Set your AWS user name as an environment variable for Terraform
export TF_VAR_aws_user=<your AWS user name>
-
Apply the Terraform module
terraform apply
-
Invoke the function via the AWS CLI
aws lambda invoke --function-name "$(terraform output -raw function_name)" output.json
-
Invoke the function via HTTP. Access the
invoke_url
that's returned byterraform apply
either through your browser or through curl:curl <invoke_url>
-
Navigate back to the workshop repo
popd
To reach level 1, you'll need to learn about the following topics:
- Logging
- Event parameters
- Permissions
-
In the Lambda GUI of your function, copy the code from level-1/function/index.mjs and paste it over the existing code in the editor field
-
In the
Test
tab, press theTest
button and create a test event calledbob
-
Paste the following JSON object to the editor field
{ "name": "Bob" }
-
Press the
Test
button 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 back to your function in the Lambda console and switch to the tab
Configuration
and 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
andACCOUNT_ID
environment variables are still set.export AWSUSER=<your AWS user name> export ACCOUNT_ID=<your account ID>
-
Create a deployment package for your new function:
zip -j function.zip level-1/function/index.mjs
-
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" }' output.json --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
e.g.$LATEST
should be\$LATEST
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 user variable is still set
export TF_VAR_aws_user=<your AWS user name>
-
Navigate to the Terraform module
cp -r level-1/function my-tf-module
-
Navigate to your work directory
pushd my-tf-module
-
Apply the Terraform module again
terraform apply
-
Invoke the function with a test event:
aws lambda invoke --function-name "my-function-tf-${TF_VAR_aws_user}" --cli-binary-format raw-in-base64-out --payload '{ "name": "Bob" }' output.json --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-tf-${TF_VAR_aws_user}"
-
Inspect the log events of the log stream:
aws logs get-log-events --log-group-name "/aws/lambda/my-function-tf-${TF_VAR_aws_user}" --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 X-Ray. Additionally, we will use a DynamoDB table, and trace calls from the function to the table.
We modify 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.
-
From your terminal,
cd
into the level-2/function directory of this repo -
Run
npm install
-
Create a zip file from the folder level-2/function
zip -r function.zip .
-
Upload the zip file to the function by using the
Upload from
button -
In the
Configuration
tab, clickPermissions
and then click the link to the execution role -
Click the
Add permissions
button, selectAttach policies
and add the "AmazonDynamoDBReadOnlyAccess" and the "AWSXRayDaemonWriteAccess" permissions to grant your function access to DynamoDB and the X-Ray service -
In the
Configuration
tab of the lambda function, select theMonitoring and operations tools
, click edit and enableActive tracing
in theAWS X-Ray
section -
On the function's
Test
tab, create a test event with the following payload{ "jokeID": "1" }
and click theTest
button. You should see the joke loaded from the database in the response -
On the
Monitor
tab, select theTraces
menu 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. -
To call your function via HTTP, you must now provide a payload (you can find the gateway API URL in the
Configuration
tab underTriggers
):curl -v -X POST <api-gateway-url> --data '{"jokeID":"1"}'
Try it with the AWS CLI!
-
Make sure the
AWSUSER
andACCOUNT_ID
environment variables are still set.export AWSUSER=<your AWS user name> export ACCOUNT_ID=<your account ID>
-
Attach a policy for X-Ray 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 X-Ray 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}" output.json --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 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/* my-tf-module cp -r level-2/function my-tf-module
-
Install the functions dependencies
pushd my-tf-module/function npm install popd
-
Navigate to your Terraform module
pushd my-tf-module
-
Apply the Terraform module again
terraform apply
-
Switch on X-Ray tracing for your function
aws lambda update-function-configuration --function-name "my-function-tf-${TF_VAR_aws_user}" --tracing-config "Mode=Active"
-
Invoke the function with a test event:
aws lambda invoke --function-name "my-function-tf-${TF_VAR_aws_user}" output.json --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. 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 in level-3/function
.
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
Functions
in the left navigation - Choose the function
my-function-AWSUSER
, which you created in level 0 - In the
Configuration
tab of the function, select theGeneral configuration
menu 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
- Run
npm install
in thefunction
folder - Create a zip file from the
function
folder and upload it to the function - On the functions
Test
tab, create a test event with the following payload{ "jokeID": "1" }
and click theTest
button. 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
andACCOUNT_ID
environment variables are still set.export AWSUSER=<your AWS user name> 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}" output.json --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}" output.json --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 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 my-tf-module cp -r level-3/function my-tf-module
-
Install the functions dependencies
pushd my-tf-module/function npm install popd
-
Navigate to your Terraform module
pushd my-tf-module
-
Apply the Terraform module again
terraform apply
-
Invoke the function with a test event:
aws lambda invoke --function-name "my-function-tf-${TF_VAR_aws_user}" output.json --cli-binary-format raw-in-base64-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-tf-${TF_VAR_aws_user}" output.json --cli-binary-format raw-in-base64-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.
-
Navigate back to the workshop repo
popd
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 your handler.
-
Go to the AWS Lambda UI
-
Click on
Functions
in the left navigation -
Choose the function
my-function-AWSUSER
, which you updated in level 3 -
Run
npm install
in the folder level-4/function -
Create a zip file from the folder level-4/function and upload it to the function
-
Press the
Test
button and create a test event calledjoke
-
Paste the following JSON object to the editor field
{ "jokeID": "1" }
-
Press the
Test
button again to run the test -
Observe the test output
Try it with the AWS CLI!
-
Make sure the
AWSUSER
andACCOUNT_ID
environment variables are still set.export AWSUSER=<your AWS user name> 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" }' output.json --log-type Tail
Still bored? Then try it with Terraform!
-
Make sure your user variable is still set
export TF_VAR_aws_user=<your AWS user name>
-
Copy the updated function code to your working directory
cp -r level-4/function my-tf-module
-
Install the functions dependencies
pushd my-tf-module/function npm install popd
-
Navigate to your Terraform module
pushd my-tf-module
-
Apply the Terraform module again
terraform apply
-
Invoke the function with a test event:
aws lambda invoke --function-name "my-function-tf-${TF_VAR_aws_user}" --cli-binary-format raw-in-base64-out --payload '{ "jokeID": "1" }' output.json --log-type Tail
-
Navigate back to the workshop repo
popd
To reach level 5, you'll need to learn how to decouple multiple functions asynchronously via a message queue.
-
Navigate to the level 5 code and install the dependencies:
cd level-5/function npm install
-
Package your function
zip -r function.zip ./*
-
In the Lambda GUI, create a new function called
sender-AWSUSER
(replace "AWSUSER" with your user name) and leave all the defaults -
Create another function called
recipient-AWSUSER
(replace "AWSUSER" with your user name) and leave all the defaults -
Upload
function.zip
to both functions and inspect the file. It defines and exports two different handlers. The first one sends an event to an SQS queue and the second one receives it. -
Scroll down to
Runtime settings
and change the "Handler" toindex.senderHandler
for the sender function and toindex.recipientHandler
for the recipient function. For Node.js, the "index" part refers to the file name and the "handler" part to the name of the export. So, we can have multiple functions in the same source code. -
Head over to the SQS GUI and create a new queue called
messages-AWSUSER
replacing "AWSUSER" with your user name and leave all the defaults. Then copy the URL of your newly created queue to the clipboard. The URL can be found in the "Details" of the queue. -
Set the
SQS_QUEUE_URL
environment variable to the name of the queue you have just created for the sender function -
Give the sender function the permission to send messages to the queue by clicking its role name in the "Configuration" tab under "Permissions". Then attach the policy called
AmazonSQSFullAccess
. -
Give the recipient function the permission to read messages from the queue by clicking its role name in the "Configuration" tab under "Permissions". Then attach the policy called
AWSLambdaSQSQueueExecutionRole
. -
Go to the recipient's GUI and add a trigger by clicking the "Add trigger" button. Then choose "SQS" and choose your newly created queue.
-
Trigger your sender function with a test event
-
Head over to CloudWatch to examine the logs of the sender function. It has dispatched a message to the message queue.
-
Examine the logs of the recipient function. It has been asynchronously triggered by the sender function via the message queue.
Try it with the AWS CLI!
-
Make sure the
AWSUSER
andACCOUNT_ID
environment variables are still set.export AWSUSER=<your AWS user name>
-
Create a deployment package for your new functions:
cd level-5/function npm install zip -r function.zip ./*
-
Create two new roles:
aws iam create-role --role-name "sender-exec-cli-${AWSUSER}" --assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" }] }' aws iam create-role --role-name "recipient-exec-cli-${AWSUSER}" --assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" }] }'
-
Create two new functions:
export SENDER_ROLE_ARN=$(aws iam get-role --role-name "sender-exec-cli-${AWSUSER}" --query Role.Arn --output text) aws lambda create-function --function-name "sender-cli-${AWSUSER}" --zip-file fileb://function.zip --handler index.senderHandler --runtime nodejs18.x --role "$SENDER_ROLE_ARN" export RECIPIENT_ROLE_ARN=$(aws iam get-role --role-name "recipient-exec-cli-${AWSUSER}" --query Role.Arn --output text) aws lambda create-function --function-name "recipient-cli-${AWSUSER}" --zip-file fileb://function.zip --handler index.recipientHandler --runtime nodejs18.x --role "$RECIPIENT_ROLE_ARN"
-
Create a new message queue
aws sqs create-queue --queue-name "messages-cli-${AWSUSER}"
-
Set the
SQS_QUEUE_URL
environment variable of the sender function to the queue's URL:export SQS_QUEUE_URL=$(aws sqs get-queue-url --queue-name "messages-cli-${AWSUSER}" --query QueueUrl --output text) aws lambda update-function-configuration --function-name "sender-cli-${AWSUSER}" --environment "Variables={SQS_QUEUE_URL='$SQS_QUEUE_URL'}"
-
Give your sender function the permission to log and to post messages to the queue:
aws iam attach-role-policy --role-name "sender-exec-cli-${AWSUSER}" --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole aws iam attach-role-policy --role-name "sender-exec-cli-${AWSUSER}" --policy-arn arn:aws:iam::aws:policy/AmazonSQSFullAccess
-
Give your recipient function the permission to log and read messages from the queue:
aws iam attach-role-policy --role-name "recipient-exec-cli-${AWSUSER}" --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole aws iam attach-role-policy --role-name "recipient-exec-cli-${AWSUSER}" --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole
-
Set the SQS queue as a trigger for the recipient function:
export SQS_QUEUE_ARN=$(aws sqs get-queue-attributes --queue-url "$SQS_QUEUE_URL" --attribute-names QueueArn --query Attributes.QueueArn --output text) aws lambda create-event-source-mapping --function-name "recipient-cli-${AWSUSER}" --event-source-arn "$SQS_QUEUE_ARN"
-
Invoke the sender function:
aws lambda invoke --function-name "sender-cli-${AWSUSER}" output.json --log-type Tail
-
Check out the logs of the sender function to see that the message has been sent:
aws logs tail "/aws/lambda/sender-cli-${AWSUSER}"
-
Check out the logs of the recipient function to see that it has been triggered, and the message has been received:
aws logs tail "/aws/lambda/recipient-cli-${AWSUSER}"
Still bored? Then try it with Terraform!
-
Make sure your user variable is still set
export TF_VAR_aws_user=<your AWS user name>
-
Copy the updated function code to your working directory
cp -r level-5/advanced/terraform/* my-tf-module cp -r level-5/function my-tf-module
-
Install the functions dependencies
pushd my-tf-module/function npm install popd
-
Navigate to your Terraform module
pushd my-tf-module
-
Apply the Terraform module again
terraform apply
-
Invoke the sender function with a test event:
aws lambda invoke --function-name "sender-tf-${TF_VAR_aws_user}" --cli-binary-format raw-in-base64-out output.json --log-type Tail
-
Check out the logs of the sender function to see that the message has been sent:
aws logs tail "/aws/lambda/sender-tf-${TF_VAR_aws_user}"
-
Check out the logs of the recipient function to see that it has been triggered, and the message has been received:
aws logs tail "/aws/lambda/recipient-tf-${TF_VAR_aws_user}"
-
Navigate back to the workshop repo
popd
Infrastructure as code allows us to manage the deployments of our functions and other cloud resources in a much more repeatable and testable way, bringing us to the next level.
To deploy the function from level-4, and it's required resources, follow the steps below. You can also use this to deploy it to your own AWS account with very few commands.
Note!
If you have done some extra work and already deployed the previous levels with Terraform, you may skip this level, as it redeploys the code from level 4.
For this step, need Terraform installed on your machine. Follow the Terraform installation instructions and choose the installation method best suited for your operating system.
Furthermore, you need to have valid credentials for your AWS user set up in your terminal. You can test that by running the aws sts get-caller-identity
command. You should see some basic information about your user. If that doesn't work, go back to the first "Already done? Try some bonus steps!" section in Level 0. There, in "Try it with the AWS CLI!", follow the "Installation" and "Authentication" instructions.
-
Copy the Terraform module and the function code to a separate working directory
mkdir my-tf-module cp level-6/advanced/terraform/* my-tf-module cp -r level-6/function my-tf-module
-
Navigate to the Terraform module in your work directory
pushd my-tf-module
-
Initialize the Terraform module
terraform -chdir=my-tf-module init
-
Set your AWS user name as an environment variable for Terraform
export TF_VAR_aws_user=<your AWS user name>
-
Install the functions dependencies
cd my-tf-module/function npm install cd ../..
-
Navigate to your Terraform module
pushd my-tf-module
-
Apply the Terraform module
terraform -chdir=my-tf-module apply
-
Invoke the function with a test event:
aws lambda invoke --function-name "my-function-tf-${TF_VAR_aws_user}" --cli-binary-format raw-in-base64-out --payload '{ "jokeID": "1" }' output.json --log-type Tail
To reach level 7 you need to know how to
- Unit test your functions
- Run your functions locally to make debugging easier
-
Navigate to the unit test example and install the dependencies:
pushd level-7/unit-tests npm install
-
Inspect the example function, the service mocks and the tests:
cat index.mjs cat index.test.js
-
Run the tests for the example function:
npm test popd
-
Navigate to the local execution example and install the dependencies
pushd level-7/api-to-serverless npm install
-
Inspect the express API and the function code
cat api.mjs cat index.mjs
-
Run the example locally as an express API:
npm start
-
Make a request to the API in a new terminal:
curl 'http://localhost:3000?name=alice'
-
Package your function
zip -r function.zip ./*
-
Deploy your function using the UI, the CLI or Terraform and invoke it with a test event of the following form:
{ "name": "Bob" }
When you understand that functions can be assigned just the set of privileges that they need, you have reached level 8. To do so, you will observe an example of a function which is trying to write to the "jokes" table, but does not have permission to do so.
-
Go to the AWS Lambda UI
-
Click on
Functions
in the left navigation -
Choose the function
my-function-AWSUSER
, which you updated in level 4 -
Run
npm install
in the folder level-8/function -
Create a zip file from the folder level-8/function and upload it to the function
-
Press the
Test
button and create a test event calledjoke
-
Paste the following JSON object to the editor field
{ "jokeID": "1" }
-
Press the
Test
button again to run the test -
Observe the failure in the test output. This is because your function has read only access to the "jokes" table, but is secretly trying to write to it.
Try it with the AWS CLI!
-
Make sure the
AWSUSER
andACCOUNT_ID
environment variables are still set.export AWSUSER=<your AWS user name> export ACCOUNT_ID=<your account ID>
-
Create a deployment package for your new function:
pushd level-8/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-8/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" }' output.json --log-type Tail
Still bored? Then try it with Terraform!
-
Make sure your user variable is still set
export TF_VAR_aws_user=<your AWS user name>
-
Copy the updated function code to your working directory
cp -r level-8/function my-tf-module
-
Install the functions dependencies
pushd my-tf-module/function npm install popd
-
Navigate to your Terraform module
pushd my-tf-module
-
Apply the Terraform module again
terraform apply
To reach level 9, we have to correctly deploy our app using a canary deployment. These can help greatly to reduce errors in production. In this section, you will do a rolling deployment that gradually increases the load to the new version and rolls back on errors. To do so, run through the following steps:
- Go to the AWS Lambda UI
- Click on
Functions
in the left navigation - Choose the function
my-function-AWSUSER
, which you updated in level 4 - Run
npm install
in the folder level-9/function - Create a zip file from the folder level-9/function and upload it to the function
- Go to the "Versions" tab and click "Publish a new version", then click "Publish"
- Click "Create alias" and call it "production"
- Change something in the function code (e.g. add a
console.log("new version");
statement) - Zip the folder again and upload it
- Publish another version with the new source code
Now, through a canary deployment, we want to migrate the production
alias from version 1 to version 2.
-
To allow CodeDeploy to access our function, we need to create the respective role. Head over to the IAM UI and create a new role
-
Choose "CodeDeploy" as the service and then "CodeDeploy for Lambda" as the use case
-
Call the role
codedeploy-to-my-function-AWSUSER
(replacing "AWSUSER" with your user name) -
Visit the CodeDeploy UI
-
Create a new application and give it the same name as your function. Choose "AWS Lambda" as the "Compute platform"
-
Create a new deployment group and give it the same name as your function
-
For the "Service role", choose the one we have just created
-
The deployment configuration, should be set to "CodeDeployDefault.LambdaLinear10PercentEvery1Minute" to trigger a canary deployment which will roll out to another 10% of users every minute
-
Click "Create deployment" and choose "Use AppSpec editor" with "YAML"
-
Enter the following code into the text field (replacing
AWSUSER
with your user name):version: 0.0 Resources: - my-function: Type: AWS::Lambda::Function Properties: Name: "my-function-AWSUSER" Alias: "production" CurrentVersion: "1" TargetVersion: "2"
-
Click "Create deployment"
-
You can now observe in real time how your
production
alias gets switched from version 1 to version 2 gradually using a canary deployment
Try it with Terraform!
-
Make sure your user variable is still set
export TF_VAR_aws_user=<your AWS user name>
-
Navigate to the Terraform module
cp level-9/advanced/terraform/* my-tf-module cp -r level-9/function my-tf-module
-
Install the functions dependencies
pushd my-tf-module/function npm install popd
-
Navigate to your Terraform module
pushd my-tf-module
-
Apply the Terraform module again
terraform apply
-
Change something about the function code and apply again to publish a new version (notice the
publish: true
flag infunction.tf
) -
Visit the CodeDeploy UI
-
Choose your application
-
Click "Create deployment" and choose "Use AppSpec editor" with "YAML"
-
Enter the following code into the text field (replacing
AWSUSER
with your user name):version: 0.0 Resources: - my-function: Type: AWS::Lambda::Function Properties: Name: "my-function-tf-AWSUSER" Alias: "production" CurrentVersion: "1" TargetVersion: "2"
-
Click "Create deployment"
-
You can now observe in real time how your
production
alias gets switched from version 1 to version 2 gradually using a canary deployment