I wanted to create a React site with an authentication function easily, but I gave up because it was too much trouble to build a backend each time.
However, we tried AWS Amplify to see if we could easily build a backend.
Since I have tried all the functions I wanted, I would like to make a rough note of how to make it so that it can be reused.
Since it is not a public site, it will not be elaborate, but will include the following minimum necessary functions.
hosting
- Basic Authentication
Certification
- DynamoDB access by authenticator
- Lambda Access by Authenticator
document
Also refer to the help for the command, as the documentation may be out of date.
amplify --help amplify [command] --help
Amplify CLI Installation
Since we will be working with the Amplify CLI, we will first install and initially configure it.
Create a user for Amplify in AWS IAM
- Users must be granted administrative privileges.
Add a user for Amplify to the AWS CLI profile
- See addendum for AWS CLI profile settings
- Amplify CLI Installation
npm install -g @aws-amplify/cli
- Amplify CLI Initial Setup
amplify configure
Project Creation
- React app creation
npx create-react-app my-app --template typescript
Amplify Project Creation
- Execute commands in the React directory
- The
amplify
directory is created, and all Amplify-related files are created under this directory.
amplify init
- Change settings of created projects
amplify configure project
Project Deletion
- Sometimes the removal fails due to dependencies. In such cases, remove them from the Amplify Console as a last resort.
amplify delete
supplementary explanation
- Every project needs one app.
- Multiple React sites cannot be created in one project (as there is only one hosting per project)
- The root directory of the application and the root directory of the project must be the same
hosting
Additional Hosting
- You can choose between Amplify dedicated hosting and hosting using CloudFront and S3, but choose Amplify dedicated hosting.
- Amplify dedicated hosting details are hidden and cannot be tampered with by the user
amplify hosting add
- Deploy the added backend (hosting) to the actual AWS service
amplify push
Upload site content under React's
build
directory to hosting- They also do
amplify push
behind the scenes.
- They also do
amplify publish
- The URL of the hosting site will be displayed, and the site can be accessed from that URL.
Basic authentication for hosting
Amplify Console
- [App Settings]-[Access Control]-[Manage Access]
Amplify command description
backend
amplify [category] add
- Various backend settings added locally
amplify status
- Displays status of local backend configuration
amplify push
- Deploy the backend to the actual AWS service based on the backend configuration added locally.
- Local backend settings are pushed to S3 as data
- Always match S3 data with the state of the backend that is actually currently deployed in AWS
amplify [category] remove
- Delete various backend settings added locally
- To delete a locally deleted backend configuration from the actual backend deployed on the AWS service, use
amplify push
.
amplify pull
- Override local backend configuration with actual backend built on AWS
env
It is possible to build the backend separately into environments such as dev and prod.
amplify env add
- Add a new environment
amplify env checkout [env_name]
- environment switchover
amplify env list
- Environment List
amplify env remove [env_name]
- environment deletion
Certification
Authentication addition
- With
amplify auth import
, you can also use an existing Cognito without creating a new Cognito. Even if you use an existing Cognito, you can change Cognito depending on the environment.
- With
amplify auth add
React Code
- Ensure that only logged-in users can access the site
If you use only
<AmplifyAuthenticator>
, "Loading..." may appear and stop. may appear and stop, so check the login status and display it differently.
import React, { useEffect, useState } from 'react'; import Amplify, { Auth } from 'aws-amplify'; import { AmplifyAuthenticator, AmplifySignOut } from "@aws-amplify/ui-react"; import awsconfig from './aws-exports'; Amplify.configure(awsconfig); function App() { const [username, setUsername] = useState<any>(); useEffect(() => { Auth.currentAuthenticatedUser().then(user => { setUsername(user.username); }).catch(err => { }); }, []); if (!username) { return (<AmplifyAuthenticator />); } return ( <div> <div>{username}</div> <AmplifySignOut /> </div> ); } export default App;
GraphQL
Amplify can also use REST API, but since Lambda Function can also be called from GraphQL, access to the server is consolidated in GraphQL.
Access to backends such as "DynamoDB," "Lambda," and "S3" through GraphQL as a hub.
Add GraphQL
- Authentication is done using Cognito. Then, only logged-in users can access GraphQL.
amplify api add
Schema Editing
- Edit
amplify/backend/api/[api_name]/schema.graphql
- Edit
- Schema Reflection
amplify api gql-compile
supplementary explanation
- GraphQL here refers to AppSync.
- AppSync is a gateway service that receives GraphQL queries and calls various AWS services such as DynamoDB, Lambda, and S3
amplify/backend/api/[api_name]/schema.graphql
is a schema that serves as a template- Generate the actual GraphQL schema from the template with the
amplify api gql-compile
command amplify/backend/api/[api_name]/build/schema.graphql
is the actual GraphQL schema generated from the template- When you write a GraphQL query, you will be looking at this schema
DynamoDB
By simply writing a GraphQL schema, you can use GraphQL queries to read and write data in DynamoDB tables. (Convenient!)
Schema Editing
- Edit
amplify/backend/api/[api_name]/schema.graphql
- Edit
How to write schema (basic)
simple
type Todo @model { id: ID! name: String! description: String }
- Table is created in DynamoDB with
@model
The
id
column is required as the primary key. If it is omitted, it will be added automatically.- No primary sort key
- Creation date
createdAt
and update dateupdatedAt
columns are automatically added. - Anyone can create, edit, and delete.
No Subscription
type Todo @model(subscriptions: null) { id: ID! name: String! description: String }
- Do not generate schema for subscriptions
Schema Writing (Owner)
Only the owner can Read, Edit, and Delete.
type Todo @model @auth(rules: [{ allow: owner }]) { id: ID! name: String! description: String }
- If
@auth
is added, the owner information (owner
) column is automatically added to allow access control. - Specify the subject with
allow
and the action that only the subject can perform withoperations
. - Actions not specified in
operations
can be done by others. - If
operations
is not specified, all actions are covered. In other words, only the person designated byallow
can do it.
Have owner information, but allow everyone to Read, Edit, and Delete.
type Todo @model @auth(rules: [{ allow: owner, operations: [create] }]) { id: ID! name: String! description: String }
- Only creation (CREATE) is restricted to the owner (OWNER). In other words, other users can Read, Edit, and Delete.
Only the owner and the Admin group can Read, Edit, and Delete.
type Todo @model @auth(rules: [{ allow: owner }]) @auth(rules: [{ allow: groups, groups: ["Admin"]}]) { id: ID! name: String! description: String }
- More than one
@auth
can be written. - Group permissions can be set with
groups
.
How to write schema (index)
DynamoDB index can be created with @key
.
- Enable to get values in specified columns
- Allows sorting on specified columns
type Todo @model @auth(rules: [{allow: owner}]) @key(name: "byOwner", fields: ["owner", "createdAt"], queryField: "listTodoByOwner") { id: ID! name: String! description: String owner: String createdAt: AWSDateTime! }
name
is the index name- In the
fields
array, the first is the partition key and the second is the sort key. The sort key can be omitted. queryField
is the function name for query. If omitted, no query is created.
GraphQL (Client)
- Called by
API.graphql({query, variables})
- You may write the query directly.
Type Definition
After pushing api, amplify codegen
- TypeScript schema type definitions are generated in
./API
- A sample query is generated for
./graphql/[queries|mutations|subscriptions]
Update queries with type definitions in
amplify codegen
when the GraphQL schema is edited- However, schema information is obtained from the actual AppSync, so this should be done after
amplify push
.
- However, schema information is obtained from the actual AppSync, so this should be done after
Usage examples
import { API } from 'aws-amplify'; import { getTodo } from './graphql/queries'; const result: any = await API.graphql({ query: getTodo, variables: { id: '123' } }); console.log(result.data.getTodo);
Usage example (directly writing a query)
const rawQuery = /* GraphQL */ ` query ($id: ID!) { getTodo(id: $id) { id name } } `; const result: any = await API.graphql({ query: rawQuery, variables: { id: '123' } }); console.log(result.data.getTodo);
Example of use (ToDo application)
schema.graphql
type Todo @model @auth(rules: [{ allow: owner }]) @key(name: "byOwner", fields: ["owner", "createdAt"], queryField: "listTodosByOwner") { id: ID! name: String! owner: String! createdAt: AWSDateTime! }
App.tsx
import React, { useEffect, useState } from 'react'; import { Box, Button, Input, Flex, Spacer } from "@chakra-ui/react" import { atom, CallbackInterface, useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import Amplify, { API, Auth } from 'aws-amplify'; import { AmplifyAuthenticator, AmplifySignOut } from "@aws-amplify/ui-react"; import awsconfig from './aws-exports'; import { listTodosByOwner } from './graphql/queries'; import { createTodo, deleteTodo } from './graphql/mutations'; import { Todo, ListTodosByOwnerQuery } from './API'; import { GraphQLResult } from '@aws-amplify/api-graphql'; Amplify.configure(awsconfig); const usernameAtom = atom<string>({ key: `usernameAtom`, default: '' }); const todoListAtom = atom<Array<Todo>>({ key: `todoListAtom`, default: [] }); const loadTodoListAtom: (a: CallbackInterface) => () => void = ({ snapshot, set }) => async () => { const username = await snapshot.getPromise(usernameAtom); if (username) { const result = (await API.graphql({ query: listTodosByOwner, variables: { owner: username, sortDirection: "DESC" } }) as GraphQLResult<ListTodosByOwnerQuery>); const items = result.data?.listTodosByOwner?.items; if (items) { set(todoListAtom, (items as Array<Todo>)); } } }; function App() { const [username, setUsername] = useRecoilState(usernameAtom); const todoList = useRecoilValue(todoListAtom); const loadTodoList = useRecoilCallback(loadTodoListAtom); const [inputText, setInputText] = useState<string>(''); useEffect(() => { Auth.currentAuthenticatedUser().then((user) => { if (username !== user.username) { setUsername(user.username); } loadTodoList(); }).catch(err => { }); }, [username]); if (!username) { return (<AmplifyAuthenticator />); } const todoListDom = todoList.map((v) => { return ( <Flex key={v.id} my="2" border="1px" borderColor="gray.300" p="2" borderRadius="md"> <Box> <Box fontWeight="bold">{v.name}</Box> <Box>{v.createdAt}</Box> </Box> <Spacer /> <Button onClick={async () => { await API.graphql({ query: deleteTodo, variables: { input: { id: v.id } } }); await loadTodoList(); }}>delete</Button> </Flex> ); }); return ( <Box p="4"> <Flex my="2"> <Input mr="2" type="text" value={inputText} onChange={(e) => { setInputText(e.target.value) }} /> <Button onClick={async () => { if (inputText.length === 0) { return; } await API.graphql({ query: createTodo, variables: { input: { owner: username, name: inputText } } }); setInputText(''); await loadTodoList(); }}>add</Button> </Flex> <Box> {todoListDom} </Box> <AmplifySignOut /> </Box> ); } export default App;
Lambda Function
Add a Lambda Function so that it can be called from GraphQL.
Lambda Function追加
amplify function add
GraphQL schema editing
- Edit
amplify/backend/api/[api_name]/schema.graphql
- Call the Lambda specified by
@function
.
- Edit
type Post{ id: ID! msg: String! comment: [Comment!] @function(name: "[function_name]-${env}") } type Comment{ postID: ID! msg: String! } type Query { getPost(id: ID!): Post @function(name: "[function_name]-${env}") }
Local execution (mock)
- Specify event data with
--event
When adding environment variables, prefix them with
[env_name]=[env_value] ...
.- Add
AWS_REGION
environment variable when an error occurs in a region during local execution
- Add
- Specify event data with
amplify mock function [function_name] --event=[event_json_file_path]
AWS_REGION=ap-northeast-1 \ amplify mock function [function_name] --event [event_json_file_path]
Lambda Function 実装
Excerpts from the following documents
event data
- Query contents are stored in event
Determine from the event where in the query the Function was called.
- Often create a resolver and use it for discriminative use, as in the sample in the document
(Example) When calling from getPost()
{ typeName: "Query", fieldName: "getPost", arguments: { id: "123" }, identity: { username: "...." /* cognito username */ } }
(Example) When calling from Post.comment
{ typeName: "Post", fieldName: "comment", arguments: { }, source: { id: "123", msg: "..." } identity: { username: "...." /* cognito username */ } }
Mounting example
const posts = [ { id: 1, msg: "POST_01" }, { id: 2, msg: "POST_02" }, { id: 3, msg: "POST_03" } ]; const comments = [ { postID: 1, msg: "COMMENT_01_A" }, { postID: 1, msg: "COMMENT_01_B" }, { postID: 3, msg: "COMMENT_03_C" } ]; const resolvers = { Query: { getPost: queryGetPost }, Post: { comment: postComment } } function queryGetPost(event) { console.log(event); return posts.find(v => v.id === Number.parseInt(event.arguments.id)); } function postComment(event) { console.log(event); return comments.filter(v => v.postID === event.source.id); } exports.handler = async (event) => { const typeHandler = resolvers[event.typeName]; if (typeHandler) { const resolver = typeHandler[event.fieldName]; if (resolver) { return await resolver(event); } } throw new Error("Resolver not found."); };
Retrieve Cognito user information from within a Lambda Function
Granting access to Cognito to a Function
- The function's environment variable
AUTH_[cognito_name]_USERPOOLID
stores the cognito userpoolid, which is used to access cognito.
- The function's environment variable
amplify function update
- mounting
import { CognitoIdentityServiceProvider } from 'aws-sdk'; const cognito = new CognitoIdentityServiceProvider(); const USER_POOL_ID = process.env.AUTH_[cognito_name]_USERPOOLID; exports.handler = async (event) => { const cognitoUser = await cognito.adminGetUser({ UserPoolId: USER_POOL_ID, Username: event.identity.username }).promise(); return JSON.stringify(cognitoUser); };
Call GraphQL from within a Lambda Function
Make Cognito the default authentication for GraphQL (AppSync), add authentication by API Key, and from Lambda Function, call GraphQL using API Key.
Add authentication method to GraphQL
- Add API Key Authentication
amplify api update
Grant access to GraphQL to Lambda Function
The GraphQL URL and API Key environment variable are created and passed to the Lambda Function.
API_[api_name]_GRAPHQLAPIENDPOINTOUTPUT
API_[api_name]_GRAPHQLAPIKEYOUTPUT
- Environment variables must be set to
amplify push
to match those set in AppSync before use.
amplify function update
GraphQL schema editing
- Edit
amplify/backend/api/[api_name]/schema.graphql
- Default authentication is Cognito, so API Key authentication is not available.
- Add
@aws_api_key
to the schema so that it can be accessed with API Key authentication.
- Edit
type Todo @model @auth(rules: [{allow: owner}]) @aws_api_key { id: ID! name: String! owner: String! } type Mutation @aws_api_key{ sample: String @function(name: "[function_name]-${env}") }
mounting
- Set the API Key in the header
- Calling method is the same as normal GraphQL
import axios from 'axios'; const GRAPHQL_EP = process.env.API_AMPLIFYTEST_GRAPHQLAPIENDPOINTOUTPUT; const GRAPHQL_KEY = process.env.API_AMPLIFYTEST_GRAPHQLAPIKEYOUTPUT; const createTodo = /* GraphQL */ ` mutation CreateTodo( $input: CreateTodoInput! $condition: ModelTodoConditionInput ) { createTodo(input: $input, condition: $condition) { id name owner createdAt updatedAt } } `; exports.handler = async (event) => { const variables = { input: { name: "TEST", owner: "owner_test" } }; const graphqlData = await axios({ url: GRAPHQL_EP, method: 'post', headers: { 'x-api-key': GRAPHQL_KEY }, data: { query: createTodo, variables } }); return JSON.stringify(graphqlData.data); };
Pass any environment variable to the Lambda Function
Directly in the CloudFormation template
amplify/backend/function/[function_name]/[function_name]-cloudformation-template.json
Using AWS Secrets Manager
Impressions, etc.
I tried to use it without spending too much time, but it was quite difficult because of the various functions involved.
When an error occurs in Amplify or CloudFormation, you have to solve it by yourself, and since the error is caused by the actual infrastructure, you need to have some knowledge of the back-end.
However, the backend that Amplify creates is safe for production use. Considering that considerable skills are required to create a backend of the same level, we thought that the merit of using Amplify was sufficient.