大晦日にCognito / APIGateway / Lumenでいかがでしょうか – 時々WAF –

はじめに

こんにちは。受託開発チームの江口です。

「アシアル」 = 「Monaca」 = 「ハイブリッドアプリ」のイメージが強いですが、
受託開発ではサーバサイド・バックエンド開発も盛んに行われています。

Vueの影に隠れて忘れられないように、ネタを小出ししていこうと思い、2018最後に執筆しました!

年末ですね!毎日呑んでます!

https://3.bp.blogspot.com/-rPTPV8qyqc8/W5IAR6v75iI/AAAAAAABOzY/ByMcMGbiLOkZILU1za4AVCyXwdP_-CC7ACLcBGAs/s800/yopparai_businessman.png

blog.asial.co.jp
前回はCognitoを使って認証を行い、Authorization Headerを通じて認可されたリソースへのアクセスを一通り体感しました。

と言っても、LambdaのモックAPIが固定文字列を返すだけの簡単なAPIでした。

今回は前回触れた「各APIへのつなぎ」方法のハンズオンと、
「各API」に何を選定したか?の紹介になります。
必ずしも正解ではないですが、私はこんなやり方で繋いでみましたよって簡単なアーキテクチャ紹介と、後述する「各API」についての感想を書こうと思います。

全てをハンズオンにできなくて申し訳ないですが、例によって最後にソースを丸っと紹介しているので、そちらを参考にトライしてみてください。

アーキテクチャ

全体構成図

今回の構成は前回の図から少し変わりまして、このような図になります。

f:id:eguchi_asial:20181231123436p:plain
各APIまでの基本設計図

APIGatewayからAPIへのつなぎは「http proxy」に変更しました。
さらに、http proxyさせる際にカスタムヘッダ(x-hogehoge-authみたいなもの)を付与し、ヘッダの存在と値の完全一致をWAFでチェックさせています。

WAFのAccess Controlを通過したリクエストのみが「各API」にアクセス可能となります。
それ以外はForbiddenです。
APIは今回もECSにしようと思ってましたが、docker自体やめてCloudFormationで普通にEC2インスタンス立ち上げる形にしました。

API詳細設計図

さらにさらに、より詳細にAPIの構成図はCloudFormationのテンプレートで見るとこんな感じになってます。

f:id:eguchi_asial:20181231005134p:plain
cloudformation-template

一番右にあるInternetからのINが先ほどの構成図にあるWAFに守られています。

ALBの下にAutoScalableなEC2が指定数台ぶら下がっています。
webへのアクセスはALBのセキュリティグループからのみとなっているため、クライアントから直接アクセスはできません。

RDSはリードレプリカ。
さらにwebのセキュリティグループ経由からのアクセスのみに絞られます。

各APIに何を選んだか??

私は現在アシアルの受託チームに所属していますが、APIに選定する技術はLaravelが圧倒的に多いです。

ですが、APIを建てたいだけなのにLaravelでは多機能すぎて持て余すし、余計なもの持ちたくないなーって気持ちも正直あり、今回のサンプルではLumenを真面目に使ってみました。

LumenはLaravelをAPIに特化させた軽量フレームワークです。
不要な機能・モジュールはガシガシ削ってあり、非常に高速であると公式が謳ってます。

Lightning fast micro-services and APIs delivered with the elegance you expect.

今回はLumenを使ってみてどうだったか?も所感紹介して終わろうと思います。

APIGatewayから各APIへのつなぎ方(ハンズオン続き)

CognitoとAPIGatewaとの連携は前回のハンズオンで体験済みですので、APIGatewayから各APIへのアクセス方法だけ、詳細に記載させて頂きます。

使用するAPIは、先ほど紹介したCloudFrontTemplateの構成で構築済みとします。
ですので、ALBでアクセスできるAPIにどのようにアクセスするかに焦点を当てます。

単純にALB -> EC2でアクセスできるAPIがあれば何でも良いのですが、
こちらのtemplateの中身を以下の点に注意して書き換えてもらえれば、パッと環境の用意はできると思いますので、参考程度に使ってくださっても構いません。

  • ALBに設定するACMのSSL証明書を自前で用意して指定していただく or httpにする
  • EC2のユーザーデータで使うgitのrepositoryを自分のものに変える
  • VPCから作成されるので、5個以上既に持っているアカウントだと作れない

http proxy

前回作成したcognito-tutorial-apiに手を加えます。
まず、前回作成したリソースとメソッドは全部丸っと削除してください。
(オーソライザは引き続きそのまま使います。)

削除した上で、以下のように新規リソースをANYで作成します!

f:id:eguchi_asial:20181231103359p:plain
ANYリソース

エンドポイントURLには、接続させたい各APIのエンドポイントを設定します。

設定するとこうなります。

f:id:eguchi_asial:20181231104022p:plain
httpproxy

一旦、http-proxyの設定に関してはこれで完了です!

WAFによるALBへのアクセス制限

これは必須ではないのですが、ALBへのアクセスをAPIGawatewayからのhttp-proxyに制限したいなーって思いました。
他にもっと良い方法があればそちらを採用したいのですが、今回は私はWAFでカスタムヘッダの値の完全一致でaccess controlすることにしました。

ACLの作成

まずはWAFでACLを作成します。

f:id:eguchi_asial:20181231110352p:plain
ALB-ACLの作成

AWS resource to associateには各自事前に作成したALBを選択してください。

Conditionsの作成

次にConditionsの設定画面です。

f:id:eguchi_asial:20181231110717p:plain
create-conditions-1

f:id:eguchi_asial:20181231110740p:plain
create-conditions-2

今回は「x-hogehoge-auth」というカスタムheaderに「ランダムに選定された固定文字列」が完全に一致していたらリクエストを通過させ、それ以外はForbiddenとするConditionsを作成します。

ですので、一番下にある「String and regex match conditions」の「Create Conditions」を押下します。

f:id:eguchi_asial:20181231111226p:plain
create-match-condition-1
f:id:eguchi_asial:20181231111249p:plain
create-match-condition-2

Value to matchのhogefugapiyoは適切な文字列を設定してください。

Ruleの作成

次にRuleを作成し、先ほど作成したConditionsと紐付けます。

f:id:eguchi_asial:20181231111746p:plain
create-rules-1

この画面に遷移しますが、まずはこの中の「Create rule」ボタンを押し、以下のように選択・記入します。

f:id:eguchi_asial:20181231111844p:plain
create-rule-2

「when a request」の中で「先ほど作成したConditions」が「一致したら」というRuleを作成します。
なので、「Please select」の箇所は先ほど各自が作成したConditionsを選択します。

これでACLに作成したRuleを追加できるようになりましたので、添付画像create-rule-1の「Rules」で、今作成したRuleを選択し、「Add rule to web ACL」ボタンを押下します。

f:id:eguchi_asial:20181231112417p:plain
create-rule-3

追加したRuleは上記のようにActionに「Allow」を設定し、それ以外はデフォルトで拒否させたい(=Forbidden)ので「Block all requests...」を選択します。

これで完成です!

APIGawatewayのhttp-proxyにカスタムheaderを追加する

作成した ACLがALBにアタッチされたので、単純なリクエストは弾かれてしまします。

なので、APIGatewayの「統合リクエスト」を押下し、以下のようにHTTPヘッダを追加します!

f:id:eguchi_asial:20181231113819p:plain
add-custom-auth-header

この時、「マッピング元」にCreate Conditionsで設定したランダム文字列を設定するのですが、シングルコートなどで囲い、文字列として認識させるようにしないとエラーになるので注意です。

作業はこれだけです。
WAFで設定したkey/valueを正確に一致させることでアクセス許可が降りる仕組みになってます。

ハンズオンはこれで全て完了です。

Lumenを使ってみて

Lumenを初めて真面目に扱いましたが、その時にハマったこと、対処したことを最後に簡単に紹介して終わろうと思います。

ハマったこと

tinkerがない!

というより、artisanがかなり機能が絞られてる!
make:middlewareだったり、controllerがなかったりもします。

$ php artisan
Laravel Framework Lumen (5.7.6) (Laravel Components 5.7.*)

Usage:
command [options] [arguments]

Options:
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
--env[=ENV] The environment the command should run under
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
help Displays help for a command
list Lists commands
migrate Run the database migrations
tinker Interact with your application
auth
auth:clear-resets Flush expired password reset tokens
cache
cache:clear Flush the application cache
cache:forget Remove an item from the cache
cache:table Create a migration for the cache database table
db
db:seed Seed the database with records
make
make:migration Create a new migration file
make:seeder Create a new seeder class
migrate
migrate:fresh Drop all tables and re-run all migrations
migrate:install Create the migration repository
migrate:refresh Reset and re-run all migrations
migrate:reset Rollback all database migrations
migrate:rollback Rollback the last database migration
migrate:status Show the status of each migration
queue
queue:failed List all of the failed queue jobs
queue:failed-table Create a migration for the failed queue jobs database table
queue:flush Flush all of the failed queue jobs
queue:forget Delete a failed queue job
queue:listen Listen to a given queue
queue:restart Restart queue worker daemons after their current job
queue:retry Retry a failed queue job
queue:table Create a migration for the queue jobs database table
queue:work Start processing jobs on the queue as a daemon
schedule
schedule:finish Handle the completion of a scheduled command
schedule:run Run the scheduled commands

FormRequestがない!!!

これが一番衝撃が大きかったです。
Laravelでvalidator扱う時にはFormRequestが個人的に一番重宝していたので、ショッキングでした。
Lumenerの方々はどうしているのでしょうか????

AppServiceProviderがデフォで有効になってない
  • 小一時間悩んだ。。bootstrap/app.phpでコメントアウトされているので外す。

解決方法

▼tinkerがない

tinkerはdebug開発時に本当にお世話になっているので、迷わず以下を入れましたw
本当ありがとうございます。

github.com

▼FormRequestがない

こちらも同様に以下をinstallしました。
本当ありがとうございます。

github.com

tips(?)共有

Lumenを使ったサンプルリポジトリをGitにあげたので、こちらを見ながらの紹介になります。

github.com

私はFormRequestを使う時、以下のような使い方をしています。
https://github.com/yueguchi/cognito-lumen-sample/blob/master/app/Http/FormRequests/BaseFormRequest.php

こちらのBaseFormRequest.phpでextends FormRequestしてます。
認証はCognitoに任せている = APIでチェックする必要ないので、以下のようにします。

public function authorize()
{
  return true;
}

また、validatorに失敗した時は全て400パラメータエラーにして返却しています。
エラーメッセージ配列を全て「/」で文字列として繋げています。

/**
 * バリデーション試行の失敗の処理
 *
 * @param Validator|\Illuminate\Contracts\Validation\Validator $validator
 * @throws HttpResponseException
 */
protected function failedValidation(Validator $validator)
{
  abort(400, implode('/', $validator->errors()->all()));
}

また、リクエストパラメータだけでなく、ルートパラメータ(例: http://localhost:9999/users/1)もvalidation対象にしたいため、以下のように統合させています。

/**
  * URLのpathパラメータ(route parameter)もvalidationターゲットに含める
  *
  * @param array|null $keys
  * @return array
  */
public function all($keys = null)
{
  return array_replace_recursive(parent::all(), $this->route());
}

こうした上で、あとは必要に応じてBaseFormRequest.phpを継承した子クラスを以下のように作成・利用します。

https://github.com/yueguchi/cognito-lumen-sample/blob/master/app/Http/FormRequests/UserListRequest.php

class UserListRequest extends BaseFormRequest
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
          'sub' => 'required|unique:users',
          'page' => 'required|integer|min:0',
          'limit' => 'required|in:20,40,50'
        ];
    }
}

あとはControllerのConstructorで以下のようにインジェクトすることでFormRequestのvalidatorがControllerの内部処理に先行して評価されます。

https://github.com/yueguchi/cognito-lumen-sample/blob/master/app/Http/Controllers/UserController.php

/**
  * [GET] Users List
  *
  * @param UserListRequest $request リクエスト
  * @return mixed Array
  */
public function index(UserListRequest $request)
{
  return $this->userService->get($request->only(['sub', 'page', 'limit']));
}

最後に、Exceptions/Handler.phpでjson形式でエラーを返却するように指定します。
ここはよしなに、やりたいようにラッピングしたら良いと思います。

public function render($request, Exception $e)
{
  $status = $e->getStatusCode();
  $message = $e->getMessage() ?: 'Error';
  switch ($status) {
    case 404:
      message = 'Not Found.';
      break;
    case 405:
      $message = 'Method Not Allowed.';
      break;
    case 500:
      $message = 'Internal Server Error.';
       break;
  }
  return response()->json([
      'code' => $status,
      'message' => $message
   ], $status);
}

こんな感じでFormRequestにvalidatorを丸っとお願いできていたので、FormRequestがないLumenでは公式でも

Form requests are not supported by Lumen. If you would like to use form requests, you should use the full Laravel framework.

はい。FormRequest使いたいならLaravel使おうねって言ってますw
結局あれもこれもと入れていくなら、もう大人しくLaravelで良いのでは?と思いましたw

最後に

  • CognitoやAPIGatewayを使えば認証/認可が必要なAPIは手軽に作れて便利
  • ALBへのproxyアクセス制限はWAF意外にも他にやり方があるのでは?とモヤモヤも残る結果
  • Lumenを使ってみてやっぱLaravelで良いや感w

でした。

来年はNuxt/SSRあたりの記事を書こうと考えています!

以上です。ありがとうございました!
良いお年をお過ごしください!!