sol 개발 블로그 로고
Published on

AWS Serverless API 만들어보기 (1)

Authors
  • avatar
    Name
    Chan Sol OH
    Twitter

목차

이번 포스팅은 앞으로 있을 짱구 프로젝트를 준비하기 앞서 서버리스 app 개발 준비의 일환이다. 또한 간단한 SAM의 사용법은 AWS SAM 정리와 사용법을 참고하자.

VARTEQ에서 진행한 밴치마크 테스트에서는 Node.js 보다는 검증되고 빠른 속도의 Java with ORM를 이용해서 Lambda 함수를 실행시키는게 좋다고 한다. 하지만, 대부분의 Lambda 예제들이 Node.js로 작성되었기에 이번 예제도 Node.js로 진행한다.

이번 포스팅은 수동으로 DynamoDB에 CRUD하는 Lambda 함수를 API로 만들어서 API Gateway로 배포하는 걸 목표로한다.

AWS 자습서를 참고해서 개발했다.

서버리스 서비스 3계층 구조

DynamoDB

DynamoDB는 10밀리초의 지연시간과 수평 확장(scale up)이 자동화된다. 또한 다중 리전 복제 기능을 가진 NoSQL 데이터베이스인 Amazon Dynamo 글로벌 테이블을 통해 다중 활성 다중 리전 복제(한 리전의 테이블 수정이 다른 리전의 테이블로 복사)를 1초내 지연 시간으로 이뤄서 비즈니스 영속성과 일관성을 유지시킨다. NoSQL이지만, 무려 ACID(원자성, 일관성, 격리성, 내구성)를 지원하기 때문에 트랜젝션 당 최대 100개의 작업에 대해 all-or-nothing을 보장한다.

DynamoDB 시작 방법

  1. DynamoDB 콘솔에서 DynamoDB 콘솔 접속
  2. 테이블 생성
  3. 테이블 명에 http-crud-tutorial-items를 입력
  4. 파티션 키(Partition key)에 id를 입력
DynamoDB 테이블 생성 결과

Lambda

AWS Lambda는 서버를 프로비저닝하지 않고도 코드를 실행시킬 수 있는 컴퓨팅 서비스다. Lambda는 필요할 때만 함수를 실행하고 자동으로 확장되며, 사용한 컴퓨팅 시간만큼만 비용을 지불한다.

Lambda는 함수 당 최대 10GB RAM을 사용할 수 있고, HTTP API(API Gateway, ALB)로 동기적 호출이나 Event Queue를 이용한 비동기적 호출이 가능하다.

Lambda는 단시간에 수요가 몰릴 때 빠르게 스케일 업하고 수요가 없을 때는 0으로 스케일 다운할 때 사용된다.

  • 파일 전처리 : 파일을 S3에 업로드하고, Lambda로 데이터를 실시간으로 처리한다.
  • 스트림 처리 : Amazon Kinesis와 함께 거래 주문 처리, 로그 필터링, 사물 인터넷(IoT) 디바이스 데이터 텔레메트리 및 계측을 위한 실시간 스트리밍 데이터를 처리한다.
  • IoT 백엔드 : Lambda로 서버리스 백엔드를 구축하여 고가용성 구성으로 자동으로 스케일 업/스케일 다운되는 IoT 서드 파티 API 요청을 처리한다.

Lambda 시작 방법

Lambda 생성 후 index.mjs 편집
  1. Lambda 콘솔에서 Lambda 콘솔에 접속
  2. 함수 생성 버튼 클릭
  3. 함수 이름 http-crud-tutorial-function을 입력
  4. 기본 실행 역할 변경에서 AWS 정책 템플릿에서 새 역할 생성을 선택
  5. http-crud-tutorial-role라는 역할 생성
  6. Lambda 함수가 DynamoDB 접속과 CRUD를 할 수 있게하기 위해 Simple microservice permissions를 선택하고 함수 생성
  7. 콘솔에 코드 편집기에서 index.mjs를 아래처럼 편집
index.mjs
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  ScanCommand,
  PutCommand,
  GetCommand,
  DeleteCommand,
} from "@aws-sdk/lib-dynamodb";

// Lambda 함수의 execution context는 추후 다른 Lambda 함수에서도 사용될 수 있도록 일시적으로 저장된다.
// DB 연결, HTTP client, SDK client 같이 시간이 오래 걸리는 외부 연결을 미리해서 함수 속도를 빠르게 할 수 있다.
const client = new DynamoDBClient({});
const dynamo = DynamoDBDocumentClient.from(client);
const tableName = "http-crud-tutorial-items";

export const handler = async (event, context) => {
  let body;
  let statusCode = 200;
  const headers = {
    "Content-Type": "application/json",
  };

  try {
    switch (event.routeKey) {
      case "DELETE /items/{id}": // 특정 item 삭제
        await dynamo.send(
          new DeleteCommand({
            TableName: tableName,
            Key: {
              id: event.pathParameters.id,
            },
          })
        );
        body = `Deleted item ${event.pathParameters.id}`;
        break;
      case "GET /items/{id}": // 특정 item 읽기
        body = await dynamo.send(
          new GetCommand({
            TableName: tableName,
            Key: {
              id: event.pathParameters.id,
            },
          })
        );
        body = body.Item;
        break;
      case "GET /items": // 테이블의 모든 item 읽기
        body = await dynamo.send(
          new ScanCommand({ TableName: tableName })
        );
        body = body.Items;
        break;
      case "PUT /items": // 테이블에 item 생성
        let requestJSON = JSON.parse(event.body);
        await dynamo.send(
          new PutCommand({
            TableName: tableName,
            Item: {
              id: requestJSON.id,
              price: requestJSON.price,
              name: requestJSON.name,
            },
          })
        );
        body = `Put item ${requestJSON.id}`;
        break;
      default:
        throw new Error(`Unsupported route: "${event.routeKey}"`);
    }
  } catch (err) {
    statusCode = 400;
    body = err.message;
  } finally {
    body = JSON.stringify(body);
  }

  return {
    statusCode,
    body,
    headers,
  };
};

HTTP API

클라이언트가 Lambda 함수를 호출하기 위해선 IAM 자격 증명이 필요하다. 퍼블릭하게 Lambda를 열어둔다면, 모든 클라이언트에게 자격 증명을 제공할 수 없을 것이다. 따라서 Lambda 함수로 클라이언트의 요청을 프록시할 무언가가 필요하다.

ALBAPI Gateway는 Lambda 함수에 대한 Restful API와 WebSocket 엔드포인트를 제공한다.

API Gateway는 자동 스케일링과 dev, test, prod 같은 여러 Lambda 환경을 다룰 수 있고, 인증과 인가 같은 보안 기능도 제공한다. 또한, Swagger나 OpenAPI로 API 문서를 자동으로 생성할 수 있다. 이 밖에 CORS, 모니터링, API key나 API 버저닝 같은 유용한 기능을 제공한다.

HTTP API 생성 방법

  1. API Gateway 콘솔에서 API Gateway 콘솔에 접속
  2. API 유형 선택에서 HTTP API를 선택하고 API 이름에 http-crud-tutorial-api를 작성
  3. 경로 설정과 스테이징은 추후에 진행하기 때문에 스킵하고 생성

HTTP API 경로 생성

경로는 수신된 요청을 백엔드 리소스로 보내는 방법이다. 경로는 HTTP 메서드(get, post,...)와 리소스 경로("/items")로 구성된다. 이번 포스팅은 아래 4가지 경로를 사용한다.

  • GET /items/{id}
  • GET /items
  • PUT /items
  • DELETE /items/{id}
  1. API Gateway 콘솔에서 API Gateway 콘솔에 접속
  2. 생성한 API에서 경로 생성 클릭
  3. 4가지 경로에 맞도록 HTTP 메서드리소스 경로를 설정
API Gateway 경로 생성

Lambda와 API Gateway 연결하기

API Gateway로 외부로 노출되는 경로를 생성했다면, 이 경로와 백엔드 리소스를 연결해야한다.

  1. API Gateway 콘솔에서 API Gateway 콘솔에 접속
  2. 경로를 클릭해 보여지는 통합 연결 클릭
  3. 통합 대상은 Lambda로 설정, 통합 세부 정보에서 Lambda 함수의 리전과 설정할 함수의 ARN을 작성(함수 이름을 작성하면 자동 검색)
  4. 기본 설정된 호출 권한을 건드리면 API Gateway가 Lambda를 호출할 권한이 없게된다. 건들지 말 것
  5. 한 경로에 대해 통합을 생성했다면, 다른 경로는 기존 통합을 그대로 사용할 수 있다.

API 통합 테스트하기

API URL 확인

위 그림처럼 API의 세부 정보 페이지로 가면 API Gateway의 URL을 확인할 수 있다.

  1. 항목 생성 또는 업데이트하기
    solsol~ % curl -X "PUT" -H "Content-Type: application/json" -d "{\"id\": \"123\", \"price\": 12345, \"name\": \"myitem\"}" https://abcdef123.execute-api.us-west-2.amazonaws.com/items
    solsol~ % "Put item 123"
    
  2. 모든 항목 가져오기
    solsol~ % curl -X "GET" https://abcdef123.execute-api.us-west-2.amazonaws.com/items
    solsol~ % [{"price":12345,"id":123,"name":"myitem"}]
    
  3. 항목 가져오기
    solsol~ % curl -X "GET" https://abcdef123.execute-api.us-west-2.amazonaws.com/items/123
    solsol~ % {"price":12345,"id":"123","name":"myitem"}
    

위처럼 클라이언트가 API를 호출할 수 있도록 할 수 있다.