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 を連携させるために必要なすべての要素と設定が含まれています。アプリケーションのニーズに合わせて、テンプレートをカスタマイズできます。
次の図は、このソリューションを示しています。
ダイアグラムに示されている通り:
- クライアントは、API Gateway に HTTP 要求を送信します。
- API Gateway 要求を適切な Lambda 関数に転送します。
- Lambda 関数は、VPC 内で実行され、DAX クラスターなどの VPC リソースにアクセスできます。
- 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 クライアントが初期化コードを除いてソース互換であることも示しています。
パッケージ情報
最後に必要なのは、npm の package.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シェルから以下のコマンドをすべて実行します。
npm と AWS 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 アプリケーションを組み合わせることで、パフォーマンスを向上させながらコストを削減することができます。
著者について
ジェフハーディは、アマゾン ウェブ・サービスのソフトウェア開発エンジニアです。