The AI-Native IDP -- Parte 8

La Arquitectura de Referencia

#platform-engineering #backstage #ai #architecture #kubernetes #reference

El Problema

Has leído siete artículos. Has visto el código para enriquecimiento del catálogo, scaffolding con IA, code review con contexto, RAG de documentación, governance e incident response. Cada artículo mostraba una pieza. Pero, ¿cómo encajan todas?

¿Dónde corre el servicio .NET de IA? ¿Cómo se conecta a PostgreSQL? ¿Cómo habla Backstage con él? ¿Cómo es el deployment en Kubernetes? ¿Qué variables de entorno necesitas? ¿Cómo añades una nueva funcionalidad de IA sin romper las que ya existen?

Este artículo responde a todo eso. Sin funcionalidades nuevas — solo la imagen completa.

La Solución

La arquitectura tiene cuatro capas:

  1. Backstage — Los plugins frontend y backend. Los desarrolladores interactúan con esto.
  2. AI Service — La Minimal API en .NET que gestiona todas las operaciones de IA. Los plugins de Backstage llaman a esto.
  3. PostgreSQL + pgvector — Almacena datos del catálogo, vector embeddings, logs de uso y policies.
  4. AI Provider — Cualquier API compatible con OpenAI: Scaleway Generative APIs con Mistral, Azure AI Foundry, o Mistral AI directamente.
graph TB
    subgraph "Developer"
        Browser[Browser]
    end

    subgraph "Backstage :7009"
        FE[Frontend - React :3456]
        BE[Backend - Node.js :7009]
        P1[Catalog Enricher Plugin]
        P2[AI Scaffolder Plugin]
        P3[AI Code Review Plugin]
        P4[TechDocs RAG Plugin]
        P5[AI Governance Plugin]
        P6[AI Incident Plugin]
    end

    subgraph "AI Service"
        API[".NET Minimal API :5100"]
        E1["/api/enrich"]
        E2["/api/scaffold"]
        E3["/api/review"]
        E4["/api/index-doc"]
        E5["/api/ask"]
        E6["/api/incident/analyze"]
        E7["/api/governance/*"]
        E8["/api/scaffold-terraform"]
    end

    subgraph "Data"
        PG[(PostgreSQL + pgvector)]
        AO[AI Provider - Scaleway/Mistral/Azure]
    end

    subgraph "External"
        GH[GitHub API]
        WH[Webhooks - GitHub, Alertmanager]
    end

    Browser --> FE
    FE --> BE
    BE --> P1 & P2 & P3 & P4 & P5 & P6
    P1 & P2 & P3 & P4 & P5 & P6 --> API
    API --> E1 & E2 & E3 & E4 & E5 & E6 & E7 & E8
    API --> PG
    API --> AO
    P1 & P3 & P6 --> GH
    WH --> P3 & P6

Ejecución

La Estructura del Repositorio

forge/
├── app-config.yaml                    # Backstage configuration
├── catalog/
│   └── all.yaml                       # Catalog entities
├── templates/
│   ├── dotnet-api/                    # Static template (article 1)
│   ├── ai-service/                    # AI-powered template (article 3)
│   └── terraform-module/              # Terraform module template (devops-02)
├── modules/                           # Real Terraform modules in the catalog
├── ai-service/
│   └── CatalogEnricher/
│       ├── Program.cs                 # All AI endpoints in one service
│       ├── CatalogEnricher.csproj
│       ├── AiUsageLogger.cs           # Governance middleware
│       ├── appsettings.json
│       └── Dockerfile
├── plugins/
│   ├── catalog-enricher-backend/      # Article 2
│   ├── ai-scaffolder/                 # Article 3
│   ├── ai-code-review/               # Article 4
│   ├── techdocs-rag/                  # Article 5
│   ├── techdocs-rag-widget/           # Article 5 (frontend widget)
│   ├── ai-governance/                 # Article 6
│   ├── ai-incident/                   # Article 7
│   ├── admin/                         # Admin plugin (frontend)
│   └── admin-backend/                 # Admin plugin (backend)
├── packages/
│   ├── app/                           # Backstage frontend
│   └── backend/
│       └── src/
│           └── index.ts               # All plugin registrations
├── k8s/
│   ├── backstage.yaml                 # Backstage deployment
│   ├── ai-service.yaml               # AI service deployment
│   └── postgresql.yaml                # PostgreSQL with pgvector
├── .env.example
└── README.md

El Servicio .NET de IA Completo

Todos los endpoints viven en un solo Program.cs. Aquí tienes el archivo completo que combina todos los artículos. Cada endpoint sigue el mismo patrón: leer config, crear un ChatClient, construir un system prompt con contexto, llamar al modelo, devolver JSON estructurado.

La implementación completa de cada endpoint está en su propio artículo. Aquí mostramos las firmas reales y la infraestructura compartida — este es el archivo real, no un resumen.

// ai-service/CatalogEnricher/Program.cs
using System.ClientModel;
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.AI.OpenAI;
using Npgsql;
using OpenAI;
using OpenAI.Chat;
using OpenAI.Embeddings;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("DevCors", policy =>
        policy.WithOrigins("http://localhost:3456", "http://localhost:7007", "http://localhost:7008")
              .AllowAnyHeader()
              .AllowAnyMethod());
});

var app = builder.Build();
app.UseCors("DevCors");

app.MapGet("/healthz", () => Results.Ok(new { status = "healthy" }));

// --- Article 2: Catalog enrichment ---
app.MapPost("/api/enrich", async (EnrichRequest request, IConfiguration config) =>
{
    // Validates request.Files is not empty
    // Creates ChatClient from config (AI:Provider, AI:Endpoint, AI:Key, AI:ChatModel)
    // System prompt: "Analyze source files, return JSON with description, tags, dependencies, apiEndpoints"
    // Returns: CatalogMetadata { Description, Tags, Dependencies, ApiEndpoints }
    // Full implementation → article 2
});

// --- Article 3: Project scaffolding ---
app.MapPost("/api/scaffold", async (ScaffoldRequest request, IConfiguration config) =>
{
    // Validates request.Description is not empty
    // Creates ChatClient from config
    // System prompt: "Given a service description, produce name, type, dependencies, auth, kubernetes, nugetPackages, envVars, gotchaPrompt"
    // Returns: ScaffoldResult with normalized gotchaPrompt (handles string or object)
    // Full implementation → article 3
});

// --- Article 4: Code review ---
app.MapPost("/api/review", async (ReviewRequest request, IConfiguration config) =>
{
    // Validates request.Diff is not empty
    // Creates ChatClient from config
    // System prompt includes: ServiceDescription, Tags, Dependencies, GotchaHeuristics
    // Focus: architectural rule violations, security issues, service-specific patterns
    // Returns: { review: string }
    // Full implementation → article 4
});

// --- Article 5: RAG indexing ---
app.MapPost("/api/index-doc", async (IndexDocRequest request, IConfiguration config) =>
{
    // Validates request.Content is not empty
    // Creates EmbeddingClient from config (AI:EmbeddingModel, uses Rag:PostgresConnection)
    // Splits content into 2000-char chunks, embeds each, upserts to doc_chunks table
    // Returns: { chunksIndexed: int }
    // Full implementation → article 5
});

// --- Article 5: RAG search ---
app.MapPost("/api/ask", async (AskRequest request, IConfiguration config) =>
{
    // Validates request.Question is not empty
    // Creates EmbeddingClient + ChatClient from config
    // Embeds question → vector search in doc_chunks → top 5 results
    // System prompt: "Answer using ONLY retrieved documentation, cite sources"
    // Returns: AskResponse { Answer, Sources[] }
    // Full implementation → article 5
});

// --- Article 6: Governance ---
app.MapGet("/api/governance/usage", async (string? action, string? team, int? days, IConfiguration config) =>
{
    // Queries ai_usage_log grouped by action, team, status
    // Returns: list of { Action, Team, Status, CallCount, TotalInputTokens, TotalOutputTokens, AvgDurationMs }
});

app.MapGet("/api/governance/costs", async (int? days, IConfiguration config) =>
{
    // Queries ai_usage_log aggregated by day, calculates estimated cost
    // Cost formula: (inputTokens * 2.0 / 1M) + (outputTokens * 6.0 / 1M)
    // Returns: list of { Day, InputTokens, OutputTokens, EstimatedCostUsd }
});

app.MapGet("/api/governance/policies", async (IConfiguration config) =>
{
    // Reads all rows from ai_policies table
    // Returns: list of { Id, Team, Action, Enabled, MaxDailyCalls }
});

app.MapPut("/api/governance/policies", async (PolicyUpdate update, IConfiguration config) =>
{
    // Upserts policy: INSERT ... ON CONFLICT (team, action) DO UPDATE
    // Returns: Ok()
});

// --- Article 7: Incident response ---
app.MapPost("/api/incident/analyze", async (IncidentRequest request, IConfiguration config) =>
{
    // Creates ChatClient from config
    // System prompt includes: ServiceName, ServiceDescription, Dependencies, Tags,
    //   RecentDeployments, RecentErrors, GotchaHeuristics
    // Returns: { analysis: string } with probable cause, evidence, suggested actions, related services
    // Full implementation → article 7
});

// --- Infrastructure Hub: Terraform scaffolding (devops-02) ---
app.MapPost("/api/scaffold-terraform", async (ScaffoldTerraformRequest request, IConfiguration config) =>
{
    // Validates request.Cloud against supported providers (azure, scaleway, aws, gcp)
    // Creates ChatClient from config
    // System prompt: "Generate Terraform module with main.tf, variables.tf, outputs.tf"
    // Returns: ScaffoldTerraformResult { Main, Variables, Outputs }
    // Full implementation → devops-02
});

app.Run();

// --- Record types ---

// Article 2
record EnrichRequest(List<SourceFile> Files);
record SourceFile(string Path, string Content);
record CatalogMetadata(
    [property: JsonPropertyName("description")] string Description,
    [property: JsonPropertyName("tags")] List<string> Tags,
    [property: JsonPropertyName("dependencies")] List<string> Dependencies,
    [property: JsonPropertyName("apiEndpoints")] List<string> ApiEndpoints);

// Article 3
record ScaffoldRequest(string Description);
record ScaffoldResult(
    [property: JsonPropertyName("name")] string Name,
    [property: JsonPropertyName("description")] string Description,
    [property: JsonPropertyName("type")] string Type,
    [property: JsonPropertyName("dependencies")] Dictionary<string, bool> Dependencies,
    [property: JsonPropertyName("auth")] string Auth,
    [property: JsonPropertyName("kubernetes")] bool Kubernetes,
    [property: JsonPropertyName("nugetPackages")] List<string> NugetPackages,
    [property: JsonPropertyName("envVars")] List<string> EnvVars,
    [property: JsonPropertyName("gotchaPrompt")] JsonElement GotchaPrompt);

// Article 4
record ReviewRequest(
    string ServiceName, string ServiceDescription,
    string[] Tags, string[] Dependencies,
    string GotchaHeuristics, string PrTitle, string Diff);

// Article 5
record IndexDocRequest(string EntityRef, string DocPath, string Content);
record AskRequest(string Question, string? EntityRef);
record AskResponse(string Answer, SourceReference[] Sources);
record SourceReference(string EntityRef, string DocPath, float Similarity);

// Article 6
record PolicyUpdate(string Team, string Action, bool Enabled, int? MaxDailyCalls);

// Article 7
record IncidentRequest(
    string ServiceName, string ServiceDescription,
    string[] Dependencies, string[] Tags,
    string RecentDeployments, string RecentErrors,
    string GotchaHeuristics, string AlertTitle,
    string Severity, string StartedAt);

// Infrastructure Hub (devops-02)
record ScaffoldTerraformRequest(string Cloud, string Name, string Description);
record ScaffoldTerraformResult(string Main, string Variables, string Outputs);

static class SerializerOptions
{
    public static readonly JsonSerializerOptions Default = new()
    {
        PropertyNameCaseInsensitive = true,
    };
}

Cada endpoint que llama al modelo de IA usa la misma lógica de creación de ChatClient. El middleware de governance (AiUsageLogger.Track() del artículo 6) envuelve cada llamada de IA para registrar tokens y aplicar policies automáticamente.

Sobre seguridad: Estos endpoints no tienen autenticación, ni validación de input, ni rate limiting. Es a propósito — esta serie se centra en funcionalidad, no en hardening. Si piensas desplegarlo, lee la sección de Seguridad más abajo.

El Schema de Base de Datos Completo

-- pgvector (article 5)
CREATE EXTENSION IF NOT EXISTS vector;

-- Doc chunks for RAG (article 5)
CREATE TABLE doc_chunks (
    id SERIAL PRIMARY KEY,
    entity_ref VARCHAR(255) NOT NULL,
    doc_path VARCHAR(500) NOT NULL,
    chunk_index INTEGER NOT NULL,
    content TEXT NOT NULL,
    embedding vector(3584) NOT NULL,  -- dimensions depend on the embedding model
    created_at TIMESTAMP DEFAULT NOW(),
    UNIQUE (entity_ref, doc_path, chunk_index)
);

CREATE INDEX ON doc_chunks
USING hnsw (embedding vector_cosine_ops);

-- AI usage log (article 6)
CREATE TABLE ai_usage_log (
    id SERIAL PRIMARY KEY,
    timestamp TIMESTAMP DEFAULT NOW(),
    action VARCHAR(50) NOT NULL,
    entity_ref VARCHAR(255),
    team VARCHAR(100),
    user_ref VARCHAR(255),
    input_tokens INTEGER DEFAULT 0,
    output_tokens INTEGER DEFAULT 0,
    model VARCHAR(100),
    duration_ms INTEGER DEFAULT 0,
    status VARCHAR(20) DEFAULT 'success',
    metadata JSONB DEFAULT '{}'
);

CREATE INDEX idx_usage_action ON ai_usage_log(action);
CREATE INDEX idx_usage_team ON ai_usage_log(team);
CREATE INDEX idx_usage_timestamp ON ai_usage_log(timestamp);

-- AI policies (article 6)
CREATE TABLE ai_policies (
    id SERIAL PRIMARY KEY,
    team VARCHAR(100),
    action VARCHAR(50) NOT NULL,
    enabled BOOLEAN DEFAULT true,
    max_daily_calls INTEGER,
    updated_at TIMESTAMP DEFAULT NOW(),
    UNIQUE (team, action)
);

La Configuración de Backstage

El app-config.yaml conecta Backstage con el AI service y configura los plugins. Estas son las secciones clave que hacen que todo funcione:

# app-config.yaml (relevant sections)

app:
  baseUrl: http://localhost:3456

backend:
  baseUrl: http://localhost:7009
  listen:
    port: 7009

# Proxy — all Backstage plugins call the AI service through this
proxy:
  endpoints:
    /ai-service:
      target: http://localhost:5100

# Plugin-specific config
catalogEnricher:
  aiServiceUrl: http://localhost:5100

forge:
  aiServiceUrl: http://localhost:5100

techdocs:
  builder: 'local'
  generator:
    runIn: 'local'
  publisher:
    type: 'local'

La configuración del proxy es importante — los plugins de Backstage no llaman al AI service directamente. Pasan por proxy/ai-service, que el backend reenvía a http://localhost:5100. Esto mantiene la URL del AI service en un solo sitio.

Deployment en Kubernetes

El deployment del AI service:

# k8s/ai-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: forge-ai-service
  labels:
    app: forge-ai-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: forge-ai-service
  template:
    metadata:
      labels:
        app: forge-ai-service
    spec:
      containers:
        - name: ai-service
          image: ghcr.io/victorZKov/forge-ai-service:latest
          ports:
            - containerPort: 5100
          env:
            - name: AI__Provider
              value: openai  # "openai" for Scaleway/Mistral/OpenAI, "azure" for Azure AI Foundry
            - name: AI__Endpoint
              valueFrom:
                secretKeyRef:
                  name: forge-secrets
                  key: AI_ENDPOINT
            - name: AI__Key
              valueFrom:
                secretKeyRef:
                  name: forge-secrets
                  key: AI_KEY
            - name: AI__ChatModel
              value: mistral-small-3.2-24b-instruct-2506
            - name: AI__EmbeddingModel
              value: bge-multilingual-gemma2
            - name: ConnectionStrings__Default
              valueFrom:
                secretKeyRef:
                  name: forge-secrets
                  key: POSTGRESQL_CONNECTION
          livenessProbe:
            httpGet:
              path: /healthz
              port: 5100
            initialDelaySeconds: 10
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /healthz
              port: 5100
            initialDelaySeconds: 5
            periodSeconds: 10
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
  name: forge-ai-service
spec:
  selector:
    app: forge-ai-service
  ports:
    - port: 5100
      targetPort: 5100

Deployment de Backstage:

# k8s/backstage.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: forge-backstage
  labels:
    app: forge-backstage
spec:
  replicas: 1
  selector:
    matchLabels:
      app: forge-backstage
  template:
    metadata:
      labels:
        app: forge-backstage
    spec:
      containers:
        - name: backstage
          image: ghcr.io/victorZKov/forge-backstage:latest
          ports:
            - containerPort: 7009
          env:
            - name: POSTGRES_HOST
              value: forge-postgresql
            - name: POSTGRES_PORT
              value: "5432"
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: forge-secrets
                  key: POSTGRES_USER
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: forge-secrets
                  key: POSTGRES_PASSWORD
            - name: GITHUB_TOKEN
              valueFrom:
                secretKeyRef:
                  name: forge-secrets
                  key: GITHUB_TOKEN
          livenessProbe:
            httpGet:
              path: /healthcheck
              port: 7009
            initialDelaySeconds: 30
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /healthcheck
              port: 7009
            initialDelaySeconds: 15
            periodSeconds: 10
          resources:
            requests:
              cpu: 200m
              memory: 512Mi
            limits:
              cpu: 1000m
              memory: 1Gi
---
apiVersion: v1
kind: Service
metadata:
  name: forge-backstage
spec:
  selector:
    app: forge-backstage
  ports:
    - port: 7009
      targetPort: 7009

Variables de Entorno

El .env.example completo:

# OIDC Authentication (QuantumID, Entra ID, Keycloak, Auth0)
OIDC_METADATA_URL=https://auth.quantumapi.eu/.well-known/openid-configuration
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
BACKEND_SECRET=change-this-in-production

# AI Provider
# AI_PROVIDER: "openai" for Scaleway/Mistral/OpenAI (default), "azure" for Azure AI Foundry
AI_PROVIDER=openai
AI_ENDPOINT=https://api.scaleway.ai/v1
AI_KEY=your-key
AI_CHAT_MODEL=mistral-small-3.2-24b-instruct-2506
AI_EMBEDDING_MODEL=bge-multilingual-gemma2

# Examples for other providers:
# Mistral AI:  AI_ENDPOINT=https://api.mistral.ai/v1  AI_CHAT_MODEL=mistral-large-latest
# OpenAI:      AI_ENDPOINT=https://api.openai.com/v1  AI_CHAT_MODEL=gpt-5
# Azure:       AI_PROVIDER=azure  AI_ENDPOINT=https://your-instance.openai.azure.com  AI_CHAT_MODEL=gpt-5

# PostgreSQL (shared between Backstage and AI service)
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=forge
POSTGRES_PASSWORD=your-password

# GitHub
GITHUB_TOKEN=ghp_your-token

# AI Service
AI_SERVICE_URL=http://localhost:5100

# Webhook secrets
AI_CODE_REVIEW_WEBHOOK_SECRET=your-webhook-secret

Flexibilidad de Proveedor

El AI service usa dos paquetes NuGet: OpenAI y Azure.AI.OpenAI. El AzureOpenAIClient extiende OpenAIClient, así que todo el código de los endpoints funciona con ambos — la única diferencia es cómo creas el client. Pon AI:Provider a "azure" para Azure AI Foundry, o déjalo como "openai" (por defecto) para todo lo demás.

ProviderAI:ProviderEndpointChat modelEmbedding model
Scalewayopenaihttps://api.scaleway.ai/v1mistral-small-3.2-24b-instruct-2506bge-multilingual-gemma2
Mistral AIopenaihttps://api.mistral.ai/v1mistral-large-latestmistral-embed
Azure AI Foundryazurehttps://your-instance.openai.azure.comgpt-5, claude-sonnet-4-6, mistral-largetext-embedding-3-small
OpenAIopenaihttps://api.openai.com/v1gpt-5text-embedding-3-small

Azure AI Foundry es la opción más flexible — aloja modelos de OpenAI, Anthropic, Mistral y otros en un solo endpoint. Puedes ejecutar Claude Sonnet o Mistral Large a través de Azure sin gestionar API keys separadas por proveedor. El proyecto Forge funciona sobre Scaleway con Mistral Large, pero Azure AI Foundry es una buena elección si tu organización ya usa Azure.

El Registro del Backend de Backstage

Todos los plugins registrados en un solo sitio:

// packages/backend/src/index.ts
import { createBackend } from '@backstage/backend-defaults';

const backend = createBackend();

// Core plugins (standard Backstage)
backend.add(import('@backstage/plugin-app-backend'));
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'));
backend.add(import('@backstage/plugin-catalog-backend-module-logs'));
backend.add(import('@backstage/plugin-scaffolder-backend'));
backend.add(import('@backstage/plugin-scaffolder-backend-module-github'));
backend.add(import('@backstage/plugin-techdocs-backend'));
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
backend.add(import('@backstage/plugin-proxy-backend'));

// Search (PostgreSQL-backed)
backend.add(import('@backstage/plugin-search-backend'));
backend.add(import('@backstage/plugin-search-backend-module-pg'));
backend.add(import('@backstage/plugin-search-backend-module-catalog'));
backend.add(import('@backstage/plugin-search-backend-module-techdocs'));

// Permissions
backend.add(import('@backstage/plugin-permission-backend'));
backend.add(import('@backstage/plugin-permission-backend-module-allow-all-policy'));

// Kubernetes
backend.add(import('@backstage/plugin-kubernetes-backend'));

// OIDC auth (QuantumID, Entra ID, Keycloak, Auth0)
backend.add(import('./modules/auth'));

// AI modules — extend existing plugins
import { catalogEnricherModule } from '@internal/plugin-catalog-enricher-backend';
backend.add(catalogEnricherModule);                    // Article 2 (catalog module)

import { aiScaffoldModule } from './modules/aiScaffoldModule';
backend.add(aiScaffoldModule);                         // Article 3 (scaffolder module)

// AI plugins — standalone with own routes
import { aiCodeReviewPlugin } from '@internal/plugin-ai-code-review';
backend.add(aiCodeReviewPlugin);                       // Article 4

import { techDocsRagPlugin } from '@internal/plugin-techdocs-rag';
backend.add(techDocsRagPlugin);                        // Article 5

import { aiIncidentPlugin } from '@internal/plugin-ai-incident';
backend.add(aiIncidentPlugin);                         // Article 7

// Admin plugin
import { adminBackendPlugin } from '@internal/plugin-admin-backend';
backend.add(adminBackendPlugin);

backend.start();

La distinción importa: el catalog enricher y el AI scaffolder son modules (createBackendModule) porque extienden plugins existentes (catalog y scaffolder). Los plugins de code review, RAG e incident son standalone plugins (createBackendPlugin) porque tienen sus propias rutas HTTP.

El governance dashboard (artículo 6) es un plugin solo de frontend — lee datos del AI service a través del proxy y no necesita un backend plugin. Se registra en App.tsx, no aquí.

Cómo Añadir una Nueva Funcionalidad de IA

Cada funcionalidad de IA en Forge sigue el mismo patrón:

  1. Añadir un endpoint al AI service — Un nuevo app.MapPost en Program.cs. Recibe contexto, llama al modelo de IA, devuelve un resultado estructurado.
  2. Crear un backend plugin de Backstage — Llama al endpoint del AI service. Lee datos del catálogo como contexto. Se ejecuta en un schedule o responde a eventos.
  3. Crear un componente frontend de Backstage — Una card, página o widget que muestra resultados al desarrollador.
  4. Envolver con governance — Usar AiUsageLogger.Track() alrededor de la llamada de IA. Los logs se generan automáticamente. Las policies se aplican solas.

Ejemplo: añadir un “dependency vulnerability scanner”:

// 1. AI service endpoint
app.MapPost("/api/scan-deps", async (ScanRequest request, IConfiguration config) =>
{
    // Same pattern: create ChatClient from IConfiguration, system prompt with context
});
// 2. Backstage backend plugin
export const depScanPlugin = createBackendPlugin({
  pluginId: 'dep-scanner',
  register(env) {
    // Same pattern: schedule, iterate catalog, call AI service
  },
});
// 3. Frontend component
export const DepScanCard = () => {
  // Same pattern: fetch from proxy, render results
};

La arquitectura es la misma cada vez. Lo que cambia es la IA. La fontanería no.

Costes Reales

Con el governance dashboard del artículo 6, así se ve el uso real para un equipo de 20 desarrolladores:

FuncionalidadFrecuenciaTokens/llamadaCoste mensual
Catalog enrichmentCiclo 24h, ~30 servicios~2K input, ~500 output~$0.21
Scaffolding~10 servicios nuevos/mes~1K input, ~2K output~$0.14
Code review~200 PRs/mes~5K input, ~1K output~$3.20
RAG queries~500 preguntas/mes~3K input, ~800 output~$5.40
Incident analysis~5 incidentes/mes~4K input, ~1K output~$0.07

Total: ~$9/mes para un equipo de 20 desarrolladores. Menos que la comida de un desarrollador.

Estimado con precios de Mistral Small en Scaleway Generative APIs. Las llamadas de embedding (RAG indexing) usan bge-multilingual-gemma2 — coste insignificante. Si cambias a Claude Sonnet o GPT-5 vía Azure AI Foundry, los costes son más altos pero la arquitectura es la misma.

El governance dashboard rastrea esto automáticamente. Si los costes crecen, lo ves. Si un equipo está haciendo demasiadas RAG queries, puedes poner un límite diario.

Seguridad: Lo Que Falta

Dejamos la seguridad fuera a propósito. Esta serie va de funcionalidad — catalog enrichment, scaffolding, code review, RAG, governance, incident response. Cada artículo se centra en hacer que la funcionalidad funcione, no en hardenizarla.

Pero no lleves esto a producción sin arreglar estas cosas:

  • Sin autenticación en los endpoints de IA — Cualquiera con acceso a la red puede llamar a /api/enrich, /api/scaffold, o cualquier otro endpoint. No hay validación JWT, ni API key check, nada.
  • Sin validación de input — Un usuario podría enviar un prompt manipulado en el campo Description que haga que la IA haga algo inesperado. La prompt injection es real.
  • Sin rate limiting — Un simple bucle llamando a /api/ask repetidamente podría quemar tu presupuesto del AI provider en minutos. Las governance policies solo bloquean después de alcanzar el límite diario — no hacen throttle de requests.
  • Sin scrubbing de PII — Código fuente, logs de errores y datos de incidentes van directamente al AI provider. Si esos datos contienen información personal, salen de tu infraestructura.

La serie AI en Producción cubre todo esto:

Mismo servicio .NET, misma arquitectura. La serie de producción añade las capas que necesitas antes de que usuarios reales lo toquen.

La Serie

ArtículoQué hacePlugin
1. Por Qué Tu IDP No AyudaSetup de Backstage + catálogo + template estático
2. Enseñar a Tu Catálogo a PensarLa IA lee código, actualiza metadata del catálogocatalog-enricher-backend
3. Software Templates con IAScaffolder que entiende lenguaje natural + GOTCHA.mdai-scaffolder
4. El Plugin de AI Code ReviewPR review con contexto del catálogo + heurísticas GOTCHAai-code-review
5. TechDocs RAGBúsqueda vectorial sobre documentación de la plataformatechdocs-rag
6. El Dashboard de AI GovernanceTracking de uso, estimación de costes, control de policiesai-governance
7. Incident Response Asistido por IADiagnóstico automático de incidentes desde catálogo + logsai-incident
8. La Arquitectura de ReferenciaEste artículo — cómo encaja todo

Troubleshooting

Problemas comunes al montar Forge:

  • El AI service devuelve 502 — Endpoint o API key incorrectos. Comprueba AI:Endpoint y AI:Key en appsettings.json. Prueba a llamar a /healthz directamente: curl http://localhost:5100/healthz.
  • Backstage no llega al AI service — Comprueba que la configuración del proxy en app-config.yaml apunta a http://localhost:5100. El proxy endpoint debería ser /ai-service.
  • La extensión pgvector no se encuentra — Ejecuta CREATE EXTENSION IF NOT EXISTS vector; en tu base de datos PostgreSQL antes de crear la tabla doc_chunks.
  • Errores de CORS en el navegador — El frontend corre en 3456, el backend en 7009, el AI service en 5100. Los tres tienen que estar en la configuración CORS del Program.cs.
  • El catalog enricher no se ejecuta — Comprueba catalogEnricher.aiServiceUrl en app-config.yaml. El module se ejecuta en un schedule — mira los logs de Backstage para ver errores.
  • TechDocs RAG devuelve vacío — Indexa documentos primero con /api/index-doc antes de consultar con /api/ask. Sin índice = sin resultados.
  • La búsqueda no funciona — Necesitas @backstage/plugin-search-backend-module-pg para búsqueda con PostgreSQL. La búsqueda in-memory por defecto no persiste.

Qué Viene Después

Forge es un punto de partida. La arquitectura soporta cualquier funcionalidad de IA que siga el mismo patrón: contexto del catálogo, inteligencia del modelo, control del governance.

Tus servicios están catalogados y mejorados con IA. Ahora haz lo mismo con tu infraestructura. La siguiente serie — Infrastructure Hub — extiende Forge para gestionar módulos Terraform, recursos cloud y entornos multi-tenant. Mismo Backstage, mismo AI service, misma filosofía.

La idea: registrar módulos Terraform como entidades del catálogo, generar golden-path modules con IA (usando el mismo endpoint /api/scaffold-terraform que ya tienes), y gestionar infraestructura para equipos internos y clientes MSP desde una sola instancia de Backstage. Stay tuned.

El código está en GitHub: victorZKov/forge. Cada artículo tiene su tag correspondiente (article-01 hasta article-08).

Y si quieres estructurar tus prompts de IA para mejores resultados — no solo en el IDP, sino en cualquier desarrollo asistido por IA — echa un vistazo a la serie ATLAS + GOTCHA. De ahí viene el formato de prompt GOTCHA usado en toda esta serie.


Si esta serie te ayuda, puedes invitarme a un café.

Este es el artículo 8 — el artículo final de la serie AI-Native IDP. Anterior: Incident Response Asistido por IA.

Comments

Loading comments...