Example Application

Now that we have covered the basics, here is a step by step example that goes through how to create a simple Lazysusan application. Since all of the topics have already been covered, this will mostly be a lot of terminal commands and files to copy and paste.

Repo setup

Let’s add some of the boilerplate and get ready to write our application:

$ mkdir recipe_helper
$ cd recipe_helper

Docker setup

As noted in the Introduction, using Docker isn’t required but it can make your life easier and get you up and running faster. Our Docker image is set up with:

Assuming you have Docker installed on your system, pull the joinspartan/serverless image. It’s Suggested you use a specific tag which corresponds to a specific version of Serverless. Here, we’ll be using 1.4:

$ docker pull joinspartan/serverless:1.4

Makefile

There are a few things we’ll need to do multiple times such as deploying our application to AWS Lambda, updating supporting libraries, etc. The Makefile will make most common tasks much easier and also set you up to deploy your Lambda functions to different “environments” with different variables.

This Makefile will be the first file in the recipe_helper directory:

NAME = "joinspartan/serverless:1.4"

ENVDIR=envs
LIBS_DIR=src/lib


.PHONY:     libs shell env-dirs tests deploy function check-env

run = docker run --rm -it \
        -v `pwd`:/code \
        --env ENV=$(ENV) \
        --env-file envs/$2 \
        --name=recipe-helper-serverless-$(ENV) $(NAME) $1


libs :
    @test -d $(LIBS_DIR) || mkdir -p $(LIBS_DIR)
    rm -rf $(LIBS_DIR)/*
    pip install -t $(LIBS_DIR) PyYAML
    @test -f $(LIBS_DIR)/_yaml.so && rm $(LIBS_DIR)/_yaml.so
    pip install -t $(LIBS_DIR) python-dateutil
    pip install -t $(LIBS_DIR) --no-deps -U git+https://github.com/spartansystems/lazysusan.git

shell : check-env env-dirs
    $(call run,bash,$(ENV))

env-dirs :
    @test -d $(ENVDIR)

tests : check-env
    $(call run,py.test tests,$(ENV))

# NOTE:
#
#   Deployments assume you are already running inside the docker container
#
#
deploy : check-env
    cd src && sls deploy -s $(ENV)

function : check-env
    cd src && sls deploy -s $(ENV) function -f age

# Note the ifndef must be unindented
check-env:
ifndef ENV
    $(error ENV is undefined)
endif

Env setup

The next step is getting the minimum set of environment variables set up. The Makefile above is set up to work with different “environments”...examples of this may be “dev”, “qa” and “production”. Using “environments” is a method of developing in different systems/stacks so that you can manage and release code without harming or overwriting a stable stack such as “production”.

Let’s start by simply creating a single “environment” called dev:

$ mkdir envs
$ touch envs/dev

At a very minimum you’ll need the following AWS environment variables in the environment file. Here, I’ll put the following into the envs/dev file. Of course, you’ll need to put your own AWS credentials in this file:

AWS_REGION=us-east-1
AWS_SECRET_ACCESS_KEY=abc123saUMOVIENOWPLEASE3asasdf
AWS_ACCESS_KEY_ID=1BE3PQTZO872U6

Note

As of this writing AWS Lambda functions used with Alexa must be deployed to the us-east-1 Northern Virginia region

Bootstrap application

Now, we can start a Docker container and start bootstrapping our application:

$ ENV=dev make shell
docker run --rm -it -v `pwd`:/code --env ENV=dev --env-file envs/dev --name=recipe-helper-serverless-dev "joinspartan/serverless:1.4" bash
root@9fcf3335e5aa:/code#
root@9fcf3335e5aa:/code# sls create --template aws-python -p src -n recipe_helper

You can see both in the container and on your local host system that src directory was created with two files:

$ ls -l src/
-rw-r--r--   1 user  staff   490 Jan  4 11:54 handler.py
-rw-r--r--   1 user  staff  2308 Jan  4 11:54 serverless.yml

We’ll edit these files soon. Next, we’ll need to setup our supporting libraries which are dependencies for your application code. These are listed out in the Makefile libs directive.

In the container or on your local system run make libs

root@9fcf3335e5aa:/code# make libs
rm -rf src/lib/*
pip install -t src/lib PyYAML
....
Successfully installed lazysusan-0.6

There is now a src/lib folder which contains the supporting libraries code.

Application code

Open up handler.py and replace it with the following. We’ll walk through what each line does but in short this is all of the code you’ll need for a basic Lazysusan app.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import os
import sys

CWD = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, os.path.join(CWD, "lib"))

from lazysusan import LazySusanApp


def main(event, lambda_context):
    state_path = os.path.join(CWD, "states.yml")
    os.environ["LAZYSUSAN_SESSION_STORAGE_BACKEND"] = "cookie"
    app = LazySusanApp(state_path, session_key="FRIED_EGGS_STATE")
    response = app.handle(event)
    return response

Because we’re deploying our application code to AWS Lambda there is some system path munging needed in order for our application to find the needed libraries. Lines 4-5 simple add the lib/ directory to Lambda system path. You may recall that the lib/ directory is where we installed our supporting packages such as lazysusan.

Note

Any third party libraries which you install in lib/ must be imported after the path munging. This is why the lazysusan import occurs after the call to sys.path.insert

AWS Lambda will call a single function when invoked. We’ll configure this in the serverless.yml file in the next section. It should be obvious that there is only one function which is our entry point into the application.

One line 11 we tell Lazysusan where our main states.yml file is. This file is criticial and defines the flow of our Alexa application in terms of the Voice User Interface.

Line 12 sets an environment variable for session storage. By default sessions will use DynamoDB as a storage backend...this requires additional setup which we don’t need in this example application. By using cookie the sessions are stored in the request/response cycle of the Alexa application. This allows us a very short-term session storage...as long as the application is executing and the user is interacting with the application the session is alive. As soon as an application quits the session is erased.

Note

Line 12 could be removed and set using the environment variable file. However, this would require some changes to the serverless deployment process so the environment variable is properly set in the AWS Lambda function.

Lines 13-15 are quite simple. The only thing to note is that you should set the session_key variable to something which makes sense for your application. This is the name of the key which stores the current state for a user in the session backend. This isn’t important when using the cookie backend, however when using the dynamodb backend you will actually see this named key in DynamoDB...so it’s nice to have it named something which is clear and makes sense.

serverless.yml

Next we need to configure Serverless to set up our Lambda function correctly. Crack open the generated serverless.yml file and replace the contents with the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
service: Recipes

provider:
  name: aws
  runtime: python2.7
  region: ${env:AWS_REGION}
  memorySize: 128

package:
  exclude:
    - "**/*.pyc"
    - "**/*.swp"

functions:
  recipes:
    handler: handler.main
    events:
      - alexaSkill

States

In the src directory, create a file called states.yml with the following content:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
initialState:
  response:
    shouldEndSession: false
    outputSpeech:
      type: SSML
      ssml: >
        <speak>
          Welcome to simple recipe helper. Would you like to make some scrambled
          eggs?
        </speak>
    reprompt:
      type: SSML
      ssml: >
        <speak>
          Would you like to make some scrambled eggs?
        </speak>
  branches:
    AMAZON.YesIntent: ingredientsScrambledEggs
    default: goodBye

ingredientsScrambledEggs:
  response:
    shouldEndSession: false
    outputSpeech:
      type: SSML
      ssml: >
        <speak>
          For this recipe you will need a non stick frying pan, a spatula, a
          bowl, a fork, and 2 eggs. Have you located all of these and are you
          ready to begin?
        </speak>
    reprompt:
      type: SSML
      ssml: >
        <speak>
          For this recipe you will need a non stick frying pan, a spatula, a
          bowl, a fork, and 2 eggs. Have you located all of these and are you
          ready to begin?
        </speak>
  branches:
    AMAZON.YesIntent: stepOneScrambledEggs
    AMAZON.NoIntent: ingredientsScrambledEggs
    default: goodBye

stepOneScrambledEggs:
  response:
    shouldEndSession: false
    outputSpeech:
      type: SSML
      ssml: >
        <speak>
          Without getting the egg shell into the bowl, crack the first egg into
          the bowl.
          <break time="3s" />
          Repeat this for the second egg.
          <break time="3s" />
          Are you ready for the next step?
        </speak>
    reprompt:
      type: SSML
      ssml: >
        <speak>
          Without getting the egg shell into the bowl, crack the first egg into
          the bowl.
          <break time="3s" />
          Repeat this for the second egg.
          <break time="3s" />
          Are you ready for the next step?
        </speak>
  branches:
    AMAZON.YesIntent: stepTwoScrambledEggs
    AMAZON.NoIntent: stepOneScrambledEggs
    default: goodBye

stepTwoScrambledEggs:
  response:
    shouldEndSession: false
    outputSpeech:
      type: SSML
      ssml: >
        <speak>
          Take the fork and whisk the eggs in the bowl until the egg yolks are
          mixed with the egg whites.
          <break time="5s" />
          Are you ready for the next step?
        </speak>
    reprompt:
      type: SSML
      ssml: >
        <speak>
          Take the fork and whisk the eggs in the bowl until the egg yolks are
          mixed with the egg whites.
          <break time="5s" />
          Are you ready for the next step?
        </speak>
  branches:
    AMAZON.YesIntent: stepThreeScrambledEggs
    AMAZON.NoIntent: stepTwoScrambledEggs
    default: goodBye

stepThreeScrambledEggs:
  response:
    shouldEndSession: false
    outputSpeech:
      type: SSML
      ssml: >
        <speak>
          Pour the beaten eggs into the non stick frying pan.
          <break time="5s" />
          Are you ready for the next step?
        </speak>
    reprompt:
      type: SSML
      ssml: >
        <speak>
          Pour the beaten eggs into the non stick frying pan.
          <break time="5s" />
          Are you ready for the next step?
        </speak>
  branches:
    AMAZON.YesIntent: stepFourScrambledEggs
    AMAZON.NoIntent: stepThreeScrambledEggs
    default: goodBye

stepFourScrambledEggs:
  response:
    shouldEndSession: false
    outputSpeech:
      type: SSML
      ssml: >
        <speak>
          Take the non stick frying pan and place it on one of the eyes of your
          cook top. Make sure to turn on the eye to low heat.
          <break time="5s" />
          Are you ready for the next step?
        </speak>
    reprompt:
      type: SSML
      ssml: >
        <speak>
          Take the non stick frying pan and place it on one of the eyes of your
          cook top. Make sure to turn on the eye to low heat.
          <break time="5s" />
          Are you ready for the next step?
        </speak>
  branches:
    AMAZON.YesIntent: stepFiveScrambledEggs
    AMAZON.NoIntent: stepFourScrambledEggs
    default: goodBye

stepFiveScrambledEggs:
  response:
    shouldEndSession: false
    outputSpeech:
      type: SSML
      ssml: >
        <speak>
          Occasionally stir and flip the eggs in the frying pan with your
          spatula to make sure they cook evenly while slightly increasing the
          heat of the cooking eye once every minute.
          <break time="5s" />
          Are you ready for the next step?
        </speak>
    reprompt:
      type: SSML
      ssml: >
        <speak>
          Occasionally stir and flip the eggs in the frying pan with your
          spatula to make sure they cook evenly while slightly increasing the
          heat of the cooking eye once every minute.
          <break time="5s" />
          Are you ready for the next step?
        </speak>
  branches:
    AMAZON.YesIntent: stepSixScrambledEggs
    AMAZON.NoIntent: stepFiveScrambledEggs
    default: goodBye

stepSixScrambledEggs:
  response:
    shouldEndSession: true
    outputSpeech:
      type: SSML
      ssml: >
        <speak>
          The scrambled eggs will be done when they are no longer runny. When
          they are done, transfer them to a plate and enjoy.
          <break time="5s" />
          Thanks for trying simple recipe helper.
        </speak>

goodBye:
  response:
    shouldEndSession: true
    outputSpeech:
      type: SSML
      ssml: >
        <speak>
          Thanks for trying simple recipe helper.
        </speak>

Deploy

With that, everything is ready to create our stack and Lambda function. Inside the Docker container in the same directory as the Makefile we’ll execute make deploy:

root@9fcf3335e5aa:/code# make deploy
cd src && sls deploy -s dev
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading service .zip file to S3 (272.67 KB)...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...................
Serverless: Stack update finished...
Service Information
service: Recipes
stage: dev
region: us-east-1
api keys:
  None
endpoints:
  None
functions:
  Recipes-dev-recipes: arn:aws:lambda:us-east-1:234123421348:function:Recipes-dev-recipes

Make note of the Lambda arn in the last line. This is the arn which you’ll need to plug into your Alexa skill’s “Configuraton -> Endpoint”

Iteration

Once the initial deploy is done you’ll likely be updating code and need to redeploy. This can be accomplished by using the make function target. This will re-upload your application code to the Lambda function and takes 5-10 seconds usually.

If you make any changes to the actual stack, (i.e., adding a DynamoDB table, updating an envrionment variable, or the like) you’ll want to do an make deploy again.

Configuring Alexa

At this point your backend system is fully ready to handle Alexa requests. Provided your Alexa app is configured correctly in the Amazon Developer portal everything should be working.

Cloud Watch

If you have the LAZYSUSAN_LOG_LEVEL environment variable for your AWS Lambda function set to logging.INFO you will be able to read fairly detailed logs that have been created by your Alexa skill.