<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jules</title>
    <description>The latest articles on DEV Community by Jules (@_juliantellez).</description>
    <link>https://dev.to/_juliantellez</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F740350%2F901fe29e-7f8a-40b6-8f86-0ae855a38861.jpg</url>
      <title>DEV Community: Jules</title>
      <link>https://dev.to/_juliantellez</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_juliantellez"/>
    <language>en</language>
    <item>
      <title>Who needs a VPN anyway?</title>
      <dc:creator>Jules</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:03:34 +0000</pubDate>
      <link>https://dev.to/_juliantellez/who-needs-a-vpn-anyway-f62</link>
      <guid>https://dev.to/_juliantellez/who-needs-a-vpn-anyway-f62</guid>
      <description>&lt;p&gt;As &lt;a href="https://juliantellez.com/blog/what-is-a-vpn" rel="noopener noreferrer"&gt;previously&lt;/a&gt; discussed; the internet is a collection of interconnected computers, commonly referred as nodes. When your computer requests data the sender must dispatch it (over the wire) through many intermediate nodes until it reaches you. This strategy for dispatching data is commonly referred to as the &lt;a href="https://en.wikipedia.org/wiki/Store_and_forward" rel="noopener noreferrer"&gt;store and forward model&lt;/a&gt;. But how do all these intermediate nodes know where to redirect the data so it gets to you? Well, there are a &lt;a href="https://en.wikipedia.org/wiki/Internet_Protocol" rel="noopener noreferrer"&gt;few rules to this game&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="/blog-media/who-needs-a-vpn/store-and-forward.gif" class="article-body-image-wrapper"&gt;&lt;img src="/blog-media/who-needs-a-vpn/store-and-forward.gif" alt="Data hopping between intermediate nodes A, B, C and D" width="{503}" height="{356}"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;A,B,C,D represent middlemen nodes. From &lt;a href="https://en.wikipedia.org/wiki/Store_and_forward" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt;
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;There is a limit to how big the sent data can be, this is because there's an actual physical limit which is why an optimal size has been established. To overcome this limitation, data is split into smaller packets. Each packet or &lt;a href="https://en.wikipedia.org/wiki/IP_packet" rel="noopener noreferrer"&gt;IP Packet&lt;/a&gt; contains a &lt;strong&gt;source&lt;/strong&gt; and a &lt;strong&gt;destination&lt;/strong&gt; address. Sounds familiar? This is how a postal service may implement their dispatching model.&lt;/p&gt;

&lt;p&gt;&lt;a href="/blog-media/who-needs-a-vpn/cover.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="/blog-media/who-needs-a-vpn/cover.jpeg" alt="Rows of post office boxes" width="{1400}" height="{933}"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Photo by &lt;a href="https://unsplash.com/@suehughes" rel="noopener noreferrer"&gt;sue hughes&lt;/a&gt; on &lt;a href="https://unsplash.com/" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The relay between sender to receiver may involve several intermediate nodes, where each node will inspect the packet and forward it closer to its final destination. When the requested data finally arrives to you it has been inspected by many different nodes.&lt;/p&gt;

&lt;p&gt;But why should we care about middlemen glancing over our data? As we mentioned in our &lt;a href="https://juliantellez.com/blog/what-is-a-vpn" rel="noopener noreferrer"&gt;previous article&lt;/a&gt;; The internet isn't trusted because of its architecture, but because of its users. Allowing third parties to inspect even just the route of your data can have several ripple effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gain commercial advantage&lt;/strong&gt;. If a third party or ISP is affiliated to a certain vendor and you purchase from its competitor, said party may slow down ('throttle') or even block access to it. Since your overall experience with the vendor was poor, next time you purchase the same or similar item you will consider its competitor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a commercial profile&lt;/strong&gt;. By analysing your data, a third party may build a profile of your preferences, correlate them with your local peers and sell the findings to a vendor. Ever wondered why everybody gets the same kind of adverts in the same area?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Censoring Information.&lt;/strong&gt; A third party could have an affinity for a certain topic and could therefore block access to the opposition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small misdirections such as throttling would ultimately change your behaviour.&lt;/p&gt;

&lt;p&gt;I will leave you with this reflection: 20% of the causes are responsible for 80% of the effects. The law of the vital few or &lt;a href="https://en.wikipedia.org/wiki/Pareto_principle" rel="noopener noreferrer"&gt;Pareto Principle&lt;/a&gt; can be traced down to human behaviour in the form of habits. Allowing others to determine how you interact with the world will ultimately change you, only you won't be able to notice.&lt;/p&gt;

&lt;p&gt;There are other reasons to use a VPN, such as privacy (not secrecy) and net-neutrality. I will be writing about them as they all deserve attention, until then. 👋&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://medium.com/freemiumpn/who-needs-a-vpn-anyway-32c879e91e36" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published on &lt;a href="https://juliantellez.com/blog/who-needs-a-vpn" rel="noopener noreferrer"&gt;juliantellez.com&lt;/a&gt;. It is part of my newsletter on video, streaming, and building reliable systems from scratch: the architecture, the tradeoffs, and the occasional 3am incident.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If it was useful, &lt;a href="https://juliantellez.com/#newsletter" rel="noopener noreferrer"&gt;subscribe for the next one&lt;/a&gt;. No spam, unsubscribe anytime. If you are building something similar and want to compare notes, &lt;a href="mailto:hello@juliantellez.com"&gt;say hello&lt;/a&gt;. I read every message.&lt;/p&gt;

&lt;p&gt;Built from scratch, like everything here.&lt;/p&gt;

</description>
      <category>vpn</category>
      <category>networking</category>
      <category>privacy</category>
    </item>
    <item>
      <title>What is a VPN?</title>
      <dc:creator>Jules</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:02:58 +0000</pubDate>
      <link>https://dev.to/_juliantellez/what-is-a-vpn-4mii</link>
      <guid>https://dev.to/_juliantellez/what-is-a-vpn-4mii</guid>
      <description>&lt;p&gt;&lt;a href="/blog-media/what-is-a-vpn/cover.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="/blog-media/what-is-a-vpn/cover.jpeg" alt="Looking down a long lit tunnel" width="{1400}" height="{933}"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Photo by &lt;a href="https://unsplash.com/@jakobsoeby" rel="noopener noreferrer"&gt;Jakob Søby&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/tunnel" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Virtual_private_network" rel="noopener noreferrer"&gt;VPN&lt;/a&gt; stands for Virtual Private Network. From a very high level, a network is simply a group of computers (commonly referred as nodes) that share resources, such as pictures, media, data, etc. These networks are said to be private when no external nodes have access to its resources.&lt;/p&gt;

&lt;p&gt;You can create a small network at home by connecting two computers to each other with say, an ethernet cable. In fact, you can connect as many nodes as you like; you will need a &lt;a href="https://en.wikipedia.org/wiki/Network_switch" rel="noopener noreferrer"&gt;network switch&lt;/a&gt; and a lot of patience. It's quiet fun! provided you don't mind thousands of cables lying around the house.&lt;/p&gt;

&lt;p&gt;&lt;a href="/blog-media/what-is-a-vpn/home-network.jpg" class="article-body-image-wrapper"&gt;&lt;img src="/blog-media/what-is-a-vpn/home-network.jpg" alt="A tangle of network cables" width="{564}" height="{657}"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;From Pinterest
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;As you can imagine, having every node hard-wired to its peers alone just doesn't scale. What happens when you run out of room? what if you would like to connect thousands, millions or even billions of nodes? How about making their resources accessible in different locations? To overcome this problem, nodes connect to an specialised node called a &lt;a href="https://en.wikipedia.org/wiki/Router_(computing)" rel="noopener noreferrer"&gt;router&lt;/a&gt;, thanks &lt;a href="https://en.wikipedia.org/wiki/William_Yeager" rel="noopener noreferrer"&gt;William 'Bill' Yeager&lt;/a&gt;! 🚀&lt;/p&gt;

&lt;p&gt;Since routers are nodes themselves, they can connect smaller networks; allowing the parent network to scale infinitely. Sounds familiar? This is a very simplified version of how the &lt;a href="https://en.wikipedia.org/wiki/Internet" rel="noopener noreferrer"&gt;internet&lt;/a&gt; works.&lt;/p&gt;

&lt;p&gt;&lt;a href="/blog-media/what-is-a-vpn/router-diagram.png" class="article-body-image-wrapper"&gt;&lt;img src="/blog-media/what-is-a-vpn/router-diagram.png" alt="Diagram of nodes connecting through routers" width="{600}" height="{563}"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;From developer.mozilla.org
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;When your computer connects to the internet, it becomes accessible on the network. In order to retrieve and send information on the web, it has to go through many routers and the information flowing back and forth isn't guaranteed to be private. The internet is an untrusted network, of course safety measures have been placed, such as &lt;a href="https://en.wikipedia.org/wiki/Transport_Layer_Security" rel="noopener noreferrer"&gt;SSL&lt;/a&gt;; However the internet isn't trusted because of its network architecture, but because of its users.&lt;/p&gt;

&lt;p&gt;So we know that for networks to be private, no external nodes should have access to its resources. But what about the word virtual? what are we referring to? Well, a virtual network is a sub network of geographically unrelated nodes; connected through the parent network, such as the internet. It simply means that these nodes aren't connected physically.&lt;/p&gt;

&lt;p&gt;We now know what it means to be Virtual, to be Private and what a Network is.&lt;/p&gt;

&lt;p&gt;&lt;a href="/blog-media/what-is-a-vpn/virtual.gif" class="article-body-image-wrapper"&gt;&lt;img src="/blog-media/what-is-a-vpn/virtual.gif" alt="Mind blown" width="{320}" height="{320}"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;From giphy.com
  &lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Virtual_private_network" rel="noopener noreferrer"&gt;VPN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Network_switch" rel="noopener noreferrer"&gt;Network switch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Router_(computing)" rel="noopener noreferrer"&gt;Router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/William_Yeager" rel="noopener noreferrer"&gt;William 'Bill' Yeager&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Internet" rel="noopener noreferrer"&gt;Internet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Transport_Layer_Security" rel="noopener noreferrer"&gt;SSL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://medium.com/freemiumpn/what-is-a-vpn-15a0089b93c1" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published on &lt;a href="https://juliantellez.com/blog/what-is-a-vpn" rel="noopener noreferrer"&gt;juliantellez.com&lt;/a&gt;. It is part of my newsletter on video, streaming, and building reliable systems from scratch: the architecture, the tradeoffs, and the occasional 3am incident.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If it was useful, &lt;a href="https://juliantellez.com/#newsletter" rel="noopener noreferrer"&gt;subscribe for the next one&lt;/a&gt;. No spam, unsubscribe anytime. If you are building something similar and want to compare notes, &lt;a href="mailto:hello@juliantellez.com"&gt;say hello&lt;/a&gt;. I read every message.&lt;/p&gt;

&lt;p&gt;Built from scratch, like everything here.&lt;/p&gt;

</description>
      <category>vpn</category>
      <category>networking</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Handling complexity in lambda functions</title>
      <dc:creator>Jules</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:02:22 +0000</pubDate>
      <link>https://dev.to/_juliantellez/handling-complexity-in-lambda-functions-4db</link>
      <guid>https://dev.to/_juliantellez/handling-complexity-in-lambda-functions-4db</guid>
      <description>&lt;p&gt;&lt;a href="/blog-media/handling-complexity-in-lambda-functions/cover.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="/blog-media/handling-complexity-in-lambda-functions/cover.jpeg" alt="A rocket launch against a dark sky" width="{1400}" height="{932}"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Photo by &lt;a href="https://unsplash.com/@nasa" rel="noopener noreferrer"&gt;NASA&lt;/a&gt; on &lt;a href="https://unsplash.com/" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;
  &lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TLDR;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Middlewares can handle the complexity of your lambdas while isolating business logic and cross-cutting concerns in reusable components that can be modelled by event cycles.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I have had the luck and pleasure of working with lambda functions recently and boy I am having fun! The idea of having an event-driven execution environment is both daring and exciting. If you are new to the FaaS world, don't worry, the community has already prepared a curated list of reads for you &lt;a href="https://github.com/anaibol/awesome-serverless" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://github.com/pmuens/awesome-serverless" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Have a browse, drink some coffee. ☕&lt;/p&gt;

&lt;p&gt;As you can see, (because you checked at least one of the links, didn't you?) A huge weight has been lifted. We can now develop, run and manage applications without the intricacy of building and maintaining infrastructure. But with new beginnings, new challenges arise. One of such problems and the reason for this blog is function complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  How complex is too complex?
&lt;/h2&gt;

&lt;p&gt;Generally speaking, when writing lambda functions you would organise them into a series of dedicated modules following the &lt;a href="https://en.wikipedia.org/wiki/Single_responsibility_principle" rel="noopener noreferrer"&gt;single responsibility principle&lt;/a&gt;. If you would rather do otherwise, please read this excellent blog comparing &lt;a href="https://hackernoon.com/aws-lambda-should-you-have-few-monolithic-functions-or-many-single-purposed-functions-8c3872d4338f" rel="noopener noreferrer"&gt;monolithic vs single purposed functions&lt;/a&gt;. Isolating application from business logic reduces code complexity, simplifies reasoning and will help you debug your code more efficiently. Your colleagues and your future self will thank you.&lt;/p&gt;

&lt;p&gt;Take for example some sort of CRUD API, we would like to make sure that a payload is properly formatted before handling any data. The payload validation could emit an error that would communicate its status to the consumer. If the validation was successful it would then move onto executing some business logic. For both scenarios, we would also want to set up some percentile tracing and error logging. While all of these sound great, we have essentially mapped out a request lifecycle with various &lt;a href="https://en.wikipedia.org/wiki/Cross-cutting_concern" rel="noopener noreferrer"&gt;cross-cutting concerns&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data validation.&lt;/li&gt;
&lt;li&gt;Error detection and correction.&lt;/li&gt;
&lt;li&gt;Logging.&lt;/li&gt;
&lt;li&gt;Monitoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="/blog-media/handling-complexity-in-lambda-functions/thinking.gif" class="article-body-image-wrapper"&gt;&lt;img src="/blog-media/handling-complexity-in-lambda-functions/thinking.gif" alt="Thinking" width="{300}" height="{300}"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;I know what you are thinking… (gif from giphy)
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Following the serverless mantra, we should outsource anything that isn't related to the core business, so wouldn't it be great if we could just focus on the business logic and outsource these application-specific problems too? Of course, this isn't a new concept and we have been using it in web frameworks like &lt;a href="http://hapijs.com/" rel="noopener noreferrer"&gt;hapijs&lt;/a&gt; and &lt;a href="https://expressjs.com/" rel="noopener noreferrer"&gt;express&lt;/a&gt; for quite some time now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lambda Middleware
&lt;/h2&gt;

&lt;p&gt;A lambda &lt;a href="https://en.wikipedia.org/wiki/Middleware" rel="noopener noreferrer"&gt;middleware&lt;/a&gt; is essentially a function that contributes to managing cross-cutting concerns in a consistent manner. It provides a clear separation of application and business logic and outlines an easy to reason configuration cycle. It is the glue that when adopted by everyone in a team becomes the lingua franca that supports and creates features. But do we really need a middleware? Well, look at the current state of your lambdas and ask yourself a few questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What does the code look like?&lt;/li&gt;
&lt;li&gt;Can you envision anyone supporting it in at least 2 years?&lt;/li&gt;
&lt;li&gt;How much business logic expertise do you need? (if specialised then separation is imperative)&lt;/li&gt;
&lt;li&gt;Is logic intertwined and hard to reason about?&lt;/li&gt;
&lt;li&gt;Does the project have a clear path or do you see it going in different directions?&lt;/li&gt;
&lt;li&gt;How will the project look like the day the expert leaves?&lt;/li&gt;
&lt;li&gt;Are best-practices enforced and maintained?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="/blog-media/handling-complexity-in-lambda-functions/jack-nicholson.gif" class="article-body-image-wrapper"&gt;&lt;img src="/blog-media/handling-complexity-in-lambda-functions/jack-nicholson.gif" alt="Jack Nicholson nodding in approval" width="{300}" height="{229}"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Jack Nicholson approves. (gif from giphy)
  &lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Lambcycle
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/juliantellez/lambcycle" rel="noopener noreferrer"&gt;Lambcycle&lt;/a&gt; 🐑 🛵 is a declarative middleware for lambda functions. It defines a configurable event cycle and allows you to focus on your application's logic. It has a "Feature as Plugin" approach, so you can easily create your own plugins or reuse your favourite packages with very little effort.&lt;/p&gt;

&lt;p&gt;In practice there is no such thing as an un-opinionated framework, every idea is an opinion and a way of working with your application. Lambcycle favours configuration over code and believes that business logic should be as isolated as possible.&lt;/p&gt;

&lt;p&gt;&lt;a href="/blog-media/handling-complexity-in-lambda-functions/lambcycle-flow-chart.png" class="article-body-image-wrapper"&gt;&lt;img src="/blog-media/handling-complexity-in-lambda-functions/lambcycle-flow-chart.png" alt="Lambcycle event cycle flow chart" width="{380}" height="{514}"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Labmcycle flow chart.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Lambcycle enhances lambda functions with a few extension points (see graph), each of which can be used to interact with the event in a decomposed manner.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first extension point is &lt;code&gt;Request&lt;/code&gt; which occurs immediately after the lambda is called. You can use this step for parsing, validation, etc...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: If you are thinking of auth, please consider a &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html" rel="noopener noreferrer"&gt;lambda authoriser&lt;/a&gt; instead.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;Pre Handler&lt;/code&gt; extension comes in handy when you need to adapt data to fit an interface. It is also a great place for fetching secrets.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Handler&lt;/code&gt;, where your beautiful business logic lives.&lt;/li&gt;
&lt;li&gt;Next up is the &lt;code&gt;Post Handler&lt;/code&gt;, use this extension to validate and/or cache the output of your business logic.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Error&lt;/code&gt; is an implicit extension for logging and tracing.&lt;/li&gt;
&lt;li&gt;And finally &lt;code&gt;Pre Response&lt;/code&gt;, your chance to format a response to the consumer (which could be data or an error).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using the middleware
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Joi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;joi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;lambcycle&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lambcycle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;joiPlugin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lambcycle/dist/plugin-joi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;bodyParser&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lambcycle/dist/plugin-body-parser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;realTimeEventTracker&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./bugFreeBizLogic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;businessLogic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;realTimeEventTracker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Joi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;assetId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Joi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;competition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Joi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;sport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Joi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;tournamentCalendar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Joi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lambcycle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;businessLogic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;bodyParser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;joiPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example above leverages &lt;a href="https://github.com/expressjs/body-parser" rel="noopener noreferrer"&gt;express' body parser&lt;/a&gt; and &lt;a href="https://github.com/hapijs/joi" rel="noopener noreferrer"&gt;hapi's joi validation&lt;/a&gt;. We have configured both plugins with a parsing type and a validation schema respectively. By abstracting these two we have rid of complexity and shift the focus to the business logic. Note that a more robust lambda would include error tracing, and response plugins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crafting a Plugin
&lt;/h2&gt;

&lt;p&gt;The possibilities are endless when it comes to plugins! Do you have something in mind? &lt;a href="https://github.com/juliantellez/lambcycle/blob/develop/contributing.md" rel="noopener noreferrer"&gt;Contributions&lt;/a&gt; are more than welcome ❤️&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Sentry&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sentry/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MyAwesomeIntegration&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./MyAwesomeIntegration&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sentryPlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@sentry.io/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MyAwesomeIntegration&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;onPreResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handlerWrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;captureMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;some percentile log perhaps?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="na"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handlerWrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;captureException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handlerWrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;sentryPlugin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your lambda ….&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;lambcycle&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lambcycle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sentryPlugin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./sentryPlugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myApplicationLogic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;someLogic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lambcycle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myApplicationLogic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;sentryPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTRY_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTRY_PROJECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  DX
&lt;/h2&gt;

&lt;p&gt;Lambcycle has been built with &lt;a href="https://hackernoon.com/the-best-practices-for-a-great-developer-experience-dx-9036834382b0" rel="noopener noreferrer"&gt;developer experience "DX"&lt;/a&gt; in mind and ships with &lt;a href="https://www.typescriptlang.org/" rel="noopener noreferrer"&gt;type&lt;/a&gt; definitions, for consistency and auto-completion 🚀 (VScode only).&lt;/p&gt;

&lt;p&gt;&lt;a href="/blog-media/handling-complexity-in-lambda-functions/lambcycle-dx.gif" class="article-body-image-wrapper"&gt;&lt;img src="/blog-media/handling-complexity-in-lambda-functions/lambcycle-dx.gif" alt="Lambcycle type definitions providing auto-completion in the editor" width="{830}" height="{352}"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Lambcycle DX.
  &lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;It is a brave new world and serverless is here to stay! Promoting reusable components and consistent error handling will help you and your team create and support features in a more controlled and organised fashion. Lambcycle's main goals are to shift the conversation around a predictable unidirectional cycle and encouraging component reusability across features. Its worth mentioning that for more complex scenarios you should be looking at &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html" rel="noopener noreferrer"&gt;Step functions&lt;/a&gt;. Let me know if you would like to know more about them in the comments, in the meantime check out these &lt;a href="https://aws.amazon.com/step-functions/use-cases/" rel="noopener noreferrer"&gt;use cases&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Special thanks to the legendary &lt;a href="https://theburningmonk.com/" rel="noopener noreferrer"&gt;burning monk&lt;/a&gt; for the in-depth review of this article.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published in the DAZN Engineering publication on &lt;a href="https://medium.com/dazn-tech/handling-complexity-in-lambda-functions-e7acfbeb920a" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published on &lt;a href="https://juliantellez.com/blog/handling-complexity-in-lambda-functions" rel="noopener noreferrer"&gt;juliantellez.com&lt;/a&gt;. It is part of my newsletter on video, streaming, and building reliable systems from scratch: the architecture, the tradeoffs, and the occasional 3am incident.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If it was useful, &lt;a href="https://juliantellez.com/#newsletter" rel="noopener noreferrer"&gt;subscribe for the next one&lt;/a&gt;. No spam, unsubscribe anytime. If you are building something similar and want to compare notes, &lt;a href="mailto:hello@juliantellez.com"&gt;say hello&lt;/a&gt;. I read every message.&lt;/p&gt;

&lt;p&gt;Built from scratch, like everything here.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>aws</category>
      <category>lambda</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Talking to Claude Code through an Alexa</title>
      <dc:creator>Jules</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:01:45 +0000</pubDate>
      <link>https://dev.to/_juliantellez/talking-to-claude-code-through-an-alexa-1k71</link>
      <guid>https://dev.to/_juliantellez/talking-to-claude-code-through-an-alexa-1k71</guid>
      <description>&lt;p&gt;This was an afternoon experiment, so read it in that spirit. The goal was to start real coding work by talking to a speaker: not "set a timer", but "work on the board script", and have an agent plan it with me and go and do it. No laptop open.&lt;/p&gt;

&lt;p&gt;I gave myself a few hours, which means there are shortcuts everywhere. The backend is literally my laptop. The public entry point is a free tunnel. There is exactly one user, me, and no accounts. None of that would survive contact with real users, and I will come back to what a grown-up version looks like at the end. But the core turned out to be more interesting than I expected, because the main constraint forced a better design than the obvious one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall everything runs into
&lt;/h2&gt;

&lt;p&gt;Alexa cuts off a skill's response after about eight seconds. That is the whole design problem in one sentence.&lt;/p&gt;

&lt;p&gt;A coding agent does not fit in eight seconds. Even just asking Claude to &lt;em&gt;think of a few options&lt;/em&gt; for a task takes anywhere from four to twenty-plus seconds. So you cannot do the obvious thing, which is take what the person said, ask Claude about it, and speak the answer. Alexa will have hung up long before Claude replies.&lt;/p&gt;

&lt;p&gt;Once you accept that nothing useful finishes in the window, the design falls out of it: &lt;strong&gt;acknowledge instantly, think in the background, and surface the thinking somewhere the eight seconds do not apply.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A planning conversation, not a command
&lt;/h2&gt;

&lt;p&gt;Here is what actually happens when I use it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;me:    "Alexa, open assistant"
claude: "Claude ready. What should I work on?"

me:    "work on the board script"
claude: "Okay, work on the board script. I'm putting a few options on the
         dashboard. Tell me which one or describe what you want, then say go."

me:    "the second one"
claude: "Here's the plan: add a --since flag to the board script. Say go to
         start, or add more."

me:    "go"
claude: "Starting now: add a --since flag to the board script. Watch the
         dashboard, and ask me for the result."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment I describe a task, the server does two things at once. It replies immediately with a short acknowledgement, well inside the window, and it spawns a detached job that asks Claude to turn my vague sentence into two or three concrete, short options. Those options land on a dashboard, and the next thing I say is resolved against them. "The second one", "option two", and "three" all map to a choice; anything else is folded in as more detail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
  actor You
  participant Echo as "Echo / Alexa cloud"
  participant Mac as "alexa-claude (my Mac)"
  participant Claude
  You-&amp;gt;&amp;gt;Echo: "work on the board script"
  Echo-&amp;gt;&amp;gt;Mac: signed request
  Mac--&amp;gt;&amp;gt;Echo: ack in under a second
  Mac-&amp;gt;&amp;gt;Claude: generate options (background)
  Claude--&amp;gt;&amp;gt;Mac: options appear on dashboard
  You-&amp;gt;&amp;gt;Echo: "the second one" ... "go"
  Echo-&amp;gt;&amp;gt;Mac: signed request
  Mac-&amp;gt;&amp;gt;Claude: run task in a tmux session
  Mac--&amp;gt;&amp;gt;Echo: "starting, ask me for status"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the latency that made the naive design impossible becomes invisible: by the time I have heard the acknowledgement and decided what I want, the options are already there. The prompt gets built up across a couple of turns, and "go" commits it.&lt;/p&gt;

&lt;p&gt;(The skill is invoked as "assistant" rather than "claude", because Amazon will not let you claim a single well-known word as a custom invocation name. It just introduces itself as Claude.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The jobs are real sessions
&lt;/h2&gt;

&lt;p&gt;When I say "go", the task does not run as some opaque background process. It starts in its own &lt;strong&gt;tmux session&lt;/strong&gt;, which means it shows up in the same &lt;code&gt;sessions&lt;/code&gt; tool I use for every other Claude session on the machine, and I can attach and watch it work live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sessions go &amp;lt;pid&amp;gt;   &lt;span class="c"&gt;# drop into the running voice-started job&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That mattered more than I expected. A voice command that spawns invisible work is unnerving. A voice command that spawns a session I can see, attach to, and read line by line is just a faster way to start something I already understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  A dashboard that stays home
&lt;/h2&gt;

&lt;p&gt;Voice is a poor channel for output, so the real surface is a small web dashboard the server renders. It polls every couple of seconds and shows the prompt being built right now with its options, the jobs with running, done, and failed badges, and a log of voice turns so I can see what it heard and what it said back.&lt;/p&gt;

&lt;p&gt;The one decision I am quietly pleased with: the dashboard is &lt;strong&gt;refused on the public URL.&lt;/strong&gt; The tunnel that lets Amazon reach my Mac would also let anyone who found it load my activity dashboard. But tunnelled requests carry an &lt;code&gt;X-Forwarded-For&lt;/code&gt; header and direct localhost requests do not, so the server serves the dashboard only to local requests and returns a 403 to anything that arrived over the tunnel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hearing the answer
&lt;/h2&gt;

&lt;p&gt;Long work reports back through whatever channel suits. The job streams to its log and its tmux pane the whole time. When I want the result out loud, I ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;me:    "Alexa, ask assistant for the status"
claude: "Done, add a --since flag to the board script. &amp;lt;reads the tail of the
         output, kept short enough to be pleasant&amp;gt;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the machine has Telegram configured it also pushes the result to my phone, but that is optional sugar now rather than the main path. Short questions are the one thing answered inline: "ask assistant to tell me X" runs Claude read-only with a few seconds of grace, and speaks the answer if it finishes in time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it safe enough
&lt;/h2&gt;

&lt;p&gt;Even an afternoon toy that starts a coding agent from a public URL needs a lock, so every request has to prove itself. The server follows Amazon's verification for self-hosted endpoints: the certificate chain has to be a genuine Amazon address, the certificate has to be valid and issued for &lt;code&gt;echo-api.amazon.com&lt;/code&gt;, the body has to verify against its signature, and the timestamp has to be recent so a captured request cannot be replayed. The skill id is a second lock on top. Questions run read-only; tasks run with a permission mode that edits files but still stops before risky commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real product would look like
&lt;/h2&gt;

&lt;p&gt;Everything above is held together with the cheapest possible parts, and that is the right call for a few hours of play. But it is worth being honest about where the shortcuts are and what you would actually build instead.&lt;/p&gt;

&lt;p&gt;The shortcuts: the backend is my Mac, so it only works while the machine is awake with the server and tunnel running. The entry point is a personal free tunnel, not a hosted endpoint. There is one user and no authentication, so "lock it to my skill id" is doing a lot of heavy lifting. And the agent runs directly on my real machine with access to my real files, which is fine for me and unthinkable for anyone else.&lt;/p&gt;

&lt;p&gt;A product version inverts most of that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  Echo["Echo"] --&amp;gt; Alexa["Alexa cloud"]
  Alexa --&amp;gt;|"account linking (OAuth)"| GW["API Gateway"]
  GW --&amp;gt; Fn["Skill function: validate + enqueue"]
  Fn --&amp;gt; Q["Queue"]
  Fn --&amp;gt; DB[("Jobs / sessions store")]
  Q --&amp;gt; W["Sandboxed agent workers"]
  W --&amp;gt; DB
  W --&amp;gt;|result| Push["Proactive notifications / companion app"]
  Push --&amp;gt; Echo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill endpoint becomes a hosted, stateless function behind an API gateway, so there is no laptop to keep awake and no tunnel to babysit. Its only jobs are to verify the request, look up the user, and drop work on a queue, all comfortably inside the eight seconds. &lt;strong&gt;Account linking&lt;/strong&gt; turns the single-user assumption into real multi-tenancy: each person links their own identity, and the skill id stops being the only thing standing between a stranger and a shell.&lt;/p&gt;

&lt;p&gt;The actual agent work moves onto &lt;strong&gt;sandboxed workers&lt;/strong&gt;, one isolated environment per user, so a task can never touch anyone else's files or the host. Job and conversation state lives in a small &lt;strong&gt;database&lt;/strong&gt; rather than on one machine's disk, which is what lets a result reach you later, on a different device, after the voice session is long gone. And the results come back through Alexa's &lt;strong&gt;proactive notifications&lt;/strong&gt; or a companion app, so "ask me for the status" becomes "your phone buzzes when it is done."&lt;/p&gt;

&lt;p&gt;None of that is exotic. It is the boring, well-trodden shape of any async job system. The interesting part, the planning-conversation-around-latency, stays exactly the same. The afternoon version just runs it on a laptop instead of a fleet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it taught me
&lt;/h2&gt;

&lt;p&gt;I went in thinking the hard part would be the voice. It was not; Amazon's speech to text is good and free. The hard part, and the fun part, was that the eight second wall made the naive design impossible and pushed me toward a better one: a system that never pretends to be fast, acknowledges instantly, thinks out of band, and gives you real attachable sessions and a live dashboard instead of a single hopeful reply.&lt;/p&gt;

&lt;p&gt;It stopped being "an agent I sit in front of" and became "an agent I can start a conversation with from across the room, then go and watch it work." For an afternoon's wiring, that is a surprisingly nice way to begin a piece of work.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built from scratch, like everything in this newsletter. Questions about the build? &lt;a href="https://www.linkedin.com/in/juliantellez" rel="noopener noreferrer"&gt;Say hello&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published on &lt;a href="https://juliantellez.com/blog/talking-to-claude-code-through-alexa" rel="noopener noreferrer"&gt;juliantellez.com&lt;/a&gt;. It is part of my newsletter on video, streaming, and building reliable systems from scratch: the architecture, the tradeoffs, and the occasional 3am incident.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If it was useful, &lt;a href="https://juliantellez.com/#newsletter" rel="noopener noreferrer"&gt;subscribe for the next one&lt;/a&gt;. No spam, unsubscribe anytime. If you are building something similar and want to compare notes, &lt;a href="mailto:hello@juliantellez.com"&gt;say hello&lt;/a&gt;. I read every message.&lt;/p&gt;

&lt;p&gt;Built from scratch, like everything here.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>alexa</category>
      <category>homelab</category>
      <category>automation</category>
    </item>
    <item>
      <title>Stop Letting AI Mark Its Own Homework</title>
      <dc:creator>Jules</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:01:09 +0000</pubDate>
      <link>https://dev.to/_juliantellez/stop-letting-ai-mark-its-own-homework-4p07</link>
      <guid>https://dev.to/_juliantellez/stop-letting-ai-mark-its-own-homework-4p07</guid>
      <description>&lt;p&gt;Last month I watched Claude Code write a beautiful authentication module. Clean abstractions, sensible error handling, well named functions. It looked like something a developer would write. Then I ran it. It failed immediately. The module was calling a method that didn't exist on the object it was importing. Claude hadn't made a logic error. It had made a confidence error. It wrote code that read correctly, explained correctly, and looked like it would work. But nobody had asked it to prove it. I stared at the screen for a minute, fixed the bug manually, and then thought: how many times will this happen again? Who is checking if all this code actually works?&lt;/p&gt;

&lt;h2&gt;
  
  
  The false confidence problem
&lt;/h2&gt;

&lt;p&gt;Here's what I've noticed after a year of building with AI coding tools: the failure mode isn't that the code is bad. It's that the code is almost right. Close enough to pass a glance. Close enough that Claude will confidently explain why it works. Close enough that you could ship it, and if you don't have guards in place bugs will show up in production.&lt;/p&gt;

&lt;p&gt;This isn't a Claude problem per se, it's an LLM problem. These models are trained to produce plausible output. And plausible code is the most dangerous kind of code, because it doesn't look wrong. The traditional answer to this has always been testing. Write the test first, write the implementation, run the test, see green. TDD isn't new. But here's what changed: the developer writing the code is now also an AI, and if the same AI writes the code and decides whether the code works, we've got a circular trust problem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Reviewing your own code is basically marking your own homework. AI is no different.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The search for a solution
&lt;/h2&gt;

&lt;p&gt;So I went looking for answers. The Claude Code ecosystem has some genuinely brilliant tools. GSD, short for Get Shit Done, stood out immediately. It interviews you about what you're building, creates atomic plans, and executes in phases using fresh subagents with clean context windows. It solves a great part of the context rot problem. If you haven't tried it, you should.&lt;/p&gt;

&lt;p&gt;But after a few weeks, I kept hitting the same wall. GSD manages complexity and it's rather good at it. The planning is structured, the execution is disciplined, the main context stays clean. But when it comes to verification, the same agent that writes the code also checks the code. It basically asks "what must be TRUE for this to work?", which is a sensible question. But the agent is answering a question about its own output.&lt;/p&gt;

&lt;p&gt;GSD was great at planning, but we also needed a trust boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the boundary
&lt;/h2&gt;

&lt;p&gt;The idea is simple: go back to basics and embrace TDD. What if the agent that writes the test is not allowed to write the implementation? That one constraint changes the flow. Once you enforce that separation, the agent can't fake confidence. The tester doesn't know how the code will work. The coder can't change the tests. And the thing that decides pass or fail isn't AI at all, it's your pytest, jest, go test. A mechanical process with no opinions and no ego.&lt;/p&gt;

&lt;p&gt;I called it Greenlight. Green means done. Not "the agent thinks it's done." Actually done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Greenlight
&lt;/h2&gt;

&lt;p&gt;It has agents, each with hard boundaries. Here is the core team:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Architect: defines what needs to be built, system design, constraints, scope.&lt;/li&gt;
&lt;li&gt;The Tester: writes the (failing) tests based on the design. It can't see any implementation. It only knows what the code should do.&lt;/li&gt;
&lt;li&gt;The Developer: receives the contracts and tests and writes code to fulfil the requirements. It is not done until both the requirements and tests pass. It can see the tests but cannot modify them.&lt;/li&gt;
&lt;li&gt;The SecOps: reviews everything independently for vulnerabilities, injection risks, auth issues, and security antipatterns.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The test runner sits in the middle and judges. Not Claude. Not any AI. The machine. If tests pass, we move forward. If they don't, the coder keeps working. The project lives at &lt;a href="https://github.com/atlantic-blue/greenlight" rel="noopener noreferrer"&gt;github.com/atlantic-blue/greenlight&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Greenlight
&lt;/h2&gt;

&lt;p&gt;Claude Code is powerful but permissive. It writes code, reviews its own work, and moves on. There's no external verification. Greenlight fixes this by enforcing agent isolation: one agent writes tests from contracts, another implements until tests pass, a third scans for vulnerabilities, and the test runner is the only judge. No agent ever sees its own tests.&lt;/p&gt;

&lt;p&gt;This matters because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI reviews its own code poorly. Separating test writing from implementation prevents Claude from gaming its own tests.&lt;/li&gt;
&lt;li&gt;TDD works better with AI. Contracts define WHAT, agents figure out HOW, and the test suite proves it works.&lt;/li&gt;
&lt;li&gt;Security can't be an afterthought. Every slice gets a security scan. Every vulnerability becomes a failing test.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What Greenlight is
&lt;/h3&gt;

&lt;p&gt;A set of Claude Code slash commands, agents, and engineering standards that enforce test-driven development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10 agents with strict isolation boundaries (designer, architect, test writer, implementer, security, verifier, debugger, codebase mapper, assessor, wrapper)&lt;/li&gt;
&lt;li&gt;16 slash commands (&lt;code&gt;/gl:init&lt;/code&gt;, &lt;code&gt;/gl:design&lt;/code&gt;, &lt;code&gt;/gl:slice&lt;/code&gt;, &lt;code&gt;/gl:ship&lt;/code&gt;, and so on) that orchestrate the workflow&lt;/li&gt;
&lt;li&gt;Engineering standards (CLAUDE.md) covering error handling, naming, security, API design, testing, and more&lt;/li&gt;
&lt;li&gt;Context degradation awareness, so agents stay under 50% context usage to maintain quality&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What Greenlight is not
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Not a framework, library, or runtime dependency. It installs config files and does nothing at build time.&lt;/li&gt;
&lt;li&gt;Not a replacement for your test framework. It orchestrates Claude Code to use whatever test runner your project already has.&lt;/li&gt;
&lt;li&gt;Not opinionated about language or stack. The contracts and standards are language agnostic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why is this interesting?
&lt;/h2&gt;

&lt;p&gt;Claude Code just shipped Agent Teams, an experimental feature where multiple Claude sessions coordinate, message each other, and work in parallel. The agent roles already exist in Greenlight. The trust boundaries are already defined. What Agent Teams adds is concurrency and real-time coordination.&lt;/p&gt;

&lt;p&gt;Right now, Greenlight's agents run sequentially. With Agent Teams, picture the test writer and implementer running as parallel teammates. The security agent scanning code as it's written, not after. The architect coordinating as team lead. Slices without dependencies executing simultaneously. All communicating through Claude Code's native messaging, all respecting their information boundaries.&lt;/p&gt;

&lt;p&gt;The roles exist. The primitives exist. Wiring them together is the natural next step.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The real bottleneck isn't capability. The bottleneck is trust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We're at a place in time where AI coding tools are good enough that capability isn't the limiting factor. Claude can write authentication modules, API gateways, database migrations, React components, infrastructure. The models will keep getting better. Costs will keep dropping. The bottleneck is trust.&lt;/p&gt;

&lt;p&gt;Can you trust that what it wrote actually works? Can you deploy it without manually reading every line? Can you sleep at night knowing unreviewed code is running in production? TDD has always been the answer to that question. It just needed to be adapted for a world where the developer is also an AI.&lt;/p&gt;

&lt;p&gt;Happy coding.&lt;/p&gt;




&lt;p&gt;If you're building with Claude Code and want to stop vibe coding, give it a go. Or tell me I'm wrong. I'd genuinely love to hear how others are solving the trust problem. Greenlight is open source, MIT licensed: &lt;a href="https://github.com/atlantic-blue/greenlight" rel="noopener noreferrer"&gt;github.com/atlantic-blue/greenlight&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First published in my LinkedIn newsletter, &lt;a href="https://www.linkedin.com/newsletters/built-from-scratch-6973801137594343424/" rel="noopener noreferrer"&gt;Built from Scratch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published on &lt;a href="https://juliantellez.com/blog/stop-letting-ai-mark-its-own-homework" rel="noopener noreferrer"&gt;juliantellez.com&lt;/a&gt;. It is part of my newsletter on video, streaming, and building reliable systems from scratch: the architecture, the tradeoffs, and the occasional 3am incident.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If it was useful, &lt;a href="https://juliantellez.com/#newsletter" rel="noopener noreferrer"&gt;subscribe for the next one&lt;/a&gt;. No spam, unsubscribe anytime. If you are building something similar and want to compare notes, &lt;a href="mailto:hello@juliantellez.com"&gt;say hello&lt;/a&gt;. I read every message.&lt;/p&gt;

&lt;p&gt;Built from scratch, like everything here.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>tdd</category>
      <category>greenlight</category>
    </item>
    <item>
      <title>Raspberry Pi Kubernetes Cluster: Start the Cluster (Issue #4)</title>
      <dc:creator>Jules</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:00:33 +0000</pubDate>
      <link>https://dev.to/_juliantellez/raspberry-pi-kubernetes-cluster-start-the-cluster-issue-4-35l</link>
      <guid>https://dev.to/_juliantellez/raspberry-pi-kubernetes-cluster-start-the-cluster-issue-4-35l</guid>
      <description>&lt;p&gt;Welcome to the Raspberry Pi Kubernetes home lab series. A bite sized informative guide to help you provision a cluster from scratch. By now we should have 3+ nodes running the latest version of Ubuntu and we should be able to SSH into them.&lt;/p&gt;

&lt;p&gt;Today's issue will walk you through installing Kubernetes into your newly created cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;At least 3 nodes with Ubuntu installed on them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following 12 steps should be replicated on every node. We will set them up manually in this lab session, but will provide an automated way of setting up nodes via Ansible in a coming issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. SSH into the node
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh ubuntu@192.168.0.25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Set up hostnames and static IPs
&lt;/h2&gt;

&lt;p&gt;Slave nodes need to provide a unique name to the master node upon registration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On each node&lt;/span&gt;
&lt;span class="nv"&gt;NODE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;k8s-master &lt;span class="c"&gt;# k8s-node-1 etc..&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;hostnamectl set-hostname &lt;span class="nv"&gt;$NODE_NAME&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Edit the hosts file so it maps IP addresses to hostnames
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/hosts
192.168.0.25 k8s-master
192.168.0.26 k8s-node-1
192.168.0.24 k8s-node-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Disable swap memory
&lt;/h2&gt;

&lt;p&gt;Swap is space on the host disk that is used when the amount of physical RAM is full. Even though swap support has been added to Kubernetes, our Raspberry Pis have limited resources and we should account for such limitations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;swapoff &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt purge &lt;span class="nt"&gt;-y&lt;/span&gt; dphys-swapfile
&lt;span class="c"&gt;# Test swap has been disabled&lt;/span&gt;
free &lt;span class="nt"&gt;-m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Edit the boot configuration
&lt;/h2&gt;

&lt;p&gt;Append the following config to the boot file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /boot/firmware/cmdline.txt
ipv6.disable&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;cgroup_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cpuset &lt;span class="nv"&gt;cgroup_enable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;memory &lt;span class="nv"&gt;cgroup_memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Set up a container runtime (CRI)
&lt;/h2&gt;

&lt;p&gt;Kubernetes has moved away from Docker. There are a few options out there (Docker included). We will be setting up containerd as our CRI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt dist-upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; containerd containernetworking-plugins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Create a config for containerd
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="err"&gt;sudo&lt;/span&gt; &lt;span class="err"&gt;mkdir&lt;/span&gt; &lt;span class="err"&gt;/etc/containerd&lt;/span&gt;
&lt;span class="err"&gt;cat&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;&amp;lt;EOF&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="err"&gt;sudo&lt;/span&gt; &lt;span class="err"&gt;tee&lt;/span&gt; &lt;span class="err"&gt;/etc/containerd/config.toml&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="nn"&gt;[plugins]&lt;/span&gt;
  &lt;span class="nn"&gt;[plugins."io.containerd.grpc.v1.cri"]&lt;/span&gt;
    &lt;span class="nn"&gt;[plugins."io.containerd.grpc.v1.cri".containerd]&lt;/span&gt;
      &lt;span class="nn"&gt;[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]&lt;/span&gt;

&lt;span class="nn"&gt;[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]&lt;/span&gt;
          &lt;span class="py"&gt;runtime_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"io.containerd.runc.v2"&lt;/span&gt;
          &lt;span class="nn"&gt;[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]&lt;/span&gt;
            &lt;span class="py"&gt;SystemdCgroup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="err"&gt;EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Forward IPv4 and set up IP tables
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;modprobe overlay
&lt;span class="nb"&gt;sudo &lt;/span&gt;modprobe br_netfilter

&lt;span class="c"&gt;# sysctl params required by setup, params persist across reboots&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Apply sysctl params without reboot&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;--system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  10. Install kubeadm
&lt;/h2&gt;

&lt;p&gt;Update the &lt;code&gt;apt&lt;/code&gt; package index and install packages needed to use the Kubernetes &lt;code&gt;apt&lt;/code&gt; repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; apt-transport-https ca-certificates curl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Download the Google Cloud public signing key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSLo&lt;/span&gt; /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the Kubernetes &lt;code&gt;apt&lt;/code&gt; repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/kubernetes.list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;apt&lt;/code&gt; package index, install kubelet, kubeadm and kubectl, and pin their version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; kubelet kubeadm kubectl
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark hold kubelet kubeadm kubectl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  11. Install a Container Network Interface (CNI)
&lt;/h2&gt;

&lt;p&gt;A CNI is required to implement the Kubernetes network model. We are going to install Flannel in this lab as it is compatible with MetalLB. Be sure to check out different networking models too, such as Calico.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://github.com/flannel-io/flannel/releases/download/v0.19.2/flanneld-arm64
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x flanneld-arm64
&lt;span class="nb"&gt;sudo cp &lt;/span&gt;flanneld-arm64 /usr/local/bin/flanneld
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/k8s/flannel/networks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will install Flannel and apply its configuration once the master node is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Reboot (optional)
&lt;/h2&gt;

&lt;p&gt;Reboot the nodes and start fresh. It will ensure that the boot file from step 6 is loaded in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Start the cluster
&lt;/h2&gt;

&lt;p&gt;SSH into the master node and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;kubeadm config images pull
&lt;span class="nb"&gt;sudo &lt;/span&gt;kubeadm init &lt;span class="nt"&gt;--pod-network-cidr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.244.0.0/16 &lt;span class="nt"&gt;--token-ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the token and discovery token provided by the initialisation. You can always generate a new token later by doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;KUBE_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubeadm token generate&lt;span class="si"&gt;)&lt;/span&gt;
kubeadm token create &lt;span class="nv"&gt;$KUBE_TOKEN&lt;/span&gt; &lt;span class="nt"&gt;--print-join-command&lt;/span&gt; &lt;span class="nt"&gt;--ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create the kube config
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; /etc/kubernetes/admin.conf &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube/config
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Node verification
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes
NAME       STATUS   ROLES  AGE VERSION
k8s-master NotReady master 55m v1.25.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the node to become ready we need to install our CNI from step 11.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply the CNI config
&lt;/h2&gt;

&lt;p&gt;Assuming we are connected to our master node from the previous section, download Flannel's configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect the YAML configuration and replace the backend. More info is in the Flannel backend documentation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/vxlan/host-gw/'&lt;/span&gt; kube-flannel.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create &lt;span class="nt"&gt;-f&lt;/span&gt; kube-flannel.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add slave nodes to the cluster
&lt;/h2&gt;

&lt;p&gt;SSH into the slave nodes (k8s-node-1, k8s-node-2, ...) and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;K8S_MASTER_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;192.168.0.25
&lt;span class="nb"&gt;sudo &lt;/span&gt;kubeadm &lt;span class="nb"&gt;join&lt;/span&gt; &lt;span class="nv"&gt;$K8S_MASTER_IP&lt;/span&gt;:6443 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--token&lt;/span&gt; &amp;lt;token here&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--discovery-token-ca-cert-hash&lt;/span&gt; sha256:&amp;lt;&lt;span class="nb"&gt;hash &lt;/span&gt;value here&amp;gt;
&lt;span class="c"&gt;# Output:&lt;/span&gt;
This node has joined the cluster:
&lt;span class="k"&gt;*&lt;/span&gt; Certificate signing request was sent to apiserver and a response was received.
&lt;span class="k"&gt;*&lt;/span&gt; The Kubelet was informed of the new secure connection details.
Run &lt;span class="s1"&gt;'kubectl get nodes'&lt;/span&gt; on the control-plane to see this node &lt;span class="nb"&gt;join &lt;/span&gt;the cluster.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are now ready to start inspecting and extending our cluster.&lt;/p&gt;

&lt;p&gt;Leave a comment if you have any questions. We will be automating this setup next. Happy coding.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;First published in my LinkedIn newsletter, &lt;a href="https://www.linkedin.com/newsletters/built-from-scratch-6973801137594343424/" rel="noopener noreferrer"&gt;Built from Scratch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published on &lt;a href="https://juliantellez.com/blog/raspberry-pi-kubernetes-cluster-start-the-cluster" rel="noopener noreferrer"&gt;juliantellez.com&lt;/a&gt;. It is part of my newsletter on video, streaming, and building reliable systems from scratch: the architecture, the tradeoffs, and the occasional 3am incident.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If it was useful, &lt;a href="https://juliantellez.com/#newsletter" rel="noopener noreferrer"&gt;subscribe for the next one&lt;/a&gt;. No spam, unsubscribe anytime. If you are building something similar and want to compare notes, &lt;a href="mailto:hello@juliantellez.com"&gt;say hello&lt;/a&gt;. I read every message.&lt;/p&gt;

&lt;p&gt;Built from scratch, like everything here.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>homelab</category>
      <category>raspberrypi</category>
    </item>
    <item>
      <title>Raspberry Pi Kubernetes Cluster: SSH into the Nodes (Issue #3)</title>
      <dc:creator>Jules</dc:creator>
      <pubDate>Wed, 29 Jul 2026 12:59:56 +0000</pubDate>
      <link>https://dev.to/_juliantellez/raspberry-pi-kubernetes-cluster-ssh-into-the-nodes-issue-3-7cm</link>
      <guid>https://dev.to/_juliantellez/raspberry-pi-kubernetes-cluster-ssh-into-the-nodes-issue-3-7cm</guid>
      <description>&lt;p&gt;Welcome to the Raspberry Pi Kubernetes home lab series. A bite sized informative guide to help you provision a cluster from scratch. By now you should have 3+ nodes running the latest version of Ubuntu. Refer to the &lt;a href="https://juliantellez.com/blog/raspberry-pi-kubernetes-cluster-os-installation" rel="noopener noreferrer"&gt;previous guide&lt;/a&gt; if you would like to know more.&lt;/p&gt;

&lt;p&gt;This issue walks you through communicating with your Raspberry Pi nodes through SSH.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Raspberry Pi node with the latest version of Ubuntu, connected to either your home router or an Ethernet switch&lt;/li&gt;
&lt;li&gt;SSH should be enabled on the node&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install Nmap
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;nmap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nmap ("Network Mapper") is an open source tool for network exploration and security auditing, designed to rapidly scan networks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the device's IP address
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find the router's IP&lt;/span&gt;
netstat &lt;span class="nt"&gt;-nr&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;default

&lt;span class="c"&gt;# Inspect nodes on your local network&lt;/span&gt;
nmap &lt;span class="nt"&gt;-sP&lt;/span&gt; 192.168.0.1/24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the snippet above, the nodes are on 192.168.0.[24-26].&lt;/p&gt;

&lt;h2&gt;
  
  
  SSH into nodes manually
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ssh into device, password is usually "ubuntu"&lt;/span&gt;
ssh ubuntu@192.168.0.26
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  SSH into nodes with private keys
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; rsa &lt;span class="nt"&gt;-b&lt;/span&gt; 4096
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the SSH key to the node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-copy-id &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/ansible ubuntu@192.168.0.26
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SSH with the RSA key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/ansible ubuntu@192.168.0.26
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You are now ready to start your Kubernetes setup. It is important that you know how to access the nodes independently and that you can reach them both via SSH keys and as a root user.&lt;/p&gt;

&lt;p&gt;Leave a comment if you have any questions. The next step will be &lt;a href="https://juliantellez.com/blog/raspberry-pi-kubernetes-cluster-start-the-cluster" rel="noopener noreferrer"&gt;installing dependencies on the nodes&lt;/a&gt;. Happy coding.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;First published in my LinkedIn newsletter, &lt;a href="https://www.linkedin.com/newsletters/built-from-scratch-6973801137594343424/" rel="noopener noreferrer"&gt;Built from Scratch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published on &lt;a href="https://juliantellez.com/blog/raspberry-pi-kubernetes-cluster-ssh-into-the-nodes" rel="noopener noreferrer"&gt;juliantellez.com&lt;/a&gt;. It is part of my newsletter on video, streaming, and building reliable systems from scratch: the architecture, the tradeoffs, and the occasional 3am incident.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If it was useful, &lt;a href="https://juliantellez.com/#newsletter" rel="noopener noreferrer"&gt;subscribe for the next one&lt;/a&gt;. No spam, unsubscribe anytime. If you are building something similar and want to compare notes, &lt;a href="mailto:hello@juliantellez.com"&gt;say hello&lt;/a&gt;. I read every message.&lt;/p&gt;

&lt;p&gt;Built from scratch, like everything here.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>homelab</category>
      <category>raspberrypi</category>
    </item>
    <item>
      <title>Raspberry Pi Kubernetes Cluster: OS Installation (Issue #2)</title>
      <dc:creator>Jules</dc:creator>
      <pubDate>Wed, 29 Jul 2026 12:59:20 +0000</pubDate>
      <link>https://dev.to/_juliantellez/raspberry-pi-kubernetes-cluster-os-installation-issue-2-4o9f</link>
      <guid>https://dev.to/_juliantellez/raspberry-pi-kubernetes-cluster-os-installation-issue-2-4o9f</guid>
      <description>&lt;p&gt;Welcome to the Raspberry Pi Kubernetes home lab series. A bite sized informative guide to help you provision a cluster from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Install Ubuntu on a Raspberry Pi
&lt;/h2&gt;

&lt;p&gt;The Raspberry Pi nodes are provisioned using &lt;a href="https://ubuntu.com/download/raspberry-pi" rel="noopener noreferrer"&gt;Ubuntu 22.04 LTS&lt;/a&gt; for ARM. Download and unzip the image from the link.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Locate the SD card
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ss64.com/osx/diskutil.html" rel="noopener noreferrer"&gt;Diskutil&lt;/a&gt; provides information about the formats of disks, which include disk images.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Unmount the disk
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diskutil unmountDisk /dev/disk2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unmount the disk so you can write onto it. Note that unmount is not the same as eject. Unmount means to remove the connection to the disk internally; eject means to physically remove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. dd the image into the disk
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;path-to-image&amp;gt;/raspbian.img &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/rdisk2 &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is important that you set the block size read/write buffering to optimise the overall time of the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Enable SSH
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; /Volumes/system-boot/ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SSH needs to be enabled so we can talk to the nodes later.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Unmount and eject/remove the card
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diskutil unmountDisk /dev/disk2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave a comment if you have any questions. We will be installing and preparing the nodes next, first manually and then in an automated fashion via Ansible. Happy coding.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;First published in my LinkedIn newsletter, &lt;a href="https://www.linkedin.com/newsletters/built-from-scratch-6973801137594343424/" rel="noopener noreferrer"&gt;Built from Scratch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published on &lt;a href="https://juliantellez.com/blog/raspberry-pi-kubernetes-cluster-os-installation" rel="noopener noreferrer"&gt;juliantellez.com&lt;/a&gt;. It is part of my newsletter on video, streaming, and building reliable systems from scratch: the architecture, the tradeoffs, and the occasional 3am incident.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If it was useful, &lt;a href="https://juliantellez.com/#newsletter" rel="noopener noreferrer"&gt;subscribe for the next one&lt;/a&gt;. No spam, unsubscribe anytime. If you are building something similar and want to compare notes, &lt;a href="mailto:hello@juliantellez.com"&gt;say hello&lt;/a&gt;. I read every message.&lt;/p&gt;

&lt;p&gt;Built from scratch, like everything here.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>homelab</category>
      <category>raspberrypi</category>
    </item>
    <item>
      <title>Raspberry Pi Kubernetes Cluster: NFS Storage (Issue #5)</title>
      <dc:creator>Jules</dc:creator>
      <pubDate>Wed, 29 Jul 2026 12:58:44 +0000</pubDate>
      <link>https://dev.to/_juliantellez/raspberry-pi-kubernetes-cluster-nfs-storage-issue-5-4fm0</link>
      <guid>https://dev.to/_juliantellez/raspberry-pi-kubernetes-cluster-nfs-storage-issue-5-4fm0</guid>
      <description>&lt;p&gt;Welcome to the Raspberry Pi Kubernetes home lab series. A bite sized informative guide to help you provision a cluster from scratch. By now we should have our &lt;a href="https://juliantellez.com/blog/raspberry-pi-kubernetes-cluster-start-the-cluster" rel="noopener noreferrer"&gt;k8s cluster up and running&lt;/a&gt;; great job everyone.&lt;/p&gt;

&lt;p&gt;Today's issue will walk you through installing and creating a &lt;a href="https://en.wikipedia.org/wiki/Network_File_System" rel="noopener noreferrer"&gt;Network File System&lt;/a&gt; (NFS) server so it can be used as our default cluster storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1 &lt;a href="https://juliantellez.com/blog/raspberry-pi-kubernetes-cluster-start-the-cluster" rel="noopener noreferrer"&gt;k8s cluster&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;1 external hard drive&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Locate the hard drive
&lt;/h2&gt;

&lt;p&gt;Plug your hard drive into one of your slave nodes and &lt;a href="https://juliantellez.com/blog/raspberry-pi-kubernetes-cluster-ssh-into-the-nodes" rel="noopener noreferrer"&gt;SSH into it&lt;/a&gt; to find its location.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;lsblk &lt;span class="nt"&gt;-o&lt;/span&gt; UUID,NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL,MODEL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Format the hard drive with mkfs.ext3
&lt;/h2&gt;

&lt;p&gt;We can see that our hard drive is located at /dev/sda2. Let's go ahead and format the hard drive's filesystem so it can be used by the Linux system. Other examples include /dev/sdb2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mkfs.ext3 &lt;span class="nt"&gt;-L&lt;/span&gt; k8s-volume /dev/sda2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Create a directory for the volume and mount it
&lt;/h2&gt;

&lt;p&gt;This could be in any location. We are going to "namespace" it under k8s-mount and label it k8s-volume-1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /k8s-mount/k8s-volume-1
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount /dev/sda2 /k8s-mount/k8s-volume-1 &lt;span class="c"&gt;# mount disk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Install the NFS server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;nfs-kernel-server &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Start the server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start nfs-server
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status nfs-server &lt;span class="c"&gt;# verify it is running&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Allocate permissions
&lt;/h2&gt;

&lt;p&gt;In order to have access to your NFS server we need to create some read/write permissions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# owner (read/write) group (read)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;find /k8s-mount/k8s-volume-1/ &lt;span class="nt"&gt;-type&lt;/span&gt; d &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;755 &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;find /k8s-mount/k8s-volume-1/ &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;644 &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;find /k8s-mount/k8s-volume-1/ &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;777 &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Set up remote access
&lt;/h2&gt;

&lt;p&gt;The line below will give permissions to all hosts in the network, so be careful!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/exports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/k8s-mount/k8s-volume-1/ *(rw,sync,no_subtree_check,insecure)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can set the permissions to be restrictive per host. Let me know if you would like to see how this is done. In the meantime, here is a &lt;a href="https://pimylifeup.com/raspberry-pi-nfs/" rel="noopener noreferrer"&gt;walk through of this setup&lt;/a&gt; and the &lt;a href="https://linux.die.net/man/5/exports" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; too.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Export the etc config
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;exportfs &lt;span class="nt"&gt;-ra&lt;/span&gt; &lt;span class="c"&gt;# export config&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;exportfs &lt;span class="nt"&gt;-v&lt;/span&gt;  &lt;span class="c"&gt;# see current config&lt;/span&gt;
showmount &lt;span class="nt"&gt;-e&lt;/span&gt;      &lt;span class="c"&gt;# see what is being exported&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  10. Install the NFS client on all the other worker nodes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ssh ubuntu@192.168.0.xx # WORKER_NODE&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nfs-common &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  11. Create volume manifests
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# system-volume.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Namespace&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system-volume&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system-volume&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PersistentVolume&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pv-k8s-volume-1&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;storageClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;k8s-volume-1&lt;/span&gt;
  &lt;span class="na"&gt;capacity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1Ti&lt;/span&gt;
  &lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ReadWriteMany&lt;/span&gt;
  &lt;span class="na"&gt;nfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;192.168.0.26&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/k8s-mount/k8s-volume-1&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pvc-k8s-volume-1&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;storageClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;k8s-volume-1&lt;/span&gt;
  &lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ReadWriteMany&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1Ti&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  12. Apply the manifests
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-k&lt;/span&gt; path-to-manifest.yaml
kubectl &lt;span class="nt"&gt;--namespace&lt;/span&gt; system-volume get pv,pvc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;

&lt;p&gt;This isn't a required section but more of an educational addendum. Since we have given global access to our drive, we can now access it anywhere from within the network.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Find nodes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-sP&lt;/span&gt; 192.168.0.1/24 &lt;span class="c"&gt;# list nodes&lt;/span&gt;

&lt;span class="nv"&gt;WORKER_NODE_1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;192.168.0.24
&lt;span class="nv"&gt;WORKER_NODE_2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;192.168.0.26 &lt;span class="c"&gt;# hard drive lives here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Find the volume on the network
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh ubuntu@&lt;span class="nv"&gt;$WORKER_NODE_1&lt;/span&gt; &lt;span class="c"&gt;# a worker node&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nfs-common

showmount &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;$WORKER_NODE_2&lt;/span&gt; &lt;span class="c"&gt;# see what is being exported&lt;/span&gt;

&lt;span class="c"&gt;# output&lt;/span&gt;
Export list &lt;span class="k"&gt;for &lt;/span&gt;192.168.0.26:
/k8s-mount/k8s-volume-1 &lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Access the hard drive
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /test
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-t&lt;/span&gt; nfs &lt;span class="nv"&gt;$WORKER_NODE_2&lt;/span&gt;:/k8s-mount/k8s-volume-1 /test/
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-al&lt;/span&gt; /test/ &lt;span class="c"&gt;# inspect drive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. End of test
&lt;/h3&gt;

&lt;p&gt;Clear the test area by removing the files created previously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;umount /test
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are now ready to start using our local storage. It is important that you know how to access the nodes independently and that you understand how to manipulate the NFS server across the network.&lt;/p&gt;

&lt;p&gt;Leave a comment if you have any questions. We will be integrating with &lt;a href="https://metallb.universe.tf/" rel="noopener noreferrer"&gt;MetalLB&lt;/a&gt;'s load balancer next. Happy coding.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;First published in my LinkedIn newsletter, &lt;a href="https://www.linkedin.com/newsletters/built-from-scratch-6973801137594343424/" rel="noopener noreferrer"&gt;Built from Scratch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published on &lt;a href="https://juliantellez.com/blog/raspberry-pi-kubernetes-cluster-nfs-storage" rel="noopener noreferrer"&gt;juliantellez.com&lt;/a&gt;. It is part of my newsletter on video, streaming, and building reliable systems from scratch: the architecture, the tradeoffs, and the occasional 3am incident.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If it was useful, &lt;a href="https://juliantellez.com/#newsletter" rel="noopener noreferrer"&gt;subscribe for the next one&lt;/a&gt;. No spam, unsubscribe anytime. If you are building something similar and want to compare notes, &lt;a href="mailto:hello@juliantellez.com"&gt;say hello&lt;/a&gt;. I read every message.&lt;/p&gt;

&lt;p&gt;Built from scratch, like everything here.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>homelab</category>
      <category>raspberrypi</category>
    </item>
    <item>
      <title>Raspberry Pi Kubernetes Cluster: Load Balancer (Issue #6)</title>
      <dc:creator>Jules</dc:creator>
      <pubDate>Wed, 29 Jul 2026 12:58:08 +0000</pubDate>
      <link>https://dev.to/_juliantellez/raspberry-pi-kubernetes-cluster-load-balancer-issue-6-241e</link>
      <guid>https://dev.to/_juliantellez/raspberry-pi-kubernetes-cluster-load-balancer-issue-6-241e</guid>
      <description>&lt;p&gt;Welcome to the Raspberry Pi Kubernetes home lab series. A bite sized informative guide to help you provision a cluster from scratch. Our cluster should now be up and running and we should be able to easily provision services, deployments, volumes, and so on.&lt;/p&gt;

&lt;p&gt;Today's issue will walk you through &lt;a href="https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types" rel="noopener noreferrer"&gt;exposing a service to an external IP address&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The challenge with bare metal
&lt;/h2&gt;

&lt;p&gt;Since our cluster is running on bare metal we don't have the luxury of provisioning an external load balancer. Kubernetes doesn't provide a direct implementation for network load balancers and instead comes with built-in functionality to integrate with different cloud providers (AWS, GCP, Azure, and so on). This is normally done by allowing Kubernetes to automatically create load balancers on your cloud provider. Without the ability to load balance traffic we are left with two options, "NodePort" and "ClusterIP", which are solutions better suited for internal network or cluster communication.&lt;/p&gt;

&lt;p&gt;In this lab we will be implementing &lt;a href="https://metallb.universe.tf/" rel="noopener noreferrer"&gt;MetalLB&lt;/a&gt;, a solution that offers network balancing so that external traffic and services can communicate with our cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1 k8s cluster with a compatible CNI&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Preparation
&lt;/h2&gt;

&lt;p&gt;Please check the compatibility matrix before you implement MetalLB. If you are following this series you would have installed &lt;a href="https://github.com/flannel-io/flannel" rel="noopener noreferrer"&gt;Flannel&lt;/a&gt; and therefore are good to go! Kubernetes CNI integrations are very interesting and worth a read in their own right. &lt;a href="https://projectcalico.docs.tigera.io/about/about-networking" rel="noopener noreferrer"&gt;Calico&lt;/a&gt;, for example, has a really interesting implementation and is definitely worth exploring.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Installation
&lt;/h2&gt;

&lt;p&gt;Version 0.13.5 has come a long way; integrating with MetalLB is incredibly simple. We will resource this latest version and supply a configuration file along with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  kustomization.yaml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kustomize.config.k8s.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Kustomization&lt;/span&gt;
&lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metallb-system&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;github.com/metallb/metallb/config/native?ref=v0.13.5&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;config.yaml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MetalLB is set up as an L2 balancer by default and will be idle until configured. The easiest deployment is probably the Layer 2 configuration, which works by responding to &lt;a href="https://en.wikipedia.org/wiki/Address_Resolution_Protocol" rel="noopener noreferrer"&gt;ARP requests&lt;/a&gt; and providing the machine's MAC address to clients.&lt;/p&gt;

&lt;p&gt;As a bare minimum (no pun intended) it will expect a pool of available addresses in your network, and a reference for them. From the network perspective it is represented as if the cluster had multiple IPs assigned to its network interface. As you can see from the configuration below, we are assigning ports 100 to 250, which should be more than enough for our Raspberry Pi experiments.&lt;/p&gt;

&lt;h3&gt;
  
  
  config.yaml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metallb.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IPAddressPool&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;first-pool&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metallb-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;addresses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;192.168.0.100-192.168.1.250&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metallb.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;L2Advertisement&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;advertisment&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metallb-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ipAddressPools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;first-pool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the &lt;a href="https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/" rel="noopener noreferrer"&gt;kustomization&lt;/a&gt; file and verify. MetalLB will create a daemon set "speaker" on every slave node, which will ensure the load balancer can reach our services, and a controller (responsible for address assignments).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apply&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-k&lt;/span&gt; &amp;lt;kustomization_directory&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Test the load balancer
&lt;/h2&gt;

&lt;p&gt;Let's deploy an nginx service and assign a load balancer to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run nginx &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nginx
kubectl expose deploy nginx &lt;span class="nt"&gt;--port&lt;/span&gt; 80 &lt;span class="nt"&gt;--type&lt;/span&gt; LoadBalancer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's check that our service has indeed deployed and that it has been assigned an external IP.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c"&gt;# Output&lt;/span&gt;
NAME            TYPE           CLUSTER-IP   EXTERNAL-IP
service/nginx   LoadBalancer   10.96.4.10   192.168.0.230
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there we go, our service is now available to the network!&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;We are now ready to start assigning IPs to our services. It is important that you know how Layer 2 balancers work so you can extend the basic configuration for MetalLB.&lt;/p&gt;

&lt;p&gt;Leave a comment if you have any questions. We will be looking into provisioning Kubernetes secrets powered by &lt;a href="https://git-secret.io/" rel="noopener noreferrer"&gt;git-secret&lt;/a&gt; next. Happy coding.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;First published in my LinkedIn newsletter, &lt;a href="https://www.linkedin.com/newsletters/built-from-scratch-6973801137594343424/" rel="noopener noreferrer"&gt;Built from Scratch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published on &lt;a href="https://juliantellez.com/blog/raspberry-pi-kubernetes-cluster-load-balancer" rel="noopener noreferrer"&gt;juliantellez.com&lt;/a&gt;. It is part of my newsletter on video, streaming, and building reliable systems from scratch: the architecture, the tradeoffs, and the occasional 3am incident.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If it was useful, &lt;a href="https://juliantellez.com/#newsletter" rel="noopener noreferrer"&gt;subscribe for the next one&lt;/a&gt;. No spam, unsubscribe anytime. If you are building something similar and want to compare notes, &lt;a href="mailto:hello@juliantellez.com"&gt;say hello&lt;/a&gt;. I read every message.&lt;/p&gt;

&lt;p&gt;Built from scratch, like everything here.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>homelab</category>
      <category>raspberrypi</category>
    </item>
    <item>
      <title>Raspberry Pi Kubernetes Cluster: Equipment (Issue #1)</title>
      <dc:creator>Jules</dc:creator>
      <pubDate>Wed, 29 Jul 2026 12:56:17 +0000</pubDate>
      <link>https://dev.to/_juliantellez/raspberry-pi-kubernetes-cluster-equipment-issue-1-4la7</link>
      <guid>https://dev.to/_juliantellez/raspberry-pi-kubernetes-cluster-equipment-issue-1-4la7</guid>
      <description>&lt;p&gt;This is the first in a series on building a Kubernetes home lab from Raspberry Pi computers. Treat it as an informative guide rather than a tutorial. If you want the deep, step-by-step path, "Kubernetes the Hard Way" is the place to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;The usual Kubernetes prerequisites apply to each machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A compatible Linux host&lt;/li&gt;
&lt;li&gt;At least 2GB of RAM per machine&lt;/li&gt;
&lt;li&gt;2 or more CPUs&lt;/li&gt;
&lt;li&gt;Full network connectivity between all nodes&lt;/li&gt;
&lt;li&gt;A unique hostname, MAC address and product UUID per node&lt;/li&gt;
&lt;li&gt;The required ports open between nodes&lt;/li&gt;
&lt;li&gt;Swap disabled, so the kubelet behaves correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Equipment
&lt;/h2&gt;

&lt;p&gt;What I used to build the cluster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compute:&lt;/strong&gt; 4 x Raspberry Pi 4 (one master node and three worker nodes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; 4 x 32GB microSD cards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networking:&lt;/strong&gt; an Ethernet switch, a router with DHCP, and a cable per node&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power:&lt;/strong&gt; USB-C / micro USB cables and a multi-amp USB charger&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cooling:&lt;/strong&gt; a case with fans and heatsinks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional:&lt;/strong&gt; an external hard drive for extra storage&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;img src="/blog-media/raspberry-pi-kubernetes-cluster-equipment/raspberry-pi-4.jpeg" alt="Raspberry Pi 4"&amp;gt;
&amp;lt;figcaption&amp;gt;Raspberry Pi 4&amp;lt;/figcaption&amp;gt;


&amp;lt;img src="/blog-media/raspberry-pi-kubernetes-cluster-equipment/microsd-card.jpeg" alt="MicroSD card"&amp;gt;
&amp;lt;figcaption&amp;gt;MicroSD card&amp;lt;/figcaption&amp;gt;


&amp;lt;img src="/blog-media/raspberry-pi-kubernetes-cluster-equipment/ethernet-switch.jpeg" alt="Ethernet switch"&amp;gt;
&amp;lt;figcaption&amp;gt;Ethernet switch&amp;lt;/figcaption&amp;gt;


&amp;lt;img src="/blog-media/raspberry-pi-kubernetes-cluster-equipment/router.jpeg" alt="Router"&amp;gt;
&amp;lt;figcaption&amp;gt;Router&amp;lt;/figcaption&amp;gt;


&amp;lt;img src="/blog-media/raspberry-pi-kubernetes-cluster-equipment/ethernet-cables.jpeg" alt="Ethernet cables"&amp;gt;
&amp;lt;figcaption&amp;gt;Ethernet cables&amp;lt;/figcaption&amp;gt;


&amp;lt;img src="/blog-media/raspberry-pi-kubernetes-cluster-equipment/usb-c-cables.jpeg" alt="USB-C cables"&amp;gt;
&amp;lt;figcaption&amp;gt;USB-C cables&amp;lt;/figcaption&amp;gt;


&amp;lt;img src="/blog-media/raspberry-pi-kubernetes-cluster-equipment/usb-charger.jpeg" alt="USB charger"&amp;gt;
&amp;lt;figcaption&amp;gt;USB charger&amp;lt;/figcaption&amp;gt;


&amp;lt;img src="/blog-media/raspberry-pi-kubernetes-cluster-equipment/cluster-case.jpeg" alt="Cluster case"&amp;gt;
&amp;lt;figcaption&amp;gt;Cluster case&amp;lt;/figcaption&amp;gt;


&amp;lt;img src="/blog-media/raspberry-pi-kubernetes-cluster-equipment/external-hard-drive.jpeg" alt="External hard drive"&amp;gt;
&amp;lt;figcaption&amp;gt;External hard drive&amp;lt;/figcaption&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;A good case with proper cooling makes a real difference once the nodes are under load, so it is worth following an assembly guide for the heatsink and fan setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;Later issues in the series cover configuring the nodes, SSH access, initialising the cluster, storage, and load balancing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;First published in my LinkedIn newsletter, &lt;a href="https://www.linkedin.com/newsletters/built-from-scratch-6973801137594343424/" rel="noopener noreferrer"&gt;Built from Scratch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published on &lt;a href="https://juliantellez.com/blog/raspberry-pi-kubernetes-cluster-equipment" rel="noopener noreferrer"&gt;juliantellez.com&lt;/a&gt;. It is part of my newsletter on video, streaming, and building reliable systems from scratch: the architecture, the tradeoffs, and the occasional 3am incident.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If it was useful, &lt;a href="https://juliantellez.com/#newsletter" rel="noopener noreferrer"&gt;subscribe for the next one&lt;/a&gt;. No spam, unsubscribe anytime. If you are building something similar and want to compare notes, &lt;a href="mailto:hello@juliantellez.com"&gt;say hello&lt;/a&gt;. I read every message.&lt;/p&gt;

&lt;p&gt;Built from scratch, like everything here.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>homelab</category>
      <category>raspberrypi</category>
    </item>
    <item>
      <title>Can Software Hear an Accent?</title>
      <dc:creator>Jules</dc:creator>
      <pubDate>Wed, 29 Jul 2026 12:56:16 +0000</pubDate>
      <link>https://dev.to/_juliantellez/can-software-hear-an-accent-15da</link>
      <guid>https://dev.to/_juliantellez/can-software-hear-an-accent-15da</guid>
      <description>&lt;p&gt;I have been learning Russian. Vocabulary I can grind. Grammar I can study. The part that humbles me every week is the accent. I say a word slowly, feel like I nailed it, and my tutor smiles and says "close." Close where? Which sound? By how much? A human ear knows instantly and cannot always explain it. So I started wondering whether software could tell me instead. Could a machine hear my accent the way my tutor can?&lt;/p&gt;

&lt;p&gt;That is the question I wanted to test, so I wrote it down as a claim I could try to break.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hypothesis: an ordinary speech engine can catch a single mispronounced sound as reliably as a trained ear can.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What "hearing an accent" even means
&lt;/h2&gt;

&lt;p&gt;Before testing it, it helps to be clear about what we are asking the machine to hear, because an accent is not one thing. It is at least two.&lt;/p&gt;

&lt;p&gt;The first is the individual sounds, the phonemes. Russian has a vowel, ы, that English simply does not, and English speakers reach for the nearest thing they know, и, the "ee" in "see." Swap one for the other and you have said a different word.&lt;/p&gt;

&lt;p&gt;The second, and the one people underrate, is &lt;strong&gt;prosody&lt;/strong&gt;: the melody, stress and rhythm of speech. Prosody is the tune you hum when you have forgotten the words. It is how "really" can mean agreement or sarcasm with the same three syllables. It is why a sentence with every individual sound correct can still land as foreign, because the rises and falls sit in the wrong places.&lt;/p&gt;

&lt;p&gt;For humans, prosody does a lot of quiet work. It marks a question without a single word changing. It puts the emphasis that tells you which part of a sentence matters. It carries emotion, and it manages the back and forth of a conversation. Research on second language speech keeps landing on the same finding: getting the melody and rhythm right does more for being understood than getting every individual sound perfect. We forgive a foreign vowel. We stumble when the stress lands on the wrong syllable.&lt;/p&gt;

&lt;p&gt;Which is exactly what makes prosody the interesting problem for software. A single sound is a short, local event you can line up against a reference and grade. Prosody is a shape spread across a whole phrase: pitch rising and falling, syllables stretching and compressing. To score it, a machine has to follow the tune of your voice over time and judge the whole contour, not a snapshot. Getting a machine to hear that shape, and then say something useful about it, is genuinely hard.&lt;/p&gt;

&lt;p&gt;So the test has two halves. Can the machine hear a single wrong sound, and can it hear the melody around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you test whether a machine can hear a sound
&lt;/h2&gt;

&lt;p&gt;The tool for the first half is pronunciation assessment. You hand the engine a recording and the words you meant to say, and it scores how closely the sounds match, right down to each phoneme. I used &lt;a href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-pronunciation-assessment" rel="noopener noreferrer"&gt;Azure's pronunciation assessment&lt;/a&gt;, because it is one of the few that claims to support Russian, not just English.&lt;/p&gt;

&lt;p&gt;The trap with any scorer is that it might simply be lenient. Feed it anything and it says "great, 95." So the test is built around minimal pairs. A minimal pair is two words that differ by exactly one sound. Russian мыл (washed) and мил (nice) differ only in that one vowel, ы against и. If the engine can really hear an accent, then a recording of мил, submitted as an attempt at мыл, should score badly, and badly because of that one vowel. If it waves it through, the whole idea is dead on arrival.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Russian test
&lt;/h2&gt;

&lt;p&gt;Here is the reference word мыл, said cleanly, next to мил, the exact slip an English speaker makes, scored against мыл. Press play on both and watch the number.&lt;/p&gt;

&lt;p&gt;
  reference="мыл" note="washed, and the vowel is ы"&lt;br&gt;
  aWord="мыл" aIpa="/mɨl/" aTag="correct ы" aScore="100" aTone="good" aSrc="ru-myl"&lt;br&gt;
  bWord="мил" bIpa="/mʲil/" bNote="the ы → и slip" bTag="error caught" bScore="4.8" bTone="bad" bSrc="ru-mil"&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;p&gt;The correct vowel scores 100. The single wrong vowel drops it to under five. Same story with быть (to be) and бить (to hit), which again part ways on that one vowel.&lt;/p&gt;

&lt;p&gt;
  reference="быть" note="to be, and the vowel is ы"&lt;br&gt;
  aWord="быть" aIpa="/bɨtʲ/" aTag="correct ы" aScore="100" aTone="good" aSrc="ru-byt"&lt;br&gt;
  bWord="бить" bIpa="/bʲitʲ/" bNote="the ы → и slip" bTag="error caught" bScore="0" bTone="bad" bSrc="ru-bit"&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it just failing everything?
&lt;/h2&gt;

&lt;p&gt;A scorer that flags errors is only useful if it also passes good speech and rejects nonsense. So, two controls. Clean native recordings of ordinary words all score around 100, so the ceiling is where it should be. And when the audio does not match the claimed words at all, the score falls to zero rather than coasting in the eighties. It is discriminating, not merely harsh.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A single wrong vowel took the score from 100 to under 5. In a language the engine barely supports, it heard the accent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  English, and the melody
&lt;/h2&gt;

&lt;p&gt;English is the engine's home turf, and it behaves a little differently there. Take sheep and ship, a pair split by one vowel, and think and sink, split by their first sound, the English "th" that a lot of languages lack.&lt;/p&gt;

&lt;p&gt;
  reference="sheep" note="the long FLEECE vowel"&lt;br&gt;
  aWord="sheep" aIpa="/ʃiːp/" aTag="correct vowel" aScore="100" aTone="good" aSrc="en-sheep"&lt;br&gt;
  bWord="ship" bIpa="/ʃɪp/" bNote="the ee → i slip" bTag="vowel flagged" bScore="88" bTone="warn" bSrc="en-ship"&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;p&gt;
  reference="think" note="the first sound is the English th"&lt;br&gt;
  aWord="think" aIpa="/θɪŋk/" aTag="correct th" aScore="94" aTone="good" aSrc="en-think"&lt;br&gt;
  bWord="sink" bIpa="/sɪŋk/" bNote="the θ → s slip" bTag="th flagged" bScore="68" bTone="warn" bSrc="en-sink"&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;p&gt;Here the errors are caught but scored more gently. The engine hands out partial credit and points at the offending sound rather than collapsing to zero. Arguably that is the more honest behaviour: someone saying "ship" for "sheep" is wrong, but not unintelligible.&lt;/p&gt;

&lt;p&gt;English also unlocks the second half of the hypothesis, the prosody. On English the engine returns a separate score for intonation and rhythm, the melody from earlier. This next clip is a whole sentence, and the number is not about its individual sounds at all. It is the machine judging the tune.&lt;/p&gt;

&lt;p&gt;
  heading="bonus" note="prosody, intonation and rhythm"&lt;br&gt;
  word="I would like a cup of tea, please."&lt;br&gt;
  sub="scored for the melody, not the individual sounds"&lt;br&gt;
  score="91" tone="good" tag="prosody scored" src="en-sentence"&lt;br&gt;
/&amp;gt;&lt;/p&gt;

&lt;p&gt;That score is the engine tracking the shape of a voice across a whole phrase and rating it, which is exactly the thing that is hard to teach and hard to measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this breaks down
&lt;/h2&gt;

&lt;p&gt;The demo looks cleaner than the reality, so three honest caveats.&lt;/p&gt;

&lt;p&gt;The voices here are synthesized, not human. Real learners make graded, halfway errors that should land in the middle of the scale, not at zero. The engine clearly has the sensitivity. How it behaves on messy human speech is the next experiment.&lt;/p&gt;

&lt;p&gt;The engine will tell you a word is wrong, but it is coarse about which sound and by how much, and for some languages it does not label the sounds at all. Turning "this word scored low" into "your ы specifically drifted toward и" has to be reconstructed on top of it.&lt;/p&gt;

&lt;p&gt;And prosody scoring is only offered for some accents. The melody is the hardest part to hear, and it is also the least evenly supported, which is a little ironic given how much of an accent it carries.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, can software hear an accent?
&lt;/h2&gt;

&lt;p&gt;On this evidence, more than I expected. It hears a single wrong vowel in a language it is barely trained on. It tells the difference between a real sound and a nonsense noise. And in English it hears the prosody, the melody and rhythm that carry so much of what makes an accent sound foreign in the first place.&lt;/p&gt;

&lt;p&gt;What it does not do yet is coach. Hearing the error is the easy half. Telling a learner what to do with their tongue, and being kind about it, is the harder and more human half. But the part I assumed would be science fiction, a machine hearing the exact sound I got wrong, already works today. That is a good place to start.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was originally published on &lt;a href="https://juliantellez.com/blog/can-software-hear-an-accent" rel="noopener noreferrer"&gt;juliantellez.com&lt;/a&gt;. It is part of my newsletter on video, streaming, and building reliable systems from scratch: the architecture, the tradeoffs, and the occasional 3am incident.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If it was useful, &lt;a href="https://juliantellez.com/#newsletter" rel="noopener noreferrer"&gt;subscribe for the next one&lt;/a&gt;. No spam, unsubscribe anytime. If you are building something similar and want to compare notes, &lt;a href="mailto:hello@juliantellez.com"&gt;say hello&lt;/a&gt;. I read every message.&lt;/p&gt;

&lt;p&gt;Built from scratch, like everything here.&lt;/p&gt;

</description>
      <category>speech</category>
      <category>languagelearning</category>
      <category>ai</category>
      <category>prosody</category>
    </item>
  </channel>
</rss>
