logo

1. Introduction

In this activity we’ll learn how to write microservices on the cloud using several of the free tier products provided by Amazon Web Services (AWS). But first let’s cover some concepts.

1.1. Cloud Computing

Cloud computing is the delivery of computing services (e.g., servers, storage, databases, networking, and software) over the Internet (the “cloud”) typically paying only for the services you actually use. It has important benefits: lower costs, better scalability, and simpler manageability.

There are three cloud computing models popular on the market:

Infrastructure as Service (IaaS)

Provides virtualized computing resources over the internet. A cloud provider hosts the infrastructure components traditionally present in an on-premises data center, including servers, storage and networking hardware.

Examples:

  • Amazon Elastic Compute Cloud (EC2)

  • Google Compute Engine (GCE)

Platform as a Service (PaaS)

Provides a computing platform which typically includes operating system, programming language execution environments, databases, and web servers.

Examples:

  • AWS Elastic Beanstalk

  • Google App Engine (GAE)

  • Heroku

Software as a Service (SaaS)

Provides access to application services installed at a server. No need to worry about installation, maintenance or coding of that software. You access and operate the software using a web browser.

Examples:

  • Gmail

  • Office 365

  • Netflix

Understanding IaaS, PaaS, and SaaS with a Car Example

Taken from The WebSpecia blog:

  • With IaaS, it’s like leasing a car. Keeping the car repaired is someone else’s problem, you just need to supply it with fuel (setting it up, maintaining software, etc.) and you get to go pretty much wherever you want to.

  • PaaS is a bit like getting a taxi. You get in and choose where you want to go to and how to get there. Keeping the car running and figuring out the details is up to the driver.

  • SaaS is a bit like public transport. Cheap, someone else takes care of pretty much everything, you just get to use it. This comes at the price of not always getting as close as you want (less customizability).

1.2. Wep API

An Application Programming Interface (API) is a set of functions and procedures that allow the creation of applications that access the features or data of an operating system, application, or other service. A Web API, as the name suggests, is an API over the web which can be accessed using the HTTP protocol.

1.3. Microservice Architecture

According to James Lewis and Martin Fowler, a microservice architectural style is an approach for developing a software application as a suite of small loosely coupled services, each running in its own process and communicating through lightweight mechanisms, such as HTTP. These services are built around one specific business capability, for example: user management, user roles, e-commerce cart, search engine, or social media logins. They are independent of each other, which means that they can be written in different programming languages, use different data storages, and have practically unlimited scalability.

microservice architecture
Figure 1. Monolithic vs. Microservices Architecture. Source: dzone.com

1.4. Serverless Computing

Serverless computing, as explained by Scott Carey, is a method of providing backend services on a pay-per-use basis. A serverless provider allows developers to write and deploy code without the hassle of worrying about the underlying infrastructure. A company that gets backend services from a serverless vendor is charged based on their computation and do not have to reserve and pay for a fixed amount of bandwidth or number of servers, as the service is auto-scaling. Note that although called serverless, physical servers are still used but developers do not need to be aware of them.

Serverless relies on a Functions as a Service (FaaS) platform, where developers break down their applications into small, stateless chunks, meaning they can execute without any context regarding the underlying server.

The leading FaaS providers are:

Putting Everything Together

Microservices is the way you architect your solution. A single microservice typically exposes a well-defined Web API (this is what your consumers see) and it can be conveniently executed on a Serverless computing (FaaS) platform, which in turn is part of a larger cloud computing ecosystem.

1.5. Preparing your Cloud Development Environment

It’s assumed that you already have an AWS account and have created and AWS Cloud9 environment.
  1. Open your AWS Cloud9 environment.

  2. Install the two Python modules that we’ll be using during this activity:

    Requests

    This is an elegant and simple HTTP library for Python, built for human beings. It allows you to send organic, grass-fed HTTP/1.1 requests, without the need for manual labor. There’s no need to manually add query strings to your URLs, or to form-encode your POST data.
    Documentation: docs.python-requests.org.

    Boto 3

    This is the Amazon Web Services (AWS) SDK for Python. It enables Python developers to create, configure, and manage AWS services, such as DynamoDB. Boto provides an easy to use, object-oriented API, as well as low-level access to AWS services.
    Documentation: boto3.amazonaws.com.

    We’ll be using pip (the Python package management system) to install external libraries. At the terminal type:

    python3 -m pip install requests boto3

Make sure to use the command python3 at the terminal. If you run the python command you’ll end up running Python 2.7 instead of Python 3, and some of the code might not work properly.

2. Hello, World!

We’ll start with a simple “Hello, World!” microservice. All of our microservices will be implemented as AWS Lambda functions.

2.1. Creating a Lambda Function

  1. Follow these steps to create a new Lambda function.

    1. In a new browser window or tab, go to the AWS Management Console.

    2. Open the Lambda console. At the top search field type Lambda and select Lambda Run Code without Thinking about Servers.

    3. Press Create function.

    4. On the Create function page, choose Author from scratch.

    5. On the Basic information form, do the following:

      • In Function name, specify your Lambda function name. For example: hello_world.

      • In Runtime, select Python 3.9.

      • In Architecture, select x86_64.

      • Expand the section Change default execution role and choose Create a new role from AWS policy templates

      • In Role name, enter a name for your role. For example: my_microservices_role.

      • In Policy templates, search and select Simple microservice permissions.

    6. Press Create function.

  2. Create an API Gateway for the Lambda function.

    An API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure Web APIs at any scale. The API Gateway handles all the tasks involved in accepting and processing up to hundreds of thousands of concurrent API calls, including traffic management, authorization and access control, monitoring, and API version management.
    1. Press Add trigger.

    2. In Trigger configuration, do the following:

      • Select API Gateway.

      • In API, select Create an API.

      • In API type, select REST API.

      • In Security, select Open.

      • Press Add.

  3. Test your new function and API Gateway using curl.

    curl, which stands for client URL, is a command line tool for file transfers. It supports a number of protocols including HTTP, HTTPS, FTP, and many more. It’s a great tool for interacting with Web APIs. Alternatively, or additionally, you can use the Postman client.
    1. From the Configuration tab, copy the API endpoint link address from the hello_world-API.

    2. At the Cloud9 terminal, type curl followed by the link from the previous point, something like this:

      curl https://some.end.point.amazonaws.com/default/hello_world

      The output should be:

      "Hello from Lambda!"

2.2. Extending the Lambda Function

  1. Import the Lambda function into the Cloud9 environment.

    1. Go to the Cloud9 IDE.

    2. Click on the AWS tab from the left hand side of the IDE.

    3. Expand the region element (for example US East (Ohio)) and then the Lambda element.

    4. Obtain the context menu for the hello_world function (this can typically be achieved by right clicking on the said name) and select Download…​

    5. Click on the Cloud9 environment’s name.

      A folder with the Lambda function’s name will be created in your environment’s file system. Whatever you place in this folder will be considered part of you Lambda function whenever you upload it back to the cloud.
  2. Click on the Environment tab from the left hand side of the IDE. Select and inspect the Lambda function by opening the hello_world/lambda_function.py file in the editor.

    The automatically generated code for the Lambda function looks like this:

    File: lambda_function.py (original version)
    import json
    
    def lambda_handler(event, context): (1)
        # TODO implement
        return { (2)
            'statusCode': 200,
            'body': json.dumps('Hello from Lambda!')
        }
    1 The lambda_handler function has two parameters:
    • event: This parameter is a dictionary that is used to pass in event data to the handler. It contains the keys in Table 1, among others.

      Table 1. Dictionary Keys for Event
      Key Description
      httpMethod

      A string with the request HTTP method, for example: GET, POST, PUT or DELETE.

      path

      A string with the request path (the name of the Lambda function starting with a slash).

      queryStringParameters

      A dictionary with the query string parameters, or None if the request has no query string.

      headers

      A dictionary with the request headers.

      body

      A string with the request body, or None if the request has no body.

    • context: This parameter provides runtime information to your handler. Its type is LambdaContext.

    2 The handler should return a dictionary from which the HTTP response will be constructed.
    Table 2. Dictionary Keys for HTTP Response
    Key Description
    statusCode

    An integer number with the HTTP response status code. Python’s http.HTTPStatus class can be used to provide clearer descriptions written in English.

    body

    A string with the response body. In this activity we’ll be using always the json.dumps method to produce JSON output.

    headers

    This is an optional key, but if provided it should refer to a dictionary with additional response headers. See the modified version of the Lambda function for an example of this.

  3. Replace the complete contents of the lambda_function.py file with the following code:

    File: lambda_function.py (modified version)
    import json
    from http import HTTPStatus
    
    def lambda_handler(event, context):
        return {
            'statusCode': HTTPStatus.OK.value, (1)
            'body': json.dumps({
                'message': 'Hello, World!',
                'answer': 42,
                'happy': True,
                'name': context.function_name (2)
            }, indent=2), (3)
            'headers': { (4)
                'X-Powered-By': 'AWS Lambda'
            }
        }
    1 Here we use http.HTTPStatus.OK.value as a more comprehensible way to represent a response status code.
    2 context.function_name allows us to obtain dynamically the name of our Lambda function.
    3 The keyword argument indent for the json.dumps method produces a string where each element is on a line by its own and indented using the specified number of blank spaces.
    4 Here, headers specifies a dictionary of additional HTTP response headers.
  4. Upload your updated Lambda function.

    1. In the AWS tab get the context menu of your Lambda function. Select Upload Lambda…​

    2. In Select Upload Type click on Directory.

    3. Choose the directory where your Lambda function resides and press Open.

    4. In Step 2 of 2: Build directory? select No.

    5. A warning dialog window will appear saying that your uploaded code will be the latest version of your Lambda function. Press Yes to continue.

    6. You should see this message: “Successfully uploaded Lambda function”.

      If you get the message “The security token included in the request is expired” just reload the page and repeat the upload steps.
  5. Test the new version of the Lambda function.

    1. In the AWS tab get the context menu of your Lambda function. Select Invoke on AWS. In the Invoke function page that opens in the editor pane go to the bottom and press Invoke.

      You should test your functions using the Invoke on AWS option before doing it with curl or other clients. This allows you to detect basic syntax and runtime errors that otherwise just get reported as internal server errors without any further information.
    2. Check the AWS Remote Invocations panel to see if the response makes any sense.

    3. If there are no errors, you can proceed to test it using curl or other clients by repeating the same steps as described in the testing with curl section.

    4. The output should now be as follows:

      {
        "message": "Hello, World!",
        "answer": 42,
        "happy": true,
        "name": "hello_world"
      }

      To see the complete output of the response, including the headers, run curl with the -i command line option, something like this:

      curl -i https://some.end.point.amazonaws.com/default/hello_world
      Remember to replace the above URL with your API endpoint.

      The output should be similar to this:

      TTP/2 200
      date: Thu, 07 Apr 2022 02:51:03 GMT
      content-type: application/json
      content-length: 90
      x-amzn-requestid: f2bd3b81-c8c6-4136-96ed-772b88ee8ee9
      x-amz-apigw-id: QMGvuGIOPHcFiUw=
      x-powered-by: AWS Lambda
      x-amzn-trace-id: Root=1-624e5197-6789a8c314e426d6601f16e5;Sampled=0
      
      {
        "message": "Hello, World!",
        "answer": 42,
        "happy": true,
        "name": "hello_world"
      }

2.3. Writing a Python Client

This is how a Python 3 text client of the hello_world Lambda function would like like (you can create the file in your Cloud9 environment’s main folder):

File: hello_world_client.py
import requests

URL = 'https://some.end.point.amazonaws.com/default/hello_world' (1)

result = requests.get(URL) (2)
print(result.status_code) (3)
print(result.headers['X-Powered-By']) (4)
body = result.json() (5)
print(body['message']) (6)
1 Make sure to change this URL with your API endpoint.
2 requests.get performs an HTTP GET method request. Other HTTP methods are supported, such as POST, PUT, DELETE, and HEAD. Check the requests API documentation for more information.
3 Prints the status code of the HTTP response.
4 Prints the value of a specific header.
5 Retrieves the body of the response, parsing it from a JSON string and converting it into a Python dictionary.
6 Prints the associated value from the message key of the body dictionary.

Run the above code. At the Cloud9 terminal type:

python3 hello_world_client.py

The output should be:

200
AWS Lambda
Hello, World!

2.4. Exercise A ★

Instances of the LambdaContext class (the type of the context parameter defined in the lambda_handler function) have a property called memory_limit_in_mb, which reports the amount of memory, expressed in megabytes, that has been allocated for the execution of the Lambda function.

Change the code of the modified version of the hello_world Lambda function so that the response object contains an attribute called memory associated to the value described above.

Upload and test the Lambda function using the Invoke on AWS option from the AWS tab and using the curl command as well. Make sure the output is as follows:

{
  "message": "Hello, World!",
  "answer": 42,
  "happy": true,
  "name": "hello_world",
  "memory": "128"
}

2.5. Exercise B ★

Modify the client Python file so that it also displays the value of the memory attribute added to the response object in Exercise A. The output of the program should be:

200
AWS Lambda
Hello, World!
128

2.6. Exercise C ★★

Modify the hello_world Lambda function so that if a parameter name is provided in the query string then the message attribute in the response object should be the string “Hello, name!”, where name is the value of said parameter. If there is no name parameter in the query string, or if there is no query string at all, then the associated value for the message attribute should remain “Hello, World!”.

When testing the Lambda function with the Invoke on AWS option, use the following JSON test codes in the Payload section:

Test 1
{
  "queryStringParameters": {"name": "Thanos"}
}
Test 2
{
  "queryStringParameters": {"color": "Blue"}
}
Test 3
{
  "queryStringParameters": null
}
Test 4
{}

The first test should produce a message with “Hello, Thanos!”. All the other tests should produce “Hello, World!”.

Upload your function once you’re sure it works correctly.

Now test it using curl. At the Cloud9 terminal type:

curl https://some.end.point.amazonaws.com/default/hello_world?name=Thanos
curl https://some.end.point.amazonaws.com/default/hello_world?color=Blue
curl https://some.end.point.amazonaws.com/default/hello_world
Remember to replace the above URLs with your API endpoint.

The output should be the same as previously described.

3. RESTful Web Services

3.1. General Overview

REST stands for Representational State Transfer. It relies on a stateless, client-server, cacheable communications protocol. REST is an architectural style for designing networked applications. RESTful applications use HTTP requests to post and put data (create and/or update), read data (make queries), and delete data. Thus, REST uses HTTP for all four CRUD (Create/Read/Update/Delete) operations (see table 3). When building web services the use of REST is often preferred over the more heavyweight SOAP (Simple Object Access Protocol) style because REST is less complex and does not leverage as much bandwidth, which makes it a better fit for use over the Internet.

Table 3. CRUD/HTTP/SQL Mapping
CRUD Operation HTTP Method SQL Statement Idempotent? Safe?

Create

POST

INSERT

No

No

Read

GET

SELECT

Yes

Yes

Update

PUT

UPDATE

Yes

No

Delete

DELETE

DELETE

Yes

No

We say an operation is idempotent if it can be applied multiple times without changing the result beyond the initial application. For example, in mathematics the absolute value is an idempotent operation: applying it once or multiple times gives us the same answer.

An operation is safe if it’s only used for data retrieval (it doesn’t have any side-effects on the server).

REST was defined by Roy Thomas Fielding in his 2000 PhD dissertation “Architectural Styles and the Design of Network-based Software Architectures”.

3.2. API Guidelines

REST is more a collection of principles than it is a set of standards. There are “best practices” and de-facto standards but those are constantly evolving. Fortunately for us, there are a couple of documents, both written by Todd Fredrich, that provide some useful guidelines on things to consider when writing RESTful APIs:

In the following sections we will incorporate several recommendations from these documents. Specifically:

  1. We’ll use a noun to name our resource, not a verb, and it will be in plural form (for example 'users' instead of 'user').

  2. We’ll supply links in the response body for retrieval of the resource object itself or related objects. This is a constraint of the REST application architecture known as HATEOAS (Hypermedia as the Engine of Application State).

  3. We’ll use HTTP methods to mean something useful:

    • GET — Read a resource or collection.

    • POST — Create.

    • PUT — Update.

    • DELETE — Remove a resource or collection.

  4. We’ll make sure that the GET, PUT and DELETE operations are idempotent.

  5. We’ll use the JSON format for the request and response bodies.

  6. We’ll use meaningful HTTP status codes:

    • 200 — OK. The request was successful.

    • 201 — Created. New resource was created successfully.

    • 400 — Bad Request. Malformed syntax or a bad query.

    • 404 — Not Found. Requested resource does not exist.

4. Interactive Fiction

4.1. The Pugnacious Orc Application

Let’s now look at a complete application built around several microservices. For this example we’ll build an interactive fiction program (a.k.a. a text adventure game) called “The Pugnacious Orc”.

What is Interactive Fiction?

Interactive fiction, generally abbreviated as IF, is software that uses text to create virtual environments where a player inhabits. The program provides you with a simple written description of your surroundings, then asks you what you want do next. To move around or interact with your virtual surroundings, you key in text commands telling the game what you want your avatar to do. By reading and typing text, you make your way through the virtual world, collecting treasures, fighting monsters, avoiding traps, and solving puzzles until you finally reached the end of the game.

This is how the application will look when running:

T H E   P U G N A C I O U S   O R C
===================================

After a drunken night out with friends, you awaken the
next morning in a thick, dank forest. Head spinning and
fighting the urge to vomit, you stand and marvel at
your new, unfamiliar setting. The peace quickly fades
when you hear a grotesque sound emitting behind you. A
slobbering pugnacious orc is running towards you.

What do you do?

A. Grab a nearby rock and throw it at the orc
B. Lie down and wait to be mauled
C. Run
Q. Quit program

> b

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Welp, that was quick.

You died!

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
G A M E   O V E R
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The following diagram depicts our application’s architecture:

pugnacious orc architecture
Figure 2. Architecture for “The Pugnacious Orc” Application

As you can see, there are two microservices:

places

This microservice will provide all the information regarding the different places where an IF player con go. The data store for this microservice will be a read-only YAML text file.

YAML (acronym for “YAML Ain’t Markup Language”) is a human-readable data serialization language. It is commonly used for configuration files, but could be used in many applications where data is being stored or transmitted. YAML targets many of the same applications as XML but has a minimal syntax.

scores

This microservice will be used for storing and retrieving the scores of all the IF players. Its data store will be a DynamoDB table.

Amazon DynamoDB is a fully managed proprietary NoSQL database service that supports key-value and document data structures and is offered as part of the Amazon Web Services portfolio.

4.2. Places Microservice

Follow these steps to create the places microservice as a Lambda function:

  1. Repeat the steps in the section Create a new Lambda function. The new Lambda function should be called places. Select Choose an existing role and use the role we created before.

  2. Create the corresponding API Gateway exactly as we did before.

  3. Import the new Lambda function into the Cloud9 environment.

  4. In the Cloud9 terminal, change the current working directory to where the Lambda function code resides. Type:

    cd ~/environment/places
  5. As noted before, this microservice will use a YAML file as its read-only data store. So we’ll need to install the pyyaml module. At the terminal type:

    sudo python3 -m pip install pyyaml --target .

    If the Lambda function code depends on external libraries other than Boto 3, it is necessary to install them with pip in the function’s the local directory. The “--target .” command line option is used precisely to achieve this.

  6. Retrieve the microservice’s pugnacious_orc.yaml file. At the terminal type:

    wget bit.ly/2XmFTis -O pugnacious_orc.yaml

    Open this file in the editor and take some time to inspect it.

    In any YAML file, structure is shown through indentation (one or more spaces). Sequence items (lists) are denoted by a dash, and key value pairs within a map (dictionaries) are separated by a colon.

    Let’s discuss the design of the pugnacious_orc.yaml file. At the highest level it represents a map of different places where an IF game player can be. Each key in this map is the name of a place, and its associated value is a also map, but containing the information of the named place using the following keys:

    • statement — A text describing this place.

    • options — This key may be inexistent, and if so it means that the game must end if a player happens to arrive here. Yet, if the key does exist, it contains a sequence of two or more option maps, each of these with the following keys:

      • text — A text explaining a possible action that the player may take at this very moment.

      • target — The name of a place where the player will go to if she/he takes this particular action.

    • points — If this key exists, it indicates a certain number of points that the player should accumulate as a reward for reaching this place.

    This is an example of a place named “run” in the YAML file:

    run:
      statement: You run, but the orc's speed is too great. Now what?
      options:
        - text: Hide behind a boulder
          target: hide
        - text: Fight against the orc
          target: fight
        - text: Run towards an abandoned town
          target: town
      points: 5

    A last thing to point out: a place whose name has a “__” (double underscore, or dunder) prefix marks the game’s starting place. Obviously, there should only be one of those in the entire the YAML file.

  7. Now, retrieve the microservice’s lambda_function.py source file. Type at the terminal:

    wget bit.ly/2H0468C -O lambda_function.py

    Open the file in the editor and inspect it to understand how the microservice works.

  8. Upload and test the Lambda function, using the following JSON code in the payload section:

    Test 1
    {
        "httpMethod": "GET",
        "path": "/places",
        "headers": {
            "Host": "localhost"
        }
    }
    Test 2
    {
        "httpMethod": "GET",
        "path": "/places",
        "queryStringParameters": {
            "place": "rock"
        },
        "headers": {
            "Host": "localhost"
        }
    }

    The corresponding response output should be:

    Output 1
    {
        "statusCode": 200,
        "body": "[\n  {\n    \"place\": \"__start\",\n    \"url\": \"https://localhost/default/places/?place=__start\"\n  },\n  {\n    \"place\": \"cave\",\n    \"url\": \"https://localhost/default/places/?place=cave\"\n  },\n  {\n    \"place\": \"fight\",\n    \"url\": \"https://localhost/default/places/?place=fight\"\n  },\n  {\n    \"place\": \"fight_with_sword\",\n    \"url\": \"https://localhost/default/places/?place=fight_with_sword\"\n  },\n  {\n    \"place\": \"fight_without_sword\",\n    \"url\": \"https://localhost/default/places/?place=fight_without_sword\"\n  },\n  {\n    \"place\": \"hide_behind_boulder\",\n    \"url\": \"https://localhost/default/places/?place=hide_behind_boulder\"\n  },\n  {\n    \"place\": \"hide_in_cave\",\n    \"url\": \"https://localhost/default/places/?place=hide_in_cave\"\n  },\n  {\n    \"place\": \"lie\",\n    \"url\": \"https://localhost/default/places/?place=lie\"\n  },\n  {\n    \"place\": \"next_with_sword\",\n    \"url\": \"https://localhost/default/places/?place=next_with_sword\"\n  },\n  {\n    \"place\": \"next_without_sword\",\n    \"url\": \"https://localhost/default/places/?place=next_without_sword\"\n  },\n  {\n    \"place\": \"rock\",\n    \"url\": \"https://localhost/default/places/?place=rock\"\n  },\n  {\n    \"place\": \"rock_again\",\n    \"url\": \"https://localhost/default/places/?place=rock_again\"\n  },\n  {\n    \"place\": \"run\",\n    \"url\": \"https://localhost/default/places/?place=run\"\n  },\n  {\n    \"place\": \"sneak_out\",\n    \"url\": \"https://localhost/default/places/?place=sneak_out\"\n  },\n  {\n    \"place\": \"town\",\n    \"url\": \"https://localhost/default/places/?place=town\"\n  },\n  {\n    \"place\": \"wait_with_flower\",\n    \"url\": \"https://localhost/default/places/?place=wait_with_flower\"\n  },\n  {\n    \"place\": \"wait_without_flower\",\n    \"url\": \"https://localhost/default/places/?place=wait_without_flower\"\n  }\n]"
    }
    Output 2
    {
        "statusCode": 200,
        "body": "{\n  \"statement\": \"The orc is stunned, but regains control. He begins\\nrunning towards you again.\\n\\nWhat do you want to do?\\n\",\n  \"options\": [\n    {\n      \"text\": \"Run\",\n      \"target\": \"run\"\n    },\n    {\n      \"text\": \"Throw another rock\",\n      \"target\": \"rock_again\"\n    },\n    {\n      \"text\": \"Go to a nearby cave\",\n      \"target\": \"cave\"\n    }\n  ],\n  \"points\": 10,\n  \"place\": \"rock\"\n}"
    }

    If there are no errors, upload the Lambda function and test it using curl:

    Test 3
    curl https://some.end.point.amazonaws.com/default/places
    Test 4
    curl https://some.end.point.amazonaws.com/default/places?place=rock
    Remember to replace the above URLs with your API endpoint for the places-API.

    The corresponding output should look like this:

    Output 3
    [
      {
        "place": "__start",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=__start"
      },
      {
        "place": "cave",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=cave"
      },
      {
        "place": "fight",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=fight"
      },
      {
        "place": "fight_with_sword",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=fight_with_sword"
      },
      {
        "place": "fight_without_sword",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=fight_without_sword"
      },
      {
        "place": "hide_behind_boulder",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=hide_behind_boulder"
      },
      {
        "place": "hide_in_cave",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=hide_in_cave"
      },
      {
        "place": "lie",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=lie"
      },
      {
        "place": "next_with_sword",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=next_with_sword"
      },
      {
        "place": "next_without_sword",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=next_without_sword"
      },
      {
        "place": "rock",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=rock"
      },
      {
        "place": "rock_again",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=rock_again"
      },
      {
        "place": "run",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=run"
      },
      {
        "place": "sneak_out",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=sneak_out"
      },
      {
        "place": "town",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=town"
      },
      {
        "place": "wait_with_flower",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=wait_with_flower"
      },
      {
        "place": "wait_without_flower",
        "url": "https://some.end.point.amazonaws.com/default/places/?place=wait_without_flower"
      }
    ]
    Output 4
    {
      "statement": "The orc is stunned, but regains control. He begins\nrunning towards you again.\n\nWhat do you want to do?\n",
      "options": [
        {
          "text": "Run",
          "target": "run"
        },
        {
          "text": "Throw another rock",
          "target": "rock_again"
        },
        {
          "text": "Go to a nearby cave",
          "target": "cave"
        }
      ],
      "points": 10,
      "place": "rock"
    }

    If everything is working fine, proceed to the next section in order to create our application’s client code.

4.3. The Client

  1. In the terminal change the current working directory to the environment’s main folder. At the Cloud9 terminal, type:

    cd ~/environment
  2. Retrieve the application’s client code. Type at the terminal:

    wget bit.ly/2T4vvgF -O pugnacious_orc_client.py
  3. Open the pugnacious_orc_client.py file in the editor and replace the value of the URL_PLACES variable with the API endpoint of the places-API.

  4. Run the program. At the terminal, type:

    python3 pugnacious_orc_client.py

    Play with the program for a while to make sure it works fine.

4.4. Exercise D ★

Modify the pugnacious_orc.yaml file to add a new terminal place (a place that has no options attribute, thus terminates the program) and add it as a target reachable from the __start place.

4.5. Exercise E ★★

Modify the pugnacious_orc_client.py so that it accumulates all the points a player obtains when going through all the the different places visited. Remember that the JSON response returned by the places Lambda function, when the places query string parameter has been provided, might have an integer attribute called points, but this is not always the case.

At the end of the game the total amount of points accumulated by the player in the current run should be displayed, as demonstrated in the very last lines of the following example:

T H E   P U G N A C I O U S   O R C
===================================

After a drunken night out with friends, you awaken the
next morning in a thick, dank forest. Head spinning and
fighting the urge to vomit, you stand and marvel at
your new, unfamiliar setting. The peace quickly fades
when you hear a grotesque sound emitting behind you. A
slobbering pugnacious orc is running towards you.

What do you do?

A. Grab a nearby rock and throw it at the orc
B. Lie down and wait to be mauled
C. Run
Q. Quit program

> a

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The orc is stunned, but regains control. He begins
running towards you again.

What do you want to do?

A. Run
B. Throw another rock
C. Go to a nearby cave
Q. Quit program

> a

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You run as quickly as possible, but the orc's
speed is too great.

What do you want to do?

A. Hide behind boulder
B. Fight
C. Run towards an abandoned town
Q. Quit program

> b

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You're no match for an orc.

You died!

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Your score is: 15
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
G A M E   O V E R
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

4.6. Scores Microservice

4.6.1. The DynamoDB Table

We’re going to use a DynamoDB table to store the scores of the users of our IF game.

  1. In a new browser window or tab, go to the AWS Management Console.

  2. Open the DynamoDB console. At the top search field type DynamoDB and select DynamoDB Managed NoSQL Database.

  3. Press Create table.

  4. On the Create table page, do the following:

    • In Table name, type scores.

    • In Partition key, type uuid and keep String as its corresponding data type.

      A Universal Unique Identifier (UUID) is a 128-bit number used to uniquely identify some object or entity on the Internet. Depending on the specific mechanisms used, a UUID is either guaranteed to be different or is, at least, extremely likely to be different from any other UUID.

    • Keep the rest of the default settings and press Create table at the bottom.

Once the table has been created we can add to it new items. The following Python code shows how to add a new item to the DynamoDB table (create the file in your environment’s main folder):

File: put_item.py
import boto3
import uuid
from datetime import datetime

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('scores')
data = {
    'uuid' : uuid.uuid1().hex, (1)
    'initials': 'JS',
    'score': 99,
    'timestamp': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') (2)
}
table.put_item(Item=data)
print('Item added.')
1 The only required attribute is uuid because we declared it to be the primary key when we created the DynamoDB table. In this case we generate a version 1 UUID which uses a timestamp and the system’s MAC address. Besides the primary key, we are free to include any additional attributes we want to store in the database, in our case: initials, score, and timestamp.
2 Here we create our own timestamp string using the current UTC date (YYYY-MM-DD) and time (HH:MM:SS).

To run the program, type at the terminal:

python3 put_item.py

Go back to the DynamoDB console and press Explore table items to verify that a new item has been added to the scores table.

The following code demonstrates how to scan the full contents of the table (create the file in your environment’s main folder):

File: scan.py
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('scores')

items = table.scan()['Items']
for item in items:
    print(item)

To run the program, type at the terminal:

python3 scan.py

The output should look something like this:

{'score': Decimal('99'), 'uuid': '37ee677e3c8b11e9ac910a9b1c6b71fe', 'initials': 'JS', 'timestamp': '2022-03-02 01:33:44'}
Note that the score attribute was originally set to a regular Python int value, but when retrieved from the database its type is Decimal. Be careful with this situation because sometimes this produces some unexpected errors.

4.6.2. The scores Lambda Function

Let’s now create the scores microservice. Once again, we create a new Lambda function:

  1. Repeat the steps in the section Create a new Lambda function. This time the Lambda function should be called scores. Select Choose an existing role and use the role we created before.

  2. Create the corresponding API Gateway exactly as we did before.

  3. Import the new Lambda function into the Cloud9 environment.

  4. In the Cloud9 terminal, change the current working directory to where the Lambda function code resides. Type:

    cd ~/environment/scores
  5. Retrieve the microservice’s lambda_function.py source file. Type at the terminal:

    wget bit.ly/2Tq2NGz -O lambda_function.py

    Open the file in the editor and inspect it to understand how the microservice works.

  6. Upload and test the Lambda function, using the following JSON code in the payload section:

    Test 1
    {
        "httpMethod": "POST",
        "body": "{ \"initials\": \"MJ\", \"score\": 100 }"
    }
    Test 2
    {
        "httpMethod": "GET"
    }

    The corresponding response output should be something like this:

    Output 1
    {
        "statusCode": 201,
        "body": "{\n  \"message\": \"New resource created with uuid = 5db3b7203c8b11e9ad200242ac110002.\"\n}"
    }
    Output 2
    {
        "statusCode": 200,
        "body": "[\n  {\n    \"uuid\": \"5db3b7203c8b11e9ad200242ac110002\",\n    \"initials\": \"MJ\",\n    \"score\": 100,\n    \"timestamp\": \"2022-03-02 01:34:48\"\n  },\n  {\n    \"uuid\": \"37ee677e3c8b11e9ac910a9b1c6b71fe\",\n    \"initials\": \"JS\",\n    \"score\": 99,\n    \"timestamp\": \"2022-03-02 01:33:44\"\n  }\n]"
    }

    If there are no errors, upload the Lambda function and test it using curl:

    Test 3
    curl https://some.end.point.amazonaws.com/default/scores \
    -X POST \
    -d '{ "initials": "DA", "score": 42 }'
    Test 4
    curl https://some.end.point.amazonaws.com/default/scores
    Remember to replace the above URLs with your API endpoint for the scores-API.

    The corresponding output should look like this:

    Output 3
    {
      "message": "New resource created with uuid = 3335aace3c9211e9b04f3a7836691d34."
    }
    Output 4
    [
      {
        "uuid": "5db3b7203c8b11e9ad200242ac110002",
        "initials": "MJ",
        "score": 100,
        "timestamp": "2022-03-02 01:34:48"
      },
      {
        "uuid": "37ee677e3c8b11e9ac910a9b1c6b71fe",
        "initials": "JS",
        "score": 99,
        "timestamp": "2022-03-02 01:33:44"
      },
      {
        "uuid": "3335aace3c9211e9b04f3a7836691d34",
        "initials": "DA",
        "score": 42,
        "timestamp": "2022-03-02 02:23:43"
      }
    ]

4.7. Exercise F ★★★

Modify the pugnacious_orc_client.py file so that it behaves as follows once the game has ended:

  1. If the player has obtained more than 0 points the program should request the user to type her/his initials and then it should call the scores microservice using the corresponding POST method to store that information in the database.

    This is how you make a POST method request in Python:

    File: post_example.py
    import requests
    
    URL = 'https://some.end.point.amazonaws.com/default/scores'
    
    payload = {
        'initials': 'PJ',
        'score': 15,
    }
    result = requests.post(URL, json=payload)
    print(result.status_code)
    body = result.json()
    print(body['message'])
  2. The program must display the scores of every player by calling the scores microservice using the corresponding GET method.

This is how the end of the game should look, assuming that the current user scored 10 points:

Input your initials: AO

SCORES
------------------------------
MJ  100 on 2022-03-02 01:34:48
JS   99 on 2022-03-02 01:33:44
DA   42 on 2022-03-02 02:23:43
PJ   15 on 2022-03-02 03:46:44
AO   10 on 2022-03-02 03:49:35
------------------------------