<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Gcp on Kinoko's TIL Log</title><link>https://kinoko-tech-blog-theta.vercel.app/tags/gcp/</link><description>Recent content in Gcp on Kinoko's TIL Log</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>pippimotta@gmail.com (Kinoko)</managingEditor><webMaster>pippimotta@gmail.com (Kinoko)</webMaster><copyright>&amp;copy; Brewed by Kinoko</copyright><lastBuildDate>Sun, 14 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://kinoko-tech-blog-theta.vercel.app/tags/gcp/index.xml" rel="self" type="application/rss+xml"/><item><title>GCP Pub-Sub Message Lifecycle Mental Model</title><link>https://kinoko-tech-blog-theta.vercel.app/posts/gcp-pub-sub-message-lifecycle-mental-model/</link><pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate><author>pippimotta@gmail.com (Kinoko)</author><guid>https://kinoko-tech-blog-theta.vercel.app/posts/gcp-pub-sub-message-lifecycle-mental-model/</guid><description>&lt;h2 id="the-point"&gt;The Point&lt;/h2&gt;
&lt;p&gt;All core GCP Pub/Sub settings can be mapped to four stages of a message&amp;rsquo;s lifecycle: how long it lives, how many to pull at once, how long to wait for a response, and what to do on failure.&lt;/p&gt;
&lt;h2 id="explanation"&gt;Explanation&lt;/h2&gt;
&lt;p&gt;The parameters scattered across the docs look like a lot, but each one guards a specific stage in a message&amp;rsquo;s journey from publish to final processing (or abandonment). Organizing them by lifecycle stage &amp;ndash; instead of memorizing each one individually &amp;ndash; lets you instantly locate what any new parameter controls. This mental model also ties together the responsibility boundaries of the three resource layers: Topic, Subscription, and Dead Letter.&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="the-point">The Point</h2>
<p>All core GCP Pub/Sub settings can be mapped to four stages of a message&rsquo;s lifecycle: how long it lives, how many to pull at once, how long to wait for a response, and what to do on failure.</p>
<h2 id="explanation">Explanation</h2>
<p>The parameters scattered across the docs look like a lot, but each one guards a specific stage in a message&rsquo;s journey from publish to final processing (or abandonment). Organizing them by lifecycle stage &ndash; instead of memorizing each one individually &ndash; lets you instantly locate what any new parameter controls. This mental model also ties together the responsibility boundaries of the three resource layers: Topic, Subscription, and Dead Letter.</p>
<h2 id="knowledge-sugar">Knowledge Sugar</h2>
<h3 id="1-parameters-by-lifecycle-stage">1. Parameters by lifecycle stage</h3>
<table>
	<thead>
			<tr>
					<th>Stage</th>
					<th>Key parameter</th>
					<th>One-liner</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Retention</td>
					<td><code>MessageRetentionDuration</code></td>
					<td>How long an unacked message stays alive</td>
			</tr>
			<tr>
					<td>Retention</td>
					<td><code>TopicRetentionDuration</code></td>
					<td>How long the topic keeps a message after ack (for replay)</td>
			</tr>
			<tr>
					<td>Flow Control</td>
					<td><code>MaxOutstandingMessages</code></td>
					<td>Max unacked messages a subscriber can hold</td>
			</tr>
			<tr>
					<td>Flow Control</td>
					<td><code>MaxOutstandingBytes</code></td>
					<td>Same, but measured in bytes (OR logic with messages)</td>
			</tr>
			<tr>
					<td>Flow Control</td>
					<td><code>NumGoroutines</code></td>
					<td>Number of parallel workers running callbacks</td>
			</tr>
			<tr>
					<td>Ack/Extension</td>
					<td><code>AckDeadline</code></td>
					<td>Seconds to ack before redelivery</td>
			</tr>
			<tr>
					<td>Ack/Extension</td>
					<td><code>MaxExtension</code></td>
					<td>Upper bound on client library auto-extending the deadline</td>
			</tr>
			<tr>
					<td>Retry/DLQ</td>
					<td><code>MaxDeliveryAttempts</code></td>
					<td>Max attempts before moving to DLQ</td>
			</tr>
			<tr>
					<td>Retry/DLQ</td>
					<td><code>MinBackoff</code> / <code>MaxBackoff</code></td>
					<td>Exponential backoff range for push retry</td>
			</tr>
			<tr>
					<td>Semantics</td>
					<td><code>ExactlyOnceDelivery</code></td>
					<td>Whether to enable exactly-once (has throughput cost)</td>
			</tr>
			<tr>
					<td>Semantics</td>
					<td><code>EnableMessageOrdering</code></td>
					<td>Preserve order within the same ordering key</td>
			</tr>
	</tbody>
</table>
<p>The relationship between the three flow control params: <code>MaxOutstandingMessages</code> is the number of seats in the waiting room, <code>NumGoroutines</code> is the number of exam rooms, and <code>MaxOutstandingBytes</code> is the physical space limit of the waiting room.</p>
<h3 id="2-topic-vs-subscription-two-layer-responsibilities">2. Topic vs Subscription: two-layer responsibilities</h3>
<p>A Topic is a named channel responsible for fan-out: one published message gives each subscription its own complete copy. A Subscription handles load balancing: with N pods underneath, they act as competing consumers where the same message goes to only one pod. Each layer has its own job &ndash; the Topic decides &ldquo;which systems receive this event,&rdquo; and the Subscription decides &ldquo;how this system distributes workload internally.&rdquo;</p>
<h3 id="3-subscription-to-topic-binding-is-infrastructure">3. Subscription-to-Topic binding is infrastructure</h3>
<p>A Subscription binds to a Topic at creation time. After that, application code only uses <code>client.Subscription(&quot;name&quot;)</code> to retrieve the existing resource without referencing the topic. This is the same pattern as K8s Pod <code>volumeMounts</code> pointing to a PVC name &ndash; the infrastructure layer binds things up front, and the application layer just uses the name. In practice, this step is usually handled by Terraform, not written in application code.</p>
<h3 id="4-dead-letter-policy-isolates-poison-messages">4. Dead Letter Policy isolates poison messages</h3>
<p>When a message gets nacked or times out repeatedly up to <code>MaxDeliveryAttempts</code>, the broker stops sending it to the original subscription and forwards it to a dead letter topic. A DLQ topic is just a regular topic that semantically collects &ldquo;messages that could not be processed.&rdquo; The core value is separating &ldquo;cannot process&rdquo; from &ldquo;not yet processed,&rdquo; preventing a single poison message from occupying <code>MaxOutstandingMessages</code> quota and dragging down overall throughput. This is even more critical with <code>EnableMessageOrdering</code> on &ndash; messages behind the same ordering key would all be blocked.</p>
]]></content:encoded></item><item><title>GCP PubSub Retry &amp; Pusher</title><link>https://kinoko-tech-blog-theta.vercel.app/posts/gcp-pubsub-retry-pusher/</link><pubDate>Thu, 12 Mar 2026 00:00:00 +0000</pubDate><author>pippimotta@gmail.com (Kinoko)</author><guid>https://kinoko-tech-blog-theta.vercel.app/posts/gcp-pubsub-retry-pusher/</guid><description>&lt;h2 id="the-point"&gt;The Point&lt;/h2&gt;
&lt;p&gt;GCP Pub/Sub retry is ACK-based &amp;ndash; no ACK means redeliver. With a Pusher in the middle, retry becomes two layers: the Pusher&amp;rsquo;s own retry (fast, finely configurable) + Pub/Sub redelivery (slow, last resort).&lt;/p&gt;
&lt;h2 id="explanation"&gt;Explanation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Native Pub/Sub retry mechanism&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After a subscriber pulls a message, it must return an ACK within the ACK deadline. Otherwise Pub/Sub treats it as a failure and automatically redelivers:&lt;/p&gt;





&lt;pre tabindex="0"&gt;&lt;code&gt;Pub/Sub
 ↓ deliver message
Subscriber
 ↓ success → ACK → message removed from subscription
 ↓ failure / timeout → no ACK → Pub/Sub redelivers&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Redelivery continues until the message is ACKed or exceeds the retention period (default 7 days).&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="the-point">The Point</h2>
<p>GCP Pub/Sub retry is ACK-based &ndash; no ACK means redeliver. With a Pusher in the middle, retry becomes two layers: the Pusher&rsquo;s own retry (fast, finely configurable) + Pub/Sub redelivery (slow, last resort).</p>
<h2 id="explanation">Explanation</h2>
<p><strong>Native Pub/Sub retry mechanism</strong></p>
<p>After a subscriber pulls a message, it must return an ACK within the ACK deadline. Otherwise Pub/Sub treats it as a failure and automatically redelivers:</p>





<pre tabindex="0"><code>Pub/Sub
  ↓ deliver message
Subscriber
  ↓ success → ACK → message removed from subscription
  ↓ failure / timeout → no ACK → Pub/Sub redelivers</code></pre><p>Redelivery continues until the message is ACKed or exceeds the retention period (default 7 days).</p>
<hr>
<p><strong>With a Pusher: two-layer retry</strong></p>
<p>The Pusher sits in between, creating two independent retry layers:</p>





<pre tabindex="0"><code>Pub/Sub Subscription
    ↓ pull
  Pusher
    ↓ push → Target Service</code></pre><p><strong>Layer 1 &ndash; Pusher&rsquo;s own retry (CRD config)</strong></p>
<p>When the Pusher&rsquo;s push to the target service fails, it retries internally first (count and backoff are configurable via CRD) without going back to Pub/Sub.</p>
<p><strong>Layer 2 &ndash; Pub/Sub redelivery</strong></p>
<ul>
<li>If the target service processes successfully -&gt; Pusher ACKs to Pub/Sub -&gt; message done</li>
<li>If all of Pusher&rsquo;s retries are exhausted and it still fails -&gt; NACK to Pub/Sub -&gt; Pub/Sub redelivers to the Pusher</li>
</ul>
<p>So Pub/Sub retry is the <strong>last resort</strong>. Day-to-day transient failures are absorbed by the Pusher layer.</p>
<p><strong>Key: when does the Pusher ACK to Pub/Sub?</strong></p>
<p>The ACK timing determines whether the entire retry chain works correctly:</p>
<table>
	<thead>
			<tr>
					<th>ACK timing</th>
					<th>Result</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>ACK immediately on pull</td>
					<td>Pub/Sub thinks it succeeded; if the target service fails, the message is lost forever</td>
			</tr>
			<tr>
					<td>ACK only after target service succeeds</td>
					<td>Any layer&rsquo;s failure still has a chance to retry</td>
			</tr>
	</tbody>
</table>
<h2 id="knowledge-sugar">Knowledge Sugar</h2>
<p><strong>Dead Letter Topic</strong></p>
<p>When a message fails repeatedly beyond the maximum retry count, instead of letting it loop forever, move it to a dedicated topic for isolation:</p>





<pre tabindex="0"><code>Normal:  Pub/Sub → Pusher → Target Service ✓ → ACK
Failure: Pub/Sub → Pusher → Target Service ✗ → NACK → retry N times
                                                        ↓ exceeds limit
                                               Dead Letter Topic</code></pre><p>Three uses for a Dead Letter Topic:</p>
<ul>
<li><strong>Unblock normal traffic</strong>: problematic messages are moved away, the rest keep flowing</li>
<li><strong>Post-mortem investigation</strong>: see which messages keep failing and why</li>
<li><strong>Manual replay</strong>: after fixing the bug, replay dead letter messages back into the normal flow</li>
</ul>
<p><strong>Benefits of two-layer retry</strong></p>
<p>Pure Pub/Sub retry uses exponential backoff, which is slow. The Pusher layer can use faster, finer-grained retry strategies. Most transient failures get resolved at this layer without going through the full Pub/Sub redelivery cycle.</p>
<p>For the Pusher architecture background, see the gRPC Pusher Pattern post. For Pub/Sub Topic &amp; Subscription basics, see the GCP Pub Sub Topic &amp; Subscription post.</p>
]]></content:encoded></item><item><title>GCP Pub Sub Topic &amp; Subscription</title><link>https://kinoko-tech-blog-theta.vercel.app/posts/gcp-pub-sub-topic-subscription/</link><pubDate>Wed, 11 Mar 2026 00:00:00 +0000</pubDate><author>pippimotta@gmail.com (Kinoko)</author><guid>https://kinoko-tech-blog-theta.vercel.app/posts/gcp-pub-sub-topic-subscription/</guid><description>&lt;h2 id="the-point"&gt;The Point&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Topic is where messages are published to; subscription is how a consumer receives messages from that topic.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A topic is the publishing channel for messages. A subscription is how a consumer subscribes to that channel. Separating the two lets multiple subscribers independently consume from the same topic.&lt;/p&gt;
&lt;h2 id="explanation"&gt;Explanation&lt;/h2&gt;





&lt;pre tabindex="0"&gt;&lt;code&gt;Publisher → Topic → Subscription A → Service A
 → Subscription B → Service B&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Topic&lt;/strong&gt;: the target for publishing. The publisher just sends messages to the topic without caring who reads them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subscription&lt;/strong&gt;: a consumption channel attached to a topic. Each subscription receives &lt;strong&gt;an independent copy of every message&lt;/strong&gt; on the topic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each subscription tracks its own progress &amp;ndash; where Service A has read up to and where Service B has read up to are independent. Messages stay in a subscription until they are acknowledged or exceed the retention period.&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="the-point">The Point</h2>
<blockquote>
<p>Topic is where messages are published to; subscription is how a consumer receives messages from that topic.</p>
</blockquote>
<p>A topic is the publishing channel for messages. A subscription is how a consumer subscribes to that channel. Separating the two lets multiple subscribers independently consume from the same topic.</p>
<h2 id="explanation">Explanation</h2>





<pre tabindex="0"><code>Publisher → Topic → Subscription A → Service A
                 → Subscription B → Service B</code></pre><ul>
<li><strong>Topic</strong>: the target for publishing. The publisher just sends messages to the topic without caring who reads them.</li>
<li><strong>Subscription</strong>: a consumption channel attached to a topic. Each subscription receives <strong>an independent copy of every message</strong> on the topic.</li>
</ul>
<p>Each subscription tracks its own progress &ndash; where Service A has read up to and where Service B has read up to are independent. Messages stay in a subscription until they are acknowledged or exceed the retention period.</p>
<p><strong>Two subscription types</strong></p>
<ul>
<li><strong>Pull</strong>: the subscriber actively asks &ldquo;any new messages?&rdquo; &ndash; suited for backend services that want to control their own consumption rate</li>
<li><strong>Push</strong>: Pub/Sub proactively sends messages to a specified HTTP endpoint &ndash; suited for serverless or webhook scenarios</li>
</ul>
<h2 id="knowledge-sugar">Knowledge Sugar</h2>
<p><strong>Why separate topic and subscription?</strong></p>
<p>If there were only topics, all consumers would compete for the same messages &ndash; once read, they are gone. With subscriptions, the same message can be consumed independently by a logging service, an analytics service, and a notification service without interference. This is the <strong>fan-out</strong> pattern.</p>
<p><strong>What happens if nobody consumes from a subscription?</strong></p>
<p>Messages pile up until the retention period expires (default 7 days), then get auto-deleted. So if you create a subscription, make sure something is consuming from it &ndash; otherwise it is wasted resources.</p>
<p><strong>Topic ownership should follow the domain</strong></p>
<p>The topic should be created by the service that owns the domain. For example, if monolith ServiceA publishes events to a topic belonging to ServiceB&rsquo;s domain, the topic should live in ServiceB&rsquo;s GCP project, with IAM granting ServiceA publish permission:</p>





<pre tabindex="0"><code>ServiceA (monolith)
  │  roles/pubsub.publisher (IAM grant)
  ▼
Topic (owned by ServiceB)
  ▼
Subscription → ServiceC consume</code></pre><p>After migration completes, you only need to remove ServiceA&rsquo;s publish logic &ndash; everything else stays the same.</p>
<p><strong>Subscription ownership design</strong></p>
<p>The subscription owner does not have to match the topic owner. Two options:</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Option 1: ServiceB owns subscription</th>
					<th>Option 2: ServiceC owns subscription</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Created by</td>
					<td>ServiceB</td>
					<td>ServiceC (cross-project)</td>
			</tr>
			<tr>
					<td>Control</td>
					<td>ServiceB manages the entire message flow</td>
					<td>ServiceC is fully autonomous</td>
			</tr>
			<tr>
					<td>Best for</td>
					<td>Migration transition, centralized management needed</td>
					<td>ServiceC is independent, no dependency on ServiceB</td>
			</tr>
	</tbody>
</table>
<p>During migration, <strong>Option 1</strong> is recommended &ndash; ServiceB owns both the topic and subscription, then grants ServiceC consume permission. This gives ServiceB full visibility over the message flow.</p>
<p>ServiceC connects to the subscription via IAM:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_pubsub_subscription_iam_member&#34; &#34;subscriber&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  subscription</span> <span class="o">=</span> <span class="k">google_pubsub_subscription</span><span class="p">.</span><span class="k">my_subscription</span><span class="p">.</span><span class="k">name</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  role</span>         <span class="o">=</span> <span class="s2">&#34;roles/pubsub.subscriber&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">  member</span>       <span class="o">=</span> <span class="s2">&#34;serviceAccount:service-c@service-c-project.iam.gserviceaccount.com&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">}</span></span></code></pre></div><p>ServiceC&rsquo;s code just needs to pull using its own service account. GCP verifies IAM and grants access &ndash; no extra configuration needed.</p>
<p><strong>Creating Topic and Subscription with Terraform</strong></p>
<p>Like buckets, these are GCP resources that can be managed directly with Terraform:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_pubsub_topic&#34; &#34;my_topic&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  name</span>    <span class="o">=</span> <span class="s2">&#34;my-topic&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  project</span> <span class="o">=</span> <span class="s2">&#34;service-b-project&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_pubsub_subscription&#34; &#34;my_subscription&#34;</span> {
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">  name</span>  <span class="o">=</span> <span class="s2">&#34;my-subscription&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  topic</span> <span class="o">=</span> <span class="k">google_pubsub_topic</span><span class="p">.</span><span class="k">my_topic</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">}<span class="c1">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># Grant ServiceA&#39;s service account publish permission
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_pubsub_topic_iam_member&#34; &#34;publisher&#34;</span> {
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">  topic</span>  <span class="o">=</span> <span class="k">google_pubsub_topic</span><span class="p">.</span><span class="k">my_topic</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">  role</span>   <span class="o">=</span> <span class="s2">&#34;roles/pubsub.publisher&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">  member</span> <span class="o">=</span> <span class="s2">&#34;serviceAccount:service-a@service-a-project.iam.gserviceaccount.com&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">}</span></span></code></pre></div>]]></content:encoded></item><item><title>OIDC Federation from GKE to AWS S3</title><link>https://kinoko-tech-blog-theta.vercel.app/posts/oidc-gke-to-aws-s3/</link><pubDate>Tue, 24 Feb 2026 00:00:00 +0000</pubDate><author>pippimotta@gmail.com (Kinoko)</author><guid>https://kinoko-tech-blog-theta.vercel.app/posts/oidc-gke-to-aws-s3/</guid><description>&lt;h2 id="the-point"&gt;The Point&lt;/h2&gt;
&lt;p&gt;An application deployed on GKE can use OIDC federation to exchange a Kubernetes-issued JWT token for temporary AWS credentials, eliminating the need to hardcode AWS Access Keys.&lt;/p&gt;
&lt;h2 id="explanation"&gt;Explanation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What OIDC does in this scenario&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OIDC (OpenID Connect) is essentially a protocol for &amp;ldquo;let me prove this identity is real.&amp;rdquo; In this use case, GKE acts as the &lt;strong&gt;OIDC Identity Provider (IdP)&lt;/strong&gt;, and AWS is the party that &lt;strong&gt;trusts what GKE says&lt;/strong&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="the-point">The Point</h2>
<p>An application deployed on GKE can use OIDC federation to exchange a Kubernetes-issued JWT token for temporary AWS credentials, eliminating the need to hardcode AWS Access Keys.</p>
<h2 id="explanation">Explanation</h2>
<p><strong>What OIDC does in this scenario</strong></p>
<p>OIDC (OpenID Connect) is essentially a protocol for &ldquo;let me prove this identity is real.&rdquo; In this use case, GKE acts as the <strong>OIDC Identity Provider (IdP)</strong>, and AWS is the party that <strong>trusts what GKE says</strong>.</p>
<p>The overall flow looks like this:</p>





<pre tabindex="0"><code>GKE Pod
  │
  │ 1. Kubernetes automatically mounts a Service Account JWT token on the Pod
  │    (this token contains the workload&#39;s identity info, signed by GKE)
  │
  ▼
AWS STS (AssumeRoleWithWebIdentity)
  │
  │ 2. Submit the JWT token to AWS STS
  │    AWS validates the token against GKE&#39;s OIDC endpoint
  │
  ▼
Temporary AWS credentials (Access Key + Secret + Session Token)
  │
  │ 3. Use temporary credentials to operate on S3
  │
  ▼
S3 Bucket ✓</code></pre><pre class="mermaid">sequenceDiagram
    participant Pod as GKE Pod
    participant STS as AWS STS
    participant OIDC as GKE OIDC
    participant S3

    Note over Pod: K8s mounts JWT token
    Pod->>STS: AssumeRoleWithWebIdentity(JWT)
    STS->>OIDC: Verify signature
    OIDC-->>STS: OK
    STS-->>Pod: Temporary credentials (with TTL)
    Pod->>S3: Upload CSV
    S3-->>Pod: 200 OK
</pre>

<p><strong>What needs to be configured in Terraform</strong></p>
<ol>
<li><strong>AWS IAM OIDC Provider</strong> &ndash; tells AWS &ldquo;I trust tokens issued by this GKE cluster&rdquo;; requires the GKE OIDC issuer URL and thumbprint</li>
<li><strong>AWS IAM Role + Trust Policy</strong> &ndash; defines which GKE service account can assume this role; the condition typically requires the <code>sub</code> claim to match <code>system:serviceaccount:&lt;namespace&gt;:&lt;ksa-name&gt;</code></li>
<li><strong>S3 Permission</strong> &ndash; attach the S3 read/write policy to this IAM Role</li>
</ol>
<p><strong>Go STS package upgrade</strong></p>
<p>In the Go application, use <code>AssumeRoleWithWebIdentity</code>, passing in the JWT token path read from the Pod (typically <code>/var/run/secrets/kubernetes.io/serviceaccount/token</code>). After exchanging for temporary credentials, initialize the S3 client. The package upgrade was mainly to ensure compatibility with SDK v2&rsquo;s credential provider interface.</p>
<h2 id="knowledge-sugar">Knowledge Sugar</h2>
<p><strong>Why not just use AWS Access Keys?</strong>
Hardcoding keys has rotation issues, leakage risks, and makes it hard to audit which workload is accessing what. OIDC federation&rsquo;s temporary credentials have a TTL and expire automatically, making them significantly more secure.</p>
<p><strong>What&rsquo;s inside the JWT token?</strong>
The token issued by GKE is a standard JWT. The payload contains:</p>
<ul>
<li><code>iss</code>: OIDC issuer (the GKE cluster&rsquo;s URL)</li>
<li><code>sub</code>: <code>system:serviceaccount:&lt;namespace&gt;:&lt;name&gt;</code></li>
<li><code>exp</code>: expiration time</li>
</ul>
<p>When AWS STS receives the token, it fetches the public key from the <code>iss</code> URL&rsquo;s <code>/.well-known/openid-configuration</code> endpoint to verify the signature.</p>
<p><strong>How is the JWT token mounted into the Pod?</strong>
This is a built-in Kubernetes mechanism &ndash; every Pod automatically gets a Service Account token mounted at creation, no extra config needed. However, the default token doesn&rsquo;t specify an <code>audience</code>, which AWS STS won&rsquo;t accept. So you need to mount a dedicated one using a projected volume in the Pod spec:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">aws-token</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">projected</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">      </span><span class="nt">sources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">        </span>- <span class="nt">serviceAccountToken</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">            </span><span class="nt">audience</span><span class="p">:</span><span class="w"> </span><span class="l">sts.amazonaws.com</span><span class="w"> </span><span class="c"># here!</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">            </span><span class="nt">expirationSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">3600</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">            </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">token</span></span></span></code></pre></div><p>Terraform only handles the AWS side (OIDC Provider, IAM Role); the token mounting itself is done by Kubernetes.</p>
<p><strong>The official name for this pattern: Workload Identity Federation</strong>
GCP&rsquo;s own Workload Identity uses the same principle, just with GCP resources. For cross-cloud scenarios (GKE -&gt; AWS), OIDC serves as the universal standard that bridges them.</p>
]]></content:encoded></item></channel></rss>