この記事について
はじめにサーバーレスに対して、「サーバーレスに触れたいけどクラウドとか使うから面倒くさそう」、「興味あるけど難しそう」、「サクッと実装できるならいいけど実際どうなの?」などイメージや疑問があることでしょう。そのような方が実際にサーバーレスに触れ、サーバーレスビギナーへの道の足がかりとなれるようチュートリアル記事です。
そもそもサーバーレスとは
サーバーレスとは、サーバーがないという意味ではなく、サーバーの管理が不要という意味です。
基本的に、サーバー周りの設定など一切なしで、何らかのイベントに対してさ実行させたい(関数)コードを登録しておくことで、イベントが発火したら勝手に関数を実行してくれる代物です。
サービスとしてFaaS(Function as a Service)と言われています。
AWSのlambda、AzureのAzure Functions、GCPのGoogle Cloud Functionsが有名です。
チュートリアルを実施する前に
このチュートリアルでは、Nodejsで実装していきます。また今回はAWS(Amazon Web Service)のサーバーレス代表格であるlambdaを利用します。そのため、Nodejsが使えることとAWSアカウントを持っていなければ進めることはできません。
チュートリアル
このチュートリアルでは、自身(もしくはkousaku-maron)のQiitaの記事情報を1時間おきに取得し、DBに保存するサーバーレスアプリケーションを作成します。
Serverless導入
サーバーレスアプリケーションを作成するとき、実行させたいコードをデプロイし、AWSのコンソール画面(GUI)で実行タイミングやログ等の設定をしないといけません。
GUIでゴニョゴニョせずに、AWS側の設定を含めて実行させたいコードとともにデプロイできるフレームワークがあります。便利なフレームワークであるServerlessをインストールしましょう。
npm install -g serverless
Serverlessのインストールが完了したら、今回作成するサーバーレスアプリケーションを作成しましょう。
Serverlessは使用するクラウドサービス、言語に合わせたテンプレートが用意されているので、そちらを利用して作成していきます。
今回は、lambdaをnodejsでアプリケーションを作るので、テンプレートをaws-nodejs-ecma-script
で作成します。ES6/ES7で書けなくてもいいのであれば、テンプレートはaws-nodejs
でもよいです。
serverless create --template aws-nodejs-ecma-script --name serverless-sample --path serverless-sample
必要なパッケージがまだインストールされていない状態なので、インストールします。
cd serverless-sample
npm install
Serverlessアプリケーションの構成について
Serverlessアプリケーションには「関数(実行させたいコード)が書かれたファイル」と「クラウド側の設定が書かれたファイル」があります。
「関数が書かれたファイル」は、各クラウドベンダーのFaaSサービスの書き方なので、説明は省きます。詳しく知りたい方は、各クラウドベンダーのFaaSサービスのドキュメントを読んでみてください。
「クラウド側の設定が書かれたファイル」はserverless.yml
に書かれています。
重要なのは、provider
とfunctions
の項目です。
provider
には利用するクラウドサービス、ランタイム(言語)等、アプリケーション全体の設定ができます。functions
にはアプリケーション内で実行させるコード、そのコードを実行させるタイミング、そのコードで利用する環境変数等、関数(コード)個別の設定ができます。
service:
name: serverless-sample
# Add the serverless-webpack plugin
plugins:
- serverless-webpack
provider:
name: aws
runtime: nodejs6.10
functions:
first:
handler: first.hello
second:
handler: second.hello
events:
- http:
method: get
path: second
Serverlessアプリケーションをデプロイしてみる
アプリケーションをAWSへデプロイするとき権限が必要になります。
AWSコンソール画面で、IAMよりServerless用のユーザーを作成します。
ユーザーの追加からプログラムによるアクセスにチェックを入れ、既存のポリシーを直接アタッチよりAdministratorAccessをアタッチしユーザーを作成しましょう。作成完了後、key
とsecret
をメモしといて下さい。
serverlessのコンフィグにkey
とsecret
を登録しましょう。
serverless config credentials --provider aws --key メモしたkey --secret メモしたsecret
デプロイしてみましょう。
serverless deploy
デプロイが完了すれば、AWSコンソール画面のlambdaで関数がアップロードされたことが確認できるはずです。
serverless-sample-dev-second
を開いてみましょう。
serverless.yml
でsecond
のイベントトリガーはhttpになっていたので、httpリクエストに対してこの関数は実行されるようになっています。
※ブラウザでAPI GatewayのAPI エンドポイントに書いてるURLにアクセスすると実際に関数が実行されます。
関数内でQiita APIを叩いてみる
関数内でhttpリクエストを送信するため、axios
をインストールします。
npm install axios --save
functionsにデフォルトで登録されていたfirst
とsecond
はもう使わないので削除しましょう。ファイルも削除してしまって大丈夫です。代わりに新しくqiita
を登録し、qiita.js
を作成しておきましょう。
async/awaitを使いたいので、ランタイムをnodejs8.10に変更します。また、Qiita APIのエンドポイントを環境変数としてendpointに設定します。
※qiitaアカウントを持っているかつ投稿したことがあるのであれば、kousaku-maron
を自身のアカウント名に変えてください。
service:
name: serverless-sample
# Add the serverless-webpack plugin
plugins:
- serverless-webpack
provider:
name: aws
runtime: nodejs8.10
functions:
qiita:
handler: qiita.hello
environment:
endpoint: https://qiita.com/api/v2/users/kousaku-maron/items
qiita.js
で実行させる関数を書いていきます。
中身は**axios
で環境変数に登録したendpoint
へhttpリクエストを送信し、帰ってきたデータをcallback
で返しているだけです**。
import axios from 'axios'
export const hello = async (event, context, callback) => {
const res = await axios({
method: 'get',
url: process.env.endpoint,
params: {
page: 1,
per_page: 100,
}
})
const result = []
if(res.data) {
Promise.all(res.data.map(async element => {
const record = {
id: element.id,
title: element.title,
url: element.url,
likes_count: element.likes_count,
created_at: element.created_at,
updated_at: element.updated_at,
tags: element.tags,
}
result.push(record)
}))
}
callback(null, {
message: 'qiita article data success.',
data: result,
event,
})
}
serverlessにはローカルで関数を実行させる機能があります。
ローカルで実行させて結果が正しく出ているか確認してみましょう。
※ちなみにlocal
を省けば、デプロイされたlambda関数を実行させることができます。
serverless invoke local qiita
こんな感じで、結果がコンソールに表示されていれば成功です。
{
"message": "qiita article data success.",
"data": [
{
"id": "3887e57c5c5519abb46d",
"title": "【Unity】音ゲーの仕組みを学び「〇〇の達人」をUnityで作る パート3",
"url": "https://qiita.com/kousaku-maron/items/3887e57c5c5519abb46d",
"likes_count": 1,
"created_at": "2019-02-16T15:45:47+09:00",
"updated_at": "2019-02-16T15:46:29+09:00",
"tags": [
{
"name": "C#",
"versions": []
},
{
"name": "Unity",
"versions": []
},
{
"name": "ゲーム制作",
"versions": []
}
]
}
...
]
...
}
取得したデータをDynamoDBに保存してみる
aws-sdk
を使用するのでインストールします。
npm install aws-sdk --save
Qiita APIで取得したデータをDynamoDBに保存するため、まずはserverless.yml
にDynamoDBの設定を書き加えます。
※難しく見えますが、細かく説明しますのでご安心を、、、
service:
name: serverless-sample
# Add the serverless-webpack plugin
plugins:
- serverless-webpack
provider:
name: aws
runtime: nodejs8.10
timeout: 60
environment:
DYNAMODB_TABLE: qiitaTable
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
# Resource: arn:aws:dynamodb:${opt:stage, self:provider.stage}:*:table/${self:provider.environment.DYNAMODB_TABLE}
Resource: arn:aws:dynamodb:us-east-1:*:table/qiitaTable
functions:
qiita:
handler: qiita.hello
environment:
endpoint: https://qiita.com/api/v2/users/kousaku-maron/items
tableName: ${self:provider.environment.DYNAMODB_TABLE}
resources:
Resources:
qiitaTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.DYNAMODB_TABLE}
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
provider
には、lambdaのタイムアウト、保存するテーブル名、DynamoDBに保存できるようにするための権限の設定を追記しています。
タイムアウトはDB保存処理に時間がかかるため余裕をみて適当に設定します。
権限はiamRoleStatements
で設定でき、どのリソースに対して何のアクションを許可するという形で設定しています。
※${self:provider.stage}
の書き方でymlに記載している値を参照できます。しかし、iamRoleStatements
のResource
で使用するとエラーになったため直接値を入力しました。
provider:
name: aws
runtime: nodejs8.10
timeout: 60
environment:
DYNAMODB_TABLE: qiitaTable
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
# Resource: arn:aws:dynamodb:${opt:stage, self:provider.stage}:*:table/${self:provider.environment.DYNAMODB_TABLE}
Resource: arn:aws:dynamodb:us-east-1:*:table/qiitaTable
functions
には、tableName
の環境変数を設定します。この値はqiita.js
で利用します。
functions:
qiita:
handler: qiita.hello
environment:
endpoint: https://qiita.com/api/v2/users/kousaku-maron/items
tableName: ${self:provider.environment.DYNAMODB_TABLE}
recources
という項目を追加し、デプロイ時、DynamoDBのテーブルを作成するように設定します。
ここではDynamoDBだけでなく、S3などのリソースも作成するように設定できます。
DynamoDBのテーブルは必ずプライマリーキーを設定しないといけないので、AttributeDefinitions
とKeySchema
で設定します。
resources:
Resources:
qiitaTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.DYNAMODB_TABLE}
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
qiita.jsで、DynamoDBに保存する処理を書きます。
aws-sdk
でputItem
というDynamoDBに保存する関数をPromiseを利用して作成し、Qiita APIで取得したデータを保存させています。
import axios from 'axios'
import AWS from 'aws-sdk'
const documentClient = new AWS.DynamoDB.DocumentClient()
export const hello = async (event, context, callback) => {
const res = await axios({
method: 'get',
url: process.env.endpoint,
params: {
page: 1,
per_page: 100,
}
})
if(res.data) {
Promise.all(res.data.map(async element => {
const params = {
TableName: process.env.tableName,
Item:{
'id': element.id,
'title': element.title,
'url': element.url,
'likes_count': element.likes_count,
'created_at': element.created_at,
'updated_at': element.updated_at,
'tags': element.tags,
}
}
await putItem(params)
}))
}
callback(null, {
message: 'write qiita article data to dynamoDB success.',
event,
})
}
const putItem = (params) => {
return new Promise((resolve, reject) => {
documentClient.put(params, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
ここまで作成できたら、一度デプロイし、実行させてみましょう。
※serverless invoke --function qiitaでコマンドラインからlambda関数を実行できます。
serverless deploy
serverless invoke --function qiita
AWSコンソール画面でDynamoDBを見にいくと、データが入っていることが確認できるはずです。
1時間おきに関数を実行させるようにスケジューリングしてみる
serverless.yml
にちょこっと加えるだけで、作成した関数を1時間おきに実行させるようできます。
events
を作成し、schedule
を追加します。
たった2行です、むちゃくちゃ簡単。
※scheduleの細かい設定はRate または Cron を使用したスケジュール式をみてください。
service:
name: serverless-sample
# Add the serverless-webpack plugin
plugins:
- serverless-webpack
provider:
name: aws
runtime: nodejs8.10
timeout: 60
environment:
DYNAMODB_TABLE: qiitaTable
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
# Resource: arn:aws:dynamodb:${opt:stage, self:provider.stage}:*:table/${self:provider.environment.DYNAMODB_TABLE}
Resource: arn:aws:dynamodb:us-east-1:*:table/qiitaTable
functions:
qiita:
handler: qiita.hello
events:
- schedule: cron(0 * * * ? *)
environment:
endpoint: https://qiita.com/api/v2/users/kousaku-maron/items
tableName: ${self:provider.environment.DYNAMODB_TABLE}
resources:
Resources:
qiitaTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.DYNAMODB_TABLE}
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
デプロイしてみましょう。
serverless deploy
AWSコンソール画面でデプロイしたlambda関数をみると、Cloudwatch Eventsがトリガーとして設定されているはずです。
デプロイしたlambda関数を削除しましょう
下のコマンドを実行するだけです。
serverless remove
プラグインについて(オプション)
このセクションはオプションですので、みなくても特に問題はありません。
チュートリアルでは触れませんでしたが、ServerlessにはプラグインというServerlessを容易に拡張できる機能があります。
このセクションではプラグインを使った拡張例をいくつか紹介します。
DynamoDBを連携した関数をローカルで実行させてみる
ローカルで動くDynamoDBをServerlessで操作するプラグインをインストールします。
※自分の環境では最新のプラグインがちゃんと動作しなかったためバージョンを指定しています。githubのissueに上がっていました。
npm install --save-dev serverless-dynamodb-local@0.2.35
serverless.yml
にインストールしたプラグインを設定します。
service:
name: serverless-sample
# Add the serverless-webpack plugin
plugins:
- serverless-webpack
- serverless-dynamodb-local
...
プラグインを利用して、ローカルで動くdynamoDBをインストールします。
serverless dynamodb install
dynamoDBのインストールが完了したら、serverless.yml
にローカルのdynamoDBで使用するテーブルの設定をしましょう。
custom
を追加し、port
を8000
で起動するように設定しています。
...
custom:
dynamodb:
start:
port: 8000
inMemory: true
migrate: true
resources:
Resources:
qiitaTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.DYNAMODB_TABLE}
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
dynamoDBを起動してみましょう。
serverless dynamodb start
http://localhost:8000/shell/
にアクセスするとdynamoDBを操作できる画面がみれるはずです。
qiita関数で使用するdynamoDBをローカルのdynamoDBに変更します。
region
をendpoint
を指定してあげるだけでローカルのdynamoDBに切り替え可能です。
...
const documentClient = new AWS.DynamoDB.DocumentClient({
region: 'localhost',
endpoint: 'http://localhost:8000'
})
...
ローカル環境でqiita関数を実行させましょう。
serverless invoke local --function qiita
ローカルのdynamoDBにデータが保存されているか確認します。
dynamoDBを操作できる画面の左側に下記のコードを貼り付けて実行させてみてください。
const params = {
TableName: 'qiitaTable',
}
dynamodb.scan(params, (err, data) => {
if (err) ppJson(err)
else ppJson(data)
})
ちゃんとデータが保存されていることが確認できると思います。
最後に8000ポートをしようしているプロセスをキルするコマンドでローカルで起動しているdynamoDBを落としましょう。
lsof -i :8000 -t | xargs kill
古いバージョンのコードを整理し容量を削減させてみる
後日、追記
最後に
この記事ではQiita APIで取得したデータをDynamoDBに保存させる関数を作成しました。
APIの利用とAWSのリソース操作が学べたはずなので、Twitter APIを使ってBotを作成してみたり、DBのバックアップをとるバッチを作成したり、サーバーレスで色々できるようになったのではないでしょうか。
サンプルコード公開してます。
https://github.com/kousaku-maron/qiita-sample/tree/master/serverless-sample