node.jsでベクトルタイルサーバを作成（レンタルサーバ編）

# はじめに
[こちらの記事](https://qiita.com/k96mz/items/668bee0c6d751a444b5c)でローカルホストでのnode.jsでベクトルタイルサーバの作成を解説しました。本記事では、レンタルサーバにおいて、node.jsでベクトルタイルサーバを作成する方法を解説します。使用したコードは[こちら](https://github.com/k96mz/20240822hostingPracticeServer)にあります。

# レンタルサーバの設定
レンタルサーバについてあまり分かっておらず、始めは[ロリポップさんのライトプラン](https://lolipop.jp/low-price-plan/)に申し込みました。少し使ったあとに気づきましたが、こちらのサーバにはNode.jsがインストールできないということが判明したので、無料お試し期間内に解約しました。共用サーバー（1台の物理サーバーを複数人で借り受けて使用する形式のサーバー）はOSやミドルウェアなども共用しますので、自分の都合で変更することはできません。
レンタルサーバについて調べるうちに、「VPS(Virtual Private Server)」というものがあり、仮想的に専用サーバーを利用できるため、OSやミドルウェアを自由に選択することができるということが分かりました。共有サーバーとは違い、いわゆる「管理者権限」を付与されるため、自社のニーズに応じた自由な運用が可能です。
今回は、[KAGOYAさんのVPS](https://www.kagoya.jp/vps/)をレンタルすることにしました。

## 環境
OS: CentOS Linux 7
メモリ：　1GB
ストレージ: 25GB SSD
node.js version: v16.13.1 ※nvmで管理
npm version: 8.1.2 ※nvmで管理

## サーバへの接続
まずは、kagoyaさんのページでログイン用認証キーを取得。その後、以下のようにすると、サーバへ接続出来ます。

```
ssh -i ~/.ssh/ログイン用認証キー番号.key root@サーバIPアドレス
```


オプションの-iは、「接続に使用する公開鍵ファイルを指定する」ものです。

## サーバへのファイルコピー
サーバへのファイルコピーは以下のコマンドを実行します。

```
scp -i ~/.ssh/ログイン用認証キー番.key -r ~/コピー元フォルダ root@サーバIPアドレス:~/ 
```

# 静的フォルダからベクトルタイルを配信
ローカルホスト編とほとんど変わりませんが、コードは以下です。

```javascript:app0.js
// Set Module
const config = require('config');
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');

// Configure constant
const port = config.get('port');
const htdocsPath = config.get('htdocsPath');
const morganFormat = config.get('morganFormat');
const logDirPath = config.get('logDirPath');

// Logger configuration
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new DailyRotateFile({
      filename: `${logDirPath}/server-%DATE%.log`,
      datePattern: 'YYYY-MM-DD',
    }),
  ],
});

logger.stream = {
  write: message => {
    logger.info(message.trim());
  },
};

// Middleware
const app = express();
app.use(cors());
app.use(
  morgan(morganFormat, {
    stream: logger.stream,
  })
);
app.use(express.static(`${__dirname}/${htdocsPath}`));

app.listen(port, () => {
  console.log(`Starting server at port ${port}`);
});
```

```./config/default.hjson
{
  htdocsPath: htdocs
  port: 3000
  logDirPath: log
  morganFormat: tiny
  mbtilesDir: mbtiles
}
```
const cors = require('cors');
app.use(cors());
上記２つの行を追記しています。

これは、CORS(Cross-Origin Resource Sharing)というライブラリを読み込んでオリジン間リソース共有をするためのものです。

また、styleO-pbf.jsonとstyleO-mbtiles.jsonのtile URLを以下の通り変更します。

修正前
"tiles": ["http://localhost:3000/VT/zxy/VTpracticegzip/{z}/{x}/{y}.pbf"]

修正後
"tiles": ["http://(your server name):3000/VT/zxy/VTpracticegzip/{z}/{x}/{y}.pbf"]

レンタルサーバにログインした後に、
node app0.js
とすることで実行出来ます。
http\://サーバのIPアドレス:3000/
とするとウェブページを見ることが出来、さらにここから地図のページに飛ぶことが出来ます。



# MBTilesファイルからベクトルタイルを配信
こちらも、ローカルホスト編とほとんど変わりませんが、コードは以下です。

```javascript:app.js
// Set Module
const config = require('config');
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');

// Configure constant
const port = config.get('port');
const htdocsPath = config.get('htdocsPath');
const morganFormat = config.get('morganFormat');
const logDirPath = config.get('logDirPath');

// Logger configuration
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new DailyRotateFile({
      filename: `${logDirPath}/server-%DATE%.log`,
      datePattern: 'YYYY-MM-DD',
    }),
  ],
});

logger.stream = {
  write: message => {
    logger.info(message.trim());
  },
};

// Middleware
const app = express();
app.use(cors());
const VTRouter = require('./routes/VT');
app.use(
  morgan(morganFormat, {
    stream: logger.stream,
  })
);
app.use(express.static(`${__dirname}/${htdocsPath}`));
app.use('/VT', VTRouter);

app.listen(port, () => {
  console.log(`Starting server at port ${port}`);
});
```
こちらもCORSの記載が加わっています。VT.jsのコードも以下に示します。


 ```javascript:./routes/VT.js
// Set Module
const express = require('express');
const router = express.Router();
const config = require('config');
const fs = require('fs');
const cors = require('cors');
const MBTiles = require('@mapbox/mbtiles');

// Configure constant
const mbtilesDir = config.get('mbtilesDir');

// Global variables
let mbtilesPool = {};
let busy = false;

const app = express();
app.use(cors());

// Specify mbtiles
const getMBTiles = async (t, z, x, y) => {
  let mbtilesPath = `${mbtilesDir}/${t}.mbtiles`;
  return new Promise((resolve, reject) => {
    if (mbtilesPool[mbtilesPath]) {
      resolve(mbtilesPool[mbtilesPath].mbtiles);
    } else {
      if (fs.existsSync(mbtilesPath)) {
        new MBTiles(`${mbtilesPath}?mode=ro`, (err, mbtiles) => {
          if (err) {
            reject(new Error(`${mbtilesPath} could not open.`));
          } else {
            mbtilesPool[mbtilesPath] = {
              mbtiles: mbtiles,
              openTime: new Date(),
            };
            resolve(mbtilesPool[mbtilesPath].mbtiles);
          }
        });
      } else {
        reject(new Error(`${mbtilesPath} was not found.`));
      }
    }
  });
};

// Get tile
const getTile = async (mbtiles, z, x, y) => {
  return new Promise((resolve, reject) => {
    mbtiles.getTile(z, x, y, (err, tile, headers) => {
      if (err) {
        reject();
      } else {
        // console.log(`---------before------------`);
        // console.log({ tile: tile, headers: headers });
        resolve({ tile: tile, headers: headers });
      }
    });
  });
};

router.get('/zxy/:t/:z/:x/:y.pbf', async (req, res) => {
  busy = true;
  const t = req.params.t;
  const z = parseInt(req.params.z);
  const x = parseInt(req.params.x);
  const y = parseInt(req.params.y);

  // console.log(`t: ${t}, z: ${z}, x: ${x}, y: ${y}`);

  getMBTiles(t, z, x, y)
    .then(mbtiles => {
      // console.log(mbtiles);
      getTile(mbtiles, z, x, y)
        .then(r => {
          if (r.tile) {
            res.set('content-type', 'application/vnd.mapbox-vector-tile');
            res.set('content-encoding', 'gzip');
            res.set('last-modified', r.headers['Last-Modified']);
            res.set('etag', r.headers['ETag']);
            // console.log(`---------after------------`);
            // console.log({ tile: tile, headers: headers });
            res.send(r.tile);
            busy = false;
          } else {
            res
              .status(404)
              .send(`tile not found: /zxy/${t}/${z}/${x}/${y}.pbf`);
            busy = false;
          }
        })
        .catch(e => {
          res
            .status(404)
            .send(
              `tile not found (getTile error): /zxy/${t}/${z}/${x}/${y}.pbf`
            );
          busy = false;
        });
    })
    .catch(e => {
      res
        .status(404)
        .send(`mbtiles not found for /zxy/${t}/${z}/${x}/${y}.pbf`);
    });
});

module.exports = router;
```
こちらについても、ローカルホスト編と比べてCORSの記載が加わっています。


## sqlite3のバージョンについての注意事項
package.jsonにおいて、
"sqlite3": "5.0.2",
とするとうまくいきます。

sqlite3のバージョンを指定しないと、5.1.7となりますが、このバージョンでは
node app.js
とした段階でエラーが発生します。レンタルサーバで使用しているCent OS 7　において依存しているシステムライブラリ libstdc++　が古いようです。

また、node.jsのバージョンを20にして試してみましたが、nodeが実行出来ませんでした。chatGPTによると、Node.js バイナリが依存する GLIBC と libstdc++ のバージョンが、CentOS 7 の標準ライブラリバージョンと一致していないことが原因だそうです。node.jsのバージョンは16にしておく必要があります。

今後は、CentOS 7からバージョンアップさせる必要がありそうです。
ローカルPCで上記のことを試しましたが、sqlite3の5.0.2がうまくインストール出来ませんでした。レンタルサーバ上ではうまくいきます。

# FTP設定（断念）
今までは、sshコマンドを使用してレンタルサーバにアクセスしていましたが、FTPソフトを使用して直感的にファイル移動等を行いたいと思い、FileZillaを使用してみることにしました。私は、Mac OSの「M3」シリーズのチップを利用していますが、[こちらのサイト](https://filezilla-project.org/download.php?platform=osx#close)でダウンロードできるものは、「Download FileZilla Client for macOS (Intel)」と記載されています。しかし、chatGPTに聞いてみたところ、「FileZillaの公式サイトでは「Download FileZilla Client for macOS (Intel)」という表記がされていますが、これはApple Silicon（M1やM2）で動作するファイルも含まれています。」とのことだったので大丈夫そうなので、ダウンロードしてインストールしてみました。
その後の、FileZilla の設定は[こちらのサイト](https://support.kagoya.jp/kir/manual/filezilla_mac/filezilla_mac_1.html)の記載を参考にしながら進めようとしましたが、VPSについてはvsftpd などのFTPサーバーのインストールが必要であることが分かりました。vsftpdのインストールも試みましたが、 CentOS 7 向けに提供されていた標準、およびミラーサイトのリポジトリによるパッケージの提供が停止された影響？により出来ませんでした。ファイルコピー等はscpコマンドで実施出来、FTP接ソフトは必須ではないためFTP設定は断念しました。


# httpからhttpsにする

## ドメインの取得
調べていくうちに分かりましたが、ドメインを取得しないとLet's Encryptが利用出来ないようですので、ドメインを取得しました。個人・法人を問わず、誰でも取得できるgTLD(Generic Top Level Domain)の一つである、.netを選びました。
ドメインを取得した後は、サーバの設定画面で、「正引き設定」と「逆引き設定」を設定します。kagoyaさんのネームサーバーは、15分間隔(毎時0分, 15分, 30分, 45分)で更新をしているようなので、少し待てば反映されました。

## Let's Encryptを利用したSSLサーバー証明書の入手
Let's Encryptを利用すれば、無料で簡単にSSLサーバー証明書を入手することができます。やり方がよく分からなかったのですが、インターネットで調べながら、chatGPTに聞きながらなんとか出来たような気がします。fullchain.pemとprivkey.pemを入手しました。
また、WebページのURLの左にある項目から証明書の確認を行うことも出来ました。

## httpsのためのコード修正

https接続のために、新しくappHttps.jsというファイルを作成します。
```javascript:appHttps.js
// Set Module
const config = require("config");
const express = require("express");
const cors = require("cors");
const morgan = require("morgan");
const winston = require("winston");
const DailyRotateFile = require("winston-daily-rotate-file");
const fs = require("fs");
const spdy = require("spdy"); //for https

// Configure constant
const port = config.get("port");
const htdocsPath = config.get("htdocsPath");
const morganFormat = config.get("morganFormat");
const logDirPath = config.get("logDirPath");
const privkeyPath = config.get("privkeyPath");
const fullchainPath = config.get("fullchainPath");

// Logger configuration
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new DailyRotateFile({
      filename: `${logDirPath}/server-%DATE%.log`,
      datePattern: "YYYY-MM-DD",
    }),
  ],
});

logger.stream = {
  write: (message) => {
    logger.info(message.trim());
  },
};

// Middleware
const app = express();
app.use(cors());
const VTRouter = require("./routes/VT");
app.use(
  morgan(morganFormat, {
    stream: logger.stream,
  })
);
app.use(express.static(`${__dirname}/${htdocsPath}`));
app.use("/VT", VTRouter);

// for http
// app.listen(port, () => {
//   console.log(`Starting server at port ${port}`);
// });

// for https
spdy
  .createServer(
    {
      key: fs.readFileSync(privkeyPath),
      cert: fs.readFileSync(fullchainPath),
    },
    app
  )
  .listen(port);
```
app.jsからの変更点は以下です。
◯npmパッケージの追記
const fs = require('fs')
const spdy = require('spdy') //for https

◯https用変数の追記
const privkeyPath = config.get('privkeyPath')
const fullchainPath = config.get('fullchainPath')

◯https用の追記
// for https
spdy.createServer({
key: fs.readFileSync(privkeyPath),
cert: fs.readFileSync(fullchainPath)
}, app).listen(port)

http用のサーバ開始のためのコードはコメントアウトします。

```./config/default.hjson
{
  htdocsPath: htdocs
  port: 3000
  logDirPath: log
  morganFormat: tiny
  mbtilesDir: mbtiles
  privkeyPath: ./key/privkey.pem
  fullchainPath: ./key/cert.pem
}
```
https用にdefault.hjsonに以下を追記しました。
privkeyPath: ./key/privkey.pem
fullchainPath: ./key/cert.pem

また、Let's Encryptを利用して入手した、fullchain.pemとprivkey.pemをサーバ上のkeyレポジトリに保存します。保存する際には、fullchain.pemの名前をcert.pemに変更します。（この変更が下段で記載しているエラーを引き起こしているのかもしれません。。）


```json:package.json
{
  "name": "geojson",
  "version": "1.0.0",
  "description": "Practice hosting ",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@mapbox/mbtiles": "^0.12.1",
    "config": "^3.3.12",
    "cors": "^2.8.5",
    "express": "^4.19.2",
    "hh-mm-ss": "^1.2.0",
    "hjson": "^3.2.2",
    "morgan": "^1.10.0",
    "winston": "^3.13.1",
    "sqlite3": "5.0.2",
    "winston-daily-rotate-file": "^5.0.0",
    "spdy": "^4.0.2"
  }
}

```
package.jsonには以下の記述を追加しました。
"spdy": "^4.0.2"

また、スタイルファイルのタイル参照先URLもhttpからhttpsに変更しておきます。

## 地図表示の確認
node appHttps.js
とするとindex.html（こちらも少し修正しています）が見られるので、そこからmbtilesから配信される地図を見ることが出来ます。しかし、挙動が不安定でmaplibre-gl.jsに関して、(failed)net::ERR_HTTP2_PROTOCOL_ERRORというエラーが出るときがあり、HTTP/2プロトコルに関連する問題が発生している模様です。

※追記
node appHttps.js
として、後日再度試してみたところ、以下のようなメッセージが出て、地図を見ることが出来ませんでした。。一般的には、使用しているNode.jsのバージョンが古いモジュールやパッケージと互換性がなくなるときに表示されるようなのですが、原因不明です。
```
(node:14953) [DEP0111] DeprecationWarning: Access to process.binding('http_parser') is deprecated.
(Use `node --trace-deprecation ...` to show where the warning was created)
```

# まとめ
レンタルサーバ上に、node.jsでベクトルタイルサーバを立てました。試行錯誤で進めた部分もあり、曖昧な部分が残っていますが、解決方法等分かれば修正等していきたいと思います。

※追記
こちらに更新した[新しい記事](https://qiita.com/k96mz/items/e33f493be15e9253e2ff)を記載しました。

# Reference
https://qiita.com/T-ubu/items/545d9f995ef7496a2ec4

https://qiita.com/k96mz/items/668bee0c6d751a444b5c

https://www.nttpc.co.jp/column/network/whats_vps.html

https://filezilla-project.org/download.php?platform=osx