2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【OCI アーキテクチャ大全】3層アーキテクチャ =初級編=

Posted at

アーキテクチャ大全とは

AWSでよく見るクラウドアーキテクチャをOCIで作りながら、OCIの基本的なアーキテクチャを学習してみようという企画。

3層アーキテクチャ =初級編=

第一弾は、Web-App-DB 3層アーキテクチャを作っていきます。

AWS アーキテクチャ

image.png

クラウドサービス比較

AWS OCI Role
VPC VCN 仮想ネットワーク
Application Load Balancer Flexible Load Balacer ロードバランサー
EC2 Compute VM 仮想サーバー
RDS for MySQL MySQL HeatWave Database

上記がAWSの各サービスに対応するOCIのサービスです。
Web層とApp層にはCompute VM、DB層にMySQL HeatWaveを採用します。

OCI アーキテクチャ

image.png

エンドユーザーはインターネット経由でFLBにアクセスし、アプリケーションを利用します。
Web層とApp層は1つのCompute VMに共存しています。フロントエンドにVue.js、バックエンドにpythonを採用しました。
Compute VMはプライベートサブネットにあるため、パッケージをインストールする目的でNAT Gatewayを配置します。
DatabaseはMySQL HeatWaveを利用します。

構築アプリケーション概要

今回構築するアプリケーションは、シンプルなタスク管理アプリケーションです。
アプリケーションソースは後ほどGitHubに上げます。

完成イメージは、ざっくりこんな感じです。
image.png

構築手順

  1. VCNの構築
  2. 開発用サーバの構築
  3. Compute VMの構築
  4. MySQL HeatWaveの構築
  5. Flexible Load Balancerの構築
  6. 動作確認

1. VCNの構築

1-1. VCNの作成

image.png
OCIのクラウドコンソールから、ネットワーキング>仮想クラウド・ネットワークを選択し、仮想クラウド・ネットワークのサービス画面に遷移します。
遷移後、「VCNの作成」ボタンを押下します。

さらに画面遷移後、IPv4 CIDRブロックに10.0.0.0/16 と入力し、「VCNの作成」ボタンを押下します。
image.png

1-2. ゲートウェイの作成

インターネット・ゲートウェイとNATゲートウェイの2つのゲートウェイを作成します。

1-2-1. インターネット・ゲートウェイの作成

VCN作成後、ゲートウェイのタブを選択し、「インターネット・ゲートウェイの作成」ボタンを押下します。
image.png

"three-tier-igw"という名前で、インターネット・ゲートウェイを作成します。
image.png
インターネット・ゲートウェイが作成されたことを確認できます。
image.png

1-2-2. NATゲートウェイの作成

同じ要領で、「NATゲートウェイの作成」ボタンを押下します。
image.png

"three-tier-ngw"という名前で、NATゲートウェイを作成します。
image.png
NATゲートウェイが作成されたことを確認できます。
image.png

1-3. ルート表の作成

ルーティングのタブを選択します。"Default Route Table for three-tier-vcn"という名前で既にルート表が存在していることが確認できます。このルート表はプライベートサブネット用のルートテーブルとして利用します。

1-3-1. パブリックサブネット用のルート表の作成

インターネット・ゲートウェイにルートを持つパブリックサブネット用のルート表を作成しましょう。
「ルート表の作成」ボタンを押下します。
image.png
"three-tier-pub-route"という名前で、パブリックサブネット用のルートテーブルを作成します。
image.png
ルート・ルールに、インターネット・ゲートウェイとのルート・ルールを追加します。
ターゲット・タイプにて"インターネット・ゲートウェイ"を選択し、宛先CIDRブロックに "0.0.0.0/0" を入力します。ターゲット・インターネット・ゲートウェイに、1-2-1で作成した"three-tier-igw"を選択します。
その後、「作成」ボタンを押下します。
image.png
ルート表が追加されていることが確認できます。

1-3-2. プライベートサブネット用のルート表の編集

次に、プライベートサブネット用のルート表を編集します。
"Default Route Table for three-tier-vcn"のラベルを選択し、画面遷移します。
image.png
ルート・ルールタブを押下し、「ルート・ルールの追加」ボタンを押下します。
image.png
ルート・ルールに、NATゲートウェイとのルート・ルールを追加します。
image.png
ターゲット・タイプにて"NATゲートウェイ"を選択し、宛先CIDRブロックに "0.0.0.0/0" を入力します。ターゲット・NATゲートウェイに、1-2-1で作成した"three-tier-ngw"を選択します。
その後、「作成」ボタンを押下します。
image.png
ルート表が追加されていることが確認できます。

1-4. サブネットの作成

サブネットのタブを選択し、「サブネットの作成」ボタンを押下します。
image.png

1-4-1. パブリックサブネットの作成

"public-subnet-flb"という名前のパブリックサブネットを作成します。
名前に"public-subnet-flb"と入力し、IPv4 CIDRブロックに"10.0.0.0/24"を入力します。
IPv6接頭辞の下で、ルート表に"three-tier-pub-route"を選択します。
サブネット・アクセスは"パブリック・サブネット"を選択します。
セキュリティ・リストは後ほど設定するので、設定しないでください。
ここまで入力、選択できたら「サブネットの作成」ボタンを押下します。
image.png
しばらくすると、パブリックサブネットが作成されたことを確認できます。
image.png

1-4-2. プライベートサブネットの作成 -app用

"private-subnet-app"という名前のプライベートサブネットを作成します。
名前に"private-subnet-app"と入力し、IPv4 CIDRブロックに"10.0.1.0/24"を入力します。
IPv6接頭辞の下で、ルート表に"Default Route Table for three-tier-vcn"を選択します。
サブネット・アクセスは"プライベート・サブネット"を選択します。
セキュリティ・リストは後ほど設定するので、設定しないでください。
ここまで入力、選択できたら「サブネットの作成」ボタンを押下します。
image.png
しばらくすると、プライベートサブネットが作成されたことを確認できます。
image.png

1-4-3. プライベートサブネットの作成 -db用

"private-subnet-db"という名前のプライベートサブネットを作成します。
サブネット名に"private-subnet-db"を入力し、IPv4 CIDRブロックに"10.0.2.0/24"を入力します。
上記以外は、1-4-2と手順は同じなので割愛します。
ここまでの手順で、サブネットが3つできたことを確認できます。
image.png

1-5. セキュリティ・リストの作成

FLB,Compute,MySQL HeatWaveが配置されるサブネット毎に、別々のセキュリティ・リストを用意します。

1-5-1. FLB用のセキュリティ・リストの作成

セキュリティのタブを選択し、「セキュリティ・リストの作成」ボタンを押下します。
image.png
名前に、"security-list-lb"と入力します。
イングレースのルール許可に、FLB用のルールを追加します。
ソースCIDRに"0.0.0.0/0"、ソース・ポート範囲に"ALL"、宛先ポート範囲に"80,443"を入力し、「イングレス・ルールの追加」ボタンを押下します。

image.png

セキュリティ・リストが追加されたことを確認できます。
image.png

1-5-2. Compute用のセキュリティ・リストの作成

同じ要領で追加していきます。
名前に、"security-list-compute"と入力します。
イングレースのルール許可に、Compute用のルールを追加します。
イングレス・ルール1に、ソースCIDRに"10.0.0.0/24"、宛先ポート範囲に"80"を入力します。
image.png

のちに、ComputeへSSHしてサーバーの設定を行うためのイングレス・ルールも設定しておきましょう。
パブリックサブネット上に踏み台・運用サーバとしてCompute VMから接続することを想定します。
イングレス・ルール2に、ソースCIDRに"10.0.0.0/24"、宛先ポート範囲に"22"を入力します。
image.png

「イングレス・ルールの追加」ボタンを押下します。

image.png

セキュリティ・リストが追加されたことを確認できます。

1-5-3. MySQL HeatWave用のセキュリティ・リストの作成

同じ要領で追加していきます。
名前に、"security-list-db"と入力します。
イングレースのルール許可に、DB用のルールを追加します。
イングレス・ルール1に、ソースCIDRに"10.0.1.0/24"、宛先ポート範囲に"3306"を入力します。
image.png

「イングレス・ルールの追加」ボタンを押下します。

ここまでの手順で、セキュリティ・リストが3つできたことを確認できます。
image.png

1-5-4. セキュリティ・リストとサブネットの紐づけ

セキュリティ・リストとサブネットの対応表は下記の通りです。

セキュリティ・リスト サブネット パブリック/プライベート
security-list-lb public-subnet-flb パブリック
security-list-compute private-subnet-app プライベート
security-list-db private-subnet-db プライベート

サブネットの画面に遷移し、public-subnet-flb の画面に遷移します。
image.png

セキュリティタブを選択して、「セキュリティ・リストの追加」ボタンを押下します。
image.png

セキュリティ・リストに"security-list-lb"を選択し、「セキュリティ・リストの追加」ボタンを押下します。
image.png

この操作を、"private-subnet-app"と"private-subnet-db"においても行います。

ここまでの手順で作成されたリソースを図解すると、このようになります。

image.png

2. 開発用サーバの構築

パブリックサブネット "public-subnet-flb"にCompute VMを作成し、Cloud Shellから接続します。
Compute VMの作成方法については、下記を参照してください。

OSはOracle Linux9, ShapeはVM.Standard.E4.Flexを選択しました。
この開発用サーバから、このあとプライベートサブネット上に作成するCompute VMへ接続します。

image.png

3. Compute VMの構築

プライベートサブネット"private-subnet-app"に、アプリケーションを稼働させるCompute VMを構築します。

3-1. Compute VMの作成

OCIのクラウドコンソールから、コンピュート>インスタンスを選択し、Computeのサービス画面に遷移します。
遷移後、「インスタンスの作成」ボタンを押下します。

①基本情報での入力・選択事項は下記の通りです。
名前:instance-three-tier-app
イメージ:Oracle Linux9
シェイプ:VM.Standard.E5.Flex
入力、選択後、「次」ボタンを押下します。

②セキュリティはデフォルトのまま、「次」ボタンを押下します。

③ネットワーキングでの入力・選択事項は下記の通りです。
プライマリ・ネットワーク:three-tier-vcn を選択。
サブネット:private-subnet-app (リージョナル)を選択。
SSHキーの追加:「秘密キーのダウンロード」ボタンを押下、"app-instance.key"という名前で保存。
入力、選択後、「次」ボタンを押下します。

④ストレージはデフォルトのまま、「次」ボタンを押下します。
⑤確認画面で①~④の入力内容を確認し、「作成」ボタンを押下します。

3-2. Compute VMへの接続

Cloud Shell->開発用サーバ->instance-three-tier-appに接続します。
開発用サーバからappサーバに接続するため、秘密キーを送付します。

$ scp -i ssh-key-2025-08-16.key app-instance.key opc@132.226.239.95:/home/opc
$ scp -i 開発用サーバの接続キー 送付するappサーバの接続キー opc@開発用サーバのIPアドレス階層

Cloud Shellから開発用サーバに接続します。

$ ssh -i ssh-key-2025-08-16.key opc@132.226.239.95
[opc@dev-three-tier ~]$ 

開発用サーバからappサーバに接続します。

$ [opc@dev-three-tier ~]$ ssh -i app-instance.key opc@10.0.1.95
[opc@instance-three-tier-app ~]$ 

3-3. フロントエンド構築

フロントエンドをVue.jsで実装していきます。

3-3-1. nginxのインストール

appサーバ上で構築する、nginxをインストールしていきます。

$ sudo dnf update -y
$ sudo dnf install -y nginx

nginxを起動し、自動起動を設定します。

$ sudo systemctl enable --now nginx
Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service  /usr/lib/systemd/system/nginx.service.

3-3-2. Node.jsのインストール

まずは、Vue.jsを扱うための下準備として、Node.jsをインストールします。

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 16555  100 16555    0     0  73906      0 --:--:-- --:--:-- --:--:-- 73906
=> Downloading nvm as script to '/home/opc/.nvm'

=> Appending nvm source string to /home/opc/.bashrc
=> Appending bash_completion source string to /home/opc/.bashrc
=> Close and reopen your terminal to start using nvm or run the following to use it now:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
$ source ~/.bashrc

最新版のLTSバージョンを導入します。

$ nvm install --lts
Installing latest LTS version.
Downloading and installing node v22.18.0...
Downloading https://nodejs.org/dist/v22.18.0/node-v22.18.0-linux-x64.tar.xz...
######################################################################################################################################################################################################################################################### 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v22.18.0 (npm v10.9.3)
Creating default alias: default -> lts/* (-> v22.18.0)
$ nvm use --lts
Now using node v22.18.0 (npm v10.9.3)

3-3-3. Vueプロジェクト作成

$ mkdir -p /home/opc/app/frontend
$ cd /home/opc/app/frontend
$ npx create-vite@latest frontend -- --template vue

  Select a framework:
  Vue

  Select a variant:
  JavaScript

  Scaffolding project in /home/opc/app/frontend/frontend...

  Done. Now run:

  cd frontend
  npm install
  npm run dev
$ npm install

added 34 packages, and audited 35 packages in 10s

6 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

3-4. Vue実装

/home/opc/app/frontend/frontend/src 配下のApp.jsを下記コードに書き換えます。

App.js
<script setup>
import { ref, onMounted } from 'vue'

const todos = ref([])
const newTitle = ref('')
const loading = ref(false)
const errorMsg = ref('')

// 共通:API呼び出し
async function api(path, opts = {}) {
  const res = await fetch(path, { headers: { 'Content-Type': 'application/json' }, ...opts })
  if (!res.ok) {
    const t = await res.text().catch(() => '')
    throw new Error(`${res.status} ${res.statusText} ${t}`)
  }
  // 一部エンドポイントは204/空ボディの可能性を考慮
  const text = await res.text()
  return text ? JSON.parse(text) : null
}

async function fetchTodos() {
  loading.value = true
  errorMsg.value = ''
  try {
    todos.value = await api('/api/todos')
  } catch (e) {
    errorMsg.value = '一覧の取得に失敗しました。' + (e?.message ? ` (${e.message})` : '')
  } finally {
    loading.value = false
  }
}

async function addTodo() {
  const title = newTitle.value.trim()
  if (!title) return
  errorMsg.value = ''
  try {
    await api('/api/todos', { method: 'POST', body: JSON.stringify({ title }) })
    newTitle.value = ''
    await fetchTodos()
  } catch (e) {
    errorMsg.value = '追加に失敗しました。' + (e?.message ? ` (${e.message})` : '')
  }
}

async function toggle(todo) {
  errorMsg.value = ''
  try {
    await api(`/api/todos/${todo.id}/toggle`, { method: 'POST' })
    await fetchTodos()
  } catch (e) {
    errorMsg.value = '更新に失敗しました。' + (e?.message ? ` (${e.message})` : '')
  }
}

function onKeydown(e) {
  if (e.key === 'Enter') addTodo()
}

onMounted(fetchTodos)
</script>

<template>
  <main class="container">
    <h1>Vue + Flask + MySQL (HeatWave)</h1>

    <section class="composer">
      <input
        v-model="newTitle"
        @keydown="onKeydown"
        placeholder="新規TODOを入力して Enter"
        aria-label="新規TODO入力"
      />
      <button @click="addTodo">追加</button>
    </section>

    <p v-if="loading" class="muted">読み込み中...</p>
    <p v-if="errorMsg" class="error">{{ errorMsg }}</p>

    <ul class="list">
      <li v-for="t in todos" :key="t.id" class="item">
        <label class="row">
          <input type="checkbox" :checked="t.done === 1 || t.done === true" @change="toggle(t)" />
          <span :class="{ done: t.done === 1 || t.done === true }">{{ t.title }}</span>
        </label>
        <time class="stamp" v-if="t.created_at">{{ new Date(t.created_at).toLocaleString() }}</time>
      </li>
      <li v-if="!loading && !todos.length" class="muted">まだTODOがありません</li>
    </ul>
  </main>
</template>

<style>
:root {
  --fg: #111;
  --muted: #666;
  --border: #e5e7eb;
  --accent: #0ea5e9;
  --bg: #fff;
}

* { box-sizing: border-box; }
html, body, #app { height: 100%; }
body {
  margin: 0;
  color: var(--fg);
  background: var(--bg);
  font-family: system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,Apple Color Emoji,Segoe UI Emoji;
}

.container {
  max-width: 720px;
  margin: 40px auto;
  padding: 16px;
}

h1 {
  font-size: 22px;
  margin: 0 0 16px;
}

.composer {
  display: flex;
  gap: 8px;
  margin: 12px 0 20px;
}

.composer input {
  flex: 1;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  outline: none;
}

.composer input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(14,165,233,0.12);
}

.composer button {
  padding: 10px 14px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: #f8fafc;
  cursor: pointer;
}

.list { list-style: none; padding: 0; margin: 0; }
.item {
  padding: 10px 8px;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.item:first-child { border-top: none; }

.row { display: flex; align-items: center; gap: 10px; }

.done { text-decoration: line-through; color: var(--muted); }
.muted { color: var(--muted); margin: 8px 0; }
.error { color: #b91c1c; background: #fee2e2; border: 1px solid #fecaca; padding: 8px 10px; border-radius: 8px; }
.stamp { color: var(--muted); font-size: 12px; }
</style>

Vue.jsをビルドします。

$ npm run build

> frontend@0.0.0 build
> vite build

vite v7.1.2 building for production...
 11 modules transformed.
dist/index.html                  0.46 kB  gzip:  0.29 kB
dist/assets/index-d1igmdZn.css   2.19 kB  gzip:  0.96 kB
dist/assets/index-YY-sP1k2.js   63.08 kB  gzip: 25.43 kB
 built in 646ms

ビルドによって生成されたdist配下のリソースを公開します。

$ sudo rm -rf /usr/share/nginx/html/*
$ cd /home/opc/app/frontend/frontend
$ sudo cp -r dist/* /usr/share/nginx/html/

nginxを起動します。

$ sudo systemctl enable --now nginx
$ sudo nginx -t && sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

3-5. nginxの設定

nginxのパスを設定します。

$ sudo tee /etc/nginx/conf.d/app.conf >/dev/null <<'EOF'
server {
    listen 80 default_server;
    server_name _;

    # 静的リソースを配信
    root /usr/share/nginx/html;
    index index.html;

    # # SPA のルーティング
    location / {
        try_files $uri $uri/ /index.html;
    }

    # API は Flask(Gunicorn)へ
    location /api/ {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # ヘルスチェック
    location /health {
        proxy_pass http://127.0.0.1:8000/health;
    }
}
EOF

$ sudo nginx -t && sudo systemctl reload nginx

3-5. バックエンド構築

バックエンドをPythonで実装していきます。

3-5-1. パッケージのインストール

作業用ディレクトリを作成し、権限を付与します。

$ sudo mkdir -p /opt/backend /opt/venvs
$ sudo chown -R opc:opc /opt/backend /opt/venvs

必要なパッケージをインストールします。
今回は、Flask、gunicornを利用してバックエンドを構築します。

# Python パッケージのインストール
$ sudo dnf update -y
$ sudo dnf install -y gcc python3.11 python3.11-devel policycoreutils-python-utils

# mysql clientのインストール
$ sudo dnf install -y mysql

# venv(仮想環境)作成・有効化
$ python3.11 -m venv /opt/venvs/backend
$ source /opt/venvs/backend/bin/activate

# ライブラリのインストール
$ pip install -U pip
$ pip install Flask==3.0.3 gunicorn==22.0.0 PyMySQL==1.1.1 python-dotenv==1.0.1
$ deactivate

# 権限の付与
# 所有者を opc に統一
$ sudo chown -R opc:opc /opt/backend /opt/venvs

# ディレクトリに実行権限を付与
chmod 755 /opt/backend
chmod 755 /opt/venvs /opt/venvs/backend /opt/venvs/backend/bin

# pyファイルに読み取り権限を付与
chmod 644 /opt/backend/*.py

# .env に読取り権限を所有者のみ付与
chmod 600 /opt/backend/.env

# SELinux コンテキストを整える
$ sudo restorecon -Rv /opt/backend /opt/venvs

3-6. Python実装

今回構築するリソース構成は下記の通りです。

backend/
  app.py
  db.py
  requirements.txt
  .env
  wsgi.py

警告
DBの認証情報をコードにベタ打ちする禁忌を犯していますが、初級編では、まず動かすことを目的に実装します。
OCI Vaultを利用した秘匿化については、中級編以降で触れます。悪しからず…

実装していきます。

app.py
from flask import Flask, request, jsonify
from db import get_db, close_db

app = Flask(__name__)
app.teardown_appcontext(close_db)

@app.get("/health")
def health(): return {"status": "ok"}

@app.get("/api/todos")
def list_todos():
    with get_db().cursor() as cur:
        cur.execute("SELECT id, title, done, created_at FROM todos ORDER BY id DESC")
        return jsonify(cur.fetchall())

@app.post("/api/todos")
def add_todo():
    title = (request.get_json() or {}).get("title","").strip()
    if not title: return {"error":"title required"}, 400
    with get_db().cursor() as cur:
        cur.execute("INSERT INTO todos(title, done) VALUES(%s, 0)", (title,))
    return {"ok": True}, 201

@app.post("/api/todos/<int:todo_id>/toggle")
def toggle(todo_id):
    with get_db().cursor() as cur:
        cur.execute("UPDATE todos SET done = 1 - done WHERE id=%s", (todo_id,))
    return {"ok": True}, 200
db.py
import os, pymysql
from flask import g

def get_db():
    if 'db' not in g:
        g.db = pymysql.connect(
            host=os.getenv('DB_HOST'),
            user=os.getenv('DB_USER'),
            password=os.getenv('DB_PASSWORD'),
            database=os.getenv('DB_NAME'),
            cursorclass=pymysql.cursors.DictCursor,
            autocommit=True
        )
    return g.db

def close_db(e=None):
    db = g.pop('db', None)
    if db: db.close()
requirements.txt
Flask==3.0.3
gunicorn==22.0.0
PyMySQL==1.1.1
python-dotenv==1.0.1

.envに書くパラメータは、後ほど MySQL HeatWave作成後に修正します。

.env
DB_HOST=your-heatwave-endpoint.example.com
DB_USER=appuser
DB_PASSWORD=************
DB_NAME=sampledb
wsgi.py
from app import app
if __name__ == "__main__":
    app.run()

Flask アプリを Gunicorn 経由で systemd サービスとして常駐化するための設定ファイルを作成します。

$ cat | sudo tee /etc/systemd/system/backend.service >/dev/null <<'UNIT'
[Unit]
Description=Gunicorn for Flask App
After=network.target

[Service]
User=opc
WorkingDirectory=/opt/backend
Environment="PYTHONUNBUFFERED=1"
EnvironmentFile=-/opt/backend/.env
ExecStart=/opt/venvs/backend/bin/python -m gunicorn -w 2 -b 0.0.0.0:8000 wsgi:app
Restart=always

[Install]
WantedBy=multi-user.target
UNIT

systemd に反映し、サービスを起動します。

# systemd に反映
$ sudo systemctl daemon-reload

# サービスを有効化(自動起動ON)
$ sudo systemctl enable --now backend
Created symlink /etc/systemd/system/multi-user.target.wants/backend.service  /etc/systemd/system/backend.service.

# サービスを起動
$ sudo systemctl start backend

# ステータス確認
$ sudo systemctl status backend --no-pager
 backend.service - Gunicorn for Flask App
     Loaded: loaded (/etc/systemd/system/backend.service; enabled; preset: disabled)
     Active: active (running) since Sun 2025-08-17 06:12:54 GMT; 4s ago
   Main PID: 79349 (python)
      Tasks: 3 (limit: 72770)
     Memory: 44.2M
        CPU: 188ms
     CGroup: /system.slice/backend.service
             ├─79349 /opt/venvs/backend/bin/python -m gunicorn -w 2 -b 0.0.0.0:8000 wsgi:app
             ├─79350 /opt/venvs/backend/bin/python -m gunicorn -w 2 -b 0.0.0.0:8000 wsgi:app
             └─79351 /opt/venvs/backend/bin/python -m gunicorn -w 2 -b 0.0.0.0:8000 wsgi:app

Aug 17 06:12:54 instance-three-tier-app systemd[1]: Started Gunicorn for Flask App.
Aug 17 06:12:54 instance-three-tier-app python[79349]: [2025-08-17 06:12:54 +0000] [79349] [INFO] Starting gunicorn 22.0.0
Aug 17 06:12:54 instance-three-tier-app python[79349]: [2025-08-17 06:12:54 +0000] [79349] [INFO] Listening at: http://0.0.0.0:8000 (79349)
Aug 17 06:12:54 instance-three-tier-app python[79349]: [2025-08-17 06:12:54 +0000] [79349] [INFO] Using worker: sync
Aug 17 06:12:54 instance-three-tier-app python[79350]: [2025-08-17 06:12:54 +0000] [79350] [INFO] Booting worker with pid: 79350
Aug 17 06:12:54 instance-three-tier-app python[79351]: [2025-08-17 06:12:54 +0000] [79351] [INFO] Booting worker with pid: 79351

# 動作確認
$ curl -s http://127.0.0.1:8000/health
{"status":"ok"}

フロントエンド、バックエンドを構築しました。

image.png
appサーバの構築は以上です。

4. MySQL HeatWaveの構築

DBサーバを構築していきます。
MySQL HeatWaveを利用し、マネージドなMySQL Databaseを構築します。

4-1. MySQL HeatWaveの作成

データベース->HeatWave MySQL->DBシステム を選択し、MySQL HeatWaveの画面に遷移します。
「DBシステムの作成」ボタンを押下し、

テンプレートは "開発またはテスト" を選択します。
DB名には、"three-tier-mysql"としました。
image.png

管理者資格証明の作成にて、任意のユーザ名とパスワードを入力します。
設定は、スタンドアロンを選択します。
ネットワーキングの構成では、1-4-3で作成したDB用のサブネットを選択します。
image.png

HeatWaveクラスタ、自動バックアップは無効にしました。
運用上の通知およびお知らせ用の連絡先に任意のメールアドレスを入力します。

ここまで入力した後、画面左下の「作成」ボタンを押下します。

4-2. MySQL HeatWaveへの接続

DBシステムがアクティブになったら、appサーバから接続していきます。

接続タブを選択し、内部FQDNの編集ラベルをクリックします。
右に「ホスト名の更新」というポップアップ画面が出てくるので、ホスト名に任意の文字列を入力します。
今回は、"threetiermysql"と入力しました。

image.png
このとき、"threetiermysql.privatesubnetdb.threetiervcn.oraclevcn.com"という文字列がappサーバから、MySQL HeatWaveの接続エンドポイントとなります。

appサーバから接続していきます。

# mysql -h <db-private-endpoint> -u username -p
$ mysql -h threetiermysql.privatesubnetdb.threetiervcn.oraclevcn.com -u coenginner -p
Enter password: <password入力> + Enter

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 54
Server version: 8.4.6-cloud MySQL Enterprise - Cloud

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

接続に成功しました。

4-2. スキーマ作成

アプリ用のDBユーザーを作成し、DBとテーブルを作成します。

mysql> CREATE DATABASE sampledb;
Query OK, 1 row affected (0.01 sec)

mysql> CREATE USER 'appuser'@'%' IDENTIFIED BY '************';
Query OK, 0 rows affected (0.00 sec)

mysql> GRANT ALL PRIVILEGES ON sampledb.* TO 'appuser'@'%';
Query OK, 0 rows affected (0.00 sec)

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)

mysql> USE sampledb;
Database changed
mysql> CREATE TABLE todos (
    ->   id INT AUTO_INCREMENT PRIMARY KEY,
    ->   title VARCHAR(255) NOT NULL,
    ->   done TINYINT(1) NOT NULL DEFAULT 0,
    ->   created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    -> );
Query OK, 0 rows affected, 1 warning (0.02 sec)

mysql> show tables;
+--------------------+
| Tables_in_sampledb |
+--------------------+
| todos              |
+--------------------+
1 row in set (0.00 sec)

mysql> SHOW COLUMNS FROM todos FROM sampledb;
+------------+--------------+------+-----+-------------------+-------------------+
| Field      | Type         | Null | Key | Default           | Extra             |
+------------+--------------+------+-----+-------------------+-------------------+
| id         | int          | NO   | PRI | NULL              | auto_increment    |
| title      | varchar(255) | NO   |     | NULL              |                   |
| done       | tinyint(1)   | NO   |     | 0                 |                   |
| created_at | timestamp    | YES  |     | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
+------------+--------------+------+-----+-------------------+-------------------+
4 rows in set (0.00 sec)

4-3. DB接続ファイルの更新

バックエンドからDBへ接続できるようにするため、appサーバで作成したファイル ".env"を更新します。

mysql>exit
$ vi /opt/backend/.env

DB_HOST=threetiermysql.privatesubnetdb.threetiervcn.oraclevcn.com
DB_USER=appuser
DB_PASSWORD=************
DB_NAME=sampledb

Gunicornを再起動します。

$ sudo systemctl restart backend
$ sudo systemctl status backend --no-pager
 backend.service - Gunicorn for Flask App
     Loaded: loaded (/etc/systemd/system/backend.service; enabled; preset: disabled)
     Active: active (running) since Sun 2025-08-17 07:43:06 GMT; 5s ago
   Main PID: 83461 (python)
      Tasks: 3 (limit: 72770)
     Memory: 44.1M
        CPU: 188ms
     CGroup: /system.slice/backend.service
             ├─83461 /opt/venvs/backend/bin/python -m gunicorn -w 2 -b 0.0.0.0:8000 wsgi:app
             ├─83462 /opt/venvs/backend/bin/python -m gunicorn -w 2 -b 0.0.0.0:8000 wsgi:app
             └─83463 /opt/venvs/backend/bin/python -m gunicorn -w 2 -b 0.0.0.0:8000 wsgi:app

Aug 17 07:43:06 instance-three-tier-app systemd[1]: Started Gunicorn for Flask App.
Aug 17 07:43:06 instance-three-tier-app python[83461]: [2025-08-17 07:43:06 +0000] [83461] [INFO] Starting gunicorn 22.0.0
Aug 17 07:43:06 instance-three-tier-app python[83461]: [2025-08-17 07:43:06 +0000] [83461] [INFO] Listening at: http://0.0.0.0:8000 (83461)
Aug 17 07:43:06 instance-three-tier-app python[83461]: [2025-08-17 07:43:06 +0000] [83461] [INFO] Using worker: sync
Aug 17 07:43:06 instance-three-tier-app python[83462]: [2025-08-17 07:43:06 +0000] [83462] [INFO] Booting worker with pid: 83462
Aug 17 07:43:06 instance-three-tier-app python[83463]: [2025-08-17 07:43:06 +0000] [83463] [INFO] Booting worker with pid: 83463

appサーバからDBに接続できているか確認します。
このAPIは、pythonで実装したコードにおける、下記SQLを実行するものです。

SELECT id, title, done, created_at FROM todos ORDER BY id DESC;

今の時点では、Databaseのテーブルには何もデータは入っていないので、空文字[ ] が返ってくれば成功です。

$ curl -s http://127.0.0.1:8000/api/todos
[]

接続に成功していることが確認できました。

5. Flexible Load Balancerの構築

プライベートサブネット上にあるappサーバにインターネット経由で接続するため、パブリックサブネット上にLoad Balancerを作成します。

5-1. Flexible Load Balancerの作成

ネットワーキング->ロード・バランサーを選択し、ロード・バランサの作成画面に遷移します。
遷移後、「ロード・バランサ」の作成ボタンを押下します。

5-1-1. 詳細の追加

ロード・バランサ名に、"lb_threetier" を入力します。
可視性タイプの選択では、パブリックを選択します。
パブリックIPアドレスの割当てでは、エフェメラルIPアドレスを選択します。
シェイプはフレキシブル・シェイプを選択します。
ネットワーキングの選択では、仮想クラウド・ネットワークに "three-tier-vcn"、サブネットに "public-subnet-flb"を選択します。
image.png
image.png
image.png

「次」ボタンを押下します。

5-1-2. バックエンドの選択

ロード・バランシング・ポリシーの指定に、重み付けラウンド・ロビンを選択します。
バックエンド・サーバーの選択で、「インスタンスの追加」ボタンを押下し、""を選択します。
選択後、「インスタンスの追加」ボタンを押下します。
image.png

追加後、ポートが80になっていることを確認します。

バックエンド・セット名に、"bs_lb_threetier"を入力します。
セキュリティ・リストを設定します。

image.png

エグレス・セキュリティ・リストは、Load Balancerからappサーバへのアウトバウンドです。
イングレス・セキュリティ・リストは、appサーバが通信を受けるための、Load Balancerからのインバウンドです。

「次」ボタンを押下します。

5-1-3. リスナーの構成

リスナー名に、"listener_lb_threetier"を入力します。
トラフィックのタイプはHTTP、ポートは80で指定します。
タイムアウトは、60秒としました。
image.png

「次」ボタンを押下します。

5-1-4. ロギングの管理

ロギングの管理は、デフォルト値のままとしました。

「次」ボタンを押下します。

5-1-5. 確認および作成

入力内容を確認し、問題なければ「送信」ボタンを押下します。

5-2. Oracle Linux 9のfirewalld 無効化

ここまでの操作で、Load balancerは作成されますが、バックエンド・セットのヘルスが「クリティカル」になるかと思います。
OS レベルのファイアウォールが HTTP:80を遮断しているためです。下記コマンドでHTTP を通します。

# HTTP サービスを恒久的に許可
sudo firewall-cmd --add-service=http --permanent
sudo firewall-cmd --reload

# 許可されたサービスを確認
sudo firewall-cmd --list-services

バックエンド・セットのヘルスが "OK"となりました。
image.png

5-3. SELinuxの許可設定

このまま動作確認に入ると、nginx→Gunicorn の内部プロキシが失敗し、502エラーが出ます。
これは、SELinuxがnginx→Gunicornの接続をブロックし、nginx(httpd_t)がネットワーク接続するのを既定で拒否するためです。
httpd_can_network_connect を on にし、Nginx が TCP でGunicornに接続できるようにします。

$ sudo setsebool -P httpd_can_network_connect on

最後に、appサーバ上から、正常にパスが通っているか確認します。

$ curl -I http://<Load balancerのパブリックIPアドレス>/health
HTTP/1.1 200 OK
Server: nginx/1.20.1
Date: Sun, 17 Aug 2025 10:24:17 GMT
Content-Type: application/octet-stream
Content-Length: 2
Connection: keep-alive
Content-Type: text/plain

$ curl -s http://<Load balancerのパブリックIPアドレス>/api/todos
[]

ヘルスチェックが200、apiのパスを叩くと、空文字[ ] が返ってくるので正常に動作していることが確認できます。

6. 動作確認

表示確認

ではいざ、ローカルPCのブラウザからにアクセスして、アプリケーションを確認しましょう。

image.png

無事、ブラウザからアプリケーションが表示されました。

操作確認

タスクを入れてみましょう。
image.png

追加ボタンを押すと、、、

image.png

追加されました!

データ確認

DBにデータが追加されているか確認してみます。

mysql> select * from todos;
+----+--------------+------+---------------------+
| id | title        | done | created_at          |
+----+--------------+------+---------------------+
|  1 | 犬の散歩     |    0 | 2025-08-17 10:30:45 |
+----+--------------+------+---------------------+
1 row in set (0.00 sec)

データが入っていることが確認できました。
犬の散歩が終わったとして、チェックボタンをつけてみます。

画面上はこんな感じ
image.png

DBを確認すると、doneカラムにフラグが立って、完了タスクとして認識されました。

mysql> select * from todos;
+----+--------------+------+---------------------+
| id | title        | done | created_at          |
+----+--------------+------+---------------------+
|  1 | 犬の散歩     |    1 | 2025-08-17 10:30:45 |
+----+--------------+------+---------------------+
1 row in set (0.00 sec)

さいごに

思いがけず長編になってしまいました。
最後までお付き合い頂き、ありがとうございます。

OCIで、簡単な Web-App-DB 3層アーキテクチャを作ってみました。
httpアクセスだったり、DBパスワードをベタ打ちだったり、可用性を考慮していなかったりと、本番利用するにはまだまだ考慮すべき点が残っています。

次回は中級編として、OCIのサービスを活用しつつ、より完成度の高いアーキテクチャに仕上げていきます。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?