<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Kuber Roman]]></title><description><![CDATA[DevOps Notes]]></description><link>https://blog.rgeraskin.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1710879665451/3efcd061-1c5b-4035-abd1-371e37f02ce0.png</url><title>Kuber Roman</title><link>https://blog.rgeraskin.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 07:05:22 GMT</lastBuildDate><atom:link href="https://blog.rgeraskin.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Infrastructure testing in practice]]></title><description><![CDATA[Modern infrastructure moves fast. Configs change, components get upgraded, and small tweaks can have big ripple effects. Are you sure everything still works after each change?
That’s where infrastructure auto tests shine: they validate real behavior ...]]></description><link>https://blog.rgeraskin.dev/infra-tests</link><guid isPermaLink="true">https://blog.rgeraskin.dev/infra-tests</guid><category><![CDATA[Python]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Infrastructure as code]]></category><dc:creator><![CDATA[Roman Geraskin]]></dc:creator><pubDate>Mon, 29 Sep 2025 15:22:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759158885141/3b6114ce-6b61-444f-98ed-fa94c20b4467.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Modern infrastructure moves fast. Configs change, components get upgraded, and small tweaks can have big ripple effects. Are you sure everything still works after each change?</p>
<p>That’s where infrastructure auto tests shine: they validate real behavior in a real cluster and act as living specs for your platform.</p>
<p>In this post, we’ll walk through a practical example: a test that validates Kubernetes Cluster Autoscaler by forcing a scale-up and checking end-to-end results.</p>
<h2 id="heading-what-we-test-cluster-autoscaler">What we test: Cluster Autoscaler</h2>
<p>This is a simple example to show the approach. The goal: prove that writing infra tests is straightforward and genuinely useful.</p>
<p>Cluster Autoscaler watches pending pods and scales the right node group when capacity is tight.</p>
<p>Why test it? What can go wrong?</p>
<ul>
<li><p>A bad autoscaler release.</p>
</li>
<li><p>Helm chart breaking change.</p>
</li>
<li><p>Misconfiguration.</p>
</li>
<li><p>Incompatible cluster components.</p>
</li>
<li><p>Access/quotas/IAM issues that block scale-up.</p>
</li>
<li><p>Networking issues (for external autoscalers).</p>
</li>
</ul>
<p>Our test covers the full path end-to-end:</p>
<p><code>Pending pods ──&gt; Cluster Autoscaler ──&gt; Node group scales ──&gt; Pods Ready</code></p>
<ol>
<li><p>Detect the target node group for the current cluster context.</p>
</li>
<li><p>Create a deployment with replicas set to <code>current_nodes + 1</code>.</p>
</li>
<li><p>Use pod anti-affinity, so pods spread across nodes, guaranteeing the need for an extra node.</p>
</li>
<li><p>Use node affinity/selector, so pods land only on the intended node group.</p>
</li>
<li><p>Verify we see the expected pending pod initially; then wait for all pods to be <code>Ready</code> within a timeout.</p>
</li>
<li><p>Assert success when all pods are scheduled and ready.</p>
</li>
</ol>
<h2 id="heading-how-the-test-is-designed">How the test is designed</h2>
<p>I wrote the test in Python because it’s easy to read, most engineers already know it, and it’s usually available by default. No extra Python deps needed.</p>
<p>If you prefer Bash, check out the <a target="_blank" href="https://github.com/bats-core/bats-core">bats framework</a>.</p>
<ul>
<li><p><strong>Node group detection</strong>: EKS clusters use label <code>eks.amazonaws.com/nodegroup</code> and expect group <code>main</code>.</p>
</li>
<li><p><strong>Workload shape</strong>: A tiny deployment using <code>registry.k8s.io/pause:3.9</code> with small CPU/memory requests. We add strict scheduling constraints:</p>
<ul>
<li><p>Pod anti-affinity (<code>requiredDuringSchedulingIgnoredDuringExecution</code>) to force one pod per node.</p>
</li>
<li><p>Node selector/affinity so we only use the target node group.</p>
</li>
</ul>
</li>
<li><p><strong>Assertions</strong>:</p>
<ul>
<li><p>Immediately after creation, we expect exactly one pod to be <code>Pending</code>.</p>
</li>
<li><p>Within the timeout (5 minutes by default), all pods must become <code>Ready</code>.</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-running-the-test">Running the test</h2>
<p>I use <code>mise</code> to run the test. It’s a handy tool to manage dependencies and tasks, so teammates don’t need to learn a custom Python CLI.</p>
<p>Run <code>mise tasks</code> to see what’s available. For the autoscaler test: <code>mise test:cluster-autoscaler</code>.</p>
<p>It’s easy to drop into CI: add <code>kubectl</code> and <code>python</code> to <code>.mise.toml</code> and run <code>mise install</code> up front. More details in my post about <a target="_blank" href="https://blog.rgeraskin.dev/dev-env-with-mise">mise</a>.</p>
<h2 id="heading-code-examples">Code examples</h2>
<p>Below are minimal snippets from the real test suite. Full code is in the <a target="_blank" href="https://github.com/rgeraskin/infra-tests">repo</a>.</p>
<pre><code class="lang-python"><span class="hljs-comment"># cluster_autoscaler/test.py</span>

<span class="hljs-keyword">from</span> utils <span class="hljs-keyword">import</span> BaseInfraTest
<span class="hljs-comment"># other imports</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ClusterAutoscaler</span>(<span class="hljs-params">BaseInfraTest</span>):</span>
    RESOURCE_NAME = <span class="hljs-string">"infra-tests-cluster-autoscaler"</span>
    TIMEOUT_SECONDS = <span class="hljs-number">300</span>  <span class="hljs-comment"># 5 minutes</span>
    DEPLOYMENT_CHECK_DELAY_SECONDS = <span class="hljs-number">5</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">setUp</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-comment"># prepare common things that are used in all tests</span>
        super().setUp()

        <span class="hljs-comment"># set internal variables</span>
        <span class="hljs-comment"># ...</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_get_target_nodes</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-comment"># Get all nodes and filter for target nodegroup</span>
        <span class="hljs-comment"># ...</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_cluster_autoscaler</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-comment"># Get current nodes in target node group</span>
        target_nodes, nodegroup_label_value = self._get_target_nodes()
        current_node_count = len(target_nodes)

        <span class="hljs-comment"># Calculate required replicas (current nodes + extra to trigger scaling)</span>
        required_replicas = current_node_count + <span class="hljs-number">1</span>

        <span class="hljs-comment"># Read and prepare deployment manifest</span>
        <span class="hljs-keyword">with</span> open(self.deployment_path, encoding=<span class="hljs-string">"utf-8"</span>) <span class="hljs-keyword">as</span> f:
            manifest = f.read()

        <span class="hljs-comment"># Apply deployment manifest</span>
        self.command(
            <span class="hljs-string">f"kubectl --context <span class="hljs-subst">{self.current_context}</span> apply -f -"</span>,
            input=manifest
            % (
                self.RESOURCE_NAME,  <span class="hljs-comment"># deployment name</span>
                self.namespace,  <span class="hljs-comment"># namespace</span>
                self.RESOURCE_NAME,  <span class="hljs-comment"># app label</span>
                required_replicas,  <span class="hljs-comment"># replicas count</span>
                self.RESOURCE_NAME,  <span class="hljs-comment"># selector matchLabels app</span>
                self.RESOURCE_NAME,  <span class="hljs-comment"># pod template app label</span>
                json.dumps(
                    {self.nodegroup_label_key: nodegroup_label_value}
                ),  <span class="hljs-comment"># nodeSelector</span>
                self.RESOURCE_NAME,  <span class="hljs-comment"># pod anti-affinity</span>
            ),
        )

        <span class="hljs-comment"># Give k8s a moment to process the deployment</span>
        time.sleep(self.DEPLOYMENT_CHECK_DELAY_SECONDS)

        <span class="hljs-comment"># Check that there is 1 pod in pending state</span>
        pods_cmd = (
            <span class="hljs-string">f"kubectl --context <span class="hljs-subst">{self.current_context}</span>"</span>
            <span class="hljs-string">f" --namespace <span class="hljs-subst">{self.namespace}</span>"</span>
            <span class="hljs-string">f" get pods"</span>
            <span class="hljs-string">f" -l app=<span class="hljs-subst">{self.RESOURCE_NAME}</span>"</span>
            <span class="hljs-string">f" -o jsonpath='{{.items[*].status.phase}}'"</span>
        )
        pod_phases = self.command(pods_cmd).split()
        self.assertEqual(
            pod_phases.count(<span class="hljs-string">"Pending"</span>),
            <span class="hljs-number">1</span>,
            <span class="hljs-string">f"Expected 1 pod in pending state, got <span class="hljs-subst">{pod_phases.count(<span class="hljs-string">'Pending'</span>)}</span>"</span>,
        )

        <span class="hljs-comment"># Wait for all pods to be ready using kubectl wait</span>
        wait_cmd = (
            <span class="hljs-string">f"kubectl --context <span class="hljs-subst">{self.current_context}</span>"</span>
            <span class="hljs-string">f" --namespace <span class="hljs-subst">{self.namespace}</span>"</span>
            <span class="hljs-string">f" wait pod"</span>
            <span class="hljs-string">f" -l app=<span class="hljs-subst">{self.RESOURCE_NAME}</span>"</span>
            <span class="hljs-string">f" --for=condition=Ready"</span>
            <span class="hljs-string">f" --timeout=<span class="hljs-subst">{self.TIMEOUT_SECONDS}</span>s"</span>
        )

        self.command(wait_cmd)
</code></pre>
<p>Mise configuration:</p>
<pre><code class="lang-toml"><span class="hljs-comment"># .mise.toml</span>
<span class="hljs-section">[tools]</span>
<span class="hljs-attr">python</span> = <span class="hljs-string">"3.13.0"</span>
<span class="hljs-attr">kubectl</span> = <span class="hljs-string">"1.31.1"</span>

<span class="hljs-section">[tasks.test]</span>
<span class="hljs-attr">description</span> = <span class="hljs-string">"Run all infrastructure tests"</span>
<span class="hljs-attr">run</span> = <span class="hljs-string">"python -m unittest discover -v"</span>

<span class="hljs-section">[tasks."test:cluster-autoscaler"]</span>
<span class="hljs-attr">description</span> = <span class="hljs-string">"Run tests for cluster-autoscaler"</span>
<span class="hljs-attr">run</span> = <span class="hljs-string">"""
python -m unittest -v cluster_autoscaler.test.ClusterAutoscaler
"""</span>
</code></pre>
<p><code>deployment.yaml</code> is a regular Kubernetes Deployment with <code>%s</code> placeholders that get templated at runtime. A full example is <a target="_blank" href="https://github.com/rgeraskin/blog-infra-tests">here</a>.</p>
<h2 id="heading-principles-of-solid-infra-tests">Principles of solid infra tests</h2>
<ul>
<li><p><strong>Simple</strong>: easy to maintain and add new.</p>
</li>
<li><p><strong>Small</strong>: extract boilerplate into shared utilities.</p>
</li>
<li><p><strong>Fast</strong>: speed matters.</p>
</li>
<li><p><strong>Wait on conditions</strong>: wait for states, not fixed sleeps.</p>
</li>
</ul>
<p>Well-structured tests make it easy to add new ones — even with help from LLMs. Write the test case, then ask to implement it using the existing ones as a reference.</p>
<h2 id="heading-closing-thoughts">Closing thoughts</h2>
<p>Infrastructure tests are your early-warning system for platform regressions. By validating real behavior in real clusters, you turn assumptions into executable specs.</p>
<p>The Cluster Autoscaler test is a concise, low-risk example that catches issues you might otherwise notice only after a late-night surprise.</p>
<p>Apply the same approach to other components to keep changes safe and verified.</p>
]]></content:encoded></item><item><title><![CDATA[Introducing GoDiffYAML Tool 💪]]></title><description><![CDATA[The YAML Diffing Dilemma
YAML files are everywhere in software development — think Kubernetes manifests, Ansible playbooks, or any configuration management system. But when these files contain multiple documents (separated by ---), comparing changes ...]]></description><link>https://blog.rgeraskin.dev/godiffyaml</link><guid isPermaLink="true">https://blog.rgeraskin.dev/godiffyaml</guid><category><![CDATA[Devops]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[k8s]]></category><category><![CDATA[YAML]]></category><category><![CDATA[Diff]]></category><dc:creator><![CDATA[Roman Geraskin]]></dc:creator><pubDate>Mon, 05 May 2025 23:04:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746485562551/5124cff8-014b-4550-bea3-43f79a0050a7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-the-yaml-diffing-dilemma">The YAML Diffing Dilemma</h2>
<p>YAML files are everywhere in software development — think Kubernetes manifests, Ansible playbooks, or any configuration management system. But when these files contain multiple documents (separated by <code>---</code>), comparing changes with traditional diff tools becomes a mess.</p>
<p>These tools treat the file as a single blob of text, often producing confusing or outright incorrect diffs. This problem gets worse with large YAML files packed with many documents, making it nearly impossible to spot specific changes.</p>
<p>That’s why I created <a target="_blank" href="https://github.com/rgeraskin/godiffyaml"><strong>godiffyaml</strong></a>, a tool designed to solve this exact issue. Whether you’re a developer, DevOps engineer, or system admin, <strong>godiffyaml</strong> makes diffing multi-document YAML files painless.</p>
<h2 id="heading-why-standard-diff-tools-struggle">Why Standard Diff Tools Struggle</h2>
<p>Here’s what makes diffing multi-document YAML tricky with standard tools:</p>
<ol>
<li><p><strong>Document Order Awareness:</strong> If a document is moved within a YAML file, standard diff tools display it as a completely new document. Any changes inside it might be overlooked.</p>
</li>
<li><p><strong>Document Awareness</strong>: Tools like <code>diff</code> don't recognize YAML's <code>---</code> separators, so changes across documents get mixed up.</p>
</li>
<li><p><strong>Values Notation Awareness</strong>: According to the YAML specification, the way values are noted is quite flexible. For example, strings can be quoted or not. However, for diff tools, these differences still appear as changes.</p>
</li>
<li><p><strong>Nested Structure Awareness</strong>: YAML’s hierarchical structure—nested keys and values—means small changes can drown in a flood of irrelevant differences.</p>
</li>
</ol>
<p>I built <strong>godiffyaml</strong> to address these pain points head-on, offering a smarter way to compare multi-document YAML files.</p>
<p>Let's explain the issues and how <strong>godiffyaml</strong> solves them in detail.</p>
<h3 id="heading-document-order-awareness">Document Order Awareness</h3>
<p>A basic YAML file with random Kubernetes manifests:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ConfigMap</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">app-config</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">data:</span>
  <span class="hljs-attr">DB_HOST:</span> <span class="hljs-string">"localhost"</span>
  <span class="hljs-attr">DB_PORT:</span> <span class="hljs-string">"5432"</span>
  <span class="hljs-attr">CACHE_TTL:</span> <span class="hljs-string">"3600"</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">frontend-svc</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">ports:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
      <span class="hljs-attr">targetPort:</span> <span class="hljs-number">3000</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">frontend</span>
</code></pre>
<p>Now reverse the order of the documents: place <code>kind: Service</code> before <code>kind: ConfigMap</code> and then compare the differences.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746474180299/fd0e8f09-ffba-4eb3-b8b7-5319b6c2e6ec.png" alt class="image--center mx-auto" /></p>
<p>Can you quickly tell if the <code>ConfigMap</code> has just been moved in the YAML, or if there are additional changes? Imagine having many such "movements" inside; reviewing them would become a nightmare.</p>
<p>With <strong>godiffyaml</strong>, you can sort YAML files by path, such as <code>.kind</code>, and compare them with any standard tool.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746477055917/6532afa8-ce08-4e98-a46f-86320b51b7cf.png" alt class="image--center mx-auto" /></p>
<p>Or just</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746478594428/7ff63695-2407-4837-abbc-5d08eadd3a28.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-document-awareness">Document Awareness</h3>
<p>Let's change the value of <code>PGHOST</code> from <code>host2</code> to <code>host-changed</code> in <code>app2-secret</code> within a YAML file containing two secrets, and then compare the original with the modified version.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Secret</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">app1-secret</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">stringData:</span>
  <span class="hljs-attr">PGDATABASE:</span> <span class="hljs-string">db1</span>
  <span class="hljs-attr">PGHOST:</span> <span class="hljs-string">host1</span>
  <span class="hljs-attr">PGPASSWORD:</span> <span class="hljs-string">pass1</span>
  <span class="hljs-attr">PGUSER:</span> <span class="hljs-string">user1</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Secret</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">app2-secret</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">stringData:</span>
  <span class="hljs-attr">PGDATABASE:</span> <span class="hljs-string">db2</span>
  <span class="hljs-attr">PGHOST:</span> <span class="hljs-string">host2</span> <span class="hljs-comment"># this will be changed</span>
  <span class="hljs-attr">PGPASSWORD:</span> <span class="hljs-string">pass2</span>
  <span class="hljs-attr">PGUSER:</span> <span class="hljs-string">user2</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746478871250/4e08f4ef-b012-482b-9e84-62e828db6263.png" alt class="image--center mx-auto" /></p>
<p>Can you guess which secret has changed? To find its name, you need to expand the context for the difft tool or check the full diff in your favorite diff tool. Now, imagine there are many such changes across several documents.</p>
<p><strong>godiffyaml</strong> solves this problem:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746479236924/708c9492-da21-4f9f-bf40-9394558b54df.png" alt class="image--center mx-auto" /></p>
<p>See the name in a rectangle? It's templated from the <code>-paths</code> flag: <code>&lt;.kind&gt;_&lt;.metadata.name&gt;.yaml</code>.</p>
<h3 id="heading-values-notation-awareness">Values Notation Awareness</h3>
<p>Let’s compare the same YAML files, but the first one will have unquoted strings and the second one will have quoted strings. According to the YAML specification, they are considered the same, but your IDE probably doesn't see it that way.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746475336123/d5ea3a57-38cb-499c-af9d-e254a3201185.png" alt class="image--center mx-auto" /></p>
<p>It's just distracting noise because it's YAML with the same values.</p>
<p>Just like the <em>'No Document Order Awareness'</em> example above, we can either use <code>godiffyaml sort</code> and compare with a usual tool or simply use <code>godiffyaml diff</code>.</p>
<p>But now let's use the <code>k8s</code> subcommand: <code>godiffyaml k8s</code> is a shortcut for <code>godiffyaml diff -paths apiVersion,kind,metadata.namespace,metadata.name</code>. Notice that the dot at the beginning of each path is optional.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746480194333/52dadd48-ffe2-46e9-97ec-0e9ae552118d.png" alt class="image--center mx-auto" /></p>
<p>Note that we don’t use the <code>-order</code> flag for the <code>sort</code> subcommand. Details are provided below in the <em>‘Magic’</em> section.</p>
<h3 id="heading-nested-structure-awareness">Nested Structure Awareness</h3>
<p>Typical diff tools show changes for lines, not values. However, it's useful to see specific differences. <a target="_blank" href="https://github.com/Wilfred/difftastic">Difftastic</a> addresses this issue, and <strong>godiffyaml</strong> uses it behind the scenes.</p>
<blockquote>
<p>Difftastic — a structural diff tool that compares files based on their syntax. It’s a great tool that shows differences only for elements that changed, not the lines.</p>
</blockquote>
<h2 id="heading-how-godiffyaml-works">How GoDiffYAML Works</h2>
<p>It has two subcommands: <code>sort</code> and <code>diff</code>. Let's break them down first.</p>
<h3 id="heading-sort-subcommand">Sort subcommand</h3>
<ol>
<li><p>Reads a YAML file.</p>
</li>
<li><p>Sorts documents inside by YAML paths <strong>if</strong> the <code>-order</code> flag is provided.</p>
</li>
<li><p>Outputs the YAML file with all documents to stdout.</p>
</li>
</ol>
<h3 id="heading-diff-subcommand">Diff subcommand</h3>
<ol>
<li><p>Reads YAML files.</p>
</li>
<li><p>Saves each document in a temporary directory with names based on path values.</p>
<p> <em>E.g. for</em> <code>paths=apiVersion,kind,metadata.namespace,metadata.name</code> <em>document name will be like</em> <code>apps_v1_Deployment_default_backend-api.yaml</code></p>
</li>
<li><p>Runs <strong>difftastic</strong> on the directories to compare them.</p>
</li>
</ol>
<h3 id="heading-magic">💫 Magic 💫</h3>
<p>All the magic inside is based on two ideas:</p>
<ol>
<li><p>By reading and then dumping YAML with the same Go YAML library, we ensure that YAML documents have consistent value notation and key ordering. This applies to both <code>sort</code> and <code>diff</code> modes.</p>
</li>
<li><p>By saving files with templated names, we can compare directories with <strong>difftastic</strong>, which will display the file names. The file names help us identify documents due to the templating.</p>
</li>
</ol>
<p>The structural diff relies entirely on <strong>difftastic</strong> because I don't want to reinvent the wheel. So, we need <strong>difftastic</strong> to use the <code>diff</code> or <code>k8s</code> subcommands.</p>
<p>Additionally, any extra flags for <strong>godiffyaml</strong> are passed to the <strong>difftastic</strong> backend.</p>
<h2 id="heading-why-youll-love-godiffyaml">Why You’ll Love GoDiffYAML</h2>
<ul>
<li><p><strong>Saves Time</strong>: No more digging through messy diffs to find what changed.</p>
</li>
<li><p><strong>Reduces Errors</strong>: Clear, structured output means fewer mistakes when reviewing updates.</p>
</li>
<li><p><strong>Handles Scale</strong>: Works seamlessly with large, complex YAML files.</p>
</li>
</ul>
<h2 id="heading-wrap-up">Wrap-Up</h2>
<p>If multi-document YAML files are part of your daily grind, <strong>godiffyaml</strong> is here to make your life easier. By respecting YAML’s structure and delivering precise, readable diffs, it turns a frustrating task into a breeze. Give it a try — I think you’ll wonder how you ever managed without it!</p>
<p>So go diff that YAML💪</p>
]]></content:encoded></item><item><title><![CDATA[AI with Kubernetes: Operations for Developers 🤖]]></title><description><![CDATA[Artificial Intelligence is making Kubernetes cluster management accessible to everyone, even those without deep Kubernetes expertise. By integrating AI tools through the Model Context Protocol (MCP), beginners can interact with clusters using natural...]]></description><link>https://blog.rgeraskin.dev/ai-with-kubernetes</link><guid isPermaLink="true">https://blog.rgeraskin.dev/ai-with-kubernetes</guid><category><![CDATA[Devops]]></category><category><![CDATA[AI]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[claude.ai]]></category><category><![CDATA[cursor]]></category><category><![CDATA[chatgpt]]></category><dc:creator><![CDATA[Roman Geraskin]]></dc:creator><pubDate>Fri, 28 Mar 2025 23:27:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1743205418403/9c40752b-38e6-40f2-a74d-4ac88ad494a4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Artificial Intelligence is making Kubernetes cluster management accessible to everyone, even those without deep Kubernetes expertise. By integrating AI tools through the Model Context Protocol (MCP), beginners can interact with clusters using natural language, avoiding complex <code>kubectl</code> commands.</p>
<p>This article explores three beginner-friendly approaches — <a target="_blank" href="https://claude.ai/download">Claude Desktop</a>, <a target="_blank" href="https://www.cursor.com">Cursor</a>, and <a target="_blank" href="https://k9scli.io">K9S</a> with <a target="_blank" href="https://github.com/robusta-dev/holmesgpt">HolmesGPT</a> — each designed to simplify Kubernetes management for newcomers.</p>
<blockquote>
<p>The Model Context Protocol (MCP) is a powerful framework for integrating AI models into development environments. MCP enables seamless communication between AI systems and tools like Kubernetes by providing a standardized way to pass context and commands. With MCP, you can query cluster states, automate resource management, or even troubleshoot issues using natural language. To use MCP effectively with Kubernetes, you’ll need a compatible server setup that bridges your AI tool with kubectl, the Kubernetes command-line interface.</p>
</blockquote>
<p>There are many MCPs for Kubernetes, each with different features and programming languages like TypeScript, Go, and Python. They are still being actively developed, so there are some bugs. They are not quite ready for serious everyday use yet.</p>
<ol>
<li><p><a target="_blank" href="https://github.com/Flux159/mcp-server-kubernetes">https://github.com/Flux159/mcp-server-kubernetes</a> - TypeScript. This MCP is mentioned in the Anthropic documentation and looks promising. It also has more GitHub stars than the others.</p>
</li>
<li><p><a target="_blank" href="https://github.com/rohitg00/kubectl-mcp-server">https://github.com/rohitg00/kubectl-mcp-server</a> - A lot of declared functionality but currently less stable than others, Python-based.</p>
</li>
<li><p><a target="_blank" href="https://github.com/strowk/mcp-k8s-go">https://github.com/strowk/mcp-k8s-go</a> - Golang-based</p>
</li>
<li><p><a target="_blank" href="https://github.com/manusa/kubernetes-mcp-server">https://github.com/manusa/kubernetes-mcp-server</a> - Golang-based, but not just a wrapper around <code>kubectl</code> or <code>helm</code>—it doesn't require external dependencies.</p>
</li>
<li><p><a target="_blank" href="https://github.com/wenhuwang/mcp-k8s-eye">https://github.com/wenhuwang/mcp-k8s-eye</a> - Golang</p>
</li>
</ol>
<p>In this article, I use the first two to demonstrate basic functionality.</p>
<h2 id="heading-approach-1-claude-desktop">Approach 1: Claude Desktop</h2>
<h5 id="heading-in-my-opinion-claude-desktophttpsclaudeaidownload-is-an-excellent-solution-for-communicating-with-mcp-servers-today-because-of-its-user-friendly-interface-i-will-use-mcp-server-kubernetes-to-link-claude-with-kubernetes">In my opinion, <a target="_blank" href="https://claude.ai/download">Claude Desktop</a> is an excellent solution for communicating with MCP servers today because of its user-friendly interface. I will use <code>mcp-server-kubernetes</code> to link Claude with Kubernetes.</h5>
<h3 id="heading-step-1-configure-claude-desktop">Step 1: Configure Claude Desktop</h3>
<ol>
<li><h5 id="heading-configure-claude-desktop-mcp-settings-go-to-settings-gt-developer-menu-to-add-mcp-server-or-just-edit-libraryapplication-supportclaudeclaudedesktopconfigjson-macos-with">Configure Claude Desktop MCP Settings: go to <strong>Settings =&gt; Developer</strong> menu to add MCP server or just edit <code>~/Library/Application Support/Claude/claude_desktop_config.json</code> (macOS) with:</h5>
</li>
</ol>
<pre><code class="lang-json">{
  <span class="hljs-attr">"mcpServers"</span>: {
    <span class="hljs-attr">"kubernetes"</span>: {
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"npx"</span>,
      <span class="hljs-attr">"args"</span>: [<span class="hljs-string">"mcp-server-kubernetes"</span>]
    }
  }
}
</code></pre>
<ol start="2">
<li>Save and restart Claude.</li>
</ol>
<h3 id="heading-step-2-chat-with-your-cluster">Step 2: Chat with Your Cluster</h3>
<p>Open a chat in Claude and ask something like:</p>
<ul>
<li><p>“List deployments in default k8s ns”</p>
</li>
<li><p>“Show me nginx logs”</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743199513208/76eeebd2-8809-4198-aa6a-eae8ffceae62.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>See full example conversation <a target="_blank" href="https://claude.ai/share/6c2c4d8d-43ae-422f-bcfa-c12dec42913a">here</a>.</p>
</blockquote>
<p>Claude uses <code>mcp-server-kubernetes</code> to handle the technical details, making Kubernetes approachable for beginners by translating plain English into actions.</p>
<h2 id="heading-approach-2-cursor-ide">Approach 2: Cursor IDE</h2>
<p><a target="_blank" href="https://www.cursor.com">Cursor</a> is a VS Code fork designed to be a more AI-focused code editor. While it may not be as convenient as Claude, if you spend a lot of time in an IDE, it can be useful to have Kubernetes access with natural language right in the same window.</p>
<h3 id="heading-step-1-install-mcp-server">Step 1: Install mcp server</h3>
<p>Now I will use <a target="_blank" href="https://github.com/rohitg00/kubectl-mcp-server">kubectl-mcp-server</a>. You can also use the MCP server from the previous approach or any other from the list of MCPs mentioned earlier—they are interchangeable.</p>
<pre><code class="lang-bash">pipx install kubectl-mcp-tool
</code></pre>
<h3 id="heading-step-2-configure-cursor">Step 2: Configure Cursor</h3>
<ol>
<li><p>Go to <strong>Cursor =&gt; Settings =&gt; Cursor Settings =&gt; MCP</strong>.</p>
</li>
<li><p>Click <strong>"Add new global MCP server"</strong>.</p>
</li>
<li><p>Add:</p>
</li>
</ol>
<pre><code class="lang-json">{
  <span class="hljs-attr">"mcpServers"</span>: {
    <span class="hljs-attr">"kubernetes"</span>: {
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"~/.local/pipx/venvs/kubectl-mcp-tool/bin/python"</span>,
      <span class="hljs-attr">"args"</span>: [
        <span class="hljs-string">"-m"</span>,
        <span class="hljs-string">"kubectl_mcp_tool.minimal_wrapper"</span>
      ]
    }
  }
}
</code></pre>
<h3 id="heading-step-3-interact-with-your-cluster">Step 3: Interact with Your Cluster</h3>
<p>Open chat in Cursor and use prompts like:</p>
<ul>
<li><p>“List all pods in default k8s ns”</p>
</li>
<li><p>“What is the status of my nginx k8s deployment?”</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743200348371/0bb49fa0-b04b-491c-b0f9-feab1d68ee28.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-approach-3-using-k9s-with-holmesgpt">Approach 3: Using K9s with HolmesGPT</h2>
<p><a target="_blank" href="https://github.com/robusta-dev/holmesgpt">HolmesGPT</a>, from Robusta, is a tool that simplifies Kubernetes troubleshooting. It investigates issues automatically, requiring no prior expertise. Use it via the Robusta SaaS platform or CLI with queries like <code>holmes ask "what pods are unhealthy and why?"</code>.</p>
<p>We can integrate it with k9s, a terminal-based UI that allows you to interact with your Kubernetes clusters. It's a popular tool for working with Kubernetes, making it easier to navigate, observe, and manage deployed applications.</p>
<p>Actually, this approach is different from the previous ones. It doesn’t use MCP. However, it is powerful, so I have to mention it.</p>
<h3 id="heading-step-1-configuring-k9s-with-holmesgpt">Step 1: Configuring K9s with HolmesGPT</h3>
<ol>
<li><p>Install HolmesGPT (<a target="_blank" href="https://github.com/robusta-dev/holmesgpt/blob/master/docs/installation.md#cli-installation">instructions</a>)</p>
</li>
<li><pre><code class="lang-bash">  <span class="hljs-comment"># for mac</span>
  brew tap robusta-dev/homebrew-holmesgpt
  brew install holmesgpt
</code></pre>
</li>
<li><p>Export OpenAI API key, for example with <code>~/.zshrc</code>: <code>export OPENAI_API_KEY=XXX</code></p>
<p> I bet you can easily Google how to get an API key. Certainly, you can use <a target="_blank" href="https://github.com/robusta-dev/holmesgpt/blob/master/docs/api-keys.md">other AIs</a>.</p>
</li>
<li><p>Add Holmes as plugin to k9s: edit <code>~/Library/Application Support/k9s/plugins.yaml</code> (mac). See detailed instructions <a target="_blank" href="https://github.com/robusta-dev/holmesgpt/blob/master/docs/k9s.md">here</a>.</p>
</li>
</ol>
<pre><code class="lang-yaml"><span class="hljs-attr">plugins:</span>
  <span class="hljs-attr">holmesgpt:</span>
    <span class="hljs-attr">shortCut:</span> <span class="hljs-string">Shift-H</span>
    <span class="hljs-attr">description:</span> <span class="hljs-string">Ask</span> <span class="hljs-string">HolmesGPT</span>
    <span class="hljs-attr">scopes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">all</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">bash</span>
    <span class="hljs-attr">background:</span> <span class="hljs-literal">false</span>
    <span class="hljs-attr">confirm:</span> <span class="hljs-literal">false</span>
    <span class="hljs-attr">args:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">-c</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">|
        holmes ask "why is $NAME of $RESOURCE_NAME in namespace $NAMESPACE not working as expected?"
        echo "Press 'q' to exit"
        while : ; do
        read -n 1 k &lt;&amp;1
        if [[ $k = q ]] ; then
        break
        fi
        done
</span>  <span class="hljs-attr">custom-holmesgpt:</span>
    <span class="hljs-attr">shortCut:</span> <span class="hljs-string">Shift-Q</span>
    <span class="hljs-attr">description:</span> <span class="hljs-string">Custom</span> <span class="hljs-string">HolmesGPT</span> <span class="hljs-string">Ask</span>
    <span class="hljs-attr">scopes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">all</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">bash</span>
    <span class="hljs-attr">background:</span> <span class="hljs-literal">false</span>
    <span class="hljs-attr">confirm:</span> <span class="hljs-literal">false</span>
    <span class="hljs-attr">args:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">-c</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">|
        INSTRUCTIONS="# Edit the line below. Lines starting with '#' will be ignored."
        DEFAULT_ASK_COMMAND="why is $NAME of $RESOURCE_NAME in namespace $NAMESPACE not working as expected?"
        QUESTION_FILE=$(mktemp)
</span>
        <span class="hljs-string">echo</span> <span class="hljs-string">"$INSTRUCTIONS"</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">"$QUESTION_FILE"</span>
        <span class="hljs-string">echo</span> <span class="hljs-string">"$DEFAULT_ASK_COMMAND"</span> <span class="hljs-string">&gt;&gt;</span> <span class="hljs-string">"$QUESTION_FILE"</span>

        <span class="hljs-comment"># Open the line in the default text editor</span>
        <span class="hljs-string">${EDITOR:-nano}</span> <span class="hljs-string">"$QUESTION_FILE"</span>

        <span class="hljs-comment"># Read the modified line, ignoring lines starting with '#'</span>
        <span class="hljs-string">user_input=$(grep</span> <span class="hljs-string">-v</span> <span class="hljs-string">'^#'</span> <span class="hljs-string">"$QUESTION_FILE"</span><span class="hljs-string">)</span>
        <span class="hljs-attr">echo running:</span> <span class="hljs-string">holmes</span> <span class="hljs-string">ask</span> <span class="hljs-string">"\"$user_input\""</span>

        <span class="hljs-string">holmes</span> <span class="hljs-string">ask</span> <span class="hljs-string">"$user_input"</span>
        <span class="hljs-string">echo</span> <span class="hljs-string">"Press 'q' to exit"</span>
        <span class="hljs-attr">while :</span> <span class="hljs-string">;</span> <span class="hljs-string">do</span>
        <span class="hljs-string">read</span> <span class="hljs-string">-n</span> <span class="hljs-number">1</span> <span class="hljs-string">k</span> <span class="hljs-string">&lt;&amp;1</span>
        <span class="hljs-string">if</span> [[ <span class="hljs-string">$k</span> <span class="hljs-string">=</span> <span class="hljs-string">q</span> ]] <span class="hljs-string">;</span> <span class="hljs-string">then</span>
        <span class="hljs-string">break</span>
        <span class="hljs-string">fi</span>
        <span class="hljs-string">done</span>
</code></pre>
<ol start="4">
<li>Start K9s, select pod and ask Holmes with <code>Shift-H</code> or <code>Shift-Q</code></li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743201575892/8c713aca-2265-47c9-9ddd-55e478e9513b.png" alt class="image--center mx-auto" /></p>
<p>HolmesGPT can do much more. Check the <a target="_blank" href="https://github.com/robusta-dev/holmesgpt">repository</a> for more details. Also, there are other tools, like <a target="_blank" href="https://k8sgpt.ai">k8sgpt</a>, that can make the life easier. Maybe some of them can be integrated with Lens too.</p>
<h2 id="heading-why-this-matters-for-beginners">Why This Matters for Beginners</h2>
<p>These tools democratize Kubernetes:</p>
<ul>
<li><p><strong>Claude</strong>: A desktop app where anyone can chat with their cluster.</p>
</li>
<li><p><strong>Cursor</strong>: An IDE that lets developers code and manage k8s without expertise.</p>
</li>
<li><p><strong>K9s</strong>: A visual tool with AI to troubleshoot effortlessly.</p>
</li>
</ul>
<p>Needless to say, all of this is not meant for production use. Moreover, MCP servers are still full of bugs and have limited functionality. However, it can still be useful for local development.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Claude Desktop, Cursor, and K9s with HolmesGPT empower beginners to manage Kubernetes clusters with AI, no solid K8s experience required. They turn tasks into simple conversations, making DevOps accessible to all. Try them out and start exploring Kubernetes!</p>
]]></content:encoded></item><item><title><![CDATA[Automating Development Environment with Mise: Comprehensive Guide 💫]]></title><description><![CDATA[When working in a team, it's important for everyone to have a consistent environment. Also, different projects might require different versions of tools.
Additionally, automating routine tasks with the codebase is helpful, so some form of task manage...]]></description><link>https://blog.rgeraskin.dev/dev-env-with-mise</link><guid isPermaLink="true">https://blog.rgeraskin.dev/dev-env-with-mise</guid><category><![CDATA[Devops]]></category><category><![CDATA[CI/CD]]></category><category><![CDATA[Makefile]]></category><category><![CDATA[tools]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[development environments]]></category><dc:creator><![CDATA[Roman Geraskin]]></dc:creator><pubDate>Tue, 29 Oct 2024 09:06:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1730150532839/428f7559-7364-4c3e-9228-f439b6c22a0c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When working in a team, it's important for everyone to have a consistent environment. Also, different projects might require different versions of tools.</p>
<p>Additionally, automating routine tasks with the codebase is helpful, so some form of task management is also necessary.</p>
<h2 id="heading-background">Background</h2>
<p>As always, there are many tools available to solve these problems. For example, to create a development environment, there are:</p>
<ol>
<li><p><strong>Language-specific tools</strong></p>
<ol>
<li><p>Node.js: nvm / n</p>
</li>
<li><p>Python: venv / pyenv / poetry / conda</p>
</li>
<li><p>Ruby: rbenv</p>
</li>
<li><p>Java: jenv</p>
</li>
<li><p>Infrastructure tools:</p>
<ol>
<li><p>tfenv for Terraform</p>
</li>
<li><p>kbenv for kubectl</p>
</li>
<li><p>helmenv for Helm</p>
</li>
</ol>
</li>
<li><p>Environment variables: <a target="_blank" href="https://github.com/direnv/direnv">direnv</a></p>
</li>
</ol>
</li>
<li><p><strong>Language-agnostic tools</strong></p>
<ol>
<li><p><a target="_blank" href="https://asdf-vm.com">asdf</a> - popular all-in-one solution</p>
</li>
<li><p><a target="_blank" href="https://nix.dev">nix</a> - it’s better to have a lot of free time, lol</p>
</li>
<li><p>or even <a target="_blank" href="https://code.visualstudio.com/docs/devcontainers/tutorial">devcontainers</a> for VS Code: the docker-way</p>
</li>
</ol>
</li>
</ol>
<p>As we can see, there are many tools available 🤯, and there are even more for task management. While we can use <code>make</code>, it's not very user-friendly. So, there are many alternatives to <code>make</code>. Trust me, I've tried most of them</p>
<ol>
<li><p><a target="_blank" href="https://taskfile.dev">task</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/casey/just">just</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/earthly/earthly">Earthly</a> - docker inside 🤟</p>
</li>
<li><p><a target="_blank" href="https://github.com/trinio-labs/bake">bake</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/TekWizely/run">run</a> - last commit year ago</p>
</li>
<li><p><a target="_blank" href="https://github.com/xonixx/makesure"><strong>makesure</strong></a> - weird syntax</p>
</li>
<li><p><a target="_blank" href="https://github.com/zaaack/foy">foy</a> - if we love js</p>
</li>
<li><p><a target="_blank" href="https://github.com/tj/mmake">mmake</a> - <code>make</code> on steroids</p>
</li>
<li><p><a target="_blank" href="https://github.com/tj/robo">robo</a> - last commit 2 years ago</p>
</li>
<li><p><a target="_blank" href="https://github.com/apenwarr/redo">redo</a> - last commit 3 years ago</p>
</li>
<li><p>Or even <a target="_blank" href="https://buck2.build">buck2</a> / <a target="_blank" href="https://github.com/bazelbuild/bazel">bazel</a> / <a target="_blank" href="https://github.com/pantsbuild/pants">pants</a> / <a target="_blank" href="https://please.build">please</a> if we already have it in the project</p>
</li>
</ol>
<p>I actually really like the first three.</p>
<h2 id="heading-introducing-mise-development-tools-and-tasks-in-one-app">Introducing Mise: Development Tools and Tasks in One App</h2>
<p><a target="_blank" href="https://mise.jdx.dev">Mise</a>, inspired by <a target="_blank" href="https://asdf-vm.com">asdf</a> — the multiple runtime version manager, can use asdf’s repository with hundreds of tools as its successor. However, mise goes further:</p>
<ol>
<li><p>It supports several other "backends" to install tools outside the built-in repository.</p>
</li>
<li><p>It has a simpler CLI.</p>
</li>
<li><p>Tasks!</p>
</li>
</ol>
<h3 id="heading-define-a-toolset">Define a Toolset</h3>
<p>Mise is set up using a single file called <code>.mise.toml</code>. Here's an example:</p>
<pre><code class="lang-ini"><span class="hljs-comment"># .mise.toml</span>
<span class="hljs-section">[tools]</span>
<span class="hljs-attr">terraform</span> = <span class="hljs-string">"1.9"</span>
<span class="hljs-attr">terramate</span> = <span class="hljs-string">"0.9"</span>
<span class="hljs-attr">pre-commit</span> = <span class="hljs-string">"3"</span>
<span class="hljs-attr">awscli</span> = <span class="hljs-string">"2"</span>
<span class="hljs-attr">"pipx:detect-secrets"</span> = <span class="hljs-string">"1.4"</span>
<span class="hljs-attr">"go:github.com/containerscrew/tftools"</span> = <span class="hljs-string">"0.9.0"</span>
</code></pre>
<p>I can install it with a single command: <code>mise install</code>, see a demo gif in the next sections. Let me explain the contents.</p>
<p>Our team uses this file for the infrastructure repository with Terraform and <a target="_blank" href="https://github.com/terramate-io/terramate">Terramate</a> manifests. Terraform requires awscli to work with the AWS API provider.</p>
<p><code>detect-secrets</code> is a tool used in a <a target="_blank" href="https://github.com/pre-commit/pre-commit">pre-commit hook</a> to ensure no secrets are accidentally exposed to git. This tool is installed with <code>pipx</code>, and mise supports this backend out of the box.</p>
<p><code>tftools</code> is a tool that summarizes changes in Terraform plans. It's useful if you want to review plans for several environments or stacks at once. As we can see, it uses the Go backend to fetch a binary from the repository.</p>
<p>There are many other backends: <a target="_blank" href="https://mise.jdx.dev/dev-tools/backends/asdf.html">asdf</a>, <a target="_blank" href="https://mise.jdx.dev/dev-tools/backends/cargo.html">cargo</a>, <a target="_blank" href="https://mise.jdx.dev/dev-tools/backends/go.html">go</a>, <a target="_blank" href="https://mise.jdx.dev/dev-tools/backends/npm.html">npm</a>, <a target="_blank" href="https://mise.jdx.dev/dev-tools/backends/pipx.html">pipx</a>, <a target="_blank" href="https://mise.jdx.dev/dev-tools/backends/spm.html">spm</a>, <a target="_blank" href="https://mise.jdx.dev/dev-tools/backends/ubi.html">ubi</a>, <a target="_blank" href="https://mise.jdx.dev/dev-tools/backends/vfox.html">vfox</a>. Honestly, I'm not sure what spm and vfox are 🙂, but ubi is <a target="_blank" href="https://github.com/houseabsolute/ubi">The Universal Binary Installer</a>, which lets you install any binary from a tool’s GitHub release page.</p>
<p>Need to add more tools? Edit the file or run <code>mise use TOOL_NAME</code>. To see the built-in tool registry, run <code>mise registry</code>. For tools not listed there, we can use the full notation, as I did with <em>tftools</em> above.</p>
<h3 id="heading-manage-environments"><strong>Manage Environments</strong></h3>
<p>By default, mise installs a tool <strong>not system-wide</strong>. This means we can have different tool versions for different directories aka projects. If you prefer a system-wide installation, you can configure it by placing the settings in <code>~/.config/mise/config.toml</code>, making tools and tasks (see below) available in any directory.</p>
<p>Mise supports nested configurations. To find out which configuration provides a tool or task, run <code>mise ls</code>.</p>
<p>For example, you can have different Python versions for different projects. Mise determines which Python version to use based on the nearest <code>.mise.toml</code> file. It even supports <a target="_blank" href="https://mise.jdx.dev/lang/python.html#automatic-virtualenv-activation">automatic virtualenv activation</a>.</p>
<p>You can also set different environment variables for different directories, similar to <a target="_blank" href="https://github.com/direnv/direnv">direnv</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730137795481/33c39289-c7bf-4bf4-986b-804e6bd41f35.gif" alt class="image--center mx-auto" /></p>
<h3 id="heading-prepare-dev-env">Prepare Dev Env</h3>
<ol>
<li><p><code>brew install mise</code> or <a target="_blank" href="https://mise.jdx.dev/getting-started.html#alternate-installation-methods">use alternative installation methods</a></p>
</li>
<li><p><a target="_blank" href="https://mise.jdx.dev/getting-started.html#_2a-activate-mise">Activate mise</a> in your shell. Zsh example:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">echo</span> <span class="hljs-string">'eval "$(~/.local/bin/mise activate zsh)"'</span> &gt;&gt; ~/.zshrc
 <span class="hljs-comment"># restart shell or `source ~/.zshrc`</span>
</code></pre>
</li>
<li><p><code>mise install</code> in a dir with <code>.mise.toml</code> file<br /> or use my <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=rgeraskin.mise">VSCode extension</a></p>
</li>
</ol>
<p>So, add this simple instruction to the repo's <code>README.md</code>, place <code>.mise.toml</code> in the root of the repo, and your teammates can simply run <code>mise install</code> to get the same toolset. Make sure to pin tool versions to ensure consistency. 🙂</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730192549906/91e81498-e018-4a3b-bbcd-88fe83ffdbea.gif" alt class="image--center mx-auto" /></p>
<p>Also, mise is cross-platform, so people using Linux or Mac will follow the same instructions. No more juggling with brew, apt, or yum.</p>
<h3 id="heading-run-tasks">Run Tasks</h3>
<p>Let's define several tasks to make daily routines easier with that infrastructure repo. To run Terraform, <code>*.tf</code> files should be generated with Terramate. To create a plan, the <em>tf-state</em> must be initialized. To initialize a state, it's best to be logged into AWS. That's a lot to keep track of.</p>
<p>Here's a quick demo:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730140976742/282babf6-8269-4dce-90d9-9952cc0e9456.gif" alt class="image--center mx-auto" /></p>
<p>We can use mise tasks to make the process easier. I've removed the Terramate and SSO details to keep the example brief:</p>
<pre><code class="lang-ini"><span class="hljs-comment"># .mise.toml</span>

<span class="hljs-section">[tasks."tf.init"]</span>
<span class="hljs-attr">alias</span> = <span class="hljs-string">"tfi"</span>
<span class="hljs-attr">description</span> = <span class="hljs-string">"`terraform init`"</span>
<span class="hljs-attr">run</span> = <span class="hljs-string">"terraform init"</span>

<span class="hljs-section">[tasks."tf.validate"]</span>
<span class="hljs-attr">alias</span> = <span class="hljs-string">"tfv"</span>
<span class="hljs-attr">depends</span> = [<span class="hljs-string">"tf.init"</span>]
<span class="hljs-attr">description</span> = <span class="hljs-string">"`terraform validate`"</span>
<span class="hljs-attr">run</span> = <span class="hljs-string">"terraform validate"</span>

<span class="hljs-section">[tasks."tf.plan"]</span>
<span class="hljs-attr">alias</span> = <span class="hljs-string">"tfp"</span>
<span class="hljs-attr">depends</span> = [<span class="hljs-string">"tf.init"</span>]
<span class="hljs-attr">description</span> = <span class="hljs-string">"`terraform plan`"</span>
<span class="hljs-attr">run</span> = <span class="hljs-string">"""
#!/usr/bin/env bash

# can use args for this task
terraform plan -out=tfplan $@ 
# ring terminal when complete
tput bel 
"""</span>

<span class="hljs-section">[tasks."tf.summarize"]</span>
<span class="hljs-attr">alias</span> = <span class="hljs-string">"tfs"</span>
<span class="hljs-attr">description</span> = <span class="hljs-string">"`tftools summarize` with pre-generated plan file (terraform plan should be generated in advance)"</span>
<span class="hljs-attr">run</span> = <span class="hljs-string">"""
#!/usr/bin/env bash

terraform show -json tfplan &gt; plan.json
tftools summarize &lt; plan.json
"""</span>

<span class="hljs-section">[tasks."tf.apply"]</span>
<span class="hljs-attr">alias</span> = <span class="hljs-string">"tfa"</span>
<span class="hljs-attr">description</span> = <span class="hljs-string">"`terraform apply` with pre-generated plan file (terraform plan should be generated in advance)"</span>
<span class="hljs-attr">run</span> = <span class="hljs-string">"""
#!/usr/bin/env bash

cmd="terraform apply tfplan $@"
read -p "$cmd\n\nAre you sure? (Y/n) " choice
[ "$choice" = "n" ] || ($cmd; tput bel)
"""</span>

<span class="hljs-section">[tasks."tf.lock"]</span>
<span class="hljs-attr">depends</span> = [<span class="hljs-string">"tf.init"</span>]
<span class="hljs-attr">description</span> = <span class="hljs-string">"`terraform providers lock` with predefined platform list"</span>
<span class="hljs-attr">run</span> = <span class="hljs-string">"terraform providers lock -platform=linux_amd64 -platform=darwin_amd64 -platform=darwin_arm64 ; tput bel"</span>

<span class="hljs-section">[tasks."tf.console"]</span>
<span class="hljs-attr">alias</span> = <span class="hljs-string">"tfc"</span>
<span class="hljs-attr">depends</span> = [<span class="hljs-string">"tf.init"</span>]
<span class="hljs-attr">description</span> = <span class="hljs-string">"`terraform console`"</span>
<span class="hljs-attr">raw</span> = <span class="hljs-literal">true</span>
<span class="hljs-attr">run</span> = <span class="hljs-string">"terraform console"</span>
</code></pre>
<p>To run <em>plan</em>, use <code>mise run tf.plan</code>. Mise has aliases, so we can use <code>mise run tfp</code>. The shell also supports aliases 🙂. So why not create an alias with <code>alias mr="mise run"</code> and just use <code>mr tfp</code>.</p>
<blockquote>
<p>If you use VSCode, check out my <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=rgeraskin.mise">VSCode extension</a> to run tasks for your workspace directly from the IDE.</p>
</blockquote>
<p>This is a simple, straightforward flow for Terraform. To see the full example I use daily, you can check <a target="_blank" href="https://gist.github.com/rgeraskin/88c895e393aa8727464401980482f4e0">this gist</a>.</p>
<p>Clean and beautiful syntax, in my opinion.</p>
<blockquote>
<p>Makefile is for C developers, while TOML is for humans 😊</p>
</blockquote>
<p>List all available tasks, and you'll get a nice table with descriptions:</p>
<pre><code class="lang-bash">$ mise tasks

Name          Description                               Source
tf.apply      `terraform apply` with pre-generated pl…  ~/.config/mise/config.toml
tf.console    `terraform console`                       ~/.config/mise/config.toml
tf.init       `terraform init`                          ~/.config/mise/config.toml
tf.lock       `terraform providers lock` with predefi…  ~/.config/mise/config.toml
tf.plan       `terraform plan`                          ~/.config/mise/config.toml
tf.summarize  `tftools summarize` with pre-generated …  ~/.config/mise/config.toml
tf.validate   `terraform validate`                      ~/.config/mise/config.toml
</code></pre>
<h3 id="heading-cicd-pipelines">CI/CD Pipelines</h3>
<p>Tired of writing boilerplate code to install necessary tools for pipelines? With <code>.mise.toml</code>, why not use <code>mise install</code> in pipelines too? Install mise in advance or use the Docker image <code>jdxcode/mise</code>.</p>
<p>There's no need to track tool versions separately for development environments and CI anymore. You no longer need to remember to update tool versions in different places to prevent any issues. Mise serves as a single source of truth.</p>
<p>If you use a similar task flow in CI as you do locally, you can run the same mise tasks there too. If not, you can use <a target="_blank" href="https://mise.jdx.dev/profiles.html">mise profiles</a> to define CI tasks. Bonus: you can easily use it locally to debug pipeline issues.</p>
<p>See the <a target="_blank" href="https://mise.jdx.dev/tips-and-tricks.html#ci-cd">docs</a> for a GitHub Actions example.</p>
<h3 id="heading-best-practices">Best Practices</h3>
<ol>
<li><p>Place <code>.mise.toml</code> in the root of your Git repository to define tools and tasks for the entire repository.</p>
</li>
<li><p>If a repository contains several different projects (like a monorepo), keep common items (like pre-commit tools) in the root config and project-specific items in the project directories.</p>
</li>
<li><p>Use your own <code>~/.config/mise/config.toml</code> for personal automation tasks.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Mise is a unified tool and task management solution that simplifies the process of installing and managing tools for different projects. It supports various backends for tool installation and allows users to define tasks with custom scripts.</p>
<p>Mise can be used in both local development environments and CI/CD pipelines, serving as a single source of truth for tool versions and task flows.</p>
<p>Mise is an excellent alternative to <code>make</code>, <code>tfenv</code>, <code>direnv</code>, and whatever-else-env. <a target="_blank" href="https://mise.jdx.dev">Try it!</a></p>
]]></content:encoded></item><item><title><![CDATA[Mastering Terraform Debugging: Tips and Techniques 🔧]]></title><description><![CDATA[There is a wealth of information available about Terraform and the various tools within the Terraform ecosystem. However, there seems to be a noticeable gap when it comes to resources on debugging techniques specific to Terraform.
Let's address this ...]]></description><link>https://blog.rgeraskin.dev/terraform-expressions-debugging</link><guid isPermaLink="true">https://blog.rgeraskin.dev/terraform-expressions-debugging</guid><category><![CDATA[Devops]]></category><category><![CDATA[Terraform]]></category><dc:creator><![CDATA[Roman Geraskin]]></dc:creator><pubDate>Sun, 02 Jun 2024 12:50:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1717332404288/ec29589f-831b-4e56-bb6c-9daa2f494ee5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There is a wealth of information available about Terraform and the various tools within the Terraform ecosystem. However, there seems to be a noticeable gap when it comes to resources on debugging techniques specific to Terraform.</p>
<p>Let's address this issue and expand the knowledge base to include detailed debugging methods.</p>
<h2 id="heading-terraform-console">Terraform Console</h2>
<p>The built-in Terraform feature can make your life much simpler. It's often been avoided by engineers and is not so popular in blogs.</p>
<p>What can you do with the console? Basically, it can be used not only to show info about a resource in a state.</p>
<ol>
<li><h3 id="heading-drop-state-info">Drop state info</h3>
</li>
</ol>
<p>When you run <code>terraform console</code> in a Terraform project directory, it tries to use the state information to get actual details. It's useful for getting information about created resources or data objects.</p>
<p>But it sometimes slows down the debugging process. For example, if you place your state in an S3 bucket, it fetches it first. Tools like <code>terraform-repl</code> run <code>terraform console</code> under the hood for every command, making the process really slow. Also, <code>console</code> sets a lock on the state while you work with it.</p>
<p>To work with <code>console</code> without an S3 state, you can comment out the Terraform <code>backend</code> section, remove the <code>.terraform</code> folder, and rerun <code>terraform init</code>.</p>
<ol start="2">
<li><h3 id="heading-test-your-expressions">Test your expressions</h3>
</li>
</ol>
<p>Often we have to convert data between various formats to meet the requirements of module interfaces or for other cases. And we usually do it blindly, evaluating complicated expressions in our minds only. That leads to unexpected issues in some corner cases.</p>
<p>Just test it:</p>
<pre><code class="lang-plaintext">&gt; coalesce(["", "b"]...)
b
</code></pre>
<p>Yes, it's a simple example from the tf docs, but you can use <code>console</code> for much more complicated things.</p>
<blockquote>
<p>Did you know that terraform console can be launched not only in a directory with a terraform project? To test expressions, you can start it in any directory.</p>
</blockquote>
<ol start="3">
<li><h3 id="heading-define-your-own-locals-interactively">Define your own <code>locals</code> interactively</h3>
</li>
</ol>
<p>One of the common drawbacks of terraform console is that it doesn't allow you to set your own variables or locals. To compose a complicated expression, it's handy to use intermediate local variables.</p>
<p>To make it possible, you can use terraform console wrappers such as <a target="_blank" href="https://github.com/paololazzari/terraform-repl">terraform-repl</a>. It also adds a tab-completion feature.</p>
<pre><code class="lang-plaintext">&gt; local.a=1
&gt; local.a+1
2
</code></pre>
<p>By using the <code>local</code> command, you can display all local variables.</p>
<p>There is another tool called <a target="_blank" href="https://github.com/ysoftwareab/tfrepl">tfrepl</a> with similar functionality. However, it doesn't have tab-completion, and it can't show all of your defined <code>locals</code>.</p>
<ol start="4">
<li><h3 id="heading-debug-your-modules">Debug your modules</h3>
</li>
</ol>
<p>Modules are the way to keep your tf code <a target="_blank" href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a>. But it's a pain in the neck when you debug and refactor it. Use console tools to make it simpler.</p>
<p>If you have default values for your variables defined, just run <code>terraform console</code> or <code>terraform-repl</code> to debug expressions.</p>
<p>If you have no default values, it's safe to place <code>terraform.tfvars</code> just inside the module folder. Terraform will ignore these vars when this module is used somewhere in a parent project.</p>
<ol start="5">
<li><h3 id="heading-test-your-modules">Test your modules</h3>
</li>
</ol>
<p>The practice of testing Terraform code is not widely used in the infrastructure world. Actually, I've never seen tests in any single company :)</p>
<blockquote>
<p>Personally, I don't see any significant benefit from tests in the way they are meant to be used. See <a target="_blank" href="https://developer.hashicorp.com/terraform/language/tests#example">this</a> example from the official docs: the test checks if the bucket's name is created as expected.</p>
<p>Do we really need it? We just use the variable in the bucket name, so it's obvious that nothing will happen to it later!</p>
</blockquote>
<p>But we can use tests to make sure that we will not break a module's 'interface' sometime in the future while refactoring. Just write a test and do anything with local expressions and variables.</p>
<p>Stupid simple test example:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># main.tftest.hcl</span>
run <span class="hljs-string">"test"</span> {
  <span class="hljs-built_in">command</span> = plan
  assert {
    condition = jsonencode(local.users) == jsonencode({
      <span class="hljs-string">"john"</span> = {
        <span class="hljs-string">"grants"</span> = null
        <span class="hljs-string">"login"</span>  = <span class="hljs-literal">true</span>
        <span class="hljs-string">"member_of"</span> = [
          <span class="hljs-string">"admin"</span>,
        ]
        <span class="hljs-string">"password_enabled"</span> = <span class="hljs-literal">false</span>
      }
    })
    error_message = <span class="hljs-string">"Wrong format of local.users : <span class="hljs-variable">${jsonencode(local.users)}</span>"</span>
  }
}
</code></pre>
<pre><code class="lang-bash">❯ terraform <span class="hljs-built_in">test</span>
main.tftest.hcl... <span class="hljs-keyword">in</span> progress
  run <span class="hljs-string">"test"</span>... pass
main.tftest.hcl... tearing down
main.tftest.hcl... pass

Success! 1 passed, 0 failed.
</code></pre>
<p>Again, if you already have default values in your <code>variables.tf</code>, you are ready to use tests. If you don't, place <code>terraform.tfvars</code> as stated above.</p>
<p>I don't recommend placing variables inside <code>*.tftest.hcl</code> in simple cases because you will not be able to use those variables with <code>console</code> later. Also, avoid complicated tests. Simple tests are easier to support. So, there is a chance that they will be supported in the future at least.</p>
<p>Testing expressions is the way to be confident that you will have the expected value in your resource property.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I shared a few tricks on debugging Terraform expressions. Do you have any to add? Share them in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[Terramate meets Atlantis 🚀]]></title><description><![CDATA[Atlantis is a pull request automation tool that works well with plain Terraform right away. But what if we're already using Terramate to generate Terraform code?

Below, I assume the use of the official Atlantis Helm chart for deployment.

1. Add Ter...]]></description><link>https://blog.rgeraskin.dev/terramate-atlantis</link><guid isPermaLink="true">https://blog.rgeraskin.dev/terramate-atlantis</guid><category><![CDATA[Devops]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[atlantis]]></category><category><![CDATA[Terramate]]></category><dc:creator><![CDATA[Roman Geraskin]]></dc:creator><pubDate>Wed, 03 Apr 2024 20:10:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712174792674/d98e45b2-88d5-42d7-8744-ca64ecd39f3f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://www.runatlantis.io">Atlantis</a> is a pull request automation tool that works well with plain Terraform right away. But what if we're already using <a target="_blank" href="https://github.com/terramate-io/terramate">Terramate</a> to generate Terraform code?</p>
<blockquote>
<p>Below, I assume the use of the official Atlantis Helm chart for deployment.</p>
</blockquote>
<p><strong>1. Add Terramate binary</strong></p>
<p>Add the following to the Atlantis chart's <code>values.yaml</code> to download the binary and mount it to the Atlantis pod. This allows Atlantis to use it for generating Terraform code.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">initContainers:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">args:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">&gt;-
        curl -L https://github.com/terramate-io/terramate/releases/download/v${TERRAMATE_VERSION}/terramate_${TERRAMATE_VERSION}_linux_x86_64.tar.gz | tar xz
</span>    <span class="hljs-attr">command:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">sh</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">-c</span>
    <span class="hljs-attr">env:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">TERRAMATE_VERSION</span>
        <span class="hljs-attr">value:</span> <span class="hljs-number">0.4</span><span class="hljs-number">.5</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">curlimages/curl</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">get-terramate</span>
    <span class="hljs-attr">volumeMounts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/home/curl_user/</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">terramate</span>
    <span class="hljs-attr">workingDir:</span> <span class="hljs-string">/home/curl_user/</span>
  <span class="hljs-attr">extraVolumes:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">terramate</span>
      <span class="hljs-attr">emptyDir:</span> {}
  <span class="hljs-attr">extraVolumeMounts:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">terramate</span>
      <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/usr/local/bin/terramate</span>
      <span class="hljs-attr">subPath:</span> <span class="hljs-string">terramate</span>
      <span class="hljs-attr">readOnly:</span> <span class="hljs-literal">true</span>
</code></pre>
<p><strong>2. Use server-side config</strong></p>
<p>Let's use server-side configuration for our repository. Therefore, add more values to the <code>values.yaml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">environment:</span>
  <span class="hljs-attr">ATLANTIS_REPO_CONFIG:</span> <span class="hljs-string">/etc/atlantis/server-side-config.yaml</span>
<span class="hljs-attr">extraVolumes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">atlantis-server-side-config</span>
    <span class="hljs-attr">configMap:</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">atlantis-server-side-config</span>
<span class="hljs-attr">extraVolumeMounts:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">atlantis-server-side-config</span>
    <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/etc/atlantis/server-side-config.yaml</span>
    <span class="hljs-attr">subPath:</span> <span class="hljs-string">server-side-config.yaml</span>
    <span class="hljs-attr">readOnly:</span> <span class="hljs-literal">true</span>
</code></pre>
<p><strong>3. Deploy server-side config</strong></p>
<p>Now, place the server-side configuration in a configMap and deploy it to the Atlantis namespace:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ConfigMap</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">atlantis-server-side-config</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-comment"># I deploy it with the Atlantis chart, so here is helm templating</span>
    <span class="hljs-attr">app:</span> {{ <span class="hljs-string">.Chart.Name</span> }}
    <span class="hljs-attr">chart:</span> {{ <span class="hljs-string">.Chart.Name</span> }}<span class="hljs-string">-{{</span> <span class="hljs-string">.Chart.AppVersion</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">release:</span> {{ <span class="hljs-string">.Release.Name</span> }}
    <span class="hljs-attr">heritage:</span> {{ <span class="hljs-string">.Release.Service</span> }}
<span class="hljs-attr">data:</span>
  <span class="hljs-attr">server-side-config.yaml:</span> <span class="hljs-string">|
    repos:
    # this steps run before every workflow. 
    # So we will generate tf-code here
    - pre_workflow_hooks:
        # to run 'safeguards' terramate wants origin master 
        #   and some previous commits
        - run: |
            git config --add remote.origin.fetch +refs/heads/master:refs/remotes/origin/master
            git fetch --depth=1 origin master
            git fetch --depth=2
          description: Fetch origin master branch
</span>
        <span class="hljs-comment"># this step is optional</span>
        <span class="hljs-comment"># I use .tm-run-order later to made Atlantis </span>
        <span class="hljs-comment">#   to run plan/apply in a specified order</span>
        <span class="hljs-comment"># Also, I generate an atlantis.yaml config </span>
        <span class="hljs-comment">#   with terramate too, so I exclude it from run order: </span>
        <span class="hljs-comment">#   it's just a yaml, not tf-code</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">terramate</span> <span class="hljs-string">list</span> <span class="hljs-string">--run-order</span> <span class="hljs-string">-c</span> <span class="hljs-string">--no-tags</span> <span class="hljs-string">atlantis</span> <span class="hljs-string">-B</span> <span class="hljs-string">origin/master</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">.tm-run-order</span>
          <span class="hljs-attr">description:</span> <span class="hljs-string">Get</span> <span class="hljs-string">changed</span> <span class="hljs-string">stacks</span>

        <span class="hljs-comment"># And the obvious final step =)</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">run:</span> <span class="hljs-string">terramate</span> <span class="hljs-string">generate</span>
          <span class="hljs-attr">description:</span> <span class="hljs-string">Generating</span> <span class="hljs-string">tf</span> <span class="hljs-string">code</span>
      <span class="hljs-comment"># some other options, offtopic</span>
      <span class="hljs-attr">id:</span> <span class="hljs-string">github.com/XXX/YYY</span>
      <span class="hljs-attr">apply_requirements:</span> [<span class="hljs-string">mergeable</span>, <span class="hljs-string">approved</span>, <span class="hljs-string">undiverged</span>]
      <span class="hljs-attr">delete_source_branch_on_merge:</span> <span class="hljs-literal">true</span>
</code></pre>
<p><strong>4. (Optional) Generate</strong> <code>atlantis.yaml</code> with Terramate too</p>
<p>Add this 'stack' to your Terramate repository:</p>
<pre><code class="lang-ini">stack {
  <span class="hljs-attr">name</span>        = <span class="hljs-string">"atlantis"</span>
  <span class="hljs-attr">description</span> = <span class="hljs-string">"atlantis"</span>
  <span class="hljs-attr">id</span>          = <span class="hljs-string">"your-uniq-id"</span>

  <span class="hljs-attr">tags</span> = [
    <span class="hljs-string">"atlantis"</span>
  ]
}

generate_file "atlantis.yaml" {
  <span class="hljs-attr">condition</span> = (
    <span class="hljs-comment"># I place it in the root repo dir so I want to avoid </span>
    <span class="hljs-comment">#   file generation with child stacks, only with</span>
    <span class="hljs-comment">#   atlantis stack and with not empty .tm-run-order</span>
    <span class="hljs-attr">terramate.stack.name</span> == <span class="hljs-string">"atlantis"</span> &amp;&amp; tm_length(let.run_order) &gt; <span class="hljs-number">0</span>
  )

  lets {
    <span class="hljs-attr">run_order</span> = tm_try(
      tm_compact(tm_split("\n", tm_trim(tm_file(".tm-run-order"), "\n"))),
    <span class="hljs-section">[]</span>)
    <span class="hljs-comment"># dirty magic to fill config options by my folder structure</span>
    <span class="hljs-attr">projects</span> = [for x in let.run_order : {
      name = tm_replace(x, <span class="hljs-string">"/^terraform/(projectX/)?/"</span>, <span class="hljs-string">""</span>)
      dir  = x
      autoplan = {
        <span class="hljs-comment"># I want to trigger Atlantis for every project</span>
        <span class="hljs-comment">#   because I already know that every project </span>
        <span class="hljs-comment">#   here is modified</span>
        when_modified = (
          tm_substr(x, <span class="hljs-number">0</span>, <span class="hljs-number">14</span>) == <span class="hljs-string">"terraform/projectX/"</span> ? [<span class="hljs-string">"../../../**/*"</span>] : [<span class="hljs-string">"../**/*"</span>]
        )
        enabled = <span class="hljs-literal">false</span>
      }
    }]
    <span class="hljs-attr">config</span> = {
      <span class="hljs-attr">version</span>        = <span class="hljs-number">3</span>
      <span class="hljs-attr">automerge</span>      = <span class="hljs-literal">true</span>
      <span class="hljs-attr">parallel_plan</span>  = <span class="hljs-literal">false</span>
      <span class="hljs-attr">parallel_apply</span> = <span class="hljs-literal">false</span>
      <span class="hljs-attr">projects</span>       = let.projects
    }
  }

  <span class="hljs-attr">content</span> = tm_yamlencode(let.config)
}
</code></pre>
<p>Done! Now, for every PR, Atlantis will:</p>
<ol>
<li><p>Run pre-workflow hooks to generate Terraform code.</p>
</li>
<li><p>(Optional) Generate its own configuration to run projects in the desired order.</p>
</li>
</ol>
<p>If you're looking for more tips and useful info, definitely swing by my blog post <a target="_blank" href="https://rgeraskin.hashnode.dev/terramate-zsh">10 Useful Aliases and Functions for Terramate and Zsh Users</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Terramate ❤️ Zsh]]></title><description><![CDATA[Terramate is an amazing tool to enhance your Terraform experience. However, there is a way to make it even more handy for everyday use.
Solve annoying things
1. Too long to type
Obvious one: terraform and terramate commands are too long to type :)
Us...]]></description><link>https://blog.rgeraskin.dev/terramate-zsh</link><guid isPermaLink="true">https://blog.rgeraskin.dev/terramate-zsh</guid><category><![CDATA[Devops]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[zsh]]></category><category><![CDATA[Terramate]]></category><dc:creator><![CDATA[Roman Geraskin]]></dc:creator><pubDate>Tue, 26 Mar 2024 08:07:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1711272562002/bf8a1b01-36a1-4610-afd9-c3e2499903aa.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://github.com/terramate-io/terramate">Terramate</a> is an amazing tool to enhance your Terraform experience. However, there is a way to make it even more handy for everyday use.</p>
<h2 id="heading-solve-annoying-things">Solve annoying things</h2>
<h4 id="heading-1-too-long-to-type">1. Too long to type</h4>
<p>Obvious one: <code>terraform</code> and <code>terramate</code> commands are <strong>too long to type</strong> :)</p>
<p>Use aliases:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># ~/.zshrc</span>
<span class="hljs-built_in">alias</span> tf=<span class="hljs-string">"terraform "</span>
<span class="hljs-built_in">alias</span> tm=<span class="hljs-string">"terramate "</span>
<span class="hljs-built_in">alias</span> tmg=<span class="hljs-string">"terramate generate "</span>
</code></pre>
<p>You still have to type <code>terraform</code> after <code>tm run</code> because tm knows nothing about your aliases. But it will be solved below too.</p>
<h4 id="heading-2-command-chaining">2. Command chaining</h4>
<p>Often, to execute a Terraform command, you need to run a preceding command. For example, before <code>tf init</code>, you should run <code>tm generate</code>. Similarly, before <code>plan</code>, you might need to run <code>init</code>, and so on.</p>
<p>You could use Makefiles or the <code>terramate script run</code> feature that comes with Terramate to handle command chaining. However, this only addresses the issue of chaining commands. Plus, you'd have to add these configurations to every repository you work with.</p>
<p>And it's still too much typing. You'll likely resort to using aliases anyway. So, why not start with shell configuration right from the start?</p>
<p>Zsh functions can help:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># ~/.zshrc</span>
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">tmi</span></span> () {
  terramate generate &amp;&amp; \
  terramate run terraform init <span class="hljs-variable">$@</span>
}
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">tmp</span></span> () {
  tmi <span class="hljs-variable">$1</span> &amp;&amp; \
  terramate run terraform plan -out=tfplan <span class="hljs-variable">$@</span>
}
</code></pre>
<h4 id="heading-3-tags-for-stacks">3. Tags for stacks</h4>
<p>Tags are a fantastic feature of Terramate, especially if you're managing many stacks. They allow all <code>tm run</code> commands to be executed in stack directories selected by tags. However, their usability could be better—you always need to type them somewhere in the middle of a <code>tm</code> command.</p>
<p>For example, if you want to check the <code>plan</code> for the dev environment and then for the stage environment:</p>
<pre><code class="lang-bash">terramate run --tags=mngt:dev terraform plan
terramate run --tags=mngt:stage terraform plan
</code></pre>
<p>To create the last command, you can use a sequence of keys like this before typing <code>stage</code>: up, option+left, option+left, left, esc, backspace.</p>
<p>Make functions smarter:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># ~/.zshrc</span>
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">tmi</span></span> () {
  terramate generate &amp;&amp; \
  terramate run --tags=<span class="hljs-variable">$1</span> terraform init <span class="hljs-variable">${@:2}</span>
}
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">tmp</span></span> () {
  tmi <span class="hljs-variable">$1</span> &amp;&amp; \
  terramate run --tags=<span class="hljs-variable">$1</span> terraform plan -out=tfplan <span class="hljs-variable">${@:2}</span>
}
</code></pre>
<p>Now, you have fewer steps before you start typing <code>stage</code>: up, esc, backspace.</p>
<p>Additionally, you can specify <code>plan</code> options as the second or later arguments thanks to <code>${@:2}</code> in the function.</p>
<h4 id="heading-4-not-sure-about-what-youve-typed">4. Not sure about what you've typed?</h4>
<p>Print the command before executing it:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># ~/.zshrc</span>
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">tmr</span></span> () {
  <span class="hljs-built_in">print</span> -z <span class="hljs-string">"terramate run --tags=<span class="hljs-variable">$@</span> "</span>
}
</code></pre>
<p>So <code>tmr dev:asd ls</code> + enter will lead to <code>terramate run --tags=dev:asd ls</code> in the command line.</p>
<h2 id="heading-lets-put-it-all-together">Let's put it all together</h2>
<pre><code class="lang-bash"><span class="hljs-comment"># ~/.zshrc</span>
<span class="hljs-built_in">alias</span> tf=<span class="hljs-string">"terraform "</span>
<span class="hljs-built_in">alias</span> tm=<span class="hljs-string">"terramate "</span>
<span class="hljs-built_in">alias</span> tmg=<span class="hljs-string">"terramate generate "</span>
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">tmi</span></span> () {
  terramate generate &amp;&amp; \
  terramate run --tags=<span class="hljs-variable">$1</span> terraform init <span class="hljs-variable">${@:2}</span>
}
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">tmv</span></span> () {
  tmi <span class="hljs-variable">$1</span> &amp;&amp; \
  terramate run --tags=<span class="hljs-variable">$1</span> terraform validate <span class="hljs-variable">${@:2}</span>
}
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">tmp</span></span> () {
  tmi <span class="hljs-variable">$1</span> &amp;&amp; \
  terramate run --tags=<span class="hljs-variable">$1</span> terraform plan -out=tfplan <span class="hljs-variable">${@:2}</span>
}
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">tma</span></span> () {
  <span class="hljs-built_in">print</span> -z <span class="hljs-string">"terramate run --tags=<span class="hljs-variable">$1</span> terraform apply tfplan <span class="hljs-variable">${@:2}</span>"</span>
}
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">tmpl</span></span> () {
  tmi <span class="hljs-variable">$1</span> &amp;&amp; \
  terramate run --tags=<span class="hljs-variable">$1</span> terraform providers lock -platform=linux_amd64 -platform=darwin_amd64 -platform=darwin_arm64 <span class="hljs-variable">${@:2}</span>
}
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">tmc</span></span> () {
  tmi <span class="hljs-variable">$1</span> &amp;&amp; \
  terramate run --tags=<span class="hljs-variable">$1</span> terraform console <span class="hljs-variable">${@:2}</span>
}
<span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">tmr</span></span> () {
  <span class="hljs-built_in">print</span> -z <span class="hljs-string">"terramate run --tags=<span class="hljs-variable">$@</span> "</span>
}
</code></pre>
<h2 id="heading-usage">Usage</h2>
<h3 id="heading-examples">Examples</h3>
<pre><code class="lang-bash">tmi admin        <span class="hljs-comment"># will run `terramate run --tags=admin terraform init`</span>
tmp dev:infra    <span class="hljs-comment"># will run `terramate run --tags=dev:infra terraform plan -out=tfplan`</span>
tmr admin        <span class="hljs-comment"># will print `terramate run --tags=admin` in prompt so you can run any command in the admin stack dir</span>
tmr admin ls -la <span class="hljs-comment"># will print `terramate run --tags=admin ls -la` in prompt, press 'enter' to execute `ls -la` in the admin stack dir or append more args</span>
</code></pre>
<h3 id="heading-shortcuts-description">Shortcuts description</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Command</td><td>Action</td></tr>
</thead>
<tbody>
<tr>
<td><code>tf</code></td><td><code>terraform</code></td></tr>
<tr>
<td><code>tm</code></td><td><code>terramate</code></td></tr>
<tr>
<td><code>tmg</code></td><td><code>terramate generate</code></td></tr>
<tr>
<td><code>tmi &lt;TAGS&gt;</code> [ARGS]</td><td><code>tm generate</code> =&gt; <code>tf init [ARGS]</code></td></tr>
<tr>
<td><code>tmv &lt;TAGS&gt;</code> [ARGS]</td><td><code>tm generate</code> =&gt; <code>tf init</code> =&gt; <code>tf validate [ARGS]</code></td></tr>
<tr>
<td><code>tmp &lt;TAGS&gt;</code> [ARGS]</td><td><code>tm generate</code> =&gt; <code>tf init</code> =&gt; <code>tf plan -out=tfplan [ARGS]</code></td></tr>
<tr>
<td><code>tma &lt;TAGS&gt;</code> [ARGS]</td><td><code>tf apply tfplan [ARGS]</code></td></tr>
<tr>
<td><code>tmpl &lt;TAGS&gt;</code> [ARGS]</td><td><code>tm generate</code> =&gt; <code>tf init</code> =&gt; <code>tf providers lock -platform=linux_amd64 -platform=darwin_amd64 -platform=darwin_arm64 [ARGS]</code></td></tr>
<tr>
<td><code>tml &lt;TAGS&gt;</code> [ARGS]</td><td><code>tm generate</code> =&gt; <code>tf init</code> =&gt; <code>tf console [ARGS]</code></td></tr>
<tr>
<td><code>tmc &lt;TAGS&gt;</code> [ARGS]</td><td><code>aws sso login [ARGS]</code></td></tr>
<tr>
<td><code>tmr &lt;TAGS&gt; &lt;CMD&gt; [ARGS]</code></td><td><code>terramate run --tags=&lt;TAGS&gt; &lt;CMD&gt; [ARGS]</code></td></tr>
</tbody>
</table>
</div><h2 id="heading-bonus">Bonus</h2>
<p>Use different environment variable values for different stacks.</p>
<p>For example, if stacks use different AWS accounts, place the account name in the stack's globals and add it to your repository's root tm-file.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># repo root terramate.tm</span>
terramate {
  config {
    run {
      env {
        AWS_PROFILE = <span class="hljs-string">"<span class="hljs-variable">${global.aws_profile}</span>"</span>
      }
    }
  }
}
</code></pre>
<p><code>tmr aws aws sso login</code> FTW 🤟</p>
]]></content:encoded></item><item><title><![CDATA[Get a Specific apiVersion Manifest From K8S]]></title><description><![CDATA[TL;DR

To get a manifest from k8s in any supported API version you can use an extended kubectl get notation like kubectl get deployments.v1beta1.extensions mydeploy -o yaml

Briefly and with examples
Everybody knows how to get a resource manifest fro...]]></description><link>https://blog.rgeraskin.dev/get-a-specific-apiversion-manifest-from-k8s</link><guid isPermaLink="true">https://blog.rgeraskin.dev/get-a-specific-apiversion-manifest-from-k8s</guid><category><![CDATA[Devops]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[k8s]]></category><dc:creator><![CDATA[Roman Geraskin]]></dc:creator><pubDate>Tue, 19 Mar 2024 16:47:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710882241617/465b34ab-c33d-48d6-8688-ef8300773072.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr">TL;DR</h2>
<blockquote>
<p>To get a manifest from k8s in any supported API version you can use an extended <code>kubectl get</code> notation like <code>kubectl get deployments.v1beta1.extensions mydeploy -o yaml</code></p>
</blockquote>
<h2 id="heading-briefly-and-with-examples">Briefly and with examples</h2>
<p>Everybody knows how to get a resource manifest from Kubernetes. But do you know that you can put a manifest with one <code>apiVersion</code> set and get the same resource manifest with another <code>apiVersion</code> back?</p>
<h3 id="heading-imagine">Imagine</h3>
<ol>
<li><p>You have a <code>deployment</code> in a cluster, that uses <code>api-version extensions/v1beta1</code> from a git repo.</p>
</li>
<li><p>You've updated the cluster (1.15 =&gt; 1.16). Old deployment works fine, but you can't deploy nothing new with the old manifest because in k8s 1.16 <code>api-version extensions/v1beta1</code> is absent.</p>
</li>
<li><p>You have two options:</p>
<ol>
<li><p>Rewrite it manually or</p>
</li>
<li><p>Get it from the cluster in updated spec format (<code>apps/v1</code>)</p>
</li>
</ol>
</li>
</ol>
<p>To get manifest from k8s you can:</p>
<blockquote>
<p>below there is an example for v1.15, where extensions/v1beta1 is not removed yet</p>
</blockquote>
<pre><code class="lang-bash"><span class="hljs-comment"># from extensions</span>
kubectl get deployments.extensions ext -o yaml
<span class="hljs-comment"># from extensions, but for v1beta1</span>
kubectl get deployments.v1beta1.extensions ext -o yaml
<span class="hljs-comment"># from apps</span>
kubectl get deployments.apps ext -o yaml
<span class="hljs-comment"># from apps, but v1</span>
kubectl get deployments.v1.apps ext -o yaml
</code></pre>
<p>Notice that if you do just <code>kubectl get deployments ext -o yaml</code>, you will get a manifest from <code>extensions/v1beta1</code> nevertheless you've even applied <code>apps/v1</code> before. Details <a target="_blank" href="https://github.com/kubernetes/kubernetes/issues/58131#issuecomment-356823588">here</a>.</p>
<h2 id="heading-prove-it">Prove it</h2>
<ol>
<li><p>Start 1.15</p>
<pre><code class="lang-bash"> minikube start --kubernetes-version=v1.15.12
</code></pre>
</li>
<li><p>Make 2 manifests for <code>deployments</code> with different versions. Note the absence of <code>spec.selector</code> in <code>extensions/v1beta1</code></p>
<pre><code class="lang-yaml"> <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
 <span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
 <span class="hljs-attr">metadata:</span>
   <span class="hljs-attr">creationTimestamp:</span> <span class="hljs-literal">null</span>
   <span class="hljs-attr">labels:</span>
     <span class="hljs-attr">run:</span> <span class="hljs-string">apps</span>
   <span class="hljs-attr">name:</span> <span class="hljs-string">apps</span>
 <span class="hljs-attr">spec:</span>
   <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
   <span class="hljs-attr">selector:</span>
     <span class="hljs-attr">matchLabels:</span>
       <span class="hljs-attr">run:</span> <span class="hljs-string">apps</span>
   <span class="hljs-attr">template:</span>
     <span class="hljs-attr">metadata:</span>
       <span class="hljs-attr">creationTimestamp:</span> <span class="hljs-literal">null</span>
       <span class="hljs-attr">labels:</span>
         <span class="hljs-attr">run:</span> <span class="hljs-string">apps</span>
     <span class="hljs-attr">spec:</span>
       <span class="hljs-attr">containers:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-attr">image:</span> <span class="hljs-string">nginx</span>
         <span class="hljs-attr">name:</span> <span class="hljs-string">apps</span>
 <span class="hljs-string">---</span>
 <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">extensions/v1beta1</span>
 <span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
 <span class="hljs-attr">metadata:</span>
   <span class="hljs-attr">labels:</span>
     <span class="hljs-attr">run:</span> <span class="hljs-string">ext</span>
   <span class="hljs-attr">name:</span> <span class="hljs-string">ext</span>
 <span class="hljs-attr">spec:</span>
   <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
   <span class="hljs-attr">template:</span>
     <span class="hljs-attr">metadata:</span>
       <span class="hljs-attr">creationTimestamp:</span> <span class="hljs-literal">null</span>
       <span class="hljs-attr">labels:</span>
         <span class="hljs-attr">run:</span> <span class="hljs-string">ext</span>
     <span class="hljs-attr">spec:</span>
       <span class="hljs-attr">containers:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-attr">image:</span> <span class="hljs-string">nginx</span>
         <span class="hljs-attr">name:</span> <span class="hljs-string">ext</span>
</code></pre>
</li>
<li><p>Apply</p>
<pre><code class="lang-bash"> ❯ minikube kubectl -- apply -f .
 deployment.apps/apps created
 deployment.extensions/ext created
</code></pre>
</li>
<li><p>Check that resources <code>apps</code> and <code>ext</code> are in <code>extensions/v1beta1</code> and in <code>apps/v1</code> too</p>
<ul>
<li>The hard way - curl:</li>
</ul>
</li>
</ol>
<pre><code class="lang-bash">    ❯ minikube kubectl -- proxy
    Starting to serve on 127.0.0.1:8001
    ❯ curl -s 127.0.0.1:8001/apis/extensions/v1beta1/namespaces/default/deployments/apps | yq . --yaml-output
    kind: Deployment
    apiVersion: extensions/v1beta1
    <span class="hljs-comment"># ...</span>
    ❯ curl -s 127.0.0.1:8001/apis/extensions/v1beta1/namespaces/default/deployments/ext | yq . --yaml-output
    kind: Deployment
    apiVersion: extensions/v1beta1
    <span class="hljs-comment"># ...</span>
    ❯ curl -s 127.0.0.1:8001/apis/apps/v1/namespaces/default/deployments/apps | yq . --yaml-output
    kind: Deployment
    apiVersion: apps/v1
    <span class="hljs-comment"># ...</span>
    ❯ curl -s 127.0.0.1:8001/apis/apps/v1/namespaces/default/deployments/ext | yq . --yaml-output
    kind: Deployment
    apiVersion: apps/v1
    <span class="hljs-comment"># ...</span>
</code></pre>
<ul>
<li>Or kubectl:</li>
</ul>
<pre><code class="lang-bash">    <span class="hljs-comment"># from extensions</span>
    kubectl get deployments.extensions ext -o yaml
    <span class="hljs-comment"># from extensions, but v1beta1</span>
    kubectl get deployments.v1beta1.extensions ext -o yaml
    <span class="hljs-comment"># from apps</span>
    kubectl get deployments.apps ext -o yaml
    <span class="hljs-comment"># from apps, but v1</span>
    kubectl get deployments.v1.apps ext -o yaml
</code></pre>
<h2 id="heading-bonus-kubectl-explain">Bonus: kubectl explain</h2>
<p>If you do <code>kubectl explain deployment</code> than (surprise!) you'll get a description for <code>extensions/v1beta1</code>. Because <code>kubectl explain</code> <a target="_blank" href="https://github.com/kubernetes/kubernetes/issues/73062">works the same way</a>, just like <code>kubectl get</code>:</p>
<p>If you want a specific version, use an <code>--api-version</code> flag :</p>
<pre><code class="lang-bash">minikube kubectl -- explain deployment.spec --api-version apps/v1
minikube kubectl -- explain deployment.spec --api-version extensions/v1beta1
</code></pre>
]]></content:encoded></item></channel></rss>