<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Google Cloud on inetshell</title><link>https://inet.sh/es/posts/gcp/</link><description>Recent content in Google Cloud on inetshell</description><generator>Hugo</generator><language>es</language><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/es/posts/gcp/index.xml" rel="self" type="application/rss+xml"/><item><title>Certificado gestionado de Google que nunca se emite: CAA (pki.goog, letsencrypt.org), FAILED_NOT_VISIBLE y el orden correcto</title><link>https://inet.sh/es/posts/gcp/certificado-gestionado-google-caa/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/gcp/certificado-gestionado-google-caa/</guid><description>El certificado gestionado de Google para un balanceador HTTPS lleva horas en PROVISIONING o cae a FAILED_NOT_VISIBLE o FAILED_CAA_FORBIDDEN. Las tres causas: el DNS todavía no apunta a la IP del balanceador, el certificado no está adjunto a un target proxy, o un registro CAA de la zona no autoriza a pki.goog ni a letsencrypt.org. Cómo comprobar el estado con gcloud, el CAA correcto, y Certificate Manager con autorización DNS para emitir antes de cambiar el DNS.</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">example.com.   CAA 0 issue &#34;pki.goog&#34;
</span></span><span class="line"><span class="cl">example.com.   CAA 0 issue &#34;letsencrypt.org&#34;
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">gcloud compute ssl-certificates describe web-cert --global --format<span class="o">=</span><span class="s1">&#39;yaml(managed)&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># managed:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   domainStatus:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#     www.example.com: PROVISIONING | ACTIVE | FAILED_NOT_VISIBLE | FAILED_CAA_FORBIDDEN | FAILED_CAA_CHECKING | FAILED_RATE_LIMITED</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   status: PROVISIONING | ACTIVE</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Para que pase a <code>ACTIVE</code> tienen que cumplirse tres cosas a la vez: el <strong>A/AAAA</strong> del dominio apunta a la IP del balanceador (y a nada más), el certificado está <strong>adjunto a un target proxy</strong> con su forwarding rule, y ningún <strong>CAA</strong> de la zona prohíbe a las CAs de Google. Después, hasta <strong>60 minutos</strong>.</p>
<h2 id="por-qué">Por qué</h2>
<p>Google emite los certificados gestionados con <strong>Google Trust Services</strong> (<code>pki.goog</code>) o con <strong>Let&rsquo;s Encrypt</strong>, y elige una u otra en cada emisión o renovación. Para validar el dominio hace una comprobación HTTP contra la IP a la que resuelve el nombre, así que hasta que el DNS apunte al balanceador no hay nada que validar. Y como cualquier CA, respeta los registros <strong>CAA</strong>: si tu zona dice que solo <code>digicert.com</code> puede emitir (política corporativa típica), Google no puede.</p>
<p>Los estados lo cuentan:</p>
<table>
  <thead>
      <tr>
          <th><code>domainStatus</code></th>
          <th>Causa</th>
          <th>Qué hacer</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>PROVISIONING</code></td>
          <td>En curso</td>
          <td>Esperar; si pasa de 60 minutos, revisar lo de abajo</td>
      </tr>
      <tr>
          <td><code>FAILED_NOT_VISIBLE</code></td>
          <td>El nombre no resuelve a la IP del balanceador, o el certificado no está adjunto a un target proxy</td>
          <td>Corregir A/AAAA, adjuntar el certificado, esperar la propagación</td>
      </tr>
      <tr>
          <td><code>FAILED_CAA_CHECKING</code></td>
          <td>Google no pudo leer los CAA (DNS que no responde, DNSSEC roto)</td>
          <td>Arreglar la zona</td>
      </tr>
      <tr>
          <td><code>FAILED_CAA_FORBIDDEN</code></td>
          <td>Hay CAA y no incluye a <code>pki.goog</code> ni a <code>letsencrypt.org</code></td>
          <td>Añadir los dos registros</td>
      </tr>
      <tr>
          <td><code>FAILED_RATE_LIMITED</code></td>
          <td>Demasiados intentos de emisión</td>
          <td>Esperar; no recrear el certificado en bucle</td>
      </tr>
  </tbody>
</table>
<h2 id="el-caa-correcto">El CAA correcto</h2>
<p>Sin ningún registro CAA, cualquier CA puede emitir y Google funciona. Si tienes CAA (o tu organización lo exige), añade <strong>las dos</strong> CAs:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">example.com.   CAA 0 issue &#34;pki.goog&#34;
</span></span><span class="line"><span class="cl">example.com.   CAA 0 issue &#34;letsencrypt.org&#34;
</span></span></code></pre></td></tr></table>
</div>
</div><p>Poner solo una obliga a Google a usar siempre esa; la documentación lo desaconseja porque una renovación puede salir por la otra. Los CAA se heredan hacia abajo: un CAA en <code>example.com</code> cubre <code>www.example.com</code> si el subdominio no tiene el suyo.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">dig +short CAA example.com
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="certificate-manager-emitir-antes-de-cambiar-el-dns">Certificate Manager: emitir antes de cambiar el DNS</h2>
<p>Con los certificados clásicos (<code>compute ssl-certificates</code>) el dominio tiene que apuntar ya al balanceador, lo que en una migración significa unos minutos de HTTPS roto. <strong>Certificate Manager</strong> resuelve eso con <strong>autorización DNS</strong>: publicas un CNAME de validación y Google emite el certificado mientras el tráfico sigue en el sitio viejo.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">gcloud certificate-manager dns-authorizations create www-auth --domain<span class="o">=</span>www.example.com
</span></span><span class="line"><span class="cl">gcloud certificate-manager dns-authorizations describe www-auth --format<span class="o">=</span><span class="s1">&#39;yaml(dnsResourceRecord)&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># dnsResourceRecord: name: _acme-challenge.www.example.com. type: CNAME data: xxxx.authorize.certificatemanager.goog.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># publica ese CNAME en tu zona y luego:</span>
</span></span><span class="line"><span class="cl">gcloud certificate-manager certificates create www-cert <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --domains<span class="o">=</span>www.example.com --dns-authorizations<span class="o">=</span>www-auth
</span></span><span class="line"><span class="cl">gcloud certificate-manager maps create web-map
</span></span><span class="line"><span class="cl">gcloud certificate-manager maps entries create www --map<span class="o">=</span>web-map --certificate<span class="o">=</span>www-cert --hostname<span class="o">=</span>www.example.com
</span></span><span class="line"><span class="cl">gcloud compute target-https-proxies update web-proxy --certificate-map<span class="o">=</span>web-map
</span></span></code></pre></td></tr></table>
</div>
</div><p>Cuando el certificado esté <code>ACTIVE</code>, cambias el A/AAAA y el corte es cero. Los CAA aplican igual.</p>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong>Cloudflare u otro proxy delante</strong> del balanceador: el A apunta al proxy, no a Google, y la validación falla con <code>FAILED_NOT_VISIBLE</code>. Nube gris durante la emisión, o Certificate Manager con autorización DNS.</li>
<li><strong>Un certificado gestionado no adjunto a nada no se emite.</strong> Crearlo y esperar no hace nada; adjúntalo al target proxy primero.</li>
<li><strong>Cambiar la lista de dominios</strong> crea un certificado nuevo: adjunta el nuevo, espera <code>ACTIVE</code>, y solo entonces quita el viejo del proxy, o tendrás un hueco.</li>
<li><strong>Wildcards</strong> solo con Certificate Manager y autorización DNS; los certificados gestionados clásicos no los admiten.</li>
<li><strong>DNSSEC mal firmado</strong> rompe la lectura de CAA: <code>FAILED_CAA_CHECKING</code> aunque no tengas ningún CAA.</li>
</ul>
<p>Referencia: <a href="https://docs.cloud.google.com/load-balancing/docs/ssl-certificates/google-managed-certs">Google-managed SSL certificates</a> y <a href="https://docs.cloud.google.com/certificate-manager/docs/overview">Certificate Manager</a> en la documentación de Google Cloud.</p>
]]></content:encoded></item><item><title>Cloud VPN de GCP no levanta: leer los logs IKE (peer not responding, traffic selectors narrowed) y el BGP con pfSense</title><link>https://inet.sh/es/posts/gcp/cloud-vpn-no-levanta-logs-ike/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/gcp/cloud-vpn-no-levanta-logs-ike/</guid><description>Cuando un túnel de Cloud VPN se queda en &amp;#39;No incoming packets&amp;#39; o sube y baja, la respuesta está en Cloud Logging: las consultas por fase (IKE_SA, CHILD_SA, rekey, DELETE) y qué significa cada mensaje: peer not responding, traffic selectors narrowed, received DELETE. Más el caso de HA VPN con BGP contra pfSense/FRR, donde las rutas no entran hasta que añades route maps.</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</h2>
<p>En <strong>Logs Explorer</strong>, cada fase del túnel tiene su consulta:</p>
<table>
  <thead>
      <tr>
          <th>Qué buscas</th>
          <th>Consulta</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cloud VPN no llega al peer</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; &quot;establishing IKE_SA failed, peer not responding&quot;</code></td>
      </tr>
      <tr>
          <td>Fase 1 arranca</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; (&quot;initiating IKE_SA&quot; OR &quot;generating IKE_SA_INIT request&quot;)</code></td>
      </tr>
      <tr>
          <td>Autenticación IKE</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; (&quot;generating IKE_AUTH request&quot; OR &quot;parsed IKE_AUTH response&quot;)</code></td>
      </tr>
      <tr>
          <td>PSK aceptada</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; (&quot;authentication of&quot; AND &quot;with pre-shared key successful&quot;)</code></td>
      </tr>
      <tr>
          <td>Fase 1 establecida</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; (&quot;IKE_SA&quot; AND &quot;established between&quot;)</code></td>
      </tr>
      <tr>
          <td>Todo lo de fase 2 (incluye rekeys)</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; &quot;CHILD_SA&quot;</code></td>
      </tr>
      <tr>
          <td>Las subredes no coinciden</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; (&quot;Remote traffic selectors narrowed&quot; OR &quot;Local traffic selectors narrowed&quot;)</code></td>
      </tr>
      <tr>
          <td>Alguien cierra la fase 2</td>
          <td><code>resource.type=&quot;vpn_gateway&quot; (&quot;received DELETE for ESP CHILD_SA&quot; OR &quot;sending DELETE for ESP CHILD_SA&quot;)</code></td>
      </tr>
  </tbody>
</table>
<p>Cloud VPN corre strongSwan por debajo, así que los mensajes son los de strongSwan: si conoces <code>charon</code>, ya sabes leerlos.</p>
<h2 id="qué-significa-cada-mensaje">Qué significa cada mensaje</h2>
<p><strong><code>establishing IKE_SA failed, peer not responding</code>.</strong> Cloud VPN manda IKE_SA_INIT y nadie contesta. En orden de probabilidad: el peer está en IKEv1 y el túnel en IKEv2 (o al revés), la IP del peer configurada no es la pública real (NAT delante del firewall), UDP 500 y 4500 no llegan al peer, o el peer ni siquiera tiene el túnel activo. La PSK <strong>no</strong> produce este mensaje: con PSK incorrecta la fase 1 arranca y falla en <code>IKE_AUTH</code>.</p>
<p><strong><code>IKE_SA … established between</code> y después nada.</strong> Fase 1 bien, fase 2 no. Busca <code>CHILD_SA</code> y <code>traffic selectors</code>.</p>
<p><strong><code>Remote traffic selectors narrowed</code> / <code>Local traffic selectors narrowed</code>.</strong> Las redes de fase 2 no coinciden en los dos lados. En HA VPN (y en Classic VPN basada en rutas) Cloud VPN propone <strong><code>0.0.0.0/0</code> a ambos lados</strong> y espera lo mismo; un peer <em>policy-based</em> con subredes concretas hace que strongSwan estreche los selectores y solo pase tráfico entre lo que casó, o nada. En el peer: fase 2 con <code>0.0.0.0/0</code> local y remoto (en pfSense, modo <em>Routed</em>, que crea una interfaz de túnel <code>ipsecN</code>), y las rutas se resuelven por BGP o estáticas.</p>
<p><strong><code>received DELETE for ESP CHILD_SA</code> en bucle.</strong> El peer tumba la fase 2 cada pocos minutos: lifetimes de fase 2 distintos con rekey mal negociado, o DPD agresivo. Iguala lifetimes (Cloud VPN usa 3 horas en fase 2 y 10 en fase 1) y deja que el rekey lo inicie un solo lado.</p>
<p><strong>Sube, pasa tráfico un rato y muere con NAT-T.</strong> Un peer detrás de NAT con UDP 4500 y un middlebox que &ldquo;ayuda&rdquo; (ALG de IPsec en el router) descarta paquetes ESP encapsulados. Desactiva el ALG o pasa el firewall a IP pública directa.</p>
<h2 id="ha-vpn-con-bgp-contra-pfsense-frr">HA VPN con BGP contra pfSense (FRR)</h2>
<p>El túnel sube, <code>bgp</code> se establece, y <strong>ninguna ruta entra ni sale</strong>. FRR en pfSense arranca con una política que rechaza todo lo que no esté permitido explícitamente:</p>
<ol>
<li><strong>Services → FRR → BGP → Route Maps</strong>: crea <code>ALLOW-ALL</code>, <em>Action</em> Permit, <em>Sequence</em> 100, sin criterios.</li>
<li><strong>BGP → Neighbors</strong> → el peer de Cloud Router: <em>Remote AS</em> el ASN del Cloud Router, y en <em>Peer Filtering</em> asigna <code>ALLOW-ALL</code> como route map <strong>in</strong> y <strong>out</strong>.</li>
<li><strong>BGP → Network Distribution</strong>: lista las redes locales que quieres anunciar (o <em>Default Originate</em> si todo va por el túnel).</li>
<li>En GCP, el Cloud Router con <code>--advertisement-mode custom</code> si quieres controlar qué prefijos salen; por defecto anuncia las subredes de la VPC.</li>
</ol>
<p>Sin el paso 2, <code>vtysh -c &quot;show ip bgp summary&quot;</code> muestra la sesión <em>Established</em> con 0 prefijos en ambas direcciones, que es exactamente el síntoma.</p>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong>IKEv2</strong> salvo que el peer no pueda: IKEv1 sigue soportado, pero sin IPv6 y con menos cifrados. Classic VPN sigue existiendo (99,9 %), pero parte de su funcionalidad está retirada; lo nuevo va en HA VPN (99,99 %).</li>
<li><strong>Dos túneles por gateway</strong> en HA VPN para el 99.99 %: si solo levantas uno, el estado del gateway queda en aviso aunque funcione.</li>
<li><strong>MTU</strong>: 1460 por defecto en la VPC de GCP; con IPsec encima, deja que TCP haga MSS clamping en el peer (pfSense lo hace en la interfaz <code>ipsecN</code> del túnel) o verás transferencias que se cuelgan tras el handshake.</li>
<li><strong>Los logs tardan</strong>: un minuto o dos entre el evento y Logs Explorer. Refresca antes de cambiar la configuración otra vez.</li>
<li><strong>PSK con caracteres especiales</strong> copiada de un gestor de contraseñas: comprueba en el peer que no se comió nada; el síntoma es <code>IKE_AUTH</code> que falla, no <em>peer not responding</em>.</li>
</ul>
<h2 id="ver-también">Ver también</h2>
<ul>
<li><a href="/es/posts/pfsense/nat-reflection-port-forward-desde-lan/">pfSense: NAT reflection y split DNS</a> — el otro clásico de &ldquo;el túnel está bien y aun así no llego&rdquo;.</li>
<li><a href="https://docs.cloud.google.com/network-connectivity/docs/vpn/support/troubleshooting">Cloud VPN troubleshooting</a> en la documentación de Google Cloud.</li>
</ul>
]]></content:encoded></item><item><title>GitHub Actions → Google Cloud sin llaves: Workload Identity Federation con gcloud y Terraform</title><link>https://inet.sh/es/posts/gcp/github-actions-workload-identity-federation/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/gcp/github-actions-workload-identity-federation/</guid><description>Autenticar un workflow de GitHub Actions en Google Cloud sin guardar una llave JSON de service account: pool y proveedor OIDC de Workload Identity Federation, attribute condition que limita a tu organización o repo, binding workloadIdentityUser, y el paso google-github-actions/auth@v3 con permissions id-token: write. La misma configuración en Terraform con el módulo gh-oidc, y los errores típicos de permisos.</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PROJECT_ID</span><span class="o">=</span>my-project
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PROJECT_NUMBER</span><span class="o">=</span><span class="k">$(</span>gcloud projects describe <span class="nv">$PROJECT_ID</span> --format<span class="o">=</span><span class="s1">&#39;value(projectNumber)&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">GITHUB_ORG</span><span class="o">=</span>my-org
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">REPO</span><span class="o">=</span>my-org/my-repo
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">gcloud iam workload-identity-pools create github <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --project<span class="o">=</span><span class="nv">$PROJECT_ID</span> --location<span class="o">=</span>global --display-name<span class="o">=</span><span class="s2">&#34;GitHub Actions&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">gcloud iam workload-identity-pools providers create-oidc github-oidc <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --project<span class="o">=</span><span class="nv">$PROJECT_ID</span> --location<span class="o">=</span>global --workload-identity-pool<span class="o">=</span>github <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --display-name<span class="o">=</span><span class="s2">&#34;GitHub OIDC&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --issuer-uri<span class="o">=</span><span class="s2">&#34;https://token.actions.githubusercontent.com&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --attribute-mapping<span class="o">=</span><span class="s2">&#34;google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --attribute-condition<span class="o">=</span><span class="s2">&#34;assertion.repository_owner == &#39;</span><span class="nv">$GITHUB_ORG</span><span class="s2">&#39;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">gcloud iam service-accounts create github-actions --project<span class="o">=</span><span class="nv">$PROJECT_ID</span> --display-name<span class="o">=</span><span class="s2">&#34;GitHub Actions&#34;</span>
</span></span><span class="line"><span class="cl">gcloud iam service-accounts add-iam-policy-binding github-actions@<span class="nv">$PROJECT_ID</span>.iam.gserviceaccount.com <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --project<span class="o">=</span><span class="nv">$PROJECT_ID</span> --role<span class="o">=</span>roles/iam.workloadIdentityUser <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --member<span class="o">=</span><span class="s2">&#34;principalSet://iam.googleapis.com/projects/</span><span class="nv">$PROJECT_NUMBER</span><span class="s2">/locations/global/workloadIdentityPools/github/attribute.repository/</span><span class="nv">$REPO</span><span class="s2">&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Y en el workflow:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write           </span><span class="w"> </span><span class="c"># sin esto GitHub no emite el token OIDC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">auth</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">google-github-actions/auth@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">workload_identity_provider</span><span class="p">:</span><span class="w"> </span><span class="l">projects/123456789012/locations/global/workloadIdentityPools/github/providers/github-oidc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">service_account</span><span class="p">:</span><span class="w"> </span><span class="l">github-actions@my-project.iam.gserviceaccount.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">google-github-actions/setup-gcloud@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">gcloud storage ls</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Los roles que necesite el pipeline (Artifact Registry, Cloud Run, GKE…) se los das a <code>github-actions@…</code> como a cualquier service account.</p>
<h2 id="por-qué">Por qué</h2>
<p>La alternativa clásica es exportar una llave JSON del service account y guardarla como secreto en GitHub. Esa llave no caduca, cualquiera con acceso al repo o a los logs puede copiarla, y rotarla es un trabajo manual que nadie hace. Con <strong>Workload Identity Federation</strong> GitHub emite un token OIDC de corta vida para cada job, Google lo verifica contra el issuer de GitHub, comprueba tu <em>attribute condition</em> y entrega credenciales temporales del service account. No hay secreto que guardar ni rotar, y puedes limitar quién entra hasta el nivel de repo o rama.</p>
<h2 id="las-piezas">Las piezas</h2>
<table>
  <thead>
      <tr>
          <th>Pieza</th>
          <th>Qué es</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Pool</strong> (<code>github</code>)</td>
          <td>Contenedor de identidades externas; uno por proveedor basta</td>
      </tr>
      <tr>
          <td><strong>Proveedor OIDC</strong> (<code>github-oidc</code>)</td>
          <td>Confía en <code>https://token.actions.githubusercontent.com</code> y mapea claims del token (<code>repository</code>, <code>repository_owner</code>, <code>actor</code>, <code>ref</code>) a atributos</td>
      </tr>
      <tr>
          <td><strong>Attribute condition</strong></td>
          <td>Filtro CEL sobre esos claims. Sin ella, <strong>cualquier repo de GitHub</strong> podría intentar entrar al pool; el README oficial pide ponerla siempre</td>
      </tr>
      <tr>
          <td><strong>Binding <code>workloadIdentityUser</code></strong></td>
          <td>Qué identidades del pool pueden hacerse pasar por el service account: <code>attribute.repository/my-org/my-repo</code>, o <code>attribute.repository_owner/my-org</code> para toda la organización</td>
      </tr>
      <tr>
          <td><strong><code>auth@v3</code></strong></td>
          <td>Cambia el token de GitHub por credenciales de Google y las deja configuradas para <code>gcloud</code>, los SDK y Terraform</td>
      </tr>
  </tbody>
</table>
<p>Para restringir a una rama, mapea el claim y úsalo en la condición y en el binding:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">--attribute-mapping=&#34;…,attribute.ref=assertion.ref&#34;
</span></span><span class="line"><span class="cl">--attribute-condition=&#34;assertion.repository_owner == &#39;my-org&#39; &amp;&amp; assertion.ref == &#39;refs/heads/main&#39;&#34;
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="con-terraform">Con Terraform</h2>
<p>El módulo oficial hace pool, proveedor y bindings en un bloque:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">module</span> <span class="s2">&#34;github_actions_sa&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  source</span>     <span class="o">=</span> <span class="s2">&#34;terraform-google-modules/service-accounts/google&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  version</span>    <span class="o">=</span> <span class="s2">&#34;~&gt; 4.0&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  project_id</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">project_id</span>
</span></span><span class="line"><span class="cl"><span class="n">  names</span>      <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;github-actions&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">  project_roles</span> <span class="o">=</span><span class="n"> [&#34;${var.project_id}</span><span class="o">=</span><span class="err">&gt;</span><span class="k">roles</span><span class="err">/</span><span class="k">artifactregistry</span><span class="p">.</span><span class="k">writer</span><span class="err">&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">module</span> <span class="s2">&#34;gh_oidc&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  source</span>              <span class="o">=</span> <span class="s2">&#34;terraform-google-modules/github-actions-runners/google//modules/gh-oidc&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  project_id</span>          <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">project_id</span>
</span></span><span class="line"><span class="cl"><span class="n">  pool_id</span>             <span class="o">=</span> <span class="s2">&#34;github&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  provider_id</span>         <span class="o">=</span> <span class="s2">&#34;github-oidc&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  attribute_condition</span> <span class="o">=</span><span class="n"> &#34;assertion.repository_owner</span> <span class="o">==</span> <span class="err">&#39;</span><span class="k">my</span><span class="err">-</span><span class="k">org</span><span class="err">&#39;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  sa_mapping</span> <span class="o">=</span> {
</span></span><span class="line"><span class="cl"><span class="n">    &#34;github-actions&#34;</span> <span class="o">=</span> {
</span></span><span class="line"><span class="cl"><span class="n">      sa_name</span>   <span class="o">=</span> <span class="s2">&#34;projects/${var.project_id}/serviceAccounts/${module.github_actions_sa.email}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">      attribute</span> <span class="o">=</span> <span class="s2">&#34;attribute.repository/my-org/my-repo&#34;</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">output &#34;workload_identity_provider&#34; { value</span> <span class="o">=</span> <span class="k">module</span><span class="p">.</span><span class="k">gh_oidc</span><span class="p">.</span><span class="k">provider_name</span> }
</span></span></code></pre></td></tr></table>
</div>
</div><p>El <code>output</code> es exactamente el valor que va en <code>workload_identity_provider</code> del workflow.</p>
<h2 id="sin-service-account">Sin service account</h2>
<p><code>auth@v3</code> también admite <strong>federación directa</strong>: el <code>principalSet</code> recibe roles IAM sin pasar por un service account (<code>workload_identity_provider</code> y <code>project_id</code>, sin <code>service_account</code>). Menos piezas, pero no todos los servicios aceptan principales federados; para Artifact Registry, GCS y la mayoría de APIs sí, y es el camino que Google recomienda para casos nuevos.</p>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong><code>id-token: write</code></strong> en <code>permissions</code> del job. Sin él, <code>auth</code> falla con <em>&ldquo;Unable to get ACTIONS_ID_TOKEN_REQUEST_URL&rdquo;</em>.</li>
<li><strong>Número de proyecto, no ID</strong>, en la ruta del proveedor y del <code>principalSet</code>: <code>projects/123456789012/…</code>.</li>
<li><strong><code>Permission 'iam.serviceAccounts.getAccessToken' denied</code></strong>: falta el binding <code>roles/iam.workloadIdentityUser</code> o el <code>attribute.repository</code> no coincide (mayúsculas y nombre exacto del repo).</li>
<li><strong>La condición debe referirse a claims mapeados</strong>: si usas <code>assertion.ref</code> en la condición sin haberlo mapeado a un atributo, el proveedor se crea pero nunca casa.</li>
<li><strong>Forks y PRs externos</strong>: los tokens de un PR desde un fork llevan <code>repository</code> del fork; una condición por <code>repository_owner</code> los deja fuera, que suele ser lo que quieres.</li>
<li><strong>Rotación</strong>: no hay nada que rotar, pero sí que revisar. <code>gcloud iam workload-identity-pools providers describe</code> y los logs de auditoría de STS dicen quién está entrando.</li>
</ul>
<p>Referencia: <a href="https://github.com/google-github-actions/auth">google-github-actions/auth</a> y <a href="https://docs.cloud.google.com/iam/docs/workload-identity-federation">Workload Identity Federation</a> en la documentación de Google Cloud.</p>
]]></content:encoded></item><item><title>Importar dumps a Cloud SQL sin errores: DEFINER y SUPER (ERROR 1227) en MySQL, extensiones y owners en PostgreSQL</title><link>https://inet.sh/es/posts/gcp/cloud-sql-importar-dumps-definer-gtid-extensiones/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/gcp/cloud-sql-importar-dumps-definer-gtid-extensiones/</guid><description>Un dump de mysqldump o pg_dump que funciona en cualquier servidor falla al importarlo en Cloud SQL: ERROR 1227 &amp;#39;you need the SUPER privilege&amp;#39; por los DEFINER y por SET @@GLOBAL.GTID_PURGED, &amp;#39;must be owner of extension plpgsql&amp;#39; y &amp;#39;role does not exist&amp;#39; en PostgreSQL. Los flags de exportación que Cloud SQL espera, qué hacer con los DEFINER (crear los usuarios, quitarlos o pasar a INVOKER), el sed que comenta los EXTENSION, el permiso del bucket y la importación con gcloud sql import sql.</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</h2>
<p><strong>MySQL</strong>, en el servidor origen:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">mysqldump --databases app -h db.example -u dumper -p <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --hex-blob --single-transaction --set-gtid-purged<span class="o">=</span>OFF <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --default-character-set<span class="o">=</span>utf8mb4 --routines --triggers --events <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> sed -E <span class="s1">&#39;s/DEFINER=`[^`]+`@`[^`]+`//g&#39;</span> <span class="p">|</span> gzip &gt; app.sql.gz
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>PostgreSQL</strong>, en el servidor origen:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pg_dump app -h db.example -U dumper --format<span class="o">=</span>plain --no-owner --no-acl <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> sed -E <span class="s1">&#39;s/(DROP|CREATE|COMMENT ON) EXTENSION/-- \1 EXTENSION/g&#39;</span> <span class="p">|</span> gzip &gt; app.sql.gz
</span></span></code></pre></td></tr></table>
</div>
</div><p>Y en los dos casos:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">SA</span><span class="o">=</span><span class="k">$(</span>gcloud sql instances describe my-instance --format<span class="o">=</span><span class="s1">&#39;value(serviceAccountEmailAddress)&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">gcloud storage buckets add-iam-policy-binding gs://my-dumps <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --member<span class="o">=</span><span class="s2">&#34;serviceAccount:</span><span class="nv">$SA</span><span class="s2">&#34;</span> --role<span class="o">=</span>roles/storage.objectAdmin
</span></span><span class="line"><span class="cl">gcloud storage cp app.sql.gz gs://my-dumps/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">gcloud sql import sql my-instance gs://my-dumps/app.sql.gz --database<span class="o">=</span>app                 <span class="c1"># MySQL</span>
</span></span><span class="line"><span class="cl">gcloud sql import sql my-instance gs://my-dumps/app.sql.gz --database<span class="o">=</span>app --user<span class="o">=</span>postgres  <span class="c1"># PostgreSQL</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>gcloud sql import sql</code> acepta SQL plano o <code>.gz</code>: el formato <em>custom</em> de <code>pg_dump</code> va por <code>pg_restore</code> (abajo).</p>
<h2 id="los-errores-y-de-dónde-salen">Los errores y de dónde salen</h2>
<p>En Cloud SQL nadie tiene <code>SUPER</code> (MySQL) ni es superusuario (PostgreSQL); el administrador es <code>root@%</code> o <code>postgres</code>, miembro de <code>cloudsqlsuperuser</code>. Todo lo que un dump normal da por hecho de un servidor propio se rompe justo ahí:</p>
<table>
  <thead>
      <tr>
          <th>Error al importar</th>
          <th>Motor</th>
          <th>Causa</th>
          <th>Arreglo</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ERROR 1227 (42000) at line N: Access denied; you need (at least one of) the SUPER privilege(s) for this operation</code> (línea de un <code>CREATE … VIEW</code>, <code>TRIGGER</code>, <code>PROCEDURE</code>, <code>FUNCTION</code> o <code>EVENT</code>)</td>
          <td>MySQL</td>
          <td><code>DEFINER=`user`@`host` </code> de un usuario que no existe en la instancia, o de <code>root@localhost</code>, que en Cloud SQL nunca existe. El dump no trae usuarios (y no debe traerlos)</td>
          <td>Crear esos usuarios antes de importar, o quitar los <code>DEFINER=</code> con el <code>sed</code></td>
      </tr>
      <tr>
          <td>El mismo <code>ERROR 1227</code> en las primeras líneas, en <code>SET @@GLOBAL.GTID_PURGED=…</code></td>
          <td>MySQL</td>
          <td>El origen tiene GTID y <code>mysqldump</code> escribe la posición, que solo <code>SUPER</code> puede fijar</td>
          <td><code>--set-gtid-purged=OFF</code></td>
      </tr>
      <tr>
          <td><code>ERROR 1449 (HY000): The user specified as a definer ('app'@'localhost') does not exist</code> al <strong>usar</strong> la vista, no al importar</td>
          <td>MySQL</td>
          <td>Dejaste un <code>DEFINER</code> de un usuario que no existe en la instancia</td>
          <td>Quitar el <code>DEFINER</code> o crear el usuario con ese host exacto</td>
      </tr>
      <tr>
          <td><code>ERROR: must be owner of extension plpgsql</code></td>
          <td>PostgreSQL</td>
          <td><code>COMMENT ON EXTENSION plpgsql</code> del dump: en Cloud SQL las extensiones pertenecen a <code>cloudsqladmin</code></td>
          <td>Comentar las líneas <code>EXTENSION</code> con el <code>sed</code></td>
      </tr>
      <tr>
          <td><code>ERROR: role &quot;app_owner&quot; does not exist</code></td>
          <td>PostgreSQL</td>
          <td><code>ALTER … OWNER TO</code> y <code>GRANT … TO</code> para roles del servidor viejo</td>
          <td><code>--no-owner --no-acl</code>, o crear los roles antes</td>
      </tr>
      <tr>
          <td><code>ERROR: permission denied for schema public</code> o <code>must be owner of …</code> a mitad del dump</td>
          <td>PostgreSQL</td>
          <td>Importaste con un <code>--user</code> que no es el dueño de lo que ya existe en la base</td>
          <td>Importa como <code>postgres</code> en una base vacía; reasigna después</td>
      </tr>
  </tbody>
</table>
<h2 id="mysql-qué-hacer-con-los-definer">MySQL: qué hacer con los DEFINER</h2>
<p>Primero mira cuántos y cuáles hay en el origen:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="k">DEFINER</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">INFORMATION_SCHEMA</span><span class="p">.</span><span class="n">VIEWS</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="k">DEFINER</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">INFORMATION_SCHEMA</span><span class="p">.</span><span class="n">TRIGGERS</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="k">DEFINER</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">INFORMATION_SCHEMA</span><span class="p">.</span><span class="n">ROUTINES</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="k">DEFINER</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">INFORMATION_SCHEMA</span><span class="p">.</span><span class="n">EVENTS</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Tres salidas, de más a menos fiel al original:</p>
<ol>
<li><strong>Crear los usuarios en la instancia antes de importar</strong>, con el mismo <code>user@host</code>: <code>gcloud sql users create app --host=% --instance=my-instance --password=…</code>. Es lo que recomienda la documentación, y mantiene las vistas y procedimientos exactamente como estaban. <code>root@localhost</code> es la excepción: no se puede crear en Cloud SQL, así que un <code>DEFINER</code> con ese valor hay que cambiarlo sí o sí.</li>
<li><strong>Quitar el <code>DEFINER</code></strong> con el <code>sed</code> de arriba: los objetos se crean con el usuario que importa como definer. Suficiente cuando los usuarios del origen no van a existir en Cloud SQL.</li>
<li><strong>Pasar a <code>SQL SECURITY INVOKER</code></strong> (<code>sed -E 's/SQL SECURITY DEFINER/SQL SECURITY INVOKER/g'</code> además del anterior): la vista o el procedimiento corre con los privilegios de quien lo llama y el definer deja de importar. Cambia el modelo de permisos; revisa que los usuarios de la aplicación puedan leer las tablas de debajo.</li>
</ol>
<p>El <code>sed</code> es deliberadamente amplio porque <code>DEFINER=</code> aparece también dentro de comentarios condicionales (<code>/*!50013 DEFINER=… SQL SECURITY DEFINER */</code>); borrar solo el <code>DEFINER=</code> deja el resto intacto y válido.</p>
<h2 id="mysql-los-flags-uno-por-uno">MySQL: los flags, uno por uno</h2>
<table>
  <thead>
      <tr>
          <th>Flag</th>
          <th>Por qué</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>--hex-blob</code></td>
          <td>Columnas binarias en hexadecimal; sin él, un <code>BLOB</code> con bytes raros se corrompe al pasar por el juego de caracteres</td>
      </tr>
      <tr>
          <td><code>--single-transaction</code></td>
          <td>Snapshot consistente de InnoDB sin bloquear tablas; imprescindible en producción</td>
      </tr>
      <tr>
          <td><code>--set-gtid-purged=OFF</code></td>
          <td>No escribir <code>SET @@GLOBAL.GTID_PURGED</code>, que Cloud SQL no deja ejecutar</td>
      </tr>
      <tr>
          <td><code>--default-character-set=utf8mb4</code></td>
          <td>Que el dump y el <code>SET NAMES</code> que trae coincidan con lo que hay en las tablas; evita el clásico <code>Ã©</code></td>
      </tr>
      <tr>
          <td><code>--databases app</code></td>
          <td>Obligatorio según la documentación, y nunca con la base <code>mysql</code>. Añade <code>CREATE DATABASE IF NOT EXISTS</code> y <code>USE app</code>, así <code>--database</code> en la importación es opcional</td>
      </tr>
      <tr>
          <td><code>--routines --triggers --events</code></td>
          <td><code>mysqldump</code> solo incluye triggers por defecto; procedimientos, funciones y eventos hay que pedirlos. Todos llevan <code>DEFINER</code></td>
      </tr>
  </tbody>
</table>
<p>Si el usuario con el que haces el dump no tiene <code>PROCESS</code>, <code>mysqldump</code> 8.0.21+ falla al inicio con <em>&ldquo;Access denied; you need (at least one of) the PROCESS privilege(s)&rdquo;</em> al consultar tablespaces: añade <code>--no-tablespaces</code>.</p>
<h2 id="postgresql-plano-o-custom">PostgreSQL: plano o custom</h2>
<p><code>--no-owner --no-acl</code> hacen que todo lo importado pertenezca al <code>--user</code> y no haya <code>GRANT</code> a roles fantasma. Si la aplicación se conecta con otro rol, crea el rol antes y usa <code>--user=app</code>, o reasigna después:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">REASSIGN</span><span class="w"> </span><span class="n">OWNED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">postgres</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">app</span><span class="p">;</span><span class="w">   </span><span class="c1">-- conectado como postgres, en la base importada
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Las extensiones que necesite la base (<code>pg_trgm</code>, <code>postgis</code>, <code>uuid-ossp</code>…) créalas <strong>antes</strong> de importar, como <code>postgres</code>, en la base destino: <code>CREATE EXTENSION IF NOT EXISTS pg_trgm;</code>. Solo funcionan las de la lista de extensiones soportadas de Cloud SQL; <code>plpgsql</code> ya está. Con eso, comentar los <code>CREATE EXTENSION</code> del dump no quita nada.</p>
<p>Si prefieres el formato <em>custom</em> (<code>-Fc</code>), el filtro de extensiones va en la lista de contenidos de <code>pg_restore</code>, tal como lo documenta Google, y restauras a través del Cloud SQL Auth Proxy:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pg_dump app -h db.example -U dumper --format<span class="o">=</span>custom --no-owner --no-acl &gt; app.dmp
</span></span><span class="line"><span class="cl">pg_restore --list app.dmp <span class="p">|</span> sed -E <span class="s1">&#39;s/(.*EXTENSION )/; \1/g&#39;</span> &gt; app.toc
</span></span><span class="line"><span class="cl">pg_restore --use-list<span class="o">=</span>app.toc --no-owner --no-acl -j <span class="m">4</span> -h 127.0.0.1 -U postgres -d app app.dmp
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>-j 4</code> restaura en paralelo, cosa que la importación desde el bucket no hace.</p>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong>El rol del bucket va a la service account de la instancia</strong>, no a la tuya: la documentación pide <code>storage.objectAdmin</code> (para importar bastan <code>storage.objects.get</code> y <code>list</code>, es decir, <code>objectViewer</code>). Sin él, <em>&ldquo;The service account does not have the required permissions for the bucket&rdquo;</em> aunque tú puedas leer el objeto.</li>
<li><strong>Una operación a la vez por instancia</strong>: si hay un backup o export en marcha, la importación falla con <em>&ldquo;operation already in progress&rdquo;</em>. <code>gcloud sql operations list --instance=my-instance</code> lo muestra.</li>
<li><strong>Dumps grandes</strong>: la importación es en serie y tarda; lanza con <code>--async</code> y vigila con <code>gcloud sql operations wait OPERATION_ID --timeout=unlimited</code>, o el <code>gcloud</code> se te muere por el camino.</li>
<li><strong>Versión de <code>pg_dump</code></strong>: usa la misma versión mayor que el destino o inferior. <code>pg_dump</code> 17 escribe <code>SET transaction_timeout = 0;</code>, que un PostgreSQL 16 rechaza con <em>&ldquo;unrecognized configuration parameter&rdquo;</em> y aborta la importación.</li>
<li><strong><code>utf8mb4</code> en la instancia</strong>: <code>--default-character-set=utf8mb4</code> en el dump no arregla una instancia creada con <code>latin1</code>; comprueba <code>character_set_server</code> en los flags de la instancia antes de importar.</li>
<li><strong>El <code>sed</code> de DEFINER también toca datos</strong>: si alguna fila contiene literalmente <code>DEFINER=`x`@`y` </code> (raro, pero un CMS que guarda SQL puede), sáltala con un patrón anclado a <code>CREATE</code>, o revisa el diff del dump.</li>
</ul>
<h2 id="ver-también">Ver también</h2>
<ul>
<li><a href="/es/posts/databases/postgresql-cheat-sheet-administracion/">PostgreSQL: cheat sheet de administración</a> — roles, <code>pg_dump</code>, <code>REASSIGN OWNED</code> y compañía.</li>
<li><a href="https://docs.cloud.google.com/sql/docs/mysql/import-export/import-export-sql">Import and export using SQL dump files (MySQL)</a>, <a href="https://docs.cloud.google.com/sql/docs/mysql/import-export#definer-clause">DEFINER clause</a> y <a href="https://docs.cloud.google.com/sql/docs/postgres/import-export/import-export-dmp">pg_dump / pg_restore (PostgreSQL)</a> en la documentación de Cloud SQL.</li>
</ul>
]]></content:encoded></item></channel></rss>