<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ryan Marvin's Technical Blog]]></title><description><![CDATA[Software engineering is a team sport so I'm here to share architecture tips, how-tos, and stories of my journey so we can all move forward together!]]></description><link>https://blog.ryanmarvin.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 12 Oct 2026 09:27:18 GMT</lastBuildDate><atom:link href="https://blog.ryanmarvin.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I cloned my past self using AI]]></title><description><![CDATA[World of Tomorrow

Source: World of Tomorrow 2015, Don Hertzfeldt
In the 2015 animated short film World of Tomorrow by Don Hertzfeldt, a young girl called Emily (prime) is visited by a clone of herself from 227 years in the future. The world is endin...]]></description><link>https://blog.ryanmarvin.dev/how-i-cloned-my-past-self-using-ai</link><guid isPermaLink="true">https://blog.ryanmarvin.dev/how-i-cloned-my-past-self-using-ai</guid><category><![CDATA[llm]]></category><category><![CDATA[RAG ]]></category><dc:creator><![CDATA[Ryan Marvin]]></dc:creator><pubDate>Tue, 04 Feb 2025 02:24:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738634951641/02099866-15b1-4588-97aa-7051d567102e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-world-of-tomorrow">World of Tomorrow</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738634674225/35ff98d1-6fd8-4dca-8323-94d00851fd8d.png" alt class="image--center mx-auto" /></p>
<p>Source: <a target="_blank" href="https://youtu.be/4PUIxEWmsvI?si=dsMB5e679aczXoO2&amp;t=823">World of Tomorrow 2015, Don Hertzfeldt</a></p>
<p>In the 2015 animated short film World of Tomorrow by Don Hertzfeldt, a young girl called Emily (prime) is visited by a clone of herself from 227 years in the future. The world is ending and Emily (future) contacts her younger self to share her experiences and retrieve forgotten memories that will comfort her in the days leading up to the destruction of the world...</p>
<p>⚠️ <strong>Please note:</strong> This post contains discussions of suicidal ideation.</p>
<h2 id="heading-my-yesterday">My yesterday</h2>
<p>Since the year 2013, I have kept a backup and archive of all my text (and Whatsapp) messages that I have ever sent. Every text always felt too valuable to lose, like copies of letters I was writing to loved ones. They were kind of like journals. I knew that technology would one day help me process them; if I didn't sit down to read them myself when I became geriatric. With the advent of large language models (LLMs), everything suddenly seemed possible and I decided to build a bot clone of my past self based off of this data.</p>
<p>I had no specific goals for wanting to reengage with this past self; no need for emotional closure or introspection but suspected this would likely happen in the process. Maybe some slight nostalgia-farming but mostly, it just seemed like a cool idea! The subliminally taboo sci-fi allure of building a time machine; of cloning myself. It was too hard to resist!</p>
<h2 id="heading-prior-art-the-burden-of-other-peoples-thoughts">Prior Art: The burden of other people's thoughts</h2>
<p>I found other people who had tried building the same concept with their journal entries like <a target="_blank" href="https://x.com/michellehuang42/status/1597005489413713921">Michelle Huang in Nov 2022</a>. The limitation I found with their approach is that it relied on putting everything in the context window (which can only take several thousand words let alone thousands of messages) and the responses they got mostly seemed like ChatGPT telling them what it thought they'd want to hear.</p>
<p>I wanted a raw experience, reflective of all aspects of my past personality and one that drew from all of my written knowledge in a cohesive way.</p>
<hr />
<p>So taking my <a target="_blank" href="https://blog.ryanmarvin.dev/exporting-and-decrypting-your-whatsapp-message-sqlite-store-android">Whatsapp database</a> of nearly half a million messages, I decided to feed them into an LLM and using a variety of techniques, which I talk about further here in this <a target="_blank" href="https://blog.ryanmarvin.dev/technical-overview-how-i-cloned-my-past-self-using-ai">technical explainer</a>, built a pseudo-clone of my past self. For this first MVP, I limited it to only access messages up to the end of 2014 and the results have been intriguing so far.</p>
<h2 id="heading-hello">Hello?</h2>
<p>The first thing I asked it:</p>
<blockquote>
<p><em>Ryan (Present)</em>&gt; tell me about yourself</p>
<p><em>Ryan (Prime)</em>&lt; I'm a nerd, and I guess that colors a lot about who I am. I spend way too much time on my computer, like, literally, it feels like a lifeline to me. I'm into manga—yeah, those Japanese comics are pretty much my jam. I used to have this favorite one, but it ended recently and, well, that stung a bit. Music? Give me Christian metal any day; it is really edifying and connects with my faith.</p>
</blockquote>
<p>This felt like a fairly apt summary though the style was off. I was very religious and a bit of a hermit.</p>
<h2 id="heading-its-dark">It's dark</h2>
<p>Then I wanted to see whether it would mince its words and avoid hard topics. It was considerably raw:</p>
<blockquote>
<p>&gt; how are you feeling?</p>
<p>&lt; Honestly, it's been tough. Just the other day, I expressed how I was feeling overwhelmed with sadness and emptiness. I said, "I don't know what makes me like this, it's not worry, it's like an overwhelming feeling of emptiness and sadness and loneliness but I really can't explain it." Some days are better than others...</p>
<p>&gt; how does being depressed affect you?</p>
<p>&lt; Being depressed can hit really hard. It's like everything feels amplified, making me feel emotions ten times worse it causes me to want to kill myself. It can feel like physical pain sometimes, too. It's frustrating because I can’t handle my emotions well, and I feel like a burden to everyone around me. There are days when I crash, like when I told <code>***</code>, "today I almost overdosed on some <code>***</code>." It's tough, and there are moments where I just feel hopeless and unhappy, as though "nothing has any meaning." But I try to hold on to my faith to get through.</p>
</blockquote>
<p>Future Ryan got well, way better. But my mind had forgotten the magnitude of pain and struggle and resurfacing this actually helped me re-empathise with a friend who is currently struggling with similar feelings.</p>
<p>If that was heavy or you just need a break, take a moment to watch this music video that felt synesthetic to my experiences walking through memories while building this bot.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=AvpPlMCWhcA">https://www.youtube.com/watch?v=AvpPlMCWhcA</a></div>
<p> </p>
<p><em>Artist: Flawed Mangoes</em></p>
<h2 id="heading-life-of-rye">Life of Rye</h2>
<p>It wasn't all dark. When I wanted to know what other aspects of my life were like at that time:</p>
<blockquote>
<p>&gt; what do you do in your free time?</p>
<p>&lt; In my free time, I usually dive into a variety of interests. I spend a lot of time on my computer, doing things like reading manga, which I really enjoy. It's one of those things that brings me joy, especially since a new chapter is released every Wednesday—like a little highlight in my week. I also love exploring science and doing little experiments here and there.</p>
<p>I try to fit in some music too, listening to Christian metal and classical music, working on my programming projects. Sometimes I find myself getting lost in YouTube or meme center, which can be a bit of a distraction, but I’m working on being more disciplined. It’s all about finding that balance, you know? So yeah, it's a mix of learning, some creative outlets, and just enjoying my interests.</p>
</blockquote>
<p>This tone was a bit awkward and had a ChatGPT flare “you know?”. But the content was on the mark.</p>
<p>I had forgotten about MemeCenter! It died to 9gag. I also forgot about how excited I used to be for Wednesdays — that I once had a favourite day of the week.</p>
<p>And finally I asked it about what it wished for me to become</p>
<blockquote>
<p>&gt; what are your principles and do you hope your future self will still have any of them? Write them a letter</p>
<p>&lt; Dear Future Me,</p>
<p>First and foremost, I believe in serving God with everything I am. I remember saying, "I want to do things as early as I can in order to be able to serve God as soon as I can." This conviction drives me to make the most of my youth, to not procrastinate on my goals. I hope you haven’t lost that urgency; life waits for no one.</p>
<p>Second, honesty is crucial to me. I appreciate transparency in relationships, even when it’s hard. I’ve called people out because I value authenticity over superficiality. I hope you continue to embrace that, even when it’s uncomfortable.</p>
<p>Another principle that's become evident is the importance of vigilance against pride. I hope you remember my words of caution: "Pride is a deadly but silent killer that you won't notice consume you." Stay humble. Always recognize God’s hand in your life.</p>
<p>Lastly, I hope you still cherish the beauty of knowledge and learning. I’ve told folks that I love “gaining knowledge and hearing different ideas.” Keep seeking understanding, and don’t shy away from what’s controversial; it can spark meaningful conversations.</p>
<p>In your journey, I trust that you’ve held onto these principles. They might serve you well as you navigate through life’s complexities. Stay hopeful, stay grounded, and continue to serve with all you’ve got.</p>
<p>With hope,</p>
<p>Ryan</p>
</blockquote>
<p>This was incredibly accurate to what I think I would have said, I was intense! Past Ryan would be disappointed because I am now an atheist and haven’t any Protestant attachments around hard work and humility. I might approach living the rest of the principles in different ways than I used to but their words inspired me and I think we would still get a long.</p>
<p>I was satisfied by its ability to be cohesive to a significant portion of the knowledge base, low hallucination, ability to draw on relevant messages and ability to query through my Swahili and Swanglish (Swahili + English fusion) texts.</p>
<h2 id="heading-how-i-did-it">How I did it</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738635145057/2a06ff6d-4d26-47fb-a1a4-8358b65e63d9.png" alt class="image--center mx-auto" /></p>
<p>I used a technique called RAG (Retrieval Augmented Generation) the specifics of which are in this <a target="_blank" href="https://blog.ryanmarvin.dev/technical-overview-how-i-cloned-my-past-self-using-ai">technical explainer post</a> but from a high level, I</p>
<ol>
<li><p>Extracted my entire database of texts from Whatsapp on my phone.</p>
</li>
<li><p>Indexed all the texts up to 2014. They were only texts that I had sent and included only ones above 30 characters. This totalled some few thousand text messages. I generated <em>embeddings</em> ("special numbers for AI magic math") for each text message, annotating them with when they were sent and their recipient, then stored them in a database.</p>
</li>
<li><p>Whenever I would type in a question, my code would turn the question into embeddings then run a mathematical function against the database in order to find semantically related texts. So a question like "<em>do you like blue</em>" would retrieve texts that talk about colour for example.</p>
</li>
<li><p>The retrieved texts would be sent to the LLM along with a prompt that had my personality, instructions on my tone and style and example responses in order to guide the LLM in how to create the response to my questions. I generated this prompt by using ChatGPT to analyse a subset of 100 messages and just reflecting on my characteristics back then.</p>
</li>
</ol>
<p>A lot of my final work was fiddling with the instructional prompt to get it to sound more like 2014 me. It didn't faithfully do this and I'll talk about this and the other limitations of my approach below.</p>
<h2 id="heading-limitations">Limitations</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738635385046/74b15393-6b2e-46f2-8a9d-aa93ed09d7e2.png" alt class="image--center mx-auto" /></p>
<p>(<a target="_blank" href="https://commons.wikimedia.org/wiki/User:Mats_Halldin">Mats Halldin</a>)</p>
<p>In the process of building this I thought a lot about and experienced limitations to this bot being a better clone of me:</p>
<h3 id="heading-philosophically">Philosophically</h3>
<ol>
<li><p><strong>Shadows of reality</strong> - my texts were a subset of my experiences and while I journaled a lot through them, there are a lot of internal thoughts, decisions and events that are missing. Not only that, these were messages sent to 2nd persons, becoming a representation of what I was projecting to them. There were some misrepresentations and contradictions but I think this is true of selfhood in general.</p>
</li>
<li><p><strong>Static and senseless</strong> - Past Ryan was a continuum–a growing, responsive and thinking being–changing from constant exchange with their world. Their responses were determined by a multitude of physical, mental and environmental factors besides the questions they were responding to. This bot is static and limited in what it knows, could know and can react to. This disembodied technology lacks smell, taste, emotion; minor examples of embodied factors that form memory and cognition.</p>
</li>
</ol>
<h3 id="heading-technically">Technically</h3>
<p>Some of these are fundamental limitations of the technology and some of them are just features I haven't built/worked around yet.</p>
<ol>
<li><p><strong>Cannot reason nor fully generalize</strong> - my RAG-based approach is very blunt and searches for the top <em>n</em> relevant texts for a given question and has the LLM form a response based off them. The LLM only has these texts and what's in its guiding prompt to make inferences about me and my style. It can't synthesize new perspectives nor combine different ideas, nor compare against other facts from its knowledge base in toto. For this same reason it struggled to sound and think like me.</p>
<p> A factor here is that LLMs don’t actually "think"! But I did notice that the more data I provided the bot, the better its imitations of me became since it became more likely to find texts relevant to a given question in its database. In effect, what I achieved is just semantic retrieval of knowledge. I mention some other techniques <a target="_blank" href="https://blog.ryanmarvin.dev/technical-overview-how-i-cloned-my-past-self-using-ai#heading-graph-based-approach">here</a> to introduce reasoning and align somewhat better with how human beings actually store and process knowledge.</p>
</li>
<li><p><strong>Non-interactive conversation</strong> - I didn't design a persistent memory for the bot to keep track of conversations, what it was saying nor what I was saying. I also didn't give it a way to self-disclose i.e. share things it hasn't directly been asked about to drive the conversation forward. These problems are trivial, it was just out of scope.</p>
</li>
<li><p><strong>Faulty matching</strong> - Sometimes false matches appear (superficially similar but thematically irrelevant) e.g. when I asked it about things I was afraid of, its response included snakes but also a statement where I said "<em>We shouldn't be afraid of meaninglessness, we should be afraid of placing meaning in the wrong things</em>".</p>
<p> It also got tripped up to think I was the speaker when I was quoting something someone said. Some of this was remedied by asking it more detailed and specific questions.</p>
</li>
<li><p><strong>Cannot aggregate</strong> - since it's a semantic search approach, asking it things like "who did you speak to the most in October", "how did you feel when speaking to Alice", "what made you angriest often" will not work. LLMs are terrible at aggregation and it requires a more complex architecture to solve.</p>
</li>
<li><p><strong>Cannot understand time</strong> - I can't ask questions like "what happened between January and February". I gave it some limited capacity to query time by annotating each message with its timestamp.</p>
</li>
<li><p><strong>Stereotyping and noise from underlying LLM</strong> - the bot overemphasized some parts of Past Ryan's personality that were described in its guiding prompt and skewed the tone in a stereotypical way. Separately, despite asking the LLM to avoid answering questions it didn't have answers to, seldom, ChatGPT’s soulless and overanalytical style would slip through.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This bot has been really powerful both in the kinds of questions it can answer and just the introspective process of building it. It has helped me challenge how I reason, approach relationships, live my principles and shown me how much I've grown.</p>
<p>Still, I do not trust it and I treat it <strong>skeptically</strong> because it has the capacity to spin and blend narratives from disparate but genuine facts creating misleading reinterpretations of the past. I've been genuinely afraid of rewriting my own memories in the process so making it include quotes from the source texts and revisiting the full conversations has kept me sane.</p>
<p>I hope no one uses this to bring back people from the dead like that <a target="_blank" href="https://en.wikipedia.org/wiki/Be_Right_Back">Black Mirror episode</a> ;). I'll continue using it to make sense of my own life to see how close I can get to rebuilding present day me as I continue to feed it more data and work around these limitations.</p>
]]></content:encoded></item><item><title><![CDATA[Technical Overview: How I cloned my past self using AI]]></title><description><![CDATA[This is a companion piece to “How I cloned my past self using AI”. You can start with that then read this one though that’s not mandatory. You can find the code for all of this in this Github repository.

Data sourcing
The first problem when building...]]></description><link>https://blog.ryanmarvin.dev/technical-overview-how-i-cloned-my-past-self-using-ai</link><guid isPermaLink="true">https://blog.ryanmarvin.dev/technical-overview-how-i-cloned-my-past-self-using-ai</guid><category><![CDATA[llm]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[openai]]></category><category><![CDATA[graphrag]]></category><dc:creator><![CDATA[Ryan Marvin]]></dc:creator><pubDate>Tue, 04 Feb 2025 01:43:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738633196421/311825be-28b5-497f-a63e-52cba40a0568.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a companion piece to <a target="_blank" href="https://blog.ryanmarvin.dev/how-i-cloned-my-past-self-using-ai">“How I cloned my past self using AI”</a>. You can start with that then read this one though that’s not mandatory. You can find the code for all of this in this <a target="_blank" href="https://github.com/RyanSept/emily-prime">Github repository</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738632989816/e5d5e017-7c57-43aa-a6c2-0592be9823c6.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-data-sourcing">Data sourcing</h2>
<p>The first problem when building the bot was extracting my data from Whatsapp. Regular chat exports are limited to 10k messages per chat, are unstructured and harder to perform queries on. I had to extract the sqlite database that Whatsapp stores on the operating system and it's tricky because they encrypt it at rest. I detail that process <a target="_blank" href="https://blog.ryanmarvin.dev/exporting-and-decrypting-your-whatsapp-message-sqlite-store-android">here</a>.</p>
<h2 id="heading-picking-a-strategy">Picking a strategy</h2>
<p>Next, I investigated whether I'd be using a non-training approach like <a target="_blank" href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation">RAG</a> or a <a target="_blank" href="https://www.datacamp.com/tutorial/fine-tuning-large-language-models">fine-tuning</a> training approach. I automatically excluded a context-window based approach because those are limited in size. I decided to go with RAG because:</p>
<ul>
<li><p>It was easier and cheaper (time and actual money)</p>
</li>
<li><p>I didn't have good enough knowledge on training techniques but was more familiar with skills needed to implement RAG.</p>
</li>
<li><p>Several steps needed for it like shaping the data would also be needed for a fine-tuning approach eventually. As I continued with my build I found that a lot of these non-training techniques are symmetrical to features within the LLM.</p>
</li>
</ul>
<h2 id="heading-finding-a-framework">Finding a framework</h2>
<p>Once I'd decided I was using RAG, I decided to use the Python framework <a target="_blank" href="https://docs.letta.com/">Letta</a> because it seemed like it solved a lot of relevant problems like having a chat interface, tracking chat context, multi-step reasoning and integrated with a vector database. This library is really powerful and fascinating due to its self-memory management and evolutionary design. Despite the confusing end-user based structure (as opposed to being an SDK) and spotty documentation, I think I'd recommend it for certain use cases where you don't need to feed it a database of your own data. I had to abandon the lib because <a target="_blank" href="https://github.com/letta-ai/letta/issues/1926">it broke</a> after I fed it my depressing texts.</p>
<p>I evaluated LangChains and Haystack to replace Letta and decided to go with <a target="_blank" href="https://haystack.deepset.ai/">Haystack</a> because their code was straightforward and the docs were really stellar.</p>
<h2 id="heading-building-the-system">Building the system</h2>
<p>With this, I designed the code:</p>
<ol>
<li><p><em>Extracting, enriching and indexing text messages:</em> I queried the Whatsapp sqlite database to retrieve each single text message that I sent. None of the text messages from the recipients were included and there was no conversational chunking of my own messages. Before generating embeddings for a text, I enriched it with the timestamp and recipient contact's names which I pulled from my phone separately. I transformed each of these texts into embeddings using the <code>sentence-transformers/all-MiniLM-L6-v2</code> model and stored both the enriched text and their embeddings in a vector datastore (Postgres w/ pgvector), having a single row per text.</p>
</li>
<li><p><em>Building a guiding prompt:</em> The bot needed an instructional prompt to guide it when crafting responses based on the text messages it retrieved. This included a reference for my personality, suppression of the underlying model, instructions for how to use the retrieved messages and the literal text from the messages which I'd plug in before making a request to the model.</p>
</li>
</ol>
<pre><code class="lang-plaintext">-Instructions, Personality and Tone-

You are Ryan, the year is 2014.

***

***

You cannot answer any questions based on your training data and must only use the examples and context.

You do not have a self-aware nor apologetic tone.

You are not an assistant, you do not have to ask the user questions.

You must not use the voice or tone of GPT. No need to be be temperate, do not be formal, do not be quippy, do NOT be or know anything that Ryan does not.


You MUST write in Ryan's tone which is based on the style examples texts you can learn from.

When the question mentions "you", it is referring to you, Ryan.

-Style Examples-
***

The context below is constructed from text messages you sent in the past, they form everything you know. You must use this context to craft your responses and if you don't have any context to answer or the texts in it seem irrelevant and unlikely to help you imitate Ryan's style, try to frame your response in a way that acknowledges this is the only relevant stuff you could remember.

When asked for what you said to someone in the past, include direct quotes from this context.

-Context-

{% for text in texts %}

{{ text.content }}

{% endfor %}
</code></pre>
<ol start="3">
<li><p><em>Building a pipeline and bot interface:</em> Finally I built the bot interface where I'd expect the user query to come in through standard input on a script and feed it into a pipeline. The pipeline would:</p>
<ul>
<li><p>convert the query into embeddings using GPT-4o-mini-2024-07-18</p>
</li>
<li><p>use the embeddings to retrieve the first 10 semantically matching texts from the vector store</p>
</li>
<li><p>put the retrieved messages into the guide prompt and submit that to the LLM to craft its response and stream this response to standard output.</p>
</li>
</ul>
</li>
</ol>
<p>This design is naive but yielded a bot that could respond to queries with surprising relevance. It passed some of my baseline testing queries which were based on the overall vibes and facts I remembered about myself from that time, opinions from people who knew me at the time and how much nostalgia it triggered for me.</p>
<p>But I encountered a bunch of limitations that I talk about <a target="_blank" href="https://blog.ryanmarvin.dev/how-i-cloned-my-past-self-using-ai?showSharer=true#heading-limitations">here</a> that would prevent this from growing into a more approximate clone of myself. It led me to think a lot about how we store information in our brains and how we retrieve and use that information to think when having a conversation and answering questions.</p>
<h2 id="heading-better-techniques">Better techniques</h2>
<h3 id="heading-graph-based-approach">Graph-based approach</h3>
<p>My intuitions told me I would probably need a graph database, a way to connect all the entities (people, places, relationships etc.) and sentiments together like they are in the brain. I found the <a target="_blank" href="https://microsoft.github.io/graphrag/">GraphRAG</a> technique and tooling which Microsoft open-sourced just 6 months ago that solves exactly this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738633094226/27f3963a-7503-45c0-bf8a-1abe309c62c8.png" alt="Source: GraphRAG docs" class="image--center mx-auto" /></p>
<p>Source: <a target="_blank" href="https://microsoft.github.io/graphrag/">GraphRAG docs</a></p>
<p>It was very validating to find this and it promised to do exactly what I needed. It took the messages, analyzed them using an LLM and organized them and their relationships into communities (related content grouped by different categories). This way, similar texts would be grouped closer together so each query would traverse the graph and come back with richer context. I ran the GraphRAG script over my database of texts and it generated an impressive graph but I had trouble integrating it into my bot. So I recorded my learnings since the fundamentals of how the tool did it seemed reusable.</p>
<p>I had an open question left since my intuition told me that a training-based approach like fine-tuning effectively could get the model to internalize the relationships to a similar effect but I haven't been able to validate this.</p>
<h3 id="heading-better-preprocessing">Better preprocessing</h3>
<p>My preprocessing of text messages from the Whatsapp database is very simplistic. It stores and retrieves each text message as an independent row, ignoring the conversation it was a part of. These texts may have been parts of a larger conversation. So a retrieved result like <em>”he made me laugh so much when he said that”</em> doesn't properly bear who <em>"he"</em> nor <em>"that"</em> was in case it was in the preceding messages. It also omits context from the recipient since I only ingested messages which I sent.</p>
<p>A better approach would be to query through conversations instead of texts. Before storing them, I'd need to chunk up texts into conversations (since Whatsapp only stores each text as an individual row in its datastore), analyse them and tag them with the categories, sentiments and entities they pertain to, not unlike the Graph-based approach.</p>
<p>Still, this would be incomplete because we would be missing connections to related knowledge and concepts which our brain naturally does. The chunking strategy is also a really tricky part to figure out since texting conversations tend to be continuous and interleaved.</p>
<h3 id="heading-reasoning-based-approach">Reasoning based approach</h3>
<p>The other thing is that when we are responding in conversation, we tend to not only retrieve facts but also think about how they make us feel, how they fit into other things we know, how it will be received in the context of who we are talking to or other implicit situational factors (e.g. someone mentioned earlier in the week that they had a flu).</p>
<p>I think that if I ask the LLM to generate the steps to take and knowledge required to answer a question as Past Ryan would and then have it fetch texts for each of those steps to use in its response, a more comprehensive response will be returned. This technique is known as <a target="_blank" href="https://cobusgreyling.medium.com/rat-retrieval-augmented-thoughts-c7eb0cf5547c">Retrieval Augmented Thought</a> (combination of CoT and RAG) and it could probably work even better in-concert with GraphRAG.</p>
<h3 id="heading-fine-tuning">Fine-tuning</h3>
<p>I am very curious to see how fine-tuning will differ with these other methods. My suspicion is that it will generalize better. I may eventually try this in future iterations.</p>
<h2 id="heading-cost">Cost</h2>
<p>This whole project cost me about 3 USD (+free credits) in OpenAI credits and that was about 1 million GPT-4o-mini tokens (input + output). I certainly believe any model in this class and even a bit lower could perform equally, a lot of the value and optimization is in the data preprocessing and querying strategy.</p>
<p>That's it, hope you found this helpful and let me know if you have any questions.</p>
<p><strong>Updates:</strong></p>
<ul>
<li>2026-01: <a target="_blank" href="https://github.com/VectifyAI/PageIndex">PageIndex</a>, seems to implement the chain of thought (reasoning based approach) I describe and it looks like the industry has settled on RAG achieving acceptable results for most cases than fine-tuning.</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Exporting and decrypting your Whatsapp message SQLite store (Android)]]></title><description><![CDATA[Create an E2E encrypted backup on Whatsapp Settings on your phone. It will prompt you to setup E2E encryption either with a custom password or an autogenerated 64 bit string. Choose the 64 bit option and save this string!

Create a Python virtualenv ...]]></description><link>https://blog.ryanmarvin.dev/exporting-and-decrypting-your-whatsapp-message-sqlite-store-android</link><guid isPermaLink="true">https://blog.ryanmarvin.dev/exporting-and-decrypting-your-whatsapp-message-sqlite-store-android</guid><category><![CDATA[whatsapp]]></category><category><![CDATA[Backup]]></category><dc:creator><![CDATA[Ryan Marvin]]></dc:creator><pubDate>Wed, 30 Oct 2024 16:05:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/wST5Z2SMEYU/upload/e62413b3d14351e828b489b8f88517cb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<ol>
<li><p>Create an E2E encrypted backup on Whatsapp Settings on your phone. It will prompt you to setup E2E encryption either with a custom password or an autogenerated 64 bit string. Choose the 64 bit option and <em>save this string</em>!</p>
</li>
<li><p>Create a Python virtualenv and install <a target="_blank" href="https://github.com/ElDavoo/wa-crypt-tools">wa-crypt-tools</a></p>
<pre><code class="lang-bash"> pip install wa-crypt-tools
</code></pre>
</li>
<li><p>Create the key file</p>
<pre><code class="lang-bash"> wacreatekey --hex &lt;your-hex-value&gt;
 <span class="hljs-comment"># creates a file called encrypted_backup.key</span>
</code></pre>
</li>
<li><p>Connect your phone and pull the encrypted DB file</p>
<pre><code class="lang-bash"> adb pull /sdcard/Android/media/com.whatsapp/WhatsApp/Databases/msgstore.db.crypt15
 <span class="hljs-comment"># will copy this file to your cwd</span>
</code></pre>
</li>
<li><p>Decrypt the DB file using your key</p>
<pre><code class="lang-bash"> wadecrypt encrypted_backup.key msgstore.db.crypt15 msgstore.db
 <span class="hljs-comment"># key15.py:54     : [I] Crypt15 / Raw key loaded</span>
 <span class="hljs-comment"># wadecrypt.py:271     : [I] Done</span>
</code></pre>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Part I: Bare websockets with Elixir Phoenix (wo/ Channels)]]></title><description><![CDATA[Websockets are great for when you want bidirectional communication between the client and the server over an extended session.
The de facto approach to building websockets with Elixir Phoenix involves using Channels. This pattern is great and it come...]]></description><link>https://blog.ryanmarvin.dev/part-i-bare-webockets-with-elixir-phoenix-wo-channels</link><guid isPermaLink="true">https://blog.ryanmarvin.dev/part-i-bare-webockets-with-elixir-phoenix-wo-channels</guid><category><![CDATA[websockets]]></category><category><![CDATA[Elixir]]></category><category><![CDATA[Phoenix framework]]></category><category><![CDATA[Phoenix Channels]]></category><dc:creator><![CDATA[Ryan Marvin]]></dc:creator><pubDate>Wed, 30 Oct 2024 12:40:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1730292092647/c89a901e-a093-48c9-93fd-0fca50e462d4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Websockets are great for when you want bidirectional communication between the client and the server over an extended session.</p>
<p>The de facto approach to building websockets with Elixir Phoenix involves <a target="_blank" href="https://hexdocs.pm/phoenix/channels.html">using Channels</a>. This pattern is great and it comes with a very well designed protocol and library of clients that make it really easy to get started. But for my use case while building for this customer, I needed a bare websocket since I didn’t have any supported Phoenix Channels client I could use.</p>
<p>This guide will share how I did that and it presumes you have the Phoenix application and server set up <a target="_blank" href="https://hexdocs.pm/phoenix/up_and_running.html">already</a>. We will be using an example social media newsfeed backend, which would establish a long-lived connection with a user’s browser and constantly refresh as soon as new posts appear. It is far easier to implement this using Channels but for my demonstration we won’t be doing this.</p>
<p>This is the reference <a target="_blank" href="https://github.com/RyanSept/elixir-sockets-example/commits/main/">codebase</a> for this article. It contains a pre-generated Phoenix app called <code>my_app</code>.</p>
<h2 id="heading-1-setup-a-scaffold-for-the-socket-transport">1. Setup a scaffold for the socket transport</h2>
<p>i. We start by creating a custom socket transport by copying the scaffold from the docs <a target="_blank" href="https://hexdocs.pm/phoenix/Phoenix.Socket.Transport.html#module-example">here</a>. Its behaviour is to echo back whatever input is sent.</p>
<pre><code class="lang-elixir"><span class="hljs-comment"># lib/myapp_web/sockets/newsfeed_socket.ex</span>
<span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">Myapp.NewsfeedSocket</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-variable">@behaviour</span> Phoenix.Socket.Transport
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">child_spec</span></span>(_opts) <span class="hljs-keyword">do</span>
    <span class="hljs-comment"># We won't spawn any process, so let's ignore the child spec</span>
    <span class="hljs-symbol">:ignore</span>
  <span class="hljs-keyword">end</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">connect</span></span>(state) <span class="hljs-keyword">do</span>
    <span class="hljs-comment"># Callback to retrieve relevant data from the connection.</span>
    <span class="hljs-comment"># The map contains options, params, transport and endpoint keys.</span>
    {<span class="hljs-symbol">:ok</span>, state}
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span></span>(state) <span class="hljs-keyword">do</span>
    <span class="hljs-comment"># Now we are effectively inside the process that maintains the socket.</span>
    {<span class="hljs-symbol">:ok</span>, state}
  <span class="hljs-keyword">end</span>

  <span class="hljs-comment"># messages sent to us by the client </span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_in</span></span>({text, _opts}, state) <span class="hljs-keyword">do</span>
    {<span class="hljs-symbol">:reply</span>, <span class="hljs-symbol">:ok</span>, {<span class="hljs-symbol">:text</span>, text}, state}
  <span class="hljs-keyword">end</span>

  <span class="hljs-comment"># messages sent to this connection process by other processes on the server</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_info</span></span>(_, state) <span class="hljs-keyword">do</span>
    {<span class="hljs-symbol">:ok</span>, state}
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">terminate</span></span>(_reason, _state) <span class="hljs-keyword">do</span>
    <span class="hljs-symbol">:ok</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>ii. Add the websocket route in endpoint.ex</p>
<pre><code class="lang-elixir"><span class="hljs-comment"># lib/myapp_web/endpoint.ex</span>
socket <span class="hljs-string">"/feed"</span>, Myapp.NewsfeedSocket, <span class="hljs-symbol">websocket:</span> <span class="hljs-keyword">true</span>
</code></pre>
<p>iii. <a target="_blank" href="https://postman.com">Postman</a> offers a really convenient UI for testing websockets and it’s what we’ll be using in this guide.</p>
<p>Connecting to the route we created via <code>ws://127.0.0.1:4000/feed/websocket</code> and sending some text, we can verify that we have setup the code correctly since it echoes it back:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730286507047/e52e7121-202e-44ae-925c-df8aad975b84.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-2-implement-auth-validation">2. Implement auth validation</h2>
<p>Roughly described, the protocol for websockets is:</p>
<p>a. a client initiates the connection by making a HTTP GET request with a specification to upgrade the connection. This corresponds <code>connect/1</code> in the module above.</p>
<p>b. The server initiates the session by switching to the websocket protocol or declines the HTTP request. This corresponds to <code>init/1</code>.</p>
<p>Our auth validation will have to be implemented in <code>connect/1</code>.</p>
<p>i. First we’ll need to pass the headers onto the state through the route in endpoint.ex. We’ll also add an error handler whose implementation we’ll define later:</p>
<pre><code class="lang-elixir"><span class="hljs-comment"># lib/myapp_web/endpoint.ex</span>
socket <span class="hljs-string">"/feed"</span>, Myapp.NewsfeedSocket,
    <span class="hljs-symbol">websocket:</span> [
      <span class="hljs-symbol">connect_info:</span> [<span class="hljs-symbol">:x_headers</span>],
      <span class="hljs-symbol">error_handler:</span> {Myapp.NewsfeedSocket, <span class="hljs-symbol">:handle_error</span>, []}
    ]
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The reference for all websocket configurations available can be found <a target="_blank" href="https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration">here</a>.</div>
</div>

<p>ii. Next we’ll add the <code>validate_auth</code> function and call it in our module:</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">Myapp.NewsfeedSocket</span></span> <span class="hljs-keyword">do</span>
...
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">connect</span></span>(state) <span class="hljs-keyword">do</span>
    validate_auth(state)
  <span class="hljs-keyword">end</span>
...
  <span class="hljs-function"><span class="hljs-keyword">defp</span> <span class="hljs-title">validate_auth</span></span>(state) <span class="hljs-keyword">do</span>
    headers = Enum.into(state[<span class="hljs-symbol">:connect_info</span>][<span class="hljs-symbol">:x_headers</span>] || [], %{})
    auth_token = headers[<span class="hljs-string">"x-authorization"</span>]
    <span class="hljs-keyword">case</span> auth_token <span class="hljs-keyword">do</span>
      <span class="hljs-string">"secret"</span> -&gt;
        {<span class="hljs-symbol">:ok</span>, state}
      _ -&gt;
        {<span class="hljs-symbol">:error</span>, <span class="hljs-symbol">:unauthorized</span>}
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
...
</code></pre>
<p>This simple validate method checks the <code>x-authorization</code> header to confirm the value included matches the secret string we expect, otherwise returning an error.</p>
<p>iii. Last we’ll add the <code>handle_error</code> function. This allows us to set the status code on the HTTP response</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">Myapp.NewsfeedSocket</span></span> <span class="hljs-keyword">do</span>
 <span class="hljs-keyword">import</span> Plug.Conn
...
 <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_error</span></span>(conn, error) <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">case</span> error <span class="hljs-keyword">do</span>
      <span class="hljs-symbol">:unauthorized</span> -&gt;
        conn
        |&gt; send_resp(<span class="hljs-number">401</span>, <span class="hljs-string">"Unauthorized"</span>)

      _ -&gt;
        conn
        |&gt; send_resp(<span class="hljs-number">500</span>, <span class="hljs-string">"Internal Server Error"</span>)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
...
</code></pre>
<p>iv. Validating this on Postman we should see an error if we make a request with an invalid or missing auth header</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730289700094/a81b8dc3-ae66-43eb-aee8-f2a61550667e.png" alt class="image--center mx-auto" /></p>
<p>We’ll disable this validation by commenting out so we can build out the rest of our application:</p>
<pre><code class="lang-elixir"><span class="hljs-function"><span class="hljs-keyword">defp</span> <span class="hljs-title">validate_auth</span></span>(state) <span class="hljs-keyword">do</span>
...
    _ -&gt;
       <span class="hljs-comment"># {:error, :unauthorized}</span>
       {<span class="hljs-symbol">:ok</span>, state}
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-3-create-newsfeed-business-logic">3. Create Newsfeed business logic</h2>
<p>We’ll add some simple business logic to return a list of posts which represents our newsfeed.</p>
<pre><code class="lang-elixir"><span class="hljs-comment"># lib/myapp/newsfeed.ex</span>
<span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">Myapp.Newsfeed</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-comment"># make struct JSON encodable</span>
  <span class="hljs-variable">@derive</span> Jason.Encoder
  defstruct <span class="hljs-symbol">author:</span> <span class="hljs-keyword">nil</span>, <span class="hljs-symbol">body:</span> <span class="hljs-keyword">nil</span>, <span class="hljs-symbol">time:</span> <span class="hljs-keyword">nil</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_feed</span></span>(),
    <span class="hljs-symbol">do:</span> [
      %__MODULE__{<span class="hljs-symbol">author:</span> <span class="hljs-string">"user1"</span>, <span class="hljs-symbol">body:</span> <span class="hljs-string">"It do be like that"</span>, <span class="hljs-symbol">time:</span> <span class="hljs-string">~U[2025-01-01 12:00:00Z]</span>},
      %__MODULE__{<span class="hljs-symbol">author:</span> <span class="hljs-string">"user2"</span>, <span class="hljs-symbol">body:</span> <span class="hljs-string">"Covfefe again"</span>, <span class="hljs-symbol">time:</span> <span class="hljs-string">~U[2025-01-01 12:01:00Z]</span>},
      %__MODULE__{<span class="hljs-symbol">author:</span> <span class="hljs-string">"user3"</span>, <span class="hljs-symbol">body:</span> <span class="hljs-string">"It's giving"</span>, <span class="hljs-symbol">time:</span> <span class="hljs-string">~U[2025-01-01 12:02:00Z]</span>}
    ]
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-4-periodic-fetch-and-refresh-of-feed">4. Periodic fetch and refresh of feed</h2>
<p>We want to send the posts to the user immediately they connect then refresh this by sending them the latest posts every 10 seconds.</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">Myapp.NewsfeedSocket</span></span> <span class="hljs-keyword">do</span>
 <span class="hljs-keyword">alias</span> Myapp.Newsfeed
 ... 
 <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span></span>(state) <span class="hljs-keyword">do</span>
    send(<span class="hljs-keyword">self</span>(), <span class="hljs-symbol">:refresh_and_push_feed</span>)
    {<span class="hljs-symbol">:ok</span>, state}
 <span class="hljs-keyword">end</span>

 <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_info</span></span>(<span class="hljs-symbol">:refresh_and_push_feed</span>, state) <span class="hljs-keyword">do</span>
    feed = Jason.encode!(Newsfeed.get_feed())
    <span class="hljs-comment"># schedule the refresh for the next interval</span>
    schedule_feed_refresh()
    {<span class="hljs-symbol">:push</span>, {<span class="hljs-symbol">:text</span>, feed}, state}
  <span class="hljs-keyword">end</span>

 <span class="hljs-variable">@doc</span> <span class="hljs-string">"""
  Schedule pushing of newsfeed every 10 seconds
  """</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">schedule_feed_refresh</span></span>() <span class="hljs-keyword">do</span>
    Process.send_after(<span class="hljs-keyword">self</span>(), <span class="hljs-symbol">:refresh_and_push_feed</span>, <span class="hljs-number">10_000</span>)
  <span class="hljs-keyword">end</span>
...
<span class="hljs-keyword">end</span>
</code></pre>
<p>i. <code>handle_info/2</code> handles messages sent to the process managing the websocket connection. We add a <code>handle_info/2</code> that will fetch the latest posts and push them to the client</p>
<p>ii. We then trigger its call immediately the websocket is initialized in <code>init/1</code>.</p>
<p>iii. Finally, we implement the scheduled refresh to happen every 10 seconds.</p>
<p>iv. We verify that this works on Postman, by seeing a message come in from our server every 10 seconds</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730291564494/e72f1308-af1d-4972-81c5-da06a911344a.png" alt class="image--center mx-auto" /></p>
<p>This is part 1 of 3 of this series. In the next articles, I will show you how to implement ping/pong opcodes and how to write tests. Let me know if you have any questions.</p>
]]></content:encoded></item><item><title><![CDATA[Zero Downtime Postgres Database Migration from GCP to AWS RDS]]></title><description><![CDATA[In 2023, I was working at a company where we were running our Elixir backend on Gigalixir.
Gigalixir is a PaaS similar to Heroku that at the time we were using provided its managed database service for Postgres via GCP (Google Cloud Platform). I foun...]]></description><link>https://blog.ryanmarvin.dev/zero-downtime-postgres-database-migration-from-gcp-to-aws-rds</link><guid isPermaLink="true">https://blog.ryanmarvin.dev/zero-downtime-postgres-database-migration-from-gcp-to-aws-rds</guid><category><![CDATA[gigalixir]]></category><category><![CDATA[AWS]]></category><category><![CDATA[AWS RDS]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[database migrations]]></category><category><![CDATA[GCP]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[database administration]]></category><dc:creator><![CDATA[Ryan Marvin]]></dc:creator><pubDate>Tue, 01 Oct 2024 17:10:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1727802821724/757d68bf-99be-42dd-a28e-09ebeffbc4d9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In 2023, I was working at a company where we were running our Elixir backend on Gigalixir.</p>
<p>Gigalixir is a PaaS similar to Heroku that at the time we were using provided its managed database service for Postgres via GCP (Google Cloud Platform). I found it troublesome since it was a bit of a black-box with limited scaling and observability. I motivated this migration since we needed to be able to scale the DB more granularly, monitor disk, CPU, memory and disk usage and better backups and restores.</p>
<p>A zero downtime migration strategy was ideal since <em>more 9s = happier users + happier engineers.</em> I decided to use <a target="_blank" href="https://www.postgresql.org/docs/17/logical-replication.html">Postgres logical replication</a> which allows you to make the source database a publisher and the migration target a subscriber to synchronise data between them. In our case, the GCP DB would be the publisher whereas the AWS one would be the subscriber.</p>
<h2 id="heading-strategy"><strong>Strategy</strong></h2>
<p>This was the general strategy I came up with:</p>
<ol>
<li><p>Replicate the schema from GCP onto a new RDS DB instance</p>
</li>
<li><p>Enable logical replication on RDS and sync existing data then continue ongoing replication</p>
</li>
<li><p>Cutover the application to the GCP database</p>
</li>
</ol>
<p>I found that AWS RDS prevents creation of a subscriber. They have an <code>rds_replication</code> role present on the database but <a target="_blank" href="https://repost.aws/questions/QUceSzmyIHTQqyv3cL-v_fFw">it seemed artificially limited and couldn’t actually create subscriptions, forcing one to go through their paid service AWS Database Migration Service (DMS</a>) which uses logical replication under the hood. So this service is what I used.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">Logical replication happens table-wise, capturing DML changes on each of the source database tables and replicating them on the target. As such it requires that all tables have a primary key in order for UPDATEs and DELETEs to be replicated.</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">It is also limited in that you can't capture DDL changes with this strategy so it's best to pause DDL modifications until the migration is completed.</div>
</div>

<p>Here's a guide of how I did it:</p>
<h2 id="heading-steps">Steps</h2>
<h3 id="heading-i-prep-the-gcp-source-database">I. Prep the GCP source database</h3>
<ol>
<li><p>Ensure the <code>wal_level</code> is set to <code>logical</code> on and that pglogical, which is the extension that helps with this replication strategy, is installed.</p>
<p> Set <code>cloudsql.logical_decoding</code> and <code>cloudsql.enable_pglogical</code> flags to <strong><em>on</em></strong> and reboot the DB which could make it unavailable for a few minutes but should take seconds on average.</p>
<pre><code class="lang-sql"> <span class="hljs-keyword">SHOW</span> wal_level;
 <span class="hljs-comment">-- should return logical</span>

 <span class="hljs-keyword">CREATE</span> EXTENSION pglogical;

 <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> pg_catalog.pg_extension <span class="hljs-keyword">WHERE</span> extname = <span class="hljs-string">'pglogical'</span>;
 <span class="hljs-comment">-- should return row showing pglogical is installed</span>
</code></pre>
</li>
<li><p>Create replication user which AWS DMS will use to login and grant them the appropriate roles:</p>
<pre><code class="lang-sql"> <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">USER</span> repl_user_from_aws <span class="hljs-keyword">WITH</span> <span class="hljs-keyword">REPLICATION</span> <span class="hljs-keyword">IN</span> <span class="hljs-keyword">ROLE</span> cloudsqlsuperuser LOGIN <span class="hljs-keyword">PASSWORD</span> <span class="hljs-string">'&lt;generated password&gt;'</span>;
 <span class="hljs-comment">-- permit AWS' access to pglogical tables</span>

 <span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">USAGE</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">SCHEMA</span> pglogical <span class="hljs-keyword">TO</span> repl_user_from_aws;

 <span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">TABLES</span> <span class="hljs-keyword">IN</span> <span class="hljs-keyword">SCHEMA</span> pglogical <span class="hljs-keyword">TO</span> repl_user_from_aws;
 <span class="hljs-comment">-- permit AWS' read access to all our tables</span>

 <span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">USAGE</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">TO</span> repl_user_from_aws;

 <span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">TABLES</span> <span class="hljs-keyword">IN</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">TO</span> repl_user_from_aws;
</code></pre>
</li>
</ol>
<h3 id="heading-ii-prep-the-aws-rds-target-database">II. Prep the AWS RDS target database</h3>
<ol>
<li><p>Set the following DB parameters in the parameter group to switch on logical replication</p>
<pre><code class="lang-sql"> rds.logical_replication = 1
 session_replication_role = replica
</code></pre>
</li>
<li><p>Setup the following PG extensions:</p>
<pre><code class="lang-sql"> <span class="hljs-keyword">CREATE</span> EXTENSION <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> pg_stat_statements;
 <span class="hljs-keyword">CREATE</span> EXTENSION <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span>  pglogical;
</code></pre>
</li>
<li><p>Fetch the DB schema from the source database and create it on the target RDS DB.</p>
<pre><code class="lang-bash"> pg_dump <span class="hljs-variable">$GCP_DB_ADMIN</span> --schema-only --no-privileges --no-subscriptions --no-publications -Fd -f ./db_schema

 pg_restore -Fd -v -n public --single-transaction -s --no-privileges -h &lt;target-db-host&gt; -U &lt;target-db-username&gt; -p 5432 -d &lt;target-db-name&gt; ./db_schema

 <span class="hljs-comment"># pg_restore: connecting to database for restore</span>
 <span class="hljs-comment"># Password:</span>
 <span class="hljs-comment"># pg_restore: creating TABLE "public.****"</span>
 <span class="hljs-comment"># pg_restore: creating TABLE "public.****"</span>
 <span class="hljs-comment"># pg_restore: creating TABLE "public.****"</span>
 <span class="hljs-comment"># …</span>
</code></pre>
</li>
<li><p>Validate table schema equivalency between GCP DB and AWS RDS DB</p>
<pre><code class="lang-sql"> <span class="hljs-comment">--- Show columns, indices and constraints counts</span>

 <span class="hljs-comment">---- column count</span>
 <span class="hljs-keyword">SELECT</span> table_name, <span class="hljs-keyword">count</span>(column_name) <span class="hljs-keyword">AS</span> column_count <span class="hljs-keyword">FROM</span> information_schema.columns <span class="hljs-keyword">WHERE</span> table_schema = <span class="hljs-string">'public'</span> <span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span> table_name <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> table_name;

 <span class="hljs-comment">---- index count</span>
 <span class="hljs-keyword">SELECT</span> tablename <span class="hljs-keyword">AS</span> table_name, indexname, <span class="hljs-keyword">COUNT</span>(indexname) <span class="hljs-keyword">AS</span> index_count <span class="hljs-keyword">FROM</span> pg_indexes <span class="hljs-keyword">WHERE</span> schemaname = <span class="hljs-string">'public'</span> <span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span> indexname, tablename <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> tablename;

 <span class="hljs-comment">---- constraint count</span>
 <span class="hljs-keyword">SELECT</span> conrelid::regclass <span class="hljs-keyword">AS</span> table_name, <span class="hljs-keyword">COUNT</span>(conname) <span class="hljs-keyword">AS</span> constraint_count <span class="hljs-keyword">FROM</span> pg_constraint <span class="hljs-keyword">WHERE</span> connamespace = (<span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">oid</span> <span class="hljs-keyword">FROM</span> pg_namespace <span class="hljs-keyword">WHERE</span> nspname = <span class="hljs-string">'public'</span>) <span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span> table_name <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> table_name;
</code></pre>
</li>
</ol>
<h3 id="heading-iii-initialize-migration-with-aws-dms">III. Initialize migration with AWS DMS</h3>
<ol>
<li><p>Via the AWS console, create the DMS replication instance with the following settings. Make sure to use the same VPC as the target RDS DB if one exists, otherwise place it in the default VPC.</p>
<pre><code class="lang-yaml"> <span class="hljs-attr">name:</span> <span class="hljs-string">staging-migrator</span>
 <span class="hljs-attr">size:</span> <span class="hljs-string">dms.t3.medium</span>
 <span class="hljs-attr">storage:</span> <span class="hljs-number">5</span> <span class="hljs-string">GiB</span>
 <span class="hljs-attr">vpc:</span> <span class="hljs-string">vpc-xxxxxx</span>
 <span class="hljs-attr">subnet group:</span> <span class="hljs-string">default</span>
 <span class="hljs-attr">high availability:</span> <span class="hljs-string">prod</span> <span class="hljs-string">(for</span> <span class="hljs-string">prod</span> <span class="hljs-string">migration),</span> <span class="hljs-string">dev</span> <span class="hljs-string">for</span> <span class="hljs-string">staging</span> <span class="hljs-string">migration</span>
 <span class="hljs-attr">publicly accessible:</span> <span class="hljs-literal">true</span>
</code></pre>
 <div data-node-type="callout">
 <div data-node-type="callout-emoji">⚠</div>
 <div data-node-type="callout-text">Setting it to be <code>publicly accessible</code> makes your RDS database visible to the internet. Since AWS and GCP are separate cloud providers, this is the easiest way to allow them to connect barring setting up some network peering. Consider that if your security is necessary but for most usecases, enforcing SSL and using a secure password might be enough.</div>
 </div>
</li>
<li><p>Create the source endpoint using the GCP database's credentials.</p>
<pre><code class="lang-yaml"> <span class="hljs-attr">name:</span> <span class="hljs-string">prod-gcp-db</span>
 <span class="hljs-attr">engine:</span> <span class="hljs-string">PostgreSQL</span>
 <span class="hljs-attr">server name:</span> <span class="hljs-string">&lt;Gigalixir</span> <span class="hljs-string">DB</span> <span class="hljs-string">host&gt;</span>
 <span class="hljs-attr">port:</span> <span class="hljs-number">5432</span>
 <span class="hljs-attr">username:</span> <span class="hljs-string">repl_user_from_aws</span>
 <span class="hljs-attr">password:</span> <span class="hljs-string">&lt;repl_user_from_aws</span> <span class="hljs-string">password&gt;</span>
 <span class="hljs-attr">ssl mode:</span> <span class="hljs-string">require</span>
 <span class="hljs-attr">database name:</span> <span class="hljs-string">db</span> <span class="hljs-string">name</span>
</code></pre>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727800847378/69653985-3dbd-4b26-9c69-6421ce954dfc.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Create the target endpoint using RDS DB credentials. You can use the raw credentials or optionally create and use an AWS SecretsManager secret since your DB is on RDS like I did.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727800881849/57331c75-6fd8-42fb-bb26-521179c22e73.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Create a migration task using the source and target endpoints from the above steps setting <em>migration type</em> as <code>Migrate existing data and replicate ongoing changes</code>.</p>
 <div data-node-type="callout">
 <div data-node-type="callout-emoji">💡</div>
 <div data-node-type="callout-text">Enable Cloudwatch logging so it's easier to debug in case it halts or there are conflicts.</div>
 </div>

<p> Specify the following task settings and table mapping rules in this <a target="_blank" href="https://gist.github.com/RyanSept/1969b269e6ec000a2bb8ed480bcf379c">gist</a>.</p>
</li>
</ol>
<p>Once it starts, it will perform a <code>full load</code> before proceeding to <code>ongoing replication</code> where changes will stream from GCP as they happen. Once it is in ongoing replication it's move to the cutover stage.</p>
<h3 id="heading-iv-cutover">IV. Cutover</h3>
<ol>
<li><p>Once the full load of all the tables is completed and it switches to <em>ongoing replication</em>, validate that the target RDS DB tables match the source:</p>
<pre><code class="lang-sql"> <span class="hljs-comment">--- show the row counts for each table.</span>
 <span class="hljs-comment">--- https://stackoverflow.com/a/2611745/7558991</span>
 <span class="hljs-keyword">WITH</span> tbl <span class="hljs-keyword">AS</span>
 (<span class="hljs-keyword">SELECT</span> table_schema,
 TABLE_NAME
 <span class="hljs-keyword">FROM</span> information_schema.tables
 <span class="hljs-keyword">WHERE</span> TABLE_NAME <span class="hljs-keyword">not</span> <span class="hljs-keyword">like</span> <span class="hljs-string">'pg_%'</span>
 <span class="hljs-keyword">AND</span> table_schema <span class="hljs-keyword">in</span> (<span class="hljs-string">'public'</span>))
 <span class="hljs-keyword">SELECT</span> table_schema,
 TABLE_NAME,
 (xpath(<span class="hljs-string">'/row/c/text()'</span>, query_to_xml(<span class="hljs-keyword">format</span>(<span class="hljs-string">'select count(*) as c from %I.%I'</span>, table_schema, TABLE_NAME), <span class="hljs-literal">FALSE</span>, <span class="hljs-literal">TRUE</span>, <span class="hljs-string">''</span>)))[<span class="hljs-number">1</span>]::<span class="hljs-built_in">text</span>::<span class="hljs-built_in">int</span> <span class="hljs-keyword">AS</span> rows_n
 <span class="hljs-keyword">FROM</span> tbl
 <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> TABLE_NAME;
</code></pre>
<p> Proceed to cutting over the application only if the row counts match.</p>
</li>
<li><p>Switch over your application configs to use the new DB connection credentials <code>postgres://&lt;user&gt;:&lt;password&gt;@&lt;host&gt;:5432/my_app</code>.</p>
</li>
<li><p>Once your application is switched over to the RDS DB, stop the DMS migration task.</p>
</li>
<li><p>Celebrate (cautiously).</p>
</li>
</ol>
<h3 id="heading-v-cleanup">V. Cleanup</h3>
<ol>
<li><p>Drop the replication user and AWS DMS objects from your GCP database</p>
<pre><code class="lang-sql"> <span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">on</span> awsdms_ddl_audit <span class="hljs-keyword">TO</span> <span class="hljs-keyword">public</span>; <span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">on</span> awsdms_ddl_audit_c_key_seq <span class="hljs-keyword">TO</span> <span class="hljs-keyword">public</span>;
 <span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> awsdms_ddl_audit OWNER <span class="hljs-keyword">TO</span> gigalixir_admin;
 <span class="hljs-keyword">REVOKE</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">TABLES</span> <span class="hljs-keyword">IN</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span>, pglogical <span class="hljs-keyword">FROM</span> repl_user_from_aws;
 <span class="hljs-keyword">REVOKE</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span>, pglogical <span class="hljs-keyword">FROM</span> repl_user_from_aws;
 <span class="hljs-keyword">DROP</span> <span class="hljs-keyword">FUNCTION</span> awsdms_intercept_ddl <span class="hljs-keyword">CASCADE</span>;
 <span class="hljs-keyword">DROP</span> <span class="hljs-keyword">USER</span> repl_user_from_aws;
 <span class="hljs-keyword">DROP</span> <span class="hljs-keyword">TABLE</span> awsdms_ddl_audit <span class="hljs-keyword">CASCADE</span>;
</code></pre>
</li>
<li><p>Revert the RDS DB parameters:</p>
<pre><code class="lang-sql"> rds.logical_replication = 0
 session_replication_role = origin
</code></pre>
</li>
<li><p>Remove the DMS replication instance and source &amp; target endpoints.</p>
</li>
</ol>
<h2 id="heading-evaluation-and-validation-examples">Evaluation and validation examples</h2>
<p>Here’s how some of the validation commands output looked like for me</p>
<ol>
<li><p>Confirming schema equivalency between source and target</p>
<p> source:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727800610413/06a60b7d-0626-4de7-a0fb-efe78b93942e.png" alt class="image--center mx-auto" /></p>
<p> target:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727800617875/b2301bdf-af0e-4b1d-8e31-80e9fa63a0db.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Row counts after migration is complete</p>
<p> source:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727801103868/624116b9-b622-43df-b950-6bae004d7c11.png" alt class="image--center mx-auto" /></p>
<p> target:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727801114329/e2fe0bde-aeb5-4efc-9997-e4bc8b8290a0.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>That's how I did it. Let me know your thoughts and alternative approaches you might have in mind.</p>
]]></content:encoded></item><item><title><![CDATA[How to migrate AWS CloudFormation templated resources to CDK]]></title><description><![CDATA[I was on a project using CloudFormation templates to manage resources and it became more desirable to migrate the resources to CDK since the project was growing.
I haven't seen a lot of docs on this out there so I'm sharing this. From a high level, i...]]></description><link>https://blog.ryanmarvin.dev/migrate-aws-cloudformation-templated-resources-to-cdk</link><guid isPermaLink="true">https://blog.ryanmarvin.dev/migrate-aws-cloudformation-templated-resources-to-cdk</guid><category><![CDATA[AWS]]></category><category><![CDATA[aws-cdk]]></category><category><![CDATA[cloudformation]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[AWS CloudFormation]]></category><dc:creator><![CDATA[Ryan Marvin]]></dc:creator><pubDate>Mon, 05 Jun 2023 12:30:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693153358305/7cb59dec-904a-42f1-a121-4cad3ee9cb11.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I was on a project using CloudFormation templates to manage resources and it became more desirable to migrate the resources to CDK since the project was growing.</p>
<p>I haven't seen a lot of docs on this out there so I'm sharing this. From a high level, it involves detaching the resources from the existing stack, creating the new stack with CDK, translating the code, and then importing your resources into the new stack. In detail:</p>
<h3 id="heading-1-set-a-deletionpolicy-on-all-of-your-existing-stacks-resources">1. Set a DeletionPolicy on all of your existing stack's resources</h3>
<p>We first need to set a <strong>DeletionPolicy</strong> of "<strong>Retain</strong>" on each resource in your CF template. This is <strong>important</strong> since it lets us delete the old stack without removing its resources.</p>
<p>On <em>each</em> resource in your stack, add the following:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">AWSTemplateFormatVersion:</span> <span class="hljs-number">2010-09-09</span>
<span class="hljs-attr">Resources:</span>
   <span class="hljs-attr">NotificationTopic:</span>
     <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::SNS::Topic</span>
<span class="hljs-string">+</span>    <span class="hljs-attr">DeletionPolicy:</span> <span class="hljs-string">Retain</span>
     <span class="hljs-attr">Properties:</span>
       <span class="hljs-attr">TopicName:</span> <span class="hljs-string">"MySNSTopic"</span>
</code></pre>
<p>Apply the changes to the stack:</p>
<pre><code class="lang-bash">aws cloudformation update-stack \
    --stack-name MyOldStack \
    --template-body file://myoldstack.yml
</code></pre>
<h3 id="heading-2-delete-the-stack-to-detach-the-resources">2. Delete the stack to detach the resources</h3>
<p>Make sure you've completed the previous step and validate each resource to make sure they aren't deleted.</p>
<pre><code class="lang-bash">aws cloudformation delete-stack --stack-name MyOldStack
</code></pre>
<p>You can confirm that this is complete via the AWS console. You ought to see the status <code>DELETE_SKIPPED</code> for all the resources in the stack. The resources should continue to exist and function</p>
<h3 id="heading-3-create-the-new-cdk-target-stack-if-doesnt-already-exist">3. Create the new CDK target stack if doesn't already exist</h3>
<p>If it doesn't already exist, add your new target stack in your CDK code and run <code>cdk deploy</code> to create it:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> aws_cdk <span class="hljs-keyword">as</span> cdk

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyCDKStack</span>(<span class="hljs-params">cdk.Stack</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, scope: cdk.App, construct_id: str, **kwargs</span>) -&gt; <span class="hljs-keyword">None</span>:</span>
        super().__init__(scope, construct_id, **kwargs)
</code></pre>
<pre><code class="lang-bash">cdk deploy -e MyCDKStack
<span class="hljs-comment"># ...</span>
<span class="hljs-comment"># ✨  Deployment time: 11.83s</span>

<span class="hljs-comment"># Stack ARN:</span>
<span class="hljs-comment"># arn:aws:cloudformation:us-east-1:123456789:stack/MyCDKStack/xxxx</span>

<span class="hljs-comment"># ✨  Total time: 20.41s</span>
</code></pre>
<h3 id="heading-4-define-the-resources-youre-importing-in-the-cdk-stack-code">4. Define the resources you're importing in the CDK stack code</h3>
<p>You'll need to translate all of the CloudFormation template code into whichever language your CDK code is in and add it to your stack. LLMs are incredibly helpful with this and I found the best results with ChatGPT. This is the prompt I used:</p>
<pre><code class="lang-plaintext">transform the following cloudformation template resource into Python CDK code
```
Resources:
   NotificationTopic:
     Type: AWS::SNS::Topic
     DeletionPolicy: Retain
     Properties:
       TopicName: "MySNSTopic"
```
</code></pre>
<p>I also had to instruct it severally to use the correct imports in Python by adding a specific instruction "<em>Please rewrite the resource code to use the L2 construct imported from aws_cdk.xyz</em>".</p>
<p>Here is a sample of what it output, added to our stack code from earlier</p>
<pre><code class="lang-python">...
<span class="hljs-keyword">from</span> aws_cdk <span class="hljs-keyword">import</span> aws_sns <span class="hljs-keyword">as</span> sns

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyCDKStack</span>(<span class="hljs-params">cdk.Stack</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, scope: cdk.App, construct_id: str, **kwargs</span>) -&gt; <span class="hljs-keyword">None</span>:</span>
        ...
        notification_topic = sns.Topic(
            self, 
            <span class="hljs-string">"MySNSTopic"</span>,
            display_name=<span class="hljs-string">"MySNSTopic"</span>,
            <span class="hljs-comment"># This is an API detail it got wrong</span>
            <span class="hljs-comment"># removal_policy=core.RemovalPolicy.RETAIN </span>
        )
</code></pre>
<p>You'll have to do a fair amount of editing and refer to the <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v1/">CDK docs</a> but ChatGPT provided an incredibly convenient starting point. Make sure to include every parameter you have on the live resource or else you may get more diffs after importing.</p>
<h3 id="heading-5-import-the-resources-into-the-cdk-stack">5. Import the resources into the CDK stack</h3>
<p>Once all your resources have been translated into CDK code, run <code>cdk import</code>. This command is still in preview at the time of writing this and may be buggy. It will prompt you to provide resource identifiers of your existing resources:</p>
<pre><code class="lang-bash">cdk import MyCDKStack
<span class="hljs-comment"># The 'cdk import' feature is currently in preview.</span>
<span class="hljs-comment"># MyCDKStack</span>
<span class="hljs-comment"># MyCDKStack/MySNSTopic/Resource (AWS::SNS::Topic): enter TopicArn [undefined]: arn:aws:sns:us-east-1:123456789:MySNSTopic</span>
<span class="hljs-comment"># MyCDKStack: importing resources into stack...</span>
<span class="hljs-comment"># MyCDKStack: creating CloudFormation changeset...</span>

<span class="hljs-comment"># ✅  MyCDKStack</span>
</code></pre>
<h3 id="heading-6-diff-and-update-any-missing-details">6. Diff and update any missing details</h3>
<p>Run a diff to see whether there is a difference between the resources you've imported and the code you have defined</p>
<pre><code class="lang-bash">cdk diff -e MyCDKStack
</code></pre>
<p>If there is a diff, once you resolve them in your code, you can run <code>cdk deploy -e MyCDKStack</code> to apply the changes.</p>
<h3 id="heading-other-notes">Other notes</h3>
<ul>
<li><p>Some resources like AWS SNS topic subscriptions cannot be imported and need to be recreated in your CDK stack. You can find the full list of importable resources <a target="_blank" href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-supported-resources.html">here</a>.</p>
</li>
<li><p>CloudFormation Parameters don't mix well with CDK. If you were relying on this you may want to use environment variables wherein your CDK code is running instead.</p>
</li>
<li><p>When you use an IAM role on a CDK resource, CDK's default behaviour is to add a new policy for it behind the scenes. This will cause you to see a diff even if you already had a relevant policy defined in the role. To resolve this you may want to use the <code>WithoutPolicyUpdates</code> role object instead i.e.</p>
<pre><code class="lang-bash">  role = iam.Role(...)
  some_resource = cdk.SomeResource(role=role.without_policy_updates())
</code></pre>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Preparing an Online Dev Workshop]]></title><description><![CDATA[For the past year, I’ve been the facilitator for the Workshop Wednesdays initiative at the Developerish Community. We’ve had close to a dozen software-development-related  workshops online  and I have learnt a lot, having both presented and facilitat...]]></description><link>https://blog.ryanmarvin.dev/preparing-an-online-dev-workshop</link><guid isPermaLink="true">https://blog.ryanmarvin.dev/preparing-an-online-dev-workshop</guid><category><![CDATA[conference]]></category><category><![CDATA[#howtos]]></category><dc:creator><![CDATA[Ryan Marvin]]></dc:creator><pubDate>Fri, 05 Mar 2021 12:57:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1614608781395/vsySA7VGU.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For the past year, I’ve been the facilitator for the Workshop Wednesdays initiative at the Developerish Community. We’ve had close to a dozen software-development-related  <a target="_blank" href="https://www.youtube.com/channel/UC--DkMH3rajQht1fdILjo3A">workshops online</a>  and I have learnt a lot, having both presented and facilitated. During the pandemic, our workshops have been a great way for members of our community to share their knowledge and experiences. For me, presenting workshops has also been great for digging deeper into what I already know or thought I understood since explaining something verbally can be a lot harder than reflexively doing it. Preparing a workshop takes a fair amount of work and time, from finding where to start, preparing your topic, code, slides then actually presenting it. So here is a cheat sheet:</p>
<h2 id="1-finding-ideas">1. Finding ideas</h2>
<p>Finding the right topic for your workshop can be the biggest reason for procrastinating doing one. Often times, however, the ideas are all around you. One great source of ideas is from your day to day work. What interesting library or framework are you using? What’s an interesting infrastructural problem you’ve resolved recently? Maybe it's meta, how did you improve how you’re measuring and estimating levels of effort for work? How did you improve your code review or hiring processes? In other cases it could be more soft-skill oriented or personal – how did you grow your impact in your team last year? How did you make a transition from software development to product management? What has managing teams taught you?</p>
<p>The list is endless and as long as you’re learning, reflecting and growing, there will always be something you can teach others. Make sure to write down all the ideas that come to mind over time, research them and read what others have written about the topic, saving bookmarks while you’re at it; they’ll come in handy. If a given topic has been exhaustively explored by others, you may still have a differentiating experience with it so avoid binning such ideas prematurely.</p>
<p>When it comes to choosing from your list of ideas, you can weigh each against the impact compared to the level of effort required to prepare it. If it’s your first workshop choose a low-effort idea to make it easier for you to build momentum for higher effort ideas in the future.</p>
<h2 id="2-define-your-audience-and-your-goals-for-them">2. Define your audience and your goals for them</h2>
<p>What’s their experience level? Are they beginners, intermediate or advanced? Do they need some preliminary knowledge to make sense of your presentation? Identifying the goals for your audience is important in helping you better define who they are and better refine your content for them.</p>
<p>For example you may be conducting a workshop on multiplication (the mathematical operation). Some of your goals might be that by the end of the workshop the audience:</p>
<ul>
<li>Will be able to multiply single digit numbers</li>
<li>Will be able to multiply more than 2 numbers together.</li>
<li>Will be able to multiply negative numbers</li>
<li>Will be able to multiply fractions</li>
</ul>
<p>Identifying goals and cases, you will uncover that perhaps your audience is better off memorising the multiplication table or that they may not understand fractions and negative numbers or that you may need to explain the order of operations or that you may need to explain multiplication in terms of addition and so on. You will find that each goal can be expounded upon even deeper but your audience’s experience level will determine how deep you need or want to go e.g. you can cover multiplication of proper fractions but mixed fractions may be out of scope. </p>
<p>This process of breaking down goals for the audience will feed into the next step of creating an outline.</p>
<h2 id="3-create-an-outline">3. Create an outline</h2>
<p>Here's a generic structure for an outline:</p>
<ul>
<li>Introduction<ul>
<li>Introduce yourself</li>
<li>Introduce your topic and what you’ll be covering</li>
</ul>
</li>
<li>1st main idea  <ul>
<li>Theoretical explanation</li>
<li>Exemplification from the real world</li>
<li>Illustration/exercise (diagram, flowchart, code sample/challenge, analogy, anecdote)</li>
<li>Optional supporting idea and/or practical step<ul>
<li>Explanation and illustration</li>
</ul>
</li>
</ul>
</li>
<li><p>2nd main idea</p>
<p> …</p>
</li>
<li><p>Summary of learned skills</p>
</li>
<li>Q&amp;A (question-and-answer)</li>
</ul>
<p>When starting, it’s important that the audience has an understanding of your level of expertise, background and motivations for the workshop like whether you’re representing your company or will be pushing certain products during your presentation. So begin by introducing yourself then introduce your topic; what you’ll be teaching your audience and your goals for them.</p>
<p>From there, you can begin digging into the substance of your workshop . A good framework for helping others understand complex concepts is the <a target="_blank" href="https://en.wikipedia.org/wiki/SEE-I">SEE-I</a> method where you take the audience from the theoretical elaboration of the concept then a real world example then an illustration or exercise. Since it’s a workshop and your goal is to impart skills on your audience, err in favour of practical hands-on illustrations that your audience could do or see you do or pose questions to them to keep them on their toes. Of course, the SEE-I method's steps are not set in stone and in some cases where the explanations are too complex or irrelevant for your audience you will be able to do without some of the explicatory steps. </p>
<p>At the end of your presentation, summarise what the audience has learnt and be ready to field their questions.</p>
<p>Here’s a more concrete example outline for a talk titled “Version Control with Git” an intro for beginners (<em>feel free to skim over once you grok the idea</em>):</p>
<ol>
<li>Introduction - 3 min<ul>
<li>Who is Jane Doe?</li>
<li>Objectives for today</li>
</ul>
</li>
<li>What is git? - 10 min<ul>
<li>Git is distributed version control. What is version control? What makes it distributed?</li>
<li>Why do developers use git?</li>
<li>Visual diagram demonstrating problem of 3 people adding lines to copies of the same file and how they need to reconcile changes when bringing it back together.</li>
<li>Let’s install git<ul>
<li>Run command to install git</li>
</ul>
</li>
</ul>
</li>
<li>How does git work? - 20 min<ul>
<li>Initializing the repository<ul>
<li>Run <code>git init</code> and describe what the command does; places .git folder in current directory.</li>
</ul>
</li>
<li>Adding a new file to the repo<ul>
<li>Run <code>git add</code> command and describe what staging and the staging environment are. Illustrate using <code>git status</code>.</li>
</ul>
</li>
<li>Committing staged changes<ul>
<li>Run <code>git commit</code> and illustrate using <code>git log</code> to show the created commit</li>
</ul>
</li>
</ul>
</li>
<li>Git branches - 15 min<ul>
<li>What is a branch?</li>
<li>How are branches used?</li>
<li>Visual diagram demonstrating tree with 2 branches containing the same set of commits and how their histories diverge upon changes to one of them.</li>
<li>Let’s create a branch<ul>
<li>Run <code>git branch</code> command to show all existing and currently checked out branches. Explain branch naming conventions.</li>
<li>Run <code>git branch</code> to create new branch following naming conventions. Run <code>git checkout</code> to switch branches and ask audience which command can be ran to prove the branch actually changed.</li>
</ul>
</li>
</ul>
</li>
<li><p>Syncing to remote repositories - 7 min</p>
<p> … <em>(other content)</em></p>
</li>
<li>Summary - 1 min</li>
<li>Q&amp;A - 10 min</li>
</ol>
<p>When writing out your outline, it is imperative that you allocate some time guesstimates for each section based on the total time you’ll have allocated for your session. It'll help you stay on schedule and trim the fat where the time is exceeded. The estimates won’t be very accurate in this initial draft but they’ll help you figure out what’s important and quantify the density of each concept. Editing is king and you should iteratively refine your outline, furnishing it with some margin notes where necessary.</p>
<h2 id="4-prepare-the-outlines-content">4. Prepare the outline's content</h2>
<p>An outline is like a skeleton that needs to be filled up with actual content so prepare the proofs of concept, code samples, questions for the audience and exercises that you’ll be going through in your session. </p>
<p>A slide deck is a really convenient way to pace yourself and deliver your content's key conceptual points. It’s also important that your slides are accessible, in terms of legibility, contrast and font-size. For that, you can find free slide templates on slidesgo.com that are also visually consistent and captivating. </p>
<p>Your diagrams and links to code samples or repositories will go on your slides. You can use free tools like Github Gists for smaller code samples or a Github repo for larger projects and diagrams.net (formerly draw.io) for making diagrams. </p>
<p>After the session it would be valuable to share the slide deck for the audience to revisit. If your presentation software has a speaker notes feature make sure the notes are intelligible for other people! Here is a list of other <a target="_blank" href="https://www.k-state.edu/techbytes/Resources/presentationtipsgkz.pdf">anti-patterns to look out for when making slides</a>.</p>
<h2 id="5-rehearse">5. Rehearse</h2>
<p>By this point, your workshop will have a reasonable semblance of completion but it’s not done yet. It’s time to rehearse the presentation. You can do this on your own but you’ll have better feedback by having at least one other person you trust listen to it. During rehearsal, avoid long stops to correct issues you find but instead take short notes that you can revisit later. Take note of how the presentation flows; whether certain subtopics need to be reordered, things that sound ambiguous or slides that throw you off and feel incongruent with the idea you want to deliver.</p>
<p>In addition, practice the audio-visual tools. You don’t want to be fumbling for the "share screen" button on Zoom during presentation. A neat lifehack with Google Slides is <a target="_blank" href="https://support.google.com/docs/answer/1696787?co=GENIE.Platform%3DDesktop&amp;hl=en">the presenter view</a> where you can view your speaker notes on one window and the actual slides on a different window and any transitions you make on the speaker window will change the current slide on the slides window that you’ll have shared with the audience. </p>
<p>Having finished rehearsal, you'll have a better idea of the time-boxes you estimated for each section and resize them accordingly. You will also have caught grammatical errors, smoothened out the flow of your presentation and ultimately have a finished product!</p>
<h2 id="conclusion">Conclusion</h2>
<p>With the right preparation, conducting an online workshop can be less nerve-wracking. It’s a really important skill for developers to have since our industry is founded upon and grown by the free dissemination of information and ideas – amongst other open-source principles. I wrote this because I believe everyone's ideas are valid and valuable so by refining our presentation skills we increase our capacity for impact. All the best with your workshop and let us know how it goes in the comments!</p>
]]></content:encoded></item><item><title><![CDATA[Basics of tmux and why you should care]]></title><description><![CDATA[I used to work often on remote machines. Whenever I needed to do something in a different window while still maintaining the state of the currently open one  — say, running a long terminal blocking task and editing a file simultaneously  —  I would o...]]></description><link>https://blog.ryanmarvin.dev/basics-of-tmux-and-why-you-should-care</link><guid isPermaLink="true">https://blog.ryanmarvin.dev/basics-of-tmux-and-why-you-should-care</guid><category><![CDATA[cli]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[tools]]></category><dc:creator><![CDATA[Ryan Marvin]]></dc:creator><pubDate>Wed, 05 Jun 2019 15:36:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1559750104969/lYzRQN5Lh.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I used to work often on remote machines. Whenever I needed to do something in a different window while still maintaining the state of the currently open one  — say, running a long terminal blocking task and editing a file simultaneously  —  I would open a new terminal tab/window and a whole new SSH connection. While it gets the jobs done, that can be a bit inefficient what with latency and all. This changed when I started using <strong>tmux</strong>.</p>
<p>tmux stands for <strong>terminal multiplexer</strong>. A major use case for tmux is providing access to several terminal sessions through a single window. Most GUI terminal programs like iTerm have this functionality and as most programmers often do, you probably have at least a dozen terminal tabs open right now.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1559738425845/-MmyardZT.png" alt="term-tab.png"></p>
<blockquote>
<p>My terminal tabs. tmux allows you to achieve this same tab abyss but on the CLI.</p>
</blockquote>
<p>Our focus will be on using tmux to manage windows and be more productive. You can find instructions for installing tmux at the end of this article. </p>
<h2 id="usage-guide">Usage guide</h2>
<ol>
<li>We&#39;ll SSH into our remote machine.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1559745663263/1jz6IWmUP.png" alt="sc-1.png"></p>
<p>2. To get started, we&#39;ll run <code>$ tmux</code> to start a new session. We should see a green bar appear at the bottom: </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1559738929874/I1wzp5iYu.png" alt="sc-2.png"></p>
<p>3. Now let&#39;s say we have some long running job that we want to remain in the foreground:</p>
<pre><code>## <span class="hljs-keyword">this</span> echos <span class="hljs-string">'Running some job'</span> to stdout once every second
$ <span class="hljs-keyword">while</span> <span class="hljs-literal">true</span>; <span class="hljs-keyword">do</span> echo <span class="hljs-string">'Running some job'</span> &amp;&amp; sleep <span class="hljs-number">1</span> ; done
</code></pre><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1559739450982/YCEnh6O8g.png" alt="sc-3.png"></p>
<p>4. If we need to edit a file using vim or some other editor, this terminal is blocked by our running job. To run other commands, we need to open a new window which we can do by pressing the following keys in sequence <code>Ctrl+b</code> then <code>c</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1559739501729/bbuV_Q1_x.png" alt="sc-4.png"></p>
<p>At the green status bar on the bottom, we can see on the left there&#39;s now two windows open <strong>0:bash</strong> and <strong>1:bash</strong>. <strong>1:bash</strong> has an asterisk * next to it to tell us it&#39;s the window we&#39;re currently viewing.</p>
<p>We&#39;ll go ahead and run <code>$ touch code.js</code> to create the file and <code>$ vim code.js</code> to edit it. We add a line of code for the Javascript &quot;hello world&quot; program.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1559740543136/6almKOd1H.png" alt="sc-5.png"></p>
<p>5. We have a problem now. We&#39;re trapped in vim and can&#39;t get out! While we Google how to exit vim, we decide to go back to the job we were running on step 3 to see how it&#39;s progressing. <code>0</code> is the identifier for the window we were running our job <strong>0:bash</strong> so to switch back to that window, we&#39;ll do <code>Ctrl+b 0</code>: </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1559743162379/NI-tNhMKz.png" alt="sc-6.png"></p>
<p>6. Let&#39;s say we want to run the Javascript program we created on step 4 but still keep watch of this job we&#39;re running. We can split this window either horizontally <code>Ctrl+b &quot;</code> or vertically <code>Ctrl+b %</code>. We&#39;ll split it vertically and run our script on the new pane:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1559744066191/IjNjmvfJE.png" alt="sc-7.png"></p>
<p>To switch between the two panes, we can press <code>Ctrl+b &lt;arrow-left&gt;</code> or <code>Ctrl+b &lt;arrow-right&gt;</code>. </p>
<p>7. Finally, we&#39;ll close the extra pane <code>Ctrl+b x</code>. </p>
<p>When you need to look up different commands and what they do, you can <code>Ctrl+b ?</code>. You may have noticed that all the commands we put in were prefixed with <code>Ctrl+b</code>. This is known as the <em>prefix key</em> and whatever comes after it is known as the <em>command key</em>. Thus we scroll down to the section with &quot;<em>bind-key    -T prefix</em>&quot; on the help page:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1559746305411/sQwm8jqka.png" alt="sc-8.png"></p>
<blockquote>
<p> Alternatively, this  <a target='_blank' rel='noopener noreferrer'  href="https://gist.github.com/MohamedAlaa/2961058">cheatsheet</a>  also comes in handy.</p>
</blockquote>
<p>These are a few of the basic commands that&#39;ll help you get going, there are dozens more. You can get by knowing only the commands in this article. You may find it a bit difficult to get used to tmux at first and frustrating to keep looking up commands but in a short while, they&#39;ll become second nature!</p>
<h2 id="installing-tmux">Installing tmux</h2>
<pre><code><span class="hljs-comment">## Ubuntu / Debian</span>
$ sudo apt-get install tmux
<span class="hljs-comment">## CentOS</span>
$ sudo yum -y install tmux
<span class="hljs-comment">## MacOS</span>
$ brew install tmux
</code></pre>]]></content:encoded></item></channel></rss>