<?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[Brewing Elixir]]></title><description><![CDATA[Learn about everything Elixir!]]></description><link>https://brewingelixir.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1692143724408/8Ff43xMNi.png</url><title>Brewing Elixir</title><link>https://brewingelixir.com</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 14 Apr 2026 18:36:53 GMT</lastBuildDate><atom:link href="https://brewingelixir.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Intro to Elixir Applications on Kubernetes]]></title><description><![CDATA[Welcome to this new series about running Elixir applications on K8s (short for Kubernetes) where we explore the world of Kubernetes through the eyes of an Elixir programmer to achieve even higher availability, reliability and robustness by levering m...]]></description><link>https://brewingelixir.com/intro-to-elixir-applications-on-kubernetes</link><guid isPermaLink="true">https://brewingelixir.com/intro-to-elixir-applications-on-kubernetes</guid><category><![CDATA[k8s]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Elixir]]></category><category><![CDATA[Phoenix framework]]></category><category><![CDATA[Docker]]></category><category><![CDATA[kubectl]]></category><category><![CDATA[containers]]></category><dc:creator><![CDATA[Brewing Elixir]]></dc:creator><pubDate>Sat, 09 Dec 2023 16:43:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1702074911480/ed293843-e90a-4753-bed5-d3f29b05fc24.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome to this new series about running Elixir applications on K8s (short for Kubernetes) where we explore the world of Kubernetes through the eyes of an Elixir programmer to achieve even higher availability, reliability and robustness by levering most tools in the K8s toolbox in a way that would play nice Elixir/OTP and Phoenix applications.</p>
<p>But before we continue let's define some base requirements to get the most out of the series.</p>
<h2 id="heading-requirements">Requirements</h2>
<p>This series assumes some familiarity with the following technologies:</p>
<ul>
<li><p><strong>Elixir programming language and Phoenix web framework</strong>: We'll dig just enough into any of these items to prove a certain feature or situation, so having a basic understanding of both should be enough to get through the content. E.g. being able to generate and run a Phoenix application and knowing how an endpoint gets called up to a controller's function should be enough.</p>
</li>
<li><p><strong>Container platform</strong>: A basic understanding of what containers are and what benefits they offer is important to get a better sense of why Kubernetes exists and how it complements Elixir. So, if you have written a <code>Dockerfile</code>, built and run an image you already have a good base to continue.</p>
</li>
<li><p><strong>Kubernetes concepts</strong>: The following concepts are necessary to have a smooth progress through the series' articles: Pods, Deployments, Services, Nodes, Secrets, Control/Data plane, Namespaces, kubectl.</p>
</li>
</ul>
<p>If this sounds like too much you are most certainly right, but through this series I'll do my best to keep the cognitive load to the minimum and provide explanations of each concept as we introduce them while adding references to learning resources to get all the information you might need to get a great learning experience.</p>
<p>This combination of tools offers a great deal of flexibility and power, but as we learned from Uncle Ben's most famous quote: with great power comes with great <s>responsibility</s> potential complexity.</p>
<p>If you are down to get your hands wet and your skin slightly burnt (as we are going on a sailing adventure with K8s ⎈😉) please keep reading as we're going to level up your deployment game.</p>
<h2 id="heading-docker-kubernetes-elixir-phoenix">Docker + Kubernetes + Elixir + Phoenix = 🚀</h2>
<p>Some people might ask: Why run Elixir/Phoenix applications on Kubernetes if the BEAM already offers slightly similar features like self-healing (i.e. Supervision trees)? Or would running on a container with fewer assigned vCPUs go against letting the BEAM effectively use all available cores on a host to run faster (as more schedules would be available increasing the chances of having more concurrent jobs running in parallel)? Or even, does adding K8s into the mix (assuming we are running our app on a VM or directly on bare metal) provide any extra benefit that outweighs the extra complexity introduced?</p>
<p>Instead of answering each question I would like to describe some of the benefits K8s provides that are not Elixir related:</p>
<ul>
<li><p>Efficient and secure rollout (and rollback) of new versions: Because you are dealing with containers you get greater boot speeds compared to VMs which reduces the rollout times significantly, and most importantly rollbacks, creating a safe deployment environment to run applications on.</p>
</li>
<li><p>Horizontal scaling: Scale in the number of containers and hosts running them at a much higher speed than scaling VMs.</p>
</li>
<li><p>Efficient and controlled resource usage: By leveraging bin-packing and fine-tuning resource requirements you can efficiently run applications on a set of hosts.</p>
</li>
<li><p>Effectively run Distributed Elixir: By incorporating libreries like <a target="_blank" href="https://github.com/bitwalker/libcluster">libcluster</a> we can get clusters of Erlang nodes getting automatically formed while they can efficiently communicate within the cluster.</p>
</li>
<li><p>Use a heterogeneous set of nodes to run different types of applications on them. Think of running AI applications on GPU-optimized VMs while having the rest of the applications running on regular VMs, all orchestrated by the same cluster.</p>
</li>
<li><p>Run your application locally, on several cloud platforms like AWS, GCP, Azure and others, or even set up your on-premise clusters, while using the same mental model and most descriptors ready to be reused.</p>
</li>
<li><p>And much more: run several application versions at the same time, organize them, manage configuration, orchestrate storage, extend and customize the cluster behavior, etc.</p>
</li>
</ul>
<p>So K8s becomes an abstraction layer between our cluster of hosts and our containers offering a more reliable abstraction to run our application. We can still get all the benefits of the BEAM but they operate at a higher level than K8s so most of the time they complement each other.</p>
<p>That's a lot 😮‍💨 But bear with me as I promise that if you continue and give the tutorial a try you'll unlock a new set of possibilities for your Elixir applications, and you'll be able to answer the initial questions by yourself.</p>
<p>Let's go back to basics to start learning through experimentation on how we can get all these benefits.</p>
<h1 id="heading-run-a-simple-phoenix-app-on-k8s">Run a simple Phoenix app on K8s</h1>
<p>To grasp how a Phoenix app runs on K8s we'll start by creating a new project to run it on our local host by using a local Kubernetes cluster distribution provided by <a target="_blank" href="https://docs.docker.com/desktop/kubernetes/">Docker Desktop</a>.</p>
<p>This post uses the following tools and versions:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/asdf-vm/asdf-erlang">Erlang</a>: <code>26.1.2</code></p>
</li>
<li><p><a target="_blank" href="https://github.com/asdf-vm/asdf-elixir">Elixir</a>: <code>1.15.7 (compiled with Erlang/OTP 26)</code></p>
</li>
<li><p><a target="_blank" href="https://hexdocs.pm/phoenix/installation.html">Phoenix</a>: <code>1.7.10</code></p>
</li>
<li><p><a target="_blank" href="https://www.docker.com/products/docker-desktop/">Docker Desktop</a> : <code>Docker version 24.0.6-rd</code></p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/tools/">kubectl</a>: <code>v1.28.4</code></p>
</li>
</ul>
<p>Each tool listed has a link to its installation doc. For Erlang/Elixir I recommend the <a target="_blank" href="https://asdf-vm.com/guide/getting-started.html">asdf</a> version manager.</p>
<p>To be able to run Kubernetes locally you'll need to enable it in Docker Desktop by opening <em>Settings &gt; Kubernetes</em> and clicking on <em>Enable Kubernetes</em>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702128064901/4d5381ea-91fb-4422-aed6-a8927a9b6b0e.png" alt class="image--center mx-auto" /></p>
<p>This will restart the Docker Desktop application but once it completes you'll have a ready-to-use local K8s environment. To check that's true you can run:</p>
<pre><code class="lang-bash">$ kubectl version
Client Version: v1.28.4
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
Server Version: v1.28.2
$ kubectl get nodes
NAME             STATUS   ROLES           AGE   VERSION
docker-desktop   Ready    control-plane   16h   v1.28.2
</code></pre>
<p>If you get anything different (besides the versions) check if docker is running (i.e. <code>docker ps</code> to list containers).</p>
<p>In case you feel adventurous there are other great options to run K8s locally like <a target="_blank" href="https://docs.docker.com/desktop/extensions-sdk/guides/kubernetes/">Docker's Kubernetes extension</a>, <a target="_blank" href="https://rancherdesktop.io/">Rancher Desktop</a>, <a target="_blank" href="https://microk8s.io/">Microk8s</a> and many others, but they are not required to complete this tutorial as we'll be using <strong>Docker Desktop</strong>.</p>
<h2 id="heading-creating-the-app">Creating the app</h2>
<p>The first thing we need is our Phoenix app so let's generate one (without ecto as we won't use it for now):</p>
<pre><code class="lang-bash">mix phx.new myapp --no-ecto
<span class="hljs-built_in">cd</span> myapp
</code></pre>
<p>Next, we'll perform a few changes to the project to simulate it is a "real" application. So open your editor of choice and perform the following changes:</p>
<ol>
<li>Add a new route to receive <code>POST</code> requests with an optional <code>name</code> query parameter and then use it to return <code>"Hello #{name}"</code> as text with a <code>HTTP 200</code> status code.</li>
</ol>
<p><code>lib/myapp_web/router.ex</code></p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">MyappWeb.Router</span></span> <span class="hljs-keyword">do</span>
<span class="hljs-comment"># ...</span>
 scope <span class="hljs-string">"/api"</span>, MyappWeb <span class="hljs-keyword">do</span>
    pipe_through <span class="hljs-symbol">:api</span>

    post <span class="hljs-string">"/hello"</span>, HelloController, <span class="hljs-symbol">:hello</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-comment"># ...</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p><code>lib/myapp_web/controllers/hello_controller.ex</code></p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">MyappWeb.HelloController</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">use</span> MyappWeb, <span class="hljs-symbol">:controller</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hello</span></span>(conn, params) <span class="hljs-keyword">do</span>
    name = Map.get(params, <span class="hljs-string">"name"</span>, <span class="hljs-string">"World"</span>)

    conn |&gt; put_status(<span class="hljs-number">200</span>) |&gt; text(<span class="hljs-string">"Hello <span class="hljs-subst">#{name}</span>"</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Let's give it a try by running it and testing <strong>http://localhost:4000</strong> with <code>curl</code> (or your HTTP client of choice).</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Start the server</span>
mix phx.server
<span class="hljs-comment"># Reach the route using curl (or your HTTP tool of choice)</span>
curl -X POST http://localhost:4000/api/hello -d <span class="hljs-string">'name=BrewingElixir'</span>
</code></pre>
<p>Nice! We go <code>Hello BrewingElixir</code> which proves our app works locally so we can proceed with the next steps.</p>
<h2 id="heading-build-a-container-image">Build a container image</h2>
<p>We are entering the world of containers now and because our application's release is custom we need to define a <code>Dockerfile</code> to instruct Docker on what our app's image will contain. Luckily for us, we can use the release generator task from the Phoenix project to get a ready-to-user <code>Dockerfile</code>. Just run:</p>
<pre><code class="lang-bash">mix phx.gen.release --docker
</code></pre>
<p>Let's slow down to inspect and describe what the <code>Dockerfile</code> provides because is very important to understand what the image will contain as is the foundation of what will get executed. This same image is what will get used and managed by K8s once we deploy it.</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># 1. Build-time configureable arguments</span>
<span class="hljs-keyword">ARG</span> ELIXIR_VERSION=<span class="hljs-number">1.15</span>.<span class="hljs-number">7</span>
<span class="hljs-keyword">ARG</span> OTP_VERSION=<span class="hljs-number">26.1</span>.<span class="hljs-number">2</span>
<span class="hljs-keyword">ARG</span> DEBIAN_VERSION=bullseye-<span class="hljs-number">20231009</span>-slim

<span class="hljs-keyword">ARG</span> BUILDER_IMAGE=<span class="hljs-string">"hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"</span>
<span class="hljs-keyword">ARG</span> RUNNER_IMAGE=<span class="hljs-string">"debian:${DEBIAN_VERSION}"</span>

<span class="hljs-comment"># 2. The base image use to build the release.</span>
<span class="hljs-comment"># The file has to stages to leave "build time" files out</span>
<span class="hljs-comment"># from the runtime to slim down the final image to use.</span>
<span class="hljs-comment"># Ref: https://docs.docker.com/build/building/multi-stage/</span>
<span class="hljs-keyword">FROM</span> ${BUILDER_IMAGE} as builder

<span class="hljs-comment"># install build dependencies</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update -y &amp;&amp; apt-get install -y build-essential git \
    &amp;&amp; apt-get clean &amp;&amp; rm -f /var/lib/apt/lists/*_*</span>

<span class="hljs-comment"># prepare build dir</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-comment"># install hex + rebar</span>
<span class="hljs-keyword">RUN</span><span class="bash"> mix local.hex --force &amp;&amp; \
    mix local.rebar --force</span>

<span class="hljs-comment"># set build ENV</span>
<span class="hljs-keyword">ENV</span> MIX_ENV=<span class="hljs-string">"prod"</span>

<span class="hljs-comment"># install mix dependencies</span>
<span class="hljs-keyword">COPY</span><span class="bash"> mix.exs mix.lock ./</span>
<span class="hljs-keyword">RUN</span><span class="bash"> mix deps.get --only <span class="hljs-variable">$MIX_ENV</span></span>
<span class="hljs-keyword">RUN</span><span class="bash"> mkdir config</span>

<span class="hljs-comment"># copy compile-time config files before we compile dependencies</span>
<span class="hljs-comment"># to ensure any relevant config change will trigger the dependencies</span>
<span class="hljs-comment"># to be re-compiled.</span>
<span class="hljs-keyword">COPY</span><span class="bash"> config/config.exs config/<span class="hljs-variable">${MIX_ENV}</span>.exs config/</span>
<span class="hljs-keyword">RUN</span><span class="bash"> mix deps.compile</span>

<span class="hljs-keyword">COPY</span><span class="bash"> priv priv</span>

<span class="hljs-keyword">COPY</span><span class="bash"> lib lib</span>

<span class="hljs-keyword">COPY</span><span class="bash"> assets assets</span>

<span class="hljs-comment"># compile assets</span>
<span class="hljs-keyword">RUN</span><span class="bash"> mix assets.deploy</span>

<span class="hljs-comment"># Compile the release</span>
<span class="hljs-keyword">RUN</span><span class="bash"> mix compile</span>

<span class="hljs-comment"># Changes to config/runtime.exs don't require recompiling the code</span>
<span class="hljs-keyword">COPY</span><span class="bash"> config/runtime.exs config/</span>

<span class="hljs-keyword">COPY</span><span class="bash"> rel rel</span>
<span class="hljs-keyword">RUN</span><span class="bash"> mix release</span>

<span class="hljs-comment"># start a new build stage so that the final image will only contain</span>
<span class="hljs-comment"># the compiled release and other runtime necessities</span>
<span class="hljs-keyword">FROM</span> ${RUNNER_IMAGE}

<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update -y &amp;&amp; \
  apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \
  &amp;&amp; apt-get clean &amp;&amp; rm -f /var/lib/apt/lists/*_*</span>

<span class="hljs-comment"># Set the locale</span>
<span class="hljs-keyword">RUN</span><span class="bash"> sed -i <span class="hljs-string">'/en_US.UTF-8/s/^# //g'</span> /etc/locale.gen &amp;&amp; locale-gen</span>

<span class="hljs-keyword">ENV</span> LANG en_US.UTF-<span class="hljs-number">8</span>
<span class="hljs-keyword">ENV</span> LANGUAGE en_US:en
<span class="hljs-keyword">ENV</span> LC_ALL en_US.UTF-<span class="hljs-number">8</span>

<span class="hljs-keyword">WORKDIR</span><span class="bash"> <span class="hljs-string">"/app"</span></span>
<span class="hljs-keyword">RUN</span><span class="bash"> chown nobody /app</span>

<span class="hljs-comment"># set runner ENV</span>
<span class="hljs-keyword">ENV</span> MIX_ENV=<span class="hljs-string">"prod"</span>

<span class="hljs-comment"># Only copy the final release from the build stage</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=builder --chown=nobody:root /app/_build/<span class="hljs-variable">${MIX_ENV}</span>/rel/myapp ./</span>

<span class="hljs-keyword">USER</span> nobody

<span class="hljs-comment"># If using an environment that doesn't automatically reap zombie processes, it is</span>
<span class="hljs-comment"># advised to add an init process such as tini via `apt-get install`</span>
<span class="hljs-comment"># above and adding an entrypoint. See https://github.com/krallin/tini for details</span>
<span class="hljs-comment"># ENTRYPOINT ["/tini", "--"]</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"/app/bin/server"</span>]</span>
</code></pre>
<p>The file is ready to use but keep in mind this is where you'll come to add system dependencies required by our application. E.g. Installing <code>ffmpeg</code> if you intend to use it for video manipulation. In other cases, you'll want to use a different "base image" for your container image to increase security and/or reduce image size. Those are also actions that will be applied to this same file.</p>
<p>K8s truly excels is being able to run multiple <code>Pods</code> from different images to easily validate their correct functionality (think canary or blue/green deployments) while running the same underlying nodes.</p>
<p>Continuing with our application, let's build the docker image based on the <code>Dockerfile</code> we've just seen by running:</p>
<pre><code class="lang-bash">docker build -t myapp  .
</code></pre>
<p>This should effectively build the image and store it with the tag <code>latest</code>. You can verify this by running:</p>
<pre><code class="lang-bash">docker images myapp
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">REPOSITORY   TAG       IMAGE ID       CREATED              SIZE
myapp        latest    68e102c0eb9d   About a minute ago   125MB
</code></pre>
<p>Sweet! We have our image ready and is only 125MB! 😅 . You are probably wondering: <em>Is that the best we can do?</em> Is not, but should be enough for now. If you are curious about what to use when aiming at a small image check out <a target="_blank" href="https://hub.docker.com/_/alpine">Alpine Linux at Docker Hub</a>. Images start at 5MB which is 4% of our current image size!</p>
<p>Now let's run it to verify the image is good.</p>
<pre><code class="lang-bash">docker run -p 4000:4000 myapp:latest
</code></pre>
<p>Oh no! The output looks starts like this:</p>
<blockquote>
<p>** (RuntimeError) environment variable SECRET_KEY_BASE is missing. You can generate one by calling: mix phx.gen.secret</p>
</blockquote>
<p>Ah! That's because we are running a <em>release</em> which executes <code>config/runtime.exs</code> during start-up which ends up raising an exception when <code>SECRET_KEY_BASE</code> is not provided. Let's do as suggested and provide the value to docker like this:</p>
<pre><code class="lang-bash">SECRET_KEY_BASE=$(mix phx.gen.secret)
docker run -p 4000:4000 -e SECRET_KEY_BASE=<span class="hljs-variable">$SECRET_KEY_BASE</span> myapp:latest
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">13:59:38.371 [info] Running MyappWeb.Endpoint with cowboy 2.10.0 at :::4000 (http)
13:59:38.372 [info] Access MyappWeb.Endpoint at https://example.com
</code></pre>
<p>Congratulations! You have a Phoenix application running within a Docker container.</p>
<p>You probably noticed the <code>-p 4000:4000</code> parameter when running <code>docker</code>. This is necessary to instruct Docker to connect the host's port to the container's port. You can use a different one if you like (as long as is above 1024 and below 65535) as long as it connects to the listening port in the container which is 4000 for this application.</p>
<p>In another terminal run the following <code>curl</code> command to verify the app's code gets executed:</p>
<pre><code class="lang-bash">curl -X POST http://localhost:4000/api/hello -d <span class="hljs-string">'name=BrewingElixirFromDocker'</span>
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">Hello BrewingElixirFromDocker⏎
</code></pre>
<p>Great! The app works as expected. You can stop the container by typing <code>ctrl+c</code>.</p>
<p>At this point, you might have noticed you had to:</p>
<ul>
<li><p>Manually start/stop the app.</p>
</li>
<li><p>Provide the needed configuration to docker to let the app start correctly.</p>
</li>
<li><p>Set up the hosts and container ports to allow access to the service.</p>
</li>
</ul>
<p>which is fine for testing apps locally, but what would happen if you need to perform a deployment with zero downtime? Or if the universe has a glitch that makes the BEAM unexpectedly crash? 💥 At this moment you would have to start looking for other tools that would help solve these issues. But look no further! That's one of the many features Kubernetes offers, so let's finally test this thing 😎</p>
<h2 id="heading-deploy-to-kubernetes">Deploy to Kubernetes⎈</h2>
<p>To deploy your application to <code>K8s</code> you'll use a combination of the following resources:</p>
<ul>
<li><p><code>Deployment</code>: provides declarative updates for <code>Pods</code> (and <code>ReplicaSets</code>). This is where we define what docker image to use.</p>
</li>
<li><p><code>Service</code>: used to expose our application running in your cluster behind a single outward-facing endpoint. In simple terms, it connects a deployment to one or more ports.</p>
</li>
<li><p><code>ConfigMaps</code>: an object used to store non-confidential data in key-value pairs.</p>
</li>
<li><p><code>Secrets</code>: similar to <code>ConfigMaps</code> but are specifically intended to hold confidential data.</p>
</li>
</ul>
<p>These resources are all defined as <code>YAML</code> files that get applied by <code>kubectl</code> which instructs K8s on what resources to create or update. This is key as K8s allows you to declaratively (and sometimes imperatively) define resources and the state you want for them. Can't stress how useful and important this is. It allows you to define the "end state" of the resource, even when something is happening mid-flight. K8s will "figure out" which steps are needed to get to the desired state effectively.</p>
<p>Continuing with the setup, go ahead and create a directory under the root folder of our Phoenix projects called <code>infra</code>.</p>
<pre><code class="lang-bash">mkdir infra
</code></pre>
<p>You'll place all K8s descriptors there for now. In more sophisticated deployments these files could be included in a separate repository with Terraform and Ansible files in case that's the IaC preference of the team/company.</p>
<p>Now create a deployment descriptor. This is usually a <code>YAML</code> file that declares what images to use within the <code>Pods</code>, the naming used within the cluster, what arguments to pass and many other configurations to instruct K8s on how to orchestrate the deployment. So go ahead and create:</p>
<p><code>infra/deployment.yaml</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">name:</span> <span class="hljs-string">myapp-deployment</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">myapp</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">app:</span> <span class="hljs-string">myapp</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">myapp</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">myapp</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">myapp:latest</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">4000</span>
</code></pre>
<p>You can see how the name of the deployment is defined as well as the image and ports used by the container.</p>
<p>With the following command, you'll instruct your Kubernetes cluster through <code>kubectl</code> to read the deployment descriptor and apply the desired state:</p>
<pre><code class="lang-bash">kubectl apply -f infra/deployment.yaml
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">deployment.apps/myapp-deployment created
</code></pre>
<p>This command returns almost instantaneously but the resources are still getting created. To check the state run:</p>
<pre><code class="lang-bash">kubectl get deploy
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">NAME               READY   UP-TO-DATE   AVAILABLE   AGE
myapp-deployment   0/1     1            0           7s
</code></pre>
<p>Or check the <code>Pods</code> associated with the deployment:</p>
<pre><code class="lang-bash">kubectl get pod
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">NAME                                READY   STATUS             RESTARTS   AGE
myapp-deployment-7cc9745dcb-bpkt6   0/1     ImagePullBackOff   0          3m
</code></pre>
<p>Something looks off as the <code>READY</code> column shows <code>0/1</code> deployments are ready, and the same when we list the <code>Pods</code>. By checking the <code>STATUS</code> of the latest output we can see it is reporting <code>ImagePullBackOff</code>. This is expected because the <code>image</code> provided can't be found by the cluster because is not stored in a registry. A registry is an image repository that K8s uses to look for images. By default, K8s will look for images at DockerHub and only be able to access them if they are public or the cluster has credentials configured to access private images.</p>
<p>For the sake of simplicity, we'll stick with Docker's product and create an account at <a target="_blank" href="https://hub.docker.com/">DockerHub</a>. Once your account is ready create a private repository for your docker image:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1702131750410/60bd774b-5e3e-4ae6-8229-9637962eaaa0.png" alt class="image--center mx-auto" /></p>
<p><em>⚠️The namespace will be different so remember to change</em> <code>brewingelixir</code> <em>with the name of your namespace.</em></p>
<p>To be able to push to the right registry we need to tag our existing image (or build it with the desired tag). To achieve that run:</p>
<pre><code class="lang-bash">docker tag myapp:latest brewingelixir/myapp:0.0.1
</code></pre>
<p>A good practice for image tags is using <a target="_blank" href="https://semver.org/">Semantic Versioning</a> (or some combination of semver + Git SHA). This way we keep the image builds unique and immutable. This will get us a more important benefit in K8s as this will help cache images in the nodes which speeds up rollouts and rollbacks.</p>
<p>This will create a new image name and tag. You can check they have the same image's id by running:</p>
<pre><code class="lang-bash">docker images | grep myapp
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">brewingelixir/myapp  0.0.1      366738717276   17 hours ago    125MB
myapp                latest     68e102c0eb9d   27 hours ago    125MB
</code></pre>
<p>You'll notice two images, one named <code>myapp</code> and another one similar to this one: <code>brewingelixir/myapp</code>. Finally, push this image to the <code>DockerHub</code> registry:</p>
<pre><code class="lang-bash">docker push brewingelixir/myapp:0.0.1
</code></pre>
<p>At this point your app is stored in DockerHub ready to be pulled by your Kubernetes cluster. In more sophisticated deployments you'll need to configure some credentials to instruct K8s on what to use to pull private images from other registries. But that's something we don't need to think about for the local version.</p>
<p>Continue with editing <code>infra/deployment.yaml</code> to update the <code>image</code> value:</p>
<pre><code class="lang-yaml"><span class="hljs-string">...</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">brewingelixir/myapp:0.0.1</span>
<span class="hljs-string">...</span>
</code></pre>
<p>And apply the change with <code>kubectl</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">apply</span> <span class="hljs-string">-f</span> <span class="hljs-string">infra/deployment.yaml</span>
</code></pre>
<p>And let's check again if the <code>pod</code> is running:</p>
<pre><code class="lang-bash">kubectl get po
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">NAME                                READY   STATUS   RESTARTS      AGE
myapp-deployment-546cbb5f96-9ptjb   0/1     Error    1 (14s ago)   16s
</code></pre>
<p>We now have a different error 🤔 Let's inspect the events by executing:</p>
<pre><code class="lang-bash">kubectl describe pod myapp-deployment-546cbb5f96-9ptjb
</code></pre>
<p>The output should look similar to the following one:</p>
<pre><code class="lang-bash">Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  87s                default-scheduler  Successfully assigned default/myapp-deployment-546cbb5f96-9ptjb to docker-desktop
  Normal   Pulled     43s (x4 over 87s)  kubelet            Container image <span class="hljs-string">"brewingelixir/myapp:0.0.1"</span> already present on machine
  Normal   Created    43s (x4 over 87s)  kubelet            Created container myapp
  Normal   Started    43s (x4 over 87s)  kubelet            Started container myapp
  Warning  BackOff    13s (x6 over 83s)  kubelet            Back-off restarting failed container myapp <span class="hljs-keyword">in</span> pod myapp-deployment-546cbb5f96-9ptjb_default(e5f972bc-d5fa-47df-b59f-e6e18ba31520)
</code></pre>
<p>It seems like K8s could fetch the docker image from <code>DockerHub</code> but is reporting <code>ack-off restarting failed container myapp</code>. Maybe the container's logs will give some extra insight:</p>
<pre><code class="lang-bash">kubectl logs myapp-deployment-546cbb5f96-9ptjb
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">ERROR! Config provider Config.Reader failed with:
** (RuntimeError) environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret

    /app/releases/0.1.0/runtime.exs:31: (file)
    (elixir 1.15.7) src/elixir.erl:396: :elixir.eval_external_handler/3
    (stdlib 5.1.1) erl_eval.erl:750: :erl_eval.do_apply/7
    (stdlib 5.1.1) erl_eval.erl:494: :erl_eval.expr/6
    (stdlib 5.1.1) erl_eval.erl:136: :erl_eval.exprs/6
    (elixir 1.15.7) src/elixir.erl:375: :elixir.eval_forms/4
    (elixir 1.15.7) lib/module/parallel_checker.ex:112: Module.ParallelChecker.verify/1
    (elixir 1.15.7) lib/code.ex:543: Code.validated_eval_string/3

Runtime terminating during boot ({<span class="hljs-comment">#{message=&gt;&lt;&lt;101,110,118,105,114,111,110,109,101,110,116,32,118,97,114,105,97,98,108,101,32,83,69,67,82,69,84,95,75,69,89,95,66,65,83,69,32,105,115,32,109,105,115,115,105,110,103,46,10,89,111,117,32,99,97,110,32,103,101,110,101,114,97,116,101,32,111,110,101,32,98,121,32,99,97,108,108,105,110,103,58,32,109,105,120,32,112,104,120,46,103,101,110,46,115,101,99,114,101,116,10&gt;&gt;,'__struct__'=&gt;'Elixir.RuntimeError','__exception__'=&gt;true},[{elixir_eval,'__FILE__',1,[{file,"/app/releases/0.1.0/runtime.exs"},{line,31}]},{elixir,eval_external_handler,3,[{file,"src/elixir.erl"},{line,396},{error_info,#{module=&gt;'Elixir.Exception'}}]},{erl_eval,do_apply,7,[{file,"erl_eval.erl"},{line,750}]},{erl_eval,expr,6,[{file,"erl_eval.erl"},{line,494}]},{erl_eval,exprs,6,[{file,"erl_eval.erl"},{line,136}]},{elixir,eval_forms,4,[{file,"src/elixir.erl"},{line,375}]},{'Elixir.Module.ParallelChecker',verify,1,[{file,"lib/module/parallel_checker.ex"},{line,112}]},{'Elixir.Code',validated_eval_string,3,[{</span>

Crash dump is being written to: erl_crash.dump...done
</code></pre>
<p>Oh! We forgot to set the value of the<code>SECRET_KEY_BASE</code> env var. This can easily be solved by creating a secret resource. But first, create a key using the handy <code>phx.gen.secret</code> mix task.</p>
<pre><code class="lang-bash">SECRET_KEY_BASE=$(mix phx.gen.secret)
<span class="hljs-built_in">echo</span> -n <span class="hljs-variable">$SECRET_KEY_BASE</span> | base64
</code></pre>
<p>The output of the last command will give you the base 64 representation of the generated value. K8s requires us to do this because any control character can easily create a syntax error in the YAML file.</p>
<p>Next, create a secret descriptor with the name <code>infra/secret.yaml</code></p>
<p><em>Important: The names of these files have no relation with the resource they are creating.</em></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">myapp-secret</span>
<span class="hljs-attr">data:</span>
  <span class="hljs-attr">key_base:</span> <span class="hljs-string">c3Z3b3loQWt0MlJ2bytvRlBwMU4zMjhmNlhBVkRzaTE4cmVoZHRaUGlEMWJ1b0w5Y25sQnJwZjhRWnhETm5ScA==</span>
</code></pre>
<p>To effectively create the secret in K8s run:</p>
<pre><code class="lang-bash">kubectl apply -f infra/secret.yaml
</code></pre>
<p>To verify the resource exists execute:</p>
<pre><code class="lang-bash">kubectl get secret
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">NAME           TYPE     DATA   AGE
myapp-secret   Opaque   1      17h
</code></pre>
<p>The last piece of this puzzle is connecting this secret to the deployment so the app knows where to fetch the value from. So edit <code>infra/deployment.yaml</code> to add the <code>env</code> element at the same level as the <code>image</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-string">....</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">myapp</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">brewingelixir/myapp:0.0.1</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">4000</span>
        <span class="hljs-attr">env:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">SECRET_KEY_BASE</span> 
          <span class="hljs-attr">valueFrom:</span>
            <span class="hljs-attr">secretKeyRef:</span>
              <span class="hljs-attr">name:</span> <span class="hljs-string">myapp-secret</span>
              <span class="hljs-attr">key:</span> <span class="hljs-string">key_base</span>
</code></pre>
<p>You can see how <code>SECRET_KEY_BASE</code> is the name of the env var that will get extracted from <code>myapp-secret</code> secret under the <code>key</code> <code>key_base</code>.</p>
<p>You can save the change and apply by running: <code>kubectl apply -f infra/deployment.yaml</code></p>
<p>Going back to running <code>kubectl get pods</code> list you can see the status is now <code>Running</code>!</p>
<pre><code class="lang-bash">NAME                                READY   STATUS    RESTARTS   AGE
myapp-deployment-78dd444d79-sdmpq   1/1     Running   0          1m
</code></pre>
<p>This is great, but how do we access your application? That's a task for a <code>Service</code>. So let's create one and associate it with the deployment.</p>
<p>Create <code>infra/service.yaml</code> and fil it wth:</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">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">myapp-service</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">myapp</span>
  <span class="hljs-attr">ports:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
      <span class="hljs-attr">port:</span> <span class="hljs-number">4000</span>
      <span class="hljs-attr">targetPort:</span> <span class="hljs-number">4000</span>
  <span class="hljs-attr">type:</span> <span class="hljs-string">LoadBalancer</span>
</code></pre>
<p>Apply the change:</p>
<pre><code class="lang-bash">kubectl apply -f infra/service.yaml
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">service/myapp-service created
</code></pre>
<p>And check the service's resource state:</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">get</span> <span class="hljs-string">svc</span>
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">NAME            TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
kubernetes      ClusterIP      10.96.0.1      &lt;none&gt;        443/TCP          51m
myapp-service   LoadBalancer   10.110.24.84   localhost     4000:32660/TCP   8m45s
</code></pre>
<p>Let's give the app a try by going to http://localhost:4000. And also try the <code>curl</code> command one more time:</p>
<pre><code class="lang-bash">curl -X POST http://localhost:4000/api/hello -d <span class="hljs-string">'name=BrewingElixirFromK8s'</span>
</code></pre>
<p>Output:</p>
<pre><code class="lang-bash">Hello BrewingElixirFromK8s⏎
</code></pre>
<p>Awesome! You now have a Phoenix app running on K8s⛵️! Next, let's try a few K8s tricks to get a sense of the power of this powerhouse.</p>
<h3 id="heading-perform-a-rollout">Perform a rollout</h3>
<p>To get a sense of how fast and easy is to perform a rollout we'll simulate creating a new version to deploy it. To do so edit the <code>HelloController</code> controller and add an <code>/echo</code> route.</p>
<p><code>lib/myapp_web/router.ex</code></p>
<pre><code class="lang-elixir">...
  scope <span class="hljs-string">"/api"</span>, MyappWeb <span class="hljs-keyword">do</span>
    pipe_through <span class="hljs-symbol">:api</span>

    post <span class="hljs-string">"/hello"</span>, HelloController, <span class="hljs-symbol">:hello</span>
    post <span class="hljs-string">"/echo"</span>, HelloController, <span class="hljs-symbol">:echo</span>
  <span class="hljs-keyword">end</span>
...
</code></pre>
<p>Then add a new function within the <code>HelloControler</code> to return the <code>params</code> as <code>json</code>.</p>
<p><code>lib/myapp_web/controllers/hello_controller.ex</code></p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">MyappWeb.HelloController</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-comment">#Some existing code.....</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">echo</span></span>(conn, params) <span class="hljs-keyword">do</span>
    conn |&gt; put_status(<span class="hljs-number">200</span>) |&gt; json(params)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>After you verify that this works as expected by running <code>mix phx.server</code> and hitting it with <code>curl</code>, you can build and tag the new image to push it to the registry:</p>
<pre><code class="lang-bash">docker build -t brewingelixir/myapp:0.0.2 .
docker push brewingelixir/myapp:0.0.2
</code></pre>
<p>This time we started by defining the tag you'll use to push from the start. This should save you from having to perform the tag step.</p>
<p>Finally, edit the deployment descriptor to update it with the new tag version.</p>
<p><code>infra/deploymnet.yaml</code></p>
<pre><code class="lang-yaml"><span class="hljs-string">...</span>
     <span class="hljs-attr">image:</span> <span class="hljs-string">brewingelixir/myapp:0.0.2</span>
<span class="hljs-string">...</span>
</code></pre>
<p>and apply the changes to the deployment:</p>
<pre><code class="lang-bash">kubectl apply -f infra/deployment.yaml
</code></pre>
<p>To watch the deployment or pod replacement in near real time you can run:</p>
<pre><code class="lang-bash">kubectl get deploy -w
<span class="hljs-comment"># or</span>
kubectl get pod -w
</code></pre>
<p>To verify the rollout is complete run:</p>
<pre><code class="lang-bash">kubectl rollout status deploy myapp-deployment
</code></pre>
<p>Expected output:</p>
<pre><code class="lang-bash">deployment <span class="hljs-string">"myapp-deployment"</span> successfully rolled out
</code></pre>
<p>In case you want to verify the image currently used by the deployment is the same one you have just applied then run:</p>
<pre><code class="lang-bash">kubectl describe deploy myapp-deployment
</code></pre>
<p>It will provide all the runtime details of the <code>myapp-deployment</code> deployment object. Look for <code>Image</code> to verify the name and tag are the right ones.</p>
<h3 id="heading-huston-we-have-a-problem-aka-rollback-asap">"Huston, we have a problem" a.k.a. Rollback ASAP!</h3>
<p>Let's imagine this deployment starts causing issues and you need to perform a rollback as fast as possible!</p>
<p>To go back to the immediate previous version just run:</p>
<pre><code class="lang-bash">kubectl rollout undo deployment/myapp-deployment
</code></pre>
<p>Verify the version is back to <code>0.0.1</code> by running:</p>
<pre><code class="lang-yaml"><span class="hljs-string">kubectl</span> <span class="hljs-string">describe</span> <span class="hljs-string">deploy</span> <span class="hljs-string">myapp-deployment</span> <span class="hljs-string">|</span> <span class="hljs-string">grep</span> <span class="hljs-string">Image</span>
</code></pre>
<p>Great! That was a really fast rollback. Doesn't that feel great?!</p>
<p>To go back to <code>0.0.2</code> just run <code>kubectl apply -f infra/deployment.yaml</code> again or create a new tag with the "fixes" so the app works as expected this time 🫡.</p>
<h3 id="heading-scale-out-our-app">Scale-out our app</h3>
<p>K8s has the concept of replicas which are more instances of the same <code>pod</code> running in the cluster. They are normally independent of each other but associated with the <code>service</code> to let this one redistribute the load between them.</p>
<pre><code class="lang-bash">$ kubectl scale deployment/myapp-deployment --replicas=10
$ kubectl get po
kubectl get po
NAME                                READY   STATUS    RESTARTS   AGE
myapp-deployment-8674fb85bc-8qcjp   1/1     Running   0          2s
myapp-deployment-8674fb85bc-clvgv   1/1     Running   0          2s
myapp-deployment-8674fb85bc-fdv5t   1/1     Running   0          2s
myapp-deployment-8674fb85bc-h5bmb   1/1     Running   0          3m19s
myapp-deployment-8674fb85bc-ptn4c   1/1     Running   0          2s
myapp-deployment-8674fb85bc-pzl26   1/1     Running   0          2s
myapp-deployment-8674fb85bc-snnpl   1/1     Running   0          2s
myapp-deployment-8674fb85bc-tpqnc   1/1     Running   0          2s
myapp-deployment-8674fb85bc-v7lrq   1/1     Running   0          2s
myapp-deployment-8674fb85bc-xwqvm   1/1     Running   0          2s
</code></pre>
<p>Wow, that was fast! You can even go to <code>0</code> if that's needed.</p>
<pre><code class="lang-bash">kubectl scale deployment/myapp-deployment --replicas=0
</code></pre>
<p>Manual scaling is fine if you have a predictable load that won't change <em>significantly</em> over time. In case you need to deal with significant highs and lows over time then you can use a combination of:</p>
<ul>
<li><p>Horizontal Pod Autoscaler (HPA): adjusts the number of replicas of an application based on resources and/or custom metrics.</p>
</li>
<li><p>Vertical Pod Autoscaler (VPA): adjusts the resource requests and limits of a container.</p>
</li>
<li><p>Cluster Autoscaler (CA): adjusts the number of nodes in the cluster when pods fail to schedule or when nodes are underutilized.</p>
</li>
</ul>
<p>But that's something for a future post in this series 😉</p>
<p><em>Note: The code for this post can be found</em> <a target="_blank" href="https://github.com/brewingelixir/elixir-on-kubernetes/tree/main/myapp"><em>here</em></a><em>.</em></p>
<h1 id="heading-conclusion-and-whats-next-in-the-series">Conclusion and what's next in the series</h1>
<p>If you've deployed applications to Kubernetes in the past (e.g. Go, Node, Java, etc) you might be thinking: This looks very similar to what needs to be done to deploy those non-Elixir applications. And you'll be right! The process is pretty much agnostic to the application running inside the container. It will only become slightly different when you aim at running Distributed Erlang applications where you'd need to let the application easily discover other Erlang nodes. Luckily, we can avoid having to deal with that situation as long as our application is fine without it. E.g. Using shared databases, Phoenix PubSub through a Redis cluster, etc.</p>
<p>In summary, with a couple of commands we end up with a scalable deployment for an application that performs safe rollouts and rollbacks in an environment that closely resembles the ones running on cloud providers. Isn't that great?!</p>
<p>Next in this series, we'll explore deploying this same app to the Internet, starting from simple K8s distributions like Rancher's K3s to full-featured distributions like EKS which is AWS managed Kubernetes solution.</p>
<div class="hn-embed-widget" id="convertkit"></div>]]></content:encoded></item><item><title><![CDATA[CLI apps in Elixir. Part 2]]></title><description><![CDATA[In this post, we'll explore each tool described in Part 1 to see for ourselves the benefits and limitations of each alternative with the hope we'll end up with enough knowledge to decide which one fits best for each use case.
A nice approach to easil...]]></description><link>https://brewingelixir.com/cli-apps-in-elixir-part-2</link><guid isPermaLink="true">https://brewingelixir.com/cli-apps-in-elixir-part-2</guid><category><![CDATA[burrito]]></category><category><![CDATA[Elixir]]></category><category><![CDATA[cli]]></category><category><![CDATA[Scripting]]></category><category><![CDATA[escript]]></category><category><![CDATA[mix]]></category><category><![CDATA[Erlang]]></category><dc:creator><![CDATA[Brewing Elixir]]></dc:creator><pubDate>Sun, 26 Nov 2023 20:02:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1701026542947/0a186a02-1d3e-4782-8cab-8e830e07fe0c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this post, we'll explore each tool described in <a target="_blank" href="https://brewingelixir.com/cli-apps-in-elixir-part-1">Part 1</a> to see for ourselves the benefits and limitations of each alternative with the hope we'll end up with enough knowledge to decide which one fits best for each use case.</p>
<p>A nice approach to easily compare alternatives is building the same app with each tool. That way you can easily spot the similarities and differences between them.</p>
<p>For the sake of simplicity, you are going to build a simplified version of the <a target="_blank" href="https://en.wikipedia.org/wiki/Wc_(Unix)">wc command</a>. The <code>wc</code> command is short of "word count" and allows counting new lines, words, characters and a few more. But the core features we'll implement are:</p>
<ul>
<li><p>Parse command line arguments.</p>
</li>
<li><p>Read from <code>stdin</code> and output to <code>stdout</code>.</p>
</li>
<li><p>Support reading a single file when provided as an argument.</p>
</li>
<li><p>Return the stats for newline, word and grapheme (this is not standard but we'll do it this way because it is nicer with UTF-8 files).</p>
</li>
</ul>
<p>We'll ignore showing comprehensive help, well-formatted error messages, reading multiple files when provided and any other feature defined in its man page.</p>
<p>Now let's get into the code 🧑‍💻</p>
<h1 id="heading-business-logic">Business logic</h1>
<p>To make things easier to read and understand let's create a POEM (Plain Old Elixir Module(*)). We'll be able to use this module across implementations to focus on the differences.</p>
<p><em>(\</em>) I've just made this up but took some inspiration from the Java world where classes holding only business logic are called POJOs (Plain Old Java Objects).*</p>
<p>Here's the definition of <code>WC</code>, a module that holds the logic to perform a subset of features offered by <code>wc</code>, specifically it counts graphemes, words and lines.</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">WC</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run</span></span>(args) <span class="hljs-keyword">do</span>
    args
    |&gt; parse_options()
    |&gt; execute()
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">parse_options</span></span>(args) <span class="hljs-keyword">do</span>
    OptionParser.parse(args,
      <span class="hljs-symbol">aliases:</span> [<span class="hljs-symbol">l:</span> <span class="hljs-symbol">:lines</span>, <span class="hljs-symbol">w:</span> <span class="hljs-symbol">:words</span>, <span class="hljs-symbol">c:</span> <span class="hljs-symbol">:chars</span>],
      <span class="hljs-symbol">switches:</span> [<span class="hljs-symbol">chars:</span> <span class="hljs-symbol">:boolean</span>, <span class="hljs-symbol">words:</span> <span class="hljs-symbol">:boolean</span>, <span class="hljs-symbol">lines:</span> <span class="hljs-symbol">:boolean</span>]
    )
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">execute</span></span>(options) <span class="hljs-keyword">do</span>
    {file, opts} =
      <span class="hljs-keyword">case</span> options <span class="hljs-keyword">do</span>
        {opts, [], _} -&gt;
          {<span class="hljs-symbol">:stdio</span>, opts}

        {opts, [file | _], _} -&gt;
          {file, opts}
      <span class="hljs-keyword">end</span>

    <span class="hljs-keyword">case</span> read_file(file) <span class="hljs-keyword">do</span>
      {<span class="hljs-symbol">:ok</span>, content} -&gt;
        content
        |&gt; count_content()
        |&gt; print_results(file, opts)

      {<span class="hljs-symbol">:error</span>, <span class="hljs-symbol">:file_not_found</span>} -&gt;
        IO.puts(<span class="hljs-string">"File not found: <span class="hljs-subst">#{file}</span>"</span>)
        System.halt(<span class="hljs-number">1</span>)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-variable">@default_opts</span> [<span class="hljs-symbol">lines:</span> <span class="hljs-keyword">true</span>, <span class="hljs-symbol">words:</span> <span class="hljs-keyword">true</span>, <span class="hljs-symbol">chars:</span> <span class="hljs-keyword">true</span>]

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">print_results</span></span>(results, file, []) <span class="hljs-keyword">do</span>
    print_results(results, file, <span class="hljs-variable">@default_opts</span>)
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">print_results</span></span>(results, file, opts) <span class="hljs-keyword">do</span>
    result =
      Enum.reduce(<span class="hljs-variable">@default_opts</span>, <span class="hljs-string">""</span>, <span class="hljs-keyword">fn</span> {key, _}, acc -&gt;
        if opts[key] <span class="hljs-keyword">do</span>
          acc &lt;&gt; <span class="hljs-string">"\t<span class="hljs-subst">#{results[key]}</span>"</span>
        else
          acc
        <span class="hljs-keyword">end</span>
      <span class="hljs-keyword">end</span>)

    if file == <span class="hljs-symbol">:stdio</span> <span class="hljs-keyword">do</span>
      IO.puts(result &lt;&gt; <span class="hljs-string">" "</span> &lt;&gt; <span class="hljs-string">"\n"</span>)
    else
      IO.puts(result &lt;&gt; <span class="hljs-string">" "</span> &lt;&gt; file &lt;&gt; <span class="hljs-string">"\n"</span>)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">count_content</span></span>(content) <span class="hljs-keyword">do</span>
    content
    |&gt; String.graphemes()
    |&gt; Enum.reduce(%{<span class="hljs-symbol">lines:</span> 0, <span class="hljs-symbol">words:</span> 0, <span class="hljs-symbol">chars:</span> 0}, <span class="hljs-keyword">fn</span> char, acc -&gt;
      <span class="hljs-keyword">cond</span> <span class="hljs-keyword">do</span>
        char == <span class="hljs-string">"\n"</span> -&gt;
          %{acc | <span class="hljs-symbol">lines:</span> acc.lines + <span class="hljs-number">1</span>, <span class="hljs-symbol">chars:</span> acc.chars + <span class="hljs-number">1</span>, <span class="hljs-symbol">words:</span> acc.words + <span class="hljs-number">1</span>}

        char <span class="hljs-keyword">in</span> [<span class="hljs-string">" "</span>, <span class="hljs-string">"\t"</span>] -&gt;
          %{acc | <span class="hljs-symbol">words:</span> acc.words + <span class="hljs-number">1</span>, <span class="hljs-symbol">chars:</span> acc.chars + <span class="hljs-number">1</span>}

        <span class="hljs-keyword">true</span> -&gt;
          %{acc | <span class="hljs-symbol">chars:</span> acc.chars + <span class="hljs-number">1</span>}
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>)
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">read_file</span></span>(<span class="hljs-symbol">:stdio</span>) <span class="hljs-keyword">do</span>
    {<span class="hljs-symbol">:ok</span>, IO.read(<span class="hljs-symbol">:stdio</span>, <span class="hljs-symbol">:all</span>)}
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">read_file</span></span>(file) <span class="hljs-keyword">do</span>
    if File.exists?(file) <span class="hljs-keyword">do</span>
      File.read(file)
    else
      {<span class="hljs-symbol">:error</span>, <span class="hljs-symbol">:file_not_found</span>}
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Here's a summary of its features:</p>
<ul>
<li><p>Supports <code>-l</code> to count lines, <code>-w</code> to count words and <code>-c</code> to count graphemes.</p>
</li>
<li><p>The first argument after the options should be a path to an existing file.</p>
</li>
<li><p>When no file is provided it reads from <code>stdin</code>.</p>
</li>
<li><p>When no option is provided it assumes the caller wants all stats (all switches are on).</p>
</li>
<li><p>Returns an error code of <code>1</code> when the file doesn't exist and <code>0</code> if the execution was successful.</p>
</li>
</ul>
<p><em>Note: This is a naive implementation that takes some shortcuts to simplify the code for readability while still having some utility when running some examples.</em></p>
<h1 id="heading-implementations">Implementations</h1>
<p>For testing purposes let's create a file named <code>sample.txt</code> with the following content:</p>
<pre><code class="lang-bash">This is one
simple text
file
1234
end line is this
</code></pre>
<p>From here on we'll focus only on the differences of each alternative. The full code can be found in <a target="_blank" href="https://github.com/brewingelixir/cli-apps-in-elixir-series">this Github repo</a>.</p>
<p>Also, will be using the <code>$</code> character before a shell command to indicate it runs as a non-root user, but most importantly to differentiate a command from its output within the same code block.</p>
<h2 id="heading-elixir-scripts">Elixir Scripts</h2>
<pre><code class="lang-elixir"><span class="hljs-comment"># Assume the previous WC module is included here. E.g.</span>
<span class="hljs-comment"># defmodule WC do</span>
<span class="hljs-comment"># ...</span>

args = System.argv()
WC.run(args)
</code></pre>
<p>Let's call this file <code>wc.exs</code> and run a few examples:</p>
<ol>
<li><h4 id="heading-default-run">Default run</h4>
</li>
</ol>
<pre><code class="lang-bash">$ elixir wc.exs sample.txt
    5    11    51 sample.txt
</code></pre>
<ol>
<li>Use a CLI pipe</li>
</ol>
<pre><code class="lang-bash">$ cat sample.txt | elixir wc.exs
    5    11    51 sample.txt
</code></pre>
<ol>
<li>Pass specific parameters</li>
</ol>
<pre><code class="lang-bash">$ elixir wc.exs -l sample.txt
    5 sample.txt
</code></pre>
<p>Here you can see how the script gets interpreted by the <code>elixir</code> cli app and passes its arguments by taking everything after the <code>wc.exs</code> file. Notice how <code>elixir</code> needs to be installed as well as having the source code to run the app.</p>
<h2 id="heading-mix-run">Mix Run</h2>
<p>Once the project starts requiring more structure and code distribution the defacto standard tool to use is Mix. So let's create an app using <code>mix</code> and reuse <code>wc.exs</code> by promoting to a <code>.ex</code> file. Also copy the <code>sample.txt</code> file within the project only for convenience.</p>
<pre><code class="lang-bash">mix new app1
cp wc.exs app1/lib/wc.ex
cp sample.txt app1/
<span class="hljs-built_in">cd</span> app1
</code></pre>
<p>You should edit <code>app1/lib/wc.ex</code> by removing the last two lines and placing them in a new file called <code>run.exs</code> :</p>
<pre><code class="lang-elixir">args = System.argv()
WC.run(args)
</code></pre>
<p>Now let's run the application:</p>
<pre><code class="lang-bash">$ mix run run.exs sample.txt
    5    11    51 sample.txt
</code></pre>
<p>Awesome! You can leverage Mix features to easily organize and improve your projects. You still need the source code to run it this way but this is a quick way to run scripts from a Mix project. Let's improve this by using Mix releases.</p>
<h2 id="heading-mix-releases">Mix Releases</h2>
<p>Create another mix project like you did before but call it <code>app2</code> to have a fresh start.</p>
<pre><code class="lang-bash">mix new app2
cp wc.exs app2/lib/wc.ex
cp sample.txt app2/
<span class="hljs-built_in">cd</span> app2
</code></pre>
<p>Remove the last 2 lines of <code>wc.ex</code> as before and create a module under <code>lib/cli.ex</code> with the following content:</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">CLI</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run</span></span> <span class="hljs-keyword">do</span>
    args = System.argv()
    WC.run(args)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>This module will be the starting point for the app.</p>
<p>Next, you need to configure the project. For demo purposes, you'll create a tarball file of the project to be able to distribute it as a single file. So let's edit <code>mix.exs</code> and add:</p>
<pre><code class="lang-elixir"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">project</span></span> <span class="hljs-keyword">do</span>
    [
      ...
      <span class="hljs-symbol">releases:</span> releases()
    ]
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">releases</span></span> <span class="hljs-keyword">do</span>
    [
      <span class="hljs-symbol">app2:</span> [
        <span class="hljs-symbol">include_executables_for:</span> [<span class="hljs-symbol">:unix</span>],
        <span class="hljs-symbol">applications:</span> [<span class="hljs-symbol">runtime_tools:</span> <span class="hljs-symbol">:permanent</span>],
        <span class="hljs-symbol">steps:</span> [<span class="hljs-symbol">:assemble</span>, <span class="hljs-symbol">:tar</span>]
      ]
    ]
  <span class="hljs-keyword">end</span>
</code></pre>
<p>To build a release run:</p>
<pre><code class="lang-elixir">MIX_ENV=prod mix release
</code></pre>
<p>We provided <code>MIX_ENV=prod</code> to build a release optimized for production use. If you don't pass the environment variable it will use <code>dev</code> by default.</p>
<p>The app is ready. Let's use <code>eval</code> and pass the <code>Module.Function</code> as the first argument and the rest will be provided to the CLI app as its arguments.</p>
<pre><code class="lang-bash">$ _build/prod/rel/app2/bin/app2 <span class="hljs-built_in">eval</span> <span class="hljs-string">"CLI.run"</span> -l sample.txt
    5 sample.txt
</code></pre>
<p>Even <code>stdin</code> will work:</p>
<pre><code class="lang-bash">$ cat sample.txt | _build/dev/rel/app2/bin/app2 <span class="hljs-built_in">eval</span> <span class="hljs-string">"CLI.run"</span> -lw
    5    11
</code></pre>
<p>Note: There's no filename in the output because it uses <code>stdin</code> as the source of information to parse.</p>
<p>This is all great and you can find the tarball containing the CLI app in <code>_build/prod/app2-0.1.0.tar.gz</code>. However, the person who will run this in their host still needs to uncompress and untar it (i.e. <code>tar xvzf _build/prod/app2-0.1.0.tar.gz</code>) to use it. In other words it isn't a single executable that you can pass around.</p>
<p>Let's check the next two options to address this final limitation while maintaining all the great features you collected so far.</p>
<h2 id="heading-escript">Escript</h2>
<p>Once again create a new project and reuse <code>wc.exs</code> like you did so far:</p>
<pre><code class="lang-bash">mix new app3
cp wc.exs app3/lib/wc.ex
cp sample.txt app3/
<span class="hljs-built_in">cd</span> app3
</code></pre>
<p><em>Note: Remember to remove the last 2 lines used to execute the module's function.</em></p>
<p>Next, set up the project to use <code>escript</code> and instruct which module should be used to kick off the app. Modify <code>mix.exs</code> to include:</p>
<pre><code class="lang-elixir">  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">project</span></span> <span class="hljs-keyword">do</span>
    [
      ...
      <span class="hljs-symbol">escript:</span> escript()
    ]
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">escript</span></span> <span class="hljs-keyword">do</span>
    [<span class="hljs-symbol">main_module:</span> CLI]
  <span class="hljs-keyword">end</span>
</code></pre>
<p>Create a file under <code>lib/cli.ex</code> with:</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">CLI</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span></span>(args) <span class="hljs-keyword">do</span>
    <span class="hljs-comment"># No need to call System.argv() as it is provided by escript</span>
    <span class="hljs-comment"># as an argument to this function</span>
    WC.run(args)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>To build the project use the <code>escript.build</code> task:</p>
<pre><code class="lang-bash">$ MIX_ENV=prod mix escript.build
Generated app3 app
Generated escript app3 with MIX_ENV=prod
</code></pre>
<p>Success! You have a single binary file representing your CLI app. Let's check its type and then test it!</p>
<pre><code class="lang-bash">$ file app3
app3: a /usr/bin/env escript script executable (binary data)
</code></pre>
<pre><code class="lang-bash">./app3 sample.txt
    5    11    51 sample.txt
</code></pre>
<p>Very cool! This single file can be easily distributed as long as the limitations described in <a target="_blank" href="https://brewingelixir.com/cli-apps-in-elixir-part-1">Part 1</a> don't affect your use case. In case some do then prepare your hot sauce because you'll need it for the next tasty solution 🔥🌯.</p>
<h2 id="heading-burrito">Burrito</h2>
<p>Until now all alternatives were part of the standard Elixir distribution but thanks to the great work of the community and Burrito maintainers we now have a full-featured solution to build and distribute single binary apps for Elixir: <a target="_blank" href="https://github.com/burrito-elixir/burrito">https://github.com/burrito-elixir/burrito</a></p>
<p>Burrito requires <a target="_blank" href="https://ziglang.org/">Zig</a> to be installed as well as <code>xz</code> so make sure you have them installed:</p>
<pre><code class="lang-bash">$ whereis xz
$
</code></pre>
<p>Let's set up a fresh app and reuse the <code>WC</code> module:</p>
<pre><code class="lang-bash">mix new app4
cp wc.exs app4/lib/wc.ex
cp sample.txt app4/
<span class="hljs-built_in">cd</span> app4
</code></pre>
<p>Burrito is an external dependency so you'll need to add it to <code>mix.exs</code> under <code>deps</code> :</p>
<pre><code class="lang-elixir">  <span class="hljs-function"><span class="hljs-keyword">defp</span> <span class="hljs-title">deps</span></span> <span class="hljs-keyword">do</span>
    [
      {<span class="hljs-symbol">:burrito</span>, <span class="hljs-symbol">github:</span> <span class="hljs-string">"burrito-elixir/burrito"</span>}
    ]
  <span class="hljs-keyword">end</span>
</code></pre>
<p>And then fetch the dependency package using mix:</p>
<pre><code class="lang-bash">mix deps.get
</code></pre>
<p>Now let's set it up in <code>mix.exs</code></p>
<pre><code class="lang-elixir"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">project</span></span> <span class="hljs-keyword">do</span>
  [
    <span class="hljs-comment"># ... other project configuration</span>
    <span class="hljs-symbol">releases:</span> releases()
  ]
<span class="hljs-keyword">end</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">releases</span></span> <span class="hljs-keyword">do</span>
  [
    <span class="hljs-symbol">app4:</span> [
      <span class="hljs-symbol">steps:</span> [<span class="hljs-symbol">:assemble</span>, &amp;Burrito.wrap/<span class="hljs-number">1</span>],
      <span class="hljs-symbol">burrito:</span> [
        <span class="hljs-symbol">targets:</span> [
          <span class="hljs-symbol">macos:</span> [<span class="hljs-symbol">os:</span> <span class="hljs-symbol">:darwin</span>, <span class="hljs-symbol">cpu:</span> <span class="hljs-symbol">:x86_64</span>],
          <span class="hljs-symbol">linux:</span> [<span class="hljs-symbol">os:</span> <span class="hljs-symbol">:linux</span>, <span class="hljs-symbol">cpu:</span> <span class="hljs-symbol">:x86_64</span>]
        ]
      ]
    ]
  ]
<span class="hljs-keyword">end</span>
</code></pre>
<p>Sweet! Burrito leverages Mix releases which means you get all their benefits plus the ones from Burrito.</p>
<p>Next You need to define a starting point for the app, so edit <code>mix.exs</code> but this time add the following change to it:</p>
<pre><code class="lang-elixir"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">application</span></span> <span class="hljs-keyword">do</span>
  [
    ...
    <span class="hljs-symbol">mod:</span> {CLI, []}
  ]
<span class="hljs-keyword">end</span>
</code></pre>
<p><code>CLI</code> is just a module name so let's create it under <code>lib/cli.ex</code></p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">CLI</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">use</span> Application

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">start</span></span>(_type, _args) <span class="hljs-keyword">do</span>
    args = Burrito.Util.Args.get_arguments()
    WC.run(args)

    System.halt(0)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>To build the artifact let's run:</p>
<pre><code class="lang-bash">MIX_ENV=prod mix release
</code></pre>
<p>The targets can be found under the <code>burrito_out</code> directory within the current project. Without specifying a target you end up building all of them listed in your mix configuration file.</p>
<p>To test the app run:</p>
<pre><code class="lang-bash">$ ./burrito_out/app4_macos -l sample.txt
    5 sample.txt
</code></pre>
<p>Awesome! Let's check the file's type:</p>
<pre><code class="lang-bash">$ file burrito_out/*
burrito_out/app4_linux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, <span class="hljs-keyword">for</span> GNU/Linux 2.0.0, stripped
burrito_out/app4_macos: Mach-O 64-bit executable x86_64
</code></pre>
<p>Beautiful! That looks like executables for specific OS and architectures. For more details check out the <a target="_blank" href="https://github.com/burrito-elixir/burrito#preparation-and-requirements">Preparation and Requirements section</a> of their readme.</p>
<p>🚨 Important: Burrito will install the app based on its mix version. If you perform a change to your code and run <code>mix release</code> without uninstalling the app you'll get the previous version executed not the current one. So make sure you either:</p>
<p>a. Bump the version in <code>mix.exs</code></p>
<p>b. Uninstall the current version: <code>burrito_out/app4_macos maintenance uninstall</code></p>
<p>Hope this last tip saves you some time or headaches 😉</p>
<h1 id="heading-summary">Summary</h1>
<p>All options are valid and useful but in general, <strong>Escript</strong> or <strong>Burrito</strong> solutions are what you want to use when building non-trivial single binaries CLI apps in Elixir. But if in doubt then start with a single <code>.exs</code> file and see how far you can get until you start needing more sophisticated solutions.</p>
<p>This concludes the second part of this series. Hope you enjoyed it and found it useful! 🍺</p>
<div class="hn-embed-widget" id="convertkit"></div>]]></content:encoded></item><item><title><![CDATA[CLI apps in Elixir. Part 1]]></title><description><![CDATA[From a programmer's perspective, one of the simplest and most flexible ways to interact with a computer is through a terminal. By only using plain text to get input and provide outputs, the program's interface not only becomes easy to reason about bu...]]></description><link>https://brewingelixir.com/cli-apps-in-elixir-part-1</link><guid isPermaLink="true">https://brewingelixir.com/cli-apps-in-elixir-part-1</guid><category><![CDATA[escript]]></category><category><![CDATA[Elixir]]></category><category><![CDATA[cli]]></category><category><![CDATA[command line]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Scripting]]></category><dc:creator><![CDATA[Brewing Elixir]]></dc:creator><pubDate>Wed, 22 Nov 2023 21:46:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1700605916077/b6e0a60b-5159-4be0-8e69-8677e8ac0ca4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>From a programmer's perspective, one of the simplest and most flexible ways to interact with a computer is through a terminal. By only using plain text to get input and provide outputs, the program's interface not only becomes easy to reason about but also simple to reuse by other programs. This last feature is one of the main ideas behind the <a target="_blank" href="http://www.catb.org/~esr/writings/taoup/html/ch01s06.html">Unix Philosophy</a>, specifically the rules of <em>Modularity and Composition</em>.</p>
<p>These types of programs are called <strong>command-line interface</strong> applications or <strong>CLI</strong> apps. They get started from a Shell process (e.g. Bash) through a Terminal (usually a virtual one like iTerm). In the case of Elixir, there are a couple of ways to kick off a CLI app but every one of them ends up creating a Beam process.</p>
<p>To better understand the default interfaces here's a high-level diagram of the CLI app running:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700605070339/2d3d7f51-4f6a-4c7c-97c6-34516500a21d.png" alt class="image--center mx-auto" /></p>
<p>From the diagram we can see how the process (our program being executed) can interact with the operator (the person executing the CLI app) by using the <code>stdin</code>, <code>stdout</code> and <code>stderr</code>. The first one is used to get data into the process by either keyboard typing or another file stream, and the other two (<code>stdout</code> and <code>stderr)</code> outputs data. Here the programmer decides which stream to use for each case which usually end up having the <code>stderr</code> for general errors and <code>stdout</code> for anything else.</p>
<p>Thanks to the Shell we can pipe (connect) a process's <code>stdout</code> to another process's <code>stdin</code> to create a processing pipeline and through composition complete more complex tasks.</p>
<p>The are other ways to interact with the process that range from simple and common like OS signals (e.g. when the Shell gets a <code>ctrl-c</code> it sends a TERM signal to the foreground job) to more sophisticated <a target="_blank" href="https://en.wikipedia.org/wiki/Inter-process_communication">IPC mechanisms</a>. With the latter, we can interact with other processes in the same host or even remote ones allowing our program to perform more types of tasks.</p>
<h1 id="heading-cli-apps-requirements">CLI apps requirements</h1>
<p>Now that we have a mental model of what a CLI program running from a Shell looks like we can start thinking about common CLI requirements to control what the process will do.</p>
<ul>
<li><p><strong>Get initial parameters</strong>: When a program starts it needs initial parameters to decide how to run (or not). These parameters can be provided by CLI command arguments, system environment variables, configuration files or any other mechanism programmed into the app (e.g. pull configuration from a well-known configuration server).</p>
</li>
<li><p><strong>Run in the foreground</strong>: CLI tools are normally run by human operators (or indirectly via another script or program using it) and are normally expected to run in the foreground.</p>
</li>
<li><p><strong>Prompt input interactively</strong>: Some CLI applications might need to get input interactively as they progress through their tasks. E.g. ask for <code>root</code> password to do sensitive tasks or ask for further configuration options.</p>
</li>
<li><p>I<strong>nteract with the File System</strong>: Depending on the application, having access to the FS is important because large amounts of data are usually faster and more convenient to handle in files than using the standard streams.</p>
</li>
<li><p><strong>Interact with other processes</strong>: With the shell or other running processes within the same host or remote ones.</p>
</li>
</ul>
<p>This is not an exhaustive list and each application can require a different set of features to achieve its goals. In the next section we'll explore what tools exist in the Elixir ecosystem, what each option offers and what are their main downsides.</p>
<h1 id="heading-tools-to-build-clis-app-in-elixir">Tools to build CLIs app in Elixir</h1>
<p>The main focus of Erlang/Elixir and specifically the Beam is building and running highly concurrent, fault-tolerant distributed systems, not CLI applications. However, that doesn't mean it doesn't offer a good starting point to cover use cases where a CLI app is needed.<br />In this section we'll go from the default solutions included by Elixir/Erlang to external projects that can be used to build full-featured CLI apps.</p>
<h2 id="heading-elixir-scripts">Elixir Scripts</h2>
<p>The simplest tool to get the job done is <em>Elixir Scripts</em>. These are <code>.exs</code> files that are interpreted by the <code>elixir</code> command. E.g.</p>
<pre><code class="lang-bash">elixir my_cli.exs
</code></pre>
<p>It doesn't need any project structure (i.e. Mix project) and thanks to the <a target="_blank" href="https://hexdocs.pm/mix/Mix.html#install/2">Mix.install/2</a> addition (since Elixir 1.12) it can install external projects as part of the script execution. For inspiration check out <a target="_blank" href="https://github.com/wojtekmach/mix_install_examples/tree/main">this repo</a>.</p>
<p>The simplicity comes with a cost, that could be acceptable depending on the use case, but in general they impose the following restrictions and limitations:</p>
<p>a. Needs the source code to run</p>
<p>b. It needs <code>elixir</code> to be installed on the host</p>
<p>c. Code organization doesn't scale well</p>
<p>d. Doesn't leverage Mix which is the default build tool for Elixir projects which provides tasks for creating, compiling, and testing Elixir projects, managing its dependencies, and much more.</p>
<p>One good use case for this type of solution is one-off tasks where the person who wrote the code is the same one who would execute it.</p>
<h2 id="heading-mix-run">Mix Run</h2>
<p>To easily enhance <code>exs</code> scripts we can create a Mix project and place the script within the project to let it use the modules defined in the project. From here it is easy to add dependencies, set up supervision trees, organize code in modules, add unit tests and much more.</p>
<p>To run the script from the context we just need to execute:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> project_name
mix run my_cli.exs
</code></pre>
<p><code>my_cli.exs</code> has access to modules defined under <code>lib/</code> and all dependencies defined in the project.</p>
<p>This alternative has solved downsides <code>c.</code> and <code>d.</code> from the Elixir scripts but it still requires <code>a.</code> and <code>b.</code> . But don't despair, this is something we can address with some of the alternatives to be described.</p>
<h2 id="heading-mix-releases">Mix Releases</h2>
<p>A <em>release</em> is a self-contained artifact that contains compiled code for the current project.</p>
<p>From the docs:</p>
<blockquote>
<p>Once a release is assembled, it can be packaged and deployed to a target, as long as the target runs on the same operating system (OS) distribution and version as the machine running the <a target="_blank" href="https://hexdocs.pm/mix/1.12.3/Mix.Tasks.Release.html#content"><code>mix release</code></a> command.</p>
</blockquote>
<p>This means they don't even require Erlang or Elixir in the running hosts because it includes the Erlang VM and its runtime by default. They don't even require the source code by default which can be convenient for some cases.</p>
<p>This is great! We've eliminated all downsides from <em>Elixir Scripts</em> but there's one limitation to be aware of: releases are optimized to run Elixir/Beam applications, not CLI ones. This means they work great as <a target="_blank" href="https://en.wikipedia.org/wiki/Daemon_(computing)">daemons</a> but have limited support for foreground CLI apps. There are two workarounds to slightly overcome these limitations:</p>
<p>a. Eval a function:</p>
<pre><code class="lang-bash">bin/RELEASE_NAME <span class="hljs-built_in">eval</span> <span class="hljs-string">"IO.puts(:hello)"</span>
</code></pre>
<p>b. Call a remote function:</p>
<pre><code class="lang-bash">bin/RELEASE_NAME rpc <span class="hljs-string">"IO.puts(:hello)"</span>
</code></pre>
<p>In both cases, we can leverage all the benefits from the Beam and the environment where the process is running but <code>stdin</code>, <code>stdout</code> and <code>stderr</code> as well as the command arguments can become a bit challenging to work with. Also, the <code>eval</code> function doesn't start any application within the program by default, and the <code>rpc</code> function requires the release to be running to be able to execute successfully. For more details please check out <a target="_blank" href="https://hexdocs.pm/mix/1.12.3/Mix.Tasks.Release.html#module-one-off-commands-eval-and-rpc">the docs</a>.</p>
<h2 id="heading-escript">Escript</h2>
<p>Similar limitations already existed for CLI apps even before Mix Releases or even Elixir existed! The solution is called <code>Escript</code> and is <a target="_blank" href="https://www.erlang.org/doc/man/escript.html">originally available in Erlang</a>.</p>
<p>Luckily for us we can build <code>escript</code> from Mix projects to create a single, largely self-contained executable! They can run on any machine that has Erlang/OTP installed, and it doesn't require Elixir to be installed by default as Elixir is embedded as part of it. However, it does require Erlang/OTP to be installed on the host.</p>
<p>Setting up and running escript is as easy as configuring <code>mix.exs</code>, defining an entry module with a <code>main/1</code> function, and then executing the following command to build the artifact:</p>
<pre><code class="lang-bash">mix escript.build
</code></pre>
<p>From here, a single file will be available to be used as an executable. E.g. Assuming the app is called <code>example_app</code> we type:</p>
<pre><code class="lang-bash">./example_app
</code></pre>
<p>This looks perfect but it has one downside: it doesn't support projects or dependencies that need to store or read from the <code>priv</code> directory. A well-known library called <a target="_blank" href="https://github.com/lau/tzdata">tzdata</a> is one of those libraries. However, there are workarounds to overcome this limitation but it does leave the feeling we can probably do better.</p>
<h2 id="heading-burrito">Burrito</h2>
<p>To overcome most if not all limitations, and be able to produce a single binary artifact, there's a fantastic OSS library called <a target="_blank" href="https://github.com/burrito-elixir/burrito">Burrito</a>. It lets you wrap your meaty app so you can delight your CLI app users!</p>
<p>From the README.md:</p>
<blockquote>
<p>Burrito is our answer to the problem of distributing Elixir CLI applications across varied environments, where we cannot guarantee that the Erlang runtime is installed, and where we lack the permissions to install it ourselves.</p>
</blockquote>
<p>Burrito uses Mix releases so we get all their benefits as well as a self-extracting archive. It creates a native binary for macOS, Linux, and Windows (*).</p>
<p>In the next part of this series we'll explore Burrito in depth to build a complete CLI app.</p>
<p>(*) This can be configured and cross compilation depends on the build host.</p>
<h2 id="heading-honorable-mentions">Honorable mentions</h2>
<p>Even though these are not strictly speaking alternatives to build CLI apps they are mentioned here because they are alternatives to achieve some of the goals a CLI app can do. And in some cases they are better alternatives for niche cases. E.g. Mix tasks.</p>
<ul>
<li><p><strong>Mix tasks and archive</strong>: Even though these are not tools to create general-purpose scripts they are great alternatives when we need to extend mix tooling to improve our development workflow. Tasks can live within the same mix project and be reused by other programmers within the team and can also leverage archiving to install tasks globally.</p>
</li>
<li><p><strong>Livebook</strong>: yeah, you heard me right! This is a fantastic environment where you can code and run Elixir scripts, use Kino to display charts, run machine learning models, and organize the code for a human to understand it step by step just to name a few.</p>
</li>
<li><p><strong>Docker</strong>: When we want to have full control of the environment where our app will run without requiring changes to the running environment we can always count on Docker. In recent years it has become ubiquitous so any CLI app that can accept the extra delay of running its app via docker can wrap the CLI with it and distribute it as a docker image.</p>
</li>
</ul>
<h1 id="heading-coming-up-next">Coming up Next</h1>
<p>In <a target="_blank" href="https://brewingelixir.com/cli-apps-in-elixir-part-2">Part 2</a> we'll explore each tool we have described to implement a well-known CLI app to see for ourselves where the benefits and limitations of each alternative are, with the hope you'll end with enough knowledge to decide what tool fits best for your future use cases 🚀</p>
<div class="hn-embed-widget" id="convertkit"></div>]]></content:encoded></item><item><title><![CDATA[Unlocking the Power of Elixir's Enumerables]]></title><description><![CDATA[Dealing with data structures is at the core of any programming activity and high-level languages like Elixir provide well-structured constructs in the standard library to easily work with them.
In this post, we'll go through how the Enum and Stream m...]]></description><link>https://brewingelixir.com/unlocking-the-power-of-elixirs-enumerables</link><guid isPermaLink="true">https://brewingelixir.com/unlocking-the-power-of-elixirs-enumerables</guid><category><![CDATA[Elixir]]></category><category><![CDATA[programming languages]]></category><category><![CDATA[concurrency]]></category><dc:creator><![CDATA[Brewing Elixir]]></dc:creator><pubDate>Sun, 12 Nov 2023 21:03:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1699821479311/70bd2edb-c899-4b3f-a86e-3b35a1a155a7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Dealing with data structures is at the core of any programming activity and high-level languages like Elixir provide well-structured constructs in the standard library to easily work with them.</p>
<p>In this post, we'll go through how the <code>Enum</code> and <code>Stream</code> modules work with data types like <code>List</code>, <code>Map</code> and <code>Stream</code> through the use of the <code>Enumerable</code> and <code>Collectable</code> protocols to provide a batteries-included system that can also be reused and extended for other data structures.</p>
<h2 id="heading-high-level-overview">High-level overview</h2>
<p>To understand how all the pieces work together we first need to define which those pieces are:</p>
<ul>
<li><p>Enumerables: Technically speaking these are any data type that implements the <code>Enumerable</code> protocol. We can think of them as collections that share a common way of being accessed.</p>
</li>
<li><p><code>Enum</code> and <code>Stream</code> utility modules: Group functions to interact with enumerables <em>mainly</em> through the <code>Enumerable</code> and <code>Collectable</code> protocols. They have clear tradeoffs that lead to module separation.</p>
</li>
<li><p><code>List</code>, <code>Map</code>, <code>Stream</code> data structures: These are the modules defining the data types and the specific functions to work with them.</p>
</li>
</ul>
<p>From here we can organize these abstractions by separating which modules <em>use</em> the enumerable via protocol functions and which types <em>implement</em> the protocol.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699385272452/ca2e8252-6bb5-4501-9d0f-8f94bb59ba06.png" alt class="image--center mx-auto" /></p>
<p>Here we can see in the diagram how <code>Stream</code> and <code>Enum</code> utility functions don't <em>access</em> the types (<code>List</code>, <code>Map</code>, <code>Function</code>, etc) directly when dealing with them. This separation helps achieve two key extensibility benefits:</p>
<ul>
<li><p>Utility functions can be reused by <em>any</em> <code>Enumerable</code>: Which means they don't need to know more about the data type than what the protocol requires.</p>
</li>
<li><p>Any data type can implement the <code>Enumerable</code> protocol to be reusable by the Utility functions.</p>
</li>
</ul>
<p>In general, protocols were designed to achieve this separation and reusability. That means the <code>Collectable</code> protocol, which deals with traversing the data structure, also has similar separations and benefits.</p>
<p>But protocols don't need to live in isolation from each other. <code>Enumerable</code> and <code>Collectable</code> relation is well explained in the <a target="_blank" href="https://hexdocs.pm/elixir/1.16.0-rc.0/Collectable.html#module-why-collectable">docs</a>. Here's an extract of the core parts:</p>
<blockquote>
<p>The <a target="_blank" href="https://hexdocs.pm/elixir/1.16.0-rc.0/Enumerable.html"><code>Enumerable</code></a> protocol is useful to take values out of a collection. To support a wide range of values, the functions provided by the <a target="_blank" href="https://hexdocs.pm/elixir/1.16.0-rc.0/Enumerable.html"><code>Enumerable</code></a> protocol do not keep shape. It was designed to support infinite collections, resources and other structures with fixed shape.</p>
<p>The <a target="_blank" href="https://hexdocs.pm/elixir/1.16.0-rc.0/Collectable.html#content"><code>Collectable</code></a> module was designed to fill the gap left by the <a target="_blank" href="https://hexdocs.pm/elixir/1.16.0-rc.0/Enumerable.html"><code>Enumerable</code></a> protocol. If the functions in <a target="_blank" href="https://hexdocs.pm/elixir/1.16.0-rc.0/Enumerable.html"><code>Enumerable</code></a> are about taking values out, then a <a target="_blank" href="https://hexdocs.pm/elixir/1.16.0-rc.0/Collectable.html#into/1"><code>Collectable</code></a> is about collecting those values into a structure.</p>
</blockquote>
<p>To learn more about which modules implement the <code>Enumerable</code> protocol we can run <code>iex -S mix</code> from our mix project and the:</p>
<pre><code class="lang-elixir">iex&gt; Enumerable.__protocol__(<span class="hljs-symbol">:impls</span>)
{<span class="hljs-symbol">:consolidated</span>,
 [Date.Range, File.Stream, Function, GenEvent.Stream, HashDict, HashSet,
  IO.Stream, List, Map, MapSet, Range, Stream]}

iex&gt; Collectable.__protocol__(<span class="hljs-symbol">:impls</span>)
{<span class="hljs-symbol">:consolidated</span>,
 [BitString, File.Stream, HashDict, HashSet, IO.Stream, List, Map, MapSet,
  Mix.Shell]}
</code></pre>
<p>Now that we have a general understanding of the organization of enumerables we can continue with the main modules used to interact with them (besides their own module functions).</p>
<h2 id="heading-enum-and-stream">Enum and Stream</h2>
<p>Elixir defines these two modules with functions to work with enumerables and collectables interchangeably most of the time. The key difference lies in the way functions return results.</p>
<ul>
<li><p><code>Enum</code>: focuses on eager operations. This means most functions included in this module will process the collection and return the final result right away.</p>
</li>
<li><p><code>Stream</code>: operations are lazy, allowing processing functions to get chained together to process each element as needed.</p>
</li>
</ul>
<h3 id="heading-enum-eager-operations">Enum: Eager operations</h3>
<p>To better understand what eagerness implies here's a simple example with calls to <code>Enum</code> functions linked together with pipe operators.</p>
<pre><code class="lang-elixir">result = <span class="hljs-number">1</span>..<span class="hljs-number">100_000</span>
       |&gt; Enum.map(<span class="hljs-keyword">fn</span> item -&gt; item * <span class="hljs-number">10</span> <span class="hljs-keyword">end</span>)
       |&gt; Enum.filter(<span class="hljs-keyword">fn</span> item -&gt; item &gt; <span class="hljs-number">10</span> <span class="hljs-keyword">end</span>)
       |&gt; Enum.map(<span class="hljs-keyword">fn</span> item -&gt; (item + <span class="hljs-number">3</span>) / <span class="hljs-number">2</span> <span class="hljs-keyword">end</span>)
       |&gt; Enum.reduce(<span class="hljs-keyword">fn</span> item, acc -&gt; acc + item <span class="hljs-keyword">end</span>)
       |&gt; dbg()
</code></pre>
<p>Each function operates on the result of the previous call which holds the final computed result for the intermedia operation.</p>
<p>By appending <code>dbg/0</code> at the end of the pipe we can see how these intermediate lists are created.</p>
<pre><code class="lang-elixir"><span class="hljs-number">1</span>..<span class="hljs-number">100_000</span> <span class="hljs-comment">#=&gt; 1..100000</span>
|&gt; Enum.map(<span class="hljs-keyword">fn</span> item -&gt; item * <span class="hljs-number">10</span> <span class="hljs-keyword">end</span>) <span class="hljs-comment">#=&gt; [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170,</span>
 <span class="hljs-number">180</span>, <span class="hljs-number">190</span>, <span class="hljs-number">200</span>, <span class="hljs-number">210</span>, <span class="hljs-number">220</span>, <span class="hljs-number">230</span>, <span class="hljs-number">240</span>, <span class="hljs-number">250</span>, <span class="hljs-number">260</span>, <span class="hljs-number">270</span>, <span class="hljs-number">280</span>, <span class="hljs-number">290</span>, <span class="hljs-number">300</span>, <span class="hljs-number">310</span>, <span class="hljs-number">320</span>, <span class="hljs-number">330</span>,
 <span class="hljs-number">340</span>, <span class="hljs-number">350</span>, <span class="hljs-number">360</span>, <span class="hljs-number">370</span>, <span class="hljs-number">380</span>, <span class="hljs-number">390</span>, <span class="hljs-number">400</span>, <span class="hljs-number">410</span>, <span class="hljs-number">420</span>, <span class="hljs-number">430</span>, <span class="hljs-number">440</span>, <span class="hljs-number">450</span>, <span class="hljs-number">460</span>, <span class="hljs-number">470</span>, <span class="hljs-number">480</span>, <span class="hljs-number">490</span>,
 <span class="hljs-number">500</span>, ...]
|&gt; Enum.filter(<span class="hljs-keyword">fn</span> item -&gt; item &gt; <span class="hljs-number">10</span> <span class="hljs-keyword">end</span>) <span class="hljs-comment">#=&gt; [20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180,</span>
 <span class="hljs-number">190</span>, <span class="hljs-number">200</span>, <span class="hljs-number">210</span>, <span class="hljs-number">220</span>, <span class="hljs-number">230</span>, <span class="hljs-number">240</span>, <span class="hljs-number">250</span>, <span class="hljs-number">260</span>, <span class="hljs-number">270</span>, <span class="hljs-number">280</span>, <span class="hljs-number">290</span>, <span class="hljs-number">300</span>, <span class="hljs-number">310</span>, <span class="hljs-number">320</span>, <span class="hljs-number">330</span>, <span class="hljs-number">340</span>,
 <span class="hljs-number">350</span>, <span class="hljs-number">360</span>, <span class="hljs-number">370</span>, <span class="hljs-number">380</span>, <span class="hljs-number">390</span>, <span class="hljs-number">400</span>, <span class="hljs-number">410</span>, <span class="hljs-number">420</span>, <span class="hljs-number">430</span>, <span class="hljs-number">440</span>, <span class="hljs-number">450</span>, <span class="hljs-number">460</span>, <span class="hljs-number">470</span>, <span class="hljs-number">480</span>, <span class="hljs-number">490</span>, <span class="hljs-number">500</span>,
 <span class="hljs-number">510</span>, ...]
|&gt; Enum.map(<span class="hljs-keyword">fn</span> item -&gt; (item + <span class="hljs-number">3</span>) / <span class="hljs-number">2</span> <span class="hljs-keyword">end</span>) <span class="hljs-comment">#=&gt; [11.5, 16.5, 21.5, 26.5, 31.5, 36.5, 41.5, 46.5, 51.5, 56.5, 61.5, 66.5, 71.5,</span>
 <span class="hljs-number">76.5</span>, <span class="hljs-number">81.5</span>, <span class="hljs-number">86.5</span>, <span class="hljs-number">91.5</span>, <span class="hljs-number">96.5</span>, <span class="hljs-number">101.5</span>, <span class="hljs-number">106.5</span>, <span class="hljs-number">111.5</span>, <span class="hljs-number">116.5</span>, <span class="hljs-number">121.5</span>, <span class="hljs-number">126.5</span>, <span class="hljs-number">131.5</span>,
 <span class="hljs-number">136.5</span>, <span class="hljs-number">141.5</span>, <span class="hljs-number">146.5</span>, <span class="hljs-number">151.5</span>, <span class="hljs-number">156.5</span>, <span class="hljs-number">161.5</span>, <span class="hljs-number">166.5</span>, <span class="hljs-number">171.5</span>, <span class="hljs-number">176.5</span>, <span class="hljs-number">181.5</span>, <span class="hljs-number">186.5</span>,
 <span class="hljs-number">191.5</span>, <span class="hljs-number">196.5</span>, <span class="hljs-number">201.5</span>, <span class="hljs-number">206.5</span>, <span class="hljs-number">211.5</span>, <span class="hljs-number">216.5</span>, <span class="hljs-number">221.5</span>, <span class="hljs-number">226.5</span>, <span class="hljs-number">231.5</span>, <span class="hljs-number">236.5</span>, <span class="hljs-number">241.5</span>,
 <span class="hljs-number">246.5</span>, <span class="hljs-number">251.5</span>, <span class="hljs-number">256.5</span>, ...]
|&gt; Enum.reduce(<span class="hljs-keyword">fn</span> item, acc -&gt; acc + item <span class="hljs-keyword">end</span>) <span class="hljs-comment">#=&gt; 25000399993.5</span>
</code></pre>
<p>Now imagine having to deal with collections of millions of records. From there is easy to imagine how working with large lists and multiple <em>eager</em> calls can lead to high memory usage. And even more on multitenant systems.</p>
<p>A good rule of thumb when working with <code>Enum</code> is: Use it by default unless you know you'll deal with <em>very large</em> collections or memory consumption gets affected by the way long pipelines transform data. In that case, profile your application and evaluate how using lazy operations via the <code>Stream</code> module functions behave.</p>
<p>For a complete list of <code>Enum</code> functions please check the <a target="_blank" href="https://hexdocs.pm/elixir/1.16/enum-cheat.html">cheatshet</a> and the <a target="_blank" href="https://hexdocs.pm/elixir/1.16.0-rc.0/Enumerable.html#summary">module's docs</a>.</p>
<h3 id="heading-stream-lazy-operations">Stream: lazy operations</h3>
<p><code>Stream</code> module functions are lazy and exist to solve some of the problems <code>Enum</code> creates due to its eager nature. Besides that it also provides other features not available in <code>Enum</code> like infinite collections.</p>
<p>To see the difference in action we'll take the previous <code>Enum</code>example and rewrite it using <code>Stream</code> functions:</p>
<pre><code class="lang-elixir">result = <span class="hljs-number">1</span>..<span class="hljs-number">100_000</span>
       |&gt; Stream.map(<span class="hljs-keyword">fn</span> item -&gt; item * <span class="hljs-number">10</span> <span class="hljs-keyword">end</span>)
       |&gt; Stream.filter(<span class="hljs-keyword">fn</span> item -&gt; item &gt; <span class="hljs-number">10</span> <span class="hljs-keyword">end</span>)
       |&gt; Stream.map(<span class="hljs-keyword">fn</span> item -&gt; (item + <span class="hljs-number">3</span>) / <span class="hljs-number">2</span> <span class="hljs-keyword">end</span>)
       |&gt; Enum.reduce(<span class="hljs-keyword">fn</span> item, acc -&gt; acc + item <span class="hljs-keyword">end</span>)
       |&gt; dbg()
</code></pre>
<p>The final function needs to be an eager one (From the <code>Enum</code> module or <code>Stream.run/1</code>) to execute the stream.</p>
<p>The output of each pipe operation can be visualized here:</p>
<pre><code class="lang-elixir"><span class="hljs-number">1</span>..<span class="hljs-number">100_000</span> <span class="hljs-comment">#=&gt; 1..100000</span>
|&gt; Stream.map(<span class="hljs-keyword">fn</span> item -&gt; item * <span class="hljs-number">10</span> <span class="hljs-keyword">end</span>) <span class="hljs-comment">#=&gt; #Stream&lt;[enum: 1..100000, funs: [#Function&lt;48.53678557/1 in Stream.map/2&gt;]]&gt;</span>
|&gt; Stream.filter(<span class="hljs-keyword">fn</span> item -&gt; item &gt; <span class="hljs-number">10</span> <span class="hljs-keyword">end</span>) <span class="hljs-comment">#=&gt; #Stream&lt;[</span>
  <span class="hljs-symbol">enum:</span> <span class="hljs-number">1</span>..<span class="hljs-number">100000</span>,
  <span class="hljs-symbol">funs:</span> [<span class="hljs-comment">#Function&lt;48.53678557/1 in Stream.map/2&gt;,</span>
   <span class="hljs-comment">#Function&lt;40.53678557/1 in Stream.filter/2&gt;]</span>
]&gt;
|&gt; Stream.map(<span class="hljs-keyword">fn</span> item -&gt; (item + <span class="hljs-number">3</span>) / <span class="hljs-number">2</span> <span class="hljs-keyword">end</span>) <span class="hljs-comment">#=&gt; #Stream&lt;[</span>
  <span class="hljs-symbol">enum:</span> <span class="hljs-number">1</span>..<span class="hljs-number">100000</span>,
  <span class="hljs-symbol">funs:</span> [<span class="hljs-comment">#Function&lt;48.53678557/1 in Stream.map/2&gt;,</span>
   <span class="hljs-comment">#Function&lt;40.53678557/1 in Stream.filter/2&gt;,</span>
   <span class="hljs-comment">#Function&lt;48.53678557/1 in Stream.map/2&gt;]</span>
]&gt;
|&gt; Enum.reduce(<span class="hljs-keyword">fn</span> item, acc -&gt; acc + item <span class="hljs-keyword">end</span>) <span class="hljs-comment">#=&gt; 25000399993.5</span>

<span class="hljs-number">25000399993.5</span>
</code></pre>
<p>We can see how there aren't any intermediate collection results on each pipe operation but we still get the same result as before. You probably noticed what is going on already so there's no need to explain that streams are like chainable functions that get executed (in order) for each element of the original enumerable 😉.</p>
<p>That's all very cool but <code>Stream</code> shines when it comes to:</p>
<ul>
<li><p>Running a function concurrently on each element in an enumerable: By using <code>Task.async_stream/2</code>, <code>Task.Supervisor.async_stream/6</code> or <code>Task.Supervisor.async_stream_nolink/6</code> depending on the application requirements.</p>
</li>
<li><p>Need to emit a sequence of values from a resource: By using <code>Stream.iterate/2</code>, <code>Stream.resource/3</code> , <code>Stream.unfold/2</code> and other we can compute or get values to create our stream.</p>
</li>
</ul>
<p>For a complete list of functions please check the <a target="_blank" href="https://hexdocs.pm/elixir/1.16.0-rc.0/Stream.html">module's docs</a>.</p>
<h2 id="heading-use-cases">Use cases</h2>
<p>When it comes to choosing when to use <code>Enum</code> vs <code>Streams</code> a good rule of thumb is start using <code>Enum</code> by default but evaluate <code>Stream</code> when collections are <em>large</em> and pipelines are <em>long</em>. Nevertheless, there are also cases where <code>Stream</code> is the best initial choice and we'll see 3 cases where they mostly are.</p>
<h3 id="heading-case-1-file-processing">Case 1: File processing</h3>
<p>A very common use case for Streams involves: reading a file, doing some processing per line and finally writing the results to another one.</p>
<pre><code class="lang-elixir">orig_file = <span class="hljs-string">"/path/to/file"</span>
dest_file = <span class="hljs-string">"/path/to/other/file"</span>

File.stream!(orig_file)
|&gt; Stream.map(&amp;String.replace(&amp;<span class="hljs-number">1</span>, <span class="hljs-string">"#"</span>, <span class="hljs-string">"%"</span>))
|&gt; Stream.into(File.stream!(dest_file))
|&gt; Stream.run()
</code></pre>
<p>We can use this template to process log files, jsonsd, csv, tsv an any other line-oriented file. <code>File.stream!/3</code> also accepts modes to instruct the stream to uncompress or compress the stream which comes in very handy to deal with even larger files at the cost of CPU cycles.</p>
<h3 id="heading-case-2-processing-enumerables-concurrently">Case 2: Processing enumerables concurrently</h3>
<p>Sometimes our application can leverage concurrency by splitting operations and running them potentially in parallel. With streams, we can collect initial parameters into an enumerable and pass them to <code>Task.async_stream/3</code> to let it process them.</p>
<pre><code class="lang-elixir">[<span class="hljs-string">"resource1"</span>, <span class="hljs-string">"resource2"</span>, <span class="hljs-string">"resource3"</span>]
|&gt; Task.async_stream(<span class="hljs-keyword">fn</span> item -&gt;
  fetch(item)
<span class="hljs-keyword">end</span>)
|&gt; Enum.to_list()
</code></pre>
<p>Here it will process 3 resources and call <code>fetch</code> for each of them concurrently. The default max equals the number of online schedulers. Most of the time this can map 1:1 with the number of cores or virtual CPUs the hardware or VM has. Assuming this is running on a 2vCPU VM only 2 of them will run concurrently and the third will wait for its turn when one of the two running resources is complete. By default, it timeouts after 5 seconds and when that happens the process that spawned the tasks exits.</p>
<p>The beauty of this function lies in its simplicity and configurability where we can control the max concurrency, timeouts, processing order and what to do during timeouts. E.g.</p>
<pre><code class="lang-elixir"><span class="hljs-number">1</span>..<span class="hljs-number">100_000</span>
|&gt; Task.async_stream(<span class="hljs-keyword">fn</span> item -&gt;
  process(item)
<span class="hljs-keyword">end</span>,
  <span class="hljs-symbol">ordered:</span> <span class="hljs-keyword">false</span>,
  <span class="hljs-symbol">max_concurrenty:</span> <span class="hljs-number">1000</span>,
  <span class="hljs-symbol">timeout:</span> <span class="hljs-number">60_000</span>,
  <span class="hljs-symbol">on_timeout:</span> <span class="hljs-symbol">:kill_task</span>)
|&gt; Stream.reject(<span class="hljs-keyword">fn</span> 
  {<span class="hljs-symbol">:exit</span>, _} -&gt; <span class="hljs-keyword">false</span>
  _ -&gt; <span class="hljs-keyword">true</span>
  <span class="hljs-keyword">end</span>)
|&gt; Enum.to_list()
</code></pre>
<p>The only difference with the original example is how the resulting list is wrapped to accommodate the <code>:kill_task</code> option. For this example, we filter out any non-error simulating caring only about the side effect but knowing how many errors happened. Depending on your needs you can adapt how to process them through <code>Stream</code> or <code>Enum</code>.</p>
<h3 id="heading-case-3-remote-resource-as-a-stream">Case 3: Remote Resource as a stream</h3>
<p>Sometimes we can find resources that can be easily abstracted as streams to allow callers to emit values as needed. For instance, here's a sample where we abstract a particular resource that offers simple sequential access.</p>
<pre><code class="lang-elixir">Stream.resource(
  <span class="hljs-keyword">fn</span> -&gt; %{<span class="hljs-symbol">url:</span> <span class="hljs-string">"http://example.com/some/resource"</span>, <span class="hljs-symbol">index:</span> 0} <span class="hljs-keyword">end</span>,
  <span class="hljs-keyword">fn</span> %{<span class="hljs-symbol">url:</span> url, <span class="hljs-symbol">page:</span> page} = resource -&gt;
    <span class="hljs-keyword">case</span> fetch(url, page) <span class="hljs-keyword">do</span>
      {<span class="hljs-symbol">:ok</span>, %{<span class="hljs-symbol">next_index:</span> next_index} = result} -&gt; 
         {[result], %{ resource | <span class="hljs-symbol">index:</span> next_index }}
      _ -&gt; {<span class="hljs-symbol">:halt</span>, resource}
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>,
  <span class="hljs-keyword">fn</span> _ -&gt; <span class="hljs-symbol">:ok</span> <span class="hljs-keyword">end</span>
)
</code></pre>
<p>In real cases the index will take the form of a cursor but the idea is the same: The resource can now be accessed as a stream to leverage every function that handles them.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The true power of Elixir enumerables comes from the combination of reusable utility modules, well-defined protocols and existing data types ready to be used. Armed with these tools you can take on most tasks easily and when the abstractions are not enough you can easily extend or build on top of them to suit your needs.</p>
<p>To conclude with this post here are some general recommendations to keep in mind when working with these abstractions.</p>
<ul>
<li><p>Use <code>Enum</code> by default: When in doubt start with this module and move to <code>Streams</code> as necessary.</p>
</li>
<li><p>Go for <code>Stream</code> for large data sets or resources that can be abstracted to emit values and will benefit from being treated as a stream.</p>
</li>
<li><p>If still unsure and don't want to throw money at the problem then profile to understand where is/are potential bottlenecks. Finally, create alternative solutions and benchmark them.</p>
</li>
<li><p>Anti-pattern:</p>
<ul>
<li><p>Using Streams from the start: Stream is not a silver bullet. Start simple unless you know the use case works better in general with Streams.</p>
</li>
<li><p>Use Streams for everything to prevent scaling issues: This is a form of early optimization that could also cause the opposite in some cases.</p>
</li>
<li><p>Reinvent the wheel by ignoring <code>Enumerable</code>, <code>Collectable</code>, <code>Stream</code>, <code>Enum</code> and others: For simple solutions is fine to do so but if you find yourself reimplementing some of these functions for your data types then start thinking about how to implement <code>Enumerable</code> and <code>Collectable</code> to give superpowers to your key data structures.</p>
</li>
</ul>
</li>
</ul>
<p>I hope you liked this post and hope you subscribe to my newsletter 💌</p>
<div class="hn-embed-widget" id="convertkit"></div>]]></content:encoded></item></channel></rss>