※KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。 本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。
KDDIコマースフォワード(以下KCF)モール開発部に所属するフロントエンドエンジニアの早川です。
早速ですが
みなさんSPA(Single Page Application)の開発行ってますか?
KCFでは一部のプロダクトや社内ツールで
SPAを採用し開発を行っています。
SPAによるユーザーへ提供できるメリットとして
- 快適なUI操作(アクションからのレスポンスの速さやサクサクした画面切り替え)
- 通信データ量の削減(必要なデータのみを取得することが可能)
などが考えられユーザー体験を向上させることができます。
そのためSPAを採用したコンテンツの開発を加速して行なっていきたいと考えています。
そうなるとサーバ側のAPIの開発についても
合わせて加速させていくことが重要になってきます。
みなさんはAPIの開発ってどのように行ってますか?
- バックエンドエンジニアへ依頼する
- 自分で開発する
バックエンドエンジニアへ依頼する形がセオリーかもしれませんが
手っ取り早く開発を進めるには 自分で作ってしまうのも1つの解決手段であると考えています。
そこで今回はフロントエンドエンジニアに馴染みのある
expressやTypescriptで開発できるServerless Frameworkを使用して
API Gateway、AWS Lambda、Amazon DynamoDBの構成で作る
RESTfullなAPIをサクッと作成していきます。
- 今回利用するツールの紹介
- 準備
- ローカルでアプリケーションの起動
- Typescriptの設定
- Lambdaで呼び出すコードを作成
- サンプルデータを作成
- DynamoDB Localをインストール
- DynamoDB Localを起動
- ローカルでアプリケーションを起動
- ブラウザからアクセス
- 記事を登録してみる
- ブラウザで確認
- 記事を削除する
- AWSへdeploy
- 所感
今回利用するツールの紹介
まず今回利用する各サービスを紹介します。
API Gateway とは
APIの作成と管理が簡単にできるサービスです。
どのようなスケールであっても、開発者は簡単に API の配布、保守、監視、保護が行えます。
AWS Lambdaで実行される処理の玄関として振る舞うAPIを作成できます。
AWS Lambda とは
何かしらのイベントによって処理を実行する環境です。
イベントとはAWS上のS3にファイルをアップロードや特定のエンドポイントにアクセス
といった何かしらのアクションのようなものです。
このアクションをトリガーに処理を実行することができます。
Amazon DynamoDB とは
フルマネージドなNoSQLデータベースです。
フルマネージドとは運用をAWSにおまかせでき
利用者はOSやMiddlewareのことを意識する必要がありません。
また、高い拡張性、データへの高速アクセスが可能で
AWS Lambdaとの連携も簡単に行うことができます。
Serverless Frameworkとは
AWS LambdaとAWS API Gatewayを利用したサーバレスなアプリケーションを構築するためのツールです。
作成、管理、デプロイ管理などを簡単に行うことができます。
Serverless Framework Documentation
準備
環境
- Mac OSX 10.13.4
- yarn 1.7.0
- NodeJs 8.1.0
AWSでIAMの設定
Serverless Framework用のユーザーを作成します。
AWSアカウントを作成しIAMユーザー作成ページへ移動します。
https://console.aws.amazon.com/iam/home?region=ap-northeast-1#/users
ユーザーの追加をクリックします。
ユーザー名を入力し、プログラムによるアクセスにチェックを入れます。 そして「次のステップ:アクセス権限」をクリックします。
ポリシーのフィルタへ「AdministratorAccess」と入力し「AdministratorAccessへチェックを入れます。 そして「次のステップ:確認」をクリックします。
入力内容を確認し「ユーザーの作成」をクリックします。
作成された「アクセスキーID」と「シークレットアクセスキー」をメモします。
AWS CLI のインストール
Homebrewからawscliをインストールします。
$ brew install awscli
$ aws --version aws-cli/1.15.50 Python/3.7.0 Darwin/17.6.0 botocore/1.10.49
AWS CLIにAWSのアカウント情報を紐付ける
awscli
をインストールすると利用できるawsコマンドからaws configure
を実行します。
そして先ほどIAMで作成したユーザの設定を紐付けます。
$ aws configure
AWS Access Key ID [None]: AKIAIUKT4JXXXXXXXXXX AWS Secret Access Key [None]: XXXXXXXXXX Default region name [None]: ap-northeast-1 Default output format [None]: json
package.jsonを作成
packageはyarn
で管理します。
$ yarn init -y
Serverless Frameworkのインストール
Serverless Frameworkはyarnでインストールします。
$ yarn add -D serverless
必要パッケージのインストール
インストールするアプリケーション用のパッケージ
No. | パッケージ名 | 概要 |
---|---|---|
1 | @types/aws-lambda | aws-lambdaの型定義パッケージ |
2 | @types/aws-sdk | aws-sdkの型定義パッケージ |
3 | @types/core-js | core-jsの型定義パッケージ |
4 | @types/express | expressの型定義パッケージ |
5 | @types/node | nodeの型定義パッケージ |
6 | @types/webpack | webpackの型定義パッケージ |
7 | aws-lambda | AWS Lambdaにデプロイするためのパッケージ |
8 | aws-serverless-express | serverlessでexpressを使用するためのパッケージ |
9 | path | パスを操作するためのパッケージ |
10 | serverless | serverlessを使用するためのパッケージ |
11 | serverless-dynamodb-local | DynamoDB Localを操作できるようにするためのパッケージ |
12 | serverless-offline | ローカルでAPI Gatewayの代用として利用するためのパッケージ |
13 | serverless-webpack | Serverless Frameworkでwebpackのビルドを利用するためのパッケージ |
14 | ts-loader | webpackでtypescriptをトランスパイルするためのパッケージ |
15 | typescript | typescriptを利用するためのパッケージ |
16 | webpack | webpackを利用するためのパッケージ |
$ yarn add -D @types/aws-lambda @types/aws-sdk @types/express @types/node @types/webpack aws-lambda aws-serverless-express path serverless serverless-dynamodb-local serverless-offline serverless-webpack ts-loader typescript webpack webpack-node-externals
No. | パッケージ名 | 概要 |
---|---|---|
1 | aws-sdk | JavascriptからAWS各サービスを操作するパッケージ |
2 | body-parser | HTTPリクエストボディのデータを取得するためのパッケージ |
3 | express | Node.jsで手軽にサーバを起動したりするためのパッケージ |
4 | serverless-http | Serverless FrameworkでExpressを利用するためのパッケージ |
$ yarn add aws-sdk body-parser express serverless-http
ローカルでアプリケーションの起動
Serverless Frameworkの設定
serverless.yml
serverless.ymlの設定ファイルです。
ローカルで動かす設定やdynamodb、Lambdaの設定をここに記述します。
service: demo-serverless # プラグインの設定 plugins: - serverless-webpack - serverless-offline - serverless-dynamodb-local # AWS側の設定 provider: name: aws runtime: nodejs8.10 stage: dev region: ap-northeast-1 environment: DYNAMODB_TABLE: items iamRoleStatements: - Effect: Allow Action: - dynamodb:Query - dynamodb:Scan - dynamodb:GetItem - dynamodb:PutItem - dynamodb:UpdateItem - dynamodb:DeleteItem Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}" package: excludeDevDependencies: true exclude: - serverless-http custom: # webpackの設定 webpackIncludeModules: true webpack: webpackConfig: 'webpack.config.js' packager: 'yarn' packagerOptions: {} # Dyamodbをローカルで起動させるための設定 dynamodb: start: port: 3030 inMemory: true migrate: true seed: true seed: development: sources: - table: items sources: [./dynamo/items.json] resources: Resources: # サンプルで作成するDynamodbのテーブル ArticlesTable: Type: AWS::DynamoDB::Table Properties: TableName: items AttributeDefinitions: - AttributeName: id AttributeType: S KeySchema: - AttributeName: id KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 # lambdaの設定 functions: app: handler: app.handler events: - http: method: ANY path: '/' cors: true - http: method: ANY path: '{proxy+}' cors: true
webpackの設定
webpack.config.js
webpackの設定を記述します。
Typescriptをトランスパイルするts-loader
の設定を記述します。
const path = require('path'); const slsw = require('serverless-webpack'); const nodeExternals = require('webpack-node-externals'); module.exports = { entry: slsw.lib.entries, target: 'node', mode: slsw.lib.webpack.isLocal ? 'development': 'production', externals: [nodeExternals()], module: { rules: [ { test: /\.ts$/, exclude: /node_modules/, include: __dirname, use: { loader: 'ts-loader', options: { transpileOnly: true } }, }, ] }, resolve: { extensions: ['.ts'] }, output: { libraryTarget: 'commonjs', path: path.join(__dirname, '.webpack'), filename: '[name].js' }, };
Typescriptの設定
tsconfig.json
{ "compilerOptions": { "target": "es6", "module": "commonjs", "strictNullChecks": true, "newLine": "LF", "noEmitOnError": false, "sourceMap": true, "strict": true, "allowJs": true, "lib": [ "es2017" ], "baseUrl": ".", "typeRoots": [ "./node_modules/@types", "@types" ] }, "exclude": [ "node_modules" ] }
Lambdaで呼び出すコードを作成
app.ts
import * as express from 'express'; import * as serverless from 'serverless-http'; import * as aws from 'aws-sdk'; import * as bodyParser from 'body-parser'; const app: express.Application = express(); /** * dynamodbClient 開発 */ const localDynamodb: aws.DynamoDB.DocumentClient = new aws.DynamoDB.DocumentClient({ region: 'ap-northeast-1', endpoint: "http://localhost:3030" }); /** * dynamodbClient 本番 */ const dynamodb: aws.DynamoDB.DocumentClient = new aws.DynamoDB.DocumentClient({ region: 'ap-northeast-1', }); /** * * IPによって環境ごとのDynamoClientを返す * @param {string} ip * @returns */ const getDynamodbClient = (ip: string): aws.DynamoDB.DocumentClient => { return ip === "127.0.0.1" ? localDynamodb : dynamodb; } /** * bodyParserの設定 */ app.use(bodyParser.json({ strict: false })); /** * アクセスコントロールの設定 */ app.use((req: express.Request, res: express.Response, next: express.NextFunction): void => { res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'); res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); next() }); /** * アイテム一覧の取得 */ app.get('/api/items', async (req: express.Request, res: express.Response): Promise<void> => { const dynamodb = getDynamodbClient(req.ip); const params = { TableName: 'items', Limit: 100 } const result: aws.DynamoDB.ScanOutput = await dynamodb.scan(params).promise(); res.json({ items: result.Items }); }); /** * アイテムの取得 */ app.get('/api/items/:id', async (req: express.Request, res: express.Response): Promise<void> => { const dynamodb = getDynamodbClient(req.ip); const params = { TableName: 'items', Key: { id: req.params.id, } } const result: aws.DynamoDB.GetItemOutput = await dynamodb.get(params).promise(); res.json({ article: result.Item }); }); /** * アイテムの更新 */ app.put('/api/items/:id/update', async (req: express.Request, res: express.Response): Promise<void> => { const dynamodb = getDynamodbClient(req.ip); const params = { TableName: 'items', Key: { id: req.params.id }, UpdateExpression: "set title = :title, description = :description, modified_at = :modified_at", ExpressionAttributeValues:{ ':title': req.body.title, ':description': req.body.description, ':modified_at': req.body.modified_at }, ReturnValues: "UPDATED_NEW" } try { const result = await dynamodb.update(params).promise(); res.json(result); } catch (error) { res.json({error}); } }); /** * アイテムの作成 */ app.post('/api/items/create', async (req: express.Request, res: express.Response): Promise<void> => { const dynamodb = getDynamodbClient(req.ip); const params = { TableName: 'items', Item: { id: req.body.id, title: req.body.title, description: req.body.description, created_at: new Date().getTime(), modified_at: new Date().getTime() } } try { const result = await dynamodb.put(params).promise(); res.json(result); } catch (error) { res.json({error}); } }); /** * アイテムの削除 */ app.delete('/api/items/:id/delete', async (req: express.Request, res: express.Response): Promise<void> => { const dynamodb = getDynamodbClient(req.ip); const params = { TableName: 'items', Key: { id: req.params.id } } try { const result = await dynamodb.delete(params).promise(); res.json(result); } catch (error) { res.json({error}); } }); export const handler = serverless(app);
@types/serverless-http.d.ts
公開されている @types
がないため作成します。
declare module 'serverless-http';
サンプルデータを作成
ローカルで確認するためのサンプルデータを作成します。
dynamo/items.json
[ { "id": "1", "title": "タイトルのテスト1", "description": "ディスクリプションのテスト1", "created_at": 1532274721534, "modified_at": 1532274721534 }, { "id": "2", "title": "タイトルのテスト2", "description": "ディスクリプションのテスト2", "created_at": 1532274843309, "modified_at": 1532274843309 } ]
DynamoDB Localをインストール
$ yarn run sls dynamodb install Installation complete! ✨ Done in 48.31s.
DynamoDB Localを起動
$ yarn run sls dynamodb start
ローカルでアプリケーションを起動
$ yarn run sls offline start
ブラウザからアクセス
http://localhost:3000/api/items/
dynamo/items.json
に登録したデータが確認できます。
記事を登録してみる
$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"description":"ディスクリプションのテスト3","created_at":1532274721534,"id":"3","title":"タイトルのテスト3","modified_at":1532274721534}' http://localhost:3000/api/items/create
ブラウザで確認
http://localhost:3000/api/items/
記事が登録されていることを確認できました。
記事を削除する
$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X DELETE -d '{}' http://localhost:3000/api/items/2/delete
idが2の記事が削除されたことが確認できました。
AWSへdeploy
作成したアプリケーションを以下のコマンドでAWSへデプロイできます。
$ yarn run sls deploy -v
ServiceEndpoint: https://mi76zpf26a.execute-api.ap-northeast-1.amazonaws.com/dev
ブラウザで確認
ServiceEndpoint
で生成されたurlから/dev/api/items/
へアクセスしてみてください。
https://mi76zpf26a.execute-api.ap-northeast-1.amazonaws.com/dev/api/items/
記事を登録してみる
$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"description":"ディスクリプションのテスト1","created_at":1532274721534,"id":"1","title":"タイトルのテスト1","modified_at":1532274721534}' https://mi76zpf26a.execute-api.ap-northeast-1.amazonaws.com/dev/api/items/create
実際に記事が登録されていることを確認できます。
実際にAWSで確認するとAPIが作成されていることを確認できます。
https://ap-northeast-1.console.aws.amazon.com/apigateway/home?region=ap-northeast-1#/apis
所感
- 馴染みのある
express
typescript
を使えるため開発がやりやすい - 難しいことを考えずコマンド一つでデプロイができるので手軽に使える
- APIを作りたいときにサクッと作れるため今後の開発が加速できそう