まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:node

REST API やフロントエンドフレームワークを使ったウェブアプリケーションを作っていると、CORS (Cross-Origin Resource Sharing) に悩まされることが珍しくありません:
cors.001


CORS とはウェブブラウザ(フロントエンド)の JavaScript におけるセキュリティ制約の1つで、AJAX などを使って HTTP リクエストを行う場合、原則的には同一オリジンからのリクエストだけが許可され、異なるオリジンからのリクエストは CORS 制約によって失敗するようになっています。バックエンドからの HTTP リクエストに関しては一切制約はありませんが、フロントエンドからのリクエストに対してのみ適用されるものです。詳しくはこちらもどうぞ:
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS


ただ REST API とフロントエンド・ウェブアプリケーションを作っている人が同一であったり、そういったリクエストが来るとわかっている前提で REST API を用意する場合など、API 提供側からもこの CORS 制約を無視したくなることがあります。そういった場合のために「REST API 側で特定のオリジンからの HTTP リクエストについては許可する」設定も可能です。具体的には許可するオリジン(https://allowed-origin.com)を指定して、
 Access-Control-Allow-Origin: https://allowed-origin.com
という HTTP レスポンスヘッダを返します。リクエスト元ではこのヘッダの内容を自分自身のオリジンと比較して、一致している場合のみ正常処理とみなす(一致していない場合はエラーとする)という処理が行われます。なお「全てのオリジンからのリクエストを許可する」という指定も可能で、その場合は例外的に
 Access-Control-Allow-Origin: *
という HTTP レスポンスヘッダを返すことで実現できるようになっています。

さて、実際にフロントエンド側を開発していると、以下のような問題に直面することがあります:
(1)開発中やテスト中は http://localhost:3000 で動かす
(2)開発終了後は https://xxxxx.com で実運用する
(3)REST API は(1)とも(2)とも異なる環境で稼働している(つまり CORS 設定しないと動かない)

開発中は自分の PC でコーディングするので http://localhost:3000 のようなローカルホストを使った動作確認となり、実運用中は運用のために用意した https://xxxxx.com というインターネット上のホストを使ってアクセスする、というケースです(特別に珍しいケースではないと思います)。開発(テスト)環境でも実運用環境でも REST API を動かす必要があるため、REST API 側ではいずれのケースでも動作するような CORS 設定ができると楽です(http://localhost:3000 からのリクエストを許可する是非はともかく)。しかし CORS の Access-Control-Allow-Origin ヘッダには1つのオリジン(それも * 以外の正規表現とかは使えずに)しか記述できない、という制約があります。つまり「http://localhost:3000 または https://xxxxx.com からのリクエストを許可する」という Access-Control-Allow-Origin の指定はできないのですが、これをうまく回避・実現する方法はないでしょうか?

1つの考えられる方法として「開発中だけは "Access-Control-Allow-Origin: *" を指定」して全てのオリジンからのリクエストを許可する、という方法も考えられます。が、これはあまりに無防備な設定でもあります。

そこで考えたのが以下の方法です。あらかじめ許可するオリジンを配列として用意した上で、リクエスト元が許可オリジン配列の中に含まれていた場合はそのオリジンを Access-Control-Allow-Origin ヘッダに動的に指定して返す、という方法です。Node.js + Express 環境向けに具体的に作るとこんな感じ:
// app.js
var express = require( 'express' ),
    app = express();

app.use( express.Router() );

var settings_cors = 'CORS' in process.env ? process.env.CORS : '';
app.all( '/*', function( req, res, next ){
  if( settings_cors ){
    var origin = req.headers.origin;
    if( origin ){
      var cors = settings_cors.split( " " ).join( "" ).split( "," );

      //. cors = [ "*" ] への対応が必要
      if( cors.indexOf( '*' ) > -1 ){
        res.setHeader( 'Access-Control-Allow-Origin', '*' );
        res.setHeader( 'Vary', 'Origin' );
      }else{
        if( cors.indexOf( origin ) > -1 ){
          res.setHeader( 'Access-Control-Allow-Origin', origin );
          res.setHeader( 'Vary', 'Origin' );
        }
      }
    }
  }
  next();
});

app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  res.write( JSON.stringify( { status: true, message: 'PONG' }, null, 2 ) );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );


起動時の環境変数 CORS として許可オリジンの配列(以下の例では "http://localhost:3000" と "https://xxxxx.herokuapp.com" の2つ)をカンマ区切りで指定します:
(例)$ CORS=http://localhost:3000,https://xxxxx.herokuapp.com node app

このサンプル(8080 番ポートで起動)では GET /ping というリクエストに { status: true, message: 'PONG' } というレスポンスを返す REST API が定義されていますが、リクエスト元のオリジンが http://localhost:3000 か https://xxxxx.herokuapp.com のいずれかの場合は Access-Control-Allow-Origin ヘッダによって CORS の制約を回避して実行できます。

これでテスト環境、ステージング環境、本番環境などで CORS の設定を変えずに運用できる API サーバーが用意できそうです。


上記サンプルソースコードはこちらに用意しました:
https://github.com/dotnsf/multicors


運用中のウェブアプリケーションに対して、セキュリティ面を考慮して以下のようなリクエスト制限をかける必要が生じたとします:

・(例えば)10分間で 100 回のリクエストを許可する
・許可数を超えた場合はリクエストを処理しない


クラウドやホスティングサーバーで運用する場合は、クラウド/ホスティング側にそのような機能が提供されていることもあると思います。が、もしそのような機能が提供されていない条件下でこのような要件が生じた場合、アプリケーションの実装としてリクエスト制限を用意する必要が出てくるかもしれません。 今回のブログエントリで紹介するのは、Node.js アプリケーションにリクエスト制限をかける実装方法です。


といっても Node.js (バージョン 14 以上)で Express ライブラリを使っている場合であれば express-rate-limit という Express 向けミドルウェアを使うことで簡単に実装できます:
20220228


以下でサンプルを紹介しますが、サンプルコードはこちらに公開しています:
https://github.com/dotnsf/express-rate-limit-sample


例えば現行のコード(app_old.js)が以下のようになっていたとします:
//. app_old.js
var express = require( 'express' ),
    app = express();

app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

"GET /" リクエストに対して "{ status: true }" を返すだけの内容ですが、処理内容自体はもっと複雑でも構いません。

このアプリケーションに「10分間で 100 回」というリクエスト制限をかける場合は以下のようなコード(app_new.js)に変更します:
//. app_new.js
var express = require( 'express' ),
    app = express();

//. rate limit : 100 times per 10 minutes
var rate = require( 'express-rate-limit' );
var limit = rate({
  windowMs: 10*60*1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false
});
app.use( limit );

app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

express-rate-limit ライブラリをインスタンス化して、属性を指定して app.use() でリクエスト処理前に実行されるようにしています。なお各属性値は以下のような意味です:
・windowMs: 制限をかける時間(ミリ秒)
・max: windowMs で定義した時間でのリクエスト処理上限数
・standardHeaders: "RateLimit-*" でリクエスト制限情報を HTTP ヘッダに含める※
・legacyHeaders: "X-RateLimit-*" でリクエスト制限情報を HTTP ヘッダに含める※

※上述のサンプルでは standardHeaders: true, legacyHeaders: false に指定している

これだけでアプリケーションレベルでリクエスト制限を実装できます。 ただし、この制限は「1インスタンスごとの制限」である点に注意が必要です。クラウド的な言い方だと「1コンテナ」あたりでこの制限が有効になりますが、複数インスタンスで運用した場合、例えば可用性を高める目的で3つのコンテナを起動して運用した場合は、事実上設定値の3倍のリクエスト処理を受け付けることになる、という点に注意が必要です。



備忘録的なブログエントリです。

高速なメモリストアである RedisNode.js から使う場合に多く利用されるライブラリの代表が node-redis です:
2022013000


自分もこれまでに開発した多くのアプリの中で Redis を利用し、Node.js で実装していた場合はほぼ全てで node-redis を使っていました。いわゆる Key-Value 型のデータベースで、高速なデータストア用途だけでなく、ログインセッションの共有保管場所としても使っていました。

これまで使ってきた中でこのような用途がなかったからではあるのですが、今回「登録されている全てのデータのキー」を取得したい、ということがあり、すぐにわからなかったので調べてみました。要は全データを取り出したいのですが、そのためには全キーが必要で、その取得方法を知りたかったのでした。本ブログエントリはその調査結果でもあります。

結論としては RedisClient の keys() というメソッドを使うことで全キーを取得できます。以下サンプルです:
var redis = require( 'redis' );
var redisClient = redis.createClient( "redis://localhost:6379", {} );
redisClient.keys( '*', function( err, results ){
  if( err ){
  }else{
    :
  (results に全キーが配列で格納されている)
    :
  }
});


keys() メソッドを '*'(全て)というパラメータを付けて実行することで全てのキーを取得することができます。

redis CLI でも、
> keys *

のように実行してキー一覧を取得できることは知っていたのですが、node-redis ライブラリで取得する場合の方法を調べるのに手間取ったこともあって、自分でまとめておきました。

以前にとある人から
 (例えば仮想マシン環境と比較して)コンテナ環境のデメリットはなんですか?
と質問され、一瞬返答に詰まってしまったことがありました。メリットを考えたことはあったけど、積極的にデメリットを考えたことがなく、即答できませんでした。ちゃんと正しく理解していないとなかなか難しい質問だと思っています。

改めて時間のある時に考えてみるといくつか思いつきます。まあ「デメリット」といえるかどうかはともかく、「VMでできてコンテナでできないこと」はいくつかあります。

その一つが "cron" ジョブだと思っています。特定時刻とか、何分おきにとか、実行タイミングのスケジュールを決めた上で実行する機能です。例えば kubernetes であれば CronJob を使って実現するなど、厳密には「コンテナで実現できない」わけではないのですが、そのコンテナ環境に合わせた対応が必要になるのはそれはそれでデメリットになりえますよね。

一方、アプリケーション開発レベルでは、これらのスケジュールジョブを併用することでアプリケーションとしての必要な機能を実現することは珍しくありません。1分毎にどこかからデータを取得して更新するとか、毎日○時に自動的にバックアップを取得するとかといった場合です。それらの機能が必要なアプリケーションをコンテナ環境で動かす可能性がある中で実装する場合、どういった方法を検討する必要があるでしょうか?


その答えの1つが「アプリケーションレベルで(アプリケーションの機能の一部として) cron ジョブを実装する」方法です。Node.js アプリケーションの場合は Node-Schedule ライブラリを使うと簡単に実現できそうだったので、その内容を以下で紹介します:
2020071200



Node-Schedule は Node.js で使えるライブラリで、cron ライクなスケジュールジョブを比較的簡単に(setTimeout とかを意識することなく)実現できます。またスケジュールの定義フォーマットは cron のものと互換性があるので、crontab に1行追加する感覚で、アプリケーション内にスケジュールジョブを追加・更新・削除できるものです。アプリケーションの中でスケジュールジョブを定義できるので、アプリケーションの実行環境(実機とか、VM とか、コンテナとか、・・)を意識する必要もありません。

例を1つ記述しておきます。以下のコードで1分おきにコンソールにメッセージを表示するウェブアプリケーションが作成できます(Node-Schedule に関係している部分のみ赤字):
// app.js
var schedule = require( 'node-schedule' );
var express = require( 'express' );
var app = express();

//. 毎分実行
schedule.scheduleJob( '* * * * *', function(){
  console.log( 'running a task every minute' );
});


app.get( '/', function( req, res ){
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( 'server started on ' + port );

上記コードから赤字部分を抜くと、ごくシンプルな Node.js + Express のウェブアプリケーションになるのがわかると思います。つまり Node-Schedule に関係しているのは赤字部分の実質4行だけです。

で、その赤字部分で何をしているのかというと、まず先頭行で require() して Node-Schedule ライブラリのモジュールを呼び出します:
// app.js
var schedule = require( 'node-schedule' );

そして scheduleJob() メソッドを使ってジョブを(イメージとしては cron に)登録します:
//. 毎分実行
schedule.scheduleJob( '* * * * *', function(){
  console.log( 'running a task every minute' );
});

この第一パラメータは cron に登録する時に指定する時刻フォーマットと互換性のある文字列表現を使います。'* * * * *' は「毎分実行」を意味しています。

そして第二パラメータには該当時刻になったら実行するコールバック関数を指定します。上の例では 'running a task every minute' とコンソールに表示するだけの内容にしていますが、実際にはここに crontab の最後に指定するコマンドを登録することになります。これで1分ごとに 'running a task every minute' という文字列がコンソールに表示され続けるジョブが登録できたことになります。


なお、一度登録したジョブをキャンセルするには登録時の scheduleJob() メソッドの実行結果オブジェクトを受け取り、そのオブジェクトの cancel() メソッドを実行します:
//. 毎分実行
var job = schedule.scheduleJob( '* * * * *', function(){
  console.log( 'running a task every minute' );
});

  :

job.cancel(); //. 登録したジョブをキャンセル

ジョブの実行条件や実行内容を更新する場合は一度キャンセルしてから再登録することで実現できます。


最近のクラウド環境は PaaS 化が進み、どういうコンテナ環境を使っているのかよくわからないことがあるかもしれません。アプリケーションを Node.js で記述するという条件はありますが、この方法で実装していればコンテナ環境に依存しないスケジュールジョブが実現できそうです。


Node.js + Express 環境でウェブアプリケーションを動かす際のリクエストタイムアウト値(デフォルトは2分)を変更する方法を調べました。

Node.js はシングルスレッドなので、あまりタイムアウト値を長くしすぎるとそれはそれで支障がでることも考慮する必要があります。その上でタイムアウト値を変更するには以下のようにします:
var express = require( "express" ),
    app = express();

  :
  :

var app_server = app.listen( 8080 );
app_server.timeout = 1000 * 60 * 3;   //. 3分

ポート番号(上例では 8080)を指定して listen() を実行した結果の返り値(app_server)を取得し、その timeout プロパティ値をミリ秒単位で指定するだけです。


くどいようですがもう一度。Node.js はシングルスレッドなので、この値を大きく設定しすぎると、リクエスト処理に待ち行列が発生しやすくなるので要注意を。


このページのトップヘ