AWS Compute Blog

Using Amazon API Gateway as a proxy for DynamoDB

by Stefano Buliani | on | in Amazon API Gateway | | Comments

Andrew Baird Andrew Baird, AWS Solutions Architect

Amazon API Gateway has a feature that enables customers to create their own API definitions directly in front of an AWS service API. This tutorial will walk you through an example of doing so with Amazon DynamoDB.

Why use API Gateway as a proxy for AWS APIs?

Many AWS services provide APIs that applications depend on directly for their functionality. Examples include:

  • Amazon DynamoDB – An API-accessible NoSQL database.
  • Amazon Kinesis – Real-time ingestion of streaming data via API.
  • Amazon CloudWatch – API-driven metrics collection and retrieval.

If AWS already exposes internet-accessible APIs, why would you want to use API Gateway as a proxy for them? Why not allow applications to just directly depend on the AWS service API itself?

Here are a few great reasons to do so:

  1. You might want to enable your application to integrate with very specific functionality that an AWS service provides, without the need to manage access keys and secret keys that AWS APIs require.
  2. There may be application-specific restrictions you’d like to place on the API calls being made to AWS services that you would not be able to enforce if clients integrated with the AWS APIs directly.
  3. You may get additional value out of using a different HTTP method from the method that is used by the AWS service. For example, creating a GET request as a proxy in front of an AWS API that requires an HTTP POST so that the response will be cached.
  4. You can accomplish the above things without having to introduce a server-side application component that you need to manage or that could introduce increased latency. Even a lightweight Lambda function that calls a single AWS service API is code that you do not need to create or maintain if you use API Gateway directly as an AWS service proxy.

Here, we will walk you through a hypothetical scenario that shows how to create an Amazon API Gateway AWS service proxy in front of Amazon DynamoDB.

The Scenario

You would like the ability to add a public Comments section to each page of your website. To achieve this, you’ll need to accept and store comments and you will need to retrieve all of the comments posted for a particular page so that the UI can display them.

We will show you how to implement this functionality by creating a single table in DynamoDB, and creating the two necessary APIs using the AWS service proxy feature of Amazon API Gateway.

Defining the APIs

The first step is to map out the APIs that you want to create. For both APIs, we’ve linked to the DynamoDB API documentation. Take note of how the API you define below differs in request/response details from the native DynamoDB APIs.

Post Comments

First, you need an API that accepts user comments and stores them in the DynamoDB table. Here’s the API definition you’ll use to implement this functionality:

Resource: /comments
HTTP Method: POST
HTTP Request Body:
{
  "pageId":   "example-page-id",
  "userName": "ExampleUserName",
  "message":  "This is an example comment to be added."
}

After you create it, this API becomes a proxy in front of the DynamoDB API PutItem.

Get Comments

Second, you need an API to retrieve all of the comments for a particular page. Use the following API definition:

Resource: /comments/{pageId}
HTTP Method: GET

The curly braces around {pageId} in the URI path definition indicate that pageId will be treated as a path variable within the URI.

This API will be a proxy in front of the DynamoDB API Query. Here, you will notice the benefit: your API uses the GET method, while the DynamoDB GetItem API requires an HTTP POST and does not include any cache headers in the response.

Creating the DynamoDB Table

First, Navigate to the DynamoDB console and select Create Table. Next, name the table Comments, with commentId as the Primary Key. Leave the rest of the default settings for this example, and choose Create.

After this table is populated with comments, you will want to retrieve them based on the page that they’ve been posted to. To do this, create a secondary index on an attribute called pageId. This secondary index enables you to query the table later for all comments posted to a particular page. When viewing your table, choose the Indexes tab and choose Create index.

When querying this table, you only want to retrieve the pieces of information that matter to the client: in this case, these are the pageId, the userName, and the message itself. Any other data you decide to store with each comment does not need to be retrieved from the table for the publically accessible API. Type the following information into the form to capture this and choose Create index:

Creating the APIs

Now, using the AWS service proxy feature of Amazon API Gateway, we’ll demonstrate how to create each of the APIs you defined. Navigate to the API Gateway service console, and choose Create API. In API name, type CommentsApi and type a short description. Finally, choose Create API.

Now you’re ready to create the specific resources and methods for the new API.

Creating the Post Comments API

In the editor screen, choose Create Resource. To match the description of the Post Comments API above, provide the appropriate details and create the first API resource:

Now, with the resource created, set up what happens when the resource is called with the HTTP POST method. Choose Create Method and select POST from the drop down. Click the checkmark to save.

To map this API to the DynamoDB API needed, next to Integration type, choose Show Advanced and choose AWS Service Proxy.

Here, you’re presented with options that define which specific AWS service API will be executed when this API is called, and in which region. Fill out the information as shown, matching the DynamoDB table you created a moment ago. Before you proceed, create an AWS Identity and Access Management (IAM) role that has permission to call the DynamoDB API PutItem for the Comments table; this role must have a service trust relationship to API Gateway. For more information on IAM policies and roles, see the Overview of IAM Policies topic.

After inputting all of the information as shown, choose Save.

If you were to deploy this API right now, you would have a working service proxy API that only wraps the DynamoDB PutItem API. But, for the Post Comments API, you’d like the client to be able to use a more contextual JSON object structure. Also, you’d like to be sure that the DynamoDB API PutItem is called precisely the way you expect it to be called. This eliminates client-driven error responses and removes the possibility that the new API could be used to call another DynamoDB API or table that you do not intend to allow.

You accomplish this by creating a mapping template. This enables you to define the request structure that your API clients will use, and then transform those requests into the structure that the DynamoDB API PutItem requires.

From the Method Execution screen, choose Integration Request:

In the Integration Request screen expand the Mapping Templates section and choose Add mapping template. Under Content-Type, type application/json and then choose the check mark:

Next, choose the pencil icon next to Input passthrough and choose Mapping template from the dropdown. Now, you’ll be presented with a text box where you create the mapping template. For more information on creating mapping templates, see API Gateway Mapping Template Reference.

The mapping template will be as follows. We’ll walk through what’s important about it next:

{ 
    "TableName": "Comments",
    "Item": {
	"commentId": {
            "S": "$context.requestId"
            },
        "pageId": {
            "S": "$input.path('$.pageId')"
            },
        "userName": {
            "S": "$input.path('$.userName')"
        },
        "message": {
            "S": "$input.path('$.message)"
        }
    }
}

This mapping template creates the JSON structure required by the DynamoDB PutItem API. The entire mapping template is static. The three input variables are referenced from the request JSON using the $input variable and each comment is stamped with a unique identifier. This unique identifier is the commentId and is extracted directly from the API request’s $context variable. This $context variable is set by the API Gateway service itself. To review other parameters that are available to a mapping template, see API Gateway Mapping Template Reference. You may decide that including information like sourceIp or other headers could be valuable to you.

With this mapping template, no matter how your API is called, the only variance from the DynamoDB PutItem API call will be the values of pageId, userName, and message. Clients of your API will not be able to dictate which DynamoDB table is being targeted (because “Comments” is statically listed), and they will not have any control over the object structure that is specified for each item (each input variable is explicitly declared a string to the PutItem API).

With this mapping template, no matter how your API is called, the only variance from the DynamoDB PutItem API call will be the values of pageId, userName, and message. Clients of your API will not be able to dictate which DynamoDB table is being targeted (because “Comments” is statically listed), and they will not have any control over the object structure that is specified for each item (each input variable is explicitly declared a string to the PutItem API).

Back in the Method Execution pane click TEST.

Create an example Request Body that matches the API definition documented above and then choose Test. For example, your request body could be:

{
  "pageId":   "breaking-news-story-01-18-2016",
  "userName": "Just Saying Thank You",
  "message":  "I really enjoyed this story!!"
}

Navigate to the DynamoDB console and view the Comments table to show that the request really was successfully processed:

Great! Try including a few more sample items in the table to further test the Get Comments API.

If you deployed this API, you would be all set with a public API that has the ability to post public comments and store them in DynamoDB. For some use cases you may only want to collect data through a single API like this: for example, when collecting customer and visitor feedback, or for a public voting or polling system. But for this use case, we’ll demonstrate how to create another API to retrieve records from a DynamoDB table as well. Many of the details are similar to the process above.

Creating the Get Comments API

Return to the Resources view, choose the /comments resource you created earlier and choose Create Resource, like before.

This time, include a request path parameter to represent the pageId of the comments being retrieved. Input the following information and then choose Create Resource:

In Resources, choose your new /{pageId} resource and choose Create Method. The Get Comments API will be retrieving data from our DynamoDB table, so choose GET for the HTTP method.

In the method configuration screen choose Show advanced and then select AWS Service Proxy. Fill out the form to match the following. Make sure to use the appropriate AWS Region and IAM execution role; these should match what you previously created. Finally, choose Save.

Modify the Integration Request and create a new mapping template. This will transform the simple pageId path parameter on the GET request to the needed DynamoDB Query API, which requires an HTTP POST. Here is the mapping template:

{
    "TableName": "Comments",
    "IndexName": "pageId-index",
    "KeyConditionExpression": "pageId = :v1",
    "ExpressionAttributeValues": {
        ":v1": {
            "S": "$input.params('pageId')"
        }
    }
}

Now test your mapping template. Navigate to the Method Execution pane and choose the Test icon on the left. Provide one of the pageId values that you’ve inserted into your Comments table and choose Test.

You should see a response like the following; it is directly passing through the raw DynamoDB response:

Now you’re close! All you need to do before you deploy your API is to map the raw DynamoDB response to the similar JSON object structure that you defined on the Post Comment API.

This will work very similarly to the mapping template changes you already made. But you’ll configure this change on the Integration Response page of the console by editing the default mapping response’s mapping template.

Navigate to Integration Response and expand the 200 response code by choosing the arrow on the left. In the 200 response, expand the Mapping Templates section. In Content-Type choose application/json then choose the pencil icon next to Output Passthrough.

Now, create a mapping template that extracts the relevant pieces of the DynamoDB response and places them into a response structure that matches our use case:

#set($inputRoot = $input.path('$'))
{
    "comments": [
        #foreach($elem in $inputRoot.Items) {
            "commentId": "$elem.commentId.S",
            "userName": "$elem.userName.S",
            "message": "$elem.message.S"
        }#if($foreach.hasNext),#end
	#end
    ]
}

Now choose the check mark to save the mapping template, and choose Save to save this default integration response. Return to the Method Execution page and test your API again. You should now see a formatted response.

Now you have two working APIs that are ready to deploy! See our documentation to learn about how to deploy API stages.

But, before you deploy your API, here are some additional things to consider:

  • Authentication: you may want to require that users authenticate before they can leave comments. Amazon API Gateway can enforce IAM authentication for the APIs you create. To learn more, see Amazon API Gateway Access Permissions.
  • DynamoDB capacity: you may want to provision an appropriate amount of capacity to your Comments table so that your costs and performance reflect your needs.
  • Commenting features: Depending on how robust you’d like commenting to be on your site, you might like to introduce changes to the APIs described here. Examples are attributes that track replies or timestamp attributes.

Conclusion

Now you’ve got a fully functioning public API to post and retrieve public comments for your website. This API communicates directly with the Amazon DynamoDB API without you having to manage a single application component yourself!

Using Amazon API Gateway with microservices deployed on Amazon ECS

by Stefano Buliani | on | in Amazon API Gateway | | Comments

Rudy Krol Rudy Krol, AWS Solutions Architect

One convenient way to run microservices is to deploy them as Docker containers. Docker containers are quick to provision, easily portable, and provide process isolation. Amazon EC2 Container Service (Amazon ECS) provides a highly scalable, high performance container management service. This service supports Docker containers and enables you to easily run microservices on a managed cluster of Amazon EC2 instances.

Microservices usually expose REST APIs for use in front ends, third-party applications, and other microservices. A best practice is to manage these APIs with an API gateway. This provides a unique entry point for all of your APIs and also eliminates the need to implement API-specific code for things like security, caching, throttling, and monitoring for each of your microservices. You can implement this pattern in a few minutes using Amazon API Gateway. Amazon API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale.

In this post, we’ll explain how to use Amazon API Gateway to expose APIs for microservices running on Amazon ECS by leveraging the HTTP proxy mode of Amazon API Gateway. Amazon API Gateway can make proxy calls to any publicly accessible endpoint; for example, an Elastic Load Balancing load balancer endpoint in front of a microservice that is deployed on Amazon ECS. The following diagram shows the high level architecture described in this article:

You will see how you can benefit from stage variables to dynamically set the endpoint value depending on the stage of the API deployment.

In the first part of this post, we will walk through the AWS Management Console to create the dev environment (ECS cluster, ELB load balancers, and API Gateway configuration). The second part explains how to automate the creation of a production environment with AWS CloudFormation and AWS CLI.

Creating a dev environment with the AWS Management Console

Let’s begin by provisioning a sample helloworld microservice using the Getting Started wizard.

Sign in to Amazon ECS console. If this is the first time you’re using the Amazon ECS console, you’ll see a welcome page. Otherwise, you’ll see the console home page and the Create Cluster button.

Step 1: Create a task definition

  1. In the Amazon ECS console, do one of the following:
  2. Optional: (depending on the AWS Region) Deselect the Store container images securely with Amazon ECR checkbox and choose Continue.
  3. For Task definition name, type ecsconsole-helloworld.
  4. For Container name, type helloworld.
  5. Choose Advanced options and type the following text in the Command field: /bin/sh -c "echo '{ \"hello\" : \"world\" }' > /usr/local/apache2/htdocs/index.html && httpd-foreground"
  6. Choose Update and then choose Next step

Step 2: Configure service

  1. For Service name, type ecsconsole-service-helloworld.
  2. For Desired number of tasks, type 2.
  3. In the Elastic load balancing section, for Container name: host port, choose helloworld:80.
  4. For Select IAM role for service, choose Create new role or use an existing ecsServiceRole if you already created the required role.
  5. Choose Next Step.

Step 3: Configure cluster

  1. For Cluster name, type dev.
  2. For Number of instances, type 2.
  3. For Select IAM role for service, choose Create new role or use an existing ecsInstanceRole if you already created the required role.
  4. Choose Review and Launch and then choose Launch Instance & Run Service.

At this stage, after a few minutes of pending process, the helloworld microservice will be running in the dev ECS cluster with an ELB load balancer in front of it. Make note of the DNS Name of the ELB load balancer for later use; you can find it in the Load Balancers section of the EC2 console.

Configuring API Gateway

Now, let’s configure API Gateway to expose the APIs of this microservice. Sign in to the API Gateway console. If this is your first time using the API Gateway console, you’ll see a welcome page. Otherwise, you’ll see the API Gateway console home page and the Create API button.

Step 1: Create an API

  1. In the API Gateway console, do one of the following:
    • If Get Started Now is displayed, choose it.
    • If Create API is displayed, choose it.
    • If neither is displayed, in the secondary navigation bar, choose the API Gateway console home button, and then choose Create API.
  2. For API name, type EcsDemoAPI.
  3. Choose Create API.

Step 2: Create Resources

  1. In the API Gateway console, choose the root resource (/), and then choose Create Resource.
  2. For Resource Name, type HelloWorld.
  3. For Resource Path, leave the default value of /helloworld.
  4. Choose Create Resource.

Step 3: Create GET Methods

  1. In the Resources pane, choose /helloworld, and then choose Create Method.
  2. For the HTTP method, choose GET, and then save your choice.

Step 4: Specify Method Settings

  1. In the Resources pane, in /helloworld, choose GET.
  2. In the Setup pane, for Integration type, choose HTTP Proxy.
  3. For HTTP method, choose GET.
  4. For Endpoint URL, type http://${stageVariables.helloworldElb}
  5. Choose Save.

Step 5: Deploy the API

  1. In the Resources pane, choose Deploy API.
  2. For Deployment stage, choose New Stage.
  3. For Stage name, type dev.
  4. Choose Deploy.
  5. In the stage settings page, choose the Stage Variables tab.
  6. Choose Add Stage Variable, type helloworldElb for Name, type the DNS Name of the ELB in the Value field and then save.

Step 6: Test the API

  1. In the Stage Editor pane, next to Invoke URL, copy the URL to the clipboard. It should look something like this: https://.execute-api..amazonaws.com/dev
  2. Paste this URL in the address box of a new browser tab.
  3. Append /helloworld to the URL and validate. You should see the following JSON document: { "hello": "world" }

Automating prod environment creation

Now we’ll improve this setup by automating the creation of the prod environment. We use AWS CloudFormation to set up the prod ECS cluster, deploy the helloworld service, and create an ELB in front of the service. You can use the template with your preferred method:

Using AWS CLI

aws cloudformation create-stack --stack-name EcsHelloworldProd --template-url https://s3.amazonaws.com/rko-public-bucket/ecs_cluster.template --parameters ParameterKey=AsgMaxSize,ParameterValue=2 ParameterKey=CreateElasticLoadBalancer,ParameterValue=true ParameterKey=EcsInstanceType,ParameterValue=t2.micro

Using AWS console
Launch the AWS CloudFormation stack with the Launch Stack button below and use these parameter values:

  • AsgMaxSize: 2
  • CreateElasticLoadBalancer: true
  • EcsInstanceType: t2.micro

Configuring API Gateway with AWS CLI

We’ll use the API Gateway configuration that we created earlier and simply add the prod stage.

Here are the commands to create the prod stage and configure the stage variable to point to the ELB load balancer:

#Retrieve API ID
API_ID=$(aws apigateway get-rest-apis --output text --query "items[?name=='EcsDemoAPI'].{ID:id}")

#Retrieve ELB DNS name from CloudFormation Stack outputs
ELB_DNS=$(aws cloudformation describe-stacks --stack-name EcsHelloworldProd --output text --query "Stacks[0].Outputs[?OutputKey=='EcsElbDnsName'].{DNS:OutputValue}")

#Create prod stage and set helloworldElb variable
aws apigateway create-deployment --rest-api-id $API_ID --stage-name prod --variables helloworldElb=$ELB_DNS

You can then test the API on the prod stage using this simple cURL command:

AWS_REGION=$(aws configure get region)
curl https://$API_ID.execute-api.$AWS_REGION.amazonaws.com/prod/helloworld

You should see { "hello" : "world" } as the result of the cURL request. If the result is an error message like {"message": "Internal server error"}, verify that you have healthy instances behind your ELB load balancer. It can take some time to pass the health checks, so you’ll have to wait for a minute before trying again.

From the stage settings page you also have the option to export the API configuration to a Swagger file, including the API Gateway extension. Exporting the API configuration as a Swagger file enables you to keep the definition in your source repository. You can then import it at any time, either by overwriting the existing API or by importing it as a brand new API. The API Gateway import tool helps you parse the Swagger definition and import it into the service.

Conclusion

In this post, we looked at how to use Amazon API Gateway to expose APIs for microservices deployed on Amazon ECS. The integration with the HTTP proxy mode pointing to ELB load balancers is a simple method to ensure the availability and scalability of your microservice architecture. With ELB load balancers, you don’t have to worry about how your containers are deployed on the cluster.

We also saw how stage variables help you connect your APIs on different ELB load balancers, depending on the stage where the API is deployed.

Scheduling SSH jobs using AWS Lambda

by Vyom Nagrani | on | | Comments

Puneet Agarwal Puneet Agarwal, AWS Solution Architect

 
With the addition of the Scheduled Events feature, you can now set up AWS Lambda to invoke your code on a regular, scheduled basis. You can now schedule various AWS API activities in your account (such as creation or deletion of CloudFormation stacks, EBS volume snapshots, etc.) with AWS Lambda. In addition, you can use AWS Lambda to connect to your Linux instances by using SSH and run desired commands and scripts at regular time intervals. This is especially useful for scheduling tasks (e.g., system updates, log cleanups, maintenance tasks) on your EC2 instances, when you don’t want to manage cron or external schedulers for a dynamic fleet of instances.

In the following example, you will run a simple shell script that prints “Hello World” to an output file on instances tagged as “Environment=Dev” in your account. You will trigger this shell script through a Lambda function written in Python 2.7.

At a high level, this is what you will do in this example:

  1. Create a Lambda function to fetch IP addresses of EC2 instances with “Environment=Dev” tag. This function will serve as a trigger function. This trigger function will invoke a worker function, for each IP address. The worker function will connect to EC2 instances using SSH and run a HelloWorld.sh script.
  2. Configure Scheduled Event as an event source to invoke the trigger function every 15 minutes.
  3. Create a Python deployment package (.zip file), with worker function code and other dependencies.
  4. Upload the worker function package to AWS Lambda.

 

Advantages of Scheduled Lambda Events over Ubiquitous Cron

Cron is indeed simple and well understood, which makes it a very popular tool for running scheduled operations. However, there are many architectural benefits that make scheduled Lambda functions and custom scripts a better choice in certain scenarios:

  • Decouple job schedule and AMI: If your cron jobs are part of an AMI, each schedule change requires you to create a new AMI version, and update existing instances running with that AMI. This is both cumbersome and time-consuming. Using scheduled Lambda functions, you can keep the job schedule outside of your AMI and change the schedule on the fly.
  • Flexible targeting of EC2 instances: By abstracting the job schedule from AMI and EC2 instances, you can flexibly target a subset of your EC2 instance fleet based on tags or other conditions. In this example, we are targeting EC2 instances with the “Environment=Dev” tag.
  • Intelligent scheduling: With scheduled Lambda functions, you can add custom logic to you abstracted job scheduler.

While there are many ways of achieving the above benefits, scheduled Lambda functions are an easy-to-use option in your toolkit.

 

Trigger Function

This is a simple Python function that extracts IP addresses of all instances with the “Environment=Dev” tag and invokes the worker function for each of the instances. Decoupling the trigger function from the worker function enables a simpler programming model for parallel execution of tasks on multiple instances.

Steps:

  1. Sign in to the AWS Management Console and open the AWS Lambda console.
  2. Choose Create a Lambda function.
  3. On the Select blueprint page, type cron in the search box.
  4. Choose lambda-canary.
  5. On the Configure event sources page, Event source type defaults to Scheduled Event.  You can create a new schedule by entering a name for the schedule, or can select one of your existing schedules.  For Schedule expression, you can specify a fixed rate (number of minutes, hours, or days between invocations) or you can specify a cron-like expression. Note that rate frequencies of less than five minutes are not supported at this time.
     Lambda SSH Configure Events 
  6. Choose Next. The Configure Function page appears.
      
    Here, you can enter the name and description of your function. Replace the sample code here with the following code.
    trigger_function.py

    import boto3
    
    def trigger_handler(event, context):
        #Get IP addresses of EC2 instances
        client = boto3.client('ec2')
        instDict=client.describe_instances(
                Filters=[{'Name':'tag:Environment','Values':['Dev']}]
            )
    
        hostList=[]
        for r in instDict['Reservations']:
            for inst in r['Instances']:
                hostList.append(inst['PublicIpAddress'])
    
        #Invoke worker function for each IP address
        client = boto3.client('lambda')
        for host in hostList:
            print "Invoking worker_function on " + host
            invokeResponse=client.invoke(
                FunctionName='worker_function',
                InvocationType='Event',
                LogType='Tail',
                Payload='{"IP":"'+ host +'"}'
            )
            print invokeResponse
    
        return{
            'message' : "Trigger function finished"
        }
  7. After adding the trigger code in the console, create the appropriate execution role and set a timeout. Note that the execution role must have permissions to execute EC2 DescribeInstances and invoke Lambda functions. Example IAM Policies for the trigger Lambda role are as follows:
  8. Choose Next, choose Enable later, and then choose Create function.

 

Worker Function

Next, put together the worker Lambda function that connects to an Amazon EC2 instance using SSH, and then run the HelloWorld.sh script. To initiate SSH connections from the Lambda client, use the Paramiko library. Paramiko is an open source Python implementation of the SSHv2 protocol, providing both client and server functionality. Worker function will irst download a private key file from a secured Amazon S3 bucket to the local /tmp folder, and then use that key file to connect to the EC2 instances by using SSH. You must keep your private key secure and make sure that only the worker function has read access to the file on S3. Assuming that EC2 instances have S3 access permissions through an EC2 role, worker function will download the HelloWorld.sh script from S3 and execute it locally on each EC2 instance.

Steps:

  1. Create worker_function.py file on your local Linux machine or on an EC2 instance using following code
    worker_function.py

    import boto3
    import paramiko
    def worker_handler(event, context):
    
        s3_client = boto3.client('s3')
        #Download private key file from secure S3 bucket
        s3_client.download_file('s3-key-bucket','keys/keyname.pem', '/tmp/keyname.pem')
    
        k = paramiko.RSAKey.from_private_key_file("/tmp/keyname.pem")
        c = paramiko.SSHClient()
        c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
        host=event['IP']
        print "Connecting to " + host
        c.connect( hostname = host, username = "ec2-user", pkey = k )
        print "Connected to " + host
    
        commands = [
            "aws s3 cp s3://s3-bucket/scripts/HelloWorld.sh /home/ec2-user/HelloWorld.sh",
            "chmod 700 /home/ec2-user/HelloWorld.sh",
            "/home/ec2-user/HelloWorld.sh"
            ]
        for command in commands:
            print "Executing {}".format(command)
            stdin , stdout, stderr = c.exec_command(command)
            print stdout.read()
            print stderr.read()
    
        return
        {
            'message' : "Script execution completed. See Cloudwatch logs for complete output"
        }
    

     
    Now, creating a deployment package is straightforward. For this example, create a deployment package using Virtualenv.

  2. Install Virtualenv on your local Linux machine or an EC2 instance.
    $ pip install virtualenv
  3. Create a virtual environment named “helloworld-env“, which will use a Python2.7 interpreter.
    $ virtualenv –p /usr/bin/python2.7 path/to/my/helloworld-env
  4. Activate helloworld-env.
    source path/to/my/helloworld-env/bin/activate
  5. Install dependencies.
    $pip install pycrypto

    PyCrypto provides the low-level (C-based) encryption algorithms we need to implement the SSH protocol.

    $pip install paramiko
  6. Add worker_function.py to the zip file.
    $zip path/to/zip/worker_function.zip worker_function.py
  7. Add dependencies from helloworld-env to the zip file.
    $cd path/to/my/helloworld-env/lib/python2.7/site-packages
    $zip –r path/to/zip/worker_function.zip
    $cd path/to/my/helloworld-env/lib64/python2.7/site-packages
    $zip –r path/to/zip/worker_function.zip

    Using the AWS console (skip the blueprint step) or AWS CLI, create a new Lambda function named worker_function and upload worker_function.zip.
      
    Example IAM policies for the worker Lambda role are as follows:

    Caution: To keep your keys secure, make sure no other IAM users or roles, other than intended users, have access to this S3 bucket.

 

Upload key and script to S3

All you need to do now is upload your key and script file to S3 buckets and then you are ready to run the example.

Steps:

  1. Upload HellowWorld.sh to an appropriate S3 bucket (e.g., s3://s3-bucket/scripts/). HelloWorld.sh is a simple shell script that prints “Hello World from instanceID” to a log file and copies that log file to your S3 folder.
     
    HelloWorld.sh

    #Get instanceId from metadata
    instanceid=`wget -q -O - http://instance-data/latest/meta-data/instance-id`
    LOGFILE="/home/ec2-user/$instanceid.$(date +"%Y%m%d_%H%M%S").log"
    
    #Run Hello World and redirect output to a log file
    echo "Hello World from $instanceid" > $LOGFILE
    
    #Copy log file to S3 logs folder
    aws s3 cp $LOGFILE s3://s3-bucket/logs/
    
  2. Upload keyname.pem file, which is your private key to connect to EC2 instances, to a secure S3 bucket (e.g., s3://s3-key-bucket/keys/keyname.pem). To keep your keys secure, make sure no IAM users or roles, other than intended users and the Lambda worker role, have access to this S3 bucket.

 

Running the example

As a final step, enable your trigger_function event source by choosing trigger_function from the list of Lambda functions, choosing the Event sources tab, and clicking Disabled in the State column.

You can now test your newly created Lambda functions and monitor execution logs. AWS Lambda logs all requests handled by your function and automatically stores logs generated by your code using Amazon CloudWatch Logs. The following screenshots show my CloudWatch Logs after completing the preceding steps.

Trigger function log in CloudWatch Logs:
  

Worker function log in Cloudwatch Logs:
  

Log files that were generated in my S3 bucket:
  

 

Other considerations

  • With the new Lambda VPC support, you can connect to your EC2 instances running in your private VPC by providing private subnet IDs and EC2 security group IDs as part of your Lambda function configuration.
  • AWS Lambda now supports a maximum function duration of 5 minutes, and so you can use scheduled Lambda functions to run jobs that are expected to finish within 5 minutes. For longer running jobs, you can use following syntax to run jobs in the background so that the Lambda function doesn’t wait for command execution to finish.
    c.exec_command(cmd + ' > /dev/null 2>&1 &')

Introducing custom authorizers in Amazon API Gateway

by Stefano Buliani | on | in Amazon API Gateway | | Comments

Today Amazon API Gateway is launching custom request authorizers. With custom request authorizers, developers can authorize their APIs using bearer token authorization strategies, such as OAuth using an AWS Lambda function. For each incoming request, API Gateway verifies whether a custom authorizer is configured, and if so, API Gateway calls the Lambda function with the authorization token. You can use Lambda to implement various authorization strategies (e.g., JWT verification, OAuth provider callout). Custom authorizers must return AWS Identity and Access Management (IAM) policies. These policies are used to authorize the request. If the policy returned by the authorizer is valid, API Gateway caches the returned policy associated with the incoming token for up to 1 hour so that your Lambda function doesn’t need to be invoked again.

Configuring custom authorizers

You can configure custom authorizers from the API Gateway console or using the APIs. In the console, we have added a new section called custom authorizers inside your API.

An API can have multiple custom authorizers and each method within your API can use a different authorizer. For example, the POST method for the /login resource can use a different authorizer than the GET method for the /pets resource.

To configure an authorizer you must specify a unique name and select a Lambda function to act as the authorizer. You also need to indicate which field of the incoming request contains your bearer token. API Gateway will pass the value of the field to your Lambda authorizer. For example, in most cases your bearer token will be in the Authorization header; you can select this field using the method.request.header.Authorization mapping expression. Optionally, you can specify a regular expression to validate the incoming token before your authorizer is triggered and you can also specify a TTL for the policy cache.

Once you have configured a custom authorizer, you can simply select it from the authorization dropdown in the method request page.

The authorizer function in AWS Lambda

API Gateway invokes the Lambda authorizer by passing in the Lambda event. The Lambda event includes the bearer token from the request and full ARN of the API method being invoked. The authorizer Lambda event looks like this:

{
    "type":"TOKEN",
    "authorizationToken":"<Incoming bearer token>",
    "methodArn":"arn:aws:execute-api:<Region id>:<Account id>:<API id>/<Stage>/<Method>/<Resource path>"
}

Your Lambda function must return a valid IAM policy. API Gateway uses this policy to make authorization decisions for the token. For example, if you use JWT tokens, you can use the Lambda function to open the token and then generate a policy based on the scopes included in the token. Later today we will publish authorizer Lambda blueprints for Node.js and Python that include a policy generator object. This sample function uses AWS Key Management Service (AWS KMS) to decrypt the signing key for the token, the nJwt library for Node.js to validate a token, and then the policy generator object included in the Lambda blueprint to generate and return a valid policy to Amazon API Gateway.

var nJwt = require('njwt');
var AWS = require('aws-sdk');
var signingKey = "CiCnRmG+t+ BASE 64 ENCODED ENCRYPTED SIGNING KEY Mk=";

exports.handler = function(event, context) {
  console.log('Client token: ' + event.authorizationToken);
  console.log('Method ARN: ' + event.methodArn);
  var kms = new AWS.KMS();

  var decryptionParams = {
    CiphertextBlob : new Buffer(signingKey, 'base64')
  }

  kms.decrypt(decryptionParams, function(err, data) {
    if (err) {
      console.log(err, err.stack);
      context.fail("Unable to load encryption key");
    } else {
      key = data.Plaintext;

      try {
        verifiedJwt = nJwt.verify(event.authorizationToken, key);
        console.log(verifiedJwt);

        // parse the ARN from the incoming event
        var apiOptions = {};
        var tmp = event.methodArn.split(':');
        var apiGatewayArnTmp = tmp[5].split('/');
        var awsAccountId = tmp[4];
        apiOptions.region = tmp[3];
        apiOptions.restApiId = apiGatewayArnTmp[0];
        apiOptions.stage = apiGatewayArnTmp[1];
       
        policy = new AuthPolicy(verifiedJwt.body.sub, awsAccountId, apiOptions);

        if (verifiedJwt.body.scope.indexOf("admins") > -1) {
           policy.allowAllMethods();
        } else {
          policy.allowMethod(AuthPolicy.HttpVerb.GET, "*");
          policy.allowMethod(AuthPolicy.HttpVerb.POST, "/users/" + verifiedJwt.body.sub);
        }

        context.succeed(policy.build());

      } catch (ex) {
        console.log(ex, ex.stack);
        context.fail("Unauthorized");
      }
    }
  });
};

You can also generate a policy in your code instead of using the provided AuthPolicy object. Valid policies include the principal identifier associated with the token and a named IAM policy that can be cached and used to authorize future API calls with the same token. The principalId will be accessible in the mapping template.

{
  "principalId": "xxxxxxx", // the principal user identification associated with the token send by the client
  "policyDocument": { // example policy shown below, but this value is any valid policy
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "execute-api:Invoke"
        ],
        "Resource": [
          "arn:aws:execute-api:us-east-1:xxxxxxxxxxxx:xxxxxxxx:/test/*/mydemoresource/*"
        ]
      }
    ]
  }
}

To learn more about the possible options in a policy, see the public access permissions reference for API Gateway. All of the variables that are normally available in IAM policies are also available to custom authorizer policies. For example, you could restrict access using the ${aws:sourceIp} variable. To learn more, see the policy variables reference.

Because policies are cached for a configured TTL, API Gateway only invokes your Lambda function the first time it sees a token; all of the calls that follow during the TTL period are authorized by API Gateway using the cached policy.

Conclusion

You can use custom authorizers in API Gateway to support any bearer token. This allows you to authorize access to your APIs using tokens from an OAuth flow or SAML assertions. Further, you can leverage all of the variables available to IAM policies without setting up your API to use IAM authorization.

Custom authorizers are available in the API Gateway console and APIs now, and authorizer Lambda blueprints will follow later today. Get in touch through the API Gateway forum if you have questions or feedback about custom authorizers.

Using API Gateway mapping templates to handle changes in your back-end APIs

by Stefano Buliani | on | in Amazon API Gateway | | Comments

Maitreya Ranganath Maitreya Ranganath, AWS Solutions Architect

Changes to APIs are always risky, especially if changes are made in ways that are not backward compatible. In this blog post, we show you how to use Amazon API Gateway mapping templates to isolate your API consumers from API changes. This enables your API consumers to migrate to new API versions on their own schedule.

For an example scenario, we start with a very simple Store Front API with one resource for orders and one GET method. For this example, the API target is implemented in AWS Lambda to keep things simple – but you can of course imagine the back end being your own endpoint.

The structure of the API V1 is:

Method:		GET
Path:		/orders
Query Parameters:
	start = timestamp
	end = timestamp

Response:
[
  {
    “orderId” : string,
    “orderTs” : string,
    “orderAmount” : number
  }
]

The initial version (V1) of the API was implemented when there were few orders per day. The API was not paginated; if the number of orders that match the query is larger than 5, an error returns. The API consumer must then submit a request with a smaller time range.

The API V1 is exposed through API Gateway and you have several consumers of this API in Production.

After you upgrade the back end, the API developers make a change to support pagination. This makes the API more scalable and allows the API consumers to handle large lists of orders by paging through them with a token. This is a good design change but it breaks backward compatibility. It introduces a challenge because you have a large base of API consumers using V1 and their code can’t handle the changed nesting structure of this response.

The structure of API V2 is:

Method:		GET
Path:		/orders
Query Parameters:
	start =	timestamp
	end =	timestamp
	token =	string (optional)

Response:
{
  “nextToken” : string,
  “orders” : [
    {
      “orderId” : string,
      “orderTs” :  string
      “orderAmount” : number
    }
  ]
}

Using mapping templates, you can isolate your API consumers from this change: your existing V1 API consumers will not be impacted when you publish V2 of the API in parallel. You want to let your consumers migrate to V2 on their own schedule.

We’ll show you how to do that in this blog post. Let’s get started.

Deploying V1 of the API

To deploy V1 of the API, create a simple Lambda function and expose that through API Gateway:

  1. Sign in to the AWS Lambda console.
  2. Choose Create a Lambda function.
  3. In Step 1: Select blueprint, choose Skip; you’ll enter the details for the Lambda function manually.
  4. In Step 2: Configure function, use the following values:
    • In Name, type getOrders.
    • In Description, type Returns orders for a time-range.
    • In Runtime, choose Node.js.
    • For Code entry type, choose Edit code inline. Copy and paste the code snippet below into the code input box.
MILISECONDS_DAY = 3600*1000*24;

exports.handler = function(event, context) {
    console.log('start =', event.start);
    console.log('end =', event.end);
    
    start = Date.parse(decodeURIComponent(event.start));
    end = Date.parse(decodeURIComponent(event.end));
    
    if(isNaN(start)) {
        context.fail("Invalid parameter 'start'");
    }
    if(isNaN(end)) {
        context.fail("Invalid parameter 'end'");
    }

    duration = end - start;
    
    if(duration  5 * MILISECONDS_DAY) {
        context.fail("Too many results, try your request with a shorter duration");
    }
    
    orderList = [];
    count = 0;
    
    for(d = start; d < end; d += MILISECONDS_DAY) {
        order = {
            "orderId" : "order-" + count,
            "orderTs" : (new Date(d).toISOString()),
            "orderAmount" : Math.round(Math.random()*100.0)
        };
        count += 1;
        orderList.push(order);
    }
    
    console.log('Generated', count, 'orders');
    context.succeed(orderList);
};
    • In Handler, leave the default value of index.handler.
    • In Role, choose Basic execution role or choose an existing role if you’ve created one for Lambda before.
    • In Advanced settings, leave the default values and choose Next.

Finally, review the settings in the next page and choose Create function.

Your Lambda function is now created. You can test it by sending a test event. Enter the following for your test event:

{
  "start": "2015-10-01T00:00:00Z",
  "end": "2015-10-04T00:00:00Z"
}

Check the execution result and log output to see the results of your test.

Next, choose the API endpoints tab and then choose Add API endpoint. In Add API endpoint, use the following values:

  • In API endpoint type, choose API Gateway
  • In API name, type StoreFront
  • In Resource name, type /orders
  • In Method, choose GET
  • In Deployment stage, use the default value of prod
  • In Security, choose Open to allow the API to be publicly accessed
  • Choose Submit to create the API

The API is created and the API endpoint URL is displayed for the Lambda function.

Next, switch to the API Gateway console and verify that the new API appears on the list of APIs. Choose StoreFront to view its details.

To view the method execution details, in the Resources pane, choose GET. Choose Integration Request to edit the method properties.

On the Integration Request details page, expand the Mapping Templates section and choose Add mapping template. In Content-Type, type application/json and choose the check mark to accept.

Choose the edit icon to the right of Input passthrough. From the drop down, choose Mapping template and copy and paste the mapping template text below into the Template input box. Choose the check mark to create the template.

{
#set($queryMap = $input.params().querystring)

#foreach( $key in $queryMap.keySet())
  "$key" : "$queryMap.get($key)"
  #if($foreach.hasNext),#end
#end
}

This step is needed because the Lambda function requires its input as a JSON document. The mapping template takes query string parameters from the GET request and creates a JSON input document. Mapping templates use Apache Velocity, expose a number of utility functions, and give you access to all of the incoming requests data and context parameters. You can learn more from the mapping template reference page.

Back to the GET method configuration page, in the left pane, choose the GET method and then open the Method Request settings. Expand the URL Query String Parameters section and choose Add query string. In Name, type start and choose the check mark to accept. Repeat the process to create a second parameter named end.

From the GET method configuration page, in the top left, choose Test to test your API. Type the following values for the query string parameters and then choose Test:

  • In start, type 2015-10-01T00:00:00Z
  • In end, type 2015-10-04T00:00:00Z

Verify that the response status is 200 and the response body contains a JSON response with 3 orders.

Now that your test is successful, you can deploy your changes to the production stage. In the Resources pane, choose Deploy API. In Deployment stage, choose prod. In Deployment description, type a description of the deployment, and then choose Deploy.

The prod Stage Editor page appears, displaying the Invoke URL. In the CloudWatch Settings section, choose Enable CloudWatch Logs so you can see logs and metrics from this stage. Keep in mind that CloudWatch logs are charged to your account separately from API Gateway.

You have now deployed an API that is backed by V1 of the Lambda function.

Testing V1 of the API

Now you’ll test V1 of the API with curl and confirm its behavior. First, copy the Invoke URL and add the query parameters ?start=2015-10-01T00:00:00Z&end=2015-10-04T00:00:00Z and make a GET invocation using curl.

$ curl -s "https://your-invoke-url-and-path/orders?start=2015-10-01T00:00:00Z&end=2015-10-04T00:00:00Z" 

[
  {
    "orderId": "order-0",
    "orderTs": "2015-10-01T00:00:00.000Z",
    "orderAmount": 82
  },
  {
    "orderId": "order-1",
    "orderTs": "2015-10-02T00:00:00.000Z",
    "orderAmount": 3
  },
  {
    "orderId": "order-2",
    "orderTs": "2015-10-03T00:00:00.000Z",
    "orderAmount": 75
  }
]

This should output a JSON response with 3 orders. Next, check what happens if you use a longer time-range by changing the end timestamp to 2015-10-15T00:00:00Z:

$ curl -s "https://your-invoke-url-and-path/orders?start=2015-10-01T00:00:00Z&end=2015-10-15T00:00:00Z"
 
{
  "errorMessage": "Too many results, try your request with a shorter duration"
}

You see that the API returns an error indicating the time range is too long. This is correct V1 API behavior, so you are all set.

Updating the Lambda Function to V2

Next, you will update the Lambda function code to V2. This simulates the scenario of the back end of your API changing in a manner that is not backward compatible.

Switch to the Lambda console and choose the getOrders function. In the code input box, copy and paste the code snippet below. Be sure to replace all of the existing V1 code with V2 code.

MILISECONDS_DAY = 3600*1000*24;

exports.handler = function(event, context) {
    console.log('start =', event.start);
    console.log('end =', event.end);
    
    start = Date.parse(decodeURIComponent(event.start));
    end = Date.parse(decodeURIComponent(event.end));
    
    token = NaN;
    if(event.token) {
        s = new Buffer(event.token, 'base64').toString();
        token = Date.parse(s);
    }
    

    if(isNaN(start)) {
        context.fail("Invalid parameter 'start'");
    }
    if(isNaN(end)) {
        context.fail("Invalid parameter 'end'");
    }
    if(!isNaN(token)) {
        start = token;
    }

    duration = end - start;
    
    if(duration <= 0) {
        context.fail("Invalid parameters 'end' must be greater than 'start'");
    }
    
    orderList = [];
    count = 0;
    
    console.log('start=', start, ' end=', end);
    
    for(d = start; d < end && count < 5; d += MILISECONDS_DAY) {
        order = {
            "orderId" : "order-" + count,
            "orderTs" : (new Date(d).toISOString()),
            "orderAmount" : Math.round(Math.random()*100.0)
        };
        count += 1;
        orderList.push(order);
    }

    nextToken = null;
    if(d < end) {
        nextToken = new Buffer(new Date(d).toISOString()).toString('base64');
    }
    
    console.log('Generated', count, 'orders');

    result = {
        orders : orderList,
    };

    if(nextToken) {
        result.nextToken = nextToken;
    }
    context.succeed(result);
};

Choose Save to save V2 of the code. Then choose Test. Note that the output structure is different in V2 and there is a second level of nesting in the JSON document. This represents the updated V2 output structure that is different from V1.

Next, repeat the curl tests from the previous section. First, do a request for a short time duration. Notice that the response structure is nested differently from V1 and this is a problem for our API consumers that expect V1 responses.

$ curl -s "https://your-invoke-url-and-path/orders?start=2015-10-01T00:00:00Z&end=2015-10-04T00:00:00Z" 

{
  "orders": [
    {
      "orderId": "order-0",
      "orderTs": "2015-10-01T00:00:00.000Z",
      "orderAmount": 8
    },
    {
      "orderId": "order-1",
      "orderTs": "2015-10-02T00:00:00.000Z",
      "orderAmount": 92
    },
    {
      "orderId": "order-2",
      "orderTs": "2015-10-03T00:00:00.000Z",
      "orderAmount": 84
    }
  ]
}

Now, repeat the request for a longer time range and you’ll see that instead of an error message, you now get the first page of information with 5 orders and a nextToken that will let you request the next page. This is the paginated behavior of V2 of the API.

$ curl -s "https://your-invoke-url-and-path/orders?start=2015-10-01T00:00:00Z&end=2015-10-15T00:00:00Z"

{
  "orders": [
    {
      "orderId": "order-0",
      "orderTs": "2015-10-01T00:00:00.000Z",
      "orderAmount": 62
    },
    {
      "orderId": "order-1",
      "orderTs": "2015-10-02T00:00:00.000Z",
      "orderAmount": 59
    },
    {
      "orderId": "order-2",
      "orderTs": "2015-10-03T00:00:00.000Z",
      "orderAmount": 21
    },
    {
      "orderId": "order-3",
      "orderTs": "2015-10-04T00:00:00.000Z",
      "orderAmount": 95
    },
    {
      "orderId": "order-4",
      "orderTs": "2015-10-05T00:00:00.000Z",
      "orderAmount": 84
    }
  ],
  "nextToken": "MjAxNS0xMC0wNlQwMDowMDowMC4wMDBa"
}

It is clear from these tests that V2 will break the current V1 consumer’s code. Next, we show how to isolate your V1 consumers from this change using API Gateway mapping templates.

Cloning the API

Because you want both V1 and V2 of the API to be available simultaneously to your API consumers, you first clone the API to create a V2 API. You then modify the V1 API to make it behave as your V1 consumers expect.

Go back to the API Gateway console, and choose Create API. Configure the new API with the following values:

  • In API name, type StoreFrontV2
  • In Clone from API, choose StoreFront
  • In Description, type a description
  • Choose Create API to clone the StoreFront API as StoreFrontV2

Open the StoreFrontV2 API and choose the GET method of the /orders resource. Next, choose Integration Request. Choose the edit icon next to the getOrders Lambda function name.

Keep the name as getOrders and choose the check mark to accept. In the pop up, choose OK to allow the StoreFrontV2 to invoke the Lambda function.

Once you have granted API Gateway permissions to access your Lambda function, choose Deploy API. In Deployment stage, choose New stage. In Stage name, type prod, and then choose Deploy. Now you have a new StoreFrontV2 API that invokes the same Lambda function. Confirm that the API has V2 behavior by testing it with curl. Use the Invoke URL for the StoreFrontV2 API instead of the previously used Invoke URL.

Update the V1 of the API

Now you will use mapping templates to update the original StoreFront API to preserve V1 behavior. This enables existing consumers to continue to consume the API without having to make any changes to their code.

Navigate to the API Gateway console, choose the StoreFront API and open the GET method of the /orders resource. On the Method Execution details page, choose Integration Response.

Expand the default response mapping (HTTP status 200), and expand the Mapping Templates section. Choose Add Mapping Template.

In Content-type, type application/json and choose the check mark to accept. Choose the edit icon next to Output passthrough to edit the mapping templates. Select Mapping template from the drop down and copy and paste the mapping template below into the Template input box.

#set($nextToken = $input.path('$.nextToken'))

#if($nextToken && $nextToken.length() != 0)
  {
    "errorMessage" : "Too many results, try your request with a shorter duration"
  }
#else
  $input.json('$.orders[*]')
#end

Choose the check mark to accept and save. The mapping template transforms the V2 output from the Lambda function into the original V1 response. The mapping template also generates an error if the V2 response indicates that there are more results than can fit in one page. This emulates V1 behavior.

Finally click Save on the response mapping page. Deploy your StoreFront API and choose prod as the stage to deploy your changes.

Verify V1 behavior

Now that you have updated the original API to emulate V1 behavior, you can verify that using curl again. You will essentially repeat the tests from the earlier section. First, confirm that you have the Invoke URL for the original StoreFront API. You can always find the Invoke URL by looking at the stage details for the API.

Try a test with a short time range.

$ curl -s "https://your-invoke-url-and-path/orders?start=2015-10-01T00:00:00Z&end=2015-10-04T00:00:00Z"

[
  {
    "orderId": "order-0",
    "orderTs": "2015-10-01T00:00:00.000Z",
    "orderAmount": 50
  },
  {
    "orderId": "order-1",
    "orderTs": "2015-10-02T00:00:00.000Z",
    "orderAmount": 16
  },
  {
    "orderId": "order-2",
    "orderTs": "2015-10-03T00:00:00.000Z",
    "orderAmount": 14
  }
]

Try a test with a longer time range and note that the V1 behavior of returning an error is recovered.

$ curl -s "https://your-invoke-url-and-path/orders?start=2015-10-01T00:00:00Z&end=2015-10-15T00:00:00Z"

{
  "errorMessage": "Too many results, try your request with a shorter duration"
}

Congratulations, you have successfully used Amazon API Gateway mapping templates to expose both V1 and V2 versions of your API allowing your API consumers to migrate to V2 on their own schedule.

Be sure to delete the two APIs and the AWS Lambda function that you created for this walkthrough to avoid being charged for their use.

Powering your Amazon ECS Clusters with Spot Fleet

by Chris Barclay | on | in Amazon EC2, Amazon ECS | | Comments

My colleague Drew Dennis sent a nice guest post that shows how to use Amazon ECS with Spot fleet.

There are advantages to using on-demand EC2 instances. However, for many workloads, such as stateless or task-based scenarios that simply run as long as they need to run and are easily replaced with subsequent identical processes, Spot fleet can provide additional compute resources that are more economical. Furthermore, Spot fleet attempts to replace any terminated instances to maintain the requested target capacity.

Amazon ECS is a highly scalable, high performance, container management service that supports Docker containers and allows you to run applications on a managed cluster of Amazon EC2 instances easily. ECS already handles the placement and scheduling of containers on EC2 instances. When combined with Spot fleet, ECS can deliver significant savings over EC2 on-demand pricing.

Why Spot fleet?

Amazon EC2 Spot instances allow you to bid on spare Amazon EC2 computing capacity. Because Spot instances are often available at a discount compared to On-Demand pricing, you can significantly reduce the cost of running your applications. Spot fleet enables customers to request a collection of Spot instances across multiple Availability Zones and instance types with a single API call.

The Spot fleet API call can specify a target capacity and an allocation strategy. The two available allocation strategies are lowest price and diversified. Lowest price means the instances are provisioned based solely on the lowest current Spot price available while diversified fulfills the request equally across multiple Spot pools (instances of the same type and OS within an Availability Zone) to help mitigate the risk of a sudden Spot price increase. For more information, see How Spot Fleet Works.

Using Spot fleet

The Spot fleet console is available at https://console.aws.amazon.com/ec2spot/home. It provides a simple approach to creating a Spot fleet request and setting up all necessary attributes of the request, including creating an IAM role and base64-encoding user data. The console also provides the option to download the request JSON, which can be used with the CLI if desired.

If you prefer not to use the Spot fleet console, you need to make sure you have an IAM role created with the necessary privileges for the Spot fleet request to bid on, launch, and terminate instances. Note that the iam:PassRole action is needed in this scenario so that Spot fleet can launch instances with a role to participate in an ECS cluster. You need to make sure that you have an AWS SDK or the AWS CLI installed.

This post assumes you are familiar with the process of creating an ECS cluster, creating an ECS task definition, and launching the task definition as a manual task or service. If not, see the ECS documentation.

Creating a Spot fleet request

Before you make your Spot fleet request, make sure you know the instance types, Availability Zones, and bid prices that you plan to request. Note that individual bid prices for various instance types can be used in a Spot fleet request. When you have decided on these items, you are ready to begin the request. In the screenshot below, a fleet request is being created for four c4.large instances using an Amazon Linux ECS-optimized AMI. You can obtain the most up-to-date list of ECS optimized AMIs by region in the Launching an Amazon ECS Container Instance topic.

Notice the very useful warnings if your bid price is below the minimum price to initially launch the instance. From here, you can also access the Spot pricing history and Spot Bid Advisor to better understand past pricing volatility. After choosing Next, you see options to spread the request across multiple zones, specify values for User data, and define other request attributes as shown below. In this example, the user data sets the ECS cluster to which the ECS container agent connects.

Other examples could create a Spot fleet request that contains multiple instance types with Spot price overrides for each instance type in a single Availability Zone. The allocation strategy could still be diversified, which means it will pull equally from the two instance-type pools. This could easily be combined with the previous example to create a fleet request that spans multiple Availability Zones and instance types, further mitigating the risk of Spot instance termination.

Running ECS tasks and services on your Spot fleet

After your instances have joined your ECS cluster, you are ready to start tasks or services on them. This involves first creating a task definition. For more information, see the Docker basics walkthrough. After the task definition is created, you can run the tasks manually, or schedule them as a long-running process or service.

In the case of an ECS service, if one of the Spot fleet instances is terminated due to a Spot price interruption, ECS re-allocates the running containers on another EC2 instance within the cluster to maintain the desired number of running tasks, assuming that sufficient resources are available.

If not, within a few minutes, the instance is replaced with a new instance by the Spot fleet request. The new instance is launched according to the configuration of the initial Spot fleet request and rejoins the cluster to participate and run any outstanding containers needed to meet the desired quantity.

In summary, Spot fleet provides an effective and economical way to add instances to an ECS cluster. Because a Spot fleet request can span multiple instance types and Availability Zones, and will always try to maintain a target number of instances, it is a great fit for running stateless containers and adding inexpensive capacity to your ECS clusters.

Auto Scaling and Spot fleet requests

Auto Scaling has proven to be a great way to add or remove EC2 capacity to many AWS workloads. ECS supports Auto Scaling on cluster instances and provides CloudWatch metrics to help facilitate this scenario. For more information, see Tutorial: Scaling Container Instances with CloudWatch Alarms. The combination of Auto Scaling and Spot fleet provides a nice way to have a pool of fixed capacity and variable capacity on demand while reducing costs.

Currently, Spot fleet requests cannot be integrated directly with Auto Scaling policies as they can with Spot instance requests. However, the Spot fleet API does include an action called ModifySpotFleetRequest that can change the target capacity of your request. The Dynamic Scaling with EC2 Spot Fleet blog post shows an example of a scenario that leverages CloudWatch metrics to invoke a Lambda function and change the Spot fleet target capacity. Using ModifySpotFleetRequest can be a great way to not only fine-tune your fleet requests, but also minimize over-provisioning and further lower costs.

Conclusion

Amazon ECS manages clusters of EC2 instances for reliable state management and flexible container scheduling. Docker containers lend themselves to flexible and portable application deployments, and when used with ECS provide a simple and effective way to manage fleets of instances and containers, both large and small.

Combining Spot fleet with ECS can provide lower-cost options to augment existing clusters and even provision new ones. Certainly, this can be done with traditional Spot instance requests. However, because Spot fleet allows requests to span instance families and Availability Zones (with multiple allocation strategies, prices, etc.), it is a great way to enhance your ECS strategy by increasing availability and lowering the overall cost of your cluster’s compute capacity.

Amazon ECS launches new deployment capabilities; CloudWatch metrics; Singapore and Frankfurt regions

by Chris Barclay | on | in Amazon ECS | | Comments

Today, we launched two improvements that make it easier to run Docker-enabled applications on Amazon EC2 Container Service (ECS). Amazon ECS is a highly scalable, high performance container management service that supports Docker containers and allows you to easily run applications on a managed cluster of Amazon EC2 instances.

The first improvement allows more flexible deployments. The ECS service scheduler is used for long running stateless services and applications. The service scheduler ensures that the specified number of tasks are constantly running and can optionally register tasks with an Elastic Load Balancing load balancer. Previously, during a deployment the service scheduler created a task with the new task definition; after the new task reached the RUNNING state, a task that was using the old task definition was drained and stopped. This process continued until all of the desired tasks in the service were using the new task definition. This process maintains the service’s capacity during the deployment, but requires enough spare capacity in the cluster to start one additional task. Sometimes that’s not desired, because you do not want to use additional capacity in your cluster to perform a deployment.

Now, a service’s minimumHealthyPercent lets you specify a lower limit on the number of running tasks during a deployment. A minimumHealthyPercent of 100% ensures that you always have the desiredCount of tasks running and values below 100% allow the scheduler to violate desiredCount temporarily during a deployment. For example, if you have 4 Amazon EC2 instances in your cluster, and 4 tasks each running on a separate instances, changing minumumHealthyPercent from 100% to 50% would allow the scheduler to stop 2 tasks before deploying 2 new tasks.

A service’s maximumPercent represents an upper limit on the number of running tasks during a deployment, enabling you to define the deployment batch size. For example, if you have 8 instances in your cluster, and 4 tasks, each running on a separate instance, maximumPercent of 200% starts 4 new tasks before stopping the 4 old tasks. For more information on these new deployment options, see the documentation.

To illustrate these options visually, consider a scenario where you want to deploy using the least space. You could set minimumHealthyPercent to 50% and maximumPercent to 100%. The deployment would look like this:

Another scenario is to deploy quickly without reducing your service’s capacity. You could set set minimumHealthyPercent to 100% and maximumPercent to 200%. The deployment would look like this:

The next improvement involves scaling the EC2 instances in your ECS cluster automatically. When ECS schedules a task, it requires an EC2 instance that meets the constraints in the task definition. For example, if a task definition requires 1 GB RAM, ECS finds an EC2 instance that has at least that much memory so that the container can start. If the scheduler cannot find an EC2 instance that meets the constraints required to place a task, it fails to place the task.

Managing the cluster capacity is thus essential to successful task scheduling. Auto Scaling can enable clusters of EC2 instances to scale dynamically in response to CloudWatch alarms. ECS now publishes CloudWatch metrics for the reserved amount of CPU and memory used by running tasks in the cluster. You can create a CloudWatch alarm using these metrics that adds more EC2 instances to the Auto Scaling group when the cluster’s available capacity drops below a threshold that you define. For more information, see Tutorial: Scaling Container Instances with CloudWatch Alarms.

Last, Amazon ECS is now available in the Asia Pacific (Singapore) region and EU (Frankfurt) regions, bringing ECS to eight regions.

Using AWS Lambda with Auto Scaling Lifecycle Hooks

by Vyom Nagrani | on | | Comments

Nathan Mcguirt Nathan Mcguirt, AWS Solution Architect

 

Using automation to extend Auto Scaling functionality

Auto Scaling provides customers a great way to dynamically scale applications, and we frequently meet customers with new and interesting use cases who want to extend Auto Scaling with additional actions. For example, notifying an auditing system of a new instance launch or taking some sort of extra action on the instance like attaching a secondary network interface. To support these types of use cases, Auto Scaling supports adding a hook to the launching and terminating stages of the Auto Scaling instance lifecycle, which will send an SNS notification and then hold the instance in a pending state waiting for a callback to the API. It also includes a user configurable timeout and default action if your external operation doesn’t complete in a timely fashion or returns an error.

By attaching a Lambda function to the lifecycle hook by way of SNS, we can add a virtually limitless number of custom actions to our Auto Scaling group. For example, we recently had a customer with a requirement for their Auto Scaling instances to have a secondary network interface in an isolated administrative subnet in order to meet their compliance requirements. They achieved this by using a lifecycle hook to a Lambda function that created an Elastic Network Interface (ENI) in the appropriate subnet and attached it to the instance. For this use case, it was critical that that the instance have the secondary ENI before going into production, and they were able to meet this requirement by configuring the lifecycle hook to abandon the instance after a timeout period or if the Lambda function were to fail.

 

Demonstration: Adding Secondary Elastic Network Interface to Auto Scaling Instances

In this post, we’ll walk through a demonstration of how to implement the above use case of adding a secondary network interface to Auto Scaling instances. A caveat for console users, the functionality to add a lifecycle hook to an Auto Scaling group is available through the API only, so we’ll be using the AWS CLI for this demonstration rather than the AWS Console.

 

Prerequistites and Caveats

If you’re doing to follow along in your own account, you should have the following completed first:

  • Your VPC, Subnets and LaunchConfig are prepared and you have their ID’s recorded.
  • Your Auto Scaling group is configured with a desired size of 0 (the hook will only apply to new instances joining the pool.)
  • You have a subnet and security group for the secondary ENI configured (and the subnet is in the same availability zone as the one you’re using for your Auto Scaling instances)
  • You have AWS CLI installed and configured, and are using credentials with sufficient permissions.

A couple of caveats before we begin, the secondary ENI must be in the same availability zone the instance was launched in (the same zone as the primary ENI). For the sake of simplicity, the code we’re using in this demo isn’t aware of multiple subnets and availability zones. It will attempt to create the secondary ENI in whatever subnet is passed to it. If you’re going to do this in production, you’ll also likely want to configure a second lifecycle hook on instance termination and a Lambda function to delete the secondary ENI of the terminating instance. Also, pay attention to naming and capitalization as you create resources. The names of Auto Scaling groups and Lambda functions are case sensitive. You can name these resources however you wish, but consistent naming will help you avoid troubleshooting later.

 

Part 1: Configure the Notification Topic

First, we’ll need to create the Simple Notification Service notification topic.

$ aws sns create-topic --name ENI-Demo-Topic

In the response, Note the ARN of the topic for later use.

{
"TopicArn": "arn:aws:sns:us-west-2:012345678901:ENI-Demo-Topic"
}

 

Part 2: Configure an IAM role to allow posting to the SNS topic

The lifecycle hook uses an IAM role to send messages to SNS, so before we can create the hook we need to have the role prepared. To create the IAM role, we’ll need two policies, the first is a trust policy allowing Auto Scaling to assume the role, and the second gives the role permission to publish to the SNS topic. We’ll create the role in two steps, first creating the role and submitting the trust policy, and the second to apply the inline policy.

With the CLI, it’s a sometimes easier to first put the policies into text files to manage them, rather than trying to escape them in your shell, so we’ll do that here. We can specify these files on the command line later. The assume role policy (sometimes called trust policy) should look as follows, and we’ll save it in a text file called SNS-Role-Trust-Policy.json.

{
  "Version": "2012-10-17",
  "Statement": [ {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "autoscaling.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
  } ]
}

The next policy will be the inline policy granting the role access to publish to our SNS topic, and should look as follows. We’ll save it as SNS-Role-Inline-Policy.json for the example. Don’t forget to replace the example ARN with the one from your SNS topic from step 2.

{
  "Version": "2012-10-17",
  "Statement": [ {
      "Effect": "Allow",
      "Resource": "arn:aws:sns:us-west-2:012345678901:ENI-Demo-Topic",
      "Action": [
        "sns:Publish"
      ]
  } ]
}

With these policies ready, we can make the call to create the role as shown below, specifying the text files with the policy documents.

$ aws iam create-role \
--role-name ENI-Demo-Topic-Publisher-Role \
--assume-role-policy-document file://SNS-Role-Trust-Policy.json

And we then apply the inline policy to it.

$ aws iam put-role-policy \
--role-name ENI-Demo-Topic-Publisher-Role \
--policy-name AllowPublishToEniDemoTopic \
--policy-document file://SNS-Role-Inline-Policy.json

 

Part 3: Configure the Lambda Function’s IAM role

With the lifecycle hook in place, the next step is to configure the Lambda function. Lambda functions need an IAM role to give them their execution permissions, so we’ll start there. If you are using the CloudFormation sample, you can skip this, the IAM role has already been configured by CloudFormation for you.

First, like the above IAM role, we’ll need some policy documents, and will go ahead and put them in text files first for ease of use. We’ll start with the role’s Assume Role Policy (Trust Policy) which we’ll save as Lambda-Role-Trust-Policy.json, and it’s contents should be as follows:

{
  "Version": "2012-10-17",
  "Statement": [ {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
  } ]
}

Next we need the inline policy that defines what the Lambda function is allowed to do. We’ll save it as Lambda-Role-Inline-Policy.json, and it should look like the below.

{
  "Version": "2012-10-17",
  "Statement": [ {
      "Effect": "Allow",
      "Resource": "*",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:CreateNetworkInterface",
        "ec2:AttachNetworkInterface",
        "ec2:DescribeNetworkInterfaces",
        "autoscaling:CompleteLifecycleAction"
      ]
  },
  {
    "Action": [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ],
    "Resource": "arn:aws:logs:*:*:*",
    "Effect": "Allow"
  } ]
}

With the policies prepared, we can create the role, specifying the Assume Role (trust) Policy.

$ aws iam create-role \
--role-name ENI-Demo-Lambda-Role \
--assume-role-policy-document file://Lambda-Role-Trust-Policy.json

Note the ARN of the new role, we’ll need it when we go to set up the Lambda function. Once the role is created, we apply the inline policy, specifying the file we created earlier.

$ aws iam put-role-policy \
--role-name ENI-Demo-Lambda-Role \
--policy-name CreateAndAttachEnisWithLogging \
--policy-document file://Lambda-Role-Inline-Policy.json

 

Part 4: Put the lifecycle hook

Next, we’ll put the lifecycle hook on the Auto Scaling group. Lifecycle hooks support a metadata field that can be used to embed information specific to the hook in the message. In this case, we’ll specify the resource ids of the admin subnet and security groups, which are specific to this Auto Scaling group. This way, our Lambda function could be used across multiple different Auto Scaling groups. These metadata items should reflect the subnet and security groups you’d like to use for the secondary ENI. For ease of use on the CLI, we’ll write that metadata to a text file as we did with IAM. It should look like the below, and we’ll save it as Lifecycle-Hook-Metadata.json.

{
  "SubnetId":"subnet-abcdefg0",
  "SecurityGroups":["sg-abcdefg0"]
}

Now, we can put the lifecycle hook.

$ aws autoscaling put-lifecycle-hook \
--notification-metadata file://Lifecycle-Hook-Metadata.json \
--lifecycle-hook-name ENI-Demo-Hook \
--auto-scaling-group-name ENI-Demo-ASG \
--notification-target-arn arn:aws:sns:us-west-2:0123456789012:ENI-Demo-Topic \
--role-arn arn:aws:iam::0123456789012:role/ENI-Demo-Topic-Publisher-Role \
--lifecycle-transition autoscaling:EC2_INSTANCE_LAUNCHING \
--heartbeat-timeout 60

 

Part 5: Create the Lambda function

With the rest of the building blocks in place, we can create the lambda function. We’ll include the main function here, but our sample code uses an extra library called Async to make it easier to have node.js do things like wait for object an to be ready. External packages are supported in Lambda by adding the additional modules along with your code and package them in a zip file. Instructions on how to package the code and module are available in our previous blog post on using packages in AWS Lambda. You’ll need to use Async version greater than 1.0.0 for this sample.

Below is the sample code for our lambda function. It receives the message from SNS as a parameter in JSON format and unpacks the message to get the required parameters from Auto Scaling, such as the instance, subnet, and security group IDs. With this data, the Lambda function can create the network interface. Once the interface has been created, the function waits, polling for the interface state to show ‘available’ and then attaches it to the instance.

// This is sample Node.js code for AWS Lambda, to attach a secondary Elastic 
// Network Interface to an instance. To use this function, create an Auto Scaling
// lifecycle hook on instance creation notifying a SNS topic, and 
// subscribe the lambda function to the SNS topic.
// Sane values for Memory and Timeout are 128MB and 30s respectively.


var AWS = require('aws-sdk');
var ec2 = new AWS.EC2();
var as = new AWS.AutoScaling();

var async = require('async');

exports.handler = function (notification, context) {
  // Log the request
  console.log("INFO: request Recieved.\nDetails:\n", JSON.stringify(notification));
  var message = JSON.parse(notification.Records[0].Sns.Message);
  var metadata = JSON.parse(message.NotificationMetadata);
  console.log("DEBUG: SNS message contents. \nMessage:\n", message);
  console.log("DEBUG: Extracted Message Data\nData:\n", metadata);

  // Pull out metadata
  var instanceId = message.EC2InstanceId;
  var subnetId = metadata.SubnetId;
  var securityGroups = metadata.SecurityGroups;

  //define a closure for easy termination later on
  var terminate = function (success, err) {
    var lifecycleParams = {
      "AutoScalingGroupName" : message.AutoScalingGroupName,
      "LifecycleHookName" : message.LifecycleHookName,
      "LifecycleActionToken" : message.LifecycleActionToken,
      "LifecycleActionResult" : "ABANDON"
    };
    //log that we're terminating and why
    if(!success){
      console.log("ERROR: Lambda function reporting failure to AutoScaling with error:\n", err);
    }else{
      console.log("INFO: Lambda function reporting success to AutoScaling.");
      lifecycleParams.LifecycleActionResult = "CONTINUE";
    }
    //call autoscaling
    completeAsLifecycleAction (lifecycleParams, function lifecycleActionResponseHandler (err){
      if(err){
        context.fail();
      }else{
        //if we successfully notified AutoScaling of the instance status, tell lambda we succeeded
        //even if the operation on the instance failed
        context.succeed();
      }
    });
  }; 
    
  //Create the interface and wait for it to be ready
  createEni(subnetId, securityGroups, function CreateEniCallback(err, eniId){
    if(err){
      console.log("ERROR: Could not create ENI. Errors:\n", err);
      terminate(false,err);
    }
    //Wait for the ENI to be 'available'
    waitEniReady(eniId, function waitEniReadyCallback (err){
      if(err){
        console.log("ERROR: Failure waiting for ENI to be ready");
        terminate(false,err);
      }
      //attach it to the instance
      attachNetworkInterface(eniId, instanceId, function attachNetworkInterfaceCallback(err,data){
        if(err){
          console.log("ERROR: Could not attach ENI. Error Data:\n", err);
          terminate(false,err);
        }else{
          console.log("INFO: Successfully attached ENI");
          terminate(true, err);
        }
      });
    });
  });
};

function attachNetworkInterface (networkInterfaceId, instanceId, callback){
  //Attaches an ENI, passes the AttachmentId to callback.
  var nic_params = {
    'NetworkInterfaceId' : networkInterfaceId,
    'InstanceId' : instanceId,
    'DeviceIndex' : 1 // Should be safe to assume index 1 is available
  };
  ec2.attachNetworkInterface(nic_params, function evaluateEniAttachment(err,data) {
    if (err) {
      console.log("ERROR: ENI Attachment failed.\nDetails:\n", err);
      callback(err, null);
    } 
    console.log("INFO: ENI Attached.\nDetails:\n", data);
    callback(null, data.AttachmentId);
  });
}

function createEni(subnetId, securityGroups, callback){
  //Create a network interface, pass the Interface ID to callback 
  var eniCreationParams = {
    "SubnetId":subnetId,
    "Groups":securityGroups
  };
  console.log("DEBUG: CreateEni Params:\n",eniCreationParams);
  ec2.createNetworkInterface(eniCreationParams, function createEniCallback(err, data) {
    if (err) {
      console.log("ERROR: ENI creation failed.\nDetails:\n", err);
      return callback(err, null);
    } 
    console.log("INFO: ENI Created.\nData:\n", data);
    return callback(null, data.NetworkInterface.NetworkInterfaceId);
  });
}

function waitEniReady (eniId, waitEniReadyCallback){
  //terminate is the termination function if there's an issue.
  var getEniParams={
    "NetworkInterfaceIds":[
      eniId
    ]
  };
  console.log("INFO: Waiting on ENI to be ready:", eniId);
  var eniStatus = undefined;
  async.until(
    function isReady (err) { return eniStatus === "available"; },
    function getEniStatus(getEniStatusCallback){
      ec2.describeNetworkInterfaces(getEniParams,function handleGetEniResponse(err,data){
        eniStatus = data.NetworkInterfaces[0].Status;
        console.log("DEBUG: ENI status is:", eniStatus);
        getEniStatusCallback(err);
      });
    },
    function waitEniReadyCallbackClosure(err){
      if(err){
        console.log("ERROR: error waiting for ENI to be ready:\n",err);
      }
      waitEniReadyCallback(err);
    }
  );
}

function completeAsLifecycleAction(lifecycleParams, callback){
  //returns true on success or false on failure
  //notifies AutoScaling that it should either continue or abandon the instance
  as.completeLifecycleAction(lifecycleParams, function(err, data){
    if (err) {
      console.log("ERROR: AS lifecycle completion failed.\nDetails:\n", err);
      console.log("DEBUG: CompleteLifecycleAction\nParams:\n", lifecycleParams);
      callback(err);
    } else {
      console.log("INFO: CompleteLifecycleAction Successful.\nReported:\n", data);
      callback(null);
    }
  });
}

Below are the CLI commands to create the lambda function. In a later part, we’ll have to come back and set the required permissions on it to allow SNS to invoke the function, but for now we’re just going to create it. We’ll create this function with the name ENI-Demo-Lambda-Func, configure the function to use the IAM role, and set the timeout for 30 seconds. The timeout should allow plenty of time for the resources to create and become ready during execution, in most cases the function will not require that much time. Note the fileb:// prefix on the URI for the zip file.

aws lambda create-function \
--function-name ENI-Demo-Lambda-Func \
--zip-file fileb://ENI-Demo-Lambda-1-0.zip \
--runtime nodejs \
--role arn:aws:iam::012345678901:role/ENI-Demo-Lambda-Role \
--handler ENI-Demo-Lambda-1-0.handler \
--timeout 30

 

Part 6: Subscribe the Lambda function to the SNS topic

Now that the lambda function has been created, we need to subscribe it to the SNS topic so that it will recieve the messages from the lifecycle hook.

To subscribe the Lambda function to the SNS topic, we’ll call the SNS Subscribe action, specifying the ARN for the topic and the Lambda function, with Lambda as the protocol. The command looks like this:

aws sns subscribe --protocol lambda \
--topic-arn arn:aws:sns:us-west-2:012345678901:ENI-Demo-Topic \
--notification-endpoint arn:aws:lambda:us-west-2:012345678901:function:ENI-Demo-Lambda-Func

 

Part 7: Grant permissions on the lambda function to the SNS topic

And Finally, with the function created and subscribed, we can set the permissions on it that allow the SNS topic to invoke the function. This is done with the AddPermission call to Lambda.

aws lambda add-permission \
--function-name ENI-Demo-Lambda-Func \
--statement-id 1 \
--action "lambda:InvokeFunction" \
--principal sns.amazonaws.com \
--source-arn arn:aws:sns:us-west-2:012345678901:ENI-Demo-Topic

 

Testing and Reviewing the Logs

To test, simply edit your Auto Scaling group to increase the desired size, causing an instance to be added to the group. Aside from just waiting to see the secondary ENI, you can monitor and troubleshoot in a few different ways. From Auto Scaling, you can describe the instance status for the group. New instances will be in a pending state until the function succeeds, fails or times out. If successful, they’ll show as in service. If it fails or times out, they’ll be abandoned and replaced with a new instance (that’s the default behavior we configured above.) From CloudWatch, you can look at the Invocation and Errors metric for the function (or view graphs from the Lambda web console.) If the Lambda function has CloudWatch logs access, which is included in the example policy above, the Lambda function will create a log group for itself and then each function execution will create a new log stream, creating log events for any output from the function. The example code here is configured for detailed logging of it’s actions. We’ll demonstrate what that looks like here.

First, we need to find the appropriate log stream. The following command will list the log streams within the Log Group for the function.

aws logs describe-log-streams --log-group /aws/lambda/ENI-Demo-Lambda-Func

From the output, choose the execution you want to review and run the following command, using the log-stream from the previous step.

$ aws logs get-log-events --log-group-name /aws/lambda/ENI-Demo-Lambda-Func \
--log-stream-name 2015/08/11/[HEAD]0123456789abcdef0123456789abcdef \
--start-from-head

And below is a short sample of what the output looks like:

{
    "ingestionTime": 1439317268902, 
    "timestamp": 1439317254204, 
    "message": "2015-08-11T18:20:54.178Z12345678-0123-0123-0123-0123456789ab\tDEBUG: ENI status is: available\n"
}, 
{
    "ingestionTime": 1439317268902, 
    "timestamp": 1439317254584, 
    "message": "2015-08-11T18:20:54.582Z12345678-0123-0123-0123-0123456789ab\tINFO: ENI Attached.\nDetails:\n { AttachmentId: 'eni-attach-01234567' }\n"
}

Using API Gateway stage variables to manage Lambda functions

by Stefano Buliani | on | in Amazon API Gateway | | Comments

Ed Lima Ed Lima, Cloud Support Engineer

There’s a new feature on Amazon API Gateway called stage variables. Stage variables act like environment variables and can be used to change the behavior of your API Gateway methods for each deployment stage; for example, making it possible to reach a different back end depending on which stage the API is running on. This blog post will demonstrate how to use stage variables with two different AWS Lambda functions.

For this example we will use the sample functions from the Lambda Walkthrough. Sign in to the AWS Management console, open the Lambda console, and create the required functions (make sure you’re using the appropriate IAM execution role:

GetHelloWorld

console.log('Loading event');

exports.handler = function(event, context) {
  context.done(null, {"Hello":"World"});  // SUCCESS with message
};

GetHelloWorldWithName

console.log('Loading event');
            
exports.handler = function(event, context) {
  var name = (event.name === undefined ? 'No-Name' : event.name);
  context.done(null, {"Hello":name}); // SUCCESS with message
};

In the API Gateway console, create a new API called LambdaVar:

In the root resource, create a new GET method. In Integration type for the new method, choose Lambda Function, then select your Lambda Region, and type ${stageVariables.lbfunc} in the Lambda Function field. This tells API Gateway to read the value for this field from a stage variable at runtime:

The console detects the stage variable and displays the Add Permission to Lambda Function message:

Next, you manually give permissions to your Lambda functions, using the AWS CLI. This enables API Gateway to execute the functions. The CLI command must be issued with credentials that have permission to call the “add-permission” action of the Lambda APIs. The output from the AWS CLI will contain the policy statement that was set on the Lambda function resource policies.

aws lambda add-permission --function-name arn:aws:lambda:us-west-2:XXXXXXXXXXXXX:function:GetHelloWithName --source-arn arn:aws:execute-api:us-west-2:XXXXXXXXXXXXX:y91j2l4bnd/*/GET/ --principal apigateway.amazonaws.com --statement-id 95486b16-7d8a-4aca-9322-5f883ab702a6 --action lambda:InvokeFunction

# expected output
{
    "Statement": "{\"Condition\":{\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:execute-api:us-west-2: XXXXXXXXXXXX:y91j2l4bnd/*/GET/\"}},\"Action\":[\"lambda:InvokeFunction\"],\"Resource\":\"arn:aws:lambda:us-west-2:XXXXXXXXXXXX:function:GetHelloWithName\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"apigateway.amazonaws.com\"},\"Sid\":\"95486b16-7d8a-4aca-9322-5f883ab702a6\"}"
}

aws lambda add-permission --function-name arn:aws:lambda:us-west-2:XXXXXXXXXXXXX:function:GetHelloWorld --source-arn arn:aws:execute-api:us-west-2:XXXXXXXXXXXXX:y91j2l4bnd/*/GET/ --principal apigateway.amazonaws.com --statement-id 95486b16-7d8a-4aca-9322-5f883ab702a6 --action lambda:InvokeFunction

# expected output
{
    "Statement": "{\"Condition\":{\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:execute-api:us-west-2: XXXXXXXXXXXXX:y91j2l4bnd/*/GET/\"}},\"Action\":[\"lambda:InvokeFunction\"],\"Resource\":\"arn:aws:lambda:us-west-2: XXXXXXXXXXXXX:function:GetHelloWorld\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"apigateway.amazonaws.com\"},\"Sid\":\"95486b16-7d8a-4aca-9322-5f883ab702a6\"}"
}

Back in the console, you can now create your first stage. Choose Deploy API. In Stage name, type dev. In Stage description, type a description for your new stage, and then choose Deploy.

After the API deploys, on the Stage Editor page, choose the Stage Variables tab and add the stage variable from your API configuration, lbfunc. As you can see from the screenshot the value assigned to the new stage variable is the name of the Lambda function we want to invoke:

The second Lambda function, GetHelloWithName, can also receive a name parameter. You can configure the API to read the incoming parameter from the query string and pass it to the JSON body for the Lambda function by using mapping templates. To do this, go back to the GET method and choose Integration Request. Under Mapping Templates, add the following mapping template for the application/json content type:

{ "name": "$input.params('name')" }

To apply the change, deploy the API to a new stage called prod:

Next, set up the stage variable in the new deployment stage to point to the second Lambda function, GetHelloWithName:

Now you are ready to test!

The dev stage invoke URL directs you to the GetHelloWorld Lambda function:

The prod stage invoke URL with the appropriate query string directs you to the GetHelloWithName Lambda function and returns a value:

If you try to use the query string on the first stage, the query string will simply be ignored because the Lambda function is not configured to handle the parameter:

There it is: a nice way to optimize your Amazon API Gateway resources by using a single method with 2 different stages that use 2 different Lambda functions.

Alternatively, you can mix and match static names with stage variables in the integrations. For example, instead of having 2 different Lambda functions you can have a single Lambda function with multiple versions and aliases. Then, in the integration setup, you can simply use the stage variables to point to the correct alias. For instance, using an alias to one of the Lambda functions above, add the following: GetHelloWithName:${stageVariables.lambdaAlias} as Integration Type:

In your stage, add the lambdaAlias stage variable accordingly:

This variable will refer to the Lambda alias of your function:

As you can see, the new stage variables feature enables you to dynamically access different back ends, using fewer configuration steps and resources/methods in your API Gateway. The variables add even more flexibility to stages when deploying your API, which can enable different use cases in your environments.

Getting Started with JAWS on Amazon Web Services

by Stefano Buliani | on | in Amazon API Gateway, AWS Lambda | | Comments

Nathan Mcguirt Nick Corbett, AWS Professional Services, Big Data Consultant

Amazon API Gateway and AWS Lambda empower developers to deliver a microservice architecture without managing infrastructure.  Building scalable, secure, and durable applications has never been easier.  However, managing the deployment of a large project is not always easy.  A global app, deployed across multiple AWS regions in multiple environments will collect API Gateway resources, AWS Lambda functions, Amazon Identity and Access Management (IAM) roles and other AWS resources.  As your project grows, so will the number of resources.  The need to coordinate and organize your efforts quickly becomes apparent.

In this post I will introduce JAWS, an open source application framework that you can use to develop massively scalable and complex apps using API Gateway and AWS Lambda whilst helping you manage your codebase and deployments.  I will show you how to build a simple microservice that you can use to manage users for a sample application.  You will build CRUD methods to support the management of users and persist details in Amazon DynamoDB.

To get started, you’ll first need to install Node.js.  Once you’ve done that, you can install JAWS using node.js’ package manger from a command prompt (note that on some systems you may need to run this command as super user):

npm install jaws-framework -g

Now that JAWS is installed, you are ready to create your first project. Navigate to the directory where you want to create your project and type:

jaws project create

JAWS will walk you through the process of creating a project. When prompted, enter the following information:

  • Project Name: Specify “userManagement” as value. Camel case is recommended here. JAWS uses AWS CloudFormation to deploy your project and some items use the project name. CloudFormation tokenizes the project name with hyphens so it is best to avoid adding any more.
  • Project Domain: Use any domain you own. It is important to make this unique for your project. The project domain is used as part of the name for a new Amazon Simple Storage Service (Amazon S3) bucket. This Amazon S3 bucket is used to deploy your solution.
  • Email Address for CloudWatch Alarms: Your email address.
  • Stage: Specify “dev“. A stage is an environment, such as dev, UAT, or production. Each region can have multiple stages. You can easily add more stages after the project is created.
  • Region: Any AWS region. The AWS region in which you will deploy your solution. You can add other regions after the project is created. Regardless of the region you pick, API Gateway will create a global Amazon CloudFront distribution for your project to provide your users with the lowest possible latency for their API requests.
  • Profile: Your AWS profile. JAWS uses a profile in your AWS Command Line Interface credentials file (in ~/.aws/credentials) to make API calls. If you have multiple profiles defined, you can select the one to use.

As it creates your project, the framework builds and runs a CloudFormation script containing some shared resources that are needed to support your project, such as IAM roles and the Amazon S3 bucket named after the project domain.

After this is complete, you are ready to create your first AWS Module (awsm). An awsm, is how JAWS describes your microservice and includes references to both your API Gateway endpoints and AWS Lambda functions. To create a module, navigate to the userManagement project directory that JAWS created and type:

jaws module create users create

This creates a new endpoint (users) with a method behind it for creating a new user (create). The following folders and files are created in the aws_modules directory of your project:

The create directory that JAWS made contains 4 files:

  • awsm.json: Contains configuration for the API Gateway endpoint and AWS Lambda method
  • index.js: Contains the code you write to implement the method
  • handler.js
  • event.json: Defines the event that is used when your code is tested with the jaws run command

JAWS creates a thin wrapper around your code (in handler.js) to integrate with AWS Lambda. This means that you can develop and test your code (in index.js) before deploying to AWS Lambda. To demonstrate this, go to the index.js file in the create directory and update the code to:

// Export for Lambda Handler
module.exports.run = function(event, context, cb) {
  return cb(null, action(event));
};

// Your code
var action = function(event) {
  return {
    message: 'You have created user ' + event.username
  };
};

Next, edit the event.json file to read:

{
  "username" : "Nick"
}

Finally, from the create directory, type the following command:

jaws run

The JAWS framework uses the event that is defined in event.json to test your code. The following message is returned:

JAWS: {"message":"You have created user Nick"}

The run command is good for simple testing but as your project grows, a unit test framework, such as Mocha, is recommended.

As well as developing an AWS Lambda function to implement your business logic, you also need to configure a REST endpoint in API Gateway. In our sample application, users are created using the following url:

/users		called with POST

Go into awsm.json in the create directory and find the apiGateway section. Update the Path to users and the Method to POST. This indicates to JAWS that it should create a users resource in API gateway with a POST method that is integrated to your AWS Lambda code. There are other settings in the awsm.json file to control how your project is deployed, although there is no need to change anything else at the moment.

You are now ready to deploy the first iteration of your project to AWS. At the command line, type:

jaws dash

Use the arrow keys and enter to highlight both the endpoint and AWS Lambda function in yellow before navigating to deploy selected and pressing enter. Your code is then packaged, using Browserify and Minify to improve run-time efficiency, and zipped. This package is then uploaded to an S3 bucket.

For each project, JAWS maintains two CloudFormation stacks. The first stack, containing shared resources, was deployed when you made the project. JAWS now creates a second stack that contains your new AWS Lambda function (the code in the S3 bucket is used as a source). Any additional AWS Lambda functions that you write are added to this stack. After this stack is deployed, JAWS creates the API Gateway resources and methods.

You are now ready to test the deployment. Go to the AWS Management Console and open the Amazon API Gateway console. Click the userManagement-dev API and then click the POST method that JAWS created for the users resource. Click Test to test the function and use the JSON object from event.json in the request body. If everything has worked, you will see a response:

{
  "message": "You have created user Nick"
}

You can view more detailed logging from your AWS Lambda function in Amazon CloudWatch Logs.

The next step is to add a similar stub for the GET function. This is accessed by the url:

/users/		called with GET

To create the endpoint and method, go to your command line and, from the project directory, type:

jaws module create users get

JAWS creates another sub-directory in your AWSM for the new method. Go into awsm.json in the get subdirectory and update the apiGateway section of the file by making the following changes to the cloudformation section:

"Path": "users/{username}"
"Method": "GET"
"RequestTemplates": {
  "application/json": "{\"username\": \"$input.params('username') \"}"
}

This change indicates to JAWS that the AWS Lambda function will be invoked when the path users/{username} is called. It also specifies the format of the JSON event sent to the AWS Lambda function. For example, if the url users/Anna is called with a GET verb, then your AWS Lambda function is called with the following event:

{
  "username": "Anna"
}

Go into index.js for the GET method and change the code to the following:

// Export For Lambda Handler
module.exports.run = function(event, context, cb) {
  return cb(null, action(event));
};

var action = function(event) {
  return {
    message: "User requested: " + event.username
  };
};

You are then ready to deploy your project again (using the jaws dash command). When you test this method in API Gateway console, you are asked for the username:

You’ve now created stub functions for the CREATE and GET methods. Hopefully you can see how this process can be used to make the UPDATE and DELETE methods to complete the set of CRUD functions. Its now time to replace your stub code with something more meaningful.

Before replacing your stub code, you need a data store for your users. Open the resources-cf.json file in the cloudformation folder for your stage and region. This file contains the shared resources CloudFormation stack that JAWS deployed when you created your project. Add the following to resource:

"myDynamoDBTable" : {
  "Type" : "AWS::DynamoDB::Table",
  "Properties" : {
    "AttributeDefinitions": [
      {
        "AttributeName": "username",
        "AttributeType": "S"
      }
    ],
    "KeySchema": [
      {
        "AttributeName": "username",
        "KeyType": "HASH"
      }
    ],
    "ProvisionedThroughput": {
      "ReadCapacityUnits": "5",
      "WriteCapacityUnits": "5"
    },
    "TableName": {
      "Fn::Join": [
        "-",
        [
          "users",
          {
            "Ref": "aaDataModelStage"
          }
        ]
      ]
    }
  }
}

In addition to creating the DynamoDB table, you also need to update the IAM role used by your AWS Lambda functions so they have permission to use the service. Find the IAMPolicyLambda policy in the same file and add the following extra statement to the policy document:

{
  "Effect": "Allow",
  "Resource": "*",
  "Action": [
    "dynamodb:*Item",
    "dynamodb:Query",
    "dynamodb:Scan"
  ]
}

You can deploy changes to the resources for your stage by running the following JAWS command:

jaws deploy resources dev

JAWS updates the CloudFormation stack for your resources, creating a DynamoDB table (named users-dev) and updating the IAM role used by your AWS Lambda functions. The only remaining task is to inject this dependency into your AWS Lambda function, so it can find the location to write the data. You can use the recently released API Gateway stage variables for this, or you can use an environment variable in JAWS.

Environment variables are set for each stage and region, allowing you to run the same code across multiple regions and environments and configure the code at runtime. In this case, for example, you can have a different users table for dev and production.

Open the aswm.json file for the GET function and modify the envVars section at the top of the file:

"lambda": {
  "envVars": [
    "TABLE_NAME",
    "JAWS_STAGE"
  ],

This indicates to the framework to include environment variables called TABLE_NAME and JAWS_STAGE in the deployment package for your AWS Lambda function. Your code can access these as environment variables of the runtime:

// Export For Lambda Handler
module.exports.run = function(event, context, cb) {
  return cb(null, action(event));
}

// Your Code
var action = function(event) {
  const tableName = process.env.TABLE_NAME + "-" + process.env.JAWS_STAGE;

  return {
    message: "User requested: " + event.username,
    table: tableName
  };
};

The final task is to set the environment variable for the stage and region. To do this, use the following JAWS command:

jaws env set dev <region> TABLE_NAME users

JAWS maintains a file in S3 that contains the environment variables. You can now deploy your project again using the jaws dash command. Before packaging your code, JAWS pulls down the environment variables from S3 and includes them in your distribution.

At runtime, your code builds the name for the DynamoDB table by combining TABLE_NAME and the JAWS_STAGE environment variable. The JAWS_STAGE environment variable is maintained by the framework. If you test your method now in API Gateway, you should see a result similar to this:

Its now a simple task to update the logic of your AWS Lambda functions to read and write from the DynamoDB table.

After you finish building your sample app, you can tidy up by deleting the two CloudFormation stacks that JAWS created. You also need to manually delete the userManagement-dev API from API Gateway.

Summary

In this post I have shown you how to get started with JAWS, a framework for building microservices using API Gateway and AWS Lambda. As you build your project you can focus on producing exciting functionality since all of the infrastructure you need is fully managed. You can start to build your microservice application, leaving the heavy lifting to AWS and the organization of your project to JAWS.

If you have any questions or suggestions, please leave a comment below.