Build Modern Serverless Applications with AWS Amplify and AppSync

In the fast-paced field of web applications, containerization has become not only common but the preferred mode of packaging and delivering web applications. Containers allow us to package our applications and deploy them anywhere without having to reconfigure or adapt our applications to the deployment platform.

Amazon Elastic Container Service (Amazon ECS) is the service Amazon provide to run Docker applications on a scalable cluster.

AWS Amplify

AWS Amplify is a set of products and tools that enable mobile and front-end web developers to build and deploy secure, scalable full-stack applications, powered by AWS.

Common Command Line

  • amplify <category> <subcommand>
    • amplify <category> add: Add resources of a category to the cloud.
      • Place a CloudFormation template for the resources of this category in the category’s subdirectory amplify/backend/\<category\>
      • Insert its reference into the above-mentioned root stack as the nested child stack.
      • When working in teams, it is good practice to run an amplify pull before modifying the backend categories.
    • amplify <category> update
    • amplify <category> remove
    • amplify <category> push
  • amplify push: Once you have made your category updates, run the command amplify push to update the cloud resources.
  • amplify pull: Operates similar to a git pull.
  • amplify env <subcommand>: Control multiple environment
    • amplify env add
    • amplify env list
    • amplify env checkout
    • amplify env remove
  • amplify console: Launches the browser directing you to your cloud project in the AWS Amplify Console.
  • amplify delete
  • amplify init: the root stack is created with three resources:
    • IAM role for unauthenticated users
    • IAM role for authenticated users
    • S3 bucket, the deployment bucket, to support this provider’s workflow
  • amplify publish
  • amplify run
  • amplify status

Amplify CLI

The Amplify Command Line Interface (CLI) is a unified tool-chain to create, integrate, and manage the AWS cloud services for your app.

  • Authentication: The Amplify CLI supports configuring many different Authentication and Authorization workflows, including simple and advanced configurations of the login options, triggering Lambda functions during different lifecycle events, and administrative actions which you can optionally expose to your applications.
  • API(GraphQL): The GraphQL Transform provides a simple to use abstraction that helps you quickly create backends for your web and mobile applications on AWS. With the GraphQL Transform, you define your application’s data model using the GraphQL Schema Definition Language (SDL) and the library handles converting your SDL definition into a set of fully descriptive AWS CloudFormation templates that implement your data model.
  • Serverless Functions: You can add a Lambda function to your project which you can use alongside a REST API or as a data source in your GraphQL API using the @function directive.
  • Storage: Amplify CLI’s storage category enables you to create and manage cloud-connected file & data storage. Use the storage category when you need to store:
    1. app content (images, audio, video etc.) in an public, protected or private storage bucket or
    2. app data in a NoSQL database and access it with a REST API + Lambda

Directives

The Amplify CLI provides GraphQL directives to enhance your schema with additional capabilities, such as custom indexes, authorization rules, function triggers and more.

@model: Defines a top level object type in your API that are backed by Amazon DynamoDB

  • Allows you to easily define top level object types in your API that are backed by Amazon DynamoDB.

    1
    2
    3
    4
    5
    6
    # override the names of any generated queries, mutations and subscriptions, or remove operations entirely.
    type Post @model(queries: { get: "post" }, mutations: null, subscriptions: null) {
    id: ID! # id: ID! is a required attribute.
    title: String!
    tags: [String!]!
    }

@key: Configures custom index structures for @model types

  • The @key directive makes it simple to configure custom index structures for @model types.

    1
    directive @key(fields: [String!]!, name: String, queryField: String) on OBJECT
  • A @key without a name specifies the key for the DynamoDB table’s primary index. You may only provide 1 @key without a name per @model type.

  • Argument

    • fields
      • The first field in the list will always be the HASH key.
      • If two fields are provided the second field will be the SORT key.
      • If more than two fields are provided, a single composite SORT key will be created from a combination of fields[1...n].
    • name
      • When provided, specifies the name of the secondary index.
      • When omitted, specifies that the @key is defining the primary index.
    • queryField
      • When defining a secondary index (by specifying the name argument), this specifies that a new top level query field that queries the secondary index should be generated with the given name.
  • Using the new ‘todosByStatus’ query you can fetch todos by ‘status’

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    type Todo @model
    @key(name: "todosByStatus", fields: ["status"], queryField: "todosByStatus") {
    id: ID!
    name: String!
    status: String!
    }

    query todosByStatus {
    todosByStatus(status: "completed") {
    items {
    id
    name
    status
    }
    }
    }

@auth: Defines authorization rules for your @model types and fields

  • Authorization is required for applications to interact with your GraphQL API. API Keys are best used for public APIs (or parts of your schema which you wish to be public) or prototyping, and you must specify the expiration time before deploying.

  • When applied to a type, augments the application with owner and group-based authorization rules.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    directive @auth(rules: [AuthRule!]!) on OBJECT | FIELD_DEFINITION
    input AuthRule {
    allow: AuthStrategy!
    provider: AuthProvider
    ownerField: String # defaults to "owner" when using owner auth
    identityClaim: String # defaults to "username" when using owner auth
    groupClaim: String # defaults to "cognito:groups" when using Group auth
    groups: [String] # Required when using Static Group auth
    groupsField: String # defaults to "groups" when using Dynamic Group auth
    operations: [ModelOperation] # Required for finer control
  • only the owner of the object has the authorization to perform read (getTodo and listTodos), update (updateTodo), and delete (deleteTodo) operations on the owner created object

    1
    2
    3
    4
    5
    6
    type Todo @model
    @auth(rules: [{ allow: owner }]) {
    id: ID!
    updatedAt: AWSDateTime!
    content: String!
    }
  • only the owner of the object has the authorization to perform update (updateTodo) and delete (deleteTodo) operations on the owner created object, but anyone can read them (getTodo, listTodos).

    1
    2
    3
    4
    5
    6
    type Todo @model
    @auth(rules: [{ allow: owner, operations: [create, delete, update] }]) {
    id: ID!
    updatedAt: AWSDateTime!
    content: String!
    }

@connection: Defines 1:1, 1:M, and N:M relationships between @model types

  • Has one: In the simplest case, you can define a one-to-one connection where a project has one team:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type Project @model {
    id: ID!
    name: String
    team: Team @connection
    }

    type Team @model {
    id: ID!
    name: String!
    }
  • Has many: The following schema defines a Post that can have many comments:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    type Post @model {
    id: ID!
    title: String!
    comments: [Comment] @connection(keyName: "byPost", fields: ["id"])
    }

    type Comment @model
    @key(name: "byPost", fields: ["postID", "content"]) {
    id: ID!
    postID: ID!
    content: String!
    }

@function: Configures a Lambda function resolvers for a field

  • The @function directive allows you to quickly & easily configure AWS Lambda resolvers within your AWS AppSync API.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    directive @function(name: String!, region: String) on FIELD_DEFINITION

    # You can connect this function to your AppSync API deployed via Amplify using this schema:

    # Using this as the entry point, you can use a single function to handle many resolvers.

    type Query {
    posts: [Post] @function(name: "GraphQLResolverFunction")
    }
    type Post {
    id: ID!
    title: String!
    comments: [Comment] @function(name: "GraphQLResolverFunction")
    }
    type Comment {
    postId: ID!
    content: String
    }

@http: Configures an HTTP resolver for a field

  • The @http directive allows you to quickly configure HTTP resolvers within your AWS AppSync API.

    1
    2
    3
    4
    5
    6
    directive @http(method: HttpMethod, url: String!, headers: [HttpHeader]) on FIELD_DEFINITION
    enum HttpMethod { PUT POST GET DELETE PATCH }
    input HttpHeader {
    key: String
    value: String
    }
  • The directive allows you to define URL path parameters, and specify a query string and/or specify a request body.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type Post {
    id: ID!
    title: String
    description: String
    views: Int
    }

    type Query {
    listPosts: Post @http(url: "https://www.example.com/posts")
    }

@predictions: Queries an orchestration of AI/ML services such as Amazon Rekognition, Amazon Translate, and/or Amazon Polly

  • The @predictions directive allows you to query an orchestration of AI/ML services such as Amazon Rekognition, Amazon Translate, and/or Amazon Polly.

    1
    2
    3
    4
    5
    6
    7
    directive @predictions(actions: [PredictionsActions!]!) on FIELD_DEFINITION
    enum PredictionsActions {
    identifyText # uses Amazon Rekognition to detect text
    identifyLabels # uses Amazon Rekognition to detect labels
    convertTextToSpeech # uses Amazon Polly in a lambda to output a presigned url to synthesized speech
    translateText # uses Amazon Translate to translate text from source to target language
    }

@searchable: Makes your data searchable by streaming it to Elasticsearch

  • The @searchable directive handles streaming the data of an @model object type to Amazon Elasticsearch Service and configures search resolvers that search that information.

    1
    2
    3
    # Streams data from DynamoDB to Elasticsearch and exposes search capabilities.
    directive @searchable(queries: SearchableQueryMap) on OBJECT
    input SearchableQueryMap { search: String }

@versioned: Defines the versioning and conflict resolution strategy for an @model type

  • The @versioned directive adds object versioning and conflict resolution to a type. Do not use this directive when leveraging DataStore as the conflict detection and resolution features are automatically handled inside AppSync and are incompatible with the @versioned directive.

    1
    directive @versioned(versionField: String = "version", versionInput: String = "expectedVersion") on OBJECT

Team Environment

For multiple environments, Amplify matches the standard Git workflow where you switch between different branches using the env checkout command - similar to running git checkout BRANCHNAME, run amplify env checkout ENVIRONMENT_NAME to switch between environments.

Image


Modeling Relational Data in DynamoDB

Data access patterns

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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
type Order @model
# -----------------------------------------------------------
# 6. Show all open orders within a given date range across all customers
# -----------------------------------------------------------
# The @key byCustomerByStatusByDate enables you to run a query that would work for this access pattern.
# In this example, a composite sort key (combination of two or more keys) with the status and date is used.
# -----------------------------------------------------------
# query getCustomerWithOrdersByStatusDate($customerID: ID!) {
# getCustomer(id: $customerID) {
# ordersByStatusDate (statusDate: {
# between: [{ status: "pending", date: "2018-01-22"},{ status: "pending", date: "2020-10-11"}]})
# {items {}}}}
# -----------------------------------------------------------
@key(name: "byCustomerByStatusByDate", fields: ["customerID", "status", "date"])
# -----------------------------------------------------------
# 5. Get orders for a given customer within a given date range
# -----------------------------------------------------------
# There is a one-to-many relation that lets all the orders of a customer be queried.
# This relationship is created by having the @key name `byCustomerByDate` on the Order model
# that is queried by the connection on the orders field of the Customer model.
# -----------------------------------------------------------
# query getCustomerWithOrdersByDate($customerID: ID!) {
# getCustomer(id: $customerID) {
# ordersByDate(date: {between: [ "2018-01-22", "2020-10-11" ]})
# {items {}}}}
# -----------------------------------------------------------
@key(name: "byCustomerByDate", fields: ["customerID", "date"])
# -----------------------------------------------------------
# 12. Get orders by account representative and date
# -----------------------------------------------------------
# As can be seen in the AccountRepresentative model
# this connection uses the byRepresentativebyDate field on the Order model to create the connection needed.
# -----------------------------------------------------------
# query getOrdersForAccountRepresentative($representativeId: ID!) {
# getAccountRepresentative(id: $representativeId) {
# id
# orders(date: {between: ["2010-01-22", "2020-10-11"]})
# {items {}}}}
# -----------------------------------------------------------
@key(name: "byRepresentativebyDate", fields: ["accountRepresentativeID", "date"])
# -----------------------------------------------------------
# 9. Get all items on order for a given product
# -----------------------------------------------------------
# This access-pattern would use a one-to-many relation from products to orders
# With this query we can get all orders of a given product:
# -----------------------------------------------------------
# query getProductOrders($productID: ID!) {
# getProduct(id: $productID) {
# id
# orders {items {}}}}
# -----------------------------------------------------------
@key(name: "byProduct", fields: ["productID", "id"])
{
id: ID!
customerID: ID!
accountRepresentativeID: ID!
productID: ID!
status: String!
amount: Int!
date: String!
}

type Customer @model
# -----------------------------------------------------------
# 11. Get customers by account representative
# -----------------------------------------------------------
# This uses a one-to-many connection between account representatives and customers
# -----------------------------------------------------------
# query getCustomersForAccountRepresentative($representativeId: ID!) {
# getAccountRepresentative(id: $representativeId) {
# customers
# {items {}}}}
# -----------------------------------------------------------
@key(name: "byRepresentative", fields: ["accountRepresentativeID", "id"]) {
id: ID!
name: String!
phoneNumber: String
accountRepresentativeID: ID!
# 5. Get orders for a given customer within a given date range
ordersByDate: [Order] @connection(keyName: "byCustomerByDate", fields: ["id"])
# 6. Show all open orders within a given date range across all customers
ordersByStatusDate: [Order] @connection(keyName: "byCustomerByStatusByDate", fields: ["id"])
}

type Employee @model
# -----------------------------------------------------------
# 7. See all employees hired recently
# -----------------------------------------------------------
# Query by whether an employee has been hired recently
# -----------------------------------------------------------
# query employeesNewHire {
# employeesNewHire(newHire: "true")
# {items {}}}
# -----------------------------------------------------------
@key(name: "newHire", fields: ["newHire", "id"], queryField: "employeesNewHire")
# -----------------------------------------------------------
# Query and have the results returned by start date
# -----------------------------------------------------------
# query employeesNewHireByDate {
# employeesNewHireByStartDate(newHire: "true")
# {items {}}}
# -----------------------------------------------------------
@key(name: "newHireByStartDate", fields: ["newHire", "startDate"], queryField: "employeesNewHireByStartDate")
# -----------------------------------------------------------
# 2. Query employee details by employee name
# -----------------------------------------------------------
# query employeeByName($name: String!) {
# employeeByName(name: $name) {items {}}}
# -----------------------------------------------------------
@key(name: "byName", fields: ["name", "id"], queryField: "employeeByName")
# -----------------------------------------------------------
# 14. Get all employees with a given job title
# -----------------------------------------------------------
# Using the byTitle @key makes this access pattern quite easy
# -----------------------------------------------------------
# query employeesByJobTitle {
# employeesByJobTitle(jobTitle: "Manager")
# {items {}}}
# -----------------------------------------------------------
@key(name: "byTitle", fields: ["jobTitle", "id"], queryField: "employeesByJobTitle")
# -----------------------------------------------------------
# 8. Find all employees working in a given warehouse
# -----------------------------------------------------------
# This needs a one to many relationship from warehouses to employees
# This connection uses the byWarehouse key on the Employee model.
# -----------------------------------------------------------
# query getWarehouse($warehouseID: ID!) {
# getWarehouse(id: $warehouseID) {
# id
# employees{items {}}}}
# -----------------------------------------------------------
@key(name: "byWarehouse", fields: ["warehouseID", "id"]) {
id: ID!
name: String!
startDate: String!
phoneNumber: String!
warehouseID: ID!
jobTitle: String!
newHire: String! # We have to use String type, because Boolean types cannot be sort keys
}

type Warehouse @model {
id: ID!
# 8. Find all employees working in a given warehouse
employees: [Employee] @connection(keyName: "byWarehouse", fields: ["id"])
}

type AccountRepresentative @model
# -----------------------------------------------------------
# 17. Get sales representatives ranked by order total and sales period
# -----------------------------------------------------------
# The sales period is either a date range or maybe even a month or week.
# Therefore we can set the sales period as a string and query using the combination of salesPeriod and orderTotal.
# We can also set the sortDirection in order to get the return values from largest to smallest
# -----------------------------------------------------------
# query repsByPeriodAndTotal {
# repsByPeriodAndTotal(
# sortDirection: DESC,
# salesPeriod: "January 2019",
# orderTotal: {ge: 1000})
# {items {}}}
# -----------------------------------------------------------
@key(name: "bySalesPeriodByOrderTotal", fields: ["salesPeriod", "orderTotal"], queryField: "repsByPeriodAndTotal") {
id: ID!
# 11. Get customers by account representative
customers: [Customer] @connection(keyName: "byRepresentative", fields: ["id"])
# 12. Get orders by account representative and date
orders: [Order] @connection(keyName: "byRepresentativebyDate", fields: ["id"])
orderTotal: Int
salesPeriod: String
}

type Inventory @model
# -----------------------------------------------------------
# 15. Get inventory by product by warehouse
# -----------------------------------------------------------
# We can also get all inventory from an individual warehouse
# by using the itemsByWarehouseID query created by the byWarehouseID key
# -----------------------------------------------------------
# query byWarehouseId($warehouseID: ID!) {
# itemsByWarehouseID(warehouseID: $warehouseID) {
# items {}}}
# -----------------------------------------------------------
@key(name: "byWarehouseID", fields: ["warehouseID"], queryField: "itemsByWarehouseID")
# -----------------------------------------------------------
# 10. Get current inventories for a product at all warehouses
# -----------------------------------------------------------
# The query needed to get the inventories of a product in all warehouses
# -----------------------------------------------------------
# query getProductInventoryInfo($productID: ID!) {
# getProduct(id: $productID) {
# id
# inventories {items {}}}}
# -----------------------------------------------------------
# 15. Get inventory by product by warehouse
# -----------------------------------------------------------
# Here having the inventories be held in a separate model is particularly useful
# since this model can have its own partition key and sort key
# such that the inventories themselves can be queried as is needed for this access-pattern.
# -----------------------------------------------------------
# query inventoryByProductAndWarehouse($productID: ID!, $warehouseID: ID!) {
# getInventory(productID: $productID, warehouseID: $warehouseID) {
# productID
# warehouseID
# inventoryAmount}}
# -----------------------------------------------------------
@key(fields: ["productID", "warehouseID"]) {
productID: ID!
warehouseID: ID!
inventoryAmount: Int!
}

type Product @model {
id: ID!
name: String!
# 9. Get all items on order for a given product
orders: [Order] @connection(keyName: "byProduct", fields: ["id"])
# 10. Get current inventories for a product at all warehouses
inventories: [Inventory] @connection(fields: ["id"])
}

AWS Lambda in Python

Lambda deployment packages

Your AWS Lambda function’s code consists of scripts or compiled programs and their dependencies. You use a deployment package to deploy your function code to Lambda. Lambda supports two types of deployment packages: container images and .zip files.

Author

Haojun(Vincent) Gao

Posted on

2021-01-10

Updated on

2022-02-22

Licensed under

Comments