Hay un problema en Contoso que ninguna de las lecciones anteriores ha tocado. La clase Reserva, con su validación del localizador, sus reglas de fechas y su cálculo de plazas, está copiada y pegada en tres repositorios: en contoso-reservas, en contoso-api-disponibilidad y en el proceso nocturno que genera las tarjetas de embarque. Nacieron iguales, pero cada una se ha ido tocando por su cuenta y hoy hay tres versiones distintas. La consecuencia se ve en producción: la web acepta un localizador de siete caracteres que la API rechaza, y nadie sabe cuál de las tres tiene razón.

Copiar código compartido no escala. La solución es tratarlo como lo que es —una biblioteca con versión propia— y distribuirlo como un paquete. Azure Artifacts es el servicio de Azure DevOps que aloja esos paquetes de forma privada, y de paso resuelve un problema mayor del que quizá no eras consciente: la seguridad de tu cadena de suministro de dependencias.

Contenido

  1. Gestores de paquetes y feeds
  2. Tipos de paquete soportados y Universal Packages
  3. Crear el feed contoso-paquetes: ámbito y vistas
  4. Orígenes ascendentes y la confusión de dependencias
  5. Publicar y consumir contoso.reservas.modelos
  6. Versionado semántico automatizado desde el pipeline
  7. Retención, limpieza y coste
  8. Seguridad de la cadena de suministro
  9. Comparación con GitHub Packages y con un registro de contenedores
  10. Errores Comunes y Consejos
  11. Ejercicios
  12. Conclusión

  1. Gestores de paquetes y feeds

Un gestor de paquetes (NuGet, npm, Maven, pip) hace tres cosas: descarga una biblioteca y sus dependencias transitivas, resuelve las versiones compatibles entre todas ellas y deja constancia de lo que se usó. Un feed es un repositorio de paquetes: el sitio del que el gestor descarga y al que se publica.

Frente a copiar y pegar, un paquete aporta tres garantías que Contoso necesita:

Código copiado Paquete en un feed
Identidad Ninguna: tres copias sin nombre común Nombre y versión explícitos
Actualización Manual, repositorio por repositorio dotnet add package y un número que sube
Saber quién usa qué Imposible La lista de consumidores del feed
Cambios incompatibles Se descubren en producción Se anuncian subiendo la versión mayor

  1. Tipos de paquete soportados y Universal Packages

Tipo Ecosistema Uso típico en Contoso
NuGet .NET contoso.reservas.modelos, la biblioteca compartida
npm JavaScript / TypeScript Componentes web comunes del portal
Maven Java Integración con el sistema heredado de tripulaciones
Python Python Utilidades del proceso analítico de 03-06
Cargo Rust No se usa hoy
Universal Packages Cualquier cosa Ficheros que no son paquetes de código

Los Universal Packages merecen explicación porque resuelven un caso muy real. No todo lo que hay que versionar y compartir es una biblioteca: Contoso tiene un conjunto de datos de aeropuertos y rutas de 300 MB, plantillas de tarjeta de embarque en PDF y ficheros de configuración de la pasarela de pago. Nada de eso encaja en NuGet, no debe ir al repositorio Git (05-02) y sin embargo necesita versión, historial y trazabilidad. Un Universal Package es simplemente una carpeta versionada, publicada y descargada con la CLI:

# Publicar la carpeta de datos de aeropuertos como paquete universal
az artifacts universal publish \
  --organization https://dev.azure.com/contoso-airlines \
  --feed contoso-paquetes --name datos-aeropuertos --version 1.4.0 \
  --path ./datos --description "Catalogo de aeropuertos y rutas"

# Consumirlo desde un pipeline o desde una maquina
az artifacts universal download \
  --organization https://dev.azure.com/contoso-airlines \
  --feed contoso-paquetes --name datos-aeropuertos --version 1.4.0 --path ./datos

  1. Crear el feed contoso-paquetes: ámbito y vistas

# El feed se crea desde el portal (Artifacts) o con la API REST.
# Ambito de organizacion: visible desde cualquier proyecto de contoso-airlines.
az artifacts universal publish --help   # La CLI cubre publicacion y descarga

La primera decisión es el ámbito:

Ámbito de proyecto Ámbito de organización
Quién lo ve Solo el proyecto que lo contiene Cualquier proyecto de la organización
Permisos Heredados del proyecto Se gestionan aparte
Cuándo elegirlo Paquetes internos de un producto Bibliotecas compartidas entre productos

Contoso crea contoso-paquetes con ámbito de organización, porque contoso.reservas.modelos la consumirá también el proyecto contoso-millas cuando exista. Un feed de ámbito de proyecto obligaría a duplicarlo.

La segunda decisión son las vistas, que son el mecanismo de promoción. Un feed nuevo trae tres:

Vista Qué contiene Quién consume de ella
@local Todo lo publicado, más lo descargado de orígenes ascendentes Los propios pipelines de compilación
@prerelease Versiones promocionadas para probar Entornos de desarrollo y preproducción
@release Versiones aprobadas para producción Los pipelines que despliegan en producción

El flujo es el mismo patrón de puertas de 05-04, aplicado a los paquetes: cada compilación publica en @local, se promociona a @prerelease cuando pasa las pruebas de integración, y a @release solo cuando se valida. Los consumidores apuntan a la vista que les corresponde mediante la URL del feed, que incluye el nombre de la vista:

https://pkgs.dev.azure.com/contoso-airlines/_packaging/contoso-paquetes@release/nuget/v3/index.json

Una versión publicada nunca se modifica: la promoción no cambia el paquete, solo lo hace visible en otra vista. Esa inmutabilidad es lo que permite afirmar que la versión 2.3.1 es la misma en todas partes.

  1. Orígenes ascendentes y la confusión de dependencias

Un origen ascendente (upstream source) hace que tu feed actúe como intermediario del registro público: cuando alguien pide Newtonsoft.Json, el feed lo busca primero entre sus propios paquetes, y si no lo tiene lo descarga de nuget.org, guarda una copia y la sirve. Aporta dos cosas:

  • Una única URL en la configuración de todos los proyectos, en lugar de dos.
  • Protección ante la desaparición de un paquete. Si un autor retira su biblioteca del registro público —ha ocurrido, y ha roto miles de compilaciones—, la copia guardada en tu feed sigue ahí y tus pipelines siguen compilando.

Pero la razón más importante es de seguridad, y conviene entenderla bien.

La confusión de dependencias

Es un ataque real de cadena de suministro, demostrado en 2021 contra decenas de grandes empresas. Funciona así:

  1. Contoso tiene un paquete interno llamado contoso.reservas.modelos, que existe solo en su feed privado.
  2. Un atacante descubre ese nombre —aparece en un package.json filtrado, en una captura de pantalla de una charla, en un mensaje de error público— y publica en el registro público un paquete con el mismo nombre y una versión altísima, 99.0.0.
  3. Si el gestor de paquetes está configurado con dos orígenes —el feed privado y el público— muchas implementaciones consultan ambos y eligen la versión más alta. La versión más alta es la del atacante.
  4. El pipeline de Contoso descarga y ejecuta código del atacante durante la compilación, con acceso a las variables del entorno y a la red del agente.
graph TD
    A["Pipeline pide<br/>contoso.reservas.modelos"] --> B{"¿Cuántos orígenes<br/>configurados?"}
    B -->|"Dos orígenes:<br/>privado y público"| C["Compara versiones:<br/>2.3.1 privada vs 99.0.0 pública"]
    C --> D["Descarga la 99.0.0<br/>del atacante"]
    B -->|"Un solo origen:<br/>feed con upstream"| E["El feed resuelve primero<br/>sus paquetes internos"]
    E --> F["Descarga la 2.3.1<br/>legítima"]

La defensa que da Azure Artifacts es doble. Primero, un único origen configurado: el feed, con el registro público como origen ascendente, de modo que la resolución la decide el feed y los paquetes internos siempre ganan. Segundo, el feed marca los paquetes que ha guardado desde un origen ascendente y impide que un paquete público suplante a uno interno del mismo nombre. A esto se añaden dos buenas prácticas: usar un prefijo o espacio de nombres reservado para tus paquetes, y no configurar nunca varios orígenes en paralelo en el nuget.config o el .npmrc.

  1. Publicar y consumir contoso.reservas.modelos

El repositorio contoso-modelos deja de ser una carpeta copiada y pasa a tener su propio pipeline de publicación:

name: 2.3.$(Rev:r)        # MAYOR.MENOR fijas a mano; PARCHE automatico

trigger:
  branches: { include: [ main ] }

pool: { vmImage: ubuntu-latest }

variables:
  - name: feed
    value: 'contoso-airlines/contoso-paquetes'   # organizacion/feed

steps:
  - task: UseDotNet@2
    inputs: { packageType: sdk, version: '8.0.x' }

  # Autenticacion contra el feed: obtiene un token de la ejecucion, sin secretos
  - task: NuGetAuthenticate@1

  - script: dotnet build src/Contoso.Reservas.Modelos -c Release
  - script: dotnet test src/Contoso.Reservas.Modelos.Pruebas -c Release

  # El numero de version del paquete se toma del numero de compilacion:
  # una version por compilacion, sin que nadie edite un fichero a mano
  - script: |
      dotnet pack src/Contoso.Reservas.Modelos \
        -c Release -o $(Build.ArtifactStagingDirectory) \
        -p:PackageVersion=$(Build.BuildNumber)
    displayName: Empaquetar

  # Publicar en la vista @local del feed
  - task: NuGetCommand@2
    inputs:
      command: push
      packagesToPush: '$(Build.ArtifactStagingDirectory)/*.nupkg'
      nuGetFeedType: internal
      publishVstsFeed: '$(feed)'
    displayName: Publicar en contoso-paquetes

NuGetAuthenticate@1 es la pieza clave de seguridad: obtiene credenciales del propio contexto de la ejecución, así que no hay ningún token de acceso personal guardado en ninguna parte. La misma lógica de identidad sin secretos del módulo 4.

Del lado del consumidor, en contoso-reservas, el nuget.config declara un solo origen:

<configuration>
  <packageSources>
    <clear />   <!-- Imprescindible: elimina nuget.org heredado de la maquina -->
    <add key="contoso-paquetes"
         value="https://pkgs.dev.azure.com/contoso-airlines/_packaging/contoso-paquetes@release/nuget/v3/index.json" />
  </packageSources>
</configuration>

El <clear /> no es decorativo: sin él, el origen público global de la máquina sigue activo y se reabre la puerta a la confusión de dependencias.

  1. Versionado semántico automatizado desde el pipeline

Las reglas de 05-02 se aplican igual a los paquetes, con una consecuencia contractual: la versión es una promesa a quien consume.

Cambio en contoso.reservas.modelos Versión
Se añade una propiedad opcional a Reserva 2.3.1 → 2.4.0 (menor)
Se corrige la validación del localizador sin cambiar la firma 2.3.1 → 2.3.2 (parche)
Se renombra CodigoReserva a Localizador 2.3.1 → 3.0.0 (mayor)
Publicación de prueba antes de consolidar 2.4.0-preview.5

El automatismo consiste en derivar el número del pipeline. Con name: 2.3.$(Rev:r) y -p:PackageVersion=$(Build.BuildNumber), cada compilación de main produce una versión nueva sin que nadie edite un fichero: las partes mayor y menor las decide una persona al cambiar el name —porque son una decisión de diseño—, y el parche lo lleva la máquina. Las versiones de prueba se marcan con sufijo (-preview.N), y los gestores de paquetes las ignoran por defecto, lo que las hace ideales para la vista @prerelease.

  1. Retención, limpieza y coste

Aviso de coste: Azure Artifacts factura por almacenamiento total del feed, con 2 GiB gratuitos por organización y un coste creciente por encima (del orden de 2 $/GiB al mes en el primer tramo). Parece poco hasta que un pipeline publica una versión en cada compilación: 20 compilaciones diarias × 30 MB son 600 MB al mes, y en un semestre te has comido la cuota. Además, los paquetes guardados desde orígenes ascendentes también cuentan, igual que el almacenamiento de Git LFS de 05-02.

La defensa es una directiva de retención por feed: conservar las N versiones más recientes de cada paquete y eliminar automáticamente el resto pasados X días. Dos precauciones al configurarla: los paquetes promocionados a una vista nunca se eliminan por retención —que es exactamente el motivo por el que se promociona lo que importa—, y conviene revisar el consumo periódicamente en la vista de facturación, porque el crecimiento es silencioso.

  1. Seguridad de la cadena de suministro

Tus dependencias son código de terceros que se ejecuta con tus permisos. Cinco prácticas, de más elemental a más avanzada:

  1. Fijar versiones y usar ficheros de bloqueo. packages.lock.json en .NET, package-lock.json en npm: registran la versión exacta de cada dependencia, incluidas las transitivas, y hacen la compilación reproducible. Sin bloqueo, la misma confirmación puede compilar distinto mañana. Debe estar confirmado en el repositorio.
  2. Analizar vulnerabilidades. dotnet list package --vulnerable --include-transitive en el pipeline de 05-03, y herramientas de análisis de composición que cruzan tus dependencias con las bases de datos de CVE. Lo transitivo importa: casi siempre la vulnerabilidad está tres niveles por debajo de lo que tú declaraste.
  3. Evitar la confusión de dependencias con un solo origen y <clear />, como se ha visto.
  4. Generar una lista de materiales de software (SBOM), el inventario completo de todo lo que compone tu artefacto. Cuando aparece una vulnerabilidad grave en una biblioteca —el caso de Log4Shell es el ejemplo canónico—, la pregunta «¿estamos afectados?» se responde consultando el SBOM en minutos en lugar de auditando repositorios durante días. Se genera en el pipeline y se publica junto al artefacto.
  5. Firmar los paquetes, para que quien los consume pueda verificar que provienen de Contoso y no han sido alterados. El certificado vive en kv-contoso-pro (04-03) y la firma se hace en el pipeline, nunca en un portátil.

  1. Comparación con GitHub Packages y con un registro de contenedores

Azure Artifacts GitHub Packages Azure Container Registry
Qué aloja NuGet, npm, Maven, Python, Cargo, universales Los mismos, más contenedores Imágenes de contenedor y artefactos OCI
Orígenes ascendentes Muy buenos, con protección de suplantación Limitados Caché de registros públicos
Vistas y promoción Sí (@local, @prerelease, @release) No, se usan etiquetas Etiquetas y bloqueo de imagen
Facturación Por almacenamiento, 2 GiB gratuitos Por almacenamiento y transferencia Por nivel del registro

La distinción con un registro de contenedores es conceptual y conviene tenerla clara: un feed de paquetes distribuye piezas para construir una aplicación —bibliotecas que se compilan dentro de tu binario—, mientras que un registro de contenedores distribuye la aplicación entera ya construida, con su sistema de ficheros y sus dependencias del sistema operativo. No compiten: el pipeline consume paquetes de Artifacts para producir una imagen que publica en el registro. Ese registro, Azure Container Registry, es justamente donde arranca la lección 06-01, y volverá a aparecer en 05-06 como almacén de módulos de Bicep.

Errores Comunes y Consejos

  • Copiar y pegar la biblioteca compartida. Es el problema con el que empezó la lección: tres copias, tres verdades y un fallo en producción.
  • Configurar el feed privado y el público en paralelo. Es la puerta abierta a la confusión de dependencias. Un solo origen, con orígenes ascendentes, y <clear /> para eliminar los heredados.
  • Guardar un token de acceso personal para publicar. Innecesario y peligroso: NuGetAuthenticate@1 (o su equivalente para npm y Maven) usa la identidad de la ejecución.
  • Publicar sin directiva de retención. El feed crece en silencio hasta que llega la factura. Configúrala el mismo día que creas el feed.
  • Editar el número de versión a mano. Se olvida, se duplica y bloquea la publicación. Derívalo de Build.BuildNumber.
  • Subir la versión mayor sin avisar. El versionado semántico es un contrato: un cambio incompatible publicado como parche rompe a quien consume sin previo aviso.
  • Ignorar las dependencias transitivas. Es donde vive la mayoría de las vulnerabilidades. --include-transitive, siempre.
  • Consejo: reserva un prefijo para tus paquetes (contoso.*) y documenta que ese espacio de nombres es interno. Facilita detectar suplantaciones.
  • Consejo: confirma el fichero de bloqueo en el repositorio y trátalo como código: si cambia en una solicitud de cambios, alguien debe mirar por qué.

Ejercicios

Ejercicio 1: diseñar el feed

Contoso Millas va a compartir dos cosas con el proyecto de reservas: una biblioteca .NET de cálculo de puntos y un conjunto de datos de 200 MB con las tablas de canje, que cambia mensualmente.

  1. ¿Un feed nuevo o el mismo contoso-paquetes? ¿Con qué ámbito?
  2. ¿Qué tipo de paquete corresponde a cada uno de los dos elementos y por qué?
  3. Estima el crecimiento anual de almacenamiento del conjunto de datos y propón una directiva de retención.

Ejercicio 2: confusión de dependencias

Un desarrollador de Contoso publica en un foro público una traza de error que incluye la línea Contoso.Reservas.Modelos, Version=2.3.1. Dos semanas después, el pipeline de la API empieza a descargar contoso.reservas.modelos 99.0.0, un paquete que nadie del equipo ha publicado.

  1. Explica el mecanismo del ataque paso a paso.
  2. Revisa este nuget.config e indica qué falla: <packageSources><add key="nuget.org" value="https://api.nuget.org/v3/index.json" /><add key="contoso" value="https://pkgs.dev.azure.com/.../contoso-paquetes@release/nuget/v3/index.json" /></packageSources>
  3. ¿Qué tres medidas aplicarías y cuál es la más importante?

Ejercicio 3: versionado y promoción

La biblioteca contoso.reservas.modelos está en la versión 2.3.1. Diego necesita: (a) corregir la validación del localizador sin cambiar ninguna firma pública; (b) añadir una propiedad opcional AsientoPreferente; (c) renombrar CodigoReserva a Localizador.

  1. Asigna número de versión a cada cambio y justifícalo.
  2. Describe el recorrido del cambio (c) por las vistas del feed hasta llegar a producción.
  3. ¿Qué debe ocurrir en contoso-reservas y contoso-api-disponibilidad para consumir (c), y cómo se evita romperlos a la vez?

Soluciones

Solución 1:

  1. El mismo contoso-paquetes, con ámbito de organización, que es justamente para lo que se eligió ese ámbito: permite que dos proyectos distintos consuman las mismas bibliotecas sin duplicar feeds ni configuración. Un feed por proyecto solo tendría sentido si los permisos debieran estar tajantemente separados.
  2. La biblioteca de cálculo de puntos, paquete NuGet: es código .NET que se compila dentro de las aplicaciones consumidoras y necesita resolución de dependencias. Las tablas de canje, Universal Package: son datos, no código, no encajan en ningún gestor de paquetes, no deben ir al repositorio Git por su tamaño y aun así necesitan versión y trazabilidad.
  3. 200 MB × 12 publicaciones al año = 2,4 GiB anuales, que por sí solos superan la cuota gratuita de 2 GiB de toda la organización. Directiva razonable: conservar las 3 versiones más recientes y eliminar el resto pasados 90 días, promocionando a @release la versión en uso en producción para protegerla del borrado automático. Así el consumo se estabiliza en torno a 600-800 MB.

Solución 2:

  1. El atacante obtiene el nombre interno del paquete desde la traza pública. Publica en nuget.org un paquete homónimo con versión 99.0.0. El pipeline de la API tiene configurados dos orígenes en paralelo, así que consulta ambos y elige la versión más alta, que es la del atacante. Descarga ese paquete y ejecuta su código durante la compilación, con acceso a las variables del agente y a la red. A partir de ahí el atacante puede exfiltrar tokens o alterar el artefacto que se desplegará.
  2. Falla en dos cosas: declara dos orígenes en paralelo —el público y el privado—, que es la condición necesaria del ataque; y no lleva <clear />, así que además arrastra los orígenes globales configurados en la máquina o en la imagen del agente. Lo correcto es <clear /> seguido de un único origen, el feed de Contoso, con nuget.org configurado como origen ascendente del propio feed.
  3. (a) Un solo origen con <clear /> y orígenes ascendentes —la más importante, porque elimina la ambigüedad de resolución que hace posible el ataque—. (b) Reservar y documentar el prefijo contoso.* como espacio de nombres interno. (c) Fichero de bloqueo confirmado y análisis de dependencias en el pipeline, que habría detectado el salto de versión anómalo. Además, rotar cualquier credencial a la que el agente tuviera acceso durante las compilaciones comprometidas.

Solución 3:

  1. (a) 2.3.2, parche: corrige comportamiento sin alterar la superficie pública. (b) 2.4.0, menor: añade funcionalidad de forma compatible; quien no use la propiedad nueva no se entera. (c) 3.0.0, mayor: renombrar un miembro público es un cambio incompatible y todo el código consumidor deja de compilar.
  2. El pipeline de contoso-modelos publica 3.0.0 en @local. Se promociona a @prerelease —posiblemente antes como 3.0.0-preview.1— y los entornos de desarrollo la consumen y validan. Cuando la web y la API han migrado y sus pruebas de integración pasan, se promociona a @release, que es la vista que consumen los pipelines de producción. La versión no se modifica en ningún momento: solo cambia dónde es visible.
  3. Cada repositorio debe actualizar su referencia a 3.0.0 y adaptar el código al nombre nuevo, cada uno en su propia solicitud de cambios con su revisión. Como el consumo se hace por versión explícita, no se rompen a la vez: mientras la API siga referenciando 2.4.0, sigue compilando y desplegando con normalidad. Ese es precisamente el valor del versionado frente al código copiado, donde el cambio habría llegado a todos de golpe. Para suavizarlo aún más, una versión intermedia 2.5.0 puede introducir Localizador marcando CodigoReserva como obsoleto, dando margen a migrar antes de que la 3.0.0 lo elimine.

Conclusión

La biblioteca de modelos de Contoso ha dejado de estar copiada en tres sitios. Entiendes qué aporta un gestor de paquetes frente a copiar y pegar —identidad, versión, actualización trazable y un contrato explícito sobre los cambios incompatibles—, conoces los tipos que aloja Azure Artifacts y sabes cuándo un Universal Package es la respuesta correcta: cuando lo que hay que versionar y compartir no es código y no cabe en el repositorio Git, como el catálogo de aeropuertos de Contoso. Has creado el feed contoso-paquetes con ámbito de organización para que lo pueda consumir también contoso-millas, y has usado las vistas @local, @prerelease y @release como mecanismo de promoción —el mismo patrón de puertas de 05-04 aplicado a los paquetes—, sabiendo que una versión publicada es inmutable y que promocionar solo cambia dónde es visible.

Has entendido a fondo los orígenes ascendentes: la URL única, la copia guardada que te protege si un paquete público desaparece y, sobre todo, la defensa contra la confusión de dependencias, ese ataque de cadena de suministro en el que un paquete público homónimo con versión altísima suplanta al interno y consigue ejecutar código en tu agente de compilación. La defensa se resume en una línea de configuración: <clear /> y un único origen. Has publicado contoso.reservas.modelos desde un pipeline que se autentica con NuGetAuthenticate@1 —sin ningún token guardado— y que deriva el número de versión de Build.BuildNumber, dejando a las personas solo la decisión de diseño de cuándo sube la parte mayor o menor. Y has cerrado con lo que sostiene todo lo demás: retención configurada desde el primer día, porque el feed crece en silencio sobre una cuota gratuita de 2 GiB, y las cinco prácticas de seguridad de la cadena de suministro —fijar versiones con ficheros de bloqueo, analizar vulnerabilidades incluyendo las transitivas, un solo origen, generar el SBOM que responde en minutos a «¿estamos afectados?» y firmar con el certificado de kv-contoso-pro—.

Con esto, el ciclo de entrega de Contoso está casi completo: el trabajo se planifica en Boards, el código se versiona y revisa en Repos, se compila y prueba en Pipelines, se despliega con aprobaciones y ranuras, y las piezas compartidas se distribuyen con versión desde Artifacts. Casi. Porque toda la plataforma sobre la que se despliega —las redes, las aplicaciones, las bases de datos, las políticas de gobierno del módulo 4— sigue existiendo únicamente porque alguien tecleó una vez los comandos correctos. No hay forma de recrearla, ni de saber quién cambió qué, ni de levantarla en North Europe tras un desastre. En la última lección del módulo, Infraestructura como código con Bicep, eso se acaba: la plataforma entera pasará a ser código revisable, versionado y repetible, desplegado por su propio pipeline con vista previa, aprobación y todo lo que has aprendido aquí.

Curso de Azure

Módulo 1: Introducción a Azure

Módulo 2: Servicios principales de Azure

Módulo 3: Bases de datos de Azure

Módulo 4: Seguridad en Azure

Módulo 5: Azure DevOps

Módulo 6: Servicios avanzados de Azure

Módulo 7: Monitoreo y gestión

Módulo 8: Gestión y optimización de costos

Módulo 9: Estudios de caso y mejores prácticas

© Copyright 2026. Todos los derechos reservados