アーキテクチャ大全とは
AWSでよく見るクラウドアーキテクチャをOCIで作りながら、OCIの基本的なアーキテクチャを学習してみようという企画。
3層アーキテクチャ =初級編=
第一弾は、Web-App-DB 3層アーキテクチャを作っていきます。
AWS アーキテクチャ
クラウドサービス比較
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 アーキテクチャ
エンドユーザーはインターネット経由でFLBにアクセスし、アプリケーションを利用します。
Web層とApp層は1つのCompute VMに共存しています。フロントエンドにVue.js、バックエンドにpythonを採用しました。
Compute VMはプライベートサブネットにあるため、パッケージをインストールする目的でNAT Gatewayを配置します。
DatabaseはMySQL HeatWaveを利用します。
構築アプリケーション概要
今回構築するアプリケーションは、シンプルなタスク管理アプリケーションです。
アプリケーションソースは後ほどGitHubに上げます。
構築手順
- VCNの構築
- 開発用サーバの構築
- Compute VMの構築
- MySQL HeatWaveの構築
- Flexible Load Balancerの構築
- 動作確認
1. VCNの構築
1-1. VCNの作成
OCIのクラウドコンソールから、ネットワーキング>仮想クラウド・ネットワークを選択し、仮想クラウド・ネットワークのサービス画面に遷移します。
遷移後、「VCNの作成」ボタンを押下します。
さらに画面遷移後、IPv4 CIDRブロックに10.0.0.0/16 と入力し、「VCNの作成」ボタンを押下します。
1-2. ゲートウェイの作成
インターネット・ゲートウェイとNATゲートウェイの2つのゲートウェイを作成します。
1-2-1. インターネット・ゲートウェイの作成
VCN作成後、ゲートウェイのタブを選択し、「インターネット・ゲートウェイの作成」ボタンを押下します。
"three-tier-igw"という名前で、インターネット・ゲートウェイを作成します。
インターネット・ゲートウェイが作成されたことを確認できます。
1-2-2. NATゲートウェイの作成
同じ要領で、「NATゲートウェイの作成」ボタンを押下します。
"three-tier-ngw"という名前で、NATゲートウェイを作成します。
NATゲートウェイが作成されたことを確認できます。
1-3. ルート表の作成
ルーティングのタブを選択します。"Default Route Table for three-tier-vcn"という名前で既にルート表が存在していることが確認できます。このルート表はプライベートサブネット用のルートテーブルとして利用します。
1-3-1. パブリックサブネット用のルート表の作成
インターネット・ゲートウェイにルートを持つパブリックサブネット用のルート表を作成しましょう。
「ルート表の作成」ボタンを押下します。
"three-tier-pub-route"という名前で、パブリックサブネット用のルートテーブルを作成します。
ルート・ルールに、インターネット・ゲートウェイとのルート・ルールを追加します。
ターゲット・タイプにて"インターネット・ゲートウェイ"を選択し、宛先CIDRブロックに "0.0.0.0/0" を入力します。ターゲット・インターネット・ゲートウェイに、1-2-1で作成した"three-tier-igw"を選択します。
その後、「作成」ボタンを押下します。
ルート表が追加されていることが確認できます。
1-3-2. プライベートサブネット用のルート表の編集
次に、プライベートサブネット用のルート表を編集します。
"Default Route Table for three-tier-vcn"のラベルを選択し、画面遷移します。
ルート・ルールタブを押下し、「ルート・ルールの追加」ボタンを押下します。
ルート・ルールに、NATゲートウェイとのルート・ルールを追加します。
ターゲット・タイプにて"NATゲートウェイ"を選択し、宛先CIDRブロックに "0.0.0.0/0" を入力します。ターゲット・NATゲートウェイに、1-2-1で作成した"three-tier-ngw"を選択します。
その後、「作成」ボタンを押下します。
ルート表が追加されていることが確認できます。
1-4. サブネットの作成
サブネットのタブを選択し、「サブネットの作成」ボタンを押下します。
1-4-1. パブリックサブネットの作成
"public-subnet-flb"という名前のパブリックサブネットを作成します。
名前に"public-subnet-flb"と入力し、IPv4 CIDRブロックに"10.0.0.0/24"を入力します。
IPv6接頭辞の下で、ルート表に"three-tier-pub-route"を選択します。
サブネット・アクセスは"パブリック・サブネット"を選択します。
セキュリティ・リストは後ほど設定するので、設定しないでください。
ここまで入力、選択できたら「サブネットの作成」ボタンを押下します。
しばらくすると、パブリックサブネットが作成されたことを確認できます。
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"を選択します。
サブネット・アクセスは"プライベート・サブネット"を選択します。
セキュリティ・リストは後ほど設定するので、設定しないでください。
ここまで入力、選択できたら「サブネットの作成」ボタンを押下します。
しばらくすると、プライベートサブネットが作成されたことを確認できます。
1-4-3. プライベートサブネットの作成 -db用
"private-subnet-db"という名前のプライベートサブネットを作成します。
サブネット名に"private-subnet-db"を入力し、IPv4 CIDRブロックに"10.0.2.0/24"を入力します。
上記以外は、1-4-2と手順は同じなので割愛します。
ここまでの手順で、サブネットが3つできたことを確認できます。
1-5. セキュリティ・リストの作成
FLB,Compute,MySQL HeatWaveが配置されるサブネット毎に、別々のセキュリティ・リストを用意します。
1-5-1. FLB用のセキュリティ・リストの作成
セキュリティのタブを選択し、「セキュリティ・リストの作成」ボタンを押下します。
名前に、"security-list-lb"と入力します。
イングレースのルール許可に、FLB用のルールを追加します。
ソースCIDRに"0.0.0.0/0"、ソース・ポート範囲に"ALL"、宛先ポート範囲に"80,443"を入力し、「イングレス・ルールの追加」ボタンを押下します。
1-5-2. Compute用のセキュリティ・リストの作成
同じ要領で追加していきます。
名前に、"security-list-compute"と入力します。
イングレースのルール許可に、Compute用のルールを追加します。
イングレス・ルール1に、ソースCIDRに"10.0.0.0/24"、宛先ポート範囲に"80"を入力します。
のちに、ComputeへSSHしてサーバーの設定を行うためのイングレス・ルールも設定しておきましょう。
パブリックサブネット上に踏み台・運用サーバとしてCompute VMから接続することを想定します。
イングレス・ルール2に、ソースCIDRに"10.0.0.0/24"、宛先ポート範囲に"22"を入力します。
「イングレス・ルールの追加」ボタンを押下します。
セキュリティ・リストが追加されたことを確認できます。
1-5-3. MySQL HeatWave用のセキュリティ・リストの作成
同じ要領で追加していきます。
名前に、"security-list-db"と入力します。
イングレースのルール許可に、DB用のルールを追加します。
イングレス・ルール1に、ソースCIDRに"10.0.1.0/24"、宛先ポート範囲に"3306"を入力します。
「イングレス・ルールの追加」ボタンを押下します。
ここまでの手順で、セキュリティ・リストが3つできたことを確認できます。
1-5-4. セキュリティ・リストとサブネットの紐づけ
セキュリティ・リストとサブネットの対応表は下記の通りです。
セキュリティ・リスト | サブネット | パブリック/プライベート |
---|---|---|
security-list-lb | public-subnet-flb | パブリック |
security-list-compute | private-subnet-app | プライベート |
security-list-db | private-subnet-db | プライベート |
サブネットの画面に遷移し、public-subnet-flb の画面に遷移します。
セキュリティタブを選択して、「セキュリティ・リストの追加」ボタンを押下します。
セキュリティ・リストに"security-list-lb"を選択し、「セキュリティ・リストの追加」ボタンを押下します。
この操作を、"private-subnet-app"と"private-subnet-db"においても行います。
ここまでの手順で作成されたリソースを図解すると、このようになります。
2. 開発用サーバの構築
パブリックサブネット "public-subnet-flb"にCompute VMを作成し、Cloud Shellから接続します。
Compute VMの作成方法については、下記を参照してください。
OSはOracle Linux9, ShapeはVM.Standard.E4.Flexを選択しました。
この開発用サーバから、このあとプライベートサブネット上に作成するCompute VMへ接続します。
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を下記コードに書き換えます。
<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を利用した秘匿化については、中級編以降で触れます。悪しからず…
実装していきます。
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
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()
Flask==3.0.3
gunicorn==22.0.0
PyMySQL==1.1.1
python-dotenv==1.0.1
.envに書くパラメータは、後ほど MySQL HeatWave作成後に修正します。
DB_HOST=your-heatwave-endpoint.example.com
DB_USER=appuser
DB_PASSWORD=************
DB_NAME=sampledb
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"}
フロントエンド、バックエンドを構築しました。
4. MySQL HeatWaveの構築
DBサーバを構築していきます。
MySQL HeatWaveを利用し、マネージドなMySQL Databaseを構築します。
4-1. MySQL HeatWaveの作成
データベース->HeatWave MySQL->DBシステム を選択し、MySQL HeatWaveの画面に遷移します。
「DBシステムの作成」ボタンを押下し、
テンプレートは "開発またはテスト" を選択します。
DB名には、"three-tier-mysql"としました。
管理者資格証明の作成にて、任意のユーザ名とパスワードを入力します。
設定は、スタンドアロンを選択します。
ネットワーキングの構成では、1-4-3で作成したDB用のサブネットを選択します。
HeatWaveクラスタ、自動バックアップは無効にしました。
運用上の通知およびお知らせ用の連絡先に任意のメールアドレスを入力します。
ここまで入力した後、画面左下の「作成」ボタンを押下します。
4-2. MySQL HeatWaveへの接続
DBシステムがアクティブになったら、appサーバから接続していきます。
接続タブを選択し、内部FQDNの編集ラベルをクリックします。
右に「ホスト名の更新」というポップアップ画面が出てくるので、ホスト名に任意の文字列を入力します。
今回は、"threetiermysql"と入力しました。
このとき、"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"を選択します。
「次」ボタンを押下します。
5-1-2. バックエンドの選択
ロード・バランシング・ポリシーの指定に、重み付けラウンド・ロビンを選択します。
バックエンド・サーバーの選択で、「インスタンスの追加」ボタンを押下し、""を選択します。
選択後、「インスタンスの追加」ボタンを押下します。
追加後、ポートが80になっていることを確認します。
バックエンド・セット名に、"bs_lb_threetier"を入力します。
セキュリティ・リストを設定します。
エグレス・セキュリティ・リストは、Load Balancerからappサーバへのアウトバウンドです。
イングレス・セキュリティ・リストは、appサーバが通信を受けるための、Load Balancerからのインバウンドです。
「次」ボタンを押下します。
5-1-3. リスナーの構成
リスナー名に、"listener_lb_threetier"を入力します。
トラフィックのタイプはHTTP、ポートは80で指定します。
タイムアウトは、60秒としました。
「次」ボタンを押下します。
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
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のブラウザからにアクセスして、アプリケーションを確認しましょう。
無事、ブラウザからアプリケーションが表示されました。
操作確認
追加ボタンを押すと、、、
追加されました!
データ確認
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)
データが入っていることが確認できました。
犬の散歩が終わったとして、チェックボタンをつけてみます。
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のサービスを活用しつつ、より完成度の高いアーキテクチャに仕上げていきます。