Author: Zen and the Art of Computer Programming
1. Introduction
The term “Serverless” has attracted more and more attention in recent years. It allows developers to only focus on business logic development without having to worry about a series of cumbersome processes such as server operation and maintenance, resource configuration, and application deployment.
As a technician, I believe that any technological innovation is inseparable from the understanding and grasp of current business. For mobile terminal research and development, how to use AWS services to build a low-cost, high-efficiency, and highly scalable backend is an important topic.
In the past period of time, I have been exploring the mobile backend technology stack, including services on AWS such as Lambda, API Gateway, DynamoDB, etc., as well as similar domestic products, such as Microsoft’s Mobile Apps backend as a service (MBaaS) , LeanCloud, etc.
I’ve long been aware of AWS Mobile Hub, which is designed to quickly create mobile application backends. However, although it provides a convenient interface to help create various services required for mobile applications, its functionality is not comprehensive enough. For example, it has no support for a database access layer. Therefore, when I need to build a mobile application backend myself, I will refer to other solutions, including Leancloud, OneAPM, Tencent Cloud SCF, etc. However, these platforms often charge fees and are not versatile enough.
Based on this, I hope to use this article to share how I use AWS services to build a low-cost, high-efficiency, and highly scalable mobile application backend.
2.Basic concepts and terminology
2.1 Service
2.1.1 AWS Lambda
AWS Lambda is a serverless computing service that provides an environment for running functions. You can upload a code package or write the code directly, and then specify the execution time, memory size, and disk space, and AWS will automatically allocate the running environment and resources. Since it is serverless computing, users only need to focus on their own business logic and do not need to care about server resource allocation, network connections, load balancing, etc. Its architecture is shown in the figure below:
2.1.2 Amazon API Gateway
Amazon API Gateway is a RESTful API gateway service that hosts web services. It helps you define RESTful APIs, map them to backend services (such as Lambda), and add security controls (such as authentication and authorization). You can call backend services through this gateway, and it can also publish your API to the public Internet for use by third-party developers. Its architecture is shown in the figure below:
2.1.3 Amazon DynamoDB
Amazon DynamoDB is a fast, highly scalable NoSQL database service. It allows you to store and retrieve structured and unstructured data with elasticity, high availability, and scalability. Its architecture is shown in the figure below:
2.2 Tools
2.2.1 AWS CLI
AWS CLI is a command line tool for managing AWS services. You can use it to complete many tasks, such as creating, updating and deleting EC2 instances, IAM policies, S3 buckets, etc. Please refer to the official documentation for installation methods.
2.2.2 AWS SDKs
AWS SDKs are a collection of APIs used by developers to interact with AWS services. You can use them to build your own applications or scripts to implement various functions, such as sending emails, querying cloud monitoring information, uploading files, etc. Please refer to the official documentation for details.
3. Core algorithm principles and specific operating steps
3.1 Create backend service
The first step is to create a new project and initialize the AWS environment. If you are not familiar with the AWS CLI, you can read the relevant documentation first and configure the appropriate credentials.
- Initialize project
mkdir mobile-app-backend & amp; & amp; cd mobile-app-backend npm init -y
- Install dependent libraries
npm install --save aws-sdk
- Configure AWS credentials
export AWS_ACCESS_KEY_ID=<YOUR_ACCESS_KEY> export AWS_SECRET_ACCESS_KEY=<YOUR_SECRET_KEY> export AWS_REGION=<YOUR_REGION>
- Create configuration file
Create a file called serverless.yml
with the following content:
service: mobile-app-backend # Project name provider: name: aws # Use AWS services runtime: nodejs10.x # Specify the runtime environment as Nodejs stage: dev # define environment region: us-east-1 # Set region environment: TABLE_NAME: todos #Set environment variables iamRoleStatements: # Set permissions for Lamdba roles - Effect: Allow Action: - dynamodb:DescribeTable - dynamodb:Query - dynamodb:Scan - dynamodb:GetItem - dynamodb:PutItem - dynamodb:UpdateItem - dynamodb:DeleteItem Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.TABLE_NAME}" functions: # Function configuration createTodo: # Function to create TODO items handler: functions/createTodo.handler # Function entry events: - http: POST /todos # HTTP interface configuration -apigw: method: any # Allow any HTTP method path: /todos # Set path getTodos: # Function to get all TODO items handler: functions/getTodos.handler # Function entry events: - http: GET /todos # HTTP interface configuration -apigw: method: any # Allow any HTTP method path: /todos # Set path deleteTodo: # Function to delete a single TODO item handler: functions/deleteTodo.handler # Function entry events: - http: DELETE /todos/{todoId} # Path parameter binding -apigw: method: delete # Only DELETE requests are allowed requestTemplates: application/json: '{"todoId": $input.params("todoId")}' apiKeyRequired: false # API Key verification is not required updateTodo: # Function to update a single TODO item handler: functions/updateTodo.handler # Function entry events: - http: PUT /todos/{todoId} # Path parameter binding -apigw: method: put # Only PUT requests are allowed requestTemplates: application/json: |- { "description": "$input.body('$.description')", "done": "$input.body('$.done')" } integration: lambda # Use Lambda as Integration uri: arn:aws:apigateway:${self:provider.region}:lambda:path/2015-03-31/functions/${self:service}-${sls:stage}-createTodo/invocations # Set Lambda address resources: # Resource configuration Resources: TodosTable: # Configuration of TODO item table Type: 'AWS::DynamoDB::Table' # The type is DynamoDB table Properties: #Table properties TableName: ${self:provider.environment.TABLE_NAME} #Set the table name AttributeDefinitions: # Define table fields - AttributeName: userId #User ID field AttributeType: S #The data type is string - AttributeName: todoId # TODO item ID field AttributeType: N #The data type is number KeySchema: # Primary key index - AttributeName: userId KeyType: HASH # as hash key - AttributeName: todoId KeyType: RANGE # as range key ProvisionedThroughput: # Set throughput ReadCapacityUnits: 1 # Read capacity per second is 1 WriteCapacityUnits: 1 # The write capacity per second is 1
The main services and tools involved in the above configuration are as follows:
Services and Tools | Description |
---|---|
AWS Lambda | Serverless computing service for hosting functions |
AWS API Gateway | RESTful API gateway, used to define API |
AWS DynamoDB | NoSQL database service for storing data |
AWS CLI | Command line tool for managing AWS services |
AWS SDKs | SDKs, used to interact with AWS services |
3.2 Data Model Design
In order to store and manage TODO items, we need to design a suitable data model. Here’s a simple example:
{ id:'string', // unique identifier description:'string', // description information done: boolean // Whether it has been completed }
The two fields id and userId are composite primary keys and therefore cannot be repeated. Additionally, we can use DynamoDB’s query and scan operations to obtain and search TODO items.
3.3 Create function
Next, we need to create some functions for handling TODO items. The specific implementation of each function is given here.
3.3.1 Create TODO items
const uuidv4 = require('uuid').v4; module.exports.handler = async function(event, context, callback) { const body = JSON.parse(event.body); if (!body ||!body.description) { return {statusCode: 400}; } const timestamp = new Date().toISOString(); const todo = { id: `t_${timestamp}_${Math.floor(Math.random() * 100)}`, // Generate unique identifier userId: event.requestContext.identity.cognitoIdentityId, // Get the user ID from the request context createdAt: timestamp, updatedAt: timestamp, ...body }; try { await this.ddb.put({ TableName: process.env.TABLE_NAME, Item: todo }).promise(); return { statusCode: 201, headers: {'Access-Control-Allow-Origin': '*'}, body: JSON.stringify(todo), }; } catch (err) { console.error(err); return {statusCode: 500}; } };
First, we get the JSON data in the HTTP request body from the event object. If necessary parameters are missing, error response code 400 is returned. Otherwise, generate a UUID as the id value of the TODO item, and set the user ID, creation time, update time, and description information in the request body.
Next, we try to insert TODO items into DynamoDB. If it fails, the error log is printed and error response code 500 is returned. When successful, status code 201 is returned, cross-domain access is allowed, and the JSON representation of the TODO item is returned.
3.3.2 Get all TODO items
module.exports.handler = async function(event, context, callback) { let responseBody = ''; try { const result = await this.ddb.scan({ TableName: process.env.TABLE_NAME, FilterExpression: `#userId = :userId`, ExpressionAttributeNames: {"#userId": "userId"}, ExpressionAttributeValues: {":userId": event.requestContext.identity.cognitoIdentityId}, ProjectionExpression: '#id, #desc, #createdAt, #updatedAt, #done', ExpressionAttributeNames: { "#id": "id", "#desc": "description", "#createdAt": "createdAt", "#updatedAt": "updatedAt", "#done": "done" }, Limit: 100 // Return up to 100 pieces of data }).promise(); for (let i = 0; i < result.Items.length; i + + ) { responseBody + = `${result.Items[i].id}\t${result.Items[i].description}\t${result.Items[i].createdAt}\t${result. Items[i].updatedAt}\t${result.Items[i].done? 'Yes' : 'No'}\r\\ `; } return { statusCode: 200, headers: {'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*'}, body: responseBody }; } catch (err) { console.error(err); return {statusCode: 500}; } };
First, we define a variable responseBody to save the result data. We then try to scan all TODO items from DynamoDB and filter out items that the user is not their own. In order to improve query speed, we only select necessary fields. If it fails, the error log is printed and error response code 500 is returned.
If the query is successful, it loops through all items, assembles them into a string similar to a csv format, and returns status code 200, allowing cross-domain access, and this string.
3.3.3 Delete a single TODO item
module.exports.handler = async function(event, context, callback) { const params = { TableName: process.env.TABLE_NAME, Key: { userId: event.requestContext.identity.cognitoIdentityId, todoId: parseInt(event.pathParameters.todoId) } }; try { await this.ddb.delete(params).promise(); return { statusCode: 204, headers: {'Access-Control-Allow-Origin': '*'} }; } catch (err) { console.error(err); return {statusCode: 500}; } };
First, we parse out the todoId based on the request path parameters. After that, we construct a Key object containing the user ID and TODO item ID.
Next, we try to delete the specified TODO item from DynamoDB. If it fails, the error log is printed and error response code 500 is returned. When successful, status code 204 is returned, allowing cross-domain access.
3.3.4 Update a single TODO item
module.exports.handler = async function(event, context, callback) { const body = JSON.parse(event.body); if ((!body.description || typeof body.description!=='string') || (!body.done || typeof body.done!== 'boolean')) { return {statusCode: 400}; } const timestamp = new Date().toISOString(); const params = { TableName: process.env.TABLE_NAME, Key: { userId: event.requestContext.identity.cognitoIdentityId, todoId: parseInt(event.pathParameters.todoId) }, UpdateExpression: `SET description=:description, done=:done, updatedAt=:updatedAt`, ExpressionAttributeValues: { ':description': body.description, ':done': body.done, ':updatedAt': timestamp } }; try { const data = await this.ddb.update(params).promise(); if (!data.Attributes) { return {statusCode: 404}; } return { statusCode: 200, headers: {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'}, body: JSON.stringify(data.Attributes), }; } catch (err) { console.error(err); return {statusCode: 500}; } };
First, we check whether the request body contains the correct description and done fields. If the requirements are not met, error response code 400 is returned.
Next, we construct an UpdateExpression object to modify the description information and completion status of the TODO item. Finally, we try to update the specified TODO item. If the specified TODO item cannot be found, error response code 404 is returned; if the update is successful, status code 200 is returned, cross-domain access is allowed, and the complete attributes of the TODO item are returned.