Claudia.jsで簡単サーバレスアーキテクチャー

サーバレスアーキテクチャと聞いて、どのようなアーキテクチャを思い浮かべるでしょうか。Martin Fowler.comのサーバレスアーキテクチャの解説記事ではいくつかのサーバレスアーキテクチャの実装例がリストアップされています。

ここでは、最もよく使われると思われるフロントエンドアプリのためのサーバレスアーキテクチャを考えてみます。

フロントエンドアプリのためのサーバレスアーキテクチャ

API Gateway、Lambdaを中心とした標準的な構成です。

  • フロントエンド(SPA/Native)
  • 認証認可API
    • 代表的なサービス: Facebook, Twitter, Google, Auth0
    • 役割: ユーザの認証認可、OAuth Access TokenやJWTの発行・更新・検証
  • API Gateway
    • 代表的なサービス: AWS API Gateway, Apigee
    • 役割: フロントエンドが利用するAPIの公開、APIの共通処理(認証認可)、FaaS連携
  • FaaS (Function as a Service)
    • 代表的なサービス: AWS Lambda
    • 役割: BaaS連携、業務ロジック実行
  • BaaS (Backend as a Service)
    • 代表的なサービス: AWS DynamoDB, S3

しかしながら、実際にAWS上で一から実装しようとするとAPI GatewayとLambdaの連携、認証認可の実装、Lambdaの管理、デプロイや更新処理など面倒極まりない作業に直面します。何らかの抽象化レイヤーやツール無しにAWS上でサーバレスアーキテクチャを実現するのは困難だと実感しています。

ここでは、Claudia.jsというNode.js製のフレームワークを用いてこれらの課題を解決し、フロントエンドアプリのためのサーバレスアーキテクチャーを簡単に実現してみます。

Claudia.jsとは

AWS上でサーバレスアーキテクチャを実現するためのフレームワークの一つです。同様のフレームワークとしてServerlessやApex等が知られています。

  • Apex: Lambdaにフォーカス。作者のTJがAPI Gatewayをの1st citizenで無いと言っており連携が煩雑。
  • Serverless: YamlAWSの設定を大量に記述するスタイル。抽象化度合いが低い。

Claudia.jsはこれらと比較した際に、API Gatewayや外部認証までをも含めたサーバレスアーキテクチャ全体を通して実装するのが簡単です。Node.jsでExpressを用いたAPIサーバを書いていくようなイメージで、設定ファイルを記述していくServerless等と比較して柔軟な処理が行えます。

準備

1. Claudia.jsをインストールします。

npm install -g claudia

2. プロジェクトを作成します。

mkdir [Proj Name] && cd [Proj Name]
npm init
npm install --save claudia-api-builder
npm install --save aws-sdk
npm install --save axios

実装

以下のようなフロントエンドアプリを実装してみます。エラー処理や詳細は割愛します。また、Hello World的なものはCloudia.jsのTutorialをみた方が手っ取り早いです。

  1. フロントエンドアプリがSNSの認証認可APIからOAuthのアクセストークンを取得する。(略)
  2. フロントエンドアプリがアクセストークンを付与してAWS API Gatewayにリクエストを投げる。(略)
  3. AWS API GatewaySNSの認証認可APIを用いてアクセストークンを検証する。
  4. AWS API GatewayがFaaSの関数を呼び出す。
  5. AWS Lambdaの関数がAWS DynamoDBのテーブルからデータを取得してフロントに返す。

1. カスタム認証の作成

1. カスタム認証用のLambda関数を作成します。

authorizer.jsを作成します。Access Token検証APIを呼び出して有効な場合にAPI実行権限を付与しています。

var axios = require('axios');
    
//API実行ポリシーの生成
var generatePolicy = function(authToken, methodArn, context) {
    var awsInfo = methodArn.split(':');
    var region = awsInfo[3];
    var accountId = awsInfo[4];
    
    var apiInfo = awsInfo[5].split('/');
    var restApiId = apiInfo[0];
    var stage = apiInfo[1];
    var method = apiInfo[2];
    
    //アクセスさせたいリソース
    var resource = 'arn:aws:execute-api:' + region + ':' + accountId + ':' + restApiId + '/' + stage  +'/' + method + '/*';
    
    return {
        'principalId': authToken.split('-')[0],
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [{
                'Effect': 'Allow',
                'Action': ['execute-api:Invoke'],
                'Resource': [resource]
            }]
        }
    };
};
    
var url = "Access TokenのValidateを行うAPIのURL";
    
//Tokenの検証とポリシーの付与
exports.auth = function auth(event, context, cb) {
  axios.get(url, {
    params: {
      access_token: event.authorizationToken //HTTP headerのAuthorizationキーの値
    }
  }).then(function (res) {
    var is_valid = true|false; //Tokenの有効性を検証して結果を格納
    if (is_valid) {
       cb(null, generatePolicy(event.authorizationToken, event.methodArn));
    } else {
       cb("Invalid Access Token");
    }
  });
};

2. デプロイ

claudiaコマンドでカスタム認証用のLambda関数をAWSにデプロイします。

claudia create --name custom-authorizer --region us-east-1 --handler authorizer.auth --config claudia-auth.json
  • --name: AWS Lambda上の関数名
  • --region: AWSのリージョン名
  • --handler: カスタム認証の関数名
  • --config: Claudia.js用の設定ファイル名(自動作成されるので任意)
{
  "lambda": {
    "role": "custom-authorizer-executor",
    "name": "custom-authorizer",
    "region": "us-east-1"
  }
}

2. APIとLambda関数の作成

1. APIとLambda関数を作成します。

app.jsという名前にします。

var ApiBuilder = require('claudia-api-builder');
var api = new ApiBuilder();
    
var AWS = require('aws-sdk');
var dynamodb = new AWS.DynamoDB.DocumentClient({region: 'us-east-1'});
    
api.registerAuthorizer('apiGatewayAuthorizer', {
  lambdaName: 'custom-authorizer', //AWS Labmda上のカスタム認証の名前
  headerName: 'Authorization' //Tokenを格納するHTTP headerのKey名
});
    
api.get('/news', function (req) {
  return dynamodb.get({
    TableName:'news',
    Key:{ "id": req.headers.id }
  }).promise().then(function(res) {
    return res.Item;
  });
},{ 
  customAuthorizer: 'apiGatewayAuthorizer' 
});
    
module.exports = api;

2. デプロイ

claudiaコマンドでAWSにデプロイします。設定ファイルclaudia.jsonが自動で作られます。

claudia create --region us-east-1 --api-module app
{
  "lambda": {
    "role": "myproj-executor",
    "name": "myproj-proj",
    "region": "us-east-1"
  },
  "api": {
    "id": "[PID]",
    "module": "app",
    "url": "[エンドポイントのベースURL]"
  }
}

3. AWS上の設定

  1. AWS DynamoDBのテーブル作成

    newsテーブルを作成する

     aws dynamodb create-table --table-name news --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1
    

    idキーで検索できるようにレコードを追加。

     aws dynamodb put-item --table-name news --item '{"id":{"S":"id001"}, "content":{"S":"News content 001"}}'
     aws dynamodb put-item --table-name news --item '{"id":{"S":"id002"}, "content":{"S":"News content 002"}}'
         :
    
  2. AWS Lambdaのrole設定

    1. IAM > Roles > [作成したRole名]
    2. Attach Pollicy > AmazonDynamoDBFullAccess
      • 強すぎるので実際は適当なPolicyを作成

4. API実行

  1. PostmanなどでHTTPヘッダのAuthorizationキーにAccess Tokenを、idキーに存在するidを付与して投げます。

    Claudia.jsでAPIを作成した際にはCORSはデフォルトでONになっていますが制限することもできます。

5. アップデート

  1. スクリプトを修正します。
  2. claudiaコマンドを実行します。
claudia update --config claudia-auth.json
claudia update --config claudia.json

まとめ

Claudia.jsを利用すれば、外部の認証認可APIを用いたフロントエンドアプリのためのサーバレスアーキテクチャを簡単に実装できます。

以上

Web API用の負荷テストツールで遊んでみる

これはWeb API Advent Calendar 2014の16日目の記事です。

はじめに

Web API Advent Calendar 2014 8日目ではkazuchikaさんよりApigee製のAPI負荷ツールapibの紹介がありました。

この記事ではapibに加えてBoomというGolang製の類似ツールを簡単に紹介し、様々なマイクロWebフレームワークに対して両者を使ってみます。

Boom

rakyll / boom

インストール

Golang環境がセットアップされているものとします。

go get github.com/rakyll/boom

実行

boom -n 回数 -c 並列度 [URL]

その他オプションはboomコマンドで閲覧できます。

apib

apigee / apib

  • C言語
  • Apigeeエンジニアが開発
  • リクエスト回数ではなく計測時間を指定する
  • Warm upの時間を指定できる
  • リモートサーバにデーモンを仕込んだ測定もできる

インストール

brew install apib

実行

apib -d 計測時間 -c 並列度 [URL]

その他オプションはapibコマンドで閲覧できます。

実行結果

複数プログラミング言語のマイクロフレームワークを用いて単純なWebAPIを作成して負荷テストツールを実行してみます。条件は以下です。

  • boomの場合、並列度100で1万リクエスト
  • apibの場合、並列度100で10秒

Node.js - Express

Node.jsのスタンダードなWAFです。

var express = require('express')
var app = express()
var http = require('http');
http.globalAgent.maxSockets = 50;

app.get('/', function (req, res) {
  res.send('Hello')
})

var server = app.listen(3000, function () {
})

f:id:slowquery:20141216221017p:plain

f:id:slowquery:20141216221101p:plain

Node.js - Restify

Expressに似ていますが、Web APIに特化しています。

var restify = require('restify');
http = require('http');
http.globalAgent.maxSockets = 50;

function respond(req, res, next) {
    res.send('hello');
    next();
}

var server = restify.createServer();
server.get('/', respond);

server.listen(3000, function(){});

f:id:slowquery:20141216221128p:plain

f:id:slowquery:20141216221138p:plain

Node.js - Hapi

Walmart Lab製のWAFです。自由度の高いExpressや類似フレームワークに対して、ある程度統制を効かせてコードをクリーンに保とうという意識を感じます。WebAPIのルーティングも可読性が高いです。日本ではあまり知られていないように思いますが、小売系の他npm、Mozillaなどtech系企業のユースケースも目立ちます。

var Hapi = require('hapi');
var http = require('http');
http.globalAgent.maxSockets = 50;

var server = new Hapi.Server();
server.connection({ port: 3000 });

server.route({
    method: 'GET',
    path: '/',
    handler: function (request, reply) {
        reply('Hello');
    }
});

server.start(function () {});

f:id:slowquery:20141216221156p:plain

f:id:slowquery:20141216221209p:plain

Ruby - Sinatra

言わずと知れたマイクロフレームワークです。フォロワーのWAFが大量に出たのがわかるミニマルな記法です。WEBRickは遅かったためThinに変更しています。

require 'sinatra'

set :server, 'thin'
set :logging, nil

get '/' do
  'Hello'
end

f:id:slowquery:20141216221231p:plain

f:id:slowquery:20141216221244p:plain

Ruby - Espresso

速度が売りのマイクロフレームワークです。こちらもSinatraに合わせてThinに変更しています。期待してなかったのですが検討しています。

require 'e'

class App < E
  map '/'

  def index
    "Hello"
  end
end

App.run :server => :Thin

f:id:slowquery:20141216221257p:plain

f:id:slowquery:20141216221305p:plain

Python - bottle

Bottle: Python Web Framework

恐らくPythonで最もミニマルなWAFで個人的にもよく使っています。WSGIに対応していてアプリケーションサーバFacebook製のTornadoに変更しました。

from bottle import route, run


@route('/', methods=['GET'])
def index():
    return "Hello"

run(server='tornado', host="0.0.0.0", port=3000, debug=False)

f:id:slowquery:20141216221318p:plain

Haskell - Scotty

Sinatraに影響を受けたHaskellのマイクロWebフレームワークです。Haskellの中では最も親しみやすい印象でした。この記法でこの性能はいい感じです。

{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty

main = scotty 3000 $ do
    get "/" $ do
        text "Hello"

f:id:slowquery:20141216221342p:plain

f:id:slowquery:20141216221350p:plain

Go - Martini

DIの仕組みをもったGoのマイクロフレームワークです。通常の設定だと標準出力にログが出て速度がやや遅くなったのでzishe/app.goを参考にログを消しています。Golangの人気がうかがえる速度です。

package main

import (
    "log"
    "github.com/go-martini/martini"
)

func init(){
    martini.Env = martini.Prod
}

type myClassic struct {
    *martini.Martini
    martini.Router
}

func withoutLogging() *myClassic {
    r := martini.NewRouter()
    m := martini.New()
    m.Use(martini.Recovery())
    m.MapTo(r, (*martini.Routes)(nil))
    m.Action(r.Handle)
    return &myClassic{m, r}
}

func main() {
    m := withoutLogging()
    m.Get("/", func(lg *log.Logger) string {
        return "Hello"
    })
    m.Run()
}

f:id:slowquery:20141216221416p:plain

f:id:slowquery:20141216221424p:plain

Go - beego

MartiniよりリッチなGoのフレームワークです。マイクロフレームワークかと思っていたらフルスタックとのこと。フルスタックかつ高性能と謳っています。

package main

import (
    "github.com/astaxie/beego"
)

type MainController struct {
    beego.Controller
}

func (this *MainController) Get() {
    this.Ctx.WriteString("Hello")
}

func main() {
    beego.Router("/", &MainController{})
    beego.Run()
}

f:id:slowquery:20141216221442p:plain

f:id:slowquery:20141216221450p:plain

注意

この記事はフレームワークの性能を表すものではありません。以下のようにフレームワーク周辺領域での選択やアプリやミドルウェアの構成による影響が大きく、何をもって"フレームワークの性能"と定めるか容易には決められないからです。

  • ディスクやネットワークのIO。その性能もORMやコネクタに左右されます。
  • シリアライズやバリデーションなど入れ替え可能な関連ライブラリ
  • PythonWSGIRubyのRackのようにWebサーバとWebアプリケーションフレームワーク間の統一インタフェースが提供されている場合、Webサーバの選択が性能に大きく影響します。
  • ルーティングを辞書でなく配列に格納してマッチングするWAFの場合、探索の計算量がO(N)となるため登録されているルーティングが多くなると性能が劣化してしまいます。

※サンプルコードはテキストでHelloという文字列を返すだけでWeb APIとしては不完全です。

感想

WebAPIの負荷テストツールを使うことで新たなフレームワークを試すのが楽しくなりました。この記事以外にも様々なフレームワークを試した結果、記法が美しいものから魅力的なレスポンスタイムを持つものまで様々なフレームワークに出会えましたが、その中で共通して感じたのはWeb APIを実装する敷居が低くなったということです。

昨今、サービスがWeb APIを公開するのはもちろんのこと、Web APIが組み込まれたソフトウェアも目立つようになりました。ソフトウェアのWebAPIがこの記事で使ったようなマイクロフレームワークで実装されていることもあります。

このような流れが加速してサービスもソフトウェアもWeb APIを持つのが当たり前になり、誰もがコードから簡単に操作できる自由な世界が訪れるといいですね。

Enjoy!

OSXとHomebrew-caskでHaskellの開発環境を構築

この記事ではHomebrewではなくHomebrew-caskを使ってHaskellの環境を構築します。

インストールには、以下のOSX用のHaskellディストリビューションを使います。コンパイル済みのバイナリとcabalが入っていて早くてお手軽です。

GHC

  1. インストール

     brew cask install ghc
    
  2. GHCのパスを通す

    1. F4を押してlaunchpadを起動します。
    2. ghcと入力してghc-7.8.3を起動します。
    3. 画面左上にあるAppend to ~/.bash_profileボタンをクリックします。

      f:id:slowquery:20140727115203p:plain

      ※ 表示されているスクリプト.bash_profileに追加されます。

      zshの場合は表示されているスクリプト.zshrcなどに自分で追加します。

  3. GHC起動

     ghc     
    

Cabal

Haskellのパッケージ管理ツールcabalは、初めからインストールされているのでアップデートするだけです。

  • cabal自体のアップグレード

      cabal install cabal-install
    
  • パッケージアップデート

      cabal update
    

OSX 10.10 Yosemiteでhomebrewによるgccインストールのエラーを回避する方法

OSX 10.10 YosemiteのDeveloper Preview版で普通にbrew install gccgccをインストールしようとするとエラーになると思います。ここではパッチを当ててgcc4.9.1をインストールしてみます。

前提

  • OSX 10.10 Developer Preview 4
  • gcc 4.9.1

参考

https://github.com/Homebrew/homebrew/issues/29845

方法

  1. homebrewをアップデートしてgccのformulaを最新版にします。

     brew update
    
  2. gccのformulaの編集モードに入ります。

     brew edit gcc
    
  3. 2行目に以下の4行を追加します。

     patch do
       url "https://gcc.gnu.org/bugzilla/attachment.cgi?id=33180"
       sha1 "def0cb036a255175db86f106e2bb9dd66d19b702"
     end
    

     class GCC < Formula
    
       # Patch for OSX 10.10 Developer Preview 4
       # ref. https://github.com/Homebrew/homebrew/issues/29845
       patch do
         url "https://gcc.gnu.org/bugzilla/attachment.cgi?id=33180"
         sha1 "def0cb036a255175db86f106e2bb9dd66d19b702"
       end
    
       def arch
         if Hordware::CPU.type +== : intel
         略
    
  4. インストール

     brew install gcc
    

以上

OSXでssh-copy-idを使ってリモートサーバにパスワードなしでログインする

OSXssh-copy-idを使う方法です。

何ができる?

  1. sshでサーバにログインする際のパスワード入力が不要になる。
  2. そのために必要なssh公開鍵の配布を簡単に行うことができる。

インストール

beautifulcode/ssh-copy-id-for-OSX - GitHubを使います。

  1. ダウンロード

     curl https://raw.githubusercontent.com/beautifulcode/ssh-copy-id-for-OSX/master/ssh-copy-id.sh -o /usr/local/bin/ssh-copy-id
    
  2. 実行権限を付与

     chmod +x /usr/local/bin/ssh-copy-id
    

使い方

  1. 公開鍵を生成します。~/.ssh/id_rsa.pubが存在する場合は不要です。

     ssh-keygen  
    

    以下が生成されます。

     ~/.ssh/id_rsa.pub
    
  2. 公開鍵を配布します。

    通常

     ssh-copy-id [ユーザID]@[リモートサーバ]
     ssh-id-copy root@192.168.100.10 #例
    

    ~/.ssh/id_rsa.pub以外の公開鍵を使う場合

     ssh-copy-id -i 公開鍵のフルパス [ユーザID]@[リモートサーバ]
     ssh-id-copy ~/.ssh/myidentity.pub root@192.168.100.10 #例
    

    ※ ログインパスワードを聞かれるので入力します。

  3. sshでリモートのサーバにログインします。

     ssh [ユーザID]@[リモートサーバのアドレス]
     ssh root@192.168.100.10 #例
    

    ※ ログインパスワードは聞かれません。