- Architecting Cloud Native Applications
- Kamal Arora Erik Farr John Gilbert Piyum Zonooz
- 943字
- 2021-06-24 15:21:05
Example – CRUD service
This example demonstrates creating a simple and secure RESTful CRUD service, using the AWS API Gateway along with AWS Cognito, AWS Lambda, and AWS DynamoDB. The following Serverless Framework serverless.yml file fragment defines an API Gateway with a RESTful resource to PUT, GET, and DELETE items. Each of these HTTP methods is mapped to a Lambda function: save, get, and delete, respectively. We also enable Cross-Origin Resource Sharing (CORS) support so that the resource can be accessed from a web application, usually written in React or Angular. To add OAuth 2.0 support, we provide the Amazon Resource Name (ARN) for the AWS Cognito User Pool that is used to authenticate the users. Throttling is provided by default and can be overridden as needed. Finally, we declare an environment variable for the table name, so that it is not hardcoded in the functions and can change based on the deployment stage.
All of this is completely declarative, which frees the team to focus on the business logic. There are additional settings, such as API Keys and Usage Plans, but most components will use these example settings as is, other than the specific resource paths. The API Gateway will automatically run behind an AWS CloudFront distribution, which is the AWS CDN service. However, it is common to manage your own CloudFront distribution in front of the API Gateway to access the full power of CloudFront, such as WAF integration and caching support. As an alternative to AWS Lambda, the API Gateway could proxy to an application load balancer, which fronts an AWS ECS cluster running Docker containers. However, I recommend starting with the disposable architecture of AWS Lambda to empower teams to experiment with functionality more quickly.
custom: # variables
authorizerArn: arn:aws:cognito-idp:us-east-1:xxx:userpool/us-east-1_ZZZ
# configure CRUD functions for api gateway and cognito jwt authorizer
functions:
save:
handler: handler.save
events:
- http:
path: items/{id}
method: put
cors: true
authorizer:
arn: ${self:custom.authorizerArn}
get:
handler: handler.get
events:
- http:
path: items/{id}
method: get
cors: true
authorizer:
arn: ${self:custom.authorizerArn}
delete:
handler: handler.delete
events:
- http:
path: items/{id}
method: delete
cors: true
authorizer:
arn: ${self:custom.authorizerArn}
Next, we have example code for the save function. Note that this is the first synchronous code example we have covered. All other code examples have been for asynchronous stream processors, because our objective is to perform most logic asynchronously. But ultimately, some data enters the system manually through synchronous human interaction.
The path parameters, relevant context information, and the message body are merged together, saved to the database and the status code and appropriate headers are returned. The complex domain object is transported as a JSON string and stored as JSON; thus there is no need to transform the object to another format. We do adorn the object with some contextual information. The API Gateway decodes the JWT token and passes the valid values along with the request. We can store these values with the object to support event streaming. We are leveraging the DynamoDB Stream to perform Database-First Event Sourcing. Therefore we only need to store the current state of the object in this source database. We will discuss the idea of source/authoring components in the Backend For Frontend pattern.
This example only includes the bare essentials to keep it simple and not hide any moving parts. I will usually add some very lightweight layers to reuse common code fragments and facilitate unit testing with spies, stubs, and mocks. I normally wrap the data access code in a simple connector and add data validation utilities along with error handling. GraphQL is also a recommended tool, as we will discuss in the Backend For Frontend pattern.
// PUT items/{id}
export const save = (event, context, callback) => {
const params = {
TableName: process.env.TABLE_NAME,
// merge the id and username with the JSON in the request body
Item: Object.assign(
{
id: event.pathParameters.id,
lastModifiedBy:
event.requestContext.authorizer.claims['cognito:username'],
},
JSON.parse(event.body)
)
};
const db = new aws.DynamoDB.DocumentClient();
// insert or update the item in the database
db.put(params).promise()
.then((result) => {
// return a success response
callback(null, {
statusCode: 200,
headers: {
'access-control-allow-origin': '*', // CORS support
'cache-control': 'no-cache',
},
});
}, (err) => {
// return an error response
callback(err);
});
};
Finally, we have example code for the Get By ID function. The path parameter is used as the key to retrieve the object from the database. The object is stored as JSON, so there is no need to transform the object for the response. We are leveraging the DynamoDB Stream to perform Database-First Event Sourcing. Therefore, there is no need to calculate the current state of the object from the events because only the current state is maintained in this table. This greatly simplifies the development experience.
This is also an example of the level of complexity we want to have in our user- facing synchronous boundary components. We will discuss this in detail next, in the CQRS pattern. We do not want components synchronously calling other components, much less multiple components. A query should make a straightforward synchronous call to its own cloud-native database and return the data, as it is stored in the database. All complex join logic is performed when the data is inserted into the materialized view.
// GET items/{id}
export const get = (event, context, callback) => {
const params = {
TableName: process.env.TABLE_NAME,
Key: {
id: event.pathParameters.id,
},
};
const db = new aws.DynamoDB.DocumentClient();
// retrieve the item from the database
db.get(params).promise()
.then((data) => {
// return a success response
callback(null, {
statusCode: 200,
headers: {
'access-control-allow-origin': '*', // CORS support
'cache-control': 'max-age=3',
},
body: JSON.stringify(data),
});
}, (err) => {
// return an error response
callback(err);
});
};