<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title>SysAdmin Journal · Google Cloud Platform</title>
        <link>https://sysadmin-journal.com/tag/google-cloud-platform</link>
        <description>Posts tagged with Google Cloud Platform</description>
        <language>en</language>
        <lastBuildDate>Sun, 02 Aug 2026 08:57:00 +0000</lastBuildDate>
        <atom:link href="https://sysadmin-journal.com/tag/google-cloud-platform/rss" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        <item>
            <title>Essential Linux Skills for Google Cloud Platform — DevCon 2026</title>
            <link>https://sysadmin-journal.com/essential-linux-skills-for-google-cloud-platform-devcon-2026</link>
            <guid isPermaLink="true">https://sysadmin-journal.com/essential-linux-skills-for-google-cloud-platform-devcon-2026</guid>
            <pubDate>Sun, 02 Aug 2026 08:57:00 +0000</pubDate>
            <dc:creator>Ish Sookun</dc:creator>
            <category>MSCC</category>
            <category>Conference</category>
            <category>Google Cloud Platform</category>
            <description>My first talk as a Google Developer Expert in Cloud: the Linux skills that matter most on GCP, delivered to a packed room at the Developers Conference 2026 in Mauritius.</description>
            <media:content url="https://sysadmin-journal.com/content/images/2026/07/ish-sookun-developers-conference-2026-essential-linux-for-google-cloud-platform.jpeg" medium="image" />
            <content:encoded><![CDATA[<p>On Thursday, 23 July 2026, at 1 p.m., I walked into the Educator 1 room at the
Voilà Hotel in Bagatelle to deliver a talk I had been looking forward to for
weeks: <strong>Essential Linux Skills for Google Cloud Platform</strong>, at the Developers
Conference 2026.</p>
<p>It was a first for me in more ways than one. Six days earlier, on Friday 17 July,
I received the email confirming my acceptance as a <strong>Google Developer Expert in Google Cloud — Modern and Enterprise Infrastructure</strong>.</p>
<figure class="mt-10 max-w-4xl mx-auto">
<img src="https://sysadmin-journal.com/content/images/2026/07/developers-conference-2026-new-gdes-in-mauritius.jpeg" />
<figcaption class="-mt-10 text-center text-xs font-mono text-ink-500 dark:text-ink-400">JoKi on the left, with Noor (right) and myself (center) — the two recent GDEs from Mauritius</figcaption>
</figure>
<p>DevCon 2026 was my first GCP talk wearing that hat, and I will admit the timing made the session feel special. <em>The photo was taken on Day 3, since the new GDE announcement was made during the Google keynote.</em></p>
<h2>A packed room, and a lot of new faces</h2>
<p>The room filled up quickly — with a young audience, many of whom I saw for the first time. After years in the local community — MSCC, DevCon, DevFest, openSUSE, Cloud Native, meetups — you start to recognise everyone.</p>
<p><img src="https://sysadmin-journal.com/content/images/2026/07/developers-conference-2026-essential-linux-for-google-cloud-platform-attendees.jpeg" alt="" /></p>
<p>Thursday afternoon was the opposite of that, and it was genuinely encouraging. There is a new generation coming up that wants to do infrastructure work, and they showed up at 1 p.m. on a working day to learn Linux.</p>
<h2>The premise: the cloud didn't replace Linux — it runs on it</h2>
<p>That was the line on my title slide, and the whole talk hangs on it. Roughly 90%
of public-cloud workloads run on Linux. Every GKE node boots a Linux kernel.
Every container on Cloud Run or GKE is a Linux process in namespaces and cgroups.
Abstractions leak, and when a deployment fails at 2 a.m., the fix is almost never
a button in the console — it's a Linux skill.</p>
<p>So rather than a deep-dive on any one tool, I gave the room a <strong>skills map</strong>:</p>
<ul>
<li><strong>Level 1 — VM operations:</strong> SSH and key management, packages
(<code>apt</code> / <code>zypper</code> / <code>dnf</code>), files, permissions, <code>sudo</code>, disks and mounts.</li>
<li><strong>Level 2 — Services and network:</strong> systemd units, firewalls at both layers,
sockets with <code>ss</code>, logs with <code>journalctl</code>.</li>
<li><strong>Level 3 — Containers and GKE:</strong> containers as Linux processes, images as
packaged Linux. Your VM skills transfer; GKE stops being magic.</li>
</ul>
<h2>Six demos, under two minutes each</h2>
<p>I kept the talk hands-on with six short live demos on <code>africa-south1</code>:</p>
<ol>
<li><strong>Compute Engine</strong> — create a VM and get a shell entirely from <code>gcloud</code>,
with image families so you never chase image names. Ubuntu 24.04 for the demo,
with a nod to <code>--image-family=opensuse-leap</code> for those who know better. 😄
Rather than let the command scroll past, I read it out in plain English —
create an instance, in this zone, of this machine type, from this image
family, published by this project — so nobody had to guess what a flag did.
Being able to read a command and translate it into the actions it performs is
an acquired skill, and a valuable one. It comes with time and repetition.</li>
<li><strong>OS Login</strong> — IAM as your new <code>/etc/passwd</code>. I took a short detour first
through how SSH keys actually work: what the public key does, what the private
key never does. Then a light touch on PAM and the Google Cloud guest agent,
which is what allows <code>gcloud</code> to handle authentication instead of leaning on a
local Unix account. Then the proof — I logged into the instance from the first
demo with no Unix account on the system, no SSH key on it and no password. No
key sprawl, instant revocation, every login in Cloud Audit Logs.
<code>roles/compute.osLogin</code> for a shell, <code>osAdminLogin</code> for sudo.</li>
<li><strong>systemd and journalctl</strong> — the plan was to install nginx, break it and
triage it live, but the clock said otherwise. So I stayed on the instance we
already had and read the OpenSSH logs and the PAM entries written the moment I
connected through OS Login — the same authentication path we had just talked
about, now visible in the journal. The three filters that solve most
incidents: <code>-u</code> for one unit, <code>-p err</code> for errors only, <code>--since</code> to bound the
window.</li>
<li><strong>Networking</strong> — two firewalls, one mental model. I covered <code>ufw</code> and
<code>firewalld</code> and the job each one does, then made the case for filtering a
layer higher. If you handle everything at the instance, a heavy traffic attack
still reaches your VM and your CPU peaks filtering out the nonsense. Push it
to the Cloud Firewall and GCP absorbs that pressure before it ever touches
you — and the rules stay one command away. The heuristic that saves hours: a
<strong>timeout</strong> usually means the VPC firewall dropped your packets, a
<strong>connection refused</strong> means you reached the host and nothing was listening.
<code>ss -tulpn</code> settles the argument.</li>
<li><strong>Storage</strong> — grow a disk with zero downtime: resize the Persistent Disk,
then <code>growpart</code>, then <code>resize2fs</code> (or <code>xfs_growfs</code>). Best wow-per-second of
the session.</li>
<li><strong>Observability</strong> — install the Ops Agent and watch journald entries land in
Cloud Logging, then query the whole fleet with <code>gcloud logging read</code>. Your
Linux logging knowledge doesn't get replaced in the cloud, it gets multiplied.</li>
</ol>
<p>A few things needed no demo but earn their place in the muscle-memory layer:
<code>chmod</code> / <code>chown</code> / <code>umask</code> (where most &quot;the app can't write&quot; bugs live), sudoers
drop-ins instead of editing <code>/etc/sudoers</code>, <code>grep -r</code> and <code>tail -f</code> for text
triage, and systemd timers over cron — logged, dependency-aware, testable.</p>
<h2>For those coming from Windows</h2>
<p>Instead of arguing the case, I opened <a href="https://top500.org">top500.org</a> live and
generated a list of the world's 500 fastest supercomputers with the operating
system and operating system family columns showing. The most recent benchmark had
been published in June 2026, only weeks before the talk. No surprise to anyone
who follows this list: all 500 of them run a variant of Linux. Five hundred out
of five hundred.</p>
<p>It makes the point better than I could. The machines at the very top of the field
and the VM we had spun up ten minutes earlier on Compute Engine speak the same
language.</p>
<h2>Homework</h2>
<p>I closed with a ten-item checklist, one skill per evening on the free tier: from
&quot;create and SSH into a VM entirely from <code>gcloud</code>&quot; to &quot;explain why a container is
just a Linux process.&quot; Tick all ten and you're production-ready on GCP —
everything else is depth, not breadth.</p>
<p>The Q&amp;A ran the full ten minutes, with the expected and very welcome question
about which distributions we run in production and why.</p>
<p>Thank you to everyone who came, and to the DevCon team for another well-run
edition. The slides are on their way here, and the small workaround needed to get
the Ops Agent repo script happy on openSUSE Leap 16 deserves a post of its own.</p>
<blockquote>
<p>The cloud is someone else's computer. It still speaks Linux.</p>
</blockquote>
]]></content:encoded>
        </item>
        <item>
            <title>From Flask to Cloud Run: A Workshop at ALCHE Mauritius</title>
            <link>https://sysadmin-journal.com/from-flask-to-cloud-run-a-workshop-at-alche-mauritius</link>
            <guid isPermaLink="true">https://sysadmin-journal.com/from-flask-to-cloud-run-a-workshop-at-alche-mauritius</guid>
            <pubDate>Thu, 30 Apr 2026 09:00:51 +0000</pubDate>
            <dc:creator>Ish Sookun</dc:creator>
            <category>Google Cloud Platform</category>
            <description>On Friday 17 April 2026, I delieverd a technical workshop at the African Leadership College of Higher Education (ALCHE) in Pamplemousses. The topic: taking a Python Flask application from a developer&#039;...</description>
            <content:encoded><![CDATA[<p>On Friday 17 April 2026, I delieverd a technical workshop at the <a href="https://alcheducation.com/">African Leadership College of Higher Education (ALCHE)</a> in Pamplemousses. The topic: taking a Python Flask application from a developer's laptop all the way to a live, publicly accessible URL on Google Cloud Run, backed by Cloud SQL for PostgreSQL. Sixteen students from the Year 2 Software Engineering cohort attended.</p>
<h2>How it came about to be?</h2>
<p>A few weeks ago, I had a conversation with Allan, Programme Officer for Software Engineering at ALCHE, about Google Cloud Platform and specifically how students could make use of the <em>Free Trial and Free Tier</em> services to deploy their school projects. He walked me through the typical stack the students work with — Python, Flask, HTML, CSS, JavaScript — the standard university curriculum. The natural next step felt obvious: let's show them what happens after the code is written.</p>
<p>I pitched the idea of a session focused on making your app accessible to people on the Internet, and Allan was on board.</p>
<p>I wasn't presenting alone. Eddy, IT Director at La Sentinelle and a certified Google Cloud Architect, joined as co-presenter. He kicked things off with an introduction to cloud computing in general before narrowing in on GCP, which was exactly the warm-up the audience needed before I dove into the more specialised services.</p>
<div class="mb-4 grid place-items-center">
    <img class="kg-image" src="https://sysadmin-journal.com/content/images/2026/04/ALCHE_Workshop_Eddy_Lareine.jpeg" alt="Eddy Lareine presenting GCP Regions & Zones at ALCHE Mauritius" />
<figcaption class="text-xs text-gray-900 -mt-8">Eddy Lareine presenting GCP Regions & Zones at ALCHE Mauritius</figcaption>
<div>
<p>His segment covered Regions &amp; Zones — a topic that trips up a lot of beginners. Not all GCP services are available in all regions, pricing varies, and critically, Free Tier services are constrained to specific regions. If you're a student in Mauritius spinning up a Cloud Run service and you pick the wrong region, you might burn through your free credits faster than expected. That context mattered.</p>
<h2>The Demo App: Petrol Watch</h2>
<p>I always prefer to demo with something tangible. The week I was planning the workshop, a petrol price hike had just been announced — so I built Petrol Watch, a small Flask application backed by PostgreSQL that tracks fuel prices published by the State Trading Corporation (STC) going back to 2004.</p>
<p>Although I'm a Laravel person at heart, I built this in Flask so the students could follow along with the stack they already know. It felt right to meet them where they are.</p>
<div class="grid place-items-center">
    <img src="https://sysadmin-journal.com/content/images/2026/04/ALCHE_Cloud_Run_Workshop_1.jpeg" alt="Presentation on Deploying Flask to Google Cloud Run" />
</div>
<h2>Containers: The Full Picture</h2>
<p>Before touching a single GCP service, I spent time on <strong>containers</strong> — what they are, where they came from, and why they matter.</p>
<p>I explained that a container image is simply your code, your runtime, your system libraries, and a minimal base OS image bundled into a single portable artefact. The same image runs on your laptop, your colleague's Mac, and Google's servers. The &quot;it works on my machine&quot; excuse disappears.</p>
<p>Then came the history lesson. Containers didn't start with Docker. I walked them through:</p>
<ul>
<li><strong>Unix chroot</strong> — the earliest form of process isolation</li>
<li><strong>BSD Jails</strong> — proper containerisation before Linux had it</li>
<li><strong>Linux Namespaces</strong> — the first namespace (<code>mount</code>) arrived in kernel 2.4.19 in August 2002; by kernel 2.6.24, the full suite of namespaces (PID, network, IPC, UTS, user) was in place, completing the isolation story</li>
<li><strong>cgroups</strong> — introduced in 2006, giving the kernel the ability to limit and account for the resources a group of processes can use; together with namespaces, this is the technical foundation everything else builds on</li>
<li><strong>Docker</strong> — popularised containers by wrapping the complexity in a usable developer experience</li>
</ul>
<p>I made a point of clarifying something that often causes confusion: <strong>Docker is a brand name, not a technology</strong>. The same instructions you put in a <code>Dockerfile</code> can equally live in a file named <strong><code>Containerfile</code></strong> — the <a href="https://github.com/containers/common/blob/main/docs/Containerfile.5.md">Containerfile specification</a> from the <code>containers/common</code> project formally defines this format, and tools like <strong>Podman</strong> and <strong>Buildah</strong> default to looking for a <code>Containerfile</code> first before falling back to <code>Dockerfile</code>. Docker, by contrast, only looks for <code>Dockerfile</code>. The syntax is identical — it's purely a naming convention, but it reflects a broader and healthier ecosystem that isn't tied to any single vendor. The industry has standardised on the Open Container Initiative (OCI) format for images, so whichever tool you use to build, you're not locked in.</p>
<p>I touched on <strong>Kubernetes</strong> as well — but deliberately kept it as a signpost to an advanced future topic. When you're deploying a single container application, Kubernetes is not the right starting point.</p>
<h2>The Deployment Spectrum</h2>
<p>One slide I always find useful is the deployment spectrum:</p>
<div class="grid place-items-center">
    <img class="-mt-8 -mb-4" src="https://sysadmin-journal.com/content/images/2026/04/Flask_to_CloudRun_ALCHE_v3_5.jpg" alt="Slide 5 showing the deployment spectrum" />
</div>
<p>Cloud Run sits in the sweet spot for most student projects and even a good chunk of production workloads: bring a container, Google handles everything else.</p>
<h2>The Dockerfile</h2>
<p>Cloud Run has exactly one requirement: a container image. Here's the Dockerfile I showed the students for Petrol Watch:</p>
<pre><code class="language-dockerfile">FROM python:3.14-slim

ENV PORT=8080 PYTHONUNBUFFERED=1

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

CMD exec gunicorn --bind :$PORT --workers 2 \
       --threads 4 --timeout 0 app:app
</code></pre>
<p>A few things worth highlighting:</p>
<ul>
<li><strong><code>python:3.14-slim</code></strong> — a smaller base image means faster cold starts and a smaller attack surface.</li>
<li><strong><code>$PORT</code></strong> — Cloud Run injects this environment variable. Never hard-code port 5000. Your container must listen on whatever <code>$PORT</code> says.</li>
<li><strong>Gunicorn</strong> — Flask's built-in development server is not for production. Gunicorn is. I took a moment to explain <em>why</em>: a web server like Apache httpd or Nginx is very good at serving static files efficiently, but it has no built-in way to execute Python code. That's where <strong>WSGI — the Web Server Gateway Interface</strong> — comes in. WSGI is a standard protocol (defined in PEP 3333) that acts as a bridge between the web server and your Python application. Gunicorn is a WSGI server: it sits between the network and Flask, receives HTTP requests, calls your application code, and returns the response. In the Cloud Run context, Gunicorn <em>is</em> the entry point — there's no separate Nginx in front of it — but the principle is the same one students will encounter whenever they deploy Python apps on a traditional server.</li>
</ul>
<p>I also explained what happens when you build the image: you get layers — files, directories, and an <strong>overlay filesystem</strong>. Each instruction in the Dockerfile adds a layer. I explained the role of <strong>container registries</strong> (Artifact Registry on GCP, Docker Hub elsewhere) as the distribution mechanism — you push your image there, and Cloud Run pulls it.</p>
<h2>Cloud SQL: Managed Postgres Without the 3 a.m. Alerts</h2>
<p>Deploying the database was almost the more interesting part of the session. I showed them how to spin up a <strong>Cloud SQL PostgreSQL</strong> instance using the <code>gcloud</code> CLI:</p>
<pre><code class="language-bash">gcloud sql instances create petrol-db \
    --database-version=POSTGRES_16 \
    --tier=db-f1-micro \
    --region=africa-south1 \
    --root-password=&quot;&lt;strong&gt;&quot;

gcloud sql databases create petrol_watch \
    --instance=petrol-db

gcloud sql users create appuser \
    --instance=petrol-db \
    --password=&quot;&lt;from Secret Manager&gt;&quot;
</code></pre>
<p>The <code>db-f1-micro</code> tier is modest — it's the &quot;tiny&quot; instance I referenced throughout the session — but it's sufficient for a demo and keeps costs near zero. Same region as your Cloud Run service means lower latency and no cross-region egress charges.</p>
<div class="grid place-items-center">
    <img src="https://sysadmin-journal.com/content/images/2026/04/ALCHE_Cloud_Run_Workshop_2.jpeg" alt="Cloud SQL on GCP" />
</div>
<h2>Wiring It All Together</h2>
<p>Deploying to Cloud Run is a single command:</p>
<pre><code class="language-bash">gcloud run deploy petrol-watch \
     --source . \
     --region=africa-south1 \
     --add-cloudsql-instances=lsl-it:africa-south1:petrol-db \
     --set-env-vars=DB_NAME=petrol_watch,DB_USER=appuser \
     --set-secrets=DB_PASSWORD=petrol-db-pass:latest \
     --allow-unauthenticated
</code></pre>
<p><code>--source .</code> tells Cloud Build to detect the Dockerfile, build the image, and push it to Artifact Registry — all automatically. <code>--add-cloudsql-instances</code> opens a secure Unix socket to Cloud SQL with no public IPs and no passwords on the wire.</p>
<h2>Sidecar Containers and One-Shot Jobs</h2>
<p>One concept I introduced was <strong>sidecar containers</strong> — the Cloud SQL Auth Proxy that Cloud Run injects alongside your app container to handle the database connection securely. Your <code>app.py</code> just connects to a local Unix socket. Google's sidecar handles the TLS and IAM authentication. You write boring, straightforward code.</p>
<p>I also explained how you can create <strong>single-use containers</strong> — Cloud Run Jobs — designed to run once and exit. The canonical use case: running database migrations and seeding data before your service goes live. Not everything needs to be a long-running HTTP server.</p>
<h2>Secrets: The One Non-Negotiable</h2>
<p>I spent a slide on this because it matters and students often get it wrong the first time:</p>
<pre><code class="language-bash"># Store once in Secret Manager
echo -n &quot;Sup3rS3cret&quot; | gcloud secrets \
    create petrol-db-pass --data-file=-

# Wire it into Cloud Run at deploy time
--set-secrets=DB_PASSWORD=petrol-db-pass:latest

# In app.py — just read an env var
os.environ['DB_PASSWORD']
</code></pre>
<p><strong>Never commit credentials to Git.</strong> Not even in a student project. Git history is permanent, and sharing your screen during a Friday demo is enough to leak a password. Secret Manager is versioned, auditable, revocable, and free for the first handful of secrets.</p>
<h2>The Questions They Asked</h2>
<p>The 2-hour session ended with a Q&amp;A, and the students' questions were genuinely good:</p>
<p><strong>Managing cloud costs</strong> came up immediately. I walked them through the Free Tier limits — 2 million Cloud Run requests per month, 360,000 GB-seconds of memory, 180,000 vCPU-seconds — and reiterated the importance of Eddy's earlier point about choosing the right region.</p>
<p><strong>Processing large datasets</strong> was the other big one. A group mentioned they had a project involving a 120 MB CSV file. I smiled. I told them the real world is considerably harsher than that — production datasets that would make a 120 MB file look like a hello world. The lesson: Cloud Run has a 60-minute request timeout, and long-running jobs belong in <strong>Cloud Run Jobs</strong> or <strong>Cloud Tasks</strong>, not a synchronous HTTP handler.</p>
<h2>Reflections</h2>
<p>Workshops like this are why I enjoy the community side of my work. These students are building with the same stack that runs production systems — Python, PostgreSQL, containers. Giving them a path from &quot;it works on my laptop&quot; to a live HTTPS URL, covered by Google's infrastructure, in under two hours, felt worthwhile.</p>
<p>If you're an educator or a developer community organiser in Mauritius and want to run something similar, reach out. I'm happy to do it again.</p>
<hr />
<p><strong>Resources mentioned during the workshop:</strong></p>
<ul>
<li><a href="https://cloud.google.com/run/docs">Cloud Run documentation</a></li>
<li><a href="https://cloud.google.com/sql/docs/postgres/connect-run">Cloud Run + Cloud SQL quickstart</a></li>
<li><a href="https://cloud.google.com/free">GCP Free Tier details</a></li>
<li><a href="https://github.com/ish-sookun/petrol-watch">Petrol Watch source code</a></li>
</ul>
<hr />
]]></content:encoded>
        </item>
        <item>
            <title>How to Deploy a Laravel Application on Google Cloud Run?</title>
            <link>https://sysadmin-journal.com/how-to-deploy-a-laravel-application-on-google-cloud-run</link>
            <guid isPermaLink="true">https://sysadmin-journal.com/how-to-deploy-a-laravel-application-on-google-cloud-run</guid>
            <pubDate>Fri, 13 Mar 2026 09:06:59 +0000</pubDate>
            <dc:creator>Ish Sookun</dc:creator>
            <category>Google Cloud Platform</category>
            <category>Laravel</category>
            <description>Deploy a Laravel application on Google Cloud Run with PHP 8.5, connect to Cloud SQL PostgreSQL, and scale to zero when idle. A practical guide covering containerization, deployment to the africa-south1 region, and tips for handling storage, sessions, and queues in a serverless environment.</description>
            <media:content url="https://sysadmin-journal.com/content/images/2026/03/laravel-on-google-cloud-run.jpeg" medium="image" />
            <content:encoded><![CDATA[<p>Google Cloud Run is a serverless platform that lets you run containerized applications without managing any infrastructure. You push a container, Cloud Run handles scaling — including scaling to zero when there's no traffic, which means you only pay for what you use.</p><p>For someone like me who has been deploying Laravel applications on traditional Compute Engine instances and Kubernetes clusters, Cloud Run feels like a breath of fresh air for certain workloads. Not everything needs a full-blown GKE cluster or a dedicated VM running 24/7. Sometimes you just need your app to be available, scale when needed, and not cost you anything when it's idle.</p><p>What makes this particularly interesting right now is that Google Cloud has made the <strong>PHP 8.5 runtime generally available on Cloud Run</strong>. If you're running Laravel 12 on PHP 8.5 — as I am — this is great news.</p><h2 id="why-cloud-run-for-laravel">Why Cloud Run for Laravel?</h2><p>Laravel is traditionally deployed on a web server like Nginx or Apache, sitting behind PHP-FPM, on a Linux server you manage yourself. That's perfectly fine for production workloads that need full control. But for staging environments, internal tools, API backends, or even personal projects, Cloud Run removes a lot of the operational overhead.</p><p>Key advantages that stand out for me:</p><ul><li><strong>Scale to zero</strong> — no traffic, no cost. This is ideal for development and staging environments.</li><li><strong>Automatic HTTPS</strong> — Cloud Run provisions and manages TLS certificates for you.</li><li><strong>Built-in revision management</strong> — every deployment creates a new revision, making rollbacks trivial.</li><li><strong>Regional deployment</strong> — and yes, <code>africa-south1</code> (Johannesburg) is supported, which means latency from Mauritius is reasonable thanks to the submarine cable connectivity.</li></ul><h2 id="preparing-your-laravel-application">Preparing Your Laravel Application</h2><p>Before deploying to Cloud Run, your Laravel application needs to be containerized. If you've worked with Docker before — and if you're deploying Laravel in 2026, you probably have — this is straightforward.</p><p>Create a <code>Dockerfile</code> in the root of your Laravel project:</p><pre><code>FROM php:8.5-apache

RUN apt-get update &amp;&amp; apt-get install -y \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip \
    &amp;&amp; docker-php-ext-install pdo_pgsql mbstring exif pcntl bcmath gd

RUN a2enmod rewrite

ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf

COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html
COPY . .

RUN composer install --no-dev --optimize-autoloader
RUN php artisan config:cache &amp;&amp; php artisan route:cache &amp;&amp; php artisan view:cache

RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

EXPOSE 8080

RUN sed -i 's/80/8080/g' /etc/apache2/sites-available/000-default.conf /etc/apache2/ports.conf

CMD ["apache2-foreground"]</code></pre><p>A couple of things to note here. Cloud Run expects your container to listen on port <code>8080</code> by default — that's why we're modifying the Apache configuration. I'm using <code>pdo_pgsql</code> because I connect to Cloud SQL PostgreSQL, but swap that for <code>pdo_mysql</code> if you're on MySQL.</p><h2 id="deploying-to-cloud-run">Deploying to Cloud Run</h2><p>With the Dockerfile ready, deploying is a single <code>gcloud</code> command. First, make sure you have the Google Cloud SDK installed and authenticated.</p><pre><code>gcloud run deploy my-laravel-app \
    --source . \
    --region africa-south1 \
    --allow-unauthenticated \
    --set-env-vars APP_KEY=base64:YOUR_APP_KEY_HERE \
    --set-env-vars APP_ENV=production \
    --set-env-vars LOG_CHANNEL=stderr \
    --set-env-vars DB_CONNECTION=pgsql \
    --set-env-vars DB_HOST=/cloudsql/PROJECT_ID:africa-south1:INSTANCE_NAME \
    --add-cloudsql-instances PROJECT_ID:africa-south1:INSTANCE_NAME \
    --memory 512Mi \
    --cpu 1 \
    --min-instances 0 \
    --max-instances 5</code></pre><p>The <code>--source .</code> flag tells Cloud Run to build the container image for you using Cloud Build — so you don't even need to push to Artifact Registry manually. Google handles the build and deployment in one step.</p><p>Notice the <code>--min-instances 0</code> — this is what enables scale-to-zero. For a staging environment or a low-traffic application, this keeps your costs minimal. For production, you might want to set <code>--min-instances 1</code> to avoid cold starts.</p><p>The <code>LOG_CHANNEL=stderr</code> setting is important. Cloud Run captures <code>stderr</code> output and sends it to Cloud Logging, so you get your Laravel logs in the Google Cloud Console without any additional configuration.</p><h2 id="connecting-to-cloud-sql">Connecting to Cloud SQL</h2><p>If you're using Cloud SQL — and I've written about <a href="https://sysadmin-journal.com/google-cloud-workload-identity-federation-a-guide-to-keyless-authentication-for-multi-cloud-environments/">scaling Laravel with Cloud SQL read replicas</a> before — Cloud Run has built-in support for connecting through the Cloud SQL Auth Proxy. The <code>--add-cloudsql-instances</code> flag in the deploy command sets this up automatically.</p><p>The connection happens over a Unix socket, which is why the <code>DB_HOST</code> is set to <code>/cloudsql/PROJECT_ID:REGION:INSTANCE_NAME</code> rather than an IP address. This is secure by default — no public IP required on your Cloud SQL instance.</p><h2 id="what-about-file-storage">What About File Storage?</h2><p>Laravel's default file storage driver writes to the local filesystem. That won't work on Cloud Run because the container filesystem is ephemeral — it gets wiped on every new deployment or instance scale event. For file uploads and storage, switch to Google Cloud Storage using the <code>league/flysystem-google-cloud-storage</code> package:</p><pre><code>composer require league/flysystem-google-cloud-storage</code></pre><p>Configure a <code>gcs</code> disk in your <code>config/filesystems.php</code> and set <code>FILESYSTEM_DISK=gcs</code> in your environment variables. Cloud Run's service account will handle authentication automatically if you've granted it the <code>Storage Object Admin</code> role — no API keys needed.</p><h2 id="things-to-keep-in-mind">Things to Keep in Mind</h2><p>Cloud Run is stateless. This means you cannot rely on the local filesystem for sessions or cache. Use <strong>Redis</strong> (via Memorystore) or <strong>database sessions</strong> instead. Similarly, Laravel's scheduler (<code>php artisan schedule:run</code>) doesn't work in Cloud Run's request-driven model. For scheduled tasks, pair Cloud Scheduler with Cloud Run by having the scheduler trigger an HTTP endpoint on your application.</p><p>Queue workers are another consideration. Cloud Run isn't designed for long-running processes, so running <code>php artisan queue:work</code> inside a Cloud Run container isn't ideal. Instead, use Cloud Tasks to dispatch jobs to a dedicated Cloud Run endpoint, or run your workers on a Compute Engine instance or GKE.</p><h2 id="wrapping-up">Wrapping Up</h2><p>Cloud Run sits in a sweet spot between the full control of a Compute Engine VM and the abstraction of a purely serverless function. For Laravel applications that don't need persistent background processes or filesystem state, it's an excellent deployment target — especially now that PHP 8.5 is fully supported.</p><p>I've been using it for a couple of internal tools and staging environments, and the cost savings alone from scale-to-zero make it worth exploring. If you're already on Google Cloud and deploying Laravel, it's worth giving Cloud Run a try on your next project.</p>]]></content:encoded>
        </item>
        <item>
            <title>openSUSE Leap 16.0 is now available on Google Cloud Platform</title>
            <link>https://sysadmin-journal.com/opensuse-leap-16-0-is-now-available-on-google-cloud-platform</link>
            <guid isPermaLink="true">https://sysadmin-journal.com/opensuse-leap-16-0-is-now-available-on-google-cloud-platform</guid>
            <pubDate>Wed, 18 Feb 2026 07:55:36 +0000</pubDate>
            <dc:creator>Ish Sookun</dc:creator>
            <category>openSUSE</category>
            <category>Google Cloud Platform</category>
            <description>openSUSE Leap 16.0 is now available on Google Cloud Platform with x86_64 and Arm64 images. You can launch a Compute Engine instance by selecting openSUSE Leap 16.0 under Public images. The Cloud Observability Ops Agent is not yet supported.</description>
            <content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/opensuse-leap-on-google-cloud-platform-2026.png" class="kg-image" alt="" loading="lazy" width="2000" height="1335" srcset="/content/images/size/w600/2026/02/opensuse-leap-on-google-cloud-platform-2026.png 600w, /content/images/size/w1000/2026/02/opensuse-leap-on-google-cloud-platform-2026.png 1000w, /content/images/size/w1600/2026/02/opensuse-leap-on-google-cloud-platform-2026.png 1600w, /content/images/2026/02/opensuse-leap-on-google-cloud-platform-2026.png 2184w" sizes="(min-width: 720px) 720px"></figure><p>openSUSE Leap 16.0 is now available as a public image on Google Cloud Platform. Both <code>x86_64</code> and <code>Arm64</code> images were built on 16 February 2026 and are ready for use with Compute Engine instances.</p><h2 id="launching-an-instance-with-opensuse-leap-160">Launching an instance with openSUSE Leap 16.0</h2><p>Getting started with Leap 16.0 on GCP is straightforward. From the Google Cloud Console, navigate to <strong>Compute Engine &gt; VM instances</strong> and click <strong>Create Instance</strong>.</p><p>In the instance configuration page, scroll down to the <strong>OS and storage</strong> section and click <strong>Change</strong> to open the boot disk configuration panel. </p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/gcp-compute-os-change.png" class="kg-image" alt="" loading="lazy" width="1832" height="662" srcset="/content/images/size/w600/2026/02/gcp-compute-os-change.png 600w, /content/images/size/w1000/2026/02/gcp-compute-os-change.png 1000w, /content/images/size/w1600/2026/02/gcp-compute-os-change.png 1600w, /content/images/2026/02/gcp-compute-os-change.png 1832w" sizes="(min-width: 720px) 720px"></figure><p>Under the <strong>Public images</strong> tab, select <strong>openSUSE</strong> from the <strong>Operating system</strong> dropdown. You will then see <strong>openSUSE Leap</strong> listed under the <strong>Version</strong> dropdown — select the <strong>openSUSE Leap 16.0</strong> entry. Choose your preferred boot disk type and size, then click <strong>Select</strong> to confirm.</p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/gcp-compute-select-opensuse-leap.png" class="kg-image" alt="" loading="lazy" width="1134" height="1294" srcset="/content/images/size/w600/2026/02/gcp-compute-select-opensuse-leap.png 600w, /content/images/size/w1000/2026/02/gcp-compute-select-opensuse-leap.png 1000w, /content/images/2026/02/gcp-compute-select-opensuse-leap.png 1134w" sizes="(min-width: 720px) 720px"></figure><p>From there, configure the rest of your instance settings — machine type, networking, firewall rules — as you normally would, and click <strong>Create</strong> to launch your new Leap 16.0 VM.</p><h2 id="a-note-on-google-cloud-observability">A note on Google Cloud Observability</h2><p>During instance creation, you will notice the <strong>Observability - Ops Agent</strong> section near the bottom of the configuration page. For supported operating systems, this provides a convenient <strong>Install Ops Agent for Monitoring and Logging</strong> checkbox that automatically installs the agent when the VM boots. However, with openSUSE Leap 16.0 selected as the boot disk image, this checkbox is greyed out and displays the message: <em>"Not available for the selected image."</em> This is because the automated installation method does not yet support Leap 16.0.</p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/gcp-compute-ops-agent.png" class="kg-image" alt="" loading="lazy" width="1084" height="444" srcset="/content/images/size/w600/2026/02/gcp-compute-ops-agent.png 600w, /content/images/size/w1000/2026/02/gcp-compute-ops-agent.png 1000w, /content/images/2026/02/gcp-compute-ops-agent.png 1084w" sizes="(min-width: 720px) 720px"></figure><p>You might think the next step would be to install the Ops Agent manually via the command line. Google's <a href="https://docs.cloud.google.com/stackdriver/docs/solutions/agents/ops-agent/installation">installation documentation</a> provides a script for exactly this:</p><pre><code>curl -sSO https://dl.google.com/cloudagents/add-google-cloud-ops-agent-repo.sh
sudo bash add-google-cloud-ops-agent-repo.sh --also-install</code></pre><p>Unfortunately, this does not work on Leap 16.0 either — and the reason is buried in how the script identifies your operating system.</p><h2 id="why-does-the-script-fail">Why does the script fail?</h2><p>The script reads <code>/etc/os-release</code> to determine the OS family and version. For any SUSE-based distribution, including openSUSE Leap, it routes to a <code>handle_suse</code> function via a case statement in <code>main()</code>:</p><pre><code>sles|opensuse-leap) handle_suse ;;</code></pre><p>Inside <code>handle_suse</code>, the <code>add_repo()</code> function extracts the major version number and constructs a SLES-based repository codename:</p><pre><code>local SUSE_VERSION=${VERSION_ID%%.*}
local CODENAME="${REPO_CODENAME:-"sles${SUSE_VERSION}"}"</code></pre><p>On openSUSE Leap 16.0, <code>VERSION_ID</code> is <code>16.0</code>, so <code>SUSE_VERSION</code> resolves to <code>16</code> and the codename becomes <code>sles16</code>. The script then attempts to add a repository at:</p><pre><code>https://packages.cloud.google.com/yum/repos/google-cloud-ops-agent-sles16-$basearch-all</code></pre><p>The problem is that this repository simply does not exist. Google currently publishes Ops Agent packages for <code>sles12</code> and <code>sles15</code>, but there is no <code>sles16</code> repository — SLES 16 itself has not been released yet. The script makes no distinction between SLES and openSUSE Leap; it treats both as the same platform and derives the repo path purely from the major version number. The result is a <code>refresh_failed</code> error telling you to check your network connectivity and verify that you are running a supported distribution.</p><p>This leaves openSUSE Leap users on GCP in a bit of a gap. While the Ops Agent officially supports Leap 15.6, that image is no longer available on GCP — only Leap 16.0 is offered as a public image. So the supported version cannot be deployed, and the deployable version is not supported. Until Google publishes a compatible repository for Leap 16.0, there is no straightforward path to running the Ops Agent on an openSUSE instance on GCP. Keep an eye on the <a href="https://docs.cloud.google.com/stackdriver/docs/solutions/agents/ops-agent">Ops Agent supported platforms list</a> for updates.</p><p>Installing the Leap 15.6 version of the agent isn't a viable workaround due to missing libraries.</p><pre><code>Problem: 1: nothing provides 'libcrypto.so.1.1()(64bit)' needed by the to be installed google-cloud-ops-agent-2.63.0-1.sles15.x86_64</code></pre><hr><p>As always, the <a href="https://opensuse.org" rel="noreferrer">openSUSE community</a> welcomes feedback and bug reports — if you run into issues with the GCP images that are specific to the openSUSE project, report them through the <a href="https://bugzilla.opensuse.org/buglist.cgi?component=Cloud%3AImages&amp;product=openSUSE%20Distribution&amp;resolution=---" rel="noreferrer">openSUSE Bugzilla</a>.</p>]]></content:encoded>
        </item>
        <item>
            <title>How to Bring Your Own IP to Google Cloud Platform?</title>
            <link>https://sysadmin-journal.com/how-to-bring-your-own-ip-to-google-cloud-platform</link>
            <guid isPermaLink="true">https://sysadmin-journal.com/how-to-bring-your-own-ip-to-google-cloud-platform</guid>
            <pubDate>Mon, 16 Feb 2026 21:17:34 +0000</pubDate>
            <dc:creator>Ish Sookun</dc:creator>
            <category>Google Cloud Platform</category>
            <description>Learn how to bring your AFRINIC-allocated IP addresses to Google Cloud Platform using BYOIP. This guide covers the process from ROA configuration to deploying addresses on Compute Engine, with practical insights for organizations in Mauritius and Africa using the South Africa (Johannesburg) region.</description>
            <content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/byoip-cover-image-3.png" class="kg-image" alt="" loading="lazy" width="1600" height="900" srcset="/content/images/size/w600/2026/02/byoip-cover-image-3.png 600w, /content/images/size/w1000/2026/02/byoip-cover-image-3.png 1000w, /content/images/2026/02/byoip-cover-image-3.png 1600w" sizes="(min-width: 720px) 720px"></figure><p>When migrating workloads to the cloud, one of the most overlooked challenges is IP address management. Organizations that have built their reputation, whitelists, and infrastructure around specific IP addresses often face a difficult choice: abandon those trusted addresses or maintain expensive on-premises equipment just to keep using them. This challenge is particularly relevant in Mauritius, where Internet leased circuit offerings remain "unfortunately" expensive, making it impractical for small networks to manage their own IP number resources on-premises. Google Cloud's <strong>Bring Your Own IP (BYOIP)</strong> feature solves this problem, and for organizations in Africa and the Indian Ocean region holding AFRINIC allocations, it opens up exciting possibilities.</p><p>This article is based on my experience managing a Public IP Address Prefix allocated by AFRINIC to the company I work for. I'll share the practical insights gained through this process, from understanding the Regional Internet Registry ecosystem to the actual implementation steps that made our IPv4 migration to Google Cloud seamless.</p><h2 id="understanding-regional-internet-registries-and-afrinic">Understanding Regional Internet Registries and AFRINIC</h2><p>Before diving into the technical bits, it's worth understanding where your IP addresses come from. I find that many developers and even sysadmins don't fully grasp the infrastructure that makes global Internet addressing possible.</p><p>The Internet's addressing system is coordinated through a hierarchical structure. At the top sits the <a href="https://www.iana.org/" rel="noreferrer">Internet Assigned Numbers Authority (IANA)</a>, which allocates large blocks of IP addresses to five Regional Internet Registries (RIRs). These RIRs are nonprofit organizations that manage the distribution of Internet number resources within their geographic regions:</p><ul><li><a href="https://www.afrinic.net/" rel="noreferrer"><strong>AFRINIC</strong></a> (African Network Information Centre) — Africa and the Indian Ocean</li><li><a href="https://www.apnic.net/" rel="noreferrer"><strong>APNIC</strong></a> (Asia-Pacific Network Information Centre) — East, South, and Southeast Asia, plus Oceania</li><li><a href="https://www.arin.net/" rel="noreferrer"><strong>ARIN</strong></a> (American Registry for Internet Numbers) — North America, parts of the Caribbean, and Antarctica</li><li><a href="https://www.lacnic.net/" rel="noreferrer"><strong>LACNIC</strong></a> (Latin America and Caribbean Network Information Centre) — South and Central America</li><li><a href="https://www.ripe.net/" rel="noreferrer"><strong>RIPE NCC</strong></a> (Réseaux IP Européens Network Coordination Centre) — Europe, the Middle East, and Central Asia</li></ul><p>AFRINIC is headquartered in Ébène, Mauritius — yes, right here on our island! It was established in 2004 and received ICANN's final recognition in April 2005, making it the youngest of the five RIRs. As a not-for-profit organization, AFRINIC serves approximately 2,400 members across 56 countries, including Internet service providers, Internet exchange points, governments, academic institutions, and businesses that operate networks.</p><p>For a company based in Mauritius holding a <code>/24</code> Public Advertised Prefix from AFRINIC, this means you have 256 IPv4 addresses that are officially registered and recognized globally as belonging to your organization.</p><h2 id="why-bring-your-own-ip-byoip-matters-to-us-in-africa-particularly-mauritius">Why Bring Your Own IP (BYOIP) Matters to us (in Africa, particularly Mauritius)?</h2><p>Google Cloud <a href="https://cloud.google.com/blog/products/networking/bring-your-own-ip-addresses-the-secret-to-bitlys-shortened-cloud-migration" rel="noreferrer">announced BYOIP</a> in October 2019, becoming the first cloud provider to make this feature globally available across all its regions. However, here's the catch for us in Africa — while BYOIP was available globally since 2019, Google Cloud had no region on the African continent until January 2024, when the <a href="https://cloud.google.com/blog/products/infrastructure/heita-south-africa-new-cloud-region" rel="noreferrer">africa-south1 region (Johannesburg, South Africa) became operational</a>. This was a significant milestone as it marked Google Cloud's first presence in Africa.</p><p>For organizations in Mauritius and across the AFRINIC region, this meant that for nearly five years, using BYOIP required routing traffic through distant regions like <code>europe-west1</code> or <code>me-west1</code> (Tel Aviv). Not ideal for latency-sensitive applications. The Johannesburg region changes everything — we now have a Google Cloud region connected to Mauritius via three submarine cables.</p><h2 id="the-byoip-architecture-on-google-cloud">The BYOIP Architecture on Google Cloud</h2><p>Google Cloud's BYOIP implementation revolves around two key concepts: the Public Advertised Prefix (PAP) and Public Delegated Prefixes (PDPs).</p><p>A <strong>Public Advertised Prefix</strong> is your IP address block as registered with Google Cloud. This is where ownership verification happens through Route Origin Authorization (ROA) and reverse DNS validation. After verification is complete, Google configures the announcement of this prefix to the Internet, but the prefix is not advertised until it is provisioned. The provisioning process takes approximately four weeks.</p><p>A <strong>Public Delegated Prefix</strong> is a subdivision of your PAP that you configure for use in a specific scope—either a particular region or globally. You can break up your PAP into multiple PDPs to distribute addresses across different regions or use cases. These PDPs can be further divided into sub-prefixes, giving you granular control over how your IP addresses are used.</p><h2 id="setting-up-byoip">Setting Up BYOIP</h2><p>Let me walk you through how I configured our company's AFRINIC-allocated <code>/24</code> prefix on Google Cloud. The process requires coordination between your Regional Internet Registry (AFRINIC in our case) and Google Cloud.</p><h3 id="prerequisites-and-roa-configuration">Prerequisites and ROA Configuration</h3><p>Before touching the Google Cloud Console, you need to create a Route Origin Authorization (ROA) with AFRINIC. The ROA is a cryptographically signed statement that authorizes Google's Autonomous System Number (ASN) to advertise your IP prefix. Google uses ASN 396982 for BYOIP announcements.</p><p>The ROA must include your prefix range, Google's ASN, and an appropriate expiration date. This authorization is registered with the Resource Public Key Infrastructure (RPKI), which allows BGP routers worldwide to verify the legitimacy of route announcements.</p><p>I won't go into the details of creating the ROA on AFRINIC's portal here as it is purely <a href="https://afrinic.net/support/rpki/create-add-rpki-roa" rel="noreferrer">textbook instruction</a>. If you're an AFRINIC member, you should be familiar with their MyAFRINIC portal where the RPKI management is done.</p><h3 id="creating-the-public-advertised-prefix">Creating the Public Advertised Prefix</h3><p>With the ROA in place, navigate to the VPC network section in Google Cloud Console. The process begins by creating your Public Advertised Prefix.</p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/create-pap-1.jpg" class="kg-image" alt="" loading="lazy" width="698" height="239" srcset="/content/images/size/w600/2026/02/create-pap-1.jpg 600w, /content/images/2026/02/create-pap-1.jpg 698w"></figure><p>Click on "Add PAP" to start creating your Public Advertised Prefix. The first step requires you to enter details for the prefix you want to bring to Google Cloud. Notice the reminder about Route Origin Authorisation Verification — this is why we set up the ROA with AFRINIC beforehand.</p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/enter-pap-details.jpg" class="kg-image" alt="" loading="lazy" width="823" height="836" srcset="/content/images/size/w600/2026/02/enter-pap-details.jpg 600w, /content/images/2026/02/enter-pap-details.jpg 823w" sizes="(min-width: 720px) 720px"></figure><p>You'll need to provide a name for your PAP, select the IP version (IPv4 or IPv6), enter your prefix in CIDR notation, and choose the scope (Regional or Global). The form even shows an example format: <code>203.0.113.0/24</code>.</p><p>After entering your prefix details, Google Cloud will ask you to confirm ownership. Pay attention to the warning here — this information cannot be edited later. Double-check that the net range and CIDR are correct before proceeding.</p><p>The final step is validation. Google Cloud verifies your ownership through two methods: DNS validation and Route origin attestation. For DNS validation, you'll need to create a PTR record using the IP address and name provided. The ROA validation checks against the RPKI to confirm that you've authorized Google's ASN to announce your prefix.</p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/byoip-validation.jpg" class="kg-image" alt="" loading="lazy" width="837" height="736" srcset="/content/images/size/w600/2026/02/byoip-validation.jpg 600w, /content/images/2026/02/byoip-validation.jpg 837w" sizes="(min-width: 720px) 720px"></figure><p>Once you've created the PTR record and your ROA is properly configured with AFRINIC, click the checkbox to confirm and hit Validate. When both validations show "Completed", you're good to go!</p><p>After validation completes, you'll see your PAP listed with "Prefix configuration in progress" status. This is where patience comes in.</p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/byoip-config-progress.jpg" class="kg-image" alt="" loading="lazy" width="1019" height="206" srcset="/content/images/size/w600/2026/02/byoip-config-progress.jpg 600w, /content/images/size/w1000/2026/02/byoip-config-progress.jpg 1000w, /content/images/2026/02/byoip-config-progress.jpg 1019w" sizes="(min-width: 720px) 720px"></figure><p>The full provisioning process takes about four weeks. This isn't Google being slow— it's the time needed for route propagation across global BGP infrastructure to ensure stable, reliable routing.</p><p>Once provisioning completes, the status changes to "Ready to announce". This is the moment you've been waiting for!</p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/ready-to-announce.jpg" class="kg-image" alt="" loading="lazy" width="1307" height="169" srcset="/content/images/size/w600/2026/02/ready-to-announce.jpg 600w, /content/images/size/w1000/2026/02/ready-to-announce.jpg 1000w, /content/images/2026/02/ready-to-announce.jpg 1307w" sizes="(min-width: 720px) 720px"></figure><p>Click on the three-dot menu under Actions and select "Announce" to start advertising your prefix from Google's network.</p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/announce.jpg" class="kg-image" alt="" loading="lazy" width="288" height="283"></figure><p>After announcing, your prefix will be advertised to the Internet from Google Cloud's infrastructure. You can now create Public Delegated Prefixes and start using your BYOIP addresses.</p><h3 id="creating-public-delegated-prefixes">Creating Public Delegated Prefixes</h3><p>While waiting for your PAP to provision, you can plan how to divide your address space. For a <code>/24</code> prefix, you might allocate addresses for:</p><ul><li>A <code>/26</code> for your primary regional workloads</li><li>A <code>/27</code> for global load balancers</li><li>A <code>/28</code> for development and testing</li><li>Reserve the remaining space for future use</li></ul><p>Each Public Delegated Prefix needs a scope assignment. Regional scope is required if you want to use addresses with Compute Engine VMs or regional load balancers. Global scope is needed for global Application Load Balancers but requires your project to be on an allowlist.</p><p>Oh... and about region selection. My usual trick for obtaining the best latency to a region is to understand how our submarine fiber cables connect us to the rest of the world. With the <code>africa-south1</code> region now available in Johannesburg, this is the obvious choice for organizations in Mauritius and across the AFRINIC region. The region is connected to Europe via the <a href="https://blog.google/intl/en-africa/company-news/inside-google/equianos-next-stop-is-in-nigeria/" rel="noreferrer">Equiano subsea cable</a> and to Mauritius via the SAFE, METISS and T3 subsea cables.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://sysadmin-journal.com/content/images/2026/02/mauritius-south-africa-submarine-cables.jpg" class="kg-image" alt="" loading="lazy" width="1055" height="752" srcset="/content/images/size/w600/2026/02/mauritius-south-africa-submarine-cables.jpg 600w, /content/images/size/w1000/2026/02/mauritius-south-africa-submarine-cables.jpg 1000w, /content/images/2026/02/mauritius-south-africa-submarine-cables.jpg 1055w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Map source: </span><a href="https://www2.telegeography.com/?__hstc=196094579.01e45c8d6f58c0cc4af8b54b80cc8fd8.1771274031194.1771274031194.1771274031194.1&amp;__hssc=196094579.1.1771274031194&amp;__hsfp=e9958329393ef73a8608d92d0cb7bf8d" target="_blank" rel="noopener noreferrer"><span style="white-space: pre-wrap;">TeleGeography</span></a></figcaption></figure><h3 id="delegating-sub-prefixes-to-projects">Delegating Sub-Prefixes to Projects</h3><p>Once your PDPs are provisioned, you can delegate sub-prefixes to specific projects within your organization. This is where BYOIP shines for enterprise environments—you can maintain centralized control over your IP address space while allowing individual teams to use addresses in their projects.</p><p>From the PDP details page, you can see all the information about your Public Delegated Prefix — the status (hopefully "Announced to Internet" ✅), the prefix details, scope, and any existing sub-delegates. Notice in the screenshot below that the scope is set to <code>africa-south1</code>.</p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/pdp-details.jpg" class="kg-image" alt="" loading="lazy" width="717" height="801" srcset="/content/images/size/w600/2026/02/pdp-details.jpg 600w, /content/images/2026/02/pdp-details.jpg 717w"></figure><p>To create a sub-prefix, click on "Create sub-prefix". You'll need to provide a name, select the prefix length (how many addresses you want to delegate), choose the specific IP range, and assign it to a project.</p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/create-delegation-1.jpg" class="kg-image" alt="" loading="lazy" width="842" height="611" srcset="/content/images/size/w600/2026/02/create-delegation-1.jpg 600w, /content/images/2026/02/create-delegation-1.jpg 842w" sizes="(min-width: 720px) 720px"></figure><p>In this example, I'm creating a <code>/26</code> sub-prefix (64 addresses) and assigning it to the "LSL IT" project. You can create multiple sub-prefixes of different sizes depending on your needs.</p><p>Once the sub-prefix is delegated to a project, you can create addresses from that pool. The "Create addresses" dialog lets you permanently delegate specific addresses to the project.</p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/create-addresses.jpg" class="kg-image" alt="" loading="lazy" width="572" height="368"></figure><p>Here I'm creating 2 addresses from a <code>/31</code> block. Once created, these addresses become available for use with Compute Engine instances, load balancers, and other Google Cloud resources within that project.</p><h3 id="using-byoip-addresses-with-compute-engine">Using BYOIP Addresses with Compute Engine</h3><p>With your addresses provisioned and delegated, using them with Compute Engine instances is straightforward. When creating a VM instance, navigate to the Networking section and expand the network interface settings. Under "External IPv4 address", you'll see your BYOIP addresses listed alongside the usual options.</p><figure class="kg-card kg-image-card"><img src="https://sysadmin-journal.com/content/images/2026/02/create-vm-instance.jpg" class="kg-image" alt="" loading="lazy" width="804" height="692" srcset="/content/images/size/w600/2026/02/create-vm-instance.jpg 600w, /content/images/2026/02/create-vm-instance.jpg 804w" sizes="(min-width: 720px) 720px"></figure><p>Notice in the screenshot that the BYOIP addresses (the <code>102.x.x.x</code> addresses) appear in the dropdown with "Premium tier" — these are your AFRINIC-allocated addresses now usable directly on Google Cloud! Also notice the machine is being created in <code>africa-south1-a</code> and running openSUSE Leap 16.0. 😉</p><p>The address behaves exactly like any Google-provided address — you can assign it to VM instances, use it with NAT gateways (with some limitations), and include it in firewall rules. The key differences are that your BYOIP addresses are available only to your organization, and there are no charges for idle or in-use addresses.</p>]]></content:encoded>
        </item>
        <item>
            <title>Google Cloud Workload Identity Federation: A Guide to Keyless Authentication for Multi-Cloud Environments</title>
            <link>https://sysadmin-journal.com/google-cloud-workload-identity-federation-a-guide-to-keyless-authentication-for-multi-cloud-environments</link>
            <guid isPermaLink="true">https://sysadmin-journal.com/google-cloud-workload-identity-federation-a-guide-to-keyless-authentication-for-multi-cloud-environments</guid>
            <pubDate>Wed, 28 Jan 2026 16:58:07 +0000</pubDate>
            <dc:creator>Ish Sookun</dc:creator>
            <category>Google Cloud Platform</category>
            <description>Learn how to eliminate service account keys and implement keyless authentication for GitHub Actions using Google Cloud Workload Identity Federation with Cloud SQL.</description>
            <content:encoded><![CDATA[
<!--kg-card-begin: html-->
<img src="https://sysadmin-journal.com/content/images/2026/01/workload-identity-federation-cover-2026.jpg" />
<!--kg-card-end: html-->
<h2 id="introduction">Introduction</h2><p>In today's multi-cloud landscape, managing authentication credentials across different cloud providers has become a significant security challenge. Traditional approaches using long-lived service account keys pose serious risks: they can be leaked, stolen, or compromised, and they're difficult to rotate and audit effectively.</p><p>Google Cloud Workload Identity Federation solves this problem by enabling keyless authentication. It allows workloads running outside Google Cloud—such as AWS EC2 instances, Azure VMs, GitHub Actions, or on-premises systems—to securely access Google Cloud resources without managing service account keys.</p><p>In this comprehensive guide, we'll walk through a real-world scenario and implement Workload Identity Federation step-by-step.</p><h2 id="the-scenario-multi-cloud-cicd-pipeline">The Scenario: Multi-Cloud CI/CD Pipeline</h2><p><strong>Company:</strong> TechFlow Inc., a fintech startup<br><strong>Challenge:</strong> Their application runs on Google Cloud (Cloud Run, Cloud SQL PostgreSQL), but their CI/CD pipelines run on GitHub Actions. They need to deploy applications and run database migrations on Cloud SQL from GitHub workflows without storing service account keys in GitHub secrets.</p><p><strong>Current Problem:</strong></p><ul><li>Service account keys stored in GitHub Secrets</li><li>Keys need manual rotation every 90 days</li><li>Risk of key exposure if repository is compromised</li><li>Compliance concerns about credential management</li><li>Database credentials management complexity</li></ul><p><strong>Goal:</strong> Implement Workload Identity Federation to enable GitHub Actions to authenticate to Google Cloud using OIDC tokens, eliminating the need for service account keys entirely.</p><h2 id="architecture-overview">Architecture Overview</h2><p>Here's how Workload Identity Federation works:</p><ol><li><strong>GitHub Actions</strong> generates an OIDC token containing claims about the workflow (repository, branch, actor)</li><li><strong>Workload Identity Pool</strong> in Google Cloud validates the token</li><li><strong>Workload Identity Provider</strong> maps the external identity to a Google Cloud service account</li><li>GitHub Actions receives short-lived Google Cloud credentials</li><li>Workflow uses credentials to access Google Cloud resources</li></ol><pre><code>┌─────────────────┐         ┌──────────────────────────┐
│  GitHub Actions │         │  Google Cloud            │
│                 │         │                          │
│  1. Get OIDC    │────────▶│  2. Workload Identity    │
│     Token       │         │     Pool validates token │
│                 │         │                          │
│  4. Receive     │◀────────│  3. Map to Service       │
│     GCP Token   │         │     Account              │
│                 │         │                          │
│  5. Access GCP  │────────▶│  6. Cloud Run, Cloud SQL │
│     Resources   │         │     (authorized access)  │
└─────────────────┘         └──────────────────────────┘
</code></pre><h2 id="prerequisites">Prerequisites</h2><p>Before we begin, ensure you have:</p><ol><li>A Google Cloud project with billing enabled</li><li><code>gcloud</code> CLI installed and configured</li><li>Appropriate IAM permissions:<ul><li><code>roles/iam.workloadIdentityPoolAdmin</code></li><li><code>roles/iam.serviceAccountAdmin</code></li><li><code>roles/resourcemanager.projectIamAdmin</code></li></ul></li><li>A GitHub repository where you'll run workflows</li></ol><h2 id="enable-required-apis">Enable Required APIs</h2><p>First, enable the necessary Google Cloud APIs:</p><pre><code class="language-bash"># Set your project ID
export PROJECT_ID="your-project-id"
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")

# Enable required APIs
gcloud services enable iamcredentials.googleapis.com \
    cloudresourcemanager.googleapis.com \
    sts.googleapis.com \
    --project=$PROJECT_ID
</code></pre><h2 id="create-a-service-account">Create a Service Account</h2><p>Create a service account that will be impersonated by GitHub Actions:</p><pre><code class="language-bash"># Create service account
gcloud iam service-accounts create github-actions-sa \
    --display-name="GitHub Actions Service Account" \
    --description="Service account for GitHub Actions workflows" \
    --project=$PROJECT_ID

# Store the service account email
export SA_EMAIL="github-actions-sa@${PROJECT_ID}.iam.gserviceaccount.com"
</code></pre><h2 id="grant-necessary-permissions-to-service-account">Grant Necessary Permissions to Service Account</h2><p>Grant the service account permissions to perform required operations. For our scenario, we need Cloud Run deployment and Cloud SQL access:</p><pre><code class="language-bash"># Cloud Run Admin (to deploy services)
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/run.admin"

# Service Account User (to deploy as a service account)
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/iam.serviceAccountUser"

# Cloud SQL Client (to connect to Cloud SQL instances)
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/cloudsql.client"

# Cloud SQL Admin (to manage instances and run migrations)
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/cloudsql.admin"

# Storage Admin (for artifact storage)
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/storage.admin"

# Secret Manager Secret Accessor (to access database credentials)
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/secretmanager.secretAccessor"
</code></pre><h2 id="create-workload-identity-pool">Create Workload Identity Pool</h2><p>The Workload Identity Pool is a container for managing external identities:</p><pre><code class="language-bash"># Create the workload identity pool
gcloud iam workload-identity-pools create "github-pool" \
    --location="global" \
    --display-name="GitHub Actions Pool" \
    --description="Identity pool for GitHub Actions workflows" \
    --project=$PROJECT_ID

# Verify creation
gcloud iam workload-identity-pools describe "github-pool" \
    --location="global" \
    --project=$PROJECT_ID
</code></pre><h2 id="create-workload-identity-provider">Create Workload Identity Provider</h2><p>The provider configures how external tokens are validated and mapped:</p><pre><code class="language-bash"># Create the OIDC provider for GitHub Actions
gcloud iam workload-identity-pools providers create-oidc "github-provider" \
    --location="global" \
    --workload-identity-pool="github-pool" \
    --display-name="GitHub Actions Provider" \
    --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
    --attribute-condition="assertion.repository_owner=='your-github-org'" \
    --issuer-uri="https://token.actions.githubusercontent.com" \
    --project=$PROJECT_ID
</code></pre><h3 id="understanding-attribute-mapping">Understanding Attribute Mapping</h3><p>The <code>--attribute-mapping</code> parameter is crucial. It maps claims from the GitHub OIDC token to Google Cloud attributes:</p><ul><li><code>google.subject=assertion.sub</code>: Maps the subject claim (unique identifier for the workflow)</li><li><code>attribute.actor=assertion.actor</code>: Maps the GitHub user who triggered the workflow</li><li><code>attribute.repository=assertion.repository</code>: Maps the repository name</li><li><code>attribute.repository_owner=assertion.repository_owner</code>: Maps the repository owner</li></ul><h3 id="understanding-attribute-conditions">Understanding Attribute Conditions</h3><p>The <code>--attribute-condition</code> parameter adds security by restricting which external identities can authenticate:</p><pre><code class="language-bash"># Only allow specific organization
assertion.repository_owner=='techflow-inc'

# Only allow specific repository
assertion.repository=='techflow-inc/main-app'

# Allow multiple repositories
assertion.repository in ['techflow-inc/app1', 'techflow-inc/app2']

# Combine conditions
assertion.repository_owner=='techflow-inc' &amp;&amp; assertion.repository.startsWith('techflow-inc/prod-')
</code></pre><p><strong>Important:</strong> Replace <code>'your-github-org'</code> with your actual GitHub organization or username.</p><h2 id="grant-service-account-impersonation-permission">Grant Service Account Impersonation Permission</h2><p>Allow the Workload Identity Pool to impersonate your service account. This is where you define precisely which external identities can impersonate the service account:</p><pre><code class="language-bash"># For a specific repository
gcloud iam service-accounts add-iam-policy-binding "${SA_EMAIL}" \
    --role="roles/iam.workloadIdentityUser" \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.repository/your-github-username/your-repo-name" \
    --project=$PROJECT_ID
</code></pre><h3 id="advanced-iam-binding-examples">Advanced IAM Binding Examples</h3><p><strong>Option 1: Allow any repository in your organization</strong></p><pre><code class="language-bash">gcloud iam service-accounts add-iam-policy-binding "${SA_EMAIL}" \
    --role="roles/iam.workloadIdentityUser" \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.repository_owner/your-github-org" \
    --project=$PROJECT_ID
</code></pre><p><strong>Option 2: Allow multiple specific repositories</strong></p><pre><code class="language-bash"># First repository
gcloud iam service-accounts add-iam-policy-binding "${SA_EMAIL}" \
    --role="roles/iam.workloadIdentityUser" \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.repository/your-org/repo-1" \
    --project=$PROJECT_ID

# Second repository
gcloud iam service-accounts add-iam-policy-binding "${SA_EMAIL}" \
    --role="roles/iam.workloadIdentityUser" \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.repository/your-org/repo-2" \
    --project=$PROJECT_ID
</code></pre><p><strong>Option 3: Allow specific branch in a repository</strong></p><pre><code class="language-bash"># This requires adding branch to attribute mapping first
gcloud iam workload-identity-pools providers update-oidc "github-provider" \
    --location="global" \
    --workload-identity-pool="github-pool" \
    --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner,attribute.ref=assertion.ref" \
    --project=$PROJECT_ID

# Then bind for specific branch
gcloud iam service-accounts add-iam-policy-binding "${SA_EMAIL}" \
    --role="roles/iam.workloadIdentityUser" \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.ref/refs/heads/main" \
    --project=$PROJECT_ID
</code></pre><h2 id="get-the-workload-identity-provider-resource-name">Get the Workload Identity Provider Resource Name</h2><p>You'll need this for your GitHub Actions workflow:</p><pre><code class="language-bash">gcloud iam workload-identity-pools providers describe "github-provider" \
    --location="global" \
    --workload-identity-pool="github-pool" \
    --project=$PROJECT_ID \
    --format="value(name)"
</code></pre><p>The output will look like:</p><pre><code>projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider
</code></pre><p>Save this value—you'll use it in your GitHub workflow.</p><h2 id="set-up-cloud-sql-instance-and-secrets">Set Up Cloud SQL Instance and Secrets</h2><p>Before configuring the workflow, set up your Cloud SQL instance and store credentials securely:</p><h3 id="create-cloud-sql-instance">Create Cloud SQL Instance</h3><pre><code class="language-bash"># Create a PostgreSQL instance
gcloud sql instances create production-db \
    --database-version=POSTGRES_15 \
    --tier=db-custom-2-7680 \
    --region=us-central1 \
    --network=projects/$PROJECT_ID/global/networks/default \
    --no-assign-ip \
    --database-flags=cloudsql.iam_authentication=on \
    --project=$PROJECT_ID

# Create a database
gcloud sql databases create production \
    --instance=production-db \
    --project=$PROJECT_ID

# Create a PostgreSQL user
gcloud sql users create dbuser \
    --instance=production-db \
    --password=STRONG_PASSWORD_HERE \
    --project=$PROJECT_ID
</code></pre><h3 id="store-database-password-in-secret-manager">Store Database Password in Secret Manager</h3><pre><code class="language-bash"># Enable Secret Manager API
gcloud services enable secretmanager.googleapis.com --project=$PROJECT_ID

# Create secret for database password
echo -n "STRONG_PASSWORD_HERE" | gcloud secrets create db-password \
    --data-file=- \
    --replication-policy="automatic" \
    --project=$PROJECT_ID

# Grant service account access to the secret
gcloud secrets add-iam-policy-binding db-password \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/secretmanager.secretAccessor" \
    --project=$PROJECT_ID
</code></pre><h3 id="alternative-use-iam-database-authentication-recommended">Alternative: Use IAM Database Authentication (Recommended)</h3><p>For enhanced security, use IAM authentication instead of passwords:</p><pre><code class="language-bash"># Create an IAM database user
gcloud sql users create github-actions-sa@$PROJECT_ID.iam \
    --instance=production-db \
    --type=CLOUD_IAM_SERVICE_ACCOUNT \
    --project=$PROJECT_ID

# Grant the service account Cloud SQL Client role
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/cloudsql.client"
</code></pre><p>Then connect using IAM authentication in your workflow:</p><pre><code class="language-bash"># Get an IAM token for database authentication
gcloud sql generate-login-token

# Or use Cloud SQL Proxy which handles this automatically
./cloud_sql_proxy -instances=$INSTANCE_CONNECTION_NAME=tcp:5432 \
    --auto-iam-authn
</code></pre><h3 id="configure-cloud-sql-for-private-ip-best-practice">Configure Cloud SQL for Private IP (Best Practice)</h3><pre><code class="language-bash"># Enable Private Service Access
gcloud compute addresses create google-managed-services-default \
    --global \
    --purpose=VPC_PEERING \
    --prefix-length=16 \
    --network=default \
    --project=$PROJECT_ID

# Create private connection
gcloud services vpc-peerings connect \
    --service=servicenetworking.googleapis.com \
    --ranges=google-managed-services-default \
    --network=default \
    --project=$PROJECT_ID

# Create instance with private IP only
gcloud sql instances create production-db-private \
    --database-version=POSTGRES_15 \
    --tier=db-custom-2-7680 \
    --region=us-central1 \
    --network=projects/$PROJECT_ID/global/networks/default \
    --no-assign-ip \
    --database-flags=cloudsql.iam_authentication=on \
    --project=$PROJECT_ID
</code></pre><h2 id="configure-github-actions-workflow">Configure GitHub Actions Workflow</h2><p>Now, create a GitHub Actions workflow that uses Workload Identity Federation:</p><pre><code class="language-yaml">name: Deploy to Cloud Run and Run Migrations

on:
  push:
    branches:
      - main

# Required for OIDC token generation
permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      # Authenticate to Google Cloud using Workload Identity Federation
      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v2'
        with:
          workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
          service_account: 'github-actions-sa@your-project-id.iam.gserviceaccount.com'
          token_format: 'access_token'

      # Setup Cloud SDK
      - name: 'Set up Cloud SDK'
        uses: 'google-github-actions/setup-gcloud@v2'

      # Install Cloud SQL Proxy
      - name: 'Install Cloud SQL Proxy'
        run: |
          wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O cloud_sql_proxy
          chmod +x cloud_sql_proxy

      # Build and push Docker image
      - name: 'Build and Push to Artifact Registry'
        run: |
          gcloud auth configure-docker us-central1-docker.pkg.dev
          docker build -t us-central1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/apps/myapp:${{ github.sha }} .
          docker push us-central1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/apps/myapp:${{ github.sha }}

      # Run database migrations using Cloud SQL Proxy
      - name: 'Run Database Migrations'
        env:
          INSTANCE_CONNECTION_NAME: ${{ secrets.GCP_PROJECT_ID }}:us-central1:production-db
        run: |
          # Start Cloud SQL Proxy in background
          ./cloud_sql_proxy -instances=$INSTANCE_CONNECTION_NAME=tcp:5432 &amp;
          PROXY_PID=$!
          
          # Wait for proxy to be ready
          sleep 5
          
          # Get database password from Secret Manager
          DB_PASSWORD=$(gcloud secrets versions access latest --secret="db-password")
          
          # Run migrations (example using Flyway)
          export PGPASSWORD=$DB_PASSWORD
          psql -h 127.0.0.1 -p 5432 -U dbuser -d production -f migrations/001_initial_schema.sql
          
          # Or using a migration tool like golang-migrate
          # migrate -path=./migrations -database "postgresql://dbuser:${DB_PASSWORD}@127.0.0.1:5432/production?sslmode=disable" up
          
          # Kill the proxy
          kill $PROXY_PID

      # Deploy to Cloud Run with Cloud SQL connection
      - name: 'Deploy to Cloud Run'
        run: |
          gcloud run deploy myapp \
            --image=us-central1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/apps/myapp:${{ github.sha }} \
            --region=us-central1 \
            --platform=managed \
            --allow-unauthenticated \
            --add-cloudsql-instances=${{ secrets.GCP_PROJECT_ID }}:us-central1:production-db \
            --set-env-vars="DB_USER=dbuser,DB_NAME=production" \
            --set-secrets="DB_PASSWORD=db-password:latest"

      # Verify deployment and database connectivity
      - name: 'Health Check'
        run: |
          # Get the service URL
          SERVICE_URL=$(gcloud run services describe myapp --region=us-central1 --format='value(status.url)')
          
          # Check health endpoint
          curl -f ${SERVICE_URL}/health || exit 1
          
          # Verify database connection through the app
          curl -f ${SERVICE_URL}/db/ping || exit 1
</code></pre><h3 id="key-workflow-elements">Key Workflow Elements</h3><ol><li><strong>Permissions Block</strong>: The <code>id-token: write</code> permission is crucial—it allows GitHub to generate OIDC tokens.</li><li><strong>Auth Action</strong>: The <code>google-github-actions/auth@v2</code> action handles the token exchange automatically.</li><li><strong>No Secrets Required</strong>: Notice we don't store any service account keys in GitHub Secrets, only the project ID.</li><li><strong>Cloud SQL Proxy</strong>: The proxy creates a secure tunnel to Cloud SQL, handling authentication via the service account.</li><li><strong>Secret Manager Integration</strong>: Database passwords are stored securely and accessed at runtime.</li></ol><h2 id="advanced-cloud-sql-workflow-patterns">Advanced Cloud SQL Workflow Patterns</h2><h3 id="pattern-1-using-iam-authentication-most-secure">Pattern 1: Using IAM Authentication (Most Secure)</h3><pre><code class="language-yaml">name: Deploy with IAM Database Authentication

on:
  push:
    branches:
      - main

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4

      - id: 'auth'
        uses: 'google-github-actions/auth@v2'
        with:
          workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
          service_account: 'github-actions-sa@your-project-id.iam.gserviceaccount.com'

      - uses: 'google-github-actions/setup-gcloud@v2'

      # Use Cloud SQL Auth Proxy with IAM authentication
      - name: 'Run Migrations with IAM Auth'
        run: |
          # Install Cloud SQL Auth Proxy
          curl -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.8.0/cloud-sql-proxy.linux.amd64
          chmod +x cloud-sql-proxy
          
          # Start proxy with automatic IAM authentication
          ./cloud-sql-proxy --auto-iam-authn ${{ secrets.GCP_PROJECT_ID }}:us-central1:production-db &amp;
          PROXY_PID=$!
          sleep 5
          
          # Connect using IAM - no password needed!
          psql "host=127.0.0.1 port=5432 sslmode=disable user=github-actions-sa@${{ secrets.GCP_PROJECT_ID }}.iam dbname=production" \
            -c "SELECT version();"
          
          # Run migrations
          psql "host=127.0.0.1 port=5432 sslmode=disable user=github-actions-sa@${{ secrets.GCP_PROJECT_ID }}.iam dbname=production" \
            -f migrations/schema.sql
          
          kill $PROXY_PID
</code></pre><h3 id="pattern-2-using-flyway-for-database-migrations">Pattern 2: Using Flyway for Database Migrations</h3><pre><code class="language-yaml">      - name: 'Run Flyway Migrations'
        env:
          INSTANCE_CONNECTION_NAME: ${{ secrets.GCP_PROJECT_ID }}:us-central1:production-db
        run: |
          # Start Cloud SQL Proxy
          ./cloud_sql_proxy -instances=$INSTANCE_CONNECTION_NAME=tcp:5432 &amp;
          PROXY_PID=$!
          sleep 5
          
          # Get database password
          DB_PASSWORD=$(gcloud secrets versions access latest --secret="db-password")
          
          # Install Flyway
          wget -qO- https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/10.4.1/flyway-commandline-10.4.1-linux-x64.tar.gz | tar xvz
          
          # Configure Flyway
          ./flyway-10.4.1/flyway \
            -url=jdbc:postgresql://127.0.0.1:5432/production \
            -user=dbuser \
            -password=$DB_PASSWORD \
            -locations=filesystem:./sql/migrations \
            migrate
          
          # Verify migration status
          ./flyway-10.4.1/flyway \
            -url=jdbc:postgresql://127.0.0.1:5432/production \
            -user=dbuser \
            -password=$DB_PASSWORD \
            info
          
          kill $PROXY_PID
</code></pre><h3 id="pattern-3-blue-green-database-migrations-with-rollback">Pattern 3: Blue-Green Database Migrations with Rollback</h3><pre><code class="language-yaml">      - name: 'Blue-Green Migration with Rollback'
        run: |
          # Start proxy
          ./cloud_sql_proxy -instances=${{ secrets.GCP_PROJECT_ID }}:us-central1:production-db=tcp:5432 &amp;
          PROXY_PID=$!
          sleep 5
          
          DB_PASSWORD=$(gcloud secrets versions access latest --secret="db-password")
          export PGPASSWORD=$DB_PASSWORD
          
          # Create backup before migration
          pg_dump -h 127.0.0.1 -p 5432 -U dbuser production &gt; backup_$(date +%Y%m%d_%H%M%S).sql
          
          # Upload backup to Cloud Storage
          gcloud storage cp backup_*.sql gs://${{ secrets.GCP_PROJECT_ID }}-db-backups/
          
          # Create test database from production
          psql -h 127.0.0.1 -p 5432 -U dbuser -d postgres \
            -c "CREATE DATABASE production_test WITH TEMPLATE production;"
          
          # Test migrations on copy
          if psql -h 127.0.0.1 -p 5432 -U dbuser -d production_test -f migrations/schema.sql; then
            echo "✓ Migration test successful"
            
            # Apply to production
            psql -h 127.0.0.1 -p 5432 -U dbuser -d production -f migrations/schema.sql
            
            # Verify production migration
            if psql -h 127.0.0.1 -p 5432 -U dbuser -d production -c "SELECT COUNT(*) FROM users;" &gt; /dev/null; then
              echo "✓ Production migration successful"
            else
              echo "✗ Production migration failed, restoring backup"
              psql -h 127.0.0.1 -p 5432 -U dbuser -d production &lt; backup_*.sql
              exit 1
            fi
            
            # Cleanup test database
            psql -h 127.0.0.1 -p 5432 -U dbuser -d postgres \
              -c "DROP DATABASE production_test;"
          else
            echo "✗ Migration test failed, aborting"
            exit 1
          fi
          
          kill $PROXY_PID
</code></pre><h3 id="pattern-4-multi-region-database-deployment">Pattern 4: Multi-Region Database Deployment</h3><pre><code class="language-yaml">jobs:
  deploy-multi-region:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        region: 
          - name: us-central1
            db: production-db-us
          - name: europe-west1
            db: production-db-eu
          - name: asia-southeast1
            db: production-db-asia
    
    steps:
      - uses: actions/checkout@v4
      
      - id: 'auth'
        uses: 'google-github-actions/auth@v2'
        with:
          workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
          service_account: 'github-actions-sa@your-project-id.iam.gserviceaccount.com'
      
      - uses: 'google-github-actions/setup-gcloud@v2'
      
      - name: 'Deploy to ${{ matrix.region.name }}'
        run: |
          # Install Cloud SQL Proxy
          curl -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.8.0/cloud-sql-proxy.linux.amd64
          chmod +x cloud-sql-proxy
          
          # Start proxy for this region
          ./cloud-sql-proxy ${{ secrets.GCP_PROJECT_ID }}:${{ matrix.region.name }}:${{ matrix.region.db }} &amp;
          PROXY_PID=$!
          sleep 5
          
          # Get region-specific password
          DB_PASSWORD=$(gcloud secrets versions access latest --secret="db-password-${{ matrix.region.name }}")
          export PGPASSWORD=$DB_PASSWORD
          
          # Run common migrations
          psql -h 127.0.0.1 -p 5432 -U dbuser -d production \
            -f migrations/common/schema.sql
          
          # Run region-specific configurations
          psql -h 127.0.0.1 -p 5432 -U dbuser -d production \
            -v region=${{ matrix.region.name }} \
            -f migrations/regional/config.sql
          
          kill $PROXY_PID
          
          # Deploy Cloud Run in this region
          gcloud run deploy myapp-${{ matrix.region.name }} \
            --image=us-central1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/apps/myapp:${{ github.sha }} \
            --region=${{ matrix.region.name }} \
            --add-cloudsql-instances=${{ secrets.GCP_PROJECT_ID }}:${{ matrix.region.name }}:${{ matrix.region.db }} \
            --set-env-vars="REGION=${{ matrix.region.name }},DB_NAME=production"
</code></pre><h2 id="advanced-configurationmultiple-environments">Advanced Configuration - Multiple Environments</h2><p>For production deployments, you'll likely need different service accounts for different environments:</p><pre><code class="language-bash"># Create production service account
gcloud iam service-accounts create github-actions-prod-sa \
    --display-name="GitHub Actions Production SA" \
    --project=$PROJECT_ID

export PROD_SA_EMAIL="github-actions-prod-sa@${PROJECT_ID}.iam.gserviceaccount.com"

# Bind only to main branch
gcloud iam service-accounts add-iam-policy-binding "${PROD_SA_EMAIL}" \
    --role="roles/iam.workloadIdentityUser" \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.ref/refs/heads/main" \
    --project=$PROJECT_ID

# Create staging service account
gcloud iam service-accounts create github-actions-staging-sa \
    --display-name="GitHub Actions Staging SA" \
    --project=$PROJECT_ID

export STAGING_SA_EMAIL="github-actions-staging-sa@${PROJECT_ID}.iam.gserviceaccount.com"

# Bind to develop branch
gcloud iam service-accounts add-iam-policy-binding "${STAGING_SA_EMAIL}" \
    --role="roles/iam.workloadIdentityUser" \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.ref/refs/heads/develop" \
    --project=$PROJECT_ID
</code></pre><p>Then in your workflow:</p><pre><code class="language-yaml">- id: 'auth'
  name: 'Authenticate to Google Cloud'
  uses: 'google-github-actions/auth@v2'
  with:
    workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
    service_account: ${{ github.ref == 'refs/heads/main' &amp;&amp; 'github-actions-prod-sa@project.iam.gserviceaccount.com' || 'github-actions-staging-sa@project.iam.gserviceaccount.com' }}
</code></pre><h2 id="verification-and-testing">Verification and Testing</h2><h3 id="test-1-verify-token-exchange">Test 1: Verify Token Exchange</h3><p>Create a simple test workflow:</p><pre><code class="language-yaml">name: Test Workload Identity

on:
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - id: 'auth'
        uses: 'google-github-actions/auth@v2'
        with:
          workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
          service_account: 'github-actions-sa@your-project-id.iam.gserviceaccount.com'
      
      - name: 'Verify Authentication'
        run: |
          gcloud auth list
          gcloud projects describe ${{ secrets.GCP_PROJECT_ID }}
</code></pre><h3 id="test-2-audit-logs">Test 2: Audit Logs</h3><p>Check Cloud Audit Logs to verify authentication:</p><pre><code class="language-bash">gcloud logging read "protoPayload.methodName=GenerateAccessToken" \
    --limit=10 \
    --format=json \
    --project=$PROJECT_ID
</code></pre><p>Look for entries showing GitHub Actions successfully exchanging tokens.</p><h2 id="security-best-practices">Security Best Practices</h2><h3 id="1-principle-of-least-privilege">1. Principle of Least Privilege</h3><p>Grant only the minimum permissions needed:</p><pre><code class="language-bash"># Instead of broad roles, use specific permissions
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/run.developer"  # Instead of run.admin

gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/cloudsql.client"  # Instead of cloudsql.admin for read-only access
</code></pre><h3 id="2-use-attribute-conditions-strictly">2. Use Attribute Conditions Strictly</h3><p>Always restrict which external identities can authenticate:</p><pre><code class="language-bash"># Bad: Too permissive
--attribute-condition=""

# Good: Specific organization
--attribute-condition="assertion.repository_owner=='your-org'"

# Better: Specific repositories and branches
--attribute-condition="assertion.repository_owner=='your-org' &amp;&amp; assertion.repository.startsWith('your-org/prod-') &amp;&amp; assertion.ref=='refs/heads/main'"
</code></pre><h3 id="3-separate-service-accounts-by-environment">3. Separate Service Accounts by Environment</h3><p>Never use the same service account for production and staging:</p><pre><code>✗ github-actions-sa → All environments
✓ github-actions-prod-sa → Production only
✓ github-actions-staging-sa → Staging only
✓ github-actions-dev-sa → Development only
</code></pre><h3 id="4-enable-audit-logging">4. Enable Audit Logging</h3><p>Ensure Data Access audit logs are enabled:</p><pre><code class="language-bash"># Create audit config
cat &gt; audit-config.yaml &lt;&lt;EOF
auditConfigs:
- auditLogConfigs:
  - logType: ADMIN_READ
  - logType: DATA_READ
  - logType: DATA_WRITE
  service: iam.googleapis.com
EOF

gcloud projects set-iam-policy $PROJECT_ID audit-config.yaml
</code></pre><h3 id="5-monitor-token-exchange-activity">5. Monitor Token Exchange Activity</h3><p>Set up monitoring alerts for suspicious activity:</p><pre><code class="language-bash"># Create a log-based metric
gcloud logging metrics create workload-identity-failures \
    --description="Failed workload identity token exchanges" \
    --log-filter='protoPayload.methodName="GenerateAccessToken"
    AND protoPayload.status.code!=0'

# Create alert policy
gcloud alpha monitoring policies create \
    --notification-channels=CHANNEL_ID \
    --display-name="Workload Identity Failures" \
    --condition-display-name="High failure rate" \
    --condition-threshold-value=5 \
    --condition-threshold-duration=60s
</code></pre><h3 id="6-cloud-sql-security-best-practices">6. Cloud SQL Security Best Practices</h3><p>Implement these security measures for Cloud SQL:</p><pre><code class="language-bash"># Enable automatic backups
gcloud sql instances patch production-db \
    --backup-start-time=03:00 \
    --enable-bin-log \
    --project=$PROJECT_ID

# Enable point-in-time recovery
gcloud sql instances patch production-db \
    --enable-point-in-time-recovery \
    --project=$PROJECT_ID

# Require SSL for all connections
gcloud sql instances patch production-db \
    --require-ssl \
    --project=$PROJECT_ID

# Enable database flags for security
gcloud sql instances patch production-db \
    --database-flags=\
cloudsql.iam_authentication=on,\
log_checkpoints=on,\
log_connections=on,\
log_disconnections=on,\
log_lock_waits=on,\
log_statement=ddl,\
log_min_duration_statement=1000 \
    --project=$PROJECT_ID

# Restrict network access (private IP only)
gcloud sql instances patch production-db \
    --no-assign-ip \
    --network=projects/$PROJECT_ID/global/networks/default \
    --project=$PROJECT_ID
</code></pre><p><strong>Database User Security:</strong></p><pre><code class="language-sql">-- Revoke public schema privileges
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
REVOKE ALL ON DATABASE production FROM PUBLIC;

-- Create read-only role for analytics
CREATE ROLE analytics_readonly;
GRANT CONNECT ON DATABASE production TO analytics_readonly;
GRANT USAGE ON SCHEMA public TO analytics_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO analytics_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO analytics_readonly;

-- Create application role with limited privileges
CREATE ROLE app_user;
GRANT CONNECT ON DATABASE production TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;

-- Assign IAM service account to role
GRANT app_user TO "github-actions-sa@project-id.iam";
</code></pre><p><strong>Automated Security Scanning:</strong></p><pre><code class="language-yaml"># Add to your GitHub workflow
- name: 'Scan Database for Security Issues'
  run: |
    # Check for overly permissive grants
    psql -h 127.0.0.1 -p 5432 -U dbuser -d production &lt;&lt;EOF
    SELECT 
      grantee, 
      string_agg(privilege_type, ', ') as privileges,
      table_schema,
      table_name
    FROM information_schema.table_privileges
    WHERE grantee = 'PUBLIC'
    GROUP BY grantee, table_schema, table_name;
    EOF
    
    # Check for weak passwords (if not using IAM auth)
    psql -h 127.0.0.1 -p 5432 -U dbuser -d production &lt;&lt;EOF
    SELECT usename 
    FROM pg_shadow 
    WHERE passwd IS NULL OR passwd = '';
    EOF
</code></pre><h3 id="7-secrets-rotation-strategy">7. Secrets Rotation Strategy</h3><p>Automate credential rotation for enhanced security:</p><pre><code class="language-bash"># Create a rotation script
cat &gt; rotate-db-password.sh &lt;&lt;'EOF'
#!/bin/bash
set -e

PROJECT_ID=$1
INSTANCE_NAME=$2
DB_USER=$3
SECRET_NAME=$4

# Generate new password
NEW_PASSWORD=$(openssl rand -base64 32)

# Update Cloud SQL user
gcloud sql users set-password $DB_USER \
    --instance=$INSTANCE_NAME \
    --password=$NEW_PASSWORD \
    --project=$PROJECT_ID

# Update Secret Manager
echo -n "$NEW_PASSWORD" | gcloud secrets versions add $SECRET_NAME \
    --data-file=- \
    --project=$PROJECT_ID

# Disable old secret versions (after grace period)
OLD_VERSIONS=$(gcloud secrets versions list $SECRET_NAME \
    --filter="state:ENABLED" \
    --format="value(name)" \
    --sort-by="~createTime" \
    --limit=5 \
    | tail -n +3)

for VERSION in $OLD_VERSIONS; do
    gcloud secrets versions disable $VERSION --secret=$SECRET_NAME --project=$PROJECT_ID
done

echo "Password rotated successfully"
EOF

chmod +x rotate-db-password.sh

# Schedule rotation with Cloud Scheduler
gcloud scheduler jobs create http db-password-rotation \
    --schedule="0 0 1 * *" \
    --uri="https://your-cloud-function-url/rotate-password" \
    --http-method=POST \
    --oidc-service-account-email=$SA_EMAIL \
    --project=$PROJECT_ID
</code></pre><h2 id="troubleshooting-common-issues">Troubleshooting Common Issues</h2><h3 id="issue-1-permission-denied-when-exchanging-token">Issue 1: "Permission denied" when exchanging token</h3><p><strong>Symptom:</strong></p><pre><code>Error: google-github-actions/auth failed with: failed to generate Google Cloud access token: 
(400) {"error":"invalid_target","error_description":"The provided target service account is invalid"}
</code></pre><p><strong>Solution:</strong> Verify the IAM binding:</p><pre><code class="language-bash">gcloud iam service-accounts get-iam-policy $SA_EMAIL \
    --project=$PROJECT_ID \
    --flatten="bindings[].members" \
    --filter="bindings.role:roles/iam.workloadIdentityUser"
</code></pre><p>Ensure the principal matches your repository exactly.</p><h3 id="issue-2-attribute-condition-not-satisfied">Issue 2: "Attribute condition not satisfied"</h3><p><strong>Symptom:</strong></p><pre><code>Error: (403) Attribute condition not satisfied
</code></pre><p><strong>Solution:</strong> Check your attribute condition matches the token claims:</p><pre><code class="language-bash"># Update condition to be more permissive temporarily for debugging
gcloud iam workload-identity-pools providers update-oidc "github-provider" \
    --location="global" \
    --workload-identity-pool="github-pool" \
    --attribute-condition="" \
    --project=$PROJECT_ID
</code></pre><p>Then examine the actual token claims in audit logs and adjust your condition.</p><h3 id="issue-3-missing-id-token-write-permission">Issue 3: Missing <code>id-token: write</code> permission</h3><p><strong>Symptom:</strong></p><pre><code>Error: Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable
</code></pre><p><strong>Solution:</strong> Add to your workflow:</p><pre><code class="language-yaml">permissions:
  id-token: write
  contents: read
</code></pre><h3 id="issue-4-insufficient-permissions-for-service-account">Issue 4: Insufficient permissions for service account</h3><p><strong>Symptom:</strong></p><pre><code>Error: (403) Permission denied on resource
</code></pre><p><strong>Solution:</strong> Review and grant required roles:</p><pre><code class="language-bash"># Check current roles
gcloud projects get-iam-policy $PROJECT_ID \
    --flatten="bindings[].members" \
    --filter="bindings.members:serviceAccount:${SA_EMAIL}"

# Add missing role
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/REQUIRED_ROLE"
</code></pre><h3 id="issue-5-cloud-sql-proxy-connection-timeout">Issue 5: Cloud SQL Proxy Connection Timeout</h3><p><strong>Symptom:</strong></p><pre><code>Error: couldn't connect to &lt;instance&gt;: dial tcp 127.0.0.1:5432: connect: connection refused
</code></pre><p><strong>Solution:</strong> Ensure the proxy has started and the service account has <code>cloudsql.client</code> role:</p><pre><code class="language-bash"># Verify service account has Cloud SQL Client role
gcloud projects get-iam-policy $PROJECT_ID \
    --flatten="bindings[].members" \
    --filter="bindings.members:serviceAccount:${SA_EMAIL}" \
    --filter="bindings.role:roles/cloudsql.client"

# Grant if missing
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/cloudsql.client"

# In your workflow, add more wait time
./cloud_sql_proxy -instances=$INSTANCE &amp;
sleep 10  # Increase from 5 to 10 seconds

# Or check proxy logs
./cloud_sql_proxy -instances=$INSTANCE -verbose &amp;
</code></pre><h3 id="issue-6-iam-database-authentication-failures">Issue 6: IAM Database Authentication Failures</h3><p><strong>Symptom:</strong></p><pre><code>psql: error: connection to server at "127.0.0.1", port 5432 failed: 
FATAL: password authentication failed for user "github-actions-sa@project.iam"
</code></pre><p><strong>Solution:</strong> Verify IAM user exists and has proper grants:</p><pre><code class="language-bash"># Check if IAM user exists
gcloud sql users list --instance=production-db --project=$PROJECT_ID

# Create IAM user if missing
gcloud sql users create github-actions-sa@$PROJECT_ID.iam \
    --instance=production-db \
    --type=CLOUD_IAM_SERVICE_ACCOUNT \
    --project=$PROJECT_ID

# Grant database permissions (connect via Cloud SQL Proxy first)
psql "host=127.0.0.1 port=5432 user=postgres dbname=production" &lt;&lt;EOF
GRANT CONNECT ON DATABASE production TO "github-actions-sa@$PROJECT_ID.iam";
GRANT USAGE ON SCHEMA public TO "github-actions-sa@$PROJECT_ID.iam";
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "github-actions-sa@$PROJECT_ID.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "github-actions-sa@$PROJECT_ID.iam";
EOF
</code></pre><h3 id="issue-7-secret-manager-access-denied">Issue 7: Secret Manager Access Denied</h3><p><strong>Symptom:</strong></p><pre><code>Error: gcloud secrets versions access latest --secret="db-password"
ERROR: (gcloud.secrets.versions.access) PERMISSION_DENIED
</code></pre><p><strong>Solution:</strong> Grant Secret Manager access:</p><pre><code class="language-bash"># Grant access to specific secret
gcloud secrets add-iam-policy-binding db-password \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/secretmanager.secretAccessor" \
    --project=$PROJECT_ID

# Or grant project-wide access (less secure)
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/secretmanager.secretAccessor"
</code></pre><h3 id="issue-8-cloud-sql-instance-not-found">Issue 8: Cloud SQL Instance Not Found</h3><p><strong>Symptom:</strong></p><pre><code>Error: instance does not exist or you are not authorized to access it
</code></pre><p><strong>Solution:</strong> Verify instance name and permissions:</p><pre><code class="language-bash"># List all Cloud SQL instances
gcloud sql instances list --project=$PROJECT_ID

# Describe specific instance
gcloud sql instances describe production-db --project=$PROJECT_ID

# Ensure correct connection name format: PROJECT_ID:REGION:INSTANCE_NAME
# Example: my-project:us-central1:production-db
</code></pre><h3 id="issue-9-migration-script-fails-silently">Issue 9: Migration Script Fails Silently</h3><p><strong>Symptom:</strong> Workflow succeeds but migrations don't apply.</p><p><strong>Solution:</strong> Add explicit error handling:</p><pre><code class="language-yaml">- name: 'Run Migrations with Error Handling'
  run: |
    set -e  # Exit on any error
    set -o pipefail  # Catch errors in pipes
    
    ./cloud_sql_proxy -instances=$INSTANCE_CONNECTION_NAME=tcp:5432 &amp;
    PROXY_PID=$!
    
    # Ensure proxy cleanup on exit
    trap "kill $PROXY_PID 2&gt;/dev/null || true" EXIT
    
    sleep 5
    
    DB_PASSWORD=$(gcloud secrets versions access latest --secret="db-password")
    export PGPASSWORD=$DB_PASSWORD
    
    # Run migration with verbose output
    psql -h 127.0.0.1 -p 5432 -U dbuser -d production \
      -v ON_ERROR_STOP=1 \
      -f migrations/schema.sql \
      2&gt;&amp;1 | tee migration.log
    
    # Check exit code explicitly
    if [ ${PIPESTATUS[0]} -ne 0 ]; then
      echo "Migration failed!"
      cat migration.log
      exit 1
    fi
</code></pre><h3 id="issue-10-connection-pool-exhaustion">Issue 10: Connection Pool Exhaustion</h3><p><strong>Symptom:</strong></p><pre><code>Error: remaining connection slots are reserved for non-replication superuser connections
</code></pre><p><strong>Solution:</strong> Implement connection pooling and limits:</p><pre><code class="language-yaml">- name: 'Use Connection Pooling'
  run: |
    # Increase max_connections on Cloud SQL instance
    gcloud sql instances patch production-db \
      --database-flags=max_connections=100 \
      --project=$PROJECT_ID
    
    # Or use PgBouncer for connection pooling
    cat &gt; pgbouncer.ini &lt;&lt;EOF
[databases]
production = host=127.0.0.1 port=5432 dbname=production

[pgbouncer]
pool_mode = transaction
max_client_conn = 100
default_pool_size = 20
reserve_pool_size = 5
EOF
    
    pgbouncer -d pgbouncer.ini
    
    # Connect through PgBouncer on port 6432
    psql -h 127.0.0.1 -p 6432 -U dbuser -d production -f migrations/schema.sql
</code></pre><h2 id="advanced-topics">Advanced Topics</h2><h3 id="complete-infrastructure-provisioning-with-gcloud">Complete Infrastructure Provisioning with gcloud</h3><p>Here's a complete script to provision all infrastructure using gcloud commands:</p><pre><code class="language-bash">#!/bin/bash
# complete-setup.sh - Complete Workload Identity Federation setup script

set -e  # Exit on error

# Configuration variables
export PROJECT_ID="your-project-id"
export GITHUB_ORG="your-github-org"
export GITHUB_REPO="your-repo-name"
export REGION="us-central1"
export DB_INSTANCE_NAME="production-db"
export DB_NAME="production"
export DB_USER="dbuser"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo -e "${GREEN}Starting Workload Identity Federation setup...${NC}"

# Step 1: Enable required APIs
echo -e "${YELLOW}Enabling required APIs...${NC}"
gcloud services enable \
    iamcredentials.googleapis.com \
    cloudresourcemanager.googleapis.com \
    sts.googleapis.com \
    sqladmin.googleapis.com \
    secretmanager.googleapis.com \
    run.googleapis.com \
    cloudbuild.googleapis.com \
    artifactregistry.googleapis.com \
    --project=$PROJECT_ID

# Get project number
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")
echo -e "${GREEN}Project Number: $PROJECT_NUMBER${NC}"

# Step 2: Create Workload Identity Pool
echo -e "${YELLOW}Creating Workload Identity Pool...${NC}"
if gcloud iam workload-identity-pools describe "github-pool" \
    --location="global" \
    --project=$PROJECT_ID &amp;&gt;/dev/null; then
    echo -e "${GREEN}Pool already exists, skipping...${NC}"
else
    gcloud iam workload-identity-pools create "github-pool" \
        --location="global" \
        --display-name="GitHub Actions Pool" \
        --description="Identity pool for GitHub Actions workflows" \
        --project=$PROJECT_ID
    echo -e "${GREEN}✓ Pool created${NC}"
fi

# Step 3: Create Workload Identity Provider
echo -e "${YELLOW}Creating Workload Identity Provider...${NC}"
if gcloud iam workload-identity-pools providers describe "github-provider" \
    --location="global" \
    --workload-identity-pool="github-pool" \
    --project=$PROJECT_ID &amp;&gt;/dev/null; then
    echo -e "${GREEN}Provider already exists, skipping...${NC}"
else
    gcloud iam workload-identity-pools providers create-oidc "github-provider" \
        --location="global" \
        --workload-identity-pool="github-pool" \
        --display-name="GitHub Actions Provider" \
        --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner,attribute.ref=assertion.ref" \
        --attribute-condition="assertion.repository_owner=='${GITHUB_ORG}'" \
        --issuer-uri="https://token.actions.githubusercontent.com" \
        --project=$PROJECT_ID
    echo -e "${GREEN}✓ Provider created${NC}"
fi

# Step 4: Create Service Accounts
echo -e "${YELLOW}Creating service accounts...${NC}"

# Production service account
if gcloud iam service-accounts describe github-actions-prod-sa@${PROJECT_ID}.iam.gserviceaccount.com \
    --project=$PROJECT_ID &amp;&gt;/dev/null; then
    echo -e "${GREEN}Production SA already exists, skipping...${NC}"
else
    gcloud iam service-accounts create github-actions-prod-sa \
        --display-name="GitHub Actions Production SA" \
        --description="Production deployment service account" \
        --project=$PROJECT_ID
    echo -e "${GREEN}✓ Production SA created${NC}"
fi

# Staging service account
if gcloud iam service-accounts describe github-actions-staging-sa@${PROJECT_ID}.iam.gserviceaccount.com \
    --project=$PROJECT_ID &amp;&gt;/dev/null; then
    echo -e "${GREEN}Staging SA already exists, skipping...${NC}"
else
    gcloud iam service-accounts create github-actions-staging-sa \
        --display-name="GitHub Actions Staging SA" \
        --description="Staging deployment service account" \
        --project=$PROJECT_ID
    echo -e "${GREEN}✓ Staging SA created${NC}"
fi

export PROD_SA_EMAIL="github-actions-prod-sa@${PROJECT_ID}.iam.gserviceaccount.com"
export STAGING_SA_EMAIL="github-actions-staging-sa@${PROJECT_ID}.iam.gserviceaccount.com"

# Step 5: Grant IAM permissions to service accounts
echo -e "${YELLOW}Granting IAM permissions...${NC}"

# Production permissions
for ROLE in "roles/run.admin" "roles/iam.serviceAccountUser" "roles/cloudsql.client" \
            "roles/storage.admin" "roles/secretmanager.secretAccessor"; do
    gcloud projects add-iam-policy-binding $PROJECT_ID \
        --member="serviceAccount:${PROD_SA_EMAIL}" \
        --role="$ROLE" \
        --condition=None \
        --quiet
done

# Staging permissions (more limited)
for ROLE in "roles/run.developer" "roles/cloudsql.client" \
            "roles/secretmanager.secretAccessor"; do
    gcloud projects add-iam-policy-binding $PROJECT_ID \
        --member="serviceAccount:${STAGING_SA_EMAIL}" \
        --role="$ROLE" \
        --condition=None \
        --quiet
done

echo -e "${GREEN}✓ IAM permissions granted${NC}"

# Step 6: Bind workload identity to service accounts
echo -e "${YELLOW}Binding workload identity...${NC}"

# Production: main branch only
gcloud iam service-accounts add-iam-policy-binding "${PROD_SA_EMAIL}" \
    --role="roles/iam.workloadIdentityUser" \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.ref/refs/heads/main" \
    --project=$PROJECT_ID \
    --quiet

# Staging: develop branch
gcloud iam service-accounts add-iam-policy-binding "${STAGING_SA_EMAIL}" \
    --role="roles/iam.workloadIdentityUser" \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.ref/refs/heads/develop" \
    --project=$PROJECT_ID \
    --quiet

echo -e "${GREEN}✓ Workload identity bindings created${NC}"

# Step 7: Create Artifact Registry repository
echo -e "${YELLOW}Creating Artifact Registry repository...${NC}"
if gcloud artifacts repositories describe apps \
    --location=$REGION \
    --project=$PROJECT_ID &amp;&gt;/dev/null; then
    echo -e "${GREEN}Repository already exists, skipping...${NC}"
else
    gcloud artifacts repositories create apps \
        --repository-format=docker \
        --location=$REGION \
        --description="Docker images for applications" \
        --project=$PROJECT_ID
    echo -e "${GREEN}✓ Artifact Registry created${NC}"
fi

# Step 8: Create Cloud SQL instance
echo -e "${YELLOW}Creating Cloud SQL instance (this may take 5-10 minutes)...${NC}"
if gcloud sql instances describe $DB_INSTANCE_NAME \
    --project=$PROJECT_ID &amp;&gt;/dev/null; then
    echo -e "${GREEN}Cloud SQL instance already exists, skipping...${NC}"
else
    gcloud sql instances create $DB_INSTANCE_NAME \
        --database-version=POSTGRES_15 \
        --tier=db-custom-2-7680 \
        --region=$REGION \
        --network=projects/$PROJECT_ID/global/networks/default \
        --no-assign-ip \
        --database-flags=cloudsql.iam_authentication=on,log_connections=on,log_disconnections=on \
        --backup-start-time=03:00 \
        --enable-bin-log \
        --enable-point-in-time-recovery \
        --maintenance-window-day=SUN \
        --maintenance-window-hour=4 \
        --project=$PROJECT_ID
    echo -e "${GREEN}✓ Cloud SQL instance created${NC}"
fi

# Step 9: Create database
echo -e "${YELLOW}Creating database...${NC}"
if gcloud sql databases describe $DB_NAME \
    --instance=$DB_INSTANCE_NAME \
    --project=$PROJECT_ID &amp;&gt;/dev/null; then
    echo -e "${GREEN}Database already exists, skipping...${NC}"
else
    gcloud sql databases create $DB_NAME \
        --instance=$DB_INSTANCE_NAME \
        --project=$PROJECT_ID
    echo -e "${GREEN}✓ Database created${NC}"
fi

# Step 10: Create IAM database users
echo -e "${YELLOW}Creating IAM database users...${NC}"

# Production IAM user
if gcloud sql users describe "${PROD_SA_EMAIL}" \
    --instance=$DB_INSTANCE_NAME \
    --project=$PROJECT_ID &amp;&gt;/dev/null; then
    echo -e "${GREEN}Production IAM user already exists, skipping...${NC}"
else
    gcloud sql users create "${PROD_SA_EMAIL}" \
        --instance=$DB_INSTANCE_NAME \
        --type=CLOUD_IAM_SERVICE_ACCOUNT \
        --project=$PROJECT_ID
    echo -e "${GREEN}✓ Production IAM user created${NC}"
fi

# Staging IAM user
if gcloud sql users describe "${STAGING_SA_EMAIL}" \
    --instance=$DB_INSTANCE_NAME \
    --project=$PROJECT_ID &amp;&gt;/dev/null; then
    echo -e "${GREEN}Staging IAM user already exists, skipping...${NC}"
else
    gcloud sql users create "${STAGING_SA_EMAIL}" \
        --instance=$DB_INSTANCE_NAME \
        --type=CLOUD_IAM_SERVICE_ACCOUNT \
        --project=$PROJECT_ID
    echo -e "${GREEN}✓ Staging IAM user created${NC}"
fi

# Step 11: Create secrets in Secret Manager
echo -e "${YELLOW}Creating secrets...${NC}"

# Generate a strong password (as fallback)
DB_PASSWORD=$(openssl rand -base64 32)

if gcloud secrets describe db-password \
    --project=$PROJECT_ID &amp;&gt;/dev/null; then
    echo -e "${GREEN}Secret already exists, skipping...${NC}"
else
    echo -n "$DB_PASSWORD" | gcloud secrets create db-password \
        --data-file=- \
        --replication-policy="automatic" \
        --project=$PROJECT_ID
    echo -e "${GREEN}✓ Database password secret created${NC}"
fi

# Grant access to secrets
gcloud secrets add-iam-policy-binding db-password \
    --member="serviceAccount:${PROD_SA_EMAIL}" \
    --role="roles/secretmanager.secretAccessor" \
    --project=$PROJECT_ID \
    --quiet

gcloud secrets add-iam-policy-binding db-password \
    --member="serviceAccount:${STAGING_SA_EMAIL}" \
    --role="roles/secretmanager.secretAccessor" \
    --project=$PROJECT_ID \
    --quiet

# Step 12: Enable audit logging
echo -e "${YELLOW}Configuring audit logging...${NC}"
cat &gt; /tmp/audit-config.json &lt;&lt;EOF
{
  "auditConfigs": [
    {
      "service": "iam.googleapis.com",
      "auditLogConfigs": [
        {"logType": "ADMIN_READ"},
        {"logType": "DATA_READ"},
        {"logType": "DATA_WRITE"}
      ]
    },
    {
      "service": "cloudsql.googleapis.com",
      "auditLogConfigs": [
        {"logType": "ADMIN_READ"},
        {"logType": "DATA_READ"},
        {"logType": "DATA_WRITE"}
      ]
    }
  ]
}
EOF

# Note: Full audit config merge would require getting current policy first
echo -e "${YELLOW}Note: Audit logging configuration template created at /tmp/audit-config.json${NC}"
echo -e "${YELLOW}Apply manually if needed to preserve existing policies${NC}"

# Step 13: Create monitoring alerts
echo -e "${YELLOW}Creating monitoring metrics and alerts...${NC}"

# Workload Identity failures
gcloud logging metrics create workload-identity-failures \
    --description="Failed workload identity token exchanges" \
    --log-filter='protoPayload.methodName="GenerateAccessToken" AND protoPayload.status.code!=0' \
    --project=$PROJECT_ID &amp;&gt;/dev/null || echo -e "${YELLOW}Metric already exists${NC}"

# Cloud SQL connection failures
gcloud logging metrics create cloudsql-connection-failures \
    --description="Failed Cloud SQL connections" \
    --log-filter='resource.type="cloudsql_database" AND severity="ERROR"' \
    --project=$PROJECT_ID &amp;&gt;/dev/null || echo -e "${YELLOW}Metric already exists${NC}"

echo -e "${GREEN}✓ Monitoring metrics created${NC}"

# Step 14: Output important information
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}Setup Complete!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo -e "${YELLOW}Important Information:${NC}"
echo ""
echo "Workload Identity Provider:"
echo "  projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github-provider"
echo ""
echo "Service Accounts:"
echo "  Production: $PROD_SA_EMAIL"
echo "  Staging: $STAGING_SA_EMAIL"
echo ""
echo "Cloud SQL Instance:"
echo "  Connection Name: $PROJECT_ID:$REGION:$DB_INSTANCE_NAME"
echo "  Database: $DB_NAME"
echo ""
echo "Artifact Registry:"
echo "  Repository: $REGION-docker.pkg.dev/$PROJECT_ID/apps"
echo ""
echo -e "${YELLOW}Next Steps:${NC}"
echo "1. Copy the Workload Identity Provider string above to your GitHub workflow"
echo "2. Add PROJECT_ID to your GitHub repository secrets"
echo "3. Configure database grants by connecting to Cloud SQL"
echo "4. Test the workflow with a deployment to develop branch (staging)"
echo "5. After validation, deploy to main branch (production)"
echo ""
echo -e "${GREEN}Save this output for reference!${NC}"
</code></pre><h3 id="using-the-provisioning-script">Using the Provisioning Script</h3><p>Save the script and run it:</p><pre><code class="language-bash"># Make executable
chmod +x complete-setup.sh

# Run with your configuration
./complete-setup.sh
</code></pre><h3 id="incremental-updates-with-gcloud">Incremental Updates with gcloud</h3><p>For updating specific components:</p><p><strong>Update Workload Identity Provider:</strong></p><pre><code class="language-bash"># Add new attribute mapping
gcloud iam workload-identity-pools providers update-oidc "github-provider" \
    --location="global" \
    --workload-identity-pool="github-pool" \
    --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner,attribute.ref=assertion.ref,attribute.environment=assertion.environment" \
    --project=$PROJECT_ID

# Update attribute condition
gcloud iam workload-identity-pools providers update-oidc "github-provider" \
    --location="global" \
    --workload-identity-pool="github-pool" \
    --attribute-condition="assertion.repository_owner=='your-org' &amp;&amp; assertion.repository.startsWith('your-org/prod-')" \
    --project=$PROJECT_ID
</code></pre><p><strong>Add New Service Account Binding:</strong></p><pre><code class="language-bash"># Bind new repository
gcloud iam service-accounts add-iam-policy-binding "${PROD_SA_EMAIL}" \
    --role="roles/iam.workloadIdentityUser" \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.repository/your-org/new-repo" \
    --project=$PROJECT_ID
</code></pre><p><strong>Update Cloud SQL Configuration:</strong></p><pre><code class="language-bash"># Increase instance size
gcloud sql instances patch production-db \
    --tier=db-custom-4-15360 \
    --project=$PROJECT_ID

# Add database flags
gcloud sql instances patch production-db \
    --database-flags=cloudsql.iam_authentication=on,max_connections=200,shared_buffers=2GB \
    --project=$PROJECT_ID

# Enable high availability
gcloud sql instances patch production-db \
    --availability-type=REGIONAL \
    --project=$PROJECT_ID
</code></pre><h3 id="cross-project-access">Cross-Project Access</h3><p>Enable a service account in one project to be impersonated from another:</p><pre><code class="language-bash"># Project A: Create workload identity pool
export PROJECT_A="project-a-id"
export PROJECT_B="project-b-id"
export PROJECT_A_NUMBER=$(gcloud projects describe $PROJECT_A --format="value(projectNumber)")

# Create pool in Project A
gcloud iam workload-identity-pools create "cross-project-pool" \
    --location="global" \
    --display-name="Cross-Project Pool" \
    --project=$PROJECT_A

# Create provider in Project A
gcloud iam workload-identity-pools providers create-oidc "cross-project-provider" \
    --location="global" \
    --workload-identity-pool="cross-project-pool" \
    --display-name="Cross-Project Provider" \
    --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
    --attribute-condition="assertion.repository_owner=='your-org'" \
    --issuer-uri="https://token.actions.githubusercontent.com" \
    --project=$PROJECT_A

# Create service account in Project B
gcloud iam service-accounts create cross-project-sa \
    --display-name="Cross-Project SA" \
    --project=$PROJECT_B

# Grant impersonation from Project A pool to Project B service account
gcloud iam service-accounts add-iam-policy-binding \
    "cross-project-sa@${PROJECT_B}.iam.gserviceaccount.com" \
    --role="roles/iam.workloadIdentityUser" \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_A_NUMBER}/locations/global/workloadIdentityPools/cross-project-pool/attribute.repository/your-org/your-repo" \
    --project=$PROJECT_B

# Grant Project B service account permissions in Project B
gcloud projects add-iam-policy-binding $PROJECT_B \
    --member="serviceAccount:cross-project-sa@${PROJECT_B}.iam.gserviceaccount.com" \
    --role="roles/cloudsql.client" \
    --project=$PROJECT_B
</code></pre><h3 id="cleanup-script">Cleanup Script</h3><p>To remove all resources:</p><pre><code class="language-bash">#!/bin/bash
# cleanup.sh - Remove all Workload Identity Federation resources

export PROJECT_ID="your-project-id"
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")

echo "Removing service account bindings..."
gcloud iam service-accounts remove-iam-policy-binding \
    "github-actions-prod-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
    --role="roles/iam.workloadIdentityUser" \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-pool/attribute.ref/refs/heads/main" \
    --project=$PROJECT_ID

echo "Deleting workload identity provider..."
gcloud iam workload-identity-pools providers delete "github-provider" \
    --location="global" \
    --workload-identity-pool="github-pool" \
    --project=$PROJECT_ID \
    --quiet

echo "Deleting workload identity pool..."
gcloud iam workload-identity-pools delete "github-pool" \
    --location="global" \
    --project=$PROJECT_ID \
    --quiet

echo "Deleting service accounts..."
gcloud iam service-accounts delete "github-actions-prod-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
    --project=$PROJECT_ID \
    --quiet

gcloud iam service-accounts delete "github-actions-staging-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
    --project=$PROJECT_ID \
    --quiet

echo "Cleanup complete!"
</code></pre><h3 id="token-lifetime-and-caching">Token Lifetime and Caching</h3><p>Understand token lifetimes:</p><ul><li><strong>OIDC tokens from GitHub</strong>: Valid for 10 minutes</li><li><strong>GCP access tokens</strong>: Valid for 1 hour by default</li><li><strong>The auth action caches tokens</strong>: Reuses valid tokens within the workflow</li></ul><p>For long-running jobs, tokens are automatically refreshed.</p><h2 id="performance-optimization">Performance Optimization</h2><h3 id="1-cache-authentication-step">1. Cache Authentication Step</h3><p>The <code>google-github-actions/auth</code> action automatically caches credentials. Ensure subsequent steps reuse them:</p><pre><code class="language-yaml">steps:
  - id: 'auth'
    uses: 'google-github-actions/auth@v2'
    with:
      workload_identity_provider: '...'
      service_account: '...'
      token_format: 'access_token'  # Cache access token
      access_token_lifetime: '3600s'  # 1 hour
</code></pre><h3 id="2-parallel-job-authentication">2. Parallel Job Authentication</h3><p>Each job needs separate authentication:</p><pre><code class="language-yaml">jobs:
  deploy-frontend:
    runs-on: ubuntu-latest
    steps:
      - id: 'auth'
        uses: 'google-github-actions/auth@v2'
        # ... auth config
  
  deploy-backend:
    runs-on: ubuntu-latest
    steps:
      - id: 'auth'
        uses: 'google-github-actions/auth@v2'
        # ... same auth config
</code></pre><p>Both jobs authenticate independently in parallel.</p><h2 id="monitoring-and-observability">Monitoring and Observability</h2><h3 id="create-custom-dashboard">Create Custom Dashboard</h3><p>Monitor Workload Identity Federation usage:</p><pre><code class="language-bash"># Create log-based metrics
gcloud logging metrics create wif-token-exchanges \
    --description="Workload Identity token exchanges" \
    --log-filter='protoPayload.methodName="GenerateAccessToken"'

gcloud logging metrics create wif-successful-exchanges \
    --description="Successful WIF exchanges" \
    --log-filter='protoPayload.methodName="GenerateAccessToken" AND protoPayload.status.code=0'

gcloud logging metrics create wif-failed-exchanges \
    --description="Failed WIF exchanges" \
    --log-filter='protoPayload.methodName="GenerateAccessToken" AND protoPayload.status.code!=0'
</code></pre><h3 id="query-recent-authentications">Query Recent Authentications</h3><pre><code class="language-bash"># Last 10 authentication attempts
gcloud logging read \
    'protoPayload.methodName="GenerateAccessToken"
    AND resource.labels.service_account_id:"github-actions-sa"' \
    --limit=10 \
    --format='table(timestamp,protoPayload.authenticationInfo.principalEmail,protoPayload.status.code)' \
    --project=$PROJECT_ID
</code></pre><h2 id="cost-considerations">Cost Considerations</h2><p>Workload Identity Federation has <strong>no additional cost</strong>:</p><ul><li>✓ Token exchange operations: Free</li><li>✓ Workload Identity Pools: Free</li><li>✓ Workload Identity Providers: Free</li><li>✓ IAM operations: Free (within quota)</li></ul><p>You only pay for:</p><ul><li>Resources accessed by the service account (Cloud Run, BigQuery, etc.)</li><li>Cloud Audit Logs storage (if enabled for Data Access logs)</li></ul><h2 id="migration-from-service-account-keys">Migration from Service Account Keys</h2><p>If you're currently using service account keys, here's the migration path:</p><h3 id="step-1-set-up-workload-identity-federation-as-shown-above">Step 1: Set up Workload Identity Federation (as shown above)</h3><h3 id="step-2-update-workflows-to-use-both-methods-temporarily">Step 2: Update workflows to use both methods temporarily</h3><pre><code class="language-yaml">- id: 'auth'
  uses: 'google-github-actions/auth@v2'
  with:
    workload_identity_provider: 'projects/.../providers/github-provider'
    service_account: 'github-actions-sa@project.iam.gserviceaccount.com'
    # Fallback to key if WIF fails during migration
    credentials_json: ${{ secrets.GCP_SA_KEY }}
</code></pre><h3 id="step-3-test-thoroughly-in-staging">Step 3: Test thoroughly in staging</h3><h3 id="step-4-remove-the-fallback">Step 4: Remove the fallback</h3><pre><code class="language-yaml">- id: 'auth'
  uses: 'google-github-actions/auth@v2'
  with:
    workload_identity_provider: 'projects/.../providers/github-provider'
    service_account: 'github-actions-sa@project.iam.gserviceaccount.com'
    # No fallback - fully migrated to WIF
</code></pre><h3 id="step-5-delete-service-account-keys">Step 5: Delete service account keys</h3><pre><code class="language-bash"># List keys
gcloud iam service-accounts keys list \
    --iam-account=$SA_EMAIL \
    --project=$PROJECT_ID

# Delete each key
gcloud iam service-accounts keys delete KEY_ID \
    --iam-account=$SA_EMAIL \
    --project=$PROJECT_ID
</code></pre><h3 id="step-6-remove-github-secrets">Step 6: Remove GitHub Secrets</h3><p>Delete the <code>GCP_SA_KEY</code> secret from your repository settings.</p><h2 id="conclusion">Conclusion</h2><p>Workload Identity Federation represents a significant security improvement over traditional service account keys. By implementing the steps in this guide, you've:</p><p>✓ Eliminated long-lived credentials from your CI/CD pipeline<br>✓ Reduced the risk of credential leakage<br>✓ Simplified credential rotation (it's now automatic)<br>✓ Improved audit trail with detailed token exchange logs<br>✓ Implemented fine-grained access control based on repository, branch, and actor<br>✓ Secured database access with Cloud SQL IAM authentication<br>✓ Automated database migrations without storing database credentials</p>]]></content:encoded>
        </item>
    </channel>
</rss>
