Service Azure Kubernetes (AKS)

Cette rubrique convertit une application Java autonome existante en un microservice conteneurisé qui s'exécute sur le service Azure Kubernetes (AKS) et se connecte à Oracle Autonomous AI Database à l'aide d'un portefeuille mTLS.

Conditions requises

Cette section décrit les exigences d'Oracle Database et des tables pour que l'application Java se connecte à Oracle Autonomous AI Database et accède à la table Produit.

Oracle Autonomous AI Database

Effectuez les étapes suivantes pour provisionner un service Oracle Autonomous AI Database et créer un utilisateur et une table :
  • Portefeuille Oracle Autonomous AI Database pour la connexion.
  • Données d'identification de l'utilisateur Oracle Database pour créer une session de base de données et exécuter des commandes SQL.
  • Connectivité du serveur d'applications à Oracle Database.
  • Une table de produits dans Oracle Database.
Exécutez la commande suivante pour créer la table Produit et insérer un enregistrement de 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;

Mise en oeuvre

  1. Configuration de la machine de développement
    1. Outils et bibliothèques : Installez les bibliothèques et outils suivants sur l'ordinateur de développement :
      1. Java Development Kit (JDK) : JDK 25 ou supérieur.
      2. Pilote JDBC Oracle : Téléchargez le fichier ojdbc17.jar autonome.
      3. Bureau d'analyse : Installez et sélectionnez le moteur de conteneur dockerd (moby) lors de la configuration. Vous disposez ainsi de la commande d'interface de ligne de commande docker standard.
        1. Vous pouvez utiliser d'autres applications similaires à Rancher Desktop comme Docker Desktop, Podman Desktop, Colima, OrbStack.
      4. Interface de ligne de commande Azure (az) : Pour provisionner les ressources en nuage.
      5. Interface de ligne de commande Kubernetes (kubectl) : Pour interagir avec la grappe AKS.
    2. Code source Java (ProductApiApp.java)
      1. Créez le fichier ProductApiApp.java et copiez-y le contenu suivant.
        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. Conteneurisation (Dockerfile)
      1. Créez un fichier nommé Dockerfile dans le même répertoire que votre code Java et le fichier ojdbc17.jar. Compilez le code à l'intérieur du conteneur pour éviter d'installer des dépendances de compilation sur l'ordinateur local.
        
        # 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. Environnement de déploiement - Service Azure Kubernetes (AKS)
    Ouvrez PowerShell, Invite de commande ou Zsh, puis connectez-vous à Azure :
    az login
    1. Provisionner le service Azure Kubernetes et le registre de conteneurs
      1. Définissez les variables, puis créez le groupe de ressources Azure, Azure Container Registry (ACR) et 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. Créer et pousser le conteneur (à l'aide de Rancher Desktop)
      1. Assurez-vous que Rancher Desktop est en cours d'exécution, puis exécutez les commandes suivantes.
        
        # 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. Configurer Oracle Wallet et les clés secrètes de base de données
      1. Oracle Autonomous AI Database utilise un portefeuille mTLS. Téléchargez le fichier zip du portefeuille d'instance à partir de la console OCI, puis extrayez-le dans un dossier local. Par exemple, ./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. Déployer vers AKS
      1. Créez un fichier nommé deployment.yaml. La valeur DB_URL utilise l'alias TNS Oracle trouvé dans le fichier tnsnames.ora et pointe vers le répertoire de portefeuille (/app/wallet) que Kubernetes monte.
        • Remplacez le point d'extrémité du registre de conteneurs.
        • Remplacez my_adb_high par le nom TNS réel.
        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. Déployer l'application
      Une fois EXTERNAL-IP affiché, votre API est entièrement accessible sur Internet.
      
      kubectl apply -f deployment.yaml
      
      # Monitor the deployment until an EXTERNAL-IP is assigned
      kubectl get services --watch
  3. Interaction avec l'API

    Maintenant que l'API est séparée de l'interface utilisateur et déployée sur AKS, vous pouvez interagir avec elle à l'aide de n'importe quel client REST ou d'un serveur frontal découplé.

    1. Accès à l'application en cours d'exécution

      Une fois que EXTERNAL-IP apparaît, par exemple 20.124.x.x, l'API est accessible sur Internet.

      Même si votre application Java est EXPOSE 8080 dans le fichier Dockerfile, le service Kubernetes (api-service défini ci-dessus) mappe le port Web standard 80 au port du conteneur 8080. Par conséquent, vous n'avez pas besoin de spécifier un port dans votre URL.

      Format d'URL d'accès : http://<EXTERNAL-IP>/api/products

      1. Testez-le à partir de votre terminal :
        curl http://<EXTERNAL-IP>/api/products
    2. Activer les spécifications OpenAPI (facultatif)

      Vous pouvez utiliser openai.yaml dans Postman ou un autre client REST pour interagir avec l'interface utilisateur graphique.

      1. Enregistrez le contenu suivant sous le nom openai.yaml. Importez le fichier dans Postman et remplacez <your-aks-api-external-ip> par l'adresse IP de l'étape précédente. L'IA utilise ce schéma pour générer automatiquement des données utiles JSON valides et extraire les données Oracle courantes.
        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. Nettoyer

    Une fois les tests terminés, supprimez les ressources en nuage pour éviter les coûts de calcul Azure. Comme les ressources se trouvent dans un seul groupe de ressources, vous pouvez les nettoyer à l'aide d'une seule commande.

    1. Exécutez la commande suivante pour nettoyer la ressource.
      
      # 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