この記事はただの集団 Advent Calendar 2018 - Adventarの19日目です。昨日は todokr さんのScalaをGraalVMでネイティブイメージにコンパイルしてAWS Lambdaでサクサク動かすでした。
目的
本記事ではsam-cliを使用したnodejsのサーバーレスアプリケーションの開発の流れを環境構築からデプロイまで行います。
入門向けのため、例として、Lambda, API Gateway, DynamoDBを一通り使ったtodoアプリを作ってみます。
sam-cliはSAMの定義に従ったCloudFormationのコード生成や、ローカルでの動作確認用コマンドを有する便利なツールです。
サーバーレスアプリケーションの開発は愚か、DynamoDBを触ったことさえない素人の私でも簡単にアプリを構築することができましたので、ぜひ触ってみてはいかがでしょうか。
本記事の手順完了後の実装はこちらにあります。
https://github.com/umeneri/sam-todo
環境
- MacOS High Sierra 10.13.6
- Docker 18.06.1-ce
- Node v8.10.0 (ローカルはndenvでlambdaのバージョンと合わせています)
- NPM 5.6.0
- Python 2.7.15
- AWS CLI aws-cli 1.16.28
- Lambda Runtime: nodejs8.10
作るもの
構成:
ApiGateway -> lambda -> dynamodb
シンプルなサーバーレス構成です。
動作:
- ApiGatewayへのgetでタスク一覧を取得
- ApiGatewayへのputでタスクを新規作成
文面の都合上、今回は2つのメソッドだけ作成します。
sam-cliの環境構築
※事前にaws-cliは使えるものとします。
sam-cliをpipでインストールします。
$ pip install aws-sam-cli
雛形アプリケーション作成
まず雛形を作ります。
sam initを行うと、簡単にサーバーレスアプリケーションの雛形を作成できます。また、デフォルトではnodejsの雛形が作成されます。
$ sam init --name sam-todo
$ cd sam-todo
雛形ができていることを確認します。 hello-worldディレクトリ内にlambdaで使用するアプリケーションコードがあります。
$ ls
README.md hello-world/ template.yaml
hello-worldの名前を変え、タスク管理用のソースを作ります。
$ mv hello-world task
ローカルでのlambdaアプリケーション作成
task/app.jsを下記コードに変更します。lambdaHandlerがlambdaで実行する関数です。
exports.lambdaHandler = async (event, context) => {
try {
switch (event.httpMethod) {
case "GET": {
const tasks = [
{
id: 1,
name: 'task1'
},
{
id: 2,
name: 'task2'
}
];
return {
"statusCode": 200,
"body": JSON.stringify(tasks)
};
}
case "PUT": {
return {
"statusCode": 200,
"body": JSON.stringify({})
};
}
default:
return {
"statusCode": 501
};
}
} catch (err) {
console.log(err);
return err;
}
};
eventにはAPI Gatewayからのリクエストが入る想定なので、リクエストのhttpMethodがGET, PUT, それ以外で場合分けしてレスポンスを変えています。
template.ymlは下記の様に修正します。元のファイルと違い、DynamoDBへのアクセス権限をつけたり、ApiGatewayのメソッドを2種類用意しています。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: sam-task
Globals:
Function:
Timeout: 3
Resources:
# lambdaの関数
TaskFunction:
Type: AWS::Serverless::Function
Properties:
# 関数内で実行するコードのパス
CodeUri: task/
Handler: app.lambdaHandler
Runtime: nodejs8.10
Policies: AmazonDynamoDBFullAccess
# Lambda内環境変数
Environment:
Variables:
PARAM1: VALUE
Events:
# API Gatewayでアクセスする際のパスとメソッド
TaskList:
Type: Api
Properties:
Path: /task
Method: get
PutTask:
Type: Api
Properties:
Path: /task
Method: put
lambdaの動作確認
では、以下で動作確認用のリクエストデータを作成します。samはこのようなデータも自動生成できて大変便利ですね。
$ sam local generate-event apigateway aws-proxy --method GET > task/data/get_event.json
動作確認します。sam local invoke
でtemplate.ymlで定義したTaskFunction
を実行します。
-e
で先程生成したgetのリクエストを入れたときの動作を確認します。
最後の行にタスク一覧のjsonが確認できればOKです。
[git][* master]:~/dev/infra/sam-todo/ $ sam local invoke TaskFunction -e task/data/get_event.json
2018-12-16 11:19:37 Found credentials in shared credentials file: ~/.aws/credentials
2018-12-16 11:19:37 Invoking app.lambdaHandler (nodejs8.10)
Fetching lambci/lambda:nodejs8.10 Docker container image......
2018-12-16 11:19:41 Mounting /Users/koishi/dev/infra/sam-todo/task as /var/task:ro inside runtime container
START RequestId: 03df4092-e556-143e-8038-187fd187896b Version: $LATEST
END RequestId: 03df4092-e556-143e-8038-187fd187896b
REPORT RequestId: 03df4092-e556-143e-8038-187fd187896b Duration: 7.60 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 30 MB
{"statusCode":200,"body":"[{\"id\":1,\"name\":\"task1\"},{\"id\":2,\"name\":\"task2\"}]"}
PUTイベントも同じように動作確認します。{"statusCode":200,"body":"{}"}
が出力されればOKです。
$ sam local generate-event apigateway aws-proxy --method PUT > task/data/put_event.json
$ sam local invoke TaskFunction -e task/data/put_event.json
DynamoDBを使用する
それではDynamoDBも加え、タスクを永続的に保存できるようにします。
DynamoDBの準備
まず、ローカルでDynamoDBを使うためにDynamoDB Localをインストールします。
今後の運用を考えdocker-compose.ymlに記述します。
version: '3'
services:
dynamodb:
image: amazon/dynamodb-local
container_name: dynamodb
ports:
- 8000:8000
DynamoDBを起動しておきます。
$ docker-compose up -d
DynamoDBへのアクセスを確認します。
$ curl http://localhost:8000
以下が出力されます。少なくとも動いてはいることが確認できました。
{"__type":"com.amazonaws.dynamodb.v20120810#MissingAuthenticationToken","message":"Request must contain either a valid (registered) AWS access key ID or X.509 certificate."}%
localのLambdaがDynamoDBにアクセスするためには、DynamoDBのエンドポイントを知る必要があります。
今回はエンドポイントを環境ごとに分けたいので、環境変数にエンドポイントを設定することにします。
まずtemplate.ymlのEnvironmentでDynamoDBのエンドポイントを指定します。空でもまずは大丈夫です。
Environment:
Variables:
DYNAMODB_ENDPOINT: ""
続いて、task/env.jsonを作成します。こちらは環境ごとに環境変数を切り替えるためのファイルです。今回はローカルのDynamoDBのエンドポイントのみ環境変数として指定します。
{
"TaskFunction": {
"DYNAMODB_ENDPOINT": "http://192.168.100.185:8000"
}
}
注意です。エンドポイントをlocalhostにすると、後述しますsam local start-api
でDynamoDBへアクセス出来ませんでした。そこでローカルのIP192.168.100.185
をifconfig等で取得し入力しています。
テーブル定義作成
以下のスキーマを用意し、DynamoDBにタスク管理用テーブルを作成します。
{
"AttributeDefinitions": [
{
"AttributeName": "Id",
"AttributeType": "N"
}
],
"TableName": "Task",
"KeySchema": [
{
"AttributeName": "Id",
"KeyType": "HASH"
}
]
}
各タスクはIdを持ち、Idに紐づくHashにタスク名等を保存する形式です。
では、下記でテーブルを作成します。
$ aws dynamodb create-table --cli-input-json file://task/schema/task.json --endpoint-url http://127.0.0.1:8000
長かったですが、ようやくDynamoDBの準備ができました。
アプリ修正
テーブルができたところで、アプリからテーブルにアクセスできるよう修正します。
以下の様にDynamoDBを扱うためのクラスとして、task/dynamodb-client.jsを作成します。
今後行数が増え複雑化することを考えると、lambdaのコードとファイルを分離させておいたほうが全体の見通しがよく再利用性も上がります。
const AWS = require('aws-sdk')
class DynamoDBClient {
constructor(tableName) {
const endpoint = process.env.DYNAMODB_ENDPOINT;
const config = endpoint !== "" ? { endpoint } : { region: 'ap-northeast-1' };
this.documentClient = new AWS.DynamoDB.DocumentClient(config);
this.tableName = tableName;
}
scan() {
return this.documentClient.scan({ TableName: this.tableName }).promise();
}
put(itemParams) {
const dbParams = {
TableName: this.tableName,
Item: itemParams,
}
return this.documentClient.put(dbParams).promise();
}
}
exports.DynamoDBClient = DynamoDBClient;
DyamoDBClientのメソッドはscanとputの2つです。それぞれタスク一覧、新規作成に対応します。
次にapp.jsをDynamoDBClientを使用するように修正します。
const DynamoDB = require('./dynamodb-client')
const dbClient = new DynamoDB.DynamoDBClient('Task');
exports.lambdaHandler = async (event, context) => {
try {
switch (event.httpMethod) {
case "GET": {
const dbOutput = await dbClient.scan();
return {
"statusCode": 200,
"body": JSON.stringify(dbOutput)
};
}
case "PUT": {
const body = JSON.parse(event.body);
const dbOutput = await dbClient.put(body);
return {
"statusCode": 200,
"body": JSON.stringify(dbOutput)
};
}
default:
return {
"statusCode": 501
};
}
} catch (err) {
console.log(err);
return err;
}
};
put_event.jsonのbodyを下記のように修正します。json形式で新しいタスクのIdとNameが入ってくる想定ですね。
"body": "{ \"Id\": 1, \"Name\": \"task1\"}",
それでは、DynamoDBも含めてローカルで動作確認してみましょう。lambdaの実行は、先程と同じくsam local invoke
です。
はじめにputで作成したタスクをgetで出力する手順で、正しくタスクができていることを確認します。
タスクの作成:
$ sam local invoke TaskFunction -e task/data/put_event.json --env-vars task/env.json
タスクの一覧表示:
$ sam local invoke TaskFunction -e task/data/get_event.json --env-vars task/env.json
API Gatewayからの動作確認
では、API Gatewayも含めて動作確認をします。sam local start-api
を行うと、http://127.0.0.1:3000
にApi Gatewayのサーバーが立ち上がるので、今度はcurlでリクエストできるようになります。
$ sam local start-api --env-vars task/env.json
別ターミナルを開き、http://127.0.0.1:3000
をcurlで叩いてみましょう。
taskの作成:
$ curl -XPUT http://127.0.0.1:3000/task -d '{"Id": 2, "Name": "task2" }'
taskの一覧表示:
curl http://127.0.0.1:3000/task
PUTでタスクを作成し、一覧表示でタスクができていることが確認できればOKです!
これでローカルの開発は完了です。
本番デプロイ
本番デプロイし、最後の動作確認をしましょう。
DynamoDBをtemplateに追加
まず本番でDynamoDBを生成するためにtemplate内にDynamoDBの定義を追加します。
Resouces:の配下に以下を追加します。
TaskDynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: Id
AttributeType: N
TableName: Task
KeySchema:
- AttributeName: Id
KeyType: HASH
DynamoDBのBillingModeは従量課金制にしています。開発中で使用頻度が小さいのであれば従来のProvisiondの場合より料金が安くすみます。
TypeはAWS::Serverless::SimpleTableとするのがSAMの流儀なのでしょうが、こちらだと従量課金設定がtemplate上でできませんでした。
デプロイする
さて、準備が整ったので、いよいよデプロイです。
まずlambdaのソースをzip化したものを置くために、S3バケットを作成します。
$ aws s3 mb s3://sam-todo-backet
sam package
でzipをS3のバケットにアップロードします。また、--output-template-file
で指定したpackaged.yml
に整形されたCloudFormationのテンプレートファイルが作られます。
$ sam package --template-file template.yaml --s3-bucket sam-todo-backet --output-template-file packaged.yaml
次に、sam deploy
でデプロイを行い、AWS上にリソースを作成できればOKです。sam-todo-stackがCloud Formationのstack名です。
$ sam deploy --template-file packaged.yaml --stack-name sam-todo-stack --capabilities CAPABILITY_IAM
本番の動作確認する
API Gatewayができているので、そのURIにアクセスして動作確認します。
URIとリソースができているかの確認もしたいので、AWSのコンソールのLambdaの関数TaskFunction
に行きます。
画像のように、LambdaとAPI Gatewayが紐付いていますね。
APIGateWayのパネルをクリックするとURIが表示されます。
URIに対して下記のようにリクエストしてみます。
タスクの作成(PUT):
curl -XPUT <API Gateway URL> -d '{"Id": 1, "Name": "task1" }'
タスク一覧(GET):
curl <API Gateway URL>
GETして以下のようにタスクが作成できていればOKです! お疲れ様でした。
{"Items":[{"Id":1,"Name":"task1"}],"Count":1,"ScannedCount":1}%