<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Daniel J. Saldaña</title>
    <description>The latest articles on Forem by Daniel J. Saldaña (@danieljsaldana).</description>
    <link>https://forem.com/danieljsaldana</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F872345%2F99cfb176-71ad-463d-92ec-81356bed8c27.png</url>
      <title>Forem: Daniel J. Saldaña</title>
      <link>https://forem.com/danieljsaldana</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/danieljsaldana"/>
    <language>en</language>
    <item>
      <title>Cómo crear un podcast generado con IA usando Azure</title>
      <dc:creator>Daniel J. Saldaña</dc:creator>
      <pubDate>Mon, 23 Mar 2026 23:24:20 +0000</pubDate>
      <link>https://forem.com/danieljsaldana/como-crear-un-podcast-generado-con-ia-usando-azure-430b</link>
      <guid>https://forem.com/danieljsaldana/como-crear-un-podcast-generado-con-ia-usando-azure-430b</guid>
      <description>&lt;p&gt;Llevo tiempo viendo cómo los podcasts no paran de crecer en popularidad. Cada vez somos más los que preferimos consumir contenido en audio en lugar de leer largos artículos o ver vídeos que requieren el 100% de nuestra atención. Ya sea mientras programamos un nuevo componente, vamos de camino al trabajo en el coche, o simplemente cuando salimos a hacer deporte; el podcasting se ha convertido en el compañero perfecto para nuestro día a día.&lt;/p&gt;

&lt;p&gt;Y no es para menos, plataformas como Spotify, Apple Podcasts o iVoox nos lo han puesto facilísimo para distribuir o descubrir lo que otros tienen que contar.&lt;/p&gt;

&lt;p&gt;Pero claro, la realidad del creador de contenido es muy distinta. Si alguna vez te ha picado el gusanillo y has intentado producir uno, sabrás de sobra que lleva su tiempo. No se trata solo de sentarse frente a un micrófono y hablar. Tienes que pensar la idea, estructurar y escribir un guion medianamente coherente, grabar (y regrabar porque te has trabado en la misma frase cuarenta veces), y finalmente pelearte con el editor de audio para cortar los silencios incómodos y darle fluidez.&lt;/p&gt;

&lt;p&gt;Por suerte, la Inteligencia Artificial está aquí para echarnos un cable tremendo.&lt;/p&gt;

&lt;p&gt;Seguro que a estas alturas ya has trasteado con cosas como ChatGPT para pedirle que te genere textos, te resuma documentos o incluso te ayude a debuggear algo de código. Pero la cosa va mucho más allá. ¿Sabías que la síntesis de voz ha evolucionado hasta el punto de sonar casi 100% humana? Atrás quedaron los tiempos de Loquendo y esas voces robóticas sin alma.&lt;/p&gt;

&lt;p&gt;En este artículo te quiero enseñar paso a paso cómo he logrado montar un podcast entero de forma semiautomática usando Inteligencia Artificial. Y para conseguir ese nivel de calidad en el audio que marque la diferencia, me he apoyado en los servicios de Azure. Vamos a repasar juntos todo el flujo de trabajo: desde que le pedimos a la IA que nos escupa el texto, hasta que picamos un script para convertirlo a un archivo de audio con un sonido espectacular y un acento de España que da el pego por completo.&lt;/p&gt;




&lt;h2&gt;
  
  
  ¿Por qué elegir Azure para esta locura?
&lt;/h2&gt;

&lt;p&gt;Es probable que te preguntes por qué me he decantado por la nube de Microsoft habiendo otras opciones como Google Cloud, AWS o plataformas específicas como ElevenLabs. He probado varios sistemas y APIs de Text-to-Speech durante estos últimos meses, pero al final siempre acabo volviendo a Azure por varios motivos de peso que, para mí, marcan la diferencia.&lt;/p&gt;

&lt;p&gt;El primero y más evidente: Azure Speech ofrece unas voces neuronales que son una auténtica pasada. Cuentan con un abanico de tonalidades muy variadas y, lo más importante, su &lt;strong&gt;acento en español de España&lt;/strong&gt; (al igual que el de otros muchos países hispanohablantes) prácticamente no suena a robot. Puedes jugar con voces como la de "Álvaro" o "Elvira" y notar la cadencia y la entonación que esperarías de un humano normal y corriente leyendo un texto.&lt;/p&gt;

&lt;p&gt;Además, hay otras ventajas clave en el entorno de desarrollo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;La API es híper robusta:&lt;/strong&gt; Se nota que es un servicio maduro. La documentación es completísima y Microsoft mantiene los SDKs súper actualizados.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integración sin fricciones:&lt;/strong&gt; Instalar su paquete de NPM en cualquier script de Node.js o Python y tenerlo funcionando te lleva literalmente un par de minutos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSML (Speech Synthesis Markup Language):&lt;/strong&gt; Si la voz de base no te convence del todo, puedes usar etiquetas estándar XML para meter pausas de un segundo, subir el tono de voz al final de una pregunta o cambiar la velocidad de ciertas frases clave de forma súper granular. Esto da mucho juego si quieres automatizar hasta la respiración del locutor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La capa gratuita (Free Tier):&lt;/strong&gt; Azure te da una tarifa F0 en el servicio de Speech que te ofrece miles de caracteres gratuitos al mes, lo que da de sobra para hacer pruebas, proyectos personales y validar ideas sin tener que pasar la tarjeta por caja a las primeras de cambio.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lo que vas a necesitar antes de empezar
&lt;/h2&gt;

&lt;p&gt;Antes de ponernos a ensuciarnos las manos picando código, asegúrate de tener todo esto preparado en tu entorno de trabajo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Una cuenta de Azure activa. Si no la tienes, &lt;a href="https://azure.microsoft.com/es-es/free/" rel="noopener noreferrer"&gt;te puedes registrar totalmente gratis&lt;/a&gt; desde su portal y te dan crédito inicial.&lt;/li&gt;
&lt;li&gt;Preferiblemente Node.js instalado en su versión 16 LTS o superior.&lt;/li&gt;
&lt;li&gt;Tu editor de código favorito listo para la acción (yo soy "team VS Code" a muerte).&lt;/li&gt;
&lt;li&gt;Una cuenta de OpenAI (o Azure OpenAI) con su API Key lista por si quieres seguir el flujo completo y automatizar también la parte creativa de la generación de guiones.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Paso 1: Configurar nuestro servicio de Azure Speech
&lt;/h2&gt;

&lt;p&gt;Lo primero de todo es irnos al &lt;a href="https://portal.azure.com/" rel="noopener noreferrer"&gt;portal de Azure&lt;/a&gt; para levantar el servicio que hará el trabajo duro de poner voz a nuestras palabras.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Una vez dentro de tu dashboard de Azure, haz clic en &lt;strong&gt;"Crear un recurso"&lt;/strong&gt; (el clásico botón del menú superior) y usa la barra de búsqueda para encontrar &lt;strong&gt;"Speech"&lt;/strong&gt; (o Voz, dependiendo del idioma de tu portal).&lt;/li&gt;
&lt;li&gt;Dale al botón de crear. Se te abrirá un formulario para rellenar los datos. Ponle un nombre descriptivo a la instancia, algo como &lt;code&gt;podcast-ia-demo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Selecciona la suscripción que vayas a usar, escoge un grupo de recursos existente (o crea uno nuevo para tenerlo todo organizadito) y como tarifa asegúrate de escoger la &lt;strong&gt;F0 (gratuita)&lt;/strong&gt; para empezar. Esta tarifa es tu mejor amiga para hacer experimentos.&lt;/li&gt;
&lt;li&gt;Asígnalo a tu región más cercana. En mi caso, estando en España, casi siempre suelo elegir &lt;code&gt;West Europe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Una vez que Azure termine de desplegar el recurso (suele tardar muy poco), vete al panel del servicio y busca la sección &lt;strong&gt;"Claves y punto de conexión"&lt;/strong&gt; en el menú lateral de la izquierda.&lt;/li&gt;
&lt;li&gt;Aquí verás dos claves y el nombre de la región. Te va a hacer falta copiar la "Clave 1" y la región exacta. Guárdalas a buen recaudo que luego las vamos a inyectar en nuestras variables de entorno. ¡Y recuerda no subirlas a ningún repositorio público!&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Paso 2: Preparando nuestra estructura local
&lt;/h2&gt;

&lt;p&gt;Con el backend en la nube preparado, vamos a montar la estructura de nuestro proyecto en el PC desde la terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;podcast-ia-azure
&lt;span class="nb"&gt;cd &lt;/span&gt;podcast-ia-azure
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora nos toca instalar las tres librerías de NPM que nos van a dar la vida y hacer que todo esto conecte con apenas un par de bloques de código: el SDK oficial de Azure, la librería de OpenAI, y &lt;code&gt;dotenv&lt;/code&gt; para no tener que harcodear nuestras credenciales.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;microsoft-cognitiveservices-speech-sdk openai dotenv

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para organizarme un poco mejor y no acabar con un mar de archivos sueltos, yo me creo un par de subcarpetas para ir almacenando de forma limpia los guiones generados, los audios resultantes y los propios scripts de ejecución. Tu proyecto debería quedar parecido a esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;podcast-ia-azure/
├── guiones/
├── audios/
├── scripts/
│ ├── generarGuion.js
│ └── generarPodcast.js
├── .env
├── package.json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Paso 3: Poniendo a GPT a currar como nuestro guionista
&lt;/h2&gt;

&lt;p&gt;Llegados a este punto podrías ser tú quien escriba el texto si ya tienes inspiración o quieres narrar un post tuyo. Pero ya que estamos automatizando cosas, ¿por qué no le pedimos a un modelo de lenguaje que nos haga él el borrador del guion?&lt;/p&gt;

&lt;p&gt;Crea un archivo llamado &lt;code&gt;.env&lt;/code&gt; en la raíz de tu proyecto e introduce ahí tu key de OpenAI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sk-tu_clave_super_secreta_de_openai&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Después, crea tu primer script al que vamos a llamar &lt;code&gt;scripts/generarGuion.js&lt;/code&gt;. Le pasaremos un tema por consola y nos tiene que devolver un archivo con el speech completo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OpenAIApi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;configuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAIApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generarGuion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;archivoSalida&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Ajustamos el prompt para que suene a locutor amigable y casual&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Escribe un guion para un podcast de unos 5 minutos. El tema del que tienes que hablar de manera amena es: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tema&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Exprésate con un tono muy cercano, entretenido y natural. Puedes saludar a la audiencia como si estuvieran contigo.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Pensando el guion para el tema: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tema&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"... Dame un momento.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createChatCompletion&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// La temperatura a 0.7 suele ir genial para equilibrar creatividad y coherencia narrativa&lt;/span&gt;
    &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Eres el locutor principal de un podcast de éxito sobre desarrollo y curiosidades tech. Hablas como si estuvieras charlando tranquilamente con un buen amigo.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Guardamos el texto en la ruta que le hayamos indicado&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;archivoSalida&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guion&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`¡Guion escrito y pulido! Lo he guardado sano y salvo en &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;archivoSalida&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Capturamos los argumentos de la terminal&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;archivoSalida&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;generarGuion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tema&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Por qué migrar a la nube no es magia negra&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;archivoSalida&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;guiones/episodio1.txt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¡Es la hora de probarlo! Vamos a ejecutar el script pasándole el parámetro de lo que queremos hablar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node scripts/generarGuion.js &lt;span class="s2"&gt;"La revolución de los desarrollos Serverless"&lt;/span&gt; guiones/episodio_serverless.txt

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En cuestión de segundos tendrás tu guion esperándote en la carpeta de &lt;code&gt;guiones&lt;/code&gt;. Es muy recomendable que le eches un primer vistazo, ya que el modelo suele organizar muy bien el saludo, el desarrollo del contenido y suele incluir hasta una buena llamada a la acción al final ("no olvides suscribirte si te ha gustado...").&lt;/p&gt;




&lt;h2&gt;
  
  
  Paso 4: La magia ocurre (Convertir texto a voz)
&lt;/h2&gt;

&lt;p&gt;Es aquí donde el código cobra vida, literalmente. Volvemos un segundo a nuestro &lt;code&gt;.env&lt;/code&gt; y añadimos los valores que nos apuntamos del portal de Azure hace un rato:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;AZURE_SPEECH_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;la_clave_que_sacaste_del_portal&lt;/span&gt;
&lt;span class="py"&gt;AZURE_SPEECH_REGION&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;tu_region_ejemplo_westeurope&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A continuación toca armar nuestro plato principal: &lt;code&gt;scripts/generarPodcast.js&lt;/code&gt;. En este archivo le vamos a decir al SDK de Voz de Azure: &lt;em&gt;"Coge este fichero de texto, aplícale tus modelos neuronales, y devuélveme un clip de audio prístino"&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Importamos el SDK oficial&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;microsoft-cognitiveservices-speech-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;speechKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AZURE_SPEECH_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;serviceRegion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AZURE_SPEECH_REGION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Recuperamos el archivo origen y el destino de los argumentos, o pillamos los default&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;archivoGuion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guiones/episodio1.txt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;archivoAudio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;audios/episodio1.wav&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;texto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;archivoGuion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Inicializamos la config con las keys&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;speechConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SpeechConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;speechKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;serviceRegion&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Asignamos una voz específica. 'es-ES-AlvaroNeural' funciona increíble para un tono neutro español.&lt;/span&gt;
&lt;span class="c1"&gt;// Si prefieres intentar con voz femenina, la variante 'es-ES-ElviraNeural' también tiene matices tremendos.&lt;/span&gt;
&lt;span class="nx"&gt;speechConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speechSynthesisVoiceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es-ES-AlvaroNeural&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 

&lt;span class="c1"&gt;// Le decimos al SDK que en vez de hablar por los altavoces de tu portátil, renderice directo a un archivo&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AudioConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAudioFileOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;archivoAudio&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;synthesizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SpeechSynthesizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;speechConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;audioConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Renderizando la voz del archivo &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;archivoGuion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Esto suele tardar unos segundos...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Lanzamos la acción asíncrona principal&lt;/span&gt;
&lt;span class="nx"&gt;synthesizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;speakTextAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;texto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResultReason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SynthesizingAudioCompleted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`¡Boom! Renderización completada al 100%. Tienes el audio esperándote en &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;archivoAudio&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Si algo falla (ej. clave mal puesta), nos escupe el error por consola&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ha ocurrido un error al intentar sintetizar la voz:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorDetails&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Importante: cerrar el sintetizador para liberar recursos&lt;/span&gt;
  &lt;span class="nx"&gt;synthesizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Probemos a ejecutar esta maravilla por primera vez:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node scripts/generarPodcast.js guiones/episodio_serverless.txt audios/episodio_serverless.wav

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;De verdad, ponte unos buenos auriculares, navega hasta tu carpeta de &lt;code&gt;audios&lt;/code&gt;, y dale al play al WAV resultante. Si es tu primera vez usando voces neuronales de Azure, te va a sorprender una barbaridad lo humana que llega a sonar la entonación y las pausas que hace al leer las comas y los puntos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Un paso más allá: Expandiendo tu automatización
&lt;/h2&gt;

&lt;p&gt;A estas alturas ya tenemos lo fundamental: un archivo con una voz narrando el contenido. Sin embargo, si lo escuchas tal cual, posiblemente notes el audio un pelín "clínico" o vacío, ya que el locutor está hablando en el absoluto de los vacíos sonoros.&lt;/p&gt;

&lt;p&gt;Para darle el toque maestro, hay un par de cositas de post-producción que te recomiendo aplicar siempre.&lt;/p&gt;

&lt;p&gt;Si buscas el control absoluto y no te importa invertir un par de minutos más, te puedes abrir el archivo directamente en un programa libre de edición como Audacity. Te echas el WAV generado a la pista principal, añades una pista secundaria pregrabada con alguna melodía de Lo-Fi o jazz relajante (ojo con los derechos de autor, &lt;a href="https://freemusicarchive.org/" rel="noopener noreferrer"&gt;Free Music Archive&lt;/a&gt; es genial para esto) y un par de "woosh" de efectos de sonido para la intro y el outro. Subirle unos decibelios a la voz y bajar la música te dará un nivel súper profesional al instante.&lt;/p&gt;

&lt;p&gt;Pero si, al igual que a nosotros, te va la marcha pura y dura y prefieres ser fiel a la filosofía de &lt;strong&gt;automatizarlo absolutamente todo&lt;/strong&gt; , puedes usar herramientas como &lt;code&gt;ffmpeg&lt;/code&gt; desde la propia terminal para mezclar automáticamente tu podcast con una base musical. La idea es que baje el volumen de la música de fondo para que tu voz se escuche clara (el famoso efecto "ducking"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; audios/episodio1.wav &lt;span class="nt"&gt;-i&lt;/span&gt; musica_fondo.mp3 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:a][1:a]amix=inputs=2:duration=first:dropout_transition=2"&lt;/span&gt; audios/episodio1_final.mp3

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¿Te sabe a poco? Vamos a subir el nivel integrando la API de YouTube. Imagina que fabricamos un vídeo MP4 con una carátula estática y nuestro audio final. Puedes usar este comando rapidísimo de ffmpeg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-loop&lt;/span&gt; 1 &lt;span class="nt"&gt;-i&lt;/span&gt; portada.png &lt;span class="nt"&gt;-i&lt;/span&gt; audios/episodio1_final.mp3 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-tune&lt;/span&gt; stillimage &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 192k &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-shortest&lt;/span&gt; videos/episodio1.mp4

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una vez que tienes el archivo MP4 listo en tu carpeta &lt;code&gt;videos/&lt;/code&gt;, el siguiente paso lógico de automatización es subirlo directamente a tu canal usando OAuth 2.0 y el cliente oficial de Google APIs. Te dejo aquí un snippet bastante avanzado donde he creado un objeto &lt;code&gt;uploadToYouTube&lt;/code&gt; para enviar el archivo directamente a los servidores de YouTube sin tener que abrir el navegador:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Lógica que envuelve la llamada a googleapis e instanciación del cliente de YouTube&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;uploadToYouTube&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/youtube-upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Le pasamos el buffer de nuestro vídeo recién renderizado&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videoBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;videos/episodio1.mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Rellenamos los metadatos. ¡Esto también te lo podría generar GPT!&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Episodio 1: La historia de la IA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Podcast generado con IA y Azure. En este episodio hablamos sobre la historia de la inteligencia artificial.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;podcast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inteligencia artificial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;azure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;featureImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;portada.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scriptData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt; 
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Subiendo vídeo a YouTube...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Lanzamos nuestra mega-integración&lt;/span&gt;
&lt;span class="nf"&gt;uploadToYouTube&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scriptData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;¡Vídeo subido y listado con éxito!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;El script ha petado subiendo a YouTube:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si metes esto último en un entorno de integración continua (como GitHub Actions) tras hacer un &lt;em&gt;push&lt;/em&gt; de tus guiones en texto plano, tienes un bot autónomo que renderiza voces neuronales, hace postproducción de vídeo final al vuelo, y auto-publica la obra maestra en YouTube sin que te hayas despeinado.&lt;/p&gt;

&lt;p&gt;Espero que de verdad este recorrido te haya aportado ideas frescas de todo lo que puedes lograr combinando tan solo un entorno básico de Node, junto a un par de servicios cloud impresionantes como los que tenemos a nuestra disposición hoy en día. ¿Te imaginas configurando una alarma matutina que te lea el resumen de los pull requests que te han asignado en voz de locutor de radio? Porque con estas herramientas, la barrera principal acaba siendo puramente la imaginación.&lt;/p&gt;

&lt;p&gt;Si acabas de probar el código, encuentras alguna cosa curiosa en Azure Speech o te apetece enseñarme los resultados, ya sabes dónde estoy en los comentarios o puedes buscar mis &lt;a href="https://danieljsaldana.com/" rel="noopener noreferrer"&gt;redes sociales&lt;/a&gt;. ¡Dale duro!&lt;/p&gt;

</description>
      <category>desarrollo</category>
    </item>
    <item>
      <title>Cómo diseñar una pasarela de pagos resiliente con Stripe y Azure</title>
      <dc:creator>Daniel J. Saldaña</dc:creator>
      <pubDate>Thu, 19 Feb 2026 08:15:00 +0000</pubDate>
      <link>https://forem.com/danieljsaldana/como-disenar-una-pasarela-de-pagos-resiliente-con-stripe-y-azure-458c</link>
      <guid>https://forem.com/danieljsaldana/como-disenar-una-pasarela-de-pagos-resiliente-con-stripe-y-azure-458c</guid>
      <description>&lt;p&gt;Integrar pagos parece sencillo hasta que deja de serlo. Mientras todo funciona, un webhook que guarda datos en una base de datos puede parecer suficiente. El problema aparece cuando hay latencia, duplicados, fallos temporales o errores de lógica. En ese momento descubres que procesar dinero no es solo recibir un evento HTTP.&lt;/p&gt;

&lt;p&gt;En este artículo quiero explicarte cómo plantear una arquitectura de pagos sólida usando Stripe sobre Microsoft Azure, desplegada como infraestructura como código con Terraform. No vamos a centrarnos en fragmentos de código, sino en las decisiones arquitectónicas que marcan la diferencia entre algo que “funciona” y algo que está preparado para producción real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separar responsabilidades desde el primer momento
&lt;/h2&gt;

&lt;p&gt;Cuando Stripe envía un webhook, exige una respuesta rápida. Si no contestas en pocos segundos, reintentará el envío. Eso es parte de su modelo de garantía de entrega.&lt;/p&gt;

&lt;p&gt;El error habitual es procesarlo todo dentro del mismo endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validar firma.&lt;/li&gt;
&lt;li&gt;Consultar base de datos.&lt;/li&gt;
&lt;li&gt;Crear o actualizar usuario.&lt;/li&gt;
&lt;li&gt;Registrar pago.&lt;/li&gt;
&lt;li&gt;Enviar correos.&lt;/li&gt;
&lt;li&gt;Devolver respuesta.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si algo falla en ese flujo —una consulta lenta, un timeout, un problema de red— puedes terminar con estados inconsistentes o duplicados.&lt;/p&gt;

&lt;p&gt;La solución es simple conceptualmente, pero potente arquitectónicamente: &lt;strong&gt;desacoplar&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;La función de entrada debe hacer únicamente esto:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validar la firma criptográfica.&lt;/li&gt;
&lt;li&gt;Publicar el evento en un sistema de eventos.&lt;/li&gt;
&lt;li&gt;Devolver 200 inmediatamente.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nada más.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event Grid como columna vertebral
&lt;/h2&gt;

&lt;p&gt;En esta arquitectura, Azure Event Grid actúa como intermediario entre el webhook y la lógica de negocio.&lt;/p&gt;

&lt;p&gt;Esto cambia completamente el comportamiento del sistema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Si una función falla, Event Grid reintenta automáticamente.&lt;/li&gt;
&lt;li&gt;Si el fallo persiste, el evento puede enviarse a Dead Letter.&lt;/li&gt;
&lt;li&gt;Cada tipo de evento puede procesarse en una función distinta.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El sistema deja de depender de un único punto crítico.&lt;/p&gt;

&lt;p&gt;Separar eventos como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;checkout.session.completed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;invoice.payment_succeeded&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;customer.subscription.deleted&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;en funciones independientes permite escalar y evolucionar cada dominio sin romper los demás.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cosmos DB para el estado
&lt;/h3&gt;

&lt;p&gt;Cosmos DB almacena el estado actual:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Usuarios.&lt;/li&gt;
&lt;li&gt;Suscripciones.&lt;/li&gt;
&lt;li&gt;Pagos procesados.&lt;/li&gt;
&lt;li&gt;Identificadores de eventos para idempotencia.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El modo Serverless permite pagar solo por consumo real y escalar automáticamente según tráfico.&lt;/p&gt;

&lt;p&gt;Además, su modelo NoSQL se adapta bien a la naturaleza variable de los eventos de Stripe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage Account para auditoría y resiliencia
&lt;/h3&gt;

&lt;p&gt;Una única Storage Account cumple tres funciones importantes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Table Storage&lt;/strong&gt; para auditoría ligera.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blob Storage&lt;/strong&gt; como Dead Letter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue Storage&lt;/strong&gt; para errores lógicos.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si un evento no puede procesarse tras múltiples reintentos, no desaparece. Se almacena en Blob Storage para reprocesarlo cuando el problema esté resuelto.&lt;/p&gt;

&lt;p&gt;Si el error es de negocio (por ejemplo, datos inconsistentes), se envía a una cola específica para revisión manual.&lt;/p&gt;

&lt;p&gt;Este diseño permite diferenciar entre fallos técnicos y fallos funcionales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotencia: imprescindible en sistemas de pagos
&lt;/h2&gt;

&lt;p&gt;Stripe puede enviar el mismo evento más de una vez. Esto no es un error, es una característica de fiabilidad.&lt;/p&gt;

&lt;p&gt;Por eso, antes de procesar cualquier evento, es obligatorio comprobar si ya fue tratado.&lt;/p&gt;

&lt;p&gt;Normalmente se guarda el &lt;code&gt;eventId&lt;/code&gt; en Cosmos DB o en una tabla de control. Si ya existe, se ignora. Si no existe, se procesa y se registra.&lt;/p&gt;

&lt;p&gt;Sin este control puedes acabar con:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pagos duplicados.&lt;/li&gt;
&lt;li&gt;Suscripciones dobles.&lt;/li&gt;
&lt;li&gt;Estados incoherentes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La idempotencia no es opcional cuando trabajas con dinero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seguridad basada en identidad
&lt;/h2&gt;

&lt;p&gt;La seguridad debe basarse en identidad, no en secretos distribuidos por el código.&lt;/p&gt;

&lt;p&gt;Cada Azure Function puede tener una Managed Identity asignada. Los permisos se gestionan mediante RBAC:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;La función de entrada puede publicar en Event Grid.&lt;/li&gt;
&lt;li&gt;Las funciones de procesamiento pueden acceder a Cosmos DB y Storage.&lt;/li&gt;
&lt;li&gt;No se usan connection strings en el código.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Además, la validación de la firma de Stripe en el webhook es obligatoria. Si la firma no coincide, se devuelve 401 inmediatamente.&lt;/p&gt;

&lt;p&gt;Este enfoque reduce superficie de ataque y simplifica la gestión de credenciales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué ocurre cuando algo falla
&lt;/h2&gt;

&lt;p&gt;Una arquitectura madura define qué pasa cuando el sistema no funciona perfectamente.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Si Cosmos DB tiene una caída temporal, Event Grid reintentará automáticamente durante horas.&lt;/li&gt;
&lt;li&gt;Si hay un bug persistente, el evento terminará en Dead Letter y podrá reprocesarse.&lt;/li&gt;
&lt;li&gt;Si hay un error de lógica, se enviará a una cola para revisión manual.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nada se pierde. Todo queda registrado.&lt;/p&gt;

&lt;p&gt;Ese es el objetivo real de la arquitectura: &lt;strong&gt;garantizar consistencia incluso cuando el entorno falla&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infraestructura como código
&lt;/h2&gt;

&lt;p&gt;Desplegar todo esto con Terraform aporta ventajas claras:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Entornos reproducibles.&lt;/li&gt;
&lt;li&gt;Versionado de infraestructura.&lt;/li&gt;
&lt;li&gt;Integración con pipelines de despliegue.&lt;/li&gt;
&lt;li&gt;Control de cambios auditable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La infraestructura deja de ser algo manual y pasa a ser parte del ciclo de desarrollo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflexión final
&lt;/h2&gt;

&lt;p&gt;Procesar pagos no es añadir un endpoint HTTP. Es diseñar un sistema distribuido que debe ser consistente, seguro y tolerante a fallos.&lt;/p&gt;

&lt;p&gt;Separar entrada y procesamiento, utilizar un backbone de eventos, aplicar idempotencia, implementar seguridad basada en identidad y definir mecanismos claros de recuperación transforma una integración básica en una arquitectura preparada para producción.&lt;/p&gt;

&lt;p&gt;La simplicidad inicial puede ser atractiva. Pero cuando el tráfico crece o aparece el primer incidente serio, la arquitectura es lo que marca la diferencia.&lt;/p&gt;

</description>
      <category>devops</category>
    </item>
    <item>
      <title>Arquitectura Evolutiva en Azure: El Arte de no Sobre-diseñar</title>
      <dc:creator>Daniel J. Saldaña</dc:creator>
      <pubDate>Sun, 01 Feb 2026 10:15:00 +0000</pubDate>
      <link>https://forem.com/danieljsaldana/arquitectura-evolutiva-en-azure-el-arte-de-no-sobre-disenar-3fg0</link>
      <guid>https://forem.com/danieljsaldana/arquitectura-evolutiva-en-azure-el-arte-de-no-sobre-disenar-3fg0</guid>
      <description>&lt;p&gt;Si llevas tiempo en el mundo de la arquitectura Cloud, conoces esa sensación. Estás frente al lienzo en blanco (o un &lt;code&gt;main.tf&lt;/code&gt; vacío) y te encuentras en ese punto crítico: la delgada línea entre sentar unas bases sólidas y caer en la trampa del sobre-diseño (&lt;em&gt;over-engineering&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;Recientemente, trabajando en la arquitectura base para una Prueba de Concepto (PoC) en Azure, me vi tentado a desplegar todo el arsenal que Microsoft nos ofrece. Sin embargo, recordé una máxima que a veces olvidamos: &lt;strong&gt;La arquitectura no es solo tecnología; es contexto y momento.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hoy quiero compartir cómo he abordado este diseño, diferenciando entre lo que es "innegociable" desde el día 1 y lo que es "evolutivo".&lt;/p&gt;

&lt;h2&gt;
  
  
  El Enfoque: Sólido pero Ligero
&lt;/h2&gt;

&lt;p&gt;En los diagramas que acompañan este post, podéis ver el enfoque actual que he implementado. No es la arquitectura final de una multinacional, pero tampoco es un "Hello World" inseguro.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Gestión del Estado y Ciclo de Vida (Terraform)
&lt;/h3&gt;

&lt;p&gt;Como se ve en el diagrama de secuencia, he optado por una estrategia de Infraestructura como Código (IaC) en capas. No todo se despliega a la vez.&lt;/p&gt;

&lt;p&gt; &lt;a href="/images/posts/diagrama-secuencia-fases-despliegue-terraform-azure.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="/images/posts/diagrama-secuencia-fases-despliegue-terraform-azure.jpeg" alt="Diagrama de Secuencia y Fases de Despliegue"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fase de Fundación:&lt;/strong&gt; Storage Accounts, Networking base.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fase de Orquestación:&lt;/strong&gt; Identidades, Recursos compartidos (ACR, KeyVault).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fase de Aplicación:&lt;/strong&gt; El despliegue "quirúrgico" de los servicios.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esto evita el temido &lt;em&gt;monolito de Terraform&lt;/em&gt; y reduce el radio de explosión si algo falla. Separar el ciclo de vida de la infraestructura del ciclo de vida de la aplicación es vital para mantener la agilidad.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Seguridad e Identidad: El Innegociable
&lt;/h3&gt;

&lt;p&gt;Aquí es donde no se puede negociar. En el diagrama de jerarquía de identidades, veréis que he implementado &lt;strong&gt;Federación OIDC&lt;/strong&gt; (OpenID Connect) entre GitHub Actions y Azure.&lt;/p&gt;

&lt;p&gt; &lt;a href="/images/posts/jerarquia-identidades-federadas-oidc-y-acceso-estado.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="/images/posts/jerarquia-identidades-federadas-oidc-y-acceso-estado.jpeg" alt="Jerarquía de Identidades y Gestión de Estado"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;¿El objetivo?&lt;/strong&gt; Eliminar los secretos rotativos (Client Secrets) que caducan o se filtran.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Trust:&lt;/strong&gt; Reforzamos la seguridad asignando identidades granulares. Cada servicio tiene permiso solo para lo que necesita (ej. &lt;code&gt;AcrPush&lt;/code&gt;, &lt;code&gt;Secret User&lt;/code&gt;), ni más, ni menos.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Lo que NO ves (y por qué es importante)
&lt;/h3&gt;

&lt;p&gt;Si analizas el diagrama de arquitectura completo, notarás ausencias notables.&lt;/p&gt;

&lt;p&gt; &lt;a href="/images/posts/arquitectura-general-poc-azure-container-apps.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="/images/posts/arquitectura-general-poc-azure-container-apps.jpeg" alt="Arquitectura General de Infraestructura Azure"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Dónde está &lt;strong&gt;Azure Front Door&lt;/strong&gt;?&lt;/li&gt;
&lt;li&gt;¿Por qué no hay &lt;strong&gt;Private Endpoints&lt;/strong&gt; en cada servicio PaaS?&lt;/li&gt;
&lt;li&gt;¿Dónde está la suite completa de &lt;strong&gt;Defender for Cloud&lt;/strong&gt;?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Técnicamente, podría implementarlo todo hoy. Pero aplicar todos los patrones &lt;em&gt;Enterprise&lt;/em&gt; en una fase de PoC o MVP es el error más común. Frena el desarrollo, dispara la complejidad cognitiva del equipo y aumenta los costes innecesariamente.&lt;/p&gt;

&lt;p&gt;Sin embargo, el segundo error más grave es usar la excusa de la "agilidad" para crear deuda técnica permanente (ej. usar &lt;em&gt;Connection Strings&lt;/em&gt; en texto plano o un solo Service Principal para todo).&lt;/p&gt;

&lt;h2&gt;
  
  
  La Lección: Innegociables vs. Evolutivos
&lt;/h2&gt;

&lt;p&gt;La clave del éxito en la arquitectura Cloud moderna está en saber clasificar tus decisiones:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Los Innegociables (Día 1):&lt;/strong&gt; Gestión de identidad robusta (OIDC), segregación de responsabilidades, IaC modular y control de secretos (Key Vault). Si esto falla, los cimientos se rompen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los Evolutivos (Día 2+):&lt;/strong&gt; Capas de seguridad perimetral avanzadas (WAF, Front Door), redundancia geográfica o networking privado complejo. Estos llegarán cuando el tráfico y el riesgo de negocio lo justifiquen.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Las buenas arquitecturas no nacen perfectas; &lt;strong&gt;nacen preparadas para evolucionar.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Diseñar para el futuro no significa construirlo todo hoy, sino dejar los huecos necesarios para que, cuando llegue el momento de escalar, no tengas que demoler lo que ya has construido.&lt;/p&gt;

&lt;p&gt;¿Y vosotros? ¿Dónde ponéis la línea roja entre una buena base y el sobre-diseño en vuestras PoCs? Os leo en los comentarios.&lt;/p&gt;

</description>
      <category>devops</category>
    </item>
    <item>
      <title>¡Respaldo seguro y eficiente: Usando Azure Blob Storage para proteger tus datos!</title>
      <dc:creator>Daniel J. Saldaña</dc:creator>
      <pubDate>Fri, 02 Jan 2026 21:41:39 +0000</pubDate>
      <link>https://forem.com/danieljsaldana/respaldo-seguro-y-eficiente-usando-azure-blob-storage-para-proteger-tus-datos-1ip5</link>
      <guid>https://forem.com/danieljsaldana/respaldo-seguro-y-eficiente-usando-azure-blob-storage-para-proteger-tus-datos-1ip5</guid>
      <description>&lt;p&gt;Hoy quiero abordar uno de los temas más críticos para la seguridad de cualquier sitio o proyecto: &lt;strong&gt;los respaldos&lt;/strong&gt;. En este post, exploraremos cómo garantizar la protección y accesibilidad de tus datos, utilizando &lt;strong&gt;Azure Blob Storage&lt;/strong&gt; como la solución ideal para crear respaldos seguros y automatizados.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Por qué es tan importante un sistema de respaldo?
&lt;/h2&gt;

&lt;p&gt;Los datos son el activo más valioso en cualquier proyecto digital. Sin embargo, a menudo subestimamos la importancia de tener un respaldo confiable. Un fallo del sistema, un ataque malicioso o incluso un error humano pueden provocar la pérdida de información crítica. Es por eso que, tener una estrategia de respaldos sólida y eficiente es fundamental.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Por qué elegir Azure Blob Storage?
&lt;/h3&gt;

&lt;p&gt;Cuando hablamos de soluciones en la nube, &lt;strong&gt;Azure&lt;/strong&gt; se destaca como uno de los proveedores más robustos y confiables. &lt;strong&gt;Azure Blob Storage&lt;/strong&gt; es una opción poderosa para almacenar grandes volúmenes de datos y realizar respaldos de forma segura, con múltiples capas de protección.&lt;/p&gt;

&lt;p&gt;Con Azure, puedes aprovechar sus características como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Escalabilidad&lt;/strong&gt; : Puedes almacenar desde unos pocos gigabytes hasta petabytes de datos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seguridad&lt;/strong&gt; : Azure proporciona múltiples opciones de autenticación y control de acceso, asegurando que tus datos estén protegidos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facilidad de integración&lt;/strong&gt; : Usando herramientas como &lt;strong&gt;Azure CLI&lt;/strong&gt; y APIs, podemos automatizar el proceso de respaldo y facilitar su gestión.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cómo crear un Azure Storage Account y Blob Container
&lt;/h2&gt;

&lt;p&gt;Antes de comenzar con la automatización del respaldo, necesitamos crear un &lt;strong&gt;Storage Account&lt;/strong&gt; y un &lt;strong&gt;Blob Container&lt;/strong&gt; en Azure. Aquí te explico cómo hacerlo:&lt;/p&gt;

&lt;h3&gt;
  
  
  Paso 1: Crear un Azure Storage Account
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accede al portal de Azure&lt;/strong&gt; : Dirígete a &lt;a href="https://portal.azure.com/" rel="noopener noreferrer"&gt;Azure Portal&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Crear un nuevo Storage Account&lt;/strong&gt; :&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Revisar y crear&lt;/strong&gt; : Revisa los detalles y haz clic en &lt;strong&gt;"Crear"&lt;/strong&gt;. El proceso de creación tomará unos minutos.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Paso 2: Crear un Blob Container
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accede a tu Storage Account&lt;/strong&gt; : Una vez creado el Storage Account, haz clic en él desde el portal de Azure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Crear un Blob Container&lt;/strong&gt; :&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;¡Listo! Ahora tienes un &lt;strong&gt;Storage Account&lt;/strong&gt; y un &lt;strong&gt;Blob Container&lt;/strong&gt; donde podrás almacenar tus respaldos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ejemplo práctico: Respaldo automatizado de Ghost con Azure Blob Storage
&lt;/h2&gt;

&lt;p&gt;En este ejemplo, vamos a ver cómo podemos respaldar los datos de un sitio web usando &lt;strong&gt;Azure Blob Storage&lt;/strong&gt;. Aunque el ejemplo es sobre &lt;strong&gt;Ghost&lt;/strong&gt; , este proceso puede adaptarse fácilmente a cualquier otro sistema.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué haremos?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generar el respaldo&lt;/strong&gt; : Primero, crearemos un respaldo de los datos de Ghost y su base de datos MySQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comprimir los archivos&lt;/strong&gt; : Luego, comprimiremos el respaldo en un archivo &lt;code&gt;.tar.gz&lt;/code&gt; para reducir el tamaño y hacerlo más eficiente.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subir a Azure&lt;/strong&gt; : Finalmente, utilizaremos &lt;strong&gt;Azure CLI&lt;/strong&gt; para subir el archivo comprimido a Azure Blob Storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Paso 1: Crear el respaldo de los datos
&lt;/h3&gt;

&lt;p&gt;El primer paso es crear el respaldo de los datos de tu servidor, que incluye tanto los archivos de Ghost como la base de datos MySQL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Conectar al servidor y crear respaldo
ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
  "tar -czf /tmp/ghost-backup.tar.gz /opt/ghost/data /tmp/ghost_database.sql"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este comando crea un archivo comprimido que contiene tanto los archivos de Ghost como la base de datos MySQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paso 2: Subir el respaldo a Azure Blob Storage
&lt;/h3&gt;

&lt;p&gt;Una vez que tenemos el archivo de respaldo, el siguiente paso es subirlo a &lt;strong&gt;Azure Blob Storage&lt;/strong&gt;. Esto se hace de manera sencilla utilizando &lt;strong&gt;Azure CLI&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Subir el archivo de respaldo a Azure Blob Storage
az storage blob upload \
  --account-name $AZURE_STORAGE_ACCOUNT \
  --container-name $CONTAINER_NAME \
  --name "ghost-backup-${TIMESTAMP}.tar.gz" \
  --file "/tmp/ghost-backup.tar.gz" \
  --sas-token $SAS_TOKEN

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este comando sube el archivo comprimido a tu contenedor en Azure Blob Storage, asegurando que el respaldo esté almacenado de forma segura en la nube.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paso 3: Limpiar respaldos antiguos
&lt;/h3&gt;

&lt;p&gt;La gestión de los respaldos no termina solo con la creación y almacenamiento. También es importante mantener tu almacenamiento limpio y organizado. Por eso, puedes establecer una política de retención que elimine los respaldos antiguos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Limpiar respaldos antiguos en Azure
az storage blob list \
  --account-name $AZURE_STORAGE_ACCOUNT \
  --container-name $CONTAINER_NAME \
  --query "[?properties.lastModified &amp;lt; '${CUTOFF_DATE}'].name" \
  --output tsv \
  --sas-token $SAS_TOKEN | while read blob_name; do
    az storage blob delete \
      --account-name $AZURE_STORAGE_ACCOUNT \
      --container-name $CONTAINER_NAME \
      --name "$blob_name" \
      --sas-token $SAS_TOKEN
  done

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este comando lista los respaldos antiguos y los elimina automáticamente según la fecha de retención configurada, ayudando a liberar espacio en Azure y mantener solo los respaldos más recientes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Código completo de la automatización con GitHub Actions
&lt;/h2&gt;

&lt;p&gt;A continuación, te presento el workflow completo de GitHub Actions que implementa todo el sistema de respaldos automatizado. Este archivo YAML incluye todas las funcionalidades que hemos discutido: conexión SSH segura, respaldo de base de datos MySQL, compresión de archivos, subida a Azure Blob Storage y limpieza automática de respaldos antiguos.&lt;/p&gt;

&lt;p&gt;El workflow está diseñado para ejecutarse automáticamente cada día a las 3:00 AM (hora española) y también puede ejecutarse manualmente cuando sea necesario. Incluye manejo robusto de errores, validaciones de espacio en disco y notificaciones automáticas en caso de fallos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Características principales del código:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Respaldo automático diario y manual bajo demanda&lt;/li&gt;
&lt;li&gt;✅ Validación de espacio en disco antes de crear respaldos&lt;/li&gt;
&lt;li&gt;✅ Compresión eficiente con tar.gz&lt;/li&gt;
&lt;li&gt;✅ Subida segura a Azure Blob Storage&lt;/li&gt;
&lt;li&gt;✅ Limpieza automática de respaldos antiguos (60 días de retención)&lt;/li&gt;
&lt;li&gt;✅ Notificaciones automáticas en caso de errores&lt;/li&gt;
&lt;li&gt;✅ Logs detallados para facilitar el debugging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Guarda este código en &lt;code&gt;.github/workflows/backup-ghost-data.yml&lt;/code&gt; en tu repositorio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Respaldo de Datos Ghost

on:
  # Ejecutar manualmente
  workflow_dispatch:
  # Ejecutar automáticamente todos los días a las 3:00 AM hora española (1:00 AM UTC en invierno, 2:00 AM UTC en verano)
  schedule:
    - cron: "0 2 * * *" # 3:00 AM CEST (verano)

env:
  RETENTION_DAYS: 60
  CONTAINER_NAME: ghost-backups
  AZURE_STORAGE_ACCOUNT: aprendejaponesbackup

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  backup:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Descargar repositorio
        uses: actions/checkout@v5

      - name: Configurar clave SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" &amp;gt; ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -H ${{ secrets.SERVER_HOST }} &amp;gt;&amp;gt; ~/.ssh/known_hosts

      - name: Crear directorio de respaldo
        run: |
          echo "📁 Creando directorio de backup local..."
          mkdir -p backup
          echo "✅ Directorio creado en: $(pwd)/backup"
          echo "💾 Espacio disponible en disco:"
          df -h $(pwd) | tail -1

      - name: Generar nombre del archivo de respaldo
        id: backup_info
        run: |
          echo "🏷️ Generando nombre de archivo de backup..."
          TIMESTAMP=$(date +%Y%m%d-%H%M%S)
          BACKUP_START_TIME=$(date +%s)
          if ["${{ github.event_name }}" = "workflow_dispatch"]; then
            # Ejecución manual - usar carpeta snapshots
            FOLDER="snapshots"
            FILENAME="ghost-snapshot-${TIMESTAMP}.tar.gz"
            echo "📸 Tipo: Snapshot manual"
          else
            # Ejecución automática - carpeta raíz
            FOLDER=""
            FILENAME="ghost-data-backup-${TIMESTAMP}.tar.gz"
            echo "⏰ Tipo: Backup automático"
          fi
          echo "📝 Archivo: ${FILENAME}"
          echo "📂 Carpeta: ${FOLDER:-'(raíz)'}"
          echo "filename=${FILENAME}" &amp;gt;&amp;gt; $GITHUB_OUTPUT
          echo "folder=${FOLDER}" &amp;gt;&amp;gt; $GITHUB_OUTPUT
          echo "timestamp=${TIMESTAMP}" &amp;gt;&amp;gt; $GITHUB_OUTPUT
          echo "start_time=${BACKUP_START_TIME}" &amp;gt;&amp;gt; $GITHUB_OUTPUT

      - name: Respaldar base de datos MySQL
        run: |
          echo "🗄️ Iniciando backup de la base de datos MySQL..."
          echo "🔗 Conectando al servidor ${{ secrets.SERVER_HOST }}..."

          # Crear directorio temporal para el backup de la BD
          ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
            "mkdir -p /tmp/ghost-backup-${{ steps.backup_info.outputs.timestamp }}"

          # Obtener el nombre del contenedor MySQL
          echo "🔍 Buscando contenedor MySQL..."
          MYSQL_CONTAINER=$(ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
            "sudo docker ps --format '{{.Names}}' | grep -i mysql | head -1")

          if [-z "$MYSQL_CONTAINER"]; then
            echo "⚠️ No se encontró contenedor MySQL, intentando con 'db' o 'mysql'..."
            MYSQL_CONTAINER=$(ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
              "sudo docker ps --format '{{.Names}}' | grep -E 'db|mysql' | head -1")
          fi

          if [-z "$MYSQL_CONTAINER"]; then
            echo "❌ ERROR: No se pudo encontrar el contenedor MySQL"
            echo "📋 Contenedores disponibles:"
            ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} "sudo docker ps --format '{{.Names}}'"
            exit 1
          fi

          echo "✅ Contenedor MySQL encontrado: ${MYSQL_CONTAINER}"

          # Exportar la base de datos
          echo "💾 Exportando base de datos 'ghost'..."

          # Intentar primero con el usuario de la base de datos
          echo "🔐 Intentando con usuario de base de datos..."
          if ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
            "sudo docker exec -e MYSQL_PWD='${{ secrets.DATABASE_PASSWORD }}' ${MYSQL_CONTAINER} mysqldump -u ${{ secrets.DATABASE_USER }} ghost | sudo tee /tmp/ghost-backup-${{ steps.backup_info.outputs.timestamp }}/ghost_database.sql &amp;gt; /dev/null" 2&amp;gt;/dev/null; then
            echo "✅ Backup exitoso con usuario de base de datos"
          else
            echo "⚠️ Falló con usuario de BD, intentando con root..."
            ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
              "sudo docker exec -e MYSQL_PWD='${{ secrets.MYSQL_ROOT_PASSWORD }}' ${MYSQL_CONTAINER} mysqldump -u root ghost | sudo tee /tmp/ghost-backup-${{ steps.backup_info.outputs.timestamp }}/ghost_database.sql &amp;gt; /dev/null"
            echo "✅ Backup exitoso con usuario root"
          fi

          # Verificar que el backup se creó correctamente
          DB_SIZE=$(ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
            "ls -lh /tmp/ghost-backup-${{ steps.backup_info.outputs.timestamp }}/ghost_database.sql | awk '{print \$5}'")

          echo "✅ Base de datos exportada exitosamente"
          echo "📊 Tamaño del dump SQL: ${DB_SIZE}"

      - name: Respaldar datos de Ghost vía SSH
        run: |
          echo "🔗 Conectando al servidor ${{ secrets.SERVER_HOST }}..."
          echo "📦 Creando backup comprimido en el servidor..."
          echo "📍 Origen archivos: /opt/ghost/data/ghost"
          echo "📍 Origen BD: /tmp/ghost-backup-${{ steps.backup_info.outputs.timestamp }}/ghost_database.sql"
          echo "📍 Destino temporal: /tmp/${{ steps.backup_info.outputs.filename }}"

          # Verificar espacio disponible en el servidor
          echo "💾 Verificando espacio en servidor..."
          ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
            "echo 'Disco principal:' &amp;amp;&amp;amp; df -h /"

          # Validar espacio libre mínimo (30%)
          echo "🔍 Validando espacio libre mínimo..."
          DISK_USAGE=$(ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
            "df / | tail -1 | awk '{print \$5}' | sed 's/%//'")

          echo "📊 Uso actual del disco: ${DISK_USAGE}%"

          if ["$DISK_USAGE" -gt 70]; then
            echo "❌ ERROR: Espacio insuficiente en el servidor"
            echo " 💾 Uso actual: ${DISK_USAGE}% (máximo permitido: 70%)"
            echo " 🚨 Se requiere al menos 30% de espacio libre para crear el backup"
            echo " 💡 Libera espacio en el servidor antes de continuar"
            exit 1
          else
            echo "✅ Espacio suficiente: ${DISK_USAGE}% usado ($((100-DISK_USAGE))% libre)"
          fi

          # Estimar tamaño del backup y verificar si cabe
          echo "📏 Estimando tamaño del backup..."
          DATA_SIZE_KB=$(ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
            "du -sk /opt/ghost/data 2&amp;gt;/dev/null | awk '{print \$1}' || echo '0'")

          if ["$DATA_SIZE_KB" -gt 0]; then
            # Estimar que el backup comprimido será ~60% del tamaño original
            ESTIMATED_BACKUP_KB=$((DATA_SIZE_KB * 60 / 100))
            AVAILABLE_KB=$(ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
              "df / | tail -1 | awk '{print \$4}'")

            echo "📊 Tamaño de datos: $((DATA_SIZE_KB/1024)) MB"
            echo "📊 Backup estimado: $((ESTIMATED_BACKUP_KB/1024)) MB (comprimido)"
            echo "📊 Espacio disponible: $((AVAILABLE_KB/1024)) MB"

            if ["$ESTIMATED_BACKUP_KB" -gt "$AVAILABLE_KB"]; then
              echo "❌ ERROR: El backup estimado no cabe en el disco"
              echo " 📦 Backup estimado: $((ESTIMATED_BACKUP_KB/1024)) MB"
              echo " 💾 Espacio disponible: $((AVAILABLE_KB/1024)) MB"
              echo " 🚨 Se necesitan al menos $((ESTIMATED_BACKUP_KB/1024)) MB libres"
              exit 1
            else
              echo "✅ El backup estimado cabe en el disco"
            fi
          else
            echo "⚠️ No se pudo estimar el tamaño, continuando..."
          fi

          # Crear backup que incluye archivos y base de datos
          echo "📦 Creando archivo tar.gz con archivos y base de datos..."
          ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
            "sudo tar -czf /tmp/${{ steps.backup_info.outputs.filename }} \
              -C /opt/ghost/data --exclude='ghost/content/themes' ghost \
              -C /tmp/ghost-backup-${{ steps.backup_info.outputs.timestamp }} ghost_database.sql"

          echo "✅ Backup creado en el servidor"
          echo "⬇️ Descargando backup al runner de GitHub..."

          scp -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }}:/tmp/${{ steps.backup_info.outputs.filename }} backup/

          echo "✅ Backup descargado exitosamente"
          echo "📊 Información del archivo:"
          FILE_SIZE=$(ls -lh backup/${{ steps.backup_info.outputs.filename }} | awk '{print $5}')
          FILE_SIZE_BYTES=$(stat -c%s backup/${{ steps.backup_info.outputs.filename }})
          echo " 📏 Tamaño: ${FILE_SIZE} (${FILE_SIZE_BYTES} bytes)"
          echo " 📍 Ubicación: $(pwd)/backup/${{ steps.backup_info.outputs.filename }}"
          echo " 🗜️ Compresión: tar.gz"
          echo " 📦 Contenido: archivos Ghost + base de datos MySQL"

      - name: Limpiar archivos temporales del servidor
        if: always()
        run: |
          echo "🧹 Limpiando archivos temporales del servidor..."
          echo "📍 Eliminando: /tmp/${{ steps.backup_info.outputs.filename }}"
          ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
            "sudo rm -f /tmp/${{ steps.backup_info.outputs.filename }}" || true
          echo "📍 Eliminando directorio temporal de BD: /tmp/ghost-backup-${{ steps.backup_info.outputs.timestamp }}"
          ssh -i ~/.ssh/id_rsa ${{ secrets.SSH_USER }}@${{ secrets.SERVER_HOST }} \
            "sudo rm -rf /tmp/ghost-backup-${{ steps.backup_info.outputs.timestamp }}" || true
          echo "✅ Limpieza del servidor completada"

      - name: Instalar Azure CLI
        run: |
          echo "⚙️ Instalando Azure CLI..."
          curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
          echo "✅ Azure CLI instalado"

      - name: Subir respaldo a Azure Storage
        run: |
          echo "☁️ Preparando subida a Azure Storage..."

          # Determinar la ruta completa del blob
          if [-n "${{ steps.backup_info.outputs.folder }}"]; then
            BLOB_NAME="${{ steps.backup_info.outputs.folder }}/${{ steps.backup_info.outputs.filename }}"
          else
            BLOB_NAME="${{ steps.backup_info.outputs.filename }}"
          fi

          echo "📍 Storage Account: ${{ env.AZURE_STORAGE_ACCOUNT }}"
          echo "📍 Container: ${{ env.CONTAINER_NAME }}"
          echo "📍 Blob: ${BLOB_NAME}"

          # Mostrar tamaño antes de subir
          FILE_SIZE=$(ls -lh backup/${{ steps.backup_info.outputs.filename }} | awk '{print $5}')
          echo "📦 Archivo a subir: ${FILE_SIZE}"
          echo "⬆️ Iniciando subida a Azure Storage..."

          az storage blob upload \
            --account-name ${{ env.AZURE_STORAGE_ACCOUNT }} \
            --container-name ${{ env.CONTAINER_NAME }} \
            --name "${BLOB_NAME}" \
            --file backup/${{ steps.backup_info.outputs.filename }} \
            --sas-token "${{ secrets.AZURE_SAS_TOKEN }}"

          echo "✅ Backup subido exitosamente a Azure Storage"
          echo "🌐 URL completa: https://${{ env.AZURE_STORAGE_ACCOUNT }}.blob.core.windows.net/${{ env.CONTAINER_NAME }}/${BLOB_NAME}"

      - name: Limpiar respaldos antiguos en Azure Storage
        run: |
          echo "🗓️ Iniciando limpieza de backups antiguos..."
          # Obtener fecha límite para retención
          CUTOFF_DATE=$(date -d "${{ env.RETENTION_DAYS }} days ago" +%Y-%m-%d)
          echo "📅 Fecha límite: ${CUTOFF_DATE} (archivos anteriores serán eliminados)"
          echo "⏳ Retención configurada: ${{ env.RETENTION_DAYS }} días"

          # Limpiar backups automáticos antiguos
          echo "🧹 Limpiando backups automáticos antiguos..."
          echo "🔍 Buscando archivos con prefijo: ghost-data-backup-"
          az storage blob list \
            --account-name ${{ env.AZURE_STORAGE_ACCOUNT }} \
            --container-name ${{ env.CONTAINER_NAME }} \
            --prefix "ghost-data-backup-" \
            --query "[?properties.lastModified &amp;lt; '${CUTOFF_DATE}'].name" \
            --output tsv \
            --sas-token "${{ secrets.AZURE_SAS_TOKEN }}" | while read blob_name; do
              if [! -z "$blob_name"]; then
                echo "🗑️ Eliminando backup automático antiguo: $blob_name"
                az storage blob delete \
                  --account-name ${{ env.AZURE_STORAGE_ACCOUNT }} \
                  --container-name ${{ env.CONTAINER_NAME }} \
                  --name "$blob_name" \
                  --sas-token "${{ secrets.AZURE_SAS_TOKEN }}"
              fi
            done

          # Limpiar snapshots manuales antiguos
          echo "🧹 Limpiando snapshots manuales antiguos..."
          echo "🔍 Buscando archivos con prefijo: snapshots/ghost-snapshot-"
          az storage blob list \
            --account-name ${{ env.AZURE_STORAGE_ACCOUNT }} \
            --container-name ${{ env.CONTAINER_NAME }} \
            --prefix "snapshots/ghost-snapshot-" \
            --query "[?properties.lastModified &amp;lt; '${CUTOFF_DATE}'].name" \
            --output tsv \
            --sas-token "${{ secrets.AZURE_SAS_TOKEN }}" | while read blob_name; do
              if [! -z "$blob_name"]; then
                echo "🗑️ Eliminando snapshot manual antiguo: $blob_name"
                az storage blob delete \
                  --account-name ${{ env.AZURE_STORAGE_ACCOUNT }} \
                  --container-name ${{ env.CONTAINER_NAME }} \
                  --name "$blob_name" \
                  --sas-token "${{ secrets.AZURE_SAS_TOKEN }}"
              fi
            done

          echo "✅ Limpieza de archivos antiguos completada"

      - name: Limpiar clave SSH
        if: always()
        run: |
          echo "🔐 Limpiando clave SSH temporal..."
          rm -f ~/.ssh/id_rsa
          echo "✅ Clave SSH eliminada"

      - name: Resumen del respaldo
        id: backup_summary
        run: |
          echo "✅ Backup completado exitosamente"
          echo "📁 Archivo: ${{ steps.backup_info.outputs.filename }}"

          # Calcular tiempo total de backup
          END_TIME=$(date +%s)
          DURATION=$((END_TIME - ${{ steps.backup_info.outputs.start_time }}))
          DURATION_MIN=$((DURATION / 60))
          DURATION_SEC=$((DURATION % 60))

          echo "⏱️ Tiempo total: ${DURATION_MIN}m ${DURATION_SEC}s"
          echo "duration=${DURATION}" &amp;gt;&amp;gt; $GITHUB_OUTPUT

          # Obtener tamaño del backup
          BACKUP_SIZE=$(ls -lh backup/${{ steps.backup_info.outputs.filename }} 2&amp;gt;/dev/null | awk '{print $5}' || echo "N/A")
          echo "backup_size=${BACKUP_SIZE}" &amp;gt;&amp;gt; $GITHUB_OUTPUT

          if ["${{ github.event_name }}" = "workflow_dispatch"]; then
            echo "📸 Tipo: Snapshot manual"
            echo "☁️ Ubicación: ${{ env.AZURE_STORAGE_ACCOUNT }}/${{ env.CONTAINER_NAME }}/snapshots/"
          else
            echo "⏰ Tipo: Backup automático"
            echo "☁️ Ubicación: ${{ env.AZURE_STORAGE_ACCOUNT }}/${{ env.CONTAINER_NAME }}/"
          fi
          echo "🗓️ Retención configurada: ${{ env.RETENTION_DAYS }} días"

  # Notificación solo en caso de fallo
  notificar-fallo:
    needs: backup
    runs-on: ubuntu-latest
    if: failure()
    permissions:
      issues: write
    steps:
      - name: Crear notificación de fallo
        uses: actions/github-script@v7
        with:
          script: |
            const timestamp = new Date().toLocaleString('es-ES', { 
              timeZone: 'Europe/Madrid',
              year: 'numeric',
              month: '2-digit',
              day: '2-digit',
              hour: '2-digit',
              minute: '2-digit'
            });

            const backupType = '${{ github.event_name }}' === 'workflow_dispatch' ? 'Snapshot Manual' : 'Backup Automático';
            const title = `🚨 Error en ${backupType} - ${timestamp}`;

            const body = `## 🚨 Error en el Sistema de Backups

            **Fecha:** ${timestamp}
            **Tipo:** ${backupType}
            **Estado:** ❌ El backup ha fallado

            ### 📋 Detalles del Error

            - **Workflow:** \`${{ github.workflow }}\`
            - **Run ID:** \`${{ github.run_id }}\`
            - **Commit:** \`${{ github.sha }}\`
            - **Rama:** \`${{ github.ref_name }}\`

            ### 🔗 Enlaces Útiles

            - [Ver logs del workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
            - [Ejecutar backup manual](https://github.com/${{ github.repository }}/actions/workflows/backup-ghost-data.yml)

            ### 🛠️ Acciones Recomendadas

            1. **Revisar los logs** del workflow para identificar el error específico
            2. **Verificar conectividad** SSH al servidor
            3. **Comprobar espacio** disponible en el servidor
            4. **Validar credenciales** de Azure Storage
            5. **Ejecutar backup manual** una vez solucionado el problema

            ### ⚠️ Importante

            - Los datos de Ghost no están siendo respaldados automáticamente
            - Se recomienda solucionar este problema lo antes posible
            - Considera ejecutar un backup manual mientras se resuelve el issue

            ---

            *Notificación automática del sistema de backups*

            /cc @danieljsaldana`;

            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: title,
              body: body,
              assignees: ['danieljsaldana'],
              labels: ['backup', 'error', 'urgent']
            });

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Beneficios de usar Azure Blob Storage para tus respaldos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatización&lt;/strong&gt; : Gracias a herramientas como &lt;strong&gt;Azure CLI&lt;/strong&gt; , puedes automatizar el proceso de creación y almacenamiento de respaldos, eliminando la intervención manual.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalabilidad&lt;/strong&gt; : Puedes almacenar un número prácticamente ilimitado de respaldos sin preocuparte por quedarte sin espacio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seguridad&lt;/strong&gt; : Con Azure, tus datos están protegidos por varias capas de seguridad, incluyendo autenticación avanzada y cifrado en reposo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retención eficiente&lt;/strong&gt; : Azure te permite configurar políticas de retención para eliminar respaldos antiguos, lo que te ayuda a optimizar el uso del espacio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="/images/posts/respaldo-seguro-y-eficiente-usando-azure-blob-storage-para-proteger-tus-datos-2.png" class="article-body-image-wrapper"&gt;&lt;img src="/images/posts/respaldo-seguro-y-eficiente-usando-azure-blob-storage-para-proteger-tus-datos-2.png" alt="Backups de ejemplo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;El respaldo de tus datos no tiene por qué ser complicado. Usando &lt;strong&gt;Azure Blob Storage&lt;/strong&gt; , puedes garantizar que tus datos estén siempre seguros y accesibles, al mismo tiempo que automatizas el proceso para que no tengas que preocuparte por hacerlo manualmente. No importa si trabajas con Ghost o cualquier otro sistema, Azure te ofrece la flexibilidad y escalabilidad que necesitas.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>azure</category>
      <category>dev</category>
    </item>
    <item>
      <title>He lanzado un nuevo proyecto para aprender japonés</title>
      <dc:creator>Daniel J. Saldaña</dc:creator>
      <pubDate>Sat, 06 Dec 2025 21:06:28 +0000</pubDate>
      <link>https://forem.com/danieljsaldana/he-lanzado-un-nuevo-proyecto-para-aprender-japones-2o6f</link>
      <guid>https://forem.com/danieljsaldana/he-lanzado-un-nuevo-proyecto-para-aprender-japones-2o6f</guid>
      <description>&lt;p&gt;Después de meses de trabajo, por fin puedo compartir algo que me hace mucha ilusión: he lanzado aprendejapones.es, un proyecto personal donde combino dos cosas que me apasionan: aprender japonés y construir productos web.&lt;/p&gt;

&lt;p&gt;Lo que empezó como un simple blog para documentar mi camino hacia el JLPT N5 ha terminado convirtiéndose en una plataforma completa. Incluye rutinas de estudio, apuntes, vocabulario interactivo, historias cortas, validación de pronunciación con IA, un pequeño sistema de karma y un ninja que modera la comunidad.&lt;br&gt;
Ah, y una mascota llamada Goliat, un bulldog samurái que se ha convertido en mi compañero de batalla durante todo el desarrollo.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Por qué crear mi propia plataforma?
&lt;/h2&gt;

&lt;p&gt;Porque ninguna herramienta existente representaba justo lo que quería: un espacio simple, motivador y con un toque personal. Empecé montándolo encima de Ghost, pero al crecer el proyecto tuve que construir extensiones y servicios propios para superar sus limitaciones.&lt;/p&gt;

&lt;p&gt;El resultado es una mezcla de contenido educativo, automatizaciones y pequeñas funcionalidades que hacen que aprender japonés sea más divertido… y que a mí me permita mejorar como desarrollador.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué incluye ahora mismo?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Lecciones básicas orientadas al JLPT N5&lt;/li&gt;
&lt;li&gt;Diccionario y vocabulario con fichas&lt;/li&gt;
&lt;li&gt;Validación de pronunciación&lt;/li&gt;
&lt;li&gt;Historias cortas para practicar lectura&lt;/li&gt;
&lt;li&gt;Sistema de comentarios con moderación&lt;/li&gt;
&lt;li&gt;Página de progreso&lt;/li&gt;
&lt;li&gt;Integración con notas personalizadas (próximamente)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ¿Por qué lo comparto aquí?
&lt;/h2&gt;

&lt;p&gt;Porque este proyecto ha sido, sobre todo, una excusa para aprender mientras construyo: arquitectura, automatización, diseño de producto, y cómo resolver problemas reales para usuarios reales… incluso si ese usuario soy yo mismo.&lt;/p&gt;

&lt;p&gt;Si te interesa el japonés o simplemente te gusta ver cómo evoluciona un proyecto indie, te dejo el enlace:&lt;br&gt;
👉 &lt;a href="https://aprendejapones.es" rel="noopener noreferrer"&gt;https://aprendejapones.es&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Y si tienes feedback, ideas o quieres decirle algo a Goliat el samurái, estaré encantado de leerte.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ghost</category>
      <category>product</category>
      <category>development</category>
    </item>
    <item>
      <title>Goliat Dashboard: Mi nueva aventura en la gestión de recursos Cloud</title>
      <dc:creator>Daniel J. Saldaña</dc:creator>
      <pubDate>Wed, 22 Oct 2025 08:04:21 +0000</pubDate>
      <link>https://forem.com/danieljsaldana/goliat-dashboard-mi-nueva-aventura-en-la-gestion-de-recursos-cloud-27gc</link>
      <guid>https://forem.com/danieljsaldana/goliat-dashboard-mi-nueva-aventura-en-la-gestion-de-recursos-cloud-27gc</guid>
      <description>&lt;p&gt;Estoy emocionado de compartir un nuevo proyecto en el que estoy trabajando: &lt;strong&gt;Goliat Dashboard&lt;/strong&gt;. 🎉 Este sistema es mi próximo gran paso, diseñado específicamente para ofrecer &lt;strong&gt;control total sobre los despliegues realizados con Terraform&lt;/strong&gt; , facilitando la organización y la gestión de los recursos en la nube.&lt;/p&gt;

&lt;p&gt;Aunque aún está en desarrollo, quiero aprovechar esta oportunidad para contarles más sobre el enfoque y las ideas que están impulsando esta iniciativa.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Qué es Goliat Dashboard?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Goliat Dashboard&lt;/strong&gt; tiene como objetivo centralizar y organizar los despliegues realizados mediante Terraform, ya sea usando Terraform Cloud o su provider oficial, para proporcionar una &lt;strong&gt;visión clara y estructurada de lo que se ha desplegado en tu infraestructura&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Algunas de las funcionalidades clave incluyen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gestión eficiente y centralizada&lt;/strong&gt; de recursos desplegados con Terraform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visión clara y en tiempo real&lt;/strong&gt; de los despliegues organizados por &lt;strong&gt;organizaciones, proyectos y workspaces&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acceso a métricas detalladas&lt;/strong&gt; de los recursos creados, ayudando a los equipos a tomar decisiones informadas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En un entorno donde cada vez más aplicaciones se despliegan en la nube, mantener un control efectivo de los recursos puede ser un desafío. &lt;strong&gt;Goliat Dashboard&lt;/strong&gt; busca resolver este problema al proporcionar una herramienta que no solo organice los despliegues, sino que también facilite su monitoreo y gestión.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;El valor de la IA en Goliat Dashboard&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Una característica diferenciadora de este proyecto es la integración de &lt;strong&gt;inteligencia artificial&lt;/strong&gt; , diseñada para:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analizar los despliegues realizados&lt;/strong&gt; y ofrecer información útil sobre el estado de la infraestructura.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolver dudas sobre los recursos creados&lt;/strong&gt; , como detalles sobre configuraciones, relaciones o estados actuales.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facilitar informes y análisis estructurados&lt;/strong&gt; que ayuden a los usuarios a entender mejor su infraestructura y optimizar su uso.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esta funcionalidad convierte a Goliat Dashboard en una herramienta que no solo organiza, sino que también simplifica el entendimiento de tu infraestructura en la nube.&lt;/p&gt;

&lt;h5&gt;
  
  
  &lt;strong&gt;La seguridad como pilar fundamental: Shield&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;Desde el inicio, estoy poniendo un fuerte énfasis en la seguridad. Por eso, he comenzado a desarrollar &lt;strong&gt;Shield&lt;/strong&gt; , el módulo de seguridad que garantizará que los despliegues gestionados estén siempre protegidos.&lt;/p&gt;

&lt;h6&gt;
  
  
  &lt;strong&gt;¿Qué incluirá Shield?&lt;/strong&gt;
&lt;/h6&gt;

&lt;p&gt;🔒 &lt;strong&gt;Bloqueo por país, IP o rango de IP:&lt;/strong&gt; Para prevenir accesos no autorizados.&lt;br&gt;&lt;br&gt;
🔒 &lt;strong&gt;Tokens seguros con caducidad:&lt;/strong&gt; Gestiona los accesos de forma controlada y protege los endpoints.&lt;br&gt;&lt;br&gt;
🤖 &lt;strong&gt;IA para análisis de seguridad:&lt;/strong&gt; Diseñado para identificar actividades sospechosas, generar informes de seguridad y responder preguntas críticas sobre la seguridad de tus recursos.&lt;/p&gt;

&lt;p&gt;Shield no solo será un complemento de seguridad, sino una herramienta esencial para mantener tu infraestructura protegida de manera proactiva y eficiente.&lt;/p&gt;

&lt;h6&gt;
  
  
  &lt;strong&gt;Estado actual del proyecto&lt;/strong&gt;
&lt;/h6&gt;

&lt;p&gt;Aun se encuentra en una fase temprana de desarrollo y estoy definiendo el alcance y las funcionalidades clave. Por lo que aun no puedo compartir una hoja de ruta detallada, espero en los proximos meses poder compartir más detalles sobre el progreso y los próximos pasos.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¡Únete a este viaje!&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Si esta idea te resulta interesante, te invito a seguir el desarrollo de &lt;strong&gt;Goliat Dashboard&lt;/strong&gt;. Pronto compartiré más avances, y ya puedes explorar una &lt;strong&gt;demo interactiva&lt;/strong&gt; en:&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://goliat-dashboard.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;goliat-dashboard.com&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Además, este proyecto será &lt;strong&gt;open source&lt;/strong&gt; , y estará disponible en GitHub para que la comunidad pueda colaborar, aprender o simplemente explorar cómo está construido.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Repositorio de Goliat Dashboard:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/danieljsaldana/goliat-dashboard" rel="noopener noreferrer"&gt;&lt;strong&gt;https://github.com/danieljsaldana/goliat-dashboard&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gracias por acompañarme en este emocionante camino. ¡Estoy deseando compartir más pronto! 🚀&lt;/p&gt;

&lt;p&gt;Os comporato un video de la demostración del proyecto y recordar que ire subiendo a mi canal de YouTube más contenido sobre el desarrollo de este proyecto.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>github</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Convierte texto a audio con Azure Speech y almacenamiento en la nube</title>
      <dc:creator>Daniel J. Saldaña</dc:creator>
      <pubDate>Wed, 22 Oct 2025 08:00:51 +0000</pubDate>
      <link>https://forem.com/danieljsaldana/convierte-texto-a-audio-con-azure-speech-y-almacenamiento-en-la-nube-3j07</link>
      <guid>https://forem.com/danieljsaldana/convierte-texto-a-audio-con-azure-speech-y-almacenamiento-en-la-nube-3j07</guid>
      <description>&lt;p&gt;Si alguna vez has querido transformar contenido de texto en archivos de audio de alta calidad y almacenarlos de forma segura en la nube, &lt;strong&gt;Azure Speech Services&lt;/strong&gt; y &lt;strong&gt;Azure Blob Storage&lt;/strong&gt; ofrecen una solución potente y escalable. En este artículo, te guiaré a través del proceso de convertir texto a voz usando &lt;strong&gt;Azure Speech Services&lt;/strong&gt; y luego almacenarlo en &lt;strong&gt;Azure Blob Storage&lt;/strong&gt; , una opción ideal para almacenar y gestionar tus archivos de audio de manera eficiente y segura.&lt;/p&gt;

&lt;p&gt;Este proceso es útil en una variedad de escenarios, como la conversión de artículos, posts de blogs, documentos educativos o cualquier otro tipo de contenido textual en audios. A lo largo de este tutorial, utilizaremos ejemplos prácticos basados en &lt;strong&gt;Ghost CMS&lt;/strong&gt; , pero los conceptos y el código son totalmente aplicables a cualquier sistema de gestión de contenido o aplicación que genere texto.&lt;/p&gt;

&lt;h4&gt;
  
  
  ¿Por Qué convertir texto a audio?
&lt;/h4&gt;

&lt;p&gt;El proceso de convertir texto a audio tiene múltiples aplicaciones, entre las que se incluyen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accesibilidad&lt;/strong&gt; : Permite a personas con dificultades visuales o de lectura acceder al contenido de forma más cómoda.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mejor Retención&lt;/strong&gt; : Escuchar contenido puede mejorar la comprensión y retención, especialmente en contextos educativos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactividad&lt;/strong&gt; : Ofrece a los usuarios la opción de consumir contenido sin necesidad de leerlo, lo que fomenta una mayor interacción y flexibilidad.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Servicios necesarios
&lt;/h4&gt;

&lt;p&gt;En este tutorial, utilizaremos dos servicios clave de &lt;strong&gt;Azure&lt;/strong&gt; :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Azure Speech Services&lt;/strong&gt; : Un servicio de conversión de texto a voz que ofrece voces de alta calidad, como la &lt;strong&gt;voz HD Dragon Nanami&lt;/strong&gt; en japonés.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Blob Storage&lt;/strong&gt; : Un servicio de almacenamiento en la nube donde guardaremos los archivos de audio generados, con un acceso seguro mediante &lt;strong&gt;SAS Tokens&lt;/strong&gt; (firma de acceso compartido).&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Flujo de trabajo
&lt;/h4&gt;

&lt;p&gt;El proceso de convertir texto a audio y almacenarlo en la nube sigue estos pasos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prepara el texto&lt;/strong&gt; : Recibimos el contenido textual que queremos convertir a voz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convierte el texto a audio&lt;/strong&gt; : Usamos &lt;strong&gt;Azure Speech Services&lt;/strong&gt; para generar el audio en el formato adecuado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sube el archivo de audio a Azure Blob Storage&lt;/strong&gt; : Usamos &lt;strong&gt;Azure Blob Storage&lt;/strong&gt; para almacenar el archivo de audio, generando una URL temporal que permite acceder al archivo.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Paso 1: Configuración de Azure Speech Services
&lt;/h4&gt;

&lt;p&gt;El primer paso es configurar &lt;strong&gt;Azure Speech Services&lt;/strong&gt; , que nos permitirá convertir el texto a voz. Necesitarás crear un recurso de &lt;strong&gt;Speech Services&lt;/strong&gt; en el portal de Azure y obtener una clave de suscripción.&lt;/p&gt;

&lt;h5&gt;
  
  
  Configuración de la voz
&lt;/h5&gt;

&lt;p&gt;Seleccionamos una voz adecuada para el proceso de conversión. En este caso, usaremos la &lt;strong&gt;voz HD Dragon Nanami&lt;/strong&gt; para la conversión de texto a voz en japonés. Aquí tienes el código para configurar la voz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Configuración de la voz
export const VOICE_CONFIG = {
  voice_name: 'ja-JP-Nanami:DragonHDLatestNeural', // Voz japonesa HD Dragon
  language: 'ja-JP', // Idioma japonés
  name: 'Nanami Dragon HD Latest',
  description: 'Voz japonesa HD Dragon Latest multilingüe'
};

// Función para obtener la configuración de la voz
export function getVoiceConfig() {
  return {
    voice_name: VOICE_CONFIG.voice_name,
    language: VOICE_CONFIG.language
  };
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En este fragmento, definimos la voz &lt;strong&gt;Nanami Dragon HD&lt;/strong&gt; en japonés. Este paso es crucial para que Azure pueda procesar correctamente el texto.&lt;/p&gt;

&lt;h4&gt;
  
  
  Paso 2: Convertir texto a audio con Azure Speech
&lt;/h4&gt;

&lt;p&gt;Azure Speech Services utiliza &lt;strong&gt;SSML (Speech Synthesis Markup Language)&lt;/strong&gt; para procesar el texto. &lt;strong&gt;SSML&lt;/strong&gt; nos permite controlar cómo se debe leer el texto, incluyendo pausas, tono y velocidad. Vamos a crear una función que convierte el texto en el formato SSML y lo envía a la API de Azure para generar el audio.&lt;/p&gt;

&lt;h5&gt;
  
  
  Ejemplo de conversión de texto a SSML
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export function wrapWithSSML(text) {
  const BREAK_MARKER = ' ___SSML_BREAK___';
  const breaks = [];

  let sanitizedText = text.replace(/&amp;lt;break\s+time="[^"]+"\s*\/&amp;gt;/g, _ =&amp;gt; {
    breaks.push(_);
    return BREAK_MARKER;
  });

  sanitizedText = sanitizedText
    .replace(/&amp;amp;/g, '&amp;amp;amp;')
    .replace(/&amp;lt;/g, '&amp;amp;lt;')
    .replace(/&amp;gt;/g, '&amp;amp;gt;')
    .replace(/"/g, '&amp;amp;quot;')
    .replace(/'/g, '&amp;amp;apos;')
    .replace(/\*/g, '')
    .replace(/[“”«»]/g, '"')
    .replace(/[–—]/g, '-');

  let breakIdx = 0;
  sanitizedText = sanitizedText.replace(new RegExp(BREAK_MARKER, 'g'), () =&amp;gt; {
    return breaks[breakIdx++] || '&amp;lt;break time="500ms"/&amp;gt;';
  });

  return `&amp;lt;speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="${VOICE_CONFIG.language}"&amp;gt;
    &amp;lt;voice name="${VOICE_CONFIG.voice_name}"&amp;gt;
      ${sanitizedText}
    &amp;lt;/voice&amp;gt;
  &amp;lt;/speak&amp;gt;`;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este código toma el texto de entrada, lo sanitiza y lo convierte en &lt;strong&gt;SSML&lt;/strong&gt; para enviarlo a la API de Azure Speech. Las pausas también se gestionan en este formato, lo que hace que el audio resultante sea más natural y comprensible.&lt;/p&gt;

&lt;h5&gt;
  
  
  Llamada a la API de Azure Speech
&lt;/h5&gt;

&lt;p&gt;Ahora que tenemos el texto en formato SSML, podemos enviarlo a &lt;strong&gt;Azure Speech Services&lt;/strong&gt; para generar el audio. Aquí te mostramos cómo hacerlo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const AZURE_SPEECH_KEY = 'tu-clave-de-api';
const AZURE_ENDPOINT = 'https://westeurope.api.cognitive.microsoft.com'; // Cambiar por la región correspondiente

async function generateAudio(ssmlInput) {
  const response = await fetch(`${AZURE_ENDPOINT}/cognitiveservices/v1`, {
    method: 'POST',
    headers: {
      'Ocp-Apim-Subscription-Key': AZURE_SPEECH_KEY,
      'Content-Type': 'application/ssml+xml',
      'X-Microsoft-OutputFormat': 'audio-24khz-160kbitrate-mono-mp3'
    },
    body: ssmlInput
  });

  if (!response.ok) {
    throw new Error(`Error en Azure Speech: ${response.statusText}`);
  }

  return await response.arrayBuffer(); // Devuelve el audio en formato binario
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este código envía una solicitud &lt;code&gt;POST&lt;/code&gt; a &lt;strong&gt;Azure Speech Services&lt;/strong&gt; con el SSML. La respuesta es un archivo de audio, que podemos almacenar en el siguiente paso.&lt;/p&gt;

&lt;h4&gt;
  
  
  Paso 3: Subir el asudio a Azure Blob Storage
&lt;/h4&gt;

&lt;p&gt;Una vez que el audio ha sido generado, debemos subirlo a &lt;strong&gt;Azure Blob Storage&lt;/strong&gt; para su almacenamiento seguro y accesible. &lt;strong&gt;Azure Blob Storage&lt;/strong&gt; nos permite generar &lt;strong&gt;SAS Tokens&lt;/strong&gt; , que son URLs temporales para acceder al archivo sin exponer la cuenta de almacenamiento.&lt;/p&gt;

&lt;h5&gt;
  
  
  Subir el archivo a Azure Blob Storage
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function uploadToAzureBlob(audioBuffer, filePath, sasUrl) {
  const sasIndex = sasUrl.indexOf('?');
  const baseUrl = sasUrl.slice(0, sasIndex);
  const sasToken = sasUrl.slice(sasIndex + 1);

  const uploadUrl = `${baseUrl}/${filePath}?${sasToken}`;

  const response = await fetch(uploadUrl, {
    method: 'PUT',
    headers: {
      'x-ms-blob-type': 'BlockBlob',
      'Content-Type': 'audio/mpeg'
    },
    body: audioBuffer
  });

  if (!response.ok) {
    throw new Error(`Error al subir el audio a Azure Blob Storage: ${response.statusText}`);
  }

  return uploadUrl; // URL del archivo de audio en Azure Blob Storage
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este código sube el archivo de audio generado a &lt;strong&gt;Azure Blob Storage&lt;/strong&gt; , utilizando la URL con el SAS Token para acceder de manera controlada al archivo.&lt;/p&gt;

&lt;p&gt;Al integrar Azure Speech Services y Azure Blob Storage en tu flujo de trabajo, podrás transformar cualquier contenido textual en audios de alta calidad y almacenarlos de manera segura en la nube. Esta solución no solo mejora la accesibilidad y la interacción con el contenido, sino que también ofrece una manera eficiente de gestionar los archivos de audio generados, optimizando el almacenamiento y el acceso.&lt;/p&gt;

&lt;p&gt;Si queréis ver esta aplicación en funcionamiento, podéis visitar el siguiente enlace: &lt;a href="https://aprendejapones.es/" rel="noopener noreferrer"&gt;Aprende Japonés&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>azure</category>
      <category>developer</category>
      <category>spanish</category>
    </item>
    <item>
      <title>Prueba de concepto: interpretación de infraestructura con ChatGPT + Terraform</title>
      <dc:creator>Daniel J. Saldaña</dc:creator>
      <pubDate>Mon, 02 Jun 2025 16:00:00 +0000</pubDate>
      <link>https://forem.com/danieljsaldana/prueba-de-concepto-interpretacion-de-infraestructura-con-chatgpt-terraform-mgf</link>
      <guid>https://forem.com/danieljsaldana/prueba-de-concepto-interpretacion-de-infraestructura-con-chatgpt-terraform-mgf</guid>
      <description>&lt;p&gt;En esta prueba de concepto se explora cómo integrar modelos de lenguaje como ChatGPT en flujos de trabajo de infraestructura como código (IaC), utilizando un caso concreto basado en Terraform sobre Azure. El objetivo no es reemplazar las herramientas existentes, sino complementarlas con explicaciones automáticas, resúmenes funcionales y validación semántica del código antes de su ejecución.&lt;/p&gt;

&lt;h4&gt;
  
  
  Objetivo
&lt;/h4&gt;

&lt;p&gt;Demostrar cómo una integración con ChatGPT puede servir como asistente técnico para interpretar archivos Terraform, anticipar la creación de recursos, y generar documentación o validaciones automatizadas de forma contextual.&lt;/p&gt;

&lt;h4&gt;
  
  
  Proyecto base
&lt;/h4&gt;

&lt;p&gt;Se parte del repositorio &lt;a href="https://gitlab.com/danieljsaldana/k3s-azure-example-chatgpt" rel="noopener noreferrer"&gt;k3s-azure-example-chatgpt&lt;/a&gt;, que automatiza el despliegue de una máquina virtual en Azure con la instalación automática de un clúster Kubernetes ligero mediante k3s. El repositorio se organiza en tres niveles principales:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Infraestructura como código (&lt;code&gt;Iac/&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;Contiene los módulos y archivos de Terraform necesarios para crear la infraestructura en Azure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;main.tf&lt;/code&gt;: definición de red, VM, IP pública, grupo de seguridad y demás recursos.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;variables.tf&lt;/code&gt;: variables expuestas para personalizar despliegues.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform.tfvars&lt;/code&gt;: valores concretos definidos por el usuario.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputs.tf&lt;/code&gt;: muestra la IP pública y otros outputs útiles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con ChatGPT es posible interpretar estos archivos y devolver un resumen legible que explique:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qué recursos se van a crear.&lt;/li&gt;
&lt;li&gt;Cómo se relacionan entre sí.&lt;/li&gt;
&lt;li&gt;Qué configuraciones sensibles o críticas se deben revisar.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Instalación de k3s (&lt;code&gt;Config/&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;Esta capa utiliza Ansible para conectarse a la VM desplegada y automatizar la instalación de k3s.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;inventory.ini&lt;/code&gt;: definición de los hosts objetivo.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;playbook.yml&lt;/code&gt;: pasos para instalar k3s y configurar el nodo como maestro.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con ayuda de ChatGPT, se puede entender el flujo de ejecución del playbook y anticipar qué configuraciones se aplican al clúster, cómo se gestionan los certificados y qué ajustes deben realizarse para exponer el servidor fuera de la red local.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Configuración del clúster (&lt;code&gt;Requirements/&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;Contiene manifiestos Kubernetes predefinidos para:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configurar &lt;code&gt;cert-manager&lt;/code&gt; con Let's Encrypt (staging y producción).&lt;/li&gt;
&lt;li&gt;Instalar Ingress Controllers como Traefik o Kong.&lt;/li&gt;
&lt;li&gt;Desplegar herramientas de gestión como ArgoCD y Kubernetes Dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El análisis asistido permite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detectar errores comunes en los manifiestos.&lt;/li&gt;
&lt;li&gt;Verificar buenas prácticas (TLS, namespaces, anotaciones).&lt;/li&gt;
&lt;li&gt;Sugerir configuraciones más robustas o seguras.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Casos de uso
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Revisión técnica de cambios en infraestructura antes de ejecutar &lt;code&gt;terraform apply&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Soporte para desarrolladores que consumen IaC sin conocer profundamente el proveedor (Azure en este caso).&lt;/li&gt;
&lt;li&gt;Generación de documentación viva para equipos DevOps.&lt;/li&gt;
&lt;li&gt;Mejora del onboarding técnico en proyectos de infraestructura.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Limitaciones
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Este enfoque no reemplaza validaciones estáticas ni pruebas de integración.&lt;/li&gt;
&lt;li&gt;Requiere una base de código ordenada y bien estructurada para obtener buenos resultados.&lt;/li&gt;
&lt;li&gt;La precisión depende del contexto disponible en los archivos; no tiene visibilidad sobre el estado real de la nube.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Conclusión
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyffgzwscrs9vsuln8je0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyffgzwscrs9vsuln8je0.png" alt="Image description" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;La integración de ChatGPT en flujos de trabajo de infraestructura como código abre nuevas posibilidades para mejorar la comprensión y trazabilidad del código. Al proporcionar explicaciones automáticas y validaciones semánticas, se facilita el trabajo en equipo y se reduce la curva de aprendizaje para nuevos integrantes. La prueba de concepto demuestra que herramientas como ChatGPT pueden formar parte del flujo técnico sin invadirlo, mejorando la comprensión y la trazabilidad del código desde una perspectiva mucho más humana. Esto demuestra que la inteligencia artificial puede ser un aliado poderoso en la gestión de infraestructuras complejas, permitiendo a los equipos enfocarse en tareas de mayor valor añadido.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>chatgtp</category>
      <category>terraform</category>
      <category>azure</category>
    </item>
    <item>
      <title>Cómo Goliat - Shield protege la API pública de Goliat - Dashboard con IA, tokens e inteligencia geográfica</title>
      <dc:creator>Daniel J. Saldaña</dc:creator>
      <pubDate>Fri, 16 May 2025 11:32:17 +0000</pubDate>
      <link>https://forem.com/danieljsaldana/como-goliat-shield-protege-la-api-publica-de-goliat-dashboard-con-ia-tokens-e-inteligencia-4el7</link>
      <guid>https://forem.com/danieljsaldana/como-goliat-shield-protege-la-api-publica-de-goliat-dashboard-con-ia-tokens-e-inteligencia-4el7</guid>
      <description>&lt;p&gt;Desde hace meses estoy desarrollando un proyecto personal que me está permitiendo aprender, crear y explorar nuevas soluciones de forma libre: &lt;strong&gt;Goliat - Dashboard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Una plataforma pensada para unificar &lt;strong&gt;DevOps, FinOps y seguridad cloud&lt;/strong&gt;, con visibilidad completa sobre entornos multicloud, repositorios, costes, despliegues y más. Pero hay una capa que me ha permitido dar un salto importante en este proyecto: la &lt;strong&gt;seguridad&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Presentamos Goliat - Shield
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Goliat - Shield&lt;/strong&gt; es el componente encargado de proteger la API pública de &lt;strong&gt;Goliat - Dashboard&lt;/strong&gt;, y está diseñado desde el principio para combinar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Seguridad práctica y efectiva&lt;/li&gt;
&lt;li&gt;Visibilidad clara para los equipos&lt;/li&gt;
&lt;li&gt;Capacidades inteligentes a través de IA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Este módulo no es una idea más: nace de la necesidad real de &lt;strong&gt;asegurar APIs públicas&lt;/strong&gt; en productos que requieren apertura sin sacrificar control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aprendizajes con IA aplicada a seguridad
&lt;/h2&gt;

&lt;p&gt;Uno de los aprendizajes más profundos en este proceso ha sido &lt;strong&gt;diseñar e integrar inteligencia artificial en una solución real, enfocada 100 % a la seguridad de APIs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Goliat - Shield&lt;/strong&gt; no solo restringe accesos: &lt;strong&gt;analiza, detecta, y aprende&lt;/strong&gt; del comportamiento de los consumidores de la API pública. A través de IA, el sistema puede identificar patrones anómalos, comportamientos fuera de lo común o incluso potenciales amenazas, alertando de forma contextual.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué funcionalidades incluye Goliat - Shield?
&lt;/h2&gt;

&lt;p&gt;Estas son algunas de las características clave del módulo de protección:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Autenticación por token&lt;/strong&gt;: controla el acceso a la API mediante claves únicas y seguras.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control de acceso por IP y país&lt;/strong&gt;: define reglas de acceso a nivel geográfico, bloqueando solicitudes no autorizadas desde orígenes no permitidos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Análisis de logs en tiempo real&lt;/strong&gt;: explora eventos de acceso con filtros por token, país o fecha.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agente inteligente con IA&lt;/strong&gt;: interpreta patrones de uso y detecta anomalías sin intervención manual.
Gestión visual desde el dashboard: pensado para facilitar la vida a los equipos de seguridad.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Todo esto está diseñado para ofrecer &lt;strong&gt;seguridad real&lt;/strong&gt;, no solo desde la teoría, sino desde lo que realmente se necesita en producción.&lt;/p&gt;

&lt;h2&gt;
  
  
  Construido con tecnologías que funcionan
&lt;/h2&gt;

&lt;p&gt;Este módulo está construido con una combinación de tecnologías y enfoques que, a lo largo de mi carrera, he comprobado que funcionan de verdad:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI&lt;/strong&gt; para análisis inteligente y detección de patrones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure y AWS&lt;/strong&gt; como entornos de despliegue y origen de datos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Astro + React&lt;/strong&gt; para un frontend limpio, modular y rápido&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RBAC + validaciones a nivel API&lt;/strong&gt; para control granular de accesos&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mira cómo funciona Goliat - Shield
&lt;/h2&gt;

&lt;p&gt;En este video comparto una demo del funcionamiento de **Goliat - Shield **desde dentro:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visualización y filtrado de logs de acceso&lt;/li&gt;
&lt;li&gt;Validación por país y direcciones IP&lt;/li&gt;
&lt;li&gt;Gestión de tokens activos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/zO3Ke-2OJcw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto es solo el principio
&lt;/h2&gt;

&lt;p&gt;Desarrollar &lt;strong&gt;Goliat - Shield&lt;/strong&gt; ha sido una experiencia &lt;strong&gt;emocionante y reveladora&lt;/strong&gt;. Me ha permitido consolidar conocimientos, explorar nuevos enfoques con IA aplicada a seguridad, y sobre todo construir algo que &lt;strong&gt;no solo protege, sino que aporta valor real al equipo de seguridad&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Goliat - Dashboard&lt;/strong&gt; sigue creciendo, y con él sus capacidades de seguridad.&lt;br&gt;
Y lo mejor: aún queda mucho por construir.&lt;/p&gt;

&lt;p&gt;Recordar que podeis seguir el proyecto en su sitio web &lt;a href="https://goliat-dashboard.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Goliat - Dashboard&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>devops</category>
      <category>goliatdashboard</category>
    </item>
    <item>
      <title>Goliat Dashboard: Conoce los avances del proyecto que revolucionará la gestión de tu infraestructura</title>
      <dc:creator>Daniel J. Saldaña</dc:creator>
      <pubDate>Tue, 13 May 2025 09:24:05 +0000</pubDate>
      <link>https://forem.com/danieljsaldana/goliat-dashboard-conoce-los-avances-del-proyecto-que-revolucionara-la-gestion-de-tu-infraestructura-4h1c</link>
      <guid>https://forem.com/danieljsaldana/goliat-dashboard-conoce-los-avances-del-proyecto-que-revolucionara-la-gestion-de-tu-infraestructura-4h1c</guid>
      <description>&lt;p&gt;En los últimos meses, he estado trabajando intensamente en &lt;strong&gt;Goliat Dashboard&lt;/strong&gt;, una solución avanzada que busca facilitar la gestión, monitorización y optimización de recursos, workflows y costes en entornos GitHub. Hoy quiero compartirte en detalle los avances realizados y el impacto positivo que puede tener en la gestión cotidiana de tu infraestructura.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;¿Qué hace diferente a Goliat Dashboard?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;El objetivo central de &lt;strong&gt;Goliat Dashboard&lt;/strong&gt; es simplificar tareas complejas relacionadas con la administración técnica y financiera de infraestructuras cloud y repositorios de código. Más allá de ser un simple panel informativo, es una herramienta integral diseñada para optimizar procesos, reducir costes y aumentar la eficiencia operativa mediante inteligencia aplicada y automatización avanzada.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Funcionalidades destacadas y cómo pueden ayudarte&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gestión efectiva de Issues y Pull Requests&lt;/strong&gt;&lt;br&gt;
Goliat Dashboard proporciona una visión clara y detallada de todos los Issues y Pull Requests activos en tus proyectos. Permite filtrar, ordenar y analizar rápidamente la actividad por repositorio u organización, facilitando la detección de cuellos de botella, agilizando revisiones y mejorando significativamente los tiempos de respuesta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Control dinámico de Workflows CI/CD&lt;/strong&gt;&lt;br&gt;
Una de sus funcionalidades clave es la posibilidad de ejecutar y controlar directamente workflows desde el propio Dashboard. Puedes revisar en tiempo real los logs de ejecución, verificar estados, gestionar artefactos generados, y tomar acciones rápidas sin salir del panel. Esto acelera notablemente los ciclos de integración continua y entrega continua (CI/CD), reduciendo tiempos muertos y optimizando procesos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gestión financiera cloud simplificada (FinOps)&lt;/strong&gt;&lt;br&gt;
El Dashboard permite visualizar y controlar en tiempo real el gasto de tus infraestructuras desplegadas en la nube (actualmente Azure y pronto AWS). Puedes definir presupuestos dinámicos por proyecto y recibir alertas visuales inmediatas cuando te acerques a límites preestablecidos, facilitando así un control financiero más proactivo y preciso.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Administración segura de variables y secretos&lt;/strong&gt;&lt;br&gt;
La seguridad es crucial en cualquier proyecto. Con Goliat Dashboard gestionas de forma centralizada y segura todas las variables de entorno y secretos asociados a tus repositorios. Además, la plataforma sincroniza automáticamente estos datos con GitHub, lo que simplifica enormemente la gestión y reduce riesgos operativos y de seguridad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recomendaciones inteligentes (Advisor)&lt;/strong&gt;&lt;br&gt;
Una de las funcionalidades más valiosas es su sistema inteligente de recomendaciones. Goliat Dashboard analiza continuamente tu infraestructura cloud y proporciona sugerencias específicas para mejorar aspectos clave como alta disponibilidad, seguridad, rendimiento o costes. Esto se traduce en una gestión más eficiente, segura y rentable de tus recursos en la nube.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diario de actividad para trazabilidad completa&lt;/strong&gt;&lt;br&gt;
La plataforma registra detalladamente todas las acciones realizadas. Esto incluye modificaciones en configuraciones, workflows ejecutados, cambios en variables o secretos, y otras operaciones clave. Este diario integrado es una herramienta indispensable para auditorías internas, revisiones de seguridad y cumplimiento normativo (compliance).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Colaboración eficiente y control basado en roles (RBAC)&lt;/strong&gt;&lt;br&gt;
Facilita la colaboración segura entre diferentes equipos mediante anotaciones colaborativas directamente en cada repositorio. Además, implementa un sistema robusto de control de acceso basado en roles (RBAC), permitiendo definir con precisión qué usuarios o equipos tienen acceso a cada parte de la plataforma, asegurando la integridad y confidencialidad de los datos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agente Inteligente: asistencia proactiva y análisis avanzado&lt;/strong&gt;&lt;br&gt;
Una de las innovaciones más destacadas de Goliat Dashboard es el Agente Inteligente integrado, una herramienta interactiva que responde en tiempo real a consultas complejas sobre tu infraestructura. Puedes pedirle informes personalizados, estadísticas detalladas, análisis predictivos sobre costes futuros, o información específica sobre eventos pasados, simplificando notablemente la toma de decisiones.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/IfE9uAD4sVY"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;¿Qué viene próximamente?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Estamos trabajando para incorporar próximamente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integración completa con GitLab.&lt;/li&gt;
&lt;li&gt;Mejoras adicionales al Agente Inteligente para automatizar aún más tareas operativas.&lt;/li&gt;
&lt;li&gt;Nuevas capacidades predictivas en el control presupuestario cloud.&lt;/li&gt;
&lt;li&gt;Ampliación de funcionalidades de la API pública para integraciones externas avanzadas.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Próximamente disponible&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Actualmente, Goliat Dashboard sigue en desarrollo activo, en fases avanzadas de pruebas y mejoras continuas para asegurar que, desde su lanzamiento, cumpla con las expectativas más exigentes.&lt;/p&gt;

&lt;p&gt;Recordar que podeis seguir el proyecto en su sitio web &lt;a href="https://goliat-dashboard.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Goliat - Dashboard&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;¡Te invitamos a estar atento a las próximas novedades!&lt;/p&gt;

&lt;p&gt;Tu feedback es esencial para nosotros, así que cualquier sugerencia o pregunta será más que bienvenida.&lt;/p&gt;

&lt;p&gt;¡Muchas gracias por seguir el proyecto! &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>goliatdashboard</category>
      <category>devops</category>
    </item>
    <item>
      <title>Integración con Azure Advisor, Azure Service Health y próximos pasos</title>
      <dc:creator>Daniel J. Saldaña</dc:creator>
      <pubDate>Wed, 23 Apr 2025 07:06:29 +0000</pubDate>
      <link>https://forem.com/danieljsaldana/integracion-con-azure-advisor-azure-service-health-y-proximos-pasos-1lhk</link>
      <guid>https://forem.com/danieljsaldana/integracion-con-azure-advisor-azure-service-health-y-proximos-pasos-1lhk</guid>
      <description>&lt;p&gt;¡Hola a todos! Hoy quiero compartir contigo los avances más recientes que he realizado en &lt;strong&gt;Goliat - Dashboard&lt;/strong&gt;, además de mostrarte algunas ideas clave para las próximas versiones, especialmente orientadas a la gestión de proyectos multicloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nuevas Integraciones Disponibles: Azure Advisor y Azure Service Health
&lt;/h2&gt;

&lt;p&gt;Ya puedes disfrutar de dos integraciones clave en el Dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Azure Advisor:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Desde ahora puedes consultar directamente en &lt;strong&gt;Goliat - Dashboard&lt;/strong&gt; todas las recomendaciones proporcionadas por Azure Advisor, identificando fácilmente el impacto en tus recursos, filtrando por categoría o tipo de recurso, y actuando rápidamente para optimizar costes, rendimiento y seguridad.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdacgap68u5nc07roskzk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdacgap68u5nc07roskzk.png" alt="Azure Advisor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure Service Health:&lt;/strong&gt;
También he incorporado la capacidad de monitorizar en tiempo real el estado de los servicios desplegados en Azure, identificando fácilmente si existe alguna afectación en tus recursos. Podrás ver claramente si se trata de incidentes, mantenimientos programados o acciones requeridas, permitiéndote reaccionar con mayor agilidad.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fner1iy2heqfos0dbqt85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fner1iy2heqfos0dbqt85.png" alt="Azure Service Health"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Aquí tienes un breve vídeo donde explico en detalle estas funcionalidades:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/tFrHZNk8B24"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Mejoras visuales y experiencia del usuario&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Además, he optimizado el diseño y distribución del menú, agrupando claramente las métricas relacionadas con &lt;strong&gt;Issues&lt;/strong&gt;, &lt;strong&gt;Pull Requests&lt;/strong&gt; y &lt;strong&gt;Actions&lt;/strong&gt;, e implementado mejoras visuales para evitar espacios en blanco, ofreciendo una interfaz más intuitiva y limpia.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Próximos pasos: Gestión de proyectos multicloud&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Uno de los objetivos principales para las próximas versiones es permitir gestionar proyectos que involucren múltiples organizaciones y proveedores desde un mismo lugar.&lt;/p&gt;

&lt;p&gt;Mi idea principal es la siguiente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Proyectos multiorganización y multiproveedor:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Podrás crear proyectos que engloben diferentes organizaciones o repositorios independientemente del proveedor cloud (&lt;strong&gt;AWS&lt;/strong&gt; o &lt;strong&gt;Azure&lt;/strong&gt;, por ejemplo). Cada proyecto podría contener diversas aplicaciones desplegadas en múltiples proveedores y organizaciones, centralizando así la información, recomendaciones, métricas y estados directamente en &lt;strong&gt;Goliat - Dashboard&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sincronización mediante Local Storage:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Implementaré un sistema en Local Storage que permitirá definir repositorios y organizaciones específicas. Al seleccionar un repositorio u organización, esta configuración se sincronizará automáticamente entre todos los controladores del Dashboard. Así tendrás siempre una experiencia fluida y consistente, sin necesidad de repetir configuraciones manuales.&lt;/p&gt;

&lt;p&gt;Este enfoque facilitará enormemente el seguimiento y la explotación de métricas por proyecto, organización o proveedor cloud, convirtiendo &lt;strong&gt;Goliat - Dashboard&lt;/strong&gt; en una herramienta integral de gestión multicloud.&lt;/p&gt;

&lt;p&gt;¡Próximamente tendrás más novedades sobre estos avances!&lt;/p&gt;

&lt;p&gt;Espero tus comentarios y sugerencias para seguir avanzando juntos.¡Muchas gracias por apoyar el desarrollo de &lt;strong&gt;Goliat - Dashboard&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;Recuerda que puedes seguir el proyecto en:&lt;/p&gt;

&lt;p&gt;🌐 Proyecto: &lt;a href="http://goliat-dashboard.com" rel="noopener noreferrer"&gt;http://goliat-dashboard.com&lt;/a&gt;&lt;br&gt;
🚀 Demo: &lt;a href="http://demo.goliat-dashboard.com" rel="noopener noreferrer"&gt;http://demo.goliat-dashboard.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>goliatdashboard</category>
      <category>webdev</category>
      <category>devops</category>
      <category>azure</category>
    </item>
    <item>
      <title>Descubre cómo el Agente de Arquitectura expone recomendaciones de Azure Advisor</title>
      <dc:creator>Daniel J. Saldaña</dc:creator>
      <pubDate>Tue, 08 Apr 2025 07:16:49 +0000</pubDate>
      <link>https://forem.com/danieljsaldana/descubre-como-el-agente-de-arquitectura-expone-recomendaciones-de-azure-advisor-55e9</link>
      <guid>https://forem.com/danieljsaldana/descubre-como-el-agente-de-arquitectura-expone-recomendaciones-de-azure-advisor-55e9</guid>
      <description>&lt;p&gt;&lt;strong&gt;Goliat Dashboard&lt;/strong&gt; sigue evolucionando para hacer que la gestión de tus recursos en la nube sea cada vez más eficiente. Hoy te presentamos una funcionalidad del Agente de Arquitectura que se conecta con &lt;strong&gt;Azure Advisor&lt;/strong&gt; para mostrar sugerencias clave de manera centralizada. Con esta integración, tu equipo recibe las recomendaciones específicas que afectan a cada proyecto, sin necesidad de acceder al portal de Azure ni filtrar manualmente cada recurso.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿En qué consiste esta funcionalidad?
&lt;/h2&gt;

&lt;p&gt;El Agente de Arquitectura de &lt;strong&gt;Goliat Dashboard&lt;/strong&gt; solicita a &lt;strong&gt;Azure Advisor&lt;/strong&gt; las recomendaciones que guardan relación con el proyecto o repositorio de la organización. Eso quiere decir que, en un solo lugar, obtendrás avisos sobre mejoras de alta disponibilidad, seguridad o escalabilidad, cuidadosamente filtrados para enfocarse solo en los recursos que realmente importan a ese proyecto en concreto.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Cómo ayuda a solventar problemas reales?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Centralización de información&lt;/strong&gt;&lt;br&gt;
Sin tener que navegar entre portales externos o herramientas especializadas, cualquiera en el equipo puede ver de un vistazo qué necesita ajustes o mejoras, desde máquinas virtuales hasta servicios de contenedor o almacenamiento. Esto ahorra tiempo y reduce la complejidad en la supervisión diaria.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recomendaciones relevantes y filtradas&lt;/strong&gt;&lt;br&gt;
Al concentrarse solo en las recomendaciones de recursos que pertenecen al proyecto consultado, se evitan datos genéricos que podrían dispersar la atención del equipo. Así, se pueden priorizar aquellas acciones que tengan más impacto o urgencia.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Control de datos sensibles&lt;/strong&gt;&lt;br&gt;
La organización decide qué información está autorizada a procesar y mostrar la IA. De esta forma, se garantiza la confidencialidad y se cumplen con las directrices de seguridad internas sin sacrificar la utilidad de las recomendaciones.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Toma de decisiones más rápida&lt;/strong&gt;&lt;br&gt;
Al visualizar de forma clara qué recursos necesitan migrar a una máquina virtual más potente, habilitar NAT gateway o mejorar sus discos, el equipo puede actuar de inmediato, evitando riesgos y optimizando el desempeño de la infraestructura en la nube.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Con esta nueva funcionalidad de &lt;strong&gt;Goliat Dashboard&lt;/strong&gt;, el Agente de Arquitectura expone de manera sencilla las recomendaciones de &lt;strong&gt;Azure Advisor&lt;/strong&gt; que realmente importan para tu proyecto. El resultado es una &lt;strong&gt;experiencia más ágil&lt;/strong&gt;, en la que tanto perfiles técnicos como de negocio obtienen la información necesaria para tomar decisiones fundamentadas, sin perder tiempo en configuraciones manuales ni exponer datos que no sean estrictamente necesarios.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/3GZUa23431I"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Recuerda que puedes seguir el proyecto en:&lt;/p&gt;

&lt;p&gt;🌐 Proyecto: &lt;a href="http://goliat-dashboard.com" rel="noopener noreferrer"&gt;http://goliat-dashboard.com&lt;/a&gt;&lt;br&gt;
🚀 Demo: &lt;a href="http://demo.goliat-dashboard.com" rel="noopener noreferrer"&gt;http://demo.goliat-dashboard.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>goliatdashboard</category>
      <category>azure</category>
      <category>advisor</category>
    </item>
  </channel>
</rss>
