Nginx + Lua (Openresty) のキワモノ設定プラクティス

食べログ DevOps チームの @weakboson です。

この記事は Advent Calendar の9日目の投稿です。

Nginx とのどつきあいの果てに産まれたキワモノ設定プラクティスをお披露目します。


アクセス元IPでログを分ける


nginx.conf

http {

geo $client_location {
10.0.0.0/8 intra;
172.16.0.0/12 intra;
192.168.0.0/16 intra;
10.255.241.0/24 monitor;
default customer;
}
map $client_location $intra_log {
intra 1;
default 0;
}
map $client_location $customer_log {
customer 1;
default 0;
}
map $client_location $monitor_log {
monitor 1;
default 0;
}

server {
server_name example.com;
access_log /var/log/nginx/intra/access.log intra_format if=$intra_log;
access_log /var/log/nginx/monitor/access.log minimum_format if=$monitor_log;
access_log /var/log/nginx/customer/access.log customer_format if=$customer_log;
}
}


ストレージのコストがそんな高くもない昨今、こんなことせずにアクセスログは1箇所に富豪的に全部のせ1フォーマットで吐いて、集計するときに加工する方がよいと思います。(いきなりの全否定)


proxy_pass したときの妙なデフォルト Host ヘッダを活用する

proxy_set_header ディレクティブを使わなくて済む。1行くらいお得。


first-proxy.conf

http {

upstream application {
app1.example.com;
app2.example.com;
}
upstream asset {
app1.example.com;
app2.example.com;
}
server {
server_name example.com;
location ~ \.(js|css)$ {
proxy_pass http://asset;
}
location / {
proxy_pass http://application;
}
}
}


second-proxy.conf

  upstream rails {

server unix:/application/current/tmp/sockets/rails.socket;
}
server {
server_name application;
location / {
proxy_pass http://rails;
}
}
server {
server_name asset;
root /application/current/public;
}

実際どういうときこんな構成にするのよ?

アプリのコードをアプリケーションサーバだけにデプロイしたいときいいかもですね。Capistrano の rollback で巻き戻しができるかも。


パスごとのアクセス制限をえいやーと実装


nginx.conf

http {

geo $customer {
100.64.0.1/32 customer_a;
100.64.0.2/32 customer_b;
100.64.0.3/32 customer_c;
default not_customer;
}
server {
server_name example.com;
access_by_lua_file customer_acl.lua;
}
}


customer_acl.lua

if not string.match(ngx.var.uri, "^/" .. ngx.var.customer .. "/") then

return ngx.exit(ngx.HTTP_FORBIDDEN)
end

http://example.com/customer_a/ は 100.64.0.1 からのみアクセス可、http://example.com/customer_b/ は 100.64.0.2 からのみアクセス可……というアクセス制限が Lua 3行で実現できそうです。


外部ネットワークアクセスのときだけ Server ヘッダを消す


nginx.conf

http {

geo $server_visible {
10.0.0.0/8 1;
172.16.0.0/12 1;
192.168.0.0/16 1;
default 0;
}
header_filter_by_lua_file hide_server.lua;
}


hide_server.lua

if ngx.var.server_visible == "0" then

ngx.headers['Server'] = nil
end

なんかこんなのばかりで飽きてきた…… Nginx を使っているコト自体伏せたいときにでも。他にも内部アクセスのときは拡張 HTTP ヘッダで詳細な情報を返して、外部アクセスのときは消しちゃうという使い方もできそうです。

わざわざ Lua を使わなくても more_clear_headers でもできそうですが、more_clear_headers は location 以外で条件分岐できません。こんな感じなら ok ですが、location が増えたとき大変なことに。


nginx.conf

server {

server_name example.com;
location / {
if ($server_visible) {
more_clear_headers Server;
}
}
}


CORS ヘッダをきめ細やかにセット

NGINX Cookbook by Derek DeJonghe の 11.2 Allowing Cross-Origin Resource Sharing で紹介されていた CORS 実装の location if を使わない版です。


nginx.conf

http {

map $request_method $cors_method {
OPTIONS 11;
GET 1;
POST 1;
default 0;
}
map $uri $need_cors {
~\.(svg|eot|ttf|woff)$ 1;
}
server {
server_name example.com;
header_filter_by_lua_file add_cors_header.lua;
}
}


add_cors_header.lua

local function add_cors_headers()

local cors_method = ngx.var.cors_method
if cors_method == "0" then
return
end

if string.match(cors_method, "1") then
ngx.header["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS"
-- アセット専用ドメインがあったり複数ドメインで共有してたりするなら
-- Access-Control-Allow-Origin は server_name じゃだめかもしれない
ngx.header["Access-Control-Allow-Origin"] = ngx.var.server_name
ngx.header["Access-Control-Allow-Headers"] = "DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type"
end

if cors_method == "11" then
ngx.header["Access-Control-Max-Age"] = "1728000"
ngx.header["Content-Type"] = "text/plain; charset=UTF-8"
ngx.header["Content-Length"] = "0"
ngx.exit(ngx.HTTP_NO_CONTENT)
end
end

if ngx.var.need_cors == "1" then
add_cors_headers()
end



CORSヘッダ不要な場合

$ curl -I -X GET http://localhost/index.html

HTTP/1.1 200 OK
Server: openresty
Date: Thu, 06 Dec 2018 13:23:00 GMT
Content-Type: text/html
Content-Length: 53
Last-Modified: Thu, 06 Dec 2018 13:18:24 GMT
Connection: keep-alive
ETag: "5c0921a0-35"
Accept-Ranges: bytes


CORSヘッダが必要な場合

$ curl -I -X GET http://localhost/example-glyph.woff

HTTP/1.1 200 OK
Server: openresty
Date: Thu, 06 Dec 2018 13:23:55 GMT
Content-Type: application/x-font-woff
Content-Length: 43244
Last-Modified: Thu, 06 Dec 2018 13:16:04 GMT
Connection: keep-alive
ETag: "5c092114-a8ec"
Access-Control-Allow-Methods: GET,POST,OPTIONS
Access-Control-Allow-Origin: example.com
Access-Control-Allow-Headers: DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type
Accept-Ranges: bytes


プリフライトリクエスト

$ curl -I -X OPTIONS http://localhost/example-glyph.woff

HTTP/1.1 204 No Content
Server: openresty
Date: Thu, 06 Dec 2018 13:21:30 GMT
Connection: keep-alive
Access-Control-Allow-Methods: GET,POST,OPTIONS
Access-Control-Allow-Origin: example.com
Access-Control-Allow-Headers: DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type
Access-Control-Max-Age: 1728000
Content-Length: 0


投げやりな締め

Nginx は server や http といった大きな範囲に共通して適用したい分岐ルールというのが非常に実装しづらい Web サーバだと思います。 それは geomap という http コンテキストに書ける便利な変数設定ディレクティブはあっても、その変数を条件やパラメータにできるディレクティブが location にしか書けないことが多いからでないかと思います。

例えば「外部ネットワークアクセスのときだけ Server ヘッダを消す」を例に挙げると、http コンテキストにおいて判定はできているのに、条件付きで Server ヘッダを隠すディレクティブが location にしか記述できません。一方 Lua の Hook は http, server に設定できるため、上層で判定したことについて同じく上層で実施もできます。条件判定は宣言的に nginx.conf に記述して、その条件に従い何かを施行するのは Lua に記述するというのが nginx.conf をメンテしやすく保つプラクティスの一つなのかもなーと思います。

明日、21日目は @satoru_fukagawa による「iOSのUILabelのサイズそのままでHiragino SansのqyjpやÉの上下が見切れないようにする」です。