<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jakob Gillich</title>
    <description>The latest articles on DEV Community by Jakob Gillich (@jgillich).</description>
    <link>https://dev.to/jgillich</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F939740%2F28ad97ed-b5bf-44a1-b236-0906b1011f2b.jpeg</url>
      <title>DEV Community: Jakob Gillich</title>
      <link>https://dev.to/jgillich</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jgillich"/>
    <language>en</language>
    <item>
      <title>Why Kubernetes Is So Complex</title>
      <dc:creator>Jakob Gillich</dc:creator>
      <pubDate>Mon, 17 Oct 2022 19:20:17 +0000</pubDate>
      <link>https://dev.to/jgillich/why-kubernetes-is-so-complex-247l</link>
      <guid>https://dev.to/jgillich/why-kubernetes-is-so-complex-247l</guid>
      <description>&lt;p&gt;The container wars are over, and Kubernetes won. Docker the company has given up on Swarm, and is refocusing on developer tools, and Mesos development has slowed down to a crawl.&lt;/p&gt;

&lt;p&gt;Kubernetes is embraced by large companies with dedicated infrastructure teams, but the hobbyists and small businesses are being left behind. Unnecessary complexity is probably the commonly mentioned reason for not adopting Kubernetes.&lt;/p&gt;

&lt;p&gt;But why is Kubernetes so complex? And is the complexity actually unnecessary? Let's look at how Kubernetes works behind the scenes, and why the complexity may be a tradeoff worth making.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes is modular
&lt;/h2&gt;

&lt;p&gt;Beginners are often overwhelmed by Kubernetes. &lt;code&gt;CRD&lt;/code&gt;? &lt;code&gt;CNI&lt;/code&gt;? &lt;code&gt;CSI&lt;/code&gt;? &lt;code&gt;etcd&lt;/code&gt;? &lt;code&gt;GroupVersionKind&lt;/code&gt;? Why is deploying containers so complicated?&lt;/p&gt;

&lt;p&gt;At the core of Kubernetes is the API server, which is a CRUD API, meaning we can create, read, update and delete resources. The key to understanding the API server is the &lt;code&gt;CustomResourceDefinition&lt;/code&gt;. They tell the API server which resources exist and what fields they have. Core resources are technically not CRDs, but their behavior is otherwise identical.&lt;/p&gt;

&lt;p&gt;When we send a resource manifest to the API server, the following happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It validates all fields against a stored &lt;code&gt;CustomResourceDefinition&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It calls registered webhooks ("admission controllers"). They perform additional validations that are specific to the resource.&lt;/li&gt;
&lt;li&gt;It stores the resource in its storage backend (typically etcd).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At this stage, we haven't actually done anything yet. No images are being pulled, no containers are being deployed. How does that happen? We left out one feature of the API server: The ability to watch for changes.&lt;/p&gt;

&lt;p&gt;Nearly everything in Kubernetes happens through programs that watch the API server for changes, we call them &lt;strong&gt;controllers&lt;/strong&gt;. When a resource changes, a controller watching it will run and perform an action. We call that reconciliation, turning the desired state into the actual state. When we create a &lt;code&gt;Deployment&lt;/code&gt;, a controller uses the information provided in the deployment manifest to find one or more nodes, and creates &lt;code&gt;Pod&lt;/code&gt; resources for the nodes. The node agent or &lt;em&gt;kubelet&lt;/em&gt; watches the pod resource and deploys the container(s). When the pods are deployed (or failed to deploy), the kubelet returns its result by writing to the pod's status field.&lt;/p&gt;

&lt;p&gt;This mode of operation is at the very core of Kubernetes, resources are detached from their implementation. The CSI (Container Storage Interface) is an excellent example of the strengths of this approach. Where Docker volumes are stored locally, Kubernetes' flexibility allows it to support any kind of volume: local, Ceph, NFS or provider-specific volume implementations.&lt;/p&gt;

&lt;p&gt;It is important that we don't do the same thing twice however - if we try to provision a local and a NFS volume to the same mount point, we'll run into problems. Most resources are reconciled by a single controller, but for storage, you sometimes have more. The storage class is a string field on volume claims that identified the responsible controller. Notably, this is not a feature of the API server, we depend on the controllers to respect it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes is eventually consistent
&lt;/h2&gt;

&lt;p&gt;Anyone using Kubernetes will have come across one thing: &lt;code&gt;CrashLoopBackOff&lt;/code&gt;. But why does it occur?&lt;/p&gt;

&lt;p&gt;Kubernetes is a big system with many components, and some things must happen in a certain order. However, there is no attempt to synchronize or order operations, instead, &lt;strong&gt;failed operations are retried&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a pod requires a volume, it will have to wait for the CSI driver to create and mount it. How long does that take? Usually a couple of seconds, but if there's an error with the CSI driver, it may not happen for hours or days. So when a pod is created, the kubelet will check if the volume is available. If it is not, it will wait a few seconds and try again. But, as to not waste system resources, it will wait a little longer every time. This is called error back-off, and it's built into all controllers.&lt;/p&gt;

&lt;p&gt;All the above has one major implication: It's difficult to figure out where things went wrong. When &lt;code&gt;docker run&lt;/code&gt; fails, you will be told why. When you create a deployment, your initial feedback will be nothing at all. Only the status of the deployment and pod will tell you more. And even they may not point directly at the problem.&lt;/p&gt;

&lt;p&gt;With some experience and a user interface like &lt;a href="https://k8slens.dev/"&gt;Lens&lt;/a&gt;, debugging becomes easier. And there are great monitoring solutions for production use. But this is still a big hurdle for beginners taking their first steps with Kubernetes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Kubernetes is declarative
&lt;/h2&gt;

&lt;p&gt;Much has been said about the advantages of declarative configuration, infrastructure as code or gitops. If you believe in them, Kubernetes is for you. Kubernetes is the natural progression of Terraform into an always running API. Only Kubernetes does not require fixed ordering and scales infinitely better.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fluxcd.io/"&gt;Flux&lt;/a&gt; lets us use a git repository as the single source of truth for our cluster, but for dynamic resources we will want to use the API server instead. We use both of these approaches; Flux sets up our cluster, and applications are created using our own &lt;code&gt;Application&lt;/code&gt; resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's write a controller
&lt;/h2&gt;

&lt;p&gt;What does a controller actually look like? For Cloudplane, we have a resource to request SMTP credentials for our applications.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudplane.org/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SMTPCredentials&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-app-smtp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we submit this resources to the API server, a controller will be watching this resource type and use it to generate a secret. By convention, we emit the secret with the same name as the SMTPCredentials resource.&lt;/p&gt;

&lt;p&gt;For development, we have deployed a instance of &lt;a href="https://github.com/mailhog/MailHog"&gt;MailHog&lt;/a&gt; to our cluster, a simple mail server that catches all mails without forwarding them. It allows any user/password combination. Here is what our mailhog controller looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SMTPCredentialsReconciler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;corev1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TypeMeta&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metav1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TypeMeta&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;APIVersion&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;corev1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SchemeGroupVersion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;Kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Secret"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;ObjectMeta&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metav1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ObjectMeta&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;Namespace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;][]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"host"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mailhog.dev"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="s"&gt;"port"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1025"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="s"&gt;"username"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"foo@example.com"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="s"&gt;"password"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"smtp-password"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetControllerReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Apply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ForceOwnership&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FieldOwner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"smtpcredentials-controller"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we already know the hostname of our mailhog instance, and we don't need to generate any username/password, all values are static. Setting an owner reference on the secret means it will be deleted by the API server when the &lt;code&gt;SMTPCredentials&lt;/code&gt; resource is deleted, and finally &lt;code&gt;Patch&lt;/code&gt; creates or updates the secret. If this function returned an error, it would be automatically retried and we would see the error back-off behavior described earlier.&lt;/p&gt;

&lt;p&gt;In production, we replace this controller with our implementation for SES.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes works
&lt;/h2&gt;

&lt;p&gt;Kubernetes was created by engineers at Google who had been running a similar system for years. They knew exactly what they were doing. The design of Kubernetes is very intentional.&lt;/p&gt;

&lt;p&gt;There are aspects of Kubernetes that are unfinished (StatefulSets) or need to be reworked (Ingress-&amp;gt;Gateway API), it is not a perfect system. But it probably is the best system we currently have.&lt;/p&gt;

&lt;p&gt;The good news is that there are many efforts to make Kubernetes easier to use. &lt;a href="https://acorn.io/"&gt;Acorn&lt;/a&gt; aims to simplify application packaging and deployment. And services like &lt;a href="https://render.com/"&gt;Render&lt;/a&gt; and our very own &lt;a href="https://cloudplane.org/"&gt;Cloudplane&lt;/a&gt; use Kubernetes under the hood to deliver a user-friendly solution that requires zero Kubernetes knowledge.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://cloudplane.org/blog/why-kubernetes-is-so-complex"&gt;https://cloudplane.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>docker</category>
    </item>
    <item>
      <title>Apko: A Better Way To Build Containers?</title>
      <dc:creator>Jakob Gillich</dc:creator>
      <pubDate>Thu, 13 Oct 2022 18:15:46 +0000</pubDate>
      <link>https://dev.to/jgillich/apko-a-better-way-to-build-containers-4fn</link>
      <guid>https://dev.to/jgillich/apko-a-better-way-to-build-containers-4fn</guid>
      <description>&lt;p&gt;Chainguard &lt;a href="https://www.chainguard.dev/unchained/introducing-wolfi-the-first-linux-un-distro"&gt;recently announced Wolfi&lt;/a&gt;, a set of container images that are built directly from Alpine packages and not Dockerfiles.&lt;/p&gt;

&lt;p&gt;Ever since Docker released in 2013, we have been using Dockerfiles to build containers. They work well, but there are some problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They make it hard to share components between images, a single base image is the only option. Code reuse is difficult.&lt;/li&gt;
&lt;li&gt;Some components like package managers are not required to run a container, but they are included for the convenience of the builder. There is no separation between build and runtime dependencies (although multi-stage builds can mostly achieve this).&lt;/li&gt;
&lt;li&gt;Docker does not support reproducible builds (but kaniko does).&lt;/li&gt;
&lt;li&gt;Layers serve no purpose for production builds, most images use one giant and messy RUN statement that does everything. That however slows down rebuilds during development.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's look at the solutions provided by Chainguard that try to address these (and more) problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  melange
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/chainguard-dev/melange"&gt;Melange&lt;/a&gt; is a builder for Alpine packages. It uses pipelines similar to common CI/CD services, and it builds for multiple architectures by default. Here is a simplified example of a package build for the forum software NodeBB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nodebb&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.5.3&lt;/span&gt;
  &lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nodejs&lt;/span&gt;

&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;alpine-baselayout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ca-certificates-bundle&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nodejs&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git&lt;/span&gt;

&lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fetch&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/NodeBB/NodeBB/archive/refs/tags/v${{package.version}}.tar.gz&lt;/span&gt;
      &lt;span class="na"&gt;expected-sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;92e390d7cda190e7f098833cbbbf03fbe1c50f25653656ad589ae97dc18a7684&lt;/span&gt;
      &lt;span class="na"&gt;strip-components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;runs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;mkdir -p "${{targets.destdir}}/usr/share/nodebb"&lt;/span&gt;
      &lt;span class="s"&gt;cd NodeBB-${{package.version}}&lt;/span&gt;
      &lt;span class="s"&gt;cp install/package.json .&lt;/span&gt;
      &lt;span class="s"&gt;npm install --omit=dev&lt;/span&gt;
      &lt;span class="s"&gt;cp -a ./. "${{targets.destdir}}/usr/share/nodebb"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the environment section, we set up a build environment. Then we call the built-in &lt;a href="https://github.com/chainguard-dev/melange/blob/main/pipelines/fetch.yaml"&gt;fetch&lt;/a&gt; pipeline to fetch the source code bundle, verify the hash and extract it. There are a few more built-in pipelines, but we can also write our own.&lt;/p&gt;

&lt;p&gt;Next, we run a short script to install dependencies, run a build and copy the resulting files into a destination directory. Everything under &lt;code&gt;targets.destdir&lt;/code&gt; is copied into the package, which will only depend on nodejs, as we do not need npm or git at runtime.&lt;/p&gt;

&lt;p&gt;Now, we can use melange to build an apk package. This isn't a container yet, but for that, we use the next tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  apko
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/chainguard-dev/apko"&gt;apko&lt;/a&gt; takes apk packages and builds them into OCI images (aka Docker images). Sounds quite simple, because it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repositories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://dl-cdn.alpinelinux.org/alpine/edge/main&lt;/span&gt;
  &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;alpine-baselayout&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nodebb&lt;/span&gt;

&lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/share/nodebb/nodebb start&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that there is no base image being used, instead, we get exactly what's in the packages we list. &lt;code&gt;alpine-baselayout&lt;/code&gt; adds busybox and the standard directory structure.&lt;/p&gt;

&lt;p&gt;We can include other apko definitions as a base or create users and groups and set up file system permissions, but that's about it. apko lacks most capabilities of traditional OCI image builders as it's meant to be combined with melange, adding a files and folders directly is not supported. But what it does have is native support for s6 in case we want to run multiple services.&lt;/p&gt;

&lt;p&gt;apko is quite fast, all it does is extract apk packages into OCI images and it doesn't run scripts. This is what enables fast rebuilds of images after an individual package is updated, and also makes them reproducible.&lt;/p&gt;

&lt;p&gt;Using both tools, did we solve the problems we laid out before? Let's see.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;melange's pipelines enable modularity at the build level, and apko lets us combine multiple packages into one image. Code reuse is easy.&lt;/li&gt;
&lt;li&gt;Images built with apko contain exactly the packages we want, and nothing else. Images are smaller than comparable Docker images using the Alpine base image.&lt;/li&gt;
&lt;li&gt;Both melange and apko provide fully reproducible builds.&lt;/li&gt;
&lt;li&gt;Instead of layers, we use packages to keep build times fast and image sizes small.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Should I use this?
&lt;/h2&gt;

&lt;p&gt;We already use apko to create images for our own use (planned public release at a later stage). The technology works well, but we have questions. The state of glibc support isn't clear to us, and documentation is bare-bones. And while melange and apko are &lt;em&gt;technically&lt;/em&gt; faster than Dockerfiles because they can be more selective about what needs to be rebuilt, there isn't tooling that takes advantage of that yet.&lt;/p&gt;

&lt;p&gt;There are always the &lt;a href="https://www.chainguard.dev/chainguard-images"&gt;Wolfi images&lt;/a&gt; to use as base images in standard Dockerfiles. For the story behind the creation of Wolfi, &lt;a href="https://twitter.com/ariadneconill/status/1572952744096399360"&gt;this insightful Twitter thread&lt;/a&gt; by Ariadne Conill is worth a read.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post was &lt;a href="https://cloudplane.org/blog/apko"&gt;originally released on the Cloudplane blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Why We Use CUE (and Not Helm)</title>
      <dc:creator>Jakob Gillich</dc:creator>
      <pubDate>Sun, 09 Oct 2022 10:56:46 +0000</pubDate>
      <link>https://dev.to/jgillich/why-we-use-cue-and-not-helm-48kj</link>
      <guid>https://dev.to/jgillich/why-we-use-cue-and-not-helm-48kj</guid>
      <description>&lt;p&gt;&lt;a href="https://cloudplane.org/"&gt;Cloudplane&lt;/a&gt; is a managed hosting platform built on Kubernetes using our own resource templates. We evaluated several templating tools and eventually settled on &lt;a href="https://cuelang.org/"&gt;CUE&lt;/a&gt;. Here's how we arrived at that conclusion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Go
&lt;/h3&gt;

&lt;p&gt;At the core of Cloudplane is a Kubernetes operator that extends the Kubernetes API with various types, such as &lt;code&gt;Application&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;small&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;discourse&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our templating layer uses application resources to generate Kubernetes resources such as deployments and services, similar to what Helm charts do with &lt;code&gt;values&lt;/code&gt;. Since we wrote our operator in Go, it seemed logical to use Go for templating as well. But it quickly became apparent that this approach would not scale well.&lt;/p&gt;

&lt;p&gt;For one, Go is not an elegant language. Constructing Kubernetes resources in Go is significantly more verbose than e.g. YAML, and string interpolation via placeholders is difficult to read. On top of that, Go does not have built-in support for merging objects (but there is mergo).&lt;/p&gt;

&lt;p&gt;Go works when you have few resources, but for heavy templating use-cases, you want a language dedicated to that purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jsonnet
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://jsonnet.org/"&gt;Jsonnet&lt;/a&gt; promises to be JSON plus templating, and that's exactly what it delivers. It allows you to include other files and has many useful features such as variables and functions. Jsonnet overall was a pretty decent&lt;br&gt;
experience, and we could've stopped there.&lt;/p&gt;

&lt;p&gt;But Jsonnet is a dynamic language with barely any editor integration, and we strongly prefer static types for editor hints and autocomplete. So we kept looking.&lt;/p&gt;
&lt;h3&gt;
  
  
  Dhall
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://dhall-lang.org/"&gt;Dhall&lt;/a&gt; is a functional configuration and programming language. Static typing and its purely functional nature are things we liked about Dhall, but for a configuration language, it can be comically verbose and difficult to read. The following YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app.kubernetes.io/instance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;.App.Metadata.Name&lt;/span&gt;&lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turns into this Dhall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;matchLabels = Some
  ( toMap
      { `app.kubernetes.io/instance` =
          merge
            { Some = λ(_ : Text) → _, None = "" }
            app.metadata.name
      }
  )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the biggest issue with Dhall is performance. Even small templates can take several minutes and gigabytes of RAM to build, and Dhall's VS Code plugin was nearly unusable for us. This is a &lt;a href="https://discourse.dhall-lang.org/t/roadmap-for-improved-kubernetes-support/313"&gt;known issue&lt;/a&gt;, and until it is resolved, we cannot recommend Dhall for Kubernetes users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Helm
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://helm.sh/"&gt;Helm&lt;/a&gt; is a package manager for Kubernetes. We like using it for third-party dependencies, but templating in Helm is not very good. It uses YAML, a whitespace-sensitive data language, and layers a generic text templating engine on top of it. Charts are typically full of boilerplate and indentation hacks.&lt;/p&gt;

&lt;p&gt;We originally started with third-party Helm charts, but ran into frequent issues around secrets. We provide all kinds of credentials through secrets, and pods have a simple mechanism for injecting environment variables from them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FOO&lt;/span&gt;
  &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret-name&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FOO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, most charts require passing secrets as plain text in the values object, and if they do support secrets, the keys are often not configurable. Helm's biggest strength and also its biggest weakness is the abstraction via values, you don't have to think about the underlying K8s resources, but when they matter to you, your only way forward is forking the chart.&lt;/p&gt;

&lt;p&gt;We continue to use Helm for third-party packages, but for our own needs, we decided to use...&lt;/p&gt;

&lt;h3&gt;
  
  
  CUE
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://cuelang.org/"&gt;CUE&lt;/a&gt; is a configuration language that is statically typed and also supports validations. Like Jsonnet, it's a superset of JSON, but with many useful extensions such as comments, variables, modules, and of course&lt;br&gt;
types. It's not quite as powerful at templating as Jsonnet, it does not have functions, but so far we haven't run into serious roadblocks.&lt;/p&gt;

&lt;p&gt;Its design is inspired by Go, all files in a single directory (called package) are combined into the same output. Since configuration can be spread out over many files (and modules), CUE has strict rules about what you can and cannot do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;types&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;just&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;values,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;denotes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;baz:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"baz"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;overriding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fine&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;baz:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"buz"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;conflicting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;value,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;baz:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bux"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you might have guessed, CUE makes quotes and curly brackets mostly optional. It's not quite as slick as YAML, but it's much improved from raw JSON.&lt;/p&gt;

&lt;p&gt;The killer feature for Kubernetes users is that CUE can import Go types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cue get go k8s.io/api/core/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will fetch the Go source code and convert it to CUE so we get full type validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;corev&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"k8s.io/api/core/v1"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;foo:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;corev&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;.#ConfigMap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;apiVersion:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;kind:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="s2"&gt;"ConfigMap"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;bar:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bar"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;invalid:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;will&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are some issues we have with CUE. For one, its syntax is very unusual, &lt;code&gt;_|_&lt;/code&gt; is assigned when there is an error and &lt;code&gt;"\(foo)-bar"&lt;/code&gt; is what string interpolation looks like. We would gladly give up JSON compatibility for a more elegant language.&lt;/p&gt;

&lt;p&gt;Programmability is also limited. There are built-in functions one can call, but defining custom functions is not possible. There is &lt;code&gt;if&lt;/code&gt; but not &lt;code&gt;else&lt;/code&gt;. A lot can be achieved with &lt;a href="https://cuetorials.com/patterns/switch/"&gt;some trickery&lt;/a&gt;, but once again, we wish these were built-in concepts using elegant syntax.&lt;/p&gt;

&lt;p&gt;CUE is a young project with some rough edges, but we think CUE is the future of Kubernetes templating. Its adoption is growing quickly, with &lt;a href="https://dagger.io/"&gt;Dagger&lt;/a&gt; and &lt;a href="https://acorn.io/"&gt;Acorn&lt;/a&gt; as some of its recent adopters.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
