<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet type="text/xsl" href="https://perrotta.dev/rss.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Terraform on ¬ just serendipity 🍀</title>
    <link>https://perrotta.dev/</link>
    <description>Recent content in Terraform on ¬ just serendipity 🍀</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <managingEditor>serendipity@perrotta.dev (Thiago Perrotta)</managingEditor>
    <webMaster>serendipity@perrotta.dev (Thiago Perrotta)</webMaster>
    <copyright>© 2013 - 2026 Thiago Perrotta ·
  a fork of [hugo ʕ•ᴥ•ʔ bear](https://github.com/janraasch/hugo-bearblog/)
</copyright>
    <lastBuildDate>Mon, 08 Jun 2026 01:42:33 +0200</lastBuildDate>
    <atom:link href="https://perrotta.dev/tags/terraform/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>terraform police
      </title>
      <link>https://perrotta.dev/2026/06/terraform-police/</link>
      <pubDate>Mon, 08 Jun 2026 01:21:44 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>serenity</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2026/06/terraform-police/</guid>
      <description>&lt;p&gt;♠ The Master wakes to a pager. Coffee is ready.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;Master, who created the drift?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;Which drift?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;The bucket tag — live state ≠ terraform config in git.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;Show me the chain.&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;They follow: pipeline → cronjob → github actions container → terraform CI bot → github pull request → human.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;We found them. Shall we &lt;del&gt;retaliate&lt;/del&gt; punish?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;What would punishing do to the state?&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;Teach them not to drift.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;Teach the pipeline to ask &amp;lsquo;why&amp;rsquo; first.&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;And what if entropy persists?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;Record why. The record is a lamp in the fog.&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;Who made the first drift in all systems?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;Entropy.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apprentice&lt;/strong&gt;: &amp;ldquo;Then we&amp;rsquo;ll blame entropy?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Master&lt;/strong&gt;: &amp;ldquo;Blame nothing. Write the JIRA ticket.&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;The apprentice writes the ticket. The pipeline asks &amp;ldquo;why&amp;rdquo; and then sleeps. The&#xA;pager blinks once. Dawn sets. Just another ordinary day goes by.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;What does the &amp;ldquo;Terraform police&amp;rdquo; do, you asked?&lt;/p&gt;&#xA;&lt;p&gt;It pursues active drifts in the terraform repository.&lt;/p&gt;&#xA;&lt;p&gt;It chases the individual(s?) responsible for deepening chaos.&lt;/p&gt;&#xA;&lt;p&gt;It teaches them how not to do so anymore.&lt;/p&gt;&#xA;&lt;p&gt;Rinse and repeat.&lt;/p&gt;&#xA;&lt;p&gt;Will the workload of the &amp;ldquo;Terraform police&amp;rdquo; ever end?&lt;/p&gt;&#xA;&lt;p&gt;Is drift an ephemeral state?&lt;/p&gt;&#xA;&lt;p&gt;Is this world a simulation?&lt;/p&gt;&#xA;&lt;p&gt;Is this a world simulation?&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: terraform police&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>just apply it
      </title>
      <link>https://perrotta.dev/2026/02/just-apply-it/</link>
      <pubDate>Wed, 04 Feb 2026 11:08:58 +0100</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>serenity</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2026/02/just-apply-it/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Thiago&lt;/strong&gt;: now that your PR is merged, you can &lt;em&gt;just apply&lt;/em&gt; it&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Teammate&lt;/strong&gt;: ah never did that, what would I need to do?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Thiago&lt;/strong&gt;: &lt;code&gt;just apply github-{corp}-ai-resources&lt;/code&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/02/just-apply-it/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I love these accidental word puns (ft. &lt;a href=&#34;https://just.systems/&#34;&gt;just&lt;/a&gt;).&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;In this context, &lt;code&gt;just apply&lt;/code&gt; is a wrapper of &lt;code&gt;terraform apply&lt;/code&gt;.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/02/just-apply-it/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: just apply it&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>terraform import arrays
      </title>
      <link>https://perrotta.dev/2026/01/terraform-import-arrays/</link>
      <pubDate>Sat, 24 Jan 2026 17:00:07 +0100</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2026/01/terraform-import-arrays/</guid>
      <description>&lt;p&gt;♠ Given an existing resource&#xA;&lt;code&gt;module.atlas_mongo_external.mongodbatlas_alert_configuration.maintenance_no_longer_needed[0]&lt;/code&gt;&#xA;to be imported into terraform state through an&#xA;&lt;a href=&#34;https://developer.hashicorp.com/terraform/language/v1.14.x/import/bulk?page=import&amp;amp;page=bulk&#34;&gt;&lt;code&gt;imports.tf&lt;/code&gt;&lt;/a&gt;&#xA;file with &lt;a href=&#34;https://developer.hashicorp.com/terraform/language/import&#34;&gt;&lt;code&gt;import&lt;/code&gt;&lt;/a&gt;&#xA;blocks, a targeted imported can be performed like this:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;terraform apply -target=&amp;#39;module.atlas_mongo_external.mongodbatlas_alert_configuration.maintenance_no_longer_needed[0]&amp;#39;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Nothing special here.&#xA;The &amp;ldquo;TIL&amp;rdquo; bit of the day is that the array suffix &lt;code&gt;[0]&lt;/code&gt; is not needed:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;terraform apply -target=&amp;#39;module.atlas_mongo_external.mongodbatlas_alert_configuration.maintenance_no_longer_needed&amp;#39;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&amp;hellip;works as well.&lt;/p&gt;&#xA;&lt;p&gt;It feels like a silly optimization to know about but, when you double-click the&#xA;resource name in most terminal emulators, only&#xA;&lt;code&gt;module.atlas_mongo_external.mongodbatlas_alert_configuration.maintenance_no_longer_needed&lt;/code&gt;&#xA;gets selected by default, stopping right before &lt;code&gt;[&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: terraform import arrays&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Terraformer
      </title>
      <link>https://perrotta.dev/2025/11/terraformer/</link>
      <pubDate>Wed, 19 Nov 2025 10:22:18 -0300</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>aws</category>
      <category>dev</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2025/11/terraformer/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://github.com/GoogleCloudPlatform/terraformer&#34;&gt;terraformer&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;CLI tool to generate terraform files from existing infrastructure (reverse&#xA;Terraform). Infrastructure to Code&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;In other words: &amp;ldquo;reverse terraform&amp;rdquo;.&lt;/p&gt;&#xA;&lt;p&gt;I was quite excited to add this project to my tool belt, as it can save a lot of&#xA;time with scaffolding and &lt;code&gt;terraform import&lt;/code&gt; commands.&lt;/p&gt;&#xA;&lt;p&gt;Then &lt;a href=&#34;https://www.claude.com/product/claude-code&#34;&gt;Claude Code&lt;/a&gt;, once again,&#xA;surprised me.&lt;/p&gt;&#xA;&lt;p&gt;It turns out there&amp;rsquo;s absolutely no need to adopt &lt;code&gt;terraformer&lt;/code&gt; if you have a&#xA;superb agent available and a few cents/dollars to spare.&lt;/p&gt;&#xA;&lt;p&gt;Claude can figure out which CLI arguments to pass to &lt;code&gt;aws&lt;/code&gt;, &lt;code&gt;az&lt;/code&gt;, etc. to list&#xA;all relevant cloud resources. Then it can generate / scaffold a basic set of&#xA;Terraform files to manage them&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2025/11/terraformer/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. And then it can craft an one-off shell or&#xA;python script to import them all in terraform.&lt;/p&gt;&#xA;&lt;p&gt;Easy. Quick. Painless.&lt;/p&gt;&#xA;&lt;p&gt;The interesting part is that I had initially asked Claude to explicitly use&#xA;&lt;em&gt;terraformer&lt;/em&gt; to do so, but it turned out to be more complicated / less&#xA;efficient than having the agent perform the task &lt;em&gt;directly&lt;/em&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I am sorry if you were expecting a &lt;code&gt;terraformer&lt;/code&gt; tutorial.&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;You&amp;rsquo;ll need to refactor these later for maintainability.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2025/11/terraformer/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: Terraformer&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/aws/&#34;&gt;#aws&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>terraform: update outputs only
      </title>
      <link>https://perrotta.dev/2025/11/terraform-update-outputs-only/</link>
      <pubDate>Mon, 17 Nov 2025 12:04:26 -0300</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2025/11/terraform-update-outputs-only/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: Given a Terraform project full of pending changes&#xA;(&lt;code&gt;terraform plan&lt;/code&gt;), update its&#xA;&lt;a href=&#34;https://developer.hashicorp.com/terraform/cli/commands/output&#34;&gt;outputs&lt;/a&gt; only.&lt;/p&gt;&#xA;&lt;p&gt;I would expect to be able to use &lt;code&gt;-target&lt;/code&gt; to do so, but that&amp;rsquo;s not possible.&#xA;This flag is intended for resources only.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;a href=&#34;https://devops.stackexchange.com/questions/14286/terraform-apply-output-only&#34;&gt;correct&#xA;approach&lt;/a&gt;&#xA;is &lt;code&gt;terraform apply -refresh-only&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Running terraform apply -refresh-only should take care of any new outputs. It&#xA;will read the latest data from each resource and then update all of the&#xA;outputs in terms of those updates, which includes re-evaluating your output&#xA;expressions to incorporate any changes.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I tested this and it indeed works as expected.&lt;/p&gt;&#xA;&lt;p&gt;The next action from here would be to run &lt;code&gt;terraform apply&lt;/code&gt; for select &lt;code&gt;-target&lt;/code&gt;&#xA;resources, a few at a time, until drift is completely eliminated.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: terraform: update outputs only&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>terraform: bypass lock
      </title>
      <link>https://perrotta.dev/2025/08/terraform-bypass-lock/</link>
      <pubDate>Mon, 04 Aug 2025 11:49:16 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2025/08/terraform-bypass-lock/</guid>
      <description>&lt;p&gt;♠ Terraform supports &lt;a href=&#34;https://developer.hashicorp.com/terraform/language/state/locking&#34;&gt;state&#xA;locking&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;If supported by your backend, Terraform will lock your state for all&#xA;operations that could write state. This prevents others from acquiring the&#xA;lock and potentially corrupting your state.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Neat, right?&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% op run --env-file=.env -- terraform plan&#xA;╷&#xA;│ Error: Error acquiring the state lock&#xA;│&#xA;│ Error message: operation error S3: PutObject, https response error&#xA;│ StatusCode: 412, RequestID: {redacted}, HostID:&#xA;│ {redacted},&#xA;│ api error PreconditionFailed: At least one of the pre-conditions you&#xA;│ specified did not hold&#xA;│ Lock Info:&#xA;│   ID:        {redacted}&#xA;│   Path:      {redacted}/chartmuseum.tfstate&#xA;│   Operation: OperationTypePlan&#xA;│   Who:       {redacted}&#xA;│   Version:   1.10.5&#xA;│   Created:   2025-08-04 09:46:55.995791 &amp;#43;0000 UTC&#xA;│   Info:&#xA;│&#xA;│&#xA;│ Terraform acquires a state lock to protect the state from being written&#xA;│ by multiple users at the same time. Please resolve the issue above and try&#xA;│ again. For most commands, you can disable locking with the &amp;#34;-lock=false&amp;#34;&#xA;│ flag, but this is not recommended.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It even uses a custom &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/412&#34;&gt;HTTP&#xA;Status&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;The HTTP 412 Precondition Failed&lt;/strong&gt; &lt;em&gt;client error response status code&lt;/em&gt;&#xA;indicates that access to the target resource was denied. This happens with&#xA;conditional requests on methods other than GET or HEAD when the condition&#xA;defined by the If-Unmodified-Since or If-Match headers is not fulfilled. In&#xA;that case, the request (usually an upload or a modification of a resource)&#xA;cannot be made and this error response is sent back.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;If you&amp;rsquo;re sure no one else is working on that workspace, you can proceed with&#xA;&lt;code&gt;-lock=false&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% op run --env-file=.env -- terraform plan -lock=false&#xA;uptime_check_http.chartmuseum: Refreshing state... [name=chartmuseum]&#xA;data.aws_availability_zones.available: Reading...&#xA;data.aws_availability_zones.available: Read complete after 1s [id=us-east-1]&#xA;[...]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That&amp;rsquo;s an one-off. A subsequent run without that argument will fail again:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% op run --env-file=.env -- terraform plan&#xA;╷&#xA;│ Error: Error acquiring the state lock&#xA;[...]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;In this case it would be better to simply remove the lock with&#xA;&lt;a href=&#34;https://developer.hashicorp.com/terraform/language/state/locking#force-unlock&#34;&gt;&lt;code&gt;force-unlock&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% op run --env-file=.env -- terraform force-unlock {lock-id}&#xA;Do you really want to force-unlock?&#xA;  Terraform will remove the lock on the remote state.&#xA;  This will allow local Terraform commands to modify this state, even though it&#xA;  may still be in use. Only &amp;#39;yes&amp;#39; will be accepted to confirm.&#xA;&#xA;  Enter a value: yes&#xA;&#xA;Terraform state has been successfully unlocked!&#xA;&#xA;The state has been unlocked, and Terraform commands should now be able to&#xA;obtain a new lock on the remote state.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The &lt;code&gt;lock-id&lt;/code&gt; can be obtained from the &lt;code&gt;plan&lt;/code&gt; output.&lt;/p&gt;&#xA;&lt;p&gt;Now we can &lt;code&gt;plan&lt;/code&gt; as usual:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% op run --env-file=.env -- terraform plan&#xA;uptime_check_http.chartmuseum: Refreshing state... [name=chartmuseum]&#xA;data.aws_availability_zones.available: Reading...&#xA;data.aws_availability_zones.available: Read complete after 0s [id=us-east-1]&#xA;[...]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: terraform: bypass lock&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Terraform: rename resources
      </title>
      <link>https://perrotta.dev/2025/03/terraform-rename-resources/</link>
      <pubDate>Wed, 19 Mar 2025 11:40:43 +0100</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>aws</category>
      <category>dev</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2025/03/terraform-rename-resources/</guid>
      <description>&lt;p&gt;♠ Sometimes I need to make a simple refactoring rename change to a terraform&#xA;resource that has already been applied. For example:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;resource &amp;#34;aws_api_gateway_request_validator&amp;#34; &amp;#34;delete&amp;#34; {&#xA;  name                        = &amp;#34;Validate body, query string parameters, and headers&amp;#34;&#xA;  rest_api_id                 = aws_api_gateway_rest_api.this.id&#xA;  validate_request_body       = true&#xA;  validate_request_parameters = true&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;to&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;resource &amp;#34;aws_api_gateway_request_validator&amp;#34; &amp;#34;this&amp;#34; {&#xA;  name                        = &amp;#34;Validate body, query string parameters, and headers&amp;#34;&#xA;  rest_api_id                 = aws_api_gateway_rest_api.this.id&#xA;  validate_request_body       = true&#xA;  validate_request_parameters = true&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The simplest way to do so is to &lt;code&gt;terraform apply&lt;/code&gt;. However, that incurs&#xA;destruction + re-creation of the resource, which would incur downtime in case&#xA;it&amp;rsquo;s already deployed in prod.&lt;/p&gt;&#xA;&lt;p&gt;The best way is &lt;code&gt;% terraform state mv aws_api_gateway_request_validator.{delete,this}&lt;/code&gt;. In-place move, simple and&#xA;easy.&lt;/p&gt;&#xA;&lt;p&gt;When doing it at scale, running multiple &lt;code&gt;terraform state mv&lt;/code&gt; commands could be&#xA;cumbersome.&lt;/p&gt;&#xA;&lt;p&gt;There&amp;rsquo;s a second way, using the&#xA;&lt;a href=&#34;https://developer.hashicorp.com/terraform/language/moved&#34;&gt;&lt;code&gt;moved&lt;/code&gt;&lt;/a&gt; block in HCL:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;moved {&#xA;    from = aws_api_gateway_request_validator.delete&#xA;    to = aws_api_gateway_request_validator.this&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;code&gt;terraform&lt;/code&gt; docs call this operation &amp;ldquo;move&amp;rdquo;, instead of &amp;ldquo;rename&amp;rdquo;.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: Terraform: rename resources&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/aws/&#34;&gt;#aws&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Terraform: create zip files
      </title>
      <link>https://perrotta.dev/2025/03/terraform-create-zip-files/</link>
      <pubDate>Mon, 17 Mar 2025 21:06:28 +0100</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2025/03/terraform-create-zip-files/</guid>
      <description>&lt;p&gt;♠ When creating &lt;code&gt;.zip&lt;/code&gt; files with terraform, for example, with the purpose of&#xA;uploading them to an &lt;a href=&#34;https://aws.amazon.com/lambda/&#34;&gt;AWS Lambda&lt;/a&gt;, at least two&#xA;approaches exist to do so.&lt;/p&gt;&#xA;&lt;p&gt;In the examples below, assume a NodeJS source file named &lt;code&gt;index.mjs&lt;/code&gt;. It should&#xA;be packaged into a &lt;code&gt;lambda.zip&lt;/code&gt; archive.&lt;/p&gt;&#xA;&lt;h2 id=&#34;1-use-null_resource&#34;&gt;&#xA;  1) Use &lt;a href=&#34;https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource&#34;&gt;&lt;code&gt;null_resource&lt;/code&gt;&lt;/a&gt;&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2025/03/terraform-create-zip-files/#1-use-null_resource&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;resource &amp;#34;null_resource&amp;#34; &amp;#34;build_lambda&amp;#34; {&#xA;  triggers = {&#xA;    source_code_hash = filebase64sha256(&amp;#34;${path.module}/lambda/index.mjs&amp;#34;)&#xA;  }&#xA;&#xA;  provisioner &amp;#34;local-exec&amp;#34; {&#xA;    command = &amp;lt;&amp;lt;EOF&#xA;            cd ${path.module}/lambda&#xA;            zip -r lambda.zip index.mjs&#xA;        EOF&#xA;  }&#xA;}&#xA;&#xA;resource &amp;#34;aws_lambda_function&amp;#34; &amp;#34;this&amp;#34; {&#xA;  function_name = &amp;#34;dns-changer-delete&amp;#34;&#xA;  role          = aws_iam_role.this.arn&#xA;&#xA;  filename         = &amp;#34;${path.module}/lambda/lambda.zip&amp;#34;&#xA;  source_code_hash = filebase64sha256(&amp;#34;${path.module}/lambda/lambda.zip&amp;#34;)&#xA;  handler          = &amp;#34;index.handler&amp;#34;&#xA;  runtime          = &amp;#34;nodejs22.x&amp;#34;&#xA;&#xA;  depends_on = [null_resource.build_lambda]&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;2-use-archive_file&#34;&gt;&#xA;  2) Use &lt;a href=&#34;https://registry.terraform.io/providers/hashicorp/archive/latest/docs/resources/file&#34;&gt;&lt;code&gt;archive_file&lt;/code&gt;&lt;/a&gt;&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2025/03/terraform-create-zip-files/#2-use-archive_file&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;data &amp;#34;archive_file&amp;#34; &amp;#34;lambda_zip&amp;#34; {&#xA;  type        = &amp;#34;zip&amp;#34;&#xA;  source_file = &amp;#34;${path.module}/lambda/index.mjs&amp;#34;&#xA;  output_path = &amp;#34;${path.module}/lambda/lambda.zip&amp;#34;&#xA;}&#xA;&#xA;resource &amp;#34;aws_lambda_function&amp;#34; &amp;#34;this&amp;#34; {&#xA;  function_name = &amp;#34;dns-changer-delete&amp;#34;&#xA;  description   = &amp;#34;Delete DNS record sets for DNS Changer&amp;#34;&#xA;  role          = aws_iam_role.this.arn&#xA;&#xA;  filename         = data.archive_file.lambda_zip.output_path&#xA;  source_code_hash = data.archive_file.lambda_zip.output_base64sha256&#xA;  handler          = &amp;#34;index.handler&amp;#34;&#xA;  runtime          = &amp;#34;nodejs22.x&amp;#34;&#xA;&#xA;  depends_on = [data.archive_file.lambda_zip]&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I prefer the second approach.&lt;/p&gt;&#xA;&lt;p&gt;Furthermore, it only requires one &lt;code&gt;terraform apply&lt;/code&gt; per file update, whereas&#xA;the first one requires two.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: Terraform: create zip files&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Terraform: generate diagrams
      </title>
      <link>https://perrotta.dev/2025/03/terraform-generate-diagrams/</link>
      <pubDate>Mon, 17 Mar 2025 13:42:57 +0100</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2025/03/terraform-generate-diagrams/</guid>
      <description>&lt;p&gt;♠ There are a few options to visualize a &lt;code&gt;terraform&lt;/code&gt; module in the form of a graph&#xA;or diagram.&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraform-graph-dot--graphviz&#34;&gt;&#xA;  &lt;code&gt;terraform graph&lt;/code&gt;: dot / graphviz&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2025/03/terraform-generate-diagrams/#terraform-graph-dot--graphviz&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://developer.hashicorp.com/terraform/cli/commands/graph&#34;&gt;&lt;code&gt;terraform graph&lt;/code&gt;&lt;/a&gt; outputs a&#xA;&lt;a href=&#34;https://graphviz.org/doc/info/lang.html&#34;&gt;DOT&lt;/a&gt; graph, which can be manipulated /&#xA;visualized with &lt;code&gt;graphviz&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;terraform graph | dot -Tpng &amp;gt; graph.png&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The graph can then be embedded into a markdown file:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;![graph](graph.png)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;terramaid-mermaid&#34;&gt;&#xA;  &lt;code&gt;terramaid&lt;/code&gt;: mermaid&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2025/03/terraform-generate-diagrams/#terramaid-mermaid&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;The &lt;a href=&#34;https://github.com/RoseSecurity/Terramaid&#34;&gt;&lt;code&gt;terramaid&lt;/code&gt;&lt;/a&gt; project generates&#xA;&lt;a href=&#34;https://mermaid.js.org/&#34;&gt;Mermaid&lt;/a&gt; diagrams from Terraform configurations:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;% brew install terramaid&#xA;% terramaid run --output README.md&#xA;Mermaid diagram successfully written to README.md&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The default filename is &lt;code&gt;Terramaid.md&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Why use Mermaid? It was previously covered &lt;a href=&#34;https://perrotta.dev/2024/10/mermaid-rich-diagrams-in-markdown/&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Mermaid has native integration with&#xA;&lt;a href=&#34;https://github.blog/developer-skills/github/include-diagrams-markdown-files-mermaid/&#34;&gt;GitHub&lt;/a&gt;&#xA;and &lt;a href=&#34;https://docs.gitlab.com/ee/user/markdown.html#mermaid&#34;&gt;GitLab&lt;/a&gt;, and that&amp;rsquo;s&#xA;hard to beat. Graphviz, sadly, &lt;a href=&#34;https://forum.graphviz.org/t/github-adding-support-for-mermaid-diagrams/998&#34;&gt;does&#xA;not&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: Terraform: generate diagrams&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Terraform: AWS deployment to random availability zones
      </title>
      <link>https://perrotta.dev/2024/05/terraform-aws-deployment-to-random-availability-zones/</link>
      <pubDate>Tue, 21 May 2024 14:31:03 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>aws</category>
      <category>dev</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2024/05/terraform-aws-deployment-to-random-availability-zones/</guid>
      <description>&lt;p&gt;♠ A common scenario: there&amp;rsquo;s a new deployment you would like to roll out to AWS.&#xA;Let&amp;rsquo;s say you pick &amp;ldquo;us-east-1&amp;rdquo; as your cloud region. There are multiple&#xA;availability zones within it:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;us-east-1a&lt;/li&gt;&#xA;&lt;li&gt;us-east-1b&lt;/li&gt;&#xA;&lt;li&gt;us-east-1c&lt;/li&gt;&#xA;&lt;li&gt;us-east-1d&lt;/li&gt;&#xA;&lt;li&gt;us-east-1e&lt;/li&gt;&#xA;&lt;li&gt;us-east-1f&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Suppose you want to pick two of them for your service/app, and you don&amp;rsquo;t&#xA;particularly care about which one. How to proceed?&lt;/p&gt;&#xA;&lt;h2 id=&#34;option-1-hard-coding&#34;&gt;&#xA;  Option #1: Hard-coding&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2024/05/terraform-aws-deployment-to-random-availability-zones/#option-1-hard-coding&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Pick two arbitrary zones and hard-code them.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;variable &amp;#34;availability_zones&amp;#34; {&#xA;  type    = list(string)&#xA;  default = [&amp;#34;us-east-1a&amp;#34;, &amp;#34;us-east-1b&amp;#34;]&#xA;}&#xA;&#xA;resource &amp;#34;aws_subnet&amp;#34; &amp;#34;private&amp;#34; {&#xA;  vpc_id            = aws_vpc.chartmuseum.id&#xA;  cidr_block        = element(var.private_subnets, count.index)&#xA;  availability_zone = element(var.availability_zones, count.index)&#xA;  count             = length(var.private_subnets)&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;Caveat&lt;/strong&gt;: &lt;a href=&#34;https://www.goodreads.com/book/show/10639.The_Paradox_of_Choice&#34;&gt;The paradox of&#xA;choice&lt;/a&gt;,&#xA;unnecessary decision fatigue.&lt;/p&gt;&#xA;&lt;h2 id=&#34;option-2-pick-the-first-two&#34;&gt;&#xA;  Option #2: Pick the first two&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2024/05/terraform-aws-deployment-to-random-availability-zones/#option-2-pick-the-first-two&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Use the AWS data source to dynamically find all zones, and pick the first two.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;data &amp;#34;aws_availability_zones&amp;#34; &amp;#34;available&amp;#34; {&#xA;  state = &amp;#34;available&amp;#34;&#xA;}&#xA;&#xA;resource &amp;#34;aws_subnet&amp;#34; &amp;#34;private&amp;#34; {&#xA;  vpc_id            = aws_vpc.chartmuseum.id&#xA;  cidr_block        = element(var.private_subnets, count.index)&#xA;  availability_zone = element(data.aws_availability_zones.available.names, count.index)&#xA;  count             = length(var.private_subnets)&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Note that &lt;code&gt;terraform plan&lt;/code&gt; should display the full zone list.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Caveat&lt;/strong&gt;: Heavily biased towards the first two zones.&lt;/p&gt;&#xA;&lt;h2 id=&#34;option-3-random-shuffling&#34;&gt;&#xA;  Option #3: Random shuffling&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2024/05/terraform-aws-deployment-to-random-availability-zones/#option-3-random-shuffling&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Pick two zones at random!&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;data &amp;#34;aws_availability_zones&amp;#34; &amp;#34;available&amp;#34; {&#xA;  state = &amp;#34;available&amp;#34;&#xA;}&#xA;&#xA;resource &amp;#34;random_shuffle&amp;#34; &amp;#34;aws_availability_zone_names&amp;#34; {&#xA;  input        = data.aws_availability_zones.available.names&#xA;  result_count = 2&#xA;}&#xA;&#xA;resource &amp;#34;aws_subnet&amp;#34; &amp;#34;private&amp;#34; {&#xA;  vpc_id            = aws_vpc.chartmuseum.id&#xA;  cidr_block        = element(var.private_subnets, count.index)&#xA;  availability_zone = element(random_shuffle.aws_availability_zone_names.result, count.index)&#xA;  count             = length(var.private_subnets)&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;Winner&lt;/strong&gt;: In my opinion, this is the most elegant approach.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;random_shuffle&lt;/code&gt; will output the selected regions upon running &lt;code&gt;terraform apply&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: Terraform: AWS deployment to random availability zones&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/aws/&#34;&gt;#aws&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ Integrating terraform with ansible
      </title>
      <link>https://perrotta.dev/2024/02/integrating-terraform-with-ansible/</link>
      <pubDate>Thu, 01 Feb 2024 14:02:28 -0300</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bestof</category>
      <category>dev</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2024/02/integrating-terraform-with-ansible/</guid>
      <description>&lt;p&gt;♠ This post is a follow-up of &lt;a href=&#34;https://perrotta.dev/2024/01/terraforming-a-linode-hello-world/&#34;&gt;Terraforming a Linode: hello world&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;In a future post, we will continue from here by using Ansible to install and&#xA;set up Miniflux in our new Linode.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Before we extensively use Ansible to configure our VPS instance, first let&amp;rsquo;s&#xA;set up a basic integration between Terraform and Ansible.&lt;/p&gt;&#xA;&lt;p&gt;First of all, here&amp;rsquo;s an overview of where I stopped last time. There were a&#xA;couple of lightweight modifications since then. I&amp;rsquo;ll explain some of them&#xA;below.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;variable &amp;#34;github_username&amp;#34; {&#xA;  type    = string&#xA;  default = &amp;#34;thiagowfx&amp;#34;&#xA;}&#xA;&#xA;variable &amp;#34;linode_hostname&amp;#34; {&#xA;  type    = string&#xA;  default = &amp;#34;coruscant&amp;#34;&#xA;}&#xA;&#xA;variable &amp;#34;linode_region&amp;#34; {&#xA;  type    = string&#xA;  default = &amp;#34;eu-central&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;All variables were moved to a &lt;code&gt;variables.tf&lt;/code&gt; file. This is to follow standard&#xA;terraform&#xA;&lt;a href=&#34;https://developer.hashicorp.com/terraform/language/modules/develop/structure&#34;&gt;conventions&lt;/a&gt;&#xA;/ recommendations for module structures. Furthermore, it becomes easier to&#xA;manage variables when they are all stored in a single place.&lt;/p&gt;&#xA;&lt;p&gt;The main module file now looks like this:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;terraform {&#xA;  required_providers {&#xA;    http = {&#xA;      source = &amp;#34;hashicorp/http&amp;#34;&#xA;    }&#xA;    linode = {&#xA;      source = &amp;#34;linode/linode&amp;#34;&#xA;    }&#xA;  }&#xA;}&#xA;&#xA;provider &amp;#34;linode&amp;#34; {}&#xA;&#xA;data &amp;#34;http&amp;#34; &amp;#34;github_keys&amp;#34; {&#xA;  url = &amp;#34;https://api.github.com/users/${var.github_username}/keys&amp;#34;&#xA;}&#xA;&#xA;locals {&#xA;  keys = jsondecode(data.http.github_keys.response_body)[*].key&#xA;}&#xA;&#xA;resource &amp;#34;linode_instance&amp;#34; &amp;#34;nanode&amp;#34; {&#xA;  type             = &amp;#34;g6-nanode-1&amp;#34;&#xA;  image            = &amp;#34;linode/alpine3.19&amp;#34;&#xA;  label            = var.linode_hostname&#xA;  region           = var.linode_region&#xA;  authorized_keys  = local.keys&#xA;  backups_enabled  = &amp;#34;false&amp;#34;&#xA;  booted           = &amp;#34;true&amp;#34;&#xA;  watchdog_enabled = &amp;#34;true&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I removed the token from the linode provider. Now it is supplied via the&#xA;&lt;code&gt;LINODE_TOKEN&lt;/code&gt; environment variable. In order to automatically populate that&#xA;variable, I use &lt;a href=&#34;https://perrotta.dev/2022/01/direnv-automate-your-environment-variables/&#34;&gt;&lt;code&gt;direnv&lt;/code&gt;&lt;/a&gt;. There&amp;rsquo;s an &lt;code&gt;.envrc&lt;/code&gt; file that provides its value, like so:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh&#xA;# terraform init&#xA;&#xA;export LINODE_TOKEN=&amp;#34;my-token-here&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I also created a repository for this project:&#xA;&lt;a href=&#34;https://github.com/thiagowfx/knol&#34;&gt;https://github.com/thiagowfx/knol&lt;/a&gt;. That&amp;rsquo;s enough for preliminaries, now let&amp;rsquo;s&#xA;go back to Ansible.&lt;/p&gt;&#xA;&lt;p&gt;The first component we&amp;rsquo;ll need is an Ansible&#xA;&lt;a href=&#34;https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html&#34;&gt;inventory&lt;/a&gt;&#xA;file, containing the IP address of the host we&amp;rsquo;ll manage. It could look like&#xA;this:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;[all]&#xA;1.2.3.4 ansible_user=root&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&amp;hellip;wherein &lt;code&gt;1.2.3.4&lt;/code&gt; is the IP address of our VPS.&lt;/p&gt;&#xA;&lt;p&gt;That said, due to the fact the VPS instance is created dynamically, maintaining&#xA;that IP address manually would be tedious. Therefore, let&amp;rsquo;s have Terraform&#xA;manage it.&lt;/p&gt;&#xA;&lt;p&gt;We can do so with a&#xA;&lt;a href=&#34;https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file&#34;&gt;&lt;code&gt;local_file&lt;/code&gt;&lt;/a&gt;.&#xA;Heck, we could even use a&#xA;&lt;a href=&#34;https://registry.terraform.io/providers/hashicorp/template/latest/docs/data-sources/file&#34;&gt;&lt;code&gt;template_file&lt;/code&gt;&lt;/a&gt;,&#xA;however it would be overkill as there are only two simple lines in our&#xA;inventory at this point. A &lt;code&gt;local_file&lt;/code&gt; is created upon &lt;code&gt;terraform apply&lt;/code&gt; and&#xA;deleted upon &lt;code&gt;terraform destroy&lt;/code&gt;. Therefore it doesn&amp;rsquo;t even need to be tracked&#xA;by our VCS:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;resource &amp;#34;local_file&amp;#34; &amp;#34;ansible_inventory&amp;#34; {&#xA;  content  = &amp;lt;&amp;lt;-EOF&#xA;[all]&#xA;${linode_instance.nanode.ip_address} ansible_user=root&#xA;EOF&#xA;  filename = &amp;#34;inventory.ini&amp;#34;&#xA;  file_permission = &amp;#34;0644&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Once we run terraform (plan + apply), an &lt;code&gt;inventory.ini&lt;/code&gt; file should be created&#xA;with the above contents.&lt;/p&gt;&#xA;&lt;p&gt;Because the IP address is ephemeral and dynamic, we should have a&#xA;straightforward way to see its value. A terraform&#xA;&lt;a href=&#34;https://developer.hashicorp.com/terraform/language/values/outputs&#34;&gt;&lt;code&gt;output&lt;/code&gt;&lt;/a&gt;&#xA;is perfect for that:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;output &amp;#34;ip_address&amp;#34; {&#xA;  value = linode_instance.nanode.ip_address&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Later on (after terraforming) we will be able to use &lt;code&gt;terraform output&lt;/code&gt; to see&#xA;the server IP address.&lt;/p&gt;&#xA;&lt;p&gt;We have the inventory file. Now we need a&#xA;&lt;a href=&#34;https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html&#34;&gt;playbook&lt;/a&gt;.&#xA;A playbook contains a sequence of tasks to be applied to our server.&lt;/p&gt;&#xA;&lt;p&gt;Let&amp;rsquo;s start with a basic playbook that just installs and starts &lt;code&gt;nginx&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---&#xA;- hosts: all&#xA;  tasks:&#xA;    - name: Install the web server (nginx)&#xA;      community.general.apk:&#xA;        name: nginx&#xA;        state: present&#xA;    - name: Start the web server&#xA;      service:&#xA;        name: nginx&#xA;        state: started&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Save this to a &lt;code&gt;playbook.yml&lt;/code&gt; file.&lt;/p&gt;&#xA;&lt;p&gt;After terraforming, we should now be able to run ansible:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% ansible-playbook -i inventory.ini playbook.yml&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;In order to make this setup more ergonomic, let&amp;rsquo;s create a &lt;code&gt;Makefile&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-makefile&#34;&gt;TERRAFORM := terraform&#xA;&#xA;all: terraform ansible&#xA;&#xA;ansible:&#xA;&#x9;ansible-playbook -i inventory.ini playbook.yml&#xA;&#xA;terraform:&#xA;&#x9;$(TERRAFORM) init&#xA;&#x9;$(TERRAFORM) plan&#xA;&#x9;$(TERRAFORM) apply&#xA;&#xA;clean:&#xA;&#x9;$(TERRAFORM) destroy&#xA;&#xA;.PHONY: all ansible terraform clean&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then we can just run &lt;code&gt;make terraform&lt;/code&gt; or &lt;code&gt;make ansible&lt;/code&gt; for granular steps. Or&#xA;just &lt;code&gt;make&lt;/code&gt; to run everything in the right order.&lt;/p&gt;&#xA;&lt;p&gt;I extracted the &lt;code&gt;terraform&lt;/code&gt; binary to its own variable because it facilitates&#xA;the use of &lt;a href=&#34;https://opentofu.org/&#34;&gt;OpenTofu&lt;/a&gt; (a fork) in lieu of terraform.&lt;/p&gt;&#xA;&lt;p&gt;And that&amp;rsquo;s it for today! In a future post, we&amp;rsquo;ll look into extending our&#xA;Ansible usage to fully bootstrap Miniflux on the server.&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: Integrating terraform with ansible&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bestof/&#34;&gt;#bestof&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ Terraforming a Linode: hello world
      </title>
      <link>https://perrotta.dev/2024/01/terraforming-a-linode-hello-world/</link>
      <pubDate>Tue, 23 Jan 2024 23:27:04 -0300</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>alpine-linux</category>
      <category>bestof</category>
      <category>dev</category>
      <category>terraform</category>
      <guid>https://perrotta.dev/2024/01/terraforming-a-linode-hello-world/</guid>
      <description>&lt;p&gt;♠ I host my own &lt;a href=&#34;https://miniflux.app/&#34;&gt;Miniflux&lt;/a&gt; instance, which happens to be&#xA;my favorite RSS reader. Currently it is hosted on Linode (Akamai Cloud)&#xA;running &lt;a href=&#34;https://www.alpinelinux.org/&#34;&gt;Alpine Linux&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;My current setup was performed manually. I was thinking that, for fun, it would&#xA;be cool to fully automate it under the principles of&#xA;&lt;a href=&#34;https://en.wikipedia.org/wiki/Infrastructure_as_code&#34;&gt;IaC&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The current setup does not use any containers. I had proudly made it as KISS as&#xA;possible at the time:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Linode is a very beginner-friendly (and cheap) VPS&lt;/li&gt;&#xA;&lt;li&gt;Alpine Linux is a first-class citizen on Linode&lt;/li&gt;&#xA;&lt;li&gt;There&amp;rsquo;s an &lt;code&gt;apk&lt;/code&gt; &lt;a href=&#34;https://pkgs.alpinelinux.org/packages?name=miniflux&#34;&gt;package&lt;/a&gt; for &lt;code&gt;miniflux&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;There&amp;rsquo;s an OpenRC&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2024/01/terraforming-a-linode-hello-world/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; script for &lt;code&gt;miniflux&lt;/code&gt; (so that it can be controlled via &lt;code&gt;service&lt;/code&gt;)&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;For the first part of this automation we will look into provisioning a Linode&#xA;with an Alpine Linux installation. In order to do so we will use HashiCorp&#xA;&lt;a href=&#34;https://www.terraform.io/&#34;&gt;Terraform&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;requirements&#34;&gt;&#xA;  Requirements&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2024/01/terraforming-a-linode-hello-world/#requirements&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Provision a new Linode&lt;/li&gt;&#xA;&lt;li&gt;Deploy it in Europe&lt;/li&gt;&#xA;&lt;li&gt;Use the smallest shape (a so-called &lt;a href=&#34;https://www.linode.com/community/questions/211/what-is-a-nanode&#34;&gt;Nanode&lt;/a&gt;)&lt;/li&gt;&#xA;&lt;li&gt;Run Alpine Linux&lt;/li&gt;&#xA;&lt;li&gt;Set it up with my &lt;a href=&#34;https://github.com/thiagowfx.keys&#34;&gt;public ssh key&lt;/a&gt;, which is hosted on Github&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;terraform-setup&#34;&gt;&#xA;  Terraform setup&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2024/01/terraforming-a-linode-hello-world/#terraform-setup&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Install a provider for Linode: &lt;a href=&#34;https://registry.terraform.io/providers/linode/linode/latest/docs&#34;&gt;https://registry.terraform.io/providers/linode/linode/latest/docs&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Scaffold it like this, in a &lt;code&gt;main.tf&lt;/code&gt; file:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;terraform {&#xA;  required_providers {&#xA;    linode = {&#xA;      source = &amp;#34;linode/linode&amp;#34;&#xA;    }&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then run:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% terraform init&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Generate a Linode API token&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Go to &lt;a href=&#34;https://cloud.linode.com/profile/tokens&#34;&gt;https://cloud.linode.com/profile/tokens&lt;/a&gt;, create a new token called&#xA;&lt;code&gt;terraform&lt;/code&gt;. with the &amp;ldquo;Linodes&amp;rdquo; scope set to &amp;ldquo;Read/Write&amp;rdquo;.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Append this API token to &lt;code&gt;main.tf&lt;/code&gt;:&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;provider &amp;#34;linode&amp;#34; {&#xA;  token = &amp;#34;&amp;lt;your token here&amp;gt;&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Add a&#xA;&lt;a href=&#34;https://registry.terraform.io/providers/linode/linode/latest/docs/resources/instance&#34;&gt;&lt;code&gt;linode_instance&lt;/code&gt;&lt;/a&gt;&#xA;with the appropriate fields set according to the documentation:&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;resource &amp;#34;linode_instance&amp;#34; &amp;#34;coruscant&amp;#34; {&#xA;  label  = &amp;#34;coruscant&amp;#34;&#xA;  image  = &amp;#34;linode/alpine3.19&amp;#34;&#xA;  region = &amp;#34;eu-central&amp;#34;&#xA;  type   = &amp;#34;g6-nanode-1&amp;#34;&#xA;  authorized_keys  = [&amp;#34;&amp;lt;your ssh public key here&amp;gt;&amp;#34;]&#xA;  backups_enabled  = &amp;#34;false&amp;#34;&#xA;  watchdog_enabled = &amp;#34;true&amp;#34;&#xA;  booted           = &amp;#34;true&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then run:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% terraform plan&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&amp;ldquo;Plan&amp;rdquo; is basically a dry-run. Terraform will output what it intends to do, but nothing will be done yet.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Analyze the output and double check that it looks correct.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;To actually perform the provisioning, run:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% terraform apply&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then confirm the prompt.&lt;/p&gt;&#xA;&lt;p&gt;Within a few seconds (or maybe minutes), you should see your new Linode in the&#xA;&lt;a href=&#34;https://cloud.linode.com/&#34;&gt;Linode Console&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;We can test our deployment by ssh&amp;rsquo;ing to our new machine:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% ssh root@&amp;lt;public IP address&amp;gt; -i ~/.ssh/my_ssh_key&#xA;Welcome to Alpine!&#xA;&#xA;The Alpine Wiki contains a large amount of how-to guides and general&#xA;information about administrating Alpine systems.&#xA;See &amp;lt;https://wiki.alpinelinux.org/&amp;gt;.&#xA;&#xA;You can setup the system with the command: setup-alpine&#xA;&#xA;You may change this message by editing /etc/motd.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Let&amp;rsquo;s take a pause to appreciate how lightweight it is:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;localhost:~# df -h&#xA;Filesystem                Size      Used Available Use% Mounted on&#xA;devtmpfs                 10.0M         0     10.0M   0% /dev&#xA;shm                     487.8M         0    487.8M   0% /dev/shm&#xA;/dev/sda                 24.1G    238.1M     22.6G   1% /&#xA;tmpfs                   195.1M    268.0K    194.8M   0% /run&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Only 238 MiB!&lt;/p&gt;&#xA;&lt;p&gt;To deprovision it, run:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% terraform plan -destroy&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;If everything looks correct, run:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% terraform destroy&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: It turns out the &amp;ldquo;Linodes&amp;rdquo; scope was not enough to do the&#xA;deprovisioning. I needed to create a new scope, with more permissions, in order&#xA;to do so.&lt;/p&gt;&#xA;&lt;p&gt;As you can see, terraform makes it very trivial to deprovision systems.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Bonus points&lt;/strong&gt;: run &lt;code&gt;terraform fmt&lt;/code&gt; to format your file. Never go &lt;a href=&#34;https://www.youtube.com/watch?v=-CmadmM5cOk&#34;&gt;out of&#xA;style&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Tip&lt;/strong&gt;: At any point you can run &lt;code&gt;terraform validate&lt;/code&gt; to verify your &lt;code&gt;main.tf&lt;/code&gt;&#xA;file is syntactically correct.&lt;/p&gt;&#xA;&lt;p&gt;Two things could be improved in the previous setup:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;We could use &lt;code&gt;authorized_users&lt;/code&gt; to pass in our linode username. If we add an&#xA;SSH key to our linode account, then that key would be automatically deployed&#xA;to the system, thereby removing the need to specify &lt;code&gt;authorized_keys&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Alternatively, we could fetch our key from an URL endpoint with the use of&#xA;the &lt;code&gt;hashicorp/http&lt;/code&gt; provider, like so:&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;terraform {&#xA;  required_providers {&#xA;    http = {&#xA;      source = &amp;#34;hashicorp/http&amp;#34;&#xA;    }&#xA;  }&#xA;}&#xA;&#xA;data &amp;#34;http&amp;#34; &amp;#34;thiagowfx_ssh_keys&amp;#34; {&#xA;  url = &amp;#34;https://github.com/thiagowfx.keys&amp;#34;&#xA;}&#xA;&#xA;resource &amp;#34;linode_instance&amp;#34; &amp;#34;coruscant&amp;#34; {&#xA;  # ...&#xA;  authorized_keys  = compact([for line in split(&amp;#34;\n&amp;#34;, data.http.thiagowfx_ssh_keys.response_body) : chomp(line)])&#xA;  # ...&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The &amp;ldquo;list comprehension&amp;rdquo; above does line splitting magic to convert them to a&#xA;list of string, and the &lt;code&gt;compact&lt;/code&gt; removes the empty new line at the end.&lt;/p&gt;&#xA;&lt;p&gt;We could improve the example above even further.&lt;/p&gt;&#xA;&lt;p&gt;For starters, let&amp;rsquo;s parameterize out the username to a variable:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;variable &amp;#34;github_username&amp;#34; {&#xA;  type    = string&#xA;  default = &amp;#34;thiagowfx&amp;#34;&#xA;}&#xA;&#xA;data &amp;#34;http&amp;#34; &amp;#34;user_ssh_keys&amp;#34; {&#xA;  url = &amp;#34;https://github.com/${var.github_username}.keys&amp;#34;&#xA;}&#xA;&#xA;resource &amp;#34;linode_instance&amp;#34; &amp;#34;coruscant&amp;#34; {&#xA;  # ...&#xA;  authorized_keys  = compact([for line in split(&amp;#34;\n&amp;#34;, data.http.user_ssh_keys.response_body) : chomp(line)])&#xA;  # ...&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;We could then easily supply another username with &lt;code&gt;-var&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% terraform plan -var github_username=torvalds&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Note that the above example leverages &lt;a href=&#34;https://developer.hashicorp.com/terraform/language/expressions/strings&#34;&gt;string interpolation&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;We could also extract the SSH keys list to its own &amp;ldquo;variable&amp;rdquo; (&lt;a href=&#34;https://developer.hashicorp.com/terraform/language/values/locals&#34;&gt;locals&lt;/a&gt;):&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;locals {&#xA;  ssh_keys = compact([for line in split(&amp;#34;\n&amp;#34;, data.http.user_ssh_keys.response_body) : chomp(line)])&#xA;}&#xA;&#xA;resource &amp;#34;linode_instance&amp;#34; &amp;#34;coruscant&amp;#34; {&#xA;  # ...&#xA;  authorized_keys  = local.ssh_keys&#xA;  # ...&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A more robust (and stable) way to query the key though is through the Github API:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-terraform&#34;&gt;data &amp;#34;http&amp;#34; &amp;#34;github_keys&amp;#34; {&#xA;  url = &amp;#34;https://api.github.com/users/${var.github_username}/keys&amp;#34;&#xA;}&#xA;&#xA;locals {&#xA;  ssh_keys = jsondecode(data.http.github_keys.response_body)[*].key&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Note that a typical response body looks like the following:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;[&#xA;  {&#xA;    &amp;#34;id&amp;#34;: &amp;#34;&amp;lt;id&amp;gt;&amp;#34;,&#xA;    &amp;#34;key&amp;#34;: &amp;#34;&amp;lt;ssh key&amp;gt;&amp;#34;&#xA;  }&#xA;]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;API endpoint documentation:&#xA;&lt;a href=&#34;https://docs.github.com/en/rest/users/keys?apiVersion=2022-11-28#list-public-keys-for-a-user&#34;&gt;https://docs.github.com/en/rest/users/keys?apiVersion=2022-11-28#list-public-keys-for-a-user&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;If we use &lt;code&gt;output&lt;/code&gt; instead of &lt;code&gt;locals&lt;/code&gt;, then we can debug (inspect) it with&#xA;&lt;code&gt;terraform output&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;And that&amp;rsquo;s it for today! In a future post, we will continue from here by using&#xA;&lt;a href=&#34;https://www.ansible.com/&#34;&gt;Ansible&lt;/a&gt; to install and set up Miniflux in our new&#xA;Linode.&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;Alpine Linux does not use &lt;code&gt;systemd&lt;/code&gt;.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2024/01/terraforming-a-linode-hello-world/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: Terraforming a Linode: hello world&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/alpine-linux/&#34;&gt;#alpine-linux&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bestof/&#34;&gt;#bestof&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terraform/&#34;&gt;#terraform&lt;/a&gt;&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
