0
0

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 アーキテクチャ大全】Container Instancesで構築するコンテナアーキテクチャ =初級編=

Last updated at Posted at 2025-10-05

アーキテクチャ大全とは

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

コンテナアーキテクチャ =初級編=

第二弾は、コンテナアーキテクチャです。

Container Instancesとは

OCI Container Instancesは、サーバーレスでコンテナを実行できるマネージドサービスです。

特徴

  • インフラ管理不要: サーバーのプロビジョニングや管理が不要で、コンテナイメージさえあればすぐにデプロイ可能
  • 迅速なデプロイ: コンテナイメージから数秒~数分でデプロイが完了
  • 柔軟なスケーリング: 必要に応じてOCPUとメモリを調整可能
  • コスト効率: 使用した分だけの課金で、停止時は課金されない
  • セキュア: VCN内のプライベートサブネットに配置可能

制約事項

  • SSH接続不可: コンテナに直接SSH接続はできません(デバッグはログやコンテナExec機能を利用)
  • 永続ストレージ: デフォルトではエフェメラルストレージのみ

今回は、ステートレスなタスク管理APIを構築するため、Container Instancesが最適です。

AWSアーキテクチャ

以下のAWS構成をOCIで再現していきます。

CleanShot 2025-09-18 at 16.37.44.png

クラウドサービス比較

AWS OCI 役割
VPC VCN 仮想ネットワーク
Application Load Balancer Network Load Balancer ロードバランサー
Amazon ECS Container Instance コンテナ実行環境
Amazon Aurora MySQL HeatWave データベース

上記がAWSの各サービスに対応するOCIのサービスです。

Container Instance にAPIコンテナを、DB層にMySQL HeatWaveを採用します。

OCIアーキテクチャ

image.png

エンドユーザーはインターネット経由でNLBにアクセスし、APIを利用します。

DatabaseはMySQL HeatWaveを利用します。

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

今回構築するアプリケーションは、シンプルなタスク管理APIです。

CleanShot 2025-10-06 at 02.41.50.png

前提条件

この手順を実施するにあたり、以下の前提条件があります。

管理用Computeインスタンスの準備

本手順では、アプリケーションのデプロイ、MySQL HeatWaveへの接続テスト、Container InstanceのAPI接続テスト、などを行うため、パブリックサブネット上に管理用Computeインスタンスを用意します。
OCIへ適切な接続ができるローカルPCや環境があれば、適宜そちらをお使いください。

構築手順

  1. VCNの構築
  2. アプリケーションの準備
  3. MySQL HeatWaveの構築
  4. Container Instanceの構築
  5. Network Load Balancer (NLB) の構築
  6. 動作確認

1. VCNの構築

1-1. VCNの作成

OCIのクラウドコンソールから、ネットワーキング>仮想クラウド・ネットワークを選択し、仮想クラウド・ネットワークのサービス画面に遷移します
CleanShot 2025-10-05 at 17.03.25.png

遷移後、「VCNの作成」ボタンを押下します

項目 設定値 説明
名前 demo-vcn VCNの名前
IPv4 CIDRブロック 10.0.0.0/16 VCN全体のIPアドレス範囲

「VCNの作成」ボタンを押下します。

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

プライベートサブネットとパブリックサブネットで適切な通信を行うため、3つのゲートウェイを作成します。

ゲートウェイ 用途 説明
Internet Gateway パブリックサブネット用 Load Balancerがインターネットと通信
NAT Gateway プライベートサブネット用 Container Instancesが外部インターネットにアクセス
Service Gateway プライベートサブネット用 OCIRなどOCIサービスへの高速・セキュアなアクセス

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

VCN作成後、ゲートウェイのタブを選択し、「インターネット・ゲートウェイの作成」ボタンを押下します
CleanShot 2025-10-05 at 17.09.49.png

名前: demo-igw という名前を入れ、「インターネット・ゲートウェイの作成」ボタンを押下します

インターネット・ゲートウェイが作成されたことを確認します

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

同様に、「NATゲートウェイの作成」ボタンを押下します。
CleanShot 2025-10-05 at 17.16.30.png

名前: demo-natgw という名前を入れ、「NATゲートウェイの作成_ボタンを押下します

NATゲートウェイが作成されたことを確認します

1-2-3. サービス・ゲートウェイの作成

同様に、「サービス・ゲートウェイの作成」ボタンを押下します
CleanShot 2025-10-05 at 17.17.49.png

以下の情報を入力します

項目 設定値 説明
名前 demo-sgw サービス・ゲートウェイの名前
サービス All <region> Services In Oracle Services Network 東京リージョンの場合: All NRT Services In Oracle Services Network

この設定により、OCIR、Object Storage、その他のOCIサービスへのプライベートアクセスが可能になります。
CleanShot 2025-10-05 at 17.21.27.png

「サービス・ゲートウェイの作成」ボタンを押下します。

サービス・ゲートウェイが作成されたことを確認できます。
ここまでで、3つのゲートウェイ(インターネット・ゲートウェイ、NATゲートウェイ、サービス・ゲートウェイ)が作成されました。

1-3. ルート表の作成

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

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

インターネット・ゲートウェイにルートを持つパブリックサブネット用のルート表を作成します。

「ルート表の作成」ボタンを押下します
CleanShot 2025-10-05 at 17.30.26.png

項目 設定値 説明
名前 demo-rtb-public パブリックサブネット用のルートテーブル名
ルート・ルール
「+別のルート・ルール」を押下
ターゲット・タイプ: インターネット・ゲートウェイ
宛先CIDRブロック: 0.0.0.0/0
ターゲット・インターネット・ゲートウェイ: demo-igw
全ての通信をインターネット・ゲートウェイにルーティング

その後、「作成」ボタンを押下し、ルート表が追加されていることが確認します
CleanShot 2025-10-05 at 17.39.52.png

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

次に、プライベートサブネット用のルート表を編集します。
"Default Route Table for demo-vcn"のラベルを選択し、画面遷移します。

プライベートサブネットからは、OCIサービスと外部インターネットの両方にアクセスできるよう、2つのルート・ルールを追加します。

CleanShot 2025-10-05 at 17.40.42.png

ルート・ルール1: サービス・ゲートウェイ経由のOCIサービスへのアクセス

項目 設定値 説明
ターゲット・タイプ サービス・ゲートウェイ OCIサービスへのアクセス用
宛先サービス All <region> Services In Oracle Services Network 東京リージョンの場合: All NRT Services In Oracle Services Network
ターゲット・サービス・ゲートウェイ demo-sgw 1-2-3で作成したサービス・ゲートウェイ

「別のルート・ルールの追加」ボタンを押下します。

ルート・ルール2: NATゲートウェイ経由の外部インターネットへのアクセス

項目 設定値 説明
ターゲット・タイプ NATゲートウェイ 外部インターネットへのアクセス用
宛先CIDRブロック 0.0.0.0/0 全ての通信をNATゲートウェイにルーティング
ターゲット・NATゲートウェイ demo-natgw 1-2-2で作成したNATゲートウェイ

すべてのルール入力後、「ルート・ルールの追加」ボタンを押下します

2つのルート・ルールが追加されたことを確認します
CleanShot 2025-10-05 at 17.43.27.png

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

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

セキュリティのタブを選択し、「セキュリティ・リストの作成」ボタンを押下します
CleanShot 2025-10-05 at 18.24.55.png

1-4-1. パブリックサブネット用のセキュリティ・リストの作成

項目 設定値 説明
名前 demo-seclist-public パブリックサブネット用セキュリティ・リストの名前
イングレス・ルール1 ソースCIDR: 0.0.0.0/0
宛先ポート範囲: 22
SSH接続用(管理用Computeインスタンス)
イングレス・ルール2 ソースCIDR: 0.0.0.0/0
宛先ポート範囲: 80,443
Load Balancer用HTTP(80)とHTTPS(443)ポート
エグレス・ルール 宛先CIDR: 0.0.0.0/0
宛先ポート範囲: ALL
全通信許可(デフォルト設定)

1-4-2. Container Instance用のセキュリティ・リストの作成

項目 設定値 説明
名前 demo-seclist-app Container Instance用セキュリティ・リストの名前
イングレス・ルール1 ソースCIDR: 10.0.0.0/24
宛先ポート範囲: 8000
パブリックサブネットからのFlaskアプリ(8000)へのアクセス許可
エグレス・ルール 宛先CIDR: 0.0.0.0/0
宛先ポート範囲: ALL
全通信許可(デフォルト設定)

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

項目 設定値 説明
名前 demo-seclist-db MySQL HeatWave用セキュリティ・リストの名前
イングレス・ルール1 ソースCIDR: 10.0.1.0/24
宛先ポート範囲: 3306
アプリサブネットからのMySQL(3306)へのアクセス許可
イングレス・ルール2 ソースCIDR: 10.0.0.0/24
宛先ポート範囲: 3306
パブリックサブネット(管理用Compute)からのMySQL(3306)へのアクセス許可
エグレス・ルール 宛先CIDR: 0.0.0.0/0
宛先ポート範囲: ALL
全通信許可(デフォルト設定)

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

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

サブネットのタブを選択し、「サブネットの作成」ボタンを押下します。
CleanShot 2025-10-05 at 17.54.57.png

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

パブリックサブネットを作成します。

項目 設定値 説明
名前 demo-subnet-public パブリックサブネットの名前
IPv4 CIDRブロック 10.0.0.0/24 パブリックサブネットのIPアドレス範囲
ルート表 demo-rtb-public 1-3-1で作成したパブリック用ルート表
サブネット・アクセス パブリック・サブネット インターネットからアクセス可能
セキュリティ・リスト demo-seclist-public 1-4-1で作成したパブリックサブネット用セキュリティ・リスト

「サブネットの作成」ボタンを押下します

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

Container Instance用のプライベートサブネットを作成します。

項目 設定値 説明
名前 demo-subnet-app アプリケーション用プライベートサブネットの名前
IPv4 CIDRブロック 10.0.1.0/24 アプリケーション用プライベートサブネットのIPアドレス範囲
ルート表 Default Route Table for demo-vcn 1-3-2で編集したプライベート用ルート表
サブネット・アクセス プライベート・サブネット インターネットから直接アクセス不可
セキュリティ・リスト demo-seclist-app 1-4-2で作成したContainer Instance用セキュリティ・リスト

「サブネットの作成」ボタンを押下します

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

MySQL HeatWave用のプライベートサブネットを作成します

項目 設定値 説明
名前 demo-subnet-db データベース用プライベートサブネットの名前
IPv4 CIDRブロック 10.0.2.0/24 データベース用プライベートサブネットのIPアドレス範囲
ルート表 Default Route Table for demo-vcn 1-3-2で編集したプライベート用ルート表
サブネット・アクセス プライベート・サブネット インターネットから直接アクセス不可
セキュリティ・リスト demo-seclist-db 1-4-3で作成したMySQL HeatWave用セキュリティ・リスト

「サブネットの作成」ボタンを押下します

ここまでの手順で、サブネットが3つできたことを確認します
CleanShot 2025-10-05 at 18.20.01.png

1-6. 管理用Computeインスタンスの構築

VCN構築完了後、管理用Computeインスタンスを作成します。

1-6-1. インスタンスの作成

  1. OCIコンソールから「コンピュート」→「インスタンス」を選択し、「インスタンスの作成」ボタンを押下します
項目 設定値 説明
名前 demo-mgmt-server 管理用Computeインスタンスの名前
コンパートメント 現在のコンパートメント デフォルト設定
可用性ドメイン AD-1 任意の可用性ドメイン
イメージ Oracle Linux 9 最新のOracle Linux 9イメージ
シェイプ VM.Standard.E2 任意でOK。最小構成(Always Free対象)
ネットワーキング 既存のVCNを選択 VCN: demo-vcn、サブネット: demo-subnet-public
SSHキー 公開キーの貼付けまたはアップロード 接続用のSSHキーペア

1-6-2. インスタンスの詳細確認

作成完了後、以下の情報を確認します:

項目 説明
パブリックIPアドレス SSH接続時に使用
プライベートIPアドレス VCN内での通信に使用
ステータス 「実行中」であることを確認

1-6-3. 必要なツールのインストール

SSH接続後、以下のコマンドで各種ツールをインストールします:

# Oracle Linux 9の場合
sudo dnf update -y
sudo dnf install mysql -y
sudo dnf install docker -y

1-6-4. 接続確認

# SSH接続例
ssh -i <秘密鍵のパス> opc@<パブリックIPアドレス>

# MySQL Clientのインストール確認
mysql --version

注意:

  • 本番環境では、Bastionサービスやプライベート接続を使用したセキュアな管理方法を検討してください

image.png

2. アプリケーションの準備

Container Instanceで稼働させるTODO管理アプリケーションのソースコードを準備します。

2-1. アプリケーションの概要

本記事では、GUI対応のTODO管理アプリケーションを構築します。以下の機能を提供します:

  • タスクの追加: 新しいTODOアイテムの作成
  • タスクの完了: 完了/未完了の切り替え
  • タスクの削除: 不要なタスクの削除

2-2. ファイル構成

アプリケーションのファイル構成は下記の通りです。

container-api/
├── Dockerfile
├── requirements.txt
└── app.py

2-3. ソースコード

2-3-1. requirements.txt

Flask==2.3.3
PyMySQL==1.1.0
gunicorn==21.2.0
cryptography==41.0.7

2-3-2. app.py

すごく長いので折りたたみ表示
from flask import Flask, request, jsonify, render_template_string
import pymysql
import os
from datetime import datetime

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False  # 日本語をエスケープしない

# データベース接続設定
DB_CONFIG = {
    'host': os.getenv('DB_HOST'),
    'user': os.getenv('DB_USER'),
    'password': os.getenv('DB_PASSWORD'),
    'database': os.getenv('DB_NAME'),
    'port': 3306,
    'charset': 'utf8mb4'
}

def get_db_connection():
    return pymysql.connect(**DB_CONFIG)

# HTMLテンプレート
HTML_TEMPLATE = '''
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TODO管理アプリ - Container Instance</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        .header { background: #2c3e50; color: white; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
        .todo-form { background: #ecf0f1; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
        .todo-form input[type="text"] { width: 70%; padding: 10px; border: 1px solid #bdc3c7; border-radius: 3px; }
        .todo-form button { padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 3px; cursor: pointer; margin-left: 10px; }
        .todo-form button:hover { background: #2980b9; }
        .todo-item { background: white; border: 1px solid #bdc3c7; padding: 15px; margin: 10px 0; border-radius: 5px; display: flex; justify-content: space-between; align-items: center; }
        .todo-item.completed { background: #d5f4e6; border-color: #27ae60; }
        .todo-item.completed .todo-title { text-decoration: line-through; color: #7f8c8d; }
        .todo-title { flex-grow: 1; margin-right: 15px; }
        .todo-actions button { padding: 5px 10px; margin: 0 5px; border: none; border-radius: 3px; cursor: pointer; }
        .toggle-btn { background: #f39c12; color: white; }
        .toggle-btn:hover { background: #e67e22; }
        .delete-btn { background: #e74c3c; color: white; }
        .delete-btn:hover { background: #c0392b; }
        .message { padding: 10px; margin: 10px 0; border-radius: 3px; }
        .success { background: #d5f4e6; color: #27ae60; border: 1px solid #27ae60; }
        .error { background: #fadbd8; color: #e74c3c; border: 1px solid #e74c3c; }
    </style>
</head>
<body>
    <div class="header">
        <h1>📝 TODO管理アプリ</h1>
        <p>OCI Container Instance + MySQL HeatWave で構築</p>
    </div>

    <div class="todo-form">
        <form id="todoForm">
            <input type="text" id="todoTitle" placeholder="新しいタスクを入力してください..." required>
            <button type="submit">追加</button>
        </form>
    </div>

    <div id="message"></div>

    <div id="todos">
        <h2>タスク一覧</h2>
        <div id="todoList">
            <!-- TODOリストがここに動的に挿入されます -->
        </div>
    </div>

    <script>
        // ページ読み込み時にTODOリストを取得
        loadTodos();

        // フォーム送信処理
        document.getElementById('todoForm').addEventListener('submit', async function(e) {
            e.preventDefault();
            const title = document.getElementById('todoTitle').value.trim();
            if (!title) return;

            try {
                const response = await fetch('/todos', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ title: title })
                });

                if (response.ok) {
                    document.getElementById('todoTitle').value = '';
                    loadTodos();
                    showMessage('タスクが追加されました!', 'success');
                } else {
                    showMessage('エラーが発生しました', 'error');
                }
            } catch (error) {
                showMessage('接続エラーが発生しました', 'error');
            }
        });

        // TODOリストの読み込み
        async function loadTodos() {
            try {
                const response = await fetch('/todos');
                const todos = await response.json();
                displayTodos(todos);
            } catch (error) {
                showMessage('TODOリストの読み込みに失敗しました', 'error');
            }
        }

        // TODOリストの表示
        function displayTodos(todos) {
            const todoList = document.getElementById('todoList');
            if (todos.length === 0) {
                todoList.innerHTML = '<p>タスクがありません。新しいタスクを追加してください。</p>';
                return;
            }

            todoList.innerHTML = todos.map(todo => `
                <div class="todo-item ${todo.done ? 'completed' : ''}">
                    <div class="todo-title">${todo.title}</div>
                    <div class="todo-actions">
                        <button class="toggle-btn" onclick="toggleTodo(${todo.id}, ${todo.done})">
                            ${todo.done ? '未完了に戻す' : '完了'}
                        </button>
                        <button class="delete-btn" onclick="deleteTodo(${todo.id})">削除</button>
                    </div>
                </div>
            `).join('');
        }

        // TODOの完了/未完了切り替え
        async function toggleTodo(id, currentDone) {
            try {
                const response = await fetch(`/todos/${id}`, {
                    method: 'PUT',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ done: currentDone ? 0 : 1 })
                });

                if (response.ok) {
                    loadTodos();
                    showMessage('タスクが更新されました!', 'success');
                } else {
                    showMessage('更新に失敗しました', 'error');
                }
            } catch (error) {
                showMessage('接続エラーが発生しました', 'error');
            }
        }

        // TODOの削除
        async function deleteTodo(id) {
            if (!confirm('このタスクを削除しますか?')) return;

            try {
                const response = await fetch(`/todos/${id}`, {
                    method: 'DELETE'
                });

                if (response.ok) {
                    loadTodos();
                    showMessage('タスクが削除されました!', 'success');
                } else {
                    showMessage('削除に失敗しました', 'error');
                }
            } catch (error) {
                showMessage('接続エラーが発生しました', 'error');
            }
        }

        // メッセージ表示
        function showMessage(message, type) {
            const messageDiv = document.getElementById('message');
            messageDiv.innerHTML = `<div class="message ${type}">${message}</div>`;
            setTimeout(() => {
                messageDiv.innerHTML = '';
            }, 3000);
        }
    </script>
</body>
</html>
'''

@app.route('/')
def index():
    return render_template_string(HTML_TEMPLATE)

@app.route('/health')
def health_check():
    return jsonify({'status': 'healthy', 'service': 'TODO Management App'}), 200

@app.route('/todos', methods=['GET'])
def get_todos():
    try:
        conn = get_db_connection()
        cursor = conn.cursor(pymysql.cursors.DictCursor)
        cursor.execute("SELECT * FROM todos ORDER BY created_at DESC")
        todos = cursor.fetchall()
        cursor.close()
        conn.close()
        return jsonify(todos)
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/todos', methods=['POST'])
def create_todo():
    try:
        data = request.get_json()
        title = data.get('title')
        if not title:
            return jsonify({'error': 'Title is required'}), 400
        
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute("INSERT INTO todos (title, done) VALUES (%s, %s)", (title, 0))
        conn.commit()
        todo_id = cursor.lastrowid
        cursor.close()
        conn.close()
        
        return jsonify({'id': todo_id, 'title': title, 'done': 0}), 201
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/todos/<int:todo_id>', methods=['PUT'])
def update_todo(todo_id):
    try:
        data = request.get_json()
        done = data.get('done')
        
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute("UPDATE todos SET done = %s WHERE id = %s", (done, todo_id))
        conn.commit()
        cursor.close()
        conn.close()
        
        return jsonify({'message': 'Todo updated successfully'})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/todos/<int:todo_id>', methods=['DELETE'])
def delete_todo(todo_id):
    try:
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute("DELETE FROM todos WHERE id = %s", (todo_id,))
        conn.commit()
        cursor.close()
        conn.close()
        
        return jsonify({'message': 'Todo deleted successfully'})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000, debug=False)

2-3-3. Dockerfile

FROM python:3.11-slim

WORKDIR /app

# 依存関係をインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# アプリケーションファイルをコピー
COPY . .

# 非rootユーザーを作成
RUN adduser --disabled-password --gecos '' appuser
RUN chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:8000", "app:app"]

2-4. Container Registryへの登録

OCIコンソールから「開発者サービス」>「コンテナ・レジストリ」を選択し、コンテナレジストリのサービス画面に遷移します。
CleanShot 2025-10-05 at 18.38.05.png

遷移後、「リポジトリの作成」ボタンを押下し、以下の設定でリポジトリを作成します。

項目 設定値 説明
名前 container-api コンテナイメージ用リポジトリの名前
アクセス設定 プライベート セキュリティ強化のためプライベート設定

CleanShot 2025-10-05 at 19.31.56.png

2-5. イメージのビルドとプッシュ

Container RegistryにイメージをプッシュするためにAuth Tokenが必要です。

2-5-1. Auth Tokenの取得

OCIコンソールにログインし、以下の手順でAuth Tokenを生成します。

  1. OCIコンソール右上のプロファイルアイコンをクリックし、表示されるメニューから「ユーザー設定」を選択します
    CleanShot 2025-10-05 at 19.45.53.png

  2. 左側メニューの「リソース」セクションから「認証トークン」を選択し、「トークンの作成」ボタンを押下します
    CleanShot 2025-10-05 at 19.46.56.png

  3. 説明欄に「Container Registry用」など、わかりやすい説明を入力し、「トークンの生成」ボタンを押下します

  4. 生成されたトークンが表示されます。このトークンは再表示できないため、必ずコピーして安全な場所に保存してください。
    CleanShot 2025-10-05 at 19.48.42.png

2-5-2. 必要な情報の確認

docker loginコマンドで使用する以下の情報を確認します。

region-key(リージョンキー)

リージョンに応じたキーを使用します。主要なリージョンキーは以下の通りです:

リージョン リージョンキー
東京リージョン nrt
大阪リージョン kix

その他のリージョンキーはOCIドキュメントを参照してください。

※本手順では ソウルリージョン: icn にて実施しています。

tenancy-namespace(テナンシ・ネームスペース)
  1. OCIコンソール右上のプロファイルアイコンをクリックし、表示されるメニューから「テナンシ:<テナンシ名>」を選択します
  2. 「オブジェクト・ストレージ・ネームスペース」の値をコピー
    CleanShot 2025-10-05 at 20.01.17.png
username(ユーザー名)

OCIのユーザー名です。以下のいずれかの形式になります:

  • 一般的なOCIユーザー: user@example.com または username
  • SSO・IDCSなどのフェデレーション・ユーザー: oracleidentitycloudservice/user@example.com

2-5-3. 管理用Computeでのイメージビルドとプッシュ

上記で確認した情報を使用して、管理用Computeインスタンスで以下のコマンドを実行します。

手順:

  1. 管理用ComputeインスタンスにSSH接続します。

  2. アプリケーションファイルを準備します。

# 作業ディレクトリを作成
mkdir -p ~/container-api
cd ~/container-api

# 各ファイルを作成(2-1のコードを使用)
# 以下のいずれかの方法でファイルを作成してください:

# 方法1: viエディタを使用
vi requirements.txt
vi app.py
vi Dockerfile

# 方法2: catコマンドでヒアドキュメントを使用
cat > requirements.txt << 'EOF'
Flask==3.0.3
gunicorn==22.0.0
PyMySQL==1.1.1
python-dotenv==1.0.1
EOF

# 方法3: ローカルで作成し、アップロード

Tip: Cloud Shellのエディタ機能を使うと、ブラウザ上で快適に編集できます。Cloud Shellツールバーの「コードエディタ」アイコンをクリック

  1. Container Registryにログインし、イメージのビルドとPushを実行

# Container Registryにログイン
echo "<auth-token>" | docker login <region-key>.ocir.io -u <tenancy-namespace>/<username> --password-stdin

# イメージのビルド
docker build -t <region-key>.ocir.io/<tenancy-namespace>/container-api:latest .

# イメージのプッシュ
docker push <region-key>.ocir.io/<tenancy-namespace>/container-api:latest

プッシュが成功すると、OCIコンソールのContainer Registryの画面でイメージが確認できます。

CleanShot 2025-10-05 at 20.57.13.png

3. MySQL HeatWaveの構築

DBサーバを構築していきます。

MySQL HeatWaveを利用し、マネージドなMySQL Databaseを構築します。

3-1. MySQL HeatWaveの作成

データベース→HeatWave MySQL→DBシステム を選択し、MySQL HeatWaveの画面に遷移します。
CleanShot 2025-10-05 at 21.08.53.png
「DBシステムの作成」ボタンを押下します

項目 設定値 説明
テンプレート 開発またはテスト 開発・検証用途に適した設定
名前 demo-mysql MySQL HeatWaveインスタンスの名前
管理者資格証明 ユーザー名: admin
パスワード: 任意の強力なパスワード
DB管理者アカウント
設定 スタンドアロン 単一インスタンス構成
ネットワーキング demo-subnet-db 1-4-3で作成したDB用プライベートサブネット
HeatWaveクラスタ 無効 今回は使用しない
自動バックアップ 無効 検証用途のため無効化
運用通知連絡先 任意のメールアドレス 障害通知等の受信用

画面左下の「作成」ボタンを押下します
※作成完了に30分ぐらいかかります

CleanShot 2025-10-05 at 21.20.59.png

3-2. 管理用ComputeインスタンスからMySQL HeatWaveへの接続

DBシステムがアクティブになったら、管理用Computeインスタンスから接続します。

3-2-1. 接続情報の確認

MySQL HeatWaveの接続情報を確認します。

  1. OCIコンソールのMySQL HeatWave画面で「接続」タブを選択
  2. 「内部FQDNの編集」をクリック
  3. ホスト名に demo-mysql を入力

CleanShot 2025-10-05 at 21.29.56.png

以下のFQDNが接続エンドポイントとなります:

demo-mysql.demosubnetdb.demovcn.oraclevcn.com

3-2-2. 管理用Computeインスタンスでの接続

管理用ComputeインスタンスにSSH接続して、MySQLクライアントで接続します。

# 管理用ComputeインスタンスにSSH接続
ssh opc@<管理用ComputeのパブリックIP>

# MySQLクライアントで接続
mysql -h demo-mysql.demosubnetdb.demovcn.oraclevcn.com -u admin -p

パスワードを入力すると、MySQL HeatWaveに接続できます。

Enter password: <password入力>
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 221
Server version: 8.4.6-u2-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>

接続に成功しました。

3-3. スキーマ作成

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

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

mysql> CREATE USER 'appuser'@'%' IDENTIFIED BY 'your_secure_password_here';
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. Container Instanceの構築

プライベートサブネット demo-subnet-app に、APIコンテナを稼働させるContainer Instanceを構築します。

4-1. Container Instanceの作成

OCIのクラウドコンソールから、開発者サービス > コンテナ・インスタンス を選択し、Container Instanceのサービス画面に遷移します
CleanShot 2025-10-05 at 23.05.43.png

遷移後、「Container Instanceの作成」ボタンを押下します

Container Instanceの作成は単一の画面で行います。以下の項目を順番に入力・選択していきます。

基本情報の設定

項目 設定値 説明
名前 demo-container Container Instanceの名前
コンパートメント 任意のコンパートメントを選択 デフォルト設定
可用性ドメイン AD-1 任意のADを選択
シェイプ CI.Standard.E4.Flex Container Instance用のシェイプ
OCPU数 1 最小値、必要に応じて調整可能
メモリ(GB) 1 最小値、必要に応じて調整可能

CleanShot 2025-10-05 at 23.08.09.png

ネットワーキングの設定

項目 設定値 説明
仮想クラウド・ネットワーク demo-vcn 1章で作成したVCN
サブネット demo-subnet-app (リージョナル) アプリケーション用プライベートサブネット
プライベートIPアドレスの割当て 自動割当て デフォルト設定のまま

CleanShot 2025-10-05 at 23.10.07.png

コンテナの設定

「コンテナ1」として以下を設定します。

項目 設定値 説明
名前 api-container コンテナの名前

イメージの設定:

項目 設定値 説明
イメージ・レジストリ OCIR OCI Container Registry
リポジトリ container-api (プライベート) 2-2で作成したリポジトリ
イメージ latest プッシュしたイメージのタグ
資格証明を手動で指定 ✓ チェック 手動で認証情報を入力
レジストリ・ユーザー名 docker loginしたユーザー名
レジストリ・パスワード 2-3-1で取得したAuth Token ユーザーの認証トークン

CleanShot 2025-10-05 at 23.22.41.png

警告ボックス
画面に表示されるオレンジの警告ボックスは、IAMポリシーを使用することが推奨されることを示していますが、本記事では初学者向けに認証トークン方式を使用します。

環境変数:

環境変数を以下の4つ追加します。「別の環境変数の追加」を押下して、それぞれ設定してください。

変数名
DB_HOST demo-mysql.demosubnetdb.demovcn.oraclevcn.com
DB_USER appuser
DB_PASSWORD ************(3-3で設定したappuserのパスワード)
DB_NAME sampledb

CleanShot 2025-10-05 at 23.29.07.png

すべて入力後、画面下部の「作成」ボタンを押下します

数分待つと、Container Instanceがアクティブ状態になります
CleanShot 2025-10-06 at 00.30.22.png

4-2. 動作確認

Container Instanceが作成されたら、動作確認をしてみます。

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

CleanShot 2025-10-06 at 00.49.06.png

管理用Computeインスタンスで以下のコマンドを実行して、Container InstanceのAPIにアクセスします:

# 管理用ComputeインスタンスにSSH接続
ssh -i ~/.ssh/id_rsa opc@<management-compute-public-ip>

# Container InstanceのプライベートIPアドレスを確認
# (例: 10.0.1.XXX)

# APIにアクセスしてテスト
curl -s http://10.0.1.XXX:8000/todos

# 新しいTODOの作成
curl -X POST http://10.0.1.XXX:8000/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Container Instance test"}'

# TODOリストの再取得(作成されたTODOが表示される)
curl -s http://10.0.1.XXX:8000/todos

TODO作成後に作成されたTODOが表示されます。

[{"created_at":"Sun, 05 Oct 2025 15:46:10 GMT","done":0,"id":1,"title":"Container Instance test"}]

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

5. Network Load Balancer (NLB) の構築

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

重要: Container Instanceをバックエンドとして使用する場合は、Flexible Load BalancerではなくNetwork Load Balancer (NLB) を使用する必要があります。

5-1. Network Load Balancer (NLB) の作成

ネットワーキング→ネットワーク・ロード・バランサを選択します
CleanShot 2025-10-06 at 01.06.34.png

遷移後、「ネットワーク・ロード・バランサの作成」ボタンを押下します

5-1-1. 詳細の追加

項目 設定値 説明
ロード・バランサ名 demo-lb Network Load Balancerの名前
可視性タイプ パブリック インターネットからアクセス可能
パブリックIPアドレスの割当て エフェメラルIPアドレス 動的IPアドレス割当て
仮想クラウド・ネットワーク demo-vcn 1章で作成したVCN
サブネット demo-subnet-public パブリックサブネット

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

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

項目 設定値 説明
リスナー名 demo-listener リスナーの名前
トラフィックのタイプ TCP TCPプロトコル(Layer 4)
ポート ポートを指定
80
HTTP標準ポート

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

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

項目 設定値 説明
バックエンドセット名 demo-backend-set バックエンドセットの名前
ソースIPの保持 チェックを外す 先にこのチェックを外さないとIPアドレスが指定できません
バックエンドの追加
→IPアドレス
IPアドレス:
ポート:8000
重み: 1

ヘルスチェック・ポリシーの詳細設定:

項目 設定値 説明
プロトコル TCP TCPプロトコルでヘルスチェック
ポート 8000 Flaskアプリケーションのポート
間隔(ミリ秒) 10000 10秒間隔でチェック
タイムアウト(ミリ秒) 3000 3秒でタイムアウト
再試行回数 3 3回まで再試行

セキュリティ・リストの管理:
「ネットワーク・ロード・バランサの作成後にセキュリティ・リスト・ルールを手動で構成します」を選択します

ロード・バランシング・ポリシー:

項目 設定値 説明
ロード・バランシング・ポリシーの指定 5タプル・ハッシュ 均等にトラフィックを分散

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

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

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

CleanShot 2025-10-06 at 01.26.26.png

5-2. 動作確認

ここまでの操作で、Load balancerは作成されますが、正常にパスが通っているか確認します。

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

空の配列もしくは以前に実施した値が返ってくれば正常に動作していることが確認できます。

6. 動作確認

表示確認

Webブラウザから、動作確認してみましょう。

http://<Load balancerのパブリックIPアドレス>/todos

CleanShot 2025-10-06 at 02.41.50.png

さいごに

OCIで、シンプルなコンテナアーキテクチャを作ってみました。
とはいえ、手順の一つ一つ丁寧に作ると長文になりますね。

Container Instanceを使用することで以下のメリットを享受できます。

  • デプロイの簡素化: アプリケーションをコンテナイメージとして管理
  • 環境の一貫性: ローカル開発環境と本番環境の統一
  • スケーラビリティ: コンテナ単位でのリソース調整

今回の手順は、HTTPアクセスだったり、DBパスワードをベタ打ちだったり、可用性を考慮していなかったりと、本番利用するにはまだまだ考慮すべき点が残っています。

そういった点は次回以降では触れていけたらと思います。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?