Amazon Web Services ブログ

AWS Lambda から Amazon DynamoDB Accelerator(DAX)を使用して、コストを削減しながらパフォーマンスを向上させる

Amazon DynamoDB Accelerator (DAX) で AWS Lambda を使用すると、 も使用するサーバーレスアプリケーションにいくつかの利点があります。DAX は、読み取りレイテンシを大幅に短縮することにより、DynamoDB を使用する場合と比較して、アプリケーションの応答時間を向上させることができます。また、DAX を使用すると、読み取り負荷の高いアプリケーションに必要なプロビジョニングされた読み取りスループットの量を減らすことで、DynamoDB のコストを削減できます。サーバーレスアプリケーションの場合、DAX には、次のようなメリットがあります。レイテンシが短くなると、Lambda 関数の実行時間が短縮され、コストが削減されます。

Lambda 関数から DAX クラスターに接続するには、特別な設定が必要です。この記事では、AWS Serverless Application Model (AWS SAM) に基づいた URL 短縮アプリケーションの例を示します。このアプリケーションでは、Amazon API Gateway、Lambda, DynamoDBmDAX、および AWS CloudFormation を使用して、Lambda から DAX にアクセスする方法をデモします。

シンプルなサーバレス URL 短縮機能

この記事のアプリケーション例では、シンプルな URL 短縮機能を示します。ここでは、AWS SAM templates を使用して、API Gateway、Lambda、および DynamoDB の設定を簡易化します。全体の設定は、繰り返し可能な展開のための AWS CloudFormation テンプレートに表示されます。DAX クラスター、ロール、セキュリティグループ、およびサブネットグループを作成するセクションは、SAM テンプレートに依存していないので、通常の AWS CloudFormation テンプレートを使用できます。

すべての AWS services と同様に、DAX は、主にセキュリティを考慮して設計されています。ですから、クライアントは、Virtual Private Cloud (VPC) の一部として、DAX クラスターに接続する必要があり、直接 DAX クラスターにインターネット経由でアクセスすることはできません。したがって、DAX クラスターにアクセスする必要のある Lambda 関数は、そのクラスターにアクセスできる VPC に接続する必要があります。次のセクションの AWS CloudFormation テンプレートには、DAX と Lambda を連携させるために必要なすべての要素と設定が含まれています。アプリケーションのニーズに合わせて、テンプレートをカスタマイズできます。

次の図は、このソリューションを示しています。 ソリューションダイアグラム

ダイアグラムに示されている通り:

  1. クライアントは、API Gateway に HTTP 要求を送信します。
  2. API Gateway 要求を適切な Lambda 関数に転送します。
  3. Lambda 関数は、VPC 内で実行され、DAX クラスターなどの VPC リソースにアクセスできます。
  4. DAX クラスターは、VPC 内にもあります。これは、Lambda 関数がアクセスすることができます。

AWS CloudFormation テンプレート

AWS CloudFormation テンプレート (template.yaml)から始めましょう。コードの最初のセクションには、AWS CloudFormation テンプレート、AWS SAMプロローグ、AWS SAM 関数定義が含まれています。

AWSTemplateFormatVersion: '2010-09-09'
説明:AWS Lambda と AWS CloudFormation でAmazon DynamoDB Accelerator(DAX)を使用する方法を示すサンプルアプリケーションです。
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 フォーマット
  • セキュリティポリシー
  • 言語ランタイム
  • VPC 設定(VpcConfig スタンザ内):Lambda 関数が DAX クラスターにアクセスできるようにします

この例では、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 カラムのみがあります)。ProvisionedThroughput ReadCapacityUnitsは、DAX が読み取りトラフィックのほとんどを処理するため、低く保たれます。DynamoDB は、DAX がアイテムをキャッシュしていない場合にのみ呼び出されます。

テンプレートは、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

デモ用に1つのdax.t2.small ノードを使用してノードを使用してクラスタを作成します。本番環境でのワークロードでは、冗長性のために、少なくとも 3 つのクラスターサイズ(ReplicationFactor)を使用し、適切なサイズの dax.r4.* インスタンス(NodeType)の使用を検討する必要があります。getUrlRole スタンザは、DAX クラスタに DynamoDB データへのアクセス権を与える AWS Identity and Access Management (IAM) ロールを定義します。(このロールを作成した後に、編集または削除しないでください。クラスタは DynamoDB にアクセスできなくなります。)

次に、テンプレートは、Lambda がトラフィックをTCP ポート 8111 の DAX に送信できるようにするルールを持つセキュリティグループを設定します。この記事の前半にあるサーバーレス関数定義を見ると、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 Region の最初に使用可能な Availability Zone にサブネットが追加され、そのサブネット用の DAX サブネットグループが作成されます。DAX は、サブネットグループ内のサブネットを使用して、Cluster Nodes の配布方法を決定します。本番環境で使用する場合は、複数の Availability Zones で複数のノードを使用して冗長性を確保することを強くお勧めします。それぞれの Availability Zone では、独自のサブネットを作成して、サブネットグループに追加する必要があります。

コード

簡単にするために、URL 短縮コードを 1 つのファイル(lambda/index.js)に示します。コードの動作: POST リクエストはURL を受け取り、そのハッシュを作成し、そのハッシュを DynamoDB に格納し、ハッシュを返します。そのハッシュに対する GET リクエストは、DynamoDB で URL を検索し、実際の URL にリダイレクトします。完全なコード例は GitHub で利用できます。

const AWS = require('aws-sdk');
const AmaxonDaxClient = require('amazon-dax-client');
const crypto = require('crypto');

// これをファイルレベルで保存して、ラムダの実行の間に保存されるようにします
var dynamodb;

exports.handler = function(event, context, callback) {
  event.headers = event.headers || [];
  main(event, context, callback);
};

function main(event, context, callback) {
  // 「dynamodb」変数が初期化されていなければそれを初期化します。これにより 
  // 初期化は、Lambda 実行間で共有されるので、 
  // 実行時間が短くなります。ラムダがコンテナをリサイクルしなければならない場合、これは再実行されるか、 
  // 新しいインスタンスを使用します。
  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 は、dynamodb-local または別のローカルテスト環境で lambda-local
      // を使用する場合に設定できます
      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();
      }
    }
  }

  // HTTP メソッドに応じて、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);
  }
}

// データベースから URL を取得して返します
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);
    }
  });
}

/**
 * URL ごとに固有の ID を計算します。
 *
 * これを行うには、URL の MD5 ハッシュを取り、最初の 40 ビットを抽出し、 
 * それを Base32 記数法で返します。
 *
 * ソルトが提供されている場合は、最初に URL に追加してください。
 * これはハッシュの衝突を解決します。
 * 
 */
function computeId(url, salt) {
  if(salt) {
    url = salt + '$' + url
  }

  // デモンストレーションの目的であれば、MD5 は問題ありません
  let md5 = crypto.createHash('md5');

  // MD5 を計算し、最初の 40 ビットのみを使用します
  let h = md5.update(url).digest('hex').slice(0, 10);

  // 結果を Base32 で返します(したがって、40 ビット、8 * 5)
  return parseInt(h, 16).toString(32);
}

// URLをデータベースに保存します
function setUrl(url, callback, salt) {
  let id = computeId(url, salt);

  const params = {
    TableName: process.env.DDB_TABLE,
    Item: {
      id: { S: id },
      target: { S: url }
    },
    // puts が冪等であることを確認します。
    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...');
        // 試行した ID をソルトとして再試行してください。
        // 最終的に、衝突しません。
        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);
    }
  });
}

// これで終わりです。Lambda、指定されたパラメータをクライアントに返します
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 は、クラスター endpoint です。この例では、これらの変数は AWS CloudFormation テンプレートで自動的に設定されます。

dynamodb のインスタンスはグローバルスコープにあり、関数の実行の間に存続します。最初の実行時に初期化され、基礎となるLambda インスタンスが存在する限り存在し続けます。その結果、すべての実行時ごとに再接続する必要はありません、DAX を使用すると高価な操作になる可能性があります。直接 DynamoDB アクセスと DAX アクセスの両方で、dynamodb インスタンスを再利用することにより、コードはDynamoDB と DAX クライアントが初期化コードを除いてソース互換であることも示しています。

パッケージ情報

最後に必要なのは、npmpackage.json(最も一般的なJavaScript パッケージマネージャ)です。サンプルの依存関係の適切なバージョンを見つけてダウンロードすることができます。

{
  "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

#必要なフォルダに zip ファイルを作成します
zip -qur geturl node_modules lambda

このコードは、Lamdaパッケージである 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 endpoint を使用して、URL 短縮機能にアクセスできます。API Gateway によって作成された URL には、各 endpoint に固有のREST ID が含まれています。AWS CLI を使用して、サンプルの endpoint の ID を見つけることができます。

#AWS CLI を使用して API Gateway の REST ID を検索します
gwId=$(aws apigateway get-rest-apis --query "items[?name == 'geturl'].id | [0]" --output text)
# REST ID を使用して endpoint URL を構成します
endpointUrl="https://$gwId.execute-api.region.amazonaws.com/Prod"

URL を短縮するには、次のコマンドを使用します。

curl -d 'https://www.amazon.com' "$endpointUrl"

このコマンドは、URL に移動するために使用できる「スラッグ」を返します。

curl -v "$endpointUrl/$slug"

Amazon Route 53 を使用してカスタム URL を作成することもできます。

結論

この記事では、AWS CloudFormation を使用して、DAX と DynamoDBを使用するLambda関数を作成して、シンプルな URL 短縮機能を実装する方法を示しました。AWS CloudFormation テンプレートには、Lambda 関数が DAX クラスターにアクセスし、DynamoDB のデータにアクセスするために必要なすべての設定が含まれています。

DAX の高性能とサーバーレスの DAXの高性能とサーバーレスの Lambda アプリケーションを組み合わせることで、パフォーマンスを向上させながらコストを削減することができます。


著者について

ジェフハーディは、アマゾン ウェブ・サービスのソフトウェア開発エンジニアです。