ネットワーク #9 - Nginx

Last Edited: 3/6/2025

このブログ記事ではnginxを用いてWebサーバーやリバースプロキシを設定する方法を紹介します。

DevOps

前回の記事では、Docker Composeを使用して水平スケーリングを簡単に実現できることを説明しましたが、 トラフィックをサーバーに分散するためのリバースプロキシを設定しなければ実現できず、 その設定についてはまだ説明していませんでした。今回の記事では、静的コンテンツの提供にも使用できる 最も人気のあるリバースプロキシの設定方法の一つであるnginxの基本について説明します。

Nginx

nginxとそのモジュールをインストールすることで、静的コンテンツの提供やトラフィックの処理を行うサーバーを設定できます。 これらの機能は設定ファイル(通常はデフォルトで/etc/nginx/nginx.confに配置)に基づいて動作します。 そのため、設定ファイルの書き方を学ぶだけで、ほぼすべての機能を実現できます。以下はHTMLや画像を提供するためのnginx.confの例です。

nginx.conf
user nginx; # プロセスを実行するユーザー
worker_processes auto; # ワーカーの数 (auto=コアの数)
 
error_log /var/log/nginx/error.log notice; # ログファイルのパス (noticeはエラーのレベル)
pid /var/run/nginx.pid; # プロセスIDファイルのパス
 
# イベントのモジュール
events {
    worker_connections: 1024; # ワーカーの接続数
    multi_accept on; # 同時に複数の接続を受け入れるか
}
 
# HTTP接続に関するモジュール
http {
    include /etc/nginx/mime.types; # Nginxのデフォルトのマイムタイプのマッピング
    default_type text/plain; # Nginxにないデフォルトのマイムタイプ
 
    # mainというログフォーマットを$remote_addrなどの変数を用いて作成
    log_format main ' [$time_local] $remote_addr - $remote_user "$request" $status'; 
 
    access_log /var/log/nginx/access.log main; # mainを用いてアクセスのログを設定
 
    keepalive_timeout 65; # TCP接続の最大接続時間
 
    server {
        listens 80; # 開くポート
        server_name example.com; # Hostセクションがサーバー名と一致するかのチェック
        root /usr/share/nginx/html; # デフォルトのルートディレクトリ
        location / {
            root /data/www; # http://localhost:80/ を ディレクトリ/data/www にマッピング
        }
        location /images/ {
            root /data; # maps http://localhost:80/images/ を ディレクトリ /data/images/ にマッピング
        }
    }
}

設定ファイルでは、キーと値のペア(ディレクティブ)にはスペースを使用し、ブロック(コンテキスト)には{}を、コメントには#を使用します。 上記の例を読み、研究することでnginxでサーバーを設定する基本を理解できます。不明な点がある場合は、 オンラインで検索することをお勧めします。また、以下のようにlocationモジュール内でproxy_passを使用して、 トラフィックを分散するリバースプロキシを設定することもできます。

nginx.conf
http {
    ...
    upstream backend {
        server 127.0.0.1:3000;
        server 127.0.0.1:3001;
        server 127.0.0.1:3002;
    }
    server {
        listens: 80
        server_name: localhost;
        location / {
            proxy_pass http://backend;
        }
    }
}

proxy_passディレクティブはバックエンドのupstreamを探し、upstreamにあるサーバーにトラフィックを分散します。 デフォルトではラウンドロビン方式を使用しますが、最小接続数や他の負荷分散アルゴリズムを使用するように設定することもできます。 また、upstreamコンテキスト内の各サーバーにweight=<数値>パラメータを追加することで、重みを割り当てることもできます。 設定ファイルが長くなる場合、以下のようにincludeディレクティブを使用して設定ファイルを分割できます。

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

上記の設定により、/etc/nginx/conf.d/ディレクトリ内の設定をhttpコンテキスト内で使用できるようになります。 したがって、serverupstreamコンテキストの設定を別々の.confファイルに記述し、/etc/nginx/conf.d/ディレクトリにコピーすることができます。 これは、設定ファイル内で環境変数を使用したい場合にも便利です。設定ファイルを/etc/nginx/templates/default.conf.templateにコピーすると、 .confファイル内で定義された環境変数が自動的に置換され、/etc/nginx/conf.d/に保存されるため、それらをincludeディレクティブで使用できます。

NginxとDocker

Docker と nginx イメージを使用することで、nginxで設定されたリバースプロキシを簡単にコンテナ化できます。 そのためには、まずnginx.confupstream.confを以下のように準備する必要があります。

# ./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 ' [$time_local] $remote_addr - $remote_user "$request" $status'; 
 
    access_log /var/log/nginx/access.log main;
 
    keepalive_timeout 65;
 
    include /etc/nginx/conf.d/*.conf;
}
 
# ./conf/upstream.conf
upstream backend {
    server ${PROJECT_NAME}-server-1:${BACKEND_PORT};
    server ${PROJECT_NAME}-server-2:${BACKEND_PORT};
    server ${PROJECT_NAME}-server-3:${BACKEND_PORT};
    server ${PROJECT_NAME}-server-4:${BACKEND_PORT};
}
 
server {
    server {
        listens: ${NGINX_PORT}
        server_name: ${NGINX_CONTAINER_NAME};
 
        root /usr/share/nginx/html;
 
        location /api {
            proxy_pass http://backend;
        }
    }
}

ここで重要なのは、upstream.conf.envファイルで定義された環境変数を使用していることです。これらの設定ファイルには、以下のようなDockerfileを使用できます。

FROM nginx:latest
 
# 静的ファイルをコピー
COPY ./public /usr/share/nginx/html
 
# nginx.confをコピー
COPY ./conf/nginx.conf /etc/nginx/nginx.conf
 
# defalt.conf.templateへコピーし、環境変数を代入したconfをconf.dディレクトリにコピー
COPY ./conf/upstream.conf /etc/nginx/templates/defalt.conf.template

次に、docker-compose.yamlファイルでリバースプロキシとサーバーの構築方法を設定できます。(前の記事と同じ方法でサーバーをセットアップします。)

docker-compose.yaml
name: ${PROJECT_NAME}
 
services:
  server:
    build: ./server
    deploy:
      replicas: 4
  reverse-proxy:
    build: ./reverse-proxy # confディレクトリの場所
    container_name: ${NGINX_CONTAINER_NAME}
    env_file: .env
    ports:
      - ${NGINX_PORT}:${NGINX_PORT}
    depends_on:
      - server

docker compose upコマンドを実行すると、4つのサーバーすべてが構築され、その後リバースプロキシが起動するのが確認できるはずです。 http://localhost:${NGINX_PORT}にアクセスすると、リバースプロキシを介していずれかのサーバーにアクセスできるはずです。 docker statsコマンドを使用すると、コンテナの統計情報を表示できるため、リバースプロキシにダミーリクエストを大量に送信した後、 サーバーコンテナがリクエストを受信していることを確認できます。docker exec -it でコンテナのシェルにアクセスして、 catでエラーログやアクセスログを確認することもできます。

HTTPSの設定

ここまでで、Dockerを使用してアプリケーションをコンテナ化し、Dockerと一緒にnginxを使用して水平スケーリング用のコンテナ化されたリバースプロキシを設定し、 Gitをアプリケーションのバージョン管理に使用し、GitHubをリモートリポジトリのホスティングとCI/CDパイプラインを使用した効果的な協力のために使用する方法を学びました。 また、Linuxベースのコンテナを設定し、ログを監視するためのいくつかのbashコマンドの使用方法も学びました。

そうして構築したアプリケーションをローカルデバイスから公開するには(リモートサーバーをレンタルしてSSH経由で設定する方法が一般的ですが)、ルーターのNAT機能を使用して、 パブリックIPアドレスとポートをリバースプロキシのコンテナのポートにマッピングされたローカルポートにマッピングするだけです。 オプションとして、ドメイン名レジストラからドメイン名を購入して、ドメイン名経由でのアクセスを可能にすることもできます。 ただし、今のままではユーザーがブラウザでドメインを入力してサービスにアクセスすると、HTTPのみの接続なので「保護されていない」という警告が表示されます。 HTTPSを設定するには、nginxの設定を以下のように編集します。

upstream.conf
server {
    listen ${NGINX_PORT} ssl;
    server_name: ${NGINX_CONTAINER_NAME};
 
    ssl_certificate /etc/nginx-certs/example.com/fullchain.pem;
    ssl_certificate_key /etc/nginx-certs/example.com/privatekey.pem;
 
    location {
        ...
    }
}

ここでは、listen ディレクティブで ssl を使用し、それぞれのディレクティブでSSL証明書と鍵へのパスを指定します。 鍵と証明書署名リクエスト(CSR)は、openssl genrsa -out example.com.key 2048openssl req -new -key example.com.key -out example.com.csr -subject "<subjects>" のようにOpenSSLを使用して生成でき、 これを信頼できる認証局に送信して有効なSSL証明書と鍵を取得できます。あるいは、Let's EncryptのCertbotのようなAPIを使用してSSL証明書と鍵を取得することもできます。 (デモンストレーション目的では、OpenSSLで自己署名証明書を生成できます。Certbotの詳細については、こちらの公式ドキュメントを参照してください。)

結論

この記事では、nginxの基本と、それをDockerと一緒に使用して水平スケーリング用のリバースプロキシを設定する方法を紹介しました。 また、これまでに取り上げたツールを組み合わせて、HTTPS設定を備えたスケーラブルでメンテナブルなウェブアプリケーションをセルフホスティング(またはリモートサーバーでホスティング)する方法も紹介しました。 ただし、開発と運用には他にも多くの問題があり、特にサービスが多くのユーザーに対応するために大規模にスケールする必要がある場合はより多くの問題が顕在化します。 したがって、このDevOpsシリーズでは、それらの問題と解決するためのツールについての議論を続けていきます。

リソース