Azure Kubernetesサービス(AKS)

このトピックでは、レガシー・スタンドアロンJavaアプリケーションを、Azure Kubernetes Service (AKS)上で実行され、mTLSウォレットを使用してOracle Autonomous AI Databaseに接続するコンテナ化されたマイクロサービスに変換します。

前提条件

この項では、Oracle Databaseの要件と、JavaアプリケーションがOracle Autonomous AI Databaseに接続し、「製品」表にアクセスするための表について説明します。

Oracle Autonomous AI Database

次のステップを実行して、Oracle Autonomous AI Databaseをプロビジョニングし、ユーザーおよび表を作成します:
  • 接続用のOracle Autonomous AI Database Wallet。
  • データベース・セッションを作成してSQLコマンドを実行するためのOracle Databaseユーザー資格証明。
  • アプリケーション・サーバーからOracle Databaseへの接続。
  • Oracle Databaseの製品表
次のコマンドを実行してProduct表を作成し、テスト・レコードを挿入します:
-- Create the Product table
CREATE TABLE Product (
    id NUMBER PRIMARY KEY,
    name VARCHAR2(100) NOT NULL,
    price NUMBER(10, 2) NOT NULL
);

-- Insert a quick test record (optional, so your UI isn't empty on first load)
INSERT INTO Product (id, name, price) 
VALUES (1, 'Test Migration Item', 99.99);

-- Commit the transaction
COMMIT;

実装

  1. 開発マシン設定
    1. ツールとライブラリ:次のライブラリとツールを開発マシンにインストールします。
      1. Java Development Kit (JDK): JDK 25以上。
      2. Oracle JDBCドライバ:スタンドアロンojdbc17.jarをダウンロードします。
      3. Rancher Desktop:設定中にdockerd (moby)コンテナ・エンジンをインストールして選択します。これにより、標準のdocker CLIコマンドが提供されます。
        1. Rancher Desktopに似た他のアプリケーション(Docker Desktop、Podman Desktop、Colima、OrbStackなど)を使用できます。
      4. Azure CLI (az): クラウド・リソースをプロビジョニングします。
      5. Kubernetes CLI (kubectl): AKSクラスタと対話します。
    2. Javaソース・コード(ProductApiApp.java)
      1. ProductApiApp.javaファイルを作成し、そのファイルに次の内容をコピーします。
        import java.io.*;
        import java.net.InetSocketAddress;
        import java.sql.*;
        import com.sun.net.httpserver.*;
        
        public class ProductApiApp {
            // These environment variables are injected by the Kubernetes deployment.yaml
            private static final String DB_URL = System.getenv("DB_URL");
            private static final String DB_USER = System.getenv("DB_USER");
            private static final String DB_PASS = System.getenv("DB_PASS");
        
            public static void main(String[] args) throws Exception {
                if (DB_URL == null || DB_USER == null || DB_PASS == null) {
                    System.err.println("ERROR: Missing DB_URL, DB_USER, or DB_PASS");
                    System.exit(1);
                }
        
                // Bind to 0.0.0.0 (all interfaces) so the Kubernetes LoadBalancer can route traffic to it
                HttpServer server = HttpServer.create(new InetSocketAddress("0.0.0.0", 8080), 0);
                server.createContext("/api/products", new ProductApiHandler());
                server.setExecutor(null); 
                server.start();
                System.out.println("API Microservice running on port 8080...");
                System.out.println("Connecting to database using URL: " + DB_URL);
            }
        
            static class ProductApiHandler implements HttpHandler {
                @Override
                public void handle(HttpExchange exchange) throws IOException {
                    // Enable CORS so the UI microservice and remote callers can fetch data from this API
                    exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
                    exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
                    exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type");
                    
                    // Handle preflight requests for CORS
                    if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
                        exchange.sendResponseHeaders(204, -1);
                        return;
                    }
        
                    exchange.getResponseHeaders().add("Content-Type", "application/json");
                    String method = exchange.getRequestMethod();
                    StringBuilder jsonResponse = new StringBuilder();
                    
                    try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS)) {
                        
                        if ("GET".equalsIgnoreCase(method)) {
                            // READ: List all products
                            jsonResponse.append("[");
                            try (Statement stmt = conn.createStatement();
                                 ResultSet rs = stmt.executeQuery("SELECT id, name, price FROM Product ORDER BY id")) {
                                boolean first = true;
                                while (rs.next()) {
                                    if (!first) jsonResponse.append(",");
                                    jsonResponse.append("{")
                                                .append("\"id\":").append(rs.getInt("id")).append(",")
                                                .append("\"name\":\"").append(rs.getString("name")).append("\",")
                                                .append("\"price\":").append(rs.getDouble("price"))
                                                .append("}");
                                    first = false;
                                }
                            }
                            jsonResponse.append("]");
                        } 
                        else if ("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method)) {
                            // Read the ENTIRE request payload (handles multi-line pretty JSON from Postman)
                            InputStreamReader isr = new InputStreamReader(exchange.getRequestBody(), "utf-8");
                            StringBuilder payloadBuilder = new StringBuilder();
                            int b;
                            while ((b = isr.read()) != -1) {
                                payloadBuilder.append((char) b);
                            }
                            String payload = payloadBuilder.toString();
                            
                            String idStr = extractJsonValue(payload, "id");
                            String name = extractJsonValue(payload, "name");
                            String priceStr = extractJsonValue(payload, "price");
        
                            int id = idStr.isEmpty() ? 0 : Integer.parseInt(idStr);
                            double price = priceStr.isEmpty() ? 0.0 : Double.parseDouble(priceStr);
        
                            if ("POST".equalsIgnoreCase(method)) {
                                // CREATE
                                String sql = "INSERT INTO Product (id, name, price) VALUES (?, ?, ?)";
                                try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                                    pstmt.setInt(1, id);
                                    pstmt.setString(2, name);
                                    pstmt.setDouble(3, price);
                                    pstmt.executeUpdate();
                                }
                                jsonResponse.append("{\"status\": \"Product created successfully\"}");
                            } else if ("PUT".equalsIgnoreCase(method)) {
                                // UPDATE
                                String sql = "UPDATE Product SET name=?, price=? WHERE id=?";
                                try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                                    pstmt.setString(1, name);
                                    pstmt.setDouble(2, price);
                                    pstmt.setInt(3, id);
                                    pstmt.executeUpdate();
                                }
                                jsonResponse.append("{\"status\": \"Product updated successfully\"}");
                            } else if ("DELETE".equalsIgnoreCase(method)) {
                                // DELETE
                                String sql = "DELETE FROM Product WHERE id=?";
                                try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                                    pstmt.setInt(1, id);
                                    pstmt.executeUpdate();
                                }
                                jsonResponse.append("{\"status\": \"Product deleted successfully\"}");
                            }
                        }
                        
                        byte[] responseBytes = jsonResponse.toString().getBytes("UTF-8");
                        exchange.sendResponseHeaders(200, responseBytes.length);
                        OutputStream os = exchange.getResponseBody();
                        os.write(responseBytes);
                        os.close();
        
                    } catch (SQLException e) {
                        String errorJson = "{\"error\":\"" + e.getMessage().replace("\"", "\\\"") + "\"}";
                        byte[] responseBytes = errorJson.getBytes("UTF-8");
                        exchange.sendResponseHeaders(500, responseBytes.length);
                        OutputStream os = exchange.getResponseBody();
                        os.write(responseBytes);
                        os.close();
                    }
                }
        
                // Lightweight JSON parser helper for zero-dependency constraint
                // Updated to handle arbitrary spaces and multi-line structures
                private String extractJsonValue(String json, String key) {
                    if (json == null) return "";
                    String searchKey = "\"" + key + "\"";
                    int start = json.indexOf(searchKey);
                    if (start == -1) return "";
                    start = json.indexOf(":", start) + 1;
                    int end = json.indexOf(",", start);
                    if (end == -1) end = json.indexOf("}", start);
                    if (end == -1) end = json.length();
                    return json.substring(start, end).replace("\"", "").trim();
                }
            }
        }
    3. コンテナ化(Dockerfile)
      1. Javaコードおよびojdbc17.jarファイルと同じディレクトリにDockerfileという名前のファイルを作成します。ローカル・マシンにビルド依存性をインストールしないように、コンテナ内のコードをコンパイルします。
        
        # Use Eclipse Temurin as a highly trusted, industry-standard base image for Java
        FROM eclipse-temurin:25-jdk-jammy
        
        WORKDIR /app
        COPY ProductApiApp.java /app/
        COPY ojdbc17.jar /app/
        
        # Compile the Java application
        RUN javac -cp ojdbc17.jar ProductApiApp.java
        
        EXPOSE 8080
        CMD ["java", "-cp", ".:ojdbc17.jar", "ProductApiApp"]
  2. デプロイメント環境- Azure Kubernetesサービス(AKS)
    PowerShell、Command PromptまたはZshを開き、Azureにサインインします。
    az login
    1. Azure Kubernetesサービスおよびコンテナ・レジストリのプロビジョニング
      1. 変数を定義し、Azureリソース・グループ、Azure Container Registry (ACR)およびAzure Kubernetes Service (AKS)を作成します。
        RESOURCE_GROUP="oracle-aks-rg"
        LOCATION="eastus"
        ACR_NAME="mycompanyacr123" # Must be globally unique
        AKS_NAME="oracle-aks-cluster"
        
        # 1. Create Resource Group
        az group create --name $RESOURCE_GROUP --location $LOCATION
        
        # 2. Create Azure Container Registry
        az acr create --resource-group $RESOURCE_GROUP --name $ACR_NAME --sku Basic
        
        # 3. Create AKS Cluster and attach the ACR (so AKS can pull your images)
        az aks create \
            --resource-group $RESOURCE_GROUP \
            --name $AKS_NAME \
            --node-count 1 \
            --generate-ssh-keys \
            --attach-acr $ACR_NAME
        
        # 4. Get kubectl credentials to connect to your new cluster
        az aks get-credentials --resource-group $RESOURCE_GROUP --name $AKS_NAME
    2. コンテナの構築およびプッシュ(Rancher Desktopの使用)
      1. Rancher Desktopが実行されていることを確認してから、次のコマンドを実行します。
        
        # 1. Log into Azure ACR
        az acr login --name $ACR_NAME
        
        # 2. Build the image 
        docker build --platform linux/amd64 -t $ACR_NAME.azurecr.io/product-api:v1 .
        
        # 3. Push the image to ACR
        docker push $ACR_NAME.azurecr.io/product-api:v1
    3. Oracle Walletおよびデータベース・シークレットの構成
      1. Oracle Autonomous AI DatabaseはmTLSウォレットを使用します。インスタンス・ウォレット・チップ・ファイルをOCIコンソールからダウンロードし、ローカル・フォルダに抽出します。たとえば、./adb-walletです。
        
        # 1. Upload the Wallet files into Kubernetes as a Secret
        kubectl create secret generic adb-wallet \
          --from-file=./adb-wallet/cwallet.sso \
          --from-file=./adb-wallet/tnsnames.ora \
          --from-file=./adb-wallet/sqlnet.ora
        
        # 2. Upload your Database Credentials as a Secret
        kubectl create secret generic db-credentials \
          --from-literal=username="ADMIN" \
          --from-literal=password="<Your_ADB_Password>"
    4. AKSへのデプロイ
      1. deployment.yamlという名前のファイルを作成します。DB_URL値は、tnsnames.oraファイルにあるOracle TNS別名を使用し、Kubernetesがマウントするウォレット・ディレクトリ(/app/wallet)を指します。
        • コンテナ・レジストリ・エンドポイントを置換します。
        • my_adb_highを実際のTNS名に置き換えます。
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: product-api
        spec:
          replicas: 2
          selector:
            matchLabels:
              app: product-api
          template:
            metadata:
              labels:
                app: product-api
            spec:
              containers:
              - name: api
                image: mycompanyacr123.azurecr.io/product-api:v1  # UPDATE THIS to your ACR name
        	    imagePullPolicy: Always  # Forces Kubernetes to download the newest image from ACR
                ports:
                - containerPort: 8080
                env:
                - name: DB_URL
                  # Replace 'my_adb_high' with the actual alias from your tnsnames.ora
                  value: "jdbc:oracle:thin:@my_adb_high?TNS_ADMIN=/app/wallet"
                - name: DB_USER
                  valueFrom:
                    secretKeyRef:
                      name: db-credentials
                      key: username
                - name: DB_PASS
                  valueFrom:
                    secretKeyRef:
                      name: db-credentials
                      key: password
                volumeMounts:
                - name: wallet-volume
                  mountPath: /app/wallet
                  readOnly: true
              volumes:
              - name: wallet-volume
                secret:
                  secretName: adb-wallet
        ---
        apiVersion: v1
        kind: Service
        metadata:
          name: api-service
        spec:
          type: LoadBalancer
          ports:
          - port: 80
            targetPort: 8080
          selector:
            app: product-api
    5. アプリケーションの導入
      EXTERNAL-IPが表示されたら、APIにインターネット経由で完全にアクセスできます。
      
      kubectl apply -f deployment.yaml
      
      # Monitor the deployment until an EXTERNAL-IP is assigned
      kubectl get services --watch
  3. APIとの対話

    APIがUIから分離され、AKSにデプロイされたため、任意のRESTクライアントまたは分離されたフロントエンドを使用してAPIと対話できます。

    1. 実行中のアプリケーションへのアクセス

      EXTERNAL-IP (20.124.x.xなど)が表示されると、APIはインターネットを介してアクセス可能になります。

      JavaアプリケーションがDockerfileのEXPOSE 8080であっても、Kubernetesサービス(前述で定義したapi-service)は標準のWebポート80をコンテナのポート8080にマップします。したがって、URLにポートを指定する必要はありません。

      アクセスURL形式: http://<EXTERNAL-IP>/api/products

      1. ターミナルからテストします。
        curl http://<EXTERNAL-IP>/api/products
    2. OpenAPI指定の有効化(オプション)

      Postmanまたは別のRESTクライアントでopenai.yamlを使用して、グラフィカル・ユーザー・インタフェースと対話できます。

      1. 次のコンテンツをopenai.yamlとして保存します。ファイルをPostmanにインポートし、<your-aks-api-external-ip>を前のステップのIPアドレスに置き換えます。AIは、このスキーマを使用して、有効なJSONペイロードを自動的に生成し、現在のOracleデータをフェッチします。
        openapi: 3.0.0
        info:
          title: Oracle Product API
          version: 1.0.0
          description: Full CRUD API to perform operations on the Product table.
        servers:
          - url: http://<your-aks-api-external-ip>
        paths:
          /api/products:
            get:
              summary: Read all products
              operationId: getProducts
              responses:
                '200':
                  description: A JSON array of products
                  content:
                    application/json:
                      schema:
                        type: array
                        items:
                          $ref: '#/components/schemas/Product'
            post:
              summary: Create a new product
              operationId: createProduct
              requestBody:
                required: true
                content:
                  application/json:
                    schema:
                      $ref: '#/components/schemas/Product'
              responses:
                '200':
                  description: Product created successfully
            put:
              summary: Update an existing product
              operationId: updateProduct
              requestBody:
                required: true
                content:
                  application/json:
                    schema:
                      $ref: '#/components/schemas/Product'
              responses:
                '200':
                  description: Product updated successfully
            delete:
              summary: Delete a product
              operationId: deleteProduct
              requestBody:
                required: true
                content:
                  application/json:
                    schema:
                      type: object
                      properties:
                        id:
                          type: integer
              responses:
                '200':
                  description: Product deleted successfully
        components:
          schemas:
            Product:
              type: object
              properties:
                id:
                  type: integer
                name:
                  type: string
                price:
                  type: number
  4. 消去

    テストが終了したら、クラウド・リソースを削除してAzureのコンピュート・コストを回避します。リソースは単一のリソースグループ内にあるため、単一のコマンドを使用してリソースをクリーンアップできます。

    1. 次のコマンドを実行してリソースをクリーンアップします。
      
      # Delete pods
      kubectl delete pods -l app=product-api
      
      # Delete the entire resource group (AKS, ACR, Disks, Load Balancers, and IPs)
      az group delete --name oracle-aks-rg --yes --no-wait
      
      # Optional: Remove the local kubectl context
      kubectl config delete-context oracle-aks-cluster