概要
AWS SAMはサーバーレスアプリケーションを定義するモデルで、その定義に従ってAWS上に環境構築してくれるCloudFormationのサーバーレス版です。
CloudFormationでも同じことができますが、権限周りや設定すべき項目が桁違いに増えるので、LambdaやAPIGateWayを使ったサーバーレス環境を構築するときはSAM一択になると思います。
今回はこのSAMを使って、よく見かけるサーバーレスアプリを構成して見ます。
作成内容
簡単なツイートアプリを作ります。
できることはツイートと過去のツイート一覧表示のみです。誰からもツイートは見られないし他の人のを見ることもできません。シンプルイズベスト。
ソースコードはこちらhttps://github.com/YuuSatoh/sam-lambda
説明することしないこと
- 説明すること
- SAMでサーバーレスアプリを作った話
- 実装の簡単な説明
- 説明しないこと
- SAMのインストール方法とか使い方とか細かい話
アーキテクチャ
S3に置いたHTMLファイルをウェブサイトホスティングして、バックエンドはAPIGateWay+Lambdaのサーバーレス。DBはDynamoDBを使用します。
CORSの有効化
SAMでLambdaの呼び出し元をAPIGateWayにすると、構築されたAPIGateWayはデフォルトでLambdaプロキシ統合の使用にチェックが入ってしまう
(リファレンス見ても外し方がわからなかった…誰か知ってたら教えてください…)
そのためAPIGateWayでCORSを有効化するだけではうまくいきませんでした。
Lambdaプロキシ統合はAPIGateWayがこれまで丸めていたHTTPヘッダーの情報をLambdaに丸ごと送るからそっちでいい感じにしてね!となっているようなので、Lambdaの方でCORSを有効化する処理を入れる必要がありました。
結果ヘッダーに"Access-Control-Allow-Origin”を入れるだけだったけどCORSは結構ハマって辛かった…
コード
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: "サーバーレスハンズオン"
Globals:
Function:
Environment:
Variables:
TABLE_NAME:
!Ref HandsOnTable
Runtime: nodejs8.10
Resources:
GetFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/src/
Handler: index_get.handler
Policies: AmazonDynamoDBReadOnlyAccess
Events:
getAPI:
Type: Api
Properties:
Path: /tweet
Method: get
PostFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/src/
Handler: index_post.handler
Policies: AmazonDynamoDBFullAccess
Events:
postAPI:
Type: Api
Properties:
Path: /tweet
Method: post
HandsOnTable:
Type: AWS::Serverless::SimpleTable
Properties:
PrimaryKey:
Name: tweetId
Type: String
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
Outputs:
HandsOn:
Description: "API Gateway endpoint URL for Prod stage for HandsOn function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/tweet/"
- Resources:
- LambdaやDynamoDBといったリソースを設定するところ(そのまんま)
- CodeUri
- Lambdaのソースコードを相対パス指定する
- packaged.yamlに変換後S3にアップロードされたソースへのパスが自動的に入る
- パスとかメソッドとかハンドラーとかポリシーとかは見たまんま
- Outputs:
- CloudFormationの出力欄に表示されるもの
- APIGateWayのエンドポイントを出力するようにしているのでAPIGateWayに見に行かなくていい。便利
- Globals:
- リソースに共通するところをまとめて書ける
- ランタイムとか環境変数とか
- Resources:で同じパラメータを指定すると上書きされる
- リソースに共通するところをまとめて書ける
Lambda(index_get.js)
'use strict';
var aws = require('aws-sdk');
var docClient = new aws.DynamoDB.DocumentClient({ region: 'ap-northeast-1' });
exports.handler = function (event, context, callback) {
docClient.scan({ TableName: process.env.TABLE_NAME }, function (err, data) {
if (err) {
context.fail(err);
} else {
var response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify(data),
isBase64Encoded: false
};
callback(null, response);
}
});
};
DynamoDBからツイートデータを取得するコード。DynamoDBにフルスキャンかけてます。
レスポンスにはCORSを有効化するためヘッダーに許可するリソースを指定します。今回はとりあえず全てを許可する設定。
Lambda(index_post.js)
'use strict';
var aws = require('aws-sdk');
var docClient = new aws.DynamoDB.DocumentClient({ region: 'ap-northeast-1' });
exports.handler = function (event, context, callback) {
var requestBody = JSON.parse(event.body);
var message = decodeURIComponent(requestBody.message);
var item = {
tweetId: new Date().getTime().toString(),
message: message
};
var params = {
TableName: process.env.TABLE_NAME,
Item: item
};
docClient.put(params, function (err, data) {
if (err) {
console.log(err);
} else {
var response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify(data),
isBase64Encoded: false
};
console.log("Success:", data);
callback(null, response);
}
});
};
こちらはDynamoDBにツイートデータをputするコードです。DynamoDBにputしてるだけ。
こちらも同様にCORSを許可するヘッダーを付加
index.html(一部)
<script language="javascript">
// tweetのget
$.ajax({
type: 'get',
url: "APIGatewayURL Change Todo",
success: function (res) {
res.Items.forEach(function (item) {
var tweet = `
<div class="popover right show" style="position:relative; max-width:100%;">
<div class="arrow"></div>
<div class="popover-content">${item.message}</div>
</div>`;
$("#tweets").append(tweet);
});
}
});
// tweetのpost
function post() {
var $form = $('#tweet-form');
var JSONdata = {
message: $('#message').val()
};
$.ajax({
url: $form.attr('action'),
type: $form.attr('method'),
data: JSON.stringify(JSONdata),
timeout: 10000,
complete: function (xhr, textStatus) {
window.location.reload(); // post完了後に画面のリロード
},
});
}
</script>
HTMLファイルのJavaScript部分はこんな感じ
Ajaxで非同期にツイートのgetとpostをしてます。
通信先のurlはCloudFormationの出力に表示されるAPIGateWayのエンドポイントを入れてあげます
動作確認
1. SAMでパッケージしてデプロイ
2. HTMLのツイートを投稿/取得するURLをAPIGateWayのエンドポイントに修正
3. S3にHTMLをアップロードしてウェブサイトホスティングして公開
# コマンド(備忘録)
- パッケージ化
- sam package --template-file template.yaml --s3-bucket "s3BucketName" --output-template-file packaged.yaml
- デプロイ
- sam deploy --template-file packaged.yaml --stack-name ServerlessHandsON --capabilities CAPABILITY_IAM