Tuesday, 19 July 2016

Building a Dynamic DNS for Route 53 using CloudWatch Events and Lambda

Introduction


Dynamic registration of resource records is useful when you have instances that are not behind a load balancer that you would like to address by a host name and domain suffix of your choosing, rather than the default <region>.compute.internal or ec2.internal.
In this post, we explore how you can use CloudWatch Events and Lambda to create a Dynamic DNS for Route 53. Besides creating A records, this solution allows you to create alias, i.e. CNAME records, for when you want to address a server by a “friendly” or alternate name. Although this is antithetical to treating instances as disposable resources, there are still a lot of shops that find this useful.

Using CloudWatch and Lambda to respond to infrastructure changes in real-time


With the advent of CloudWatch Events in January 2016, you can now get near real-time information when an AWS resource changes its state, including when instances are launched or terminated.  When you combine this with the power of Amazon Route 53 and AWS Lambda, you can create a system that closely mimics the behavior of Dynamic DNS.
For example, when a newly-launched instance changes its state from pending to running, an event can be sent to a Lambda function that creates a resource record in the appropriate Route 53 hosted zone.  Similarly, when instances are stopped or terminated, Lambda can automatically remove resource records from Route 53.
The example provided in this post works precisely this way.  It uses information from a CloudWatch event to gather information about the instance, such as its public and private DNS name, its public and private IP address, the VPC ID of the VPC that the instance was launched in, its tags, and so on.  It then uses this information to create A, PTR, and CNAME records in the appropriate Route 53 public or private hosted zone.  The solution persists data about the instances in an Amazon DynamoDB table so it can remove resource records when instances are stopped or terminated.

Route 53 Hosted zones


Route 53 offers the convenience of domain name services without having to build a globally distributed highly reliable DNS infrastructure.  It allows instances within your VPC to resolve the names of resources that run within your AWS environment.  It also lets clients on the Internet resolve names of your public-facing resources.  This is accomplished by querying resource record sets that reside within a Route 53 public or private hosted zone.
A private hosted zone is basically a container that holds information about how you want to route traffic for a domain and its subdomains within one or more VPCs, whereas a public hosted zone is a container that holds information about how you want to route traffic from the Internet.

Choosing between VPC DNS or Route 53 Private Hosted Zones


Admittedly, you can use VPC DNS for internal name resolution instead of Route 53 private hosted zones.  Although it doesn’t dynamically create resource records, VPC DNS will provide name resolution for all the hosts within a VPC’s CIDR range.
Unless you create a DHCP option set with a custom domain name and disable hostnames at the VPC, you can’t change the domain suffix; all instances are either assigned the ec2.internal or <region>.compute.internal domain suffix.  You can’t create aliases or other resource record types with VPC DNS either.
Private hosted zones help you overcome these challenges by allowing you to create different resource record types with a custom domain suffix.  Moreover, with Route 53 you can create a subdomain for your current DNS namespace or you can migrate an existing subdomain to Route 53.  By using these options, you can create a contiguous DNS namespace between your on-premises environment and AWS.
So, while VPC DNS can provide basic name resolution for your VPC, Route 53 private hosted zones offer richer functionality by comparison.  It also has a programmable API that can be used to automate the creation/removal of records sets and hosted zones which we’re going leverage later in this post.
Route 53 doesn’t offer support for dynamic registration of resource record sets for public or private hosted zones.  This can pose challenges when an Auto Scaling event occurs and the instances are not behind a load balancer.  A common workaround is to use an automation framework like Chef, Puppet, Ansible, or Salt to create resource records, or by adding instance user data to the launch profile of the Auto Scaling group.  The drawbacks to these approaches are that:
1)   automation frameworks typically require you to manage additional infrastructure.
2)   instance user data doesn’t handle the removal of resource records when the instance is terminated.
This was the motivation for creating a serverless architecture that dynamically creates and removes resource records from Route 53 as EC2 instances are created and destroyed.

DDNS/Lambda example


Make sure that you have the latest version of the AWS CLI installed locally.   For more information, see Getting Set Up with the AWS Command Line Interface.
For this example, create a new VPC configured with a private and public subnet, using Scenario 2: VPC with Public and Private Subnets (NAT) from the Amazon VPC User Guide.  Ensure that the VPC has the DNS resolution and DNS hostnames options set to yes.
After the VPC is created, you can proceed to the next steps.

Step 1 – Create an IAM role for the Lambda function


In this step, you use the AWS Command Line Interface (AWS CLI) to create the Identity and Access Management (IAM) role that the Lambda function assumes when the function is invoked.  You need to create an IAM policy with the required permissions and then attach this policy to the role.
Download the ddns-policy.json and ddns-trust.json files from the AWS Labs GitHub repo.

ddns-policy.json


The policy includes ec2:Describe permission, required for the function to obtain the EC2 instance’s attributes, including the private IP address, public IP address, and DNS hostname.   The policy also includes DynamoDB and Route 53 full access, required for the function to create the DynamoDB table and to update the Route 53 DNS records.  The policy also allows the function to create log groups and log events.
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "ec2:Describe*",
    "Resource": "*"
  }, {
    "Effect": "Allow",
    "Action": [
      "dynamodb:*"
    ],
    "Resource": "*"
  }, {
    "Effect": "Allow",
    "Action": [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ],
    "Resource": "*"
  }, {
    "Effect": "Allow",
    "Action": [
      "route53:*"
    ],
    "Resource": [
      "*"
    ]
  }]
}


ddns-trust.json


The ddns-trust.json file contains the trust policy that grants the Lambda service permission to assume the role.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Create the policy using the policy document in the ddns-pol.json file.  You need to replace <LOCAL PATH> with your local path to the ddns-pol.json file.   The output of the aws iam create-policy command includes the Amazon Resource Locator (ARN).  Save the ARN, since you will need it for future steps.
aws iam create-policy --policy-name ddns-lambda-policy --policy-document file://<LOCAL PATH>/ddns-pol.json
Create the ddns-lambda-role IAM role using the trust policy in the ddns-trust.json file.  You need to replace <LOCAL PATH> with your local path to the ddns-trust.json file.  The output of the aws iam create-role command includes the ARN associated with the role that you created.  Save this ARN,  since you will need it when you create the Lambda function in the next section.
aws iam create-role --role-name ddns-lambda-role --assume-role-policy-document file://<LOCAL PATH>/ddns-trust.json
Attach the policy to the role.  Use the ARN returned in step 2 for the –policy-arn input parameter.
aws iam attach-role-policy --role-name ddns-lambda-role --policy-arn <enter-your-policy-arn-here>

Step 2 – Create the Lambda function


The Lambda function uses modules included in the Python 2.7 Standard Library and the AWS SDK for Python module (boto3), which is preinstalled as part of the Lambda service.  As such, you do not need to create a deployment package for this example.
The function code performs the following:
  • Checks to see whether the “DDNS” table exists in DynamoDB and creates the table if it does not. This table is used to keep a record of instances that have been created along with their attributes. It’s necessary to persist the instance attributes in a table because once an EC2 instance is terminated, its attributes are no longer available to be queried via the EC2 API. Instead, they must be fetched from the table.
  • Queries the event data to determine the instance’s state. If the state is “running”, the function queries the EC2 API for the data it will need to update DNS. If the state is anything else, e.g. “stopped” or “terminated”, it will retrieve the necessary information from the “DDNS” DynamoDB table.
  • Verifies that “DNS resolution” and “DNS hostnames” are enabled for the VPC, as these are required in order to use Route 53 for private name resolution. The function then checks whether a reverse lookup zone for the instance already exists. If it does, it checks to see whether the reverse lookup zone is associated with the instance’s VPC. If it isn’t, it creates the association. This association is necessary in order for the VPC to use Route 53 zone for private name resolution.
  • Checks the EC2 instance’s tags for the CNAME and ZONE tags. If the ZONE tag is found, the function creates A and PTR records in the specified zone. If the CNAME tag is found, the function creates a CNAME record in the specified zone.
  • Verifies whether there’s a DHCP option set assigned to the VPC. If there is, it uses the value of the domain name to create resource records in the appropriate Route 53 private hosted zone. The function also checks to see whether there’s an association between the instance’s VPC and the private hosted zone. If there isn’t, it creates it.
  • Deletes the required DNS resource records if the state of the EC2 instance changes to “shutting down” or “stopped”.

Use the AWS CLI to create the Lambda function:
  • Download the union.py.zip file from the AWS Labs GitHub repo.
  • Execute the following command to create the function.  Note that you need to update the command to use the ARN of the role that you created earlier, as well as the local path to the union.py.zip file containing the Python code for the Lambda function.
aws lambda create-function --function-name ddns_lambda --runtime python2.7 --role <enter-your-role-arn-here> --handler union.lambda_handler --timeout 30 --zip-file fileb://<LOCAL PATH>/union.py.zip
  • The output of the command returns the ARN of the newly-created function. Save this ARN, as you will need it in the next section

Step 3 – Create the CloudWatch Events Rule


In this step, you create the CloudWatch Events rule that triggers the Lambda function whenever CloudWatch detects a change to the state of an EC2 instance.  You configure the rule to fire when any EC2 instance state changes to “running”, “shutting down”, or “stopped”.  Use the aws events put-rule command to create the rule and set the Lambda function as the execution target:
aws events put-rule --event-pattern "{\"source\":[\"aws.ec2\"],\"detail-type\":[\"EC2 Instance State-change Notification\"],\"detail\":{\"state\":[\"running\",\"shutting-down\",\"stopped\"]}}" --state ENABLED --name ec2_lambda_ddns_rule
The output of the command returns the ARN to the newly created CloudWatch Events rule, named ec2_lambda_ddns_rule. Save the ARN, as you will need it to associate the rule with the Lambda function and to set the appropriate Lambda permissions.
Next, set the target of the rule to be the Lambda function.  Note that the —targets input parameter requires that you include a unique identifier for the Id target.  You also need to update the command to use the ARN of the Lambda function that you created previously.
aws events put-targets --rule ec2_lambda_ddns_rule --targets Id=id123456789012,Arn=<enter-your-lambda-function-arn-here>
Next, you add the permissions required for the CloudWatch Events rule to execute the Lambda function.   Note that you need to provide a unique value for the –statement-id input parameter.  You also need to provide the ARN of the CloudWatch Events rule you created earlier.
aws lambda add-permission --function-name ddns_lambda --statement-id 45 --action lambda:InvokeFunction --principal events.amazonaws.com --source-arn <enter-your-cloudwatch-events-rule-arn-here>

Step 4 – Create the private hosted zone in Route 53


To create the private hosted zone in Route 53, follow the steps outlined in Creating a Private Hosted Zone.

Step 5 – Create a DHCP options set and associate it with the VPC


In this step, you create a new DHCP options set and set the domain to be that of your private hosted zone.
  1. Follow the steps outlined in Creating a DHCP Options Set to create a new set of DHCP options.
  2. In the Create DHCP options set dialog box, give the new options set a name, set Domain name to the name of the private hosted zone that you created in Route 53, and set Domain name servers to “AmazonProvidedDNS”.  Choose Yes, Create.DHCP Options Set
  3. Next, follow the steps outlined in Changing the Set of DHCP Options a VPC Uses to update the VPC to use the newly-created DHCP options set.

Step 6 – Launching the EC2 instance and validating results


In this step, you launch an EC2 instance and verify that the function executed successfully.
As mentioned previously, the Lambda function looks for the ZONE or CNAME tags associated with the EC2 instance.  If you specify these tags when you launch the instance, you should include the trailing dot.  In this example, the ZONE tag would be set to “ddnslambda.com.” and the CNAME tag could be set to “test.ddnslambda.com.”.
Because you updated the DHCP options set in this example, the Lambda function uses the specified zone when it creates the Route 53 DNS resource records.  You can use the ZONE tag to override this behavior if you want the function to update a different hosted zone.
In this example, you launch an EC2 instance into the private subnet of the VPC.  Because you updated the domain value of the DHCP options set to be that of the private hosted zone, the Lambda function creates the DNS resource records in the Route 53 zone file.

Launching the EC2 instance


  1. Follow the steps to launch an EC2 instance outlined in Launching an Instance.
  2. In Step 3: Configure Instance Details, for Network, select the VPC.  For Subnet, select the private subnet.  Choose Review and Launch.
  3. (Optional) If you would like to update a different private hosted zone than the one you associated with the VPC, specify the ZONE tag in this step.  You can also specify the CNAME tag if you would like the function to create a CNAME resource record in the associated zone.
Choose Edit tags in the Step 7: Review Instance Launch.
Enter the key and value for Step 5: Tag Instance then choose Review and Launch.
Tag Instance
Complete the launch of the instance and wait until the instance state changes to “running”.  Then, continue to the next step.

Validating results


In this step, you verify that your Lambda function successfully updated the Route 53 resource records.
  1. Log in to the Route 53 console.
  2. In the left navigation pane, choose Hosted Zones to view the list of private and public zones currently configured in Route 53.
  3. Select the hosted zone that you created in step 4 to view the zone file.Verify Hosted Zone
  4. Verify that the resource records were created.Verify Resource Records
  5. Now that you’ve verified that the Lambda function successfully updated the Route 53 resource records in the zone file, stop the EC2 instance and verify that the records are removed by the function.
  6. Log in to the EC2 console.
  7. Choose Instances in the left navigation pane.Select Instance
  8. Select the EC2 instance you launched earlier and choose Stop.Stop Instance
  9. Follow Steps 1 – 3 to view the DNS resource records in the Route 53 zone.
  10. Verify that the records have been removed from the zone file by the Lambda function.

Conclusion


Now that you’ve seen how you can combine various AWS services to automate the creation and removal of Route 53 resource records, we hope you are inspired to create your own solutions.  CloudWatch Events is a powerful tool because it allows you to respond to events in real-time, such as when an instance changes its state.  When used with Lambda, you can create highly scalable serverless infrastructures that react instantly to infrastructure changes.
To learn more about CloudWatch Events, see Using CloudWatch Events in the Amazon CloudWatch Developer Guide.  To learn more about Lambda and serverless infrastructures, see the AWS Lambda Developer Guide and the “Microservices without the Servers” blog post.
We’ve open-sourced the code in this example in the AWS Labs GitHub repo and can’t wait to see your feedback and your ideas about how to improve the solution

Indexing Amazon DynamoDB Content with Amazon Elasticsearch Service Using AWS Lambda

A lot of AWS customers have adopted Amazon DynamoDB for its predictable performance and seamless scalability. The main querying capabilities of DynamoDB are centered around lookups using a primary key. However, there are certain times where richer querying capabilities are required. Indexing the content of your DynamoDB tables with a search engine such as Elasticsearch would allow for full-text search.
In this post, we show how you can send changes to the content of your DynamoDB tables to an Amazon Elasticsearch Service (Amazon ES) cluster for indexing, using the DynamoDB Streams feature combined with AWS Lambda.

Architectural overview

Here’s a high-level overview of the architecture:
DynamoDB Streams to Elasticsearch bridge
We’ll cover the main steps required to put this bridge in place:
  1. Choosing the DynamoDB tables to index and enabling DynamoDB Streams on them.
  2. Creating an IAM role for accessing the Amazon ES cluster.
  3. Configuring and enabling the Lambda blueprint.

Choosing the DynamoDB table to index

In this post, you look at indexing the content of a product catalog in order to provide full-text search capabilities. You’ll index the content of a DynamoDB table called all_products, which is acting as the catalog of all products.
Here’s an example of an item stored in that table:
{
  "product_id": "B016JOMAEE",
  "name": "Serverless Single Page Apps: Fast, Scalable, and Available",
  "category": "ebook",
  "description": "AWS Lambda - A Guide to Serverless Microservices
                  takes a comprehensive look at developing 
                  serverless workloads using the new
                  Amazon Web Services Lambda service.",
  "author": "Matthew Fuller",
  "price": 15.0,
  "rating": 4.8
}

Enabling DynamoDB Streams

In the DynamoDB console, enable the DynamoDB Streams functionality on the all_products table by selecting the table and choosing Manage Stream.
Enabling DynamoDB Streams
Multiple options are available for the stream. For this use case, you need new items to appear in the stream; choose either New image or New and old images. For more information, see Capturing Table Activity with DynamoDB Streams.
DynamoDB Streams Options
After the stream is set up, make a good note of the stream ARN. You’ll need that information later, when configuring the access permissions.
Finding a DynamoDB Stream ARN

Creating a new IAM role

The Lambda function needs read access to the DynamoDB stream just created. In addition, the function also requires access to the Amazon ES cluster to submit new records for indexing.
In the AWS Identity and Access Management (IAM) console, create a new role for the Lambda function and call it ddb-elasticsearch-bridge.
Creating new IAM role
As this role will be used by the Lambda function, choose AWS Lambda from the AWS Service Roles list.
Attaching policy to the role
On the following screens, choose the AWSLambdaBasicExecutionRole managed policy, which allows the Lambda function to send logs to Amazon CloudWatch Logs.

Configuring access to the Amazon ES cluster

First, you need a running Amazon ES cluster. In this example, create a search domain called inventory. After the domain has been created, note its ARN:
Attaching policy to the role
In the IAM console, select the ddb-elasticsearch-bridge role created earlier and add two inline policies to that role:
Attaching policy to the role
Here’s the policy to add to allow the Lambda code to push new documents to Amazon ES (replace the resource ARN with the ARN of your Amazon ES cluster):
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "es:ESHttpPost"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:es:us-east-1:0123456789:domain/inventory/*"
        }
    ]
}
Important: you need to add /* to the resource ARN as depicted above.
Next, add a second policy for read access to the DynamoDB stream (replace the resource ARN with the ARN of your DynamoDB stream):
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "dynamodb:DescribeStream",
                "dynamodb:GetRecords",
                "dynamodb:GetShardIterator",
                "dynamodb:ListStreams"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:dynamodb:us-east-1:0123456789:table/all_products/stream/2016-02-16T23:13:07.600"
            ]
        }
    ]
}

Enabling the Lambda blueprint

When you log into the Lambda console and choose Create a Lambda Function, you are presented with a list of blueprints to use. Select the blueprint called dynamodb-to-elasticsearch.
dynamodb-to-elasticsearch blueprint
Next, select the DynamoDB table all_products as the event source:
Lambda event source
Then, customize the Lambda code to specify the Elasticsearch endpoint:
Customizing the blueprint
Finally, select the ddb-elasticsearch-bridge role created earlier to give the Lambda function the permissions required to interact with DynamoDB and the Amazon ES cluster:
Choosing a role

Testing the result

You’re all set!
After a few records have been added to your DynamoDB table, you can go back to the Amazon ES console and validate that a new index for your items has been automatically created:
Amazon ES indices

Playing with Kibana (Optional)

Elasticsearch is commonly used with Kibana for visual exploration of data.
To start querying the indexed data, create an index pattern in Kibana. Use the name of the DynamoDB table as an index pattern:
Kibana Index pattern
Kibana automatically determines the best type for each field:
Kibana Index pattern
Use a simple query to search the product catalog for all items in the category book containing the word aws in any field:
Kibana Index pattern

Other considerations

Indexing pre-existing content

The solution presented earlier is ideal to ensure that new data is indexed as soon it is added to the DynamoDB table. But what about pre-existing data stored in the table?
Luckily, the Lambda function used earlier can also be used to process data from an Amazon Kinesis stream, as long as the format of the data is similar to the DynamoDB Streams records.
Provided that you have an Amazon Kinesis stream set up as an additional input source for the Lambda code above, you can use the (very naive) sample Python3 code below to read the entire content of a DynamoDB table and push it to an Amazon Kinesis stream called ddb-all-products for indexing in Amazon ES.
import json
import boto3
import boto3.dynamodb.types

# Load the service resources in the desired region.
# Note: AWS credentials should be passed as environment variables
# or through IAM roles.
dynamodb = boto3.resource('dynamodb', region_name="us-east-1")
kinesis = boto3.client('kinesis', region_name="us-east-1")

# Load the DynamoDB table.
ddb_table_name = "all_products"
ks_stream_name = "ddb-all-products"
table = dynamodb.Table(ddb_table_name)

# Get the primary keys.
ddb_keys_name = [a['AttributeName'] for a in table.attribute_definitions]

# Scan operations are limited to 1 MB at a time.
# Iterate until all records have been scanned.
response = None
while True:
    if not response:
        # Scan from the start.
        response = table.scan()
    else:
        # Scan from where you stopped previously.
        response = table.scan(ExclusiveStartKey=response['LastEvaluatedKey'])

    for i in response["Items"]:
        # Get a dict of primary key(s).
        ddb_keys = {k: i[k] for k in i if k in ddb_keys_name}
        # Serialize Python Dictionaries into DynamoDB notation.
        ddb_data = boto3.dynamodb.types.TypeSerializer().serialize(i)["M"]
        ddb_keys = boto3.dynamodb.types.TypeSerializer().serialize(ddb_keys)["M"]
        # The record must contain "Keys" and "NewImage" attributes to be similar
        # to a DynamoDB Streams record. Additionally, you inject the name of
        # the source DynamoDB table in the record so you can use it as an index
        # for Amazon ES.
        record = {"Keys": ddb_keys, "NewImage": ddb_data, "SourceTable": ddb_table_name}
        # Convert the record to JSON.
        record = json.dumps(record)
        # Push the record to Amazon Kinesis.
        res = kinesis.put_record(
            StreamName=ks_stream_name,
            Data=record,
            PartitionKey=i["product_id"])
        print(res)

    # Stop the loop if no additional records are
    # available.
    if 'LastEvaluatedKey' not in response:
        break
Note: In the code example above, you are passing the name of the source DynamoDB table as an extra record attribute SourceTable. The Lambda function uses that attribute to build the Amazon ES index name. Another approach for passing that information is tagging the Amazon Kinesis stream.
Now, create the Amazon Kinesis stream ddb-all-productsand then add permissions to the ddb-elasticsearch-bridge role in IAM to allow the Lambda function to read from the stream:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "kinesis:Get*",
                "kinesis:DescribeStream"
            ],
            "Resource": [
                "arn:aws:kinesis:us-east-1:0123456789:stream/ddb-all-products"
            ]
        }
    ]
}
Finally, set the Amazon Kinesis stream as an additional input source to the Lambda function:
Amazon Kinesis input source
Neat tip: Doing a full re-index of the content this way will not create duplicate entries in Amazon ES.

Paying attention to attribute types

With DynamoDB, you can use different types for the same attribute on different records, but Amazon ES expects a given attribute to be of only one type. Similarly, changing the type of an existing attribute after it has been indexed in Amazon ES causes problems and some searches won’t work as expected.
In these cases, you must rebuild the Amazon ES index. For more information, see Reindexing Your Data in the Elasticsearch documentation.

Conclusion

In this post, you have seen how you can use AWS Lambda with DynamoDB to index your table content in Amazon ES as changes happen.
Because you are relying entirely on Lambda for the business logic, you don’t have to deal with servers at any point: everything is managed by the AWS platform in a highly available and scalable fashion. To learn more about Lambda and serverless infrastructures, see the Microservices without the Servers blog post.
Now that you have added full-text search to your DynamoDB table, you might be interested in exposing its content through a small REST API. For more information, see Using Amazon API Gateway as a proxy for DynamoDB.

Amazon API Gateway mapping improvements

Yesterday we announced the new Swagger import API. You may have also noticed a new first time user experience in the API Gateway console that automatically creates a sample Pet Store API and guides you though API Gateway features. That is not all we’ve been doing:
Over the past few weeks, we’ve made mapping requests and responses easier. This post takes you through the new features we introduced and gives practical examples of how to use them.

Multiple 2xx responses

We heard from many of you that you want to return more than one 2xx response code from your API. You can now configure Amazon API Gateway to return multiple 2xx response codes, each with its own header and body mapping templates. For example, when creating resources, you can return 201 for “created” and 202 for “accepted”.

Context variables in parameter mapping

We have added the ability to reference context variables from the parameter mapping fields. For example, you can include the identity principal or the stage name from the context variable in a header to your HTTP backend. To send the principalId returned by a custom authorizer in an X-User-ID header to your HTTP backend, use this mapping expression:
context.authorizer.principalId
For more information, see the context variable in the Mapping Template Reference page of the documentation.

Access to raw request body

Mapping templates in API Gateway help you transform incoming requests and outgoing responses from your API’s back end. The $input variable in mapping templates enables you to read values from a JSON body and its properties. You can now also return the raw payload, whether it’s JSON, XML, or a string using the $input.body property.
For example, if you have configured your API to receive raw data and pass it to Amazon Kinesis using an AWS service proxy integration, you can use the body property to read the incoming body and the $util variable to encode it for an Amazon Kinesis stream.
{
  "Data" : "$util.base64Encode($input.body)",
  "PartitionKey" : "key",
  "StreamName" : "Stream"
}

JSON parse function

We have also added a parseJson() method to the $util object in mapping templates. The parseJson() method parses stringified JSON input into its object representation. You can manipulate this object representation in the mapping templates. For example, if you need to return an error from AWS Lambda, you can now return it like this:
exports.handler = function(event, context) {
    var myErrorObj = {
        errorType : "InternalFailure",
        errorCode : 9130,
        detailedMessage : "This is my error message",
        stackTrace : ["foo1", "foo2", "foo3"],
        data : {
            numbers : [1, 2, 3]
        }
    }
    
    context.fail(JSON.stringify(myErrorObj));
};
Then, you can use the parseJson() method in the mapping template to extract values from the error and return a meaningful message from your API, like this:
#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
#set ($bodyObj = $util.parseJson($input.body))

{
"type" : "$errorMessageObj.errorType",
"code" : $errorMessageObj.errorCode,
"message" : "$errorMessageObj.detailedMessage",
"someData" : "$errorMessageObj.data.numbers[2]"
}
This will produce a response that looks like this:
{
"type" : "InternalFailure",
"code" : 9130,
"message" : "This is my error message",
"someData" : "3"
}

Conclusion

We continuously release new features and improvements to Amazon API Gateway. Your feedback is extremely important and guides our priorities. Keep sending us feedback on the API Gateway forum and on social media.

Building Enterprise Level Web Applications on AWS Lambda with the DEEP Framework

From the beginning, Mitoc Group has been building web applications for enterprise customers. We are a small group of developers who are helping customers with their entire web development process, from conception through execution and down to maintenance. Being in the business of doing everything is very hard, and it would be impossible without using AWS foundational services, but we incrementally needed more. That is why we became early adopters of the serverless computing approach and developed an ecosystem called Digital Enterprise End-to-end Platform (DEEP) with AWS Lambda at the core.
In this post, we dive deeper into how DEEP is using AWS Lambda to empower developers to build cloud-native applications or platforms using microservices architecture. We will walk you through the process of identifying the front-end, back-end and data tiers required to build web applications with AWS Lambda at the core. We will focus on the structure of the AWS Lambda functions we use, as well as security, performance and benchmarking steps that we take to build enterprise-level web applications.

Enterprise-level web applications

Our approach to web development is full-stack and user-driven, focused on UI (aka the user interface) and UX (aka user eXperience). Before going into the details, we’d like to emphasize the strategical (biased and opinionated) decisions we made early on:
  • We don’t say “no” to customers; every problem is seriously evaluated and sometimes we offer options that involve our direct competitors.
  • We are developers and we focus only on the application level; everything else (platform level and infrastructure level) must be managed by AWS.
  • We focus 20% of effort to solve 80% of work load; everything must be automated and pushed on the service side rather than the client side.
To be honest and fair, it doesn’t work all the time as expected, but it does help us to learn fast and move quickly, sustainably and incrementally solving business problems through technical solutions that really matter. However, the definition of “really matter” differs from customer to customer, quite uniquely in some cases.
Nevertheless, what we have learned from our customers is that enterprise-level web applications must provide the following common expectations:

Architecture

This post describes how we transformed a self-managed task management application (aka todo app) in minutes. The original version can be seen on www.todomvc.com and the original code can be downloaded from https://github.com/tastejs/todomvc/tree/master/examples/angularjs.
The architecture of every web application we build or transform, including the one described above, is similar to the reference architecture of the realtime voting application published recently by AWS on GitHub.
The todo app is written in AngularJS and deployed on Amazon S3, behind Amazon CloudFront (front-end). Task management is processed by AWS Lambda, optionally behind Amazon API Gateway (back-end). Task metadata is stored in Amazon DynamoDB (data tier). The transformed todo app, along with instructions on how to install and deploy this web application, is described in the Building Scalable Web Apps with AWS Lambda and Home-Grown Serverless blog post and the todo code is available on GitHub.
Let’s look at AWS Lambda functions and the value proposition they offer to us and our customers.

AWS Lambda functions

The goal of the todo app is to manage tasks in a self-service mode. End users can view tasks, create new tasks, mark or unmark a task as done, and clear completed tasks. From the UI point of view, that leads to four user interactions that require different back-end calls:
  • web service that retrieves tasks
  • web service that creates tasks
  • web service that deletes tasks
  • web service that updates tasks
A simple reordering of the above identified back-end services calls leads to basic CRUD (create, retrieve, update, delete) operations on the Task data object. These are the simple logical steps that we take to identify the front-end, back-end, and data tiers of (drums beating, trumpets playing) our approach to microservices, which we prefer to call microapplications.
Therefore, coming back to AWS Lambda, we have written four small Node.js functions that are context-bounded and self-sustained (each microservice corresponds to the above identified back-end web service):
Microservice that retrieves tasks
JavaScript
'use strict';

import DeepFramework from 'deep-framework';

export default class Handler extends DeepFramework.Core.AWS.Lambda.Runtime {
  /**
   * @param {Array} args
   */
  constructor(...args) {
    super(...args);
  }

  /**
   * @param request
   */
  handle(request) {
    let taskId = request.getParam('Id');

    if (taskId) {
      this.retrieveTask(taskId, (task) => {
        return this.createResponse(task).send();
      });
    } else {
      this.retrieveAllTasks((result) => {
        return this.createResponse(result).send();
      });
    }
  }

  /**
   * @param {Function} callback
   */
  retrieveAllTasks(callback) {
    let TaskModel = this.kernel.get('db').get('Task');

    TaskModel.findAll((err, task) => {
      if (err) {
        throw new DeepFramework.Core.Exception.DatabaseOperationException(err);
      }

      return callback(task.Items);
    });
  }

  /**
   * @param {String} taskId
   * @param {Function} callback
   */
  retrieveTask(taskId, callback) {
    let TaskModel = this.kernel.get('db').get('Task');

    TaskModel.findOneById(taskId, (err, task) => {
      if (err) {
        throw new DeepFramework.Core.Exception.DatabaseOperationException(err);
      }

      return callback(task ? task.get() : null);
    });
  }
}
Microservice that creates a task
JavaScript
'use strict';

import DeepFramework from 'deep-framework';

export default class extends DeepFramework.Core.AWS.Lambda.Runtime {
  /**
   * @param {Array} args
   */
  constructor(...args) {
    super(...args);
  }

  /**
   * @param request
   */
  handle(request) {
    let TaskModel = this.kernel.get('db').get('Task');

    TaskModel.createItem(request.data, (err, task) => {
      if (err) {
        throw new DeepFramework.Core.Exception.DatabaseOperationException(err);
      }

      return this.createResponse(task.get()).send();
    });
  }
}
Microservice that updates a task
JavaScript
'use strict';

import DeepFramework from 'deep-framework';

export default class Handler extends DeepFramework.Core.AWS.Lambda.Runtime {
  /**
   * @param {Array} args
   */
  constructor(...args) {
    super(...args);
  }

  /**
   * @param request
   */
  handle(request) {
    let taskId = request.getParam('Id');

    if (typeof taskId !== 'string') {
      throw new InvalidArgumentException(taskId, 'string');
    }

    let TaskModel = this.kernel.get('db').get('Task');

    TaskModel.updateItem(taskId, request.data, (err, task) => {
      if (err) {
        throw new DeepFramework.Core.Exception.DatabaseOperationException(err);
      }

      return this.createResponse(task.get()).send();
    });
  }
}
Microservice that deletes a task
JavaScript
'use strict';

import DeepFramework from 'deep-framework';

export default class extends DeepFramework.Core.AWS.Lambda.Runtime {
  /**
   * @param {Array} args
   */
  constructor(...args) {
    super(...args);
  }

  /**
   * @param request
   */
  handle(request) {
    let taskId = request.getParam('Id');

    if (typeof taskId !== 'string') {
      throw new DeepFramework.Core.Exception.InvalidArgumentException(taskId, 'string');
    }

    let TaskModel = this.kernel.get('db').get('Task');

    TaskModel.deleteById(taskId, (err) => {
      if (err) {
        throw new DeepFramework.Core.Exception.DatabaseOperationException(err);
      }

      return this.createResponse({}).send();
    });
  }
}
Each above file with related dependencies is compressed into .zip file and uploaded to AWS Lambda. If you’re new to this process, we strongly recommend following the How to Create, Upload and Invoke an AWS Lambda function tutorial.
Back to the four small Node.js functions, you can see that we have adopted ES6 (aka ES2015) as our coding standard. And we are importing deep-framework in every function. What is this framework anyway and why are we using it everywhere?

Full-stack web framework

Step back for a minute. Building and uploading AWS Lambda functions to the service is very simple and straight-forward, but now imagine that you need to manage 100–150 web services to access a web page, multiplied by hundreds or thousands of web pages.
We believe that the only way to achieve this kind of flexibility and scale is automation and code reuse. These principles led us to build and open source DEEP Framework — a full-stack web framework that abstracts web services and web applications from specific cloud services — and DEEP CLI (aka deepify) — a development tool-chain that abstracts package management and associated development operations.
Therefore, to make sure that the process of managing AWS Lambda functions is streamlined and automated, we consistently include two more files in each uploaded .zip:
DEEP microservice bootstrap
JavaScript
'use strict';

import DeepFramework from 'deep-framework';
import Handler from './Handler';

export default DeepFramework.LambdaHandler(Handler);
DEEP microservice package metadata (for npm) 
JavaScript
{
  "name": "deep-todo-task-create",
  "version": "0.0.1",
  "description": "Create a new todo task",
  "scripts": {
    "postinstall": "npm run compile",
    "compile": "deepify compile-es6 `pwd`"
  },
  "dependencies": {
    "deep-framework": "^1.8.x"
  },
  "preferGlobal": false,
  "private": true,
  "analyze": true
}
Having these three files (Handler.es6, bootstrap.es6, and package.json) in each Lambda function doesn’t mean that your final .zip file will be that small. Actually, a lot of additional operations happen before the .zip file is created. To name a few:
  • AWS Lambda performs better when the uploaded codebase is smaller. Because we provide both local development capabilities and one-step push to production, our process optimizes resources before deploying to AWS.
  • ES6 is not supported by the node.js v0.10.x runtime that we use in AWS Lambda, it is however available in the Node 4.3 runtime, so we compile .es6 files into ES5-compliant .js files using Babel.
  • Dependencies that are defined in package.json are automatically pulled and fine-tuned for node.js v0.10.x to provide the best performance possible.

Putting everything together

First, you need the following pre-requisites:
  1. AWS account (Create an Amazon Web Services Account)
  2. AWS CLI (Configure AWS Command Line Interface)
  3. Git v2+ (Get Started — Installing Git)
  4. Java / JRE v6+ (JDK 8 and JRE 8 Installation Start Here)
  5. js v4+ (Install nvm and Use latest node v4)
Note: Don’t use sudo to install nvm. Otherwise, you’ll have to fix npm permissions.
Second, install the DEEP CLI with the following command:
npm install deepify -g
Next, deploy the todo app using deepify:
deepify install github://MitocGroup/deep-microservices-todo-app ~/deep-todo-app
deepify server ~/deep-todo-app
deepify deploy ~/deep-todo-app
Note: When the deepify server command is finished, you can open http://localhost:8000 in your browser and enjoy the todo app running locally.

Cleaning up

There are at least half a dozen services and several dozen of resources created during deepify deploy. If only there was a simple command that would clean up everything when we’re done. We thought of that and created deepify undeploy to address this need. When you are done using todo app and want to remove web app related resources, execute the following:
deepify undeploy ~/deep-todo-app
As you can see, we empower developers to build hassle-free, cloud-native applications or platforms using microservices architecture and serverless computing.
And what about security?

Security

One of the biggest value propositions on AWS is out-of-the-box security and compliance. The beauty of the cloud-native approach is that security comes by design (in other words, it won’t work otherwise). We take full advantage of that shared responsibility model and enforce security in every layer.
End users benefit from IAM best practices through streamlined implementations of least privilege access, delegated roles instead of credentials, and integration with logging and monitoring services (e.g., AWS CloudTrail, Amazon CloudWatch, and Amazon Elasticsearch Service + Kibana). For example, developers and end users of the todo app didn’t need to explicitly define any security roles (it was done by deepify deploy), but they can rest assured that only their instance of todo app will be using their infrastructure, platform, and application resources.
The following are two security roles (back-end and front-end) that have been seamlessly generated and enforced in each layer:
IAM role that allows back-end invocation of AWS Lambda function (e.g. DeepProdTodoCreate1234abcd) in web application AWS account (e.g. 123456789000)
JavaScript
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": ["lambda:InvokeFunction"],
            "Resource": ["arn:aws:lambda:us-east-1:123456789000:function:DeepProdTodoCreate1234abcd*"]
        }
    ]
}
DEEP role that allows front-end resource (e.g deep.todo:task) to execute action (e.g. deep.todo:task:create)
JavaScript
{
  "Version": "2015-10-07",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["deep.todo:task:create"],
      "Resource": ["deep.todo:task"]
    }
  ]
}

Benchmarking

We have been continuously benchmarking AWS Lambda for various use cases in our microapplications. After a couple of repetitive situations doing similar analysis, we decided to build the benchmarking as another microapplication and re-use the ecosystem to include it automatically where we needed it. You can find the open-source code for the benchmarking microapplication on GitHub:
Particularly, for todo app, we performed various benchmarking analysis on AWS Lambda by tweaking different components in a specific function (e.g. function size, memory size, billable cost, etc.). Next, we would like to share results with you:

Benchmarking for todo app

Req No Function Size (MB) Memory Size (MB) Max Memory Used (MB) Start time Stop time Front-end Call (ms) Back-end Call (ms) Billed Time (ms) Billed Time ($)
1 1.1 128 34 20:15.8 20:16.2 359 200.47 300 0.000000624
2 1.1 128 34 20:17.8 20:18.2 381 202.45 300 0.000000624
3 1.1 128 34 20:19.9 20:20.3 406 192.52 200 0.000000416
4 1.1 128 34 20:21.9 20:22.2 306 152.19 200 0.000000416
5 1.1 128 34 20:23.9 20:24.2 333 175.01 200 0.000000416
6 1.1 128 34 20:25.9 20:26.3 431 278.03 300 0.000000624
7 1.1 128 34 20:27.9 20:28.2 323 170.97 200 0.000000416
8 1.1 128 34 20:29.9 20:30.2 327 160.24 200 0.000000416
9 1.1 128 34 20:31.9 20:32.4 556 225.25 300 0.000000624
10 1.1 128 35 20:33.9 20:34.2 333 179.59 200 0.000000416





Average 375.50 193.67 Total 0.000004992

Performance

Speaking of performance, we find AWS Lambda mature enough to power large-scale web applications. The key is to build the functions as small as possible, focusing on a simple rule of one function to achieve only one task. Over time, these functions might grow in size; therefore, we always keep an eye on them and re-factor / split into the lowest possible logical denominator (smallest task).
Using the benchmarking tool, we ran multiple scenarios on the same function from todo app
Function Size (MB) Memory Size (MB) Max Memory Used (MB) Avg Front-end (ms) Avg Back-end (ms) Total Calls (#) Total Billed (ms) Total Billed ($/1B)*
1.1 128 34-35 375.50 193.67 10 2,400 4,992
1.1 256 34-37 399.40 153.25 10 2,000 8,340
1.1 512 33-35 341.60 134.32 10 1,800 15,012
1.1 128 34-49 405.57 223.82 100 27,300 56,784
1.1 256 28-48 354.75 177.91 100 23,800 99,246
1.1 512 32-47 345.92 163.17 100 23,100 192,654
55.8 128 49-50 543.00 284.03 10 3,400 7,072
55.8 256 49-50 339.80 153.13 10 2,100 8,757
55.8 512 49-50 342.60 141.02 10 2,000 16,680
55.8 128 83-87 416.10 220.91 100 26,900 55,952
55.8 256 50-71 377.69 194.22 100 25,600 106,752
55.8 512 57-81 353.46 174.65 100 23,300 194,322
Based on performance data, we have learned some pretty cool stuff:
  • The smaller the function is, the better it performs; On the other hand, if more memory is allocated, the size of the function matters less and less.
  • Memory size is not directly proportional to billable costs; developers can decide the memory size based on performance requirements combined with associated costs.
  • The key to better performance is continuous load, thanks to container reuse in AWS Lambda.

Conclusion

In this post, we presented a small web application that is built with AWS Lambda at the core. We walked you through the process of identifying the front-end, back-end, and data tiers required to build the todo app. You can fork the example code repository as a starting point for your own web applications.
If you have questions or suggestions, please leave a comment below.

Popular Posts

Powered by Blogger.

Recent Comments

Contact Form

Name

Email *

Message *

Followers