AKS (Azure Kubernetes Service)

Este tópico converte um aplicativo Java standalone legado em um microsserviço em contêiner que é executado no Azure Kubernetes Service (AKS) e se conecta ao Oracle Autonomous AI Database usando uma wallet mTLS.

Pré-requisitos

Esta seção descreve os requisitos do Oracle Database e das tabelas para que o aplicativo Java se conecte ao Oracle Autonomous AI Database e acesse a tabela Produto.

Oracle Autonomous AI Database

Siga as etapas abaixo para provisionar um Oracle Autonomous AI Database e criar um usuário e uma tabela:
  • Wallet do Oracle Autonomous AI Database para a conexão.
  • Credenciais de usuário do Oracle Database para criar uma sessão de banco de dados e executar comandos SQL.
  • Conectividade do servidor de aplicativos com o Oracle Database.
  • Uma tabela de produtos no Oracle Database.
Execute o seguinte comando para criar a tabela Produto e inserir um registro de teste:
-- 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;

Implementação

  1. Configuração da Máquina de Desenvolvimento
    1. Ferramentas e Bibliotecas: Instale as seguintes bibliotecas e ferramentas na máquina de desenvolvimento:
      1. JDK (Java Development Kit): JDK 25 ou mais recente.
      2. Driver Oracle JDBC: Faça download do ojdbc17.jar stand-alone.
      3. Rancher Desktop: Instale e selecione o mecanismo de contêiner dockerd (moby) durante a configuração. Isso fornece o comando padrão da CLI docker.
        1. Você pode usar outros aplicativos semelhantes ao Rancher Desktop, como Docker Desktop, Podman Desktop, Colima, OrbStack.
      4. CLI do Azure (az): Para provisionar os recursos de nuvem.
      5. CLI do Kubernetes (kubectl): Para interagir com o Cluster do AKS.
    2. O Código-fonte Java (ProductApiApp.java)
      1. Crie o arquivo ProductApiApp.java e copie o conteúdo a seguir para ele.
        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. A Conteinerização (Dockerfile)
      1. Crie um arquivo chamado Dockerfile no mesmo diretório que o código Java e o arquivo ojdbc17.jar. Compile o código dentro do contêiner para evitar a instalação de dependências de build na máquina 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. Ambiente de Implantação - Azure Kubernetes Service (AKS)
    Abra PowerShell, Prompt de Comando ou Zsh e, em seguida, acesse o Azure:
    az login
    1. Provisionar o Azure Kubernetes Service e o Container Registry
      1. Defina as variáveis e, em seguida, crie o grupo de recursos do Azure, o Azure Container Registry (ACR) e o 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. Criar e enviar o contêiner (usando o Rancher Desktop)
      1. Verifique se o Rancher Desktop está em execução e, em seguida, execute os comandos a seguir.
        
        # 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. Configurar Segredos do Oracle Wallet e do Banco de Dados
      1. O Oracle Autonomous AI Database usa uma wallet mTLS. Faça download do arquivo zip da wallet da instância na Console do OCI e extraia-o para uma pasta local. Por exemplo, ./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. Implantar no AKS
      1. Crie um arquivo chamado deployment.yaml. O valor DB_URL usa o alias do Oracle TNS encontrado no arquivo tnsnames.ora e aponta para o diretório da wallet (/app/wallet) que o Kubernetes monta.
        • Substitua o ponto final do registro do contêiner.
        • Substitua my_adb_high pelo nome TNS real.
        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. Implantar o Aplicativo
      Quando o EXTERNAL-IP for exibido, sua API ficará totalmente acessível pela internet.
      
      kubectl apply -f deployment.yaml
      
      # Monitor the deployment until an EXTERNAL-IP is assigned
      kubectl get services --watch
  3. Interagindo com a API

    Agora que a API está separada da interface do usuário e implantada no AKS, você pode interagir com ela usando qualquer cliente REST ou um frontend desacoplado.

    1. Acessando o Seu Aplicativo em Execução

      Quando o EXTERNAL-IP é exibido, por exemplo, 20.124.x.x, a API fica acessível pela internet.

      Embora seu aplicativo Java seja EXPOSE 8080 no Dockerfile, o Kubernetes Service (api-service definido acima) mapeia a porta Web padrão 80 para a porta do contêiner 8080. Portanto, não é necessário especificar uma porta no URL.

      Formato de URL de Acesso: http://<EXTERNAL-IP>/api/products

      1. Teste-o no seu terminal:
        curl http://<EXTERNAL-IP>/api/products
    2. Ativar Especificações OpenAPI (Opcional)

      Você pode usar openai.yaml no Postman ou em outro cliente REST para interagir com a interface gráfica do usuário.

      1. Salve o conteúdo a seguir como openai.yaml. Importe o arquivo para o Postman e substitua <your-aks-api-external-ip> pelo endereço IP da etapa anterior. A IA usa esse esquema para gerar automaticamente payloads JSON válidos e extrair dados atuais da 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. Limpar

    Depois de concluir o teste, exclua os recursos de nuvem para evitar custos de computação do Azure. Como os recursos estão em um único grupo de recursos, você pode limpar os recursos usando um único comando.

    1. Execute o comando a seguir para limpar o recurso.
      
      # 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