📖

AWSで構築する3層クライアントサーバシステム

に公開

AWS で構築する 3 層クライアントサーバシステム

この指南書では、AWS の EC2 および RDS を利用して、Apache・Tomcat・MariaDB を用いた 3 層クライアントサーバシステムを構築する手順を説明します。Tomcat はセキュリティの観点からプライベートサブネットに配置し、Apache との接続方法として HTTP リバースプロキシと AJP の両方を紹介します。

システム構成

  • フロントエンド(Web サーバ):Apache(パブリックサブネット/EC2)
  • アプリケーションサーバ:Tomcat(プライベートサブネット/EC2)
  • データベースサーバ:MariaDB(RDS)

1. ネットワーク構成の準備

1-1. VPC・サブネット作成

  • VPC:CIDR 10.0.0.0/16
  • パブリックサブネット(例:10.0.1.0/24)
  • プライベートサブネット(例:10.0.2.0/24)

1-2. ルートテーブル設定

  • パブリックサブネットにはインターネットゲートウェイ(IGW)をアタッチ
  • プライベートサブネットからインターネットにアクセスするには、以下のいずれかを利用します:
    • NAT インスタンスを構築する
    • ネットワーク ACL を利用して通信制御を行う(制御ルールの強化に用いる)

NAT インスタンスを構築する場合:

  1. パブリックサブネットに Amazon Linux 2 ベースの EC2 インスタンスを作成(インスタンスタイプ:t2.micro)
  2. Elastic IP を割り当ててアタッチ
  3. セキュリティグループで HTTP/HTTPS、SSH、ICMP を許可(必要に応じて)
  4. ソース/送信先チェックを無効化(NAT 用途のため)
  5. インスタンスに以下の NAT 設定を行う:
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
  1. 永続化設定:/etc/sysctl.conf に以下を追記:
net.ipv4.ip_forward = 1
  1. ルートテーブル設定:プライベートサブネットのルートテーブルに以下を追加:
0.0.0.0/0 → NATインスタンスのインスタンスID

ネットワーク ACL を利用する場合:

以下は、プライベートサブネットに配置された EC2 インスタンスにパブリック IP を割り当て、インターネットアクセスを許可する構成におけるネットワーク ACL の設定例です(参考:SSMU3 さんの Qiita 記事)。

この方法では、NAT を使わずともインターネット接続が可能になります(ただしパブリック IP の付与が必要)。

パブリックサブネット用 NACL(Apache サーバなど)
  • インバウンドルール:

    • 100 番:0.0.0.0/0 → ALLOW(HTTPS 443)
    • 110 番:0.0.0.0/0 → ALLOW(HTTP 80)
    • 120 番:任意(例:自分の IP)→ ALLOW(SSH 22)
    • 130 番:0.0.0.0/0 → ALLOW(Ephemeral ポート 1024-65535)
    • 32767 番:すべて → DENY(デフォルト拒否)
  • アウトバウンドルール:

    • 100 番:0.0.0.0/0 → ALLOW(すべて)
    • 32767 番:すべて → DENY(デフォルト拒否)
プライベートサブネット用 NACL(Tomcat サーバなど)
  • インバウンドルール:

    • 100 番:0.0.0.0/0 → ALLOW(Ephemeral ポート 1024-65535)
    • 110 番:パブリックサブネットの CIDR → ALLOW(Tomcat ポート 8080, AJP 8009)
    • 120 番:0.0.0.0/0 → ALLOW(SSH 22)※必要な場合
    • 32767 番:すべて → DENY
  • アウトバウンドルール:

    • 100 番:0.0.0.0/0 → ALLOW(HTTPS 443)
    • 110 番:0.0.0.0/0 → ALLOW(HTTP 80)
    • 32767 番:すべて → DENY

※ Ephemeral ポートは、通常 1024-65535 の範囲を指定します。送信リクエストに対する戻りパケットを許可するために重要です。

※ NACL はステートレスのため、インバウンドとアウトバウンドの両方向で許可設定が必要です。

1-3. セキュリティグループの設計

  • Apache EC2: HTTP/HTTPS (0.0.0.0/0), SSH(必要に応じて)
  • Tomcat EC2: HTTP/AJP(Apache の SG からのみ許可)
  • NAT インスタンス: HTTP/HTTPS(アウトバウンドのみ)、プライベートサブネットからのトラフィックを許可
  • RDS: MariaDB ポート(3306)、Tomcat EC2 からのみ許可

2. EC2 インスタンス構築

2-1. Apache 用 EC2(パブリック)

  • Amazon Linux 2 または AlmaLinux 2023 を選択
  • Apache インストール:
sudo -i
sudo dnf install httpd -y
sudo systemctl enable --now httpd

🙌(余談)HTML・CSS・JS でファイルを作成して/var/www/htmlに配置してもよい!

サンプルHTML
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="style.css" />
    <title>Database Viewer</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
      crossorigin="anonymous"
    />
  </head>
  <body class="d-flex flex-column vh-100">
    <header>
      <h1 class="sticky-top text-center mb-0 py-2 header-text">
        データベーステーブル確認
      </h1>
    </header>
    <main class="container mb-auto">
      <!-- Button trigger modal -->
      <div id="button-group" class="d-flex justify-content-evenly">
        <button
          type="button"
          class="btn btn-info mt-3 px-5 py-2"
          data-bs-toggle="modal"
          data-bs-target="#addFormModal"
        >
          追加
        </button>
      </div>

      <!-- Modal -->
      <div
        class="modal fade"
        id="addFormModal"
        tabindex="-1"
        aria-labelledby="formModalLabel"
        aria-hidden="true"
      >
        <!-- レコードの追加 -->
        <div class="modal-dialog modal-lg">
          <div class="modal-content">
            <div class="modal-header">
              <h1 class="modal-title fs-5" id="addformModalLabel">
                レコードの追加
              </h1>
              <button
                type="button"
                class="btn-close"
                data-bs-dismiss="modal"
                aria-label="Close"
              ></button>
            </div>
            <div class="modal-body">
              <form id="addRecord" class="needs-validation" novalidate>
                <div class="mb-3">
                  <label for="nameInput" class="form-label"
                    >名前<span class="badge text-danger">*</span></label
                  >
                  <input
                    type="text"
                    name="name"
                    class="form-control"
                    placeholder="Taro"
                    aria-label="input-area"
                    aria-describedby="button-addon2"
                    required
                  />
                  <div class="invalid-feedback">入力必須です。</div>
                </div>
                <div class="my-3">
                  <label for="ageInput" class="form-label"
                    >年齢<span class="badge text-danger">*</span></label
                  >
                  <input
                    type="number"
                    name="age"
                    min="0"
                    step="1"
                    class="form-control"
                    placeholder="20"
                    aria-label="input-area"
                    aria-describedby="button-addon2"
                    required
                  />
                  <div class="invalid-feedback">入力必須です。</div>
                </div>
                <div class="my-3">
                  <label for="birthplaceInput" class="form-label"
                    >出身地<span class="badge text-danger">*</span></label
                  >
                  <input
                    type="text"
                    name="birthplace"
                    class="form-control"
                    placeholder="東京都"
                    aria-label="input-area"
                    aria-describedby="button-addon2"
                    required
                  />
                  <div class="invalid-feedback">入力必須です。</div>
                </div>
              </form>
            </div>
            <div class="modal-footer d-flex justify-content-center">
              <button
                type="submit"
                class="btn btn-warning px-5 py-2"
                form="addRecord"
              >
                送信
              </button>
            </div>
          </div>
        </div>
      </div>
      <!-- レコードの更新 -->
      <div
        class="modal fade"
        id="updateFormModal"
        tabindex="-1"
        aria-labelledby="formModalLabel"
        aria-hidden="true"
      >
        <div class="modal-dialog modal-lg">
          <div class="modal-content">
            <div class="modal-header">
              <h1 class="modal-title fs-5" id="updateformModalLabel">
                レコードの更新
              </h1>
              <button
                type="button"
                class="btn-close"
                data-bs-dismiss="modal"
                aria-label="Close"
              ></button>
            </div>
            <div class="modal-body">
              <form id="updateRecord" class="needs-validation" novalidate>
                <div class="mb-3">
                  <label for="idSelect" class="form-label"
                    >ID<span class="badge text-danger">*</span></label
                  >
                  <select id="selectID" name="id" class="form-select">
                    <option id="defaultSelected" selected>IDを選択</option>
                  </select>
                  <div class="invalid-feedback">入力必須です。</div>
                </div>
                <div class="mb-3">
                  <label for="nameInput" class="form-label"
                    >名前<span class="badge text-danger">*</span></label
                  >
                  <input
                    id="updateInputName"
                    type="text"
                    name="name"
                    class="form-control"
                    placeholder="Taro"
                    aria-label="input-area"
                    aria-describedby="button-addon2"
                    required
                  />
                  <div class="invalid-feedback">入力必須です。</div>
                </div>
                <div class="my-3">
                  <label for="ageInput" class="form-label"
                    >年齢<span class="badge text-danger">*</span></label
                  >
                  <input
                    id="updateInputAge"
                    type="number"
                    name="age"
                    min="0"
                    step="1"
                    class="form-control"
                    placeholder="20"
                    aria-label="input-area"
                    aria-describedby="button-addon2"
                    required
                  />
                  <div class="invalid-feedback">入力必須です。</div>
                </div>
                <div class="my-3">
                  <label for="birthplaceInput" class="form-label"
                    >出身地<span class="badge text-danger">*</span></label
                  >
                  <input
                    id="updateInputBirthplace"
                    type="text"
                    name="birthplace"
                    class="form-control"
                    placeholder="東京都"
                    aria-label="input-area"
                    aria-describedby="button-addon2"
                    required
                  />
                  <div class="invalid-feedback">入力必須です。</div>
                </div>
              </form>
            </div>
            <div class="modal-footer d-flex justify-content-evenly">
              <button
                id="putButton"
                type="submit"
                class="btn btn-success px-5 py-2"
                form="updateRecord"
                name="action"
                value="update"
              >
                送信
              </button>
              <button
                id="deleteButton"
                type="submit"
                class="btn btn-danger px-5 py-2"
                form="updateRecord"
                name="action"
                value="delete"
              >
                削除
              </button>
            </div>
          </div>
        </div>
      </div>
      <table id="membersTable" class="table table-striped my-4">
        <thead>
          <!-- <tr>
            <th class="thead-color">ID</th>
            <th class="thead-color">名前</th>
            <th class="thead-color">年齢</th>
            <th class="thead-color">出身地</th>
          </tr> -->
        </thead>
        <tbody>
          <!-- <tr>
            <th scope="row">1</th>
            <td>Mark</td>
            <td>Otto</td>
            <td>@mdo</td>
          </tr>
          <tr>
            <th scope="row">2</th>
            <td>Jacob</td>
            <td>Thornton</td>
            <td>@fat</td>
          </tr>
          <tr>
            <th scope="row">3</th>
            <td>Larry the Bird</td>
            <td>Xbox</td>
            <td>@twitter</td>
          </tr> -->
        </tbody>
      </table>
    </main>
    <footer>
      <p class="text-center mb-0 py-1 footer-text">©All right reserved.</p>
    </footer>
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
      crossorigin="anonymous"
    ></script>
    <script src="script.js"></script>
  </body>
</html>
サンプルCS
* {
  margin: 0px;
  padding: 0px;
  box-sizing: border-box;
}

.header-text {
  background-color: rgb(152, 78, 255);
}

.table-striped > tbody > tr:nth-of-type(odd) > * {
  --bs-table-bg-type: rgba(255, 88, 196, 0.3) !important;
}

.thead-color {
  background-color: rgb(255, 88, 196) !important;
}

.footer-text {
  background-color: rgb(56, 255, 79);
}

/* 以下はダークモード時 */

@media (prefers-color-scheme: dark) {
  [data-bs-theme="dark"] {
    --bs-body-color: #ffffff !important;
  }

  .header-text {
    background-color: rgb(186, 137, 255);
  }

  .table-striped > tbody > tr:nth-of-type(odd) > * {
    --bs-table-bg-type: rgba(255, 133, 212, 0.507) !important;
  }

  .thead-color {
    background-color: rgb(255, 57, 186) !important;
  }

  .footer-text {
    background-color: rgb(85, 188, 97);
  }
}
サンプルJS
"use strict";

const html = document.documentElement;
const isDark = window.matchMedia("(prefers-color-scheme: dark)");

if (isDark.matches) {
  html.setAttribute("data-bs-theme", "dark");
}

document.addEventListener("DOMContentLoaded", () => {
  const getSelector = document.querySelector("#selectID");
  const defaultOption = document.getElementById("defaultSelected");
  const updateInputName = document.getElementById("updateInputName");
  const updateInputAge = document.getElementById("updateInputAge");
  const updateInputBirthplace = document.getElementById(
    "updateInputBirthplace"
  );
  const putButton = document.getElementById("putButton");
  const deleteButton = document.getElementById("deleteButton");

  // 🔽 ページ表示時にGETでデータ取得してテーブルに表示!
  fetch("http://WebサーバのパブリックIPアドレス/tomcat/sample/api/members")
    .then((res) => res.json())
    .then((data) => {
      const tableHead = document.querySelector("#membersTable thead");
      tableHead.innerHTML = "";
      // データベースにデータがない場合は何も表示しない
      if (data.length === 0) {
        return;
      }

      const buttonGroup = document.querySelector("#button-group");
      const updateButton = document.createElement("button");
      updateButton.setAttribute("type", "button");
      updateButton.setAttribute("data-bs-toggle", "modal");
      updateButton.setAttribute("data-bs-target", "#updateFormModal");
      updateButton.classList.add(
        "btn",
        "btn-secondary",
        "mt-3",
        "px-5",
        "py-2"
      );
      updateButton.innerText = "更新";
      buttonGroup.appendChild(updateButton);

      const theadRow = document.createElement("tr");
      theadRow.innerHTML = `
        <th class="thead-color">ID</th>
        <th class="thead-color">名前</th>
        <th class="thead-color">年齢</th>
        <th class="thead-color">出身地</th>
      `;
      tableHead.appendChild(theadRow);

      const tableBody = document.querySelector("#membersTable tbody");
      const formSelect = document.querySelector("#selectID");

      data.forEach((member) => {
        const tbodyRow = document.createElement("tr");
        tbodyRow.innerHTML = `
          <th scope="row">${member.id}</th>
          <td>${member.name}</td>
          <td>${member.age}</td>
          <td>${member.birthplace}</td>
        `;
        tableBody.appendChild(tbodyRow);

        const selectOptions = document.createElement("option");
        selectOptions.setAttribute("value", `${member.id}`);
        selectOptions.innerText = `${member.id}`;
        formSelect.appendChild(selectOptions);
        getSelector.addEventListener("change", (event) => {
          if (event.target.value === `${member.id}`) {
            updateInputName.setAttribute("value", `${member.name}`);
            updateInputAge.setAttribute("value", `${member.age}`);
            updateInputBirthplace.setAttribute("value", `${member.birthplace}`);
          }
        });
      });
    })
    .catch((error) => {
      const main = document.querySelector("main");
      const h1 = document.createElement("h1");
      h1.classList.add("text-center", "text-danger");
      h1.innerHTML = `データベース接続エラー<br>${error}`;
      main.appendChild(h1);
    });

  if (defaultOption.textContent === "IDを選択") {
    updateInputName.setAttribute("disabled", true);
    updateInputName.removeAttribute("placeholder");
    updateInputAge.setAttribute("disabled", true);
    updateInputAge.removeAttribute("placeholder");
    updateInputBirthplace.setAttribute("disabled", true);
    updateInputBirthplace.removeAttribute("placeholder");
    putButton.setAttribute("disabled", true);
    deleteButton.setAttribute("disabled", true);
  }

  getSelector.addEventListener("change", (event) => {
    if (event.target.value === "IDを選択") {
      updateInputName.setAttribute("disabled", true);
      updateInputName.removeAttribute("placeholder");
      updateInputName.removeAttribute("value");
      updateInputAge.setAttribute("disabled", true);
      updateInputAge.removeAttribute("placeholder");
      updateInputAge.removeAttribute("value");
      updateInputBirthplace.setAttribute("disabled", true);
      updateInputBirthplace.removeAttribute("placeholder");
      updateInputBirthplace.removeAttribute("value");
      putButton.setAttribute("disabled", true);
      deleteButton.setAttribute("disabled", true);
    } else {
      updateInputName.removeAttribute("disabled");
      updateInputName.setAttribute("placeholder", "Taro");
      updateInputAge.removeAttribute("disabled");
      updateInputAge.setAttribute("placeholder", "20");
      updateInputBirthplace.removeAttribute("disabled");
      updateInputBirthplace.setAttribute("placeholder", "東京都");
      putButton.removeAttribute("disabled");
      deleteButton.removeAttribute("disabled");
    }
  });

  // 🔽 POST時のバリデーション&登録
  const submitButton = document.querySelector('button[form="addRecord"]');
  const addForm = document.getElementById("addRecord");
  submitButton.addEventListener("click", (event) => {
    event.preventDefault();

    if (!addForm.checkValidity()) {
      // Bootstrapのスタイル表示用
      addForm.classList.add("was-validated");
      return;
    }

    const formData = new FormData(addForm);
    const params = new URLSearchParams(formData);

    fetch("http://WebサーバのパブリックIPアドレス/tomcat/sample/api/members", {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body: params.toString(),
    })
      .then((res) => res.json())
      .then((data) => {
        console.log("追加しました。", data);
        // ✅ 追加後に再取得 or その場で1行追加もOK!
        location.reload(); // ← 再読み込みが簡単!;
      })
      .catch((error) => {
        console.log("追加失敗", error);
      });
    addForm.classList.add("was-validated");
  });

  // 🔽 PUT時のバリデーション&登録
  const updateForm = document.getElementById("updateRecord");
  putButton.addEventListener("click", (event) => {
    event.preventDefault();

    if (!updateForm.checkValidity()) {
      // Bootstrapのスタイル表示用
      updateForm.classList.add("was-validated");
      return;
    }

    const formData = new FormData(updateForm);
    const obj = {};
    formData.forEach((value, key) => {
      obj[key] = value;
    });

    // 更新ボタン押下時
    fetch("http://WebサーバのパブリックIPアドレス/tomcat/sample/api/members", {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(obj),
    })
      .then((res) => res.json())
      .then((data) => {
        console.log("更新しました。", data);
        // ✅ 更新後に再取得 or その場で1行追加もOK!
        location.reload(); // ← 再読み込みが簡単!;
      })
      .catch((error) => {
        console.log("更新失敗", error);
      });
    updateForm.classList.add("was-validated");
  });

  // Delete時
  deleteButton.addEventListener("click", (event) => {
    event.preventDefault();

    const formData = new FormData(updateForm);
    const id = formData.get("id"); // 後述するけど、select に `name="id"` が必要!

    // 削除ボタン押下時
    fetch(
      `http://WebサーバのパブリックIPアドレス/tomcat/sample/api/members/${id}`,
      {
        method: "DELETE",
      }
    )
      .then((res) => res.json())
      .then((data) => {
        console.log("削除しました", data);
        // ✅ 削除後に再取得 or その場で1行追加もOK!
        location.reload(); // ← 再読み込みが簡単!;
      })
      .catch((error) => {
        console.log("削除失敗", error);
      });
  });
});

// 58行目と75行目のaddForm.classList.add("was-validated");は共通化可能。
// 53~60行目を以下のように修正。

// addForm.addEventListener("submit", (event) => {
//   event.preventDefault();

//   const isValid = addForm.checkValidity();
//   addForm.classList.add("was-validated"); // 共通で1回だけ!

//   if (!isValid) return;

2-2. Tomcat 用 EC2(プライベート)

sudo useradd -r -m -U -d /opt/tomcat -s /bin/false tomcat
  • Tomcat のダウンロードと展開(バージョン 11.0.6):
cd /opt/tomcat
sudo curl -O https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.6/bin/apache-tomcat-11.0.6.tar.gz
sudo tar -zxvf apache-tomcat-11.0.6.tar.gz -C /opt/tomcat --strip-components=1
sudo chown -R tomcat: /opt/tomcat
sudo bash -c 'chmod +x /opt/tomcat/bin/*.sh'
sudo ls -l /opt/tomcat/bin | grep '\.sh'  # rwx がついていれば成功

⚠️ 補足:sudo chmod +x /opt/tomcat/bin/*.shに対する実行時の注意

通常、以下のようなコマンドで実行します:

sudo chmod +x /opt/tomcat/bin/*.sh

ですが、ワイルドカード * の展開は 実行前にシェルが処理するため、 ec2-user などの一般ユーザーが /opt/tomcat/bin/ を読み取れないと、 No such file or directory エラーになることがあります。

✅ 回避策として以下のように sudo の中で bash を実行して展開させる方法がおすすめです:

sudo bash -c 'chmod +x /opt/tomcat/bin/*.sh'

これは「root 権限で bash を起動し、その中で *.sh を展開して chmod を実行する」という意味です。

✅ 確認コマンド:

sudo ls -l /opt/tomcat/bin | grep '\.sh'

rwx がついていれば成功です ✨

⚠️ Java 21 を使う場合の注意

JAVA_HOME は次のように JDK ディレクトリに設定してください:

Environment="JAVA_HOME=/usr/lib/jvm/java-21-<version>"

JAVA_HOME/bin/java を含めないように注意してください。

✅ 例:実際の JAVA_HOME の設定例

readlink -f /usr/bin/java の出力が次のようになっていた場合:

/usr/lib/jvm/java-21-amazon-corretto.x86_64/bin/java

このとき設定すべき JAVA_HOME は次のとおり:

Environment="JAVA_HOME=/usr/lib/jvm/java-21-amazon-corretto.x86_64"

/bin/java は含めず、その親ディレクトリを指定)


  • systemd ユニットファイルの作成(方法 ①:teeコマンドを使う):
sudo tee /etc/systemd/system/tomcat.service > /dev/null <<EOF
[Unit]
Description=Apache Tomcat
After=network.target

[Service]
Type=forking
User=tomcat
Group=tomcat
Environment="JAVA_HOME=/usr/lib/jvm/java-21-amazon-corretto.x86_64"
Environment="CATALINA_PID=/opt/tomcat/temp/tomcat.pid"
Environment="CATALINA_HOME=/opt/tomcat"
ExecStart=/opt/tomcat/bin/startup.sh
ExecStop=/opt/tomcat/bin/shutdown.sh
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF
  • systemd ユニットファイルの作成(方法 ②:viエディタを使う):
sudo vi /etc/systemd/system/tomcat.service

以下の内容を貼り付けて保存(:wq):

[Unit]
Description=Apache Tomcat
After=network.target

[Service]
Type=forking
User=tomcat
Group=tomcat
Environment="JAVA_HOME=/usr/lib/jvm/java-21-amazon-corretto.x86_64"
Environment="CATALINA_PID=/opt/tomcat/temp/tomcat.pid"
Environment="CATALINA_HOME=/opt/tomcat"
ExecStart=/opt/tomcat/bin/startup.sh
ExecStop=/opt/tomcat/bin/shutdown.sh
Restart=on-failure

[Install]
WantedBy=multi-user.target
  • サービスとして登録・起動:
sudo systemctl daemon-reexec
sudo systemctl enable --now tomcat

3. RDS(MariaDB)構築

  • エンジン:MariaDB
  • バージョン:10.6 以上
  • パブリックアクセス無効
  • セキュリティグループ:Tomcat EC2 からの 3306 ポートを許可

3-1. Mysql コマンドのインストール

AP サーバの EC2 から RDS に接続するために使用する。

sudo dnf install -y mariadb105

3-2. RDS にログイン

mysql -h {RDSインスタンスのエンドポイント} -u {ユーザー名} -p
Enter password:

3-3. データベース確認

select user, host from mysql.user;
SHOW CREATE DATABASE データベース名\g
ALTER DATABASE データベース名 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON データベース名.* TO ユーザー名@'%';
FLUSH PRIVILEGES;

-- 以下は独自のアプリケーションを配置し、DBと接続する場合
USE データベース名;
CREATE TABLE members (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  age INT NOT NULL,
  birthplace VARCHAR(100) NOT NULL
);
ALTER TABLE members CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
SHOW TABLES;
DESCRIBE members;

4. Apache と Tomcat の連携

4-1. HTTP リバースプロキシ設定

  • Apache に mod_proxy を有効化:
sudo dnf install mod_proxy_html -y
  • 設定ファイル /etc/httpd/conf.d/proxy.conf に以下を追加:
<VirtualHost *:80>
    ServerName your-domain.com

    ProxyPreserveHost On

    ProxyPass /tomcat http://<TomcatのプライベートIP>:8080/
    ProxyPassReverse /tomcat http://<TomcatのプライベートIP>:8080/

    ProxyPass /tomcat.css http://<TomcatのプライベートIP>:8080/tomcat.css
    ProxyPassReverse /tomcat.css http://<TomcatのプライベートIP>:8080/tomcat.css

    ProxyPass /tomcat.svg http://<TomcatのプライベートIP>:8080/tomcat.svg
    ProxyPassReverse /tomcat.svg http://<TomcatのプライベートIP>:8080/tomcat.svg
</VirtualHost>
  • Apache 再起動:
sudo systemctl restart httpd

4-2. AJP 接続設定

Tomcat 側設定(server.xml

<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

Apache 側設定(mod_jk 使用)

  • mod_jk モジュールをインストール(Amazon Linux の場合ソースから)
  • workers.properties の例:
worker.list=ajp13
worker.ajp13.type=ajp13
worker.ajp13.host=<TomcatのプライベートIP>
worker.ajp13.port=8009
  • httpd.conf に以下を追加:
JkMount /* ajp13
  • Apache 再起動:
sudo systemctl restart httpd

5. Tomcat ⇔ MariaDB JDBC 接続設定

Tomcat から MariaDB へ接続するために、JDBC ドライバの配置と接続設定を行います。

1. MariaDB JDBC ドライバのダウンロードと配置

sudo -i   # ec2-userではアクセスできないため、一時的に root シェルになる
cd /opt/tomcat/lib
sudo curl -O https://downloads.mariadb.com/Connectors/java/connector-java-3.5.3/mariadb-java-client-3.5.3.jar
chown tomcat: mariadb-java-client-3.5.3.jar

2. Tomcat Web アプリに DB 接続設定を追加

WEB-INF/web.xml や アプリケーション側の context.xml、Java ソース内など、使用するアプリケーションに応じて JDBC 接続設定を行います。

2-1. Tomcat の Web アプリ設定例(例:/opt/tomcat/webapps/sample/WEB-INF/web.xml

<resource-ref>
  <description>MariaDB Connection</description>
  <res-ref-name>jdbc/データベース名</res-ref-name>
  <res-type>javax.sql.DataSource</res-type>
  <res-auth>Container</res-auth>
</resource-ref>

2-2. context.xml にリソース定義(例:/opt/tomcat/conf/context.xml

<Context>
  <Resource name="jdbc/<データベース名>" auth="Container"
            type="javax.sql.DataSource"
            maxTotal="100" maxIdle="30" maxWaitMillis="10000"
            username="<ユーザー名>" password="<パスワード>"
            driverClassName="org.mariadb.jdbc.Driver"
            url="jdbc:mariadb://<RDSのエンドポイント>:3306/<データベース名>"/>
</Context>

⚠️ JDBC 接続 URL の例:

jdbc:mariadb://<RDSのエンドポイント>:3306/<データベース名>?user=<ユーザー名>&password=<パスワード>

3. Tomcat 再起動

sudo systemctl restart tomcat

接続確認には Java コードまたはデプロイした WAR アプリを使用して確認してください。

4.Tomcat ⇔ MariaDB 接続確認方法(JDBC テスト)

Tomcat から MariaDB に JDBC で正しく接続できているか確認するには、以下のいずれかの方法でテストします。

✅ 方法 1:JSP ページで JDBC 接続テスト(接続成功確認済み)

  1. 次の内容で JSP ファイルを作成:

ファイル名: /opt/tomcat/webapps/ROOT/testdb.jsp

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.sql.*, javax.naming.*, javax.sql.*" %>
<%
String msg = "";
try {
    Context initContext = new InitialContext();
    Context envContext = (Context) initContext.lookup("java:/comp/env");
    DataSource ds = (DataSource) envContext.lookup("jdbc/<データベース名>");
    Connection conn = ds.getConnection();
    msg = "✅ 接続成功!DB名: " + conn.getCatalog();
    conn.close();
} catch (Exception e) {
    msg = "❌ 接続失敗: " + e.getMessage();
}
%>
<h2><%= msg %></h2>
  1. ブラウザでアクセス:
http://[アプリケーションサーバのIP]:8080/testdb.jsp

Apache 経由の場合:

http://[WebサーバのIP]/tomcat/testdb.jsp

✅ 方法 2:Tomcat ログで接続エラー確認

Tomcat が起動時に JDBC 接続に失敗している場合、ログにエラーメッセージが出力されます。

cat /opt/tomcat/logs/catalina.out | grep -i error

または

journalctl -u tomcat

✅ 方法 3:WAR アプリケーションで確認

実際のアプリケーションが .war ファイルとしてデプロイされている場合、そのアプリ内の JDBC コードが正常に動作していれば接続確認となります。

以下🙌(余談)Tomcat に Java アプリケーションを配置してデータベースと接続することも可能!参照。

  • /opt/tomcat/webapps/.war ファイルを配置すれば自動展開されます
  • ログや画面でエラーが出ないかを確認

✅ 接続成功時の例(画面表示)

以下のような表示があれば接続は成功です:

✅ 接続成功!DB名: <データベース名>

(※ 表示が文字化けしている場合は contentType の設定で UTF-8 を明示してください)

💡 文字化けを直したい場合(オプション)

testdb.jsp の先頭にこれを追加してみて:

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>

✅ 修正後の先頭はこうなる:

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.sql.*, javax.naming.*, javax.sql.*" %>

🙌(余談)Tomcat に Java アプリケーションを配置してデータベースと接続することも可能!

1. コンパイル用、.jarライブラリ用、Java ファイル格納用のフォルダをそれぞれ作成

# コンパイル用  後ろのcom/exampleはjavaファイルの先頭のpackage文と同じにする。
# package com.example;  だったら以下のようにフォルダを作成しよう
mkdir -p /opt/tomcat/webapps/sample/WEB-INF/classes/com/example

# .jarライブラリ用
mkdir -p /opt/tomcat/webapps/sample/WEB-INF/lib

# Javaファイル格納用  後ろのcom/exampleはjavaファイルの先頭のpackage文と同じにする。
# package com.example;  だったら以下のようにフォルダを作成しよう
mkdir -p /opt/tomcat/webapps/sample/src/com/example

2. Java アプリケーション用の Java ファイルを保存

Java ファイルを/opt/tomcat/webapps/sample/src/com/exampleに格納。

サンプルファイル①
package com.example;

import java.io.IOException;
import java.sql.*;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.json.JSONObject;
import jakarta.servlet.ServletException;

public class BaseServlet extends HttpServlet {

  // DB接続情報
  // MySQLのときは↓こっち!
  // protected static final String JDBC_URL =
  // "jdbc:mysql://RDSのエンドポイント:3306/データベース名?serverTimezone=UTC";
  protected static final String JDBC_URL = "jdbc:mariadb://RDSのエンドポイント:3306/データベース名?useUnicode=true&characterEncoding=utf8mb4";
  protected static final String DB_USER = "ユーザー名";
  protected static final String DB_PASS = "パスワード";

  protected Connection getConnection() throws SQLException {
    System.out.println("🛠 DB接続開始");
    try {
      return DriverManager.getConnection(JDBC_URL, DB_USER, DB_PASS);
    } catch (Exception e) {
      System.out.println("❌ DB接続失敗:" + e.getMessage());
      e.printStackTrace();
      throw e;
    }
  }

  protected void prepareJsonResponse(HttpServletResponse response) {
    response.setContentType("application/json; charset=UTF-8");
    response.setCharacterEncoding("UTF-8");

    // ✅ CORS対応ヘッダー
    response.setHeader("Access-Control-Allow-Origin", "http://WebサーバのパブリックIPアドレス");
    response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
    response.setHeader("Access-Control-Allow-Headers", "Content-Type");
  }

  protected void sendJsonError(HttpServletResponse response, int status, String message) throws IOException {
    response.setStatus(status);
    response.setContentType("application/json; charset=UTF-8");
    JSONObject error = new JSONObject();
    error.put("error", message);
    response.getWriter().write(error.toString());
  }

  @Override
  protected void doOptions(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    prepareJsonResponse(response); // CORS用ヘッダーも含まれる
    response.setStatus(HttpServletResponse.SC_OK);
  }
}
サンプルファイル②
package com.example;

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
import java.io.*;
import java.sql.*;
import org.json.JSONArray;
import org.json.JSONObject;

@WebServlet("/api/members/*")
public class MembersServlet extends BaseServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    prepareJsonResponse(response);

    JSONArray jsonArray = new JSONArray();

    try (Connection conn = getConnection();
        PreparedStatement stmt = conn.prepareStatement("SELECT id, name, age, birthplace FROM members");
        ResultSet rs = stmt.executeQuery()) {

      while (rs.next()) {
        JSONObject member = new JSONObject();
        member.put("id", rs.getInt("id"));
        member.put("name", rs.getString("name"));
        member.put("age", rs.getInt("age"));
        member.put("birthplace", rs.getString("birthplace"));
        jsonArray.put(member);
      }

    } catch (Exception e) {
      e.printStackTrace();
      response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
      sendJsonError(response, 400, "データ取得エラー");
      return;
    }

    response.getWriter().write(jsonArray.toString());
  }

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    request.setCharacterEncoding("UTF-8");
    System.out.println("🔥 doPost reached!");
    prepareJsonResponse(response);
    String name = request.getParameter("name");
    System.out.println("✅ name: " + name);
    String ageStr = request.getParameter("age");
    System.out.println("✅ ageStr: " + ageStr);
    String birthplace = request.getParameter("birthplace");
    System.out.println("✅ birthplace: " + birthplace);

    // 入力チェック
    if (name == null || name.trim().isEmpty() || ageStr == null || !ageStr.matches("\\d+") || birthplace == null
        || birthplace.trim().isEmpty()) {
      response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 400 Bad Request
      sendJsonError(response, 400, "名前と年齢、出身地は必須です(年齢は数字)\"}");
      return;
    }

    try (Connection conn = getConnection();
        PreparedStatement stmt = conn.prepareStatement("INSERT INTO members (name, age, birthplace) VALUES (?, ?, ?)",
            Statement.RETURN_GENERATED_KEYS)) {

      System.out.println("✅ DB接続成功");
      stmt.setString(1, name);
      stmt.setInt(2, Integer.parseInt(ageStr));
      stmt.setString(3, birthplace);
      stmt.executeUpdate();

      ResultSet generatedKeys = stmt.getGeneratedKeys();
      JSONObject result = new JSONObject();
      if (generatedKeys.next()) {
        result.put("id", generatedKeys.getInt(1));
      }

      result.put("name", name);
      result.put("age", Integer.parseInt(ageStr));
      result.put("birthplace", birthplace);
      response.setContentType("application/json; charset=UTF-8");
      response.getWriter().write(result.toString());

    } catch (Exception e) {
      System.out.println("💥 DB処理失敗!");
      e.printStackTrace(); // ← ★絶対入れよう!!
      response.setStatus(500);
      sendJsonError(response, 500, "登録失敗");
    }

  }

  @Override
  protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    request.setCharacterEncoding("UTF-8");
    System.out.println("🔥 doPut reached!");
    prepareJsonResponse(response);

    BufferedReader reader = request.getReader();
    StringBuilder jsonBuilder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null)
      jsonBuilder.append(line);

    try {
      JSONObject json = new JSONObject(jsonBuilder.toString());

      // 🔽 バリデーション項目を取り出す
      if (!json.has("id") || !json.has("name") || !json.has("age") ||
          !json.has("birthplace")) {
        sendJsonError(response, 400, "名前と年齢、出身地は必須です(年齢は数字)\\\"}");
        return;
      }

      int id = json.getInt("id");
      System.out.println("✅ id: " + id);
      String name = json.getString("name");
      System.out.println("✅ name: " + name);
      int age = json.getInt("age");
      System.out.println("✅ age: " + age);
      String ageStr = Integer.toString(age);
      System.out.println("✅ ageStr: " + ageStr);
      String birthplace = json.getString("birthplace");
      System.out.println("✅ birthplace: " + birthplace);

      if (name.trim().isEmpty() || age < 0 || id <= 0 ||
          birthplace.trim().isEmpty()) {
        sendJsonError(response, 400, "不正なデータが含まれています");
        return;
      }

      // 入力チェック
      if (name == null || name.trim().isEmpty() || ageStr == null ||
          !ageStr.matches("\\d+") || birthplace == null
          || birthplace.trim().isEmpty()) {
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 400 Bad Request
        sendJsonError(response, 400, "名前と年齢、出身地は必須です(年齢は数字)\"}");
        return;
      }

      try (Connection conn = getConnection();
          PreparedStatement stmt = conn
              .prepareStatement("UPDATE members SET name = ?, age = ?, birthplace = ? WHERE id = ?")) {

        System.out.println("✅ DB接続成功");
        stmt.setString(1, name);
        stmt.setInt(2, age);
        stmt.setString(3, birthplace);
        stmt.setInt(4, id);
        int updated = stmt.executeUpdate();

        response.setContentType("application/json; charset=UTF-8");
        response.getWriter().write("{\"updated\": " + updated + "}");

      }

    } catch (Exception e) {
      System.out.println("💥 DB処理失敗!");
      e.printStackTrace(); // ← ★絶対入れよう!!
      response.setStatus(500);
      sendJsonError(response, 500, "更新失敗");
    }
  }

  @Override
  protected void doDelete(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    request.setCharacterEncoding("UTF-8");
    System.out.println("🔥 doDelete reached!");
    prepareJsonResponse(response);

    String pathInfo = request.getPathInfo(); // 例: "/3"
    System.out.println("✅ pathInfo: " + pathInfo);

    if (pathInfo == null || pathInfo.length() <= 1) {
      sendJsonError(response, 400, "IDが指定されていません");
      return;
    }

    String idStr = pathInfo.substring(1); // "/3" → "3"
    System.out.println("✅ id: " + idStr);

    if (idStr == null || !idStr.matches("\\d+")) {
      sendJsonError(response, 400, "IDが不正です");
      return;
    }

    int id = Integer.parseInt(idStr);

    try (Connection conn = getConnection();
        PreparedStatement stmt = conn.prepareStatement("DELETE FROM members WHERE id = ?")) {

      System.out.println("✅ DB接続成功");
      stmt.setInt(1, id);
      int deleted = stmt.executeUpdate();
      response.getWriter().write("{\"deleted\": " + deleted + "}");

    } catch (Exception e) {
      System.out.println("💥 DB処理失敗!");
      e.printStackTrace(); // ← ★絶対入れよう!!
      response.setStatus(500);
      sendJsonError(response, 500, "削除失敗");
    }
  }
}

3. .jarライブラリを集める

3-1. MariaDB JDBC ドライバ(Connector)

公式サイト → https://mariadb.com/kb/en/mariadb-connector-j/

または、https://downloads.mariadb.com/Connectors/javaから任意のバージョンをダウンロードするか、1. MariaDB JDBC ドライバのダウンロードと配置で予めダウンロードしたファイルでもよい。

ファイル例:

mariadb-java-client-3.5.3.jar

</br>

3-2. JSON ライブラリ(org.json)

ここから任意のバージョンをダウンロード可能 → https://repo1.maven.org/maven2/org/json/json/20250107/json-20250107.jar

ファイル例:

json-20250107.jar

ファイルをダウンロードし、その所有権を変更。

cd /opt/tomcat/lib
curl -O https://repo1.maven.org/maven2/org/json/json/20250107/json-20250107.jar
chown tomcat: json-20250107.jar

3-3. MariaDB JDBC ドライバ(Connector)、 JSON ライブラリ(org.json)を/opt/tomcat/webapps/sample/WEB-INF/libに保存!

cp /opt/tomcat/lib/mariadb-java-client-3.5.3.jar /opt/tomcat/webapps/sample/WEB-INF/lib/ &&
cp /opt/tomcat/lib/json-20250107.jar /opt/tomcat/webapps/sample/WEB-INF/lib/

3-4. jakarta.servlet-api-6.0.x.jarをダウンロードして/opt/tomcat/webapps/sample/WEB-INF/libに保存!

cd /opt/tomcat/webapps/sample/WEB-INF/lib/
curl -O https://repo1.maven.org/maven2/jakarta/servlet/jakarta.servlet-api/6.0.0/jakarta.servlet-api-6.0.0.jar

⚠️ また、Maven を使用している場合は、/opt/tomcat/webapps/sample/pom.xmlに以下の依存関係を追加することで、ビルド時に自動的にダウンロードされる。</br>
(本手順書では Maven は使用していない)

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>sample</artifactId>
  <version>1.0.0</version>
  <packaging>war</packaging>

  <dependencies>
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>6.0.0</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-war-plugin</artifactId>
        <version>3.3.2</version>
      </plugin>
    </plugins>
  </build>
</project>

scopeprovidedに設定することで、実行時にはサーブレットコンテナ(この場合は Tomcat)によって提供される

⚠️⚠️ ただし、Tomcat 11 には jakarta.servlet-api が同梱されていないため、WAR ファイルに同梱することをおすすめ

3-5. コンパイルする

cd /opt/tomcat/webapps/sample
javac -d WEB-INF/classes -classpath "WEB-INF/lib/*" src/com/example/*.java

4. war ファイルを作成する

jar -cvf ../sample.war *

<!-- Java ファイルを以下のコマンドでコンパイル

javac -encoding UTF-8 -cp ".:../WEB-INF/lib/*" -d ../WEB-INF/classes BaseServlet.java MembersServlet.java

これで、WEB-INF/classes.classファイルができているか確認。 -->

<p style="color: red;"><b>💥 作成を確認出来たら再度<code>/opt/tomcat</code>以下の権限を tomcat ユーザーに付与!!忘れずに!!</b></p>

chown -R tomcat: /opt/tomcat

4-1. 作成したフォルダを削除(今回の場合 sample フォルダ)

cd /opt/tomcat/webapps
rm -rf sample

4-2. WAR アプリの自動展開設定

Tomcat のconf/server.xmlに以下の設定があると、webapps/sample/に自動で展開される

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">

unpackWARs="true"で自動展開、autoDeploy="true"でホットデプロイ(起動中でも反映)できる。

5. web.xml を作成(アノテーション使わない場合)

/opt/tomcat/webapps/sample/WEB-INF/web.xml

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" version="5.0">
  <servlet>
    <servlet-name>MembersServlet</servlet-name>
    <servlet-class>MembersServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>MembersServlet</servlet-name>
    <url-pattern>/api/members</url-pattern>
  </servlet-mapping>
</web-app>

‼️@WebServlet("/api/members")MembersServletに書いていればweb.xml省略しても OK!(2. Java アプリケーション用の Java ファイルを保存でサンプルファイルを使用する場合は、省略 OK!)2. Tomcat の Web アプリ設定例web.xmlを作成して締まった場合は削除する。

6. Tomcat を再起動

systemctl restart tomcat

その後ブラウザで:

http://<TomcatのIP>:8080/tomcat/sample/api/members

もしくはリバースプロキシを使用している場合:

http://<WebサーバのIP>/tomcat/sample/api/members

レコード追加したのに反映しない場合

AP サーバで以下コマンドを実行し、トレースする。

tail -f /opt/tomcat/logs/catalina.out

Web サーバ側で POST してみる。

curl -X POST http://APサーバのIPアドレス:8080/sample/api/members -d "name=テスト&age=20&birthplace=東京"

5. 動作確認

  • ブラウザから Apache のパブリック IP にアクセス
  • Tomcat の Web アプリが表示されることを確認

6. 補足

  • SSL 対応は mod_ssl を使って別途設定可能
  • Apache/Tomcat におけるログ確認:
    • Apache: /var/log/httpd/
    • Tomcat: /opt/tomcat/logs/

必要に応じて画面キャプチャや具体的な設定ファイルの中身を補足していきます。

Discussion