Skip to main content

Amazon Web Services

Automation, CloudFormation and Amazon Connect

CloudFormation is a tool from Amazon used to automate the deployment of AWS services. In this post, we’ll cover some tips and tricks for using CloudFormation to automate deploying Amazon Lambda functions for Amazon Connect. This automation is repeatable, testable and far less error prone than asking a person to do it all by hand. Let’s get started!

 

CloudFormation Templates

AWS CloudFormation provides a common language for you to describe and provision all the infrastructure resources in your cloud environment. CloudFormation allows you to use a simple text file to model and provision, in an automated and secure manner, all the resources needed for your applications across all regions and accounts.

https://aws.amazon.com/cloudformation/

The simple text files mentioned above are called CloudFormation templates. Templates are written in either JSON or YAML (Yet Another Markup Language), we will focus on these templates for the rest of the post.

There is a visual designer in AWS for CloudFormation templates, but we will focus on writing them from scratch. AWS provides a number of sample templates at https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/sample-templates-services-us-west-2.html. The full template language documentation is at: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-reference.html

 

Whither the Weather?

For this post, we will create a template that deploys two Lambda functions that can be used in Amazon Connect contact flows. These functions will get local weather conditions for a city. We’ll use the OpenWeatherMap service. If you’d like to follow along, you can sign up for a free account at https://home.openweathermap.org/users/sign_up

With an account and API access key we issue simple HTTP requests to get the current weather in a city, let’s say Chicago:

GET https://api.openweathermap.org/data/2.5/weather?id=4887398&appid=8b2...

And we get back a JSON payload of current weather data (trimmed for space):

{
       "coord": {
           "lon": -87.65,
           "lat": 41.85
       },
       "weather": [
           {
               "id": 701,
               "main": "Mist",
               "description": "mist",
               "icon": "50d"
           },
         ...
       "id": 4887398,
       "name": "Chicago",
         ...
}

We’ll have one Lambda function that gets the current weather for a city and another Lambda that takes a city name and returns the OpenWeatherMap API city id used in the current weather query string.

 

Putting it Together

I’m going to start by showing you the completed CloudFormation template and then we’ll work our way back over the tricky parts. If you’ve never used CloudFormation templates, you can try to follow along, but you’ll feel a lot better if you’ve worked through some of the Amazon examples and documentation first.

You can take a look at the full CloudFormation template here:

Amazon Web Services - Avoid Contact Center Outages: Plan Your Upgrade to Amazon Connect
Avoid Contact Center Outages: Plan Your Upgrade to Amazon Connect

Learn the six most common pitfalls when upgrading your contact center, and how Amazon Connect can help you avoid them.

Get the Guide

https://gist.github.com/phmiller/07c2e220ca3e8d747be7645aaf2b7c64

 

GetOpenWeatherMapCityId Lambda

We’ll start with the Lambda function to get an OpenWeatherMap API city id. The body of this function is declared inline in the template as shown below:

  getOpenWeatherMapCityIdLambdaFunction:
       Type: "AWS::Lambda::Function"
       Properties:
         Description: "Gets the OpenWeatherMap API city id for a city"
         Code:
           ZipFile: !Sub |
             exports.handler = (event, context, callback) => {
               const cityName = event.cityName;
               console.log(
                 "Looking up city id for " + cityName
               );
               //hardcoded for sample purposes;
               var cityId = 4887398; //Chicago
               callback(null, { cityName: cityName, cityId: cityId});
             }
         Handler: "index.handler"
         Role: !GetAtt 
           - "executeOwnLambdaIAMRole"
           - "Arn"
         Runtime: "nodejs6.10"
         Timeout: 8
         MemorySize: 128
       DependsOn:
         - "executeOwnLambdaIAMRole"

The actual JavaScript code here is unimportant. I just hard-coded a return value for Chicago.

What’s more interesting is the Role attribute where we assign the executeOwnLambdaIAMRole to this Lambda function. Every Lambda function needs to be assigned an IAM role so it can execute at all. This is an easy step to miss.

Within the Role attribute is a bit of interesting syntax that we will see it again later. Using the !GetAtt operator along with the name of the IAM role and the attribute we want is a common pattern that allows you to refer to other objects within the template.

I defined the executeOwnLambdaIAMRole immediately above the Lambda in the template using the system policy of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole.

 

getCurrentWeatherForCity Lambda

The getCurrentWeatherForCity Lambda function that we will actually want to call from an Amazon Connect contact flow is declared inline as seen below:

getCurrentWeatherForCityLambdaFunction:
       Type: "AWS::Lambda::Function"
       Properties:
         Description: "Looks up current weather conditions in a city from OpenWeatherMap API"
         Code:
           ZipFile: !Sub |
             const https = require("https");
             const aws = require("aws-sdk");
             const openWeatherMapApiKey = process.env["OpenWeatherMapApiKey"];
             
             exports.handler = (event, context, callback) => {
               const cityName = event.Details.ContactData.Attributes.CityName;
               var lambda = new aws.Lambda({
                 region: "${AWS::Region}"
               });
               var payloadObject = { cityName: event.Details.ContactData.Attributes.CityName };
               lambda.invoke(
                 {
                   FunctionName: "${getOpenWeatherMapCityIdLambdaFunction}",
                   Payload: JSON.stringify(payloadObject)
                 },
                 function(error, data) {
                   if (error) {
                     console.error("Failed to invoke Lambda to get city id", error);
                     callback("Failed to invoke Lambda to get city id: " + error);
                   }
                   if (data) {
                     var cityId = JSON.parse(data.Payload).cityId;
                     console.log("Got city id " + cityId + " for city " + cityName);
                     //make open weather api call
                     const queryString = "/data/2.5/weather?id=" + cityId + "&appid=" + openWeatherMapApiKey;
                     console.log("Querying API for weather with query string of " + queryString);
                     //hardcoded for sample purposes
                     callback(null, { name: "Chicago", weather: "Mist" });
                   }
                 }
               );
             }
         Handler: "index.handler"
         Role: !GetAtt 
           - "executeOwnAndGetCityIdLambdaIAMRole"
           - "Arn"
         Runtime: "nodejs6.10"
         Timeout: 8
         MemorySize: 128
         Environment:
           Variables:
             OpenWeatherMapApiKey: !Sub ${OpenWeatherMapApiKey}
       DependsOn:
         - "executeOwnAndGetCityIdLambdaIAMRole"

Again, the JavaScript logic here is mostly unimportant, but there are a few items to highlight. This function needs the OpenWeatherMap API Key to construct valid requests. Rather than hardcode that value into the function body, we instead set an environment variable, using the CloudFormation parameter OpenWeatherMapApiKey which a user supplies when running the template. We use the !Sub operator and curly braces to get the value.

OpenWeatherMapApiKey: !Sub ${OpenWeatherMapApiKey}

Then, from within the JavaScript function, we access the environment variable in standard Node style from the process object.

const openWeatherMapApiKey = process.env["OpenWeatherMapApiKey"];

Like the previous Lambda function, we assign an IAM Role. In this case the Role is executeOwnAndGetCityIdLambdaIAMRole. This Role has the permissions we saw before in addition to a policy that allows it to invoke the Get City Id Lambda function from code.

Policies: 
           - 
             PolicyName: "invokeCityIdLambda"
             PolicyDocument: 
               Version: "2012-10-17"
               Statement: 
                 - 
                   Effect: "Allow"
                   Action: "lambda:InvokeFunction"
                   Resource:
                     - !GetAtt 
                       - "getOpenWeatherMapCityIdLambdaFunction"
                       - "Arn"

We are using the !GetAtt operator and getting the ARN of a resource we created in the template.

The Lambda function uses the aws-sdk package to invoke the Get City Id function.

 

Amazon Connect Permissions

To use the Get Weather Lambda function from Amazon Connect, we need to grant Amazon Connect permissions on it. We can do this manually through the AWS command line, but in this template, we script it instead. We grant Amazon Connect (the principal), permission to invoke the Lambda Function from our AWS Account. Within the SourceAccount attribute we use AWS::AccountId a built-in variable in CloudFormation templates.

    # permission so that connect can invoke it
     getCurrentWeatherForCityLambdaFunctionInvokePermission:
       Type: AWS::Lambda::Permission
       DependsOn: getCurrentWeatherForCityLambdaFunction
       Properties:
         FunctionName:
           Ref: getCurrentWeatherForCityLambdaFunction
         Action: lambda:InvokeFunction
         Principal: connect.amazonaws.com
         SourceAccount:
           Ref: AWS::AccountId

Final Notes

If you found this post interesting, I encourage to take some time and work through the rest of the template. There are a couple of other useful nuggets in there. For example, the template sets up CloudWatch events to periodically trigger and keep the Lambda functions warm (as discussed in my prior post: https://blogs.perficient.com/integrate/2017/11/27/keeping-lambdas-warm-in-amazon-connect/).

Thanks for reading. Any questions, comments or corrections are greatly appreciated. To learn more about what we can do with Amazon Connect, check out Helping You Get the Most Out of Amazon Connect

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Peter Miller

Peter Miller is a Solutions Architect at Perficient focused on call center solutions including Amazon Connect

More from this Author

Follow Us
TwitterLinkedinFacebookYoutubeInstagram