.NET on AWS Blog

Building an Alexa Skill with AWS Lambda and Amazon DynamoDB – Part 1: Creating Data for the Skill

This blog series will walk you through the process of creating an Amazon Alexa skill that queries data from an Amazon DynamoDB table. Part 1 focuses on creating the data source that the skill will query and part 2 focuses on creating the AWS Lambda function to query the data and creating the skill. In Part 1 of the series, you will create an AWS Lambda function that writes to Amazon DynamoDB. The function will run daily to create a DynamoDB table, and will insert records into the table.

Architecture Diagram

In this blog series, we will create 2 AWS Lambda functions, one to create a datastore that will contain our movie data, and another that will be called via an Alexa Skill. The diagram for what we are building is below.

Architectural diagram of Amazon Alexa interacting with AWS Lambda and Amazon DynamoDB inside the AWS Cloud.

What kind of skill are you building?

I am a huge fan of movies, and use The Movie Database (TMDB) often when looking for new movies. We will build an Alexa skill that when given a popular movie, will respond with the date it was released in theaters (or released in general if it was a streaming exclusive). Before we begin, there are a few things to consider.

  • How do you get access (and information) to the most popular movies from TMDB?
  • How can you get to this information from the Alexa skill?
  • What services and tools are needed for the job?

One of the advantages of Alexa skills is that you can host the backend service that the skill calls in AWS Lambda, which is a serverless, event-driven compute service that allows you to run specific code without the need to obtain any underlying infrastructure. You can build Lambda functions in multiple languages through the use of runtimes. One thing to note is that there are specific regions you should create your Lambda functions in depending on your location.

Requirements

In order to follow to steps outlined in this blog, you will need to perform a handful of prerequisites to set up your developer environment as well as gain access to the resources needed to complete the outlined steps. Those requirements are listed below:

The first thing after getting the accounts setup is to think how you want this solution to work. It is recommended that you use The Movie Database (TMDB) API to get a list of popular movies, so you don’t have to hit the API every single time the skill runs. With this thought in mind, you can then utilize a cache to set a number of popular movies somewhere and have the skill hit the data store. In AWS, there are several AWS Cloud Database options to host a datastore, but considering our needs and the fact that it is pretty cost effective, you should go with DynamoDB. DynamoDB is a key-value NoSQL database and a great fit for web applications with high traffic (which hopefully if the skill is popular is). So, the solution is as follows:

  • Have some process to load data into DynamoDB on a set schedule (popular movies can change)
  • Have another process that is the backend for the Alexa Skill

For both parts of the solution, it is advantageous to use AWS Lambda because you can schedule Lambda functions using AWS EventBridge and there are triggers for Alexa built in to AWS Lambda. The next thing is to get started using Visual Studio.

Setting up the solution

If you haven’t worked with AWS and Visual Studio before, the first time you open Visual Studio after installing the Toolkit extension, you will need to do some setup. The first thing you should do is follow the tutorial on setting up your profile. If you are configured correctly, the AWS Explorer will populate in Visual Studio.

AWS Explorer populated after profile added to Visual Studio

Now you are all setup and can start building the solution. Let’s start with the Lambda function that gets popular movies and caches it in DynamoDB.

Building the Movie Database

As mentioned before, you are going to create a Lambda function that runs on a schedule and populates a DynamoDB table. The first thing to do is create a new AWS Lambda project in Visual Studio. Follow the steps in Create a Visual Studio .NET Core Lambda Project to learn how to create your project. In our case, the project will be called SeedData. After choosing a location, choose the Create button. You will than be prompted to choose a Blueprint. To learn more about Blueprint, review the documentation. In our case, we will choose the Empty Function blueprintand choose Finish.

New AWS Lambda C# Project Select Blueprint page in Visual Studio 2022 with "Empty Function" highlighted

After the project is created, you will see that the default Lambda function template is one function that converts a provided string to upper case.

public class Function
{
    
    /// <summary>
    /// A simple function that takes a string and does a ToUpper
    /// </summary>
    /// <param name="input"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public string FunctionHandler(string input, ILambdaContext context)
    {
        return input.ToUpper();
    }
}

You can test the function to get familiar with the Mock Lambda Test Tool. The Test Tool is an efficient way to test Lambda functions locally before deploying to AWS. It is configured as the default start project in Visual Studio and hitting F5 or the Green Arrow presents us with the browser-based tool.

AWS .NET Core 6.0 Mock Lambda Test Tool

The tool allows you to specify the function and provide a request to test locally. For more information on the Test Tool, check out the AWS .NET Mock Lambda Test Tool documentation on GitHub. You can run a quick test by passing in a string in the sample box and choosing Execute Function.

AWS .NET Core 6.0 Mock Lambda Test Tool with Function Input populated with "Lambda is awesome" and highlighted as well as Response populated with "LAMBDA IS AWESOME" in all capital letters and highlighted

Now that you are familiar with creating and testing a Lambda function, let’s start adding some code! A few things to call out:

  • You will need to communicate with the DynamoDB table from this project AS WELL as the other project created for the Alexa Skill.
  • There are some secrets in the project (TMDB key for instance) and they should be stored somewhere not in code.
  • You also want to use Dependency Injection to wire-up any services that exist.

The next step is to do the following:

  • Create a new Class Library project that has a service to communicate with DynamoDB.
  • Use a ServiceProvider to register any services, as well as Logging and Configuration.

The new project that was created `TMDBAlexa.Shared` contains 1 class, `DynamoDBService` which will provide methods to create a table, write data to that table, and scan (DynamoDB jargon for search) that table. The reason there is code to write the table is that there will be a need to recreate the table every time the function runs, as it is simpler to delete and create the table than truncate all the items.

Creating DynamoDB table

The below example is just one way to create the DynamoDB table. The full DynamoDBService class is available in the GitHub Repo but here is the code that creates the table.

AmazonDynamoDBClient _client = new AmazonDynamoDBClient();

var request = new CreateTableRequest
{
    AttributeDefinitions = new List<AttributeDefinition>()
{
    new AttributeDefinition
    {
        AttributeName = "id",
        AttributeType = "N"
    },
    new AttributeDefinition
    {
        AttributeName = "popularity",
        AttributeType = "N"
    }
},
    KeySchema = new List<KeySchemaElement>
{
    new KeySchemaElement
    {
        AttributeName = "id",
        KeyType = "HASH" //Partition key
    },
    new KeySchemaElement
    {
        AttributeName = "popularity",
        KeyType = "RANGE" //Sort key
    }
},
    ProvisionedThroughput = new ProvisionedThroughput
    {
        ReadCapacityUnits = 5,
        WriteCapacityUnits = 6
    },
    TableName = _tableName
};
 
var response = await _client.CreateTableAsync(request);

One of the advantages of building .NET apps with AWS, is the AWS SDK for .NET, which has experiences for interacting with many AWS services. In the above snippet, an instance of AmazonDynamoDBClient as well as CreateTableRequest is defined where a partition key and a sort key are configured. Once completed, the table is created using the SDK. For more information on interacting with DynamoDB in C#, read the .NET code examples in the Amazon DynamoDB documentation.

Now that the table is created, TMDB API will be queried and those records will be written to the table.

Writing to DynamoDB table

for (int i = 1; i < 100; i++)
{
    var results = await client.GetMoviePopularListAsync("en-US", i);
 
    await _dbService.WriteToTable(results);
}

public async Task WriteToTable(SearchContainer<SearchMovie> results)
{
    DynamoDBContext context = new DynamoDBContext(_client);
 
    BatchWrite<MovieModel> model = context.CreateBatchWrite<MovieModel>();
 
    foreach (var movie in results.Results)
    {
        model.AddPutItem(new MovieModel
        {
            Id = movie.Id,
            Overview = movie.Overview,
            Popularity = movie.Popularity,
            ReleaseDate = movie.ReleaseDate.ToString(),
            Title = movie.Title,
            TitleLower = movie.Title.ToLower(),
            VoteAverage = movie.VoteAverage,
            VoteCount = movie.VoteCount
        });
    }
 
    await model.ExecuteAsync();
}
 
       
The TMDB API is used to get popular movies and utilizes paging, where each API call returns a page of 20. In order to cache more than 20 movies, a crude loop can be setup to get roughly 2000 movies (100 pages) than write those movies to the table. In order to write to the table, an instance of BatchWrite is created for each batch and add each movie via the AddPutItem method (by batching a page at a time in this case). Now that the DynamoDBService is wired up, you need to call that service from the Lambda project. You should utilize dependency injection for logging and configuration, so a few things need to be done. First, setup a Startup class, which will be familiar to ASP.NET Core developers, that creates an instance of IServiceProvider, registers a ConfigurationBuilder, ILoggingBuilder, and creates the DynamoDB service.
 
 
public class Startup
    {
        public IServiceProvider Setup()
        {
            var configuration = new ConfigurationBuilder()
                    .SetBasePath(Directory.GetCurrentDirectory())
                    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                    .AddEnvironmentVariables()
                    .Build();
 
            var services = new ServiceCollection();
 
            services.AddSingleton<IConfiguration>(configuration);
 
            services.AddLogging(loggingBuilder =>
            {
                loggingBuilder.ClearProviders();                
            });
 
 
            ConfigureServices(configuration, services);
 
            IServiceProvider provider = services.BuildServiceProvider();
 
            return provider;
        }
 
        private void ConfigureServices(IConfiguration configuration, ServiceCollection services)
        {
            AWSOptions awsOptions = configuration.GetAWSOptions();
            services.AddDefaultAWSOptions(awsOptions);
            services.AddAWSService<IAmazonDynamoDB>();
            
            services.AddSingleton<LambdaEntryPoint, LambdaEntryPoint>();
            services.AddSingleton<DynamoDBService, DynamoDBService>();
        }
    }
 
       
With the default template for a Lambda Function, a Function.cs class is provided. In this class, the default entry point for the Lambda function is a FunctionHandler. In order to take advantage of the Dependency Injection we setup earlier, we will need to update the Function.cs class. There are a few ways to do it, including the new Lambda Annotations framework, which is provided by AWS. In our case, we are going to create a new class called LambdaEntryPoint.cs which will have constructor-based injection for the dependencies we defined in Startup.cs. This gives all methods in this new class access to these services as they are declared at the class level and initialized in the class’s constructor.

LambdaEntryPoint.cs

Here is a completed version of LambdaEntryPoint.cs. We initialize the services we registered in our Startup.cs in the constructor as mentioned above. This class also has one additional method called Handler, which will be executed from our FunctionHandler in Function.cs
 
 
public class LambdaEntryPoint
{
    private readonly ILogger<LambdaEntryPoint> _logger;
    private readonly DynamoDBService _dbService;
    private readonly IConfiguration _config;
    public LambdaEntryPoint(ILogger<LambdaEntryPoint> logger, DynamoDBService dbService, IConfiguration config)
    {
        _logger = logger;
        _dbService = dbService;
       _config = config;
    }
 
    public async Task<string> Handler()
    {
        _logger.LogInformation("Handler invoked");
 
        TMDbClient client = new TMDbClient(_config["TMDBApiKey"]);
 
        await _dbService.CreateTable();
 
        for (int i = 1; i < 100; i++)
        {
            var results = await client.GetMoviePopularListAsync("en-US", i);
 
            await _dbService.WriteToTable(results);
        }
 
        return "Done";
    }
}

Function.cs

The final step to wire-up dependency injection is to initialize an instance of our Startup class, than register the services via a ServiceProvicer and finally use the GetRequiredService() extension to get a registered instance of our LambdaEntryPoint. This instance will be resolved and all dependencies of that service will be resolved as well. At this point we can call the Handler method of the resolved instance of LambdaEntryPoint. That code is below.

private readonly LambdaEntryPoint _entryPoint;
 
public Function()
{
    var startup = new Startup();
    IServiceProvider provider = startup.Setup();
 
    _entryPoint = provider.GetRequiredService<LambdaEntryPoint>();
}
 
public async Task<string> FunctionHandler(ILambdaContext context)
{
    return await _entryPoint.Handler();
 
       

If you run this Lambda function locally, you can validate that the records are inserted into the table from the AWS Explorer in Visual Studio.

AWS Explorer displaying Amazon DynamoDB table view in Visual Studio 2022

The second thing to do is deploy the Lambda function to AWS using Visual Studio. After a successful deployment, open the context (right-click) menu for the project and choose Publish to AWS Lambda.

Right-click project context menu with "Publish to AWS Lambda" highlighted

Specify a name for the function and choose Next.

Upload Lambda Function Dialog in Visual Studio 2022 with function name populated with "TMDBAlexaSeedData"

Lastly, specify a role for this function. For this example, you can use AWSLambdaBasicExecutionRole since this is just a demo. To learn more about AWS Identity and Access Management (roles being a part of this), take a look at the IAM roles section in the AWS Identity and Access Management user guide.

Advanced function details pane of Upload to AWS Lambda dialog in Visual Studio 2022. In Role Name dropdown, value AWSLambdaBasicExecutionRole is highlighted

When completed, you can upload the function by choosing the Upload button in the wizard. Publishing is fairly quick and when successful, the AWS Toolkit provides a screen to test the function after it is deployed directly from Visual Studio.

Output pane of Upload to AWS Lambda screen in Visual Studio 2022

The code for the SeedData project is complete. Now additional configurations are needed, which you can add via the AWS Console.

Configuring the Lambda function to run on a schedule

Lambda functions can be configured to run on a schedule with EventBridge. To do this, navigate to the management page of the Lambda function and select Add Trigger. From there, choose EventBridge.

Add trigger screen of Lambda function menu in AWS Console. "Select a source dropdown is open with "EventBridge (Cloudwatch Events)" is highlighted

After selecting EventBridge, create a new rule, give it a name and specify an expression to represent the schedule in cron format. In this case, it should run once a day at midnight UTC.

Trigger Configuration Screen in AWS Console. Create a new rule radio button is selected. Rule name is "Run_TMDB_Seed". Schedule expression value is "cron(0 0 * * ? *)

After the rule is created, the Function overview will contain the new trigger.

Function overview for AWS Lambda function named TMDBAlexaSeedData with EventBridge trigger added in the AWS Console

Granting the Lambda function access to DynamoDB

When you were building the app locally, you took advantage of the local profile to interact with DynamoDB. When the function is running in AWS you need to update the IAM role that was just created to have DynamoDB access. This can be done by going to the manage page of the Lambda function, navigating to the Configuration tab, and choosing or clicking on the Execution role name.

AWS Lambda Function named TMDBAlexaSeedData Configuration Page with Execution role Role Name "lambda_exec_TMDBAlexaSeedData" highlighted

From there go to the Permissions tab, choose Add permissions, and Attach policies.

IAM Roles Section of AWS Console. Selected role is "lambda_exec_TMDBAlexaSeedData". Add permission dropdown opened with "attach policies" highlighted

In Other permissions policies search for “DynamoDB” policies, and select “AmazonDynamoDBFullAccess”.

Attach policy page for ""lambda_exec_TMDBAlexaSeedData" in AWS Console. Policy name "AmazonDynamoDBFullAccess" highlighted

After configuring access to DynamoDB, our function will execute on the defined CRON cycle above. After it executes, we can go to the DynamoDB section of the AWS Console, choose our movies table, than choose the Explore table items button. In that view, we should see data residing in the table

DynamoDB table list in AWS Console

Table view in AWS Console for movies table.

Conclusion

Now we have an AWS Lambda Function that runs on a schedule to create a datastore filled with movie information. In the next post in this series, you will create the AWS Lambda instance to query the data in DynamoDB and then create an Alexa skill that uses that AWS Lambda instance as the backend to respond to users requests via their Alexa device.