lambda
APIGateway
serverless

[Lambda + APIGateway] ソース管理のためにServerlessを使ってみた

More than 1 year has passed since last update.

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のプログラムを記述するファイル


serverless.yml

service: lambdatest     # アプリ名

provider: # アプリ全般の設定を記述するところ
name: aws # プロバイダ名
runtime: nodejs6.10 # 言語

functions: # Lambda関数の設定
hello: # 関数名
handler: handler.hello #Lambdaのプログラムが置いてあるパス指定


→ 「helloというLambda関数一つをAWS上にデプロイするよ」(serverless.yml)


index.js

'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を以下のように編集します。


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ファイルを以下のように編集します。


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:を以下のように追加します。


serverless.yml

# 関数定義

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の形で関数にタグをつけることができます。

※ 全ての関数に同じタグをつけたい場合は以下のように設定することもできます。


serverless.yml

provider:

name: aws
runtime: nodejs6.10
stage: dev
region: ap-northeast-1
stackTags:
type: serviceX # functionセクションで定義された関数に付くタグ

デプロイをします。

$ sls deploy -v

AWSの管理コンソールで確認すると、サービスが作成されていることがわかります。

APIGateway

スクリーンショット 2017-09-14 15.10.25.png

Lambda Function

スクリーンショット 2017-09-14 15.12.55.png


Serverless 便利なところ

ServerlessアプリでLambdaとAPI Gateway以外にもAWSのサービスのセットアップができます。

例えば、Lambdaの処理で使用するDynamoDBやS3のセットアップ、ロールの設定も行えます。


テーブルの作成

Lambda関数で使用するDynamoDBのテーブルを作成することができます。

ymlファイルでテーブルの定義とロールの設定を行うと自動デプロイされ、Lambdaから使用できるようになります。

serverless.ymlにテーブル定義を追加します。

以下の例では、lamtestUsers、lamtestProjectsという2つのテーブルの定義をしました。


serverless.yml

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

:warning: yamlファイルでテーブル名を変更してしまうと、元の名前のテーブルはレコード毎削除されて新しいテーブルが作成されるのでご注意ください。

「このテーブルは削除しない!」という場合は、DeletionPolicy: Retainをyamlのテーブルの設定に入れて置いてください。(上記のコメントアウト部分)


環境変数の使用

例えば、テーブル名を環境変数として設定しておくことで、nodeのコードからも環境変数を使ってテーブルを呼び出すことができるようになります。

(ymlファイルでテーブル名を書き換えても、jsソースをいじらずに済むので便利)

ymlファイルでテーブル名を環境変数として設定します。


serverless.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で取得できます。


project.js

'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で定義できます。

ここでは、ロールの設定とバケットの定義をしています。


serverless.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を記述します。


serverless.yml

# 関数定義

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

エラーレスポンスのマッピングもコードで行う必要があります。

以下のようにレスポンスを返すようにしてみました。


project.js

'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の値)が使用されている際の取得方法は以下のようにします。


project.js

module.exports.get = (event, context, callback) => {

// クエリパラメータ(userId)
let userId = event.queryStringParameters.userId;

// .......
}



リクエストボディに含まれるパラメータ取得

POSTなどでリクエストボディの値を取得するには以下のようにします。


project.js

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対応してくれると良いですなぁ...:relaxed: