<?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>Pulumi on Nelson Figueroa</title><link>https://nelson.cloud/categories/pulumi/</link><description>Recent content in Pulumi on Nelson Figueroa</description><image><title>Nelson Figueroa</title><url>https://nelson.cloud/opengraph-images/default.png</url><link>https://nelson.cloud/opengraph-images/default.png</link></image><language>en</language><lastBuildDate>Sat, 25 Apr 2026 23:51:05 -0700</lastBuildDate><atom:link href="https://nelson.cloud/categories/pulumi/index.xml" rel="self" type="application/rss+xml"/><item><title>Proxying GoatCounter Requests Through CloudFront to Bypass Ad Blockers</title><link>https://nelson.cloud/proxying-goatcounter-requests-through-cloudfront-to-bypass-ad-blockers/?ref=rss</link><pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate><guid>https://nelson.cloud/proxying-goatcounter-requests-through-cloudfront-to-bypass-ad-blockers/?ref=rss</guid><description>How to configure CloudFront to proxy requests to GoatCounter so that adblockers don&amp;rsquo;t block page views.</description><content:encoded><![CDATA[<p>This blog is a 

<a href="https://gohugo.io/" target="_blank" rel="noopener">Hugo</a>-generated static site hosted on AWS using S3 and CloudFront. I&rsquo;ve been running 

<a href="https://www.goatcounter.com/" target="_blank" rel="noopener">GoatCounter</a> on my site using 

<a href="https://gc.zgo.at/count.js" target="_blank" rel="noopener">the provided script</a> to see who views my blog posts. Every time someone visits my site, a request goes out to GoatCounter. The problem is that adblockers like uBlock Origin block it (understandably).</p>
<img src="/proxying-goatcounter-requests/before.webp" alt="uBlock Origin showing a blocked domain" width="720" height="546" style="max-width: 100%; height: auto; aspect-ratio: 1292 / 980;" loading="lazy" decoding="async">
<p>To get around this, I set up proxying so that the GoatCounter requests go to an endpoint under my domain <code>nelson.cloud/gc/count</code>, and then from there CloudFront handles it and sends it to GoatCounter. Most ad blockers work based on domain and GoatCounter is on the blocklists. Since the browser is now sending requests to the same domain as my site, it shouldn&rsquo;t trigger any ad blockers. This post explains how I did it in case it&rsquo;s useful for anyone else.</p>
<p>It&rsquo;s possible to 

<a href="https://github.com/arp242/goatcounter" target="_blank" rel="noopener">self-host</a> GoatCounter, but my approach was easier to do and less infrastructure to maintain. Perhaps in the future.</p>
<h2 id="on-analytics-and-privacy">On Analytics and Privacy</h2>
<p>I know I&rsquo;m bypassing a user&rsquo;s preference to not be tracked, even if it&rsquo;s (in my opinion) a harmless analytics tool. I just want to see who reads my stuff, that&rsquo;s all.</p>
<p>Read the GoatCounter developer&rsquo;s take if you want another opinion: 

<a href="https://www.arp242.net/personal-analytics.html" target="_blank" rel="noopener">Analytics on personal websites</a>.</p>
<h2 id="managing-infrastructure-with-pulumi">Managing Infrastructure with Pulumi</h2>
<p>Clicking through the AWS console to configure CloudFront distributions is a pain in the ass. I took the time to finally get the infrastructure for my blog managed as infrastructure-as-code with 

<a href="https://www.pulumi.com/docs/iac/languages-sdks/python/" target="_blank" rel="noopener">Pulumi and Python</a>. So while you can click around the console and do all of this, I will be showing how to configure everything with Pulumi.</p>
<p>If you don&rsquo;t want to use IaC, you can still find all of these options/settings in AWS itself.</p>
<h2 id="setting-it-up">Setting it up</h2>
<p>To set up GoatCounter proxying via CloudFront, we&rsquo;ll need to</p>
<ul>
<li>Create a new 

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html" target="_blank" rel="noopener">CloudFront function</a> resource</li>
<li>Add a second 

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistS3AndCustomOrigins.html" target="_blank" rel="noopener">origin</a> to the distribution</li>
<li>Add an ordered 

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesCacheBehavior.html" target="_blank" rel="noopener">cache behavior</a> to the distribution (which references the CloudFront function using its ARN)</li>
<li>Update the GoatCounter script to point to this new endpoint</li>
</ul>
<h3 id="cloudfront-function">CloudFront Function</h3>
<p>CloudFront functions are JavaScript scripts that run before a request reaches a CloudFront distribution&rsquo;s origin. In this case, the function strips the <code>/gc</code> from <code>nelson.cloud/gc/count</code>.</p>
<p>We need to strip <code>/gc</code> for two reasons:</p>
<ol>
<li>I chose to proxy requests that hit the <code>/gc/count</code> endpoint on my site to make sure there&rsquo;s no collision with post titles/slugs. I&rsquo;ll never use the <code>/gc/*</code> path for posts.</li>
<li>GoatCounter accepts requests under <code>/count</code>, not <code>/gc/count</code></li>
</ol>
<p>Here is the code for the function:</p>
<div class="highlight"><span class="code-lang">JavaScript</span><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">handler</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">request</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">request</span><span class="p">.</span><span class="nx">uri</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">uri</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/^\/gc/</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">uri</span> <span class="o">===</span> <span class="s1">&#39;&#39;</span><span class="p">)</span> <span class="nx">request</span><span class="p">.</span><span class="nx">uri</span> <span class="o">=</span> <span class="s1">&#39;/&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">request</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>And here is the CloudFront function resource defined in Pulumi (using Python) that includes the JavaScript from above. This is a new resource defined in the same Python file where my existing distribution already exists:</p>
<div class="highlight"><span class="code-lang">Python</span><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">goatcounter_rewrite</span> <span class="o">=</span> <span class="n">aws</span><span class="o">.</span><span class="n">cloudfront</span><span class="o">.</span><span class="n">Function</span><span class="p">(</span><span class="s2">&#34;goatcounter-rewrite&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span><span class="o">=</span><span class="s2">&#34;goatcounter-rewrite&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">runtime</span><span class="o">=</span><span class="s2">&#34;cloudfront-js-2.0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">code</span><span class="o">=</span><span class="s2">&#34;&#34;&#34;</span><span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="s2">function handler(event) {
</span></span></span><span class="line"><span class="cl"><span class="s2">    var request = event.request;
</span></span></span><span class="line"><span class="cl"><span class="s2">    request.uri = request.uri.replace(/^</span><span class="se">\\</span><span class="s2">/gc/, &#39;&#39;);
</span></span></span><span class="line"><span class="cl"><span class="s2">    if (request.uri === &#39;&#39;) request.uri = &#39;/&#39;;
</span></span></span><span class="line"><span class="cl"><span class="s2">    return request;
</span></span></span><span class="line"><span class="cl"><span class="s2">}
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div><h3 id="cloudfront-distribution-origin-and-cache-behavior">CloudFront Distribution Origin and Cache Behavior</h3>
<p>Here is my existing CloudFront distribution being updated with a new origin and cache behavior in Pulumi code.</p>
<blockquote><p><strong>Note:</strong></p><p>At the time of writing CloudFront only allows <code>allowed_methods</code> to be a list of HTTP methods in specific combinations. The value must be one of these:</p>
<ul>
<li><code>[HEAD, GET]</code></li>
<li><code>[HEAD, GET, OPTIONS]</code></li>
<li><code>[HEAD, DELETE, POST, GET, OPTIONS, PUT, PATCH]</code></li>
</ul>
<p>Since the GoatCounter JavaScript sends a <code>POST</code> request, and the third option is the only one that includes <code>POST</code>, we&rsquo;re forced to use all HTTP verbs. It should be harmless though.</p>
</blockquote>

<div class="highlight"><span class="code-lang">Python</span><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">nelson_cloud_distribution</span> <span class="o">=</span> <span class="n">aws</span><span class="o">.</span><span class="n">cloudfront</span><span class="o">.</span><span class="n">Distribution</span><span class="p">(</span><span class="s2">&#34;nelson-cloud&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">aliases</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;nelson.cloud&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="c1">###</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># other configuration</span>
</span></span><span class="line"><span class="cl">    <span class="c1">###</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># route /gc/* requests to GoatCounter, stripping the /gc prefix via CloudFront function</span>
</span></span><span class="line"><span class="cl">    <span class="n">ordered_cache_behaviors</span><span class="o">=</span><span class="p">[{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;path_pattern&#34;</span><span class="p">:</span> <span class="s2">&#34;/gc/*&#34;</span><span class="p">,</span> <span class="c1"># the path gets matched here</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;allowed_methods&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;GET&#34;</span><span class="p">,</span> <span class="s2">&#34;HEAD&#34;</span><span class="p">,</span> <span class="s2">&#34;OPTIONS&#34;</span><span class="p">,</span> <span class="s2">&#34;PUT&#34;</span><span class="p">,</span> <span class="s2">&#34;PATCH&#34;</span><span class="p">,</span> <span class="s2">&#34;POST&#34;</span><span class="p">,</span> <span class="s2">&#34;DELETE&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;cached_methods&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;GET&#34;</span><span class="p">,</span> <span class="s2">&#34;HEAD&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;target_origin_id&#34;</span><span class="p">:</span> <span class="s2">&#34;goatcounter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;viewer_protocol_policy&#34;</span><span class="p">:</span> <span class="s2">&#34;redirect-to-https&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;compress&#34;</span><span class="p">:</span> <span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;cache_policy_id&#34;</span><span class="p">:</span> <span class="s2">&#34;4135ea2d-6df8-44a3-9df3-4b5a84be39ad&#34;</span><span class="p">,</span>  <span class="c1"># CachingDisabled</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;origin_request_policy_id&#34;</span><span class="p">:</span> <span class="s2">&#34;b689b0a8-53d0-40ab-baf2-68738e2966ac&#34;</span><span class="p">,</span>  <span class="c1"># AllViewerExceptHostHeader</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;function_associations&#34;</span><span class="p">:</span> <span class="p">[{</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;event_type&#34;</span><span class="p">:</span> <span class="s2">&#34;viewer-request&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;function_arn&#34;</span><span class="p">:</span> <span class="n">goatcounter_rewrite</span><span class="o">.</span><span class="n">arn</span><span class="p">,</span> <span class="c1"># CloudFront function from above</span>
</span></span><span class="line"><span class="cl">        <span class="p">}],</span>
</span></span><span class="line"><span class="cl">    <span class="p">}],</span>
</span></span><span class="line"><span class="cl">    <span class="n">origins</span><span class="o">=</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="c1">###</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># other existing origins</span>
</span></span><span class="line"><span class="cl">        <span class="c1">###</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># a new origin to proxy GoatCounter requests</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;custom_origin_config&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;http_port&#34;</span><span class="p">:</span> <span class="mi">80</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;https_port&#34;</span><span class="p">:</span> <span class="mi">443</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;origin_protocol_policy&#34;</span><span class="p">:</span> <span class="s2">&#34;https-only&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;origin_ssl_protocols&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;TLSv1.2&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">            <span class="p">},</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;domain_name&#34;</span><span class="p">:</span> <span class="s2">&#34;nelsonfigueroa.goatcounter.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;origin_id&#34;</span><span class="p">:</span> <span class="s2">&#34;goatcounter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="c1">###</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># rest of configuration</span>
</span></span><span class="line"><span class="cl">    <span class="c1">###</span>
</span></span><span class="line"><span class="cl">    <span class="n">opts</span> <span class="o">=</span> <span class="n">pulumi</span><span class="o">.</span><span class="n">ResourceOptions</span><span class="p">(</span><span class="n">protect</span><span class="o">=</span><span class="kc">True</span><span class="p">))</span></span></span></code></pre></div><p>Now that my Pulumi code has both the CloudFront function defined and the CloudFront distribution has been updated, I ran <code>pulumi up</code> to apply changes.</p>
<h3 id="update-the-goatcounter-script">Update the GoatCounter Script</h3>
<p>Finally, I updated the GoatCounter JavaScript to use the new endpoint. So instead of <code>goatcounter.com</code> I changed the <code>data-goatcounter</code> attribute to my own domain <code>nelson.cloud/gc/count</code>:</p>
<div class="highlight"><span class="code-lang">HTML</span><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">data-goatcounter</span><span class="o">=</span><span class="s">&#34;https://nelson.cloud/gc/count&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="cm">/* rest of script */</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span></span></span></code></pre></div><p>After this, I built my site with Hugo and deployed it on S3/CloudFront by updating the freshly built HTML/CSS/JS in my S3 Bucket and then 

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html" target="_blank" rel="noopener">invalidating the existing CloudFront cache</a>.</p>
<h2 id="verifying-that-it-works">Verifying that it Works</h2>
<p>Now, GoatCounter should no longer be blocked by uBlock Origin. I tested by loading my site on an incognito browser window and checked that uBlock Origin was no longer blocking anything on my domain.</p>
<img src="/proxying-goatcounter-requests/after.webp" alt="uBlock Origin no longer showing a blocked domain" width="720" height="540" style="max-width: 100%; height: auto; aspect-ratio: 1290 / 968;" loading="lazy" decoding="async">
<p>And for further proof, checking the network tab shows a successful <code>POST</code> request to the <code>/gc/count</code> endpoint on my domain along with response headers from AWS/CloudFront:</p>
<img src="/proxying-goatcounter-requests/network.webp" alt="Firefox network tab showing a successful request to /gc/count" width="609" height="720" style="max-width: 100%; height: auto; aspect-ratio: 870 / 1028;" loading="lazy" decoding="async">
<p>Everything looks good!</p>
<h2 id="support-goatcounter">Support GoatCounter</h2>
<p>If you&rsquo;re using GoatCounter you should consider 

<a href="https://github.com/sponsors/arp242/" target="_blank" rel="noopener">sponsoring the developer</a>. It&rsquo;s a great project.</p>
<h2 id="references">References</h2>
<ul>
<li>

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html" target="_blank" rel="noopener">https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html</a></li>
<li>

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistS3AndCustomOrigins.html" target="_blank" rel="noopener">https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistS3AndCustomOrigins.html</a></li>
<li>

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesCacheBehavior.html" target="_blank" rel="noopener">https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesCacheBehavior.html</a></li>
<li>

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html" target="_blank" rel="noopener">https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html</a></li>
<li>

<a href="https://www.goatcounter.com/help/js" target="_blank" rel="noopener">https://www.goatcounter.com/help/js</a></li>
<li>

<a href="https://www.goatcounter.com/help/backend" target="_blank" rel="noopener">https://www.goatcounter.com/help/backend</a></li>
<li>

<a href="https://www.goatcounter.com/help/countjs-host" target="_blank" rel="noopener">https://www.goatcounter.com/help/countjs-host</a></li>
</ul>
]]></content:encoded></item><item><title>GitHub Actions for Pulumi with an AWS S3 Backend</title><link>https://nelson.cloud/github-actions-for-pulumi-with-an-aws-s3-backend/?ref=rss</link><pubDate>Thu, 11 Dec 2025 00:00:00 +0000</pubDate><guid>https://nelson.cloud/github-actions-for-pulumi-with-an-aws-s3-backend/?ref=rss</guid><description>How to set up GitHub Actions for Pulumi when the state is stored in an AWS S3 Bucket.</description><content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>This is a quick guide to set up GitHub Actions for Pulumi with an 

<a href="https://aws.amazon.com/s3/" target="_blank" rel="noopener">AWS S3</a> backend. It builds on a previous guide I wrote: 

<a href="https://nelson.cloud/how-to-use-an-aws-s3-bucket-as-a-pulumi-state-backend/">How to Use an AWS S3 Bucket as a Pulumi State Backend</a>.</p>
<p>This guide assumes you have the following:</p>
<ul>
<li>An AWS S3 Bucket created and ready to be used with Pulumi</li>
<li>An IAM User that has permissions to read/write to the S3 bucket</li>
<li>The Access Key and Secret Access Key for the IAM User to use for authenticating to AWS within GitHub Actions</li>
<li>A passphrase of your choosing that will be used to encrypt secrets in the pulumi stack</li>
<li>A GitHub repository</li>
</ul>
<h2 id="setting-up-repository-secrets">Setting up Repository Secrets</h2>
<p>First, set up secrets on your GitHub repository. These will be filled in by GitHub Actions once we create a workflow YAML. You&rsquo;ll need to create 3 secrets. You can name them whatever you want, but I&rsquo;ll be naming them:</p>
<ul>
<li><code>AWS_ACCESS_KEY_ID</code></li>
<li><code>AWS_SECRET_ACCESS_KEY</code></li>
<li><code>PULUMI_CONFIG_PASSPHRASE</code></li>
</ul>
<p>You can create these by browsing to your GitHub repository &gt; Settings &gt; Secrets and variables &gt; Actions. There are &ldquo;Environment secrets&rdquo; and &ldquo;Repository secrets&rdquo;. In this case go with &ldquo;Repository secrets&rdquo;.</p>
<p>Create the three secrets and fill in their respective values. <code>PULUMI_CONFIG_PASSPHRASE</code> can be whatever you want and doesn&rsquo;t come from AWS.</p>
<blockquote><p><strong>Warning:</strong></p>Make sure you don&rsquo;t change this passphrase after the fact, or your Pulumi state may break.</blockquote>

<p>The end result should look like this:</p>
<img src="/github-actions-with-pulumi-s3/github-secrets.webp" alt="GitHub repository secrets showing AWS credentials and Pulumi passphrase" width="3096" height="1708" style="max-width: 100%; height: auto; aspect-ratio: 774 / 427;" loading="lazy" decoding="async">
<h2 id="defining-the-github-actions-workflow">Defining the GitHub Actions Workflow</h2>
<p>Next, clone the repository locally with <code>git clone</code>. We&rsquo;ll need to create a few files for a minimum viable Pulumi program. Run <code>pulumi new</code> and it&rsquo;ll guide you through the creation of a basic Pulumi program. The language you choose doesn&rsquo;t matter.</p>
<p>Then we can create the YAML file to set up GitHub Actions. Create a YAML file under <code>&lt;your-repository-name&gt;/.github/workflows/</code>. I&rsquo;ll call it <code>preview.yml</code> in my example. Fill it in with the following YAML, changing values as needed.</p>
<div class="highlight"><span class="code-lang">YAML</span><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Pulumi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">main</span><span class="w"> </span><span class="c"># change this if you&#39;re using a different branch name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">preview</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Preview</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Pulumi Preview</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">pulumi/actions@v6.6.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">preview</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">stack-name</span><span class="p">:</span><span class="w"> </span><span class="l">dev</span><span class="w"> </span><span class="c"># change this to your stack&#39;s name (or whatever you want it to be if it doesn&#39;t exist)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cloud-url</span><span class="p">:</span><span class="w"> </span><span class="l">s3://nelson-test-bucket-for-github-actions</span><span class="w"> </span><span class="c"># change this to your bucket name to be used as a pulumi backend</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">upsert</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="c"># creates a stack if it doesn&#39;t exist (along with Pulumi.&lt;stack&gt;.yaml)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">AWS_ACCESS_KEY_ID</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.AWS_ACCESS_KEY_ID }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">AWS_SECRET_ACCESS_KEY</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.AWS_SECRET_ACCESS_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">PULUMI_CONFIG_PASSPHRASE</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.PULUMI_CONFIG_PASSPHRASE }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">AWS_REGION</span><span class="p">:</span><span class="w"> </span><span class="l">us-east-1</span><span class="w"> </span><span class="c"># change the region as needed</span></span></span></code></pre></div><p>This runs a <code>pulumi preview</code> so it&rsquo;ll verify that everything is set up correctly without actually deploying anything.</p>
<h2 id="testing-the-workflow">Testing the Workflow</h2>
<p>Now push your code to GitHub and see if the GitHub Action workflow ran successfully. You should see output from a successful <code>pulumi preview</code>.</p>
<img src="/github-actions-with-pulumi-s3/github-actions-logs.webp" alt="GitHub Actions workflow logs showing successful Pulumi preview" width="2854" height="1632" style="max-width: 100%; height: auto; aspect-ratio: 1427 / 816;" loading="lazy" decoding="async">
<p>If this works, you should be good to go. You can update your Pulumi code and change <code>command: preview</code> to <code>command: up</code> in the GitHub Actions YAML file and run it again to actually deploy some infrastructure.</p>
<h2 id="references">References</h2>
<ul>
<li>

<a href="https://github.com/pulumi/actions" target="_blank" rel="noopener">https://github.com/pulumi/actions</a></li>
</ul>
]]></content:encoded></item><item><title>How to Use an AWS S3 Bucket as a Pulumi State Backend</title><link>https://nelson.cloud/how-to-use-an-aws-s3-bucket-as-a-pulumi-state-backend/?ref=rss</link><pubDate>Sat, 20 Sep 2025 00:00:00 +0000</pubDate><guid>https://nelson.cloud/how-to-use-an-aws-s3-bucket-as-a-pulumi-state-backend/?ref=rss</guid><description>Self-host Pulumi state with an S3 Bucket, an IAM User, and the Pulumi CLI.</description><content:encoded><![CDATA[<p>I&rsquo;ll be starting from scratch and creating an IAM user with access to an S3 bucket that will be used to store the Pulumi state file. If you&rsquo;re working in an enterprise setting, your authentication methods may vary.</p>
<blockquote><p><strong>tl;dr:</strong></p><p>You can run this command (replacing placeholders as needed) if you already have an S3 bucket and AWS credentials configured on your machine:</p>
<div class="highlight"><span class="code-lang">Shell</span><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pulumi login <span class="s1">&#39;s3://&lt;bucket-name&gt;?region=&lt;region&gt;&amp;awssdk=v2&amp;profile=&lt;aws-profile-name&gt;&#39;</span></span></span></code></pre></blockquote>

<p>This post assumes you have the Pulumi CLI installed. Check out the following guide if you don&rsquo;t have it installed: 

<a href="https://www.pulumi.com/docs/iac/download-install/" target="_blank" rel="noopener">Download &amp; install Pulumi</a>.</p>
<h2 id="creating-an-s3-bucket">Creating an S3 Bucket</h2>
<p>First, we need to create the S3 bucket where the Pulumi state file will be stored. I created a bucket called <code>nelsons-pulumi-state-backend</code> and left all the default settings as-is.</p>
<img src="/pulumi-with-s3-backend/s3-bucket.webp" alt="S3 Bucket naming" width="720" height="391" style="max-width: 100%; height: auto; aspect-ratio: 1520 / 824;" loading="lazy" decoding="async">
<h2 id="creating-an-iam-user">Creating an IAM User</h2>
<p>Then we need to create an IAM user in AWS that the Pulumi CLI can use. This IAM user needs permissions to access the S3 bucket we just created.</p>
<p>I go to IAM and create a new user. I just called it <code>pulumi</code>:</p>
<img src="/pulumi-with-s3-backend/iam-user.webp" alt="Creating an IAM user" width="720" height="434" style="max-width: 100%; height: auto; aspect-ratio: 1500 / 904;" loading="lazy" decoding="async">
<p>Then in the next step, I selected &ldquo;Attach policies directly&rdquo; and selected the AWS-managed &ldquo;AdministratorAccess&rdquo; policy just to keep things simple. You can provide more fine-grained access depending on your needs. Then click &ldquo;Next&rdquo; at the bottom.</p>
<img src="/pulumi-with-s3-backend/iam-permissions.webp" alt="IAM permissions" width="720" height="418" style="max-width: 100%; height: auto; aspect-ratio: 1968 / 1144;" loading="lazy" decoding="async">
<p>In the next screen, double-check everything and then click on &ldquo;Create user&rdquo;.</p>
<img src="/pulumi-with-s3-backend/iam-review.webp" alt="Reviewing IAM user" width="720" height="384" style="max-width: 100%; height: auto; aspect-ratio: 2540 / 1356;" loading="lazy" decoding="async">
<p>Now that we have a user with the appropriate permissions, we&rsquo;ll need to get an AWS access key and secret to use with the Pulumi CLI.</p>
<p>Go to your IAM user and click on &ldquo;Create access key&rdquo; on the right side.</p>
<img src="/pulumi-with-s3-backend/iam-user-view.webp" alt="The new IAM user" width="720" height="183" style="max-width: 100%; height: auto; aspect-ratio: 2348 / 596;" loading="lazy" decoding="async">
<p>In the next screen, select &ldquo;Command Line Interface (CLI)&rdquo;. Check the box at the bottom, then click &ldquo;Next&rdquo;.</p>
<img src="/pulumi-with-s3-backend/iam-access-key.webp" alt="Creating an access key" width="720" height="601" style="max-width: 100%; height: auto; aspect-ratio: 1836 / 1532;" loading="lazy" decoding="async">
<p>The next screen will ask for setting a description tag. This is optional. I chose to skip it and clicked on &ldquo;Create access key&rdquo;.</p>
<img src="/pulumi-with-s3-backend/iam-description-tag.webp" alt="Access key description tag" width="720" height="206" style="max-width: 100%; height: auto; aspect-ratio: 1820 / 520;" loading="lazy" decoding="async">
<p>We finally have our Access key and Secret access key. Save these somewhere safe and click &ldquo;Done&rdquo;. (Don&rsquo;t worry, the credentials in the screenshot are fake.)</p>
<img src="/pulumi-with-s3-backend/retrieve-access-keys.webp" alt="Retrieving AWS access keys" width="720" height="435" style="max-width: 100%; height: auto; aspect-ratio: 2244 / 1356;" loading="lazy" decoding="async">
<h2 id="setting-up-aws-credentials-for-the-pulumi-cli">Setting Up AWS Credentials for the Pulumi CLI</h2>
<p>Now we can try using these credentials to tell the Pulumi CLI to use the S3 bucket as a backend.</p>
<blockquote><p><strong>Note:</strong></p>Note that you do NOT need the 

<a href="https://aws.amazon.com/cli/" target="_blank" rel="noopener">AWS CLI</a> installed. Pulumi just needs the AWS credentials.</blockquote>

<p>Create the file <code>~/.aws/credentials</code> if you don&rsquo;t have it. Then add in your credentials there under the <code>[default]</code> profile. (You can add more profiles, but this is beyond the scope of this post.)</p>
<div class="highlight"><span class="code-lang">INI</span><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[default]</span>
</span></span><span class="line"><span class="cl"><span class="na">aws_access_key_id</span> <span class="o">=</span> <span class="s">&lt;key_id&gt;</span>
</span></span><span class="line"><span class="cl"><span class="na">aws_secret_access_key</span> <span class="o">=</span> <span class="s">&lt;access_key&gt;</span></span></span></code></pre></div><p>You&rsquo;ll need the bucket&rsquo;s region and your local AWS profile name to use S3 as a backend.</p>
<p>The command formula looks like this:</p>
<div class="highlight"><span class="code-lang">Shell</span><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pulumi login <span class="s1">&#39;s3://&lt;bucket-name&gt;?region=&lt;region&gt;&amp;awssdk=v2&amp;profile=&lt;aws-profile-name&gt;&#39;</span></span></span></code></pre></div><p>In my case, the command looks like this (make sure to edit for your needs):</p>
<div class="highlight"><span class="code-lang">Shell</span><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pulumi login <span class="s1">&#39;s3://nelsons-pulumi-state-backend?region=us-west-1&amp;awssdk=v2&amp;profile=default&#39;</span></span></span></code></pre></div><p>A successful login shows the following message:</p>
<div class="highlight"><span class="code-lang">Text</span><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Logged in to 0x6E.local as nelson (s3://nelsons-pulumi-state-backend?region=us-west-1&amp;awssdk=v2&amp;profile=default)</span></span></code></pre></div><p>Alternatively, you can add your backend to your <code>Pulumi.yaml</code> file. This is useful if you&rsquo;re working on multiple Pulumi projects that each have different backends. You won&rsquo;t need to run <code>pulumi login</code> all the time. Just add a <code>backend</code> key and a nested <code>url</code> key:</p>
<div class="highlight"><span class="code-lang">YAML</span><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-pulumi-project</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">a pulumi project for testing</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">runtime</span><span class="p">:</span><span class="w"> </span><span class="l">nodejs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># add this section</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">s3://nelsons-pulumi-state-backend?region=us-west-1&amp;awssdk=v2&amp;profile=default</span></span></span></code></pre></div><p>More information here: 

<a href="https://www.pulumi.com/docs/iac/concepts/projects/project-file/" target="_blank" rel="noopener">Pulumi project file reference</a>.</p>
<h2 id="testing-the-setup">Testing The Setup</h2>
<p>Finally, it&rsquo;s time to test this out.</p>
<p>To demonstrate, I created a simple Pulumi program by running:</p>
<div class="highlight"><span class="code-lang">Shell</span><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pulumi new aws-python</span></span></code></pre></div><p>You can choose whatever language you want though.</p>
<p>This is the main Pulumi code that is generated. It&rsquo;s code for creating an S3 bucket:</p>
<div class="highlight"><span class="code-lang">Python</span><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;An AWS Python Pulumi program&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pulumi</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pulumi_aws</span> <span class="kn">import</span> <span class="n">s3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create an AWS resource (S3 Bucket)</span>
</span></span><span class="line"><span class="cl"><span class="n">bucket</span> <span class="o">=</span> <span class="n">s3</span><span class="o">.</span><span class="n">Bucket</span><span class="p">(</span><span class="s1">&#39;my-bucket&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Export the name of the bucket</span>
</span></span><span class="line"><span class="cl"><span class="n">pulumi</span><span class="o">.</span><span class="n">export</span><span class="p">(</span><span class="s1">&#39;bucket_name&#39;</span><span class="p">,</span> <span class="n">bucket</span><span class="o">.</span><span class="n">id</span><span class="p">)</span></span></span></code></pre></div><p>Then I ran <code>pulumi up -y</code> and it worked!</p>
<img src="/pulumi-with-s3-backend/pulumi-up-y.gif" alt="terminal output when running pulumi up -y" width="720" height="475" style="max-width: 100%; height: auto; aspect-ratio: 3532 / 2329;" loading="lazy" decoding="async">
<p>And just to double check, I can see that my previously empty S3 bucket now has contents created by the Pulumi CLI:</p>
<img src="/pulumi-with-s3-backend/s3-bucket-with-contents.webp" alt="S3 Bucket with Pulumi state contents" width="720" height="274" style="max-width: 100%; height: auto; aspect-ratio: 2236 / 850;" loading="lazy" decoding="async">
<p>Everything works!</p>
<hr>
<p>If you&rsquo;re interested in setting up CI/CD with this setup, I wrote a post showing how to do so: 

<a href="https://nelson.cloud/github-actions-for-pulumi-with-an-aws-s3-backend/">GitHub Actions for Pulumi with an AWS S3 Backend</a>.</p>
<h2 id="references">References</h2>
<ul>
<li>

<a href="https://www.pulumi.com/docs/iac/download-install/" target="_blank" rel="noopener">https://www.pulumi.com/docs/iac/download-install/</a></li>
<li>

<a href="https://www.pulumi.com/registry/packages/aws/installation-configuration/" target="_blank" rel="noopener">https://www.pulumi.com/registry/packages/aws/installation-configuration/</a></li>
<li>

<a href="https://www.pulumi.com/docs/iac/concepts/state-and-backends/#aws-s3" target="_blank" rel="noopener">https://www.pulumi.com/docs/iac/concepts/state-and-backends/#aws-s3</a></li>
<li>

<a href="https://www.pulumi.com/docs/iac/concepts/projects/project-file/" target="_blank" rel="noopener">https://www.pulumi.com/docs/iac/concepts/projects/project-file/</a></li>
<li>

<a href="https://ashoksubburaj.medium.com/pulumi-with-aws-s3-as-backend-ac79533820f1" target="_blank" rel="noopener">https://ashoksubburaj.medium.com/pulumi-with-aws-s3-as-backend-ac79533820f1</a></li>
</ul>
]]></content:encoded></item><item><title>Delete All Pulumi Stacks with One Command</title><link>https://nelson.cloud/delete-all-pulumi-stacks-with-one-command/?ref=rss</link><pubDate>Tue, 18 Feb 2025 00:00:00 +0000</pubDate><guid>https://nelson.cloud/delete-all-pulumi-stacks-with-one-command/?ref=rss</guid><description>How to delete all Pulumi stacks with a shell one-liner.</description><content:encoded><![CDATA[<p>There currently isn&rsquo;t a way to delete all stacks with <code>pulumi stack rm</code> so this is an alternative way to achieve that.</p>
<h2 id="delete-all-stacks-in-the-current-project">Delete All Stacks in the Current Project</h2>
<p>To delete all Pulumi stacks in the current Pulumi project you can run the following command:</p>
<div class="highlight"><span class="code-lang">Shell</span><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pulumi stack ls <span class="p">|</span> tail -n +2 <span class="p">|</span> tr -d <span class="s2">&#34;*&#34;</span> <span class="p">|</span> awk <span class="s1">&#39;{print $1}&#39;</span> <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r stack<span class="p">;</span> <span class="k">do</span> pulumi stack rm -y <span class="s2">&#34;</span><span class="nv">$stack</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">done</span><span class="p">;</span></span></span></code></pre></div><h2 id="delete-all-stacks-across-all-projects">Delete All Stacks Across All Projects</h2>
<blockquote><p><strong>Warning:</strong></p>It should go without saying but, <strong>be careful when doing this</strong>.</blockquote>

<p>To delete all Pulumi stacks across all Pulumi projects we need to use <code>pulumi stack ls -a</code> instead of <code>pulumi stack ls</code>. So the full command is:</p>
<div class="highlight"><span class="code-lang">Shell</span><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pulumi stack ls -a <span class="p">|</span> tail -n +2 <span class="p">|</span> tr -d <span class="s2">&#34;*&#34;</span> <span class="p">|</span> awk <span class="s1">&#39;{print $1}&#39;</span> <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r stack<span class="p">;</span> <span class="k">do</span> pulumi stack rm -y <span class="s2">&#34;</span><span class="nv">$stack</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">done</span><span class="p">;</span></span></span></code></pre></div><h2 id="command-breakdown">Command Breakdown</h2>
<p>List pulumi stacks (use <code>-a</code> option for all stacks across all projects):</p>
<div class="highlight"><span class="code-lang">Shell</span><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pulumi stack ls</span></span></code></pre></div><p>Start at the second line of the previous output:</p>
<div class="highlight"><span class="code-lang">Shell</span><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">tail -n +2</span></span></code></pre></div><p>Delete all occurrences of <code>*</code>. There is a <code>*</code> character next to the currently selected stack and we need to remove this:</p>
<div class="highlight"><span class="code-lang">Shell</span><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">tr -d <span class="s2">&#34;*&#34;</span></span></span></code></pre></div><p>Print only the first column:</p>
<div class="highlight"><span class="code-lang">Shell</span><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">awk <span class="s1">&#39;{print $1}&#39;</span></span></span></code></pre></div><p>This is a loop in one-liner format. It reads the previous output line by line and assigns each line to a string variable called <code>stack</code>, then runs the command <code>pulumi stack rm -y</code> on each stack.</p>
<div class="highlight"><span class="code-lang">Shell</span><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="k">while</span> <span class="nb">read</span> -r stack<span class="p">;</span> <span class="k">do</span> pulumi stack rm -y <span class="s2">&#34;</span><span class="nv">$stack</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">done</span><span class="p">;</span></span></span></code></pre></div><h2 id="references">References</h2>
<ul>
<li>

<a href="https://stackoverflow.com/questions/7318497/omitting-the-first-line-from-any-linux-command-output" target="_blank" rel="noopener">https://stackoverflow.com/questions/7318497/omitting-the-first-line-from-any-linux-command-output</a></li>
<li>

<a href="https://www.pulumi.com/docs/iac/cli/commands/pulumi_stack_ls/" target="_blank" rel="noopener">https://www.pulumi.com/docs/iac/cli/commands/pulumi_stack_ls/</a></li>
<li>

<a href="https://www.pulumi.com/docs/iac/cli/commands/pulumi_stack_rm/" target="_blank" rel="noopener">https://www.pulumi.com/docs/iac/cli/commands/pulumi_stack_rm/</a></li>
</ul>
]]></content:encoded></item></channel></rss>