Motore Google Kubernetes (GKE)

Questo argomento modernizza un'applicazione Java standalone precedente in un microservizio containerizzato eseguito su Google Kubernetes Engine (GKE) e si connette a Oracle Autonomous AI Database utilizzando un wallet mTLS.

Requisiti indispensabili

Questa sezione descrive i requisiti di Oracle AI Database e le tabelle per l'applicazione Java per connettersi a Oracle Autonomous AI Database (serverless) e accedere alla tabella Prodotto.

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 AI Database.
  • Una tabella dei prodotti in Oracle AI 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 Google Cloud (gcloud): per eseguire il provisioning delle risorse cloud.
      5. Kubernetes CLI (kubectl) e Plugin di autenticazione GKE: per interagire con il cluster GKE.
    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 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 - Google Kubernetes Engine (GKE)
    Aprire PowerShell, Prompt dei comandi o Zsh, quindi accedere a Google Cloud:
    gcloud auth login
    1. Esegui provisioning di Google Kubernetes Engine e Container Registry
      1. Definire le variabili, quindi creare il GAR (Google Artifact Registry) e il cluster GKE (Google Kubernetes Engine).
        
        PROJECT_ID="your-gcp-project-id" # REPLACE with your actual GCP Project ID
        REGION="us-central1"
        REPO_NAME="mycompanyrepo123"
        CLUSTER_NAME="oracle-gke-cluster"
        
        # 1. Set the active project
        gcloud config set project $PROJECT_ID
        
        # 2. Enable Required APIs (Artifact Registry and Kubernetes Engine)
        gcloud services enable artifactregistry.googleapis.com container.googleapis.com
        
        # 3. Create Google Artifact Registry (GAR) for Docker images
        gcloud artifacts repositories create $REPO_NAME \
            --repository-format=docker \
            --location=$REGION \
            --description="Docker repository for Oracle microservices"
        
        # 4. Create GKE Cluster (Standard, 1 node for testing)
        gcloud container clusters create $CLUSTER_NAME \
            --region=$REGION \
            --num-nodes=1
        
        # 5. Get kubectl credentials to connect to your new cluster
        gcloud container clusters get-credentials $CLUSTER_NAME --region=$REGION
    2. Creazione e push del contenitore (utilizzando Rancher Desktop)
      1. Assicurarsi che Rancher Desktop sia in esecuzione, quindi eseguire i seguenti comandi.
        
        # 1. Configure local Docker/Rancher CLI to authenticate with Google Artifact Registry
        gcloud auth configure-docker $REGION-docker.pkg.dev
        
        # 2. Define your full image path
        IMAGE_PATH="$REGION-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/product-api:v1"
        
        # 3. Build the image locally (Enforce AMD64 architecture for cloud compatibility)
        docker build --platform linux/amd64 -t $IMAGE_PATH .
        
        # 4. Push the image to Google Cloud
        docker push $IMAGE_PATH
    3. Configurare Oracle Wallet e i segreti del database
      1. Autonomous AI Database (Serverless) utilizza un wallet mTLS. Scaricare il file zip del wallet dell'istanza da OCI Console, quindi estrarlo nella 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_Password123!>"
    4. Distribuisci su GKE
      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.
        • Aggiornare <your-gcp-project-id> all'interno dell'attributo image: riportato di seguito in modo che corrisponda all'ID progetto effettivo.
        • 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
                # UPDATE THIS image string with your actual project ID
                image: us-central1-docker.pkg.dev/<your-gcp-project-id>/mycompanyrepo123/product-api:v1
                imagePullPolicy: Always  # Forces K8s to download the newest image from GAR
                ports:
                - containerPort: 8080
                # THIS IS WHERE THE ENVIRONMENT VARIABLES ARE SET FOR JAVA
                env:
                - name: DB_URL
                  # The '?TNS_ADMIN=/app/wallet' parameter tells the JDBC driver where to look for the wallet.
                  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 GKE, 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-GKE-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 ADB-S Product API
          version: 1.0.0
          description: Full CRUD API to perform operations on the Product table.
        servers:
          - url: http://<your-gke-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 dei test, elimina le risorse cloud per evitare i costi di computazione di Google Cloud.

    1. Eseguire il comando seguente per eseguire il cleanup della risorsa.
      
      # 1. Delete the GKE Cluster
      gcloud container clusters delete $CLUSTER_NAME --region=$REGION --quiet
      
      # 2. Delete the Artifact Registry repository
      gcloud artifacts repositories delete $REPO_NAME --location=$REGION --quiet
      
      # Optional: Remove the local kubectl context
      kubectl config delete-context gke_${PROJECT_ID}_${REGION}_${CLUSTER_NAME}