<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Amrou Bellalouna</title><link>https://shtlrs.com/</link><atom:link href="https://shtlrs.com/tags/encoding/" rel="self" type="application/rss+xml"/><item><title>How one single byte lead to silent Celery task corruption</title><link>https://shtlrs.com/posts/one_byte_that_led_to_task_corruption/</link><pubDate>Fri, 04 Sep 2026 15:25:56 +0100</pubDate><guid>https://shtlrs.com/posts/one_byte_that_led_to_task_corruption/</guid><content:encoded>&lt;p&gt;During our &lt;a href="https://shtlrs.com/posts/rabbitmq-celery-upgrade/"&gt;RabbitMQ v4.x migration preparation&lt;/a&gt;, we started seeing exceptions
we couldn&amp;rsquo;t make sense of — the kind that knock your services over with no obvious reason why.&lt;/p&gt;
&lt;p&gt;One of the steps I mentioned in our &lt;a href="https://shtlrs.com/posts/rabbitmq-celery-upgrade/"&gt;RabbitMQ v4.x migration&lt;/a&gt; was to
&lt;a href="https://shtlrs.com/posts/rabbitmq-celery-upgrade/#phase-3-message-transfer-with-eta-transformation"&gt;transfer messages between queues in different vhosts&lt;/a&gt;, and
that meant reading from &lt;code&gt;queue1&lt;/code&gt; in &lt;code&gt;vhost1&lt;/code&gt; and copying those messages to &lt;code&gt;queue2&lt;/code&gt; in &lt;code&gt;vhost2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We rolled this out environment by environment, and it went smoothly for the first few — until one day Sentry lit up with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;TypeError: unsupported operand type(s) for +: &amp;#39;float&amp;#39; and &amp;#39;str&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Digging into the stack trace, the crash was happening deep inside &lt;code&gt;Billiard&lt;/code&gt;, one of Celery&amp;rsquo;s core libraries, at &lt;a href="https://github.com/celery/billiard/blob/bd2f803b78f9f2a7fce62ed6d846333270f29e07/billiard/pool.py#L731"&gt;this line&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; monotonic() &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; start &lt;span style="color:#f92672"&gt;+&lt;/span&gt; timeout:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;start&lt;/code&gt; was a perfectly valid float. &lt;code&gt;timeout&lt;/code&gt;, on the other hand, was &lt;code&gt;,&lt;/code&gt;. Just a comma, when it was supposed to be a float/int as well.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Sentry Snippet&lt;/summary&gt;
&lt;p&gt;&lt;img src="sentry_snippet.png" alt="App"&gt;&lt;/p&gt;
&lt;/details&gt;
&lt;p&gt;Where did a comma come from? The answer is buried in how RabbitMQ encodes values on the wire — and specifically in a disagreement between two Python libraries about what a single byte means.&lt;/p&gt;
&lt;p&gt;Strap in.&lt;/p&gt;
&lt;h2 id="amqp-frames"&gt;AMQP frames&lt;/h2&gt;
&lt;p&gt;RabbitMQ speaks AMQP — it&amp;rsquo;s the protocol that handles everything from publishing messages to consuming them.&lt;/p&gt;
&lt;p&gt;AMQP runs over TCP but doesn&amp;rsquo;t just pour raw bytes down the pipe. It wraps everything in &lt;a href="https://www.brianstorti.com/speaking-rabbit-amqps-frame-structure/"&gt;frames&lt;/a&gt; — structured envelopes that each carry a type, a channel number, and a payload. The type tells the receiver what kind of data it&amp;rsquo;s looking at.&lt;/p&gt;
&lt;p&gt;If you want the full picture, the &lt;a href="https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf"&gt;official protocol spec&lt;/a&gt; has it all. Brian&amp;rsquo;s article on &lt;a href="https://www.brianstorti.com/speaking-rabbit-amqps-frame-structure/"&gt;AMQP&amp;rsquo;s frame structure&lt;/a&gt; is a much quicker read if you&amp;rsquo;d rather not wade through a technical PDF.&lt;/p&gt;
&lt;h2 id="the-headers-property"&gt;The &lt;code&gt;headers&lt;/code&gt; property&lt;/h2&gt;
&lt;p&gt;When you publish a message, one of the frames contains what AMQP calls &amp;ldquo;properties&amp;rdquo; — metadata about the message like &lt;code&gt;content-type&lt;/code&gt;, &lt;code&gt;content-encoding&lt;/code&gt;, and so on. Think of it as HTTP headers but for your queue.&lt;/p&gt;
&lt;p&gt;One of those properties is &lt;code&gt;headers&lt;/code&gt;, which lets you attach arbitrary key-value pairs to a message. In AMQP terms, that structure is called a &lt;strong&gt;Field Table&lt;/strong&gt; — which is really just a binary encoding of a dictionary.&lt;/p&gt;
&lt;h2 id="field-table-encoding"&gt;Field table encoding&lt;/h2&gt;
&lt;p&gt;This is the part that actually matters, so it&amp;rsquo;s worth slowing down here.&lt;/p&gt;
&lt;p&gt;Each entry in a field table is packed as four consecutive pieces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Key length&lt;/strong&gt; — 1 byte telling you how many bytes the key name occupies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key name&lt;/strong&gt; — N bytes (where N came from the previous byte)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type tag&lt;/strong&gt; — 1 byte identifying what kind of value follows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Value&lt;/strong&gt; — M bytes, where M is determined entirely by the type tag&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That type tag is the crux of everything. AMQP defines a fixed set of value types — short int, long string, boolean, decimal, etc. — and each one has a single-byte identifier. The decoder reads the tag first, then knows exactly how many bytes to consume for the value.&lt;/p&gt;
&lt;p&gt;Section 4.2.1 of the &lt;a href="https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf"&gt;spec&lt;/a&gt; lists all of them.&lt;/p&gt;
&lt;p&gt;If you want more details of how the decoding happens, you can dive into the following section:&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Field table entry Example&lt;/summary&gt;
The best way to make sense out of this is via an example.
&lt;p&gt;Let&amp;rsquo;s suppose we want to send &lt;code&gt;{&amp;quot;amount&amp;quot;: 300}&lt;/code&gt; as the &lt;code&gt;headers&lt;/code&gt; property.&lt;/p&gt;
&lt;p&gt;In AMQP, the dictionary itself is called a field-table, and each key-value pair inside it is a field-value pair.&lt;/p&gt;
&lt;p&gt;So for the &lt;code&gt;amount=300&lt;/code&gt; field-value pair, the byte sequence in the frame is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;06 61 6D 6F 75 6E 74 55 01 2C
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Let&amp;rsquo;s go over how this gets interpreted, step-by-step:&lt;/p&gt;
&lt;p&gt;First, it reads &lt;code&gt;06&lt;/code&gt;, which tells it that the key of this entry has a length of &lt;code&gt;6&lt;/code&gt; bytes.&lt;/p&gt;
&lt;p&gt;This leads it to reading 6 bytes to get the key name, those 6 bytes being: &lt;code&gt;61 6D 6F 75 6E 74&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;These are the hexadecimal ASCII characters that decode to &lt;code&gt;amount&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, it reads the &lt;code&gt;type tag&lt;/code&gt; to determine the value that comes with the &lt;code&gt;amount&lt;/code&gt; key.&lt;/p&gt;
&lt;p&gt;Type tags are always 1 byte. Here, that byte is &lt;code&gt;55&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;55&lt;/code&gt; in hexadecimal corresponds to the ASCII character &lt;code&gt;U&lt;/code&gt;, which means the value is a &lt;code&gt;Short Int&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Short Ints are always 2 bytes, so AMQP reads the next 2: &lt;code&gt;01 2C&lt;/code&gt;, which evaluates to 300.&lt;/p&gt;
&lt;p&gt;Why a Short Int and not something smaller? A &lt;code&gt;Short Short Int&lt;/code&gt; (signed, 1 byte) has a max value of 127. 300 exceeds that, so the next type up is a &lt;code&gt;Short Int&lt;/code&gt;, which uses 2 bytes.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an animation of how the decoding happens:&lt;/p&gt;
&lt;p&gt;&lt;img src="amqp_bytes.gif" alt="Decoding"&gt;&lt;/p&gt;
&lt;/details&gt;
&lt;h2 id="the-discrepancy-in-codec"&gt;The discrepancy in codec&lt;/h2&gt;
&lt;p&gt;To transfer the messages, we used &lt;a href="https://docs.aio-pika.com/"&gt;&lt;code&gt;aio-pika&lt;/code&gt;&lt;/a&gt; — read from the source queue, tweak the headers, republish to the destination.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;aio-pika&lt;/code&gt; delegates wire encoding to &lt;a href="https://github.com/gmr/pamqp"&gt;pamqp&lt;/a&gt;, and pamqp uses &lt;code&gt;s&lt;/code&gt; as the type tag for 16-bit signed integers.&lt;/p&gt;
&lt;p&gt;Celery, on the other hand, uses Kombu at the transport layer, which uses &lt;a href="https://github.com/celery/py-amqp"&gt;py-amqp&lt;/a&gt; for encoding and decoding.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the problem: pamqp and py-amqp disagree on what the &lt;code&gt;s&lt;/code&gt; type tag means. That single-byte disagreement is where everything falls apart.&lt;/p&gt;
&lt;p&gt;Suppose we want to send the following headers over the wire &lt;code&gt;{&amp;quot;some-key&amp;quot;: 300}&lt;/code&gt;, so by the time we&amp;rsquo;d want to encode the &lt;code&gt;300&lt;/code&gt; value,
the following happens:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pamqp&lt;/code&gt; sends the following &lt;code&gt;73 01 2C&lt;/code&gt; byte sequence over the wire:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The type tag byte is &lt;code&gt;73&lt;/code&gt; (hex), which is the ASCII character &lt;code&gt;s&lt;/code&gt; — meaning signed 16-bit integer in pamqp&amp;rsquo;s convention&lt;/li&gt;
&lt;li&gt;Since it&amp;rsquo;s a 16-bit integer, &lt;code&gt;300&lt;/code&gt; is encoded over the next 2 bytes: &lt;code&gt;01 2C&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;py-amqp&lt;/code&gt; receives the same &lt;code&gt;73 01 2C&lt;/code&gt; byte sequence:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The type tag byte is &lt;code&gt;73&lt;/code&gt;, ASCII character &lt;code&gt;s&lt;/code&gt; — but in py-amqp&amp;rsquo;s convention, &lt;code&gt;s&lt;/code&gt; means short string&lt;/li&gt;
&lt;li&gt;For a short string, the next byte is the string length: &lt;code&gt;01&lt;/code&gt; = 1 character&lt;/li&gt;
&lt;li&gt;It then reads 1 byte for the content: &lt;code&gt;2C&lt;/code&gt;, which is the ASCII character &lt;code&gt;,&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s exactly what happened to us. The headers on our messages included a &lt;code&gt;timelimit&lt;/code&gt; key with the value &lt;code&gt;[None, 300]&lt;/code&gt; — &lt;code&gt;None&lt;/code&gt; for the soft timeout, &lt;code&gt;300&lt;/code&gt; for the hard timeout in seconds.&lt;/p&gt;
&lt;p&gt;pamqp encoded &lt;code&gt;300&lt;/code&gt; with the &lt;code&gt;s&lt;/code&gt; tag. When py-amqp decoded it, it read &lt;code&gt;s&lt;/code&gt; as &amp;ldquo;short string&amp;rdquo;, consumed &lt;code&gt;\x01&lt;/code&gt; as the length (1 character), then read &lt;code&gt;\x2c&lt;/code&gt; as the content — which is ASCII for &lt;code&gt;,&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;timeout&lt;/code&gt; became &lt;code&gt;,&lt;/code&gt;, and Billiard blew up trying to do &lt;code&gt;float + str&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="why-does-this-discrepancy-exist"&gt;Why does this discrepancy exist?&lt;/h2&gt;
&lt;p&gt;The obvious question is: who&amp;rsquo;s doing it wrong?&lt;/p&gt;
&lt;p&gt;The answer is frustratingly: it depends on which spec you follow, and the specs contradict each other.&lt;/p&gt;
&lt;p&gt;AMQP 0-9-1 introduced a set of field table type tags, but RabbitMQ (and Qpid before it) had already shipped their own extensions using the same byte values with different meanings. The two conventions conflict.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://www.rabbitmq.com/amqp-0-9-1-errata.html"&gt;RabbitMQ errata page&lt;/a&gt; documents this conflict explicitly. Here&amp;rsquo;s the relevant part of the type tag table:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;0-9-1 spec&lt;/th&gt;
&lt;th&gt;Qpid / RabbitMQ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;short string&lt;/td&gt;
&lt;td&gt;signed 16-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;U&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;signed 16-bit&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;long string&lt;/td&gt;
&lt;td&gt;long string&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Worth noting: the original 0-9 spec didn&amp;rsquo;t define &lt;code&gt;s&lt;/code&gt; or &lt;code&gt;U&lt;/code&gt; at all — it only had &lt;code&gt;S&lt;/code&gt;, &lt;code&gt;I&lt;/code&gt;, &lt;code&gt;D&lt;/code&gt;, &lt;code&gt;T&lt;/code&gt;, &lt;code&gt;F&lt;/code&gt;, &lt;code&gt;V&lt;/code&gt;. Both tags were introduced in 0-9-1, but Qpid and RabbitMQ were already shipping their own extensions that reused the same byte values differently.&lt;/p&gt;
&lt;p&gt;The errata puts it plainly:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In Qpid and Rabbit, &lt;code&gt;s&lt;/code&gt; means a signed 16-bit integer; in 0-9-1, it means a short string.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And then:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;RabbitMQ continues to use the tags in the third column.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So RabbitMQ&amp;rsquo;s broker uses &lt;code&gt;s&lt;/code&gt; for signed 16-bit. &lt;strong&gt;pamqp deliberately follows this&lt;/strong&gt; — its v1.5.0 &lt;a href="https://gmr.github.io/pamqp/changelog/#150-2014-11-05"&gt;changelog&lt;/a&gt; explicitly says it aligned field table type indicators to the RabbitMQ protocol errata. py-amqp went the other way and follows the 0-9-1 spec — &lt;code&gt;s&lt;/code&gt; = short string, &lt;code&gt;U&lt;/code&gt; = signed 16-bit — with no mention of the conflict anywhere in its codebase.&lt;/p&gt;
&lt;p&gt;Neither library is being reckless. They each picked a side of a genuine spec conflict. The problem is they disagree on the one tag that affected our &lt;code&gt;timelimit&lt;/code&gt; value, and the disagreement produces no error — just silently wrong data.&lt;/p&gt;
&lt;h2 id="how-we-solved-this"&gt;How we solved this&lt;/h2&gt;
&lt;p&gt;We swapped &lt;code&gt;aio-pika&lt;/code&gt; out for &lt;code&gt;pika&lt;/code&gt; to do the message transfer. &lt;code&gt;pika&lt;/code&gt; has its own wire encoder and uses the &lt;code&gt;U&lt;/code&gt; type tag for 16-bit integers — the same convention py-amqp expects on the receiving end, so the two sides finally agreed. Problem solved for new messages.&lt;/p&gt;
&lt;p&gt;The already-corrupt messages in the queues needed a different approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set a delivery limit policy in RabbitMQ&lt;/li&gt;
&lt;li&gt;When messages hit that delivery limit, route them to a dead letter queue&lt;/li&gt;
&lt;li&gt;Wrote a script to read from those DLQs, check the &lt;code&gt;timelimit&lt;/code&gt; header, and fix the value from &lt;code&gt;[None, ',']&lt;/code&gt; to &lt;code&gt;[None, 300]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Redrive those messages back to the original destination queue&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;I guess the real lesson here isn&amp;rsquo;t &amp;ldquo;don&amp;rsquo;t use aio-pika&amp;rdquo; — it&amp;rsquo;s that AMQP&amp;rsquo;s fragmented spec history means two fully compliant-looking libraries can silently disagree at the byte level.
If you&amp;rsquo;re ever mixing libraries across the publish/consume boundary, verify they share the same type tag conventions before you find out the hard way.&lt;/p&gt;</content:encoded></item><item><title>Making it easy to track my diet via a web app</title><link>https://shtlrs.com/posts/diet-tracking-application/</link><pubDate>Sun, 16 Aug 2026 12:25:56 +0100</pubDate><guid>https://shtlrs.com/posts/diet-tracking-application/</guid><content:encoded>&lt;p&gt;I used an AI agent to build meal and workout plans that would help me lose weight and deployed it to Cloudflare pages&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;
&lt;p&gt;Back in my university days, I used to be super lean, workout everyday, and was able to remain motivated and stay fit.&lt;/p&gt;
&lt;p&gt;As I was getting older, my metabolism started slowing down, and I had gotten injured a lot of times, and most importantly, got caught up in life.&lt;/p&gt;
&lt;p&gt;All of these factors made me gain weight, and lose my motivation to get back in shape.&lt;/p&gt;
&lt;p&gt;The difference was that back when I was younger, it was much easier to just go from slim to fit, as the process didn&amp;rsquo;t involve losing weight at all. It was
just about training day after day, and the body responded quite nicely.&lt;/p&gt;
&lt;p&gt;However now, I had to work my way backwards as I had gained over 20 Kgs from my university days, and trying to just raw-dogging it by trying to remember what I ate,
ballpark estimate the calories and refrain from certain stuff was just not working out for me. I was seeing no results, and just kept on gaining weight.&lt;/p&gt;
&lt;p&gt;I think we all know that diets are no fun at all, and I personally think that it&amp;rsquo;s super difficult to stay consistent, but to also remember what you should and should not
eat, how much you should/must eat, but also include enough things to not get bored, not starve yourself out if you have a big appetite like I do, but also not feel like you&amp;rsquo;re in a
military boot camp and you have a sergeant yelling constantly at you and treating you like his bitch.&lt;/p&gt;
&lt;h2 id="doing-it-the-conventional-way"&gt;Doing it the &amp;ldquo;conventional&amp;rdquo; way&lt;/h2&gt;
&lt;p&gt;Of course, this wasn&amp;rsquo;t the first time I had tried to do this, and I think it&amp;rsquo;s a &amp;ldquo;classic&amp;rdquo; problem that people try to solve.&lt;/p&gt;
&lt;p&gt;It usually goes by making a google search on how to lose some weight, find some diet program, and try to follow that.&lt;/p&gt;
&lt;p&gt;That stuff just doesn&amp;rsquo;t really work well with me, and I can&amp;rsquo;t really explain how, it felt like I needed some sort of process that just grouped it all in once place:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The list of stuff I need to buy&lt;/li&gt;
&lt;li&gt;The meals I can prepare, with the portions&lt;/li&gt;
&lt;li&gt;The workout plan, because the purpose was to keep the muscle mass, and lose fat&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I tried doing it this way twice or three times before, but it had never worked out.&lt;/p&gt;
&lt;p&gt;So this made me wonder: What if I built a custom app that would:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Give me daily meal plans, that change every day&lt;/li&gt;
&lt;li&gt;Gives me a list of ingredients to buy once and for all, enough stock for a week.&lt;/li&gt;
&lt;li&gt;Adapt it to my taste, so that it makes meals I&amp;rsquo;d be willing to eat.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;m sure that there are apps that exist and do this already, but I figured this could be a fun project to do, and I write code for a living so why not ?&lt;/p&gt;
&lt;p&gt;Moreover, we live in an era where agents can do this for us super easily, so why the heck not ?&lt;/p&gt;
&lt;h2 id="making-a-diet-tracking-app"&gt;Making a diet tracking app&lt;/h2&gt;
&lt;p&gt;The requirements were the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I wanted to maximize fat loss, and minimize muscle loss, without being too aggressive on myself&lt;/li&gt;
&lt;li&gt;I wanted a diverse, but no so boring diet, meals that changed weekly&lt;/li&gt;
&lt;li&gt;I wanted the meals to be easy to make from both a time/prep and ingredient perspective&lt;/li&gt;
&lt;li&gt;I wanted the diet to be volume heavy, because I like to eat a lot&lt;/li&gt;
&lt;li&gt;I wanted to avoid going on an ingredient hunt at all costs, so only stuff I can almost find everywhere&lt;/li&gt;
&lt;li&gt;I wanted it to be high on fibers as well, for good transit&lt;/li&gt;
&lt;li&gt;Include sweet treats from time to time, because who doesn&amp;rsquo;t like that&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now that I had the requirements in place, it was time to lay out how I imagined this would work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I wanted a web app that only I could access, deployed on some domain I own&lt;/li&gt;
&lt;li&gt;I wanted the app to list all the ingredients I need to buy for the entire week, as checkboxes I could tick to keep track&lt;/li&gt;
&lt;li&gt;I wanted the list of meals to shuffle every day, and have a button to shuffle either the entire day meal plan, or individual meals should I not feel like it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The above resulted in the following initial prompt:&lt;/p&gt;
&lt;details class="collapsible-code"&gt;
&lt;summary&gt;Show/hide code&lt;/summary&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;I want to build a web app that allows me to track my diet.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Here are the diet&lt;span style="color:#e6db74"&gt;&amp;#39;s requirements:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;* I wanted to maximize fat loss, and minimize muscle loss, without being too aggressive on myself, so make it protein heavy
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;* I wanted a diverse, but no so boring diet, meals that changed weekly, preferably from a list of cuisines that I can chose: asian/italian/mexicain
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;* I wanted the meals to be easy to make from both a time/prep and ingredient perspective
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;* I wanted the diet to be volume heavy, but not high on calories, because I like to eat a lot
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;* I want the ingredients list to be super easy to find in most supermarkets
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;* Include sweet treats from time to time
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;* I wanted it to be high on fibers as well, for good transit. I have crackers that have these nutritional values per 100g: 46g carbs, 5g fat, 26g fiber. Include it in breakfasts and snacks
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;As for the app, i want it to do the following:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;* I want a static site, deployed on cloudflare pages
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;* I want it so that only I could access it
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;* The app needs to allow me to change the weekly cuisine should I change my mind
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;* The app will list the weekly ingredients, with exact portions to buy. These might be persisted somwehre to be able to track what I bought and what I haven&amp;#39;&lt;/span&gt;t yet, and resets every Sunday
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;* Randomize the meals per day, so that I don&lt;span style="color:#960050;background-color:#1e0010"&gt;&amp;#39;&lt;/span&gt;t eat the same thing everyday. And make it so that I can shuffle each meal individually should I feel like it.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;For deployment, I had &lt;a href="https://www.cloudflare.com/"&gt;Cloudflare&lt;/a&gt; as my domain registrar, and I knew that they had
a solution called &lt;a href="https://pages.cloudflare.com/"&gt;Pages&lt;/a&gt;, so I thought this could be more than enough for what I wanted to do, and it fits nicely since it&amp;rsquo;s all configured
in one place. As for persisting state, I used &lt;a href="https://developers.cloudflare.com/kv/"&gt;Cloudflare Worker KV&lt;/a&gt;, which is a simple key-value store.&lt;/p&gt;
&lt;p&gt;For the curious ones, the application code can be found at the following &lt;a href="https://github.com/shtlrs/operation-80"&gt;repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This of course took a heuristic approach where I&amp;rsquo;d serve the content locally, test out the difference features before deploying and potentially fix issues.&lt;/p&gt;
&lt;p&gt;Some of the issues were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clicking a checkbox would immediately uncheck itself: The click event was fired twice - once from the row and once from the input inside it. Fixed by checking the origin of the event: &lt;code&gt;e.target.type === &amp;quot;checkbox&amp;quot; ? e.target.checked: undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The state was being saved, but not retrieved properly: This was a dumb AI moment where key construction was wrong, so just made it so that they to use to fetch data is as simple as the {section}/{date}, where section is either the ingredients list, workouts, other misc actions such as measuring my daily weight, etc.&lt;/li&gt;
&lt;li&gt;Sometimes the app would deploy in preview mode: This was because the wrangler CLI would check the branch you&amp;rsquo;re in, and if it&amp;rsquo;s not the main one, it won&amp;rsquo;t deploy to production. I fixed this by having a &lt;code&gt;deploy.sh&lt;/code&gt; script that would explicitly prompt me to which environment I&amp;rsquo;d like to deploy, regardless of the branch.&lt;/li&gt;
&lt;li&gt;The deploy version was not being rendered properly, and that&amp;rsquo;s because it built a function that fetches the deployed version, but wasn&amp;rsquo;t rendering it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="results"&gt;Results&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been doing this for 3 weeks now, and I&amp;rsquo;m losing the projected weight of 1Kg/Week.&lt;/p&gt;
&lt;p&gt;The agent added a checkbox to check weight daily, but I felt that doing this on a weekly basis would yield greater satisfaction, so that&amp;rsquo;s a part I wasn&amp;rsquo;t using of the application.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s still a long way to go, because the overall weight I&amp;rsquo;m trying to lose is 10Kgs, so that&amp;rsquo;s 7 more weeks in total, but the road ahead seems solid as long as I keep my spirits high and do my best to follow the plan.&lt;/p&gt;
&lt;p&gt;This was a fun project that I don&amp;rsquo;t regret at all, even though I hardly spent 2 hours in total on it, mostly giving feedback to the agent about what&amp;rsquo;s not working.&lt;/p&gt;
&lt;p&gt;Whether this is worth it for others to do is a completely subjective topic, as things that don&amp;rsquo;t work for me can work pretty well for others, and vice versa.&lt;/p&gt;
&lt;p&gt;And finally, for the curious, here&amp;rsquo;s what the application looks like.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Full application preview&lt;/summary&gt;
&lt;p&gt;&lt;img src="app-screenshot.png" alt="App"&gt;.&lt;/p&gt;</content:encoded></item><item><title>Writing easily accessible text from your terminal</title><link>https://shtlrs.com/posts/quick-text-editing-in-terminal/</link><pubDate>Fri, 17 Jul 2026 12:25:56 +0100</pubDate><guid>https://shtlrs.com/posts/quick-text-editing-in-terminal/</guid><content:encoded>&lt;p&gt;A shell function I wrote to stop reaching for Sublime Text every time I need to write a Slack message.&lt;/p&gt;
&lt;h2 id="the-itch"&gt;The Itch&lt;/h2&gt;
&lt;p&gt;The further I go in my career, the more obsessed I get with shaving steps off things that mildly annoy me. It&amp;rsquo;s a sickness, and I&amp;rsquo;ve made peace with it.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;At work, I juggle three things all day: my terminal, my IDE, and Slack.&lt;/p&gt;
&lt;p&gt;Slack is the weak link. As a Vim user, editing messages is a pain — navigating text with arrow keys and no modal editing makes me feel like I&amp;rsquo;ve forgotten how to type. So I&amp;rsquo;d do this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open Sublime Text (vim mode, no save required)&lt;/li&gt;
&lt;li&gt;Open a new tab&lt;/li&gt;
&lt;li&gt;Write and polish the text&lt;/li&gt;
&lt;li&gt;Select all, copy&lt;/li&gt;
&lt;li&gt;Close the tab (maybe)&lt;/li&gt;
&lt;li&gt;Send the message&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Cumbersome. Four tools open just to write a Slack message.&lt;/p&gt;
&lt;p&gt;So I wrote this:&lt;/p&gt;
&lt;details class="collapsible-code"&gt;
&lt;summary&gt;Show/hide code&lt;/summary&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### in your ~/.zshrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;s&lt;span style="color:#f92672"&gt;()&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; local f
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; f&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;mktemp /tmp/scratch.XXXXXX&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vi &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$f&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cat &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$f&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; | pbcopy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rm -f &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$f&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Now it&amp;rsquo;s:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Type &lt;code&gt;s&lt;/code&gt; in the terminal&lt;/li&gt;
&lt;li&gt;Write and edit the text in Vim&lt;/li&gt;
&lt;li&gt;Close the file — it&amp;rsquo;s already in your clipboard&lt;/li&gt;
&lt;li&gt;Send the message&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="thats-it"&gt;&amp;ldquo;That&amp;rsquo;s it?&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;Yes. Two steps fewer, one tool fewer, zero context switching.&lt;/p&gt;
&lt;p&gt;I know it sounds absurdly small. But small friction compounds. Every time you reach for a separate tool, you break flow, lose a few seconds, and add just enough overhead that you sometimes just&amp;hellip; don&amp;rsquo;t bother refining what you&amp;rsquo;re writing.&lt;/p&gt;
&lt;p&gt;This little function removed that resistance entirely.&lt;/p&gt;
&lt;p&gt;If you have a similar itch, here&amp;rsquo;s a challenge: what other tools in your workflow could be replaced by a scratch buffer and a copy to clipboard?&lt;/p&gt;</content:encoded></item><item><title>Testing Ansible Playbooks Locally</title><link>https://shtlrs.com/posts/testing-ansible-playbooks-locally/</link><pubDate>Sat, 03 Jan 2026 17:25:56 +0100</pubDate><guid>https://shtlrs.com/posts/testing-ansible-playbooks-locally/</guid><content:encoded>&lt;p&gt;At &lt;a href="https://pythondiscord.com"&gt;Python Discord&lt;/a&gt;, we use &lt;a href="https://docs.ansible.com/"&gt;Ansible&lt;/a&gt; to set up a part of our
infrastructure.&lt;/p&gt;
&lt;p&gt;A &amp;ldquo;bad&amp;rdquo; pattern we had is that we always tested these playbooks in production. I say &amp;ldquo;bad&amp;rdquo; because I think this can be a
pretty controversial topic, and I&amp;rsquo;ve worked in a lot of companies who do this, or cheated their way around it, but that&amp;rsquo;s
off-topic for now.&lt;/p&gt;
&lt;p&gt;This led us to setting up a way to easily test our playbooks, but on a similar environment, without running the risk of
unintentionally performing an undesired/destructive action or whatever.&lt;/p&gt;
&lt;h2 id="disclaimer"&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;This post assumes familiarity with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.ansible.com/ansible/latest/getting_started/index.html"&gt;Ansible fundamentals&lt;/a&gt; (playbooks, roles, inventory)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/vagrant/tutorials/getting-started"&gt;Vagrant basics&lt;/a&gt; (managing VMs via Vagrantfiles)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;ll explain concepts briefly as they come up, but having baseline knowledge will help you follow along more easily.&lt;/p&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;At the time of writing this, I was on macOS Tahoe 26.1, using &lt;code&gt;HomeBrew&lt;/code&gt; version &lt;code&gt;5.0.8-43-gfe0a384&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To do this, you&amp;rsquo;ll have to install:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;virtualbox&lt;/code&gt; version &lt;code&gt;7.2.4,170995&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vagrant&lt;/code&gt; version &lt;code&gt;2.4.9&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="project-overview"&gt;Project overview&lt;/h2&gt;
&lt;p&gt;The project we&amp;rsquo;ll be working on in this tutorial is a made up version for the sake of simplicity.&lt;/p&gt;
&lt;p&gt;The one we use at Python Discord is open source, and can be found in our &lt;a href="https://github.com/python-discord/infra/tree/040ae8a484c2b27b164f751b0dd57f921e8aae67/ansible"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The project root&amp;rsquo;s will be at the &lt;code&gt;infra&lt;/code&gt; directory&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── infra/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├── Vagrantfile
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├── playbook.yml
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├── roles/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ └── hello_world/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ └── tasks/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ └── main.yml
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── inventory/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├── hosts.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── vagrant_hosts.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Vagrantfile&lt;/code&gt;: The &lt;code&gt;Vagrant&lt;/code&gt; configuration that we&amp;rsquo;ll use to configure our VMs to simulate our servers.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;playbook.yml&lt;/code&gt;: The &lt;code&gt;Ansible&lt;/code&gt; playbook that orchestrates all the roles/tasks that need to run on our infra&lt;/li&gt;
&lt;li&gt;&lt;code&gt;roles/hello_world/tasks/main.yml&lt;/code&gt;: The main task of the &lt;code&gt;hello_world&lt;/code&gt; &lt;a href="https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_reuse_roles.html"&gt;Ansible Role&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inventory/hosts.yaml&lt;/code&gt;: The production hosts file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inventory/vagrant_hosts.yaml&lt;/code&gt;: The hosts to use when testing the playbooks locally with &lt;code&gt;Vagrant&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can find the content of all of these files in the &lt;a href="#file-contents"&gt;File content section&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="setting-up-vagrant"&gt;Setting Up Vagrant&lt;/h2&gt;
&lt;p&gt;Very quickly, &lt;code&gt;Vagrant&lt;/code&gt; is a tool that allows you to configure/provision/manage virtual machines on your host in complete isolation.&lt;/p&gt;
&lt;p&gt;This configuration and provisioning is done via a &lt;a href="https://developer.hashicorp.com/vagrant/docs/vagrantfile"&gt;VagrantFile&lt;/a&gt;
which allows you to write some sort of manifest in Ruby that&amp;rsquo;ll set up the virtual machines for you.&lt;/p&gt;
&lt;h3 id="general-box-configuration"&gt;General box configuration&lt;/h3&gt;
&lt;p&gt;Vagrant allows you to have either &amp;ldquo;global&amp;rdquo; or per VM configuration.
We actually need and will be using both, but the one in this section explains the global one.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Vagrant&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;configure(&lt;span style="color:#e6db74"&gt;&amp;#34;2&amp;#34;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;config&lt;span style="color:#f92672"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# This is the base image that will be used for all VMs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; config&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;box &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;bento/debian-13&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# This is provisioning that will run on all VMs, but custom provisioning per specific VM is also possible. &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; config&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;provision &lt;span style="color:#e6db74"&gt;&amp;#34;shell&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;inline&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;lt;&amp;lt;-SHELL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sudo apt&lt;span style="color:#f92672"&gt;-&lt;/span&gt;get update
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; apt&lt;span style="color:#f92672"&gt;-&lt;/span&gt;get install &lt;span style="color:#f92672"&gt;-&lt;/span&gt;y python3 python3&lt;span style="color:#f92672"&gt;-&lt;/span&gt;pip sshpass
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;SHELL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="configuring-the-control-nodevm"&gt;Configuring the control node/VM&lt;/h3&gt;
&lt;p&gt;The playbook/roles, etc. need to run from what Ansible calls the &lt;a href="https://docs.ansible.com/ansible/latest/getting_started/basic_concepts.html#control-node"&gt;control node&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The following is the &lt;code&gt;Vagrant&lt;/code&gt; configuration to create that node.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Vagrant&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;configure(&lt;span style="color:#e6db74"&gt;&amp;#34;2&amp;#34;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;config&lt;span style="color:#f92672"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Global provisioning seen previously has been omitted to focus solely on the control node&amp;#39;s config&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Define `control` as the primary VM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; config&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;define &lt;span style="color:#e6db74"&gt;&amp;#34;control&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;primary&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;control&lt;span style="color:#f92672"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Copy the content of the folder running `Vagrant` into control&amp;#39;s `/infra` folder&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# This allows us to have the playbooks/roles that will run from the control node&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; config&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;synced_folder &lt;span style="color:#e6db74"&gt;&amp;#34;.&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/infra&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;type&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;rsync&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# We assign a static IP for all VMs to easily identify them&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; control&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;network &lt;span style="color:#e6db74"&gt;&amp;#34;private_network&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;ip&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;192.168.56.2&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;virtualbox__intnet&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; control&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;hostname &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;control&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Install Ansible on the control node, required to run the playbooks &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; control&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;provision &lt;span style="color:#e6db74"&gt;&amp;#34;Install Ansible&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;type&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;shell&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;inline&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;lt;&amp;lt;-SHELL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; apt&lt;span style="color:#f92672"&gt;-&lt;/span&gt;get update
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; apt&lt;span style="color:#f92672"&gt;-&lt;/span&gt;get install &lt;span style="color:#f92672"&gt;-&lt;/span&gt;y ansible
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;SHELL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# SSH configuration to allow the control node to connect to all managed nodes effortlessly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; control&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;provision &lt;span style="color:#e6db74"&gt;&amp;#34;setup_ssh&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;type&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;shell&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;privileged&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;run&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;never&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;inline&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;lt;&amp;lt;-SHELL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ssh&lt;span style="color:#f92672"&gt;-&lt;/span&gt;keygen &lt;span style="color:#f92672"&gt;-&lt;/span&gt;t rsa &lt;span style="color:#f92672"&gt;-&lt;/span&gt;N &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;f &lt;span style="color:#f92672"&gt;~&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/.ssh/i&lt;/span&gt;d_rsa &lt;span style="color:#f92672"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; y
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ssh&lt;span style="color:#f92672"&gt;-&lt;/span&gt;keyscan &lt;span style="color:#ae81ff"&gt;192&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;168&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;56&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#f92672"&gt;~&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/.ssh/&lt;/span&gt;known_hosts
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sshpass &lt;span style="color:#f92672"&gt;-&lt;/span&gt;p vagrant ssh&lt;span style="color:#f92672"&gt;-&lt;/span&gt;copy&lt;span style="color:#f92672"&gt;-&lt;/span&gt;id &lt;span style="color:#ae81ff"&gt;192&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;168&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;56&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;SHELL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Specify the provider to use for creating the VM &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; control&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;provider &lt;span style="color:#e6db74"&gt;&amp;#34;virtualbox&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;v&lt;span style="color:#f92672"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; v&lt;span style="color:#f92672"&gt;.&lt;/span&gt;name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;control_node&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="ssh-provisioning"&gt;SSH Provisioning&lt;/h4&gt;
&lt;p&gt;You might have noticed that &lt;code&gt;setup_ssh&lt;/code&gt; that is configured to never execute upon running &lt;code&gt;vagrant&lt;/code&gt; up.&lt;/p&gt;
&lt;p&gt;The reason we have it in the first place is to allow the &lt;code&gt;control&lt;/code&gt; node to easily connect over SSH to &lt;code&gt;hopper&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We do this by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Including &lt;code&gt;hopper&lt;/code&gt; in the list of &lt;code&gt;control&lt;/code&gt;&amp;rsquo;s known hosts&lt;/li&gt;
&lt;li&gt;Copying &lt;code&gt;control&lt;/code&gt;&amp;rsquo;s public key over to &lt;code&gt;hopper&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, it&amp;rsquo;s set to &lt;code&gt;never&lt;/code&gt; because we want to run the provisioning when both &lt;code&gt;control&lt;/code&gt; and &lt;code&gt;hopper&lt;/code&gt; are up and running,
since it requires fetching &lt;code&gt;hopper&lt;/code&gt;&amp;rsquo;s host key, and adding &lt;code&gt;control&lt;/code&gt;&amp;lsquo;ls public key to &lt;code&gt;hopper&lt;/code&gt;&lt;/p&gt;
&lt;h3 id="managed-nodes-configuration"&gt;Managed node&amp;rsquo;s configuration&lt;/h3&gt;
&lt;p&gt;The actual machines being configured by Ansible are called the &lt;a href="https://docs.ansible.com/ansible/latest/getting_started/basic_concepts.html#managed-nodes"&gt;managed nodes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In our case, we will be configuring one VM only to keep things simple.&lt;/p&gt;
&lt;p&gt;If you want to manage and configure multiple VMs, the principle is the same, you just need to replicate the configuration
for this one, and alter the assigned static ip.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Vagrant&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;configure(&lt;span style="color:#e6db74"&gt;&amp;#34;2&amp;#34;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;config&lt;span style="color:#f92672"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Global provisioning seen previously has been omitted to focus solely on the control node&amp;#39;s config&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; config&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;define &lt;span style="color:#e6db74"&gt;&amp;#34;hopper&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;hopper&lt;span style="color:#f92672"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# We assign a static IP for the managed node to easily identify it&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hopper&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;network &lt;span style="color:#e6db74"&gt;&amp;#34;private_network&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;ip&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;192.168.56.3&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;virtualbox__intnet&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hopper&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;hostname &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;hopper&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Disable any folder syncing, as the machines are supposedly fresh/empty&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hopper&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;synced_folder &lt;span style="color:#e6db74"&gt;&amp;#39;.&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;/vagrant&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;disabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Chose the provider to create the VM. &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hopper&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;provider &lt;span style="color:#e6db74"&gt;&amp;#34;virtualbox&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;v&lt;span style="color:#f92672"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; v&lt;span style="color:#f92672"&gt;.&lt;/span&gt;name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;hopper&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This one&amp;rsquo;s quite simple, we&amp;rsquo;re basically just creating a fresh VM, and assigning a static ip of &lt;code&gt;192.168.56.3&lt;/code&gt; to it
so that we can easily configure and target it in &lt;code&gt;Ansible&lt;/code&gt;&amp;rsquo;s host inventory.&lt;/p&gt;
&lt;h3 id="complete-vagrantfile-content"&gt;Complete VagrantFile content&lt;/h3&gt;
&lt;p&gt;This is included here just to see what the &lt;code&gt;Vagrantfile&lt;/code&gt; will finally look like once we&amp;rsquo;ve combined all the previously
explained sections.&lt;/p&gt;
&lt;details class="collapsible-code"&gt;
&lt;summary&gt;Show/hide code&lt;/summary&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Vagrant&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;configure(&lt;span style="color:#e6db74"&gt;&amp;#34;2&amp;#34;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;config&lt;span style="color:#f92672"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; config&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;box &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;bento/debian-13&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; config&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;provision &lt;span style="color:#e6db74"&gt;&amp;#34;shell&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;inline&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;lt;&amp;lt;-SHELL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sudo apt&lt;span style="color:#f92672"&gt;-&lt;/span&gt;get update
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; apt&lt;span style="color:#f92672"&gt;-&lt;/span&gt;get install &lt;span style="color:#f92672"&gt;-&lt;/span&gt;y python3 python3&lt;span style="color:#f92672"&gt;-&lt;/span&gt;pip sshpass
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;SHELL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; config&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;define &lt;span style="color:#e6db74"&gt;&amp;#34;control&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;primary&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;control&lt;span style="color:#f92672"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; config&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;synced_folder &lt;span style="color:#e6db74"&gt;&amp;#34;.&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/infra&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;type&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;rsync&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; control&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;network &lt;span style="color:#e6db74"&gt;&amp;#34;private_network&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;ip&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;192.168.56.2&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;virtualbox__intnet&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; control&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;hostname &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;control&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; control&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;provision &lt;span style="color:#e6db74"&gt;&amp;#34;Install Ansible&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;type&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;shell&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;inline&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;lt;&amp;lt;-SHELL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; apt&lt;span style="color:#f92672"&gt;-&lt;/span&gt;get update
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; apt&lt;span style="color:#f92672"&gt;-&lt;/span&gt;get install &lt;span style="color:#f92672"&gt;-&lt;/span&gt;y ansible
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;SHELL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; control&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;provision &lt;span style="color:#e6db74"&gt;&amp;#34;setup_ssh&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;type&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;shell&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;privileged&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;run&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;never&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;inline&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;lt;&amp;lt;-SHELL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ssh&lt;span style="color:#f92672"&gt;-&lt;/span&gt;keygen &lt;span style="color:#f92672"&gt;-&lt;/span&gt;t rsa &lt;span style="color:#f92672"&gt;-&lt;/span&gt;N &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;f &lt;span style="color:#f92672"&gt;~&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/.ssh/i&lt;/span&gt;d_rsa &lt;span style="color:#f92672"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; y
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ssh&lt;span style="color:#f92672"&gt;-&lt;/span&gt;keyscan &lt;span style="color:#ae81ff"&gt;192&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;168&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;56&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#f92672"&gt;~&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/.ssh/&lt;/span&gt;known_hosts
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sshpass &lt;span style="color:#f92672"&gt;-&lt;/span&gt;p vagrant ssh&lt;span style="color:#f92672"&gt;-&lt;/span&gt;copy&lt;span style="color:#f92672"&gt;-&lt;/span&gt;id &lt;span style="color:#ae81ff"&gt;192&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;168&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;56&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;SHELL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; control&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;provider &lt;span style="color:#e6db74"&gt;&amp;#34;virtualbox&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;v&lt;span style="color:#f92672"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; v&lt;span style="color:#f92672"&gt;.&lt;/span&gt;name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;control_node&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; config&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;define &lt;span style="color:#e6db74"&gt;&amp;#34;hopper&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;hopper&lt;span style="color:#f92672"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hopper&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;network &lt;span style="color:#e6db74"&gt;&amp;#34;private_network&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;ip&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;192.168.56.3&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;virtualbox__intnet&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hopper&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;hostname &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;hopper&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hopper&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;synced_folder &lt;span style="color:#e6db74"&gt;&amp;#39;.&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;/vagrant&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;disabled&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hopper&lt;span style="color:#f92672"&gt;.&lt;/span&gt;vm&lt;span style="color:#f92672"&gt;.&lt;/span&gt;provider &lt;span style="color:#e6db74"&gt;&amp;#34;virtualbox&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;do&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;v&lt;span style="color:#f92672"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; v&lt;span style="color:#f92672"&gt;.&lt;/span&gt;name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;hopper&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;h3 id="setting-up-the-virtual-machines"&gt;Setting Up The Virtual Machines&lt;/h3&gt;
&lt;p&gt;Enough with the boring configuration stuff. It&amp;rsquo;s now time to bring those VMs up and running in order to be able to use them.&lt;/p&gt;
&lt;p&gt;This is actually quite simple, and requires running the following two commands from within the &lt;code&gt;infra&lt;/code&gt; directory&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vagrant up
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vagrant provision control --provision-with setup_ssh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="running-the-playbooks"&gt;Running The Playbooks&lt;/h2&gt;
&lt;p&gt;Once the VMs are provisioned, we can now move on to testing our &lt;code&gt;Ansible&lt;/code&gt; playbooks.&lt;/p&gt;
&lt;p&gt;First, we ssh into &lt;code&gt;control&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vagrant ssh control
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, move into the &lt;code&gt;infra&lt;/code&gt; folder.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd /infra
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, we run the playbooks&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ansible-playbook playbook.yml --inventory inventory/vagrant_hosts.yml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can verify that the &lt;code&gt;hello_world&lt;/code&gt; role ran by running the following commands&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vagrant ssh hopper -c &lt;span style="color:#e6db74"&gt;&amp;#34;cat ~/hello.txt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will display &amp;ldquo;Hello World&amp;rdquo; in the console, proving the role ran successfully&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It&amp;rsquo;s important to note that whenever you change something, e.g. the roles, the playbook, etc. you&amp;rsquo;re gonna have to
sync those into the VM by running &lt;code&gt;vagrant rsync&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="cleanup"&gt;Cleanup&lt;/h2&gt;
&lt;p&gt;When you&amp;rsquo;re done testing or want to start fresh, simply destroy all VMs by running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vagrant destroy -f
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;While I was writing this, it came to my knowledge that there is an &lt;code&gt;Ansible&lt;/code&gt; provisioner that can be used to do all of this
but differently.&lt;/p&gt;
&lt;p&gt;However, I still wanted to demonstrate how we actually did it back then.&lt;/p&gt;
&lt;p&gt;I must say that this is obviously not the only way to test playbooks locally, but it is the one we decided to use back then.&lt;/p&gt;
&lt;p&gt;I might make updates to this post at some point to illustrate how we could the Ansible provisioner instead, or maybe even
test the playbooks on a docker container instead of using &lt;code&gt;Vagrant&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="file-contents"&gt;File contents&lt;/h2&gt;
&lt;p&gt;This section exists to display the contents of all the different files that take part of this tutorial, in case someone
wants to test this end to end.&lt;/p&gt;
&lt;h3 id="playbookyml"&gt;Playbook.yml&lt;/h3&gt;
&lt;details class="collapsible-code"&gt;
&lt;summary&gt;Show/hide code&lt;/summary&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Setup infra&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hosts&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;all&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;roles&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;hello_world&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;h3 id="roleshello_worldtasksmainyml"&gt;roles/hello_world/tasks/main.yml&lt;/h3&gt;
&lt;details class="collapsible-code"&gt;
&lt;summary&gt;Show/hide code&lt;/summary&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Create hello world file in user&amp;#39;s home directory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ansible.builtin.copy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;content&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Hello world&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;dest&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;{{ ansible_env.HOME }}/hello.txt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;mode&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;a=r&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;h3 id="vagrant_hostsyml"&gt;vagrant_hosts.yml&lt;/h3&gt;
&lt;details class="collapsible-code"&gt;
&lt;summary&gt;Show/hide code&lt;/summary&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;all&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hosts&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hopper&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ansible_host&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;192.168.56.3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ip&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;192.168.56.3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;access_ip&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;192.168.56.3&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;</content:encoded></item><item><title>Upgrading RabbitMQ To v4.x Without Breaking Celery ETA Tasks</title><link>https://shtlrs.com/posts/rabbitmq-celery-upgrade/</link><pubDate>Sun, 21 Dec 2025 13:25:56 +0100</pubDate><guid>https://shtlrs.com/posts/rabbitmq-celery-upgrade/</guid><content:encoded>&lt;p&gt;Upgrading to RabbitMQ v4 threatened to break our entire usage of Celery, more specifically tasks with ETAs.&lt;/p&gt;
&lt;p&gt;At 8M messages/day with zero downtime tolerance, we needed a migration strategy that preserves delayed task execution while switching from classic to quorum queues.&lt;/p&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;This post assumes familiarity with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.celeryq.dev/en/stable/getting-started/introduction.html"&gt;Celery task queues&lt;/a&gt; (workers, tasks, brokers)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rabbitmq.com/tutorials/tutorial-one-python.html"&gt;RabbitMQ fundamentals&lt;/a&gt; (exchanges, queues, routing)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cloudamqp.com/blog/part1-rabbitmq-for-beginners-what-is-rabbitmq.html"&gt;Message queue concepts&lt;/a&gt; (producers, consumers, brokers)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cloudamqp.com/blog/what-is-a-rabbitmq-vhost.html"&gt;Virtual hosts&lt;/a&gt; (multi-tenancy in RabbitMQ)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;re comfortable with distributed task processing in Python, you&amp;rsquo;re good to go.&lt;/p&gt;
&lt;h2 id="context"&gt;Context&lt;/h2&gt;
&lt;p&gt;At &lt;a href="https://kraken.tech"&gt;Kraken&lt;/a&gt;, we use &lt;a href="https://docs.celeryq.dev/"&gt;Celery&lt;/a&gt; to offload long-running tasks to workers via &lt;a href="https://www.rabbitmq.com/"&gt;RabbitMQ&lt;/a&gt; (v3.13.7.1). Our platform team needed to upgrade to RabbitMQ v4.2.2, which introduces breaking changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rabbitmq.com/blog/2021/08/21/4.0-deprecation-announcements#removal-of-global-qos"&gt;Global QoS removal&lt;/a&gt; - ETA/countdown tasks now &lt;a href="https://docs.celeryq.dev/en/v5.6.0/getting-started/backends-and-brokers/rabbitmq.html#limitations"&gt;block workers until execution time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rabbitmq.com/blog/2021/08/21/4.0-deprecation-announcements#removal-of-classic-queue-mirroring"&gt;Classic Queue Mirroring removal&lt;/a&gt; - forced migration to &lt;a href="https://www.rabbitmq.com/docs/quorum-queues"&gt;Quorum Queues&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="challenges"&gt;Challenges&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Can&amp;rsquo;t switch queue types on the fly&lt;/strong&gt;: RabbitMQ doesn&amp;rsquo;t let you change a queue&amp;rsquo;s type after it&amp;rsquo;s created. Normally you&amp;rsquo;d just delete the queue and recreate it with the new type, but that wasn&amp;rsquo;t an option for us. We&amp;rsquo;re pushing 8M messages/day across &lt;a href="https://engineering.kraken.tech/news/2025/02/07/how-we-ship.html"&gt;many environments&lt;/a&gt;, and our SLAs don&amp;rsquo;t allow for any downtime.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Workers would block indefinitely&lt;/strong&gt;: Without &lt;a href="https://www.rabbitmq.com/docs/consumer-prefetch"&gt;global QoS&lt;/a&gt;, any task with an ETA would block a worker until that ETA arrived, completely defeating the purpose of async processing. Fortunately, Celery has a feature called &lt;a href="https://docs.celeryq.dev/en/v5.5.3/getting-started/backends-and-brokers/rabbitmq.html#native-delayed-delivery"&gt;Native Delayed Delivery&lt;/a&gt; that solves this—but it requires &lt;a href="https://www.rabbitmq.com/docs/quorum-queues"&gt;quorum queues&lt;/a&gt; bound to &lt;a href="https://www.rabbitmq.com/tutorials/tutorial-five-python.html"&gt;topic exchanges&lt;/a&gt;. Lucky for us, that&amp;rsquo;s exactly what we needed to migrate to anyway.&lt;/p&gt;
&lt;h2 id="solution"&gt;Solution&lt;/h2&gt;
&lt;p&gt;Our migration strategy:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new &lt;a href="https://www.cloudamqp.com/blog/what-is-a-rabbitmq-vhost.html"&gt;vhost&lt;/a&gt; (&lt;code&gt;qhost&lt;/code&gt;), which will host quorum queues bound to topic exchanges&lt;/li&gt;
&lt;li&gt;Configure application code to support both queue types via a feature flag&lt;/li&gt;
&lt;li&gt;Transfer messages from old vhost (&lt;code&gt;chost&lt;/code&gt;) to new vhost without losing ETA information&lt;/li&gt;
&lt;li&gt;Decommission &lt;code&gt;chost&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rabbitmq.com/docs/rolling-upgrade"&gt;Rolling upgrade&lt;/a&gt; to RabbitMQ v4&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="phase-1-new-virtual-host"&gt;Phase 1: New Virtual Host&lt;/h3&gt;
&lt;p&gt;The first step was straightforward: create a new virtual host (&lt;code&gt;qhost&lt;/code&gt;) to run alongside our existing one (&lt;code&gt;chost&lt;/code&gt;). For us, this meant updating some infrastructure manifests to provision the new vhost. Not the most exciting part, but essential for what comes next—we needed both vhosts running simultaneously to avoid any downtime.&lt;/p&gt;
&lt;h3 id="phase-2-feature-flagged-queue-configuration"&gt;Phase 2: Feature-Flagged Queue Configuration&lt;/h3&gt;
&lt;p&gt;Next, we needed to make our application code flexible enough to handle both the old and new queue configurations. We introduced a &lt;code&gt;USE_QUORUM_QUEUES&lt;/code&gt; &lt;a href="https://12factor.net/config"&gt;environment variable&lt;/a&gt; (a common &lt;a href="https://martinfowler.com/articles/feature-toggles.html"&gt;feature flag&lt;/a&gt; pattern) to control which type of queues to create:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; kombu &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Queue, Exchange
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; django.conf &lt;span style="color:#f92672"&gt;import&lt;/span&gt; settings
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;build_queue&lt;/span&gt;(queue_name: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; Queue:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; queue_type &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;quorum&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; settings&lt;span style="color:#f92672"&gt;.&lt;/span&gt;USE_QUORUM_QUEUES &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;classic&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; exchange_type &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;topic&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; settings&lt;span style="color:#f92672"&gt;.&lt;/span&gt;USE_QUORUM_QUEUES &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;direct&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Queue(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#f92672"&gt;=&lt;/span&gt;queue_name,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; exchange&lt;span style="color:#f92672"&gt;=&lt;/span&gt;Exchange(queue_type, type&lt;span style="color:#f92672"&gt;=&lt;/span&gt;exchange_type),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; queue_arguments&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;x-queue-type&amp;#34;&lt;/span&gt;: queue_type}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;task_queues &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [build_queue(&lt;span style="color:#e6db74"&gt;&amp;#34;first_queue&amp;#34;&lt;/span&gt;), &lt;span style="color:#f92672"&gt;...&lt;/span&gt;, build_queue(&lt;span style="color:#e6db74"&gt;&amp;#34;last_queue&amp;#34;&lt;/span&gt;)]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When we deployed this change, Kubernetes did its usual &lt;a href="https://kubernetes.io/docs/tutorials/kubernetes-basics/update/update-intro/"&gt;rolling update&lt;/a&gt;, and pods gradually switched from &lt;code&gt;chost&lt;/code&gt; to &lt;code&gt;qhost&lt;/code&gt;. This meant we temporarily had some pods on the old vhost and some on the new one, which was totally fine. Any stragglers would get caught in Phase 3.&lt;/p&gt;
&lt;h3 id="phase-3-message-transfer-with-eta-transformation"&gt;Phase 3: Message Transfer with ETA Transformation&lt;/h3&gt;
&lt;p&gt;Here&amp;rsquo;s where things got interesting. We needed to transfer potentially millions of messages from &lt;code&gt;chost&lt;/code&gt; to &lt;code&gt;qhost&lt;/code&gt; without losing any data or causing downtime.&lt;/p&gt;
&lt;p&gt;At first glance, this sounds like a perfect job for RabbitMQ&amp;rsquo;s &lt;a href="https://www.rabbitmq.com/docs/shovel"&gt;Shovel plugin&lt;/a&gt;, right? Just copy messages from one vhost to another. Unfortunately, a shovel copies messages as-is, including the &lt;code&gt;eta&lt;/code&gt; header. That&amp;rsquo;s exactly what we&amp;rsquo;re trying to avoid—those headers would cause workers to block, bringing us right back to square one.&lt;/p&gt;
&lt;p&gt;Instead, we had to transform messages during transfer. The idea was to replicate what &lt;a href="https://github.com/celery/celery/blob/0527296acb1f1790788301d4395ba6d5ce2a9704/celery/app/base.py#L854-L876"&gt;Celery does internally&lt;/a&gt; when Native Delayed Delivery is enabled: extract the ETA header, calculate the appropriate delay-based routing key, and route to the right exchange. If you want to understand how this works under the hood, &lt;a href="https://docs.particular.net/transports/rabbitmq/delayed-delivery"&gt;this guide&lt;/a&gt; explains the mechanics well.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the core logic (simplified for clarity):&lt;/p&gt;
&lt;details class="collapsible-code"&gt;
&lt;summary&gt;Show/hide code&lt;/summary&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; pika &lt;span style="color:#75715e"&gt;# RabbitMQ Python client: &amp;lt;https://pika.readthedocs.io/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; kombu.transport &lt;span style="color:#f92672"&gt;import&lt;/span&gt; native_delayed_delivery &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; kombu_utils
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;get_routing_details&lt;/span&gt;(method, properties, queue_name):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target_exchange_name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; method&lt;span style="color:#f92672"&gt;.&lt;/span&gt;exchange &lt;span style="color:#f92672"&gt;or&lt;/span&gt; queue_name
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target_routing_key &lt;span style="color:#f92672"&gt;=&lt;/span&gt; method&lt;span style="color:#f92672"&gt;.&lt;/span&gt;routing_key &lt;span style="color:#f92672"&gt;or&lt;/span&gt; queue_name
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; eta_str &lt;span style="color:#f92672"&gt;=&lt;/span&gt; str(properties&lt;span style="color:#f92672"&gt;.&lt;/span&gt;headers&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pop(&lt;span style="color:#e6db74"&gt;&amp;#34;eta&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; countdown_in_seconds &lt;span style="color:#f92672"&gt;=&lt;/span&gt; compute_countdown(eta_str)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; countdown_in_seconds &lt;span style="color:#f92672"&gt;and&lt;/span&gt; countdown_in_seconds &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target_routing_key &lt;span style="color:#f92672"&gt;=&lt;/span&gt; kombu_utils&lt;span style="color:#f92672"&gt;.&lt;/span&gt;calculate_routing_key(int(countdown_in_seconds), target_routing_key)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target_exchange_name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;celery_delayed_27&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; target_routing_key, target_exchange_name
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;chost_connection_string &lt;span style="color:#f92672"&gt;=&lt;/span&gt; read_from_env(&lt;span style="color:#e6db74"&gt;&amp;#34;CHOST_CONNECTION_STRING&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;qhost_connection_string &lt;span style="color:#f92672"&gt;=&lt;/span&gt; read_from_env(&lt;span style="color:#e6db74"&gt;&amp;#34;QHOST_CONNECTION_STRING&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;source_channel &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pika&lt;span style="color:#f92672"&gt;.&lt;/span&gt;BlockingConnection(pika&lt;span style="color:#f92672"&gt;.&lt;/span&gt;URLParameters(chost_connection_string))&lt;span style="color:#f92672"&gt;.&lt;/span&gt;channel()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dest_channel &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pika&lt;span style="color:#f92672"&gt;.&lt;/span&gt;BlockingConnection(pika&lt;span style="color:#f92672"&gt;.&lt;/span&gt;URLParameters(&lt;span style="color:#e6db74"&gt;&amp;#34;amqps://user:pwd@host:port/qhost&amp;#34;&lt;/span&gt;))&lt;span style="color:#f92672"&gt;.&lt;/span&gt;channel()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; method, properties, body &lt;span style="color:#f92672"&gt;in&lt;/span&gt; source_channel&lt;span style="color:#f92672"&gt;.&lt;/span&gt;consume(&lt;span style="color:#e6db74"&gt;&amp;#34;target_queue&amp;#34;&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; routing_key, exchange &lt;span style="color:#f92672"&gt;=&lt;/span&gt; get_routing_details(method, properties, &lt;span style="color:#e6db74"&gt;&amp;#34;target_queue&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dest_channel&lt;span style="color:#f92672"&gt;.&lt;/span&gt;basic_publish(exchange&lt;span style="color:#f92672"&gt;=&lt;/span&gt;exchange, routing_key&lt;span style="color:#f92672"&gt;=&lt;/span&gt;routing_key,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; body&lt;span style="color:#f92672"&gt;=&lt;/span&gt;body, properties&lt;span style="color:#f92672"&gt;=&lt;/span&gt;properties)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; source_conn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;channel()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;basic_ack(delivery_tag&lt;span style="color:#f92672"&gt;=&lt;/span&gt;method&lt;span style="color:#f92672"&gt;.&lt;/span&gt;delivery_tag)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;except&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Exception&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; source_conn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;channel()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;basic_nack(delivery_tag&lt;span style="color:#f92672"&gt;=&lt;/span&gt;method&lt;span style="color:#f92672"&gt;.&lt;/span&gt;delivery_tag, requeue&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;h4 id="some-notes-on-the-shoveling-part"&gt;Some notes on the shoveling part&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;The above is pseudocode to illustrate the concept. Our production version uses &lt;a href="https://aio-pika.readthedocs.io/"&gt;&lt;code&gt;aio_pika&lt;/code&gt;&lt;/a&gt; for &lt;a href="https://docs.python.org/3/library/asyncio.html"&gt;async I/O&lt;/a&gt; (to avoid blocking), &lt;a href="https://docs.python.org/3/library/multiprocessing.html"&gt;multiprocessing&lt;/a&gt; to handle high throughput, message backups for disaster recovery, and extensive logging to track everything.&lt;/li&gt;
&lt;li&gt;The script was deployed to run as a daemon, and would only work if &lt;code&gt;USE_QUORUM_QUEUES&lt;/code&gt; was set to &lt;code&gt;True&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;More mechanics were in place to determine if there were any messages to transfer in the first place, do transfers in batches, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="phase-4--5-cleanup-and-upgrade"&gt;Phase 4 &amp;amp; 5: Cleanup and Upgrade&lt;/h3&gt;
&lt;p&gt;Once all messages were safely transferred to &lt;code&gt;qhost&lt;/code&gt;, our platform team took over for the final steps. They deleted the &lt;code&gt;chost&lt;/code&gt; virtual host (which removed all the classic queues that are incompatible with v4), and then performed a &lt;a href="https://www.rabbitmq.com/docs/rolling-upgrade"&gt;rolling upgrade&lt;/a&gt; to RabbitMQ v4.2.2.&lt;/p&gt;
&lt;p&gt;And that was it! We successfully migrated from RabbitMQ v3 to v4 with zero downtime, while preserving ETA task behavior and handling 8M messages/day across all our environments.&lt;/p&gt;</content:encoded></item><item><title>About</title><link>https://shtlrs.com/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://shtlrs.com/about/</guid><content:encoded>&lt;p&gt;My name is &lt;strong&gt;Amrou Bellalouna&lt;/strong&gt;, and I&amp;rsquo;m a Software Engineer at &lt;a href="https://kraken.tech"&gt;Kraken Technologies&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="community-work"&gt;Community Work&lt;/h2&gt;
&lt;p&gt;Beyond my professional work, I&amp;rsquo;m deeply involved in the &lt;a href="https://pythondiscord.com"&gt;Python Discord&lt;/a&gt; community:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.pythondiscord.com/pages/acknowledgements/#core-developers"&gt;&lt;strong&gt;Core Developer&lt;/strong&gt;&lt;/a&gt; - contributing to the development and maintenance of the platform that serves the largest Python community on Discord&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pythondiscord.com/pages/acknowledgements/#devops-team"&gt;&lt;strong&gt;DevOps Team Member&lt;/strong&gt;&lt;/a&gt; - helping maintain infrastructure and ensure smooth operations for thousands of community members&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Python Discord is a community where developers of all skill levels come together to learn, share knowledge, and collaborate.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in Python, I highly encourage you to &lt;a href="https://discord.com/invite/python"&gt;join us&lt;/a&gt;!&lt;/p&gt;
&lt;h2 id="what-i-write-about"&gt;What I Write About&lt;/h2&gt;
&lt;p&gt;I will try to use this space to share some of the technical lessons I ran into on my path, or some thoughts, whenever
I have any.&lt;/p&gt;
&lt;h2 id="get-in-touch"&gt;Get In Touch&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m always happy to connect further beyond this blog. Feel free to reach out if you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Have questions about something I&amp;rsquo;ve written&lt;/li&gt;
&lt;li&gt;Want to discuss Python, DevOps, or software architecture&lt;/li&gt;
&lt;li&gt;Are interested in contributing to open source&lt;/li&gt;
&lt;li&gt;Just want to chat about technology!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can find me on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/shtlrs"&gt;@shtlrs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;: &lt;a href="https://linkedin.com/in/amrbellalouna"&gt;amrou.bellalouna&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python Discord&lt;/strong&gt;: Look for me in the community!&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>