BLOG

AWS Lambda에서 Amazon DynamoDB Accelerator(DAX)를 사용하여 비용 절감과 성능 향상하기
작성일: 2018-05-28

AWS LambdaAmazon DynamoDB Accelerator(DAX)를 사용하면 Amazon DynamoDB도 사용할 수 있는 서버가 없는 애플리케이션에 여러 가지 이점이 있습니다. DAX는 DynamoDB를 사용할 때와 비교하여 읽기 지연 시간을 크게 줄여 애플리케이션의 응답 시간을 단축할 수 있습니다. 또한 DAX를 사용하면 읽기 어려운 애플리케이션에 필요한 프로비저닝된 읽기 처리량을 줄여 DynamoDB 비용을 절감할 수 있습니다. 서버가 없는 애플리케이션의 경우, DAX는 추가적인 이점을 제공합니다. 즉, 지연 시간이 짧을수록 Lambda 실행 시간이 단축되므로 비용이 절감됩니다.

 

Lambda 기능에서 DAX 클러스터에 연결하려면 몇 가지 특별한 구성이 필요합니다. 이 게시물에서는 AWS SAM(Serverless Application Model)을 기반으로 하는 URL-단축 애플리케이션의 예를 보여 줍니다. 이 애플리케이션은 Amazon API Gateway, Lambda, DynamoDB, DAX 및 AWS CloudFormation을 사용하여 Lambda에서 DAX에 액세스 하는 방법을 시연합니다.

 

 

간단한 서버 없는 URL 단축 키

 

이 게시물의 예제 어플리케이션은 단순 URL 단축 키입니다. AWS SAM 템플릿을 사용하여 API Gateway, Lambda, DynamoDB에 대한 설정을 단순화합니다. 전체 구성은 반복 가능한 배포를 위해 AWS CloudFormation 템플릿에 표시됩니다. DAX 클러스터, 역할, 보안그룹 및 서브넷 그룹을 생성하는 섹션은 SAM 템플릿에 따라 달라지지 않으며 일반 AWS CloudFormation 템플릿과 함께 사용할 수 있습니다.

 

모든 AWS 서비스와 마찬가지로 DAX도 보안을 기본적으로 고려하여 설계되었습니다. 따라서 클라이언트가 VPC(Virtua Private Cloud)의 일부로 DAX 클러스터에 연결해야 하므로 인터넷을 통해 DAX 클러스터에 직접 액세스 할 수 없습니다. 따라서 DAX 클러스터에 액세스 하는 데 필요한 Lambda 기능을 클러스터에 액세스 할 수 있는 VPC에 연결해야 합니다. 다음 섹션의 AWS CloudFormation 템플릿에는 DAX와 Lambda가 함께 작동하는 데 필요한 모든 요소와 구성이 포함되어 있습니다. 애플리케이션의 요구에 맞게 템플릿을 사용자 지정할 수 있습니다.

 

다음 다이어그램에서는 이 솔루션을 보여 줍니다.

 

 

다이어그램에 표시된 것처럼:

 

  1. 클라이언트가 HTTP요청을 API Gateway로 보냅니다.
  2. API Gateway는 요청을 적절한 Lambda 기능으로 전달합니다.
  3. Lambda 기능은 VPC 내에서 실행되어 DAX 클러스터와 같은 VPC 리소스에 액세스 할 수 있도록 합니다.
  4. 또한 DAX 클러스터는 VPC 내부에도 있으므로 Lambda 기능을 통해 연결할 수 있습니다.

 

 

AWS CloudFormation 템플릿

 

먼저 AWS CloudFormation 템플릿(template.yaml)부터 살펴보겠습니다. 코드의 첫 번째 섹션에는 AWS CloudFormation 템플릿, AWS SAM Prologue 및 AWS SAM 함수 정의가 포함되어 있습니다.

 

AWSTemplateFormatVersion: ‘2010-09-09’

Description: A sample application showing how to use Amazon DynamoDB Accelerator (DAX) with AWS Lambda and AWS CloudFormation.

Transform: AWS::Serverless-2016-10-31

Resources:

  siteFunction:

    Type: AWS::Serverless::Function

    Properties:

      CodeUri: geturl.zip

      Description: Resolve/Store URLs

      Environment:

        Variables:

          DAX_ENDPOINT: !GetAtt getUrlCluster.ClusterDiscoveryEndpoint

          DDB_TABLE: !Ref getUrlTable

      Events:

        getUrl:

          Type: Api

          Properties:

            Method: get

            Path: /{id+}

        postUrl:

          Type: Api

          Properties:

            Method: post

            Path: /

      Handler: lambda/index.handler

      Policies:

          – AmazonDynamoDBFullAccess

          – AWSLambdaVPCAccessExecutionRole

      Runtime: nodejs6.10

      Timeout: 10

      VpcConfig:

        SecurityGroupIds:

            – !GetAtt getUrlSecurityGroup.GroupId

        SubnetIds:

            – !Ref getUrlSubnet

 

 

템플릿의 이 섹션은 다음을 지정합니다.

 

  • 코드 패키지 위치
  • 함수에 사용되는 환경 변수
  • URL 형식
  • 보안 정책
  • 언어 런타임
  • Lambda 기능이 DAX 클러스터에 연결할 수 있도록 해 주는 VPC 구성 (VpcConfig stanza 안에 위치)

 

이 예제에서는 VPC 및 서브넷을 생성하여 파일의 나중에 있는 섹션에 대한 참조를 사용하여 정의합니다. VPC가 이미 있는 경우 기존 식별자를 대신 사용해야 합니다.

 

AWS::Serverless::Function을 사용하면 각 HTTP요청에 대해 Lambda 함수를 호출하는 API Gateway 엔드포인트를 만드는 것 외에도 적절한 사용 권한으로 Lambda 함수 정의를 만듭니다. 사용자는 이 엔드포인트를 통해 URL 단축 키에 액세스 합니다.

이 코드 예제의 다음 섹션에서는 DynamoDB 테이블을 생성합니다.

 

getUrlTable:

    Type: AWS::DynamoDB::Table

    Properties:

      TableName: GetUrl-sample

      AttributeDefinitions:

        –

          AttributeName: id

          AttributeType: S

      KeySchema:

        –

          AttributeName: id

          KeyType: HASH

      ProvisionedThroughput:

        ReadCapacityUnits: 100

        WriteCapacityUnits: 10

 

 

이 테이블에는 해시 키가 하나만 있습니다(KeySchema에는 id열만 있음). DAX가 대부분의 읽기 트래픽을 처리하므로 ProvisionedThroughput ReadCapacityUnits는 낮은 상태로 유지됩니다. DAX가 항목을 캐시 하지 않은 경우에만 DynamoDB를 호출합니다.

 

이제 템플릿이 DAX 클러스터를 지정합니다.

 

getUrlCluster:

    Type: AWS::DAX::Cluster

    Properties:

      ClusterName: getUrl-sample

      Description: Cluster for GetUrl Sample

      IAMRoleARN: !GetAtt getUrlRole.Arn

      NodeType: dax.t2.small

      ReplicationFactor: 1

      SecurityGroupIds:

        – !GetAtt getUrlSecurityGroup.GroupId

      SubnetGroupName: !Ref getUrlSubnetGroup

 

  getUrlRole:

    Type: AWS::IAM::Role

    Properties:

      AssumeRolePolicyDocument:

        Statement:

          – Action:

            – sts:AssumeRole

            Effect: Allow

            Principal:

              Service:

              – dax.amazonaws.com

        Version: ‘2012-10-17’

      ManagedPolicyArns:

        – arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess

      RoleName: getUrl-sample-Role

 

클러스터는 데모 목적으로 단일 dax.t2.small를 사용하여 생성됩니다. 운영 워크 최소 3개의 클러스터 크기 (ReplicationFactor)를 사용해야 하며 적절한 크기의 dax.r4.* 인스턴스(NodeType)를 사용합니다. getUrlRole 스탠자는 DAX 클러스터에서 DynamoDB 데이터에 액세스 할 수 있는 권한을 부여하는 AWS IAM(AWS Identity and Access Management)역할을 정의합니다. (이 역할을 생성한 후 이 역할을 편집하거나 제거하지 마십시오. 그렇지 않으면 클러스터가 DynamoDB에 액세스 할 수 없습니다.)

 

다음으로 템플릿은 Lambda가 TCP포트 8111을 통해 DAX로 트래픽을 보낼 수 있도록 Lambda를허용하여 규칙을 사용해서 보안그룹을 설정합니다. 이 게시물의 앞부분에서 서버 없는 기능 정의를 살펴보면, VpcConfig 스탠자는 보안그룹을 의미합니다. 보안그룹은 VPC에서 네트워크 트래픽의 흐름을 제어합니다.

 

getUrlSecurityGroup:

    Type: AWS::EC2::SecurityGroup

    Properties:

      GroupDescription: Security Group for GetUrl

      GroupName: getUrl-sample

      VpcId: !Ref getUrlVpc

 

  getUrlSecurityGroupIngress:

    Type: AWS::EC2::SecurityGroupIngress

    DependsOn: getUrlSecurityGroup

    Properties:

      GroupId: !GetAtt getUrlSecurityGroup.GroupId

      IpProtocol: tcp

      FromPort: 8111

      ToPort: 8111

      SourceSecurityGroupId: !GetAtt getUrlSecurityGroup.GroupId

 

마지막으로 템플릿은 VPC, 서브넷서브넷 그룹을 포함한 예제에 대한 네트워킹 구성을 생성합니다.

 

getUrlVpc:

    Type: AWS::EC2::VPC

    Properties:

      CidrBlock: 10.0.0.0/16

      EnableDnsHostnames: true

      EnableDnsSupport: true

      InstanceTenancy: default

      Tags:

        – Key: Name

          Value: getUrl-sample

 

getUrlSubnet:

    Type: AWS::EC2::Subnet

    Properties:

      AvailabilityZone:

        Fn::Select:

          – 0

          – Fn::GetAZs: ”

      CidrBlock: 10.0.0.0/20

      Tags:

        – Key: Name

          Value: getUrl-sample

      VpcId: !Ref getUrlVpc

 

  getUrlSubnetGroup:

    Type: AWS::DAX::SubnetGroup

    Properties:

      Description: Subnet group for GetUrl Sample

      SubnetGroupName: getUrl-sample

      SubnetIds:

        – !Ref getUrlSubnet

 

템플릿의 이 부분은 새 VPC를 생성하고 현재 AWS 리전의 사용 가능한 첫번째 가용 영역에 서브넷을 추가한 다음 해당 서브넷에 대한 DAX 서브넷 그룹을 생성합니다. DAX는 서브넷 그룹의 서브넷을 사용하여 클러스터 노드를 배포하는 방법을 결정합니다. 운영 환경에 사용할 경우 이중화를 위해 여러 가용성 영역에서 여러 노드를 사용하는 것이 좋습니다. 각 가용 영역에는 자체 서브넷을 생성하여 서브넷 그룹에 추가해야 합니다.

 

 

코드

 

단순성을 위해 단일 파일(lambda/index.js)에 URL-단축 코드를 제시합니다. 코드 작동 방법: POST요청은 URL을 가져오고, URL의 해시를 생성하고, DynamoDB에 해시를 저장하고, 해시를 반환합니다. 해당 해시에 대한 GET요청은 DynamoDB의 URL을 조회하여 실제 URL로 리디렉션합니다. 전체 코드 예는 GitHub에서 사용할 수 있습니다.

 

const AWS = require(‘aws-sdk’);

const AmaxonDaxClient = require(‘amazon-dax-client’);

const crypto = require(‘crypto’);

 

// Store this at file level so that it is preserved between Lambda executions

var dynamodb;

 

exports.handler = function(event, context, callback) {

  event.headers = event.headers || [];

  main(event, context, callback);

};

 

function main(event, context, callback) {

  // Initialize the ‘dynamodb’ variable if it has not already been done. This

  // allows the initialization to be shared between Lambda runs to reduce

  // execution time. This will be rerun if Lambda has to recycle the container

  // or use a new instance.

  if(!dynamodb) {

    if(process.env.DAX_ENDPOINT) {

      console.log(‘Using DAX endpoint’, process.env.DAX_ENDPOINT);

      dynamodb = new AmaxonDaxClient({endpoints: [process.env.DAX_ENDPOINT]});

    } else {

      // DDB_LOCAL can be set if using lambda-local with dynamodb-local or another local

      // testing environment

      if(process.env.DDB_LOCAL) {

        console.log(‘Using DynamoDB local’);

        dynamodb = new AWS.DynamoDB({endpoint: ‘http://localhost:8000’, region: ‘ddblocal’});

      } else {

        console.log(‘Using DynamoDB’);

        dynamodb = new AWS.DynamoDB();

      }

    }

  }

 

  // Depending on the HTTP method, save or return the URL

  if (event.httpMethod == ‘GET’) {

    return getUrl(event.pathParameters.id, callback);

  } else if (event.httpMethod == ‘POST’ && event.body) {

    return setUrl(event.body, callback);

  } else {

    return done(400, JSON.stringify({error: ‘Missing or invalid HTTP Method’}), ‘application/json’, callback);

  }

}

 

// Get URLs from the database and return

function getUrl(id, callback) {

  const params = {

    TableName: process.env.DDB_TABLE,

    Key: { id: { S: id } }

  };

 

  console.log(‘Fetching URL for’, id);

  dynamodb.getItem(params, (err, data) => {

    if(err) {

      console.error(‘getItem error:’, err);

      return done(500, JSON.stringify({error: ‘Internal Server Error: ‘ + err}), ‘application/json’, callback);

    }

 

    if(data && data.Item && data.Item.target) {

      let url = data.Item.target.S;

      return done(301, url, ‘text/plain’, callback, {Location: url});

    } else {

      return done(404, ‘404 Not Found’, ‘text/plain’, callback);

    }

  });

}

 

/**

 * Compute a unique ID for each URL.

 *

 * To do this, take the MD5 hash of the URL, extract the first 40 bits, and

 * then return that in Base32 representation.

 *

 * If the salt is provided, prepend that to the URL first.

 * This resolves hash collisions.

 *

 */

function computeId(url, salt) {

  if(salt) {

    url = salt + ‘$’ + url

  }

 

  // For demonstration purposes MD5 is fine

  let md5 = crypto.createHash(‘md5’);

 

  // Compute the MD5, and then use only the first 40 bits

  let h = md5.update(url).digest(‘hex’).slice(0, 10);

 

  // Return results in Base32 (hence 40 bits, 8*5)

  return parseInt(h, 16).toString(32);

}

 

// Save the URLs to the database

function setUrl(url, callback, salt) {

  let id = computeId(url, salt);

 

  const params = {

    TableName: process.env.DDB_TABLE,

    Item: {

      id: { S: id },

      target: { S: url }

    },

    // Ensure that puts are idempotent

    ConditionExpression: “attribute_not_exists(id) OR target = :url”,

    ExpressionAttributeValues: {

      “:url”: {S: url}

    }

  };

 

  dynamodb.putItem(params, (err, data) => {

    if (err) {

      if(err.code === ‘ConditionalCheckFailedException’) {

        console.warn(‘Collision on ‘ + id + ‘ for ‘ + url + ‘; retrying…’);

        // Retry with the attempted ID as the salt.

        // Eventually, there will not be a collision.

        return setUrl(url, callback, id);

      } else {

        console.error(‘Dynamo error on save: ‘, err);

        return done(500, JSON.stringify({error: ‘Internal Server Error: ‘ + err}), ‘application/json’, callback);

      }

    } else {

      return done(200, id, ‘text/plain’, callback);

    }

  });

}

 

// We’re done with this. Lambda, return to the client with given parameters

function done(statusCode, body, contentType, callback, headers) {

  full_headers = {

      ‘Content-Type’: contentType

  }

 

  if(headers) {

    full_headers = Object.assign(full_headers, headers);

  }

 

  callback(null, {

    statusCode: statusCode,

    body: body,

    headers: full_headers,

    isBase64Encoded: false,

  });

}

 

 

Lambda 핸들러는 구성에 환경 변수를 사용합니다. DDB_TABLE은 URL 정보가 들어 있는 테이블의 이름이며, DAX_ENDPOINT는 클러스터 엔드포인트입니다. 이 예에서는 AWS CloudFormation 템플릿에 이러한 변수가 자동으로 구성되어 있습니다.

 

dynamodb 인스턴스는 함수 실행 사이에 지속되도록 글로벌 범위에 있습니다. 첫 번째 실행에서 초기화되고 기본 Lambda 인스턴스가 있는 한 계속 존재합니다. 따라서 실행할 때마다 다시 연결할 필요가 없으므로 DAX를 사용할 때 비용이 많이 들 수 있습니다. 직접 DynamoDB 액세스와 DAX 액세스에 모두 dynamodb 인스턴스를 재사용하면 초기화 코드를 제외한 DynamoDB 및 DAX 클라이언트도 소스와 호환됨을 보여 줍니다.

 

 

패키지 정보

 

마지막으로 필요한 부분은 예제의 의존성의 적절한 버전을 찾아 다운로드할 수 있도록 npm(JavaScript 패키지 매니저의 일반적인 경우)에 대한 package.json입니다.

 

{

  “name”: “geturljs”,

  “version”: “1.0.0”,

  “repository”: “https://github.com/aws-samples/amazon-dax-lambda-nodejs-sample”,

  “description”: “Amazon DynamoDB Accelerator (DAX) Lambda Node.js Sample”,

  “main”: “index.js”,

  “scripts”: {

    “test”: “test”

  },

  “author”: “author@example.com”,

  “license”: “MIT”,

  “dependencies”: {

    “amazon-dax-client”: “^1.1.0”,

    “aws-sdk”: “^2.202.0”

  }

}

 

 

배포

 

Lambda 패키지는 배포를 위한. zip파일로 작동합니다. 이 예에서. zip아카이브는 lambda 디렉토리(예시 코드)와 node_modules 디렉토리(종속성의 경우)를 포함해야 Lambda가 함수를 실행하는 데 필요한 모든 것을 가질 수 있습니다. Bash 셸에서 다음 명령을 모두 실행합니다.

 

아직 설치하지 않은 경우 npmAWS CLI를 모두 설치합니다.

 

# Install dependencies with npm

npm install

 

# Make a zip file with necessary folders

zip -qur geturl node_modules lambda

 

이 코드는 Lambda 패키지인 geturl.zip을 만듭니다. AWS CloudFormation에서 찾을 수 있도록 패키지를 넣으려면 Amazon S3 버킷이 필요합니다.

 

aws s3 mb s3://bucket-name

 

그런 다음 해당 버킷에 있는 코드의 AWS CloudFormation 패키지를 만듭니다.

 

aws cloudformation package –template-file template.yaml –output-template-file packaged-template.yaml –s3-bucket bucket-name

 

마지막으로 AWS CloudFormation 스택을 배포하여 모든 리소스를 생성합니다.

 

aws cloudformation deploy –template-file packaged-template.yaml –capabilities CAPABILITY_NAMED_IAM –stack-name geturl

 

URL 단축키 사용하기

 

이제 AWS CloudFormation 템플릿에서 생성한 API Gateway 엔드포인트를 사용하여 URL 단축키에 액세스 할 수 있습니다. API Gateway에 의해 생성된 URL에는 각 엔드포인트와 관련된 REST ID가 포함됩니다. AWS CLI를 사용하여 예제 엔드포인트의 ID를 찾을 수 있습니다.

 

# Use the AWS CLI to find the API Gateway REST ID

gwId=$(aws apigateway get-rest-apis –query “items[?name == ‘geturl’].id | [0]” –output text)

# Construct the endpoint URL using the REST ID

endpointUrl=https://$gwId.execute-api.region.amazonaws.com/Prod

 

URL을 줄이려면 다음 명령을 사용합니다.

 

curl -d ‘https://www.amazon.com’ “$endpointUrl”

 

이 명령은 URL로 이동하는 데 사용할 수 있는 ” slug”를 반환합니다.

 

curl -v “$endpointUrl/$slug”

 

Amazon Route 53을 사용하여 사용자 지정 URL을 만들 수도 있습니다.

 

결론

 

이 글에서는 AWS CloudFormation을 사용하여 DAX 및 DynamoDB를 사용하여 간단한 URL단축키를 구현하는 Lambda 함수를 만드는 방법을 보여 주었습니다. AWS CloudFormation 템플릿에는 Lambda기능이 DAX클러스터에 연결하여 DynamoDB의 데이터에 액세스 하는 데 필요한 모든 구성이 포함되어 있습니다.

 

DAX의 고성능을 서버 없는 Lambda 애플리케이션과 결합하여 성능을 향상시키는 동시에 비용을 절감할 수 있습니다.

 

 

원문 URL: https://aws.amazon.com/ko/blogs/database/how-to-increase-performance-while-reducing-costs-by-using-amazon-dynamodb-accelerator-dax-and-aws-lambda/

** 메가존 TechBlog는 AWS BLOG 영문 게재글중에서 한국 사용자들에게 유용한 정보 및 콘텐츠를 우선적으로 번역하여 내부 엔지니어 검수를 받아서, 정기적으로 게재하고 있습니다. 추가로 번역및 게재를 희망하는 글에 대해서 관리자에게 메일 또는 SNS페이지에 댓글을 남겨주시면, 우선적으로 번역해서 전달해드리도록 하겠습니다.