ウェブサイトのデプロイメント:バックエンド連携とドメインバインディング

本稿では、前回の記事で静的サイトをクラウドサーバーにデプロイする方法を紹介しましたが、今回はより実践的なウェブサイト構築に焦点を当てます。具体的には、バックエンドサービスとの連携を実現し、ドメインをバインドしてユーザーがアクセスできるようにする方法を解説します。

ソースコード:https://github.com/baburwang/web-deploy-demo-2.git

プロジェクトの準備

デモンストレーションのため、バックエンドサービスとフロントエンドアプリケーションの2つのプロジェクトを準備します。

バックエンドプロジェクト

まず、バックエンドプロジェクトを作成し、ウェブサイトのバックエンドサービスとして機能させます。このサービスはフロントエンドからのリクエストを受け取り、処理します。

mkdir backend-service
cd backend-service

npm init -y
npm install express

vim server.js

server.jsファイルの内容はシンプルで、Expressフレームワークを使用してWebサービスを起動し、/authエンドポイントでHTTPリクエストを処理します。

const express = require('express');

const app = express();

app.use(express.json());

app.post('/auth', (req, res) => {
  const { username = '', password = '' } = req.body;
  if (username !== 'user123') {
    res.send({
      status: {
        code: 1,
        message: `${username} は存在しません`
      }
    });
    return;
  }

  if (password !== 'password123') {
    res.send({
      status: {
        code: 2,
        message: `パスワードが間違っています`
      }
    });
    return;
  }

  res.send({
    status: {
      code: 0,
      message: '',
    },
    result: {
      id: 101,
      username: 'user123',
    }
  });
});

app.listen(8081, () => {
  console.log('バックエンドサービスが http://localhost:8081 で実行中');
});
nohup node server.js &

バックエンドプロジェクトファイルをクラウドサーバーにアップロードし、サービスを起動します。その後、Postmanなどのツールを使用してインターフェースをテストし、正常に動作することを確認します。

フロントエンドプロジェクト

次にフロントエンドプロジェクトを準備します。フロントエンドプロジェクトを初期化した後、ユーザーログインコンポーネントを作成します。

npm create vue@latest 
<template>
  <div class="login-container">
    <div class="login-header">
      <h1>ログイン</h1>
    </div>
    <div class="login-body">
      <form @submit.prevent="handleLogin">
        <div class="form-group">
          <input v-model="credentials.username" type="text" placeholder="ユーザー名を入力" name="username" required>
        </div>
        <div class="form-group">
          <input v-model="credentials.password" type="password" placeholder="パスワードを入力" name="password" required>
        </div>
      </form>
    </div>
    <div class="login-footer">
      <button @click="handleLogin">ログイン</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const credentials = ref({
  username: '',
  password: ''
});

const handleLogin = () => {
  console.log('ユーザー名:', credentials.value.username);
  console.log('パスワード:', credentials.value.password);

  const xhr = new XMLHttpRequest();
  xhr.open('POST', 'http://203.0.113.45:8081/auth', true);
  xhr.responseType = 'json';

  xhr.setRequestHeader('Content-Type', 'application/json');

  xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
      console.log('応答:', xhr.response);
    } else {
      console.error('リクエストが失敗しました。ステータス:', xhr.status);
    }
  };

  xhr.onerror = function() {
    console.error('ネットワークエラー');
  };

  xhr.send(JSON.stringify({
    username: credentials.value.username,
    password: credentials.value.password,
  }));
};
</script>

<style scoped>
.login-container {
  text-align: center;
  width: 320px;
  height: 320px;
  border-radius: 12px;
  box-shadow: 0 0.5vw 1.5vw 0.4vw rgba(0,0,0,0.05), 0 0.33vw 0.83vw rgba(0,0,0,0.08), 0 0.17vw 0.33vw -0.21vw rgba(0,0,0,0.12);
  padding: 25px 0;

  .login-header {
    h1 {
      color: #2a2a2a !important;
      font-size: 1.5vw;
      font-weight: 500;
      line-height: 2vw;
      text-align: center;
    }
  }

  .login-body {
    margin: 25px 25px;

    input {
      height: 52px;
      width: 100%;
      background-color: #f0f0f0;
      border: 1px solid transparent;
      border-radius: 12px;
      padding: 0 22px;
    }

    .form-group + .form-group {
      margin-top: 12px;
    }
  }

  .login-footer {
    button {
      font-size: 1.1vw;
      font-weight: 500;
      height: 2.8vw;
      margin: 0 auto;
      width: 15vw;
      box-shadow: none;
      border-radius: 0.65vw;
      padding-left: 1.1vw;
      padding-right: 1.1vw;
      text-shadow: none;
      background: #2a2a2a;
      border-color: #2a2a2a;
      color: #fff;
      border: 1px solid #e0e0e0;
    }
  }
}
</style>

開発環境

まずローカル開発環境でログイン機能をテストします。ログインボタンをクリックすると、ブラウザコンソールに「Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.」というエラーが表示されます。これは明らかなクロスオリジン問題です。フロントエンドプロジェクトのアドレスはhttp://localhost:5173ですが、インターフェースリクエストアドレスはhttp://203.0.113.45:8081であり、ドメインとポートが異なるため、ブラウザの同一オリジンポリシー制限がトリガーされています。

重要な点は、クロスオリジンはブラウザの動作であり、サーバー側が正常に応答を返したとしても、ブラウザはセキュリティ上の理由からウェブページがその結果を読み取るのを阻止します。

開発プロセス中にインターフェースアクセスのクロスオリジン問題を解決するために、通常は開発サーバー(devServer)のプロキシ機能を利用します。devServerを構成することで、フロントエンドリクエストをバックエンドサーバーにプロキシし、ブラウザの同一オリジンポリシー制限を回避できます。これにより、開発段階でバックエンドインターフェースに正常にアクセスし、デバッグを行うことができます。

  1. まず、フロントエンドコード内のXHRリクエストアドレスを変更します。バックエンドサーバーのアドレスhttp://203.0.113.45:8081/authに直接アクセスするのではなく、/api/authに置き換える必要があります。これにより、devServerによってインターセプトされるようになります。
const handleLogin = () => {
  ...
  xhr.open('POST', '/api/auth', true);
  ...
};
  1. devServer設定で/apiで始まるURLリクエストをプロキシします:
export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://203.0.113.45:8081',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      }
    }
  }
})

なぜdevServerを使用するとクロスオリジン問題が解決されるのか、簡単に説明します。

  1. ログインボタンをクリックすると、ブラウザはリクエストを送信します。この時のリクエストアドレスはhttp://localhost:5173/api/authです。このリクエストアドレスのプロトコル、IP、およびポートはウェブページと一致しているため、ブラウザはクロスオリジンがないと判断します。

  2. http://localhost:5173はdevServerプロセスであるため、devServerは自分自身へのインターフェースリクエストを監視し、かつそのURLが/apiで始まる場合、devServerは実際のバックエンドサーバーにリクエストを送信し、その応答結果をフロントエンドに返します。

この方法により、開発環境でのクロスオリジンリクエスト問題を解決しました。

本番環境

本番環境でのクロスオリジンリクエスト処理は、開発環境とは異なる戦略が必要です。なぜなら、開発環境で構成したdevServerは本番環境では機能しないためです。

本番環境でのクロスオリジンリクエスト処理には、以下のいくつかの方法があります:

  1. Nginx
  2. Node
  3. Nginx + Node
  4. サーバー側のCORS設定(多くの場合、バックエンド側はCORSを設定しないため、ここでは説明を省略します)

Nginx

Nginxのみを使用する場合、Nginxはウェブページをホストするだけでなく、プロキシサーバーとしてインターフェースリクエストも処理します。この構成により、フロントエンドアプリケーションとバックエンドサービス間のインタラクションが同一オリジンのように見え、ブラウザのクロスオリジン制限を回避できます。

まずフロントエンドの静的リソースをビルドし、クラウドサーバーにアップロードします。

次に、nginxの設定ファイル/etc/nginx/nginx.confを修正し、nginxを再起動します。

server {
    listen       80 default_server;
    listen       [::]:80 default_server;
    server_name  _;
    root         /home/lighthouse/web-deploy-demo-2/frontend/dist;

    location /api/ {
        rewrite "^/api/(.*)$" /$1 break;
        proxy_pass http://203.0.113.45:8081; # バックエンドサーバーのアドレスとポート
        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;
    }

    error_page 404 /404.html;
    location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
    }

    access_log /var/log/nginx/access.log;
}
  1. /home/lighthouse/web-deploy-demo-2/frontend/distはプロジェクトビルド後のディレクトリです。

  2. location /api/はプロキシ設定で、URLが/api/で始まる場合、http://203.0.113.45:8081に転送されます。

Node

Nodeのみを使用する場合、Nodeはウェブページをホストし、プロキシサーバーとしてインターフェースリクエストも処理します。

まず、静的リソースをホストするためのwebサービスプロセスを起動します。このwebサービスプロセスは、インターフェースプロキシも行うことができます。

mkdir web-service
cd web-service

npm init -y

npm install express
npm install http-proxy-middleware

vim app.js

app.jsの内容は以下の通りで、それぞれウェブページのホストとインターフェースプロキシを実装しています。

const path = require('path');
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// 静的リソースのホスト
app.use(express.static(path.resolve(__dirname, 'dist')));

// インターフェースプロキシ
app.use('/api', createProxyMiddleware({
  target: 'http://203.0.113.45:8081',
  pathRewrite: {
    '^/api': '' // パスを書き換え、/apiプレフィックスを削除
  },
  changeOrigin: true
}));

app.listen(3000, () => {
  console.log(`サーバーが起動しました。ポート: 3000`);
});

Nginx + Node

NginxとNode.jsを組み合わせて使用することは、より推奨されるデプロイメント方式です。Nginxは非常に強力な処理性能を持ち、Nodeを通じてより柔軟な実装が可能になります。

  • Nginxはインターフェースプロキシを担当
  • Nodeは静的リソースのホストを担当

具体的な実装方案は以下の通りです。ユーザーがアクセスするエントリーポイントはNginxプロセスであり、Nginxはインターフェースプロキシだけでなく、フロントエンドリソースのアクセスリクエストをホストするサービスにプロキシします。

server {
    listen       80 default_server;
    listen       [::]:80 default_server;
    server_name  _;
    # root         /usr/share/nginx/html;
    # root         /home/lighthouse/web-deploy-demo-2/frontend/dist;

    # デフォルトサーバーブロックの設定ファイルを読み込む。
    include /etc/nginx/default.d/*.conf;

    location /api/ {
        rewrite "^/api/(.*)$" /$1 break;
        proxy_pass http://203.0.113.45:8081; # バックエンドサーバーのアドレスとポート
        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;
    }

    location / {
        proxy_pass http://203.0.113.45:3000; # フロントエンドwebサーバーのホストアドレスとポート
        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;
    }

    error_page 404 /404.html;
    location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
    }

    access_log /var/log/nginx/access.log;
}

このデモンストレーションコードはGitHubにアップロードされており、cloneして直接実行できます。

git clone https://github.com/baburwang/web-deploy-demo-2.git

cd web-deploy-demo-2
sh deploy.sh

ドメインバインディング

ドメインバインディングのプロセスは比較的簡単です。ドメインを購入した後、クラウドサーバーでドメイン解決を選択するだけで、ドメイン経由でアクセスできるようになります。

注意点として、ドメイン登録が完了していない場合、アクセス時にブロックされる可能性があります。

タグ: nginx Node.js express vue.js CORS

5月30日 08:24 投稿