The Infrastructure Hub -- Parte 4

Pipelines desde Backstage — IA, Change Requests y la realidad enterprise

#platform-engineering #backstage #pipelines #ci-cd #azure-devops #github-actions #gitlab-ci #change-management #ai

El problema

Tu equipo de infraestructura usa tres plataformas de CI/CD. El cliente A esta en Azure DevOps. El cliente B usa GitHub Actions. El cliente C tiene todo en GitLab — self-hosted, porque su equipo de compliance lo decidio asi.

Tres interfaces. Tres flujos de autenticacion. Tres formas de ver los logs. Eso es molesto, pero no es el problema real.

El problema real es lo que pasa despues de que el pipeline ejecuta terraform plan.

En una startup, haces push a main y se despliega. En una empresa grande, haces push a main y… no pasa nada. Porque antes de que algo llegue a produccion, necesitas:

  1. Un Change Request (CR) con una descripcion de que cambia, por que, y cual es el plan de rollback
  2. Una evaluacion de riesgo — es un cambio estandar o necesita aprobacion del CAB?
  3. Evidencia — el output del plan, resultados del scan de seguridad, resultados de tests, quien aprobo el PR
  4. Aprobacion del CAB — el Change Advisory Board revisa los cambios de alto riesgo antes de que pasen a produccion
  5. Una revision post-implementacion — funciono el cambio? Hubo incidentes?

Hoy, los ingenieros hacen esto a mano. Copian el output de terraform plan en un ticket de ServiceNow. Escriben la evaluacion de riesgo de memoria. Adjuntan capturas de pantalla de las ejecuciones del pipeline como evidencia. Esperan a la reunion del CAB del jueves. Y despliegan el viernes por la tarde porque es la primera ventana disponible.

Este proceso existe por buenas razones — los cambios en produccion necesitan control. Pero la forma en que la mayoria de empresas lo hacen desperdicia horas de tiempo de ingenieria en papeleo que la IA podria preparar en segundos.

La solucion

Combinamos tres cosas:

  1. Backstage como la interfaz unificada de pipelines — un solo sitio para ver todos los pipelines de Azure DevOps, GitHub Actions y GitLab CI
  2. IA que hace el trabajo aburrido — genera documentacion de change requests, resume los planes de Terraform en lenguaje claro, diagnostica fallos de pipeline y prepara evaluaciones de riesgo
  3. Un workflow de change management que se adapta a la empresa — desde totalmente automatizado (cambios estandar de bajo riesgo) hasta revisado por el CAB (cambios de alto riesgo en produccion)

La idea clave: la IA no reemplaza al CAB. La IA prepara todo lo que el CAB necesita para tomar una decision rapida. El ingeniero sube el codigo. La IA genera el CR, la evaluacion de riesgo, el paquete de evidencia y el plan de rollback. El CAB recibe una solicitud completa y bien estructurada en lugar de un ticket escrito deprisa a las 4 de la tarde un miercoles.

Y para los cambios estandar — los que tu organizacion ha pre-aprobado (como actualizar el valor de un tag o escalar un node pool) — la IA los clasifica automaticamente y pasan sin necesidad de reunion.

Execute

El catalog-info.yaml de cada modulo ya existe desde el articulo 1. Anade anotaciones que le digan a Backstage donde vive el pipeline:

# Azure DevOps
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: tf-azurerm-storage-account
  title: Azure Storage Account Module
  annotations:
    dev.azure.com/project-repo: acme-org/acme-infra-modules
    dev.azure.com/build-definition: tf-storage-account-ci
    forge.io/change-policy: standard  # or "cab-required" or "auto-approve"
  tags:
    - terraform-module
    - azure
    - client-acme
spec:
  type: terraform-module
  lifecycle: production
  owner: team-acme
  system: client-acme-infrastructure
# GitHub Actions
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: tf-scaleway-instance
  title: Scaleway Instance Module
  annotations:
    github.com/project-slug: victorZKov/forge
    github.com/workflows: tf-scaleway-instance-ci.yml
    forge.io/change-policy: standard
spec:
  type: terraform-module
  lifecycle: production
  owner: team-globex
  system: client-globex-infrastructure
# GitLab CI
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: tf-aws-vpc
  title: AWS VPC Module
  annotations:
    gitlab.com/project-slug: client-initech/infra-modules
    gitlab.com/pipeline-branch: main
    forge.io/change-policy: cab-required  # VPC changes always need CAB
spec:
  type: terraform-module
  lifecycle: production
  owner: team-initech
  system: client-initech-infrastructure

Fijate en forge.io/change-policy. Esta anotacion controla lo que pasa despues de que el pipeline se ejecuta. La usaremos en el Paso 5.

Paso 2: Instalar los plugins de CI/CD y conectar la pagina de entidad

Instala los plugins de la comunidad para las tres plataformas:

# Azure DevOps
yarn --cwd packages/app add @backstage-community/plugin-azure-devops
yarn --cwd packages/backend add @backstage-community/plugin-azure-devops-backend

# GitHub Actions
yarn --cwd packages/app add @backstage-community/plugin-github-actions

# GitLab
yarn --cwd packages/app add @immobiliarelabs/backstage-plugin-gitlab
yarn --cwd packages/backend add @immobiliarelabs/backstage-plugin-gitlab-backend

Configura las credenciales en app-config.yaml:

integrations:
  azure:
    - host: dev.azure.com
      credentials:
        - organizations:
            - acme-org
          personalAccessToken: ${AZURE_DEVOPS_PAT}
  github:
    - host: github.com
      token: ${GITHUB_TOKEN}
  gitlab:
    - host: gitlab.com
      token: ${GITLAB_TOKEN}

Conecta los plugins en la pagina de entidad con EntitySwitch — Backstage detecta que anotaciones estan presentes y renderiza el plugin correcto:

// packages/app/src/components/catalog/EntityPage.tsx
import {
  EntityAzurePipelinesContent,
  isAzureDevOpsAvailable,
} from '@backstage-community/plugin-azure-devops';
import {
  EntityGithubActionsContent,
  isGithubActionsAvailable,
} from '@backstage-community/plugin-github-actions';
import {
  EntityGitlabPipelinesTable,
  isGitlabAvailable,
} from '@immobiliarelabs/backstage-plugin-gitlab';

const cicdContent = (
  <EntitySwitch>
    <EntitySwitch.Case if={isAzureDevOpsAvailable}>
      <EntityAzurePipelinesContent defaultLimit={10} />
    </EntitySwitch.Case>
    <EntitySwitch.Case if={isGithubActionsAvailable}>
      <EntityGithubActionsContent />
    </EntitySwitch.Case>
    <EntitySwitch.Case if={isGitlabAvailable}>
      <EntityGitlabPipelinesTable />
    </EntitySwitch.Case>
    <EntitySwitch.Case>
      <EmptyState
        title="No CI/CD configured"
        description="Add pipeline annotations to this module's catalog-info.yaml"
        missing="info"
      />
    </EntitySwitch.Case>
  </EntitySwitch>
);

Esto es Backstage estandar. Nada nuevo aqui. El valor real empieza ahora.

Paso 3: Resumenes de Terraform plan con IA

Cuando un pipeline ejecuta terraform plan, el output es tecnico. Un plan de 200 lineas con direcciones de recursos, cambios de atributos y avisos de force-replacement. El ingeniero lo entiende. El miembro del CAB que revisa el change request? Quizas no.

La IA lee el plan y produce un resumen que cualquiera puede entender:

// plugins/pipeline-ai-backend/src/services/PlanSummaryService.ts
import { CatalogClient } from '@backstage/catalog-client';

interface PlanSummary {
  humanReadable: string;
  riskLevel: 'low' | 'medium' | 'high' | 'critical';
  resourcesCreated: number;
  resourcesModified: number;
  resourcesDestroyed: number;
  destructiveChanges: string[];
  rollbackStrategy: string;
}

export class PlanSummaryService {
  constructor(
    private readonly aiBaseUrl: string,
    private readonly catalogClient: CatalogClient,
  ) {}

  async summarizePlan(
    planJson: string,
    entityRef: string,
  ): Promise<PlanSummary> {
    // Get module context from catalog
    const entity = await this.catalogClient.getEntityByRef(entityRef);
    const moduleDescription = entity?.metadata.description || '';
    const client = entity?.metadata.tags
      ?.find(t => t.startsWith('client-'))
      ?.replace('client-', '') || 'unknown';
    const changePolicy = entity?.metadata.annotations?.['forge.io/change-policy'] || 'standard';

    const response = await fetch(`${this.aiBaseUrl}/api/pipeline/summarize-plan`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        plan: planJson,
        context: {
          module: entity?.metadata.name,
          description: moduleDescription,
          client,
          changePolicy,
        },
        prompt: `You are reviewing a Terraform plan for an infrastructure module.

Module: ${entity?.metadata.name}
Client: ${client}
Description: ${moduleDescription}
Change policy: ${changePolicy}

Analyze this Terraform plan and provide:

1. A plain-English summary of what will change (2-3 sentences, understandable by a non-technical CAB reviewer)
2. Risk level: low (tags, descriptions), medium (config changes, scaling), high (networking, security groups, IAM), critical (destroy/recreate stateful resources)
3. List any destructive changes (resources being destroyed or replaced)
4. A rollback strategy specific to these changes

Be direct. No filler. If this plan destroys a database, say it clearly.`,
      }),
    });

    return response.json();
  }
}

El servicio de IA (el mismo servicio .NET de la serie IDP) procesa el plan:

// AiService/Controllers/PipelineController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.AI;

namespace AiService.Controllers;

[ApiController]
[Route("api/pipeline")]
public class PipelineController : ControllerBase
{
    private readonly IChatClient _chat;
    private readonly ILogger<PipelineController> _logger;

    public PipelineController(IChatClient chat, ILogger<PipelineController> logger)
    {
        _chat = chat;
        _logger = logger;
    }

    [HttpPost("summarize-plan")]
    public async Task<IActionResult> SummarizePlan([FromBody] PlanSummaryRequest request)
    {
        var messages = new List<ChatMessage>
        {
            new(ChatRole.System, request.Prompt),
            new(ChatRole.User, $"Terraform plan output:\n\n{request.Plan}"),
        };

        var response = await _chat.GetResponseAsync(messages);

        // Parse structured output from AI response
        var summary = ParsePlanSummary(response.Text, request.Plan);

        _logger.LogInformation(
            "Plan summary for {Module}: risk={Risk}, destroy={Destroy}",
            request.Context.Module,
            summary.RiskLevel,
            summary.ResourcesDestroyed);

        return Ok(summary);
    }

    private PlanSummary ParsePlanSummary(string aiResponse, string rawPlan)
    {
        // Count resources from the raw plan
        var created = CountPattern(rawPlan, "will be created");
        var modified = CountPattern(rawPlan, "will be updated");
        var destroyed = CountPattern(rawPlan, "will be destroyed");
        var replaced = CountPattern(rawPlan, "must be replaced");

        // AI determines risk level and writes the summary
        var riskLevel = destroyed + replaced > 0 ? "critical"
            : aiResponse.Contains("high", StringComparison.OrdinalIgnoreCase) ? "high"
            : modified > 3 ? "medium"
            : "low";

        return new PlanSummary
        {
            HumanReadable = aiResponse,
            RiskLevel = riskLevel,
            ResourcesCreated = created,
            ResourcesModified = modified,
            ResourcesDestroyed = destroyed + replaced,
        };
    }

    private static int CountPattern(string text, string pattern) =>
        text.Split(pattern).Length - 1;
}

El pipeline publica este resumen como un comentario en el PR. Asi es como se ve:

Terraform Plan Summary (AI-generated)

Este plan modifica el node pool del cluster AKS del cliente ACME. Escalara el node pool por defecto de 3 a 5 nodos y actualizara la version de Kubernetes de 1.29 a 1.30. El cluster hara un rolling upgrade — no se espera downtime, pero los pods se reprogramaran durante el proceso.

MetricaValor
Recursos creados0
Recursos modificados2
Recursos destruidos0
Nivel de riesgoMedio

Estrategia de rollback: Escalar el node pool a 3 nodos via terraform apply con los valores de variables anteriores. El downgrade de Kubernetes de 1.30 a 1.29 no esta soportado — necesitaria recrear el cluster.

El revisor del CAB lee esto en 30 segundos en lugar de analizar 200 lineas de output de Terraform.

Paso 4: Diagnostico de fallos de pipeline con IA

Cuando un pipeline falla, el ingeniero abre los logs, hace scroll por 500 lineas y averigua que salio mal. A veces es obvio (error de sintaxis). A veces lleva 20 minutos (conflicto de version del provider, state lock, problema de permisos).

La IA lee los logs, el codigo del modulo y los cambios recientes — y te dice que paso:

// plugins/pipeline-ai-backend/src/services/FailureDiagnosisService.ts
export class FailureDiagnosisService {
  constructor(
    private readonly aiBaseUrl: string,
    private readonly catalogClient: CatalogClient,
  ) {}

  async diagnoseFailure(input: {
    entityRef: string;
    pipelineLogs: string;
    recentCommits: string[];
    platform: string;
  }): Promise<FailureDiagnosis> {
    const entity = await this.catalogClient.getEntityByRef(input.entityRef);

    const response = await fetch(`${this.aiBaseUrl}/api/pipeline/diagnose`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        logs: input.pipelineLogs,
        module: entity?.metadata.name,
        description: entity?.metadata.description,
        recentCommits: input.recentCommits,
        platform: input.platform,
        prompt: `You are diagnosing a CI/CD pipeline failure for a Terraform module.

Module: ${entity?.metadata.name}
Platform: ${input.platform}
Recent commits: ${input.recentCommits.join('\n')}

Analyze the pipeline logs and provide:
1. Root cause (one sentence)
2. Evidence (the specific log lines that confirm the cause)
3. Fix (specific steps to resolve — not generic advice)
4. Prevention (what to add to the pipeline or CLAUDE.md to prevent this)

If the root cause is in the recent commits, say which commit caused it.`,
      }),
    });

    return response.json();
  }
}

El diagnostico aparece en la pagina de entidad de Backstage, justo al lado de la ejecucion fallida del pipeline:

// plugins/pipeline-dashboard/src/components/FailureDiagnosisCard.tsx
import React, { useEffect, useState } from 'react';
import { InfoCard, WarningPanel } from '@backstage/core-components';
import { useApi, discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api';
import { useEntity } from '@backstage/plugin-catalog-react';

interface Diagnosis {
  rootCause: string;
  evidence: string[];
  fix: string[];
  prevention: string;
}

export const FailureDiagnosisCard = ({ runId }: { runId: string }) => {
  const { entity } = useEntity();
  const discoveryApi = useApi(discoveryApiRef);
  const fetchApi = useApi(fetchApiRef);
  const [diagnosis, setDiagnosis] = useState<Diagnosis | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const load = async () => {
      const baseUrl = await discoveryApi.getBaseUrl('pipeline-ai');
      const res = await fetchApi.fetch(
        `${baseUrl}/diagnose/${entity.metadata.name}/${runId}`,
      );
      if (res.ok) {
        setDiagnosis(await res.json());
      }
      setLoading(false);
    };
    load();
  }, [discoveryApi, fetchApi, entity.metadata.name, runId]);

  if (loading) return <InfoCard title="AI Diagnosis">Analyzing failure...</InfoCard>;
  if (!diagnosis) return null;

  return (
    <WarningPanel
      title="AI Failure Diagnosis"
      message={diagnosis.rootCause}
      severity="error"
    >
      <div>
        <h4>Evidence</h4>
        <pre>{diagnosis.evidence.join('\n')}</pre>
        <h4>How to fix</h4>
        <ol>
          {diagnosis.fix.map((step, i) => <li key={i}>{step}</li>)}
        </ol>
        <h4>Prevention</h4>
        <p>{diagnosis.prevention}</p>
      </div>
    </WarningPanel>
  );
};

Ejemplo real de lo que produce el diagnostico:

Root cause: Azure provider 4.x elimino el argumento enable_rbac de azurerm_kubernetes_cluster. Ahora siempre esta activado.

Evidence:

Error: Unsupported argument "enable_rbac"
  on main.tf line 47, in resource "azurerm_kubernetes_cluster":

Fix:

  1. Elimina enable_rbac = true de main.tf linea 47
  2. Ejecuta terraform plan para confirmar que no hay cambios en el state
  3. Commit: “Remove deprecated enable_rbac argument (always true in provider 4.x)”

Prevention: Anade un paso de terraform validate antes de terraform plan en el pipeline. Los templates del scaffolder del articulo 2 ya lo incluyen — este modulo fue creado antes de que existiera el golden path.

Paso 5: El workflow de change management enterprise

Aqui es donde la cosa se pone seria. En una startup, los pasos 3 y 4 son suficientes. Pero en una empresa grande — un banco, una compania energetica, un proveedor de salud — no despliegas solo porque el plan tiene buena pinta. Necesitas un Change Request.

El workflow depende de la organizacion. Algunas empresas tienen tres niveles. Otras tienen cinco. Algunas requieren una reunion del CAB para todo. Otras tienen cambios estandar pre-aprobados que pasan automaticamente. La IA se adapta a lo que tu organizacion necesite.

Este es el modelo que usamos:

Tipo de cambioRiesgoQue pasa
StandardBajo — tags, descripciones, escalado dentro de limitesAuto-aprobado. La IA crea el CR, adjunta evidencia, lo cierra. Sin humanos en el proceso.
NormalMedio — cambios de config, nuevos recursos, reglas de security groupsLa IA prepara el paquete completo del CR. El team lead aprueba en Backstage. Sin reunion del CAB.
EmergencyNo planificado — fix de incidente, hotfixDespliega primero, documenta despues. La IA crea el CR post-hoc con toda la evidencia.
CAB-requiredAlto — networking, IAM, destroy/recreate, cross-clientLa IA prepara el CR + evaluacion de riesgo + plan de rollback. Va a la cola del CAB. El CAB revisa en Backstage, no en una sala de reuniones.

La anotacion forge.io/change-policy en el catalogo le dice a la IA la politica por defecto de cada modulo. Pero la IA puede sobreescribirla — si un modulo “standard” tiene un plan que destruye recursos, la IA lo escala a “cab-required” automaticamente.

// plugins/change-management-backend/src/services/ChangeRequestService.ts
import { CatalogClient } from '@backstage/catalog-client';
import { PlanSummary } from '../types';

interface ChangeRequest {
  id: string;
  module: string;
  client: string;
  type: 'standard' | 'normal' | 'emergency' | 'cab-required';
  status: 'draft' | 'pending-approval' | 'approved' | 'rejected' | 'implemented' | 'closed';
  summary: string;
  riskAssessment: RiskAssessment;
  evidence: Evidence;
  rollbackPlan: string;
  createdBy: string;
  approvedBy?: string;
  implementedAt?: string;
}

interface RiskAssessment {
  level: string;
  factors: string[];
  blastRadius: string;
  affectedServices: string[];
}

interface Evidence {
  planSummary: PlanSummary;
  prUrl: string;
  prApprovers: string[];
  securityScan: string;
  testResults: string;
  pipelineRunUrl: string;
}

export class ChangeRequestService {
  constructor(
    private readonly aiBaseUrl: string,
    private readonly catalogClient: CatalogClient,
    private readonly db: any,
  ) {}

  async createFromPipeline(input: {
    entityRef: string;
    planSummary: PlanSummary;
    prUrl: string;
    prApprovers: string[];
    pipelineRunUrl: string;
    triggeredBy: string;
  }): Promise<ChangeRequest> {
    const entity = await this.catalogClient.getEntityByRef(input.entityRef);
    const defaultPolicy = entity?.metadata.annotations?.['forge.io/change-policy'] || 'normal';

    // AI decides the actual change type based on the plan
    const changeType = this.determineChangeType(defaultPolicy, input.planSummary);

    // AI generates the risk assessment
    const riskAssessment = await this.generateRiskAssessment(
      entity, input.planSummary,
    );

    // AI generates the rollback plan
    const rollbackPlan = await this.generateRollbackPlan(
      entity, input.planSummary,
    );

    const cr: ChangeRequest = {
      id: `CR-${Date.now()}`,
      module: entity?.metadata.name || 'unknown',
      client: entity?.metadata.tags
        ?.find(t => t.startsWith('client-'))
        ?.replace('client-', '') || 'unknown',
      type: changeType,
      status: changeType === 'standard' ? 'approved' : 'pending-approval',
      summary: input.planSummary.humanReadable,
      riskAssessment,
      evidence: {
        planSummary: input.planSummary,
        prUrl: input.prUrl,
        prApprovers: input.prApprovers,
        securityScan: 'passed', // from pipeline
        testResults: 'passed',  // from pipeline
        pipelineRunUrl: input.pipelineRunUrl,
      },
      rollbackPlan,
      createdBy: input.triggeredBy,
      approvedBy: changeType === 'standard' ? 'auto-approved' : undefined,
    };

    await this.db.changeRequests.insert(cr);

    return cr;
  }

  private determineChangeType(
    defaultPolicy: string,
    plan: PlanSummary,
  ): ChangeRequest['type'] {
    // AI can escalate but never downgrade
    if (plan.riskLevel === 'critical' || plan.resourcesDestroyed > 0) {
      return 'cab-required';
    }
    if (plan.riskLevel === 'high') {
      return defaultPolicy === 'cab-required' ? 'cab-required' : 'normal';
    }
    if (plan.riskLevel === 'low' && defaultPolicy === 'standard') {
      return 'standard';
    }
    return defaultPolicy as ChangeRequest['type'];
  }

  private async generateRiskAssessment(
    entity: any,
    plan: PlanSummary,
  ): Promise<RiskAssessment> {
    const response = await fetch(`${this.aiBaseUrl}/api/pipeline/risk-assessment`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        module: entity?.metadata.name,
        client: entity?.metadata.tags?.find((t: string) => t.startsWith('client-')),
        plan,
        prompt: `Assess the risk of this infrastructure change.

Consider:
- Blast radius: how many services or users are affected if this goes wrong?
- Reversibility: can we undo this change quickly?
- Timing: is this a high-traffic period?
- Dependencies: do other modules depend on this one?

Return: risk level, risk factors (list), blast radius (sentence), affected services (list).
Be honest. If it's low risk, say so. Don't inflate risk to look thorough.`,
      }),
    });

    return response.json();
  }

  private async generateRollbackPlan(
    entity: any,
    plan: PlanSummary,
  ): Promise<string> {
    const response = await fetch(`${this.aiBaseUrl}/api/pipeline/rollback-plan`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        module: entity?.metadata.name,
        plan,
        prompt: `Write a rollback plan for this Terraform change.

Be specific. Include:
1. Exact steps to rollback (terraform commands, git commands)
2. Expected time to rollback
3. What to verify after rollback
4. Any data that cannot be recovered (if resources are destroyed)

Keep it short. An engineer at 2am should be able to follow this.`,
      }),
    });

    const data = await response.json();
    return data.rollbackPlan;
  }
}

Paso 6: La interfaz de revision del CAB en Backstage

El CAB no necesita abrir ServiceNow. No necesita una sala de reuniones. Revisan los cambios en Backstage, donde todo el contexto esta disponible:

// plugins/change-management/src/components/ChangeRequestReview.tsx
import React, { useEffect, useState } from 'react';
import {
  Page, Header, Content, InfoCard,
  Table, TableColumn, StatusOK, StatusError,
  StatusPending, StatusWarning,
} from '@backstage/core-components';
import { Button, Chip, Typography } from '@material-ui/core';
import { useApi, discoveryApiRef, fetchApiRef, identityApiRef } from '@backstage/core-plugin-api';

interface ChangeRequest {
  id: string;
  module: string;
  client: string;
  type: string;
  status: string;
  summary: string;
  riskAssessment: {
    level: string;
    factors: string[];
    blastRadius: string;
  };
  evidence: {
    prUrl: string;
    prApprovers: string[];
    securityScan: string;
    testResults: string;
    pipelineRunUrl: string;
  };
  rollbackPlan: string;
  createdBy: string;
}

const RiskChip = ({ level }: { level: string }) => {
  const colors: Record<string, 'default' | 'primary' | 'secondary'> = {
    low: 'default',
    medium: 'primary',
    high: 'secondary',
    critical: 'secondary',
  };
  return <Chip label={level.toUpperCase()} color={colors[level] || 'default'} size="small" />;
};

export const ChangeRequestReview = () => {
  const discoveryApi = useApi(discoveryApiRef);
  const fetchApi = useApi(fetchApiRef);
  const identityApi = useApi(identityApiRef);
  const [requests, setRequests] = useState<ChangeRequest[]>([]);

  useEffect(() => {
    const load = async () => {
      const baseUrl = await discoveryApi.getBaseUrl('change-management');
      const res = await fetchApi.fetch(`${baseUrl}/requests?status=pending-approval`);
      const data = await res.json();
      setRequests(data.requests);
    };
    load();
  }, [discoveryApi, fetchApi]);

  const handleApprove = async (crId: string) => {
    const { userEntityRef } = await identityApi.getBackstageIdentity();
    const baseUrl = await discoveryApi.getBaseUrl('change-management');
    await fetchApi.fetch(`${baseUrl}/requests/${crId}/approve`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ approvedBy: userEntityRef }),
    });
    setRequests(prev => prev.filter(r => r.id !== crId));
  };

  const handleReject = async (crId: string, reason: string) => {
    const baseUrl = await discoveryApi.getBaseUrl('change-management');
    await fetchApi.fetch(`${baseUrl}/requests/${crId}/reject`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ reason }),
    });
    setRequests(prev => prev.filter(r => r.id !== crId));
  };

  return (
    <Page themeId="tool">
      <Header
        title="Change Advisory Board"
        subtitle={`${requests.length} changes pending review`}
      />
      <Content>
        {requests.map(cr => (
          <InfoCard
            key={cr.id}
            title={`${cr.id} — ${cr.module}`}
            subheader={`Client: ${cr.client} | Type: ${cr.type} | By: ${cr.createdBy}`}
          >
            <Typography variant="body1" paragraph>
              {cr.summary}
            </Typography>

            <Typography variant="h6">Risk Assessment</Typography>
            <RiskChip level={cr.riskAssessment.level} />
            <Typography variant="body2">
              Blast radius: {cr.riskAssessment.blastRadius}
            </Typography>
            <ul>
              {cr.riskAssessment.factors.map((f, i) => (
                <li key={i}>{f}</li>
              ))}
            </ul>

            <Typography variant="h6">Evidence</Typography>
            <ul>
              <li>PR: <a href={cr.evidence.prUrl}>View PR</a> (approved by {cr.evidence.prApprovers.join(', ')})</li>
              <li>Security scan: {cr.evidence.securityScan}</li>
              <li>Tests: {cr.evidence.testResults}</li>
              <li>Pipeline: <a href={cr.evidence.pipelineRunUrl}>View run</a></li>
            </ul>

            <Typography variant="h6">Rollback Plan</Typography>
            <pre style={{ background: '#f5f5f5', padding: '12px', borderRadius: '4px' }}>
              {cr.rollbackPlan}
            </pre>

            <div style={{ marginTop: '16px', display: 'flex', gap: '8px' }}>
              <Button
                variant="contained"
                color="primary"
                onClick={() => handleApprove(cr.id)}
              >
                Approve
              </Button>
              <Button
                variant="outlined"
                color="secondary"
                onClick={() => handleReject(cr.id, 'Needs more context')}
              >
                Reject
              </Button>
            </div>
          </InfoCard>
        ))}
      </Content>
    </Page>
  );
};

Paso 7: El flujo completo del pipeline

Asi es como se conecta todo. Cuando un ingeniero sube un cambio a un modulo de Terraform:

1. Push to branch → PR created
2. Pipeline runs: terraform fmt → terraform validate → terraform plan
3. AI reads the plan → generates human-readable summary → posts as PR comment
4. PR approved by team → merge to main
5. Main pipeline runs: terraform plan (again, for the CR)
6. AI creates Change Request:
   - Reads plan summary (from step 3)
   - Reads module context from catalog
   - Generates risk assessment
   - Generates rollback plan
   - Attaches all evidence (PR, approvers, scan, tests, pipeline URL)
   - Determines change type (standard / normal / cab-required)
7. Route based on type:
   - Standard → auto-approved → terraform apply
   - Normal → team lead approves in Backstage → terraform apply
   - CAB-required → CAB reviews in Backstage → approve/reject → terraform apply
8. Post-implementation:
   - AI verifies the apply succeeded
   - CR status updated to "implemented"
   - If apply fails → AI diagnoses failure → CR updated with incident details

El espectro de la realidad enterprise

No todas las organizaciones son iguales. Asi es como el mismo sistema se adapta:

Startup / equipo pequeno: Salta los pasos 5-7. El resumen del plan y el diagnostico de fallos son suficientes. Despliegas al hacer merge.

Empresa mediana: Usa los tipos de cambio “standard” y “normal”. Los team leads aprueban los cambios normales. Sin reuniones del CAB. La documentacion generada por IA te da audit trail para compliance sin la carga de trabajo.

Enterprise regulado (banco, energia, salud): Workflow completo del CAB. Pero el CAB se reune con menos frecuencia porque la IA lo prepara todo. Un cambio que tardaba 3 dias en pasar por el CAB ahora tarda 3 horas — porque el CR esta completo, bien estructurado, e incluye evaluacion de riesgo y plan de rollback. El revisor del CAB tarda 2 minutos en leer en lugar de 20 minutos haciendo preguntas.

MSP gestionando multiples clientes: Cada cliente puede tener politicas de cambio diferentes. El cliente ACME quiere CAB para todo. El cliente Globex confia en auto-aprobacion para cambios estandar. La anotacion forge.io/change-policy por modulo lo gestiona — mismo Backstage, reglas diferentes.

El punto es: la IA no elimina el proceso. La IA elimina el papeleo. Las decisiones se quedan con los humanos. Pero los humanos reciben mejor informacion, mas rapido.

El dashboard unificado

El dashboard de pipelines desde la perspectiva del equipo de plataforma ahora incluye el estado del change request:

// Extended PipelineRun interface
interface PipelineRun {
  module: string;
  client: string;
  platform: 'azure-devops' | 'github' | 'gitlab';
  status: 'success' | 'failed' | 'running' | 'pending';
  branch: string;
  startedAt: string;
  duration: string;
  url: string;
  // New: change management fields
  changeRequest?: {
    id: string;
    type: string;
    status: string;
    riskLevel: string;
  };
  aiDiagnosis?: string;  // populated when status === 'failed'
}

Una tabla. Todos los pipelines. Todas las plataformas. Todos los clientes. Con estado del change request, nivel de riesgo y diagnostico de IA para los fallos. Sin mas cambiar entre ServiceNow, Azure DevOps, GitHub y GitLab.

Checklist

  • Cada modulo de Terraform tiene anotaciones de pipeline en catalog-info.yaml
  • Anotacion forge.io/change-policy configurada por modulo
  • Plugins de CI/CD instalados para todas las plataformas usadas
  • El resumen del plan con IA se publica como comentario en el PR en cada terraform plan
  • El diagnostico de fallos con IA se activa automaticamente cuando falla un pipeline
  • Change Request creado automaticamente despues del merge a main
  • Los cambios estandar se auto-aprueban y despliegan
  • Los cambios normales se enrutan al team lead para aprobacion
  • Los cambios que requieren CAB aparecen en la pagina de revision del CAB
  • Plan de rollback generado para cada change request
  • Paquete de evidencia completo: PR, aprobadores, scan, tests, URL del pipeline

Challenge

Antes del siguiente articulo:

  1. Anade forge.io/change-policy a uno de tus modulos
  2. Configura el resumen del plan con IA — aunque no tengas el workflow completo de CR, tener resumenes legibles de los planes en tus PRs es un quick win
  3. Piensa en los tipos de cambio de tu organizacion — que seria “standard” (auto-aprobacion), “normal” (team lead) y “cab-required”?

En el siguiente articulo, construimos Secrets and Post-Quantum Identities — gestionar secretos, rotar credenciales y usar IA para detectar secret sprawl y tokens expirados en tu infraestructura. Porque tus API keys en Key Vault estan bien hoy, pero no lo estaran para siempre.

El codigo completo esta en GitHub.

Si esta serie te ayuda, considera invitarme a un cafe.

Este es el articulo 4 de la serie Infrastructure Hub. Anterior: Multi-tenant Infrastructure. Siguiente: Secrets and Post-Quantum Identities — protege las credenciales de tu infraestructura para la era cuantica.

Comments

Loading comments...