AWS Lambda + API Gatewayで今流行りのサーバーレスなWebサービスを使ってみる機会がありました。
Lambda関数を管理コンソール上でインライン編集をしていたのですが、ある時間違えてソースをいじってしまい、関数、ひいてはAPI自体が動かないということに...。
ソース管理は大事!!ということを身にしみて感じたので、Lambdaのソース管理を行う方法を調べました。
- AWS SAM (AWS公式)
- Serverless Framework
2つの方法があることを見つけたのですが、AWS SAMは現時点でAPI GatewayでのCORS対応が行えないようなので、Serverless Frameworkを使用してLambdaのソース管理を行うことにしました。
(ローカルでServerlessのプロジェクトを作成し、gitでソース管理を行う。)
Serverlessで、Lambdaのソース管理も、今までAWS管理コンソール上でポチポチ設定していた内容も一括でローカルからデプロイできるようになりました。
「Serverlessってどんなフレームワークなの?何ができるの?」 →検索してみてください。
参考:Serverless Framework入門
#概要
Lambda + API Gatewayのソース管理をするために、ローカルでServerlessプロジェクトを作成し、AWS上にデプロイする + α を紹介します。
- α では、Serverlessを使ってみて便利だった点、注意する点をメモします。
この記事でやっていること
STEP1: Serverlessで新規テンプレートアプリ作成
STEP2: Lambda + APIGateway サービス作成
STEP3: Serverless 便利なところ、注意するところ
Serverlessを使ってデプロイするAWSサービス
- Lambda
- API Gateway
- DynamoDB
- S3
- IAM ロール
##前提条件
ローカル環境
- Node.jsのインストール(npm使用)
- awscliのインストール
- serverless frameworkのインストール
- (gitのインストール)
その他
- AWSアカウント取得済み
開発環境(動作確認済)
Node.js v6.10
npm v5.3.0
awscli v1.11.123
serverless v1.21.1
#Serverlessで新規テンプレートの作成
※serverless frameworkやawscliのインストール、AWSアカウントとの紐付けが済んでいない場合、こちらを参考にセットアップしてみてください。
参考:今から始めるServerless Frameworkで簡単Lambda開発環境の構築
awscli, serverlessが使用できる状態であるところからスタートします。
$ aws --version
aws-cli/1.11.123 Python/2.7.10 Darwin/16.7.0 botocore/1.5.86
$ serverless --version
1.21.1
##Serverlessの新規テンプレートアプリ作成
任意のディレクトリで以下のコマンドを実行、テンプレートアプリを作成します。
sls create -t <テンプレート種類> -p <プロジェクト名>
*<テンプレート種類>*のところで、使用する言語を選択できます。node以外にもPythonなど使用できます。
ターミナルで実行すると以下のようになります。
$ sls create -t aws-nodejs -p lambdatest
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/yukakoyokota/Desktop/lambdatest"
_______ __
| _ .-----.----.--.--.-----.----| .-----.-----.-----.
| |___| -__| _| | | -__| _| | -__|__ --|__ --|
|____ |_____|__| \___/|_____|__| |__|_____|_____|_____|
| | | The Serverless Application Framework
| | serverless.com, v1.21.1
-------'
Serverless: Successfully generated boilerplate for template: "aws-nodejs"
lambdatestというフォルダが作成されました。
テンプレートアプリ内容
以下の二つのファイルが入っています。
serverless.yml LambdaやAPIGatewayなど、AWSの管理コンソールでポチポチ設定していた内容を記述するファイル(設定関係は全てここに書く)
handler.js Lambda関数のnodeのプログラムを記述するファイル
service: lambdatest # アプリ名
provider: # アプリ全般の設定を記述するところ
name: aws # プロバイダ名
runtime: nodejs6.10 # 言語
functions: # Lambda関数の設定
hello: # 関数名
handler: handler.hello #Lambdaのプログラムが置いてあるパス指定
→ 「helloというLambda関数一つをAWS上にデプロイするよ」(serverless.yml)
'use strict';
module.exports.hello = (event, context, callback) => {
const response = {
statusCode: 200,
body: JSON.stringify({
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
}),
};
callback(null, response);
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event });
};
→ 「index.jsのhelloが呼ばれたら、メッセージ返すよ」(index.js)
##新規プロジェクトのデプロイ
プロジェクトをAWS上にデプロイします。
$ cd lambdatest
$ sls deploy
成功すると、以下のような結果が返ってきます。
Service Information
service: lambdatest
stage: dev
region: us-east-1
stack: lambdatest-dev
api keys:
None
endpoints:
None
functions:
hello: lambdatest-dev-hello
hello関数がAWSにデプロイされました。
yamlでリージョンの設定がない場合、デフォルトでus-east-1に作成されます。
##関数の実行
$ sls invoke -f UserGet
{
"statusCode": 200,
"body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\",\"input\":{}}"
}
関数のローカル実行もできるそうです。
参考:Serverless Framework v1.6.0でPythonのローカル実行がサポートされます
##サービスの削除
デプロイしたサービス群をローカルからコマンドで全て削除することができます。
$ sls remove
オプションで--stage
や--region
などを指定でき、特定のステージやリージョンのサービスを削除することもできるようです。
#Lambda + APIGateway サービス作成
LambdaとAPI Gatewayを統合してAPIを作成する方法です。
##Lambda関数の作成
Lambda関数の作成方法です。
serverless.ymlを以下のように編集します。
service: lambdatest
# 一般設定
provider:
name: aws
runtime: nodejs6.10
stage: dev # サービスのステージ
region: ap-northeast-1 # AWSのリージョン
# 関数定義
functions:
UserGet: # 関数名
handler: manage/project.get # 関数の場所
timeout: 30 # タイムアウトの設定
description: Serverless test # 関数説明
memorySize: 512 # メモリー(default: 1024MB)
ymlファイルで指定したUserGet関数を作成します。
handler
で関数の置いてある場所を指定しています。
handler: manage/project.get
→ manageフォルダの配下のproject.jsのmodule.exports.get
を見に行きます。
以下のようなフォルダ構成にします。
[lambdatest]
- [manage]
- project.js
- serverless.yml
project.jsファイルを以下のように編集します。
'use strict';
module.exports.get = (event, context, callback) => {
const response = {
statusCode: 200,
body: JSON.stringify({
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
}),
};
callback(null, response);
};
##APIGatewayとの統合
serverless.ymlのfunctions:の中に、events:を以下のように追加します。
# 関数定義
functions:
UserGet: # 関数名
handler: manage/project.get # 関数の場所
timeout: 30
description: Serverless test # 関数説明
memorySize: 512 # メモリー(default: 1024MB)
tags:
type: test # タグ
events:
- http: # APIGateway設定
path: manage/project # APIのパス
method: get # HTTPメソッド
eventsセクションで関数を呼び出すトリガーを記述します。
例では、- http:
以下でAPIGatewayのセットをしています。
events:
> - http:
> path
にAPIのパスをセットします。
この場合、/manage/projectでGETリクエストを受けるためAPIが作成されます。
また、tags:
では、key:value
の形で関数にタグをつけることができます。
※ 全ての関数に同じタグをつけたい場合は以下のように設定することもできます。
provider:
name: aws
runtime: nodejs6.10
stage: dev
region: ap-northeast-1
stackTags:
type: serviceX # functionセクションで定義された関数に付くタグ
デプロイをします。
$ sls deploy -v
AWSの管理コンソールで確認すると、サービスが作成されていることがわかります。
#Serverless 便利なところ
ServerlessアプリでLambdaとAPI Gateway以外にもAWSのサービスのセットアップができます。
例えば、Lambdaの処理で使用するDynamoDBやS3のセットアップ、ロールの設定も行えます。
##テーブルの作成
Lambda関数で使用するDynamoDBのテーブルを作成することができます。
ymlファイルでテーブルの定義とロールの設定を行うと自動デプロイされ、Lambdaから使用できるようになります。
serverless.ymlにテーブル定義を追加します。
以下の例では、lamtestUsers、lamtestProjectsという2つのテーブルの定義をしました。
service: lambdatest
provider:
name: aws
runtime: nodejs6.10
stage: dev
region: ap-northeast-1
# ロール設定(DynamoDB)
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:*
Resource: arn:aws:dynamodb:*:*:table/*
# 関数定義
functions:
UserGet:
handler: manage/project.get
timeout: 30
events:
- http:
path: manage/project
method: get
cors: true
# テーブル定義
resources:
Resources:
lamtestUsers: # テーブル名
Type: 'AWS::DynamoDB::Table'
# DeletionPolicy: Retain プロジェクト削除等でこのテーブルを残すか
Properties:
AttributeDefinitions:
-
AttributeName: id # プライマリキー
AttributeType: S
KeySchema:
-
AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 2 # Read Capacity
WriteCapacityUnits: 2 # Write Capacity
TableName: lamtestUsers # テーブル名
lamtestProjects:
Type: 'AWS::DynamoDB::Table'
# DeletionPolicy: Retain
Properties:
AttributeDefinitions:
-
AttributeName: projectId
AttributeType: S
KeySchema:
-
AttributeName: projectId
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 3
WriteCapacityUnits: 3
TableName: lamtestProjects
このymlファイルをデプロイすると新しく2つのテーブルが作成されます。
LambdaからDynamoDBを利用するためにロールの設定も行っています。(iamRoleStatements:
)
ロールの設定を記述すると、IAMで新しくロールが作成されます。
$ sls deploy -v
yamlファイルでテーブル名を変更してしまうと、元の名前のテーブルはレコード毎削除されて新しいテーブルが作成されるのでご注意ください。
「このテーブルは削除しない!」という場合は、DeletionPolicy: Retain
をyamlのテーブルの設定に入れて置いてください。(上記のコメントアウト部分)
##環境変数の使用
例えば、テーブル名を環境変数として設定しておくことで、nodeのコードからも環境変数を使ってテーブルを呼び出すことができるようになります。
(ymlファイルでテーブル名を書き換えても、jsソースをいじらずに済むので便利)
ymlファイルでテーブル名を環境変数として設定します。
service: lambdatest
provider:
name: aws
runtime: nodejs6.10
stage: dev
region: ap-northeast-1
# 環境変数
environment:
DYNAMODB_USER_TABLE: lamtestUsers # 環境変数名:テーブル名
DYNAMODB_PROJECT_TABLE: lamtestProjects
# 関数定義
functions:
...
# テーブル定義
resources:
Resources:
lamtestUsers:
Type: 'AWS::DynamoDB::Table'
# DeletionPolicy: Retain
Properties:
# ... テーブルの設定 ...
TableName: lamtestUsers #このテーブル名が環境変数に反映される
lamtestProjects:
Type: 'AWS::DynamoDB::Table'
# DeletionPolicy: Retain
Properties:
# ... テーブルの設定 ...
TableName: lamtestProjects
provider
セクションの直下、environment:
に記述します。
JSで呼び出す環境変数名 : 呼び出したい値
の形式で記述します。
設定した環境変数は、以下のようにnodeで取得できます。
'use strict';
let AWS = require('aws-sdk');
let docClient = new AWS.DynamoDB.DocumentClient();
// 使用するテーブル(環境変数で設定されたもの)
const projectTable = process.env.DYNAMODB_PROJECT_TABLE;
const robotTable = process.env.DYNAMODB_ROBOT_TABLE;
...
##S3のバケットの作成
S3のバケットもymlで定義できます。
ここでは、ロールの設定とバケットの定義をしています。
service: lambdatest
provider:
name: aws
runtime: nodejs6.10
stage: dev
region: ap-northeast-1
# 環境変数
environment:
DYNAMODB_USER_TABLE: lamtestUsers
DYNAMODB_PROJECT_TABLE: lamtestProjects
S3_IMAGE_BUCKET: image-bucket
# ロール設定(DynamoDB, S3)
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:*
Resource: arn:aws:dynamodb:*:*:table/*
- Effect: Allow
Action:
- s3:*
Resource: arn:aws:s3:::nao-image-bucket/*
# 関数定義
functions:
UserGet:
handler: manage/project.get
timeout: 30
events:
- http:
path: manage/project
method: get
cors: true
resources:
Resources:
# テーブル定義
# ...
#S3 バケット
imageBucket: # バケット名
Type: AWS::S3::Bucket # S3の指定
Properties:
BucketName: image-bucket # バケット名
これでデプロイすると、S3のバケットが新規作成され、ロール設定をしてあると、Lambda関数からアクセスできるようになります。
また、環境変数も設定しておくと、コーディングの際便利です。
#Serverless 注意するところ
ServerlessからLambda+APIGatewayを使用する時に、オンラインでコーディングしたり、管理コンソールで設定したりするのと異なる部分があります。
気づいた点などをメモします。
##CORSをTrueにしている時の注意
APIのCORS許可を行いたい場合以下のようにAPIGatewayの設定にcors:true
を記述します。
# 関数定義
functions:
UserPost:
handler: manage/user.post
timeout: 30
description: ユーザー新規作成
events:
- http:
path: manage/user
method: post
cors: true # CORSの許可
この場合、nodeでリクエストレスポンスを書くときに注意が必要になります。
- ヘッダーで
Access-Control-Allow-Origin
をセット - ボディのコンテンツはString
エラーレスポンスのマッピングもコードで行う必要があります。
以下のようにレスポンスを返すようにしてみました。
'use strict';
let AWS = require('aws-sdk');
let docClient = new AWS.DynamoDB.DocumentClient();
const s3 = new AWS.S3({apiVersion: '2006-03-01'});
const bucket = process.env.S3_IMAGE_BUCKET;
// 使用するテーブル
const projectTable = process.env.DYNAMODB_PROJECT_TABLE;
const userTable = process.env.DYNAMODB_USER_TABLE;
// 成功時のレスポンス
const createResponse = (statusCode, body) => {
return {
statusCode: statusCode,
headers: {
"Access-Control-Allow-Origin" : "*" // Required for CORS support to work
},
body: JSON.stringify(body)
}
};
// エラーレスポンス
const createErrorResponse = (statusCode, errMessage) => {
return {
isBase64Encoded: false,
statusCode: statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin' : '*' // Required for CORS support to work
},
body: JSON.stringify({message: errMessage})
}
};
module.exports.post = (event, context, callback) => {
// クエリパラメータ取得
let userId = !event.queryStringParameters ? null : event.queryStringParameters.userId;
// 取得できなければエラーにする
if (!userId) {
const res = createErrorResponse(400, 'Bad Request. query parameter[userId] is empty.');
callback(null,res);
return;
}
// ユーザーが存在するかチェック
const userParams = {
TableName: userTable,
Key: {
"id": userId
}
};
docClient.get(userParams, function(err, data) {
// 様々な処理......
//成功時のレスポンス
callback(null, createResponse(200, projectJsonData));
});
};
成功時は、レスポンスの中に入れたJSON
失敗時は、{"message": "何かしらのエラーメッセージ"}
の形式でリターンされます。
##リクエストのクエリパラメータ取得
GETリクエストの時などにクエリパラメータ(http://~~~?id=xxxxxの時のidの値)が使用されている際の取得方法は以下のようにします。
module.exports.get = (event, context, callback) => {
// クエリパラメータ(userId)
let userId = event.queryStringParameters.userId;
// .......
}
##リクエストボディに含まれるパラメータ取得
POSTなどでリクエストボディの値を取得するには以下のようにします。
module.exports.post = (event, context, callback) => {
// リクエストボディ取得
const item_data = JSON.parse(event.body);
// パラメータ取得
let userId = item_data.userId;
let projectTitle = item_data.title;
let description = item_data.description;
// なんやかんや......
}
以上、Serverlessの使い方入門でした。
Lambda+APIGatewayのサービスをServerlessフレームワークに移行してみて、デプロイが簡単で驚きでした!yamlでのサービスのセットアップの記述も慣れれば、わかりやすいです。
AWS SAMも、早くCORS対応してくれると良いですなぁ...