はじめに

今回でこのシリーズも第四回目となりました。

UbuntuのインストールからFTP、ウェブサーバの導入にHTTPS化、静的サイトジェネレータの導入と続き、この記事ではリバースプロキシを使った静的コンテンツ専用サーバの作成を行います。

Nginxのリバースプロキシで更に静的コンテンツ専用サーバという形では、そこまで需要は無いかもしれませんが(小さなウェブサイトではサーバ費用を考えるとServiceWorkerの方が使い勝手が良いかも)、旧サイトで実験的に使っていたこともあるため、メモ代わりとして置いておきます。

これまでのシリーズは以下のとおりです。

なお、このシリーズではホスティングにVultrを使っています。

サーバのスペックおよび構成は以下の通りです。

  • $ cat /etc/lsb-release

    • DISTRIB_ID=Ubuntu
    • DISTRIB_RELEASE=20.04
    • DISTRIB_CODENAME=focal
    • DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS"
  • Vultr : High Frequency Server

    • 32GB MVMe
    • 1CPU
    • 1GB Memory
    • Additional Features
      • Virtual Private Clouds: ON

プライベートネットワークの作成

キャッシュサーバとメインサーバで接続を行うプライベートネットワークを利用するため、まずはメインサーバとは別のサーバを用意してください。

キャッシュサーバのドメインはstatic.example.comとして扱います。自身のサイトに用いるときは使うドメインに置き換え、サブドメイン(例ではstatic)のDNSも事前に設定しておくことをおすすめします。

また、リバースプロキシにはNginxを使うため、キャッシュサーバにNginxが入っていない場合は導入を行い、更にHTTPSで接続する場合はサブドメインの証明書取得も行ってください。

Vultrでプライベートネットワークを構築するには、サーバの初期設定でEnable Virtual Private Cloudsにチェックを入れてプライベートネットワークをオンにします。

既存のサーバにVPCを適用

  1. もし既に存在するサーバに適用する場合には、VultrコントロールパネルのProductsから+ボタンを選択してView More Optionsを選ぶと項目が増えるので、その中からAdd VPC Networkを選んでください。

  2. すると初期設定項目が出てくるので、Locationは使っているサーバの場所を選択し、Configure IP RangeおよびManage RoutesはRecommendになっているAuto-Assignを選択します。

  3. VPC Network Nameに関しては自由に名前を付けてください。

  4. すべての設定が終わればAdd Networkを選択することでプライベートネットワークが構築されます。

  5. ただし、このままでは既存サーバがネットワーク内にまだ入っていません。そのため、コントロールパネルのProducts 内にあるServer一覧にて接続を行いたいサーバを選択して個別サーバ設定に移動します。

  6. 個別サーバ設定に移動したらSettingsタブからIPv4の項目が選択されていることをチェックし、下部にあるVPC Networkに選択タブがあるので、先程作成したプライベートネットワークを選びます。

  7. そしてAttach VPCを選択すると確認が表示されるので、もう一度Attach VPCを選ぶことでそのサーバは再起動が行われ、その後は選択したVPC内のネットワークに入ることができます。

  8. これをメインサーバとキャッシュサーバどちらにも行ってください。

VPCの個別設定

注意してほしいのが、選択したプライベートネットワークを両サーバに設定しても、この時点では相互に通信を行うことはまだできません。どちらにも追加の設定が必要となっています。

  1. まずVultrの個別サーバ設定でSettingsIPv4VPC Networkの順に移動し、AddressMAC Addressをどちらのサーバもメモしてください。

  2. そしてその中のManageを選択し、Virtual Private CloudsSubnetにあるIPアドレス/数値数値も控えておきます(数値は同じなので一つでOK)。

  3. 次にどちらかのサーバにログインしたら$ ip aでサーバのIPを確認します。

  4. すると1:lo:2: enp1s0:3: enp6s0:といった情報が数行に渡って表示されます。

    1. enp1s0enp6s0という表示は端末によって異なります。異なる場合はそちらを使ってください。
    2. この情報のうち、loはローカルであり、次のenp1s0はパブリックネットワークとして使われ、そしてenp6s0はプライベートネットワークとして用いられます。
    3. enp6s0にあるlink/etherの部分がMACアドレスです。
  5. $ sudo vim /etc/netplan/10-enp6s0.yamlを使ってnetplanの編集を行います。

    1. netplanでは00から99の数値-*.yamlを順番に読み込んでいくようになっています。

10-enp6s0.yaml

network:
  version: 2
  renderer: networkd
  ethernets:
    enp6s0:
      match:
        macaddress: ここにMAC Address
      mtu: 1450
      dhcp4: no
      addresses: [メモしたサーバのプライベートIPアドレス/数値]
  1. 記述が完了したら$ sudo netplan applyで設定を適用します。

  2. 適用後もう一度$ ip aを実行し、enp6s0の項目内にinet プライベートIPアドレス/数値が表示されていれば、正しく設定ができています。

  3. 上記設定を両サーバ共に行います。

  4. どちらも設定が完了したら$ ping 相手サーバのプライベートIPアドレスを行い、どちらのサーバでも0% packetlossが表示されていれば問題なく通信が行えています。

  5. これにより、プライベートネットワークの構築が完了しました。

Nginxリバースプロキシの設定

次はNginxリバースプロキシの設定を行います。

基本的にほとんどキャッシュサーバ側の設定のみです。

  1. まずは$ sudo vim /etc/nginx/nginx.confでnginxのコア設定を行います。

nginx.conf

## 省略 ##

    # Proxy Cache Settings

    # プロキシキャッシュのパスを指定し、階層レベルを1:2
    # キャッシュゾーン名をst_cache、そのゾーンのメモリ使用を256MB、全キャッシュの最大量を10GB
    # アクセスが360分無ければ該当キャッシュを削除
    proxy_cache_path /var/cache/nginx/static levels=1:2 keys_zone=st_cache:256m max_size=10g inactive=360m;

    # キャッシュ時に使用するキーを設定
    proxy_cache_key     "$scheme://$host$request_uri";

    # プロキシサーバのリクエストヘッダに付与するフィールドを設定
    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;

    # キャッシュを効かせるために特定のリクエストヘッダを無効にする
    proxy_ignore_headers X-Accel-Redirect X-Accel-Expires Cache-Control Expires Set-Cookie;
    #/ Proxy Cache settings

    # Openfilecache

    # キャッシュサーバなのでopen_file_cacheを通常より多めに設定する
    open_file_cache max=100000 inactive=20s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;
    #/ Openfilecache

    # 各種設定読み込み
    include /etc/nginx/conf.d/*.conf;
  1. 次に$ sudo vim /etc/nginx/conf.d/ドメイン名.confでサーバ個別設定を行います。

ドメイン名.conf

# IP直打ちのアクセスを排除
server {
    listen   80 default_server;
    listen [::]:80 default_server; # ipv6接続設定

    # HTTPSを使わない場合はコメントアウト
    listen   443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server; # ipv6接続設定

    # 無意味なサーバネームを指定することで未定義のHostをフィルタリングする
    server_name    _;

    # ステータスコード444はnginxがレスポンスを返さずに接続を閉じる
    return    444;
}

# HTTPSを使わないときはコメントアウト
# HTTPでアクセスしてきたときにHTTPSにリダイレクトする
server {
    listen   80;
    listen [::]:80;

    server_name static.example.com;

    return 301 https://static.example.com$request_uri;
}

# リダイレクトを流される側(本体)の設定
server {
    # 接続するポート番号を指定
    listen    443 ssl http2;
    listen [::]:443 ssl http2;

    # URLに表示されるドメインまたはIPアドレス
    server_name    static.example.com;

    # ウェブサイト表示時に参照されるディレクトリ
    root /var/www/example.com/;

    # indexページとして読み込むファイル(indexは各locationにも継承される)
    index   index.php index.html index.htm;

    # サブドメイン直下に直接アクセスしてきたら本体ドメインへ流す
    location / {
        return 301 http://example.com$request_uri;
    }

    # 404ページの指定
    #error_page 404 /404.html;
    #    location = /404.html {
    #}

    # 50*ページの指定
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        # 50x.htmlを参照するルートを指定
        root   /usr/share/nginx/html;
    }

    # 指定したファイルのみリバースプロキシを行う
    location ~ \.(png|jpg|jpeg|gif|ico|css|js|webp|avif)$ {

        # プロキシ機能のONOFF
        proxy_buffering on;

        # アクセス時にURLを上書きしない
        proxy_redirect off;

        # レスポンスを読み取るために使用されるバッファの数とサイズ
        proxy_buffers 4 32k;

        # 受信したレスポンスの最初の部分を読み取るために使用されるバッファのサイズ
        proxy_buffer_size 32k;

        # プロキシキャッシュに使用するゾーン名
        proxy_cache st_cache;

        # レスポンスパターンごとのキャッシュ時間(例では180分、5分、30分)
        proxy_cache_valid 200 180m;
        proxy_cache_valid 404 5m;
        proxy_cache_valid any 30m;

        # プロキシサーバとの接続を確立する際のタイムアウト時間
        proxy_connect_timeout 10;

        # プロキシサーバにリクエストを送信する際のタイムアウト時間
        proxy_send_timeout 10;

        # プロキシサーバからのレスポンスを読み取る際のタイムアウト時間
        proxy_read_timeout 90;

        # 指定したパラメータが発生した際に古いキャッシュを使用
        proxy_cache_use_stale timeout invalid_header updating http_500 http_502 http_503 http_504;

        # 同一のリクエストが同時間に複数あった場合、リクエストを一纏めにする
        proxy_cache_lock on;

        # proxy_cache_lockのタイムアウト時間
        proxy_cache_lock_timeout 5s;

        # キャッシュサーバが受け付けるURLを指定
        proxy_pass https://メインサーバのプライベートIPアドレス;

        }

        # 画像ファイルはユーザーにキャッシュさせる(30日間)
        location ~ .*\.(jpe?g|gif|png|ico|webp|avif)\ {
                access_log off;
                expires 30d;
        }

        # CSS JSファイルはユーザーにキャッシュさせる(7日間)
        location ~ .*\.(css|js)\ {
                access_log off;
                expires 7d;
        }

}
  1. 設定が完了したら$ sudo service nginx restartでNginxの再起動を行います。

  2. これにより、リバースプロキシの設定ができましたので、キャッシュサーバとして稼働することが可能になりました。

  3. ただし、IP直打ち排除やContentSecurityPolicy、Cross-Origin-Resource-Policyなどのセキュリティヘッダを使っている場合にはエラーが発生する可能性があります。対策方法については後述します。

ファイルURLをキャッシュサーバに置き換える

キャッシュサーバの設定は完了しましたが、メインサーバ側でURLの変更を行う必要があります。

例えばhttps://example.com/main.cssというCSSファイルがある場合、これをhttps://static.example.com/main.cssのように、キャッシュサーバのサブドメインに置き換えなければなりません。

動的にページを作るウェブサイトであれば置き換えは比較的簡単ですが、静的コンテンツの場合は手動あるいはツール(Meryなど)を使って変換する必要があります。

このシリーズでも紹介したZolaのget_urlを使っている場合には{{ get_url(path='main.css', cachebust=true) | replace(from="https://", to="https://static.") }}というようにreplaceを使うことで置き換えを行うことができます。

他にもWordPressの場合、function.phpに以下を記入することで自動的にコンテンツ内の要素がキャッシュサーバに置き換わります。

function.php

// 省略 //

// WordPressで利用する画像のURLをフックで変更する
// https://worklog.be/archives/3217

// キャッシュサーバ url切り替え
function url_replaces( $content ) {
 
    $url1 = '/example.com\/wp-content\/uploads/';
    $url2 = 'static.example.com/wp-content/uploads';
 
    $content = preg_replace( $url1, $url2, $content );
 
    return $content;
 
}
add_filter( 'post_thumbnail_html', 'url_replaces' ); //アイキャッチ画像のフィルターフック
add_filter( 'the_content', 'url_replaces' ); //コンテンツデータのフィルターフック
add_filter( 'widget_text', 'url_replaces' ); //テキストウィジェットのフィルターフック

ただし、上記の方法でも動的に作られない静的ファイルは手動で変換する必要があります。

エラー発生時

リバースプロキシを作成する際、Nginxにupstream prematurely closed connection while reading response header from upstreamというエラーが発生する場合があります。

今回の作成時にいくつかのエラーに遭遇したため、それら原因の対処方法を一部置いておきます。

メインサーバがIP直打ちを排除している

メインサーバ側がserver_name _;でのIP直打ちの排除を行っている場合はエラーが発生するため、変更を行う必要があります(キャッシュサーバがproxy_passで直接IPを指定して繋いでいるため)。

  1. $ sudo vim /etc/nginx/conf.d/ドメイン名.confで『メインサーバ』の編集を行います。

ドメイン名.conf

# server_name _;を使うserverディレクションをコメントアウトする
# IP直打ち等のアクセスを排除
#server {
#    listen   80 default_server;
#    listen [::]:80 default_server; # ipv6接続設定
#
#    listen   443 ssl http2 default_server;
#    listen [::]:443 ssl http2 default_server; # ipv6接続設定
#
#    # 無意味なサーバネームを指定することで未定義のHostをフィルタリングする
#    server_name    _;
#    # ステータスコード444はnginxがレスポンスを返さずに接続を閉じる
#    return    444;
#}

# 代わりにこちらを使う
# IP直打ちでのアクセスを排除
server {
    listen   80;
    listen [::]:80; # ipv6接続設定

    listen   443 ssl http2;
    listen [::]:443 ssl http2; # ipv6接続設定

    # サーバのIPを指定する
    server_name メインサーバのパブリックIPアドレス;

    # ステータス444を返す
    return 444;
}

# コメントアウトによりdefault_serverがどこにも設定されていないため、
# 本体となるserverディレクションにdefault_serverを付与しておくことを推奨
  1. $ sudo service nginx restartでNginxを再起動します。

  2. これでIP直打ちに関連するエラーは発生しなくなりました。

CSPを設定している

ContentSecurityPolicyを使っている場合、キャッシュサーバからコンテンツを読み込む際にimg-srcscript-srcstyle-srcなどが原因でコンテンツが表示できなくなる場合があります。

これはCSPの設定によって別ドメインからのコンテンツを排除していることが原因です。

例えばimg-srcの場合、同じURLスキームとホスト、ポート番号およびキャッシュサーバを許可するには、img-src 'self' static.example.com;を指定することでコンテンツを読み込むことが可能となります。

script-srcに関してはstrict-dynamicnonceを用いることでより安全な設定を行えますが、この方法では毎回動的にページを生成する必要があるため、キャッシュを使いたい場合や動的に生成されない静的ファイルで管理している場合には使うことができません。

CORPやCOEPを設定している

Cross-Origin-Resource-PolicyCross-Origin-Embedder-Policyというセキュリティヘッダを使用している場合には、異なるオリジンを跨いでコンテンツを取得することはできません。

この場合にはAccess-Control-Allow-Originを『キャッシュサーバ』に付与し、メインサーバではキャッシュサーバに取得されるコンテンツにcrossorigin="anonymous"を付与する必要があります。

  1. シリーズでの設定方法に基づき、まずキャッシュサーバ側で$ sudo vim /etc/nginx/conf.d/ドメイン名.sh.confでヘッダの追加を行います。
    1. 複数サイトの管理を行っている場合はドメイン名.confで直接記述しても構いません。

ドメイン名.sh.conf

## 省略 ##

# 自サイトのコンテンツを外部で使用させない(CORP)
add_header Cross-Origin-Resource-Policy "cross-origin" always;

# 許可するオリジンを指定
# Access-Control-Allow-Origin "*" はセキュリティ上良くないので記述しない
add_header Access-Control-Allow-Origin "https://メインサーバのドメイン" always;

# 埋め込むリソースに対しCORPを強制的に明示し許可していないリソースを読み込ませない(COEP)
add_header Cross-Origin-Embedder-Policy "require-corp" always;

## 省略 ##
  1. 設定が終われば$ sudo service nginx restartでNginxを再起動します。

  2. 次にメインサーバ側の取得されるコンテンツにcrossorigin="anonymous"を付与します。

    1. 画像であれば<img src="static.example.com/test.jpg" crossorigin="anonymous">のようにします。
    2. CSSの場合は<link rel="stylesheet" href="static.example.com/main.css" type="text/css" media="all" crossorigin="anonymous">というように記述します。
  3. これによりキャッシュサーバはメインサーバのコンテンツを取得でき、メインサーバはキャッシュサーバのコンテンツを表示することができます。

スペックが足りない

キャッシュサーバを使う時、設定した項目に対してスペック(特にメモリ)が足りない場合があります。

$ free -mで現在のメモリ使用率を調べることができますが、この時freeがほとんど無い場合はメモリ不足が原因でNginxがクラッシュする可能性があります。

メモリ不足であることが判明した場合には素直にマシンのスペックを上げるか、Nginxで割り当てした各項目の数値を下げる、もしくはメモリを多く食っているプロセスの停止や削除を行ってください。

リバースプロキシに割り当てされていない

今回の設定では、画像(jpg,gif,png,ico,webp,avif)とCSSとJSファイルに限定してキャッシュサーバはリバースプロキシを行っています。

そのため、上記に記載されていない拡張子をキャッシュサーバに要求した場合には、正しく取得できないのでエラーが発生します。

もし他の拡張子を追加する場合には、Nginxの設定でその拡張子を含めるようにしてください。

次回はWordPressとMariaDBの導入および設定

今回の設定により、リバースプロキシによって静的ファイルを配信するキャッシュサーバの構築がなるべく簡単に行えます。

最初に指摘したように小さなウェブサイトではあまり需要は無いとは思われますが、それでもウェブサーバの応答速度向上やPageInsightsなどのスコア向上に少しでも役立てるかもしれません。

今回のようにサーバを分けてプライベートネットワークで接続する方法を使うと、例えばWordPressであればウェブサーバとデータベースサーバに分けて管理することも可能です(詳細は次回)。

ということで次回はWordPressおよびMariaDBの導入と設定について紹介します。

WordPressもMariaDBも当サイトでは既に使っていませんが、旧サイトでは使用していたため、こちらもメモ代わりとして使いつつ解説していきます。

参考資料