Serverless Cloud API Development
Welcome to the world of AWS Lambda and API Gateway
If you look closely, yes, there are certainly servers running our serverless applications in the cloud. However, the serverless approach and the platforms that enable it provide impressive capabilities without requiring us to manage the server environment. In this post I will begin developing a basic REST API with a serverless approach on AWS, starting with Lambda and API Gateway.
This series of articles expects that you have basic experience with AWS. If you are looking to kickstart your cloud developer journey, I highly recommend A Cloud Guru. In addition to providing a lot of great learning material, their Cloud Playground lets you spin up an actual - but sandboxed - cloud environment for AWS, Azure, and GCP. This allows you to practice in a real cloud environment without the risk of accidentally paying for the resources you forgot to terminate after your experimentation.
Required Tools
I will be using the AWS Serverless Application Model (SAM) and the SAM CLI (command line interface). The main AWS CLI is also quite useful, including for configuring AWS credentials needed to use the SAM CLI tool.
Java 11 will be used to implement the Lambda functions described here, but you can certainly use any of the languages & platforms that Lambda supports.
A bonus benefit of using a command line tool like SAM CLI is that these tools work extremely well in automated workflows like a CI/CD pipeline.
After your install SAM, a great way to get started is to run sam init as described in Tutorial: Deploying a Hello World application in the AWS documentation. You can skip the deployment step if you just want to see the generated code.
Source Code
You can find the source code for this project in my GitHub repo. I will start with the Lambda implementation, beginning with a simple Maven-based Java project using IntelliJ and including the dependencies shown below.
Initial Maven Dependencies & Plugins
- AWS Lambda Java Core Library
- AWS Lambda Java Events Library
- Jackson Databind (which pulls in Annotations and Core)
- Apache Maven Shade Plugin
Initial Data Model
This will be a simple implementation, but I still prefer to pick a subject other than the well traveled To Do List. In keeping with my preferred theme, I’ll go with a very basic Ride Log system, using the data model shown below.
The LocalDateTime accessors are annotated with @JsonSerialize and @JsonDeserialize which, via the using element, provide specific formatting of the string value when marshalled to & from JSON.
package dev.ericrybarczyk.ridelog;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.time.LocalDateTime;
public class RideLog {
private LocalDateTime startDateTime;
private LocalDateTime endDateTime;
private double startLatitude;
private double startLongitude;
private double endLatitude;
private double endLongitude;
private double distance;
private String rideTitle;
private String rideLocation;
@JsonSerialize(using = LocalDateTimeSerializer.class)
public LocalDateTime getStartDateTime() {
return startDateTime;
}
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
public void setStartDateTime(LocalDateTime startDateTime) {
this.startDateTime = startDateTime;
}
@JsonSerialize(using = LocalDateTimeSerializer.class)
public LocalDateTime getEndDateTime() {
return endDateTime;
}
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
public void setEndDateTime(LocalDateTime endDateTime) {
this.endDateTime = endDateTime;
}
// remaining simple getters and setters omitted for brevity
}
Lambda Implementation
You might expect the Lambda function to use the RideLog class in the method signature. However, this system will use API Gateway to route HTTP requests & responses to/from the Lambda functions. This makes APIGatewayProxyRequestEvent and APIGatewayProxyResponseEvent the appropriate types. The RideLog instance is obtained from the input event object, using Jackson’s ObjectMapper to deserialize it from the input event.
For this initial implementation, no data processing or persistence is implemented. For now, the code simply generates a random UUID value which simulates an ID value generated when the data object is saved. Following best practices, this ID is used to identify the created resource and is used in the Location HTTP header returned with the 201 response code.
public class CreateRideLogHandler
implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input,
Context context) {
final String requestBody = input.getBody();
final ObjectMapper objectMapper = new ObjectMapper();
try {
final RideLog rideLog = objectMapper.readValue(requestBody, RideLog.class);
} catch (JsonProcessingException e) {
return new APIGatewayProxyResponseEvent()
.withStatusCode(HttpURLConnection.HTTP_BAD_REQUEST)
.withBody(e.getMessage());
}
// TODO: persistence of the RideLog instance
final UUID rideLogId = UUID.randomUUID();
final Map<String, String> headers = new HashMap<>();
headers.put("Location", "/ridelogs/" + rideLogId.toString());
return new APIGatewayProxyResponseEvent()
.withStatusCode(HttpURLConnection.HTTP_CREATED)
.withHeaders(headers);
}
}
SAM Template
The AWS Serverless Application Model uses a YAML template file, an extension of CouldFormation templates, to specify the resources and configuration for the application deployment to AWS.
Below is the initial SAM template for this project, adapted from the template provided by sam init. Combined with the JAR file built from the Lambda implementation, this template is all that is necessary to get a basic API Gateway and Lambda function deployed and operational. Impressive indeed!
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
RideLog-Serverless-API
Demo project with AWS Lambda, API Gateway, and DynamoDB
Globals:
Function:
Timeout: 30
Runtime: java11
Architectures:
- x86_64
MemorySize: 512
Resources:
CreateRideLogHandlerFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: RideLog-Lambda-Functions
Handler: dev.ericrybarczyk.ridelog.CreateRideLogHandler::handleRequest
Events:
RideLogs:
Type: Api
Properties:
Path: /ridelogs
Method: Post
Outputs:
RideLogsStageApi:
Description: "API Gateway STAGE endpoint URL to POST a Ride Log"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Stage/ridelogs/"
RideLogsProdApi:
Description: "API Gateway PRODUCTION endpoint URL to POST a Ride Log"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/ridelogs/"
CreateRideLogHandlerFunction:
Description: "CreateRideLogHandler Lambda Function ARN"
Value: !GetAtt CreateRideLogHandlerFunction.Arn
CreateRideLogHandlerFunctionIamRole:
Description: "Implicit IAM Role created for CreateRideLogHandler Lambda Function"
Value: !GetAtt CreateRideLogHandlerFunction.Arn
The optional Outputs section of the SAM template is quite useful, allowing you to retrieve and display values that are not available until AWS generates the resources during the deployment. The intrinsic functions !Sub and !GetAtt (using the short-form syntax) are used here to obtain the generated URL for the API Gateway endpoints, as well as generated ARN values.
Try It Out
The SAM toolkit is used to build and deploy the serverless application. To compile the code, run the sam build command, from the directory containing the template.yaml file. The CodeUri property indicates the location of the Lambda function source code to be built. Alternatively, the -t parameter allows you to specify a different location for the template.
Build
$ sam build
Building codeuri: /some/path/RideLog-Serverless-API/RideLog-Lambda-Functions runtime: java11 metadata: {} architecture: x86_64 functions: ['CreateRideLogHandlerFunction']
Running JavaMavenWorkflow:CopySource
Running JavaMavenWorkflow:MavenBuild
Running JavaMavenWorkflow:MavenCopyDependency
Running JavaMavenWorkflow:MavenCopyArtifacts
Build Succeeded
Built Artifacts : .aws-sam/build
Built Template : .aws-sam/build/template.yaml
Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch
[*] Deploy: sam deploy --guided
Local Execution
SAM also supports local execution of the application. This requires Docker on your local system, and the first time you run this for a serverless application, the tool will download a Docker image from AWS to provide the runtime for this invocation. Subsequent invocations will reuse this image, as shown in the example output below.
$ sam local invoke CreateRideLogHandlerFunction -e ./events/post-event.json
Invoking dev.ericrybarczyk.ridelog.CreateRideLogHandler::handleRequest (java11)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-java11:rapid-1.36.0-x86_64.
Mounting /some/path/RideLog-Serverless-API/.aws-sam/build/CreateRideLogHandlerFunction as /var/task:ro,delegated inside runtime container
START RequestId: 078c8d00-ede8-4b70-8c78-45333d1205a3 Version: $LATEST
{"statusCode":201,"headers":{"Location":"/ridelogs/a66911ff-956d-4b9e-8f45-5add7db0a908"}}END RequestId: 078c8d00-ede8-4b70-8c78-45333d1205a3
REPORT RequestId: 078c8d00-ede8-4b70-8c78-45333d1205a3 Init Duration: 0.23 ms Duration: 865.80 ms Billed Duration: 866 ms Memory Size: 512 MB Max Memory Used: 512 MB
Of particular interest in the output above is the response from the Lambda execution:
{“statusCode”:201,“headers”:{“Location”:"/ridelogs/a66911ff-956d-4b9e-8f45-5add7db0a908"}}
This simply confirms the Lambda implementation was executed.
Deployment
Proceed at your own risk. Running the deploy command will create resources in your AWS account, and may result in charges on your AWS bill.
TIP: If you have access to a paid A Cloud Guru account, you can use their Cloud Playground and configure your AWS CLI to use the access keys provided when you launch an AWS Sandbox. This will allow you to deploy resources to the Sandbox rather than to your own AWS account.
Using the --guided option with the sam deploy command provides an interactive deployment with prompts for a variety of options, as shown below. Simply pressing enter accepts the default value for that item.
$ sam deploy --guided
Configuring SAM deploy
======================
Looking for config file [samconfig.toml] : Not found
Setting default arguments for 'sam deploy'
=========================================
Stack Name [sam-app]: ridelogs-api
AWS Region [us-east-1]:
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]: N
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: Y
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]: N
CreateRideLogHandlerFunction may not have authorization defined, Is this okay? [y/N]: Y
Save arguments to configuration file [Y/n]: Y
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]:
While the stack is being deployed, the CLI will update with status for each resource, such as CREATE_IN_PROGRESS and CREATE_COMPLETE. Upon successful completion, the result of the Outputs section of the template.yaml file will be displayed, such as the example shown below.
CloudFormation outputs from deployed stack
---------------------------------------------------------------------------------------------------------------------------
Outputs
---------------------------------------------------------------------------------------------------------------------------
Key RideLogsStageApi
Description API Gateway STAGE endpoint URL to POST a Ride Log
Value https://da----9f.execute-api.us-east-1.amazonaws.com/Stage/ridelogs/
Key RideLogsProdApi
Description API Gateway PRODUCTION endpoint URL to POST a Ride Log
Value https://da----9f.execute-api.us-east-1.amazonaws.com/Prod/ridelogs/
Key CreateRideLogHandlerFunction
Description CreateRideLogHandler Lambda Function ARN
Value arn:aws:lambda:us-east-1:1234567890:function:ridelogs-api-CreateRideLogHandlerFunction-0x----SG
Key CreateRideLogHandlerFunctionIamRole
Description Implicit IAM Role created for CreateRideLogHandler Lambda Function
Value arn:aws:lambda:us-east-1:1234567890:function:ridelogs-api-CreateRideLogHandlerFunction-0x----SG
---------------------------------------------------------------------------------------------------------------------------
AWS Console
After the deployment completes, the resources created can be seen in the AWS console. First, the
CloudFormation stack
is named according to the Stack Name [sam-app]: ridelogs-api
value used in the deployment. You can review all the resources created.
The API Gateway resources and endpoint information, with Lambda proxy integration, can be reviewed and even tested using the built-in testing capability in the web console.
Finally, the deployed Lambda function can be reviewed, and likewise tested using the built-in testing options in the web console.
External Test via Postman
Using the endpoint provided by the SAM CLI Outputs, or obtained from the AWS console, we can run a simple test in Postman.
In addition to the successful 201 created
response code, we see the expected Location
HTTP header.
Cleanup
When you are finished exploring the results of the deployment, you likely want to terminate all the resources created to minimize any costs.
The SAM tool provides the sam delete
command which you can run to delete the entire CloudFormation stack that was created by the deployment action.
Where Do We Go From Here?
We now have a functional, deployed API working with a simple Lambda function. However, we only have a POST endpoint, and we aren’t actually handling the inbound data yet. We will tackle persistence next, serverless-style.