KCF Labo Blog

KDDI Commerce Forwardの開発ブログ

Serverless Frameworkでローカル環境のセットアップからAWSへDeployまでを試してみる

KDDIコマースフォワード(以下KCF)モール開発部に所属するフロントエンドエンジニアの早川です。

早速ですが
みなさんSPA(Single Page Application)の開発行ってますか?

KCFでは一部のプロダクトや社内ツールで
SPAを採用し開発を行っています。

SPAによるユーザーへ提供できるメリットとして

  • 快適なUI操作(アクションからのレスポンスの速さやサクサクした画面切り替え)
  • 通信データ量の削減(必要なデータのみを取得することが可能)

などが考えられユーザー体験を向上させることができます。
そのためSPAを採用したコンテンツの開発を加速して行なっていきたいと考えています。

そうなるとサーバ側のAPIの開発についても
合わせて加速させていくことが重要になってきます。

みなさんはAPIの開発ってどのように行ってますか?

  • バックエンドエンジニアへ依頼する
  • 自分で開発する

バックエンドエンジニアへ依頼する形がセオリーかもしれませんが
手っ取り早く開発を進めるには 自分で作ってしまうのも1つの解決手段であると考えています。

そこで今回はフロントエンドエンジニアに馴染みのある
expressやTypescriptで開発できるServerless Frameworkを使用して

API GatewayAWS Lambda、Amazon DynamoDBの構成で作る
RESTfullなAPIをサクッと作成していきます。

今回利用するツールの紹介

まず今回利用する各サービスを紹介します。

API Gateway とは

APIの作成と管理が簡単にできるサービスです。
どのようなスケールであっても、開発者は簡単に API の配布、保守、監視、保護が行えます。
AWS Lambdaで実行される処理の玄関として振る舞うAPIを作成できます。

docs.aws.amazon.com

AWS Lambda とは

何かしらのイベントによって処理を実行する環境です。
イベントとはAWS上のS3にファイルをアップロードや特定のエンドポイントにアクセス
といった何かしらのアクションのようなものです。
このアクションをトリガーに処理を実行することができます。

docs.aws.amazon.com

Amazon DynamoDB とは

フルマネージドなNoSQLデータベースです。
フルマネージドとは運用をAWSにおまかせでき
利用者はOSやMiddlewareのことを意識する必要がありません。
また、高い拡張性、データへの高速アクセスが可能で
AWS Lambdaとの連携も簡単に行うことができます。

docs.aws.amazon.com

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

ユーザーの追加をクリックします。 f:id:yuki-hayakawa-kcf:20180901150019j:plain

ユーザー名を入力し、プログラムによるアクセスにチェックを入れます。 そして「次のステップ:アクセス権限」をクリックします。 f:id:yuki-hayakawa-kcf:20180901150103j:plain

ポリシーのフィルタへ「AdministratorAccess」と入力し「AdministratorAccessへチェックを入れます。 そして「次のステップ:確認」をクリックします。 f:id:yuki-hayakawa-kcf:20180901150155j:plain

入力内容を確認し「ユーザーの作成」をクリックします。 f:id:yuki-hayakawa-kcf:20180901150213j:plain

作成された「アクセスキーID」と「シークレットアクセスキー」をメモします。 f:id:yuki-hayakawa-kcf:20180923170335j:plain

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 CLIAWSのアカウント情報を紐付ける

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 に登録したデータが確認できます。

f:id:yuki-hayakawa-kcf:20180901150954j:plain

記事を登録してみる

$ 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/

記事が登録されていることを確認できました。

f:id:yuki-hayakawa-kcf:20180901151025j:plain

記事を削除する

$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X DELETE -d '{}' http://localhost:3000/api/items/2/delete

idが2の記事が削除されたことが確認できました。

f:id:yuki-hayakawa-kcf:20180901151051j:plain

AWSへdeploy

作成したアプリケーションを以下のコマンドでAWSへデプロイできます。

$ yarn run sls deploy -v

AWS側で自動でドメインが割り当てられます。

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/

f:id:yuki-hayakawa-kcf:20180901151114j:plain

記事を登録してみる

$ 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

実際に記事が登録されていることを確認できます。 f:id:yuki-hayakawa-kcf:20180901151136j:plain

実際にAWSで確認するとAPIが作成されていることを確認できます。

https://ap-northeast-1.console.aws.amazon.com/apigateway/home?region=ap-northeast-1#/apis

f:id:yuki-hayakawa-kcf:20180901151218j:plain

所感

  • 馴染みのあるexpress typescript を使えるため開発がやりやすい
  • 難しいことを考えずコマンド一つでデプロイができるので手軽に使える
  • APIを作りたいときにサクッと作れるため今後の開発が加速できそう

KCFではSPAやサーバレスな環境での開発経験のあるフロントエンドエンジニアを絶賛募集中です。
まずは気軽にお話しましょう。下記よりご連絡くださいませ。

hrmos.co

※選考ではなく面談スタートも可能です