Servizio Azure Kubernetes (AKS)

Questo argomento converte un'applicazione Java standalone precedente in un microservizio containerizzato eseguito in Azure Kubernetes Service (AKS) e si connette a Oracle Autonomous AI Database utilizzando un wallet mTLS.

Requisiti indispensabili

In questa sezione vengono descritti i requisiti di Oracle Database e le tabelle per l'applicazione Java per connettersi a Oracle Autonomous AI Database e accedere alla tabella Product.

Oracle Autonomous AI Database

Completare i passi riportati di seguito per eseguire il provisioning di Oracle Autonomous AI Database e creare un utente e una tabella.
  • Wallet Oracle Autonomous AI Database per la connessione.
  • Credenziali utente di Oracle Database per creare una sessione del database ed eseguire comandi SQL.
  • Connettività dall'Application Server a Oracle Database.
  • Una tabella dei prodotti in Oracle Database.
Eseguire il comando riportato di seguito per creare la tabella Product e inserire un record di test.
-- 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;

Implementazione

  1. Impostazione macchina di sviluppo
    1. Strumenti e librerie: installare le seguenti librerie e gli strumenti sul computer di sviluppo:
      1. Java Development Kit (JDK): JDK 25 o versione successiva.
      2. Driver JDBC Oracle: scaricare il file standalone ojdbc17.jar.
      3. Rancher Desktop: installare e selezionare il motore contenitore dockerd (moby) durante l'impostazione. Ciò consente di ottenere il comando CLI standard docker.
        1. È possibile utilizzare altre applicazioni simili a Rancher Desktop come Docker Desktop, Podman Desktop, Colima, OrbStack.
      4. CLI di Azure (az): per eseguire il provisioning delle risorse cloud.
      5. CLI Kubernetes (kubectl): per interagire con il cluster AKS.
    2. Codice sorgente Java (ProductApiApp.java)
      1. Creare il file ProductApiApp.java e copiarvi il contenuto seguente.
        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. La containerzzazione (Dockerfile)
      1. Creare un file denominato Dockerfile nella stessa directory del codice Java e del file ojdbc17.jar. Compilare il codice all'interno del contenitore per evitare di installare dipendenze di build sul computer locale.
        
        # 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. Ambiente di distribuzione - Servizio Azure Kubernetes (AKS)
    Aprire PowerShell, Prompt dei comandi o Zsh, quindi accedere ad Azure:
    az login
    1. Esegui provisioning del servizio Azure Kubernetes e del registro container
      1. Definire le variabili, quindi creare il gruppo di risorse Azure, Azure Container Registry (ACR) e il servizio Azure Kubernetes (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. Creazione e push del contenitore (utilizzando Rancher Desktop)
      1. Assicurarsi che Rancher Desktop sia in esecuzione, quindi eseguire i seguenti comandi.
        
        # 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. Configurare Oracle Wallet e i segreti del database
      1. Oracle Autonomous AI Database utilizza un wallet mTLS. Scaricare il file zip del wallet dell'istanza da OCI Console, quindi estrarlo in una cartella locale. Ad esempio, ./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. Distribuisci su AKS
      1. Creare un file denominato deployment.yaml. Il valore DB_URL utilizza l'alias Oracle TNS presente nel file tnsnames.ora e punta alla directory wallet (/app/wallet) di cui Kubernetes esegue l'installazione.
        • Sostituire l'endpoint del registro contenitore.
        • Sostituire my_adb_high con il nome TNS effettivo.
        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. Distribuire l'applicazione
      Una volta visualizzato EXTERNAL-IP, l'API è completamente accessibile tramite Internet.
      
      kubectl apply -f deployment.yaml
      
      # Monitor the deployment until an EXTERNAL-IP is assigned
      kubectl get services --watch
  3. Interazione con l'API

    Ora che l'API è separata dall'interfaccia utente e distribuita su AKS, puoi interagire con essa utilizzando qualsiasi client REST o un frontend disaccoppiato.

    1. Accesso all'applicazione in esecuzione

      Una volta visualizzato EXTERNAL-IP, ad esempio 20.124.x.x, l'API è accessibile tramite Internet.

      Anche se l'applicazione Java è EXPOSE 8080 nel Dockerfile, il servizio Kubernetes (api-service definito sopra) mappa la porta Web standard 80 alla porta del contenitore 8080. Pertanto, non è necessario specificare una porta nell'URL.

      Formato URL di accesso: http://<EXTERNAL-IP>/api/products

      1. Testarlo dal terminale:
        curl http://<EXTERNAL-IP>/api/products
    2. Abilitazione delle specifiche di OpenAPI (facoltativo)

      È possibile utilizzare openai.yaml in Postman o in un altro client REST per interagire con l'interfaccia utente grafica.

      1. Salvare il contenuto seguente come openai.yaml. Importare il file in Postman e sostituire <your-aks-api-external-ip> con l'indirizzo IP del passo precedente. L'intelligenza artificiale utilizza questo schema per generare automaticamente payload JSON validi e recuperare i dati Oracle correnti.
        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. Esegui cleanup

    Al termine del test, elimina le risorse cloud per evitare i costi di calcolo di Azure. Poiché le risorse si trovano in un singolo gruppo di risorse, è possibile eseguire il cleanup delle risorse utilizzando un singolo comando.

    1. Eseguire il comando seguente per eseguire il cleanup della risorsa.
      
      # 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