Skip to main content

Serverless With AWS

2021-07-06#

List#

  • DynamoDB
  • Notes App
  • Create

DynamoDB#

AWS DynamoDB

Amazon DynamoDB는 어떤 규모에서도 10밀리초 미만의 성능을 제공하는 키-값 및 문서 데이터베이스입니다.

완전관리형의 내구성이 뛰어난 다중 리전, 다중 활성 데이터베이스로서, 인터넷 규모 애플리케이션을 위한 보안, 백업 및 복원, 인 메모리 캐싱 기능을 기본적으로 제공합니다. DynamoDB는 하루에 10조 개 이상의 요청을 처리할 수 있고, 초당 2,000만 개 이상의 피크 요청을 지원할 수 있습니다.

Lyft, Airbnb, Redfin 등과 같이 세계에서 가장 빠르게 성장하는 다수의 비즈니스뿐만 아니라 삼성, Toyota, Capital One과 같은 엔터프라이즈에서도 자사의 미션 크리티컬 워크로드를 지원하기 위해 DynamoDB의 규모와 성능을 활용하고 있습니다.

Setting DynamoDB#

  • 각 DynamoDB 테이블에는 기본 키가 있으며 한 번 설정하면 변경할 수 없음
  • 기본 키는 테이블의 각 항목을 고유하게 식별하므로 두 항목이 동일한 키를 가질 수 없음

DynamoDB는 두 가지 종류의 기본 키를 지원

  • 파티션 키
  • 파티션 키 및 정렬 키 (복합)

데이터를 쿼리 할 때 추가적인 유연성을 제공하는 복합 기본 키를 사용해본다.

예를 들어 사용자 아이디에 대한 값만 제공하면 userId 해당하는 사용자의 모든 메모를 검색할 수 있다. 또는 userId와 특정 noteId를 제공하여 특정 메모를 검색 할 수 있다.

Create Table#

dynamodb-dashboard-create-table

Set Key#

set-table-primary-key

Set Capacity#

select-on-demand-capacity

Notes App#

Architecture#

Hello World

serverless-hello-world-api-architecture

Notes App

serverless-public-api-architecture

create notes function#

//create.js
import * as uuid from 'uuid';
import AWS from 'aws-sdk';
const dynamoDb = new AWS.DynamoDB.DocumentClient();
export async function main(event, context) {
// Request body is passed in as a JSON encoded string in 'event.body'
const data = JSON.parse(event.body);
const params = {
TableName: process.env.tableName,
Item: {
// DynamoDB Items
userId: '123', // userId
noteId: uuid.v1(), // noteId
content: data.content, // 데이터 콘텐츠
attachment: data.attachment, // 이미지 파일네임 (사용 X)
createdAt: Date.now(), // 현재 시간
},
};
try {
await dynamoDb.put(params).promise();
return {
statusCode: 200,
body: JSON.stringify(params.Item),
};
} catch (e) {
return {
statusCode: 500,
body: JSON.stringify({ error: e.message }),
};
}
}

Configure API Endpoint#

service: notes-api
package:
individually: true
plugins:
- serverless-bundle # 함수 웹팩 번들
- serverless-offline
- serverless-dotenv-plugin # .env 사용
provider:
name: aws
runtime: nodejs12.x
stage: prod
region: ap-northeast-2
environment:
tableName: notes
# 'iamRoleStatements'에 AWS 권한을 정의
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Scan
- dynamodb:Query
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:DescribeTable
Resource: 'arn:aws:dynamodb:ap-northeast-2:*:*'
functions:
# Defines an HTTP API endpoint that calls the main function in create.js
# - path: url path is /notes
# - method: POST request
create:
handler: create.main
events:
- http:
path: notes
method: post

Refactor Code#

// libs/dynamodb-lib.js
import AWS from 'aws-sdk';
const client = new AWS.DynamoDB.DocumentClient();
export default {
get: (params) => client.get(params).promise(),
put: (params) => client.put(params).promise(),
query: (params) => client.query(params).promise(),
update: (params) => client.update(params).promise(),
delete: (params) => client.delete(params).promise(),
};
// libs/handler-lib.js
export default function handler(lambda) {
return async function (event, context) {
let body, statusCode;
try {
// Run the Lambda
body = await lambda(event, context);
statusCode = 200;
} catch (e) {
body = { error: e.message };
statusCode = 500;
}
// Return HTTP response
return {
statusCode,
body: JSON.stringify(body),
};
};
}

Refactor Create.js

import * as uuid from 'uuid';
import handler from './libs/handler-lib';
import dynamoDb from './libs/dynamodb-lib';
export const main = handler(async (event, context) => {
const data = JSON.parse(event.body);
const params = {
TableName: process.env.tableName,
Item: {
// The attributes of the item to be created
userId: '123', // The id of the author
noteId: uuid.v1(), // A unique uuid
content: data.content, // Parsed from request body
attachment: data.attachment, // Parsed from request body
createdAt: Date.now(), // Current Unix timestamp
},
};
await dynamoDb.put(params);
return params.Item;
});

Test#

// mocks/create-event.json
{
"body": "{\"content\":\"hello world\",\"attachment\":\"hello.jpg\"}"
}

폴더 구조#

folder-1

serverless invoke local --function create --path mocks/create-event.json

2021-07-13#

List#

  • Get API
  • List API
  • Update API
  • Delete API
  • Serverless Deploy

get.js#

import handler from './libs/handler-lib';
import dynamoDb from './libs/dynamodb-lib';
// Test -> serverless invoke local --function get --path mocks/get-event.json
export const main = handler(async (event, context) => {
const params = {
TableName: process.env.tableName,
Key: {
userId: '123', // 유저의 ID
noteId: event.pathParameters.id, // 노트의 ID
},
};
const result = await dynamoDb.get(params);
if (!result.Item) {
throw new Error('Item not found.');
}
return result.Item;
});
get:
# Defines an HTTP API endpoint that calls the main function in get.js
# - path: url path is /notes/{id}
# - method: GET request
handler: get.main
events:
- http:
path: notes/{id}
method: get

Test#

// mocks/get-event.json
{
"pathParameters": {
"id": "a80aa570-ddf8-11eb-b5ab-3d8748e0d96c"
}
}
serverless invoke local --function get --path mocks/get-event.json

list.js#

import handler from './libs/handler-lib';
import dynamoDb from './libs/dynamodb-lib';
// Test -> serverless invoke local --function list
export const main = handler(async (event, context) => {
const params = {
TableName: process.env.tableName,
// 'KeyConditionExpression' defines the condition for the query
// - 'userId = :userId': only return items with matching 'userId'
// partition key
KeyConditionExpression: 'userId = :userId',
// 'ExpressionAttributeValues' defines the value in the condition
// - ':userId': defines 'userId' to be the id of the author
ExpressionAttributeValues: {
':userId': '123',
},
};
const result = await dynamoDb.query(params);
// Return the matching list of items in response body
return result.Items;
});
list:
# Defines an HTTP API endpoint that calls the main function in list.js
# - path: url path is /notes
# - method: GET request
handler: list.main
events:
- http:
path: notes
method: get

Test#

serverless invoke local --function list

update.js#

import handler from './libs/handler-lib';
import dynamoDb from './libs/dynamodb-lib';
// Test -> serverless invoke local --function get --path mocks/update-event.json
export const main = handler(async (event, context) => {
const data = JSON.parse(event.body);
const params = {
TableName: process.env.tableName,
Key: {
userId: '123', // 사용자 id
noteId: event.pathParameters.id, // 노트 id
},
UpdateExpression: 'SET content = :content, attachment = :attachment',
ExpressionAttributeValues: {
':attachment': data.attachment || null,
':content': data.content || null,
},
ReturnValues: 'ALL_NEW',
};
await dynamoDb.update(params);
return { status: true };
});
update:
# Defines an HTTP API endpoint that calls the main function in update.js
# - path: url path is /notes/{id}
# - method: PUT request
handler: update.main
events:
- http:
path: notes/{id}
method: put

Test#

// mocks/update-event.json
{
"body": "{\"content\":\"update world\",\"attachment\":\"new.jpg\"}",
"pathParameters": {
"id": "a80aa570-ddf8-11eb-b5ab-3d8748e0d96c"
}
}
serverless invoke local --function get --path mocks/update-event.json

delete.js#

import handler from './libs/handler-lib';
import dynamoDb from './libs/dynamodb-lib';
// Test -> serverless invoke local --function get --path mocks/delete-event.json
export const main = handler(async (event, context) => {
const params = {
TableName: process.env.tableName,
Key: {
userId: '123', // 사용자 id
noteId: event.pathParameters.id, // 노트 id
},
};
await dynamoDb.delete(params);
return { status: true };
});
delete:
# Defines an HTTP API endpoint that calls the main function in delete.js
# - path: url path is /notes/{id}
# - method: DELETE request
handler: delete.main
events:
- http:
path: notes/{id}
method: delete

Test#

// mocks/delete-event.json
{
"pathParameters": {
"id": "a80aa570-ddf8-11eb-b5ab-3d8748e0d96c"
}
}
serverless invoke local --function get --path mocks/delete-event.json

serverless deploy#

serverless deploy

폴더 구조#

folder-1

참고#

2021-07-20#

List#

  • Auth in Serverless APIs
  • Cognito User Pool

Authenticated API Architecture#

serverless-auth-api-architecture

  • Sign Up
  • Login
  • Authenticated API Requests

Connect API#

  • React 클라이언트는 IAM Auth를 사용하여 보안이 설정된 API Gateway에 요청
  • API Gateway는 사용자가 사용자 풀로 인증되었는지 여부를 자격 증명 풀로 확인
  • 인증 역할을 사용하여 이 사용자가 이 API에 액세스할 수 있는지 확인
  • 모든것이 통과되면 Lambda 함수가 호출되고 자격 증명 풀 사용자 ID를 전달

Cognito#

AWS Cognito

fill-in-user-pool-info

Review Defaults 이메일로 인증 설정#

select-email-address-as-username

Set Default And Create Pool#

user-pool-created

App Clients Create#

fill-user-pool-app-info

App Client Id#

user-pool-app-created

Create Domain Name#

user-pool-domain-name

Fill Identity-pool-info#

fill-identity-pool-info

Fill Authentication-provider-info#

fill-authentication-provider-info

Select edit-policy-document#

select-edit-policy-document

Select email-address-as-username#

select-email-address-as-username

Create

aws cognito-idp sign-up \
--region YOUR_COGNITO_REGION \
--client-id YOUR_COGNITO_APP_CLIENT_ID \
--username admin@example.com \
--password Passw0rd!

SignUp

$ aws cognito-idp admin-confirm-sign-up \
--region YOUR_COGNITO_REGION \
--user-pool-id YOUR_COGNITO_USER_POOL_ID \
--username admin@example.com
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"mobileanalytics:PutEvents",
"cognito-sync:*",
"cognito-identity:*"
],
"Resource": ["*"]
},
{
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": [
"arn:aws:s3:::YOUR_S3_UPLOADS_BUCKET_NAME/private/${cognito-identity.amazonaws.com:sub}/*"
]
},
{
"Effect": "Allow",
"Action": ["execute-api:Invoke"],
"Resource": [
"arn:aws:execute-api:YOUR_API_GATEWAY_REGION:*:YOUR_API_GATEWAY_ID/*/*/*"
]
}
]
}

참고#

2021-07-27#

List#

  • IAM Auth Set
  • Handle CORS in Serverless APIs
  • Handle API Gateway CORS Errors

IAM Auth Set#

인증없이 사용이 가능했던 API설정에 대해 cognito 인증이 완료된 유저만 호출할 수 있도록 변경

functions:
# Defines an HTTP API endpoint that calls the main function in create.js
# - path: url path is /notes
# - method: POST request
# - authorizer: authenticate using the AWS IAM role
create:
handler: create.main
events:
- http:
path: notes
method: post
authorizer: aws_iam
get:
# Defines an HTTP API endpoint that calls the main function in get.js
# - path: url path is /notes/{id}
# - method: GET request
handler: get.main
events:
- http:
path: notes/{id}
method: get
authorizer: aws_iam
list:
# Defines an HTTP API endpoint that calls the main function in list.js
# - path: url path is /notes
# - method: GET request
handler: list.main
events:
- http:
path: notes
method: get
authorizer: aws_iam
update:
# Defines an HTTP API endpoint that calls the main function in update.js
# - path: url path is /notes/{id}
# - method: PUT request
handler: update.main
events:
- http:
path: notes/{id}
method: put
authorizer: aws_iam
delete:
# Defines an HTTP API endpoint that calls the main function in delete.js
# - path: url path is /notes/{id}
# - method: DELETE request
handler: delete.main
events:
- http:
path: notes/{id}
method: delete
authorizer: aws_iam

CognitoIdentityId#

create, get, update, delete, list.js 에서 ID 인자 수정

userId: event.requestContext.identity.cognitoIdentityId, // The id of the author

Handle CORS in Serverless APIs#

CORS란?#

Cross-Origin Resource Sharing(CORS)은 추가적인 HTTP header를 사용해서 애플리케이션이 다른 origin의 리소스에 접근할 수 있도록 하는 메커니즘

다른 origin에서 내 리소스에 함부로 접근하지 못하게 하기 위해 사용되는 기법.
(브라우저가 리소스를 요청할 때 추가적인 헤더에 정보를 담는곳에 CORS관련 내용이 존재한다.)

CORS mozilla

요청 헤더#

  • Origin
  • Access-Control-Request-Method
    • preflight요청을 할 때 실제 요청에서 어떤 메서드를 사용할 것인지 서버에게 알리기 위해 사용됩니다.
  • Access-Control-Request-Headers
    • preflight 요청을 할 때 실제 요청에서 어떤 header를 사용할 것인지 서버에게 알리기 위해 사용됩니다.

응답 헤더#

  • Access-Control-Allow-Origin
    • 브라우저가 해당 origin이 자원에 접근할 수 있도록 허용합니다. 혹은 *은 credentials이 없는 요청에 한해서 모든 origin에서 접근이 가능하도록 허용합니다.
  • Access-Control-Expose-Headers
    • 브라우저가 액세스할 수있는 서버 화이트리스트 헤더를 허용합니다.
  • Access-Control-Max-Age
    • 얼마나 오랫동안 preflight요청이 캐싱 될 수 있는지를 나타낸다.
  • Access-Control-Allow-Credentials  - Credentials가 true 일 때 요청에 대한 응답이 노출될 수 있는지를 나타냅니다.
    • preflight요청에 대한 응답의 일부로 사용되는 경우 실제 자격 증명을 사용하여 실제 요청을 수행 할 수 있는지를 나타냅니다.
    • 간단한 GET 요청은 preflight되지 않으므로 자격 증명이 있는 리소스를 요청하면 헤더가 리소스와 함께 반환되지 않으면 브라우저에서 응답을 무시하고 웹 콘텐츠로 반환하지 않습니다.
  • Access-Control-Allow-Methods
    • preflight`요청에 대한 대한 응답으로 허용되는 메서드들을 나타냅니다.
  • ccess-Control-Allow-Headers
    • preflight요청에 대한 대한 응답으로 실제 요청 시 사용할 수 있는 HTTP 헤더를 나타냅니다.

Serverless API에서 CORS를 지원하려면 두 가지 작업을 수행해야

  1. 실행 전 OPTIONS 요청
  • 특정 유형의 교차 도메인 요청(PUT, DELETE, 인증 헤더가 있는 요청 등)의 경우 브라우저는 먼저 요청 방법 OPTIONS를 사용하여 실행 전 요청을 만든다.

  • 이러한 API는 이 API에 액세스할 수 있는 도메인과 허용되는 HTTP 메서드로 응답해야 한다.

  1. CORS 헤더로 응답
  • 다른 모든 유형의 요청에 대해 적절한 CORS 헤더를 포함해야 한다.
  • 위의 헤더와 마찬가지로 이러한 헤더에는 허용되는 도메인이 포함되어야 한다.

CORS에 더 자세한 내용은 -> Wikipedia 기사

위의 항목을 설정하지 않으면 HTTP 응답에 이와 같은 내용이 표시됨.

No 'Access-Control-Allow-Origin' header is present on the requested resource

serverless.yml에 내용 수정

functions:
# Defines an HTTP API endpoint that calls the main function in create.js
# - path: url path is /notes
# - method: POST request
# - authorizer: authenticate using the AWS IAM role
create:
handler: create.main
events:
- http:
path: notes
method: post
cors: true
authorizer: aws_iam
get:
# Defines an HTTP API endpoint that calls the main function in get.js
# - path: url path is /notes/{id}
# - method: GET request
handler: get.main
events:
- http:
path: notes/{id}
method: get
cors: true
authorizer: aws_iam
list:
# Defines an HTTP API endpoint that calls the main function in list.js
# - path: url path is /notes
# - method: GET request
handler: list.main
events:
- http:
path: notes
method: get
cors: true
authorizer: aws_iam
update:
# Defines an HTTP API endpoint that calls the main function in update.js
# - path: url path is /notes/{id}
# - method: PUT request
handler: update.main
events:
- http:
path: notes/{id}
method: put
cors: true
authorizer: aws_iam
delete:
# Defines an HTTP API endpoint that calls the main function in delete.js
# - path: url path is /notes/{id}
# - method: DELETE request
handler: delete.main
events:
- http:
path: notes/{id}
method: delete
cors: true
authorizer: aws_iam

Lambda Function CORS Header#

libs/handler-lib.js reutrn 문 아래처럼 변경

return {
statusCode,
body: JSON.stringify(body),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true,
},
};

변경 후 list.js로 테스트

serverless invoke local --function list --path mocks/list-event.json

!응답에 headers 정보가 같이 담겨있어야 한다.

{
"statusCode": 200,
"body": "[{\"attachment\":\"hello.jpg\",\"content\":\"hello world\",\"createdAt\":1602891322039,\"noteId\":\"42244c70-1008-11eb-8be9-4b88616c4b39\",\"userId\":\"123\"}]",
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": true
}
}

참고#