Cognitoを使用したアプリケーション作成では、クライアント側のJavaScript の中にDynamoDB をアクセスするコードがあるため、DynamoDBの構造等を推測されてしまいます。
AWS Lambda の関数の中に DynamoDB のアクセスを記述するようにします。
ここでは、以下を実現します。
  1. Lambda 関数から、DynamoDBをアクセスします。
  2. Cognito認証とLambdaで協調しアクセス制限をします。
サンプルコードは、以下からダウンロードできます。

Lambda 関数で使用するIAM Role を作成

Lambda 関数から DynamoDB をアクセスするための、IAM Role を作成します。

ポリシーの作成

  1. AWS IAM コンソール のポリシー画面で、「ポリシーの作成」を選択します。


  2. ポリシーの作成画面で、「Policy Genarator」 を選択します。


  3. アクセス許可の編集画面で下表の通り設定をし、「ステートメントを追加」をクリックし、ステートメントを追加した後、「次のステップ」に進みます。
    設定項目 設定値
    AWS サービス Amazon DynamoDB
    アクション BatchGetItem
    BatchWriteItem
    DeleteItem
    GetItem
    PutItem
    Query
    UpdateItem
    UpdateTable
    Amazon リソースネーム DynamoDB のテーブルのARN


  4. ポリシーの確認画面で、ポリシー名を「mynote-lambda-dynamo」にして、「ポリシーの作成」をクリックします。


ロールの作成

  1. AWS コンソール IAM の Role 画面で、「新しいロールの作成」をクリックします。


  2. ロール名の設定画面で、ロール名を「mynote-lambda-role」として入力します。


  3. ロールタイプの選択画面で、「AWS サービスロール」→「AWS Lambda」を選択します。


  4. ポリシーのアタッチ画面で、作成した mynote-lambda-dynamo ポリシーを選択して「次のステップ」に進みます


  5. 確認画面で「ロールの作成」をクリックし、ロールを作成します。


Lambda 関数を作成

  1. AWS Lambda コンソール で、「Create a Lambda Function」をクリックします。


  2. Step 1: Select blueprint で、Hello-world nodejs を選択します。


  3. Step 2: Configure function で、Name を MyNoteFunc とします。
    Runtime が Node.js になっていることを確認します。


  4. Handler は、 デフォルトのindex.handler のままとします。
    Role は、mynote-lambda-role を選択します。


  5. Code entity type は、 Edit code inline とします。


  6. コードは、以下を入力します。
    <リージョン名> は、DynamoDB を作成したリージョンを指定します。
    console.log('Loading mynote_lambda function');
    
    var AWS = require("aws-sdk");
    
    var m_dynamodbDoc = null;
    var table = "Notes";
    var regin = "<リージョン名>";
    
    
    exports.handler = function(event, context) {
    	switch (event.func) {
    	case 'getNoteList':
    		execGetNoteList(event, context);
    		break;
    	case 'updateNote':
    		execUpdateNote(event, context);
    		break;
    	case 'addNote':
    		execUpdateNote(event, context);
    		break;
    	case 'clearAll':
    		execClearAll(event, context);
    		break;
    	case 'deleteNote':
    		execDeleteNote(event, context);
    		break;
    	default:
    		console.log('unknown event.func=' + event.func);
    		break;
    	}
    };
    
    function execGetNoteList(event, context) {
    	openDb();
    	getNoteList(context.identity.cognitoIdentityId, function(isSuccess, data) {
    		if (isSuccess) {
    			context.succeed({"operation" : "new", "notes" : data});
    		} else {
    			context.fail("failed getNoteList: " + data);
    		}
    	});
    }
    
    function execUpdateNote(event, context) {
    	openDb();
    	updateNote(context.identity.cognitoIdentityId, event.param, function(isSuccess, data) {
    		if (isSuccess) {
    			context.succeed({"operation" : "add", "note" : data});
    		} else {
    			context.fail("failed updateNote: " + data);
    		}
    	});
    }
    
    function execClearAll(event, context) {
    	openDb();
    	clearAll(context.identity.cognitoIdentityId, function(isSuccess, data) {
    		if (isSuccess) {
    			context.succeed({"operation" : "new", "notes" : []});
    		} else {
    			context.fail("failed clearAll: " + data);
    		}
    	});
    }
    
    function execDeleteNote(event, context) {
    	openDb();
    	deleteNote(context.identity.cognitoIdentityId, event.param.date_time, function(isSuccess, data) {
    		if (isSuccess) {
    			context.succeed({"operation" : "delete", "note" : data});
    		} else {
    			context.fail("failed deleteNote: " + data);
    		}
    	});
    }
    
    function openDb() {
    	if (m_dynamodbDoc === null) {
    		console.log('start open dynamoDB');
    		AWS.config.update({
    		  region: region,
    		  endpoint: "https://dynamodb." + region + ".amazonaws.com"
    		});
    		
    		m_dynamodbDoc = new AWS.DynamoDB.DocumentClient();
    		console.log('end open dynamoDB');
    	}
    }
    
    function getNoteList(userId, callback) {
    	console.log("userId", userId);
    	var params = {
    		"TableName" : table,
    		"KeyConditionExpression": "user_id = :userId",
    		"ExpressionAttributeValues": {
    			":userId": userId
    		}
    	};
    	console.log("query param ", JSON.stringify(params));
    	m_dynamodbDoc.query(params, function(err, data) {
    		var notes = [];
    		if (err) {
    			var errStr = "Unable to query. Error:" + JSON.stringify(err, null, 2);
    			console.log(errStr);
    			callback(false, errStr);
    		} else {
    			console.log("Query succeeded.");
    			var itemSize = data.Items.length;
    			if (itemSize === 0) {
    				callback(true, notes);
    			} else {
    				data.Items.forEach(function(item) {
    					console.log(" -", JSON.stringify(item));
    					var note = {
    						"user_id" : item.user_id,
    						"date_time" : item.date_time,
    						"text_type" : item.text_type,
    						"text" : item.text
    					};
    					notes.push(note);
    				});
    				callback(true, notes);
    			}
    		}
    	});
    }
    
    function updateNote(userId, item, callback) {
    	var params = {
    		"TableName" : table,
    		"Item" : {
    			"user_id" : userId,
    			"date_time" : item.date_time,
    			"text_type" : item.text_type,
    			"text" : item.text
    		}
    	};
    	
    	console.log("Adding a new item...", JSON.stringify(params));
    	m_dynamodbDoc.put(params, function(err, data) {
    	    if (err) {
    	        console.error("Unable to add item. Error JSON:", JSON.stringify(err, null, 2));
        	    callback(false, JSON.stringify(err));
    	    } else {
    	        console.log("Added item:", JSON.stringify(data, null, 2));
        	    callback(true, params.Item);
    	    }
    	});
    }
    
    function deleteNote(userId, dateTime, callback) {
    	var data = {
    		user_id: userId,
    		date_time: dateTime
    	};
    	var dataList= [];
    	dataList.push(data);
    	deleteItems(dataList, function(isSuccess, deletedItems) {
    	    if (isSuccess) {
    	        callback(isSuccess, deletedItems[0]);
    	    } else {
    	        callback(isSuccess, deletedItems);
    	    }
    	});
    }
    
    function clearAll(userId, callback) {
    	var params = {
    		"TableName" : table,
    		"KeyConditionExpression": "user_id = :userId",
    		"ExpressionAttributeValues": {
    			":userId": userId
    		}
    	};
    	console.log("query param ", JSON.stringify(params));
    	m_dynamodbDoc.query(params, function(err, data) {
    		var notes = [];
    		if (err) {
    			var errStr = "Unable to query. Error: " + JSON.stringify(err, null, 2);
    			console.log(errStr);
    			callback(false, errStr);
    		} else {
    			console.log("Query succeeded.");
    			deleteItems(data.Items, callback);
    		}
    	});
    }
    
    function deleteItems(items, callback) {
    	params = {
    		"RequestItems": {
    		}
    	};
    	var deletedItems = [];
    	params.RequestItems[table] = [];
    	console.log("test ", JSON.stringify(params));
    	items.forEach(function(item) {
    		console.log(" -", JSON.stringify(item));
    		var request = {
    			"DeleteRequest" : {
    				"Key": {
    					"user_id": item.user_id,
    					"date_time" : item.date_time
    				}
    			}
    		};
    		params.RequestItems[table].push(request);
    		deletedItems.push({
    			"user_id": item.user_id,
    			"date_time" : item.date_time
    		});
    	});
    	console.log("delete req:", JSON.stringify(params));
    	m_dynamodbDoc.batchWrite(params, function(err, data) {
    		if (err) {
    			var errStr = "Unable to batchWriteItem. Error: " + JSON.stringify(err, null, 2);
    			console.log(errStr);
    			callback(false, errStr);
    		} else {
    			console.log("batchWriteItem succeeded.");
    			callback(true, deletedItems);
    		}
    	});
    }
    		
  7. APIの仕様
    NodeJsサーバーでは、Noteリストの取得、Noteの追加等の機能ごとに、REST API を作成しましたが、今回の Lambdaでは、呼び出しする関数を一つにして、ハンドラで、それぞれの要求を振り分けることにしました。
    ハンドラのパラメータの形式は以下のようにしました。
    {
        func: "要求機能",
        param: 要求パラメータ
    }
    		
    ユーザーIdは、Cognito認証して、Lambda Function の invoke をすると、Lambda の context.identity.cognitoIdentity で取得することができるので、パラメータとはしません。
    今回作成した Lambdaの関数では、この context.identity.cognitoIdentity を利用して、認証したユーザー以外での、note データの参照を防ぐようにしています。
    機能 func param
    Noteリストの取得 'getNoteList' {}
    Noteの追加 'addNote'
    {
        date_time : 日時,
        text : テキスト
    }
    					
    Noteの更新 'updateNote'
    {
        date_time : 日時,
        text : テキスト
    }
    					
    Noteの削除 'deleteNote'
    {
        date_time : 日時
    }					
    					
    全Noteの削除 'clearAll' {}
  8. 「Next」→「Create function」をクリックして、Lambda function の作成を終了します。

Lambda関数を呼び出すためのIAM Role を作成

認証したユーザーがLambda をアクセスできるようにするため、ロールを作成します。

ポリシーの作成

  1. AWS IAM コンソールで、「ポリシーの作成」をクリックします。
  2. ポリシーの作成画面で、「Policy Generator」を選択します。
  3. アクセ許可の編集画面で下表のように設定します。
    設定項目 設定値
    AWS サービス AWS Lambda
    アクション Invoke Function
    Amazon リソースネーム DynamoDB のテーブルのARN


  4. 「ステートメントを追加」して「次のステップ」をクリックして、ポリシーの確認画面に進みます。
    ポリシー名を mynote_func_lambda_invoke としました。


  5. 「ポリシーの作成」をクリックし、ポリシーを作成します。

ロールの作成

  1. AWS IAM コンソールのロール画面で、「新しいロールの作成」をクリックします。
  2. ロール名をmynote-cognito-lambdaとします。
  3. ロールタイプの選択で、「IDプロバイダアクセス用のロール」の「ウェブIDプロバイダにアクセスを付与」を選択します。


  4. IDプロバイダをAmazon Cognito、IDプールのID に Cognito のプールID を入力します。


  5. ポリシーのアタッチ画面で、mynote_func_lambda を選択します。


  6. このとき表示されるロールARNを控えておきます。
  7. このRoleをクライアントのCognitoのRoleとして指定することで、Cognito で認証したときに、Lambda Function の MyNoteFunc がinvoke で呼び出せるようになります。

クライアントコードへの組み込み

cognito.js で、認証してユーザーが使用する role ARN に先ほど作成した、ロールを指定するようにします。

function Cognito() {
	var m_roleArn = role の ARN;
	var m_identityPoolId = cognito のプールID;
この指定によって、cognitoに認証したユーザーが、MyNodeFunc の Lambda function にアクセスできるようになります。

Lmbdaアクセスは、dbControllerLambda.js で行います。
Cognitoを使用したアプリケーション作成のときのように、index.html のdbContoller を入れ替えるだけで、Lambdaを使ったアクセスができるようになります。

  • index.html
    function initApp() {
    
    	…
    	g_dbControllerLambda = new DbControllerLambda();
    	g_dbControllerLambda.init();
    	g_doc.setDbController(g_dbControllerLambda);
    		

Lambda 関数の呼び出し

Lambda 関数の呼び出しは、AWS SDK の AWS.Lambda.invoke API で行います。
lambda の関数では、処理の結果で context.fail を返した場合、 AWS SDK の lambda.invoke 関数は、 err ではなく、data.FunctionError が付加され、context.fail の呼び出し情報は、data.Payload に格納されるので、注意が必要です。
以下のコードとなります。
  • dbCmtrollerLambda.js
    function invoke(payLoad, callback) {
    	console.log('AWS.config=');
    	console.log(AWS.config);
    	var lambda = new AWS.Lambda();
    	var params = {
    		FunctionName : 'MyNoteFunc',
    		InvocationType : 'RequestResponse',
    		Payload : JSON.stringify(payLoad)
    	};
    	lambda.invoke(params, function(err, data) {
    		if (err) {
    			console.log('error:' + err);
    			callback(false, err);
    		} else {
    			if (data.FunctionError) {
    				console.log('function error:' + data);
    				callback(false, data.Payload);
    				}
    			} else {
    				console.log(data);
    				callback(true, JSON.parse(data.Payload));
    			}
    		}
    	});
    }
    
    this.getNoteList = function(userId, callback) {
    	var payload = {
    		func : 'getNoteList',
    		param : {}
    	};
    	invoke(payload, function(isSuccess, data) {
    		if (isSuccess) {
    			callback(true, data.operation, data.notes);
    		} else {
    			callback(false, data, null);
    		}
    	});
    };
    		
    その他の機能は、dbControllerLambda.js の実装を参照してください。

動作確認

Node.js のローカルサーバーで動作を確認します。
確認後、AWS S3 にアップロードし、動作確認します。

サンプルコードの実行方法

  • mynote-sample-lambda.zip をダウンロードしてください。
  • ダウンロード後、zip ファイルを展開してください。
  • zipファイルの中には、Node.js のmodule は含まれていないので、npm install で、各 module のインストールが必要です。
    c:\projects>cd mynote
    c:\projects\mynote>npm install
    		
  • mynote\public\index.html のリージョン名を指定してください。
    var g_region = '<リージョン名>';
    		
  • mynote\public\js\cognito.js のIAM Role の ARN, Cognito Identity pool Id を指定してください。
    var m_roleArn = '<Role ARN>';
    var m_identityPoolId = '<cognito identity pool id>';
    		
  • mynote\NoteCreateTable.js のリージョン名を指定してください。
    var region = "<リージョン名>";
    		
  • lambda function は、zip を展開した root directory の mynote_lambda.js にあります。

次回

次は、API Gatewayを使用したアプリケーション作成でAWS API-Gateway 経由で呼び出した Lambda 関数からDynamoDBののアクセスを行います。