Amazon API Gateway の Custom Authorizer で ServerlessなAPIを作ってみた

## 対象読者
- AWSでサーバーレスアーキテクチャでマイクロサービスのAPIを開発しようと思っている方
- AWS Lambda、Amazon API Gateway、Serverless Frameworkの概要をある程度理解している方

## 今回作成した物
サーバーレスアーキテクチャでAPIを作成してみました。
ソースコードはgithub上に公開してあります。

- https://github.com/keita-nishimoto/aws-serverless-prototype

環境は以下の通りです。

- HostsOS（MacOS 10.12.3）
- vagrant（1.9.1）
- VirtualBOX（5.1.12） ※GuestOSはCentOSの7系を利用
- Node.js(v6.10.0)
- typescript(2.3.4)
- serverless(1.15.3)

今回開発に利用した環境は [Ansible Playbook](https://github.com/keita-nishimoto/aws-lambda-ansible) として公開してあります。

## サーバーレスアーキテクチャ？
名前の通り、サーバーが存在しないアーキテクチャとなるのですが、厳密には存在しない訳ではなく、サーバの作成やスケールアウトの管理等をクラウドサービスが担ってくれるイメージです。

バックエンドのサービスだと [AWS Lambda](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html) や [Google Cloud Functions](https://cloud.google.com/functions/docs/) のみを用いて作成するとサーバーレスアーキテクチャが実現可能です。

従来のEC2インスタンスやオンプレミスサーバを使ったアーキテクチャよりも以下のようなメリットが考えられます。

- ランニングコストが安い。（EC2インスタンスと違って使った分だけしか課金されない。）
    - 今回取り上げたAWSのサービスに関しては [こちらの記事](http://qiita.com/teradonburi/items/a382a17e1e0245b7d831) が詳しくまとまっています。
- クラウドサービスのほうで勝手にスケールアウトを行ってくれるので急なアクセスの増加にも耐えられる。（開発者も管理が楽になるので、その点でも良い。）
    - EC2を利用する場合でもAuto Scalingを使えば対応は出来ますが、こちらは更に手間がかかりません。

## 今回利用した技術
ほぼ全てAWSのサービスで構築を行っています。

全体像は下記のような形になります。

![serverless-architecture.jpg](https://qiita-image-store.s3.amazonaws.com/0/71899/8d1c9f95-c8cd-2acb-b05f-b01d260ec608.jpeg)

### [Amazon API Gateway](https://aws.amazon.com/jp/api-gateway/)

Amazonが提供するREST APIを公開する為の仕組みです。

クライアントの認証を行ったりアクセスの制御の仕組みを提供してくれます。

全てのクライアント（APIの利用者）はこのAPI Gatewayに対してリクエストを行います。

### [AWS Lambda](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html)

何らかのイベントをトリガーとして処理を実行する仕組みです。

処理はLambda関数という単位で作成します。

今回のビジネスロジックは全てこのLambda関数を用いて作成しています。

それなりに癖があるので、まずは [公式ガイド](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html) に目を通しておく事をオススメします。

CPU能力は割り当てメモリ量に応じて変わってきますので、メモリ割り当てを多めに設定する事でそのまま性能UPに繋げる事が出来ます。

以下、 [公式サイトのよくある質問](https://aws.amazon.com/jp/lambda/faqs/) より抜粋↓

>AWS Lambda のリソースモデルでは、お客様が関数に必要なメモリ量を指定するとそれに比例した CPU パワーとその他のリソースが割り当てられます。たとえば、256 MB のメモリを指定すると約 2 倍の CPU パワーが Lambda 関数に割り当てられます。128 MB のメモリを指定した場合と比較すると CPU パワーは倍となり、512 MB のメモリを指定した場合と比較すると半分になります。メモリは 128 MB から 1.5 GB まで、64 MB ごとに増加できます。

### [DynamoDB](https://aws.amazon.com/jp/dynamodb/)

AWSが提供しているNoSQLデータベースです。

こちらも完全マネージド型なので、開発者がサーバの作成を行う必要はありません。


### [Serverless Framework](https://serverless.com/)

Amazon API Gatewayの設定、AWS Lambdaのデプロイ、DynamoDBのテーブル作成等を管理してくれるフレームワークです。

これを知った当初、フレームワークという名前から [Ruby on Rails](http://rubyonrails.org/) のようなアプリケーションフレームワークを想像していましたが、全然違うものでした。

デプロイやロールバックが非常に簡単になるので、一度使ったらすぐに離れられなくなりました。開発が活発に行われている点も良いと思います。

以前 [Serverless Frameworkのインストールと初期設定](http://qiita.com/keita-nishimoto/items/879bb8c075db802906df) という記事を書きました。

こちらを見て頂ければインストールや初期設定をある程度理解して頂けるのではないかと思います。

### [TypeScript](https://www.typescriptlang.org/)
~~AWSが提供するAWS Lambdaの実行環境（Node.jsの場合）はv4.3.2です。~~

2017-03-30 追記
ついにAWS LambdaでNode.js 6.10がサポートされました。
https://aws.amazon.com/jp/about-aws/whats-new/2017/03/aws-lambda-supports-node-js-6-10/

[async/await](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function) 等の便利な構文を利用したかったので [Babel](https://babeljs.io/) を使おうかと思ったのですが、今回はTypeScriptを利用してみました。

個人的には型があるのは安心出来ますし、JavaScriptとの互換性もあるので非常に書きやすい言語でした。

### [Authlete](https://www.authlete.com/)
APIの保護をアクセストークンで行うのが良いと思い、OAuth2.0の仕組みを利用する事を考えました。

※OAuthやOpenIDConnectの詳しい説明に関してはこの記事では割愛させて頂きます。
下記の記事にとても詳しい解説が載っていますので、こちらに記載させて頂きます。

- [OAuth 2.0 + OpenID Connect のフルスクラッチ実装者が知見を語る](http://qiita.com/TakahikoKawasaki/items/f2a0d25a4f05790b3baa)
- [【第二弾】OAuth 2.0 + OpenID Connect のフルスクラッチ実装者が知見を語る](http://qiita.com/TakahikoKawasaki/items/30fbd546935cea914e4f)
- [OAuth & OpenID Connect 関連仕様まとめ](http://qiita.com/TakahikoKawasaki/items/185d34814eb9f7ac7ef3)

OAuthProviderを自前で実装するとなると、かなり大変そうだったので、今回は [Authlete](https://www.authlete.com/) というサービスを利用する事にしました。

非常に便利なサービスでOAuth OpenIDConnect Providerに必要な実装を全てWebAPIによって提供してくれます。

認証と認可が疎結合になっているので、既存の認証基盤を持つシステムに対しても導入が可能な点が魅力的でした。

全体像の図にある 「Custom Authorization」 はこの [Authlete](https://www.authlete.com/) のAPIを利用して実装されています。

実装は [Authleteのサンプル](https://www.authlete.com/documents/article/custom_authorizer/example) を参考にさせて頂きました。

余談になりますが他の選択肢として [Amazon Cognito User Pools](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html) という仕組みがあり、これを利用出来たらいいなあ、と考えていましたが、既存のサービスに組み込む事を重視して今回は採用を見送りました。

## 工夫した点や苦労した点
開発を進めていく上で工夫した点や苦労した点などを説明します。
開発は [AWS Lambda 関数を使用する際のベストプラクティス](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/best-practices.html) に記載されている注意点を守りながら行いました。

### ~~webpackの設定~~

#### ~~[serverless-webpack](https://github.com/elastic-coders/serverless-webpack) の利用~~
~~Serverless Frameworkでwebpackを利用する為に、[serverless-webpack](https://github.com/elastic-coders/serverless-webpack) を利用しています。~~

~~これを有効にする為には、serverless.yml に以下の記述を追加する必要があります。~~

```yaml:serverless.yml
plugins:
  - serverless-webpack
```

~~これを追加するとデプロイ前にwebpackのbuildが実行されるようになります。~~

2017-06-21 追記
[serverless-webpack](https://github.com/elastic-coders/serverless-webpack) の更新が止まっているので、webpackを利用するのを辞めて TypeScript標準である ```tsc``` を利用する構成に変更を行っております。

変更時の内容は[こちら](https://github.com/keita-nishimoto/aws-serverless-prototype/pull/160/files)です。

```tsconfig.json``` で ```outDir``` の設定を行い、webpack関連のファイルを削除してあります。

#### node_modulesをパッケージ対象から除外する
デプロイ対象のファイルサイズはそのまま性能に直結するので出来る限り小さいほうが望ましいとされています。

~~webpackはdefaultだとbuild対象に全てのnode_modulesを含めてしまうので、[webpack-node-externals](https://github.com/liady/webpack-node-externals) を使って、build対象からnode_modulesを除外します。~~

```js:webpack.config.js
// ↓これを読み込む
const nodeExternals = require('webpack-node-externals');

// 中略

module.exports = {
  target:  'node',
  // この記述を追加
  externals: [nodeExternals()],
  // 中略
};
```

2017-06-21 追記
webpackを辞めたので、この設定も現在削除されています。

今まではwebpackが必要なファイルだけをデプロイパッケージに含めるようにしてくれていましたが、それが無くなった事でデプロイパッケージのサイズが大幅に増えてしまいました。

現状は ```serverless.yml``` に以下の内容を追加する事で対応を行っています。
※ [対応時のissues](https://github.com/keita-nishimoto/aws-serverless-prototype/issues/152) です。

```yaml:serverless.ymlに追記した内容
package:
  exclude:
    - .git/**
    - .nyc_output/**
    - coverage/**
    - .idea/**
    - src/**
    - config/**
    - node_modules/aws-sdk/**
    - node_modules/.bin/**
    - node_modules/.cache/**
    - node_modules/@types/**
    - node_modules/chai/**
    - node_modules/json-loader/**
    - node_modules/node-inspector/**
    - node_modules/serverless/**
    - node_modules/serverless-dynamodb-local/**
    - node_modules/serverless-offline/**
    - node_modules/ts-loader/**
    - node_modules/ts-node/**
    - node_modules/tslint/**
    - node_modules/typescript/**
```

厳密にはこれでも必要最低限のファイルだけがデプロイされている訳ではないのですが何もしないよりはサイズは小さくなっています。

#### sourceMapの出力設定を行う

開発中これがないと結構キツイので出力されるように設定を行います。

~~webpack.config.jsに以下の記述を追加します。~~

```js:webpack.config.js
// 中略
module.exports = {
  // これを追加する
  devtool: 'source-map',
  // 中略
};
```

~~他にもpackageのインストールやソースコードの修正が必要なのですが、以前 [Serverless Framework + TypeScriptでデバッグを行う](http://qiita.com/keita-nishimoto/items/9c27bfcd1e67c0dc4ad1) という記事を書いたのでこちらを参照して下さい。~~

```tsconfig.json``` に ```"sourceMap": true``` を追加する事でsourceMapの出力を行っています。

### ローカルでの開発
最初のうちは都度AWSにデプロイして修正 → 開発 → 修正を繰り返していたのですが、すぐに面倒になったので、ローカルで API Gateway + AWS Lambdaが実行出来る環境を用意しました。

最初は [serverless-webpack](https://github.com/elastic-coders/serverless-webpack) の ```serverless webpack serve``` を利用していましたが、Custom Authorizerが動いてくれない問題があったので、現在は [serverless-offline](http://qiita.com/keita-nishimoto/items/9c27bfcd1e67c0dc4ad1) というPluginを利用しています。

[serverless-offline](http://qiita.com/keita-nishimoto/items/9c27bfcd1e67c0dc4ad1) に変えてからはローカルでもCustom Authorizerの検証が出来るようになりました。

余談ですが、他のローカル開発環境だと [Serverless Framework](https://serverless.com/) の開発メンバーである @horike37 さんに教えて頂いた、[serverless-plugin-simulate](https://github.com/serverless-community-labs/serverless-plugin-simulate) も良さそうなので、試してみようと思います。
こちらはDockerを使ってLambdaをシュミレートする方法のようです。
このあたりはまとまり次第別途、記事を書こうと思います。

### DynamoDBの設定

Serverless Frameworkはserverless.ymlでDynamoDBの構造を管理出来るのですが、DynamoDBに対する理解不足もあってかなり苦戦しました。（幸いな事に作成しようと思っているAPIはデータ構造が単純なのが救いでした。）
下記に設定部分を抜粋しておきます。

環境ごとにテーブルを分けたかったので、TableNameのprefixにデプロイ先の環境変数を付けています。

ReadCapacityUnits、WriteCapacityUnitsは検証用なので1になっていますが、実環境での運用時にはサービスの規模に合わせて調整する必要があります。

DeletionPolicyがDeleteになっていますが、これは実運用では辞めておいたほうが良いと考えます。（Retainを設定するべき） DeletionPolicyがDeleteだと ```serverless remove``` を実行した際にテーブルを丸ごと削除してしまうので、オペレーションミスが起きた際のリカバリーが大変になってしまうからです。

```yaml:serverless.yml
resources:
  Resources:
    UsersDynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Delete
      Properties:
        AttributeDefinitions:
          -
            AttributeName: id
            AttributeType: S
          -
            AttributeName: email
            AttributeType: S
        KeySchema:
          -
            AttributeName: id
            KeyType: HASH
        GlobalSecondaryIndexes:
          -
            IndexName: globalIndex1
            KeySchema:
              -
                AttributeName: email
                KeyType: HASH
            Projection:
              ProjectionType: ALL
            ProvisionedThroughput:
              ReadCapacityUnits: 1
              WriteCapacityUnits: 1
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${env:DEPLOY_STAGE}_Users
    ResourcesDynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Delete
      Properties:
        AttributeDefinitions:
          -
            AttributeName: id
            AttributeType: S
        KeySchema:
          -
            AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${env:DEPLOY_STAGE}_Resources
```

### TypeScriptのコンパイルオプションについて

下記に現在利用しているtsconfig.jsonの内容を記載します。

```json:tsconfig.json
{
  "compilerOptions": {
    // モジュール形式
    "module": "commonjs",
    // モジュールの解決方法。AWS Lambdaで利用するならnode。
    "moduleResolution": "node",
    // コンパイル後のターゲット（'es3', 'es5', 'es2015', 'es2016', 'es2017'）
    // 何も指定しない場合のデフォルト値は'es3'の模様
    "target": "es2015",
    // SourceMapを出力する
    "sourceMap": true,
    // 出力先のディレクトリ
    "outDir": "build",
    // コンパイル時にコメントを削除する
    "removeComments": true,
    // 暗黙のany型を禁止する
    "noImplicitAny": true,
    // Null安全かどうかチェックする（一部抜け道はありなので完璧ではない）
    "strictNullChecks": true,
    // 未使用なローカル変数はエラーにする
    "noUnusedLocals": true,
    // 到達しえないコードがある場合はエラーにする
    "allowUnreachableCode": false,
    // 到達しえないラベルがある場合はエラーにする
    "allowUnusedLabels": false,
    // use strict; を自動で追加
    "alwaysStrict": true,
    // 暗黙的な undefined を返す事を禁止する
    "noImplicitReturns": true,
    "lib": [
      "es2017"
    ],
    "types": [
      "node",
      "mocha"
    ]
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}
```

~~2017-03-27 現在AWS LambdaのNode実行環境は v4.3.2になります。
よって "target"はes5を指定しておくのが無難かと思い、es5を指定しました。~~

他の設定に関しては各プロジェクトの方針等にもよると思いますが、noImplicitAny（暗黙のany型を禁止する）、strictNullChecks（Null安全なコードを強制する）、noImplicitReturns（暗黙的な undefined を返す事を禁止する） あたりはtrueに設定しておく事でバグが少ないコードを書く事が出来ると感じました。

ただstrictNullChecksには一部抜け道があります。

[TypeScriptのstrictNullChecksをtrueにして1ヶ月経って分かったこと](http://qiita.com/shimohi/items/21cce33890708ce8e1a2) に詳しくまとめてくれている方がいます。

```lambda.callback()``` にはnull、もしくはError型を渡すようになっているのですが、strictNullChecksをtrueにしているとコンパイルエラーになってしまいます。

現状、この状況を回避する為に下記のように ```undefined``` を渡しています。

```ts
callback(undefined, successResponse);
```

これは苦肉の策だと思っているので、より良い方法があればそちらを採用したいと考えております。

2017-03-30 追記
ついにAWS LambdaでNode.js 6.10がサポートされました。
https://aws.amazon.com/jp/about-aws/whats-new/2017/03/aws-lambda-supports-node-js-6-10/

targetをES2015に変更してあります。
tsconfig.jsonの他にserverless.ymlの変更も行う必要があります。

"nodejs4.3" の部分を "nodejs6.10" 変えるだけでOKです。
[対応時の変更内容](https://github.com/keita-nishimoto/aws-serverless-prototype/pull/149/files) を参照して下さい。

今のところ [本件で紹介しているプロジェクト](https://github.com/keita-nishimoto/aws-serverless-prototype) では全てのテストコードを通過しており、不具合は出ていません。

### テストコードについて

このシステムはAWSのサービスに大きく依存しているので、ローカル環境を用意したものの、AWS上での動作担保が出来て初めてデプロイ出来る状態と言えます。

その為、integrationテスト（結合テスト）とunitテスト（単体テスト）の2種類のテストコードを書く事にしました。

テストコードに関しては両方とも [mocha](https://mochajs.org/) + [chai](http://chaijs.com/) で作成しました。

詳しくは [README.md](https://github.com/keita-nishimoto/aws-serverless-prototype) にテストの実行方法が載っていますので、興味のある方は参照してみて下さい。

[README.md](https://github.com/keita-nishimoto/aws-serverless-prototype)にも記載がありますが、unitテストに関してはコードカバレッジを出力する設定も行ってあります。

## 感想
Serverless Frameworkは学習コストが少々かかりますが、ドキュメントも豊富でかなり細かい設定が出来るので非常に使い勝手が良いと感じました。開発も活発に行われているので、今後さらに便利になる事が期待出来ます。

最後まで読んで頂きありがとうございます。