3-3. Nginx - リバースプロキシ・ロードバランサ・静的配信

所要時間: 40-50分(がっつりなら2セッション分) ゴール: Nginxの3つの役割(リバプロ/LB/静的)を理解し、Go API への安全なフロントエンドとして本番投入できる設定を書ける コミット内容: 動作する nginx.conf を ~/learn/linux/day15 にコミット

この章が終わるとできること

  • Nginx の3つの役割(リバプロ / ロードバランサ / 静的配信)を区別できる
  • nginx.conf のブロック構造(main / events / http / server / location)を書ける
  • proxy_pass末尾スラッシュの有無 の挙動の違いを説明できる
  • proxy_set_header の重要4ヘッダ(Host / X-Real-IP / X-Forwarded-For / X-Forwarded-Proto)を暗記している
  • nginx -tnginx -s reload無停止反映 できる
  • limit_req でレート制限を設定できる

Day 11 / Day 13 / Day 16 とのつながり

  • Day 11 (systemd) で常駐させた Go サーバーの 前段 に立つのが Nginx
  • Day 13 (ログ管理) の access.log / error.log がここで本格運用
  • Day 16 (ユーザー管理) で学んだ「専用ユーザー(nginx)」「最小権限」がここで活きる
  • Day 5 (パイプ) の awk '$9==404' が本物の Nginx ログ集計で使える

これができると何が嬉しいか

  • Go の http.ListenAndServe を 80番直接公開」の事故設計を絶対しなくなる
  • reload で無停止反映」「Blue/Green 切替」の基礎が手に入る
  • HTTPS化(Day 19以降)・Day 21 デプロイの土台が固まる

大前提: なぜ Web の前段にほぼ必ず Nginx がいるのか

「Webアプリ = アプリケーションサーバー単体」で済む時代は終わっています。 本番構成でほぼ100%、アプリの前段にリバースプロキシ(Nginx / Envoy / HAProxy / ALB など)が立っています。理由は:

  1. アプリの開発言語に依存しない共通の機能を切り出せる: HTTPS 終端、ヘッダ書き換え、レート制限、ログ
  2. 静的ファイルはアプリで配信するな: Go/Node/Rails より Nginx の方が桁違いに速い(C で書かれた専用サーバー)
  3. ロードバランシング: アプリインスタンスを複数並べて負荷分散 + 死活監視
  4. 段階的デプロイ: アプリ再起動中もリクエストを保持(Nginx がバッファ)
  5. セキュリティ層: 怪しいリクエストをアプリに到達する前に弾く

Nginx を知らないと「Go の http.ListenAndServe で80番直接公開」みたいな本番設計をしてしまう。これは性能的にもセキュリティ的にも事故です。今日は 本番で通用する Nginx 設定 を、設計思想込みで身に着けます。


セッション①: Nginx の本質と設定構造(25-30分)

0. 録画スタートと環境準備

mkdir -p ~/log ~/learn/linux/day15
cd ~/learn/linux/day15
script ~/log/linux_day15.log
 
# Mac でローカル動作確認するなら
brew install nginx
nginx -v
# nginx version: nginx/1.27.x
 
# Linux ならパッケージから
# sudo apt install nginx
# nginx -v
 
# 設定ファイルの場所
nginx -V 2>&1 | tr ' ' '\n' | grep conf

1. Nginx が解決した問題(vs Apache)

Nginx の本質

2004年、Igor Sysoev(ロシア人エンジニア)が「C10K問題」を解決するために開発したWebサーバー。

C10K問題: 1台のサーバーで同時1万接続を処理する課題。当時の主流 Apache は接続ごとに1プロセス or 1スレッドを割り当てるため、メモリと context switch のコストで詰まった。

Nginx の解法: イベント駆動 + 非同期ノンブロッキングI/O。1つのワーカープロセスが数千〜数万の接続を1つのイベントループで捌く。

項目Apache (prefork/worker)Nginx
アーキテクチャ1接続=1プロセス/スレッド1ワーカー=多数の接続をイベントループ
同時接続スケール数百〜数千数万〜数十万
メモリ使用接続数に比例接続数にほぼ依存しない
静的ファイル配信普通圧倒的に高速
設定の柔軟性.htaccess でディレクトリ別設定可能中央設定
モジュール動的ロード可能(豊富)コンパイル時組み込みが主流

歴史: 2011年に商用版 NGINX Plus が登場、2019年にF5が買収。OSS版は今もコミュニティ運営されている。

2. Nginx の役割 3つ

静的配信 vs リバースプロキシ vs API ゲートウェイ

ざっくり言うと

Nginx の使い方には3つの典型パターンがあり、それぞれ「自分でレスポンスを返すか」「誰かに転送するか」「賢く中継するか」で別物。

パターン1: 静的ファイル配信(自前で返す)
クライアント ──→ [Nginx] ──→ ファイルシステム
                  └─ /var/www/html/index.html を読んで返す
location / {
    root /var/www/html;
}

イメージ: コンビニのおにぎり棚。在庫を直接渡すだけ。

パターン2: リバースプロキシ(裏のアプリに丸投げ)
クライアント ──→ [Nginx] ──→ [Go API on :8080]
                  └─ そのまま転送、レスポンスを返す
location /api/ {
    proxy_pass http://127.0.0.1:8080;
}

イメージ: 受付の取次。「ちょっとお待ちください」と裏に通す。

パターン3: API ゲートウェイ(賢い中継)

リバプロを発展させて、認証・レート制限・ヘッダ書き換え・ルーティングまで担う層。

クライアント ──→ [Nginx API Gateway] ──┬──→ [認証サービス]
                                     ├──→ [Go API]
                                     └──→ [Node API]
                  └─ レート制限・JWT検証・ヘッダ書換
location /api/users/ {
    limit_req zone=api burst=20 nodelay;
    auth_request /auth;
    proxy_pass http://users_service;
}
location /api/orders/ {
    proxy_pass http://orders_service;
}

イメージ: 空港のセキュリティチェック。「身分証は?」「持ち込み制限超えてない?」「行き先はあちらです」と賢く捌く。

対比表
静的配信リバースプロキシAPI ゲートウェイ
誰がレスポンスを作るNginx 自身裏のアプリ裏のアプリ
配信内容ファイルアプリの動的レスポンスアプリの動的レスポンス
認証・レート制限不要任意積極的にやる
ルーティングパスマッチ単純転送サービスごとに分岐
典型ユースケースSPA / 画像Go API への中継マイクロサービス前段
実際の1台 Nginx 構成
[クライアント]
    │ HTTPS (443)
    ▼
[Nginx]──────────────────────┐
  ├─ /static/* → ファイル        ← 静的配信
  ├─ /api/*    → Go API         ← リバプロ
  └─ /         → SPA (React)    ← 静的配信(SPA fallback)
一番覚えやすい説明
  • 静的配信 = 自分で答える
  • リバプロ = 取り次ぐ
  • API ゲートウェイ = 取り次ぎ + 警備員 + 受付

3. 設定ファイルの構造

Nginx で「どこをいじれば何が変わるか」を即座に判断するには、設定ファイルのディレクトリ構造を頭に入れるのが先決です。

  • 何のコマンドか: /etc/nginx/ 配下のディレクトリ階層(nginx.conf / conf.d/ / sites-available/ / sites-enabled/ / snippets/)の役割分担
  • いつ使うか: 新しいサイトを追加する時、設定を一時停止する時、本番で問題が出て「どのファイルを見ればいいか」を判断する時
  • 解決する具体的な問題: 「nginx.conf 1ファイル巨大化」を避け、サイトごと / 共通断片ごとにファイル分割して管理性を保つ
# Ubuntu/Debian 流(推奨)
/etc/nginx/
├── nginx.conf              # メイン設定(最低限の枠組み)
├── conf.d/                 # 追加設定(http ブロック内)
   └── *.conf
├── sites-available/        # サイト定義の保管庫
   ├── default
   ├── api.example.com
   └── www.example.com
├── sites-enabled/          # 有効なサイト(sites-availableへのsymlink)
   ├── api.example.com -> ../sites-available/api.example.com
   └── www.example.com -> ../sites-available/www.example.com
└── snippets/               # 共通設定の断片(SSL、proxy headers等)

sites-available と sites-enabled の分離

Debian/Ubuntu の伝統的なパターン。

  • sites-available/: サイト設定を書き溜める場所
  • sites-enabled/: そのうち有効化したいものへのsymlink

サイトを一時停止したい時:

sudo rm /etc/nginx/sites-enabled/api.example.com   # symlink削除(実体は残る)
sudo nginx -s reload

再開時は ln -s ../sites-available/api.example.com /etc/nginx/sites-enabled/ で復活。

CentOS/RHEL / macOS の Homebrew: conf.d/ だけのフラットな構造が主流。どちらも本質は同じで「メイン設定 + 個別ファイル群」のパターン。

nginx.conf のブロック構造

# main コンテキスト(最上位)
user www-data;
worker_processes auto;
 
events {
    # ワーカープロセスの挙動
    worker_connections 1024;
}
 
http {
    # HTTP 共通設定
    include /etc/nginx/mime.types;
    gzip on;
 
    # 個別サイト
    server {
        listen 80;
        server_name example.com;
 
        location / {
            root /var/www/html;
        }
 
        location /api/ {
            proxy_pass http://localhost:8080;
        }
    }
}

階層構造:

  • main: グローバル設定(worker_processes、user)
  • events: 接続処理(worker_connections)
  • http: HTTPサーバー全体(共通設定)
  • server: 1つの仮想ホスト
  • location: URL パスごとの処理ルール
  • upstream: バックエンドサーバー群の定義(ロードバランシング)

4. server ブロック - 仮想ホスト

1台の Nginx で複数のドメインを捌くには、server ブロックでドメインごとの仮想ホストを定義します。本番では1サーバーに5〜10サイト同居するのも普通です。

  • 何のコマンドか: server { listen ...; server_name ...; ... } で「このドメインに来たリクエストはこう処理する」を宣言する
  • いつ使うか: 新ドメインを追加する時、サブドメイン(api / www / admin)を分けて運用する時、不正な Host ヘッダを default_server で弾く時
  • 解決する具体的な問題: 1IP で複数サイト運用、Host ヘッダによる仮想ホスト切替、IP 直接アクセスや不審 Host の遮断
server {
    listen 80;
    listen [::]:80;          # IPv6
    server_name example.com www.example.com;
 
    root /var/www/example.com;
    index index.html;
 
    location / {
        try_files $uri $uri/ =404;
    }
}

server_name のマッチングルール

Nginx は HTTP リクエストの Host ヘッダで server ブロックを選ぶ。

優先順位:

  1. 完全一致: server_name example.com;
  2. 先頭ワイルドカード: *.example.com
  3. 末尾ワイルドカード: example.*
  4. 正規表現: ~^(www|api)\.example\.com$
  5. default_server: どれにもマッチしない時

default_server の指定:

server {
    listen 80 default_server;
    server_name _;
    return 444;       # 444: 接続を黙って閉じる Nginx 独自レスポンス
}

「IP直接アクセス」「不正なHostヘッダ」をデフォルトで弾く設定。本番で重要。

5. location と try_files

server ブロック内で「URL パスごとに違う処理」を分岐するのが location です。Nginx 設定の難所であり、ここの理解が浅いと 404 や予期せぬバックエンド転送が頻発します。

  • 何のコマンドか: location は URL のパス(前方一致・完全一致・正規表現)に基づくルーティング、try_files は「複数候補を順に試してフォールバック」する仕組み
  • いつ使うか: SPA のクライアントルーティングで「全部 index.html に流す」時、静的アセットを長期キャッシュさせる時、/api/ だけバックエンドに流す時
  • 解決する具体的な問題: 「URL ごとにキャッシュ戦略やバックエンドを変えたい」「ファイルが無ければ別ファイルにフォールバック」を1ブロックで宣言的に表現する
# 完全一致
location = /favicon.ico {
    log_not_found off;
}
 
# 前方一致(普通の動作)
location / {
    try_files $uri $uri/ /index.html;     # SPA用パターン
}
 
# 静的アセット(拡張子マッチ、正規表現)
location ~* \.(jpg|jpeg|png|gif|svg|webp|css|js)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}
 
# 正規表現
location ~ ^/api/(.+)$ {
    proxy_pass http://backend;
}

location のマッチング順を物語で理解する

ざっくり言うと

location ブロックを複数並べた時、Nginx は決まった優先順位で「どれが当たるか」を判定する。記述順ではない。

まず素朴な疑問
location /api/ { proxy_pass http://backend; }
location ~ \.jpg$ { expires 30d; }
location = /favicon.ico { log_not_found off; }
location ^~ /static/ { root /var/www; }
location / { root /var/www/html; }

/static/logo.jpg というリクエストが来た時、どの location が選ばれるか?

普通に上から評価すると思いがちだが、Nginx はそうしない。

Nginx の判定フロー
リクエスト URI 受け取る
   ↓
[ステップ1] = で完全一致するものはあるか?
   ├─ ある → 即決定(終了)
   └─ ない ↓

[ステップ2] ^~ または 普通の prefix で「一番長くマッチ」するものを記憶
   ↓ (まだ決定ではない、暫定キープ)

[ステップ3] その暫定マッチが ^~ ならそこで確定(終了)
   ↓

[ステップ4] ~ または ~* の正規表現を「上から順に」試す
   ├─ マッチした → そっちを採用(終了)
   └─ どれもマッチしない ↓

[ステップ5] ステップ2の暫定マッチを採用
例で追う: /static/logo.jpg の場合
[ステップ1] = で完全一致 → なし
[ステップ2] prefix で長くマッチ → /static/ (^~ 付き)
[ステップ3] ^~ なので確定 → location ^~ /static/ を使う

正規表現 ~ \.jpg$ はマッチするはずだが、^~ が付いた prefix が勝つので評価されない。

例で追う: /api/users の場合
[ステップ1] = で完全一致 → なし
[ステップ2] prefix で長くマッチ → /api/ (^~ なし)
[ステップ3] ^~ じゃないので暫定キープ
[ステップ4] 正規表現を試す → \.jpg$ にもマッチしない
[ステップ5] 暫定の /api/ を採用 → proxy_pass
優先順位の覚え方(4段階)
優先記法名前意味
1location = /exact完全一致最優先、終了
2location ^~ /prefix前方一致(正規表現スキップ)これがマッチすれば正規表現は見ない
3location ~ regex / ~* regex正規表現上から順に評価
4location /prefix普通の前方一致一番長くマッチしたものが採用

イメージ: 空港の優先搭乗順

  • = = ファーストクラス(最優先で即通過)
  • ^~ = ビジネス(正規表現の列に並ぶ必要なし)
  • ~ = エコノミー(普通に並ぶ)
  • / = エコノミー後方(誰もいなかったら案内)
一番覚えやすい説明
  • 完全一致 → 前方一致(^) → 正規表現() → 普通の前方一致
  • 順序は 「強い記法から弱い記法へ」

proxy_pass vs try_files vs return の使い分け

ざっくり言うと

location の中で「何をするか」の主要3パターン:

  • proxy_pass = 裏のサーバーに転送
  • try_files = ファイルを順に探して見つかったものを返す
  • return = その場で即レスポンス
proxy_pass: 取り次ぎ
location /api/ {
    proxy_pass http://127.0.0.1:8080;
}
クライアント → Nginx → Go アプリ → Nginx → クライアント
                     (ここで処理)

イメージ: 受付の取次

try_files: ファイルを順に試す
location / {
    try_files $uri $uri/ /index.html;
    #         ①    ②     ③
}
①リクエストされたファイルがあるか?
   ├─ あった → 返す
   └─ ない ↓
②同名ディレクトリがあるか?
   ├─ あった → index ファイルを返す
   └─ ない ↓
③/index.html を返す(SPA fallback)

イメージ: 棚を3つ順に開ける。「ない、ない、あった!」

return: その場で即レスポンス
location = /health {
    return 200 "OK\n";
}
 
location = / {
    return 301 https://www.example.com$request_uri;
}
クライアント → Nginx → クライアント
              (アプリにもファイルにも触れず即答)

イメージ: インターホン越しの即答。「はい OK」「あちらへどうぞ」。

対比表
proxy_passtry_filesreturn
何をする裏のサーバーに転送ファイルを順に探す即レスポンス
必要なものupstream / URLファイルステータスコード
典型用途API 中継SPA / 静的 / WPヘルスチェック / リダイレクト
アプリ動かす?はいいいえいいえ
ディスク I/O?なしありなし
一番速いのは圧倒的に速い
一番覚えやすい説明
  • proxy_pass = 取り次ぐ
  • try_files = 棚を順番に探す
  • return = 即答する

rootalias の罠

# root: location パスをそのまま付け足す
location /static/ {
    root /var/www;
    # → /static/foo.png は /var/www/static/foo.png を探す
}
 
# alias: location パスを置き換える
location /static/ {
    alias /var/www/assets/;
    # → /static/foo.png は /var/www/assets/foo.png を探す
}

これを混同すると404の原因になる。alias の場合は 末尾スラッシュの一致が必須。


セッション②: リバースプロキシ・LB・本番運用(25-30分)

6. リバースプロキシ - Go API への中継

Nginx の最重要ユースケースがリバースプロキシです。バックエンドアプリ(Go/Node/Rails/Python)への中継を、HTTPS 終端+ヘッダ転送+タイムアウト制御込みでこなします。

  • 何のコマンドか: upstream でバックエンド先を定義し、proxy_pass でリクエストを転送、proxy_set_header で必要なヘッダを付け加える
  • いつ使うか: Go API を 127.0.0.1:8080 に立てて Nginx 経由で公開する時、複数アプリインスタンスへの負荷分散をする時、HTTPS をフロントで終端してバックエンドに HTTP で渡す時
  • 解決する具体的な問題: 「Go の http.ListenAndServe で 80/443 直接公開」を避け、TLS・ヘッダ・タイムアウト・バッファ・ロギングといったインフラ関心事をアプリから剥がす
# /etc/nginx/sites-available/api.example.com
upstream go_backend {
    server 127.0.0.1:8080;
    keepalive 32;
}
 
server {
    listen 80;
    server_name api.example.com;
 
    location / {
        proxy_pass http://go_backend;
 
        # 必須ヘッダの転送
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;
 
        # HTTP/1.1 + keepalive
        proxy_http_version 1.1;
        proxy_set_header Connection "";
 
        # タイムアウト
        proxy_connect_timeout 5s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;
 
        # バッファ
        proxy_buffering on;
        proxy_buffers 8 16k;
    }
}

proxy_pass の本質

「来たリクエストを別サーバーに転送する」ディレクティブ。これだけで:

  1. クライアント → Nginx へのリクエストを受ける
  2. Nginx が新しい HTTP リクエストを構築してバックエンドへ送信
  3. バックエンドからのレスポンスをクライアントに返す

末尾スラッシュで挙動が変わる:

location /api/ {
    proxy_pass http://backend;       # /api/users → http://backend/api/users
}
 
location /api/ {
    proxy_pass http://backend/;      # /api/users → http://backend/users  ← パスを書き換える
}

proxy_set_header で転送する重要ヘッダ

ヘッダ役割
Host元のホスト名(バックエンドが仮想ホスト判定に使う)
X-Real-IPクライアントの実IP(バックエンドのログ・レート制限用)
X-Forwarded-Forプロキシ経由のIP履歴(, 区切りでチェーン)
X-Forwarded-Proto元のスキーム(http/https)。HTTPS終端後にバックエンドに http で渡す時、アプリ側で redirect 判定に使う
X-Forwarded-Host元のHost

これを設定しないと: バックエンド側でクライアントIPが取れない(全部Nginxのlocalhostに見える)、HTTPSかHTTPか判別できずリダイレクトループ、などの事故が起きる。

X-Forwarded-For を信用するな(重要)

X-Forwarded-For はクライアントが自由に偽装できるヘッダ。

悪意あるクライアント:

GET /admin HTTP/1.1
X-Forwarded-For: 127.0.0.1

これを受けたバックエンドが「127.0.0.1 = ローカル管理者」と判定したら認証バイパスになる。

正しい運用:

  1. Nginx が proxy_add_x_forwarded_for を使う → 既存のXFFに $remote_addr追記する形になる
  2. バックエンドは「最後尾のIP = 直前のプロキシのIP」「最後から数えて N 番目が信頼可能な実IP」と判定(プロキシ段数を知っている前提)
  3. または X-Real-IP を使う(Nginx が直前のIPだけ書く)

アプリケーション側で信頼するプロキシのIP範囲を明示する設定が必要(Go なら httputil.ReverseProxy の前段で IP リストをチェック、Rails なら config.action_dispatch.trusted_proxies)。

7. ロードバランシング

アプリインスタンスを複数台並べた瞬間、Nginx の役割に「負荷分散」が加わります。同じ upstream ブロック内で複数サーバーを並べるだけで、Nginx が自動的に振り分けてくれます。

  • 何のコマンドか: upstream ブロック内に複数 server を列挙し、least_conn / ip_hash / 重み付け(weight=)でアルゴリズムを切替える
  • いつ使うか: トラフィック増でアプリを3台に増やした時、特定インスタンスをメンテに入れる時(down フラグ)、セッションをインスタンスに固定する時(ip_hash
  • 解決する具体的な問題: 「1台では捌けない」「1台が落ちたら全部止まる」を、複数台 + 死活監視(max_fails / fail_timeout)で解決する
upstream go_backend {
    # ロードバランシング方式(デフォルト: ラウンドロビン)
    # least_conn;          # 接続数最少のサーバーへ
    # ip_hash;             # クライアントIPでハッシュ → 同じIPは同じサーバー(セッション固定)
    # hash $request_uri;   # URL でハッシュ
 
    server app1.internal:8080 weight=3;          # 3倍多くリクエストを送る
    server app2.internal:8080;
    server app3.internal:8080 backup;            # 他が全部死んだ時だけ使う
 
    # 死活検知パラメータ
    server app4.internal:8080 max_fails=3 fail_timeout=30s;
 
    keepalive 32;
}

LB方式の使い分け

方式特徴使い所
round-robin(デフォルト)順番に振るステートレスAPI
least_conn接続数が少ない方へ処理時間にばらつきが大きい場合
ip_hashクライアントIPで固定化セッションがメモリにある古いアプリ
hash $custom_varカスタムキーキャッシュサーバー振り分け

現代的ベストプラクティス: アプリはステートレスにして round-robin / least_conn を使う。セッションは Redis などの共有ストアに持つ。ip_hash でセッションを固定化する設計は今は推奨されない(インスタンス追加時に分散が崩れる)。

8. 静的ファイル配信の最適化

Nginx の 真骨頂は静的ファイル配信の速さですが、デフォルト設定だと十分な性能が出ません。gzip 圧縮・キャッシュヘッダ・アクセスログ抑制の3点を入れて初めて本領発揮します。

  • 何のコマンドか: gzip on で転送量を減らし、expires / Cache-Control でブラウザキャッシュを効かせ、access_log off で書き込みI/Oを削減する
  • いつ使うか: SPA の build 成果物を配信する時、画像 CDN を自前で立てる時、ハッシュ付きファイル名(main.a8f3.js)に長期キャッシュを当てる時
  • 解決する具体的な問題: 「Go や Node でファイル配信して CPU を食う」「キャッシュが効かず毎回フルダウンロード」を、Nginx 1台でまとめて解決する
server {
    listen 80;
    server_name www.example.com;
    root /var/www/example.com;
 
    # gzip 圧縮
    gzip on;
    gzip_vary on;                                    # Vary: Accept-Encoding ヘッダ追加
    gzip_min_length 1024;                            # 1KB以下は圧縮しない(オーバーヘッド回避)
    gzip_types text/plain text/css text/xml
               application/json application/javascript application/xml+rss
               application/atom+xml image/svg+xml;
    gzip_comp_level 6;                               # 1(速)〜9(高圧縮)、6が定番
 
    # 静的アセットは長期キャッシュ
    location ~* \.(jpg|jpeg|png|gif|webp|svg|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;                              # 静的ログは抑制(性能)
    }
 
    # CSS/JS は version 付き想定で長期キャッシュ
    location ~* \.(css|js)$ {
        expires 30d;
        add_header Cache-Control "public";
    }
 
    # HTML はキャッシュさせない(最新版を毎回取得)
    location ~* \.html$ {
        expires -1;
        add_header Cache-Control "no-cache, no-store, must-revalidate";
    }
 
    # SPA フォールバック
    location / {
        try_files $uri $uri/ /index.html;
    }
}

gzip vs brotli

  • gzip: 1992年〜、すべてのブラウザが対応、圧縮率は中庸
  • brotli: 2015年Google、gzipより15-20%高圧縮、主要ブラウザ対応

Nginx で brotli を使うには ngx_brotli モジュールが必要(標準では入っていない)。CDN/Cloudflare 経由なら勝手に brotli 化されることが多い。

本番判断: Nginx 単体なら gzip でOK。CDN を前段に置く構成なら gzip も CDN に任せて Nginx は無効化することも。

Cache-Control: immutable の意味

ファイル名にハッシュ付き(main.a8f3b9.js)の場合、そのURLは絶対に中身が変わらない。これに immutable を付けると、ブラウザは期限内は再検証もしない(304 もない)。読み込みが最速化。

ビルドツール(Webpack/Vite)でハッシュ付きファイル名が出る前提の最適化。

9. ログ設定とフォーマット

Nginx の強みの1つが詳細なアクセスログです。デフォルト形式でも有用ですが、JSON 形式 + upstream タイムを追加することで本番の障害調査が劇的に楽になります。

  • 何のコマンドか: log_format でログのフィールドと書式を定義、access_log / error_log で出力先を指定する
  • いつ使うか: 「サイトが遅い」と言われて upstream の処理時間を見たい時、ログ集約基盤(Loki / Datadog / Fluentd)に流す時、特定エンドポイントの 5xx を集計する時
  • 解決する具体的な問題: 「ログがあっても見方が分からない」「テキストパースに正規表現を書く手間」を、JSON 形式で一発解決する
# /etc/nginx/nginx.conf の http ブロック内
http {
    log_format main '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" '
                    'rt=$request_time uct="$upstream_connect_time" '
                    'uht="$upstream_header_time" urt="$upstream_response_time"';
 
    # JSON フォーマット(ログ集約に流す時はこちら推奨)
    log_format json escape=json '{'
        '"time":"$time_iso8601",'
        '"remote_addr":"$remote_addr",'
        '"x_forwarded_for":"$http_x_forwarded_for",'
        '"method":"$request_method",'
        '"uri":"$request_uri",'
        '"status":$status,'
        '"bytes_sent":$body_bytes_sent,'
        '"request_time":$request_time,'
        '"upstream_response_time":"$upstream_response_time",'
        '"user_agent":"$http_user_agent"'
    '}';
 
    access_log /var/log/nginx/access.log main;
    error_log  /var/log/nginx/error.log warn;
}

ログを読む実務感覚

  • access.log: 全リクエスト記録。1日数GBになることも → logrotate 必須
  • error.log: アクセスログでない異常(接続エラー、設定ミス、upstream タイムアウト)
  • upstream_response_time を見る: バックエンドが遅いか、Nginxの問題か切り分け
  • $request_time$upstream_response_time の差 = Nginx 自身の処理時間 + ネットワーク時間

JSON フォーマット推奨理由: Fluentd / Vector / Promtail でログ集約する時、JSON ならパース不要でそのまま流せる。Grok 正規表現でパースする時代は過ぎつつある。

10. セキュリティ設定

Nginx を世界に晒す以上、最初に入れるセキュリティ設定は決まっています。これを入れないと、攻撃面が広い状態で公開され続けることになります。

  • 何のコマンドか: server_tokens off(バージョン秘匿)、各種 add_header(HSTS / CSP / X-Frame-Options など)、ssl_protocols で TLS バージョン制限、limit_req でレート制限
  • いつ使うか: 本番公開直前のチェック、セキュリティ診断対応、ログインAPIに厳しめのレート制限を入れる時、HSTS preload に登録する時
  • 解決する具体的な問題: 「Nginx バージョン丸見え」「クリックジャッキング」「ブルートフォース攻撃」「巨大ボディ送信による DoS」を、設定数行で同時に塞ぐ
server {
    listen 443 ssl http2;
    server_name api.example.com;
 
    # サーバー情報を隠す
    server_tokens off;            # nginx version を Server ヘッダから消す(http コンテキストでも可)
 
    # セキュリティヘッダ
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header Content-Security-Policy "default-src 'self'" always;
 
    # SSL 設定(Mozilla Intermediate 推奨)
    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
 
    # HSTSにより HTTP は 301 で HTTPS に転送
    # (別 server ブロックで listen 80 を作って return 301 https://$host$request_uri)
 
    # レート制限
    limit_req zone=api_limit burst=20 nodelay;
 
    # body サイズ制限
    client_max_body_size 10M;
}
 
# レート制限ゾーン定義(http コンテキスト)
# limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

レート制限 limit_req

# http コンテキストでゾーン定義
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
#               IPで識別          ゾーン名 10MB  毎秒10req
 
# server / location で適用
limit_req zone=api_limit burst=20 nodelay;
#                        バースト20  待たせない
  • rate=10r/s: 1秒あたり10リクエストを通す
  • burst=20: 一時的に20まで貯められる(瞬間的なバーストを許容)
  • nodelay: バーストを即時通す(無いと順次解放で遅延)

攻撃緩和とアプリ保護の両方に効く。ログインAPIには厳しめ、一般APIは緩めに設定するのが定石。

アンチパターン: 全部 location / で受ける

# NG
server {
    listen 80;
    location / {
        proxy_pass http://backend;
    }
}

静的ファイルもAPIも全部バックエンドに流す設計。バックエンドのCPU/メモリが浪費される

# OK
server {
    listen 80;
    location /static/ {
        alias /var/www/static/;
        expires 30d;
    }
    location /api/ {
        proxy_pass http://backend;
    }
    location / {
        root /var/www/html;
        try_files $uri /index.html;
    }
}

アンチパターン: server_tokens を有効のまま本番運用

デフォルトだと:

Server: nginx/1.24.0

がレスポンスヘッダに付く。Nginxのバージョンが分かる = 既知の脆弱性を攻撃者が当てやすい

# nginx.conf の http ブロックで
server_tokens off;

これで Server: nginx だけになる(完全に消すには more_clear_headers モジュールが必要)。

アンチパターン: デフォルト設定のまま本番

/etc/nginx/sites-enabled/default を放置して本番に出すと:

  • default_server で何も返さない
  • 不正Hostでアクセスされた時、適当なバックエンドに繋がってしまう

正解: default_server を明示的に作って return 444;(接続を黙って閉じる)か return 421; を返す。

11. デバッグと運用

Nginx 設定は1文字のタイポでサービス停止に直結します。「設定変更 → reload」のフローを体に染み込ませることが、事故防止の最大の対策です。

  • 何のコマンドか: nginx -t で構文チェック、nginx -s reload でゼロダウンタイム反映、nginx -Tinclude 展開後の全設定を確認
  • いつ使うか: 設定編集後の必須チェック、本番でのリロード、設定が複雑になった時の全展開確認、tail -f でリアルタイムログ追跡
  • 解決する具体的な問題: 「設定ミスで Nginx が起動しなくなる」「再起動でユーザー接続が切れる」をそれぞれ -treload で回避する
# 構文チェック(必須)
sudo nginx -t
 
# 設定リロード(既存接続は維持される)
sudo nginx -s reload
sudo systemctl reload nginx
 
# プロセス確認
ps aux | grep nginx
# master + worker x N の構成が見える
 
# リアルタイムでアクセスログ追跡
sudo tail -f /var/log/nginx/access.log
 
# エラーログ
sudo tail -f /var/log/nginx/error.log
 
# 404 を集計
sudo awk '$9 == 404 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
 
# 設定の完全ダンプ(include 展開後)
sudo nginx -T

nginx -s reload の挙動

SIGHUP を master プロセスに送る。

  1. master が新しい設定を読む
  2. 新しい worker プロセスを起動
  3. 古い worker は 既存接続を処理し終わったら終了

ゼロダウンタイムで設定を反映できる。本番中でも問題なくreload可能(ただし nginx -t で構文チェック必須)。


実例: Go API + 静的アセット(フル設定)

# /etc/nginx/sites-available/app.example.com
upstream go_app {
    server 127.0.0.1:8080;
    keepalive 32;
}
 
# レート制限ゾーン
# (これは http コンテキストに置くので /etc/nginx/conf.d/ratelimit.conf に分割)
# limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
# limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
 
# HTTP は HTTPS にリダイレクト
server {
    listen 80;
    server_name app.example.com;
    return 301 https://$host$request_uri;
}
 
# 本体(HTTPS)
server {
    listen 443 ssl http2;
    server_name app.example.com;
 
    server_tokens off;
    client_max_body_size 10M;
 
    # SSL
    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
 
    # セキュリティヘッダ
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
 
    # ログ
    access_log /var/log/nginx/app.example.com.access.log json;
    error_log  /var/log/nginx/app.example.com.error.log warn;
 
    # 静的アセット(ハッシュ付きファイル名想定)
    location /static/ {
        alias /var/www/app/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
        gzip_static on;          # 事前圧縮済み .gz を優先配信
    }
 
    # ヘルスチェック(ALB用などにバックエンドを介さず即200)
    location = /health {
        access_log off;
        return 200 "OK\n";
        add_header Content-Type text/plain;
    }
 
    # ログインだけ厳しいレート制限
    location = /api/login {
        limit_req zone=login burst=3 nodelay;
        proxy_pass http://go_app;
        include /etc/nginx/snippets/proxy_headers.conf;
    }
 
    # 通常 API
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        proxy_pass http://go_app;
        include /etc/nginx/snippets/proxy_headers.conf;
    }
 
    # SPA フォールバック
    location / {
        root /var/www/app/dist;
        try_files $uri $uri/ /index.html;
    }
}
# /etc/nginx/snippets/proxy_headers.conf
proxy_set_header Host              $host;
proxy_set_header X-Real-IP         $remote_addr;
proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host  $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 5s;
proxy_send_timeout    60s;
proxy_read_timeout    60s;

練習課題

# 1. ローカルで Go の超軽量 HTTPサーバーを起動(仮想バックエンド)
# (Go = Google製のコンパイル言語。詳しくは「Go」章で扱う。
#  ここでは「Nginx の裏に置く適当な HTTP サーバー」として使うだけ。
#  Go が未インストールなら https://go.dev/dl/ から入手。
#  代わりに python3 -m http.server 8080 などでも可)
cd ~/learn/linux/day15
cat > server.go <<'EOF'
package main
 
import (
    "fmt"
    "log"
    "net/http"
)
 
func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello from Go! Path=%s X-Real-IP=%s X-Forwarded-For=%s\n",
            r.URL.Path, r.Header.Get("X-Real-IP"), r.Header.Get("X-Forwarded-For"))
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}
EOF
go run server.go &
 
# 2. Nginx 設定を書く
mkdir -p static
echo "<h1>Static OK</h1>" > static/index.html
 
cat > nginx_local.conf <<'EOF'
worker_processes 1;
events { worker_connections 1024; }
 
http {
    server {
        listen 8090;
        server_name localhost;
 
        location /static/ {
            alias /Users/takato/learn/linux/day15/static/;
        }
 
        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}
EOF
 
# 3. nginx を起動(フォアグラウンドで挙動確認)
nginx -t -c $(pwd)/nginx_local.conf
nginx -c $(pwd)/nginx_local.conf
 
# 4. テスト
curl http://localhost:8090/
curl http://localhost:8090/static/
 
# 5. ログ確認
tail -f $(brew --prefix)/var/log/nginx/access.log

締め: git で証跡を残す

exit  # script から抜ける
 
cd ~/learn/linux/day15
git init
git add nginx_local.conf server.go
git commit -m "feat(linux): Nginx リバプロ + 静的配信のローカル検証"

チェックリスト

  • Nginx と Apache のアーキテクチャの違いを1分で説明できる
  • nginx.conf のブロック構造(main/events/http/server/location)を書ける
  • proxy_pass で末尾スラッシュの有無の挙動の違いを説明できる
  • rootalias の違いを実例で書ける
  • X-Forwarded-For を盲目的に信用してはいけない理由を説明できる
  • gzip 設定で gzip_types に含めるべきMIME型を3つ即答できる
  • nginx -tnginx -s reload のフローを身に着けた
  • server_tokens off の理由を説明できる
  • limit_req の rate/burst の使い分けを書ける

詰まった時のチートシート

やりたいことコマンド/設定
構文チェックsudo nginx -t
設定再読み込みsudo nginx -s reload
完全再起動sudo systemctl restart nginx
設定全ダンプsudo nginx -T
バージョン確認nginx -V
サイト有効化ln -s /etc/nginx/sites-available/X /etc/nginx/sites-enabled/
サイト無効化rm /etc/nginx/sites-enabled/X
ログ追跡sudo tail -f /var/log/nginx/error.log
404 集計awk '$9==404{print $7}' access.log | sort | uniq -c | sort -rn
upstream 設定upstream name { server ip:port; }
ヘッダ転送proxy_set_header X-Real-IP $remote_addr;

「実務OK」基準

  • Go/Node アプリへのリバプロ設定をゼロから書ける
  • 静的アセットとAPIをパスで分離した設定ができる
  • proxy_set_header の重要4ヘッダを暗記している: Host / X-Real-IP / X-Forwarded-For / X-Forwarded-Proto
  • nginx -t を絶対打つ癖がついている
  • アクセスログから怪しいリクエストを見つけられる: 404多発、UA偽装、特定IPからの大量アクセス
  • HSTS / CSP / X-Frame-Options の意味を即答できる: セキュリティヘッダ設計
  • 「サイト遅い」と言われたら upstream_response_time を見る

さらに深掘るなら


アンチパターン / 初心者やらかし事例

NG 1: proxy_pass http://backend に末尾 / の有無で挙動が変わる罠

location /api/ {
    proxy_pass http://backend;        # → http://backend/api/users
    proxy_pass http://backend/;       # → http://backend/users(/api が消える!)
}

→ 対策: 意図的に選ぶ。末尾 / あり = location の prefix を切り捨て、なし = そのままパス連結。

NG 2: X-Forwarded-For を盲信して認証に使う

# 攻撃者が `X-Forwarded-For: 127.0.0.1` を偽装すると「社内アクセス」扱いになる

→ 対策: set_real_ip_from <trusted_proxy>信用できる送信元IPを限定。アプリ側でも追加検証。

NG 3: reload の代わりに restart で瞬断発生

sudo systemctl restart nginx   # 既存接続が切れる

→ 対策: 設定変更は sudo nginx -t && sudo nginx -s reloadreload は接続を保ったまま設定だけ再読み込み。

NG 4: 静的ファイルをアプリ経由で配信

location / { proxy_pass http://app; }   # /static/ も全部アプリへ

→ Go/Node より Nginx のほうが桁違いに速い。 → 対策: location /static/ { root /var/www/; }Nginx に直接配信 させる。


自己評価チェックリスト

  • Nginx と Apache の イベント駆動 vs スレッド の違いを答えられる
  • nginx.conf のブロック構造を空で書ける
  • proxy_pass の末尾 / の有無の挙動の差を実例で言える
  • proxy_set_header の4ヘッダを覚えた
  • nginx -t を打つクセが付いた

次のレッスン: Day 18 - ファイアウォール

明日は ファイアウォール ─ Nginx の前段、ネットワークレイヤーでの防御。

iptables / nftables / ufw / fail2ban、最小許可ポリシー、クラウドの Security Group との関係まで。今日 Nginx で「アプリの前段に立つ」を学んだので、その さらに前段 に何があるべきかを学ぶ。

Day 18: ファイアウォール