<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adrien F</title>
    <description>The latest articles on DEV Community by Adrien F (@adrienf).</description>
    <link>https://dev.to/adrienf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F141100%2F0eeddc9b-9f78-408d-9bbb-ebdd6384ed02.png</url>
      <title>DEV Community: Adrien F</title>
      <link>https://dev.to/adrienf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adrienf"/>
    <language>en</language>
    <item>
      <title>Use Cloudflare Workers to store your Terraform states</title>
      <dc:creator>Adrien F</dc:creator>
      <pubDate>Tue, 09 Jan 2024 20:46:04 +0000</pubDate>
      <link>https://dev.to/adrienf/use-cloudflare-workers-to-store-your-terraform-states-1kkc</link>
      <guid>https://dev.to/adrienf/use-cloudflare-workers-to-store-your-terraform-states-1kkc</guid>
      <description>&lt;p&gt;&lt;em&gt;TL;DR: Check out the resulting worker &lt;a href="https://github.com/adrien-f/tf-state-worker" rel="noopener noreferrer"&gt;on Github&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When starting a Cloudflare focused project, you’re probably tempted like me to use Terraform to configure everything the right way, and quickly comes the question of where to store the states.&lt;/p&gt;

&lt;p&gt;Terraform Cloud offers a free tier but is limited to 500 managed resources across all states (from what I understand of &lt;a href="https://www.hashicorp.com/products/terraform/pricing" rel="noopener noreferrer"&gt;the pricing page&lt;/a&gt;), and if you're really starting from scratch, you might not already have one of the supported backends ready, be it an object storage engine, Kubernetes or PostgreSQL. So what are we left with?&lt;/p&gt;

&lt;p&gt;Well, for a few years now, Cloudflare has offered an object storage engine called &lt;a href="https://www.cloudflare.com/developer-platform/r2/" rel="noopener noreferrer"&gt;R2&lt;/a&gt;! And what’s even better, is that it’s compatible with the S3 API, enabling us to use it with the Terraform S3 backend. Will it be that easy?&lt;/p&gt;

&lt;h2&gt;
  
  
  The non-obvious S3 way
&lt;/h2&gt;

&lt;p&gt;You might be thinking that since Cloudflare implemented the S3 API, it should be quite easy to configure Terraform to use it. &lt;/p&gt;

&lt;p&gt;Create a new R2 bucket in the Cloudflare dashboard, and then a new API token scoped to the bucket. You will be provided with the proper credentials and URLs to provide to Terraform:&lt;/p&gt;

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

&lt;p&gt;Finally, we just need to override the S3 URL with our bucket, provide our access keys and voilà, we can run &lt;code&gt;terraform init&lt;/code&gt; and go on our way. You start to read &lt;a href="https://developer.hashicorp.com/terraform/language/settings/backends/s3" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt; and see that you can indeed override the endpoints. It might look like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tf-states"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"foo.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;endpoints&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://xxx.r2.cloudflarestorage.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"xxx"&lt;/span&gt;
    &lt;span class="nx"&gt;secret_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"xxx"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;⚠ Please don't commit your access/secret keys, use environment variables or a secret engine 😅&lt;/p&gt;

&lt;p&gt;But sadly, you will quickly get an error:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

❯ terraform init

Initializing the backend...
╷
│ Error: Missing region value
│
│   on main.tf line 2, &lt;span class="k"&gt;in &lt;/span&gt;terraform:
│    2:   backend &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
│
│ The &lt;span class="s2"&gt;"region"&lt;/span&gt; attribute or the &lt;span class="s2"&gt;"AWS_REGION"&lt;/span&gt; or &lt;span class="s2"&gt;"AWS_DEFAULT_REGION"&lt;/span&gt; environment variables must be set.


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

&lt;/div&gt;

&lt;p&gt;Alright, that makes sense, let’s add a region parameter. Reading &lt;a href="https://developers.cloudflare.com/r2/api/s3/api/#bucket-region" rel="noopener noreferrer"&gt;R2 documentation&lt;/a&gt;, we can safely set it to &lt;code&gt;us-east-1&lt;/code&gt; .&lt;/p&gt;

&lt;p&gt;This will look like this, will we finally get lucky?&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tf-states"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"foo.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;endpoints&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://xxx.r2.cloudflarestorage.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"xxx"&lt;/span&gt;
    &lt;span class="nx"&gt;secret_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"xxx"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Wrong!&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

❯ terraform init

Initializing the backend...
╷
│ Error: validating provider credentials: retrieving &lt;span class="nb"&gt;caller &lt;/span&gt;identity from STS: operation error STS: GetCallerIdentity, https response error StatusCode: 403, RequestID: abb0da81-771e-4724-ad9b-842ceb81e6de, api error InvalidClientTokenId: The security token included &lt;span class="k"&gt;in &lt;/span&gt;the request is invalid.
│
│
╵


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

&lt;/div&gt;

&lt;p&gt;And so this will go on and on. This &lt;a href="https://github.com/hashicorp/terraform/issues/33847" rel="noopener noreferrer"&gt;Github issue&lt;/a&gt; summarizes everything everyone went through to identify the right set of parameters, and you will also find &lt;a href="https://developers.cloudflare.com/r2/examples/terraform-aws/" rel="noopener noreferrer"&gt;more information&lt;/a&gt; if you want to use the AWS provider to use some S3 resources with R2. This is how your backend configuration should look like:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;backend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;bucket&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tf-stats"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;key&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"foo.tfstate"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;endpoints&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;s&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://xxx.r2.cloudflarestorage.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;access_key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xxx"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;secret_key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xxx"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;region&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="err"&gt;skip_credentials_validation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;skip_region_validation&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;skip_requesting_account_id&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;skip_metadata_api_check&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;skip_s&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;_checksum&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;


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

&lt;/div&gt;

&lt;p&gt;And finally, it goes through 🎉&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

❯ terraform init

Initializing the backend...

Successfully configured the backend &lt;span class="s2"&gt;"s3"&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...

Terraform has been successfully initialized!


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

&lt;/div&gt;

&lt;p&gt;So we can store states, that’s great, right? The configuration is a bit verbose, and you’ll need to repeat these as well so if you use CDKTF, you can quickly write it as a function:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;constructs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;S3Backend&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cdktf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Configure a S3 backend for Cloudflare R2
 * You will need to set the following environment variables:
 * - AWS_ACCESS_KEY_ID &amp;gt; xxx
 * - AWS_SECRET_ACCESS_KEY &amp;gt; xxx
 * - AWS_ENDPOINT_URL_S3 &amp;gt; https://xxx.r2.cloudflarestorage.com
 *
 * @param scope This needs to be a Stack
 * @param stateName The name of the state file
 * @param bucketName The name of the R2 bucket
 * @returns
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cloudflareR2Backend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;stateName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;bucketName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;S3Backend&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;S3Backend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bucketName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stateName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;skipCredentialsValidation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;skipMetadataApiCheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;skipRegionValidation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;skipRequestingAccountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;skipS3Checksum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;


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

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ At the time of writing, all the S3 backend configuration keys were not backported to CDKTF. They’ve been updated with &lt;a href="https://github.com/hashicorp/terraform-cdk/pull/3352" rel="noopener noreferrer"&gt;this PR&lt;/a&gt; and will land in the v0.20 release. You can install a pre-release in the meantime.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Alright, this was fun right, we’re done, right? Well… Not exactly. You see, one key feature of the AWS S3 Backend is that it also supports state locking via DynamoDB, a NoSQL datastore. These locks prevent others from overwriting the state file while you’re also doing something on the stack and are pretty much needed when multiple people work on the same projects. If you need this feature, please read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Worker way
&lt;/h2&gt;

&lt;p&gt;Cloudflare does not have a DynamoDB API compatible service, so if we want to have lock support, we have to either implement some kind of Frankenstein DynamoDB API Worker for Terraform to use it (which sounds like a fun weekend to be honest), or evaluate another backend. In this case, we’ll look at the &lt;a href="https://developer.hashicorp.com/terraform/language/settings/backends/http" rel="noopener noreferrer"&gt;HTTP backend&lt;/a&gt; and implement it with a Cloudflare Worker, backed by R2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design
&lt;/h3&gt;

&lt;p&gt;The HTTP backend needs an API service to implement the basic GET, POST, and DELETE methods. For the lock feature, it can use the LOCK and UNLOCK methods from &lt;a href="https://datatracker.ietf.org/doc/html/rfc2518" rel="noopener noreferrer"&gt;RFC2518&lt;/a&gt; (WebDav). Not all frameworks and HTTP servers might support these unusual methods so they are configurable by Terraform. In our case, no need to do that, they will work natively.&lt;/p&gt;

&lt;p&gt;So we will develop a quick Worker to manage Terraform states with R2 storage. What about our locks?&lt;/p&gt;

&lt;p&gt;You might be aware that Cloudflare has a KV value product which could be a tempting use case for the locks and it was my first thought but in the case of locks, consistency is important (to avoid race conditions if you have a coworker in the US and another in Asia), and only R2 has &lt;a href="https://developers.cloudflare.com/r2/reference/consistency/" rel="noopener noreferrer"&gt;a strong consistency model&lt;/a&gt; so we will also use it to store locks. &lt;em&gt;(I’m also aware of the &lt;a href="https://developers.cloudflare.com/durable-objects/" rel="noopener noreferrer"&gt;Durable Objects&lt;/a&gt; product, it could be an interesting alternative!)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing a worker
&lt;/h3&gt;

&lt;p&gt;I’ve been wanting to write a Worker for quite some time and already had a library on my radar: &lt;a href="https://hono.dev/" rel="noopener noreferrer"&gt;Hono&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;It is a full-featured HTTP router with support for Cloudflare Workers and so onward I went with it and created my project like so: &lt;code&gt;npm create hono@latest tf-state-worker&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;This will set up a basic project and configure &lt;code&gt;wrangler&lt;/code&gt;, a Cloudflare tool to develop and deploy your brand-new worker.&lt;/p&gt;

&lt;p&gt;From then, it was a matter of implementing the API (which is not exactly documented, a lot of reading &lt;a href="https://github.com/hashicorp/terraform/blob/main/internal/backend/remote-state/http/backend.go" rel="noopener noreferrer"&gt;Terraform source code&lt;/a&gt;) for reading and writing states, as well as the locking logic:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statesRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Hono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;STATE_BUCKET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;R2Bucket&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;statesRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:stateId{[a-zA-Z0-9][&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.]*}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;statesRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:stateId{[a-zA-Z0-9][&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.]*}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;statesRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:stateId{[a-zA-Z0-9][&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.]*}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;statesRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LOCK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:stateId{[a-zA-Z0-9][&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.]*}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;statesRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UNLOCK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:stateId{[a-zA-Z0-9][&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.]*}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;As a bonus, I quickly wrote a listing endpoint, which could also probably send some HTML later:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

❯ curl https://tf-state-worker.xxx.workers.dev/states
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"states"&lt;/span&gt;:[&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;:&lt;span class="s2"&gt;"states/foo.tfstate"&lt;/span&gt;,&lt;span class="s2"&gt;"size"&lt;/span&gt;:247,&lt;span class="s2"&gt;"uploaded"&lt;/span&gt;:&lt;span class="s2"&gt;"2024-01-07T22:21:52.030Z"&lt;/span&gt;&lt;span class="o"&gt;}]&lt;/span&gt;,&lt;span class="s2"&gt;"locks"&lt;/span&gt;:[]&lt;span class="o"&gt;}&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  The result
&lt;/h3&gt;

&lt;p&gt;The resulting worker can be found at &lt;a href="https://github.com/adrien-f/tf-state-worker" rel="noopener noreferrer"&gt;https://github.com/adrien-f/tf-state-worker&lt;/a&gt; and you can follow along the README to deploy it on your Cloudflare account. As an interesting exercise to you dear reader, I suppose with a few tweaks this could be deployed as a Deno worker with their KV storage solution.&lt;/p&gt;

&lt;p&gt;Clone the repository on your development environment, install everything then move the &lt;code&gt;wrangler_example.toml&lt;/code&gt; to &lt;code&gt;wrangler.toml&lt;/code&gt;, and finally run &lt;code&gt;npx wrangler deploy&lt;/code&gt; to get an URL pointing to your new worker:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;

&lt;span class="err"&gt;❯&lt;/span&gt; &lt;span class="nx"&gt;npx&lt;/span&gt; &lt;span class="nx"&gt;wrangler&lt;/span&gt; &lt;span class="nx"&gt;deploy&lt;/span&gt;
 &lt;span class="err"&gt;⛅️&lt;/span&gt; &lt;span class="nx"&gt;wrangler&lt;/span&gt; &lt;span class="mf"&gt;3.22&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="o"&gt;-------------------&lt;/span&gt;
&lt;span class="nx"&gt;Your&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="nx"&gt;has&lt;/span&gt; &lt;span class="nx"&gt;access&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;following&lt;/span&gt; &lt;span class="nx"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;R2&lt;/span&gt; &lt;span class="nx"&gt;Buckets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;STATE_BUCKET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tf&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;states&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;Vars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;AUTH_PLUGIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="nx"&gt;Total&lt;/span&gt; &lt;span class="nx"&gt;Upload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;55.37&lt;/span&gt; &lt;span class="nx"&gt;KiB&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;12.93&lt;/span&gt; &lt;span class="nx"&gt;KiB&lt;/span&gt;
&lt;span class="nx"&gt;Uploaded&lt;/span&gt; &lt;span class="nx"&gt;tf&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;worker &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.06&lt;/span&gt; &lt;span class="nx"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;Published&lt;/span&gt; &lt;span class="nx"&gt;tf&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;worker &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;6.00&lt;/span&gt; &lt;span class="nx"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//tf-state-worker.xxx.workers.dev&lt;/span&gt;
&lt;span class="nx"&gt;Current&lt;/span&gt; &lt;span class="nx"&gt;Deployment&lt;/span&gt; &lt;span class="nx"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;xxx&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Aaaand wait a minute, no security? Dear astute reader, we're indeed missing something quite important. We do not want our worker to accept any requests to read and write anything in your R2 bucket, especially since states could contain sensitive data 😱!&lt;/p&gt;

&lt;p&gt;Since security could be implemented in many ways, by default the worker will reject all requests. There is a plugin system with the first one being a basic username/password combo, but JWT support is planned. By default, the worker will not work until an implementation is configured.&lt;/p&gt;

&lt;p&gt;As an alternative, you could also put that worker behind Zero Trust or an mTLS-secured route with &lt;a href="https://developers.cloudflare.com/api-shield/security/mtls/configure/" rel="noopener noreferrer"&gt;API Shield&lt;/a&gt;. The choice is yours.&lt;/p&gt;

&lt;p&gt;With that done, it’s just a matter of configuring your backend:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://foo:bar@tf-state-worker.xxx.workers.dev/states/foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;lock_address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://foo:bar@tf-state-worker.xxx.workers.dev/states/foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;unlock_address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://foo:bar@tf-state-worker.xxx.workers.dev/states/foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;And there we go, a functional Terraform state solution with locks:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;

&lt;p&gt;&lt;span class="err"&gt;❯&lt;/span&gt; &lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="nx"&gt;apply&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;approve&lt;/span&gt;&lt;br&gt;
&lt;span class="nx"&gt;Acquiring&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;This&lt;/span&gt; &lt;span class="nx"&gt;may&lt;/span&gt; &lt;span class="nx"&gt;take&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;few&lt;/span&gt; &lt;span class="nx"&gt;moments&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="nx"&gt;Changes&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;Outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;br&gt;
  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="nx"&gt;You&lt;/span&gt; &lt;span class="nx"&gt;can&lt;/span&gt; &lt;span class="nx"&gt;apply&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;save&lt;/span&gt; &lt;span class="nx"&gt;these&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;Terraform&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;without&lt;/span&gt; &lt;span class="nx"&gt;changing&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="nx"&gt;real&lt;/span&gt; &lt;span class="nx"&gt;infrastructure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="nx"&gt;Apply&lt;/span&gt; &lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nx"&gt;Resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="nx"&gt;added&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="nx"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="nx"&gt;destroyed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="nx"&gt;Outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;/p&gt;

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

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  What’s next?&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;On the TODO list I have for the worker is a better HTML view, some audit logs, webhook support, and more. Contributions are super much welcomed so let’s get in touch in Github's issues 🙂&lt;/p&gt;

&lt;p&gt;Terraform also has a Remote backend which is used by some external vendors like Jfrog to store state, but implementation documentation can not be found and I've reached my limit of reverse-engineering APIs for the weekend, until next time!&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>cloudflare</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Rollbacking Helm releases from an UI with Windmill - Part 2</title>
      <dc:creator>Adrien F</dc:creator>
      <pubDate>Sun, 10 Dec 2023 12:48:59 +0000</pubDate>
      <link>https://dev.to/adrienf/rollbacking-helm-releases-from-an-ui-with-windmill-part-2-f9f</link>
      <guid>https://dev.to/adrienf/rollbacking-helm-releases-from-an-ui-with-windmill-part-2-f9f</guid>
      <description>&lt;p&gt;Welcome back to part two of our Windmill series! Today, we'll learn how to chain together our scripts into a flow and trigger it using the user interface we made before. Let's begin!&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a test release
&lt;/h2&gt;

&lt;p&gt;To avoid disrupting too much our Kubernetes infrastructure, we’ll install a test release so we can play with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade &lt;span class="nt"&gt;-i&lt;/span&gt; test-a oci://ghcr.io/stefanprodan/charts/podinfo
&lt;span class="c"&gt;# another time so we can rollback it&lt;/span&gt;
helm upgrade &lt;span class="nt"&gt;-i&lt;/span&gt; test-a oci://ghcr.io/stefanprodan/charts/podinfo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And refreshing our UI, the release will appear ready to be controlled from our table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going with the flow
&lt;/h2&gt;

&lt;p&gt;To showcase the last (and not the least) feature of Windmill, let’s put together a script to rollback a release and then send a notification on Slack, we’re going to build a Flow!&lt;/p&gt;

&lt;p&gt;Let’s start by clicking on the “+ Flow” button on our homepage, and we’ll start by giving it a good name, and then click on the “Input” box on the diagram on the left.&lt;/p&gt;

&lt;p&gt;We’ll be prompted for the inputs of our flow. You can go a few ways, one really nice feature is that Windmill can open an endpoint to which you can POST data, which will then get converted automatically into inputs. But in this case, we’ll need only two inputs: the release name and its namespace.&lt;/p&gt;

&lt;p&gt;And it will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--F3SNkuBM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mua91vkxo66laaf8cbna.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--F3SNkuBM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mua91vkxo66laaf8cbna.png" alt="Input of our flow" width="800" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With that, let’s click on the “+” button just below inputs to add our script:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fe-UuRU4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnoixygiphhbs7i0kgmh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fe-UuRU4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnoixygiphhbs7i0kgmh.png" alt="New action" width="800" height="833"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’ll create a new Bash inline script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# shellcheck shell=bash&lt;/span&gt;
&lt;span class="c"&gt;# arguments of the form X="$I" are parsed as parameters X of type string&lt;/span&gt;
&lt;span class="nv"&gt;helm_release&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;helm rollback &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$namespace&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$helm_release&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;

jq &lt;span class="nt"&gt;--null-input&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; output &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; status &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s1"&gt;'{"output": $output, "status": $status}'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ./result.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this script, we take our two inputs, store the rollback command and its status in variables then use JQ to quickly construct a JSON valid output. This will enable us to handle any errors.&lt;/p&gt;

&lt;p&gt;And, as for the UI, by clicking on the “Plug” connect icon, we can pass our flow inputs to this step:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QvfXA-sN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lax7i4h5n4gxn3paa2ro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QvfXA-sN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lax7i4h5n4gxn3paa2ro.png" alt="Rollback script" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before testing the rollback, we will need to add new permissions to our workers so they can actually do the rollback. This step will be left to your discretion.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ This is all for the sake of the demo but remember to properly separate the workers into their own pool with their proper permissions and lock down on their usage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now that we have the rollback, let’s also send a notification to Slack! You will need to create a new application for your workspace, the &lt;a href="https://api.slack.com/start/quickstart#scopes"&gt;Quick Start&lt;/a&gt; guide is the recommended way. Come back when you have an App Token (it should start with &lt;code&gt;xoxb&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;Let’s click again on the “+” sign after our rollback script, and let’s do a quick search on the community hub for a script to send a message, you can directly search for “Send message to channel (slack)”. If you’re prompted to add inputs to your flow, reject it.&lt;/p&gt;

&lt;p&gt;This community script is the perfect occasion to showcase another feature of Windmill: “Resources”.&lt;/p&gt;

&lt;p&gt;A resource is simply a connector to another service like Datadog, AWS, SQL database, etc… It’s a great way to share scoped accesses to services inside a script. Here,  we’ll go ahead and click on the “+” sign next to the Slack Resource input, paste our App token and save it.  &lt;/p&gt;

&lt;p&gt;We’ll also provide the channel and write a templated text message, and since it’s a JS expression we can interpolate some variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="s2"&gt;`Rollbacked &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;flow_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;helm_release&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt; in namespace &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;flow_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should now look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QJt9AImM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b0j5tr4n2o7tdhzn92ok.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QJt9AImM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b0j5tr4n2o7tdhzn92ok.png" alt="Slack script" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, if our rollback fails, we may want to handle this error and send a different Slack notification. We could do this using a ternary condition in our 'text' input. But for the sake of this example, let's create a branch. Click on the "+" sign before our Slack script and select the "Branch to one" option. To handle cases when our status is not 0, we'll edit the predicate. We'll then duplicate and align our Slack messages accordingly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AxUFSfYm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o2kti8sm6t1q44yost1c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AxUFSfYm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o2kti8sm6t1q44yost1c.png" alt="Branch configuration" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: in practice you would fork the Slack script and handle the status directly with JS for example, this way you could also customize the message sent using &lt;a href="https://api.slack.com/block-kit"&gt;Slack Blocks&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's test our new flow. If everything was configured correctly, we should see the result window:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3YGS5Bo_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0n845fijfwyj3iu5vmsl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3YGS5Bo_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0n845fijfwyj3iu5vmsl.png" alt="Test run" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, let's integrate this into our UI to wrap up for the day! Deploy the flow and then return to the App UI editor. Here, select our table component. On the right, there's a section titled "Table Actions", where we can set up various interactions with our table rows. For now, let's add a new button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6fFYff0c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgoer61z3cwab0gmaewx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6fFYff0c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgoer61z3cwab0gmaewx.png" alt="Table configuration" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can now rename the button, change its color, and even add an icon. Once you're satisfied, select "Event Handler → Select a script or flow" to choose the Helm Release flow we created earlier. Then, you can input the release and namespace based on the table row. The final result should look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9ZO91QGK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gksiwifgk6xggwdsh6qp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9ZO91QGK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gksiwifgk6xggwdsh6qp.png" alt="Button configuration" width="800" height="755"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the end of the settings, make sure to toggle the recompute of the table so changes are reloaded! And now, if all goes well, you should have a shiny UI with a functional rollback buttons, and Slack notifications 🎉&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--F-ppXzbF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oxnaqvdx47ios0pwifh2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--F-ppXzbF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oxnaqvdx47ios0pwifh2.png" alt="Full table" width="800" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you click on Rollback, the following notification will be sent to your Slack channel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6Lo2h_Wo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rgsiogm2o9k3u84yxfle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6Lo2h_Wo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rgsiogm2o9k3u84yxfle.png" alt="Slack notification" width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;That's all for now! If you enjoyed this series, let me know and we can certainly plan another deep dive into the flow feature. We can explore how to connect more data sources to build more complex user interfaces! I hope you enjoyed this presentation of Windmill. See you soon!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>lowcode</category>
      <category>windmill</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Building a no-code Helm UI with Windmill - Part 1</title>
      <dc:creator>Adrien F</dc:creator>
      <pubDate>Wed, 06 Dec 2023 15:28:08 +0000</pubDate>
      <link>https://dev.to/adrienf/building-a-no-code-helm-ui-with-windmill-1mem</link>
      <guid>https://dev.to/adrienf/building-a-no-code-helm-ui-with-windmill-1mem</guid>
      <description>&lt;p&gt;Hey everyone! Today I would like to introduce you to &lt;a href="https://www.windmill.dev/" rel="noopener noreferrer"&gt;Windmill&lt;/a&gt;, a serverless platform enabling you to write scripts in many languages, chain them together, build an UI around them and much, much more. Sounds too good to be true? Well let’s put it to the test by building together a dynamic UI for managing Helm charts in a Kubernetes cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;There's no doubt that you've found yourself in a situation like this at some point: you’ve been asked to build a quick workflow, where you had to chain a few actions, send a notification somewhere or a piece of data into a system. You have the code ready but then, all of that needs to be packaged and deployed (after tests), and if you’re lucky, you already have all the resources and systems for that part of the process.&lt;/p&gt;

&lt;p&gt;But releasing your code into the world is only the initial step in a larger process. Once the code is out there, it needs to be scheduled, executed and observed. This doesn't just happen on its own and you need systems in place to do just that.&lt;/p&gt;

&lt;p&gt;This is where serverless Cloud solutions such as &lt;a href="https://aws.amazon.com/fr/step-functions/" rel="noopener noreferrer"&gt;“AWS Step Functions”&lt;/a&gt; caters to these needs. Step Functions allows you to chain Lambdas, and call upon AWS services with extra logic to your workflow. In essence, it provides a robust platform that allows your code to function as intended, while also offering the necessary tools to monitor and optimize its performance.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;See &lt;a href="https://aws.amazon.com/fr/blogs/aws/new-aws-step-functions-workflow-studio-a-low-code-visual-tool-for-building-state-machines/" rel="noopener noreferrer"&gt;https://aws.amazon.com/fr/blogs/aws/new-aws-step-functions-workflow-studio-a-low-code-visual-tool-for-building-state-machines/&lt;/a&gt; for more info! &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cloud solutions are a great fit indeed, and they’ll probably be good enough for many use cases. But today we’ll talk about an entire platform centered around these workflows, a platform where you could share actions with your coworkers, tie them together in workflows and build UIs around them. &lt;/p&gt;

&lt;p&gt;And that’s where Windmill comes in, with three key concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scripts, which are re-usable pieces of code that have inputs and outputs&lt;/li&gt;
&lt;li&gt;Flows, which tie scripts together with logic, branching and retries&lt;/li&gt;
&lt;li&gt;Apps, made of dynamic React components which can trigger and display scripts and flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these three building blocks, you’ll be able to create powerful and reliable workflows, let’s put it to the test.&lt;/p&gt;

&lt;p&gt;I’ve created a local cluster with &lt;a href="https://k3s.io/" rel="noopener noreferrer"&gt;K3S&lt;/a&gt; and installing Windmill could not be simpler with just one chart to configure, which already has sane defaults to get started. For this demo we will also configure workers to passthrough environment variables to our scripts so that they have access to the Kubernetes API server for later.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

helm repo add windmill https://windmill-labs.github.io/windmill-helm-charts/
helm show values windmill/windmill &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; values.yaml
yq &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'.windmill.workerGroups[].extraEnv += {"name": "WHITELIST_ENVS", "value": "KUBERNETES_SERVICE_HOST,KUBERNETES_SERVICE_PORT"}'&lt;/span&gt; values.yaml
helm &lt;span class="nb"&gt;install &lt;/span&gt;windmill windmill/windmill &lt;span class="nt"&gt;--namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;windmill &lt;span class="nt"&gt;--create-namespace&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; values.yaml &lt;span class="nt"&gt;--set&lt;/span&gt; windmill.appReplicas&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;--set&lt;/span&gt; windmill.lsbReplicas&lt;span class="o"&gt;=&lt;/span&gt;1


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

&lt;/div&gt;

&lt;p&gt;Note: see the &lt;a href="https://github.com/windmill-labs/windmill-helm-charts/blob/main/README.md" rel="noopener noreferrer"&gt;Chart’s README&lt;/a&gt; for all potential values. I recommend managing them finely, with dedicated service accounts for example.&lt;/p&gt;

&lt;p&gt;We’ll then watch the pods being created:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get pods
NAME                                       READY   STATUS    RESTARTS   AGE
windmill-app-567858d877-gqp26              0/1     Running   0          32s
windmill-lsp-7bd57bfdfc-bdfg8              1/1     Running   0          32s
windmill-postgresql-0                      1/1     Running   0          32s
windmill-workers-default-dc4cc4d76-5wvtl   1/1     Running   0          32s
windmill-workers-native-8df7667f-6xl29     1/1     Running   0          32s                   0/1     ContainerCreating   0          20s


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

&lt;/div&gt;

&lt;p&gt;While we wait, let’s explore a bit more the architecture of the project and what’s being deployed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We have a PostgreSQL instance starting&lt;/li&gt;
&lt;li&gt;An app deployment for the UI and API&lt;/li&gt;
&lt;li&gt;Several workers deployments to execute our scripts&lt;/li&gt;
&lt;li&gt;And an LSP server to provide autocompletions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once running, you’ll notice an Ingress configuration has also been created, you can adjust the hostname either via values or via the initial configuration steps. Open a browser to your ingress and you’ll be greeted with the default login screen, it will be &lt;code&gt;admin@windmill.dev:changeme&lt;/code&gt;:&lt;/p&gt;

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

&lt;p&gt;And after a few settings that you can tweak, you’ll be prompted to change the admin &amp;amp; password email and finally you’ll be able to create a new workspace that will contains all your workflows.&lt;/p&gt;

&lt;p&gt;⚠️ There is an opt-out Telemetry option you might not want to miss if testing in a sensitive environment, but it’s also helpful data for the project to grow! &lt;/p&gt;

&lt;p&gt;And there you go, we’re in !&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Building an Helm UI
&lt;/h2&gt;

&lt;p&gt;To showcase the basic set of features of Windmill, let’s build ourselves a quick Helm UI, that our users could then use to list and rollback releases. On rollback, we’ll also send a notification to Slack. &lt;/p&gt;

&lt;h3&gt;
  
  
  Listing namespaces
&lt;/h3&gt;

&lt;p&gt;We’ll need a script to list our cluster namespaces, and then the releases it contains.&lt;/p&gt;

&lt;p&gt;Click on the “+ Script” button at the top of the window, we’ll select Python as the runtime, give it a good name, and you can click on the editor on the left to get the Settings window out of the way.&lt;/p&gt;

&lt;p&gt;We’ll import the &lt;a href="https://github.com/kr8s-org/kr8s" rel="noopener noreferrer"&gt;&lt;code&gt;kr8s&lt;/code&gt;&lt;/a&gt; package to interact with the cluster, and when you’ll remove arguments from the &lt;code&gt;main&lt;/code&gt; function, you will also see them disappear from the input section! Quite neat.&lt;/p&gt;

&lt;p&gt;Let’s write a script to return a list of current namespaces:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kr8s&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;kr8s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;namespaces&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;And by pressing CTRL+Enter, execution will start and will error-out:&lt;/p&gt;

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

&lt;p&gt;Which should not be surprising as we did not configure additional permissions to Windmill workers.&lt;/p&gt;

&lt;p&gt;Let’s add a quick ClusterRole and ClusterRoleBinding to the &lt;code&gt;windmill&lt;/code&gt; user. In practice, we would start adding a custom ServiceAccount for this dedicated worker pool:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterRole&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windmill-k8s&lt;/span&gt;
&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;namespaces"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterRoleBinding&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windmill-k8s&lt;/span&gt;
&lt;span class="na"&gt;subjects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceAccount&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windmill&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windmill&lt;/span&gt;
&lt;span class="na"&gt;roleRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterRole&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;windmill-k8s&lt;/span&gt;
  &lt;span class="na"&gt;apiGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Re-running the job, we will see the expected result:&lt;/p&gt;

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

&lt;p&gt;Note the "found cached resolution" debug line. It indicates that Windmill computed and cached all the dependencies necessary to run this script. This will prevent subsequent pip install actions until changes are made.&lt;/p&gt;

&lt;p&gt;You can now click on the Deploy button at the top right and CTRL+ENTER on your keyboard to execute the script, where you'll get more details on the duration, memory usage and status.&lt;/p&gt;

&lt;h3&gt;
  
  
  Listing Helm releases
&lt;/h3&gt;

&lt;p&gt;Since Helm is included by default in the worker's Docker image (thanks for that!), we're able to create a new Bash script:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="c"&gt;# shellcheck shell=bash&lt;/span&gt;
&lt;span class="nv"&gt;namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
helm list &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$namespace&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ./result.json


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

&lt;/div&gt;

&lt;p&gt;Our script is accepting an argument “namespace” and we’ll write the result to &lt;code&gt;result.json&lt;/code&gt; which will be parsed by Windmill and made available.&lt;/p&gt;

&lt;p&gt;We will now have to adjust our RBAC permissions to grant read on the secrets:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

kubectl patch clusterrole windmill-k8s &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'json'&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'[{"op": "add", "path": "/rules/-", "value": {"apiGroups": [""], "resources": ["secrets"], "verbs": ["get", "list"]}}]'&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;And by saving and running our script, we’ll see our list of releases:&lt;/p&gt;

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

&lt;p&gt;Notice how we automatically had an input field for our namespace? Windmill automatically built a quick UI for our script so we're able to run them whenever we want.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building an Helm UI
&lt;/h3&gt;

&lt;p&gt;With our first scripts ready, we are now able to chain them together to build an UI! Back on the homepage of the workspace, let’s click on the “+ App” button which will open the App editor.&lt;/p&gt;

&lt;p&gt;There are many things to present of this editor, but I will refer you to the &lt;a href="https://www.youtube.com/playlist?list=PLITtG6lMX-nJRtJMQnYrPf5eubN2xWjkA" rel="noopener noreferrer"&gt;Youtube playlist&lt;/a&gt; made by Windmill authors and the &lt;a href="https://www.windmill.dev/docs/getting_started/apps_quickstart" rel="noopener noreferrer"&gt;documentation website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For now, let’s focus on our quick &amp;amp; dirty! Scroll down the list of available components and drag and drop the “Select” component.&lt;/p&gt;

&lt;p&gt;Now we need to prepare the list of namespaces for this component. At the bottom of the editor you will find the “Background Runnable” section, click on the + (plus) sign to create a new runnable, click on “Select a script or flow” then on the opened window, select the “Workspace scripts”, then finally, your script listing namespaces.&lt;/p&gt;

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

&lt;p&gt;Note: you might have not missed the “Frontend” language section when creating a new background runnable, you can indeed add extra Javascript code that will be executed in the browser.&lt;/p&gt;

&lt;p&gt;The Select input form expect data in a specific format, so we’ll add a transformer, which is just a quick Javascript function to transform our namespaces into what’s expected. When selecting the runnable, you will notice on the right in the settings panel, a “Transformer” section.&lt;/p&gt;

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

&lt;p&gt;Click on the “Add” button and in the new script view, transform the result then click on “Run”:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;You can inspect the current state of the app and view our reflected data model.&lt;/p&gt;

&lt;p&gt;Let’s now plug our Select input. Click on the component, and on its configuration tab, click on the little plug icon next to “Items”, this will allow us to configure the data source for this select. Click on the “result” button in the &lt;code&gt;bg_0&lt;/code&gt; output and the select will be automatically populated!&lt;/p&gt;

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

&lt;p&gt;Next up, we’ll do the same thing with the Helm releases. We create a new background runnable, we select our “List Helm releases” script and this time we’ll notice we’re expected to configure the input. Again, click on the “Connect” plug symbol and we’ll select the result of our select component in the Outputs panel at the left (or you can also write &lt;code&gt;a.result&lt;/code&gt;) if you’re in a hurry. Make sure to disable the “Run on start and app refresh” option as we’ll need to have the namespaces first.&lt;/p&gt;

&lt;p&gt;Now, every changes to the select value will make Windmill execute again the script with the proper variable, making that data available. Let’s display it with the Table component!&lt;/p&gt;

&lt;p&gt;Again, drag &amp;amp; drop the Table component, and select the plug symbol. Select the result of &lt;code&gt;bg_1&lt;/code&gt; which is our list of Helm releases, and boom, you’ve got a table with your releases:&lt;/p&gt;

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

&lt;p&gt;When you select another namespace, the table will refresh with the releases from this namespace. Adjust width and re-organization or add more components if you wish, then you can click on Deploy to get a pretty view that you can share with coworkers:&lt;/p&gt;

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

&lt;p&gt;Great success! You can re-order columns or even hide some if you want by configuring the component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;And that's it for this first part! I hope I was able to share with you the same excitation I had when I first started using this tool! In the next and final part, we'll look at how we can implement a Flow and add a Rollback button to our table. See you!&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;p&gt;The following resources might be of interest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/@WindmillDev" rel="noopener noreferrer"&gt;Windmill Youtube channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.windmill.dev/docs/intro" rel="noopener noreferrer"&gt;Windmill documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Trevor Sullivan has made &lt;a href="https://www.youtube.com/playlist?list=PLDbRgZ0OOEpWT68Jn_I-B3KpZ9xp2Vukn" rel="noopener noreferrer"&gt;a series of videos&lt;/a&gt; using Windmill if you want to deep dive&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>windmill</category>
      <category>kubernetes</category>
      <category>serverless</category>
      <category>lowcode</category>
    </item>
  </channel>
</rss>
