클라우드/AWS

[Hands-On] DynamoDB with Serverless (e.g. API Gateway+Lambda+DynamoDB)

dayeonsheep 2023. 8. 2. 17:55

Lambda 함수를 사용하여 DynamoDB에서 제품을 삽입, 읽기, 업데이트 및 삭제하는 실습


<전체적인 프로세스>

  1. DynamoDB : AWS Database
  2. Lambda : DynamoDB에 데이터를 Push하는 FaaS (Function as a Service) 서비스
  3. API Gateway : 클라이언트(Postman)은 이 AWS 서비스의 도움으로 Lambda와 상호작용

 

 

Create Serverless REST APIs using - AWS Lambda, DynamoDB, API Gateway, JavaScript

→ In this article, we create an AWS serverless REST API using Lambda, DynamoDB, API Gateway (three AWS services), and JavaScript

aws.plainenglish.io


1. DynamoDB

;Serverless 데이터베이스로데이터를 저장할 제품 table을 생성

테이블 이름과 기본 키 작성

 테이블 생성 완료

 

2. Lambda

;새로운 역할을 가진 Node.js 기술로 빈 Lambda 함수를 생성

;Cloudwatchlogs 및 DynamoDB 전체 액세스라는 두 가지 정책을 첨부

;Lambda 함수의 로직은 로컬 코드 편집기의 JavaScript 파일에서 작성

람다 함수 이름 작성

aws-sdk 모델 import 오류를 막기 위해 Node.js 버전 16.xxx로 다운그레이드 해주기

기본 실행 역할 변경에서 새 역할 생성 후 역할 이름 설정

람다 함수 생성 완료

람다 함수에 정책 연결을 하기 위해

구성->권한 탭 들어가기

역할 이름 들어가준다음에 권한 추가에서 정책 연결 클릭

CloudWatchLogsFullAccess와 AmazonDynamoDBFullAccess 권한 추가

 

3. API Gateway

;Javascript/Lambda 코드는 DynamoDB와 직접 상호작용 불가 -> 이를 위해서 aws 서비스가 필요

;API Gateway는 JavaScript 코드가 람다 함수를 연결하고 실행할 수 있는 REST 환경을 생성

;API Gateway를 통해 *리소스를 생성할 수 있음

*리소스: API 앤드포인트를 의미 -> 리소스의 각 메서드는 postman에서 실행할 URL

3번째 REST API 선택

새 API 생성 후 이름 지정

이제 각 리소스생성 + 각 리소스에 대한 메서드를 생성해줌

API 상태 확인용인 /health 리소스 만들기

리소스 선택 -> 작업 클릭 -> 메서드 생성 클릭해서 방법 유형 선택

각 메서드는 람다 함수에서 트리거 됨

예) GET 유형, 여기서는 람다 함수로 구성, 나중에 람다 함수를 사용하여 이 메서드를 호출함

통합 유형: 람다 함수

람다 프록시 통합 사용 선택하고 가장 가까운 리전 선택한 다음 최근에 생성된 람다 함수 선택 후 저장

실습할 애플리케이션에서는 여러 엔드포인트가 필요하고 

모두 제품의 CRUD에 대한 논리를 작성하는 람다로 구성됨

이렇게 리소스와 메서드를 위와 같은 방식으로 다 만들어 줌

작업 -> API 배포 클릭 

스테이지 탭에서 beta 이름 설정 후 배포

모든 API 메서드는 하나의 엔드포인트를 제공함

람다 함수로 구성된 엔드포인트가 준비됨

 

이제 각 엔드포인트를 호출할 람다 코드를 작성해야함 

4. JavaScript

더보기
const AWS = require("aws-sdk");
const AWS_REGION = "ap-northeast-2";
AWS.config.update({
  region: AWS_REGION,
});
const dynamoDB = new AWS.DynamoDB.DocumentClient();
const dynamoDBTableName = "product-inventory";
// Resources(endpoints) created in API Gateway
const healthPath = "/health";
const productPath = "/product";
const productsPath = "/products";
exports.handler = async function (event) {
  console.log("Request event" + event);
  let response;
  switch (true) {
    case event.httpMethod === "GET" && event.path === healthPath:
      response = buildResponse(200);
      break;
    case event.httpMethod === "GET" && event.path === productPath:
      response = await getProduct(event.queryStringParameters.productId);
      break;
    case event.httpMethod === "GET" && event.path === productsPath:
      response = await getProducts();
      break;
    case event.httpMethod === "POST" && event.path === productPath:
      response = await saveProduct(JSON.parse(event.body));
      break;
    case event.httpMethod === "PATCH" && event.path === productPath:
      const requestBody = JSON.parse(event.body);
      response = await modifyProduct(
        requestBody.productId,
        requestBody.updateKey,
        requestBody.updateValue
      );
    break;
   case event.httpMethod === "DELETE" && event.path === productPath:
      response = await deleteProduct(JSON.parse(event.body).productId);
      break;
default:
    response = buildResponse(404, "404 Not Found");
}
return response;
};
// Get Specific Product
async function getProduct(productId) {
  const params = {
      TableName: dynamoDBTableName,
      Key: {
        productId: productId,
      },
  };
  return await dynamoDB
      .get(params)
      .promise()
      .then((response) => {
         return buildResponse(200, response.Item);
      },
   (err) => console.log("ERROR: ", err)
);
}

// Gets all products
async function getProducts() {
  const params = { TableName: dynamoDBTableName };
const allProducts = await scanDynamoRecords(params, []);
const body = {
products: allProducts,
};
return buildResponse(200, body);
}
async function scanDynamoRecords(scanParams, itemArray) {
  try {
    // Read Dynamo DB data, pushing into array
    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 (err) {
    console.log("ERROR in Scan Dynamo Records: ", err);
}
}
// Add a Product 
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);
     },(err) => {
      console.log("ERROR in Save Product: ", err);
     }
  );
}
async function modifyProduct(productId, updateKey, updateValue) {
  const params = {
    TableName: dynamoDBTableName,
    Key: {
      productId: productId,
    },
    UpdateExpression: `set ${updateKey} = :value`,
    ExpressionAttributeValues: {
      ":value": updateValue,
    },
    ReturnValues: "UPDATED_NEW",
 };
return await dynamoDB
  .update(params)
  .promise()
  .then(
    (response) => {
      const body = {
         Operation: "UPDATE",
         Message: "SUCCESS",
         UpdatedAttributes: response,
      };
     return buildResponse(200, body);
  }, (err) => {
     console.log("ERROR in Update Product: ", err);
  }
 );
}
// Delete a Product
async function deleteProduct(productId) {
  const params = {
    TableName: dynamoDBTableName,
      Key: {
        productId: productId,
      },
      ReturnValues: "ALL_OLD",
   };
  return await dynamoDB
        .delete(params)
        .promise()
        .then((response) => {
          const body = {
             Operation: "DELETE",
             Message: "SUCCESS",
             Item: response,
          };
        return buildResponse(200, body);
   },
  (err) => {
     console.log("ERROR in Delete Product: ", err);
   }
 );
 }
// For specific response structure
function buildResponse(statusCode, body) {
  return {
    statusCode,
    headers: {
       "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  };
}

위 코드를 복사하여

람다 함수에 붙여넣고 테스트 및 배포 클릭

테스트 뜰 때 이런 게 나오는데 

대강 이렇게 설정하고 저장해줬다... 

배포했는데

statusCode가 성공적인 200이 아니고 404로 떠서 어라 싶었는데

 

5. Postman

;API 확인을 해보자

포스트맨 처음 써봐서

저기 New 누르고 HTTP 들어가줌

POST - 상품 추가 API

{
  "productId": "103",
  "productName": "Nescafe Classic",
  "price": 50,
  "color": "classic",
  "inventory": 2000
}

이거 복붙해서 Body 데이터에 넣고 Send 해주면

밑에 성공적으로 정보가 뜬다!

HTTP 메소드 중에서 POST는 서버한테 데이터 처리해달라는 거여서 body에 데이터 같이 보내줘야 한다고 함!!!

GET - 상품 조회 API

productID=103 key-value값 넣어준 다음 Send 해주면

이녀석도 정보가 잘 뜬다

 

예이 잘 돼서 신남

 

근데 이제 404 뜨는 이유를 다시 찾아봐야...