Lambda, DynamoDB 연결하기

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다. (예시 코드는 works on my machine 상태라 다르게 작동될 수 있습니다.)
  • Udemy강의(AWS Serverless APIs & Apps - A Complete Introduction)를 기반으로 작성되었습니다. 필자의 프로젝트 요구사항에 맞춰 일부 변형하였으니 자세한 이해를 원하신다면 결제해서 수강해보시길 권장드립니다. 좋은 강의라고 생각합니다. 항상 할인 중이니 저렴한 가격에 줍줍하세요
  • Lambda에서 DynamoDB에 접근하여 데이터를 다뤄봅니다.

이번 글에서 할 일

AWS_Serverless_004_01

위 그림에 표시된 영역을 구현해보려 합니다.

Dynamo DB 시작하기

Lambda가 DB 데이터에 접근하기로 결정했으므로 적당히 함수가 처리할 테이블과 데이터를 생성한다.

AWS_Serverless_004_02

임의의 테이블 이름을 입력하고 파티션 키를 설정한다. 파티션 키 이름을 입력한 뒤 우측에 형식을 선택할 수 있다. 페이지에 볼 수 있듯이 SQL에서의 기본키 역할을 한다고 생각하면 된다. 그러므로 중복 값이 존재하면 안된다. 그리고 하단 체크박스에선 기본키와 함께 사용할 수 있는 정렬 키 추가 여부를 묻는다. 정렬 키 역시 중복을 허용하지 않으며 한 테이블에서 특정 기본 키를 갖는 레코드가 중복될 수밖에 없다면 정렬 키를 함께 결합하여 사용할 수 있다. 예를 들어 basic-dynamo 테이블에 user_123321에 대한 두 개의 레코드를 저장하고자 한다면 정렬 키를 추가하여 구분되게끔 저장하는 방식이다. 만들려는 기능은 Id값에 대해 중복 삽입이 불가능하게끔 설계할 예정이므로 정렬 키를 추가하지는 않았다.

AWS_Serverless_004_03

한 가지 참고할 점으로 AWS Dynamo DB는 여러 개의 데이터베이스를 관리하는 구조가 아니다. 한 지역에 한 개의 데이터베이스를 할당하고 테이블을 생성하여 관리한다. 만약 다른 데이터베이스를 추가하고 싶다면 지역을 다른 곳으로 설정한 뒤 생성해야 한다. 적당히 예시 삼아서 항목을 만들어보자.

AWS_Serverless_004_04

미리 지정해둔 기본 키 UserId에 어떤 value를 입력할 것인지 박스가 있고 우측 버튼을 클릭하여 새로운 속성을 추가/삭제가 가능하다. 속성을 추가한다면 자료형까지 지정할 수 있다. 우선 DB에 삽입할 데이터를 고려하여 임의의 레코드를 직접 생성하였다.

AWS_Serverless_004_05

레코드를 삽입하면 웹 콘솔에서 테이블에 저장된 데이터를 볼 수 있다. AWS RDS를 사용하면 단순히 엔진만 지원하기 때문에 별도의 VPC를 설정하고 외부에서 연결하거나 코드를 실행하여 레코드를 조회해야 하는 번거로움이 있는데 DynamoDB를 사용하면 그런 과정 없이 이렇게 확인할 수 있어서 직관적이다.

Lambda에서 DynamoDB 데이터 생성하기

Lambda가 접근할 수 있는 테이블과 레코드를 생성하였으니 이제 Lambda에서 DB 테이블에 저장된 레코드를 조회해보자. Lambda에서 다른 AWS 서비스에 접근하기 위해선 AWS SDK를 사용해야 하고 이를 위해 별도의 모듈을 import 해야한다. 필자는 Lambda 함수를 Python으로 작성하고 있기 때문에 Python 기준으로 설명하였다. Python의 경우엔 Python용 AWS SDK(Boto3)에서 시작하기를 누르면 확인할 수 있다. 다른 언어를 사용한다면 도구 유형 기준으로 찾아보기 - SDK에서 언어별로 접근할 수 있다. 페이지를 참고하여 이전에 만들어 두었던 basic-store-data 함수에 코드를 작성하면 아래와 같이 연결할 수 있다.

import json
import boto3

dynamodb = boto3.client('dynamodb')

def lambda_handler(event, context):
    return {
        "mPrevScore": event['prevScore']*2,
        "mPrevRank": event['prevRank']*2,
        "mCurrScore": event['currScore']*2,
        "mCurrRank": event['currRank']*2
    }

DynamoDB를 연결하는 코드를 핸들러 밖에 작성한 이유는 처음 Lambda 함수가 실행될 때 AWS는 요구에 따라 cold-start를 하지만 함수의 기능이 종료되고도 일정 시간 동안 스트림이 유지된다. 그러므로 특정 시간 대에 사용량이 몰린다거나 빈번하게 호출되는 API라면 굳이 스트림을 새롭게 생성할 필요 없이 이미 로드되어 있으므로 핸들러만 실행해도 됨을 의미한다. 가시적인 퍼포먼스 향상을 기대할 순 없지만 사소하게 활용할 수 있다.

Lambda 함수에서 DynamoDB로 접근할 수 있도록 스트림을 열었으니 이제 레코드를 삽입해보자. 관련 레퍼런스는 위에서 링크했던 Python용 AWS SDK(Boto3)에서 API 참조를 클릭한 뒤 Availalbe services 탭의 DynamoDB 항목을 클릭하면 확인할 수 있다. 문서를 읽어보면 레코드를 삽입하는 함수는 put_item()라고 설명한다. 문서에 따르면 요청하는 스키마는 다음과 같다.

response = client.put_item(
    TableName='string',
    Item={
        'string': {
            'S': 'string',
            'N': 'string',
            'B': b'bytes',
            'SS': [
                'string',
            ],
            'NS': [
                'string',
            ],
            'BS': [
                b'bytes',
            ],
            'M': {
                'string': {'... recursive ...'}
            },
            'L': [
                {'... recursive ...'},
            ],
            'NULL': True|False,
            'BOOL': True|False
        }})

특이한 점으로 string 자료형을 제외한 나머지 Number 형식의 데이터에도 string으로 감싸서 전송해야 한다. 참고해서 적당한 데이터를 함수에 작성한 뒤 전송해보자.

import json
import boto3
from random import randint

dynamodb = boto3.client('dynamodb')

def lambda_handler(event, context):
    response = dynamodb.put_item(
        TableName = 'basic-dynamo',
        Item = {
            'UserId' : {
                'S' : 'user_' + str(randint(1,999999)).zfill(6)
            },
            'prevScore' : {
                'N' : "5066"
            },
            'prevRank' : {
                'N' : "8"
            },
            'currScore' : {
                'N' : "1505"
            },
            'currRank' : {
                'N' : "5"
            }
        })

    return response

UserId는 적당히 중복되지 않게끔 예시로 랜덤 데이터를 넣기로 했다. 실제에선 충돌의 우려가 있기 때문에 하지 말자. 테스트를 실행해보면 권한 에러가 발생한다. AWS에선 원칙적으로 다른 AWS 서비스에 접근할 때 권한을 주지 않으므로 권한 설정을 해주어야 한다. 간단하게 FullAccess를 부여할 수도 있지만 보안 관점에서 좀 더 세밀한 권한 컨트롤이 바람직하므로 함수 별로 권한을 설정하기로 하였다.

IAM 권한 설정

Lambda에게 DynamoDB 접근 권한을 위해 AWS 서비스 탭에서 IAM을 검색해서 들어가자. 역할 탭을 클릭하면 이전에 함수를 생성하면서 생성된 역할들을 확인할 수 있다.

AWS_Serverless_004_06

basic-store-data에 연결된 역할을 클릭하면 현재 기본적인 Lambda 실행 권한만 적용되어 있다. 여기에 정책을 연결하여 권한을 부여한다.

AWS_Serverless_004_07

AWS_Serverless_004_08

DynamoDB와 관련된 정책들을 검색해보니 세밀한 컨트롤을 하기엔 조금 부족해보인다. store 함수는 오로지 put_item 함수만 사용할 수 있도록 권한을 부여하고 싶다면 좌측 정책탭을 클릭하여 정책을 새롭게 생성할 수 있다.

AWS_Serverless_004_09

AWS_Serverless_004_10

정책 생성을 하면 아래와 같이 서비스, 작업, 리소스를 입력하는 화면이 있다. 서비스에 DynamoDB, 작업에 PutItem을 검색하여 선택하자.

AWS_Serverless_004_11

PutItem까지 선택하면 경고를 출력하면서 ARN을 추가하라고 한다. 어떤 테이블을 연결할 지 묻는 항목으로 생성해뒀던 DynamoDB 테이블 페이지로 가서 개요 탭을 보자.

AWS_Serverless_004_12

테이블 개요에 표시된 ARN을 복사한 뒤 다시 IAM 정책 생성 화면으로 가서 ARN 추가를 클릭하고 ARN 지정 칸에 복사해둔 값을 붙여넣으면 자동으로 잡아준다. ARN 추가까지 끝냈으니 정책 검토를 눌러 넘어간다.

AWS_Serverless_004_13

적당히 원하는 정책 이름과 설명을 입력하고 정책 생성을 클릭하여 커스텀 정책을 생성한다. 다시 역할 탭으로 가서 이전에 설정하려 했던 basic-store-data 함수로 들어가 정책을 연결하자.

AWS_Serverless_004_14

DynamoDB를 키워드로 갖는 정책을 검색한 결과 방금 생성했던 커스텀 정책을 확인하였다.

AWS_Serverless_004_15

정책의 내용을 보면 API 버전과 부여할 권한의 성격, 어떤 서비스의 함수에 대해서 부여하는지, 권한이 적용될 리소스는 무엇인지를 정의하고 있다. 좌측 체크박스에 체크하여 연결하자.

AWS_Serverless_004_16

AWS_Serverless_004_17

권한을 부여하여 다시 Lambda 테스트를 실행하면 함수 실행의 성공과 함께 DynamoDB에 데이터가 삽입되었음을 확인할 수 있다. 동일한 방법으로 basic-get-data 함수에 조회 함수(get_item, scan)에 대한 권한을 부여하고 basic-delete-data함수에 삭제 함수(delete_item)에 대한 권한을 부여하여 사용할 수 있다.

AWS_Serverless_004_18

AWS_Serverless_004_19

Lambda에서 DynamoDB 데이터 조회, 삭제하기

위에서 데이터 삽입을 해봤으니 간단하게 데이터를 조회하고 삭제를 해보자. 우선 IAM에서 get_item, scan 함수를 basic-get-data에 부여하고, delete_item 함수를 basic-delete-data에 부여한 뒤 코드를 작성하였다.

import json
import boto3

dynamodb = boto3.client('dynamodb')

def lambda_handler(event, context):
    type = event['type']
    done = False
    start_key = None
    items = []
    if type == 'all':
        while not done:
            if start_key:
                scan_kwargs['ExclusiveStartKey'] = start_key
            response = dynamodb.scan(
                TableName = 'basic-dynamo'
                )
            start_key = response.get('LastEvaluatedKey', None)
            done = start_key is None
        for item in response['Items']:
            items.append({
                'UserId': item['UserId']['S'],
                'prevScore': item['prevScore']['N'],
                'prevRank': item['prevRank']['N'],
                'currScore': item['currScore']['N'],
                'currRank': item['currRank']['N']
            })
        return items
    elif type == 'single':
        item = dynamodb.get_item(
            Key = {
                'UserId': {
                    'S': event['UserId']
                }
            },
            TableName = 'basic-dynamo')['Item']
        return [{
            'UserId': item['UserId']['S'],
            'prevScore': item['prevScore']['N'],
            'prevRank': item['prevRank']['N'],
            'currScore': item['currScore']['N'],
            'currRank': item['currRank']['N']
        }]
    else:
        return "OOPS! Invalid Path Params"

AWS_Serverless_004_20

모든 데이터를 조회하는 scan함수를 실행할 때 추가적인 코드를 작성하였다. DynamoDB는 조금 특이하게 쿼리된 결과가 1MB를 초과하면 직전까지 자른 후 LastEvaluatedKey를 제공한다. 이는 추가로 조회하려면 해당 키 좌표 값부터 시작하라는 의미이다. 따라서, 전체 데이터를 스캔하기 앞서 시작지점을 검사하고 스캔을 한 뒤 조회할 데이터가 더 남아있다면 탐색 시작 순번을 패치한 뒤 다시 탐색을 진행한다. return하는 코드를 보면 위의 자료형 명세에서 볼 수 있듯이 Dict 구조를 따라가서 매핑을 한다. 특이한 점은 키 값에 해당 값이 갖는 자료형을 붙여야 한다는 점이다. 이 때문에 그대로 item리스트를 반환하면 클라이언트에서 처리하기가 번잡하다. 문제점을 해결하기 위해 Lambda 함수에서 따로 매핑을 하였다. 테스트 이벤트 콘솔에서 type 속성과 UserId 속성에 값을 입력하여 테스트 이벤트를 실행하면 성공적으로 데이터가 조회됨을 확인할 수 있다.

import json
import boto3

dynamodb = boto3.client('dynamodb')

def lambda_handler(event, context):
    dynamodb.delete_item(
        Key = {
            'UserId':{
                'S': event["UserId"]
            }
        },
        TableName = 'basic-dynamo')
    return 'deleted UserId: ' + event['UserId']

AWS_Serverless_004_21

AWS_Serverless_004_22

이로써 Lambda 함수에서 DynamoDB로 접근하여 데이터를 삽입, 조회, 삭제까지 모두 구현하였다.

+ Recent posts