AWS API Gateway and AWS Lambda are part of the Serverless Architecture paradigm shift. The learning curve is steep and for this reason Amazon has a step-by-step tutorial on how to get started. This blog post aims to outline the required AWS resources for a similar project, but this time using AWS CloudFormation instead of the AWS Console for configuration. The sample code is deliberately simplistic, nevertheless I suggest that you start with the AWS tutorial if you have not worked with API Gateway before. If you plan to use this setup in production, I recommend that you study the provided reference documentation carefully so that you do not miss any additional configuration setting that your service may need. The source code is available in my GitHub repo.

Goal

The business logic in this example consists of Lambda function called GreetingLambda which has been configured with an appropriate execution role. If the event passed to the Lambda contains a name property, a JSON document with a greeting containing the name is returned. If not, the greeting Hello, World! is returned.

exports.handler = (event, context, callback) => {
  const name = event.name || 'World';
  const response = {greeting: `Hello, ${name}!`};
  callback(null, response);
};

When proxied by an API Gateway, it should be possible to perform a HTTP request with the name as a request parameter.

$ curl https://abc123.execute-api.eu-west-1.amazonaws.com/LATEST/greeting?name=Superman
{"greeting":"Hello, Superman!"}

CloudFormation Resources

API Gateway Rest API

First of all, we need to create an API Gateway REST API, a resource that contains a collection API Gateway resources.

"GreetingApi": {
  "Type": "AWS::ApiGateway::RestApi",
  "Properties": {
    "Name": "Greeting API",
    "Description": "API used for Greeting requests",
    "FailOnWarnings" : true
  }
}

Ref: AWS::ApiGateway::RestApi

Lambda Permission

As with all AWS resources, the API Gateway needs permission to execute the Lambda function.

"LambdaPermission": {
  "Type": "AWS::Lambda::Permission",
  "Properties": {
    "Action": "lambda:invokeFunction",
    "FunctionName": {"Fn::GetAtt": ["GreetingLambda", "Arn"]},
    "Principal": "apigateway.amazonaws.com",
    "SourceArn": {"Fn::Join": ["", 
      ["arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", {"Ref": "GreetingApi"}, "/*"]
    ]}
  }
}

Ref: AWS::Lambda::Permission

API Gateway Stage

A stage defines the path through which an API deployment is accessible.

"GreetingApiStage": {
  "DependsOn" : ["ApiGatewayAccount"],
  "Type": "AWS::ApiGateway::Stage",
  "Properties": {
    "DeploymentId": {"Ref": "ApiDeployment"},
    "MethodSettings": [{
      "DataTraceEnabled": true,
      "HttpMethod": "*",
      "LoggingLevel": "INFO",
      "ResourcePath": "/*"
    }],
    "RestApiId": {"Ref": "GreetingApi"},
    "StageName": "LATEST"
  }
}

Here, CloudWatch logging has been enabled in the MethodSettings configuration. I choose to configure StageName as LATEST to indicate that this stage will target the $LATEST version of the Lambda function (which is the default). Ref: AWS::ApiGateway::Stage

Logging

It is not enough to just declare logging on the stage as exemplified above. In order to get any CloudWatch logs, you must also specify an IAM role that enables API Gateway to write information to CloudWatch.

"ApiGatewayCloudWatchLogsRole": {
  "Type": "AWS::IAM::Role",
  "Properties": {
    "AssumeRolePolicyDocument": {
      "Version": "2012-10-17",
      "Statement": [{
        "Effect": "Allow",
        "Principal": { "Service": ["apigateway.amazonaws.com"] },
        "Action": ["sts:AssumeRole"]
      }]
    },
    "Policies": [{
      "PolicyName": "ApiGatewayLogsPolicy",
      "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [{
          "Effect": "Allow",
          "Action": [
            "logs:CreateLogGroup",
            "logs:CreateLogStream",
            "logs:DescribeLogGroups",
            "logs:DescribeLogStreams",
            "logs:PutLogEvents",
            "logs:GetLogEvents",
            "logs:FilterLogEvents"
          ],
          "Resource": "*"
        }]
      }
    }]
  }
}

This role must subsequently be passed as a property to the API Gateway Account.

"ApiGatewayAccount": {
  "Type" : "AWS::ApiGateway::Account",
  "Properties" : {
    "CloudWatchRoleArn" : {"Fn::GetAtt" : ["ApiGatewayCloudWatchLogsRole", "Arn"] }
  }
}

Ref: AWS::ApiGateway::Account

API Gateway Deployment

The API Gateway Deployment resource deploys an Amazon API Gateway RestApi resource to a stage so that clients can call the API over the Internet.

"ApiDeployment": {
  "Type": "AWS::ApiGateway::Deployment",
  "DependsOn": ["GreetingRequest"],
  "Properties": {
    "RestApiId": {"Ref": "GreetingApi"},
    "StageName": "DummyStage"
  }
}

Note that I have configured of StageName as DummyStage. This is not a mistake. If you read the documentation of the StageName property you will find that:

Note This property is required by API Gateway. We recommend that you specify a name using any value (see Examples) and that you don’t use this stage. We recommend not using this stage because it is tied to this deployment, which means you can’t delete one without deleting the other. For example, if you delete this deployment, API Gateway also deletes this stage, which you might want to keep. Instead, use the AWS::ApiGateway::Stage resource to create and associate a stage with this deployment.

Ref: AWS::ApiGateway::Deployment

API Gateway Resource

Now that we have all the internal bits and pieces setup for the API Gateway, we can start working on the public HTTP parts of the API. In this simplified example, there will only be one endpoint, namely the /greeting resource:

"GreetingResource": {
  "Type": "AWS::ApiGateway::Resource",
  "Properties": {
    "RestApiId": {"Ref": "GreetingApi"},
    "ParentId": {"Fn::GetAtt": ["GreetingApi", "RootResourceId"]},
    "PathPart": "greeting"
  }
}

The properties are pretty straight forward, we tie the resource to a REST API, we choose a specific parent resource (in this example it is placed directly under the API root) and we define the path part name. Ref: AWS::ApiGateway::Resource

API Gateway Method

Defining just a resource is not enough, one needs to specify one or more HTTP methods associated with the resource. This turns out to be a verbose exercise and there are more properties available than stated below, see the reference docs for details.

"GreetingRequest": {
  "DependsOn": "LambdaPermission",
  "Type": "AWS::ApiGateway::Method",
  "Properties": {
    "AuthorizationType": "NONE",
    "HttpMethod": "GET",
    "Integration": {
      "Type": "AWS",
      "IntegrationHttpMethod": "POST",
      "Uri": {"Fn::Join" : ["", 
        ["arn:aws:apigateway:", {"Ref": "AWS::Region"}, ":lambda:path/2015-03-31/functions/", {"Fn::GetAtt": ["GreetingLambda", "Arn"]}, "/invocations"]
      ]},
      "IntegrationResponses": [{
        "StatusCode": 200
      }],
      "RequestTemplates": {
        "application/json": {"Fn::Join" : ["", [
          "{",
            "\"name\": \"$input.params('name')\"",
          "}"
        ]]}
      }
    },
    "RequestParameters": {
      "method.request.querystring.name": false
    },
    "ResourceId": {"Ref": "GreetingResource"},
    "RestApiId": {"Ref": "GreetingApi"},
    "MethodResponses": [{
      "StatusCode": 200
    }]
  }
}

There is a lot of things going on:

  • The HttpMethod tells us that the client will use a HTTP GET request to call the resource.
  • The Type and Uri integration properties has been configured to target a Lambda function.
  • The StatusCode(s) listed in the IntegrationResponses are mapped to the corresponding StatusCode in the MethodResponses (together with other properties that have been omitted in this example).
  • RequestParameters declares the name and type of any request parameters. In this example, an optional query string called name is defined.
  • RequestTemplates is used to convert the incoming request to the format that the Lambda function expects. The value of the name query string just mentioned is converted to a JSON document such as {"name": "Superman"} that is passed to the Lambda function.

For more information about request and response mappings, see API Gateway API Request and Response Parameter-Mapping Reference and API Gateway API Request and Response Payload-Mapping Template Reference respectively. Ref: API::Gateway::Method

Test

When an API Gateway is deployed, it gets a root url of the https://[api-id].execute-api.[region].amazonaws.com format. If you create a stack that contains all the CloudFormation resources mentioned above (see the cloudforation.template file in the GitHub sample repo), you can verify that the API Gateway works by issuing a GET request to an endpoint made up of the stage name and resource name appended to the root url (make sure that you use the api-id and region used by your API Gateway, check the Stack Outputs).

$ curl https://abc123.execute-api.eu-west-1.amazonaws.com/LATEST/greeting
{"greeting":"Hello, World!"}

You can also provide a request parameter:

$ curl https://abc123.execute-api.eu-west-1.amazonaws.com/LATEST/greeting?name=Superman
{"greeting":"Hello, Superman!"}

Considerations

  • An AWS generated domain name may be ok for some applications, but other times a custom domain name is preferred. For the latter case, you may find the article Use a Custom Domain Name in API Gateway interesting. For CloudFormation configuration you can then look into how a AWS::ApiGateway::BasePathMapping resource can be configured.
  • An API Gateway Stage corresponds to a version of the API in service. After it has been deployed, it is not possible to modify it. If you find the need for updates, e.g. if you would like to change the request template, you must either deploy a new stage or delete the old stage first in order for the new request mapping to be applied to any request from the clients. This limitation does not apply to Lambda implementation code which can be updated independently of the API Gateway Stage.

Update

On the November 18th, 2016 AWS introduced the Serverless Application Model (or SAM for short) that provides an alternative solution to the one described in this blog post. Please read the AWS blog post and study the related project at the AWS Labs GitHub account for more information.

Updated: