この記事は何か
nginxについて、丁寧に書いてあるQiitaとかを見つけられなかったので、自分なりに簡単に仕組みと使い方をまとめたものです。
間違ったことなどを書いていたら、コメント頂けますと幸いです。
nginxは何をしてくれるものなのか
HTTPおよび、HTTPSでのアクセスに使われる軽量サーバーのことで、
nginx(えんじんえっくす)は、HTTPサーバーとしての基本的な機能はもちろん、
以下のような機能を備えています。
- アクセス制御
- URI Rewrite
- gzip圧縮
- リバースプロキシ
- ロードバランシング
- HTTP/2ゲートウェイ
この他にもかなりたくさんの機能があるようですが、ここでは一般的なHTTPSサーバーとしての機能に留めます。
nginxのアーキテクチャ
- イベント駆動
- ノンブロッキングI/O
- マスタプロセスとワーカプロセスのマルチプロセス
によってnginxの軽量な動作を実現しています。
これらの3つのアーキテクチャについて説明します。
イベント駆動
通常のプログラムでは、
int main(void) {
....
}
int func1(void) {
....
}
とあったら、まずはmain関数から呼び出されます。
main関数の中でfunc1が呼ばれればそのタイミングでfunc1が呼ばれますね。
nginxでは、コールバック関数が呼ばれるまでmain関数も呼ばれません。
逆にmain関数が呼ばれなくともfunc1が呼ばれることもあります。
コールバック関数はどのようなタイミングで呼ばれるかというと、
- クライアントから接続要求が行われた。
- ファイルやソケットに対する読み込みや書き込みが可能になった。
などの場合です。
このようなタイミングで、定義されている関数が呼ばれる仕組みになっています。
イメージとしては、jsのAddEventListenerのような形でしょうか。
ノンブロッキングI/O
ファイルやソケットへのI/Oが処理を完了していない状態で、プログラムがイベント駆動によって発生した場合、
通常であれば、処理が完了するまでブロッキングするのですが、
ノンブロッキングI/Oでは、即座にエラーを返し適切なerrnoがセットされます。
そうすることでプログラムの並行性を高めているそうです。
nginxはマスタプロセスとワーカプロセスのマルチプロセス構成で稼働する。
マスタープロセスは1つに対してワーカプロセスは設定することで複数起動可能です。
これとノンブロッキングI/Oは非常に相性が良く、適切に複数のシグナルを受け取ることができます。
マスタープロセスはワーカープロセスを管理します。
マスタープロセスが受信したシグナルの種類に応じて各ワーカープロセスにもシグナルを送る形です。
kubernetesのmaster nodeのような動きですね。
実際にインストールしてみる
nginx自体のインストールは公式にあるので、curlで簡単に落とせます。
PATHを通すのが大変なので、yum, aptで入れたほうが楽かもですが、ここでは
wget http://nginx.org/download/nginx-1.7.7.tar.gz
で入れることを想定します。
PATHは nginxの中のsbinに通してあげてください。
export PATH=/usr/local/nginx/sbin
ビルドに必要なもののインストールもしておきましょう。
yum install gcc
yum install pcre pcre-devel
yum install zlib zlib-devel
yum install openssl openssl-devel
入ってなさそうなものがあったら入れておきましょう。
これらは、
- GCC nginxのビルドに必要
- PCRE 正規表現処理に必要
- zlib gzip圧縮に必要
- OpenSSL SSL/TLSがないとWebサイトは公開出来ません。
./configure
によってあらゆるモジュールを有効化します。
./configure \
--user=riita\
--group=riita\
--with-http_ssl_module\
--with-openssl=OpensslPath\
--with-zlib=zlibPath
こんな感じです。
色々なモジュールがあるのでそれは別途調べてみてください。
# make
...中略
# make install
...中略
でビルドされてインストールされます。
nginxの起動
sudo service nginx start
で起動します。
デフォルトでは、80番ポートをバインドした状態でデーモンとしてnginxのプロセスが立ちます。
serviceを指定しているので、serviceとして立ち上がります。
daemonもserviceも常駐するプログラムを指すもので特に違いはないと思います。
sudo nginx -s quit
で終了します。
設定ファイルの構成
普段ぼくがフロント書くこと少ないのですが、フロントエンドを書くとしたら、
Reactで書くことが多いです。
そうすると、ファイル構成としては
- .circleci
- app
- docker-compose.yml
- nginx
- Dockerfile
- nginx.conf
- conf.d
- webapp.conf
という感じの設計になります。
nginx.confの書き方としては
server {
listen 80;
server_name _;
root /var/www/html;
index index.html;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
}
といったように、HTTPサーバーの設定を記述します。
ディレクティブは、プログラミングにおいてコマンドのような意味で使われる用語で、nginx.confの設定項目です。
モジュールは、システムなどを構成する機能的なまとまりで、nginx.confのディレクティブの集まりです。
- Coreモジュール
- Eventsモジュール
- HTTPモジュール
の4種類があります。
Core
Coreモジュールでは、プロセスの管理、セキュリティ、ロギングなどの設定をおこないます。
pid /var/run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 100000;
load_module /etc/nginx/modules/ngx_http_perl_module.so;
env SERVER_NAME;
env API_URI;
env WEBAPP_URI;
のように使います。
Event
パフォーマンスやチューニングを行います。
events {
worker_connections 2048;
multi_accept on;
use epoll;
}
HTTP
サーバーに関する設定を行います。
http{
# listenはポートのみ指定でも可
listen 80;
server_name www.example.com;
root /var/www/html/www.example.com;
charset UTF-8;
}
nginx.confで設定できることまとめ
書き方については、公式documentをご参照ください。
- アクセス制限
- remoteIP
- Basic認証
- 大量リクエスト制限
- リクエストの書き換え
- リダイレクト
- エラーページを返す
- URIとファイルパスを別のものにしたい
- 条件分岐
- リファラ
- UserAgent
- 国によって言語の変更
- gzip圧縮転送
- 圧縮して転送
- あらかじめ圧縮してあるファイルを転送
セキュリティについて
HTTPSを用いた通信をすることで、安全性が保たれます。
HTTPSはTLSによって提供される暗号化通信上でHTTP通信を行う方法です。
そのために必要なことについて説明します。
TLS証明書と鍵ファイルの指定
nginx.confで443番を指定して
listen 443;
とするだけでは不十分でTLSを有効化するにはsslパラメーターを追加する必要があります。
listen 443 ssl;
ssl_certificate nginx/ssl/cert.pem;
ssl_certificate_key nginx/ssl/cert.key;
HTTPS通信では、認証局によって署名されたサーバ証明書とその秘密鍵が必要になります。
それは上のようにパスを指定することでnginxは証明書を利用します。
また、TLSの使用にはOpenSSLを使用しているので、そのモジュールをビルド時にオプションを指定するのを忘れないようにしてください。
HTTP/2
HTTP/2はHTTPの新しいバージョンとして開発されたプロトコルです。
Googleが開発したSPDYを元に策定され、通信路の多重化、ヘッダ圧縮、パイプライニングなどの技術によりより、高速かつ効率よくレスポンスを転送可能です。
nginx1.9.5以上でサポートしています。
また、こちらもモジュールをビルド時に指定してください。
server {
listen 443 ssl http2;
}
とするだけでプロトコルを変更できます。
セッションキャッシュ
TLSにおけるハンドシェイクでは、複雑な処理が複数あり、大きなオーバーヘッドとなっているので、それを削減するために必要なのが、セッションキャッシュの利用です。
セッションキャッシュでは、TLSハンドシェイクに用いられるセッションIDをキーにして次回のTLSハンドシェイクを省略します。
ssl_session_cache shared:SSL:10m;
リバースプロキシ
ユーザーのリクエストを受け取り、それをより上位のサーバーに転送する役割のプロキシサーバーのことです。
具体的な役割としては、
- ロードバランシング
- コンテンツキャッシュ
- HTTPS通信の終端化
- アクセス制御
- リクエストの書き換え
- gzip圧縮転送
- ロギング
- バッファリング
があげられます。
server{
server_name example.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /example/ {
proxy_pass http://localhost:8080/app/;
}
location /example2/ {
proxy_pass http://example2.com/example/;
}
}
ロードバランシング
DNSラウンドロビン
これはロードバランサではないのですが、こちら側のサーバーをいじらなくてもいい方法です。
マイクロサービスのサーバのIPアドレスをDNSレコードとして登録し、DNSの問い合わせ時にバックエンドサーバのIPアドレスを順番に返事することで負荷分散する。
ただ、自前のサーバーではなくあくまでもDNSサーバーのやることなので、TTLの問題が起きたり、結局負荷が偏る可能性があったりと問題があります。
L7ロードバランサ
レイヤー7で負荷分散をします。 (URLやHTTPヘッダーで負荷分散が可能)
よって、通信内容に応じて、リクエストを特定のサーバー群に割り振ることができます。
TCPコネクションが LB で終端するため、合計で 2 本のコネクションが張られることになりますね。
L4ロードバランサ
レイヤー4以下で負荷分散をします。
IPアドレスとポート番号によって割り振ることになります。
通信内容に応じることはできませんが、
LB で TCP コネクションが終端しないため、コネクションは 1 本で済みますね。
MACアドレス変換方式(DSR方式)
- 宛先MACアドレスを差し替ることで転送先を変更する。
- レイヤー2での話なのでネットワークを超えられない。
- ロードバランサとバックエンドサーバは同一セグメントに置く必要がある。
- クライアントから見たときの接続先IPアドレスはLBのIPアドレスになる。
- バックエンドサーバから見たときの接続元IPアドレスはクライアントのIPアドレスになる。
- バックエンドサーバの戻りパケットが、ロードバランサを経由せずにクライアントと直接通信する。
- レスポンスの接続元IPアドレスをロードバランサに見せかける必要がある。
- ロードバランサのIPアドレスをバックエンドサーバのループバックIPアドレスに設定する。
- ループバックIPアドレスはARPに答えてはいけない。
IPアドレス変換方式(NAT方式)
- NATを利用してIPアドレスを変更することで転送先を変更する。
- クライアントから見たときの接続先IPアドレスはLBのIPアドレスになる。
- バックエンドサーバから見たときの接続元IPアドレスはLBのIPアドレスになる。
- IPアドレス変換のために全ての通信がLBを経由するので、LBのネットワーク帯域に負荷がかかる。
https://www.kimullaa.com/entry/2019/12/01/135430
に非常にわかりやすく書いてあるので、ご参照ください。
アクセスログの記録
ログの収集転送などはfluentdがやってくれます。
FluentdはRubyで記述されたログコレクターツールで、ログファイルの収集、柔軟なルーティング、ログの出力が可能です。
nginxのアクセスログをfluentdで収集する。
ディレクトリ構成
- .circleci
- app
- docker-compose.yml
- nginx
- Dockerfile
- nginx.conf
- conf.d
- webapp.conf
- fluentd
- Dockerfile
- fluent.conf
のような感じにしてください。
k8sだとしても、開発環境はdocker-composeでまとめて立ち上げるのがいいと思います。
本番環境はマルチポッドパターンでの運用が多いと思います。
サービスとnginxとfluentdを1つのpodに配置します。
Nginxからfluentdにログを渡してるって勘違いしてる人がいるんですけど、そういうわけじゃないです。
サーバーの永続ディスクのディレクトリ上のファイルにログを順番に置いていくんです。
fluentdはそのディレクトリのtailf(最終行)を見つけて集めてくれてるんです。
loggerによるアプリケーションログもtailではなかったりしますが、基本的な仕組みは同じです。
なので、アプリやNginxとfluentdは繋がってるわけじゃないんです。
当然この仕組みはk8sでも同じです。
fluentdをk8s上に作る時には、replicaset
ではなく、daemonsetを用いて各podに一つづつ配置するように気を使う必要があります。
あるpodにだけfluentdがいなくてそこのログだけ取れてなかったことがあります(苦笑)
fluentdの設定はディレクティブに記述します。
<source>
type tail
format ltsv
path /var/log/nginx/access.log
tag nginx
pos_file /var/log/nginx/access.log.pos
</source>
nginx側は、指定したファイルにログを出しておけばあとはやってくれるので、
そこだけ指定しておきましょう。
http {
log_format ltsv 'time:$time_iso8601\t'
'remote_addr:$remote_addr\t'
'request_method:$request_method\t'
'request_length:$request_length\t'
'request_uri:$request_uri\t'
'https:$https\t'
'uri:$uri\t'
access_log /var/log/nginx/access.log ltsv;
}
こんな感じですね。
これで、nginxの基本的な役割と使い方を一通り書いたと思います。
DockerとかKubernetesもやろうかなと思います。
もっと理論的な方がいいんですかね。
最後まで読んでいただきありがとうございました。