Open31

Nginx入門

akitatataakitatata

やりたいこと:
・nginxのセットアップ(nginx.conf利用)
・リバースプロキシの設定と動作確認

akitatataakitatata

https://hub.docker.com/_/nginx
Dockerページに色々書いてある。これを試せば良さそう
とりあえずConfをみてみたいので、ローカル端末に落とす

cmd
$ docker run --name tmp-nginx-container -d nginx
$ docker cp tmp-nginx-container:/etc/nginx/nginx.conf /host/path/nginx.conf
$ docker rm -f tmp-nginx-container
akitatataakitatata

取得したnginx.confの中身

nginx.conf

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

これの意味を調べていく

akitatataakitatata
  • user nginx;

  • worker_processes auto;

  • error_log /var/log/nginx/error.log notice;

    • http://nginx.org/en/docs/ngx_core_module.html#error_log
    • Syntax: error_log file [level];となっている
    • パス指定しているのがエラーログの配置場所
    • noticeの部分がログレベルに当たる。これはオプションパラメータ
      • デフォルトはerrorとなっている
      • debug, info, notice, warn, error, crit, alert, or emerg が指定可能
    • “syslog:” prefixで syslog吐き出しもできるみたいだ?
  • pid /var/run/nginx.pid;

akitatataakitatata
cnf
events {
    worker_connections  1024;
}
  • http://nginx.org/en/docs/ngx_core_module.html#worker_connections
  • ワーカープロセスにより開かれる接続の最大数を定義する
  • クライアントだけではなく、プロキシサーバーとの接続数などすべてがここに含まれる
  • 値は好きに設定できるが、OSで開けるファイル数の最大値を超えることはできないので注意
  • ファイル数の最大値~の部分はworker_rlimit_nofileにて設定できるとのこと
  • 接続数を増やしたいとき実際どうすればいいのか?
    • ここが分かりやすい
    • OSで開けるファイル数の最大値をX、ワーカープロセス数をYとしたとき
      • worker_rlimit_nofile < 0.95 * X/Y
      • ここでOSで開けるファイル数の最大値の95%をNginxに割り当てるよう係数をかけている
    • 算出したworker_rlimit_nofileから適切なworker_connectionsを計算
      • worker_connections * 4 < worker_rlimit_nofile
      • ファイル接続時の諸々で~4倍くらいの接続数が必要になるらしい
akitatataakitatata

httpブロックの中身をみていく

  • include /etc/nginx/mime.types;

  • include /etc/nginx/conf.d/*.conf;

    • 文字通りinclude
  • default_type application/octet-stream;

  • log format

    • logformat
      log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                            '$status $body_bytes_sent "$http_referer" '
                            '"$http_user_agent" "$http_x_forwarded_for"';
      
    • http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format
    • ログフォーマットの設定
    • Syntax: log_format name [escape=default|json|none] string ...;
    • 上の例ではmain という名前が定義されている
    • 必要に応じてログにescapeを設定できるらしい
    • Nginxのドキュメントに各項目の意味するところも記載されていた
      • 時間関係はきちんと定義確認しておきたい
      • `$time_local‘:ローカル日時
        • 公式ドキュメントをサラッと見た感じでは、これがどのタイミングなのか記載なし
        • ここ でレスポンスを返した日時と確認されている
      • $msec:ミリ秒解像度でのログ書き込み時刻
      • $request_time:リクエストの処理時間
        • ミリ秒解像度
        • リクエストの最初の1byteを読み込んだ時点から、レスポンスの最後の1byteを送信しログ書き込みをするまでの時間
      • その他、upstreamとの通信に関する詳細ログも出せるらしい
akitatataakitatata
  • access_log /var/log/nginx/access.log main;
    • http://nginx.org/en/docs/http/ngx_http_log_module.html#access_log
    • Syntax: access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
      • 無効化:access_log off;
    • format にはログフォーマット名を指定する
    • buffer か gzip が指定されているとログは(メモリ上に?)バッファとして保持される
      • あまり具体的な記載がない。想像。
    • flushはバッファに保持されるログデータの生存時間を設定するみたいだ
      • バッファに持ったままでflush指定の時間が経過したデータはファイルに書き込まれる
    • gzipは1~9が指定可能。1は圧縮甘いが早く、9はしっかり圧縮するが遅い
      • gzip指定時はバッファからログファイル書き込み時に圧縮適用される
    • ログ書き込みするかどうかの条件を ifで定義可能らしい。すごい
      • status 2xx, 3xxは書き込まないようにする設定のサンプル
sample
map $status $loggable {
    ~^[23]  0;
    default 1;
}
access_log /path/to/access.log combined if=$loggable;
akitatataakitatata
akitatataakitatata
  • keepalive_timeout 65;
    • http://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_timeout
    • Syntax: keepalive_timeout timeout [header_timeout];
    • クライアントとの間でアクセスがないまま開きっぱなしとなっているコネクションを閉じるまでの時間
    • header_timeoutを指定するとレスポンスヘッダーにKeep-Alive: timeout=timeが付与される

keepalive系は気になるので他のも確認しておく

↓が非常に分かりやすかった
https://doudonn.com/saba/2365/

akitatataakitatata

timeout周りの確認

全体のタイムアウト設定は見つからず、、
これ全部デフォルトの場合 リクエストheader受信59秒, body受信59秒, サーバー内での処理X秒, クライアントへのレスポンスsend59秒 ... というケースでタイムアウト発生しないということなのか?

akitatataakitatata

プロキシ設定する場合は proxy*timeout を設定するらしい

akitatataakitatata

https://zenn.dev/teasy/articles/nginx-reverse-proxy を読む

  • リバースプロキシを構成するには設定ファイルを読み込ませる必要がある
  • 設定ファイルは/etc/nginx/conf.d/*.confとして配置すればOK(nginx.confでincludeしているため)
  • 設定ファイルでは serverブロックを使う
  • locationとしてパスを指定し、そのエンドポイントで有効にしたい設定を書いていくのが基本なようだ
  • proxy_passでプロキシ先を指定するらしい
akitatataakitatata

プロキシ用の設定ファイルを準備

proxy.conf
server {
    location /home {
        proxy_pass http://localhost:3000/internal/home;
    }
}

Docker準備

Dockerfile
FROM nginx
COPY nginx.conf /etc/nginx/nginx.conf
COPY proxy.conf /etc/nginx/conf.d/proxy.conf

実行

cmd
cd {作業ディレクトリ}
docker build -t custom-nginx .
docker run --name my-custom-nginx-container -d -p 8080:80 custom-nginx

とりあえず、これで起動はOK。 http://localhost:8080/にアクセスできる

akitatataakitatata

サーバー側をExpressで準備

main.js
var express = require("express");
var app = express();

var server = app.listen(3000, function(){
    console.log('express start. port:' + server.address().port);
});

app.get("/internal/home", function(req, res){
  res.send('get /internal/home success!');
});
cmd
npm init
{適当な設定}
npm install -s express
node main.js

http://localhost:3000/internal/homeにアクセスして文字列表示を確認。OK
これでhttp://localhost:8080/homeにアクセスすればhttp://localhost:3000/internal/homeにプロキシされるはず... →404。なぜ

akitatataakitatata

docker やり直しコマンドメモ

cmd
docker rm -f my-custom-nginx-container
docker image rm custom-nginx

docker build -t custom-nginx .
docker run --name my-custom-nginx-container -d -p 8080:80 custom-nginx
akitatataakitatata

Docker内外の通信ができずにコケている予感。後で調べる

akitatataakitatata

検証環境の情報書いてなかった

  • OS: Windows 11 Home 22H2
  • Docker Desktop v20.10.7
akitatataakitatata

docker-composeで nginxも serverもまとめて管理する方針に切り替えることにする。

nodejsの公式ページを参考にセットアップ。
https://nodejs.org/ja/docs/guides/nodejs-docker-webapp

Dockerfile
FROM node:14

WORKDIR /usr/src/app

COPY package*.json ./
COPY main.js ./

RUN npm install

CMD [ "node", "main.js" ]
cmd
docker build . -t node-server
docker run -p 3000:3000 -d node-server

localhost:3000/internal/homeにアクセスできることを確認。
一旦Dockerの準備はOKそうなので、docker-composeの準備に移ることにする

akitatataakitatata
docker-compose.yml
version: '3'
services:
  server:
    build: ./server/
  nginx:
    build: ./nginx/
    ports:
      - "80:80"
cmd
docker compose up

nginx port を80に変更。
解決せず

akitatataakitatata
main.js
var express = require('express');
var morgan = require('morgan');

var app = express();
app.use(morgan('combined'));

var server = app.listen(3000, function(){
    console.log('express start. port:' + server.address().port);
});

app.get('/internal/home', function(req, res){
  res.send('get /internal/home success!');
});

server側にログ出力を追加。
/internal/home以外へのアクセスでもログが出るようになったが、Nginxからプロキシアクセスされた形跡はなし。何か設定が間違っているのか足りないのか

akitatataakitatata

server_name 定義して再トライしたら違うログ出てきた。これ必須なのか
プロキシが動くところまではOK, あとはexpressに接続できてないところをどうにかすればよい

log
nginx_1   | 2023/06/20 01:04:19 [error] 33#33: *1 connect() failed (111: Connection refused) while connecting to upstream, client: 172.18.0.1, server: localhost.example.com, request: "GET /home/ HTTP/1.1", upstream: "http://127.0.0.1:3000/internal/home/", host: "localhost.example.com"
nginx_1   | 172.18.0.1 - - [20/Jun/2023:01:04:19 +0000] "GET /home/ HTTP/1.1" 502 559 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" "-"
akitatataakitatata

色々調べてやっと動作確認できた。
この時点でのソース↓
https://github.com/bwsunkist/nginx-proxy-trial/commit/d488488766bbc1869fa0c427aa739673991f3891

  • docker-composeでコンテナ間通信する際のお作法(サービス名でアクセスできるとか)をあまり分かっていなかったため、そこで1つ躓いた。

    • このあたりを読んでプロキシ先指定の記載を修正した
    • これだけだと動かない
  • どうやら resolverを指定してやらないと上で使い始めたサービス名を解決できずアクセスに失敗するようだ

    • ここの回答をみて追加したら動くようになった
    • resolver 127.0.0.11 ipv6=off;
    • resolver句と、このIPが何者なのかはあとで調べる。TODO
akitatataakitatata
  • resolver

    • http://nginx.org/en/docs/http/ngx_http_core_module.html#resolver
    • 名前解決先を指定する
    • ドメイン名、IPどちらでも可。ポートしていない場合は53ポートになる
    • デフォルトでは IPv4, v6どちらも使われる。必要ない場合はipv6=offなどと無効化する
    • キャッシュの仕組みがある。基本はTTL5分。validパラメータで上書き可能
  • 127.0.0.11の正体

    • ここに書いてあった
    • DockerデーモンにDNSサーバが内蔵されているらしい
akitatataakitatata

その他、ざっと抑えた方がよさそうな設定を確認

その他、セキュリティ対策系は確認しておきたい。
このあたり後で読む

akitatataakitatata

各種モジュールも見ておきたい
認証モジュール(ngx_http_auth_request_module)

  • サブリクエストを飛ばし、その結果でアクセスを許可するか決定する

    • 2xxなら許可
    • 401 or 403なら拒否、そのエラーを返却(?)
    • その他はエラー
  • Syntax: auth_request uri | off;

    • サブリクエストの送信先を指定
  • Syntax: auth_request_set $variable value;

    • 認証サブリクエストの完了後、リクエストに変数を設定する
    • 認証リクエストの値も設定可能
akitatataakitatata

auth_request確認用のサーバーを準備する。
/internal/homeに加え、認証エンドポイントとして/internal/authを追加する(例なので認証はヘッダーの確認のみとする)

main.js
var express = require('express');
var morgan = require('morgan');

var app = express();
app.use(morgan('combined'));

var server = app.listen(3000, function(){
    console.log('express start. port:' + server.address().port);
});

app.post('/internal/home', function(req, res){
  const authHeader = req.headers.authorization;

  if (authHeader === "accessToken_home") {
    res.send('get /internal/home success!');
  } else {
    res.status(401).send('Unauthorized /internal/home')
  }
});

app.get('/internal/auth', function(req, res){
  const authHeader = req.headers.authorization;

  if (authHeader === "accessToken_auth") {
    res.setHeader('Authorization', 'accessToken_home')
    res.send('get /internal/auth success!');
  } else {
    res.status(401).send('Unauthorized /internal/auth')
  }
});

動作確認。
指定のAuthorizationヘッダーが付与されている場合のみ200を返す

cmd
C:\work\nginx-test>curl -X POST  http://localhost:3000/internal/home
Unauthorized /internal/home
C:\work\nginx-test>curl -X POST  http://localhost:3000/internal/home -H "Authorization: accessToken_home"
get /internal/home success!

C:\work\nginx-test>curl  http://localhost:3000/internal/auth
Unauthorized /internal/auth
C:\work\nginx-test>curl  http://localhost:3000/internal/auth -H "Authorization: accessToken_auth"
get /internal/auth success!
akitatataakitatata

これで動いた。

proxy.conf
server {
    listen 80;
    server_name localhost.example.com;

    location /home {
        resolver 127.0.0.11 ipv6=off;

        auth_request /auth;

        auth_request_set $authorization $upstream_http_authorization;
        proxy_set_header Authorization $authorization;

        set $url http://node-server:3000/internal/home;
        proxy_pass $url;
    }

    location /auth {
        resolver 127.0.0.11 ipv6=off;

        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        proxy_set_header X-Original-URI $request_uri;

        proxy_pass http://node-server:3000/internal/auth;
    }
}

動作確認

cmd
C:\work\nginx-test>curl -X POST  http://localhost.example.com/home  -H "Authorization: accessToken_auth"
get /internal/home success!
server_log
node-server_1  | ::ffff:172.19.0.3 - - [25/Jun/2023:00:35:15 +0000] "GET /internal/auth HTTP/1.0" 200 27 "-" "curl/8.0.1"
nginx_1        | 172.19.0.1 - - [25/Jun/2023:00:35:15 +0000] "POST /home HTTP/1.1" 200 27 "-" "curl/8.0.1" "-"
node-server_1  | ::ffff:172.19.0.3 - - [25/Jun/2023:00:35:15 +0000] "POST /internal/home HTTP/1.0" 200 27 "-" "curl/8.0.1"

ちょっとハマったポイント

  • auth_requestでURL指定したら 404。/authではうまくいった
  • location /authの中でもset $url xxxからproxy_passとしていたが、そうすると/internal/authにGETとPOSTが1度ずつ飛んでしまった。どうも/authでの書き換えが/homeに影響してしまっているようにみえる
  • auth_request_setは認証リクエストの値を変数にセットするもの。それを本リクエストに利用したい場合は変数をproxy_set_headerなどでセットする必要あり