サイト群・マルチIP

マルチ IP サーバーで一括サイト構築:1 サイト 1 IP の Nginx テンプレート、SSL 一括発行と検証

6 ステップ 13 分で読めます 81 回閲覧 0
目次

1 つ以上の /24 ブロックを持つサイト群(サイトクラスター)用サーバーなら、数百のウェブサイトをそれぞれ専用の IP アドレスで運用できます。コントロールパネルで数百回クリックするのは時間がかかりミスも起こりやすいですが、少数のテンプレートとスクリプトがあれば同じ作業を数分で終えられ、すべてのサイトを同一の設定に保てます。この記事では、ドメインと IP の対応の計画、サイトが別の IP で表示されてしまう問題の防止、テンプレートからの Nginx バーチャルホストの生成、acme.sh による SSL 証明書の一括発行、各ドメインが自分の IP で応答しているかの確認方法を解説します。

この記事は、すべての IP がサーバーにバインド済みであること(Linux で /24 全体をバインドする方法を参照)、各ドメインの A レコードが割り当てた IP を指していることを前提としています。数百件で実行する前に、2〜3 件のドメインでスクリプトを試してください。

ステップ1:対応表を計画

唯一の基準となるデータとして、プレーンテキストファイルを 1 つ用意します。1 行に 1 サイトで、ドメインとその IP を記述します。他のすべてはこのファイルから生成します。

mkdir -p /root/cluster
cat > /root/cluster/sites.txt <<'EOF2'
example-a.com 203.0.113.10
example-b.com 203.0.113.11
example-c.net 203.0.113.12
EOF2

# Check that every IP in the list is actually bound on the server
awk '{print $2}' /root/cluster/sites.txt | while read ip; do ip -br addr | grep -qw "$ip" || echo "NOT BOUND: $ip"; done

ステップ2:サイトが他の IP で表示されるのを防ぐ

Nginx はまず待ち受けアドレス(IP とポート)を選び、その後でホスト名を照合します。未知のホスト名のリクエストには、そのアドレスの default server(デフォルトサーバー)が応答し、明示的に指定していなければ、そのアドレスで最初に設定されたサイトが応答します。サイト群では、誰かが IP を直接開いたり、別サイトのホスト名をその IP に送ったりすると、あなたのサイトのいずれかが表示され、サイト同士の関連が露呈してしまいます。こうしたリクエストは 2 か所で遮断します。まだサイトがない IP 用のキャッチオール(下記)と、テンプレート内の IP ごとのデフォルトサーバー(ステップ3)です。

cat > /etc/nginx/conf.d/00-default.conf <<'EOF2'
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}
EOF2

# Hide the Nginx version in headers and error pages
sed -i 's/^\s*#\?\s*server_tokens.*/    server_tokens off;/' /etc/nginx/nginx.conf
grep -q 'server_tokens off' /etc/nginx/nginx.conf || sed -i '/http {/a\    server_tokens off;' /etc/nginx/nginx.conf

ステップ3:バーチャルホストのテンプレートを作成

テンプレートにはプレースホルダーを使い、スクリプトがサイトごとに置き換えます。各サイトは自分の IP でのみ待ち受け、その IP に届いた他のホスト名へのリクエストは遮断されます。

mkdir -p /etc/nginx/sites-cluster /www/sites
cat > /root/cluster/vhost-http.tpl <<'EOF2'
# Unknown hostnames on this IP: close the connection
server {
    listen __IP__:80 default_server;
    server_name _;
    return 444;
}

server {
    listen __IP__:80;
    server_name __DOMAIN__ www.__DOMAIN__;
    root /www/sites/__DOMAIN__/public;
    index index.html index.php;
    access_log /www/sites/__DOMAIN__/logs/access.log;
    error_log  /www/sites/__DOMAIN__/logs/error.log;

    location /.well-known/acme-challenge/ { allow all; }
    location / { try_files $uri $uri/ =404; }
}
EOF2

# Load the generated files from the http block
grep -q 'sites-cluster' /etc/nginx/nginx.conf || sed -i '/http {/a\    include /etc/nginx/sites-cluster/*.conf;' /etc/nginx/nginx.conf

ステップ4:スクリプトで全サイトを生成

cat > /root/cluster/deploy.sh <<'EOF2'
#!/bin/bash
set -e
while read -r DOMAIN IP; do
  [ -z "$DOMAIN" ] && continue
  mkdir -p /www/sites/$DOMAIN/public /www/sites/$DOMAIN/logs
  [ -f /www/sites/$DOMAIN/public/index.html ] || echo "<h1>$DOMAIN</h1>" > /www/sites/$DOMAIN/public/index.html
  sed -e "s/__DOMAIN__/$DOMAIN/g" -e "s/__IP__/$IP/g" /root/cluster/vhost-http.tpl > /etc/nginx/sites-cluster/$DOMAIN.conf
done < /root/cluster/sites.txt
chown -R www-data:www-data /www/sites 2>/dev/null || chown -R nginx:nginx /www/sites
nginx -t && systemctl reload nginx
EOF2
chmod +x /root/cluster/deploy.sh && /root/cluster/deploy.sh

各サイトのコンテンツは /www/sites/DOMAIN/public にアップロードします。後からサイトを追加するときは、sites.txt に行を追記してスクリプトを再実行します。既存のコンテンツは上書きされません。

ステップ5:SSL 証明書を一括発行

acme.sh をインストールし、CA に Let's Encrypt を設定します(Certbot または acme.sh による無料 SSLを参照)。以下のループは、各ドメインの証明書をウェブルート経由で発行・インストールし、サイト自身の IP 上に HTTPS 用の server ブロックを追加します。

cat > /root/cluster/ssl.sh <<'EOF2'
#!/bin/bash
mkdir -p /etc/nginx/ssl
while read -r DOMAIN IP; do
  [ -z "$DOMAIN" ] && continue
  [ -f /etc/nginx/ssl/$DOMAIN.crt ] && continue          # already done
  ~/.acme.sh/acme.sh --issue -d $DOMAIN -d www.$DOMAIN -w /www/sites/$DOMAIN/public || { echo "FAILED: $DOMAIN"; continue; }
  ~/.acme.sh/acme.sh --install-cert -d $DOMAIN \
     --key-file /etc/nginx/ssl/$DOMAIN.key --fullchain-file /etc/nginx/ssl/$DOMAIN.crt \
     --reloadcmd "systemctl reload nginx"
  cat >> /etc/nginx/sites-cluster/$DOMAIN.conf <<EOF3
server {
    listen $IP:443 ssl default_server;
    ssl_reject_handshake on;      # unknown hostnames on this IP (Nginx 1.19.4+)
}
server {
    listen $IP:443 ssl;
    http2 on;
    server_name $DOMAIN www.$DOMAIN;
    ssl_certificate     /etc/nginx/ssl/$DOMAIN.crt;
    ssl_certificate_key /etc/nginx/ssl/$DOMAIN.key;
    root /www/sites/$DOMAIN/public;
    index index.html index.php;
    access_log /www/sites/$DOMAIN/logs/access.log;
}
EOF3
  sleep 5
done < /root/cluster/sites.txt
nginx -t && systemctl reload nginx
EOF2
chmod +x /root/cluster/ssl.sh && /root/cluster/ssl.sh

数十ドメインずつに分けて実行してください。Let's Encrypt では、1 つのアカウントが短時間に発行できる新規証明書の数に制限があります。DNS がまだ正しい IP を指していないドメインは失敗します。DNS を修正してスクリプトを再実行すれば、発行済みのものはスキップされます。更新は acme.sh の cron ジョブにより自動で行われます。

ステップ6:全サイトを検証

# Each domain must answer on its own IP with HTTP 200 (or 301 if you redirect to HTTPS)
while read -r DOMAIN IP; do
  code=$(curl -s -o /dev/null -w '%{http_code}' --resolve $DOMAIN:80:$IP http://$DOMAIN/)
  dns=$(dig +short $DOMAIN A | tail -1)
  echo "$DOMAIN $IP http=$code dns=$dns $( [ "$dns" = "$IP" ] && echo OK || echo DNS-MISMATCH)"
done < /root/cluster/sites.txt

# The bare IP must not show any site (connection closed = correct)
curl -sI http://203.0.113.10/ || echo "closed as expected"
curl -skI https://203.0.113.10/ || echo "handshake rejected as expected"

よくある質問

aaPanel で同じことはできますか?

はい。aaPanel ではサイト設定で各サイトを IP にバインドでき、数十サイト程度なら十分です(ウェブサイトごとに専用 IP を割り当てる方法を参照)。数百サイトになると、テンプレートとスクリプトのほうが速くミスも少なく済みます。データベースや監視には引き続き aaPanel を使えます。

送信トラフィックも各サイトの IP を使うべきですか?

サイトがコンテンツを取得したり API を呼び出したりする場合、送信リクエストは既定でサーバーのメイン IP から出ていきます。各サイトの IP を使わせるには、ウェブサイトごとに専用 IP を割り当てる方法の送信 IP のセクションを参照してください。

サイトを IP ブロックにどう分散させるべきですか?

SEO 目的のサイト群では、IP の数よりも、サイトを複数の /24 ブロック(異なる C クラス)に分散させ、テンプレート、アクセス解析のアカウント、ホスティング情報をサイトごとに分けておくことのほうが重要です。複数 C クラス IP の SEO サイト群向けサーバーを参照してください。

上記の手順で解決しない場合は、サポートチケットを送信して IMIDC の 24 時間 365 日対応テクニカルサポートへご連絡ください。サーバー IP、OS バージョン、失敗するドメインと IP、nginx -t の出力を添えていただくと、より迅速に調査できます。

この回答はお役に立ちましたか?

関連チュートリアル