Saturday, January 26, 2019

Serverless Computing: Introduction and Hello World on Azure and AWS

A term that is all the rage now in cloud computing is "Serverless Computing". In this post we'll explain what serverless computing is, why it is important, and how to get started with it—on both Microsoft Azure and Amazon Web Services. We'll finish up with a Hello World function in both Azure Functions and AWS Lambda.


Leave the Driving to Us

If you're a developer, a term like "Serverless Computing" is immediately suspicious, since you know there must be servers somewhere—and indeed there are: it's simply that they are not visible to you. The cloud computing platform takes care of allocating servers, deploying your code to them, and managing scale. What does that leave for you to worry about? Just one thing, your code. As Greyhound used to say, "Leave the Driving to Us."

Functions as a Service (FaaS)

Serverless computing is often described as "Functions as a Service". If you've been keeping up with cloud computing, you've no doubt encounterd the _aaS (something as a Service) labels. Focusing on the primary categories of cloud computing, let's see how things have been progressing.

In its earliest years, cloud computing provided Infrastructure as a Service (IaaS), which meant you had a place in the cloud where you could set up virtual machines and related infrastructure like storage and load balancers. As an IT professional, you could get any kind of resource you needed minutes after requesting it and get right to work. As a service, you only paid for what you used/allocated and could stop anytime without penalty. This do-it-yourself instant computing was a hit, and it wasn't long before great minds started thinking about how to make it even better.

When Platform-as-a-Service (PaaS) services arrived, the cloud platform took care of underlying details like allocating and managing servers for you, including keeping servers patched and scaling them: you could now just focus on your application. A a PaaS developer, you could publish to the cloud right from your development environment and the rest would be handled for you. Similarly, on the data side of things, DBaaS (database as a service) provided the same ease of use for databases. It was hard to see how things could get much better.

Enter Functions as a Service (FaaS) and now things get even simpler. What's the smallest unit of code you can write? A function. FaaS lets you deploy just the code of a single function to the cloud. Like PaaS, all the details of server allocation, deployment, and scale are handled automatically. Unlike PaaS, you don't even allocate servers; you've uploaded and configured a function, and that's the only artifact you see in the cloud. The cloud platform takes it from there: if your function is used with increasing demand, it will scale onto mulitple servers. If your function use decreases, fewer resources will be allocated to it. FaaS is also typically delivered with a pay-by-request model, meaning you only pay for usage of your function. You pay nothing when it's idle.

Uses for FaaS

If you're used to deploying an entire application to the cloud as a single unit, it may strike you that a single function is way too granular a thing to deploy to the cloud. But consider the following:
  • There's growing interest in Microservices Architecture, in which small services are independently deployed.
  • The Internet of Things (IoT) phenomenon is connecting devices left and right; these devices need cloud-hosted code to manage them.
  • APIs are big nowadays; and to have a good one, you need a highly scalable platform and an easy way to deploy updates.
  • Many cloud applications in Production have a requirement for minimal disruption, yet traditional methods of deploying an entire application are overkill when you only need to apply a patch or update to one small area. 
For each of the above scenarios, FaaS is a perfect fit. Naturally, you are going to have to think carefully about security. As a new FaaS developer, you should follow the AWS Lambda Best Practices guidance or Azure Functions Best Pratices guidance.

Whether you've been aware of it or not, serverless computing is probably already impacting your life. Here are some real-world case studies of AWS Lambda:
And here are some real-world case studies of Azure Functions:

Please note that using FaaS does not necessarily mean you have to re-think your entire architecture: you can introduce FaaS organically if you like. It is completely feasible to take a more traditional cloud app, add a FaaS function, and grow your use of FaaS functions over time.

How FaaS Works

Services and Pricing

On Microsoft Azure, the serverless computing service is called Azure Functions. On AWS, the serverless computing service is called AWS Lambda

You can view Azure Functions rates and AWS Lambda rates to compare pricing. At the time of this writing, both platforms are generously providing a million free requests/month before charges apply. Cost really isn't a barrier to experimenting with FaaS.

How Does a FaaS Function Communicate?

It's all well and good to say we're going to host a function in the cloud, but how exactly will that function be invoked? You're going to like the answer, because it's one of the most versatile examples of software re-use that ever existed: any way you want them to. Once you understand how many ways there are to plug in your FaaS function, your mind will be reeling with possibilities.

Do you want your function to act like a service, responding to HTTP traffic? Or to process tasks that have been queued? Or to fire whenever a database record change occurs? Or to respond to incoming email messages? Or to run regularly at scheduled intervals? Or to furnish the back-end logic for a smart assistant voice user interface? Or to connect to an IoT device? All of these connections are easily made for FaaS functions. 


Here's a (partial!) table of currently-supported event sources for AWS Lambda and Azure Functions. For the most up-to-date information and more detail about function inputs/outputs, see AWS Lambda Supported Event Sources and Azure Functions Triggers and Bindings Concepts. Both platforms are regularly introducing new ways of invoking functions.

TriggerAWSAzure
HTTPAPI GatewayAPI Management
DatabaseDynamoDBCosmosDB
Email MessagesSESSendGrid
File StorageS3 bucket eventsAzure Blob Storage
IoTAWS IoT ButtonIoT Hub
Push Notifications  SNSAzure Notification Hubs
Queue MessagesSQSAzure Queue Storage
TimersCloudWatch ScheduledEvents  Timer Trigger
VoiceAlexa Skill

As you configure the trigger for your function, your cloud platform will often provide features for data binding and security. Typically, you'll secure your function by configuring the security model of the trigger (such as an API or storage queue).

Limitations of FaaS: Programming Languages

You knew there had to be some drawbacks, didn't you? In return for the easy, auto-scaling, pay-for-invocation model, there are some limitations.

In terms of execution time, your function must execute quickly. Both platforms originally required functions to execute in 5 minutes' time or less. At the time of this writing, your AWS Lambda function must execute within 15 minutes and an Azure Function within 10 minutes.

Cloud platforms are only prepared to support a limited collection of development languages (although this is sure to grow over time). Even so, you're likely to find something you know in the list of support languages. Do note that if you're a dedicated .NET developer, .NET Core is supported on both platforms but not the .NET Framework.

LanguageAzure FunctionsAWS Lambda
C# (.NET Core)
F# (.NET Core)
Go
Java
JavaScript (Node.js)
Powershell
Python
Ruby

FaaS isn't available in every region but is available in most of them. You can view the AWS Regions and Azure Regions lists to confirm whether a particular region supports FaaS. If you are going to depend on FaaS, you should take region availability into account before deciding where to deploy your solution.

Another "limitation" we should consider is fragmentation. This is not anything imposed by a cloud platform, merely a side-effect of the granularity of deploying something as small as a function. If you have dependencies between your functions, deploying a function-at-a-time is a notable departure from prior methods of publishing when you deployed your application as an atomic unit. If you're not disciplined, you could end up with a poor mix of software that doesn't work together. You'll need to think through how you keep track of versioning and dependencies. Maintaining a comprehensive set of unit tests for your functions is highly recommended.

Differences Between AWS Lambda and Azure Functions

So far, we've seen an awful lot of similarities between Azure and AWS offerings: both platforms offer low-cost, auto-scaling FaaS in a variety of languages that can be invoked in many different ways. Are there any differences worth noting? There's a good comparison article here.

First off, credit where credit is due: AWS was first to offer serverless computing. AWS Lambda debuted in 2014; Azure Functions came in 2016. AWS Lambda functions execute on Linux machines; Azure Functions can run on Windows or Linux (Linux support is in Preview).

Although both platforms offer an impressive selection of ways to trigger functions, they are not identical. If your needs are specific, one platform may provide a distinct advantage over the other. AWS, for example, can integrate AWS Lambda functions with Alexa Skills for voice user interfaces. (see an example here). Azure, on the other hand, can integrate Azure Functions with Excel spreadsheets via Microsoft Graph. 

AWS Lambda and Azure Functions originally targeted a function execution to take no more than 5 minutes. More recently, AWS Lambda now allows up to 15 minutes; and Azure Functions can be configured for up to 10 minutes' duration.

AWS Lambda also offers Lambda@Edge, which permits functions to be deployed to CloudFront (CDN) edge locations around the world.

Azure Functions are organized into Azure Function Apps, which can contain multiple functions. This allows you to organize related functions and, if you wish, update all functions with a single publish action. In AWS Lambda, you can tag functions to group them.

Azure Functions have CI features allowing them to be deployed from GitHub, BitBucket, Dropbox, , and Azure DevOps. AWS Lambda Functions have CI features that leverage AWS' CodePipeline, CodeCommit, and CodeBuild services (CodeCommit can use GitHub or S3 as sources).

Although both platforms strive to support developers and platforms of all kinds, if you're a Microsoft developer you'll find some nice things in Azure Functions such as the ability to debug functions remotely from within Visual Studio.

Hello World in Azure Functions

Let's create an Azure Function so we can see all this goodness in action on Microsoft Azure.

Step 1: Create a Function app in the Azure Portal.

Ensure you have a subscription, access the Azure Portal, and start creation of a function.

A. Sign up for a free Azure account if you don't already have one.
B. Sign in to the Azure Portal.
C. At top left, click Create a Resource.
D. Select Compute > Function App
E. Select the Author from scratch option.

Step 2: Provision your Function App

Create a Function.

A. Set a name for your function app. I used davidpallmann-hello. As you enter your name, you'll notice the full URL path will add .azurewebsites.net, and must be unique.
B. Select your operating system.I'll use Windows.
C. Choose a location near you. I'll use Central US.
D. Choose a Runtime Stack (platform/language). I'll use JavaScript.
E. You can default the remaining options.
F. Click Create.

Azure Funtion App Create Dialog

G. Wait for a portal notification to arrive that your function has been provisioned.

Provisoning Complete Portal Notification

H. Finally, locate your function in the portal. It should now be listed under the Azure Functions area.

Newly-Provisioned Function in Azure Portal

3. Write Your Function App

Write the code of your function.

A. Viewing your function in the portal, click the + button next to Functions in the outline at left. You'll now be given a choice as to where you want to author your code. Choices include using Microsoft's Visual Studio Code editor, some other editor of your choice, or writing code in the portal. We'll choose the In-Portal option and click Continue,

Selecting Code Editor

B. Next, you're given a choice of how your function will be invoked. We want this simple function to respond to HTTP traffic and be testable in a browser, so we'll choose Webhook + API, and click Create.

C. You're now taken to your function's code editing area in the portal, where a starter function has been written for us in JavaScript. The code given to us checks whether there is a query string parameter called name. If there is, it says 'Hello ' + name. If not, it prompts us to pass name. Note this is written to work with both GET and POST requests: the name may be passed as a URL argument or in the body of the HTTP request.

Default JavaScript Function Code

D. If you've made any changes to the code, click Save to save your changes.

4. Test Your Function

Test your function by invoking it.

A. To give yourself more room, collapse the black Azure services outline at left.

B. On the right, click on Test to open the Test Panel.

C. Note you can control the request type (GET or POST). We'll leave it set to POST.

D. Note you can edit the Request body. The default sets the name to "Azure". I'll change it to my name, David.

E. Click Run. The function is invoked, and you see the results in the Output area at bottom right. You've just run your first Azure function!

Testing Function in Portal

F. Now we'd like to test outside of the portal, in a browser. In the portal, click the </> Get function URL link to view your function's HTTP URL and copy it to the clipboard. This will give you a link similar to the following:

https://davidpallmann-hello.azurewebsites.net/api/HttpTrigger1?code=BXQPobZ5euQJATM3d4psQz4lSZzz1ssJLjyeUXS92acshFlsaztaJg==

Perhaps this is longer than you expected; what's that code= parameter? It's simply a security mechanism: you don't necessarily want everyone in the world invoking your function.

G. Enter the URL you copied in Step F. It responds with:

Please pass a name on the query string or in the request body

H. Mess up the code value in the URL and try again. This time, an HTTP 401 results.

I. Now, go back to the original URL from Step G and add &name= to the end. This time, the function sees the name parameter and greets you:

Hello Stranger

Testing Function in Browser with Name Parameter

Congratulations, you've now created and tested your first Azure Function and seen how easy it is. From here, you can explore creating real functions that do useful things, different ways to trigger functions, different ways of authoring function code, and monitoring your function.

For the official Microsoft Azure tutorial, see Create your first function in the Azure portal for detailed steps. Their example uses .NET (C#) rather than JavaScript but is otherwise similar.

Hello World in AWS Lambda

Let's now also create Hello World in AWS Lambda.

Step 1: Create a Function app in the AWS Console.

Ensure you have a subscription, access the AWS Console, and start creation of a function.

A. Sign up for a free AWS account if you don't already have one.
B. Sign in to the AWS Console.
C. Navigate to the AWS Lambda service area of the console.
D. Click Create Function.

Step 2: Provision your Function App

Configure a Function.

A. Set a name for your function app, such as yourname-hello.. I used davidpallmann-hello.
B. Select a Runtime. I selected Node.js 8.10, which means I'll be coding in JavaScript.
C .For Role, choose Create a a new role from one or more templates.
D. For Role Name, enter any name you like. I entered hello-role.
E. You can default the remaining options.
F. Click Create Function.

AWS Lambda Create Function Dialog

Step 3: Write Your Function

Configure and code your function.

A. Your function now looks like this in the console:

AWS Lamda Function in Console after Creation 

B. In the Designer panel under Add Triggers, select API Gateway. We are adding this trigger so our function can be invoked via an HTTP request. An API Gateway item appears in the Designer panel.

C. Below the Designer panelj, under Configure triggers, leave the default selection for Create a new API in place.

D. Under Security,  select Open (note that we could choose to require an API Key, or could associate the API with an IAM Role if we wished).

E. Click the Add button. The API Gateway connection to your function is now configured, and the API name is shown (davidpallmann-hello-API in my case).

F. In the Designer panel, click on the name of your function (davidpallmann-hello in my case). A Function code panel should now be visible showing your code.

G. Click Save

4. Test Your Function

Test your function by invoking it.

A. To test your function in the console, click Test. A dialog appears for configuring a test event..

Configure Test Event Dialog

B. On the Configure Test dialog, enter a name for the event such as HelloTest1.

C. Click Create. Your test event is created.

D. Again click Test on the console. This time, test results are shown. Expand and inspect.


Test Results

E. We've now seen the function work within the console, but we also want to see it work from a browser. But there's another thing: the current code is too simple: it only outputs a message. We'd like our function to be able to retreieve a query parameter as incoming data, so we will make some changes to the code.

Click the function name in the console so the code panel is visible, and replace the existing code with the code below. This new code will change its message based on whether or not a parameter named name is present.

exports.handler = async (event) => {

    var message = "Hello. Tell me your name by adding a ?name= parameter to the URL";
    if (event.queryStringParameters && event.queryStringParameters.name)
    {
            message = "Hello, " + event.queryStringParameters.name;
    }
    
    const response = {
        statusCode: 200,
        body: JSON.stringify(message),
    };
    return response;
};

F. In the console, click the API Gateway area at top  to see the API Gateway panel. Note the API Endpoint and copy it to your clipboard. It will be a URL with a format similar to this:

https://6rgtclt94i.execute-api.us-east-1.amazonaws.com/default/davidpallmann-hello

Viewing API Endpoint

G. In another browser tab, paste the URL from Step E. You see the service respond.

"Hello. Tell me your name by adding a ?name= parameter to the URL"



Testing Function in Browser
H. Now, add a ?name= parameter to the end of the URL. This time, we get a different response, showing the function saw an input parameter.
"Hello, Handsome"

Testing in Browser with a URL Parameter

Congratulations, you've now created and tested your first AWS Lambda Function and seen how easy it is. From here, you can explore creating real functions that do useful things, different ways to trigger functions, different ways of authoring function code, and monitoring your functions with AWS CloudWatch.

In Conclusion

Serverless Computing is a real phenomenon worth looking into. There has never been an easier or more effective way to get maximum reuse from your code. Cloud platforms have spared no effort to let you connect functions to, well, anything. You only pay for functions when they are used, giving them a superior payment model to always-allocated application servers. If you're a developer who has been using PaaS cloud services because they let you focus on your application instead of infrastructure details, you should look at FaaS to take that idea even further.










Wednesday, January 23, 2019

10 SaaS Essentials: #2 Highly Available

In this series I am discussing 10 essential characteristics of good Software-as-a-Service applications. In this second post I cover Essential #2: Highly Available, along with some thoughts on how to achieve High Availability (HA) on Microsoft Azure or Amazon Web Services. I'll be discussing specific features and cloud services of both AWS and Azure in this series, as I've implemented SaaS solutions on both platforms.

#2 Highly Available

Essential #2 is being Highly Available. If you have paying customers for your SaaS, odds are you're likely providing a vital service for them. Whether you're providing collaboration services or sales team services or HR services or legal services or developer services or legal services, your customers will want your service to be available any time they need it. That might be business hours for some, or 24 x 7 for others.


That being said, not all SaaS applications will have the same availability requirements. You should consider what needs to happen when a required cloud service is temporarily unavailable. Your options range from your app being temporarily unavailable (which might be acceptable for some applications) to some kind of alternative processing all the way to re-routing users to another deployment of your application in another region. Think through what your availability needs are and whether you're willing to invest the time, effort, and additional costs associated with some of the remedies we'll be discussing.

Before we get to the technical considerations of high availability, we need to discuss what availability means in terms of SLAs.

Setting Expectations with Availability Targets

What availability can you expect to provide, and what expectations should you set with prospects and clients? That's both an important question and a scary one: since your SaaS is highly dependent on a cloud platform that you don't control, what assurances can you provide that won't backfire on you? To answer this question, begin with the assurances your cloud platform gives. Uptime commitments for cloud providers are commonly expressed as a Service Level Agreement (SLA), in which you'll find availability targets. Understand that availability targets are merely objectives. They are not guarantees.

Understanding the Nines

Availability is usually quoted as a percentage of uptime: For example, 99.99% uptime/month ("4 nines") means you can expect 4.38 minutes of downtime a month, which works out to 52 minutes of downtime per year. Compare that to the similar-sounding 99.95%, which is 21.92 minutes of downtime a month, 4.38 hours a year. Here's a comparison of availability targets to put things in perspective.

Availability %
Also Known As
Downtime per Month
Downtime per Year
99.999%
5 nines
26.3 seconds
5.26 minutes
99.99%
4 nines
4.38 minutes
52.6 minutes
99.95%

21.92 minutes
4.38 hours
99.9%
3 nines
43.83 minutes
8.77 hours
99%
2 nines
7.31 hours
3.65 days


Cloud Service SLAs

You can view the AWS Service Level Agreements and Azure Service Level Agreements online. Let's take a look at one. At the time of this writing, the AWS Compute Service Level Agreement (covering EC2 and several other services) commits to a monthly uptime target of 4 nines, 99.99%. Note, however, the fine print: there's a specific formula for how availability is measured. AWS will take "commercially reasonable efforts" to meet their availability target, not heroic measures. Other services you use will have their own separate SLA agreements, which might have lower availability. For example, AWS S3 is 99.9% (3 nines) as of this writing. The cloud providers' remedy for not meeting their target is merely a partial refund of your monthly bill, but the damages your clients might incur if your service is unavailable could be far higher. In formulating your own SLA, it's a good idea to clearly state the obligations and remedies assumed by your cloud platform, and then the obligations and remedies your company assumes.

Although it's helpful to know the expected availability of cloud services, that in itself tells you little about the uptime you can expect from your solution in the cloud. There are two reasons for that. The first is that you almost certainly consume multiple cloud services together, so you must calcualte a composite SLA. The second is that your application's architecture and implementation could compromise that SLA.

Composite SLAs

Let's use the term cloud workload to mean something you have running in the cloud that relies on a collection of services. That is, if even one of those cloud services isn't available then your workload can't run. In that case, your availability is going to be the arithmetic product of the SLAs of the individual services.

For example, let's say you have a web site hosted in EC2 that relies on an RDS Oracle database and also the S3 Storage Service. You review the latest SLAs, and find that EC2 has expected availability of 99.99%,  while RDS Oracle is 99.95%, and S3 is 99.9%. The composite SLA would be 

99.99% x 99.95% x 99.9% = 99.84%

Using an availability calculator, that's downtime of  1.17 hours a month or 14.03 hours a year.

On the other hand, if your application is written in such as way that it can get by without some cloud services when they aren't available, you can remove those services from the calculation. In the prior example, if the app is able to run just fine when S3 is down, the calculation becomes 99.99% x 99.95% = 99.94%. That's downtime of 26.30 minutes a month or 5.26 hours per year—much improved.

The composite SLA can be sobering, but it's important to have a realistic view of availability. Don't make the mistake of touting one cloud service's availability as your SaaS's overall availability: it won't be.

Not Compromising the SLA: Resiliency

Once you have an informed view of what availability to expect from the arrangement of cloud services you're using, you must now ensure your application doesn't compromse the SLA. This can be hard to do, but it's vital. One of the most important things you can do is make your application resilient to the transient failures that come with the cloud.

If your code simply fails when a needed resource isn't available, it isn't going to fare well in the cloud. This is especially true of legacy code, often written for an enterprise environment where there is a reasonable expectation that everything is available during business hours. If such code doesn't handle failures well, moving it to the cloud can seem okay at first—but the first time a needed resource isn't available what will be the effect on your application? It could result in data loss or perhaps even an improperly executed business transaction. A better coding strategy in cloud-hosted applications is to anticipate and proactively handle tempory unavailability.

There's a valuable article on resiliency in the Microsoft Azure documentation. Let's consider some of the recommended patterns for cloud application resiliency.

Retry

Your code usually has no problem connecting to your database, but this time it fails: the database service has become unavailable. Applying the retry pattern, your code could attempt retries before giving up on the idea of accessing the database. You would do this in a loop, with a delay between attempts. You don't want to overdo it, and under no circumstances should this be an infinite loop. On a successful connection you exit the loop and get back to work. If you complete the loop without a successful connection, it's time to either fail gracefully or consider one of the other patterns such as a fallback path.


This same approach can be applied to any cloud service you use, not just databases. Before implementing, check whether your API already performs retries; if it does, there's no reason to add redundant retry code. Many of the Cloud APIs not only perform retries but also implement exponential backoff algorithms.

Circuit Breaker

The retry pattern is a good technique when trying to connect to a cloud service, but what should we do if we zoom out to the level where a complete business process is invoked? The circuit breaker pattern, like a real-world house circuit breaker, trips when a certain threshold has been reached. When a business process is seen to be failing, the circuit breaker code around it can switch to an alternative processing path or make the process unavailable—but it should also periodically re-try the process code to see whether things are working again, in which case the process can be switched back on.

Bulkhead

Temporary cloud service unavailability is sometimes worsened by code that uses the same client connection to access multiple resources of the same type. For example, you use the same web client to talk to multiple web services, or you use the same database client to talk to multiple partitions. When that's the case, it's possible an error such as an exhausted connection pool will compromise your client connection as it tries to deal with an availability problem—at the same time also denying you access to those other resources which may in fact be ready and available.

On a ship, bulkheads are the partitioned compartments that prevent a hull breach from flooding the entire ship. The bulkhead pattern says you should not only logically group your cloud resources into functional areas (like related web services or database partitions or storage accounts) but also make separate client connections to them. That way, a failure connecting to your shipping services won't also derail a connection to your wareheouse services.


Fallback Paths

One choice you have when a cloud service isn't available is to use a different cloud service and/or to defer the processing for another time. For example, you can handle the temporary unavailability of a database by writing transactions to a storage queue, where they can be processed later when the database is available.


Of course, not all activities require this level of resiliency. For some activities a try again later message to the user may suffice—for your SaaS, you'll have to make the call. The important thing is proactively planning how you will handle unavailability of the cloud services you make use of.

Graceful Degradation

It may be that your app simply doesn't have a good path forward because one or more cloud resources aren't available at the moment. Even in this case, there's a big difference in user experience between apps that simply give up vs. apps that strive for graceful degradation.

Graceful degradation means providing whatever limited functionality you can under failing circumstances: it's the difference between a bumpy landing and a crash landing to your users. If possible, preserve what information you can—perhaps the user has filled out a very large form—somewhere for later processing. It might mean displaying alternative guidance, such as a manual procedure the user can follow for their task. It might be simply putting up an error message, but in a friendly, reassuring way.

High Availability through Redundancy

Let's now talk about the arrangement of your software in the cloud, and what is needed to maintain high availability.

Avoid Single Points of Failure

While cloud platforms contain amazingly smart infrastructure, the hardware utilized is chosen to keep costs low. That means, at any given time, the server that hosts your web server VM could fail; or the database server hosting your database VM could fail; and so on. If you only have one of these components, you're down. But if there are multiple instances, with an ability to recover what was lost, your users don't even need to know that anything has happened.

High Availability for Web Farms

To make your web tier highly-available, you should run multiple instances in a web farm fronted by a load balancer. The minimum number of instances you want is 2: that way, a failure of one server instance won't kill off your application because the other instance will still be alive. If you use a PaaS service (which I highly recommend) like Azure Cloud Service or AWS Elastic Beanstalk, your lost instance will be automatically re-created: the farm will maintain the minimum instance count your configure.

Of course, a failure might be larger than just a single server. Your cloud region (data center) is further divided into smaller logical data centers, called availability zones. Services like Azure Cloud Services and AWS Beanstalk can be configured to deploy your instances across multiple availability zones. This allows your SaaS to remain available even if an entire availability zone fails.

Web Server HA in a Region

Thinking more extremely, It's also possible for an entire data center to become unavailable. If you're needing to manage even this condition, you'll need to deploy your application to multiple regions around the world. You can then use services like Azure Traffic Manager or AWS Route 53 to distribute web traffic to the most appropriate data center, based on availability and other criteria such as user location. You are of course paying significantly more for this level of redundancy. There are different ways to utilize multiple data centers: you could choose to operate a primary region with a secondary only used for failover; or you could have two or more regions in regular operation because your clientele are located in different geographies.

Site HA Across Regions

It's important for the web software to cooperate with the cloud HA model in terms of state. The ideal fit for the cloud's model for high availability is stateless web server software, meaning any kind of session state or global state information is stored in an external resource such as a distributed memory cache or database. As the load balancer distributes traffic across multiple instances, user session context is maintained—even if the cloud needs to reallocate a failed instance or increase/decrease the number of instances in order to auto-scale.

Legacy software that originally targeted a single server and keeps state in memory won't work in this model. One possible option in this case is to use a load balancer configured for server affinity ("sticky sessions"), which will continually route each client to the same instance. This remedy is not ideal, because cloud instances can always come and go. A better strategy is to update the software to store state in a distributed memory cache (such as Redis on AWS ElastiCache / Azure Cache) or cloud database (such as AWS DynamoDB, Azure CosmoDB, or your relational cloud database).

High Availability for Databases

If you're using a PaaS database service (highly recommended) for a relational database such as Azure SQL Database or Amazon RDS, those services will allocate multiple servers with automatic failover. The standby instances will be kept up-to-date as the primary is updated. In the event of a failure of the primary, a standby instance takes over. However, to span multiple availability zones, you'll need to configure your cloud service for multi-zone availability which will likely cost more. Your cloud service may also offer features for geo-replication which you should investigate.

What about globally available database solutions? There are various solutions and patterns for this on Azure and AWS for different databases. In general, adapting a traditional relational database to be global is going to require some work involving replication and synchronization, and if you're going in this direction you'll need to follow guidance from your cloud provider specific to that database and carefully consider the related costs and performance characteristics. You're strongly advised to follow the prescription for a proven global database pattern rather than inventing your own solution.

If you're really intent on global presence for your database, you're advised to consider using one of the newer cloud databases specifically designed for global scale. These include NoSQL databases like Azure's Cosmos DB and AWS's DynamoDB as well as relational databases like Amazon Aurora Global Database. There's a great deal of ongoing innovation by cloud providers around reinventing databases for high availability and scale. With these databases, there's significantly less work for you to do in configuring and operating them and there are fewer moving parts exposed to you. Many of them offer excellent performance and 4 9's availability (or better). Whether one of these databases is a good fit for you and the kinds of queries you need to perform is of course an individual consideration you'll have to research.

High Availability for your Services Tier

For traditional HTTP/HTTPS/TCP web services, you can follow the earlier web server guidance for high availability: configure at least 2 instances in a load-balanced farm, across multiple availability zones. If your services take their tasks from a queue or database, you can likewise configure a worker farm of multiple instances across multiple availability zones.

There is another kind of service, however, that takes advantage of the new server-less computing capabilities cloud platforms are now offering. Function-as-a-Service services like AWS Lambda and Azure Functions are frequently used for Microservices and provide built-in high availability. There are no pre-allocated servers to fail with your serverless function: the cloud platform will allocate servers as needed in response to demand. Availability comes out-of-the-box with server-less computing.

High Availability for Single-Instance Software

We've spent much of this post attesting to redundancy as a necessary ingredient in providing high availability, but what if there's a component of your solution that isn't capable of running on multiple instances? That might be something like a background service or a web service. The answer to this question lies in whether the component is required to always be available.

If a single-instance component does not need to be continually available, it can remain single instance. For example, a background service that only runs once overnight does not need to always be available. In that case, loss of the instance due to hardware failure would be replaced by the cloud infrastructure before long, the component would resume, and all would be well. The same would be true for any kind of processing worker that takes work from a non-interactive source such as a queue or database.

However, if a single-instance component must be available in order for the rest of the solution to function that is another story. For example, the temporary loss of a web service that processes payments could cause significant disruption. In this case, the component is a single point of failure and compromises high availability. In order to realize high availability, the service must be modified so that it can run multi-instance.

Test Availability—Or Your Customers Will!

High availability needs to be addressed for every tier of your SaaS application. When you think you have an HA architecture in place, you should prove it out—with some "sabotage": every recovery mechanism you think is in place needs to be proven out. For example, deallocate web server instances while the solution is being used: no one should be disrupted. If you've made your application resilient against temporary unavailability of a cloud service, re-configure that cloud service so that it is no longer accessible: did your planned graceful handling work as intended? If you've deployed to multiple regions with traffic management and failover, shut down your site in a region and observe what happens to users.

Monitor Availability

Cloud platforms offer some superb tools and services for achieving high availability. Nevertheless, it's easy to overlook a software component or fail to configure things correctly. Be sure to monitor operations and pay attention to your application's actual availability.

In Conclusion

Availability: it's being there for your clientele. Eliminate single points of failure and think horizontal in your application architecture: redundancy is your greatest tool for high availability in the cloud. Make your instances stateless, and code for resiliency. Above all, take advantage of the newer cloud services where high availability features are built in and automatic. Operations monitoring is essential.

Tuesday, January 22, 2019

Ghost Game, Part 2: Voice-Enabling with Alexa

Note: If you don't see proper syntax highlighting of code, change https to http in URL.

I few weeks ago, I wrote about a father-daugher project in which I and my college student daughter Susan collaborated on a web site implementing the game of GHOST. This is one of those games your family might have played when you were in the car or sitting around the table or a campfire: you say letters in turn until somebody makes a word, and then they lose the round. We'd next planned to have her design a user experience during Christmas break, but we didn't get around to it. This month, I got the idea to voice-enable Ghost for Amazon Alexa (accessible on the Echo, Echo Dot, FireStick, the Alexa App on phones, and lots of other places).

I had been looking for an excuse to create a voice-assistant application for Alexa, and decided a speech front-end for GHOST would be the perfect project since the back-end already existed. This post describes how I went about creating my Alexa Skill (as they're called), and how it turned out. I have not published my game--it isn't fully debugged yet, and there is already a Ghost Game skill on the Alexa marketplace (not mine). My main purpose was learning how to do an Alexa Skill, and that's what I accomplished.

The End Result

Before we go into how I created the skill, let's take a look at the end result. Below are some audio clips of actual gameplay on Alexa. Take a listen.

Session 1: Human makes word M-E-M-O-R-Y
Session 2: Computer bluffs, challenged by human. Human bluffs, challenged by computer.
Session 3: Human makes a word game doesn't know, teaches it a new word.
Session 4: Full game

Here's a visual rendering of a dialog with the game (bolded words spoken by human).

Human: Alexa, open Ghost David.
Alexa: Welcome to Ghost. To hear the rules, say help. I'll start. "M".
Human: "E"
Alexa; "M"
Human: "O"
Alexa: "R."
Human: "Y"
Alexa: "Memory. That's a word! I win. Youre a G. Let's start a new game. You go first."

Human: "T"
Alexa: "A"
Human: "R"
Alexa: "I challenge you. What's your word?"
Human: "My word was 'tarnish'."
Alexa: I see. You win, and I'm a G. Congratulations. Should I add this word to my dictionary? Tarnish. T-A-R-N-I-S-H
Human: Add Word
Alexa: Thanks. I have learned the word 'tarnish'.
Alexa: Let's start a new game. You can start.

Adapting a Web Site to be an Alexa Skill

Our GHOST web site had a pretty simple user interface: the word-in-progress letters were displayed. Users could type a letter to take a turn, and also had a few buttons they could click such as I Challenge You, That's a Word, and New Game. This should translate pretty readily to spoken phrases:

  • Instead of typing a letter to take a turn, the user will speak a letter: "C"
  • Instead of clicking a That's a Word button, the user will say "That's a word."
  • Instead of clicking the I Challenge You button, the user can say "I challenge you."
  • Instead of clicking the New Game button, the user can say "New Game."
Since this is a spoken interface, we'll also add some new action phrases:
  • If the user forgets what the current word is, they can ask "What is word?"
  • If the user forgets the rules for Ghost, they can say "help" or "What are the rules?"
And, when the computer challenge the human, we'll need to process their response:
  • They state their word: "My word was tofu."
  • They admit they were bluffing: "I was bluffing" or "you got me."

Architecture

In terms of the components we need to create, an Alexa Custom Skill usually has two (or more) parts:
  1. There's the skill defnition, which you create and configure in the Amazon Developer portal. This includes:
    • invocation name: official name of skill used to open it.
    • utterances: speech patterns to recognize.
    • intents: groups of utterances that express the same intended action.
    • slots: variables of various types that can occur in intents. You can define your own custom slot types by providing a list of values--such as letters of the alphabet.
  2. A cloud service, usuallly implemented as an AWS Lambda Function (a server-less computing component). This can be written in a variety of languages, including Node.js (JavaScript) or Python. 
  3. Lastly, your cloud service may in turn need to talk to something in the outside world, such as an API or service. For us, that's the existing back-end server functions of the Ghost web site.
In the case of Ghost, these components are:
  1. A skill definition named Ghost, invoked with the name "Ghost David".
  2. A Node.js AWS Lambda function. This function has to do the same things the Ghost web site's front-end does: implement the game logic and make calls to the back end server. The JavaScript logic that was originally in the Ghost web site's front end is replicated here. 
  3. The original Ghost web site's server will respond to the Lambda function, providing functionality such as playing a turn; checking the validity of a word; responding to a player challenge; and adding new words. The server queries and updates the word list SQL Server database. The Lambda function will be calling actions like /Home/Play/word, /Home/ValidWord/word, and /Home/Challenge/word.
Architecture: Alexa and GHOST Web Site Share a Common Back-End

This entire skill was written in Amazon portal interfaces: the Skill definition in the developer portal, and the JavaScript code for the Lambda Function in the AWS portal. No desktop tools required!

Skill Definition

A skill definition is configured in the Amazon developer portal. In the Intents section you specify intended user actions. Some of the intents for Ghost are "My Letter Is...", "I Challenge You", and "That's a Word".

Skills Developer Portal

For each intent, you can specify a collection of utterances. This allows you to cover variations in what a user might say. Your utterances can contain variable placeholder such as {Letter}, called slots. A slot is passsed to the Lambda Function. Thus, when someone says "My letter is B" to Ghost, that utterance will be recognized as intent MyLetterIsIntent. That will be passed to the back end with slot Letter set to "B". The back end will then have the opportunity to exercise logic and decide on a suitable response.

Intent

These slots can be of different types. There are many pre-defined slot types, such as a date or city. However, you can also define your own custom slots, where you provide a list of values. That is what was done for the Letter slot. The values define include not only A-Z, but also Alpha-Zulu: the NATO alphabet, a useful fallback if Alexa can't understand a single letter. Thus, I can say "B" or "Bravo": both will be understood as B by the Lambda Function.

Slot Values

On the Endpoint page, the skill is linked to the AWS Lambda Function. The Lambda function's ARN ID is stored in the skill's configuration. You can also associate the skill ID in the Lambda function's configuration which is recommended.

Skill Endpoint 

Lambda Function

The Lambda Function has functions for announcing a welcome message, and for responding to intents. Let's look at some of them.

The method getWelcomeResponse provides the initial spoken prompts when the game is started. We also create a structure of session variables here: we will need to maintain state throughout the sesson in order to track the word-in-play, who went first, and player scores. In this function the computer also takes the first turn, so there is an HTTP call to the server to get a letter to play. The letter is included in the spoken response. A callback passes the response back to the skill, and the user hears something like the following (the initial letter is random each time):

"Welcome to Ghost. To hear the rules, say help. I'll start. P."
// --------------- Functions that control the skill's behavior -----------------------
// getWelcomeResponse: intiial greeting to user. Initialize session attributes. Make first turn.

function getWelcomeResponse(callback) {
    // If we wanted to initialize the session to have some attributes we could add those here.
    let sessionAttributes = {
        inChallenge: false,  // true if in a challenge.
        gameInSession: true, // true if a game is in-progress.
        turn: true,          // true = human player turn, false = computer player turn
        word: '',            // current partial word in play.
        saveWord: '',        // saved word before last player move
        startPlayer: "0",    // 0: computer player starts, 1: human player starts.
        humanGhost: 0,       // human player's loss count toward being a G-H-O-S-T.
        computerGhost: 0,    // computer player's loss count toward being a G-H-O-S-T.
        unrecognizedLetterCount: 0, // number of times we've failed to understand the human's spoken letter.
        newWord: ''          // new word we learned from human
    };
    const cardTitle = 'Welcome';
    sessionAttributes.startPlayer = "0"; // 0: computer goes first, 1: human goes first
    var speechOutput = 'Welcome to Ghost. To hear the rules, say help. I\'ll start. ';
    var repromptText = 'Say a letter';
    var shouldEndSession = false;
    
     httpGet('Home/Play/' + sessionAttributes.word + '?sp=' + sessionAttributes.startPlayer,  (letter) => { 
         letter = trimLetter(letter);
         if (letter != null) {
             speechOutput = speechOutput + '"' + letter + '".';
             sessionAttributes.word = sessionAttributes.word + letter;
        }

        callback(sessionAttributes,
            buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
     });
}

One important method is onIntent, whose purpose is to invoke a handler function for an intent based its name. For example, the intent type MyLetterIsIntent is handled by the function humanPlay.
/**
 * Called when the user specifies an intent for this skill.
 */
function onIntent(intentRequest, session, callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);

    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;

    // Dispatch to your skill's intent handlers
    if (intentName === 'MyLetterIsIntent') {
        humanPlay(intent, session, callback);
    } else if (intentName === 'WhatIsWordIntent') {
        whatIsWord(intent, session, callback);
    } else if (intentName === 'ThatsAWordIntent') {
        thatsAWord(intent, session, callback);
    } else if (intentName==='NewGameIntent') {
        newGame(intent, session, callback);
    } else if (intentName==='IChallengeYouIntent') {
        humanChallenge(intent, session, callback);
    } else if (intentName==='MyWordIsIntent') {
        humanMyWordIs(intent, session, callback);
    } else if (intentName==='UndoIntent') {
        undoIntent(intent, session, callback);
    } else if (intentName==='BluffingIntent') {
        humanBluffing(intent, session, callback);
    } else if (intentName==='AddNewWordIntent') {
        addNewWord(intent, session, callback);
    } else if (intentName==='HelpIntent') {
        helpIntent(intent, session, callback);
    } else if (intentName==='DebugIntent') {
        debugIntent(intent, session, callback);
    } else if (intentName === 'AMAZON.HelpIntent') {
        getWelcomeResponse(callback);
    } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
        handleSessionEndRequest(callback);
    } else if (intentName === 'AMAZON.FallbackIntent') {
         handleFallbackIntent(intent, session, callback)
    } else {
        console.log('intentName: ' + intentName);
        throw new Error('Invalid intent');
    }
}

the humanPlay function is one of the larger functions. It handles the human game play, where the human has spoken a letter. The letter can come in a few different ways, including a NATO alphabet word, so a function named trimLetter is employed to identify the letter and return a value like "A", "B", etc. The letter is added to the current word, and then evaluated.

If the user has made a valid word, determined by asking the server to check its word list, then the human has lost. Their GHOST count is incremented, and they are told they are now a G, or a G-H, etc. A new round starts. 

        "Stone. That's a word. I win! Let's start a new game. You go first."

Otherwise, the computer attempts to play, by passing the current word to the server and asking it to play. If the response is a letter, it is played (spoken) and added to the current word. If however the server responds with "?", a challenge is issued.
// -------------------- Human Plays Letter --------------------
// A letter was played. Set letter in session and prepares speech to reply to the user.

function humanPlay(intent, session, callback) {
    const cardTitle = intent.name;
    const letterSlot = intent.slots.Letter;
    let sessionAttributes = session.attributes;

    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';

    sessionAttributes.saveWord = sessionAttributes.word;
    console.log('humanPlay 02 saveWord: ' + sessionAttributes.saveWord);

    if (!sessionAttributes.gameInSession) {
        speechOutput = "To start a new game, say 'New Game'.";
        callback(sessionAttributes,
             buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
        return;
    }

    if (sessionAttributes.inChallenge) {
        speechOutput = "Please respond to the challenge. Either tell me what your word was, or admit you were bluffing. Or say 'New Game' to start a new game..";
        callback(sessionAttributes,
             buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
        return;
    }

    if (letterSlot) {
        var letter = trimLetter(letterSlot.value);
        
        if (letter===null) {
            speechOutput = "I'm not sure what your letter is.";
            sessionAttributes.unrecognizedLetterCount++;
            if (sessionAttributes.unrecognizedLetterCount==2) 
                speechOutput += " Please try again, or say a word from the NATO alphabet like Alpha or Bravo.";
            else
                speechOutput += " Please try again.";
            callback(sessionAttributes,
                 buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
             return;
        }
        else {
            console.log('Human played: ' + letter + ' | current word: ' + sessionAttributes.word);
            sessionAttributes.unrecognizedLetterCount = 0;
            //sessionAttributes = createLetterAttributes(letter);
            sessionAttributes.word = sessionAttributes.word + letter;

            // Human play - check for a completed word
            
            if (sessionAttributes.word.length >= 3) {
                httpGet('Home/ValidWord/' + sessionAttributes.word,  (res) => {
                    if (res=='true') {
                        sessionAttributes.humanGhost++;
                        speechOutput = sessionAttributes.word + ". That\'s a word! I win! You\'re a " + score(sessionAttributes.humanGhost) + ". Let's start a new game. You go first.";
                         sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
                        sessionAttributes.word = '';
                        callback(sessionAttributes,
                            buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
                        return;
                    }
                });
            }
    
            // Computer play
    
            httpGet('Home/Play/' + sessionAttributes.word + '?sp=' + sessionAttributes.startPlayer,  (res) => {
                
                res = trimLetter(res);
                     
                if (res==='?') {   // challenge
                    speechOutput = 'I challenge you! What\'s your word?';
                }
                else {
                    console.log('Computer played: ' + letter + ' | current word: ' + sessionAttributes.word);
                    speechOutput = speechOutput + '"' + res + '".';
                    sessionAttributes.word = sessionAttributes.word + res;
                }
    
                // If the user either does not reply to the welcome message or says something that is not
                // understood, they will be prompted again with this text.
    
                callback(sessionAttributes,
                    buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
                return;
             });
        }
    } else {
        speechOutput = "There is a data problem in the skill: letter slot not found.";

        callback(sessionAttributes,
             buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    }
}

humanPlay function

The user has other options besides playing a letter. The human can say "I challenge you", in which case the humanChallenge function is run. If the program had a valid word, it will spell it out and score a loss for the human. Otherwise, it will admit it was bluffing and score a loss for itself.
// -------------------- Human Challenges Computer --------------------
// Human challenges computer. Tell them our word, or admit we were bluffing.

function humanChallenge(intent, session, callback) {
    const cardTitle = intent.name;
    let sessionAttributes = session.attributes;
    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';

    httpGet('Home/Challenge/' + sessionAttributes.word,  (res) => {
        if (res==='^' || res==='"^"') {
            sessionAttributes.computerGhost++;
            speechOutput ="You got me - I was bluffing. That makes me a " + score(sessionAttributes.computerGhost) + ". Let's start a new game. You go first.";
            sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
            sessionAttributes.word = '';
            callback(sessionAttributes,
                buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
            return;
        }
        else {
            sessionAttributes.humanGhost++;
            speechOutput = "My word was " + res + ". " + spellWord(res) + ". I win! You\'re a " + score(sessionAttributes.humanGhost) + ". Let's start a new game. You go first.";
            sessionAttributes.startPlayer = "1";
            sessionAttributes.word = '';
            callback(sessionAttributes,
                buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
            return;
        }
    });

}

humanChallenge function

Test and Debug

How do you test this kind of application? From the Developer Portal, there's an Alexa Simulator; but if you have an actual device on the same account then you'll be able to call up your skill even though it hasn't been officially published.

How do you debug an Alexa Skill? Well, all you have to do is add console.log(text); statements in your Lambda Function's JavaScript. The output will be captured in CloudWatch logs. Every time you conduct an Alexa Session, there's a CloudWatch log you can inspect. These logs are also where error detail will be found if your Lambda Function has an unhandled exception. In the log excerpt below, the "Human played..." and "Computer played..." lines were added by console.log statements.

CloudWatch Logs


Post-Mortem: Rapid development, Prolonged Debugging

Ghost was my very first attempt at developing an Alexa skill. To give you some idea of how rapidly you can learn Alexa Skills, here's what I experienced in my first 3 days:

1. Saturday afternoon. I went to Best Buy and purchased an Echo Dot for $24.
2. I went through the Color Picker quickstart.  I registered as a developer with Amazon. Some of the instructions in the quickstart were hard to follow, because some of the developer site screens and AWS screens had changed since the tutorial was written. But I stayed with it, and in a couple of hours I was able to say "Open Color Picker" to my Echo Dot and tell it my favorite color. I chose the Node.js version (JavaScript), but there are also versions of this quickstart where the Lambda Function is written in other languages such as Python.
3. Now feeling empowered, I worked on a derivative of Color Picker that would search my blog. To achieve that, I had to look up how to do an HTTP request from a Node.js Lambda function. In another couple of hours, I had this project working. I now felt ready and empowered to do something real.
4. On Sunday, I started work on my Ghost Alexa skill, again using Color Picker as my starting point.
5. By end of day Monday, I was finished with the basic implementation: I could play Ghost with my Echo Dot. All that remains is thorough testing and debug.

If you're going to work on an Alexa Skill of any depth, you're likely going to find like me that initial development goes pretty quickly, but you can expect to spend quite a bit of time on test and debug before your skill is ready for prime time. There are a number of reasons for this, chief of which is the surprise factor: the user may not say things you expect them to, or what they say may be misunderstood. It took some time to learn the nuances of  effectively communicating this way (you might call this "elastic communication"). If you want your Alexa Skill to be smarter than a voicemail menu, then you'll need to invest some effort--but it will be worth it in the usability of what you end up with.  All in all, I'm pleased with how quickly I became functional in this new area.

Will I publish Ghost? Perhaps, after I finish debugging and refining it.

Source Code

Below is the full source code to the Alexa Skill and the Node.js Lambda Function.

Alexa Skill

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "ghost david",
            "intents": [
                {
                    "name": "AMAZON.FallbackIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": [
                        "exit game",
                        "exit",
                        "stop game",
                        "quit game",
                        "stop",
                        "quit",
                        "end game"
                    ]
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "MyLetterIsIntent",
                    "slots": [
                        {
                            "name": "Letter",
                            "type": "LIST_OF_LETTERS"
                        }
                    ],
                    "samples": [
                        "Let's try {Letter}",
                        "{Letter}",
                        "My letter is {Letter}"
                    ]
                },
                {
                    "name": "WhatIsWordIntent",
                    "slots": [],
                    "samples": [
                        "What is the current word",
                        "What is current word",
                        "what's the word again",
                        "what's the word so far",
                        "tell me word",
                        "What's the current word",
                        "What's the word",
                        "Say word",
                        "What is the word",
                        "What is word"
                    ]
                },
                {
                    "name": "ThatsAWordIntent",
                    "slots": [],
                    "samples": [
                        "You lose",
                        "That is a word",
                        "I win",
                        "You made a word",
                        "Tha'ts a word"
                    ]
                },
                {
                    "name": "NewGameIntent",
                    "slots": [],
                    "samples": [
                        "redo",
                        "start over",
                        "start new game",
                        "new game"
                    ]
                },
                {
                    "name": "IChallengeYouIntent",
                    "slots": [],
                    "samples": [
                        "challenge",
                        "Tell me your word",
                        "What is your word",
                        "I give up",
                        "I challenge you"
                    ]
                },
                {
                    "name": "MyWordIsIntent",
                    "slots": [
                        {
                            "name": "ClaimedWord",
                            "type": "ANY_WORD"
                        }
                    ],
                    "samples": [
                        "My word was {ClaimedWord}",
                        "My word is {ClaimedWord}"
                    ]
                },
                {
                    "name": "DebugIntent",
                    "slots": [],
                    "samples": [
                        "log debug",
                        "log variables",
                        "debug log"
                    ]
                },
                {
                    "name": "UndoIntent",
                    "slots": [],
                    "samples": [
                        "do over",
                        "take it back",
                        "undo my letter",
                        "change my letter",
                        "change my last turn",
                        "backspace",
                        "undo",
                        "undo my last turn"
                    ]
                },
                {
                    "name": "HelpIntent",
                    "slots": [],
                    "samples": [
                        "i need the rule",
                        "explain",
                        "what do I do now",
                        "what do I do",
                        "what are the rules",
                        "what commands do you know",
                        "help"
                    ]
                },
                {
                    "name": "BluffingIntent",
                    "slots": [],
                    "samples": [
                        "busted",
                        "Oops",
                        "I have no word",
                        "I had no word",
                        "You caught me I was bluffing",
                        "You caught me",
                        "I didn't have a word",
                        "You got me I was bluffing",
                        "You got me",
                        "I was bluffing"
                    ]
                },
                {
                    "name": "AddNewWordIntent",
                    "slots": [],
                    "samples": [
                        "add word",
                        "add new word",
                        "yes add new word",
                        "yes add word",
                        "yes"
                    ]
                }
            ],
            "types": [
                {
                    "name": "LIST_OF_LETTERS",
                    "values": [
                        {
                            "name": {
                                "value": "zulu"
                            }
                        },
                        {
                            "name": {
                                "value": "yankee"
                            }
                        },
                        {
                            "name": {
                                "value": "x-ray"
                            }
                        },
                        {
                            "name": {
                                "value": "whiskey"
                            }
                        },
                        {
                            "name": {
                                "value": "victor"
                            }
                        },
                        {
                            "name": {
                                "value": "uniform"
                            }
                        },
                        {
                            "name": {
                                "value": "tango"
                            }
                        },
                        {
                            "name": {
                                "value": "sierra"
                            }
                        },
                        {
                            "name": {
                                "value": "romeo"
                            }
                        },
                        {
                            "name": {
                                "value": "quebec"
                            }
                        },
                        {
                            "name": {
                                "value": "papa"
                            }
                        },
                        {
                            "name": {
                                "value": "oscar"
                            }
                        },
                        {
                            "name": {
                                "value": "november"
                            }
                        },
                        {
                            "name": {
                                "value": "mike"
                            }
                        },
                        {
                            "name": {
                                "value": "lima"
                            }
                        },
                        {
                            "name": {
                                "value": "kilo"
                            }
                        },
                        {
                            "name": {
                                "value": "juliett"
                            }
                        },
                        {
                            "name": {
                                "value": "india"
                            }
                        },
                        {
                            "name": {
                                "value": "hotel"
                            }
                        },
                        {
                            "name": {
                                "value": "golf"
                            }
                        },
                        {
                            "name": {
                                "value": "foxtrot"
                            }
                        },
                        {
                            "name": {
                                "value": "echo"
                            }
                        },
                        {
                            "name": {
                                "value": "delta"
                            }
                        },
                        {
                            "name": {
                                "value": "alpha"
                            }
                        },
                        {
                            "name": {
                                "value": "charlie"
                            }
                        },
                        {
                            "name": {
                                "value": "bravo"
                            }
                        },
                        {
                            "id": "LETTER_Z",
                            "name": {
                                "value": "z",
                                "synonyms": [
                                    "zulu"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_Y",
                            "name": {
                                "value": "y",
                                "synonyms": [
                                    "Yankee"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_X",
                            "name": {
                                "value": "x",
                                "synonyms": [
                                    "X-Ray"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_W",
                            "name": {
                                "value": "w",
                                "synonyms": [
                                    "Whiskey"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_V",
                            "name": {
                                "value": "v",
                                "synonyms": [
                                    "Victor"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_U",
                            "name": {
                                "value": "u",
                                "synonyms": [
                                    "Uniform"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_T",
                            "name": {
                                "value": "t",
                                "synonyms": [
                                    "Tango"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_S",
                            "name": {
                                "value": "s",
                                "synonyms": [
                                    "Sierra"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_R",
                            "name": {
                                "value": "r",
                                "synonyms": [
                                    "Romeo"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_Q",
                            "name": {
                                "value": "q",
                                "synonyms": [
                                    "Quebec"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_P",
                            "name": {
                                "value": "p",
                                "synonyms": [
                                    "Papa"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_O",
                            "name": {
                                "value": "o",
                                "synonyms": [
                                    "Oscar"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_N",
                            "name": {
                                "value": "n",
                                "synonyms": [
                                    "November"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_M",
                            "name": {
                                "value": "m",
                                "synonyms": [
                                    "Mike"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_L",
                            "name": {
                                "value": "l",
                                "synonyms": [
                                    "Lima"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_K",
                            "name": {
                                "value": "k",
                                "synonyms": [
                                    "Kilo"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_J",
                            "name": {
                                "value": "j",
                                "synonyms": [
                                    "Juliett"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_I",
                            "name": {
                                "value": "i",
                                "synonyms": [
                                    "India"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_H",
                            "name": {
                                "value": "h",
                                "synonyms": [
                                    "Hotel"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_G",
                            "name": {
                                "value": "g",
                                "synonyms": [
                                    "Golf"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_F",
                            "name": {
                                "value": "f",
                                "synonyms": [
                                    "Foxtrot"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_E",
                            "name": {
                                "value": "e",
                                "synonyms": [
                                    "Echo"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_D",
                            "name": {
                                "value": "d",
                                "synonyms": [
                                    "Delta"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_C",
                            "name": {
                                "value": "c",
                                "synonyms": [
                                    "Charlie"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_B",
                            "name": {
                                "value": "b",
                                "synonyms": [
                                    "Bravo"
                                ]
                            }
                        },
                        {
                            "id": "LETTER_A",
                            "name": {
                                "value": "a",
                                "synonyms": [
                                    "alpha"
                                ]
                            }
                        }
                    ]
                },
                {
                    "name": "ANY_WORD",
                    "values": [
                        {
                            "name": {
                                "value": "Alpha"
                            }
                        }
                    ]
                }
            ]
        }
    }
}

Lambda Function

'use strict';
var http = require('http'); 

//---------- G H O S T ----------
// Ghost game AWS Lambda function. This function pairs with the Alexa skill David Ghost. 

// The Ghost game for Alexa has 3 components:
// 1. An Alexa skill named Ghost David (unpublished).
// 2. AWS Lambda function (this code), which takes the place of the web site JavaScript at http://ghost-susanpallmann.com.
// 3. The ASP.NET MVC web site back-end at http://ghost-susanpallmann.com. see http://davidpallmann.blogspot.com/2018/12/ghost-father-daughter-project-part-1.html
//
// To output to the CloudWatch logs for this function, simply add console.log('message'); statements to the code.

/**
 * This sample demonstrates a simple skill built with the Amazon Alexa Skills Kit.
 * The Intent Schema, Custom Slots, and Sample Utterances for this skill, as well as
 * testing instructions are located at http://amzn.to/1LzFrj6
 *
 * For additional samples, visit the Alexa Skills Kit Getting Started guide at
 * http://amzn.to/1LGWsLG
 */


// --------------- Helpers that build all of the responses -----------------------

function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: 'PlainText',
            text: output,
        },
        card: {
            type: 'Simple',
            title: `SessionSpeechlet - ${title}`,
            content: `SessionSpeechlet - ${output}`,
        },
        reprompt: {
            outputSpeech: {
                type: 'PlainText',
                text: repromptText,
            },
        },
        shouldEndSession,
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: '1.0',
        sessionAttributes,
        response: speechletResponse,
    };
}


// --------------- Functions that control the skill's behavior -----------------------

// getWelcomeResponse: intiial greeting to user. Initialize session attributes. Make first turn.

function getWelcomeResponse(callback) {
    // If we wanted to initialize the session to have some attributes we could add those here.
    let sessionAttributes = {
        inChallenge: false,  // true if in a challenge.
        gameInSession: true, // true if a game is in-progress.
        turn: true,          // true = human player turn, false = computer player turn
        word: '',            // current partial word in play.
        saveWord: '',        // saved word before last player move
        startPlayer: "0",    // 0: computer player starts, 1: human player starts.
        humanGhost: 0,       // human player's loss count toward being a G-H-O-S-T.
        computerGhost: 0,    // computer player's loss count toward being a G-H-O-S-T.
        unrecognizedLetterCount: 0, // number of times we've failed to understand the human's spoken letter.
        newWord: ''          // new word we learned from human
    };
    const cardTitle = 'Welcome';
    sessionAttributes.startPlayer = "0"; // 0: computer goes first, 1: human goes first
    var speechOutput = 'Welcome to Ghost. To hear the rules, say help. I\'ll start. ';
    var repromptText = 'Say a letter';
    var shouldEndSession = false;
    
     httpGet('Home/Play/' + sessionAttributes.word + '?sp=' + sessionAttributes.startPlayer,  (letter) => { 
         letter = trimLetter(letter);
         if (letter != null) {
             speechOutput = speechOutput + '"' + letter + '".';
             sessionAttributes.word = sessionAttributes.word + letter;
        }

        callback(sessionAttributes,
            buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
     });
}


// -------------------- Human Plays Letter --------------------
// A letter was played. Set letter in session and prepares speech to reply to the user.

function humanPlay(intent, session, callback) {
    const cardTitle = intent.name;
    const letterSlot = intent.slots.Letter;
    let sessionAttributes = session.attributes;

    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';

    sessionAttributes.saveWord = sessionAttributes.word;
    console.log('humanPlay 02 saveWord: ' + sessionAttributes.saveWord);

    if (!sessionAttributes.gameInSession) {
        speechOutput = "To start a new game, say 'New Game'.";
        callback(sessionAttributes,
             buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
        return;
    }

    if (sessionAttributes.inChallenge) {
        speechOutput = "Please respond to the challenge. Either tell me what your word was, or admit you were bluffing. Or say 'New Game' to start a new game..";
        callback(sessionAttributes,
             buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
        return;
    }

    if (letterSlot) {
        var letter = trimLetter(letterSlot.value);
        
        if (letter===null) {
            speechOutput = "I'm not sure what your letter is.";
            sessionAttributes.unrecognizedLetterCount++;
            if (sessionAttributes.unrecognizedLetterCount==2) 
                speechOutput += " Please try again, or say a word from the NATO alphabet like Alpha or Bravo.";
            else
                speechOutput += " Please try again.";
            callback(sessionAttributes,
                 buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
             return;
        }
        else {
            console.log('Human played: ' + letter + ' | current word: ' + sessionAttributes.word);
            sessionAttributes.unrecognizedLetterCount = 0;
            //sessionAttributes = createLetterAttributes(letter);
            sessionAttributes.word = sessionAttributes.word + letter;

            // Human play - check for a completed word
            
            if (sessionAttributes.word.length >= 3) {
                httpGet('Home/ValidWord/' + sessionAttributes.word,  (res) => {
                    if (res=='true') {
                        sessionAttributes.humanGhost++;
                        speechOutput = sessionAttributes.word + ". That\'s a word! I win! You\'re a " + score(sessionAttributes.humanGhost) + ". Let's start a new game. You go first.";
                         sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
                        sessionAttributes.word = '';
                        callback(sessionAttributes,
                            buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
                        return;
                    }
                });
            }
    
            // Computer play
    
            httpGet('Home/Play/' + sessionAttributes.word + '?sp=' + sessionAttributes.startPlayer,  (res) => {
                
                res = trimLetter(res);
                     
                if (res==='?') {   // challenge
                    speechOutput = 'I challenge you! What\'s your word?';
                }
                else {
                    console.log('Computer played: ' + letter + ' | current word: ' + sessionAttributes.word);
                    speechOutput = speechOutput + '"' + res + '".';
                    sessionAttributes.word = sessionAttributes.word + res;
                }
    
                // If the user either does not reply to the welcome message or says something that is not
                // understood, they will be prompted again with this text.
    
                callback(sessionAttributes,
                    buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
                return;
             });
        }
    } else {
        speechOutput = "There is a data problem in the skill: letter slot not found.";

        callback(sessionAttributes,
             buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    }
}

// Return human or computer loss score as a phrase in the form of G-H-O...

function score(ghostScore) {
    switch(ghostScore) {
        case 0:
            return "";
        case 1:
            return "G";
        case 2:
            return "G-H";
        case 3:
            return "G-H-O";
        case 4:
            return "G-H-O-S";
        default:
        case 5:
            return "G-H-O-S-T";
    }
}


// --------------- Start New Game -----------------------

function newGame(intent, session, callback) {
    // If we wanted to initialize the session to have some attributes we could add those here.
    let sessionAttributes = session.attributes;
    const cardTitle = 'Welcome';
    sessionAttributes.startPlayer = "0"; // 0: computer goes first, 1: human goes first
    sessionAttributes.word = '';
    var speechOutput = 'New game. I\'ll start. ';
    var repromptText = 'Say a letter';
    var shouldEndSession = false;
    
     httpGet('Home/Play/' + sessionAttributes.word + '?sp=' + sessionAttributes.startPlayer,  (letter) => { 
         
         letter = trimLetter(letter);
         
         if (letter != null) {
             speechOutput = speechOutput + '"' + letter + '".';
             sessionAttributes.word = sessionAttributes.word + letter;
        }

        callback(sessionAttributes,
            buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
     });
}


function handleSessionEndRequest(callback) {
    const cardTitle = 'Session Ended';
    const speechOutput = 'Thank you for playng Ghost. Be seeing you!';
    // Setting this to true ends the session and exits the skill.
    const shouldEndSession = true;

    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}

// -------------------- Human Asks for Word --------------------
// Tell the user the current word, by spelling it out. e.g. "The current word is S-T-R-E".

function whatIsWord(intent, session, callback) {
    const cardTitle = intent.name;
    let sessionAttributes = session.attributes;
    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';

    speechOutput = "The current word is: ";
    
    if (sessionAttributes.word === '') {
        speechOutput = speechOutput + ' empty';
    }
    else {
        for (var w = 0; w < sessionAttributes.word.length; w++) {
            speechOutput = speechOutput + sessionAttributes.word.substring(w, w+1) + '. ';
        }
    }

    callback(sessionAttributes,
         buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

// Input not understood. Say something.

function handleFallbackIntent(intent, session, callback) {
    const cardTitle = intent.name;
    let sessionAttributes = session.attributes;
    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';

    speechOutput = "It\'s your turn to say a letter.";
    
    callback(sessionAttributes,
         buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

// -------------------- Human Says Computer Made a Word --------------------
// Accept human's word for it, and accept a loss. TOOD: add new word to vocabulary.

function thatsAWord(intent, session, callback) {
    const cardTitle = intent.name;
    let sessionAttributes = session.attributes;
    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';

    sessionAttributes.computerGhost++;
    speechOutput = "Congratulations, you beat me. Now I'm a " + score(sessionAttributes.computerGhost) + ". Why don't you start a new word?";
    
    sessionAttributes.word = '';
    sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first

    callback(sessionAttributes,
         buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

// -------------------- Human Challenges Computer --------------------
// Human challenges computer. Tell them our word, or admit we were bluffing.

function humanChallenge(intent, session, callback) {
    const cardTitle = intent.name;
    let sessionAttributes = session.attributes;
    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';

    httpGet('Home/Challenge/' + sessionAttributes.word,  (res) => {
        if (res==='^' || res==='"^"') {
            sessionAttributes.computerGhost++;
            speechOutput ="You got me - I was bluffing. That makes me a " + score(sessionAttributes.computerGhost) + ". Let's start a new game. You go first.";
            sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
            sessionAttributes.word = '';
            callback(sessionAttributes,
                buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
            return;
        }
        else {
            sessionAttributes.humanGhost++;
            speechOutput = "My word was " + res + ". " + spellWord(res) + ". I win! You\'re a " + score(sessionAttributes.humanGhost) + ". Let's start a new game. You go first.";
            sessionAttributes.startPlayer = "1";
            sessionAttributes.word = '';
            callback(sessionAttributes,
                buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
            return;
        }
    });

}


// -------------------- Human Challenges Computer --------------------
// Human admis they were bluffing after being challenged by computer. 

function humanBluffing(intent, session, callback) {
    const cardTitle = intent.name;
    let sessionAttributes = session.attributes;
    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';

    sessionAttributes.humanGhost++;
    speechOutput = "Sorry, you lose. You're now a " + score(sessionAttributes.humanGhost) + " Let's start a new game. You go first.";
    sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
    sessionAttributes.word = '';
    callback(sessionAttributes,
        buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}


// -------------------- Human Suppies Word upon Challenge --------------------
// Human claims they had a valid word after being challenged.

function humanMyWordIs(intent, session, callback) {
    const cardTitle = intent.name;
    var claimedWordSlot = intent.slots.ClaimedWord;
    let sessionAttributes = session.attributes;
    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';
    
    if (claimedWordSlot && claimedWordSlot.value) {
        var claimedWord = claimedWordSlot.value;

        sessionAttributes.newWord = claimedWordSlot.value;
        
        if (sessionAttributes.newWord && sessionAttributes.word) {
            sessionAttributes.newWord = sessionAttributes.newWord.toUpperCase();
            var existingWord = sessionAttributes.word.toUpperCase();

            var index = sessionAttributes.newWord.indexOf(existingWord);
            if (index != 0) {
                speechOutput = "I'm sorry, but that word doesn't match the word in play. The word in play is " + spellWord(sessionAttributes.word) + ".";
            }
            else {
                if (claimedWord.toLowerCase()===sessionAttributes.word.toLowerCase()) {
                    sessionAttributes.humanGhost++;
                    speechOutput = sessionAttributes.word + "You just made a complete word so I win! You\'re a " + score(sessionAttributes.humanGhost) + ". Let's start a new game. You go first.";
                    sessionAttributes.word = '';
                    sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
                }
                else {
                    sessionAttributes.computerGhost++;
                    speechOutput = "I see. You win, and I'm a " + score(sessionAttributes.computerGhost) + ". Congratulations. Should I add this word to my dictionary? " + claimedWord + '. ' + spellWord(claimedWordSlot.value) + " Say 'Add Word' to confirm.";
                }
            }
        }
    }
    else {
        speechOutput = "Can you say your word again please?";
    }
    
    callback(sessionAttributes,
        buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

// Human confirms, Yes add new word.

function addNewWord(intent, session, callback) {
    const cardTitle = intent.name;
    let sessionAttributes = session.attributes;
    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';
    
    if (sessionAttributes.newWord != '') {
        speechOutput = "Thanks. I have learned the word '" + sessionAttributes.newWord + "'. Let's start a new word. You can start.";
        sessionAttributes.startPlayer = "1";
        httpGet('Home/AddWord/' + sessionAttributes.newWord,  (letter) => { 
            sessionAttributes.newWord = '';
            sessionAttributes.word = '';
            sessionAttributes.startPlayer = "1"; // 0: computer goes first, 1: human goes first
            callback(sessionAttributes,
                buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
     });
    }
    else {
        speechOutput = "Sorry, I don\'t understand. Please say a letter.";
        callback(sessionAttributes,
            buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    }
}


// Undo last turn (Alexa may have misunderstood letter)

function undoIntent(intent, session, callback) {
    
    const cardTitle = intent.name;
    let sessionAttributes = session.attributes;
    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';
    
    if (sessionAttributes.word==='') {
        speechOutput = "There\'s nothing to undo: the current word is empty.";
    }
    else {
        sessionAttributes.word = sessionAttributes.saveWord;

        speechOutput = "I undid your last turn. The current word is: " + spellWord(sessionAttributes.word);
    
    }
    callback(sessionAttributes,
            buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

// Help - explain the rules of Ghost and available commands.

function helpIntent(intent, session, callback) {
    
    const cardTitle = intent.name;
    let sessionAttributes = session.attributes;
    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';
    
    speechOutput = "Let's play Ghost! Each player takes a turn adding a letter. If you make a real word of 3 letters or more, you're a G. Next time that happens, you're a G-H. When you get to G-H-O-S-T you're out. If you don't think a valid word can be made, you can challenge the other player.";
    
    callback(sessionAttributes,
            buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}


// Render speech phrase to spell out a word a letter a time.

function spellWord(word) {
    let speechOutput = '';
    if (word === '') {
        speechOutput = speechOutput + ' empty';
    }
    else {
        for (var w = 0; w < word.length; w++) {
            speechOutput = speechOutput + word.substring(w, w+1) + '. ';
        }
    }
    return speechOutput;
}

// output key variables to log

function debugIntent(intent, session, callback) {
    const cardTitle = intent.name;
    let sessionAttributes = session.attributes;
    let repromptText = 'Please say a letter to continue making a word.';
    let shouldEndSession = false;
    let speechOutput = '';
    
    console.log('sessionAttributes.word: ' + sessionAttributes.word + ', startPlayer: ' + sessionAttributes.startPlayer);

    speechOutput = "Variables have been logged";

    callback(sessionAttributes,
            buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

// Determine the letter from input that may be quoted or may be a NATO alphabetic code like Alpha / Bravo / etc. Return just the letter.
// NOTE: ? is recognized as a letter, as that response is given by the back end for a challenge during computer play.

function trimLetter(letter) {
    if (!letter) return null;
    letter = letter.toUpperCase();
    if (letter.indexOf('?') != -1) return '?';
    if (letter.indexOf('A') != -1) return 'A';
    if (letter.indexOf('B') != -1) return 'B';
    if (letter.indexOf('C') != -1) return 'C';
    if (letter.indexOf('D') != -1) return 'D';
    if (letter.indexOf('E') != -1) return 'E';
    if (letter.indexOf('F') != -1) return 'F';
    if (letter.indexOf('G') != -1) return 'G';
    if (letter.indexOf('H') != -1) return 'H';
    if (letter.indexOf('I') != -1) return 'I';
    if (letter.indexOf('J') != -1) return 'J';
    if (letter.indexOf('K') != -1) return 'K';
    if (letter.indexOf('L') != -1) return 'L';
    if (letter.indexOf('M') != -1) return 'M';
    if (letter.indexOf('N') != -1) return 'N';
    if (letter.indexOf('O') != -1) return 'O';
    if (letter.indexOf('P') != -1) return 'P';
    if (letter.indexOf('Q') != -1) return 'Q';
    if (letter.indexOf('R') != -1) return 'R';
    if (letter.indexOf('S') != -1) return 'S';
    if (letter.indexOf('T') != -1) return 'T';
    if (letter.indexOf('U') != -1) return 'U';
    if (letter.indexOf('V') != -1) return 'V';
    if (letter.indexOf('W') != -1) return 'W';
    if (letter.indexOf('X') != -1) return 'X';
    if (letter.indexOf('Y') != -1) return 'Y';
    if (letter.indexOf('Z') != -1) return 'Z';
    return null;
}

// --------------- Events -----------------------

/**
 * Called when the session starts.
 */
function onSessionStarted(sessionStartedRequest, session) {
    console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`);
}

/**
 * Called when the user launches the skill without specifying what they want.
 */
function onLaunch(launchRequest, session, callback) {
    console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`);

    // Dispatch to your skill's launch.
    getWelcomeResponse(callback);
}

/**
 * Called when the user specifies an intent for this skill.
 */
function onIntent(intentRequest, session, callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);

    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;

    // Dispatch to your skill's intent handlers
    if (intentName === 'MyLetterIsIntent') {
        humanPlay(intent, session, callback);
    } else if (intentName === 'WhatIsWordIntent') {
        whatIsWord(intent, session, callback);
    } else if (intentName === 'ThatsAWordIntent') {
        thatsAWord(intent, session, callback);
    } else if (intentName==='NewGameIntent') {
        newGame(intent, session, callback);
    } else if (intentName==='IChallengeYouIntent') {
        humanChallenge(intent, session, callback);
    } else if (intentName==='MyWordIsIntent') {
        humanMyWordIs(intent, session, callback);
    } else if (intentName==='UndoIntent') {
        undoIntent(intent, session, callback);
    } else if (intentName==='BluffingIntent') {
        humanBluffing(intent, session, callback);
    } else if (intentName==='AddNewWordIntent') {
        addNewWord(intent, session, callback);
    } else if (intentName==='HelpIntent') {
        helpIntent(intent, session, callback);
    } else if (intentName==='DebugIntent') {
        debugIntent(intent, session, callback);
    } else if (intentName === 'AMAZON.HelpIntent') {
        getWelcomeResponse(callback);
    } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
        handleSessionEndRequest(callback);
    } else if (intentName === 'AMAZON.FallbackIntent') {
         handleFallbackIntent(intent, session, callback)
    } else {
        console.log('intentName: ' + intentName);
        throw new Error('Invalid intent');
    }
}

/**
 * Called when the user ends the session.
 * Is not called when the skill returns shouldEndSession=true.
 */
function onSessionEnded(sessionEndedRequest, session) {
    console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}`);
    // Add cleanup logic here
}

// Retrieve a web page from the server. Query passed in as a parameter.

function httpGet(query, callback) {

    var options = {
        host: 'ec2-18-224-78-19.us-east-2.compute.amazonaws.com',
        path: '/' + query,
        method: 'GET',
    };
    
    var req = http.request(options, res => {
        res.setEncoding('utf8');
        var responseString = "";
        
        //accept incoming data asynchronously
        res.on('data', chunk => {
            responseString = responseString + chunk;
        });
        
        //return the data when streaming is complete
        res.on('end', () => {
            //console.log('httpGet 02');
            //console.log(responseString);
            //console.log('httpGet 03');
            callback(responseString);
        });

    });
    req.end();
}



// --------------- Main handler -----------------------

// Route the incoming request based on type (LaunchRequest, IntentRequest,
// etc.) The JSON body of the request is provided in the event parameter.
exports.handler = (event, context, callback) => {
    try {
        console.log(`event.session.application.applicationId=${event.session.application.applicationId}`);

        /**
         * Uncomment this if statement and populate with your skill's application ID to
         * prevent someone else from configuring a skill that sends requests to this function.
         */
        /*
        if (event.session.application.applicationId !== 'amzn1.echo-sdk-ams.app.[unique-value-here]') {
             callback('Invalid Application ID');
        }
        */

        if (event.session.new) {
            onSessionStarted({ requestId: event.request.requestId }, event.session);
        }

        if (event.request.type === 'LaunchRequest') {
            onLaunch(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'IntentRequest') {
            onIntent(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'SessionEndedRequest') {
            onSessionEnded(event.request, event.session);
            callback();
        }
    } catch (err) {
        callback(err);
    }
};