Drag-and-Drop Development on Serverless Applications: Using AWS Application Composer and AWS Serverless Application Model
AWS Application Composer and AWS Serverless Application Model (SAM) can help you visually design and build serverless applications. Here's how.
- How to get started with a blank canvas and build a SAM application
- How to load an existing SAM application, visualize and optionally extend it
About | |
---|---|
✅ AWS experience | 100 - Beginner |
⏱ Time to complete | 30 minutes |
💰 Cost to complete | Free tier eligible |
🧩 Prerequisites | - AWS Account and the CLI installed - Any Supported Local IDE from list - AWS SAM CLI - Node and NPM installed |
💻 Code Sample | Code sample used in tutorial on Serverless Land |
📢 Feedback | Any feedback, issues, or just a 👍 / 👎 ? |
⏰ Last Updated | 2023-06-09 |
buildon-sam-app
. We will place all the contents from this blog post into this folder.Create project
button in the Application Composer Dashboard to create a new project. As displayed in the image below, for Type of Project
, select the radio button for New blank project
.- In the Connected mode, we can provide application composer access to a local folder on our computer. It will then automatically sync and save our template file and project folders locally as we design.
- In the Unconnected mode, we have to manually import and export our template files. We have to select
Menu
>save changes
as often as needed to save and download the latest configuration of our template file. When designing in unconnected mode, only the application template file is generated and can be manually exported.
Select folder
and provide an empty local folder.Remember all this is happening in our browser! So we have to provide access to the browser to view and edit the local file in Connected mode. When prompted to allow access, select View and Edit files.
- Application template file – When we design in Application Composer, a singular application template file is generated.
- Project folders – When we design a Lambda function, a general Lambda directory structure is generated.
- Backup template file – A backup directory named
.aws-composer
will be created at the root of our project location and will contain a backup copy of our application template file and our project folders.
"Canvas"
and "Template"
. You can switch between the two anytime while designing.Resources
tab. The List
tab is to quickly identify resources already on the canvas/template. To make this pattern, drag and drop components from the Resources
tab onto the Canvas
.Template
. Also, as this is connected mode, these changes are done to our local template file. Any changes we make on either canvas or template are automatically reflected on other side.Canvas
, the template.yaml
is automatically populated with its appropriate yaml definition."Template"
tab. Or we can select the resource on "Canvas"
and click "Details"
to modify resources."Details"
we can see configurable "Resource properties"
on the right. We can rename CloudFormation's logical ID of the resource, quickly add routes, authorizers, and CORS configuration.- Resource:
API Gateway
- Properties:
- Name:
Api
- Routes:
- Method:
GET
; Path:/
- Method:
POST
; Path:/
- Resource:
Lambda Function
- Properties:
- Name:
CustomerFunctionCreate
- Source path:
src/CreateCustomer
- Runtime:
nodejs18.x
- Handler:
index.handler
- Resource:
Lambda Function
- Properties:
- Name:
CustomerFunctionList
- Source path:
src/ListCustomer
- Runtime:
nodejs18.x
- Handler:
index.handler
- Resource:
DynamoDB Table
- Properties:
- LogicalId:
CustomerTable
- Partition key:
id
- Partition key type:
String
Create
and List
customers. We can group these functions into a Functions group
for better visibility. Click on one of the functions and select Group
.CustomerFunctionsGroup
."Tracing"
by default and add Amazon CloudWatch LogGroup for the function. Lambda logs all requests handled by our function and also automatically stores logs generated by our code through Amazon CloudWatch Logs.GET /
route to CustomerFunctionList
and the POST /
to CustomerFunctionCreate
. Also connect both the Lambda functions to the DynamoDB table.- We can manually connect them using the
"Canvas"
as shown in the image above. (Notice that we can only make connections where service integrations are possible.)
"Template"
section to see the design pattern come alive.- template.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
Transform: AWS::Serverless-2016-10-31
Resources:
CustomerFunctionList:
Type: AWS::Serverless::Function
Properties:
Description: !Sub
- Stack ${AWS::StackName} Function ${ResourceName}
- ResourceName: CustomerFunctionList
CodeUri: src/ListCustomer
Handler: index.handler
Runtime: nodejs18.x
MemorySize: 3008
Timeout: 30
Tracing: Active
Events:
Api23GET:
Type: Api
Properties:
Path: /
Method: GET
RestApiId: !Ref Api23
Environment:
Variables:
TABLE_NAME: !Ref CustomerTable
TABLE_ARN: !GetAtt CustomerTable.Arn
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref CustomerTable
CustomerFunctionListLogGroup:
Type: AWS::Logs::LogGroup
DeletionPolicy: Retain
Properties:
LogGroupName: !Sub /aws/lambda/${CustomerFunctionList}
CustomerFunctionCreate:
Type: AWS::Serverless::Function
Properties:
Description: !Sub
- Stack ${AWS::StackName} Function ${ResourceName}
- ResourceName: CustomerFunctionCreate
CodeUri: src/CreateCustomer
Handler: index.handler
Runtime: nodejs18.x
MemorySize: 3008
Timeout: 30
Tracing: Active
Environment:
Variables:
TABLE_NAME: '!Ref CustomerTable'
TABLE_ARN: '!GetAtt CustomerTable.Arn'
TABLE_NAME_2: !Ref CustomerTable
TABLE_ARN_2: !GetAtt CustomerTable.Arn
Events:
Api23POST:
Type: Api
Properties:
Path: /
Method: POST
RestApiId: !Ref Api23
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref CustomerTable
CustomerFunctionCreateLogGroup:
Type: AWS::Logs::LogGroup
DeletionPolicy: Retain
Properties:
LogGroupName: !Sub /aws/lambda/${CustomerFunctionCreate}
CustomerTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: id
KeyType: HASH
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
Api:
Type: AWS::Serverless::Api
Properties:
Name: !Sub
- ${ResourceName} From Stack ${AWS::StackName}
- ResourceName: Api23
StageName: Prod
DefinitionBody:
openapi: '3.0'
info: {}
paths:
/:
get:
x-amazon-apigateway-integration:
httpMethod: POST
type: aws_proxy
uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${CustomerFunctionList.Arn}/invocations
responses: {}
post:
x-amazon-apigateway-integration:
httpMethod: POST
type: aws_proxy
uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${CustomerFunctionCreate.Arn}/invocations
responses: {}
EndpointConfiguration: REGIONAL
TracingEnabled: true
Metadata:
AWS::Composer::Groups:
Group:
Label: CustomerFunctionsGroup
Members:
- CustomerFunctionList
- CustomerFunctionCreate
Outputs:
EndpointUrl:
Description: HTTP REST endpoint URL
Value: !Sub https://${Api23}.execute-api.${AWS::Region}.amazonaws.com/Prod
Output
section at the end of the template.yaml
to get the API Endpoint once the stack is created:1
2
3
4
Outputs:
EndpointUrl:
Description: HTTP REST endpoint URL
Value: !Sub https://${Api}.execute-api.${AWS::Region}.amazonaws.com/Prod
- CreateCustomer/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, PutCommand } = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({});
const ddbClient = DynamoDBDocumentClient.from(client);
exports.handler = async event => {
try {
console.log(JSON.stringify(event, undefined, 2));
const requestBody = JSON.parse(event.body);
const customer = {
id: Date.now().toString(), // Use a timestamp as the ID
...requestBody,
};
console.log(`Adding customer with ID '${customer.id}' to table '${process.env.TABLE_NAME}' with attributes: ${JSON.stringify(customer, null, 2)}`);
const params = {
TableName: process.env.TABLE_NAME,
Item: customer,
};
// Add data to DynamoDB table
await ddbClient.send(new PutCommand(params));
console.log(`Successfully saved customer '${customer.id}'`);
return {
statusCode: 201,
body: JSON.stringify(customer),
};
} catch (err) {
console.error(err);
return {
statusCode: 500,
body: 'Internal Server Error',
};
}
};
- CreateCustomer/package.json
1
2
3
4
5
6
7
{
"name": "function",
"version": "1.0.0",
"devDependencies": {
"@aws-sdk/lib-dynamodb": "^3.214.0"
}
}
- ListCustomer/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const { DynamoDBClient, ScanCommand } = require("@aws-sdk/client-dynamodb");
const client = new DynamoDBClient({});
exports.handler = async event => {
try {
// Log the event argument for debugging and for use in local development.
console.log(JSON.stringify(event, undefined, 2));
console.log(`Listing customers from table '${process.env.TABLE_NAME}'`);
let ids = [];
// Loop over all customers. If there are more customers after a request,
// the response LastEvaluatedKey will have a non-null value that we can
// pass in the next request to continue fetching more customers.
let
lastEvaluatedKey;
do {
const command = new ScanCommand({
TableName: process.env.TABLE_NAME,
ProjectionExpression: "id",
ExclusiveStartKey: lastEvaluatedKey
});
const response = await client.send(command);
const additionalIds = response.Items.map(customer => customer.id);
ids = ids.concat(additionalIds);
lastEvaluatedKey = response.LastEvaluatedKey;
} while (lastEvaluatedKey);
console.log(`Successfully scanned for list of IDs: ${JSON.stringify(ids, null, 2)}`);
return {
statusCode: 200,
body: JSON.stringify({
ids
})
};
} catch (err) {
console.error(`Failed to list customers: ${err.message} (${err.constructor.name})`);
return {
statusCode: 500,
body: "Internal Service Error"
};
}
};
- ListCustomer/package.json
1
2
3
4
5
6
7
{
"name": "function",
"version": "1.0.0",
"devDependencies": {
"@aws-sdk/lib-dynamodb": "^3.214.0"
}
}
1
npm install aws-sdk
sam init
command, as we already have a sample application created for us. We will build, deploy, and test the application in this section..aws-sam
directory and organizes our function dependencies, project code, and project files there.1
sam build
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
➜ buildon-sam-app sam build
Building codeuri: ../buildon-sam-app/src/ListCustomer runtime: nodejs18.x metadata: {} architecture: x86_64 functions: CustomerFunctionList
Running NodejsNpmBuilder:NpmPack
Running NodejsNpmBuilder:CopyNpmrcAndLockfile
Running NodejsNpmBuilder:CopySource
Running NodejsNpmBuilder:NpmInstall
Running NodejsNpmBuilder:CleanUpNpmrc
Running NodejsNpmBuilder:LockfileCleanUp
Building codeuri: ../buildon-sam-app/src/CreateCustomer runtime: nodejs18.x metadata: {} architecture: x86_64 functions: CustomerFunctionCreate
Running NodejsNpmBuilder:NpmPack
Running NodejsNpmBuilder:CopyNpmrcAndLockfile
Running NodejsNpmBuilder:CopySource
Running NodejsNpmBuilder:NpmInstall
Running NodejsNpmBuilder:CleanUpNpmrc
Running NodejsNpmBuilder:LockfileCleanUp
Build Succeeded
Built Artifacts: .aws-sam/build
Built Template : .aws-sam/build/template.yaml
Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided
1
sam deploy --guided
- Stack Name:
buildon-sam-app
- AWS Region:
<REGION>
- #Shows you resources changes to be deployed and require a 'Y' to initiate deploy. Confirm changes before deploy:
y
- #SAM needs permission to be able to create roles to connect to the resources in your template. Allow SAM CLI IAM role creation:
y
- #Preserves the state of previously provisioned resources when an operation fails.
Disable rollback:n
CustomerFunctionList
may not have authorization defined, Is this okay?:y
CustomerFunctionCreate
may not have authorization defined, Is this okay?:y
- Save arguments to configuration file:
y
- SAM configuration file:
samconfig.toml
- SAM configuration environment:
default
- Deploy this changeset?:
y
1
2
3
4
5
6
7
8
9
10
11
12
...
CREATE_COMPLETE AWS::CloudFormation::Stack buildon-sam-app -
------------------------------------------------------------------------
CloudFormation outputs from deployed stack
Outputs
------------------------------------------------------------------------
Key EndpointUrl
Description HTTP REST endpoint URL
Value https://11yakge1yd.execute-api.<REGION>.amazonaws.com/Prod
------------------------------------------------------------------------
Successfully created/updated stack - buildon-sam-app in <REGION>
samconfig.toml
, so that next time we run sam deploy
it gets all the inputs from this file. The S3 bucket name is just an example here that SAM will use, we can set a different default S3 bucket here. The REGION will be your AWS Region.- samconfig.toml
1
2
3
4
5
6
7
8
9
10
11
version = 0.1
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "buildon-sam-app"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-<randomletters>"
s3_prefix = "buildon-sam-app"
region = "REGION"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
<API_ENDPOINT>
with the URL you noted in previous step. It will look something like this: https://<API>.execute-api.<REGION>.amazonaws.com/Prod
.1
2
3
4
5
6
7
8
curl -X POST \
https://<API_ENDPOINT> \
-H 'Content-Type: application/json' \
-d '{
"name": "John Doe",
"email": "johndoe@example.com"
}'
1
{"id":"1680294275167","name":"John Doe","email":"johndoe@example.com"}%
1
curl -X GET https://<API_ENDPOINT>
1
{"ids":[{"S":"1680294275167"}]}%
1
2
git clone https://github.com/aws-samples/fresh-tracks.git
cd fresh-tracks/backend/FreshTracks/
Create project
dialog box, select the radio option Load existing project
.Connected
mode again, and in the Project location
, navigate to the folder fresh-tracks/backend/FreshTracks/
that has this web applications backend template.yaml
. Provide permissions for Application Composer in the browser to View
the folders and files..yaml
files it can find in the folder. Here let's select template.yaml
and select Create
to visualize it.Edit
access and we can see the visualization come to life:- Delete the stack. Replace STACK_NAME with the stack name. In the above example our stack name was
buildon-sam-app
.```bash
sam delete --stack-name STACK_NAME - Confirm the stack has been deleted in the AWS CloudFormation console or using the following command. Replace STACK_NAME with the stack name and ensure the name is within single quotes.```bash
aws cloudformation list-stacks --query "StackSummaries[?contains(StackName,'STACK_NAME')].StackStatus"
- There is no cost for visualizing applications in Application Composer. However, if you run the commands
sam build
andsam deploy
in your AWS Account, you will be charged for the resources it creates. This is outside of the scope of this blog and may go beyond your free-tier limits. - Application Composer is going to help you create the visualized template, but you should continue using your local testing, peer review, or regular deployment through your own current method.
Any opinions in this post are those of the individual author and may not reflect the opinions of AWS.