nginxをJavaScriptで動的に設定 - njs - NGINX JavaScript

本記事は「[NGINX Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nginx)」に参加しています。
## TL;DR
- njsとはNGINXの内部で使用可能な、JavaScriptのサブセット
- NGINX自体の動的な設定や拡張に利用可能なもので、Node.jsなどに置き換わるWebアプリ用ではない
- 動的なURLの書き換えやヘッダー・レスポンスの生成が可能。nginx.confの静的な設定以上のことをしたいがWebアプリを作るほどではない場合に有用

## インストール

### NGINX (OSS) の場合
[nginx.orgのレポジトリ](http://nginx.org/packages)で配布されているパッケージでnginx本体をインストールしている場合は、ダイナミックモジュールのパッケージを追加インストールします。
nginx.orgのレポジトリからnginx本体をインストールする方法は[こちら](https://nginx.org/en/linux_packages.html)にあります。

```sh:
$ sudo yum (apt) install nginx-module-njs
```

Linuxディストリビューションで配布されているnginxを使用している場合などに、ソースからビルドする手順

```sh:
# njsライブラリ
cd ~/src # 任意の作業ディレクトリ
hg clone http://hg.nginx.org/njs
cd njs
./configure
make

# njsコマンド
# readline エラーが出る場合は libedit-dev パッケージを追加してみる (Ubuntu)
make njs
sudo cp build/njs /usr/local/bin

# nginx モジュール
# すでに nginx-1.15.6 パッケージがインストールされている状態で、
# ダイナミックモジュールを生成する例
cd ~/src
curl http://nginx.org/download/nginx-1.15.6.tar.gz | tar xfoz -
cd nginx-1.15.6

# ./configure --add-dynamic-module=<njsディレクトリ>/nginx \
#      <nginx -V の configure オプションを全てコピー＆ペースト>
# libssl-dev など、必要パッケージを適宜追加インストールしながら configure コマンドを完了させる
./configure --add-dynamic-module=../njs/nginx --prefix=/etc/nginx
 --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules
 --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log
 --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid
 --lock-path=/var/run/nginx.lock ...
make modules
sudo cp objs/ngx_*_js_module.so /usr/lib/nginx/modules
```

### NGINX Plusの場合

```sh:
$ sudo yum (apt) install nginx-plus-module-njs
```

## ドキュメンテーション

- 総合: https://nginx.org/en/docs/njs/
- リファレンス: https://nginx.org/en/docs/njs/reference.html
- ngx_http_js_module: https://nginx.org/en/docs/http/ngx_http_js_module.html
- ngx_stream_js_module: https://nginx.org/en/docs/stream/ngx_stream_js_module.html

## 基本的な使い方

- スクリプトはnginx.conf等の設定とは別のファイルに記述
- confファイルから呼び出す
- パスは /etc/nginx 相対

```nginx:nginx.conf
# mainコンテキスト
load_module modules/ngx_http_js_module.so;

http {
  js_include js/hello_world.js;
  server {
    location /njs {
        js_content hello;
    }
  }
}
```

```js:hello_world.js
function hello(r) {
    r.headersOut['Content-Type'] = "text/plain";
    r.return(200, "Hello world!\n");
}
```

```sh:実行結果
$ curl localhost/njs
Hello world!
```

## 時刻でレスポンスを変える例

```js:hours.js
function hours(r) {
    var h = new Date().getHours();

    r.headersOut['Content-Type'] = "text/plain";
    if (h >= 9 && h <= 17) {
      r.return(200, "Business hours\n");
    } else {
      r.return(200, "Outside hours\n");
    }
}
```

```nginx:nginx.conf
http {
  js_include js/hours.js;
  server {
    location /hours {
        js_content hours;
    }
  }
}
```

```sh:実行結果
$ curl localhost/hours
Business hours
(9-17時の場合)
```

## サブリクエストの例
あるURLにリクエストがあると、裏でサブリクエストとして天気情報APIにアクセスし、その結果を返すような例を考えます。ここではAPIとしてOpenWeatherMapのWeather APIを利用しました。
Weather API: https://openweathermap.org/api

サブリクエストはr.subquest()で呼び、第3引数にコールバック関数を登録し、そこで戻り値を操作するようになっています。

```js:weather.js
function weather(r) {
    r.subrequest('/weather-api',
      { method: 'GET', args: 'q=Tokyo&APPID=xxxxxxxxxxxx' },
      function(res) {
        if (res.status != 200) {
            r.return(res.status);
            return;
        }

        var json = JSON.parse(res.responseBody);
        r.headersOut['Content-Type'] = "text/plain";
        r.return(200, JSON.stringify(json, undefined, 1));
      }
    );
}
```

サブリクエストはlocalhostにだけアクセスできるようです。今回は外部のAPIをUpstreamとしてReverse Proxyを設定しました。内部リクエストのみを許可しています。

```nginx:nginx.conf
http {
  js_include js/hours.js;
  server{
    location /weather {
        js_content weather;
    }
    location /weather-api {
        internal;
        proxy_pass http://api.openweathermap.org/data/2.5/weather;
    }
  }
}
```

```sh:実行結果
$ curl localhost/weather
{
 "coord": {
  "lon": 139.76,
  "lat": 35.68
 },
 "base": "stations",
 "wind": {
  "speed": 6.2,
  "deg": 330,
  "gust": 12.9
 },
 "name": "Tokyo",
 "visibility": 10000,
 "dt": 1544749200,
 "clouds": {
  "all": 20
 },
 "id": 1850147,
 "sys": {
  "type": 1,
  "id": 8077,
  "message": 0.0051,
  "country": "JP",
  "sunrise": 1544737340,
  "sunset": 1544772509
 },
 "weather": [
  {
   "id": 801,
   "main": "Clouds",
   "description": "few clouds",
   "icon": "02d"
  }
 ],
 "cod": 200,
 "main": {
  "temp": 281.84,
  "pressure": 1020,
  "humidity": 36,
  "temp_min": 281.15,
  "temp_max": 282.15
 }
}
```

## コマンドラインツール
njsというコマンドラインツールがあり、デバッグや動作確認に便利です。

```sh:
$ njs
interactive njs 0.2.6

v.<Tab> -> the properties and prototype methods of v.
type console.help() for more information

>> var d=new Date().getHours()
undefined
>> d
10
```

## 気に入っている点
- nginxの設定を動的に変えられる
- JavaScriptなので取っ付きやすい
- 今の所仕様が小さいので覚えられる

## 注意点
- jsファイルの変更は、nginx -s reload しないと反映されない
- js_include は1行しか書けない
- r.subrequest() は localhost しかアクセスできない
- require() で外部ファイルを読み込めない

## 余談
- シンタックスハイライト可能な言語にnginxが入っていて驚いた。ちょっと色付けがヘンなところがあるが…