Back-end (Building SAM)
Now it is time to create our backend using AWS SAM
Step 1. Begin by generating a new directory and name it "amplify-sam-backend".
mkdir amplify-sam-backend
Step 2. Move into the newly created directory and launch it in Visual Studio Code.
cd amplify-sam-backend
code .
Subsequently, create a file named template.yml within the amplify-sam-backend directory for configuration.
Step 3. Now we will define cloud formation notations. Since AWS SAM does not have syntax, this section mainly involves copy and pasting.
The first step is to specify the AWS template format version, while also indicating that it incorporates the Serverless Transform.
Following that, we will introduce parameters to allow for dynamic code deployment to GitHub.
AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Parameters:
GithubRepository:
Type: String
Description: GitHub repository URL
Stage:
Type: String
Description: Deployment stage
Step 4. We will configure global variables, specifically for our DynamoDB database, and designate the table with the name "sam-table" (this will be the name of our DynamoDB table).
Globals:
Function:
Environment:
Variables:
TABLE_NAME: sam-table
Step 5. Next, we will focus on creating our resources, starting with the Amplify Application. We will set the front-end repository name in the property field.
NOTE: The name of our front end repository is "amplify-sam-app".
NOTE: If your repository is set to private mode, the following line should be included under the Repository field:
AccessToken: "{{resolve:secretsmanager:github-token}}"
Name: amplify-sam-app
Repository: !Ref GithubRepository
AccessToken: "{{resolve:secretsmanager:github-token}}"
IAMServiceRole: !GetAtt AmplifyRole.Arn
For the scope of our project, the repository is publicly accessible; therefore, including an access token is not required.
Resources:
AmplifyApp:
Type: AWS::Amplify::App
Properties:
Name: amplify-sam-app
Repository: !Ref GithubRepository
IAMServiceRole: !GetAtt AmplifyRole.Arn
EnvironmentVariables:
- Name: ENDPOINT
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/"
Step 6. we will set up our Amplify branches. In our example, the main branch of our frontend application (amplify-sam-app) is labeled as "main." This serves to direct SAM on how to connect our backend with the respective branch.
NOTE: (Optional) Multiple branches can be configured if desired.
AmplifyBranch:
Type: AWS::Amplify::Branch
Properties:
BranchName: main
AppId: !GetAtt AmplifyApp.AppId
EnableAutoBuild: true
Step 7. Next, we will establish the necessary IAM roles and permissions required for the effective operation of Amplify. This template encapsulates all the roles and permissions that Amplify necessitates for its operation.
AmplifyRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- amplify.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: Amplify
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: "amplify:*"
Resource: "*"
Step 8. Next, we will build our API Gateway with CORS configuration. For this project we will set our API Gateway to public access.
NOTE: For adherence to security best practices, it is recommended to configure authorizers on your API Gateway, or alternatively, restrict access exclusively to a Virtual Private Cloud (VPC).
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Stage
Cors:
AllowMethods: "'*'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"
Step 9. Now lets build our resources, API Gateway methods and Lambda functions.
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .
Handler: handler.handler
Runtime: nodejs16.x
Policies: AmazonDynamoDBFullAccess
Events:
PostApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /car
Method: POST
PutApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /car
Method: PUT
DeleteApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /car
Method: DELETE
HealthCheckApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /check
Method: GET
InventoryApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /inventory
Method: GET
This SAM template defines multiple Lambda functions that resides in the current directory (handler.js). It uses Node.js 16x as its runtime, has full access to DynamoDB to perform CRUD operations and is triggered by either GET, POST, PUT or DELETE.
NOTE: As an alternative you can also use Method: ANY for the Path: /car
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .
Handler: handler.handler
Runtime: nodejs16.x
Policies: AmazonDynamoDBFullAccess
Events:
PostPutDeleteApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /car
Method: ANY
HealthCheckApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /check
Method: GET
InventoryApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /inventory
Method: GET
Step 11. We will establish our DynamoDB table along with the corresponding output parameters. For the ProvisionedThroughput settings of our DynamoDB table, we will opt for a 'PAY_PER_REQUEST' pricing model, effectively incurring charges only for the operations we execute. Accordingly, these settings will be configured to 0.
DynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: sam-table
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 0
WriteCapacityUnits: 0
Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
OrderApi:
Description: "API Gateway endpoint URL for Prod stage for Order function"
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/inventory/"
Here is our full template.yml code:
AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Parameters:
GithubRepository:
Type: String
Description: GitHub repository URL
Stage:
Type: String
Description: Deployment stage
Globals:
Function:
Environment:
Variables:
TABLE_NAME: sam-table
Resources:
AmplifyApp:
Type: AWS::Amplify::App
Properties:
Name: amplify-sam-app
Repository: !Ref GithubRepository
IAMServiceRole: !GetAtt AmplifyRole.Arn
EnvironmentVariables:
- Name: ENDPOINT
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/"
AmplifyBranch:
Type: AWS::Amplify::Branch
Properties:
BranchName: main
AppId: !GetAtt AmplifyApp.AppId
EnableAutoBuild: true
AmplifyRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- amplify.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: Amplify
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: "amplify:*"
Resource: "*"
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Stage
Cors:
AllowMethods: "'*'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .
Handler: handler.handler
Runtime: nodejs16.x
Policies: AmazonDynamoDBFullAccess
Events:
PostApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /car
Method: POST
PutApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /car
Method: PUT
DeleteApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /car
Method: DELETE
HealthCheckApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /check
Method: GET
InventoryApi:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /inventory
Method: GET
DynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: sam-table
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 0
WriteCapacityUnits: 0
Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
OrderApi:
Description: "API Gateway endpoint URL for Prod stage for Order function"
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/inventory/"
Step 12. Next, we will generate a handler.js file within our amplify-sam-backend directory. This file is designated to act as our Lambda handler, encompassing its requisite functionalities. Essentially, this is a Node.js script responsible for processing requests from our API Gateway and fetching data from our DynamoDB table via the AWS SDK.
NOTE: I plan to publish an additional blog post that will provide an in-depth explanation of this handler.
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
const dynamodbTableName = process.env.TABLE_NAME;
const checkPath = '/check';
const carPath = '/car';
const inventoryPath = '/inventory';
exports.handler = async function(event) {
console.log('Request event: ', event);
let response;
switch(true) {
case event.httpMethod === 'GET' && event.path === checkPath:
response = buildResponse(200);
break;
case event.httpMethod === 'GET' && event.path === carPath:
response = await getProduct(event.queryStringParameters.id);
break;
case event.httpMethod === 'GET' && event.path === inventoryPath:
response = await getProducts();
break;
case event.httpMethod === 'POST' && event.path === carPath:
response = await saveProduct(JSON.parse(event.body));
break;
case event.httpMethod === 'DELETE' && event.path === carPath:
response = await deleteProduct(JSON.parse(event.body).id);
break;
default:
response = buildResponse(404, '404 Not Found');
}
return response;
}
async function getProduct(id) {
const params = {
TableName: dynamodbTableName,
Key: {
'id': id
}
}
return await dynamodb.get(params).promise().then((response) => {
return buildResponse(200, response.Item);
}, (error) => {
console.error('Get product error: ', error);
});
}
async function getProducts() {
const params = {
TableName: dynamodbTableName
}
const allProducts = await scanDynamoRecords(params, []);
const body = {
inventory: allProducts
}
return buildResponse(200, body);
}
//Recursize function for DynamoDB scan. DynamoDB limit on return on one querey
async function scanDynamoRecords(scanParams, itemArray) {
try {
const dynamoData = await dynamodb.scan(scanParams).promise();
itemArray = itemArray.concat(dynamoData.Items);
if (dynamoData.LastEvaluatedKey) {
scanParams.ExclusiveStartkey = dynamoData.LastEvaluatedKey;
return await scanDynamoRecords(scanParams, itemArray);
}
return itemArray;
} catch(error) {
console.error('DynamoDB scan error: ', error);
}
}
async function saveProduct(requestBody) {
const params = {
TableName: dynamodbTableName,
Item: requestBody
}
return await dynamodb.put(params).promise().then(() => {
const body = {
Operation: 'SAVE',
Message: 'SUCCESS',
Item: requestBody
}
return buildResponse(200, body);
}, (error) => {
console.error('POST error: ', error);
})
}
async function deleteProduct(id) {
const params = {
TableName: dynamodbTableName,
Key: {
'id': id
},
ReturnValues: 'ALL_OLD'
}
return await dynamodb.delete(params).promise().then((response) => {
const body = {
Operation: 'DELETE',
Message: 'SUCCESS',
Item: response
}
return buildResponse(200, body);
}, (error) => {
console.error('Delete error: ', error);
})
}
function buildResponse(statusCode, body) {
return {
statusCode: statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
'Access-Control-Allow-Methods': 'GET, POST, PATCH, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Origin': '*',
'X-Request-With': '*'
},
body: JSON.stringify(body)
}
}
Step 13. Next, we will initiate the deployment of our assets to the cloud using the guided SAM deploy process. This action will result in the creation of our Serverless Application Model (SAM) application.
To commence deployment, navigate to the amplify-sam-backend directory within your terminal and execute the following command:
sam deploy --guided
You will be prompted to enter various parameters:
Stack Name: amplify-sam-backend
AWS region: us-east-1
Parameter GithubRepository: https://github.com/'your github username'/amplify-sam-app
Confirm changes before deploy [Y/n]: Y
Allow SAM CLI role creation [Y/n]: Y
Save arguments to configuration file [Y/n]: Y
SAM configuration file [samconfig.toml]: samconfig.toml
SAM configuration environment [default]:
The SAM CLI will orchestrate the deployment to the cloud, a process that generally requires approximately 2-3 minutes for completion.
Upon successful deployment, navigate to your AWS Management Console and proceed to CloudFormation > amplify-sam-backend > Resources to confirm that the application has been instantiated as expected.
Finally, return to the AWS Management Console and locate AWS Amplify. Identify our application, denoted as "amplify-sam-app," and select it. Once the deployment has successfully concluded, you may click the provided link to verify the application's functionality.
Top comments (0)