<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tailored Cloud</title>
	<atom:link href="https://tailored.cloud/feed/" rel="self" type="application/rss+xml" />
	<link>https://tailored.cloud/</link>
	<description>Kubernetes, devops and everything cloud</description>
	<lastBuildDate>Thu, 18 Jul 2024 17:16:02 +0000</lastBuildDate>
	<language>en-GB</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.6</generator>
	<item>
		<title>How to Get a Fully Functional, GitOps Driven Kubernetes Cluster for Free</title>
		<link>https://tailored.cloud/devops/gitops-driven-kubernetes-cluster-for-free/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=gitops-driven-kubernetes-cluster-for-free</link>
					<comments>https://tailored.cloud/devops/gitops-driven-kubernetes-cluster-for-free/#respond</comments>
		
		<dc:creator><![CDATA[tc-admin]]></dc:creator>
		<pubDate>Thu, 18 Jul 2024 17:02:38 +0000</pubDate>
				<category><![CDATA[devops]]></category>
		<category><![CDATA[flux]]></category>
		<category><![CDATA[kubernetes]]></category>
		<guid isPermaLink="false">https://tailored.cloud/?p=489</guid>

					<description><![CDATA[<p>For the last few years, I&#8217;ve been working with my team at Giant Swarm on GitOps setup for our Kubernetes <a class="more-link" href="https://tailored.cloud/devops/gitops-driven-kubernetes-cluster-for-free/">Continue Reading</a></p>
<p>The post <a href="https://tailored.cloud/devops/gitops-driven-kubernetes-cluster-for-free/">How to Get a Fully Functional, GitOps Driven Kubernetes Cluster for Free</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>For the last few years, I&#8217;ve been working with my team at Giant Swarm on GitOps setup for our Kubernetes clusters (well, amongst other things). Recently, I&#8217;ve been more vocal about what we do and how do that &#8211; you can check my video from the <a href="https://www.youtube.com/watch?v=i4SsVIB_VkQ">Cloud Native Rejekts EU &#8217;24</a> presentation and an <a href="https://www.youtube.com/watch?v=_WGLpoxI0pg">episode I did recently with Johannes Koch</a> on his YouTube channel.</p>



<p>For some time now, I&#8217;m also doing a project that anyone can use to create your own Kubernetes cluster, that is entirely driven by the <a href="https://fluxcd.io/">Flux project</a>. What&#8217;s probably the best part of it, by using a free layer infrastructure from Oracle Cloud, you can run this cluster completely for free! So this gives you a fully functional Kubernetes cluster, managed by a great GitOps tool, and without paying a penny. Check the repo here: <a href="https://github.com/piontec/free-oci-kubernetes/">https://github.com/piontec/free-oci-kubernetes/</a></p>
<p>The post <a href="https://tailored.cloud/devops/gitops-driven-kubernetes-cluster-for-free/">How to Get a Fully Functional, GitOps Driven Kubernetes Cluster for Free</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tailored.cloud/devops/gitops-driven-kubernetes-cluster-for-free/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Repairing the Gaggia Classic</title>
		<link>https://tailored.cloud/hobby/repairing-the-gaggia-classic/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=repairing-the-gaggia-classic</link>
					<comments>https://tailored.cloud/hobby/repairing-the-gaggia-classic/#respond</comments>
		
		<dc:creator><![CDATA[tc-admin]]></dc:creator>
		<pubDate>Sat, 06 May 2023 16:35:43 +0000</pubDate>
				<category><![CDATA[hobby]]></category>
		<category><![CDATA[coffee]]></category>
		<category><![CDATA[espresso]]></category>
		<category><![CDATA[gaggia classic]]></category>
		<category><![CDATA[IoT]]></category>
		<category><![CDATA[open source]]></category>
		<guid isPermaLink="false">https://tailored.cloud/?p=477</guid>

					<description><![CDATA[<p>When I bought my machine, it wasn&#8217;t working and was sold as &#8220;unknown, non-working condition&#8221;. This model of espresso machine <a class="more-link" href="https://tailored.cloud/hobby/repairing-the-gaggia-classic/">Continue Reading</a></p>
<p>The post <a href="https://tailored.cloud/hobby/repairing-the-gaggia-classic/">Repairing the Gaggia Classic</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="768" src="https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220812_115731990-1024x768.jpg" alt="" class="wp-image-484" srcset="https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220812_115731990-1024x768.jpg 1024w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220812_115731990-300x225.jpg 300w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220812_115731990-768x576.jpg 768w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220812_115731990-1536x1152.jpg 1536w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220812_115731990.jpg 2016w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>When I bought my machine, it wasn&#8217;t working and was sold as &#8220;unknown, non-working condition&#8221;. This model of espresso machine is super popular, and you can find a lot of info about how it works and how to repair every single piece of it. So, I won&#8217;t write everything again over here, but will make it easier for you and link all the resources I used. My main source of knowledge was a blog dedicated to modding and fixing the Gaggia Classic (mainly), which is absolutely great, detailed, and insightful, but available only in Polish. If you don&#8217;t speak this language, I suggest trying auto-translation, as the blog is totally worth it.</p>



<p>Using this set of links, you can basically repair anything you want in your Gaggia as well <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<ul class="wp-block-list">
<li><a href="https://gaggiaclassicmods.blogspot.com/" target="_blank" rel="noreferrer noopener">home page of the &#8220;Moja Gaggia, Moje Mody&#8221; blog</a></li>



<li><a href="https://gaggiaclassicmods.blogspot.com/2015/12/o-ringi-w-gaggia-czyli-jakie-i-gdzie_83.html" target="_blank" rel="noreferrer noopener">how, where and which seals to change in your machine</a></li>



<li><a href="https://gaggiaclassicmods.blogspot.com/2016/01/rozbiorka-i-czyszczenie-zaworu.html" target="_blank" rel="noreferrer noopener">fixing/cleaning the 3-way valve</a></li>



<li><a href="https://youtu.be/MWrp4_ozUT0" target="_blank" rel="noreferrer noopener">how is the Ulka vibartion pump built and how to fix it</a></li>



<li><a href="https://gaggiaclassicmods.blogspot.com/2018/02/ekspres-nie-dziaa-sprawdzamy-grzake-i.html" target="_blank" rel="noreferrer noopener">how to check of the heaters of your boiler are OK</a></li>



<li><a href="https://gaggiaclassicmods.blogspot.com/2017/12/gaggia-classic-wielka-baza-czesci-i.html" target="_blank" rel="noreferrer noopener">big list of spare parts</a></li>



<li><a href="https://gaggiaclassicmods.blogspot.com/2018/09/gaggia-schematy-diagramy-elektryczne-i.html" target="_blank" rel="noreferrer noopener">electric connections diagram</a></li>
</ul>
<p>The post <a href="https://tailored.cloud/hobby/repairing-the-gaggia-classic/">Repairing the Gaggia Classic</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tailored.cloud/hobby/repairing-the-gaggia-classic/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How a single boiler espresso machine works?</title>
		<link>https://tailored.cloud/hobby/how-a-single-boiler-espresso-machine-works/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=how-a-single-boiler-espresso-machine-works</link>
					<comments>https://tailored.cloud/hobby/how-a-single-boiler-espresso-machine-works/#respond</comments>
		
		<dc:creator><![CDATA[tc-admin]]></dc:creator>
		<pubDate>Sat, 06 May 2023 16:25:03 +0000</pubDate>
				<category><![CDATA[hobby]]></category>
		<category><![CDATA[espresso]]></category>
		<category><![CDATA[gaggia classic]]></category>
		<category><![CDATA[IoT]]></category>
		<category><![CDATA[open source]]></category>
		<guid isPermaLink="false">https://tailored.cloud/?p=472</guid>

					<description><![CDATA[<p>I think that in order to better understand the modding process of an espresso machine, you need to first understand <a class="more-link" href="https://tailored.cloud/hobby/how-a-single-boiler-espresso-machine-works/">Continue Reading</a></p>
<p>The post <a href="https://tailored.cloud/hobby/how-a-single-boiler-espresso-machine-works/">How a single boiler espresso machine works?</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><img decoding="async" class="size-medium wp-image-474 aligncenter" src="https://tailored.cloud/wp-content/uploads/2023/05/PXL_20230315_214521329-225x300.jpg" alt="Wires, wires everywhere ;)" width="225" height="300" srcset="https://tailored.cloud/wp-content/uploads/2023/05/PXL_20230315_214521329-225x300.jpg 225w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20230315_214521329-768x1024.jpg 768w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20230315_214521329-1152x1536.jpg 1152w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20230315_214521329.jpg 1512w" sizes="(max-width: 225px) 100vw, 225px" /></p>
<p>I think that in order to better understand the modding process of an espresso machine, you need to first understand how it&#8217;s working.<br />
What I bought for my project was a roughly 20 years old Gaggia Classic, in a quite rare black Coffee version. Still, in regard to how it&#8217;s built, it&#8217;s the same as Gaggia Classic v1. What was really interesting to me, is that this machine uses no electronics at all: everything is driven directly using the AC power and switches on the front of the machine &#8211; really nice and simple solution!</p>
<h2>How coffee is made</h2>
<p>When a machine is turned on, it turns on both heaters integrated with the water boiler. Depending on whether the machine is in &#8216;coffee brewing&#8217; mode or &#8216;steaming&#8217; mode, there&#8217;s one of two thermocouples included in the circuit with the heaters (one disconnects when the boiler gets to &#8216;coffee brewing&#8217; temperature, the other disconnects when it gets to a higher &#8216;steaming&#8217; temperature). By default, they are internally connected (on), but when they reach a certain temperature, they break (turn off internally) the circuit and boiler heaters stop working. When the water in the boiler cools down a bit, the thermocouple switches back on and heaters start again. That way, the machine executes its first function: heating water. By the way, this process is what is causing some problems in espresso brewing: the difference between off-and-back-on temperatures (called hysteresis) of the heating circuit can be as high as 10 °C, which will already impact the taste of espresso. The PID controller we want to add will keep our temperature within 1-2 °C of the target value.</p>
<p>Unless you flip the brewing switch, nothing else happens. When you do, the brewing process looks like this:</p>
<ul>
<li>The pump starts pumping the water from the water tank to the hose that goes to the Over-Pressure Valve (OPV). The pressure of this water is roughly 15 bar.</li>
<li>Next, the water goes through OPV. In general, you can have OPV in a form of a simple plastic wedge, that is pushed against the water current by a spring or a full regulated OPV. My advice: don&#8217;t even buy machines that have the simple one. Gaggia Classic v1 has a regulated OPV valve. This valve has 1 input and 2 outputs, so to speak. The water flows in and pushes internally on a spring-loaded rubber seal. If the pressure is low, the seal holds and all the water goes out through the first output, which is used to brew coffee. When the pressure of the water is high enough, the spring is compressed, the seal opens and some water gets out the 2nd overflow output of the valve. This output leads back to the water container, where there&#8217;s no resistance on the way, so there&#8217;s also no pressure buildup and any excess water can flow this way. That allows the OPV to limit the water pressure to a constant value &#8211; the one that is needed to open the overflow output. By regulating a nut within the OPV you can preload the spring as hard as needed and that way also regulate the pressure at which the overflow opens. Still, to calibrate the OPV precisely, you need to connect a pressure gauge somewhere. One option to do that is to block water output out of the coffee group using a blind sieve, then split the water hose and connect your gauge there. The other, simpler way, is to connect the pressure gauge to your normal portafilter, blocking water outflow using the gauge directly. So, here&#8217;s the 2nd step of the espresso process: setting the water pressure to a correct value, typically 9 bar.
<p><figure id="attachment_479" aria-describedby="caption-attachment-479" style="width: 300px" class="wp-caption aligncenter"><img decoding="async" class="wp-image-479 size-medium" src="https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220802_180257308-300x225.jpg" alt="Over Pressure Valve (OPV)" width="300" height="225" srcset="https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220802_180257308-300x225.jpg 300w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220802_180257308-1024x768.jpg 1024w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220802_180257308-768x576.jpg 768w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220802_180257308-1536x1152.jpg 1536w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220802_180257308.jpg 2016w" sizes="(max-width: 300px) 100vw, 300px" /><figcaption id="caption-attachment-479" class="wp-caption-text">Over Pressure Valve (OPV)</figcaption></figure></li>
<li>After the OPV, the water passes through a 3-way valve. In theory, this part is not really needed, but you still want to have it. The way this valve works is that it has 3 connections: from the OPV (so the water you want to brew your coffee with), one exits to the coffee group (where you want your water to go to make a coffee) and the second output to the drip tray. The 3-way valve works by enabling one or (actually xor <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> the other of paths water can flow. When the AC power is enabled, the valve allows water to flow between OPV and the coffee group, but blocks output to the drip tray. When it&#8217;s not energized by AC, it allows the water to flow between the coffee group and the drip tray. Why is it useful? Just after finishing the brew, there&#8217;s still quite high pressure in the coffee group and your portafilter. If you remove your portafilter from the group while there&#8217;s still some pressure, droplets and bits of coffee will start flying around your kitchen! To avoid that, as soon as you turn off the water pump, also the 3-way valve is turned off and allows the extra pressure to be released to the drip tray through the 2nd path.
<p><figure id="attachment_481" aria-describedby="caption-attachment-481" style="width: 300px" class="wp-caption alignnone"><img loading="lazy" decoding="async" class="size-medium wp-image-481" src="https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220802_180247128-300x225.jpg" alt="3 Way Valve" width="300" height="225" srcset="https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220802_180247128-300x225.jpg 300w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220802_180247128-1024x768.jpg 1024w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220802_180247128-768x576.jpg 768w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220802_180247128-1536x1152.jpg 1536w, https://tailored.cloud/wp-content/uploads/2023/05/PXL_20220802_180247128.jpg 2016w" sizes="(max-width: 300px) 100vw, 300px" /><figcaption id="caption-attachment-481" class="wp-caption-text">3 Way Valve</figcaption></figure></li>
<li>Now we&#8217;re almost done. We have water at the right temperature and pressure, we&#8217;re protected from a mess by the 3-way valve, we can just push out the hot water from the boiler into the portafilter &#8211; it is the fresh water coming from the water tank that is pushing out the hot water from the boiler onto your coffee. So, put your ground coffee into the portafilter and let the water flow!</li>
</ul>
<p>The post <a href="https://tailored.cloud/hobby/how-a-single-boiler-espresso-machine-works/">How a single boiler espresso machine works?</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tailored.cloud/hobby/how-a-single-boiler-espresso-machine-works/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Short espresso machine project update</title>
		<link>https://tailored.cloud/hobby/short-espresso-machine-project-update/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=short-espresso-machine-project-update</link>
					<comments>https://tailored.cloud/hobby/short-espresso-machine-project-update/#respond</comments>
		
		<dc:creator><![CDATA[tc-admin]]></dc:creator>
		<pubDate>Tue, 02 May 2023 11:39:25 +0000</pubDate>
				<category><![CDATA[hobby]]></category>
		<category><![CDATA[coffee]]></category>
		<category><![CDATA[espresso]]></category>
		<category><![CDATA[gaggia classic]]></category>
		<category><![CDATA[IoT]]></category>
		<category><![CDATA[open source]]></category>
		<guid isPermaLink="false">https://tailored.cloud/?p=465</guid>

					<description><![CDATA[<p>It has been a long time, but my espresso project is going forward, still in an always-alpha state. Stuff is <a class="more-link" href="https://tailored.cloud/hobby/short-espresso-machine-project-update/">Continue Reading</a></p>
<p>The post <a href="https://tailored.cloud/hobby/short-espresso-machine-project-update/">Short espresso machine project update</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>It has been a long time, but my espresso project is going forward, still in an always-alpha state. Stuff is hanging on the wires behind the machine, but it makes coffee, so I&#8217;m spending my time on other projects.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="671" src="https://tailored.cloud/wp-content/uploads/2023/05/hardware-alpha-1024x671.jpg" alt="Stuff hanging on wires..." class="wp-image-467" srcset="https://tailored.cloud/wp-content/uploads/2023/05/hardware-alpha-1024x671.jpg 1024w, https://tailored.cloud/wp-content/uploads/2023/05/hardware-alpha-300x197.jpg 300w, https://tailored.cloud/wp-content/uploads/2023/05/hardware-alpha-768x504.jpg 768w, https://tailored.cloud/wp-content/uploads/2023/05/hardware-alpha.jpg 1342w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Hardware &#8211; always alpha</figcaption></figure>



<p>Still, I want to drop at least a short update here.</p>



<p>Over the last few months, I tried the projects I mentioned <a href="https://tailored.cloud/hobby/smart-espresso-machine-starting-a-new-hobby-project/" target="_blank" rel="noreferrer noopener">in the previous post</a>. I also have a decision about when to use which:</p>



<p>&#8211; if you need wireless connectivity, MQTT integration and such &#8211; use the <a href="https://github.com/medlor/bleeding-edge-ranciliopid">bleeding edge</a></p>



<p>&#8211; if you want the most advanced features, yet no connectivity and a very opinionated main developer &#8211; go with <a href="https://gaggiuino.github.io/">gaggiuino</a>.</p>



<p>Still, no matter which you choose, you need an operational Gaggia machine first. I have an almost ready short entry about the best resources I found on the Internet when I was fixing mine.</p>
<p>The post <a href="https://tailored.cloud/hobby/short-espresso-machine-project-update/">Short espresso machine project update</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tailored.cloud/hobby/short-espresso-machine-project-update/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Smart Espresso Machine &#8211; starting a new hobby project</title>
		<link>https://tailored.cloud/hobby/smart-espresso-machine-starting-a-new-hobby-project/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=smart-espresso-machine-starting-a-new-hobby-project</link>
					<comments>https://tailored.cloud/hobby/smart-espresso-machine-starting-a-new-hobby-project/#respond</comments>
		
		<dc:creator><![CDATA[tc-admin]]></dc:creator>
		<pubDate>Fri, 09 Sep 2022 12:26:05 +0000</pubDate>
				<category><![CDATA[hobby]]></category>
		<category><![CDATA[coffee]]></category>
		<category><![CDATA[espresso]]></category>
		<category><![CDATA[gaggia classic]]></category>
		<category><![CDATA[IoT]]></category>
		<category><![CDATA[open source]]></category>
		<category><![CDATA[smart]]></category>
		<guid isPermaLink="false">http://localhost:8080/?p=452</guid>

					<description><![CDATA[<p>As you may have heard, an IT engineer is powered by coffee. While not always true, it definitely is in <a class="more-link" href="https://tailored.cloud/hobby/smart-espresso-machine-starting-a-new-hobby-project/">Continue Reading</a></p>
<p>The post <a href="https://tailored.cloud/hobby/smart-espresso-machine-starting-a-new-hobby-project/">Smart Espresso Machine &#8211; starting a new hobby project</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>As you may have heard, an IT engineer is powered by coffee. While not always true, it definitely is in my case. I love coffee, and I&#8217;m a big fan of Italian coffee style: mainly espresso.</p>



<p>For a few years, I was a moderately happy owner of an Ascaso Dream espresso machine and i2 mini grinder. They create a nice set and the coffee was… OK. Espresso was hit-and-miss, but mostly miss, and it always frustrated me that an espresso at a regular coffee bar, like Costa, is much better.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="768" src="https://localhost:8080/wp-content/uploads/2022/08/ascaso-dream-and-i2-mini-1024x768.jpg" alt="My Ascaso Dream, i-2 mini grinder and the tamper station" class="wp-image-457" srcset="https://tailored.cloud/wp-content/uploads/2022/08/ascaso-dream-and-i2-mini-1024x768.jpg 1024w, https://tailored.cloud/wp-content/uploads/2022/08/ascaso-dream-and-i2-mini-300x225.jpg 300w, https://tailored.cloud/wp-content/uploads/2022/08/ascaso-dream-and-i2-mini-768x576.jpg 768w, https://tailored.cloud/wp-content/uploads/2022/08/ascaso-dream-and-i2-mini-1536x1152.jpg 1536w, https://tailored.cloud/wp-content/uploads/2022/08/ascaso-dream-and-i2-mini-2048x1536.jpg 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption><em>My Ascaso Dream, i-2 mini grinder and the tamper station</em></figcaption></figure>



<p>If my espresso quality is subpar, then what is required to get a good espresso? I started to check again what is considered important when making espresso. The non-ultimate list is:</p>



<ul class="wp-block-list"><li>first and foremost, a good grinder that can get you the needed fine-grained coffee powder of uniform distribution</li><li>precise water temperature control (and good water quality in general)</li><li>water pressure control (should be 9 bar, at least as a start value)</li><li>even distribution, tamping and extraction of coffee puck in the basket (this can be a hard one)</li><li>repeatability and control over everything mentioned above.</li></ul>



<p>After a long break, I got back to a polish coffee forum, where I wanted to check if someone figured out how to make better coffee using hardware I have. I learned there that one folk has basically remade his Ascaso Arc (same hardware, different case than my Dream), added an AT Mega based MCU and turned it into a beast. That started me off. I started to read about how espresso machines are made and how they work, and I found a few open source projects on GitHub, where people create a firmware for home class espresso machines. That was it &#8211; at this point, I knew what my next big hobby project is.</p>



<p>This blog entry starts a series of note-to-future-self entries, where I want to document where I found stuff needed to fix and make the open source espresso machine based on Gaggia Classic.</p>



<p>The firmware projects I found are:</p>



<ul class="wp-block-list"><li><a href="https://github.com/Zer0-bit/gaggiuino" target="_blank" rel="noreferrer noopener">https://github.com/Zer0-bit/gaggiuino</a></li><li><a href="https://github.com/rancilio-pid/ranciliopid" target="_blank" rel="noreferrer noopener">https://github.com/rancilio-pid/ranciliopid</a> and its fork <a href="https://github.com/medlor/bleeding-edge-ranciliopid" target="_blank" rel="noreferrer noopener">https://github.com/medlor/bleeding-edge-ranciliopid</a></li></ul>



<p>The first project on the list is aimed at Gaggia Classic machine, while the second one supports a few machines, mainly Rancilio Silvia, but also Gaggia Classic. I read about the projects, compared them, checked on the features and decided to go with &#8216;ranciliopid&#8217; or its fork. So, I created a plan and arranged it in stages:</p>



<ul class="wp-block-list"><li>stage 0<ul><li>buy a used Gaggia Classic</li><li>learn how it&#8217;s built, repair it (if needed), clean it and make nice</li><li>make it work as the original design intended to</li><li>add precision baskets, precision shower filter and an NPF (naked portafilter &#8211; it doesn&#8217;t change the taste of coffee, but allows you to better see how your extraction works and looks awesome &#8211; I always wanted to have one <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> )</li></ul></li><li>stage 1 &#8211; add ESP32 MCU in the &#8220;full&#8221; scenario, with features including<ul><li>digital control through relays of all the main electric components: 3-way valve, pump and heaters, and the TSIC 306 temperature sensor, which allows for:<ul><li>very precise temperature control with PID controller (with 1 <strong>°</strong>C resolution)</li><li>timed shots: the machine stops after the set number of seconds</li><li>pre-infusion: when a brew starts, the machine first pumps water for a brief moment of time (like 2 s), then takes a short pause, then starts the actual extraction &#8211; this presoaking of coffee allows getting rid of the air trapped in between the coffee powder and should make your extraction more even</li><li>backflush: a cleaning mode, where brew group is first pressurized (with a blind sieve/basket attached), then expelled through the 3-way valve to the drip tray</li></ul></li><li>add an OLED screen</li><li>configure MQTT integration</li><li>insulate the boiler of the machine to eliminate the gigantic heat loss of the naked metal boiler and speed up the heating process on start</li><li>add a water level sensor: to tell you when you run out of water, so you&#8217;re not surprised mid-brew</li><li>figure out how to place all of that in the machine, add necessary case for the electronics</li></ul></li><li>stage 2 &#8211; add scales<ul><li>Good espresso needs to have a correct extraction ration and extraction time. Extraction ratio is the ratio between the weight of your final product &#8211; the espresso in your glass &#8211; to the weight of coffee beans used to make it. There&#8217;s no single correct value, but in general people agree that your extraction ratio should be somewhere between 2-3x (so from 10 g of freshly ground coffee powder you should get 20-30 g of espresso). As for time, the typical value is 25 s, but it also can vary between 20-30 s. So, to make your coffee right, you need to find (dial in) your grinding settings for a specific coffee to land within these ranges. The basic option is to set your brew time to a constant value, like 25 s, then change your grinding setting until you get the 2-3x extraction ratio. But to be able to tell the ratio, you have to put a scale under your espresso glass when you pull the shot. Now, the firmware that I want to use (and gaggiuino is working on it as well) has a killer feature: you can integrate a scale into your drip tray and connect it directly to the ESP32. That way, you can start making your coffee by weight: your machine will stop the extraction when you reach a certain weight of espresso and will tell you how much time that needed. This is my next stage after the basic functionality.</li></ul></li><li>stage 3 &#8211; features that need to be created and I would like to help contribute<ul><li>Pressure profiling: this is the hot topic in espresso world (as far as I&#8217;ve seen). The idea is that you add 2 hardware elements: a water pressure sensor to check the pressure of water flowing through your coffee puck and an AC light dimmer, which you can use to cut down the power of the water pump. By cutting the power of the pump, you make the pump lower the extraction pressure. Controlling the pump using data from the pressure sensor, you can create a pressure profile of water during your extraction. Ideas that I&#8217;ve seen so far are based on linear change, so for example your brew starts at 9 atm but then falls down to 6 atm at the end of brew. Still, everything is possible &#8211; you just have to create a code that can execute that.</li><li>Drip tray overflow warning. Since the scale I want to integrate into my machine is actually based under the drip tray, it should be possible to get the weight of the tray when it&#8217;s almost full of water, then display a warning for me that I should empty it. Sounds simple and useful.</li><li>Automatic extraction rate brewing. As I explained above, you can either brew your espresso by time (for a given number of seconds) or until it reaches a certain weight (like until we get 30 g of espresso). The problem that I see with that second method is that my initial coffee powder weight might vary. I&#8217;d like to try to integrate a digital scale connected to the espresso machine&#8217;s MCU, that weights how much coffee powder has my grinder produced. Having this information in the MCU, I can stop the extraction when the ratio reaches a specific configured extraction rate, like 2,5 x.</li></ul></li></ul>



<h2 class="wp-block-heading">Wrapping up</h2>



<p>This is going to be a long project for me, but I&#8217;m actually writing this after already (almost) completing my stage 1. In this blog series, I want to save a set of &#8220;notes for my future self&#8221;, so that I have a written reference when I need to service my own machine again.</p>



<p>In the next few entries, I&#8217;ll try to start by explaining how an espresso machine works and how you can repair it if you have to.</p>
<p>The post <a href="https://tailored.cloud/hobby/smart-espresso-machine-starting-a-new-hobby-project/">Smart Espresso Machine &#8211; starting a new hobby project</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tailored.cloud/hobby/smart-espresso-machine-starting-a-new-hobby-project/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cloud-native observability series: season finale available now!</title>
		<link>https://tailored.cloud/devops/cloud-native-observability-series-season-finale-available-now/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=cloud-native-observability-series-season-finale-available-now</link>
					<comments>https://tailored.cloud/devops/cloud-native-observability-series-season-finale-available-now/#respond</comments>
		
		<dc:creator><![CDATA[tc-admin]]></dc:creator>
		<pubDate>Wed, 03 Feb 2021 19:03:46 +0000</pubDate>
				<category><![CDATA[devops]]></category>
		<guid isPermaLink="false">http://localhost:8080/?p=447</guid>

					<description><![CDATA[<p>For a long time now I&#8217;m writing more on the company blog than here. If you&#8217;re interested in the observability <a class="more-link" href="https://tailored.cloud/devops/cloud-native-observability-series-season-finale-available-now/">Continue Reading</a></p>
<p>The post <a href="https://tailored.cloud/devops/cloud-native-observability-series-season-finale-available-now/">Cloud-native observability series: season finale available now!</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>For a long time now I&#8217;m writing more on the company blog than here. If you&#8217;re interested in the observability series I was writing on Giant Swarm&#8217;s blog, the final episode of the series is <a href="https://www.giantswarm.io/blog/part-7-the-observability-finale">available</a>. Try to check the whole thing <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> </p>
<p>The post <a href="https://tailored.cloud/devops/cloud-native-observability-series-season-finale-available-now/">Cloud-native observability series: season finale available now!</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tailored.cloud/devops/cloud-native-observability-series-season-finale-available-now/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Building applications for the cloud-native stack &#8211; continuation.</title>
		<link>https://tailored.cloud/dev/building-applications-for-the-cloud-native-stack-continuation/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=building-applications-for-the-cloud-native-stack-continuation</link>
					<comments>https://tailored.cloud/dev/building-applications-for-the-cloud-native-stack-continuation/#respond</comments>
		
		<dc:creator><![CDATA[tc-admin]]></dc:creator>
		<pubDate>Fri, 05 Jun 2020 15:41:38 +0000</pubDate>
				<category><![CDATA[dev]]></category>
		<category><![CDATA[devops]]></category>
		<category><![CDATA[kubernetes]]></category>
		<category><![CDATA[cloud]]></category>
		<category><![CDATA[cloud-native]]></category>
		<guid isPermaLink="false">http://localhost:8080/?p=444</guid>

					<description><![CDATA[<p>The next parts of my blog series I wrote for the company blog are available. Here&#8217;s part 2 and part <a class="more-link" href="https://tailored.cloud/dev/building-applications-for-the-cloud-native-stack-continuation/">Continue Reading</a></p>
<p>The post <a href="https://tailored.cloud/dev/building-applications-for-the-cloud-native-stack-continuation/">Building applications for the cloud-native stack &#8211; continuation.</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>The next parts of my blog series I wrote for the company blog are available. Here&#8217;s <a rel="noreferrer noopener" href="https://www.giantswarm.io/blog/a-few-dozen-lines-of-code-part-2-creating-the-application" target="_blank">part 2</a> and <a rel="noreferrer noopener" href="https://www.giantswarm.io/blog/part-3-deploying-the-application-with-helm" target="_blank">part 3</a>. I posted about starting the series and <a href="http://localhost:8080/kubernetes/schrodingers-come-back-to-blogging-using-the-cloud-native-stack-series/">part 1</a> before.</p>



<p>In part 2, I&#8217;m discussing how to build a simple micro services app that will be used as a &#8216;lab rat&#8217; for our experiments with the cloud-native stack later.</p>



<p>In part 3, I&#8217;m describing how you can deploy your application (just a binary) as a full fledged Kubernetes concept, up to the point where we can pack everything into a Helm chart.</p>



<p>Enjoy!</p>
<p>The post <a href="https://tailored.cloud/dev/building-applications-for-the-cloud-native-stack-continuation/">Building applications for the cloud-native stack &#8211; continuation.</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tailored.cloud/dev/building-applications-for-the-cloud-native-stack-continuation/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Schrödinger&#8217;s come back to blogging: using the cloud-native stack series.</title>
		<link>https://tailored.cloud/kubernetes/schrodingers-come-back-to-blogging-using-the-cloud-native-stack-series/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=schrodingers-come-back-to-blogging-using-the-cloud-native-stack-series</link>
					<comments>https://tailored.cloud/kubernetes/schrodingers-come-back-to-blogging-using-the-cloud-native-stack-series/#respond</comments>
		
		<dc:creator><![CDATA[tc-admin]]></dc:creator>
		<pubDate>Sat, 04 Apr 2020 12:13:32 +0000</pubDate>
				<category><![CDATA[dev]]></category>
		<category><![CDATA[devops]]></category>
		<category><![CDATA[kubernetes]]></category>
		<category><![CDATA[cloud]]></category>
		<category><![CDATA[cloud-native]]></category>
		<guid isPermaLink="false">http://localhost:8080/?p=439</guid>

					<description><![CDATA[<p>It is a year since a wrote a blog post here. A lot has changed over this year in my <a class="more-link" href="https://tailored.cloud/kubernetes/schrodingers-come-back-to-blogging-using-the-cloud-native-stack-series/">Continue Reading</a></p>
<p>The post <a href="https://tailored.cloud/kubernetes/schrodingers-come-back-to-blogging-using-the-cloud-native-stack-series/">Schrödinger&#8217;s come back to blogging: using the cloud-native stack series.</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="682" src="https://localhost:8080/wp-content/uploads/2020/04/code-820275_1280-1024x682.jpg" alt="" class="wp-image-440" srcset="https://tailored.cloud/wp-content/uploads/2020/04/code-820275_1280-1024x682.jpg 1024w, https://tailored.cloud/wp-content/uploads/2020/04/code-820275_1280-300x200.jpg 300w, https://tailored.cloud/wp-content/uploads/2020/04/code-820275_1280-768x512.jpg 768w, https://tailored.cloud/wp-content/uploads/2020/04/code-820275_1280.jpg 1280w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption>A Few Dozen Lines of Code&#8230; just don&#8217;t count YAML as code <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></figcaption></figure>



<p>It is a year since a <a href="http://localhost:8080/kubernetes/smartnat-dirt-cheap-kubernetes-ingress-controller-for-tcp-udp-services/">wrote a blog post here</a>. A lot has changed over this year in my professional life. After quitting my previous job, I was looking for new opportunities and eventually decided to join the cool team at <a href="https://www.giantswarm.io/">Giant Swarm</a> as a platform architect. I have to say this was a great choice! My focus is now on enabling our customers to run applications on top of Kubernetes clusters.</p>



<p>Still, I didn&#8217;t want to stop blogging. Fortunately, I can now connect everything as part of my new job! So, I&#8217;m switching my focus slightly to cloud-native applications running on Kubernetes than just Kubernetes. This is also the topic of a blog series called &#8220;A Few Dozen Lines of Code&#8221;, the first part of which was <a href="https://blog.giantswarm.io/a-few-dozen-lines-of-code/">just published</a> on our company blog!</p>



<p>This series is meant to show you how using an opinionated cloud-native stack of tools installed on your Kubernetes cluster can make your dev and operations team happier than ever <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Stay tuned for next parts, they should come shortly.</p>



<p>So, this entry is kind of &#8220;Schrödinger&#8217;s entry&#8221;. It is a new entry and it is not &#8211; as I&#8217;m just pointing you to <a href="https://blog.giantswarm.io/a-few-dozen-lines-of-code/">my article on the company blog</a>. Still, I will inform you if anything written by me is published there. Meanwhile, you can check other engineering articles published there, it&#8217;s good content.</p>
<p>The post <a href="https://tailored.cloud/kubernetes/schrodingers-come-back-to-blogging-using-the-cloud-native-stack-series/">Schrödinger&#8217;s come back to blogging: using the cloud-native stack series.</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tailored.cloud/kubernetes/schrodingers-come-back-to-blogging-using-the-cloud-native-stack-series/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SmartNat &#8211; dirt cheap Kubernetes ingress controller for TCP/UDP services</title>
		<link>https://tailored.cloud/kubernetes/smartnat-dirt-cheap-kubernetes-ingress-controller-for-tcp-udp-services/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=smartnat-dirt-cheap-kubernetes-ingress-controller-for-tcp-udp-services</link>
					<comments>https://tailored.cloud/kubernetes/smartnat-dirt-cheap-kubernetes-ingress-controller-for-tcp-udp-services/#comments</comments>
		
		<dc:creator><![CDATA[tc-admin]]></dc:creator>
		<pubDate>Wed, 03 Apr 2019 19:20:09 +0000</pubDate>
				<category><![CDATA[devops]]></category>
		<category><![CDATA[kubernetes]]></category>
		<category><![CDATA[controllers]]></category>
		<category><![CDATA[networking]]></category>
		<category><![CDATA[operators]]></category>
		<guid isPermaLink="false">http://localhost:8080/?p=420</guid>

					<description><![CDATA[<p>TL;DR SmartNat is a Kubernetes ingress controller for exposing a massive number of TCP/UDP services to the outside world using <a class="more-link" href="https://tailored.cloud/kubernetes/smartnat-dirt-cheap-kubernetes-ingress-controller-for-tcp-udp-services/">Continue Reading</a></p>
<p>The post <a href="https://tailored.cloud/kubernetes/smartnat-dirt-cheap-kubernetes-ingress-controller-for-tcp-udp-services/">SmartNat &#8211; dirt cheap Kubernetes ingress controller for TCP/UDP services</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">TL;DR</h2>



<ul class="wp-block-list"><li>SmartNat is a Kubernetes ingress controller for exposing a massive number of TCP/UDP services to the outside world using just 1 server</li><li>available on GitHub: <a href="https://github.com/DevFactory/smartnat">https://github.com/DevFactory/smartnat</a></li></ul>



<h2 class="wp-block-heading">SmartNat &#8211; Kubernetes ingress controller for TCP/UDP services</h2>



<p>Some time ago I wrote posts about <a href="http://localhost:8080/kubernetes/simple-custom-kubernetes-controller/">writing a very simple Kubernetes controller</a> and <a href="http://localhost:8080/kubernetes/write-a-kubernetes-controller-operator-sdk/">using operator framework</a> to create a more complete one. Well, at the same time, I was starting to work on one at the company I&#8217;m working for. Meanwhile, I convinced management at the company to release the project as an OpenSource &#8211; and here it is!</p>



<p>The project is called SmartNat. It&#8217;s a Kubernetes ingress controller for TCP/UDP services that allows you to drive external traffic to your Services. It&#8217;s kind of Service with NodePort, but on strong steroids. It runs on a separate instance (well, you can run however you like, but this makes the most sense) and interconnects external (usually public) network with the subnet used by your Kubernetes cluster. SmartNat allows you to use multiple network interfaces, each one having multiple IP addresses to forward traffic from an external network to your services on a port-by-port basis. That way using just a single server or instance you can easily expose hundreds or even thousands of Services. The important property is that all of this is done using L3/L4 tools only, so SmartNat helps where HTTP based Ingresses can&#8217;t. Additionally, SmartNat supports simple traffic filtering of traffic coming from external subnet and also HA mode.</p>



<p>If you&#8217;re interested, check the project on github: <a href="https://github.com/DevFactory/smartnat">https://github.com/DevFactory/smartnat</a>.</p>
<p>The post <a href="https://tailored.cloud/kubernetes/smartnat-dirt-cheap-kubernetes-ingress-controller-for-tcp-udp-services/">SmartNat &#8211; dirt cheap Kubernetes ingress controller for TCP/UDP services</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tailored.cloud/kubernetes/smartnat-dirt-cheap-kubernetes-ingress-controller-for-tcp-udp-services/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Write a Kubernetes controller (operator) with operator-sdk</title>
		<link>https://tailored.cloud/kubernetes/write-a-kubernetes-controller-operator-sdk/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=write-a-kubernetes-controller-operator-sdk</link>
					<comments>https://tailored.cloud/kubernetes/write-a-kubernetes-controller-operator-sdk/#respond</comments>
		
		<dc:creator><![CDATA[tc-admin]]></dc:creator>
		<pubDate>Sat, 15 Sep 2018 19:25:25 +0000</pubDate>
				<category><![CDATA[dev]]></category>
		<category><![CDATA[kubernetes]]></category>
		<category><![CDATA[controllers]]></category>
		<category><![CDATA[go]]></category>
		<category><![CDATA[longer entry]]></category>
		<category><![CDATA[operators]]></category>
		<guid isPermaLink="false">http://localhost:8080/?p=377</guid>

					<description><![CDATA[<p>TL;DR: unless you need a really low-level control or are writing a specialized controller, do use one of the helper <a class="more-link" href="https://tailored.cloud/kubernetes/write-a-kubernetes-controller-operator-sdk/">Continue Reading</a></p>
<p>The post <a href="https://tailored.cloud/kubernetes/write-a-kubernetes-controller-operator-sdk/">Write a Kubernetes controller (operator) with operator-sdk</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h2>TL;DR:</h2>
<ul>
<li>unless you need a really low-level control or are writing a specialized controller, do use one of the helper libraries like  the <a href="https://github.com/operator-framework/operator-sdk">operator-sdk</a> or <a href="https://github.com/kubernetes-sigs/kubebuilder">kubebuilder</a> to avoid writing a lot of boilerplate code,</li>
<li>handling events and objects in the controller is about synchronizing the state between API object and the system, not about reacting to events,</li>
<li>try to be stateless, as keeping state is your problem,</li>
<li>when you write a Kubernetes controller, be sure you know what your state is and how to make it durable,</li>
<li>remember your controller can be restarted any time,</li>
<li>use <a href="https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/">Custom Resource Definition</a> to create any API object you need,</li>
<li><a href="https://github.com/piontec/netperf-operator/blob/436c7c101ddf98a7a23a4402693eb9d4f027ec58/pkg/apis/app/v1alpha1/zz_generated.deepcopy.go#L22">DeepCopy()</a> API objects if you&#8217;re changing them,</li>
<li>if you&#8217;re creating new API objects, always set their <a href="https://github.com/piontec/netperf-operator/blob/436c7c101ddf98a7a23a4402693eb9d4f027ec58/pkg/netperf-operator/operator.go#L164">OwnerReference</a>.</li>
</ul>
<h2>Introduction</h2>
<p>Some time ago <a href="http://localhost:8080/kubernetes/simple-custom-kubernetes-controller/">I posted an entry</a> about how to write a Kubernetes controller. It was a very minimalistic example of a controller, which aimed only at putting the main pieces in place. Still, I wanted to explore the topic further and write something more realistic and useful. As you can learn in the previous blog entry, despite the simple idea of how a controller operates, writing one requires a lot of boilerplate code. Fortunately, there are already libraries/frameworks that are trying to remove this boilerplate stuff and get you on track much faster. I had a look at two of them: the <a href="https://github.com/operator-framework/operator-sdk">operator-sdk</a> and <a href="https://github.com/kubernetes-sigs/kubebuilder">kubebuilder</a>. As I found the operator-sdk first, I started with it. In this blog entry, I want to show you how to write a Kubernetes controller with operator-sdk.</p>
<p>My idea was pretty simple. I wanted to write an operator, which is useful, but still very simple, so it can serve as a tutorial material, showing how to build operators. This turned into a Netperf Operator, a kubernetes tool that allows you to run the good old network performance benchmarking tool called <a href="https://hewlettpackard.github.io/netperf/">netperf</a> (yes, click this URL and admire pure HTML at its best!). I wanted to be able to run netperf, a client-server application, between 2 kubernetes pods, preferably running on different cluster nodes. Such tests are pretty valuable as they are the only way you can check your real pod-to-pod network performance, including the impact of all the networking layers, like overlay networks and such. Normally, running netperf requires you to start a server at one place and a netperf client at the other end of the tested connection. I wanted to make this a one-step process, managed by an operator. And of course, learn and how you how to write a Kubernetes controller. You can check the resulting project on my github page: <a href="https://github.com/piontec/netperf-operator">netperf-operator</a>.</p>
<p>Oh, but why do I call it &#8220;operator&#8221;, not &#8220;controller&#8221;? These concepts are very close: people tend to name Kubernetes controllers specialized in running and configuring a single application  &#8220;an operator&#8221;. There are already many awesome operators. <a href="https://github.com/coreos/prometheus-operator">Prometheus-operator</a> is just one of them, but it really shows the power of an idea of running an application integrated with your kubernetes cluster and managed &#8220;the cluster way&#8221;. You can find other operators on the <a href="https://github.com/operator-framework/awesome-operators">awesome-operators</a> github page &#8211; be sure to check them.</p>
<h2>How does kubernetes controller/operator work and run?</h2>
<p>Well, this is mainly a reminder, but still a crucial one, so let&#8217;s say it once again. A very simplified controller&#8217;s life looks like this:</p>
<pre class="brush: plain; title: ; notranslate">
while true {
  receiveInfoAboutAPIObjects()
  synchronizeRealStateToMatchFetchedInfo()
}
</pre>
<p>There are a few consequences coming from the simple pseudocode above. The first one is that you need to receive notifications about the state and its changes of kubernetes objects that you&#8217;re interested in. Thankfully, the operator-sdk comes to the rescue here. It takes care of the synchronization with kubernetes API server and the notification loop. It also allows you to decide what type of API objects you want to observe.</p>
<p>Another important property is that the synchronization loop is not really event-driven, but state-driven. It means that your controller won&#8217;t receive events only when something changes, for example, a Pod dies or is created. You will get them then, but moreover, you will also get periodic refreshers: updates that show you the complete required state of the Kubernetes object, no matter if it changed or not since the last update. This is a very important property and you have to get it right. It took me some time to stick to it, so let me rephrase it. The notification loop doesn&#8217;t tell what change you need to perform in a system, but how the system should look like. It declares and describes the desired state, not the change that&#8217;s needed to produce it. Figuring out how to get to the desired state is a task for the controller.</p>
<p>Let me give you an example here. Let&#8217;s suppose we&#8217;re writing ReplicaSet controller. The ReplicaSet controller takes a pod configuration template and a desired number of pods to create. Its task is to ensure that the declared number of the pods are always running in the system. Let&#8217;s now assume we have created a ReplicaSet object with a Pod template and we set the pod count to 5 and also already 5 pods are running in the cluster. Now, if any of the pods die, the ReplicaSet controller gets a pod status update. But its reaction is not just &#8220;create a new pod&#8221;. Remember, a controller doesn&#8217;t react directly to the event, but checks for a difference between the configured and the real state. So, the controller checks that there are currently 4 pods running in the system, the required count is 5, so the solution is to start 1 new pod. But the same synchronization logic is also run periodically, even if there are no pod events in the system. Imagine now, that a whole node dies in the cluster and this node was running 2 out of 5 of our pods. Now, the cluster is not solving the problem of &#8220;how to notify the controller that 2 pods have died&#8221;. Instead, the controller will run the synchronization loop and check that there are now only 3 pods in the system and 5 are required, so the solution to reconcile the state is to start 2 new pods. So, we&#8217;re checking for the desired state and make it a reality, not simply react to events like.</p>
<p>One more important thing is that you should expect your controller/operator process to be terminated at any time. Most probably your controller will be run just as a pod in the cluster, and pods are mortal and should be easy to restart and recreate. This means, that if you need to keep a state, you basically have to keep it outside of your controller so that after a controller pod restart you can resume controller&#8217;s operations.</p>
<h2>How to write a Kubernetes controller with operator-sdk &#8211; bootstrapping</h2>
<p>After the general concepts presented above, we&#8217;re ready to write a Kubernetes controller and bootstrap a new operator project. This topic is nicely covered on the <a href="https://github.com/operator-framework/operator-sdk/blob/master/doc/user-guide.md">project&#8217;s github page</a>. Definitely, have a look there for a reference. I bootstrapped my project with</p>
<pre class="brush: bash; title: ; notranslate">
operator-sdk new netperf-operator --api-version=app.example.com/v1alpha1 --kind=Netperf
</pre>
<h2>Custom Resources and Custom Resource Definitions</h2>
<p>OK, but let&#8217;s pause for a second. You might be wondering how the controller pattern is really useful, except for controllers that are already in the Kubernetes, like Deployment or StatefulSet controller. After all, not everything is about pods and services, that are native objects in Kubernetes. But can you write a controller that handles some other objects? The great thing is that Kubernetes API and objects system is easily extensible. The same way the cluster gets information for standard controllers from your description of Pods, Deployments or Services, it can also handle any <a href="https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/">Custom Resource</a>. It just needs to be configured using a <a href="https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/">Custom Resource Definition</a>. Using this mechanism, you can introduce any object you need into your cluster and configure it using YAML files and &#8220;kubectl&#8221; or API calls. You can find the CRD for netperf-operator <a href="https://github.com/piontec/netperf-operator/blob/master/deploy/crd.yaml">here</a>. <a href="https://github.com/piontec/netperf-operator/blob/master/deploy/crd.yaml">The whole declaration</a> is really in these few lines:</p>
<pre class="brush: yaml; title: ; notranslate">
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: netperfs.app.example.com
spec:
  group: app.example.com
  names:
    kind: Netperf
    listKind: NetperfList
    plural: netperfs
    singular: netperf
  scope: Namespaced
  version: v1alpha1
</pre>
<p>It just defines the name of your Custom Resource (singular and plural), API object kind (Netperf) and that your CRD is scoped to a single namespace. And that&#8217;s it, you&#8217;re ready to create your own new &#8220;Netperf&#8221; objects. As you can see, we&#8217;re not giving here any data schema that we expect from the Custom Resource object. We handle that in the controller code; for CRD definition the object schema is irrelevant. Still, there&#8217;s a new feature in kubernetes 1.11 that allows for validating a Custom Resource by embedding <a href="https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#validation">Custom Resource validation schema</a> in CRD definition, but it&#8217;s a new thing and just beta in 1.11.</p>
<p>So, the next step is to define your Custom Resource schema in code. Basic stuff is already generated by the &#8220;operator-sdk new&#8221; command used to bootstrap the project (<a href="https://github.com/piontec/netperf-operator/tree/master/pkg/apis/app/v1alpha1">check this directory</a>). The important part is that we have to add the Spec and Status parts of the Custom Resource (CR) object. Spec is the specification, so basically, an input describing the object. Status shows, well, the status of the object. Here&#8217;s how the <a href="https://github.com/piontec/netperf-operator/blob/master/pkg/apis/app/v1alpha1/types.go">full definition looks like</a>, but the most important part is:</p>
<pre class="brush: golang; title: ; notranslate">
type Netperf struct {
	metav1.TypeMeta   `json:&quot;,inline&quot;`
	metav1.ObjectMeta `json:&quot;metadata&quot;`
	Spec              NetperfSpec   `json:&quot;spec&quot;`
	Status            NetperfStatus `json:&quot;status,omitempty&quot;`
}

type NetperfSpec struct {
	ServerNode string `json:&quot;serverNode&quot;`
	ClientNode string `json:&quot;clientNode&quot;`
}

type NetperfStatus struct {
	Status          string  `json:&quot;status&quot;`
	ServerPod       string  `json:&quot;serverPod&quot;`
	ClientPod       string  `json:&quot;clientPod&quot;`
	SpeedBitsPerSec float64 `json:&quot;speedBitsPerSec&quot;`
}
</pre>
<p>Lines 1-6 were already generated. We&#8217;re just providing the NetperfSpec and NetperfStatus definition. In Spec, I&#8217;m expecting just 2 input variables: names of kubernetes nodes where the controller should run the netperf server and client pods. With Spec like that, we can define a netperf test between any 2 cluster nodes we want. The Status is responsible for showing what are the names of ServerPod and ClientPod, the overall Status of the current CR (a single netperf test) and the final test result in bps.</p>
<p>When your definition is done, you have to run the command:</p>
<pre class="brush: bash; title: ; notranslate">
operator-sdk generate k8s
</pre>
<p>This will generate some helper functions for your CR, like deep copying the object instances. Now your definition of the Custom Resource is done and you can already use it!</p>
<h2>The controller loop &#8211; checking what needs to be done</h2>
<p>Do you remember how much code it took to <a href="http://localhost:8080/kubernetes/simple-custom-kubernetes-controller/">start a control loop without any controller library</a>? You might be surprised when you check the <a href="https://github.com/piontec/netperf-operator/blob/master/cmd/netperf-operator/main.go">main.go</a> file. Basically, the bootstrap code is this:</p>
<pre class="brush: golang; title: ; notranslate">
resource := &quot;app.example.com/v1alpha1&quot;
kind := &quot;Netperf&quot;
namespace, err := k8sutil.GetWatchNamespace()
if err != nil {
	logrus.Fatalf(&quot;Failed to get watch namespace: %v&quot;, err)
}
resyncPeriod := 5
sdk.Watch(resource, kind, namespace, resyncPeriod)
sdk.Watch(&quot;v1&quot;, &quot;Pod&quot;, namespace, resyncPeriod)
sdk.Handle(stub.NewHandler(operator.NewNetperf(realkube.NewRealProvider())))
sdk.Run(context.TODO())
</pre>
<p>We&#8217;re declaring API resource type and kind, how frequently we want to get object status update from the API server and then in lines 8 and 9 we just start to watch changes on our &#8220;Netperf&#8221; and &#8220;Pod&#8221; objects. Why &#8220;Pod&#8221;? Because our Netperf Operator creates new pods with netperf client and server and it needs to react to changes and control their lifetime. After that, we register the Handler (line 10, the stub was generated during the bootstrap step as well) and then you Run the whole thing in line 11. The generated handler stub is just a generic handler of any type of objects. These generic objects are switched into domain-specific object types as soon as possible and handled accordingly with (<a href="https://github.com/piontec/netperf-operator/blob/master/pkg/stub/handler.go">full source</a>):</p>
<pre class="brush: golang; title: ; notranslate">
func (h *Handler) Handle(ctx context.Context, event sdk.Event) error {
	switch event.Object.(type) {
	case *v1alpha1.Netperf:
		netperf := event.Object.(*v1alpha1.Netperf)
		return h.operator.HandleNetperf(netperf, event.Deleted)
	case *v1.Pod:
		pod := event.Object.(*v1.Pod)
		return h.operator.HandlePod(pod, event.Deleted)
	default:
		logrus.Warnf(&quot;unknown event received: %s&quot;, event)
	}
	return nil
}
</pre>
<p>OK, now we have everything needed to write a custom Kubernetes controller in place: Custom Resource Definition (CRD), Netperf Custom Resource (CR) and the control loop, where we can easily react to the incoming information about the system state we have to create. Now the &#8220;only&#8221; thing left is to add our business logic that does that.</p>
<h2>Business logic, a.k.a. how to run Netperf on Kubernetes using API calls</h2>
<p>I won&#8217;t describe the whole code line by line here, you can check the <a href="https://github.com/piontec/netperf-operator/blob/master/pkg/netperf-operator/operator.go">source here</a>. But I want to give an overview that will hopefully make the code easier to understand.</p>
<h3>Handling state</h3>
<p>The main concept is that we need to react to events in a different way, depending on what we have already completed and the state Netperf object is in. It&#8217;s basically a <a href="https://en.wikipedia.org/wiki/Finite-state_machine">finite state machine</a>: our action depends on the state we&#8217;re in and the event we receive. We assume a single Netperf object can be in one of the following states:</p>
<ul>
<li>NetperfPhaseInitial: resource was created, but no actions were taken yet;</li>
<li>NetperfPhaseServer: server pod is being created, but the client is not yet started;</li>
<li>NetperfPhaseTest: the client pod is created as well and the test should be in progress;</li>
<li>NetperfPhaseDone: the client pod has stopped, we collect the result and stop both server and client pods; the test is complete;</li>
<li>NetperfPhaseError: something bad happened and the controller was not able to complete the test; we can&#8217;t proceed and finish with an error.</li>
</ul>
<p>Check the following image for visualization of states (circles), events (black font) and actions (violet font) we have to handle.</p>
<p><figure id="attachment_393" aria-describedby="caption-attachment-393" style="width: 628px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" class="wp-image-393 size-large" title="How to write a Kubernetes controller - state diagram of Netperf object" src="https://localhost:8080/wp-content/uploads/2018/09/netperf-operator-state-diagram-1024x605.png" alt="How to write a Kubernetes controller - state diagram of Netperf object" width="628" height="371" srcset="https://tailored.cloud/wp-content/uploads/2018/09/netperf-operator-state-diagram-1024x605.png 1024w, https://tailored.cloud/wp-content/uploads/2018/09/netperf-operator-state-diagram-300x177.png 300w, https://tailored.cloud/wp-content/uploads/2018/09/netperf-operator-state-diagram-768x454.png 768w, https://tailored.cloud/wp-content/uploads/2018/09/netperf-operator-state-diagram.png 1650w" sizes="(max-width: 628px) 100vw, 628px" /><figcaption id="caption-attachment-393" class="wp-caption-text">The state diagram of a Netperf object</figcaption></figure></p>
<p>As you can see, to make everything work, we have to keep state &#8211; our current action depends on what previously already was applied to a Netperf object. That&#8217;s a major issue. We can&#8217;t just keep the state as the object&#8217;s state in the RAM memory, as we have to deal with failures. No matter if we want to run our controller on Kubernetes itself, as a Pod, or outside the cluster, as a standalone process &#8211; we have to handle restarts. So, the solution is to offload the problem of keeping state to some external durable storage. If our state is big, we could use some external key-value storage, like Redis or Etcd. Fortunately, our whole state is just a single variable with the state&#8217;s name. In this case, we can just keep the state within the Netperf object itself &#8211; the kubernetes API server will be our durable storage. And in the same go, we provide a better feedback to our users, who can check what&#8217;s the state of a Netperf test. When you write a Kubernetes controller, make sure you know what your state is and how to make it durable.</p>
<p>Still, the bottom line is: your process can terminate at any moment. Write a kubernetes controller assuming it can be terminated and restarted at any time.</p>
<p>Oh, one more thing: in your code, you should never change the object that you received in the control loop. Always make a copy of the object first &#8211; that&#8217;s why you have this generated <a href="https://github.com/piontec/netperf-operator/blob/436c7c101ddf98a7a23a4402693eb9d4f027ec58/pkg/apis/app/v1alpha1/zz_generated.deepcopy.go#L22">DeepCopy()</a> function. Then, save the copy with API server call.</p>
<h3>Control flow overview</h3>
<p>Let&#8217;s briefly go over what Netperf operator does for events in particular states:</p>
<ul>
<li>We start with <a href="https://github.com/piontec/netperf-operator/blob/436c7c101ddf98a7a23a4402693eb9d4f027ec58/pkg/netperf-operator/operator.go#L41">HandleNetperf()</a> method, which just checks if the event is about a Netperf object being deleted or created/updated (since we synchronize the state to match the API object, there&#8217;s actually no need to tell a create from an update event),
<ul>
<li>If it was a create/update, we go to <a href="https://github.com/piontec/netperf-operator/blob/436c7c101ddf98a7a23a4402693eb9d4f027ec58/pkg/netperf-operator/operator.go#L65:19">handleNetperfUpdateEvent()</a>, where we make sure that the Server pod is started and existing. Remember, we can receive this event multiple times, not just once or only when a new Netperf object is created! So, we check for the current Netperf object state. If it is &#8220;Initial&#8221; or &#8220;Server&#8221;, we run <a href="https://github.com/piontec/netperf-operator/blob/436c7c101ddf98a7a23a4402693eb9d4f027ec58/pkg/netperf-operator/operator.go#L78">startServerPod()</a>, which makes sure the pod either already exists or is created (but not blindly creates the pod). In other states, we ignore the update request, as to get to these states the server pod must have been already created.</li>
<li>If the delete flag was set,  we call <a href="https://github.com/piontec/netperf-operator/blob/436c7c101ddf98a7a23a4402693eb9d4f027ec58/pkg/netperf-operator/operator.go#L60">deleteNetperfPods()</a>, which basically does&#8230; nothing! Our netperf pods will be automatically deleted by the API server when the Netperf object that created them is deleted. This is possible because we correctly set the <a href="https://github.com/piontec/netperf-operator/blob/436c7c101ddf98a7a23a4402693eb9d4f027ec58/pkg/netperf-operator/operator.go#L164">OwnerReference</a> for our pods.</li>
</ul>
</li>
<li>The second entry point into our business logic controller is with <a href="https://github.com/piontec/netperf-operator/blob/436c7c101ddf98a7a23a4402693eb9d4f027ec58/pkg/netperf-operator/operator.go#L49">HandlePod()</a> method. Keep in mind that with this approach that method is called for every pod in the same namespace. So, we start with checking if the owner of the pod is an existing Netperf object. If not, our controller has nothing to care for it. If it is a related pod event, in <a href="https://github.com/piontec/netperf-operator/blob/436c7c101ddf98a7a23a4402693eb9d4f027ec58/pkg/netperf-operator/operator.go#L212:19">handlePodUpdateEvent()</a> we check if it is about a Server or a Client pod. Then, we call the event handler function for the specific kind of pod.
<ul>
<li>in <a href="https://github.com/piontec/netperf-operator/blob/436c7c101ddf98a7a23a4402693eb9d4f027ec58/pkg/netperf-operator/operator.go#L329:19">handleServerPodEvent()</a> we&#8217;re checking if the Server pod is already up and running. If not, we&#8217;re waiting. If it&#8217;s ready, we check if the client pod is already created for this Netperf object. Again, we&#8217;re not blindly creating a client pod, as it might have been already created. Instead, we check the current state and create the Client pod only if it doesn&#8217;t yet exist.</li>
<li>in <a href="https://github.com/piontec/netperf-operator/blob/436c7c101ddf98a7a23a4402693eb9d4f027ec58/pkg/netperf-operator/operator.go#L244">handleClientPodEvent()</a> we&#8217;re checking for the Client pod status. If it has already completed, we can get the output log from it, parse it and get the netperf speed result value. We&#8217;re also cleaning up both the Client and Server pod. Finally, we do the one last update of the Nerperf object, to include the test result in the Status part of the object definition.</li>
</ul>
</li>
</ul>
<h2>Building and running the project</h2>
<p>When you wrote a Kubernetes controller you naturally want to build and run it. Project building is described with a fairly complex workflow on the operator-sdk github page. This workflow includes building a docker image, then pushing it to the registry and after that deploying to a test cluster. This is nice when you want to test the full deployment cycle, but it&#8217;s terrible for development, as it takes a long time to build and deploy. On the github page of <a href="https://github.com/piontec/netperf-operator">Netperf Operator</a>, I described another approach for the development cycle, where you can just build and debug a local go binary, without even touching docker and deploying to cluster. Be sure to <a href="https://github.com/piontec/netperf-operator#-developers-guide">check it out</a>. You can also find information about how to run the Netperf Operator there.</p>
<h2>How to write a Kubernetes controller &#8211; a short summary</h2>
<p>OK, this entry is a lengthy one, not mentioning the linked code. Still, I really think that using a library like operator-sdk makes the whole thing much easier and faster. And creating a custom controller using a Custom Resource Definition really opens up plenty of possibilities for your needs. After all, Kubernetes is &#8220;just a new operating system&#8221; &#8211; at some point you have to write your own application&#8230; or rather write a Kubernets controller!</p>
<p>&nbsp;</p>
<p>The post <a href="https://tailored.cloud/kubernetes/write-a-kubernetes-controller-operator-sdk/">Write a Kubernetes controller (operator) with operator-sdk</a> appeared first on <a href="https://tailored.cloud">Tailored Cloud</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://tailored.cloud/kubernetes/write-a-kubernetes-controller-operator-sdk/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
