食べログ DevOps チームの @weakboson です。
この記事は Advent Calendar の9日目の投稿です。
Nginx とのどつきあいの果てに産まれたキワモノ設定プラクティスをお披露目します。
アクセス元IPでログを分ける
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行くらいお得。
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;
}
}
}
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 で巻き戻しができるかも。
パスごとのアクセス制限をえいやーと実装
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;
}
}
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
ヘッダを消す
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;
}
if ngx.var.server_visible == "0" then
ngx.headers['Server'] = nil
end
なんかこんなのばかりで飽きてきた…… Nginx を使っているコト自体伏せたいときにでも。他にも内部アクセスのときは拡張 HTTP ヘッダで詳細な情報を返して、外部アクセスのときは消しちゃうという使い方もできそうです。
わざわざ Lua を使わなくても more_clear_headers
でもできそうですが、more_clear_headers は location 以外で条件分岐できません。こんな感じなら ok ですが、location が増えたとき大変なことに。
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 を使わない版です。
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;
}
}
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
$ 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
$ 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 サーバだと思います。 それは geo や map という http コンテキストに書ける便利な変数設定ディレクティブはあっても、その変数を条件やパラメータにできるディレクティブが location にしか書けないことが多いからでないかと思います。
例えば「外部ネットワークアクセスのときだけ Server ヘッダを消す」を例に挙げると、http コンテキストにおいて判定はできているのに、条件付きで Server ヘッダを隠すディレクティブが location にしか記述できません。一方 Lua の Hook は http, server に設定できるため、上層で判定したことについて同じく上層で実施もできます。条件判定は宣言的に nginx.conf に記述して、その条件に従い何かを施行するのは Lua に記述するというのが nginx.conf をメンテしやすく保つプラクティスの一つなのかもなーと思います。
明日、21日目は @satoru_fukagawa による「iOSのUILabelのサイズそのままでHiragino SansのqyjpやÉの上下が見切れないようにする」です。